Coverage for tests/test_PsfexPsf.py: 15%
185 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-29 10:17 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-29 10:17 +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, fluxField=None):
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
224 if fluxField is not None:
225 psfDeterminerConfig.photometricFluxField = fluxField
227 self.psfDeterminer = psfDeterminerClass(psfDeterminerConfig)
229 def subtractStars(self, exposure, catalog, chi_lim=-1):
230 """Subtract the exposure's PSF from all the sources in catalog"""
231 mi, psf = exposure.getMaskedImage(), exposure.getPsf()
233 subtracted = mi.Factory(mi, True)
234 for s in catalog:
235 xc, yc = s.getX(), s.getY()
236 bbox = subtracted.getBBox(afwImage.PARENT)
237 if bbox.contains(geom.PointI(int(xc), int(yc))):
238 measAlg.subtractPsf(psf, subtracted, xc, yc)
239 chi = subtracted.Factory(subtracted, True)
240 var = subtracted.getVariance()
241 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt
242 chi /= var
244 if display:
245 afwDisplay.Display(frame=1).mtv(subtracted, title="Subtracted")
246 afwDisplay.Display(frame=2).mtv(chi, title="Chi")
247 xc, yc = exposure.getWidth()//2, exposure.getHeight()//2
248 afwDisplay.Display(frame=3).mtv(psf.computeImage(geom.Point2D(xc, yc)),
249 title="Psf %.1f,%.1f" % (xc, yc))
251 chi_min, chi_max = np.min(chi.getImage().getArray()), np.max(chi.getImage().getArray())
253 if chi_lim > 0:
254 self.assertGreater(chi_min, -chi_lim)
255 self.assertLess(chi_max, chi_lim)
257 def testPsfexDeterminer(self):
258 """Test the (Psfex) psfDeterminer on subImages"""
260 self.setupDeterminer(self.exposure)
261 metadata = dafBase.PropertyList()
263 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
264 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, exposure=self.exposure).psfCandidates
265 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata)
266 self.exposure.setPsf(psf)
268 # Test how well we can subtract the PSF model
269 self.subtractStars(self.exposure, self.catalog, chi_lim=5.6)
271 # Test PsfexPsf.computeBBox
272 pos = psf.getAveragePosition()
273 self.assertEqual(psf.computeBBox(pos), psf.computeKernelImage(pos).getBBox())
274 self.assertEqual(psf.computeBBox(pos), psf.getKernel(pos).getBBox())
276 def testPsfexDeterminerTooFewStars(self):
277 """Test the (Psfex) psfDeterminer with too few stars."""
278 self.setupDeterminer(self.exposure)
279 metadata = dafBase.PropertyList()
281 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
282 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, exposure=self.exposure).psfCandidates
284 psfCandidateListShort = psfCandidateList[0: 3]
286 with self.assertRaisesRegex(RuntimeError, "Failed to determine"):
287 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateListShort, metadata)
289 def testPsfDeterminerChangeFluxField(self):
290 """Test the psfDeterminer with a different flux normalization field."""
291 # We test here with an aperture that we would be unlikely to ever use
292 # as a default.
293 self.setupDeterminer(self.exposure, fluxField="base_CircularApertureFlux_6_0_instFlux")
294 metadata = dafBase.PropertyList()
296 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
297 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, exposure=self.exposure).psfCandidates
298 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata)
299 self.exposure.setPsf(psf)
301 # Test how well we can subtract the PSF model
302 self.subtractStars(self.exposure, self.catalog, chi_lim=5.6)
305class TestMemory(lsst.utils.tests.MemoryTestCase):
306 pass
309def setup_module(module):
310 lsst.utils.tests.init()
313if __name__ == "__main__": 313 ↛ 314line 313 didn't jump to line 314, because the condition on line 313 was never true
314 lsst.utils.tests.init()
315 unittest.main()