22__all__ = (
'DeferredChargeConfig',
'DeferredChargeTask',
'SerialTrap',
'DeferredChargeCalib')
25from astropy.table
import Table
30from .isrFunctions
import gainContext
31from .calibType
import IsrCalib
33import scipy.interpolate
as interp
37 """Represents a serial register trap.
42 Size of the charge trap, in electrons.
43 emission_time : `float`
44 Trap emission time constant,
in inverse transfers.
46 Serial pixel location of the trap, including the prescan.
48 Type of trap capture to use. Should be one of ``linear``,
49 ``logistic``,
or ``spline``.
50 coeffs : `list` [`float`]
51 Coefficients
for the capture process. Linear traps need one
52 coefficient, logistic traps need two,
and spline based traps
53 need to have an even number of coefficients that can be split
54 into their spline locations
and values.
59 Raised
if the specified parameters are out of expected range.
62 def __init__(self, size, emission_time, pixel, trap_type, coeffs):
64 raise ValueError(
'Trap size must be greater than or equal to 0.')
67 if emission_time <= 0.0:
68 raise ValueError(
'Emission time must be greater than 0.')
69 if np.isnan(emission_time):
70 raise ValueError(
'Emission time must be real-valued, not NaN')
73 if int(pixel) != pixel:
74 raise ValueError(
'Fraction value for pixel not allowed.')
80 if self.
trap_type not in (
'linear',
'logistic',
'spline'):
81 raise ValueError(
'Unknown trap type: %s', self.
trap_type)
84 centers, values = np.split(np.array(self.
coeffs, dtype=np.float), 2)
86 values = values[~np.isnan(centers)]
87 centers = centers[~np.isnan(centers)]
88 centers = centers[~np.isnan(values)]
89 values = values[~np.isnan(values)]
90 self.
interp = interp.interp1d(centers, values)
99 if self.
size != other.size:
103 if self.
pixel != other.pixel:
107 if self.
coeffs != other.coeffs:
120 """Initialize trapping arrays for simulated readout.
125 Number of rows to simulate.
127 Number of columns to simulate.
128 prescan_width : `int`
129 Additional transfers due to prescan.
134 Raised if the trap falls outside of the image.
136 if self.
pixel > nx+prescan_width:
137 raise ValueError(
'Trap location {0} must be less than {1}'.format(self.
pixel,
140 self.
_trap_array = np.zeros((ny, nx+prescan_width))
145 """Release charge through exponential decay.
149 released_charge : `float`
155 return released_charge
158 """Perform charge capture using a logistic function.
162 free_charge : `float`
163 Charge available to be trapped.
167 captured_charge : `float`
168 Amount of charge actually trapped.
174 return captured_charge
177 """Trap capture function.
181 pixel_signals : `list` [`float`]
186 captured_charge : `list` [`float`]
187 Amount of charge captured from each pixel.
192 Raised
if the trap type
is invalid.
196 return np.minimum(self.
size, pixel_signals*scaling)
199 return self.
size/(1.+np.exp(-k*(pixel_signals-f0)))
201 return self.
interp(pixel_signals)
203 raise RuntimeError(f
"Invalid trap capture type: {self.trap_type}.")
207 r"""Calibration containing deferred charge/CTI parameters.
212 Additional parameters to pass to parent constructor.
216 The charge transfer inefficiency attributes stored are:
218 driftScale : `dict` [`str`, `float`]
219 A dictionary, keyed by amplifier name, of the local electronic
220 offset drift scale parameter, A_L
in Snyder+2021.
221 decayTime : `dict` [`str`, `float`]
222 A dictionary, keyed by amplifier name, of the local electronic
223 offset decay time, \tau_L
in Snyder+2021.
224 globalCti : `dict` [`str`, `float`]
225 A dictionary, keyed by amplifier name, of the mean
global CTI
226 paramter, b
in Snyder+2021.
228 A dictionary, keyed by amplifier name, containing a single
229 serial trap
for each amplifier.
232 _SCHEMA =
'Deferred Charge'
245 """Read metadata parameters from a detector.
249 detector : `lsst.afw.cameraGeom.detector`
250 Input detector with parameters to use.
255 The calibration constructed
from the detector.
262 """Construct a calibration from a dictionary of properties.
267 Dictionary of properties.
271 calib : `lsst.ip.isr.CalibType`
272 Constructed calibration.
277 Raised if the supplied dictionary
is for a different
282 if calib._OBSTYPE != dictionary[
'metadata'][
'OBSTYPE']:
283 raise RuntimeError(f
"Incorrect CTI supplied. Expected {calib._OBSTYPE}, "
284 f
"found {dictionary['metadata']['OBSTYPE']}")
286 calib.setMetadata(dictionary[
'metadata'])
288 calib.driftScale = dictionary[
'driftScale']
289 calib.decayTime = dictionary[
'decayTime']
290 calib.globalCti = dictionary[
'globalCti']
292 for ampName
in dictionary[
'serialTraps']:
293 ampTraps = dictionary[
'serialTraps'][ampName]
294 calib.serialTraps[ampName] =
SerialTrap(ampTraps[
'size'], ampTraps[
'emissionTime'],
295 ampTraps[
'pixel'], ampTraps[
'trap_type'],
297 calib.updateMetadata()
301 """Return a dictionary containing the calibration properties.
302 The dictionary should be able to be round-tripped through
308 Dictionary of properties.
318 outDict[
'serialTraps'] = {}
321 'emissionTime': self.
serialTraps[ampName].emission_time,
325 outDict[
'serialTraps'][ampName] = ampTrap
331 """Construct calibration from a list of tables.
333 This method uses the ``fromDict`` method to create the
334 calibration, after constructing an appropriate dictionary from
339 tableList : `list` [`lsst.afw.table.Table`]
340 List of tables to use to construct the crosstalk
341 calibration. Two tables are expected
in this list, the
342 first containing the per-amplifier CTI parameters,
and the
343 second containing the parameters
for serial traps.
348 The calibration defined
in the tables.
353 Raised
if the trap type
or trap coefficients are
not
356 ampTable = tableList[0]
359 inDict['metadata'] = ampTable.meta
361 amps = ampTable[
'AMPLIFIER']
362 driftScale = ampTable[
'DRIFT_SCALE']
363 decayTime = ampTable[
'DECAY_TIME']
364 globalCti = ampTable[
'GLOBAL_CTI']
366 inDict[
'driftScale'] = {amp: value
for amp, value
in zip(amps, driftScale)}
367 inDict[
'decayTime'] = {amp: value
for amp, value
in zip(amps, decayTime)}
368 inDict[
'globalCti'] = {amp: value
for amp, value
in zip(amps, globalCti)}
370 inDict[
'serialTraps'] = {}
371 trapTable = tableList[1]
373 amps = trapTable[
'AMPLIFIER']
374 sizes = trapTable[
'SIZE']
375 emissionTimes = trapTable[
'EMISSION_TIME']
376 pixels = trapTable[
'PIXEL']
377 trap_type = trapTable[
'TYPE']
378 coeffs = trapTable[
'COEFFS']
380 for index, amp
in enumerate(amps):
382 ampTrap[
'size'] = sizes[index]
383 ampTrap[
'emissionTime'] = emissionTimes[index]
384 ampTrap[
'pixel'] = pixels[index]
385 ampTrap[
'trap_type'] = trap_type[index]
389 inCoeffs = coeffs[index]
391 nanValues = np.where(np.isnan(inCoeffs))[0]
392 if nanValues
is not None:
393 coeffLength = len(inCoeffs)
394 while breakIndex < coeffLength:
395 if coeffLength - breakIndex
in nanValues:
401 outCoeffs = inCoeffs[0: coeffLength - breakIndex]
404 ampTrap[
'coeffs'] = outCoeffs.tolist()
406 if ampTrap[
'trap_type'] ==
'linear':
407 if len(ampTrap[
'coeffs']) < 1:
408 raise ValueError(
"CTI Amplifier %s coefficients for trap has illegal length %d.",
409 amp, len(ampTrap[
'coeffs']))
410 elif ampTrap[
'trap_type'] ==
'logistic':
411 if len(ampTrap[
'coeffs']) < 2:
412 raise ValueError(
"CTI Amplifier %s coefficients for trap has illegal length %d.",
413 amp, len(ampTrap[
'coeffs']))
414 elif ampTrap[
'trap_type'] ==
'spline':
415 if len(ampTrap[
'coeffs']) % 2 != 0:
416 raise ValueError(
"CTI Amplifier %s coefficients for trap has illegal length %d.",
417 amp, len(ampTrap[
'coeffs']))
419 raise ValueError(
'Unknown trap type: %s', ampTrap[
'trap_type'])
421 inDict[
'serialTraps'][amp] = ampTrap
426 """Construct a list of tables containing the information in this
429 The list of tables should create an identical calibration
430 after being passed to this class's fromTable method.
434 tableList : `list` [`lsst.afw.table.Table`]
435 List of tables containing the crosstalk calibration
436 information. Two tables are generated for this list, the
437 first containing the per-amplifier CTI parameters,
and the
438 second containing the parameters
for serial traps.
454 ampTable = Table({
'AMPLIFIER': ampList,
455 'DRIFT_SCALE': driftScale,
456 'DECAY_TIME': decayTime,
457 'GLOBAL_CTI': globalCti,
461 tableList.append(ampTable)
473 maxCoeffLength = np.maximum(maxCoeffLength, len(trap.coeffs))
478 sizeList.append(trap.size)
479 timeList.append(trap.emission_time)
480 pixelList.append(trap.pixel)
481 typeList.append(trap.trap_type)
484 if len(coeffs) != maxCoeffLength:
485 coeffs = np.pad(coeffs, (0, maxCoeffLength - len(coeffs)),
486 constant_values=np.nan).tolist()
487 coeffList.append(coeffs)
489 trapTable = Table({
'AMPLIFIER': ampList,
491 'EMISSION_TIME': timeList,
494 'COEFFS': coeffList})
496 tableList.append(trapTable)
502 """Settings for deferred charge correction.
506 doc="Number of prior pixels to use for local offset correction.",
511 doc=
"Number of prior pixels to use for trap correction.",
516 doc=
"If true, scale by the gain.",
521 doc=
"If true, set serial prescan and parallel overscan to zero before correction.",
527 """Task to correct an exposure for charge transfer inefficiency.
529 This uses the methods described by Snyder et al. 2021, Journal of
530 Astronimcal Telescopes, Instruments, and Systems, 7,
531 048002. doi:10.1117/1.JATIS.7.4.048002 (Snyder+21).
533 ConfigClass = DeferredChargeConfig
534 _DefaultName = 'isrDeferredCharge'
536 def run(self, exposure, ctiCalib, gains=None):
537 """Correct deferred charge/CTI issues.
542 Exposure to correct the deferred charge on.
544 Calibration object containing the charge transfer
546 gains : `dict` [`str`, `float`]
547 A dictionary, keyed by amplifier name, of the gains to
548 use. If gains is None, the nominal gains
in the amplifier
554 The corrected exposure.
556 image = exposure.getMaskedImage().image
557 detector = exposure.getDetector()
563 if self.config.useGains:
565 gains = {amp.getName(): amp.getGain()
for amp
in detector.getAmplifiers()}
567 with gainContext(exposure, image, self.config.useGains, gains):
568 for amp
in detector.getAmplifiers():
569 ampName = amp.getName()
571 ampImage = image[amp.getRawBBox()]
572 if self.config.zeroUnusedPixels:
575 ampImage[amp.getRawParallelOverscanBBox()].array[:, :] = 0.0
576 ampImage[amp.getRawSerialPrescanBBox()].array[:, :] = 0.0
581 ampData = self.
flipData(ampImage.array, amp)
583 if ctiCalib.driftScale[ampName] > 0.0:
585 ctiCalib.driftScale[ampName],
586 ctiCalib.decayTime[ampName],
587 self.config.nPixelOffsetCorrection)
589 correctedAmpData = ampData.copy()
592 ctiCalib.serialTraps[ampName],
593 ctiCalib.globalCti[ampName],
594 self.config.nPixelTrapCorrection)
597 correctedAmpData = self.
flipData(correctedAmpData, amp)
598 image[amp.getRawBBox()].array[:, :] = correctedAmpData[:, :]
604 """Flip data array such that readout corner is at lower-left.
608 ampData : `numpy.ndarray`, (nx, ny)
611 Amplifier to get readout corner information.
615 ampData : `numpy.ndarray`, (nx, ny)
618 X_FLIP = {ReadoutCorner.LL: False,
619 ReadoutCorner.LR:
True,
620 ReadoutCorner.UL:
False,
621 ReadoutCorner.UR:
True}
622 Y_FLIP = {ReadoutCorner.LL:
False,
623 ReadoutCorner.LR:
False,
624 ReadoutCorner.UL:
True,
625 ReadoutCorner.UR:
True}
627 if X_FLIP[amp.getReadoutCorner()]:
628 ampData = np.fliplr(ampData)
629 if Y_FLIP[amp.getReadoutCorner()]:
630 ampData = np.flipud(ampData)
636 r"""Remove CTI effects from local offsets.
638 This implements equation 10 of Snyder+21. For an image with
639 CTI, s
'(m, n), the correction factor is equal to the maximum
644 {A_L s'(m, n - j) exp(-j t / \tau_L)}_j=0^jmax
648 inputArr : `numpy.ndarray`, (nx, ny)
649 Input image data to correct.
650 drift_scale : `float`
651 Drift scale (Snyder+21 A_L value) to use in correction.
653 Decay time (Snyder+21 \tau_L) of the correction.
654 num_previous_pixels : `int`, optional
655 Number of previous pixels to use
for correction. As the
656 CTI has an exponential decay, this essentially truncates
657 the correction where that decay scales the input charge to
662 outputArr : `numpy.ndarray`, (nx, ny)
663 Corrected image data.
665 r = np.exp(-1/decay_time)
666 Ny, Nx = inputArr.shape
669 offset = np.zeros((num_previous_pixels, Ny, Nx))
670 offset[0, :, :] = drift_scale*np.maximum(0, inputArr)
673 for n
in range(1, num_previous_pixels):
674 offset[n, :, n:] = drift_scale*np.maximum(0, inputArr[:, :-n])*(r**n)
676 Linv = np.amax(offset, axis=0)
677 outputArr = inputArr - Linv
683 r"""Apply localized trapping inverse operator to pixel signals.
685 This implements equation 13 of Snyder+21. For an image with
686 CTI, s
'(m, n), the correction factor is equal to the maximum
691 {A_L s'(m, n - j) exp(-j t / \tau_L)}_j=0^jmax
695 inputArr : `numpy.ndarray`, (nx, ny)
696 Input image data to correct.
698 Serial trap describing the capture and release of charge.
700 Mean charge transfer inefficiency, b
from Snyder+21.
701 num_previous_pixels : `int`, optional
702 Number of previous pixels to use
for correction.
706 outputArr : `numpy.ndarray`, (nx, ny)
707 Corrected image data.
710 Ny, Nx = inputArr.shape
712 r = np.exp(-1/trap.emission_time)
715 trap_occupancy = np.zeros((num_previous_pixels, Ny, Nx))
716 for n
in range(num_previous_pixels):
717 trap_occupancy[n, :, n+1:] = trap.capture(np.maximum(0, inputArr))[:, :-(n+1)]*(r**n)
718 trap_occupancy = np.amax(trap_occupancy, axis=0)
721 C = trap.capture(np.maximum(0, inputArr)) - trap_occupancy*r
725 R = np.zeros(inputArr.shape)
726 R[:, 1:] = trap_occupancy[:, 1:]*(1-r)
729 outputArr = inputArr - a*T
std::vector< SchemaItem< Flag > > * items
Geometry and electronic information about raw amplifier images.
A class to contain the data, WCS, and other information needed to describe an image of the sky.
def requiredAttributes(self, value)
def updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
def fromDict(cls, dictionary, **kwargs)
def requiredAttributes(self)
def __init__(self, **kwargs)
def fromDetector(self, detector)
def fromTable(cls, tableList)
def fromDict(cls, dictionary)
def local_trap_inverse(inputArr, trap, global_cti=0.0, num_previous_pixels=6)
def local_offset_inverse(inputArr, drift_scale, decay_time, num_previous_pixels=15)
def flipData(ampData, amp)
def initialize(self, ny, nx, prescan_width)
def trap_charge(self, free_charge)
def __init__(self, size, emission_time, pixel, trap_type, coeffs)
def capture(self, pixel_signals)