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