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