Coverage for tests/test_isrTaskLSST.py: 4%
1216 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-14 02:03 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-14 02:03 -0700
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#
23import copy
24import unittest
25import numpy as np
26import logging
27import galsim
28from scipy.stats import median_abs_deviation
30import lsst.afw.geom as afwGeom
31import lsst.geom as geom
32from lsst.pipe.base import UnprocessableDataError
33import lsst.ip.isr.isrMockLSST as isrMockLSST
34import lsst.utils.tests
35from lsst.ip.isr.isrTaskLSST import (IsrTaskLSST, IsrTaskLSSTConfig)
36from lsst.ip.isr.crosstalk import CrosstalkCalib
37from lsst.ip.isr import PhotonTransferCurveDataset
38from lsst.ip.isr.vignette import maskVignettedRegion
41class IsrTaskLSSTTestCase(lsst.utils.tests.TestCase):
42 """Test IsrTaskLSST"""
43 def setUp(self):
44 mock = isrMockLSST.IsrMockLSST()
45 self.camera = mock.getCamera()
46 self.detector = self.camera[mock.config.detectorIndex]
47 self.namp = len(self.detector)
49 # Create adu (bootstrap) calibration frames
50 self.bias_adu = isrMockLSST.BiasMockLSST(adu=True).run()
51 self.dark_adu = isrMockLSST.DarkMockLSST(adu=True).run()
52 self.flat_adu = isrMockLSST.FlatMockLSST(adu=True).run()
53 self.flat_adu.metadata["FLATSRC"] = "DOME"
55 # Create calibration frames
56 self.bias = isrMockLSST.BiasMockLSST().run()
57 self.dark = isrMockLSST.DarkMockLSST().run()
58 self.flat = isrMockLSST.FlatMockLSST().run()
59 self.flat.metadata["FLATSRC"] = "DOME"
60 self.bf_kernel = isrMockLSST.BfKernelMockLSST().run()
61 self.cti = isrMockLSST.DeferredChargeMockLSST().run()
63 # The crosstalk ratios in isrMockLSST are in electrons.
64 self.crosstalk = CrosstalkCalib(nAmp=self.namp)
65 self.crosstalk.hasCrosstalk = True
66 self.crosstalk.coeffs = isrMockLSST.CrosstalkCoeffMockLSST().run()
67 for i, amp in enumerate(self.detector):
68 self.crosstalk.fitGains[i] = mock.config.gainDict[amp.getName()]
69 self.crosstalk.crosstalkRatiosUnits = "electron"
71 self.defects = isrMockLSST.DefectMockLSST().run()
73 amp_names = [x.getName() for x in self.detector.getAmplifiers()]
74 self.ptc = PhotonTransferCurveDataset(amp_names,
75 ptcFitType='DUMMY_PTC',
76 covMatrixSide=1)
78 self.saturation_adu = 100_000.0
80 # PTC records noise units in electron, same as the
81 # configuration parameter.
82 for amp_name in amp_names:
83 self.ptc.gain[amp_name] = mock.config.gainDict.get(amp_name, mock.config.gain)
84 self.ptc.noise[amp_name] = mock.config.readNoise
85 self.ptc.ptcTurnoff[amp_name] = self.saturation_adu
86 pre_level = mock.config.clockInjectedOffsetLevel
87 overscan_level = mock.config.biasLevel + (pre_level / self.ptc.gain[amp_name])
88 self.ptc.overscanMedian[amp_name] = overscan_level
89 # We set this sigma level very large because the
90 # noise characteristics of the mock image with
91 # a large serial gradient and small overscan region
92 # amplifies the noise vs LSSTCam.
93 self.ptc.overscanMedianSigma[amp_name] = 10.0
95 # TODO:
96 # self.cti = isrMockLSST.DeferredChargeMockLSST().run()
98 self.linearizer = isrMockLSST.LinearizerMockLSST().run()
99 # We currently only have high-signal non-linearity.
100 mock_config = self.get_mock_config_no_signal()
101 for amp_name in amp_names:
102 coeffs = self.linearizer.linearityCoeffs[amp_name]
103 centers, values = np.split(coeffs, 2)
104 values[centers < mock_config.highSignalNonlinearityThreshold] = 0.0
105 self.linearizer.linearityCoeffs[amp_name] = np.concatenate((centers, values))
107 def _check_applied_keys(self, metadata, isr_config):
108 """Check if the APPLIED keys have been set properly.
110 Parameters
111 ----------
112 metadata : `lsst.daf.base.PropertyList`
113 isr_config : `lsst.ip.isr.IsrTaskLSSTConfig`
115 """
116 key = "LSST ISR CROSSTALK APPLIED"
117 self.assertIn(key, metadata)
118 self.assertEqual(metadata[key], isr_config.doCrosstalk)
120 key = "LSST ISR OVERSCANLEVEL CHECKED"
121 self.assertIn(key, metadata)
122 self.assertEqual(metadata[key], np.isfinite(isr_config.serialOverscanMedianShiftSigmaThreshold))
124 key = "LSST ISR NOISE CHECKED"
125 self.assertIn(key, metadata)
126 self.assertEqual(metadata[key], np.isfinite(isr_config.ampNoiseThreshold))
128 key = "LSST ISR LINEARIZER APPLIED"
129 self.assertIn(key, metadata)
130 self.assertEqual(metadata[key], isr_config.doLinearize)
132 key = "LSST ISR CTI APPLIED"
133 self.assertIn(key, metadata)
134 self.assertEqual(metadata[key], isr_config.doDeferredCharge)
136 key = "LSST ISR BIAS APPLIED"
137 self.assertIn(key, metadata)
138 self.assertEqual(metadata[key], isr_config.doBias)
140 key = "LSST ISR DARK APPLIED"
141 self.assertIn(key, metadata)
142 self.assertEqual(metadata[key], isr_config.doDark)
144 key = "LSST ISR BF APPLIED"
145 self.assertIn(key, metadata)
146 self.assertEqual(metadata[key], isr_config.doBrighterFatter)
148 key = "LSST ISR FLAT APPLIED"
149 self.assertIn(key, metadata)
150 self.assertEqual(metadata[key], isr_config.doFlat)
152 if metadata[key]:
153 key2 = "LSST ISR FLAT SOURCE"
154 self.assertIn(key2, metadata)
155 self.assertEqual(metadata[key2], "DOME")
157 key = "LSST ISR DEFECTS APPLIED"
158 self.assertIn(key, metadata)
159 self.assertEqual(metadata[key], isr_config.doDefect)
161 def test_isrBootstrapBias(self):
162 """Test processing of a ``bootstrap`` bias frame.
164 This will be output with ADU units.
165 """
166 mock_config = self.get_mock_config_no_signal()
168 mock = isrMockLSST.IsrMockLSST(config=mock_config)
169 input_exp = mock.run()
171 isr_config = self.get_isr_config_minimal_corrections()
172 isr_config.doBootstrap = True
173 isr_config.doApplyGains = False
174 isr_config.doBias = True
175 isr_config.doCrosstalk = True
177 # Need to make sure we are not masking the negative variance
178 # pixels when directly comparing calibration images and
179 # calibration-corrected calibrations.
180 isr_config.maskNegativeVariance = False
182 isr_task = IsrTaskLSST(config=isr_config)
183 with self.assertLogs(level=logging.WARNING) as cm:
184 result = isr_task.run(
185 input_exp.clone(),
186 bias=self.bias_adu,
187 ptc=self.ptc,
188 crosstalk=self.crosstalk,
189 )
190 self.assertIn("Ignoring provided PTC", cm.output[0])
191 self._check_applied_keys(result.exposure.metadata, isr_config)
193 # Rerun without doing the bias correction.
194 isr_config.doBias = False
195 isr_task2 = IsrTaskLSST(config=isr_config)
196 with self.assertNoLogs(level=logging.WARNING):
197 result2 = isr_task2.run(input_exp.clone(), crosstalk=self.crosstalk)
199 good_pixels = self.get_non_defect_pixels(result.exposure.mask)
201 self.assertLess(
202 np.mean(result.exposure.image.array[good_pixels]),
203 np.mean(result2.exposure.image.array[good_pixels]),
204 )
205 self.assertLess(
206 np.std(result.exposure.image.array[good_pixels]),
207 np.std(result2.exposure.image.array[good_pixels]),
208 )
210 delta = result2.exposure.image.array - result.exposure.image.array
211 self.assertFloatsAlmostEqual(delta[good_pixels], self.bias_adu.image.array[good_pixels], atol=1e-5)
213 metadata = result.exposure.metadata
215 key = "LSST ISR BOOTSTRAP"
216 self.assertIn(key, metadata)
217 self.assertEqual(metadata[key], True)
219 key = "LSST ISR UNITS"
220 self.assertIn(key, metadata)
221 self.assertEqual(metadata[key], "adu")
223 key = "LSST ISR READNOISE UNITS"
224 self.assertIn(key, metadata)
225 self.assertEqual(metadata[key], "electron")
227 for amp in self.detector:
228 amp_name = amp.getName()
229 key = f"LSST ISR GAIN {amp_name}"
230 self.assertIn(key, metadata)
231 self.assertEqual(metadata[key], 1.0)
233 self._check_bad_column_crosstalk_correction(result.exposure)
235 def test_isrBootstrapDark(self):
236 """Test processing of a ``bootstrap`` dark frame.
238 This will be output with ADU units.
239 """
240 mock_config = self.get_mock_config_no_signal()
241 mock_config.doAddDark = True
243 mock = isrMockLSST.IsrMockLSST(config=mock_config)
244 input_exp = mock.run()
246 isr_config = self.get_isr_config_minimal_corrections()
247 isr_config.doBootstrap = True
248 isr_config.doApplyGains = False
249 isr_config.doBias = True
250 isr_config.doDark = True
251 isr_config.maskNegativeVariance = False
252 isr_config.doCrosstalk = True
254 isr_task = IsrTaskLSST(config=isr_config)
255 with self.assertLogs(level=logging.WARNING) as cm:
256 result = isr_task.run(
257 input_exp.clone(),
258 bias=self.bias_adu,
259 dark=self.dark_adu,
260 ptc=self.ptc,
261 crosstalk=self.crosstalk,
262 )
263 self.assertIn("Ignoring provided PTC", cm.output[0])
264 self._check_applied_keys(result.exposure.metadata, isr_config)
266 # Rerun without doing the dark correction.
267 isr_config.doDark = False
268 isr_task2 = IsrTaskLSST(config=isr_config)
269 with self.assertNoLogs(level=logging.WARNING):
270 result2 = isr_task2.run(input_exp.clone(), bias=self.bias_adu, crosstalk=self.crosstalk)
272 good_pixels = self.get_non_defect_pixels(result.exposure.mask)
274 self.assertLess(
275 np.mean(result.exposure.image.array[good_pixels]),
276 np.mean(result2.exposure.image.array[good_pixels]),
277 )
279 delta = result2.exposure.image.array - result.exposure.image.array
280 exp_time = input_exp.getInfo().getVisitInfo().getExposureTime()
281 self.assertFloatsAlmostEqual(
282 delta[good_pixels],
283 self.dark_adu.image.array[good_pixels] * exp_time,
284 atol=1e-5,
285 )
287 metadata = result.exposure.metadata
289 key = "LSST ISR BOOTSTRAP"
290 self.assertIn(key, metadata)
291 self.assertEqual(metadata[key], True)
293 key = "LSST ISR UNITS"
294 self.assertIn(key, metadata)
295 self.assertEqual(metadata[key], "adu")
297 self._check_bad_column_crosstalk_correction(result.exposure)
299 def test_isrBootstrapFlat(self):
300 """Test processing of a ``bootstrap`` flat frame.
302 This will be output with ADU units.
303 """
304 mock_config = self.get_mock_config_no_signal()
305 mock_config.doAddDark = True
306 mock_config.doAddFlat = True
307 # The doAddSky option adds the equivalent of flat-field flux.
308 mock_config.doAddSky = True
310 mock = isrMockLSST.IsrMockLSST(config=mock_config)
311 input_exp = mock.run()
313 isr_config = self.get_isr_config_minimal_corrections()
314 isr_config.doBootstrap = True
315 isr_config.doApplyGains = False
316 isr_config.doBias = True
317 isr_config.doDark = True
318 isr_config.doFlat = True
319 isr_config.maskNegativeVariance = False
320 isr_config.doCrosstalk = True
322 isr_task = IsrTaskLSST(config=isr_config)
323 with self.assertLogs(level=logging.WARNING) as cm:
324 result = isr_task.run(
325 input_exp.clone(),
326 bias=self.bias_adu,
327 dark=self.dark_adu,
328 flat=self.flat_adu,
329 ptc=self.ptc,
330 crosstalk=self.crosstalk,
331 )
332 self.assertIn("Ignoring provided PTC", cm.output[0])
333 self._check_applied_keys(result.exposure.metadata, isr_config)
335 # Rerun without doing the flat correction.
336 isr_config.doFlat = False
337 isr_task2 = IsrTaskLSST(config=isr_config)
338 with self.assertNoLogs(level=logging.WARNING):
339 result2 = isr_task2.run(
340 input_exp.clone(),
341 bias=self.bias_adu,
342 dark=self.dark_adu,
343 crosstalk=self.crosstalk,
344 )
346 good_pixels = self.get_non_defect_pixels(result.exposure.mask)
348 # Applying the flat will increase the counts.
349 self.assertGreater(
350 np.mean(result.exposure.image.array[good_pixels]),
351 np.mean(result2.exposure.image.array[good_pixels]),
352 )
353 # And will decrease the sigma.
354 self.assertLess(
355 np.std(result.exposure.image.array[good_pixels]),
356 np.std(result2.exposure.image.array[good_pixels]),
357 )
359 ratio = result2.exposure.image.array / result.exposure.image.array
360 self.assertFloatsAlmostEqual(ratio[good_pixels], self.flat_adu.image.array[good_pixels], atol=1e-5)
362 # Test the variance plane in the case of adu units.
363 # The expected variance starts with the image array.
364 expected_variance = result.exposure.image.clone()
365 # We have to remove the flat-fielding from the image pixels.
366 expected_variance.array *= self.flat_adu.image.array
367 # And add in the bias variance.
368 expected_variance.array += self.bias_adu.variance.array
369 # And add in the scaled dark variance.
370 scale = result.exposure.visitInfo.darkTime / self.dark_adu.visitInfo.darkTime
371 expected_variance.array += scale**2. * self.dark_adu.variance.array
372 # And add the gain and read noise (in electron) per amp.
373 for amp in self.detector:
374 # We need to use the gain and read noise from the header
375 # because these are bootstraps.
376 gain = result.exposure.metadata[f"LSST ISR GAIN {amp.getName()}"]
377 read_noise = result.exposure.metadata[f"LSST ISR READNOISE {amp.getName()}"]
379 expected_variance[amp.getBBox()].array /= gain
380 # Read noise is always in electron units, but since this is a
381 # bootstrap, the gain is 1.0.
382 expected_variance[amp.getBBox()].array += (read_noise/gain)**2.
384 # And apply the full formula for dividing by the flat with variance.
385 # See https://github.com/lsst/afw/blob/efa07fa68475fbe12f8f16df245a99ba3042166d/src/image/MaskedImage.cc#L353-L358 # noqa: E501, W505
386 unflat_image_array = result.exposure.image.array * self.flat_adu.image.array
387 expected_variance.array = ((unflat_image_array**2. * self.flat_adu.variance.array
388 + self.flat_adu.image.array**2. * expected_variance.array)
389 / self.flat_adu.image.array**4.)
391 self.assertFloatsAlmostEqual(
392 result.exposure.variance.array[good_pixels],
393 expected_variance.array[good_pixels],
394 rtol=1e-6,
395 )
397 metadata = result.exposure.metadata
399 key = "LSST ISR BOOTSTRAP"
400 self.assertIn(key, metadata)
401 self.assertEqual(metadata[key], True)
403 key = "LSST ISR UNITS"
404 self.assertIn(key, metadata)
405 self.assertEqual(metadata[key], "adu")
407 self._check_bad_column_crosstalk_correction(result.exposure)
409 def test_isrBootstrapAndRegularFlat(self):
410 """Test that bootstrap and "regular" flat processing are equivalent."""
411 # This is a test for DM-52684, for the linearizer units.
413 mock_config = self.get_mock_config_no_signal()
414 mock_config.doAddDark = True
415 mock_config.doAddFlat = True
416 # The doAddSky option adds the equivalent of flat-field flux.
417 mock_config.doAddSky = True
418 # We set the sky/flat level to a range where the "high signal"
419 # non-linearity has kicked in.
420 mock_config.skyLevel = 40000.0
422 mock = isrMockLSST.IsrMockLSST(config=mock_config)
423 input_exp = mock.run()
425 # First config is with linearizer in bootstrap mode.
427 isr_config_bootstrap = self.get_isr_config_minimal_corrections()
428 isr_config_bootstrap.doBootstrap = True
429 isr_config_bootstrap.doApplyGains = False
430 isr_config_bootstrap.doLinearize = True
431 isr_config_bootstrap.doBias = False
432 isr_config_bootstrap.doDark = False
433 isr_config_bootstrap.doFlat = False
434 isr_config_bootstrap.maskNegativeVariance = False
435 isr_config_bootstrap.doCrosstalk = True
437 isr_task_bootstrap = IsrTaskLSST(config=isr_config_bootstrap)
438 with self.assertNoLogs(level=logging.WARNING):
439 result = isr_task_bootstrap.run(
440 input_exp.clone(),
441 crosstalk=self.crosstalk,
442 linearizer=self.linearizer,
443 )
445 exp_bootstrap = result.exposure
447 # Apply the gains (after the linearization); this is
448 # similar to processing in PTC building.
449 for amp in self.detector:
450 exp_bootstrap[amp.getBBox()].image.array *= self.ptc.gain[amp.getName()]
452 # Run again with non-bootstrap mode.
453 isr_config = self.get_isr_config_minimal_corrections()
454 isr_config.doBootstrap = False
455 isr_config.doApplyGains = True
456 isr_config.doLinearize = True
457 isr_config.doBias = False
458 isr_config.doDark = False
459 isr_config.doFlat = False
460 isr_config.maskNegativeVariance = False
461 isr_config.doCrosstalk = True
463 isr_task = IsrTaskLSST(config=isr_config)
464 with self.assertNoLogs(level=logging.WARNING):
465 result = isr_task.run(
466 input_exp.clone(),
467 crosstalk=self.crosstalk,
468 linearizer=self.linearizer,
469 ptc=self.ptc,
470 )
472 exp = result.exposure
474 good_pixels = self.get_non_defect_pixels(result.exposure.mask)
476 delta = exp.image.array - exp_bootstrap.image.array
478 self.assertFloatsAlmostEqual(delta[good_pixels], 0.0, atol=1e-2)
480 def test_isrBias(self):
481 """Test processing of a bias frame."""
482 mock_config = self.get_mock_config_no_signal()
484 mock = isrMockLSST.IsrMockLSST(config=mock_config)
485 input_exp = mock.run()
487 isr_config = self.get_isr_config_electronic_corrections()
488 isr_config.doBias = True
489 # We do not do defect correction when processing biases.
490 isr_config.doDefect = False
491 isr_config.maskNegativeVariance = False
493 isr_task = IsrTaskLSST(config=isr_config)
494 with self.assertNoLogs(level=logging.WARNING):
495 result = isr_task.run(
496 input_exp.clone(),
497 bias=self.bias,
498 crosstalk=self.crosstalk,
499 ptc=self.ptc,
500 linearizer=self.linearizer,
501 deferredChargeCalib=self.cti,
502 )
503 self._check_applied_keys(result.exposure.metadata, isr_config)
505 # Rerun without doing the bias correction.
506 isr_config.doBias = False
507 isr_task2 = IsrTaskLSST(config=isr_config)
508 with self.assertNoLogs(level=logging.WARNING):
509 result2 = isr_task2.run(
510 input_exp.clone(),
511 crosstalk=self.crosstalk,
512 ptc=self.ptc,
513 linearizer=self.linearizer,
514 deferredChargeCalib=self.cti,
515 )
517 good_pixels = self.get_non_defect_pixels(result.exposure.mask)
519 self.assertLess(
520 np.mean(result.exposure.image.array[good_pixels]),
521 np.mean(result2.exposure.image.array[good_pixels]),
522 )
524 self.assertLess(
525 np.std(result.exposure.image.array[good_pixels]),
526 np.std(result2.exposure.image.array[good_pixels]),
527 )
529 # Confirm that it is flat with an arbitrary cutoff that depends
530 # on the read noise.
531 self.assertLess(np.std(result.exposure.image.array[good_pixels]), 2.0*mock_config.readNoise)
533 delta = result2.exposure.image.array - result.exposure.image.array
535 # Note that the bias is made with bias noise + read noise, and
536 # the image contains read noise.
537 self.assertFloatsAlmostEqual(
538 delta[good_pixels],
539 self.bias.image.array[good_pixels],
540 atol=1e-5,
541 )
543 self._check_bad_column_crosstalk_correction(result.exposure)
545 def test_isrBiasNoParallelOscanCorrection(self):
546 """Test processing of a bias frame with parallel
547 overscan correction turned off."""
548 mock_config = self.get_mock_config_no_signal()
549 mock_config.doAddParallelOverscanRamp = False
551 mock = isrMockLSST.IsrMockLSST(config=mock_config)
552 input_exp = mock.run()
554 isr_config = self.get_isr_config_electronic_corrections()
556 # Turn off the parallel overscan correction
557 amp_oscan_config = isr_config.overscanCamera.getOverscanDetectorConfig(self.detector).defaultAmpConfig
558 amp_oscan_config.doParallelOverscan = False
559 isr_config.doBias = True
561 # We do not do defect correction when processing biases.
562 isr_config.doDefect = False
563 isr_config.maskNegativeVariance = False
565 isr_task = IsrTaskLSST(config=isr_config)
566 with self.assertNoLogs(level=logging.WARNING):
567 result = isr_task.run(
568 input_exp.clone(),
569 bias=self.bias,
570 crosstalk=self.crosstalk,
571 ptc=self.ptc,
572 linearizer=self.linearizer,
573 deferredChargeCalib=self.cti,
574 )
575 self._check_applied_keys(result.exposure.metadata, isr_config)
577 # Rerun without doing the bias correction.
578 isr_config.doBias = False
579 isr_task2 = IsrTaskLSST(config=isr_config)
580 with self.assertNoLogs(level=logging.WARNING):
581 result2 = isr_task2.run(
582 input_exp.clone(),
583 crosstalk=self.crosstalk,
584 ptc=self.ptc,
585 linearizer=self.linearizer,
586 deferredChargeCalib=self.cti,
587 )
589 good_pixels = self.get_non_defect_pixels(result.exposure.mask)
591 self.assertLess(
592 np.mean(result.exposure.image.array[good_pixels]),
593 np.mean(result2.exposure.image.array[good_pixels]),
594 )
596 self.assertLess(
597 np.std(result.exposure.image.array[good_pixels]),
598 np.std(result2.exposure.image.array[good_pixels]),
599 )
601 # Confirm that it is flat with an arbitrary cutoff that depends
602 # on the read noise.
603 self.assertLess(np.std(result.exposure.image.array[good_pixels]), 2.0*mock_config.readNoise)
605 delta = result2.exposure.image.array - result.exposure.image.array
607 # Note that the bias is made with bias noise + read noise, and
608 # the image contains read noise.
609 self.assertFloatsAlmostEqual(
610 delta[good_pixels],
611 self.bias.image.array[good_pixels],
612 atol=1e-5,
613 )
615 self._check_bad_column_crosstalk_correction(result.exposure)
617 def test_isrBiasCti(self):
618 """Test over-correction of bias amp edges from prescan."""
619 mock_config = self.get_mock_config_no_signal()
620 mock_config.doAddBrightDefects = False
622 mock = isrMockLSST.IsrMockLSST(config=mock_config)
623 input_exp = mock.run()
625 isr_config = self.get_isr_config_electronic_corrections()
626 # We do not do defect correction when processing biases.
627 isr_config.doDefect = False
628 isr_config.maskNegativeVariance = False
630 isr_task = IsrTaskLSST(config=isr_config)
631 with self.assertNoLogs(level=logging.WARNING):
632 result = isr_task.run(
633 input_exp.clone(),
634 crosstalk=self.crosstalk,
635 ptc=self.ptc,
636 linearizer=self.linearizer,
637 deferredChargeCalib=self.cti,
638 )
639 self._check_applied_keys(result.exposure.metadata, isr_config)
641 # Rerun without doing the CTI correction
642 isr_config.doDeferredCharge = False
644 isr_task2 = IsrTaskLSST(config=isr_config)
645 with self.assertNoLogs(level=logging.WARNING):
646 result2 = isr_task2.run(
647 input_exp.clone(),
648 crosstalk=self.crosstalk,
649 ptc=self.ptc,
650 linearizer=self.linearizer,
651 )
653 # This confirms that things are *close* to equal. Unfortunately,
654 # the unusual camera geometry in the test camera doesn't completely
655 # zero out the prescan pixels, so we need a higher threshold.
656 std_delta = np.std(result2.exposure.image.array - result.exposure.image.array)
657 self.assertLess(std_delta, 0.15)
659 def test_isrDark(self):
660 """Test processing of a dark frame."""
661 mock_config = self.get_mock_config_no_signal()
662 mock_config.doAddDark = True
663 # We turn off the bad parallel overscan column because it does
664 # add more noise to that region.
665 mock_config.doAddBadParallelOverscanColumn = False
667 mock = isrMockLSST.IsrMockLSST(config=mock_config)
668 input_exp = mock.run()
670 isr_config = self.get_isr_config_electronic_corrections()
671 isr_config.doBias = True
672 isr_config.doDark = True
673 # We do not do defect correction when processing darks.
674 isr_config.doDefect = False
675 isr_config.maskNegativeVariance = False
677 isr_task = IsrTaskLSST(config=isr_config)
678 with self.assertNoLogs(level=logging.WARNING):
679 result = isr_task.run(
680 input_exp.clone(),
681 bias=self.bias,
682 dark=self.dark,
683 crosstalk=self.crosstalk,
684 ptc=self.ptc,
685 linearizer=self.linearizer,
686 deferredChargeCalib=self.cti,
687 )
688 self._check_applied_keys(result.exposure.metadata, isr_config)
690 # Rerun without doing the dark correction.
691 isr_config.doDark = False
692 isr_task2 = IsrTaskLSST(config=isr_config)
693 with self.assertNoLogs(level=logging.WARNING):
694 result2 = isr_task2.run(
695 input_exp.clone(),
696 bias=self.bias,
697 crosstalk=self.crosstalk,
698 ptc=self.ptc,
699 linearizer=self.linearizer,
700 deferredChargeCalib=self.cti,
701 )
703 good_pixels = self.get_non_defect_pixels(result.exposure.mask)
705 self.assertLess(
706 np.mean(result.exposure.image.array[good_pixels]),
707 np.mean(result2.exposure.image.array[good_pixels]),
708 )
709 # The mock dark has no noise, so these should be equal.
710 self.assertFloatsAlmostEqual(
711 np.std(result.exposure.image.array[good_pixels]),
712 np.std(result2.exposure.image.array[good_pixels]),
713 atol=1e-12,
714 )
716 # This is a somewhat arbitrary comparison that includes a fudge
717 # factor for the extra noise from the overscan subtraction.
718 self.assertLess(
719 np.std(result.exposure.image.array[good_pixels]),
720 1.6*np.sqrt(mock_config.darkRate*mock_config.expTime + mock_config.readNoise),
721 )
723 delta = result2.exposure.image.array - result.exposure.image.array
724 exp_time = input_exp.getInfo().getVisitInfo().getExposureTime()
726 # Allow <3 pixels to fail this test due to rounding error
727 # if doRoundAdu=True
728 diff = np.abs(delta[good_pixels] - self.dark.image.array[good_pixels] * exp_time)
729 self.assertLess(np.count_nonzero(diff >= 1e-12), 3)
731 self._check_bad_column_crosstalk_correction(result.exposure)
733 def test_isrFlat(self):
734 """Test processing of a flat frame."""
735 mock_config = self.get_mock_config_no_signal()
736 mock_config.doAddDark = True
737 mock_config.doAddFlat = True
738 # The doAddSky option adds the equivalent of flat-field flux.
739 mock_config.doAddSky = True
741 mock = isrMockLSST.IsrMockLSST(config=mock_config)
742 input_exp = mock.run()
744 isr_config = self.get_isr_config_electronic_corrections()
745 isr_config.doBias = True
746 isr_config.doDark = True
747 isr_config.doFlat = True
748 # Although we usually do not do defect interpolation when
749 # processing flats, this is a good test of the interpolation.
750 isr_config.doDefect = True
751 isr_config.maskNegativeVariance = False
753 isr_task = IsrTaskLSST(config=isr_config)
754 with self.assertNoLogs(level=logging.WARNING):
755 result = isr_task.run(
756 input_exp.clone(),
757 bias=self.bias,
758 dark=self.dark,
759 flat=self.flat,
760 crosstalk=self.crosstalk,
761 defects=self.defects,
762 ptc=self.ptc,
763 linearizer=self.linearizer,
764 deferredChargeCalib=self.cti,
765 )
766 self._check_applied_keys(result.exposure.metadata, isr_config)
768 # Rerun without doing the bias correction.
769 isr_config.doFlat = False
770 isr_task2 = IsrTaskLSST(config=isr_config)
771 with self.assertNoLogs(level=logging.WARNING):
772 result2 = isr_task2.run(
773 input_exp.clone(),
774 bias=self.bias,
775 dark=self.dark,
776 crosstalk=self.crosstalk,
777 defects=self.defects,
778 ptc=self.ptc,
779 linearizer=self.linearizer,
780 deferredChargeCalib=self.cti,
781 )
783 # With defect correction, we should not need to filter out bad
784 # pixels.
786 # Applying the flat will increase the counts.
787 self.assertGreater(
788 np.mean(result.exposure.image.array),
789 np.mean(result2.exposure.image.array),
790 )
791 # And will decrease the sigma.
792 self.assertLess(
793 np.std(result.exposure.image.array),
794 np.std(result2.exposure.image.array),
795 )
797 # Check that the resulting image is approximately flat.
798 # In particular that the noise is consistent with sky + margin.
799 self.assertLess(np.std(result.exposure.image.array), np.sqrt(mock_config.skyLevel) + 3.0)
801 # Generate a flat without any defects for comparison
802 # (including interpolation)
803 flat_nodefect_config = isrMockLSST.FlatMockLSST.ConfigClass()
804 flat_nodefect_config.doAddBrightDefects = False
805 flat_nodefects = isrMockLSST.FlatMockLSST(config=flat_nodefect_config).run()
807 ratio = result2.exposure.image.array / result.exposure.image.array
808 self.assertFloatsAlmostEqual(ratio, flat_nodefects.image.array, atol=1e-4)
810 self._check_bad_column_crosstalk_correction(result.exposure)
812 def test_isrNoise(self):
813 """Test the recorded noise and gain in the metadata."""
814 mock_config = self.get_mock_config_no_signal()
815 # Remove the overscan scale so that the only variation
816 # in the overscan is from the read noise.
817 mock_config.overscanScale = 0.0
819 mock = isrMockLSST.IsrMockLSST(config=mock_config)
820 input_exp = mock.run()
822 isr_config = self.get_isr_config_electronic_corrections()
823 isr_config.doBias = True
824 # We do not do defect correction when processing biases.
825 isr_config.doDefect = False
826 isr_config.maskNegativeVariance = False
828 isr_task = IsrTaskLSST(config=isr_config)
829 with self.assertNoLogs(level=logging.WARNING):
830 result = isr_task.run(
831 input_exp.clone(),
832 bias=self.bias,
833 crosstalk=self.crosstalk,
834 ptc=self.ptc,
835 deferredChargeCalib=self.cti,
836 linearizer=self.linearizer,
837 )
838 self._check_applied_keys(result.exposure.metadata, isr_config)
840 metadata = result.exposure.metadata
842 for amp in self.detector:
843 # The overscan noise is always in adu and the readnoise is always
844 # in electron.
845 gain = result.exposure.metadata[f"LSST ISR GAIN {amp.getName()}"]
846 read_noise = result.exposure.metadata[f"LSST ISR READNOISE {amp.getName()}"]
848 # Check that the gain and read noise are consistent with the
849 # values stored in the PTC.
850 self.assertEqual(gain, self.ptc.gain[amp.getName()])
851 self.assertEqual(read_noise, self.ptc.noise[amp.getName()])
853 key = f"LSST ISR OVERSCAN RESIDUAL SERIAL STDEV {amp.getName()}"
854 self.assertIn(key, metadata)
856 # Determine if the residual serial overscan stddev is consistent
857 # with the PTC readnoise within 3xstandard error.
858 serial_overscan_area = amp.getRawHorizontalOverscanBBox().area
859 self.assertFloatsAlmostEqual(
860 metadata[key] * gain,
861 read_noise,
862 atol=3*read_noise / np.sqrt(serial_overscan_area),
863 )
865 def test_isrBrighterFatter(self):
866 """Test processing of a flat frame."""
867 # Image with brighter-fatter correction
868 mock_config = self.get_mock_config_no_signal()
869 mock_config.isTrimmed = False
870 mock_config.doAddDark = True
871 mock_config.doAddFlat = True
872 mock_config.doAddSky = True
873 mock_config.doAddSource = True
874 mock_config.sourceFlux = [75000.0]
875 mock_config.doAddBrighterFatter = True
877 mock = isrMockLSST.IsrMockLSST(config=mock_config)
878 input_exp = mock.run()
880 isr_config = self.get_isr_config_electronic_corrections()
881 isr_config.doBias = True
882 isr_config.doDark = True
883 isr_config.doFlat = True
884 isr_config.doBrighterFatter = True
886 isr_task = IsrTaskLSST(config=isr_config)
887 with self.assertNoLogs(level=logging.WARNING):
888 result = isr_task.run(
889 input_exp.clone(),
890 bias=self.bias,
891 dark=self.dark,
892 flat=self.flat,
893 deferredChargeCalib=self.cti,
894 crosstalk=self.crosstalk,
895 defects=self.defects,
896 ptc=self.ptc,
897 linearizer=self.linearizer,
898 bfKernel=self.bf_kernel,
899 )
900 self._check_applied_keys(result.exposure.metadata, isr_config)
902 mock_config = self.get_mock_config_no_signal()
903 mock_config.isTrimmed = False
904 mock_config.doAddDark = True
905 mock_config.doAddFlat = True
906 mock_config.doAddSky = True
907 mock_config.doAddSource = True
908 mock_config.sourceFlux = [75000.0]
909 mock_config.doAddBrighterFatter = False
911 mock = isrMockLSST.IsrMockLSST(config=mock_config)
912 input_truth = mock.run()
914 isr_config = self.get_isr_config_electronic_corrections()
915 isr_config.doBias = True
916 isr_config.doDark = True
917 isr_config.doFlat = True
918 isr_config.doBrighterFatter = False
920 isr_task = IsrTaskLSST(config=isr_config)
921 with self.assertNoLogs(level=logging.WARNING):
922 result_truth = isr_task.run(
923 input_truth.clone(),
924 bias=self.bias,
925 dark=self.dark,
926 flat=self.flat,
927 deferredChargeCalib=self.cti,
928 crosstalk=self.crosstalk,
929 defects=self.defects,
930 ptc=self.ptc,
931 linearizer=self.linearizer,
932 bfKernel=self.bf_kernel,
933 )
935 # Measure the source size in the BF-corrected image.
936 # The injected source is a Gaussian with 3.0px
937 image = galsim.ImageF(result.exposure.image.array)
938 image_truth = galsim.ImageF(result_truth.exposure.image.array)
939 source_centroid = galsim.PositionD(mock_config.sourceX[0], mock_config.sourceY[0])
940 hsm_result = galsim.hsm.FindAdaptiveMom(image, guess_centroid=source_centroid, strict=False)
941 hsm_result_truth = galsim.hsm.FindAdaptiveMom(image_truth, guess_centroid=source_centroid,
942 strict=False)
943 measured_sigma = hsm_result.moments_sigma
944 true_sigma = hsm_result_truth.moments_sigma
945 self.assertFloatsAlmostEqual(measured_sigma, true_sigma, rtol=3e-3)
947 # Check that the variance in an amp far away from the
948 # source is expected. The source is in amp 0; this will
949 # check the variation in neighboring amp 1
950 test_amp_bbox = result.exposure.detector.getAmplifiers()[1].getBBox()
951 n_pixels = test_amp_bbox.getArea()
952 stdev = np.std(result.exposure[test_amp_bbox].image.array)
953 stdev_truth = np.std(result_truth.exposure[test_amp_bbox].image.array)
954 self.assertFloatsAlmostEqual(stdev, stdev_truth, atol=3*stdev_truth/np.sqrt(n_pixels))
956 # Check that the variance in the amp with a defect is
957 # unchanged as a result of applying the BF correction after
958 # interpolating. The defect was added to amplifier 2.
959 test_amp_bbox = result.exposure.detector.getAmplifiers()[2].getBBox()
960 good_pixels = self.get_non_defect_pixels(result.exposure[test_amp_bbox].mask)
961 stdev = np.nanstd(result.exposure[test_amp_bbox].image.array[good_pixels])
962 stdev_truth = np.nanstd(result_truth.exposure[test_amp_bbox].image.array[good_pixels])
963 self.assertFloatsAlmostEqual(stdev, stdev_truth, atol=3*stdev_truth/np.sqrt(n_pixels))
965 # Check that BF has converged in the expected number of iterations.
966 metadata = result.exposure.metadata
967 key = "LSST ISR BF ITERS"
968 self.assertIn(key, metadata)
969 self.assertEqual(metadata[key], 2)
971 def test_isrSkyImage(self):
972 """Test processing of a sky image."""
973 mock_config = self.get_mock_config_no_signal()
974 mock_config.doAddDark = True
975 mock_config.doAddFlat = True
976 # Set this to False until we have fringe correction.
977 mock_config.doAddFringe = False
978 mock_config.doAddSky = True
979 mock_config.doAddSource = True
981 mock = isrMockLSST.IsrMockLSST(config=mock_config)
982 input_exp = mock.run()
984 isr_config = self.get_isr_config_electronic_corrections()
985 isr_config.doBias = True
986 isr_config.doDark = True
987 isr_config.doFlat = True
989 isr_task = IsrTaskLSST(config=isr_config)
990 with self.assertNoLogs(level=logging.WARNING):
991 result = isr_task.run(
992 input_exp.clone(),
993 bias=self.bias,
994 dark=self.dark,
995 flat=self.flat,
996 crosstalk=self.crosstalk,
997 defects=self.defects,
998 ptc=self.ptc,
999 linearizer=self.linearizer,
1000 deferredChargeCalib=self.cti,
1001 )
1002 self._check_applied_keys(result.exposure.metadata, isr_config)
1004 # Confirm that the output has the defect line as bad.
1005 sat_val = 2**result.exposure.mask.getMaskPlane("BAD")
1006 for defect in self.defects:
1007 np.testing.assert_array_equal(
1008 result.exposure.mask[defect.getBBox()].array & sat_val,
1009 sat_val,
1010 )
1012 clean_mock_config = self.get_mock_config_clean()
1013 # We want the dark noise for more direct comparison.
1014 clean_mock_config.doAddDarkNoiseOnly = True
1015 clean_mock_config.doAddSky = True
1016 clean_mock_config.doAddSource = True
1018 clean_mock = isrMockLSST.IsrMockLSST(config=clean_mock_config)
1019 clean_exp = clean_mock.run()
1021 delta = result.exposure.image.array - clean_exp.image.array
1023 good_pixels = self.get_non_defect_pixels(result.exposure.mask)
1025 # We compare the good pixels in the entirety.
1026 self.assertLess(np.std(delta[good_pixels]), 5.0)
1027 self.assertLess(np.max(np.abs(delta[good_pixels])), 5.0*7)
1029 # Make sure the corrected image is overall consistent with the
1030 # straight image.
1031 self.assertLess(np.abs(np.median(delta[good_pixels])), 0.51)
1033 # And overall where the interpolation is a bit worse but
1034 # the statistics are still fine.
1035 self.assertLess(np.std(delta), 5.5)
1037 metadata = result.exposure.metadata
1039 key = "LSST ISR BOOTSTRAP"
1040 self.assertIn(key, metadata)
1041 self.assertEqual(metadata[key], False)
1043 key = "LSST ISR UNITS"
1044 self.assertIn(key, metadata)
1045 self.assertEqual(metadata[key], "electron")
1047 for amp in self.detector:
1048 amp_name = amp.getName()
1049 key = f"LSST ISR GAIN {amp_name}"
1050 self.assertIn(key, metadata)
1051 self.assertEqual(metadata[key], gain := self.ptc.gain[amp_name])
1052 key = f"LSST ISR READNOISE {amp_name}"
1053 self.assertIn(key, metadata)
1054 self.assertEqual(metadata[key], self.ptc.noise[amp_name])
1055 key = f"LSST ISR SATURATION LEVEL {amp_name}"
1056 self.assertIn(key, metadata)
1057 self.assertEqual(metadata[key], self.saturation_adu * gain)
1058 key = f"LSST ISR SUSPECT LEVEL {amp_name}"
1059 self.assertIn(key, metadata)
1060 self.assertEqual(metadata[key], self.saturation_adu * gain)
1062 # Test the variance plane in the case of electron units.
1063 # The expected variance starts with the image array.
1064 expected_variance = result.exposure.image.clone()
1065 # We have to remove the flat-fielding from the image pixels.
1066 expected_variance.array *= self.flat.image.array
1067 # And add in the bias variance.
1068 expected_variance.array += self.bias.variance.array
1069 # And add in the scaled dark variance.
1070 scale = result.exposure.visitInfo.darkTime / self.dark.visitInfo.darkTime
1071 expected_variance.array += scale**2. * self.dark.variance.array
1072 # And add the read noise (in electrons) per amp.
1073 for amp in self.detector:
1074 gain = self.ptc.gain[amp.getName()]
1075 read_noise = self.ptc.noise[amp.getName()]
1077 # The image, read noise, and variance plane should all have
1078 # units of electrons, electrons, and electrons^2.
1079 expected_variance[amp.getBBox()].array += read_noise**2.
1081 # And apply the full formula for dividing by the flat with variance.
1082 # See https://github.com/lsst/afw/blob/efa07fa68475fbe12f8f16df245a99ba3042166d/src/image/MaskedImage.cc#L353-L358 # noqa: E501, W505
1083 unflat_image_array = result.exposure.image.array * self.flat.image.array
1084 expected_variance.array = ((unflat_image_array**2. * self.flat.variance.array
1085 + self.flat.image.array**2. * expected_variance.array)
1086 / self.flat.image.array**4.)
1088 self.assertFloatsAlmostEqual(
1089 result.exposure.variance.array[good_pixels],
1090 expected_variance.array[good_pixels],
1091 rtol=1e-6,
1092 )
1094 def test_isrSkyImageSaturated(self):
1095 """Test processing of a sky image.
1097 This variation uses saturated pixels instead of defects.
1099 This additionally tests the gain config override.
1100 """
1101 mock_config = self.get_mock_config_no_signal()
1102 mock_config.doAddDark = True
1103 mock_config.doAddFlat = True
1104 # Set this to False until we have fringe correction.
1105 mock_config.doAddFringe = False
1106 mock_config.doAddSky = True
1107 mock_config.doAddSource = True
1108 mock_config.brightDefectLevel = 170_000.0 # Above saturation.
1110 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1111 input_exp = mock.run()
1113 isr_config = self.get_isr_config_electronic_corrections()
1114 isr_config.doBias = True
1115 isr_config.doDark = True
1116 isr_config.doFlat = True
1117 # We turn off defect masking to test the saturation code.
1118 # However, the same pixels below should be masked/interpolated.
1119 isr_config.doDefect = False
1121 # Use a config override saturation value, confirm it is picked up.
1122 saturation_level = self.saturation_adu * 1.05
1124 # This code will set the gain of one amp to the same as the ptc
1125 # value, and we will check that it is logged and used but the
1126 # results should be the same.
1127 detectorConfig = isr_config.overscanCamera.getOverscanDetectorConfig(self.detector)
1128 detectorConfig.defaultAmpConfig.saturation = saturation_level
1129 overscanAmpConfig = copy.copy(detectorConfig.defaultAmpConfig)
1130 overscanAmpConfig.gain = self.ptc.gain[self.detector[1].getName()]
1131 detectorConfig.ampRules[self.detector[1].getName()] = overscanAmpConfig
1133 isr_task = IsrTaskLSST(config=isr_config)
1134 with self.assertLogs(level=logging.WARNING) as cm:
1135 result = isr_task.run(
1136 input_exp.clone(),
1137 bias=self.bias,
1138 dark=self.dark,
1139 flat=self.flat,
1140 deferredChargeCalib=self.cti,
1141 crosstalk=self.crosstalk,
1142 defects=self.defects,
1143 ptc=self.ptc,
1144 linearizer=self.linearizer,
1145 )
1146 self.assertIn("Overriding gain", cm.output[0])
1147 self._check_applied_keys(result.exposure.metadata, isr_config)
1149 # Confirm that the output has the defect line as saturated.
1150 sat_val = 2**result.exposure.mask.getMaskPlane("SAT")
1151 for defect in self.defects:
1152 np.testing.assert_array_equal(
1153 result.exposure.mask[defect.getBBox()].array & sat_val,
1154 sat_val,
1155 )
1157 clean_mock_config = self.get_mock_config_clean()
1158 # We want the dark noise for more direct comparison.
1159 clean_mock_config.doAddDarkNoiseOnly = True
1160 clean_mock_config.doAddSky = True
1161 clean_mock_config.doAddSource = True
1163 clean_mock = isrMockLSST.IsrMockLSST(config=clean_mock_config)
1164 clean_exp = clean_mock.run()
1166 delta = result.exposure.image.array - clean_exp.image.array
1168 bad_val = 2**result.exposure.mask.getMaskPlane("BAD")
1169 good_pixels = np.where((result.exposure.mask.array & (sat_val | bad_val)) == 0)
1171 # We compare the good pixels in the entirety.
1172 self.assertLess(np.std(delta[good_pixels]), 5.0)
1173 # This is sensitive to parallel overscan masking.
1174 self.assertLess(np.max(np.abs(delta[good_pixels])), 5.0*7)
1176 # Make sure the corrected image is overall consistent with the
1177 # straight image.
1178 self.assertLess(np.abs(np.median(delta[good_pixels])), 0.51)
1180 # And overall where the interpolation is a bit worse but
1181 # the statistics are still fine. Note that this is worse than
1182 # the defect case because of the widening of the saturation
1183 # trail.
1184 self.assertLess(np.std(delta), 7.0)
1186 metadata = result.exposure.metadata
1188 for amp in self.detector:
1189 amp_name = amp.getName()
1190 key = f"LSST ISR GAIN {amp_name}"
1191 self.assertIn(key, metadata)
1192 self.assertEqual(metadata[key], gain := self.ptc.gain[amp_name])
1193 key = f"LSST ISR READNOISE {amp_name}"
1194 self.assertIn(key, metadata)
1195 self.assertEqual(metadata[key], self.ptc.noise[amp_name])
1196 key = f"LSST ISR SATURATION LEVEL {amp_name}"
1197 self.assertIn(key, metadata)
1198 self.assertEqual(metadata[key], saturation_level * gain)
1200 def test_isrFlatVignette(self):
1201 """Test ISR when the flat has a validPolygon and vignetted region."""
1203 # We use a flat frame for this test for convenience.
1204 mock_config = self.get_mock_config_no_signal()
1205 mock_config.doAddDark = True
1206 mock_config.doAddFlat = True
1207 # The doAddSky option adds the equivalent of flat-field flux.
1208 mock_config.doAddSky = True
1210 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1211 input_exp = mock.run()
1213 isr_config = self.get_isr_config_electronic_corrections()
1214 isr_config.doBias = True
1215 isr_config.doDark = True
1216 isr_config.doFlat = True
1217 isr_config.doDefect = True
1219 flat = self.flat.clone()
1220 bbox = geom.Box2D(
1221 corner=geom.Point2D(0, 0),
1222 dimensions=geom.Extent2D(50, 50),
1223 )
1224 polygon = afwGeom.Polygon(bbox)
1225 flat.info.setValidPolygon(polygon)
1226 maskVignettedRegion(flat, polygon, vignetteValue=0.0)
1228 isr_task = IsrTaskLSST(config=isr_config)
1229 with self.assertNoLogs(level=logging.WARNING):
1230 result = isr_task.run(
1231 input_exp.clone(),
1232 bias=self.bias,
1233 dark=self.dark,
1234 flat=flat,
1235 crosstalk=self.crosstalk,
1236 ptc=self.ptc,
1237 linearizer=self.linearizer,
1238 defects=self.defects,
1239 deferredChargeCalib=self.cti,
1240 )
1242 self.assertEqual(result.exposure.info.getValidPolygon(), polygon)
1244 noDataFlat = (flat.mask.array & flat.mask.getPlaneBitMask("NO_DATA")) > 0
1245 noDataExp = (result.exposure.mask.array & result.exposure.mask.getPlaneBitMask("NO_DATA")) > 0
1246 np.testing.assert_array_equal(noDataExp, noDataFlat)
1247 np.testing.assert_array_equal(result.exposure.image.array[noDataExp], 0.0)
1248 np.testing.assert_array_equal(result.exposure.variance.array[noDataExp], 0.0)
1249 self.assertFalse(np.any(~np.isfinite(result.exposure.image.array)))
1250 self.assertFalse(np.any(~np.isfinite(result.exposure.variance.array)))
1252 def test_isrFloodedSaturatedE2V(self):
1253 """Test ISR when the amps are completely saturated.
1255 This version tests what happens when the parallel overscan
1256 region is flooded like E2V detectors, where the saturation
1257 spreads evenly, but at a greater level than the saturation
1258 value.
1259 """
1260 # We are simulating a flat field.
1261 # Note that these aren't very important because we are replacing
1262 # the flux, but we may as well.
1263 mock_config = self.get_mock_config_no_signal()
1264 mock_config.doAddDark = True
1265 mock_config.doAddFlat = True
1266 # The doAddSky option adds the equivalent of flat-field flux.
1267 mock_config.doAddSky = True
1269 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1270 input_exp = mock.run()
1272 isr_config = self.get_isr_config_minimal_corrections()
1273 isr_config.doBootstrap = True
1274 isr_config.doApplyGains = False
1275 isr_config.doBias = True
1276 isr_config.doDark = True
1277 isr_config.doFlat = False
1278 # Tun off saturation masking to simulate a PTC flat.
1279 isr_config.doSaturation = False
1280 isr_config.doE2VEdgeBleedMask = False
1281 isr_config.doITLEdgeBleedMask = False
1283 amp_config = isr_config.overscanCamera.defaultDetectorConfig.defaultAmpConfig
1284 parallel_overscan_saturation = amp_config.parallelOverscanConfig.parallelOverscanSaturationLevel
1286 detector = input_exp.getDetector()
1287 for i, amp in enumerate(detector):
1288 # For half of the amps we are testing what happens when the
1289 # parallel overscan region is above the configured saturation
1290 # level; for the other half we are testing the other branch
1291 # when it saturates below this level (which is a priori
1292 # unknown).
1293 if i < len(detector) // 2:
1294 data_level = (parallel_overscan_saturation * 1.05
1295 + mock_config.biasLevel
1296 + mock_config.clockInjectedOffsetLevel)
1297 parallel_overscan_level = (parallel_overscan_saturation * 1.1
1298 + mock_config.biasLevel
1299 + mock_config.clockInjectedOffsetLevel)
1300 else:
1301 data_level = (parallel_overscan_saturation * 0.7
1302 + mock_config.biasLevel
1303 + mock_config.clockInjectedOffsetLevel)
1304 parallel_overscan_level = (parallel_overscan_saturation * 0.75
1305 + mock_config.biasLevel
1306 + mock_config.clockInjectedOffsetLevel)
1308 input_exp[amp.getRawDataBBox()].image.array[:, :] = data_level
1309 input_exp[amp.getRawParallelOverscanBBox()].image.array[:, :] = parallel_overscan_level
1311 isr_task = IsrTaskLSST(config=isr_config)
1312 with self.assertLogs(level=logging.WARNING) as cm:
1313 result = isr_task.run(
1314 input_exp.clone(),
1315 bias=self.bias_adu,
1316 dark=self.dark_adu,
1317 )
1318 self.assertEqual(len(cm.records), len(detector))
1320 n_all = 0
1321 n_level = 0
1322 for record in cm.records:
1323 if "All overscan pixels masked" in record.message:
1324 n_all += 1
1325 if "The level in the overscan region" in record.message:
1326 n_level += 1
1328 self.assertEqual(n_all, len(detector) // 2)
1329 self.assertEqual(n_level, len(detector) // 2)
1331 # And confirm that the post-ISR levels are high for each amp.
1332 for amp in detector:
1333 med = np.median(result.exposure[amp.getBBox()].image.array)
1334 self.assertGreater(med, parallel_overscan_saturation*0.8)
1336 def test_isrFloodedSaturatedITL(self):
1337 """Test ISR when the amps are completely saturated.
1339 This version tests what happens when the parallel overscan
1340 region is flooded like ITL detectors, where the saturation
1341 is at a lower level than the imaging region, and also
1342 spreads partly into the serial/parallel region.
1343 """
1344 # We are simulating a flat field.
1345 # Note that these aren't very important because we are replacing
1346 # the flux, but we may as well.
1347 mock_config = self.get_mock_config_no_signal()
1348 mock_config.doAddDark = True
1349 mock_config.doAddFlat = True
1350 # The doAddSky option adds the equivalent of flat-field flux.
1351 mock_config.doAddSky = True
1353 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1354 input_exp = mock.run()
1356 isr_config = self.get_isr_config_minimal_corrections()
1357 isr_config.doBootstrap = True
1358 isr_config.doApplyGains = False
1359 isr_config.doBias = True
1360 isr_config.doDark = True
1361 isr_config.doFlat = False
1362 # Tun off saturation masking to simulate a PTC flat.
1363 isr_config.doSaturation = False
1364 isr_config.doE2VEdgeBleedMask = False
1365 isr_config.doITLEdgeBleedMask = False
1367 amp_config = isr_config.overscanCamera.defaultDetectorConfig.defaultAmpConfig
1368 parallel_overscan_saturation = amp_config.parallelOverscanConfig.parallelOverscanSaturationLevel
1370 detector = input_exp.getDetector()
1371 for i, amp in enumerate(detector):
1372 # For half of the amps we are testing what happens when the
1373 # parallel overscan region is above the configured saturation
1374 # level; for the other half we are testing the other branch
1375 # when it saturates below this level (which is a priori
1376 # unknown).
1377 if i < len(detector) // 2:
1378 data_level = (parallel_overscan_saturation * 1.1
1379 + mock_config.biasLevel
1380 + mock_config.clockInjectedOffsetLevel)
1381 parallel_overscan_level = (parallel_overscan_saturation * 1.05
1382 + mock_config.biasLevel
1383 + mock_config.clockInjectedOffsetLevel)
1384 else:
1385 data_level = (parallel_overscan_saturation * 0.75
1386 + mock_config.biasLevel
1387 + mock_config.clockInjectedOffsetLevel)
1388 parallel_overscan_level = (parallel_overscan_saturation * 0.7
1389 + mock_config.biasLevel
1390 + mock_config.clockInjectedOffsetLevel)
1392 input_exp[amp.getRawDataBBox()].image.array[:, :] = data_level
1393 input_exp[amp.getRawParallelOverscanBBox()].image.array[:, :] = parallel_overscan_level
1394 # The serial/parallel region for the test camera looks like this:
1395 serial_overscan_bbox = amp.getRawSerialOverscanBBox()
1396 parallel_overscan_bbox = amp.getRawParallelOverscanBBox()
1398 overscan_corner_bbox = geom.Box2I(
1399 geom.Point2I(
1400 serial_overscan_bbox.getMinX(),
1401 parallel_overscan_bbox.getMinY(),
1402 ),
1403 geom.Extent2I(
1404 serial_overscan_bbox.getWidth(),
1405 parallel_overscan_bbox.getHeight(),
1406 ),
1407 )
1408 input_exp[overscan_corner_bbox].image.array[-2:, :] = parallel_overscan_level
1410 isr_task = IsrTaskLSST(config=isr_config)
1411 with self.assertLogs(level=logging.WARNING) as cm:
1412 result = isr_task.run(
1413 input_exp.clone(),
1414 bias=self.bias_adu,
1415 dark=self.dark_adu,
1416 )
1417 self.assertEqual(len(cm.records), len(detector))
1419 n_all = 0
1420 n_level = 0
1421 for record in cm.records:
1422 if "All overscan pixels masked" in record.message:
1423 n_all += 1
1424 if "The level in the overscan region" in record.message:
1425 n_level += 1
1427 self.assertEqual(n_all, len(detector) // 2)
1428 self.assertEqual(n_level, len(detector) // 2)
1430 # And confirm that the post-ISR levels are high for each amp.
1431 for amp in detector:
1432 med = np.median(result.exposure[amp.getBBox()].image.array)
1433 self.assertGreater(med, parallel_overscan_saturation*0.8)
1435 def test_isrBadParallelOverscanColumnsBootstrap(self):
1436 """Test processing a bias when we have a bad parallel overscan column.
1438 This tests in bootstrap mode.
1439 """
1440 # We base this on the bootstrap bias, and make sure
1441 # that the bad column remains.
1442 mock_config = self.get_mock_config_no_signal()
1443 isr_config = self.get_isr_config_minimal_corrections()
1444 isr_config.doSaturation = False
1445 isr_config.doE2VEdgeBleedMask = False
1446 isr_config.doITLEdgeBleedMask = False
1447 isr_config.doBootstrap = True
1448 isr_config.doApplyGains = False
1450 amp_config = isr_config.overscanCamera.defaultDetectorConfig.defaultAmpConfig
1451 overscan_sat_level_adu = amp_config.parallelOverscanConfig.parallelOverscanSaturationLevel
1452 # The defect is in amp 2.
1453 amp_gain = mock_config.gainDict[self.detector[2].getName()]
1454 overscan_sat_level = amp_gain * overscan_sat_level_adu
1455 # The expected defect level is in adu for the bootstrap bias.
1456 expected_defect_level = mock_config.brightDefectLevel / amp_gain
1458 # The levels are set in electron units.
1459 # We test 3 levels:
1460 # * 10.0, a very low outlier, to test median smoothing detection
1461 # code. This value is given by gain*threshold + cushion.
1462 # * 575.0, a lowish but outlier level, given by gain*threshold +
1463 # 100.0 (average of the parallel overscan offset) + 10.0
1464 # (an additional cushion).
1465 # * 1.05*saturation.
1466 # Note that the default parallel overscan saturation level for
1467 # bootstrap (pre-saturation-measure) analysis is very low, in
1468 # order to capture all types of amps, even with low saturation.
1469 # Therefore, we only need to test above this saturation level.
1470 # (c.f. test_isrBadParallelOverscanColumns).
1471 levels = np.array([10.0, 575.0, 1.05*overscan_sat_level])
1473 for level in levels:
1474 mock_config.badParallelOverscanColumnLevel = level
1475 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1476 input_exp = mock.run()
1478 isr_task = IsrTaskLSST(config=isr_config)
1479 with self.assertNoLogs(level=logging.WARNING):
1480 result = isr_task.run(input_exp.clone())
1482 for defect in self.defects:
1483 bbox = defect.getBBox()
1484 defect_image = result.exposure[bbox].image.array
1486 # Check that the defect is the correct level
1487 # (not subtracted away).
1488 defect_median = np.median(defect_image)
1489 self.assertFloatsAlmostEqual(defect_median, expected_defect_level, rtol=1e-4)
1491 # Check that the neighbors aren't over-subtracted.
1492 for neighbor in [-1, 1]:
1493 bbox_neighbor = bbox.shiftedBy(geom.Extent2I(neighbor, 0))
1494 neighbor_image = result.exposure[bbox_neighbor].image.array
1496 neighbor_median = np.median(neighbor_image)
1497 self.assertFloatsAlmostEqual(neighbor_median, 0.0, atol=7.0)
1499 def test_isrBadParallelOverscanColumns(self):
1500 """Test processing a bias when we have a bad parallel overscan column.
1502 This test uses regular non-bootstrap processing.
1503 """
1504 mock_config = self.get_mock_config_no_signal()
1505 isr_config = self.get_isr_config_electronic_corrections()
1506 # We do not do defect correction when processing biases.
1507 isr_config.doDefect = False
1509 # The defect is in amp 2.
1510 sat_level_adu = self.ptc.ptcTurnoff[self.detector[2].getName()]
1511 amp_gain = mock_config.gainDict[self.detector[2].getName()]
1512 sat_level = amp_gain * sat_level_adu
1513 # The expected defect level is in electron for the full bias.
1514 expected_defect_level = mock_config.brightDefectLevel
1516 # The levels are set in electron units.
1517 # We test 4 levels:
1518 # * 10.0, a very low outlier, to test median smoothing detection
1519 # code. This value is given by gain*threshold + cushion.
1520 # * 575.0, a lowish but outlier level, given by gain*threshold +
1521 # 100.0 (average of the parallel overscan offset) + 10.0
1522 # (an additional cushion).
1523 # * 0.9*saturation, following ITL-style parallel overscan bleeds.
1524 # * 1.05*saturation, following E2V-style parallel overscan bleeds.
1525 levels = np.array([10.0, 575.0, 0.9*sat_level, 1.1*sat_level])
1527 for level in levels:
1528 mock_config.badParallelOverscanColumnLevel = level
1529 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1530 input_exp = mock.run()
1532 isr_task = IsrTaskLSST(config=isr_config)
1533 with self.assertNoLogs(level=logging.WARNING):
1534 result = isr_task.run(
1535 input_exp.clone(),
1536 crosstalk=self.crosstalk,
1537 ptc=self.ptc,
1538 linearizer=self.linearizer,
1539 deferredChargeCalib=self.cti,
1540 )
1542 for defect in self.defects:
1543 bbox = defect.getBBox()
1544 defect_image = result.exposure[bbox].image.array
1546 # Check that the defect is the correct level
1547 # (not subtracted away).
1548 defect_median = np.median(defect_image)
1549 self.assertFloatsAlmostEqual(defect_median, expected_defect_level, rtol=1e-4)
1551 # Check that the neighbors aren't over-subtracted.
1552 for neighbor in [-1, 1]:
1553 bbox_neighbor = bbox.shiftedBy(geom.Extent2I(neighbor, 0))
1554 neighbor_image = result.exposure[bbox_neighbor].image.array
1556 neighbor_median = np.median(neighbor_image)
1557 self.assertFloatsAlmostEqual(neighbor_median, 0.0, atol=7.0)
1559 def test_isrBadPtcGain(self):
1560 """Test processing when an amp has a bad (nan) PTC gain.
1561 """
1562 # We use a flat frame for this test for convenience.
1563 mock_config = self.get_mock_config_no_signal()
1564 mock_config.doAddDark = True
1565 mock_config.doAddFlat = True
1566 # The doAddSky option adds the equivalent of flat-field flux.
1567 mock_config.doAddSky = True
1569 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1570 input_exp = mock.run()
1572 isr_config = self.get_isr_config_electronic_corrections()
1573 isr_config.doBias = True
1574 isr_config.doDark = True
1575 isr_config.doFlat = False
1576 isr_config.doDefect = True
1578 # Set a bad amplifier to a nan gain.
1579 bad_amp = self.detector[0].getName()
1581 ptc = copy.copy(self.ptc)
1582 ptc.gain[bad_amp] = np.nan
1584 # We also want non-zero (but very small) crosstalk values
1585 # to ensure that these don't propagate nans.
1586 crosstalk = copy.copy(self.crosstalk)
1587 for i in range(len(self.detector)):
1588 for j in range(len(self.detector)):
1589 if i == j:
1590 continue
1591 if crosstalk.coeffs[i, j] == 0:
1592 crosstalk.coeffs[i, j] = 1e-10
1594 isr_task = IsrTaskLSST(config=isr_config)
1595 with self.assertLogs(level=logging.WARNING) as cm:
1596 result = isr_task.run(
1597 input_exp.clone(),
1598 bias=self.bias,
1599 dark=self.dark,
1600 crosstalk=crosstalk,
1601 ptc=ptc,
1602 linearizer=self.linearizer,
1603 defects=self.defects,
1604 deferredChargeCalib=self.cti,
1605 )
1606 self.assertIn(f"Amplifier {bad_amp} is bad (non-finite gain)", cm.output[0])
1608 # Confirm that the bad_amp is marked bad and the other amps are not.
1609 # We have to special case the amp with the defect.
1610 mask = result.exposure.mask
1612 for amp in self.detector:
1613 bbox = amp.getBBox()
1614 bad_in_amp = ((mask[bbox].array & 2**mask.getMaskPlaneDict()["BAD"]) > 0)
1616 if amp.getName() == bad_amp:
1617 self.assertTrue(np.all(bad_in_amp))
1618 elif amp.getName() == "C:0,2":
1619 # This is the amp with the defect.
1620 self.assertEqual(np.sum(bad_in_amp), 51)
1621 else:
1622 self.assertTrue(np.all(~bad_in_amp))
1624 def test_saturationModes(self):
1625 """Test the different saturation modes."""
1626 # Use a simple bias run for these.
1627 mock_config = self.get_mock_config_no_signal()
1629 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1630 input_exp = mock.run()
1632 isr_config = self.get_isr_config_electronic_corrections()
1633 isr_config.doSaturation = True
1634 isr_config.maskNegativeVariance = False
1635 detector_config = copy.copy(isr_config.overscanCamera.defaultDetectorConfig)
1636 amp_config = copy.copy(detector_config.defaultAmpConfig)
1638 for mode in ["NONE", "CAMERAMODEL", "PTCTURNOFF"]:
1639 isr_config.defaultSaturationSource = mode
1641 # Reset the PTC.
1642 ptc = copy.copy(self.ptc)
1643 # Reset the detector config.
1644 isr_config.overscanCamera.defaultDetectorConfig = detector_config
1645 if mode == "NONE":
1646 # We must use the config.
1647 sat_level = 1.2 * self.saturation_adu
1648 amp_config_new = copy.copy(amp_config)
1649 amp_config_new.saturation = sat_level
1650 detector_config_new = copy.copy(detector_config)
1651 detector_config_new.defaultAmpConfig = amp_config_new
1652 isr_config.overscanCamera.defaultDetectorConfig = detector_config_new
1653 elif mode == "CAMERAMODEL":
1654 sat_level = input_exp.getDetector()[0].getSaturation()
1655 elif mode == "PTCTURNOFF":
1656 sat_level = 1.3 * self.saturation_adu
1657 for amp_name in ptc.ampNames:
1658 ptc.ptcTurnoff[amp_name] = sat_level
1660 isr_task = IsrTaskLSST(config=isr_config)
1661 with self.assertNoLogs(level=logging.WARNING):
1662 result = isr_task.run(
1663 input_exp.clone(),
1664 bias=self.bias,
1665 crosstalk=self.crosstalk,
1666 ptc=self.ptc,
1667 linearizer=self.linearizer,
1668 deferredChargeCalib=self.cti,
1669 defects=self.defects,
1670 )
1672 metadata = result.exposure.metadata
1674 for amp in self.detector:
1675 amp_name = amp.getName()
1676 key = f"LSST ISR GAIN {amp_name}"
1677 self.assertIn(key, metadata, msg=mode)
1678 self.assertEqual(metadata[key], gain := self.ptc.gain[amp_name], msg=mode)
1679 key = f"LSST ISR SATURATION LEVEL {amp_name}"
1680 self.assertIn(key, metadata, msg=mode)
1681 self.assertEqual(metadata[key], sat_level * gain, msg=mode)
1683 def test_noPTC(self):
1684 """Test if we do not supply a PTC."""
1685 mock_config = self.get_mock_config_no_signal()
1687 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1688 input_exp = mock.run()
1690 isr_config = self.get_isr_config_minimal_corrections()
1691 isr_task = IsrTaskLSST(config=isr_config)
1693 with self.assertRaises(RuntimeError) as cm:
1694 _ = isr_task.run(input_exp.clone())
1695 self.assertIn("A PTC must be supplied", cm.exception.args[0])
1697 def test_suspectModes(self):
1698 """Test the different suspect modes."""
1699 # Use a simple bias run for these.
1700 mock_config = self.get_mock_config_no_signal()
1702 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1703 input_exp = mock.run()
1705 isr_config = self.get_isr_config_electronic_corrections()
1706 isr_config.doSaturation = True
1707 isr_config.maskNegativeVariance = False
1708 detector_config = copy.copy(isr_config.overscanCamera.defaultDetectorConfig)
1709 amp_config = copy.copy(detector_config.defaultAmpConfig)
1711 for mode in ["NONE", "CAMERAMODEL", "PTCTURNOFF"]:
1712 isr_config.defaultSuspectSource = mode
1714 # Reset the PTC.
1715 ptc = copy.copy(self.ptc)
1716 # Reset the detector config.
1717 isr_config.overscanCamera.defaultDetectorConfig = detector_config
1718 if mode == "NONE":
1719 # We must use the config.
1720 suspect_level = 1.2 * self.saturation_adu
1721 amp_config_new = copy.copy(amp_config)
1722 amp_config_new.suspectLevel = suspect_level
1723 detector_config_new = copy.copy(detector_config)
1724 detector_config_new.defaultAmpConfig = amp_config_new
1725 isr_config.overscanCamera.defaultDetectorConfig = detector_config_new
1726 elif mode == "CAMERAMODEL":
1727 suspect_level = input_exp.getDetector()[0].getSuspectLevel()
1728 elif mode == "PTCTURNOFF":
1729 suspect_level = 1.3 * self.saturation_adu
1730 for amp_name in ptc.ampNames:
1731 ptc.ptcTurnoff[amp_name] = suspect_level
1733 isr_task = IsrTaskLSST(config=isr_config)
1734 with self.assertNoLogs(level=logging.WARNING):
1735 result = isr_task.run(
1736 input_exp.clone(),
1737 bias=self.bias,
1738 crosstalk=self.crosstalk,
1739 ptc=self.ptc,
1740 linearizer=self.linearizer,
1741 deferredChargeCalib=self.cti,
1742 defects=self.defects,
1743 )
1745 metadata = result.exposure.metadata
1747 for amp in self.detector:
1748 amp_name = amp.getName()
1749 key = f"LSST ISR GAIN {amp_name}"
1750 self.assertIn(key, metadata, msg=mode)
1751 self.assertEqual(metadata[key], gain := self.ptc.gain[amp_name], msg=mode)
1752 key = f"LSST ISR SUSPECT LEVEL {amp_name}"
1753 self.assertIn(key, metadata, msg=mode)
1754 self.assertEqual(metadata[key], suspect_level * gain, msg=mode)
1756 def test_sequencerMismatches(self):
1757 """Test with a pile of sequencer mismatches."""
1758 mock_config = self.get_mock_config_no_signal()
1759 mock_config.doAddDark = True
1760 mock_config.doAddFlat = True
1761 # Set this to False until we have fringe correction.
1762 mock_config.doAddFringe = False
1763 mock_config.doAddSky = True
1764 mock_config.doAddSource = True
1766 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1767 input_exp = mock.run()
1768 input_exp.metadata["SEQFILE"] = "a_sequencer"
1770 isr_config = self.get_isr_config_electronic_corrections()
1771 isr_config.doBias = True
1772 isr_config.doDark = True
1773 isr_config.doFlat = True
1774 isr_config.cameraKeywordsToCompare = ["SEQFILE"]
1776 bias = self.bias.clone()
1777 bias.metadata["SEQFILE"] = "b_sequencer"
1778 dark = self.dark.clone()
1779 dark.metadata["SEQFILE"] = "b_sequencer"
1780 flat = self.flat.clone()
1781 flat.metadata["SEQFILE"] = "b_sequencer"
1782 crosstalk = copy.copy(self.crosstalk)
1783 crosstalk.metadata["SEQFILE"] = "b_sequencer"
1784 defects = copy.copy(self.defects)
1785 defects.metadata["SEQFILE"] = "b_sequencer"
1786 ptc = copy.copy(self.ptc)
1787 ptc.metadata["SEQFILE"] = "b_sequencer"
1788 linearizer = copy.copy(self.linearizer)
1789 linearizer.metadata["SEQFILE"] = "b_sequencer"
1790 cti = copy.copy(self.cti)
1791 cti.metadata["SEQFILE"] = "b_sequencer"
1793 isr_task = IsrTaskLSST(config=isr_config)
1794 with self.assertLogs(level=logging.WARNING):
1795 result = isr_task.run(
1796 input_exp.clone(),
1797 bias=bias,
1798 dark=dark,
1799 flat=flat,
1800 crosstalk=crosstalk,
1801 defects=defects,
1802 ptc=ptc,
1803 linearizer=linearizer,
1804 deferredChargeCalib=cti,
1805 )
1807 for ctype in ["BIAS", "DARK", "FLAT", "CROSSTALK", "DEFECTS", "PTC", "LINEARIZER", "CTI"]:
1808 self.assertTrue(result.exposure.metadata[f"ISR {ctype} SEQUENCER MISMATCH"])
1810 def test_highPtcNoiseAmps(self):
1811 """Test for masking of high noise amps (in PTC)."""
1812 # We use a flat frame for this test for convenience.
1813 mock_config = self.get_mock_config_no_signal()
1814 mock_config.doAddDark = True
1815 mock_config.doAddFlat = True
1816 # The doAddSky option adds the equivalent of flat-field flux.
1817 mock_config.doAddSky = True
1819 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1820 input_exp = mock.run()
1822 isr_config = self.get_isr_config_electronic_corrections()
1823 isr_config.doBias = True
1824 isr_config.doDark = True
1825 isr_config.doFlat = False
1826 isr_config.doDefect = True
1828 # Set a bad amplifier to a high noise.
1829 bad_amp = self.detector[0].getName()
1831 ptc = copy.copy(self.ptc)
1832 ptc.noise[bad_amp] = 50.0
1834 isr_task = IsrTaskLSST(config=isr_config)
1836 # With the PTC this should not warn.
1837 with self.assertNoLogs(level=logging.WARNING):
1838 result = isr_task.run(
1839 input_exp.clone(),
1840 bias=self.bias,
1841 dark=self.dark,
1842 crosstalk=self.crosstalk,
1843 ptc=ptc,
1844 linearizer=self.linearizer,
1845 defects=self.defects,
1846 deferredChargeCalib=self.cti,
1847 )
1849 # Confirm that the bad_amp is marked bad and the other amps are not.
1850 # We have to special case the amp with the defect.
1851 mask = result.exposure.mask
1853 for amp in self.detector:
1854 bbox = amp.getBBox()
1855 bad_in_amp = ((mask[bbox].array & 2**mask.getMaskPlaneDict()["BAD"]) > 0)
1857 if amp.getName() == bad_amp:
1858 self.assertTrue(np.all(bad_in_amp))
1859 elif amp.getName() == "C:0,2":
1860 # This is the amp with the defect.
1861 self.assertEqual(np.sum(bad_in_amp), 51)
1862 else:
1863 self.assertTrue(np.all(~bad_in_amp))
1865 def test_changedOverscanAmps(self):
1866 """Tests for masking of amps where the overscan level changed."""
1868 # We use a flat frame for this test for convenience.
1869 mock_config = self.get_mock_config_no_signal()
1870 mock_config.doAddDark = True
1871 mock_config.doAddFlat = True
1872 # The doAddSky option adds the equivalent of flat-field flux.
1873 mock_config.doAddSky = True
1875 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1876 input_exp = mock.run()
1878 # Offset one amp with a constant value.
1879 bad_amp = "C:0,0"
1881 input_exp2 = input_exp.clone()
1882 input_exp2.image[self.detector[bad_amp].getRawBBox()].array[:, :] -= 2000.0
1884 isr_config = self.get_isr_config_electronic_corrections()
1885 isr_config.doBias = True
1886 isr_config.doDark = True
1887 isr_config.doFlat = False
1888 isr_config.doDefect = True
1889 isr_config.doInterpolate = False
1890 isr_config.serialOverscanMedianShiftSigmaThreshold = 100.0
1892 isr_task = IsrTaskLSST(config=isr_config)
1893 with self.assertLogs(level=logging.WARNING) as cm:
1894 _ = isr_task.run(
1895 input_exp2,
1896 bias=self.bias,
1897 dark=self.dark,
1898 crosstalk=self.crosstalk,
1899 ptc=self.ptc,
1900 linearizer=self.linearizer,
1901 defects=self.defects,
1902 deferredChargeCalib=self.cti,
1903 )
1904 self.assertEqual(len(cm.output), 1)
1905 self.assertIn(f"Amplifier {bad_amp} has an overscan level", cm.output[0])
1907 # Offset all amps to see that it is now unprocessable.
1909 input_exp2 = input_exp.clone()
1910 input_exp2.image.array[:, :] -= 2000.0
1912 with self.assertLogs(level=logging.WARNING):
1913 with self.assertRaises(UnprocessableDataError):
1914 _ = isr_task.run(
1915 input_exp2,
1916 bias=self.bias,
1917 dark=self.dark,
1918 crosstalk=self.crosstalk,
1919 ptc=self.ptc,
1920 linearizer=self.linearizer,
1921 defects=self.defects,
1922 deferredChargeCalib=self.cti,
1923 )
1925 # Remove the values in the PTC and turn off check.
1926 # This should run without warnings.
1927 ptc = copy.copy(self.ptc)
1928 for amp in self.detector:
1929 ptc.overscanMedian[amp.getName()] = np.nan
1931 isr_config.serialOverscanMedianShiftSigmaThreshold = np.inf
1933 isr_task = IsrTaskLSST(config=isr_config)
1934 with self.assertNoLogs(level=logging.WARNING):
1935 _ = isr_task.run(
1936 input_exp.clone(),
1937 bias=self.bias,
1938 dark=self.dark,
1939 crosstalk=self.crosstalk,
1940 ptc=ptc,
1941 linearizer=self.linearizer,
1942 defects=self.defects,
1943 deferredChargeCalib=self.cti,
1944 )
1946 # Turn the check back on; this should have 1 warning.
1947 isr_config.serialOverscanMedianShiftSigmaThreshold = 100.0
1949 isr_task = IsrTaskLSST(config=isr_config)
1950 with self.assertLogs(level=logging.WARNING) as cm:
1951 _ = isr_task.run(
1952 input_exp.clone(),
1953 bias=self.bias,
1954 dark=self.dark,
1955 crosstalk=self.crosstalk,
1956 ptc=ptc,
1957 linearizer=self.linearizer,
1958 defects=self.defects,
1959 deferredChargeCalib=self.cti,
1960 )
1961 self.assertEqual(len(cm.output), 1)
1962 self.assertIn("No PTC overscan information", cm.output[0])
1964 def test_highOverscanNoiseAmps(self):
1965 """Test for masking of high noise amps (in overscan)."""
1967 # We use a flat frame for this test for convenience.
1968 mock_config = self.get_mock_config_no_signal()
1969 mock_config.doAddDark = True
1970 mock_config.doAddFlat = True
1971 # The doAddSky option adds the equivalent of flat-field flux.
1972 mock_config.doAddSky = True
1973 mock_config.readNoise = 50.0
1975 mock = isrMockLSST.IsrMockLSST(config=mock_config)
1976 input_exp = mock.run()
1978 isr_config = self.get_isr_config_electronic_corrections()
1979 isr_config.doBias = True
1980 isr_config.doDark = True
1981 isr_config.doFlat = False
1982 isr_config.doDefect = True
1983 isr_config.doInterpolate = False
1984 # Let all the amps fail to check the logging.
1985 isr_config.doCheckUnprocessableData = False
1987 isr_task = IsrTaskLSST(config=isr_config)
1988 with self.assertLogs(level=logging.WARNING) as cm:
1989 result = isr_task.run(
1990 input_exp.clone(),
1991 bias=self.bias,
1992 dark=self.dark,
1993 crosstalk=self.crosstalk,
1994 ptc=self.ptc,
1995 linearizer=self.linearizer,
1996 defects=self.defects,
1997 deferredChargeCalib=self.cti,
1998 )
1999 self.assertEqual(len(cm.output), len(self.detector))
2001 # All pixels should be BAD
2002 bad_value = result.exposure.mask.getPlaneBitMask("BAD")
2003 np.testing.assert_array_equal(result.exposure.mask.array & bad_value, bad_value)
2005 # And run again to check the UnprocessableDataError.
2006 isr_config.doCheckUnprocessableData = True
2007 isr_task = IsrTaskLSST(config=isr_config)
2009 with self.assertRaises(UnprocessableDataError):
2010 with self.assertLogs(level=logging.WARNING):
2011 result = isr_task.run(
2012 input_exp.clone(),
2013 bias=self.bias,
2014 dark=self.dark,
2015 crosstalk=self.crosstalk,
2016 ptc=self.ptc,
2017 linearizer=self.linearizer,
2018 defects=self.defects,
2019 deferredChargeCalib=self.cti,
2020 )
2022 def test_bssVoltageChecks(self):
2023 """Test the BSS voltage checks."""
2024 # We use a flat frame for this test for convenience.
2025 mock_config = self.get_mock_config_no_signal()
2026 mock_config.doAddDark = True
2027 mock_config.doAddFlat = True
2028 # The doAddSky option adds the equivalent of flat-field flux.
2029 mock_config.doAddSky = True
2031 mock = isrMockLSST.IsrMockLSST(config=mock_config)
2032 input_exp = mock.run()
2034 isr_config = self.get_isr_config_electronic_corrections()
2035 isr_config.doBias = True
2036 isr_config.doDark = True
2037 isr_config.doFlat = False
2038 isr_config.doDefect = True
2040 # Set the voltage.
2041 input_exp.metadata["BSSVBS"] = 0.25
2043 # Check that processing runs with checks turned off.
2044 isr_config.doCheckUnprocessableData = False
2046 isr_task = IsrTaskLSST(config=isr_config)
2047 with self.assertNoLogs(level=logging.WARNING):
2048 _ = isr_task.run(
2049 input_exp.clone(),
2050 bias=self.bias,
2051 dark=self.dark,
2052 crosstalk=self.crosstalk,
2053 ptc=self.ptc,
2054 linearizer=self.linearizer,
2055 defects=self.defects,
2056 deferredChargeCalib=self.cti,
2057 )
2059 # Check that processing runs with other way of turning checks off.
2060 isr_config.doCheckUnprocessableData = True
2061 isr_config.bssVoltageMinimum = 0.0
2063 isr_task = IsrTaskLSST(config=isr_config)
2064 with self.assertNoLogs(level=logging.WARNING):
2065 _ = isr_task.run(
2066 input_exp.clone(),
2067 bias=self.bias,
2068 dark=self.dark,
2069 crosstalk=self.crosstalk,
2070 ptc=self.ptc,
2071 linearizer=self.linearizer,
2072 defects=self.defects,
2073 deferredChargeCalib=self.cti,
2074 )
2076 # Check that processing runs but warns if header keyword is None.
2077 isr_config.doCheckUnprocessableData = True
2078 isr_config.bssVoltageMinimum = 10.0
2080 input_exp2 = input_exp.clone()
2081 input_exp2.metadata["BSSVBS"] = None
2083 isr_task = IsrTaskLSST(config=isr_config)
2084 with self.assertLogs(level=logging.WARNING) as cm:
2085 _ = isr_task.run(
2086 input_exp2,
2087 bias=self.bias,
2088 dark=self.dark,
2089 crosstalk=self.crosstalk,
2090 ptc=self.ptc,
2091 linearizer=self.linearizer,
2092 defects=self.defects,
2093 deferredChargeCalib=self.cti,
2094 )
2095 self.assertEqual(len(cm.output), 1)
2096 self.assertIn("Back-side bias voltage BSSVBS not found", cm.output[0])
2098 # Check that it raises.
2099 isr_config.doCheckUnprocessableData = True
2100 isr_config.bssVoltageMinimum = 10.0
2102 isr_task = IsrTaskLSST(config=isr_config)
2103 with self.assertRaises(UnprocessableDataError):
2104 _ = isr_task.run(
2105 input_exp.clone(),
2106 bias=self.bias,
2107 dark=self.dark,
2108 crosstalk=self.crosstalk,
2109 ptc=self.ptc,
2110 linearizer=self.linearizer,
2111 defects=self.defects,
2112 deferredChargeCalib=self.cti,
2113 )
2115 def test_overrideMaskBadAmp(self):
2116 """Test overriding config to mask an amp."""
2117 # We use a flat frame for this test for convenience.
2118 mock_config = self.get_mock_config_no_signal()
2119 mock_config.doAddDark = True
2120 mock_config.doAddFlat = True
2121 # The doAddSky option adds the equivalent of flat-field flux.
2122 mock_config.doAddSky = True
2124 mock = isrMockLSST.IsrMockLSST(config=mock_config)
2125 input_exp = mock.run()
2127 isr_config = self.get_isr_config_electronic_corrections()
2128 isr_config.doBias = True
2129 isr_config.doDark = True
2130 isr_config.doFlat = False
2131 isr_config.doDefect = True
2133 bad_amps = ["C:0,0", "C:0,1"]
2135 detector_name = self.detector.getName()
2136 isr_config.badAmps = [f"{detector_name}_{bad_amp}" for bad_amp in bad_amps]
2138 isr_task = IsrTaskLSST(config=isr_config)
2139 with self.assertNoLogs(level=logging.WARNING):
2140 result = isr_task.run(
2141 input_exp.clone(),
2142 bias=self.bias,
2143 dark=self.dark,
2144 crosstalk=self.crosstalk,
2145 ptc=self.ptc,
2146 linearizer=self.linearizer,
2147 defects=self.defects,
2148 deferredChargeCalib=self.cti,
2149 )
2151 for amp_name in bad_amps:
2152 mask_array = result.exposure[self.detector[amp_name].getBBox()].mask.array
2153 bad_mask = result.exposure.mask.getPlaneBitMask("BAD")
2154 self.assertTrue(np.all((mask_array & bad_mask) > 0))
2156 # Make sure it didn't obliterate everything.
2157 mask_array = result.exposure.mask.array
2158 self.assertFalse(np.all((mask_array & bad_mask) > 0))
2160 def test_hvBiasChecks(self):
2161 """Test the HVBIAS checks."""
2162 # We use a flat frame for this test for convenience.
2163 mock_config = self.get_mock_config_no_signal()
2164 mock_config.doAddDark = True
2165 mock_config.doAddFlat = True
2166 # The doAddSky option adds the equivalent of flat-field flux.
2167 mock_config.doAddSky = True
2169 mock = isrMockLSST.IsrMockLSST(config=mock_config)
2170 input_exp = mock.run()
2172 isr_config = self.get_isr_config_electronic_corrections()
2173 isr_config.doBias = True
2174 isr_config.doDark = True
2175 isr_config.doFlat = False
2176 isr_config.doDefect = True
2178 # Set the voltage.
2179 input_exp.metadata["HVBIAS"] = "OFF"
2181 # Check that processing runs with checks turned off.
2182 isr_config.doCheckUnprocessableData = False
2184 isr_task = IsrTaskLSST(config=isr_config)
2185 with self.assertNoLogs(level=logging.WARNING):
2186 _ = isr_task.run(
2187 input_exp.clone(),
2188 bias=self.bias,
2189 dark=self.dark,
2190 crosstalk=self.crosstalk,
2191 ptc=self.ptc,
2192 linearizer=self.linearizer,
2193 defects=self.defects,
2194 deferredChargeCalib=self.cti,
2195 )
2197 # Check that processing runs with other way of turning checks off.
2198 isr_config.doCheckUnprocessableData = True
2199 isr_config.bssVoltageMinimum = 0.0
2201 isr_task = IsrTaskLSST(config=isr_config)
2202 with self.assertNoLogs(level=logging.WARNING):
2203 _ = isr_task.run(
2204 input_exp.clone(),
2205 bias=self.bias,
2206 dark=self.dark,
2207 crosstalk=self.crosstalk,
2208 ptc=self.ptc,
2209 linearizer=self.linearizer,
2210 defects=self.defects,
2211 deferredChargeCalib=self.cti,
2212 )
2214 # Check that processing runs but warns if header keyword is None.
2215 isr_config.doCheckUnprocessableData = True
2216 isr_config.bssVoltageMinimum = 10.0
2218 input_exp2 = input_exp.clone()
2219 input_exp2.metadata["HVBIAS"] = None
2221 isr_task = IsrTaskLSST(config=isr_config)
2222 with self.assertLogs(level=logging.WARNING) as cm:
2223 _ = isr_task.run(
2224 input_exp2,
2225 bias=self.bias,
2226 dark=self.dark,
2227 crosstalk=self.crosstalk,
2228 ptc=self.ptc,
2229 linearizer=self.linearizer,
2230 defects=self.defects,
2231 deferredChargeCalib=self.cti,
2232 )
2233 self.assertEqual(len(cm.output), 1)
2234 self.assertIn("HV bias on HVBIAS not found in metadata", cm.output[0])
2236 # Check that it raises.
2237 isr_config.doCheckUnprocessableData = True
2238 isr_config.bssVoltageMinimum = 10.0
2240 isr_task = IsrTaskLSST(config=isr_config)
2241 with self.assertRaises(UnprocessableDataError):
2242 _ = isr_task.run(
2243 input_exp.clone(),
2244 bias=self.bias,
2245 dark=self.dark,
2246 crosstalk=self.crosstalk,
2247 ptc=self.ptc,
2248 linearizer=self.linearizer,
2249 defects=self.defects,
2250 deferredChargeCalib=self.cti,
2251 )
2253 def get_mock_config_no_signal(self):
2254 """Get an IsrMockLSSTConfig with all signal set to False.
2256 This will have all the electronic effects turned on (including
2257 2D bias).
2258 """
2259 mock_config = isrMockLSST.IsrMockLSSTConfig()
2260 mock_config.isTrimmed = False
2261 mock_config.doAddDark = False
2262 mock_config.doAddFlat = False
2263 mock_config.doAddFringe = False
2264 mock_config.doAddSky = False
2265 mock_config.doAddSource = False
2267 mock_config.doAdd2DBias = True
2268 mock_config.doAddBias = True
2269 mock_config.doAddCrosstalk = True
2270 mock_config.doAddDeferredCharge = True
2271 mock_config.doAddBrightDefects = True
2272 mock_config.doAddClockInjectedOffset = True
2273 mock_config.doAddParallelOverscanRamp = True
2274 mock_config.doAddSerialOverscanRamp = True
2275 mock_config.doAddHighSignalNonlinearity = True
2276 mock_config.doApplyGain = True
2277 mock_config.doRoundAdu = True
2279 # We always want to generate the image with these configs.
2280 mock_config.doGenerateImage = True
2282 return mock_config
2284 def get_mock_config_clean(self):
2285 """Get an IsrMockLSSTConfig trimmed with all electronic signatures
2286 turned off.
2287 """
2288 mock_config = isrMockLSST.IsrMockLSSTConfig()
2289 mock_config.doAddBias = False
2290 mock_config.doAdd2DBias = False
2291 mock_config.doAddClockInjectedOffset = False
2292 mock_config.doAddDark = False
2293 mock_config.doAddDarkNoiseOnly = False
2294 mock_config.doAddFlat = False
2295 mock_config.doAddFringe = False
2296 mock_config.doAddSky = False
2297 mock_config.doAddSource = False
2298 mock_config.doRoundAdu = False
2299 mock_config.doAddHighSignalNonlinearity = False
2300 mock_config.doApplyGain = False
2301 mock_config.doAddCrosstalk = False
2302 mock_config.doAddBrightDefects = False
2303 mock_config.doAddParallelOverscanRamp = False
2304 mock_config.doAddSerialOverscanRamp = False
2306 mock_config.isTrimmed = True
2307 mock_config.doGenerateImage = True
2309 return mock_config
2311 def get_isr_config_minimal_corrections(self):
2312 """Get an IsrTaskLSSTConfig with minimal corrections.
2313 """
2314 isr_config = IsrTaskLSSTConfig()
2315 isr_config.bssVoltageMinimum = 0.0
2316 isr_config.ampNoiseThreshold = np.inf
2317 isr_config.serialOverscanMedianShiftSigmaThreshold = np.inf
2318 isr_config.doBias = False
2319 isr_config.doDark = False
2320 isr_config.doDeferredCharge = False
2321 isr_config.doLinearize = False
2322 isr_config.doCorrectGains = False
2323 isr_config.doCrosstalk = False
2324 isr_config.doDefect = False
2325 isr_config.doBrighterFatter = False
2326 isr_config.doFlat = False
2327 isr_config.doSaturation = False
2328 isr_config.doE2VEdgeBleedMask = False
2329 isr_config.doITLEdgeBleedMask = False
2330 isr_config.doSuspect = False
2331 # We override the leading/trailing to skip here because of the limited
2332 # size of the test camera overscan regions.
2333 defaultAmpConfig = isr_config.overscanCamera.getOverscanDetectorConfig(self.detector).defaultAmpConfig
2334 defaultAmpConfig.doSerialOverscan = True
2335 defaultAmpConfig.serialOverscanConfig.leadingToSkip = 0
2336 defaultAmpConfig.serialOverscanConfig.trailingToSkip = 0
2337 defaultAmpConfig.doParallelOverscan = True
2338 defaultAmpConfig.parallelOverscanConfig.leadingToSkip = 0
2339 defaultAmpConfig.parallelOverscanConfig.trailingToSkip = 0
2340 # Our strong overscan slope in the tests requires an override.
2341 defaultAmpConfig.parallelOverscanConfig.maxDeviation = 300.0
2343 isr_config.doAssembleCcd = True
2344 isr_config.crosstalk.doSubtrahendMasking = True
2345 isr_config.crosstalk.minPixelToMask = 1.0
2347 return isr_config
2349 def get_isr_config_electronic_corrections(self):
2350 """Get an IsrTaskLSSTConfig with electronic corrections.
2352 This tests all the corrections that we support in the mocks/ISR.
2353 """
2354 isr_config = IsrTaskLSSTConfig()
2355 # We add these as appropriate in the tests.
2356 isr_config.doBias = False
2357 isr_config.doDark = False
2358 isr_config.doFlat = False
2360 # These are the electronic effects the tests support (in addition
2361 # to overscan).
2362 isr_config.doCrosstalk = True
2363 isr_config.doDefect = True
2364 isr_config.doLinearize = True
2365 isr_config.doDeferredCharge = True
2367 # This is False because it is only used in a single test case
2368 # as it takes a while to solve
2369 isr_config.doBrighterFatter = False
2371 # These are the electronic effects we do not support in tests yet.
2372 isr_config.doCorrectGains = False
2374 # We override the leading/trailing to skip here because of the limited
2375 # size of the test camera overscan regions.
2376 defaultAmpConfig = isr_config.overscanCamera.getOverscanDetectorConfig(self.detector).defaultAmpConfig
2377 defaultAmpConfig.doSerialOverscan = True
2378 defaultAmpConfig.serialOverscanConfig.leadingToSkip = 0
2379 defaultAmpConfig.serialOverscanConfig.trailingToSkip = 0
2380 defaultAmpConfig.doParallelOverscan = True
2381 defaultAmpConfig.parallelOverscanConfig.leadingToSkip = 0
2382 defaultAmpConfig.parallelOverscanConfig.trailingToSkip = 0
2383 # Our strong overscan slope in the tests requires an override.
2384 defaultAmpConfig.parallelOverscanConfig.maxDeviation = 300.0
2386 isr_config.doAssembleCcd = True
2387 isr_config.crosstalk.doSubtrahendMasking = True
2388 isr_config.crosstalk.minPixelToMask = 1.0
2390 return isr_config
2392 def get_non_defect_pixels(self, mask_origin):
2393 """Get the non-defect pixels to compare.
2395 Parameters
2396 ----------
2397 mask_origin : `lsst.afw.image.MaskX`
2398 The origin mask (for shape and type).
2400 Returns
2401 -------
2402 pix_x, pix_y : `tuple` [`np.ndarray`]
2403 x and y values of good pixels.
2404 """
2405 mask_temp = mask_origin.clone()
2406 mask_temp[:, :] = 0
2408 for defect in self.defects:
2409 mask_temp[defect.getBBox()] = 1
2411 return np.where(mask_temp.array == 0)
2413 def _check_bad_column_crosstalk_correction(
2414 self,
2415 exp,
2416 nsigma_cut=5.0,
2417 ):
2418 """Test bad column crosstalk correction.
2420 This includes possible provblems from parallel overscan
2421 crosstalk and gain mismatches.
2423 The target amp is self.detector[0], "C:0,0".
2425 Parameters
2426 ----------
2427 exp : `lsst.afw.image.Exposure`
2428 Input exposure.
2429 nsigma_cut : `float`, optional
2430 Number of sigma to check for outliers.
2431 """
2432 amp = self.detector[0]
2433 amp_image = exp[amp.getBBox()].image.array
2434 sigma = median_abs_deviation(amp_image.ravel(), scale="normal")
2436 med = np.median(amp_image.ravel())
2437 self.assertLess(amp_image.max(), med + nsigma_cut*sigma)
2438 self.assertGreater(amp_image.min(), med - nsigma_cut*sigma)
2441class MemoryTester(lsst.utils.tests.MemoryTestCase):
2442 pass
2445def setup_module(module):
2446 lsst.utils.tests.init()
2449if __name__ == "__main__": 2449 ↛ 2450line 2449 didn't jump to line 2450 because the condition on line 2449 was never true
2450 lsst.utils.tests.init()
2451 unittest.main()