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