Coverage for python/lsst/ip/isr/isrTask.py: 65%
1006 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 11:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 11:20 +0000
1# This file is part of ip_isr.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22__all__ = ["IsrTask", "IsrTaskConfig"]
24import math
25import numpy
26import numbers
28import lsst.geom
29import lsst.afw.image as afwImage
30import lsst.afw.math as afwMath
31import lsst.pex.config as pexConfig
32import lsst.pipe.base as pipeBase
33import lsst.pipe.base.connectionTypes as cT
35from contextlib import contextmanager
36from deprecated.sphinx import deprecated
37from lsstDebug import getDebugFrame
39from lsst.afw.cameraGeom import NullLinearityType
40from lsst.afw.display import getDisplay
41from lsst.meas.algorithms.detection import SourceDetectionTask
42from lsst.utils.timer import timeMethod
44from . import isrFunctions
45from . import isrQa
46from . import linearize
47from .defects import Defects
49from .assembleCcdTask import AssembleCcdTask
50from .binImageDataTask import BinImageDataTask
51from .crosstalk import CrosstalkTask, CrosstalkCalib
52from .fringe import FringeTask
53from .isr import maskNans
54from .masking import MaskingTask
55from .overscan import OverscanCorrectionTask
56from .straylight import StrayLightTask
57from .vignette import VignetteTask
58from .ampOffset import AmpOffsetTask
59from .deferredCharge import DeferredChargeTask
60from .isrStatistics import IsrStatisticsTask
61from .ptcDataset import PhotonTransferCurveDataset
62from .isrFunctions import compareCameraKeywords
63from .brighterFatterKernel import (brighterFatterCorrection,
64 fluxConservingBrighterFatterCorrection)
67def crosstalkSourceLookup(datasetType, registry, quantumDataId, collections):
68 """Lookup function to identify crosstalkSource entries.
70 This should return an empty list under most circumstances. Only
71 when inter-chip crosstalk has been identified should this be
72 populated.
74 Parameters
75 ----------
76 datasetType : `str`
77 Dataset to lookup.
78 registry : `lsst.daf.butler.Registry`
79 Butler registry to query.
80 quantumDataId : `lsst.daf.butler.DataCoordinate`
81 Expanded data id to transform to identify crosstalkSources. The
82 ``detector`` entry will be stripped.
83 collections : `lsst.daf.butler.CollectionSearch`
84 Collections to search through.
86 Returns
87 -------
88 results : `list` [`lsst.daf.butler.DatasetRef`]
89 List of datasets that match the query that will be used as
90 crosstalkSources.
91 """
92 newDataId = quantumDataId.subset(registry.dimensions.conform(["instrument", "exposure"]))
93 results = set(registry.queryDatasets(datasetType, collections=collections, dataId=newDataId,
94 findFirst=True))
95 # In some contexts, calling `.expanded()` to expand all data IDs in the
96 # query results can be a lot faster because it vectorizes lookups. But in
97 # this case, expandDataId shouldn't need to hit the database at all in the
98 # steady state, because only the detector record is unknown and those are
99 # cached in the registry.
100 records = {k: newDataId.records[k] for k in newDataId.dimensions.elements}
101 return [ref.expanded(registry.expandDataId(ref.dataId, records=records)) for ref in results]
104class IsrTaskConnections(pipeBase.PipelineTaskConnections,
105 dimensions={"instrument", "exposure", "detector"},
106 defaultTemplates={}):
107 ccdExposure = cT.Input(
108 name="raw",
109 doc="Input exposure to process.",
110 storageClass="Exposure",
111 dimensions=["instrument", "exposure", "detector"],
112 )
113 camera = cT.PrerequisiteInput(
114 name="camera",
115 storageClass="Camera",
116 doc="Input camera to construct complete exposures.",
117 dimensions=["instrument"],
118 isCalibration=True,
119 )
121 crosstalk = cT.PrerequisiteInput(
122 name="crosstalk",
123 doc="Input crosstalk object",
124 storageClass="CrosstalkCalib",
125 dimensions=["instrument", "detector"],
126 isCalibration=True,
127 minimum=0, # can fall back to cameraGeom
128 )
129 crosstalkSources = cT.PrerequisiteInput(
130 name="isrOverscanCorrected",
131 doc="Overscan corrected input images.",
132 storageClass="Exposure",
133 dimensions=["instrument", "exposure", "detector"],
134 deferLoad=True,
135 multiple=True,
136 lookupFunction=crosstalkSourceLookup,
137 minimum=0, # not needed for all instruments, no config to control this
138 )
139 bias = cT.PrerequisiteInput(
140 name="bias",
141 doc="Input bias calibration.",
142 storageClass="ExposureF",
143 dimensions=["instrument", "detector"],
144 isCalibration=True,
145 )
146 dark = cT.PrerequisiteInput(
147 name='dark',
148 doc="Input dark calibration.",
149 storageClass="ExposureF",
150 dimensions=["instrument", "detector"],
151 isCalibration=True,
152 )
153 flat = cT.PrerequisiteInput(
154 name="flat",
155 doc="Input flat calibration.",
156 storageClass="ExposureF",
157 dimensions=["instrument", "physical_filter", "detector"],
158 isCalibration=True,
159 )
160 ptc = cT.PrerequisiteInput(
161 name="ptc",
162 doc="Input Photon Transfer Curve dataset",
163 storageClass="PhotonTransferCurveDataset",
164 dimensions=["instrument", "detector"],
165 isCalibration=True,
166 )
167 fringes = cT.PrerequisiteInput(
168 name="fringe",
169 doc="Input fringe calibration.",
170 storageClass="ExposureF",
171 dimensions=["instrument", "physical_filter", "detector"],
172 isCalibration=True,
173 minimum=0, # only needed for some bands, even when enabled
174 )
175 strayLightData = cT.PrerequisiteInput(
176 name='yBackground',
177 doc="Input stray light calibration.",
178 storageClass="StrayLightData",
179 dimensions=["instrument", "physical_filter", "detector"],
180 deferLoad=True,
181 isCalibration=True,
182 minimum=0, # only needed for some bands, even when enabled
183 )
184 bfKernel = cT.PrerequisiteInput(
185 name='bfKernel',
186 doc="Input brighter-fatter kernel.",
187 storageClass="NumpyArray",
188 dimensions=["instrument"],
189 isCalibration=True,
190 minimum=0, # can use either bfKernel or newBFKernel
191 )
192 newBFKernel = cT.PrerequisiteInput(
193 name='brighterFatterKernel',
194 doc="Newer complete kernel + gain solutions.",
195 storageClass="BrighterFatterKernel",
196 dimensions=["instrument", "detector"],
197 isCalibration=True,
198 minimum=0, # can use either bfKernel or newBFKernel
199 )
200 defects = cT.PrerequisiteInput(
201 name='defects',
202 doc="Input defect tables.",
203 storageClass="Defects",
204 dimensions=["instrument", "detector"],
205 isCalibration=True,
206 )
207 linearizer = cT.PrerequisiteInput(
208 name='linearizer',
209 storageClass="Linearizer",
210 doc="Linearity correction calibration.",
211 dimensions=["instrument", "detector"],
212 isCalibration=True,
213 minimum=0, # can fall back to cameraGeom
214 )
215 opticsTransmission = cT.PrerequisiteInput(
216 name="transmission_optics",
217 storageClass="TransmissionCurve",
218 doc="Transmission curve due to the optics.",
219 dimensions=["instrument"],
220 isCalibration=True,
221 )
222 filterTransmission = cT.PrerequisiteInput(
223 name="transmission_filter",
224 storageClass="TransmissionCurve",
225 doc="Transmission curve due to the filter.",
226 dimensions=["instrument", "physical_filter"],
227 isCalibration=True,
228 )
229 sensorTransmission = cT.PrerequisiteInput(
230 name="transmission_sensor",
231 storageClass="TransmissionCurve",
232 doc="Transmission curve due to the sensor.",
233 dimensions=["instrument", "detector"],
234 isCalibration=True,
235 )
236 atmosphereTransmission = cT.PrerequisiteInput(
237 name="transmission_atmosphere",
238 storageClass="TransmissionCurve",
239 doc="Transmission curve due to the atmosphere.",
240 dimensions=["instrument"],
241 isCalibration=True,
242 )
243 illumMaskedImage = cT.PrerequisiteInput(
244 name="illum",
245 doc="Input illumination correction.",
246 storageClass="MaskedImageF",
247 dimensions=["instrument", "physical_filter", "detector"],
248 isCalibration=True,
249 )
250 deferredChargeCalib = cT.PrerequisiteInput(
251 name="cpCtiCalib",
252 doc="Deferred charge/CTI correction dataset.",
253 storageClass="IsrCalib",
254 dimensions=["instrument", "detector"],
255 isCalibration=True,
256 )
258 outputExposure = cT.Output(
259 name='postISRCCD',
260 doc="Output ISR processed exposure.",
261 storageClass="Exposure",
262 dimensions=["instrument", "exposure", "detector"],
263 )
264 preInterpExposure = cT.Output(
265 name='preInterpISRCCD',
266 doc="Output ISR processed exposure, with pixels left uninterpolated.",
267 storageClass="ExposureF",
268 dimensions=["instrument", "exposure", "detector"],
269 )
270 outputBin1Exposure = cT.Output(
271 name="postIsrBin1",
272 doc="First binned image.",
273 storageClass="ExposureF",
274 dimensions=["instrument", "exposure", "detector"],
275 )
276 outputBin2Exposure = cT.Output(
277 name="postIsrBin2",
278 doc="Second binned image.",
279 storageClass="ExposureF",
280 dimensions=["instrument", "exposure", "detector"],
281 )
283 outputOssThumbnail = cT.Output(
284 name="OssThumb",
285 doc="Output Overscan-subtracted thumbnail image.",
286 storageClass="Thumbnail",
287 dimensions=["instrument", "exposure", "detector"],
288 )
289 outputFlattenedThumbnail = cT.Output(
290 name="FlattenedThumb",
291 doc="Output flat-corrected thumbnail image.",
292 storageClass="Thumbnail",
293 dimensions=["instrument", "exposure", "detector"],
294 )
295 outputStatistics = cT.Output(
296 name="isrStatistics",
297 doc="Output of additional statistics table.",
298 storageClass="StructuredDataDict",
299 dimensions=["instrument", "exposure", "detector"],
300 )
302 def __init__(self, *, config=None):
303 super().__init__(config=config)
305 if config.doBias is not True:
306 self.prerequisiteInputs.remove("bias")
307 if config.doLinearize is not True:
308 self.prerequisiteInputs.remove("linearizer")
309 if config.doCrosstalk is not True:
310 self.prerequisiteInputs.remove("crosstalkSources")
311 self.prerequisiteInputs.remove("crosstalk")
312 if config.doBrighterFatter is not True:
313 self.prerequisiteInputs.remove("bfKernel")
314 self.prerequisiteInputs.remove("newBFKernel")
315 if config.doDefect is not True:
316 self.prerequisiteInputs.remove("defects")
317 if config.doDark is not True:
318 self.prerequisiteInputs.remove("dark")
319 if config.doFlat is not True:
320 self.prerequisiteInputs.remove("flat")
321 if config.doFringe is not True:
322 self.prerequisiteInputs.remove("fringes")
323 if config.doStrayLight is not True:
324 self.prerequisiteInputs.remove("strayLightData")
325 if config.usePtcGains is not True and config.usePtcReadNoise is not True:
326 self.prerequisiteInputs.remove("ptc")
327 if config.doAttachTransmissionCurve is not True:
328 self.prerequisiteInputs.remove("opticsTransmission")
329 self.prerequisiteInputs.remove("filterTransmission")
330 self.prerequisiteInputs.remove("sensorTransmission")
331 self.prerequisiteInputs.remove("atmosphereTransmission")
332 else:
333 if config.doUseOpticsTransmission is not True:
334 self.prerequisiteInputs.remove("opticsTransmission")
335 if config.doUseFilterTransmission is not True:
336 self.prerequisiteInputs.remove("filterTransmission")
337 if config.doUseSensorTransmission is not True:
338 self.prerequisiteInputs.remove("sensorTransmission")
339 if config.doUseAtmosphereTransmission is not True:
340 self.prerequisiteInputs.remove("atmosphereTransmission")
341 if config.doIlluminationCorrection is not True:
342 self.prerequisiteInputs.remove("illumMaskedImage")
343 if config.doDeferredCharge is not True:
344 self.prerequisiteInputs.remove("deferredChargeCalib")
346 if config.doWrite is not True:
347 self.outputs.remove("outputExposure")
348 self.outputs.remove("preInterpExposure")
349 self.outputs.remove("outputFlattenedThumbnail")
350 self.outputs.remove("outputOssThumbnail")
351 self.outputs.remove("outputStatistics")
352 self.outputs.remove("outputBin1Exposure")
353 self.outputs.remove("outputBin2Exposure")
354 else:
355 if config.doBinnedExposures is not True:
356 self.outputs.remove("outputBin1Exposure")
357 self.outputs.remove("outputBin2Exposure")
358 if config.doSaveInterpPixels is not True:
359 self.outputs.remove("preInterpExposure")
360 if config.qa.doThumbnailOss is not True:
361 self.outputs.remove("outputOssThumbnail")
362 if config.qa.doThumbnailFlattened is not True:
363 self.outputs.remove("outputFlattenedThumbnail")
364 if config.doCalculateStatistics is not True:
365 self.outputs.remove("outputStatistics")
368class IsrTaskConfig(pipeBase.PipelineTaskConfig,
369 pipelineConnections=IsrTaskConnections):
370 """Configuration parameters for IsrTask.
372 Items are grouped in the order in which they are executed by the task.
373 """
374 datasetType = pexConfig.Field(
375 dtype=str,
376 doc="Dataset type for input data; users will typically leave this alone, "
377 "but camera-specific ISR tasks will override it",
378 default="raw",
379 )
381 fallbackFilterName = pexConfig.Field(
382 dtype=str,
383 doc="Fallback default filter name for calibrations.",
384 optional=True
385 )
386 useFallbackDate = pexConfig.Field(
387 dtype=bool,
388 doc="Pass observation date when using fallback filter.",
389 default=False,
390 )
391 expectWcs = pexConfig.Field(
392 dtype=bool,
393 default=True,
394 doc="Expect input science images to have a WCS (set False for e.g. spectrographs)."
395 )
396 fwhm = pexConfig.Field(
397 dtype=float,
398 doc="FWHM of PSF in arcseconds (currently unused).",
399 default=1.0,
400 )
401 qa = pexConfig.ConfigField(
402 dtype=isrQa.IsrQaConfig,
403 doc="QA related configuration options.",
404 )
405 doHeaderProvenance = pexConfig.Field(
406 dtype=bool,
407 default=True,
408 doc="Write calibration identifiers into output exposure header?",
409 )
411 # Calib checking configuration:
412 doRaiseOnCalibMismatch = pexConfig.Field(
413 dtype=bool,
414 default=False,
415 doc="Should IsrTask halt if exposure and calibration header values do not match?",
416 )
417 cameraKeywordsToCompare = pexConfig.ListField(
418 dtype=str,
419 doc="List of header keywords to compare between exposure and calibrations.",
420 default=[],
421 )
423 # Image conversion configuration
424 doConvertIntToFloat = pexConfig.Field(
425 dtype=bool,
426 doc="Convert integer raw images to floating point values?",
427 default=True,
428 )
430 # Saturated pixel handling.
431 doSaturation = pexConfig.Field(
432 dtype=bool,
433 doc="Mask saturated pixels? NB: this is totally independent of the"
434 " interpolation option - this is ONLY setting the bits in the mask."
435 " To have them interpolated make sure doSaturationInterpolation=True",
436 default=True,
437 )
438 saturatedMaskName = pexConfig.Field(
439 dtype=str,
440 doc="Name of mask plane to use in saturation detection and interpolation",
441 default="SAT",
442 )
443 saturation = pexConfig.Field(
444 dtype=float,
445 doc="The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
446 default=float("NaN"),
447 )
448 growSaturationFootprintSize = pexConfig.Field(
449 dtype=int,
450 doc="Number of pixels by which to grow the saturation footprints",
451 default=1,
452 )
454 # Suspect pixel handling.
455 doSuspect = pexConfig.Field(
456 dtype=bool,
457 doc="Mask suspect pixels?",
458 default=False,
459 )
460 suspectMaskName = pexConfig.Field(
461 dtype=str,
462 doc="Name of mask plane to use for suspect pixels",
463 default="SUSPECT",
464 )
465 numEdgeSuspect = pexConfig.Field(
466 dtype=int,
467 doc="Number of edge pixels to be flagged as untrustworthy.",
468 default=0,
469 )
470 edgeMaskLevel = pexConfig.ChoiceField(
471 dtype=str,
472 doc="Mask edge pixels in which coordinate frame: DETECTOR or AMP?",
473 default="DETECTOR",
474 allowed={
475 'DETECTOR': 'Mask only the edges of the full detector.',
476 'AMP': 'Mask edges of each amplifier.',
477 },
478 )
480 # Initial masking options.
481 doSetBadRegions = pexConfig.Field(
482 dtype=bool,
483 doc="Should we set the level of all BAD patches of the chip to the chip's average value?",
484 default=True,
485 )
486 badStatistic = pexConfig.ChoiceField(
487 dtype=str,
488 doc="How to estimate the average value for BAD regions.",
489 default='MEANCLIP',
490 allowed={
491 "MEANCLIP": "Correct using the (clipped) mean of good data",
492 "MEDIAN": "Correct using the median of the good data",
493 },
494 )
496 # Overscan subtraction configuration.
497 doOverscan = pexConfig.Field(
498 dtype=bool,
499 doc="Do overscan subtraction?",
500 default=True,
501 )
502 overscan = pexConfig.ConfigurableField(
503 target=OverscanCorrectionTask,
504 doc="Overscan subtraction task for image segments.",
505 )
507 # Amplifier to CCD assembly configuration
508 doAssembleCcd = pexConfig.Field(
509 dtype=bool,
510 default=True,
511 doc="Assemble amp-level exposures into a ccd-level exposure?"
512 )
513 assembleCcd = pexConfig.ConfigurableField(
514 target=AssembleCcdTask,
515 doc="CCD assembly task",
516 )
518 # General calibration configuration.
519 doAssembleIsrExposures = pexConfig.Field(
520 dtype=bool,
521 default=False,
522 doc="Assemble amp-level calibration exposures into ccd-level exposure?"
523 )
524 doTrimToMatchCalib = pexConfig.Field(
525 dtype=bool,
526 default=False,
527 doc="Trim raw data to match calibration bounding boxes?"
528 )
530 # Bias subtraction.
531 doBias = pexConfig.Field(
532 dtype=bool,
533 doc="Apply bias frame correction?",
534 default=True,
535 )
536 biasDataProductName = pexConfig.Field(
537 dtype=str,
538 doc="Name of the bias data product",
539 default="bias",
540 )
541 doBiasBeforeOverscan = pexConfig.Field(
542 dtype=bool,
543 doc="Reverse order of overscan and bias correction.",
544 default=False
545 )
547 # Deferred charge correction.
548 doDeferredCharge = pexConfig.Field(
549 dtype=bool,
550 doc="Apply deferred charge correction?",
551 default=False,
552 )
553 deferredChargeCorrection = pexConfig.ConfigurableField(
554 target=DeferredChargeTask,
555 doc="Deferred charge correction task.",
556 )
558 # Variance construction
559 doVariance = pexConfig.Field(
560 dtype=bool,
561 doc="Calculate variance?",
562 default=True
563 )
564 gain = pexConfig.Field(
565 dtype=float,
566 doc="The gain to use if no Detector is present in the Exposure (ignored if NaN)",
567 default=float("NaN"),
568 )
569 readNoise = pexConfig.Field(
570 dtype=float,
571 doc="The read noise to use if no Detector is present in the Exposure",
572 default=0.0,
573 )
574 doEmpiricalReadNoise = pexConfig.Field(
575 dtype=bool,
576 default=False,
577 doc="Calculate empirical read noise instead of value from AmpInfo data?"
578 )
579 usePtcReadNoise = pexConfig.Field(
580 dtype=bool,
581 default=False,
582 doc="Use read noise values from the Photon Transfer Curve?"
583 )
584 maskNegativeVariance = pexConfig.Field(
585 dtype=bool,
586 default=True,
587 doc="Mask pixels that claim a negative variance? This likely indicates a failure "
588 "in the measurement of the overscan at an edge due to the data falling off faster "
589 "than the overscan model can account for it."
590 )
591 negativeVarianceMaskName = pexConfig.Field(
592 dtype=str,
593 default="BAD",
594 doc="Mask plane to use to mark pixels with negative variance, if `maskNegativeVariance` is True.",
595 )
596 # Linearization.
597 doLinearize = pexConfig.Field(
598 dtype=bool,
599 doc="Correct for nonlinearity of the detector's response?",
600 default=True,
601 )
603 # Crosstalk.
604 doCrosstalk = pexConfig.Field(
605 dtype=bool,
606 doc="Apply intra-CCD crosstalk correction?",
607 default=False,
608 )
609 doCrosstalkBeforeAssemble = pexConfig.Field(
610 dtype=bool,
611 doc="Apply crosstalk correction before CCD assembly, and before trimming?",
612 default=False,
613 )
614 crosstalk = pexConfig.ConfigurableField(
615 target=CrosstalkTask,
616 doc="Intra-CCD crosstalk correction",
617 )
619 # Masking options.
620 doDefect = pexConfig.Field(
621 dtype=bool,
622 doc="Apply correction for CCD defects, e.g. hot pixels?",
623 default=True,
624 )
625 doNanMasking = pexConfig.Field(
626 dtype=bool,
627 doc="Mask non-finite (NAN, inf) pixels?",
628 default=True,
629 )
630 doWidenSaturationTrails = pexConfig.Field(
631 dtype=bool,
632 doc="Widen bleed trails based on their width?",
633 default=True
634 )
636 # Brighter-Fatter correction.
637 doBrighterFatter = pexConfig.Field(
638 dtype=bool,
639 default=False,
640 doc="Apply the brighter-fatter correction?"
641 )
642 doFluxConservingBrighterFatterCorrection = pexConfig.Field(
643 dtype=bool,
644 default=False,
645 doc="Apply the flux-conserving BFE correction by Miller et al.?"
646 )
647 brighterFatterLevel = pexConfig.ChoiceField(
648 dtype=str,
649 default="DETECTOR",
650 doc="The level at which to correct for brighter-fatter.",
651 allowed={
652 "AMP": "Every amplifier treated separately.",
653 "DETECTOR": "One kernel per detector",
654 }
655 )
656 brighterFatterMaxIter = pexConfig.Field(
657 dtype=int,
658 default=10,
659 doc="Maximum number of iterations for the brighter-fatter correction"
660 )
661 brighterFatterThreshold = pexConfig.Field(
662 dtype=float,
663 default=1000,
664 doc="Threshold used to stop iterating the brighter-fatter correction. It is the "
665 "absolute value of the difference between the current corrected image and the one "
666 "from the previous iteration summed over all the pixels."
667 )
668 brighterFatterApplyGain = pexConfig.Field(
669 dtype=bool,
670 default=True,
671 doc="Should the gain be applied when applying the brighter-fatter correction?"
672 )
673 brighterFatterMaskListToInterpolate = pexConfig.ListField(
674 dtype=str,
675 doc="List of mask planes that should be interpolated over when applying the brighter-fatter "
676 "correction.",
677 default=["SAT", "BAD", "NO_DATA", "UNMASKEDNAN"],
678 )
679 brighterFatterMaskGrowSize = pexConfig.Field(
680 dtype=int,
681 default=0,
682 doc="Number of pixels to grow the masks listed in config.brighterFatterMaskListToInterpolate "
683 "when brighter-fatter correction is applied."
684 )
686 # Dark subtraction.
687 doDark = pexConfig.Field(
688 dtype=bool,
689 doc="Apply dark frame correction?",
690 default=True,
691 )
692 darkDataProductName = pexConfig.Field(
693 dtype=str,
694 doc="Name of the dark data product",
695 default="dark",
696 )
698 # Camera-specific stray light removal.
699 doStrayLight = pexConfig.Field(
700 dtype=bool,
701 doc="Subtract stray light in the y-band (due to encoder LEDs)?",
702 default=False,
703 )
704 strayLight = pexConfig.ConfigurableField(
705 target=StrayLightTask,
706 doc="y-band stray light correction"
707 )
709 # Flat correction.
710 doFlat = pexConfig.Field(
711 dtype=bool,
712 doc="Apply flat field correction?",
713 default=True,
714 )
715 flatDataProductName = pexConfig.Field(
716 dtype=str,
717 doc="Name of the flat data product",
718 default="flat",
719 )
720 flatScalingType = pexConfig.ChoiceField(
721 dtype=str,
722 doc="The method for scaling the flat on the fly.",
723 default='USER',
724 allowed={
725 "USER": "Scale by flatUserScale",
726 "MEAN": "Scale by the inverse of the mean",
727 "MEDIAN": "Scale by the inverse of the median",
728 },
729 )
730 flatUserScale = pexConfig.Field(
731 dtype=float,
732 doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
733 default=1.0,
734 )
735 doTweakFlat = pexConfig.Field(
736 dtype=bool,
737 doc="Tweak flats to match observed amplifier ratios?",
738 default=False
739 )
741 # Amplifier normalization based on gains instead of using flats
742 # configuration.
743 doApplyGains = pexConfig.Field(
744 dtype=bool,
745 doc="Correct the amplifiers for their gains instead of applying flat correction",
746 default=False,
747 )
748 usePtcGains = pexConfig.Field(
749 dtype=bool,
750 doc="Use the gain values from the input Photon Transfer Curve?",
751 default=False,
752 )
753 normalizeGains = pexConfig.Field(
754 dtype=bool,
755 doc="Normalize all the amplifiers in each CCD to have the same median value.",
756 default=False,
757 )
759 # Fringe correction.
760 doFringe = pexConfig.Field(
761 dtype=bool,
762 doc="Apply fringe correction?",
763 default=True,
764 )
765 fringe = pexConfig.ConfigurableField(
766 target=FringeTask,
767 doc="Fringe subtraction task",
768 )
769 fringeAfterFlat = pexConfig.Field(
770 dtype=bool,
771 doc="Do fringe subtraction after flat-fielding?",
772 default=True,
773 )
775 # Amp offset correction.
776 doAmpOffset = pexConfig.Field(
777 doc="Calculate amp offset corrections?",
778 dtype=bool,
779 default=False,
780 )
781 ampOffset = pexConfig.ConfigurableField(
782 doc="Amp offset correction task.",
783 target=AmpOffsetTask,
784 )
786 # Initial CCD-level background statistics options.
787 doMeasureBackground = pexConfig.Field(
788 dtype=bool,
789 doc="Measure the background level on the reduced image?",
790 default=False,
791 )
793 # Camera-specific masking configuration.
794 doCameraSpecificMasking = pexConfig.Field(
795 dtype=bool,
796 doc="Use camera-specific masking task to mask bad regions?",
797 default=False,
798 )
799 masking = pexConfig.ConfigurableField(
800 target=MaskingTask,
801 doc="Masking task. The default does nothing; retarget to a camera-specific "
802 "task and set doCameraSpecificMasking.",
803 )
805 # Interpolation options.
806 doInterpolate = pexConfig.Field(
807 dtype=bool,
808 doc="Interpolate masked pixels?",
809 default=True,
810 )
811 doSaturationInterpolation = pexConfig.Field(
812 dtype=bool,
813 doc="Perform interpolation over pixels masked as saturated?"
814 " NB: This is independent of doSaturation; if that is False this plane"
815 " will likely be blank, resulting in a no-op here.",
816 default=True,
817 )
818 doNanInterpolation = pexConfig.Field(
819 dtype=bool,
820 doc="Perform interpolation over pixels masked as NaN?"
821 " NB: This is independent of doNanMasking; if that is False this plane"
822 " will likely be blank, resulting in a no-op here.",
823 default=True,
824 )
825 doNanInterpAfterFlat = pexConfig.Field(
826 dtype=bool,
827 doc=("If True, ensure we interpolate NaNs after flat-fielding, even if we "
828 "also have to interpolate them before flat-fielding."),
829 default=False,
830 )
831 maskListToInterpolate = pexConfig.ListField(
832 dtype=str,
833 doc="List of mask planes that should be interpolated.",
834 default=['SAT', 'BAD'],
835 )
836 doSaveInterpPixels = pexConfig.Field(
837 dtype=bool,
838 doc="Save a copy of the pre-interpolated pixel values?",
839 default=False,
840 )
841 useLegacyInterp = pexConfig.Field(
842 dtype=bool,
843 doc="Use the legacy interpolation algorithm. If False use Gaussian Process.",
844 default=True,
845 )
847 # Default photometric calibration options.
848 fluxMag0T1 = pexConfig.DictField(
849 keytype=str,
850 itemtype=float,
851 doc="The approximate flux of a zero-magnitude object in a one-second exposure, per filter.",
852 default=dict((f, pow(10.0, 0.4*m)) for f, m in (("Unknown", 28.0),
853 ))
854 )
855 defaultFluxMag0T1 = pexConfig.Field(
856 dtype=float,
857 doc="Default value for fluxMag0T1 (for an unrecognized filter).",
858 default=pow(10.0, 0.4*28.0)
859 )
861 # Vignette correction configuration.
862 doVignette = pexConfig.Field(
863 dtype=bool,
864 doc=("Compute and attach the validPolygon defining the unvignetted region to the exposure "
865 "according to vignetting parameters?"),
866 default=False,
867 )
868 doMaskVignettePolygon = pexConfig.Field(
869 dtype=bool,
870 doc=("Add a mask bit for pixels within the vignetted region. Ignored if doVignette "
871 "is False"),
872 default=True,
873 )
874 vignetteValue = pexConfig.Field(
875 dtype=float,
876 doc="Value to replace image array pixels with in the vignetted region? Ignored if None.",
877 optional=True,
878 default=None,
879 )
880 vignette = pexConfig.ConfigurableField(
881 target=VignetteTask,
882 doc="Vignetting task.",
883 )
885 # Transmission curve configuration.
886 doAttachTransmissionCurve = pexConfig.Field(
887 dtype=bool,
888 default=False,
889 doc="Construct and attach a wavelength-dependent throughput curve for this CCD image?"
890 )
891 doUseOpticsTransmission = pexConfig.Field(
892 dtype=bool,
893 default=True,
894 doc="Load and use transmission_optics (if doAttachTransmissionCurve is True)?"
895 )
896 doUseFilterTransmission = pexConfig.Field(
897 dtype=bool,
898 default=True,
899 doc="Load and use transmission_filter (if doAttachTransmissionCurve is True)?"
900 )
901 doUseSensorTransmission = pexConfig.Field(
902 dtype=bool,
903 default=True,
904 doc="Load and use transmission_sensor (if doAttachTransmissionCurve is True)?"
905 )
906 doUseAtmosphereTransmission = pexConfig.Field(
907 dtype=bool,
908 default=True,
909 doc="Load and use transmission_atmosphere (if doAttachTransmissionCurve is True)?"
910 )
912 # Illumination correction.
913 doIlluminationCorrection = pexConfig.Field(
914 dtype=bool,
915 default=False,
916 doc="Perform illumination correction?"
917 )
918 illuminationCorrectionDataProductName = pexConfig.Field(
919 dtype=str,
920 doc="Name of the illumination correction data product.",
921 default="illumcor",
922 )
923 illumScale = pexConfig.Field(
924 dtype=float,
925 doc="Scale factor for the illumination correction.",
926 default=1.0,
927 )
928 illumFilters = pexConfig.ListField(
929 dtype=str,
930 default=[],
931 doc="Only perform illumination correction for these filters."
932 )
934 # Calculate image quality statistics?
935 doStandardStatistics = pexConfig.Field(
936 dtype=bool,
937 doc="Should standard image quality statistics be calculated?",
938 default=True,
939 )
940 # Calculate additional statistics?
941 doCalculateStatistics = pexConfig.Field(
942 dtype=bool,
943 doc="Should additional ISR statistics be calculated?",
944 default=False,
945 )
946 isrStats = pexConfig.ConfigurableField(
947 target=IsrStatisticsTask,
948 doc="Task to calculate additional statistics.",
949 )
951 # Make binned images?
952 doBinnedExposures = pexConfig.Field(
953 dtype=bool,
954 doc="Should binned exposures be calculated?",
955 default=False,
956 )
957 binning = pexConfig.ConfigurableField(
958 target=BinImageDataTask,
959 doc="Task to bin the exposure.",
960 )
961 binFactor1 = pexConfig.Field(
962 dtype=int,
963 doc="Binning factor for first binned exposure. This is intended for a finely binned output.",
964 default=8,
965 check=lambda x: x > 1,
966 )
967 binFactor2 = pexConfig.Field(
968 dtype=int,
969 doc="Binning factor for second binned exposure. This is intended for a coarsely binned output.",
970 default=64,
971 check=lambda x: x > 1,
972 )
974 # Write the outputs to disk. If ISR is run as a subtask, this may not
975 # be needed.
976 doWrite = pexConfig.Field(
977 dtype=bool,
978 doc="Persist postISRCCD?",
979 default=True,
980 )
982 def validate(self):
983 super().validate()
984 if self.doFlat and self.doApplyGains: 984 ↛ 985line 984 didn't jump to line 985 because the condition on line 984 was never true
985 raise ValueError("You may not specify both doFlat and doApplyGains")
986 if self.doBiasBeforeOverscan and self.doTrimToMatchCalib: 986 ↛ 987line 986 didn't jump to line 987 because the condition on line 986 was never true
987 raise ValueError("You may not specify both doBiasBeforeOverscan and doTrimToMatchCalib")
988 if self.doSaturationInterpolation and self.saturatedMaskName not in self.maskListToInterpolate: 988 ↛ 989line 988 didn't jump to line 989 because the condition on line 988 was never true
989 self.maskListToInterpolate.append(self.saturatedMaskName)
990 if not self.doSaturationInterpolation and self.saturatedMaskName in self.maskListToInterpolate:
991 self.maskListToInterpolate.remove(self.saturatedMaskName)
992 if self.doNanInterpolation and "UNMASKEDNAN" not in self.maskListToInterpolate:
993 self.maskListToInterpolate.append("UNMASKEDNAN")
994 if self.ampOffset.doApplyAmpOffset and not self.doAmpOffset: 994 ↛ 995line 994 didn't jump to line 995 because the condition on line 994 was never true
995 raise ValueError("ampOffset.doApplyAmpOffset requires doAmpOffset to be True.")
998class IsrTask(pipeBase.PipelineTask):
999 """Apply common instrument signature correction algorithms to a raw frame.
1001 The process for correcting imaging data is very similar from
1002 camera to camera. This task provides a vanilla implementation of
1003 doing these corrections, including the ability to turn certain
1004 corrections off if they are not needed. The inputs to the primary
1005 method, `run()`, are a raw exposure to be corrected and the
1006 calibration data products. The raw input is a single chip sized
1007 mosaic of all amps including overscans and other non-science
1008 pixels.
1010 The __init__ method sets up the subtasks for ISR processing, using
1011 the defaults from `lsst.ip.isr`.
1013 Parameters
1014 ----------
1015 args : `list`
1016 Positional arguments passed to the Task constructor.
1017 None used at this time.
1018 kwargs : `dict`, optional
1019 Keyword arguments passed on to the Task constructor.
1020 None used at this time.
1021 """
1022 ConfigClass = IsrTaskConfig
1023 _DefaultName = "isr"
1025 def __init__(self, **kwargs):
1026 super().__init__(**kwargs)
1027 self.makeSubtask("assembleCcd")
1028 self.makeSubtask("crosstalk")
1029 self.makeSubtask("strayLight")
1030 self.makeSubtask("fringe")
1031 self.makeSubtask("masking")
1032 self.makeSubtask("overscan")
1033 self.makeSubtask("vignette")
1034 self.makeSubtask("ampOffset")
1035 self.makeSubtask("deferredChargeCorrection")
1036 self.makeSubtask("isrStats")
1037 self.makeSubtask("binning")
1039 def runQuantum(self, butlerQC, inputRefs, outputRefs):
1040 inputs = butlerQC.get(inputRefs)
1042 try:
1043 inputs['detectorNum'] = inputRefs.ccdExposure.dataId['detector']
1044 except Exception as e:
1045 raise ValueError("Failure to find valid detectorNum value for Dataset %s: %s." %
1046 (inputRefs, e))
1048 detector = inputs['ccdExposure'].getDetector()
1050 # This is use for header provenance.
1051 additionalInputDates = {}
1053 if self.config.doCrosstalk is True:
1054 # Crosstalk sources need to be defined by the pipeline
1055 # yaml if they exist.
1056 if 'crosstalk' in inputs and inputs['crosstalk'] is not None:
1057 if not isinstance(inputs['crosstalk'], CrosstalkCalib):
1058 inputs['crosstalk'] = CrosstalkCalib.fromTable(inputs['crosstalk'])
1059 else:
1060 coeffVector = (self.config.crosstalk.crosstalkValues
1061 if self.config.crosstalk.useConfigCoefficients else None)
1062 crosstalkCalib = CrosstalkCalib().fromDetector(detector, coeffVector=coeffVector)
1063 inputs['crosstalk'] = crosstalkCalib
1064 if inputs['crosstalk'].interChip and len(inputs['crosstalk'].interChip) > 0:
1065 if 'crosstalkSources' not in inputs:
1066 self.log.warning("No crosstalkSources found for chip with interChip terms!")
1068 if self.doLinearize(detector) is True:
1069 if 'linearizer' in inputs:
1070 if isinstance(inputs['linearizer'], dict):
1071 linearizer = linearize.Linearizer(detector=detector, log=self.log)
1072 linearizer.fromYaml(inputs['linearizer'])
1073 self.log.warning("Dictionary linearizers will be deprecated in DM-28741.")
1074 elif isinstance(inputs['linearizer'], numpy.ndarray):
1075 linearizer = linearize.Linearizer(table=inputs.get('linearizer', None),
1076 detector=detector,
1077 log=self.log)
1078 self.log.warning("Bare lookup table linearizers will be deprecated in DM-28741.")
1079 else:
1080 linearizer = inputs['linearizer']
1081 self.log.info("Loading linearizer from the Butler.")
1082 linearizer.log = self.log
1083 inputs['linearizer'] = linearizer
1084 else:
1085 inputs['linearizer'] = linearize.Linearizer(detector=detector, log=self.log)
1086 self.log.info("Constructing linearizer from cameraGeom information.")
1088 if self.config.doDefect is True:
1089 if "defects" in inputs and inputs['defects'] is not None:
1090 # defects is loaded as a BaseCatalog with columns
1091 # x0, y0, width, height. Masking expects a list of defects
1092 # defined by their bounding box
1093 if not isinstance(inputs["defects"], Defects):
1094 inputs["defects"] = Defects.fromTable(inputs["defects"])
1096 # Load the correct style of brighter-fatter kernel, and repack
1097 # the information as a numpy array.
1098 brighterFatterSource = None
1099 if self.config.doBrighterFatter:
1100 brighterFatterKernel = inputs.pop('newBFKernel', None)
1101 if brighterFatterKernel is None:
1102 # This type of kernel must be in (y, x) index
1103 # ordering, as it used directly as the .array
1104 # component of the afwImage kernel.
1105 brighterFatterKernel = inputs.get('bfKernel', None)
1106 brighterFatterSource = 'bfKernel'
1107 additionalInputDates[brighterFatterSource] = self.extractCalibDate(brighterFatterKernel)
1109 if brighterFatterKernel is None:
1110 # This was requested by the config, but none were found.
1111 raise RuntimeError("No brighter-fatter kernel was supplied.")
1112 elif not isinstance(brighterFatterKernel, numpy.ndarray):
1113 # This is a ISR calib kernel. These kernels are
1114 # generated in (x, y) index ordering, and need to be
1115 # transposed to be used directly as the .array
1116 # component of the afwImage kernel. This is done
1117 # explicitly below when setting the ``bfKernel``
1118 # input.
1119 brighterFatterSource = 'newBFKernel'
1120 additionalInputDates[brighterFatterSource] = self.extractCalibDate(brighterFatterKernel)
1122 detName = detector.getName()
1123 level = brighterFatterKernel.level
1125 # This is expected to be a dictionary of amp-wise gains.
1126 inputs['bfGains'] = brighterFatterKernel.gain
1127 if self.config.brighterFatterLevel == 'DETECTOR':
1128 kernel = None
1129 if level == 'DETECTOR':
1130 if detName in brighterFatterKernel.detKernels:
1131 kernel = brighterFatterKernel.detKernels[detName]
1132 else:
1133 raise RuntimeError("Failed to extract kernel from new-style BF kernel.")
1134 elif level == 'AMP':
1135 self.log.warning("Making DETECTOR level kernel from AMP based brighter "
1136 "fatter kernels.")
1137 brighterFatterKernel.makeDetectorKernelFromAmpwiseKernels(detName)
1138 kernel = brighterFatterKernel.detKernels[detName]
1139 if kernel is None:
1140 raise RuntimeError("Could not identify brighter-fatter kernel!")
1141 # Do the one single transpose here so the kernel
1142 # can be directly loaded into the afwImage .array
1143 # component.
1144 inputs['bfKernel'] = numpy.transpose(kernel)
1145 elif self.config.brighterFatterLevel == 'AMP':
1146 raise NotImplementedError("Per-amplifier brighter-fatter correction not implemented")
1148 if self.config.doFringe is True and self.fringe.checkFilter(inputs['ccdExposure']):
1149 expId = inputs['ccdExposure'].info.id
1150 inputs['fringes'] = self.fringe.loadFringes(inputs['fringes'],
1151 expId=expId,
1152 assembler=self.assembleCcd
1153 if self.config.doAssembleIsrExposures else None)
1154 else:
1155 inputs['fringes'] = pipeBase.Struct(fringes=None)
1157 if self.config.doStrayLight is True and self.strayLight.checkFilter(inputs['ccdExposure']):
1158 if 'strayLightData' not in inputs:
1159 inputs['strayLightData'] = None
1161 if self.config.doHeaderProvenance:
1162 # Add calibration provenanace info to header.
1163 exposureMetadata = inputs['ccdExposure'].getMetadata()
1165 # These inputs change name during this step. These should
1166 # have matching entries in the additionalInputDates dict.
1167 additionalInputs = []
1168 if self.config.doBrighterFatter:
1169 additionalInputs.append(brighterFatterSource)
1171 for inputName in sorted(list(inputs.keys()) + additionalInputs):
1172 reference = getattr(inputRefs, inputName, None)
1173 if reference is not None and hasattr(reference, "run"):
1174 runKey = f"LSST CALIB RUN {inputName.upper()}"
1175 runValue = reference.run
1176 idKey = f"LSST CALIB UUID {inputName.upper()}"
1177 idValue = str(reference.id)
1178 dateKey = f"LSST CALIB DATE {inputName.upper()}"
1180 if inputName in additionalInputDates:
1181 dateValue = additionalInputDates[inputName]
1182 else:
1183 dateValue = self.extractCalibDate(inputs[inputName])
1185 exposureMetadata[runKey] = runValue
1186 exposureMetadata[idKey] = idValue
1187 exposureMetadata[dateKey] = dateValue
1189 outputs = self.run(**inputs)
1190 butlerQC.put(outputs, outputRefs)
1192 @timeMethod
1193 def run(self, ccdExposure, *, camera=None, bias=None, linearizer=None,
1194 crosstalk=None, crosstalkSources=None,
1195 dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None,
1196 fringes=pipeBase.Struct(fringes=None), opticsTransmission=None, filterTransmission=None,
1197 sensorTransmission=None, atmosphereTransmission=None,
1198 detectorNum=None, strayLightData=None, illumMaskedImage=None,
1199 deferredChargeCalib=None,
1200 ):
1201 """Perform instrument signature removal on an exposure.
1203 Steps included in the ISR processing, in order performed, are:
1205 - saturation and suspect pixel masking
1206 - overscan subtraction
1207 - CCD assembly of individual amplifiers
1208 - bias subtraction
1209 - variance image construction
1210 - linearization of non-linear response
1211 - crosstalk masking
1212 - brighter-fatter correction
1213 - dark subtraction
1214 - fringe correction
1215 - stray light subtraction
1216 - flat correction
1217 - masking of known defects and camera specific features
1218 - vignette calculation
1219 - appending transmission curve and distortion model
1221 Parameters
1222 ----------
1223 ccdExposure : `lsst.afw.image.Exposure`
1224 The raw exposure that is to be run through ISR. The
1225 exposure is modified by this method.
1226 camera : `lsst.afw.cameraGeom.Camera`, optional
1227 The camera geometry for this exposure. Required if
1228 one or more of ``ccdExposure``, ``bias``, ``dark``, or
1229 ``flat`` does not have an associated detector.
1230 bias : `lsst.afw.image.Exposure`, optional
1231 Bias calibration frame.
1232 linearizer : `lsst.ip.isr.linearize.LinearizeBase`, optional
1233 Functor for linearization.
1234 crosstalk : `lsst.ip.isr.crosstalk.CrosstalkCalib`, optional
1235 Calibration for crosstalk.
1236 crosstalkSources : `list`, optional
1237 List of possible crosstalk sources.
1238 dark : `lsst.afw.image.Exposure`, optional
1239 Dark calibration frame.
1240 flat : `lsst.afw.image.Exposure`, optional
1241 Flat calibration frame.
1242 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
1243 Photon transfer curve dataset, with, e.g., gains
1244 and read noise.
1245 bfKernel : `numpy.ndarray`, optional
1246 Brighter-fatter kernel.
1247 bfGains : `dict` of `float`, optional
1248 Gains used to override the detector's nominal gains for the
1249 brighter-fatter correction. A dict keyed by amplifier name for
1250 the detector in question.
1251 defects : `lsst.ip.isr.Defects`, optional
1252 List of defects.
1253 fringes : `lsst.pipe.base.Struct`, optional
1254 Struct containing the fringe correction data, with
1255 elements:
1257 ``fringes``
1258 fringe calibration frame (`lsst.afw.image.Exposure`)
1259 ``seed``
1260 random seed derived from the ``ccdExposureId`` for random
1261 number generator (`numpy.uint32`)
1262 opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional
1263 A ``TransmissionCurve`` that represents the throughput of the,
1264 optics, to be evaluated in focal-plane coordinates.
1265 filterTransmission : `lsst.afw.image.TransmissionCurve`
1266 A ``TransmissionCurve`` that represents the throughput of the
1267 filter itself, to be evaluated in focal-plane coordinates.
1268 sensorTransmission : `lsst.afw.image.TransmissionCurve`
1269 A ``TransmissionCurve`` that represents the throughput of the
1270 sensor itself, to be evaluated in post-assembly trimmed detector
1271 coordinates.
1272 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1273 A ``TransmissionCurve`` that represents the throughput of the
1274 atmosphere, assumed to be spatially constant.
1275 detectorNum : `int`, optional
1276 The integer number for the detector to process.
1277 strayLightData : `object`, optional
1278 Opaque object containing calibration information for stray-light
1279 correction. If `None`, no correction will be performed.
1280 illumMaskedImage : `lsst.afw.image.MaskedImage`, optional
1281 Illumination correction image.
1283 Returns
1284 -------
1285 result : `lsst.pipe.base.Struct`
1286 Result struct with component:
1288 ``exposure``
1289 The fully ISR corrected exposure.
1290 (`lsst.afw.image.Exposure`)
1291 ``outputExposure``
1292 An alias for ``exposure``. (`lsst.afw.image.Exposure`)
1293 ``ossThumb``
1294 Thumbnail image of the exposure after overscan subtraction.
1295 (`numpy.ndarray`)
1296 ``flattenedThumb``
1297 Thumbnail image of the exposure after flat-field correction.
1298 (`numpy.ndarray`)
1299 ``outputStatistics``
1300 Values of the additional statistics calculated.
1302 Raises
1303 ------
1304 RuntimeError
1305 Raised if a configuration option is set to `True`, but the
1306 required calibration data has not been specified.
1308 Notes
1309 -----
1310 The current processed exposure can be viewed by setting the
1311 appropriate `lsstDebug` entries in the ``debug.display``
1312 dictionary. The names of these entries correspond to some of
1313 the `IsrTaskConfig` Boolean options, with the value denoting the
1314 frame to use. The exposure is shown inside the matching
1315 option check and after the processing of that step has
1316 finished. The steps with debug points are:
1318 * doAssembleCcd
1319 * doBias
1320 * doCrosstalk
1321 * doBrighterFatter
1322 * doDark
1323 * doFringe
1324 * doStrayLight
1325 * doFlat
1327 In addition, setting the ``postISRCCD`` entry displays the
1328 exposure after all ISR processing has finished.
1329 """
1331 ccdExposure = self.ensureExposure(ccdExposure, camera, detectorNum)
1332 bias = self.ensureExposure(bias, camera, detectorNum)
1333 dark = self.ensureExposure(dark, camera, detectorNum)
1334 flat = self.ensureExposure(flat, camera, detectorNum)
1336 ccd = ccdExposure.getDetector()
1337 filterLabel = ccdExposure.getFilter()
1338 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
1340 if not ccd: 1340 ↛ 1341line 1340 didn't jump to line 1341 because the condition on line 1340 was never true
1341 assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd."
1342 ccd = [FakeAmp(ccdExposure, self.config)]
1344 # Validate Input
1345 if self.config.doBias and bias is None: 1345 ↛ 1346line 1345 didn't jump to line 1346 because the condition on line 1345 was never true
1346 raise RuntimeError("Must supply a bias exposure if config.doBias=True.")
1347 if self.doLinearize(ccd) and linearizer is None: 1347 ↛ 1348line 1347 didn't jump to line 1348 because the condition on line 1347 was never true
1348 raise RuntimeError("Must supply a linearizer if config.doLinearize=True for this detector.")
1349 if self.config.doBrighterFatter and bfKernel is None: 1349 ↛ 1350line 1349 didn't jump to line 1350 because the condition on line 1349 was never true
1350 raise RuntimeError("Must supply a kernel if config.doBrighterFatter=True.")
1351 if self.config.doDark and dark is None: 1351 ↛ 1352line 1351 didn't jump to line 1352 because the condition on line 1351 was never true
1352 raise RuntimeError("Must supply a dark exposure if config.doDark=True.")
1353 if self.config.doFlat and flat is None: 1353 ↛ 1354line 1353 didn't jump to line 1354 because the condition on line 1353 was never true
1354 raise RuntimeError("Must supply a flat exposure if config.doFlat=True.")
1355 if self.config.doDefect and defects is None: 1355 ↛ 1356line 1355 didn't jump to line 1356 because the condition on line 1355 was never true
1356 raise RuntimeError("Must supply defects if config.doDefect=True.")
1357 if (self.config.doFringe and physicalFilter in self.fringe.config.filters 1357 ↛ 1363line 1357 didn't jump to line 1363 because the condition on line 1357 was never true
1358 and fringes.fringes is None):
1359 # The `fringes` object needs to be a pipeBase.Struct, as
1360 # we use it as a `dict` for the parameters of
1361 # `FringeTask.run()`. The `fringes.fringes` `list` may
1362 # not be `None` if `doFringe=True`. Otherwise, raise.
1363 raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct.")
1364 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters 1364 ↛ 1366line 1364 didn't jump to line 1366 because the condition on line 1364 was never true
1365 and illumMaskedImage is None):
1366 raise RuntimeError("Must supply an illumcor if config.doIlluminationCorrection=True.")
1367 if (self.config.doDeferredCharge and deferredChargeCalib is None): 1367 ↛ 1368line 1367 didn't jump to line 1368 because the condition on line 1367 was never true
1368 raise RuntimeError("Must supply a deferred charge calibration if config.doDeferredCharge=True.")
1369 if (self.config.usePtcGains and ptc is None): 1369 ↛ 1370line 1369 didn't jump to line 1370 because the condition on line 1369 was never true
1370 raise RuntimeError("No ptcDataset provided to use PTC gains.")
1371 if (self.config.usePtcReadNoise and ptc is None): 1371 ↛ 1372line 1371 didn't jump to line 1372 because the condition on line 1371 was never true
1372 raise RuntimeError("No ptcDataset provided to use PTC read noise.")
1374 # Validate that the inputs match the exposure configuration.
1375 exposureMetadata = ccdExposure.getMetadata()
1376 doRaise = self.config.doRaiseOnCalibMismatch
1377 keywords = self.config.cameraKeywordsToCompare
1378 if self.config.doBias:
1379 compareCameraKeywords(doRaise, keywords, exposureMetadata, bias, "bias", log=self.log)
1380 self.compareUnits(bias.metadata, "bias")
1381 if self.config.doBrighterFatter:
1382 compareCameraKeywords(doRaise, keywords, exposureMetadata, bfKernel, "bf", log=self.log)
1383 if self.config.doCrosstalk:
1384 compareCameraKeywords(doRaise, keywords, exposureMetadata, crosstalk, "crosstalk", log=self.log)
1385 if self.config.doDark:
1386 compareCameraKeywords(doRaise, keywords, exposureMetadata, dark, "dark", log=self.log)
1387 self.compareUnits(dark.metadata, "dark")
1388 if self.config.doDefect:
1389 compareCameraKeywords(doRaise, keywords, exposureMetadata, defects, "defects", log=self.log)
1390 if self.config.doDeferredCharge: 1390 ↛ 1391line 1390 didn't jump to line 1391 because the condition on line 1390 was never true
1391 compareCameraKeywords(
1392 doRaise,
1393 keywords,
1394 exposureMetadata,
1395 deferredChargeCalib,
1396 "CTI",
1397 log=self.log,
1398 )
1399 if self.config.doFlat:
1400 compareCameraKeywords(doRaise, keywords, exposureMetadata, flat, "flat", log=self.log)
1401 self.compareUnits(flat.metadata, "flat")
1402 if (self.config.doFringe and physicalFilter in self.fringe.config.filters): 1402 ↛ 1403line 1402 didn't jump to line 1403 because the condition on line 1402 was never true
1403 compareCameraKeywords(
1404 doRaise,
1405 keywords,
1406 exposureMetadata,
1407 fringes.fringes,
1408 "fringe",
1409 log=self.log,
1410 )
1411 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters): 1411 ↛ 1412line 1411 didn't jump to line 1412 because the condition on line 1411 was never true
1412 compareCameraKeywords(
1413 doRaise,
1414 keywords,
1415 exposureMetadata,
1416 illumMaskedImage,
1417 "illumination",
1418 log=self.log,
1419 )
1420 if self.doLinearize(ccd): 1420 ↛ 1421line 1420 didn't jump to line 1421 because the condition on line 1420 was never true
1421 compareCameraKeywords(doRaise, keywords, exposureMetadata, linearizer, "linearizer", log=self.log)
1422 if self.config.usePtcGains or self.config.usePtcReadNoise: 1422 ↛ 1423line 1422 didn't jump to line 1423 because the condition on line 1422 was never true
1423 compareCameraKeywords(doRaise, keywords, exposureMetadata, ptc, "PTC", log=self.log)
1424 if self.config.doStrayLight:
1425 compareCameraKeywords(
1426 doRaise,
1427 keywords,
1428 exposureMetadata,
1429 strayLightData,
1430 "straylight",
1431 log=self.log,
1432 )
1434 # Start in adu. Update units to electrons when gain is applied:
1435 # updateVariance, applyGains
1436 # Check if needed during/after BFE correction, CTI correction.
1437 exposureMetadata["LSST ISR UNITS"] = "adu"
1438 exposureMetadata["LSST ISR CROSSTALK APPLIED"] = False
1439 exposureMetadata["LSST ISR LINEARIZER APPLIED"] = False
1440 exposureMetadata["LSST ISR CTI APPLIED"] = False
1441 exposureMetadata["LSST ISR BIAS APPLIED"] = False
1442 exposureMetadata["LSST ISR DARK APPLIED"] = False
1443 exposureMetadata["LSST ISR BF APPLIED"] = False
1444 exposureMetadata["LSST ISR FLAT APPLIED"] = False
1445 exposureMetadata["LSST ISR DEFECTS APPLIED"] = False
1447 # Begin ISR processing.
1448 if self.config.doConvertIntToFloat:
1449 self.log.info("Converting exposure to floating point values.")
1450 ccdExposure = self.convertIntToFloat(ccdExposure)
1452 if self.config.doBias and self.config.doBiasBeforeOverscan: 1452 ↛ 1453line 1452 didn't jump to line 1453 because the condition on line 1452 was never true
1453 self.log.info("Applying bias correction.")
1454 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1455 trimToFit=self.config.doTrimToMatchCalib)
1456 self.debugView(ccdExposure, "doBias")
1457 ccdExposure.metadata["LSST ISR BIAS APPLIED"] = True
1459 # Amplifier level processing.
1460 overscans = []
1462 if self.config.doOverscan and self.config.overscan.doParallelOverscan:
1463 # This will attempt to mask bleed pixels across all amplifiers.
1464 self.overscan.maskParallelOverscan(ccdExposure, ccd)
1466 for amp in ccd:
1467 # if ccdExposure is one amp,
1468 # check for coverage to prevent performing ops multiple times
1469 if ccdExposure.getBBox().contains(amp.getBBox()): 1469 ↛ 1518line 1469 didn't jump to line 1518 because the condition on line 1469 was always true
1470 # Check for fully masked bad amplifiers,
1471 # and generate masks for SUSPECT and SATURATED values.
1472 badAmp = self.maskAmplifier(ccdExposure, amp, defects)
1474 if self.config.doOverscan and not badAmp:
1475 # Overscan correction on amp-by-amp basis.
1476 overscanResults = self.overscanCorrection(ccdExposure, amp)
1477 self.log.debug("Corrected overscan for amplifier %s.", amp.getName())
1478 if overscanResults is not None and \ 1478 ↛ 1516line 1478 didn't jump to line 1516 because the condition on line 1478 was always true
1479 self.config.qa is not None and self.config.qa.saveStats is True:
1480 if isinstance(overscanResults.overscanMean, float):
1481 # Only serial overscan was run
1482 mean = overscanResults.overscanMean
1483 median = overscanResults.overscanMedian
1484 sigma = overscanResults.overscanSigma
1485 residMean = overscanResults.residualMean
1486 residMedian = overscanResults.residualMedian
1487 residSigma = overscanResults.residualSigma
1488 else:
1489 # Both serial and parallel overscan were
1490 # run. Only report serial here.
1491 mean = overscanResults.overscanMean[0]
1492 median = overscanResults.overscanMedian[0]
1493 sigma = overscanResults.overscanSigma[0]
1494 residMean = overscanResults.residualMean[0]
1495 residMedian = overscanResults.residualMedian[0]
1496 residSigma = overscanResults.residualSigma[0]
1498 self.metadata[f"FIT MEDIAN {amp.getName()}"] = median
1499 self.metadata[f"FIT MEAN {amp.getName()}"] = mean
1500 self.metadata[f"FIT STDEV {amp.getName()}"] = sigma
1501 self.log.debug(" Overscan stats for amplifer %s: %f +/- %f",
1502 amp.getName(), mean, sigma)
1504 self.metadata[f"RESIDUAL MEDIAN {amp.getName()}"] = residMedian
1505 self.metadata[f"RESIDUAL MEAN {amp.getName()}"] = residMean
1506 self.metadata[f"RESIDUAL STDEV {amp.getName()}"] = residSigma
1507 self.log.debug(" Overscan stats for amplifer %s after correction: %f +/- %f",
1508 amp.getName(), residMean, residSigma)
1510 ccdExposure.getMetadata().set('OVERSCAN', "Overscan corrected")
1511 else:
1512 if badAmp: 1512 ↛ 1513line 1512 didn't jump to line 1513 because the condition on line 1512 was never true
1513 self.log.warning("Amplifier %s is bad.", amp.getName())
1514 overscanResults = None
1516 overscans.append(overscanResults if overscanResults is not None else None)
1517 else:
1518 self.log.info("Skipped OSCAN for %s.", amp.getName())
1520 # Define an effective PTC that will contain the gain and readout
1521 # noise to be used throughout the ISR task.
1522 ptc = self.defineEffectivePtc(ptc, ccd, bfGains, overscans, exposureMetadata)
1524 if self.config.doDeferredCharge: 1524 ↛ 1525line 1524 didn't jump to line 1525 because the condition on line 1524 was never true
1525 self.log.info("Applying deferred charge/CTI correction.")
1526 self.deferredChargeCorrection.run(
1527 ccdExposure,
1528 deferredChargeCalib,
1529 gains=ptc.gain,
1530 )
1531 self.debugView(ccdExposure, "doDeferredCharge")
1532 ccdExposure.metadata["LSST ISR CTI APPLIED"] = True
1534 if self.config.doCrosstalk and self.config.doCrosstalkBeforeAssemble: 1534 ↛ 1535line 1534 didn't jump to line 1535 because the condition on line 1534 was never true
1535 self.log.info("Applying crosstalk correction.")
1536 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1537 crosstalkSources=crosstalkSources, camera=camera)
1538 self.debugView(ccdExposure, "doCrosstalk")
1539 ccdExposure.metadata["LSST ISR CROSSTALK APPLIED"] = True
1541 if self.config.doAssembleCcd:
1542 self.log.info("Assembling CCD from amplifiers.")
1543 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1545 if self.config.expectWcs and not ccdExposure.getWcs(): 1545 ↛ 1546line 1545 didn't jump to line 1546 because the condition on line 1545 was never true
1546 self.log.warning("No WCS found in input exposure.")
1547 self.debugView(ccdExposure, "doAssembleCcd")
1549 ossThumb = None
1550 if self.config.qa.doThumbnailOss:
1551 ossThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1553 if self.config.doBias and not self.config.doBiasBeforeOverscan:
1554 self.log.info("Applying bias correction.")
1555 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1556 trimToFit=self.config.doTrimToMatchCalib)
1557 self.debugView(ccdExposure, "doBias")
1558 ccdExposure.metadata["LSST ISR BIAS APPLIED"] = True
1560 if self.config.doVariance:
1561 for amp in ccd:
1562 if ccdExposure.getBBox().contains(amp.getBBox()): 1562 ↛ 1561line 1562 didn't jump to line 1561 because the condition on line 1562 was always true
1563 self.log.debug("Constructing variance map for amplifer %s.", amp.getName())
1564 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1565 self.updateVariance(ampExposure, amp, ptc)
1567 if self.config.qa is not None and self.config.qa.saveStats is True: 1567 ↛ 1561line 1567 didn't jump to line 1561 because the condition on line 1567 was always true
1568 qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
1569 afwMath.MEDIAN | afwMath.STDEVCLIP)
1570 self.metadata[f"ISR VARIANCE {amp.getName()} MEDIAN"] = \
1571 qaStats.getValue(afwMath.MEDIAN)
1572 self.metadata[f"ISR VARIANCE {amp.getName()} STDEV"] = \
1573 qaStats.getValue(afwMath.STDEVCLIP)
1574 self.log.debug(" Variance stats for amplifer %s: %f +/- %f.",
1575 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1576 qaStats.getValue(afwMath.STDEVCLIP))
1577 if self.config.maskNegativeVariance:
1578 self.maskNegativeVariance(ccdExposure)
1580 if self.doLinearize(ccd): 1580 ↛ 1581line 1580 didn't jump to line 1581 because the condition on line 1580 was never true
1581 self.log.info("Applying linearizer.")
1582 linearizer.applyLinearity(image=ccdExposure.getMaskedImage().getImage(),
1583 detector=ccd, log=self.log)
1584 ccdExposure.metadata["LSST ISR LINEARIZER APPLIED"] = True
1586 if self.config.doCrosstalk and not self.config.doCrosstalkBeforeAssemble:
1587 self.log.info("Applying crosstalk correction.")
1588 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1589 crosstalkSources=crosstalkSources)
1590 self.debugView(ccdExposure, "doCrosstalk")
1591 ccdExposure.metadata["LSST ISR CROSSTALK APPLIED"] = True
1593 # Masking block. Optionally mask known defects,NaN/inf pixels,
1594 # widen trails, and do anything else the camera needs. Saturated and
1595 # suspect pixels have already been masked.
1596 if self.config.doDefect:
1597 self.log.info("Masking defects.")
1598 self.maskDefect(ccdExposure, defects)
1599 ccdExposure.metadata["LSST ISR DEFECTS APPLIED"] = True
1601 if self.config.numEdgeSuspect > 0:
1602 self.log.info("Masking edges as SUSPECT.")
1603 self.maskEdges(ccdExposure, numEdgePixels=self.config.numEdgeSuspect,
1604 maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
1606 if self.config.doNanMasking:
1607 self.log.info("Masking non-finite (NAN, inf) value pixels.")
1608 self.maskNan(ccdExposure)
1610 if self.config.doWidenSaturationTrails:
1611 self.log.info("Widening saturation trails.")
1612 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1614 if self.config.doCameraSpecificMasking:
1615 self.log.info("Masking regions for camera specific reasons.")
1616 self.masking.run(ccdExposure)
1618 if self.config.doBrighterFatter:
1619 # We need to apply flats and darks before we can interpolate, and
1620 # we need to interpolate before we do B-F, but we do B-F without
1621 # the flats and darks applied so we can work in units of electrons
1622 # or holes. This context manager applies and then removes the darks
1623 # and flats.
1624 #
1625 # We also do not want to interpolate values here, so operate on
1626 # temporary images so we can apply only the BF-correction and roll
1627 # back the interpolation.
1628 interpExp = ccdExposure.clone()
1629 with self.flatContext(interpExp, flat, dark):
1630 isrFunctions.interpolateFromMask(
1631 maskedImage=interpExp.getMaskedImage(),
1632 fwhm=self.config.fwhm,
1633 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1634 maskNameList=list(self.config.brighterFatterMaskListToInterpolate),
1635 useLegacyInterp=self.config.useLegacyInterp,
1636 )
1637 bfExp = interpExp.clone()
1639 self.log.info("Applying brighter-fatter correction using kernel type %s / gains %s.",
1640 type(bfKernel), type(bfGains))
1641 if self.config.doFluxConservingBrighterFatterCorrection: 1641 ↛ 1642line 1641 didn't jump to line 1642 because the condition on line 1641 was never true
1642 bfResults = fluxConservingBrighterFatterCorrection(
1643 bfExp,
1644 bfKernel,
1645 self.config.brighterFatterMaxIter,
1646 self.config.brighterFatterThreshold,
1647 self.config.brighterFatterApplyGain,
1648 bfGains
1649 )
1650 else:
1651 bfResults = brighterFatterCorrection(
1652 bfExp,
1653 bfKernel,
1654 self.config.brighterFatterMaxIter,
1655 self.config.brighterFatterThreshold,
1656 self.config.brighterFatterApplyGain,
1657 bfGains
1658 )
1659 bfCorrIters = bfResults[1]
1660 self.metadata["LSST ISR BF ITERS"] = bfCorrIters
1661 if bfCorrIters == self.config.brighterFatterMaxIter - 1: 1661 ↛ 1665line 1661 didn't jump to line 1665 because the condition on line 1661 was always true
1662 self.log.warning("Brighter-fatter correction did not converge, final difference %f.",
1663 bfResults[0])
1664 else:
1665 self.log.info("Finished brighter-fatter correction in %d iterations.",
1666 bfResults[1])
1667 image = ccdExposure.getMaskedImage().getImage()
1668 bfCorr = bfExp.getMaskedImage().getImage()
1669 bfCorr -= interpExp.getMaskedImage().getImage()
1670 image += bfCorr
1672 # Applying the brighter-fatter correction applies a
1673 # convolution to the science image. At the edges this
1674 # convolution may not have sufficient valid pixels to
1675 # produce a valid correction. Mark pixels within the size
1676 # of the brighter-fatter kernel as EDGE to warn of this
1677 # fact.
1678 self.log.info("Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1679 self.maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1680 maskPlane="EDGE")
1682 if self.config.brighterFatterMaskGrowSize > 0: 1682 ↛ 1683line 1682 didn't jump to line 1683 because the condition on line 1682 was never true
1683 self.log.info("Growing masks to account for brighter-fatter kernel convolution.")
1684 for maskPlane in self.config.brighterFatterMaskListToInterpolate:
1685 isrFunctions.growMasks(ccdExposure.getMask(),
1686 radius=self.config.brighterFatterMaskGrowSize,
1687 maskNameList=maskPlane,
1688 maskValue=maskPlane)
1690 self.debugView(ccdExposure, "doBrighterFatter")
1691 ccdExposure.metadata["LSST ISR BF APPLIED"] = True
1693 if self.config.doDark:
1694 self.log.info("Applying dark correction.")
1695 self.darkCorrection(ccdExposure, dark)
1696 self.debugView(ccdExposure, "doDark")
1697 ccdExposure.metadata["LSST ISR DARK APPLIED"] = True
1699 if self.config.doFringe and not self.config.fringeAfterFlat: 1699 ↛ 1700line 1699 didn't jump to line 1700 because the condition on line 1699 was never true
1700 self.log.info("Applying fringe correction before flat.")
1701 self.fringe.run(ccdExposure, **fringes.getDict())
1702 self.debugView(ccdExposure, "doFringe")
1704 if self.config.doStrayLight and self.strayLight.check(ccdExposure): 1704 ↛ 1705line 1704 didn't jump to line 1705 because the condition on line 1704 was never true
1705 self.log.info("Checking strayLight correction.")
1706 self.strayLight.run(ccdExposure, strayLightData)
1707 self.debugView(ccdExposure, "doStrayLight")
1709 if self.config.doFlat:
1710 self.log.info("Applying flat correction.")
1711 self.flatCorrection(ccdExposure, flat)
1712 self.debugView(ccdExposure, "doFlat")
1713 ccdExposure.metadata["LSST ISR FLAT APPLIED"] = True
1714 # TODO: DM-49159
1715 # Add metadata re: type of flat.
1717 if self.config.doApplyGains:
1718 self.log.info("Applying gain correction instead of flat.")
1719 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains,
1720 ptcGains=ptc.gain)
1721 exposureMetadata["LSST ISR UNITS"] = "electron"
1723 if self.config.doFringe and self.config.fringeAfterFlat:
1724 self.log.info("Applying fringe correction after flat.")
1725 self.fringe.run(ccdExposure, **fringes.getDict())
1727 if self.config.doVignette:
1728 if self.config.doMaskVignettePolygon: 1728 ↛ 1731line 1728 didn't jump to line 1731 because the condition on line 1728 was always true
1729 self.log.info("Constructing, attaching, and masking vignette polygon.")
1730 else:
1731 self.log.info("Constructing and attaching vignette polygon.")
1732 self.vignettePolygon = self.vignette.run(
1733 exposure=ccdExposure, doUpdateMask=self.config.doMaskVignettePolygon,
1734 vignetteValue=self.config.vignetteValue, log=self.log)
1736 if self.config.doAttachTransmissionCurve:
1737 self.log.info("Adding transmission curves.")
1738 isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission,
1739 filterTransmission=filterTransmission,
1740 sensorTransmission=sensorTransmission,
1741 atmosphereTransmission=atmosphereTransmission)
1743 flattenedThumb = None
1744 if self.config.qa.doThumbnailFlattened:
1745 flattenedThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1747 if self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters: 1747 ↛ 1748line 1747 didn't jump to line 1748 because the condition on line 1747 was never true
1748 self.log.info("Performing illumination correction.")
1749 isrFunctions.illuminationCorrection(ccdExposure.getMaskedImage(),
1750 illumMaskedImage, illumScale=self.config.illumScale,
1751 trimToFit=self.config.doTrimToMatchCalib)
1753 preInterpExp = None
1754 if self.config.doSaveInterpPixels: 1754 ↛ 1755line 1754 didn't jump to line 1755 because the condition on line 1754 was never true
1755 preInterpExp = ccdExposure.clone()
1757 # Reset and interpolate bad pixels.
1758 #
1759 # Large contiguous bad regions (which should have the BAD mask
1760 # bit set) should have their values set to the image median.
1761 # This group should include defects and bad amplifiers. As the
1762 # area covered by these defects are large, there's little
1763 # reason to expect that interpolation would provide a more
1764 # useful value.
1765 #
1766 # Smaller defects can be safely interpolated after the larger
1767 # regions have had their pixel values reset. This ensures
1768 # that the remaining defects adjacent to bad amplifiers (as an
1769 # example) do not attempt to interpolate extreme values.
1770 if self.config.doSetBadRegions:
1771 badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure)
1772 if badPixelCount > 0:
1773 self.log.info("Set %d BAD pixels to %f.", badPixelCount, badPixelValue)
1775 if self.config.doInterpolate:
1776 self.log.info("Interpolating masked pixels.")
1777 isrFunctions.interpolateFromMask(
1778 maskedImage=ccdExposure.getMaskedImage(),
1779 fwhm=self.config.fwhm,
1780 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1781 maskNameList=list(self.config.maskListToInterpolate),
1782 useLegacyInterp=self.config.useLegacyInterp,
1783 )
1785 self.roughZeroPoint(ccdExposure)
1787 # Calculate amp offset corrections within the CCD.
1788 if self.config.doAmpOffset: 1788 ↛ 1789line 1788 didn't jump to line 1789 because the condition on line 1788 was never true
1789 if self.config.ampOffset.doApplyAmpOffset:
1790 self.log.info("Measuring and applying amp offset corrections.")
1791 else:
1792 self.log.info("Measuring amp offset corrections only, without applying them.")
1793 self.ampOffset.run(ccdExposure)
1795 if self.config.doMeasureBackground:
1796 self.log.info("Measuring background level.")
1797 self.measureBackground(ccdExposure, self.config.qa)
1799 if self.config.qa is not None and self.config.qa.saveStats is True: 1799 ↛ 1812line 1799 didn't jump to line 1812 because the condition on line 1799 was always true
1800 for amp in ccd:
1801 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1802 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1803 afwMath.MEDIAN | afwMath.STDEVCLIP)
1804 self.metadata[f"ISR BACKGROUND {amp.getName()} MEDIAN"] = qaStats.getValue(afwMath.MEDIAN)
1805 self.metadata[f"ISR BACKGROUND {amp.getName()} STDEV"] = \
1806 qaStats.getValue(afwMath.STDEVCLIP)
1807 self.log.debug(" Background stats for amplifer %s: %f +/- %f",
1808 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1809 qaStats.getValue(afwMath.STDEVCLIP))
1811 # Calculate standard image quality statistics
1812 if self.config.doStandardStatistics: 1812 ↛ 1840line 1812 didn't jump to line 1840 because the condition on line 1812 was always true
1813 metadata = ccdExposure.getMetadata()
1814 for amp in ccd:
1815 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1816 ampName = amp.getName()
1817 metadata[f"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels(
1818 ampExposure.getMaskedImage(),
1819 [self.config.saturatedMaskName]
1820 )
1821 metadata[f"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels(
1822 ampExposure.getMaskedImage(),
1823 ["BAD"]
1824 )
1825 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1826 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP)
1828 metadata[f"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN)
1829 metadata[f"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN)
1830 metadata[f"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP)
1832 k1 = f"LSST ISR FINAL MEDIAN {ampName}"
1833 k2 = f"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
1834 if self.config.doOverscan and k1 in metadata and k2 in metadata:
1835 metadata[f"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2]
1836 else:
1837 metadata[f"LSST ISR LEVEL {ampName}"] = numpy.nan
1839 # calculate additional statistics.
1840 outputStatistics = None
1841 if self.config.doCalculateStatistics: 1841 ↛ 1842line 1841 didn't jump to line 1842 because the condition on line 1841 was never true
1842 outputStatistics = self.isrStats.run(ccdExposure, serialOverscanResults=overscans,
1843 parallelOverscanResults=[None for _ in overscans],
1844 bias=bias, dark=dark, flat=flat, ptc=ptc,
1845 defects=defects,
1846 doLegacyCtiStatistics=True).results
1848 # do any binning.
1849 outputBin1Exposure = None
1850 outputBin2Exposure = None
1851 if self.config.doBinnedExposures:
1852 self.log.info("Creating binned exposures.")
1853 outputBin1Exposure = self.binning.run(
1854 ccdExposure,
1855 binFactor=self.config.binFactor1,
1856 ).outputData
1857 outputBin2Exposure = self.binning.run(
1858 ccdExposure,
1859 binFactor=self.config.binFactor2,
1860 ).outputData
1862 self.debugView(ccdExposure, "postISRCCD")
1864 return pipeBase.Struct(
1865 exposure=ccdExposure,
1866 ossThumb=ossThumb,
1867 flattenedThumb=flattenedThumb,
1869 outputBin1Exposure=outputBin1Exposure,
1870 outputBin2Exposure=outputBin2Exposure,
1872 preInterpExposure=preInterpExp,
1873 outputExposure=ccdExposure,
1874 outputOssThumbnail=ossThumb,
1875 outputFlattenedThumbnail=flattenedThumb,
1876 outputStatistics=outputStatistics,
1877 )
1879 def defineEffectivePtc(self, ptcDataset, detector, bfGains, overScans, metadata):
1880 """Define an effective Photon Transfer Curve dataset
1881 with nominal gains and noise.
1883 Parameters
1884 ----------
1885 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`
1886 Input Photon Transfer Curve dataset.
1887 detector : `lsst.afw.cameraGeom.Detector`
1888 Detector object.
1889 bfGains : `dict`
1890 Gains from running the brighter-fatter code.
1891 A dict keyed by amplifier name for the detector
1892 in question.
1893 overScans : `list` [`lsst.pipe.base.Struct`]
1894 List of overscanResults structures
1895 metadata : `lsst.daf.base.PropertyList`
1896 Exposure metadata to update gain and read noise
1897 provenance.
1899 Returns
1900 -------
1901 effectivePtc : `lsst.ip.isr.PhotonTransferCurveDataset`
1902 PTC dataset containing gains and readout noise
1903 values to be used throughout
1904 Instrument Signature Removal.
1905 """
1906 amps = detector.getAmplifiers()
1907 ampNames = [amp.getName() for amp in amps]
1908 detName = detector.getName()
1909 effectivePtc = PhotonTransferCurveDataset(ampNames, 'EFFECTIVE_PTC', 1)
1910 boolGainMismatch = False
1911 doWarningPtcValidation = True
1913 for amp, overscanResults in zip(amps, overScans):
1914 ampName = amp.getName()
1915 # Gain:
1916 # Try first with the PTC gains.
1917 gainProvenanceString = "amp"
1918 if self.config.usePtcGains:
1919 gain = ptcDataset.gain[ampName]
1920 gainProvenanceString = "ptc"
1921 self.log.debug("Using gain from Photon Transfer Curve.")
1922 else:
1923 # Try then with the amplifier gain.
1924 # We already have a detector at this point. If there was no
1925 # detector to begin with, one would have been created with
1926 # self.config.gain and self.config.noise. Same comment
1927 # applies for the noise block below.
1928 gain = amp.getGain()
1930 # Check if the gain up to this point differs from the
1931 # gain in bfGains. If so, raise or warn, accordingly.
1932 if not boolGainMismatch and bfGains is not None and ampName in bfGains:
1933 bfGain = bfGains[ampName]
1934 if not math.isclose(gain, bfGain, rel_tol=1e-4): 1934 ↛ 1948line 1934 didn't jump to line 1948 because the condition on line 1934 was always true
1935 if self.config.doRaiseOnCalibMismatch:
1936 raise RuntimeError("Gain mismatch for det %s amp %s: "
1937 "(gain (%s): %s, bfGain: %s)",
1938 detName, ampName, gainProvenanceString,
1939 gain, bfGain)
1940 else:
1941 self.log.warning("Gain mismatch for det %s amp %s: "
1942 "(gain (%s): %s, bfGain: %s)",
1943 detName, ampName, gainProvenanceString,
1944 gain, bfGain)
1945 boolGainMismatch = True
1947 # Gain:
1948 if math.isnan(gain): 1948 ↛ 1949line 1948 didn't jump to line 1949 because the condition on line 1948 was never true
1949 gain = 1.0
1950 self.log.warning("Gain for amp %s set to NaN! Updating to"
1951 " 1.0 to generate Poisson variance.", ampName)
1952 elif gain <= 0: 1952 ↛ 1953line 1952 didn't jump to line 1953 because the condition on line 1952 was never true
1953 patchedGain = 1.0
1954 self.log.warning("Gain for amp %s == %g <= 0; setting to %f.",
1955 ampName, gain, patchedGain)
1956 gain = patchedGain
1958 # Noise:
1959 # Try first with the empirical noise from the overscan.
1960 noiseProvenanceString = "amp"
1961 if self.config.doEmpiricalReadNoise and overscanResults is not None:
1962 noiseProvenanceString = "serial overscan"
1963 if isinstance(overscanResults.residualSigma, float):
1964 # Only serial overscan was run
1965 noise = overscanResults.residualSigma
1966 else:
1967 # Both serial and parallel overscan were
1968 # run. Only report noise from serial here.
1969 noise = overscanResults.residualSigma[0]
1971 # Overscan noise is always in adu; we must standardize
1972 # the read noise attributes of all
1973 # PhotonTransferCurveDataset objects to units of
1974 # electrons.
1975 noise *= gain
1976 elif self.config.usePtcReadNoise:
1977 # Try then with the PTC noise.
1978 self.log.debug("Using noise from Photon Transfer Curve.")
1979 noise = ptcDataset.noise[ampName]
1980 noiseProvenanceString = "ptc"
1981 else:
1982 # Finally, try with the amplifier noise.
1983 # We already have a detector at this point. If there was no
1984 # detector to begin with, one would have been created with
1985 # self.config.gain and self.config.noise.
1986 # Amplifier object read noise is always in electron
1987 noise = amp.getReadNoise()
1989 # PTC Turnoff:
1990 # Copy it over from the input PTC if it's positive. If it's a nan
1991 # set it to a high value.
1992 if ptcDataset is not None:
1993 ptcTurnoff = ptcDataset.ptcTurnoff[ampName]
1994 else:
1995 ptcTurnoff = 2e19
1997 if (isinstance(ptcTurnoff, numbers.Real) and ptcTurnoff > 0):
1998 effectivePtc.ptcTurnoff[ampName] = ptcTurnoff
1999 elif math.isnan(ptcTurnoff): 1999 ↛ 2002line 1999 didn't jump to line 2002 because the condition on line 1999 was always true
2000 effectivePtc.ptcTurnoff[ampName] = 2e19
2002 effectivePtc.gain[ampName] = gain
2003 effectivePtc.noise[ampName] = noise
2004 # Make sure read noise, turnoff, and gain make sense
2005 effectivePtc.validateGainNoiseTurnoffValues(ampName, doWarn=doWarningPtcValidation)
2006 doWarningPtcValidation = False
2008 # These keys are duplicated for compatability with isrTaskLSST
2009 metadata[f"LSST GAIN {amp.getName()}"] = effectivePtc.gain[ampName]
2010 metadata[f"LSST ISR GAIN {amp.getName()}"] = effectivePtc.gain[ampName]
2011 metadata[f"LSST READNOISE {amp.getName()}"] = effectivePtc.noise[ampName]
2012 metadata[f"LSST ISR READNOISE {amp.getName()}"] = effectivePtc.noise[ampName]
2014 self.log.info("Det: %s - Noise provenance: %s, Gain provenance: %s",
2015 detName,
2016 noiseProvenanceString,
2017 gainProvenanceString)
2018 metadata["LSST ISR GAIN SOURCE"] = gainProvenanceString
2019 metadata["LSST ISR NOISE SOURCE"] = noiseProvenanceString
2020 metadata["LSST ISR READNOISE UNITS"] = "electron"
2022 return effectivePtc
2024 def ensureExposure(self, inputExp, camera=None, detectorNum=None):
2025 """Ensure that the data returned by Butler is a fully constructed exp.
2027 ISR requires exposure-level image data for historical reasons, so if we
2028 did not recieve that from Butler, construct it from what we have,
2029 modifying the input in place.
2031 Parameters
2032 ----------
2033 inputExp : `lsst.afw.image` image-type.
2034 The input data structure obtained from Butler.
2035 Can be `lsst.afw.image.Exposure`,
2036 `lsst.afw.image.DecoratedImageU`,
2037 or `lsst.afw.image.ImageF`
2038 camera : `lsst.afw.cameraGeom.camera`, optional
2039 The camera associated with the image. Used to find the appropriate
2040 detector if detector is not already set.
2041 detectorNum : `int`, optional
2042 The detector in the camera to attach, if the detector is not
2043 already set.
2045 Returns
2046 -------
2047 inputExp : `lsst.afw.image.Exposure`
2048 The re-constructed exposure, with appropriate detector parameters.
2050 Raises
2051 ------
2052 TypeError
2053 Raised if the input data cannot be used to construct an exposure.
2054 """
2055 if isinstance(inputExp, afwImage.DecoratedImageU): 2055 ↛ 2056line 2055 didn't jump to line 2056 because the condition on line 2055 was never true
2056 inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp))
2057 elif isinstance(inputExp, afwImage.ImageF): 2057 ↛ 2058line 2057 didn't jump to line 2058 because the condition on line 2057 was never true
2058 inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp))
2059 elif isinstance(inputExp, afwImage.MaskedImageF): 2059 ↛ 2060line 2059 didn't jump to line 2060 because the condition on line 2059 was never true
2060 inputExp = afwImage.makeExposure(inputExp)
2061 elif isinstance(inputExp, afwImage.Exposure):
2062 pass
2063 elif inputExp is None: 2063 ↛ 2067line 2063 didn't jump to line 2067 because the condition on line 2063 was always true
2064 # Assume this will be caught by the setup if it is a problem.
2065 return inputExp
2066 else:
2067 raise TypeError("Input Exposure is not known type in isrTask.ensureExposure: %s." %
2068 (type(inputExp), ))
2070 if inputExp.getDetector() is None: 2070 ↛ 2071line 2070 didn't jump to line 2071 because the condition on line 2070 was never true
2071 if camera is None or detectorNum is None:
2072 raise RuntimeError('Must supply both a camera and detector number when using exposures '
2073 'without a detector set.')
2074 inputExp.setDetector(camera[detectorNum])
2076 return inputExp
2078 @staticmethod
2079 def extractCalibDate(calib):
2080 """Extract common calibration metadata values that will be written to
2081 output header.
2083 Parameters
2084 ----------
2085 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib`
2086 Calibration to pull date information from.
2088 Returns
2089 -------
2090 dateString : `str`
2091 Calibration creation date string to add to header.
2092 """
2093 if hasattr(calib, "getMetadata"):
2094 if 'CALIB_CREATION_DATE' in calib.getMetadata():
2095 return " ".join((calib.getMetadata().get("CALIB_CREATION_DATE", "Unknown"),
2096 calib.getMetadata().get("CALIB_CREATION_TIME", "Unknown")))
2097 else:
2098 return " ".join((calib.getMetadata().get("CALIB_CREATE_DATE", "Unknown"),
2099 calib.getMetadata().get("CALIB_CREATE_TIME", "Unknown")))
2100 else:
2101 return "Unknown Unknown"
2103 def compareUnits(self, calibMetadata, calibName):
2104 """Compare units from calibration to ISR units.
2106 For the regular IsrTask this is used to confirm that calibs
2107 suitable for IsrTaskLSST are not used with the old IsrTask.
2109 Parameters
2110 ----------
2111 calibMetadata : `lsst.daf.base.PropertyList`
2112 Calibration metadata from header.
2113 calibName : `str`
2114 Calibration name for log message.
2115 """
2116 calibUnits = calibMetadata.get("LSST ISR UNITS", "adu")
2117 isrUnits = "adu"
2118 if calibUnits != isrUnits: 2118 ↛ 2119line 2118 didn't jump to line 2119 because the condition on line 2118 was never true
2119 if self.config.doRaiseOnCalibMismatch:
2120 raise RuntimeError(
2121 "Unit mismatch: isr has %s units but %s has %s units",
2122 isrUnits,
2123 calibName,
2124 calibUnits,
2125 )
2126 else:
2127 self.log.warning(
2128 "Unit mismatch: isr has %s units but %s has %s units",
2129 isrUnits,
2130 calibName,
2131 calibUnits,
2132 )
2134 def convertIntToFloat(self, exposure):
2135 """Convert exposure image from uint16 to float.
2137 If the exposure does not need to be converted, the input is
2138 immediately returned. For exposures that are converted to use
2139 floating point pixels, the variance is set to unity and the
2140 mask to zero.
2142 Parameters
2143 ----------
2144 exposure : `lsst.afw.image.Exposure`
2145 The raw exposure to be converted.
2147 Returns
2148 -------
2149 newexposure : `lsst.afw.image.Exposure`
2150 The input ``exposure``, converted to floating point pixels.
2152 Raises
2153 ------
2154 RuntimeError
2155 Raised if the exposure type cannot be converted to float.
2157 """
2158 if isinstance(exposure, afwImage.ExposureF): 2158 ↛ 2162line 2158 didn't jump to line 2162 because the condition on line 2158 was always true
2159 # Nothing to be done
2160 self.log.debug("Exposure already of type float.")
2161 return exposure
2162 if not hasattr(exposure, "convertF"):
2163 raise RuntimeError("Unable to convert exposure (%s) to float." % type(exposure))
2165 newexposure = exposure.convertF()
2166 newexposure.variance[:] = 1
2167 newexposure.mask[:] = 0x0
2169 return newexposure
2171 def maskAmplifier(self, ccdExposure, amp, defects):
2172 """Identify bad amplifiers, saturated and suspect pixels.
2174 Parameters
2175 ----------
2176 ccdExposure : `lsst.afw.image.Exposure`
2177 Input exposure to be masked.
2178 amp : `lsst.afw.cameraGeom.Amplifier`
2179 Catalog of parameters defining the amplifier on this
2180 exposure to mask.
2181 defects : `lsst.ip.isr.Defects`
2182 List of defects. Used to determine if the entire
2183 amplifier is bad.
2185 Returns
2186 -------
2187 badAmp : `Bool`
2188 If this is true, the entire amplifier area is covered by
2189 defects and unusable.
2191 """
2192 maskedImage = ccdExposure.getMaskedImage()
2194 badAmp = False
2196 # Check if entire amp region is defined as a defect
2197 # NB: need to use amp.getBBox() for correct comparison with current
2198 # defects definition.
2199 if defects is not None:
2200 badAmp = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects]))
2202 # In the case of a bad amp, we will set mask to "BAD"
2203 # (here use amp.getRawBBox() for correct association with pixels in
2204 # current ccdExposure).
2205 if badAmp: 2205 ↛ 2206line 2205 didn't jump to line 2206 because the condition on line 2205 was never true
2206 dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
2207 afwImage.PARENT)
2208 maskView = dataView.getMask()
2209 maskView |= maskView.getPlaneBitMask("BAD")
2210 del maskView
2211 return badAmp
2213 # Mask remaining defects after assembleCcd() to allow for defects that
2214 # cross amplifier boundaries. Saturation and suspect pixels can be
2215 # masked now, though.
2216 limits = dict()
2217 if self.config.doSaturation and not badAmp:
2218 # Set to the default from the camera model.
2219 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
2220 # And update if it is set in the config.
2221 if math.isfinite(self.config.saturation):
2222 limits.update({self.config.saturatedMaskName: self.config.saturation})
2223 if self.config.doSuspect and not badAmp:
2224 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
2226 for maskName, maskThreshold in limits.items():
2227 if not math.isnan(maskThreshold): 2227 ↛ 2226line 2227 didn't jump to line 2226 because the condition on line 2227 was always true
2228 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2229 isrFunctions.makeThresholdMask(
2230 maskedImage=dataView,
2231 threshold=maskThreshold,
2232 growFootprints=0,
2233 maskName=maskName
2234 )
2236 # Determine if we've fully masked this amplifier with SUSPECT and
2237 # SAT pixels.
2238 maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
2239 afwImage.PARENT)
2240 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
2241 self.config.suspectMaskName])
2242 if numpy.all(maskView.getArray() & maskVal > 0): 2242 ↛ 2243line 2242 didn't jump to line 2243 because the condition on line 2242 was never true
2243 badAmp = True
2244 maskView |= maskView.getPlaneBitMask("BAD")
2246 return badAmp
2248 def overscanCorrection(self, ccdExposure, amp):
2249 """Apply overscan correction in place.
2251 This method does initial pixel rejection of the overscan
2252 region. The overscan can also be optionally segmented to
2253 allow for discontinuous overscan responses to be fit
2254 separately. The actual overscan subtraction is performed by
2255 the `lsst.ip.isr.overscan.OverscanTask`, which is called here
2256 after the amplifier is preprocessed.
2258 Parameters
2259 ----------
2260 ccdExposure : `lsst.afw.image.Exposure`
2261 Exposure to have overscan correction performed.
2262 amp : `lsst.afw.cameraGeom.Amplifer`
2263 The amplifier to consider while correcting the overscan.
2265 Returns
2266 -------
2267 overscanResults : `lsst.pipe.base.Struct`
2268 Result struct with components:
2270 ``imageFit``
2271 Value or fit subtracted from the amplifier image data.
2272 (scalar or `lsst.afw.image.Image`)
2273 ``overscanFit``
2274 Value or fit subtracted from the overscan image data.
2275 (scalar or `lsst.afw.image.Image`)
2276 ``overscanImage``
2277 Image of the overscan region with the overscan
2278 correction applied. This quantity is used to estimate
2279 the amplifier read noise empirically.
2280 (`lsst.afw.image.Image`)
2281 ``edgeMask``
2282 Mask of the suspect pixels. (`lsst.afw.image.Mask`)
2283 ``overscanMean``
2284 Median overscan fit value. (`float`)
2285 ``overscanSigma``
2286 Clipped standard deviation of the overscan after
2287 correction. (`float`)
2289 Raises
2290 ------
2291 RuntimeError
2292 Raised if the ``amp`` does not contain raw pixel information.
2294 See Also
2295 --------
2296 lsst.ip.isr.overscan.OverscanTask
2297 """
2298 if amp.getRawHorizontalOverscanBBox().isEmpty(): 2298 ↛ 2299line 2298 didn't jump to line 2299 because the condition on line 2298 was never true
2299 self.log.info("ISR_OSCAN: No overscan region. Not performing overscan correction.")
2300 return None
2302 # Perform overscan correction on subregions.
2303 overscanResults = self.overscan.run(ccdExposure, amp)
2305 metadata = ccdExposure.getMetadata()
2306 ampName = amp.getName()
2308 keyBase = "LSST ISR OVERSCAN"
2309 # The overscan statistics units will always match the units of
2310 # the image at the point they are calculated.
2311 metadata[f"{keyBase} SERIAL UNITS"] = metadata.get("LSST ISR UNITS")
2313 # Updated quantities
2314 if isinstance(overscanResults.overscanMean, float):
2315 # Serial overscan correction only:
2316 metadata[f"{keyBase} SERIAL MEAN {ampName}"] = overscanResults.overscanMean
2317 metadata[f"{keyBase} SERIAL MEDIAN {ampName}"] = overscanResults.overscanMedian
2318 metadata[f"{keyBase} SERIAL STDEV {ampName}"] = overscanResults.overscanSigma
2320 metadata[f"{keyBase} RESIDUAL SERIAL MEAN {ampName}"] = overscanResults.residualMean
2321 metadata[f"{keyBase} RESIDUAL SERIAL MEDIAN {ampName}"] = overscanResults.residualMedian
2322 metadata[f"{keyBase} RESIDUAL SERIAL STDEV {ampName}"] = overscanResults.residualSigma
2323 elif isinstance(overscanResults.overscanMean, tuple): 2323 ↛ 2342line 2323 didn't jump to line 2342 because the condition on line 2323 was always true
2324 # Both serial and parallel overscan have run:
2325 metadata[f"{keyBase} PARALLEL UNITS"] = metadata.get("LSST ISR UNITS")
2326 metadata[f"{keyBase} SERIAL MEAN {ampName}"] = overscanResults.overscanMean[0]
2327 metadata[f"{keyBase} SERIAL MEDIAN {ampName}"] = overscanResults.overscanMedian[0]
2328 metadata[f"{keyBase} SERIAL STDEV {ampName}"] = overscanResults.overscanSigma[0]
2330 metadata[f"{keyBase} PARALLEL MEAN {ampName}"] = overscanResults.overscanMean[1]
2331 metadata[f"{keyBase} PARALLEL MEDIAN {ampName}"] = overscanResults.overscanMedian[1]
2332 metadata[f"{keyBase} PARALLEL STDEV {ampName}"] = overscanResults.overscanSigma[1]
2334 metadata[f"{keyBase} RESIDUAL SERIAL MEAN {ampName}"] = overscanResults.residualMean[0]
2335 metadata[f"{keyBase} RESIDUAL SERIAL MEDIAN {ampName}"] = overscanResults.residualMedian[0]
2336 metadata[f"{keyBase} RESIDUAL SERIAL STDEV {ampName}"] = overscanResults.residualSigma[0]
2338 metadata[f"{keyBase} RESIDUAL PARALLEL MEAN {ampName}"] = overscanResults.residualMean[1]
2339 metadata[f"{keyBase} RESIDUAL PARALLEL MEDIAN {ampName}"] = overscanResults.residualMedian[1]
2340 metadata[f"{keyBase} RESIDUAL PARALLEL STDEV {ampName}"] = overscanResults.residualSigma[1]
2341 else:
2342 self.log.warning("Unexpected type for overscan values; none added to header.")
2344 return overscanResults
2346 def updateVariance(self, ampExposure, amp, ptcDataset):
2347 """Set the variance plane using the gain and read noise
2349 The read noise is calculated from the ``overscanImage`` if the
2350 ``doEmpiricalReadNoise`` option is set in the configuration; otherwise
2351 the value from the amplifier data is used.
2353 Parameters
2354 ----------
2355 ampExposure : `lsst.afw.image.Exposure`
2356 Exposure to process.
2357 amp : `lsst.afw.cameraGeom.Amplifier` or `FakeAmp`
2358 Amplifier detector data.
2359 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`
2360 Effective PTC dataset containing the gains and read noise.
2362 See also
2363 --------
2364 lsst.ip.isr.isrFunctions.updateVariance
2365 """
2366 ampName = amp.getName()
2367 # At this point, the effective PTC should have
2368 # gain and read noise values.
2369 gain = ptcDataset.gain[ampName]
2370 readNoise = ptcDataset.noise[ampName]
2372 # The image will always be in adu and the noise
2373 # will always be in electrons, and we will output
2374 # the variance plane in the image units (adu^2).
2375 isrFunctions.updateVariance(
2376 maskedImage=ampExposure.getMaskedImage(),
2377 gain=gain,
2378 readNoise=readNoise,
2379 )
2381 def maskNegativeVariance(self, exposure):
2382 """Identify and mask pixels with negative variance values.
2384 Parameters
2385 ----------
2386 exposure : `lsst.afw.image.Exposure`
2387 Exposure to process.
2389 See Also
2390 --------
2391 lsst.ip.isr.isrFunctions.updateVariance
2392 """
2393 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName)
2394 bad = numpy.where(exposure.getVariance().getArray() <= 0.0)
2395 exposure.mask.array[bad] |= maskPlane
2397 def darkCorrection(self, exposure, darkExposure, invert=False):
2398 """Apply dark correction in place.
2400 Parameters
2401 ----------
2402 exposure : `lsst.afw.image.Exposure`
2403 Exposure to process.
2404 darkExposure : `lsst.afw.image.Exposure`
2405 Dark exposure of the same size as ``exposure``.
2406 invert : `Bool`, optional
2407 If True, re-add the dark to an already corrected image.
2409 Raises
2410 ------
2411 RuntimeError
2412 Raised if either ``exposure`` or ``darkExposure`` do not
2413 have their dark time defined.
2415 See Also
2416 --------
2417 lsst.ip.isr.isrFunctions.darkCorrection
2418 """
2419 expScale = exposure.getInfo().getVisitInfo().getDarkTime()
2420 if math.isnan(expScale):
2421 raise RuntimeError("Exposure darktime is NaN.")
2422 if darkExposure.getInfo().getVisitInfo() is not None \
2423 and not math.isnan(darkExposure.getInfo().getVisitInfo().getDarkTime()):
2424 darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
2425 else:
2426 # DM-17444: darkExposure.getInfo.getVisitInfo() is None
2427 # so getDarkTime() does not exist.
2428 self.log.warning("darkExposure.getInfo().getVisitInfo() does not exist. Using darkScale = 1.0.")
2429 darkScale = 1.0
2431 isrFunctions.darkCorrection(
2432 maskedImage=exposure.getMaskedImage(),
2433 darkMaskedImage=darkExposure.getMaskedImage(),
2434 expScale=expScale,
2435 darkScale=darkScale,
2436 invert=invert,
2437 trimToFit=self.config.doTrimToMatchCalib
2438 )
2440 def doLinearize(self, detector):
2441 """Check if linearization is needed for the detector cameraGeom.
2443 Checks config.doLinearize and the linearity type of the first
2444 amplifier.
2446 Parameters
2447 ----------
2448 detector : `lsst.afw.cameraGeom.Detector`
2449 Detector to get linearity type from.
2451 Returns
2452 -------
2453 doLinearize : `Bool`
2454 If True, linearization should be performed.
2455 """
2456 return self.config.doLinearize and \
2457 detector.getAmplifiers()[0].getLinearityType() != NullLinearityType
2459 def flatCorrection(self, exposure, flatExposure, invert=False):
2460 """Apply flat correction in place.
2462 Parameters
2463 ----------
2464 exposure : `lsst.afw.image.Exposure`
2465 Exposure to process.
2466 flatExposure : `lsst.afw.image.Exposure`
2467 Flat exposure of the same size as ``exposure``.
2468 invert : `Bool`, optional
2469 If True, unflatten an already flattened image.
2471 See Also
2472 --------
2473 lsst.ip.isr.isrFunctions.flatCorrection
2474 """
2475 isrFunctions.flatCorrection(
2476 maskedImage=exposure.getMaskedImage(),
2477 flatMaskedImage=flatExposure.getMaskedImage(),
2478 scalingType=self.config.flatScalingType,
2479 userScale=self.config.flatUserScale,
2480 invert=invert,
2481 trimToFit=self.config.doTrimToMatchCalib
2482 )
2484 def saturationDetection(self, exposure, amp):
2485 """Detect and mask saturated pixels in config.saturatedMaskName.
2487 Parameters
2488 ----------
2489 exposure : `lsst.afw.image.Exposure`
2490 Exposure to process. Only the amplifier DataSec is processed.
2491 amp : `lsst.afw.cameraGeom.Amplifier`
2492 Amplifier detector data.
2494 See Also
2495 --------
2496 lsst.ip.isr.isrFunctions.makeThresholdMask
2497 """
2498 if not math.isnan(amp.getSaturation()): 2498 ↛ exitline 2498 didn't return from function 'saturationDetection' because the condition on line 2498 was always true
2499 maskedImage = exposure.getMaskedImage()
2500 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2501 isrFunctions.makeThresholdMask(
2502 maskedImage=dataView,
2503 threshold=amp.getSaturation(),
2504 growFootprints=0,
2505 maskName=self.config.saturatedMaskName,
2506 )
2508 def saturationInterpolation(self, exposure):
2509 """Interpolate over saturated pixels, in place.
2511 This method should be called after `saturationDetection`, to
2512 ensure that the saturated pixels have been identified in the
2513 SAT mask. It should also be called after `assembleCcd`, since
2514 saturated regions may cross amplifier boundaries.
2516 Parameters
2517 ----------
2518 exposure : `lsst.afw.image.Exposure`
2519 Exposure to process.
2521 See Also
2522 --------
2523 lsst.ip.isr.isrTask.saturationDetection
2524 lsst.ip.isr.isrFunctions.interpolateFromMask
2525 """
2526 isrFunctions.interpolateFromMask(
2527 maskedImage=exposure.getMaskedImage(),
2528 fwhm=self.config.fwhm,
2529 growSaturatedFootprints=self.config.growSaturationFootprintSize,
2530 maskNameList=list(self.config.saturatedMaskName),
2531 useLegacyInterp=self.config.useLegacyInterp,
2532 )
2534 def suspectDetection(self, exposure, amp):
2535 """Detect and mask suspect pixels in config.suspectMaskName.
2537 Parameters
2538 ----------
2539 exposure : `lsst.afw.image.Exposure`
2540 Exposure to process. Only the amplifier DataSec is processed.
2541 amp : `lsst.afw.cameraGeom.Amplifier`
2542 Amplifier detector data.
2544 See Also
2545 --------
2546 lsst.ip.isr.isrFunctions.makeThresholdMask
2548 Notes
2549 -----
2550 Suspect pixels are pixels whose value is greater than
2551 amp.getSuspectLevel(). This is intended to indicate pixels that may be
2552 affected by unknown systematics; for example if non-linearity
2553 corrections above a certain level are unstable then that would be a
2554 useful value for suspectLevel. A value of `nan` indicates that no such
2555 level exists and no pixels are to be masked as suspicious.
2556 """
2557 suspectLevel = amp.getSuspectLevel()
2558 if math.isnan(suspectLevel):
2559 return
2561 maskedImage = exposure.getMaskedImage()
2562 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2563 isrFunctions.makeThresholdMask(
2564 maskedImage=dataView,
2565 threshold=suspectLevel,
2566 growFootprints=0,
2567 maskName=self.config.suspectMaskName,
2568 )
2570 def maskDefect(self, exposure, defectBaseList):
2571 """Mask defects using mask plane "BAD", in place.
2573 Parameters
2574 ----------
2575 exposure : `lsst.afw.image.Exposure`
2576 Exposure to process.
2577 defectBaseList : defect-type
2578 List of defects to mask. Can be of type `lsst.ip.isr.Defects`
2579 or `list` of `lsst.afw.image.DefectBase`.
2581 Notes
2582 -----
2583 Call this after CCD assembly, since defects may cross amplifier
2584 boundaries.
2585 """
2586 maskedImage = exposure.getMaskedImage()
2587 if not isinstance(defectBaseList, Defects): 2587 ↛ 2589line 2587 didn't jump to line 2589 because the condition on line 2587 was never true
2588 # Promotes DefectBase to Defect
2589 defectList = Defects(defectBaseList)
2590 else:
2591 defectList = defectBaseList
2592 defectList.maskPixels(maskedImage, maskName="BAD")
2594 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'):
2595 """Mask edge pixels with applicable mask plane.
2597 Parameters
2598 ----------
2599 exposure : `lsst.afw.image.Exposure`
2600 Exposure to process.
2601 numEdgePixels : `int`, optional
2602 Number of edge pixels to mask.
2603 maskPlane : `str`, optional
2604 Mask plane name to use.
2605 level : `str`, optional
2606 Level at which to mask edges.
2607 """
2608 maskedImage = exposure.getMaskedImage()
2609 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane)
2611 if numEdgePixels > 0: 2611 ↛ exitline 2611 didn't return from function 'maskEdges' because the condition on line 2611 was always true
2612 if level == 'DETECTOR':
2613 boxes = [maskedImage.getBBox()]
2614 elif level == 'AMP': 2614 ↛ 2617line 2614 didn't jump to line 2617 because the condition on line 2614 was always true
2615 boxes = [amp.getBBox() for amp in exposure.getDetector()]
2617 for box in boxes:
2618 # This makes a bbox numEdgeSuspect pixels smaller than the
2619 # image on each side
2620 subImage = maskedImage[box]
2621 box.grow(-numEdgePixels)
2622 # Mask pixels outside box
2623 SourceDetectionTask.setEdgeBits(
2624 subImage,
2625 box,
2626 maskBitMask)
2628 def maskAndInterpolateDefects(self, exposure, defectBaseList):
2629 """Mask and interpolate defects using mask plane "BAD", in place.
2631 Parameters
2632 ----------
2633 exposure : `lsst.afw.image.Exposure`
2634 Exposure to process.
2635 defectBaseList : defects-like
2636 List of defects to mask and interpolate. Can be
2637 `lsst.ip.isr.Defects` or `list` of `lsst.afw.image.DefectBase`.
2639 See Also
2640 --------
2641 lsst.ip.isr.isrTask.maskDefect
2642 """
2643 self.maskDefect(exposure, defectBaseList)
2644 self.maskEdges(exposure, numEdgePixels=self.config.numEdgeSuspect,
2645 maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
2646 isrFunctions.interpolateFromMask(
2647 maskedImage=exposure.getMaskedImage(),
2648 fwhm=self.config.fwhm,
2649 growSaturatedFootprints=0,
2650 maskNameList=["BAD"],
2651 useLegacyInterp=self.config.useLegacyInterp,
2652 )
2654 def maskNan(self, exposure):
2655 """Mask NaNs using mask plane "UNMASKEDNAN", in place.
2657 Parameters
2658 ----------
2659 exposure : `lsst.afw.image.Exposure`
2660 Exposure to process.
2662 Notes
2663 -----
2664 We mask over all non-finite values (NaN, inf), including those
2665 that are masked with other bits (because those may or may not be
2666 interpolated over later, and we want to remove all NaN/infs).
2667 Despite this behaviour, the "UNMASKEDNAN" mask plane is used to
2668 preserve the historical name.
2669 """
2670 maskedImage = exposure.getMaskedImage()
2672 # Find and mask NaNs
2673 maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
2674 maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
2675 numNans = maskNans(maskedImage, maskVal)
2676 self.metadata["NUMNANS"] = numNans
2677 if numNans > 0: 2677 ↛ 2678line 2677 didn't jump to line 2678 because the condition on line 2677 was never true
2678 self.log.warning("There were %d unmasked NaNs.", numNans)
2680 def maskAndInterpolateNan(self, exposure):
2681 """"Mask and interpolate NaN/infs using mask plane "UNMASKEDNAN",
2682 in place.
2684 Parameters
2685 ----------
2686 exposure : `lsst.afw.image.Exposure`
2687 Exposure to process.
2689 See Also
2690 --------
2691 lsst.ip.isr.isrTask.maskNan
2692 """
2693 self.maskNan(exposure)
2694 isrFunctions.interpolateFromMask(
2695 maskedImage=exposure.getMaskedImage(),
2696 fwhm=self.config.fwhm,
2697 growSaturatedFootprints=0,
2698 maskNameList=["UNMASKEDNAN"],
2699 useLegacyInterp=self.config.useLegacyInterp,
2700 )
2702 def measureBackground(self, exposure, IsrQaConfig=None):
2703 """Measure the image background in subgrids, for quality control.
2705 Parameters
2706 ----------
2707 exposure : `lsst.afw.image.Exposure`
2708 Exposure to process.
2709 IsrQaConfig : `lsst.ip.isr.isrQa.IsrQaConfig`
2710 Configuration object containing parameters on which background
2711 statistics and subgrids to use.
2712 """
2713 if IsrQaConfig is not None: 2713 ↛ exitline 2713 didn't return from function 'measureBackground' because the condition on line 2713 was always true
2714 statsControl = afwMath.StatisticsControl(IsrQaConfig.flatness.clipSigma,
2715 IsrQaConfig.flatness.nIter)
2716 maskVal = exposure.getMaskedImage().getMask().getPlaneBitMask(["BAD", "SAT", "DETECTED"])
2717 statsControl.setAndMask(maskVal)
2718 maskedImage = exposure.getMaskedImage()
2719 stats = afwMath.makeStatistics(maskedImage, afwMath.MEDIAN | afwMath.STDEVCLIP, statsControl)
2720 skyLevel = stats.getValue(afwMath.MEDIAN)
2721 skySigma = stats.getValue(afwMath.STDEVCLIP)
2722 self.log.info("Flattened sky level: %f +/- %f.", skyLevel, skySigma)
2723 metadata = exposure.getMetadata()
2724 metadata["SKYLEVEL"] = skyLevel
2725 metadata["SKYSIGMA"] = skySigma
2727 # calcluating flatlevel over the subgrids
2728 stat = afwMath.MEANCLIP if IsrQaConfig.flatness.doClip else afwMath.MEAN
2729 meshXHalf = int(IsrQaConfig.flatness.meshX/2.)
2730 meshYHalf = int(IsrQaConfig.flatness.meshY/2.)
2731 nX = int((exposure.getWidth() + meshXHalf) / IsrQaConfig.flatness.meshX)
2732 nY = int((exposure.getHeight() + meshYHalf) / IsrQaConfig.flatness.meshY)
2733 skyLevels = numpy.zeros((nX, nY))
2735 for j in range(nY):
2736 yc = meshYHalf + j * IsrQaConfig.flatness.meshY
2737 for i in range(nX):
2738 xc = meshXHalf + i * IsrQaConfig.flatness.meshX
2740 xLLC = xc - meshXHalf
2741 yLLC = yc - meshYHalf
2742 xURC = xc + meshXHalf - 1
2743 yURC = yc + meshYHalf - 1
2745 bbox = lsst.geom.Box2I(lsst.geom.Point2I(xLLC, yLLC), lsst.geom.Point2I(xURC, yURC))
2746 miMesh = maskedImage.Factory(exposure.getMaskedImage(), bbox, afwImage.LOCAL)
2748 skyLevels[i, j] = afwMath.makeStatistics(miMesh, stat, statsControl).getValue()
2750 good = numpy.where(numpy.isfinite(skyLevels))
2751 if len(good[0]) == 0:
2752 # There are no good pixels.
2753 self.log.warning("No good pixels to measure sky levels.")
2754 skyMedian = numpy.nan
2755 flatness = numpy.nan
2756 flatness_rms = numpy.nan
2757 flatness_pp = numpy.nan
2758 else:
2759 skyMedian = numpy.median(skyLevels[good])
2760 flatness = (skyLevels[good] - skyMedian) / skyMedian
2761 flatness_rms = numpy.std(flatness)
2762 flatness_pp = flatness.max() - flatness.min() if len(flatness) > 0 else numpy.nan
2764 self.log.info("Measuring sky levels in %dx%d grids: %f.", nX, nY, skyMedian)
2765 self.log.info("Sky flatness in %dx%d grids - pp: %f rms: %f.",
2766 nX, nY, flatness_pp, flatness_rms)
2768 metadata["FLATNESS_PP"] = float(flatness_pp)
2769 metadata["FLATNESS_RMS"] = float(flatness_rms)
2770 metadata["FLATNESS_NGRIDS"] = '%dx%d' % (nX, nY)
2771 metadata["FLATNESS_MESHX"] = IsrQaConfig.flatness.meshX
2772 metadata["FLATNESS_MESHY"] = IsrQaConfig.flatness.meshY
2774 def roughZeroPoint(self, exposure):
2775 """Set an approximate magnitude zero point for the exposure.
2777 Parameters
2778 ----------
2779 exposure : `lsst.afw.image.Exposure`
2780 Exposure to process.
2781 """
2782 filterLabel = exposure.getFilter()
2783 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
2785 if physicalFilter in self.config.fluxMag0T1: 2785 ↛ 2788line 2785 didn't jump to line 2788 because the condition on line 2785 was always true
2786 fluxMag0 = self.config.fluxMag0T1[physicalFilter]
2787 else:
2788 self.log.warning("No rough magnitude zero point defined for filter %s.", physicalFilter)
2789 fluxMag0 = self.config.defaultFluxMag0T1
2791 expTime = exposure.getInfo().getVisitInfo().getExposureTime()
2792 if expTime == 0.0:
2793 self.log.debug("Received exposure with 0.0 expTime; skipping rough zero point.")
2794 return
2795 elif not expTime > 0: # handle NaN as well as <= 0 2795 ↛ 2796line 2795 didn't jump to line 2796 because the condition on line 2795 was never true
2796 self.log.warning("Non-positive exposure time; skipping rough zero point.")
2797 return
2799 self.log.info("Setting rough magnitude zero point for filter %s: %f",
2800 physicalFilter, 2.5*math.log10(fluxMag0*expTime))
2801 exposure.setPhotoCalib(afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0*expTime, 0.0))
2803 @contextmanager
2804 def flatContext(self, exp, flat, dark=None):
2805 """Context manager that applies and removes flats and darks,
2806 if the task is configured to apply them.
2808 Parameters
2809 ----------
2810 exp : `lsst.afw.image.Exposure`
2811 Exposure to process.
2812 flat : `lsst.afw.image.Exposure`
2813 Flat exposure the same size as ``exp``.
2814 dark : `lsst.afw.image.Exposure`, optional
2815 Dark exposure the same size as ``exp``.
2817 Yields
2818 ------
2819 exp : `lsst.afw.image.Exposure`
2820 The flat and dark corrected exposure.
2821 """
2822 if self.config.doDark and dark is not None: 2822 ↛ 2824line 2822 didn't jump to line 2824 because the condition on line 2822 was always true
2823 self.darkCorrection(exp, dark)
2824 if self.config.doFlat: 2824 ↛ 2826line 2824 didn't jump to line 2826 because the condition on line 2824 was always true
2825 self.flatCorrection(exp, flat)
2826 try:
2827 yield exp
2828 finally:
2829 if self.config.doFlat: 2829 ↛ 2831line 2829 didn't jump to line 2831 because the condition on line 2829 was always true
2830 self.flatCorrection(exp, flat, invert=True)
2831 if self.config.doDark and dark is not None: 2831 ↛ exitline 2831 didn't return from function 'flatContext' because the condition on line 2831 was always true
2832 self.darkCorrection(exp, dark, invert=True)
2834 @deprecated(
2835 reason=(
2836 "makeBinnedImages is no longer used. "
2837 "Please subtask lsst.ip.isr.BinImageDataTask instead."
2838 ),
2839 version="v28", category=FutureWarning
2840 )
2841 def makeBinnedImages(self, exposure):
2842 """Make visualizeVisit style binned exposures.
2844 Parameters
2845 ----------
2846 exposure : `lsst.afw.image.Exposure`
2847 Exposure to bin.
2849 Returns
2850 -------
2851 bin1 : `lsst.afw.image.Exposure`
2852 Binned exposure using binFactor1.
2853 bin2 : `lsst.afw.image.Exposure`
2854 Binned exposure using binFactor2.
2855 """
2856 mi = exposure.getMaskedImage()
2858 bin1 = afwMath.binImage(mi, self.config.binFactor1)
2859 bin2 = afwMath.binImage(mi, self.config.binFactor2)
2861 bin1 = afwImage.makeExposure(bin1)
2862 bin2 = afwImage.makeExposure(bin2)
2864 bin1.setInfo(exposure.getInfo())
2865 bin2.setInfo(exposure.getInfo())
2867 return bin1, bin2
2869 def debugView(self, exposure, stepname):
2870 """Utility function to examine ISR exposure at different stages.
2872 Parameters
2873 ----------
2874 exposure : `lsst.afw.image.Exposure`
2875 Exposure to view.
2876 stepname : `str`
2877 State of processing to view.
2878 """
2879 frame = getDebugFrame(self._display, stepname)
2880 if frame: 2880 ↛ 2881line 2880 didn't jump to line 2881 because the condition on line 2880 was never true
2881 display = getDisplay(frame)
2882 display.scale('asinh', 'zscale')
2883 display.mtv(exposure)
2884 prompt = "Press Enter to continue [c]... "
2885 while True:
2886 ans = input(prompt).lower()
2887 if ans in ("", "c",):
2888 break
2891class FakeAmp(object):
2892 """A Detector-like object that supports returning gain and saturation level
2894 This is used when the input exposure does not have a detector.
2896 Parameters
2897 ----------
2898 exposure : `lsst.afw.image.Exposure`
2899 Exposure to generate a fake amplifier for.
2900 config : `lsst.ip.isr.isrTaskConfig`
2901 Configuration to apply to the fake amplifier.
2902 """
2904 def __init__(self, exposure, config):
2905 self._bbox = exposure.getBBox(afwImage.LOCAL)
2906 self._RawHorizontalOverscanBBox = lsst.geom.Box2I()
2907 self._gain = config.gain
2908 self._readNoise = config.readNoise
2909 self._saturation = config.saturation
2911 def getBBox(self):
2912 return self._bbox
2914 def getRawBBox(self):
2915 return self._bbox
2917 def getRawHorizontalOverscanBBox(self):
2918 return self._RawHorizontalOverscanBBox
2920 def getGain(self):
2921 return self._gain
2923 def getReadNoise(self):
2924 return self._readNoise
2926 def getSaturation(self):
2927 return self._saturation
2929 def getSuspectLevel(self):
2930 return float("NaN")