22from astropy.table
import Table
26from lsst.pipe.base
import Task
27from .isrFunctions
import gainContext
28from .calibType
import IsrCalib
30import scipy.interpolate
as interp
33__all__ = (
'DeferredChargeConfig',
'DeferredChargeTask',
'SerialTrap',
'DeferredChargeCalib')
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), 2)
85 self.
interp = interp.interp1d(centers, values)
94 if self.
size != other.size:
98 if self.
pixel != other.pixel:
102 if self.
coeffs != other.coeffs:
115 """Initialize trapping arrays for simulated readout.
120 Number of rows to simulate.
122 Number of columns to simulate.
123 prescan_width : `int`
124 Additional transfers due to prescan.
129 Raised if the trap falls outside of the image.
131 if self.
pixel > nx+prescan_width:
132 raise ValueError(
'Trap location {0} must be less than {1}'.format(self.
pixel,
135 self.
_trap_array = np.zeros((ny, nx+prescan_width))
140 """Release charge through exponential decay.
144 released_charge : `float`
150 return released_charge
153 """Perform charge capture using a logistic function.
157 free_charge : `float`
158 Charge available to be trapped.
162 captured_charge : `float`
163 Amount of charge actually trapped.
169 return captured_charge
172 """Trap capture function.
176 pixel_signals : `list` [`float`]
181 captured_charge : `list` [`float`]
182 Amount of charge captured from each pixel.
187 Raised
if the trap type
is invalid.
191 return np.minimum(self.
size, pixel_signals*scaling)
194 return self.
size/(1.+np.exp(-k*(pixel_signals-f0)))
196 return self.
interp(pixel_signals)
198 raise RuntimeError(f
"Invalid trap capture type: {self.trap_type}.")
202 r"""Calibration containing deferred charge/CTI parameters.
207 Additional parameters to pass to parent constructor.
211 The charge transfer inefficiency attributes stored are:
213 driftScale : `dict` [`str`, `float`]
214 A dictionary, keyed by amplifier name, of the local electronic
215 offset drift scale parameter, A_L
in Snyder+2021.
216 decayTime : `dict` [`str`, `float`]
217 A dictionary, keyed by amplifier name, of the local electronic
218 offset decay time, \tau_L
in Snyder+2021.
219 globalCti : `dict` [`str`, `float`]
220 A dictionary, keyed by amplifier name, of the mean
global CTI
221 paramter, b
in Snyder+2021.
223 A dictionary, keyed by amplifier name, containing a single
224 serial trap
for each amplifier.
227 _SCHEMA =
'Deferred Charge'
240 """Read metadata parameters from a detector.
244 detector : `lsst.afw.cameraGeom.detector`
245 Input detector with parameters to use.
250 The calibration constructed
from the detector.
257 """Construct a calibration from a dictionary of properties.
262 Dictionary of properties.
266 calib : `lsst.ip.isr.CalibType`
267 Constructed calibration.
272 Raised if the supplied dictionary
is for a different
277 if calib._OBSTYPE != dictionary[
'metadata'][
'OBSTYPE']:
278 raise RuntimeError(f
"Incorrect CTI supplied. Expected {calib._OBSTYPE}, "
279 f
"found {dictionary['metadata']['OBSTYPE']}")
281 calib.setMetadata(dictionary[
'metadata'])
283 calib.driftScale = dictionary[
'driftScale']
284 calib.decayTime = dictionary[
'decayTime']
285 calib.globalCti = dictionary[
'globalCti']
287 for ampName
in dictionary[
'serialTraps']:
288 ampTraps = dictionary[
'serialTraps'][ampName]
289 calib.serialTraps[ampName] =
SerialTrap(ampTraps[
'size'], ampTraps[
'emissionTime'],
290 ampTraps[
'pixel'], ampTraps[
'trap_type'],
292 calib.updateMetadata()
296 """Return a dictionary containing the calibration properties.
297 The dictionary should be able to be round-tripped through
303 Dictionary of properties.
313 outDict[
'serialTraps'] = {}
316 'emissionTime': self.
serialTraps[ampName].emission_time,
320 outDict[
'serialTraps'][ampName] = ampTrap
326 """Construct calibration from a list of tables.
328 This method uses the ``fromDict`` method to create the
329 calibration, after constructing an appropriate dictionary from
334 tableList : `list` [`lsst.afw.table.Table`]
335 List of tables to use to construct the crosstalk
336 calibration. Two tables are expected
in this list, the
337 first containing the per-amplifier CTI parameters,
and the
338 second containing the parameters
for serial traps.
343 The calibration defined
in the tables.
348 Raised
if the trap type
or trap coefficients are
not
351 ampTable = tableList[0]
354 inDict['metadata'] = ampTable.meta
356 amps = ampTable[
'AMPLIFIER']
357 driftScale = ampTable[
'DRIFT_SCALE']
358 decayTime = ampTable[
'DECAY_TIME']
359 globalCti = ampTable[
'GLOBAL_CTI']
361 inDict[
'driftScale'] = {amp: value
for amp, value
in zip(amps, driftScale)}
362 inDict[
'decayTime'] = {amp: value
for amp, value
in zip(amps, decayTime)}
363 inDict[
'globalCti'] = {amp: value
for amp, value
in zip(amps, globalCti)}
365 inDict[
'serialTraps'] = {}
366 trapTable = tableList[1]
368 amps = trapTable[
'AMPLIFIER']
369 sizes = trapTable[
'SIZE']
370 emissionTimes = trapTable[
'EMISSION_TIME']
371 pixels = trapTable[
'PIXEL']
372 trap_type = trapTable[
'TYPE']
373 coeffs = trapTable[
'COEFFS']
375 for index, amp
in enumerate(amps):
377 ampTrap[
'size'] = sizes[index]
378 ampTrap[
'emissionTime'] = emissionTimes[index]
379 ampTrap[
'pixel'] = pixels[index]
380 ampTrap[
'trap_type'] = trap_type[index]
381 ampTrap[
'coeffs'] = np.array(coeffs[index])[~np.isnan(coeffs[index])].tolist()
383 if ampTrap[
'trap_type'] ==
'linear':
384 if len(ampTrap[
'coeffs']) < 1:
385 raise ValueError(
"CTI Amplifier %s coefficients for trap has illegal length %d.",
386 amp, len(ampTrap[
'coeffs']))
387 elif ampTrap[
'trap_type'] ==
'logistic':
388 if len(ampTrap[
'coeffs']) < 2:
389 raise ValueError(
"CTI Amplifier %s coefficients for trap has illegal length %d.",
390 amp, len(ampTrap[
'coeffs']))
391 elif ampTrap[
'trap_type'] ==
'spline':
392 if len(ampTrap[
'coeffs']) % 2 != 0:
393 raise ValueError(
"CTI Amplifier %s coefficients for trap has illegal length %d.",
394 amp, len(ampTrap[
'coeffs']))
396 raise ValueError(
'Unknown trap type: %s', ampTrap[
'trap_type'])
398 inDict[
'serialTraps'][amp] = ampTrap
403 """Construct a list of tables containing the information in this
406 The list of tables should create an identical calibration
407 after being passed to this class's fromTable method.
411 tableList : `list` [`lsst.afw.table.Table`]
412 List of tables containing the crosstalk calibration
413 information. Two tables are generated for this list, the
414 first containing the per-amplifier CTI parameters,
and the
415 second containing the parameters
for serial traps.
431 ampTable = Table({
'AMPLIFIER': ampList,
432 'DRIFT_SCALE': driftScale,
433 'DECAY_TIME': decayTime,
434 'GLOBAL_CTI': globalCti,
438 tableList.append(ampTable)
450 maxCoeffLength = np.maximum(maxCoeffLength, len(trap.coeffs))
455 sizeList.append(trap.size)
456 timeList.append(trap.emission_time)
457 pixelList.append(trap.pixel)
458 typeList.append(trap.trap_type)
461 if len(coeffs) != maxCoeffLength:
462 coeffs = np.pad(coeffs, (0, maxCoeffLength - len(coeffs)),
463 constant_values=np.nan).tolist()
464 coeffList.append(coeffs)
466 trapTable = Table({
'AMPLIFIER': ampList,
468 'EMISSION_TIME': timeList,
471 'COEFFS': coeffList})
473 tableList.append(trapTable)
479 """Settings for deferred charge correction.
481 nPixelOffsetCorrection = Field(
483 doc="Number of prior pixels to use for local offset correction.",
486 nPixelTrapCorrection = Field(
488 doc=
"Number of prior pixels to use for trap correction.",
493 doc=
"If true, scale by the gain.",
496 zeroUnusedPixels = Field(
498 doc=
"If true, set serial prescan and parallel overscan to zero before correction.",
504 """Task to correct an exposure for charge transfer inefficiency.
506 This uses the methods described by Snyder et al. 2021, Journal of
507 Astronimcal Telescopes, Instruments, and Systems, 7,
508 048002. doi:10.1117/1.JATIS.7.4.048002 (Snyder+21).
510 ConfigClass = DeferredChargeConfig
511 _DefaultName = 'isrDeferredCharge'
513 def run(self, exposure, ctiCalib, gains=None):
514 """Correct deferred charge/CTI issues.
519 Exposure to correct the deferred charge on.
521 Calibration object containing the charge transfer
523 gains : `dict` [`str`, `float`]
524 A dictionary, keyed by amplifier name, of the gains to
525 use. If gains is None, the nominal gains
in the amplifier
531 The corrected exposure.
533 image = exposure.getMaskedImage().image
534 detector = exposure.getDetector()
540 if self.config.useGains:
542 gains = {amp.getName(): amp.getGain()
for amp
in detector.getAmplifiers()}
544 with gainContext(exposure, image, self.config.useGains, gains):
545 for amp
in detector.getAmplifiers():
546 ampName = amp.getName()
548 ampImage = image[amp.getRawBBox()]
549 if self.config.zeroUnusedPixels:
552 ampImage[amp.getRawParallelOverscanBBox()].array[:, :] = 0.0
553 ampImage[amp.getRawSerialPrescanBBox()].array[:, :] = 0.0
558 ampData = self.
flipData(ampImage.array, amp)
560 if ctiCalib.driftScale[ampName] > 0.0:
562 ctiCalib.driftScale[ampName],
563 ctiCalib.decayTime[ampName],
564 self.config.nPixelOffsetCorrection)
566 correctedAmpData = ampData.copy()
569 ctiCalib.serialTraps[ampName],
570 ctiCalib.globalCti[ampName],
571 self.config.nPixelTrapCorrection)
574 correctedAmpData = self.
flipData(correctedAmpData, amp)
575 image[amp.getBBox()].array[:, :] = correctedAmpData[:, :]
581 """Flip data array such that readout corner is at lower-left.
585 ampData : `np.ndarray`, (nx, ny)
588 Amplifier to get readout corner information.
592 ampData : `np.ndarray`, (nx, ny)
595 X_FLIP = {ReadoutCorner.LL: False,
596 ReadoutCorner.LR:
True,
597 ReadoutCorner.UL:
False,
598 ReadoutCorner.UR:
True}
599 Y_FLIP = {ReadoutCorner.LL:
False,
600 ReadoutCorner.LR:
False,
601 ReadoutCorner.UL:
True,
602 ReadoutCorner.UR:
True}
604 if X_FLIP(amp.getReadoutCorner()):
605 ampData = np.fliplr(ampData)
606 if Y_FLIP(amp.getReadoutCorner()):
607 ampData = np.flipud(ampData)
613 r"""Remove CTI effects from local offsets.
615 This implements equation 10 of Snyder+21. For an image with
616 CTI, s
'(m, n), the correction factor is equal to the maximum
618 {A_L s'(m, n - j) exp(-j t / \tau_L)}_j=0^jmax
622 inputArr : `np.ndarray`, (nx, ny)
623 Input image data to correct.
624 drift_scale : `float`
625 Drift scale (Snyder+21 A_L value) to use in correction.
627 Decay time (Snyder+21 \tau_L) of the correction.
628 num_previous_pixels : `int`, optional
629 Number of previous pixels to use
for correction. As the
630 CTI has an exponential decay, this essentially truncates
631 the correction where that decay scales the input charge to
636 outputArr : `np.ndarray`, (nx, ny)
637 Corrected image data.
639 r = np.exp(-1/decay_time)
640 Ny, Nx = inputArr.shape
643 offset = np.zeros((num_previous_pixels, Ny, Nx))
644 offset[0, :, :] = drift_scale*np.maximum(0, inputArr)
647 for n
in range(1, num_previous_pixels):
648 offset[n, :, n:] = drift_scale*np.maximum(0, inputArr[:, :-n])*(r**n)
650 Linv = np.amax(offset, axis=0)
651 outputArr = inputArr - Linv
657 r"""Apply localized trapping inverse operator to pixel signals.
659 This implements equation 13 of Snyder+21. For an image with
660 CTI, s
'(m, n), the correction factor is equal to the maximum
662 {A_L s'(m, n - j) exp(-j t / \tau_L)}_j=0^jmax
666 inputArr : `np.ndarray`, (nx, ny)
667 Input image data to correct.
669 Serial trap describing the capture and release of charge.
671 Mean charge transfer inefficiency, b
from Snyder+21.
672 num_previous_pixels : `int`, optional
673 Number of previous pixels to use
for correction.
677 outputArr : `np.ndarray`, (nx, ny)
678 Corrected image data.
681 Ny, Nx = inputArr.shape
683 r = np.exp(-1/trap.emission_time)
686 trap_occupancy = np.zeros((num_previous_pixels, Ny, Nx))
687 for n
in range(num_previous_pixels):
688 trap_occupancy[n, :, n+1:] = trap.capture(np.maximum(0, inputArr))[:, :-(n+1)]*(r**n)
689 trap_occupancy = np.amax(trap_occupancy, axis=0)
692 C = trap.capture(np.maximum(0, inputArr)) - trap_occupancy*r
696 R = np.zeros(inputArr.shape)
697 R[:, 1:] = trap_occupancy[:, 1:]*(1-r)
700 outputArr = inputArr - a*T
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 run(self, exposure, ctiCalib, gains=None)
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)