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

529 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-12 12:06 -0700

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 

33from lsst.cp.pipe._lookupStaticCalibration import lookupStaticCalibration 

34 

35from lsst.cp.pipe.utils import ( 

36 funcAstier, 

37 funcPolynomial, 

38 calculateWeightedReducedChi2, 

39 getFitDataFromCovariances, 

40) 

41from matplotlib.ticker import MaxNLocator 

42 

43 

44class PlotPhotonTransferCurveConnections( 

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

46): 

47 inputPtcDataset = cT.Input( 

48 name="calib", 

49 doc="Input PTC dataset.", 

50 storageClass="PhotonTransferCurveDataset", 

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

52 isCalibration=True, 

53 ) 

54 camera = cT.PrerequisiteInput( 

55 name="camera", 

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

57 storageClass="Camera", 

58 dimensions=("instrument",), 

59 isCalibration=True, 

60 lookupFunction=lookupStaticCalibration, 

61 ) 

62 # ptcFitType = "FULLCOVARIANCE" produces 12 plots 

63 ptcPlot1 = cT.Output( 

64 name="ptcVarMean", 

65 doc="Variance vs mean.", 

66 storageClass="Plot", 

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

68 ) 

69 ptcPlot2 = cT.Output( 

70 name="ptcVarMeanLog", 

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

72 storageClass="Plot", 

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

74 ) 

75 ptcPlot3 = cT.Output( 

76 name="ptcNormalizedVar", 

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

78 storageClass="Plot", 

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

80 ) 

81 ptcPlot4 = cT.Output( 

82 name="ptcCov01Mean", 

83 doc="Cov01 vs mean.", 

84 storageClass="Plot", 

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

86 ) 

87 ptcPlot5 = cT.Output( 

88 name="ptcCov10Mean", 

89 doc="Cov10 vs mean.", 

90 storageClass="Plot", 

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

92 ) 

93 ptcPlot6 = cT.Output( 

94 name="ptcVarResiduals", 

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

96 storageClass="Plot", 

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

98 ) 

99 ptcPlot7 = cT.Output( 

100 name="ptcNormalizedCov01", 

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

102 storageClass="Plot", 

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

104 ) 

105 ptcPlot8 = cT.Output( 

106 name="ptcNormalizedCov10", 

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

108 storageClass="Plot", 

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

110 ) 

111 ptcPlot9 = cT.Output( 

112 name="ptcAandBMatrices", 

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

114 storageClass="Plot", 

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

116 ) 

117 ptcPlot10 = cT.Output( 

118 name="ptcAandBDistance", 

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

120 storageClass="Plot", 

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

122 ) 

123 ptcPlot11 = cT.Output( 

124 name="ptcACumulativeSum", 

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

126 storageClass="Plot", 

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

128 ) 

129 ptcPlot12 = cT.Output( 

130 name="ptcARelativeBias", 

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

132 storageClass="Plot", 

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

134 ) 

135 

136 

137class PlotPhotonTransferCurveConfig( 

138 pipeBase.PipelineTaskConfig, pipelineConnections=PlotPhotonTransferCurveConnections 

139): 

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

141 

142 signalElectronsRelativeA = pexConfig.Field( 

143 dtype=float, 

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

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

146 default=75000.0, 

147 ) 

148 plotNormalizedCovariancesNumberOfBins = pexConfig.Field( 

149 dtype=int, 

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

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

152 default=10, 

153 ) 

154 

155 

156class PlotPhotonTransferCurveTask(pipeBase.PipelineTask): 

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

158 

159 Parameters 

160 ---------- 

161 outDir : `str`, optional 

162 Path to the output directory where the final PDF will 

163 be placed. 

164 

165 signalElectronsRelativeA : `float`, optional 

166 Signal value for relative systematic bias between different 

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

168 

169 plotNormalizedCovariancesNumberOfBins : `float`, optional 

170 Number of bins in `plotNormalizedCovariancesNumber` function 

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

172 

173 Notes 

174 ----- 

175 See DM-36388 for usage exammple. 

176 """ 

177 

178 ConfigClass = PlotPhotonTransferCurveConfig 

179 _DefaultName = "cpPlotPtc" 

180 

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

182 inputs = butlerQC.get(inputRefs) 

183 outputs = self.run(**inputs) 

184 butlerQC.put(outputs, outputRefs) 

185 

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

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

188 

189 Parameters 

190 ---------- 

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

192 Output dataset from Photon Transfer Curve task. 

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

194 Camera to use for camera geometry information. 

195 """ 

196 ptcFitType = inputPtcDataset.ptcFitType 

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

198 

199 if ptcFitType in [ 

200 "FULLCOVARIANCE", 

201 ]: 

202 figDict = self._covAstierMakeAllPlots(inputPtcDataset) 

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

204 figDict = self._plotStandardPtc(inputPtcDataset) 

205 else: 

206 raise RuntimeError( 

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

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

209 ) 

210 

211 maxNumberPlots = 12 

212 if len(figDict) < maxNumberPlots: 

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

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

215 

216 return pipeBase.Struct( 

217 ptcPlot1=figDict[0], 

218 ptcPlot2=figDict[1], 

219 ptcPlot3=figDict[2], 

220 ptcPlot4=figDict[3], 

221 ptcPlot5=figDict[4], 

222 ptcPlot6=figDict[5], 

223 ptcPlot7=figDict[6], 

224 ptcPlot8=figDict[7], 

225 ptcPlot9=figDict[8], 

226 ptcPlot10=figDict[9], 

227 ptcPlot11=figDict[10], 

228 ptcPlot12=figDict[11], 

229 ) 

230 

231 def _covAstierMakeAllPlots(self, dataset): 

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

233 

234 This function call other functions that mostly reproduce the 

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

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

237 

238 Parameters 

239 ---------- 

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

241 The dataset containing the necessary information to 

242 produce the plots. 

243 

244 Returns 

245 ------- 

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

247 Figure dictionary, keyd by an integer index: 

248 0: ptcPlot1 ('ptcVarMean') 

249 1: ptcPlot2 ('ptcVarMeanLog') 

250 2: ptcPlot3 ('ptcNormalizedVar') 

251 3: ptcPlot4 ('ptcCov01Mean') 

252 4: ptcPlot5 ('ptcCov10Mean') 

253 5: ptcPlot6 ('ptcVarResiduals') 

254 6: ptcPlot7 ('ptcNormalizedCov01') 

255 7: ptcPlot8 ('ptcNormalizedCov10') 

256 8: ptcPlot9 ('ptcAandBDistance') 

257 9: ptcPlot10 ('ptcAandBDistance') 

258 10: ptcPlot11 ('ptcACumulativeSum') 

259 11: ptcPlot12 ('ptcARelativeBias') 

260 """ 

261 mu = dataset.finalMeans 

262 # dictionaries with ampNames as keys 

263 fullCovs = dataset.covariances 

264 fullCovsModel = dataset.covariancesModel 

265 fullCovWeights = dataset.covariancesSqrtWeights 

266 aDict = dataset.aMatrix 

267 bDict = dataset.bMatrix 

268 fullCovWeights = dataset.covariancesSqrtWeights 

269 aDict = dataset.aMatrix 

270 bDict = dataset.bMatrix 

271 fullCovWeights = dataset.covariancesSqrtWeights 

272 aDict = dataset.aMatrix 

273 bDict = dataset.bMatrix 

274 fullCovWeights = dataset.covariancesSqrtWeights 

275 aDict = dataset.aMatrix 

276 bDict = dataset.bMatrix 

277 fullCovsNoB = dataset.covariances 

278 fullCovsModelNoB = dataset.covariancesModelNoB 

279 fullCovWeightsNoB = dataset.covariancesSqrtWeights 

280 aDictNoB = dataset.aMatrixNoB 

281 gainDict = dataset.gain 

282 noiseDict = dataset.noise 

283 

284 figList1 = self.plotCovariances( 

285 mu, 

286 fullCovs, 

287 fullCovsModel, 

288 fullCovWeights, 

289 fullCovsNoB, 

290 fullCovsModelNoB, 

291 fullCovWeightsNoB, 

292 gainDict, 

293 noiseDict, 

294 aDict, 

295 bDict, 

296 ) 

297 figList2 = self.plotNormalizedCovariances( 

298 0, 

299 0, 

300 mu, 

301 fullCovs, 

302 fullCovsModel, 

303 fullCovWeights, 

304 fullCovsNoB, 

305 fullCovsModelNoB, 

306 fullCovWeightsNoB, 

307 offset=0.01, 

308 topPlot=True, 

309 numberOfBins=self.config.plotNormalizedCovariancesNumberOfBins, 

310 ) 

311 figList3 = self.plotNormalizedCovariances( 

312 0, 

313 1, 

314 mu, 

315 fullCovs, 

316 fullCovsModel, 

317 fullCovWeights, 

318 fullCovsNoB, 

319 fullCovsModelNoB, 

320 fullCovWeightsNoB, 

321 numberOfBins=self.config.plotNormalizedCovariancesNumberOfBins, 

322 ) 

323 figList4 = self.plotNormalizedCovariances( 

324 1, 

325 0, 

326 mu, 

327 fullCovs, 

328 fullCovsModel, 

329 fullCovWeights, 

330 fullCovsNoB, 

331 fullCovsModelNoB, 

332 fullCovWeightsNoB, 

333 numberOfBins=self.config.plotNormalizedCovariancesNumberOfBins, 

334 ) 

335 figList5 = self.plot_a_b(aDict, bDict) 

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

337 figList7 = self.plotAcoeffsSum(aDict, bDict) 

338 figList8 = self.plotRelativeBiasACoeffs( 

339 aDict, 

340 aDictNoB, 

341 fullCovsModel, 

342 fullCovsModelNoB, 

343 self.config.signalElectronsRelativeA, 

344 gainDict, 

345 maxr=4, 

346 ) 

347 

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

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

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

351 # matches when FITTYPE is FULLCOVARIANCE or 

352 # [EXPAPPROXIMATION, POLYNOMIAL] 

353 temp = figList1[2] 

354 figList1[2] = figList2[0] 

355 figList2 = [temp] 

356 

357 figList = ( 

358 figList1 

359 + figList2 

360 + figList3 

361 + figList4 

362 + figList5 

363 + figList6 

364 + figList7 

365 + figList8 

366 ) 

367 

368 figDict = {} 

369 for i, fig in enumerate(figList): 

370 figDict[i] = fig 

371 

372 return figDict 

373 

374 @staticmethod 

375 def plotCovariances( 

376 mu, 

377 covs, 

378 covsModel, 

379 covsWeights, 

380 covsNoB, 

381 covsModelNoB, 

382 covsWeightsNoB, 

383 gainDict, 

384 noiseDict, 

385 aDict, 

386 bDict, 

387 ): 

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

389 

390 Figs. 6 and 7 of Astier+19 

391 

392 Parameters 

393 ---------- 

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

395 Dictionary keyed by amp name with mean signal values. 

396 

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

398 Dictionary keyed by amp names containing a list of measued 

399 covariances per mean flux. 

400 

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

402 Dictionary keyed by amp names containinging covariances 

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

404 

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

406 Dictionary keyed by amp names containinging sqrt. of 

407 covariances weights. 

408 

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

410 Dictionary keyed by amp names containing a list of measued 

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

412 

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

414 Dictionary keyed by amp names containing covariances model 

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

416 

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

418 Dictionary keyed by amp names containing sqrt. of 

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

420 

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

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

423 

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

425 Dictionary keyed by amp names containing the rms redout 

426 noise in e-. 

427 

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

429 Dictionary keyed by amp names containing 'a' coefficients 

430 (Eq. 20 of Astier+19). 

431 

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

433 Dictionary keyed by amp names containing 'b' coefficients 

434 (Eq. 20 of Astier+19). 

435 """ 

436 legendFontSize = 6.5 

437 labelFontSize = 7 

438 titleFontSize = 9 

439 supTitleFontSize = 18 

440 markerSize = 25 

441 

442 nAmps = len(covs) 

443 if nAmps == 2: 

444 nRows, nCols = 2, 1 

445 nRows = np.sqrt(nAmps) 

446 mantissa, _ = np.modf(nRows) 

447 if mantissa > 0: 

448 nRows = int(nRows) + 1 

449 nCols = nRows 

450 else: 

451 nRows = int(nRows) 

452 nCols = nRows 

453 

454 f, ax = plt.subplots( 

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

456 ) 

457 f2, ax2 = plt.subplots( 

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

459 ) 

460 fResCov00, axResCov00 = plt.subplots( 

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

462 ) 

463 fCov01, axCov01 = plt.subplots( 

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

465 ) 

466 fCov10, axCov10 = plt.subplots( 

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

468 ) 

469 

470 assert len(covsModel) == nAmps 

471 assert len(covsWeights) == nAmps 

472 

473 assert len(covsNoB) == nAmps 

474 assert len(covsModelNoB) == nAmps 

475 assert len(covsWeightsNoB) == nAmps 

476 

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

478 zip( 

479 covs, 

480 ax.flatten(), 

481 ax2.flatten(), 

482 axResCov00.flatten(), 

483 axCov01.flatten(), 

484 axCov10.flatten(), 

485 ) 

486 ): 

487 

488 muAmp, cov, model, weight = ( 

489 mu[amp], 

490 covs[amp], 

491 covsModel[amp], 

492 covsWeights[amp], 

493 ) 

494 if not np.isnan( 

495 np.array(cov) 

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

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

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

499 ( 

500 meanVecFinal, 

501 varVecFinal, 

502 varVecModelFinal, 

503 varWeightsFinal, 

504 _, 

505 ) = getFitDataFromCovariances( 

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

507 ) 

508 

509 # Get weighted reduced chi2 

510 chi2FullModelVar = calculateWeightedReducedChi2( 

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

512 ) 

513 

514 ( 

515 meanVecFinalCov01, 

516 varVecFinalCov01, 

517 varVecModelFinalCov01, 

518 _, 

519 _, 

520 ) = getFitDataFromCovariances( 

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

522 ) 

523 

524 ( 

525 meanVecFinalCov10, 

526 varVecFinalCov10, 

527 varVecModelFinalCov10, 

528 _, 

529 _, 

530 ) = getFitDataFromCovariances( 

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

532 ) 

533 

534 # cuadratic fit for residuals below 

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

536 varModelFinalQuadratic = np.polyval(par2, meanVecFinal) 

537 chi2QuadModelVar = calculateWeightedReducedChi2( 

538 varVecFinal, 

539 varModelFinalQuadratic, 

540 varWeightsFinal, 

541 len(meanVecFinal), 

542 3, 

543 ) 

544 

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

546 covNoB, modelNoB, weightNoB = ( 

547 covsNoB[amp], 

548 covsModelNoB[amp], 

549 covsWeightsNoB[amp], 

550 ) 

551 ( 

552 meanVecFinalNoB, 

553 varVecFinalNoB, 

554 varVecModelFinalNoB, 

555 varWeightsFinalNoB, 

556 _, 

557 ) = getFitDataFromCovariances( 

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

559 ) 

560 

561 chi2FullModelNoBVar = calculateWeightedReducedChi2( 

562 varVecFinalNoB, 

563 varVecModelFinalNoB, 

564 varWeightsFinalNoB, 

565 len(meanVecFinalNoB), 

566 3, 

567 ) 

568 stringLegend = ( 

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

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

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

572 + "\n" 

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

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

575 ) 

576 minMeanVecFinal = np.nanmin(meanVecFinal) 

577 maxMeanVecFinal = np.nanmax(meanVecFinal) 

578 deltaXlim = maxMeanVecFinal - minMeanVecFinal 

579 

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

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

582 a.tick_params(labelsize=11) 

583 a.set_xscale("linear") 

584 a.set_yscale("linear") 

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

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

587 a.text( 

588 0.03, 

589 0.7, 

590 stringLegend, 

591 transform=a.transAxes, 

592 fontsize=legendFontSize, 

593 ) 

594 a.set_title(amp, fontsize=titleFontSize) 

595 a.set_xlim( 

596 [ 

597 minMeanVecFinal - 0.2 * deltaXlim, 

598 maxMeanVecFinal + 0.2 * deltaXlim, 

599 ] 

600 ) 

601 

602 # Same as above, but in log-scale 

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

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

605 a2.tick_params(labelsize=11) 

606 a2.set_xscale("log") 

607 a2.set_yscale("log") 

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

609 a2.scatter( 

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

611 ) 

612 a2.text( 

613 0.03, 

614 0.7, 

615 stringLegend, 

616 transform=a2.transAxes, 

617 fontsize=legendFontSize, 

618 ) 

619 a2.set_title(amp, fontsize=titleFontSize) 

620 a2.set_xlim([minMeanVecFinal, maxMeanVecFinal]) 

621 

622 # Residuals var - model 

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

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

625 aResVar.tick_params(labelsize=11) 

626 aResVar.set_xscale("linear") 

627 aResVar.set_yscale("linear") 

628 aResVar.plot( 

629 meanVecFinal, 

630 varVecFinal - varVecModelFinal, 

631 color="blue", 

632 linestyle="-", 

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

634 ) 

635 aResVar.plot( 

636 meanVecFinal, 

637 varVecFinal - varModelFinalQuadratic, 

638 color="red", 

639 linestyle="-", 

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

641 ) 

642 aResVar.plot( 

643 meanVecFinalNoB, 

644 varVecFinalNoB - varVecModelFinalNoB, 

645 color="green", 

646 linestyle="-", 

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

648 % chi2FullModelNoBVar, 

649 ) 

650 aResVar.axhline(color="black") 

651 aResVar.set_title(amp, fontsize=titleFontSize) 

652 aResVar.set_xlim( 

653 [ 

654 minMeanVecFinal - 0.2 * deltaXlim, 

655 maxMeanVecFinal + 0.2 * deltaXlim, 

656 ] 

657 ) 

658 aResVar.legend(fontsize=7) 

659 

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

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

662 a3.tick_params(labelsize=11) 

663 a3.set_xscale("linear") 

664 a3.set_yscale("linear") 

665 a3.scatter( 

666 meanVecFinalCov01, 

667 varVecFinalCov01, 

668 c="blue", 

669 marker="o", 

670 s=markerSize, 

671 ) 

672 a3.plot( 

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

674 ) 

675 a3.set_title(amp, fontsize=titleFontSize) 

676 a3.set_xlim( 

677 [ 

678 minMeanVecFinal - 0.2 * deltaXlim, 

679 maxMeanVecFinal + 0.2 * deltaXlim, 

680 ] 

681 ) 

682 

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

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

685 a4.tick_params(labelsize=11) 

686 a4.set_xscale("linear") 

687 a4.set_yscale("linear") 

688 a4.scatter( 

689 meanVecFinalCov10, 

690 varVecFinalCov10, 

691 c="blue", 

692 marker="o", 

693 s=markerSize, 

694 ) 

695 a4.plot( 

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

697 ) 

698 a4.set_title(amp, fontsize=titleFontSize) 

699 a4.set_xlim( 

700 [ 

701 minMeanVecFinal - 0.2 * deltaXlim, 

702 maxMeanVecFinal + 0.2 * deltaXlim, 

703 ] 

704 ) 

705 

706 else: 

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

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

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

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

711 

712 f.suptitle( 

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

714 fontsize=supTitleFontSize, 

715 ) 

716 f2.suptitle( 

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

718 fontsize=supTitleFontSize, 

719 ) 

720 fResCov00.suptitle( 

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

722 ) 

723 fCov01.suptitle( 

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

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

726 fontsize=supTitleFontSize, 

727 ) 

728 fCov10.suptitle( 

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

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

731 fontsize=supTitleFontSize, 

732 ) 

733 

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

735 

736 def plotNormalizedCovariances( 

737 self, 

738 i, 

739 j, 

740 inputMu, 

741 covs, 

742 covsModel, 

743 covsWeights, 

744 covsNoB, 

745 covsModelNoB, 

746 covsWeightsNoB, 

747 offset=0.004, 

748 numberOfBins=10, 

749 plotData=True, 

750 topPlot=False, 

751 ): 

752 """Plot C_ij/mu vs mu. 

753 

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

755 

756 Parameters 

757 ---------- 

758 i : `int` 

759 Covariance lag. 

760 j : `int` 

761 Covariance lag. 

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

763 Dictionary keyed by amp name with mean signal values. 

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

765 Dictionary keyed by amp names containing a list of measued 

766 covariances per mean flux. 

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

768 Dictionary keyed by amp names containinging covariances 

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

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

771 Dictionary keyed by amp names containinging sqrt. of 

772 covariances weights. 

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

774 Dictionary keyed by amp names containing a list of measued 

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

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

777 Dictionary keyed by amp names containing covariances model 

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

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

780 Dictionary keyed by amp names containing sqrt. of 

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

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

783 Dictionary keyed by amp names containing the masked 

784 exposure pairs. 

785 offset : `float`, optional 

786 Constant offset factor to plot covariances in same panel 

787 (so they don't overlap). 

788 numberOfBins : `int`, optional 

789 Number of bins for top and bottom plot. 

790 plotData : `bool`, optional 

791 Plot the data points? 

792 topPlot : `bool`, optional 

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

794 plot with the model residuals? 

795 """ 

796 if not topPlot: 

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

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

799 gs.update(hspace=0) 

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

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

802 else: 

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

804 ax0 = plt.subplot(111) 

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

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

807 mue, rese, wce = [], [], [] 

808 mueNoB, reseNoB, wceNoB = [], [], [] 

809 for counter, amp in enumerate(covs): 

810 muAmp, fullCov, fullCovModel, fullCovWeight = ( 

811 inputMu[amp], 

812 covs[amp], 

813 covsModel[amp], 

814 covsWeights[amp], 

815 ) 

816 if len(fullCov) == 0: 

817 continue 

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

819 i, 

820 j, 

821 muAmp, 

822 fullCov, 

823 fullCovModel, 

824 fullCovWeight, 

825 divideByMu=True, 

826 returnMasked=True, 

827 ) 

828 

829 mue += list(mu) 

830 rese += list(cov - model) 

831 wce += list(weightCov) 

832 

833 fullCovNoB, fullCovModelNoB, fullCovWeightNoB = ( 

834 covsNoB[amp], 

835 covsModelNoB[amp], 

836 covsWeightsNoB[amp], 

837 ) 

838 if len(fullCovNoB) == 0: 

839 continue 

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

841 i, 

842 j, 

843 muAmp, 

844 fullCovNoB, 

845 fullCovModelNoB, 

846 fullCovWeightNoB, 

847 divideByMu=True, 

848 returnMasked=True, 

849 ) 

850 

851 mueNoB += list(muNoB) 

852 reseNoB += list(covNoB - modelNoB) 

853 wceNoB += list(weightCovNoB) 

854 

855 # the corresponding fit 

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

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

858 gind = self.indexForBins(mu, numberOfBins) 

859 

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

861 plt.errorbar( 

862 xb, 

863 yb + counter * offset, 

864 yerr=sigyb, 

865 marker="o", 

866 linestyle="none", 

867 markersize=6.5, 

868 color=fit_curve.get_color(), 

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

870 ) 

871 # plot the data 

872 if plotData: 

873 (points,) = plt.plot( 

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

875 ) 

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

877 # end loop on amps 

878 mue = np.array(mue) 

879 rese = np.array(rese) 

880 wce = np.array(wce) 

881 mueNoB = np.array(mueNoB) 

882 reseNoB = np.array(reseNoB) 

883 wceNoB = np.array(wceNoB) 

884 

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

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

887 if not topPlot: 

888 gind = self.indexForBins(mue, numberOfBins) 

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

890 

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

892 ax1.errorbar( 

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

894 ) 

895 gindNoB = self.indexForBins(mueNoB, numberOfBins) 

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

897 

898 ax1.errorbar( 

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

900 ) 

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

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

903 # horizontal line at zero 

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

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

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

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

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

909 plt.tight_layout() 

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

911 # overlapping y labels: 

912 fig.canvas.draw() 

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

914 labels0[0] = "" 

915 ax0.set_yticklabels(labels0) 

916 

917 return [fig] 

918 

919 @staticmethod 

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

921 """Fig. 12 of Astier+19 

922 

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

924 

925 Parameters 

926 ---------- 

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

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

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

930 `ptcFitType` is `FULLCOVARIANCE`). 

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

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

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

934 `ptcFitType` is `FULLCOVARIANCE`). 

935 bRange : `int` 

936 Maximum lag for b arrays. 

937 """ 

938 a, b = [], [] 

939 for amp in aDict: 

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

941 continue 

942 a.append(aDict[amp]) 

943 b.append(bDict[amp]) 

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

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

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

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

948 im0 = ax0.imshow( 

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

950 ) 

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

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

953 ax0.xaxis.set_ticks_position("bottom") 

954 cb0 = plt.colorbar(im0) 

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

956 

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

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

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

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

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

962 cb1 = plt.colorbar(im1) 

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

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

965 ax1.xaxis.set_ticks_position("bottom") 

966 plt.tight_layout() 

967 

968 return [fig] 

969 

970 @staticmethod 

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

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

973 

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

975 function of distance. 

976 

977 Parameters 

978 ---------- 

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

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

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

982 `ptcFitType` is `FULLCOVARIANCE`). 

983 

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

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

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

987 `ptcFitType` is `FULLCOVARIANCE`). 

988 bRange : `int` 

989 Maximum lag for b arrays. 

990 """ 

991 assert len(aDict) == len(bDict) 

992 a = [] 

993 for amp in aDict: 

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

995 continue 

996 a.append(aDict[amp]) 

997 a = np.array(a) 

998 y = a.mean(axis=0) 

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

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

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

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

1003 y = y.ravel() 

1004 sy = sy.ravel() 

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

1006 ax = fig.add_subplot(211) 

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

1008 ax.errorbar( 

1009 r[upper], 

1010 y[upper], 

1011 yerr=sy[upper], 

1012 marker="o", 

1013 linestyle="none", 

1014 color="b", 

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

1016 ) 

1017 ax.errorbar( 

1018 r[~upper], 

1019 y[~upper], 

1020 yerr=sy[~upper], 

1021 marker="o", 

1022 linestyle="none", 

1023 color="r", 

1024 label="$i<j$", 

1025 ) 

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

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

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

1029 ax.set_yscale("log") 

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

1031 

1032 # 

1033 axb = fig.add_subplot(212) 

1034 b = [] 

1035 for amp in bDict: 

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

1037 continue 

1038 b.append(bDict[amp]) 

1039 b = np.array(b) 

1040 yb = b.mean(axis=0) 

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

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

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

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

1045 yb = yb.ravel() 

1046 syb = syb.ravel() 

1047 xmin = -0.2 

1048 xmax = bRange 

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

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

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

1052 axb.errorbar( 

1053 rb[cutu], 

1054 yb[cutu], 

1055 yerr=syb[cutu], 

1056 marker="o", 

1057 linestyle="none", 

1058 color="b", 

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

1060 ) 

1061 axb.errorbar( 

1062 rb[cutl], 

1063 yb[cutl], 

1064 yerr=syb[cutl], 

1065 marker="o", 

1066 linestyle="none", 

1067 color="r", 

1068 label="$i<j$", 

1069 ) 

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

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

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

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

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

1075 plt.tight_layout() 

1076 

1077 return [fig] 

1078 

1079 @staticmethod 

1080 def plotAcoeffsSum(aDict, bDict): 

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

1082 

1083 Cumulative sum of a_ij as a function of maximum 

1084 separation. This plot displays the average over channels. 

1085 

1086 Parameters 

1087 ---------- 

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

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

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

1091 `ptcFitType` is `FULLCOVARIANCE`). 

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

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

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

1095 `ptcFitType` is `FULLCOVARIANCE`). 

1096 """ 

1097 assert len(aDict) == len(bDict) 

1098 a, b = [], [] 

1099 for amp in aDict: 

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

1101 continue 

1102 a.append(aDict[amp]) 

1103 b.append(bDict[amp]) 

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

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

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

1107 w = 4 * np.ones_like(a) 

1108 w[0, 1:] = 2 

1109 w[1:, 0] = 2 

1110 w[0, 0] = 1 

1111 wa = w * a 

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

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

1114 ax = plt.subplot(111) 

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

1116 ax.set_yscale("log") 

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

1118 ax.set_ylim(None, 1.2) 

1119 ax.set_ylabel( 

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

1121 ) 

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

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

1124 plt.tight_layout() 

1125 

1126 return [fig] 

1127 

1128 @staticmethod 

1129 def plotRelativeBiasACoeffs( 

1130 aDict, 

1131 aDictNoB, 

1132 fullCovsModel, 

1133 fullCovsModelNoB, 

1134 signalElectrons, 

1135 gainDict, 

1136 maxr=None, 

1137 ): 

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

1139 

1140 Illustrates systematic bias from estimating 'a' 

1141 coefficients from the slope of correlations as opposed to the 

1142 full model in Astier+19. 

1143 

1144 Parameters 

1145 ---------- 

1146 aDict : `dict` 

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

1148 names as keys. 

1149 aDictNoB : `dict` 

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

1151 with amp names as keys. 

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

1153 Dictionary keyed by amp names containing covariances model 

1154 per mean flux. 

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

1156 Dictionary keyed by amp names containing covariances model 

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

1158 signalElectrons : `float` 

1159 Signal at which to evaluate the a_ij coefficients. 

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

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

1162 maxr : `int`, optional 

1163 Maximum lag. 

1164 """ 

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

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

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

1168 

1169 for k, pair in enumerate(data): 

1170 diffs = [] 

1171 amean = [] 

1172 for amp in pair[0]: 

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

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

1175 continue 

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

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

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

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

1180 # to the more complete model in Astier+19 

1181 # (1905.08677). 

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

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

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

1185 a = pair[0][amp] 

1186 amean.append(a) 

1187 diffs.append((aOld - a)) 

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

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

1190 diff = diff / amean 

1191 diff = diff[:] 

1192 # The difference should be close to zero 

1193 diff[0, 0] = 0 

1194 if maxr is None: 

1195 maxr = diff.shape[0] 

1196 diff = diff[:maxr, :maxr] 

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

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

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

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

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

1202 plt.colorbar(im0) 

1203 ax0.set_title(title[k]) 

1204 

1205 plt.tight_layout() 

1206 

1207 return [fig] 

1208 

1209 def _plotStandardPtc(self, dataset): 

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

1211 vs signal per amplifier. 

1212 

1213 Parameters 

1214 ---------- 

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

1216 The dataset containing the means, variances, exposure 

1217 times, and mask. 

1218 

1219 Returns 

1220 ------- 

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

1222 Figure dictionary, keyd by an integer index: 

1223 0: ptcPlot1 ('ptcVarMean') 

1224 1: ptcPlot2 ('ptcVarMeanLog') 

1225 2: ptcPlot3 ('ptcNormalizedVar') 

1226 """ 

1227 ptcFitType = dataset.ptcFitType 

1228 if ptcFitType == "EXPAPPROXIMATION": 

1229 ptcFunc = funcAstier 

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

1231 elif ptcFitType == "POLYNOMIAL": 

1232 ptcFunc = funcPolynomial 

1233 for key in dataset.ptcFitPars: 

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

1235 break 

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

1237 else: 

1238 raise RuntimeError( 

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

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

1241 ) 

1242 

1243 legendFontSize = 6.5 

1244 labelFontSize = 8 

1245 titleFontSize = 9 

1246 supTitleFontSize = 18 

1247 markerSize = 25 

1248 

1249 # General determination of the size of the plot grid 

1250 nAmps = len(dataset.ampNames) 

1251 if nAmps == 2: 

1252 nRows, nCols = 2, 1 

1253 nRows = np.sqrt(nAmps) 

1254 mantissa, _ = np.modf(nRows) 

1255 if mantissa > 0: 

1256 nRows = int(nRows) + 1 

1257 nCols = nRows 

1258 else: 

1259 nRows = int(nRows) 

1260 nCols = nRows 

1261 

1262 f, ax = plt.subplots( 

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

1264 ) 

1265 f2, ax2 = plt.subplots( 

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

1267 ) 

1268 f3, ax3 = plt.subplots( 

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

1270 ) 

1271 

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

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

1274 ): 

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

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

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

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

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

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

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

1282 continue 

1283 else: 

1284 mask = mask.astype(bool) 

1285 meanVecFinal = meanVecOriginal[mask] 

1286 varVecFinal = varVecOriginal[mask] 

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

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

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

1290 dataset.ptcFitParsError[amp] 

1291 ) 

1292 ptcRedChi2 = dataset.ptcFitChiSq[amp] 

1293 if ptcFitType == "EXPAPPROXIMATION": 

1294 if len(meanVecFinal): 

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

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

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

1298 ptcNoiseAdu = ptcNoise * (1.0 / ptcGain) 

1299 ptcNoiseError = ( 

1300 0.5 

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

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

1303 ) 

1304 stringLegend = ( 

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

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

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

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

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

1310 ) 

1311 

1312 if ptcFitType == "POLYNOMIAL": 

1313 if len(meanVecFinal): 

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

1315 parsErr[1] / pars[1] 

1316 ) 

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

1318 ptcNoise = ptcNoiseAdu * ptcGain 

1319 ptcNoiseError = ( 

1320 0.5 

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

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

1323 ) * ptcGain 

1324 stringLegend = ( 

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

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

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

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

1329 ) 

1330 

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

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

1333 a.tick_params(labelsize=11) 

1334 a.set_xscale("linear") 

1335 a.set_yscale("linear") 

1336 

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

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

1339 a2.tick_params(labelsize=11) 

1340 a2.set_xscale("log") 

1341 a2.set_yscale("log") 

1342 

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

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

1345 a3.tick_params(labelsize=11) 

1346 a3.set_xscale("log") 

1347 a3.set_yscale("linear") 

1348 minMeanVecFinal = np.nanmin(meanVecFinal) 

1349 maxMeanVecFinal = np.nanmax(meanVecFinal) 

1350 meanVecFit = np.linspace( 

1351 minMeanVecFinal, maxMeanVecFinal, 100 * len(meanVecFinal) 

1352 ) 

1353 minMeanVecOriginal = np.nanmin(meanVecOriginal) 

1354 maxMeanVecOriginal = np.nanmax(meanVecOriginal) 

1355 deltaXlim = maxMeanVecOriginal - minMeanVecOriginal 

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

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

1358 a.scatter( 

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

1360 ) 

1361 a.text( 

1362 0.03, 

1363 0.66, 

1364 stringLegend, 

1365 transform=a.transAxes, 

1366 fontsize=legendFontSize, 

1367 ) 

1368 a.set_title(amp, fontsize=titleFontSize) 

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

1370 

1371 # Same, but in log-log scale 

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

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

1374 a2.scatter( 

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

1376 ) 

1377 a2.text( 

1378 0.03, 

1379 0.66, 

1380 stringLegend, 

1381 transform=a2.transAxes, 

1382 fontsize=legendFontSize, 

1383 ) 

1384 a2.set_title(amp, fontsize=titleFontSize) 

1385 a2.set_xlim([minMeanVecOriginal, maxMeanVecOriginal]) 

1386 

1387 # Var/mu vs mu 

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

1389 a3.scatter( 

1390 meanVecFinal, 

1391 varVecFinal / meanVecFinal, 

1392 c="blue", 

1393 marker="o", 

1394 s=markerSize, 

1395 ) 

1396 a3.scatter( 

1397 meanVecOutliers, 

1398 varVecOutliers / meanVecOutliers, 

1399 c="magenta", 

1400 marker="s", 

1401 s=markerSize, 

1402 ) 

1403 a3.text( 

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

1405 ) 

1406 a3.set_title(amp, fontsize=titleFontSize) 

1407 a3.set_xlim( 

1408 [ 

1409 minMeanVecOriginal - 0.2 * deltaXlim, 

1410 maxMeanVecOriginal + 0.2 * deltaXlim, 

1411 ] 

1412 ) 

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

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

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

1416 

1417 f.tight_layout() 

1418 f2.tight_layout() 

1419 f3.tight_layout() 

1420 

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

1422 

1423 return figDict 

1424 

1425 @staticmethod 

1426 def indexForBins(x, nBins): 

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

1428 binData. 

1429 

1430 Parameters 

1431 ---------- 

1432 x : `numpy.array` 

1433 Data to bin. 

1434 nBins : `int` 

1435 Number of bin. 

1436 

1437 Returns 

1438 ------- 

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

1440 Bin indices. 

1441 """ 

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

1443 return np.digitize(x, bins) 

1444 

1445 @staticmethod 

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

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

1448 

1449 Parameters 

1450 ---------- 

1451 x : `numpy.array` 

1452 Data to bin. 

1453 y : `numpy.array` 

1454 Data to bin. 

1455 binIdex : `list` 

1456 Bin number of each datum. 

1457 wy : `numpy.array` 

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

1459 actual weight is wy**2). 

1460 

1461 Returns 

1462 ------- 

1463 xbin : `numpy.array` 

1464 Binned data in x. 

1465 ybin : `numpy.array` 

1466 Binned data in y. 

1467 wybin : `numpy.array` 

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

1469 sybin : `numpy.array` 

1470 Uncertainty on the bin average, considering actual 

1471 scatter, and ignoring weights. 

1472 """ 

1473 if wy is None: 

1474 wy = np.ones_like(x) 

1475 binIndexSet = set(binIndex) 

1476 w2 = wy * wy 

1477 xw2 = x * (w2) 

1478 xbin = np.array( 

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

1480 ) 

1481 

1482 yw2 = y * w2 

1483 ybin = np.array( 

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

1485 ) 

1486 

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

1488 sybin = np.array( 

1489 [ 

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

1491 for i in binIndexSet 

1492 ] 

1493 ) 

1494 

1495 return xbin, ybin, wybin, sybin