Coverage for python/lsst/cp/pipe/ptc/cpPlotPtcTask.py: 10%
528 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-09 04:04 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-09 04:04 -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
34from lsst.cp.pipe.utils import (
35 funcAstier,
36 funcPolynomial,
37 calculateWeightedReducedChi2,
38 getFitDataFromCovariances,
39)
40from matplotlib.ticker import MaxNLocator
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 )
135class PlotPhotonTransferCurveConfig(
136 pipeBase.PipelineTaskConfig, pipelineConnections=PlotPhotonTransferCurveConnections
137):
138 """Configuration for the measurement of covariances from flats."""
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 )
154class PlotPhotonTransferCurveTask(pipeBase.PipelineTask):
155 """A class to plot the dataset from MeasurePhotonTransferCurveTask.
157 Parameters
158 ----------
159 outDir : `str`, optional
160 Path to the output directory where the final PDF will
161 be placed.
163 signalElectronsRelativeA : `float`, optional
164 Signal value for relative systematic bias between different
165 methods of estimating a_ij (Fig. 15 of Astier+19).
167 plotNormalizedCovariancesNumberOfBins : `float`, optional
168 Number of bins in `plotNormalizedCovariancesNumber` function
169 (Fig. 8, 10., of Astier+19).
171 Notes
172 -----
173 See DM-36388 for usage exammple.
174 """
176 ConfigClass = PlotPhotonTransferCurveConfig
177 _DefaultName = "cpPlotPtc"
179 def runQuantum(self, butlerQC, inputRefs, outputRefs):
180 inputs = butlerQC.get(inputRefs)
181 outputs = self.run(**inputs)
182 butlerQC.put(outputs, outputRefs)
184 def run(self, inputPtcDataset, camera=None):
185 """Make the plots for the PTC task.
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"]
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 )
209 maxNumberPlots = 12
210 if len(figDict) < maxNumberPlots:
211 for i in range(len(figDict), maxNumberPlots + 1):
212 figDict.setdefault(i, plt.figure())
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 )
229 def _covAstierMakeAllPlots(self, dataset):
230 """Make plots for PTC dataset when fitType=FULLCOVARIANCE.
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
236 Parameters
237 ----------
238 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`
239 The dataset containing the necessary information to
240 produce the plots.
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
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 )
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]
355 figList = (
356 figList1
357 + figList2
358 + figList3
359 + figList4
360 + figList5
361 + figList6
362 + figList7
363 + figList8
364 )
366 figDict = {}
367 for i, fig in enumerate(figList):
368 figDict[i] = fig
370 return figDict
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.
388 Figs. 6 and 7 of Astier+19
390 Parameters
391 ----------
392 mu : `dict` [`str`, `list`]
393 Dictionary keyed by amp name with mean signal values.
395 covs : `dict` [`str`, `list`]
396 Dictionary keyed by amp names containing a list of measued
397 covariances per mean flux.
399 covsModel : `dict` [`str`, `list`]
400 Dictionary keyed by amp names containinging covariances
401 model (Eq. 20 of Astier+19) per mean flux.
403 covsWeights : `dict` [`str`, `list`]
404 Dictionary keyed by amp names containinging sqrt. of
405 covariances weights.
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).
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.
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).
419 gainDict : `dict` [`str`, `float`]
420 Dictionary keyed by amp names containing the gains in e-/ADU.
422 noiseDict : `dict` [`str`, `float`]
423 Dictionary keyed by amp names containing the rms redout
424 noise in e-.
426 aDict : `dict` [`str`, `numpy.array`]
427 Dictionary keyed by amp names containing 'a' coefficients
428 (Eq. 20 of Astier+19).
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
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
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 )
468 assert len(covsModel) == nAmps
469 assert len(covsWeights) == nAmps
471 assert len(covsNoB) == nAmps
472 assert len(covsModelNoB) == nAmps
473 assert len(covsWeightsNoB) == nAmps
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 ):
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 )
507 # Get weighted reduced chi2
508 chi2FullModelVar = calculateWeightedReducedChi2(
509 varVecFinal, varVecModelFinal, varWeightsFinal, len(meanVecFinal), 4
510 )
512 (
513 meanVecFinalCov01,
514 varVecFinalCov01,
515 varVecModelFinalCov01,
516 _,
517 _,
518 ) = getFitDataFromCovariances(
519 0, 0, muAmp, cov, model, weight, returnMasked=True
520 )
522 (
523 meanVecFinalCov10,
524 varVecFinalCov10,
525 varVecModelFinalCov10,
526 _,
527 _,
528 ) = getFitDataFromCovariances(
529 1, 0, muAmp, cov, model, weight, returnMasked=True
530 )
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 )
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 )
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
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 )
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])
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)
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 )
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 )
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)
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 )
732 return [f, f2, fResCov00, fCov01, fCov10]
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.
752 Figs. 8, 10, and 11 of Astier+19
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 )
827 mue += list(mu)
828 rese += list(cov - model)
829 wce += list(weightCov)
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 )
849 mueNoB += list(muNoB)
850 reseNoB += list(covNoB - modelNoB)
851 wceNoB += list(weightCovNoB)
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)
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)
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)
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)
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)
915 return [fig]
917 @staticmethod
918 def plot_a_b(aDict, bDict, bRange=3):
919 """Fig. 12 of Astier+19
921 Color display of a and b arrays fits, averaged over channels.
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")
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()
966 return [fig]
968 @staticmethod
969 def ab_vs_dist(aDict, bDict, bRange=4):
970 """Fig. 13 of Astier+19.
972 Values of a and b arrays fits, averaged over amplifiers, as a
973 function of distance.
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`).
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")
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()
1075 return [fig]
1077 @staticmethod
1078 def plotAcoeffsSum(aDict, bDict):
1079 """Fig. 14. of Astier+19
1081 Cumulative sum of a_ij as a function of maximum
1082 separation. This plot displays the average over channels.
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()
1124 return [fig]
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.
1138 Illustrates systematic bias from estimating 'a'
1139 coefficients from the slope of correlations as opposed to the
1140 full model in Astier+19.
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)]
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])
1203 plt.tight_layout()
1205 return [fig]
1207 def _plotStandardPtc(self, dataset):
1208 """Plot PTC (linear and log scales ) and var/signal
1209 vs signal per amplifier.
1211 Parameters
1212 ----------
1213 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`
1214 The dataset containing the means, variances, exposure
1215 times, and mask.
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 )
1241 legendFontSize = 6.5
1242 labelFontSize = 8
1243 titleFontSize = 9
1244 supTitleFontSize = 18
1245 markerSize = 25
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
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 )
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 )
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 )
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")
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")
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])
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])
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)
1415 f.tight_layout()
1416 f2.tight_layout()
1417 f3.tight_layout()
1419 figDict = {0: f, 1: f2, 2: f3}
1421 return figDict
1423 @staticmethod
1424 def indexForBins(x, nBins):
1425 """Builds an index with regular binning. The result can be fed into
1426 binData.
1428 Parameters
1429 ----------
1430 x : `numpy.array`
1431 Data to bin.
1432 nBins : `int`
1433 Number of bin.
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)
1443 @staticmethod
1444 def binData(x, y, binIndex, wy=None):
1445 """Bin data (usually for display purposes).
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).
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 )
1480 yw2 = y * w2
1481 ybin = np.array(
1482 [yw2[binIndex == i].sum() / w2[binIndex == i].sum() for i in binIndexSet]
1483 )
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 )
1493 return xbin, ybin, wybin, sybin