129 """Calculate amp offset values, determine corrective pedestals for each
130 amp, and update the input exposure in-place.
134 exposure: `lsst.afw.image.Exposure`
135 Exposure to be corrected for amp offsets.
139 exp = exposure.clone()
140 bitMask = exp.mask.getPlaneBitMask(self.background.config.ignoredPixelMask)
141 amps = exp.getDetector().getAmplifiers()
144 ampDims = [amp.getBBox().getDimensions()
for amp
in amps]
145 if not all(dim == ampDims[0]
for dim
in ampDims):
146 raise RuntimeError(
"All amps should have the same geometry.")
152 if self.config.doBackground:
153 maskedImage = exp.getMaskedImage()
155 nX = exp.getWidth() // (self.
shortAmpSide * self.config.backgroundFractionSample) + 1
156 nY = exp.getHeight() // (self.
shortAmpSide * self.config.backgroundFractionSample) + 1
162 bg = self.background.fitBackground(maskedImage, nx=int(nX), ny=int(nY))
163 bgImage = bg.getImageF(self.background.config.algorithm, self.background.config.undersampleStyle)
164 maskedImage -= bgImage
167 if self.config.doDetection:
168 schema = SourceTable.makeMinimalSchema()
169 table = SourceTable.make(schema)
174 _ = self.detection.
run(table=table, exposure=exp, sigma=2)
177 if (exp.mask.array & bitMask).all():
179 "All pixels masked: cannot calculate any amp offset corrections. All pedestals are being set "
182 pedestals = np.zeros(len(amps))
186 im.array[(exp.mask.array & bitMask) > 0] = np.nan
188 if self.config.ampEdgeWindowFrac > 1:
190 f
"The specified fraction (`ampEdgeWindowFrac`={self.config.ampEdgeWindowFrac}) of the "
191 "edge length exceeds 1. This leads to complications downstream, after convolution in "
192 "the `getSideAmpOffset()` method. Please modify the `ampEdgeWindowFrac` value in the "
193 "config to be 1 or less and rerun."
197 ampAreas = {amp.getBBox().getArea()
for amp
in amps}
198 if len(ampAreas) > 1:
199 raise NotImplementedError(
200 "Amp offset correction is not yet implemented for detectors with differing amp sizes."
209 pedestals = np.nan_to_num(np.linalg.lstsq(A, B, rcond=
None)[0])
211 metadata = exposure.getMetadata()
212 for amp, pedestal
in zip(amps, pedestals):
213 ampIm = exposure.image[amp.getBBox()].array
215 ampName = amp.getName()
217 f
"LSST ISR AMPOFFSET PEDESTAL {ampName}",
219 f
"Pedestal level subtracted from amp {ampName}",
221 self.log.info(f
"amp pedestal values: {', '.join([f'{x:.4f}' for x in pedestals])}")
223 return Struct(pedestals=pedestals)
226 """Determine amp geometry and amp associations from a list of
229 Parse an input list of amplifiers to determine the layout of amps
230 within a detector, and identify all amp sides (i.e., the
231 horizontal and vertical junctions between amps).
233 Returns a matrix with a shape corresponding to the geometry of the amps
238 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
239 List of amplifier objects used to deduce associations.
243 ampAssociations : `numpy.ndarray`
244 An N x N matrix (N = number of amplifiers) that illustrates the
245 connections between amplifiers within the detector layout. Each row
246 and column index corresponds to the ampIds of a specific pair of
247 amplifiers, and the matrix elements indicate their associations as
250 -1: Association exists (direction specified in the ampSides matrix)
251 n >= 1: Diagonal elements indicate the number of neighboring
252 amplifiers for the corresponding ampId==row==column number.
254 ampSides : `numpy.ndarray`
255 An N x N matrix (N = the number of amplifiers) representing the amp
256 side information corresponding to the `ampAssociations`
257 matrix. The elements are integers defined as below:
258 -1: No side due to no association or the same amp (diagonals)
259 0: Side on the bottom
264 xCenters = [amp.getBBox().getCenterX()
for amp
in amps]
265 yCenters = [amp.getBBox().getCenterY()
for amp
in amps]
266 xIndices = np.ceil(xCenters / np.min(xCenters) / 2).astype(int) - 1
267 yIndices = np.ceil(yCenters / np.min(yCenters) / 2).astype(int) - 1
270 ampIds = np.zeros((len(set(yIndices)), len(set(xIndices))), dtype=int)
272 for ampId, xIndex, yIndex
in zip(np.arange(nAmps), xIndices, yIndices):
273 ampIds[yIndex, xIndex] = ampId
275 ampAssociations = np.zeros((nAmps, nAmps), dtype=int)
276 ampSides = np.full_like(ampAssociations, -1)
278 for ampId
in ampIds.ravel():
280 ampAssociations[ampId, neighbors] = -1
281 ampSides[ampId, neighbors] = sides
282 ampAssociations[ampId, ampId] = -ampAssociations[ampId].sum()
284 if ampAssociations.sum() != 0:
285 raise RuntimeError(
"The `ampAssociations` array does not sum to zero.")
287 if not np.all(ampAssociations == ampAssociations.T):
288 raise RuntimeError(
"The `ampAssociations` is not symmetric about the diagonal.")
290 self.log.debug(
"amp associations:\n%s", ampAssociations)
291 self.log.debug(
"amp sides:\n%s", ampSides)
293 return ampAssociations, ampSides
331 """Calculate the amp offsets for all amplifiers.
335 im : `lsst.afw.image._image.ImageF`
336 Amplifier image to extract data from.
337 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
338 List of amplifier objects.
339 associations : numpy.ndarray
340 An N x N matrix containing amp association information, where N is
341 the number of amplifiers.
342 sides : numpy.ndarray
343 An N x N matrix containing amp side information, where N is the
344 number of amplifiers.
348 ampsOffsets : `numpy.ndarray`
349 1D float array containing the calculated amp offsets for all
352 ampsOffsets = np.zeros(len(amps))
354 interfaceOffsetLookup = {}
355 for ampId, ampAssociations
in enumerate(associations):
356 ampNeighbors = np.ravel(np.where(ampAssociations < 0))
357 for ampNeighbor
in ampNeighbors:
358 ampSide = sides[ampId][ampNeighbor]
359 edgeA = ampsEdges[ampId][ampSide]
360 edgeB = ampsEdges[ampNeighbor][(ampSide + 2) % 4]
361 if ampId < ampNeighbor:
363 interfaceOffsetLookup[f
"{ampId}{ampNeighbor}"] = interfaceOffset
365 interfaceOffset = -interfaceOffsetLookup[f
"{ampNeighbor}{ampId}"]
366 ampsOffsets[ampId] += interfaceOffset
370 """Calculate the amp edges for all amplifiers.
374 im : `lsst.afw.image._image.ImageF`
375 Amplifier image to extract data from.
376 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
377 List of amplifier objects.
378 ampSides : `numpy.ndarray`
379 An N x N matrix containing amp side information, where N is the
380 number of amplifiers.
384 ampEdges : `dict` [`int`, `dict` [`int`, `numpy.ndarray`]]
385 A dictionary containing amp edge(s) for each amplifier,
386 corresponding to one or more potential sides, where each edge is
387 associated with a side. The outer dictionary has integer keys
388 representing amplifier IDs, and the inner dictionary has integer
389 keys representing side IDs for each amplifier and values that are
390 1D arrays of floats representing the 1D medianified strips from the
391 amp image, referred to as "amp edge":
392 {ampID: {sideID: numpy.ndarray}, ...}
394 ampEdgeOuter = self.config.ampEdgeInset + self.config.ampEdgeWidth
397 0: (slice(-ampEdgeOuter, -self.config.ampEdgeInset), slice(
None)),
398 1: (slice(
None), slice(-ampEdgeOuter, -self.config.ampEdgeInset)),
399 2: (slice(self.config.ampEdgeInset, ampEdgeOuter), slice(
None)),
400 3: (slice(
None), slice(self.config.ampEdgeInset, ampEdgeOuter)),
402 for ampId, (amp, ampSides)
in enumerate(zip(amps, ampSides)):
404 ampIm = im[amp.getBBox()].array
406 for ampSide
in ampSides:
409 strip = ampIm[slice_map[ampSide]]
411 with warnings.catch_warnings():
412 warnings.filterwarnings(
"ignore",
r"All-NaN (slice|axis) encountered")
413 ampEdges[ampId][ampSide] = np.nanmedian(strip, axis=ampSide % 2)
417 """Calculate the amp offset for a given interface between two
423 ID of the first amplifier.
425 ID of the second amplifier.
426 edgeA : numpy.ndarray
427 Amp edge for the first amplifier.
428 edgeB : numpy.ndarray
429 Amp edge for the second amplifier.
433 interfaceOffset : float
434 The calculated amp offset value for the given interface between
437 interfaceId = f
"{ampIdA}{ampIdB}"
441 edgeDiff = edgeA - edgeB
442 window = int(self.config.ampEdgeWindowFrac * len(edgeDiff))
444 edgeDiffSum = np.convolve(np.nan_to_num(edgeDiff), np.ones(window),
"same")
445 edgeDiffNum = np.convolve(~np.isnan(edgeDiff), np.ones(window),
"same")
446 edgeDiffAvg = edgeDiffSum / np.clip(edgeDiffNum, 1,
None)
447 edgeDiffAvg[np.isnan(edgeDiff)] = np.nan
449 interfaceOffset = makeStatistics(edgeDiffAvg, MEANCLIP, sctrl).getValue()
453 ampEdgeGoodFrac = 1 - (np.sum(np.isnan(edgeDiffAvg)) / len(edgeDiffAvg))
454 minFracFail = ampEdgeGoodFrac < self.config.ampEdgeMinFrac
455 maxOffsetFail = np.abs(interfaceOffset) > self.config.ampEdgeMaxOffset
456 if minFracFail
or maxOffsetFail:
459 f
"The fraction of unmasked pixels for amp interface {interfaceId} is below the threshold "
460 f
"({self.config.ampEdgeMinFrac}) or the absolute offset value exceeds the limit "
461 f
"({self.config.ampEdgeMaxOffset} ADU). Setting the interface offset to 0."
464 f
"amp interface {interfaceId} : "
465 f
"viable edge difference frac = {ampEdgeGoodFrac}, "
466 f
"interface offset = {interfaceOffset:.3f}"
468 return interfaceOffset