1024 ):
1025 """Apply intra-detector crosstalk correction
1026
1027 Parameters
1028 ----------
1029 exposure : `lsst.afw.image.Exposure`
1030 Exposure for which to remove crosstalk.
1031 crosstalkCalib : `lsst.ip.isr.CrosstalkCalib`, optional
1032 External crosstalk calibration to apply. Constructed from
1033 detector if not found.
1034 crosstalkSources : `defaultdict`, optional
1035 Image data for other detectors that are sources of
1036 crosstalk in exposure. The keys are expected to be names
1037 of the other detectors, with the values containing
1038 `lsst.afw.image.Exposure` at the same level of processing
1039 as ``exposure``.
1040 The default for intra-detector crosstalk here is None.
1041 isTrimmed : `bool`, optional
1042 This option has been deprecated and does not do anything.
1043 It will be removed after v29.
1044 camera : `lsst.afw.cameraGeom.Camera`, optional
1045 Camera associated with this exposure. Only used for
1046 inter-chip matching.
1047 parallelOverscanRegion : `bool`, optional
1048 This option has been deprecated and will be removed after v29.
1049 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`, optional
1050 This option has been deprecated and will be removed after v29.
1051 fullAmplifier : `bool`, optional
1052 Use full amplifier and not just imaging region.
1053 gains : `dict` [`str`, `float`], optional
1054 Dictionary of amp name to gain. Required if there is a unit
1055 mismatch between the exposure and the crosstalk matrix.
1056 badAmpDict : `dict` [`str`, `bool`], optional
1057 Dictionary to identify bad amplifiers that should not be
1058 source or target for crosstalk correction.
1059 ignoreVariance : `bool`, optional
1060 Ignore variance plane when applying crosstalk?
1061
1062 Raises
1063 ------
1064 RuntimeError
1065 Raised if called for a detector that does not have a
1066 crosstalk correction. Also raised if the crosstalkSource
1067 is not an expected type.
1068 """
1069
1070 if isTrimmed is not None:
1071 warnings.warn(
1072 "The isTrimmed option has been deprecated and will be removed after v29.",
1073 FutureWarning,
1074 )
1075 isTrimmed = isTrimmedImage(exposure.maskedImage, exposure.getDetector())
1076
1077
1078 if parallelOverscanRegion is not None:
1079 warnings.warn(
1080 "The parallelOverscanRegion option has been deprecated and will be removed after v29.",
1081 FutureWarning,
1082 )
1083 if detectorConfig is not None:
1084 warnings.warn(
1085 "The detectorConfig option has been deprecated and will be removed after v29.",
1086 FutureWarning,
1087 )
1088
1089 if not crosstalk:
1090 crosstalk = CrosstalkCalib(log=self.log)
1091 crosstalk = crosstalk.fromDetector(exposure.getDetector(),
1092 coeffVector=self.config.crosstalkValues)
1093 if not crosstalk.log:
1094 crosstalk.log = self.log
1095
1096 doSqrCrosstalk = self.config.doQuadraticCrosstalkCorrection
1097
1098 if doSqrCrosstalk and crosstalk.coeffsSqr is None:
1099 raise RuntimeError("Attempted to perform NL crosstalk correction without NL "
1100 "crosstalk coefficients.")
1101 if doSqrCrosstalk:
1102 crosstalkCoeffsSqr = crosstalk.coeffsSqr
1103 else:
1104 crosstalkCoeffsSqr = None
1105
1106 if not crosstalk.hasCrosstalk:
1107 raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients.")
1108
1109
1110 exposureUnits = exposure.metadata.get("LSST ISR UNITS", "adu")
1111 invertGains = False
1112 gainApply = False
1113 if crosstalk.crosstalkRatiosUnits != exposureUnits:
1114 detector = exposure.getDetector()
1115
1116 if gains is None and np.all(crosstalk.fitGains == 0.0):
1117 raise RuntimeError(
1118 f"Unit mismatch between exposure ({exposureUnits}) and "
1119 f"crosstalk ratios ({crosstalk.crosstalkRatiosUnits}) and "
1120 "no gains were supplied or available in crosstalk calibration.",
1121 )
1122 elif gains is None:
1123 self.log.info("Using crosstalk calib fitGains for gain corrections.")
1124 gains = {}
1125 for i, amp in enumerate(detector):
1126 gains[amp.getName()] = crosstalk.fitGains[i]
1127
1128
1129 gainArray = np.zeros(len(detector))
1130 for i, amp in enumerate(detector):
1131 gainArray[i] = gains[amp.getName()]
1132 badGains = (~np.isfinite(gainArray) | (gainArray == 0.0))
1133 if np.any(badGains):
1134 if np.all(badGains):
1135 raise RuntimeError("No valid (finite, non-zero) gains found for crosstalk correction.")
1136
1137 badAmpNames = [amp.getName() for i, amp in enumerate(detector) if badGains[i]]
1138
1139 self.log.warning("Illegal gain value found for %d amplifiers %r in crosstalk correction; "
1140 "substituting with median gain.", badGains.sum(), badAmpNames)
1141 medGain = np.median(gainArray[~badGains])
1142 gainArray[badGains] = medGain
1143 for i, amp in enumerate(detector):
1144 gains[amp.getName()] = gainArray[i]
1145
1146 gainApply = True
1147
1148 if crosstalk.crosstalkRatiosUnits == "adu":
1149 invertGains = True
1150
1151 if exposureUnits == "adu":
1152 self.log.info("Temporarily applying gains to perform crosstalk correction "
1153 "because crosstalk is in electron units and exposure is in "
1154 "adu units.")
1155 else:
1156 self.log.info("Temporarily un-applying gains to perform crosstalk correction "
1157 "because crosstalk is in adu units and exposure is in "
1158 "electron units.")
1159
1160 self.log.info("Applying crosstalk correction.")
1161
1162 with gainContext(
1163 exposure,
1164 exposure.image,
1165 gainApply,
1166 gains=gains,
1167 invert=invertGains,
1168 isTrimmed=isTrimmed,
1169 ):
1170 crosstalk.subtractCrosstalk(
1171 exposure,
1172 crosstalkCoeffs=crosstalk.coeffs,
1173 crosstalkCoeffsSqr=crosstalkCoeffsSqr,
1174 crosstalkCoeffsValid=crosstalk.coeffValid,
1175 minPixelToMask=self.config.minPixelToMask,
1176 doSubtrahendMasking=self.config.doSubtrahendMasking,
1177 crosstalkStr=self.config.crosstalkMaskPlane,
1178 backgroundMethod=self.config.crosstalkBackgroundMethod,
1179 doSqrCrosstalk=doSqrCrosstalk,
1180 fullAmplifier=fullAmplifier,
1181 badAmpDict=badAmpDict,
1182 ignoreVariance=ignoreVariance,
1183 )
1184
1185 if crosstalk.interChip:
1186 if crosstalkSources:
1187
1188
1190
1191 sourceNames = [exp.getDetector().getName() for exp in crosstalkSources]
1192 elif isinstance(crosstalkSources[0], lsst.daf.butler.DeferredDatasetHandle):
1193
1194 detectorList = [source.dataId['detector'] for source in crosstalkSources]
1195 sourceNames = [camera[detectorId].getName() for detectorId in detectorList]
1196 else:
1197 raise RuntimeError("Unknown object passed as crosstalk sources.",
1198 type(crosstalkSources[0]))
1199
1200 for detName in crosstalk.interChip:
1201 if detName not in sourceNames:
1202 self.log.warning("Crosstalk lists %s, not found in sources: %s",
1203 detName, sourceNames)
1204 continue
1205
1206 interChipCoeffs = crosstalk.interChip[detName]
1207
1208 sourceExposure = crosstalkSources[sourceNames.index(detName)]
1209 if isinstance(sourceExposure, lsst.daf.butler.DeferredDatasetHandle):
1210
1211 sourceExposure = sourceExposure.get()
1213 raise RuntimeError("Unknown object passed as crosstalk sources.",
1214 type(sourceExposure))
1215
1216 if sourceExposure.getBBox() != exposure.getBBox():
1217 raise RuntimeError(
1218 "Mis-match between exposure bounding box and crosstalk source "
1219 "exposure bounding box. Were these run with the same trim state?",
1220 )
1221
1222 self.log.info("Correcting detector %s with ctSource %s",
1223 exposure.getDetector().getName(),
1224 sourceExposure.getDetector().getName())
1225 crosstalk.subtractCrosstalk(
1226 exposure,
1227 sourceExposure=sourceExposure,
1228 crosstalkCoeffs=interChipCoeffs,
1229 minPixelToMask=self.config.minPixelToMask,
1230 crosstalkStr=self.config.crosstalkMaskPlane,
1231 backgroundMethod=self.config.crosstalkBackgroundMethod,
1232 ignoreVariance=ignoreVariance,
1233 )
1234 else:
1235 self.log.warning("Crosstalk contains interChip coefficients, but no sources found!")
1236
1237
A class to contain the data, WCS, and other information needed to describe an image of the sky.