Coverage for tests/test_photometryModel.py : 26%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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.daf.persistence
35import lsst.jointcal.ccdImage
36import lsst.jointcal.photometryModels
37import lsst.jointcal.star
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.afw.image.utils.resetFilters()
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, fakeCcdId=12,
215 photoCalibMean1=1e-2,
216 photoCalibMean2=1.2e-2)
217 self.ccdImageList2 = struct1.ccdImageList
218 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100, seed=101, fakeCcdId=13,
219 photoCalibMean1=2.0e-2,
220 photoCalibMean2=2.2e-2)
221 self.ccdImageList2.extend(struct2.ccdImageList)
222 camera = struct1.camera # the camera is the same in both structs
223 focalPlaneBBox = camera.getFpBBox()
224 self.model2 = Model(self.ccdImageList2, focalPlaneBBox, self.visitOrder)
226 def test_getNpar(self):
227 """
228 Order 3 => (3+1)*(3+2))/2 = 10 parameters,
229 and the chip map is fixed (only one ccd), so does not contribute.
230 """
231 expect = getNParametersPolynomial(self.visitOrder)
232 result = self.model.getNpar(self.ccdImageList[0])
233 self.assertEqual(result, expect)
234 result = self.model.getNpar(self.ccdImageList[1])
235 self.assertEqual(result, expect)
237 def testGetTotalParameters(self):
238 """Two visits, one (fixed) ccd."""
239 expect = getNParametersPolynomial(self.visitOrder) * 2
240 result = self.model.getTotalParameters()
241 self.assertEqual(result, expect)
243 def test_assignIndices(self):
244 """Test that the correct number of indices were assigned.
245 Does not check that the internal mappings are assigned the correct
246 indices.
247 """
248 # one polynomial per visit, plus one fitted scale for the second chip.
249 expect = 2 * getNParametersPolynomial(self.visitOrder) + 1
250 index = self.model2.assignIndices("Model", self.firstIndex)
251 self.assertEqual(index, expect)
253 # one polynomial per visit
254 expect = 2 * getNParametersPolynomial(self.visitOrder)
255 index = self.model2.assignIndices("ModelVisit", self.firstIndex)
256 self.assertEqual(index, expect)
258 # one fitted chip
259 expect = 1
260 index = self.model2.assignIndices("ModelChip", self.firstIndex)
261 self.assertEqual(index, expect)
263 def _testConstructor(self, expectVisit, expectChips):
264 """Post-construction, the ChipTransforms should be the PhotoCalib mean of
265 the first visit's ccds, and the VisitTransforms should be the identity.
266 """
267 # Identify to the model that we're fitting both components.
268 self.model2.assignIndices("Model", self.firstIndex)
270 # check the visitMappings
271 for ccdImage in self.ccdImageList2:
272 result = self.model2.getMapping(ccdImage).getVisitMapping().getTransform().getParameters()
273 self.assertFloatsEqual(result, expectVisit, msg=ccdImage.getName())
275 # check the chipMappings
276 for ccdImage, expect in zip(self.ccdImageList2, expectChips):
277 result = self.model2.getMapping(ccdImage).getChipMapping().getTransform().getParameters()
278 # almost equal because log() may have been involved in the math
279 self.assertFloatsAlmostEqual(result, expect, msg=ccdImage.getName())
281 def test_photoCalibMean(self):
282 """The mean of the photoCalib should match the mean over a calibrated image."""
283 image = lsst.afw.image.MaskedImageF(self.ccdImageList[0].getDetector().getBBox())
284 image[:] = 1
285 photoCalib = self.model.toPhotoCalib(self.ccdImageList[0])
286 expect = photoCalib.calibrateImage(image).image.array.mean()
287 self.assertFloatsAlmostEqual(expect, photoCalib.getCalibrationMean(), rtol=2e-5)
290class ConstrainedFluxModelTestCase(ConstrainedPhotometryModelTestCase,
291 FluxTestBase,
292 lsst.utils.tests.TestCase):
293 def setUp(self):
294 super().setUp()
295 self.model = lsst.jointcal.ConstrainedFluxModel(self.ccdImageList,
296 self.focalPlaneBBox,
297 self.visitOrder)
298 # have to call this once to let offsetParams work.
299 self.model.assignIndices("Model", self.firstIndex)
300 self.model.offsetParams(self.delta)
302 self._initModel2(lsst.jointcal.ConstrainedFluxModel)
304 def testConstructor(self):
305 expectVisit = np.zeros(int(getNParametersPolynomial(self.visitOrder)))
306 expectVisit[0] = 1
307 # chipMappings are fixed per-chip, and thus are
308 # shared between the first pair and second pair of fake ccdImages
309 expectChips = [self.ccdImageList2[0].getPhotoCalib().getCalibrationMean(),
310 self.ccdImageList2[0].getPhotoCalib().getCalibrationMean(),
311 self.ccdImageList2[2].getPhotoCalib().getCalibrationMean(),
312 self.ccdImageList2[2].getPhotoCalib().getCalibrationMean()]
313 self._testConstructor(expectVisit, expectChips)
315 def test_checkPositiveOnBBox(self):
316 self.assertTrue(self.model.checkPositiveOnBBox(self.ccdImageList[0]))
317 self.assertTrue(self.model.checkPositiveOnBBox(self.ccdImageList[1]))
319 # make the model go negative
320 self.model.offsetParams(-5*self.delta)
321 self.assertFalse(self.model.checkPositiveOnBBox(self.ccdImageList[0]))
323 def test_validate(self):
324 """Test that invalid models fail validate(), and that valid ones pass.
325 """
326 # We need at least 0 degrees of freedom (data - parameters) for the model to be valid.
327 # NOTE: model has 20 parameters (2 visits, 10 params each)
328 self.assertTrue(self.model.validate(self.ccdImageList, 0))
329 self.assertFalse(self.model.validate(self.ccdImageList, -1))
330 # Models that are negative on the bounding box are invalid
331 self.model.offsetParams(-5*self.delta)
332 # ensure ndof is high enough that it will not cause a failure
333 self.assertFalse(self.model.validate(self.ccdImageList, 100))
336class ConstrainedMagnitudeModelTestCase(ConstrainedPhotometryModelTestCase,
337 MagnitudeTestBase,
338 lsst.utils.tests.TestCase):
339 def setUp(self):
340 super().setUp()
341 self.model = lsst.jointcal.ConstrainedMagnitudeModel(self.ccdImageList,
342 self.focalPlaneBBox,
343 self.visitOrder)
344 # have to call this once to let offsetParams work.
345 self.model.assignIndices("Model", self.firstIndex)
346 self.model.offsetParams(self.delta)
348 self._initModel2(lsst.jointcal.ConstrainedMagnitudeModel)
350 self.useMagnitude = True
352 def testConstructor(self):
353 expectVisit = np.zeros(int(getNParametersPolynomial(self.visitOrder)))
355 def fluxToMag(flux):
356 # Yay astropy!
357 return (flux * units.nanojansky).to(units.ABmag).value
359 # chipMappings are fixed per-chip, and thus are
360 # shared between the first pair and second pair of fake ccdImages
361 expectChips = [fluxToMag(self.ccdImageList2[0].getPhotoCalib().getCalibrationMean()),
362 fluxToMag(self.ccdImageList2[0].getPhotoCalib().getCalibrationMean()),
363 fluxToMag(self.ccdImageList2[2].getPhotoCalib().getCalibrationMean()),
364 fluxToMag(self.ccdImageList2[2].getPhotoCalib().getCalibrationMean())]
365 self._testConstructor(expectVisit, expectChips)
368class MemoryTester(lsst.utils.tests.MemoryTestCase):
369 pass
372def setup_module(module):
373 lsst.utils.tests.init()
376if __name__ == "__main__": 376 ↛ 377line 376 didn't jump to line 377, because the condition on line 376 was never true
377 lsst.utils.tests.init()
378 unittest.main()