146 def run(self, exposure):
147 """Calculate amp offset values, determine corrective pedestals for each
148 amp, and update the input exposure in-place.
149
150 Parameters
151 ----------
152 exposure: `lsst.afw.image.Exposure`
153 Exposure to be corrected for amp offsets.
154 """
155
156
157 exp = exposure.clone()
158 bitMask = exp.mask.getPlaneBitMask(self.background.config.ignoredPixelMask)
159 amps = exp.getDetector().getAmplifiers()
160
161
162 ampDims = [amp.getBBox().getDimensions() for amp in amps]
163 if not all(dim == ampDims[0] for dim in ampDims):
164 raise RuntimeError("All amps should have the same geometry.")
165 else:
166
167 self.ampDims = ampDims[0]
168
169
170 self.interfaceLengthLookupBySide = {i: self.ampDims[i % 2] for i in range(4)}
171
172
173 ampWidths = {amp.getBBox().getWidth() for amp in amps}
174 ampHeights = {amp.getBBox().getHeight() for amp in amps}
175 if len(ampWidths) > 1 or len(ampHeights) > 1:
176 raise NotImplementedError(
177 "Amp offset correction is not yet implemented for detectors with differing amp sizes."
178 )
179
180
181 self.shortAmpSide = np.min(ampDims[0])
182
183
184 if self.config.ampEdgeWidth >= self.shortAmpSide - 2 * self.config.ampEdgeInset:
185 raise RuntimeError(
186 f"The edge width ({self.config.ampEdgeWidth}) plus insets ({self.config.ampEdgeInset}) "
187 f"exceed the amp's short side ({self.shortAmpSide}). This setup leads to incorrect results."
188 )
189
190
191 if self.config.doBackground:
192 maskedImage = exp.getMaskedImage()
193
194 nX = exp.getWidth() // (self.shortAmpSide * self.config.backgroundFractionSample) + 1
195 nY = exp.getHeight() // (self.shortAmpSide * self.config.backgroundFractionSample) + 1
196
197
198
199
200
201 bg = self.background.fitBackground(maskedImage, nx=int(nX), ny=int(nY))
202 bgImage = bg.getImageF(self.background.config.algorithm, self.background.config.undersampleStyle)
203 maskedImage -= bgImage
204
205
206 if self.config.doDetection:
207 schema = SourceTable.makeMinimalSchema()
208 table = SourceTable.make(schema)
209
210
211
212
213 _ = self.detection.run(table=table, exposure=exp, sigma=2)
214
215
216 if (exp.mask.array & bitMask).all():
217 log_fn = self.log.warning if self.config.doApplyAmpOffset else self.log.info
218 log_fn(
219 "All pixels masked: cannot calculate any amp offset corrections. "
220 "All pedestals are being set to zero."
221 )
222 pedestals = np.zeros(len(amps))
223 else:
224
225 im = exp.image
226 im.array[(exp.mask.array & bitMask) > 0] = np.nan
227
228 if self.config.ampEdgeWindowFrac > 1:
229 raise RuntimeError(
230 f"The specified fraction (`ampEdgeWindowFrac`={self.config.ampEdgeWindowFrac}) of the "
231 "edge length exceeds 1. This leads to complications downstream, after convolution in "
232 "the `getInterfaceOffset()` method. Please modify the `ampEdgeWindowFrac` value in the "
233 "config to be 1 or less and rerun."
234 )
235
236
237 A, sides = self.getAmpAssociations(amps)
238 B, interfaceOffsetDict = self.getAmpOffsets(im, amps, A, sides)
239
240
241
242 pedestals = np.nan_to_num(np.linalg.lstsq(A, B, rcond=None)[0])
243
244 metadata = exposure.getMetadata()
245 self.metadata["AMPOFFSET_PEDESTALS"] = {}
246 ampNames = [amp.getName() for amp in amps]
247
248
249 for interfaceId, interfaceOffset in interfaceOffsetDict.items():
250 metadata.set(
251 f"LSST ISR AMPOFFSET INTERFACEOFFSET {interfaceId}",
252 float(interfaceOffset),
253 f"Raw amp interface offset calculated for {interfaceId}",
254 )
255
256 for ampName, amp, pedestal in zip(ampNames, amps, pedestals):
257
258 metadata.set(
259 f"LSST ISR AMPOFFSET PEDESTAL {ampName}",
260 float(pedestal),
261 f"Pedestal level calculated for amp {ampName}",
262 )
263 if self.config.doApplyAmpOffset:
264 ampIm = exposure.image[amp.getBBox()].array
265 ampIm -= pedestal
266
267
268 self.metadata["AMPOFFSET_PEDESTALS"][ampName] = float(pedestal)
269 if self.config.doApplyAmpOffset:
270 status = "subtracted from exposure"
271 metadata.set("LSST ISR AMPOFFSET PEDESTAL SUBTRACTED", True, "Amp pedestals have been subtracted")
272 else:
273 status = "not subtracted from exposure"
274 metadata.set(
275 "LSST ISR AMPOFFSET PEDESTAL SUBTRACTED", False, "Amp pedestals have not been subtracted"
276 )
277 ampPedestalReport = ", ".join(
278 [f"{ampName}: {ampPedestal:.4f}" for (ampName, ampPedestal) in zip(ampNames, pedestals)]
279 )
280 self.log.info(f"Amp pedestal values ({status}): {ampPedestalReport}")
281
282 return Struct(pedestals=pedestals)
283