Coverage for tests/test_photoCalib.py: 10%
436 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-19 12:16 -0700
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-19 12:16 -0700
1#
2# LSST Data Management System
3# Copyright 2008-2016 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
23import os.path
24import unittest
26import numpy as np
28import astropy.units as u
30import lsst.utils.tests
31import lsst.geom
32import lsst.afw.image
33import lsst.afw.image.testUtils
34import lsst.afw.math
35import lsst.daf.base
36import lsst.pex.exceptions
39def computeNanojanskyErr(instFluxErr, instFlux, calibrationErr, calibration, flux):
40 """Return the error on the flux (nanojansky)."""
41 return flux*np.hypot(instFluxErr/instFlux, calibrationErr/calibration)
44def computeMagnitudeErr(instFluxErr, instFlux, calibrationErr, calibration, flux):
45 """Return the error on the magnitude."""
46 err = computeNanojanskyErr(instFluxErr, instFlux, calibrationErr, calibration, flux)
47 return 2.5/np.log(10) * err / flux
50def makeCalibratedMaskedImage(image, mask, variance, outImage, calibration, calibrationErr):
51 """Return a MaskedImage using outImage, mask, and a computed variance image."""
52 outErr = computeNanojanskyErr(np.sqrt(variance),
53 image,
54 calibrationErr,
55 calibration,
56 outImage).astype(np.float32) # variance plane must be 32bit
57 return lsst.afw.image.makeMaskedImageFromArrays(outImage,
58 mask,
59 outErr**2)
62def makeCalibratedMaskedImageNoCalibrationError(image, mask, variance, outImage):
63 """Return a MaskedImage using outImage, mask, and a computed variance image.
65 Ignores the contributions from the uncertainty in the calibration.
66 """
67 outErr = computeNanojanskyErr(np.sqrt(variance),
68 image,
69 0,
70 1,
71 outImage).astype(np.float32) # variance plane must be 32bit
72 return lsst.afw.image.makeMaskedImageFromArrays(outImage,
73 mask,
74 outErr**2)
77class PhotoCalibTestCase(lsst.utils.tests.TestCase):
79 def setUp(self):
80 np.random.seed(100)
82 self.point0 = lsst.geom.Point2D(0, 0)
83 self.pointXShift = lsst.geom.Point2D(-10, 0)
84 self.pointYShift = lsst.geom.Point2D(0, -10)
85 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-100, -100), lsst.geom.Point2I(100, 100))
87 # calibration and instFlux1 are selected to produce calibrated flux of 1.
88 self.calibration = 1e-3
89 self.calibrationErr = 1e-4
90 self.instFlux1 = 1000.
91 self.instFluxErr1 = 10.
92 self.flux1 = 1.0
93 self.mag1 = (self.flux1*u.nJy).to_value(u.ABmag)
95 # useful reference points: 575.44 nJy ~= 24.5 mag, 3630.78 * 10^9 nJy ~= 0 mag
96 self.flux2 = 575.44
97 self.instFlux2 = self.instFlux1*self.flux2
98 self.mag2 = (self.flux2*u.nJy).to_value(u.ABmag)
100 self.schema = lsst.afw.table.SourceTable.makeMinimalSchema()
101 self.instFluxKeyName = "SomeFlux"
102 lsst.afw.table.Point2DKey.addFields(self.schema, "centroid", "centroid", "pixels")
103 self.schema.addField(self.instFluxKeyName+"_instFlux", type="D", units="count",
104 doc="post-ISR instrumental Flux")
105 self.schema.addField(self.instFluxKeyName+"_instFluxErr", type="D", units="count",
106 doc="post-ISR instrumental flux error")
107 self.schema.addField(self.instFluxKeyName+"_flux", type="D", units="nJy",
108 doc="calibrated flux")
109 self.schema.addField(self.instFluxKeyName+"_fluxErr", type="D", units="nJy",
110 doc="calibrated flux error")
111 self.schema.addField(self.instFluxKeyName+"_mag", type="D",
112 doc="calibrated magnitude")
113 self.schema.addField(self.instFluxKeyName+"_magErr", type="D",
114 doc="calibrated magnitude error")
115 self.otherInstFluxKeyName = "OtherFlux"
116 self.schema.addField(self.otherInstFluxKeyName+"_instFlux", type="D", units="count",
117 doc="another instrumental Flux")
118 self.schema.addField(self.otherInstFluxKeyName+"_instFluxErr", type="D", units="count",
119 doc="another instrumental flux error")
120 self.noErrInstFluxKeyName = "NoErrFlux"
121 self.schema.addField(self.noErrInstFluxKeyName+"_instFlux", type="D", units="count",
122 doc="instrumental Flux with no error")
123 self.table = lsst.afw.table.SourceTable.make(self.schema)
124 self.table.defineCentroid('centroid')
125 self.catalog = lsst.afw.table.SourceCatalog(self.table)
126 record = self.catalog.addNew()
127 record.set('id', 1)
128 record.set('centroid_x', self.point0[0])
129 record.set('centroid_y', self.point0[1])
130 record.set(self.instFluxKeyName+'_instFlux', self.instFlux1)
131 record.set(self.instFluxKeyName+'_instFluxErr', self.instFluxErr1)
132 record.set(self.otherInstFluxKeyName+'_instFlux', self.instFlux1)
133 record.set(self.otherInstFluxKeyName+'_instFluxErr', self.instFluxErr1)
134 record.set(self.noErrInstFluxKeyName+'_instFlux', self.instFlux1)
135 record = self.catalog.addNew()
136 record.set('id', 2)
137 record.set('centroid_x', self.pointYShift[0])
138 record.set('centroid_y', self.pointYShift[1])
139 record.set(self.instFluxKeyName+'_instFlux', self.instFlux2)
140 record.set(self.instFluxKeyName+'_instFluxErr', self.instFluxErr1)
141 record.set(self.otherInstFluxKeyName+'_instFlux', self.instFlux2)
142 record.set(self.otherInstFluxKeyName+'_instFluxErr', self.instFluxErr1)
143 record.set(self.noErrInstFluxKeyName+'_instFlux', self.instFlux2)
145 self.constantCalibration = lsst.afw.math.ChebyshevBoundedField(self.bbox,
146 np.array([[self.calibration]]))
147 self.linearXCalibration = lsst.afw.math.ChebyshevBoundedField(self.bbox,
148 np.array([[self.calibration,
149 self.calibration]]))
151 def tearDown(self):
152 del self.schema
153 del self.table
154 del self.catalog
156 def _testPhotoCalibCenter(self, photoCalib, calibrationErr):
157 """
158 Test conversions of instFlux for the mean and (0,0) value of a photoCalib.
159 Assumes those are the same, e.g. that the non-constant terms are all
160 odd, and that the mean of the calib is self.calibration.
162 calibrationErr is provided as an option to allow testing of photoCalibs
163 that have no error specified, and those that do.
164 """
165 # test that the constructor set the calibrationMean and err correctly
166 self.assertEqual(self.calibration, photoCalib.getCalibrationMean())
167 self.assertEqual(photoCalib.instFluxToMagnitude(photoCalib.getInstFluxAtZeroMagnitude()), 0)
168 self.assertEqual(calibrationErr, photoCalib.getCalibrationErr())
170 # test with a "trivial" flux
171 self.assertEqual(self.flux1, photoCalib.instFluxToNanojansky(self.instFlux1))
172 self.assertEqual(self.mag1, photoCalib.instFluxToMagnitude(self.instFlux1))
174 # a less trivial flux
175 self.assertFloatsAlmostEqual(self.flux2, photoCalib.instFluxToNanojansky(self.instFlux2))
176 self.assertFloatsAlmostEqual(self.mag2, photoCalib.instFluxToMagnitude(self.instFlux2))
177 # test that (0,0) gives the same result as above
178 self.assertFloatsAlmostEqual(self.flux2, photoCalib.instFluxToNanojansky(self.instFlux2, self.point0))
179 self.assertFloatsAlmostEqual(self.mag2, photoCalib.instFluxToMagnitude(self.instFlux2, self.point0))
181 # test that we get a correct nJy err for the base instFlux
182 errFlux1 = computeNanojanskyErr(self.instFluxErr1,
183 self.instFlux1,
184 calibrationErr,
185 self.calibration,
186 self.flux1)
187 result = photoCalib.instFluxToNanojansky(self.instFlux1, self.instFluxErr1)
188 self.assertEqual(1, result.value)
189 self.assertFloatsAlmostEqual(errFlux1, result.error)
190 result = photoCalib.instFluxToNanojansky(self.instFlux1, self.instFluxErr1, self.point0)
191 self.assertFloatsAlmostEqual(self.flux1, result.value)
192 self.assertFloatsAlmostEqual(errFlux1, result.error)
194 # test that we get a correct magnitude err for the base instFlux
195 errMag1 = computeMagnitudeErr(self.instFluxErr1,
196 self.instFlux1,
197 calibrationErr,
198 self.calibration,
199 self.flux1)
200 result = photoCalib.instFluxToMagnitude(self.instFlux1, self.instFluxErr1)
201 self.assertEqual(self.mag1, result.value)
202 self.assertFloatsAlmostEqual(errMag1, result.error)
203 # and the same given an explicit point at the center
204 result = photoCalib.instFluxToMagnitude(self.instFlux1, self.instFluxErr1, self.point0)
205 self.assertFloatsAlmostEqual(self.mag1, result.value)
206 self.assertFloatsAlmostEqual(errMag1, result.error)
208 # test that we get a correct nJy err for flux2
209 errFlux2 = computeNanojanskyErr(self.instFluxErr1,
210 self.instFlux2,
211 calibrationErr,
212 self.calibration,
213 self.flux2)
214 result = photoCalib.instFluxToNanojansky(self.instFlux2, self.instFluxErr1)
215 self.assertFloatsAlmostEqual(self.flux2, result.value)
216 self.assertFloatsAlmostEqual(errFlux2, result.error)
217 result = photoCalib.instFluxToNanojansky(self.instFlux2, self.instFluxErr1, self.point0)
218 self.assertFloatsAlmostEqual(self.flux2, result.value)
219 self.assertFloatsAlmostEqual(errFlux2, result.error)
221 # test that we get a correct magnitude err for 575 nJy
222 errMag2 = computeMagnitudeErr(self.instFluxErr1,
223 self.instFlux2,
224 calibrationErr,
225 self.calibration,
226 self.flux2)
227 result = photoCalib.instFluxToMagnitude(self.instFlux2, self.instFluxErr1)
228 self.assertFloatsAlmostEqual(self.mag2, result.value)
229 self.assertFloatsAlmostEqual(errMag2, result.error)
230 result = photoCalib.instFluxToMagnitude(self.instFlux2, self.instFluxErr1, self.point0)
231 self.assertFloatsAlmostEqual(self.mag2, result.value)
232 self.assertFloatsAlmostEqual(errMag2, result.error)
234 # test calculations on a single sourceRecord
235 record = self.catalog[0]
236 result = photoCalib.instFluxToNanojansky(record, self.instFluxKeyName)
237 self.assertEqual(self.flux1, result.value)
238 self.assertFloatsAlmostEqual(errFlux1, result.error)
239 result = photoCalib.instFluxToMagnitude(record, self.instFluxKeyName)
240 self.assertEqual(self.mag1, result.value)
241 self.assertFloatsAlmostEqual(errMag1, result.error)
243 expectNanojansky = np.array([[self.flux1, errFlux1], [self.flux2, errFlux2]])
244 expectMag = np.array([[self.mag1, errMag1], [self.mag2, errMag2]])
245 self._testSourceCatalog(photoCalib, self.catalog, expectNanojansky, expectMag)
247 # test reverse conversion: magnitude to instFlux (no position specified)
248 self.assertFloatsAlmostEqual(self.instFlux1, photoCalib.magnitudeToInstFlux(self.mag1))
249 self.assertFloatsAlmostEqual(self.instFlux2, photoCalib.magnitudeToInstFlux(self.mag2), rtol=1e-15)
251 # test round-tripping instFlux->magnitude->instFlux (position specified)
252 mag = photoCalib.instFluxToMagnitude(self.instFlux1, self.pointXShift)
253 self.assertFloatsAlmostEqual(self.instFlux1,
254 photoCalib.magnitudeToInstFlux(mag, self.pointXShift),
255 rtol=1e-15)
256 mag2 = photoCalib.instFluxToMagnitude(self.instFlux2, self.pointXShift)
257 self.assertFloatsAlmostEqual(self.instFlux2,
258 photoCalib.magnitudeToInstFlux(mag2, self.pointXShift),
259 rtol=1e-15)
261 # test round-tripping arrays (position specified)
262 instFlux1Array = np.full(10, self.instFlux1)
263 instFlux2Array = np.full(10, self.instFlux2)
264 pointXShiftXArray = np.full(10, self.pointXShift.getX())
265 pointXShiftYArray = np.full(10, self.pointXShift.getY())
267 magArray = photoCalib.instFluxToMagnitudeArray(
268 instFlux1Array,
269 pointXShiftXArray,
270 pointXShiftYArray
271 )
272 self.assertFloatsAlmostEqual(magArray.value, mag)
273 self.assertFloatsAlmostEqual(photoCalib.magnitudeToInstFluxArray(magArray,
274 pointXShiftXArray,
275 pointXShiftYArray
276 ),
277 instFlux1Array,
278 rtol=5e-15)
279 mag2Array = photoCalib.instFluxToMagnitudeArray(
280 np.full(10, self.instFlux2),
281 np.full(10, self.pointXShift.getX()),
282 np.full(10, self.pointXShift.getY())
283 )
284 self.assertFloatsAlmostEqual(mag2Array.value, mag2)
285 self.assertFloatsAlmostEqual(photoCalib.magnitudeToInstFluxArray(mag2Array,
286 pointXShiftXArray,
287 pointXShiftYArray
288 ),
289 instFlux2Array,
290 rtol=5e-15)
292 # test getLocalCalibration.
293 meas = photoCalib.instFluxToNanojansky(self.instFlux1, self.instFluxErr1, self.pointXShift)
294 localCalib = photoCalib.getLocalCalibration(self.pointXShift)
295 flux = localCalib * self.instFlux1
296 self.assertAlmostEqual(meas.value, flux)
298 # test getLocalCalibrationArray
299 localCalib2 = photoCalib.getLocalCalibrationArray(
300 pointXShiftXArray,
301 pointXShiftYArray
302 )
303 self.assertFloatsAlmostEqual(localCalib2, localCalib)
305 def _testSourceCatalog(self, photoCalib, catalog, expectNanojansky, expectMag):
306 """Test instFluxTo*(sourceCatalog, ...), and calibrateCatalog()."""
308 # test calculations on a sourceCatalog, returning the array
309 result = photoCalib.instFluxToNanojansky(catalog, self.instFluxKeyName)
310 self.assertFloatsAlmostEqual(expectNanojansky, result)
311 result = photoCalib.instFluxToMagnitude(catalog, self.instFluxKeyName)
312 self.assertFloatsAlmostEqual(expectMag, result)
314 # Test modifying the catalog in-place with instFluxToNanojansky/instFluxToMagnitude
315 # The original instFluxes shouldn't change: save them to test that.
316 origInstFlux = catalog[self.instFluxKeyName+'_instFlux'].copy()
317 origInstFluxErr = catalog[self.instFluxKeyName+'_instFluxErr'].copy()
319 def checkCatalog(catalog, expect, keyName, outField):
320 """Test that the fields in the catalog are correct."""
321 self.assertFloatsAlmostEqual(catalog[keyName+'_%s' % outField], expect[:, 0])
322 self.assertFloatsAlmostEqual(catalog[keyName+'_%sErr' % outField], expect[:, 1])
323 self.assertFloatsAlmostEqual(catalog[keyName+'_instFlux'], origInstFlux)
324 self.assertFloatsAlmostEqual(catalog[keyName+'_instFluxErr'], origInstFluxErr)
326 testCat = catalog.copy(deep=True)
327 photoCalib.instFluxToMagnitude(testCat, self.instFluxKeyName, self.instFluxKeyName)
328 checkCatalog(testCat, expectMag, self.instFluxKeyName, "mag")
330 testCat = catalog.copy(deep=True)
331 photoCalib.instFluxToNanojansky(testCat, self.instFluxKeyName, self.instFluxKeyName)
332 checkCatalog(testCat, expectNanojansky, self.instFluxKeyName, "flux")
334 testCat = catalog.copy(deep=True)
335 photoCalib.instFluxToMagnitude(testCat, self.instFluxKeyName, self.instFluxKeyName)
336 checkCatalog(testCat, expectMag, self.instFluxKeyName, "mag")
338 # test returning a calibrated catalog with calibrateCatalog
340 # test that trying to calibrate a non-existent flux field raises
341 with self.assertRaises(lsst.pex.exceptions.NotFoundError):
342 photoCalib.calibrateCatalog(testCat, ["NotARealFluxFieldName"])
344 # test calibrating just one flux field
345 testCat = catalog.copy(deep=True)
346 result = photoCalib.calibrateCatalog(testCat, [self.otherInstFluxKeyName])
347 checkCatalog(result, expectNanojansky, self.otherInstFluxKeyName, "flux")
348 checkCatalog(result, expectMag, self.otherInstFluxKeyName, "mag")
350 # test calibrating all of the flux fields
351 testCat = catalog.copy(deep=True)
352 result = photoCalib.calibrateCatalog(testCat)
353 checkCatalog(result, expectNanojansky, self.instFluxKeyName, "flux")
354 checkCatalog(result, expectMag, self.instFluxKeyName, "mag")
355 checkCatalog(result, expectNanojansky, self.otherInstFluxKeyName, "flux")
356 checkCatalog(result, expectMag, self.otherInstFluxKeyName, "mag")
357 self.assertFloatsAlmostEqual(result[self.noErrInstFluxKeyName+'_flux'], expectNanojansky[:, 0])
358 self.assertFloatsAlmostEqual(result[self.noErrInstFluxKeyName+'_mag'], expectMag[:, 0])
359 self.assertFloatsAlmostEqual(result[self.noErrInstFluxKeyName+'_instFlux'], origInstFlux)
361 def testNonVarying(self):
362 """Test constructing with a constant calibration factor."""
363 photoCalib = lsst.afw.image.PhotoCalib(self.calibration)
364 self._testPhotoCalibCenter(photoCalib, 0)
366 # Test _isConstant
367 self.assertTrue(photoCalib._isConstant)
369 # test on positions off the center (position should not matter)
370 self.assertEqual(self.flux1, photoCalib.instFluxToNanojansky(self.instFlux1, self.pointXShift))
371 self.assertEqual(self.mag1, photoCalib.instFluxToMagnitude(self.instFlux1, self.pointXShift))
372 result = photoCalib.instFluxToNanojansky(self.instFlux1, self.instFluxErr1)
373 self.assertEqual(self.flux1, result.value)
375 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr)
376 self._testPhotoCalibCenter(photoCalib, self.calibrationErr)
378 # test converting to a photoCalib
379 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, bbox=self.bbox)
380 self._testPhotoCalibCenter(photoCalib, 0)
382 def testConstantBoundedField(self):
383 """Test constructing with a spatially-constant bounded field."""
384 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration)
385 self._testPhotoCalibCenter(photoCalib, 0)
387 # test on positions off the center (position should not matter)
388 self.assertEqual(self.flux1, photoCalib.instFluxToNanojansky(self.instFlux1, self.pointYShift))
389 self.assertEqual(self.mag1, photoCalib.instFluxToMagnitude(self.instFlux1, self.pointYShift))
390 self.assertFloatsAlmostEqual(self.flux2,
391 photoCalib.instFluxToNanojansky(self.instFlux2, self.pointXShift))
392 self.assertFloatsAlmostEqual(self.mag2,
393 photoCalib.instFluxToMagnitude(self.instFlux2, self.pointXShift))
395 # test converting to a photoCalib
396 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration, self.calibrationErr)
397 self._testPhotoCalibCenter(photoCalib, self.calibrationErr)
399 # test _isConstant (bounded field is not constant)
400 self.assertFalse(photoCalib._isConstant)
402 def testLinearXBoundedField(self):
403 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration)
404 self._testPhotoCalibCenter(photoCalib, 0)
406 # test on positions off the center (Y position should not matter)
407 self.assertEqual(self.flux1, photoCalib.instFluxToNanojansky(self.instFlux1, self.pointYShift))
408 self.assertEqual(self.mag1, photoCalib.instFluxToMagnitude(self.instFlux1, self.pointYShift))
410 # test on positions off the center (X position does matter)
411 calibration = (self.calibration + self.pointXShift.getX()*self.calibration/(self.bbox.getWidth()/2.))
412 expect = self.instFlux1*calibration
413 self.assertFloatsAlmostEqual(expect,
414 photoCalib.instFluxToNanojansky(self.instFlux1, self.pointXShift))
415 self.assertFloatsAlmostEqual((expect*u.nJy).to_value(u.ABmag),
416 photoCalib.instFluxToMagnitude(self.instFlux1, self.pointXShift))
417 expect2 = self.instFlux2*calibration
418 self.assertFloatsAlmostEqual(expect2,
419 photoCalib.instFluxToNanojansky(self.instFlux2, self.pointXShift))
420 self.assertFloatsAlmostEqual((expect2*u.nJy).to_value(u.ABmag),
421 photoCalib.instFluxToMagnitude(self.instFlux2, self.pointXShift))
423 # test converting to a photoCalib
424 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
425 self._testPhotoCalibCenter(photoCalib, self.calibrationErr)
427 # New catalog with a spatial component in the varying direction,
428 # to ensure the calculations on a catalog properly handle non-constant BF.
429 # NOTE: only the first quantity of the result (nJy or mags) should change.
430 catalog = self.catalog.copy(deep=True)
431 catalog[0].set('centroid_x', self.pointXShift[0])
432 catalog[0].set('centroid_y', self.pointXShift[1])
433 errFlux1 = computeNanojanskyErr(self.instFluxErr1,
434 self.instFlux1,
435 self.calibrationErr,
436 calibration,
437 expect)
438 errMag1 = computeMagnitudeErr(self.instFluxErr1,
439 self.instFlux1,
440 self.calibrationErr,
441 calibration,
442 expect)
443 # re-use the same instFluxErr1 for instFlux2.
444 errFlux2 = computeNanojanskyErr(self.instFluxErr1,
445 self.instFlux2,
446 self.calibrationErr,
447 self.calibration,
448 self.flux2)
449 errMag2 = computeMagnitudeErr(self.instFluxErr1,
450 self.instFlux2,
451 self.calibrationErr,
452 self.calibration,
453 self.flux2)
454 expectNanojansky = np.array([[expect, errFlux1], [self.flux2, errFlux2]])
455 expectMag = np.array([[(expect*u.nJy).to_value(u.ABmag), errMag1], [self.mag2, errMag2]])
456 self._testSourceCatalog(photoCalib, catalog, expectNanojansky, expectMag)
458 def testComputeScaledCalibration(self):
459 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, bbox=self.bbox)
460 scaledCalib = lsst.afw.image.PhotoCalib(photoCalib.computeScaledCalibration())
461 self.assertEqual(1, scaledCalib.instFluxToNanojansky(self.instFlux1)*photoCalib.getCalibrationMean())
462 self.assertEqual(photoCalib.instFluxToNanojansky(self.instFlux1),
463 scaledCalib.instFluxToNanojansky(self.instFlux1)*photoCalib.getCalibrationMean())
465 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration)
466 scaledCalib = lsst.afw.image.PhotoCalib(photoCalib.computeScaledCalibration())
468 self.assertEqual(1, scaledCalib.instFluxToNanojansky(self.instFlux1*self.calibration))
469 self.assertEqual(photoCalib.instFluxToNanojansky(self.instFlux1),
470 scaledCalib.instFluxToNanojansky(self.instFlux1)*photoCalib.getCalibrationMean())
472 @unittest.skip("Not yet implemented: see DM-10154")
473 def testComputeScalingTo(self):
474 photoCalib1 = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr, bbox=self.bbox)
475 photoCalib2 = lsst.afw.image.PhotoCalib(self.calibration*500, self.calibrationErr, bbox=self.bbox)
476 scaling = photoCalib1.computeScalingTo(photoCalib2)(self.pointXShift)
477 self.assertEqual(photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
478 photoCalib2.instFluxToNanojansky(self.instFlux1, self.pointXShift))
480 photoCalib3 = lsst.afw.image.PhotoCalib(self.constantCalibration, self.calibrationErr)
481 scaling = photoCalib1.computeScalingTo(photoCalib3)(self.pointXShift)
482 self.assertEqual(photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
483 photoCalib3.instFluxToNanojansky(self.instFlux1, self.pointXShift))
484 scaling = photoCalib3.computeScalingTo(photoCalib1)(self.pointXShift)
485 self.assertEqual(photoCalib3.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
486 photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift))
488 photoCalib4 = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
489 scaling = photoCalib1.computeScalingTo(photoCalib4)(self.pointXShift)
490 self.assertEqual(photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
491 photoCalib4.instFluxToNanojansky(self.instFlux1, self.pointXShift))
492 scaling = photoCalib4.computeScalingTo(photoCalib1)(self.pointXShift)
493 self.assertEqual(photoCalib4.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
494 photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift))
496 # Don't allow division of BoundedFields with different bounding boxes
497 photoCalibNoBBox = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr)
498 with self.assertRaises(lsst.pex.exceptions.DomainError):
499 scaling = photoCalibNoBBox.computeScalingTo(photoCalib1)
500 with self.assertRaises(lsst.pex.exceptions.DomainError):
501 scaling = photoCalibNoBBox.computeScalingTo(photoCalib4)
502 with self.assertRaises(lsst.pex.exceptions.DomainError):
503 scaling = photoCalib1.computeScalingTo(photoCalibNoBBox)
505 def _testPersistence(self, photoCalib):
506 with lsst.utils.tests.getTempFilePath(".fits") as filename:
507 photoCalib.writeFits(filename)
508 result = lsst.afw.image.PhotoCalib.readFits(filename)
509 self.assertEqual(result, photoCalib)
511 def testPersistence(self):
512 photoCalib = lsst.afw.image.PhotoCalib(self.calibration)
513 self._testPersistence(photoCalib)
515 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr)
516 self._testPersistence(photoCalib)
518 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr, self.bbox)
519 self._testPersistence(photoCalib)
521 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration, self.calibrationErr)
522 self._testPersistence(photoCalib)
524 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
525 self._testPersistence(photoCalib)
527 def testPersistenceVersions(self):
528 """Test that different versions are handled appropriately."""
529 # the values that were persisted in this photoCalib
530 mean = 123
531 err = 45
532 dataDir = os.path.join(os.path.split(__file__)[0], "data")
534 # implicit version 0 should raise (no longer compatible)
535 filePath = os.path.join(dataDir, "photoCalib-noversion.fits")
536 with self.assertRaises(RuntimeError):
537 photoCalib = lsst.afw.image.PhotoCalib.readFits(filePath)
539 # explicit version 0 should raise (no longer compatible)
540 filePath = os.path.join(dataDir, "photoCalib-version0.fits")
541 with self.assertRaises(RuntimeError):
542 photoCalib = lsst.afw.image.PhotoCalib.readFits(filePath)
544 # explicit version 1
545 filePath = os.path.join(dataDir, "photoCalib-version1.fits")
546 photoCalib = lsst.afw.image.PhotoCalib.readFits(filePath)
547 self.assertEqual(photoCalib.getCalibrationMean(), mean)
548 self.assertEqual(photoCalib.getCalibrationErr(), err)
550 def testPhotoCalibEquality(self):
551 photoCalib1 = lsst.afw.image.PhotoCalib(self.linearXCalibration, 0.5)
552 photoCalib2 = lsst.afw.image.PhotoCalib(self.linearXCalibration, 0.5)
553 photoCalib3 = lsst.afw.image.PhotoCalib(5, 0.5)
554 photoCalib4 = lsst.afw.image.PhotoCalib(5, 0.5)
555 photoCalib5 = lsst.afw.image.PhotoCalib(5)
556 photoCalib6 = lsst.afw.image.PhotoCalib(self.linearXCalibration)
557 photoCalib7 = lsst.afw.image.PhotoCalib(self.calibration, 0.5)
558 photoCalib8 = lsst.afw.image.PhotoCalib(self.constantCalibration, 0.5)
560 constantCalibration = lsst.afw.math.ChebyshevBoundedField(self.bbox, np.array([[self.calibration]]))
561 photoCalib9 = lsst.afw.image.PhotoCalib(constantCalibration, 0.5)
563 self.assertEqual(photoCalib1, photoCalib1)
564 self.assertEqual(photoCalib1, photoCalib2)
565 self.assertEqual(photoCalib3, photoCalib4)
566 self.assertEqual(photoCalib8, photoCalib9)
568 self.assertNotEqual(photoCalib1, photoCalib6)
569 self.assertNotEqual(photoCalib1, photoCalib7)
570 self.assertNotEqual(photoCalib1, photoCalib3)
571 self.assertNotEqual(photoCalib3, photoCalib5)
572 self.assertNotEqual(photoCalib1, photoCalib8)
574 self.assertFalse(photoCalib1 != photoCalib2) # using assertFalse to directly test != operator
576 def setupImage(self):
577 dim = (5, 6)
578 npDim = (dim[1], dim[0]) # numpy and afw have a different x/y order
579 sigma = 10.0
580 image = np.random.normal(loc=1000.0, scale=sigma, size=npDim).astype(np.float32)
581 mask = np.zeros(npDim, dtype=np.int32)
582 variance = (np.random.normal(loc=0.0, scale=sigma, size=npDim).astype(np.float32))**2
583 maskedImage = lsst.afw.image.basicUtils.makeMaskedImageFromArrays(image, mask, variance)
584 maskedImage.mask[0, 0] = True # set one mask bit to check propagation of mask bits.
586 return npDim, maskedImage, image, mask, variance
588 def testCalibrateImageConstant(self):
589 """Test a spatially-constant calibration."""
590 npDim, maskedImage, image, mask, variance = self.setupImage()
591 outImage = maskedImage.image.getArray()*self.calibration
592 expect = makeCalibratedMaskedImage(image, mask, variance, outImage,
593 self.calibration, self.calibrationErr)
594 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr)
595 result = photoCalib.calibrateImage(maskedImage)
596 self.assertMaskedImagesAlmostEqual(expect, result)
598 # same test, but without using the calibration error
599 expect = makeCalibratedMaskedImageNoCalibrationError(image, mask, variance, outImage)
600 result = photoCalib.calibrateImage(maskedImage, includeScaleUncertainty=False)
601 self.assertMaskedImagesAlmostEqual(expect, result)
603 def testCalibrateImageNonConstant(self):
604 """Test a spatially-varying calibration."""
605 npDim, maskedImage, image, mask, variance = self.setupImage()
606 xIndex, yIndex = np.indices(npDim, dtype=np.float64)
607 # y then x, as afw order and np order are flipped
608 calibration = self.linearXCalibration.evaluate(yIndex.flatten(), xIndex.flatten()).reshape(npDim)
609 outImage = maskedImage.image.getArray()*calibration # element-wise product, not matrix product
610 expect = makeCalibratedMaskedImage(image, mask, variance, outImage, calibration, self.calibrationErr)
612 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
613 result = photoCalib.calibrateImage(maskedImage)
614 self.assertMaskedImagesAlmostEqual(expect, result)
616 # same test, but without using the calibration error
617 expect = makeCalibratedMaskedImageNoCalibrationError(image, mask, variance, outImage)
618 result = photoCalib.calibrateImage(maskedImage, includeScaleUncertainty=False)
619 self.assertMaskedImagesAlmostEqual(expect, result)
621 def testCalibrateImageNonConstantSubimage(self):
622 """Test a non-constant calibration on a sub-image, to ensure we're
623 handling xy0 correctly.
624 """
625 npDim, maskedImage, image, mask, variance = self.setupImage()
626 xIndex, yIndex = np.indices(npDim, dtype=np.float64)
627 calibration = self.linearXCalibration.evaluate(yIndex.flatten(), xIndex.flatten()).reshape(npDim)
629 outImage = maskedImage.image.getArray()*calibration # element-wise product, not matrix product
630 expect = makeCalibratedMaskedImage(image, mask, variance, outImage, calibration, self.calibrationErr)
632 subBox = lsst.geom.Box2I(lsst.geom.Point2I(2, 4), lsst.geom.Point2I(4, 5))
633 subImage = maskedImage.subset(subBox)
634 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
635 result = photoCalib.calibrateImage(subImage)
636 self.assertMaskedImagesAlmostEqual(expect.subset(subBox), result)
638 # same test, but without using the calibration error
639 expect = makeCalibratedMaskedImageNoCalibrationError(image, mask, variance, outImage)
640 result = photoCalib.calibrateImage(maskedImage, includeScaleUncertainty=False)
641 self.assertMaskedImagesAlmostEqual(expect, result)
643 def testNonPositiveMeans(self):
644 # no negative calibrations
645 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
646 lsst.afw.image.PhotoCalib(-1.0)
647 # no negative errors
648 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
649 lsst.afw.image.PhotoCalib(1.0, -1.0)
651 # no negative calibration mean when computed from the bounded field
652 negativeCalibration = lsst.afw.math.ChebyshevBoundedField(self.bbox,
653 np.array([[-self.calibration]]))
654 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
655 lsst.afw.image.PhotoCalib(negativeCalibration)
656 # no negative calibration error
657 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
658 lsst.afw.image.PhotoCalib(self.constantCalibration, -1.0)
660 # no negative explicit calibration mean
661 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
662 lsst.afw.image.PhotoCalib(-1.0, 0, self.constantCalibration, True)
663 # no negative calibration error
664 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
665 lsst.afw.image.PhotoCalib(1.0, -1.0, self.constantCalibration, True)
667 def testPositiveErrors(self):
668 """The errors should always be positive, regardless of whether the
669 input flux is negative (as can happen in difference imaging).
670 This tests and fixes tickets/DM-16696.
671 """
672 photoCalib = lsst.afw.image.PhotoCalib(self.calibration)
673 result = photoCalib.instFluxToNanojansky(-100, 10)
674 self.assertGreater(result.error, 0)
676 def testMakePhotoCalibFromMetadata(self):
677 """Test creating a PhotoCalib from the Calib FITS metadata.
678 """
679 fluxMag0 = 12345
680 metadata = lsst.daf.base.PropertySet()
681 metadata.set('FLUXMAG0', fluxMag0)
683 photoCalib = lsst.afw.image.makePhotoCalibFromMetadata(metadata)
684 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
685 self.assertEqual(photoCalib.getCalibrationErr(), 0.0)
686 # keys aren't deleted by default
687 self.assertIn('FLUXMAG0', metadata)
689 # Test reading with the error keyword
690 fluxMag0Err = 6789
691 metadata.set('FLUXMAG0ERR', fluxMag0Err)
692 # The reference flux is "nanoJanskys at 0 magnitude".
693 referenceFlux = (0*u.ABmag).to_value(u.nJy)
694 calibrationErr = referenceFlux*fluxMag0Err/fluxMag0**2
695 photoCalib = lsst.afw.image.makePhotoCalibFromMetadata(metadata)
696 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
697 self.assertFloatsAlmostEqual(photoCalib.getCalibrationErr(), calibrationErr)
698 # keys aren't deleted by default
699 self.assertIn('FLUXMAG0', metadata)
700 self.assertIn('FLUXMAG0ERR', metadata)
702 # test stripping keys from a new metadata
703 metadata = lsst.daf.base.PropertySet()
704 metadata.set('FLUXMAG0', fluxMag0)
705 photoCalib = lsst.afw.image.makePhotoCalibFromMetadata(metadata, strip=True)
706 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
707 self.assertEqual(photoCalib.getCalibrationErr(), 0.0)
708 self.assertNotIn('FLUXMAG0', metadata)
710 metadata.set('FLUXMAG0', fluxMag0)
711 metadata.set('FLUXMAG0ERR', fluxMag0Err)
712 photoCalib = lsst.afw.image.makePhotoCalibFromMetadata(metadata, strip=True)
713 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
714 self.assertFloatsAlmostEqual(photoCalib.getCalibrationErr(), calibrationErr)
715 self.assertNotIn('FLUXMAG0', metadata)
716 self.assertNotIn('FLUXMAG0ERR', metadata)
718 def testMakePhotoCalibFromMetadataNoKey(self):
719 """Return None if the metadata does not contain a 'FLUXMAG0' key."""
720 metadata = lsst.daf.base.PropertySet()
721 metadata.set('something', 1000)
722 metadata.set('FLUXMAG0ERR', 5)
723 result = lsst.afw.image.makePhotoCalibFromMetadata(metadata)
724 self.assertIsNone(result)
726 def testMakePhotoCalibFromCalibZeroPoint(self):
727 """Test creating from the Calib-style fluxMag0/fluxMag0Err values."""
728 fluxMag0 = 12345
729 fluxMag0Err = 67890
731 referenceFlux = (0*u.ABmag).to_value(u.nJy)
732 calibrationErr = referenceFlux*fluxMag0Err/fluxMag0**2
734 # create with all zeros
735 photoCalib = lsst.afw.image.makePhotoCalibFromCalibZeroPoint(0, 0)
736 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), 0)
737 self.assertEqual(photoCalib.getCalibrationMean(), np.inf)
738 self.assertTrue(np.isnan(photoCalib.getCalibrationErr()))
740 # create with non-zero fluxMag0, but zero err
741 photoCalib = lsst.afw.image.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0)
742 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
743 self.assertEqual(photoCalib.getCalibrationErr(), 0.0)
745 # create with non-zero fluxMag0 and err
746 photoCalib = lsst.afw.image.makePhotoCalibFromCalibZeroPoint(fluxMag0, fluxMag0Err)
747 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
748 self.assertFloatsAlmostEqual(photoCalib.getCalibrationErr(), calibrationErr)
751class MemoryTester(lsst.utils.tests.MemoryTestCase):
752 pass
755def setup_module(module):
756 lsst.utils.tests.init()
759if __name__ == "__main__": 759 ↛ 760line 759 didn't jump to line 760, because the condition on line 759 was never true
760 lsst.utils.tests.init()
761 unittest.main()