Coverage for tests / test_psfFitter.py: 15%
172 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 09:16 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 09:16 +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 unittest
24import os
25import glob
26import numpy
28import lsst.utils.tests
29import lsst.shapelet
30import lsst.geom
31import lsst.utils.logging
32import lsst.meas.modelfit
33import lsst.meas.base
35numpy.random.seed(500)
37# Set trace to 0-5 to view debug messages. Level 5 enables all traces.
38lsst.utils.logging.trace_set_at("lsst.meas.modelfit.optimizer.Optimizer", -1)
39lsst.utils.logging.trace_set_at("lsst.meas.modelfit.optimizer.solveTrustRegion", -1)
41ELLIPSE_PARAMETER_NAMES = ["eta1", "eta2", "logR", "x", "y"]
42DATA_DIR = os.path.join(os.environ["MEAS_MODELFIT_DIR"], "tests", "data")
45def computeMoments(image):
46 """Helper function to compute moments of a postage stamp about its origin."""
47 maskedImage = lsst.afw.image.MaskedImageD(image)
48 result = lsst.meas.base.SdssShapeAlgorithm.computeAdaptiveMoments(
49 maskedImage,
50 lsst.geom.Point2D(0.0, 0.0)
51 )
52 return result.getShape()
55class GeneralPsfFitterTestCase(lsst.utils.tests.TestCase):
57 def setUp(self):
58 self.configs = {}
59 self.configs['fixed'] = lsst.meas.modelfit.GeneralPsfFitterConfig()
60 self.configs['fixed'].primary.ellipticityPriorSigma = 0.0
61 self.configs['fixed'].primary.radiusPriorSigma = 0.0
62 self.configs['fixed'].primary.positionPriorSigma = 0.0
63 self.configs['fixed'].wings.ellipticityPriorSigma = 0.0
64 self.configs['fixed'].wings.radiusPriorSigma = 0.0
65 self.configs['fixed'].wings.positionPriorSigma = 0.0
66 self.configs['ellipse'] = lsst.meas.modelfit.GeneralPsfFitterConfig()
67 self.configs['ellipse'].primary.positionPriorSigma = 0.0
68 self.configs['ellipse'].wings.positionPriorSigma = 0.0
69 self.configs['full'] = lsst.meas.modelfit.GeneralPsfFitterConfig()
70 self.configs['full'].inner.order = 0
71 self.configs['full'].primary.order = 4
72 self.configs['full'].wings.order = 4
73 self.configs['full'].outer.order = 0
74 self.configs['full'].inner.ellipticityPriorSigma = 0.3
75 self.configs['full'].inner.radiusPriorSigma = 0.5
76 self.configs['full'].inner.positionPriorSigma = 0.1
77 self.configs['full'].primary.ellipticityPriorSigma = 0.3
78 self.configs['full'].primary.radiusPriorSigma = 0.5
79 self.configs['full'].primary.positionPriorSigma = 0.1
80 self.configs['full'].wings.ellipticityPriorSigma = 0.3
81 self.configs['full'].wings.radiusPriorSigma = 0.5
82 self.configs['full'].wings.positionPriorSigma = 0.1
83 self.configs['full'].outer.ellipticityPriorSigma = 0.3
84 self.configs['full'].outer.radiusPriorSigma = 0.5
85 self.configs['full'].outer.positionPriorSigma = 0.1
87 def tearDown(self):
88 del self.configs
90 def testFixedModel(self):
91 fitter = lsst.meas.modelfit.GeneralPsfFitter(self.configs['fixed'].makeControl())
92 model = fitter.getModel()
94 # check that we have the right numbers and names for parameters
95 self.assertEqual(model.getNonlinearDim(), 0)
96 self.assertEqual(model.getFixedDim(), 10)
97 self.assertEqual(model.getAmplitudeDim(), 2)
98 self.assertEqual(model.getBasisCount(), 2)
99 self.assertEqual(list(model.getNonlinearNames()), [])
100 self.assertEqual(list(model.getAmplitudeNames()), ["primary.alpha[0,0]", "wings.alpha[0,0]"])
101 self.assertEqual(list(model.getFixedNames()),
102 [f"primary.fiducial.{s}" for s in ELLIPSE_PARAMETER_NAMES]
103 + [f"wings.fiducial.{s}" for s in ELLIPSE_PARAMETER_NAMES])
105 # test that we can round-trip ellipses through the model, and that this agrees
106 # with makeShapeletFunction
107 ellipseParameters = numpy.array([[0.01, -0.01, 1.1, 0.03, -0.04],
108 [0.02, -0.02, 0.9, 0.05, -0.06]])
109 ellipses1 = model.makeEllipseVector()
110 for i in range(len(ellipses1)):
111 ellipses1[i].setParameterVector(ellipseParameters[i])
112 nonlinear = numpy.zeros(model.getNonlinearDim(), dtype=lsst.meas.modelfit.Scalar)
113 fixed = numpy.zeros(model.getFixedDim(), dtype=lsst.meas.modelfit.Scalar)
114 amplitudes = numpy.array([1.0, 0.1], dtype=lsst.meas.modelfit.Scalar)
115 model.readEllipses(ellipses1, nonlinear, fixed)
116 self.assertFloatsAlmostEqual(fixed, ellipseParameters.ravel())
117 ellipses2 = model.writeEllipses(nonlinear, fixed)
118 msf = model.makeShapeletFunction(nonlinear, amplitudes, fixed)
119 self.assertFloatsAlmostEqual(len(msf.getComponents()), len(ellipses1))
120 ellipses3 = model.makeEllipseVector()
121 for i in range(len(ellipses2)):
122 self.assertFloatsAlmostEqual(ellipses1[i].getParameterVector(), ellipses2[i].getParameterVector())
123 # need to convert ellipse parametrization
124 ellipses3[i].setCore(msf.getComponents()[i].getEllipse().getCore())
125 ellipses3[i].setCenter(msf.getComponents()[i].getEllipse().getCenter())
126 self.assertFloatsAlmostEqual(ellipses1[i].getParameterVector(), ellipses3[i].getParameterVector())
127 self.assertFloatsAlmostEqual(amplitudes[i:i+1], msf.getComponents()[i].getCoefficients())
129 def testEllipseModel(self):
130 fitter = lsst.meas.modelfit.GeneralPsfFitter(self.configs['ellipse'].makeControl())
131 model = fitter.getModel()
133 # check that we have the right numbers and names for parameters
134 self.assertEqual(model.getNonlinearDim(), 6)
135 self.assertEqual(model.getFixedDim(), 10)
136 self.assertEqual(model.getAmplitudeDim(), 2)
137 self.assertEqual(model.getBasisCount(), 2)
138 self.assertEqual(list(model.getNonlinearNames()),
139 [f"primary.{s}" for s in ELLIPSE_PARAMETER_NAMES[:3]]
140 + [f"wings.{s}" for s in ELLIPSE_PARAMETER_NAMES[:3]]
141 )
142 self.assertEqual(list(model.getAmplitudeNames()), ["primary.alpha[0,0]", "wings.alpha[0,0]"])
143 self.assertEqual(list(model.getFixedNames()),
144 [f"primary.fiducial.{s}" for s in ELLIPSE_PARAMETER_NAMES]
145 + [f"wings.fiducial.{s}" for s in ELLIPSE_PARAMETER_NAMES])
147 # test that we can round-trip ellipses through the model, and that this agrees
148 # with makeShapeletFunction
149 ellipseParameters = numpy.array([[0.01, -0.01, 1.1, 0.03, -0.04],
150 [0.02, -0.02, 0.9, 0.05, -0.06]])
151 ellipses1 = model.makeEllipseVector()
152 for i in range(len(ellipses1)):
153 ellipses1[i].setParameterVector(ellipseParameters[i])
154 nonlinear = numpy.zeros(model.getNonlinearDim(), dtype=lsst.meas.modelfit.Scalar)
155 fixed = numpy.zeros(model.getFixedDim(), dtype=lsst.meas.modelfit.Scalar)
156 amplitudes = numpy.array([1.0, 0.1], dtype=lsst.meas.modelfit.Scalar)
157 model.readEllipses(ellipses1, nonlinear, fixed)
158 self.assertFloatsAlmostEqual(nonlinear, numpy.zeros(model.getNonlinearDim(),
159 dtype=lsst.meas.modelfit.Scalar))
160 self.assertFloatsAlmostEqual(fixed, ellipseParameters.ravel())
161 ellipses2 = model.writeEllipses(nonlinear, fixed)
162 msf = model.makeShapeletFunction(nonlinear, amplitudes, fixed)
163 self.assertEqual(len(msf.getComponents()), len(ellipses1))
164 ellipses3 = model.makeEllipseVector()
165 for i in range(len(ellipses2)):
166 self.assertFloatsAlmostEqual(ellipses1[i].getParameterVector(), ellipses2[i].getParameterVector(),
167 rtol=1E-8)
168 # need to convert ellipse parametrization
169 ellipses3[i].setCore(msf.getComponents()[i].getEllipse().getCore())
170 ellipses3[i].setCenter(msf.getComponents()[i].getEllipse().getCenter())
171 self.assertFloatsAlmostEqual(ellipses1[i].getParameterVector(), ellipses3[i].getParameterVector(),
172 rtol=1E-8)
173 self.assertFloatsAlmostEqual(amplitudes[i:i+1], msf.getComponents()[i].getCoefficients(),
174 rtol=1E-8)
176 # test the ellipse round-tripping again, this time starting with nonzero nonlinear parameters:
177 # this will be read back in by adding to the fixed parameters and zeroing the nonlinear parameters.
178 nonlinear[:] = 0.5*ellipseParameters[:, :3].ravel()
179 ellipses4 = model.writeEllipses(nonlinear, fixed)
180 model.readEllipses(ellipses4, nonlinear, fixed)
181 self.assertFloatsAlmostEqual(nonlinear, numpy.zeros(model.getNonlinearDim(),
182 dtype=lsst.meas.modelfit.Scalar),
183 rtol=1E-8)
184 self.assertFloatsAlmostEqual(fixed.reshape(2, 5)[:, :3], 1.5*ellipseParameters[:, :3], rtol=1E-8)
185 self.assertFloatsAlmostEqual(fixed.reshape(2, 5)[:, 3:], ellipseParameters[:, 3:], rtol=1E-8)
187 def testFullModel(self):
188 fitter = lsst.meas.modelfit.GeneralPsfFitter(self.configs['full'].makeControl())
189 model = fitter.getModel()
191 # check that we have the right numbers and names for parameters
192 self.assertEqual(model.getNonlinearDim(), 20)
193 self.assertEqual(model.getFixedDim(), 20)
194 self.assertEqual(model.getAmplitudeDim(), 2*(1 + lsst.shapelet.computeSize(4)))
195 self.assertEqual(model.getBasisCount(), 4)
196 self.assertEqual(list(model.getNonlinearNames()),
197 [f"inner.{s}" for s in ELLIPSE_PARAMETER_NAMES]
198 + [f"primary.{s}" for s in ELLIPSE_PARAMETER_NAMES]
199 + [f"wings.{s}" for s in ELLIPSE_PARAMETER_NAMES]
200 + [f"outer.{s}" for s in ELLIPSE_PARAMETER_NAMES]
201 )
202 self.assertEqual(list(model.getAmplitudeNames()),
203 ["inner.alpha[0,0]"]
204 + [f"primary.alpha[{x},{y}]"
205 for n, x, y in lsst.shapelet.HermiteIndexGenerator(4)]
206 + [f"wings.alpha[{x},{y}]"
207 for n, x, y in lsst.shapelet.HermiteIndexGenerator(4)]
208 + ["outer.alpha[0,0]"])
209 self.assertEqual(list(model.getFixedNames()),
210 [f"inner.fiducial.{s}" for s in ELLIPSE_PARAMETER_NAMES]
211 + [f"primary.fiducial.{s}" for s in ELLIPSE_PARAMETER_NAMES]
212 + [f"wings.fiducial.{s}" for s in ELLIPSE_PARAMETER_NAMES]
213 + [f"outer.fiducial.{s}" for s in ELLIPSE_PARAMETER_NAMES]
214 )
216 # test that we can round-trip ellipses through the model, and that this agrees
217 # with makeShapeletFunction
218 ellipseParameters = numpy.array([[0.01, -0.01, 1.1, 0.03, -0.04],
219 [0.015, -0.015, 1.0, 0.04, -0.05],
220 [0.02, -0.02, 0.9, 0.05, -0.06],
221 [0.025, -0.025, 0.8, 0.06, -0.07],
222 ])
223 ellipses1 = model.makeEllipseVector()
224 for i in range(len(ellipses1)):
225 ellipses1[i].setParameterVector(ellipseParameters[i])
226 nonlinear = numpy.zeros(model.getNonlinearDim(), dtype=lsst.meas.modelfit.Scalar)
227 fixed = numpy.zeros(model.getFixedDim(), dtype=lsst.meas.modelfit.Scalar)
228 amplitudes = numpy.random.randn(model.getAmplitudeDim())
229 model.readEllipses(ellipses1, nonlinear, fixed)
230 self.assertFloatsAlmostEqual(nonlinear, numpy.zeros(model.getNonlinearDim(),
231 dtype=lsst.meas.modelfit.Scalar))
232 self.assertFloatsAlmostEqual(fixed, ellipseParameters.ravel())
233 ellipses2 = model.writeEllipses(nonlinear, fixed)
234 msf = model.makeShapeletFunction(nonlinear, amplitudes, fixed)
235 self.assertFloatsAlmostEqual(len(msf.getComponents()), len(ellipses1))
236 ellipses3 = model.makeEllipseVector()
237 amplitudeOffset = 0
238 for i in range(len(ellipses2)):
239 self.assertFloatsAlmostEqual(ellipses1[i].getParameterVector(), ellipses2[i].getParameterVector(),
240 rtol=1E-8)
241 # need to convert ellipse parametrization
242 ellipses3[i].setCore(msf.getComponents()[i].getEllipse().getCore())
243 ellipses3[i].setCenter(msf.getComponents()[i].getEllipse().getCenter())
244 amplitudeCount = len(msf.getComponents()[i].getCoefficients())
245 self.assertFloatsAlmostEqual(ellipses1[i].getParameterVector(), ellipses3[i].getParameterVector(),
246 rtol=1E-8)
247 self.assertFloatsAlmostEqual(amplitudes[amplitudeOffset:amplitudeOffset+amplitudeCount],
248 msf.getComponents()[i].getCoefficients(), rtol=1E-8)
249 amplitudeOffset += amplitudeCount
251 # test the ellipse round-tripping again, this time starting with nonzero nonlinear parameters:
252 # this will be read back in by adding to the fixed parameters and zeroing the nonlinear parameters.
253 nonlinear[:] = 0.5*ellipseParameters.ravel()
254 ellipses4 = model.writeEllipses(nonlinear, fixed)
255 model.readEllipses(ellipses4, nonlinear, fixed)
256 self.assertFloatsAlmostEqual(nonlinear, numpy.zeros(model.getNonlinearDim(),
257 dtype=lsst.meas.modelfit.Scalar))
258 self.assertFloatsAlmostEqual(fixed, 1.5*ellipseParameters.ravel())
260 def testApply(self):
261 tolerances = {"full": 3E-4, "ellipse": 8E-3, "fixed": 1E-2}
262 for filename in glob.glob(os.path.join(DATA_DIR, "psfs", "great3*.fits")):
263 kernelImage = lsst.afw.image.ImageD(filename)
264 shape = computeMoments(kernelImage)
265 for configKey in ["full", "ellipse", "fixed"]:
266 fitter = lsst.meas.modelfit.GeneralPsfFitter(self.configs[configKey].makeControl())
267 multiShapeletFit = fitter.apply(kernelImage, shape, 0.01)
268 modelImage = lsst.afw.image.ImageD(kernelImage.getBBox(lsst.afw.image.PARENT))
269 multiShapeletFit.evaluate().addToImage(modelImage)
270 self.assertFloatsAlmostEqual(kernelImage.getArray(), modelImage.getArray(),
271 atol=tolerances[configKey],
272 plotOnFailure=True)
275class TestMemory(lsst.utils.tests.MemoryTestCase):
276 pass
279def setup_module(module):
280 lsst.utils.tests.init()
283if __name__ == "__main__": 283 ↛ 284line 283 didn't jump to line 284 because the condition on line 283 was never true
284 lsst.utils.tests.init()
285 unittest.main()