LSST Applications g013ef56533+d2224463a4,g199a45376c+0ba108daf9,g19c4beb06c+9f335b2115,g1fd858c14a+2459ca3e43,g210f2d0738+2d3d333a78,g262e1987ae+abbb004f04,g2825c19fe3+eedc38578d,g29ae962dfc+0cb55f06ef,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+19c3a54948,g47891489e3+501a489530,g4cdb532a89+a047e97985,g511e8cfd20+ce1f47b6d6,g53246c7159+8c5ae1fdc5,g54cd7ddccb+890c8e1e5d,g5fd55ab2c7+951cc3f256,g64539dfbff+2d3d333a78,g67b6fd64d1+501a489530,g67fd3c3899+2d3d333a78,g74acd417e5+0ea5dee12c,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+501a489530,g8d7436a09f+5ea4c44d25,g8ea07a8fe4+81eaaadc04,g90f42f885a+34c0557caf,g9486f8a5af+165c016931,g97be763408+d5e351dcc8,gbf99507273+8c5ae1fdc5,gc2a301910b+2d3d333a78,gca7fc764a6+501a489530,gce8aa8abaa+8c5ae1fdc5,gd7ef33dd92+501a489530,gdab6d2f7ff+0ea5dee12c,ge410e46f29+501a489530,geaed405ab2+e3b4b2a692,gf9a733ac38+8c5ae1fdc5,w.2025.41
LSST Data Management Base Package
Loading...
Searching...
No Matches
ptcDataset.py
Go to the documentation of this file.
2# LSST Data Management System
3# Copyright 2008-2017 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <https://www.lsstcorp.org/LegalNotices/>.
21#
22"""
23Define dataset class for MeasurePhotonTransferCurve task
24"""
25
26__all__ = ['PhotonTransferCurveDataset']
27
28import warnings
29import numbers
30import numpy as np
31import math
32from astropy.table import Table
33import numpy.polynomial.polynomial as poly
34from scipy.signal import fftconvolve
35
36from lsst.ip.isr import IsrCalib
37
38
39def symmetrize(inputArray):
40 """ Copy array over 4 quadrants prior to convolution.
41
42 Parameters
43 ----------
44 inputarray : `numpy.array`
45 Input array to symmetrize.
46
47 Returns
48 -------
49 aSym : `numpy.array`
50 Symmetrized array.
51 """
52
53 targetShape = list(inputArray.shape)
54 r1, r2 = inputArray.shape[-1], inputArray.shape[-2]
55 targetShape[-1] = 2*r1-1
56 targetShape[-2] = 2*r2-1
57 aSym = np.ndarray(tuple(targetShape))
58 aSym[..., r2-1:, r1-1:] = inputArray
59 aSym[..., r2-1:, r1-1::-1] = inputArray
60 aSym[..., r2-1::-1, r1-1::-1] = inputArray
61 aSym[..., r2-1::-1, r1-1:] = inputArray
62
63 return aSym
64
65
67 """A simple class to hold the output data from the PTC task.
68
69 The dataset is made up of a dictionary for each item, keyed by the
70 amplifiers' names, which much be supplied at construction time.
71 New items cannot be added to the class to save accidentally saving to the
72 wrong property, and the class can be frozen if desired.
73 inputExpIdPairs records the exposures used to produce the data.
74 When fitPtc() or fitCovariancesAstier() is run, a mask is built up, which
75 is by definition always the same length as inputExpIdPairs, rawExpTimes,
76 rawMeans and rawVars, and is a list of bools, which are incrementally set
77 to False as points are discarded from the fits.
78
79 Parameters
80 ----------
81 ampNames : `list`
82 List with the names of the amplifiers of the detector at hand.
83 ptcFitType : `str`, optional
84 Type of model fitted to the PTC: "EXPAPPROXIMATION",
85 or "FULLCOVARIANCE" or "FULLCOVARIANCE_NO_B".
86 covMatrixSide : `int`, optional
87 Maximum lag of measured covariances (size of square covariance
88 matrices).
89 covMatrixSideFullCovFit : `int`, optional
90 Maximum covariances lag for FULLCOVARIANCE fit. It should be less or
91 equal than covMatrixSide.
92 kwargs : `dict`, optional
93 Other keyword arguments to pass to the parent init.
94
95 Notes
96 -----
97 The stored attributes are:
98
99 badAmps : `list` [`str`]
100 List with bad amplifiers names.
101 inputExpIdPairs : `dict`, [`str`, `list`]
102 Dictionary keyed by amp names containing the input exposures IDs.
103 inputExpPairMjdStartList : `dict`, [`str`, `np.ndarray`]
104 Dictionary keyed by amp names containing the start mjd from
105 the first exposure in each flat pair.
106 expIdMask : `dict`, [`str`, `np.ndarray`]
107 Dictionary keyed by amp names containing the mask produced after
108 outlier rejection. The mask produced by the "FULLCOVARIANCE"
109 option may differ from the one produced in the other two PTC
110 fit types.
111 expIdRolloffMask : `dict`, [`str`, `np.ndarray`]
112 Dictionary keyed by amp names containing the mask produced after
113 outlier rejection and extended to fit the PTC rolloff.
114 rawExpTimes : `dict`, [`str`, `np.ndarray`]
115 Dictionary keyed by amp names containing the unmasked exposure times.
116 rawMeans : `dict`, [`str`, `np.ndarray`]
117 Dictionary keyed by amp names containing the unmasked average of the
118 means of the exposures in each flat pair (units: adu).
119 rawVars : `dict`, [`str`, `np.ndarray`]
120 Dictionary keyed by amp names containing the variance of the
121 difference image of the exposures in each flat pair (units: adu^2).
122 rawDeltas : `dict`, [`str`, `np.ndarray`]
123 Dictionary keyed by amp names containing the scaled unmasked delta of
124 the means of the exposures in each flat pair (mean2 - mean1)
125 (units: adu).
126 rowMeanVariance : `dict`, [`str`, `np.ndarray`]
127 Dictionary keyed by amp names containing the variance of the
128 means of the rows of the difference image of the exposures
129 in each flat pair (units: adu^2).
130 histVars : `dict`, [`str`, `np.ndarray`]
131 Dictionary keyed by amp names containing the variance of the
132 difference image of the exposures in each flat pair estimated
133 by fitting a Gaussian model.
134 histChi2Dofs : `dict`, [`str`, `np.ndarray`]
135 Dictionary keyed by amp names containing the chi-squared per degree
136 of freedom fitting the difference image to a Gaussian model.
137 kspValues : `dict`, [`str`, `np.ndarray`]
138 Dictionary keyed by amp names containing the KS test p-value from
139 fitting the difference image to a Gaussian model.
140 gain : `dict`, [`str`, `float`]
141 Dictionary keyed by amp names containing the fitted gains. May be
142 adjusted by amp-offset gain ratios if configured in PTC solver.
143 gainUnadjusted : `dict`, [`str`, `float`]
144 Dictionary keyed by amp names containing unadjusted (raw) fit gain
145 values. May be the same as gain values if amp-offset adjustment
146 is not turned on.
147 gainErr : `dict`, [`str`, `float`]
148 Dictionary keyed by amp names containing the errors on the
149 fitted gains.
150 gainList : `dict`, [`str`, `np.ndarray`]
151 Dictionary keyed by amp names containing the gain estimated from
152 each flat pair.
153 overscanMedianLevelList : `dict`, [`str`, `np.ndarray`]
154 Dictionary keyed by amp names containing the median overscan
155 level from each input flat pair (units: adu).
156 overscanMedian : `dict `, [`str`, `float`]
157 Dictionary keyed by amp names containing the median of
158 overscanMedianLevelList[expIdMask] (units: adu).
159 overscanMedianSigma : `dict `, [`str`, `float`]
160 Dictionary keyed by amp names containing the median absolute
161 deviation of overscanMedianLevelList[expIdMask] (units: adu).
162 noiseList : `dict`, [`str`, `np.ndarray`]
163 Dictionary keyed by amp names containing the mean overscan
164 standard deviation from each flat pair (units: adu).
165 noise : `dict`, [`str`, `float`]
166 Dictionary keyed by amp names containing the fitted noise
167 (units: electron).
168 noiseErr : `dict`, [`str`, `float`]
169 Dictionary keyed by amp names containing the errors on the fitted
170 noise (units: electron).
171 ampOffsets : `dict`, [`str`, `float`]
172 Dictionary keyed by amp names containing amp-to-amp offsets
173 (units: adu).
174 ptcFitPars : `dict`, [`str`, `np.ndarray`]
175 Dictionary keyed by amp names containing the raveled array fitted
176 parameters of the PTC model.
177 ptcFitParsError : `dict`, [`str`, `np.ndarray`]
178 Dictionary keyed by amp names containing the raveled array errors
179 on the fitted parameters of the PTC model for ptcFitType.
180 ptcFitChiSq : `dict`, [`str`, `float`]
181 Dictionary keyed by amp names containing the reduced chi squared
182 of the fit.
183 ptcTurnoff : `dict` [`str, `float`]
184 Flux value (in adu) where the variance of the PTC curve starts
185 decreasing consistently.
186 ptcTurnoffSamplingError : `dict` [`str`, `float`]
187 ``Sampling`` error on the ptcTurnoff, based on the flux sampling
188 of the input PTC (units: adu).
189 ptcRolloff : `dict` [`str, `float`]
190 Flux value (in adu) where the variance of the PTC curve begins starts
191 to move away from the PTC model by some threshold.
192 ptcRolloffError : `dict` [`str`, `float`]
193 Covariance error from the fit to the PTC rolloff (units: adu).
194 ptcRolloff : `dict` [`str, `float`]
195 Curvature parameter of the PTC rolloff.
196 ptcRolloffError : `dict` [`str`, `float`]
197 Fitting error of the curvature parameter of the PTC rolloff.
198 nPixelCovariances : `dict`, [`str`, `int`]
199 Dictionary keyed by amp names containing the number of pixels
200 that were used to measure the covariances.
201 covariances : `dict`, [`str`, `np.ndarray`]
202 Dictionary keyed by amp names containing a list of measured
203 covariances per mean flux (units: adu^2).
204 covariancesModel : `dict`, [`str`, `np.ndarray`]
205 Dictionary keyed by amp names containinging covariances model
206 (Eq. 20 of Astier+19) per mean flux (units: adu^2).
207 covariancesSqrtWeights : `dict`, [`str`, `np.ndarray`]
208 Dictionary keyed by amp names containinging sqrt. of covariances
209 weights (units: 1/adu).
210 aMatrix : `dict`, [`str`, `np.ndarray`]
211 Dictionary keyed by amp names containing the "a" parameters from
212 the model in Eq. 20 of Astier+19 (units: 1/electron).
213 bMatrix : `dict`, [`str`, `np.ndarray`]
214 Dictionary keyed by amp names containing the "b" parameters from
215 the model in Eq. 20 of Astier+19 (units: 1/electron).
216 noiseMatrix : `dict`, [`str`, `np.ndarray`]
217 Dictionary keyed by amp names containing the "noise" parameters from
218 the model in Eq. 20 of Astier+19 (units: electron^2).
219 covariancesModelNoB : `dict`, [`str`, `np.ndarray`]
220 Dictionary keyed by amp names containing covariances model
221 (with 'b'=0 in Eq. 20 of Astier+19) per mean flux (units:
222 adu^2). Will be removed after v29.
223 aMatrixNoB : `dict`, [`str`, `np.ndarray`]
224 Dictionary keyed by amp names containing the "a" parameters from the
225 model in Eq. 20 of Astier+19 (and 'b' = 0) (units: 1/electron).
226 Will be removed after v29.
227 noiseMatrixNoB : `dict`, [`str`, `np.ndarray`]
228 Dictionary keyed by amp names containing the "noise" parameters from
229 the model in Eq. 20 of Astier+19, with 'b' = 0 (units: electron^2).
230 Will be removed after v29.
231 finalVars : `dict`, [`str`, `np.ndarray`]
232 Dictionary keyed by amp names containing the masked variance of the
233 difference image of each flat
234 pair. If needed, each array will be right-padded with
235 np.nan to match the length of rawExpTimes.
236 finalModelVars : `dict`, [`str`, `np.ndarray`]
237 Dictionary keyed by amp names containing the masked modeled
238 variance of the difference image of each flat pair. If needed, each
239 array will be right-padded with np.nan to match the length of
240 rawExpTimes.
241 finalMeans : `dict`, [`str`, `np.ndarray`]
242 Dictionary keyed by amp names containing the masked average of the
243 means of the exposures in each flat pair. If needed, each array
244 will be right-padded with np.nan to match the length of
245 rawExpTimes.
246 photoCharges : `dict`, [`str`, `np.ndarray`]
247 Dictionary keyed by amp names containing the integrated photocharge
248 for linearity calibration.
249 photoChargeDeltas : `dict`, [`str`, `np.ndarray`]
250 Dictionary keyed by amp names containing the delta for the integrated
251 photocharge (photocharge2 - photocharge1).
252 auxValues : `dict`, [`str`, `np.ndarray`]
253 Dictionary of per-detector auxiliary header values that can be used
254 for PTC, linearity computation.
255
256 Version 1.1 adds the `ptcTurnoff` attribute.
257 Version 1.2 adds the `histVars`, `histChi2Dofs`, and `kspValues`
258 attributes.
259 Version 1.3 adds the `noiseMatrix` and `noiseMatrixNoB` attributes.
260 Version 1.4 adds the `auxValues` attribute.
261 Version 1.5 adds the `covMatrixSideFullCovFit` attribute.
262 Version 1.6 adds the `rowMeanVariance` attribute.
263 Version 1.7 adds the `noiseList` attribute.
264 Version 1.8 adds the `ptcTurnoffSamplingError` attribute.
265 Version 1.9 standardizes PTC noise units to electron.
266 Version 2.0 adds the `ampOffsets`, `gainUnadjusted`, and
267 `gainList` attributes.
268 Version 2.1 deprecates the `covariancesModelNoB`, `aMatrixNoB`, and
269 `noiseMatrixNoB` attributes.
270 Version 2.2 adds the `overscanMedianLevelList` and
271 `inputExpPairMjdStartList` attributes.
272 Version 2.3 adds the `overscanMedian` and
273 `overscanMedianSigma` attrbutes.
274 Version 2.4 adds the `nPixelCovariances` attribute.
275 Version 2.5 adds the `rawDeltas` and `photoChargeDeltas` attributes.
276 Version 2.6 adds the `expIdRolloffMask`, `ptcRolloff`, `ptcRolloffError`,
277 `ptcRolloffTau`, and `ptcRolloffTauError` attributes. Also
278 deprecates the POLYNOMIAL fit type.
279 """
280
281 _OBSTYPE = 'PTC'
282 _SCHEMA = 'Gen3 Photon Transfer Curve'
283 # When adding a new field to update the version, be sure to update the
284 # following methods:
285 # * __init__()
286 # * fromDict()
287 # * toDict()
288 # * fromTable()
289 # * toTable()
290 # * setAmpValuesPartialDataset()
291 # * appendPartialPtc()
292 # * sort()
293 # * _checkTypes() in test_ptcDataset.py
294 # * test_ptcDataset() in test_ptcDataset.py
295 # * test_ptcDatasetSort in test_ptcDataset.py
296 # * test_ptcDatasetAppend in test_ptcDataset.py
297 _VERSION = 2.6
298
299 def __init__(self, ampNames=[], ptcFitType=None, covMatrixSide=1,
300 covMatrixSideFullCovFit=None, **kwargs):
301 self.ptcFitType = ptcFitType
302 self.ampNames = ampNames
303 self.covMatrixSide = covMatrixSide
304 if covMatrixSideFullCovFit is None:
305 self.covMatrixSideFullCovFit = covMatrixSide
306 else:
307 self.covMatrixSideFullCovFit = covMatrixSideFullCovFit
308
309 self.badAmps = []
310
311 self.inputExpIdPairs = {ampName: [] for ampName in ampNames}
312 self.inputExpPairMjdStartList = {ampName: np.array([]) for ampName in ampNames}
313 self.expIdMask = {ampName: np.array([], dtype=bool) for ampName in ampNames}
314 self.expIdRolloffMask = {ampName: np.array([], dtype=bool) for ampName in ampNames}
315 self.rawExpTimes = {ampName: np.array([]) for ampName in ampNames}
316 self.rawMeans = {ampName: np.array([]) for ampName in ampNames}
317 self.rawVars = {ampName: np.array([]) for ampName in ampNames}
318 self.rawDeltas = {ampName: np.array([]) for ampName in ampNames}
319 self.rowMeanVariance = {ampName: np.array([]) for ampName in ampNames}
320 self.photoCharges = {ampName: np.array([]) for ampName in ampNames}
321 self.photoChargeDeltas = {ampName: np.array([]) for ampName in ampNames}
322 self.ampOffsets = {ampName: np.array([]) for ampName in ampNames}
323
324 self.gain = {ampName: np.nan for ampName in ampNames}
325 self.gainUnadjusted = {ampName: np.nan for ampName in ampNames}
326 self.gainErr = {ampName: np.nan for ampName in ampNames}
327 self.gainList = {ampName: np.array([]) for ampName in ampNames}
328 self.overscanMedianLevelList = {ampName: np.array([]) for ampName in ampNames}
329 self.overscanMedian = {ampName: np.nan for ampName in ampNames}
330 self.overscanMedianSigma = {ampName: np.nan for ampName in ampNames}
331 self.noiseList = {ampName: np.array([]) for ampName in ampNames}
332 self.noise = {ampName: np.nan for ampName in ampNames}
333 self.noiseErr = {ampName: np.nan for ampName in ampNames}
334
335 self.histVars = {ampName: np.array([]) for ampName in ampNames}
336 self.histChi2Dofs = {ampName: np.array([]) for ampName in ampNames}
337 self.kspValues = {ampName: np.array([]) for ampName in ampNames}
338
339 self.ptcFitPars = {ampName: np.array([]) for ampName in ampNames}
340 self.ptcFitParsError = {ampName: np.array([]) for ampName in ampNames}
341 self.ptcFitChiSq = {ampName: np.nan for ampName in ampNames}
342 self.ptcTurnoff = {ampName: np.nan for ampName in ampNames}
343 self.ptcTurnoffSamplingError = {ampName: np.nan for ampName in ampNames}
344 self.ptcRolloff = {ampName: np.nan for ampName in ampNames}
345 self.ptcRolloffError = {ampName: np.nan for ampName in ampNames}
346 self.ptcRolloffTau = {ampName: np.nan for ampName in ampNames}
347 self.ptcRolloffTauError = {ampName: np.nan for ampName in ampNames}
348
349 self.nPixelCovariances = {ampName: -1 for ampName in ampNames}
350 self.covariances = {ampName: np.array([]) for ampName in ampNames}
351 self.covariancesModel = {ampName: np.array([]) for ampName in ampNames}
352 self.covariancesSqrtWeights = {ampName: np.array([]) for ampName in ampNames}
353 self.aMatrix = {ampName: np.array([]) for ampName in ampNames}
354 self.bMatrix = {ampName: np.array([]) for ampName in ampNames}
355 self.noiseMatrix = {ampName: np.array([]) for ampName in ampNames}
356
357 self.finalVars = {ampName: np.array([]) for ampName in ampNames}
358 self.finalModelVars = {ampName: np.array([]) for ampName in ampNames}
359 self.finalMeans = {ampName: np.array([]) for ampName in ampNames}
360
361 # Try this as a dict of arrays.
362 self.auxValues = {}
363
364 super().__init__(**kwargs)
365 self.requiredAttributes.update(['badAmps', 'inputExpIdPairs', 'inputExpPairMjdStartList',
366 'expIdMask', 'expIdRolloffMask', 'rawExpTimes', 'rawMeans',
367 'rawVars', 'rowMeanVariance', 'gain', 'gainErr', 'gainList',
368 'noise', 'noiseErr', 'noiseList', 'overscanMedianLevelList',
369 'overscanMedian', 'overscanMedianSigma', 'ptcFitPars',
370 'ptcFitParsError', 'ptcFitChiSq', 'ptcTurnoff', 'covariances',
371 'covariancesModel', 'covariancesSqrtWeights', 'aMatrix',
372 'bMatrix', 'noiseMatrix', 'finalVars', 'finalModelVars',
373 'finalMeans', 'photoCharges', 'histVars', 'histChi2Dofs',
374 'kspValues', 'auxValues', 'ptcTurnoffSamplingError',
375 'ptcRolloff', 'ptcRolloffError', 'ptcRolloffTau',
376 'ptcRolloffTauError', 'ampOffsets', 'gainUnadjusted',
377 'nPixelCovariances', 'rawDeltas', 'photoChargeDeltas'])
378
379 self.updateMetadata(setCalibInfo=True, setCalibId=True, **kwargs)
381
383 self,
384 ampName,
385 inputExpIdPair=(-1, -1),
386 inputExpPairMjdStart=np.nan,
387 rawExpTime=np.nan,
388 rawMean=np.nan,
389 rawVar=np.nan,
390 rawDelta=np.nan,
391 rowMeanVariance=np.nan,
392 photoCharge=np.nan,
393 photoChargeDelta=np.nan,
394 ampOffset=np.nan,
395 expIdMask=False,
396 expIdRolloffMask=False,
397 nPixelCovariance=-1,
398 covariance=None,
399 covSqrtWeights=None,
400 gain=np.nan,
401 noise=np.nan,
402 overscanMedianLevel=np.nan,
403 histVar=np.nan,
404 histChi2Dof=np.nan,
405 kspValue=0.0,
406 ):
407 """
408 Set the amp values for a partial PTC Dataset (from cpExtractPtcTask).
409
410 Parameters
411 ----------
412 ampName : `str`
413 Name of the amp to set the values.
414 inputExpIdPair : `tuple` [`int`]
415 Exposure IDs of input pair.
416 inputExpPairMjdStart : `float`, optional
417 The start MJD of first exposure in the flat pair.
418 rawExpTime : `float`, optional
419 Exposure time for this exposure pair (units: sec).
420 rawMean : `float`, optional
421 Average of the means of the exposures in this pair
422 (units: adu).
423 rawVar : `float`, optional
424 Variance of the difference of the exposures in this pair
425 (units: adu^2).
426 rawDelta : `float`, optional
427 Delta of the means of the exposure in this pair
428 (units: adu).
429 rowMeanVariance : `float`, optional
430 Variance of the means of the rows in the difference image
431 of the exposures in this pair (units: adu^2).
432 photoCharge : `float`, optional
433 Integrated photocharge for flat pair for linearity calibration
434 (arbitrary units).
435 photoChargeDelta : `float`, optional
436 Delta between integrated photocharge for the flat pair
437 (arbitrary units).
438 ampOffset : `float`, optional
439 Amp offset for this amplifier.
440 expIdMask : `bool`, optional
441 Flag setting if this exposure pair should be used (True)
442 or not used (False).
443 expIdRolloffMask : `bool`, optional
444 Flag setting if this exposure pair should be used (True)
445 or not used for rolloff search (False).
446 nPixelCovariance : `int`, optional
447 Number of pixels that went into the covariance measurement.
448 covariance : `np.ndarray` or None, optional
449 Measured covariance for this exposure pair (units: adu^2).
450 covSqrtWeights : `np.ndarray` or None, optional
451 Measured sqrt of covariance weights in this exposure pair
452 (units: 1/adu).
453 gain : `float`, optional
454 Estimated gain for this exposure pair (units: electron/adu).
455 noise : `float`, optional
456 Estimated read noise for this exposure pair (units: electron).
457 overscanMedianLevel : `float`, optional
458 Average of the median overscan levels for this exposure pair.
459 (units: adu)
460 histVar : `float`, optional
461 Variance estimated from fitting a histogram with a Gaussian model
462 (units: adu).
463 histChi2Dof : `float`, optional
464 Chi-squared per degree of freedom from Gaussian histogram fit.
465 kspValue : `float`, optional
466 KS test p-value from the Gaussian histogram fit.
467 """
468 nanMatrix = np.full((self.covMatrixSide, self.covMatrixSide), np.nan)
469 nanMatrixFit = np.full((self.covMatrixSideFullCovFit,
470 self.covMatrixSideFullCovFit), np.nan)
471 if covariance is None:
472 covariance = nanMatrix
473 if covSqrtWeights is None:
474 covSqrtWeights = nanMatrix
475
476 self.inputExpIdPairs[ampName] = [inputExpIdPair]
477 self.inputExpPairMjdStartList[ampName] = np.array([inputExpPairMjdStart])
478 self.rawExpTimes[ampName] = np.array([rawExpTime])
479 self.rawMeans[ampName] = np.array([rawMean])
480 self.rawVars[ampName] = np.array([rawVar])
481 self.rawDeltas[ampName] = np.array([rawDelta])
482 self.rowMeanVariance[ampName] = np.array([rowMeanVariance])
483 self.photoCharges[ampName] = np.array([photoCharge])
484 self.photoChargeDeltas[ampName] = np.array([photoChargeDelta])
485 self.ampOffsets[ampName] = np.array([ampOffset])
486 self.expIdMask[ampName] = np.array([expIdMask])
487 self.expIdRolloffMask[ampName] = np.array([expIdRolloffMask])
488 self.nPixelCovariances[ampName] = nPixelCovariance
489 self.covariances[ampName] = np.array([covariance])
490 self.covariancesSqrtWeights[ampName] = np.array([covSqrtWeights])
491 self.gain[ampName] = gain
492 self.gainUnadjusted[ampName] = gain
493 self.gainList[ampName] = np.array([gain])
494 self.noise[ampName] = noise
495 self.noiseList[ampName] = np.array([noise])
496 self.overscanMedianLevelList[ampName] = np.array([overscanMedianLevel])
497 self.overscanMedian[ampName] = float(overscanMedianLevel)
498 self.overscanMedianSigma[ampName] = float(0.0)
499 self.histVars[ampName] = np.array([histVar])
500 self.histChi2Dofs[ampName] = np.array([histChi2Dof])
501 self.kspValues[ampName] = np.array([kspValue])
502
503 # From FULLCOVARIANCE model
504 self.covariancesModel[ampName] = np.array([nanMatrixFit])
505 self.aMatrix[ampName] = nanMatrixFit
506 self.bMatrix[ampName] = nanMatrixFit
507 self.noiseMatrix[ampName] = nanMatrixFit
508
509 # Filler values.
510 self.finalVars[ampName] = np.array([np.nan])
511 self.finalModelVars[ampName] = np.array([np.nan])
512 self.finalMeans[ampName] = np.array([np.nan])
513
514 def setAuxValuesPartialDataset(self, auxDict):
515 """
516 Set a dictionary of auxiliary values for a partial dataset.
517
518 Parameters
519 ----------
520 auxDict : `dict` [`str`, `float`]
521 Dictionary of float values.
522 """
523 for key, value in auxDict.items():
524 if isinstance(value, numbers.Integral):
525 self.auxValues[key] = np.atleast_1d(np.asarray(value).astype(np.int64))
526 elif isinstance(value, (str, np.str_, np.bytes_)):
527 self.auxValues[key] = np.atleast_1d(np.asarray(value))
528 else:
529 self.auxValues[key] = np.atleast_1d(np.array(value, dtype=np.float64))
530
531 def updateMetadata(self, **kwargs):
532 """Update calibration metadata.
533 This calls the base class's method after ensuring the required
534 calibration keywords will be saved.
535
536 Parameters
537 ----------
538 setDate : `bool`, optional
539 Update the CALIBDATE fields in the metadata to the current
540 time. Defaults to False.
541 kwargs :
542 Other keyword parameters to set in the metadata.
543 """
544 super().updateMetadata(PTC_FIT_TYPE=self.ptcFitType, **kwargs)
545
546 @classmethod
547 def fromDict(cls, dictionary):
548 """Construct a calibration from a dictionary of properties.
549 Must be implemented by the specific calibration subclasses.
550
551 Parameters
552 ----------
553 dictionary : `dict`
554 Dictionary of properties.
555
556 Returns
557 -------
558 calib : `lsst.ip.isr.PhotonTransferCurveDataset`
559 Constructed calibration.
560
561 Raises
562 ------
563 RuntimeError
564 Raised if the supplied dictionary is for a different
565 calibration.
566 """
567 calib = cls()
568 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
569 raise RuntimeError(f"Incorrect Photon Transfer Curve dataset supplied. "
570 f"Expected {calib._OBSTYPE}, found {dictionary['metadata']['OBSTYPE']}")
571 calib.setMetadata(dictionary['metadata'])
572 calib.ptcFitType = dictionary['ptcFitType']
573 calib.covMatrixSide = dictionary['covMatrixSide']
574 calib.covMatrixSideFullCovFit = dictionary['covMatrixSideFullCovFit']
575 calib.badAmps = np.array(dictionary['badAmps'], 'str').tolist()
576 calib.ampNames = []
577
578 # The cov matrices are square
579 covMatrixSide = calib.covMatrixSide
580 covMatrixSideFullCovFit = calib.covMatrixSideFullCovFit
581 # Number of final signal levels
582 covDimensionsProduct = len(np.array(list(dictionary['covariances'].values())[0]).ravel())
583 nSignalPoints = int(covDimensionsProduct/(covMatrixSide*covMatrixSide))
584
585 for ampName in dictionary['ampNames']:
586 calib.ampNames.append(ampName)
587 calib.inputExpIdPairs[ampName] = dictionary['inputExpIdPairs'][ampName]
588 calib.inputExpPairMjdStartList[ampName] = np.array(
589 dictionary['inputExpPairMjdStartList'][ampName],
590 )
591 calib.expIdMask[ampName] = np.array(dictionary['expIdMask'][ampName])
592 calib.expIdRolloffMask[ampName] = np.array(dictionary['expIdRolloffMask'][ampName])
593 calib.rawExpTimes[ampName] = np.array(dictionary['rawExpTimes'][ampName], dtype=np.float64)
594 calib.rawMeans[ampName] = np.array(dictionary['rawMeans'][ampName], dtype=np.float64)
595 calib.rawVars[ampName] = np.array(dictionary['rawVars'][ampName], dtype=np.float64)
596 calib.rawDeltas[ampName] = np.array(dictionary['rawDeltas'][ampName], dtype=np.float64)
597 calib.rowMeanVariance[ampName] = np.array(dictionary['rowMeanVariance'][ampName],
598 dtype=np.float64)
599 calib.gain[ampName] = float(dictionary['gain'][ampName])
600 calib.gainErr[ampName] = float(dictionary['gainErr'][ampName])
601 calib.gainUnadjusted[ampName] = float(dictionary['gainUnadjusted'][ampName])
602 calib.gainList[ampName] = np.array(dictionary['gainList'][ampName], dtype=np.float64)
603 calib.noiseList[ampName] = np.array(dictionary['noiseList'][ampName], dtype=np.float64)
604 calib.nPixelCovariances[ampName] = int(dictionary['nPixelCovariances'][ampName])
605 calib.overscanMedianLevelList[ampName] = np.array(
606 dictionary['overscanMedianLevelList'][ampName],
607 dtype=np.float64,
608 )
609 calib.overscanMedian[ampName] = float(dictionary['overscanMedian'][ampName])
610 calib.overscanMedianSigma[ampName] = float(dictionary['overscanMedianSigma'][ampName])
611 calib.noise[ampName] = float(dictionary['noise'][ampName])
612 calib.noiseErr[ampName] = float(dictionary['noiseErr'][ampName])
613 calib.histVars[ampName] = np.array(dictionary['histVars'][ampName], dtype=np.float64)
614 calib.histChi2Dofs[ampName] = np.array(dictionary['histChi2Dofs'][ampName], dtype=np.float64)
615 calib.kspValues[ampName] = np.array(dictionary['kspValues'][ampName], dtype=np.float64)
616 calib.ptcFitPars[ampName] = np.array(dictionary['ptcFitPars'][ampName], dtype=np.float64)
617 calib.ptcFitParsError[ampName] = np.array(dictionary['ptcFitParsError'][ampName],
618 dtype=np.float64)
619 calib.ptcFitChiSq[ampName] = float(dictionary['ptcFitChiSq'][ampName])
620 calib.ptcTurnoff[ampName] = float(dictionary['ptcTurnoff'][ampName])
621 calib.ptcTurnoffSamplingError[ampName] = float(dictionary['ptcTurnoffSamplingError'][ampName])
622 calib.ptcRolloff[ampName] = float(dictionary['ptcRolloff'][ampName])
623 calib.ptcRolloffError[ampName] = float(dictionary['ptcRolloffError'][ampName])
624 calib.ptcRolloffTau[ampName] = float(dictionary['ptcRolloff'][ampName])
625 calib.ptcRolloffTauError[ampName] = float(dictionary['ptcRolloffError'][ampName])
626 if nSignalPoints > 0:
627 # Regular dataset
628 calib.covariances[ampName] = np.array(dictionary['covariances'][ampName],
629 dtype=np.float64).reshape(
630 (nSignalPoints, covMatrixSide, covMatrixSide))
631 calib.covariancesModel[ampName] = np.array(
632 dictionary['covariancesModel'][ampName],
633 dtype=np.float64).reshape(
634 (nSignalPoints, covMatrixSideFullCovFit, covMatrixSideFullCovFit))
635 calib.covariancesSqrtWeights[ampName] = np.array(
636 dictionary['covariancesSqrtWeights'][ampName],
637 dtype=np.float64).reshape(
638 (nSignalPoints, covMatrixSide, covMatrixSide))
639 calib.aMatrix[ampName] = np.array(dictionary['aMatrix'][ampName],
640 dtype=np.float64).reshape(
641 (covMatrixSideFullCovFit, covMatrixSideFullCovFit))
642 calib.bMatrix[ampName] = np.array(dictionary['bMatrix'][ampName],
643 dtype=np.float64).reshape(
644 (covMatrixSideFullCovFit, covMatrixSideFullCovFit))
645 calib.noiseMatrix[ampName] = np.array(
646 dictionary['noiseMatrix'][ampName],
647 dtype=np.float64).reshape((covMatrixSideFullCovFit, covMatrixSideFullCovFit))
648 else:
649 # Empty dataset
650 calib.covariances[ampName] = np.array([], dtype=np.float64)
651 calib.covariancesModel[ampName] = np.array([], dtype=np.float64)
652 calib.covariancesSqrtWeights[ampName] = np.array([], dtype=np.float64)
653 calib.aMatrix[ampName] = np.array([], dtype=np.float64)
654 calib.bMatrix[ampName] = np.array([], dtype=np.float64)
655 calib.noiseMatrix[ampName] = np.array([], dtype=np.float64)
656
657 calib.finalVars[ampName] = np.array(dictionary['finalVars'][ampName], dtype=np.float64)
658 calib.finalModelVars[ampName] = np.array(dictionary['finalModelVars'][ampName], dtype=np.float64)
659 calib.finalMeans[ampName] = np.array(dictionary['finalMeans'][ampName], dtype=np.float64)
660 calib.photoCharges[ampName] = np.array(dictionary['photoCharges'][ampName], dtype=np.float64)
661 calib.photoChargeDeltas[ampName] = np.array(
662 dictionary['photoChargeDeltas'][ampName],
663 dtype=np.float64,
664 )
665 calib.ampOffsets[ampName] = np.array(dictionary['ampOffsets'][ampName], dtype=np.float64)
666
667 for key, value in dictionary['auxValues'].items():
668 if isinstance(value[0], numbers.Integral):
669 calib.auxValues[key] = np.atleast_1d(np.asarray(value).astype(np.int64))
670 elif isinstance(value[0], (str, np.str_, np.bytes_)):
671 calib.auxValues[key] = np.atleast_1d(np.asarray(value))
672 else:
673 calib.auxValues[key] = np.atleast_1d(np.array(value, dtype=np.float64))
674
675 calib.updateMetadata()
676 return calib
677
678 def toDict(self):
679 """Return a dictionary containing the calibration properties.
680 The dictionary should be able to be round-tripped through
681 `fromDict`.
682
683 Returns
684 -------
685 dictionary : `dict`
686 Dictionary of properties.
687 """
688 self.updateMetadata()
689
690 outDict = dict()
691 metadata = self.getMetadata()
692 outDict['metadata'] = metadata
693
694 def _dictOfArraysToDictOfLists(dictOfArrays):
695 dictOfLists = {}
696 for key, value in dictOfArrays.items():
697 dictOfLists[key] = value.ravel().tolist()
698
699 return dictOfLists
700
701 outDict['ptcFitType'] = self.ptcFitType
702 outDict['covMatrixSide'] = self.covMatrixSide
703 outDict['covMatrixSideFullCovFit'] = self.covMatrixSideFullCovFit
704 outDict['ampNames'] = self.ampNames
705 outDict['badAmps'] = self.badAmps
706 outDict['inputExpIdPairs'] = self.inputExpIdPairs
707 outDict['inputExpPairMjdStartList'] = _dictOfArraysToDictOfLists(self.inputExpPairMjdStartList)
708 outDict['expIdMask'] = _dictOfArraysToDictOfLists(self.expIdMask)
709 outDict['expIdRolloffMask'] = _dictOfArraysToDictOfLists(self.expIdRolloffMask)
710 outDict['rawExpTimes'] = _dictOfArraysToDictOfLists(self.rawExpTimes)
711 outDict['rawMeans'] = _dictOfArraysToDictOfLists(self.rawMeans)
712 outDict['rawVars'] = _dictOfArraysToDictOfLists(self.rawVars)
713 outDict['rawDeltas'] = _dictOfArraysToDictOfLists(self.rawDeltas)
714 outDict['rowMeanVariance'] = _dictOfArraysToDictOfLists(self.rowMeanVariance)
715 outDict['gain'] = self.gain
716 outDict['gainErr'] = self.gainErr
717 outDict['gainUnadjusted'] = self.gainUnadjusted
718 outDict['gainList'] = _dictOfArraysToDictOfLists(self.gainList)
719 outDict['noiseList'] = _dictOfArraysToDictOfLists(self.noiseList)
720 outDict['overscanMedianLevelList'] = _dictOfArraysToDictOfLists(self.overscanMedianLevelList)
721 outDict['overscanMedian'] = self.overscanMedian
722 outDict['overscanMedianSigma'] = self.overscanMedianSigma
723 outDict['noise'] = self.noise
724 outDict['noiseErr'] = self.noiseErr
725 outDict['histVars'] = self.histVars
726 outDict['histChi2Dofs'] = self.histChi2Dofs
727 outDict['kspValues'] = self.kspValues
728 outDict['ptcFitPars'] = _dictOfArraysToDictOfLists(self.ptcFitPars)
729 outDict['ptcFitParsError'] = _dictOfArraysToDictOfLists(self.ptcFitParsError)
730 outDict['ptcFitChiSq'] = self.ptcFitChiSq
731 outDict['ptcTurnoff'] = self.ptcTurnoff
732 outDict['ptcTurnoffSamplingError'] = self.ptcTurnoffSamplingError
733 outDict['ptcRolloff'] = self.ptcRolloff
734 outDict['ptcRolloffError'] = self.ptcRolloffError
735 outDict['ptcRolloffTau'] = self.ptcRolloff
736 outDict['ptcRolloffTauError'] = self.ptcRolloffTauError
737 outDict['nPixelCovariances'] = self.nPixelCovariances
738 outDict['covariances'] = _dictOfArraysToDictOfLists(self.covariances)
739 outDict['covariancesModel'] = _dictOfArraysToDictOfLists(self.covariancesModel)
740 outDict['covariancesSqrtWeights'] = _dictOfArraysToDictOfLists(self.covariancesSqrtWeights)
741 outDict['aMatrix'] = _dictOfArraysToDictOfLists(self.aMatrix)
742 outDict['bMatrix'] = _dictOfArraysToDictOfLists(self.bMatrix)
743 outDict['noiseMatrix'] = _dictOfArraysToDictOfLists(self.noiseMatrix)
744 outDict['finalVars'] = _dictOfArraysToDictOfLists(self.finalVars)
745 outDict['finalModelVars'] = _dictOfArraysToDictOfLists(self.finalModelVars)
746 outDict['finalMeans'] = _dictOfArraysToDictOfLists(self.finalMeans)
747 outDict['photoCharges'] = _dictOfArraysToDictOfLists(self.photoCharges)
748 outDict['photoChargeDeltas'] = _dictOfArraysToDictOfLists(self.photoChargeDeltas)
749 outDict['ampOffsets'] = _dictOfArraysToDictOfLists(self.ampOffsets)
750 outDict['auxValues'] = _dictOfArraysToDictOfLists(self.auxValues)
751
752 return outDict
753
754 @classmethod
755 def fromTable(cls, tableList):
756 """Construct calibration from a list of tables.
757 This method uses the `fromDict` method to create the
758 calibration, after constructing an appropriate dictionary from
759 the input tables.
760
761 Parameters
762 ----------
763 tableList : `list` [`lsst.afw.table.Table`]
764 List of tables to use to construct the datasetPtc.
765
766 Returns
767 -------
768 calib : `lsst.ip.isr.PhotonTransferCurveDataset`
769 The calibration defined in the tables.
770 """
771 ptcTable = tableList[0]
772
773 metadata = ptcTable.meta
774 inDict = dict()
775 inDict['metadata'] = metadata
776 inDict['ampNames'] = []
777 inDict['ptcFitType'] = []
778 inDict['covMatrixSide'] = []
779 inDict['covMatrixSideFullCovFit'] = []
780 inDict['inputExpIdPairs'] = dict()
781 inDict['inputExpPairMjdStartList'] = dict()
782 inDict['expIdMask'] = dict()
783 inDict['expIdRolloffMask'] = dict()
784 inDict['rawExpTimes'] = dict()
785 inDict['rawMeans'] = dict()
786 inDict['rawVars'] = dict()
787 inDict['rawDeltas'] = dict()
788 inDict['rowMeanVariance'] = dict()
789 inDict['gain'] = dict()
790 inDict['gainErr'] = dict()
791 inDict['gainUnadjusted'] = dict()
792 inDict['gainList'] = dict()
793 inDict['noiseList'] = dict()
794 inDict['overscanMedianLevelList'] = dict()
795 inDict['overscanMedian'] = dict()
796 inDict['overscanMedianSigma'] = dict()
797 inDict['noise'] = dict()
798 inDict['noiseErr'] = dict()
799 inDict['histVars'] = dict()
800 inDict['histChi2Dofs'] = dict()
801 inDict['kspValues'] = dict()
802 inDict['ptcFitPars'] = dict()
803 inDict['ptcFitParsError'] = dict()
804 inDict['ptcFitChiSq'] = dict()
805 inDict['ptcTurnoff'] = dict()
806 inDict['ptcTurnoffSamplingError'] = dict()
807 inDict['ptcRolloff'] = dict()
808 inDict['ptcRolloffError'] = dict()
809 inDict['ptcRolloffTau'] = dict()
810 inDict['ptcRolloffTauError'] = dict()
811 inDict['nPixelCovariances'] = dict()
812 inDict['covariances'] = dict()
813 inDict['covariancesModel'] = dict()
814 inDict['covariancesSqrtWeights'] = dict()
815 inDict['aMatrix'] = dict()
816 inDict['bMatrix'] = dict()
817 inDict['noiseMatrix'] = dict()
818 inDict['finalVars'] = dict()
819 inDict['finalModelVars'] = dict()
820 inDict['finalMeans'] = dict()
821 inDict['badAmps'] = []
822 inDict['photoCharges'] = dict()
823 inDict['photoChargeDeltas'] = dict()
824 inDict['ampOffsets'] = dict()
825
826 # TODO: DM-47610, remove after v29
827 inDict['noiseMatrixNoB'] = dict()
828 inDict['covariancesModelNoB'] = dict()
829 inDict['aMatrixNoB'] = dict()
830
831 calibVersion = metadata['PTC_VERSION']
832 if calibVersion == 1.0:
833 cls().log.warning(f"Previous version found for PTC dataset: {calibVersion}. "
834 f"Setting 'ptcTurnoff' in all amps to last value in 'finalMeans'.")
835 for record in ptcTable:
836 ampName = record['AMPLIFIER_NAME']
837
838 inDict['ptcFitType'] = record['PTC_FIT_TYPE']
839 inDict['covMatrixSide'] = record['COV_MATRIX_SIDE']
840 inDict['ampNames'].append(ampName)
841 inDict['inputExpIdPairs'][ampName] = record['INPUT_EXP_ID_PAIRS'].tolist()
842 inDict['expIdMask'][ampName] = record['EXP_ID_MASK']
843 inDict['rawExpTimes'][ampName] = record['RAW_EXP_TIMES']
844 inDict['rawMeans'][ampName] = record['RAW_MEANS']
845 inDict['rawVars'][ampName] = record['RAW_VARS']
846 inDict['gain'][ampName] = record['GAIN']
847 inDict['gainErr'][ampName] = record['GAIN_ERR']
848 inDict['noise'][ampName] = record['NOISE']
849 inDict['noiseErr'][ampName] = record['NOISE_ERR']
850 inDict['ptcFitPars'][ampName] = record['PTC_FIT_PARS']
851 inDict['ptcFitParsError'][ampName] = record['PTC_FIT_PARS_ERROR']
852 inDict['ptcFitChiSq'][ampName] = record['PTC_FIT_CHI_SQ']
853 inDict['covariances'][ampName] = record['COVARIANCES']
854 inDict['covariancesModel'][ampName] = record['COVARIANCES_MODEL']
855 inDict['covariancesSqrtWeights'][ampName] = record['COVARIANCES_SQRT_WEIGHTS']
856 inDict['aMatrix'][ampName] = record['A_MATRIX']
857 inDict['bMatrix'][ampName] = record['B_MATRIX']
858 inDict['finalVars'][ampName] = record['FINAL_VARS']
859 inDict['finalModelVars'][ampName] = record['FINAL_MODEL_VARS']
860 inDict['finalMeans'][ampName] = record['FINAL_MEANS']
861 inDict['badAmps'] = record['BAD_AMPS'].tolist()
862 inDict['photoCharges'][ampName] = record['PHOTO_CHARGE']
863 if calibVersion == 1.0:
864 mask = record['FINAL_MEANS'].mask
865 array = record['FINAL_MEANS'][~mask]
866 if len(array) > 0:
867 inDict['ptcTurnoff'][ampName] = record['FINAL_MEANS'][~mask][-1]
868 else:
869 inDict['ptcTurnoff'][ampName] = np.nan
870 else:
871 inDict['ptcTurnoff'][ampName] = record['PTC_TURNOFF']
872 if calibVersion < 1.2:
873 inDict['histVars'][ampName] = np.array([np.nan])
874 inDict['histChi2Dofs'][ampName] = np.array([np.nan])
875 inDict['kspValues'][ampName] = np.array([0.0])
876 else:
877 inDict['histVars'][ampName] = record['HIST_VARS']
878 inDict['histChi2Dofs'][ampName] = record['HIST_CHI2_DOFS']
879 inDict['kspValues'][ampName] = record['KS_PVALUES']
880 if calibVersion < 1.3:
881 nanMatrix = np.full_like(inDict['aMatrix'][ampName], np.nan)
882 inDict['noiseMatrix'][ampName] = nanMatrix
883 inDict['noiseMatrixNoB'][ampName] = nanMatrix
884 elif calibVersion >= 1.3 and calibVersion < 2.1:
885 inDict['noiseMatrix'][ampName] = record['NOISE_MATRIX']
886 inDict['noiseMatrixNoB'][ampName] = record['NOISE_MATRIX_NO_B']
887 else:
888 inDict['noiseMatrix'][ampName] = record['NOISE_MATRIX']
889 nanMatrix = np.full_like(inDict['aMatrix'][ampName], np.nan)
890 inDict['noiseMatrixNoB'][ampName] = nanMatrix
891 if calibVersion < 1.5:
892 # Matched to `COV_MATRIX_SIDE`. Same for all amps.
893 inDict['covMatrixSideFullCovFit'] = inDict['covMatrixSide']
894 else:
895 inDict['covMatrixSideFullCovFit'] = record['COV_MATRIX_SIDE_FULL_COV_FIT']
896 if calibVersion < 1.6:
897 inDict['rowMeanVariance'][ampName] = np.full((len(inDict['expIdMask'][ampName]),), np.nan)
898 else:
899 inDict['rowMeanVariance'][ampName] = record['ROW_MEAN_VARIANCE']
900 if calibVersion < 1.7:
901 inDict['noiseList'][ampName] = np.full_like(inDict['rawMeans'][ampName], np.nan)
902 else:
903 inDict['noiseList'][ampName] = record['NOISE_LIST']
904 if calibVersion < 1.8:
905 inDict['ptcTurnoffSamplingError'][ampName] = np.nan
906 else:
907 inDict['ptcTurnoffSamplingError'][ampName] = record['PTC_TURNOFF_SAMPLING_ERROR']
908 if calibVersion < 1.9 and inDict['ptcFitType'] == "FULLCOVARIANCE":
909 # Before version 1.9, the noise stored in the PTC was in
910 # units of electron^2 only if ptcFitType == FULLCOVARIANCE.
911 # After version 1.9, we standardized the
912 # PhotonTransferCurveDataset noise units to electron to fix
913 # this bug. If a user tries to use an earlier version of
914 # PTC with this fit type, we must be sure to do the
915 # calculations properly. More information about this noise
916 # issue can be found in DM-45976.
917 if ampName == inDict['ampNames'][0]:
918 cls().log.info(f"Input PTC VERSION ({calibVersion}) < 1.9 and"
919 " ptcFitType == FULLCOVARIANCE. Applying fix for"
920 f" the DM-45976 noise issue.")
921 # The noiseErr calculation was accidentally correct in the
922 # previous version, so we only need to upday the noise
923 # attribute.
924 inDict['noise'][ampName] = np.sqrt(record['noise'][ampName])
925 if calibVersion < 2.0:
926 inDict['ampOffsets'][ampName] = np.full_like(inDict['rawMeans'][ampName], np.nan)
927 inDict['gainUnadjusted'][ampName] = record['GAIN']
928 inDict['gainList'][ampName] = np.full_like(inDict['rawMeans'][ampName], np.nan)
929 else:
930 inDict['ampOffsets'][ampName] = record['AMP_OFFSETS']
931 inDict['gainUnadjusted'][ampName] = record['GAIN_UNADJUSTED']
932 inDict['gainList'][ampName] = record['GAIN_LIST']
933 if calibVersion < 2.1:
934 inDict['covariancesModelNoB'][ampName] = record['COVARIANCES_MODEL_NO_B']
935 inDict['aMatrixNoB'][ampName] = record['A_MATRIX_NO_B']
936 else:
937 nanMatrixList = np.full_like(inDict['covariances'][ampName], np.nan)
938 inDict['covariancesModelNoB'][ampName] = nanMatrixList
939 nanMatrix = np.full_like(inDict['aMatrix'][ampName], np.nan)
940 inDict['aMatrixNoB'][ampName] = nanMatrix
941 if calibVersion < 2.2:
942 inDict['inputExpPairMjdStartList'][ampName] = np.full_like(
943 inDict['rawMeans'][ampName],
944 np.nan,
945 )
946 inDict['overscanMedianLevelList'][ampName] = np.full_like(
947 inDict['rawMeans'][ampName],
948 np.nan,
949 )
950 else:
951 inDict['inputExpPairMjdStartList'][ampName] = record['INPUT_EXP_PAIR_MJD_START']
952 inDict['overscanMedianLevelList'][ampName] = record['OVERSCAN_MEDIAN_LIST']
953 if calibVersion < 2.3:
954 inDict['overscanMedian'][ampName] = np.nan
955 inDict['overscanMedianSigma'][ampName] = np.nan
956 else:
957 inDict['overscanMedian'][ampName] = record['OVERSCAN_MEDIAN']
958 inDict['overscanMedianSigma'][ampName] = record['OVERSCAN_MEDIAN_SIGMA']
959 if calibVersion < 2.4:
960 inDict['nPixelCovariances'][ampName] = -1
961 else:
962 inDict['nPixelCovariances'][ampName] = record['NPIXEL_COVARIANCES']
963 if calibVersion < 2.5:
964 inDict['rawDeltas'][ampName] = np.full_like(
965 inDict['rawMeans'][ampName],
966 np.nan,
967 )
968 inDict['photoChargeDeltas'][ampName] = np.full_like(
969 inDict['rawMeans'][ampName],
970 np.nan,
971 )
972 else:
973 inDict['rawDeltas'][ampName] = record['RAW_DELTAS']
974 inDict['photoChargeDeltas'][ampName] = record['PHOTO_CHARGE_DELTAS']
975 if calibVersion < 2.6:
976 inDict['ptcRolloff'][ampName] = np.nan
977 inDict['ptcRolloffError'][ampName] = np.nan
978 inDict['ptcRolloffTau'][ampName] = np.nan
979 inDict['ptcRolloffTauError'][ampName] = np.nan
980 inDict['expIdRolloffMask'][ampName] = np.full_like(
981 inDict['expIdMask'][ampName],
982 False,
983 )
984 else:
985 inDict['ptcRolloff'][ampName] = record['PTC_ROLLOFF']
986 inDict['ptcRolloffError'][ampName] = record['PTC_ROLLOFF_ERROR']
987 inDict['ptcRolloffTau'][ampName] = record['PTC_ROLLOFF_TAU']
988 inDict['ptcRolloffTauError'][ampName] = record['PTC_ROLLOFF_TAU_ERROR']
989 inDict['expIdRolloffMask'][ampName] = record['EXP_ID_ROLLOFF_MASK']
990
991 inDict['auxValues'] = {}
992 record = ptcTable[0]
993 for col in record.columns.keys():
994 if col.startswith('PTCAUX_'):
995 parts = col.split('PTCAUX_')
996 if isinstance(record[col][0], np.bytes_):
997 # Convert to a unicode string because astropy fits doesn't
998 # round-trip properly
999 inDict['auxValues'][parts[1]] = record[col].astype(np.str_)
1000 else:
1001 inDict['auxValues'][parts[1]] = record[col]
1002
1003 return cls().fromDict(inDict)
1004
1005 def toTable(self):
1006 """Construct a list of tables containing the information in this
1007 calibration.
1008
1009 The list of tables should create an identical calibration
1010 after being passed to this class's fromTable method.
1011
1012 Returns
1013 -------
1014 tableList : `list` [`astropy.table.Table`]
1015 List of tables containing the linearity calibration
1016 information.
1017 """
1018 tableList = []
1019 self.updateMetadata()
1020
1021 badAmps = np.array(self.badAmps) if len(self.badAmps) else np.array([], dtype="U3")
1022
1023 catalogList = []
1024 for ampName in self.ampNames:
1025 ampDict = {
1026 'AMPLIFIER_NAME': ampName,
1027 'PTC_FIT_TYPE': self.ptcFitType,
1028 'COV_MATRIX_SIDE': self.covMatrixSide,
1029 'COV_MATRIX_SIDE_FULL_COV_FIT': self.covMatrixSideFullCovFit,
1030 'INPUT_EXP_ID_PAIRS': self.inputExpIdPairs[ampName],
1031 'INPUT_EXP_PAIR_MJD_START': self.inputExpPairMjdStartList[ampName],
1032 'EXP_ID_MASK': self.expIdMask[ampName],
1033 'EXP_ID_ROLLOFF_MASK': self.expIdRolloffMask[ampName],
1034 'RAW_EXP_TIMES': self.rawExpTimes[ampName],
1035 'RAW_MEANS': self.rawMeans[ampName],
1036 'RAW_VARS': self.rawVars[ampName],
1037 'RAW_DELTAS': self.rawDeltas[ampName],
1038 'ROW_MEAN_VARIANCE': self.rowMeanVariance[ampName],
1039 'GAIN': self.gain[ampName],
1040 'GAIN_ERR': self.gainErr[ampName],
1041 'GAIN_UNADJUSTED': self.gainUnadjusted[ampName],
1042 'GAIN_LIST': self.gainList[ampName],
1043 'OVERSCAN_MEDIAN_LIST': self.overscanMedianLevelList[ampName],
1044 'OVERSCAN_MEDIAN': self.overscanMedian[ampName],
1045 'OVERSCAN_MEDIAN_SIGMA': self.overscanMedianSigma[ampName],
1046 'NOISE_LIST': self.noiseList[ampName],
1047 'NOISE': self.noise[ampName],
1048 'NOISE_ERR': self.noiseErr[ampName],
1049 'HIST_VARS': self.histVars[ampName],
1050 'HIST_CHI2_DOFS': self.histChi2Dofs[ampName],
1051 'KS_PVALUES': self.kspValues[ampName],
1052 'PTC_FIT_PARS': np.array(self.ptcFitPars[ampName]),
1053 'PTC_FIT_PARS_ERROR': np.array(self.ptcFitParsError[ampName]),
1054 'PTC_FIT_CHI_SQ': self.ptcFitChiSq[ampName],
1055 'PTC_TURNOFF': self.ptcTurnoff[ampName],
1056 'PTC_TURNOFF_SAMPLING_ERROR': self.ptcTurnoffSamplingError[ampName],
1057 'PTC_ROLLOFF': self.ptcRolloff[ampName],
1058 'PTC_ROLLOFF_ERROR': self.ptcRolloffError[ampName],
1059 'PTC_ROLLOFF_TAU': self.ptcRolloffTau[ampName],
1060 'PTC_ROLLOFF_TAU_ERROR': self.ptcRolloffTauError[ampName],
1061 'A_MATRIX': self.aMatrix[ampName].ravel(),
1062 'B_MATRIX': self.bMatrix[ampName].ravel(),
1063 'NOISE_MATRIX': self.noiseMatrix[ampName].ravel(),
1064 'BAD_AMPS': badAmps,
1065 'PHOTO_CHARGE': self.photoCharges[ampName],
1066 'PHOTO_CHARGE_DELTAS': self.photoChargeDeltas[ampName],
1067 'AMP_OFFSETS': self.ampOffsets[ampName],
1068 'NPIXEL_COVARIANCES': self.nPixelCovariances[ampName],
1069 'COVARIANCES': self.covariances[ampName].ravel(),
1070 'COVARIANCES_MODEL': self.covariancesModel[ampName].ravel(),
1071 'COVARIANCES_SQRT_WEIGHTS': self.covariancesSqrtWeights[ampName].ravel(),
1072 'FINAL_VARS': self.finalVars[ampName],
1073 'FINAL_MODEL_VARS': self.finalModelVars[ampName],
1074 'FINAL_MEANS': self.finalMeans[ampName],
1075 }
1076
1077 if self.auxValues:
1078 for key, value in self.auxValues.items():
1079 ampDict[f"PTCAUX_{key}"] = value
1080
1081 catalogList.append(ampDict)
1082
1083 catalog = Table(catalogList)
1084
1085 inMeta = self.getMetadata().toDict()
1086 outMeta = {k: v for k, v in inMeta.items() if v is not None}
1087 outMeta.update({k: "" for k, v in inMeta.items() if v is None})
1088 catalog.meta = outMeta
1089 tableList.append(catalog)
1090
1091 return tableList
1092
1093 def fromDetector(self, detector):
1094 """Read metadata parameters from a detector.
1095
1096 Parameters
1097 ----------
1098 detector : `lsst.afw.cameraGeom.detector`
1099 Input detector with parameters to use.
1100
1101 Returns
1102 -------
1103 calib : `lsst.ip.isr.PhotonTransferCurveDataset`
1104 The calibration constructed from the detector.
1105 """
1106
1107 pass
1108
1109 def appendPartialPtc(self, partialPtc):
1110 """Append a partial PTC dataset to this dataset.
1111
1112 Parameters
1113 ----------
1114 partialPtc : `lsst.ip.isr.PhotonTransferCurveDataset`
1115 Partial PTC to append. Should only have one element.
1116 """
1117 if self.ampNames != partialPtc.ampNames:
1118 raise ValueError("partialPtc has mis-matched amps.")
1119 if len(partialPtc.rawMeans[self.ampNames[0]]) != 1 or partialPtc.ptcFitType != "PARTIAL":
1120 raise ValueError("partialPtc does not appear to be the correct format.")
1121
1122 # Record the initial length of the PTC, for checking auxValues.
1123 initialLength = len(self.expIdMask[self.ampNames[0]])
1124
1125 for key, value in partialPtc.auxValues.items():
1126 if key in self.auxValues:
1127 self.auxValues[key] = np.append(self.auxValues[key], value)
1128 elif initialLength == 0:
1129 # This is the first partial, so we can set the dict key.
1130 self.auxValues[key] = value
1131 else:
1132 raise ValueError(f"partialPtc has mismatched auxValue key {key}.")
1133
1134 for ampName in self.ampNames:
1135 if initialLength == 0:
1136 # This is the first partial, so we can set the dict key.
1137 self.nPixelCovariances[ampName] = partialPtc.nPixelCovariances[ampName]
1138 elif partialPtc.nPixelCovariances[ampName] != self.nPixelCovariances[ampName]:
1139 raise ValueError(f"partialPtc has mismatched nPixelCovariances for amp {ampName}.")
1140
1141 for ampName in self.ampNames:
1142 # The partial dataset consists of lists of values for each
1143 # quantity. In the case of the input exposure pairs and the
1144 # input exposure MJDs, this is a list of tuples. In all cases
1145 # we only want the first (and only) element of the list.
1146 self.inputExpIdPairs[ampName].append(partialPtc.inputExpIdPairs[ampName][0])
1147 self.inputExpPairMjdStartList[ampName] = np.append(
1148 self.inputExpPairMjdStartList[ampName],
1149 partialPtc.inputExpPairMjdStartList[ampName][0],
1150 )
1151 self.expIdMask[ampName] = np.append(self.expIdMask[ampName],
1152 partialPtc.expIdMask[ampName][0])
1153 self.expIdRolloffMask[ampName] = np.append(self.expIdRolloffMask[ampName],
1154 partialPtc.expIdRolloffMask[ampName][0])
1155 self.rawExpTimes[ampName] = np.append(self.rawExpTimes[ampName],
1156 partialPtc.rawExpTimes[ampName][0])
1157 self.rawMeans[ampName] = np.append(self.rawMeans[ampName],
1158 partialPtc.rawMeans[ampName][0])
1159 self.rawVars[ampName] = np.append(self.rawVars[ampName],
1160 partialPtc.rawVars[ampName][0])
1161 self.rawDeltas[ampName] = np.append(self.rawDeltas[ampName],
1162 partialPtc.rawDeltas[ampName][0])
1163 self.rowMeanVariance[ampName] = np.append(self.rowMeanVariance[ampName],
1164 partialPtc.rowMeanVariance[ampName][0])
1165 self.photoCharges[ampName] = np.append(self.photoCharges[ampName],
1166 partialPtc.photoCharges[ampName][0])
1167 self.photoChargeDeltas[ampName] = np.append(self.photoChargeDeltas[ampName],
1168 partialPtc.photoChargeDeltas[ampName][0])
1169 self.ampOffsets[ampName] = np.append(self.ampOffsets[ampName],
1170 partialPtc.ampOffsets[ampName][0])
1171 self.histVars[ampName] = np.append(self.histVars[ampName],
1172 partialPtc.histVars[ampName][0])
1173 self.histChi2Dofs[ampName] = np.append(self.histChi2Dofs[ampName],
1174 partialPtc.histChi2Dofs[ampName][0])
1175 self.kspValues[ampName] = np.append(self.kspValues[ampName],
1176 partialPtc.kspValues[ampName][0])
1177 self.gainList[ampName] = np.append(self.gainList[ampName],
1178 partialPtc.gain[ampName])
1179 self.overscanMedianLevelList[ampName] = np.append(
1180 self.overscanMedianLevelList[ampName],
1181 partialPtc.overscanMedianLevelList[ampName][0],
1182 )
1183 self.noiseList[ampName] = np.append(self.noiseList[ampName],
1184 partialPtc.noise[ampName])
1185 self.finalVars[ampName] = np.append(self.finalVars[ampName],
1186 partialPtc.finalVars[ampName][0])
1187 self.finalModelVars[ampName] = np.append(self.finalModelVars[ampName],
1188 partialPtc.finalModelVars[ampName][0])
1189 self.finalMeans[ampName] = np.append(self.finalMeans[ampName],
1190 partialPtc.finalMeans[ampName][0])
1191 self.covariances[ampName] = np.append(
1192 self.covariances[ampName].ravel(),
1193 partialPtc.covariances[ampName].ravel()
1194 ).reshape(
1195 (
1196 len(self.rawExpTimes[ampName]),
1197 self.covMatrixSide,
1198 self.covMatrixSide,
1199 )
1200 )
1201 self.covariancesSqrtWeights[ampName] = np.append(
1202 self.covariancesSqrtWeights[ampName].ravel(),
1203 partialPtc.covariancesSqrtWeights[ampName].ravel()
1204 ).reshape(
1205 (
1206 len(self.rawExpTimes[ampName]),
1207 self.covMatrixSide,
1208 self.covMatrixSide,
1209 )
1210 )
1211 self.covariancesModel[ampName] = np.append(
1212 self.covariancesModel[ampName].ravel(),
1213 partialPtc.covariancesModel[ampName].ravel()
1214 ).reshape(
1215 (
1216 len(self.rawExpTimes[ampName]),
1217 self.covMatrixSide,
1218 self.covMatrixSide,
1219 )
1220 )
1221
1222 def sort(self, sortIndex):
1223 """Sort the components of the PTC by a given sort index.
1224
1225 The PTC is sorted in-place.
1226
1227 Parameters
1228 ----------
1229 sortIndex : `list` or `np.ndarray`
1230 The sorting index, which must be the same length as
1231 the number of elements of the PTC.
1232 """
1233 index = np.atleast_1d(sortIndex)
1234
1235 # First confirm everything matches.
1236 for ampName in self.ampNames:
1237 if len(index) != len(self.rawExpTimes[ampName]):
1238 raise ValueError(
1239 f"Length of sortIndex ({len(index)}) does not match number of PTC "
1240 f"elements ({len(self.rawExpTimes[ampName])})",
1241 )
1242
1243 # Note that gain, gainUnadjusted, gainErr, noise, noiseErr,
1244 # ptcTurnoff, ptcTurnoffSamplingError, and the full covariance fit
1245 # parameters are global and not sorted by input pair.
1246
1247 for ampName in self.ampNames:
1248 self.inputExpIdPairs[ampName] = np.array(
1249 self.inputExpIdPairs[ampName]
1250 )[index].tolist()
1251 self.inputExpPairMjdStartList[ampName] = self.inputExpPairMjdStartList[ampName][index]
1252
1253 self.expIdMask[ampName] = self.expIdMask[ampName][index]
1254 self.expIdRolloffMask[ampName] = self.expIdRolloffMask[ampName][index]
1255 self.rawExpTimes[ampName] = self.rawExpTimes[ampName][index]
1256 self.rawMeans[ampName] = self.rawMeans[ampName][index]
1257 self.rawVars[ampName] = self.rawVars[ampName][index]
1258 self.rawDeltas[ampName] = self.rawDeltas[ampName][index]
1259 self.rowMeanVariance[ampName] = self.rowMeanVariance[ampName][index]
1260 self.photoCharges[ampName] = self.photoCharges[ampName][index]
1261 self.photoChargeDeltas[ampName] = self.photoChargeDeltas[ampName][index]
1262 self.ampOffsets[ampName] = self.ampOffsets[ampName][index]
1263
1264 self.gainList[ampName] = self.gainList[ampName][index]
1265 self.noiseList[ampName] = self.noiseList[ampName][index]
1266
1267 self.overscanMedianLevelList[ampName] = self.overscanMedianLevelList[ampName][index]
1268
1269 self.histVars[ampName] = self.histVars[ampName][index]
1270 self.histChi2Dofs[ampName] = self.histChi2Dofs[ampName][index]
1271 self.kspValues[ampName] = self.kspValues[ampName][index]
1272
1273 self.covariances[ampName] = self.covariances[ampName][index]
1274 self.covariancesSqrtWeights[ampName] = self.covariancesSqrtWeights[ampName][index]
1275 self.covariancesModel[ampName] = self.covariancesModel[ampName][index]
1276
1277 self.finalVars[ampName] = self.finalVars[ampName][index]
1278 self.finalModelVars[ampName] = self.finalModelVars[ampName][index]
1279 self.finalMeans[ampName] = self.finalMeans[ampName][index]
1280
1281 # Sort the auxiliary values which are not stored per-amp.
1282 for key, value in self.auxValues.items():
1283 self.auxValues[key] = value[index]
1284
1285 def getExpIdsUsed(self, ampName):
1286 """Get the exposures used, i.e. not discarded, for a given amp.
1287 If no mask has been created yet, all exposures are returned.
1288
1289 Parameters
1290 ----------
1291 ampName : `str`
1292
1293 Returns
1294 -------
1295 expIdsUsed : `list` [`tuple`]
1296 List of pairs of exposure ids used in PTC.
1297 """
1298 if len(self.expIdMask[ampName]) == 0:
1299 return self.inputExpIdPairs[ampName]
1300
1301 # if the mask exists it had better be the same length as the expIdPairs
1302 assert len(self.expIdMask[ampName]) == len(self.inputExpIdPairs[ampName])
1303
1304 pairs = self.inputExpIdPairs[ampName]
1305 mask = self.expIdMask[ampName]
1306 # cast to bool required because numpy
1307 try:
1308 expIdsUsed = [(exp1, exp2) for ((exp1, exp2), m) in zip(pairs, mask) if m]
1309 except ValueError:
1310 self.log.warning("The PTC file was written incorrectly; you should rerun the "
1311 "PTC solve task if possible.")
1312 expIdsUsed = []
1313 for pairList, m in zip(pairs, mask):
1314 if m:
1315 expIdsUsed.append(pairList[0])
1316
1317 return expIdsUsed
1318
1319 def getGoodAmps(self):
1320 """Get the good amps from this PTC."""
1321 return [amp for amp in self.ampNames if amp not in self.badAmps]
1322
1323 def getGoodPoints(self, ampName):
1324 """Get the good points used for a given amp in the PTC.
1325
1326 Parameters
1327 ----------
1328 ampName : `str`
1329 Amplifier's name.
1330
1331 Returns
1332 -------
1333 goodPoints : `np.ndarray`
1334 Boolean array of good points used in PTC.
1335 """
1336 return self.expIdMask[ampName]
1337
1338 def validateGainNoiseTurnoffValues(self, ampName, doWarn=False):
1339 """Ensure the gain, read noise, and PTC turnoff have
1340 sensible values.
1341
1342 Parameters
1343 ----------
1344 ampName : `str`
1345 Amplifier's name.
1346 """
1347
1348 gain = self.gain[ampName]
1349 noise = self.noise[ampName]
1350 ptcTurnoff = self.ptcTurnoff[ampName]
1351
1352 # Check if gain is not positive or is np.nan
1353 if not (isinstance(gain, (int, float)) and gain > 0) or math.isnan(gain):
1354 if doWarn:
1355 self.log.warning(f"Invalid gain value {gain}"
1356 " Setting to default: Gain=1")
1357 gain = 1
1358
1359 # Check if noise is not positive or is np.nan
1360 if not (isinstance(noise, (int, float)) and noise > 0) or math.isnan(noise):
1361 if doWarn:
1362 self.log.warning(f"Invalid noise value: {noise}"
1363 " Setting to default: Noise=1")
1364 noise = 1
1365
1366 # Check if ptcTurnoff is not positive or is np.nan
1367 if not (isinstance(ptcTurnoff, (int, float)) and ptcTurnoff > 0) or math.isnan(ptcTurnoff):
1368 if doWarn:
1369 self.log.warning(f"Invalid PTC turnoff value: {ptcTurnoff}"
1370 " Setting to default: PTC Turnoff=2e19")
1371 ptcTurnoff = 2e19
1372
1373 self.gain[ampName] = gain
1374 self.noise[ampName] = noise
1375 self.ptcTurnoff[ampName] = ptcTurnoff
1376
1377 def evalPtcModel(self, mu):
1378 """Computes the covariance model at specific signal levels.
1379
1380 Parameters
1381 ----------
1382 mu : `numpy.array`, (N,)
1383 List of mean signals in ADU.
1384
1385 Raises
1386 ------
1387 RuntimeError
1388 Raised if ptcFitType is invalid.
1389
1390 Returns
1391 -------
1392 covModel : `numpy.array`, (N, M, M)
1393 Covariances model at mu (in ADU^2).
1394
1395 Notes
1396 -----
1397 Computes the covModel for all mu, and it returns
1398 cov[N, M, M], where the variance model is cov[:,0,0].
1399 Both mu and cov are in ADUs and ADUs squared. This
1400 routine evaluates the approximation in Eq. 16 of
1401 Astier+19 (1905.08677)
1402 if self.ptcFitType == EXPAPPROXIMATION, and Eq. 20 of
1403 Astier+19 if self.ptcFitType == FULLCOVARIANCE(_NO_B).
1404
1405 The EXPAPPROXIMATION model (Eq. 16 of Astier+19) is
1406 only an approximation for the variance (cov[0,0]),
1407 so the function returns covModel of shape (N,),
1408 representing an array of [C_{00}]
1409 if self.ptcFitType == EXPAPPROXIMATION.
1410 """
1411
1412 ampNames = self.ampNames
1413 covModel = {ampName: np.array([]) for ampName in ampNames}
1414
1415 if self.ptcFitType == "POLYNOMIAL":
1416 warnings.warn("The `POLYNOMIAL` fit type is deprecated; it will "
1417 "be removed from the Rubin Observatory Science "
1418 "Pipelines after v30.",
1419 category=FutureWarning)
1420 # TODO: DM-52720 - remove deprecated POLYNOMIAL fit
1421 # and legacy turnoff
1422 pars = self.ptcFitPars
1423
1424 for ampName in ampNames:
1425 c00 = poly.polyval(mu, [*pars[ampName]])
1426 covModel[ampName] = c00
1427 elif self.ptcFitType == "EXPAPPROXIMATION":
1428 pars = self.ptcFitPars
1429
1430 for ampName in ampNames:
1431 a00, gain, noise = pars[ampName]
1432 f1 = 0.5/(a00*gain*gain)*(np.exp(2*a00*mu*gain)-1)
1433 f2 = noise/(gain*gain)
1434 c00 = f1 + f2
1435 covModel[ampName] = c00
1436
1437 elif self.ptcFitType in ["FULLCOVARIANCE", "FULLCOVARIANCE_NO_B"]:
1438 for ampName in ampNames:
1439 noiseMatrix = self.noiseMatrix[ampName]
1440 gain = self.gain[ampName]
1441 aMatrix = self.aMatrix[ampName]
1442 bMatrix = self.bMatrix[ampName]
1443 cMatrix = aMatrix*bMatrix
1444
1445 matrixSideFit = self.covMatrixSideFullCovFit
1446 sa = (matrixSideFit, matrixSideFit)
1447
1448 # pad a with zeros and symmetrize
1449 aEnlarged = np.zeros((int(sa[0]*1.5)+1, int(sa[1]*1.5)+1))
1450 aEnlarged[0:sa[0], 0:sa[1]] = aMatrix
1451 aSym = symmetrize(aEnlarged)
1452
1453 # pad c with zeros and symmetrize
1454 cEnlarged = np.zeros((int(sa[0]*1.5)+1, int(sa[1]*1.5)+1))
1455 cEnlarged[0:sa[0], 0:sa[1]] = cMatrix
1456
1457 cSym = symmetrize(cEnlarged)
1458 a2 = fftconvolve(aSym, aSym, mode='same')
1459 a3 = fftconvolve(a2, aSym, mode='same')
1460 ac = fftconvolve(aSym, cSym, mode='same')
1461 (xc, yc) = np.unravel_index(np.abs(aSym).argmax(), a2.shape)
1462
1463 a1 = aMatrix[np.newaxis, :, :]
1464 a2 = a2[np.newaxis, xc:xc + matrixSideFit, yc:yc + matrixSideFit]
1465 a3 = a3[np.newaxis, xc:xc + matrixSideFit, yc:yc + matrixSideFit]
1466 ac = ac[np.newaxis, xc:xc + matrixSideFit, yc:yc + matrixSideFit]
1467 c1 = cMatrix[np.newaxis, ::]
1468
1469 # assumes that mu is 1d
1470 bigMu = mu[:, np.newaxis, np.newaxis]*gain
1471 # c(=a*b in Astier+19) also has a contribution to the last
1472 # term, that is absent for now.
1473
1474 covModel[ampName] = (bigMu/(gain*gain)*(a1*bigMu+2./3.*(bigMu*bigMu)*(a2 + c1)
1475 + (1./3.*a3 + 5./6.*ac)*(bigMu*bigMu*bigMu))
1476 + noiseMatrix[np.newaxis, :, :]/gain**2)
1477
1478 # add the Poisson term, and the read out noise (variance)
1479 covModel[ampName][:, 0, 0] += mu/gain
1480 else:
1481 raise RuntimeError("Cannot compute PTC model for "
1482 "ptcFitType %s." % self.ptcFitType)
1483
1484 return covModel
1485
1487 """Ensure covMatrixSideFullCovFit <= covMatrixSide."""
1489 self.log.warning("covMatrixSideFullCovFit > covMatrixSide "
1490 f"({self.covMatrixSideFullCovFit} > {self.covMatrixSide})."
1491 "Setting the former to the latter.")
updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition calibType.py:210
validateGainNoiseTurnoffValues(self, ampName, doWarn=False)
__init__(self, ampNames=[], ptcFitType=None, covMatrixSide=1, covMatrixSideFullCovFit=None, **kwargs)
setAmpValuesPartialDataset(self, ampName, inputExpIdPair=(-1, -1), inputExpPairMjdStart=np.nan, rawExpTime=np.nan, rawMean=np.nan, rawVar=np.nan, rawDelta=np.nan, rowMeanVariance=np.nan, photoCharge=np.nan, photoChargeDelta=np.nan, ampOffset=np.nan, expIdMask=False, expIdRolloffMask=False, nPixelCovariance=-1, covariance=None, covSqrtWeights=None, gain=np.nan, noise=np.nan, overscanMedianLevel=np.nan, histVar=np.nan, histChi2Dof=np.nan, kspValue=0.0)