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