Coverage for python/lsst/ip/isr/isrTask.py: 16%
855 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-06 02:29 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-06 02:29 -0800
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
27import lsst.geom
28import lsst.afw.image as afwImage
29import lsst.afw.math as afwMath
30import lsst.pex.config as pexConfig
31import lsst.pipe.base as pipeBase
32import lsst.pipe.base.connectionTypes as cT
34from contextlib import contextmanager
35from lsstDebug import getDebugFrame
37from lsst.afw.cameraGeom import NullLinearityType
38from lsst.afw.display import getDisplay
39from lsst.meas.algorithms.detection import SourceDetectionTask
40from lsst.utils.timer import timeMethod
42from . import isrFunctions
43from . import isrQa
44from . import linearize
45from .defects import Defects
47from .assembleCcdTask import AssembleCcdTask
48from .crosstalk import CrosstalkTask, CrosstalkCalib
49from .fringe import FringeTask
50from .isr import maskNans
51from .masking import MaskingTask
52from .overscan import OverscanCorrectionTask
53from .straylight import StrayLightTask
54from .vignette import VignetteTask
55from .ampOffset import AmpOffsetTask
56from .deferredCharge import DeferredChargeTask
57from .isrStatistics import IsrStatisticsTask
58from lsst.daf.butler import DimensionGraph
61def crosstalkSourceLookup(datasetType, registry, quantumDataId, collections):
62 """Lookup function to identify crosstalkSource entries.
64 This should return an empty list under most circumstances. Only
65 when inter-chip crosstalk has been identified should this be
66 populated.
68 Parameters
69 ----------
70 datasetType : `str`
71 Dataset to lookup.
72 registry : `lsst.daf.butler.Registry`
73 Butler registry to query.
74 quantumDataId : `lsst.daf.butler.ExpandedDataCoordinate`
75 Data id to transform to identify crosstalkSources. The
76 ``detector`` entry will be stripped.
77 collections : `lsst.daf.butler.CollectionSearch`
78 Collections to search through.
80 Returns
81 -------
82 results : `list` [`lsst.daf.butler.DatasetRef`]
83 List of datasets that match the query that will be used as
84 crosstalkSources.
85 """
86 newDataId = quantumDataId.subset(DimensionGraph(registry.dimensions, names=["instrument", "exposure"]))
87 results = set(registry.queryDatasets(datasetType, collections=collections, dataId=newDataId,
88 findFirst=True))
89 # In some contexts, calling `.expanded()` to expand all data IDs in the
90 # query results can be a lot faster because it vectorizes lookups. But in
91 # this case, expandDataId shouldn't need to hit the database at all in the
92 # steady state, because only the detector record is unknown and those are
93 # cached in the registry.
94 return [ref.expanded(registry.expandDataId(ref.dataId, records=newDataId.records)) for ref in results]
97class IsrTaskConnections(pipeBase.PipelineTaskConnections,
98 dimensions={"instrument", "exposure", "detector"},
99 defaultTemplates={}):
100 ccdExposure = cT.Input(
101 name="raw",
102 doc="Input exposure to process.",
103 storageClass="Exposure",
104 dimensions=["instrument", "exposure", "detector"],
105 )
106 camera = cT.PrerequisiteInput(
107 name="camera",
108 storageClass="Camera",
109 doc="Input camera to construct complete exposures.",
110 dimensions=["instrument"],
111 isCalibration=True,
112 )
114 crosstalk = cT.PrerequisiteInput(
115 name="crosstalk",
116 doc="Input crosstalk object",
117 storageClass="CrosstalkCalib",
118 dimensions=["instrument", "detector"],
119 isCalibration=True,
120 minimum=0, # can fall back to cameraGeom
121 )
122 crosstalkSources = cT.PrerequisiteInput(
123 name="isrOverscanCorrected",
124 doc="Overscan corrected input images.",
125 storageClass="Exposure",
126 dimensions=["instrument", "exposure", "detector"],
127 deferLoad=True,
128 multiple=True,
129 lookupFunction=crosstalkSourceLookup,
130 minimum=0, # not needed for all instruments, no config to control this
131 )
132 bias = cT.PrerequisiteInput(
133 name="bias",
134 doc="Input bias calibration.",
135 storageClass="ExposureF",
136 dimensions=["instrument", "detector"],
137 isCalibration=True,
138 )
139 dark = cT.PrerequisiteInput(
140 name='dark',
141 doc="Input dark calibration.",
142 storageClass="ExposureF",
143 dimensions=["instrument", "detector"],
144 isCalibration=True,
145 )
146 flat = cT.PrerequisiteInput(
147 name="flat",
148 doc="Input flat calibration.",
149 storageClass="ExposureF",
150 dimensions=["instrument", "physical_filter", "detector"],
151 isCalibration=True,
152 )
153 ptc = cT.PrerequisiteInput(
154 name="ptc",
155 doc="Input Photon Transfer Curve dataset",
156 storageClass="PhotonTransferCurveDataset",
157 dimensions=["instrument", "detector"],
158 isCalibration=True,
159 )
160 fringes = cT.PrerequisiteInput(
161 name="fringe",
162 doc="Input fringe calibration.",
163 storageClass="ExposureF",
164 dimensions=["instrument", "physical_filter", "detector"],
165 isCalibration=True,
166 minimum=0, # only needed for some bands, even when enabled
167 )
168 strayLightData = cT.PrerequisiteInput(
169 name='yBackground',
170 doc="Input stray light calibration.",
171 storageClass="StrayLightData",
172 dimensions=["instrument", "physical_filter", "detector"],
173 deferLoad=True,
174 isCalibration=True,
175 minimum=0, # only needed for some bands, even when enabled
176 )
177 bfKernel = cT.PrerequisiteInput(
178 name='bfKernel',
179 doc="Input brighter-fatter kernel.",
180 storageClass="NumpyArray",
181 dimensions=["instrument"],
182 isCalibration=True,
183 minimum=0, # can use either bfKernel or newBFKernel
184 )
185 newBFKernel = cT.PrerequisiteInput(
186 name='brighterFatterKernel',
187 doc="Newer complete kernel + gain solutions.",
188 storageClass="BrighterFatterKernel",
189 dimensions=["instrument", "detector"],
190 isCalibration=True,
191 minimum=0, # can use either bfKernel or newBFKernel
192 )
193 defects = cT.PrerequisiteInput(
194 name='defects',
195 doc="Input defect tables.",
196 storageClass="Defects",
197 dimensions=["instrument", "detector"],
198 isCalibration=True,
199 )
200 linearizer = cT.PrerequisiteInput(
201 name='linearizer',
202 storageClass="Linearizer",
203 doc="Linearity correction calibration.",
204 dimensions=["instrument", "detector"],
205 isCalibration=True,
206 minimum=0, # can fall back to cameraGeom
207 )
208 opticsTransmission = cT.PrerequisiteInput(
209 name="transmission_optics",
210 storageClass="TransmissionCurve",
211 doc="Transmission curve due to the optics.",
212 dimensions=["instrument"],
213 isCalibration=True,
214 )
215 filterTransmission = cT.PrerequisiteInput(
216 name="transmission_filter",
217 storageClass="TransmissionCurve",
218 doc="Transmission curve due to the filter.",
219 dimensions=["instrument", "physical_filter"],
220 isCalibration=True,
221 )
222 sensorTransmission = cT.PrerequisiteInput(
223 name="transmission_sensor",
224 storageClass="TransmissionCurve",
225 doc="Transmission curve due to the sensor.",
226 dimensions=["instrument", "detector"],
227 isCalibration=True,
228 )
229 atmosphereTransmission = cT.PrerequisiteInput(
230 name="transmission_atmosphere",
231 storageClass="TransmissionCurve",
232 doc="Transmission curve due to the atmosphere.",
233 dimensions=["instrument"],
234 isCalibration=True,
235 )
236 illumMaskedImage = cT.PrerequisiteInput(
237 name="illum",
238 doc="Input illumination correction.",
239 storageClass="MaskedImageF",
240 dimensions=["instrument", "physical_filter", "detector"],
241 isCalibration=True,
242 )
243 deferredChargeCalib = cT.PrerequisiteInput(
244 name="cpCtiCalib",
245 doc="Deferred charge/CTI correction dataset.",
246 storageClass="IsrCalib",
247 dimensions=["instrument", "detector"],
248 isCalibration=True,
249 )
251 outputExposure = cT.Output(
252 name='postISRCCD',
253 doc="Output ISR processed exposure.",
254 storageClass="Exposure",
255 dimensions=["instrument", "exposure", "detector"],
256 )
257 preInterpExposure = cT.Output(
258 name='preInterpISRCCD',
259 doc="Output ISR processed exposure, with pixels left uninterpolated.",
260 storageClass="ExposureF",
261 dimensions=["instrument", "exposure", "detector"],
262 )
263 outputOssThumbnail = cT.Output(
264 name="OssThumb",
265 doc="Output Overscan-subtracted thumbnail image.",
266 storageClass="Thumbnail",
267 dimensions=["instrument", "exposure", "detector"],
268 )
269 outputFlattenedThumbnail = cT.Output(
270 name="FlattenedThumb",
271 doc="Output flat-corrected thumbnail image.",
272 storageClass="Thumbnail",
273 dimensions=["instrument", "exposure", "detector"],
274 )
275 outputStatistics = cT.Output(
276 name="isrStatistics",
277 doc="Output of additional statistics table.",
278 storageClass="StructuredDataDict",
279 dimensions=["instrument", "exposure", "detector"],
280 )
282 def __init__(self, *, config=None):
283 super().__init__(config=config)
285 if config.doBias is not True:
286 self.prerequisiteInputs.remove("bias")
287 if config.doLinearize is not True:
288 self.prerequisiteInputs.remove("linearizer")
289 if config.doCrosstalk is not True:
290 self.prerequisiteInputs.remove("crosstalkSources")
291 self.prerequisiteInputs.remove("crosstalk")
292 if config.doBrighterFatter is not True:
293 self.prerequisiteInputs.remove("bfKernel")
294 self.prerequisiteInputs.remove("newBFKernel")
295 if config.doDefect is not True:
296 self.prerequisiteInputs.remove("defects")
297 if config.doDark is not True:
298 self.prerequisiteInputs.remove("dark")
299 if config.doFlat is not True:
300 self.prerequisiteInputs.remove("flat")
301 if config.doFringe is not True:
302 self.prerequisiteInputs.remove("fringes")
303 if config.doStrayLight is not True:
304 self.prerequisiteInputs.remove("strayLightData")
305 if config.usePtcGains is not True and config.usePtcReadNoise is not True:
306 self.prerequisiteInputs.remove("ptc")
307 if config.doAttachTransmissionCurve is not True:
308 self.prerequisiteInputs.remove("opticsTransmission")
309 self.prerequisiteInputs.remove("filterTransmission")
310 self.prerequisiteInputs.remove("sensorTransmission")
311 self.prerequisiteInputs.remove("atmosphereTransmission")
312 else:
313 if config.doUseOpticsTransmission is not True:
314 self.prerequisiteInputs.remove("opticsTransmission")
315 if config.doUseFilterTransmission is not True:
316 self.prerequisiteInputs.remove("filterTransmission")
317 if config.doUseSensorTransmission is not True:
318 self.prerequisiteInputs.remove("sensorTransmission")
319 if config.doUseAtmosphereTransmission is not True:
320 self.prerequisiteInputs.remove("atmosphereTransmission")
321 if config.doIlluminationCorrection is not True:
322 self.prerequisiteInputs.remove("illumMaskedImage")
323 if config.doDeferredCharge is not True:
324 self.prerequisiteInputs.remove("deferredChargeCalib")
326 if config.doWrite is not True:
327 self.outputs.remove("outputExposure")
328 self.outputs.remove("preInterpExposure")
329 self.outputs.remove("outputFlattenedThumbnail")
330 self.outputs.remove("outputOssThumbnail")
331 self.outputs.remove("outputStatistics")
333 if config.doSaveInterpPixels is not True:
334 self.outputs.remove("preInterpExposure")
335 if config.qa.doThumbnailOss is not True:
336 self.outputs.remove("outputOssThumbnail")
337 if config.qa.doThumbnailFlattened is not True:
338 self.outputs.remove("outputFlattenedThumbnail")
339 if config.doCalculateStatistics is not True:
340 self.outputs.remove("outputStatistics")
343class IsrTaskConfig(pipeBase.PipelineTaskConfig,
344 pipelineConnections=IsrTaskConnections):
345 """Configuration parameters for IsrTask.
347 Items are grouped in the order in which they are executed by the task.
348 """
349 datasetType = pexConfig.Field(
350 dtype=str,
351 doc="Dataset type for input data; users will typically leave this alone, "
352 "but camera-specific ISR tasks will override it",
353 default="raw",
354 )
356 fallbackFilterName = pexConfig.Field(
357 dtype=str,
358 doc="Fallback default filter name for calibrations.",
359 optional=True
360 )
361 useFallbackDate = pexConfig.Field(
362 dtype=bool,
363 doc="Pass observation date when using fallback filter.",
364 default=False,
365 )
366 expectWcs = pexConfig.Field(
367 dtype=bool,
368 default=True,
369 doc="Expect input science images to have a WCS (set False for e.g. spectrographs)."
370 )
371 fwhm = pexConfig.Field(
372 dtype=float,
373 doc="FWHM of PSF in arcseconds.",
374 default=1.0,
375 )
376 qa = pexConfig.ConfigField(
377 dtype=isrQa.IsrQaConfig,
378 doc="QA related configuration options.",
379 )
380 doHeaderProvenance = pexConfig.Field(
381 dtype=bool,
382 default=True,
383 doc="Write calibration identifiers into output exposure header?",
384 )
386 # Image conversion configuration
387 doConvertIntToFloat = pexConfig.Field(
388 dtype=bool,
389 doc="Convert integer raw images to floating point values?",
390 default=True,
391 )
393 # Saturated pixel handling.
394 doSaturation = pexConfig.Field(
395 dtype=bool,
396 doc="Mask saturated pixels? NB: this is totally independent of the"
397 " interpolation option - this is ONLY setting the bits in the mask."
398 " To have them interpolated make sure doSaturationInterpolation=True",
399 default=True,
400 )
401 saturatedMaskName = pexConfig.Field(
402 dtype=str,
403 doc="Name of mask plane to use in saturation detection and interpolation",
404 default="SAT",
405 )
406 saturation = pexConfig.Field(
407 dtype=float,
408 doc="The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
409 default=float("NaN"),
410 )
411 growSaturationFootprintSize = pexConfig.Field(
412 dtype=int,
413 doc="Number of pixels by which to grow the saturation footprints",
414 default=1,
415 )
417 # Suspect pixel handling.
418 doSuspect = pexConfig.Field(
419 dtype=bool,
420 doc="Mask suspect pixels?",
421 default=False,
422 )
423 suspectMaskName = pexConfig.Field(
424 dtype=str,
425 doc="Name of mask plane to use for suspect pixels",
426 default="SUSPECT",
427 )
428 numEdgeSuspect = pexConfig.Field(
429 dtype=int,
430 doc="Number of edge pixels to be flagged as untrustworthy.",
431 default=0,
432 )
433 edgeMaskLevel = pexConfig.ChoiceField(
434 dtype=str,
435 doc="Mask edge pixels in which coordinate frame: DETECTOR or AMP?",
436 default="DETECTOR",
437 allowed={
438 'DETECTOR': 'Mask only the edges of the full detector.',
439 'AMP': 'Mask edges of each amplifier.',
440 },
441 )
443 # Initial masking options.
444 doSetBadRegions = pexConfig.Field(
445 dtype=bool,
446 doc="Should we set the level of all BAD patches of the chip to the chip's average value?",
447 default=True,
448 )
449 badStatistic = pexConfig.ChoiceField(
450 dtype=str,
451 doc="How to estimate the average value for BAD regions.",
452 default='MEANCLIP',
453 allowed={
454 "MEANCLIP": "Correct using the (clipped) mean of good data",
455 "MEDIAN": "Correct using the median of the good data",
456 },
457 )
459 # Overscan subtraction configuration.
460 doOverscan = pexConfig.Field(
461 dtype=bool,
462 doc="Do overscan subtraction?",
463 default=True,
464 )
465 overscan = pexConfig.ConfigurableField(
466 target=OverscanCorrectionTask,
467 doc="Overscan subtraction task for image segments.",
468 )
470 # Amplifier to CCD assembly configuration
471 doAssembleCcd = pexConfig.Field(
472 dtype=bool,
473 default=True,
474 doc="Assemble amp-level exposures into a ccd-level exposure?"
475 )
476 assembleCcd = pexConfig.ConfigurableField(
477 target=AssembleCcdTask,
478 doc="CCD assembly task",
479 )
481 # General calibration configuration.
482 doAssembleIsrExposures = pexConfig.Field(
483 dtype=bool,
484 default=False,
485 doc="Assemble amp-level calibration exposures into ccd-level exposure?"
486 )
487 doTrimToMatchCalib = pexConfig.Field(
488 dtype=bool,
489 default=False,
490 doc="Trim raw data to match calibration bounding boxes?"
491 )
493 # Bias subtraction.
494 doBias = pexConfig.Field(
495 dtype=bool,
496 doc="Apply bias frame correction?",
497 default=True,
498 )
499 biasDataProductName = pexConfig.Field(
500 dtype=str,
501 doc="Name of the bias data product",
502 default="bias",
503 )
504 doBiasBeforeOverscan = pexConfig.Field(
505 dtype=bool,
506 doc="Reverse order of overscan and bias correction.",
507 default=False
508 )
510 # Deferred charge correction.
511 doDeferredCharge = pexConfig.Field(
512 dtype=bool,
513 doc="Apply deferred charge correction?",
514 default=False,
515 )
516 deferredChargeCorrection = pexConfig.ConfigurableField(
517 target=DeferredChargeTask,
518 doc="Deferred charge correction task.",
519 )
521 # Variance construction
522 doVariance = pexConfig.Field(
523 dtype=bool,
524 doc="Calculate variance?",
525 default=True
526 )
527 gain = pexConfig.Field(
528 dtype=float,
529 doc="The gain to use if no Detector is present in the Exposure (ignored if NaN)",
530 default=float("NaN"),
531 )
532 readNoise = pexConfig.Field(
533 dtype=float,
534 doc="The read noise to use if no Detector is present in the Exposure",
535 default=0.0,
536 )
537 doEmpiricalReadNoise = pexConfig.Field(
538 dtype=bool,
539 default=False,
540 doc="Calculate empirical read noise instead of value from AmpInfo data?"
541 )
542 usePtcReadNoise = pexConfig.Field(
543 dtype=bool,
544 default=False,
545 doc="Use readnoise values from the Photon Transfer Curve?"
546 )
547 maskNegativeVariance = pexConfig.Field(
548 dtype=bool,
549 default=True,
550 doc="Mask pixels that claim a negative variance? This likely indicates a failure "
551 "in the measurement of the overscan at an edge due to the data falling off faster "
552 "than the overscan model can account for it."
553 )
554 negativeVarianceMaskName = pexConfig.Field(
555 dtype=str,
556 default="BAD",
557 doc="Mask plane to use to mark pixels with negative variance, if `maskNegativeVariance` is True.",
558 )
559 # Linearization.
560 doLinearize = pexConfig.Field(
561 dtype=bool,
562 doc="Correct for nonlinearity of the detector's response?",
563 default=True,
564 )
566 # Crosstalk.
567 doCrosstalk = pexConfig.Field(
568 dtype=bool,
569 doc="Apply intra-CCD crosstalk correction?",
570 default=False,
571 )
572 doCrosstalkBeforeAssemble = pexConfig.Field(
573 dtype=bool,
574 doc="Apply crosstalk correction before CCD assembly, and before trimming?",
575 default=False,
576 )
577 crosstalk = pexConfig.ConfigurableField(
578 target=CrosstalkTask,
579 doc="Intra-CCD crosstalk correction",
580 )
582 # Masking options.
583 doDefect = pexConfig.Field(
584 dtype=bool,
585 doc="Apply correction for CCD defects, e.g. hot pixels?",
586 default=True,
587 )
588 doNanMasking = pexConfig.Field(
589 dtype=bool,
590 doc="Mask non-finite (NAN, inf) pixels?",
591 default=True,
592 )
593 doWidenSaturationTrails = pexConfig.Field(
594 dtype=bool,
595 doc="Widen bleed trails based on their width?",
596 default=True
597 )
599 # Brighter-Fatter correction.
600 doBrighterFatter = pexConfig.Field(
601 dtype=bool,
602 default=False,
603 doc="Apply the brighter-fatter correction?"
604 )
605 brighterFatterLevel = pexConfig.ChoiceField(
606 dtype=str,
607 default="DETECTOR",
608 doc="The level at which to correct for brighter-fatter.",
609 allowed={
610 "AMP": "Every amplifier treated separately.",
611 "DETECTOR": "One kernel per detector",
612 }
613 )
614 brighterFatterMaxIter = pexConfig.Field(
615 dtype=int,
616 default=10,
617 doc="Maximum number of iterations for the brighter-fatter correction"
618 )
619 brighterFatterThreshold = pexConfig.Field(
620 dtype=float,
621 default=1000,
622 doc="Threshold used to stop iterating the brighter-fatter correction. It is the "
623 "absolute value of the difference between the current corrected image and the one "
624 "from the previous iteration summed over all the pixels."
625 )
626 brighterFatterApplyGain = pexConfig.Field(
627 dtype=bool,
628 default=True,
629 doc="Should the gain be applied when applying the brighter-fatter correction?"
630 )
631 brighterFatterMaskListToInterpolate = pexConfig.ListField(
632 dtype=str,
633 doc="List of mask planes that should be interpolated over when applying the brighter-fatter "
634 "correction.",
635 default=["SAT", "BAD", "NO_DATA", "UNMASKEDNAN"],
636 )
637 brighterFatterMaskGrowSize = pexConfig.Field(
638 dtype=int,
639 default=0,
640 doc="Number of pixels to grow the masks listed in config.brighterFatterMaskListToInterpolate "
641 "when brighter-fatter correction is applied."
642 )
644 # Dark subtraction.
645 doDark = pexConfig.Field(
646 dtype=bool,
647 doc="Apply dark frame correction?",
648 default=True,
649 )
650 darkDataProductName = pexConfig.Field(
651 dtype=str,
652 doc="Name of the dark data product",
653 default="dark",
654 )
656 # Camera-specific stray light removal.
657 doStrayLight = pexConfig.Field(
658 dtype=bool,
659 doc="Subtract stray light in the y-band (due to encoder LEDs)?",
660 default=False,
661 )
662 strayLight = pexConfig.ConfigurableField(
663 target=StrayLightTask,
664 doc="y-band stray light correction"
665 )
667 # Flat correction.
668 doFlat = pexConfig.Field(
669 dtype=bool,
670 doc="Apply flat field correction?",
671 default=True,
672 )
673 flatDataProductName = pexConfig.Field(
674 dtype=str,
675 doc="Name of the flat data product",
676 default="flat",
677 )
678 flatScalingType = pexConfig.ChoiceField(
679 dtype=str,
680 doc="The method for scaling the flat on the fly.",
681 default='USER',
682 allowed={
683 "USER": "Scale by flatUserScale",
684 "MEAN": "Scale by the inverse of the mean",
685 "MEDIAN": "Scale by the inverse of the median",
686 },
687 )
688 flatUserScale = pexConfig.Field(
689 dtype=float,
690 doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
691 default=1.0,
692 )
693 doTweakFlat = pexConfig.Field(
694 dtype=bool,
695 doc="Tweak flats to match observed amplifier ratios?",
696 default=False
697 )
699 # Amplifier normalization based on gains instead of using flats
700 # configuration.
701 doApplyGains = pexConfig.Field(
702 dtype=bool,
703 doc="Correct the amplifiers for their gains instead of applying flat correction",
704 default=False,
705 )
706 usePtcGains = pexConfig.Field(
707 dtype=bool,
708 doc="Use the gain values from the Photon Transfer Curve?",
709 default=False,
710 )
711 normalizeGains = pexConfig.Field(
712 dtype=bool,
713 doc="Normalize all the amplifiers in each CCD to have the same median value.",
714 default=False,
715 )
717 # Fringe correction.
718 doFringe = pexConfig.Field(
719 dtype=bool,
720 doc="Apply fringe correction?",
721 default=True,
722 )
723 fringe = pexConfig.ConfigurableField(
724 target=FringeTask,
725 doc="Fringe subtraction task",
726 )
727 fringeAfterFlat = pexConfig.Field(
728 dtype=bool,
729 doc="Do fringe subtraction after flat-fielding?",
730 default=True,
731 )
733 # Amp offset correction.
734 doAmpOffset = pexConfig.Field(
735 doc="Calculate and apply amp offset corrections?",
736 dtype=bool,
737 default=False,
738 )
739 ampOffset = pexConfig.ConfigurableField(
740 doc="Amp offset correction task.",
741 target=AmpOffsetTask,
742 )
744 # Initial CCD-level background statistics options.
745 doMeasureBackground = pexConfig.Field(
746 dtype=bool,
747 doc="Measure the background level on the reduced image?",
748 default=False,
749 )
751 # Camera-specific masking configuration.
752 doCameraSpecificMasking = pexConfig.Field(
753 dtype=bool,
754 doc="Mask camera-specific bad regions?",
755 default=False,
756 )
757 masking = pexConfig.ConfigurableField(
758 target=MaskingTask,
759 doc="Masking task."
760 )
762 # Interpolation options.
763 doInterpolate = pexConfig.Field(
764 dtype=bool,
765 doc="Interpolate masked pixels?",
766 default=True,
767 )
768 doSaturationInterpolation = pexConfig.Field(
769 dtype=bool,
770 doc="Perform interpolation over pixels masked as saturated?"
771 " NB: This is independent of doSaturation; if that is False this plane"
772 " will likely be blank, resulting in a no-op here.",
773 default=True,
774 )
775 doNanInterpolation = pexConfig.Field(
776 dtype=bool,
777 doc="Perform interpolation over pixels masked as NaN?"
778 " NB: This is independent of doNanMasking; if that is False this plane"
779 " will likely be blank, resulting in a no-op here.",
780 default=True,
781 )
782 doNanInterpAfterFlat = pexConfig.Field(
783 dtype=bool,
784 doc=("If True, ensure we interpolate NaNs after flat-fielding, even if we "
785 "also have to interpolate them before flat-fielding."),
786 default=False,
787 )
788 maskListToInterpolate = pexConfig.ListField(
789 dtype=str,
790 doc="List of mask planes that should be interpolated.",
791 default=['SAT', 'BAD'],
792 )
793 doSaveInterpPixels = pexConfig.Field(
794 dtype=bool,
795 doc="Save a copy of the pre-interpolated pixel values?",
796 default=False,
797 )
799 # Default photometric calibration options.
800 fluxMag0T1 = pexConfig.DictField(
801 keytype=str,
802 itemtype=float,
803 doc="The approximate flux of a zero-magnitude object in a one-second exposure, per filter.",
804 default=dict((f, pow(10.0, 0.4*m)) for f, m in (("Unknown", 28.0),
805 ))
806 )
807 defaultFluxMag0T1 = pexConfig.Field(
808 dtype=float,
809 doc="Default value for fluxMag0T1 (for an unrecognized filter).",
810 default=pow(10.0, 0.4*28.0)
811 )
813 # Vignette correction configuration.
814 doVignette = pexConfig.Field(
815 dtype=bool,
816 doc=("Compute and attach the validPolygon defining the unvignetted region to the exposure "
817 "according to vignetting parameters?"),
818 default=False,
819 )
820 doMaskVignettePolygon = pexConfig.Field(
821 dtype=bool,
822 doc=("Add a mask bit for pixels within the vignetted region. Ignored if doVignette "
823 "is False"),
824 default=True,
825 )
826 vignetteValue = pexConfig.Field(
827 dtype=float,
828 doc="Value to replace image array pixels with in the vignetted region? Ignored if None.",
829 optional=True,
830 default=None,
831 )
832 vignette = pexConfig.ConfigurableField(
833 target=VignetteTask,
834 doc="Vignetting task.",
835 )
837 # Transmission curve configuration.
838 doAttachTransmissionCurve = pexConfig.Field(
839 dtype=bool,
840 default=False,
841 doc="Construct and attach a wavelength-dependent throughput curve for this CCD image?"
842 )
843 doUseOpticsTransmission = pexConfig.Field(
844 dtype=bool,
845 default=True,
846 doc="Load and use transmission_optics (if doAttachTransmissionCurve is True)?"
847 )
848 doUseFilterTransmission = pexConfig.Field(
849 dtype=bool,
850 default=True,
851 doc="Load and use transmission_filter (if doAttachTransmissionCurve is True)?"
852 )
853 doUseSensorTransmission = pexConfig.Field(
854 dtype=bool,
855 default=True,
856 doc="Load and use transmission_sensor (if doAttachTransmissionCurve is True)?"
857 )
858 doUseAtmosphereTransmission = pexConfig.Field(
859 dtype=bool,
860 default=True,
861 doc="Load and use transmission_atmosphere (if doAttachTransmissionCurve is True)?"
862 )
864 # Illumination correction.
865 doIlluminationCorrection = pexConfig.Field(
866 dtype=bool,
867 default=False,
868 doc="Perform illumination correction?"
869 )
870 illuminationCorrectionDataProductName = pexConfig.Field(
871 dtype=str,
872 doc="Name of the illumination correction data product.",
873 default="illumcor",
874 )
875 illumScale = pexConfig.Field(
876 dtype=float,
877 doc="Scale factor for the illumination correction.",
878 default=1.0,
879 )
880 illumFilters = pexConfig.ListField(
881 dtype=str,
882 default=[],
883 doc="Only perform illumination correction for these filters."
884 )
886 # Calculate image quality statistics?
887 doStandardStatistics = pexConfig.Field(
888 dtype=bool,
889 doc="Should standard image quality statistics be calculated?",
890 default=True,
891 )
892 # Calculate additional statistics?
893 doCalculateStatistics = pexConfig.Field(
894 dtype=bool,
895 doc="Should additional ISR statistics be calculated?",
896 default=False,
897 )
898 isrStats = pexConfig.ConfigurableField(
899 target=IsrStatisticsTask,
900 doc="Task to calculate additional statistics.",
901 )
903 # Write the outputs to disk. If ISR is run as a subtask, this may not
904 # be needed.
905 doWrite = pexConfig.Field(
906 dtype=bool,
907 doc="Persist postISRCCD?",
908 default=True,
909 )
911 def validate(self):
912 super().validate()
913 if self.doFlat and self.doApplyGains:
914 raise ValueError("You may not specify both doFlat and doApplyGains")
915 if self.doBiasBeforeOverscan and self.doTrimToMatchCalib:
916 raise ValueError("You may not specify both doBiasBeforeOverscan and doTrimToMatchCalib")
917 if self.doSaturationInterpolation and self.saturatedMaskName not in self.maskListToInterpolate:
918 self.maskListToInterpolate.append(self.saturatedMaskName)
919 if not self.doSaturationInterpolation and self.saturatedMaskName in self.maskListToInterpolate:
920 self.maskListToInterpolate.remove(self.saturatedMaskName)
921 if self.doNanInterpolation and "UNMASKEDNAN" not in self.maskListToInterpolate:
922 self.maskListToInterpolate.append("UNMASKEDNAN")
925class IsrTask(pipeBase.PipelineTask):
926 """Apply common instrument signature correction algorithms to a raw frame.
928 The process for correcting imaging data is very similar from
929 camera to camera. This task provides a vanilla implementation of
930 doing these corrections, including the ability to turn certain
931 corrections off if they are not needed. The inputs to the primary
932 method, `run()`, are a raw exposure to be corrected and the
933 calibration data products. The raw input is a single chip sized
934 mosaic of all amps including overscans and other non-science
935 pixels.
937 The __init__ method sets up the subtasks for ISR processing, using
938 the defaults from `lsst.ip.isr`.
940 Parameters
941 ----------
942 args : `list`
943 Positional arguments passed to the Task constructor.
944 None used at this time.
945 kwargs : `dict`, optional
946 Keyword arguments passed on to the Task constructor.
947 None used at this time.
948 """
949 ConfigClass = IsrTaskConfig
950 _DefaultName = "isr"
952 def __init__(self, **kwargs):
953 super().__init__(**kwargs)
954 self.makeSubtask("assembleCcd")
955 self.makeSubtask("crosstalk")
956 self.makeSubtask("strayLight")
957 self.makeSubtask("fringe")
958 self.makeSubtask("masking")
959 self.makeSubtask("overscan")
960 self.makeSubtask("vignette")
961 self.makeSubtask("ampOffset")
962 self.makeSubtask("deferredChargeCorrection")
963 self.makeSubtask("isrStats")
965 def runQuantum(self, butlerQC, inputRefs, outputRefs):
966 inputs = butlerQC.get(inputRefs)
968 try:
969 inputs['detectorNum'] = inputRefs.ccdExposure.dataId['detector']
970 except Exception as e:
971 raise ValueError("Failure to find valid detectorNum value for Dataset %s: %s." %
972 (inputRefs, e))
974 detector = inputs['ccdExposure'].getDetector()
976 if self.config.doCrosstalk is True:
977 # Crosstalk sources need to be defined by the pipeline
978 # yaml if they exist.
979 if 'crosstalk' in inputs and inputs['crosstalk'] is not None:
980 if not isinstance(inputs['crosstalk'], CrosstalkCalib):
981 inputs['crosstalk'] = CrosstalkCalib.fromTable(inputs['crosstalk'])
982 else:
983 coeffVector = (self.config.crosstalk.crosstalkValues
984 if self.config.crosstalk.useConfigCoefficients else None)
985 crosstalkCalib = CrosstalkCalib().fromDetector(detector, coeffVector=coeffVector)
986 inputs['crosstalk'] = crosstalkCalib
987 if inputs['crosstalk'].interChip and len(inputs['crosstalk'].interChip) > 0:
988 if 'crosstalkSources' not in inputs:
989 self.log.warning("No crosstalkSources found for chip with interChip terms!")
991 if self.doLinearize(detector) is True:
992 if 'linearizer' in inputs:
993 if isinstance(inputs['linearizer'], dict):
994 linearizer = linearize.Linearizer(detector=detector, log=self.log)
995 linearizer.fromYaml(inputs['linearizer'])
996 self.log.warning("Dictionary linearizers will be deprecated in DM-28741.")
997 elif isinstance(inputs['linearizer'], numpy.ndarray):
998 linearizer = linearize.Linearizer(table=inputs.get('linearizer', None),
999 detector=detector,
1000 log=self.log)
1001 self.log.warning("Bare lookup table linearizers will be deprecated in DM-28741.")
1002 else:
1003 linearizer = inputs['linearizer']
1004 linearizer.log = self.log
1005 inputs['linearizer'] = linearizer
1006 else:
1007 inputs['linearizer'] = linearize.Linearizer(detector=detector, log=self.log)
1008 self.log.warning("Constructing linearizer from cameraGeom information.")
1010 if self.config.doDefect is True:
1011 if "defects" in inputs and inputs['defects'] is not None:
1012 # defects is loaded as a BaseCatalog with columns
1013 # x0, y0, width, height. Masking expects a list of defects
1014 # defined by their bounding box
1015 if not isinstance(inputs["defects"], Defects):
1016 inputs["defects"] = Defects.fromTable(inputs["defects"])
1018 # Load the correct style of brighter-fatter kernel, and repack
1019 # the information as a numpy array.
1020 if self.config.doBrighterFatter:
1021 brighterFatterKernel = inputs.pop('newBFKernel', None)
1022 if brighterFatterKernel is None:
1023 brighterFatterKernel = inputs.get('bfKernel', None)
1025 if brighterFatterKernel is not None and not isinstance(brighterFatterKernel, numpy.ndarray):
1026 # This is a ISR calib kernel
1027 detName = detector.getName()
1028 level = brighterFatterKernel.level
1030 # This is expected to be a dictionary of amp-wise gains.
1031 inputs['bfGains'] = brighterFatterKernel.gain
1032 if self.config.brighterFatterLevel == 'DETECTOR':
1033 if level == 'DETECTOR':
1034 if detName in brighterFatterKernel.detKernels:
1035 inputs['bfKernel'] = brighterFatterKernel.detKernels[detName]
1036 else:
1037 raise RuntimeError("Failed to extract kernel from new-style BF kernel.")
1038 elif level == 'AMP':
1039 self.log.warning("Making DETECTOR level kernel from AMP based brighter "
1040 "fatter kernels.")
1041 brighterFatterKernel.makeDetectorKernelFromAmpwiseKernels(detName)
1042 inputs['bfKernel'] = brighterFatterKernel.detKernels[detName]
1043 elif self.config.brighterFatterLevel == 'AMP':
1044 raise NotImplementedError("Per-amplifier brighter-fatter correction not implemented")
1046 if self.config.doFringe is True and self.fringe.checkFilter(inputs['ccdExposure']):
1047 expId = inputs['ccdExposure'].info.id
1048 inputs['fringes'] = self.fringe.loadFringes(inputs['fringes'],
1049 expId=expId,
1050 assembler=self.assembleCcd
1051 if self.config.doAssembleIsrExposures else None)
1052 else:
1053 inputs['fringes'] = pipeBase.Struct(fringes=None)
1055 if self.config.doStrayLight is True and self.strayLight.checkFilter(inputs['ccdExposure']):
1056 if 'strayLightData' not in inputs:
1057 inputs['strayLightData'] = None
1059 if self.config.doHeaderProvenance:
1060 # Add calibration provenanace info to header.
1061 exposureMetadata = inputs['ccdExposure'].getMetadata()
1062 for inputName in sorted(inputs.keys()):
1063 reference = getattr(inputRefs, inputName, None)
1064 if reference is not None and hasattr(reference, "run"):
1065 runKey = f"LSST CALIB RUN {inputName.upper()}"
1066 runValue = reference.run
1067 idKey = f"LSST CALIB UUID {inputName.upper()}"
1068 idValue = str(reference.id)
1070 exposureMetadata[runKey] = runValue
1071 exposureMetadata[idKey] = idValue
1073 outputs = self.run(**inputs)
1074 butlerQC.put(outputs, outputRefs)
1076 @timeMethod
1077 def run(self, ccdExposure, *, camera=None, bias=None, linearizer=None,
1078 crosstalk=None, crosstalkSources=None,
1079 dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None,
1080 fringes=pipeBase.Struct(fringes=None), opticsTransmission=None, filterTransmission=None,
1081 sensorTransmission=None, atmosphereTransmission=None,
1082 detectorNum=None, strayLightData=None, illumMaskedImage=None,
1083 deferredChargeCalib=None,
1084 ):
1085 """Perform instrument signature removal on an exposure.
1087 Steps included in the ISR processing, in order performed, are:
1089 - saturation and suspect pixel masking
1090 - overscan subtraction
1091 - CCD assembly of individual amplifiers
1092 - bias subtraction
1093 - variance image construction
1094 - linearization of non-linear response
1095 - crosstalk masking
1096 - brighter-fatter correction
1097 - dark subtraction
1098 - fringe correction
1099 - stray light subtraction
1100 - flat correction
1101 - masking of known defects and camera specific features
1102 - vignette calculation
1103 - appending transmission curve and distortion model
1105 Parameters
1106 ----------
1107 ccdExposure : `lsst.afw.image.Exposure`
1108 The raw exposure that is to be run through ISR. The
1109 exposure is modified by this method.
1110 camera : `lsst.afw.cameraGeom.Camera`, optional
1111 The camera geometry for this exposure. Required if
1112 one or more of ``ccdExposure``, ``bias``, ``dark``, or
1113 ``flat`` does not have an associated detector.
1114 bias : `lsst.afw.image.Exposure`, optional
1115 Bias calibration frame.
1116 linearizer : `lsst.ip.isr.linearize.LinearizeBase`, optional
1117 Functor for linearization.
1118 crosstalk : `lsst.ip.isr.crosstalk.CrosstalkCalib`, optional
1119 Calibration for crosstalk.
1120 crosstalkSources : `list`, optional
1121 List of possible crosstalk sources.
1122 dark : `lsst.afw.image.Exposure`, optional
1123 Dark calibration frame.
1124 flat : `lsst.afw.image.Exposure`, optional
1125 Flat calibration frame.
1126 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
1127 Photon transfer curve dataset, with, e.g., gains
1128 and read noise.
1129 bfKernel : `numpy.ndarray`, optional
1130 Brighter-fatter kernel.
1131 bfGains : `dict` of `float`, optional
1132 Gains used to override the detector's nominal gains for the
1133 brighter-fatter correction. A dict keyed by amplifier name for
1134 the detector in question.
1135 defects : `lsst.ip.isr.Defects`, optional
1136 List of defects.
1137 fringes : `lsst.pipe.base.Struct`, optional
1138 Struct containing the fringe correction data, with
1139 elements:
1141 ``fringes``
1142 fringe calibration frame (`lsst.afw.image.Exposure`)
1143 ``seed``
1144 random seed derived from the ``ccdExposureId`` for random
1145 number generator (`numpy.uint32`)
1146 opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional
1147 A ``TransmissionCurve`` that represents the throughput of the,
1148 optics, to be evaluated in focal-plane coordinates.
1149 filterTransmission : `lsst.afw.image.TransmissionCurve`
1150 A ``TransmissionCurve`` that represents the throughput of the
1151 filter itself, to be evaluated in focal-plane coordinates.
1152 sensorTransmission : `lsst.afw.image.TransmissionCurve`
1153 A ``TransmissionCurve`` that represents the throughput of the
1154 sensor itself, to be evaluated in post-assembly trimmed detector
1155 coordinates.
1156 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1157 A ``TransmissionCurve`` that represents the throughput of the
1158 atmosphere, assumed to be spatially constant.
1159 detectorNum : `int`, optional
1160 The integer number for the detector to process.
1161 strayLightData : `object`, optional
1162 Opaque object containing calibration information for stray-light
1163 correction. If `None`, no correction will be performed.
1164 illumMaskedImage : `lsst.afw.image.MaskedImage`, optional
1165 Illumination correction image.
1167 Returns
1168 -------
1169 result : `lsst.pipe.base.Struct`
1170 Result struct with component:
1172 ``exposure``
1173 The fully ISR corrected exposure.
1174 (`lsst.afw.image.Exposure`)
1175 ``outputExposure``
1176 An alias for ``exposure``. (`lsst.afw.image.Exposure`)
1177 ``ossThumb``
1178 Thumbnail image of the exposure after overscan subtraction.
1179 (`numpy.ndarray`)
1180 ``flattenedThumb``
1181 Thumbnail image of the exposure after flat-field correction.
1182 (`numpy.ndarray`)
1183 ``outputStatistics``
1184 Values of the additional statistics calculated.
1186 Raises
1187 ------
1188 RuntimeError
1189 Raised if a configuration option is set to `True`, but the
1190 required calibration data has not been specified.
1192 Notes
1193 -----
1194 The current processed exposure can be viewed by setting the
1195 appropriate `lsstDebug` entries in the ``debug.display``
1196 dictionary. The names of these entries correspond to some of
1197 the `IsrTaskConfig` Boolean options, with the value denoting the
1198 frame to use. The exposure is shown inside the matching
1199 option check and after the processing of that step has
1200 finished. The steps with debug points are:
1202 * doAssembleCcd
1203 * doBias
1204 * doCrosstalk
1205 * doBrighterFatter
1206 * doDark
1207 * doFringe
1208 * doStrayLight
1209 * doFlat
1211 In addition, setting the ``postISRCCD`` entry displays the
1212 exposure after all ISR processing has finished.
1213 """
1215 ccdExposure = self.ensureExposure(ccdExposure, camera, detectorNum)
1216 bias = self.ensureExposure(bias, camera, detectorNum)
1217 dark = self.ensureExposure(dark, camera, detectorNum)
1218 flat = self.ensureExposure(flat, camera, detectorNum)
1220 ccd = ccdExposure.getDetector()
1221 filterLabel = ccdExposure.getFilter()
1222 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
1224 if not ccd:
1225 assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd."
1226 ccd = [FakeAmp(ccdExposure, self.config)]
1228 # Validate Input
1229 if self.config.doBias and bias is None:
1230 raise RuntimeError("Must supply a bias exposure if config.doBias=True.")
1231 if self.doLinearize(ccd) and linearizer is None:
1232 raise RuntimeError("Must supply a linearizer if config.doLinearize=True for this detector.")
1233 if self.config.doBrighterFatter and bfKernel is None:
1234 raise RuntimeError("Must supply a kernel if config.doBrighterFatter=True.")
1235 if self.config.doDark and dark is None:
1236 raise RuntimeError("Must supply a dark exposure if config.doDark=True.")
1237 if self.config.doFlat and flat is None:
1238 raise RuntimeError("Must supply a flat exposure if config.doFlat=True.")
1239 if self.config.doDefect and defects is None:
1240 raise RuntimeError("Must supply defects if config.doDefect=True.")
1241 if (self.config.doFringe and physicalFilter in self.fringe.config.filters
1242 and fringes.fringes is None):
1243 # The `fringes` object needs to be a pipeBase.Struct, as
1244 # we use it as a `dict` for the parameters of
1245 # `FringeTask.run()`. The `fringes.fringes` `list` may
1246 # not be `None` if `doFringe=True`. Otherwise, raise.
1247 raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct.")
1248 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters
1249 and illumMaskedImage is None):
1250 raise RuntimeError("Must supply an illumcor if config.doIlluminationCorrection=True.")
1251 if (self.config.doDeferredCharge and deferredChargeCalib is None):
1252 raise RuntimeError("Must supply a deferred charge calibration if config.doDeferredCharge=True.")
1254 if self.config.doHeaderProvenance:
1255 # Inputs have been validated, so we can add their date
1256 # information to the output header.
1257 exposureMetadata = ccdExposure.getMetadata()
1258 if self.config.doBias:
1259 exposureMetadata["LSST CALIB DATE BIAS"] = self.extractCalibDate(bias)
1260 if self.config.doBrighterFatter:
1261 exposureMetadata["LSST CALIB DATE BFK"] = self.extractCalibDate(bfKernel)
1262 if self.config.doCrosstalk:
1263 exposureMetadata["LSST CALIB DATE CROSSTALK"] = self.extractCalibDate(crosstalk)
1264 if self.config.doDark:
1265 exposureMetadata["LSST CALIB DATE DARK"] = self.extractCalibDate(dark)
1266 if self.config.doDefect:
1267 exposureMetadata["LSST CALIB DATE DEFECTS"] = self.extractCalibDate(defects)
1268 if self.config.doDeferredCharge:
1269 exposureMetadata["LSST CALIB DATE CTI"] = self.extractCalibDate(deferredChargeCalib)
1270 if self.config.doFlat:
1271 exposureMetadata["LSST CALIB DATE FLAT"] = self.extractCalibDate(flat)
1272 if (self.config.doFringe and physicalFilter in self.fringe.config.filters):
1273 exposureMetadata["LSST CALIB DATE FRINGE"] = self.extractCalibDate(fringes.fringes)
1274 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters):
1275 exposureMetadata["LSST CALIB DATE ILLUMINATION"] = self.extractCalibDate(illumMaskedImage)
1276 if self.doLinearize(ccd):
1277 exposureMetadata["LSST CALIB DATE LINEARIZER"] = self.extractCalibDate(linearizer)
1278 if self.config.usePtcGains or self.config.usePtcReadNoise:
1279 exposureMetadata["LSST CALIB DATE PTC"] = self.extractCalibDate(ptc)
1280 if self.config.doStrayLight:
1281 exposureMetadata["LSST CALIB DATE STRAYLIGHT"] = self.extractCalibDate(strayLightData)
1282 if self.config.doAttachTransmissionCurve:
1283 exposureMetadata["LSST CALIB DATE OPTICS_TR"] = self.extractCalibDate(opticsTransmission)
1284 exposureMetadata["LSST CALIB DATE FILTER_TR"] = self.extractCalibDate(filterTransmission)
1285 exposureMetadata["LSST CALIB DATE SENSOR_TR"] = self.extractCalibDate(sensorTransmission)
1286 exposureMetadata["LSST CALIB DATE ATMOSP_TR"] = self.extractCalibDate(atmosphereTransmission)
1288 # Begin ISR processing.
1289 if self.config.doConvertIntToFloat:
1290 self.log.info("Converting exposure to floating point values.")
1291 ccdExposure = self.convertIntToFloat(ccdExposure)
1293 if self.config.doBias and self.config.doBiasBeforeOverscan:
1294 self.log.info("Applying bias correction.")
1295 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1296 trimToFit=self.config.doTrimToMatchCalib)
1297 self.debugView(ccdExposure, "doBias")
1299 # Amplifier level processing.
1300 overscans = []
1301 for amp in ccd:
1302 # if ccdExposure is one amp,
1303 # check for coverage to prevent performing ops multiple times
1304 if ccdExposure.getBBox().contains(amp.getBBox()):
1305 # Check for fully masked bad amplifiers,
1306 # and generate masks for SUSPECT and SATURATED values.
1307 badAmp = self.maskAmplifier(ccdExposure, amp, defects)
1309 if self.config.doOverscan and not badAmp:
1310 # Overscan correction on amp-by-amp basis.
1311 overscanResults = self.overscanCorrection(ccdExposure, amp)
1312 self.log.debug("Corrected overscan for amplifier %s.", amp.getName())
1313 if overscanResults is not None and \
1314 self.config.qa is not None and self.config.qa.saveStats is True:
1316 self.metadata[f"FIT MEDIAN {amp.getName()}"] = overscanResults.overscanMean
1317 self.metadata[f"FIT STDEV {amp.getName()}"] = overscanResults.overscanSigma
1318 self.log.debug(" Overscan stats for amplifer %s: %f +/- %f",
1319 amp.getName(), overscanResults.overscanMean,
1320 overscanResults.overscanSigma)
1322 self.metadata[f"RESIDUAL MEDIAN {amp.getName()}"] = overscanResults.residualMean
1323 self.metadata[f"RESIDUAL STDEV {amp.getName()}"] = overscanResults.residualSigma
1324 self.log.debug(" Overscan stats for amplifer %s after correction: %f +/- %f",
1325 amp.getName(), overscanResults.residualMean,
1326 overscanResults.residualSigma)
1328 ccdExposure.getMetadata().set('OVERSCAN', "Overscan corrected")
1329 else:
1330 if badAmp:
1331 self.log.warning("Amplifier %s is bad.", amp.getName())
1332 overscanResults = None
1334 overscans.append(overscanResults if overscanResults is not None else None)
1335 else:
1336 self.log.info("Skipped OSCAN for %s.", amp.getName())
1338 if self.config.doDeferredCharge:
1339 self.log.info("Applying deferred charge/CTI correction.")
1340 self.deferredChargeCorrection.run(ccdExposure, deferredChargeCalib)
1341 self.debugView(ccdExposure, "doDeferredCharge")
1343 if self.config.doCrosstalk and self.config.doCrosstalkBeforeAssemble:
1344 self.log.info("Applying crosstalk correction.")
1345 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1346 crosstalkSources=crosstalkSources, camera=camera)
1347 self.debugView(ccdExposure, "doCrosstalk")
1349 if self.config.doAssembleCcd:
1350 self.log.info("Assembling CCD from amplifiers.")
1351 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1353 if self.config.expectWcs and not ccdExposure.getWcs():
1354 self.log.warning("No WCS found in input exposure.")
1355 self.debugView(ccdExposure, "doAssembleCcd")
1357 ossThumb = None
1358 if self.config.qa.doThumbnailOss:
1359 ossThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1361 if self.config.doBias and not self.config.doBiasBeforeOverscan:
1362 self.log.info("Applying bias correction.")
1363 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1364 trimToFit=self.config.doTrimToMatchCalib)
1365 self.debugView(ccdExposure, "doBias")
1367 if self.config.doVariance:
1368 for amp, overscanResults in zip(ccd, overscans):
1369 if ccdExposure.getBBox().contains(amp.getBBox()):
1370 self.log.debug("Constructing variance map for amplifer %s.", amp.getName())
1371 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1372 if overscanResults is not None:
1373 self.updateVariance(ampExposure, amp,
1374 overscanImage=overscanResults.overscanImage,
1375 ptcDataset=ptc)
1376 else:
1377 self.updateVariance(ampExposure, amp,
1378 overscanImage=None,
1379 ptcDataset=ptc)
1380 if self.config.qa is not None and self.config.qa.saveStats is True:
1381 qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
1382 afwMath.MEDIAN | afwMath.STDEVCLIP)
1383 self.metadata[f"ISR VARIANCE {amp.getName()} MEDIAN"] = \
1384 qaStats.getValue(afwMath.MEDIAN)
1385 self.metadata[f"ISR VARIANCE {amp.getName()} STDEV"] = \
1386 qaStats.getValue(afwMath.STDEVCLIP)
1387 self.log.debug(" Variance stats for amplifer %s: %f +/- %f.",
1388 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1389 qaStats.getValue(afwMath.STDEVCLIP))
1390 if self.config.maskNegativeVariance:
1391 self.maskNegativeVariance(ccdExposure)
1393 if self.doLinearize(ccd):
1394 self.log.info("Applying linearizer.")
1395 linearizer.applyLinearity(image=ccdExposure.getMaskedImage().getImage(),
1396 detector=ccd, log=self.log)
1398 if self.config.doCrosstalk and not self.config.doCrosstalkBeforeAssemble:
1399 self.log.info("Applying crosstalk correction.")
1400 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1401 crosstalkSources=crosstalkSources, isTrimmed=True)
1402 self.debugView(ccdExposure, "doCrosstalk")
1404 # Masking block. Optionally mask known defects, NAN/inf pixels,
1405 # widen trails, and do anything else the camera needs. Saturated and
1406 # suspect pixels have already been masked.
1407 if self.config.doDefect:
1408 self.log.info("Masking defects.")
1409 self.maskDefect(ccdExposure, defects)
1411 if self.config.numEdgeSuspect > 0:
1412 self.log.info("Masking edges as SUSPECT.")
1413 self.maskEdges(ccdExposure, numEdgePixels=self.config.numEdgeSuspect,
1414 maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
1416 if self.config.doNanMasking:
1417 self.log.info("Masking non-finite (NAN, inf) value pixels.")
1418 self.maskNan(ccdExposure)
1420 if self.config.doWidenSaturationTrails:
1421 self.log.info("Widening saturation trails.")
1422 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1424 if self.config.doCameraSpecificMasking:
1425 self.log.info("Masking regions for camera specific reasons.")
1426 self.masking.run(ccdExposure)
1428 if self.config.doBrighterFatter:
1429 # We need to apply flats and darks before we can interpolate, and
1430 # we need to interpolate before we do B-F, but we do B-F without
1431 # the flats and darks applied so we can work in units of electrons
1432 # or holes. This context manager applies and then removes the darks
1433 # and flats.
1434 #
1435 # We also do not want to interpolate values here, so operate on
1436 # temporary images so we can apply only the BF-correction and roll
1437 # back the interpolation.
1438 interpExp = ccdExposure.clone()
1439 with self.flatContext(interpExp, flat, dark):
1440 isrFunctions.interpolateFromMask(
1441 maskedImage=interpExp.getMaskedImage(),
1442 fwhm=self.config.fwhm,
1443 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1444 maskNameList=list(self.config.brighterFatterMaskListToInterpolate)
1445 )
1446 bfExp = interpExp.clone()
1448 self.log.info("Applying brighter-fatter correction using kernel type %s / gains %s.",
1449 type(bfKernel), type(bfGains))
1450 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1451 self.config.brighterFatterMaxIter,
1452 self.config.brighterFatterThreshold,
1453 self.config.brighterFatterApplyGain,
1454 bfGains)
1455 if bfResults[1] == self.config.brighterFatterMaxIter:
1456 self.log.warning("Brighter-fatter correction did not converge, final difference %f.",
1457 bfResults[0])
1458 else:
1459 self.log.info("Finished brighter-fatter correction in %d iterations.",
1460 bfResults[1])
1461 image = ccdExposure.getMaskedImage().getImage()
1462 bfCorr = bfExp.getMaskedImage().getImage()
1463 bfCorr -= interpExp.getMaskedImage().getImage()
1464 image += bfCorr
1466 # Applying the brighter-fatter correction applies a
1467 # convolution to the science image. At the edges this
1468 # convolution may not have sufficient valid pixels to
1469 # produce a valid correction. Mark pixels within the size
1470 # of the brighter-fatter kernel as EDGE to warn of this
1471 # fact.
1472 self.log.info("Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1473 self.maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1474 maskPlane="EDGE")
1476 if self.config.brighterFatterMaskGrowSize > 0:
1477 self.log.info("Growing masks to account for brighter-fatter kernel convolution.")
1478 for maskPlane in self.config.brighterFatterMaskListToInterpolate:
1479 isrFunctions.growMasks(ccdExposure.getMask(),
1480 radius=self.config.brighterFatterMaskGrowSize,
1481 maskNameList=maskPlane,
1482 maskValue=maskPlane)
1484 self.debugView(ccdExposure, "doBrighterFatter")
1486 if self.config.doDark:
1487 self.log.info("Applying dark correction.")
1488 self.darkCorrection(ccdExposure, dark)
1489 self.debugView(ccdExposure, "doDark")
1491 if self.config.doFringe and not self.config.fringeAfterFlat:
1492 self.log.info("Applying fringe correction before flat.")
1493 self.fringe.run(ccdExposure, **fringes.getDict())
1494 self.debugView(ccdExposure, "doFringe")
1496 if self.config.doStrayLight and self.strayLight.check(ccdExposure):
1497 self.log.info("Checking strayLight correction.")
1498 self.strayLight.run(ccdExposure, strayLightData)
1499 self.debugView(ccdExposure, "doStrayLight")
1501 if self.config.doFlat:
1502 self.log.info("Applying flat correction.")
1503 self.flatCorrection(ccdExposure, flat)
1504 self.debugView(ccdExposure, "doFlat")
1506 if self.config.doApplyGains:
1507 self.log.info("Applying gain correction instead of flat.")
1508 if self.config.usePtcGains:
1509 self.log.info("Using gains from the Photon Transfer Curve.")
1510 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains,
1511 ptcGains=ptc.gain)
1512 else:
1513 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains)
1515 if self.config.doFringe and self.config.fringeAfterFlat:
1516 self.log.info("Applying fringe correction after flat.")
1517 self.fringe.run(ccdExposure, **fringes.getDict())
1519 if self.config.doVignette:
1520 if self.config.doMaskVignettePolygon:
1521 self.log.info("Constructing, attaching, and masking vignette polygon.")
1522 else:
1523 self.log.info("Constructing and attaching vignette polygon.")
1524 self.vignettePolygon = self.vignette.run(
1525 exposure=ccdExposure, doUpdateMask=self.config.doMaskVignettePolygon,
1526 vignetteValue=self.config.vignetteValue, log=self.log)
1528 if self.config.doAttachTransmissionCurve:
1529 self.log.info("Adding transmission curves.")
1530 isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission,
1531 filterTransmission=filterTransmission,
1532 sensorTransmission=sensorTransmission,
1533 atmosphereTransmission=atmosphereTransmission)
1535 flattenedThumb = None
1536 if self.config.qa.doThumbnailFlattened:
1537 flattenedThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1539 if self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters:
1540 self.log.info("Performing illumination correction.")
1541 isrFunctions.illuminationCorrection(ccdExposure.getMaskedImage(),
1542 illumMaskedImage, illumScale=self.config.illumScale,
1543 trimToFit=self.config.doTrimToMatchCalib)
1545 preInterpExp = None
1546 if self.config.doSaveInterpPixels:
1547 preInterpExp = ccdExposure.clone()
1549 # Reset and interpolate bad pixels.
1550 #
1551 # Large contiguous bad regions (which should have the BAD mask
1552 # bit set) should have their values set to the image median.
1553 # This group should include defects and bad amplifiers. As the
1554 # area covered by these defects are large, there's little
1555 # reason to expect that interpolation would provide a more
1556 # useful value.
1557 #
1558 # Smaller defects can be safely interpolated after the larger
1559 # regions have had their pixel values reset. This ensures
1560 # that the remaining defects adjacent to bad amplifiers (as an
1561 # example) do not attempt to interpolate extreme values.
1562 if self.config.doSetBadRegions:
1563 badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure)
1564 if badPixelCount > 0:
1565 self.log.info("Set %d BAD pixels to %f.", badPixelCount, badPixelValue)
1567 if self.config.doInterpolate:
1568 self.log.info("Interpolating masked pixels.")
1569 isrFunctions.interpolateFromMask(
1570 maskedImage=ccdExposure.getMaskedImage(),
1571 fwhm=self.config.fwhm,
1572 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1573 maskNameList=list(self.config.maskListToInterpolate)
1574 )
1576 self.roughZeroPoint(ccdExposure)
1578 # correct for amp offsets within the CCD
1579 if self.config.doAmpOffset:
1580 self.log.info("Correcting amp offsets.")
1581 self.ampOffset.run(ccdExposure)
1583 if self.config.doMeasureBackground:
1584 self.log.info("Measuring background level.")
1585 self.measureBackground(ccdExposure, self.config.qa)
1587 if self.config.qa is not None and self.config.qa.saveStats is True:
1588 for amp in ccd:
1589 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1590 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1591 afwMath.MEDIAN | afwMath.STDEVCLIP)
1592 self.metadata[f"ISR BACKGROUND {amp.getName()} MEDIAN"] = qaStats.getValue(afwMath.MEDIAN)
1593 self.metadata[f"ISR BACKGROUND {amp.getName()} STDEV"] = \
1594 qaStats.getValue(afwMath.STDEVCLIP)
1595 self.log.debug(" Background stats for amplifer %s: %f +/- %f",
1596 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1597 qaStats.getValue(afwMath.STDEVCLIP))
1599 # Calculate standard image quality statistics
1600 if self.config.doStandardStatistics:
1601 metadata = ccdExposure.getMetadata()
1602 for amp in ccd:
1603 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1604 ampName = amp.getName()
1605 metadata[f"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels(
1606 ampExposure.getMaskedImage(),
1607 [self.config.saturatedMaskName]
1608 )
1609 metadata[f"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels(
1610 ampExposure.getMaskedImage(),
1611 ["BAD"]
1612 )
1613 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1614 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP)
1616 metadata[f"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN)
1617 metadata[f"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN)
1618 metadata[f"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP)
1620 k1 = f"LSST ISR FINAL MEDIAN {ampName}"
1621 k2 = f"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
1622 if self.config.doOverscan and k1 in metadata and k2 in metadata:
1623 metadata[f"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2]
1624 else:
1625 metadata[f"LSST ISR LEVEL {ampName}"] = numpy.nan
1627 # calculate additional statistics.
1628 outputStatistics = None
1629 if self.config.doCalculateStatistics:
1630 outputStatistics = self.isrStats.run(ccdExposure, overscanResults=overscans,
1631 ptc=ptc).results
1633 self.debugView(ccdExposure, "postISRCCD")
1635 return pipeBase.Struct(
1636 exposure=ccdExposure,
1637 ossThumb=ossThumb,
1638 flattenedThumb=flattenedThumb,
1640 preInterpExposure=preInterpExp,
1641 outputExposure=ccdExposure,
1642 outputOssThumbnail=ossThumb,
1643 outputFlattenedThumbnail=flattenedThumb,
1644 outputStatistics=outputStatistics,
1645 )
1647 def ensureExposure(self, inputExp, camera=None, detectorNum=None):
1648 """Ensure that the data returned by Butler is a fully constructed exp.
1650 ISR requires exposure-level image data for historical reasons, so if we
1651 did not recieve that from Butler, construct it from what we have,
1652 modifying the input in place.
1654 Parameters
1655 ----------
1656 inputExp : `lsst.afw.image` image-type.
1657 The input data structure obtained from Butler.
1658 Can be `lsst.afw.image.Exposure`,
1659 `lsst.afw.image.DecoratedImageU`,
1660 or `lsst.afw.image.ImageF`
1661 camera : `lsst.afw.cameraGeom.camera`, optional
1662 The camera associated with the image. Used to find the appropriate
1663 detector if detector is not already set.
1664 detectorNum : `int`, optional
1665 The detector in the camera to attach, if the detector is not
1666 already set.
1668 Returns
1669 -------
1670 inputExp : `lsst.afw.image.Exposure`
1671 The re-constructed exposure, with appropriate detector parameters.
1673 Raises
1674 ------
1675 TypeError
1676 Raised if the input data cannot be used to construct an exposure.
1677 """
1678 if isinstance(inputExp, afwImage.DecoratedImageU):
1679 inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp))
1680 elif isinstance(inputExp, afwImage.ImageF):
1681 inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp))
1682 elif isinstance(inputExp, afwImage.MaskedImageF):
1683 inputExp = afwImage.makeExposure(inputExp)
1684 elif isinstance(inputExp, afwImage.Exposure):
1685 pass
1686 elif inputExp is None:
1687 # Assume this will be caught by the setup if it is a problem.
1688 return inputExp
1689 else:
1690 raise TypeError("Input Exposure is not known type in isrTask.ensureExposure: %s." %
1691 (type(inputExp), ))
1693 if inputExp.getDetector() is None:
1694 if camera is None or detectorNum is None:
1695 raise RuntimeError('Must supply both a camera and detector number when using exposures '
1696 'without a detector set.')
1697 inputExp.setDetector(camera[detectorNum])
1699 return inputExp
1701 @staticmethod
1702 def extractCalibDate(calib):
1703 """Extract common calibration metadata values that will be written to
1704 output header.
1706 Parameters
1707 ----------
1708 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib`
1709 Calibration to pull date information from.
1711 Returns
1712 -------
1713 dateString : `str`
1714 Calibration creation date string to add to header.
1715 """
1716 if hasattr(calib, "getMetadata"):
1717 if 'CALIB_CREATION_DATE' in calib.getMetadata():
1718 return " ".join((calib.getMetadata().get("CALIB_CREATION_DATE", "Unknown"),
1719 calib.getMetadata().get("CALIB_CREATION_TIME", "Unknown")))
1720 else:
1721 return " ".join((calib.getMetadata().get("CALIB_CREATE_DATE", "Unknown"),
1722 calib.getMetadata().get("CALIB_CREATE_TIME", "Unknown")))
1723 else:
1724 return "Unknown Unknown"
1726 def convertIntToFloat(self, exposure):
1727 """Convert exposure image from uint16 to float.
1729 If the exposure does not need to be converted, the input is
1730 immediately returned. For exposures that are converted to use
1731 floating point pixels, the variance is set to unity and the
1732 mask to zero.
1734 Parameters
1735 ----------
1736 exposure : `lsst.afw.image.Exposure`
1737 The raw exposure to be converted.
1739 Returns
1740 -------
1741 newexposure : `lsst.afw.image.Exposure`
1742 The input ``exposure``, converted to floating point pixels.
1744 Raises
1745 ------
1746 RuntimeError
1747 Raised if the exposure type cannot be converted to float.
1749 """
1750 if isinstance(exposure, afwImage.ExposureF):
1751 # Nothing to be done
1752 self.log.debug("Exposure already of type float.")
1753 return exposure
1754 if not hasattr(exposure, "convertF"):
1755 raise RuntimeError("Unable to convert exposure (%s) to float." % type(exposure))
1757 newexposure = exposure.convertF()
1758 newexposure.variance[:] = 1
1759 newexposure.mask[:] = 0x0
1761 return newexposure
1763 def maskAmplifier(self, ccdExposure, amp, defects):
1764 """Identify bad amplifiers, saturated and suspect pixels.
1766 Parameters
1767 ----------
1768 ccdExposure : `lsst.afw.image.Exposure`
1769 Input exposure to be masked.
1770 amp : `lsst.afw.cameraGeom.Amplifier`
1771 Catalog of parameters defining the amplifier on this
1772 exposure to mask.
1773 defects : `lsst.ip.isr.Defects`
1774 List of defects. Used to determine if the entire
1775 amplifier is bad.
1777 Returns
1778 -------
1779 badAmp : `Bool`
1780 If this is true, the entire amplifier area is covered by
1781 defects and unusable.
1783 """
1784 maskedImage = ccdExposure.getMaskedImage()
1786 badAmp = False
1788 # Check if entire amp region is defined as a defect
1789 # NB: need to use amp.getBBox() for correct comparison with current
1790 # defects definition.
1791 if defects is not None:
1792 badAmp = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects]))
1794 # In the case of a bad amp, we will set mask to "BAD"
1795 # (here use amp.getRawBBox() for correct association with pixels in
1796 # current ccdExposure).
1797 if badAmp:
1798 dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
1799 afwImage.PARENT)
1800 maskView = dataView.getMask()
1801 maskView |= maskView.getPlaneBitMask("BAD")
1802 del maskView
1803 return badAmp
1805 # Mask remaining defects after assembleCcd() to allow for defects that
1806 # cross amplifier boundaries. Saturation and suspect pixels can be
1807 # masked now, though.
1808 limits = dict()
1809 if self.config.doSaturation and not badAmp:
1810 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
1811 if self.config.doSuspect and not badAmp:
1812 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
1813 if math.isfinite(self.config.saturation):
1814 limits.update({self.config.saturatedMaskName: self.config.saturation})
1816 for maskName, maskThreshold in limits.items():
1817 if not math.isnan(maskThreshold):
1818 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1819 isrFunctions.makeThresholdMask(
1820 maskedImage=dataView,
1821 threshold=maskThreshold,
1822 growFootprints=0,
1823 maskName=maskName
1824 )
1826 # Determine if we've fully masked this amplifier with SUSPECT and
1827 # SAT pixels.
1828 maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
1829 afwImage.PARENT)
1830 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
1831 self.config.suspectMaskName])
1832 if numpy.all(maskView.getArray() & maskVal > 0):
1833 badAmp = True
1834 maskView |= maskView.getPlaneBitMask("BAD")
1836 return badAmp
1838 def overscanCorrection(self, ccdExposure, amp):
1839 """Apply overscan correction in place.
1841 This method does initial pixel rejection of the overscan
1842 region. The overscan can also be optionally segmented to
1843 allow for discontinuous overscan responses to be fit
1844 separately. The actual overscan subtraction is performed by
1845 the `lsst.ip.isr.overscan.OverscanTask`, which is called here
1846 after the amplifier is preprocessed.
1848 Parameters
1849 ----------
1850 ccdExposure : `lsst.afw.image.Exposure`
1851 Exposure to have overscan correction performed.
1852 amp : `lsst.afw.cameraGeom.Amplifer`
1853 The amplifier to consider while correcting the overscan.
1855 Returns
1856 -------
1857 overscanResults : `lsst.pipe.base.Struct`
1858 Result struct with components:
1860 ``imageFit``
1861 Value or fit subtracted from the amplifier image data.
1862 (scalar or `lsst.afw.image.Image`)
1863 ``overscanFit``
1864 Value or fit subtracted from the overscan image data.
1865 (scalar or `lsst.afw.image.Image`)
1866 ``overscanImage``
1867 Image of the overscan region with the overscan
1868 correction applied. This quantity is used to estimate
1869 the amplifier read noise empirically.
1870 (`lsst.afw.image.Image`)
1871 ``edgeMask``
1872 Mask of the suspect pixels. (`lsst.afw.image.Mask`)
1873 ``overscanMean``
1874 Median overscan fit value. (`float`)
1875 ``overscanSigma``
1876 Clipped standard deviation of the overscan after
1877 correction. (`float`)
1879 Raises
1880 ------
1881 RuntimeError
1882 Raised if the ``amp`` does not contain raw pixel information.
1884 See Also
1885 --------
1886 lsst.ip.isr.overscan.OverscanTask
1888 """
1889 if amp.getRawHorizontalOverscanBBox().isEmpty():
1890 self.log.info("ISR_OSCAN: No overscan region. Not performing overscan correction.")
1891 return None
1893 # Perform overscan correction on subregions.
1894 overscanResults = self.overscan.run(ccdExposure, amp)
1896 metadata = ccdExposure.getMetadata()
1897 ampName = amp.getName()
1899 keyBase = "LSST ISR OVERSCAN"
1900 # Updated quantities
1901 if isinstance(overscanResults.overscanMean, float):
1902 # Serial overscan correction only:
1903 metadata[f"{keyBase} SERIAL MEAN {ampName}"] = overscanResults.overscanMean
1904 metadata[f"{keyBase} SERIAL MEDIAN {ampName}"] = overscanResults.overscanMedian
1905 metadata[f"{keyBase} SERIAL STDEV {ampName}"] = overscanResults.overscanSigma
1907 metadata[f"{keyBase} RESIDUAL SERIAL MEAN {ampName}"] = overscanResults.residualMean
1908 metadata[f"{keyBase} RESIDUAL SERIAL MEDIAN {ampName}"] = overscanResults.residualMedian
1909 metadata[f"{keyBase} RESIDUAL SERIAL STDEV {ampName}"] = overscanResults.residualSigma
1910 elif isinstance(overscanResults.overscanMean, tuple):
1911 # Both serial and parallel overscan have run:
1912 metadata[f"{keyBase} SERIAL MEAN {ampName}"] = overscanResults.overscanMean[0]
1913 metadata[f"{keyBase} SERIAL MEDIAN {ampName}"] = overscanResults.overscanMedian[0]
1914 metadata[f"{keyBase} SERIAL STDEV {ampName}"] = overscanResults.overscanSigma[0]
1916 metadata[f"{keyBase} PARALLEL MEAN {ampName}"] = overscanResults.overscanMean[1]
1917 metadata[f"{keyBase} PARALLEL MEDIAN {ampName}"] = overscanResults.overscanMedian[1]
1918 metadata[f"{keyBase} PARALLEL STDEV {ampName}"] = overscanResults.overscanSigma[1]
1920 metadata[f"{keyBase} RESIDUAL SERIAL MEAN {ampName}"] = overscanResults.residualMean[0]
1921 metadata[f"{keyBase} RESIDUAL SERIAL MEDIAN {ampName}"] = overscanResults.residualMedian[0]
1922 metadata[f"{keyBase} RESIDUAL SERIAL STDEV {ampName}"] = overscanResults.residualSigma[0]
1924 metadata[f"{keyBase} RESIDUAL PARALLEL MEAN {ampName}"] = overscanResults.residualMean[1]
1925 metadata[f"{keyBase} RESIDUAL PARALLEL MEDIAN {ampName}"] = overscanResults.residualMedian[1]
1926 metadata[f"{keyBase} RESIDUAL PARALLEL STDEV {ampName}"] = overscanResults.residualSigma[1]
1927 else:
1928 self.log.warning("Unexpected type for overscan values; none added to header.")
1930 return overscanResults
1932 def updateVariance(self, ampExposure, amp, overscanImage=None, ptcDataset=None):
1933 """Set the variance plane using the gain and read noise
1935 The read noise is calculated from the ``overscanImage`` if the
1936 ``doEmpiricalReadNoise`` option is set in the configuration; otherwise
1937 the value from the amplifier data is used.
1939 Parameters
1940 ----------
1941 ampExposure : `lsst.afw.image.Exposure`
1942 Exposure to process.
1943 amp : `lsst.afw.cameraGeom.Amplifier` or `FakeAmp`
1944 Amplifier detector data.
1945 overscanImage : `lsst.afw.image.MaskedImage`, optional.
1946 Image of overscan, required only for empirical read noise.
1947 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
1948 PTC dataset containing the gains and read noise.
1950 Raises
1951 ------
1952 RuntimeError
1953 Raised if either ``usePtcGains`` of ``usePtcReadNoise``
1954 are ``True``, but ptcDataset is not provided.
1956 Raised if ```doEmpiricalReadNoise`` is ``True`` but
1957 ``overscanImage`` is ``None``.
1959 See also
1960 --------
1961 lsst.ip.isr.isrFunctions.updateVariance
1962 """
1963 maskPlanes = [self.config.saturatedMaskName, self.config.suspectMaskName]
1964 if self.config.usePtcGains:
1965 if ptcDataset is None:
1966 raise RuntimeError("No ptcDataset provided to use PTC gains.")
1967 else:
1968 gain = ptcDataset.gain[amp.getName()]
1969 self.log.info("Using gain from Photon Transfer Curve.")
1970 else:
1971 gain = amp.getGain()
1973 if math.isnan(gain):
1974 gain = 1.0
1975 self.log.warning("Gain set to NAN! Updating to 1.0 to generate Poisson variance.")
1976 elif gain <= 0:
1977 patchedGain = 1.0
1978 self.log.warning("Gain for amp %s == %g <= 0; setting to %f.",
1979 amp.getName(), gain, patchedGain)
1980 gain = patchedGain
1982 if self.config.doEmpiricalReadNoise and overscanImage is None:
1983 badPixels = isrFunctions.countMaskedPixels(ampExposure.getMaskedImage(),
1984 [self.config.saturatedMaskName,
1985 self.config.suspectMaskName,
1986 "BAD", "NO_DATA"])
1987 allPixels = ampExposure.getWidth() * ampExposure.getHeight()
1988 if allPixels == badPixels:
1989 # If the image is bad, do not raise.
1990 self.log.info("Skipping empirical read noise for amp %s. No good pixels.",
1991 amp.getName())
1992 else:
1993 raise RuntimeError("Overscan is none for EmpiricalReadNoise.")
1995 if self.config.doEmpiricalReadNoise and overscanImage is not None:
1996 stats = afwMath.StatisticsControl()
1997 stats.setAndMask(overscanImage.mask.getPlaneBitMask(maskPlanes))
1998 readNoise = afwMath.makeStatistics(overscanImage.getImage(),
1999 afwMath.STDEVCLIP, stats).getValue()
2000 self.log.info("Calculated empirical read noise for amp %s: %f.",
2001 amp.getName(), readNoise)
2002 elif self.config.usePtcReadNoise:
2003 if ptcDataset is None:
2004 raise RuntimeError("No ptcDataset provided to use PTC readnoise.")
2005 else:
2006 readNoise = ptcDataset.noise[amp.getName()]
2007 self.log.info("Using read noise from Photon Transfer Curve.")
2008 else:
2009 readNoise = amp.getReadNoise()
2011 metadata = ampExposure.getMetadata()
2012 metadata[f'LSST GAIN {amp.getName()}'] = gain
2013 metadata[f'LSST READNOISE {amp.getName()}'] = readNoise
2015 isrFunctions.updateVariance(
2016 maskedImage=ampExposure.getMaskedImage(),
2017 gain=gain,
2018 readNoise=readNoise,
2019 )
2021 def maskNegativeVariance(self, exposure):
2022 """Identify and mask pixels with negative variance values.
2024 Parameters
2025 ----------
2026 exposure : `lsst.afw.image.Exposure`
2027 Exposure to process.
2029 See Also
2030 --------
2031 lsst.ip.isr.isrFunctions.updateVariance
2032 """
2033 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName)
2034 bad = numpy.where(exposure.getVariance().getArray() <= 0.0)
2035 exposure.mask.array[bad] |= maskPlane
2037 def darkCorrection(self, exposure, darkExposure, invert=False):
2038 """Apply dark correction in place.
2040 Parameters
2041 ----------
2042 exposure : `lsst.afw.image.Exposure`
2043 Exposure to process.
2044 darkExposure : `lsst.afw.image.Exposure`
2045 Dark exposure of the same size as ``exposure``.
2046 invert : `Bool`, optional
2047 If True, re-add the dark to an already corrected image.
2049 Raises
2050 ------
2051 RuntimeError
2052 Raised if either ``exposure`` or ``darkExposure`` do not
2053 have their dark time defined.
2055 See Also
2056 --------
2057 lsst.ip.isr.isrFunctions.darkCorrection
2058 """
2059 expScale = exposure.getInfo().getVisitInfo().getDarkTime()
2060 if math.isnan(expScale):
2061 raise RuntimeError("Exposure darktime is NAN.")
2062 if darkExposure.getInfo().getVisitInfo() is not None \
2063 and not math.isnan(darkExposure.getInfo().getVisitInfo().getDarkTime()):
2064 darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
2065 else:
2066 # DM-17444: darkExposure.getInfo.getVisitInfo() is None
2067 # so getDarkTime() does not exist.
2068 self.log.warning("darkExposure.getInfo().getVisitInfo() does not exist. Using darkScale = 1.0.")
2069 darkScale = 1.0
2071 isrFunctions.darkCorrection(
2072 maskedImage=exposure.getMaskedImage(),
2073 darkMaskedImage=darkExposure.getMaskedImage(),
2074 expScale=expScale,
2075 darkScale=darkScale,
2076 invert=invert,
2077 trimToFit=self.config.doTrimToMatchCalib
2078 )
2080 def doLinearize(self, detector):
2081 """Check if linearization is needed for the detector cameraGeom.
2083 Checks config.doLinearize and the linearity type of the first
2084 amplifier.
2086 Parameters
2087 ----------
2088 detector : `lsst.afw.cameraGeom.Detector`
2089 Detector to get linearity type from.
2091 Returns
2092 -------
2093 doLinearize : `Bool`
2094 If True, linearization should be performed.
2095 """
2096 return self.config.doLinearize and \
2097 detector.getAmplifiers()[0].getLinearityType() != NullLinearityType
2099 def flatCorrection(self, exposure, flatExposure, invert=False):
2100 """Apply flat correction in place.
2102 Parameters
2103 ----------
2104 exposure : `lsst.afw.image.Exposure`
2105 Exposure to process.
2106 flatExposure : `lsst.afw.image.Exposure`
2107 Flat exposure of the same size as ``exposure``.
2108 invert : `Bool`, optional
2109 If True, unflatten an already flattened image.
2111 See Also
2112 --------
2113 lsst.ip.isr.isrFunctions.flatCorrection
2114 """
2115 isrFunctions.flatCorrection(
2116 maskedImage=exposure.getMaskedImage(),
2117 flatMaskedImage=flatExposure.getMaskedImage(),
2118 scalingType=self.config.flatScalingType,
2119 userScale=self.config.flatUserScale,
2120 invert=invert,
2121 trimToFit=self.config.doTrimToMatchCalib
2122 )
2124 def saturationDetection(self, exposure, amp):
2125 """Detect and mask saturated pixels in config.saturatedMaskName.
2127 Parameters
2128 ----------
2129 exposure : `lsst.afw.image.Exposure`
2130 Exposure to process. Only the amplifier DataSec is processed.
2131 amp : `lsst.afw.cameraGeom.Amplifier`
2132 Amplifier detector data.
2134 See Also
2135 --------
2136 lsst.ip.isr.isrFunctions.makeThresholdMask
2137 """
2138 if not math.isnan(amp.getSaturation()):
2139 maskedImage = exposure.getMaskedImage()
2140 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2141 isrFunctions.makeThresholdMask(
2142 maskedImage=dataView,
2143 threshold=amp.getSaturation(),
2144 growFootprints=0,
2145 maskName=self.config.saturatedMaskName,
2146 )
2148 def saturationInterpolation(self, exposure):
2149 """Interpolate over saturated pixels, in place.
2151 This method should be called after `saturationDetection`, to
2152 ensure that the saturated pixels have been identified in the
2153 SAT mask. It should also be called after `assembleCcd`, since
2154 saturated regions may cross amplifier boundaries.
2156 Parameters
2157 ----------
2158 exposure : `lsst.afw.image.Exposure`
2159 Exposure to process.
2161 See Also
2162 --------
2163 lsst.ip.isr.isrTask.saturationDetection
2164 lsst.ip.isr.isrFunctions.interpolateFromMask
2165 """
2166 isrFunctions.interpolateFromMask(
2167 maskedImage=exposure.getMaskedImage(),
2168 fwhm=self.config.fwhm,
2169 growSaturatedFootprints=self.config.growSaturationFootprintSize,
2170 maskNameList=list(self.config.saturatedMaskName),
2171 )
2173 def suspectDetection(self, exposure, amp):
2174 """Detect and mask suspect pixels in config.suspectMaskName.
2176 Parameters
2177 ----------
2178 exposure : `lsst.afw.image.Exposure`
2179 Exposure to process. Only the amplifier DataSec is processed.
2180 amp : `lsst.afw.cameraGeom.Amplifier`
2181 Amplifier detector data.
2183 See Also
2184 --------
2185 lsst.ip.isr.isrFunctions.makeThresholdMask
2187 Notes
2188 -----
2189 Suspect pixels are pixels whose value is greater than
2190 amp.getSuspectLevel(). This is intended to indicate pixels that may be
2191 affected by unknown systematics; for example if non-linearity
2192 corrections above a certain level are unstable then that would be a
2193 useful value for suspectLevel. A value of `nan` indicates that no such
2194 level exists and no pixels are to be masked as suspicious.
2195 """
2196 suspectLevel = amp.getSuspectLevel()
2197 if math.isnan(suspectLevel):
2198 return
2200 maskedImage = exposure.getMaskedImage()
2201 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2202 isrFunctions.makeThresholdMask(
2203 maskedImage=dataView,
2204 threshold=suspectLevel,
2205 growFootprints=0,
2206 maskName=self.config.suspectMaskName,
2207 )
2209 def maskDefect(self, exposure, defectBaseList):
2210 """Mask defects using mask plane "BAD", in place.
2212 Parameters
2213 ----------
2214 exposure : `lsst.afw.image.Exposure`
2215 Exposure to process.
2216 defectBaseList : defect-type
2217 List of defects to mask. Can be of type `lsst.ip.isr.Defects`
2218 or `list` of `lsst.afw.image.DefectBase`.
2220 Notes
2221 -----
2222 Call this after CCD assembly, since defects may cross amplifier
2223 boundaries.
2224 """
2225 maskedImage = exposure.getMaskedImage()
2226 if not isinstance(defectBaseList, Defects):
2227 # Promotes DefectBase to Defect
2228 defectList = Defects(defectBaseList)
2229 else:
2230 defectList = defectBaseList
2231 defectList.maskPixels(maskedImage, maskName="BAD")
2233 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'):
2234 """Mask edge pixels with applicable mask plane.
2236 Parameters
2237 ----------
2238 exposure : `lsst.afw.image.Exposure`
2239 Exposure to process.
2240 numEdgePixels : `int`, optional
2241 Number of edge pixels to mask.
2242 maskPlane : `str`, optional
2243 Mask plane name to use.
2244 level : `str`, optional
2245 Level at which to mask edges.
2246 """
2247 maskedImage = exposure.getMaskedImage()
2248 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane)
2250 if numEdgePixels > 0:
2251 if level == 'DETECTOR':
2252 boxes = [maskedImage.getBBox()]
2253 elif level == 'AMP':
2254 boxes = [amp.getBBox() for amp in exposure.getDetector()]
2256 for box in boxes:
2257 # This makes a bbox numEdgeSuspect pixels smaller than the
2258 # image on each side
2259 subImage = maskedImage[box]
2260 box.grow(-numEdgePixels)
2261 # Mask pixels outside box
2262 SourceDetectionTask.setEdgeBits(
2263 subImage,
2264 box,
2265 maskBitMask)
2267 def maskAndInterpolateDefects(self, exposure, defectBaseList):
2268 """Mask and interpolate defects using mask plane "BAD", in place.
2270 Parameters
2271 ----------
2272 exposure : `lsst.afw.image.Exposure`
2273 Exposure to process.
2274 defectBaseList : defects-like
2275 List of defects to mask and interpolate. Can be
2276 `lsst.ip.isr.Defects` or `list` of `lsst.afw.image.DefectBase`.
2278 See Also
2279 --------
2280 lsst.ip.isr.isrTask.maskDefect
2281 """
2282 self.maskDefect(exposure, defectBaseList)
2283 self.maskEdges(exposure, numEdgePixels=self.config.numEdgeSuspect,
2284 maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
2285 isrFunctions.interpolateFromMask(
2286 maskedImage=exposure.getMaskedImage(),
2287 fwhm=self.config.fwhm,
2288 growSaturatedFootprints=0,
2289 maskNameList=["BAD"],
2290 )
2292 def maskNan(self, exposure):
2293 """Mask NaNs using mask plane "UNMASKEDNAN", in place.
2295 Parameters
2296 ----------
2297 exposure : `lsst.afw.image.Exposure`
2298 Exposure to process.
2300 Notes
2301 -----
2302 We mask over all non-finite values (NaN, inf), including those
2303 that are masked with other bits (because those may or may not be
2304 interpolated over later, and we want to remove all NaN/infs).
2305 Despite this behaviour, the "UNMASKEDNAN" mask plane is used to
2306 preserve the historical name.
2307 """
2308 maskedImage = exposure.getMaskedImage()
2310 # Find and mask NaNs
2311 maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
2312 maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
2313 numNans = maskNans(maskedImage, maskVal)
2314 self.metadata["NUMNANS"] = numNans
2315 if numNans > 0:
2316 self.log.warning("There were %d unmasked NaNs.", numNans)
2318 def maskAndInterpolateNan(self, exposure):
2319 """"Mask and interpolate NaN/infs using mask plane "UNMASKEDNAN",
2320 in place.
2322 Parameters
2323 ----------
2324 exposure : `lsst.afw.image.Exposure`
2325 Exposure to process.
2327 See Also
2328 --------
2329 lsst.ip.isr.isrTask.maskNan
2330 """
2331 self.maskNan(exposure)
2332 isrFunctions.interpolateFromMask(
2333 maskedImage=exposure.getMaskedImage(),
2334 fwhm=self.config.fwhm,
2335 growSaturatedFootprints=0,
2336 maskNameList=["UNMASKEDNAN"],
2337 )
2339 def measureBackground(self, exposure, IsrQaConfig=None):
2340 """Measure the image background in subgrids, for quality control.
2342 Parameters
2343 ----------
2344 exposure : `lsst.afw.image.Exposure`
2345 Exposure to process.
2346 IsrQaConfig : `lsst.ip.isr.isrQa.IsrQaConfig`
2347 Configuration object containing parameters on which background
2348 statistics and subgrids to use.
2349 """
2350 if IsrQaConfig is not None:
2351 statsControl = afwMath.StatisticsControl(IsrQaConfig.flatness.clipSigma,
2352 IsrQaConfig.flatness.nIter)
2353 maskVal = exposure.getMaskedImage().getMask().getPlaneBitMask(["BAD", "SAT", "DETECTED"])
2354 statsControl.setAndMask(maskVal)
2355 maskedImage = exposure.getMaskedImage()
2356 stats = afwMath.makeStatistics(maskedImage, afwMath.MEDIAN | afwMath.STDEVCLIP, statsControl)
2357 skyLevel = stats.getValue(afwMath.MEDIAN)
2358 skySigma = stats.getValue(afwMath.STDEVCLIP)
2359 self.log.info("Flattened sky level: %f +/- %f.", skyLevel, skySigma)
2360 metadata = exposure.getMetadata()
2361 metadata["SKYLEVEL"] = skyLevel
2362 metadata["SKYSIGMA"] = skySigma
2364 # calcluating flatlevel over the subgrids
2365 stat = afwMath.MEANCLIP if IsrQaConfig.flatness.doClip else afwMath.MEAN
2366 meshXHalf = int(IsrQaConfig.flatness.meshX/2.)
2367 meshYHalf = int(IsrQaConfig.flatness.meshY/2.)
2368 nX = int((exposure.getWidth() + meshXHalf) / IsrQaConfig.flatness.meshX)
2369 nY = int((exposure.getHeight() + meshYHalf) / IsrQaConfig.flatness.meshY)
2370 skyLevels = numpy.zeros((nX, nY))
2372 for j in range(nY):
2373 yc = meshYHalf + j * IsrQaConfig.flatness.meshY
2374 for i in range(nX):
2375 xc = meshXHalf + i * IsrQaConfig.flatness.meshX
2377 xLLC = xc - meshXHalf
2378 yLLC = yc - meshYHalf
2379 xURC = xc + meshXHalf - 1
2380 yURC = yc + meshYHalf - 1
2382 bbox = lsst.geom.Box2I(lsst.geom.Point2I(xLLC, yLLC), lsst.geom.Point2I(xURC, yURC))
2383 miMesh = maskedImage.Factory(exposure.getMaskedImage(), bbox, afwImage.LOCAL)
2385 skyLevels[i, j] = afwMath.makeStatistics(miMesh, stat, statsControl).getValue()
2387 good = numpy.where(numpy.isfinite(skyLevels))
2388 skyMedian = numpy.median(skyLevels[good])
2389 flatness = (skyLevels[good] - skyMedian) / skyMedian
2390 flatness_rms = numpy.std(flatness)
2391 flatness_pp = flatness.max() - flatness.min() if len(flatness) > 0 else numpy.nan
2393 self.log.info("Measuring sky levels in %dx%d grids: %f.", nX, nY, skyMedian)
2394 self.log.info("Sky flatness in %dx%d grids - pp: %f rms: %f.",
2395 nX, nY, flatness_pp, flatness_rms)
2397 metadata["FLATNESS_PP"] = float(flatness_pp)
2398 metadata["FLATNESS_RMS"] = float(flatness_rms)
2399 metadata["FLATNESS_NGRIDS"] = '%dx%d' % (nX, nY)
2400 metadata["FLATNESS_MESHX"] = IsrQaConfig.flatness.meshX
2401 metadata["FLATNESS_MESHY"] = IsrQaConfig.flatness.meshY
2403 def roughZeroPoint(self, exposure):
2404 """Set an approximate magnitude zero point for the exposure.
2406 Parameters
2407 ----------
2408 exposure : `lsst.afw.image.Exposure`
2409 Exposure to process.
2410 """
2411 filterLabel = exposure.getFilter()
2412 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
2414 if physicalFilter in self.config.fluxMag0T1:
2415 fluxMag0 = self.config.fluxMag0T1[physicalFilter]
2416 else:
2417 self.log.warning("No rough magnitude zero point defined for filter %s.", physicalFilter)
2418 fluxMag0 = self.config.defaultFluxMag0T1
2420 expTime = exposure.getInfo().getVisitInfo().getExposureTime()
2421 if not expTime > 0: # handle NaN as well as <= 0
2422 self.log.warning("Non-positive exposure time; skipping rough zero point.")
2423 return
2425 self.log.info("Setting rough magnitude zero point for filter %s: %f",
2426 physicalFilter, 2.5*math.log10(fluxMag0*expTime))
2427 exposure.setPhotoCalib(afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0*expTime, 0.0))
2429 @contextmanager
2430 def flatContext(self, exp, flat, dark=None):
2431 """Context manager that applies and removes flats and darks,
2432 if the task is configured to apply them.
2434 Parameters
2435 ----------
2436 exp : `lsst.afw.image.Exposure`
2437 Exposure to process.
2438 flat : `lsst.afw.image.Exposure`
2439 Flat exposure the same size as ``exp``.
2440 dark : `lsst.afw.image.Exposure`, optional
2441 Dark exposure the same size as ``exp``.
2443 Yields
2444 ------
2445 exp : `lsst.afw.image.Exposure`
2446 The flat and dark corrected exposure.
2447 """
2448 if self.config.doDark and dark is not None:
2449 self.darkCorrection(exp, dark)
2450 if self.config.doFlat:
2451 self.flatCorrection(exp, flat)
2452 try:
2453 yield exp
2454 finally:
2455 if self.config.doFlat:
2456 self.flatCorrection(exp, flat, invert=True)
2457 if self.config.doDark and dark is not None:
2458 self.darkCorrection(exp, dark, invert=True)
2460 def debugView(self, exposure, stepname):
2461 """Utility function to examine ISR exposure at different stages.
2463 Parameters
2464 ----------
2465 exposure : `lsst.afw.image.Exposure`
2466 Exposure to view.
2467 stepname : `str`
2468 State of processing to view.
2469 """
2470 frame = getDebugFrame(self._display, stepname)
2471 if frame:
2472 display = getDisplay(frame)
2473 display.scale('asinh', 'zscale')
2474 display.mtv(exposure)
2475 prompt = "Press Enter to continue [c]... "
2476 while True:
2477 ans = input(prompt).lower()
2478 if ans in ("", "c",):
2479 break
2482class FakeAmp(object):
2483 """A Detector-like object that supports returning gain and saturation level
2485 This is used when the input exposure does not have a detector.
2487 Parameters
2488 ----------
2489 exposure : `lsst.afw.image.Exposure`
2490 Exposure to generate a fake amplifier for.
2491 config : `lsst.ip.isr.isrTaskConfig`
2492 Configuration to apply to the fake amplifier.
2493 """
2495 def __init__(self, exposure, config):
2496 self._bbox = exposure.getBBox(afwImage.LOCAL)
2497 self._RawHorizontalOverscanBBox = lsst.geom.Box2I()
2498 self._gain = config.gain
2499 self._readNoise = config.readNoise
2500 self._saturation = config.saturation
2502 def getBBox(self):
2503 return self._bbox
2505 def getRawBBox(self):
2506 return self._bbox
2508 def getRawHorizontalOverscanBBox(self):
2509 return self._RawHorizontalOverscanBBox
2511 def getGain(self):
2512 return self._gain
2514 def getReadNoise(self):
2515 return self._readNoise
2517 def getSaturation(self):
2518 return self._saturation
2520 def getSuspectLevel(self):
2521 return float("NaN")