Coverage for tests/test_photometryModel.py : 27%

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