Coverage for tests/test_photometryModel.py: 26%
187 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-06 11:56 +0000
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-06 11:56 +0000
1# This file is part of jointcal.
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 numpy as np
24import unittest
25import lsst.utils.tests
26import lsst.jointcal.testUtils
28from astropy import units
30import lsst.afw.cameraGeom
31import lsst.afw.table
32import lsst.afw.image
33import lsst.afw.image.utils
34import lsst.jointcal
35import lsst.obs.base
38def getNParametersPolynomial(order):
39 """Number of parameters in a photometry polynomial model is (d+1)(d+2)/2."""
40 return (order + 1)*(order + 2)/2
43class PhotometryModelTestBase:
44 """Have the sublass also derive from ``lsst.utils.tests.TestCase`` to cause
45 unittest to use the test_* methods in this class.
46 """
47 def setUp(self):
48 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100)
49 self.ccdImageList = struct.ccdImageList
50 self.camera = struct.camera
51 self.catalogs = struct.catalogs
52 self.fluxFieldName = struct.fluxFieldName
54 self.stars = []
55 for catalog, ccdImage in zip(self.catalogs, self.ccdImageList):
56 pixToFocal = ccdImage.getDetector().getTransform(lsst.afw.cameraGeom.PIXELS,
57 lsst.afw.cameraGeom.FOCAL_PLANE)
58 self.stars.append(lsst.jointcal.testUtils.getMeasuredStarsFromCatalog(catalog, pixToFocal))
60 self.fittedStar = lsst.jointcal.FittedStar(self.stars[0][0])
61 # Make a refStar at this fittedStar position, but with different
62 # flux and fluxErr, so that it does interesting things when subtracted.
63 self.refStar = lsst.jointcal.RefStar(self.fittedStar.x,
64 self.fittedStar.y,
65 self.fittedStar.flux + 50,
66 self.fittedStar.fluxErr * 0.01)
68 self.firstIndex = 0 # for assignIndices
70 # Set to True in the subclass constructor to do the PhotoCalib calculations in magnitudes.
71 self.useMagnitude = False
73 def _toPhotoCalib(self, ccdImage, catalog, stars):
74 """Test converting this object to a PhotoCalib."""
75 photoCalib = self.model.toPhotoCalib(ccdImage)
76 if self.useMagnitude:
77 result = photoCalib.instFluxToMagnitude(catalog, self.fluxFieldName)
78 else:
79 result = photoCalib.instFluxToNanojansky(catalog, self.fluxFieldName)
81 expects = np.empty(len(stars))
82 for i, star in enumerate(stars):
83 expects[i] = self.model.transform(ccdImage, star)
84 self.assertFloatsAlmostEqual(result[:, 0], expects, rtol=2e-13)
85 # NOTE: don't compare transformed errors, as they will be different:
86 # photoCalib incorporates the model error, while jointcal computes the
87 # full covariance matrix, from which the model error should be derived.
89 def test_toPhotoCalib(self):
90 self._toPhotoCalib(self.ccdImageList[0], self.catalogs[0], self.stars[0])
91 self._toPhotoCalib(self.ccdImageList[1], self.catalogs[1], self.stars[1])
93 def test_freezeErrorTransform(self):
94 """After calling freezeErrorTransform(), the error transform is unchanged
95 by offsetParams().
96 """
97 ccdImage = self.ccdImageList[0]
98 star0 = self.stars[0][0]
100 self.model.offsetParams(self.delta)
101 t1 = self.model.transform(ccdImage, star0)
102 t1Err = self.model.transformError(ccdImage, star0)
103 self.model.freezeErrorTransform()
104 self.model.offsetParams(self.delta)
105 t2 = self.model.transform(ccdImage, star0)
106 t2Err = self.model.transformError(ccdImage, star0)
108 self.assertFloatsNotEqual(t1, t2)
109 self.assertFloatsEqual(t1Err, t2Err)
112class FluxTestBase:
113 """Have the sublass also derive from ``lsst.utils.tests.TestCase`` to cause
114 unittest to use the test_* methods in this class.
115 """
116 def test_offsetFittedStar(self):
117 value = self.fittedStar.flux
119 self.model.offsetFittedStar(self.fittedStar, 0)
120 self.assertEqual(self.fittedStar.flux, value)
122 self.model.offsetFittedStar(self.fittedStar, 1)
123 self.assertEqual(self.fittedStar.flux, value-1)
125 def test_computeRefResidual(self):
126 result = self.model.computeRefResidual(self.fittedStar, self.refStar)
127 self.assertEqual(result, self.fittedStar.flux - self.refStar.flux)
130class MagnitudeTestBase:
131 """Have the sublass also derive from ``lsst.utils.tests.TestCase`` to cause
132 unittest to use the test_* methods in this class.
133 """
134 def test_offsetFittedStar(self):
135 value = self.fittedStar.mag
137 self.model.offsetFittedStar(self.fittedStar, 0)
138 self.assertEqual(self.fittedStar.mag, value)
140 self.model.offsetFittedStar(self.fittedStar, 1)
141 self.assertEqual(self.fittedStar.mag, value-1)
143 def test_computeRefResidual(self):
144 result = self.model.computeRefResidual(self.fittedStar, self.refStar)
145 self.assertEqual(result, self.fittedStar.mag - self.refStar.mag)
148class SimplePhotometryModelTestBase(PhotometryModelTestBase):
149 """Have the sublass also derive from ``lsst.utils.tests.TestCase`` to cause
150 unittest to use the test_* methods in this class.
151 """
152 def test_getNpar(self):
153 result = self.model.getNpar(self.ccdImageList[0])
154 self.assertEqual(result, 1)
155 result = self.model.getNpar(self.ccdImageList[1])
156 self.assertEqual(result, 1)
158 def testGetTotalParameters(self):
159 result = self.model.getTotalParameters()
160 self.assertEqual(result, 2)
163class SimpleFluxModelTestCase(SimplePhotometryModelTestBase, FluxTestBase, lsst.utils.tests.TestCase):
164 def setUp(self):
165 super().setUp()
166 self.model = lsst.jointcal.SimpleFluxModel(self.ccdImageList)
167 self.model.assignIndices("", self.firstIndex) # have to call this once to let offsetParams work.
168 self.delta = np.arange(len(self.ccdImageList), dtype=float)*-0.2 + 1
171class SimpleMagnitudeModelTestCase(SimplePhotometryModelTestBase,
172 MagnitudeTestBase,
173 lsst.utils.tests.TestCase):
174 def setUp(self):
175 super().setUp()
176 self.model = lsst.jointcal.SimpleMagnitudeModel(self.ccdImageList)
177 self.model.assignIndices("", self.firstIndex) # have to call this once to let offsetParams work.
178 self.delta = np.arange(len(self.ccdImageList), dtype=float)*-0.2 + 1
179 self.useMagnitude = True
182class ConstrainedPhotometryModelTestCase(PhotometryModelTestBase):
183 def setUp(self):
184 super().setUp()
185 self.visitOrder = 3
186 self.focalPlaneBBox = self.camera.getFpBBox()
187 # Amount to shift the parameters to get more than just a constant field
188 # for the second ccdImage.
189 # Reverse the range so that the low order terms are the largest.
190 self.delta = (np.arange(20, dtype=float)*-0.2 + 1)[::-1]
191 # but keep the first ccdImage constant, to help distinguish test failures.
192 self.delta[:10] = 0.0
193 self.delta[0] = -5.0
195 def _initModel2(self, Model):
196 """
197 Initialize self.model2 with 2 fake sensor catalogs. Call after setUp().
199 Parameters
200 ----------
201 Model : `PhotometryModel`-type
202 The PhotometryModel-derived class to construct.
203 """
204 # We need at least two sensors to distinguish "Model" from "ModelVisit"
205 # in `test_assignIndices()`.
206 # createTwoFakeCcdImages() always uses the same two visitIds,
207 # so there will be 2 visits total here.
208 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100, seed=100,
209 fakeDetectorId=12,
210 photoCalibMean1=1e-2,
211 photoCalibMean2=1.2e-2)
212 self.ccdImageList2 = struct1.ccdImageList
213 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100, seed=101,
214 fakeDetectorId=13,
215 photoCalibMean1=2.0e-2,
216 photoCalibMean2=2.2e-2)
217 self.ccdImageList2.extend(struct2.ccdImageList)
218 camera = struct1.camera # the camera is the same in both structs
219 focalPlaneBBox = camera.getFpBBox()
220 self.model2 = Model(self.ccdImageList2, focalPlaneBBox, self.visitOrder)
222 def test_getNpar(self):
223 """
224 Order 3 => (3+1)*(3+2))/2 = 10 parameters,
225 and the chip map is fixed (only one ccd), so does not contribute.
226 """
227 expect = getNParametersPolynomial(self.visitOrder)
228 result = self.model.getNpar(self.ccdImageList[0])
229 self.assertEqual(result, expect)
230 result = self.model.getNpar(self.ccdImageList[1])
231 self.assertEqual(result, expect)
233 def testGetTotalParameters(self):
234 """Two visits, one (fixed) ccd."""
235 expect = getNParametersPolynomial(self.visitOrder) * 2
236 result = self.model.getTotalParameters()
237 self.assertEqual(result, expect)
239 def test_assignIndices(self):
240 """Test that the correct number of indices were assigned.
241 Does not check that the internal mappings are assigned the correct
242 indices.
243 """
244 # one polynomial per visit, plus one fitted scale for the second chip.
245 expect = 2 * getNParametersPolynomial(self.visitOrder) + 1
246 index = self.model2.assignIndices("Model", self.firstIndex)
247 self.assertEqual(index, expect)
249 # one polynomial per visit
250 expect = 2 * getNParametersPolynomial(self.visitOrder)
251 index = self.model2.assignIndices("ModelVisit", self.firstIndex)
252 self.assertEqual(index, expect)
254 # one fitted chip
255 expect = 1
256 index = self.model2.assignIndices("ModelChip", self.firstIndex)
257 self.assertEqual(index, expect)
259 def _testConstructor(self, expectVisit, expectChips):
260 """Post-construction, the ChipTransforms should be the PhotoCalib mean of
261 the first visit's ccds, and the VisitTransforms should be the identity.
262 """
263 # Identify to the model that we're fitting both components.
264 self.model2.assignIndices("Model", self.firstIndex)
266 # check the visitMappings
267 for ccdImage in self.ccdImageList2:
268 result = self.model2.getMapping(ccdImage).getVisitMapping().getTransform().getParameters()
269 self.assertFloatsEqual(result, expectVisit, msg=ccdImage.getName())
271 # check the chipMappings
272 for ccdImage, expect in zip(self.ccdImageList2, expectChips):
273 result = self.model2.getMapping(ccdImage).getChipMapping().getTransform().getParameters()
274 # almost equal because log() may have been involved in the math
275 self.assertFloatsAlmostEqual(result, expect, msg=ccdImage.getName())
277 def test_photoCalibMean(self):
278 """The mean of the photoCalib should match the mean over a calibrated image."""
279 image = lsst.afw.image.MaskedImageF(self.ccdImageList[0].getDetector().getBBox())
280 image[:] = 1
281 photoCalib = self.model.toPhotoCalib(self.ccdImageList[0])
282 expect = photoCalib.calibrateImage(image).image.array.mean()
283 self.assertFloatsAlmostEqual(expect, photoCalib.getCalibrationMean(), rtol=2e-5)
286class ConstrainedFluxModelTestCase(ConstrainedPhotometryModelTestCase,
287 FluxTestBase,
288 lsst.utils.tests.TestCase):
289 def setUp(self):
290 super().setUp()
291 self.model = lsst.jointcal.ConstrainedFluxModel(self.ccdImageList,
292 self.focalPlaneBBox,
293 self.visitOrder)
294 # have to call this once to let offsetParams work.
295 self.model.assignIndices("Model", self.firstIndex)
296 self.model.offsetParams(self.delta)
298 self._initModel2(lsst.jointcal.ConstrainedFluxModel)
300 def testConstructor(self):
301 expectVisit = np.zeros(int(getNParametersPolynomial(self.visitOrder)))
302 expectVisit[0] = 1
303 # chipMappings are fixed per-chip, and thus are
304 # shared between the first pair and second pair of fake ccdImages
305 expectChips = [self.ccdImageList2[0].getPhotoCalib().getCalibrationMean(),
306 self.ccdImageList2[0].getPhotoCalib().getCalibrationMean(),
307 self.ccdImageList2[2].getPhotoCalib().getCalibrationMean(),
308 self.ccdImageList2[2].getPhotoCalib().getCalibrationMean()]
309 self._testConstructor(expectVisit, expectChips)
311 def test_checkPositiveOnBBox(self):
312 self.assertTrue(self.model.checkPositiveOnBBox(self.ccdImageList[0]))
313 self.assertTrue(self.model.checkPositiveOnBBox(self.ccdImageList[1]))
315 # make the model go negative
316 self.model.offsetParams(-5*self.delta)
317 self.assertFalse(self.model.checkPositiveOnBBox(self.ccdImageList[0]))
319 def test_validate(self):
320 """Test that invalid models fail validate(), and that valid ones pass.
321 """
322 # We need at least 0 degrees of freedom (data - parameters) for the model to be valid.
323 # NOTE: model has 20 parameters (2 visits, 10 params each)
324 self.assertTrue(self.model.validate(self.ccdImageList, 0))
325 self.assertFalse(self.model.validate(self.ccdImageList, -1))
326 # Models that are negative on the bounding box are invalid
327 self.model.offsetParams(-5*self.delta)
328 # ensure ndof is high enough that it will not cause a failure
329 self.assertFalse(self.model.validate(self.ccdImageList, 100))
332class ConstrainedMagnitudeModelTestCase(ConstrainedPhotometryModelTestCase,
333 MagnitudeTestBase,
334 lsst.utils.tests.TestCase):
335 def setUp(self):
336 super().setUp()
337 self.model = lsst.jointcal.ConstrainedMagnitudeModel(self.ccdImageList,
338 self.focalPlaneBBox,
339 self.visitOrder)
340 # have to call this once to let offsetParams work.
341 self.model.assignIndices("Model", self.firstIndex)
342 self.model.offsetParams(self.delta)
344 self._initModel2(lsst.jointcal.ConstrainedMagnitudeModel)
346 self.useMagnitude = True
348 def testConstructor(self):
349 expectVisit = np.zeros(int(getNParametersPolynomial(self.visitOrder)))
351 def fluxToMag(flux):
352 # Yay astropy!
353 return (flux * units.nanojansky).to(units.ABmag).value
355 # chipMappings are fixed per-chip, and thus are
356 # shared between the first pair and second pair of fake ccdImages
357 expectChips = [fluxToMag(self.ccdImageList2[0].getPhotoCalib().getCalibrationMean()),
358 fluxToMag(self.ccdImageList2[0].getPhotoCalib().getCalibrationMean()),
359 fluxToMag(self.ccdImageList2[2].getPhotoCalib().getCalibrationMean()),
360 fluxToMag(self.ccdImageList2[2].getPhotoCalib().getCalibrationMean())]
361 self._testConstructor(expectVisit, expectChips)
364class MemoryTester(lsst.utils.tests.MemoryTestCase):
365 pass
368def setup_module(module):
369 lsst.utils.tests.init()
372if __name__ == "__main__": 372 ↛ 373line 372 didn't jump to line 373, because the condition on line 372 was never true
373 lsst.utils.tests.init()
374 unittest.main()