158 """Calculate amp offset values, determine corrective pedestals for each
159 amp, and update the input exposure in-place.
163 exposure: `lsst.afw.image.Exposure`
164 Exposure to be corrected for amp offsets.
168 exp = exposure.clone()
169 bitMask = exp.mask.getPlaneBitMask(self.background.config.ignoredPixelMask)
170 amps = exp.getDetector().getAmplifiers()
173 ampDims = [amp.getBBox().getDimensions()
for amp
in amps]
174 if not all(dim == ampDims[0]
for dim
in ampDims):
175 raise RuntimeError(
"All amps should have the same geometry.")
184 ampWidths = {amp.getBBox().getWidth()
for amp
in amps}
185 ampHeights = {amp.getBBox().getHeight()
for amp
in amps}
186 if len(ampWidths) > 1
or len(ampHeights) > 1:
187 raise NotImplementedError(
188 "Amp offset correction is not yet implemented for detectors with differing amp sizes."
195 if self.config.ampEdgeWidth >= self.
shortAmpSide - 2 * self.config.ampEdgeInset:
197 f
"The edge width ({self.config.ampEdgeWidth}) plus insets ({self.config.ampEdgeInset}) "
198 f
"exceed the amp's short side ({self.shortAmpSide}). This setup leads to incorrect results."
202 if self.config.doBackground:
203 maskedImage = exp.getMaskedImage()
205 nX = exp.getWidth() // (self.
shortAmpSide * self.config.backgroundFractionSample) + 1
206 nY = exp.getHeight() // (self.
shortAmpSide * self.config.backgroundFractionSample) + 1
213 bg = self.background.fitBackground(maskedImage, nx=int(nX), ny=int(nY))
214 except TooManyMaskedPixelsError:
216 "Background estimation failed due to too many masked pixels; "
217 "proceeding without background subtraction."
220 bgImage = bg.getImageF(
221 self.background.config.algorithm,
222 self.background.config.undersampleStyle,
224 maskedImage -= bgImage
227 if self.config.doDetection:
228 schema = SourceTable.makeMinimalSchema()
229 table = SourceTable.make(schema)
234 _ = self.detection.
run(table=table, exposure=exp, sigma=2)
238 im.array[(exp.mask.array & bitMask) > 0] = np.nan
242 ampOffsets, interfaceOffsetDict = self.
getAmpOffsets(im, amps, associations, sides)
245 if (exp.mask.array & bitMask).all():
246 log_fn = self.log.warning
if self.config.doApplyAmpOffset
else self.log.info
248 "All pixels masked: cannot calculate any amp offset corrections. "
249 "All pedestals are being set to zero."
251 pedestals = np.zeros(len(amps))
255 pedestals = np.nan_to_num(np.linalg.lstsq(associations, ampOffsets, rcond=
None)[0])
257 metadata = exposure.getMetadata()
258 self.metadata[
"AMPOFFSET_PEDESTALS"] = {}
259 ampNames = [amp.getName()
for amp
in amps]
262 for interfaceId, interfaceOffset
in interfaceOffsetDict.items():
264 f
"LSST ISR AMPOFFSET INTERFACEOFFSET {interfaceId}",
265 float(interfaceOffset),
266 f
"Raw amp interface offset calculated for {interfaceId}",
269 for ampName, amp, pedestal
in zip(ampNames, amps, pedestals):
272 f
"LSST ISR AMPOFFSET PEDESTAL {ampName}",
274 f
"Pedestal level calculated for amp {ampName}",
276 if self.config.doApplyAmpOffset
and pedestal != 0.0:
277 ampIm = exposure.image[amp.getBBox()].array
281 self.metadata[
"AMPOFFSET_PEDESTALS"][ampName] = float(pedestal)
282 if self.config.doApplyAmpOffset:
283 status =
"subtracted from exposure"
284 metadata.set(
"LSST ISR AMPOFFSET PEDESTAL SUBTRACTED",
True,
"Amp pedestals have been subtracted")
286 status =
"not subtracted from exposure"
288 "LSST ISR AMPOFFSET PEDESTAL SUBTRACTED",
False,
"Amp pedestals have not been subtracted"
290 ampPedestalReport =
", ".join(
291 [f
"{ampName}: {ampPedestal:.4f}" for (ampName, ampPedestal)
in zip(ampNames, pedestals)]
293 self.log.info(f
"Amp pedestal values ({status}): {ampPedestalReport}")
295 return Struct(pedestals=pedestals)
298 """Determine amp geometry and amp associations from a list of
301 Parse an input list of amplifiers to determine the layout of amps
302 within a detector, and identify all amp sides (i.e., the
303 horizontal and vertical junctions between amps).
305 Returns a matrix with a shape corresponding to the geometry of the amps
310 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
311 List of amplifier objects used to deduce associations.
315 ampAssociations : `numpy.ndarray`
316 An N x N matrix (N = number of amplifiers) that illustrates the
317 connections between amplifiers within the detector layout. Each row
318 and column index corresponds to the ampIds of a specific pair of
319 amplifiers, and the matrix elements indicate their associations as
323 * -1: Association exists (direction specified in the ampSides
325 * n >= 1: Diagonal elements indicate the number of neighboring
326 amplifiers for the corresponding ampId==row==column number.
328 ampSides : `numpy.ndarray`
329 An N x N matrix (N = the number of amplifiers) representing the amp
330 side information corresponding to the `ampAssociations`
331 matrix. The elements are integers defined as below:
333 * -1: No side due to no association or the same amp (diagonals)
334 * 0: Side on the bottom
335 * 1: Side on the right
337 * 3: Side on the left
339 xCenters = [amp.getBBox().getCenterX()
for amp
in amps]
340 yCenters = [amp.getBBox().getCenterY()
for amp
in amps]
341 xIndices = np.ceil(xCenters / np.min(xCenters) / 2).astype(int) - 1
342 yIndices = np.ceil(yCenters / np.min(yCenters) / 2).astype(int) - 1
345 ampIds = np.zeros((len(set(yIndices)), len(set(xIndices))), dtype=int)
347 for ampId, xIndex, yIndex
in zip(np.arange(nAmps), xIndices, yIndices):
348 ampIds[yIndex, xIndex] = ampId
350 ampAssociations = np.zeros((nAmps, nAmps), dtype=int)
351 ampSides = np.full_like(ampAssociations, -1)
353 for ampId
in ampIds.ravel():
357 if not self.config.applyWeights
360 ampAssociations[ampId, neighbors] = -1 * interfaceWeights
361 ampSides[ampId, neighbors] = sides
362 ampAssociations[ampId, ampId] = -ampAssociations[ampId].sum()
364 if ampAssociations.sum() != 0:
365 raise RuntimeError(
"The `ampAssociations` array does not sum to zero.")
367 if not np.all(ampAssociations == ampAssociations.T):
368 raise RuntimeError(
"The `ampAssociations` is not symmetric about the diagonal.")
370 with np.printoptions(linewidth=200):
371 self.log.debug(
"amp associations:\n%s", ampAssociations)
372 self.log.debug(
"amp sides:\n%s", ampSides)
374 return ampAssociations, ampSides
412 """Calculate the amp offsets for all amplifiers.
416 im : `lsst.afw.image._image.ImageF`
417 Amplifier image to extract data from.
418 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
419 List of amplifier objects.
420 associations : numpy.ndarray
421 An N x N matrix containing amp association information, where N is
422 the number of amplifiers.
423 sides : numpy.ndarray
424 An N x N matrix containing amp side information, where N is the
425 number of amplifiers.
429 ampOffsets : `numpy.ndarray`
430 1D float array containing the calculated amp offsets for all
431 amplifiers (optionally weighted by interface length).
432 interfaceOffsetDict : `dict` [`str`, `float`]
433 Dictionary mapping interface IDs to their corresponding raw
434 (uncapped) offset values.
436 ampOffsets = np.zeros(len(amps))
438 ampsNames = [amp.getName()
for amp
in amps]
439 interfaceOffsetLookup = {}
442 interfaceOffsetOriginals = []
443 ampEdgeGoodFracs = []
446 for ampId, ampAssociations
in enumerate(associations):
447 ampNeighbors = np.ravel(np.where(ampAssociations < 0))
448 for ampNeighbor
in ampNeighbors:
449 ampSide = sides[ampId][ampNeighbor]
453 edgeA = ampsEdges[ampId][ampSide]
454 edgeB = ampsEdges[ampNeighbor][(ampSide + 2) % 4]
455 if ampId < ampNeighbor:
459 interfaceOffsetOriginal,
464 interfaceIds.append(interfaceId)
465 interfaceOffsetOriginals.append(interfaceOffsetOriginal)
466 ampEdgeGoodFracs.append(ampEdgeGoodFrac)
467 minFracFails.append(minFracFail)
468 maxOffsetFails.append(maxOffsetFail)
469 interfaceOffsetLookup[f
"{ampId:02d}:{ampNeighbor:02d}"] = interfaceOffset
471 interfaceOffset = -interfaceOffsetLookup[f
"{ampNeighbor:02d}:{ampId:02d}"]
472 ampOffsets[ampId] += interfaceWeight * interfaceOffset
473 if interfaceOffsetOriginals:
475 "Raw (uncapped) amp offset values for all interfaces: %s",
478 f
"{interfaceId}={interfaceOffset:0.2f}"
479 for interfaceId, interfaceOffset
in zip(interfaceIds, interfaceOffsetOriginals)
483 quartile_summary = np.nanpercentile(interfaceOffsetOriginals, [0, 25, 50, 75, 100])
485 "Raw amp offset quartile summary for all interfaces (min, Q1, Q2, Q3, max): "
486 "%.4f, %.4f, %.4f, %.4f, %.4f",
489 log_fn = self.log.warning
if self.config.doApplyAmpOffset
else self.log.info
490 if any(minFracFails):
492 "The fraction of unmasked edge pixels for the following amp interfaces is below the "
493 "configured threshold (%s): %s",
494 self.config.ampEdgeMinFrac,
497 f
"{interfaceId} ({ampEdgeGoodFrac:0.2f})"
498 for interfaceId, ampEdgeGoodFrac, minFracFail
in zip(
499 interfaceIds, ampEdgeGoodFracs, minFracFails
505 if any(maxOffsetFails):
507 "Absolute amp offsets exceed the configured maximum (%s) and have been set to zero for the "
508 "following amp interfaces: %s",
509 self.config.ampEdgeMaxOffset,
512 f
"{interfaceId}={np.abs(interfaceOffset):0.2f}"
513 for interfaceId, interfaceOffset, maxOffsetFail
in zip(
514 interfaceIds, interfaceOffsetOriginals, maxOffsetFails
522 interfaceOffsetDict = dict(zip(interfaceIds, interfaceOffsetOriginals))
524 return ampOffsets, interfaceOffsetDict
527 """Calculate the amp edges for all amplifiers.
531 im : `lsst.afw.image._image.ImageF`
532 Amplifier image to extract data from.
533 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
534 List of amplifier objects.
535 ampSides : `numpy.ndarray`
536 An N x N matrix containing amp side information, where N is the
537 number of amplifiers.
541 ampEdges : `dict` [`int`, `dict` [`int`, `numpy.ndarray`]]
542 A dictionary containing amp edge(s) for each amplifier,
543 corresponding to one or more potential sides, where each edge is
544 associated with a side. The outer dictionary has integer keys
545 representing amplifier IDs, and the inner dictionary has integer
546 keys representing side IDs for each amplifier and values that are
547 1D arrays of floats representing the median values of strips from
548 the amp image, referred to as an "amp edge":
549 {ampID: {sideID: numpy.ndarray}, ...}
551 ampEdgeOuter = self.config.ampEdgeInset + self.config.ampEdgeWidth
554 0: (slice(-ampEdgeOuter, -self.config.ampEdgeInset), slice(
None)),
555 1: (slice(
None), slice(-ampEdgeOuter, -self.config.ampEdgeInset)),
556 2: (slice(self.config.ampEdgeInset, ampEdgeOuter), slice(
None)),
557 3: (slice(
None), slice(self.config.ampEdgeInset, ampEdgeOuter)),
559 for ampId, (amp, ampSides)
in enumerate(zip(amps, ampSides)):
561 ampIm = im[amp.getBBox()].array
563 for ampSide
in ampSides:
566 strip = ampIm[slice_map[ampSide]]
568 with warnings.catch_warnings():
569 warnings.filterwarnings(
"ignore",
r"All-NaN (slice|axis) encountered")
570 ampEdges[ampId][ampSide] = np.nanmedian(strip, axis=ampSide % 2)
574 """Calculate the amp offset for a given interface between two
580 Name of the first amplifier.
582 Name of the second amplifier.
583 edgeA : numpy.ndarray
584 Amp edge for the first amplifier.
585 edgeB : numpy.ndarray
586 Amp edge for the second amplifier.
591 Identifier for the interface between amps A and B, formatted as
592 "A-B", where A and B are the respective amp names.
593 interfaceOffset : float
594 The calculated amp offset value for the given interface between
596 interfaceOffsetOriginal : float
597 The original calculated amp offset value for the given interface
598 between amps A and B.
599 ampEdgeGoodFrac : float
600 Fraction of viable pixel rows along the amp edge.
602 True if the fraction of unmasked pixel rows is below the
603 ampEdgeMinFrac threshold.
605 True if the absolute offset value exceeds the ampEdgeMaxOffset
608 interfaceId = f
"{ampNameA}-{ampNameB}"
609 if np.all(np.isnan(edgeA))
or np.all(np.isnan(edgeB)):
610 self.log.warning(f
"Amp interface {interfaceId} is fully masked; setting amp offset to zero.")
611 return interfaceId, 0.0, 0.0, 0.0,
True,
False
616 edgeDiff = edgeA - edgeB
617 if self.config.doWindowSmoothing:
619 window = int(self.config.ampEdgeWindowFrac * len(edgeDiff))
620 edgeDiffSum = np.convolve(np.nan_to_num(edgeDiff), np.ones(window),
"same")
621 edgeDiffNum = np.convolve(~np.isnan(edgeDiff), np.ones(window),
"same")
622 edgeDiffAvg = edgeDiffSum / np.clip(edgeDiffNum, 1,
None)
625 edgeDiffAvg = edgeDiff.copy()
626 edgeDiffAvg[np.isnan(edgeDiff)] = np.nan
628 interfaceOffset = makeStatistics(edgeDiffAvg, MEANCLIP, sctrl).getValue()
629 interfaceOffsetOriginal = interfaceOffset
630 ampEdgeGoodFrac = 1 - (np.sum(np.isnan(edgeDiffAvg)) / len(edgeDiffAvg))
635 minFracFail = ampEdgeGoodFrac < self.config.ampEdgeMinFrac
636 maxOffsetFail = np.abs(interfaceOffset) > self.config.ampEdgeMaxOffset
637 if minFracFail
or maxOffsetFail:
640 f
"amp interface '{interfaceId}': "
641 f
"viable edge difference frac = {ampEdgeGoodFrac:.2f}, "
642 f
"amp offset = {interfaceOffsetOriginal:.3f}"
647 interfaceOffsetOriginal,