Coverage for python/lsst/cp/pipe/ptc/cpPlotPtcTask.py: 10%

528 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-05 11:40 +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 <https://www.gnu.org/licenses/>. 

21# 

22 

23__all__ = ["PlotPhotonTransferCurveConfig", "PlotPhotonTransferCurveTask"] 

24 

25import numpy as np 

26import matplotlib.pyplot as plt 

27import matplotlib as mpl 

28from matplotlib import gridspec 

29 

30import lsst.pex.config as pexConfig 

31import lsst.pipe.base as pipeBase 

32import lsst.pipe.base.connectionTypes as cT 

33 

34from lsst.cp.pipe.utils import ( 

35 funcAstier, 

36 funcPolynomial, 

37 calculateWeightedReducedChi2, 

38 getFitDataFromCovariances, 

39) 

40from matplotlib.ticker import MaxNLocator 

41 

42 

43class PlotPhotonTransferCurveConnections( 

44 pipeBase.PipelineTaskConnections, dimensions=("instrument", "detector") 

45): 

46 inputPtcDataset = cT.Input( 

47 name="calib", 

48 doc="Input PTC dataset.", 

49 storageClass="PhotonTransferCurveDataset", 

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

51 isCalibration=True, 

52 ) 

53 camera = cT.PrerequisiteInput( 

54 name="camera", 

55 doc="Camera associated with this data.", 

56 storageClass="Camera", 

57 dimensions=("instrument",), 

58 isCalibration=True, 

59 ) 

60 # ptcFitType = "FULLCOVARIANCE" produces 12 plots 

61 ptcPlot1 = cT.Output( 

62 name="ptcVarMean", 

63 doc="Variance vs mean.", 

64 storageClass="Plot", 

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

66 ) 

67 ptcPlot2 = cT.Output( 

68 name="ptcVarMeanLog", 

69 doc="Variance vs Mean, log scale.", 

70 storageClass="Plot", 

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

72 ) 

73 ptcPlot3 = cT.Output( 

74 name="ptcNormalizedVar", 

75 doc="Variance over mean vs mean.", 

76 storageClass="Plot", 

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

78 ) 

79 ptcPlot4 = cT.Output( 

80 name="ptcCov01Mean", 

81 doc="Cov01 vs mean.", 

82 storageClass="Plot", 

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

84 ) 

85 ptcPlot5 = cT.Output( 

86 name="ptcCov10Mean", 

87 doc="Cov10 vs mean.", 

88 storageClass="Plot", 

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

90 ) 

91 ptcPlot6 = cT.Output( 

92 name="ptcVarResiduals", 

93 doc="Variance residuals compared to model.", 

94 storageClass="Plot", 

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

96 ) 

97 ptcPlot7 = cT.Output( 

98 name="ptcNormalizedCov01", 

99 doc="Cov01 over mean vs mean.", 

100 storageClass="Plot", 

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

102 ) 

103 ptcPlot8 = cT.Output( 

104 name="ptcNormalizedCov10", 

105 doc="Cov10 over mean vs mean.", 

106 storageClass="Plot", 

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

108 ) 

109 ptcPlot9 = cT.Output( 

110 name="ptcAandBMatrices", 

111 doc="Fig. 12 of Astier+19.", 

112 storageClass="Plot", 

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

114 ) 

115 ptcPlot10 = cT.Output( 

116 name="ptcAandBDistance", 

117 doc="Fig. 13 of Astier+19.", 

118 storageClass="Plot", 

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

120 ) 

121 ptcPlot11 = cT.Output( 

122 name="ptcACumulativeSum", 

123 doc="Fig. 14 of Astier+19.", 

124 storageClass="Plot", 

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

126 ) 

127 ptcPlot12 = cT.Output( 

128 name="ptcARelativeBias", 

129 doc="Fig. 15 of Astier+19.", 

130 storageClass="Plot", 

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

132 ) 

133 

134 

135class PlotPhotonTransferCurveConfig( 

136 pipeBase.PipelineTaskConfig, pipelineConnections=PlotPhotonTransferCurveConnections 

137): 

138 """Configuration for the measurement of covariances from flats.""" 

139 

140 signalElectronsRelativeA = pexConfig.Field( 

141 dtype=float, 

142 doc="Signal value (in e-) for relative systematic bias between different " 

143 "methods of estimating a_ij (Fig. 15 of Astier+19).", 

144 default=75000.0, 

145 ) 

146 plotNormalizedCovariancesNumberOfBins = pexConfig.Field( 

147 dtype=int, 

148 doc="Number of bins in `plotNormalizedCovariancesNumber` function" 

149 "(Fig. 8, 10., of Astier+19).", 

150 default=10, 

151 ) 

152 

153 

154class PlotPhotonTransferCurveTask(pipeBase.PipelineTask): 

155 """A class to plot the dataset from MeasurePhotonTransferCurveTask. 

156 

157 Parameters 

158 ---------- 

159 outDir : `str`, optional 

160 Path to the output directory where the final PDF will 

161 be placed. 

162 

163 signalElectronsRelativeA : `float`, optional 

164 Signal value for relative systematic bias between different 

165 methods of estimating a_ij (Fig. 15 of Astier+19). 

166 

167 plotNormalizedCovariancesNumberOfBins : `float`, optional 

168 Number of bins in `plotNormalizedCovariancesNumber` function 

169 (Fig. 8, 10., of Astier+19). 

170 

171 Notes 

172 ----- 

173 See DM-36388 for usage exammple. 

174 """ 

175 

176 ConfigClass = PlotPhotonTransferCurveConfig 

177 _DefaultName = "cpPlotPtc" 

178 

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

180 inputs = butlerQC.get(inputRefs) 

181 outputs = self.run(**inputs) 

182 butlerQC.put(outputs, outputRefs) 

183 

184 def run(self, inputPtcDataset, camera=None): 

185 """Make the plots for the PTC task. 

186 

187 Parameters 

188 ---------- 

189 inputPtcDataset : `lsst.ip.isr.PhotonTransferCurveDataset` 

190 Output dataset from Photon Transfer Curve task. 

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

192 Camera to use for camera geometry information. 

193 """ 

194 ptcFitType = inputPtcDataset.ptcFitType 

195 self.detId = inputPtcDataset.getMetadata()["DETECTOR"] 

196 

197 if ptcFitType in [ 

198 "FULLCOVARIANCE", 

199 ]: 

200 figDict = self._covAstierMakeAllPlots(inputPtcDataset) 

201 elif ptcFitType in ["EXPAPPROXIMATION", "POLYNOMIAL"]: 

202 figDict = self._plotStandardPtc(inputPtcDataset) 

203 else: 

204 raise RuntimeError( 

205 f"The input dataset had an invalid dataset.ptcFitType: {ptcFitType}. \n" 

206 "Options: 'FULLCOVARIANCE', EXPAPPROXIMATION, or 'POLYNOMIAL'." 

207 ) 

208 

209 maxNumberPlots = 12 

210 if len(figDict) < maxNumberPlots: 

211 for i in range(len(figDict), maxNumberPlots + 1): 

212 figDict.setdefault(i, plt.figure()) 

213 

214 return pipeBase.Struct( 

215 ptcPlot1=figDict[0], 

216 ptcPlot2=figDict[1], 

217 ptcPlot3=figDict[2], 

218 ptcPlot4=figDict[3], 

219 ptcPlot5=figDict[4], 

220 ptcPlot6=figDict[5], 

221 ptcPlot7=figDict[6], 

222 ptcPlot8=figDict[7], 

223 ptcPlot9=figDict[8], 

224 ptcPlot10=figDict[9], 

225 ptcPlot11=figDict[10], 

226 ptcPlot12=figDict[11], 

227 ) 

228 

229 def _covAstierMakeAllPlots(self, dataset): 

230 """Make plots for PTC dataset when fitType=FULLCOVARIANCE. 

231 

232 This function call other functions that mostly reproduce the 

233 plots in Astier+19. Most of the code is ported from Pierre 

234 Astier's repository https://github.com/PierreAstier/bfptc 

235 

236 Parameters 

237 ---------- 

238 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

239 The dataset containing the necessary information to 

240 produce the plots. 

241 

242 Returns 

243 ------- 

244 figDict : `dict` [`int`, `matplotlib.figure.Figure`] 

245 Figure dictionary, keyd by an integer index: 

246 0: ptcPlot1 ('ptcVarMean') 

247 1: ptcPlot2 ('ptcVarMeanLog') 

248 2: ptcPlot3 ('ptcNormalizedVar') 

249 3: ptcPlot4 ('ptcCov01Mean') 

250 4: ptcPlot5 ('ptcCov10Mean') 

251 5: ptcPlot6 ('ptcVarResiduals') 

252 6: ptcPlot7 ('ptcNormalizedCov01') 

253 7: ptcPlot8 ('ptcNormalizedCov10') 

254 8: ptcPlot9 ('ptcAandBDistance') 

255 9: ptcPlot10 ('ptcAandBDistance') 

256 10: ptcPlot11 ('ptcACumulativeSum') 

257 11: ptcPlot12 ('ptcARelativeBias') 

258 """ 

259 mu = dataset.finalMeans 

260 # dictionaries with ampNames as keys 

261 fullCovs = dataset.covariances 

262 fullCovsModel = dataset.covariancesModel 

263 fullCovWeights = dataset.covariancesSqrtWeights 

264 aDict = dataset.aMatrix 

265 bDict = dataset.bMatrix 

266 fullCovWeights = dataset.covariancesSqrtWeights 

267 aDict = dataset.aMatrix 

268 bDict = dataset.bMatrix 

269 fullCovWeights = dataset.covariancesSqrtWeights 

270 aDict = dataset.aMatrix 

271 bDict = dataset.bMatrix 

272 fullCovWeights = dataset.covariancesSqrtWeights 

273 aDict = dataset.aMatrix 

274 bDict = dataset.bMatrix 

275 fullCovsNoB = dataset.covariances 

276 fullCovsModelNoB = dataset.covariancesModelNoB 

277 fullCovWeightsNoB = dataset.covariancesSqrtWeights 

278 aDictNoB = dataset.aMatrixNoB 

279 gainDict = dataset.gain 

280 noiseDict = dataset.noise 

281 

282 figList1 = self.plotCovariances( 

283 mu, 

284 fullCovs, 

285 fullCovsModel, 

286 fullCovWeights, 

287 fullCovsNoB, 

288 fullCovsModelNoB, 

289 fullCovWeightsNoB, 

290 gainDict, 

291 noiseDict, 

292 aDict, 

293 bDict, 

294 ) 

295 figList2 = self.plotNormalizedCovariances( 

296 0, 

297 0, 

298 mu, 

299 fullCovs, 

300 fullCovsModel, 

301 fullCovWeights, 

302 fullCovsNoB, 

303 fullCovsModelNoB, 

304 fullCovWeightsNoB, 

305 offset=0.01, 

306 topPlot=True, 

307 numberOfBins=self.config.plotNormalizedCovariancesNumberOfBins, 

308 ) 

309 figList3 = self.plotNormalizedCovariances( 

310 0, 

311 1, 

312 mu, 

313 fullCovs, 

314 fullCovsModel, 

315 fullCovWeights, 

316 fullCovsNoB, 

317 fullCovsModelNoB, 

318 fullCovWeightsNoB, 

319 numberOfBins=self.config.plotNormalizedCovariancesNumberOfBins, 

320 ) 

321 figList4 = self.plotNormalizedCovariances( 

322 1, 

323 0, 

324 mu, 

325 fullCovs, 

326 fullCovsModel, 

327 fullCovWeights, 

328 fullCovsNoB, 

329 fullCovsModelNoB, 

330 fullCovWeightsNoB, 

331 numberOfBins=self.config.plotNormalizedCovariancesNumberOfBins, 

332 ) 

333 figList5 = self.plot_a_b(aDict, bDict) 

334 figList6 = self.ab_vs_dist(aDict, bDict, bRange=4) 

335 figList7 = self.plotAcoeffsSum(aDict, bDict) 

336 figList8 = self.plotRelativeBiasACoeffs( 

337 aDict, 

338 aDictNoB, 

339 fullCovsModel, 

340 fullCovsModelNoB, 

341 self.config.signalElectronsRelativeA, 

342 gainDict, 

343 maxr=4, 

344 ) 

345 

346 # Let's switch the var/mu vs mu plot (figList1[2]) 

347 # and the variance residual plot (figList2[0]) so that 

348 # var/mu vs mu corresponds to ptcPlot3 and the name 

349 # matches when FITTYPE is FULLCOVARIANCE or 

350 # [EXPAPPROXIMATION, POLYNOMIAL] 

351 temp = figList1[2] 

352 figList1[2] = figList2[0] 

353 figList2 = [temp] 

354 

355 figList = ( 

356 figList1 

357 + figList2 

358 + figList3 

359 + figList4 

360 + figList5 

361 + figList6 

362 + figList7 

363 + figList8 

364 ) 

365 

366 figDict = {} 

367 for i, fig in enumerate(figList): 

368 figDict[i] = fig 

369 

370 return figDict 

371 

372 @staticmethod 

373 def plotCovariances( 

374 mu, 

375 covs, 

376 covsModel, 

377 covsWeights, 

378 covsNoB, 

379 covsModelNoB, 

380 covsWeightsNoB, 

381 gainDict, 

382 noiseDict, 

383 aDict, 

384 bDict, 

385 ): 

386 """Plot covariances and models: Cov00, Cov10, Cov01. 

387 

388 Figs. 6 and 7 of Astier+19 

389 

390 Parameters 

391 ---------- 

392 mu : `dict` [`str`, `list`] 

393 Dictionary keyed by amp name with mean signal values. 

394 

395 covs : `dict` [`str`, `list`] 

396 Dictionary keyed by amp names containing a list of measued 

397 covariances per mean flux. 

398 

399 covsModel : `dict` [`str`, `list`] 

400 Dictionary keyed by amp names containinging covariances 

401 model (Eq. 20 of Astier+19) per mean flux. 

402 

403 covsWeights : `dict` [`str`, `list`] 

404 Dictionary keyed by amp names containinging sqrt. of 

405 covariances weights. 

406 

407 covsNoB : `dict` [`str`, `list`] 

408 Dictionary keyed by amp names containing a list of measued 

409 covariances per mean flux ('b'=0 in Astier+19). 

410 

411 covsModelNoB : `dict` [`str`, `list`] 

412 Dictionary keyed by amp names containing covariances model 

413 (with 'b'=0 in Eq. 20 of Astier+19) per mean flux. 

414 

415 covsWeightsNoB : `dict` [`str`, `list`] 

416 Dictionary keyed by amp names containing sqrt. of 

417 covariances weights ('b' = 0 in Eq. 20 of Astier+19). 

418 

419 gainDict : `dict` [`str`, `float`] 

420 Dictionary keyed by amp names containing the gains in e-/ADU. 

421 

422 noiseDict : `dict` [`str`, `float`] 

423 Dictionary keyed by amp names containing the rms redout 

424 noise in e-. 

425 

426 aDict : `dict` [`str`, `numpy.array`] 

427 Dictionary keyed by amp names containing 'a' coefficients 

428 (Eq. 20 of Astier+19). 

429 

430 bDict : `dict` [`str`, `numpy.array`] 

431 Dictionary keyed by amp names containing 'b' coefficients 

432 (Eq. 20 of Astier+19). 

433 """ 

434 legendFontSize = 6.5 

435 labelFontSize = 7 

436 titleFontSize = 9 

437 supTitleFontSize = 18 

438 markerSize = 25 

439 

440 nAmps = len(covs) 

441 if nAmps == 2: 

442 nRows, nCols = 2, 1 

443 nRows = np.sqrt(nAmps) 

444 mantissa, _ = np.modf(nRows) 

445 if mantissa > 0: 

446 nRows = int(nRows) + 1 

447 nCols = nRows 

448 else: 

449 nRows = int(nRows) 

450 nCols = nRows 

451 

452 f, ax = plt.subplots( 

453 nrows=nRows, ncols=nCols, sharex="col", sharey="row", figsize=(13, 10) 

454 ) 

455 f2, ax2 = plt.subplots( 

456 nrows=nRows, ncols=nCols, sharex="col", sharey="row", figsize=(13, 10) 

457 ) 

458 fResCov00, axResCov00 = plt.subplots( 

459 nrows=nRows, ncols=nCols, sharex="col", sharey="row", figsize=(13, 10) 

460 ) 

461 fCov01, axCov01 = plt.subplots( 

462 nrows=nRows, ncols=nCols, sharex="col", sharey="row", figsize=(13, 10) 

463 ) 

464 fCov10, axCov10 = plt.subplots( 

465 nrows=nRows, ncols=nCols, sharex="col", sharey="row", figsize=(13, 10) 

466 ) 

467 

468 assert len(covsModel) == nAmps 

469 assert len(covsWeights) == nAmps 

470 

471 assert len(covsNoB) == nAmps 

472 assert len(covsModelNoB) == nAmps 

473 assert len(covsWeightsNoB) == nAmps 

474 

475 for i, (amp, a, a2, aResVar, a3, a4) in enumerate( 

476 zip( 

477 covs, 

478 ax.flatten(), 

479 ax2.flatten(), 

480 axResCov00.flatten(), 

481 axCov01.flatten(), 

482 axCov10.flatten(), 

483 ) 

484 ): 

485 

486 muAmp, cov, model, weight = ( 

487 mu[amp], 

488 covs[amp], 

489 covsModel[amp], 

490 covsWeights[amp], 

491 ) 

492 if not np.isnan( 

493 np.array(cov) 

494 ).all(): # If all the entries are np.nan, this is a bad amp. 

495 aCoeffs, bCoeffs = np.array(aDict[amp]), np.array(bDict[amp]) 

496 gain, noise = gainDict[amp], noiseDict[amp] 

497 ( 

498 meanVecFinal, 

499 varVecFinal, 

500 varVecModelFinal, 

501 varWeightsFinal, 

502 _, 

503 ) = getFitDataFromCovariances( 

504 0, 0, muAmp, cov, model, weight, returnMasked=True 

505 ) 

506 

507 # Get weighted reduced chi2 

508 chi2FullModelVar = calculateWeightedReducedChi2( 

509 varVecFinal, varVecModelFinal, varWeightsFinal, len(meanVecFinal), 4 

510 ) 

511 

512 ( 

513 meanVecFinalCov01, 

514 varVecFinalCov01, 

515 varVecModelFinalCov01, 

516 _, 

517 _, 

518 ) = getFitDataFromCovariances( 

519 0, 0, muAmp, cov, model, weight, returnMasked=True 

520 ) 

521 

522 ( 

523 meanVecFinalCov10, 

524 varVecFinalCov10, 

525 varVecModelFinalCov10, 

526 _, 

527 _, 

528 ) = getFitDataFromCovariances( 

529 1, 0, muAmp, cov, model, weight, returnMasked=True 

530 ) 

531 

532 # cuadratic fit for residuals below 

533 par2 = np.polyfit(meanVecFinal, varVecFinal, 2, w=varWeightsFinal) 

534 varModelFinalQuadratic = np.polyval(par2, meanVecFinal) 

535 chi2QuadModelVar = calculateWeightedReducedChi2( 

536 varVecFinal, 

537 varModelFinalQuadratic, 

538 varWeightsFinal, 

539 len(meanVecFinal), 

540 3, 

541 ) 

542 

543 # fit with no 'b' coefficient (c = a*b in Eq. 20 of Astier+19) 

544 covNoB, modelNoB, weightNoB = ( 

545 covsNoB[amp], 

546 covsModelNoB[amp], 

547 covsWeightsNoB[amp], 

548 ) 

549 ( 

550 meanVecFinalNoB, 

551 varVecFinalNoB, 

552 varVecModelFinalNoB, 

553 varWeightsFinalNoB, 

554 _, 

555 ) = getFitDataFromCovariances( 

556 0, 0, muAmp, covNoB, modelNoB, weightNoB, returnMasked=True 

557 ) 

558 

559 chi2FullModelNoBVar = calculateWeightedReducedChi2( 

560 varVecFinalNoB, 

561 varVecModelFinalNoB, 

562 varWeightsFinalNoB, 

563 len(meanVecFinalNoB), 

564 3, 

565 ) 

566 stringLegend = ( 

567 f"Gain: {gain:.4} e/ADU \n" 

568 f"Noise: {noise:.4} e \n" 

569 + r"$a_{00}$: %.3e 1/e" % aCoeffs[0, 0] 

570 + "\n" 

571 + r"$b_{00}$: %.3e 1/e" % bCoeffs[0, 0] 

572 + f"\nLast in fit: {meanVecFinal[-1]:.7} ADU " 

573 ) 

574 minMeanVecFinal = np.nanmin(meanVecFinal) 

575 maxMeanVecFinal = np.nanmax(meanVecFinal) 

576 deltaXlim = maxMeanVecFinal - minMeanVecFinal 

577 

578 a.set_xlabel(r"Mean signal ($\mu$, ADU)", fontsize=labelFontSize) 

579 a.set_ylabel(r"Variance (ADU$^2$)", fontsize=labelFontSize) 

580 a.tick_params(labelsize=11) 

581 a.set_xscale("linear") 

582 a.set_yscale("linear") 

583 a.scatter(meanVecFinal, varVecFinal, c="blue", marker="o", s=markerSize) 

584 a.plot(meanVecFinal, varVecModelFinal, color="red", linestyle="-") 

585 a.text( 

586 0.03, 

587 0.7, 

588 stringLegend, 

589 transform=a.transAxes, 

590 fontsize=legendFontSize, 

591 ) 

592 a.set_title(amp, fontsize=titleFontSize) 

593 a.set_xlim( 

594 [ 

595 minMeanVecFinal - 0.2 * deltaXlim, 

596 maxMeanVecFinal + 0.2 * deltaXlim, 

597 ] 

598 ) 

599 

600 # Same as above, but in log-scale 

601 a2.set_xlabel(r"Mean Signal ($\mu$, ADU)", fontsize=labelFontSize) 

602 a2.set_ylabel(r"Variance (ADU$^2$)", fontsize=labelFontSize) 

603 a2.tick_params(labelsize=11) 

604 a2.set_xscale("log") 

605 a2.set_yscale("log") 

606 a2.plot(meanVecFinal, varVecModelFinal, color="red", linestyle="-") 

607 a2.scatter( 

608 meanVecFinal, varVecFinal, c="blue", marker="o", s=markerSize 

609 ) 

610 a2.text( 

611 0.03, 

612 0.7, 

613 stringLegend, 

614 transform=a2.transAxes, 

615 fontsize=legendFontSize, 

616 ) 

617 a2.set_title(amp, fontsize=titleFontSize) 

618 a2.set_xlim([minMeanVecFinal, maxMeanVecFinal]) 

619 

620 # Residuals var - model 

621 aResVar.set_xlabel(r"Mean signal ($\mu$, ADU)", fontsize=labelFontSize) 

622 aResVar.set_ylabel(r"Residuals (ADU$^2$)", fontsize=labelFontSize) 

623 aResVar.tick_params(labelsize=11) 

624 aResVar.set_xscale("linear") 

625 aResVar.set_yscale("linear") 

626 aResVar.plot( 

627 meanVecFinal, 

628 varVecFinal - varVecModelFinal, 

629 color="blue", 

630 linestyle="-", 

631 label=r"Full fit ($\chi_{\rm{red}}^2$: %g)" % chi2FullModelVar, 

632 ) 

633 aResVar.plot( 

634 meanVecFinal, 

635 varVecFinal - varModelFinalQuadratic, 

636 color="red", 

637 linestyle="-", 

638 label=r"Quadratic fit ($\chi_{\rm{red}}^2$: %g)" % chi2QuadModelVar, 

639 ) 

640 aResVar.plot( 

641 meanVecFinalNoB, 

642 varVecFinalNoB - varVecModelFinalNoB, 

643 color="green", 

644 linestyle="-", 

645 label=r"Full fit (b=0) ($\chi_{\rm{red}}^2$: %g)" 

646 % chi2FullModelNoBVar, 

647 ) 

648 aResVar.axhline(color="black") 

649 aResVar.set_title(amp, fontsize=titleFontSize) 

650 aResVar.set_xlim( 

651 [ 

652 minMeanVecFinal - 0.2 * deltaXlim, 

653 maxMeanVecFinal + 0.2 * deltaXlim, 

654 ] 

655 ) 

656 aResVar.legend(fontsize=7) 

657 

658 a3.set_xlabel(r"Mean signal ($\mu$, ADU)", fontsize=labelFontSize) 

659 a3.set_ylabel(r"Cov01 (ADU$^2$)", fontsize=labelFontSize) 

660 a3.tick_params(labelsize=11) 

661 a3.set_xscale("linear") 

662 a3.set_yscale("linear") 

663 a3.scatter( 

664 meanVecFinalCov01, 

665 varVecFinalCov01, 

666 c="blue", 

667 marker="o", 

668 s=markerSize, 

669 ) 

670 a3.plot( 

671 meanVecFinalCov01, varVecModelFinalCov01, color="red", linestyle="-" 

672 ) 

673 a3.set_title(amp, fontsize=titleFontSize) 

674 a3.set_xlim( 

675 [ 

676 minMeanVecFinal - 0.2 * deltaXlim, 

677 maxMeanVecFinal + 0.2 * deltaXlim, 

678 ] 

679 ) 

680 

681 a4.set_xlabel(r"Mean signal ($\mu$, ADU)", fontsize=labelFontSize) 

682 a4.set_ylabel(r"Cov10 (ADU$^2$)", fontsize=labelFontSize) 

683 a4.tick_params(labelsize=11) 

684 a4.set_xscale("linear") 

685 a4.set_yscale("linear") 

686 a4.scatter( 

687 meanVecFinalCov10, 

688 varVecFinalCov10, 

689 c="blue", 

690 marker="o", 

691 s=markerSize, 

692 ) 

693 a4.plot( 

694 meanVecFinalCov10, varVecModelFinalCov10, color="red", linestyle="-" 

695 ) 

696 a4.set_title(amp, fontsize=titleFontSize) 

697 a4.set_xlim( 

698 [ 

699 minMeanVecFinal - 0.2 * deltaXlim, 

700 maxMeanVecFinal + 0.2 * deltaXlim, 

701 ] 

702 ) 

703 

704 else: 

705 a.set_title(f"{amp} (BAD)", fontsize=titleFontSize) 

706 a2.set_title(f"{amp} (BAD)", fontsize=titleFontSize) 

707 a3.set_title(f"{amp} (BAD)", fontsize=titleFontSize) 

708 a4.set_title(f"{amp} (BAD)", fontsize=titleFontSize) 

709 

710 f.suptitle( 

711 "PTC from covariances as in Astier+19 \n Fit: Eq. 20, Astier+19", 

712 fontsize=supTitleFontSize, 

713 ) 

714 f2.suptitle( 

715 "PTC from covariances as in Astier+19 (log-log) \n Fit: Eq. 20, Astier+19", 

716 fontsize=supTitleFontSize, 

717 ) 

718 fResCov00.suptitle( 

719 "Residuals (data-model) for Cov00 (Var)", fontsize=supTitleFontSize 

720 ) 

721 fCov01.suptitle( 

722 "Cov01 as in Astier+19 (nearest parallel neighbor covariance) \n" 

723 " Fit: Eq. 20, Astier+19", 

724 fontsize=supTitleFontSize, 

725 ) 

726 fCov10.suptitle( 

727 "Cov10 as in Astier+19 (nearest serial neighbor covariance) \n" 

728 "Fit: Eq. 20, Astier+19", 

729 fontsize=supTitleFontSize, 

730 ) 

731 

732 return [f, f2, fResCov00, fCov01, fCov10] 

733 

734 def plotNormalizedCovariances( 

735 self, 

736 i, 

737 j, 

738 inputMu, 

739 covs, 

740 covsModel, 

741 covsWeights, 

742 covsNoB, 

743 covsModelNoB, 

744 covsWeightsNoB, 

745 offset=0.004, 

746 numberOfBins=10, 

747 plotData=True, 

748 topPlot=False, 

749 ): 

750 """Plot C_ij/mu vs mu. 

751 

752 Figs. 8, 10, and 11 of Astier+19 

753 

754 Parameters 

755 ---------- 

756 i : `int` 

757 Covariance lag. 

758 j : `int` 

759 Covariance lag. 

760 inputMu : `dict` [`str`, `list`] 

761 Dictionary keyed by amp name with mean signal values. 

762 covs : `dict` [`str`, `list`] 

763 Dictionary keyed by amp names containing a list of measued 

764 covariances per mean flux. 

765 covsModel : `dict` [`str`, `list`] 

766 Dictionary keyed by amp names containinging covariances 

767 model (Eq. 20 of Astier+19) per mean flux. 

768 covsWeights : `dict` [`str`, `list`] 

769 Dictionary keyed by amp names containinging sqrt. of 

770 covariances weights. 

771 covsNoB : `dict` [`str`, `list`] 

772 Dictionary keyed by amp names containing a list of measued 

773 covariances per mean flux ('b'=0 in Astier+19). 

774 covsModelNoB : `dict` [`str`, `list`] 

775 Dictionary keyed by amp names containing covariances model 

776 (with 'b'=0 in Eq. 20 of Astier+19) per mean flux. 

777 covsWeightsNoB : `dict` [`str`, `list`] 

778 Dictionary keyed by amp names containing sqrt. of 

779 covariances weights ('b' = 0 in Eq. 20 of Astier+19). 

780 expIdMask : `dict` [`str`, `list`] 

781 Dictionary keyed by amp names containing the masked 

782 exposure pairs. 

783 offset : `float`, optional 

784 Constant offset factor to plot covariances in same panel 

785 (so they don't overlap). 

786 numberOfBins : `int`, optional 

787 Number of bins for top and bottom plot. 

788 plotData : `bool`, optional 

789 Plot the data points? 

790 topPlot : `bool`, optional 

791 Plot the top plot with the covariances, and the bottom 

792 plot with the model residuals? 

793 """ 

794 if not topPlot: 

795 fig = plt.figure(figsize=(8, 10)) 

796 gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1]) 

797 gs.update(hspace=0) 

798 ax0 = plt.subplot(gs[0]) 

799 plt.setp(ax0.get_xticklabels(), visible=False) 

800 else: 

801 fig = plt.figure(figsize=(8, 8)) 

802 ax0 = plt.subplot(111) 

803 ax0.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 

804 ax0.tick_params(axis="both", labelsize="x-large") 

805 mue, rese, wce = [], [], [] 

806 mueNoB, reseNoB, wceNoB = [], [], [] 

807 for counter, amp in enumerate(covs): 

808 muAmp, fullCov, fullCovModel, fullCovWeight = ( 

809 inputMu[amp], 

810 covs[amp], 

811 covsModel[amp], 

812 covsWeights[amp], 

813 ) 

814 if len(fullCov) == 0: 

815 continue 

816 mu, cov, model, weightCov, _ = getFitDataFromCovariances( 

817 i, 

818 j, 

819 muAmp, 

820 fullCov, 

821 fullCovModel, 

822 fullCovWeight, 

823 divideByMu=True, 

824 returnMasked=True, 

825 ) 

826 

827 mue += list(mu) 

828 rese += list(cov - model) 

829 wce += list(weightCov) 

830 

831 fullCovNoB, fullCovModelNoB, fullCovWeightNoB = ( 

832 covsNoB[amp], 

833 covsModelNoB[amp], 

834 covsWeightsNoB[amp], 

835 ) 

836 if len(fullCovNoB) == 0: 

837 continue 

838 (muNoB, covNoB, modelNoB, weightCovNoB, _) = getFitDataFromCovariances( 

839 i, 

840 j, 

841 muAmp, 

842 fullCovNoB, 

843 fullCovModelNoB, 

844 fullCovWeightNoB, 

845 divideByMu=True, 

846 returnMasked=True, 

847 ) 

848 

849 mueNoB += list(muNoB) 

850 reseNoB += list(covNoB - modelNoB) 

851 wceNoB += list(weightCovNoB) 

852 

853 # the corresponding fit 

854 (fit_curve,) = plt.plot(mu, model + counter * offset, "-", linewidth=4.0) 

855 # bin plot. len(mu) = no binning 

856 gind = self.indexForBins(mu, numberOfBins) 

857 

858 xb, yb, wyb, sigyb = self.binData(mu, cov, gind, weightCov) 

859 plt.errorbar( 

860 xb, 

861 yb + counter * offset, 

862 yerr=sigyb, 

863 marker="o", 

864 linestyle="none", 

865 markersize=6.5, 

866 color=fit_curve.get_color(), 

867 label=f"{amp} (N: {len(mu)})", 

868 ) 

869 # plot the data 

870 if plotData: 

871 (points,) = plt.plot( 

872 mu, cov + counter * offset, ".", color=fit_curve.get_color() 

873 ) 

874 plt.legend(loc="upper right", fontsize=8) 

875 # end loop on amps 

876 mue = np.array(mue) 

877 rese = np.array(rese) 

878 wce = np.array(wce) 

879 mueNoB = np.array(mueNoB) 

880 reseNoB = np.array(reseNoB) 

881 wceNoB = np.array(wceNoB) 

882 

883 plt.xlabel(r"$\mu (el)$", fontsize="x-large") 

884 plt.ylabel(r"$Cov{%d%d}/\mu + Cst (el)$" % (i, j), fontsize="x-large") 

885 if not topPlot: 

886 gind = self.indexForBins(mue, numberOfBins) 

887 xb, yb, wyb, sigyb = self.binData(mue, rese, gind, wce) 

888 

889 ax1 = plt.subplot(gs[1], sharex=ax0) 

890 ax1.errorbar( 

891 xb, yb, yerr=sigyb, marker="o", linestyle="none", label="Full fit" 

892 ) 

893 gindNoB = self.indexForBins(mueNoB, numberOfBins) 

894 xb2, yb2, wyb2, sigyb2 = self.binData(mueNoB, reseNoB, gindNoB, wceNoB) 

895 

896 ax1.errorbar( 

897 xb2, yb2, yerr=sigyb2, marker="o", linestyle="none", label="b = 0" 

898 ) 

899 ax1.tick_params(axis="both", labelsize="x-large") 

900 plt.legend(loc="upper left", fontsize="large") 

901 # horizontal line at zero 

902 plt.plot(xb, [0] * len(xb), "--", color="k") 

903 plt.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 

904 plt.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 

905 plt.xlabel(r"$\mu (el)$", fontsize="x-large") 

906 plt.ylabel(r"$Cov{%d%d}/\mu$ -model (el)" % (i, j), fontsize="x-large") 

907 plt.tight_layout() 

908 plt.suptitle(f"Nbins: {numberOfBins}") 

909 # overlapping y labels: 

910 fig.canvas.draw() 

911 labels0 = [item.get_text() for item in ax0.get_yticklabels()] 

912 labels0[0] = "" 

913 ax0.set_yticklabels(labels0) 

914 

915 return [fig] 

916 

917 @staticmethod 

918 def plot_a_b(aDict, bDict, bRange=3): 

919 """Fig. 12 of Astier+19 

920 

921 Color display of a and b arrays fits, averaged over channels. 

922 

923 Parameters 

924 ---------- 

925 aDict : `dict` [`numpy.array`] 

926 Dictionary keyed by amp names containing the fitted 'a' 

927 coefficients from the model in Eq. 20 of Astier+19 (if 

928 `ptcFitType` is `FULLCOVARIANCE`). 

929 bDict : `dict` [`numpy.array`] 

930 Dictionary keyed by amp names containing the fitted 'b' 

931 coefficients from the model in Eq. 20 of Astier+19 (if 

932 `ptcFitType` is `FULLCOVARIANCE`). 

933 bRange : `int` 

934 Maximum lag for b arrays. 

935 """ 

936 a, b = [], [] 

937 for amp in aDict: 

938 if np.isnan(aDict[amp]).all(): 

939 continue 

940 a.append(aDict[amp]) 

941 b.append(bDict[amp]) 

942 a = np.array(a).mean(axis=0) 

943 b = np.array(b).mean(axis=0) 

944 fig = plt.figure(figsize=(7, 11)) 

945 ax0 = fig.add_subplot(2, 1, 1) 

946 im0 = ax0.imshow( 

947 np.abs(a.transpose()), origin="lower", norm=mpl.colors.LogNorm() 

948 ) 

949 ax0.tick_params(axis="both", labelsize="x-large") 

950 ax0.set_title(r"$|a|$", fontsize="x-large") 

951 ax0.xaxis.set_ticks_position("bottom") 

952 cb0 = plt.colorbar(im0) 

953 cb0.ax.tick_params(labelsize="x-large") 

954 

955 ax1 = fig.add_subplot(2, 1, 2) 

956 ax1.tick_params(axis="both", labelsize="x-large") 

957 ax1.yaxis.set_major_locator(MaxNLocator(integer=True)) 

958 ax1.xaxis.set_major_locator(MaxNLocator(integer=True)) 

959 im1 = ax1.imshow(1e6 * b[:bRange, :bRange].transpose(), origin="lower") 

960 cb1 = plt.colorbar(im1) 

961 cb1.ax.tick_params(labelsize="x-large") 

962 ax1.set_title(r"$b \times 10^6$", fontsize="x-large") 

963 ax1.xaxis.set_ticks_position("bottom") 

964 plt.tight_layout() 

965 

966 return [fig] 

967 

968 @staticmethod 

969 def ab_vs_dist(aDict, bDict, bRange=4): 

970 """Fig. 13 of Astier+19. 

971 

972 Values of a and b arrays fits, averaged over amplifiers, as a 

973 function of distance. 

974 

975 Parameters 

976 ---------- 

977 aDict : `dict` [`numpy.array`] 

978 Dictionary keyed by amp names containing the fitted 'a' 

979 coefficients from the model in Eq. 20 of Astier+19 (if 

980 `ptcFitType` is `FULLCOVARIANCE`). 

981 

982 bDict : `dict` [`numpy.array`] 

983 Dictionary keyed by amp names containing the fitted 'b' 

984 coefficients from the model in Eq. 20 of Astier+19 (if 

985 `ptcFitType` is `FULLCOVARIANCE`). 

986 bRange : `int` 

987 Maximum lag for b arrays. 

988 """ 

989 assert len(aDict) == len(bDict) 

990 a = [] 

991 for amp in aDict: 

992 if np.isnan(aDict[amp]).all(): 

993 continue 

994 a.append(aDict[amp]) 

995 a = np.array(a) 

996 y = a.mean(axis=0) 

997 sy = a.std(axis=0) / np.sqrt(len(aDict)) 

998 i, j = np.indices(y.shape) 

999 upper = (i >= j).ravel() 

1000 r = np.sqrt(i**2 + j**2).ravel() 

1001 y = y.ravel() 

1002 sy = sy.ravel() 

1003 fig = plt.figure(figsize=(6, 9)) 

1004 ax = fig.add_subplot(211) 

1005 ax.set_xlim([0.5, r.max() + 1]) 

1006 ax.errorbar( 

1007 r[upper], 

1008 y[upper], 

1009 yerr=sy[upper], 

1010 marker="o", 

1011 linestyle="none", 

1012 color="b", 

1013 label="$i>=j$", 

1014 ) 

1015 ax.errorbar( 

1016 r[~upper], 

1017 y[~upper], 

1018 yerr=sy[~upper], 

1019 marker="o", 

1020 linestyle="none", 

1021 color="r", 

1022 label="$i<j$", 

1023 ) 

1024 ax.legend(loc="upper center", fontsize="x-large") 

1025 ax.set_xlabel(r"$\sqrt{i^2+j^2}$", fontsize="x-large") 

1026 ax.set_ylabel(r"$a_{ij}$", fontsize="x-large") 

1027 ax.set_yscale("log") 

1028 ax.tick_params(axis="both", labelsize="x-large") 

1029 

1030 # 

1031 axb = fig.add_subplot(212) 

1032 b = [] 

1033 for amp in bDict: 

1034 if np.isnan(bDict[amp]).all(): 

1035 continue 

1036 b.append(bDict[amp]) 

1037 b = np.array(b) 

1038 yb = b.mean(axis=0) 

1039 syb = b.std(axis=0) / np.sqrt(len(bDict)) 

1040 ib, jb = np.indices(yb.shape) 

1041 upper = (ib > jb).ravel() 

1042 rb = np.sqrt(i**2 + j**2).ravel() 

1043 yb = yb.ravel() 

1044 syb = syb.ravel() 

1045 xmin = -0.2 

1046 xmax = bRange 

1047 axb.set_xlim([xmin, xmax + 0.2]) 

1048 cutu = (r > xmin) & (r < xmax) & (upper) 

1049 cutl = (r > xmin) & (r < xmax) & (~upper) 

1050 axb.errorbar( 

1051 rb[cutu], 

1052 yb[cutu], 

1053 yerr=syb[cutu], 

1054 marker="o", 

1055 linestyle="none", 

1056 color="b", 

1057 label="$i>=j$", 

1058 ) 

1059 axb.errorbar( 

1060 rb[cutl], 

1061 yb[cutl], 

1062 yerr=syb[cutl], 

1063 marker="o", 

1064 linestyle="none", 

1065 color="r", 

1066 label="$i<j$", 

1067 ) 

1068 plt.legend(loc="upper center", fontsize="x-large") 

1069 axb.set_xlabel(r"$\sqrt{i^2+j^2}$", fontsize="x-large") 

1070 axb.set_ylabel(r"$b_{ij}$", fontsize="x-large") 

1071 axb.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 

1072 axb.tick_params(axis="both", labelsize="x-large") 

1073 plt.tight_layout() 

1074 

1075 return [fig] 

1076 

1077 @staticmethod 

1078 def plotAcoeffsSum(aDict, bDict): 

1079 """Fig. 14. of Astier+19 

1080 

1081 Cumulative sum of a_ij as a function of maximum 

1082 separation. This plot displays the average over channels. 

1083 

1084 Parameters 

1085 ---------- 

1086 aDict : `dict` [`numpy.array`] 

1087 Dictionary keyed by amp names containing the fitted 'a' 

1088 coefficients from the model in Eq. 20 of Astier+19 (if 

1089 `ptcFitType` is `FULLCOVARIANCE`). 

1090 bDict : `dict` [`numpy.array`] 

1091 Dictionary keyed by amp names containing the fitted 'b' 

1092 coefficients from the model in Eq. 20 of Astier+19 (if 

1093 `ptcFitType` is `FULLCOVARIANCE`). 

1094 """ 

1095 assert len(aDict) == len(bDict) 

1096 a, b = [], [] 

1097 for amp in aDict: 

1098 if np.isnan(aDict[amp]).all() or np.isnan(bDict[amp]).all(): 

1099 continue 

1100 a.append(aDict[amp]) 

1101 b.append(bDict[amp]) 

1102 a = np.array(a).mean(axis=0) 

1103 b = np.array(b).mean(axis=0) 

1104 fig = plt.figure(figsize=(7, 6)) 

1105 w = 4 * np.ones_like(a) 

1106 w[0, 1:] = 2 

1107 w[1:, 0] = 2 

1108 w[0, 0] = 1 

1109 wa = w * a 

1110 indices = range(1, a.shape[0] + 1) 

1111 sums = [wa[0:n, 0:n].sum() for n in indices] 

1112 ax = plt.subplot(111) 

1113 ax.plot(indices, sums / sums[0], "o", color="b") 

1114 ax.set_yscale("log") 

1115 ax.set_xlim(indices[0] - 0.5, indices[-1] + 0.5) 

1116 ax.set_ylim(None, 1.2) 

1117 ax.set_ylabel( 

1118 r"$[\sum_{|i|<n\ &\ |j|<n} a_{ij}] / |a_{00}|$", fontsize="x-large" 

1119 ) 

1120 ax.set_xlabel("n", fontsize="x-large") 

1121 ax.tick_params(axis="both", labelsize="x-large") 

1122 plt.tight_layout() 

1123 

1124 return [fig] 

1125 

1126 @staticmethod 

1127 def plotRelativeBiasACoeffs( 

1128 aDict, 

1129 aDictNoB, 

1130 fullCovsModel, 

1131 fullCovsModelNoB, 

1132 signalElectrons, 

1133 gainDict, 

1134 maxr=None, 

1135 ): 

1136 """Fig. 15 in Astier+19. 

1137 

1138 Illustrates systematic bias from estimating 'a' 

1139 coefficients from the slope of correlations as opposed to the 

1140 full model in Astier+19. 

1141 

1142 Parameters 

1143 ---------- 

1144 aDict : `dict` 

1145 Dictionary of 'a' matrices (Eq. 20, Astier+19), with amp 

1146 names as keys. 

1147 aDictNoB : `dict` 

1148 Dictionary of 'a' matrices ('b'= 0 in Eq. 20, Astier+19), 

1149 with amp names as keys. 

1150 fullCovsModel : `dict` [`str`, `list`] 

1151 Dictionary keyed by amp names containing covariances model 

1152 per mean flux. 

1153 fullCovsModelNoB : `dict` [`str`, `list`] 

1154 Dictionary keyed by amp names containing covariances model 

1155 (with 'b'=0 in Eq. 20 of Astier+19) per mean flux. 

1156 signalElectrons : `float` 

1157 Signal at which to evaluate the a_ij coefficients. 

1158 gainDict : `dict` [`str`, `float`] 

1159 Dicgionary keyed by amp names with the gains in e-/ADU. 

1160 maxr : `int`, optional 

1161 Maximum lag. 

1162 """ 

1163 fig = plt.figure(figsize=(7, 11)) 

1164 title = [f"'a' relative bias at {signalElectrons} e", "'a' relative bias (b=0)"] 

1165 data = [(aDict, fullCovsModel), (aDictNoB, fullCovsModelNoB)] 

1166 

1167 for k, pair in enumerate(data): 

1168 diffs = [] 

1169 amean = [] 

1170 for amp in pair[0]: 

1171 covModel = np.array(pair[1][amp]) 

1172 if np.isnan(covModel).all(): 

1173 continue 

1174 # Compute the "a" coefficients of the Antilogus+14 

1175 # (1402.0725) model as in Guyonnet+15 (1501.01577, 

1176 # eq. 16, the slope of cov/var at a given flux mu in 

1177 # electrons). Eq. 16 of 1501.01577 is an approximation 

1178 # to the more complete model in Astier+19 

1179 # (1905.08677). 

1180 var = covModel[0, 0, 0] # ADU^2 

1181 # For a result in electrons^-1, we have to use mu in electrons 

1182 aOld = covModel[0, :, :] / (var * signalElectrons) 

1183 a = pair[0][amp] 

1184 amean.append(a) 

1185 diffs.append((aOld - a)) 

1186 amean = np.array(amean).mean(axis=0) 

1187 diff = np.array(diffs).mean(axis=0) 

1188 diff = diff / amean 

1189 diff = diff[:] 

1190 # The difference should be close to zero 

1191 diff[0, 0] = 0 

1192 if maxr is None: 

1193 maxr = diff.shape[0] 

1194 diff = diff[:maxr, :maxr] 

1195 ax0 = fig.add_subplot(2, 1, k + 1) 

1196 im0 = ax0.imshow(diff.transpose(), origin="lower") 

1197 ax0.yaxis.set_major_locator(MaxNLocator(integer=True)) 

1198 ax0.xaxis.set_major_locator(MaxNLocator(integer=True)) 

1199 ax0.tick_params(axis="both", labelsize="x-large") 

1200 plt.colorbar(im0) 

1201 ax0.set_title(title[k]) 

1202 

1203 plt.tight_layout() 

1204 

1205 return [fig] 

1206 

1207 def _plotStandardPtc(self, dataset): 

1208 """Plot PTC (linear and log scales ) and var/signal 

1209 vs signal per amplifier. 

1210 

1211 Parameters 

1212 ---------- 

1213 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

1214 The dataset containing the means, variances, exposure 

1215 times, and mask. 

1216 

1217 Returns 

1218 ------- 

1219 figDict : `dict` [`int`, `matplotlib.figure.Figure`] 

1220 Figure dictionary, keyd by an integer index: 

1221 0: ptcPlot1 ('ptcVarMean') 

1222 1: ptcPlot2 ('ptcVarMeanLog') 

1223 2: ptcPlot3 ('ptcNormalizedVar') 

1224 """ 

1225 ptcFitType = dataset.ptcFitType 

1226 if ptcFitType == "EXPAPPROXIMATION": 

1227 ptcFunc = funcAstier 

1228 stringTitle = r"Var = $\frac{1}{2g^2a_{00}}(\exp (2a_{00} \mu g) - 1) + \frac{n_{00}}{g^2}$ " 

1229 elif ptcFitType == "POLYNOMIAL": 

1230 ptcFunc = funcPolynomial 

1231 for key in dataset.ptcFitPars: 

1232 deg = len(dataset.ptcFitPars[key]) - 1 

1233 break 

1234 stringTitle = r"Polynomial (degree: %g)" % (deg) 

1235 else: 

1236 raise RuntimeError( 

1237 f"The input dataset had an invalid dataset.ptcFitType: {ptcFitType}. \n" 

1238 "Options: 'EXPAPPROXIMATION' or 'POLYNOMIAL'." 

1239 ) 

1240 

1241 legendFontSize = 6.5 

1242 labelFontSize = 8 

1243 titleFontSize = 9 

1244 supTitleFontSize = 18 

1245 markerSize = 25 

1246 

1247 # General determination of the size of the plot grid 

1248 nAmps = len(dataset.ampNames) 

1249 if nAmps == 2: 

1250 nRows, nCols = 2, 1 

1251 nRows = np.sqrt(nAmps) 

1252 mantissa, _ = np.modf(nRows) 

1253 if mantissa > 0: 

1254 nRows = int(nRows) + 1 

1255 nCols = nRows 

1256 else: 

1257 nRows = int(nRows) 

1258 nCols = nRows 

1259 

1260 f, ax = plt.subplots( 

1261 nrows=nRows, ncols=nCols, sharex="col", sharey="row", figsize=(13, 10) 

1262 ) 

1263 f2, ax2 = plt.subplots( 

1264 nrows=nRows, ncols=nCols, sharex="col", sharey="row", figsize=(13, 10) 

1265 ) 

1266 f3, ax3 = plt.subplots( 

1267 nrows=nRows, ncols=nCols, sharex="col", sharey="row", figsize=(13, 10) 

1268 ) 

1269 

1270 for i, (amp, a, a2, a3) in enumerate( 

1271 zip(dataset.ampNames, ax.flatten(), ax2.flatten(), ax3.flatten()) 

1272 ): 

1273 meanVecOriginal = np.ravel(np.array(dataset.rawMeans[amp])) 

1274 varVecOriginal = np.ravel(np.array(dataset.rawVars[amp])) 

1275 mask = np.ravel(np.array(dataset.expIdMask[amp])) 

1276 if np.sum(mask) == 0: # The whole amp is bad 

1277 a.set_title(f"{amp} (BAD)", fontsize=titleFontSize) 

1278 a2.set_title(f"{amp} (BAD)", fontsize=titleFontSize) 

1279 a3.set_title(f"{amp} (BAD)", fontsize=titleFontSize) 

1280 continue 

1281 else: 

1282 mask = mask.astype(bool) 

1283 meanVecFinal = meanVecOriginal[mask] 

1284 varVecFinal = varVecOriginal[mask] 

1285 meanVecOutliers = meanVecOriginal[np.invert(mask)] 

1286 varVecOutliers = varVecOriginal[np.invert(mask)] 

1287 pars, parsErr = np.array(dataset.ptcFitPars[amp]), np.array( 

1288 dataset.ptcFitParsError[amp] 

1289 ) 

1290 ptcRedChi2 = dataset.ptcFitChiSq[amp] 

1291 if ptcFitType == "EXPAPPROXIMATION": 

1292 if len(meanVecFinal): 

1293 ptcA00, ptcA00error = pars[0], parsErr[0] 

1294 ptcGain, ptcGainError = pars[1], parsErr[1] 

1295 ptcNoise = np.sqrt((pars[2])) # pars[2] is in (e-)^2 

1296 ptcNoiseAdu = ptcNoise * (1.0 / ptcGain) 

1297 ptcNoiseError = ( 

1298 0.5 

1299 * (parsErr[2] / np.fabs(pars[2])) 

1300 * np.sqrt(np.fabs(pars[2])) 

1301 ) 

1302 stringLegend = ( 

1303 f"a00: {ptcA00:.2e}+/-{ptcA00error:.2e} 1/e" 

1304 f"\nGain: {ptcGain:.4}+/-{ptcGainError:.2e} e/ADU" 

1305 f"\nNoise: {ptcNoise:.4}+/-{ptcNoiseError:.2e} e\n" 

1306 r"$\chi^2_{\rm{red}}$: " + f"{ptcRedChi2:.4}" 

1307 f"\nLast in fit: {meanVecFinal[-1]:.7} ADU " 

1308 ) 

1309 

1310 if ptcFitType == "POLYNOMIAL": 

1311 if len(meanVecFinal): 

1312 ptcGain, ptcGainError = 1.0 / pars[1], np.fabs(1.0 / pars[1]) * ( 

1313 parsErr[1] / pars[1] 

1314 ) 

1315 ptcNoiseAdu = np.sqrt((pars[0])) # pars[0] is in ADU^2 

1316 ptcNoise = ptcNoiseAdu * ptcGain 

1317 ptcNoiseError = ( 

1318 0.5 

1319 * (parsErr[0] / np.fabs(pars[0])) 

1320 * (np.sqrt(np.fabs(pars[0]))) 

1321 ) * ptcGain 

1322 stringLegend = ( 

1323 f"Gain: {ptcGain:.4}+/-{ptcGainError:.2e} e/ADU\n" 

1324 f"Noise: {ptcNoise:.4}+/-{ptcNoiseError:.2e} e\n" 

1325 r"$\chi^2_{\rm{red}}$: " + f"{ptcRedChi2:.4}" 

1326 f"\nLast in fit: {meanVecFinal[-1]:.7} ADU " 

1327 ) 

1328 

1329 a.set_xlabel(r"Mean signal ($\mu$, ADU)", fontsize=labelFontSize) 

1330 a.set_ylabel(r"Variance (ADU$^2$)", fontsize=labelFontSize) 

1331 a.tick_params(labelsize=11) 

1332 a.set_xscale("linear") 

1333 a.set_yscale("linear") 

1334 

1335 a2.set_xlabel(r"Mean Signal ($\mu$, ADU)", fontsize=labelFontSize) 

1336 a2.set_ylabel(r"Variance (ADU$^2$)", fontsize=labelFontSize) 

1337 a2.tick_params(labelsize=11) 

1338 a2.set_xscale("log") 

1339 a2.set_yscale("log") 

1340 

1341 a3.set_xlabel(r"Mean signal ($\mu$, ADU)", fontsize=labelFontSize) 

1342 a3.set_ylabel(r"Variance/$\mu$ (ADU)", fontsize=labelFontSize) 

1343 a3.tick_params(labelsize=11) 

1344 a3.set_xscale("log") 

1345 a3.set_yscale("linear") 

1346 minMeanVecFinal = np.nanmin(meanVecFinal) 

1347 maxMeanVecFinal = np.nanmax(meanVecFinal) 

1348 meanVecFit = np.linspace( 

1349 minMeanVecFinal, maxMeanVecFinal, 100 * len(meanVecFinal) 

1350 ) 

1351 minMeanVecOriginal = np.nanmin(meanVecOriginal) 

1352 maxMeanVecOriginal = np.nanmax(meanVecOriginal) 

1353 deltaXlim = maxMeanVecOriginal - minMeanVecOriginal 

1354 a.plot(meanVecFit, ptcFunc(pars, meanVecFit), color="red") 

1355 a.scatter(meanVecFinal, varVecFinal, c="blue", marker="o", s=markerSize) 

1356 a.scatter( 

1357 meanVecOutliers, varVecOutliers, c="magenta", marker="s", s=markerSize 

1358 ) 

1359 a.text( 

1360 0.03, 

1361 0.66, 

1362 stringLegend, 

1363 transform=a.transAxes, 

1364 fontsize=legendFontSize, 

1365 ) 

1366 a.set_title(amp, fontsize=titleFontSize) 

1367 a.set_xlim([minMeanVecOriginal - 0.2*deltaXlim, maxMeanVecOriginal + 0.2*deltaXlim]) 

1368 

1369 # Same, but in log-log scale 

1370 a2.plot(meanVecFit, ptcFunc(pars, meanVecFit), color="red") 

1371 a2.scatter(meanVecFinal, varVecFinal, c="blue", marker="o", s=markerSize) 

1372 a2.scatter( 

1373 meanVecOutliers, varVecOutliers, c="magenta", marker="s", s=markerSize 

1374 ) 

1375 a2.text( 

1376 0.03, 

1377 0.66, 

1378 stringLegend, 

1379 transform=a2.transAxes, 

1380 fontsize=legendFontSize, 

1381 ) 

1382 a2.set_title(amp, fontsize=titleFontSize) 

1383 a2.set_xlim([minMeanVecOriginal, maxMeanVecOriginal]) 

1384 

1385 # Var/mu vs mu 

1386 a3.plot(meanVecFit, ptcFunc(pars, meanVecFit) / meanVecFit, color="red") 

1387 a3.scatter( 

1388 meanVecFinal, 

1389 varVecFinal / meanVecFinal, 

1390 c="blue", 

1391 marker="o", 

1392 s=markerSize, 

1393 ) 

1394 a3.scatter( 

1395 meanVecOutliers, 

1396 varVecOutliers / meanVecOutliers, 

1397 c="magenta", 

1398 marker="s", 

1399 s=markerSize, 

1400 ) 

1401 a3.text( 

1402 0.05, 0.1, stringLegend, transform=a3.transAxes, fontsize=legendFontSize 

1403 ) 

1404 a3.set_title(amp, fontsize=titleFontSize) 

1405 a3.set_xlim( 

1406 [ 

1407 minMeanVecOriginal - 0.2 * deltaXlim, 

1408 maxMeanVecOriginal + 0.2 * deltaXlim, 

1409 ] 

1410 ) 

1411 f.suptitle("PTC \n Fit: " + stringTitle, fontsize=supTitleFontSize) 

1412 f2.suptitle("PTC (log-log)", fontsize=supTitleFontSize) 

1413 f3.suptitle(r"Var/$\mu$", fontsize=supTitleFontSize) 

1414 

1415 f.tight_layout() 

1416 f2.tight_layout() 

1417 f3.tight_layout() 

1418 

1419 figDict = {0: f, 1: f2, 2: f3} 

1420 

1421 return figDict 

1422 

1423 @staticmethod 

1424 def indexForBins(x, nBins): 

1425 """Builds an index with regular binning. The result can be fed into 

1426 binData. 

1427 

1428 Parameters 

1429 ---------- 

1430 x : `numpy.array` 

1431 Data to bin. 

1432 nBins : `int` 

1433 Number of bin. 

1434 

1435 Returns 

1436 ------- 

1437 np.digitize(x, bins): `numpy.array` 

1438 Bin indices. 

1439 """ 

1440 bins = np.linspace(x.min(), x.max() + abs(x.max() * 1e-7), nBins + 1) 

1441 return np.digitize(x, bins) 

1442 

1443 @staticmethod 

1444 def binData(x, y, binIndex, wy=None): 

1445 """Bin data (usually for display purposes). 

1446 

1447 Parameters 

1448 ---------- 

1449 x : `numpy.array` 

1450 Data to bin. 

1451 y : `numpy.array` 

1452 Data to bin. 

1453 binIdex : `list` 

1454 Bin number of each datum. 

1455 wy : `numpy.array` 

1456 Inverse rms of each datum to use when averaging (the 

1457 actual weight is wy**2). 

1458 

1459 Returns 

1460 ------- 

1461 xbin : `numpy.array` 

1462 Binned data in x. 

1463 ybin : `numpy.array` 

1464 Binned data in y. 

1465 wybin : `numpy.array` 

1466 Binned weights in y, computed from wy's in each bin. 

1467 sybin : `numpy.array` 

1468 Uncertainty on the bin average, considering actual 

1469 scatter, and ignoring weights. 

1470 """ 

1471 if wy is None: 

1472 wy = np.ones_like(x) 

1473 binIndexSet = set(binIndex) 

1474 w2 = wy * wy 

1475 xw2 = x * (w2) 

1476 xbin = np.array( 

1477 [xw2[binIndex == i].sum() / w2[binIndex == i].sum() for i in binIndexSet] 

1478 ) 

1479 

1480 yw2 = y * w2 

1481 ybin = np.array( 

1482 [yw2[binIndex == i].sum() / w2[binIndex == i].sum() for i in binIndexSet] 

1483 ) 

1484 

1485 wybin = np.sqrt(np.array([w2[binIndex == i].sum() for i in binIndexSet])) 

1486 sybin = np.array( 

1487 [ 

1488 y[binIndex == i].std() / np.sqrt(np.array([binIndex == i]).sum()) 

1489 for i in binIndexSet 

1490 ] 

1491 ) 

1492 

1493 return xbin, ybin, wybin, sybin