Coverage for tests / test_defects.py: 8%

553 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-17 09:35 +0000

1# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2017 AURA/LSST. 

5# 

6# This product includes software developed by the 

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

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 LSST License Statement and 

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

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

22# 

23"""Test cases for lsst.cp.pipe.MeasureDefectsTask.""" 

24 

25import unittest 

26import numpy as np 

27import copy 

28from scipy.interpolate import Akima1DInterpolator 

29 

30import lsst.utils 

31import lsst.utils.tests 

32 

33import lsst.afw.image as afwImage 

34import lsst.ip.isr as ipIsr 

35import lsst.cp.pipe as cpPipe 

36from lsst.ip.isr import isrMock, countMaskedPixels 

37from lsst.geom import Box2I, Point2I, Extent2I 

38from lsst.daf.base import PropertyList 

39 

40 

41class MeasureDefectsTaskTestCase(lsst.utils.tests.TestCase): 

42 """A test case for the defect finding task.""" 

43 

44 def setUp(self): 

45 self.defaultConfig = cpPipe.MeasureDefectsTask.ConfigClass() 

46 

47 self.flatMean = 2000 

48 self.darkMean = 1 

49 self.readNoiseAdu = 10 

50 self.nSigmaBright = 8 

51 self.nSigmaDark = 8 

52 

53 mockImageConfig = isrMock.IsrMock.ConfigClass() 

54 

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

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

57 mockImageConfig.flatDrop = 0.99999 

58 mockImageConfig.isTrimmed = True 

59 

60 self.flatExp = isrMock.FlatMock(config=mockImageConfig).run() 

61 (shapeY, shapeX) = self.flatExp.getDimensions() 

62 # x, y, size tuples 

63 # always put edge defects at the start and change the value of nEdge 

64 

65 self.brightDefects = [(0, 15, 3, 3), (100, 123, 1, 1)] 

66 

67 self.darkDefects = [(5, 0, 1, 1), (7, 62, 2, 2)] 

68 

69 nEdge = 1 # NOTE: update if more edge defects are included 

70 self.noEdges = slice(nEdge, None) 

71 self.onlyEdges = slice(0, nEdge) 

72 

73 self.darkBBoxes = [Box2I(Point2I(x, y), Extent2I(sx, sy)) for (x, y, sx, sy) in self.darkDefects] 

74 self.brightBBoxes = [Box2I(Point2I(x, y), Extent2I(sx, sy)) for (x, y, sx, sy) in self.brightDefects] 

75 

76 flatWidth = np.sqrt(self.flatMean) + self.readNoiseAdu 

77 darkWidth = self.readNoiseAdu 

78 self.rng = np.random.RandomState(0) 

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

80 darkData = self.rng.normal(self.darkMean, darkWidth, (shapeX, shapeY)) 

81 

82 # NOTE: darks and flats have same defects applied deliberately to both 

83 for defect in self.brightDefects: 

84 y, x, sy, sx = defect 

85 # are these actually the numbers we want? 

86 flatData[x:x+sx, y:y+sy] += self.nSigmaBright * flatWidth 

87 darkData[x:x+sx, y:y+sy] += self.nSigmaBright * darkWidth 

88 

89 for defect in self.darkDefects: 

90 y, x, sy, sx = defect 

91 # are these actually the numbers we want? 

92 flatData[x:x+sx, y:y+sy] -= self.nSigmaDark * flatWidth 

93 darkData[x:x+sx, y:y+sy] -= self.nSigmaDark * darkWidth 

94 

95 self.darkExp = self.flatExp.clone() 

96 self.spareImage = self.flatExp.clone() # for testing edge bits and misc 

97 

98 self.flatExp.image.array[:] = flatData 

99 self.darkExp.image.array[:] = darkData 

100 

101 self.defaultTask = cpPipe.MeasureDefectsTask() 

102 

103 self.allDefectsList = ipIsr.Defects() 

104 self.brightDefectsList = ipIsr.Defects() 

105 self.darkDefectsList = ipIsr.Defects() 

106 

107 # Set image types, the defects code will use them. 

108 metaDataFlat = PropertyList() 

109 metaDataFlat["IMGTYPE"] = "FLAT" 

110 self.flatExp.setMetadata(metaDataFlat) 

111 

112 metaDataDark = PropertyList() 

113 metaDataDark["IMGTYPE"] = "DARK" 

114 self.darkExp.setMetadata(metaDataDark) 

115 

116 with self.allDefectsList.bulk_update(): 

117 with self.brightDefectsList.bulk_update(): 

118 for d in self.brightBBoxes: 

119 self.brightDefectsList.append(d) 

120 self.allDefectsList.append(d) 

121 

122 with self.darkDefectsList.bulk_update(): 

123 for d in self.darkBBoxes: 

124 self.darkDefectsList.append(d) 

125 self.allDefectsList.append(d) 

126 

127 def check_maskBlocks(self, inputDefects, expectedDefects): 

128 """A helper function for the tests of 

129 maskBlocksIfIntermitentBadPixelsInColumn. 

130 

131 """ 

132 config = copy.copy(self.defaultConfig) 

133 config.badOnAndOffPixelColumnThreshold = 10 

134 config.goodPixelColumnGapThreshold = 5 

135 config.nPixBorderUpDown = 0 

136 config.nPixBorderLeftRight = 0 

137 

138 task = self.defaultTask 

139 task.config = config 

140 

141 defectsWithColumns, count = task.maskBlocksIfIntermitentBadPixelsInColumn(inputDefects) 

142 boxesMeasured = [] 

143 for defect in defectsWithColumns: 

144 boxesMeasured.append(defect.getBBox()) 

145 

146 for boxInput in expectedDefects: 

147 self.assertIn(boxInput, boxesMeasured) 

148 

149 # Check that the code did not mask anything extra by 

150 # looking in both the input list and "expanded-column" list. 

151 unionInputExpectedBoxes = [] 

152 for defect in inputDefects: 

153 unionInputExpectedBoxes.append(defect.getBBox()) 

154 for defect in expectedDefects: 

155 unionInputExpectedBoxes.append(defect) 

156 

157 # Check that code doesn't mask more than it is supposed to. 

158 for boxMeas in boxesMeasured: 

159 self.assertIn(boxMeas, unionInputExpectedBoxes) 

160 

161 def test_maskBlocks_full_column(self): 

162 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

163 

164 Tests that a contigous bad column does not get split by the 

165 code. 

166 

167 The mock flat has a size of 200X204 pixels. This column has a 

168 maximum length of 50 pixels, otherwise there would be a split 

169 along the mock amp boundary. 

170 

171 Plots can be found in DM-19903 on Jira. 

172 

173 """ 

174 

175 defects = self.allDefectsList 

176 defects.append(Box2I(corner=Point2I(15, 1), dimensions=Extent2I(1, 50))) 

177 expectedDefects = [Box2I(corner=Point2I(15, 1), dimensions=Extent2I(1, 50))] 

178 

179 self.check_maskBlocks(defects, expectedDefects) 

180 

181 def test_maskBlocks_long_column(self): 

182 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

183 

184 Tests that a contigous bad column with Npix >= 

185 badOnAndOffPixelColumnThreshold (10) does not get split by the 

186 code. 

187 

188 Plots can be found in DM-19903 on Jira. 

189 

190 """ 

191 

192 expectedDefects = [Box2I(corner=Point2I(20, 1), dimensions=Extent2I(1, 25))] 

193 defects = self.allDefectsList 

194 defects.append(Box2I(corner=Point2I(20, 1), dimensions=Extent2I(1, 25))) 

195 

196 self.check_maskBlocks(defects, expectedDefects) 

197 

198 def test_maskBlocks_short_column(self): 

199 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

200 

201 Tests that a contigous bad column Npix < 

202 badOnAndOffPixelColumnThreshold (10) does not get split by the 

203 code. 

204 

205 Plots can be found in DM-19903 on Jira. 

206 

207 """ 

208 

209 expectedDefects = [Box2I(corner=Point2I(25, 1), dimensions=Extent2I(1, 8))] 

210 defects = self.allDefectsList 

211 defects.append(Box2I(corner=Point2I(25, 1), dimensions=Extent2I(1, 8))) 

212 

213 self.check_maskBlocks(defects, expectedDefects) 

214 

215 def test_maskBlocks_discontigous_to_single_block(self): 

216 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

217 

218 Npix discontiguous bad pixels in a column where Npix >= 

219 badOnAndOffPixelColumnThreshold (10) and gaps of good pixels < 

220 goodPixelColumnGapThreshold (5). Under these conditions, the 

221 whole block of bad pixels (including good gaps) should be 

222 masked. 

223 

224 Plots can be found in DM-19903 on Jira. 

225 

226 """ 

227 

228 expectedDefects = [Box2I(corner=Point2I(30, 1), dimensions=Extent2I(1, 48))] 

229 defects = self.allDefectsList 

230 badPixels = [Box2I(corner=Point2I(30, 1), dimensions=Extent2I(1, 2)), 

231 Box2I(corner=Point2I(30, 5), dimensions=Extent2I(1, 3)), 

232 Box2I(corner=Point2I(30, 11), dimensions=Extent2I(1, 5)), 

233 Box2I(corner=Point2I(30, 19), dimensions=Extent2I(1, 5)), 

234 Box2I(corner=Point2I(30, 27), dimensions=Extent2I(1, 4)), 

235 Box2I(corner=Point2I(30, 34), dimensions=Extent2I(1, 15))] 

236 with defects.bulk_update(): 

237 for badBox in badPixels: 

238 defects.append(badBox) 

239 

240 self.check_maskBlocks(defects, expectedDefects) 

241 

242 def test_maskBlocks_discontigous_less_than_thresholds(self): 

243 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

244 

245 Npix discontiguous bad pixels in a column where Npix < 

246 badOnAndOffPixelColumnThreshold (10) and gaps of good pixels < 

247 goodPixelColumnGapThreshold (5). Under these conditions, the 

248 expected defect boxes should be the same as the input boxes. 

249 

250 Plots can be found in DM-19903 on Jira. 

251 

252 """ 

253 

254 expectedDefects = [Box2I(corner=Point2I(35, 1), dimensions=Extent2I(1, 2)), 

255 Box2I(corner=Point2I(35, 5), dimensions=Extent2I(1, 3)), 

256 Box2I(corner=Point2I(35, 11), dimensions=Extent2I(1, 2))] 

257 defects = self.allDefectsList 

258 badPixels = [Box2I(corner=Point2I(35, 1), dimensions=Extent2I(1, 2)), 

259 Box2I(corner=Point2I(35, 5), dimensions=Extent2I(1, 3)), 

260 Box2I(corner=Point2I(35, 11), dimensions=Extent2I(1, 2))] 

261 with defects.bulk_update(): 

262 for badBox in badPixels: 

263 defects.append(badBox) 

264 

265 self.check_maskBlocks(defects, expectedDefects) 

266 

267 def test_maskBlocks_more_than_thresholds(self): 

268 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

269 

270 Npix discontiguous bad pixels in a column where Npix < 

271 badOnAndOffPixelColumnThreshold (10) and gaps of good pixels < 

272 goodPixelColumnGapThreshold (5). Npix=34 (> 10) bad pixels 

273 total, 1 "good" gap with 13 pixels big enough (13 >= 5 good 

274 pixels, from y=6 (1+5) to y=19). 

275 

276 Plots can be found in DM-19903 on Jira. 

277 

278 """ 

279 

280 expectedDefects = [Box2I(corner=Point2I(40, 1), dimensions=Extent2I(1, 7)), 

281 Box2I(corner=Point2I(40, 19), dimensions=Extent2I(1, 30))] 

282 defects = self.allDefectsList 

283 badPixels = [Box2I(corner=Point2I(40, 1), dimensions=Extent2I(1, 2)), 

284 Box2I(corner=Point2I(40, 5), dimensions=Extent2I(1, 3)), 

285 Box2I(corner=Point2I(40, 19), dimensions=Extent2I(1, 5)), 

286 Box2I(corner=Point2I(40, 27), dimensions=Extent2I(1, 4)), 

287 Box2I(corner=Point2I(40, 34), dimensions=Extent2I(1, 15))] 

288 with defects.bulk_update(): 

289 for badBox in badPixels: 

290 defects.append(badBox) 

291 

292 self.check_maskBlocks(defects, expectedDefects) 

293 

294 def test_maskBlocks_not_enough_bad_pixels_in_column(self): 

295 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

296 

297 Npix discontiguous bad pixels in a column where Npix < 

298 badOnAndOffPixelColumnThreshold (10) and and gaps of good 

299 pixels > goodPixelColumnGapThreshold (5). Since Npix < 

300 badOnAndOffPixelColumnThreshold, then it doesn't matter that 

301 the number of good pixels in gap > 

302 goodPixelColumnGapThreshold. 5<10 bad pixels total, 1 "good" 

303 gap big enough (29>=5 good pixels, from y =12 (10+2) to y=30) 

304 

305 Plots can be found in DM-19903 on Jira. 

306 

307 """ 

308 

309 expectedDefects = [Box2I(corner=Point2I(45, 10), dimensions=Extent2I(1, 2)), 

310 Box2I(corner=Point2I(45, 30), dimensions=Extent2I(1, 3))] 

311 defects = self.allDefectsList 

312 badPixels = [Box2I(corner=Point2I(45, 10), dimensions=Extent2I(1, 2)), 

313 Box2I(corner=Point2I(45, 30), dimensions=Extent2I(1, 3))] 

314 with defects.bulk_update(): 

315 for badBox in badPixels: 

316 defects.append(badBox) 

317 

318 self.check_maskBlocks(defects, expectedDefects) 

319 

320 def test_maskBlocks_every_other_pixel_bad_greater_than_threshold(self): 

321 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

322 

323 Npix discontiguous bad pixels in a column where Npix > 

324 badOnAndOffPixelColumnThreshold (10) and every other pixel is 

325 bad. 

326 

327 Plots can be found in DM-19903 on Jira. 

328 

329 """ 

330 

331 expectedDefects = [Box2I(corner=Point2I(50, 10), dimensions=Extent2I(1, 31))] 

332 defects = self.allDefectsList 

333 badPixels = [Box2I(corner=Point2I(50, 10), dimensions=Extent2I(1, 1)), 

334 Box2I(corner=Point2I(50, 12), dimensions=Extent2I(1, 1)), 

335 Box2I(corner=Point2I(50, 14), dimensions=Extent2I(1, 1)), 

336 Box2I(corner=Point2I(50, 16), dimensions=Extent2I(1, 1)), 

337 Box2I(corner=Point2I(50, 18), dimensions=Extent2I(1, 1)), 

338 Box2I(corner=Point2I(50, 20), dimensions=Extent2I(1, 1)), 

339 Box2I(corner=Point2I(50, 22), dimensions=Extent2I(1, 1)), 

340 Box2I(corner=Point2I(50, 24), dimensions=Extent2I(1, 1)), 

341 Box2I(corner=Point2I(50, 26), dimensions=Extent2I(1, 1)), 

342 Box2I(corner=Point2I(50, 28), dimensions=Extent2I(1, 1)), 

343 Box2I(corner=Point2I(50, 30), dimensions=Extent2I(1, 1)), 

344 Box2I(corner=Point2I(50, 32), dimensions=Extent2I(1, 1)), 

345 Box2I(corner=Point2I(50, 34), dimensions=Extent2I(1, 1)), 

346 Box2I(corner=Point2I(50, 36), dimensions=Extent2I(1, 1)), 

347 Box2I(corner=Point2I(50, 38), dimensions=Extent2I(1, 1)), 

348 Box2I(corner=Point2I(50, 40), dimensions=Extent2I(1, 1))] 

349 with defects.bulk_update(): 

350 for badBox in badPixels: 

351 defects.append(badBox) 

352 

353 self.check_maskBlocks(defects, expectedDefects) 

354 

355 def test_maskBlocks_every_other_pixel_bad_less_than_threshold(self): 

356 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

357 

358 Npix discontiguous bad pixels in a column where Npix > 

359 badOnAndOffPixelColumnThreshold (10) and every other pixel is 

360 bad. 

361 

362 Plots can be found in DM-19903 on Jira. 

363 

364 """ 

365 

366 expectedDefects = [Box2I(corner=Point2I(55, 20), dimensions=Extent2I(1, 1)), 

367 Box2I(corner=Point2I(55, 22), dimensions=Extent2I(1, 1)), 

368 Box2I(corner=Point2I(55, 24), dimensions=Extent2I(1, 1)), 

369 Box2I(corner=Point2I(55, 26), dimensions=Extent2I(1, 1)), 

370 Box2I(corner=Point2I(55, 28), dimensions=Extent2I(1, 1)), 

371 Box2I(corner=Point2I(55, 30), dimensions=Extent2I(1, 1))] 

372 defects = self.allDefectsList 

373 badPixels = [Box2I(corner=Point2I(55, 20), dimensions=Extent2I(1, 1)), 

374 Box2I(corner=Point2I(55, 22), dimensions=Extent2I(1, 1)), 

375 Box2I(corner=Point2I(55, 24), dimensions=Extent2I(1, 1)), 

376 Box2I(corner=Point2I(55, 26), dimensions=Extent2I(1, 1)), 

377 Box2I(corner=Point2I(55, 28), dimensions=Extent2I(1, 1)), 

378 Box2I(corner=Point2I(55, 30), dimensions=Extent2I(1, 1))] 

379 with defects.bulk_update(): 

380 for badBox in badPixels: 

381 defects.append(badBox) 

382 

383 self.check_maskBlocks(defects, expectedDefects) 

384 

385 def test_maskBlocks_blobs_one_side_good_less_than_threshold(self): 

386 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

387 

388 Npix discontiguous bad pixels in column with "blobs" of "m" 

389 bad pixels to one side, m > badOnAndOffPixelColumnThreshold 

390 (10), number of good pixel in gaps between blobs < 

391 goodPixelColumnGapThreshold (5). 

392 

393 Plots can be found in DM-19903 on Jira. 

394 

395 """ 

396 

397 expectedDefects = [Box2I(corner=Point2I(60, 1), dimensions=Extent2I(1, 29)), 

398 Box2I(corner=Point2I(61, 2), dimensions=Extent2I(2, 12))] 

399 defects = self.allDefectsList 

400 badPixels = [Box2I(corner=Point2I(60, 1), dimensions=Extent2I(1, 18)), 

401 Box2I(corner=Point2I(60, 20), dimensions=Extent2I(1, 10)), 

402 Box2I(corner=Point2I(61, 2), dimensions=Extent2I(2, 2)), 

403 Box2I(corner=Point2I(61, 6), dimensions=Extent2I(2, 8))] 

404 with defects.bulk_update(): 

405 for badBox in badPixels: 

406 defects.append(badBox) 

407 

408 self.check_maskBlocks(defects, expectedDefects) 

409 

410 def test_maskBlocks_blobs_other_side_good_less_than_threshold(self): 

411 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

412 

413 Npix discontiguous bad pixels in column with "blobs" of "m" 

414 bad pixels to the other side, m > 

415 badOnAndOffPixelColumnThreshold (10), number of good pixel in 

416 gaps between blobs < goodPixelColumnGapThreshold (5). 

417 

418 Plots can be found in DM-19903 on Jira. 

419 

420 """ 

421 

422 expectedDefects = [Box2I(corner=Point2I(70, 1), dimensions=Extent2I(1, 29)), 

423 Box2I(corner=Point2I(68, 2), dimensions=Extent2I(2, 12))] 

424 defects = self.allDefectsList 

425 badPixels = [Box2I(corner=Point2I(70, 1), dimensions=Extent2I(1, 18)), 

426 Box2I(corner=Point2I(70, 20), dimensions=Extent2I(1, 10)), 

427 Box2I(corner=Point2I(68, 2), dimensions=Extent2I(2, 2)), 

428 Box2I(corner=Point2I(68, 6), dimensions=Extent2I(2, 8))] 

429 with defects.bulk_update(): 

430 for badBox in badPixels: 

431 defects.append(badBox) 

432 

433 self.check_maskBlocks(defects, expectedDefects) 

434 

435 def test_maskBlocks_blob_both_sides_good_less_than_threshold(self): 

436 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

437 

438 Npix discontiguous bad pixels in column with "blobs" of "m" 

439 bad pixels to both sides, m > badOnAndOffPixelColumnThreshold 

440 (10), number of good pixel in gaps between blobs < 

441 goodPixelColumnGapThreshold (5). 

442 

443 Plots can be found in DM-19903 on Jira. 

444 

445 """ 

446 

447 expectedDefects = [Box2I(corner=Point2I(75, 1), dimensions=Extent2I(1, 29)), 

448 Box2I(corner=Point2I(73, 2), dimensions=Extent2I(2, 12)), 

449 Box2I(corner=Point2I(76, 2), dimensions=Extent2I(2, 12))] 

450 defects = self.allDefectsList 

451 badPixels = [Box2I(corner=Point2I(75, 1), dimensions=Extent2I(1, 18)), 

452 Box2I(corner=Point2I(75, 20), dimensions=Extent2I(1, 10)), 

453 Box2I(corner=Point2I(73, 2), dimensions=Extent2I(2, 2)), 

454 Box2I(corner=Point2I(73, 6), dimensions=Extent2I(2, 8)), 

455 Box2I(corner=Point2I(76, 2), dimensions=Extent2I(2, 2)), 

456 Box2I(corner=Point2I(76, 6), dimensions=Extent2I(2, 8))] 

457 with defects.bulk_update(): 

458 for badBox in badPixels: 

459 defects.append(badBox) 

460 

461 self.check_maskBlocks(defects, expectedDefects) 

462 

463 def test_maskBlocks_blob_one_side_good_greater_than_threshold(self): 

464 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

465 

466 Npix discontiguous bad pixels in column with "blobs" of "m" 

467 bad pixels to one side, m > badOnAndOffPixelColumnThreshold 

468 (10), number of good pixel in gaps between blobs > 

469 goodPixelColumnGapThreshold (5). 

470 

471 Plots can be found in DM-19903 on Jira. 

472 

473 """ 

474 

475 expectedDefects = [Box2I(corner=Point2I(80, 1), dimensions=Extent2I(1, 29)), 

476 Box2I(corner=Point2I(81, 2), dimensions=Extent2I(2, 2)), 

477 Box2I(corner=Point2I(81, 8), dimensions=Extent2I(2, 8))] 

478 defects = self.allDefectsList 

479 badPixels = [Box2I(corner=Point2I(80, 1), dimensions=Extent2I(1, 18)), 

480 Box2I(corner=Point2I(80, 20), dimensions=Extent2I(1, 10)), 

481 Box2I(corner=Point2I(81, 2), dimensions=Extent2I(2, 2)), 

482 Box2I(corner=Point2I(81, 8), dimensions=Extent2I(2, 8))] 

483 with defects.bulk_update(): 

484 for badBox in badPixels: 

485 defects.append(badBox) 

486 

487 self.check_maskBlocks(defects, expectedDefects) 

488 

489 def test_maskBlocks_other_side_good_greater_than_threshold(self): 

490 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

491 

492 Npix discontiguous bad pixels in column with "blobs" of "m" 

493 bad pixels to the other side, m > 

494 badOnAndOffPixelColumnThreshold (10), number of good pixel in 

495 gaps between blobs > goodPixelColumnGapThreshold (5). 

496 

497 Plots can be found in DM-19903 on Jira. 

498 

499 """ 

500 

501 expectedDefects = [Box2I(corner=Point2I(87, 1), dimensions=Extent2I(1, 29)), 

502 Box2I(corner=Point2I(85, 2), dimensions=Extent2I(2, 2)), 

503 Box2I(corner=Point2I(85, 8), dimensions=Extent2I(2, 8))] 

504 defects = self.allDefectsList 

505 badPixels = [Box2I(corner=Point2I(87, 1), dimensions=Extent2I(1, 18)), 

506 Box2I(corner=Point2I(87, 20), dimensions=Extent2I(1, 10)), 

507 Box2I(corner=Point2I(85, 2), dimensions=Extent2I(2, 2)), 

508 Box2I(corner=Point2I(85, 8), dimensions=Extent2I(2, 8))] 

509 with defects.bulk_update(): 

510 for badBox in badPixels: 

511 defects.append(badBox) 

512 

513 self.check_maskBlocks(defects, expectedDefects) 

514 

515 def test_maskBlocks_both_sides_good_greater_than_threshold(self): 

516 """A test for maskBlocksIfIntermitentBadPixelsInColumn. 

517 

518 Npix discontiguous bad pixels in column with "blobs" of "m" 

519 bad pixels to both sides, m > badOnAndOffPixelColumnThreshold 

520 (10), number of good pixel in gaps between blobs > 

521 goodPixelColumnGapThreshold (5). 

522 

523 Plots can be found in DM-19903 on Jira. 

524 

525 """ 

526 

527 expectedDefects = [Box2I(corner=Point2I(93, 1), dimensions=Extent2I(1, 34)), 

528 Box2I(corner=Point2I(91, 2), dimensions=Extent2I(2, 7)), 

529 Box2I(corner=Point2I(91, 18), dimensions=Extent2I(2, 9)), 

530 Box2I(corner=Point2I(94, 2), dimensions=Extent2I(2, 7)), 

531 Box2I(corner=Point2I(94, 18), dimensions=Extent2I(2, 9))] 

532 defects = self.allDefectsList 

533 badPixels = [Box2I(corner=Point2I(93, 1), dimensions=Extent2I(1, 12)), 

534 Box2I(corner=Point2I(93, 15), dimensions=Extent2I(1, 20)), 

535 Box2I(corner=Point2I(91, 2), dimensions=Extent2I(2, 2)), 

536 Box2I(corner=Point2I(91, 7), dimensions=Extent2I(2, 2)), 

537 Box2I(corner=Point2I(94, 2), dimensions=Extent2I(2, 2)), 

538 Box2I(corner=Point2I(94, 7), dimensions=Extent2I(2, 2)), 

539 Box2I(corner=Point2I(91, 18), dimensions=Extent2I(2, 3)), 

540 Box2I(corner=Point2I(91, 24), dimensions=Extent2I(2, 3)), 

541 Box2I(corner=Point2I(94, 18), dimensions=Extent2I(2, 3)), 

542 Box2I(corner=Point2I(94, 24), dimensions=Extent2I(2, 3))] 

543 with defects.bulk_update(): 

544 for badBox in badPixels: 

545 defects.append(badBox) 

546 

547 self.check_maskBlocks(defects, expectedDefects) 

548 

549 def test_maskBlocks_y_out_of_order_dm38103(self): 

550 """A test for maskBlocksIfIntermitentBadPixelsInColumn, y out of order. 

551 

552 This test is a variant of 

553 test_maskBlocks_every_other_pixel_bad_greater_than_threshold with 

554 an extra out-of-y-order bad pixel to trigger DM-38103. 

555 """ 

556 expectedDefects = [Box2I(corner=Point2I(50, 110), dimensions=Extent2I(1, 31))] 

557 defects = self.allDefectsList 

558 badPixels = [Box2I(corner=Point2I(50, 110), dimensions=Extent2I(1, 1)), 

559 Box2I(corner=Point2I(50, 112), dimensions=Extent2I(1, 1)), 

560 Box2I(corner=Point2I(50, 114), dimensions=Extent2I(1, 1)), 

561 Box2I(corner=Point2I(50, 116), dimensions=Extent2I(1, 1)), 

562 Box2I(corner=Point2I(50, 118), dimensions=Extent2I(1, 1)), 

563 Box2I(corner=Point2I(50, 120), dimensions=Extent2I(1, 1)), 

564 Box2I(corner=Point2I(50, 122), dimensions=Extent2I(1, 1)), 

565 Box2I(corner=Point2I(50, 124), dimensions=Extent2I(1, 1)), 

566 Box2I(corner=Point2I(50, 126), dimensions=Extent2I(1, 1)), 

567 Box2I(corner=Point2I(50, 128), dimensions=Extent2I(1, 1)), 

568 Box2I(corner=Point2I(50, 130), dimensions=Extent2I(1, 1)), 

569 Box2I(corner=Point2I(50, 132), dimensions=Extent2I(1, 1)), 

570 Box2I(corner=Point2I(50, 134), dimensions=Extent2I(1, 1)), 

571 Box2I(corner=Point2I(50, 136), dimensions=Extent2I(1, 1)), 

572 Box2I(corner=Point2I(50, 138), dimensions=Extent2I(1, 1)), 

573 Box2I(corner=Point2I(50, 140), dimensions=Extent2I(1, 1)), 

574 # This last point is out of order in y. 

575 Box2I(corner=Point2I(50, 100), dimensions=Extent2I(1, 1))] 

576 

577 # Force defect normalization off in order to trigger DM-38301, because 

578 # defects.fromFootprintList() which is called by _findHotAndColdPixels 

579 # does not do normalization. 

580 defects._bulk_update = True 

581 for badBox in badPixels: 

582 defects.append(badBox) 

583 defects._bulk_update = False 

584 

585 self.check_maskBlocks(defects, expectedDefects) 

586 

587 def check_maskBadColumns(self, exp, inputDefects, expectedDefects): 

588 """A helper function for the tests of 

589 maskBadColumns. 

590 

591 """ 

592 config = copy.copy(self.defaultConfig) 

593 config.badPixelsToFillColumnThreshold = 25 

594 config.saturatedPixelsToFillColumnThreshold = 5 

595 

596 task = self.defaultTask 

597 task.config = config 

598 

599 defectsWithColumns, count = task.maskBadColumns(exp, inputDefects) 

600 

601 self.assertEqual(count, len(expectedDefects)) 

602 

603 boxesMeasured = [] 

604 for defect in defectsWithColumns: 

605 boxesMeasured.append(defect.getBBox()) 

606 

607 for boxInput in expectedDefects: 

608 self.assertIn(boxInput, boxesMeasured) 

609 

610 # Check that the code did not mask anything extra by 

611 # looking in both the input list and "expanded-column" list. 

612 unionInputExpectedBoxes = [] 

613 for defect in inputDefects: 

614 unionInputExpectedBoxes.append(defect.getBBox()) 

615 for defect in expectedDefects: 

616 unionInputExpectedBoxes.append(defect) 

617 

618 # Check that code doesn't mask more than it is supposed to. 

619 for boxMeas in boxesMeasured: 

620 self.assertIn(boxMeas, unionInputExpectedBoxes) 

621 

622 def test_maskBadColumns_extend_full_columns(self): 

623 """Test maskBadColumns, extend to full column. 

624 """ 

625 expectedDefects = [Box2I(corner=Point2I(20, 0), dimensions=Extent2I(1, 51)), 

626 Box2I(corner=Point2I(150, 0), dimensions=Extent2I(1, 51)), 

627 Box2I(corner=Point2I(50, 153), dimensions=Extent2I(1, 51))] 

628 defects = self.allDefectsList 

629 defects.append(Box2I(corner=Point2I(20, 10), dimensions=Extent2I(1, 30))) 

630 defects.append(Box2I(corner=Point2I(150, 5), dimensions=Extent2I(1, 25))) 

631 defects.append(Box2I(corner=Point2I(50, 170), dimensions=Extent2I(1, 30))) 

632 

633 self.check_maskBadColumns(self.flatExp, defects, expectedDefects) 

634 

635 def test_maskBadColumns_no_extend_partial_columns(self): 

636 """Test maskBadColumns, do not extend to full column. 

637 """ 

638 expectedDefects = [] 

639 defects = self.allDefectsList 

640 defects.append(Box2I(corner=Point2I(20, 10), dimensions=Extent2I(1, 20))) 

641 defects.append(Box2I(corner=Point2I(150, 5), dimensions=Extent2I(1, 22))) 

642 defects.append(Box2I(corner=Point2I(50, 170), dimensions=Extent2I(1, 24))) 

643 

644 self.check_maskBadColumns(self.flatExp, defects, expectedDefects) 

645 

646 def test_maskBadColumns_extend_saturated_columns(self): 

647 """Test maskBadColumns, extend saturation to full column. 

648 """ 

649 exp = self.flatExp.clone() 

650 

651 mask = afwImage.Mask.getPlaneBitMask("SAT") 

652 

653 expectedDefects = [Box2I(corner=Point2I(20, 0), dimensions=Extent2I(1, 51)), 

654 Box2I(corner=Point2I(150, 0), dimensions=Extent2I(1, 51)), 

655 Box2I(corner=Point2I(50, 153), dimensions=Extent2I(1, 51))] 

656 defects = self.allDefectsList 

657 

658 # These defects are too small to trigger the former column extension 

659 # (as tested in test_maskBadColumns_no_extend_partial_columns) but 

660 # should still trigger the saturation extension code. 

661 satColumns = [Box2I(corner=Point2I(20, 10), dimensions=Extent2I(1, 5)), 

662 Box2I(corner=Point2I(150, 5), dimensions=Extent2I(1, 5)), 

663 Box2I(corner=Point2I(50, 170), dimensions=Extent2I(1, 5))] 

664 for satColumn in satColumns: 

665 exp.mask[satColumn] |= mask 

666 defects.append(satColumn) 

667 

668 self.check_maskBadColumns(exp, defects, expectedDefects) 

669 

670 def check_dilateSaturatedColumns(self, exp, inputDefects, expectedDefects): 

671 config = copy.copy(self.defaultConfig) 

672 config.saturatedColumnDilationRadius = 2 

673 

674 task = self.defaultTask 

675 task.config = config 

676 

677 defectsDilated = task.dilateSaturatedColumns(exp, inputDefects) 

678 

679 boxesMeasured = [] 

680 for defect in defectsDilated: 

681 boxesMeasured.append(defect.getBBox()) 

682 

683 for boxInput in expectedDefects: 

684 self.assertIn(boxInput, boxesMeasured) 

685 

686 # Check that the code did not mask anything extra by 

687 # looking in both the input list and "expanded-column" list. 

688 unionInputExpectedBoxes = [] 

689 for defect in inputDefects: 

690 unionInputExpectedBoxes.append(defect.getBBox()) 

691 for defect in expectedDefects: 

692 unionInputExpectedBoxes.append(defect) 

693 

694 # Check that code doesn't mask more than it is supposed to. 

695 for boxMeas in boxesMeasured: 

696 self.assertIn(boxMeas, unionInputExpectedBoxes) 

697 

698 def test_dilateSaturatedColumns_saturated_column(self): 

699 exp = self.flatExp.clone() 

700 

701 mask = afwImage.Mask.getPlaneBitMask("SAT") 

702 

703 # We include saturated defects hitting the side to ensure we do not 

704 # have any overflow. 

705 # The dilation radius is set to 2 pixels. 

706 expectedDefects = [Box2I(corner=Point2I(20, 5), dimensions=Extent2I(5, 20)), 

707 Box2I(corner=Point2I(197, 5), dimensions=Extent2I(3, 10)), 

708 Box2I(corner=Point2I(0, 160), dimensions=Extent2I(4, 15))] 

709 defects = self.allDefectsList 

710 

711 satColumns = [Box2I(corner=Point2I(22, 5), dimensions=Extent2I(1, 20)), 

712 Box2I(corner=Point2I(199, 5), dimensions=Extent2I(1, 10)), 

713 Box2I(corner=Point2I(1, 160), dimensions=Extent2I(1, 15))] 

714 for satColumn in satColumns: 

715 exp.mask[satColumn] |= mask 

716 defects.append(satColumn) 

717 

718 self.check_dilateSaturatedColumns(exp, defects, expectedDefects) 

719 

720 def test_dilateSaturatedColumns_no_saturated_column(self): 

721 exp = self.flatExp.clone() 

722 

723 # These are marked BAD but not saturated. 

724 mask = afwImage.Mask.getPlaneBitMask("BAD") 

725 

726 expectedDefects = [] 

727 defects = self.allDefectsList 

728 

729 satColumns = [Box2I(corner=Point2I(22, 5), dimensions=Extent2I(1, 20)), 

730 Box2I(corner=Point2I(199, 5), dimensions=Extent2I(1, 10)), 

731 Box2I(corner=Point2I(1, 160), dimensions=Extent2I(1, 15))] 

732 for satColumn in satColumns: 

733 exp.mask[satColumn] |= mask 

734 defects.append(satColumn) 

735 

736 self.check_dilateSaturatedColumns(exp, defects, expectedDefects) 

737 

738 def test_defectVampirePixels(self): 

739 config = copy.copy(self.defaultConfig) 

740 # We set configs to mask vampire pixels 

741 config.doVampirePixels = True 

742 # We choose small radius to get a only 3 bbox to check 

743 config.radiusVampirePixels = 1 

744 # This is the threshold type that is actually used 

745 # for defects search in flats 

746 config.thresholdType = 'VALUE' 

747 # Flat mock has higher pixel values than real flatBootstrap 

748 config.thresholdVampirePixels = 2550. 

749 

750 task = self.defaultTask 

751 task.config = config 

752 

753 exp = self.flatExp.clone() 

754 

755 # We set one bright pixel to test the vampire pixel masking 

756 yVampirePixel = 130 

757 xVampirePixel = 50 

758 exp.image.array[yVampirePixel, xVampirePixel] = 2600. 

759 

760 # Find usual hot and pixels as well as the bright defect 

761 defects = task._findHotAndColdPixels(exp) 

762 

763 # Make bright defect BBox 

764 vampireBBox = [Box2I(corner=Point2I(50, 129), dimensions=Extent2I(1, 3)), 

765 Box2I(corner=Point2I(49, 130), dimensions=Extent2I(1, 1)), 

766 Box2I(corner=Point2I(51, 130), dimensions=Extent2I(1, 1))] 

767 # Test that the BBox is within the defects measured 

768 boxesMeasured = [] 

769 for defect in defects: 

770 boxesMeasured.append(defect.getBBox()) 

771 for expectedBBox in vampireBBox: 

772 self.assertIn(expectedBBox, boxesMeasured) 

773 

774 def test_defectFindingAllSensor(self): 

775 config = copy.copy(self.defaultConfig) 

776 config.nPixBorderLeftRight = 0 

777 config.nPixBorderUpDown = 0 

778 

779 task = self.defaultTask 

780 task.config = config 

781 

782 defects = task._findHotAndColdPixels(self.flatExp) 

783 

784 allBBoxes = self.darkBBoxes + self.brightBBoxes 

785 

786 boxesMeasured = [] 

787 for defect in defects: 

788 boxesMeasured.append(defect.getBBox()) 

789 

790 for expectedBBox in allBBoxes: 

791 self.assertIn(expectedBBox, boxesMeasured) 

792 

793 def test_defectFindingEdgeIgnore(self): 

794 config = copy.copy(self.defaultConfig) 

795 config.nPixBorderUpDown = 0 

796 config.nPixBorderLeftRight = 7 

797 task = self.defaultTask 

798 task.config = config 

799 defects = task._findHotAndColdPixels(self.flatExp) 

800 

801 shouldBeFound = self.darkBBoxes[self.noEdges] + self.brightBBoxes[self.noEdges] 

802 

803 boxesMeasured = [] 

804 for defect in defects: 

805 boxesMeasured.append(defect.getBBox()) 

806 

807 for expectedBBox in shouldBeFound: 

808 self.assertIn(expectedBBox, boxesMeasured) 

809 

810 shouldBeMissed = self.darkBBoxes[self.onlyEdges] + self.brightBBoxes[self.onlyEdges] 

811 for boxMissed in shouldBeMissed: 

812 self.assertNotIn(boxMissed, boxesMeasured) 

813 

814 def valueThreshold(self, fileType, saturateAmpInFlat=False): 

815 """Helper function to loop over flats and darks 

816 to test thresholdType = 'VALUE'.""" 

817 config = copy.copy(self.defaultConfig) 

818 config.thresholdType = 'VALUE' 

819 task = self.defaultTask 

820 task.config = config 

821 

822 for amp in self.flatExp.getDetector(): 

823 if amp.getName() == 'C:0,0': 

824 regionC00 = amp.getBBox() 

825 

826 if fileType == 'dark': 

827 exp = self.darkExp 

828 shouldBeFound = self.brightBBoxes[self.noEdges] 

829 else: 

830 exp = self.flatExp 

831 if saturateAmpInFlat: 

832 exp.maskedImage[regionC00].image.array[:] = 0.0 

833 # Amp C:0,0: minimum=(0, 0), maximum=(99, 50) 

834 x = self.defaultConfig.nPixBorderUpDown 

835 y = self.defaultConfig.nPixBorderLeftRight 

836 width, height = regionC00.getEndX() - x, regionC00.getEndY() - y 

837 # Defects code will mark whole saturated amp as defect box. 

838 shouldBeFound = [Box2I(corner=Point2I(x, y), dimensions=Extent2I(width, height))] 

839 else: 

840 shouldBeFound = self.darkBBoxes[self.noEdges] 

841 # Change the default a bit so it works for the 

842 # existing simulated defects. 

843 task.config.fracThresholdFlat = 0.9 

844 

845 defects = task._findHotAndColdPixels(exp) 

846 

847 boxesMeasured = [] 

848 for defect in defects: 

849 boxesMeasured.append(defect.getBBox()) 

850 

851 for expectedBBox in shouldBeFound: 

852 self.assertIn(expectedBBox, boxesMeasured) 

853 

854 def test_valueThreshold(self): 

855 for fileType in ['dark', 'flat']: 

856 self.valueThreshold(fileType) 

857 # stdDev = 0.0 

858 self.valueThreshold('flat', saturateAmpInFlat=True) 

859 

860 def test_pixelCounting(self): 

861 """Test that the number of defective pixels identified is as expected. 

862 """ 

863 config = copy.copy(self.defaultConfig) 

864 config.nPixBorderUpDown = 0 

865 config.nPixBorderLeftRight = 0 

866 task = self.defaultTask 

867 task.config = config 

868 defects = task._findHotAndColdPixels(self.flatExp) 

869 

870 defectArea = 0 

871 for defect in defects: 

872 defectArea += defect.getBBox().getArea() 

873 

874 # The columnar code will cover blocks of a column with 

875 # on-and-off pixels, thus creating more bad pixels that what 

876 # initially placed in self.brightDefects and self.darkDefects. 

877 # Thus, defectArea should be >= crossCheck. 

878 crossCheck = 0 

879 for x, y, sx, sy in self.brightDefects: 

880 crossCheck += sx*sy 

881 for x, y, sx, sy in self.darkDefects: 

882 crossCheck += sx*sy 

883 

884 # Test the result of _nPixFromDefects() 

885 # via two different ways of calculating area. 

886 self.assertEqual(defectArea, task._nPixFromDefects(defects)) 

887 # defectArea should be >= crossCheck 

888 self.assertGreaterEqual(defectArea, crossCheck) 

889 

890 def test_getNumGoodPixels(self): 

891 """Test the the number of pixels in the image not masked is as 

892 expected. 

893 """ 

894 testImage = self.flatExp.clone() 

895 mi = testImage.maskedImage 

896 

897 imageSize = testImage.getBBox().getArea() 

898 nGood = self.defaultTask._getNumGoodPixels(mi) 

899 

900 self.assertEqual(imageSize, nGood) 

901 

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

903 

904 noDataBox = Box2I(Point2I(31, 49), Extent2I(3, 6)) 

905 testImage.mask[noDataBox] |= NODATABIT 

906 

907 self.assertEqual(imageSize - noDataBox.getArea(), self.defaultTask._getNumGoodPixels(mi)) 

908 # check for misfire; we're setting NO_DATA here, not BAD 

909 self.assertEqual(imageSize, self.defaultTask._getNumGoodPixels(mi, 'BAD')) 

910 

911 testImage.mask[noDataBox] ^= NODATABIT # XOR to reset what we did 

912 self.assertEqual(imageSize, nGood) 

913 

914 BADBIT = mi.mask.getPlaneBitMask("BAD") 

915 badBox = Box2I(Point2I(85, 98), Extent2I(4, 7)) 

916 testImage.mask[badBox] |= BADBIT 

917 

918 self.assertEqual(imageSize - badBox.getArea(), self.defaultTask._getNumGoodPixels(mi, 'BAD')) 

919 

920 def test_edgeMasking(self): 

921 """Check that the right number of edge pixels are masked by 

922 _setEdgeBits(). 

923 """ 

924 testImage = self.flatExp.clone() 

925 mi = testImage.maskedImage 

926 

927 self.assertEqual(countMaskedPixels(mi, 'EDGE'), 0) 

928 self.defaultTask._setEdgeBits(mi) 

929 

930 hEdge = self.defaultConfig.nPixBorderLeftRight 

931 vEdge = self.defaultConfig.nPixBorderUpDown 

932 xSize, ySize = mi.getDimensions() 

933 

934 nEdge = xSize*vEdge*2 + ySize*hEdge*2 - hEdge*vEdge*4 

935 

936 self.assertEqual(countMaskedPixels(mi, 'EDGE'), nEdge) 

937 

938 def test_badImage(self): 

939 """Check that fully-bad images do not fail. 

940 """ 

941 testImage = self.flatExp.clone() 

942 testImage.image.array[:, :] = 125000 

943 

944 config = copy.copy(self.defaultConfig) 

945 # Do not exclude any pixels, so the areas match. 

946 config.nPixBorderUpDown = 0 

947 config.nPixBorderLeftRight = 0 

948 

949 task = self.defaultTask 

950 task.config = config 

951 defects = task._findHotAndColdPixels(testImage) 

952 

953 defectArea = 0 

954 for defect in defects: 

955 defectArea += defect.getBBox().getArea() 

956 self.assertEqual(defectArea, testImage.getBBox().getArea()) 

957 

958 def test_e2vMidline(self): 

959 """Test handling of E2V midline.""" 

960 

961 class MockE2VAmp: 

962 def __init__(self, name, bbox): 

963 self._name = name 

964 self._bbox = bbox 

965 

966 def getName(self): 

967 return self._name 

968 

969 def getBBox(self): 

970 return self._bbox 

971 

972 def __repr__(self): 

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

974 

975 class MockE2VDetector(list): 

976 def __init__(self): 

977 amps = [] 

978 for i in range(8): 

979 name = f"C1{i}" 

980 bbox = Box2I(corner=Point2I(i*512, 2002), dimensions=Extent2I(512, 2002)) 

981 amps.append(MockE2VAmp(name, bbox)) 

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

983 name = f"C0{i}" 

984 bbox = Box2I(corner=Point2I(i*512, 0), dimensions=Extent2I(512, 2002)) 

985 amps.append(MockE2VAmp(name, bbox)) 

986 

987 super().__init__(amps) 

988 

989 def getBBox(self): 

990 return Box2I(corner=Point2I(0, 0), dimensions=Extent2I(4096, 4004)) 

991 

992 def getPhysicalType(self): 

993 return "E2V" 

994 

995 class MockE2VExposure(afwImage.ExposureF): 

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

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

998 

999 def setDetector(self, detector): 

1000 self._detector = detector 

1001 

1002 def getDetector(self): 

1003 return self._detector 

1004 

1005 detector = MockE2VDetector() 

1006 flat = MockE2VExposure(detector.getBBox()) 

1007 flat.setDetector(detector) 

1008 flat.metadata["IMGTYPE"] = "FLAT" 

1009 visitInfo = afwImage.VisitInfo(exposureTime=10.0, darkTime=10.0) 

1010 flat.info.setVisitInfo(visitInfo) 

1011 

1012 flat.mask.array[:, :] = 0 

1013 

1014 rng = np.random.RandomState(0) 

1015 

1016 # Make a flat, and make a very dark midline. 

1017 flat.image.array[:, :] = rng.normal(1.0, 0.01, flat.image.array.shape) 

1018 flat.image.array[2001, :] = 0.0 

1019 flat.image.array[2002, :] = 0.0 

1020 

1021 # First test that with midline checking off it is masked. 

1022 config = cpPipe.MeasureDefectsTask.ConfigClass() 

1023 config.thresholdType = "VALUE" 

1024 config.fracThresholdFlat = 0.9 

1025 config.e2vMidlineBreakNRow = 0 

1026 task = cpPipe.MeasureDefectsTask(config=config) 

1027 

1028 defects = task._findHotAndColdPixels(flat) 

1029 

1030 # There should be 16 defects (one for each amp). 

1031 self.assertEqual(len(defects), len(detector)) 

1032 

1033 flat.mask.array[:, :] = 0 

1034 for defect in defects: 

1035 flat.mask[defect.getBBox()].array |= 1 

1036 

1037 np.testing.assert_array_equal(flat.mask.array[2001, :], 1) 

1038 np.testing.assert_array_equal(flat.mask.array[2002, :], 1) 

1039 

1040 # Test again *ignoring* the midline. 

1041 config = cpPipe.MeasureDefectsTask.ConfigClass() 

1042 config.thresholdType = "VALUE" 

1043 config.fracThresholdFlat = 0.9 

1044 config.e2vMidlineBreakNRow = 1 

1045 task = cpPipe.MeasureDefectsTask(config=config) 

1046 

1047 flat.mask.array[:, :] = 0 

1048 defects = task._findHotAndColdPixels(flat) 

1049 

1050 self.assertEqual(len(defects), 0) 

1051 

1052 # Test again *requiring* the midline mask. 

1053 config = cpPipe.MeasureDefectsTask.ConfigClass() 

1054 config.thresholdType = "VALUE" 

1055 config.fracThresholdFlat = 0.9 

1056 config.e2vMidlineBreakNRow = 1 

1057 config.e2vMidlineBreakOption = "MASK" 

1058 task = cpPipe.MeasureDefectsTask(config=config) 

1059 

1060 flat.mask.array[:, :] = 0 

1061 flat.image.array[:, :] = rng.normal(1.0, 0.01, flat.image.array.shape) 

1062 

1063 defects = task._findHotAndColdPixels(flat) 

1064 

1065 # There should be 16 defects (one for each amp). 

1066 self.assertEqual(len(defects), len(detector)) 

1067 

1068 flat.mask.array[:, :] = 0 

1069 for defect in defects: 

1070 flat.mask[defect.getBBox()].array |= 1 

1071 

1072 np.testing.assert_array_equal(flat.mask.array[2001, :], 1) 

1073 np.testing.assert_array_equal(flat.mask.array[2002, :], 1) 

1074 

1075 def test_flatGradient(self): 

1076 """Test handling of a strong gradient.""" 

1077 mock = ipIsr.IsrMockLSST() 

1078 camera = mock.getCamera() 

1079 detector = camera[10] 

1080 

1081 # Create a flat with a strong gradient. 

1082 flat = afwImage.ExposureF(detector.getBBox()) 

1083 flat.setDetector(detector) 

1084 

1085 xx = np.arange(flat.image.array.shape[1], dtype=np.float64) 

1086 yy = np.arange(flat.image.array.shape[0], dtype=np.float64) 

1087 x, y = np.meshgrid(xx, yy) 

1088 x = x.ravel() 

1089 y = y.ravel() 

1090 

1091 transform = detector.getTransform(lsst.afw.cameraGeom.PIXELS, lsst.afw.cameraGeom.FOCAL_PLANE) 

1092 xy = np.vstack((x, y)) 

1093 xf, yf = np.vsplit(transform.getMapping().applyForward(xy), 2) 

1094 xf = xf.ravel() 

1095 yf = yf.ravel() 

1096 

1097 radius = np.sqrt(xf**2. + yf**2.) 

1098 radialNodes = np.linspace(radius.min(), radius.max(), 4) 

1099 radialValues = [1.0, 0.8, 0.5, 0.3] 

1100 spl = Akima1DInterpolator(radialNodes, radialValues, method="akima") 

1101 

1102 value = spl(np.clip(radius, radialNodes[0], radialNodes[-1])) 

1103 flat.image.array[:, :] = value.reshape(flat.image.array.shape) 

1104 

1105 visitInfo = afwImage.VisitInfo(exposureTime=10.0, darkTime=10.0) 

1106 flat.info.setVisitInfo(visitInfo) 

1107 

1108 # Change all the gains. 

1109 rng = np.random.RandomState(seed=12345) 

1110 gains = rng.uniform(low=1.5, high=2.5, size=len(detector)) 

1111 

1112 for i, amp in enumerate(detector): 

1113 flat[amp.getBBox()].image.array /= gains[i] 

1114 

1115 # Look for defects with default settings; there should be some 

1116 # (which we don't want). 

1117 

1118 config = cpPipe.MeasureDefectsTask.ConfigClass() 

1119 config.thresholdType = "VALUE" 

1120 config.fracThresholdFlat = 0.9 

1121 

1122 task1 = cpPipe.MeasureDefectsTask(config=config) 

1123 defects1 = task1.run(flat, camera).outputDefects 

1124 

1125 # There are almost 200 defects here, but we're just checking that 

1126 # there are some false defects. 

1127 self.assertGreater(len(defects1), 10) 

1128 

1129 # Turn on amp gradient fitting with special configs for small 

1130 # test amplifiers. 

1131 config.fitAmpGradient = True 

1132 config.ampGradientBinFactor = 2 

1133 config.ampGradientBoundarySize = 0 

1134 

1135 task2 = cpPipe.MeasureDefectsTask(config=config) 

1136 defects2 = task2.run(flat, camera).outputDefects 

1137 

1138 # Check that there are no false defects. 

1139 self.assertEqual(len(defects2), 0) 

1140 

1141 

1142class TestMemory(lsst.utils.tests.MemoryTestCase): 

1143 pass 

1144 

1145 

1146def setup_module(module): 

1147 lsst.utils.tests.init() 

1148 

1149 

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

1151 lsst.utils.tests.init() 

1152 unittest.main()