LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.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.meas.algorithms.detection import SourceDetectionTask
18
19from .ampOffset import AmpOffsetTask
20from .binExposureTask import BinExposureTask
21from .overscan import SerialOverscanCorrectionTask, ParallelOverscanCorrectionTask
22from .overscanAmpConfig import OverscanCameraConfig
23from .assembleCcdTask import AssembleCcdTask
24from .deferredCharge import DeferredChargeTask
25from .crosstalk import CrosstalkTask
26from .masking import MaskingTask
27from .isrStatistics import IsrStatisticsTask
28from .isr import maskNans
29from .ptcDataset import PhotonTransferCurveDataset
30from .isrFunctions import isTrimmedExposure
31
32
33class IsrTaskLSSTConnections(pipeBase.PipelineTaskConnections,
34 dimensions={"instrument", "exposure", "detector"},
35 defaultTemplates={}):
36 ccdExposure = cT.Input(
37 name="raw",
38 doc="Input exposure to process.",
39 storageClass="Exposure",
40 dimensions=["instrument", "exposure", "detector"],
41 )
42 camera = cT.PrerequisiteInput(
43 name="camera",
44 storageClass="Camera",
45 doc="Input camera to construct complete exposures.",
46 dimensions=["instrument"],
47 isCalibration=True,
48 )
49 dnlLUT = cT.PrerequisiteInput(
50 name="dnlLUT",
51 doc="Look-up table for differential non-linearity.",
52 storageClass="IsrCalib",
53 dimensions=["instrument", "exposure", "detector"],
54 isCalibration=True,
55 # TODO DM 36636
56 )
57 bias = cT.PrerequisiteInput(
58 name="bias",
59 doc="Input bias calibration.",
60 storageClass="ExposureF",
61 dimensions=["instrument", "detector"],
62 isCalibration=True,
63 )
64 deferredChargeCalib = cT.PrerequisiteInput(
65 name="cpCtiCalib",
66 doc="Deferred charge/CTI correction dataset.",
67 storageClass="IsrCalib",
68 dimensions=["instrument", "detector"],
69 isCalibration=True,
70 )
71 linearizer = cT.PrerequisiteInput(
72 name='linearizer',
73 storageClass="Linearizer",
74 doc="Linearity correction calibration.",
75 dimensions=["instrument", "detector"],
76 isCalibration=True,
77 )
78 ptc = cT.PrerequisiteInput(
79 name="ptc",
80 doc="Input Photon Transfer Curve dataset",
81 storageClass="PhotonTransferCurveDataset",
82 dimensions=["instrument", "detector"],
83 isCalibration=True,
84 )
85 crosstalk = cT.PrerequisiteInput(
86 name="crosstalk",
87 doc="Input crosstalk object",
88 storageClass="CrosstalkCalib",
89 dimensions=["instrument", "detector"],
90 isCalibration=True,
91 )
92 defects = cT.PrerequisiteInput(
93 name='defects',
94 doc="Input defect tables.",
95 storageClass="Defects",
96 dimensions=["instrument", "detector"],
97 isCalibration=True,
98 )
99 bfKernel = cT.PrerequisiteInput(
100 name="bfk",
101 doc="Complete kernel + gain solutions.",
102 storageClass="BrighterFatterKernel",
103 dimensions=["instrument", "detector"],
104 isCalibration=True,
105 )
106 dark = cT.PrerequisiteInput(
107 name='dark',
108 doc="Input dark calibration.",
109 storageClass="ExposureF",
110 dimensions=["instrument", "detector"],
111 isCalibration=True,
112 )
113 flat = cT.PrerequisiteInput(
114 name="flat",
115 doc="Input flat calibration.",
116 storageClass="ExposureF",
117 dimensions=["instrument", "detector", "physical_filter"],
118 isCalibration=True,
119 )
120 outputExposure = cT.Output(
121 name='postISRCCD',
122 doc="Output ISR processed exposure.",
123 storageClass="Exposure",
124 dimensions=["instrument", "exposure", "detector"],
125 )
126 preInterpExposure = cT.Output(
127 name='preInterpISRCCD',
128 doc="Output ISR processed exposure, with pixels left uninterpolated.",
129 storageClass="ExposureF",
130 dimensions=["instrument", "exposure", "detector"],
131 )
132 outputBin1Exposure = cT.Output(
133 name="postIsrBin1",
134 doc="First binned image.",
135 storageClass="ExposureF",
136 dimensions=["instrument", "exposure", "detector"],
137 )
138 outputBin2Exposure = cT.Output(
139 name="postIsrBin2",
140 doc="Second binned image.",
141 storageClass="ExposureF",
142 dimensions=["instrument", "exposure", "detector"],
143 )
144
145 outputStatistics = cT.Output(
146 name="isrStatistics",
147 doc="Output of additional statistics table.",
148 storageClass="StructuredDataDict",
149 dimensions=["instrument", "exposure", "detector"],
150 )
151
152 def __init__(self, *, config=None):
153 super().__init__(config=config)
154
155 if config.doBootstrap:
156 del self.ptc
157 if config.doDiffNonLinearCorrection is not True:
158 del self.dnlLUT
159 if config.doBias is not True:
160 del self.bias
161 if config.doDeferredCharge is not True:
162 del self.deferredChargeCalib
163 if config.doLinearize is not True:
164 del self.linearizer
165 if not config.doCrosstalk:
166 del self.crosstalk
167 if config.doDefect is not True:
168 del self.defects
169 if config.doBrighterFatter is not True:
170 del self.bfKernel
171 if config.doDark is not True:
172 del self.dark
173 if config.doFlat is not True:
174 del self.flat
175
176 if config.doBinnedExposures is not True:
177 del self.outputBin1Exposure
178 del self.outputBin2Exposure
179 if config.doSaveInterpPixels is not True:
180 del self.preInterpExposure
181
182 if config.doCalculateStatistics is not True:
183 del self.outputStatistics
184
185
186class IsrTaskLSSTConfig(pipeBase.PipelineTaskConfig,
187 pipelineConnections=IsrTaskLSSTConnections):
188 """Configuration parameters for IsrTaskLSST.
189
190 Items are grouped in the order in which they are executed by the task.
191 """
192 expectWcs = pexConfig.Field(
193 dtype=bool,
194 default=True,
195 doc="Expect input science images to have a WCS (set False for e.g. spectrographs)."
196 )
197 qa = pexConfig.ConfigField(
198 dtype=isrQa.IsrQaConfig,
199 doc="QA related configuration options.",
200 )
201 doHeaderProvenance = pexConfig.Field(
202 dtype=bool,
203 default=True,
204 doc="Write calibration identifiers into output exposure header.",
205 )
206
207 # Calib checking configuration:
208 doRaiseOnCalibMismatch = pexConfig.Field(
209 dtype=bool,
210 default=False,
211 doc="Should IsrTaskLSST halt if exposure and calibration header values do not match?",
212 )
213 cameraKeywordsToCompare = pexConfig.ListField(
214 dtype=str,
215 doc="List of header keywords to compare between exposure and calibrations.",
216 default=[],
217 )
218
219 # Differential non-linearity correction.
220 doDiffNonLinearCorrection = pexConfig.Field(
221 dtype=bool,
222 doc="Do differential non-linearity correction?",
223 default=False,
224 )
225
226 doBootstrap = pexConfig.Field(
227 dtype=bool,
228 default=False,
229 doc="Is this task to be run in a ``bootstrap`` fashion that does not require "
230 "a PTC or full calibrations?",
231 )
232
233 overscanCamera = pexConfig.ConfigField(
234 dtype=OverscanCameraConfig,
235 doc="Per-detector and per-amplifier overscan configurations.",
236 )
237
238 # Amplifier to CCD assembly configuration.
239 doAssembleCcd = pexConfig.Field(
240 dtype=bool,
241 default=True,
242 doc="Assemble amp-level exposures into a ccd-level exposure?"
243 )
244 assembleCcd = pexConfig.ConfigurableField(
245 target=AssembleCcdTask,
246 doc="CCD assembly task.",
247 )
248
249 # Bias subtraction.
250 doBias = pexConfig.Field(
251 dtype=bool,
252 doc="Apply bias frame correction?",
253 default=True,
254 )
255
256 # Deferred charge correction.
257 doDeferredCharge = pexConfig.Field(
258 dtype=bool,
259 doc="Apply deferred charge correction?",
260 default=True,
261 )
262 deferredChargeCorrection = pexConfig.ConfigurableField(
263 target=DeferredChargeTask,
264 doc="Deferred charge correction task.",
265 )
266
267 # Linearization.
268 doLinearize = pexConfig.Field(
269 dtype=bool,
270 doc="Correct for nonlinearity of the detector's response?",
271 default=True,
272 )
273
274 # Gains.
275 doCorrectGains = pexConfig.Field(
276 dtype=bool,
277 doc="Apply temperature correction to the gains?",
278 default=False,
279 )
280 doApplyGains = pexConfig.Field(
281 dtype=bool,
282 doc="Apply gains to the image?",
283 default=True,
284 )
285
286 # Variance construction.
287 doVariance = pexConfig.Field(
288 dtype=bool,
289 doc="Calculate variance?",
290 default=True
291 )
292 maskNegativeVariance = pexConfig.Field(
293 dtype=bool,
294 doc="Mask pixels that claim a negative variance. This likely indicates a failure "
295 "in the measurement of the overscan at an edge due to the data falling off faster "
296 "than the overscan model can account for it.",
297 default=True,
298 )
299 negativeVarianceMaskName = pexConfig.Field(
300 dtype=str,
301 doc="Mask plane to use to mark pixels with negative variance, if `maskNegativeVariance` is True.",
302 default="BAD",
303 )
304 doSaturation = pexConfig.Field(
305 dtype=bool,
306 doc="Mask saturated pixels? NB: this is totally independent of the"
307 " interpolation option - this is ONLY setting the bits in the mask."
308 " To have them interpolated make sure doSaturationInterpolation=True",
309 default=True,
310 )
311 saturatedMaskName = pexConfig.Field(
312 dtype=str,
313 doc="Name of mask plane to use in saturation detection and interpolation.",
314 default="SAT",
315 )
316 defaultSaturationSource = pexConfig.ChoiceField(
317 dtype=str,
318 doc="Source to retrieve default amp-level saturation values.",
319 allowed={
320 "NONE": "No default saturation values; only config overrides will be used.",
321 "CAMERAMODEL": "Use the default from the camera model (old defaults).",
322 "PTCTURNOFF": "Use the ptcTurnoff value as the saturation level.",
323 },
324 default="PTCTURNOFF",
325 )
326 doSuspect = pexConfig.Field(
327 dtype=bool,
328 doc="Mask suspect pixels?",
329 default=True,
330 )
331 suspectMaskName = pexConfig.Field(
332 dtype=str,
333 doc="Name of mask plane to use for suspect pixels.",
334 default="SUSPECT",
335 )
336 defaultSuspectSource = pexConfig.ChoiceField(
337 dtype=str,
338 doc="Source to retrieve default amp-level suspect values.",
339 allowed={
340 "NONE": "No default suspect values; only config overrides will be used.",
341 "CAMERAMODEL": "Use the default from the camera model (old defaults).",
342 "PTCTURNOFF": "Use the ptcTurnoff value as the suspect level.",
343 },
344 default="PTCTURNOFF",
345 )
346
347 # Crosstalk.
348 doCrosstalk = pexConfig.Field(
349 dtype=bool,
350 doc="Apply intra-CCD crosstalk correction?",
351 default=True,
352 )
353 crosstalk = pexConfig.ConfigurableField(
354 target=CrosstalkTask,
355 doc="Intra-CCD crosstalk correction.",
356 )
357
358 # Masking options.
359 doDefect = pexConfig.Field(
360 dtype=bool,
361 doc="Apply correction for CCD defects, e.g. hot pixels?",
362 default=True,
363 )
364 doNanMasking = pexConfig.Field(
365 dtype=bool,
366 doc="Mask non-finite (NAN, inf) pixels.",
367 default=True,
368 )
369 doWidenSaturationTrails = pexConfig.Field(
370 dtype=bool,
371 doc="Widen bleed trails based on their width.",
372 default=False,
373 )
374 masking = pexConfig.ConfigurableField(
375 target=MaskingTask,
376 doc="Masking task."
377 )
378
379 # Interpolation options.
380 doInterpolate = pexConfig.Field(
381 dtype=bool,
382 doc="Interpolate masked pixels?",
383 default=True,
384 )
385 maskListToInterpolate = pexConfig.ListField(
386 dtype=str,
387 doc="List of mask planes that should be interpolated.",
388 default=['SAT', 'BAD'],
389 )
390 doSaveInterpPixels = pexConfig.Field(
391 dtype=bool,
392 doc="Save a copy of the pre-interpolated pixel values?",
393 default=False,
394 )
395 useLegacyInterp = pexConfig.Field(
396 dtype=bool,
397 doc="Use the legacy interpolation algorithm. If False use Gaussian Process.",
398 default=True,
399 )
400
401 # Amp offset correction.
402 doAmpOffset = pexConfig.Field(
403 doc="Calculate amp offset corrections?",
404 dtype=bool,
405 default=False,
406 )
407 ampOffset = pexConfig.ConfigurableField(
408 doc="Amp offset correction task.",
409 target=AmpOffsetTask,
410 )
411
412 # Initial masking options.
413 doSetBadRegions = pexConfig.Field(
414 dtype=bool,
415 doc="Should we set the level of all BAD patches of the chip to the chip's average value?",
416 default=True,
417 )
418
419 # Brighter-Fatter correction.
420 doBrighterFatter = pexConfig.Field(
421 dtype=bool,
422 doc="Apply the brighter-fatter correction?",
423 default=True,
424 )
425 brighterFatterLevel = pexConfig.ChoiceField(
426 dtype=str,
427 doc="The level at which to correct for brighter-fatter.",
428 allowed={
429 "AMP": "Every amplifier treated separately.",
430 "DETECTOR": "One kernel per detector.",
431 },
432 default="DETECTOR",
433 )
434 brighterFatterMaxIter = pexConfig.Field(
435 dtype=int,
436 doc="Maximum number of iterations for the brighter-fatter correction.",
437 default=10,
438 )
439 brighterFatterThreshold = pexConfig.Field(
440 dtype=float,
441 doc="Threshold used to stop iterating the brighter-fatter correction. It is the "
442 "absolute value of the difference between the current corrected image and the one "
443 "from the previous iteration summed over all the pixels.",
444 default=1000,
445 )
446 brighterFatterMaskListToInterpolate = pexConfig.ListField(
447 dtype=str,
448 doc="List of mask planes that should be interpolated over when applying the brighter-fatter "
449 "correction.",
450 default=["SAT", "BAD", "NO_DATA", "UNMASKEDNAN"],
451 )
452 brighterFatterMaskGrowSize = pexConfig.Field(
453 dtype=int,
454 doc="Number of pixels to grow the masks listed in config.brighterFatterMaskListToInterpolate "
455 "when brighter-fatter correction is applied.",
456 default=2,
457 )
458 brighterFatterFwhmForInterpolation = pexConfig.Field(
459 dtype=float,
460 doc="FWHM of PSF in arcseconds used for interpolation in brighter-fatter correction "
461 "(currently unused).",
462 default=1.0,
463 )
464 growSaturationFootprintSize = pexConfig.Field(
465 dtype=int,
466 doc="Number of pixels by which to grow the saturation footprints.",
467 default=1,
468 )
469 brighterFatterMaskListToInterpolate = pexConfig.ListField(
470 dtype=str,
471 doc="List of mask planes that should be interpolated over when applying the brighter-fatter "
472 "correction.",
473 default=["SAT", "BAD", "NO_DATA", "UNMASKEDNAN"],
474 )
475
476 # Dark subtraction.
477 doDark = pexConfig.Field(
478 dtype=bool,
479 doc="Apply dark frame correction.",
480 default=True,
481 )
482
483 # Flat correction.
484 doFlat = pexConfig.Field(
485 dtype=bool,
486 doc="Apply flat field correction.",
487 default=True,
488 )
489 flatScalingType = pexConfig.ChoiceField(
490 dtype=str,
491 doc="The method for scaling the flat on the fly.",
492 default='USER',
493 allowed={
494 "USER": "Scale by flatUserScale",
495 "MEAN": "Scale by the inverse of the mean",
496 "MEDIAN": "Scale by the inverse of the median",
497 },
498 )
499 flatUserScale = pexConfig.Field(
500 dtype=float,
501 doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise.",
502 default=1.0,
503 )
504
505 # Calculate image quality statistics?
506 doStandardStatistics = pexConfig.Field(
507 dtype=bool,
508 doc="Should standard image quality statistics be calculated?",
509 default=True,
510 )
511 # Calculate additional statistics?
512 doCalculateStatistics = pexConfig.Field(
513 dtype=bool,
514 doc="Should additional ISR statistics be calculated?",
515 default=True,
516 )
517 isrStats = pexConfig.ConfigurableField(
518 target=IsrStatisticsTask,
519 doc="Task to calculate additional statistics.",
520 )
521
522 # Make binned images?
523 doBinnedExposures = pexConfig.Field(
524 dtype=bool,
525 doc="Should binned exposures be calculated?",
526 default=False,
527 )
528 binning = pexConfig.ConfigurableField(
529 target=BinExposureTask,
530 doc="Task to bin the exposure.",
531 )
532 binFactor1 = pexConfig.Field(
533 dtype=int,
534 doc="Binning factor for first binned exposure. This is intended for a finely binned output.",
535 default=8,
536 check=lambda x: x > 1,
537 )
538 binFactor2 = pexConfig.Field(
539 dtype=int,
540 doc="Binning factor for second binned exposure. This is intended for a coarsely binned output.",
541 default=64,
542 check=lambda x: x > 1,
543 )
544
545 def validate(self):
546 super().validate()
547
548 if self.doBootstrap:
549 # Additional checks in bootstrap (no PTC/gains) mode.
550 if self.doApplyGains:
551 raise ValueError("Cannot run task with doBootstrap=True and doApplyGains=True.")
552 if self.doCorrectGains:
553 raise ValueError("Cannot run task with doBootstrap=True and doCorrectGains=True.")
554 if self.doCrosstalk and self.crosstalkcrosstalk.doQuadraticCrosstalkCorrection:
555 raise ValueError("Cannot apply quadratic crosstalk correction with doBootstrap=True.")
556
557 # if self.doCalculateStatistics and self.isrStats.doCtiStatistics:
558 # DM-41912: Implement doApplyGains in LSST IsrTask
559 # if self.doApplyGains !=
560 # self.isrStats.doApplyGainsForCtiStatistics:
561 # raise ValueError("doApplyGains must match
562 # isrStats.applyGainForCtiStatistics.")
563
564 def setDefaults(self):
565 super().setDefaults()
566
567
568class IsrTaskLSST(pipeBase.PipelineTask):
569 ConfigClass = IsrTaskLSSTConfig
570 _DefaultName = "isrLSST"
571
572 def __init__(self, **kwargs):
573 super().__init__(**kwargs)
574 self.makeSubtask("assembleCcd")
575 self.makeSubtask("deferredChargeCorrection")
576 self.makeSubtask("crosstalk")
577 self.makeSubtask("masking")
578 self.makeSubtask("isrStats")
579 self.makeSubtask("ampOffset")
580 self.makeSubtask("binning")
581
582 def runQuantum(self, butlerQC, inputRefs, outputRefs):
583
584 inputs = butlerQC.get(inputRefs)
585 self.validateInput(inputs)
586
587 if self.config.doHeaderProvenance:
588 # Add calibration provenanace info to header.
589 exposureMetadata = inputs['ccdExposure'].metadata
590 for inputName in sorted(list(inputs.keys())):
591 reference = getattr(inputRefs, inputName, None)
592 if reference is not None and hasattr(reference, "run"):
593 runKey = f"LSST CALIB RUN {inputName.upper()}"
594 runValue = reference.run
595 idKey = f"LSST CALIB UUID {inputName.upper()}"
596 idValue = str(reference.id)
597 dateKey = f"LSST CALIB DATE {inputName.upper()}"
598 dateValue = self.extractCalibDate(inputs[inputName])
599
600 exposureMetadata[runKey] = runValue
601 exposureMetadata[idKey] = idValue
602 exposureMetadata[dateKey] = dateValue
603
604 outputs = self.run(**inputs)
605 butlerQC.put(outputs, outputRefs)
606
607 def validateInput(self, inputs):
608 """
609 This is a check that all the inputs required by the config
610 are available.
611 """
612
613 inputMap = {'dnlLUT': self.config.doDiffNonLinearCorrection,
614 'bias': self.config.doBias,
615 'deferredChargeCalib': self.config.doDeferredCharge,
616 'linearizer': self.config.doLinearize,
617 'ptc': self.config.doApplyGains,
618 'crosstalk': self.config.doCrosstalk,
619 'defects': self.config.doDefect,
620 'bfKernel': self.config.doBrighterFatter,
621 'dark': self.config.doDark,
622 }
623
624 for calibrationFile, configValue in inputMap.items():
625 if configValue and inputs[calibrationFile] is None:
626 raise RuntimeError("Must supply ", calibrationFile)
627
628 def diffNonLinearCorrection(self, ccdExposure, dnlLUT, **kwargs):
629 # TODO DM 36636
630 # isrFunctions.diffNonLinearCorrection
631 pass
632
633 def maskFullAmplifiers(self, ccdExposure, detector, defects, gains=None):
634 """
635 Check for fully masked bad amplifiers and mask them.
636
637 This includes defects which cover full amplifiers, as well
638 as amplifiers with nan gain values which should be used
639 if self.config.doApplyGains=True.
640
641 Full defect masking happens later to allow for defects which
642 cross amplifier boundaries.
643
644 Parameters
645 ----------
646 ccdExposure : `lsst.afw.image.Exposure`
647 Input exposure to be masked.
648 detector : `lsst.afw.cameraGeom.Detector`
649 Detector object.
650 defects : `lsst.ip.isr.Defects`
651 List of defects. Used to determine if an entire
652 amplifier is bad.
653 gains : `dict` [`str`, `float`], optional
654 Dictionary of gains to check if
655 self.config.doApplyGains=True.
656
657 Returns
658 -------
659 badAmpDict : `str`[`bool`]
660 Dictionary of amplifiers, keyed by name, value is True if
661 amplifier is fully masked.
662 """
663 badAmpDict = {}
664
665 maskedImage = ccdExposure.getMaskedImage()
666
667 for amp in detector:
668 ampName = amp.getName()
669 badAmpDict[ampName] = False
670
671 # Check if entire amp region is defined as a defect
672 # NB: need to use amp.getBBox() for correct comparison with current
673 # defects definition.
674 if defects is not None:
675 badAmpDict[ampName] = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects]))
676
677 if badAmpDict[ampName]:
678 self.log.warning("Amplifier %s is bad (completely covered with defects)", ampName)
679
680 if gains is not None and self.config.doApplyGains:
681 if not math.isfinite(gains[ampName]):
682 badAmpDict[ampName] = True
683
684 self.log.warning("Amplifier %s is bad (non-finite gain)", ampName)
685
686 # In the case of a bad amp, we will set mask to "BAD"
687 # (here use amp.getRawBBox() for correct association with pixels in
688 # current ccdExposure).
689 if badAmpDict[ampName]:
690 dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
691 afwImage.PARENT)
692 maskView = dataView.getMask()
693 maskView |= maskView.getPlaneBitMask("BAD")
694 del maskView
695
696 return badAmpDict
697
698 def maskSaturatedPixels(self, badAmpDict, ccdExposure, detector, detectorConfig, ptc=None):
699 """
700 Mask SATURATED and SUSPECT pixels and check if any amplifiers
701 are fully masked.
702
703 Parameters
704 ----------
705 badAmpDict : `str` [`bool`]
706 Dictionary of amplifiers, keyed by name, value is True if
707 amplifier is fully masked.
708 ccdExposure : `lsst.afw.image.Exposure`
709 Input exposure to be masked.
710 detector : `lsst.afw.cameraGeom.Detector`
711 Detector object.
712 defects : `lsst.ip.isr.Defects`
713 List of defects. Used to determine if an entire
714 amplifier is bad.
715 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
716 Per-amplifier configurations.
717 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
718 PTC dataset (used if configured to use PTCTURNOFF).
719
720 Returns
721 -------
722 badAmpDict : `str`[`bool`]
723 Dictionary of amplifiers, keyed by name.
724 """
725 maskedImage = ccdExposure.getMaskedImage()
726
727 metadata = ccdExposure.metadata
728
729 if self.config.doSaturation and self.config.defaultSaturationSource == "PTCTURNOFF" and ptc is None:
730 raise RuntimeError("Must provide ptc if using PTCTURNOFF as saturation source.")
731 if self.config.doSuspect and self.config.defaultSuspectSource == "PTCTURNOFF" and ptc is None:
732 raise RuntimeError("Must provide ptc if using PTCTURNOFF as suspect source.")
733
734 for amp in detector:
735 ampName = amp.getName()
736
737 ampConfig = detectorConfig.getOverscanAmpConfig(amp)
738
739 if badAmpDict[ampName]:
740 # No need to check fully bad amplifiers.
741 continue
742
743 # Mask saturated and suspect pixels.
744 limits = {}
745 if self.config.doSaturation:
746 if self.config.defaultSaturationSource == "PTCTURNOFF":
747 limits.update({self.config.saturatedMaskName: ptc.ptcTurnoff[amp.getName()]})
748 elif self.config.defaultSaturationSource == "CAMERAMODEL":
749 # Set to the default from the camera model.
750 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
751 elif self.config.defaultSaturationSource == "NONE":
752 limits.update({self.config.saturatedMaskName: 1e100})
753
754 # And update if it is set in the config.
755 if math.isfinite(ampConfig.saturation):
756 limits.update({self.config.saturatedMaskName: ampConfig.saturation})
757 metadata[f"LSST ISR SATURATION LEVEL {ampName}"] = limits[self.config.saturatedMaskName]
758
759 if self.config.doSuspect:
760 if self.config.defaultSuspectSource == "PTCTURNOFF":
761 limits.update({self.config.suspectMaskName: ptc.ptcTurnoff[amp.getName()]})
762 elif self.config.defaultSuspectSource == "CAMERAMODEL":
763 # Set to the default from the camera model.
764 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
765 elif self.config.defaultSuspectSource == "NONE":
766 limits.update({self.config.suspectMaskName: 1e100})
767
768 # And update if it set in the config.
769 if math.isfinite(ampConfig.suspectLevel):
770 limits.update({self.config.suspectMaskName: ampConfig.suspectLevel})
771 metadata[f"LSST ISR SUSPECT LEVEL {ampName}"] = limits[self.config.suspectMaskName]
772
773 for maskName, maskThreshold in limits.items():
774 if not math.isnan(maskThreshold):
775 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
776 isrFunctions.makeThresholdMask(
777 maskedImage=dataView,
778 threshold=maskThreshold,
779 growFootprints=0,
780 maskName=maskName
781 )
782
783 # Determine if we've fully masked this amplifier with SUSPECT and
784 # SAT pixels.
785 maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
786 afwImage.PARENT)
787 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
788 self.config.suspectMaskName])
789 if numpy.all(maskView.getArray() & maskVal > 0):
790 self.log.warning("Amplifier %s is bad (completely SATURATED or SUSPECT)", ampName)
791 badAmpDict[ampName] = True
792 maskView |= maskView.getPlaneBitMask("BAD")
793
794 return badAmpDict
795
796 def overscanCorrection(self, mode, detectorConfig, detector, badAmpDict, ccdExposure):
797 """Apply serial overscan correction in place to all amps.
798
799 The actual overscan subtraction is performed by the
800 `lsst.ip.isr.overscan.OverscanTask`, which is called here.
801
802 Parameters
803 ----------
804 mode : `str`
805 Must be `SERIAL` or `PARALLEL`.
806 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
807 Per-amplifier configurations.
808 detector : `lsst.afw.cameraGeom.Detector`
809 Detector object.
810 badAmpDict : `dict`
811 Dictionary of amp name to whether it is a bad amp.
812 ccdExposure : `lsst.afw.image.Exposure`
813 Exposure to have overscan correction performed.
814
815 Returns
816 -------
817 overscans : `list` [`lsst.pipe.base.Struct` or None]
818 Overscan measurements (always in adu).
819 Each result struct has components:
820
821 ``imageFit``
822 Value or fit subtracted from the amplifier image data.
823 (scalar or `lsst.afw.image.Image`)
824 ``overscanFit``
825 Value or fit subtracted from the overscan image data.
826 (scalar or `lsst.afw.image.Image`)
827 ``overscanImage``
828 Image of the overscan region with the overscan
829 correction applied. This quantity is used to estimate
830 the amplifier read noise empirically.
831 (`lsst.afw.image.Image`)
832 ``overscanMean``
833 Mean overscan fit value. (`float`)
834 ``overscanMedian``
835 Median overscan fit value. (`float`)
836 ``overscanSigma``
837 Clipped standard deviation of the overscan fit. (`float`)
838 ``residualMean``
839 Mean of the overscan after fit subtraction. (`float`)
840 ``residualMedian``
841 Median of the overscan after fit subtraction. (`float`)
842 ``residualSigma``
843 Clipped standard deviation of the overscan after fit
844 subtraction. (`float`)
845
846 See Also
847 --------
848 lsst.ip.isr.overscan.OverscanTask
849 """
850 if mode not in ["SERIAL", "PARALLEL"]:
851 raise ValueError("Mode must be SERIAL or PARALLEL")
852
853 # This returns a list in amp order, with None for uncorrected amps.
854 overscans = []
855
856 for i, amp in enumerate(detector):
857 ampName = amp.getName()
858
859 ampConfig = detectorConfig.getOverscanAmpConfig(amp)
860
861 if mode == "SERIAL" and not ampConfig.doSerialOverscan:
862 self.log.debug(
863 "ISR_OSCAN: Amplifier %s/%s configured to skip serial overscan.",
864 detector.getName(),
865 ampName,
866 )
867 results = None
868 elif mode == "PARALLEL" and not ampConfig.doParallelOverscan:
869 self.log.debug(
870 "ISR_OSCAN: Amplifier %s configured to skip parallel overscan.",
871 detector.getName(),
872 ampName,
873 )
874 results = None
875 elif badAmpDict[ampName] or not ccdExposure.getBBox().contains(amp.getBBox()):
876 results = None
877 else:
878 # This check is to confirm that we are not trying to run
879 # overscan on an already trimmed image.
880 if isTrimmedExposure(ccdExposure):
881 self.log.warning(
882 "ISR_OSCAN: No overscan region for amp %s. Not performing overscan correction.",
883 ampName,
884 )
885 results = None
886 else:
887 if mode == "SERIAL":
888 # We need to set up the subtask here with a custom
889 # configuration.
890 serialOverscan = SerialOverscanCorrectionTask(config=ampConfig.serialOverscanConfig)
891 results = serialOverscan.run(ccdExposure, amp)
892 else:
893 config = ampConfig.parallelOverscanConfig
894 parallelOverscan = ParallelOverscanCorrectionTask(
895 config=config,
896 )
897
898 metadata = ccdExposure.metadata
899
900 # We need to know the saturation level that was used
901 # for the parallel overscan masking. If it isn't set
902 # then the configured parallelOverscanSaturationLevel
903 # will be used instead (assuming
904 # doParallelOverscanSaturation is True). Note that
905 # this will have the correct units (adu or electron)
906 # depending on whether the gain has been applied.
907 if self.config.doSaturation:
908 saturationLevel = metadata[f"LSST ISR SATURATION LEVEL {amp.getName()}"]
909 saturationLevel *= config.parallelOverscanSaturationLevelAdjustmentFactor
910 else:
911 saturationLevel = config.parallelOverscanSaturationLevel
912 if ccdExposure.metadata["LSST ISR UNITS"] == "electron":
913 # Need to convert to electron from adu.
914 saturationLevel *= metadata[f"LSST ISR GAIN {amp.getName()}"]
915
916 self.log.debug(
917 "Using saturation level of %.2f for parallel overscan amp %s",
918 saturationLevel,
919 amp.getName(),
920 )
921
922 parallelOverscan.maskParallelOverscanAmp(
923 ccdExposure,
924 amp,
925 saturationLevel=saturationLevel,
926 )
927
928 results = parallelOverscan.run(ccdExposure, amp)
929
930 metadata = ccdExposure.metadata
931 keyBase = "LSST ISR OVERSCAN"
932
933 # The overscan is always in adu for the serial mode,
934 # but, it may be electron in the parallel mode if
935 # doApplyGains==True. If doApplyGains==True, then the
936 # gains are applied to the untrimmed image, so the
937 # overscan statistics units here will always match the
938 # units of the image at this point.
939 metadata[f"{keyBase} {mode} UNITS"] = ccdExposure.metadata["LSST ISR UNITS"]
940 metadata[f"{keyBase} {mode} MEAN {ampName}"] = results.overscanMean
941 metadata[f"{keyBase} {mode} MEDIAN {ampName}"] = results.overscanMedian
942 metadata[f"{keyBase} {mode} STDEV {ampName}"] = results.overscanSigma
943
944 metadata[f"{keyBase} RESIDUAL {mode} MEAN {ampName}"] = results.residualMean
945 metadata[f"{keyBase} RESIDUAL {mode} MEDIAN {ampName}"] = results.residualMedian
946 metadata[f"{keyBase} RESIDUAL {mode} STDEV {ampName}"] = results.residualSigma
947
948 overscans.append(results)
949
950 # Question: should this be finer grained?
951 ccdExposure.metadata.set("OVERSCAN", "Overscan corrected")
952
953 return overscans
954
955 def correctGains(self, exposure, ptc, gains):
956 # TODO DM 36639
957 gains = []
958 readNoise = []
959
960 return gains, readNoise
961
962 def maskNegativeVariance(self, exposure):
963 """Identify and mask pixels with negative variance values.
964
965 Parameters
966 ----------
967 exposure : `lsst.afw.image.Exposure`
968 Exposure to process.
969
970 See Also
971 --------
972 lsst.ip.isr.isrFunctions.updateVariance
973 """
974 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName)
975 bad = numpy.where(exposure.getVariance().getArray() <= 0.0)
976 exposure.mask.array[bad] |= maskPlane
977
978 def addVariancePlane(self, exposure, detector):
979 """Add the variance plane to the image.
980
981 The gain and read noise per amp must have been set in the
982 exposure metadata as ``LSST ISR GAIN ampName`` and
983 ``LSST ISR READNOISE ampName`` with the units of the image.
984 Unit conversions for the variance plane will be done as
985 necessary based on the exposure units.
986
987 The units of the variance plane will always be of the same
988 type as the units of the input image itself
989 (``LSST ISR UNITS``^2).
990
991 Parameters
992 ----------
993 exposure : `lsst.afw.image.Exposure`
994 The exposure to add the variance plane.
995 detector : `lsst.afw.cameraGeom.Detector`
996 Detector with geometry info.
997 """
998 # NOTE: this will fail if the exposure is not trimmed.
999 if not isTrimmedExposure(exposure):
1000 raise RuntimeError("Exposure must be trimmed to add variance plane.")
1001
1002 isElectrons = (exposure.metadata["LSST ISR UNITS"] == "electron")
1003
1004 for amp in detector:
1005 if exposure.getBBox().contains(amp.getBBox()):
1006 self.log.debug("Constructing variance map for amplifer %s.", amp.getName())
1007 ampExposure = exposure.Factory(exposure, amp.getBBox())
1008
1009 # The effective gain is 1.0 if we are in electron units.
1010 # The metadata read noise is in the same units as the image.
1011 gain = exposure.metadata[f"LSST ISR GAIN {amp.getName()}"] if not isElectrons else 1.0
1012 readNoise = exposure.metadata[f"LSST ISR READNOISE {amp.getName()}"]
1013
1014 isrFunctions.updateVariance(
1015 maskedImage=ampExposure.maskedImage,
1016 gain=gain,
1017 readNoise=readNoise,
1018 )
1019
1020 if self.config.qa is not None and self.config.qa.saveStats is True:
1021 qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
1022 afwMath.MEDIAN | afwMath.STDEVCLIP)
1023 self.log.debug(" Variance stats for amplifer %s: %f +/- %f.",
1024 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1025 qaStats.getValue(afwMath.STDEVCLIP))
1026
1027 if self.config.maskNegativeVariance:
1028 self.maskNegativeVariance(exposure)
1029
1030 def maskDefects(self, exposure, defectBaseList):
1031 """Mask defects using mask plane "BAD", in place.
1032
1033 Parameters
1034 ----------
1035 exposure : `lsst.afw.image.Exposure`
1036 Exposure to process.
1037
1038 defectBaseList : defect-type
1039 List of defects to mask. Can be of type `lsst.ip.isr.Defects`
1040 or `list` of `lsst.afw.image.DefectBase`.
1041 """
1042 maskedImage = exposure.getMaskedImage()
1043 if not isinstance(defectBaseList, Defects):
1044 # Promotes DefectBase to Defect
1045 defectList = Defects(defectBaseList)
1046 else:
1047 defectList = defectBaseList
1048 defectList.maskPixels(maskedImage, maskName="BAD")
1049
1050 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'):
1051 """Mask edge pixels with applicable mask plane.
1052
1053 Parameters
1054 ----------
1055 exposure : `lsst.afw.image.Exposure`
1056 Exposure to process.
1057 numEdgePixels : `int`, optional
1058 Number of edge pixels to mask.
1059 maskPlane : `str`, optional
1060 Mask plane name to use.
1061 level : `str`, optional
1062 Level at which to mask edges.
1063 """
1064 maskedImage = exposure.getMaskedImage()
1065 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane)
1066
1067 if numEdgePixels > 0:
1068 if level == 'DETECTOR':
1069 boxes = [maskedImage.getBBox()]
1070 elif level == 'AMP':
1071 boxes = [amp.getBBox() for amp in exposure.getDetector()]
1072
1073 for box in boxes:
1074 # This makes a bbox numEdgeSuspect pixels smaller than the
1075 # image on each side
1076 subImage = maskedImage[box]
1077 box.grow(-numEdgePixels)
1078 # Mask pixels outside box
1079 SourceDetectionTask.setEdgeBits(
1080 subImage,
1081 box,
1082 maskBitMask)
1083
1084 def maskNan(self, exposure):
1085 """Mask NaNs using mask plane "UNMASKEDNAN", in place.
1086
1087 Parameters
1088 ----------
1089 exposure : `lsst.afw.image.Exposure`
1090 Exposure to process.
1091
1092 Notes
1093 -----
1094 We mask over all non-finite values (NaN, inf), including those
1095 that are masked with other bits (because those may or may not be
1096 interpolated over later, and we want to remove all NaN/infs).
1097 Despite this behaviour, the "UNMASKEDNAN" mask plane is used to
1098 preserve the historical name.
1099 """
1100 maskedImage = exposure.getMaskedImage()
1101
1102 # Find and mask NaNs
1103 maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
1104 maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
1105 numNans = maskNans(maskedImage, maskVal)
1106 self.metadata["NUMNANS"] = numNans
1107 if numNans > 0:
1108 self.log.warning("There were %d unmasked NaNs.", numNans)
1109
1110 def setBadRegions(self, exposure):
1111 """Set bad regions from large contiguous regions.
1112
1113 Parameters
1114 ----------
1115 exposure : `lsst.afw.Exposure`
1116 Exposure to set bad regions.
1117
1118 Notes
1119 -----
1120 Reset and interpolate bad pixels.
1121
1122 Large contiguous bad regions (which should have the BAD mask
1123 bit set) should have their values set to the image median.
1124 This group should include defects and bad amplifiers. As the
1125 area covered by these defects are large, there's little
1126 reason to expect that interpolation would provide a more
1127 useful value.
1128
1129 Smaller defects can be safely interpolated after the larger
1130 regions have had their pixel values reset. This ensures
1131 that the remaining defects adjacent to bad amplifiers (as an
1132 example) do not attempt to interpolate extreme values.
1133 """
1134 badPixelCount, badPixelValue = isrFunctions.setBadRegions(exposure)
1135 if badPixelCount > 0:
1136 self.log.info("Set %d BAD pixels to %f.", badPixelCount, badPixelValue)
1137
1138 @contextmanager
1139 def flatContext(self, exp, flat, dark=None):
1140 """Context manager that applies and removes flats and darks,
1141 if the task is configured to apply them.
1142
1143 Parameters
1144 ----------
1145 exp : `lsst.afw.image.Exposure`
1146 Exposure to process.
1147 flat : `lsst.afw.image.Exposure`
1148 Flat exposure the same size as ``exp``.
1149 dark : `lsst.afw.image.Exposure`, optional
1150 Dark exposure the same size as ``exp``.
1151
1152 Yields
1153 ------
1154 exp : `lsst.afw.image.Exposure`
1155 The flat and dark corrected exposure.
1156 """
1157 if self.config.doDark and dark is not None:
1158 self.darkCorrection(exp, dark)
1159 if self.config.doFlat and flat is not None:
1160 self.flatCorrection(exp, flat)
1161 try:
1162 yield exp
1163 finally:
1164 if self.config.doFlat and flat is not None:
1165 self.flatCorrection(exp, flat, invert=True)
1166 if self.config.doDark and dark is not None:
1167 self.darkCorrection(exp, dark, invert=True)
1168
1169 def getBrighterFatterKernel(self, detector, bfKernel):
1170 detName = detector.getName()
1171
1172 # This is expected to be a dictionary of amp-wise gains.
1173 bfGains = bfKernel.gain
1174 if bfKernel.level == 'DETECTOR':
1175 if detName in bfKernel.detKernels:
1176 bfKernelOut = bfKernel.detKernels[detName]
1177 return bfKernelOut, bfGains
1178 else:
1179 raise RuntimeError("Failed to extract kernel from new-style BF kernel.")
1180 elif bfKernel.level == 'AMP':
1181 self.log.info("Making DETECTOR level kernel from AMP based brighter "
1182 "fatter kernels.")
1183 bfKernel.makeDetectorKernelFromAmpwiseKernels(detName)
1184 bfKernelOut = bfKernel.detKernels[detName]
1185 return bfKernelOut, bfGains
1186
1187 def applyBrighterFatterCorrection(self, ccdExposure, flat, dark, bfKernel, brighterFatterApplyGain,
1188 bfGains):
1189 """Apply a brighter fatter correction to the image using the
1190 method defined in Coulton et al. 2019.
1191
1192 Note that this correction requires that the image is in units
1193 electrons.
1194
1195 Parameters
1196 ----------
1197 ccdExposure : `lsst.afw.image.Exposure`
1198 Exposure to process.
1199 flat : `lsst.afw.image.Exposure`
1200 Flat exposure the same size as ``exp``.
1201 dark : `lsst.afw.image.Exposure`, optional
1202 Dark exposure the same size as ``exp``.
1203 bfKernel : `lsst.ip.isr.BrighterFatterKernel`
1204 The brighter-fatter kernel.
1205 brighterFatterApplyGain : `bool`
1206 Apply the gain to convert the image to electrons?
1207 bfGains : `dict`
1208 The gains to use if brighterFatterApplyGain = True.
1209
1210 Yields
1211 ------
1212 exp : `lsst.afw.image.Exposure`
1213 The flat and dark corrected exposure.
1214 """
1215 interpExp = ccdExposure.clone()
1216
1217 # We need to interpolate before we do B-F. Note that
1218 # brighterFatterFwhmForInterpolation is currently unused.
1219 isrFunctions.interpolateFromMask(
1220 maskedImage=interpExp.getMaskedImage(),
1221 fwhm=self.config.brighterFatterFwhmForInterpolation,
1222 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1223 maskNameList=list(self.config.brighterFatterMaskListToInterpolate),
1224 useLegacyInterp=self.config.useLegacyInterp,
1225 )
1226 bfExp = interpExp.clone()
1227 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1228 self.config.brighterFatterMaxIter,
1229 self.config.brighterFatterThreshold,
1230 brighterFatterApplyGain,
1231 bfGains)
1232 if bfResults[1] == self.config.brighterFatterMaxIter:
1233 self.log.warning("Brighter-fatter correction did not converge, final difference %f.",
1234 bfResults[0])
1235 else:
1236 self.log.info("Finished brighter-fatter correction in %d iterations.",
1237 bfResults[1])
1238
1239 image = ccdExposure.getMaskedImage().getImage()
1240 bfCorr = bfExp.getMaskedImage().getImage()
1241 bfCorr -= interpExp.getMaskedImage().getImage()
1242 image += bfCorr
1243
1244 # Applying the brighter-fatter correction applies a
1245 # convolution to the science image. At the edges this
1246 # convolution may not have sufficient valid pixels to
1247 # produce a valid correction. Mark pixels within the size
1248 # of the brighter-fatter kernel as EDGE to warn of this
1249 # fact.
1250 self.log.info("Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1251 self.maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1252 maskPlane="EDGE")
1253
1254 if self.config.brighterFatterMaskGrowSize > 0:
1255 self.log.info("Growing masks to account for brighter-fatter kernel convolution.")
1256 for maskPlane in self.config.brighterFatterMaskListToInterpolate:
1257 isrFunctions.growMasks(ccdExposure.getMask(),
1258 radius=self.config.brighterFatterMaskGrowSize,
1259 maskNameList=maskPlane,
1260 maskValue=maskPlane)
1261
1262 return ccdExposure
1263
1264 def darkCorrection(self, exposure, darkExposure, invert=False):
1265 """Apply dark correction in place.
1266
1267 Parameters
1268 ----------
1269 exposure : `lsst.afw.image.Exposure`
1270 Exposure to process.
1271 darkExposure : `lsst.afw.image.Exposure`
1272 Dark exposure of the same size as ``exposure``.
1273 invert : `Bool`, optional
1274 If True, re-add the dark to an already corrected image.
1275
1276 Raises
1277 ------
1278 RuntimeError
1279 Raised if either ``exposure`` or ``darkExposure`` do not
1280 have their dark time defined.
1281
1282 See Also
1283 --------
1284 lsst.ip.isr.isrFunctions.darkCorrection
1285 """
1286 expScale = exposure.visitInfo.darkTime
1287 if math.isnan(expScale):
1288 raise RuntimeError("Exposure darktime is NAN.")
1289 if darkExposure.visitInfo is not None \
1290 and not math.isnan(darkExposure.visitInfo.darkTime):
1291 darkScale = darkExposure.visitInfo.darkTime
1292 else:
1293 # DM-17444: darkExposure.visitInfo is None
1294 # so darkTime does not exist.
1295 self.log.warning("darkExposure.visitInfo does not exist. Using darkScale = 1.0.")
1296 darkScale = 1.0
1297
1298 isrFunctions.darkCorrection(
1299 maskedImage=exposure.maskedImage,
1300 darkMaskedImage=darkExposure.maskedImage,
1301 expScale=expScale,
1302 darkScale=darkScale,
1303 invert=invert,
1304 )
1305
1306 @staticmethod
1308 """Extract common calibration metadata values that will be written to
1309 output header.
1310
1311 Parameters
1312 ----------
1313 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib`
1314 Calibration to pull date information from.
1315
1316 Returns
1317 -------
1318 dateString : `str`
1319 Calibration creation date string to add to header.
1320 """
1321 if hasattr(calib, "getMetadata"):
1322 if 'CALIB_CREATION_DATE' in calib.metadata:
1323 return " ".join((calib.metadata.get("CALIB_CREATION_DATE", "Unknown"),
1324 calib.metadata.get("CALIB_CREATION_TIME", "Unknown")))
1325 else:
1326 return " ".join((calib.metadata.get("CALIB_CREATE_DATE", "Unknown"),
1327 calib.metadata.get("CALIB_CREATE_TIME", "Unknown")))
1328 else:
1329 return "Unknown Unknown"
1330
1331 def compareCameraKeywords(self, exposureMetadata, calib, calibName):
1332 """Compare header keywords to confirm camera states match.
1333
1334 Parameters
1335 ----------
1336 exposureMetadata : `lsst.daf.base.PropertyList`
1337 Header for the exposure being processed.
1338 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib`
1339 Calibration to be applied.
1340 calibName : `str`
1341 Calib type for log message.
1342 """
1343 try:
1344 calibMetadata = calib.metadata
1345 except AttributeError:
1346 return
1347 for keyword in self.config.cameraKeywordsToCompare:
1348 if keyword in exposureMetadata and keyword in calibMetadata:
1349 if exposureMetadata[keyword] != calibMetadata[keyword]:
1350 if self.config.doRaiseOnCalibMismatch:
1351 raise RuntimeError("Sequencer mismatch for %s [%s]: exposure: %s calib: %s",
1352 calibName, keyword,
1353 exposureMetadata[keyword], calibMetadata[keyword])
1354 else:
1355 self.log.warning("Sequencer mismatch for %s [%s]: exposure: %s calib: %s",
1356 calibName, keyword,
1357 exposureMetadata[keyword], calibMetadata[keyword])
1358 else:
1359 self.log.debug("Sequencer keyword %s not found.", keyword)
1360
1361 def compareUnits(self, calibMetadata, calibName):
1362 """Compare units from calibration to ISR units.
1363
1364 This compares calibration units (adu or electron) to whether
1365 doApplyGain is set.
1366
1367 Parameters
1368 ----------
1369 calibMetadata : `lsst.daf.base.PropertyList`
1370 Calibration metadata from header.
1371 calibName : `str`
1372 Calibration name for log message.
1373 """
1374 calibUnits = calibMetadata.get("LSST ISR UNITS", "adu")
1375 isrUnits = "electron" if self.config.doApplyGains else "adu"
1376 if calibUnits != isrUnits:
1377 if self.config.doRaiseOnCalibMismatch:
1378 raise RuntimeError(
1379 "Unit mismatch: isr has %s units but %s has %s units",
1380 isrUnits,
1381 calibName,
1382 calibUnits,
1383 )
1384 else:
1385 self.log.warning(
1386 "Unit mismatch: isr has %s units but %s has %s units",
1387 isrUnits,
1388 calibName,
1389 calibUnits,
1390 )
1391
1392 def convertIntToFloat(self, exposure):
1393 """Convert exposure image from uint16 to float.
1394
1395 If the exposure does not need to be converted, the input is
1396 immediately returned. For exposures that are converted to use
1397 floating point pixels, the variance is set to unity and the
1398 mask to zero.
1399
1400 Parameters
1401 ----------
1402 exposure : `lsst.afw.image.Exposure`
1403 The raw exposure to be converted.
1404
1405 Returns
1406 -------
1407 newexposure : `lsst.afw.image.Exposure`
1408 The input ``exposure``, converted to floating point pixels.
1409
1410 Raises
1411 ------
1412 RuntimeError
1413 Raised if the exposure type cannot be converted to float.
1414
1415 """
1416 if isinstance(exposure, afwImage.ExposureF):
1417 # Nothing to be done
1418 self.log.debug("Exposure already of type float.")
1419 return exposure
1420 if not hasattr(exposure, "convertF"):
1421 raise RuntimeError("Unable to convert exposure (%s) to float." % type(exposure))
1422
1423 newexposure = exposure.convertF()
1424 newexposure.variance[:] = 1
1425 newexposure.mask[:] = 0x0
1426
1427 return newexposure
1428
1429 def ditherCounts(self, exposure, detectorConfig, fallbackSeed=12345):
1430 """Dither the counts in the exposure.
1431
1432 Parameters
1433 ----------
1434 exposure : `lsst.afw.image.Exposure`
1435 The raw exposure to be dithered.
1436 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
1437 Configuration for overscan/etc for this detector.
1438 fallbackSeed : `int`, optional
1439 Random seed to fall back to if exposure.getInfo().getId() is
1440 not set.
1441 """
1442 if detectorConfig.integerDitherMode == "NONE":
1443 # Nothing to do here.
1444 return
1445
1446 # This ID is a unique combination of {exposure, detector} for a raw
1447 # image as we have here. We additionally need to take the lower
1448 # 32 bits to be used as a random seed.
1449 seed = exposure.info.id & 0xFFFFFFFF
1450 if seed is None:
1451 seed = fallbackSeed
1452 self.log.warning("No exposure ID found; using fallback random seed.")
1453
1454 self.log.info("Seeding dithering random number generator with %d.", seed)
1455 rng = numpy.random.RandomState(seed=seed)
1456
1457 if detectorConfig.integerDitherMode == "POSITIVE":
1458 low = 0.0
1459 high = 1.0
1460 elif detectorConfig.integerDitherMode == "NEGATIVE":
1461 low = -1.0
1462 high = 0.0
1463 elif detectorConfig.integerDitherMode == "SYMMETRIC":
1464 low = -0.5
1465 high = 0.5
1466 else:
1467 raise RuntimeError("Invalid config")
1468
1469 exposure.image.array[:, :] += rng.uniform(low=low, high=high, size=exposure.image.array.shape)
1470
1471 def flatCorrection(self, exposure, flatExposure, invert=False):
1472 """Apply flat correction in place.
1473
1474 Parameters
1475 ----------
1476 exposure : `lsst.afw.image.Exposure`
1477 Exposure to process.
1478 flatExposure : `lsst.afw.image.Exposure`
1479 Flat exposure of the same size as ``exposure``.
1480 invert : `Bool`, optional
1481 If True, unflatten an already flattened image.
1482
1483 See Also
1484 --------
1485 lsst.ip.isr.isrFunctions.flatCorrection
1486 """
1487 isrFunctions.flatCorrection(
1488 maskedImage=exposure.getMaskedImage(),
1489 flatMaskedImage=flatExposure.getMaskedImage(),
1490 scalingType=self.config.flatScalingType,
1491 userScale=self.config.flatUserScale,
1492 invert=invert,
1493 )
1494
1495 @deprecated(
1496 reason=(
1497 "makeBinnedImages is no longer used. "
1498 "Please subtask lsst.ip.isr.BinExposureTask instead."
1499 ),
1500 version="v28", category=FutureWarning
1501 )
1502 def makeBinnedImages(self, exposure):
1503 """Make visualizeVisit style binned exposures.
1504
1505 Parameters
1506 ----------
1507 exposure : `lsst.afw.image.Exposure`
1508 Exposure to bin.
1509
1510 Returns
1511 -------
1512 bin1 : `lsst.afw.image.Exposure`
1513 Binned exposure using binFactor1.
1514 bin2 : `lsst.afw.image.Exposure`
1515 Binned exposure using binFactor2.
1516 """
1517 mi = exposure.getMaskedImage()
1518
1519 bin1 = afwMath.binImage(mi, self.config.binFactor1)
1520 bin2 = afwMath.binImage(mi, self.config.binFactor2)
1521
1522 bin1 = afwImage.makeExposure(bin1)
1523 bin2 = afwImage.makeExposure(bin2)
1524
1525 bin1.setInfo(exposure.getInfo())
1526 bin2.setInfo(exposure.getInfo())
1527
1528 return bin1, bin2
1529
1530 def run(self, ccdExposure, *, dnlLUT=None, bias=None, deferredChargeCalib=None, linearizer=None,
1531 ptc=None, crosstalk=None, defects=None, bfKernel=None, bfGains=None, dark=None,
1532 flat=None, camera=None, **kwargs
1533 ):
1534
1535 detector = ccdExposure.getDetector()
1536
1537 overscanDetectorConfig = self.config.overscanCamera.getOverscanDetectorConfig(detector)
1538
1539 if self.config.doBootstrap and ptc is not None:
1540 self.log.warning("Task configured with doBootstrap=True. Ignoring provided PTC.")
1541 ptc = None
1542
1543 # Validation step: check inputs match exposure configuration.
1544 exposureMetadata = ccdExposure.metadata
1545 if not self.config.doBootstrap:
1546 self.compareCameraKeywords(exposureMetadata, ptc, "PTC")
1547 else:
1548 if self.config.doCorrectGains:
1549 raise RuntimeError("doCorrectGains is True but no ptc provided.")
1550 if self.config.doDiffNonLinearCorrection:
1551 if dnlLUT is None:
1552 raise RuntimeError("doDiffNonLinearCorrection is True but no dnlLUT provided.")
1553 self.compareCameraKeywords(exposureMetadata, dnlLUT, "dnlLUT")
1554 if self.config.doLinearize:
1555 if linearizer is None:
1556 raise RuntimeError("doLinearize is True but no linearizer provided.")
1557 self.compareCameraKeywords(exposureMetadata, linearizer, "linearizer")
1558 if self.config.doBias:
1559 if bias is None:
1560 raise RuntimeError("doBias is True but no bias provided.")
1561 self.compareCameraKeywords(exposureMetadata, bias, "bias")
1562 self.compareUnits(bias.metadata, "bias")
1563 if self.config.doCrosstalk:
1564 if crosstalk is None:
1565 raise RuntimeError("doCrosstalk is True but no crosstalk provided.")
1566 self.compareCameraKeywords(exposureMetadata, crosstalk, "crosstalk")
1567 if self.config.doDeferredCharge:
1568 if deferredChargeCalib is None:
1569 raise RuntimeError("doDeferredCharge is True but no deferredChargeCalib provided.")
1570 self.compareCameraKeywords(exposureMetadata, deferredChargeCalib, "CTI")
1571 if self.config.doDefect:
1572 if defects is None:
1573 raise RuntimeError("doDefect is True but no defects provided.")
1574 self.compareCameraKeywords(exposureMetadata, defects, "defects")
1575 if self.config.doDark:
1576 if dark is None:
1577 raise RuntimeError("doDark is True but no dark frame provided.")
1578 self.compareCameraKeywords(exposureMetadata, dark, "dark")
1579 self.compareUnits(bias.metadata, "dark")
1580 if self.config.doBrighterFatter:
1581 if bfKernel is None:
1582 raise RuntimeError("doBrighterFatter is True not no bfKernel provided.")
1583 self.compareCameraKeywords(exposureMetadata, bfKernel, "brighter-fatter")
1584 if self.config.doFlat:
1585 if flat is None:
1586 raise RuntimeError("doFlat is True but no flat provided.")
1587 self.compareCameraKeywords(exposureMetadata, flat, "flat")
1588
1589 if self.config.doSaturation:
1590 if self.config.defaultSaturationSource in ["PTCTURNOFF",]:
1591 if ptc is None:
1592 raise RuntimeError(
1593 "doSaturation is True and defaultSaturationSource is "
1594 f"{self.config.defaultSaturationSource}, but no ptc provided."
1595 )
1596 if self.config.doSuspect:
1597 if self.config.defaultSuspectSource in ["PTCTURNOFF",]:
1598 if ptc is None:
1599 raise RuntimeError(
1600 "doSuspect is True and defaultSuspectSource is "
1601 f"{self.config.defaultSuspectSource}, but no ptc provided."
1602 )
1603
1604 # FIXME: Make sure that if linearity is done then it is matched
1605 # with the right PTC.
1606
1607 # We keep track of units: start in adu.
1608 exposureMetadata["LSST ISR UNITS"] = "adu"
1609
1610 if self.config.doBootstrap:
1611 self.log.info("Configured using doBootstrap=True; using gain of 1.0 (adu units)")
1612 ptc = PhotonTransferCurveDataset([amp.getName() for amp in detector], "NOMINAL_PTC", 1)
1613 for amp in detector:
1614 ptc.gain[amp.getName()] = 1.0
1615 ptc.noise[amp.getName()] = 0.0
1616
1617 exposureMetadata["LSST ISR BOOTSTRAP"] = self.config.doBootstrap
1618
1619 # Set which gains to use
1620 gains = ptc.gain
1621
1622 # And check if we have configured gains to override. This is
1623 # also a warning, since it should not be typical usage.
1624 for amp in detector:
1625 if not math.isnan(gain := overscanDetectorConfig.getOverscanAmpConfig(amp).gain):
1626 gains[amp.getName()] = gain
1627 self.log.warning(
1628 "Overriding gain for amp %s with configured value of %.3f.",
1629 amp.getName(),
1630 gain,
1631 )
1632
1633 # First we convert the exposure to floating point values
1634 # (if necessary).
1635 self.log.debug("Converting exposure to floating point values.")
1636 ccdExposure = self.convertIntToFloat(ccdExposure)
1637
1638 # Then we mark which amplifiers are completely bad from defects.
1639 badAmpDict = self.maskFullAmplifiers(ccdExposure, detector, defects, gains=gains)
1640
1641 # Now we go through ISR steps.
1642
1643 # Differential non-linearity correction.
1644 # Units: adu
1645 if self.config.doDiffNonLinearCorrection:
1646 self.diffNonLinearCorrection(ccdExposure, dnlLUT)
1647
1648 # Dither the integer counts.
1649 # Input units: integerized adu
1650 # Output units: floating-point adu
1651 self.ditherCounts(ccdExposure, overscanDetectorConfig)
1652
1653 # Serial overscan correction.
1654 # Input units: adu
1655 # Output units: adu
1656 if overscanDetectorConfig.doAnySerialOverscan:
1657 serialOverscans = self.overscanCorrection(
1658 "SERIAL",
1659 overscanDetectorConfig,
1660 detector,
1661 badAmpDict,
1662 ccdExposure,
1663 )
1664
1665 if self.config.doBootstrap:
1666 # Get the empirical read noise
1667 for amp, serialOverscan in zip(detector, serialOverscans):
1668 if serialOverscan is None:
1669 ptc.noise[amp.getName()] = 0.0
1670 else:
1671 # All PhotonTransferCurveDataset objects should contain
1672 # noise attributes in units of electrons. The read
1673 # noise measured from overscans is always in adu, so we
1674 # scale it by the gain.
1675 # Note that in bootstrap mode, these gains will always
1676 # be 1.0, but we put this conversion here for clarity.
1677 ptc.noise[amp.getName()] = serialOverscan.residualSigma * gains[amp.getName()]
1678 else:
1679 serialOverscans = [None]*len(detector)
1680
1681 # After serial overscan correction, we can mask SATURATED and
1682 # SUSPECT pixels. This updates badAmpDict if any amplifier
1683 # is fully saturated after serial overscan correction.
1684
1685 # The saturation is currently assumed to be recorded in
1686 # overscan-corrected adu.
1687 badAmpDict = self.maskSaturatedPixels(
1688 badAmpDict,
1689 ccdExposure,
1690 detector,
1691 overscanDetectorConfig,
1692 ptc=ptc,
1693 )
1694
1695 if self.config.doCorrectGains:
1696 # TODO: DM-36639
1697 # This requires the PTC (tbd) with the temperature dependence.
1698 self.log.info("Apply temperature dependence to the gains.")
1699 gains, readNoise = self.correctGains(ccdExposure, ptc, gains)
1700
1701 # Do gain normalization.
1702 # Input units: adu
1703 # Output units: electron
1704 if self.config.doApplyGains:
1705 self.log.info("Using gain values to convert from adu to electron units.")
1706 isrFunctions.applyGains(ccdExposure, normalizeGains=False, ptcGains=gains, isTrimmed=False)
1707 # The units are now electron.
1708 exposureMetadata["LSST ISR UNITS"] = "electron"
1709
1710 # Update the saturation units in the metadata if there.
1711 # These will always have the same units as the image.
1712 for amp in detector:
1713 ampName = amp.getName()
1714 if (key := f"LSST ISR SATURATION LEVEL {ampName}") in exposureMetadata:
1715 exposureMetadata[key] *= gains[ampName]
1716 if (key := f"LSST ISR SUSPECT LEVEL {ampName}") in exposureMetadata:
1717 exposureMetadata[key] *= gains[ampName]
1718
1719 # Record gain and read noise in header.
1720 metadata = ccdExposure.metadata
1721 metadata["LSST ISR READNOISE UNITS"] = "electron"
1722 for amp in detector:
1723 # This includes any gain correction (if applied).
1724 metadata[f"LSST ISR GAIN {amp.getName()}"] = gains[amp.getName()]
1725
1726 # At this stage, the read noise is always in electrons.
1727 noise = ptc.noise[amp.getName()]
1728 metadata[f"LSST ISR READNOISE {amp.getName()}"] = noise
1729
1730 # Do crosstalk correction in the full region.
1731 # Output units: electron (adu if doBootstrap=True)
1732 if self.config.doCrosstalk:
1733 self.log.info("Applying crosstalk corrections to full amplifier region.")
1734 if self.config.doBootstrap and numpy.any(crosstalk.fitGains != 0):
1735 crosstalkGains = None
1736 else:
1737 crosstalkGains = gains
1738 self.crosstalk.run(
1739 ccdExposure,
1740 crosstalk=crosstalk,
1741 isTrimmed=False,
1742 gains=crosstalkGains,
1743 fullAmplifier=True,
1744 badAmpDict=badAmpDict,
1745 )
1746
1747 # Parallel overscan correction.
1748 # Output units: electron (adu if doBootstrap=True)
1749 if overscanDetectorConfig.doAnyParallelOverscan:
1750 # At the moment we do not use the return values from this task.
1751 _ = self.overscanCorrection(
1752 "PARALLEL",
1753 overscanDetectorConfig,
1754 detector,
1755 badAmpDict,
1756 ccdExposure,
1757 )
1758
1759 # Linearity correction
1760 # Output units: electron (adu if doBootstrap=True)
1761 if self.config.doLinearize:
1762 self.log.info("Applying linearizer.")
1763 # The linearizer is in units of adu.
1764 # If our units are electron, then pass in the gains
1765 # for conversion.
1766 if exposureMetadata["LSST ISR UNITS"] == "electron":
1767 linearityGains = gains
1768 else:
1769 linearityGains = None
1770 linearizer.applyLinearity(
1771 image=ccdExposure.image,
1772 detector=detector,
1773 log=self.log,
1774 gains=linearityGains,
1775 )
1776
1777 # Serial CTI (deferred charge) correction
1778 # This will be performed in electron units
1779 # Output units: same as input units
1780 if self.config.doDeferredCharge:
1781 self.deferredChargeCorrection.run(
1782 ccdExposure,
1783 deferredChargeCalib,
1784 gains=gains,
1785 )
1786
1787 # Assemble/trim
1788 # Output units: electron (adu if doBootstrap=True)
1789 if self.config.doAssembleCcd:
1790 self.log.info("Assembling CCD from amplifiers.")
1791 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1792
1793 if self.config.expectWcs and not ccdExposure.getWcs():
1794 self.log.warning("No WCS found in input exposure.")
1795
1796 # Bias subtraction
1797 # Output units: electron (adu if doBootstrap=True)
1798 if self.config.doBias:
1799 self.log.info("Applying bias correction.")
1800 # Bias frame and ISR unit consistency is checked at the top of
1801 # the run method.
1802 isrFunctions.biasCorrection(ccdExposure.maskedImage, bias.maskedImage)
1803
1804 # Dark subtraction
1805 # Output units: electron (adu if doBootstrap=True)
1806 if self.config.doDark:
1807 self.log.info("Applying dark subtraction.")
1808 # Dark frame and ISR unit consistency is checked at the top of
1809 # the run method.
1810 self.darkCorrection(ccdExposure, dark)
1811
1812 # Defect masking
1813 # Masking block (defects, NAN pixels and trails).
1814 # Saturated and suspect pixels have already been masked.
1815 # Output units: electron (adu if doBootstrap=True)
1816 if self.config.doDefect:
1817 self.log.info("Applying defect masking.")
1818 self.maskDefects(ccdExposure, defects)
1819
1820 if self.config.doNanMasking:
1821 self.log.info("Masking non-finite (NAN, inf) value pixels.")
1822 self.maskNan(ccdExposure)
1823
1824 if self.config.doWidenSaturationTrails:
1825 self.log.info("Widening saturation trails.")
1826 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1827
1828 # Brighter-Fatter
1829 # Output units: electron (adu if doBootstrap=True)
1830 if self.config.doBrighterFatter:
1831 self.log.info("Applying brighter-fatter correction.")
1832
1833 bfKernelOut, bfGains = self.getBrighterFatterKernel(detector, bfKernel)
1834
1835 # Needs to be done in electrons; applyBrighterFatterCorrection
1836 # will convert the image if necessary.
1837 if exposureMetadata["LSST ISR UNITS"] == "electron":
1838 brighterFatterApplyGain = False
1839 else:
1840 brighterFatterApplyGain = True
1841
1842 if brighterFatterApplyGain and (ptc is not None) and (bfGains != gains):
1843 # The supplied ptc should be the same as the ptc used to
1844 # generate the bfKernel, in which case they will have the
1845 # same stored amp-keyed dictionary of gains. If not, there
1846 # is a mismatch in the calibrations being used. This should
1847 # not be always be a fatal error, but ideally, everything
1848 # should to be consistent.
1849 self.log.warning("Need to apply gain for brighter-fatter, but the stored"
1850 "gains in the kernel are not the same as the gains stored"
1851 "in the PTC. Using the kernel gains.")
1852
1853 ccdExposure = self.applyBrighterFatterCorrection(ccdExposure, flat, dark, bfKernelOut,
1854 brighterFatterApplyGain, bfGains)
1855
1856 # Variance plane creation
1857 # Output units: electron (adu if doBootstrap=True)
1858 if self.config.doVariance:
1859 self.addVariancePlane(ccdExposure, detector)
1860
1861 # Flat-fielding
1862 # This may move elsewhere.
1863 # Placeholder while the LSST flat procedure is done.
1864 # Output units: electron (adu if doBootstrap=True)
1865 if self.config.doFlat:
1866 self.log.info("Applying flat correction.")
1867 self.flatCorrection(ccdExposure, flat)
1868
1869 # Pixel values for masked regions are set here
1870 preInterpExp = None
1871 if self.config.doSaveInterpPixels:
1872 preInterpExp = ccdExposure.clone()
1873
1874 if self.config.doSetBadRegions:
1875 self.log.info('Setting values in large contiguous bad regions.')
1876 self.setBadRegions(ccdExposure)
1877
1878 if self.config.doInterpolate:
1879 self.log.info("Interpolating masked pixels.")
1880 isrFunctions.interpolateFromMask(
1881 maskedImage=ccdExposure.getMaskedImage(),
1882 fwhm=self.config.brighterFatterFwhmForInterpolation,
1883 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1884 maskNameList=list(self.config.maskListToInterpolate),
1885 useLegacyInterp=self.config.useLegacyInterp,
1886 )
1887
1888 # Calculate amp offset corrections within the CCD.
1889 if self.config.doAmpOffset:
1890 if self.config.ampOffset.doApplyAmpOffset:
1891 self.log.info("Measuring and applying amp offset corrections.")
1892 else:
1893 self.log.info("Measuring amp offset corrections only, without applying them.")
1894 self.ampOffset.run(ccdExposure)
1895
1896 # Calculate standard image quality statistics
1897 if self.config.doStandardStatistics:
1898 metadata = ccdExposure.metadata
1899 for amp in detector:
1900 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1901 ampName = amp.getName()
1902 metadata[f"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels(
1903 ampExposure.getMaskedImage(),
1904 [self.config.saturatedMaskName]
1905 )
1906 metadata[f"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels(
1907 ampExposure.getMaskedImage(),
1908 ["BAD"]
1909 )
1910 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1911 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP)
1912
1913 metadata[f"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN)
1914 metadata[f"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN)
1915 metadata[f"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP)
1916
1917 k1 = f"LSST ISR FINAL MEDIAN {ampName}"
1918 k2 = f"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
1919 if overscanDetectorConfig.doAnySerialOverscan and k1 in metadata and k2 in metadata:
1920 metadata[f"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2]
1921 else:
1922 metadata[f"LSST ISR LEVEL {ampName}"] = numpy.nan
1923
1924 # calculate additional statistics.
1925 outputStatistics = None
1926 if self.config.doCalculateStatistics:
1927 outputStatistics = self.isrStats.run(ccdExposure, overscanResults=serialOverscans,
1928 bias=bias, dark=dark, flat=flat, ptc=ptc,
1929 defects=defects).results
1930
1931 # do image binning.
1932 outputBin1Exposure = None
1933 outputBin2Exposure = None
1934 if self.config.doBinnedExposures:
1935 self.log.info("Creating binned exposures.")
1936 outputBin1Exposure = self.binning.run(
1937 ccdExposure,
1938 binFactor=self.config.binFactor1,
1939 ).binnedExposure
1940 outputBin2Exposure = self.binning.run(
1941 ccdExposure,
1942 binFactor=self.config.binFactor2,
1943 ).binnedExposure
1944
1945 return pipeBase.Struct(
1946 exposure=ccdExposure,
1947
1948 outputBin1Exposure=outputBin1Exposure,
1949 outputBin2Exposure=outputBin2Exposure,
1950
1951 preInterpExposure=preInterpExp,
1952 outputExposure=ccdExposure,
1953 outputStatistics=outputStatistics,
1954 )
Represent a 2-dimensional array of bitmask pixels.
Definition Mask.h:81
runQuantum(self, butlerQC, inputRefs, outputRefs)
ditherCounts(self, exposure, detectorConfig, fallbackSeed=12345)
flatContext(self, exp, flat, dark=None)
flatCorrection(self, exposure, flatExposure, invert=False)
correctGains(self, exposure, ptc, gains)
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)
diffNonLinearCorrection(self, ccdExposure, dnlLUT, **kwargs)
compareCameraKeywords(self, exposureMetadata, calib, calibName)
maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR')
maskDefects(self, exposure, defectBaseList)
run(self, ccdExposure, *dnlLUT=None, bias=None, deferredChargeCalib=None, linearizer=None, ptc=None, crosstalk=None, defects=None, bfKernel=None, bfGains=None, dark=None, flat=None, camera=None, **kwargs)
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:484
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