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