Coverage for python/lsst/ip/isr/deferredCharge.py: 16%

253 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-06 01:44 -0700

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/>. 

21import numpy as np 

22from astropy.table import Table 

23 

24from lsst.afw.cameraGeom import ReadoutCorner 

25from lsst.pex.config import Config, Field 

26from lsst.pipe.base import Task 

27from .isrFunctions import gainContext 

28from .calibType import IsrCalib 

29 

30import scipy.interpolate as interp 

31 

32 

33__all__ = ('DeferredChargeConfig', 'DeferredChargeTask', 'SerialTrap', 'DeferredChargeCalib') 

34 

35 

36class SerialTrap(): 

37 """Represents a serial register trap. 

38 

39 Parameters 

40 ---------- 

41 size : `float` 

42 Size of the charge trap, in electrons. 

43 emission_time : `float` 

44 Trap emission time constant, in inverse transfers. 

45 pixel : `int` 

46 Serial pixel location of the trap, including the prescan. 

47 trap_type : `str` 

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. 

55 

56 Raises 

57 ------ 

58 ValueError 

59 Raised if the specified parameters are out of expected range. 

60 """ 

61 

62 def __init__(self, size, emission_time, pixel, trap_type, coeffs): 

63 if size < 0.0: 

64 raise ValueError('Trap size must be greater than or equal to 0.') 

65 self.size = size 

66 

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') 

71 self.emission_time = emission_time 

72 

73 if int(pixel) != pixel: 

74 raise ValueError('Fraction value for pixel not allowed.') 

75 self.pixel = int(pixel) 

76 

77 self.trap_type = trap_type 

78 self.coeffs = coeffs 

79 

80 if self.trap_type not in ('linear', 'logistic', 'spline'): 

81 raise ValueError('Unknown trap type: %s', self.trap_type) 

82 

83 if self.trap_type == 'spline': 

84 centers, values = np.split(np.array(self.coeffs), 2) 

85 self.interp = interp.interp1d(centers, values) 

86 

87 self._trap_array = None 

88 self._trapped_charge = None 

89 

90 def __eq__(self, other): 

91 # A trap is equal to another trap if all of the initialization 

92 # parameters are equal. All other properties are only filled 

93 # during use, and are not persisted into the calibration. 

94 if self.size != other.size: 

95 return False 

96 if self.emission_time != other.emission_time: 

97 return False 

98 if self.pixel != other.pixel: 

99 return False 

100 if self.trap_type != other.trap_type: 

101 return False 

102 if self.coeffs != other.coeffs: 

103 return False 

104 return True 

105 

106 @property 

107 def trap_array(self): 

108 return self._trap_array 

109 

110 @property 

111 def trapped_charge(self): 

112 return self._trapped_charge 

113 

114 def initialize(self, ny, nx, prescan_width): 

115 """Initialize trapping arrays for simulated readout. 

116 

117 Parameters 

118 ---------- 

119 ny : `int` 

120 Number of rows to simulate. 

121 nx : `int` 

122 Number of columns to simulate. 

123 prescan_width : `int` 

124 Additional transfers due to prescan. 

125 

126 Raises 

127 ------ 

128 ValueError 

129 Raised if the trap falls outside of the image. 

130 """ 

131 if self.pixel > nx+prescan_width: 

132 raise ValueError('Trap location {0} must be less than {1}'.format(self.pixel, 

133 nx+prescan_width)) 

134 

135 self._trap_array = np.zeros((ny, nx+prescan_width)) 

136 self._trap_array[:, self.pixel] = self.size 

137 self._trapped_charge = np.zeros((ny, nx+prescan_width)) 

138 

139 def release_charge(self): 

140 """Release charge through exponential decay. 

141 

142 Returns 

143 ------- 

144 released_charge : `float` 

145 Charge released. 

146 """ 

147 released_charge = self._trapped_charge*(1-np.exp(-1./self.emission_time)) 

148 self._trapped_charge -= released_charge 

149 

150 return released_charge 

151 

152 def trap_charge(self, free_charge): 

153 """Perform charge capture using a logistic function. 

154 

155 Parameters 

156 ---------- 

157 free_charge : `float` 

158 Charge available to be trapped. 

159 

160 Returns 

161 ------- 

162 captured_charge : `float` 

163 Amount of charge actually trapped. 

164 """ 

165 captured_charge = (np.clip(self.capture(free_charge), self.trapped_charge, self._trap_array) 

166 - self.trapped_charge) 

167 self._trapped_charge += captured_charge 

168 

169 return captured_charge 

170 

171 def capture(self, pixel_signals): 

172 """Trap capture function. 

173 

174 Parameters 

175 ---------- 

176 pixel_signals : `list` [`float`] 

177 Input pixel values. 

178 

179 Returns 

180 ------- 

181 captured_charge : `list` [`float`] 

182 Amount of charge captured from each pixel. 

183 

184 Raises 

185 ------ 

186 RuntimeError 

187 Raised if the trap type is invalid. 

188 """ 

189 if self.trap_type == 'linear': 

190 scaling = self.coeffs[0] 

191 return np.minimum(self.size, pixel_signals*scaling) 

192 elif self.trap_type == 'logistic': 

193 f0, k = (self.coeffs[0], self.coeffs[1]) 

194 return self.size/(1.+np.exp(-k*(pixel_signals-f0))) 

195 elif self.trap_type == 'spline': 

196 return self.interp(pixel_signals) 

197 else: 

198 raise RuntimeError(f"Invalid trap capture type: {self.trap_type}.") 

199 

200 

201class DeferredChargeCalib(IsrCalib): 

202 r"""Calibration containing deferred charge/CTI parameters. 

203 

204 Parameters 

205 ---------- 

206 **kwargs : 

207 Additional parameters to pass to parent constructor. 

208 

209 Notes 

210 ----- 

211 The charge transfer inefficiency attributes stored are: 

212 

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. 

222 serialTraps : `dict` [`str`, `lsst.ip.isr.SerialTrap`] 

223 A dictionary, keyed by amplifier name, containing a single 

224 serial trap for each amplifier. 

225 """ 

226 _OBSTYPE = 'CTI' 

227 _SCHEMA = 'Deferred Charge' 

228 _VERSION = 1.0 

229 

230 def __init__(self, **kwargs): 

231 self.driftScale = {} 

232 self.decayTime = {} 

233 self.globalCti = {} 

234 self.serialTraps = {} 

235 

236 super().__init__(**kwargs) 

237 self.requiredAttributes.update(['driftScale', 'decayTime', 'globalCti', 'serialTraps']) 

238 

239 def fromDetector(self, detector): 

240 """Read metadata parameters from a detector. 

241 

242 Parameters 

243 ---------- 

244 detector : `lsst.afw.cameraGeom.detector` 

245 Input detector with parameters to use. 

246 

247 Returns 

248 ------- 

249 calib : `lsst.ip.isr.Linearizer` 

250 The calibration constructed from the detector. 

251 """ 

252 

253 pass 

254 

255 @classmethod 

256 def fromDict(cls, dictionary): 

257 """Construct a calibration from a dictionary of properties. 

258 

259 Parameters 

260 ---------- 

261 dictionary : `dict` 

262 Dictionary of properties. 

263 

264 Returns 

265 ------- 

266 calib : `lsst.ip.isr.CalibType` 

267 Constructed calibration. 

268 

269 Raises 

270 ------ 

271 RuntimeError : 

272 Raised if the supplied dictionary is for a different 

273 calibration. 

274 """ 

275 calib = cls() 

276 

277 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']: 

278 raise RuntimeError(f"Incorrect CTI supplied. Expected {calib._OBSTYPE}, " 

279 f"found {dictionary['metadata']['OBSTYPE']}") 

280 

281 calib.setMetadata(dictionary['metadata']) 

282 

283 calib.driftScale = dictionary['driftScale'] 

284 calib.decayTime = dictionary['decayTime'] 

285 calib.globalCti = dictionary['globalCti'] 

286 

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'], 

291 ampTraps['coeffs']) 

292 calib.updateMetadata() 

293 return calib 

294 

295 def toDict(self): 

296 """Return a dictionary containing the calibration properties. 

297 The dictionary should be able to be round-tripped through 

298 ``fromDict``. 

299 

300 Returns 

301 ------- 

302 dictionary : `dict` 

303 Dictionary of properties. 

304 """ 

305 self.updateMetadata() 

306 outDict = {} 

307 outDict['metadata'] = self.getMetadata() 

308 

309 outDict['driftScale'] = self.driftScale 

310 outDict['decayTime'] = self.decayTime 

311 outDict['globalCti'] = self.globalCti 

312 

313 outDict['serialTraps'] = {} 

314 for ampName in self.serialTraps: 

315 ampTrap = {'size': self.serialTraps[ampName].size, 

316 'emissionTime': self.serialTraps[ampName].emission_time, 

317 'pixel': self.serialTraps[ampName].pixel, 

318 'trap_type': self.serialTraps[ampName].trap_type, 

319 'coeffs': self.serialTraps[ampName].coeffs} 

320 outDict['serialTraps'][ampName] = ampTrap 

321 

322 return outDict 

323 

324 @classmethod 

325 def fromTable(cls, tableList): 

326 """Construct calibration from a list of tables. 

327 

328 This method uses the ``fromDict`` method to create the 

329 calibration, after constructing an appropriate dictionary from 

330 the input tables. 

331 

332 Parameters 

333 ---------- 

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. 

339 

340 Returns 

341 ------- 

342 calib : `lsst.ip.isr.DeferredChargeCalib` 

343 The calibration defined in the tables. 

344 

345 Raises 

346 ------ 

347 ValueError 

348 Raised if the trap type or trap coefficients are not 

349 defined properly. 

350 """ 

351 ampTable = tableList[0] 

352 

353 inDict = {} 

354 inDict['metadata'] = ampTable.meta 

355 

356 amps = ampTable['AMPLIFIER'] 

357 driftScale = ampTable['DRIFT_SCALE'] 

358 decayTime = ampTable['DECAY_TIME'] 

359 globalCti = ampTable['GLOBAL_CTI'] 

360 

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)} 

364 

365 inDict['serialTraps'] = {} 

366 trapTable = tableList[1] 

367 

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'] 

374 

375 for index, amp in enumerate(amps): 

376 ampTrap = {} 

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() 

382 

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'])) 

395 else: 

396 raise ValueError('Unknown trap type: %s', ampTrap['trap_type']) 

397 

398 inDict['serialTraps'][amp] = ampTrap 

399 

400 return cls.fromDict(inDict) 

401 

402 def toTable(self): 

403 """Construct a list of tables containing the information in this 

404 calibration. 

405 

406 The list of tables should create an identical calibration 

407 after being passed to this class's fromTable method. 

408 

409 Returns 

410 ------- 

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. 

416 """ 

417 tableList = [] 

418 self.updateMetadata() 

419 

420 ampList = [] 

421 driftScale = [] 

422 decayTime = [] 

423 globalCti = [] 

424 

425 for amp in self.driftScale.keys(): 

426 ampList.append(amp) 

427 driftScale.append(self.driftScale[amp]) 

428 decayTime.append(self.decayTime[amp]) 

429 globalCti.append(self.globalCti[amp]) 

430 

431 ampTable = Table({'AMPLIFIER': ampList, 

432 'DRIFT_SCALE': driftScale, 

433 'DECAY_TIME': decayTime, 

434 'GLOBAL_CTI': globalCti, 

435 }) 

436 

437 ampTable.meta = self.getMetadata().toDict() 

438 tableList.append(ampTable) 

439 

440 ampList = [] 

441 sizeList = [] 

442 timeList = [] 

443 pixelList = [] 

444 typeList = [] 

445 coeffList = [] 

446 

447 # Get maximum coeff length 

448 maxCoeffLength = 0 

449 for trap in self.serialTraps.values(): 

450 maxCoeffLength = np.maximum(maxCoeffLength, len(trap.coeffs)) 

451 

452 # Pack and pad the end of the coefficients with NaN values. 

453 for amp, trap in self.serialTraps.items(): 

454 ampList.append(amp) 

455 sizeList.append(trap.size) 

456 timeList.append(trap.emission_time) 

457 pixelList.append(trap.pixel) 

458 typeList.append(trap.trap_type) 

459 

460 coeffs = trap.coeffs 

461 if len(coeffs) != maxCoeffLength: 

462 coeffs = np.pad(coeffs, (0, maxCoeffLength - len(coeffs)), 

463 constant_values=np.nan).tolist() 

464 coeffList.append(coeffs) 

465 

466 trapTable = Table({'AMPLIFIER': ampList, 

467 'SIZE': sizeList, 

468 'EMISSION_TIME': timeList, 

469 'PIXEL': pixelList, 

470 'TYPE': typeList, 

471 'COEFFS': coeffList}) 

472 

473 tableList.append(trapTable) 

474 

475 return tableList 

476 

477 

478class DeferredChargeConfig(Config): 

479 """Settings for deferred charge correction. 

480 """ 

481 nPixelOffsetCorrection = Field( 

482 dtype=int, 

483 doc="Number of prior pixels to use for local offset correction.", 

484 default=15, 

485 ) 

486 nPixelTrapCorrection = Field( 

487 dtype=int, 

488 doc="Number of prior pixels to use for trap correction.", 

489 default=6, 

490 ) 

491 useGains = Field( 

492 dtype=bool, 

493 doc="If true, scale by the gain.", 

494 default=False, 

495 ) 

496 zeroUnusedPixels = Field( 

497 dtype=bool, 

498 doc="If true, set serial prescan and parallel overscan to zero before correction.", 

499 default=False, 

500 ) 

501 

502 

503class DeferredChargeTask(Task): 

504 """Task to correct an exposure for charge transfer inefficiency. 

505 

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). 

509 """ 

510 ConfigClass = DeferredChargeConfig 

511 _DefaultName = 'isrDeferredCharge' 

512 

513 def run(self, exposure, ctiCalib, gains=None): 

514 """Correct deferred charge/CTI issues. 

515 

516 Parameters 

517 ---------- 

518 exposure : `lsst.afw.image.Exposure` 

519 Exposure to correct the deferred charge on. 

520 ctiCalib : `lsst.ip.isr.DeferredChargeCalib` 

521 Calibration object containing the charge transfer 

522 inefficiency model. 

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 

526 object are used. 

527 

528 Returns 

529 ------- 

530 exposure : `lsst.afw.image.Exposure` 

531 The corrected exposure. 

532 """ 

533 image = exposure.getMaskedImage().image 

534 detector = exposure.getDetector() 

535 

536 # If gains were supplied, they should be used. If useGains is 

537 # true, but no external gains were supplied, use the nominal 

538 # gains listed in the detector. Finally, if useGains is 

539 # false, fake a dictionary of unit gains for ``gainContext``. 

540 if self.config.useGains: 

541 if gains is None: 

542 gains = {amp.getName(): amp.getGain() for amp in detector.getAmplifiers()} 

543 

544 with gainContext(exposure, image, self.config.useGains, gains): 

545 for amp in detector.getAmplifiers(): 

546 ampName = amp.getName() 

547 

548 ampImage = image[amp.getRawBBox()] 

549 if self.config.zeroUnusedPixels: 

550 # We don't apply overscan subtraction, so zero these 

551 # out for now. 

552 ampImage[amp.getRawParallelOverscanBBox()].array[:, :] = 0.0 

553 ampImage[amp.getRawSerialPrescanBBox()].array[:, :] = 0.0 

554 

555 # The algorithm expects that the readout corner is in 

556 # the lower left corner. Flip it to be so: 

557 

558 ampData = self.flipData(ampImage.array, amp) 

559 

560 if ctiCalib.driftScale[ampName] > 0.0: 

561 correctedAmpData = self.local_offset_inverse(ampData, 

562 ctiCalib.driftScale[ampName], 

563 ctiCalib.decayTime[ampName], 

564 self.config.nPixelOffsetCorrection) 

565 else: 

566 correctedAmpData = ampData.copy() 

567 

568 correctedAmpData = self.local_trap_inverse(correctedAmpData, 

569 ctiCalib.serialTraps[ampName], 

570 ctiCalib.globalCti[ampName], 

571 self.config.nPixelTrapCorrection) 

572 

573 # Undo flips here. The method is symmetric. 

574 correctedAmpData = self.flipData(correctedAmpData, amp) 

575 image[amp.getBBox()].array[:, :] = correctedAmpData[:, :] 

576 

577 return exposure 

578 

579 @staticmethod 

580 def flipData(ampData, amp): 

581 """Flip data array such that readout corner is at lower-left. 

582 

583 Parameters 

584 ---------- 

585 ampData : `np.ndarray`, (nx, ny) 

586 Image data to flip. 

587 amp : `lsst.afw.cameraGeom.Amplifier` 

588 Amplifier to get readout corner information. 

589 

590 Returns 

591 ------- 

592 ampData : `np.ndarray`, (nx, ny) 

593 Flipped image data. 

594 """ 

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} 

603 

604 if X_FLIP(amp.getReadoutCorner()): 

605 ampData = np.fliplr(ampData) 

606 if Y_FLIP(amp.getReadoutCorner()): 

607 ampData = np.flipud(ampData) 

608 

609 return ampData 

610 

611 @staticmethod 

612 def local_offset_inverse(inputArr, drift_scale, decay_time, num_previous_pixels=15): 

613 r"""Remove CTI effects from local offsets. 

614 

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 

617 value of the set of: 

618 {A_L s'(m, n - j) exp(-j t / \tau_L)}_j=0^jmax 

619 

620 Parameters 

621 ---------- 

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. 

626 decay_time : `float` 

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 

632 near zero. 

633 

634 Returns 

635 ------- 

636 outputArr : `np.ndarray`, (nx, ny) 

637 Corrected image data. 

638 """ 

639 r = np.exp(-1/decay_time) 

640 Ny, Nx = inputArr.shape 

641 

642 # j = 0 term: 

643 offset = np.zeros((num_previous_pixels, Ny, Nx)) 

644 offset[0, :, :] = drift_scale*np.maximum(0, inputArr) 

645 

646 # j = 1..jmax terms: 

647 for n in range(1, num_previous_pixels): 

648 offset[n, :, n:] = drift_scale*np.maximum(0, inputArr[:, :-n])*(r**n) 

649 

650 Linv = np.amax(offset, axis=0) 

651 outputArr = inputArr - Linv 

652 

653 return outputArr 

654 

655 @staticmethod 

656 def local_trap_inverse(inputArr, trap, global_cti=0.0, num_previous_pixels=6): 

657 r"""Apply localized trapping inverse operator to pixel signals. 

658 

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 

661 value of the set of: 

662 {A_L s'(m, n - j) exp(-j t / \tau_L)}_j=0^jmax 

663 

664 Parameters 

665 ---------- 

666 inputArr : `np.ndarray`, (nx, ny) 

667 Input image data to correct. 

668 trap : `lsst.ip.isr.SerialTrap` 

669 Serial trap describing the capture and release of charge. 

670 global_cti: `float` 

671 Mean charge transfer inefficiency, b from Snyder+21. 

672 num_previous_pixels : `int`, optional 

673 Number of previous pixels to use for correction. 

674 

675 Returns 

676 ------- 

677 outputArr : `np.ndarray`, (nx, ny) 

678 Corrected image data. 

679 

680 """ 

681 Ny, Nx = inputArr.shape 

682 a = 1 - global_cti 

683 r = np.exp(-1/trap.emission_time) 

684 

685 # Estimate trap occupancies during readout 

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) 

690 

691 # Estimate captured charge 

692 C = trap.capture(np.maximum(0, inputArr)) - trap_occupancy*r 

693 C[C < 0] = 0. 

694 

695 # Estimate released charge 

696 R = np.zeros(inputArr.shape) 

697 R[:, 1:] = trap_occupancy[:, 1:]*(1-r) 

698 T = R - C 

699 

700 outputArr = inputArr - a*T 

701 

702 return outputArr