Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of 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 os.path 

27import unittest 

28 

29import numpy as np 

30from numpy.testing import assert_allclose 

31 

32import lsst.utils 

33import lsst.utils.tests 

34import lsst.geom 

35import lsst.afw.image as afwImage 

36from lsst.afw.image.utils import defineFilter 

37from lsst.afw.coord import Weather 

38import lsst.afw.geom as afwGeom 

39import lsst.afw.table as afwTable 

40import lsst.pex.exceptions as pexExcept 

41from lsst.afw.fits import readMetadata, FitsError 

42from lsst.afw.cameraGeom.testUtils import DetectorWrapper 

43from lsst.log import Log 

44from testTableArchivesLib import DummyPsf 

45 

46Log.getLogger("afw.image.Mask").setLevel(Log.INFO) 

47 

48try: 

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

50except pexExcept.NotFoundError: 

51 dataDir = None 

52else: 

53 InputMaskedImageName = "871034p_1_MI.fits" 

54 InputMaskedImageNameSmall = "small_MI.fits" 

55 InputImageNameSmall = "small" 

56 OutputMaskedImageName = "871034p_1_MInew.fits" 

57 

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

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

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

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

62 

63 

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

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

66 """ 

67 A test case for the Exposure Class 

68 """ 

69 

70 def setUp(self): 

71 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

72 maskedImageMD = readMetadata(inFilePathSmall) 

73 

74 self.smallExposure = afwImage.ExposureF(inFilePathSmall) 

75 self.width = maskedImage.getWidth() 

76 self.height = maskedImage.getHeight() 

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

78 self.md = maskedImageMD 

79 self.psf = DummyPsf(2.0) 

80 self.detector = DetectorWrapper().detector 

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

82 

83 self.exposureBlank = afwImage.ExposureF() 

84 self.exposureMiOnly = afwImage.makeExposure(maskedImage) 

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

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

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

88 # test with ExtentI(100, 100) too 

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

90 

91 afwImage.Filter.reset() 

92 afwImage.FilterProperty.reset() 

93 

94 defineFilter("g", 470.0) 

95 

96 def tearDown(self): 

97 del self.smallExposure 

98 del self.wcs 

99 del self.psf 

100 del self.detector 

101 del self.extras 

102 

103 del self.exposureBlank 

104 del self.exposureMiOnly 

105 del self.exposureMiWcs 

106 del self.exposureCrWcs 

107 del self.exposureCrOnly 

108 

109 def testGetMaskedImage(self): 

110 """ 

111 Test to ensure a MaskedImage can be obtained from each 

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

113 therefore each of the Exposures should return a MaskedImage. 

114 

115 MaskedImage class should throw appropriate 

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

117 obtained. 

118 """ 

119 maskedImageBlank = self.exposureBlank.getMaskedImage() 

120 blankWidth = maskedImageBlank.getWidth() 

121 blankHeight = maskedImageBlank.getHeight() 

122 if blankWidth != blankHeight != 0: 

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

124 

125 maskedImageMiOnly = self.exposureMiOnly.getMaskedImage() 

126 miOnlyWidth = maskedImageMiOnly.getWidth() 

127 miOnlyHeight = maskedImageMiOnly.getHeight() 

128 self.assertAlmostEqual(miOnlyWidth, self.width) 

129 self.assertAlmostEqual(miOnlyHeight, self.height) 

130 

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

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

133 # the WCS being copied/created. 

134 

135 maskedImageMiWcs = self.exposureMiWcs.getMaskedImage() 

136 miWcsWidth = maskedImageMiWcs.getWidth() 

137 miWcsHeight = maskedImageMiWcs.getHeight() 

138 self.assertAlmostEqual(miWcsWidth, self.width) 

139 self.assertAlmostEqual(miWcsHeight, self.height) 

140 

141 maskedImageCrWcs = self.exposureCrWcs.getMaskedImage() 

142 crWcsWidth = maskedImageCrWcs.getWidth() 

143 crWcsHeight = maskedImageCrWcs.getHeight() 

144 if crWcsWidth != crWcsHeight != 0: 

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

146 

147 maskedImageCrOnly = self.exposureCrOnly.getMaskedImage() 

148 crOnlyWidth = maskedImageCrOnly.getWidth() 

149 crOnlyHeight = maskedImageCrOnly.getHeight() 

150 if crOnlyWidth != crOnlyHeight != 0: 

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

152 

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

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

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

156 

157 def testProperties(self): 

158 self.assertMaskedImagesEqual(self.exposureMiOnly.maskedImage, 

159 self.exposureMiOnly.getMaskedImage()) 

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

161 mi2.image.array[:] = 5.0 

162 mi2.variance.array[:] = 3.0 

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

164 self.exposureMiOnly.maskedImage = mi2 

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

166 self.assertImagesEqual(self.exposureMiOnly.image, 

167 self.exposureMiOnly.maskedImage.image) 

168 

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

170 image3.array[:] = 3.0 

171 self.exposureMiOnly.image = image3 

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

173 

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

175 mask3.array[:] = 0x2 

176 self.exposureMiOnly.mask = mask3 

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

178 

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

180 var3.array[:] = 2.0 

181 self.exposureMiOnly.variance = var3 

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

183 

184 def testGetWcs(self): 

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

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

187 """ 

188 # These exposures don't contain a WCS 

189 self.assertIsNone(self.exposureBlank.getWcs()) 

190 self.assertIsNone(self.exposureMiOnly.getWcs()) 

191 self.assertIsNone(self.exposureCrOnly.getWcs()) 

192 

193 # These exposures should contain a WCS 

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

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

196 

197 def testExposureInfoConstructor(self): 

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

199 exposureInfo = afwImage.ExposureInfo() 

200 exposureInfo.setWcs(self.wcs) 

201 exposureInfo.setDetector(self.detector) 

202 gFilter = afwImage.Filter("g") 

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

204 exposureInfo.setFilter(gFilter) 

205 exposureInfo.setFilterLabel(gFilterLabel) 

206 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

207 exposure = afwImage.ExposureF(maskedImage, exposureInfo) 

208 

209 self.assertTrue(exposure.hasWcs()) 

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

211 self.wcs.getPixelOrigin()) 

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

213 self.detector.getName()) 

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

215 self.detector.getSerial()) 

216 self.assertEqual(exposure.getFilter(), gFilter) 

217 self.assertEqual(exposure.getFilterLabel(), gFilterLabel) 

218 

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

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

221 self.wcs.getPixelOrigin()) 

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

223 self.detector.getName()) 

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

225 self.detector.getSerial()) 

226 self.assertEqual(exposure.getInfo().getFilter(), gFilter) 

227 self.assertEqual(exposure.getInfo().getFilterLabel(), gFilterLabel) 

228 

229 def testNullWcs(self): 

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

231 

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

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

234 """ 

235 maskedImage = self.exposureMiOnly.getMaskedImage() 

236 exposure = afwImage.ExposureF(maskedImage, None) 

237 self.assertFalse(exposure.hasWcs()) 

238 self.assertFalse(exposure.hasPsf()) 

239 

240 def testExposureInfoSetNone(self): 

241 exposureInfo = afwImage.ExposureInfo() 

242 exposureInfo.setDetector(None) 

243 exposureInfo.setValidPolygon(None) 

244 exposureInfo.setPsf(None) 

245 exposureInfo.setWcs(None) 

246 exposureInfo.setPhotoCalib(None) 

247 exposureInfo.setCoaddInputs(None) 

248 exposureInfo.setVisitInfo(None) 

249 exposureInfo.setApCorrMap(None) 

250 for key in self.extras: 

251 exposureInfo.setComponent(key, None) 

252 

253 def testSetExposureInfo(self): 

254 exposureInfo = afwImage.ExposureInfo() 

255 exposureInfo.setWcs(self.wcs) 

256 exposureInfo.setDetector(self.detector) 

257 gFilter = afwImage.Filter("g") 

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

259 exposureInfo.setFilter(gFilter) 

260 exposureInfo.setFilterLabel(gFilterLabel) 

261 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

262 exposure = afwImage.ExposureF(maskedImage) 

263 self.assertFalse(exposure.hasWcs()) 

264 

265 exposure.setInfo(exposureInfo) 

266 

267 self.assertTrue(exposure.hasWcs()) 

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

269 self.wcs.getPixelOrigin()) 

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

271 self.detector.getName()) 

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

273 self.detector.getSerial()) 

274 self.assertEqual(exposure.getFilter(), gFilter) 

275 self.assertEqual(exposure.getFilterLabel(), gFilterLabel) 

276 

277 def testVisitInfoFitsPersistence(self): 

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

279 exposureId = 5 

280 exposureTime = 12.3 

281 boresightRotAngle = 45.6 * lsst.geom.degrees 

282 weather = Weather(1.1, 2.2, 0.3) 

283 visitInfo = afwImage.VisitInfo( 

284 exposureId=exposureId, 

285 exposureTime=exposureTime, 

286 boresightRotAngle=boresightRotAngle, 

287 weather=weather, 

288 ) 

289 photoCalib = afwImage.PhotoCalib(3.4, 5.6) 

290 exposureInfo = afwImage.ExposureInfo() 

291 exposureInfo.setVisitInfo(visitInfo) 

292 exposureInfo.setPhotoCalib(photoCalib) 

293 exposureInfo.setDetector(self.detector) 

294 gFilter = afwImage.Filter("g") 

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

296 exposureInfo.setFilter(gFilter) 

297 exposureInfo.setFilterLabel(gFilterLabel) 

298 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

299 exposure = afwImage.ExposureF(maskedImage, exposureInfo) 

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

301 exposure.writeFits(tmpFile) 

302 rtExposure = afwImage.ExposureF(tmpFile) 

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

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

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

306 self.assertEqual(rtExposure.getFilter(), gFilter) 

307 self.assertEqual(rtExposure.getFilterLabel(), gFilterLabel) 

308 

309 def testSetMembers(self): 

310 """ 

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

312 """ 

313 exposure = afwImage.ExposureF() 

314 

315 maskedImage = afwImage.MaskedImageF(inFilePathSmall) 

316 exposure.setMaskedImage(maskedImage) 

317 exposure.setWcs(self.wcs) 

318 exposure.setDetector(self.detector) 

319 exposure.setFilter(afwImage.Filter("g")) 

320 exposure.setFilterLabel(afwImage.FilterLabel(band="g")) 

321 

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

323 self.detector.getName()) 

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

325 self.detector.getSerial()) 

326 self.assertEqual(exposure.getFilter().getName(), "g") 

327 self.assertEqual(exposure.getFilterLabel().bandLabel, "g") 

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

329 

330 # The PhotoCalib tests are in test_photoCalib.py; 

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

332 self.assertIsNone(exposure.getPhotoCalib()) 

333 

334 photoCalib = afwImage.PhotoCalib(511.1, 44.4) 

335 exposure.setPhotoCalib(photoCalib) 

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

337 

338 # Psfs next 

339 self.assertFalse(exposure.hasPsf()) 

340 exposure.setPsf(self.psf) 

341 self.assertTrue(exposure.hasPsf()) 

342 

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

344 

345 # extras next 

346 info = exposure.getInfo() 

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

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

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

350 info.setComponent(key, value) 

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

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

353 info.removeComponent(key) 

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

355 

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

357 # that already has both 

358 self.exposureMiWcs.setMaskedImage(maskedImage) 

359 exposure.setWcs(self.wcs) 

360 

361 def testHasWcs(self): 

362 """ 

363 Test if an Exposure has a WCS or not. 

364 """ 

365 self.assertFalse(self.exposureBlank.hasWcs()) 

366 

367 self.assertFalse(self.exposureMiOnly.hasWcs()) 

368 self.assertTrue(self.exposureMiWcs.hasWcs()) 

369 self.assertTrue(self.exposureCrWcs.hasWcs()) 

370 self.assertFalse(self.exposureCrOnly.hasWcs()) 

371 

372 def testGetSubExposure(self): 

373 """ 

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

375 

376 The MaskedImage class should throw a 

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

378 subRegion is not fully contained within the original 

379 MaskedImage. 

380 

381 """ 

382 # 

383 # This subExposure is valid 

384 # 

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

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

387 subExposure = self.exposureCrWcs.Factory( 

388 self.exposureCrWcs, subBBox, afwImage.LOCAL) 

389 

390 self.checkWcs(self.exposureCrWcs, subExposure) 

391 

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

393 # from the MaskedImage class and should trigger an exception 

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

395 

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

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

398 

399 def getSubRegion(): 

400 self.exposureCrWcs.Factory( 

401 self.exposureCrWcs, subRegion3, afwImage.LOCAL) 

402 

403 self.assertRaises(pexExcept.LengthError, getSubRegion) 

404 

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

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

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

408 

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

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

411 

412 def getSubRegion(): 

413 self.exposureCrWcs.Factory( 

414 self.exposureCrWcs, subRegion4, afwImage.LOCAL) 

415 

416 self.assertRaises(pexExcept.LengthError, getSubRegion) 

417 

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

419 # transformation 

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

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

422 subExposure = self.exposureCrWcs.Factory( 

423 self.exposureCrWcs, subBBox, afwImage.LOCAL) 

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

425 

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

427 

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

429 

430 def testReadWriteFits(self): 

431 """Test readFits and writeFits. 

432 """ 

433 # This should pass without an exception 

434 mainExposure = afwImage.ExposureF(inFilePathSmall) 

435 mainExposure.setDetector(self.detector) 

436 

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

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

439 subExposure = mainExposure.Factory( 

440 mainExposure, subBBox, afwImage.LOCAL) 

441 self.checkWcs(mainExposure, subExposure) 

442 det = subExposure.getDetector() 

443 self.assertTrue(det) 

444 

445 subExposure = afwImage.ExposureF( 

446 inFilePathSmall, subBBox, afwImage.LOCAL) 

447 

448 self.checkWcs(mainExposure, subExposure) 

449 

450 # This should throw an exception 

451 def getExposure(): 

452 afwImage.ExposureF(inFilePathSmallImage) 

453 

454 self.assertRaises(FitsError, getExposure) 

455 

456 mainExposure.setPsf(self.psf) 

457 

458 # Make sure we can write without an exception 

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

460 mainExposure.setPhotoCalib(photoCalib) 

461 

462 mainInfo = mainExposure.getInfo() 

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

464 mainInfo.setComponent(key, value) 

465 

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

467 mainExposure.writeFits(tmpFile) 

468 

469 readExposure = type(mainExposure)(tmpFile) 

470 

471 # 

472 # Check the round-tripping 

473 # 

474 self.assertEqual(mainExposure.getFilter().getName(), 

475 readExposure.getFilter().getName()) 

476 self.assertIsNotNone(mainExposure.getFilterLabel()) 

477 self.assertEqual(mainExposure.getFilterLabel(), 

478 readExposure.getFilterLabel()) 

479 

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

481 

482 readInfo = readExposure.getInfo() 

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

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

485 

486 psf = readExposure.getPsf() 

487 self.assertIsNotNone(psf) 

488 self.assertEqual(psf, self.psf) 

489 

490 def checkWcs(self, parentExposure, subExposure): 

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

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

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

494 to get the same sky coordinates for each. 

495 """ 

496 subMI = subExposure.getMaskedImage() 

497 subDim = subMI.getDimensions() 

498 

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

500 # with WCS 

501 mainWcs = parentExposure.getWcs() 

502 subWcs = subExposure.getWcs() 

503 

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

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

506 self.assertSpherePointsAlmostEqual( 

507 mainWcs.pixelToSky( 

508 afwImage.indexToPosition(xSubInd), 

509 afwImage.indexToPosition(ySubInd), 

510 ), 

511 subWcs.pixelToSky( 

512 afwImage.indexToPosition(xSubInd), 

513 afwImage.indexToPosition(ySubInd), 

514 )) 

515 

516 def cmpExposure(self, e1, e2): 

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

518 e2.getDetector().getName()) 

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

520 e2.getDetector().getSerial()) 

521 self.assertEqual(e1.getFilter().getName(), e2.getFilter().getName()) 

522 self.assertEqual(e1.getFilterLabel(), e2.getFilterLabel()) 

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

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

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

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

527 # check PSF identity 

528 if not e1.getPsf(): 

529 self.assertFalse(e2.getPsf()) 

530 else: 

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

532 # Check extra components 

533 i1 = e1.getInfo() 

534 i2 = e2.getInfo() 

535 for key in self.extras: 

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

537 if i1.hasComponent(key): 

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

539 

540 def testCopyExposure(self): 

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

542 

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

544 exposureU.setWcs(self.wcs) 

545 exposureU.setDetector(self.detector) 

546 exposureU.setFilter(afwImage.Filter("g")) 

547 exposureU.setFilterLabel(afwImage.FilterLabel(band="g")) 

548 exposureU.setPsf(DummyPsf(4.0)) 

549 infoU = exposureU.getInfo() 

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

551 infoU.setComponent(key, value) 

552 

553 exposureF = exposureU.convertF() 

554 self.cmpExposure(exposureF, exposureU) 

555 

556 nexp = exposureF.Factory(exposureF, False) 

557 self.cmpExposure(exposureF, nexp) 

558 

559 # Ensure that the copy was deep. 

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

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

562 # x0,y0 = cen0 

563 # det = exposureF.getDetector() 

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

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

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

567 

568 def testDeepCopyData(self): 

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

570 """ 

571 exp = afwImage.ExposureF(6, 7) 

572 mi = exp.getMaskedImage() 

573 mi.getImage().set(100) 

574 mi.getMask().set(5) 

575 mi.getVariance().set(200) 

576 

577 expCopy = exp.clone() 

578 miCopy = expCopy.getMaskedImage() 

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

580 miCopy.getMask().set(2) 

581 miCopy.getVariance().set(175) 

582 

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

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

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

586 

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

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

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

590 

591 def testDeepCopySubData(self): 

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

593 """ 

594 exp = afwImage.ExposureF(6, 7) 

595 mi = exp.getMaskedImage() 

596 mi.getImage().set(100) 

597 mi.getMask().set(5) 

598 mi.getVariance().set(200) 

599 

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

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

602 miCopy = expCopy.getMaskedImage() 

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

604 miCopy.getMask().set(2) 

605 miCopy.getVariance().set(175) 

606 

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

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

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

610 

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

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

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

614 

615 def testDeepCopyMetadata(self): 

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

617 """ 

618 exp = afwImage.ExposureF(10, 10) 

619 expMeta = exp.getMetadata() 

620 expMeta.set("foo", 5) 

621 expCopy = exp.clone() 

622 expCopyMeta = expCopy.getMetadata() 

623 expCopyMeta.set("foo", 6) 

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

625 # this will fail if the bug is present 

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

627 

628 def testDeepCopySubMetadata(self): 

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

630 """ 

631 exp = afwImage.ExposureF(10, 10) 

632 expMeta = exp.getMetadata() 

633 expMeta.set("foo", 5) 

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

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

636 expCopyMeta = expCopy.getMetadata() 

637 expCopyMeta.set("foo", 6) 

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

639 # this will fail if the bug is present 

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

641 

642 def testMakeExposureLeaks(self): 

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

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

645 afwImage.makeExposure(afwImage.makeMaskedImage( 

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

647 

648 def testImageSlices(self): 

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

650 exp = afwImage.ExposureF(10, 20) 

651 mi = exp.getMaskedImage() 

652 mi.image[9, 19] = 10 

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

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

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

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

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

658 self.assertEqual(sexp.getDimensions(), 

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

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

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

662 

663 def testConversionToScalar(self): 

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

665 im = afwImage.ExposureF(10, 20) 

666 

667 # only single pixel images may be converted 

668 self.assertRaises(TypeError, float, im) 

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

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

671 

672 def testReadMetadata(self): 

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

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

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

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

677 self.exposureCrWcs.writeFits(tmpFile) 

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

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

680 # frazzle and the Wcs from the PropertySet returned by 

681 # testReadMetadata. 

682 md = readMetadata(tmpFile) 

683 wcs = afwGeom.makeSkyWcs(md, False) 

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

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

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

687 frazzle = md.getScalar("FRAZZLE") 

688 self.assertTrue(frazzle) 

689 

690 def testArchiveKeys(self): 

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

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

693 exposure1.setPsf(self.psf) 

694 exposure1.writeFits(tmpFile) 

695 exposure2 = afwImage.ExposureF(tmpFile) 

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

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

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

699 

700 def testTicket2861(self): 

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

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

703 exposure1.setPsf(self.psf) 

704 schema = afwTable.ExposureTable.makeMinimalSchema() 

705 coaddInputs = afwImage.CoaddInputs(schema, schema) 

706 exposure1.getInfo().setCoaddInputs(coaddInputs) 

707 exposure2 = afwImage.ExposureF(exposure1, True) 

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

709 exposure2.writeFits(tmpFile) 

710 exposure3 = afwImage.ExposureF(tmpFile) 

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

712 

713 def testGetCutout(self): 

714 wcs = self.smallExposure.getWcs() 

715 

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

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

718 2*self.smallExposure.getDimensions()] 

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

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

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

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

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

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

725 for cutoutSize in dimensions: 

726 for label, cutoutCenter in locations: 

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

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

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

730 centerInPixels = wcs.skyToPixel(cutoutCenter) 

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

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

733 self._checkCutoutPixels( 

734 cutout, 

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

736 msg) 

737 

738 # Need a valid WCS 

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

740 self.exposureMiOnly.getCutout(cutoutCenter, cutoutSize) 

741 else: 

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

743 self.smallExposure.getCutout(cutoutCenter, cutoutSize) 

744 

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

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

747 

748 Parameters 

749 ---------- 

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

751 The cutout to test. 

752 size : `lsst.geom.Extent2I` 

753 The expected dimensions of ``cutout``. 

754 center : `lsst.geom.SpherePoint` 

755 The expected center of ``cutout``. 

756 precision : `lsst.geom.Angle` 

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

758 msg : `str` 

759 An error message suffix describing test parameters. 

760 """ 

761 newCenter = self._getExposureCenter(cutout) 

762 self.assertIsNotNone(cutout, msg=msg) 

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

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

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

766 

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

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

769 

770 Parameters 

771 ---------- 

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

773 The cutout to test. 

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

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

776 msg : `str` 

777 An error message suffix describing test parameters. 

778 """ 

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

780 edgeMask = mask.getPlaneBitMask("NO_DATA") 

781 

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

783 maskBitsSet = mask[corner] & edgeMask 

784 if corner in validCorners: 

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

786 else: 

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

788 

789 def _getExposureCenter(self, exposure): 

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

791 

792 Parameters 

793 ---------- 

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

795 The image whose center is desired. 

796 

797 Returns 

798 ------- 

799 center : `lsst.geom.SpherePoint` 

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

801 """ 

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

803 

804 def _getValidCorners(self, imageBox, cutoutBox): 

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

806 

807 Parameters 

808 ---------- 

809 imageBox: `lsst.geom.Extent2I` 

810 The bounding box of the original image. 

811 cutoutBox : `lsst.geom.Box2I` 

812 The bounding box of the cutout. 

813 

814 Returns 

815 ------- 

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

817 The corners that are drawn from the original image. 

818 """ 

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

820 

821 

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

823 def setUp(self): 

824 super().setUp() 

825 

826 afwImage.Filter.reset() 

827 afwImage.FilterProperty.reset() 

828 defineFilter("g", 470.0) 

829 

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

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

832 np.identity(2), 

833 ) 

834 self.photoCalib = afwImage.PhotoCalib(1.5) 

835 self.psf = DummyPsf(2.0) 

836 self.detector = DetectorWrapper().detector 

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

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

839 self.coaddInputs = afwImage.CoaddInputs() 

840 self.apCorrMap = afwImage.ApCorrMap() 

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

842 

843 self.exposureInfo = afwImage.ExposureInfo() 

844 gFilter = afwImage.Filter("g") 

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

846 self.exposureInfo.setFilter(gFilter) 

847 self.exposureInfo.setFilterLabel(gFilterLabel) 

848 

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

850 self.assertFalse(has()) 

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

852 self.assertIsNone(get()) 

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

854 

855 self.exposureInfo.setComponent(key, value) 

856 self.assertTrue(has()) 

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

858 self.assertIsNotNone(get()) 

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

860 self.assertEqual(get(), value) 

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

862 

863 self.exposureInfo.removeComponent(key) 

864 self.assertFalse(has()) 

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

866 self.assertIsNone(get()) 

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

868 

869 def testAliases(self): 

870 cls = type(self.exposureInfo) 

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

872 self.exposureInfo.hasWcs, self.exposureInfo.getWcs) 

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

874 self.exposureInfo.hasPsf, self.exposureInfo.getPsf) 

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

876 self.exposureInfo.hasPhotoCalib, self.exposureInfo.getPhotoCalib) 

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

878 self.exposureInfo.hasDetector, self.exposureInfo.getDetector) 

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

880 self.exposureInfo.hasValidPolygon, self.exposureInfo.getValidPolygon) 

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

882 self.exposureInfo.hasCoaddInputs, self.exposureInfo.getCoaddInputs) 

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

884 self.exposureInfo.hasApCorrMap, self.exposureInfo.getApCorrMap) 

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

886 self.exposureInfo.hasTransmissionCurve, self.exposureInfo.getTransmissionCurve) 

887 

888 def testCopy(self): 

889 # Test that ExposureInfos have independently settable state 

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

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

892 

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

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

895 np.identity(2), 

896 ) 

897 copy.setWcs(newWcs) 

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

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

900 

901 

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

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

904 

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

906 """ 

907 def setUp(self): 

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

909 

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

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

912 nx = ny = 10 

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

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

915 mask = afwImage.MaskX(nx, ny) 

916 mask.array[5, 5] = 5 

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

918 

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

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

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

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

923 

924 def testReadUnversioned(self): 

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

926 """ 

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

928 exposure = afwImage.ExposureF.readFits(filename) 

929 

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

931 

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

933 self.assertEqual(exposure.getFilterLabel(), self.v1FilterLabel) 

934 

935 def testReadVersion0(self): 

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

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

938 is marked as ExposureInfo version 0 in the header. 

939 """ 

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

941 exposure = afwImage.ExposureF.readFits(filename) 

942 

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

944 

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

946 self.assertEqual(exposure.getFilterLabel(), self.v1FilterLabel) 

947 

948 # Check that the metadata reader parses the file correctly 

949 reader = afwImage.ExposureFitsReader(filename) 

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

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

952 

953 def testReadVersion1(self): 

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

955 Version 1 replaced Calib with PhotoCalib. 

956 """ 

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

958 exposure = afwImage.ExposureF.readFits(filename) 

959 

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

961 

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

963 self.assertEqual(exposure.getFilterLabel(), self.v1FilterLabel) 

964 

965 # Check that the metadata reader parses the file correctly 

966 reader = afwImage.ExposureFitsReader(filename) 

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

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

969 

970 def testReadVersion2(self): 

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

972 Version 2 replaced Filter with FilterLabel. 

973 """ 

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

975 exposure = afwImage.ExposureF.readFits(filename) 

976 

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

978 

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

980 self.assertEqual(exposure.getFilterLabel(), self.v2FilterLabel) 

981 

982 # Check that the metadata reader parses the file correctly 

983 reader = afwImage.ExposureFitsReader(filename) 

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

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

986 

987 

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

989 pass 

990 

991 

992def setup_module(module): 

993 lsst.utils.tests.init() 

994 

995 

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

997 lsst.utils.tests.init() 

998 unittest.main()