Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# 

2# LSST Data Management System 

3# Copyright 2008-2017 AURA/LSST. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22""" 

23Apply intra-detector crosstalk corrections 

24""" 

25import numpy as np 

26from astropy.table import Table 

27 

28import lsst.afw.math 

29import lsst.afw.detection 

30import lsst.daf.butler 

31from lsst.pex.config import Config, Field, ChoiceField, ListField 

32from lsst.pipe.base import Task 

33 

34from lsst.ip.isr import IsrCalib 

35 

36 

37__all__ = ["CrosstalkCalib", "CrosstalkConfig", "CrosstalkTask", 

38 "NullCrosstalkTask"] 

39 

40 

41class CrosstalkCalib(IsrCalib): 

42 """Calibration of amp-to-amp crosstalk coefficients. 

43 

44 Parameters 

45 ---------- 

46 detector : `lsst.afw.cameraGeom.Detector`, optional 

47 Detector to use to pull coefficients from. 

48 nAmp : `int`, optional 

49 Number of amplifiers to initialize. 

50 log : `lsst.log.Log`, optional 

51 Log to write messages to. 

52 **kwargs : 

53 Parameters to pass to parent constructor. 

54 

55 Notes 

56 ----- 

57 The crosstalk attributes stored are: 

58 

59 hasCrosstalk : `bool` 

60 Whether there is crosstalk defined for this detector. 

61 nAmp : `int` 

62 Number of amplifiers in this detector. 

63 crosstalkShape : `tuple` [`int`, `int`] 

64 A tuple containing the shape of the ``coeffs`` matrix. This 

65 should be equivalent to (``nAmp``, ``nAmp``). 

66 coeffs : `np.ndarray` 

67 A matrix containing the crosstalk coefficients. coeff[i][j] 

68 contains the coefficients to calculate the contribution 

69 amplifier_j has on amplifier_i (each row[i] contains the 

70 corrections for detector_i). 

71 coeffErr : `np.ndarray`, optional 

72 A matrix (as defined by ``coeffs``) containing the standard 

73 distribution of the crosstalk measurements. 

74 coeffNum : `np.ndarray`, optional 

75 A matrix containing the number of pixel pairs used to measure 

76 the ``coeffs`` and ``coeffErr``. 

77 coeffValid : `np.ndarray`, optional 

78 A matrix of Boolean values indicating if the coefficient is 

79 valid, defined as abs(coeff) > coeffErr / sqrt(coeffNum). 

80 interChip : `dict` [`np.ndarray`] 

81 A dictionary keyed by detectorName containing ``coeffs`` 

82 matrices used to correct for inter-chip crosstalk with a 

83 source on the detector indicated. 

84 

85 """ 

86 _OBSTYPE = 'CROSSTALK' 

87 _SCHEMA = 'Gen3 Crosstalk' 

88 _VERSION = 1.0 

89 

90 def __init__(self, detector=None, nAmp=0, **kwargs): 

91 self.hasCrosstalk = False 

92 self.nAmp = nAmp if nAmp else 0 

93 self.crosstalkShape = (self.nAmp, self.nAmp) 

94 

95 self.coeffs = np.zeros(self.crosstalkShape) if self.nAmp else None 

96 self.coeffErr = np.zeros(self.crosstalkShape) if self.nAmp else None 

97 self.coeffNum = np.zeros(self.crosstalkShape, 

98 dtype=int) if self.nAmp else None 

99 self.coeffValid = np.zeros(self.crosstalkShape, 

100 dtype=bool) if self.nAmp else None 

101 self.interChip = {} 

102 

103 super().__init__(**kwargs) 

104 self.requiredAttributes.update(['hasCrosstalk', 'nAmp', 'coeffs', 

105 'coeffErr', 'coeffNum', 'coeffValid', 

106 'interChip']) 

107 if detector: 

108 self.fromDetector(detector) 

109 

110 def updateMetadata(self, setDate=False, **kwargs): 

111 """Update calibration metadata. 

112 

113 This calls the base class's method after ensuring the required 

114 calibration keywords will be saved. 

115 

116 Parameters 

117 ---------- 

118 setDate : `bool`, optional 

119 Update the CALIBDATE fields in the metadata to the current 

120 time. Defaults to False. 

121 kwargs : 

122 Other keyword parameters to set in the metadata. 

123 """ 

124 kwargs['DETECTOR'] = self._detectorId 

125 kwargs['DETECTOR_NAME'] = self._detectorName 

126 kwargs['DETECTOR_SERIAL'] = self._detectorSerial 

127 kwargs['HAS_CROSSTALK'] = self.hasCrosstalk 

128 kwargs['NAMP'] = self.nAmp 

129 self.crosstalkShape = (self.nAmp, self.nAmp) 

130 kwargs['CROSSTALK_SHAPE'] = self.crosstalkShape 

131 

132 super().updateMetadata(setDate=setDate, **kwargs) 

133 

134 def fromDetector(self, detector, coeffVector=None): 

135 """Set calibration parameters from the detector. 

136 

137 Parameters 

138 ---------- 

139 detector : `lsst.afw.cameraGeom.Detector` 

140 Detector to use to set parameters from. 

141 coeffVector : `numpy.array`, optional 

142 Use the detector geometry (bounding boxes and flip 

143 information), but use ``coeffVector`` instead of the 

144 output of ``detector.getCrosstalk()``. 

145 

146 Returns 

147 ------- 

148 calib : `lsst.ip.isr.CrosstalkCalib` 

149 The calibration constructed from the detector. 

150 

151 """ 

152 if detector.hasCrosstalk() or coeffVector: 

153 self._detectorName = detector.getName() 

154 self._detectorSerial = detector.getSerial() 

155 

156 self.nAmp = len(detector) 

157 self.crosstalkShape = (self.nAmp, self.nAmp) 

158 

159 if coeffVector is not None: 

160 crosstalkCoeffs = coeffVector 

161 else: 

162 crosstalkCoeffs = detector.getCrosstalk() 

163 if len(crosstalkCoeffs) == 1 and crosstalkCoeffs[0] == 0.0: 

164 return self 

165 self.coeffs = np.array(crosstalkCoeffs).reshape(self.crosstalkShape) 

166 

167 if self.coeffs.shape != self.crosstalkShape: 

168 raise RuntimeError("Crosstalk coefficients do not match detector shape. " 

169 f"{self.crosstalkShape} {self.nAmp}") 

170 

171 self.interChip = {} 

172 self.hasCrosstalk = True 

173 self.updateMetadata() 

174 return self 

175 

176 @classmethod 

177 def fromDict(cls, dictionary): 

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

179 

180 Must be implemented by the specific calibration subclasses. 

181 

182 Parameters 

183 ---------- 

184 dictionary : `dict` 

185 Dictionary of properties. 

186 

187 Returns 

188 ------- 

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

190 Constructed calibration. 

191 

192 Raises 

193 ------ 

194 RuntimeError : 

195 Raised if the supplied dictionary is for a different 

196 calibration. 

197 """ 

198 calib = cls() 

199 

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

201 raise RuntimeError(f"Incorrect crosstalk supplied. Expected {calib._OBSTYPE}, " 

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

203 

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

205 

206 if 'detectorName' in dictionary: 

207 calib._detectorName = dictionary.get('detectorName') 

208 elif 'DETECTOR_NAME' in dictionary: 

209 calib._detectorName = dictionary.get('DETECTOR_NAME') 

210 elif 'DET_NAME' in dictionary['metadata']: 

211 calib._detectorName = dictionary['metadata']['DET_NAME'] 

212 else: 

213 calib._detectorName = None 

214 

215 if 'detectorSerial' in dictionary: 

216 calib._detectorSerial = dictionary.get('detectorSerial') 

217 elif 'DETECTOR_SERIAL' in dictionary: 

218 calib._detectorSerial = dictionary.get('DETECTOR_SERIAL') 

219 elif 'DET_SER' in dictionary['metadata']: 

220 calib._detectorSerial = dictionary['metadata']['DET_SER'] 

221 else: 

222 calib._detectorSerial = None 

223 

224 if 'detectorId' in dictionary: 

225 calib._detectorId = dictionary.get('detectorId') 

226 elif 'DETECTOR' in dictionary: 

227 calib._detectorId = dictionary.get('DETECTOR') 

228 elif 'DETECTOR' in dictionary['metadata']: 

229 calib._detectorId = dictionary['metadata']['DETECTOR'] 

230 elif calib._detectorSerial: 

231 calib._detectorId = calib._detectorSerial 

232 else: 

233 calib._detectorId = None 

234 

235 if 'instrument' in dictionary: 

236 calib._instrument = dictionary.get('instrument') 

237 elif 'INSTRUME' in dictionary['metadata']: 

238 calib._instrument = dictionary['metadata']['INSTRUME'] 

239 else: 

240 calib._instrument = None 

241 

242 calib.hasCrosstalk = dictionary.get('hasCrosstalk', 

243 dictionary['metadata'].get('HAS_CROSSTALK', False)) 

244 if calib.hasCrosstalk: 

245 calib.nAmp = dictionary.get('nAmp', dictionary['metadata'].get('NAMP', 0)) 

246 calib.crosstalkShape = (calib.nAmp, calib.nAmp) 

247 calib.coeffs = np.array(dictionary['coeffs']).reshape(calib.crosstalkShape) 

248 if 'coeffErr' in dictionary: 

249 calib.coeffErr = np.array(dictionary['coeffErr']).reshape(calib.crosstalkShape) 

250 else: 

251 calib.coeffErr = np.zeros_like(calib.coeffs) 

252 if 'coeffNum' in dictionary: 

253 calib.coeffNum = np.array(dictionary['coeffNum']).reshape(calib.crosstalkShape) 

254 else: 

255 calib.coeffNum = np.zeros_like(calib.coeffs, dtype=int) 

256 if 'coeffValid' in dictionary: 

257 calib.coeffValid = np.array(dictionary['coeffValid']).reshape(calib.crosstalkShape) 

258 else: 

259 calib.coeffValid = np.ones_like(calib.coeffs, dtype=bool) 

260 

261 calib.interChip = dictionary.get('interChip', None) 

262 if calib.interChip: 

263 for detector in calib.interChip: 

264 coeffVector = calib.interChip[detector] 

265 calib.interChip[detector] = np.array(coeffVector).reshape(calib.crosstalkShape) 

266 

267 calib.updateMetadata() 

268 return calib 

269 

270 def toDict(self): 

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

272 

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

274 `fromDict`. 

275 

276 Returns 

277 ------- 

278 dictionary : `dict` 

279 Dictionary of properties. 

280 """ 

281 self.updateMetadata() 

282 

283 outDict = {} 

284 metadata = self.getMetadata() 

285 outDict['metadata'] = metadata 

286 

287 outDict['hasCrosstalk'] = self.hasCrosstalk 

288 outDict['nAmp'] = self.nAmp 

289 outDict['crosstalkShape'] = self.crosstalkShape 

290 

291 ctLength = self.nAmp*self.nAmp 

292 outDict['coeffs'] = self.coeffs.reshape(ctLength).tolist() 

293 

294 if self.coeffErr is not None: 

295 outDict['coeffErr'] = self.coeffErr.reshape(ctLength).tolist() 

296 if self.coeffNum is not None: 

297 outDict['coeffNum'] = self.coeffNum.reshape(ctLength).tolist() 

298 if self.coeffValid is not None: 

299 outDict['coeffValid'] = self.coeffValid.reshape(ctLength).tolist() 

300 

301 if self.interChip: 

302 outDict['interChip'] = dict() 

303 for detector in self.interChip: 

304 outDict['interChip'][detector] = self.interChip[detector].reshape(ctLength).tolist() 

305 

306 return outDict 

307 

308 @classmethod 

309 def fromTable(cls, tableList): 

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

311 

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

313 calibration, after constructing an appropriate dictionary from 

314 the input tables. 

315 

316 Parameters 

317 ---------- 

318 tableList : `list` [`lsst.afw.table.Table`] 

319 List of tables to use to construct the crosstalk 

320 calibration. 

321 

322 Returns 

323 ------- 

324 calib : `lsst.ip.isr.CrosstalkCalib` 

325 The calibration defined in the tables. 

326 

327 """ 

328 coeffTable = tableList[0] 

329 

330 metadata = coeffTable.meta 

331 inDict = dict() 

332 inDict['metadata'] = metadata 

333 inDict['hasCrosstalk'] = metadata['HAS_CROSSTALK'] 

334 inDict['nAmp'] = metadata['NAMP'] 

335 

336 inDict['coeffs'] = coeffTable['CT_COEFFS'] 

337 if 'CT_ERRORS' in coeffTable: 

338 inDict['coeffErr'] = coeffTable['CT_ERRORS'] 

339 if 'CT_COUNTS' in coeffTable: 

340 inDict['coeffNum'] = coeffTable['CT_COUNTS'] 

341 if 'CT_VALID' in coeffTable: 

342 inDict['coeffValid'] = coeffTable['CT_VALID'] 

343 

344 if len(tableList) > 1: 

345 inDict['interChip'] = dict() 

346 interChipTable = tableList[1] 

347 for record in interChipTable: 

348 inDict['interChip'][record['IC_SOURCE_DET']] = record['IC_COEFFS'] 

349 

350 return cls().fromDict(inDict) 

351 

352 def toTable(self): 

353 """Construct a list of tables containing the information in this calibration. 

354 

355 The list of tables should create an identical calibration 

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

357 

358 Returns 

359 ------- 

360 tableList : `list` [`lsst.afw.table.Table`] 

361 List of tables containing the crosstalk calibration 

362 information. 

363 

364 """ 

365 tableList = [] 

366 self.updateMetadata() 

367 catalog = Table([{'CT_COEFFS': self.coeffs.reshape(self.nAmp*self.nAmp), 

368 'CT_ERRORS': self.coeffErr.reshape(self.nAmp*self.nAmp), 

369 'CT_COUNTS': self.coeffNum.reshape(self.nAmp*self.nAmp), 

370 'CT_VALID': self.coeffValid.reshape(self.nAmp*self.nAmp), 

371 }]) 

372 # filter None, because astropy can't deal. 

373 inMeta = self.getMetadata().toDict() 

374 outMeta = {k: v for k, v in inMeta.items() if v is not None} 

375 outMeta.update({k: "" for k, v in inMeta.items() if v is None}) 

376 catalog.meta = outMeta 

377 tableList.append(catalog) 

378 

379 if self.interChip: 

380 interChipTable = Table([{'IC_SOURCE_DET': sourceDet, 

381 'IC_COEFFS': self.interChip[sourceDet].reshape(self.nAmp*self.nAmp)} 

382 for sourceDet in self.interChip.keys()]) 

383 tableList.append(interChipTable) 

384 return tableList 

385 

386 # Implementation methods. 

387 @staticmethod 

388 def extractAmp(image, amp, ampTarget, isTrimmed=False): 

389 """Extract the image data from an amp, flipped to match ampTarget. 

390 

391 Parameters 

392 ---------- 

393 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 

394 Image containing the amplifier of interest. 

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

396 Amplifier on image to extract. 

397 ampTarget : `lsst.afw.cameraGeom.Amplifier` 

398 Target amplifier that the extracted image will be flipped 

399 to match. 

400 isTrimmed : `bool` 

401 The image is already trimmed. 

402 TODO : DM-15409 will resolve this. 

403 

404 Returns 

405 ------- 

406 output : `lsst.afw.image.Image` 

407 Image of the amplifier in the desired configuration. 

408 """ 

409 X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False, 

410 lsst.afw.cameraGeom.ReadoutCorner.LR: True, 

411 lsst.afw.cameraGeom.ReadoutCorner.UL: False, 

412 lsst.afw.cameraGeom.ReadoutCorner.UR: True} 

413 Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False, 

414 lsst.afw.cameraGeom.ReadoutCorner.LR: False, 

415 lsst.afw.cameraGeom.ReadoutCorner.UL: True, 

416 lsst.afw.cameraGeom.ReadoutCorner.UR: True} 

417 

418 output = image[amp.getBBox() if isTrimmed else amp.getRawDataBBox()] 

419 thisAmpCorner = amp.getReadoutCorner() 

420 targetAmpCorner = ampTarget.getReadoutCorner() 

421 

422 # Flipping is necessary only if the desired configuration doesn't match what we currently have 

423 xFlip = X_FLIP[targetAmpCorner] ^ X_FLIP[thisAmpCorner] 

424 yFlip = Y_FLIP[targetAmpCorner] ^ Y_FLIP[thisAmpCorner] 

425 return lsst.afw.math.flipImage(output, xFlip, yFlip) 

426 

427 @staticmethod 

428 def calculateBackground(mi, badPixels=["BAD"]): 

429 """Estimate median background in image. 

430 

431 Getting a great background model isn't important for crosstalk correction, 

432 since the crosstalk is at a low level. The median should be sufficient. 

433 

434 Parameters 

435 ---------- 

436 mi : `lsst.afw.image.MaskedImage` 

437 MaskedImage for which to measure background. 

438 badPixels : `list` of `str` 

439 Mask planes to ignore. 

440 Returns 

441 ------- 

442 bg : `float` 

443 Median background level. 

444 """ 

445 mask = mi.getMask() 

446 stats = lsst.afw.math.StatisticsControl() 

447 stats.setAndMask(mask.getPlaneBitMask(badPixels)) 

448 return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue() 

449 

450 def subtractCrosstalk(self, thisExposure, sourceExposure=None, crosstalkCoeffs=None, 

451 badPixels=["BAD"], minPixelToMask=45000, 

452 crosstalkStr="CROSSTALK", isTrimmed=False, 

453 backgroundMethod="None"): 

454 """Subtract the crosstalk from thisExposure, optionally using a different source. 

455 

456 We set the mask plane indicated by ``crosstalkStr`` in a target amplifier 

457 for pixels in a source amplifier that exceed ``minPixelToMask``. Note that 

458 the correction is applied to all pixels in the amplifier, but only those 

459 that have a substantial crosstalk are masked with ``crosstalkStr``. 

460 

461 The uncorrected image is used as a template for correction. This is good 

462 enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's 

463 larger you may want to iterate. 

464 

465 Parameters 

466 ---------- 

467 thisExposure : `lsst.afw.image.Exposure` 

468 Exposure for which to subtract crosstalk. 

469 sourceExposure : `lsst.afw.image.Exposure`, optional 

470 Exposure to use as the source of the crosstalk. If not set, 

471 thisExposure is used as the source (intra-detector crosstalk). 

472 crosstalkCoeffs : `numpy.ndarray`, optional. 

473 Coefficients to use to correct crosstalk. 

474 badPixels : `list` of `str` 

475 Mask planes to ignore. 

476 minPixelToMask : `float` 

477 Minimum pixel value (relative to the background level) in 

478 source amplifier for which to set ``crosstalkStr`` mask plane 

479 in target amplifier. 

480 crosstalkStr : `str` 

481 Mask plane name for pixels greatly modified by crosstalk 

482 (above minPixelToMask). 

483 isTrimmed : `bool` 

484 The image is already trimmed. 

485 This should no longer be needed once DM-15409 is resolved. 

486 backgroundMethod : `str` 

487 Method used to subtract the background. "AMP" uses 

488 amplifier-by-amplifier background levels, "DETECTOR" uses full 

489 exposure/maskedImage levels. Any other value results in no 

490 background subtraction. 

491 """ 

492 mi = thisExposure.getMaskedImage() 

493 mask = mi.getMask() 

494 detector = thisExposure.getDetector() 

495 if self.hasCrosstalk is False: 

496 self.fromDetector(detector, coeffVector=crosstalkCoeffs) 

497 

498 numAmps = len(detector) 

499 if numAmps != self.nAmp: 

500 raise RuntimeError(f"Crosstalk built for {self.nAmp} in {self._detectorName}, received " 

501 f"{numAmps} in {detector.getName()}") 

502 

503 if sourceExposure: 

504 source = sourceExposure.getMaskedImage() 

505 sourceDetector = sourceExposure.getDetector() 

506 else: 

507 source = mi 

508 sourceDetector = detector 

509 

510 if crosstalkCoeffs is not None: 

511 coeffs = crosstalkCoeffs 

512 else: 

513 coeffs = self.coeffs 

514 self.log.debug("CT COEFF: %s", coeffs) 

515 # Set background level based on the requested method. The 

516 # thresholdBackground holds the offset needed so that we only mask 

517 # pixels high relative to the background, not in an absolute 

518 # sense. 

519 thresholdBackground = self.calculateBackground(source, badPixels) 

520 

521 backgrounds = [0.0 for amp in sourceDetector] 

522 if backgroundMethod is None: 

523 pass 

524 elif backgroundMethod == "AMP": 

525 backgrounds = [self.calculateBackground(source[amp.getBBox()], badPixels) 

526 for amp in sourceDetector] 

527 elif backgroundMethod == "DETECTOR": 

528 backgrounds = [self.calculateBackground(source, badPixels) for amp in sourceDetector] 

529 

530 # Set the crosstalkStr bit for the bright pixels (those which will have 

531 # significant crosstalk correction) 

532 crosstalkPlane = mask.addMaskPlane(crosstalkStr) 

533 footprints = lsst.afw.detection.FootprintSet(source, 

534 lsst.afw.detection.Threshold(minPixelToMask 

535 + thresholdBackground)) 

536 footprints.setMask(mask, crosstalkStr) 

537 crosstalk = mask.getPlaneBitMask(crosstalkStr) 

538 

539 # Define a subtrahend image to contain all the scaled crosstalk signals 

540 subtrahend = source.Factory(source.getBBox()) 

541 subtrahend.set((0, 0, 0)) 

542 

543 coeffs = coeffs.transpose() 

544 for ii, iAmp in enumerate(sourceDetector): 

545 iImage = subtrahend[iAmp.getBBox() if isTrimmed else iAmp.getRawDataBBox()] 

546 for jj, jAmp in enumerate(detector): 

547 if coeffs[ii, jj] == 0.0: 

548 continue 

549 jImage = self.extractAmp(mi, jAmp, iAmp, isTrimmed) 

550 jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks 

551 jImage -= backgrounds[jj] 

552 iImage.scaledPlus(coeffs[ii, jj], jImage) 

553 

554 # Set crosstalkStr bit only for those pixels that have been significantly modified (i.e., those 

555 # masked as such in 'subtrahend'), not necessarily those that are bright originally. 

556 mask.clearMaskPlane(crosstalkPlane) 

557 mi -= subtrahend # also sets crosstalkStr bit for bright pixels 

558 

559 

560class CrosstalkConfig(Config): 

561 """Configuration for intra-detector crosstalk removal.""" 

562 minPixelToMask = Field( 

563 dtype=float, 

564 doc="Set crosstalk mask plane for pixels over this value.", 

565 default=45000 

566 ) 

567 crosstalkMaskPlane = Field( 

568 dtype=str, 

569 doc="Name for crosstalk mask plane.", 

570 default="CROSSTALK" 

571 ) 

572 crosstalkBackgroundMethod = ChoiceField( 

573 dtype=str, 

574 doc="Type of background subtraction to use when applying correction.", 

575 default="None", 

576 allowed={ 

577 "None": "Do no background subtraction.", 

578 "AMP": "Subtract amplifier-by-amplifier background levels.", 

579 "DETECTOR": "Subtract detector level background." 

580 }, 

581 ) 

582 useConfigCoefficients = Field( 

583 dtype=bool, 

584 doc="Ignore the detector crosstalk information in favor of CrosstalkConfig values?", 

585 default=False, 

586 ) 

587 crosstalkValues = ListField( 

588 dtype=float, 

589 doc=("Amplifier-indexed crosstalk coefficients to use. This should be arranged as a 1 x nAmp**2 " 

590 "list of coefficients, such that when reshaped by crosstalkShape, the result is nAmp x nAmp. " 

591 "This matrix should be structured so CT * [amp0 amp1 amp2 ...]^T returns the column " 

592 "vector [corr0 corr1 corr2 ...]^T."), 

593 default=[0.0], 

594 ) 

595 crosstalkShape = ListField( 

596 dtype=int, 

597 doc="Shape of the coefficient array. This should be equal to [nAmp, nAmp].", 

598 default=[1], 

599 ) 

600 

601 def getCrosstalk(self, detector=None): 

602 """Return a 2-D numpy array of crosstalk coefficients in the proper shape. 

603 

604 Parameters 

605 ---------- 

606 detector : `lsst.afw.cameraGeom.detector` 

607 Detector that is to be crosstalk corrected. 

608 

609 Returns 

610 ------- 

611 coeffs : `numpy.ndarray` 

612 Crosstalk coefficients that can be used to correct the detector. 

613 

614 Raises 

615 ------ 

616 RuntimeError 

617 Raised if no coefficients could be generated from this detector/configuration. 

618 """ 

619 if self.useConfigCoefficients is True: 

620 coeffs = np.array(self.crosstalkValues).reshape(self.crosstalkShape) 

621 if detector is not None: 

622 nAmp = len(detector) 

623 if coeffs.shape != (nAmp, nAmp): 

624 raise RuntimeError("Constructed crosstalk coeffients do not match detector shape. " 

625 f"{coeffs.shape} {nAmp}") 

626 return coeffs 

627 elif detector is not None and detector.hasCrosstalk() is True: 

628 # Assume the detector defines itself consistently. 

629 return detector.getCrosstalk() 

630 else: 

631 raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients") 

632 

633 def hasCrosstalk(self, detector=None): 

634 """Return a boolean indicating if crosstalk coefficients exist. 

635 

636 Parameters 

637 ---------- 

638 detector : `lsst.afw.cameraGeom.detector` 

639 Detector that is to be crosstalk corrected. 

640 

641 Returns 

642 ------- 

643 hasCrosstalk : `bool` 

644 True if this detector/configuration has crosstalk coefficients defined. 

645 """ 

646 if self.useConfigCoefficients is True and self.crosstalkValues is not None: 

647 return True 

648 elif detector is not None and detector.hasCrosstalk() is True: 

649 return True 

650 else: 

651 return False 

652 

653 

654class CrosstalkTask(Task): 

655 """Apply intra-detector crosstalk correction.""" 

656 ConfigClass = CrosstalkConfig 

657 _DefaultName = 'isrCrosstalk' 

658 

659 def prepCrosstalk(self, dataRef, crosstalk=None): 

660 """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk. 

661 

662 Parameters 

663 ---------- 

664 dataRef : `daf.persistence.butlerSubset.ButlerDataRef` 

665 Butler reference of the detector data to be processed. 

666 crosstalk : `~lsst.ip.isr.CrosstalkConfig` 

667 Crosstalk calibration that will be used. 

668 

669 See also 

670 -------- 

671 lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk 

672 """ 

673 return 

674 

675 def run(self, exposure, crosstalk=None, 

676 crosstalkSources=None, isTrimmed=False, camera=None): 

677 """Apply intra-detector crosstalk correction 

678 

679 Parameters 

680 ---------- 

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

682 Exposure for which to remove crosstalk. 

683 crosstalkCalib : `lsst.ip.isr.CrosstalkCalib`, optional 

684 External crosstalk calibration to apply. Constructed from 

685 detector if not found. 

686 crosstalkSources : `defaultdict`, optional 

687 Image data for other detectors that are sources of 

688 crosstalk in exposure. The keys are expected to be names 

689 of the other detectors, with the values containing 

690 `lsst.afw.image.Exposure` at the same level of processing 

691 as ``exposure``. 

692 The default for intra-detector crosstalk here is None. 

693 isTrimmed : `bool`, optional 

694 The image is already trimmed. 

695 This should no longer be needed once DM-15409 is resolved. 

696 camera : `lsst.afw.cameraGeom.Camera`, optional 

697 Camera associated with this exposure. Only used for 

698 inter-chip matching. 

699 

700 Raises 

701 ------ 

702 RuntimeError 

703 Raised if called for a detector that does not have a 

704 crosstalk correction. Also raised if the crosstalkSource 

705 is not an expected type. 

706 """ 

707 if not crosstalk: 

708 crosstalk = CrosstalkCalib(log=self.log) 

709 crosstalk = crosstalk.fromDetector(exposure.getDetector(), 

710 coeffVector=self.config.crosstalkValues) 

711 if not crosstalk.log: 

712 crosstalk.log = self.log 

713 if not crosstalk.hasCrosstalk: 

714 raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients.") 

715 

716 else: 

717 self.log.info("Applying crosstalk correction.") 

718 crosstalk.subtractCrosstalk(exposure, crosstalkCoeffs=crosstalk.coeffs, 

719 minPixelToMask=self.config.minPixelToMask, 

720 crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed, 

721 backgroundMethod=self.config.crosstalkBackgroundMethod) 

722 

723 if crosstalk.interChip: 

724 if crosstalkSources: 

725 # Parse crosstalkSources: Identify which detectors we have available 

726 if isinstance(crosstalkSources[0], lsst.afw.image.Exposure): 

727 # Received afwImage.Exposure 

728 sourceNames = [exp.getDetector().getName() for exp in crosstalkSources] 

729 elif isinstance(crosstalkSources[0], lsst.daf.butler.DeferredDatasetHandle): 

730 # Received dafButler.DeferredDatasetHandle 

731 detectorList = [source.dataId['detector'] for source in crosstalkSources] 

732 sourceNames = [camera[detector].getName() for detector in detectorList] 

733 else: 

734 raise RuntimeError("Unknown object passed as crosstalk sources.", 

735 type(crosstalkSources[0])) 

736 

737 for detName in crosstalk.interChip: 

738 if detName not in sourceNames: 

739 self.log.warn("Crosstalk lists %s, not found in sources: %s", 

740 detName, sourceNames) 

741 continue 

742 # Get the coefficients. 

743 interChipCoeffs = crosstalk.interChip[detName] 

744 

745 sourceExposure = crosstalkSources[sourceNames.index(detName)] 

746 if isinstance(sourceExposure, lsst.daf.butler.DeferredDatasetHandle): 

747 # Dereference the dafButler.DeferredDatasetHandle. 

748 sourceExposure = sourceExposure.get() 

749 if not isinstance(sourceExposure, lsst.afw.image.Exposure): 

750 raise RuntimeError("Unknown object passed as crosstalk sources.", 

751 type(sourceExposure)) 

752 

753 self.log.info("Correcting detector %s with ctSource %s", 

754 exposure.getDetector().getName(), 

755 sourceExposure.getDetector().getName()) 

756 crosstalk.subtractCrosstalk(exposure, sourceExposure=sourceExposure, 

757 crosstalkCoeffs=interChipCoeffs, 

758 minPixelToMask=self.config.minPixelToMask, 

759 crosstalkStr=self.config.crosstalkMaskPlane, 

760 isTrimmed=isTrimmed, 

761 backgroundMethod=self.config.crosstalkBackgroundMethod) 

762 else: 

763 self.log.warn("Crosstalk contains interChip coefficients, but no sources found!") 

764 

765 

766class NullCrosstalkTask(CrosstalkTask): 

767 def run(self, exposure, crosstalkSources=None): 

768 self.log.info("Not performing any crosstalk correction")