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