Coverage for tests/test_doubleShapeletPsfApprox.py: 16%
261 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-26 09:49 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-26 09:49 +0000
1#
2# LSST Data Management System
3#
4# Copyright 2008-2016 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
23import os
24import unittest
25import numpy
26from io import StringIO
27import warnings
29import lsst.utils.tests
30import lsst.afw.detection
31import lsst.afw.image
32import lsst.geom
33import lsst.afw.geom
34import lsst.afw.geom.ellipses
35import lsst.log
36import lsst.utils.logging
37import lsst.meas.modelfit
38import lsst.meas.algorithms
40# Set trace to 0-5 to view debug messages. Level 5 enables all traces.
41lsst.utils.logging.trace_set_at("lsst.meas.modelfit.optimizer.Optimizer", -1)
42lsst.utils.logging.trace_set_at("lsst.meas.modelfit.optimizer.solveTrustRegion", -1)
45class DoubleShapeletPsfApproxTestMixin:
47 Algorithm = lsst.meas.modelfit.DoubleShapeletPsfApproxAlgorithm
49 def initialize(self, psf, ctrl=None, atol=1E-4, **kwds):
50 if not isinstance(psf, lsst.afw.detection.Psf):
51 kernel = lsst.afw.math.FixedKernel(psf)
52 psf = lsst.meas.algorithms.KernelPsf(kernel)
53 self.psf = psf
54 self.atol = atol
55 if ctrl is None:
56 ctrl = lsst.meas.modelfit.DoubleShapeletPsfApproxControl()
57 self.ctrl = ctrl
58 for name, value in kwds.items():
59 setattr(self.ctrl, name, value)
60 self.exposure = lsst.afw.image.ExposureF(1, 1)
61 scale = 5.0e-5 * lsst.geom.degrees
62 wcs = lsst.afw.geom.makeSkyWcs(crpix=lsst.geom.Point2D(0.0, 0.0),
63 crval=lsst.geom.SpherePoint(45, 45, lsst.geom.degrees),
64 cdMatrix=lsst.afw.geom.makeCdMatrix(scale=scale))
65 self.exposure.setWcs(wcs)
66 self.exposure.setPsf(self.psf)
68 def tearDown(self):
69 del self.exposure
70 del self.psf
71 del self.ctrl
72 del self.atol
74 def setupTaskConfig(self, config):
75 config.slots.shape = None
76 config.slots.psfFlux = None
77 config.slots.apFlux = None
78 config.slots.gaussianFlux = None
79 config.slots.modelFlux = None
80 config.slots.calibFlux = None
81 config.doReplaceWithNoise = False
82 config.plugins.names = ["modelfit_DoubleShapeletPsfApprox"]
83 config.plugins["modelfit_DoubleShapeletPsfApprox"].readControl(self.ctrl)
85 def checkBounds(self, msf):
86 """Check that the bounds specified in the control object are met by a MultiShapeletFunction.
88 These requirements must be true after a call to any fit method or measure().
89 """
90 self.assertEqual(len(msf.getComponents()), 2)
91 self.assertEqual(
92 lsst.shapelet.computeSize(self.ctrl.innerOrder),
93 len(msf.getComponents()[0].getCoefficients())
94 )
95 self.assertEqual(
96 lsst.shapelet.computeSize(self.ctrl.outerOrder),
97 len(msf.getComponents()[1].getCoefficients())
98 )
99 self.assertGreater(
100 self.ctrl.maxRadiusBoxFraction * (self.psf.computeKernelImage().getBBox().getArea())**0.5,
101 lsst.afw.geom.ellipses.Axes(msf.getComponents()[0].getEllipse().getCore()).getA()
102 )
103 self.assertGreater(
104 self.ctrl.maxRadiusBoxFraction * (self.psf.computeKernelImage().getBBox().getArea())**0.5,
105 lsst.afw.geom.ellipses.Axes(msf.getComponents()[1].getEllipse().getCore()).getA()
106 )
107 self.assertLess(
108 self.ctrl.minRadius,
109 lsst.afw.geom.ellipses.Axes(msf.getComponents()[0].getEllipse().getCore()).getB()
110 )
111 self.assertLess(
112 self.ctrl.minRadius,
113 lsst.afw.geom.ellipses.Axes(msf.getComponents()[1].getEllipse().getCore()).getB()
114 )
115 self.assertLess(
116 self.ctrl.minRadiusDiff,
117 (msf.getComponents()[1].getEllipse().getCore().getDeterminantRadius()
118 - msf.getComponents()[0].getEllipse().getCore().getDeterminantRadius())
119 )
121 def checkRatios(self, msf):
122 """Check that the ratios specified in the control object are met by a MultiShapeletFunction.
124 These requirements must be true after initializeResult and fitMoments, but will are relaxed
125 in later stages of the fit.
126 """
127 inner = msf.getComponents()[0]
128 outer = msf.getComponents()[1]
129 position = msf.getComponents()[0].getEllipse().getCenter()
130 self.assertFloatsAlmostEqual(position.getX(), msf.getComponents()[1].getEllipse().getCenter().getX())
131 self.assertFloatsAlmostEqual(position.getY(), msf.getComponents()[1].getEllipse().getCenter().getY())
132 self.assertFloatsAlmostEqual(outer.evaluate()(position),
133 inner.evaluate()(position)*self.ctrl.peakRatio)
134 self.assertFloatsAlmostEqual(
135 outer.getEllipse().getCore().getDeterminantRadius(),
136 inner.getEllipse().getCore().getDeterminantRadius() * self.ctrl.radiusRatio
137 )
139 def makeImages(self, msf):
140 """Return an Image of the data and an Image of the model for comparison.
141 """
142 dataImage = self.exposure.getPsf().computeKernelImage()
143 modelImage = dataImage.Factory(dataImage.getBBox())
144 msf.evaluate().addToImage(modelImage)
145 return dataImage, modelImage
147 def checkFitQuality(self, msf):
148 """Check the quality of the fit by comparing to the PSF image.
149 """
150 dataImage, modelImage = self.makeImages(msf)
151 self.assertFloatsAlmostEqual(dataImage.getArray(), modelImage.getArray(), atol=self.atol,
152 plotOnFailure=True)
154 def testSingleFramePlugin(self):
155 """Run the algorithm as a single-frame plugin and check the quality of the fit.
156 """
157 with warnings.catch_warnings():
158 warnings.filterwarnings("ignore", message="ignoreSlotPluginChecks", category=FutureWarning)
159 config = lsst.meas.base.SingleFrameMeasurementTask.ConfigClass(ignoreSlotPluginChecks=True)
160 self.setupTaskConfig(config)
161 config.slots.centroid = "centroid"
162 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
163 centroidKey = lsst.afw.table.Point2DKey.addFields(schema, "centroid", "centroid", "pixel")
164 task = lsst.meas.base.SingleFrameMeasurementTask(config=config, schema=schema)
165 measCat = lsst.afw.table.SourceCatalog(schema)
166 measRecord = measCat.addNew()
167 measRecord.set(centroidKey, lsst.geom.Point2D(0.0, 0.0))
168 task.run(measCat, self.exposure)
169 self.assertFalse(measRecord.get("modelfit_DoubleShapeletPsfApprox_flag"))
170 key = lsst.shapelet.MultiShapeletFunctionKey(schema["modelfit"]["DoubleShapeletPsfApprox"])
171 msf = measRecord.get(key)
172 self.checkBounds(msf)
173 self.checkFitQuality(msf)
175 def testForcedPlugin(self):
176 """Run the algorithm as a forced plugin and check the quality of the fit.
177 """
178 config = lsst.meas.base.ForcedMeasurementTask.ConfigClass()
179 config.copyColumns = {"id": "objectId", "parent": "parentObjectId"}
180 self.setupTaskConfig(config)
181 config.slots.centroid = "base_TransformedCentroid"
182 config.plugins.names |= ["base_TransformedCentroid"]
183 refSchema = lsst.afw.table.SourceTable.makeMinimalSchema()
184 refCentroidKey = lsst.afw.table.Point2DKey.addFields(refSchema, "centroid", "centroid", "pixel")
185 refSchema.getAliasMap().set("slot_Centroid", "centroid")
186 refCat = lsst.afw.table.SourceCatalog(refSchema)
187 refRecord = refCat.addNew()
188 refRecord.set(refCentroidKey, lsst.geom.Point2D(0.0, 0.0))
189 refWcs = self.exposure.getWcs() # same as measurement Wcs
190 task = lsst.meas.base.ForcedMeasurementTask(config=config, refSchema=refSchema)
191 measCat = task.generateMeasCat(self.exposure, refCat, refWcs)
192 task.run(measCat, self.exposure, refCat, refWcs)
193 measRecord = measCat[0]
194 self.assertFalse(measRecord.get("modelfit_DoubleShapeletPsfApprox_flag"))
195 measSchema = measCat.schema
196 key = lsst.shapelet.MultiShapeletFunctionKey(measSchema["modelfit"]["DoubleShapeletPsfApprox"])
197 msf = measRecord.get(key)
198 self.checkBounds(msf)
199 self.checkFitQuality(msf)
201 def testInitializeResult(self):
202 """Test that initializeResult() returns a unit-flux, unit-circle MultiShapeletFunction
203 with the right peakRatio and radiusRatio.
204 """
205 msf = self.Algorithm.initializeResult(self.ctrl)
206 self.assertFloatsAlmostEqual(msf.evaluate().integrate(), 1.0)
207 moments = msf.evaluate().computeMoments()
208 axes = lsst.afw.geom.ellipses.Axes(moments.getCore())
209 self.assertFloatsAlmostEqual(moments.getCenter().getX(), 0.0)
210 self.assertFloatsAlmostEqual(moments.getCenter().getY(), 0.0)
211 self.assertFloatsAlmostEqual(axes.getA(), 1.0)
212 self.assertFloatsAlmostEqual(axes.getB(), 1.0)
213 self.assertEqual(len(msf.getComponents()), 2)
214 self.checkRatios(msf)
216 def testFitMoments(self):
217 """Test that fitMoments() preserves peakRatio and radiusRatio while setting moments
218 correctly.
219 """
220 MOMENTS_RTOL = 1E-13
221 image = self.psf.computeKernelImage()
222 array = image.getArray()
223 bbox = image.getBBox()
224 x, y = numpy.meshgrid(
225 numpy.arange(bbox.getBeginX(), bbox.getEndX()),
226 numpy.arange(bbox.getBeginY(), bbox.getEndY())
227 )
228 msf = self.Algorithm.initializeResult(self.ctrl)
229 self.Algorithm.fitMoments(msf, self.ctrl, image)
230 self.assertFloatsAlmostEqual(msf.evaluate().integrate(), array.sum(), rtol=MOMENTS_RTOL)
231 moments = msf.evaluate().computeMoments()
232 q = lsst.afw.geom.ellipses.Quadrupole(moments.getCore())
233 cx = (x*array).sum()/array.sum()
234 cy = (y*array).sum()/array.sum()
235 self.assertFloatsAlmostEqual(moments.getCenter().getX(), cx, rtol=MOMENTS_RTOL)
236 self.assertFloatsAlmostEqual(moments.getCenter().getY(), cy, rtol=MOMENTS_RTOL)
237 self.assertFloatsAlmostEqual(q.getIxx(), ((x - cx)**2 * array).sum()/array.sum(), rtol=MOMENTS_RTOL)
238 self.assertFloatsAlmostEqual(q.getIyy(), ((y - cy)**2 * array).sum()/array.sum(), rtol=MOMENTS_RTOL)
239 self.assertFloatsAlmostEqual(q.getIxy(), ((x - cx)*(y - cy)*array).sum()/array.sum(),
240 rtol=MOMENTS_RTOL)
241 self.assertEqual(len(msf.getComponents()), 2)
242 self.checkRatios(msf)
243 self.checkBounds(msf)
245 def testObjective(self):
246 """Test that model evaluation agrees with derivative evaluation in the objective object.
247 """
248 image = self.psf.computeKernelImage()
249 msf = self.Algorithm.initializeResult(self.ctrl)
250 self.Algorithm.fitMoments(msf, self.ctrl, image)
251 moments = msf.evaluate().computeMoments()
252 r0 = moments.getCore().getDeterminantRadius()
253 objective = self.Algorithm.makeObjective(moments, self.ctrl, image)
254 image, model = self.makeImages(msf)
255 parameters = numpy.zeros(4, dtype=float)
256 parameters[0] = msf.getComponents()[0].getCoefficients()[0]
257 parameters[1] = msf.getComponents()[1].getCoefficients()[0]
258 parameters[2] = msf.getComponents()[0].getEllipse().getCore().getDeterminantRadius() / r0
259 parameters[3] = msf.getComponents()[1].getEllipse().getCore().getDeterminantRadius() / r0
260 residuals = numpy.zeros(image.getArray().size, dtype=float)
261 objective.computeResiduals(parameters, residuals)
262 self.assertFloatsAlmostEqual(
263 residuals.reshape(image.getHeight(), image.getWidth()),
264 image.getArray() - model.getArray()
265 )
266 step = 1E-6
267 derivatives = numpy.zeros((parameters.size, residuals.size), dtype=float).transpose()
268 objective.differentiateResiduals(parameters, derivatives)
269 for i in range(parameters.size):
270 original = parameters[i]
271 r1 = numpy.zeros(residuals.size, dtype=float)
272 r2 = numpy.zeros(residuals.size, dtype=float)
273 parameters[i] = original + step
274 objective.computeResiduals(parameters, r1)
275 parameters[i] = original - step
276 objective.computeResiduals(parameters, r2)
277 parameters[i] = original
278 d = (r1 - r2)/(2.0*step)
279 self.assertFloatsAlmostEqual(
280 d.reshape(image.getHeight(), image.getWidth()),
281 derivatives[:, i].reshape(image.getHeight(), image.getWidth()),
282 atol=1E-11
283 )
285 def testFitProfile(self):
286 """Test that fitProfile() does not modify the ellipticity, that it improves the fit, and
287 that small perturbations to the zeroth-order amplitudes and radii do not improve the fit.
288 """
289 image = self.psf.computeKernelImage()
290 msf = self.Algorithm.initializeResult(self.ctrl)
291 self.Algorithm.fitMoments(msf, self.ctrl, image)
292 prev = lsst.shapelet.MultiShapeletFunction(msf)
293 self.Algorithm.fitProfile(msf, self.ctrl, image)
295 def getEllipticity(m, c):
296 s = lsst.afw.geom.ellipses.SeparableDistortionDeterminantRadius(
297 m.getComponents()[c].getEllipse().getCore()
298 )
299 return numpy.array([s.getE1(), s.getE2()])
300 self.assertFloatsAlmostEqual(getEllipticity(prev, 0), getEllipticity(msf, 0), rtol=1E-13)
301 self.assertFloatsAlmostEqual(getEllipticity(prev, 1), getEllipticity(msf, 1), rtol=1E-13)
303 def computeChiSq(m):
304 data, model = self.makeImages(m)
305 return numpy.sum((data.getArray() - model.getArray())**2)
306 bestChiSq = computeChiSq(msf)
307 self.assertLessEqual(bestChiSq, computeChiSq(prev))
308 step = 1E-4
309 for component in msf.getComponents():
310 # 0th-order amplitude perturbation
311 original = component.getCoefficients()[0]
312 component.getCoefficients()[0] = original + step
313 self.assertLessEqual(bestChiSq, computeChiSq(msf))
314 component.getCoefficients()[0] = original - step
315 self.assertLessEqual(bestChiSq, computeChiSq(msf))
316 component.getCoefficients()[0] = original
317 # Radius perturbation
318 original = component.getEllipse()
319 component.getEllipse().getCore().scale(1.0 + step)
320 self.assertLessEqual(bestChiSq, computeChiSq(msf))
321 component.setEllipse(original)
322 component.getEllipse().getCore().scale(1.0 - step)
323 self.assertLessEqual(bestChiSq, computeChiSq(msf))
324 component.setEllipse(original)
326 def testFitShapelets(self):
327 """Test that fitShapelets() does not modify the zeroth order coefficients or ellipse,
328 that it improves the fit, and that small perturbations to the higher-order coefficients
329 do not improve the fit.
330 """
331 image = self.psf.computeKernelImage()
332 msf = self.Algorithm.initializeResult(self.ctrl)
333 self.Algorithm.fitMoments(msf, self.ctrl, image)
334 self.Algorithm.fitProfile(msf, self.ctrl, image)
335 prev = lsst.shapelet.MultiShapeletFunction(msf)
336 self.Algorithm.fitShapelets(msf, self.ctrl, image)
337 self.assertFloatsAlmostEqual(
338 prev.getComponents()[0].getEllipse().getParameterVector(),
339 msf.getComponents()[0].getEllipse().getParameterVector()
340 )
341 self.assertFloatsAlmostEqual(
342 prev.getComponents()[1].getEllipse().getParameterVector(),
343 msf.getComponents()[1].getEllipse().getParameterVector()
344 )
346 def computeChiSq(m):
347 data, model = self.makeImages(m)
348 return numpy.sum((data.getArray() - model.getArray())**2)
349 bestChiSq = computeChiSq(msf)
350 self.assertLessEqual(bestChiSq, computeChiSq(prev))
351 step = 1E-4
352 for component in msf.getComponents():
353 for i in range(1, len(component.getCoefficients())):
354 original = component.getCoefficients()[i]
355 component.getCoefficients()[i] = original + step
356 self.assertLessEqual(bestChiSq, computeChiSq(msf))
357 component.getCoefficients()[i] = original - step
358 self.assertLessEqual(bestChiSq, computeChiSq(msf))
359 component.getCoefficients()[i] = original
361 def testSingleFrameConfigIO(self):
362 config1 = lsst.meas.base.SingleFrameMeasurementTask.ConfigClass()
363 config2 = lsst.meas.base.SingleFrameMeasurementTask.ConfigClass()
364 self.setupTaskConfig(config1)
365 stream = StringIO()
366 config1.saveToStream(stream)
367 config2.loadFromStream(stream.getvalue())
368 self.assertEqual(config1, config2)
371class SingleGaussianTestCase(DoubleShapeletPsfApproxTestMixin, lsst.utils.tests.TestCase):
373 def setUp(self):
374 numpy.random.seed(500)
375 DoubleShapeletPsfApproxTestMixin.initialize(
376 self, psf=lsst.afw.detection.GaussianPsf(25, 25, 2.0),
377 innerOrder=0, outerOrder=0, peakRatio=0.0
378 )
381class HigherOrderTestCase0(DoubleShapeletPsfApproxTestMixin, lsst.utils.tests.TestCase):
383 def setUp(self):
384 numpy.random.seed(500)
385 image = lsst.afw.image.ImageD(os.path.join(os.path.dirname(os.path.realpath(__file__)),
386 "data", "psfs/great3-0.fits"))
387 DoubleShapeletPsfApproxTestMixin.initialize(
388 self, psf=image,
389 innerOrder=3, outerOrder=2,
390 atol=0.0005
391 )
394class HigherOrderTestCase1(DoubleShapeletPsfApproxTestMixin, lsst.utils.tests.TestCase):
396 def setUp(self):
397 numpy.random.seed(500)
398 image = lsst.afw.image.ImageD(os.path.join(os.path.dirname(os.path.realpath(__file__)),
399 "data", "psfs/great3-1.fits"))
400 DoubleShapeletPsfApproxTestMixin.initialize(
401 self, psf=image,
402 innerOrder=2, outerOrder=1,
403 atol=0.002
404 )
407class TestMemory(lsst.utils.tests.MemoryTestCase):
408 pass
411def setup_module(module):
412 lsst.utils.tests.init()
415if __name__ == "__main__": 415 ↛ 416line 415 didn't jump to line 416, because the condition on line 415 was never true
416 lsst.utils.tests.init()
417 unittest.main()