Coverage for tests / test_isrFunctions.py: 12%

537 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-28 08:55 +0000

1# 

2# LSST Data Management System 

3# Copyright 2008-2017 AURA/LSST. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

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

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

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

11# (at your option) any later version. 

12# 

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

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

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

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23import unittest 

24import logging 

25import numpy as np 

26 

27from lsst.daf.base import PropertyList 

28import lsst.geom as geom 

29import lsst.afw.image as afwImage 

30import lsst.afw.detection as afwDetection 

31import lsst.utils.tests 

32import lsst.ip.isr as ipIsr 

33import lsst.ip.isr.isrFunctions as ipIsrFunctions 

34import lsst.ip.isr.isrMock as isrMock 

35 

36 

37def computeImageMedianAndStd(image): 

38 """Function to calculate median and std of image data. 

39 

40 Parameters 

41 ---------- 

42 image : `lsst.afw.image.Image` 

43 Image to measure statistics on. 

44 

45 Returns 

46 ------- 

47 median : `float` 

48 Image median. 

49 std : `float` 

50 Image stddev. 

51 """ 

52 median = np.nanmedian(image.getArray()) 

53 std = np.nanstd(image.getArray()) 

54 

55 return (median, std) 

56 

57 

58def _setExposureSatCore(exposure, x, y, halfWidthX, halfWidthY, 

59 satVal, satMaskBit): 

60 """Helper function to make a rectangular saturated core. 

61 

62 Parameters 

63 ---------- 

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

65 Input exposure. 

66 x : `int` 

67 X coordinate for the center of the rectangular saturated core. 

68 y : `int` 

69 Y coordinate for the center of the rectangular saturated core. 

70 halfWidthX : `int` 

71 Half the width of the rectangular saturated core along the x axis. 

72 halfWidthY : `int` 

73 Half the width of the rectangular saturated core along the y axis. 

74 satVal : `float` 

75 Value the saturated core is set to. 

76 satMaskBit : `int` 

77 Saturated mask bit the saturated core mask is set to. 

78 """ 

79 xmax = exposure.image.array.shape[1] 

80 lowerRange = x-halfWidthX 

81 upperRange = x+halfWidthX 

82 if lowerRange < 0: 

83 lowerRange = 0 

84 if upperRange > xmax: 

85 upperRange = xmax 

86 exposure.image.array[y-halfWidthY:y+halfWidthY, 

87 lowerRange:upperRange] = satVal 

88 exposure.mask.array[y-halfWidthY:y+halfWidthY, 

89 lowerRange:upperRange] = satMaskBit 

90 

91 

92def _setExposureSatColumns(exposure, x, y, halfWidthX, limY, 

93 satVal, satMaskBit, isTop=False): 

94 """Helper function to add saturated columns. 

95 We need to add these around a saturated core to test a typical edge bleed. 

96 

97 Parameters 

98 ---------- 

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

100 Input exposure. 

101 x : `int` 

102 X coordinate for the central saturated column. 

103 y : `int` 

104 Y coordinate for the central saturated column. 

105 halfWidthX : `int` 

106 Half the number of saturated columns. 

107 limY : `int` 

108 Y coordinate up to which the saturated columns go to. 

109 satVal : `float` 

110 Value the saturated columns are set to. 

111 satMaskBit : `int` 

112 Saturated mask bit the saturated columns mask is set to. 

113 isTop : `bool` 

114 True if the saturated columns go toward the top of the detector. 

115 """ 

116 if isTop: 

117 exposure.image.array[y:limY, 

118 x-halfWidthX:x+halfWidthX] = satVal 

119 exposure.mask.array[y:limY, 

120 x-halfWidthX:x+halfWidthX] = satMaskBit 

121 else: 

122 exposure.image.array[limY:y, 

123 x-halfWidthX:x+halfWidthX] = satVal 

124 exposure.mask.array[limY:y, 

125 x-halfWidthX:x+halfWidthX] = satMaskBit 

126 

127 

128def _makeEdgeBleed(exposure, x, extentY, edgeBleedWidth, 

129 edgeBleedConstant, edgeBleedWidthLimit, 

130 saturationLevel, saturationFrac, 

131 satVal, satMaskBit, isTop=False): 

132 """Helper function to add an edge bleed with a decaying exponential 

133 model. 

134 

135 Parameters 

136 ---------- 

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

138 Input exposure. 

139 x : `int` 

140 X coordinate for the center of the edge bleed (generally the same 

141 as the center of the saturated core and columns). 

142 extentY : `int` 

143 Y coordinate up to which the edge bleed goes to (generally the same 

144 as limY of the saturated columns). 

145 edgeBleedWidth : `int` 

146 Width of the edge bleed at the edge of the detector. 

147 edgeBleedConstant : `float` 

148 Constant for the decaying exponential model. 

149 edgeBleedWidthLimit : `int` 

150 The width the edge bleed goes to away from the edge (generally the 

151 width of the saturated columns). 

152 saturationLevel : `float` 

153 Saturation level. 

154 saturationFrac : `float` 

155 The inside of the edge bleed is set to a value equal to this fraction 

156 of the saturation level. 

157 satVal : `float` 

158 The value the contour of the edge bleed is set to, greater or equal to 

159 the saturation level. 

160 satMaskBit : `int` 

161 Saturated mask bit the contour of the edge bleed mask is set to. 

162 isTop : `bool` 

163 True if the edge bleed is a the top edge of the detector. 

164 """ 

165 xmax = exposure.image.array.shape[1] 

166 for y in range(extentY): 

167 edgeBleedWidthY = edgeBleedWidth*np.exp(-edgeBleedConstant*y) \ 

168 + edgeBleedWidthLimit 

169 if isTop: 

170 # For edge bleed in top amplifier 

171 y = exposure.image.array.shape[0]-1-y 

172 lowerRange = x-int(edgeBleedWidthY/2.) 

173 upperRange = x+int(edgeBleedWidthY/2.) 

174 if lowerRange < 0: 

175 lowerRange = 0 

176 if upperRange >= xmax: 

177 upperRange = xmax-1 

178 exposure.image.array[y, lowerRange] = satVal 

179 exposure.mask.array[y, lowerRange] = satMaskBit 

180 exposure.image.array[y, upperRange] = satVal 

181 exposure.mask.array[y, upperRange] = satMaskBit 

182 exposure.image.array[y, 

183 lowerRange+1:upperRange 

184 ] = saturationFrac*saturationLevel 

185 

186 

187class MockE2VAmp: 

188 def __init__(self, name, bbox): 

189 self._name = name 

190 self._bbox = bbox 

191 

192 def getName(self): 

193 return self._name 

194 

195 def getBBox(self): 

196 return self._bbox 

197 

198 def __repr__(self): 

199 return f"MockE2VAmp({self._name})" 

200 

201 

202class MockE2VDetector(list): 

203 def __init__(self): 

204 amps = [] 

205 for i in range(8): 

206 name = f"C1{i}" 

207 bbox = geom.Box2I(corner=geom.Point2I(i*512, 2002), dimensions=geom.Extent2I(512, 2002)) 

208 amps.append(MockITLAmp(name, bbox)) 

209 for i in reversed(range(8)): 

210 name = f"C0{i}" 

211 bbox = geom.Box2I(corner=geom.Point2I(i*512, 0), dimensions=geom.Extent2I(512, 2002)) 

212 amps.append(MockITLAmp(name, bbox)) 

213 

214 super().__init__(amps) 

215 

216 def getBBox(self): 

217 return geom.Box2I(corner=geom.Point2I(0, 0), dimensions=geom.Extent2I(4096, 4004)) 

218 

219 

220class MockE2VExposure(afwImage.ExposureF): 

221 def __init__(self, *args, **kwargs): 

222 super().__init__(*args, **kwargs) 

223 

224 def setDetector(self, detector): 

225 self._detector = detector 

226 

227 def fillSaturationMetadata(self, saturationLevel): 

228 if not self._detector: 

229 raise RuntimeError("No detector set.") 

230 for amp in self.getDetector(): 

231 self.metadata[f"LSST ISR SATURATION LEVEL {amp.getName()}"] = saturationLevel 

232 

233 def getDetector(self): 

234 return self._detector 

235 

236 

237class MockITLAmp: 

238 def __init__(self, name, bbox): 

239 self._name = name 

240 self._bbox = bbox 

241 

242 def getName(self): 

243 return self._name 

244 

245 def getBBox(self): 

246 return self._bbox 

247 

248 def __repr__(self): 

249 return f"MockITLAmp({self._name})" 

250 

251 

252class MockITLDetector(list): 

253 def __init__(self): 

254 amps = [] 

255 for i in range(8): 

256 name = f"C1{i}" 

257 bbox = geom.Box2I(corner=geom.Point2I(i*509, 2000), dimensions=geom.Extent2I(509, 2000)) 

258 amps.append(MockITLAmp(name, bbox)) 

259 for i in reversed(range(8)): 

260 name = f"C0{i}" 

261 bbox = geom.Box2I(corner=geom.Point2I(i*509, 0), dimensions=geom.Extent2I(509, 2000)) 

262 amps.append(MockITLAmp(name, bbox)) 

263 

264 super().__init__(amps) 

265 

266 def getBBox(self): 

267 return geom.Box2I(corner=geom.Point2I(0, 0), dimensions=geom.Extent2I(4072, 4000)) 

268 

269 

270class MockITLExposure(afwImage.ExposureF): 

271 def __init__(self, *args, **kwargs): 

272 super().__init__(*args, **kwargs) 

273 

274 # Monkey-patch our fake detector. 

275 def setDetector(self, detector): 

276 self._detector = detector 

277 

278 def fillSaturationMetadata(self, saturationLevel): 

279 if not self._detector: 

280 raise RuntimeError("No detector set.") 

281 for amp in self.getDetector(): 

282 self.metadata[f"LSST ISR SATURATION LEVEL {amp.getName()}"] = saturationLevel 

283 

284 def getDetector(self): 

285 return self._detector 

286 

287 

288class IsrFunctionsCases(lsst.utils.tests.TestCase): 

289 """Test that functions for ISR produce expected outputs. 

290 """ 

291 def setUp(self): 

292 self.inputExp = isrMock.TrimmedRawMock().run() 

293 self.mi = self.inputExp.getMaskedImage() 

294 

295 def test_transposeMaskedImage(self): 

296 """Expect height and width to be exchanged. 

297 """ 

298 transposed = ipIsr.transposeMaskedImage(self.mi) 

299 self.assertEqual(transposed.getImage().getBBox().getHeight(), 

300 self.mi.getImage().getBBox().getWidth()) 

301 self.assertEqual(transposed.getImage().getBBox().getWidth(), 

302 self.mi.getImage().getBBox().getHeight()) 

303 

304 def test_interpolateDefectList(self): 

305 """Expect number of interpolated pixels to be non-zero. 

306 """ 

307 defectList = isrMock.DefectMock().run() 

308 self.assertEqual(len(defectList), 1) 

309 

310 for fallbackValue in (None, -999.0): 

311 for haveMask in (True, False): 

312 with self.subTest(fallbackValue=fallbackValue, haveMask=haveMask): 

313 if haveMask is False: 

314 if 'INTRP' in self.mi.getMask().getMaskPlaneDict(): 

315 self.mi.getMask().removeAndClearMaskPlane('INTRP') 

316 else: 

317 if 'INTRP' not in self.mi.getMask().getMaskPlaneDict(): 

318 self.mi.getMask().addMaskPlane('INTRP') 

319 numBit = ipIsr.countMaskedPixels(self.mi, "INTRP") 

320 self.assertEqual(numBit, 0) 

321 

322 def test_transposeDefectList(self): 

323 """Expect bbox dimension values to flip. 

324 """ 

325 defectList = isrMock.DefectMock().run() 

326 transposed = defectList.transpose() 

327 

328 for d, t in zip(defectList, transposed): 

329 self.assertEqual(d.getBBox().getDimensions().getX(), t.getBBox().getDimensions().getY()) 

330 self.assertEqual(d.getBBox().getDimensions().getY(), t.getBBox().getDimensions().getX()) 

331 

332 def test_makeThresholdMask(self): 

333 """Expect list of defects to have elements. 

334 """ 

335 defectList = ipIsr.makeThresholdMask(self.mi, 200, 

336 growFootprints=2, 

337 maskName='SAT') 

338 

339 self.assertEqual(len(defectList), 1) 

340 

341 def test_E2VEdgeBleedMask(self): 

342 """Test expected number of masked pixels according to e2v edge bleed 

343 masking. 

344 """ 

345 detector = MockE2VDetector() 

346 exposure = MockE2VExposure(detector.getBBox()) 

347 exposure.setDetector(detector) 

348 exposure.mask.array[:, :] = 0 

349 satMaskBit = exposure.mask.getPlaneBitMask('SAT') 

350 

351 saturationLevel = 12000. 

352 exposure.fillSaturationMetadata(saturationLevel) 

353 

354 # add rectangular edge bleed to mock exposure 

355 _setExposureSatCore(exposure, x=1600, y=1000, halfWidthX=200, halfWidthY=100, satVal=saturationLevel, 

356 satMaskBit=satMaskBit) 

357 _setExposureSatColumns(exposure, x=1600, y=1000, halfWidthX=4, limY=0, satVal=saturationLevel, 

358 satMaskBit=satMaskBit, isTop=False) 

359 

360 exposure.image.array[:100, 1600:1800] = 10000. 

361 

362 numPixSatBottomEdgeBefore = len(np.where(exposure.mask.array[0, :] == satMaskBit)[0]) 

363 

364 ipIsrFunctions.maskE2VEdgeBleed(exposure, e2vEdgeBleedSatMinArea=20000, e2vEdgeBleedSatMaxArea=100000, 

365 e2vEdgeBleedYMax=350, 

366 saturatedMaskName='SAT') 

367 

368 numPixSatBottomEdgeAfter = len(np.where(exposure.mask.array[0, :] == satMaskBit)[0]) 

369 

370 # Check the number of saturated pixels 

371 self.assertEqual(ipIsr.countMaskedPixels(exposure, 'SAT'), 263600) 

372 # Check there are more pixels along the bottom edge after masking 

373 self.assertGreater(numPixSatBottomEdgeAfter, numPixSatBottomEdgeBefore) 

374 

375 def test_ITLEdgeBleedMask(self): 

376 """Expect number of masked pixels according to edge bleed masking. 

377 """ 

378 detector = MockITLDetector() 

379 exposure = MockITLExposure(detector.getBBox()) 

380 exposure.setDetector(detector) 

381 exposure.mask.array[:, :] = 0 

382 satMaskBit = exposure.mask.getPlaneBitMask('SAT') 

383 

384 saturationLevel = 12000. 

385 exposure.fillSaturationMetadata(saturationLevel) 

386 

387 badAmpDict = {} 

388 for amp in exposure.getDetector(): 

389 if amp.getName() == 'C10': 

390 badAmpDict[amp.getName()] = True 

391 else: 

392 badAmpDict[amp.getName()] = False 

393 

394 # Case 1: 

395 # Create two edge bleeds that fall into the same footprint. 

396 

397 # A saturated core in the bottom amplifier 

398 # that has a bottom edge bleed 

399 satVal = 15000. 

400 x = 70 

401 y = 120 

402 halfWidthX = 40 

403 halfWidthY = 70 

404 halfWidthColX = 3 

405 limY = 100 

406 extentY = 100 

407 _setExposureSatCore(exposure, x=x, y=y, 

408 halfWidthX=halfWidthX, halfWidthY=halfWidthY, 

409 satVal=satVal, satMaskBit=satMaskBit) 

410 _setExposureSatColumns(exposure, x=x, y=y, halfWidthX=halfWidthColX, 

411 limY=limY, satVal=satVal, satMaskBit=satMaskBit, 

412 isTop=False) 

413 edgeBleedWidth = 60 

414 edgeBleedConstant = 0.04 # we make the edge bleed a bit smaller than the masking model 

415 edgeBleedWidthLimit = 2*halfWidthColX 

416 saturationFrac = 0.85 # higher than default in masking model 

417 _makeEdgeBleed(exposure, x=x, extentY=extentY, 

418 edgeBleedWidth=edgeBleedWidth, 

419 edgeBleedConstant=edgeBleedConstant, 

420 edgeBleedWidthLimit=edgeBleedWidthLimit, 

421 saturationLevel=saturationLevel, 

422 saturationFrac=saturationFrac, 

423 satVal=satVal, satMaskBit=satMaskBit, isTop=False) 

424 

425 # A saturated core near the left edge of the detector, close to the 

426 # previous edge bleed 

427 x = 10 

428 y = 90 

429 halfWidthX = 14 

430 halfWidthY = 50 

431 halfWidthColX = 3 

432 _setExposureSatCore(exposure, x=x, y=y, halfWidthX=halfWidthX, 

433 halfWidthY=halfWidthY, satVal=satVal, 

434 satMaskBit=satMaskBit) 

435 # Bottom saturated columns to connect core and edge bleed 

436 limY = 100 

437 _setExposureSatColumns(exposure, x=x, y=y, halfWidthX=halfWidthColX, 

438 limY=limY, satVal=satVal, satMaskBit=satMaskBit, 

439 isTop=False) 

440 edgeBleedWidth = 70 # this would go beyond the edge of the detector 

441 edgeBleedWidthLimit = 2*halfWidthColX 

442 _makeEdgeBleed(exposure, x=x, extentY=extentY, 

443 edgeBleedWidth=edgeBleedWidth, 

444 edgeBleedConstant=edgeBleedConstant, 

445 edgeBleedWidthLimit=edgeBleedWidthLimit, 

446 saturationLevel=saturationLevel, 

447 saturationFrac=saturationFrac, 

448 satVal=satVal, satMaskBit=satMaskBit, isTop=False) 

449 

450 thresh = afwDetection.Threshold(exposure.mask.getPlaneBitMask("SAT"), 

451 afwDetection.Threshold.BITMASK 

452 ) 

453 fpList = afwDetection.FootprintSet(exposure.mask, thresh).getFootprints() 

454 

455 satAreas = np.asarray([fp.getArea() for fp in fpList]) 

456 largeAreas, = np.where((satAreas >= 1000) 

457 & (satAreas < 100000)) 

458 

459 # We select the only footprint 

460 fpCore = fpList[largeAreas[0]] 

461 

462 numPixSatBottomEdgeBeforeCase1 = len(np.where(exposure.mask.array[0, :] == satMaskBit)[0]) 

463 ipIsr.maskITLEdgeBleed(exposure, badAmpDict, 

464 fpCore, 

465 itlEdgeBleedSatMinArea=2400, 

466 itlEdgeBleedSatMaxArea=100000, 

467 itlEdgeBleedThreshold=5000., 

468 itlEdgeBleedModelConstant=0.02, 

469 saturatedMaskName='SAT') 

470 numPixSatBottomEdgeAfterCase1 = len(np.where(exposure.mask.array[0, :] == satMaskBit)[0]) 

471 # Check the number of saturated pixels 

472 self.assertEqual(ipIsr.countMaskedPixels(exposure, 'SAT'), 22967) 

473 # Check there are more pixels along the bottom edge after masking 

474 self.assertGreater(numPixSatBottomEdgeAfterCase1, numPixSatBottomEdgeBeforeCase1) 

475 

476 # Case 2: 

477 # A saturated core in the top amplifier 

478 # that has a top edge bleed 

479 x = 2100 

480 y = 3100 

481 halfWidthX = 50 

482 halfWidthY = 80 

483 halfWidthColX = 4 

484 limY = exposure.image.array.shape[0]-100 

485 _setExposureSatCore(exposure, x=x, y=y, halfWidthX=halfWidthX, 

486 halfWidthY=halfWidthY, satVal=satVal, 

487 satMaskBit=satMaskBit) 

488 _setExposureSatColumns(exposure, x=x, y=y, halfWidthX=halfWidthColX, 

489 limY=limY, satVal=satVal, satMaskBit=satMaskBit, 

490 isTop=True) 

491 edgeBleedWidth = 100 

492 edgeBleedWidthLimit = 2*halfWidthColX 

493 _makeEdgeBleed(exposure, x=x, extentY=extentY, 

494 edgeBleedWidth=edgeBleedWidth, 

495 edgeBleedConstant=edgeBleedConstant, 

496 edgeBleedWidthLimit=edgeBleedWidthLimit, 

497 saturationLevel=saturationLevel, 

498 saturationFrac=saturationFrac, 

499 satVal=satVal, satMaskBit=satMaskBit, isTop=True) 

500 

501 # Add a saturation sag column around the saturated core 

502 xSatSag = x 

503 yminSatSag = y - halfWidthY - 100 

504 ymaxSatSag = y - halfWidthY 

505 exposure.image.array[yminSatSag:ymaxSatSag, xSatSag] = satVal*0.7 

506 

507 # Re-measure the saturated footprints 

508 fpList = afwDetection.FootprintSet(exposure.mask, thresh).getFootprints() 

509 

510 satAreas = np.asarray([fp.getArea() for fp in fpList]) 

511 largeAreas, = np.where((satAreas >= 10000) 

512 & (satAreas < 100000)) 

513 

514 for largeAreasIndex in largeAreas: 

515 # We select the new edge bleed footprint 

516 if fpList[largeAreasIndex].getBBox().contains(x, y): 

517 fpCore = fpList[largeAreasIndex] 

518 

519 numPixSatTopEdgeBeforeCase2 = len(np.where(exposure.mask.array[-1, :] == satMaskBit)[0]) 

520 ipIsr.maskITLEdgeBleed(exposure, badAmpDict, 

521 fpCore, itlEdgeBleedSatMinArea=10000, 

522 itlEdgeBleedSatMaxArea=100000, 

523 itlEdgeBleedThreshold=5000., 

524 itlEdgeBleedModelConstant=0.02, 

525 saturatedMaskName='SAT') 

526 numPixSatTopEdgeAfterCase2 = len(np.where(exposure.mask.array[-1, :] == satMaskBit)[0]) 

527 # Check the number of saturated pixels 

528 self.assertEqual(ipIsr.countMaskedPixels(exposure, 'SAT'), 50745) 

529 # Check there are more pixels along the bottom edge 

530 # after masking 

531 self.assertGreater(numPixSatTopEdgeAfterCase2, numPixSatTopEdgeBeforeCase2) 

532 

533 # This will mask the whole column containing the saturation sag 

534 ipIsrFunctions.maskITLSatSag(exposure, fpCore, saturatedMaskName='SAT') 

535 numPixColumnMask = len(np.where(exposure.mask.array[:, xSatSag] == satMaskBit)[0]) 

536 # Test that the saturation sag column is masked 

537 self.assertEqual(numPixColumnMask, exposure.image.array.shape[0]) 

538 

539 # Case 3: 

540 # A saturated core with an edge bleed on both edges 

541 x = 3020 

542 y = 1000 

543 halfWidthX = 70 

544 halfWidthY = 150 

545 halfWidthColX = 7 

546 _setExposureSatCore(exposure, x=x, y=y, halfWidthX=halfWidthX, 

547 halfWidthY=halfWidthY, satVal=satVal, 

548 satMaskBit=satMaskBit) 

549 # Top saturated columns to connect core and edge bleed 

550 limY = exposure.image.array.shape[0]-100 

551 _setExposureSatColumns(exposure, x=x, y=y, halfWidthX=halfWidthColX, 

552 limY=limY, satVal=satVal, satMaskBit=satMaskBit, 

553 isTop=True) 

554 # Bottom saturated columns to connect core and edge bleed 

555 limY = 100 

556 _setExposureSatColumns(exposure, x=x, y=y, halfWidthX=halfWidthColX, 

557 limY=limY, satVal=satVal, satMaskBit=satMaskBit, 

558 isTop=False) 

559 edgeBleedWidth = 150 

560 edgeBleedWidthLimit = 2*halfWidthColX 

561 _makeEdgeBleed(exposure, x=x, extentY=extentY, 

562 edgeBleedWidth=edgeBleedWidth, 

563 edgeBleedConstant=edgeBleedConstant, 

564 edgeBleedWidthLimit=edgeBleedWidthLimit, 

565 saturationLevel=saturationLevel, 

566 saturationFrac=saturationFrac, 

567 satVal=satVal, satMaskBit=satMaskBit, isTop=True) 

568 _makeEdgeBleed(exposure, x=x, extentY=extentY, 

569 edgeBleedWidth=edgeBleedWidth, 

570 edgeBleedConstant=edgeBleedConstant, 

571 edgeBleedWidthLimit=edgeBleedWidthLimit, 

572 saturationLevel=saturationLevel, 

573 saturationFrac=saturationFrac, 

574 satVal=satVal, satMaskBit=satMaskBit, isTop=False) 

575 

576 # Re-measure the saturated footprints 

577 fpList = afwDetection.FootprintSet(exposure.mask, thresh).getFootprints() 

578 

579 satAreas = np.asarray([fp.getArea() for fp in fpList]) 

580 largeAreas, = np.where((satAreas >= 10000) 

581 & (satAreas < 100000)) 

582 

583 for largeAreasIndex in largeAreas: 

584 # We select the new edge bleed footprint 

585 if fpList[largeAreasIndex].getBBox().contains(x, y): 

586 fpCore = fpList[largeAreasIndex] 

587 

588 # Number of saturated pixels at the bottom edge 

589 # before applying masking 

590 numPixSatBottomEdgeBefore = len(np.where(exposure.mask.array[0, :] == satMaskBit)[0]) 

591 # Number of saturated pixels at the top edge 

592 # before applying masking 

593 numPixSatTopEdgeBefore = len(np.where(exposure.mask.array[-1, :] == satMaskBit)[0]) 

594 # Apply edge bleed masking 

595 ipIsr.maskITLEdgeBleed(exposure, badAmpDict, 

596 fpCore, 

597 itlEdgeBleedSatMinArea=10000, 

598 itlEdgeBleedSatMaxArea=100000, 

599 itlEdgeBleedThreshold=5000., 

600 itlEdgeBleedModelConstant=0.02, 

601 saturatedMaskName='SAT') 

602 # Number of saturated pixels at the bottom edge 

603 # after applying edge bleed masking 

604 numPixSatBottomEdgeAfter = len(np.where(exposure.mask.array[0, :] == satMaskBit)[0]) 

605 numPixSatTopEdgeAfter = len(np.where(exposure.mask.array[-1, :] == satMaskBit)[0]) 

606 

607 # Check the number of saturated pixels 

608 self.assertEqual(ipIsr.countMaskedPixels(exposure, 'SAT'), 184669) 

609 # Check there are more pixels along the bottom edge 

610 # after masking 

611 self.assertGreater(numPixSatBottomEdgeAfter, numPixSatBottomEdgeBefore) 

612 # Same with top edge 

613 self.assertGreater(numPixSatTopEdgeAfter, numPixSatTopEdgeBefore) 

614 

615 # Case 4: 

616 # A saturated core near the right edge of the detector 

617 x = 4060 

618 y = 3000 

619 halfWidthX = 30 

620 halfWidthY = 500 

621 halfWidthColX = 3 

622 _setExposureSatCore(exposure, x=x, y=y, halfWidthX=halfWidthX, 

623 halfWidthY=halfWidthY, satVal=satVal, 

624 satMaskBit=satMaskBit) 

625 # Top saturated columns to connect core and edge bleed 

626 limY = exposure.image.array.shape[0]-100 

627 _setExposureSatColumns(exposure, x=x, y=y, halfWidthX=halfWidthColX, 

628 limY=limY, satVal=satVal, satMaskBit=satMaskBit, 

629 isTop=True) 

630 edgeBleedWidth = 100 # this would go beyond the edge of the detector 

631 edgeBleedWidthLimit = 2*halfWidthColX 

632 _makeEdgeBleed(exposure, x=x, extentY=extentY, 

633 edgeBleedWidth=edgeBleedWidth, 

634 edgeBleedConstant=edgeBleedConstant, 

635 edgeBleedWidthLimit=edgeBleedWidthLimit, 

636 saturationLevel=saturationLevel, 

637 saturationFrac=saturationFrac, 

638 satVal=satVal, satMaskBit=satMaskBit, isTop=True) 

639 

640 # Re-measure the saturated footprints 

641 fpList = afwDetection.FootprintSet(exposure.mask, thresh).getFootprints() 

642 

643 satAreas = np.asarray([fp.getArea() for fp in fpList]) 

644 largeAreas, = np.where((satAreas >= 10000) 

645 & (satAreas < 100000)) 

646 

647 for largeAreasIndex in largeAreas: 

648 # We select the new edge bleed footprint 

649 if fpList[largeAreasIndex].getBBox().contains(x, y): 

650 fpCore = fpList[largeAreasIndex] 

651 

652 # Number of saturated pixels at the bottom edge 

653 # before applying masking 

654 numPixSatTopEdgeBefore = len(np.where(exposure.mask.array[-1, :] == satMaskBit)[0]) 

655 # Apply edge bleed masking 

656 ipIsr.maskITLEdgeBleed(exposure, badAmpDict, 

657 fpCore, 

658 itlEdgeBleedSatMinArea=10000, 

659 itlEdgeBleedSatMaxArea=100000, 

660 itlEdgeBleedThreshold=5000., 

661 itlEdgeBleedModelConstant=0.02, 

662 saturatedMaskName='SAT') 

663 # Number of saturated pixels at the bottom edge 

664 # after applying edge bleed masking 

665 numPixSatTopEdgeAfter = len(np.where(exposure.mask.array[-1, :] == satMaskBit)[0]) 

666 

667 # Check the number of saturated pixels 

668 self.assertEqual(ipIsr.countMaskedPixels(exposure, 'SAT'), 238379) 

669 # Check there are more pixels along the bottom edge 

670 # after masking 

671 self.assertGreater(numPixSatTopEdgeAfter, numPixSatTopEdgeBefore) 

672 

673 def test_itlDipMasking(self): 

674 """Test the ITL dip masking.""" 

675 detector = MockITLDetector() 

676 exposure = MockITLExposure(detector.getBBox()) 

677 exposure.setDetector(detector) 

678 exposure.mask.array[:, :] = 0 

679 exposure.mask.addMaskPlane("ITL_DIP") 

680 dipMaskValue = exposure.mask.getPlaneBitMask(["SUSPECT", "ITL_DIP"]) 

681 

682 detectorConfig = ipIsr.overscanAmpConfig.OverscanDetectorConfig() 

683 detectorConfig.itlDipBackgroundFraction = 0.0025 

684 

685 # Set the background to 1000 electrons. 

686 exposure.image.array[:, :] = 1000.0 

687 

688 # Add some saturated masks, below and above the trigger. 

689 satMaskValue = exposure.mask.getPlaneBitMask("SAT") 

690 

691 # Above the threshold in width/height. 

692 exposure.mask.array[ 

693 500: 500 + detectorConfig.itlDipMinHeight * 2, 

694 1000: 1000 + detectorConfig.itlDipMinWidth * 2 

695 ] |= satMaskValue 

696 

697 # At the threshold in width/height. 

698 exposure.mask.array[ 

699 1200: 1200 + detectorConfig.itlDipMinHeight + 1, 

700 1500: 1500 + detectorConfig.itlDipMinWidth + 1 

701 ] |= satMaskValue 

702 

703 # Below the threshold in width. 

704 exposure.mask.array[ 

705 1600: 1600 + detectorConfig.itlDipMinHeight + 1, 

706 2500: 2500 + detectorConfig.itlDipMinWidth // 2 

707 ] |= satMaskValue 

708 

709 # Below the threshold in height. 

710 exposure.mask.array[ 

711 2500: 2500 + detectorConfig.itlDipMinHeight // 2, 

712 3000: 3000 + detectorConfig.itlDipMinWidth + 1 

713 ] |= satMaskValue 

714 

715 with self.assertLogs(level=logging.INFO) as cm: 

716 ipIsr.isrFunctions.maskITLDip(exposure, detectorConfig) 

717 self.assertEqual(len(cm[1]), 2) 

718 self.assertIn("Found ITL dip (width 30; bkg 1000.00); masking column 992 to 1037", cm[1][0]) 

719 self.assertIn("Found ITL dip (width 16; bkg 1000.00); masking column 1495 to 1519", cm[1][1]) 

720 

721 # This includes the scaled edges 

722 np.testing.assert_array_equal(exposure.mask.array[:, 992: 1038] & dipMaskValue, dipMaskValue) 

723 np.testing.assert_array_equal(exposure.mask.array[:, 991] & dipMaskValue, 0) 

724 np.testing.assert_array_equal(exposure.mask.array[:, 1038] & dipMaskValue, 0) 

725 

726 np.testing.assert_array_equal(exposure.mask.array[:, 1495: 1520] & dipMaskValue, dipMaskValue) 

727 np.testing.assert_array_equal(exposure.mask.array[:, 1494] & dipMaskValue, 0) 

728 np.testing.assert_array_equal(exposure.mask.array[:, 1520] & dipMaskValue, 0) 

729 

730 # The other two should not be masked. 

731 np.testing.assert_array_equal(exposure.mask.array[:, 2500] & dipMaskValue, 0) 

732 np.testing.assert_array_equal(exposure.mask.array[:, 3000] & dipMaskValue, 0) 

733 

734 # Change the background to a much lower value, only 1 should be masked. 

735 exposure.mask.array[:, :] &= ~dipMaskValue 

736 exposure.image.array[:, :] = 50.0 

737 

738 with self.assertLogs(level=logging.INFO) as cm: 

739 ipIsr.isrFunctions.maskITLDip(exposure, detectorConfig) 

740 self.assertEqual(len(cm[1]), 1) 

741 self.assertIn("Found ITL dip (width 30; bkg 50.00); masking column 992 to 1037", cm[1][0]) 

742 

743 # This one should be masked. 

744 np.testing.assert_array_equal(exposure.mask.array[:, 992: 1038] & dipMaskValue, dipMaskValue) 

745 np.testing.assert_array_equal(exposure.mask.array[:, 991] & dipMaskValue, 0) 

746 np.testing.assert_array_equal(exposure.mask.array[:, 1038] & dipMaskValue, 0) 

747 

748 # The other three should not be masked. 

749 np.testing.assert_array_equal(exposure.mask.array[:, 1500] & dipMaskValue, 0) 

750 np.testing.assert_array_equal(exposure.mask.array[:, 2500] & dipMaskValue, 0) 

751 np.testing.assert_array_equal(exposure.mask.array[:, 3000] & dipMaskValue, 0) 

752 

753 # And blow things up so that masking is not applied. 

754 # This needs to be in several sections because of the max width. 

755 # We additionally avoid the edge. Note that there are rounding 

756 # offsets that occur due to the scaling such that this setting 

757 # will mask 42 columns per block or a total of 504. 

758 exposure.mask.array[:, :] = 0 

759 detectorConfig.itlDipWidthScale = 1.0 

760 for i in range(12): 

761 exposure.mask.array[:, (i + 1)*100: (i + 1)*100 + 41] |= satMaskValue 

762 

763 maskBak = exposure.mask.array.copy() 

764 with self.assertLogs(level=logging.WARNING) as cm: 

765 ipIsr.isrFunctions.maskITLDip(exposure, detectorConfig) 

766 self.assertEqual(len(cm[1]), 1) 

767 self.assertIn("Too many (504) columns would be masked", cm[1][0]) 

768 np.testing.assert_array_equal(exposure.mask.array, maskBak) 

769 

770 def test_interpolateFromMask(self): 

771 """Expect number of interpolated pixels to be non-zero. 

772 """ 

773 ipIsr.makeThresholdMask(self.mi, 200, growFootprints=2, 

774 maskName='SAT') 

775 for growFootprints in range(0, 3): 

776 for useLegacyInterp in (False, True): 

777 interpMaskedImage = ipIsr.interpolateFromMask(self.mi, 2.0, 

778 growSaturatedFootprints=growFootprints, 

779 maskNameList=['SAT'], 

780 useLegacyInterp=useLegacyInterp) 

781 numBit = ipIsr.countMaskedPixels(interpMaskedImage, "INTRP") 

782 if growFootprints == 0 and not useLegacyInterp: 

783 # All pixel need to be interpolated over. There is 

784 # no external information to interpolate over. In 

785 # the GP code in this case, it is not doing 

786 # anything. 

787 self.assertEqual(numBit, 0, 

788 msg=f"interpolateFromMask with growFootprints={growFootprints}") 

789 else: 

790 self.assertEqual(numBit, 40800, 

791 msg=f"interpolateFromMask with growFootprints={growFootprints}") 

792 

793 def test_saturationCorrectionInterpolate(self): 

794 """Expect number of mask pixels with SAT marked to be non-zero. 

795 """ 

796 corrMaskedImage = ipIsr.saturationCorrection(self.mi, 200, 2.0, 

797 growFootprints=2, interpolate=True, 

798 maskName='SAT') 

799 numBit = ipIsr.countMaskedPixels(corrMaskedImage, "SAT") 

800 self.assertEqual(numBit, 40800) 

801 

802 def test_saturationCorrectionNoInterpolate(self): 

803 """Expect number of mask pixels with SAT marked to be non-zero. 

804 """ 

805 corrMaskedImage = ipIsr.saturationCorrection(self.mi, 200, 2.0, 

806 growFootprints=2, interpolate=False, 

807 maskName='SAT') 

808 numBit = ipIsr.countMaskedPixels(corrMaskedImage, "SAT") 

809 self.assertEqual(numBit, 40800) 

810 

811 def test_trimToMatchCalibBBox(self): 

812 """Expect bounding boxes to match. 

813 """ 

814 darkExp = isrMock.DarkMock().run() 

815 darkMi = darkExp.getMaskedImage() 

816 

817 nEdge = 2 

818 darkMi = darkMi[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL] 

819 newInput = ipIsr.trimToMatchCalibBBox(self.mi, darkMi) 

820 

821 self.assertEqual(newInput.getImage().getBBox(), darkMi.getImage().getBBox()) 

822 

823 def test_darkCorrection(self): 

824 """Expect round-trip application to be equal. 

825 Expect RuntimeError if sizes are different. 

826 """ 

827 darkExp = isrMock.DarkMock().run() 

828 darkMi = darkExp.getMaskedImage() 

829 

830 mi = self.mi.clone() 

831 

832 # The `invert` parameter controls the direction of the 

833 # application. This will apply, and un-apply the dark. 

834 ipIsr.darkCorrection(self.mi, darkMi, 1.0, 1.0, trimToFit=True) 

835 ipIsr.darkCorrection(self.mi, darkMi, 1.0, 1.0, trimToFit=True, invert=True) 

836 

837 self.assertMaskedImagesAlmostEqual(self.mi, mi, atol=1e-3) 

838 

839 darkMi = darkMi[1:-1, 1:-1, afwImage.LOCAL] 

840 with self.assertRaises(RuntimeError): 

841 ipIsr.darkCorrection(self.mi, darkMi, 1.0, 1.0, trimToFit=False) 

842 

843 def test_biasCorrection(self): 

844 """Expect smaller median image value after. 

845 Expect RuntimeError if sizes are different. 

846 """ 

847 biasExp = isrMock.BiasMock().run() 

848 biasMi = biasExp.getMaskedImage() 

849 

850 mi = self.mi.clone() 

851 ipIsr.biasCorrection(self.mi, biasMi, trimToFit=True) 

852 self.assertLess(computeImageMedianAndStd(self.mi.getImage())[0], 

853 computeImageMedianAndStd(mi.getImage())[0]) 

854 

855 biasMi = biasMi[1:-1, 1:-1, afwImage.LOCAL] 

856 with self.assertRaises(RuntimeError): 

857 ipIsr.biasCorrection(self.mi, biasMi, trimToFit=False) 

858 

859 def test_flatCorrection(self): 

860 """Expect round-trip application to be equal. 

861 Expect RuntimeError if sizes are different. 

862 """ 

863 flatExp = isrMock.FlatMock().run() 

864 flatMi = flatExp.getMaskedImage() 

865 

866 mi = self.mi.clone() 

867 for scaling in ('USER', 'MEAN', 'MEDIAN'): 

868 # The `invert` parameter controls the direction of the 

869 # application. This will apply, and un-apply the flat. 

870 ipIsr.flatCorrection(self.mi, flatMi, scaling, userScale=1.0, trimToFit=True) 

871 ipIsr.flatCorrection(self.mi, flatMi, scaling, userScale=1.0, 

872 trimToFit=True, invert=True) 

873 

874 self.assertMaskedImagesAlmostEqual(self.mi, mi, atol=1e-3, 

875 msg=f"flatCorrection with scaling {scaling}") 

876 

877 flatMi = flatMi[1:-1, 1:-1, afwImage.LOCAL] 

878 with self.assertRaises(RuntimeError): 

879 ipIsr.flatCorrection(self.mi, flatMi, 'USER', userScale=1.0, trimToFit=False) 

880 

881 def test_flatCorrectionUnknown(self): 

882 """Raise if an unknown scaling is used. 

883 

884 The `scaling` parameter must be a known type. If not, the 

885 flat correction will raise a RuntimeError. 

886 """ 

887 flatExp = isrMock.FlatMock().run() 

888 flatMi = flatExp.getMaskedImage() 

889 

890 with self.assertRaises(RuntimeError): 

891 ipIsr.flatCorrection(self.mi, flatMi, "UNKNOWN", userScale=1.0, trimToFit=True) 

892 

893 def test_illumCorrection(self): 

894 """Expect larger median value after. 

895 Expect RuntimeError if sizes are different. 

896 """ 

897 flatExp = isrMock.FlatMock().run() 

898 flatMi = flatExp.getMaskedImage() 

899 

900 mi = self.mi.clone() 

901 ipIsr.illuminationCorrection(self.mi, flatMi, 1.0) 

902 self.assertGreater(computeImageMedianAndStd(self.mi.getImage())[0], 

903 computeImageMedianAndStd(mi.getImage())[0]) 

904 

905 flatMi = flatMi[1:-1, 1:-1, afwImage.LOCAL] 

906 with self.assertRaises(RuntimeError): 

907 ipIsr.illuminationCorrection(self.mi, flatMi, 1.0, trimToFit=False) 

908 

909 def test_brighterFatterCorrection(self): 

910 """Expect smoother image/smaller std before. 

911 """ 

912 bfKern = isrMock.BfKernelMock().run() 

913 

914 before = computeImageMedianAndStd(self.inputExp.getImage()) 

915 ipIsr.brighterFatterCorrection(self.inputExp, bfKern, 10, 1e-2, False) 

916 after = computeImageMedianAndStd(self.inputExp.getImage()) 

917 

918 self.assertLess(before[1], after[1]) 

919 

920 def test_gainContext(self): 

921 """Expect image to be unmodified before and after 

922 """ 

923 mi = self.inputExp.getMaskedImage().clone() 

924 with ipIsr.gainContext(self.inputExp, self.inputExp.getImage(), apply=True): 

925 pass 

926 

927 self.assertIsNotNone(mi) 

928 self.assertMaskedImagesAlmostEqual(self.inputExp.getMaskedImage(), mi) 

929 

930 def test_widenSaturationTrails(self): 

931 """Expect more mask pixels with SAT set after. 

932 """ 

933 numBitBefore = ipIsr.countMaskedPixels(self.mi, "SAT") 

934 

935 ipIsr.widenSaturationTrails(self.mi.getMask()) 

936 numBitAfter = ipIsr.countMaskedPixels(self.mi, "SAT") 

937 

938 self.assertGreaterEqual(numBitAfter, numBitBefore) 

939 

940 def test_setBadRegions(self): 

941 """Expect RuntimeError if improper statistic given. 

942 Expect a float value otherwise. 

943 """ 

944 for badStatistic in ('MEDIAN', 'MEANCLIP', 'UNKNOWN'): 

945 if badStatistic == 'UNKNOWN': 

946 with self.assertRaises(RuntimeError, 

947 msg=f"setBadRegions did not fail for stat {badStatistic}"): 

948 nBad, value = ipIsr.setBadRegions(self.inputExp, badStatistic=badStatistic) 

949 else: 

950 nBad, value = ipIsr.setBadRegions(self.inputExp, badStatistic=badStatistic) 

951 self.assertGreaterEqual(abs(value), 0.0, 

952 msg=f"setBadRegions did not find valid value for stat {badStatistic}") 

953 

954 def test_attachTransmissionCurve(self): 

955 """Expect no failure and non-None output from attachTransmissionCurve. 

956 """ 

957 curve = isrMock.TransmissionMock().run() 

958 combined = ipIsr.attachTransmissionCurve(self.inputExp, 

959 opticsTransmission=curve, 

960 filterTransmission=curve, 

961 sensorTransmission=curve, 

962 atmosphereTransmission=curve) 

963 # DM-19707: ip_isr functionality not fully tested by unit tests 

964 self.assertIsNotNone(combined) 

965 

966 def test_attachTransmissionCurve_None(self): 

967 """Expect no failure and non-None output from attachTransmissionCurve. 

968 """ 

969 curve = None 

970 combined = ipIsr.attachTransmissionCurve(self.inputExp, 

971 opticsTransmission=curve, 

972 filterTransmission=curve, 

973 sensorTransmission=curve, 

974 atmosphereTransmission=curve) 

975 # DM-19707: ip_isr functionality not fully tested by unit tests 

976 self.assertIsNotNone(combined) 

977 

978 def test_countMaskedPixels(self): 

979 mockImageConfig = isrMock.IsrMock.ConfigClass() 

980 

981 # flatDrop is not really relevant as we replace the data 

982 # but good to note it in case we change how this image is made 

983 mockImageConfig.flatDrop = 0.99999 

984 mockImageConfig.isTrimmed = True 

985 

986 flatExp = isrMock.FlatMock(config=mockImageConfig).run() 

987 (shapeY, shapeX) = flatExp.getDimensions() 

988 

989 rng = np.random.RandomState(0) 

990 flatMean = 1000 

991 flatWidth = np.sqrt(flatMean) 

992 flatData = rng.normal(flatMean, flatWidth, (shapeX, shapeY)) 

993 flatExp.image.array[:] = flatData 

994 

995 exp = flatExp.clone() 

996 mi = exp.maskedImage 

997 self.assertEqual(ipIsr.countMaskedPixels(mi, 'NO_DATA'), 0) 

998 self.assertEqual(ipIsr.countMaskedPixels(mi, 'BAD'), 0) 

999 

1000 NODATABIT = mi.mask.getPlaneBitMask("NO_DATA") 

1001 noDataBox = geom.Box2I(geom.Point2I(31, 49), geom.Extent2I(3, 6)) 

1002 mi.mask[noDataBox] |= NODATABIT 

1003 

1004 self.assertEqual(ipIsr.countMaskedPixels(mi, 'NO_DATA'), noDataBox.getArea()) 

1005 self.assertEqual(ipIsr.countMaskedPixels(mi, 'BAD'), 0) 

1006 

1007 mi.mask[noDataBox] ^= NODATABIT # XOR to reset what we did 

1008 self.assertEqual(ipIsr.countMaskedPixels(mi, 'NO_DATA'), 0) 

1009 

1010 def test_getExposureGains(self): 

1011 exposure = self.inputExp.clone() 

1012 metadata = exposure.metadata 

1013 amps = exposure.getDetector().getAmplifiers() 

1014 

1015 # Check the default values 

1016 values = ipIsr.getExposureGains(exposure).values() 

1017 self.assertEqual(list(values), len(amps)*[1.0]) 

1018 

1019 # Set values using old ISR keys 

1020 for amp in amps: 

1021 metadata[f"LSST GAIN {amp.getName()}"] = 2.0 

1022 values = ipIsr.getExposureGains(exposure).values() 

1023 self.assertEqual(list(values), len(amps)*[2.0]) 

1024 

1025 # Set values using new ISR keys 

1026 for amp in amps: 

1027 metadata[f"LSST ISR GAIN {amp.getName()}"] = 3.0 

1028 values = ipIsr.getExposureGains(exposure).values() 

1029 self.assertEqual(list(values), len(amps)*[3.0]) 

1030 

1031 def test_getExposureReadNoises(self): 

1032 exposure = self.inputExp.clone() 

1033 metadata = exposure.metadata 

1034 amps = exposure.getDetector().getAmplifiers() 

1035 

1036 # Check the default values 

1037 values = ipIsr.getExposureReadNoises(exposure).values() 

1038 self.assertEqual(list(values), len(amps)*[10.0]) 

1039 

1040 # Set values using old ISR keys 

1041 for amp in amps: 

1042 metadata[f"LSST READNOISE {amp.getName()}"] = 2.0 

1043 values = ipIsr.getExposureReadNoises(exposure).values() 

1044 self.assertEqual(list(values), len(amps)*[2.0]) 

1045 

1046 # Set values using new ISR keys 

1047 for amp in amps: 

1048 metadata[f"LSST ISR READNOISE {amp.getName()}"] = 3.0 

1049 values = ipIsr.getExposureReadNoises(exposure).values() 

1050 self.assertEqual(list(values), len(amps)*[3.0]) 

1051 

1052 def test_compareCameraKeywords(self): 

1053 keywords = ["SEQFILE", "SEQNAME", "VOLTAGE"] 

1054 

1055 # Test success, with with warnings and doRaise. 

1056 exposureMetadata = PropertyList() 

1057 exposureMetadata["SEQFILE"] = "filename" 

1058 exposureMetadata["SEQNAME"] = "v30" 

1059 exposureMetadata["VOLTAGE"] = 100.0 

1060 

1061 calib = ipIsr.IsrCalib() 

1062 calib._metadata["SEQFILE"] = "filename" 

1063 calib._metadata["SEQNAME"] = "v30" 

1064 calib._metadata["VOLTAGE"] = 100.0 

1065 

1066 with self.assertNoLogs(level=logging.WARNING): 

1067 ipIsrFunctions.compareCameraKeywords(False, keywords, exposureMetadata, calib, "test") 

1068 

1069 ipIsrFunctions.compareCameraKeywords(True, keywords, exposureMetadata, calib, "test") 

1070 

1071 # Test warning on missing exposure keyword. 

1072 exposureMetadata["VOLTAGE"] = None 

1073 with self.assertLogs(level=logging.DEBUG) as cm: 

1074 ipIsrFunctions.compareCameraKeywords(False, keywords, exposureMetadata, calib, "test") 

1075 self.assertEqual(len(cm[1]), 1) 

1076 self.assertIn("Sequencer keyword VOLTAGE not found", cm[1][0]) 

1077 

1078 # Test no warning + info on missing calib keywords. 

1079 exposureMetadata["VOLTAGE"] = 100.0 

1080 calib._metadata["SEQFILE"] = None 

1081 calib._metadata["SEQNAME"] = None 

1082 with self.assertNoLogs(level=logging.WARNING): 

1083 ipIsrFunctions.compareCameraKeywords(False, keywords, exposureMetadata, calib, "test") 

1084 with self.assertLogs(level=logging.INFO) as cm: 

1085 ipIsrFunctions.compareCameraKeywords(False, keywords, exposureMetadata, calib, "test") 

1086 self.assertEqual(len(cm[1]), 1) 

1087 self.assertIn("Calibration test missing keywords SEQFILE,SEQNAME", cm[1][0]) 

1088 

1089 # Test mismatch, with warnings and doRaise. 

1090 calib._metadata["VOLTAGE"] = 101.0 

1091 with self.assertLogs(level=logging.WARNING) as cm: 

1092 ipIsrFunctions.compareCameraKeywords(False, keywords, exposureMetadata, calib, "test") 

1093 self.assertEqual(len(cm[1]), 1) 

1094 self.assertIn("Sequencer mismatch", cm[1][0]) 

1095 self.assertIn("ISR TEST SEQUENCER MISMATCH", exposureMetadata) 

1096 self.assertEqual(exposureMetadata["ISR TEST SEQUENCER MISMATCH"], True) 

1097 

1098 with self.assertRaises(RuntimeError): 

1099 ipIsrFunctions.compareCameraKeywords(True, keywords, exposureMetadata, calib, "test") 

1100 

1101 

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

1103 pass 

1104 

1105 

1106def setup_module(module): 

1107 lsst.utils.tests.init() 

1108 

1109 

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

1111 lsst.utils.tests.init() 

1112 unittest.main()