Coverage for python/lsst/cp/pipe/ptc/cpPlotPtcTask.py: 8%
529 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-07 02:09 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-07 02:09 -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#
23__all__ = ["PlotPhotonTransferCurveConfig", "PlotPhotonTransferCurveTask"]
25import numpy as np
26import matplotlib.pyplot as plt
27import matplotlib as mpl
28from matplotlib import gridspec
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
35from lsst.cp.pipe.utils import (
36 funcAstier,
37 funcPolynomial,
38 calculateWeightedReducedChi2,
39 getFitDataFromCovariances,
40)
41from matplotlib.ticker import MaxNLocator
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 )
137class PlotPhotonTransferCurveConfig(
138 pipeBase.PipelineTaskConfig, pipelineConnections=PlotPhotonTransferCurveConnections
139):
140 """Configuration for the measurement of covariances from flats."""
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 )
156class PlotPhotonTransferCurveTask(pipeBase.PipelineTask):
157 """A class to plot the dataset from MeasurePhotonTransferCurveTask.
159 Parameters
160 ----------
161 outDir : `str`, optional
162 Path to the output directory where the final PDF will
163 be placed.
165 signalElectronsRelativeA : `float`, optional
166 Signal value for relative systematic bias between different
167 methods of estimating a_ij (Fig. 15 of Astier+19).
169 plotNormalizedCovariancesNumberOfBins : `float`, optional
170 Number of bins in `plotNormalizedCovariancesNumber` function
171 (Fig. 8, 10., of Astier+19).
173 Notes
174 -----
175 See DM-36388 for usage exammple.
176 """
178 ConfigClass = PlotPhotonTransferCurveConfig
179 _DefaultName = "cpPlotPtc"
181 def runQuantum(self, butlerQC, inputRefs, outputRefs):
182 inputs = butlerQC.get(inputRefs)
183 outputs = self.run(**inputs)
184 butlerQC.put(outputs, outputRefs)
186 def run(self, inputPtcDataset, camera=None):
187 """Make the plots for the PTC task.
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"]
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 )
211 maxNumberPlots = 12
212 if len(figDict) < maxNumberPlots:
213 for i in range(len(figDict), maxNumberPlots + 1):
214 figDict.setdefault(i, plt.figure())
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 )
231 def _covAstierMakeAllPlots(self, dataset):
232 """Make plots for PTC dataset when fitType=FULLCOVARIANCE.
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
238 Parameters
239 ----------
240 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`
241 The dataset containing the necessary information to
242 produce the plots.
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
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 )
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]
357 figList = (
358 figList1
359 + figList2
360 + figList3
361 + figList4
362 + figList5
363 + figList6
364 + figList7
365 + figList8
366 )
368 figDict = {}
369 for i, fig in enumerate(figList):
370 figDict[i] = fig
372 return figDict
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.
390 Figs. 6 and 7 of Astier+19
392 Parameters
393 ----------
394 mu : `dict` [`str`, `list`]
395 Dictionary keyed by amp name with mean signal values.
397 covs : `dict` [`str`, `list`]
398 Dictionary keyed by amp names containing a list of measued
399 covariances per mean flux.
401 covsModel : `dict` [`str`, `list`]
402 Dictionary keyed by amp names containinging covariances
403 model (Eq. 20 of Astier+19) per mean flux.
405 covsWeights : `dict` [`str`, `list`]
406 Dictionary keyed by amp names containinging sqrt. of
407 covariances weights.
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).
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.
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).
421 gainDict : `dict` [`str`, `float`]
422 Dictionary keyed by amp names containing the gains in e-/ADU.
424 noiseDict : `dict` [`str`, `float`]
425 Dictionary keyed by amp names containing the rms redout
426 noise in e-.
428 aDict : `dict` [`str`, `numpy.array`]
429 Dictionary keyed by amp names containing 'a' coefficients
430 (Eq. 20 of Astier+19).
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
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
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 )
470 assert len(covsModel) == nAmps
471 assert len(covsWeights) == nAmps
473 assert len(covsNoB) == nAmps
474 assert len(covsModelNoB) == nAmps
475 assert len(covsWeightsNoB) == nAmps
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 ):
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 )
509 # Get weighted reduced chi2
510 chi2FullModelVar = calculateWeightedReducedChi2(
511 varVecFinal, varVecModelFinal, varWeightsFinal, len(meanVecFinal), 4
512 )
514 (
515 meanVecFinalCov01,
516 varVecFinalCov01,
517 varVecModelFinalCov01,
518 _,
519 _,
520 ) = getFitDataFromCovariances(
521 0, 0, muAmp, cov, model, weight, returnMasked=True
522 )
524 (
525 meanVecFinalCov10,
526 varVecFinalCov10,
527 varVecModelFinalCov10,
528 _,
529 _,
530 ) = getFitDataFromCovariances(
531 1, 0, muAmp, cov, model, weight, returnMasked=True
532 )
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 )
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 )
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
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 )
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])
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)
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 )
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 )
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)
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 )
734 return [f, f2, fResCov00, fCov01, fCov10]
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.
754 Figs. 8, 10, and 11 of Astier+19
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 )
829 mue += list(mu)
830 rese += list(cov - model)
831 wce += list(weightCov)
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 )
851 mueNoB += list(muNoB)
852 reseNoB += list(covNoB - modelNoB)
853 wceNoB += list(weightCovNoB)
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)
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)
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)
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)
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)
917 return [fig]
919 @staticmethod
920 def plot_a_b(aDict, bDict, bRange=3):
921 """Fig. 12 of Astier+19
923 Color display of a and b arrays fits, averaged over channels.
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")
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()
968 return [fig]
970 @staticmethod
971 def ab_vs_dist(aDict, bDict, bRange=4):
972 """Fig. 13 of Astier+19.
974 Values of a and b arrays fits, averaged over amplifiers, as a
975 function of distance.
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`).
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")
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()
1077 return [fig]
1079 @staticmethod
1080 def plotAcoeffsSum(aDict, bDict):
1081 """Fig. 14. of Astier+19
1083 Cumulative sum of a_ij as a function of maximum
1084 separation. This plot displays the average over channels.
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()
1126 return [fig]
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.
1140 Illustrates systematic bias from estimating 'a'
1141 coefficients from the slope of correlations as opposed to the
1142 full model in Astier+19.
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)]
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])
1205 plt.tight_layout()
1207 return [fig]
1209 def _plotStandardPtc(self, dataset):
1210 """Plot PTC (linear and log scales ) and var/signal
1211 vs signal per amplifier.
1213 Parameters
1214 ----------
1215 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`
1216 The dataset containing the means, variances, exposure
1217 times, and mask.
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 )
1243 legendFontSize = 6.5
1244 labelFontSize = 8
1245 titleFontSize = 9
1246 supTitleFontSize = 18
1247 markerSize = 25
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
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 )
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 )
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 )
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")
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")
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])
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])
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)
1417 f.tight_layout()
1418 f2.tight_layout()
1419 f3.tight_layout()
1421 figDict = {0: f, 1: f2, 2: f3}
1423 return figDict
1425 @staticmethod
1426 def indexForBins(x, nBins):
1427 """Builds an index with regular binning. The result can be fed into
1428 binData.
1430 Parameters
1431 ----------
1432 x : `numpy.array`
1433 Data to bin.
1434 nBins : `int`
1435 Number of bin.
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)
1445 @staticmethod
1446 def binData(x, y, binIndex, wy=None):
1447 """Bin data (usually for display purposes).
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).
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 )
1482 yw2 = y * w2
1483 ybin = np.array(
1484 [yw2[binIndex == i].sum() / w2[binIndex == i].sum() for i in binIndexSet]
1485 )
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 )
1495 return xbin, ybin, wybin, sybin