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
271 -1: Association exists (direction specified in the ampSides matrix)
272 n >= 1: Diagonal elements indicate the number of neighboring
273 amplifiers for the corresponding ampId==row==column number.
275 ampSides : `numpy.ndarray`
276 An N x N matrix (N = the number of amplifiers) representing the amp
277 side information corresponding to the `ampAssociations`
278 matrix. The elements are integers defined as below:
279 -1: No side due to no association or the same amp (diagonals)
280 0: Side on the bottom
285 xCenters = [amp.getBBox().getCenterX()
for amp
in amps]
286 yCenters = [amp.getBBox().getCenterY()
for amp
in amps]
287 xIndices = np.ceil(xCenters / np.min(xCenters) / 2).astype(int) - 1
288 yIndices = np.ceil(yCenters / np.min(yCenters) / 2).astype(int) - 1
291 ampIds = np.zeros((len(set(yIndices)), len(set(xIndices))), dtype=int)
293 for ampId, xIndex, yIndex
in zip(np.arange(nAmps), xIndices, yIndices):
294 ampIds[yIndex, xIndex] = ampId
296 ampAssociations = np.zeros((nAmps, nAmps), dtype=int)
297 ampSides = np.full_like(ampAssociations, -1)
299 for ampId
in ampIds.ravel():
303 if not self.config.applyWeights
306 ampAssociations[ampId, neighbors] = -1 * interfaceWeights
307 ampSides[ampId, neighbors] = sides
308 ampAssociations[ampId, ampId] = -ampAssociations[ampId].sum()
310 if ampAssociations.sum() != 0:
311 raise RuntimeError(
"The `ampAssociations` array does not sum to zero.")
313 if not np.all(ampAssociations == ampAssociations.T):
314 raise RuntimeError(
"The `ampAssociations` is not symmetric about the diagonal.")
316 self.log.debug(
"amp associations:\n%s", ampAssociations)
317 self.log.debug(
"amp sides:\n%s", ampSides)
319 return ampAssociations, ampSides
357 """Calculate the amp offsets for all amplifiers.
361 im : `lsst.afw.image._image.ImageF`
362 Amplifier image to extract data from.
363 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
364 List of amplifier objects.
365 associations : numpy.ndarray
366 An N x N matrix containing amp association information, where N is
367 the number of amplifiers.
368 sides : numpy.ndarray
369 An N x N matrix containing amp side information, where N is the
370 number of amplifiers.
374 ampsOffsets : `numpy.ndarray`
375 1D float array containing the calculated amp offsets for all
378 ampsOffsets = np.zeros(len(amps))
380 interfaceOffsetLookup = {}
382 for ampId, ampAssociations
in enumerate(associations):
383 ampNeighbors = np.ravel(np.where(ampAssociations < 0))
384 for ampNeighbor
in ampNeighbors:
385 ampSide = sides[ampId][ampNeighbor]
389 edgeA = ampsEdges[ampId][ampSide]
390 edgeB = ampsEdges[ampNeighbor][(ampSide + 2) % 4]
391 if ampId < ampNeighbor:
393 interfaceOffsetLookup[f
"{ampId}{ampNeighbor}"] = interfaceOffset
395 interfaceOffset = -interfaceOffsetLookup[f
"{ampNeighbor}{ampId}"]
396 ampsOffsets[ampId] += interfaceWeight * interfaceOffset
400 """Calculate the amp edges for all amplifiers.
404 im : `lsst.afw.image._image.ImageF`
405 Amplifier image to extract data from.
406 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
407 List of amplifier objects.
408 ampSides : `numpy.ndarray`
409 An N x N matrix containing amp side information, where N is the
410 number of amplifiers.
414 ampEdges : `dict` [`int`, `dict` [`int`, `numpy.ndarray`]]
415 A dictionary containing amp edge(s) for each amplifier,
416 corresponding to one or more potential sides, where each edge is
417 associated with a side. The outer dictionary has integer keys
418 representing amplifier IDs, and the inner dictionary has integer
419 keys representing side IDs for each amplifier and values that are
420 1D arrays of floats representing the 1D medianified strips from the
421 amp image, referred to as "amp edge":
422 {ampID: {sideID: numpy.ndarray}, ...}
424 ampEdgeOuter = self.config.ampEdgeInset + self.config.ampEdgeWidth
427 0: (slice(-ampEdgeOuter, -self.config.ampEdgeInset), slice(
None)),
428 1: (slice(
None), slice(-ampEdgeOuter, -self.config.ampEdgeInset)),
429 2: (slice(self.config.ampEdgeInset, ampEdgeOuter), slice(
None)),
430 3: (slice(
None), slice(self.config.ampEdgeInset, ampEdgeOuter)),
432 for ampId, (amp, ampSides)
in enumerate(zip(amps, ampSides)):
434 ampIm = im[amp.getBBox()].array
436 for ampSide
in ampSides:
439 strip = ampIm[slice_map[ampSide]]
441 with warnings.catch_warnings():
442 warnings.filterwarnings(
"ignore",
r"All-NaN (slice|axis) encountered")
443 ampEdges[ampId][ampSide] = np.nanmedian(strip, axis=ampSide % 2)
447 """Calculate the amp offset for a given interface between two
453 ID of the first amplifier.
455 ID of the second amplifier.
456 edgeA : numpy.ndarray
457 Amp edge for the first amplifier.
458 edgeB : numpy.ndarray
459 Amp edge for the second amplifier.
463 interfaceOffset : float
464 The calculated amp offset value for the given interface between
467 interfaceId = f
"{ampIdA}{ampIdB}"
471 edgeDiff = edgeA - edgeB
472 window = int(self.config.ampEdgeWindowFrac * len(edgeDiff))
474 edgeDiffSum = np.convolve(np.nan_to_num(edgeDiff), np.ones(window),
"same")
475 edgeDiffNum = np.convolve(~np.isnan(edgeDiff), np.ones(window),
"same")
476 edgeDiffAvg = edgeDiffSum / np.clip(edgeDiffNum, 1,
None)
477 edgeDiffAvg[np.isnan(edgeDiff)] = np.nan
479 interfaceOffset = makeStatistics(edgeDiffAvg, MEANCLIP, sctrl).getValue()
483 ampEdgeGoodFrac = 1 - (np.sum(np.isnan(edgeDiffAvg)) / len(edgeDiffAvg))
484 minFracFail = ampEdgeGoodFrac < self.config.ampEdgeMinFrac
485 maxOffsetFail = np.abs(interfaceOffset) > self.config.ampEdgeMaxOffset
486 if minFracFail
or maxOffsetFail:
490 f
"The fraction of unmasked pixels for amp interface {interfaceId} is below the threshold "
491 f
"({ampEdgeGoodFrac:.2f} < {self.config.ampEdgeMinFrac}). Setting the interface offset "
492 f
"to {interfaceOffset}."
496 "The absolute offset value exceeds the limit "
497 f
"({np.abs(interfaceOffset):.2f} > {self.config.ampEdgeMaxOffset} ADU). Setting the "
498 f
"interface offset to {interfaceOffset}."
501 f
"amp interface {interfaceId} : "
502 f
"viable edge difference frac = {ampEdgeGoodFrac}, "
503 f
"interface offset = {interfaceOffset:.3f}"
505 return interfaceOffset