Coverage for tests/test_PsfexPsf.py: 16%
175 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-20 09:57 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-20 09:57 +0000
1# This file is part of meas_extensions_psfex.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import math
23import numpy as np
24import unittest
26import lsst.utils.tests
27import lsst.afw.image as afwImage
28import lsst.afw.detection as afwDetection
29import lsst.afw.geom as afwGeom
30import lsst.geom as geom
31import lsst.afw.math as afwMath
32import lsst.afw.table as afwTable
33import lsst.daf.base as dafBase
34import lsst.meas.algorithms as measAlg
35from lsst.meas.base import SingleFrameMeasurementTask
36# register the PSF determiner
37import lsst.meas.extensions.psfex.psfexPsfDeterminer
38assert lsst.meas.extensions.psfex.psfexPsfDeterminer # make pyflakes happy
40try:
41 display
42except NameError:
43 display = False
44else:
45 import lsst.afw.display as afwDisplay
46 afwDisplay.setDefaultMaskTransparency(75)
49def psfVal(ix, iy, x, y, sigma1, sigma2, b):
50 """Return the value at (ix, iy) of a double Gaussian
51 (N(0, sigma1^2) + b*N(0, sigma2^2))/(1 + b)
52 centered at (x, y)
53 """
54 dx, dy = x - ix, y - iy
55 theta = np.radians(30)
56 ab = 1.0/0.75 # axis ratio
57 c, s = np.cos(theta), np.sin(theta)
58 u, v = c*dx - s*dy, s*dx + c*dy
60 return (math.exp(-0.5*(u**2 + (v*ab)**2)/sigma1**2)
61 + b*math.exp(-0.5*(u**2 + (v*ab)**2)/sigma2**2))/(1 + b)
64class SpatialModelPsfTestCase(unittest.TestCase):
65 """A test case for SpatialModelPsf"""
67 def measure(self, footprintSet, exposure):
68 """Measure a set of Footprints, returning a SourceCatalog"""
69 catalog = afwTable.SourceCatalog(self.schema)
70 if display:
71 afwDisplay.Display(frame=0).mtv(exposure, title="Original")
73 footprintSet.makeSources(catalog)
75 self.measureSources.run(catalog, exposure)
76 return catalog
78 def setUp(self):
79 config = SingleFrameMeasurementTask.ConfigClass()
80 config.slots.apFlux = 'base_CircularApertureFlux_12_0'
81 self.schema = afwTable.SourceTable.makeMinimalSchema()
83 self.measureSources = SingleFrameMeasurementTask(self.schema, config=config)
85 width, height = 110, 301
87 self.mi = afwImage.MaskedImageF(geom.ExtentI(width, height))
88 self.mi.set(0)
89 sd = 3 # standard deviation of image
90 self.mi.getVariance().set(sd*sd)
91 self.mi.getMask().addMaskPlane("DETECTED")
93 self.ksize = 31 # size of desired kernel
95 sigma1 = 1.75
96 sigma2 = 2*sigma1
98 self.exposure = afwImage.makeExposure(self.mi)
99 self.exposure.setPsf(measAlg.DoubleGaussianPsf(self.ksize, self.ksize,
100 1.5*sigma1, 1, 0.1))
101 cdMatrix = np.array([1.0, 0.0, 0.0, 1.0])
102 cdMatrix.shape = (2, 2)
103 wcs = afwGeom.makeSkyWcs(crpix=geom.PointD(0, 0),
104 crval=geom.SpherePoint(0.0, 0.0, geom.degrees),
105 cdMatrix=cdMatrix)
106 self.exposure.setWcs(wcs)
108 #
109 # Make a kernel with the exactly correct basis functions.
110 # Useful for debugging
111 #
112 basisKernelList = []
113 for sigma in (sigma1, sigma2):
114 basisKernel = afwMath.AnalyticKernel(self.ksize, self.ksize,
115 afwMath.GaussianFunction2D(sigma, sigma))
116 basisImage = afwImage.ImageD(basisKernel.getDimensions())
117 basisKernel.computeImage(basisImage, True)
118 basisImage /= np.sum(basisImage.getArray())
120 if sigma == sigma1:
121 basisImage0 = basisImage
122 else:
123 basisImage -= basisImage0
125 basisKernelList.append(afwMath.FixedKernel(basisImage))
127 order = 1 # 1 => up to linear
128 spFunc = afwMath.PolynomialFunction2D(order)
130 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc)
131 exactKernel.setSpatialParameters([[1.0, 0, 0],
132 [0.0, 0.5*1e-2, 0.2e-2]])
134 rand = afwMath.Random() # make these tests repeatable by setting seed
136 addNoise = True
138 if addNoise:
139 im = self.mi.getImage()
140 afwMath.randomGaussianImage(im, rand) # N(0, 1)
141 im *= sd # N(0, sd^2)
142 del im
144 xarr, yarr = [], []
146 # NOTE: Warning to those trying to add sources near the edges here:
147 # self.subtractStars() assumes that every source is able to have the
148 # psf subtracted. That's not possible for sources on the edge, so the
149 # chi2 calculation that is asserted on will be off.
150 for x, y in [(20, 20), (60, 20),
151 (30, 35),
152 (50, 50),
153 (20, 90), (70, 160), (25, 265), (75, 275), (85, 30),
154 (50, 120), (70, 80),
155 (60, 210), (20, 210),
156 ]:
157 xarr.append(x)
158 yarr.append(y)
160 for x, y in zip(xarr, yarr):
161 dx = rand.uniform() - 0.5 # random (centered) offsets
162 dy = rand.uniform() - 0.5
164 k = exactKernel.getSpatialFunction(1)(x, y) # functional variation of Kernel ...
165 b = (k*sigma1**2/((1 - k)*sigma2**2)) # ... converted double Gaussian's "b"
167 # flux = 80000 - 20*x - 10*(y/float(height))**2
168 flux = 80000*(1 + 0.1*(rand.uniform() - 0.5))
169 I0 = flux*(1 + b)/(2*np.pi*(sigma1**2 + b*sigma2**2))
170 for iy in range(y - self.ksize//2, y + self.ksize//2 + 1):
171 if iy < 0 or iy >= self.mi.getHeight():
172 continue
174 for ix in range(x - self.ksize//2, x + self.ksize//2 + 1):
175 if ix < 0 or ix >= self.mi.getWidth():
176 continue
178 II = I0*psfVal(ix, iy, x + dx, y + dy, sigma1, sigma2, b)
179 Isample = rand.poisson(II) if addNoise else II
180 self.mi.image[ix, iy, afwImage.LOCAL] += Isample
181 self.mi.variance[ix, iy, afwImage.LOCAL] += II
183 bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height))
184 self.cellSet = afwMath.SpatialCellSet(bbox, 100)
186 self.footprintSet = afwDetection.FootprintSet(self.mi, afwDetection.Threshold(100), "DETECTED")
187 self.catalog = self.measure(self.footprintSet, self.exposure)
189 for source in self.catalog:
190 cand = measAlg.makePsfCandidate(source, self.exposure)
191 self.cellSet.insertCandidate(cand)
193 def tearDown(self):
194 del self.cellSet
195 del self.exposure
196 del self.mi
197 del self.footprintSet
198 del self.catalog
199 del self.schema
200 del self.measureSources
202 def setupDeterminer(self, exposure):
203 """Setup the starSelector and psfDeterminer"""
204 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"]
205 starSelectorConfig = starSelectorClass.ConfigClass()
206 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux"
207 starSelectorConfig.badFlags = ["base_PixelFlags_flag_edge",
208 "base_PixelFlags_flag_interpolatedCenter",
209 "base_PixelFlags_flag_saturatedCenter",
210 "base_PixelFlags_flag_crCenter",
211 ]
212 starSelectorConfig.widthStdAllowed = 0.5 # Set to match when the tolerance of the test was set
214 self.starSelector = starSelectorClass(config=starSelectorConfig)
216 self.makePsfCandidates = measAlg.MakePsfCandidatesTask()
218 psfDeterminerClass = measAlg.psfDeterminerRegistry["psfex"]
219 psfDeterminerConfig = psfDeterminerClass.ConfigClass()
220 width, height = exposure.getMaskedImage().getDimensions()
221 psfDeterminerConfig.sizeCellX = width
222 psfDeterminerConfig.sizeCellY = height//3
223 psfDeterminerConfig.spatialOrder = 1
225 self.psfDeterminer = psfDeterminerClass(psfDeterminerConfig)
227 def subtractStars(self, exposure, catalog, chi_lim=-1):
228 """Subtract the exposure's PSF from all the sources in catalog"""
229 mi, psf = exposure.getMaskedImage(), exposure.getPsf()
231 subtracted = mi.Factory(mi, True)
232 for s in catalog:
233 xc, yc = s.getX(), s.getY()
234 bbox = subtracted.getBBox(afwImage.PARENT)
235 if bbox.contains(geom.PointI(int(xc), int(yc))):
236 measAlg.subtractPsf(psf, subtracted, xc, yc)
237 chi = subtracted.Factory(subtracted, True)
238 var = subtracted.getVariance()
239 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt
240 chi /= var
242 if display:
243 afwDisplay.Display(frame=1).mtv(subtracted, title="Subtracted")
244 afwDisplay.Display(frame=2).mtv(chi, title="Chi")
245 xc, yc = exposure.getWidth()//2, exposure.getHeight()//2
246 afwDisplay.Display(frame=3).mtv(psf.computeImage(geom.Point2D(xc, yc)),
247 title="Psf %.1f,%.1f" % (xc, yc))
249 chi_min, chi_max = np.min(chi.getImage().getArray()), np.max(chi.getImage().getArray())
251 if chi_lim > 0:
252 self.assertGreater(chi_min, -chi_lim)
253 self.assertLess(chi_max, chi_lim)
255 def testPsfexDeterminer(self):
256 """Test the (Psfex) psfDeterminer on subImages"""
258 self.setupDeterminer(self.exposure)
259 metadata = dafBase.PropertyList()
261 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
262 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, exposure=self.exposure).psfCandidates
263 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata)
264 self.exposure.setPsf(psf)
266 # Test how well we can subtract the PSF model
267 self.subtractStars(self.exposure, self.catalog, chi_lim=5.6)
269 # Test PsfexPsf.computeBBox
270 pos = psf.getAveragePosition()
271 self.assertEqual(psf.computeBBox(pos), psf.computeKernelImage(pos).getBBox())
272 self.assertEqual(psf.computeBBox(pos), psf.getKernel(pos).getBBox())
274 def testPsfexDeterminerTooFewStars(self):
275 """Test the (Psfex) psfDeterminer with too few stars."""
276 self.setupDeterminer(self.exposure)
277 metadata = dafBase.PropertyList()
279 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
280 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, exposure=self.exposure).psfCandidates
282 psfCandidateListShort = psfCandidateList[0: 3]
284 with self.assertRaisesRegex(RuntimeError, "Failed to determine"):
285 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateListShort, metadata)
288class TestMemory(lsst.utils.tests.MemoryTestCase):
289 pass
292def setup_module(module):
293 lsst.utils.tests.init()
296if __name__ == "__main__": 296 ↛ 297line 296 didn't jump to line 297, because the condition on line 296 was never true
297 lsst.utils.tests.init()
298 unittest.main()