LSSTApplications  18.1.0
LSSTDataManagementBasePackage
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 import math
23 import numpy
24 
25 import lsst.geom
26 import lsst.afw.image as afwImage
27 import lsst.afw.math as afwMath
28 import lsst.afw.table as afwTable
29 import lsst.pex.config as pexConfig
30 import lsst.pipe.base as pipeBase
31 
32 from contextlib import contextmanager
33 from lsstDebug import getDebugFrame
34 
35 from lsst.afw.cameraGeom import PIXELS, FOCAL_PLANE, NullLinearityType
36 from lsst.afw.display import getDisplay
37 from lsst.afw.geom import Polygon
38 from lsst.daf.persistence import ButlerDataRef
39 from lsst.daf.persistence.butler import NoResults
40 from lsst.meas.algorithms.detection import SourceDetectionTask
41 from lsst.meas.algorithms import Defects
42 
43 from . import isrFunctions
44 from . import isrQa
45 from . import linearize
46 
47 from .assembleCcdTask import AssembleCcdTask
48 from .crosstalk import CrosstalkTask
49 from .fringe import FringeTask
50 from .isr import maskNans
51 from .masking import MaskingTask
52 from .straylight import StrayLightTask
53 from .vignette import VignetteTask
54 
55 __all__ = ["IsrTask", "IsrTaskConfig", "RunIsrTask", "RunIsrConfig"]
56 
57 
58 class IsrTaskConfig(pexConfig.Config):
59  """Configuration parameters for IsrTask.
60 
61  Items are grouped in the order in which they are executed by the task.
62  """
63  # General ISR configuration
64 
65  # gen3 options
66  isrName = pexConfig.Field(
67  dtype=str,
68  doc="Name of ISR",
69  default="ISR",
70  )
71 
72  # input datasets
73  ccdExposure = pipeBase.InputDatasetField(
74  doc="Input exposure to process",
75  name="raw",
76  scalar=True,
77  storageClass="Exposure",
78  dimensions=["instrument", "exposure", "detector"],
79  )
80  camera = pipeBase.InputDatasetField(
81  doc="Input camera to construct complete exposures.",
82  name="camera",
83  scalar=True,
84  storageClass="TablePersistableCamera",
85  dimensions=["instrument", "calibration_label"],
86  )
87  bias = pipeBase.InputDatasetField(
88  doc="Input bias calibration.",
89  name="bias",
90  scalar=True,
91  storageClass="ImageF",
92  dimensions=["instrument", "calibration_label", "detector"],
93  )
94  dark = pipeBase.InputDatasetField(
95  doc="Input dark calibration.",
96  name="dark",
97  scalar=True,
98  storageClass="ImageF",
99  dimensions=["instrument", "calibration_label", "detector"],
100  )
101  flat = pipeBase.InputDatasetField(
102  doc="Input flat calibration.",
103  name="flat",
104  scalar=True,
105  storageClass="MaskedImageF",
106  dimensions=["instrument", "physical_filter", "calibration_label", "detector"],
107  )
108  bfKernel = pipeBase.InputDatasetField(
109  doc="Input brighter-fatter kernel.",
110  name="bfKernel",
111  scalar=True,
112  storageClass="NumpyArray",
113  dimensions=["instrument", "calibration_label"],
114  )
115  defects = pipeBase.InputDatasetField(
116  doc="Input defect tables.",
117  name="defects",
118  scalar=True,
119  storageClass="DefectsList",
120  dimensions=["instrument", "calibration_label", "detector"],
121  )
122  opticsTransmission = pipeBase.InputDatasetField(
123  doc="Transmission curve due to the optics.",
124  name="transmission_optics",
125  scalar=True,
126  storageClass="TablePersistableTransmissionCurve",
127  dimensions=["instrument", "calibration_label"],
128  )
129  filterTransmission = pipeBase.InputDatasetField(
130  doc="Transmission curve due to the filter.",
131  name="transmission_filter",
132  scalar=True,
133  storageClass="TablePersistableTransmissionCurve",
134  dimensions=["instrument", "physical_filter", "calibration_label"],
135  )
136  sensorTransmission = pipeBase.InputDatasetField(
137  doc="Transmission curve due to the sensor.",
138  name="transmission_sensor",
139  scalar=True,
140  storageClass="TablePersistableTransmissionCurve",
141  dimensions=["instrument", "calibration_label", "detector"],
142  )
143  atmosphereTransmission = pipeBase.InputDatasetField(
144  doc="Transmission curve due to the atmosphere.",
145  name="transmission_atmosphere",
146  scalar=True,
147  storageClass="TablePersistableTransmissionCurve",
148  dimensions=["instrument"],
149  )
150 
151  # output datasets
152  outputExposure = pipeBase.OutputDatasetField(
153  doc="Output ISR processed exposure.",
154  name="postISRCCD",
155  scalar=True,
156  storageClass="ExposureF",
157  dimensions=["instrument", "visit", "detector"],
158  )
159  outputOssThumbnail = pipeBase.OutputDatasetField(
160  doc="Output Overscan-subtracted thumbnail image.",
161  name="OssThumb",
162  scalar=True,
163  storageClass="Thumbnail",
164  dimensions=["instrument", "visit", "detector"],
165  )
166  outputFlattenedThumbnail = pipeBase.OutputDatasetField(
167  doc="Output flat-corrected thumbnail image.",
168  name="FlattenedThumb",
169  scalar=True,
170  storageClass="TextStorage",
171  dimensions=["instrument", "visit", "detector"],
172  )
173 
174  quantum = pipeBase.QuantumConfig(
175  dimensions=["visit", "detector", "instrument"],
176  )
177 
178 
179  datasetType = pexConfig.Field(
180  dtype=str,
181  doc="Dataset type for input data; users will typically leave this alone, "
182  "but camera-specific ISR tasks will override it",
183  default="raw",
184  )
185 
186  fallbackFilterName = pexConfig.Field(
187  dtype=str,
188  doc="Fallback default filter name for calibrations.",
189  optional=True
190  )
191  expectWcs = pexConfig.Field(
192  dtype=bool,
193  default=True,
194  doc="Expect input science images to have a WCS (set False for e.g. spectrographs)."
195  )
196  fwhm = pexConfig.Field(
197  dtype=float,
198  doc="FWHM of PSF in arcseconds.",
199  default=1.0,
200  )
201  qa = pexConfig.ConfigField(
202  dtype=isrQa.IsrQaConfig,
203  doc="QA related configuration options.",
204  )
205 
206  # Image conversion configuration
207  doConvertIntToFloat = pexConfig.Field(
208  dtype=bool,
209  doc="Convert integer raw images to floating point values?",
210  default=True,
211  )
212 
213  # Saturated pixel handling.
214  doSaturation = pexConfig.Field(
215  dtype=bool,
216  doc="Mask saturated pixels? NB: this is totally independent of the"
217  " interpolation option - this is ONLY setting the bits in the mask."
218  " To have them interpolated make sure doSaturationInterpolation=True",
219  default=True,
220  )
221  saturatedMaskName = pexConfig.Field(
222  dtype=str,
223  doc="Name of mask plane to use in saturation detection and interpolation",
224  default="SAT",
225  )
226  saturation = pexConfig.Field(
227  dtype=float,
228  doc="The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
229  default=float("NaN"),
230  )
231  growSaturationFootprintSize = pexConfig.Field(
232  dtype=int,
233  doc="Number of pixels by which to grow the saturation footprints",
234  default=1,
235  )
236 
237  # Suspect pixel handling.
238  doSuspect = pexConfig.Field(
239  dtype=bool,
240  doc="Mask suspect pixels?",
241  default=True,
242  )
243  suspectMaskName = pexConfig.Field(
244  dtype=str,
245  doc="Name of mask plane to use for suspect pixels",
246  default="SUSPECT",
247  )
248  numEdgeSuspect = pexConfig.Field(
249  dtype=int,
250  doc="Number of edge pixels to be flagged as untrustworthy.",
251  default=0,
252  )
253 
254  # Initial masking options.
255  doSetBadRegions = pexConfig.Field(
256  dtype=bool,
257  doc="Should we set the level of all BAD patches of the chip to the chip's average value?",
258  default=True,
259  )
260  badStatistic = pexConfig.ChoiceField(
261  dtype=str,
262  doc="How to estimate the average value for BAD regions.",
263  default='MEANCLIP',
264  allowed={
265  "MEANCLIP": "Correct using the (clipped) mean of good data",
266  "MEDIAN": "Correct using the median of the good data",
267  },
268  )
269 
270  # Overscan subtraction configuration.
271  doOverscan = pexConfig.Field(
272  dtype=bool,
273  doc="Do overscan subtraction?",
274  default=True,
275  )
276  overscanFitType = pexConfig.ChoiceField(
277  dtype=str,
278  doc="The method for fitting the overscan bias level.",
279  default='MEDIAN',
280  allowed={
281  "POLY": "Fit ordinary polynomial to the longest axis of the overscan region",
282  "CHEB": "Fit Chebyshev polynomial to the longest axis of the overscan region",
283  "LEG": "Fit Legendre polynomial to the longest axis of the overscan region",
284  "NATURAL_SPLINE": "Fit natural spline to the longest axis of the overscan region",
285  "CUBIC_SPLINE": "Fit cubic spline to the longest axis of the overscan region",
286  "AKIMA_SPLINE": "Fit Akima spline to the longest axis of the overscan region",
287  "MEAN": "Correct using the mean of the overscan region",
288  "MEANCLIP": "Correct using a clipped mean of the overscan region",
289  "MEDIAN": "Correct using the median of the overscan region",
290  },
291  )
292  overscanOrder = pexConfig.Field(
293  dtype=int,
294  doc=("Order of polynomial or to fit if overscan fit type is a polynomial, " +
295  "or number of spline knots if overscan fit type is a spline."),
296  default=1,
297  )
298  overscanNumSigmaClip = pexConfig.Field(
299  dtype=float,
300  doc="Rejection threshold (sigma) for collapsing overscan before fit",
301  default=3.0,
302  )
303  overscanIsInt = pexConfig.Field(
304  dtype=bool,
305  doc="Treat overscan as an integer image for purposes of overscan.FitType=MEDIAN",
306  default=True,
307  )
308  overscanNumLeadingColumnsToSkip = pexConfig.Field(
309  dtype=int,
310  doc="Number of columns to skip in overscan, i.e. those closest to amplifier",
311  default=0,
312  )
313  overscanNumTrailingColumnsToSkip = pexConfig.Field(
314  dtype=int,
315  doc="Number of columns to skip in overscan, i.e. those farthest from amplifier",
316  default=0,
317  )
318  overscanMaxDev = pexConfig.Field(
319  dtype=float,
320  doc="Maximum deviation from the median for overscan",
321  default=1000.0, check=lambda x: x > 0
322  )
323  overscanBiasJump = pexConfig.Field(
324  dtype=bool,
325  doc="Fit the overscan in a piecewise-fashion to correct for bias jumps?",
326  default=False,
327  )
328  overscanBiasJumpKeyword = pexConfig.Field(
329  dtype=str,
330  doc="Header keyword containing information about devices.",
331  default="NO_SUCH_KEY",
332  )
333  overscanBiasJumpDevices = pexConfig.ListField(
334  dtype=str,
335  doc="List of devices that need piecewise overscan correction.",
336  default=(),
337  )
338  overscanBiasJumpLocation = pexConfig.Field(
339  dtype=int,
340  doc="Location of bias jump along y-axis.",
341  default=0,
342  )
343 
344  # Amplifier to CCD assembly configuration
345  doAssembleCcd = pexConfig.Field(
346  dtype=bool,
347  default=True,
348  doc="Assemble amp-level exposures into a ccd-level exposure?"
349  )
350  assembleCcd = pexConfig.ConfigurableField(
351  target=AssembleCcdTask,
352  doc="CCD assembly task",
353  )
354 
355  # General calibration configuration.
356  doAssembleIsrExposures = pexConfig.Field(
357  dtype=bool,
358  default=False,
359  doc="Assemble amp-level calibration exposures into ccd-level exposure?"
360  )
361  doTrimToMatchCalib = pexConfig.Field(
362  dtype=bool,
363  default=False,
364  doc="Trim raw data to match calibration bounding boxes?"
365  )
366 
367  # Bias subtraction.
368  doBias = pexConfig.Field(
369  dtype=bool,
370  doc="Apply bias frame correction?",
371  default=True,
372  )
373  biasDataProductName = pexConfig.Field(
374  dtype=str,
375  doc="Name of the bias data product",
376  default="bias",
377  )
378 
379  # Variance construction
380  doVariance = pexConfig.Field(
381  dtype=bool,
382  doc="Calculate variance?",
383  default=True
384  )
385  gain = pexConfig.Field(
386  dtype=float,
387  doc="The gain to use if no Detector is present in the Exposure (ignored if NaN)",
388  default=float("NaN"),
389  )
390  readNoise = pexConfig.Field(
391  dtype=float,
392  doc="The read noise to use if no Detector is present in the Exposure",
393  default=0.0,
394  )
395  doEmpiricalReadNoise = pexConfig.Field(
396  dtype=bool,
397  default=False,
398  doc="Calculate empirical read noise instead of value from AmpInfo data?"
399  )
400 
401  # Linearization.
402  doLinearize = pexConfig.Field(
403  dtype=bool,
404  doc="Correct for nonlinearity of the detector's response?",
405  default=True,
406  )
407 
408  # Crosstalk.
409  doCrosstalk = pexConfig.Field(
410  dtype=bool,
411  doc="Apply intra-CCD crosstalk correction?",
412  default=False,
413  )
414  doCrosstalkBeforeAssemble = pexConfig.Field(
415  dtype=bool,
416  doc="Apply crosstalk correction before CCD assembly, and before trimming?",
417  default=True,
418  )
419  crosstalk = pexConfig.ConfigurableField(
420  target=CrosstalkTask,
421  doc="Intra-CCD crosstalk correction",
422  )
423 
424  # Masking options.
425  doDefect = pexConfig.Field(
426  dtype=bool,
427  doc="Apply correction for CCD defects, e.g. hot pixels?",
428  default=True,
429  )
430  numEdgeSuspect = pexConfig.Field(
431  dtype=int,
432  doc="Number of edge pixels to be flagged as untrustworthy.",
433  default=0,
434  )
435  doNanMasking = pexConfig.Field(
436  dtype=bool,
437  doc="Mask NAN pixels?",
438  default=True,
439  )
440  doWidenSaturationTrails = pexConfig.Field(
441  dtype=bool,
442  doc="Widen bleed trails based on their width?",
443  default=True
444  )
445 
446  # Brighter-Fatter correction.
447  doBrighterFatter = pexConfig.Field(
448  dtype=bool,
449  default=False,
450  doc="Apply the brighter fatter correction"
451  )
452  brighterFatterLevel = pexConfig.ChoiceField(
453  dtype=str,
454  default="DETECTOR",
455  doc="The level at which to correct for brighter-fatter.",
456  allowed={
457  "AMP": "Every amplifier treated separately.",
458  "DETECTOR": "One kernel per detector",
459  }
460  )
461  brighterFatterKernelFile = pexConfig.Field(
462  dtype=str,
463  default='',
464  doc="Kernel file used for the brighter fatter correction"
465  )
466  brighterFatterMaxIter = pexConfig.Field(
467  dtype=int,
468  default=10,
469  doc="Maximum number of iterations for the brighter fatter correction"
470  )
471  brighterFatterThreshold = pexConfig.Field(
472  dtype=float,
473  default=1000,
474  doc="Threshold used to stop iterating the brighter fatter correction. It is the "
475  " absolute value of the difference between the current corrected image and the one"
476  " from the previous iteration summed over all the pixels."
477  )
478  brighterFatterApplyGain = pexConfig.Field(
479  dtype=bool,
480  default=True,
481  doc="Should the gain be applied when applying the brighter fatter correction?"
482  )
483 
484  # Dark subtraction.
485  doDark = pexConfig.Field(
486  dtype=bool,
487  doc="Apply dark frame correction?",
488  default=True,
489  )
490  darkDataProductName = pexConfig.Field(
491  dtype=str,
492  doc="Name of the dark data product",
493  default="dark",
494  )
495 
496  # Camera-specific stray light removal.
497  doStrayLight = pexConfig.Field(
498  dtype=bool,
499  doc="Subtract stray light in the y-band (due to encoder LEDs)?",
500  default=False,
501  )
502  strayLight = pexConfig.ConfigurableField(
503  target=StrayLightTask,
504  doc="y-band stray light correction"
505  )
506 
507  # Flat correction.
508  doFlat = pexConfig.Field(
509  dtype=bool,
510  doc="Apply flat field correction?",
511  default=True,
512  )
513  flatDataProductName = pexConfig.Field(
514  dtype=str,
515  doc="Name of the flat data product",
516  default="flat",
517  )
518  flatScalingType = pexConfig.ChoiceField(
519  dtype=str,
520  doc="The method for scaling the flat on the fly.",
521  default='USER',
522  allowed={
523  "USER": "Scale by flatUserScale",
524  "MEAN": "Scale by the inverse of the mean",
525  "MEDIAN": "Scale by the inverse of the median",
526  },
527  )
528  flatUserScale = pexConfig.Field(
529  dtype=float,
530  doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
531  default=1.0,
532  )
533  doTweakFlat = pexConfig.Field(
534  dtype=bool,
535  doc="Tweak flats to match observed amplifier ratios?",
536  default=False
537  )
538 
539  # Amplifier normalization based on gains instead of using flats configuration.
540  doApplyGains = pexConfig.Field(
541  dtype=bool,
542  doc="Correct the amplifiers for their gains instead of applying flat correction",
543  default=False,
544  )
545  normalizeGains = pexConfig.Field(
546  dtype=bool,
547  doc="Normalize all the amplifiers in each CCD to have the same median value.",
548  default=False,
549  )
550 
551  # Fringe correction.
552  doFringe = pexConfig.Field(
553  dtype=bool,
554  doc="Apply fringe correction?",
555  default=True,
556  )
557  fringe = pexConfig.ConfigurableField(
558  target=FringeTask,
559  doc="Fringe subtraction task",
560  )
561  fringeAfterFlat = pexConfig.Field(
562  dtype=bool,
563  doc="Do fringe subtraction after flat-fielding?",
564  default=True,
565  )
566 
567  # Distortion model application.
568  doAddDistortionModel = pexConfig.Field(
569  dtype=bool,
570  doc="Apply a distortion model based on camera geometry to the WCS?",
571  default=True,
572  )
573 
574  # Initial CCD-level background statistics options.
575  doMeasureBackground = pexConfig.Field(
576  dtype=bool,
577  doc="Measure the background level on the reduced image?",
578  default=False,
579  )
580 
581  # Camera-specific masking configuration.
582  doCameraSpecificMasking = pexConfig.Field(
583  dtype=bool,
584  doc="Mask camera-specific bad regions?",
585  default=False,
586  )
587  masking = pexConfig.ConfigurableField(
588  target=MaskingTask,
589  doc="Masking task."
590  )
591 
592  # Interpolation options.
593 
594  doInterpolate = pexConfig.Field(
595  dtype=bool,
596  doc="Interpolate masked pixels?",
597  default=True,
598  )
599  doSaturationInterpolation = pexConfig.Field(
600  dtype=bool,
601  doc="Perform interpolation over pixels masked as saturated?"
602  " NB: This is independent of doSaturation; if that is False this plane"
603  " will likely be blank, resulting in a no-op here.",
604  default=True,
605  )
606  doNanInterpolation = pexConfig.Field(
607  dtype=bool,
608  doc="Perform interpolation over pixels masked as NaN?"
609  " NB: This is independent of doNanMasking; if that is False this plane"
610  " will likely be blank, resulting in a no-op here.",
611  default=True,
612  )
613  doNanInterpAfterFlat = pexConfig.Field(
614  dtype=bool,
615  doc=("If True, ensure we interpolate NaNs after flat-fielding, even if we "
616  "also have to interpolate them before flat-fielding."),
617  default=False,
618  )
619  maskListToInterpolate = pexConfig.ListField(
620  dtype=str,
621  doc="List of mask planes that should be interpolated.",
622  default=['SAT', 'BAD', 'UNMASKEDNAN'],
623  )
624  doSaveInterpPixels = pexConfig.Field(
625  dtype=bool,
626  doc="Save a copy of the pre-interpolated pixel values?",
627  default=False,
628  )
629 
630  # Default photometric calibration options.
631  fluxMag0T1 = pexConfig.DictField(
632  keytype=str,
633  itemtype=float,
634  doc="The approximate flux of a zero-magnitude object in a one-second exposure, per filter.",
635  default=dict((f, pow(10.0, 0.4*m)) for f, m in (("Unknown", 28.0),
636  ))
637  )
638  defaultFluxMag0T1 = pexConfig.Field(
639  dtype=float,
640  doc="Default value for fluxMag0T1 (for an unrecognized filter).",
641  default=pow(10.0, 0.4*28.0)
642  )
643 
644  # Vignette correction configuration.
645  doVignette = pexConfig.Field(
646  dtype=bool,
647  doc="Apply vignetting parameters?",
648  default=False,
649  )
650  vignette = pexConfig.ConfigurableField(
651  target=VignetteTask,
652  doc="Vignetting task.",
653  )
654 
655  # Transmission curve configuration.
656  doAttachTransmissionCurve = pexConfig.Field(
657  dtype=bool,
658  default=False,
659  doc="Construct and attach a wavelength-dependent throughput curve for this CCD image?"
660  )
661  doUseOpticsTransmission = pexConfig.Field(
662  dtype=bool,
663  default=True,
664  doc="Load and use transmission_optics (if doAttachTransmissionCurve is True)?"
665  )
666  doUseFilterTransmission = pexConfig.Field(
667  dtype=bool,
668  default=True,
669  doc="Load and use transmission_filter (if doAttachTransmissionCurve is True)?"
670  )
671  doUseSensorTransmission = pexConfig.Field(
672  dtype=bool,
673  default=True,
674  doc="Load and use transmission_sensor (if doAttachTransmissionCurve is True)?"
675  )
676  doUseAtmosphereTransmission = pexConfig.Field(
677  dtype=bool,
678  default=True,
679  doc="Load and use transmission_atmosphere (if doAttachTransmissionCurve is True)?"
680  )
681 
682  # Write the outputs to disk. If ISR is run as a subtask, this may not be needed.
683  doWrite = pexConfig.Field(
684  dtype=bool,
685  doc="Persist postISRCCD?",
686  default=True,
687  )
688 
689  def validate(self):
690  super().validate()
691  if self.doFlat and self.doApplyGains:
692  raise ValueError("You may not specify both doFlat and doApplyGains")
693  if self.doSaturationInterpolation and "SAT" not in self.maskListToInterpolate:
694  self.config.maskListToInterpolate.append("SAT")
695  if self.doNanInterpolation and "UNMASKEDNAN" not in self.maskListToInterpolate:
696  self.config.maskListToInterpolate.append("UNMASKEDNAN")
697 
698 
699 class IsrTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
700  """Apply common instrument signature correction algorithms to a raw frame.
701 
702  The process for correcting imaging data is very similar from
703  camera to camera. This task provides a vanilla implementation of
704  doing these corrections, including the ability to turn certain
705  corrections off if they are not needed. The inputs to the primary
706  method, `run()`, are a raw exposure to be corrected and the
707  calibration data products. The raw input is a single chip sized
708  mosaic of all amps including overscans and other non-science
709  pixels. The method `runDataRef()` identifies and defines the
710  calibration data products, and is intended for use by a
711  `lsst.pipe.base.cmdLineTask.CmdLineTask` and takes as input only a
712  `daf.persistence.butlerSubset.ButlerDataRef`. This task may be
713  subclassed for different camera, although the most camera specific
714  methods have been split into subtasks that can be redirected
715  appropriately.
716 
717  The __init__ method sets up the subtasks for ISR processing, using
718  the defaults from `lsst.ip.isr`.
719 
720  Parameters
721  ----------
722  args : `list`
723  Positional arguments passed to the Task constructor. None used at this time.
724  kwargs : `dict`, optional
725  Keyword arguments passed on to the Task constructor. None used at this time.
726  """
727  ConfigClass = IsrTaskConfig
728  _DefaultName = "isr"
729 
730  def __init__(self, **kwargs):
731  super().__init__(**kwargs)
732  self.makeSubtask("assembleCcd")
733  self.makeSubtask("crosstalk")
734  self.makeSubtask("strayLight")
735  self.makeSubtask("fringe")
736  self.makeSubtask("masking")
737  self.makeSubtask("vignette")
738 
739  @classmethod
740  def getInputDatasetTypes(cls, config):
741  inputTypeDict = super().getInputDatasetTypes(config)
742 
743  # Delete entries from the dictionary of InputDatasetTypes that we know we don't
744  # need because the configuration tells us we will not be bothering with the
745  # correction that uses that IDT.
746  if config.doBias is not True:
747  inputTypeDict.pop("bias", None)
748  if config.doLinearize is not True:
749  inputTypeDict.pop("linearizer", None)
750  if config.doCrosstalk is not True:
751  inputTypeDict.pop("crosstalkSources", None)
752  if config.doBrighterFatter is not True:
753  inputTypeDict.pop("bfKernel", None)
754  if config.doDefect is not True:
755  inputTypeDict.pop("defects", None)
756  if config.doDark is not True:
757  inputTypeDict.pop("dark", None)
758  if config.doFlat is not True:
759  inputTypeDict.pop("flat", None)
760  if config.doAttachTransmissionCurve is not True:
761  inputTypeDict.pop("opticsTransmission", None)
762  inputTypeDict.pop("filterTransmission", None)
763  inputTypeDict.pop("sensorTransmission", None)
764  inputTypeDict.pop("atmosphereTransmission", None)
765  if config.doUseOpticsTransmission is not True:
766  inputTypeDict.pop("opticsTransmission", None)
767  if config.doUseFilterTransmission is not True:
768  inputTypeDict.pop("filterTransmission", None)
769  if config.doUseSensorTransmission is not True:
770  inputTypeDict.pop("sensorTransmission", None)
771  if config.doUseAtmosphereTransmission is not True:
772  inputTypeDict.pop("atmosphereTransmission", None)
773 
774  return inputTypeDict
775 
776  @classmethod
777  def getOutputDatasetTypes(cls, config):
778  outputTypeDict = super().getOutputDatasetTypes(config)
779 
780  if config.qa.doThumbnailOss is not True:
781  outputTypeDict.pop("outputOssThumbnail", None)
782  if config.qa.doThumbnailFlattened is not True:
783  outputTypeDict.pop("outputFlattenedThumbnail", None)
784  if config.doWrite is not True:
785  outputTypeDict.pop("outputExposure", None)
786 
787  return outputTypeDict
788 
789  @classmethod
790  def getPrerequisiteDatasetTypes(cls, config):
791  # Input calibration datasets should not constrain the QuantumGraph
792  # (it'd be confusing if not having flats just silently resulted in no
793  # data being processed). Our nomenclature for that is that these are
794  # "prerequisite" datasets (only "ccdExposure" == "raw" isn't).
795  names = set(cls.getInputDatasetTypes(config))
796  names.remove("ccdExposure")
797  return names
798 
799  @classmethod
800  def getPerDatasetTypeDimensions(cls, config):
801  # Input calibration datasets of different types (i.e. flat, bias) need
802  # not have the same validity range. That makes calibration_label
803  # (which maps directly to a validity range) a "per-DatasetType
804  # dimension".
805  return frozenset(["calibration_label"])
806 
807  def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler):
808  try:
809  inputData['detectorNum'] = int(inputDataIds['ccdExposure']['detector'])
810  except Exception as e:
811  raise ValueError(f"Failure to find valid detectorNum value for Dataset {inputDataIds}: {e}")
812 
813  inputData['isGen3'] = True
814 
815  if self.config.doLinearize is True:
816  if 'linearizer' not in inputData.keys():
817  detector = inputData['camera'][inputData['detectorNum']]
818  linearityName = detector.getAmpInfoCatalog()[0].getLinearityType()
819  inputData['linearizer'] = linearize.getLinearityTypeByName(linearityName)()
820 
821  if inputData['defects'] is not None:
822  # defects is loaded as a BaseCatalog with columns x0, y0, width, height.
823  # masking expects a list of defects defined by their bounding box
824  if not isinstance(inputData["defects"], Defects):
825  inputData["defects"] = Defects.fromTable(inputData["defects"])
826 
827  # Broken: DM-17169
828  # ci_hsc does not use crosstalkSources, as it's intra-CCD CT only. This needs to be
829  # fixed for non-HSC cameras in the future.
830  # inputData['crosstalkSources'] = (self.crosstalk.prepCrosstalk(inputDataIds['ccdExposure'])
831  # if self.config.doCrosstalk else None)
832 
833  # Broken: DM-17152
834  # Fringes are not tested to be handled correctly by Gen3 butler.
835  # inputData['fringes'] = (self.fringe.readFringes(inputDataIds['ccdExposure'],
836  # assembler=self.assembleCcd
837  # if self.config.doAssembleIsrExposures else None)
838  # if self.config.doFringe and
839  # self.fringe.checkFilter(inputData['ccdExposure'])
840  # else pipeBase.Struct(fringes=None))
841 
842  return super().adaptArgsAndRun(inputData, inputDataIds, outputDataIds, butler)
843 
844  def makeDatasetType(self, dsConfig):
845  return super().makeDatasetType(dsConfig)
846 
847  def readIsrData(self, dataRef, rawExposure):
848  """!Retrieve necessary frames for instrument signature removal.
849 
850  Pre-fetching all required ISR data products limits the IO
851  required by the ISR. Any conflict between the calibration data
852  available and that needed for ISR is also detected prior to
853  doing processing, allowing it to fail quickly.
854 
855  Parameters
856  ----------
857  dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
858  Butler reference of the detector data to be processed
859  rawExposure : `afw.image.Exposure`
860  The raw exposure that will later be corrected with the
861  retrieved calibration data; should not be modified in this
862  method.
863 
864  Returns
865  -------
866  result : `lsst.pipe.base.Struct`
867  Result struct with components (which may be `None`):
868  - ``bias``: bias calibration frame (`afw.image.Exposure`)
869  - ``linearizer``: functor for linearization (`ip.isr.linearize.LinearizeBase`)
870  - ``crosstalkSources``: list of possible crosstalk sources (`list`)
871  - ``dark``: dark calibration frame (`afw.image.Exposure`)
872  - ``flat``: flat calibration frame (`afw.image.Exposure`)
873  - ``bfKernel``: Brighter-Fatter kernel (`numpy.ndarray`)
874  - ``defects``: list of defects (`lsst.meas.algorithms.Defects`)
875  - ``fringes``: `lsst.pipe.base.Struct` with components:
876  - ``fringes``: fringe calibration frame (`afw.image.Exposure`)
877  - ``seed``: random seed derived from the ccdExposureId for random
878  number generator (`uint32`)
879  - ``opticsTransmission``: `lsst.afw.image.TransmissionCurve`
880  A ``TransmissionCurve`` that represents the throughput of the optics,
881  to be evaluated in focal-plane coordinates.
882  - ``filterTransmission`` : `lsst.afw.image.TransmissionCurve`
883  A ``TransmissionCurve`` that represents the throughput of the filter
884  itself, to be evaluated in focal-plane coordinates.
885  - ``sensorTransmission`` : `lsst.afw.image.TransmissionCurve`
886  A ``TransmissionCurve`` that represents the throughput of the sensor
887  itself, to be evaluated in post-assembly trimmed detector coordinates.
888  - ``atmosphereTransmission`` : `lsst.afw.image.TransmissionCurve`
889  A ``TransmissionCurve`` that represents the throughput of the
890  atmosphere, assumed to be spatially constant.
891  - ``strayLightData`` : `object`
892  An opaque object containing calibration information for
893  stray-light correction. If `None`, no correction will be
894  performed.
895 
896  Raises
897  ------
898  NotImplementedError :
899  Raised if a per-amplifier brighter-fatter kernel is requested by the configuration.
900  """
901  ccd = rawExposure.getDetector()
902  rawExposure.mask.addMaskPlane("UNMASKEDNAN") # needed to match pre DM-15862 processing.
903  biasExposure = (self.getIsrExposure(dataRef, self.config.biasDataProductName)
904  if self.config.doBias else None)
905  # immediate=True required for functors and linearizers are functors; see ticket DM-6515
906  linearizer = (dataRef.get("linearizer", immediate=True)
907  if self.doLinearize(ccd) else None)
908  crosstalkSources = (self.crosstalk.prepCrosstalk(dataRef)
909  if self.config.doCrosstalk else None)
910  darkExposure = (self.getIsrExposure(dataRef, self.config.darkDataProductName)
911  if self.config.doDark else None)
912  flatExposure = (self.getIsrExposure(dataRef, self.config.flatDataProductName)
913  if self.config.doFlat else None)
914 
915  brighterFatterKernel = None
916  if self.config.doBrighterFatter is True:
917 
918  # Use the new-style cp_pipe version of the kernel is it exists.
919  try:
920  brighterFatterKernel = dataRef.get("brighterFatterKernel")
921  except NoResults:
922  # Fall back to the old-style numpy-ndarray style kernel if necessary.
923  try:
924  brighterFatterKernel = dataRef.get("bfKernel")
925  except NoResults:
926  brighterFatterKernel = None
927  if brighterFatterKernel is not None and not isinstance(brighterFatterKernel, numpy.ndarray):
928  # If the kernel is not an ndarray, it's the cp_pipe version, so extract the kernel for
929  # this detector, or raise an error.
930  if self.config.brighterFatterLevel == 'DETECTOR':
931  brighterFatterKernel = brighterFatterKernel.kernel[ccd.getId()]
932  else:
933  # TODO DM-15631 for implementing this
934  raise NotImplementedError("Per-amplifier brighter-fatter correction not implemented")
935 
936  defectList = (dataRef.get("defects")
937  if self.config.doDefect else None)
938  fringeStruct = (self.fringe.readFringes(dataRef, assembler=self.assembleCcd
939  if self.config.doAssembleIsrExposures else None)
940  if self.config.doFringe and self.fringe.checkFilter(rawExposure)
941  else pipeBase.Struct(fringes=None))
942 
943  if self.config.doAttachTransmissionCurve:
944  opticsTransmission = (dataRef.get("transmission_optics")
945  if self.config.doUseOpticsTransmission else None)
946  filterTransmission = (dataRef.get("transmission_filter")
947  if self.config.doUseFilterTransmission else None)
948  sensorTransmission = (dataRef.get("transmission_sensor")
949  if self.config.doUseSensorTransmission else None)
950  atmosphereTransmission = (dataRef.get("transmission_atmosphere")
951  if self.config.doUseAtmosphereTransmission else None)
952  else:
953  opticsTransmission = None
954  filterTransmission = None
955  sensorTransmission = None
956  atmosphereTransmission = None
957 
958  if self.config.doStrayLight:
959  strayLightData = self.strayLight.readIsrData(dataRef, rawExposure)
960  else:
961  strayLightData = None
962 
963  # Struct should include only kwargs to run()
964  return pipeBase.Struct(bias=biasExposure,
965  linearizer=linearizer,
966  crosstalkSources=crosstalkSources,
967  dark=darkExposure,
968  flat=flatExposure,
969  bfKernel=brighterFatterKernel,
970  defects=defectList,
971  fringes=fringeStruct,
972  opticsTransmission=opticsTransmission,
973  filterTransmission=filterTransmission,
974  sensorTransmission=sensorTransmission,
975  atmosphereTransmission=atmosphereTransmission,
976  strayLightData=strayLightData
977  )
978 
979  @pipeBase.timeMethod
980  def run(self, ccdExposure, camera=None, bias=None, linearizer=None, crosstalkSources=None,
981  dark=None, flat=None, bfKernel=None, defects=None, fringes=None,
982  opticsTransmission=None, filterTransmission=None,
983  sensorTransmission=None, atmosphereTransmission=None,
984  detectorNum=None, strayLightData=None, isGen3=False,
985  ):
986  """!Perform instrument signature removal on an exposure.
987 
988  Steps included in the ISR processing, in order performed, are:
989  - saturation and suspect pixel masking
990  - overscan subtraction
991  - CCD assembly of individual amplifiers
992  - bias subtraction
993  - variance image construction
994  - linearization of non-linear response
995  - crosstalk masking
996  - brighter-fatter correction
997  - dark subtraction
998  - fringe correction
999  - stray light subtraction
1000  - flat correction
1001  - masking of known defects and camera specific features
1002  - vignette calculation
1003  - appending transmission curve and distortion model
1004 
1005  Parameters
1006  ----------
1007  ccdExposure : `lsst.afw.image.Exposure`
1008  The raw exposure that is to be run through ISR. The
1009  exposure is modified by this method.
1010  camera : `lsst.afw.cameraGeom.Camera`, optional
1011  The camera geometry for this exposure. Used to select the
1012  distortion model appropriate for this data.
1013  bias : `lsst.afw.image.Exposure`, optional
1014  Bias calibration frame.
1015  linearizer : `lsst.ip.isr.linearize.LinearizeBase`, optional
1016  Functor for linearization.
1017  crosstalkSources : `list`, optional
1018  List of possible crosstalk sources.
1019  dark : `lsst.afw.image.Exposure`, optional
1020  Dark calibration frame.
1021  flat : `lsst.afw.image.Exposure`, optional
1022  Flat calibration frame.
1023  bfKernel : `numpy.ndarray`, optional
1024  Brighter-fatter kernel.
1025  defects : `lsst.meas.algorithms.Defects`, optional
1026  List of defects.
1027  fringes : `lsst.pipe.base.Struct`, optional
1028  Struct containing the fringe correction data, with
1029  elements:
1030  - ``fringes``: fringe calibration frame (`afw.image.Exposure`)
1031  - ``seed``: random seed derived from the ccdExposureId for random
1032  number generator (`uint32`)
1033  opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional
1034  A ``TransmissionCurve`` that represents the throughput of the optics,
1035  to be evaluated in focal-plane coordinates.
1036  filterTransmission : `lsst.afw.image.TransmissionCurve`
1037  A ``TransmissionCurve`` that represents the throughput of the filter
1038  itself, to be evaluated in focal-plane coordinates.
1039  sensorTransmission : `lsst.afw.image.TransmissionCurve`
1040  A ``TransmissionCurve`` that represents the throughput of the sensor
1041  itself, to be evaluated in post-assembly trimmed detector coordinates.
1042  atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1043  A ``TransmissionCurve`` that represents the throughput of the
1044  atmosphere, assumed to be spatially constant.
1045  detectorNum : `int`, optional
1046  The integer number for the detector to process.
1047  isGen3 : bool, optional
1048  Flag this call to run() as using the Gen3 butler environment.
1049  strayLightData : `object`, optional
1050  Opaque object containing calibration information for stray-light
1051  correction. If `None`, no correction will be performed.
1052 
1053  Returns
1054  -------
1055  result : `lsst.pipe.base.Struct`
1056  Result struct with component:
1057  - ``exposure`` : `afw.image.Exposure`
1058  The fully ISR corrected exposure.
1059  - ``outputExposure`` : `afw.image.Exposure`
1060  An alias for `exposure`
1061  - ``ossThumb`` : `numpy.ndarray`
1062  Thumbnail image of the exposure after overscan subtraction.
1063  - ``flattenedThumb`` : `numpy.ndarray`
1064  Thumbnail image of the exposure after flat-field correction.
1065 
1066  Raises
1067  ------
1068  RuntimeError
1069  Raised if a configuration option is set to True, but the
1070  required calibration data has not been specified.
1071 
1072  Notes
1073  -----
1074  The current processed exposure can be viewed by setting the
1075  appropriate lsstDebug entries in the `debug.display`
1076  dictionary. The names of these entries correspond to some of
1077  the IsrTaskConfig Boolean options, with the value denoting the
1078  frame to use. The exposure is shown inside the matching
1079  option check and after the processing of that step has
1080  finished. The steps with debug points are:
1081 
1082  doAssembleCcd
1083  doBias
1084  doCrosstalk
1085  doBrighterFatter
1086  doDark
1087  doFringe
1088  doStrayLight
1089  doFlat
1090 
1091  In addition, setting the "postISRCCD" entry displays the
1092  exposure after all ISR processing has finished.
1093 
1094  """
1095 
1096  if isGen3 is True:
1097  # Gen3 currently cannot automatically do configuration overrides.
1098  # DM-15257 looks to discuss this issue.
1099 
1100  self.config.doFringe = False
1101 
1102  # Configure input exposures;
1103  if detectorNum is None:
1104  raise RuntimeError("Must supply the detectorNum if running as Gen3")
1105 
1106  ccdExposure = self.ensureExposure(ccdExposure, camera, detectorNum)
1107  bias = self.ensureExposure(bias, camera, detectorNum)
1108  dark = self.ensureExposure(dark, camera, detectorNum)
1109  flat = self.ensureExposure(flat, camera, detectorNum)
1110  else:
1111  if isinstance(ccdExposure, ButlerDataRef):
1112  return self.runDataRef(ccdExposure)
1113 
1114  ccd = ccdExposure.getDetector()
1115 
1116  if not ccd:
1117  assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd"
1118  ccd = [FakeAmp(ccdExposure, self.config)]
1119 
1120  # Validate Input
1121  if self.config.doBias and bias is None:
1122  raise RuntimeError("Must supply a bias exposure if config.doBias=True.")
1123  if self.doLinearize(ccd) and linearizer is None:
1124  raise RuntimeError("Must supply a linearizer if config.doLinearize=True for this detector.")
1125  if self.config.doBrighterFatter and bfKernel is None:
1126  raise RuntimeError("Must supply a kernel if config.doBrighterFatter=True.")
1127  if self.config.doDark and dark is None:
1128  raise RuntimeError("Must supply a dark exposure if config.doDark=True.")
1129  if fringes is None:
1130  fringes = pipeBase.Struct(fringes=None)
1131  if self.config.doFringe and not isinstance(fringes, pipeBase.Struct):
1132  raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct.")
1133  if self.config.doFlat and flat is None:
1134  raise RuntimeError("Must supply a flat exposure if config.doFlat=True.")
1135  if self.config.doDefect and defects is None:
1136  raise RuntimeError("Must supply defects if config.doDefect=True.")
1137  if self.config.doAddDistortionModel and camera is None:
1138  raise RuntimeError("Must supply camera if config.doAddDistortionModel=True.")
1139 
1140  # Begin ISR processing.
1141  if self.config.doConvertIntToFloat:
1142  self.log.info("Converting exposure to floating point values")
1143  ccdExposure = self.convertIntToFloat(ccdExposure)
1144 
1145  # Amplifier level processing.
1146  overscans = []
1147  for amp in ccd:
1148  # if ccdExposure is one amp, check for coverage to prevent performing ops multiple times
1149  if ccdExposure.getBBox().contains(amp.getBBox()):
1150  # Check for fully masked bad amplifiers, and generate masks for SUSPECT and SATURATED values.
1151  badAmp = self.maskAmplifier(ccdExposure, amp, defects)
1152 
1153  if self.config.doOverscan and not badAmp:
1154  # Overscan correction on amp-by-amp basis.
1155  overscanResults = self.overscanCorrection(ccdExposure, amp)
1156  self.log.debug("Corrected overscan for amplifier %s" % (amp.getName()))
1157  if self.config.qa is not None and self.config.qa.saveStats is True:
1158  if isinstance(overscanResults.overscanFit, float):
1159  qaMedian = overscanResults.overscanFit
1160  qaStdev = float("NaN")
1161  else:
1162  qaStats = afwMath.makeStatistics(overscanResults.overscanFit,
1163  afwMath.MEDIAN | afwMath.STDEVCLIP)
1164  qaMedian = qaStats.getValue(afwMath.MEDIAN)
1165  qaStdev = qaStats.getValue(afwMath.STDEVCLIP)
1166 
1167  self.metadata.set("ISR OSCAN {} MEDIAN".format(amp.getName()), qaMedian)
1168  self.metadata.set("ISR OSCAN {} STDEV".format(amp.getName()), qaStdev)
1169  self.log.debug(" Overscan stats for amplifer %s: %f +/- %f" %
1170  (amp.getName(), qaMedian, qaStdev))
1171  ccdExposure.getMetadata().set('OVERSCAN', "Overscan corrected")
1172  else:
1173  if badAmp:
1174  self.log.warn("Amplifier %s is bad." % (amp.getName()))
1175  overscanResults = None
1176 
1177  overscans.append(overscanResults if overscanResults is not None else None)
1178  else:
1179  self.log.info("Skipped OSCAN")
1180 
1181  if self.config.doCrosstalk and self.config.doCrosstalkBeforeAssemble:
1182  self.log.info("Applying crosstalk correction.")
1183  self.crosstalk.run(ccdExposure, crosstalkSources=crosstalkSources)
1184  self.debugView(ccdExposure, "doCrosstalk")
1185 
1186  if self.config.doAssembleCcd:
1187  self.log.info("Assembling CCD from amplifiers")
1188  ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1189 
1190  if self.config.expectWcs and not ccdExposure.getWcs():
1191  self.log.warn("No WCS found in input exposure")
1192  self.debugView(ccdExposure, "doAssembleCcd")
1193 
1194  ossThumb = None
1195  if self.config.qa.doThumbnailOss:
1196  ossThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1197 
1198  if self.config.doBias:
1199  self.log.info("Applying bias correction.")
1200  isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1201  trimToFit=self.config.doTrimToMatchCalib)
1202  self.debugView(ccdExposure, "doBias")
1203 
1204  if self.config.doVariance:
1205  for amp, overscanResults in zip(ccd, overscans):
1206  if ccdExposure.getBBox().contains(amp.getBBox()):
1207  self.log.debug("Constructing variance map for amplifer %s" % (amp.getName()))
1208  ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1209  if overscanResults is not None:
1210  self.updateVariance(ampExposure, amp,
1211  overscanImage=overscanResults.overscanImage)
1212  else:
1213  self.updateVariance(ampExposure, amp,
1214  overscanImage=None)
1215  if self.config.qa is not None and self.config.qa.saveStats is True:
1216  qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
1217  afwMath.MEDIAN | afwMath.STDEVCLIP)
1218  self.metadata.set("ISR VARIANCE {} MEDIAN".format(amp.getName()),
1219  qaStats.getValue(afwMath.MEDIAN))
1220  self.metadata.set("ISR VARIANCE {} STDEV".format(amp.getName()),
1221  qaStats.getValue(afwMath.STDEVCLIP))
1222  self.log.debug(" Variance stats for amplifer %s: %f +/- %f" %
1223  (amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1224  qaStats.getValue(afwMath.STDEVCLIP)))
1225 
1226  if self.doLinearize(ccd):
1227  self.log.info("Applying linearizer.")
1228  linearizer(image=ccdExposure.getMaskedImage().getImage(), detector=ccd, log=self.log)
1229 
1230  if self.config.doCrosstalk and not self.config.doCrosstalkBeforeAssemble:
1231  self.log.info("Applying crosstalk correction.")
1232  self.crosstalk.run(ccdExposure, crosstalkSources=crosstalkSources, isTrimmed=True)
1233  self.debugView(ccdExposure, "doCrosstalk")
1234 
1235  # Masking block. Optionally mask known defects, NAN pixels, widen trails, and do
1236  # anything else the camera needs. Saturated and suspect pixels have already been masked.
1237  if self.config.doDefect:
1238  self.log.info("Masking defects.")
1239  self.maskDefect(ccdExposure, defects)
1240 
1241  if self.config.doNanMasking:
1242  self.log.info("Masking NAN value pixels.")
1243  self.maskNan(ccdExposure)
1244 
1245  if self.config.doWidenSaturationTrails:
1246  self.log.info("Widening saturation trails.")
1247  isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1248 
1249  if self.config.doCameraSpecificMasking:
1250  self.log.info("Masking regions for camera specific reasons.")
1251  self.masking.run(ccdExposure)
1252 
1253  if self.config.doBrighterFatter:
1254  # We need to apply flats and darks before we can interpolate, and we
1255  # need to interpolate before we do B-F, but we do B-F without the
1256  # flats and darks applied so we can work in units of electrons or holes.
1257  # This context manager applies and then removes the darks and flats.
1258  #
1259  # We also do not want to interpolate values here, so operate on temporary
1260  # images so we can apply only the BF-correction and roll back the
1261  # interpolation.
1262  interpExp = ccdExposure.clone()
1263  with self.flatContext(interpExp, flat, dark):
1264  isrFunctions.interpolateFromMask(
1265  maskedImage=ccdExposure.getMaskedImage(),
1266  fwhm=self.config.fwhm,
1267  growSaturatedFootprints=self.config.growSaturationFootprintSize,
1268  maskNameList=self.config.maskListToInterpolate
1269  )
1270  bfExp = interpExp.clone()
1271 
1272  self.log.info("Applying brighter fatter correction.")
1273  isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1274  self.config.brighterFatterMaxIter,
1275  self.config.brighterFatterThreshold,
1276  self.config.brighterFatterApplyGain,
1277  )
1278  image = ccdExposure.getMaskedImage().getImage()
1279  bfCorr = bfExp.getMaskedImage().getImage()
1280  bfCorr -= interpExp.getMaskedImage().getImage()
1281  image += bfCorr
1282 
1283  self.debugView(ccdExposure, "doBrighterFatter")
1284 
1285  if self.config.doDark:
1286  self.log.info("Applying dark correction.")
1287  self.darkCorrection(ccdExposure, dark)
1288  self.debugView(ccdExposure, "doDark")
1289 
1290  if self.config.doFringe and not self.config.fringeAfterFlat:
1291  self.log.info("Applying fringe correction before flat.")
1292  self.fringe.run(ccdExposure, **fringes.getDict())
1293  self.debugView(ccdExposure, "doFringe")
1294 
1295  if self.config.doStrayLight:
1296  if strayLightData is not None:
1297  self.log.info("Applying stray light correction.")
1298  self.strayLight.run(ccdExposure, strayLightData)
1299  self.debugView(ccdExposure, "doStrayLight")
1300  else:
1301  self.log.debug("Skipping stray light correction: no data found for this image.")
1302 
1303  if self.config.doFlat:
1304  self.log.info("Applying flat correction.")
1305  self.flatCorrection(ccdExposure, flat)
1306  self.debugView(ccdExposure, "doFlat")
1307 
1308  if self.config.doApplyGains:
1309  self.log.info("Applying gain correction instead of flat.")
1310  isrFunctions.applyGains(ccdExposure, self.config.normalizeGains)
1311 
1312  if self.config.doFringe and self.config.fringeAfterFlat:
1313  self.log.info("Applying fringe correction after flat.")
1314  self.fringe.run(ccdExposure, **fringes.getDict())
1315 
1316  if self.config.doVignette:
1317  self.log.info("Constructing Vignette polygon.")
1318  self.vignettePolygon = self.vignette.run(ccdExposure)
1319 
1320  if self.config.vignette.doWriteVignettePolygon:
1321  self.setValidPolygonIntersect(ccdExposure, self.vignettePolygon)
1322 
1323  if self.config.doAttachTransmissionCurve:
1324  self.log.info("Adding transmission curves.")
1325  isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission,
1326  filterTransmission=filterTransmission,
1327  sensorTransmission=sensorTransmission,
1328  atmosphereTransmission=atmosphereTransmission)
1329 
1330  if self.config.doAddDistortionModel:
1331  self.log.info("Adding a distortion model to the WCS.")
1332  isrFunctions.addDistortionModel(exposure=ccdExposure, camera=camera)
1333 
1334  flattenedThumb = None
1335  if self.config.qa.doThumbnailFlattened:
1336  flattenedThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1337 
1338  preInterpExp = None
1339  if self.config.doSaveInterpPixels:
1340  preInterpExp = ccdExposure.clone()
1341 
1342  if self.config.doInterpolate:
1343  self.log.info("Interpolating masked pixels.")
1344  isrFunctions.interpolateFromMask(
1345  maskedImage=ccdExposure.getMaskedImage(),
1346  fwhm=self.config.fwhm,
1347  growSaturatedFootprints=self.config.growSaturationFootprintSize,
1348  maskNameList=list(self.config.maskListToInterpolate)
1349  )
1350 
1351  if self.config.doSetBadRegions:
1352  # This is like an interpolation.
1353  badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure)
1354  if badPixelCount > 0:
1355  self.log.info("Set %d BAD pixels to %f." % (badPixelCount, badPixelValue))
1356 
1357  self.roughZeroPoint(ccdExposure)
1358 
1359  if self.config.doMeasureBackground:
1360  self.log.info("Measuring background level:")
1361  self.measureBackground(ccdExposure, self.config.qa)
1362 
1363  if self.config.qa is not None and self.config.qa.saveStats is True:
1364  for amp in ccd:
1365  ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1366  qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1367  afwMath.MEDIAN | afwMath.STDEVCLIP)
1368  self.metadata.set("ISR BACKGROUND {} MEDIAN".format(amp.getName()),
1369  qaStats.getValue(afwMath.MEDIAN))
1370  self.metadata.set("ISR BACKGROUND {} STDEV".format(amp.getName()),
1371  qaStats.getValue(afwMath.STDEVCLIP))
1372  self.log.debug(" Background stats for amplifer %s: %f +/- %f" %
1373  (amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1374  qaStats.getValue(afwMath.STDEVCLIP)))
1375 
1376  self.debugView(ccdExposure, "postISRCCD")
1377 
1378  return pipeBase.Struct(
1379  exposure=ccdExposure,
1380  ossThumb=ossThumb,
1381  flattenedThumb=flattenedThumb,
1382 
1383  preInterpolatedExposure=preInterpExp,
1384  outputExposure=ccdExposure,
1385  outputOssThumbnail=ossThumb,
1386  outputFlattenedThumbnail=flattenedThumb,
1387  )
1388 
1389  @pipeBase.timeMethod
1390  def runDataRef(self, sensorRef):
1391  """Perform instrument signature removal on a ButlerDataRef of a Sensor.
1392 
1393  This method contains the `CmdLineTask` interface to the ISR
1394  processing. All IO is handled here, freeing the `run()` method
1395  to manage only pixel-level calculations. The steps performed
1396  are:
1397  - Read in necessary detrending/isr/calibration data.
1398  - Process raw exposure in `run()`.
1399  - Persist the ISR-corrected exposure as "postISRCCD" if
1400  config.doWrite=True.
1401 
1402  Parameters
1403  ----------
1404  sensorRef : `daf.persistence.butlerSubset.ButlerDataRef`
1405  DataRef of the detector data to be processed
1406 
1407  Returns
1408  -------
1409  result : `lsst.pipe.base.Struct`
1410  Result struct with component:
1411  - ``exposure`` : `afw.image.Exposure`
1412  The fully ISR corrected exposure.
1413 
1414  Raises
1415  ------
1416  RuntimeError
1417  Raised if a configuration option is set to True, but the
1418  required calibration data does not exist.
1419 
1420  """
1421  self.log.info("Performing ISR on sensor %s" % (sensorRef.dataId))
1422 
1423  ccdExposure = sensorRef.get(self.config.datasetType)
1424 
1425  camera = sensorRef.get("camera")
1426  if camera is None and self.config.doAddDistortionModel:
1427  raise RuntimeError("config.doAddDistortionModel is True "
1428  "but could not get a camera from the butler")
1429  isrData = self.readIsrData(sensorRef, ccdExposure)
1430 
1431  result = self.run(ccdExposure, camera=camera, **isrData.getDict())
1432 
1433  if self.config.doWrite:
1434  sensorRef.put(result.exposure, "postISRCCD")
1435  if result.preInterpolatedExposure is not None:
1436  sensorRef.put(result.preInterpolatedExposure, "postISRCCD_uninterpolated")
1437  if result.ossThumb is not None:
1438  isrQa.writeThumbnail(sensorRef, result.ossThumb, "ossThumb")
1439  if result.flattenedThumb is not None:
1440  isrQa.writeThumbnail(sensorRef, result.flattenedThumb, "flattenedThumb")
1441 
1442  return result
1443 
1444  def getIsrExposure(self, dataRef, datasetType, immediate=True):
1445  """!Retrieve a calibration dataset for removing instrument signature.
1446 
1447  Parameters
1448  ----------
1449 
1450  dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
1451  DataRef of the detector data to find calibration datasets
1452  for.
1453  datasetType : `str`
1454  Type of dataset to retrieve (e.g. 'bias', 'flat', etc).
1455  immediate : `Bool`
1456  If True, disable butler proxies to enable error handling
1457  within this routine.
1458 
1459  Returns
1460  -------
1461  exposure : `lsst.afw.image.Exposure`
1462  Requested calibration frame.
1463 
1464  Raises
1465  ------
1466  RuntimeError
1467  Raised if no matching calibration frame can be found.
1468  """
1469  try:
1470  exp = dataRef.get(datasetType, immediate=immediate)
1471  except Exception as exc1:
1472  if not self.config.fallbackFilterName:
1473  raise RuntimeError("Unable to retrieve %s for %s: %s" % (datasetType, dataRef.dataId, exc1))
1474  try:
1475  exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate)
1476  except Exception as exc2:
1477  raise RuntimeError("Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s" %
1478  (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2))
1479  self.log.warn("Using fallback calibration from filter %s" % self.config.fallbackFilterName)
1480 
1481  if self.config.doAssembleIsrExposures:
1482  exp = self.assembleCcd.assembleCcd(exp)
1483  return exp
1484 
1485  def ensureExposure(self, inputExp, camera, detectorNum):
1486  """Ensure that the data returned by Butler is a fully constructed exposure.
1487 
1488  ISR requires exposure-level image data for historical reasons, so if we did
1489  not recieve that from Butler, construct it from what we have, modifying the
1490  input in place.
1491 
1492  Parameters
1493  ----------
1494  inputExp : `lsst.afw.image.Exposure`, `lsst.afw.image.DecoratedImageU`, or
1495  `lsst.afw.image.ImageF`
1496  The input data structure obtained from Butler.
1497  camera : `lsst.afw.cameraGeom.camera`
1498  The camera associated with the image. Used to find the appropriate
1499  detector.
1500  detectorNum : `int`
1501  The detector this exposure should match.
1502 
1503  Returns
1504  -------
1505  inputExp : `lsst.afw.image.Exposure`
1506  The re-constructed exposure, with appropriate detector parameters.
1507 
1508  Raises
1509  ------
1510  TypeError
1511  Raised if the input data cannot be used to construct an exposure.
1512  """
1513  if isinstance(inputExp, afwImage.DecoratedImageU):
1514  inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp))
1515  elif isinstance(inputExp, afwImage.ImageF):
1516  inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp))
1517  elif isinstance(inputExp, afwImage.MaskedImageF):
1518  inputExp = afwImage.makeExposure(inputExp)
1519  elif isinstance(inputExp, afwImage.Exposure):
1520  pass
1521  elif inputExp is None:
1522  # Assume this will be caught by the setup if it is a problem.
1523  return inputExp
1524  else:
1525  raise TypeError(f"Input Exposure is not known type in isrTask.ensureExposure: {type(inputExp)}")
1526 
1527  if inputExp.getDetector() is None:
1528  inputExp.setDetector(camera[detectorNum])
1529 
1530  return inputExp
1531 
1532  def convertIntToFloat(self, exposure):
1533  """Convert exposure image from uint16 to float.
1534 
1535  If the exposure does not need to be converted, the input is
1536  immediately returned. For exposures that are converted to use
1537  floating point pixels, the variance is set to unity and the
1538  mask to zero.
1539 
1540  Parameters
1541  ----------
1542  exposure : `lsst.afw.image.Exposure`
1543  The raw exposure to be converted.
1544 
1545  Returns
1546  -------
1547  newexposure : `lsst.afw.image.Exposure`
1548  The input ``exposure``, converted to floating point pixels.
1549 
1550  Raises
1551  ------
1552  RuntimeError
1553  Raised if the exposure type cannot be converted to float.
1554 
1555  """
1556  if isinstance(exposure, afwImage.ExposureF):
1557  # Nothing to be done
1558  return exposure
1559  if not hasattr(exposure, "convertF"):
1560  raise RuntimeError("Unable to convert exposure (%s) to float" % type(exposure))
1561 
1562  newexposure = exposure.convertF()
1563  newexposure.variance[:] = 1
1564  newexposure.mask[:] = 0x0
1565 
1566  return newexposure
1567 
1568  def maskAmplifier(self, ccdExposure, amp, defects):
1569  """Identify bad amplifiers, saturated and suspect pixels.
1570 
1571  Parameters
1572  ----------
1573  ccdExposure : `lsst.afw.image.Exposure`
1574  Input exposure to be masked.
1575  amp : `lsst.afw.table.AmpInfoCatalog`
1576  Catalog of parameters defining the amplifier on this
1577  exposure to mask.
1578  defects : `lsst.meas.algorithms.Defects`
1579  List of defects. Used to determine if the entire
1580  amplifier is bad.
1581 
1582  Returns
1583  -------
1584  badAmp : `Bool`
1585  If this is true, the entire amplifier area is covered by
1586  defects and unusable.
1587 
1588  """
1589  maskedImage = ccdExposure.getMaskedImage()
1590 
1591  badAmp = False
1592 
1593  # Check if entire amp region is defined as a defect (need to use amp.getBBox() for correct
1594  # comparison with current defects definition.
1595  if defects is not None:
1596  badAmp = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects]))
1597 
1598  # In the case of a bad amp, we will set mask to "BAD" (here use amp.getRawBBox() for correct
1599  # association with pixels in current ccdExposure).
1600  if badAmp:
1601  dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
1602  afwImage.PARENT)
1603  maskView = dataView.getMask()
1604  maskView |= maskView.getPlaneBitMask("BAD")
1605  del maskView
1606  return badAmp
1607 
1608  # Mask remaining defects after assembleCcd() to allow for defects that cross amplifier boundaries.
1609  # Saturation and suspect pixels can be masked now, though.
1610  limits = dict()
1611  if self.config.doSaturation and not badAmp:
1612  limits.update({self.config.saturatedMaskName: amp.getSaturation()})
1613  if self.config.doSuspect and not badAmp:
1614  limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
1615  if math.isfinite(self.config.saturation):
1616  limits.update({self.config.saturatedMaskName: self.config.saturation})
1617 
1618  for maskName, maskThreshold in limits.items():
1619  if not math.isnan(maskThreshold):
1620  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1621  isrFunctions.makeThresholdMask(
1622  maskedImage=dataView,
1623  threshold=maskThreshold,
1624  growFootprints=0,
1625  maskName=maskName
1626  )
1627 
1628  # Determine if we've fully masked this amplifier with SUSPECT and SAT pixels.
1629  maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
1630  afwImage.PARENT)
1631  maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
1632  self.config.suspectMaskName])
1633  if numpy.all(maskView.getArray() & maskVal > 0):
1634  badAmp = True
1635 
1636  return badAmp
1637 
1638  def overscanCorrection(self, ccdExposure, amp):
1639  """Apply overscan correction in place.
1640 
1641  This method does initial pixel rejection of the overscan
1642  region. The overscan can also be optionally segmented to
1643  allow for discontinuous overscan responses to be fit
1644  separately. The actual overscan subtraction is performed by
1645  the `lsst.ip.isr.isrFunctions.overscanCorrection` function,
1646  which is called here after the amplifier is preprocessed.
1647 
1648  Parameters
1649  ----------
1650  ccdExposure : `lsst.afw.image.Exposure`
1651  Exposure to have overscan correction performed.
1652  amp : `lsst.afw.table.AmpInfoCatalog`
1653  The amplifier to consider while correcting the overscan.
1654 
1655  Returns
1656  -------
1657  overscanResults : `lsst.pipe.base.Struct`
1658  Result struct with components:
1659  - ``imageFit`` : scalar or `lsst.afw.image.Image`
1660  Value or fit subtracted from the amplifier image data.
1661  - ``overscanFit`` : scalar or `lsst.afw.image.Image`
1662  Value or fit subtracted from the overscan image data.
1663  - ``overscanImage`` : `lsst.afw.image.Image`
1664  Image of the overscan region with the overscan
1665  correction applied. This quantity is used to estimate
1666  the amplifier read noise empirically.
1667 
1668  Raises
1669  ------
1670  RuntimeError
1671  Raised if the ``amp`` does not contain raw pixel information.
1672 
1673  See Also
1674  --------
1675  lsst.ip.isr.isrFunctions.overscanCorrection
1676  """
1677  if not amp.getHasRawInfo():
1678  raise RuntimeError("This method must be executed on an amp with raw information.")
1679 
1680  if amp.getRawHorizontalOverscanBBox().isEmpty():
1681  self.log.info("ISR_OSCAN: No overscan region. Not performing overscan correction.")
1682  return None
1683 
1684  statControl = afwMath.StatisticsControl()
1685  statControl.setAndMask(ccdExposure.mask.getPlaneBitMask("SAT"))
1686 
1687  # Determine the bounding boxes
1688  dataBBox = amp.getRawDataBBox()
1689  oscanBBox = amp.getRawHorizontalOverscanBBox()
1690  dx0 = 0
1691  dx1 = 0
1692 
1693  prescanBBox = amp.getRawPrescanBBox()
1694  if (oscanBBox.getBeginX() > prescanBBox.getBeginX()): # amp is at the right
1695  dx0 += self.config.overscanNumLeadingColumnsToSkip
1696  dx1 -= self.config.overscanNumTrailingColumnsToSkip
1697  else:
1698  dx0 += self.config.overscanNumTrailingColumnsToSkip
1699  dx1 -= self.config.overscanNumLeadingColumnsToSkip
1700 
1701  # Determine if we need to work on subregions of the amplifier and overscan.
1702  imageBBoxes = []
1703  overscanBBoxes = []
1704 
1705  if ((self.config.overscanBiasJump and
1706  self.config.overscanBiasJumpLocation) and
1707  (ccdExposure.getMetadata().exists(self.config.overscanBiasJumpKeyword) and
1708  ccdExposure.getMetadata().getScalar(self.config.overscanBiasJumpKeyword) in
1709  self.config.overscanBiasJumpDevices)):
1710  if amp.getReadoutCorner() in (afwTable.LL, afwTable.LR):
1711  yLower = self.config.overscanBiasJumpLocation
1712  yUpper = dataBBox.getHeight() - yLower
1713  else:
1714  yUpper = self.config.overscanBiasJumpLocation
1715  yLower = dataBBox.getHeight() - yUpper
1716 
1717  imageBBoxes.append(lsst.geom.Box2I(dataBBox.getBegin(),
1718  lsst.geom.Extent2I(dataBBox.getWidth(), yLower)))
1719  overscanBBoxes.append(lsst.geom.Box2I(oscanBBox.getBegin() +
1720  lsst.geom.Extent2I(dx0, 0),
1721  lsst.geom.Extent2I(oscanBBox.getWidth() - dx0 + dx1,
1722  yLower)))
1723 
1724  imageBBoxes.append(lsst.geom.Box2I(dataBBox.getBegin() + lsst.geom.Extent2I(0, yLower),
1725  lsst.geom.Extent2I(dataBBox.getWidth(), yUpper)))
1726  overscanBBoxes.append(lsst.geom.Box2I(oscanBBox.getBegin() + lsst.geom.Extent2I(dx0, yLower),
1727  lsst.geom.Extent2I(oscanBBox.getWidth() - dx0 + dx1,
1728  yUpper)))
1729  else:
1730  imageBBoxes.append(lsst.geom.Box2I(dataBBox.getBegin(),
1731  lsst.geom.Extent2I(dataBBox.getWidth(), dataBBox.getHeight())))
1732  overscanBBoxes.append(lsst.geom.Box2I(oscanBBox.getBegin() + lsst.geom.Extent2I(dx0, 0),
1733  lsst.geom.Extent2I(oscanBBox.getWidth() - dx0 + dx1,
1734  oscanBBox.getHeight())))
1735 
1736  # Perform overscan correction on subregions, ensuring saturated pixels are masked.
1737  for imageBBox, overscanBBox in zip(imageBBoxes, overscanBBoxes):
1738  ampImage = ccdExposure.maskedImage[imageBBox]
1739  overscanImage = ccdExposure.maskedImage[overscanBBox]
1740 
1741  overscanArray = overscanImage.image.array
1742  median = numpy.ma.median(numpy.ma.masked_where(overscanImage.mask.array, overscanArray))
1743  bad = numpy.where(numpy.abs(overscanArray - median) > self.config.overscanMaxDev)
1744  overscanImage.mask.array[bad] = overscanImage.mask.getPlaneBitMask("SAT")
1745 
1746  statControl = afwMath.StatisticsControl()
1747  statControl.setAndMask(ccdExposure.mask.getPlaneBitMask("SAT"))
1748 
1749  overscanResults = isrFunctions.overscanCorrection(ampMaskedImage=ampImage,
1750  overscanImage=overscanImage,
1751  fitType=self.config.overscanFitType,
1752  order=self.config.overscanOrder,
1753  collapseRej=self.config.overscanNumSigmaClip,
1754  statControl=statControl,
1755  overscanIsInt=self.config.overscanIsInt
1756  )
1757 
1758  # Measure average overscan levels and record them in the metadata
1759  levelStat = afwMath.MEDIAN
1760  sigmaStat = afwMath.STDEVCLIP
1761 
1762  sctrl = afwMath.StatisticsControl(self.config.qa.flatness.clipSigma,
1763  self.config.qa.flatness.nIter)
1764  metadata = ccdExposure.getMetadata()
1765  ampNum = amp.getName()
1766  if self.config.overscanFitType in ("MEDIAN", "MEAN", "MEANCLIP"):
1767  metadata.set("ISR_OSCAN_LEVEL%s" % ampNum, overscanResults.overscanFit)
1768  metadata.set("ISR_OSCAN_SIGMA%s" % ampNum, 0.0)
1769  else:
1770  stats = afwMath.makeStatistics(overscanResults.overscanFit, levelStat | sigmaStat, sctrl)
1771  metadata.set("ISR_OSCAN_LEVEL%s" % ampNum, stats.getValue(levelStat))
1772  metadata.set("ISR_OSCAN_SIGMA%s" % ampNum, stats.getValue(sigmaStat))
1773 
1774  return overscanResults
1775 
1776  def updateVariance(self, ampExposure, amp, overscanImage=None):
1777  """Set the variance plane using the amplifier gain and read noise
1778 
1779  The read noise is calculated from the ``overscanImage`` if the
1780  ``doEmpiricalReadNoise`` option is set in the configuration; otherwise
1781  the value from the amplifier data is used.
1782 
1783  Parameters
1784  ----------
1785  ampExposure : `lsst.afw.image.Exposure`
1786  Exposure to process.
1787  amp : `lsst.afw.table.AmpInfoRecord` or `FakeAmp`
1788  Amplifier detector data.
1789  overscanImage : `lsst.afw.image.MaskedImage`, optional.
1790  Image of overscan, required only for empirical read noise.
1791 
1792  See also
1793  --------
1794  lsst.ip.isr.isrFunctions.updateVariance
1795  """
1796  maskPlanes = [self.config.saturatedMaskName, self.config.suspectMaskName]
1797  gain = amp.getGain()
1798 
1799  if math.isnan(gain):
1800  gain = 1.0
1801  self.log.warn("Gain set to NAN! Updating to 1.0 to generate Poisson variance.")
1802  elif gain <= 0:
1803  patchedGain = 1.0
1804  self.log.warn("Gain for amp %s == %g <= 0; setting to %f" %
1805  (amp.getName(), gain, patchedGain))
1806  gain = patchedGain
1807 
1808  if self.config.doEmpiricalReadNoise and overscanImage is None:
1809  self.log.info("Overscan is none for EmpiricalReadNoise")
1810 
1811  if self.config.doEmpiricalReadNoise and overscanImage is not None:
1812  stats = afwMath.StatisticsControl()
1813  stats.setAndMask(overscanImage.mask.getPlaneBitMask(maskPlanes))
1814  readNoise = afwMath.makeStatistics(overscanImage, afwMath.STDEVCLIP, stats).getValue()
1815  self.log.info("Calculated empirical read noise for amp %s: %f", amp.getName(), readNoise)
1816  else:
1817  readNoise = amp.getReadNoise()
1818 
1819  isrFunctions.updateVariance(
1820  maskedImage=ampExposure.getMaskedImage(),
1821  gain=gain,
1822  readNoise=readNoise,
1823  )
1824 
1825  def darkCorrection(self, exposure, darkExposure, invert=False):
1826  """!Apply dark correction in place.
1827 
1828  Parameters
1829  ----------
1830  exposure : `lsst.afw.image.Exposure`
1831  Exposure to process.
1832  darkExposure : `lsst.afw.image.Exposure`
1833  Dark exposure of the same size as ``exposure``.
1834  invert : `Bool`, optional
1835  If True, re-add the dark to an already corrected image.
1836 
1837  Raises
1838  ------
1839  RuntimeError
1840  Raised if either ``exposure`` or ``darkExposure`` do not
1841  have their dark time defined.
1842 
1843  See Also
1844  --------
1845  lsst.ip.isr.isrFunctions.darkCorrection
1846  """
1847  expScale = exposure.getInfo().getVisitInfo().getDarkTime()
1848  if math.isnan(expScale):
1849  raise RuntimeError("Exposure darktime is NAN")
1850  if darkExposure.getInfo().getVisitInfo() is not None:
1851  darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
1852  else:
1853  # DM-17444: darkExposure.getInfo.getVisitInfo() is None
1854  # so getDarkTime() does not exist.
1855  darkScale = 1.0
1856 
1857  if math.isnan(darkScale):
1858  raise RuntimeError("Dark calib darktime is NAN")
1859  isrFunctions.darkCorrection(
1860  maskedImage=exposure.getMaskedImage(),
1861  darkMaskedImage=darkExposure.getMaskedImage(),
1862  expScale=expScale,
1863  darkScale=darkScale,
1864  invert=invert,
1865  trimToFit=self.config.doTrimToMatchCalib
1866  )
1867 
1868  def doLinearize(self, detector):
1869  """!Check if linearization is needed for the detector cameraGeom.
1870 
1871  Checks config.doLinearize and the linearity type of the first
1872  amplifier.
1873 
1874  Parameters
1875  ----------
1876  detector : `lsst.afw.cameraGeom.Detector`
1877  Detector to get linearity type from.
1878 
1879  Returns
1880  -------
1881  doLinearize : `Bool`
1882  If True, linearization should be performed.
1883  """
1884  return self.config.doLinearize and \
1885  detector.getAmpInfoCatalog()[0].getLinearityType() != NullLinearityType
1886 
1887  def flatCorrection(self, exposure, flatExposure, invert=False):
1888  """!Apply flat correction in place.
1889 
1890  Parameters
1891  ----------
1892  exposure : `lsst.afw.image.Exposure`
1893  Exposure to process.
1894  flatExposure : `lsst.afw.image.Exposure`
1895  Flat exposure of the same size as ``exposure``.
1896  invert : `Bool`, optional
1897  If True, unflatten an already flattened image.
1898 
1899  See Also
1900  --------
1901  lsst.ip.isr.isrFunctions.flatCorrection
1902  """
1903  isrFunctions.flatCorrection(
1904  maskedImage=exposure.getMaskedImage(),
1905  flatMaskedImage=flatExposure.getMaskedImage(),
1906  scalingType=self.config.flatScalingType,
1907  userScale=self.config.flatUserScale,
1908  invert=invert,
1909  trimToFit=self.config.doTrimToMatchCalib
1910  )
1911 
1912  def saturationDetection(self, exposure, amp):
1913  """!Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place.
1914 
1915  Parameters
1916  ----------
1917  exposure : `lsst.afw.image.Exposure`
1918  Exposure to process. Only the amplifier DataSec is processed.
1919  amp : `lsst.afw.table.AmpInfoCatalog`
1920  Amplifier detector data.
1921 
1922  See Also
1923  --------
1924  lsst.ip.isr.isrFunctions.makeThresholdMask
1925  """
1926  if not math.isnan(amp.getSaturation()):
1927  maskedImage = exposure.getMaskedImage()
1928  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1929  isrFunctions.makeThresholdMask(
1930  maskedImage=dataView,
1931  threshold=amp.getSaturation(),
1932  growFootprints=0,
1933  maskName=self.config.saturatedMaskName,
1934  )
1935 
1936  def saturationInterpolation(self, exposure):
1937  """!Interpolate over saturated pixels, in place.
1938 
1939  This method should be called after `saturationDetection`, to
1940  ensure that the saturated pixels have been identified in the
1941  SAT mask. It should also be called after `assembleCcd`, since
1942  saturated regions may cross amplifier boundaries.
1943 
1944  Parameters
1945  ----------
1946  exposure : `lsst.afw.image.Exposure`
1947  Exposure to process.
1948 
1949  See Also
1950  --------
1951  lsst.ip.isr.isrTask.saturationDetection
1952  lsst.ip.isr.isrFunctions.interpolateFromMask
1953  """
1954  isrFunctions.interpolateFromMask(
1955  maskedImage=exposure.getMaskedImage(),
1956  fwhm=self.config.fwhm,
1957  growSaturatedFootprints=self.config.growSaturationFootprintSize,
1958  maskNameList=list(self.config.saturatedMaskName),
1959  )
1960 
1961  def suspectDetection(self, exposure, amp):
1962  """!Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place.
1963 
1964  Parameters
1965  ----------
1966  exposure : `lsst.afw.image.Exposure`
1967  Exposure to process. Only the amplifier DataSec is processed.
1968  amp : `lsst.afw.table.AmpInfoCatalog`
1969  Amplifier detector data.
1970 
1971  See Also
1972  --------
1973  lsst.ip.isr.isrFunctions.makeThresholdMask
1974 
1975  Notes
1976  -----
1977  Suspect pixels are pixels whose value is greater than amp.getSuspectLevel().
1978  This is intended to indicate pixels that may be affected by unknown systematics;
1979  for example if non-linearity corrections above a certain level are unstable
1980  then that would be a useful value for suspectLevel. A value of `nan` indicates
1981  that no such level exists and no pixels are to be masked as suspicious.
1982  """
1983  suspectLevel = amp.getSuspectLevel()
1984  if math.isnan(suspectLevel):
1985  return
1986 
1987  maskedImage = exposure.getMaskedImage()
1988  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1989  isrFunctions.makeThresholdMask(
1990  maskedImage=dataView,
1991  threshold=suspectLevel,
1992  growFootprints=0,
1993  maskName=self.config.suspectMaskName,
1994  )
1995 
1996  def maskDefect(self, exposure, defectBaseList):
1997  """!Mask defects using mask plane "BAD", in place.
1998 
1999  Parameters
2000  ----------
2001  exposure : `lsst.afw.image.Exposure`
2002  Exposure to process.
2003  defectBaseList : `lsst.meas.algorithms.Defects` or `list` of
2004  `lsst.afw.image.DefectBase`.
2005  List of defects to mask and interpolate.
2006 
2007  Notes
2008  -----
2009  Call this after CCD assembly, since defects may cross amplifier boundaries.
2010  """
2011  maskedImage = exposure.getMaskedImage()
2012  if not isinstance(defectBaseList, Defects):
2013  # Promotes DefectBase to Defect
2014  defectList = Defects(defectBaseList)
2015  else:
2016  defectList = defectBaseList
2017  defectList.maskPixels(maskedImage, maskName="BAD")
2018 
2019  if self.config.numEdgeSuspect > 0:
2020  goodBBox = maskedImage.getBBox()
2021  # This makes a bbox numEdgeSuspect pixels smaller than the image on each side
2022  goodBBox.grow(-self.config.numEdgeSuspect)
2023  # Mask pixels outside goodBBox as SUSPECT
2024  SourceDetectionTask.setEdgeBits(
2025  maskedImage,
2026  goodBBox,
2027  maskedImage.getMask().getPlaneBitMask("SUSPECT")
2028  )
2029 
2030  def maskAndInterpolateDefects(self, exposure, defectBaseList):
2031  """Mask and interpolate defects using mask plane "BAD", in place.
2032 
2033  Parameters
2034  ----------
2035  exposure : `lsst.afw.image.Exposure`
2036  Exposure to process.
2037  defectBaseList : `List` of `Defects`
2038 
2039  """
2040  self.maskDefects(exposure, defectBaseList)
2041  isrFunctions.interpolateFromMask(
2042  maskedImage=exposure.getMaskedImage(),
2043  fwhm=self.config.fwhm,
2044  growSaturatedFootprints=0,
2045  maskNameList=["BAD"],
2046  )
2047 
2048  def maskNan(self, exposure):
2049  """Mask NaNs using mask plane "UNMASKEDNAN", in place.
2050 
2051  Parameters
2052  ----------
2053  exposure : `lsst.afw.image.Exposure`
2054  Exposure to process.
2055 
2056  Notes
2057  -----
2058  We mask over all NaNs, including those that are masked with
2059  other bits (because those may or may not be interpolated over
2060  later, and we want to remove all NaNs). Despite this
2061  behaviour, the "UNMASKEDNAN" mask plane is used to preserve
2062  the historical name.
2063  """
2064  maskedImage = exposure.getMaskedImage()
2065 
2066  # Find and mask NaNs
2067  maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
2068  maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
2069  numNans = maskNans(maskedImage, maskVal)
2070  self.metadata.set("NUMNANS", numNans)
2071  if numNans > 0:
2072  self.log.warn(f"There were {numNans} unmasked NaNs")
2073 
2074  def maskAndInterpolateNan(self, exposure):
2075  """"Mask and interpolate NaNs using mask plane "UNMASKEDNAN", in place.
2076 
2077  Parameters
2078  ----------
2079  exposure : `lsst.afw.image.Exposure`
2080  Exposure to process.
2081 
2082  See Also
2083  --------
2084  lsst.ip.isr.isrTask.maskNan()
2085  """
2086  self.maskNan(exposure)
2087  isrFunctions.interpolateFromMask(
2088  maskedImage=exposure.getMaskedImage(),
2089  fwhm=self.config.fwhm,
2090  growSaturatedFootprints=0,
2091  maskNameList=["UNMASKEDNAN"],
2092  )
2093 
2094  def measureBackground(self, exposure, IsrQaConfig=None):
2095  """Measure the image background in subgrids, for quality control purposes.
2096 
2097  Parameters
2098  ----------
2099  exposure : `lsst.afw.image.Exposure`
2100  Exposure to process.
2101  IsrQaConfig : `lsst.ip.isr.isrQa.IsrQaConfig`
2102  Configuration object containing parameters on which background
2103  statistics and subgrids to use.
2104  """
2105  if IsrQaConfig is not None:
2106  statsControl = afwMath.StatisticsControl(IsrQaConfig.flatness.clipSigma,
2107  IsrQaConfig.flatness.nIter)
2108  maskVal = exposure.getMaskedImage().getMask().getPlaneBitMask(["BAD", "SAT", "DETECTED"])
2109  statsControl.setAndMask(maskVal)
2110  maskedImage = exposure.getMaskedImage()
2111  stats = afwMath.makeStatistics(maskedImage, afwMath.MEDIAN | afwMath.STDEVCLIP, statsControl)
2112  skyLevel = stats.getValue(afwMath.MEDIAN)
2113  skySigma = stats.getValue(afwMath.STDEVCLIP)
2114  self.log.info("Flattened sky level: %f +/- %f" % (skyLevel, skySigma))
2115  metadata = exposure.getMetadata()
2116  metadata.set('SKYLEVEL', skyLevel)
2117  metadata.set('SKYSIGMA', skySigma)
2118 
2119  # calcluating flatlevel over the subgrids
2120  stat = afwMath.MEANCLIP if IsrQaConfig.flatness.doClip else afwMath.MEAN
2121  meshXHalf = int(IsrQaConfig.flatness.meshX/2.)
2122  meshYHalf = int(IsrQaConfig.flatness.meshY/2.)
2123  nX = int((exposure.getWidth() + meshXHalf) / IsrQaConfig.flatness.meshX)
2124  nY = int((exposure.getHeight() + meshYHalf) / IsrQaConfig.flatness.meshY)
2125  skyLevels = numpy.zeros((nX, nY))
2126 
2127  for j in range(nY):
2128  yc = meshYHalf + j * IsrQaConfig.flatness.meshY
2129  for i in range(nX):
2130  xc = meshXHalf + i * IsrQaConfig.flatness.meshX
2131 
2132  xLLC = xc - meshXHalf
2133  yLLC = yc - meshYHalf
2134  xURC = xc + meshXHalf - 1
2135  yURC = yc + meshYHalf - 1
2136 
2137  bbox = lsst.geom.Box2I(lsst.geom.Point2I(xLLC, yLLC), lsst.geom.Point2I(xURC, yURC))
2138  miMesh = maskedImage.Factory(exposure.getMaskedImage(), bbox, afwImage.LOCAL)
2139 
2140  skyLevels[i, j] = afwMath.makeStatistics(miMesh, stat, statsControl).getValue()
2141 
2142  good = numpy.where(numpy.isfinite(skyLevels))
2143  skyMedian = numpy.median(skyLevels[good])
2144  flatness = (skyLevels[good] - skyMedian) / skyMedian
2145  flatness_rms = numpy.std(flatness)
2146  flatness_pp = flatness.max() - flatness.min() if len(flatness) > 0 else numpy.nan
2147 
2148  self.log.info("Measuring sky levels in %dx%d grids: %f" % (nX, nY, skyMedian))
2149  self.log.info("Sky flatness in %dx%d grids - pp: %f rms: %f" %
2150  (nX, nY, flatness_pp, flatness_rms))
2151 
2152  metadata.set('FLATNESS_PP', float(flatness_pp))
2153  metadata.set('FLATNESS_RMS', float(flatness_rms))
2154  metadata.set('FLATNESS_NGRIDS', '%dx%d' % (nX, nY))
2155  metadata.set('FLATNESS_MESHX', IsrQaConfig.flatness.meshX)
2156  metadata.set('FLATNESS_MESHY', IsrQaConfig.flatness.meshY)
2157 
2158  def roughZeroPoint(self, exposure):
2159  """Set an approximate magnitude zero point for the exposure.
2160 
2161  Parameters
2162  ----------
2163  exposure : `lsst.afw.image.Exposure`
2164  Exposure to process.
2165  """
2166  filterName = afwImage.Filter(exposure.getFilter().getId()).getName() # Canonical name for filter
2167  if filterName in self.config.fluxMag0T1:
2168  fluxMag0 = self.config.fluxMag0T1[filterName]
2169  else:
2170  self.log.warn("No rough magnitude zero point set for filter %s" % filterName)
2171  fluxMag0 = self.config.defaultFluxMag0T1
2172 
2173  expTime = exposure.getInfo().getVisitInfo().getExposureTime()
2174  if not expTime > 0: # handle NaN as well as <= 0
2175  self.log.warn("Non-positive exposure time; skipping rough zero point")
2176  return
2177 
2178  self.log.info("Setting rough magnitude zero point: %f" % (2.5*math.log10(fluxMag0*expTime),))
2179  exposure.setPhotoCalib(afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0*expTime, 0.0))
2180 
2181  def setValidPolygonIntersect(self, ccdExposure, fpPolygon):
2182  """!Set the valid polygon as the intersection of fpPolygon and the ccd corners.
2183 
2184  Parameters
2185  ----------
2186  ccdExposure : `lsst.afw.image.Exposure`
2187  Exposure to process.
2188  fpPolygon : `lsst.afw.geom.Polygon`
2189  Polygon in focal plane coordinates.
2190  """
2191  # Get ccd corners in focal plane coordinates
2192  ccd = ccdExposure.getDetector()
2193  fpCorners = ccd.getCorners(FOCAL_PLANE)
2194  ccdPolygon = Polygon(fpCorners)
2195 
2196  # Get intersection of ccd corners with fpPolygon
2197  intersect = ccdPolygon.intersectionSingle(fpPolygon)
2198 
2199  # Transform back to pixel positions and build new polygon
2200  ccdPoints = ccd.transform(intersect, FOCAL_PLANE, PIXELS)
2201  validPolygon = Polygon(ccdPoints)
2202  ccdExposure.getInfo().setValidPolygon(validPolygon)
2203 
2204  @contextmanager
2205  def flatContext(self, exp, flat, dark=None):
2206  """Context manager that applies and removes flats and darks,
2207  if the task is configured to apply them.
2208 
2209  Parameters
2210  ----------
2211  exp : `lsst.afw.image.Exposure`
2212  Exposure to process.
2213  flat : `lsst.afw.image.Exposure`
2214  Flat exposure the same size as ``exp``.
2215  dark : `lsst.afw.image.Exposure`, optional
2216  Dark exposure the same size as ``exp``.
2217 
2218  Yields
2219  ------
2220  exp : `lsst.afw.image.Exposure`
2221  The flat and dark corrected exposure.
2222  """
2223  if self.config.doDark and dark is not None:
2224  self.darkCorrection(exp, dark)
2225  if self.config.doFlat:
2226  self.flatCorrection(exp, flat)
2227  try:
2228  yield exp
2229  finally:
2230  if self.config.doFlat:
2231  self.flatCorrection(exp, flat, invert=True)
2232  if self.config.doDark and dark is not None:
2233  self.darkCorrection(exp, dark, invert=True)
2234 
2235  def debugView(self, exposure, stepname):
2236  """Utility function to examine ISR exposure at different stages.
2237 
2238  Parameters
2239  ----------
2240  exposure : `lsst.afw.image.Exposure`
2241  Exposure to view.
2242  stepname : `str`
2243  State of processing to view.
2244  """
2245  frame = getDebugFrame(self._display, stepname)
2246  if frame:
2247  display = getDisplay(frame)
2248  display.scale('asinh', 'zscale')
2249  display.mtv(exposure)
2250 
2251 
2253  """A Detector-like object that supports returning gain and saturation level
2254 
2255  This is used when the input exposure does not have a detector.
2256 
2257  Parameters
2258  ----------
2259  exposure : `lsst.afw.image.Exposure`
2260  Exposure to generate a fake amplifier for.
2261  config : `lsst.ip.isr.isrTaskConfig`
2262  Configuration to apply to the fake amplifier.
2263  """
2264 
2265  def __init__(self, exposure, config):
2266  self._bbox = exposure.getBBox(afwImage.LOCAL)
2268  self._gain = config.gain
2269  self._readNoise = config.readNoise
2270  self._saturation = config.saturation
2271 
2272  def getBBox(self):
2273  return self._bbox
2274 
2275  def getRawBBox(self):
2276  return self._bbox
2277 
2278  def getHasRawInfo(self):
2279  return True # but see getRawHorizontalOverscanBBox()
2280 
2282  return self._RawHorizontalOverscanBBox
2283 
2284  def getGain(self):
2285  return self._gain
2286 
2287  def getReadNoise(self):
2288  return self._readNoise
2289 
2290  def getSaturation(self):
2291  return self._saturation
2292 
2293  def getSuspectLevel(self):
2294  return float("NaN")
2295 
2296 
2297 class RunIsrConfig(pexConfig.Config):
2298  isr = pexConfig.ConfigurableField(target=IsrTask, doc="Instrument signature removal")
2299 
2300 
2301 class RunIsrTask(pipeBase.CmdLineTask):
2302  """Task to wrap the default IsrTask to allow it to be retargeted.
2303 
2304  The standard IsrTask can be called directly from a command line
2305  program, but doing so removes the ability of the task to be
2306  retargeted. As most cameras override some set of the IsrTask
2307  methods, this would remove those data-specific methods in the
2308  output post-ISR images. This wrapping class fixes the issue,
2309  allowing identical post-ISR images to be generated by both the
2310  processCcd and isrTask code.
2311  """
2312  ConfigClass = RunIsrConfig
2313  _DefaultName = "runIsr"
2314 
2315  def __init__(self, *args, **kwargs):
2316  super().__init__(*args, **kwargs)
2317  self.makeSubtask("isr")
2318 
2319  def runDataRef(self, dataRef):
2320  """
2321  Parameters
2322  ----------
2323  dataRef : `lsst.daf.persistence.ButlerDataRef`
2324  data reference of the detector data to be processed
2325 
2326  Returns
2327  -------
2328  result : `pipeBase.Struct`
2329  Result struct with component:
2330 
2331  - exposure : `lsst.afw.image.Exposure`
2332  Post-ISR processed exposure.
2333  """
2334  return self.isr.runDataRef(dataRef)
def getInputDatasetTypes(cls, config)
Definition: isrTask.py:740
std::shared_ptr< PhotoCalib > makePhotoCalibFromCalibZeroPoint(double instFluxMag0, double instFluxMag0Err)
Construct a PhotoCalib from the deprecated Calib-style instFluxMag0/instFluxMag0Err values...
Definition: PhotoCalib.cc:644
bool contains(VertexIterator const begin, VertexIterator const end, UnitVector3d const &v)
def runDataRef(self, sensorRef)
Definition: isrTask.py:1390
def measureBackground(self, exposure, IsrQaConfig=None)
Definition: isrTask.py:2094
def debugView(self, exposure, stepname)
Definition: isrTask.py:2235
def __init__(self, kwargs)
Definition: isrTask.py:730
def ensureExposure(self, inputExp, camera, detectorNum)
Definition: isrTask.py:1485
A class to contain the data, WCS, and other information needed to describe an image of the sky...
Definition: Exposure.h:72
def readIsrData(self, dataRef, rawExposure)
Retrieve necessary frames for instrument signature removal.
Definition: isrTask.py:847
def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler)
Definition: isrTask.py:807
def runDataRef(self, dataRef)
Definition: isrTask.py:2319
def __init__(self, args, kwargs)
Definition: isrTask.py:2315
def getPrerequisiteDatasetTypes(cls, config)
Definition: isrTask.py:790
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
daf::base::PropertySet * set
Definition: fits.cc:884
def roughZeroPoint(self, exposure)
Definition: isrTask.py:2158
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.
Definition: MaskedImage.h:1280
def maskAndInterpolateDefects(self, exposure, defectBaseList)
Definition: isrTask.py:2030
def getRawHorizontalOverscanBBox(self)
Definition: isrTask.py:2281
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
def maskNan(self, exposure)
Definition: isrTask.py:2048
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:457
Pass parameters to a Statistics object.
Definition: Statistics.h:93
table::Key< int > type
Definition: Detector.cc:167
def getOutputDatasetTypes(cls, config)
Definition: isrTask.py:777
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:78
def maskDefect(self, exposure, defectBaseList)
Mask defects using mask plane "BAD", in place.
Definition: isrTask.py:1996
def overscanCorrection(self, ccdExposure, amp)
Definition: isrTask.py:1638
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:168
def convertIntToFloat(self, exposure)
Definition: isrTask.py:1532
Holds an integer identifier for an LSST filter.
Definition: Filter.h:141
def flatCorrection(self, exposure, flatExposure, invert=False)
Apply flat correction in place.
Definition: isrTask.py:1887
def getDebugFrame(debugDisplay, name)
Definition: lsstDebug.py:90
def makeDatasetType(self, dsConfig)
Definition: isrTask.py:844
def getIsrExposure(self, dataRef, datasetType, immediate=True)
Retrieve a calibration dataset for removing instrument signature.
Definition: isrTask.py:1444
def darkCorrection(self, exposure, darkExposure, invert=False)
Apply dark correction in place.
Definition: isrTask.py:1825
def doLinearize(self, detector)
Check if linearization is needed for the detector cameraGeom.
Definition: isrTask.py:1868
def setValidPolygonIntersect(self, ccdExposure, fpPolygon)
Set the valid polygon as the intersection of fpPolygon and the ccd corners.
Definition: isrTask.py:2181
def maskAmplifier(self, ccdExposure, amp, defects)
Definition: isrTask.py:1568
def getPerDatasetTypeDimensions(cls, config)
Definition: isrTask.py:800
def run(self, ccdExposure, camera=None, bias=None, linearizer=None, crosstalkSources=None, dark=None, flat=None, bfKernel=None, defects=None, fringes=None, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None, detectorNum=None, strayLightData=None, isGen3=False)
Perform instrument signature removal on an exposure.
Definition: isrTask.py:985
Cartesian polygons.
Definition: Polygon.h:59
def flatContext(self, exp, flat, dark=None)
Definition: isrTask.py:2205
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
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
def updateVariance(self, ampExposure, amp, overscanImage=None)
Definition: isrTask.py:1776
def maskAndInterpolateNan(self, exposure)
Definition: isrTask.py:2074
An integer coordinate rectangle.
Definition: Box.h:54
daf::base::PropertyList * list
Definition: fits.cc:885
def suspectDetection(self, exposure, amp)
Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place.
Definition: isrTask.py:1961
def saturationInterpolation(self, exposure)
Interpolate over saturated pixels, in place.
Definition: isrTask.py:1936
def saturationDetection(self, exposure, amp)
Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place...
Definition: isrTask.py:1912
def __init__(self, exposure, config)
Definition: isrTask.py:2265