136 """Calculate amp offset values, determine corrective pedestals for each
137 amp, and update the input exposure in-place.
141 exposure: `lsst.afw.image.Exposure`
142 Exposure to be corrected for amp offsets.
146 exp = exposure.clone()
147 bitMask = exp.mask.getPlaneBitMask(self.background.config.ignoredPixelMask)
148 amps = exp.getDetector().getAmplifiers()
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.")
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."
173 if self.config.ampEdgeWidth >= self.
shortAmpSide - 2 * self.config.ampEdgeInset:
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."
180 if self.config.doBackground:
181 maskedImage = exp.getMaskedImage()
183 nX = exp.getWidth() // (self.
shortAmpSide * self.config.backgroundFractionSample) + 1
184 nY = exp.getHeight() // (self.
shortAmpSide * self.config.backgroundFractionSample) + 1
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
195 if self.config.doDetection:
196 schema = SourceTable.makeMinimalSchema()
197 table = SourceTable.make(schema)
202 _ = self.detection.
run(table=table, exposure=exp, sigma=2)
205 if (exp.mask.array & bitMask).all():
207 "All pixels masked: cannot calculate any amp offset corrections. All pedestals are being set "
210 pedestals = np.zeros(len(amps))
214 im.array[(exp.mask.array & bitMask) > 0] = np.nan
216 if self.config.ampEdgeWindowFrac > 1:
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."
230 pedestals = np.nan_to_num(np.linalg.lstsq(A, B, rcond=
None)[0])
232 metadata = exposure.getMetadata()
233 for amp, pedestal
in zip(amps, pedestals):
234 ampIm = exposure.image[amp.getBBox()].array
236 ampName = amp.getName()
238 f
"LSST ISR AMPOFFSET PEDESTAL {ampName}",
240 f
"Pedestal level subtracted from amp {ampName}",
242 self.log.info(f
"amp pedestal values: {', '.join([f'{x:.4f}' for x in pedestals])}")
244 return Struct(pedestals=pedestals)
247 """Determine amp geometry and amp associations from a list of
250 Parse an input list of amplifiers to determine the layout of amps
251 within a detector, and identify all amp sides (i.e., the
252 horizontal and vertical junctions between amps).
254 Returns a matrix with a shape corresponding to the geometry of the amps
259 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
260 List of amplifier objects used to deduce associations.
264 ampAssociations : `numpy.ndarray`
265 An N x N matrix (N = number of amplifiers) that illustrates the
266 connections between amplifiers within the detector layout. Each row
267 and column index corresponds to the ampIds of a specific pair of
268 amplifiers, and the matrix elements indicate their associations as
272 * -1: Association exists (direction specified in the ampSides
274 * n >= 1: Diagonal elements indicate the number of neighboring
275 amplifiers for the corresponding ampId==row==column number.
277 ampSides : `numpy.ndarray`
278 An N x N matrix (N = the number of amplifiers) representing the amp
279 side information corresponding to the `ampAssociations`
280 matrix. The elements are integers defined as below:
282 * -1: No side due to no association or the same amp (diagonals)
283 * 0: Side on the bottom
284 * 1: Side on the right
286 * 3: Side on the left
288 xCenters = [amp.getBBox().getCenterX()
for amp
in amps]
289 yCenters = [amp.getBBox().getCenterY()
for amp
in amps]
290 xIndices = np.ceil(xCenters / np.min(xCenters) / 2).astype(int) - 1
291 yIndices = np.ceil(yCenters / np.min(yCenters) / 2).astype(int) - 1
294 ampIds = np.zeros((len(set(yIndices)), len(set(xIndices))), dtype=int)
296 for ampId, xIndex, yIndex
in zip(np.arange(nAmps), xIndices, yIndices):
297 ampIds[yIndex, xIndex] = ampId
299 ampAssociations = np.zeros((nAmps, nAmps), dtype=int)
300 ampSides = np.full_like(ampAssociations, -1)
302 for ampId
in ampIds.ravel():
306 if not self.config.applyWeights
309 ampAssociations[ampId, neighbors] = -1 * interfaceWeights
310 ampSides[ampId, neighbors] = sides
311 ampAssociations[ampId, ampId] = -ampAssociations[ampId].sum()
313 if ampAssociations.sum() != 0:
314 raise RuntimeError(
"The `ampAssociations` array does not sum to zero.")
316 if not np.all(ampAssociations == ampAssociations.T):
317 raise RuntimeError(
"The `ampAssociations` is not symmetric about the diagonal.")
319 self.log.debug(
"amp associations:\n%s", ampAssociations)
320 self.log.debug(
"amp sides:\n%s", ampSides)
322 return ampAssociations, ampSides
360 """Calculate the amp offsets for all amplifiers.
364 im : `lsst.afw.image._image.ImageF`
365 Amplifier image to extract data from.
366 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
367 List of amplifier objects.
368 associations : numpy.ndarray
369 An N x N matrix containing amp association information, where N is
370 the number of amplifiers.
371 sides : numpy.ndarray
372 An N x N matrix containing amp side information, where N is the
373 number of amplifiers.
377 ampsOffsets : `numpy.ndarray`
378 1D float array containing the calculated amp offsets for all
381 ampsOffsets = np.zeros(len(amps))
383 interfaceOffsetLookup = {}
385 for ampId, ampAssociations
in enumerate(associations):
386 ampNeighbors = np.ravel(np.where(ampAssociations < 0))
387 for ampNeighbor
in ampNeighbors:
388 ampSide = sides[ampId][ampNeighbor]
392 edgeA = ampsEdges[ampId][ampSide]
393 edgeB = ampsEdges[ampNeighbor][(ampSide + 2) % 4]
394 if ampId < ampNeighbor:
396 interfaceOffsetLookup[f
"{ampId}{ampNeighbor}"] = interfaceOffset
398 interfaceOffset = -interfaceOffsetLookup[f
"{ampNeighbor}{ampId}"]
399 ampsOffsets[ampId] += interfaceWeight * interfaceOffset
403 """Calculate the amp edges for all amplifiers.
407 im : `lsst.afw.image._image.ImageF`
408 Amplifier image to extract data from.
409 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
410 List of amplifier objects.
411 ampSides : `numpy.ndarray`
412 An N x N matrix containing amp side information, where N is the
413 number of amplifiers.
417 ampEdges : `dict` [`int`, `dict` [`int`, `numpy.ndarray`]]
418 A dictionary containing amp edge(s) for each amplifier,
419 corresponding to one or more potential sides, where each edge is
420 associated with a side. The outer dictionary has integer keys
421 representing amplifier IDs, and the inner dictionary has integer
422 keys representing side IDs for each amplifier and values that are
423 1D arrays of floats representing the 1D medianified strips from the
424 amp image, referred to as "amp edge":
425 {ampID: {sideID: numpy.ndarray}, ...}
427 ampEdgeOuter = self.config.ampEdgeInset + self.config.ampEdgeWidth
430 0: (slice(-ampEdgeOuter, -self.config.ampEdgeInset), slice(
None)),
431 1: (slice(
None), slice(-ampEdgeOuter, -self.config.ampEdgeInset)),
432 2: (slice(self.config.ampEdgeInset, ampEdgeOuter), slice(
None)),
433 3: (slice(
None), slice(self.config.ampEdgeInset, ampEdgeOuter)),
435 for ampId, (amp, ampSides)
in enumerate(zip(amps, ampSides)):
437 ampIm = im[amp.getBBox()].array
439 for ampSide
in ampSides:
442 strip = ampIm[slice_map[ampSide]]
444 with warnings.catch_warnings():
445 warnings.filterwarnings(
"ignore",
r"All-NaN (slice|axis) encountered")
446 ampEdges[ampId][ampSide] = np.nanmedian(strip, axis=ampSide % 2)
450 """Calculate the amp offset for a given interface between two
456 ID of the first amplifier.
458 ID of the second amplifier.
459 edgeA : numpy.ndarray
460 Amp edge for the first amplifier.
461 edgeB : numpy.ndarray
462 Amp edge for the second amplifier.
466 interfaceOffset : float
467 The calculated amp offset value for the given interface between
470 interfaceId = f
"{ampIdA}{ampIdB}"
474 edgeDiff = edgeA - edgeB
475 window = int(self.config.ampEdgeWindowFrac * len(edgeDiff))
477 edgeDiffSum = np.convolve(np.nan_to_num(edgeDiff), np.ones(window),
"same")
478 edgeDiffNum = np.convolve(~np.isnan(edgeDiff), np.ones(window),
"same")
479 edgeDiffAvg = edgeDiffSum / np.clip(edgeDiffNum, 1,
None)
480 edgeDiffAvg[np.isnan(edgeDiff)] = np.nan
482 interfaceOffset = makeStatistics(edgeDiffAvg, MEANCLIP, sctrl).getValue()
486 ampEdgeGoodFrac = 1 - (np.sum(np.isnan(edgeDiffAvg)) / len(edgeDiffAvg))
487 minFracFail = ampEdgeGoodFrac < self.config.ampEdgeMinFrac
488 maxOffsetFail = np.abs(interfaceOffset) > self.config.ampEdgeMaxOffset
489 if minFracFail
or maxOffsetFail:
493 f
"The fraction of unmasked pixels for amp interface {interfaceId} is below the threshold "
494 f
"({ampEdgeGoodFrac:.2f} < {self.config.ampEdgeMinFrac}). Setting the interface offset "
495 f
"to {interfaceOffset}."
499 "The absolute offset value exceeds the limit "
500 f
"({np.abs(interfaceOffset):.2f} > {self.config.ampEdgeMaxOffset} ADU). Setting the "
501 f
"interface offset to {interfaceOffset}."
504 f
"amp interface {interfaceId} : "
505 f
"viable edge difference frac = {ampEdgeGoodFrac}, "
506 f
"interface offset = {interfaceOffset:.3f}"
508 return interfaceOffset