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 

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

31from lsst.pipe.base import Task 

32 

33from lsst.ip.isr import IsrCalib 

34 

35 

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

37 "NullCrosstalkTask"] 

38 

39 

40class CrosstalkCalib(IsrCalib): 

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

42 

43 Parameters 

44 ---------- 

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

46 Detector to use to pull coefficients from. 

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

48 Log to write messages to. 

49 **kwargs : 

50 Parameters to pass to parent constructor. 

51 """ 

52 _OBSTYPE = 'CROSSTALK' 

53 _SCHEMA = 'Gen3 Crosstalk' 

54 _VERSION = 1.0 

55 

56 def __init__(self, detector=None, **kwargs): 

57 self.hasCrosstalk = False 

58 self.nAmp = 0 

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

60 

61 self.coeffs = None 

62 self.coeffErr = None 

63 self.coeffNum = None 

64 self.interChip = None 

65 

66 super().__init__(**kwargs) 

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

68 'coeffErr', 'coeffNum', 'interChip']) 

69 if detector: 

70 self.fromDetector(detector) 

71 

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

73 """Update calibration metadata. 

74 

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

76 calibration keywords will be saved. 

77 

78 Parameters 

79 ---------- 

80 setDate : `bool`, optional 

81 Update the CALIBDATE fields in the metadata to the current 

82 time. Defaults to False. 

83 kwargs : 

84 Other keyword parameters to set in the metadata. 

85 """ 

86 kwargs['DETECTOR'] = self._detectorName 

87 kwargs['DETECTOR_SERIAL'] = self._detectorSerial 

88 kwargs['HAS_CROSSTALK'] = self.hasCrosstalk 

89 kwargs['NAMP'] = self.nAmp 

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

91 kwargs['CROSSTALK_SHAPE'] = self.crosstalkShape 

92 

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

94 

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

96 """Set calibration parameters from the detector. 

97 

98 Parameters 

99 ---------- 

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

101 Detector to use to set parameters from. 

102 coeffVector : `numpy.array`, optional 

103 Use the detector geometry (bounding boxes and flip 

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

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

106 

107 Returns 

108 ------- 

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

110 The calibration constructed from the detector. 

111 

112 """ 

113 if detector.hasCrosstalk() or coeffVector: 

114 self._detectorName = detector.getName() 

115 self._detectorSerial = detector.getSerial() 

116 

117 self.nAmp = len(detector) 

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

119 

120 if coeffVector is not None: 

121 crosstalkCoeffs = coeffVector 

122 else: 

123 crosstalkCoeffs = detector.getCrosstalk() 

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

125 return self 

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

127 

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

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

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

131 

132 self.interChip = {} 

133 self.hasCrosstalk = True 

134 self.updateMetadata() 

135 return self 

136 

137 @classmethod 

138 def fromDict(cls, dictionary): 

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

140 

141 Must be implemented by the specific calibration subclasses. 

142 

143 Parameters 

144 ---------- 

145 dictionary : `dict` 

146 Dictionary of properties. 

147 

148 Returns 

149 ------- 

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

151 Constructed calibration. 

152 

153 Raises 

154 ------ 

155 RuntimeError : 

156 Raised if the supplied dictionary is for a different 

157 calibration. 

158 """ 

159 calib = cls() 

160 

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

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

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

164 

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

166 calib._detectorName = dictionary['metadata']['DETECTOR'] 

167 calib._detectorSerial = dictionary['metadata']['DETECTOR_SERIAL'] 

168 

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

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

171 if calib.hasCrosstalk: 

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

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

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

175 

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

177 if calib.interChip: 

178 for detector in calib.interChip: 

179 coeffVector = calib.interChip[detector] 

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

181 

182 calib.updateMetadata() 

183 return calib 

184 

185 def toDict(self): 

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

187 

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

189 `fromDict`. 

190 

191 Returns 

192 ------- 

193 dictionary : `dict` 

194 Dictionary of properties. 

195 """ 

196 self.updateMetadata() 

197 

198 outDict = {} 

199 metadata = self.getMetadata() 

200 outDict['metadata'] = metadata 

201 

202 outDict['hasCrosstalk'] = self.hasCrosstalk 

203 outDict['nAmp'] = self.nAmp 

204 outDict['crosstalkShape'] = self.crosstalkShape 

205 

206 ctLength = self.nAmp*self.nAmp 

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

208 

209 if self.coeffErr is not None: 

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

211 if self.coeffNum is not None: 

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

213 

214 if self.interChip: 

215 outDict['interChip'] = dict() 

216 for detector in self.interChip: 

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

218 

219 return outDict 

220 

221 @classmethod 

222 def fromTable(cls, tableList): 

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

224 

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

226 calibration, after constructing an appropriate dictionary from 

227 the input tables. 

228 

229 Parameters 

230 ---------- 

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

232 List of tables to use to construct the crosstalk 

233 calibration. 

234 

235 Returns 

236 ------- 

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

238 The calibration defined in the tables. 

239 

240 """ 

241 coeffTable = tableList[0] 

242 

243 metadata = coeffTable.meta 

244 inDict = dict() 

245 inDict['metadata'] = metadata 

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

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

248 

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

250 if len(tableList) > 1: 

251 inDict['interChip'] = dict() 

252 interChipTable = tableList[1] 

253 for record in interChipTable: 

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

255 

256 return cls().fromDict(inDict) 

257 

258 def toTable(self): 

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

260 

261 The list of tables should create an identical calibration 

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

263 

264 Returns 

265 ------- 

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

267 List of tables containing the crosstalk calibration 

268 information. 

269 

270 """ 

271 tableList = [] 

272 self.updateMetadata() 

273 catalog = Table([{'CT_COEFFS': self.coeffs.reshape(self.nAmp*self.nAmp)}]) 

274 catalog.meta = self.getMetadata().toDict() 

275 tableList.append(catalog) 

276 

277 if self.interChip: 

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

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

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

281 tableList.append(interChipTable) 

282 return tableList 

283 

284 # Implementation methods. 

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

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

287 

288 Parameters 

289 ---------- 

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

291 Image containing the amplifier of interest. 

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

293 Amplifier on image to extract. 

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

295 Target amplifier that the extracted image will be flipped 

296 to match. 

297 isTrimmed : `bool` 

298 The image is already trimmed. 

299 TODO : DM-15409 will resolve this. 

300 

301 Returns 

302 ------- 

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

304 Image of the amplifier in the desired configuration. 

305 """ 

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

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

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

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

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

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

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

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

314 

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

316 thisAmpCorner = amp.getReadoutCorner() 

317 targetAmpCorner = ampTarget.getReadoutCorner() 

318 

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

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

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

322 self.log.debug("Extract amp: %s %s %s %s", 

323 amp.getName(), ampTarget.getName(), thisAmpCorner, targetAmpCorner) 

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

325 

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

327 """Estimate median background in image. 

328 

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

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

331 

332 Parameters 

333 ---------- 

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

335 MaskedImage for which to measure background. 

336 badPixels : `list` of `str` 

337 Mask planes to ignore. 

338 Returns 

339 ------- 

340 bg : `float` 

341 Median background level. 

342 """ 

343 mask = mi.getMask() 

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

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

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

347 

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

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

350 crosstalkStr="CROSSTALK", isTrimmed=False, 

351 backgroundMethod="None"): 

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

353 

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

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

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

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

358 

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

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

361 larger you may want to iterate. 

362 

363 Parameters 

364 ---------- 

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

366 Exposure for which to subtract crosstalk. 

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

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

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

370 crosstalkCoeffs : `numpy.ndarray`, optional. 

371 Coefficients to use to correct crosstalk. 

372 badPixels : `list` of `str` 

373 Mask planes to ignore. 

374 minPixelToMask : `float` 

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

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

377 in target amplifier. 

378 crosstalkStr : `str` 

379 Mask plane name for pixels greatly modified by crosstalk 

380 (above minPixelToMask). 

381 isTrimmed : `bool` 

382 The image is already trimmed. 

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

384 backgroundMethod : `str` 

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

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

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

388 background subtraction. 

389 """ 

390 mi = thisExposure.getMaskedImage() 

391 mask = mi.getMask() 

392 detector = thisExposure.getDetector() 

393 if self.hasCrosstalk is False: 

394 self.fromDetector(detector, coeffVector=crosstalkCoeffs) 

395 

396 numAmps = len(detector) 

397 if numAmps != self.nAmp: 

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

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

400 

401 if sourceExposure: 

402 source = sourceExposure.getMaskedImage() 

403 sourceDetector = sourceExposure.getDetector() 

404 else: 

405 source = mi 

406 sourceDetector = detector 

407 

408 if crosstalkCoeffs is not None: 

409 coeffs = crosstalkCoeffs 

410 else: 

411 coeffs = self.coeffs 

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

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

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

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

416 # sense. 

417 thresholdBackground = self.calculateBackground(source, badPixels) 

418 

419 backgrounds = [0.0 for amp in sourceDetector] 

420 if backgroundMethod is None: 

421 pass 

422 elif backgroundMethod == "AMP": 

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

424 for amp in sourceDetector] 

425 elif backgroundMethod == "DETECTOR": 

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

427 

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

429 # significant crosstalk correction) 

430 crosstalkPlane = mask.addMaskPlane(crosstalkStr) 

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

432 lsst.afw.detection.Threshold(minPixelToMask 

433 + thresholdBackground)) 

434 footprints.setMask(mask, crosstalkStr) 

435 crosstalk = mask.getPlaneBitMask(crosstalkStr) 

436 

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

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

439 subtrahend.set((0, 0, 0)) 

440 

441 coeffs = coeffs.transpose() 

442 for ii, iAmp in enumerate(sourceDetector): 

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

444 for jj, jAmp in enumerate(detector): 

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

446 continue 

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

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

449 jImage -= backgrounds[jj] 

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

451 

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

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

454 mask.clearMaskPlane(crosstalkPlane) 

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

456 

457 

458class CrosstalkConfig(Config): 

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

460 minPixelToMask = Field( 

461 dtype=float, 

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

463 default=45000 

464 ) 

465 crosstalkMaskPlane = Field( 

466 dtype=str, 

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

468 default="CROSSTALK" 

469 ) 

470 crosstalkBackgroundMethod = ChoiceField( 

471 dtype=str, 

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

473 default="None", 

474 allowed={ 

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

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

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

478 }, 

479 ) 

480 useConfigCoefficients = Field( 

481 dtype=bool, 

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

483 default=False, 

484 ) 

485 crosstalkValues = ListField( 

486 dtype=float, 

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

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

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

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

491 default=[0.0], 

492 ) 

493 crosstalkShape = ListField( 

494 dtype=int, 

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

496 default=[1], 

497 ) 

498 

499 def getCrosstalk(self, detector=None): 

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

501 

502 Parameters 

503 ---------- 

504 detector : `lsst.afw.cameraGeom.detector` 

505 Detector that is to be crosstalk corrected. 

506 

507 Returns 

508 ------- 

509 coeffs : `numpy.ndarray` 

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

511 

512 Raises 

513 ------ 

514 RuntimeError 

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

516 """ 

517 if self.useConfigCoefficients is True: 

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

519 if detector is not None: 

520 nAmp = len(detector) 

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

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

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

524 return coeffs 

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

526 # Assume the detector defines itself consistently. 

527 return detector.getCrosstalk() 

528 else: 

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

530 

531 def hasCrosstalk(self, detector=None): 

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

533 

534 Parameters 

535 ---------- 

536 detector : `lsst.afw.cameraGeom.detector` 

537 Detector that is to be crosstalk corrected. 

538 

539 Returns 

540 ------- 

541 hasCrosstalk : `bool` 

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

543 """ 

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

545 return True 

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

547 return True 

548 else: 

549 return False 

550 

551 

552class CrosstalkTask(Task): 

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

554 ConfigClass = CrosstalkConfig 

555 _DefaultName = 'isrCrosstalk' 

556 

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

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

559 

560 Parameters 

561 ---------- 

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

563 Butler reference of the detector data to be processed. 

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

565 Crosstalk calibration that will be used. 

566 

567 See also 

568 -------- 

569 lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk 

570 """ 

571 return 

572 

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

574 crosstalkSources=None, isTrimmed=False): 

575 """Apply intra-detector crosstalk correction 

576 

577 Parameters 

578 ---------- 

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

580 Exposure for which to remove crosstalk. 

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

582 External crosstalk calibration to apply. Constructed from 

583 detector if not found. 

584 crosstalkSources : `defaultdict`, optional 

585 Image data for other detectors that are sources of 

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

587 of the other detectors, with the values containing 

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

589 as ``exposure``. 

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

591 isTrimmed : `bool` 

592 The image is already trimmed. 

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

594 

595 Raises 

596 ------ 

597 RuntimeError 

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

599 crosstalk correction. 

600 """ 

601 if not crosstalk: 

602 crosstalk = CrosstalkCalib(log=self.log) 

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

604 coeffVector=self.config.crosstalkValues) 

605 if not crosstalk.log: 

606 crosstalk.log = self.log 

607 if not crosstalk.hasCrosstalk: 

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

609 

610 else: 

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

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

613 minPixelToMask=self.config.minPixelToMask, 

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

615 backgroundMethod=self.config.crosstalkBackgroundMethod) 

616 

617 if crosstalk.interChip: 

618 if crosstalkSources: 

619 for detName in crosstalk.interChip: 

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

621 # Received afwImage.Exposure 

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

623 else: 

624 # Received dafButler.DeferredDatasetHandle 

625 sourceNames = [expRef.get(datasetType='isrOscanCorr').getDetector().getName() 

626 for expRef in crosstalkSources] 

627 if detName not in sourceNames: 

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

629 detName, sourceNames) 

630 continue 

631 interChipCoeffs = crosstalk.interChip[detName] 

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

633 crosstalk.subtractCrosstalk(exposure, sourceExposure=sourceExposure, 

634 crosstalkCoeffs=interChipCoeffs, 

635 minPixelToMask=self.config.minPixelToMask, 

636 crosstalkStr=self.config.crosstalkMaskPlane, 

637 isTrimmed=isTrimmed, 

638 backgroundMethod=self.config.crosstalkBackgroundMethod) 

639 else: 

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

641 

642 

643class NullCrosstalkTask(CrosstalkTask): 

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

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