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