LSST Applications g00d0e8bbd7+edbf708997,g03191d30f7+6b31559d11,g118115db7c+ac820e85d2,g199a45376c+5137f08352,g1fd858c14a+90100aa1a7,g262e1987ae+64df5f6984,g29ae962dfc+1eb4aece83,g2cef7863aa+73c82f25e4,g3541666cd7+1e37cdad5c,g35bb328faa+edbf708997,g3fd5ace14f+fb4e2866cc,g47891489e3+19fcc35de2,g53246c7159+edbf708997,g5b326b94bb+d622351b67,g64539dfbff+dfe1dff262,g67b6fd64d1+19fcc35de2,g74acd417e5+cfdc02aca8,g786e29fd12+af89c03590,g7aefaa3e3d+dc1a598170,g87389fa792+a4172ec7da,g88cb488625+60ba2c3075,g89139ef638+19fcc35de2,g8d4809ba88+dfe1dff262,g8d7436a09f+db94b797be,g8ea07a8fe4+79658f16ab,g90f42f885a+6577634e1f,g9722cb1a7f+d8f85438e7,g98df359435+7fdd888faa,ga2180abaac+edbf708997,ga9e74d7ce9+128cc68277,gbf99507273+edbf708997,gca7fc764a6+19fcc35de2,gd7ef33dd92+19fcc35de2,gdab6d2f7ff+cfdc02aca8,gdbb4c4dda9+dfe1dff262,ge410e46f29+19fcc35de2,ge41e95a9f2+dfe1dff262,geaed405ab2+062dfc8cdc,w.2025.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
isrTaskLSST.py
Go to the documentation of this file.
1__all__ = ["IsrTaskLSST", "IsrTaskLSSTConfig"]
2
3import numpy
4import math
5
6from . import isrFunctions
7from . import isrQa
8from .defects import Defects
9
10from contextlib import contextmanager
11from deprecated.sphinx import deprecated
12import lsst.pex.config as pexConfig
13import lsst.afw.math as afwMath
14import lsst.pipe.base as pipeBase
15import lsst.afw.image as afwImage
16import lsst.pipe.base.connectionTypes as cT
17from lsst.pipe.base import UnprocessableDataError
18from lsst.meas.algorithms.detection import SourceDetectionTask
19import lsst.afw.detection as afwDetection
20
21from .ampOffset import AmpOffsetTask
22from .binImageDataTask import BinImageDataTask
23from .overscan import SerialOverscanCorrectionTask, ParallelOverscanCorrectionTask
24from .overscanAmpConfig import OverscanCameraConfig
25from .assembleCcdTask import AssembleCcdTask
26from .deferredCharge import DeferredChargeTask
27from .crosstalk import CrosstalkTask
28from .masking import MaskingTask
29from .isrStatistics import IsrStatisticsTask
30from .isr import maskNans
31from .ptcDataset import PhotonTransferCurveDataset
32from .isrFunctions import isTrimmedExposure, compareCameraKeywords
33
34
35class IsrTaskLSSTConnections(pipeBase.PipelineTaskConnections,
36 dimensions={"instrument", "exposure", "detector"},
37 defaultTemplates={}):
38 ccdExposure = cT.Input(
39 name="raw",
40 doc="Input exposure to process.",
41 storageClass="Exposure",
42 dimensions=["instrument", "exposure", "detector"],
43 )
44 camera = cT.PrerequisiteInput(
45 name="camera",
46 storageClass="Camera",
47 doc="Input camera to construct complete exposures.",
48 dimensions=["instrument"],
49 isCalibration=True,
50 )
51 dnlLUT = cT.PrerequisiteInput(
52 name="dnlLUT",
53 doc="Look-up table for differential non-linearity.",
54 storageClass="IsrCalib",
55 dimensions=["instrument", "exposure", "detector"],
56 isCalibration=True,
57 # TODO DM 36636
58 )
59 bias = cT.PrerequisiteInput(
60 name="bias",
61 doc="Input bias calibration.",
62 storageClass="ExposureF",
63 dimensions=["instrument", "detector"],
64 isCalibration=True,
65 )
66 deferredChargeCalib = cT.PrerequisiteInput(
67 name="cti",
68 doc="Deferred charge/CTI correction dataset.",
69 storageClass="IsrCalib",
70 dimensions=["instrument", "detector"],
71 isCalibration=True,
72 )
73 linearizer = cT.PrerequisiteInput(
74 name='linearizer',
75 storageClass="Linearizer",
76 doc="Linearity correction calibration.",
77 dimensions=["instrument", "detector"],
78 isCalibration=True,
79 )
80 ptc = cT.PrerequisiteInput(
81 name="ptc",
82 doc="Input Photon Transfer Curve dataset",
83 storageClass="PhotonTransferCurveDataset",
84 dimensions=["instrument", "detector"],
85 isCalibration=True,
86 )
87 gainCorrection = cT.PrerequisiteInput(
88 name="gain_correction",
89 doc="Gain correction dataset",
90 storageClass="IsrCalib",
91 dimensions=["instrument", "detector"],
92 isCalibration=True,
93 minimum=0,
94 )
95 crosstalk = cT.PrerequisiteInput(
96 name="crosstalk",
97 doc="Input crosstalk object",
98 storageClass="CrosstalkCalib",
99 dimensions=["instrument", "detector"],
100 isCalibration=True,
101 )
102 defects = cT.PrerequisiteInput(
103 name='defects',
104 doc="Input defect tables.",
105 storageClass="Defects",
106 dimensions=["instrument", "detector"],
107 isCalibration=True,
108 )
109 bfKernel = cT.PrerequisiteInput(
110 name="bfk",
111 doc="Complete kernel + gain solutions.",
112 storageClass="BrighterFatterKernel",
113 dimensions=["instrument", "detector"],
114 isCalibration=True,
115 )
116 dark = cT.PrerequisiteInput(
117 name='dark',
118 doc="Input dark calibration.",
119 storageClass="ExposureF",
120 dimensions=["instrument", "detector"],
121 isCalibration=True,
122 )
123 flat = cT.PrerequisiteInput(
124 name="flat",
125 doc="Input flat calibration.",
126 storageClass="ExposureF",
127 dimensions=["instrument", "detector", "physical_filter"],
128 isCalibration=True,
129 )
130 outputExposure = cT.Output(
131 name='postISRCCD',
132 doc="Output ISR processed exposure.",
133 storageClass="Exposure",
134 dimensions=["instrument", "exposure", "detector"],
135 )
136 preInterpExposure = cT.Output(
137 name='preInterpISRCCD',
138 doc="Output ISR processed exposure, with pixels left uninterpolated.",
139 storageClass="ExposureF",
140 dimensions=["instrument", "exposure", "detector"],
141 )
142 outputBin1Exposure = cT.Output(
143 name="postIsrBin1",
144 doc="First binned image.",
145 storageClass="ExposureF",
146 dimensions=["instrument", "exposure", "detector"],
147 )
148 outputBin2Exposure = cT.Output(
149 name="postIsrBin2",
150 doc="Second binned image.",
151 storageClass="ExposureF",
152 dimensions=["instrument", "exposure", "detector"],
153 )
154
155 outputStatistics = cT.Output(
156 name="isrStatistics",
157 doc="Output of additional statistics table.",
158 storageClass="StructuredDataDict",
159 dimensions=["instrument", "exposure", "detector"],
160 )
161
162 def __init__(self, *, config=None):
163 super().__init__(config=config)
164
165 doApplyGains = config.doApplyGains
166 useLinearizerGains = config.useGainsFrom == "LINEARIZER"
167
168 if config.doBootstrap or (doApplyGains and useLinearizerGains):
169 del self.ptc
170 if not config.doCorrectGains:
171 del self.gainCorrection
172 if config.doDiffNonLinearCorrection is not True:
173 del self.dnlLUT
174 if config.doBias is not True:
175 del self.bias
176 if config.doDeferredCharge is not True:
177 del self.deferredChargeCalib
178 if (config.doLinearize or (doApplyGains and useLinearizerGains)) is not True:
179 del self.linearizer
180
181 if not config.doCrosstalk:
182 del self.crosstalk
183 if config.doDefect is not True:
184 del self.defects
185 if config.doBrighterFatter is not True:
186 del self.bfKernel
187 if config.doDark is not True:
188 del self.dark
189 if config.doFlat is not True:
190 del self.flat
191
192 if config.doBinnedExposures is not True:
193 del self.outputBin1Exposure
194 del self.outputBin2Exposure
195 if config.doSaveInterpPixels is not True:
196 del self.preInterpExposure
197
198 if config.doCalculateStatistics is not True:
199 del self.outputStatistics
200
201
202class IsrTaskLSSTConfig(pipeBase.PipelineTaskConfig,
203 pipelineConnections=IsrTaskLSSTConnections):
204 """Configuration parameters for IsrTaskLSST.
205
206 Items are grouped in the order in which they are executed by the task.
207 """
208 expectWcs = pexConfig.Field(
209 dtype=bool,
210 default=True,
211 doc="Expect input science images to have a WCS (set False for e.g. spectrographs)."
212 )
213 qa = pexConfig.ConfigField(
214 dtype=isrQa.IsrQaConfig,
215 doc="QA related configuration options.",
216 )
217 doHeaderProvenance = pexConfig.Field(
218 dtype=bool,
219 default=True,
220 doc="Write calibration identifiers into output exposure header.",
221 )
222
223 # Calib checking configuration:
224 doRaiseOnCalibMismatch = pexConfig.Field(
225 dtype=bool,
226 default=False,
227 doc="Should IsrTaskLSST halt if exposure and calibration header values do not match?",
228 )
229 cameraKeywordsToCompare = pexConfig.ListField(
230 dtype=str,
231 doc="List of header keywords to compare between exposure and calibrations.",
232 default=[],
233 )
234
235 # Differential non-linearity correction.
236 doDiffNonLinearCorrection = pexConfig.Field(
237 dtype=bool,
238 doc="Do differential non-linearity correction?",
239 default=False,
240 )
241
242 doBootstrap = pexConfig.Field(
243 dtype=bool,
244 default=False,
245 doc="Is this task to be run in a ``bootstrap`` fashion that does not require "
246 "a PTC or full calibrations?",
247 )
248
249 doCheckUnprocessableData = pexConfig.Field(
250 dtype=bool,
251 default=True,
252 doc="Check if this image is completely unprocessable due to all bad amps.",
253 )
254
255 overscanCamera = pexConfig.ConfigField(
256 dtype=OverscanCameraConfig,
257 doc="Per-detector and per-amplifier overscan configurations.",
258 )
259
260 serialOverscanMedianShiftSigmaThreshold = pexConfig.Field(
261 dtype=float,
262 default=numpy.inf,
263 doc="Number of sigma difference from per-amp overscan median (as compared to PTC) to "
264 "check if an amp is in a different state than the baseline PTC calib and should "
265 "be marked BAD. Set to np.inf/np.nan to turn off overscan median checking.",
266 )
267
268 ampNoiseThreshold = pexConfig.Field(
269 dtype=float,
270 default=25.0,
271 doc="Maximum amplifier noise (e-) that is allowed before an amp is masked as bad. "
272 "Set to np.inf/np.nan to turn off noise checking.",
273 )
274
275 bssVoltageMinimum = pexConfig.Field(
276 dtype=float,
277 default=5.0,
278 doc="Minimum back-side bias voltage. Below this the detector is ``off`` and an "
279 "UnprocessableDataError will be logged. Check will be skipped if doCheckUnprocessableData "
280 "is False or if value is less than or equal to 0.",
281 )
282 bssVoltageKeyword = pexConfig.Field(
283 dtype=str,
284 default="BSSVBS",
285 doc="Back-side bias voltage header keyword. Only checked if doCheckUnprocessableData is True "
286 "and bssVoltageMinimum is greater than 0.",
287 )
288 hvBiasKeyword = pexConfig.Field(
289 dtype=str,
290 default="HVBIAS",
291 doc="Back-side bias voltage on/off header keyword. Only checked if doCheckUnprocessableData is True "
292 "and bssVoltageMinimum is greater than 0.",
293 )
294
295 # Amplifier to CCD assembly configuration.
296 doAssembleCcd = pexConfig.Field(
297 dtype=bool,
298 default=True,
299 doc="Assemble amp-level exposures into a ccd-level exposure?"
300 )
301 assembleCcd = pexConfig.ConfigurableField(
302 target=AssembleCcdTask,
303 doc="CCD assembly task.",
304 )
305
306 # Bias subtraction.
307 doBias = pexConfig.Field(
308 dtype=bool,
309 doc="Apply bias frame correction?",
310 default=True,
311 )
312
313 # Deferred charge correction.
314 doDeferredCharge = pexConfig.Field(
315 dtype=bool,
316 doc="Apply deferred charge correction?",
317 default=True,
318 )
319 deferredChargeCorrection = pexConfig.ConfigurableField(
320 target=DeferredChargeTask,
321 doc="Deferred charge correction task.",
322 )
323
324 # Linearization.
325 doLinearize = pexConfig.Field(
326 dtype=bool,
327 doc="Correct for nonlinearity of the detector's response?",
328 default=True,
329 )
330
331 # Gains.
332 doCorrectGains = pexConfig.Field(
333 dtype=bool,
334 doc="Apply gain corrections from detector restarts?",
335 default=True,
336 )
337 doApplyGains = pexConfig.Field(
338 dtype=bool,
339 doc="Apply gains to the image?",
340 default=True,
341 )
342 useGainsFrom = pexConfig.ChoiceField(
343 dtype=str,
344 doc="Where to retrieve the gains. Unused if doBootstrap is True.",
345 allowed={
346 "PTC": "Use the gains from the inputPtc calibration.",
347 "LINEARIZER": "Use the gains from the linearizer calibration.",
348 },
349 default="PTC",
350 )
351
352 # Variance construction.
353 doVariance = pexConfig.Field(
354 dtype=bool,
355 doc="Calculate variance?",
356 default=True
357 )
358 maskNegativeVariance = pexConfig.Field(
359 dtype=bool,
360 doc="Mask pixels that claim a negative variance. This likely indicates a failure "
361 "in the measurement of the overscan at an edge due to the data falling off faster "
362 "than the overscan model can account for it.",
363 default=True,
364 )
365 negativeVarianceMaskName = pexConfig.Field(
366 dtype=str,
367 doc="Mask plane to use to mark pixels with negative variance, if `maskNegativeVariance` is True.",
368 default="BAD",
369 )
370 doSaturation = pexConfig.Field(
371 dtype=bool,
372 doc="Mask saturated pixels? NB: this is totally independent of the"
373 " interpolation option - this is ONLY setting the bits in the mask."
374 " To have them interpolated make sure doInterpolate=True and"
375 " maskListToInterpolate includes SAT.",
376 default=True,
377 )
378 saturatedMaskName = pexConfig.Field(
379 dtype=str,
380 doc="Name of mask plane to use in saturation detection and interpolation.",
381 default="SAT",
382 )
383 defaultSaturationSource = pexConfig.ChoiceField(
384 dtype=str,
385 doc="Source to retrieve default amp-level saturation values.",
386 allowed={
387 "NONE": "No default saturation values; only config overrides will be used.",
388 "CAMERAMODEL": "Use the default from the camera model (old defaults).",
389 "PTCTURNOFF": "Use the ptcTurnoff value as the saturation level.",
390 },
391 default="PTCTURNOFF",
392 )
393 doSuspect = pexConfig.Field(
394 dtype=bool,
395 doc="Mask suspect pixels?",
396 default=True,
397 )
398 suspectMaskName = pexConfig.Field(
399 dtype=str,
400 doc="Name of mask plane to use for suspect pixels.",
401 default="SUSPECT",
402 )
403 defaultSuspectSource = pexConfig.ChoiceField(
404 dtype=str,
405 doc="Source to retrieve default amp-level suspect values.",
406 allowed={
407 "NONE": "No default suspect values; only config overrides will be used.",
408 "CAMERAMODEL": "Use the default from the camera model (old defaults).",
409 "PTCTURNOFF": "Use the ptcTurnoff value as the suspect level.",
410 },
411 default="PTCTURNOFF",
412 )
413
414 # Crosstalk.
415 doCrosstalk = pexConfig.Field(
416 dtype=bool,
417 doc="Apply intra-CCD crosstalk correction?",
418 default=True,
419 )
420 crosstalk = pexConfig.ConfigurableField(
421 target=CrosstalkTask,
422 doc="Intra-CCD crosstalk correction.",
423 )
424
425 # Masking options.
426 doITLDipMask = pexConfig.Field(
427 dtype=bool,
428 doc="Apply ``ITL dip`` masking. The ``itlDipMaskPlane`` mask plane "
429 "will be added even if this configuration is False.",
430 default=True,
431 )
432 itlDipMaskPlanes = pexConfig.ListField(
433 dtype=str,
434 doc="Mask plane to use for ITL dip pixels.",
435 default=["SUSPECT", "ITL_DIP"],
436 )
437 doDefect = pexConfig.Field(
438 dtype=bool,
439 doc="Apply correction for CCD defects, e.g. hot pixels?",
440 default=True,
441 )
442 badAmps = pexConfig.ListField(
443 dtype=str,
444 doc="List of bad amps that should be masked as BAD in the defect code. "
445 "Value should be of form {detector_name}_{amp_name}, e.g. ``R42_S21_C07``. "
446 "Only used if doDefect is True.",
447 default=[],
448 )
449 doNanMasking = pexConfig.Field(
450 dtype=bool,
451 doc="Mask non-finite (NAN, inf) pixels. The UNMASKEDNAN mask plane "
452 "will be added even if this configuration is False.",
453 default=True,
454 )
455 doWidenSaturationTrails = pexConfig.Field(
456 dtype=bool,
457 doc="Widen bleed trails based on their width.",
458 default=False,
459 )
460 masking = pexConfig.ConfigurableField(
461 target=MaskingTask,
462 doc="Masking task."
463 )
464 doE2VEdgeBleedMask = pexConfig.Field(
465 dtype=bool,
466 doc="Mask flag-like edge bleeds from saturated columns "
467 "in E2V amplifiers.",
468 default=True,
469 )
470 e2vEdgeBleedSatMinArea = pexConfig.Field(
471 dtype=int,
472 doc="Minimum limit of saturated cores footprint area to apply edge"
473 "bleed masking in E2V amplifiers.",
474 default=10000,
475 )
476 e2vEdgeBleedSatMaxArea = pexConfig.Field(
477 dtype=int,
478 doc="Maximum limit of saturated cores footprint area to apply edge"
479 "bleed masking in E2V amplifiers.",
480 default=100000,
481 )
482 e2vEdgeBleedYMax = pexConfig.Field(
483 dtype=int,
484 doc="Height in pixels of edge bleed masking in E2V amplifiers (width"
485 "is the width of the amplifier).",
486 default=350,
487 )
488 doITLEdgeBleedMask = pexConfig.Field(
489 dtype=bool,
490 doc="Mask edge bleeds from saturated columns in ITL amplifiers.",
491 default=True,
492 )
493 doITLSatSagMask = pexConfig.Field(
494 dtype=bool,
495 doc="Mask columns presenting saturation sag.",
496 default=True,
497 )
498 itlEdgeBleedSatMinArea = pexConfig.Field(
499 dtype=int,
500 doc="Minimum limit for saturated cores footprint area.",
501 default=10000,
502 )
503 itlEdgeBleedSatMaxArea = pexConfig.Field(
504 dtype=int,
505 doc="Maximum limit for saturated cores footprint area.",
506 default=100000,
507 )
508 itlEdgeBleedThreshold = pexConfig.Field(
509 dtype=float,
510 doc="Sky background threshold for edge bleed detection.",
511 default=5000.,
512 )
513 itlEdgeBleedModelConstant = pexConfig.Field(
514 dtype=float,
515 doc="Constant in the edge bleed exponential decay model.",
516 default=0.02,
517 )
518
519 # Interpolation options.
520 doInterpolate = pexConfig.Field(
521 dtype=bool,
522 doc="Interpolate masked pixels?",
523 default=True,
524 )
525 maskListToInterpolate = pexConfig.ListField(
526 dtype=str,
527 doc="List of mask planes that should be interpolated.",
528 default=["SAT", "BAD", "UNMASKEDNAN"],
529 )
530 doSaveInterpPixels = pexConfig.Field(
531 dtype=bool,
532 doc="Save a copy of the pre-interpolated pixel values?",
533 default=False,
534 )
535 useLegacyInterp = pexConfig.Field(
536 dtype=bool,
537 doc="Use the legacy interpolation algorithm. If False use Gaussian Process.",
538 default=True,
539 )
540
541 # Amp offset correction.
542 doAmpOffset = pexConfig.Field(
543 doc="Calculate amp offset corrections?",
544 dtype=bool,
545 default=False,
546 )
547 ampOffset = pexConfig.ConfigurableField(
548 doc="Amp offset correction task.",
549 target=AmpOffsetTask,
550 )
551
552 # Initial masking options.
553 doSetBadRegions = pexConfig.Field(
554 dtype=bool,
555 doc="Should we set the level of all BAD patches of the chip to the chip's average value?",
556 default=True,
557 )
558
559 # Brighter-Fatter correction.
560 doBrighterFatter = pexConfig.Field(
561 dtype=bool,
562 doc="Apply the brighter-fatter correction?",
563 default=True,
564 )
565 brighterFatterLevel = pexConfig.ChoiceField(
566 dtype=str,
567 doc="The level at which to correct for brighter-fatter.",
568 allowed={
569 "AMP": "Every amplifier treated separately.",
570 "DETECTOR": "One kernel per detector.",
571 },
572 default="DETECTOR",
573 )
574 brighterFatterMaxIter = pexConfig.Field(
575 dtype=int,
576 doc="Maximum number of iterations for the brighter-fatter correction.",
577 default=10,
578 )
579 brighterFatterThreshold = pexConfig.Field(
580 dtype=float,
581 doc="Threshold used to stop iterating the brighter-fatter correction. It is the "
582 "absolute value of the difference between the current corrected image and the one "
583 "from the previous iteration summed over all the pixels.",
584 default=1000,
585 )
586 brighterFatterMaskListToInterpolate = pexConfig.ListField(
587 dtype=str,
588 doc="List of mask planes that should be interpolated over when applying the brighter-fatter "
589 "correction.",
590 default=["SAT", "BAD", "NO_DATA", "UNMASKEDNAN"],
591 )
592 brighterFatterMaskGrowSize = pexConfig.Field(
593 dtype=int,
594 doc="Number of pixels to grow the masks listed in config.brighterFatterMaskListToInterpolate "
595 "when brighter-fatter correction is applied.",
596 default=2,
597 )
598 brighterFatterFwhmForInterpolation = pexConfig.Field(
599 dtype=float,
600 doc="FWHM of PSF in arcseconds used for interpolation in brighter-fatter correction "
601 "(currently unused).",
602 default=1.0,
603 )
604 growSaturationFootprintSize = pexConfig.Field(
605 dtype=int,
606 doc="Number of pixels by which to grow the saturation footprints.",
607 default=1,
608 )
609
610 # Dark subtraction.
611 doDark = pexConfig.Field(
612 dtype=bool,
613 doc="Apply dark frame correction.",
614 default=True,
615 )
616
617 # Flat correction.
618 doFlat = pexConfig.Field(
619 dtype=bool,
620 doc="Apply flat field correction.",
621 default=True,
622 )
623 flatScalingType = pexConfig.ChoiceField(
624 dtype=str,
625 doc="The method for scaling the flat on the fly.",
626 default='USER',
627 allowed={
628 "USER": "Scale by flatUserScale",
629 "MEAN": "Scale by the inverse of the mean",
630 "MEDIAN": "Scale by the inverse of the median",
631 },
632 )
633 flatUserScale = pexConfig.Field(
634 dtype=float,
635 doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise.",
636 default=1.0,
637 )
638
639 # Calculate image quality statistics?
640 doStandardStatistics = pexConfig.Field(
641 dtype=bool,
642 doc="Should standard image quality statistics be calculated?",
643 default=True,
644 )
645 # Calculate additional statistics?
646 doCalculateStatistics = pexConfig.Field(
647 dtype=bool,
648 doc="Should additional ISR statistics be calculated?",
649 default=True,
650 )
651 isrStats = pexConfig.ConfigurableField(
652 target=IsrStatisticsTask,
653 doc="Task to calculate additional statistics.",
654 )
655
656 # Make binned images?
657 doBinnedExposures = pexConfig.Field(
658 dtype=bool,
659 doc="Should binned exposures be calculated?",
660 default=False,
661 )
662 binning = pexConfig.ConfigurableField(
663 target=BinImageDataTask,
664 doc="Task to bin the exposure.",
665 )
666 binFactor1 = pexConfig.Field(
667 dtype=int,
668 doc="Binning factor for first binned exposure. This is intended for a finely binned output.",
669 default=8,
670 check=lambda x: x > 1,
671 )
672 binFactor2 = pexConfig.Field(
673 dtype=int,
674 doc="Binning factor for second binned exposure. This is intended for a coarsely binned output.",
675 default=64,
676 check=lambda x: x > 1,
677 )
678
679 def validate(self):
680 super().validate()
681
682 if self.doBootstrap:
683 # Additional checks in bootstrap (no gains) mode.
684 if self.doApplyGains:
685 raise ValueError("Cannot run task with doBootstrap=True and doApplyGains=True.")
686 if self.doCorrectGains:
687 raise ValueError("Cannot run task with doBootstrap=True and doCorrectGains=True.")
688 if self.doCrosstalk and self.crosstalk.doQuadraticCrosstalkCorrection:
689 raise ValueError("Cannot apply quadratic crosstalk correction with doBootstrap=True.")
690 if numpy.isfinite(self.serialOverscanMedianShiftSigmaThreshold):
691 raise ValueError("Cannot do amp overscan level checks with doBootstrap=True.")
692 if numpy.isfinite(self.ampNoiseThreshold):
693 raise ValueError("Cannot do amp noise thresholds with doBootstrap=True.")
694
695 if self.doITLEdgeBleedMask and not self.doSaturation:
696 raise ValueError("Cannot do ITL edge bleed masking when doSaturation=False.")
697 if self.doE2VEdgeBleedMask and not self.doSaturation:
698 raise ValueError("Cannot do e2v edge bleed masking when doSaturation=False.")
699
700 def setDefaults(self):
701 super().setDefaults()
702
703
704class IsrTaskLSST(pipeBase.PipelineTask):
705 ConfigClass = IsrTaskLSSTConfig
706 _DefaultName = "isrLSST"
707
708 def __init__(self, **kwargs):
709 super().__init__(**kwargs)
710 self.makeSubtask("assembleCcd")
711 self.makeSubtask("deferredChargeCorrection")
712 self.makeSubtask("crosstalk")
713 self.makeSubtask("masking")
714 self.makeSubtask("isrStats")
715 self.makeSubtask("ampOffset")
716 self.makeSubtask("binning")
717
718 def runQuantum(self, butlerQC, inputRefs, outputRefs):
719
720 inputs = butlerQC.get(inputRefs)
721 self.validateInput(inputs)
722
723 if self.config.doHeaderProvenance:
724 # Add calibration provenanace info to header.
725 exposureMetadata = inputs['ccdExposure'].metadata
726 for inputName in sorted(list(inputs.keys())):
727 reference = getattr(inputRefs, inputName, None)
728 if reference is not None and hasattr(reference, "run"):
729 runKey = f"LSST CALIB RUN {inputName.upper()}"
730 runValue = reference.run
731 idKey = f"LSST CALIB UUID {inputName.upper()}"
732 idValue = str(reference.id)
733 dateKey = f"LSST CALIB DATE {inputName.upper()}"
734 dateValue = self.extractCalibDate(inputs[inputName])
735 if dateValue != "Unknown Unknown":
736 butlerQC.add_additional_provenance(reference, {"calib date": dateValue})
737
738 exposureMetadata[runKey] = runValue
739 exposureMetadata[idKey] = idValue
740 exposureMetadata[dateKey] = dateValue
741
742 outputs = self.run(**inputs)
743 butlerQC.put(outputs, outputRefs)
744
745 def validateInput(self, inputs):
746 """
747 This is a check that all the inputs required by the config
748 are available.
749 """
750
751 doApplyGains = self.config.doApplyGains
752 useLinearizerGains = self.config.useGainsFrom == "LINEARIZER"
753 usePtcGains = not useLinearizerGains
754
755 inputMap = {
756 'dnlLUT': self.config.doDiffNonLinearCorrection,
757 'bias': self.config.doBias,
758 'deferredChargeCalib': self.config.doDeferredCharge,
759 # Some tasks require gains in order to be
760 # supplied regardless of whether
761 # self.config.doApplyGains is True or False.
762 'linearizer': (self.config.doLinearize or (doApplyGains and useLinearizerGains)),
763 'ptc': self.config.doApplyGains and usePtcGains,
764 'crosstalk': self.config.doCrosstalk,
765 'defects': self.config.doDefect,
766 'bfKernel': self.config.doBrighterFatter,
767 'dark': self.config.doDark,
768 }
769
770 for calibrationFile, configValue in inputMap.items():
771 if configValue and inputs[calibrationFile] is None:
772 raise RuntimeError("Must supply ", calibrationFile)
773
774 def diffNonLinearCorrection(self, ccdExposure, dnlLUT, **kwargs):
775 # TODO DM 36636
776 # isrFunctions.diffNonLinearCorrection
777 pass
778
779 def maskFullAmplifiers(self, ccdExposure, detector, defects, gains=None):
780 """
781 Check for fully masked bad amplifiers and mask them.
782
783 This includes defects which cover full amplifiers, as well
784 as amplifiers with nan gain values which should be used
785 if self.config.doApplyGains=True.
786
787 Full defect masking happens later to allow for defects which
788 cross amplifier boundaries.
789
790 Parameters
791 ----------
792 ccdExposure : `lsst.afw.image.Exposure`
793 Input exposure to be masked.
794 detector : `lsst.afw.cameraGeom.Detector`
795 Detector object.
796 defects : `lsst.ip.isr.Defects`
797 List of defects. Used to determine if an entire
798 amplifier is bad.
799 gains : `dict` [`str`, `float`], optional
800 Dictionary of gains to check if
801 self.config.doApplyGains=True.
802
803 Returns
804 -------
805 badAmpDict : `str`[`bool`]
806 Dictionary of amplifiers, keyed by name, value is True if
807 amplifier is fully masked.
808 """
809 badAmpDict = {}
810
811 maskedImage = ccdExposure.getMaskedImage()
812
813 for amp in detector:
814 ampName = amp.getName()
815 badAmpDict[ampName] = False
816
817 # Check if entire amp region is defined as a defect
818 # NB: need to use amp.getBBox() for correct comparison with current
819 # defects definition.
820 if defects is not None:
821 badAmpDict[ampName] = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects]))
822
823 if badAmpDict[ampName]:
824 self.log.warning("Amplifier %s is bad (completely covered with defects)", ampName)
825
826 if gains is not None and self.config.doApplyGains:
827 if not math.isfinite(gains[ampName]):
828 badAmpDict[ampName] = True
829
830 self.log.warning("Amplifier %s is bad (non-finite gain)", ampName)
831
832 # In the case of a bad amp, we will set mask to "BAD"
833 # (here use amp.getRawBBox() for correct association with pixels in
834 # current ccdExposure).
835 if badAmpDict[ampName]:
836 dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
837 afwImage.PARENT)
838 maskView = dataView.getMask()
839 maskView |= maskView.getPlaneBitMask("BAD")
840 del maskView
841
842 return badAmpDict
843
844 def checkAllBadAmps(self, badAmpDict, detector):
845 """Check if all amps are marked as bad.
846
847 Parameters
848 ----------
849 badAmpDict : `str`[`bool`]
850 Dictionary of amplifiers, keyed by name, value is True if
851 amplifier is fully masked.
852 detector : `lsst.afw.cameraGeom.Detector`
853 Detector object.
854
855 Raises
856 ------
857 UnprocessableDataError if all amps are bad and doCheckUnprocessableData
858 configuration is True.
859 """
860 if not self.config.doCheckUnprocessableData:
861 return
862
863 for amp in detector:
864 if not badAmpDict.get(amp.getName(), False):
865 return
866
867 raise UnprocessableDataError(f"All amps in the exposure {detector.getName()} are bad; skipping ISR.")
868
869 def checkAmpOverscanLevel(self, badAmpDict, exposure, ptc):
870 """Check if the amplifier overscan levels have changed.
871
872 Any amplifier that has an overscan median level that has changed
873 significantly will be masked as BAD and added to toe badAmpDict.
874
875 Parameters
876 ----------
877 badAmpDict : `str` [`bool`]
878 Dictionary of amplifiers, keyed by name, value is True if
879 amplifier is fully masked.
880 exposure : `lsst.afw.image.Exposure`
881 Input exposure to be masked (untrimmed).
882 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`
883 PTC dataset with gains/read noises.
884
885 Returns
886 -------
887 badAmpDict : `str`[`bool`]
888 Dictionary of amplifiers, keyed by name.
889
890 """
891 if isTrimmedExposure(exposure):
892 raise RuntimeError("checkAmpOverscanLevel must be run on an untrimmed exposure.")
893
894 # We want to consolidate all the amps into one warning if necessary.
895 # This config should not be set to a finite threshold if the necessary
896 # data is not in the PTC.
897 missingWarnString = "No PTC overscan information for amplifier "
898 missingWarnFlag = False
899 for amp in exposure.getDetector():
900 ampName = amp.getName()
901
902 if not numpy.isfinite(ptc.overscanMedian[ampName]) or \
903 not numpy.isfinite(ptc.overscanMedianSigma[ampName]):
904 missingWarnString += f"{ampName},"
905 missingWarnFlag = True
906 else:
907 key = f"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
908 # If it is missing, just return the PTC value and it
909 # will be skipped.
910 overscanLevel = exposure.metadata.get(key, ptc.overscanMedian[ampName])
911 pull = (overscanLevel - ptc.overscanMedian[ampName])/ptc.overscanMedianSigma[ampName]
912 if numpy.abs(pull) > self.config.serialOverscanMedianShiftSigmaThreshold:
913 self.log.warning(
914 "Amplifier %s has an overscan level that is %.2f sigma from the expected level; "
915 "masking it as BAD.",
916 ampName,
917 pull,
918 )
919
920 badAmpDict[ampName] = True
921 exposure.mask[amp.getRawBBox()] |= exposure.mask.getPlaneBitMask("BAD")
922
923 if missingWarnFlag:
924 self.log.warning(missingWarnString)
925
926 return badAmpDict
927
928 def checkAmpNoise(self, badAmpDict, exposure, ptc):
929 """Check if amplifier noise levels are above threshold.
930
931 Any amplifier that is above the noise level will be masked as BAD
932 and added to the badAmpDict.
933
934 Parameters
935 ----------
936 badAmpDict : `str` [`bool`]
937 Dictionary of amplifiers, keyed by name, value is True if
938 amplifier is fully masked.
939 exposure : `lsst.afw.image.Exposure`
940 Input exposure to be masked (untrimmed).
941 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`
942 PTC dataset with gains/read noises.
943
944 Returns
945 -------
946 badAmpDict : `str`[`bool`]
947 Dictionary of amplifiers, keyed by name.
948 """
949
950 if isTrimmedExposure(exposure):
951 raise RuntimeError("checkAmpNoise must be run on an untrimmed exposure.")
952
953 for amp in exposure.getDetector():
954 ampName = amp.getName()
955
956 doMask = False
957 if ptc.noise[ampName] > self.config.ampNoiseThreshold:
958 self.log.info(
959 "Amplifier %s has a PTC noise level of %.2f e-, above threshold.",
960 ampName,
961 ptc.noise[ampName],
962 )
963 doMask = True
964 else:
965 key = f"LSST ISR OVERSCAN RESIDUAL SERIAL STDEV {ampName}"
966 overscanNoise = exposure.metadata.get(key, numpy.nan)
967 if overscanNoise * ptc.gain[ampName] > self.config.ampNoiseThreshold:
968 self.log.warning(
969 "Amplifier %s has an overscan read noise level of %.2f e-, above threshold.",
970 ampName,
971 overscanNoise * ptc.gain[ampName],
972 )
973 doMask = True
974
975 if doMask:
976 badAmpDict[ampName] = True
977
978 exposure.mask[amp.getRawBBox()] |= exposure.mask.getPlaneBitMask("BAD")
979
980 return badAmpDict
981
982 def maskSaturatedPixels(self, badAmpDict, ccdExposure, detector, detectorConfig, ptc=None):
983 """
984 Mask SATURATED and SUSPECT pixels and check if any amplifiers
985 are fully masked.
986
987 Parameters
988 ----------
989 badAmpDict : `str` [`bool`]
990 Dictionary of amplifiers, keyed by name, value is True if
991 amplifier is fully masked.
992 ccdExposure : `lsst.afw.image.Exposure`
993 Input exposure to be masked.
994 detector : `lsst.afw.cameraGeom.Detector`
995 Detector object.
996 defects : `lsst.ip.isr.Defects`
997 List of defects. Used to determine if an entire
998 amplifier is bad.
999 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
1000 Per-amplifier configurations.
1001 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
1002 PTC dataset (used if configured to use PTCTURNOFF).
1003
1004 Returns
1005 -------
1006 badAmpDict : `str`[`bool`]
1007 Dictionary of amplifiers, keyed by name.
1008 """
1009 maskedImage = ccdExposure.getMaskedImage()
1010
1011 metadata = ccdExposure.metadata
1012
1013 if self.config.doSaturation and self.config.defaultSaturationSource == "PTCTURNOFF" and ptc is None:
1014 raise RuntimeError("Must provide ptc if using PTCTURNOFF as saturation source.")
1015 if self.config.doSuspect and self.config.defaultSuspectSource == "PTCTURNOFF" and ptc is None:
1016 raise RuntimeError("Must provide ptc if using PTCTURNOFF as suspect source.")
1017
1018 for amp in detector:
1019 ampName = amp.getName()
1020
1021 ampConfig = detectorConfig.getOverscanAmpConfig(amp)
1022
1023 if badAmpDict[ampName]:
1024 # No need to check fully bad amplifiers.
1025 continue
1026
1027 # Mask saturated and suspect pixels.
1028 limits = {}
1029 if self.config.doSaturation:
1030 if self.config.defaultSaturationSource == "PTCTURNOFF":
1031 limits.update({self.config.saturatedMaskName: ptc.ptcTurnoff[amp.getName()]})
1032 elif self.config.defaultSaturationSource == "CAMERAMODEL":
1033 # Set to the default from the camera model.
1034 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
1035 elif self.config.defaultSaturationSource == "NONE":
1036 limits.update({self.config.saturatedMaskName: numpy.inf})
1037
1038 # And update if it is set in the config.
1039 if math.isfinite(ampConfig.saturation):
1040 limits.update({self.config.saturatedMaskName: ampConfig.saturation})
1041 metadata[f"LSST ISR SATURATION LEVEL {ampName}"] = limits[self.config.saturatedMaskName]
1042
1043 if self.config.doSuspect:
1044 if self.config.defaultSuspectSource == "PTCTURNOFF":
1045 limits.update({self.config.suspectMaskName: ptc.ptcTurnoff[amp.getName()]})
1046 elif self.config.defaultSuspectSource == "CAMERAMODEL":
1047 # Set to the default from the camera model.
1048 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
1049 elif self.config.defaultSuspectSource == "NONE":
1050 limits.update({self.config.suspectMaskName: numpy.inf})
1051
1052 # And update if it set in the config.
1053 if math.isfinite(ampConfig.suspectLevel):
1054 limits.update({self.config.suspectMaskName: ampConfig.suspectLevel})
1055 metadata[f"LSST ISR SUSPECT LEVEL {ampName}"] = limits[self.config.suspectMaskName]
1056
1057 for maskName, maskThreshold in limits.items():
1058 if not math.isnan(maskThreshold):
1059 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1060 toMask = (dataView.image.array >= maskThreshold)
1061 dataView.mask.array[toMask] |= dataView.mask.getPlaneBitMask(maskName)
1062
1063 # Determine if we've fully masked this amplifier with SUSPECT and
1064 # SAT pixels.
1065 maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
1066 afwImage.PARENT)
1067 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
1068 self.config.suspectMaskName])
1069 if numpy.all(maskView.getArray() & maskVal > 0):
1070 self.log.warning("Amplifier %s is bad (completely SATURATED or SUSPECT)", ampName)
1071 badAmpDict[ampName] = True
1072 maskView |= maskView.getPlaneBitMask("BAD")
1073
1074 return badAmpDict
1075
1076 def maskITLSatEdgesAndColumns(self, exposure, badAmpDict):
1077
1078 # The following steps will rely on the footprint of saturated
1079 # cores with large areas.
1080 thresh = afwDetection.Threshold(exposure.mask.getPlaneBitMask("SAT"),
1081 afwDetection.Threshold.BITMASK
1082 )
1083 fpList = afwDetection.FootprintSet(exposure.mask, thresh).getFootprints()
1084
1085 satAreas = numpy.asarray([fp.getArea() for fp in fpList])
1086 largeAreas, = numpy.where((satAreas >= self.config.itlEdgeBleedSatMinArea)
1087 & (satAreas < self.config.itlEdgeBleedSatMaxArea))
1088
1089 for largeAreasIndex in largeAreas:
1090
1091 fpCore = fpList[largeAreasIndex]
1092
1093 # Edge bleed masking
1094 if self.config.doITLEdgeBleedMask:
1095 isrFunctions.maskITLEdgeBleed(ccdExposure=exposure,
1096 badAmpDict=badAmpDict,
1097 fpCore=fpCore,
1098 itlEdgeBleedSatMinArea=self.config.itlEdgeBleedSatMinArea,
1099 itlEdgeBleedSatMaxArea=self.config.itlEdgeBleedSatMaxArea,
1100 itlEdgeBleedThreshold=self.config.itlEdgeBleedThreshold,
1101 itlEdgeBleedModelConstant=self.config.itlEdgeBleedModelConstant,
1102 saturatedMaskName=self.config.saturatedMaskName,
1103 log=self.log
1104 )
1105 if self.config.doITLSatSagMask:
1106 isrFunctions.maskITLSatSag(ccdExposure=exposure, fpCore=fpCore,
1107 saturatedMaskName=self.config.saturatedMaskName)
1108
1109 def overscanCorrection(self, mode, detectorConfig, detector, badAmpDict, ccdExposure):
1110 """Apply serial overscan correction in place to all amps.
1111
1112 The actual overscan subtraction is performed by the
1113 `lsst.ip.isr.overscan.OverscanTask`, which is called here.
1114
1115 Parameters
1116 ----------
1117 mode : `str`
1118 Must be `SERIAL` or `PARALLEL`.
1119 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
1120 Per-amplifier configurations.
1121 detector : `lsst.afw.cameraGeom.Detector`
1122 Detector object.
1123 badAmpDict : `dict`
1124 Dictionary of amp name to whether it is a bad amp.
1125 ccdExposure : `lsst.afw.image.Exposure`
1126 Exposure to have overscan correction performed.
1127
1128 Returns
1129 -------
1130 overscans : `list` [`lsst.pipe.base.Struct` or None]
1131 Overscan measurements (always in adu).
1132 Each result struct has components:
1133
1134 ``imageFit``
1135 Value or fit subtracted from the amplifier image data.
1136 (scalar or `lsst.afw.image.Image`)
1137 ``overscanFit``
1138 Value or fit subtracted from the overscan image data.
1139 (scalar or `lsst.afw.image.Image`)
1140 ``overscanImage``
1141 Image of the overscan region with the overscan
1142 correction applied. This quantity is used to estimate
1143 the amplifier read noise empirically.
1144 (`lsst.afw.image.Image`)
1145 ``overscanMean``
1146 Mean overscan fit value. (`float`)
1147 ``overscanMedian``
1148 Median overscan fit value. (`float`)
1149 ``overscanSigma``
1150 Clipped standard deviation of the overscan fit. (`float`)
1151 ``residualMean``
1152 Mean of the overscan after fit subtraction. (`float`)
1153 ``residualMedian``
1154 Median of the overscan after fit subtraction. (`float`)
1155 ``residualSigma``
1156 Clipped standard deviation of the overscan after fit
1157 subtraction. (`float`)
1158
1159 See Also
1160 --------
1161 lsst.ip.isr.overscan.OverscanTask
1162 """
1163 if mode not in ["SERIAL", "PARALLEL"]:
1164 raise ValueError("Mode must be SERIAL or PARALLEL")
1165
1166 # This returns a list in amp order, with None for uncorrected amps.
1167 overscans = []
1168
1169 for i, amp in enumerate(detector):
1170 ampName = amp.getName()
1171
1172 ampConfig = detectorConfig.getOverscanAmpConfig(amp)
1173
1174 if mode == "SERIAL" and not ampConfig.doSerialOverscan:
1175 self.log.debug(
1176 "ISR_OSCAN: Amplifier %s/%s configured to skip serial overscan.",
1177 detector.getName(),
1178 ampName,
1179 )
1180 results = None
1181 elif mode == "PARALLEL" and not ampConfig.doParallelOverscan:
1182 self.log.debug(
1183 "ISR_OSCAN: Amplifier %s configured to skip parallel overscan.",
1184 detector.getName(),
1185 ampName,
1186 )
1187 results = None
1188 elif badAmpDict[ampName] or not ccdExposure.getBBox().contains(amp.getBBox()):
1189 results = None
1190 else:
1191 # This check is to confirm that we are not trying to run
1192 # overscan on an already trimmed image.
1193 if isTrimmedExposure(ccdExposure):
1194 self.log.warning(
1195 "ISR_OSCAN: No overscan region for amp %s. Not performing overscan correction.",
1196 ampName,
1197 )
1198 results = None
1199 else:
1200 if mode == "SERIAL":
1201 # We need to set up the subtask here with a custom
1202 # configuration.
1203 serialOverscan = SerialOverscanCorrectionTask(config=ampConfig.serialOverscanConfig)
1204 results = serialOverscan.run(ccdExposure, amp)
1205 else:
1206 config = ampConfig.parallelOverscanConfig
1207 parallelOverscan = ParallelOverscanCorrectionTask(
1208 config=config,
1209 )
1210
1211 metadata = ccdExposure.metadata
1212
1213 # We need to know the saturation level that was used
1214 # for the parallel overscan masking. If it isn't set
1215 # then the configured parallelOverscanSaturationLevel
1216 # will be used instead (assuming
1217 # doParallelOverscanSaturation is True). Note that
1218 # this will have the correct units (adu or electron)
1219 # depending on whether the gain has been applied.
1220 if self.config.doSaturation:
1221 saturationLevel = metadata[f"LSST ISR SATURATION LEVEL {amp.getName()}"]
1222 saturationLevel *= config.parallelOverscanSaturationLevelAdjustmentFactor
1223 else:
1224 saturationLevel = config.parallelOverscanSaturationLevel
1225 if ccdExposure.metadata["LSST ISR UNITS"] == "electron":
1226 # Need to convert to electron from adu.
1227 saturationLevel *= metadata[f"LSST ISR GAIN {amp.getName()}"]
1228
1229 self.log.debug(
1230 "Using saturation level of %.2f for parallel overscan amp %s",
1231 saturationLevel,
1232 amp.getName(),
1233 )
1234
1235 parallelOverscan.maskParallelOverscanAmp(
1236 ccdExposure,
1237 amp,
1238 saturationLevel=saturationLevel,
1239 )
1240
1241 results = parallelOverscan.run(ccdExposure, amp)
1242
1243 metadata = ccdExposure.metadata
1244 keyBase = "LSST ISR OVERSCAN"
1245
1246 # The overscan is always in adu for the serial mode,
1247 # but, it may be electron in the parallel mode if
1248 # doApplyGains==True. If doApplyGains==True, then the
1249 # gains are applied to the untrimmed image, so the
1250 # overscan statistics units here will always match the
1251 # units of the image at this point.
1252 metadata[f"{keyBase} {mode} UNITS"] = ccdExposure.metadata["LSST ISR UNITS"]
1253 metadata[f"{keyBase} {mode} MEAN {ampName}"] = results.overscanMean
1254 metadata[f"{keyBase} {mode} MEDIAN {ampName}"] = results.overscanMedian
1255 metadata[f"{keyBase} {mode} STDEV {ampName}"] = results.overscanSigma
1256
1257 metadata[f"{keyBase} RESIDUAL {mode} MEAN {ampName}"] = results.residualMean
1258 metadata[f"{keyBase} RESIDUAL {mode} MEDIAN {ampName}"] = results.residualMedian
1259 metadata[f"{keyBase} RESIDUAL {mode} STDEV {ampName}"] = results.residualSigma
1260
1261 overscans.append(results)
1262
1263 # Question: should this be finer grained?
1264 ccdExposure.metadata.set("OVERSCAN", "Overscan corrected")
1265
1266 return overscans
1267
1268 def maskNegativeVariance(self, exposure):
1269 """Identify and mask pixels with negative variance values.
1270
1271 Parameters
1272 ----------
1273 exposure : `lsst.afw.image.Exposure`
1274 Exposure to process.
1275
1276 See Also
1277 --------
1278 lsst.ip.isr.isrFunctions.updateVariance
1279 """
1280 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName)
1281 bad = numpy.where(exposure.getVariance().getArray() <= 0.0)
1282 exposure.mask.array[bad] |= maskPlane
1283
1284 def addVariancePlane(self, exposure, detector):
1285 """Add the variance plane to the image.
1286
1287 The gain and read noise per amp must have been set in the
1288 exposure metadata as ``LSST ISR GAIN ampName`` and
1289 ``LSST ISR READNOISE ampName`` with the units of the image.
1290 Unit conversions for the variance plane will be done as
1291 necessary based on the exposure units.
1292
1293 The units of the variance plane will always be of the same
1294 type as the units of the input image itself
1295 (``LSST ISR UNITS``^2).
1296
1297 Parameters
1298 ----------
1299 exposure : `lsst.afw.image.Exposure`
1300 The exposure to add the variance plane.
1301 detector : `lsst.afw.cameraGeom.Detector`
1302 Detector with geometry info.
1303 """
1304 # NOTE: this will fail if the exposure is not trimmed.
1305 if not isTrimmedExposure(exposure):
1306 raise RuntimeError("Exposure must be trimmed to add variance plane.")
1307
1308 isElectrons = (exposure.metadata["LSST ISR UNITS"] == "electron")
1309
1310 for amp in detector:
1311 if exposure.getBBox().contains(amp.getBBox()):
1312 self.log.debug("Constructing variance map for amplifer %s.", amp.getName())
1313 ampExposure = exposure.Factory(exposure, amp.getBBox())
1314
1315 # The effective gain is 1.0 if we are in electron units.
1316 # The metadata read noise is in the same units as the image.
1317 gain = exposure.metadata[f"LSST ISR GAIN {amp.getName()}"] if not isElectrons else 1.0
1318 readNoise = exposure.metadata[f"LSST ISR READNOISE {amp.getName()}"]
1319
1320 isrFunctions.updateVariance(
1321 maskedImage=ampExposure.maskedImage,
1322 gain=gain,
1323 readNoise=readNoise,
1324 replace=False,
1325 )
1326
1327 if self.config.qa is not None and self.config.qa.saveStats is True:
1328 qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
1329 afwMath.MEDIAN | afwMath.STDEVCLIP)
1330 self.log.debug(" Variance stats for amplifer %s: %f +/- %f.",
1331 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1332 qaStats.getValue(afwMath.STDEVCLIP))
1333
1334 if self.config.maskNegativeVariance:
1335 self.maskNegativeVariance(exposure)
1336
1337 def maskDefects(self, exposure, defectBaseList):
1338 """Mask defects using mask plane "BAD", in place.
1339
1340 Parameters
1341 ----------
1342 exposure : `lsst.afw.image.Exposure`
1343 Exposure to process.
1344
1345 defectBaseList : defect-type
1346 List of defects to mask. Can be of type `lsst.ip.isr.Defects`
1347 or `list` of `lsst.afw.image.DefectBase`.
1348 """
1349 maskedImage = exposure.getMaskedImage()
1350 if not isinstance(defectBaseList, Defects):
1351 # Promotes DefectBase to Defect
1352 defectList = Defects(defectBaseList)
1353 else:
1354 defectList = defectBaseList
1355 defectList.maskPixels(maskedImage, maskName="BAD")
1356
1357 if len(self.config.badAmps) == 0:
1358 return
1359
1360 detector = exposure.getDetector()
1361 mask = maskedImage.mask
1362 for badAmp in self.config.badAmps:
1363 if badAmp.startswith(detector.getName()):
1364 # Split on the full detector name plus _, which
1365 # gives us an empty string and the amp name.
1366 ampName = badAmp.split(detector.getName() + "_")[-1]
1367 self.log.info("Masking amplifier %s as bad via config.", ampName)
1368 mask[detector[ampName].getBBox()].array[:, :] |= mask.getPlaneBitMask("BAD")
1369
1370 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'):
1371 """Mask edge pixels with applicable mask plane.
1372
1373 Parameters
1374 ----------
1375 exposure : `lsst.afw.image.Exposure`
1376 Exposure to process.
1377 numEdgePixels : `int`, optional
1378 Number of edge pixels to mask.
1379 maskPlane : `str`, optional
1380 Mask plane name to use.
1381 level : `str`, optional
1382 Level at which to mask edges.
1383 """
1384 maskedImage = exposure.getMaskedImage()
1385 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane)
1386
1387 if numEdgePixels > 0:
1388 if level == 'DETECTOR':
1389 boxes = [maskedImage.getBBox()]
1390 elif level == 'AMP':
1391 boxes = [amp.getBBox() for amp in exposure.getDetector()]
1392
1393 for box in boxes:
1394 # This makes a bbox numEdgeSuspect pixels smaller than the
1395 # image on each side
1396 subImage = maskedImage[box]
1397 box.grow(-numEdgePixels)
1398 # Mask pixels outside box
1399 SourceDetectionTask.setEdgeBits(
1400 subImage,
1401 box,
1402 maskBitMask)
1403
1404 def maskNan(self, exposure):
1405 """Mask NaNs using mask plane "UNMASKEDNAN", in place.
1406
1407 Parameters
1408 ----------
1409 exposure : `lsst.afw.image.Exposure`
1410 Exposure to process.
1411
1412 Notes
1413 -----
1414 We mask over all non-finite values (NaN, inf), including those
1415 that are masked with other bits (because those may or may not be
1416 interpolated over later, and we want to remove all NaN/infs).
1417 Despite this behaviour, the "UNMASKEDNAN" mask plane is used to
1418 preserve the historical name.
1419 """
1420 maskedImage = exposure.getMaskedImage()
1421
1422 # Find and mask NaNs
1423 maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
1424 numNans = maskNans(maskedImage, maskVal)
1425 self.metadata["NUMNANS"] = numNans
1426 if numNans > 0:
1427 self.log.warning("There were %d unmasked NaNs.", numNans)
1428
1429 def setBadRegions(self, exposure):
1430 """Set bad regions from large contiguous regions.
1431
1432 Parameters
1433 ----------
1434 exposure : `lsst.afw.Exposure`
1435 Exposure to set bad regions.
1436
1437 Notes
1438 -----
1439 Reset and interpolate bad pixels.
1440
1441 Large contiguous bad regions (which should have the BAD mask
1442 bit set) should have their values set to the image median.
1443 This group should include defects and bad amplifiers. As the
1444 area covered by these defects are large, there's little
1445 reason to expect that interpolation would provide a more
1446 useful value.
1447
1448 Smaller defects can be safely interpolated after the larger
1449 regions have had their pixel values reset. This ensures
1450 that the remaining defects adjacent to bad amplifiers (as an
1451 example) do not attempt to interpolate extreme values.
1452 """
1453 badPixelCount, badPixelValue = isrFunctions.setBadRegions(exposure)
1454 if badPixelCount > 0:
1455 self.log.info("Set %d BAD pixels to %f.", badPixelCount, badPixelValue)
1456
1457 @contextmanager
1458 def flatContext(self, exp, flat, dark=None):
1459 """Context manager that applies and removes flats and darks,
1460 if the task is configured to apply them.
1461
1462 Parameters
1463 ----------
1464 exp : `lsst.afw.image.Exposure`
1465 Exposure to process.
1466 flat : `lsst.afw.image.Exposure`
1467 Flat exposure the same size as ``exp``.
1468 dark : `lsst.afw.image.Exposure`, optional
1469 Dark exposure the same size as ``exp``.
1470
1471 Yields
1472 ------
1473 exp : `lsst.afw.image.Exposure`
1474 The flat and dark corrected exposure.
1475 """
1476 if self.config.doDark and dark is not None:
1477 self.darkCorrection(exp, dark)
1478 if self.config.doFlat and flat is not None:
1479 self.flatCorrection(exp, flat)
1480 try:
1481 yield exp
1482 finally:
1483 if self.config.doFlat and flat is not None:
1484 self.flatCorrection(exp, flat, invert=True)
1485 if self.config.doDark and dark is not None:
1486 self.darkCorrection(exp, dark, invert=True)
1487
1488 def getBrighterFatterKernel(self, detector, bfKernel):
1489 detName = detector.getName()
1490
1491 # This is expected to be a dictionary of amp-wise gains.
1492 bfGains = bfKernel.gain
1493 if bfKernel.level == 'DETECTOR':
1494 if detName in bfKernel.detKernels:
1495 bfKernelOut = bfKernel.detKernels[detName]
1496 return bfKernelOut, bfGains
1497 else:
1498 raise RuntimeError("Failed to extract kernel from new-style BF kernel.")
1499 elif bfKernel.level == 'AMP':
1500 self.log.info("Making DETECTOR level kernel from AMP based brighter "
1501 "fatter kernels.")
1502 bfKernel.makeDetectorKernelFromAmpwiseKernels(detName)
1503 bfKernelOut = bfKernel.detKernels[detName]
1504 return bfKernelOut, bfGains
1505
1506 def applyBrighterFatterCorrection(self, ccdExposure, flat, dark, bfKernel, brighterFatterApplyGain,
1507 bfGains):
1508 """Apply a brighter fatter correction to the image using the
1509 method defined in Coulton et al. 2019.
1510
1511 Note that this correction requires that the image is in units
1512 electrons.
1513
1514 Parameters
1515 ----------
1516 ccdExposure : `lsst.afw.image.Exposure`
1517 Exposure to process.
1518 flat : `lsst.afw.image.Exposure`
1519 Flat exposure the same size as ``exp``.
1520 dark : `lsst.afw.image.Exposure`, optional
1521 Dark exposure the same size as ``exp``.
1522 bfKernel : `lsst.ip.isr.BrighterFatterKernel`
1523 The brighter-fatter kernel.
1524 brighterFatterApplyGain : `bool`
1525 Apply the gain to convert the image to electrons?
1526 bfGains : `dict`
1527 The gains to use if brighterFatterApplyGain = True.
1528
1529 Yields
1530 ------
1531 exp : `lsst.afw.image.Exposure`
1532 The flat and dark corrected exposure.
1533 """
1534 interpExp = ccdExposure.clone()
1535
1536 # We need to interpolate before we do B-F. Note that
1537 # brighterFatterFwhmForInterpolation is currently unused.
1538 isrFunctions.interpolateFromMask(
1539 maskedImage=interpExp.getMaskedImage(),
1540 fwhm=self.config.brighterFatterFwhmForInterpolation,
1541 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1542 maskNameList=list(self.config.brighterFatterMaskListToInterpolate),
1543 useLegacyInterp=self.config.useLegacyInterp,
1544 )
1545 bfExp = interpExp.clone()
1546 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1547 self.config.brighterFatterMaxIter,
1548 self.config.brighterFatterThreshold,
1549 brighterFatterApplyGain,
1550 bfGains)
1551 bfCorrIters = bfResults[1]
1552 if bfCorrIters == self.config.brighterFatterMaxIter:
1553 self.log.warning("Brighter-fatter correction did not converge, final difference %f.",
1554 bfResults[0])
1555 else:
1556 self.log.info("Finished brighter-fatter correction in %d iterations.",
1557 bfResults[1])
1558
1559 image = ccdExposure.getMaskedImage().getImage()
1560 bfCorr = bfExp.getMaskedImage().getImage()
1561 bfCorr -= interpExp.getMaskedImage().getImage()
1562 image += bfCorr
1563
1564 # Applying the brighter-fatter correction applies a
1565 # convolution to the science image. At the edges this
1566 # convolution may not have sufficient valid pixels to
1567 # produce a valid correction. Mark pixels within the size
1568 # of the brighter-fatter kernel as EDGE to warn of this
1569 # fact.
1570 self.log.info("Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1571 self.maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1572 maskPlane="EDGE")
1573
1574 if self.config.brighterFatterMaskGrowSize > 0:
1575 self.log.info("Growing masks to account for brighter-fatter kernel convolution.")
1576 for maskPlane in self.config.brighterFatterMaskListToInterpolate:
1577 isrFunctions.growMasks(ccdExposure.getMask(),
1578 radius=self.config.brighterFatterMaskGrowSize,
1579 maskNameList=maskPlane,
1580 maskValue=maskPlane)
1581
1582 return ccdExposure, bfCorrIters
1583
1584 def darkCorrection(self, exposure, darkExposure, invert=False):
1585 """Apply dark correction in place.
1586
1587 Parameters
1588 ----------
1589 exposure : `lsst.afw.image.Exposure`
1590 Exposure to process.
1591 darkExposure : `lsst.afw.image.Exposure`
1592 Dark exposure of the same size as ``exposure``.
1593 invert : `Bool`, optional
1594 If True, re-add the dark to an already corrected image.
1595
1596 Raises
1597 ------
1598 RuntimeError
1599 Raised if either ``exposure`` or ``darkExposure`` do not
1600 have their dark time defined.
1601
1602 See Also
1603 --------
1604 lsst.ip.isr.isrFunctions.darkCorrection
1605 """
1606 expScale = exposure.visitInfo.darkTime
1607 if math.isnan(expScale):
1608 raise RuntimeError("Exposure darktime is NAN.")
1609 if darkExposure.visitInfo is not None \
1610 and not math.isnan(darkExposure.visitInfo.darkTime):
1611 darkScale = darkExposure.visitInfo.darkTime
1612 else:
1613 # DM-17444: darkExposure.visitInfo is None
1614 # so darkTime does not exist.
1615 self.log.warning("darkExposure.visitInfo does not exist. Using darkScale = 1.0.")
1616 darkScale = 1.0
1617
1618 isrFunctions.darkCorrection(
1619 maskedImage=exposure.maskedImage,
1620 darkMaskedImage=darkExposure.maskedImage,
1621 expScale=expScale,
1622 darkScale=darkScale,
1623 invert=invert,
1624 )
1625
1626 @staticmethod
1628 """Extract common calibration metadata values that will be written to
1629 output header.
1630
1631 Parameters
1632 ----------
1633 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib`
1634 Calibration to pull date information from.
1635
1636 Returns
1637 -------
1638 dateString : `str`
1639 Calibration creation date string to add to header.
1640 """
1641 if hasattr(calib, "getMetadata"):
1642 if 'CALIB_CREATION_DATE' in calib.metadata:
1643 return " ".join((calib.metadata.get("CALIB_CREATION_DATE", "Unknown"),
1644 calib.metadata.get("CALIB_CREATION_TIME", "Unknown")))
1645 else:
1646 return " ".join((calib.metadata.get("CALIB_CREATE_DATE", "Unknown"),
1647 calib.metadata.get("CALIB_CREATE_TIME", "Unknown")))
1648 else:
1649 return "Unknown Unknown"
1650
1651 def compareUnits(self, calibMetadata, calibName):
1652 """Compare units from calibration to ISR units.
1653
1654 This compares calibration units (adu or electron) to whether
1655 doApplyGain is set.
1656
1657 Parameters
1658 ----------
1659 calibMetadata : `lsst.daf.base.PropertyList`
1660 Calibration metadata from header.
1661 calibName : `str`
1662 Calibration name for log message.
1663 """
1664 calibUnits = calibMetadata.get("LSST ISR UNITS", "adu")
1665 isrUnits = "electron" if self.config.doApplyGains else "adu"
1666 if calibUnits != isrUnits:
1667 if self.config.doRaiseOnCalibMismatch:
1668 raise RuntimeError(
1669 "Unit mismatch: isr has %s units but %s has %s units",
1670 isrUnits,
1671 calibName,
1672 calibUnits,
1673 )
1674 else:
1675 self.log.warning(
1676 "Unit mismatch: isr has %s units but %s has %s units",
1677 isrUnits,
1678 calibName,
1679 calibUnits,
1680 )
1681
1682 def convertIntToFloat(self, exposure):
1683 """Convert exposure image from uint16 to float.
1684
1685 If the exposure does not need to be converted, the input is
1686 immediately returned. For exposures that are converted to use
1687 floating point pixels, the variance is set to unity and the
1688 mask to zero.
1689
1690 Parameters
1691 ----------
1692 exposure : `lsst.afw.image.Exposure`
1693 The raw exposure to be converted.
1694
1695 Returns
1696 -------
1697 newexposure : `lsst.afw.image.Exposure`
1698 The input ``exposure``, converted to floating point pixels.
1699
1700 Raises
1701 ------
1702 RuntimeError
1703 Raised if the exposure type cannot be converted to float.
1704
1705 """
1706 if isinstance(exposure, afwImage.ExposureF):
1707 # Nothing to be done
1708 self.log.debug("Exposure already of type float.")
1709 return exposure
1710 if not hasattr(exposure, "convertF"):
1711 raise RuntimeError("Unable to convert exposure (%s) to float." % type(exposure))
1712
1713 newexposure = exposure.convertF()
1714 newexposure.variance[:] = 1
1715 newexposure.mask[:] = 0x0
1716
1717 return newexposure
1718
1719 def ditherCounts(self, exposure, detectorConfig, fallbackSeed=12345):
1720 """Dither the counts in the exposure.
1721
1722 Parameters
1723 ----------
1724 exposure : `lsst.afw.image.Exposure`
1725 The raw exposure to be dithered.
1726 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
1727 Configuration for overscan/etc for this detector.
1728 fallbackSeed : `int`, optional
1729 Random seed to fall back to if exposure.getInfo().getId() is
1730 not set.
1731 """
1732 if detectorConfig.integerDitherMode == "NONE":
1733 # Nothing to do here.
1734 return
1735
1736 # This ID is a unique combination of {exposure, detector} for a raw
1737 # image as we have here. We additionally need to take the lower
1738 # 32 bits to be used as a random seed.
1739 if exposure.info.id is not None:
1740 seed = exposure.info.id & 0xFFFFFFFF
1741 else:
1742 seed = fallbackSeed
1743 self.log.warning("No exposure ID found; using fallback random seed.")
1744
1745 self.log.info("Seeding dithering random number generator with %d.", seed)
1746 rng = numpy.random.RandomState(seed=seed)
1747
1748 if detectorConfig.integerDitherMode == "POSITIVE":
1749 low = 0.0
1750 high = 1.0
1751 elif detectorConfig.integerDitherMode == "NEGATIVE":
1752 low = -1.0
1753 high = 0.0
1754 elif detectorConfig.integerDitherMode == "SYMMETRIC":
1755 low = -0.5
1756 high = 0.5
1757 else:
1758 raise RuntimeError("Invalid config")
1759
1760 exposure.image.array[:, :] += rng.uniform(low=low, high=high, size=exposure.image.array.shape)
1761
1762 def checkBssVoltage(self, exposure):
1763 """Check the back-side bias voltage to see if the detector is on.
1764
1765 Parameters
1766 ----------
1767 exposure : `lsst.afw.image.ExposureF`
1768 Input exposure.
1769
1770 Raises
1771 ------
1772 `UnprocessableDataError` if voltage is off.
1773 """
1774 voltage = exposure.metadata.get(self.config.bssVoltageKeyword, None)
1775 if voltage is None or not numpy.isfinite(voltage):
1776 self.log.warning(
1777 "Back-side bias voltage %s not found in metadata.",
1778 self.config.bssVoltageKeyword,
1779 )
1780 return
1781
1782 hv = exposure.metadata.get(self.config.hvBiasKeyword, None)
1783 if hv is None:
1784 self.log.warning(
1785 "HV bias on %s not found in metadata.",
1786 self.config.hvBiasKeyword,
1787 )
1788 return
1789
1790 if voltage < self.config.bssVoltageMinimum or hv == "OFF":
1791 detector = exposure.getDetector()
1792 raise UnprocessableDataError(
1793 f"Back-side bias voltage is turned off for {detector.getName()}; skipping ISR.",
1794 )
1795
1796 @deprecated(
1797 reason=(
1798 "makeBinnedImages is no longer used. "
1799 "Please subtask lsst.ip.isr.BinImageDataTask instead."
1800 ),
1801 version="v28", category=FutureWarning
1802 )
1803 def makeBinnedImages(self, exposure):
1804 """Make visualizeVisit style binned exposures.
1805
1806 Parameters
1807 ----------
1808 exposure : `lsst.afw.image.Exposure`
1809 Exposure to bin.
1810
1811 Returns
1812 -------
1813 bin1 : `lsst.afw.image.Exposure`
1814 Binned exposure using binFactor1.
1815 bin2 : `lsst.afw.image.Exposure`
1816 Binned exposure using binFactor2.
1817 """
1818 mi = exposure.getMaskedImage()
1819
1820 bin1 = afwMath.binImage(mi, self.config.binFactor1)
1821 bin2 = afwMath.binImage(mi, self.config.binFactor2)
1822
1823 bin1 = afwImage.makeExposure(bin1)
1824 bin2 = afwImage.makeExposure(bin2)
1825
1826 bin1.setInfo(exposure.getInfo())
1827 bin2.setInfo(exposure.getInfo())
1828
1829 return bin1, bin2
1830
1831 def run(
1832 self,
1833 ccdExposure,
1834 *,
1835 dnlLUT=None,
1836 bias=None,
1837 deferredChargeCalib=None,
1838 linearizer=None,
1839 ptc=None,
1840 gainCorrection=None,
1841 crosstalk=None,
1842 defects=None,
1843 bfKernel=None,
1844 dark=None,
1845 flat=None,
1846 camera=None,
1847 ):
1848 """Run the IsrTaskLSST task.
1849
1850 Parameters
1851 ----------
1852 ccdExposure : `lsst.afw.image.Exposure`
1853 Exposure to run ISR.
1854 dnlLUT : `None`, optional
1855 DNL lookup table; placeholder, unused.
1856 bias : `lsst.afw.image.Exposure`, optional
1857 Bias frame.
1858 deferredChargeCalib : `lsst.ip.isr.DeferredChargeCalib`, optional
1859 Deferred charge calibration.
1860 linearizer : `lsst.ip.isr.Linearizer`, optional
1861 Linearizer calibration.
1862 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
1863 PTC dataset.
1864 gainCorrection : `lsst.ip.isr.GainCorrection`, optional
1865 Gain correction dataset.
1866 crosstalk : `lsst.ip.isr.CrosstalkCalib`, optional
1867 Crosstalk calibration dataset.
1868 defects : `lsst.ip.isr.Defects`, optional
1869 Defects dataset.
1870 bfKernel : `lsst.ip.isr.BrighterFatterKernel`, optional
1871 Brighter-fatter kernel dataset.
1872 dark : `lsst.afw.image.Exposure`, optional
1873 Dark frame.
1874 flat : `lsst.afw.image.Exposure`, optional
1875 Flat-field frame.
1876 camera : `lsst.afw.cameraGeom.Camera`, optional
1877 Camera object.
1878
1879 Returns
1880 -------
1881 result : `lsst.pipe.base.Struct`
1882 Struct with fields:
1883 ``exposure``: `lsst.afw.image.Exposure`
1884 Calibrated exposure.
1885 ``outputBin1Exposure``: `lsst.afw.image.Exposure`
1886 Binned exposure (bin1 config).
1887 ``outputBin2Exposure``: `lsst.afw.image.Exposure`
1888 Binned exposure (bin2 config).
1889 ``outputExposure``: `lsst.afw.image.Exposure`
1890 Calibrated exposure (same as ``exposure``).
1891 ``outputStatistics``: `lsst.ip.isr.isrStatistics`
1892 Calibrated exposure statistics.
1893 """
1894 detector = ccdExposure.getDetector()
1895
1896 overscanDetectorConfig = self.config.overscanCamera.getOverscanDetectorConfig(detector)
1897
1898 if self.config.doBootstrap:
1899 if ptc is not None:
1900 self.log.warning("Task configured with doBootstrap=True. Ignoring provided PTC.")
1901 ptc = None
1902 else:
1903 if self.config.useGainsFrom == "LINEARIZER":
1904 if linearizer is None:
1905 raise RuntimeError("doBootstrap==False and useGainsFrom == 'LINEARIZER' but "
1906 "no linearizer provided.")
1907 elif self.config.useGainsFrom == "PTC":
1908 if ptc is None:
1909 raise RuntimeError("doBootstrap==False and useGainsFrom == 'PTC' but no PTC provided.")
1910
1911 # Validation step: check inputs match exposure configuration.
1912 exposureMetadata = ccdExposure.metadata
1913 doRaise = self.config.doRaiseOnCalibMismatch
1914 keywords = self.config.cameraKeywordsToCompare
1915 if not self.config.doBootstrap:
1916 if self.config.useGainsFrom == "LINEARIZER":
1917 compareCameraKeywords(doRaise, keywords, exposureMetadata, linearizer,
1918 "LINEARIZER", log=self.log)
1919 elif self.config.useGainsFrom == "PTC":
1920 compareCameraKeywords(doRaise, keywords, exposureMetadata, ptc, "PTC",
1921 log=self.log)
1922 # Note that doCorrectGains can be True without a gainCorrection.
1923 if self.config.doCorrectGains and gainCorrection is not None:
1925 doRaise,
1926 keywords,
1927 exposureMetadata,
1928 gainCorrection,
1929 "gain_correction",
1930 log=self.log,
1931 )
1932 else:
1933 if self.config.doCorrectGains:
1934 raise RuntimeError("doCorrectGains is True but no ptc provided.")
1935 if self.config.doDiffNonLinearCorrection:
1936 if dnlLUT is None:
1937 raise RuntimeError("doDiffNonLinearCorrection is True but no dnlLUT provided.")
1938 compareCameraKeywords(doRaise, keywords, exposureMetadata, dnlLUT, "dnlLUT", log=self.log)
1939 if self.config.doLinearize:
1940 if linearizer is None:
1941 raise RuntimeError("doLinearize is True but no linearizer provided.")
1942 compareCameraKeywords(doRaise, keywords, exposureMetadata, linearizer, "linearizer", log=self.log)
1943 if self.config.doBias:
1944 if bias is None:
1945 raise RuntimeError("doBias is True but no bias provided.")
1946 compareCameraKeywords(doRaise, keywords, exposureMetadata, bias, "bias", log=self.log)
1947 self.compareUnits(bias.metadata, "bias")
1948 if self.config.doCrosstalk:
1949 if crosstalk is None:
1950 raise RuntimeError("doCrosstalk is True but no crosstalk provided.")
1951 compareCameraKeywords(doRaise, keywords, exposureMetadata, crosstalk, "crosstalk", log=self.log)
1952 if self.config.doDeferredCharge:
1953 if deferredChargeCalib is None:
1954 raise RuntimeError("doDeferredCharge is True but no deferredChargeCalib provided.")
1956 doRaise,
1957 keywords,
1958 exposureMetadata,
1959 deferredChargeCalib,
1960 "CTI",
1961 log=self.log,
1962 )
1963 if self.config.doDefect:
1964 if defects is None:
1965 raise RuntimeError("doDefect is True but no defects provided.")
1966 compareCameraKeywords(doRaise, keywords, exposureMetadata, defects, "defects", log=self.log)
1967 if self.config.doDark:
1968 if dark is None:
1969 raise RuntimeError("doDark is True but no dark frame provided.")
1970 compareCameraKeywords(doRaise, keywords, exposureMetadata, dark, "dark", log=self.log)
1971 self.compareUnits(bias.metadata, "dark")
1972 if self.config.doBrighterFatter:
1973 if bfKernel is None:
1974 raise RuntimeError("doBrighterFatter is True not no bfKernel provided.")
1975 compareCameraKeywords(doRaise, keywords, exposureMetadata, bfKernel, "bf", log=self.log)
1976 if self.config.doFlat:
1977 if flat is None:
1978 raise RuntimeError("doFlat is True but no flat provided.")
1979 compareCameraKeywords(doRaise, keywords, exposureMetadata, flat, "flat", log=self.log)
1980
1981 if self.config.doSaturation:
1982 if self.config.defaultSaturationSource in ["PTCTURNOFF",]:
1983 if ptc is None:
1984 raise RuntimeError(
1985 "doSaturation is True and defaultSaturationSource is "
1986 f"{self.config.defaultSaturationSource}, but no ptc provided."
1987 )
1988 if self.config.doSuspect:
1989 if self.config.defaultSuspectSource in ["PTCTURNOFF",]:
1990 if ptc is None:
1991 raise RuntimeError(
1992 "doSuspect is True and defaultSuspectSource is "
1993 f"{self.config.defaultSuspectSource}, but no ptc provided."
1994 )
1995
1996 if self.config.doCheckUnprocessableData and self.config.bssVoltageMinimum > 0.0:
1997 self.checkBssVoltage(ccdExposure)
1998
1999 # FIXME: Make sure that if linearity is done then it is matched
2000 # with the right PTC.
2001
2002 # We keep track of units: start in adu.
2003 exposureMetadata["LSST ISR UNITS"] = "adu"
2004 exposureMetadata["LSST ISR GAINCORRECTION APPLIED"] = False
2005 exposureMetadata["LSST ISR CROSSTALK APPLIED"] = False
2006 exposureMetadata["LSST ISR OVERSCANLEVEL CHECKED"] = False
2007 exposureMetadata["LSST ISR NOISE CHECKED"] = False
2008 exposureMetadata["LSST ISR LINEARIZER APPLIED"] = False
2009 exposureMetadata["LSST ISR CTI APPLIED"] = False
2010 exposureMetadata["LSST ISR BIAS APPLIED"] = False
2011 exposureMetadata["LSST ISR DARK APPLIED"] = False
2012 exposureMetadata["LSST ISR BF APPLIED"] = False
2013 exposureMetadata["LSST ISR FLAT APPLIED"] = False
2014 exposureMetadata["LSST ISR DEFECTS APPLIED"] = False
2015
2016 if self.config.doBootstrap:
2017 self.log.info("Configured using doBootstrap=True; using gain of 1.0 (adu units)")
2018 ptc = PhotonTransferCurveDataset([amp.getName() for amp in detector], "NOMINAL_PTC", 1)
2019 for amp in detector:
2020 ptc.gain[amp.getName()] = 1.0
2021 ptc.noise[amp.getName()] = 0.0
2022 elif self.config.useGainsFrom == "LINEARIZER":
2023 self.log.info("Using gains from linearizer.")
2024 # Create a dummy ptc object to hold the gains from the linearizer.
2025 ptc = PhotonTransferCurveDataset([amp.getName() for amp in detector], "NOMINAL_PTC", 1)
2026 for amp in detector:
2027 ptc.gain[amp.getName()] = linearizer.inputGain[amp.getName()]
2028 ptc.noise[amp.getName()] = 0.0
2029
2030 exposureMetadata["LSST ISR BOOTSTRAP"] = self.config.doBootstrap
2031
2032 # Choose the gains to use
2033 gains = ptc.gain
2034
2035 # And check if we have configured gains to override. This is
2036 # also a warning, since it should not be typical usage.
2037 for amp in detector:
2038 if not math.isnan(gain := overscanDetectorConfig.getOverscanAmpConfig(amp).gain):
2039 gains[amp.getName()] = gain
2040 self.log.warning(
2041 "Overriding gain for amp %s with configured value of %.3f.",
2042 amp.getName(),
2043 gain,
2044 )
2045
2046 # First we convert the exposure to floating point values
2047 # (if necessary).
2048 self.log.debug("Converting exposure to floating point values.")
2049 ccdExposure = self.convertIntToFloat(ccdExposure)
2050
2051 # Then we mark which amplifiers are completely bad from defects.
2052 badAmpDict = self.maskFullAmplifiers(ccdExposure, detector, defects, gains=gains)
2053
2054 self.checkAllBadAmps(badAmpDict, detector)
2055
2056 # Now we go through ISR steps.
2057
2058 # Differential non-linearity correction.
2059 # Units: adu
2060 if self.config.doDiffNonLinearCorrection:
2061 self.diffNonLinearCorrection(ccdExposure, dnlLUT)
2062
2063 # Dither the integer counts.
2064 # Input units: integerized adu
2065 # Output units: floating-point adu
2066 self.ditherCounts(ccdExposure, overscanDetectorConfig)
2067
2068 # Serial overscan correction.
2069 # Input units: adu
2070 # Output units: adu
2071 if overscanDetectorConfig.doAnySerialOverscan:
2072 serialOverscans = self.overscanCorrection(
2073 "SERIAL",
2074 overscanDetectorConfig,
2075 detector,
2076 badAmpDict,
2077 ccdExposure,
2078 )
2079
2080 if self.config.doBootstrap or self.config.useGainsFrom == "LINEARIZER":
2081 # Get the empirical read noise
2082 for amp, serialOverscan in zip(detector, serialOverscans):
2083 if serialOverscan is None:
2084 ptc.noise[amp.getName()] = 0.0
2085 else:
2086 # All PhotonTransferCurveDataset objects should contain
2087 # noise attributes in units of electrons. The read
2088 # noise measured from overscans is always in adu, so we
2089 # scale it by the gain.
2090 # Note that in bootstrap mode, these gains will always
2091 # be 1.0, but we put this conversion here for clarity.
2092 ptc.noise[amp.getName()] = serialOverscan.residualSigma * gains[amp.getName()]
2093 else:
2094 serialOverscans = [None]*len(detector)
2095
2096 # After serial overscan correction, we can mask SATURATED and
2097 # SUSPECT pixels. This updates badAmpDict if any amplifier
2098 # is fully saturated after serial overscan correction.
2099
2100 # The saturation is currently assumed to be recorded in
2101 # overscan-corrected adu.
2102 badAmpDict = self.maskSaturatedPixels(
2103 badAmpDict,
2104 ccdExposure,
2105 detector,
2106 overscanDetectorConfig,
2107 ptc=ptc,
2108 )
2109
2110 self.checkAllBadAmps(badAmpDict, detector)
2111
2112 if self.config.doCorrectGains and gainCorrection is not None:
2113 self.log.info("Correcting gains based on input GainCorrection.")
2114 gainCorrection.correctGains(gains, exposure=ccdExposure)
2115 exposureMetadata["LSST ISR GAINCORRECTION APPLIED"] = True
2116 elif self.config.doCorrectGains:
2117 self.log.info("Skipping gain correction because no GainCorrection available.")
2118
2119 # Do gain normalization.
2120 # Input units: adu
2121 # Output units: electron
2122 if self.config.doApplyGains:
2123 self.log.info("Using gain values to convert from adu to electron units.")
2124 isrFunctions.applyGains(ccdExposure, normalizeGains=False, ptcGains=gains, isTrimmed=False)
2125 # The units are now electron.
2126 exposureMetadata["LSST ISR UNITS"] = "electron"
2127
2128 # Update the saturation units in the metadata if there.
2129 # These will always have the same units as the image.
2130 for amp in detector:
2131 ampName = amp.getName()
2132 if (key := f"LSST ISR SATURATION LEVEL {ampName}") in exposureMetadata:
2133 exposureMetadata[key] *= gains[ampName]
2134 if (key := f"LSST ISR SUSPECT LEVEL {ampName}") in exposureMetadata:
2135 exposureMetadata[key] *= gains[ampName]
2136
2137 # Record gain and read noise in header.
2138 metadata = ccdExposure.metadata
2139 metadata["LSST ISR READNOISE UNITS"] = "electron"
2140 metadata["LSST ISR GAIN SOURCE"] = self.config.useGainsFrom
2141 for amp in detector:
2142 # This includes any gain correction (if applied).
2143 metadata[f"LSST ISR GAIN {amp.getName()}"] = gains[amp.getName()]
2144
2145 # At this stage, the read noise is always in electrons.
2146 noise = ptc.noise[amp.getName()]
2147 metadata[f"LSST ISR READNOISE {amp.getName()}"] = noise
2148
2149 # Do crosstalk correction in the full region.
2150 # Output units: electron (adu if doBootstrap=True)
2151 if self.config.doCrosstalk:
2152 self.log.info("Applying crosstalk corrections to full amplifier region.")
2153 if self.config.doBootstrap and numpy.any(crosstalk.fitGains != 0):
2154 crosstalkGains = None
2155 else:
2156 crosstalkGains = gains
2157 self.crosstalk.run(
2158 ccdExposure,
2159 crosstalk=crosstalk,
2160 gains=crosstalkGains,
2161 fullAmplifier=True,
2162 badAmpDict=badAmpDict,
2163 ignoreVariance=True,
2164 )
2165 ccdExposure.metadata["LSST ISR CROSSTALK APPLIED"] = True
2166
2167 # After crosstalk, we check for amplifier noise and state changes.
2168 if numpy.isfinite(self.config.serialOverscanMedianShiftSigmaThreshold):
2169 badAmpDict = self.checkAmpOverscanLevel(badAmpDict, ccdExposure, ptc)
2170 ccdExposure.metadata["LSST ISR OVERSCANLEVEL CHECKED"] = True
2171
2172 if numpy.isfinite(self.config.ampNoiseThreshold):
2173 badAmpDict = self.checkAmpNoise(badAmpDict, ccdExposure, ptc)
2174 ccdExposure.metadata["LSST ISR NOISE CHECKED"] = True
2175
2176 if numpy.isfinite(self.config.serialOverscanMedianShiftSigmaThreshold) or \
2177 numpy.isfinite(self.config.ampNoiseThreshold):
2178 self.checkAllBadAmps(badAmpDict, detector)
2179
2180 # Parallel overscan correction.
2181 # Output units: electron (adu if doBootstrap=True)
2182 parallelOverscans = None
2183 if overscanDetectorConfig.doAnyParallelOverscan:
2184 # At the moment we do not use the return values from this task.
2185 parallelOverscans = self.overscanCorrection(
2186 "PARALLEL",
2187 overscanDetectorConfig,
2188 detector,
2189 badAmpDict,
2190 ccdExposure,
2191 )
2192
2193 # Linearity correction
2194 # Output units: electron (adu if doBootstrap=True)
2195 if self.config.doLinearize:
2196 self.log.info("Applying linearizer.")
2197 # The linearizer is in units of adu.
2198 # If our units are electron, then pass in the gains
2199 # for conversion.
2200 if exposureMetadata["LSST ISR UNITS"] == "electron":
2201 linearityGains = gains
2202 else:
2203 linearityGains = None
2204 linearizer.applyLinearity(
2205 image=ccdExposure.image,
2206 detector=detector,
2207 log=self.log,
2208 gains=linearityGains,
2209 )
2210 ccdExposure.metadata["LSST ISR LINEARIZER APPLIED"] = True
2211
2212 # Serial CTI (deferred charge) correction
2213 # This will be performed in electron units
2214 # Output units: same as input units
2215 if self.config.doDeferredCharge:
2216 if self.config.doBootstrap:
2217 self.log.info("Applying deferred charge correction with doBootstrap=True: "
2218 "will need to use deferredChargeCalib.inputGain to apply "
2219 "CTI correction in electron units.")
2220 deferredChargeGains = deferredChargeCalib.inputGain
2221 if numpy.all(numpy.isnan(list(deferredChargeGains.values()))):
2222 self.log.warning("All gains contained in the deferredChargeCalib are "
2223 "NaN, approximating with gain of 1.0.")
2224 deferredChargeGains = gains
2225 else:
2226 deferredChargeGains = gains
2227 self.deferredChargeCorrection.run(
2228 ccdExposure,
2229 deferredChargeCalib,
2230 gains=deferredChargeGains,
2231 )
2232 ccdExposure.metadata["LSST ISR CTI APPLIED"] = True
2233
2234 # Save the untrimmed version for later statistics,
2235 # which still contains the overscan information
2236 untrimmedCcdExposure = None
2237 if self.config.isrStats.doCtiStatistics:
2238 untrimmedCcdExposure = ccdExposure.clone()
2239
2240 # Assemble/trim
2241 # Output units: electron (adu if doBootstrap=True)
2242 if self.config.doAssembleCcd:
2243 self.log.info("Assembling CCD from amplifiers.")
2244 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
2245
2246 if self.config.expectWcs and not ccdExposure.getWcs():
2247 self.log.warning("No WCS found in input exposure.")
2248
2249 # E2V edge bleed
2250 if self.config.doE2VEdgeBleedMask and detector.getPhysicalType() == "E2V":
2251 isrFunctions.maskE2VEdgeBleed(
2252 exposure=ccdExposure,
2253 e2vEdgeBleedSatMinArea=self.config.e2vEdgeBleedSatMinArea,
2254 e2vEdgeBleedSatMaxArea=self.config.e2vEdgeBleedSatMaxArea,
2255 e2vEdgeBleedYMax=self.config.e2vEdgeBleedYMax,
2256 saturatedMaskName=self.config.saturatedMaskName,
2257 log=self.log,
2258 )
2259
2260 # ITL Dip Masking
2261 for maskPlane in self.config.itlDipMaskPlanes:
2262 if maskPlane not in ccdExposure.mask.getMaskPlaneDict():
2263 self.log.info("Adding %s mask plane to image.", maskPlane)
2264 ccdExposure.mask.addMaskPlane(maskPlane)
2265
2266 if self.config.doITLDipMask:
2267 isrFunctions.maskITLDip(
2268 exposure=ccdExposure,
2269 detectorConfig=overscanDetectorConfig,
2270 log=self.log,
2271 maskPlaneNames=self.config.itlDipMaskPlanes,
2272 )
2273
2274 if (self.config.doITLSatSagMask or self.config.doITLEdgeBleedMask) \
2275 and detector.getPhysicalType() == 'ITL':
2276 self.maskITLSatEdgesAndColumns(exposure=ccdExposure,
2277 badAmpDict=badAmpDict)
2278
2279 # Bias subtraction
2280 # Output units: electron (adu if doBootstrap=True)
2281 if self.config.doBias:
2282 self.log.info("Applying bias correction.")
2283 # Bias frame and ISR unit consistency is checked at the top of
2284 # the run method.
2285 isrFunctions.biasCorrection(ccdExposure.maskedImage, bias.maskedImage)
2286 ccdExposure.metadata["LSST ISR BIAS APPLIED"] = True
2287
2288 # Dark subtraction
2289 # Output units: electron (adu if doBootstrap=True)
2290 if self.config.doDark:
2291 self.log.info("Applying dark subtraction.")
2292 # Dark frame and ISR unit consistency is checked at the top of
2293 # the run method.
2294 self.darkCorrection(ccdExposure, dark)
2295 ccdExposure.metadata["LSST ISR DARK APPLIED"] = True
2296
2297 # Defect masking
2298 # Masking block (defects, NAN pixels and trails).
2299 # Saturated and suspect pixels have already been masked.
2300 # Output units: electron (adu if doBootstrap=True)
2301 if self.config.doDefect:
2302 self.log.info("Applying defect masking.")
2303 self.maskDefects(ccdExposure, defects)
2304 ccdExposure.metadata["LSST ISR DEFECTS APPLIED"] = True
2305
2306 self.log.info("Adding UNMASKEDNAN mask plane to image.")
2307 ccdExposure.mask.addMaskPlane("UNMASKEDNAN")
2308 if self.config.doNanMasking:
2309 self.log.info("Masking non-finite (NAN, inf) value pixels.")
2310 self.maskNan(ccdExposure)
2311
2312 if self.config.doWidenSaturationTrails:
2313 self.log.info("Widening saturation trails.")
2314 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
2315
2316 # Brighter-Fatter
2317 # Output units: electron (adu if doBootstrap=True)
2318 if self.config.doBrighterFatter:
2319 self.log.info("Applying brighter-fatter correction.")
2320
2321 bfKernelOut, bfGains = self.getBrighterFatterKernel(detector, bfKernel)
2322
2323 # Needs to be done in electrons; applyBrighterFatterCorrection
2324 # will convert the image if necessary.
2325 if exposureMetadata["LSST ISR UNITS"] == "electron":
2326 brighterFatterApplyGain = False
2327 else:
2328 brighterFatterApplyGain = True
2329
2330 if brighterFatterApplyGain and (ptc is not None) and (bfGains != gains):
2331 # The supplied ptc should be the same as the ptc used to
2332 # generate the bfKernel, in which case they will have the
2333 # same stored amp-keyed dictionary of gains. If not, there
2334 # is a mismatch in the calibrations being used. This should
2335 # not be always be a fatal error, but ideally, everything
2336 # should to be consistent.
2337 self.log.warning("Need to apply gain for brighter-fatter, but the stored"
2338 "gains in the kernel are not the same as the gains used"
2339 f"by {self.config.useGainsFrom}. Using the gains stored"
2340 "in the kernel.")
2341
2342 ccdExposure, bfCorrIters = self.applyBrighterFatterCorrection(
2343 ccdExposure,
2344 flat,
2345 dark,
2346 bfKernelOut,
2347 brighterFatterApplyGain,
2348 bfGains,
2349 )
2350
2351 ccdExposure.metadata["LSST ISR BF APPLIED"] = True
2352 metadata["LSST ISR BF ITERS"] = bfCorrIters
2353
2354 # Variance plane creation
2355 # Output units: electron (adu if doBootstrap=True)
2356 if self.config.doVariance:
2357 self.addVariancePlane(ccdExposure, detector)
2358
2359 # Flat-fielding
2360 # This may move elsewhere, but this is the most convenient
2361 # location for simple flat-fielding for attractive backgrounds.
2362 # Output units: electron (adu if doBootstrap=True)
2363 if self.config.doFlat:
2364 self.log.info("Applying flat correction.")
2365 isrFunctions.flatCorrection(
2366 maskedImage=ccdExposure.maskedImage,
2367 flatMaskedImage=flat.maskedImage,
2368 scalingType=self.config.flatScalingType,
2369 userScale=self.config.flatUserScale,
2370 )
2371
2372 # Copy over valid polygon from flat if it is
2373 # available, and set NO_DATA to 0.0 (which may
2374 # be inherited from the flat in the fully
2375 # vignetted region).
2376 if (validPolygon := flat.info.getValidPolygon()) is not None:
2377 ccdExposure.info.setValidPolygon(validPolygon)
2378
2379 noData = (ccdExposure.mask.array & ccdExposure.mask.getPlaneBitMask("NO_DATA")) > 0
2380 ccdExposure.image.array[noData] = 0.0
2381 ccdExposure.variance.array[noData] = 0.0
2382
2383 ccdExposure.metadata["LSST ISR FLAT APPLIED"] = True
2384 ccdExposure.metadata["LSST ISR FLAT SOURCE"] = flat.metadata.get("FLATSRC", "UNKNOWN")
2385
2386 # Pixel values for masked regions are set here
2387 preInterpExp = None
2388 if self.config.doSaveInterpPixels:
2389 preInterpExp = ccdExposure.clone()
2390
2391 if self.config.doSetBadRegions:
2392 self.log.info('Setting values in large contiguous bad regions.')
2393 self.setBadRegions(ccdExposure)
2394
2395 if self.config.doInterpolate:
2396 self.log.info("Interpolating masked pixels.")
2397 isrFunctions.interpolateFromMask(
2398 maskedImage=ccdExposure.getMaskedImage(),
2399 fwhm=self.config.brighterFatterFwhmForInterpolation,
2400 growSaturatedFootprints=self.config.growSaturationFootprintSize,
2401 maskNameList=list(self.config.maskListToInterpolate),
2402 useLegacyInterp=self.config.useLegacyInterp,
2403 )
2404
2405 # Calculate amp offset corrections within the CCD.
2406 if self.config.doAmpOffset:
2407 if self.config.ampOffset.doApplyAmpOffset:
2408 self.log.info("Measuring and applying amp offset corrections.")
2409 else:
2410 self.log.info("Measuring amp offset corrections only, without applying them.")
2411 self.ampOffset.run(ccdExposure)
2412
2413 # Calculate standard image quality statistics
2414 if self.config.doStandardStatistics:
2415 metadata = ccdExposure.metadata
2416 for amp in detector:
2417 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
2418 ampName = amp.getName()
2419 metadata[f"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels(
2420 ampExposure.getMaskedImage(),
2421 [self.config.saturatedMaskName]
2422 )
2423 metadata[f"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels(
2424 ampExposure.getMaskedImage(),
2425 ["BAD"]
2426 )
2427 metadata[f"LSST ISR MASK SUSPECT {ampName}"] = isrFunctions.countMaskedPixels(
2428 ampExposure.getMaskedImage(),
2429 ["SUSPECT"],
2430 )
2431 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
2432 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP)
2433
2434 metadata[f"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN)
2435 metadata[f"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN)
2436 metadata[f"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP)
2437
2438 k1 = f"LSST ISR FINAL MEDIAN {ampName}"
2439 k2 = f"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
2440 if overscanDetectorConfig.doAnySerialOverscan and k1 in metadata and k2 in metadata:
2441 metadata[f"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2]
2442 else:
2443 metadata[f"LSST ISR LEVEL {ampName}"] = numpy.nan
2444
2445 # calculate additional statistics.
2446 outputStatistics = None
2447 if self.config.doCalculateStatistics:
2448 outputStatistics = self.isrStats.run(ccdExposure,
2449 untrimmedInputExposure=untrimmedCcdExposure,
2450 serialOverscanResults=serialOverscans,
2451 parallelOverscanResults=parallelOverscans,
2452 bias=bias, dark=dark, flat=flat,
2453 ptc=ptc, defects=defects).results
2454
2455 # do image binning.
2456 outputBin1Exposure = None
2457 outputBin2Exposure = None
2458 if self.config.doBinnedExposures:
2459 self.log.info("Creating binned exposures.")
2460 outputBin1Exposure = self.binning.run(
2461 ccdExposure,
2462 binFactor=self.config.binFactor1,
2463 ).outputData
2464 outputBin2Exposure = self.binning.run(
2465 ccdExposure,
2466 binFactor=self.config.binFactor2,
2467 ).outputData
2468
2469 return pipeBase.Struct(
2470 exposure=ccdExposure,
2471
2472 outputBin1Exposure=outputBin1Exposure,
2473 outputBin2Exposure=outputBin2Exposure,
2474
2475 preInterpExposure=preInterpExp,
2476 outputExposure=ccdExposure,
2477 outputStatistics=outputStatistics,
2478 )
Represent a 2-dimensional array of bitmask pixels.
Definition Mask.h:82
runQuantum(self, butlerQC, inputRefs, outputRefs)
ditherCounts(self, exposure, detectorConfig, fallbackSeed=12345)
run(self, ccdExposure, *, dnlLUT=None, bias=None, deferredChargeCalib=None, linearizer=None, ptc=None, gainCorrection=None, crosstalk=None, defects=None, bfKernel=None, dark=None, flat=None, camera=None)
flatContext(self, exp, flat, dark=None)
checkAmpOverscanLevel(self, badAmpDict, exposure, ptc)
getBrighterFatterKernel(self, detector, bfKernel)
darkCorrection(self, exposure, darkExposure, invert=False)
compareUnits(self, calibMetadata, calibName)
overscanCorrection(self, mode, detectorConfig, detector, badAmpDict, ccdExposure)
applyBrighterFatterCorrection(self, ccdExposure, flat, dark, bfKernel, brighterFatterApplyGain, bfGains)
addVariancePlane(self, exposure, detector)
checkAmpNoise(self, badAmpDict, exposure, ptc)
diffNonLinearCorrection(self, ccdExposure, dnlLUT, **kwargs)
maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR')
maskDefects(self, exposure, defectBaseList)
checkAllBadAmps(self, badAmpDict, detector)
maskITLSatEdgesAndColumns(self, exposure, badAmpDict)
maskSaturatedPixels(self, badAmpDict, ccdExposure, detector, detectorConfig, ptc=None)
maskFullAmplifiers(self, ccdExposure, detector, defects, gains=None)
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Definition Exposure.h:459
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
Definition binImage.cc:44
compareCameraKeywords(doRaiseOnCalibMismatch, cameraKeywordsToCompare, exposureMetadata, calib, calibName, log=None)
size_t maskNans(afw::image::MaskedImage< PixelT > const &mi, afw::image::MaskPixel maskVal, afw::image::MaskPixel allow=0)
Mask NANs in an image.
Definition Isr.cc:35