Coverage for tests/test_photoCalib.py: 9%

436 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2024-03-20 00:40 -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# 

22 

23import os.path 

24import unittest 

25 

26import numpy as np 

27 

28import astropy.units as u 

29 

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 

37 

38 

39def computeNanojanskyErr(instFluxErr, instFlux, calibrationErr, calibration, flux): 

40 """Return the error on the flux (nanojansky).""" 

41 return flux*np.hypot(instFluxErr/instFlux, calibrationErr/calibration) 

42 

43 

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 

48 

49 

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) 

60 

61 

62def makeCalibratedMaskedImageNoCalibrationError(image, mask, variance, outImage): 

63 """Return a MaskedImage using outImage, mask, and a computed variance image. 

64 

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) 

75 

76 

77class PhotoCalibTestCase(lsst.utils.tests.TestCase): 

78 

79 def setUp(self): 

80 np.random.seed(100) 

81 

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)) 

86 

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) 

94 

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) 

99 

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) 

144 

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]])) 

150 

151 def tearDown(self): 

152 del self.schema 

153 del self.table 

154 del self.catalog 

155 

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. 

161 

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()) 

169 

170 # test with a "trivial" flux 

171 self.assertEqual(self.flux1, photoCalib.instFluxToNanojansky(self.instFlux1)) 

172 self.assertEqual(self.mag1, photoCalib.instFluxToMagnitude(self.instFlux1)) 

173 

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)) 

180 

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) 

193 

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) 

207 

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) 

220 

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) 

233 

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) 

242 

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) 

246 

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) 

250 

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) 

260 

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()) 

266 

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) 

291 

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) 

297 

298 # test getLocalCalibrationArray 

299 localCalib2 = photoCalib.getLocalCalibrationArray( 

300 pointXShiftXArray, 

301 pointXShiftYArray 

302 ) 

303 self.assertFloatsAlmostEqual(localCalib2, localCalib) 

304 

305 def _testSourceCatalog(self, photoCalib, catalog, expectNanojansky, expectMag): 

306 """Test instFluxTo*(sourceCatalog, ...), and calibrateCatalog().""" 

307 

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) 

313 

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() 

318 

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) 

325 

326 testCat = catalog.copy(deep=True) 

327 photoCalib.instFluxToMagnitude(testCat, self.instFluxKeyName, self.instFluxKeyName) 

328 checkCatalog(testCat, expectMag, self.instFluxKeyName, "mag") 

329 

330 testCat = catalog.copy(deep=True) 

331 photoCalib.instFluxToNanojansky(testCat, self.instFluxKeyName, self.instFluxKeyName) 

332 checkCatalog(testCat, expectNanojansky, self.instFluxKeyName, "flux") 

333 

334 testCat = catalog.copy(deep=True) 

335 photoCalib.instFluxToMagnitude(testCat, self.instFluxKeyName, self.instFluxKeyName) 

336 checkCatalog(testCat, expectMag, self.instFluxKeyName, "mag") 

337 

338 # test returning a calibrated catalog with calibrateCatalog 

339 

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"]) 

343 

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") 

349 

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) 

360 

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) 

365 

366 # Test _isConstant 

367 self.assertTrue(photoCalib._isConstant) 

368 

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) 

374 

375 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr) 

376 self._testPhotoCalibCenter(photoCalib, self.calibrationErr) 

377 

378 # test converting to a photoCalib 

379 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, bbox=self.bbox) 

380 self._testPhotoCalibCenter(photoCalib, 0) 

381 

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) 

386 

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)) 

394 

395 # test converting to a photoCalib 

396 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration, self.calibrationErr) 

397 self._testPhotoCalibCenter(photoCalib, self.calibrationErr) 

398 

399 # test _isConstant (bounded field is not constant) 

400 self.assertFalse(photoCalib._isConstant) 

401 

402 def testLinearXBoundedField(self): 

403 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration) 

404 self._testPhotoCalibCenter(photoCalib, 0) 

405 

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)) 

409 

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)) 

422 

423 # test converting to a photoCalib 

424 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr) 

425 self._testPhotoCalibCenter(photoCalib, self.calibrationErr) 

426 

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) 

457 

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()) 

464 

465 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration) 

466 scaledCalib = lsst.afw.image.PhotoCalib(photoCalib.computeScaledCalibration()) 

467 

468 self.assertEqual(1, scaledCalib.instFluxToNanojansky(self.instFlux1*self.calibration)) 

469 self.assertEqual(photoCalib.instFluxToNanojansky(self.instFlux1), 

470 scaledCalib.instFluxToNanojansky(self.instFlux1)*photoCalib.getCalibrationMean()) 

471 

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)) 

479 

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)) 

487 

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)) 

495 

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) 

504 

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) 

510 

511 def testPersistence(self): 

512 photoCalib = lsst.afw.image.PhotoCalib(self.calibration) 

513 self._testPersistence(photoCalib) 

514 

515 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr) 

516 self._testPersistence(photoCalib) 

517 

518 photoCalib = lsst.afw.image.PhotoCalib(self.calibration, self.calibrationErr, self.bbox) 

519 self._testPersistence(photoCalib) 

520 

521 photoCalib = lsst.afw.image.PhotoCalib(self.constantCalibration, self.calibrationErr) 

522 self._testPersistence(photoCalib) 

523 

524 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr) 

525 self._testPersistence(photoCalib) 

526 

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") 

533 

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) 

538 

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) 

543 

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) 

549 

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) 

559 

560 constantCalibration = lsst.afw.math.ChebyshevBoundedField(self.bbox, np.array([[self.calibration]])) 

561 photoCalib9 = lsst.afw.image.PhotoCalib(constantCalibration, 0.5) 

562 

563 self.assertEqual(photoCalib1, photoCalib1) 

564 self.assertEqual(photoCalib1, photoCalib2) 

565 self.assertEqual(photoCalib3, photoCalib4) 

566 self.assertEqual(photoCalib8, photoCalib9) 

567 

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) 

573 

574 self.assertFalse(photoCalib1 != photoCalib2) # using assertFalse to directly test != operator 

575 

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. 

585 

586 return npDim, maskedImage, image, mask, variance 

587 

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) 

597 

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) 

602 

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) 

611 

612 photoCalib = lsst.afw.image.PhotoCalib(self.linearXCalibration, self.calibrationErr) 

613 result = photoCalib.calibrateImage(maskedImage) 

614 self.assertMaskedImagesAlmostEqual(expect, result) 

615 

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) 

620 

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) 

628 

629 outImage = maskedImage.image.getArray()*calibration # element-wise product, not matrix product 

630 expect = makeCalibratedMaskedImage(image, mask, variance, outImage, calibration, self.calibrationErr) 

631 

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) 

637 

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) 

642 

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) 

650 

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) 

659 

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) 

666 

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) 

675 

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) 

682 

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) 

688 

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) 

701 

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) 

709 

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) 

717 

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) 

725 

726 def testMakePhotoCalibFromCalibZeroPoint(self): 

727 """Test creating from the Calib-style fluxMag0/fluxMag0Err values.""" 

728 fluxMag0 = 12345 

729 fluxMag0Err = 67890 

730 

731 referenceFlux = (0*u.ABmag).to_value(u.nJy) 

732 calibrationErr = referenceFlux*fluxMag0Err/fluxMag0**2 

733 

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())) 

739 

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) 

744 

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) 

749 

750 

751class MemoryTester(lsst.utils.tests.MemoryTestCase): 

752 pass 

753 

754 

755def setup_module(module): 

756 lsst.utils.tests.init() 

757 

758 

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()