LSST Applications g00274db5b6+edbf708997,g00d0e8bbd7+edbf708997,g199a45376c+5137f08352,g1fd858c14a+1d4b6db739,g262e1987ae+f4d9505c4f,g29ae962dfc+7156fb1a53,g2cef7863aa+73c82f25e4,g35bb328faa+edbf708997,g3e17d7035e+5b3adc59f5,g3fd5ace14f+852fa6fbcb,g47891489e3+6dc8069a4c,g53246c7159+edbf708997,g64539dfbff+9f17e571f4,g67b6fd64d1+6dc8069a4c,g74acd417e5+ae494d68d9,g786e29fd12+af89c03590,g7ae74a0b1c+a25e60b391,g7aefaa3e3d+536efcc10a,g7cc15d900a+d121454f8d,g87389fa792+a4172ec7da,g89139ef638+6dc8069a4c,g8d7436a09f+28c28d8d6d,g8ea07a8fe4+db21c37724,g92c671f44c+9f17e571f4,g98df359435+b2e6376b13,g99af87f6a8+b0f4ad7b8d,gac66b60396+966efe6077,gb88ae4c679+7dec8f19df,gbaa8f7a6c5+38b34f4976,gbf99507273+edbf708997,gc24b5d6ed1+9f17e571f4,gca7fc764a6+6dc8069a4c,gcc769fe2a4+97d0256649,gd7ef33dd92+6dc8069a4c,gdab6d2f7ff+ae494d68d9,gdbb4c4dda9+9f17e571f4,ge410e46f29+6dc8069a4c,geaed405ab2+e194be0d2b,w.2025.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
isrTask.py
Go to the documentation of this file.
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/>.
21
22__all__ = ["IsrTask", "IsrTaskConfig"]
23
24import math
25import numpy
26import numbers
27
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
34
35from contextlib import contextmanager
36from deprecated.sphinx import deprecated
37from lsstDebug import getDebugFrame
38
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
43
44from . import isrFunctions
45from . import isrQa
46from . import linearize
47from .defects import Defects
48
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)
65
66
67def crosstalkSourceLookup(datasetType, registry, quantumDataId, collections):
68 """Lookup function to identify crosstalkSource entries.
69
70 This should return an empty list under most circumstances. Only
71 when inter-chip crosstalk has been identified should this be
72 populated.
73
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.
85
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]
102
103
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 )
120
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 )
257
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 )
282
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 )
301
302 def __init__(self, *, config=None):
303 super().__init__(config=config)
304
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")
345
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")
366
367
368class IsrTaskConfig(pipeBase.PipelineTaskConfig,
369 pipelineConnections=IsrTaskConnections):
370 """Configuration parameters for IsrTask.
371
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 )
380
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 )
410
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 )
422
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 )
429
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 )
453
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 )
479
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 )
495
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 )
506
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 )
517
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 )
529
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 )
546
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 )
557
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 )
602
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 )
618
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 )
635
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 )
685
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 )
697
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 )
708
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 )
740
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 )
758
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 )
774
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 )
785
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 )
792
793 # Camera-specific masking configuration.
794 doCameraSpecificMasking = pexConfig.Field(
795 dtype=bool,
796 doc="Mask camera-specific bad regions?",
797 default=False,
798 )
799 masking = pexConfig.ConfigurableField(
800 target=MaskingTask,
801 doc="Masking task."
802 )
803
804 # Interpolation options.
805 doInterpolate = pexConfig.Field(
806 dtype=bool,
807 doc="Interpolate masked pixels?",
808 default=True,
809 )
810 doSaturationInterpolation = pexConfig.Field(
811 dtype=bool,
812 doc="Perform interpolation over pixels masked as saturated?"
813 " NB: This is independent of doSaturation; if that is False this plane"
814 " will likely be blank, resulting in a no-op here.",
815 default=True,
816 )
817 doNanInterpolation = pexConfig.Field(
818 dtype=bool,
819 doc="Perform interpolation over pixels masked as NaN?"
820 " NB: This is independent of doNanMasking; if that is False this plane"
821 " will likely be blank, resulting in a no-op here.",
822 default=True,
823 )
824 doNanInterpAfterFlat = pexConfig.Field(
825 dtype=bool,
826 doc=("If True, ensure we interpolate NaNs after flat-fielding, even if we "
827 "also have to interpolate them before flat-fielding."),
828 default=False,
829 )
830 maskListToInterpolate = pexConfig.ListField(
831 dtype=str,
832 doc="List of mask planes that should be interpolated.",
833 default=['SAT', 'BAD'],
834 )
835 doSaveInterpPixels = pexConfig.Field(
836 dtype=bool,
837 doc="Save a copy of the pre-interpolated pixel values?",
838 default=False,
839 )
840 useLegacyInterp = pexConfig.Field(
841 dtype=bool,
842 doc="Use the legacy interpolation algorithm. If False use Gaussian Process.",
843 default=True,
844 )
845
846 # Default photometric calibration options.
847 fluxMag0T1 = pexConfig.DictField(
848 keytype=str,
849 itemtype=float,
850 doc="The approximate flux of a zero-magnitude object in a one-second exposure, per filter.",
851 default=dict((f, pow(10.0, 0.4*m)) for f, m in (("Unknown", 28.0),
852 ))
853 )
854 defaultFluxMag0T1 = pexConfig.Field(
855 dtype=float,
856 doc="Default value for fluxMag0T1 (for an unrecognized filter).",
857 default=pow(10.0, 0.4*28.0)
858 )
859
860 # Vignette correction configuration.
861 doVignette = pexConfig.Field(
862 dtype=bool,
863 doc=("Compute and attach the validPolygon defining the unvignetted region to the exposure "
864 "according to vignetting parameters?"),
865 default=False,
866 )
867 doMaskVignettePolygon = pexConfig.Field(
868 dtype=bool,
869 doc=("Add a mask bit for pixels within the vignetted region. Ignored if doVignette "
870 "is False"),
871 default=True,
872 )
873 vignetteValue = pexConfig.Field(
874 dtype=float,
875 doc="Value to replace image array pixels with in the vignetted region? Ignored if None.",
876 optional=True,
877 default=None,
878 )
879 vignette = pexConfig.ConfigurableField(
880 target=VignetteTask,
881 doc="Vignetting task.",
882 )
883
884 # Transmission curve configuration.
885 doAttachTransmissionCurve = pexConfig.Field(
886 dtype=bool,
887 default=False,
888 doc="Construct and attach a wavelength-dependent throughput curve for this CCD image?"
889 )
890 doUseOpticsTransmission = pexConfig.Field(
891 dtype=bool,
892 default=True,
893 doc="Load and use transmission_optics (if doAttachTransmissionCurve is True)?"
894 )
895 doUseFilterTransmission = pexConfig.Field(
896 dtype=bool,
897 default=True,
898 doc="Load and use transmission_filter (if doAttachTransmissionCurve is True)?"
899 )
900 doUseSensorTransmission = pexConfig.Field(
901 dtype=bool,
902 default=True,
903 doc="Load and use transmission_sensor (if doAttachTransmissionCurve is True)?"
904 )
905 doUseAtmosphereTransmission = pexConfig.Field(
906 dtype=bool,
907 default=True,
908 doc="Load and use transmission_atmosphere (if doAttachTransmissionCurve is True)?"
909 )
910
911 # Illumination correction.
912 doIlluminationCorrection = pexConfig.Field(
913 dtype=bool,
914 default=False,
915 doc="Perform illumination correction?"
916 )
917 illuminationCorrectionDataProductName = pexConfig.Field(
918 dtype=str,
919 doc="Name of the illumination correction data product.",
920 default="illumcor",
921 )
922 illumScale = pexConfig.Field(
923 dtype=float,
924 doc="Scale factor for the illumination correction.",
925 default=1.0,
926 )
927 illumFilters = pexConfig.ListField(
928 dtype=str,
929 default=[],
930 doc="Only perform illumination correction for these filters."
931 )
932
933 # Calculate image quality statistics?
934 doStandardStatistics = pexConfig.Field(
935 dtype=bool,
936 doc="Should standard image quality statistics be calculated?",
937 default=True,
938 )
939 # Calculate additional statistics?
940 doCalculateStatistics = pexConfig.Field(
941 dtype=bool,
942 doc="Should additional ISR statistics be calculated?",
943 default=False,
944 )
945 isrStats = pexConfig.ConfigurableField(
946 target=IsrStatisticsTask,
947 doc="Task to calculate additional statistics.",
948 )
949
950 # Make binned images?
951 doBinnedExposures = pexConfig.Field(
952 dtype=bool,
953 doc="Should binned exposures be calculated?",
954 default=False,
955 )
956 binning = pexConfig.ConfigurableField(
957 target=BinImageDataTask,
958 doc="Task to bin the exposure.",
959 )
960 binFactor1 = pexConfig.Field(
961 dtype=int,
962 doc="Binning factor for first binned exposure. This is intended for a finely binned output.",
963 default=8,
964 check=lambda x: x > 1,
965 )
966 binFactor2 = pexConfig.Field(
967 dtype=int,
968 doc="Binning factor for second binned exposure. This is intended for a coarsely binned output.",
969 default=64,
970 check=lambda x: x > 1,
971 )
972
973 # Write the outputs to disk. If ISR is run as a subtask, this may not
974 # be needed.
975 doWrite = pexConfig.Field(
976 dtype=bool,
977 doc="Persist postISRCCD?",
978 default=True,
979 )
980
981 def validate(self):
982 super().validate()
983 if self.doFlat and self.doApplyGains:
984 raise ValueError("You may not specify both doFlat and doApplyGains")
986 raise ValueError("You may not specify both doBiasBeforeOverscan and doTrimToMatchCalib")
991 if self.doNanInterpolation and "UNMASKEDNAN" not in self.maskListToInterpolate:
992 self.maskListToInterpolate.append("UNMASKEDNAN")
993 if self.ampOffset.doApplyAmpOffset and not self.doAmpOffset:
994 raise ValueError("ampOffset.doApplyAmpOffset requires doAmpOffset to be True.")
995
996
997class IsrTask(pipeBase.PipelineTask):
998 """Apply common instrument signature correction algorithms to a raw frame.
999
1000 The process for correcting imaging data is very similar from
1001 camera to camera. This task provides a vanilla implementation of
1002 doing these corrections, including the ability to turn certain
1003 corrections off if they are not needed. The inputs to the primary
1004 method, `run()`, are a raw exposure to be corrected and the
1005 calibration data products. The raw input is a single chip sized
1006 mosaic of all amps including overscans and other non-science
1007 pixels.
1008
1009 The __init__ method sets up the subtasks for ISR processing, using
1010 the defaults from `lsst.ip.isr`.
1011
1012 Parameters
1013 ----------
1014 args : `list`
1015 Positional arguments passed to the Task constructor.
1016 None used at this time.
1017 kwargs : `dict`, optional
1018 Keyword arguments passed on to the Task constructor.
1019 None used at this time.
1020 """
1021 ConfigClass = IsrTaskConfig
1022 _DefaultName = "isr"
1023
1024 def __init__(self, **kwargs):
1025 super().__init__(**kwargs)
1026 self.makeSubtask("assembleCcd")
1027 self.makeSubtask("crosstalk")
1028 self.makeSubtask("strayLight")
1029 self.makeSubtask("fringe")
1030 self.makeSubtask("masking")
1031 self.makeSubtask("overscan")
1032 self.makeSubtask("vignette")
1033 self.makeSubtask("ampOffset")
1034 self.makeSubtask("deferredChargeCorrection")
1035 self.makeSubtask("isrStats")
1036 self.makeSubtask("binning")
1037
1038 def runQuantum(self, butlerQC, inputRefs, outputRefs):
1039 inputs = butlerQC.get(inputRefs)
1040
1041 try:
1042 inputs['detectorNum'] = inputRefs.ccdExposure.dataId['detector']
1043 except Exception as e:
1044 raise ValueError("Failure to find valid detectorNum value for Dataset %s: %s." %
1045 (inputRefs, e))
1046
1047 detector = inputs['ccdExposure'].getDetector()
1048
1049 # This is use for header provenance.
1050 additionalInputDates = {}
1051
1052 if self.config.doCrosstalk is True:
1053 # Crosstalk sources need to be defined by the pipeline
1054 # yaml if they exist.
1055 if 'crosstalk' in inputs and inputs['crosstalk'] is not None:
1056 if not isinstance(inputs['crosstalk'], CrosstalkCalib):
1057 inputs['crosstalk'] = CrosstalkCalib.fromTable(inputs['crosstalk'])
1058 else:
1059 coeffVector = (self.config.crosstalk.crosstalkValues
1060 if self.config.crosstalk.useConfigCoefficients else None)
1061 crosstalkCalib = CrosstalkCalib().fromDetector(detector, coeffVector=coeffVector)
1062 inputs['crosstalk'] = crosstalkCalib
1063 if inputs['crosstalk'].interChip and len(inputs['crosstalk'].interChip) > 0:
1064 if 'crosstalkSources' not in inputs:
1065 self.log.warning("No crosstalkSources found for chip with interChip terms!")
1066
1067 if self.doLinearize(detector) is True:
1068 if 'linearizer' in inputs:
1069 if isinstance(inputs['linearizer'], dict):
1070 linearizer = linearize.Linearizer(detector=detector, log=self.log)
1071 linearizer.fromYaml(inputs['linearizer'])
1072 self.log.warning("Dictionary linearizers will be deprecated in DM-28741.")
1073 elif isinstance(inputs['linearizer'], numpy.ndarray):
1074 linearizer = linearize.Linearizer(table=inputs.get('linearizer', None),
1075 detector=detector,
1076 log=self.log)
1077 self.log.warning("Bare lookup table linearizers will be deprecated in DM-28741.")
1078 else:
1079 linearizer = inputs['linearizer']
1080 self.log.info("Loading linearizer from the Butler.")
1081 linearizer.log = self.log
1082 inputs['linearizer'] = linearizer
1083 else:
1084 inputs['linearizer'] = linearize.Linearizer(detector=detector, log=self.log)
1085 self.log.info("Constructing linearizer from cameraGeom information.")
1086
1087 if self.config.doDefect is True:
1088 if "defects" in inputs and inputs['defects'] is not None:
1089 # defects is loaded as a BaseCatalog with columns
1090 # x0, y0, width, height. Masking expects a list of defects
1091 # defined by their bounding box
1092 if not isinstance(inputs["defects"], Defects):
1093 inputs["defects"] = Defects.fromTable(inputs["defects"])
1094
1095 # Load the correct style of brighter-fatter kernel, and repack
1096 # the information as a numpy array.
1097 brighterFatterSource = None
1098 if self.config.doBrighterFatter:
1099 brighterFatterKernel = inputs.pop('newBFKernel', None)
1100 if brighterFatterKernel is None:
1101 # This type of kernel must be in (y, x) index
1102 # ordering, as it used directly as the .array
1103 # component of the afwImage kernel.
1104 brighterFatterKernel = inputs.get('bfKernel', None)
1105 brighterFatterSource = 'bfKernel'
1106 additionalInputDates[brighterFatterSource] = self.extractCalibDate(brighterFatterKernel)
1107
1108 if brighterFatterKernel is None:
1109 # This was requested by the config, but none were found.
1110 raise RuntimeError("No brighter-fatter kernel was supplied.")
1111 elif not isinstance(brighterFatterKernel, numpy.ndarray):
1112 # This is a ISR calib kernel. These kernels are
1113 # generated in (x, y) index ordering, and need to be
1114 # transposed to be used directly as the .array
1115 # component of the afwImage kernel. This is done
1116 # explicitly below when setting the ``bfKernel``
1117 # input.
1118 brighterFatterSource = 'newBFKernel'
1119 additionalInputDates[brighterFatterSource] = self.extractCalibDate(brighterFatterKernel)
1120
1121 detName = detector.getName()
1122 level = brighterFatterKernel.level
1123
1124 # This is expected to be a dictionary of amp-wise gains.
1125 inputs['bfGains'] = brighterFatterKernel.gain
1126 if self.config.brighterFatterLevel == 'DETECTOR':
1127 kernel = None
1128 if level == 'DETECTOR':
1129 if detName in brighterFatterKernel.detKernels:
1130 kernel = brighterFatterKernel.detKernels[detName]
1131 else:
1132 raise RuntimeError("Failed to extract kernel from new-style BF kernel.")
1133 elif level == 'AMP':
1134 self.log.warning("Making DETECTOR level kernel from AMP based brighter "
1135 "fatter kernels.")
1136 brighterFatterKernel.makeDetectorKernelFromAmpwiseKernels(detName)
1137 kernel = brighterFatterKernel.detKernels[detName]
1138 if kernel is None:
1139 raise RuntimeError("Could not identify brighter-fatter kernel!")
1140 # Do the one single transpose here so the kernel
1141 # can be directly loaded into the afwImage .array
1142 # component.
1143 inputs['bfKernel'] = numpy.transpose(kernel)
1144 elif self.config.brighterFatterLevel == 'AMP':
1145 raise NotImplementedError("Per-amplifier brighter-fatter correction not implemented")
1146
1147 if self.config.doFringe is True and self.fringe.checkFilter(inputs['ccdExposure']):
1148 expId = inputs['ccdExposure'].info.id
1149 inputs['fringes'] = self.fringe.loadFringes(inputs['fringes'],
1150 expId=expId,
1151 assembler=self.assembleCcd
1152 if self.config.doAssembleIsrExposures else None)
1153 else:
1154 inputs['fringes'] = pipeBase.Struct(fringes=None)
1155
1156 if self.config.doStrayLight is True and self.strayLight.checkFilter(inputs['ccdExposure']):
1157 if 'strayLightData' not in inputs:
1158 inputs['strayLightData'] = None
1159
1160 if self.config.doHeaderProvenance:
1161 # Add calibration provenanace info to header.
1162 exposureMetadata = inputs['ccdExposure'].getMetadata()
1163
1164 # These inputs change name during this step. These should
1165 # have matching entries in the additionalInputDates dict.
1166 additionalInputs = []
1167 if self.config.doBrighterFatter:
1168 additionalInputs.append(brighterFatterSource)
1169
1170 for inputName in sorted(list(inputs.keys()) + additionalInputs):
1171 reference = getattr(inputRefs, inputName, None)
1172 if reference is not None and hasattr(reference, "run"):
1173 runKey = f"LSST CALIB RUN {inputName.upper()}"
1174 runValue = reference.run
1175 idKey = f"LSST CALIB UUID {inputName.upper()}"
1176 idValue = str(reference.id)
1177 dateKey = f"LSST CALIB DATE {inputName.upper()}"
1178
1179 if inputName in additionalInputDates:
1180 dateValue = additionalInputDates[inputName]
1181 else:
1182 dateValue = self.extractCalibDate(inputs[inputName])
1183
1184 exposureMetadata[runKey] = runValue
1185 exposureMetadata[idKey] = idValue
1186 exposureMetadata[dateKey] = dateValue
1187
1188 outputs = self.run(**inputs)
1189 butlerQC.put(outputs, outputRefs)
1190
1191 @timeMethod
1192 def run(self, ccdExposure, *, camera=None, bias=None, linearizer=None,
1193 crosstalk=None, crosstalkSources=None,
1194 dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None,
1195 fringes=pipeBase.Struct(fringes=None), opticsTransmission=None, filterTransmission=None,
1196 sensorTransmission=None, atmosphereTransmission=None,
1197 detectorNum=None, strayLightData=None, illumMaskedImage=None,
1198 deferredChargeCalib=None,
1199 ):
1200 """Perform instrument signature removal on an exposure.
1201
1202 Steps included in the ISR processing, in order performed, are:
1203
1204 - saturation and suspect pixel masking
1205 - overscan subtraction
1206 - CCD assembly of individual amplifiers
1207 - bias subtraction
1208 - variance image construction
1209 - linearization of non-linear response
1210 - crosstalk masking
1211 - brighter-fatter correction
1212 - dark subtraction
1213 - fringe correction
1214 - stray light subtraction
1215 - flat correction
1216 - masking of known defects and camera specific features
1217 - vignette calculation
1218 - appending transmission curve and distortion model
1219
1220 Parameters
1221 ----------
1222 ccdExposure : `lsst.afw.image.Exposure`
1223 The raw exposure that is to be run through ISR. The
1224 exposure is modified by this method.
1225 camera : `lsst.afw.cameraGeom.Camera`, optional
1226 The camera geometry for this exposure. Required if
1227 one or more of ``ccdExposure``, ``bias``, ``dark``, or
1228 ``flat`` does not have an associated detector.
1229 bias : `lsst.afw.image.Exposure`, optional
1230 Bias calibration frame.
1231 linearizer : `lsst.ip.isr.linearize.LinearizeBase`, optional
1232 Functor for linearization.
1233 crosstalk : `lsst.ip.isr.crosstalk.CrosstalkCalib`, optional
1234 Calibration for crosstalk.
1235 crosstalkSources : `list`, optional
1236 List of possible crosstalk sources.
1237 dark : `lsst.afw.image.Exposure`, optional
1238 Dark calibration frame.
1239 flat : `lsst.afw.image.Exposure`, optional
1240 Flat calibration frame.
1241 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
1242 Photon transfer curve dataset, with, e.g., gains
1243 and read noise.
1244 bfKernel : `numpy.ndarray`, optional
1245 Brighter-fatter kernel.
1246 bfGains : `dict` of `float`, optional
1247 Gains used to override the detector's nominal gains for the
1248 brighter-fatter correction. A dict keyed by amplifier name for
1249 the detector in question.
1250 defects : `lsst.ip.isr.Defects`, optional
1251 List of defects.
1252 fringes : `lsst.pipe.base.Struct`, optional
1253 Struct containing the fringe correction data, with
1254 elements:
1255
1256 ``fringes``
1257 fringe calibration frame (`lsst.afw.image.Exposure`)
1258 ``seed``
1259 random seed derived from the ``ccdExposureId`` for random
1260 number generator (`numpy.uint32`)
1261 opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional
1262 A ``TransmissionCurve`` that represents the throughput of the,
1263 optics, to be evaluated in focal-plane coordinates.
1264 filterTransmission : `lsst.afw.image.TransmissionCurve`
1265 A ``TransmissionCurve`` that represents the throughput of the
1266 filter itself, to be evaluated in focal-plane coordinates.
1267 sensorTransmission : `lsst.afw.image.TransmissionCurve`
1268 A ``TransmissionCurve`` that represents the throughput of the
1269 sensor itself, to be evaluated in post-assembly trimmed detector
1270 coordinates.
1271 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1272 A ``TransmissionCurve`` that represents the throughput of the
1273 atmosphere, assumed to be spatially constant.
1274 detectorNum : `int`, optional
1275 The integer number for the detector to process.
1276 strayLightData : `object`, optional
1277 Opaque object containing calibration information for stray-light
1278 correction. If `None`, no correction will be performed.
1279 illumMaskedImage : `lsst.afw.image.MaskedImage`, optional
1280 Illumination correction image.
1281
1282 Returns
1283 -------
1284 result : `lsst.pipe.base.Struct`
1285 Result struct with component:
1286
1287 ``exposure``
1288 The fully ISR corrected exposure.
1289 (`lsst.afw.image.Exposure`)
1290 ``outputExposure``
1291 An alias for ``exposure``. (`lsst.afw.image.Exposure`)
1292 ``ossThumb``
1293 Thumbnail image of the exposure after overscan subtraction.
1294 (`numpy.ndarray`)
1295 ``flattenedThumb``
1296 Thumbnail image of the exposure after flat-field correction.
1297 (`numpy.ndarray`)
1298 ``outputStatistics``
1299 Values of the additional statistics calculated.
1300
1301 Raises
1302 ------
1303 RuntimeError
1304 Raised if a configuration option is set to `True`, but the
1305 required calibration data has not been specified.
1306
1307 Notes
1308 -----
1309 The current processed exposure can be viewed by setting the
1310 appropriate `lsstDebug` entries in the ``debug.display``
1311 dictionary. The names of these entries correspond to some of
1312 the `IsrTaskConfig` Boolean options, with the value denoting the
1313 frame to use. The exposure is shown inside the matching
1314 option check and after the processing of that step has
1315 finished. The steps with debug points are:
1316
1317 * doAssembleCcd
1318 * doBias
1319 * doCrosstalk
1320 * doBrighterFatter
1321 * doDark
1322 * doFringe
1323 * doStrayLight
1324 * doFlat
1325
1326 In addition, setting the ``postISRCCD`` entry displays the
1327 exposure after all ISR processing has finished.
1328 """
1329
1330 ccdExposure = self.ensureExposure(ccdExposure, camera, detectorNum)
1331 bias = self.ensureExposure(bias, camera, detectorNum)
1332 dark = self.ensureExposure(dark, camera, detectorNum)
1333 flat = self.ensureExposure(flat, camera, detectorNum)
1334
1335 ccd = ccdExposure.getDetector()
1336 filterLabel = ccdExposure.getFilter()
1337 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
1338
1339 if not ccd:
1340 assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd."
1341 ccd = [FakeAmp(ccdExposure, self.config)]
1342
1343 # Validate Input
1344 if self.config.doBias and bias is None:
1345 raise RuntimeError("Must supply a bias exposure if config.doBias=True.")
1346 if self.doLinearize(ccd) and linearizer is None:
1347 raise RuntimeError("Must supply a linearizer if config.doLinearize=True for this detector.")
1348 if self.config.doBrighterFatter and bfKernel is None:
1349 raise RuntimeError("Must supply a kernel if config.doBrighterFatter=True.")
1350 if self.config.doDark and dark is None:
1351 raise RuntimeError("Must supply a dark exposure if config.doDark=True.")
1352 if self.config.doFlat and flat is None:
1353 raise RuntimeError("Must supply a flat exposure if config.doFlat=True.")
1354 if self.config.doDefect and defects is None:
1355 raise RuntimeError("Must supply defects if config.doDefect=True.")
1356 if (self.config.doFringe and physicalFilter in self.fringe.config.filters
1357 and fringes.fringes is None):
1358 # The `fringes` object needs to be a pipeBase.Struct, as
1359 # we use it as a `dict` for the parameters of
1360 # `FringeTask.run()`. The `fringes.fringes` `list` may
1361 # not be `None` if `doFringe=True`. Otherwise, raise.
1362 raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct.")
1363 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters
1364 and illumMaskedImage is None):
1365 raise RuntimeError("Must supply an illumcor if config.doIlluminationCorrection=True.")
1366 if (self.config.doDeferredCharge and deferredChargeCalib is None):
1367 raise RuntimeError("Must supply a deferred charge calibration if config.doDeferredCharge=True.")
1368 if (self.config.usePtcGains and ptc is None):
1369 raise RuntimeError("No ptcDataset provided to use PTC gains.")
1370 if (self.config.usePtcReadNoise and ptc is None):
1371 raise RuntimeError("No ptcDataset provided to use PTC read noise.")
1372
1373 # Validate that the inputs match the exposure configuration.
1374 exposureMetadata = ccdExposure.getMetadata()
1375 doRaise = self.config.doRaiseOnCalibMismatch
1376 keywords = self.config.cameraKeywordsToCompare
1377 if self.config.doBias:
1378 compareCameraKeywords(doRaise, keywords, exposureMetadata, bias, "bias", log=self.log)
1379 self.compareUnits(bias.metadata, "bias")
1380 if self.config.doBrighterFatter:
1381 compareCameraKeywords(doRaise, keywords, exposureMetadata, bfKernel, "bf", log=self.log)
1382 if self.config.doCrosstalk:
1383 compareCameraKeywords(doRaise, keywords, exposureMetadata, crosstalk, "crosstalk", log=self.log)
1384 if self.config.doDark:
1385 compareCameraKeywords(doRaise, keywords, exposureMetadata, dark, "dark", log=self.log)
1386 self.compareUnits(dark.metadata, "dark")
1387 if self.config.doDefect:
1388 compareCameraKeywords(doRaise, keywords, exposureMetadata, defects, "defects", log=self.log)
1389 if self.config.doDeferredCharge:
1391 doRaise,
1392 keywords,
1393 exposureMetadata,
1394 deferredChargeCalib,
1395 "CTI",
1396 log=self.log,
1397 )
1398 if self.config.doFlat:
1399 compareCameraKeywords(doRaise, keywords, exposureMetadata, flat, "flat", log=self.log)
1400 self.compareUnits(flat.metadata, "flat")
1401 if (self.config.doFringe and physicalFilter in self.fringe.config.filters):
1403 doRaise,
1404 keywords,
1405 exposureMetadata,
1406 fringes.fringes,
1407 "fringe",
1408 log=self.log,
1409 )
1410 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters):
1412 doRaise,
1413 keywords,
1414 exposureMetadata,
1415 illumMaskedImage,
1416 "illumination",
1417 log=self.log,
1418 )
1419 if self.doLinearize(ccd):
1420 compareCameraKeywords(doRaise, keywords, exposureMetadata, linearizer, "linearizer", log=self.log)
1421 if self.config.usePtcGains or self.config.usePtcReadNoise:
1422 compareCameraKeywords(doRaise, keywords, exposureMetadata, ptc, "PTC", log=self.log)
1423 if self.config.doStrayLight:
1425 doRaise,
1426 keywords,
1427 exposureMetadata,
1428 strayLightData,
1429 "straylight",
1430 log=self.log,
1431 )
1432
1433 # Start in adu. Update units to electrons when gain is applied:
1434 # updateVariance, applyGains
1435 # Check if needed during/after BFE correction, CTI correction.
1436 exposureMetadata["LSST ISR UNITS"] = "adu"
1437 exposureMetadata["LSST ISR CROSSTALK APPLIED"] = False
1438 exposureMetadata["LSST ISR LINEARIZER APPLIED"] = False
1439 exposureMetadata["LSST ISR CTI APPLIED"] = False
1440 exposureMetadata["LSST ISR BIAS APPLIED"] = False
1441 exposureMetadata["LSST ISR DARK APPLIED"] = False
1442 exposureMetadata["LSST ISR BF APPLIED"] = False
1443 exposureMetadata["LSST ISR FLAT APPLIED"] = False
1444 exposureMetadata["LSST ISR DEFECTS APPLIED"] = False
1445
1446 # Begin ISR processing.
1447 if self.config.doConvertIntToFloat:
1448 self.log.info("Converting exposure to floating point values.")
1449 ccdExposure = self.convertIntToFloat(ccdExposure)
1450
1451 if self.config.doBias and self.config.doBiasBeforeOverscan:
1452 self.log.info("Applying bias correction.")
1453 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1454 trimToFit=self.config.doTrimToMatchCalib)
1455 self.debugView(ccdExposure, "doBias")
1456 ccdExposure.metadata["LSST ISR BIAS APPLIED"] = True
1457
1458 # Amplifier level processing.
1459 overscans = []
1460
1461 if self.config.doOverscan and self.config.overscan.doParallelOverscan:
1462 # This will attempt to mask bleed pixels across all amplifiers.
1463 self.overscan.maskParallelOverscan(ccdExposure, ccd)
1464
1465 for amp in ccd:
1466 # if ccdExposure is one amp,
1467 # check for coverage to prevent performing ops multiple times
1468 if ccdExposure.getBBox().contains(amp.getBBox()):
1469 # Check for fully masked bad amplifiers,
1470 # and generate masks for SUSPECT and SATURATED values.
1471 badAmp = self.maskAmplifier(ccdExposure, amp, defects)
1472
1473 if self.config.doOverscan and not badAmp:
1474 # Overscan correction on amp-by-amp basis.
1475 overscanResults = self.overscanCorrection(ccdExposure, amp)
1476 self.log.debug("Corrected overscan for amplifier %s.", amp.getName())
1477 if overscanResults is not None and \
1478 self.config.qa is not None and self.config.qa.saveStats is True:
1479 if isinstance(overscanResults.overscanMean, float):
1480 # Only serial overscan was run
1481 mean = overscanResults.overscanMean
1482 median = overscanResults.overscanMedian
1483 sigma = overscanResults.overscanSigma
1484 residMean = overscanResults.residualMean
1485 residMedian = overscanResults.residualMedian
1486 residSigma = overscanResults.residualSigma
1487 else:
1488 # Both serial and parallel overscan were
1489 # run. Only report serial here.
1490 mean = overscanResults.overscanMean[0]
1491 median = overscanResults.overscanMedian[0]
1492 sigma = overscanResults.overscanSigma[0]
1493 residMean = overscanResults.residualMean[0]
1494 residMedian = overscanResults.residualMedian[0]
1495 residSigma = overscanResults.residualSigma[0]
1496
1497 self.metadata[f"FIT MEDIAN {amp.getName()}"] = median
1498 self.metadata[f"FIT MEAN {amp.getName()}"] = mean
1499 self.metadata[f"FIT STDEV {amp.getName()}"] = sigma
1500 self.log.debug(" Overscan stats for amplifer %s: %f +/- %f",
1501 amp.getName(), mean, sigma)
1502
1503 self.metadata[f"RESIDUAL MEDIAN {amp.getName()}"] = residMedian
1504 self.metadata[f"RESIDUAL MEAN {amp.getName()}"] = residMean
1505 self.metadata[f"RESIDUAL STDEV {amp.getName()}"] = residSigma
1506 self.log.debug(" Overscan stats for amplifer %s after correction: %f +/- %f",
1507 amp.getName(), residMean, residSigma)
1508
1509 ccdExposure.getMetadata().set('OVERSCAN', "Overscan corrected")
1510 else:
1511 if badAmp:
1512 self.log.warning("Amplifier %s is bad.", amp.getName())
1513 overscanResults = None
1514
1515 overscans.append(overscanResults if overscanResults is not None else None)
1516 else:
1517 self.log.info("Skipped OSCAN for %s.", amp.getName())
1518
1519 # Define an effective PTC that will contain the gain and readout
1520 # noise to be used throughout the ISR task.
1521 ptc = self.defineEffectivePtc(ptc, ccd, bfGains, overscans, exposureMetadata)
1522
1523 if self.config.doDeferredCharge:
1524 self.log.info("Applying deferred charge/CTI correction.")
1525 self.deferredChargeCorrection.run(
1526 ccdExposure,
1527 deferredChargeCalib,
1528 gains=ptc.gain,
1529 )
1530 self.debugView(ccdExposure, "doDeferredCharge")
1531 ccdExposure.metadata["LSST ISR CTI APPLIED"] = True
1532
1533 if self.config.doCrosstalk and self.config.doCrosstalkBeforeAssemble:
1534 self.log.info("Applying crosstalk correction.")
1535 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1536 crosstalkSources=crosstalkSources, camera=camera)
1537 self.debugView(ccdExposure, "doCrosstalk")
1538 ccdExposure.metadata["LSST ISR CROSSTALK APPLIED"] = True
1539
1540 if self.config.doAssembleCcd:
1541 self.log.info("Assembling CCD from amplifiers.")
1542 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1543
1544 if self.config.expectWcs and not ccdExposure.getWcs():
1545 self.log.warning("No WCS found in input exposure.")
1546 self.debugView(ccdExposure, "doAssembleCcd")
1547
1548 ossThumb = None
1549 if self.config.qa.doThumbnailOss:
1550 ossThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1551
1552 if self.config.doBias and not self.config.doBiasBeforeOverscan:
1553 self.log.info("Applying bias correction.")
1554 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1555 trimToFit=self.config.doTrimToMatchCalib)
1556 self.debugView(ccdExposure, "doBias")
1557 ccdExposure.metadata["LSST ISR BIAS APPLIED"] = True
1558
1559 if self.config.doVariance:
1560 for amp in ccd:
1561 if ccdExposure.getBBox().contains(amp.getBBox()):
1562 self.log.debug("Constructing variance map for amplifer %s.", amp.getName())
1563 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1564 self.updateVariance(ampExposure, amp, ptc)
1565
1566 if self.config.qa is not None and self.config.qa.saveStats is True:
1567 qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
1568 afwMath.MEDIAN | afwMath.STDEVCLIP)
1569 self.metadata[f"ISR VARIANCE {amp.getName()} MEDIAN"] = \
1570 qaStats.getValue(afwMath.MEDIAN)
1571 self.metadata[f"ISR VARIANCE {amp.getName()} STDEV"] = \
1572 qaStats.getValue(afwMath.STDEVCLIP)
1573 self.log.debug(" Variance stats for amplifer %s: %f +/- %f.",
1574 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1575 qaStats.getValue(afwMath.STDEVCLIP))
1576 if self.config.maskNegativeVariance:
1577 self.maskNegativeVariance(ccdExposure)
1578
1579 if self.doLinearize(ccd):
1580 self.log.info("Applying linearizer.")
1581 linearizer.applyLinearity(image=ccdExposure.getMaskedImage().getImage(),
1582 detector=ccd, log=self.log)
1583 ccdExposure.metadata["LSST ISR LINEARIZER APPLIED"] = True
1584
1585 if self.config.doCrosstalk and not self.config.doCrosstalkBeforeAssemble:
1586 self.log.info("Applying crosstalk correction.")
1587 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1588 crosstalkSources=crosstalkSources)
1589 self.debugView(ccdExposure, "doCrosstalk")
1590 ccdExposure.metadata["LSST ISR CROSSTALK APPLIED"] = True
1591
1592 # Masking block. Optionally mask known defects,NaN/inf pixels,
1593 # widen trails, and do anything else the camera needs. Saturated and
1594 # suspect pixels have already been masked.
1595 if self.config.doDefect:
1596 self.log.info("Masking defects.")
1597 self.maskDefect(ccdExposure, defects)
1598 ccdExposure.metadata["LSST ISR DEFECTS APPLIED"] = True
1599
1600 if self.config.numEdgeSuspect > 0:
1601 self.log.info("Masking edges as SUSPECT.")
1602 self.maskEdges(ccdExposure, numEdgePixels=self.config.numEdgeSuspect,
1603 maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
1604
1605 if self.config.doNanMasking:
1606 self.log.info("Masking non-finite (NAN, inf) value pixels.")
1607 self.maskNan(ccdExposure)
1608
1609 if self.config.doWidenSaturationTrails:
1610 self.log.info("Widening saturation trails.")
1611 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1612
1613 if self.config.doCameraSpecificMasking:
1614 self.log.info("Masking regions for camera specific reasons.")
1615 self.masking.run(ccdExposure)
1616
1617 if self.config.doBrighterFatter:
1618 # We need to apply flats and darks before we can interpolate, and
1619 # we need to interpolate before we do B-F, but we do B-F without
1620 # the flats and darks applied so we can work in units of electrons
1621 # or holes. This context manager applies and then removes the darks
1622 # and flats.
1623 #
1624 # We also do not want to interpolate values here, so operate on
1625 # temporary images so we can apply only the BF-correction and roll
1626 # back the interpolation.
1627 interpExp = ccdExposure.clone()
1628 with self.flatContext(interpExp, flat, dark):
1629 isrFunctions.interpolateFromMask(
1630 maskedImage=interpExp.getMaskedImage(),
1631 fwhm=self.config.fwhm,
1632 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1633 maskNameList=list(self.config.brighterFatterMaskListToInterpolate),
1634 useLegacyInterp=self.config.useLegacyInterp,
1635 )
1636 bfExp = interpExp.clone()
1637
1638 self.log.info("Applying brighter-fatter correction using kernel type %s / gains %s.",
1639 type(bfKernel), type(bfGains))
1640 if self.config.doFluxConservingBrighterFatterCorrection:
1642 bfExp,
1643 bfKernel,
1644 self.config.brighterFatterMaxIter,
1645 self.config.brighterFatterThreshold,
1646 self.config.brighterFatterApplyGain,
1647 bfGains
1648 )
1649 else:
1650 bfResults = brighterFatterCorrection(
1651 bfExp,
1652 bfKernel,
1653 self.config.brighterFatterMaxIter,
1654 self.config.brighterFatterThreshold,
1655 self.config.brighterFatterApplyGain,
1656 bfGains
1657 )
1658 bfCorrIters = bfResults[1]
1659 self.metadata["LSST ISR BF ITERS"] = bfCorrIters
1660 if bfCorrIters == self.config.brighterFatterMaxIter - 1:
1661 self.log.warning("Brighter-fatter correction did not converge, final difference %f.",
1662 bfResults[0])
1663 else:
1664 self.log.info("Finished brighter-fatter correction in %d iterations.",
1665 bfResults[1])
1666 image = ccdExposure.getMaskedImage().getImage()
1667 bfCorr = bfExp.getMaskedImage().getImage()
1668 bfCorr -= interpExp.getMaskedImage().getImage()
1669 image += bfCorr
1670
1671 # Applying the brighter-fatter correction applies a
1672 # convolution to the science image. At the edges this
1673 # convolution may not have sufficient valid pixels to
1674 # produce a valid correction. Mark pixels within the size
1675 # of the brighter-fatter kernel as EDGE to warn of this
1676 # fact.
1677 self.log.info("Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1678 self.maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1679 maskPlane="EDGE")
1680
1681 if self.config.brighterFatterMaskGrowSize > 0:
1682 self.log.info("Growing masks to account for brighter-fatter kernel convolution.")
1683 for maskPlane in self.config.brighterFatterMaskListToInterpolate:
1684 isrFunctions.growMasks(ccdExposure.getMask(),
1685 radius=self.config.brighterFatterMaskGrowSize,
1686 maskNameList=maskPlane,
1687 maskValue=maskPlane)
1688
1689 self.debugView(ccdExposure, "doBrighterFatter")
1690 ccdExposure.metadata["LSST ISR BF APPLIED"] = True
1691
1692 if self.config.doDark:
1693 self.log.info("Applying dark correction.")
1694 self.darkCorrection(ccdExposure, dark)
1695 self.debugView(ccdExposure, "doDark")
1696 ccdExposure.metadata["LSST ISR DARK APPLIED"] = True
1697
1698 if self.config.doFringe and not self.config.fringeAfterFlat:
1699 self.log.info("Applying fringe correction before flat.")
1700 self.fringe.run(ccdExposure, **fringes.getDict())
1701 self.debugView(ccdExposure, "doFringe")
1702
1703 if self.config.doStrayLight and self.strayLight.check(ccdExposure):
1704 self.log.info("Checking strayLight correction.")
1705 self.strayLight.run(ccdExposure, strayLightData)
1706 self.debugView(ccdExposure, "doStrayLight")
1707
1708 if self.config.doFlat:
1709 self.log.info("Applying flat correction.")
1710 self.flatCorrection(ccdExposure, flat)
1711 self.debugView(ccdExposure, "doFlat")
1712 ccdExposure.metadata["LSST ISR FLAT APPLIED"] = True
1713 # TODO: DM-49159
1714 # Add metadata re: type of flat.
1715
1716 if self.config.doApplyGains:
1717 self.log.info("Applying gain correction instead of flat.")
1718 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains,
1719 ptcGains=ptc.gain)
1720 exposureMetadata["LSST ISR UNITS"] = "electron"
1721
1722 if self.config.doFringe and self.config.fringeAfterFlat:
1723 self.log.info("Applying fringe correction after flat.")
1724 self.fringe.run(ccdExposure, **fringes.getDict())
1725
1726 if self.config.doVignette:
1727 if self.config.doMaskVignettePolygon:
1728 self.log.info("Constructing, attaching, and masking vignette polygon.")
1729 else:
1730 self.log.info("Constructing and attaching vignette polygon.")
1731 self.vignettePolygon = self.vignette.run(
1732 exposure=ccdExposure, doUpdateMask=self.config.doMaskVignettePolygon,
1733 vignetteValue=self.config.vignetteValue, log=self.log)
1734
1735 if self.config.doAttachTransmissionCurve:
1736 self.log.info("Adding transmission curves.")
1737 isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission,
1738 filterTransmission=filterTransmission,
1739 sensorTransmission=sensorTransmission,
1740 atmosphereTransmission=atmosphereTransmission)
1741
1742 flattenedThumb = None
1743 if self.config.qa.doThumbnailFlattened:
1744 flattenedThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1745
1746 if self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters:
1747 self.log.info("Performing illumination correction.")
1748 isrFunctions.illuminationCorrection(ccdExposure.getMaskedImage(),
1749 illumMaskedImage, illumScale=self.config.illumScale,
1750 trimToFit=self.config.doTrimToMatchCalib)
1751
1752 preInterpExp = None
1753 if self.config.doSaveInterpPixels:
1754 preInterpExp = ccdExposure.clone()
1755
1756 # Reset and interpolate bad pixels.
1757 #
1758 # Large contiguous bad regions (which should have the BAD mask
1759 # bit set) should have their values set to the image median.
1760 # This group should include defects and bad amplifiers. As the
1761 # area covered by these defects are large, there's little
1762 # reason to expect that interpolation would provide a more
1763 # useful value.
1764 #
1765 # Smaller defects can be safely interpolated after the larger
1766 # regions have had their pixel values reset. This ensures
1767 # that the remaining defects adjacent to bad amplifiers (as an
1768 # example) do not attempt to interpolate extreme values.
1769 if self.config.doSetBadRegions:
1770 badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure)
1771 if badPixelCount > 0:
1772 self.log.info("Set %d BAD pixels to %f.", badPixelCount, badPixelValue)
1773
1774 if self.config.doInterpolate:
1775 self.log.info("Interpolating masked pixels.")
1776 isrFunctions.interpolateFromMask(
1777 maskedImage=ccdExposure.getMaskedImage(),
1778 fwhm=self.config.fwhm,
1779 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1780 maskNameList=list(self.config.maskListToInterpolate),
1781 useLegacyInterp=self.config.useLegacyInterp,
1782 )
1783
1784 self.roughZeroPoint(ccdExposure)
1785
1786 # Calculate amp offset corrections within the CCD.
1787 if self.config.doAmpOffset:
1788 if self.config.ampOffset.doApplyAmpOffset:
1789 self.log.info("Measuring and applying amp offset corrections.")
1790 else:
1791 self.log.info("Measuring amp offset corrections only, without applying them.")
1792 self.ampOffset.run(ccdExposure)
1793
1794 if self.config.doMeasureBackground:
1795 self.log.info("Measuring background level.")
1796 self.measureBackground(ccdExposure, self.config.qa)
1797
1798 if self.config.qa is not None and self.config.qa.saveStats is True:
1799 for amp in ccd:
1800 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1801 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1802 afwMath.MEDIAN | afwMath.STDEVCLIP)
1803 self.metadata[f"ISR BACKGROUND {amp.getName()} MEDIAN"] = qaStats.getValue(afwMath.MEDIAN)
1804 self.metadata[f"ISR BACKGROUND {amp.getName()} STDEV"] = \
1805 qaStats.getValue(afwMath.STDEVCLIP)
1806 self.log.debug(" Background stats for amplifer %s: %f +/- %f",
1807 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1808 qaStats.getValue(afwMath.STDEVCLIP))
1809
1810 # Calculate standard image quality statistics
1811 if self.config.doStandardStatistics:
1812 metadata = ccdExposure.getMetadata()
1813 for amp in ccd:
1814 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1815 ampName = amp.getName()
1816 metadata[f"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels(
1817 ampExposure.getMaskedImage(),
1818 [self.config.saturatedMaskName]
1819 )
1820 metadata[f"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels(
1821 ampExposure.getMaskedImage(),
1822 ["BAD"]
1823 )
1824 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1825 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP)
1826
1827 metadata[f"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN)
1828 metadata[f"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN)
1829 metadata[f"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP)
1830
1831 k1 = f"LSST ISR FINAL MEDIAN {ampName}"
1832 k2 = f"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
1833 if self.config.doOverscan and k1 in metadata and k2 in metadata:
1834 metadata[f"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2]
1835 else:
1836 metadata[f"LSST ISR LEVEL {ampName}"] = numpy.nan
1837
1838 # calculate additional statistics.
1839 outputStatistics = None
1840 if self.config.doCalculateStatistics:
1841 outputStatistics = self.isrStats.run(ccdExposure, serialOverscanResults=overscans,
1842 parallelOverscanResults=[None for _ in overscans],
1843 bias=bias, dark=dark, flat=flat, ptc=ptc,
1844 defects=defects,
1845 doLegacyCtiStatistics=True).results
1846
1847 # do any binning.
1848 outputBin1Exposure = None
1849 outputBin2Exposure = None
1850 if self.config.doBinnedExposures:
1851 self.log.info("Creating binned exposures.")
1852 outputBin1Exposure = self.binning.run(
1853 ccdExposure,
1854 binFactor=self.config.binFactor1,
1855 ).outputData
1856 outputBin2Exposure = self.binning.run(
1857 ccdExposure,
1858 binFactor=self.config.binFactor2,
1859 ).outputData
1860
1861 self.debugView(ccdExposure, "postISRCCD")
1862
1863 return pipeBase.Struct(
1864 exposure=ccdExposure,
1865 ossThumb=ossThumb,
1866 flattenedThumb=flattenedThumb,
1867
1868 outputBin1Exposure=outputBin1Exposure,
1869 outputBin2Exposure=outputBin2Exposure,
1870
1871 preInterpExposure=preInterpExp,
1872 outputExposure=ccdExposure,
1873 outputOssThumbnail=ossThumb,
1874 outputFlattenedThumbnail=flattenedThumb,
1875 outputStatistics=outputStatistics,
1876 )
1877
1878 def defineEffectivePtc(self, ptcDataset, detector, bfGains, overScans, metadata):
1879 """Define an effective Photon Transfer Curve dataset
1880 with nominal gains and noise.
1881
1882 Parameters
1883 ----------
1884 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`
1885 Input Photon Transfer Curve dataset.
1886 detector : `lsst.afw.cameraGeom.Detector`
1887 Detector object.
1888 bfGains : `dict`
1889 Gains from running the brighter-fatter code.
1890 A dict keyed by amplifier name for the detector
1891 in question.
1892 overScans : `list` [`lsst.pipe.base.Struct`]
1893 List of overscanResults structures
1894 metadata : `lsst.daf.base.PropertyList`
1895 Exposure metadata to update gain and read noise
1896 provenance.
1897
1898 Returns
1899 -------
1900 effectivePtc : `lsst.ip.isr.PhotonTransferCurveDataset`
1901 PTC dataset containing gains and readout noise
1902 values to be used throughout
1903 Instrument Signature Removal.
1904 """
1905 amps = detector.getAmplifiers()
1906 ampNames = [amp.getName() for amp in amps]
1907 detName = detector.getName()
1908 effectivePtc = PhotonTransferCurveDataset(ampNames, 'EFFECTIVE_PTC', 1)
1909 boolGainMismatch = False
1910 doWarningPtcValidation = True
1911
1912 for amp, overscanResults in zip(amps, overScans):
1913 ampName = amp.getName()
1914 # Gain:
1915 # Try first with the PTC gains.
1916 gainProvenanceString = "amp"
1917 if self.config.usePtcGains:
1918 gain = ptcDataset.gain[ampName]
1919 gainProvenanceString = "ptc"
1920 self.log.debug("Using gain from Photon Transfer Curve.")
1921 else:
1922 # Try then with the amplifier gain.
1923 # We already have a detector at this point. If there was no
1924 # detector to begin with, one would have been created with
1925 # self.config.gain and self.config.noise. Same comment
1926 # applies for the noise block below.
1927 gain = amp.getGain()
1928
1929 # Check if the gain up to this point differs from the
1930 # gain in bfGains. If so, raise or warn, accordingly.
1931 if not boolGainMismatch and bfGains is not None and ampName in bfGains:
1932 bfGain = bfGains[ampName]
1933 if not math.isclose(gain, bfGain, rel_tol=1e-4):
1934 if self.config.doRaiseOnCalibMismatch:
1935 raise RuntimeError("Gain mismatch for det %s amp %s: "
1936 "(gain (%s): %s, bfGain: %s)",
1937 detName, ampName, gainProvenanceString,
1938 gain, bfGain)
1939 else:
1940 self.log.warning("Gain mismatch for det %s amp %s: "
1941 "(gain (%s): %s, bfGain: %s)",
1942 detName, ampName, gainProvenanceString,
1943 gain, bfGain)
1944 boolGainMismatch = True
1945
1946 # Gain:
1947 if math.isnan(gain):
1948 gain = 1.0
1949 self.log.warning("Gain for amp %s set to NaN! Updating to"
1950 " 1.0 to generate Poisson variance.", ampName)
1951 elif gain <= 0:
1952 patchedGain = 1.0
1953 self.log.warning("Gain for amp %s == %g <= 0; setting to %f.",
1954 ampName, gain, patchedGain)
1955 gain = patchedGain
1956
1957 # Noise:
1958 # Try first with the empirical noise from the overscan.
1959 noiseProvenanceString = "amp"
1960 if self.config.doEmpiricalReadNoise and overscanResults is not None:
1961 noiseProvenanceString = "serial overscan"
1962 if isinstance(overscanResults.residualSigma, float):
1963 # Only serial overscan was run
1964 noise = overscanResults.residualSigma
1965 else:
1966 # Both serial and parallel overscan were
1967 # run. Only report noise from serial here.
1968 noise = overscanResults.residualSigma[0]
1969
1970 # Overscan noise is always in adu; we must standardize
1971 # the read noise attributes of all
1972 # PhotonTransferCurveDataset objects to units of
1973 # electrons.
1974 noise *= gain
1975 elif self.config.usePtcReadNoise:
1976 # Try then with the PTC noise.
1977 self.log.debug("Using noise from Photon Transfer Curve.")
1978 noise = ptcDataset.noise[ampName]
1979 noiseProvenanceString = "ptc"
1980 else:
1981 # Finally, try with the amplifier noise.
1982 # We already have a detector at this point. If there was no
1983 # detector to begin with, one would have been created with
1984 # self.config.gain and self.config.noise.
1985 # Amplifier object read noise is always in electron
1986 noise = amp.getReadNoise()
1987
1988 # PTC Turnoff:
1989 # Copy it over from the input PTC if it's positive. If it's a nan
1990 # set it to a high value.
1991 if ptcDataset is not None:
1992 ptcTurnoff = ptcDataset.ptcTurnoff[ampName]
1993 else:
1994 ptcTurnoff = 2e19
1995
1996 if (isinstance(ptcTurnoff, numbers.Real) and ptcTurnoff > 0):
1997 effectivePtc.ptcTurnoff[ampName] = ptcTurnoff
1998 elif math.isnan(ptcTurnoff):
1999 effectivePtc.ptcTurnoff[ampName] = 2e19
2000
2001 effectivePtc.gain[ampName] = gain
2002 effectivePtc.noise[ampName] = noise
2003 # Make sure read noise, turnoff, and gain make sense
2004 effectivePtc.validateGainNoiseTurnoffValues(ampName, doWarn=doWarningPtcValidation)
2005 doWarningPtcValidation = False
2006
2007 # These keys are duplicated for compatability with isrTaskLSST
2008 metadata[f"LSST GAIN {amp.getName()}"] = effectivePtc.gain[ampName]
2009 metadata[f"LSST ISR GAIN {amp.getName()}"] = effectivePtc.gain[ampName]
2010 metadata[f"LSST READNOISE {amp.getName()}"] = effectivePtc.noise[ampName]
2011 metadata[f"LSST ISR READNOISE {amp.getName()}"] = effectivePtc.noise[ampName]
2012
2013 self.log.info("Det: %s - Noise provenance: %s, Gain provenance: %s",
2014 detName,
2015 noiseProvenanceString,
2016 gainProvenanceString)
2017 metadata["LSST ISR GAIN SOURCE"] = gainProvenanceString
2018 metadata["LSST ISR NOISE SOURCE"] = noiseProvenanceString
2019 metadata["LSST ISR READNOISE UNITS"] = "electron"
2020
2021 return effectivePtc
2022
2023 def ensureExposure(self, inputExp, camera=None, detectorNum=None):
2024 """Ensure that the data returned by Butler is a fully constructed exp.
2025
2026 ISR requires exposure-level image data for historical reasons, so if we
2027 did not recieve that from Butler, construct it from what we have,
2028 modifying the input in place.
2029
2030 Parameters
2031 ----------
2032 inputExp : `lsst.afw.image` image-type.
2033 The input data structure obtained from Butler.
2034 Can be `lsst.afw.image.Exposure`,
2035 `lsst.afw.image.DecoratedImageU`,
2036 or `lsst.afw.image.ImageF`
2037 camera : `lsst.afw.cameraGeom.camera`, optional
2038 The camera associated with the image. Used to find the appropriate
2039 detector if detector is not already set.
2040 detectorNum : `int`, optional
2041 The detector in the camera to attach, if the detector is not
2042 already set.
2043
2044 Returns
2045 -------
2046 inputExp : `lsst.afw.image.Exposure`
2047 The re-constructed exposure, with appropriate detector parameters.
2048
2049 Raises
2050 ------
2051 TypeError
2052 Raised if the input data cannot be used to construct an exposure.
2053 """
2054 if isinstance(inputExp, afwImage.DecoratedImageU):
2056 elif isinstance(inputExp, afwImage.ImageF):
2058 elif isinstance(inputExp, afwImage.MaskedImageF):
2059 inputExp = afwImage.makeExposure(inputExp)
2060 elif isinstance(inputExp, afwImage.Exposure):
2061 pass
2062 elif inputExp is None:
2063 # Assume this will be caught by the setup if it is a problem.
2064 return inputExp
2065 else:
2066 raise TypeError("Input Exposure is not known type in isrTask.ensureExposure: %s." %
2067 (type(inputExp), ))
2068
2069 if inputExp.getDetector() is None:
2070 if camera is None or detectorNum is None:
2071 raise RuntimeError('Must supply both a camera and detector number when using exposures '
2072 'without a detector set.')
2073 inputExp.setDetector(camera[detectorNum])
2074
2075 return inputExp
2076
2077 @staticmethod
2079 """Extract common calibration metadata values that will be written to
2080 output header.
2081
2082 Parameters
2083 ----------
2084 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib`
2085 Calibration to pull date information from.
2086
2087 Returns
2088 -------
2089 dateString : `str`
2090 Calibration creation date string to add to header.
2091 """
2092 if hasattr(calib, "getMetadata"):
2093 if 'CALIB_CREATION_DATE' in calib.getMetadata():
2094 return " ".join((calib.getMetadata().get("CALIB_CREATION_DATE", "Unknown"),
2095 calib.getMetadata().get("CALIB_CREATION_TIME", "Unknown")))
2096 else:
2097 return " ".join((calib.getMetadata().get("CALIB_CREATE_DATE", "Unknown"),
2098 calib.getMetadata().get("CALIB_CREATE_TIME", "Unknown")))
2099 else:
2100 return "Unknown Unknown"
2101
2102 def compareUnits(self, calibMetadata, calibName):
2103 """Compare units from calibration to ISR units.
2104
2105 For the regular IsrTask this is used to confirm that calibs
2106 suitable for IsrTaskLSST are not used with the old IsrTask.
2107
2108 Parameters
2109 ----------
2110 calibMetadata : `lsst.daf.base.PropertyList`
2111 Calibration metadata from header.
2112 calibName : `str`
2113 Calibration name for log message.
2114 """
2115 calibUnits = calibMetadata.get("LSST ISR UNITS", "adu")
2116 isrUnits = "adu"
2117 if calibUnits != isrUnits:
2118 if self.config.doRaiseOnCalibMismatch:
2119 raise RuntimeError(
2120 "Unit mismatch: isr has %s units but %s has %s units",
2121 isrUnits,
2122 calibName,
2123 calibUnits,
2124 )
2125 else:
2126 self.log.warning(
2127 "Unit mismatch: isr has %s units but %s has %s units",
2128 isrUnits,
2129 calibName,
2130 calibUnits,
2131 )
2132
2133 def convertIntToFloat(self, exposure):
2134 """Convert exposure image from uint16 to float.
2135
2136 If the exposure does not need to be converted, the input is
2137 immediately returned. For exposures that are converted to use
2138 floating point pixels, the variance is set to unity and the
2139 mask to zero.
2140
2141 Parameters
2142 ----------
2143 exposure : `lsst.afw.image.Exposure`
2144 The raw exposure to be converted.
2145
2146 Returns
2147 -------
2148 newexposure : `lsst.afw.image.Exposure`
2149 The input ``exposure``, converted to floating point pixels.
2150
2151 Raises
2152 ------
2153 RuntimeError
2154 Raised if the exposure type cannot be converted to float.
2155
2156 """
2157 if isinstance(exposure, afwImage.ExposureF):
2158 # Nothing to be done
2159 self.log.debug("Exposure already of type float.")
2160 return exposure
2161 if not hasattr(exposure, "convertF"):
2162 raise RuntimeError("Unable to convert exposure (%s) to float." % type(exposure))
2163
2164 newexposure = exposure.convertF()
2165 newexposure.variance[:] = 1
2166 newexposure.mask[:] = 0x0
2167
2168 return newexposure
2169
2170 def maskAmplifier(self, ccdExposure, amp, defects):
2171 """Identify bad amplifiers, saturated and suspect pixels.
2172
2173 Parameters
2174 ----------
2175 ccdExposure : `lsst.afw.image.Exposure`
2176 Input exposure to be masked.
2177 amp : `lsst.afw.cameraGeom.Amplifier`
2178 Catalog of parameters defining the amplifier on this
2179 exposure to mask.
2180 defects : `lsst.ip.isr.Defects`
2181 List of defects. Used to determine if the entire
2182 amplifier is bad.
2183
2184 Returns
2185 -------
2186 badAmp : `Bool`
2187 If this is true, the entire amplifier area is covered by
2188 defects and unusable.
2189
2190 """
2191 maskedImage = ccdExposure.getMaskedImage()
2192
2193 badAmp = False
2194
2195 # Check if entire amp region is defined as a defect
2196 # NB: need to use amp.getBBox() for correct comparison with current
2197 # defects definition.
2198 if defects is not None:
2199 badAmp = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects]))
2200
2201 # In the case of a bad amp, we will set mask to "BAD"
2202 # (here use amp.getRawBBox() for correct association with pixels in
2203 # current ccdExposure).
2204 if badAmp:
2205 dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
2206 afwImage.PARENT)
2207 maskView = dataView.getMask()
2208 maskView |= maskView.getPlaneBitMask("BAD")
2209 del maskView
2210 return badAmp
2211
2212 # Mask remaining defects after assembleCcd() to allow for defects that
2213 # cross amplifier boundaries. Saturation and suspect pixels can be
2214 # masked now, though.
2215 limits = dict()
2216 if self.config.doSaturation and not badAmp:
2217 # Set to the default from the camera model.
2218 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
2219 # And update if it is set in the config.
2220 if math.isfinite(self.config.saturation):
2221 limits.update({self.config.saturatedMaskName: self.config.saturation})
2222 if self.config.doSuspect and not badAmp:
2223 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
2224
2225 for maskName, maskThreshold in limits.items():
2226 if not math.isnan(maskThreshold):
2227 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2228 isrFunctions.makeThresholdMask(
2229 maskedImage=dataView,
2230 threshold=maskThreshold,
2231 growFootprints=0,
2232 maskName=maskName
2233 )
2234
2235 # Determine if we've fully masked this amplifier with SUSPECT and
2236 # SAT pixels.
2237 maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
2238 afwImage.PARENT)
2239 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
2240 self.config.suspectMaskName])
2241 if numpy.all(maskView.getArray() & maskVal > 0):
2242 badAmp = True
2243 maskView |= maskView.getPlaneBitMask("BAD")
2244
2245 return badAmp
2246
2247 def overscanCorrection(self, ccdExposure, amp):
2248 """Apply overscan correction in place.
2249
2250 This method does initial pixel rejection of the overscan
2251 region. The overscan can also be optionally segmented to
2252 allow for discontinuous overscan responses to be fit
2253 separately. The actual overscan subtraction is performed by
2254 the `lsst.ip.isr.overscan.OverscanTask`, which is called here
2255 after the amplifier is preprocessed.
2256
2257 Parameters
2258 ----------
2259 ccdExposure : `lsst.afw.image.Exposure`
2260 Exposure to have overscan correction performed.
2261 amp : `lsst.afw.cameraGeom.Amplifer`
2262 The amplifier to consider while correcting the overscan.
2263
2264 Returns
2265 -------
2266 overscanResults : `lsst.pipe.base.Struct`
2267 Result struct with components:
2268
2269 ``imageFit``
2270 Value or fit subtracted from the amplifier image data.
2271 (scalar or `lsst.afw.image.Image`)
2272 ``overscanFit``
2273 Value or fit subtracted from the overscan image data.
2274 (scalar or `lsst.afw.image.Image`)
2275 ``overscanImage``
2276 Image of the overscan region with the overscan
2277 correction applied. This quantity is used to estimate
2278 the amplifier read noise empirically.
2279 (`lsst.afw.image.Image`)
2280 ``edgeMask``
2281 Mask of the suspect pixels. (`lsst.afw.image.Mask`)
2282 ``overscanMean``
2283 Median overscan fit value. (`float`)
2284 ``overscanSigma``
2285 Clipped standard deviation of the overscan after
2286 correction. (`float`)
2287
2288 Raises
2289 ------
2290 RuntimeError
2291 Raised if the ``amp`` does not contain raw pixel information.
2292
2293 See Also
2294 --------
2295 lsst.ip.isr.overscan.OverscanTask
2296 """
2297 if amp.getRawHorizontalOverscanBBox().isEmpty():
2298 self.log.info("ISR_OSCAN: No overscan region. Not performing overscan correction.")
2299 return None
2300
2301 # Perform overscan correction on subregions.
2302 overscanResults = self.overscan.run(ccdExposure, amp)
2303
2304 metadata = ccdExposure.getMetadata()
2305 ampName = amp.getName()
2306
2307 keyBase = "LSST ISR OVERSCAN"
2308 # The overscan statistics units will always match the units of
2309 # the image at the point they are calculated.
2310 metadata[f"{keyBase} SERIAL UNITS"] = metadata.get("LSST ISR UNITS")
2311
2312 # Updated quantities
2313 if isinstance(overscanResults.overscanMean, float):
2314 # Serial overscan correction only:
2315 metadata[f"{keyBase} SERIAL MEAN {ampName}"] = overscanResults.overscanMean
2316 metadata[f"{keyBase} SERIAL MEDIAN {ampName}"] = overscanResults.overscanMedian
2317 metadata[f"{keyBase} SERIAL STDEV {ampName}"] = overscanResults.overscanSigma
2318
2319 metadata[f"{keyBase} RESIDUAL SERIAL MEAN {ampName}"] = overscanResults.residualMean
2320 metadata[f"{keyBase} RESIDUAL SERIAL MEDIAN {ampName}"] = overscanResults.residualMedian
2321 metadata[f"{keyBase} RESIDUAL SERIAL STDEV {ampName}"] = overscanResults.residualSigma
2322 elif isinstance(overscanResults.overscanMean, tuple):
2323 # Both serial and parallel overscan have run:
2324 metadata[f"{keyBase} PARALLEL UNITS"] = metadata.get("LSST ISR UNITS")
2325 metadata[f"{keyBase} SERIAL MEAN {ampName}"] = overscanResults.overscanMean[0]
2326 metadata[f"{keyBase} SERIAL MEDIAN {ampName}"] = overscanResults.overscanMedian[0]
2327 metadata[f"{keyBase} SERIAL STDEV {ampName}"] = overscanResults.overscanSigma[0]
2328
2329 metadata[f"{keyBase} PARALLEL MEAN {ampName}"] = overscanResults.overscanMean[1]
2330 metadata[f"{keyBase} PARALLEL MEDIAN {ampName}"] = overscanResults.overscanMedian[1]
2331 metadata[f"{keyBase} PARALLEL STDEV {ampName}"] = overscanResults.overscanSigma[1]
2332
2333 metadata[f"{keyBase} RESIDUAL SERIAL MEAN {ampName}"] = overscanResults.residualMean[0]
2334 metadata[f"{keyBase} RESIDUAL SERIAL MEDIAN {ampName}"] = overscanResults.residualMedian[0]
2335 metadata[f"{keyBase} RESIDUAL SERIAL STDEV {ampName}"] = overscanResults.residualSigma[0]
2336
2337 metadata[f"{keyBase} RESIDUAL PARALLEL MEAN {ampName}"] = overscanResults.residualMean[1]
2338 metadata[f"{keyBase} RESIDUAL PARALLEL MEDIAN {ampName}"] = overscanResults.residualMedian[1]
2339 metadata[f"{keyBase} RESIDUAL PARALLEL STDEV {ampName}"] = overscanResults.residualSigma[1]
2340 else:
2341 self.log.warning("Unexpected type for overscan values; none added to header.")
2342
2343 return overscanResults
2344
2345 def updateVariance(self, ampExposure, amp, ptcDataset):
2346 """Set the variance plane using the gain and read noise
2347
2348 The read noise is calculated from the ``overscanImage`` if the
2349 ``doEmpiricalReadNoise`` option is set in the configuration; otherwise
2350 the value from the amplifier data is used.
2351
2352 Parameters
2353 ----------
2354 ampExposure : `lsst.afw.image.Exposure`
2355 Exposure to process.
2356 amp : `lsst.afw.cameraGeom.Amplifier` or `FakeAmp`
2357 Amplifier detector data.
2358 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`
2359 Effective PTC dataset containing the gains and read noise.
2360
2361 See also
2362 --------
2363 lsst.ip.isr.isrFunctions.updateVariance
2364 """
2365 ampName = amp.getName()
2366 # At this point, the effective PTC should have
2367 # gain and read noise values.
2368 gain = ptcDataset.gain[ampName]
2369 readNoise = ptcDataset.noise[ampName]
2370
2371 # The image will always be in adu and the noise
2372 # will always be in electrons, and we will output
2373 # the variance plane in the image units (adu^2).
2374 isrFunctions.updateVariance(
2375 maskedImage=ampExposure.getMaskedImage(),
2376 gain=gain,
2377 readNoise=readNoise,
2378 )
2379
2380 def maskNegativeVariance(self, exposure):
2381 """Identify and mask pixels with negative variance values.
2382
2383 Parameters
2384 ----------
2385 exposure : `lsst.afw.image.Exposure`
2386 Exposure to process.
2387
2388 See Also
2389 --------
2390 lsst.ip.isr.isrFunctions.updateVariance
2391 """
2392 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName)
2393 bad = numpy.where(exposure.getVariance().getArray() <= 0.0)
2394 exposure.mask.array[bad] |= maskPlane
2395
2396 def darkCorrection(self, exposure, darkExposure, invert=False):
2397 """Apply dark correction in place.
2398
2399 Parameters
2400 ----------
2401 exposure : `lsst.afw.image.Exposure`
2402 Exposure to process.
2403 darkExposure : `lsst.afw.image.Exposure`
2404 Dark exposure of the same size as ``exposure``.
2405 invert : `Bool`, optional
2406 If True, re-add the dark to an already corrected image.
2407
2408 Raises
2409 ------
2410 RuntimeError
2411 Raised if either ``exposure`` or ``darkExposure`` do not
2412 have their dark time defined.
2413
2414 See Also
2415 --------
2416 lsst.ip.isr.isrFunctions.darkCorrection
2417 """
2418 expScale = exposure.getInfo().getVisitInfo().getDarkTime()
2419 if math.isnan(expScale):
2420 raise RuntimeError("Exposure darktime is NaN.")
2421 if darkExposure.getInfo().getVisitInfo() is not None \
2422 and not math.isnan(darkExposure.getInfo().getVisitInfo().getDarkTime()):
2423 darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
2424 else:
2425 # DM-17444: darkExposure.getInfo.getVisitInfo() is None
2426 # so getDarkTime() does not exist.
2427 self.log.warning("darkExposure.getInfo().getVisitInfo() does not exist. Using darkScale = 1.0.")
2428 darkScale = 1.0
2429
2430 isrFunctions.darkCorrection(
2431 maskedImage=exposure.getMaskedImage(),
2432 darkMaskedImage=darkExposure.getMaskedImage(),
2433 expScale=expScale,
2434 darkScale=darkScale,
2435 invert=invert,
2436 trimToFit=self.config.doTrimToMatchCalib
2437 )
2438
2439 def doLinearize(self, detector):
2440 """Check if linearization is needed for the detector cameraGeom.
2441
2442 Checks config.doLinearize and the linearity type of the first
2443 amplifier.
2444
2445 Parameters
2446 ----------
2447 detector : `lsst.afw.cameraGeom.Detector`
2448 Detector to get linearity type from.
2449
2450 Returns
2451 -------
2452 doLinearize : `Bool`
2453 If True, linearization should be performed.
2454 """
2455 return self.config.doLinearize and \
2456 detector.getAmplifiers()[0].getLinearityType() != NullLinearityType
2457
2458 def flatCorrection(self, exposure, flatExposure, invert=False):
2459 """Apply flat correction in place.
2460
2461 Parameters
2462 ----------
2463 exposure : `lsst.afw.image.Exposure`
2464 Exposure to process.
2465 flatExposure : `lsst.afw.image.Exposure`
2466 Flat exposure of the same size as ``exposure``.
2467 invert : `Bool`, optional
2468 If True, unflatten an already flattened image.
2469
2470 See Also
2471 --------
2472 lsst.ip.isr.isrFunctions.flatCorrection
2473 """
2474 isrFunctions.flatCorrection(
2475 maskedImage=exposure.getMaskedImage(),
2476 flatMaskedImage=flatExposure.getMaskedImage(),
2477 scalingType=self.config.flatScalingType,
2478 userScale=self.config.flatUserScale,
2479 invert=invert,
2480 trimToFit=self.config.doTrimToMatchCalib
2481 )
2482
2483 def saturationDetection(self, exposure, amp):
2484 """Detect and mask saturated pixels in config.saturatedMaskName.
2485
2486 Parameters
2487 ----------
2488 exposure : `lsst.afw.image.Exposure`
2489 Exposure to process. Only the amplifier DataSec is processed.
2490 amp : `lsst.afw.cameraGeom.Amplifier`
2491 Amplifier detector data.
2492
2493 See Also
2494 --------
2495 lsst.ip.isr.isrFunctions.makeThresholdMask
2496 """
2497 if not math.isnan(amp.getSaturation()):
2498 maskedImage = exposure.getMaskedImage()
2499 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2500 isrFunctions.makeThresholdMask(
2501 maskedImage=dataView,
2502 threshold=amp.getSaturation(),
2503 growFootprints=0,
2504 maskName=self.config.saturatedMaskName,
2505 )
2506
2507 def saturationInterpolation(self, exposure):
2508 """Interpolate over saturated pixels, in place.
2509
2510 This method should be called after `saturationDetection`, to
2511 ensure that the saturated pixels have been identified in the
2512 SAT mask. It should also be called after `assembleCcd`, since
2513 saturated regions may cross amplifier boundaries.
2514
2515 Parameters
2516 ----------
2517 exposure : `lsst.afw.image.Exposure`
2518 Exposure to process.
2519
2520 See Also
2521 --------
2522 lsst.ip.isr.isrTask.saturationDetection
2523 lsst.ip.isr.isrFunctions.interpolateFromMask
2524 """
2525 isrFunctions.interpolateFromMask(
2526 maskedImage=exposure.getMaskedImage(),
2527 fwhm=self.config.fwhm,
2528 growSaturatedFootprints=self.config.growSaturationFootprintSize,
2529 maskNameList=list(self.config.saturatedMaskName),
2530 useLegacyInterp=self.config.useLegacyInterp,
2531 )
2532
2533 def suspectDetection(self, exposure, amp):
2534 """Detect and mask suspect pixels in config.suspectMaskName.
2535
2536 Parameters
2537 ----------
2538 exposure : `lsst.afw.image.Exposure`
2539 Exposure to process. Only the amplifier DataSec is processed.
2540 amp : `lsst.afw.cameraGeom.Amplifier`
2541 Amplifier detector data.
2542
2543 See Also
2544 --------
2545 lsst.ip.isr.isrFunctions.makeThresholdMask
2546
2547 Notes
2548 -----
2549 Suspect pixels are pixels whose value is greater than
2550 amp.getSuspectLevel(). This is intended to indicate pixels that may be
2551 affected by unknown systematics; for example if non-linearity
2552 corrections above a certain level are unstable then that would be a
2553 useful value for suspectLevel. A value of `nan` indicates that no such
2554 level exists and no pixels are to be masked as suspicious.
2555 """
2556 suspectLevel = amp.getSuspectLevel()
2557 if math.isnan(suspectLevel):
2558 return
2559
2560 maskedImage = exposure.getMaskedImage()
2561 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2562 isrFunctions.makeThresholdMask(
2563 maskedImage=dataView,
2564 threshold=suspectLevel,
2565 growFootprints=0,
2566 maskName=self.config.suspectMaskName,
2567 )
2568
2569 def maskDefect(self, exposure, defectBaseList):
2570 """Mask defects using mask plane "BAD", in place.
2571
2572 Parameters
2573 ----------
2574 exposure : `lsst.afw.image.Exposure`
2575 Exposure to process.
2576 defectBaseList : defect-type
2577 List of defects to mask. Can be of type `lsst.ip.isr.Defects`
2578 or `list` of `lsst.afw.image.DefectBase`.
2579
2580 Notes
2581 -----
2582 Call this after CCD assembly, since defects may cross amplifier
2583 boundaries.
2584 """
2585 maskedImage = exposure.getMaskedImage()
2586 if not isinstance(defectBaseList, Defects):
2587 # Promotes DefectBase to Defect
2588 defectList = Defects(defectBaseList)
2589 else:
2590 defectList = defectBaseList
2591 defectList.maskPixels(maskedImage, maskName="BAD")
2592
2593 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'):
2594 """Mask edge pixels with applicable mask plane.
2595
2596 Parameters
2597 ----------
2598 exposure : `lsst.afw.image.Exposure`
2599 Exposure to process.
2600 numEdgePixels : `int`, optional
2601 Number of edge pixels to mask.
2602 maskPlane : `str`, optional
2603 Mask plane name to use.
2604 level : `str`, optional
2605 Level at which to mask edges.
2606 """
2607 maskedImage = exposure.getMaskedImage()
2608 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane)
2609
2610 if numEdgePixels > 0:
2611 if level == 'DETECTOR':
2612 boxes = [maskedImage.getBBox()]
2613 elif level == 'AMP':
2614 boxes = [amp.getBBox() for amp in exposure.getDetector()]
2615
2616 for box in boxes:
2617 # This makes a bbox numEdgeSuspect pixels smaller than the
2618 # image on each side
2619 subImage = maskedImage[box]
2620 box.grow(-numEdgePixels)
2621 # Mask pixels outside box
2622 SourceDetectionTask.setEdgeBits(
2623 subImage,
2624 box,
2625 maskBitMask)
2626
2627 def maskAndInterpolateDefects(self, exposure, defectBaseList):
2628 """Mask and interpolate defects using mask plane "BAD", in place.
2629
2630 Parameters
2631 ----------
2632 exposure : `lsst.afw.image.Exposure`
2633 Exposure to process.
2634 defectBaseList : defects-like
2635 List of defects to mask and interpolate. Can be
2636 `lsst.ip.isr.Defects` or `list` of `lsst.afw.image.DefectBase`.
2637
2638 See Also
2639 --------
2640 lsst.ip.isr.isrTask.maskDefect
2641 """
2642 self.maskDefect(exposure, defectBaseList)
2643 self.maskEdges(exposure, numEdgePixels=self.config.numEdgeSuspect,
2644 maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
2645 isrFunctions.interpolateFromMask(
2646 maskedImage=exposure.getMaskedImage(),
2647 fwhm=self.config.fwhm,
2648 growSaturatedFootprints=0,
2649 maskNameList=["BAD"],
2650 useLegacyInterp=self.config.useLegacyInterp,
2651 )
2652
2653 def maskNan(self, exposure):
2654 """Mask NaNs using mask plane "UNMASKEDNAN", in place.
2655
2656 Parameters
2657 ----------
2658 exposure : `lsst.afw.image.Exposure`
2659 Exposure to process.
2660
2661 Notes
2662 -----
2663 We mask over all non-finite values (NaN, inf), including those
2664 that are masked with other bits (because those may or may not be
2665 interpolated over later, and we want to remove all NaN/infs).
2666 Despite this behaviour, the "UNMASKEDNAN" mask plane is used to
2667 preserve the historical name.
2668 """
2669 maskedImage = exposure.getMaskedImage()
2670
2671 # Find and mask NaNs
2672 maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
2673 maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
2674 numNans = maskNans(maskedImage, maskVal)
2675 self.metadata["NUMNANS"] = numNans
2676 if numNans > 0:
2677 self.log.warning("There were %d unmasked NaNs.", numNans)
2678
2679 def maskAndInterpolateNan(self, exposure):
2680 """"Mask and interpolate NaN/infs using mask plane "UNMASKEDNAN",
2681 in place.
2682
2683 Parameters
2684 ----------
2685 exposure : `lsst.afw.image.Exposure`
2686 Exposure to process.
2687
2688 See Also
2689 --------
2690 lsst.ip.isr.isrTask.maskNan
2691 """
2692 self.maskNan(exposure)
2693 isrFunctions.interpolateFromMask(
2694 maskedImage=exposure.getMaskedImage(),
2695 fwhm=self.config.fwhm,
2696 growSaturatedFootprints=0,
2697 maskNameList=["UNMASKEDNAN"],
2698 useLegacyInterp=self.config.useLegacyInterp,
2699 )
2700
2701 def measureBackground(self, exposure, IsrQaConfig=None):
2702 """Measure the image background in subgrids, for quality control.
2703
2704 Parameters
2705 ----------
2706 exposure : `lsst.afw.image.Exposure`
2707 Exposure to process.
2708 IsrQaConfig : `lsst.ip.isr.isrQa.IsrQaConfig`
2709 Configuration object containing parameters on which background
2710 statistics and subgrids to use.
2711 """
2712 if IsrQaConfig is not None:
2713 statsControl = afwMath.StatisticsControl(IsrQaConfig.flatness.clipSigma,
2714 IsrQaConfig.flatness.nIter)
2715 maskVal = exposure.getMaskedImage().getMask().getPlaneBitMask(["BAD", "SAT", "DETECTED"])
2716 statsControl.setAndMask(maskVal)
2717 maskedImage = exposure.getMaskedImage()
2718 stats = afwMath.makeStatistics(maskedImage, afwMath.MEDIAN | afwMath.STDEVCLIP, statsControl)
2719 skyLevel = stats.getValue(afwMath.MEDIAN)
2720 skySigma = stats.getValue(afwMath.STDEVCLIP)
2721 self.log.info("Flattened sky level: %f +/- %f.", skyLevel, skySigma)
2722 metadata = exposure.getMetadata()
2723 metadata["SKYLEVEL"] = skyLevel
2724 metadata["SKYSIGMA"] = skySigma
2725
2726 # calcluating flatlevel over the subgrids
2727 stat = afwMath.MEANCLIP if IsrQaConfig.flatness.doClip else afwMath.MEAN
2728 meshXHalf = int(IsrQaConfig.flatness.meshX/2.)
2729 meshYHalf = int(IsrQaConfig.flatness.meshY/2.)
2730 nX = int((exposure.getWidth() + meshXHalf) / IsrQaConfig.flatness.meshX)
2731 nY = int((exposure.getHeight() + meshYHalf) / IsrQaConfig.flatness.meshY)
2732 skyLevels = numpy.zeros((nX, nY))
2733
2734 for j in range(nY):
2735 yc = meshYHalf + j * IsrQaConfig.flatness.meshY
2736 for i in range(nX):
2737 xc = meshXHalf + i * IsrQaConfig.flatness.meshX
2738
2739 xLLC = xc - meshXHalf
2740 yLLC = yc - meshYHalf
2741 xURC = xc + meshXHalf - 1
2742 yURC = yc + meshYHalf - 1
2743
2744 bbox = lsst.geom.Box2I(lsst.geom.Point2I(xLLC, yLLC), lsst.geom.Point2I(xURC, yURC))
2745 miMesh = maskedImage.Factory(exposure.getMaskedImage(), bbox, afwImage.LOCAL)
2746
2747 skyLevels[i, j] = afwMath.makeStatistics(miMesh, stat, statsControl).getValue()
2748
2749 good = numpy.where(numpy.isfinite(skyLevels))
2750 if len(good[0]) == 0:
2751 # There are no good pixels.
2752 self.log.warning("No good pixels to measure sky levels.")
2753 skyMedian = numpy.nan
2754 flatness = numpy.nan
2755 flatness_rms = numpy.nan
2756 flatness_pp = numpy.nan
2757 else:
2758 skyMedian = numpy.median(skyLevels[good])
2759 flatness = (skyLevels[good] - skyMedian) / skyMedian
2760 flatness_rms = numpy.std(flatness)
2761 flatness_pp = flatness.max() - flatness.min() if len(flatness) > 0 else numpy.nan
2762
2763 self.log.info("Measuring sky levels in %dx%d grids: %f.", nX, nY, skyMedian)
2764 self.log.info("Sky flatness in %dx%d grids - pp: %f rms: %f.",
2765 nX, nY, flatness_pp, flatness_rms)
2766
2767 metadata["FLATNESS_PP"] = float(flatness_pp)
2768 metadata["FLATNESS_RMS"] = float(flatness_rms)
2769 metadata["FLATNESS_NGRIDS"] = '%dx%d' % (nX, nY)
2770 metadata["FLATNESS_MESHX"] = IsrQaConfig.flatness.meshX
2771 metadata["FLATNESS_MESHY"] = IsrQaConfig.flatness.meshY
2772
2773 def roughZeroPoint(self, exposure):
2774 """Set an approximate magnitude zero point for the exposure.
2775
2776 Parameters
2777 ----------
2778 exposure : `lsst.afw.image.Exposure`
2779 Exposure to process.
2780 """
2781 filterLabel = exposure.getFilter()
2782 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
2783
2784 if physicalFilter in self.config.fluxMag0T1:
2785 fluxMag0 = self.config.fluxMag0T1[physicalFilter]
2786 else:
2787 self.log.warning("No rough magnitude zero point defined for filter %s.", physicalFilter)
2788 fluxMag0 = self.config.defaultFluxMag0T1
2789
2790 expTime = exposure.getInfo().getVisitInfo().getExposureTime()
2791 if expTime == 0.0:
2792 self.log.debug("Received exposure with 0.0 expTime; skipping rough zero point.")
2793 return
2794 elif not expTime > 0: # handle NaN as well as <= 0
2795 self.log.warning("Non-positive exposure time; skipping rough zero point.")
2796 return
2797
2798 self.log.info("Setting rough magnitude zero point for filter %s: %f",
2799 physicalFilter, 2.5*math.log10(fluxMag0*expTime))
2800 exposure.setPhotoCalib(afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0*expTime, 0.0))
2801
2802 @contextmanager
2803 def flatContext(self, exp, flat, dark=None):
2804 """Context manager that applies and removes flats and darks,
2805 if the task is configured to apply them.
2806
2807 Parameters
2808 ----------
2809 exp : `lsst.afw.image.Exposure`
2810 Exposure to process.
2811 flat : `lsst.afw.image.Exposure`
2812 Flat exposure the same size as ``exp``.
2813 dark : `lsst.afw.image.Exposure`, optional
2814 Dark exposure the same size as ``exp``.
2815
2816 Yields
2817 ------
2818 exp : `lsst.afw.image.Exposure`
2819 The flat and dark corrected exposure.
2820 """
2821 if self.config.doDark and dark is not None:
2822 self.darkCorrection(exp, dark)
2823 if self.config.doFlat:
2824 self.flatCorrection(exp, flat)
2825 try:
2826 yield exp
2827 finally:
2828 if self.config.doFlat:
2829 self.flatCorrection(exp, flat, invert=True)
2830 if self.config.doDark and dark is not None:
2831 self.darkCorrection(exp, dark, invert=True)
2832
2833 @deprecated(
2834 reason=(
2835 "makeBinnedImages is no longer used. "
2836 "Please subtask lsst.ip.isr.BinImageDataTask instead."
2837 ),
2838 version="v28", category=FutureWarning
2839 )
2840 def makeBinnedImages(self, exposure):
2841 """Make visualizeVisit style binned exposures.
2842
2843 Parameters
2844 ----------
2845 exposure : `lsst.afw.image.Exposure`
2846 Exposure to bin.
2847
2848 Returns
2849 -------
2850 bin1 : `lsst.afw.image.Exposure`
2851 Binned exposure using binFactor1.
2852 bin2 : `lsst.afw.image.Exposure`
2853 Binned exposure using binFactor2.
2854 """
2855 mi = exposure.getMaskedImage()
2856
2857 bin1 = afwMath.binImage(mi, self.config.binFactor1)
2858 bin2 = afwMath.binImage(mi, self.config.binFactor2)
2859
2860 bin1 = afwImage.makeExposure(bin1)
2861 bin2 = afwImage.makeExposure(bin2)
2862
2863 bin1.setInfo(exposure.getInfo())
2864 bin2.setInfo(exposure.getInfo())
2865
2866 return bin1, bin2
2867
2868 def debugView(self, exposure, stepname):
2869 """Utility function to examine ISR exposure at different stages.
2870
2871 Parameters
2872 ----------
2873 exposure : `lsst.afw.image.Exposure`
2874 Exposure to view.
2875 stepname : `str`
2876 State of processing to view.
2877 """
2878 frame = getDebugFrame(self._display, stepname)
2879 if frame:
2880 display = getDisplay(frame)
2881 display.scale('asinh', 'zscale')
2882 display.mtv(exposure)
2883 prompt = "Press Enter to continue [c]... "
2884 while True:
2885 ans = input(prompt).lower()
2886 if ans in ("", "c",):
2887 break
2888
2889
2890class FakeAmp(object):
2891 """A Detector-like object that supports returning gain and saturation level
2892
2893 This is used when the input exposure does not have a detector.
2894
2895 Parameters
2896 ----------
2897 exposure : `lsst.afw.image.Exposure`
2898 Exposure to generate a fake amplifier for.
2899 config : `lsst.ip.isr.isrTaskConfig`
2900 Configuration to apply to the fake amplifier.
2901 """
2902
2903 def __init__(self, exposure, config):
2904 self._bbox = exposure.getBBox(afwImage.LOCAL)
2906 self._gain = config.gain
2907 self._readNoise = config.readNoise
2908 self._saturation = config.saturation
2909
2910 def getBBox(self):
2911 return self._bbox
2912
2913 def getRawBBox(self):
2914 return self._bbox
2915
2919 def getGain(self):
2920 return self._gain
2921
2922 def getReadNoise(self):
2923 return self._readNoise
2924
2925 def getSaturation(self):
2926 return self._saturation
2927
2929 return float("NaN")
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition Exposure.h:72
Represent a 2-dimensional array of bitmask pixels.
Definition Mask.h:82
Pass parameters to a Statistics object.
Definition Statistics.h:83
An integer coordinate rectangle.
Definition Box.h:55
__init__(self, exposure, config)
Definition isrTask.py:2903
maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR')
Definition isrTask.py:2593
updateVariance(self, ampExposure, amp, ptcDataset)
Definition isrTask.py:2345
maskAndInterpolateDefects(self, exposure, defectBaseList)
Definition isrTask.py:2627
roughZeroPoint(self, exposure)
Definition isrTask.py:2773
defineEffectivePtc(self, ptcDataset, detector, bfGains, overScans, metadata)
Definition isrTask.py:1878
runQuantum(self, butlerQC, inputRefs, outputRefs)
Definition isrTask.py:1038
ensureExposure(self, inputExp, camera=None, detectorNum=None)
Definition isrTask.py:2023
maskDefect(self, exposure, defectBaseList)
Definition isrTask.py:2569
convertIntToFloat(self, exposure)
Definition isrTask.py:2133
maskAndInterpolateNan(self, exposure)
Definition isrTask.py:2679
flatCorrection(self, exposure, flatExposure, invert=False)
Definition isrTask.py:2458
compareUnits(self, calibMetadata, calibName)
Definition isrTask.py:2102
debugView(self, exposure, stepname)
Definition isrTask.py:2868
measureBackground(self, exposure, IsrQaConfig=None)
Definition isrTask.py:2701
maskAmplifier(self, ccdExposure, amp, defects)
Definition isrTask.py:2170
saturationInterpolation(self, exposure)
Definition isrTask.py:2507
saturationDetection(self, exposure, amp)
Definition isrTask.py:2483
doLinearize(self, detector)
Definition isrTask.py:2439
darkCorrection(self, exposure, darkExposure, invert=False)
Definition isrTask.py:2396
run(self, ccdExposure, *, camera=None, bias=None, linearizer=None, crosstalk=None, crosstalkSources=None, dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None, fringes=pipeBase.Struct(fringes=None), opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None, detectorNum=None, strayLightData=None, illumMaskedImage=None, deferredChargeCalib=None)
Definition isrTask.py:1199
suspectDetection(self, exposure, amp)
Definition isrTask.py:2533
overscanCorrection(self, ccdExposure, amp)
Definition isrTask.py:2247
makeBinnedImages(self, exposure)
Definition isrTask.py:2840
flatContext(self, exp, flat, dark=None)
Definition isrTask.py:2803
maskNegativeVariance(self, exposure)
Definition isrTask.py:2380
std::shared_ptr< PhotoCalib > makePhotoCalibFromCalibZeroPoint(double instFluxMag0, double instFluxMag0Err)
Construct a PhotoCalib from the deprecated Calib-style instFluxMag0/instFluxMag0Err values.
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT > > image, typename std::shared_ptr< Mask< MaskPixelT > > mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT > > variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Definition Exposure.h:459
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
Definition binImage.cc:44
fluxConservingBrighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None, correctionMode=True)
brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None)
compareCameraKeywords(doRaiseOnCalibMismatch, cameraKeywordsToCompare, exposureMetadata, calib, calibName, log=None)
checkFilter(exposure, filterList, log)
crosstalkSourceLookup(datasetType, registry, quantumDataId, collections)
Definition isrTask.py:67
size_t maskNans(afw::image::MaskedImage< PixelT > const &mi, afw::image::MaskPixel maskVal, afw::image::MaskPixel allow=0)
Mask NANs in an image.
Definition Isr.cc:35