LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
ampOffset.py
Go to the documentation of this file.
1# This file is part of ip_isr.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22__all__ = ["AmpOffsetConfig", "AmpOffsetTask"]
23
24import warnings
25
26import numpy as np
27from lsst.afw.math import MEANCLIP, StatisticsControl, makeStatistics
28from lsst.afw.table import SourceTable
29from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask
30from lsst.pex.config import Config, ConfigurableField, Field
31from lsst.pipe.base import Struct, Task
32
33
35 """Configuration parameters for AmpOffsetTask."""
36
37 def setDefaults(self):
38 self.background.algorithm = "AKIMA_SPLINE"
39 self.background.useApprox = False
40 self.background.ignoredPixelMask = [
41 "BAD",
42 "SAT",
43 "INTRP",
44 "CR",
45 "EDGE",
46 "DETECTED",
47 "DETECTED_NEGATIVE",
48 "SUSPECT",
49 "NO_DATA",
50 ]
51 self.detection.reEstimateBackground = False
52
53 # This maintains existing behavior and test values after DM-39796.
54 self.detection.thresholdType = "stdev"
55
56 ampEdgeInset = Field(
57 doc="Number of pixels the amp edge strip is inset from the amp edge. A thin strip of pixels running "
58 "parallel to the edge of the amp is used to characterize the average flux level at the amp edge.",
59 dtype=int,
60 default=5,
61 )
62 ampEdgeWidth = Field(
63 doc="Pixel width of the amp edge strip, starting at ampEdgeInset and extending inwards.",
64 dtype=int,
65 default=64,
66 )
67 ampEdgeMinFrac = Field(
68 doc="Minimum allowed fraction of viable pixel rows along an amp edge. No amp offset estimate will be "
69 "generated for amp edges that do not have at least this fraction of unmasked pixel rows.",
70 dtype=float,
71 default=0.5,
72 )
73 ampEdgeMaxOffset = Field(
74 doc="Maximum allowed amp offset ADU value. If a measured amp offset value is larger than this, the "
75 "result will be discarded and therefore not used to determine amp pedestal corrections.",
76 dtype=float,
77 default=5.0,
78 )
79 doWindowSmoothing = Field(
80 doc="Smooth amp edge differences by taking a rolling average.",
81 dtype=bool,
82 default=True,
83 )
84 ampEdgeWindowFrac = Field(
85 doc="Fraction of the amp edge lengths utilized as the sliding window for generating rolling average "
86 "amp offset values. It should be reconfigured for every instrument (HSC, LSSTCam, etc.) and should "
87 "not exceed 1. If not provided, it defaults to the fraction that recovers the pixel size of the "
88 "sliding window used in obs_subaru for compatibility with existing HSC data. Only relevant if "
89 "`doWindowSmoothing` is set to True.",
90 dtype=float,
91 default=512 / 4176,
92 )
93 doBackground = Field(
94 doc="Estimate and subtract background prior to amp offset estimation?",
95 dtype=bool,
96 default=True,
97 )
98 background = ConfigurableField(
99 doc="An initial background estimation step run prior to amp offset calculation.",
100 target=SubtractBackgroundTask,
101 )
102 backgroundFractionSample = Field(
103 doc="The fraction of the shorter side of the amplifier used for background binning.",
104 dtype=float,
105 default=1.0,
106 )
107 doDetection = Field(
108 doc="Detect sources and update cloned exposure prior to amp offset estimation?",
109 dtype=bool,
110 default=True,
111 )
112 detection = ConfigurableField(
113 doc="Source detection to add temporary detection footprints prior to amp offset calculation.",
114 target=SourceDetectionTask,
115 )
116 applyWeights = Field(
117 doc="Weights the amp offset calculation by the length of the interface between amplifiers. Applying "
118 "weights does not affect outcomes for amplifiers in a 2D grid with square-shaped amplifiers or in "
119 "any 1D layout on a detector, regardless of whether the amplifiers are square.",
120 dtype=bool,
121 default=True,
122 )
123 doApplyAmpOffset = Field(
124 doc="Apply amp offset corrections to the input exposure?",
125 dtype=bool,
126 default=False,
127 )
128
129
130class AmpOffsetTask(Task):
131 """Calculate and apply amp offset corrections to an exposure."""
132
133 ConfigClass = AmpOffsetConfig
134 _DefaultName = "isrAmpOffset"
135
136 def __init__(self, *args, **kwargs):
137 super().__init__(*args, **kwargs)
138 # Always load background subtask, even if doBackground=False;
139 # this allows for default plane bit masks to be defined.
140 self.makeSubtask("background")
141 if self.config.doDetection:
142 self.makeSubtask("detection")
143 # Initialize all of the instance variables here.
145
146 def run(self, exposure):
147 """Calculate amp offset values, determine corrective pedestals for each
148 amp, and update the input exposure in-place.
149
150 Parameters
151 ----------
152 exposure: `lsst.afw.image.Exposure`
153 Exposure to be corrected for amp offsets.
154 """
155
156 # Generate an exposure clone to work on and establish the bit mask.
157 exp = exposure.clone()
158 bitMask = exp.mask.getPlaneBitMask(self.background.config.ignoredPixelMask)
159 amps = exp.getDetector().getAmplifiers()
160
161 # Check that all amps have the same gemotry.
162 ampDims = [amp.getBBox().getDimensions() for amp in amps]
163 if not all(dim == ampDims[0] for dim in ampDims):
164 raise RuntimeError("All amps should have the same geometry.")
165 else:
166 # The zeroth amp is representative of all amps in the detector.
167 self.ampDims = ampDims[0]
168 # Dictionary mapping side numbers to interface lengths.
169 # See `getAmpAssociations()` for details about sides.
170 self.interfaceLengthLookupBySide = {i: self.ampDims[i % 2] for i in range(4)}
171
172 # Determine amplifier geometry.
173 ampWidths = {amp.getBBox().getWidth() for amp in amps}
174 ampHeights = {amp.getBBox().getHeight() for amp in amps}
175 if len(ampWidths) > 1 or len(ampHeights) > 1:
176 raise NotImplementedError(
177 "Amp offset correction is not yet implemented for detectors with differing amp sizes."
178 )
179
180 # Assuming all the amps have the same geometry.
181 self.shortAmpSide = np.min(ampDims[0])
182
183 # Check that the edge width and inset are not too large.
184 if self.config.ampEdgeWidth >= self.shortAmpSide - 2 * self.config.ampEdgeInset:
185 raise RuntimeError(
186 f"The edge width ({self.config.ampEdgeWidth}) plus insets ({self.config.ampEdgeInset}) "
187 f"exceed the amp's short side ({self.shortAmpSide}). This setup leads to incorrect results."
188 )
189
190 # Fit and subtract background.
191 if self.config.doBackground:
192 maskedImage = exp.getMaskedImage()
193 # Assuming all the detectors are the same.
194 nX = exp.getWidth() // (self.shortAmpSide * self.config.backgroundFractionSample) + 1
195 nY = exp.getHeight() // (self.shortAmpSide * self.config.backgroundFractionSample) + 1
196 # This ensures that the `binSize` is as large as possible,
197 # preventing background subtraction from inadvertently removing the
198 # amp offset signature. Here it's set to the shorter dimension of
199 # the amplifier by default (`backgroundFractionSample` = 1), which
200 # seems reasonable.
201 bg = self.background.fitBackground(maskedImage, nx=int(nX), ny=int(nY))
202 bgImage = bg.getImageF(self.background.config.algorithm, self.background.config.undersampleStyle)
203 maskedImage -= bgImage
204
205 # Detect sources and update cloned exposure mask planes in-place.
206 if self.config.doDetection:
207 schema = SourceTable.makeMinimalSchema()
208 table = SourceTable.make(schema)
209 # Detection sigma, used for smoothing and to grow detections, is
210 # normally measured from the PSF of the exposure. As the PSF hasn't
211 # been measured at this stage of processing, sigma is instead
212 # set to an approximate value here (which should be sufficient).
213 _ = self.detection.run(table=table, exposure=exp, sigma=2)
214
215 # Safety check: do any pixels remain for amp offset estimation?
216 if (exp.mask.array & bitMask).all():
217 log_fn = self.log.warning if self.config.doApplyAmpOffset else self.log.info
218 log_fn(
219 "All pixels masked: cannot calculate any amp offset corrections. "
220 "All pedestals are being set to zero."
221 )
222 pedestals = np.zeros(len(amps))
223 else:
224 # Set up amp offset inputs.
225 im = exp.image
226 im.array[(exp.mask.array & bitMask) > 0] = np.nan
227
228 if self.config.ampEdgeWindowFrac > 1:
229 raise RuntimeError(
230 f"The specified fraction (`ampEdgeWindowFrac`={self.config.ampEdgeWindowFrac}) of the "
231 "edge length exceeds 1. This leads to complications downstream, after convolution in "
232 "the `getInterfaceOffset()` method. Please modify the `ampEdgeWindowFrac` value in the "
233 "config to be 1 or less and rerun."
234 )
235
236 # Obtain association and offset matrices.
237 A, sides = self.getAmpAssociations(amps)
238 B = self.getAmpOffsets(im, amps, A, sides)
239
240 # If least-squares minimization fails, convert NaNs to zeroes,
241 # ensuring that no values are erroneously added/subtracted.
242 pedestals = np.nan_to_num(np.linalg.lstsq(A, B, rcond=None)[0])
243
244 metadata = exposure.getMetadata() # Exposure metadata.
245 self.metadata["AMPOFFSET_PEDESTALS"] = {} # Task metadata.
246 ampNames = [amp.getName() for amp in amps]
247 for ampName, amp, pedestal in zip(ampNames, amps, pedestals):
248 # Add the amp pedestal to the exposure metadata.
249 metadata.set(
250 f"LSST ISR AMPOFFSET PEDESTAL {ampName}",
251 float(pedestal),
252 f"Pedestal level calculated for amp {ampName}",
253 )
254 if self.config.doApplyAmpOffset:
255 ampIm = exposure.image[amp.getBBox()].array
256 ampIm -= pedestal
257 # Add the amp pedestal to the "Task" metadata as well.
258 # Needed for Sasquatch/Chronograf!
259 self.metadata["AMPOFFSET_PEDESTALS"][ampName] = float(pedestal)
260 if self.config.doApplyAmpOffset:
261 status = "subtracted from exposure"
262 metadata.set("LSST ISR AMPOFFSET PEDESTAL SUBTRACTED", True, "Amp pedestals have been subtracted")
263 else:
264 status = "not subtracted from exposure"
265 metadata.set(
266 "LSST ISR AMPOFFSET PEDESTAL SUBTRACTED", False, "Amp pedestals have not been subtracted"
267 )
268 ampPedestalReport = ", ".join(
269 [f"{ampName}: {ampPedestal:.4f}" for (ampName, ampPedestal) in zip(ampNames, pedestals)]
270 )
271 self.log.info(f"Amp pedestal values ({status}): {ampPedestalReport}")
272
273 return Struct(pedestals=pedestals)
274
275 def getAmpAssociations(self, amps):
276 """Determine amp geometry and amp associations from a list of
277 amplifiers.
278
279 Parse an input list of amplifiers to determine the layout of amps
280 within a detector, and identify all amp sides (i.e., the
281 horizontal and vertical junctions between amps).
282
283 Returns a matrix with a shape corresponding to the geometry of the amps
284 in the detector.
285
286 Parameters
287 ----------
288 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
289 List of amplifier objects used to deduce associations.
290
291 Returns
292 -------
293 ampAssociations : `numpy.ndarray`
294 An N x N matrix (N = number of amplifiers) that illustrates the
295 connections between amplifiers within the detector layout. Each row
296 and column index corresponds to the ampIds of a specific pair of
297 amplifiers, and the matrix elements indicate their associations as
298 follows:
299
300 * 0: No association
301 * -1: Association exists (direction specified in the ampSides
302 matrix)
303 * n >= 1: Diagonal elements indicate the number of neighboring
304 amplifiers for the corresponding ampId==row==column number.
305
306 ampSides : `numpy.ndarray`
307 An N x N matrix (N = the number of amplifiers) representing the amp
308 side information corresponding to the `ampAssociations`
309 matrix. The elements are integers defined as below:
310
311 * -1: No side due to no association or the same amp (diagonals)
312 * 0: Side on the bottom
313 * 1: Side on the right
314 * 2: Side on the top
315 * 3: Side on the left
316 """
317 xCenters = [amp.getBBox().getCenterX() for amp in amps]
318 yCenters = [amp.getBBox().getCenterY() for amp in amps]
319 xIndices = np.ceil(xCenters / np.min(xCenters) / 2).astype(int) - 1
320 yIndices = np.ceil(yCenters / np.min(yCenters) / 2).astype(int) - 1
321
322 nAmps = len(amps)
323 ampIds = np.zeros((len(set(yIndices)), len(set(xIndices))), dtype=int)
324
325 for ampId, xIndex, yIndex in zip(np.arange(nAmps), xIndices, yIndices):
326 ampIds[yIndex, xIndex] = ampId
327
328 ampAssociations = np.zeros((nAmps, nAmps), dtype=int)
329 ampSides = np.full_like(ampAssociations, -1)
330
331 for ampId in ampIds.ravel():
332 neighbors, sides = self.getNeighbors(ampIds, ampId)
333 interfaceWeights = (
334 1
335 if not self.config.applyWeights
336 else np.array([self.interfaceLengthLookupBySide[side] for side in sides])
337 )
338 ampAssociations[ampId, neighbors] = -1 * interfaceWeights
339 ampSides[ampId, neighbors] = sides
340 ampAssociations[ampId, ampId] = -ampAssociations[ampId].sum()
341
342 if ampAssociations.sum() != 0:
343 raise RuntimeError("The `ampAssociations` array does not sum to zero.")
344
345 if not np.all(ampAssociations == ampAssociations.T):
346 raise RuntimeError("The `ampAssociations` is not symmetric about the diagonal.")
347
348 with np.printoptions(linewidth=200):
349 self.log.debug("amp associations:\n%s", ampAssociations)
350 self.log.debug("amp sides:\n%s", ampSides)
351
352 return ampAssociations, ampSides
353
354 def getNeighbors(self, ampIds, ampId):
355 """Get the neighbor amplifiers and their sides for a given
356 amplifier.
357
358 Parameters
359 ----------
360 ampIds : `numpy.ndarray`
361 Matrix with amp side association information.
362 ampId : `int`
363 The amplifier ID for which neighbor amplifiers and side IDs
364 are to be found.
365
366 Returns
367 -------
368 neighbors : `list` [`int`]
369 List of neighbor amplifier IDs.
370 sides : `list` [`int`]
371 List of side IDs, with each ID corresponding to its respective
372 neighbor amplifier.
373 """
374 m, n = ampIds.shape
375 r, c = np.ravel(np.where(ampIds == ampId))
376 neighbors, sides = [], []
377 sideLookup = {
378 0: (r + 1, c),
379 1: (r, c + 1),
380 2: (r - 1, c),
381 3: (r, c - 1),
382 }
383 for side, (row, column) in sideLookup.items():
384 if 0 <= row < m and 0 <= column < n:
385 neighbors.append(ampIds[row][column])
386 sides.append(side)
387 return neighbors, sides
388
389 def getAmpOffsets(self, im, amps, associations, sides):
390 """Calculate the amp offsets for all amplifiers.
391
392 Parameters
393 ----------
394 im : `lsst.afw.image._image.ImageF`
395 Amplifier image to extract data from.
396 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
397 List of amplifier objects.
398 associations : numpy.ndarray
399 An N x N matrix containing amp association information, where N is
400 the number of amplifiers.
401 sides : numpy.ndarray
402 An N x N matrix containing amp side information, where N is the
403 number of amplifiers.
404
405 Returns
406 -------
407 ampsOffsets : `numpy.ndarray`
408 1D float array containing the calculated amp offsets for all
409 amplifiers.
410 """
411 ampsOffsets = np.zeros(len(amps))
412 ampsEdges = self.getAmpEdges(im, amps, sides)
413 ampsNames = [amp.getName() for amp in amps]
414 interfaceOffsetLookup = {}
415
416 interfaceIds = []
417 interfaceOffsetOriginals = []
418 ampEdgeGoodFracs = []
419 minFracFails = []
420 maxOffsetFails = []
421 for ampId, ampAssociations in enumerate(associations):
422 ampNeighbors = np.ravel(np.where(ampAssociations < 0))
423 for ampNeighbor in ampNeighbors:
424 ampSide = sides[ampId][ampNeighbor]
425 interfaceWeight = (
426 1 if not self.config.applyWeights else self.interfaceLengthLookupBySide[ampSide]
427 )
428 edgeA = ampsEdges[ampId][ampSide]
429 edgeB = ampsEdges[ampNeighbor][(ampSide + 2) % 4]
430 if ampId < ampNeighbor:
431 (
432 interfaceId,
433 interfaceOffset,
434 interfaceOffsetOriginal,
435 ampEdgeGoodFrac,
436 minFracFail,
437 maxOffsetFail,
438 ) = self.getInterfaceOffset(ampsNames[ampId], ampsNames[ampNeighbor], edgeA, edgeB)
439 interfaceIds.append(interfaceId)
440 interfaceOffsetOriginals.append(interfaceOffsetOriginal)
441 ampEdgeGoodFracs.append(ampEdgeGoodFrac)
442 minFracFails.append(minFracFail)
443 maxOffsetFails.append(maxOffsetFail)
444 interfaceOffsetLookup[f"{ampId:02d}:{ampNeighbor:02d}"] = interfaceOffset
445 else:
446 interfaceOffset = -interfaceOffsetLookup[f"{ampNeighbor:02d}:{ampId:02d}"]
447 ampsOffsets[ampId] += interfaceWeight * interfaceOffset
448 if interfaceOffsetOriginals:
449 self.log.debug(
450 "Amp offset values for all interfaces: %s",
451 ", ".join(
452 [
453 f"{interfaceId}={interfaceOffset:0.2f}"
454 for interfaceId, interfaceOffset in zip(interfaceIds, interfaceOffsetOriginals)
455 ]
456 ),
457 )
458 quartile_summary = np.nanpercentile(interfaceOffsetOriginals, [0, 25, 50, 75, 100])
459 self.log.info(
460 "Amp offset quartile summary (min, Q1, Q2, Q3, max): %.4f, %.4f, %.4f, %.4f, %.4f",
461 *quartile_summary,
462 )
463 log_fn = self.log.warning if self.config.doApplyAmpOffset else self.log.info
464 if any(minFracFails):
465 log_fn(
466 "The fraction of unmasked edge pixels for the following amp interfaces is below the "
467 "configured threshold (%s): %s",
468 self.config.ampEdgeMinFrac,
469 ", ".join(
470 [
471 f"{interfaceId} ({ampEdgeGoodFrac:0.2f})"
472 for interfaceId, ampEdgeGoodFrac, minFracFail in zip(
473 interfaceIds, ampEdgeGoodFracs, minFracFails
474 )
475 if minFracFail
476 ]
477 ),
478 )
479 if any(maxOffsetFails):
480 log_fn(
481 "Absolute amp offsets exceed the configured maximum (%s) and have been set to zero for the "
482 "following amp interfaces: %s",
483 self.config.ampEdgeMaxOffset,
484 ", ".join(
485 [
486 f"{interfaceId}={np.abs(interfaceOffset):0.2f}"
487 for interfaceId, interfaceOffset, maxOffsetFail in zip(
488 interfaceIds, interfaceOffsetOriginals, maxOffsetFails
489 )
490 if maxOffsetFail
491 ]
492 ),
493 )
494 return ampsOffsets
495
496 def getAmpEdges(self, im, amps, ampSides):
497 """Calculate the amp edges for all amplifiers.
498
499 Parameters
500 ----------
501 im : `lsst.afw.image._image.ImageF`
502 Amplifier image to extract data from.
503 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
504 List of amplifier objects.
505 ampSides : `numpy.ndarray`
506 An N x N matrix containing amp side information, where N is the
507 number of amplifiers.
508
509 Returns
510 -------
511 ampEdges : `dict` [`int`, `dict` [`int`, `numpy.ndarray`]]
512 A dictionary containing amp edge(s) for each amplifier,
513 corresponding to one or more potential sides, where each edge is
514 associated with a side. The outer dictionary has integer keys
515 representing amplifier IDs, and the inner dictionary has integer
516 keys representing side IDs for each amplifier and values that are
517 1D arrays of floats representing the 1D medianified strips from the
518 amp image, referred to as "amp edge":
519 {ampID: {sideID: numpy.ndarray}, ...}
520 """
521 ampEdgeOuter = self.config.ampEdgeInset + self.config.ampEdgeWidth
522 ampEdges = {}
523 slice_map = {
524 0: (slice(-ampEdgeOuter, -self.config.ampEdgeInset), slice(None)),
525 1: (slice(None), slice(-ampEdgeOuter, -self.config.ampEdgeInset)),
526 2: (slice(self.config.ampEdgeInset, ampEdgeOuter), slice(None)),
527 3: (slice(None), slice(self.config.ampEdgeInset, ampEdgeOuter)),
528 }
529 for ampId, (amp, ampSides) in enumerate(zip(amps, ampSides)):
530 ampEdges[ampId] = {}
531 ampIm = im[amp.getBBox()].array
532 # Loop over identified sides.
533 for ampSide in ampSides:
534 if ampSide < 0:
535 continue
536 strip = ampIm[slice_map[ampSide]]
537 # Catch warnings to prevent all-NaN slice RuntimeWarning.
538 with warnings.catch_warnings():
539 warnings.filterwarnings("ignore", r"All-NaN (slice|axis) encountered")
540 ampEdges[ampId][ampSide] = np.nanmedian(strip, axis=ampSide % 2) # 1D medianified strip
541 return ampEdges
542
543 def getInterfaceOffset(self, ampNameA, ampNameB, edgeA, edgeB):
544 """Calculate the amp offset for a given interface between two
545 amplifiers.
546
547 Parameters
548 ----------
549 ampNameA : str
550 Name of the first amplifier.
551 ampNameB : str
552 Name of the second amplifier.
553 edgeA : numpy.ndarray
554 Amp edge for the first amplifier.
555 edgeB : numpy.ndarray
556 Amp edge for the second amplifier.
557
558 Returns
559 -------
560 interfaceOffset : float
561 The calculated amp offset value for the given interface between
562 amps A and B.
563 interfaceOffsetOriginal : float
564 The original calculated amp offset value for the given interface
565 between amps A and B.
566 ampEdgeGoodFrac : float
567 Fraction of viable pixel rows along the amp edge.
568 minFracFail : bool
569 True if the fraction of unmasked pixel rows is below the
570 ampEdgeMinFrac threshold.
571 maxOffsetFail : bool
572 True if the absolute offset value exceeds the ampEdgeMaxOffset
573 threshold.
574 """
575 interfaceId = f"{ampNameA}:{ampNameB}"
576 sctrl = StatisticsControl()
577 # NOTE: Taking the difference with the order below fixes the sign flip
578 # in the B matrix.
579 edgeDiff = edgeA - edgeB
580 if self.config.doWindowSmoothing:
581 # Compute rolling averages.
582 window = int(self.config.ampEdgeWindowFrac * len(edgeDiff))
583 edgeDiffSum = np.convolve(np.nan_to_num(edgeDiff), np.ones(window), "same")
584 edgeDiffNum = np.convolve(~np.isnan(edgeDiff), np.ones(window), "same")
585 edgeDiffAvg = edgeDiffSum / np.clip(edgeDiffNum, 1, None)
586 else:
587 # Directly use the difference.
588 edgeDiffAvg = edgeDiff.copy()
589 edgeDiffAvg[np.isnan(edgeDiff)] = np.nan
590 # Take clipped mean of rolling average data as amp offset value.
591 interfaceOffset = makeStatistics(edgeDiffAvg, MEANCLIP, sctrl).getValue()
592 interfaceOffsetOriginal = interfaceOffset
593 ampEdgeGoodFrac = 1 - (np.sum(np.isnan(edgeDiffAvg)) / len(edgeDiffAvg))
594
595 # Perform a couple of do-no-harm safety checks:
596 # a) The fraction of unmasked pixel rows is > ampEdgeMinFrac,
597 # b) The absolute offset ADU value is < ampEdgeMaxOffset.
598 minFracFail = ampEdgeGoodFrac < self.config.ampEdgeMinFrac
599 maxOffsetFail = np.abs(interfaceOffset) > self.config.ampEdgeMaxOffset
600 if minFracFail or maxOffsetFail:
601 interfaceOffset = 0
602 self.log.debug(
603 f"amp interface {interfaceId} : "
604 f"viable edge difference frac = {ampEdgeGoodFrac:.2f}, "
605 f"amp offset = {interfaceOffsetOriginal:.3f}"
606 )
607 return (
608 interfaceId,
609 interfaceOffset,
610 interfaceOffsetOriginal,
611 ampEdgeGoodFrac,
612 minFracFail,
613 maxOffsetFail,
614 )
Pass parameters to a Statistics object.
Definition Statistics.h:83
getAmpOffsets(self, im, amps, associations, sides)
Definition ampOffset.py:389
getInterfaceOffset(self, ampNameA, ampNameB, edgeA, edgeB)
Definition ampOffset.py:543
__init__(self, *args, **kwargs)
Definition ampOffset.py:136
getAmpEdges(self, im, amps, ampSides)
Definition ampOffset.py:496
getNeighbors(self, ampIds, ampId)
Definition ampOffset.py:354