Coverage for tests/test_psfDetermination.py: 14%
280 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-12 03:05 -0700
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-12 03:05 -0700
1# This file is part of meas_algorithms.
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.geom
27from lsst.afw.cameraGeom.testUtils import DetectorWrapper
28import lsst.afw.detection as afwDetection
29import lsst.afw.geom as afwGeom
30import lsst.afw.image as afwImage
31import lsst.afw.math as afwMath
32import lsst.afw.table as afwTable
33import lsst.daf.base as dafBase
34import lsst.afw.display as afwDisplay
35from lsst.log import Log
36import lsst.meas.algorithms as measAlg
37from lsst.meas.algorithms.pcaPsfDeterminer import numCandidatesToReject
38from lsst.meas.algorithms.utils import showPsfMosaic, showPsf
39import lsst.meas.base as measBase
40import lsst.utils.tests
42try:
43 type(display)
44except NameError:
45 display = False
46else:
47 afwDisplay.setDefaultMaskTransparency(75)
49# Change the level to Log.DEBUG or Log.TRACE to see debug messages
50Log.getLogger("measurement").setLevel(Log.INFO)
51Log.getLogger("psfDeterminer").setLevel(Log.TRACE)
54def psfVal(ix, iy, x, y, sigma1, sigma2, b):
55 """Return the value at (ix, iy) of a double Gaussian
56 (N(0, sigma1^2) + b*N(0, sigma2^2))/(1 + b)
57 centered at (x, y)
58 """
59 return (math.exp(-0.5*((ix - x)**2 + (iy - y)**2)/sigma1**2)
60 + b*math.exp(-0.5*((ix - x)**2 + (iy - y)**2)/sigma2**2))/(1 + b)
63class SpatialModelPsfTestCase(lsst.utils.tests.TestCase):
64 """A test case for SpatialModelPsf"""
66 def measure(self, footprintSet, exposure):
67 """Measure a set of Footprints, returning a SourceCatalog."""
68 table = afwTable.SourceCatalog(self.schema)
69 footprintSet.makeSources(table)
71 # Then run the default SFM task. Results not checked
72 self.measureTask.run(table, exposure)
74 if display:
75 afwDisplay.Display(frame=1).mtv(exposure, title=self._testMethodName + ": image")
77 return table
79 def setUp(self):
81 self.schema = afwTable.SourceTable.makeMinimalSchema()
82 config = measBase.SingleFrameMeasurementConfig()
83 config.algorithms.names = ["base_PixelFlags",
84 "base_SdssCentroid",
85 "base_GaussianFlux",
86 "base_SdssShape",
87 "base_CircularApertureFlux",
88 "base_PsfFlux",
89 ]
90 config.algorithms["base_CircularApertureFlux"].radii = [3.0]
91 config.slots.centroid = "base_SdssCentroid"
92 config.slots.psfFlux = "base_PsfFlux"
93 config.slots.apFlux = "base_CircularApertureFlux_3_0"
94 config.slots.modelFlux = None
95 config.slots.gaussianFlux = None
96 config.slots.calibFlux = None
97 config.slots.shape = "base_SdssShape"
99 self.measureTask = measBase.SingleFrameMeasurementTask(self.schema, config=config)
101 width, height = 110, 301
103 self.mi = afwImage.MaskedImageF(lsst.geom.ExtentI(width, height))
104 self.mi.set(0)
105 sd = 3 # standard deviation of image
106 self.mi.getVariance().set(sd*sd)
107 self.mi.getMask().addMaskPlane("DETECTED")
109 self.FWHM = 5
110 self.ksize = 31 # size of desired kernel
112 sigma1 = 1.75
113 sigma2 = 2*sigma1
115 self.exposure = afwImage.makeExposure(self.mi)
116 self.exposure.setPsf(measAlg.DoubleGaussianPsf(self.ksize, self.ksize,
117 1.5*sigma1, 1, 0.1))
118 self.exposure.setDetector(DetectorWrapper().detector)
120 #
121 # Make a kernel with the exactly correct basis functions. Useful for debugging
122 #
123 basisKernelList = []
124 for sigma in (sigma1, sigma2):
125 basisKernel = afwMath.AnalyticKernel(self.ksize, self.ksize,
126 afwMath.GaussianFunction2D(sigma, sigma))
127 basisImage = afwImage.ImageD(basisKernel.getDimensions())
128 basisKernel.computeImage(basisImage, True)
129 basisImage /= np.sum(basisImage.getArray())
131 if sigma == sigma1:
132 basisImage0 = basisImage
133 else:
134 basisImage -= basisImage0
136 basisKernelList.append(afwMath.FixedKernel(basisImage))
138 order = 1 # 1 => up to linear
139 spFunc = afwMath.PolynomialFunction2D(order)
141 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc)
142 exactKernel.setSpatialParameters([[1.0, 0, 0],
143 [0.0, 0.5*1e-2, 0.2e-2]])
144 self.exactPsf = measAlg.PcaPsf(exactKernel)
146 rand = afwMath.Random() # make these tests repeatable by setting seed
148 addNoise = True
150 if addNoise:
151 im = self.mi.getImage()
152 afwMath.randomGaussianImage(im, rand) # N(0, 1)
153 im *= sd # N(0, sd^2)
154 del im
156 xarr, yarr = [], []
158 for x, y in [(20, 20), (60, 20),
159 (30, 35),
160 (50, 50),
161 (20, 90), (70, 160), (25, 265), (75, 275), (85, 30),
162 (50, 120), (70, 80),
163 (60, 210), (20, 210),
164 ]:
165 xarr.append(x)
166 yarr.append(y)
168 for x, y in zip(xarr, yarr):
169 dx = rand.uniform() - 0.5 # random (centered) offsets
170 dy = rand.uniform() - 0.5
172 k = exactKernel.getSpatialFunction(1)(x, y) # functional variation of Kernel ...
173 b = (k*sigma1**2/((1 - k)*sigma2**2)) # ... converted double Gaussian's "b"
175 # flux = 80000 - 20*x - 10*(y/float(height))**2
176 flux = 80000*(1 + 0.1*(rand.uniform() - 0.5))
177 I0 = flux*(1 + b)/(2*np.pi*(sigma1**2 + b*sigma2**2))
178 for iy in range(y - self.ksize//2, y + self.ksize//2 + 1):
179 if iy < 0 or iy >= self.mi.getHeight():
180 continue
182 for ix in range(x - self.ksize//2, x + self.ksize//2 + 1):
183 if ix < 0 or ix >= self.mi.getWidth():
184 continue
186 intensity = I0*psfVal(ix, iy, x + dx, y + dy, sigma1, sigma2, b)
187 Isample = rand.poisson(intensity) if addNoise else intensity
188 self.mi.image[ix, iy, afwImage.LOCAL] += Isample
189 self.mi.variance[ix, iy, afwImage.LOCAL] += intensity
190 #
191 bbox = lsst.geom.BoxI(lsst.geom.PointI(0, 0), lsst.geom.ExtentI(width, height))
192 self.cellSet = afwMath.SpatialCellSet(bbox, 100)
194 self.footprintSet = afwDetection.FootprintSet(self.mi, afwDetection.Threshold(100), "DETECTED")
195 self.catalog = self.measure(self.footprintSet, self.exposure)
197 for source in self.catalog:
198 try:
199 cand = measAlg.makePsfCandidate(source, self.exposure)
200 self.cellSet.insertCandidate(cand)
202 except Exception as e:
203 print(e)
204 continue
206 def tearDown(self):
207 del self.cellSet
208 del self.exposure
209 del self.mi
210 del self.exactPsf
211 del self.footprintSet
212 del self.catalog
213 del self.schema
214 del self.measureTask
216 def setupDeterminer(self, exposure=None, nEigenComponents=2, starSelectorAlg="objectSize"):
217 """Setup the starSelector and psfDeterminer."""
218 if exposure is None:
219 exposure = self.exposure
221 starSelectorClass = measAlg.sourceSelectorRegistry[starSelectorAlg]
222 starSelectorConfig = starSelectorClass.ConfigClass()
224 if starSelectorAlg == "objectSize":
225 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux"
226 starSelectorConfig.badFlags = ["base_PixelFlags_flag_edge",
227 "base_PixelFlags_flag_interpolatedCenter",
228 "base_PixelFlags_flag_saturatedCenter",
229 "base_PixelFlags_flag_crCenter",
230 ]
231 starSelectorConfig.widthStdAllowed = 0.5
233 self.starSelector = starSelectorClass(config=starSelectorConfig)
235 self.makePsfCandidates = measAlg.MakePsfCandidatesTask()
237 psfDeterminerTask = measAlg.psfDeterminerRegistry["pca"]
238 psfDeterminerConfig = psfDeterminerTask.ConfigClass()
239 width, height = exposure.getMaskedImage().getDimensions()
240 psfDeterminerConfig.sizeCellX = width
241 psfDeterminerConfig.sizeCellY = height//3
242 psfDeterminerConfig.nEigenComponents = nEigenComponents
243 psfDeterminerConfig.spatialOrder = 1
244 psfDeterminerConfig.kernelSizeMin = 31
245 psfDeterminerConfig.nStarPerCell = 0
246 psfDeterminerConfig.nStarPerCellSpatialFit = 0 # unlimited
247 self.psfDeterminer = psfDeterminerTask(psfDeterminerConfig)
249 def subtractStars(self, exposure, catalog, chi_lim=-1):
250 """Subtract the exposure's PSF from all the sources in catalog."""
251 mi, psf = exposure.getMaskedImage(), exposure.getPsf()
253 subtracted = mi.Factory(mi, True)
255 for s in catalog:
256 xc, yc = s.getX(), s.getY()
257 bbox = subtracted.getBBox()
258 if bbox.contains(lsst.geom.PointI(int(xc), int(yc))):
259 measAlg.subtractPsf(psf, subtracted, xc, yc)
261 chi = subtracted.Factory(subtracted, True)
262 var = subtracted.getVariance()
263 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt
264 chi /= var
266 if display:
267 afwDisplay.Display(frame=0).mtv(subtracted, title=self._testMethodName + ": Subtracted")
268 afwDisplay.Display(frame=2).mtv(chi, title=self._testMethodName + ": Chi")
269 afwDisplay.Display(frame=3).mtv(psf.computeImage(lsst.geom.Point2D(xc, yc)),
270 title=self._testMethodName + ": Psf")
271 afwDisplay.Display(frame=4).mtv(mi, title=self._testMethodName + ": orig")
272 kern = psf.getKernel()
273 kimg = afwImage.ImageD(kern.getWidth(), kern.getHeight())
274 kern.computeImage(kimg, True, xc, yc)
275 afwDisplay.Display(frame=5).mtv(kimg, title=self._testMethodName + ": kernel")
277 chi_min, chi_max = np.min(chi.getImage().getArray()), np.max(chi.getImage().getArray())
278 if False:
279 print(chi_min, chi_max)
281 if chi_lim > 0:
282 self.assertGreater(chi_min, -chi_lim)
283 self.assertLess(chi_max, chi_lim)
285 def testPsfDeterminerObjectSize(self):
286 self._testPsfDeterminer("objectSize")
288 def _testPsfDeterminer(self, starSelectorAlg):
289 self.setupDeterminer(starSelectorAlg=starSelectorAlg)
290 metadata = dafBase.PropertyList()
292 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
293 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, self.exposure).psfCandidates
295 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata)
296 self.exposure.setPsf(psf)
298 chi_lim = 5.0
299 self.subtractStars(self.exposure, self.catalog, chi_lim)
301 def testPsfDeterminerSubimageObjectSizeStarSelector(self):
302 """Test the (PCA) psfDeterminer on subImages."""
303 w, h = self.exposure.getDimensions()
304 x0, y0 = int(0.35*w), int(0.45*h)
305 bbox = lsst.geom.BoxI(lsst.geom.PointI(x0, y0), lsst.geom.ExtentI(w-x0, h-y0))
306 subExp = self.exposure.Factory(self.exposure, bbox, afwImage.LOCAL)
308 self.setupDeterminer(subExp, starSelectorAlg="objectSize")
309 metadata = dafBase.PropertyList()
310 #
311 # Only keep the sources that lie within the subregion (avoiding lots of log messages)
312 #
314 def trimCatalogToImage(exp, catalog):
315 trimmedCatalog = afwTable.SourceCatalog(catalog.table.clone())
316 for s in catalog:
317 if exp.getBBox().contains(lsst.geom.PointI(s.getCentroid())):
318 trimmedCatalog.append(trimmedCatalog.table.copyRecord(s))
320 return trimmedCatalog
322 stars = self.starSelector.run(trimCatalogToImage(subExp, self.catalog), exposure=subExp)
323 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, subExp).psfCandidates
325 psf, cellSet = self.psfDeterminer.determinePsf(subExp, psfCandidateList, metadata)
326 subExp.setPsf(psf)
328 # Test how well we can subtract the PSF model. N.b. using self.exposure is an extrapolation
329 for exp, chi_lim in [(subExp, 4.5),
330 (self.exposure.Factory(self.exposure,
331 lsst.geom.BoxI(lsst.geom.PointI(0, 100),
332 (lsst.geom.PointI(w-1, h-1))),
333 afwImage.LOCAL), 7.5),
334 (self.exposure, 19),
335 ]:
336 cat = trimCatalogToImage(exp, self.catalog)
337 exp.setPsf(psf)
338 self.subtractStars(exp, cat, chi_lim)
340 def testPsfDeterminerNEigenObjectSizeStarSelector(self):
341 """Test the (PCA) psfDeterminer when you ask for more components than acceptable stars."""
342 self.setupDeterminer(nEigenComponents=3, starSelectorAlg="objectSize")
343 metadata = dafBase.PropertyList()
345 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
346 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, self.exposure).psfCandidates
348 psfCandidateList, nEigen = psfCandidateList[0:4], 2 # only enough stars for 2 eigen-components
349 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata)
351 self.assertEqual(psf.getKernel().getNKernelParameters(), nEigen)
353 def testCandidateList(self):
354 self.assertFalse(self.cellSet.getCellList()[0].empty())
355 self.assertTrue(self.cellSet.getCellList()[1].empty())
356 self.assertFalse(self.cellSet.getCellList()[2].empty())
357 self.assertTrue(self.cellSet.getCellList()[3].empty())
359 stamps = []
360 for cell in self.cellSet.getCellList():
361 for cand in cell:
362 cand = cell[0]
363 width, height = 29, 25
364 cand.setWidth(width)
365 cand.setHeight(height)
367 im = cand.getMaskedImage()
368 stamps.append(im)
370 self.assertEqual(im.getWidth(), width)
371 self.assertEqual(im.getHeight(), height)
373 if False and display:
374 mos = afwDisplay.utils.Mosaic()
375 mos.makeMosaic(stamps, frame=2)
377 def testRejectBlends(self):
378 """Test the PcaPsfDeterminerTask blend removal."""
379 """
380 We give it a single blended source, asking it to remove blends,
381 and check that it barfs in the expected way.
382 """
384 psfDeterminerClass = measAlg.psfDeterminerRegistry["pca"]
385 config = psfDeterminerClass.ConfigClass()
386 config.doRejectBlends = True
387 psfDeterminer = psfDeterminerClass(config=config)
389 schema = afwTable.SourceTable.makeMinimalSchema()
390 # Use The single frame measurement task to populate the schema with standard keys
391 measBase.SingleFrameMeasurementTask(schema)
392 catalog = afwTable.SourceCatalog(schema)
393 source = catalog.addNew()
395 # Make the source blended, with necessary information to calculate pca
396 spanShift = lsst.geom.Point2I(54, 123)
397 spans = afwGeom.SpanSet.fromShape(6, offset=spanShift)
398 foot = afwDetection.Footprint(spans, self.exposure.getBBox())
399 foot.addPeak(45, 123, 6)
400 foot.addPeak(47, 126, 5)
401 source.setFootprint(foot)
402 centerKey = afwTable.Point2DKey(source.schema['slot_Centroid'])
403 shapeKey = afwTable.QuadrupoleKey(schema['slot_Shape'])
404 source.set(centerKey, lsst.geom.Point2D(46, 124))
405 source.set(shapeKey, afwGeom.Quadrupole(1.1, 2.2, 1))
407 candidates = [measAlg.makePsfCandidate(source, self.exposure)]
408 metadata = dafBase.PropertyList()
410 with self.assertRaises(RuntimeError) as cm:
411 psfDeterminer.determinePsf(self.exposure, candidates, metadata)
412 self.assertEqual(str(cm.exception), "All PSF candidates removed as blends")
414 def testShowPsfMosaic(self):
415 """ Test that the showPsfMosaic function works.
417 This function is usually called without display=None, which would activate ds9
418 """
419 testDisplay = display if display else afwDisplay.getDisplay(backend="virtualDevice")
420 mos = showPsfMosaic(self.exposure, showEllipticity=True, showFwhm=True, display=testDisplay)
421 self.assertTrue(len(mos.images) > 0)
423 def testShowPsf(self):
424 """ Test that the showPsfMosaic function works.
426 This function is usually called without display=None, which would activate ds9
427 """
429 # Measure PSF so we have a real PSF to work with
430 self.setupDeterminer()
431 metadata = dafBase.PropertyList()
432 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
433 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, self.exposure).psfCandidates
434 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata)
435 testDisplay = display if display else afwDisplay.getDisplay(backend="virtualDevice")
436 mos = showPsf(psf, display=testDisplay)
437 self.assertTrue(len(mos.images) > 0)
440class PsfCandidateTestCase(lsst.utils.tests.TestCase):
441 def testNumToReject(self):
442 """Reject the correct number of PSF candidates on each iteration"""
443 # Numerical values correspond to the problem case identified in
444 # DM-8030.
446 numBadCandidates = 5
447 totalIter = 3
449 for numIter, value in [(0, 1), (1, 3), (2, 5)]:
450 self.assertEqual(numCandidatesToReject(numBadCandidates, numIter,
451 totalIter), value)
454class TestMemory(lsst.utils.tests.MemoryTestCase):
455 pass
458def setup_module(module):
459 lsst.utils.tests.init()
462if __name__ == "__main__": 462 ↛ 463line 462 didn't jump to line 463, because the condition on line 462 was never true
463 lsst.utils.tests.init()
464 unittest.main()