Coverage for python/lsst/cp/pipe/measureCrosstalk.py: 14%

278 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-05-31 08:01 +0000

1# This file is part of cp_pipe 

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 <http://www.gnu.org/licenses/>. 

21import itertools 

22import numpy as np 

23 

24from collections import defaultdict 

25 

26import lsst.pipe.base as pipeBase 

27import lsst.pipe.base.connectionTypes as cT 

28 

29from lsstDebug import getDebugFrame 

30from lsst.afw.detection import FootprintSet, Threshold 

31from lsst.afw.display import getDisplay 

32from lsst.pex.config import Field, ListField 

33from lsst.ip.isr import CrosstalkCalib, IsrProvenance 

34from lsst.cp.pipe.utils import (ddict2dict, sigmaClipCorrection) 

35 

36from ._lookupStaticCalibration import lookupStaticCalibration 

37 

38__all__ = ["CrosstalkExtractConfig", "CrosstalkExtractTask", 

39 "CrosstalkSolveTask", "CrosstalkSolveConfig"] 

40 

41 

42class CrosstalkExtractConnections(pipeBase.PipelineTaskConnections, 

43 dimensions=("instrument", "exposure", "detector")): 

44 inputExp = cT.Input( 

45 name="crosstalkInputs", 

46 doc="Input post-ISR processed exposure to measure crosstalk from.", 

47 storageClass="Exposure", 

48 dimensions=("instrument", "exposure", "detector"), 

49 multiple=False, 

50 ) 

51 # TODO: Depends on DM-21904. 

52 sourceExp = cT.Input( 

53 name="crosstalkSource", 

54 doc="Post-ISR exposure to measure for inter-chip crosstalk onto inputExp.", 

55 storageClass="Exposure", 

56 dimensions=("instrument", "exposure", "detector"), 

57 multiple=True, 

58 deferLoad=True, 

59 # lookupFunction=None, 

60 ) 

61 

62 outputRatios = cT.Output( 

63 name="crosstalkRatios", 

64 doc="Extracted crosstalk pixel ratios.", 

65 storageClass="StructuredDataDict", 

66 dimensions=("instrument", "exposure", "detector"), 

67 ) 

68 outputFluxes = cT.Output( 

69 name="crosstalkFluxes", 

70 doc="Source pixel fluxes used in ratios.", 

71 storageClass="StructuredDataDict", 

72 dimensions=("instrument", "exposure", "detector"), 

73 ) 

74 

75 def __init__(self, *, config=None): 

76 super().__init__(config=config) 

77 # Discard sourceExp until DM-21904 allows full interchip 

78 # measurements. 

79 self.inputs.discard("sourceExp") 

80 

81 

82class CrosstalkExtractConfig(pipeBase.PipelineTaskConfig, 

83 pipelineConnections=CrosstalkExtractConnections): 

84 """Configuration for the measurement of pixel ratios. 

85 """ 

86 

87 doMeasureInterchip = Field( 

88 dtype=bool, 

89 default=False, 

90 doc="Measure inter-chip crosstalk as well?", 

91 ) 

92 threshold = Field( 

93 dtype=float, 

94 default=30000, 

95 doc="Minimum level of source pixels for which to measure crosstalk." 

96 ) 

97 ignoreSaturatedPixels = Field( 

98 dtype=bool, 

99 default=False, 

100 doc="Should saturated pixels be ignored?" 

101 ) 

102 badMask = ListField( 

103 dtype=str, 

104 default=["BAD", "INTRP"], 

105 doc="Mask planes to ignore when identifying source pixels." 

106 ) 

107 isTrimmed = Field( 

108 dtype=bool, 

109 default=True, 

110 doc="Is the input exposure trimmed?" 

111 ) 

112 

113 def validate(self): 

114 super().validate() 

115 

116 # Ensure the handling of the SAT mask plane is consistent 

117 # with the ignoreSaturatedPixels value. 

118 if self.ignoreSaturatedPixels: 

119 if 'SAT' not in self.badMask: 

120 self.badMask.append('SAT') 

121 else: 

122 if 'SAT' in self.badMask: 

123 self.badMask = [mask for mask in self.badMask if mask != 'SAT'] 

124 

125 

126class CrosstalkExtractTask(pipeBase.PipelineTask): 

127 """Task to measure pixel ratios to find crosstalk. 

128 """ 

129 

130 ConfigClass = CrosstalkExtractConfig 

131 _DefaultName = 'cpCrosstalkExtract' 

132 

133 def run(self, inputExp, sourceExps=[]): 

134 """Measure pixel ratios between amplifiers in inputExp. 

135 

136 Extract crosstalk ratios between different amplifiers. 

137 

138 For pixels above ``config.threshold``, we calculate the ratio 

139 between each background-subtracted target amp and the source 

140 amp. We return a list of ratios for each pixel for each 

141 target/source combination, as nested dictionary containing the 

142 ratio. 

143 

144 Parameters 

145 ---------- 

146 inputExp : `lsst.afw.image.Exposure` 

147 Input exposure to measure pixel ratios on. 

148 sourceExp : `list` [`lsst.afw.image.Exposure`], optional 

149 List of chips to use as sources to measure inter-chip 

150 crosstalk. 

151 

152 Returns 

153 ------- 

154 results : `lsst.pipe.base.Struct` 

155 The results struct containing: 

156 

157 ``outputRatios`` 

158 A catalog of ratio lists. The dictionaries are 

159 indexed such that: 

160 outputRatios[targetChip][sourceChip][targetAmp][sourceAmp] 

161 contains the ratio list for that combination (`dict` 

162 [`dict` [`dict` [`dict` [`list`]]]]). 

163 ``outputFluxes`` 

164 A catalog of flux lists. The dictionaries are 

165 indexed such that: 

166 outputFluxes[sourceChip][sourceAmp] contains the flux 

167 list used in the outputRatios (`dict` [`dict` 

168 [`list`]]). 

169 """ 

170 outputRatios = defaultdict(lambda: defaultdict(dict)) 

171 outputFluxes = defaultdict(lambda: defaultdict(dict)) 

172 

173 threshold = self.config.threshold 

174 badPixels = list(self.config.badMask) 

175 

176 targetDetector = inputExp.getDetector() 

177 targetChip = targetDetector.getName() 

178 

179 # Always look at the target chip first, then go to any other 

180 # supplied exposures. 

181 sourceExtractExps = [inputExp] 

182 sourceExtractExps.extend(sourceExps) 

183 

184 self.log.info("Measuring full detector background for target: %s", targetChip) 

185 targetIm = inputExp.getMaskedImage() 

186 FootprintSet(targetIm, Threshold(threshold), "DETECTED") 

187 detected = targetIm.getMask().getPlaneBitMask("DETECTED") 

188 bg = CrosstalkCalib.calculateBackground(targetIm, badPixels + ["DETECTED"]) 

189 

190 self.debugView('extract', inputExp) 

191 

192 for sourceExp in sourceExtractExps: 

193 sourceDetector = sourceExp.getDetector() 

194 sourceChip = sourceDetector.getName() 

195 sourceIm = sourceExp.getMaskedImage() 

196 bad = sourceIm.getMask().getPlaneBitMask(badPixels) 

197 self.log.info("Measuring crosstalk from source: %s", sourceChip) 

198 

199 if sourceExp != inputExp: 

200 FootprintSet(sourceIm, Threshold(threshold), "DETECTED") 

201 detected = sourceIm.getMask().getPlaneBitMask("DETECTED") 

202 

203 # The dictionary of amp-to-amp ratios for this pair of 

204 # source->target detectors. 

205 ratioDict = defaultdict(lambda: defaultdict(list)) 

206 extractedCount = 0 

207 

208 for sourceAmp in sourceDetector: 

209 sourceAmpName = sourceAmp.getName() 

210 sourceAmpBBox = sourceAmp.getBBox() if self.config.isTrimmed else sourceAmp.getRawDataBBox() 

211 sourceAmpImage = sourceIm[sourceAmpBBox] 

212 sourceMask = sourceAmpImage.mask.array 

213 select = ((sourceMask & detected > 0) 

214 & (sourceMask & bad == 0) 

215 & np.isfinite(sourceAmpImage.image.array)) 

216 count = np.sum(select) 

217 self.log.debug(" Source amplifier: %s", sourceAmpName) 

218 

219 outputFluxes[sourceChip][sourceAmpName] = sourceAmpImage.image.array[select].tolist() 

220 

221 for targetAmp in targetDetector: 

222 # iterate over targetExposure 

223 targetAmpName = targetAmp.getName() 

224 if sourceAmpName == targetAmpName and sourceChip == targetChip: 

225 ratioDict[targetAmpName][sourceAmpName] = [] 

226 continue 

227 self.log.debug(" Target amplifier: %s", targetAmpName) 

228 

229 targetAmpImage = CrosstalkCalib.extractAmp(targetIm.image, 

230 targetAmp, sourceAmp, 

231 isTrimmed=self.config.isTrimmed) 

232 ratios = (targetAmpImage.array[select] - bg)/sourceAmpImage.image.array[select] 

233 ratioDict[targetAmpName][sourceAmpName] = ratios.tolist() 

234 extractedCount += count 

235 

236 self.debugPixels('pixels', 

237 sourceAmpImage.image.array[select], 

238 targetAmpImage.array[select] - bg, 

239 sourceAmpName, targetAmpName) 

240 

241 self.log.info("Extracted %d pixels from %s -> %s (targetBG: %f)", 

242 extractedCount, sourceChip, targetChip, bg) 

243 outputRatios[targetChip][sourceChip] = ratioDict 

244 

245 return pipeBase.Struct( 

246 outputRatios=ddict2dict(outputRatios), 

247 outputFluxes=ddict2dict(outputFluxes) 

248 ) 

249 

250 def debugView(self, stepname, exposure): 

251 """Utility function to examine the image being processed. 

252 

253 Parameters 

254 ---------- 

255 stepname : `str` 

256 State of processing to view. 

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

258 Exposure to view. 

259 """ 

260 frame = getDebugFrame(self._display, stepname) 

261 if frame: 

262 display = getDisplay(frame) 

263 display.scale('asinh', 'zscale') 

264 display.mtv(exposure) 

265 

266 prompt = "Press Enter to continue: " 

267 while True: 

268 ans = input(prompt).lower() 

269 if ans in ("", "c",): 

270 break 

271 

272 def debugPixels(self, stepname, pixelsIn, pixelsOut, sourceName, targetName): 

273 """Utility function to examine the CT ratio pixel values. 

274 

275 Parameters 

276 ---------- 

277 stepname : `str` 

278 State of processing to view. 

279 pixelsIn : `np.ndarray`, (N,) 

280 Pixel values from the potential crosstalk source. 

281 pixelsOut : `np.ndarray`, (N,) 

282 Pixel values from the potential crosstalk target. 

283 sourceName : `str` 

284 Source amplifier name 

285 targetName : `str` 

286 Target amplifier name 

287 """ 

288 frame = getDebugFrame(self._display, stepname) 

289 if frame: 

290 import matplotlib.pyplot as plt 

291 figure = plt.figure(1) 

292 figure.clear() 

293 

294 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8)) 

295 axes.plot(pixelsIn, pixelsOut / pixelsIn, 'k+') 

296 plt.xlabel("Source amplifier pixel value") 

297 plt.ylabel("Measured pixel ratio") 

298 plt.title(f"(Source {sourceName} -> Target {targetName}) median ratio: " 

299 f"{(np.median(pixelsOut / pixelsIn))}") 

300 figure.show() 

301 

302 prompt = "Press Enter to continue: " 

303 while True: 

304 ans = input(prompt).lower() 

305 if ans in ("", "c",): 

306 break 

307 plt.close() 

308 

309 

310class CrosstalkSolveConnections(pipeBase.PipelineTaskConnections, 

311 dimensions=("instrument", "detector")): 

312 inputRatios = cT.Input( 

313 name="crosstalkRatios", 

314 doc="Ratios measured for an input exposure.", 

315 storageClass="StructuredDataDict", 

316 dimensions=("instrument", "exposure", "detector"), 

317 multiple=True, 

318 ) 

319 inputFluxes = cT.Input( 

320 name="crosstalkFluxes", 

321 doc="Fluxes of CT source pixels, for nonlinear fits.", 

322 storageClass="StructuredDataDict", 

323 dimensions=("instrument", "exposure", "detector"), 

324 multiple=True, 

325 ) 

326 camera = cT.PrerequisiteInput( 

327 name="camera", 

328 doc="Camera the input data comes from.", 

329 storageClass="Camera", 

330 dimensions=("instrument",), 

331 isCalibration=True, 

332 lookupFunction=lookupStaticCalibration, 

333 ) 

334 

335 outputCrosstalk = cT.Output( 

336 name="crosstalk", 

337 doc="Output proposed crosstalk calibration.", 

338 storageClass="CrosstalkCalib", 

339 dimensions=("instrument", "detector"), 

340 multiple=False, 

341 isCalibration=True, 

342 ) 

343 

344 def __init__(self, *, config=None): 

345 super().__init__(config=config) 

346 

347 if config.fluxOrder == 0: 

348 self.inputs.discard("inputFluxes") 

349 

350 

351class CrosstalkSolveConfig(pipeBase.PipelineTaskConfig, 

352 pipelineConnections=CrosstalkSolveConnections): 

353 """Configuration for the solving of crosstalk from pixel ratios. 

354 """ 

355 

356 rejIter = Field( 

357 dtype=int, 

358 default=3, 

359 doc="Number of rejection iterations for final coefficient calculation.", 

360 ) 

361 rejSigma = Field( 

362 dtype=float, 

363 default=2.0, 

364 doc="Rejection threshold (sigma) for final coefficient calculation.", 

365 ) 

366 fluxOrder = Field( 

367 dtype=int, 

368 default=0, 

369 doc="Polynomial order in source flux to fit crosstalk.", 

370 ) 

371 significanceLimit = Field( 

372 dtype=float, 

373 default=3.0, 

374 doc="Sigma significance level to use in marking a coefficient valid.", 

375 ) 

376 doSignificanceScaling = Field( 

377 dtype=bool, 

378 default=True, 

379 doc="Scale error by 1/sqrt(N) in calculating significant coefficients?", 

380 ) 

381 doFiltering = Field( 

382 dtype=bool, 

383 default=False, 

384 doc="Filter generated crosstalk to remove marginal measurements?", 

385 ) 

386 

387 

388class CrosstalkSolveTask(pipeBase.PipelineTask): 

389 """Task to solve crosstalk from pixel ratios. 

390 """ 

391 

392 ConfigClass = CrosstalkSolveConfig 

393 _DefaultName = 'cpCrosstalkSolve' 

394 

395 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

396 """Ensure that the input and output dimensions are passed along. 

397 

398 Parameters 

399 ---------- 

400 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext` 

401 Butler to operate on. 

402 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection` 

403 Input data refs to load. 

404 ouptutRefs : `lsst.pipe.base.connections.OutputQuantizedConnection` 

405 Output data refs to persist. 

406 """ 

407 inputs = butlerQC.get(inputRefs) 

408 

409 # Use the dimensions to set calib/provenance information. 

410 inputs['inputDims'] = [exp.dataId.byName() for exp in inputRefs.inputRatios] 

411 inputs['outputDims'] = outputRefs.outputCrosstalk.dataId.byName() 

412 

413 outputs = self.run(**inputs) 

414 butlerQC.put(outputs, outputRefs) 

415 

416 def run(self, inputRatios, inputFluxes=None, camera=None, inputDims=None, outputDims=None): 

417 """Combine ratios to produce crosstalk coefficients. 

418 

419 Parameters 

420 ---------- 

421 inputRatios : `list` [`dict` [`dict` [`dict` [`dict` [`list`]]]]] 

422 A list of nested dictionaries of ratios indexed by target 

423 and source chip, then by target and source amplifier. 

424 inputFluxes : `list` [`dict` [`dict` [`list`]]] 

425 A list of nested dictionaries of source pixel fluxes, indexed 

426 by source chip and amplifier. 

427 camera : `lsst.afw.cameraGeom.Camera` 

428 Input camera. 

429 inputDims : `list` [`lsst.daf.butler.DataCoordinate`] 

430 DataIds to use to construct provenance. 

431 outputDims : `list` [`lsst.daf.butler.DataCoordinate`] 

432 DataIds to use to populate the output calibration. 

433 

434 Returns 

435 ------- 

436 results : `lsst.pipe.base.Struct` 

437 The results struct containing: 

438 

439 ``outputCrosstalk`` 

440 Final crosstalk calibration 

441 (`lsst.ip.isr.CrosstalkCalib`). 

442 ``outputProvenance`` 

443 Provenance data for the new calibration 

444 (`lsst.ip.isr.IsrProvenance`). 

445 

446 Raises 

447 ------ 

448 RuntimeError 

449 Raised if the input data contains multiple target detectors. 

450 """ 

451 if outputDims: 

452 calibChip = outputDims['detector'] 

453 instrument = outputDims['instrument'] 

454 else: 

455 # calibChip needs to be set manually in Gen2. 

456 calibChip = None 

457 instrument = None 

458 

459 if camera and calibChip is not None: 

460 calibDetector = camera[calibChip] 

461 ordering = [amp.getName() for amp in calibDetector] 

462 else: 

463 calibDetector = None 

464 ordering = None 

465 

466 self.log.info("Combining measurements from %d ratios and %d fluxes", 

467 len(inputRatios), len(inputFluxes) if inputFluxes else 0) 

468 

469 if inputFluxes is None: 

470 inputFluxes = [None for exp in inputRatios] 

471 

472 combinedRatios = defaultdict(lambda: defaultdict(list)) 

473 combinedFluxes = defaultdict(lambda: defaultdict(list)) 

474 for ratioDict, fluxDict in zip(inputRatios, inputFluxes): 

475 for targetChip in ratioDict: 

476 if calibChip and targetChip != calibChip and targetChip != calibDetector.getName(): 

477 raise RuntimeError(f"Target chip: {targetChip} does not match calibration dimension: " 

478 f"{calibChip}, {calibDetector.getName()}!") 

479 

480 sourceChip = targetChip 

481 if sourceChip in ratioDict[targetChip]: 

482 ratios = ratioDict[targetChip][sourceChip] 

483 

484 for targetAmp in ratios: 

485 for sourceAmp in ratios[targetAmp]: 

486 combinedRatios[targetAmp][sourceAmp].extend(ratios[targetAmp][sourceAmp]) 

487 if fluxDict: 

488 combinedFluxes[targetAmp][sourceAmp].extend(fluxDict[sourceChip][sourceAmp]) 

489 # TODO: DM-21904 

490 # Iterating over all other entries in 

491 # ratioDict[targetChip] will yield inter-chip terms. 

492 

493 for targetAmp in combinedRatios: 

494 for sourceAmp in combinedRatios[targetAmp]: 

495 self.log.info("Read %d pixels for %s -> %s", 

496 len(combinedRatios[targetAmp][sourceAmp]), 

497 targetAmp, sourceAmp) 

498 if len(combinedRatios[targetAmp][sourceAmp]) > 1: 

499 self.debugRatios('reduce', combinedRatios, targetAmp, sourceAmp) 

500 

501 if self.config.fluxOrder == 0: 

502 self.log.info("Fitting crosstalk coefficients.") 

503 

504 calib = self.measureCrosstalkCoefficients(combinedRatios, ordering, 

505 self.config.rejIter, self.config.rejSigma) 

506 else: 

507 raise NotImplementedError("Non-linear crosstalk terms are not yet supported.") 

508 

509 self.log.info("Number of valid coefficients: %d", np.sum(calib.coeffValid)) 

510 

511 if self.config.doFiltering: 

512 # This step will apply the calculated validity values to 

513 # censor poorly measured coefficients. 

514 self.log.info("Filtering measured crosstalk to remove invalid solutions.") 

515 calib = self.filterCrosstalkCalib(calib) 

516 

517 # Populate the remainder of the calibration information. 

518 calib.hasCrosstalk = True 

519 calib.interChip = {} 

520 

521 # calibChip is the detector dimension, which is the detector Id 

522 calib._detectorId = calibChip 

523 if calibDetector: 

524 calib._detectorName = calibDetector.getName() 

525 calib._detectorSerial = calibDetector.getSerial() 

526 

527 calib._instrument = instrument 

528 calib.updateMetadata(setCalibId=True, setDate=True) 

529 

530 # Make an IsrProvenance(). 

531 provenance = IsrProvenance(calibType="CROSSTALK") 

532 provenance._detectorName = calibChip 

533 if inputDims: 

534 provenance.fromDataIds(inputDims) 

535 provenance._instrument = instrument 

536 provenance.updateMetadata() 

537 

538 return pipeBase.Struct( 

539 outputCrosstalk=calib, 

540 outputProvenance=provenance, 

541 ) 

542 

543 def measureCrosstalkCoefficients(self, ratios, ordering, rejIter, rejSigma): 

544 """Measure crosstalk coefficients from the ratios. 

545 

546 Given a list of ratios for each target/source amp combination, 

547 we measure a sigma clipped mean and error. 

548 

549 The coefficient errors returned are the standard deviation of 

550 the final set of clipped input ratios. 

551 

552 Parameters 

553 ---------- 

554 ratios : `dict` [`dict` [`numpy.ndarray`]] 

555 Catalog of arrays of ratios. The ratio arrays are one-dimensional 

556 ordering : `list` [`str`] or None 

557 List to use as a mapping between amplifier names (the 

558 elements of the list) and their position in the output 

559 calibration (the matching index of the list). If no 

560 ordering is supplied, the order of the keys in the ratio 

561 catalog is used. 

562 rejIter : `int` 

563 Number of rejection iterations. 

564 rejSigma : `float` 

565 Rejection threshold (sigma). 

566 

567 Returns 

568 ------- 

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

570 The output crosstalk calibration. 

571 """ 

572 calib = CrosstalkCalib(nAmp=len(ratios)) 

573 

574 if ordering is None: 

575 ordering = list(ratios.keys()) 

576 

577 # Calibration stores coefficients as a numpy ndarray. 

578 for ii, jj in itertools.product(range(calib.nAmp), range(calib.nAmp)): 

579 if ii == jj: 

580 values = [0.0] 

581 else: 

582 values = np.array(ratios[ordering[ii]][ordering[jj]]) 

583 values = values[np.abs(values) < 1.0] # Discard unreasonable values 

584 

585 # Sigma clip using the inter-quartile distance and a 

586 # normal distribution. 

587 if ii != jj: 

588 for rej in range(rejIter): 

589 if len(values) == 0: 

590 break 

591 lo, med, hi = np.percentile(values, [25.0, 50.0, 75.0]) 

592 sigma = 0.741*(hi - lo) 

593 good = np.abs(values - med) < rejSigma*sigma 

594 if good.sum() == len(good) or good.sum() == 0: 

595 break 

596 values = values[good] 

597 

598 calib.coeffNum[ii][jj] = len(values) 

599 significanceThreshold = 0.0 

600 if len(values) == 0: 

601 self.log.warning("No values for matrix element %d,%d" % (ii, jj)) 

602 calib.coeffs[ii][jj] = np.nan 

603 calib.coeffErr[ii][jj] = np.nan 

604 calib.coeffValid[ii][jj] = False 

605 else: 

606 calib.coeffs[ii][jj] = np.mean(values) 

607 if calib.coeffNum[ii][jj] == 1: 

608 calib.coeffErr[ii][jj] = np.nan 

609 calib.coeffValid[ii][jj] = False 

610 else: 

611 correctionFactor = sigmaClipCorrection(rejSigma) 

612 calib.coeffErr[ii][jj] = np.std(values) * correctionFactor 

613 

614 # Use sample stdev. 

615 significanceThreshold = self.config.significanceLimit * calib.coeffErr[ii][jj] 

616 if self.config.doSignificanceScaling is True: 

617 # Enabling this calculates the stdev of the mean. 

618 significanceThreshold /= np.sqrt(calib.coeffNum[ii][jj]) 

619 calib.coeffValid[ii][jj] = np.abs(calib.coeffs[ii][jj]) > significanceThreshold 

620 self.debugRatios('measure', ratios, ordering[ii], ordering[jj], 

621 calib.coeffs[ii][jj], calib.coeffValid[ii][jj]) 

622 self.log.info("Measured %s -> %s Coeff: %e Err: %e N: %d Valid: %s Limit: %e", 

623 ordering[jj], ordering[ii], calib.coeffs[ii][jj], calib.coeffErr[ii][jj], 

624 calib.coeffNum[ii][jj], calib.coeffValid[ii][jj], significanceThreshold) 

625 

626 return calib 

627 

628 @staticmethod 

629 def filterCrosstalkCalib(inCalib): 

630 """Apply valid constraints to the measured values. 

631 

632 Any measured coefficient that is determined to be invalid is 

633 set to zero, and has the error set to nan. The validation is 

634 determined by checking that the measured coefficient is larger 

635 than the calculated standard error of the mean. 

636 

637 Parameters 

638 ---------- 

639 inCalib : `lsst.ip.isr.CrosstalkCalib` 

640 Input calibration to filter. 

641 

642 Returns 

643 ------- 

644 outCalib : `lsst.ip.isr.CrosstalkCalib` 

645 Filtered calibration. 

646 """ 

647 outCalib = CrosstalkCalib() 

648 outCalib.numAmps = inCalib.numAmps 

649 

650 outCalib.coeffs = inCalib.coeffs 

651 outCalib.coeffs[~inCalib.coeffValid] = 0.0 

652 

653 outCalib.coeffErr = inCalib.coeffErr 

654 outCalib.coeffErr[~inCalib.coeffValid] = np.nan 

655 

656 outCalib.coeffNum = inCalib.coeffNum 

657 outCalib.coeffValid = inCalib.coeffValid 

658 

659 return outCalib 

660 

661 def debugRatios(self, stepname, ratios, i, j, coeff=0.0, valid=False): 

662 """Utility function to examine the final CT ratio set. 

663 

664 Parameters 

665 ---------- 

666 stepname : `str` 

667 State of processing to view. 

668 ratios : `dict` [`dict` [`numpy.ndarray`]] 

669 Array of measured CT ratios, indexed by source/victim 

670 amplifier. These arrays are one-dimensional. 

671 i : `str` 

672 Index of the source amplifier. 

673 j : `str` 

674 Index of the target amplifier. 

675 coeff : `float`, optional 

676 Coefficient calculated to plot along with the simple mean. 

677 valid : `bool`, optional 

678 Validity to be added to the plot title. 

679 """ 

680 frame = getDebugFrame(self._display, stepname) 

681 if frame: 

682 if i == j or ratios is None or len(ratios) < 1: 

683 pass 

684 

685 ratioList = ratios[i][j] 

686 if ratioList is None or len(ratioList) < 1: 

687 pass 

688 

689 mean = np.mean(ratioList) 

690 std = np.std(ratioList) 

691 import matplotlib.pyplot as plt 

692 figure = plt.figure(1) 

693 figure.clear() 

694 plt.hist(x=ratioList, bins=len(ratioList), 

695 cumulative=True, color='b', density=True, histtype='step') 

696 plt.xlabel("Measured pixel ratio") 

697 plt.ylabel(f"CDF: n={len(ratioList)}") 

698 plt.xlim(np.percentile(ratioList, [1.0, 99])) 

699 plt.axvline(x=mean, color="k") 

700 plt.axvline(x=coeff, color='g') 

701 plt.axvline(x=(std / np.sqrt(len(ratioList))), color='r') 

702 plt.axvline(x=-(std / np.sqrt(len(ratioList))), color='r') 

703 plt.title(f"(Source {i} -> Target {j}) mean: {mean:.2g} coeff: {coeff:.2g} valid: {valid}") 

704 figure.show() 

705 

706 prompt = "Press Enter to continue: " 

707 while True: 

708 ans = input(prompt).lower() 

709 if ans in ("", "c",): 

710 break 

711 elif ans in ("pdb", "p",): 

712 import pdb 

713 pdb.set_trace() 

714 plt.close()