Coverage for tests/test_exposure.py: 12%

724 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-29 01:21 -0700

1# This file is part of afw. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22""" 

23Test lsst.afw.image.Exposure 

24""" 

25 

26import dataclasses 

27import os.path 

28import unittest 

29import warnings 

30 

31import numpy as np 

32from numpy.testing import assert_allclose 

33import yaml 

34import astropy.units as units 

35import astropy.io.fits 

36 

37import lsst.utils 

38import lsst.utils.tests 

39import lsst.geom 

40import lsst.afw.image as afwImage 

41from lsst.afw.coord import Weather 

42import lsst.afw.geom as afwGeom 

43import lsst.afw.table as afwTable 

44import lsst.pex.exceptions as pexExcept 

45from lsst.afw.fits import readMetadata, FitsError 

46from lsst.afw.cameraGeom.testUtils import DetectorWrapper 

47from lsst.daf.base import PropertyList 

48from lsst.log import Log 

49from testTableArchivesLib import DummyPsf 

50 

51TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

52Log.getLogger("lsst.afw.image.Mask").setLevel(Log.INFO) 

53 

54try: 

55 dataDir = os.path.join(lsst.utils.getPackageDir("afwdata"), "data") 

56except LookupError: 

57 dataDir = None 

58else: 

59 InputMaskedImageName = "871034p_1_MI.fits" 

60 InputMaskedImageNameSmall = "small_MI.fits" 

61 InputImageNameSmall = "small" 

62 OutputMaskedImageName = "871034p_1_MInew.fits" 

63 

64 currDir = os.path.abspath(os.path.dirname(__file__)) 

65 inFilePath = os.path.join(dataDir, InputMaskedImageName) 

66 inFilePathSmall = os.path.join(dataDir, InputMaskedImageNameSmall) 

67 inFilePathSmallImage = os.path.join(dataDir, InputImageNameSmall) 

68 

69 

70@unittest.skipIf(dataDir is None, "afwdata not setup") 

71class ExposureTestCase(lsst.utils.tests.TestCase): 

72 """ 

73 A test case for the Exposure Class 

74 """ 

75 

76 def setUp(self): 

77 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

78 maskedImageMD = readMetadata(inFilePathSmall) 

79 

80 self.smallExposure = afwImage.ExposureF(inFilePathSmall) 

81 self.width = maskedImage.getWidth() 

82 self.height = maskedImage.getHeight() 

83 self.wcs = afwGeom.makeSkyWcs(maskedImageMD, False) 

84 self.md = maskedImageMD 

85 self.psf = DummyPsf(2.0) 

86 self.detector = DetectorWrapper().detector 

87 self.id = 42 

88 self.extras = {"MISC": DummyPsf(3.5)} 

89 

90 self.exposureBlank = afwImage.ExposureF() 

91 self.exposureMiOnly = afwImage.makeExposure(maskedImage) 

92 self.exposureMiWcs = afwImage.makeExposure(maskedImage, self.wcs) 

93 # n.b. the (100, 100, ...) form 

94 self.exposureCrWcs = afwImage.ExposureF(100, 100, self.wcs) 

95 # test with ExtentI(100, 100) too 

96 self.exposureCrOnly = afwImage.ExposureF(lsst.geom.ExtentI(100, 100)) 

97 

98 def tearDown(self): 

99 del self.smallExposure 

100 del self.wcs 

101 del self.psf 

102 del self.detector 

103 del self.extras 

104 

105 del self.exposureBlank 

106 del self.exposureMiOnly 

107 del self.exposureMiWcs 

108 del self.exposureCrWcs 

109 del self.exposureCrOnly 

110 

111 def testGetMaskedImage(self): 

112 """ 

113 Test to ensure a MaskedImage can be obtained from each 

114 Exposure. An Exposure is required to have a MaskedImage, 

115 therefore each of the Exposures should return a MaskedImage. 

116 

117 MaskedImage class should throw appropriate 

118 lsst::pex::exceptions::NotFound if the MaskedImage can not be 

119 obtained. 

120 """ 

121 maskedImageBlank = self.exposureBlank.getMaskedImage() 

122 blankWidth = maskedImageBlank.getWidth() 

123 blankHeight = maskedImageBlank.getHeight() 

124 if blankWidth != blankHeight != 0: 

125 self.fail(f"{blankWidth} = {blankHeight} != 0") 

126 

127 maskedImageMiOnly = self.exposureMiOnly.getMaskedImage() 

128 miOnlyWidth = maskedImageMiOnly.getWidth() 

129 miOnlyHeight = maskedImageMiOnly.getHeight() 

130 self.assertAlmostEqual(miOnlyWidth, self.width) 

131 self.assertAlmostEqual(miOnlyHeight, self.height) 

132 

133 # NOTE: Unittests for Exposures created from a MaskedImage and 

134 # a WCS object are incomplete. No way to test the validity of 

135 # the WCS being copied/created. 

136 

137 maskedImageMiWcs = self.exposureMiWcs.getMaskedImage() 

138 miWcsWidth = maskedImageMiWcs.getWidth() 

139 miWcsHeight = maskedImageMiWcs.getHeight() 

140 self.assertAlmostEqual(miWcsWidth, self.width) 

141 self.assertAlmostEqual(miWcsHeight, self.height) 

142 

143 maskedImageCrWcs = self.exposureCrWcs.getMaskedImage() 

144 crWcsWidth = maskedImageCrWcs.getWidth() 

145 crWcsHeight = maskedImageCrWcs.getHeight() 

146 if crWcsWidth != crWcsHeight != 0: 

147 self.fail(f"{crWcsWidth} != {crWcsHeight} != 0") 

148 

149 maskedImageCrOnly = self.exposureCrOnly.getMaskedImage() 

150 crOnlyWidth = maskedImageCrOnly.getWidth() 

151 crOnlyHeight = maskedImageCrOnly.getHeight() 

152 if crOnlyWidth != crOnlyHeight != 0: 

153 self.fail(f"{crOnlyWidth} != {crOnlyHeight} != 0") 

154 

155 # Check Exposure.getWidth() returns the MaskedImage's width 

156 self.assertEqual(crOnlyWidth, self.exposureCrOnly.getWidth()) 

157 self.assertEqual(crOnlyHeight, self.exposureCrOnly.getHeight()) 

158 # check width/height properties 

159 self.assertEqual(crOnlyWidth, self.exposureCrOnly.width) 

160 self.assertEqual(crOnlyHeight, self.exposureCrOnly.height) 

161 

162 def testProperties(self): 

163 self.assertMaskedImagesEqual(self.exposureMiOnly.maskedImage, 

164 self.exposureMiOnly.getMaskedImage()) 

165 mi2 = afwImage.MaskedImageF(self.exposureMiOnly.getDimensions()) 

166 mi2.image.array[:] = 5.0 

167 mi2.variance.array[:] = 3.0 

168 mi2.mask.array[:] = 0x1 

169 self.exposureMiOnly.maskedImage = mi2 

170 self.assertMaskedImagesEqual(self.exposureMiOnly.maskedImage, mi2) 

171 self.assertImagesEqual(self.exposureMiOnly.image, 

172 self.exposureMiOnly.maskedImage.image) 

173 

174 image3 = afwImage.ImageF(self.exposureMiOnly.getDimensions()) 

175 image3.array[:] = 3.0 

176 self.exposureMiOnly.image = image3 

177 self.assertImagesEqual(self.exposureMiOnly.image, image3) 

178 

179 mask3 = afwImage.MaskX(self.exposureMiOnly.getDimensions()) 

180 mask3.array[:] = 0x2 

181 self.exposureMiOnly.mask = mask3 

182 self.assertMasksEqual(self.exposureMiOnly.mask, mask3) 

183 

184 var3 = afwImage.ImageF(self.exposureMiOnly.getDimensions()) 

185 var3.array[:] = 2.0 

186 self.exposureMiOnly.variance = var3 

187 self.assertImagesEqual(self.exposureMiOnly.variance, var3) 

188 

189 # Test the property getter for a null VisitInfo. 

190 self.assertIsNone(self.exposureMiOnly.visitInfo) 

191 

192 def testGetWcs(self): 

193 """Test that a WCS can be obtained from each Exposure created with 

194 a WCS, and that an Exposure lacking a WCS returns None. 

195 """ 

196 # These exposures don't contain a WCS 

197 self.assertIsNone(self.exposureBlank.getWcs()) 

198 self.assertIsNone(self.exposureMiOnly.getWcs()) 

199 self.assertIsNone(self.exposureCrOnly.getWcs()) 

200 

201 # These exposures should contain a WCS 

202 self.assertEqual(self.wcs, self.exposureMiWcs.getWcs()) 

203 self.assertEqual(self.wcs, self.exposureCrWcs.getWcs()) 

204 

205 def testExposureInfoConstructor(self): 

206 """Test the Exposure(maskedImage, exposureInfo) constructor""" 

207 exposureInfo = afwImage.ExposureInfo() 

208 exposureInfo.setWcs(self.wcs) 

209 exposureInfo.setDetector(self.detector) 

210 gFilterLabel = afwImage.FilterLabel(band="g") 

211 exposureInfo.setFilter(gFilterLabel) 

212 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

213 exposure = afwImage.ExposureF(maskedImage, exposureInfo) 

214 

215 self.assertTrue(exposure.hasWcs()) 

216 self.assertEqual(exposure.getWcs().getPixelOrigin(), 

217 self.wcs.getPixelOrigin()) 

218 self.assertEqual(exposure.getDetector().getName(), 

219 self.detector.getName()) 

220 self.assertEqual(exposure.getDetector().getSerial(), 

221 self.detector.getSerial()) 

222 self.assertEqual(exposure.getFilter(), gFilterLabel) 

223 

224 self.assertTrue(exposure.getInfo().hasWcs()) 

225 # check the ExposureInfo property 

226 self.assertTrue(exposure.info.hasWcs()) 

227 self.assertEqual(exposure.getInfo().getWcs().getPixelOrigin(), 

228 self.wcs.getPixelOrigin()) 

229 self.assertEqual(exposure.getInfo().getDetector().getName(), 

230 self.detector.getName()) 

231 self.assertEqual(exposure.getInfo().getDetector().getSerial(), 

232 self.detector.getSerial()) 

233 self.assertEqual(exposure.getInfo().getFilter(), gFilterLabel) 

234 

235 def testNullWcs(self): 

236 """Test that an Exposure constructed with second argument None is usable 

237 

238 When the exposureInfo constructor was first added, trying to get a WCS 

239 or other info caused a segfault because the ExposureInfo did not exist. 

240 """ 

241 maskedImage = self.exposureMiOnly.getMaskedImage() 

242 exposure = afwImage.ExposureF(maskedImage, None) 

243 self.assertFalse(exposure.hasWcs()) 

244 self.assertFalse(exposure.hasPsf()) 

245 

246 def testExposureInfoSetNone(self): 

247 exposureInfo = afwImage.ExposureInfo() 

248 exposureInfo.setDetector(None) 

249 exposureInfo.setValidPolygon(None) 

250 exposureInfo.setPsf(None) 

251 exposureInfo.setWcs(None) 

252 exposureInfo.setPhotoCalib(None) 

253 exposureInfo.setCoaddInputs(None) 

254 exposureInfo.setVisitInfo(None) 

255 exposureInfo.setApCorrMap(None) 

256 for key in self.extras: 

257 exposureInfo.setComponent(key, None) 

258 

259 def testSetExposureInfo(self): 

260 exposureInfo = afwImage.ExposureInfo() 

261 exposureInfo.setWcs(self.wcs) 

262 exposureInfo.setDetector(self.detector) 

263 gFilterLabel = afwImage.FilterLabel(band="g") 

264 exposureInfo.setFilter(gFilterLabel) 

265 exposureInfo.setId(self.id) 

266 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

267 exposure = afwImage.ExposureF(maskedImage) 

268 self.assertFalse(exposure.hasWcs()) 

269 

270 exposure.setInfo(exposureInfo) 

271 

272 self.assertTrue(exposure.hasWcs()) 

273 self.assertEqual(exposure.getWcs().getPixelOrigin(), 

274 self.wcs.getPixelOrigin()) 

275 self.assertEqual(exposure.getDetector().getName(), 

276 self.detector.getName()) 

277 self.assertEqual(exposure.getDetector().getSerial(), 

278 self.detector.getSerial()) 

279 self.assertEqual(exposure.getFilter(), gFilterLabel) 

280 

281 # test properties 

282 self.assertEqual(exposure.detector.getName(), self.detector.getName()) 

283 self.assertEqual(exposure.filter, gFilterLabel) 

284 self.assertEqual(exposure.wcs, self.wcs) 

285 

286 def testVisitInfoFitsPersistence(self): 

287 """Test saving an exposure to FITS and reading it back in preserves (some) VisitInfo fields""" 

288 exposureTime = 12.3 

289 boresightRotAngle = 45.6 * lsst.geom.degrees 

290 weather = Weather(1.1, 2.2, 0.3) 

291 visitInfo = afwImage.VisitInfo( 

292 exposureTime=exposureTime, 

293 boresightRotAngle=boresightRotAngle, 

294 weather=weather, 

295 ) 

296 photoCalib = afwImage.PhotoCalib(3.4, 5.6) 

297 exposureInfo = afwImage.ExposureInfo() 

298 exposureInfo.setVisitInfo(visitInfo) 

299 exposureInfo.setPhotoCalib(photoCalib) 

300 exposureInfo.setDetector(self.detector) 

301 gFilterLabel = afwImage.FilterLabel(band="g") 

302 exposureInfo.setFilter(gFilterLabel) 

303 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

304 exposure = afwImage.ExposureF(maskedImage, exposureInfo) 

305 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

306 exposure.writeFits(tmpFile) 

307 rtExposure = afwImage.ExposureF(tmpFile) 

308 rtVisitInfo = rtExposure.getInfo().getVisitInfo() 

309 self.assertEqual(rtVisitInfo.getWeather(), weather) 

310 self.assertEqual(rtExposure.getPhotoCalib(), photoCalib) 

311 self.assertEqual(rtExposure.getFilter(), gFilterLabel) 

312 

313 # Test property getters. 

314 self.assertEqual(rtExposure.photoCalib, photoCalib) 

315 self.assertEqual(rtExposure.filter, gFilterLabel) 

316 # NOTE: we can't test visitInfo equality, because most fields are NaN. 

317 self.assertIsNotNone(rtExposure.visitInfo) 

318 

319 def testSetMembers(self): 

320 """ 

321 Test that the MaskedImage and the WCS of an Exposure can be set. 

322 """ 

323 exposure = afwImage.ExposureF() 

324 

325 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

326 exposure.setMaskedImage(maskedImage) 

327 exposure.setWcs(self.wcs) 

328 exposure.setDetector(self.detector) 

329 exposure.setFilter(afwImage.FilterLabel(band="g")) 

330 

331 self.assertEqual(exposure.getDetector().getName(), 

332 self.detector.getName()) 

333 self.assertEqual(exposure.getDetector().getSerial(), 

334 self.detector.getSerial()) 

335 self.assertEqual(exposure.getFilter().bandLabel, "g") 

336 self.assertEqual(exposure.getWcs(), self.wcs) 

337 

338 # The PhotoCalib tests are in test_photoCalib.py; 

339 # here we just check that it's gettable and settable. 

340 self.assertIsNone(exposure.getPhotoCalib()) 

341 

342 photoCalib = afwImage.PhotoCalib(511.1, 44.4) 

343 exposure.setPhotoCalib(photoCalib) 

344 self.assertEqual(exposure.getPhotoCalib(), photoCalib) 

345 

346 # Psfs next 

347 self.assertFalse(exposure.hasPsf()) 

348 exposure.setPsf(self.psf) 

349 self.assertTrue(exposure.hasPsf()) 

350 

351 exposure.setPsf(DummyPsf(1.0)) # we can reset the Psf 

352 

353 # extras next 

354 info = exposure.getInfo() 

355 for key, value in self.extras.items(): 

356 self.assertFalse(info.hasComponent(key)) 

357 self.assertIsNone(info.getComponent(key)) 

358 info.setComponent(key, value) 

359 self.assertTrue(info.hasComponent(key)) 

360 self.assertEqual(info.getComponent(key), value) 

361 info.removeComponent(key) 

362 self.assertFalse(info.hasComponent(key)) 

363 

364 # Test that we can set the MaskedImage and WCS of an Exposure 

365 # that already has both 

366 self.exposureMiWcs.setMaskedImage(maskedImage) 

367 exposure.setWcs(self.wcs) 

368 

369 def testHasWcs(self): 

370 """ 

371 Test if an Exposure has a WCS or not. 

372 """ 

373 self.assertFalse(self.exposureBlank.hasWcs()) 

374 

375 self.assertFalse(self.exposureMiOnly.hasWcs()) 

376 self.assertTrue(self.exposureMiWcs.hasWcs()) 

377 self.assertTrue(self.exposureCrWcs.hasWcs()) 

378 self.assertFalse(self.exposureCrOnly.hasWcs()) 

379 

380 def testGetSubExposure(self): 

381 """ 

382 Test that a subExposure of the original Exposure can be obtained. 

383 

384 The MaskedImage class should throw a 

385 lsst::pex::exceptions::InvalidParameter if the requested 

386 subRegion is not fully contained within the original 

387 MaskedImage. 

388 

389 """ 

390 # 

391 # This subExposure is valid 

392 # 

393 subBBox = lsst.geom.Box2I(lsst.geom.Point2I(40, 50), 

394 lsst.geom.Extent2I(10, 10)) 

395 subExposure = self.exposureCrWcs.Factory( 

396 self.exposureCrWcs, subBBox, afwImage.LOCAL) 

397 

398 self.checkWcs(self.exposureCrWcs, subExposure) 

399 

400 # this subRegion is not valid and should trigger an exception 

401 # from the MaskedImage class and should trigger an exception 

402 # from the WCS class for the MaskedImage 871034p_1_MI. 

403 

404 subRegion3 = lsst.geom.Box2I(lsst.geom.Point2I(100, 100), 

405 lsst.geom.Extent2I(10, 10)) 

406 

407 def getSubRegion(): 

408 self.exposureCrWcs.Factory( 

409 self.exposureCrWcs, subRegion3, afwImage.LOCAL) 

410 

411 self.assertRaises(pexExcept.LengthError, getSubRegion) 

412 

413 # this subRegion is not valid and should trigger an exception 

414 # from the MaskedImage class only for the MaskedImage small_MI. 

415 # small_MI (cols, rows) = (256, 256) 

416 

417 subRegion4 = lsst.geom.Box2I(lsst.geom.Point2I(250, 250), 

418 lsst.geom.Extent2I(10, 10)) 

419 

420 def getSubRegion(): 

421 self.exposureCrWcs.Factory( 

422 self.exposureCrWcs, subRegion4, afwImage.LOCAL) 

423 

424 self.assertRaises(pexExcept.LengthError, getSubRegion) 

425 

426 # check the sub- and parent- exposures are using the same Wcs 

427 # transformation 

428 subBBox = lsst.geom.Box2I(lsst.geom.Point2I(40, 50), 

429 lsst.geom.Extent2I(10, 10)) 

430 subExposure = self.exposureCrWcs.Factory( 

431 self.exposureCrWcs, subBBox, afwImage.LOCAL) 

432 parentSkyPos = self.exposureCrWcs.getWcs().pixelToSky(0, 0) 

433 

434 subExpSkyPos = subExposure.getWcs().pixelToSky(0, 0) 

435 

436 self.assertSpherePointsAlmostEqual(parentSkyPos, subExpSkyPos, msg="Wcs in sub image has changed") 

437 

438 def testReadWriteFits(self): 

439 """Test readFits and writeFits. 

440 """ 

441 # This should pass without an exception 

442 mainExposure = afwImage.ExposureF(inFilePathSmall) 

443 mainExposure.info.setId(self.id) 

444 mainExposure.setDetector(self.detector) 

445 

446 subBBox = lsst.geom.Box2I(lsst.geom.Point2I(10, 10), 

447 lsst.geom.Extent2I(40, 50)) 

448 subExposure = mainExposure.Factory( 

449 mainExposure, subBBox, afwImage.LOCAL) 

450 self.checkWcs(mainExposure, subExposure) 

451 det = subExposure.getDetector() 

452 self.assertTrue(det) 

453 

454 subExposure = afwImage.ExposureF( 

455 inFilePathSmall, subBBox, afwImage.LOCAL) 

456 

457 self.checkWcs(mainExposure, subExposure) 

458 

459 # This should throw an exception 

460 def getExposure(): 

461 afwImage.ExposureF(inFilePathSmallImage) 

462 

463 self.assertRaises(FitsError, getExposure) 

464 

465 mainExposure.setPsf(self.psf) 

466 

467 # Make sure we can write without an exception 

468 photoCalib = afwImage.PhotoCalib(1e-10, 1e-12) 

469 mainExposure.setPhotoCalib(photoCalib) 

470 

471 mainInfo = mainExposure.getInfo() 

472 for key, value in self.extras.items(): 

473 mainInfo.setComponent(key, value) 

474 

475 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

476 mainExposure.writeFits(tmpFile) 

477 

478 readExposure = type(mainExposure)(tmpFile) 

479 

480 # 

481 # Check the round-tripping 

482 # 

483 self.assertIsNotNone(mainExposure.getFilter()) 

484 self.assertEqual(mainExposure.getFilter(), 

485 readExposure.getFilter()) 

486 

487 self.assertEqual(photoCalib, readExposure.getPhotoCalib()) 

488 

489 readInfo = readExposure.getInfo() 

490 self.assertEqual(mainExposure.info.getId(), readInfo.id) 

491 for key, value in self.extras.items(): 

492 self.assertEqual(value, readInfo.getComponent(key)) 

493 

494 psf = readExposure.getPsf() 

495 self.assertIsNotNone(psf) 

496 self.assertEqual(psf, self.psf) 

497 # check psf property getter 

498 self.assertEqual(readExposure.psf, self.psf) 

499 

500 def checkWcs(self, parentExposure, subExposure): 

501 """Compare WCS at corner points of a sub-exposure and its parent exposure 

502 By using the function indexToPosition, we should be able to convert the indices 

503 (of the four corners (of the sub-exposure)) to positions and use the wcs 

504 to get the same sky coordinates for each. 

505 """ 

506 subMI = subExposure.getMaskedImage() 

507 subDim = subMI.getDimensions() 

508 

509 # Note: pixel positions must be computed relative to XY0 when working 

510 # with WCS 

511 mainWcs = parentExposure.getWcs() 

512 subWcs = subExposure.getWcs() 

513 

514 for xSubInd in (0, subDim.getX()-1): 

515 for ySubInd in (0, subDim.getY()-1): 

516 self.assertSpherePointsAlmostEqual( 

517 mainWcs.pixelToSky( 

518 afwImage.indexToPosition(xSubInd), 

519 afwImage.indexToPosition(ySubInd), 

520 ), 

521 subWcs.pixelToSky( 

522 afwImage.indexToPosition(xSubInd), 

523 afwImage.indexToPosition(ySubInd), 

524 )) 

525 

526 def cmpExposure(self, e1, e2): 

527 self.assertEqual(e1.getDetector().getName(), 

528 e2.getDetector().getName()) 

529 self.assertEqual(e1.getDetector().getSerial(), 

530 e2.getDetector().getSerial()) 

531 self.assertEqual(e1.getFilter(), e2.getFilter()) 

532 xy = lsst.geom.Point2D(0, 0) 

533 self.assertEqual(e1.getWcs().pixelToSky(xy)[0], 

534 e2.getWcs().pixelToSky(xy)[0]) 

535 self.assertEqual(e1.getPhotoCalib(), e2.getPhotoCalib()) 

536 # check PSF identity 

537 if not e1.getPsf(): 

538 self.assertFalse(e2.getPsf()) 

539 else: 

540 self.assertEqual(e1.getPsf(), e2.getPsf()) 

541 # Check extra components 

542 i1 = e1.getInfo() 

543 i2 = e2.getInfo() 

544 for key in self.extras: 

545 self.assertEqual(i1.hasComponent(key), i2.hasComponent(key)) 

546 if i1.hasComponent(key): 

547 self.assertEqual(i1.getComponent(key), i2.getComponent(key)) 

548 

549 def testCopyExposure(self): 

550 """Copy an Exposure (maybe changing type)""" 

551 

552 exposureU = afwImage.ExposureU(inFilePathSmall, allowUnsafe=True) 

553 exposureU.setWcs(self.wcs) 

554 exposureU.setDetector(self.detector) 

555 exposureU.setFilter(afwImage.FilterLabel(band="g")) 

556 exposureU.setPsf(DummyPsf(4.0)) 

557 infoU = exposureU.getInfo() 

558 for key, value in self.extras.items(): 

559 infoU.setComponent(key, value) 

560 

561 exposureF = exposureU.convertF() 

562 self.cmpExposure(exposureF, exposureU) 

563 

564 nexp = exposureF.Factory(exposureF, False) 

565 self.cmpExposure(exposureF, nexp) 

566 

567 # Ensure that the copy was deep. 

568 # (actually this test is invalid since getDetector() returns a shared_ptr) 

569 # cen0 = exposureU.getDetector().getCenterPixel() 

570 # x0,y0 = cen0 

571 # det = exposureF.getDetector() 

572 # det.setCenterPixel(lsst.geom.Point2D(999.0, 437.8)) 

573 # self.assertEqual(exposureU.getDetector().getCenterPixel()[0], x0) 

574 # self.assertEqual(exposureU.getDetector().getCenterPixel()[1], y0) 

575 

576 def testDeepCopyData(self): 

577 """Make sure a deep copy of an Exposure has its own data (ticket #2625) 

578 """ 

579 exp = afwImage.ExposureF(6, 7) 

580 mi = exp.getMaskedImage() 

581 mi.getImage().set(100) 

582 mi.getMask().set(5) 

583 mi.getVariance().set(200) 

584 

585 expCopy = exp.clone() 

586 miCopy = expCopy.getMaskedImage() 

587 miCopy.getImage().set(-50) 

588 miCopy.getMask().set(2) 

589 miCopy.getVariance().set(175) 

590 

591 self.assertFloatsAlmostEqual(miCopy.getImage().getArray(), -50) 

592 self.assertTrue(np.all(miCopy.getMask().getArray() == 2)) 

593 self.assertFloatsAlmostEqual(miCopy.getVariance().getArray(), 175) 

594 

595 self.assertFloatsAlmostEqual(mi.getImage().getArray(), 100) 

596 self.assertTrue(np.all(mi.getMask().getArray() == 5)) 

597 self.assertFloatsAlmostEqual(mi.getVariance().getArray(), 200) 

598 

599 def testDeepCopySubData(self): 

600 """Make sure a deep copy of a subregion of an Exposure has its own data (ticket #2625) 

601 """ 

602 exp = afwImage.ExposureF(6, 7) 

603 mi = exp.getMaskedImage() 

604 mi.getImage().set(100) 

605 mi.getMask().set(5) 

606 mi.getVariance().set(200) 

607 

608 bbox = lsst.geom.Box2I(lsst.geom.Point2I(1, 0), lsst.geom.Extent2I(5, 4)) 

609 expCopy = exp.Factory(exp, bbox, afwImage.PARENT, True) 

610 miCopy = expCopy.getMaskedImage() 

611 miCopy.getImage().set(-50) 

612 miCopy.getMask().set(2) 

613 miCopy.getVariance().set(175) 

614 

615 self.assertFloatsAlmostEqual(miCopy.getImage().getArray(), -50) 

616 self.assertTrue(np.all(miCopy.getMask().getArray() == 2)) 

617 self.assertFloatsAlmostEqual(miCopy.getVariance().getArray(), 175) 

618 

619 self.assertFloatsAlmostEqual(mi.getImage().getArray(), 100) 

620 self.assertTrue(np.all(mi.getMask().getArray() == 5)) 

621 self.assertFloatsAlmostEqual(mi.getVariance().getArray(), 200) 

622 

623 def testDeepCopyMetadata(self): 

624 """Make sure a deep copy of an Exposure has a deep copy of metadata (ticket #2568) 

625 """ 

626 exp = afwImage.ExposureF(10, 10) 

627 expMeta = exp.getMetadata() 

628 expMeta.set("foo", 5) 

629 expCopy = exp.clone() 

630 expCopyMeta = expCopy.getMetadata() 

631 expCopyMeta.set("foo", 6) 

632 self.assertEqual(expCopyMeta.getScalar("foo"), 6) 

633 # this will fail if the bug is present 

634 self.assertEqual(expMeta.getScalar("foo"), 5) 

635 

636 def testDeepCopySubMetadata(self): 

637 """Make sure a deep copy of a subregion of an Exposure has a deep copy of metadata (ticket #2568) 

638 """ 

639 exp = afwImage.ExposureF(10, 10) 

640 expMeta = exp.getMetadata() 

641 expMeta.set("foo", 5) 

642 bbox = lsst.geom.Box2I(lsst.geom.Point2I(1, 0), lsst.geom.Extent2I(5, 5)) 

643 expCopy = exp.Factory(exp, bbox, afwImage.PARENT, True) 

644 expCopyMeta = expCopy.getMetadata() 

645 expCopyMeta.set("foo", 6) 

646 self.assertEqual(expCopyMeta.getScalar("foo"), 6) 

647 # this will fail if the bug is present 

648 self.assertEqual(expMeta.getScalar("foo"), 5) 

649 

650 def testMakeExposureLeaks(self): 

651 """Test for memory leaks in makeExposure (the test is in lsst.utils.tests.MemoryTestCase)""" 

652 afwImage.makeMaskedImage(afwImage.ImageU(lsst.geom.Extent2I(10, 20))) 

653 afwImage.makeExposure(afwImage.makeMaskedImage( 

654 afwImage.ImageU(lsst.geom.Extent2I(10, 20)))) 

655 

656 def testImageSlices(self): 

657 """Test image slicing, which generate sub-images using Box2I under the covers""" 

658 exp = afwImage.ExposureF(10, 20) 

659 mi = exp.getMaskedImage() 

660 mi.image[9, 19] = 10 

661 # N.b. Exposures don't support setting/getting the pixels so can't 

662 # replicate e.g. Image's slice tests 

663 sexp = exp[1:4, 6:10] 

664 self.assertEqual(sexp.getDimensions(), lsst.geom.ExtentI(3, 4)) 

665 sexp = exp[:, -3:, afwImage.LOCAL] 

666 self.assertEqual(sexp.getDimensions(), 

667 lsst.geom.ExtentI(exp.getWidth(), 3)) 

668 self.assertEqual(sexp.maskedImage[-1, -1, afwImage.LOCAL], 

669 exp.maskedImage[-1, -1, afwImage.LOCAL]) 

670 

671 def testConversionToScalar(self): 

672 """Test that even 1-pixel Exposures can't be converted to scalars""" 

673 im = afwImage.ExposureF(10, 20) 

674 

675 # only single pixel images may be converted 

676 self.assertRaises(TypeError, float, im) 

677 # actually, can't convert (img, msk, var) to scalar 

678 self.assertRaises(TypeError, float, im[0, 0]) 

679 

680 def testReadMetadata(self): 

681 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

682 self.exposureCrWcs.getMetadata().set("FRAZZLE", True) 

683 # This will write the main metadata (inc. FRAZZLE) to the primary HDU, and the 

684 # WCS to subsequent HDUs, along with INHERIT=T. 

685 self.exposureCrWcs.writeFits(tmpFile) 

686 # This should read the first non-empty HDU (i.e. it skips the primary), but 

687 # goes back and reads it if it finds INHERIT=T. That should let us read 

688 # frazzle and the Wcs from the PropertySet returned by 

689 # testReadMetadata. 

690 md = readMetadata(tmpFile) 

691 wcs = afwGeom.makeSkyWcs(md, False) 

692 self.assertPairsAlmostEqual(wcs.getPixelOrigin(), self.wcs.getPixelOrigin()) 

693 self.assertSpherePointsAlmostEqual(wcs.getSkyOrigin(), self.wcs.getSkyOrigin()) 

694 assert_allclose(wcs.getCdMatrix(), self.wcs.getCdMatrix(), atol=1e-10) 

695 frazzle = md.getScalar("FRAZZLE") 

696 self.assertTrue(frazzle) 

697 

698 def testArchiveKeys(self): 

699 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

700 exposure1 = afwImage.ExposureF(100, 100, self.wcs) 

701 exposure1.setPsf(self.psf) 

702 exposure1.writeFits(tmpFile) 

703 exposure2 = afwImage.ExposureF(tmpFile) 

704 self.assertFalse(exposure2.getMetadata().exists("AR_ID")) 

705 self.assertFalse(exposure2.getMetadata().exists("PSF_ID")) 

706 self.assertFalse(exposure2.getMetadata().exists("WCS_ID")) 

707 

708 def testTicket2861(self): 

709 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

710 exposure1 = afwImage.ExposureF(100, 100, self.wcs) 

711 exposure1.setPsf(self.psf) 

712 schema = afwTable.ExposureTable.makeMinimalSchema() 

713 coaddInputs = afwImage.CoaddInputs(schema, schema) 

714 exposure1.getInfo().setCoaddInputs(coaddInputs) 

715 exposure2 = afwImage.ExposureF(exposure1, True) 

716 self.assertIsNotNone(exposure2.getInfo().getCoaddInputs()) 

717 exposure2.writeFits(tmpFile) 

718 exposure3 = afwImage.ExposureF(tmpFile) 

719 self.assertIsNotNone(exposure3.getInfo().getCoaddInputs()) 

720 

721 def testGetCutoutSky(self): 

722 """Test we can get cutouts in sky coordinates, so long as there is a 

723 valid WCS. 

724 """ 

725 wcs = self.smallExposure.getWcs() 

726 

727 dimensions = [lsst.geom.Extent2I(100, 50), lsst.geom.Extent2I(15, 15), lsst.geom.Extent2I(0, 10), 

728 lsst.geom.Extent2I(25, 30), lsst.geom.Extent2I(15, -5), 

729 2*self.smallExposure.getDimensions()] 

730 locations = [("center", self._getExposureCenter(self.smallExposure)), 

731 ("edge", wcs.pixelToSky(lsst.geom.Point2D(0, 0))), 

732 ("rounding test", wcs.pixelToSky(lsst.geom.Point2D(0.2, 0.7))), 

733 ("just inside", wcs.pixelToSky(lsst.geom.Point2D(-0.5 + 1e-4, -0.5 + 1e-4))), 

734 ("just outside", wcs.pixelToSky(lsst.geom.Point2D(-0.5 - 1e-4, -0.5 - 1e-4))), 

735 ("outside", wcs.pixelToSky(lsst.geom.Point2D(-1000, -1000)))] 

736 for cutoutSize in dimensions: 

737 for label, cutoutCenter in locations: 

738 msg = 'Cutout size = %s, location = %s' % (cutoutSize, label) 

739 if "outside" not in label and all(cutoutSize.gt(0)): 

740 cutout = self.smallExposure.getCutout(cutoutCenter, cutoutSize) 

741 centerInPixels = wcs.skyToPixel(cutoutCenter) 

742 precision = (1 + 1e-4)*np.sqrt(0.5)*wcs.getPixelScale(centerInPixels) 

743 self._checkCutoutProperties(cutout, cutoutSize, cutoutCenter, precision, msg) 

744 self._checkCutoutPixels( 

745 cutout, 

746 self._getValidCorners(self.smallExposure.getBBox(), cutout.getBBox()), 

747 msg) 

748 

749 # Need a valid WCS 

750 with self.assertRaises(pexExcept.LogicError, msg=msg): 

751 self.exposureMiOnly.getCutout(cutoutCenter, cutoutSize) 

752 else: 

753 with self.assertRaises(pexExcept.InvalidParameterError, msg=msg): 

754 self.smallExposure.getCutout(cutoutCenter, cutoutSize) 

755 

756 def testGetCutoutPixel(self): 

757 """Test that we can get cutouts in pixel coordinates, even if the 

758 extent is off the edge of the image, even if there is no WCS. 

759 """ 

760 dimensions = [lsst.geom.Extent2I(100, 50), lsst.geom.Extent2I(15, 15), lsst.geom.Extent2I(0, 10), 

761 lsst.geom.Extent2I(25, 30), lsst.geom.Extent2I(15, -5), 

762 2*self.exposureMiOnly.getDimensions()] 

763 locations = [("center", lsst.geom.Box2D(self.exposureMiOnly.getBBox()).getCenter()), 

764 ("edge", lsst.geom.Point2D(0, 0)), 

765 ("rounding test", lsst.geom.Point2D(0.2, 0.7)), 

766 ("just inside", lsst.geom.Point2D(-0.5 + 1e-4, -0.5 + 1e-4)), 

767 # These two should raise; center must be within image box. 

768 ("just outside", lsst.geom.Point2D(-0.5 - 1e-4, -0.5 - 1e-4)), 

769 ("outside", lsst.geom.Point2D(-1000, -1000))] 

770 for cutoutSize in dimensions: 

771 for label, cutoutCenter in locations: 

772 msg = 'Cutout size = %s, location = %s' % (cutoutSize, label) 

773 if "outside" not in label and all(cutoutSize.gt(0)): 

774 cutout = self.exposureMiOnly.getCutout(cutoutCenter, cutoutSize) 

775 self._checkCutoutPixels( 

776 cutout, 

777 self._getValidCorners(self.exposureMiOnly.getBBox(), cutout.getBBox()), 

778 msg) 

779 

780 # Same result even if there is a wcs. 

781 cutoutWithWcs = self.smallExposure.getCutout(cutoutCenter, cutoutSize) 

782 self.assertMaskedImagesEqual(cutout.maskedImage, cutoutWithWcs.maskedImage) 

783 

784 # Getting a cutout with a bbox should produce the same result. 

785 box = lsst.geom.Box2I.makeCenteredBox(cutoutCenter, lsst.geom.Extent2I(cutoutSize)) 

786 cutoutBox2I = self.exposureMiOnly.getCutout(box) 

787 self.assertMaskedImagesEqual(cutout.maskedImage, cutoutBox2I.maskedImage) 

788 else: 

789 with self.assertRaises(pexExcept.InvalidParameterError, msg=msg): 

790 self.exposureMiOnly.getCutout(cutoutCenter, cutoutSize) 

791 

792 def testGetConvexPolygon(self): 

793 """Test the convex polygon.""" 

794 # Check that we do not have a convex polygon for the plain exposure. 

795 self.assertIsNone(self.exposureMiOnly.convex_polygon) 

796 

797 # Check that all the points in the padded bounding box are in the polygon 

798 bbox = self.exposureMiWcs.getBBox() 

799 # Grow by the default padding. 

800 bbox.grow(10) 

801 x, y = np.meshgrid(np.arange(bbox.getBeginX(), bbox.getEndX(), dtype=np.float64), 

802 np.arange(bbox.getBeginY(), bbox.getEndY(), dtype=np.float64)) 

803 wcs = self.exposureMiWcs.wcs 

804 ra, dec = wcs.pixelToSkyArray(x.ravel(), 

805 y.ravel()) 

806 

807 poly = self.exposureMiWcs.convex_polygon 

808 contains = poly.contains(ra, dec) 

809 np.testing.assert_array_equal(contains, np.ones(len(contains), dtype=bool)) 

810 

811 # Check that points one pixel outside of the bounding box are not in the polygon 

812 bbox.grow(1) 

813 

814 ra, dec = wcs.pixelToSkyArray( 

815 np.linspace(bbox.getBeginX(), bbox.getEndX(), 100), 

816 np.full(100, bbox.getBeginY())) 

817 contains = poly.contains(ra, dec) 

818 np.testing.assert_array_equal(contains, np.zeros(len(contains), dtype=bool)) 

819 

820 ra, dec = wcs.pixelToSkyArray( 

821 np.linspace(bbox.getBeginX(), bbox.getEndX(), 100), 

822 np.full(100, bbox.getEndY())) 

823 contains = poly.contains(ra, dec) 

824 np.testing.assert_array_equal(contains, np.zeros(len(contains), dtype=bool)) 

825 

826 ra, dec = wcs.pixelToSkyArray( 

827 np.full(100, bbox.getBeginX()), 

828 np.linspace(bbox.getBeginY(), bbox.getEndY(), 100)) 

829 contains = poly.contains(ra, dec) 

830 np.testing.assert_array_equal(contains, np.zeros(len(contains), dtype=bool)) 

831 

832 ra, dec = wcs.pixelToSkyArray( 

833 np.full(100, bbox.getEndX()), 

834 np.linspace(bbox.getBeginY(), bbox.getEndY(), 100)) 

835 contains = poly.contains(ra, dec) 

836 np.testing.assert_array_equal(contains, np.zeros(len(contains), dtype=bool)) 

837 

838 def testContainsSkyCoords(self): 

839 """Test the sky coord containment code.""" 

840 self.assertRaisesRegex(ValueError, 

841 "Exposure does not have a valid WCS", 

842 self.exposureMiOnly.containsSkyCoords, 

843 0.0, 

844 0.0) 

845 

846 # Check that all the points within the bounding box are contained 

847 bbox = self.exposureMiWcs.getBBox() 

848 x, y = np.meshgrid(np.arange(bbox.getBeginX() + 1, bbox.getEndX() - 1), 

849 np.arange(bbox.getBeginY() + 1, bbox.getEndY() - 1)) 

850 wcs = self.exposureMiWcs.wcs 

851 ra, dec = wcs.pixelToSkyArray(x.ravel().astype(np.float64), 

852 y.ravel().astype(np.float64)) 

853 

854 contains = self.exposureMiWcs.containsSkyCoords(ra*units.radian, 

855 dec*units.radian) 

856 np.testing.assert_array_equal(contains, np.ones(len(contains), dtype=bool)) 

857 

858 # Same test, everything in degrees. 

859 ra, dec = wcs.pixelToSkyArray(x.ravel().astype(np.float64), 

860 y.ravel().astype(np.float64), 

861 degrees=True) 

862 

863 contains = self.exposureMiWcs.containsSkyCoords(ra*units.degree, 

864 dec*units.degree) 

865 np.testing.assert_array_equal(contains, np.ones(len(contains), dtype=bool)) 

866 

867 # Prepend and append some positions out of the box. 

868 ra = np.concatenate(([300.0], ra, [180.])) 

869 dec = np.concatenate(([50.0], dec, [50.0])) 

870 

871 # Bad NaN handling appears as a warning, not an error 

872 with warnings.catch_warnings(): 

873 warnings.simplefilter("error", category=RuntimeWarning) 

874 contains = self.exposureMiWcs.containsSkyCoords(ra*units.degree, 

875 dec*units.degree) 

876 compare = np.ones(len(contains), dtype=bool) 

877 compare[0] = False 

878 compare[-1] = False 

879 np.testing.assert_array_equal(contains, compare) 

880 

881 def _checkCutoutProperties(self, cutout, size, center, precision, msg): 

882 """Test whether a cutout has the desired size and position. 

883 

884 Parameters 

885 ---------- 

886 cutout : `lsst.afw.image.Exposure` 

887 The cutout to test. 

888 size : `lsst.geom.Extent2I` 

889 The expected dimensions of ``cutout``. 

890 center : `lsst.geom.SpherePoint` 

891 The expected center of ``cutout``. 

892 precision : `lsst.geom.Angle` 

893 The precision to which ``center`` must match. 

894 msg : `str` 

895 An error message suffix describing test parameters. 

896 """ 

897 newCenter = self._getExposureCenter(cutout) 

898 self.assertIsNotNone(cutout, msg=msg) 

899 self.assertSpherePointsAlmostEqual(newCenter, center, maxSep=precision, msg=msg) 

900 self.assertEqual(cutout.getWidth(), size[0], msg=msg) 

901 self.assertEqual(cutout.getHeight(), size[1], msg=msg) 

902 

903 def _checkCutoutPixels(self, cutout, validCorners, msg): 

904 """Test whether a cutout has valid/empty pixels where expected. 

905 

906 Parameters 

907 ---------- 

908 cutout : `lsst.afw.image.Exposure` 

909 The cutout to test. 

910 validCorners : iterable of `lsst.geom.Point2I` 

911 The corners of ``cutout`` that should be drawn from the original image. 

912 msg : `str` 

913 An error message suffix describing test parameters. 

914 """ 

915 mask = cutout.getMaskedImage().getMask() 

916 edgeMask = mask.getPlaneBitMask("NO_DATA") 

917 

918 for corner in cutout.getBBox().getCorners(): 

919 maskBitsSet = mask[corner] & edgeMask 

920 if corner in validCorners: 

921 self.assertEqual(maskBitsSet, 0, msg=msg) 

922 else: 

923 self.assertEqual(maskBitsSet, edgeMask, msg=msg) 

924 

925 def _getExposureCenter(self, exposure): 

926 """Return the sky coordinates of an Exposure's center. 

927 

928 Parameters 

929 ---------- 

930 exposure : `lsst.afw.image.Exposure` 

931 The image whose center is desired. 

932 

933 Returns 

934 ------- 

935 center : `lsst.geom.SpherePoint` 

936 The position at the center of ``exposure``. 

937 """ 

938 return exposure.getWcs().pixelToSky(lsst.geom.Box2D(exposure.getBBox()).getCenter()) 

939 

940 def _getValidCorners(self, imageBox, cutoutBox): 

941 """Return the corners of a cutout that are constrained by the original image. 

942 

943 Parameters 

944 ---------- 

945 imageBox: `lsst.geom.Extent2I` 

946 The bounding box of the original image. 

947 cutoutBox : `lsst.geom.Box2I` 

948 The bounding box of the cutout. 

949 

950 Returns 

951 ------- 

952 corners : iterable of `lsst.geom.Point2I` 

953 The corners that are drawn from the original image. 

954 """ 

955 return [corner for corner in cutoutBox.getCorners() if corner in imageBox] 

956 

957 

958class ExposureAfwDataNotNecessary(lsst.utils.tests.TestCase): 

959 

960 def testExposureUnits(self): 

961 """Test that units round trip and end up in the right place.""" 

962 exposure = afwImage.ExposureF(os.path.join(TESTDIR, "data", "HSC-0908120-056-small.fits")) 

963 

964 # Set the units (there is no standard property in model at present 

965 # so assume FITS standard). 

966 units = "nJy" 

967 exposure.metadata["BUNIT"] = units 

968 

969 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

970 exposure.writeFits(tmpFile) 

971 

972 # Use astropy to read the individual HDUs. 

973 with astropy.io.fits.open(tmpFile) as hdul: 

974 # BUNIT should not be in primary. 

975 self.assertNotIn("BUNIT", hdul[0].header) 

976 for extname, expected in (("IMAGE", units), ("VARIANCE", f"{units}**2"), ("MASK", None)): 

977 ind = hdul.index_of(extname) 

978 hdu = hdul[ind] 

979 self.assertEqual(hdu.header["BUNIT"], expected) 

980 

981 # Read it back in an BUNIT should still exist. 

982 reread = afwImage.ExposureF(tmpFile) 

983 self.assertEqual(reread.metadata["BUNIT"], units) 

984 

985 

986class ExposureInfoTestCase(lsst.utils.tests.TestCase): 

987 def setUp(self): 

988 super().setUp() 

989 

990 self.wcs = afwGeom.makeSkyWcs(lsst.geom.Point2D(0.0, 0.0), 

991 lsst.geom.SpherePoint(2.0, 34.0, lsst.geom.degrees), 

992 np.identity(2), 

993 ) 

994 self.photoCalib = afwImage.PhotoCalib(1.5) 

995 self.psf = DummyPsf(2.0) 

996 self.detector = DetectorWrapper().detector 

997 self.summaryStats = afwImage.ExposureSummaryStats(ra=100.0) 

998 self.polygon = afwGeom.Polygon(lsst.geom.Box2D(lsst.geom.Point2D(0.0, 0.0), 

999 lsst.geom.Point2D(25.0, 20.0))) 

1000 self.coaddInputs = afwImage.CoaddInputs() 

1001 self.apCorrMap = afwImage.ApCorrMap() 

1002 self.transmissionCurve = afwImage.TransmissionCurve.makeIdentity() 

1003 

1004 self.exposureInfo = afwImage.ExposureInfo() 

1005 self.gFilterLabel = afwImage.FilterLabel(band="g") 

1006 self.exposureId = 42 

1007 

1008 def _checkAlias(self, exposureInfo, key, value, has, get): 

1009 self.assertFalse(has()) 

1010 self.assertFalse(exposureInfo.hasComponent(key)) 

1011 self.assertIsNone(get()) 

1012 self.assertIsNone(exposureInfo.getComponent(key)) 

1013 

1014 self.exposureInfo.setComponent(key, value) 

1015 self.assertTrue(has()) 

1016 self.assertTrue(exposureInfo.hasComponent(key)) 

1017 self.assertIsNotNone(get()) 

1018 self.assertIsNotNone(exposureInfo.getComponent(key)) 

1019 self.assertEqual(get(), value) 

1020 self.assertEqual(exposureInfo.getComponent(key), value) 

1021 

1022 self.exposureInfo.removeComponent(key) 

1023 self.assertFalse(has()) 

1024 self.assertFalse(exposureInfo.hasComponent(key)) 

1025 self.assertIsNone(get()) 

1026 self.assertIsNone(exposureInfo.getComponent(key)) 

1027 

1028 def testAliases(self): 

1029 cls = type(self.exposureInfo) 

1030 self._checkAlias(self.exposureInfo, cls.KEY_WCS, self.wcs, 

1031 self.exposureInfo.hasWcs, self.exposureInfo.getWcs) 

1032 self._checkAlias(self.exposureInfo, cls.KEY_PSF, self.psf, 

1033 self.exposureInfo.hasPsf, self.exposureInfo.getPsf) 

1034 self._checkAlias(self.exposureInfo, cls.KEY_PHOTO_CALIB, self.photoCalib, 

1035 self.exposureInfo.hasPhotoCalib, self.exposureInfo.getPhotoCalib) 

1036 self._checkAlias(self.exposureInfo, cls.KEY_DETECTOR, self.detector, 

1037 self.exposureInfo.hasDetector, self.exposureInfo.getDetector) 

1038 self._checkAlias(self.exposureInfo, cls.KEY_VALID_POLYGON, self.polygon, 

1039 self.exposureInfo.hasValidPolygon, self.exposureInfo.getValidPolygon) 

1040 self._checkAlias(self.exposureInfo, cls.KEY_COADD_INPUTS, self.coaddInputs, 

1041 self.exposureInfo.hasCoaddInputs, self.exposureInfo.getCoaddInputs) 

1042 self._checkAlias(self.exposureInfo, cls.KEY_AP_CORR_MAP, self.apCorrMap, 

1043 self.exposureInfo.hasApCorrMap, self.exposureInfo.getApCorrMap) 

1044 self._checkAlias(self.exposureInfo, cls.KEY_TRANSMISSION_CURVE, self.transmissionCurve, 

1045 self.exposureInfo.hasTransmissionCurve, self.exposureInfo.getTransmissionCurve) 

1046 self._checkAlias(self.exposureInfo, cls.KEY_SUMMARY_STATS, self.summaryStats, 

1047 self.exposureInfo.hasSummaryStats, self.exposureInfo.getSummaryStats) 

1048 self._checkAlias(self.exposureInfo, cls.KEY_FILTER, self.gFilterLabel, 

1049 self.exposureInfo.hasFilter, self.exposureInfo.getFilter) 

1050 

1051 def testId(self): 

1052 self.exposureInfo.setVisitInfo(afwImage.VisitInfo()) 

1053 

1054 self.assertFalse(self.exposureInfo.hasId()) 

1055 self.assertIsNone(self.exposureInfo.getId()) 

1056 self.assertIsNone(self.exposureInfo.id) 

1057 

1058 self.exposureInfo.setId(self.exposureId) 

1059 self.assertTrue(self.exposureInfo.hasId()) 

1060 self.assertIsNotNone(self.exposureInfo.getId()) 

1061 self.assertIsNotNone(self.exposureInfo.id) 

1062 self.assertEqual(self.exposureInfo.getId(), self.exposureId) 

1063 self.assertEqual(self.exposureInfo.id, self.exposureId) 

1064 

1065 self.exposureInfo.id = 99899 

1066 self.assertEqual(self.exposureInfo.getId(), 99899) 

1067 

1068 self.exposureInfo.id = None 

1069 self.assertFalse(self.exposureInfo.hasId()) 

1070 self.assertIsNone(self.exposureInfo.getId()) 

1071 self.assertIsNone(self.exposureInfo.id) 

1072 

1073 def testCopy(self): 

1074 # Test that ExposureInfos have independently settable state 

1075 copy = afwImage.ExposureInfo(self.exposureInfo, True) 

1076 self.assertEqual(self.exposureInfo.getWcs(), copy.getWcs()) 

1077 

1078 newWcs = afwGeom.makeSkyWcs(lsst.geom.Point2D(-23.0, 8.0), 

1079 lsst.geom.SpherePoint(0.0, 0.0, lsst.geom.degrees), 

1080 np.identity(2), 

1081 ) 

1082 copy.setWcs(newWcs) 

1083 self.assertEqual(copy.getWcs(), newWcs) 

1084 self.assertNotEqual(self.exposureInfo.getWcs(), copy.getWcs()) 

1085 

1086 def testMissingProperties(self): 

1087 # Test that invalid properties return None instead of raising 

1088 exposureInfo = afwImage.ExposureInfo() 

1089 

1090 self.assertIsNone(exposureInfo.id) 

1091 

1092 

1093class ExposureNoAfwdataTestCase(lsst.utils.tests.TestCase): 

1094 """Tests of Exposure that don't require afwdata. 

1095 

1096 These tests use the trivial exposures written to ``afw/tests/data``. 

1097 """ 

1098 def setUp(self): 

1099 self.dataDir = os.path.join(os.path.split(__file__)[0], "data") 

1100 

1101 # Check the values below against what was written by comparing with 

1102 # the code in `afw/tests/data/makeTestExposure.py` 

1103 nx = ny = 10 

1104 image = afwImage.ImageF(np.arange(nx*ny, dtype='f').reshape(nx, ny)) 

1105 variance = afwImage.ImageF(np.ones((nx, ny), dtype='f')) 

1106 mask = afwImage.MaskX(nx, ny) 

1107 mask.array[5, 5] = 5 

1108 self.maskedImage = afwImage.MaskedImageF(image, mask, variance) 

1109 self.exposureId = 12345 

1110 

1111 self.v0PhotoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(1e6, 2e4) 

1112 self.v1PhotoCalib = afwImage.PhotoCalib(1e6, 2e4) 

1113 self.v1FilterLabel = afwImage.FilterLabel(physical="ha") 

1114 self.v2FilterLabel = afwImage.FilterLabel(band="N656", physical="ha") 

1115 

1116 def testReadUnversioned(self): 

1117 """Test that we can read an unversioned (implicit verison 0) file. 

1118 """ 

1119 filename = os.path.join(self.dataDir, "exposure-noversion.fits") 

1120 exposure = afwImage.ExposureF.readFits(filename) 

1121 

1122 self.assertMaskedImagesEqual(exposure.maskedImage, self.maskedImage) 

1123 

1124 self.assertEqual(exposure.info.id, self.exposureId) 

1125 self.assertEqual(exposure.getPhotoCalib(), self.v0PhotoCalib) 

1126 self.assertEqual(exposure.getFilter(), self.v1FilterLabel) 

1127 

1128 def testReadVersion0(self): 

1129 """Test that we can read a version 0 file. 

1130 This file should be identical to the unversioned one, except that it 

1131 is marked as ExposureInfo version 0 in the header. 

1132 """ 

1133 filename = os.path.join(self.dataDir, "exposure-version-0.fits") 

1134 exposure = afwImage.ExposureF.readFits(filename) 

1135 

1136 self.assertMaskedImagesEqual(exposure.maskedImage, self.maskedImage) 

1137 

1138 self.assertEqual(exposure.info.id, self.exposureId) 

1139 self.assertEqual(exposure.getPhotoCalib(), self.v0PhotoCalib) 

1140 self.assertEqual(exposure.getFilter(), self.v1FilterLabel) 

1141 

1142 # Check that the metadata reader parses the file correctly 

1143 reader = afwImage.ExposureFitsReader(filename) 

1144 self.assertEqual(reader.readExposureInfo().getPhotoCalib(), self.v0PhotoCalib) 

1145 self.assertEqual(reader.readPhotoCalib(), self.v0PhotoCalib) 

1146 

1147 def testReadVersion1(self): 

1148 """Test that we can read a version 1 file. 

1149 Version 1 replaced Calib with PhotoCalib. 

1150 """ 

1151 filename = os.path.join(self.dataDir, "exposure-version-1.fits") 

1152 exposure = afwImage.ExposureF.readFits(filename) 

1153 

1154 self.assertMaskedImagesEqual(exposure.maskedImage, self.maskedImage) 

1155 

1156 self.assertEqual(exposure.info.id, self.exposureId) 

1157 self.assertEqual(exposure.getPhotoCalib(), self.v1PhotoCalib) 

1158 self.assertEqual(exposure.getFilter(), self.v1FilterLabel) 

1159 

1160 # Check that the metadata reader parses the file correctly 

1161 reader = afwImage.ExposureFitsReader(filename) 

1162 self.assertEqual(reader.readExposureInfo().getPhotoCalib(), self.v1PhotoCalib) 

1163 self.assertEqual(reader.readPhotoCalib(), self.v1PhotoCalib) 

1164 

1165 def testReadVersion2(self): 

1166 """Test that we can read a version 2 file. 

1167 Version 2 replaced Filter with FilterLabel. 

1168 """ 

1169 filename = os.path.join(self.dataDir, "exposure-version-2.fits") 

1170 exposure = afwImage.ExposureF.readFits(filename) 

1171 

1172 self.assertMaskedImagesEqual(exposure.maskedImage, self.maskedImage) 

1173 

1174 self.assertEqual(exposure.info.id, self.exposureId) 

1175 self.assertEqual(exposure.getPhotoCalib(), self.v1PhotoCalib) 

1176 self.assertEqual(exposure.getFilter(), self.v2FilterLabel) 

1177 

1178 # Check that the metadata reader parses the file correctly 

1179 reader = afwImage.ExposureFitsReader(filename) 

1180 self.assertEqual(reader.readExposureInfo().getPhotoCalib(), self.v1PhotoCalib) 

1181 self.assertEqual(reader.readPhotoCalib(), self.v1PhotoCalib) 

1182 

1183 def testReadDottedHeaderKey(self): 

1184 """Test that we can read a file with a dot-delimited header key.""" 

1185 original = afwImage.ExposureF.readFits(os.path.join(self.dataDir, "exposure-version-2.fits")) 

1186 original.metadata["x.y.z"] = "three" 

1187 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

1188 original.writeFits(tmpFile) 

1189 roundtripped = afwImage.ExposureF(tmpFile) 

1190 self.assertMaskedImagesEqual(original.maskedImage, roundtripped.maskedImage) 

1191 

1192 def testExposureSummaryExtraComponents(self): 

1193 """Test that we can read an exposure summary with extra components. 

1194 """ 

1195 testDict = {'ra': 0.0, 

1196 'dec': 0.0, 

1197 'nonsense': 1.0} 

1198 bytes = yaml.dump(testDict, encoding='utf-8') 

1199 with self.assertWarns(FutureWarning): 

1200 summaryStats = lsst.afw.image.ExposureSummaryStats._read(bytes) 

1201 

1202 self.assertEqual(summaryStats.ra, testDict['ra']) 

1203 self.assertEqual(summaryStats.dec, testDict['dec']) 

1204 

1205 def testExposureSummaryForwardComponents(self): 

1206 """Test that we can forward extra components (e.g. decl->dec). 

1207 """ 

1208 testDict = {'ra': 10.0, 

1209 'decl': 10.0} 

1210 bytes = yaml.dump(testDict, encoding='utf-8') 

1211 # Cleanly forwarded fields must not result in a warning. 

1212 with warnings.catch_warnings(): 

1213 warnings.simplefilter("error") 

1214 summaryStats = lsst.afw.image.ExposureSummaryStats._read(bytes) 

1215 

1216 self.assertEqual(summaryStats.ra, testDict['ra']) 

1217 self.assertEqual(summaryStats.dec, testDict['decl']) 

1218 

1219 # And check if there are both listed, it should use the new dec value. 

1220 testDict = {'ra': 10.0, 

1221 'dec': 5.0, 

1222 'decl': 10.0} 

1223 bytes = yaml.dump(testDict, encoding='utf-8') 

1224 with self.assertWarns(FutureWarning): 

1225 summaryStats = lsst.afw.image.ExposureSummaryStats._read(bytes) 

1226 

1227 self.assertEqual(summaryStats.ra, testDict['ra']) 

1228 self.assertEqual(summaryStats.dec, testDict['dec']) 

1229 

1230 def testExposureSummarySchema(self): 

1231 """Test that we can make a schema for an exposure summary and populate 

1232 records with that schema. 

1233 """ 

1234 schema = afwTable.Schema() 

1235 afwImage.ExposureSummaryStats.update_schema(schema) 

1236 self.maxDiff = None 

1237 self.assertEqual( 

1238 {field.name for field in dataclasses.fields(afwImage.ExposureSummaryStats)}, 

1239 set(schema.getNames()) | {"version"}, 

1240 ) 

1241 catalog = afwTable.BaseCatalog(schema) 

1242 summary1 = afwImage.ExposureSummaryStats() 

1243 for n, field in enumerate(dataclasses.fields(afwImage.ExposureSummaryStats)): 

1244 # Set fields to deterministic, distinct, but arbitrary values. 

1245 if field.type == "float": 

1246 setattr(summary1, field.name, float(0.5**n)) 

1247 elif field.type == "int": 

1248 setattr(summary1, field.name, 10*n) 

1249 elif field.type == "list[float]": 

1250 setattr(summary1, field.name, [n + 0.1, n + 0.2, n + 0.3, n + 0.4]) 

1251 else: 

1252 raise TypeError(f"Unexpected type: {field.type!r}.") 

1253 record = catalog.addNew() 

1254 summary1.update_record(record) 

1255 summary2 = afwImage.ExposureSummaryStats.from_record(record) 

1256 self.assertEqual(summary1, summary2) 

1257 

1258 def testMetadataProperty(self): 

1259 """Test that the metadata property works as expected. 

1260 """ 

1261 exposure = afwImage.ExposureF(3, 4) 

1262 self.assertFalse(exposure.metadata) 

1263 self.assertIsNotNone(exposure.metadata) 

1264 exposure.metadata = None 

1265 self.assertIsNone(exposure.metadata) 

1266 metadata = PropertyList() 

1267 metadata["one"] = 1 

1268 exposure.metadata = metadata 

1269 self.assertEqual(exposure.metadata["one"], 1) 

1270 

1271 

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

1273 pass 

1274 

1275 

1276def setup_module(module): 

1277 lsst.utils.tests.init() 

1278 

1279 

1280if __name__ == "__main__": 1280 ↛ 1281line 1280 didn't jump to line 1281 because the condition on line 1280 was never true

1281 lsst.utils.tests.init() 

1282 unittest.main()