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