984 Mask SATURATED and SUSPECT pixels and check if any amplifiers
989 badAmpDict : `str` [`bool`]
990 Dictionary of amplifiers, keyed by name, value is True if
991 amplifier is fully masked.
992 ccdExposure : `lsst.afw.image.Exposure`
993 Input exposure to be masked.
994 detector : `lsst.afw.cameraGeom.Detector`
996 defects : `lsst.ip.isr.Defects`
997 List of defects. Used to determine if an entire
999 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
1000 Per-amplifier configurations.
1001 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
1002 PTC dataset (used if configured to use PTCTURNOFF).
1006 badAmpDict : `str`[`bool`]
1007 Dictionary of amplifiers, keyed by name.
1009 maskedImage = ccdExposure.getMaskedImage()
1011 metadata = ccdExposure.metadata
1013 if self.config.doSaturation
and self.config.defaultSaturationSource ==
"PTCTURNOFF" and ptc
is None:
1014 raise RuntimeError(
"Must provide ptc if using PTCTURNOFF as saturation source.")
1015 if self.config.doSuspect
and self.config.defaultSuspectSource ==
"PTCTURNOFF" and ptc
is None:
1016 raise RuntimeError(
"Must provide ptc if using PTCTURNOFF as suspect source.")
1018 for amp
in detector:
1019 ampName = amp.getName()
1021 ampConfig = detectorConfig.getOverscanAmpConfig(amp)
1023 if badAmpDict[ampName]:
1029 if self.config.doSaturation:
1030 if self.config.defaultSaturationSource ==
"PTCTURNOFF":
1031 limits.update({self.config.saturatedMaskName: ptc.ptcTurnoff[amp.getName()]})
1032 elif self.config.defaultSaturationSource ==
"CAMERAMODEL":
1034 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
1035 elif self.config.defaultSaturationSource ==
"NONE":
1036 limits.update({self.config.saturatedMaskName: numpy.inf})
1039 if math.isfinite(ampConfig.saturation):
1040 limits.update({self.config.saturatedMaskName: ampConfig.saturation})
1041 metadata[f
"LSST ISR SATURATION LEVEL {ampName}"] = limits[self.config.saturatedMaskName]
1043 if self.config.doSuspect:
1044 if self.config.defaultSuspectSource ==
"PTCTURNOFF":
1045 limits.update({self.config.suspectMaskName: ptc.ptcTurnoff[amp.getName()]})
1046 elif self.config.defaultSuspectSource ==
"CAMERAMODEL":
1048 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
1049 elif self.config.defaultSuspectSource ==
"NONE":
1050 limits.update({self.config.suspectMaskName: numpy.inf})
1053 if math.isfinite(ampConfig.suspectLevel):
1054 limits.update({self.config.suspectMaskName: ampConfig.suspectLevel})
1055 metadata[f
"LSST ISR SUSPECT LEVEL {ampName}"] = limits[self.config.suspectMaskName]
1057 for maskName, maskThreshold
in limits.items():
1058 if not math.isnan(maskThreshold):
1059 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1060 toMask = (dataView.image.array >= maskThreshold)
1061 dataView.mask.array[toMask] |= dataView.mask.getPlaneBitMask(maskName)
1065 maskView =
afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
1067 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
1068 self.config.suspectMaskName])
1069 if numpy.all(maskView.getArray() & maskVal > 0):
1070 self.log.warning(
"Amplifier %s is bad (completely SATURATED or SUSPECT)", ampName)
1071 badAmpDict[ampName] =
True
1072 maskView |= maskView.getPlaneBitMask(
"BAD")
1837 deferredChargeCalib=None,
1840 gainCorrection=None,
1848 """Run the IsrTaskLSST task.
1852 ccdExposure : `lsst.afw.image.Exposure`
1853 Exposure to run ISR.
1854 dnlLUT : `None`, optional
1855 DNL lookup table; placeholder, unused.
1856 bias : `lsst.afw.image.Exposure`, optional
1858 deferredChargeCalib : `lsst.ip.isr.DeferredChargeCalib`, optional
1859 Deferred charge calibration.
1860 linearizer : `lsst.ip.isr.Linearizer`, optional
1861 Linearizer calibration.
1862 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
1864 gainCorrection : `lsst.ip.isr.GainCorrection`, optional
1865 Gain correction dataset.
1866 crosstalk : `lsst.ip.isr.CrosstalkCalib`, optional
1867 Crosstalk calibration dataset.
1868 defects : `lsst.ip.isr.Defects`, optional
1870 bfKernel : `lsst.ip.isr.BrighterFatterKernel`, optional
1871 Brighter-fatter kernel dataset.
1872 dark : `lsst.afw.image.Exposure`, optional
1874 flat : `lsst.afw.image.Exposure`, optional
1876 camera : `lsst.afw.cameraGeom.Camera`, optional
1881 result : `lsst.pipe.base.Struct`
1883 ``exposure``: `lsst.afw.image.Exposure`
1884 Calibrated exposure.
1885 ``outputBin1Exposure``: `lsst.afw.image.Exposure`
1886 Binned exposure (bin1 config).
1887 ``outputBin2Exposure``: `lsst.afw.image.Exposure`
1888 Binned exposure (bin2 config).
1889 ``outputExposure``: `lsst.afw.image.Exposure`
1890 Calibrated exposure (same as ``exposure``).
1891 ``outputStatistics``: `lsst.ip.isr.isrStatistics`
1892 Calibrated exposure statistics.
1894 detector = ccdExposure.getDetector()
1896 overscanDetectorConfig = self.config.overscanCamera.getOverscanDetectorConfig(detector)
1898 if self.config.doBootstrap:
1900 self.log.warning(
"Task configured with doBootstrap=True. Ignoring provided PTC.")
1903 if self.config.useGainsFrom ==
"LINEARIZER":
1904 if linearizer
is None:
1905 raise RuntimeError(
"doBootstrap==False and useGainsFrom == 'LINEARIZER' but "
1906 "no linearizer provided.")
1907 elif self.config.useGainsFrom ==
"PTC":
1909 raise RuntimeError(
"doBootstrap==False and useGainsFrom == 'PTC' but no PTC provided.")
1912 exposureMetadata = ccdExposure.metadata
1913 doRaise = self.config.doRaiseOnCalibMismatch
1914 keywords = self.config.cameraKeywordsToCompare
1915 if not self.config.doBootstrap:
1916 if self.config.useGainsFrom ==
"LINEARIZER":
1918 "LINEARIZER", log=self.log)
1919 elif self.config.useGainsFrom ==
"PTC":
1923 if self.config.doCorrectGains
and gainCorrection
is not None:
1933 if self.config.doCorrectGains:
1934 raise RuntimeError(
"doCorrectGains is True but no ptc provided.")
1935 if self.config.doDiffNonLinearCorrection:
1937 raise RuntimeError(
"doDiffNonLinearCorrection is True but no dnlLUT provided.")
1939 if self.config.doLinearize:
1940 if linearizer
is None:
1941 raise RuntimeError(
"doLinearize is True but no linearizer provided.")
1943 if self.config.doBias:
1945 raise RuntimeError(
"doBias is True but no bias provided.")
1948 if self.config.doCrosstalk:
1949 if crosstalk
is None:
1950 raise RuntimeError(
"doCrosstalk is True but no crosstalk provided.")
1952 if self.config.doDeferredCharge:
1953 if deferredChargeCalib
is None:
1954 raise RuntimeError(
"doDeferredCharge is True but no deferredChargeCalib provided.")
1959 deferredChargeCalib,
1963 if self.config.doDefect:
1965 raise RuntimeError(
"doDefect is True but no defects provided.")
1967 if self.config.doDark:
1969 raise RuntimeError(
"doDark is True but no dark frame provided.")
1972 if self.config.doBrighterFatter:
1973 if bfKernel
is None:
1974 raise RuntimeError(
"doBrighterFatter is True not no bfKernel provided.")
1976 if self.config.doFlat:
1978 raise RuntimeError(
"doFlat is True but no flat provided.")
1981 if self.config.doSaturation:
1982 if self.config.defaultSaturationSource
in [
"PTCTURNOFF",]:
1985 "doSaturation is True and defaultSaturationSource is "
1986 f
"{self.config.defaultSaturationSource}, but no ptc provided."
1988 if self.config.doSuspect:
1989 if self.config.defaultSuspectSource
in [
"PTCTURNOFF",]:
1992 "doSuspect is True and defaultSuspectSource is "
1993 f
"{self.config.defaultSuspectSource}, but no ptc provided."
1996 if self.config.doCheckUnprocessableData
and self.config.bssVoltageMinimum > 0.0:
2003 exposureMetadata[
"LSST ISR UNITS"] =
"adu"
2004 exposureMetadata[
"LSST ISR GAINCORRECTION APPLIED"] =
False
2005 exposureMetadata[
"LSST ISR CROSSTALK APPLIED"] =
False
2006 exposureMetadata[
"LSST ISR OVERSCANLEVEL CHECKED"] =
False
2007 exposureMetadata[
"LSST ISR NOISE CHECKED"] =
False
2008 exposureMetadata[
"LSST ISR LINEARIZER APPLIED"] =
False
2009 exposureMetadata[
"LSST ISR CTI APPLIED"] =
False
2010 exposureMetadata[
"LSST ISR BIAS APPLIED"] =
False
2011 exposureMetadata[
"LSST ISR DARK APPLIED"] =
False
2012 exposureMetadata[
"LSST ISR BF APPLIED"] =
False
2013 exposureMetadata[
"LSST ISR FLAT APPLIED"] =
False
2014 exposureMetadata[
"LSST ISR DEFECTS APPLIED"] =
False
2016 if self.config.doBootstrap:
2017 self.log.info(
"Configured using doBootstrap=True; using gain of 1.0 (adu units)")
2019 for amp
in detector:
2020 ptc.gain[amp.getName()] = 1.0
2021 ptc.noise[amp.getName()] = 0.0
2022 elif self.config.useGainsFrom ==
"LINEARIZER":
2023 self.log.info(
"Using gains from linearizer.")
2026 for amp
in detector:
2027 ptc.gain[amp.getName()] = linearizer.inputGain[amp.getName()]
2028 ptc.noise[amp.getName()] = 0.0
2030 exposureMetadata[
"LSST ISR BOOTSTRAP"] = self.config.doBootstrap
2037 for amp
in detector:
2038 if not math.isnan(gain := overscanDetectorConfig.getOverscanAmpConfig(amp).gain):
2039 gains[amp.getName()] = gain
2041 "Overriding gain for amp %s with configured value of %.3f.",
2048 self.log.debug(
"Converting exposure to floating point values.")
2060 if self.config.doDiffNonLinearCorrection:
2071 if overscanDetectorConfig.doAnySerialOverscan:
2074 overscanDetectorConfig,
2080 if self.config.doBootstrap
or self.config.useGainsFrom ==
"LINEARIZER":
2082 for amp, serialOverscan
in zip(detector, serialOverscans):
2083 if serialOverscan
is None:
2084 ptc.noise[amp.getName()] = 0.0
2092 ptc.noise[amp.getName()] = serialOverscan.residualSigma * gains[amp.getName()]
2094 serialOverscans = [
None]*len(detector)
2106 overscanDetectorConfig,
2112 if self.config.doCorrectGains
and gainCorrection
is not None:
2113 self.log.info(
"Correcting gains based on input GainCorrection.")
2114 gainCorrection.correctGains(gains, exposure=ccdExposure)
2115 exposureMetadata[
"LSST ISR GAINCORRECTION APPLIED"] =
True
2116 elif self.config.doCorrectGains:
2117 self.log.info(
"Skipping gain correction because no GainCorrection available.")
2122 if self.config.doApplyGains:
2123 self.log.info(
"Using gain values to convert from adu to electron units.")
2124 isrFunctions.applyGains(ccdExposure, normalizeGains=
False, ptcGains=gains, isTrimmed=
False)
2126 exposureMetadata[
"LSST ISR UNITS"] =
"electron"
2130 for amp
in detector:
2131 ampName = amp.getName()
2132 if (key := f
"LSST ISR SATURATION LEVEL {ampName}")
in exposureMetadata:
2133 exposureMetadata[key] *= gains[ampName]
2134 if (key := f
"LSST ISR SUSPECT LEVEL {ampName}")
in exposureMetadata:
2135 exposureMetadata[key] *= gains[ampName]
2138 metadata = ccdExposure.metadata
2139 metadata[
"LSST ISR READNOISE UNITS"] =
"electron"
2140 metadata[
"LSST ISR GAIN SOURCE"] = self.config.useGainsFrom
2141 for amp
in detector:
2143 metadata[f
"LSST ISR GAIN {amp.getName()}"] = gains[amp.getName()]
2146 noise = ptc.noise[amp.getName()]
2147 metadata[f
"LSST ISR READNOISE {amp.getName()}"] = noise
2151 if self.config.doCrosstalk:
2152 self.log.info(
"Applying crosstalk corrections to full amplifier region.")
2153 if self.config.doBootstrap
and numpy.any(crosstalk.fitGains != 0):
2154 crosstalkGains =
None
2156 crosstalkGains = gains
2159 crosstalk=crosstalk,
2160 gains=crosstalkGains,
2162 badAmpDict=badAmpDict,
2163 ignoreVariance=
True,
2165 ccdExposure.metadata[
"LSST ISR CROSSTALK APPLIED"] =
True
2168 if numpy.isfinite(self.config.serialOverscanMedianShiftSigmaThreshold):
2170 ccdExposure.metadata[
"LSST ISR OVERSCANLEVEL CHECKED"] =
True
2172 if numpy.isfinite(self.config.ampNoiseThreshold):
2173 badAmpDict = self.
checkAmpNoise(badAmpDict, ccdExposure, ptc)
2174 ccdExposure.metadata[
"LSST ISR NOISE CHECKED"] =
True
2176 if numpy.isfinite(self.config.serialOverscanMedianShiftSigmaThreshold)
or \
2177 numpy.isfinite(self.config.ampNoiseThreshold):
2182 parallelOverscans =
None
2183 if overscanDetectorConfig.doAnyParallelOverscan:
2187 overscanDetectorConfig,
2195 if self.config.doLinearize:
2196 self.log.info(
"Applying linearizer.")
2200 if exposureMetadata[
"LSST ISR UNITS"] ==
"electron":
2201 linearityGains = gains
2203 linearityGains =
None
2204 linearizer.applyLinearity(
2205 image=ccdExposure.image,
2208 gains=linearityGains,
2210 ccdExposure.metadata[
"LSST ISR LINEARIZER APPLIED"] =
True
2215 if self.config.doDeferredCharge:
2216 if self.config.doBootstrap:
2217 self.log.info(
"Applying deferred charge correction with doBootstrap=True: "
2218 "will need to use deferredChargeCalib.inputGain to apply "
2219 "CTI correction in electron units.")
2220 deferredChargeGains = deferredChargeCalib.inputGain
2221 if numpy.all(numpy.isnan(list(deferredChargeGains.values()))):
2222 self.log.warning(
"All gains contained in the deferredChargeCalib are "
2223 "NaN, approximating with gain of 1.0.")
2224 deferredChargeGains = gains
2226 deferredChargeGains = gains
2227 self.deferredChargeCorrection.run(
2229 deferredChargeCalib,
2230 gains=deferredChargeGains,
2232 ccdExposure.metadata[
"LSST ISR CTI APPLIED"] =
True
2236 untrimmedCcdExposure =
None
2237 if self.config.isrStats.doCtiStatistics:
2238 untrimmedCcdExposure = ccdExposure.clone()
2242 if self.config.doAssembleCcd:
2243 self.log.info(
"Assembling CCD from amplifiers.")
2244 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
2246 if self.config.expectWcs
and not ccdExposure.getWcs():
2247 self.log.warning(
"No WCS found in input exposure.")
2250 if self.config.doE2VEdgeBleedMask
and detector.getPhysicalType() ==
"E2V":
2251 isrFunctions.maskE2VEdgeBleed(
2252 exposure=ccdExposure,
2253 e2vEdgeBleedSatMinArea=self.config.e2vEdgeBleedSatMinArea,
2254 e2vEdgeBleedSatMaxArea=self.config.e2vEdgeBleedSatMaxArea,
2255 e2vEdgeBleedYMax=self.config.e2vEdgeBleedYMax,
2256 saturatedMaskName=self.config.saturatedMaskName,
2261 for maskPlane
in self.config.itlDipMaskPlanes:
2262 if maskPlane
not in ccdExposure.mask.getMaskPlaneDict():
2263 self.log.info(
"Adding %s mask plane to image.", maskPlane)
2264 ccdExposure.mask.addMaskPlane(maskPlane)
2266 if self.config.doITLDipMask:
2267 isrFunctions.maskITLDip(
2268 exposure=ccdExposure,
2269 detectorConfig=overscanDetectorConfig,
2271 maskPlaneNames=self.config.itlDipMaskPlanes,
2274 if (self.config.doITLSatSagMask
or self.config.doITLEdgeBleedMask) \
2275 and detector.getPhysicalType() ==
'ITL':
2277 badAmpDict=badAmpDict)
2281 if self.config.doBias:
2282 self.log.info(
"Applying bias correction.")
2285 isrFunctions.biasCorrection(ccdExposure.maskedImage, bias.maskedImage)
2286 ccdExposure.metadata[
"LSST ISR BIAS APPLIED"] =
True
2290 if self.config.doDark:
2291 self.log.info(
"Applying dark subtraction.")
2295 ccdExposure.metadata[
"LSST ISR DARK APPLIED"] =
True
2301 if self.config.doDefect:
2302 self.log.info(
"Applying defect masking.")
2304 ccdExposure.metadata[
"LSST ISR DEFECTS APPLIED"] =
True
2306 self.log.info(
"Adding UNMASKEDNAN mask plane to image.")
2307 ccdExposure.mask.addMaskPlane(
"UNMASKEDNAN")
2308 if self.config.doNanMasking:
2309 self.log.info(
"Masking non-finite (NAN, inf) value pixels.")
2312 if self.config.doWidenSaturationTrails:
2313 self.log.info(
"Widening saturation trails.")
2314 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
2318 if self.config.doBrighterFatter:
2319 self.log.info(
"Applying brighter-fatter correction.")
2325 if exposureMetadata[
"LSST ISR UNITS"] ==
"electron":
2326 brighterFatterApplyGain =
False
2328 brighterFatterApplyGain =
True
2330 if brighterFatterApplyGain
and (ptc
is not None)
and (bfGains != gains):
2337 self.log.warning(
"Need to apply gain for brighter-fatter, but the stored"
2338 "gains in the kernel are not the same as the gains used"
2339 f
"by {self.config.useGainsFrom}. Using the gains stored"
2347 brighterFatterApplyGain,
2351 ccdExposure.metadata[
"LSST ISR BF APPLIED"] =
True
2352 metadata[
"LSST ISR BF ITERS"] = bfCorrIters
2356 if self.config.doVariance:
2363 if self.config.doFlat:
2364 self.log.info(
"Applying flat correction.")
2365 isrFunctions.flatCorrection(
2366 maskedImage=ccdExposure.maskedImage,
2367 flatMaskedImage=flat.maskedImage,
2368 scalingType=self.config.flatScalingType,
2369 userScale=self.config.flatUserScale,
2376 if (validPolygon := flat.info.getValidPolygon())
is not None:
2377 ccdExposure.info.setValidPolygon(validPolygon)
2379 noData = (ccdExposure.mask.array & ccdExposure.mask.getPlaneBitMask(
"NO_DATA")) > 0
2380 ccdExposure.image.array[noData] = 0.0
2381 ccdExposure.variance.array[noData] = 0.0
2383 ccdExposure.metadata[
"LSST ISR FLAT APPLIED"] =
True
2384 ccdExposure.metadata[
"LSST ISR FLAT SOURCE"] = flat.metadata.get(
"FLATSRC",
"UNKNOWN")
2388 if self.config.doSaveInterpPixels:
2389 preInterpExp = ccdExposure.clone()
2391 if self.config.doSetBadRegions:
2392 self.log.info(
'Setting values in large contiguous bad regions.')
2395 if self.config.doInterpolate:
2396 self.log.info(
"Interpolating masked pixels.")
2397 isrFunctions.interpolateFromMask(
2398 maskedImage=ccdExposure.getMaskedImage(),
2399 fwhm=self.config.brighterFatterFwhmForInterpolation,
2400 growSaturatedFootprints=self.config.growSaturationFootprintSize,
2401 maskNameList=list(self.config.maskListToInterpolate),
2402 useLegacyInterp=self.config.useLegacyInterp,
2406 if self.config.doAmpOffset:
2407 if self.config.ampOffset.doApplyAmpOffset:
2408 self.log.info(
"Measuring and applying amp offset corrections.")
2410 self.log.info(
"Measuring amp offset corrections only, without applying them.")
2411 self.ampOffset.run(ccdExposure)
2414 if self.config.doStandardStatistics:
2415 metadata = ccdExposure.metadata
2416 for amp
in detector:
2417 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
2418 ampName = amp.getName()
2419 metadata[f
"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels(
2420 ampExposure.getMaskedImage(),
2421 [self.config.saturatedMaskName]
2423 metadata[f
"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels(
2424 ampExposure.getMaskedImage(),
2427 metadata[f
"LSST ISR MASK SUSPECT {ampName}"] = isrFunctions.countMaskedPixels(
2428 ampExposure.getMaskedImage(),
2432 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP)
2434 metadata[f
"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN)
2435 metadata[f
"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN)
2436 metadata[f
"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP)
2438 k1 = f
"LSST ISR FINAL MEDIAN {ampName}"
2439 k2 = f
"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
2440 if overscanDetectorConfig.doAnySerialOverscan
and k1
in metadata
and k2
in metadata:
2441 metadata[f
"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2]
2443 metadata[f
"LSST ISR LEVEL {ampName}"] = numpy.nan
2446 outputStatistics =
None
2447 if self.config.doCalculateStatistics:
2448 outputStatistics = self.isrStats.run(ccdExposure,
2449 untrimmedInputExposure=untrimmedCcdExposure,
2450 serialOverscanResults=serialOverscans,
2451 parallelOverscanResults=parallelOverscans,
2452 bias=bias, dark=dark, flat=flat,
2453 ptc=ptc, defects=defects).results
2456 outputBin1Exposure =
None
2457 outputBin2Exposure =
None
2458 if self.config.doBinnedExposures:
2459 self.log.info(
"Creating binned exposures.")
2460 outputBin1Exposure = self.binning.run(
2462 binFactor=self.config.binFactor1,
2464 outputBin2Exposure = self.binning.run(
2466 binFactor=self.config.binFactor2,
2469 return pipeBase.Struct(
2470 exposure=ccdExposure,
2472 outputBin1Exposure=outputBin1Exposure,
2473 outputBin2Exposure=outputBin2Exposure,
2475 preInterpExposure=preInterpExp,
2476 outputExposure=ccdExposure,
2477 outputStatistics=outputStatistics,