Coverage for tests/test_photoCalib.py: 10%
422 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-05 17:50 -0800
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-05 17:50 -0800
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 mag = photoCalib.instFluxToMagnitude(self.instFlux2, self.pointXShift)
257 self.assertFloatsAlmostEqual(self.instFlux2,
258 photoCalib.magnitudeToInstFlux(mag, self.pointXShift),
259 rtol=1e-15)
261 # test getLocalCalibration.
262 meas = photoCalib.instFluxToNanojansky(self.instFlux1, self.instFluxErr1, self.pointXShift)
263 localCalib = photoCalib.getLocalCalibration(self.pointXShift)
264 flux = localCalib * self.instFlux1
265 self.assertAlmostEqual(meas.value, flux)
267 def _testSourceCatalog(self, photoCalib, catalog, expectNanojansky, expectMag):
268 """Test instFluxTo*(sourceCatalog, ...), and calibrateCatalog()."""
270 # test calculations on a sourceCatalog, returning the array
271 result = photoCalib.instFluxToNanojansky(catalog, self.instFluxKeyName)
272 self.assertFloatsAlmostEqual(expectNanojansky, result)
273 result = photoCalib.instFluxToMagnitude(catalog, self.instFluxKeyName)
274 self.assertFloatsAlmostEqual(expectMag, result)
276 # Test modifying the catalog in-place with instFluxToNanojansky/instFluxToMagnitude
277 # The original instFluxes shouldn't change: save them to test that.
278 origInstFlux = catalog[self.instFluxKeyName+'_instFlux'].copy()
279 origInstFluxErr = catalog[self.instFluxKeyName+'_instFluxErr'].copy()
281 def checkCatalog(catalog, expect, keyName, outField):
282 """Test that the fields in the catalog are correct."""
283 self.assertFloatsAlmostEqual(catalog[keyName+'_%s' % outField], expect[:, 0])
284 self.assertFloatsAlmostEqual(catalog[keyName+'_%sErr' % outField], expect[:, 1])
285 self.assertFloatsAlmostEqual(catalog[keyName+'_instFlux'], origInstFlux)
286 self.assertFloatsAlmostEqual(catalog[keyName+'_instFluxErr'], origInstFluxErr)
288 testCat = catalog.copy(deep=True)
289 photoCalib.instFluxToMagnitude(testCat, self.instFluxKeyName, self.instFluxKeyName)
290 checkCatalog(testCat, expectMag, self.instFluxKeyName, "mag")
292 testCat = catalog.copy(deep=True)
293 photoCalib.instFluxToNanojansky(testCat, self.instFluxKeyName, self.instFluxKeyName)
294 checkCatalog(testCat, expectNanojansky, self.instFluxKeyName, "flux")
296 testCat = catalog.copy(deep=True)
297 photoCalib.instFluxToMagnitude(testCat, self.instFluxKeyName, self.instFluxKeyName)
298 checkCatalog(testCat, expectMag, self.instFluxKeyName, "mag")
300 # test returning a calibrated catalog with calibrateCatalog
302 # test that trying to calibrate a non-existent flux field raises
303 with self.assertRaises(lsst.pex.exceptions.NotFoundError):
304 photoCalib.calibrateCatalog(testCat, ["NotARealFluxFieldName"])
306 # test calibrating just one flux field
307 testCat = catalog.copy(deep=True)
308 result = photoCalib.calibrateCatalog(testCat, [self.otherInstFluxKeyName])
309 checkCatalog(result, expectNanojansky, self.otherInstFluxKeyName, "flux")
310 checkCatalog(result, expectMag, self.otherInstFluxKeyName, "mag")
312 # test calibrating all of the flux fields
313 testCat = catalog.copy(deep=True)
314 result = photoCalib.calibrateCatalog(testCat)
315 checkCatalog(result, expectNanojansky, self.instFluxKeyName, "flux")
316 checkCatalog(result, expectMag, self.instFluxKeyName, "mag")
317 checkCatalog(result, expectNanojansky, self.otherInstFluxKeyName, "flux")
318 checkCatalog(result, expectMag, self.otherInstFluxKeyName, "mag")
319 self.assertFloatsAlmostEqual(result[self.noErrInstFluxKeyName+'_flux'], expectNanojansky[:, 0])
320 self.assertFloatsAlmostEqual(result[self.noErrInstFluxKeyName+'_mag'], expectMag[:, 0])
321 self.assertFloatsAlmostEqual(result[self.noErrInstFluxKeyName+'_instFlux'], origInstFlux)
323 def testNonVarying(self):
324 """Test constructing with a constant calibration factor."""
325 photoCalib = lsst.afw.image.PhotoCalib(self.calibration)
326 self._testPhotoCalibCenter(photoCalib, 0)
328 # test on positions off the center (position should not matter)
329 self.assertEqual(self.flux1, photoCalib.instFluxToNanojansky(self.instFlux1, self.pointXShift))
330 self.assertEqual(self.mag1, photoCalib.instFluxToMagnitude(self.instFlux1, self.pointXShift))
331 result = photoCalib.instFluxToNanojansky(self.instFlux1, self.instFluxErr1)
332 self.assertEqual(self.flux1, result.value)
334 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr)
335 self._testPhotoCalibCenter(photoCalib, self.calibrationErr)
337 # test converting to a photoCalib
338 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, bbox=self.bbox)
339 self._testPhotoCalibCenter(photoCalib, 0)
341 def testConstantBoundedField(self):
342 """Test constructing with a spatially-constant bounded field."""
343 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration)
344 self._testPhotoCalibCenter(photoCalib, 0)
346 # test on positions off the center (position should not matter)
347 self.assertEqual(self.flux1, photoCalib.instFluxToNanojansky(self.instFlux1, self.pointYShift))
348 self.assertEqual(self.mag1, photoCalib.instFluxToMagnitude(self.instFlux1, self.pointYShift))
349 self.assertFloatsAlmostEqual(self.flux2,
350 photoCalib.instFluxToNanojansky(self.instFlux2, self.pointXShift))
351 self.assertFloatsAlmostEqual(self.mag2,
352 photoCalib.instFluxToMagnitude(self.instFlux2, self.pointXShift))
354 # test converting to a photoCalib
355 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration, self.calibrationErr)
356 self._testPhotoCalibCenter(photoCalib, self.calibrationErr)
358 def testLinearXBoundedField(self):
359 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration)
360 self._testPhotoCalibCenter(photoCalib, 0)
362 # test on positions off the center (Y position should not matter)
363 self.assertEqual(self.flux1, photoCalib.instFluxToNanojansky(self.instFlux1, self.pointYShift))
364 self.assertEqual(self.mag1, photoCalib.instFluxToMagnitude(self.instFlux1, self.pointYShift))
366 # test on positions off the center (X position does matter)
367 calibration = (self.calibration + self.pointXShift.getX()*self.calibration/(self.bbox.getWidth()/2.))
368 expect = self.instFlux1*calibration
369 self.assertFloatsAlmostEqual(expect,
370 photoCalib.instFluxToNanojansky(self.instFlux1, self.pointXShift))
371 self.assertFloatsAlmostEqual((expect*u.nJy).to_value(u.ABmag),
372 photoCalib.instFluxToMagnitude(self.instFlux1, self.pointXShift))
373 expect2 = self.instFlux2*calibration
374 self.assertFloatsAlmostEqual(expect2,
375 photoCalib.instFluxToNanojansky(self.instFlux2, self.pointXShift))
376 self.assertFloatsAlmostEqual((expect2*u.nJy).to_value(u.ABmag),
377 photoCalib.instFluxToMagnitude(self.instFlux2, self.pointXShift))
379 # test converting to a photoCalib
380 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
381 self._testPhotoCalibCenter(photoCalib, self.calibrationErr)
383 # New catalog with a spatial component in the varying direction,
384 # to ensure the calculations on a catalog properly handle non-constant BF.
385 # NOTE: only the first quantity of the result (nJy or mags) should change.
386 catalog = self.catalog.copy(deep=True)
387 catalog[0].set('centroid_x', self.pointXShift[0])
388 catalog[0].set('centroid_y', self.pointXShift[1])
389 errFlux1 = computeNanojanskyErr(self.instFluxErr1,
390 self.instFlux1,
391 self.calibrationErr,
392 calibration,
393 expect)
394 errMag1 = computeMagnitudeErr(self.instFluxErr1,
395 self.instFlux1,
396 self.calibrationErr,
397 calibration,
398 expect)
399 # re-use the same instFluxErr1 for instFlux2.
400 errFlux2 = computeNanojanskyErr(self.instFluxErr1,
401 self.instFlux2,
402 self.calibrationErr,
403 self.calibration,
404 self.flux2)
405 errMag2 = computeMagnitudeErr(self.instFluxErr1,
406 self.instFlux2,
407 self.calibrationErr,
408 self.calibration,
409 self.flux2)
410 expectNanojansky = np.array([[expect, errFlux1], [self.flux2, errFlux2]])
411 expectMag = np.array([[(expect*u.nJy).to_value(u.ABmag), errMag1], [self.mag2, errMag2]])
412 self._testSourceCatalog(photoCalib, catalog, expectNanojansky, expectMag)
414 def testComputeScaledCalibration(self):
415 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, bbox=self.bbox)
416 scaledCalib = lsst.afw.image.PhotoCalib(photoCalib.computeScaledCalibration())
417 self.assertEqual(1, scaledCalib.instFluxToNanojansky(self.instFlux1)*photoCalib.getCalibrationMean())
418 self.assertEqual(photoCalib.instFluxToNanojansky(self.instFlux1),
419 scaledCalib.instFluxToNanojansky(self.instFlux1)*photoCalib.getCalibrationMean())
421 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration)
422 scaledCalib = lsst.afw.image.PhotoCalib(photoCalib.computeScaledCalibration())
424 self.assertEqual(1, scaledCalib.instFluxToNanojansky(self.instFlux1*self.calibration))
425 self.assertEqual(photoCalib.instFluxToNanojansky(self.instFlux1),
426 scaledCalib.instFluxToNanojansky(self.instFlux1)*photoCalib.getCalibrationMean())
428 @unittest.skip("Not yet implemented: see DM-10154")
429 def testComputeScalingTo(self):
430 photoCalib1 = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr, bbox=self.bbox)
431 photoCalib2 = lsst.afw.image.PhotoCalib(self.calibration*500, self.calibrationErr, bbox=self.bbox)
432 scaling = photoCalib1.computeScalingTo(photoCalib2)(self.pointXShift)
433 self.assertEqual(photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
434 photoCalib2.instFluxToNanojansky(self.instFlux1, self.pointXShift))
436 photoCalib3 = lsst.afw.image.PhotoCalib(self.constantCalibration, self.calibrationErr)
437 scaling = photoCalib1.computeScalingTo(photoCalib3)(self.pointXShift)
438 self.assertEqual(photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
439 photoCalib3.instFluxToNanojansky(self.instFlux1, self.pointXShift))
440 scaling = photoCalib3.computeScalingTo(photoCalib1)(self.pointXShift)
441 self.assertEqual(photoCalib3.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
442 photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift))
444 photoCalib4 = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
445 scaling = photoCalib1.computeScalingTo(photoCalib4)(self.pointXShift)
446 self.assertEqual(photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
447 photoCalib4.instFluxToNanojansky(self.instFlux1, self.pointXShift))
448 scaling = photoCalib4.computeScalingTo(photoCalib1)(self.pointXShift)
449 self.assertEqual(photoCalib4.instFluxToNanojansky(self.instFlux1, self.pointXShift)*scaling,
450 photoCalib1.instFluxToNanojansky(self.instFlux1, self.pointXShift))
452 # Don't allow division of BoundedFields with different bounding boxes
453 photoCalibNoBBox = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr)
454 with self.assertRaises(lsst.pex.exceptions.DomainError):
455 scaling = photoCalibNoBBox.computeScalingTo(photoCalib1)
456 with self.assertRaises(lsst.pex.exceptions.DomainError):
457 scaling = photoCalibNoBBox.computeScalingTo(photoCalib4)
458 with self.assertRaises(lsst.pex.exceptions.DomainError):
459 scaling = photoCalib1.computeScalingTo(photoCalibNoBBox)
461 def _testPersistence(self, photoCalib):
462 with lsst.utils.tests.getTempFilePath(".fits") as filename:
463 photoCalib.writeFits(filename)
464 result = lsst.afw.image.PhotoCalib.readFits(filename)
465 self.assertEqual(result, photoCalib)
467 def testPersistence(self):
468 photoCalib = lsst.afw.image.PhotoCalib(self.calibration)
469 self._testPersistence(photoCalib)
471 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr)
472 self._testPersistence(photoCalib)
474 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr, self.bbox)
475 self._testPersistence(photoCalib)
477 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration, self.calibrationErr)
478 self._testPersistence(photoCalib)
480 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
481 self._testPersistence(photoCalib)
483 def testPersistenceVersions(self):
484 """Test that different versions are handled appropriately."""
485 # the values that were persisted in this photoCalib
486 mean = 123
487 err = 45
488 dataDir = os.path.join(os.path.split(__file__)[0], "data")
490 # implicit version 0 should raise (no longer compatible)
491 filePath = os.path.join(dataDir, "photoCalib-noversion.fits")
492 with self.assertRaises(RuntimeError):
493 photoCalib = lsst.afw.image.PhotoCalib.readFits(filePath)
495 # explicit version 0 should raise (no longer compatible)
496 filePath = os.path.join(dataDir, "photoCalib-version0.fits")
497 with self.assertRaises(RuntimeError):
498 photoCalib = lsst.afw.image.PhotoCalib.readFits(filePath)
500 # explicit version 1
501 filePath = os.path.join(dataDir, "photoCalib-version1.fits")
502 photoCalib = lsst.afw.image.PhotoCalib.readFits(filePath)
503 self.assertEqual(photoCalib.getCalibrationMean(), mean)
504 self.assertEqual(photoCalib.getCalibrationErr(), err)
506 def testPhotoCalibEquality(self):
507 photoCalib1 = lsst.afw.image.PhotoCalib(self.linearXCalibration, 0.5)
508 photoCalib2 = lsst.afw.image.PhotoCalib(self.linearXCalibration, 0.5)
509 photoCalib3 = lsst.afw.image.PhotoCalib(5, 0.5)
510 photoCalib4 = lsst.afw.image.PhotoCalib(5, 0.5)
511 photoCalib5 = lsst.afw.image.PhotoCalib(5)
512 photoCalib6 = lsst.afw.image.PhotoCalib(self.linearXCalibration)
513 photoCalib7 = lsst.afw.image.PhotoCalib(self.calibration, 0.5)
514 photoCalib8 = lsst.afw.image.PhotoCalib(self.constantCalibration, 0.5)
516 constantCalibration = lsst.afw.math.ChebyshevBoundedField(self.bbox, np.array([[self.calibration]]))
517 photoCalib9 = lsst.afw.image.PhotoCalib(constantCalibration, 0.5)
519 self.assertEqual(photoCalib1, photoCalib1)
520 self.assertEqual(photoCalib1, photoCalib2)
521 self.assertEqual(photoCalib3, photoCalib4)
522 self.assertEqual(photoCalib8, photoCalib9)
524 self.assertNotEqual(photoCalib1, photoCalib6)
525 self.assertNotEqual(photoCalib1, photoCalib7)
526 self.assertNotEqual(photoCalib1, photoCalib3)
527 self.assertNotEqual(photoCalib3, photoCalib5)
528 self.assertNotEqual(photoCalib1, photoCalib8)
530 self.assertFalse(photoCalib1 != photoCalib2) # using assertFalse to directly test != operator
532 def setupImage(self):
533 dim = (5, 6)
534 npDim = (dim[1], dim[0]) # numpy and afw have a different x/y order
535 sigma = 10.0
536 image = np.random.normal(loc=1000.0, scale=sigma, size=npDim).astype(np.float32)
537 mask = np.zeros(npDim, dtype=np.int32)
538 variance = (np.random.normal(loc=0.0, scale=sigma, size=npDim).astype(np.float32))**2
539 maskedImage = lsst.afw.image.basicUtils.makeMaskedImageFromArrays(image, mask, variance)
540 maskedImage.mask[0, 0] = True # set one mask bit to check propagation of mask bits.
542 return npDim, maskedImage, image, mask, variance
544 def testCalibrateImageConstant(self):
545 """Test a spatially-constant calibration."""
546 npDim, maskedImage, image, mask, variance = self.setupImage()
547 outImage = maskedImage.image.getArray()*self.calibration
548 expect = makeCalibratedMaskedImage(image, mask, variance, outImage,
549 self.calibration, self.calibrationErr)
550 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr)
551 result = photoCalib.calibrateImage(maskedImage)
552 self.assertMaskedImagesAlmostEqual(expect, result)
554 # same test, but without using the calibration error
555 expect = makeCalibratedMaskedImageNoCalibrationError(image, mask, variance, outImage)
556 result = photoCalib.calibrateImage(maskedImage, includeScaleUncertainty=False)
557 self.assertMaskedImagesAlmostEqual(expect, result)
559 def testCalibrateImageNonConstant(self):
560 """Test a spatially-varying calibration."""
561 npDim, maskedImage, image, mask, variance = self.setupImage()
562 xIndex, yIndex = np.indices(npDim, dtype=np.float64)
563 # y then x, as afw order and np order are flipped
564 calibration = self.linearXCalibration.evaluate(yIndex.flatten(), xIndex.flatten()).reshape(npDim)
565 outImage = maskedImage.image.getArray()*calibration # element-wise product, not matrix product
566 expect = makeCalibratedMaskedImage(image, mask, variance, outImage, calibration, self.calibrationErr)
568 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
569 result = photoCalib.calibrateImage(maskedImage)
570 self.assertMaskedImagesAlmostEqual(expect, result)
572 # same test, but without using the calibration error
573 expect = makeCalibratedMaskedImageNoCalibrationError(image, mask, variance, outImage)
574 result = photoCalib.calibrateImage(maskedImage, includeScaleUncertainty=False)
575 self.assertMaskedImagesAlmostEqual(expect, result)
577 def testCalibrateImageNonConstantSubimage(self):
578 """Test a non-constant calibration on a sub-image, to ensure we're
579 handling xy0 correctly.
580 """
581 npDim, maskedImage, image, mask, variance = self.setupImage()
582 xIndex, yIndex = np.indices(npDim, dtype=np.float64)
583 calibration = self.linearXCalibration.evaluate(yIndex.flatten(), xIndex.flatten()).reshape(npDim)
585 outImage = maskedImage.image.getArray()*calibration # element-wise product, not matrix product
586 expect = makeCalibratedMaskedImage(image, mask, variance, outImage, calibration, self.calibrationErr)
588 subBox = lsst.geom.Box2I(lsst.geom.Point2I(2, 4), lsst.geom.Point2I(4, 5))
589 subImage = maskedImage.subset(subBox)
590 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr)
591 result = photoCalib.calibrateImage(subImage)
592 self.assertMaskedImagesAlmostEqual(expect.subset(subBox), result)
594 # same test, but without using the calibration error
595 expect = makeCalibratedMaskedImageNoCalibrationError(image, mask, variance, outImage)
596 result = photoCalib.calibrateImage(maskedImage, includeScaleUncertainty=False)
597 self.assertMaskedImagesAlmostEqual(expect, result)
599 def testNonPositiveMeans(self):
600 # no negative calibrations
601 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
602 lsst.afw.image.PhotoCalib(-1.0)
603 # no negative errors
604 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
605 lsst.afw.image.PhotoCalib(1.0, -1.0)
607 # no negative calibration mean when computed from the bounded field
608 negativeCalibration = lsst.afw.math.ChebyshevBoundedField(self.bbox,
609 np.array([[-self.calibration]]))
610 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
611 lsst.afw.image.PhotoCalib(negativeCalibration)
612 # no negative calibration error
613 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
614 lsst.afw.image.PhotoCalib(self.constantCalibration, -1.0)
616 # no negative explicit calibration mean
617 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
618 lsst.afw.image.PhotoCalib(-1.0, 0, self.constantCalibration, True)
619 # no negative calibration error
620 with(self.assertRaises(lsst.pex.exceptions.InvalidParameterError)):
621 lsst.afw.image.PhotoCalib(1.0, -1.0, self.constantCalibration, True)
623 def testPositiveErrors(self):
624 """The errors should always be positive, regardless of whether the
625 input flux is negative (as can happen in difference imaging).
626 This tests and fixes tickets/DM-16696.
627 """
628 photoCalib = lsst.afw.image.PhotoCalib(self.calibration)
629 result = photoCalib.instFluxToNanojansky(-100, 10)
630 self.assertGreater(result.error, 0)
632 def testMakePhotoCalibFromMetadata(self):
633 """Test creating a PhotoCalib from the Calib FITS metadata.
634 """
635 fluxMag0 = 12345
636 metadata = lsst.daf.base.PropertySet()
637 metadata.set('FLUXMAG0', fluxMag0)
639 photoCalib = lsst.afw.image.makePhotoCalibFromMetadata(metadata)
640 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
641 self.assertEqual(photoCalib.getCalibrationErr(), 0.0)
642 # keys aren't deleted by default
643 self.assertIn('FLUXMAG0', metadata)
645 # Test reading with the error keyword
646 fluxMag0Err = 6789
647 metadata.set('FLUXMAG0ERR', fluxMag0Err)
648 # The reference flux is "nanoJanskys at 0 magnitude".
649 referenceFlux = (0*u.ABmag).to_value(u.nJy)
650 calibrationErr = referenceFlux*fluxMag0Err/fluxMag0**2
651 photoCalib = lsst.afw.image.makePhotoCalibFromMetadata(metadata)
652 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
653 self.assertFloatsAlmostEqual(photoCalib.getCalibrationErr(), calibrationErr)
654 # keys aren't deleted by default
655 self.assertIn('FLUXMAG0', metadata)
656 self.assertIn('FLUXMAG0ERR', metadata)
658 # test stripping keys from a new metadata
659 metadata = lsst.daf.base.PropertySet()
660 metadata.set('FLUXMAG0', fluxMag0)
661 photoCalib = lsst.afw.image.makePhotoCalibFromMetadata(metadata, strip=True)
662 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
663 self.assertEqual(photoCalib.getCalibrationErr(), 0.0)
664 self.assertNotIn('FLUXMAG0', metadata)
666 metadata.set('FLUXMAG0', fluxMag0)
667 metadata.set('FLUXMAG0ERR', fluxMag0Err)
668 photoCalib = lsst.afw.image.makePhotoCalibFromMetadata(metadata, strip=True)
669 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
670 self.assertFloatsAlmostEqual(photoCalib.getCalibrationErr(), calibrationErr)
671 self.assertNotIn('FLUXMAG0', metadata)
672 self.assertNotIn('FLUXMAG0ERR', metadata)
674 def testMakePhotoCalibFromMetadataNoKey(self):
675 """Return None if the metadata does not contain a 'FLUXMAG0' key."""
676 metadata = lsst.daf.base.PropertySet()
677 metadata.set('something', 1000)
678 metadata.set('FLUXMAG0ERR', 5)
679 result = lsst.afw.image.makePhotoCalibFromMetadata(metadata)
680 self.assertIsNone(result)
682 def testMakePhotoCalibFromCalibZeroPoint(self):
683 """Test creating from the Calib-style fluxMag0/fluxMag0Err values."""
684 fluxMag0 = 12345
685 fluxMag0Err = 67890
687 referenceFlux = (0*u.ABmag).to_value(u.nJy)
688 calibrationErr = referenceFlux*fluxMag0Err/fluxMag0**2
690 # create with all zeros
691 photoCalib = lsst.afw.image.makePhotoCalibFromCalibZeroPoint(0, 0)
692 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), 0)
693 self.assertEqual(photoCalib.getCalibrationMean(), np.inf)
694 self.assertTrue(np.isnan(photoCalib.getCalibrationErr()))
696 # create with non-zero fluxMag0, but zero err
697 photoCalib = lsst.afw.image.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0)
698 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
699 self.assertEqual(photoCalib.getCalibrationErr(), 0.0)
701 # create with non-zero fluxMag0 and err
702 photoCalib = lsst.afw.image.makePhotoCalibFromCalibZeroPoint(fluxMag0, fluxMag0Err)
703 self.assertEqual(photoCalib.getInstFluxAtZeroMagnitude(), fluxMag0)
704 self.assertFloatsAlmostEqual(photoCalib.getCalibrationErr(), calibrationErr)
707class MemoryTester(lsst.utils.tests.MemoryTestCase):
708 pass
711def setup_module(module):
712 lsst.utils.tests.init()
715if __name__ == "__main__": 715 ↛ 716line 715 didn't jump to line 716, because the condition on line 715 was never true
716 lsst.utils.tests.init()
717 unittest.main()