Coverage for tests/test_defects.py: 99%
612 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-19 03:27 -0700
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-19 03:27 -0700
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."""
25import unittest
26import numpy as np
27import copy
28from scipy.interpolate import Akima1DInterpolator
30import lsst.utils
31import lsst.utils.tests
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
41class MeasureDefectsTaskTestCase(lsst.utils.tests.TestCase):
42 """A test case for the defect finding task."""
44 def setUp(self):
45 self.defaultConfig = cpPipe.MeasureDefectsTask.ConfigClass()
47 self.flatMean = 2000
48 self.darkMean = 1
49 self.readNoiseAdu = 10
50 self.nSigmaBright = 8
51 self.nSigmaDark = 8
53 mockImageConfig = isrMock.IsrMock.ConfigClass()
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
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
65 self.brightDefects = [(0, 15, 3, 3), (100, 123, 1, 1)]
67 self.darkDefects = [(5, 0, 1, 1), (7, 62, 2, 2)]
69 nEdge = 1 # NOTE: update if more edge defects are included
70 self.noEdges = slice(nEdge, None)
71 self.onlyEdges = slice(0, nEdge)
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]
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))
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
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
95 self.darkExp = self.flatExp.clone()
96 self.spareImage = self.flatExp.clone() # for testing edge bits and misc
98 self.flatExp.image.array[:] = flatData
99 self.darkExp.image.array[:] = darkData
101 self.defaultTask = cpPipe.MeasureDefectsTask()
103 self.allDefectsList = ipIsr.Defects()
104 self.brightDefectsList = ipIsr.Defects()
105 self.darkDefectsList = ipIsr.Defects()
107 # Set image types, the defects code will use them.
108 metaDataFlat = PropertyList()
109 metaDataFlat["IMGTYPE"] = "FLAT"
110 self.flatExp.setMetadata(metaDataFlat)
112 metaDataDark = PropertyList()
113 metaDataDark["IMGTYPE"] = "DARK"
114 self.darkExp.setMetadata(metaDataDark)
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)
122 with self.darkDefectsList.bulk_update():
123 for d in self.darkBBoxes:
124 self.darkDefectsList.append(d)
125 self.allDefectsList.append(d)
127 def check_maskBlocks(self, inputDefects, expectedDefects):
128 """A helper function for the tests of
129 maskBlocksIfIntermitentBadPixelsInColumn.
131 """
132 config = copy.copy(self.defaultConfig)
133 config.badOnAndOffPixelColumnThreshold = 10
134 config.goodPixelColumnGapThreshold = 5
135 config.nPixBorderUpDown = 0
136 config.nPixBorderLeftRight = 0
138 task = self.defaultTask
139 task.config = config
141 defectsWithColumns, count = task.maskBlocksIfIntermitentBadPixelsInColumn(inputDefects)
142 boxesMeasured = []
143 for defect in defectsWithColumns:
144 boxesMeasured.append(defect.getBBox())
146 for boxInput in expectedDefects:
147 self.assertIn(boxInput, boxesMeasured)
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)
157 # Check that code doesn't mask more than it is supposed to.
158 for boxMeas in boxesMeasured:
159 self.assertIn(boxMeas, unionInputExpectedBoxes)
161 def test_maskBlocks_full_column(self):
162 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
164 Tests that a contigous bad column does not get split by the
165 code.
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.
171 Plots can be found in DM-19903 on Jira.
173 """
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))]
179 self.check_maskBlocks(defects, expectedDefects)
181 def test_maskBlocks_long_column(self):
182 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
184 Tests that a contigous bad column with Npix >=
185 badOnAndOffPixelColumnThreshold (10) does not get split by the
186 code.
188 Plots can be found in DM-19903 on Jira.
190 """
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)))
196 self.check_maskBlocks(defects, expectedDefects)
198 def test_maskBlocks_short_column(self):
199 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
201 Tests that a contigous bad column Npix <
202 badOnAndOffPixelColumnThreshold (10) does not get split by the
203 code.
205 Plots can be found in DM-19903 on Jira.
207 """
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)))
213 self.check_maskBlocks(defects, expectedDefects)
215 def test_maskBlocks_discontigous_to_single_block(self):
216 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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.
224 Plots can be found in DM-19903 on Jira.
226 """
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)
240 self.check_maskBlocks(defects, expectedDefects)
242 def test_maskBlocks_discontigous_less_than_thresholds(self):
243 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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.
250 Plots can be found in DM-19903 on Jira.
252 """
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)
265 self.check_maskBlocks(defects, expectedDefects)
267 def test_maskBlocks_more_than_thresholds(self):
268 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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).
276 Plots can be found in DM-19903 on Jira.
278 """
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)
292 self.check_maskBlocks(defects, expectedDefects)
294 def test_maskBlocks_not_enough_bad_pixels_in_column(self):
295 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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)
305 Plots can be found in DM-19903 on Jira.
307 """
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)
318 self.check_maskBlocks(defects, expectedDefects)
320 def test_maskBlocks_every_other_pixel_bad_greater_than_threshold(self):
321 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
323 Npix discontiguous bad pixels in a column where Npix >
324 badOnAndOffPixelColumnThreshold (10) and every other pixel is
325 bad.
327 Plots can be found in DM-19903 on Jira.
329 """
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)
353 self.check_maskBlocks(defects, expectedDefects)
355 def test_maskBlocks_every_other_pixel_bad_less_than_threshold(self):
356 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
358 Npix discontiguous bad pixels in a column where Npix >
359 badOnAndOffPixelColumnThreshold (10) and every other pixel is
360 bad.
362 Plots can be found in DM-19903 on Jira.
364 """
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)
383 self.check_maskBlocks(defects, expectedDefects)
385 def test_maskBlocks_blobs_one_side_good_less_than_threshold(self):
386 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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).
393 Plots can be found in DM-19903 on Jira.
395 """
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)
408 self.check_maskBlocks(defects, expectedDefects)
410 def test_maskBlocks_blobs_other_side_good_less_than_threshold(self):
411 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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).
418 Plots can be found in DM-19903 on Jira.
420 """
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)
433 self.check_maskBlocks(defects, expectedDefects)
435 def test_maskBlocks_blob_both_sides_good_less_than_threshold(self):
436 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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).
443 Plots can be found in DM-19903 on Jira.
445 """
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)
461 self.check_maskBlocks(defects, expectedDefects)
463 def test_maskBlocks_blob_one_side_good_greater_than_threshold(self):
464 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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).
471 Plots can be found in DM-19903 on Jira.
473 """
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)
487 self.check_maskBlocks(defects, expectedDefects)
489 def test_maskBlocks_other_side_good_greater_than_threshold(self):
490 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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).
497 Plots can be found in DM-19903 on Jira.
499 """
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)
513 self.check_maskBlocks(defects, expectedDefects)
515 def test_maskBlocks_both_sides_good_greater_than_threshold(self):
516 """A test for maskBlocksIfIntermitentBadPixelsInColumn.
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).
523 Plots can be found in DM-19903 on Jira.
525 """
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)
547 self.check_maskBlocks(defects, expectedDefects)
549 def test_maskBlocks_y_out_of_order_dm38103(self):
550 """A test for maskBlocksIfIntermitentBadPixelsInColumn, y out of order.
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))]
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
585 self.check_maskBlocks(defects, expectedDefects)
587 def check_maskBadColumns(self, exp, inputDefects, expectedDefects):
588 """A helper function for the tests of
589 maskBadColumns.
591 """
592 config = copy.copy(self.defaultConfig)
593 config.badPixelsToFillColumnThreshold = 25
594 config.saturatedPixelsToFillColumnThreshold = 5
596 task = self.defaultTask
597 task.config = config
599 defectsWithColumns, count = task.maskBadColumns(exp, inputDefects)
601 self.assertEqual(count, len(expectedDefects))
603 boxesMeasured = []
604 for defect in defectsWithColumns:
605 boxesMeasured.append(defect.getBBox())
607 for boxInput in expectedDefects:
608 self.assertIn(boxInput, boxesMeasured)
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)
618 # Check that code doesn't mask more than it is supposed to.
619 for boxMeas in boxesMeasured:
620 self.assertIn(boxMeas, unionInputExpectedBoxes)
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)))
633 self.check_maskBadColumns(self.flatExp, defects, expectedDefects)
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)))
644 self.check_maskBadColumns(self.flatExp, defects, expectedDefects)
646 def test_maskBadColumns_extend_saturated_columns(self):
647 """Test maskBadColumns, extend saturation to full column.
648 """
649 exp = self.flatExp.clone()
651 mask = afwImage.Mask.getPlaneBitMask("SAT")
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
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)
668 self.check_maskBadColumns(exp, defects, expectedDefects)
670 def check_dilateSaturatedColumns(self, exp, inputDefects, expectedDefects):
671 config = copy.copy(self.defaultConfig)
672 config.saturatedColumnDilationRadius = 2
674 task = self.defaultTask
675 task.config = config
677 defectsDilated = task.dilateSaturatedColumns(exp, inputDefects)
679 boxesMeasured = []
680 for defect in defectsDilated:
681 boxesMeasured.append(defect.getBBox())
683 for boxInput in expectedDefects:
684 self.assertIn(boxInput, boxesMeasured)
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)
694 # Check that code doesn't mask more than it is supposed to.
695 for boxMeas in boxesMeasured:
696 self.assertIn(boxMeas, unionInputExpectedBoxes)
698 def test_dilateSaturatedColumns_saturated_column(self):
699 exp = self.flatExp.clone()
701 mask = afwImage.Mask.getPlaneBitMask("SAT")
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
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)
718 self.check_dilateSaturatedColumns(exp, defects, expectedDefects)
720 def test_dilateSaturatedColumns_no_saturated_column(self):
721 exp = self.flatExp.clone()
723 # These are marked BAD but not saturated.
724 mask = afwImage.Mask.getPlaneBitMask("BAD")
726 expectedDefects = []
727 defects = self.allDefectsList
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)
736 self.check_dilateSaturatedColumns(exp, defects, expectedDefects)
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.
750 task = self.defaultTask
751 task.config = config
753 exp = self.flatExp.clone()
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.
760 # Find usual hot and pixels as well as the bright defect
761 defects = task._findHotAndColdPixels(exp)
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)
774 def test_defectFindingAllSensor(self):
775 config = copy.copy(self.defaultConfig)
776 config.nPixBorderLeftRight = 0
777 config.nPixBorderUpDown = 0
779 task = self.defaultTask
780 task.config = config
782 defects = task._findHotAndColdPixels(self.flatExp)
784 allBBoxes = self.darkBBoxes + self.brightBBoxes
786 boxesMeasured = []
787 for defect in defects:
788 boxesMeasured.append(defect.getBBox())
790 for expectedBBox in allBBoxes:
791 self.assertIn(expectedBBox, boxesMeasured)
793 def test_defectFindingEdgeIgnore(self):
794 config = copy.copy(self.defaultConfig)
795 config.nPixBorderUpDownE2V = 1
796 config.nPixBorderUpDownITL = 1
797 config.nPixBorderLeftRightE2V = 7
798 config.nPixBorderLeftRightITL = 7
799 task = self.defaultTask
800 task.config = config
802 expE2V = self.flatExp.clone()
803 expITL = self.flatExp.clone()
805 testDetector = expE2V.getDetector()
806 e2vDetector = testDetector.rebuild()
807 e2vDetector.setPhysicalType("E2V")
808 expE2V.setDetector(e2vDetector.finish())
810 defects = task._findHotAndColdPixels(expE2V)
812 shouldBeFound = self.darkBBoxes[self.noEdges] + self.brightBBoxes[self.noEdges]
814 boxesMeasured = []
815 for defect in defects:
816 boxesMeasured.append(defect.getBBox())
818 for expectedBBox in shouldBeFound:
819 self.assertIn(expectedBBox, boxesMeasured)
821 shouldBeMissed = self.darkBBoxes[self.onlyEdges] + self.brightBBoxes[self.onlyEdges]
822 for boxMissed in shouldBeMissed:
823 self.assertNotIn(boxMissed, boxesMeasured)
825 testDetector = expITL.getDetector()
826 itlDetector = testDetector.rebuild()
827 itlDetector.setPhysicalType("ITL")
828 expITL.setDetector(itlDetector.finish())
830 defects = task._findHotAndColdPixels(expITL)
832 shouldBeFound = self.darkBBoxes[self.noEdges] + self.brightBBoxes[self.noEdges]
834 boxesMeasured = []
835 for defect in defects:
836 boxesMeasured.append(defect.getBBox())
838 for expectedBBox in shouldBeFound:
839 self.assertIn(expectedBBox, boxesMeasured)
841 shouldBeMissed = self.darkBBoxes[self.onlyEdges] + self.brightBBoxes[self.onlyEdges]
842 for boxMissed in shouldBeMissed:
843 self.assertNotIn(boxMissed, boxesMeasured)
845 def valueThreshold(self, fileType, saturateAmpInFlat=False):
846 """Helper function to loop over flats and darks
847 to test thresholdType = 'VALUE'."""
848 config = copy.copy(self.defaultConfig)
849 config.thresholdType = 'VALUE'
850 task = self.defaultTask
851 task.config = config
853 for amp in self.flatExp.getDetector():
854 if amp.getName() == 'C:0,0':
855 regionC00 = amp.getBBox()
857 if fileType == 'dark':
858 exp = self.darkExp
859 shouldBeFound = self.brightBBoxes[self.noEdges]
860 else:
861 exp = self.flatExp
862 if saturateAmpInFlat:
863 exp.maskedImage[regionC00].image.array[:] = 0.0
864 # Amp C:0,0: minimum=(0, 0), maximum=(99, 50)
865 x = self.defaultConfig.nPixBorderUpDown
866 y = self.defaultConfig.nPixBorderLeftRight
867 width, height = regionC00.getEndX() - x, regionC00.getEndY() - y
868 # Defects code will mark whole saturated amp as defect box.
869 shouldBeFound = [Box2I(corner=Point2I(x, y), dimensions=Extent2I(width, height))]
870 else:
871 shouldBeFound = self.darkBBoxes[self.noEdges]
872 # Change the default a bit so it works for the
873 # existing simulated defects.
874 task.config.fracThresholdFlat = 0.9
876 defects = task._findHotAndColdPixels(exp)
878 boxesMeasured = []
879 for defect in defects:
880 boxesMeasured.append(defect.getBBox())
882 for expectedBBox in shouldBeFound:
883 self.assertIn(expectedBBox, boxesMeasured)
885 def test_valueThreshold(self):
886 for fileType in ['dark', 'flat']:
887 self.valueThreshold(fileType)
888 # stdDev = 0.0
889 self.valueThreshold('flat', saturateAmpInFlat=True)
891 def test_pixelCounting(self):
892 """Test that the number of defective pixels identified is as expected.
893 """
894 itlDetector = self.flatExp.getDetector().rebuild()
895 itlDetector.setPhysicalType("ITL")
896 self.flatExp.setDetector(itlDetector.finish())
898 config = copy.copy(self.defaultConfig)
899 config.nPixBorderUpDownITL = 0
900 config.nPixBorderLeftRightITL = 0
902 task = self.defaultTask
903 task.config = config
904 defects = task._findHotAndColdPixels(self.flatExp)
906 defectArea = 0
907 for defect in defects:
908 defectArea += defect.getBBox().getArea()
910 # The columnar code will cover blocks of a column with
911 # on-and-off pixels, thus creating more bad pixels that what
912 # initially placed in self.brightDefects and self.darkDefects.
913 # Thus, defectArea should be >= crossCheck.
914 crossCheck = 0
915 for x, y, sx, sy in self.brightDefects:
916 crossCheck += sx*sy
917 for x, y, sx, sy in self.darkDefects:
918 crossCheck += sx*sy
920 # Test the result of _nPixFromDefects()
921 # via two different ways of calculating area.
922 self.assertEqual(defectArea, task._nPixFromDefects(defects))
923 # defectArea should be >= crossCheck
924 self.assertGreaterEqual(defectArea, crossCheck)
926 def test_getNumGoodPixels(self):
927 """Test the the number of pixels in the image not masked is as
928 expected.
929 """
930 testImage = self.flatExp.clone()
931 mi = testImage.maskedImage
933 imageSize = testImage.getBBox().getArea()
934 nGood = self.defaultTask._getNumGoodPixels(mi)
936 self.assertEqual(imageSize, nGood)
938 NODATABIT = mi.mask.getPlaneBitMask("NO_DATA")
940 noDataBox = Box2I(Point2I(31, 49), Extent2I(3, 6))
941 testImage.mask[noDataBox] |= NODATABIT
943 self.assertEqual(imageSize - noDataBox.getArea(), self.defaultTask._getNumGoodPixels(mi))
944 # check for misfire; we're setting NO_DATA here, not BAD
945 self.assertEqual(imageSize, self.defaultTask._getNumGoodPixels(mi, 'BAD'))
947 testImage.mask[noDataBox] ^= NODATABIT # XOR to reset what we did
948 self.assertEqual(imageSize, nGood)
950 BADBIT = mi.mask.getPlaneBitMask("BAD")
951 badBox = Box2I(Point2I(85, 98), Extent2I(4, 7))
952 testImage.mask[badBox] |= BADBIT
954 self.assertEqual(imageSize - badBox.getArea(), self.defaultTask._getNumGoodPixels(mi, 'BAD'))
956 def test_edgeMasking(self):
957 """Check that the right number of edge pixels are masked by
958 _setEdgeBits().
959 """
960 config = cpPipe.MeasureDefectsTask.ConfigClass()
962 testImage = self.flatExp.clone()
963 testDetector = testImage.getDetector()
964 mi = testImage.maskedImage
966 self.assertEqual(countMaskedPixels(mi, 'EDGE'), 0)
968 # Test for default CCD mock
969 config.nPixBorderLeftRight = 3
970 config.nPixBorderUpDown = 4
971 task = self.defaultTask
972 task.config = config
973 task._setEdgeBits(mi)
975 hEdge = config.nPixBorderLeftRight
976 vEdge = config.nPixBorderUpDown
977 xSize, ySize = mi.getDimensions()
979 nEdge = xSize*vEdge*2 + ySize*hEdge*2 - hEdge*vEdge*4
980 self.assertEqual(countMaskedPixels(mi, 'EDGE'), nEdge)
982 # Test for E2V CCD (need to reset the test image)
983 testImage = self.flatExp.clone()
984 testDetector = testImage.getDetector()
986 e2vDetector = testDetector.rebuild()
987 e2vDetector.setPhysicalType("E2V")
988 testImage.setDetector(e2vDetector.finish())
989 config.nPixBorderLeftRightE2V = 2
990 config.nPixBorderUpDownE2V = 1
991 mi = testImage.maskedImage
992 task._setEdgeBits(mi, detectorType=testImage.getDetector().getPhysicalType())
994 hEdge = config.nPixBorderLeftRightE2V
995 vEdge = config.nPixBorderUpDownE2V
996 xSize, ySize = mi.getDimensions()
998 nEdge = xSize*vEdge*2 + ySize*hEdge*2 - hEdge*vEdge*4
999 self.assertEqual(countMaskedPixels(mi, 'EDGE'), nEdge)
1001 # Test for ITL CCD
1002 testImage = self.flatExp.clone()
1003 testDetector = testImage.getDetector()
1005 itlDetector = testDetector.rebuild()
1006 itlDetector.setPhysicalType("ITL")
1007 testImage.setDetector(itlDetector.finish())
1008 config.nPixBorderLeftRightITL = 5
1009 config.nPixBorderUpDownITL = 6
1010 mi = testImage.maskedImage
1011 task._setEdgeBits(mi, detectorType=testImage.getDetector().getPhysicalType())
1013 hEdge = config.nPixBorderLeftRightITL
1014 vEdge = config.nPixBorderUpDownITL
1015 xSize, ySize = mi.getDimensions()
1017 nEdge = xSize*vEdge*2 + ySize*hEdge*2 - hEdge*vEdge*4
1018 self.assertEqual(countMaskedPixels(mi, 'EDGE'), nEdge)
1020 def test_badImage(self):
1021 """Check that fully-bad images do not fail.
1022 """
1023 testImage = self.flatExp.clone()
1024 testImage.image.array[:, :] = 125000
1026 config = copy.copy(self.defaultConfig)
1027 # Do not exclude any pixels, so the areas match.
1028 config.nPixBorderUpDown = 0
1029 config.nPixBorderLeftRight = 0
1031 task = self.defaultTask
1032 task.config = config
1033 defects = task._findHotAndColdPixels(testImage)
1035 defectArea = 0
1036 for defect in defects:
1037 defectArea += defect.getBBox().getArea()
1038 self.assertEqual(defectArea, testImage.getBBox().getArea())
1040 def test_e2vMidline(self):
1041 """Test handling of E2V midline."""
1043 class MockE2VAmp:
1044 def __init__(self, name, bbox):
1045 self._name = name
1046 self._bbox = bbox
1048 def getName(self):
1049 return self._name
1051 def getBBox(self):
1052 return self._bbox
1054 def __repr__(self):
1055 return f"MockE2VAmp({self._name})"
1057 class MockE2VDetector(list):
1058 def __init__(self):
1059 amps = []
1060 for i in range(8):
1061 name = f"C1{i}"
1062 bbox = Box2I(corner=Point2I(i*512, 2002), dimensions=Extent2I(512, 2002))
1063 amps.append(MockE2VAmp(name, bbox))
1064 for i in reversed(range(8)):
1065 name = f"C0{i}"
1066 bbox = Box2I(corner=Point2I(i*512, 0), dimensions=Extent2I(512, 2002))
1067 amps.append(MockE2VAmp(name, bbox))
1069 super().__init__(amps)
1071 def getBBox(self):
1072 return Box2I(corner=Point2I(0, 0), dimensions=Extent2I(4096, 4004))
1074 def getPhysicalType(self):
1075 return "E2V"
1077 class MockE2VExposure(afwImage.ExposureF):
1078 def __init__(self, *args, **kwargs):
1079 super().__init__(*args, **kwargs)
1081 def setDetector(self, detector):
1082 self._detector = detector
1084 def getDetector(self):
1085 return self._detector
1087 detector = MockE2VDetector()
1088 flat = MockE2VExposure(detector.getBBox())
1089 flat.setDetector(detector)
1090 flat.metadata["IMGTYPE"] = "FLAT"
1091 visitInfo = afwImage.VisitInfo(exposureTime=10.0, darkTime=10.0)
1092 flat.info.setVisitInfo(visitInfo)
1094 flat.mask.array[:, :] = 0
1096 rng = np.random.RandomState(0)
1098 # Make a flat, and make a very dark midline.
1099 flat.image.array[:, :] = rng.normal(1.0, 0.01, flat.image.array.shape)
1100 flat.image.array[2001, :] = 0.0
1101 flat.image.array[2002, :] = 0.0
1103 # First test that with midline checking off it is masked.
1104 config = cpPipe.MeasureDefectsTask.ConfigClass()
1105 config.thresholdType = "VALUE"
1106 config.fracThresholdFlat = 0.9
1107 config.e2vMidlineBreakNRow = 0
1108 task = cpPipe.MeasureDefectsTask(config=config)
1110 defects = task._findHotAndColdPixels(flat)
1112 # There should be 16 defects (one for each amp).
1113 self.assertEqual(len(defects), len(detector))
1115 flat.mask.array[:, :] = 0
1116 for defect in defects:
1117 flat.mask[defect.getBBox()].array |= 1
1119 np.testing.assert_array_equal(flat.mask.array[2001, :], 1)
1120 np.testing.assert_array_equal(flat.mask.array[2002, :], 1)
1122 # Test again *ignoring* the midline.
1123 config = cpPipe.MeasureDefectsTask.ConfigClass()
1124 config.thresholdType = "VALUE"
1125 config.fracThresholdFlat = 0.9
1126 config.e2vMidlineBreakNRow = 1
1127 task = cpPipe.MeasureDefectsTask(config=config)
1129 flat.mask.array[:, :] = 0
1130 defects = task._findHotAndColdPixels(flat)
1132 self.assertEqual(len(defects), 0)
1134 # Test again *requiring* the midline mask.
1135 config = cpPipe.MeasureDefectsTask.ConfigClass()
1136 config.thresholdType = "VALUE"
1137 config.fracThresholdFlat = 0.9
1138 config.e2vMidlineBreakNRow = 1
1139 config.e2vMidlineBreakOption = "MASK"
1140 task = cpPipe.MeasureDefectsTask(config=config)
1142 flat.mask.array[:, :] = 0
1143 flat.image.array[:, :] = rng.normal(1.0, 0.01, flat.image.array.shape)
1145 defects = task._findHotAndColdPixels(flat)
1147 # There should be 16 defects (one for each amp).
1148 self.assertEqual(len(defects), len(detector))
1150 flat.mask.array[:, :] = 0
1151 for defect in defects:
1152 flat.mask[defect.getBBox()].array |= 1
1154 np.testing.assert_array_equal(flat.mask.array[2001, :], 1)
1155 np.testing.assert_array_equal(flat.mask.array[2002, :], 1)
1157 def test_flatGradient(self):
1158 """Test handling of a strong gradient."""
1159 mock = ipIsr.IsrMockLSST()
1160 camera = mock.getCamera()
1161 detector = camera[10]
1163 # Create a flat with a strong gradient.
1164 flat = afwImage.ExposureF(detector.getBBox())
1165 flat.setDetector(detector)
1167 xx = np.arange(flat.image.array.shape[1], dtype=np.float64)
1168 yy = np.arange(flat.image.array.shape[0], dtype=np.float64)
1169 x, y = np.meshgrid(xx, yy)
1170 x = x.ravel()
1171 y = y.ravel()
1173 transform = detector.getTransform(lsst.afw.cameraGeom.PIXELS, lsst.afw.cameraGeom.FOCAL_PLANE)
1174 xy = np.vstack((x, y))
1175 xf, yf = np.vsplit(transform.getMapping().applyForward(xy), 2)
1176 xf = xf.ravel()
1177 yf = yf.ravel()
1179 radius = np.sqrt(xf**2. + yf**2.)
1180 radialNodes = np.linspace(radius.min(), radius.max(), 4)
1181 radialValues = [1.0, 0.8, 0.5, 0.3]
1182 spl = Akima1DInterpolator(radialNodes, radialValues, method="akima")
1184 value = spl(np.clip(radius, radialNodes[0], radialNodes[-1]))
1185 flat.image.array[:, :] = value.reshape(flat.image.array.shape)
1187 visitInfo = afwImage.VisitInfo(exposureTime=10.0, darkTime=10.0)
1188 flat.info.setVisitInfo(visitInfo)
1190 # Change all the gains.
1191 rng = np.random.RandomState(seed=12345)
1192 gains = rng.uniform(low=1.5, high=2.5, size=len(detector))
1194 for i, amp in enumerate(detector):
1195 flat[amp.getBBox()].image.array /= gains[i]
1197 # Look for defects with default settings; there should be some
1198 # (which we don't want).
1200 config = cpPipe.MeasureDefectsTask.ConfigClass()
1201 config.thresholdType = "VALUE"
1202 config.fracThresholdFlat = 0.9
1204 task1 = cpPipe.MeasureDefectsTask(config=config)
1205 defects1 = task1.run(flat, camera).outputDefects
1207 # There are almost 200 defects here, but we're just checking that
1208 # there are some false defects.
1209 self.assertGreater(len(defects1), 10)
1211 # Turn on amp gradient fitting with special configs for small
1212 # test amplifiers.
1213 config.fitAmpGradient = True
1214 config.ampGradientBinFactor = 2
1215 config.ampGradientBoundarySize = 0
1217 task2 = cpPipe.MeasureDefectsTask(config=config)
1218 defects2 = task2.run(flat, camera).outputDefects
1220 # Check that there are no false defects.
1221 self.assertEqual(len(defects2), 0)
1224class TestMemory(lsst.utils.tests.MemoryTestCase):
1225 pass
1228def setup_module(module):
1229 lsst.utils.tests.init()
1232if __name__ == "__main__": 1232 ↛ 1233line 1232 didn't jump to line 1233 because the condition on line 1232 was never true
1233 lsst.utils.tests.init()
1234 unittest.main()