Loading [MathJax]/extensions/tex2jax.js
LSST Applications 28.0.0,g1653933729+a8ce1bb630,g1a997c3884+a8ce1bb630,g28da252d5a+5bd70b7e6d,g2bbee38e9b+638fca75ac,g2bc492864f+638fca75ac,g3156d2b45e+07302053f8,g347aa1857d+638fca75ac,g35bb328faa+a8ce1bb630,g3a166c0a6a+638fca75ac,g3e281a1b8c+7bbb0b2507,g4005a62e65+17cd334064,g414038480c+5b5cd4fff3,g41af890bb2+4ffae9de63,g4e1a3235cc+0f1912dca3,g6249c6f860+3c3976f90c,g80478fca09+46aba80bd6,g82479be7b0+77990446f6,g858d7b2824+78ba4d1ce1,g89c8672015+f667a5183b,g9125e01d80+a8ce1bb630,ga5288a1d22+2a6264e9ca,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc22bb204ba+78ba4d1ce1,gc28159a63d+638fca75ac,gcf0d15dbbd+32ddb6096f,gd6b7c0dfd1+3e339405e9,gda3e153d99+78ba4d1ce1,gda6a2b7d83+32ddb6096f,gdaeeff99f8+1711a396fd,gdd5a9049c5+b18c39e5e3,ge2409df99d+a5e4577cdc,ge33fd446bb+78ba4d1ce1,ge79ae78c31+638fca75ac,gf0baf85859+64e8883e75,gf5289d68f6+e1b046a8d7,gfa443fc69c+91d9ed1ecf,gfda6b12a05+8419469a56
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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 numbers
29import numpy as np
30import math
31from astropy.table import Table
32import numpy.polynomial.polynomial as poly
33from scipy.signal import fftconvolve
34
35from lsst.ip.isr import IsrCalib
36
37
38def symmetrize(inputArray):
39 """ Copy array over 4 quadrants prior to convolution.
40
41 Parameters
42 ----------
43 inputarray : `numpy.array`
44 Input array to symmetrize.
45
46 Returns
47 -------
48 aSym : `numpy.array`
49 Symmetrized array.
50 """
51
52 targetShape = list(inputArray.shape)
53 r1, r2 = inputArray.shape[-1], inputArray.shape[-2]
54 targetShape[-1] = 2*r1-1
55 targetShape[-2] = 2*r2-1
56 aSym = np.ndarray(tuple(targetShape))
57 aSym[..., r2-1:, r1-1:] = inputArray
58 aSym[..., r2-1:, r1-1::-1] = inputArray
59 aSym[..., r2-1::-1, r1-1::-1] = inputArray
60 aSym[..., r2-1::-1, r1-1:] = inputArray
61
62 return aSym
63
64
66 """A simple class to hold the output data from the PTC task.
67
68 The dataset is made up of a dictionary for each item, keyed by the
69 amplifiers' names, which much be supplied at construction time.
70 New items cannot be added to the class to save accidentally saving to the
71 wrong property, and the class can be frozen if desired.
72 inputExpIdPairs records the exposures used to produce the data.
73 When fitPtc() or fitCovariancesAstier() is run, a mask is built up, which
74 is by definition always the same length as inputExpIdPairs, rawExpTimes,
75 rawMeans and rawVars, and is a list of bools, which are incrementally set
76 to False as points are discarded from the fits.
77 PTC fit parameters for polynomials are stored in a list in ascending order
78 of polynomial term, i.e. par[0]*x^0 + par[1]*x + par[2]*x^2 etc
79 with the length of the list corresponding to the order of the polynomial
80 plus one.
81
82 Parameters
83 ----------
84 ampNames : `list`
85 List with the names of the amplifiers of the detector at hand.
86 ptcFitType : `str`, optional
87 Type of model fitted to the PTC: "POLYNOMIAL", "EXPAPPROXIMATION",
88 or "FULLCOVARIANCE".
89 covMatrixSide : `int`, optional
90 Maximum lag of measured covariances (size of square covariance
91 matrices).
92 covMatrixSideFullCovFit : `int`, optional
93 Maximum covariances lag for FULLCOVARIANCE fit. It should be less or
94 equal than covMatrixSide.
95 kwargs : `dict`, optional
96 Other keyword arguments to pass to the parent init.
97
98 Notes
99 -----
100 The stored attributes are:
101
102 badAmps : `list` [`str`]
103 List with bad amplifiers names.
104 inputExpIdPairs : `dict`, [`str`, `list`]
105 Dictionary keyed by amp names containing the input exposures IDs.
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 rawExpTimes : `dict`, [`str`, `np.ndarray`]
112 Dictionary keyed by amp names containing the unmasked exposure times.
113 rawMeans : `dict`, [`str`, `np.ndarray`]
114 Dictionary keyed by amp names containing the unmasked average of the
115 means of the exposures in each flat pair (units: adu).
116 rawVars : `dict`, [`str`, `np.ndarray`]
117 Dictionary keyed by amp names containing the variance of the
118 difference image of the exposures in each flat pair (units: adu^2).
119 rowMeanVariance : `dict`, [`str`, `np.ndarray`]
120 Dictionary keyed by amp names containing the variance of the
121 means of the rows of the difference image of the exposures
122 in each flat pair (units: adu^2).
123 histVars : `dict`, [`str`, `np.ndarray`]
124 Dictionary keyed by amp names containing the variance of the
125 difference image of the exposures in each flat pair estimated
126 by fitting a Gaussian model.
127 histChi2Dofs : `dict`, [`str`, `np.ndarray`]
128 Dictionary keyed by amp names containing the chi-squared per degree
129 of freedom fitting the difference image to a Gaussian model.
130 kspValues : `dict`, [`str`, `np.ndarray`]
131 Dictionary keyed by amp names containing the KS test p-value from
132 fitting the difference image to a Gaussian model.
133 gain : `dict`, [`str`, `float`]
134 Dictionary keyed by amp names containing the fitted gains. May be
135 adjusted by amp-offset gain ratios if configured in PTC solver.
136 gainUnadjusted : `dict`, [`str`, `float`]
137 Dictionary keyed by amp names containing unadjusted (raw) fit gain
138 values. May be the same as gain values if amp-offset adjustment
139 is not turned on.
140 gainErr : `dict`, [`str`, `float`]
141 Dictionary keyed by amp names containing the errors on the
142 fitted gains.
143 gainList : `dict`, [`str`, `np.ndarray`]
144 Dictionary keyed by amp names containing the gain estimated from
145 each flat pair.
146 noiseList : `dict`, [`str`, `np.ndarray`]
147 Dictionary keyed by amp names containing the mean overscan
148 standard deviation from each flat pair (units: adu).
149 noise : `dict`, [`str`, `float`]
150 Dictionary keyed by amp names containing the fitted noise
151 (units: electron).
152 noiseErr : `dict`, [`str`, `float`]
153 Dictionary keyed by amp names containing the errors on the fitted
154 noise (units: electron).
155 ampOffsets : `dict`, [`str`, `float`]
156 Dictionary keyed by amp names containing amp-to-amp offsets
157 (units: adu).
158 ptcFitPars : `dict`, [`str`, `np.ndarray`]
159 Dictionary keyed by amp names containing the fitted parameters of the
160 PTC model for ptcFitType in ["POLYNOMIAL", "EXPAPPROXIMATION"].
161 ptcFitParsError : `dict`, [`str`, `np.ndarray`]
162 Dictionary keyed by amp names containing the errors on the fitted
163 parameters of the PTC model for ptcFitType in
164 ["POLYNOMIAL", "EXPAPPROXIMATION"].
165 ptcFitChiSq : `dict`, [`str`, `float`]
166 Dictionary keyed by amp names containing the reduced chi squared
167 of the fit for ptcFitType in ["POLYNOMIAL", "EXPAPPROXIMATION"].
168 ptcTurnoff : `dict` [`str, `float`]
169 Flux value (in adu) where the variance of the PTC curve starts
170 decreasing consistently.
171 ptcTurnoffSamplingError : `dict` [`str`, `float`]
172 ``Sampling`` error on the ptcTurnoff, based on the flux sampling
173 of the input PTC (units: adu).
174 covariances : `dict`, [`str`, `np.ndarray`]
175 Dictionary keyed by amp names containing a list of measured
176 covariances per mean flux (units: adu^2).
177 covariancesModel : `dict`, [`str`, `np.ndarray`]
178 Dictionary keyed by amp names containinging covariances model
179 (Eq. 20 of Astier+19) per mean flux (units: adu^2).
180 covariancesSqrtWeights : `dict`, [`str`, `np.ndarray`]
181 Dictionary keyed by amp names containinging sqrt. of covariances
182 weights (units: 1/adu).
183 aMatrix : `dict`, [`str`, `np.ndarray`]
184 Dictionary keyed by amp names containing the "a" parameters from
185 the model in Eq. 20 of Astier+19 (units: 1/electron).
186 bMatrix : `dict`, [`str`, `np.ndarray`]
187 Dictionary keyed by amp names containing the "b" parameters from
188 the model in Eq. 20 of Astier+19 (units: 1/electron).
189 noiseMatrix : `dict`, [`str`, `np.ndarray`]
190 Dictionary keyed by amp names containing the "noise" parameters from
191 the model in Eq. 20 of Astier+19 (units: electron^2).
192 covariancesModelNoB : `dict`, [`str`, `np.ndarray`]
193 Dictionary keyed by amp names containing covariances model
194 (with 'b'=0 in Eq. 20 of Astier+19)
195 per mean flux (units: adu^2).
196 aMatrixNoB : `dict`, [`str`, `np.ndarray`]
197 Dictionary keyed by amp names containing the "a" parameters from the
198 model in Eq. 20 of Astier+19
199 (and 'b' = 0) (units: 1/electron).
200 noiseMatrixNoB : `dict`, [`str`, `np.ndarray`]
201 Dictionary keyed by amp names containing the "noise" parameters from
202 the model in Eq. 20 of Astier+19, with 'b' = 0 (units: electron^2).
203 finalVars : `dict`, [`str`, `np.ndarray`]
204 Dictionary keyed by amp names containing the masked variance of the
205 difference image of each flat
206 pair. If needed, each array will be right-padded with
207 np.nan to match the length of rawExpTimes.
208 finalModelVars : `dict`, [`str`, `np.ndarray`]
209 Dictionary keyed by amp names containing the masked modeled
210 variance of the difference image of each flat pair. If needed, each
211 array will be right-padded with np.nan to match the length of
212 rawExpTimes.
213 finalMeans : `dict`, [`str`, `np.ndarray`]
214 Dictionary keyed by amp names containing the masked average of the
215 means of the exposures in each flat pair. If needed, each array
216 will be right-padded with np.nan to match the length of
217 rawExpTimes.
218 photoCharges : `dict`, [`str`, `np.ndarray`]
219 Dictionary keyed by amp names containing the integrated photocharge
220 for linearity calibration.
221 auxValues : `dict`, [`str`, `np.ndarray`]
222 Dictionary of per-detector auxiliary header values that can be used
223 for PTC, linearity computation.
224
225 Version 1.1 adds the `ptcTurnoff` attribute.
226 Version 1.2 adds the `histVars`, `histChi2Dofs`, and `kspValues`
227 attributes.
228 Version 1.3 adds the `noiseMatrix` and `noiseMatrixNoB` attributes.
229 Version 1.4 adds the `auxValues` attribute.
230 Version 1.5 adds the `covMatrixSideFullCovFit` attribute.
231 Version 1.6 adds the `rowMeanVariance` attribute.
232 Version 1.7 adds the `noiseList` attribute.
233 Version 1.8 adds the `ptcTurnoffSamplingError` attribute.
234 Version 1.9 standardizes PTC noise units to electron.
235 Version 2.0 adds the `ampOffsets`, `gainUnadjusted`, and
236 `gainList` attributes.
237 """
238
239 _OBSTYPE = 'PTC'
240 _SCHEMA = 'Gen3 Photon Transfer Curve'
241 # When adding a new field to update the version, be sure to update the
242 # following methods:
243 # * __init__()
244 # * fromDict()
245 # * toDict()
246 # * fromTable()
247 # * toTable()
248 # * setAmpValuesPartialDataset()
249 # * appendPartialPtc()
250 # * sort()
251 # * _checkTypes() in test_ptcDataset.py
252 # * test_ptcDataset() in test_ptcDataset.py
253 # * test_ptcDatasetSort in test_ptcDataset.py
254 # * test_ptcDatasetAppend in test_ptcDataset.py
255 _VERSION = 2.0
256
257 def __init__(self, ampNames=[], ptcFitType=None, covMatrixSide=1,
258 covMatrixSideFullCovFit=None, **kwargs):
259 self.ptcFitType = ptcFitType
260 self.ampNames = ampNames
261 self.covMatrixSide = covMatrixSide
262 if covMatrixSideFullCovFit is None:
263 self.covMatrixSideFullCovFit = covMatrixSide
264 else:
265 self.covMatrixSideFullCovFit = covMatrixSideFullCovFit
266
267 self.badAmps = []
268
269 self.inputExpIdPairs = {ampName: [] for ampName in ampNames}
270 self.expIdMask = {ampName: np.array([], dtype=bool) for ampName in ampNames}
271 self.rawExpTimes = {ampName: np.array([]) for ampName in ampNames}
272 self.rawMeans = {ampName: np.array([]) for ampName in ampNames}
273 self.rawVars = {ampName: np.array([]) for ampName in ampNames}
274 self.rowMeanVariance = {ampName: np.array([]) for ampName in ampNames}
275 self.photoCharges = {ampName: np.array([]) for ampName in ampNames}
276 self.ampOffsets = {ampName: np.array([]) for ampName in ampNames}
277
278 self.gain = {ampName: np.nan for ampName in ampNames}
279 self.gainUnadjusted = {ampName: np.nan for ampName in ampNames}
280 self.gainErr = {ampName: np.nan for ampName in ampNames}
281 self.gainList = {ampName: np.array([]) for ampName in ampNames}
282 self.noiseList = {ampName: np.array([]) for ampName in ampNames}
283 self.noise = {ampName: np.nan for ampName in ampNames}
284 self.noiseErr = {ampName: np.nan for ampName in ampNames}
285
286 self.histVars = {ampName: np.array([]) for ampName in ampNames}
287 self.histChi2Dofs = {ampName: np.array([]) for ampName in ampNames}
288 self.kspValues = {ampName: np.array([]) for ampName in ampNames}
289
290 self.ptcFitPars = {ampName: np.array([]) for ampName in ampNames}
291 self.ptcFitParsError = {ampName: np.array([]) for ampName in ampNames}
292 self.ptcFitChiSq = {ampName: np.nan for ampName in ampNames}
293 self.ptcTurnoff = {ampName: np.nan for ampName in ampNames}
294 self.ptcTurnoffSamplingError = {ampName: np.nan for ampName in ampNames}
295
296 self.covariances = {ampName: np.array([]) for ampName in ampNames}
297 self.covariancesModel = {ampName: np.array([]) for ampName in ampNames}
298 self.covariancesSqrtWeights = {ampName: np.array([]) for ampName in ampNames}
299 self.aMatrix = {ampName: np.array([]) for ampName in ampNames}
300 self.bMatrix = {ampName: np.array([]) for ampName in ampNames}
301 self.noiseMatrix = {ampName: np.array([]) for ampName in ampNames}
302 self.covariancesModelNoB = {ampName: np.array([]) for ampName in ampNames}
303 self.aMatrixNoB = {ampName: np.array([]) for ampName in ampNames}
304 self.noiseMatrixNoB = {ampName: np.array([]) for ampName in ampNames}
305
306 self.finalVars = {ampName: np.array([]) for ampName in ampNames}
307 self.finalModelVars = {ampName: np.array([]) for ampName in ampNames}
308 self.finalMeans = {ampName: np.array([]) for ampName in ampNames}
309
310 # Try this as a dict of arrays.
311 self.auxValues = {}
312
313 super().__init__(**kwargs)
314 self.requiredAttributesrequiredAttributesrequiredAttributes.update(['badAmps', 'inputExpIdPairs', 'expIdMask', 'rawExpTimes',
315 'rawMeans', 'rawVars', 'rowMeanVariance', 'gain',
316 'gainErr', 'noise', 'noiseErr', 'noiseList',
317 'ptcFitPars', 'ptcFitParsError', 'ptcFitChiSq', 'ptcTurnoff',
318 'aMatrixNoB', 'covariances', 'covariancesModel',
319 'covariancesSqrtWeights', 'covariancesModelNoB',
320 'aMatrix', 'bMatrix', 'noiseMatrix', 'noiseMatrixNoB', 'finalVars',
321 'finalModelVars', 'finalMeans', 'photoCharges', 'histVars',
322 'histChi2Dofs', 'kspValues', 'auxValues', 'ptcTurnoffSamplingError',
323 'ampOffsets', 'gainUnadjusted', 'gainList'])
324
325 self.updateMetadataupdateMetadata(setCalibInfo=True, setCalibId=True, **kwargs)
327
329 self,
330 ampName,
331 inputExpIdPair=(-1, -1),
332 rawExpTime=np.nan,
333 rawMean=np.nan,
334 rawVar=np.nan,
335 rowMeanVariance=np.nan,
336 photoCharge=np.nan,
337 ampOffset=np.nan,
338 expIdMask=False,
339 covariance=None,
340 covSqrtWeights=None,
341 gain=np.nan,
342 noise=np.nan,
343 histVar=np.nan,
344 histChi2Dof=np.nan,
345 kspValue=0.0,
346 ):
347 """
348 Set the amp values for a partial PTC Dataset (from cpExtractPtcTask).
349
350 Parameters
351 ----------
352 ampName : `str`
353 Name of the amp to set the values.
354 inputExpIdPair : `tuple` [`int`]
355 Exposure IDs of input pair.
356 rawExpTime : `float`, optional
357 Exposure time for this exposure pair.
358 rawMean : `float`, optional
359 Average of the means of the exposures in this pair.
360 rawVar : `float`, optional
361 Variance of the difference of the exposures in this pair.
362 rowMeanVariance : `float`, optional
363 Variance of the means of the rows in the difference image
364 of the exposures in this pair.
365 photoCharge : `float`, optional
366 Integrated photocharge for flat pair for linearity calibration.
367 ampOffset : `float`, optional
368 Amp offset for this amplifier.
369 expIdMask : `bool`, optional
370 Flag setting if this exposure pair should be used (True)
371 or not used (False).
372 covariance : `np.ndarray` or None, optional
373 Measured covariance for this exposure pair.
374 covSqrtWeights : `np.ndarray` or None, optional
375 Measured sqrt of covariance weights in this exposure pair.
376 gain : `float`, optional
377 Estimated gain for this exposure pair.
378 noise : `float`, optional
379 Estimated read noise for this exposure pair.
380 histVar : `float`, optional
381 Variance estimated from fitting a histogram with a Gaussian model.
382 histChi2Dof : `float`, optional
383 Chi-squared per degree of freedom from Gaussian histogram fit.
384 kspValue : `float`, optional
385 KS test p-value from the Gaussian histogram fit.
386 """
387 nanMatrix = np.full((self.covMatrixSide, self.covMatrixSide), np.nan)
388 nanMatrixFit = np.full((self.covMatrixSideFullCovFit,
389 self.covMatrixSideFullCovFit), np.nan)
390 if covariance is None:
391 covariance = nanMatrix
392 if covSqrtWeights is None:
393 covSqrtWeights = nanMatrix
394
395 self.inputExpIdPairs[ampName] = [inputExpIdPair]
396 self.rawExpTimes[ampName] = np.array([rawExpTime])
397 self.rawMeans[ampName] = np.array([rawMean])
398 self.rawVars[ampName] = np.array([rawVar])
399 self.rowMeanVariance[ampName] = np.array([rowMeanVariance])
400 self.photoCharges[ampName] = np.array([photoCharge])
401 self.ampOffsets[ampName] = np.array([ampOffset])
402 self.expIdMask[ampName] = np.array([expIdMask])
403 self.covariances[ampName] = np.array([covariance])
404 self.covariancesSqrtWeights[ampName] = np.array([covSqrtWeights])
405 self.gain[ampName] = gain
406 self.gainUnadjusted[ampName] = gain
407 self.gainList[ampName] = np.array([gain])
408 self.noise[ampName] = noise
409 self.noiseList[ampName] = np.array([noise])
410 self.histVars[ampName] = np.array([histVar])
411 self.histChi2Dofs[ampName] = np.array([histChi2Dof])
412 self.kspValues[ampName] = np.array([kspValue])
413
414 # From FULLCOVARIANCE model
415 self.covariancesModel[ampName] = np.array([nanMatrixFit])
416 self.covariancesModelNoB[ampName] = np.array([nanMatrixFit])
417 self.aMatrix[ampName] = nanMatrixFit
418 self.bMatrix[ampName] = nanMatrixFit
419 self.aMatrixNoB[ampName] = nanMatrixFit
420 self.noiseMatrix[ampName] = nanMatrixFit
421 self.noiseMatrixNoB[ampName] = nanMatrixFit
422
423 # Filler values.
424 self.finalVars[ampName] = np.array([np.nan])
425 self.finalModelVars[ampName] = np.array([np.nan])
426 self.finalMeans[ampName] = np.array([np.nan])
427
428 def setAuxValuesPartialDataset(self, auxDict):
429 """
430 Set a dictionary of auxiliary values for a partial dataset.
431
432 Parameters
433 ----------
434 auxDict : `dict` [`str`, `float`]
435 Dictionary of float values.
436 """
437 for key, value in auxDict.items():
438 if isinstance(value, numbers.Integral):
439 self.auxValues[key] = np.atleast_1d(np.asarray(value).astype(np.int64))
440 elif isinstance(value, (str, np.str_, np.string_)):
441 self.auxValues[key] = np.atleast_1d(np.asarray(value))
442 else:
443 self.auxValues[key] = np.atleast_1d(np.array(value, dtype=np.float64))
444
445 def updateMetadata(self, **kwargs):
446 """Update calibration metadata.
447 This calls the base class's method after ensuring the required
448 calibration keywords will be saved.
449
450 Parameters
451 ----------
452 setDate : `bool`, optional
453 Update the CALIBDATE fields in the metadata to the current
454 time. Defaults to False.
455 kwargs :
456 Other keyword parameters to set in the metadata.
457 """
458 super().updateMetadata(PTC_FIT_TYPE=self.ptcFitType, **kwargs)
459
460 @classmethod
461 def fromDict(cls, dictionary):
462 """Construct a calibration from a dictionary of properties.
463 Must be implemented by the specific calibration subclasses.
464
465 Parameters
466 ----------
467 dictionary : `dict`
468 Dictionary of properties.
469
470 Returns
471 -------
472 calib : `lsst.ip.isr.PhotonTransferCurveDataset`
473 Constructed calibration.
474
475 Raises
476 ------
477 RuntimeError
478 Raised if the supplied dictionary is for a different
479 calibration.
480 """
481 calib = cls()
482 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
483 raise RuntimeError(f"Incorrect Photon Transfer Curve dataset supplied. "
484 f"Expected {calib._OBSTYPE}, found {dictionary['metadata']['OBSTYPE']}")
485 calib.setMetadata(dictionary['metadata'])
486 calib.ptcFitType = dictionary['ptcFitType']
487 calib.covMatrixSide = dictionary['covMatrixSide']
488 calib.covMatrixSideFullCovFit = dictionary['covMatrixSideFullCovFit']
489 calib.badAmps = np.array(dictionary['badAmps'], 'str').tolist()
490 calib.ampNames = []
491
492 # The cov matrices are square
493 covMatrixSide = calib.covMatrixSide
494 covMatrixSideFullCovFit = calib.covMatrixSideFullCovFit
495 # Number of final signal levels
496 covDimensionsProduct = len(np.array(list(dictionary['covariances'].values())[0]).ravel())
497 nSignalPoints = int(covDimensionsProduct/(covMatrixSide*covMatrixSide))
498
499 for ampName in dictionary['ampNames']:
500 calib.ampNames.append(ampName)
501 calib.inputExpIdPairs[ampName] = dictionary['inputExpIdPairs'][ampName]
502 calib.expIdMask[ampName] = np.array(dictionary['expIdMask'][ampName])
503 calib.rawExpTimes[ampName] = np.array(dictionary['rawExpTimes'][ampName], dtype=np.float64)
504 calib.rawMeans[ampName] = np.array(dictionary['rawMeans'][ampName], dtype=np.float64)
505 calib.rawVars[ampName] = np.array(dictionary['rawVars'][ampName], dtype=np.float64)
506 calib.rowMeanVariance[ampName] = np.array(dictionary['rowMeanVariance'][ampName],
507 dtype=np.float64)
508 calib.gain[ampName] = float(dictionary['gain'][ampName])
509 calib.gainErr[ampName] = float(dictionary['gainErr'][ampName])
510 calib.gainUnadjusted[ampName] = float(dictionary['gainUnadjusted'][ampName])
511 calib.gainList[ampName] = np.array(dictionary['gainList'][ampName], dtype=np.float64)
512 calib.noiseList[ampName] = np.array(dictionary['noiseList'][ampName], dtype=np.float64)
513 calib.noise[ampName] = float(dictionary['noise'][ampName])
514 calib.noiseErr[ampName] = float(dictionary['noiseErr'][ampName])
515 calib.histVars[ampName] = np.array(dictionary['histVars'][ampName], dtype=np.float64)
516 calib.histChi2Dofs[ampName] = np.array(dictionary['histChi2Dofs'][ampName], dtype=np.float64)
517 calib.kspValues[ampName] = np.array(dictionary['kspValues'][ampName], dtype=np.float64)
518 calib.ptcFitPars[ampName] = np.array(dictionary['ptcFitPars'][ampName], dtype=np.float64)
519 calib.ptcFitParsError[ampName] = np.array(dictionary['ptcFitParsError'][ampName],
520 dtype=np.float64)
521 calib.ptcFitChiSq[ampName] = float(dictionary['ptcFitChiSq'][ampName])
522 calib.ptcTurnoff[ampName] = float(dictionary['ptcTurnoff'][ampName])
523 calib.ptcTurnoffSamplingError[ampName] = float(dictionary['ptcTurnoffSamplingError'][ampName])
524 if nSignalPoints > 0:
525 # Regular dataset
526 calib.covariances[ampName] = np.array(dictionary['covariances'][ampName],
527 dtype=np.float64).reshape(
528 (nSignalPoints, covMatrixSide, covMatrixSide))
529 calib.covariancesModel[ampName] = np.array(
530 dictionary['covariancesModel'][ampName],
531 dtype=np.float64).reshape(
532 (nSignalPoints, covMatrixSideFullCovFit, covMatrixSideFullCovFit))
533 calib.covariancesSqrtWeights[ampName] = np.array(
534 dictionary['covariancesSqrtWeights'][ampName],
535 dtype=np.float64).reshape(
536 (nSignalPoints, covMatrixSide, covMatrixSide))
537 calib.aMatrix[ampName] = np.array(dictionary['aMatrix'][ampName],
538 dtype=np.float64).reshape(
539 (covMatrixSideFullCovFit, covMatrixSideFullCovFit))
540 calib.bMatrix[ampName] = np.array(dictionary['bMatrix'][ampName],
541 dtype=np.float64).reshape(
542 (covMatrixSideFullCovFit, covMatrixSideFullCovFit))
543 calib.covariancesModelNoB[ampName] = np.array(
544 dictionary['covariancesModelNoB'][ampName], dtype=np.float64).reshape(
545 (nSignalPoints, covMatrixSideFullCovFit, covMatrixSideFullCovFit))
546 calib.aMatrixNoB[ampName] = np.array(
547 dictionary['aMatrixNoB'][ampName],
548 dtype=np.float64).reshape((covMatrixSideFullCovFit, covMatrixSideFullCovFit))
549 calib.noiseMatrix[ampName] = np.array(
550 dictionary['noiseMatrix'][ampName],
551 dtype=np.float64).reshape((covMatrixSideFullCovFit, covMatrixSideFullCovFit))
552 calib.noiseMatrixNoB[ampName] = np.array(
553 dictionary['noiseMatrixNoB'][ampName],
554 dtype=np.float64).reshape((covMatrixSideFullCovFit, covMatrixSideFullCovFit))
555 else:
556 # Empty dataset
557 calib.covariances[ampName] = np.array([], dtype=np.float64)
558 calib.covariancesModel[ampName] = np.array([], dtype=np.float64)
559 calib.covariancesSqrtWeights[ampName] = np.array([], dtype=np.float64)
560 calib.aMatrix[ampName] = np.array([], dtype=np.float64)
561 calib.bMatrix[ampName] = np.array([], dtype=np.float64)
562 calib.covariancesModelNoB[ampName] = np.array([], dtype=np.float64)
563 calib.aMatrixNoB[ampName] = np.array([], dtype=np.float64)
564 calib.noiseMatrix[ampName] = np.array([], dtype=np.float64)
565 calib.noiseMatrixNoB[ampName] = np.array([], dtype=np.float64)
566
567 calib.finalVars[ampName] = np.array(dictionary['finalVars'][ampName], dtype=np.float64)
568 calib.finalModelVars[ampName] = np.array(dictionary['finalModelVars'][ampName], dtype=np.float64)
569 calib.finalMeans[ampName] = np.array(dictionary['finalMeans'][ampName], dtype=np.float64)
570 calib.photoCharges[ampName] = np.array(dictionary['photoCharges'][ampName], dtype=np.float64)
571 calib.ampOffsets[ampName] = np.array(dictionary['ampOffsets'][ampName], dtype=np.float64)
572
573 for key, value in dictionary['auxValues'].items():
574 if isinstance(value[0], numbers.Integral):
575 calib.auxValues[key] = np.atleast_1d(np.asarray(value).astype(np.int64))
576 elif isinstance(value[0], (str, np.str_, np.string_)):
577 calib.auxValues[key] = np.atleast_1d(np.asarray(value))
578 else:
579 calib.auxValues[key] = np.atleast_1d(np.array(value, dtype=np.float64))
580
581 calib.updateMetadata()
582 return calib
583
584 def toDict(self):
585 """Return a dictionary containing the calibration properties.
586 The dictionary should be able to be round-tripped through
587 `fromDict`.
588
589 Returns
590 -------
591 dictionary : `dict`
592 Dictionary of properties.
593 """
595
596 outDict = dict()
597 metadata = self.getMetadata()
598 outDict['metadata'] = metadata
599
600 def _dictOfArraysToDictOfLists(dictOfArrays):
601 dictOfLists = {}
602 for key, value in dictOfArrays.items():
603 dictOfLists[key] = value.ravel().tolist()
604
605 return dictOfLists
606
607 outDict['ptcFitType'] = self.ptcFitType
608 outDict['covMatrixSide'] = self.covMatrixSide
609 outDict['covMatrixSideFullCovFit'] = self.covMatrixSideFullCovFit
610 outDict['ampNames'] = self.ampNames
611 outDict['badAmps'] = self.badAmps
612 outDict['inputExpIdPairs'] = self.inputExpIdPairs
613 outDict['expIdMask'] = _dictOfArraysToDictOfLists(self.expIdMask)
614 outDict['rawExpTimes'] = _dictOfArraysToDictOfLists(self.rawExpTimes)
615 outDict['rawMeans'] = _dictOfArraysToDictOfLists(self.rawMeans)
616 outDict['rawVars'] = _dictOfArraysToDictOfLists(self.rawVars)
617 outDict['rowMeanVariance'] = _dictOfArraysToDictOfLists(self.rowMeanVariance)
618 outDict['gain'] = self.gain
619 outDict['gainErr'] = self.gainErr
620 outDict['gainUnadjusted'] = self.gainUnadjusted
621 outDict['gainList'] = _dictOfArraysToDictOfLists(self.gainList)
622 outDict['noiseList'] = _dictOfArraysToDictOfLists(self.noiseList)
623 outDict['noise'] = self.noise
624 outDict['noiseErr'] = self.noiseErr
625 outDict['histVars'] = self.histVars
626 outDict['histChi2Dofs'] = self.histChi2Dofs
627 outDict['kspValues'] = self.kspValues
628 outDict['ptcFitPars'] = _dictOfArraysToDictOfLists(self.ptcFitPars)
629 outDict['ptcFitParsError'] = _dictOfArraysToDictOfLists(self.ptcFitParsError)
630 outDict['ptcFitChiSq'] = self.ptcFitChiSq
631 outDict['ptcTurnoff'] = self.ptcTurnoff
632 outDict['ptcTurnoffSamplingError'] = self.ptcTurnoffSamplingError
633 outDict['covariances'] = _dictOfArraysToDictOfLists(self.covariances)
634 outDict['covariancesModel'] = _dictOfArraysToDictOfLists(self.covariancesModel)
635 outDict['covariancesSqrtWeights'] = _dictOfArraysToDictOfLists(self.covariancesSqrtWeights)
636 outDict['aMatrix'] = _dictOfArraysToDictOfLists(self.aMatrix)
637 outDict['bMatrix'] = _dictOfArraysToDictOfLists(self.bMatrix)
638 outDict['noiseMatrix'] = _dictOfArraysToDictOfLists(self.noiseMatrix)
639 outDict['covariancesModelNoB'] = _dictOfArraysToDictOfLists(self.covariancesModelNoB)
640 outDict['aMatrixNoB'] = _dictOfArraysToDictOfLists(self.aMatrixNoB)
641 outDict['noiseMatrixNoB'] = _dictOfArraysToDictOfLists(self.noiseMatrixNoB)
642 outDict['finalVars'] = _dictOfArraysToDictOfLists(self.finalVars)
643 outDict['finalModelVars'] = _dictOfArraysToDictOfLists(self.finalModelVars)
644 outDict['finalMeans'] = _dictOfArraysToDictOfLists(self.finalMeans)
645 outDict['photoCharges'] = _dictOfArraysToDictOfLists(self.photoCharges)
646 outDict['ampOffsets'] = _dictOfArraysToDictOfLists(self.ampOffsets)
647 outDict['auxValues'] = _dictOfArraysToDictOfLists(self.auxValues)
648
649 return outDict
650
651 @classmethod
652 def fromTable(cls, tableList):
653 """Construct calibration from a list of tables.
654 This method uses the `fromDict` method to create the
655 calibration, after constructing an appropriate dictionary from
656 the input tables.
657
658 Parameters
659 ----------
660 tableList : `list` [`lsst.afw.table.Table`]
661 List of tables to use to construct the datasetPtc.
662
663 Returns
664 -------
665 calib : `lsst.ip.isr.PhotonTransferCurveDataset`
666 The calibration defined in the tables.
667 """
668 ptcTable = tableList[0]
669
670 metadata = ptcTable.meta
671 inDict = dict()
672 inDict['metadata'] = metadata
673 inDict['ampNames'] = []
674 inDict['ptcFitType'] = []
675 inDict['covMatrixSide'] = []
676 inDict['covMatrixSideFullCovFit'] = []
677 inDict['inputExpIdPairs'] = dict()
678 inDict['expIdMask'] = dict()
679 inDict['rawExpTimes'] = dict()
680 inDict['rawMeans'] = dict()
681 inDict['rawVars'] = dict()
682 inDict['rowMeanVariance'] = dict()
683 inDict['gain'] = dict()
684 inDict['gainErr'] = dict()
685 inDict['gainUnadjusted'] = dict()
686 inDict['gainList'] = dict()
687 inDict['noiseList'] = dict()
688 inDict['noise'] = dict()
689 inDict['noiseErr'] = dict()
690 inDict['histVars'] = dict()
691 inDict['histChi2Dofs'] = dict()
692 inDict['kspValues'] = dict()
693 inDict['ptcFitPars'] = dict()
694 inDict['ptcFitParsError'] = dict()
695 inDict['ptcFitChiSq'] = dict()
696 inDict['ptcTurnoff'] = dict()
697 inDict['ptcTurnoffSamplingError'] = dict()
698 inDict['covariances'] = dict()
699 inDict['covariancesModel'] = dict()
700 inDict['covariancesSqrtWeights'] = dict()
701 inDict['aMatrix'] = dict()
702 inDict['bMatrix'] = dict()
703 inDict['noiseMatrix'] = dict()
704 inDict['covariancesModelNoB'] = dict()
705 inDict['aMatrixNoB'] = dict()
706 inDict['noiseMatrixNoB'] = dict()
707 inDict['finalVars'] = dict()
708 inDict['finalModelVars'] = dict()
709 inDict['finalMeans'] = dict()
710 inDict['badAmps'] = []
711 inDict['photoCharges'] = dict()
712 inDict['ampOffsets'] = dict()
713
714 calibVersion = metadata['PTC_VERSION']
715 if calibVersion == 1.0:
716 cls().log.warning(f"Previous version found for PTC dataset: {calibVersion}. "
717 f"Setting 'ptcTurnoff' in all amps to last value in 'finalMeans'.")
718 for record in ptcTable:
719 ampName = record['AMPLIFIER_NAME']
720
721 inDict['ptcFitType'] = record['PTC_FIT_TYPE']
722 inDict['covMatrixSide'] = record['COV_MATRIX_SIDE']
723 inDict['ampNames'].append(ampName)
724 inDict['inputExpIdPairs'][ampName] = record['INPUT_EXP_ID_PAIRS'].tolist()
725 inDict['expIdMask'][ampName] = record['EXP_ID_MASK']
726 inDict['rawExpTimes'][ampName] = record['RAW_EXP_TIMES']
727 inDict['rawMeans'][ampName] = record['RAW_MEANS']
728 inDict['rawVars'][ampName] = record['RAW_VARS']
729 inDict['gain'][ampName] = record['GAIN']
730 inDict['gainErr'][ampName] = record['GAIN_ERR']
731 inDict['noise'][ampName] = record['NOISE']
732 inDict['noiseErr'][ampName] = record['NOISE_ERR']
733 inDict['ptcFitPars'][ampName] = record['PTC_FIT_PARS']
734 inDict['ptcFitParsError'][ampName] = record['PTC_FIT_PARS_ERROR']
735 inDict['ptcFitChiSq'][ampName] = record['PTC_FIT_CHI_SQ']
736 inDict['covariances'][ampName] = record['COVARIANCES']
737 inDict['covariancesModel'][ampName] = record['COVARIANCES_MODEL']
738 inDict['covariancesSqrtWeights'][ampName] = record['COVARIANCES_SQRT_WEIGHTS']
739 inDict['aMatrix'][ampName] = record['A_MATRIX']
740 inDict['bMatrix'][ampName] = record['B_MATRIX']
741 inDict['covariancesModelNoB'][ampName] = record['COVARIANCES_MODEL_NO_B']
742 inDict['aMatrixNoB'][ampName] = record['A_MATRIX_NO_B']
743 inDict['finalVars'][ampName] = record['FINAL_VARS']
744 inDict['finalModelVars'][ampName] = record['FINAL_MODEL_VARS']
745 inDict['finalMeans'][ampName] = record['FINAL_MEANS']
746 inDict['badAmps'] = record['BAD_AMPS'].tolist()
747 inDict['photoCharges'][ampName] = record['PHOTO_CHARGE']
748 if calibVersion == 1.0:
749 mask = record['FINAL_MEANS'].mask
750 array = record['FINAL_MEANS'][~mask]
751 if len(array) > 0:
752 inDict['ptcTurnoff'][ampName] = record['FINAL_MEANS'][~mask][-1]
753 else:
754 inDict['ptcTurnoff'][ampName] = np.nan
755 else:
756 inDict['ptcTurnoff'][ampName] = record['PTC_TURNOFF']
757 if calibVersion < 1.2:
758 inDict['histVars'][ampName] = np.array([np.nan])
759 inDict['histChi2Dofs'][ampName] = np.array([np.nan])
760 inDict['kspValues'][ampName] = np.array([0.0])
761 else:
762 inDict['histVars'][ampName] = record['HIST_VARS']
763 inDict['histChi2Dofs'][ampName] = record['HIST_CHI2_DOFS']
764 inDict['kspValues'][ampName] = record['KS_PVALUES']
765 if calibVersion < 1.3:
766 nanMatrix = np.full_like(inDict['aMatrix'][ampName], np.nan)
767 inDict['noiseMatrix'][ampName] = nanMatrix
768 inDict['noiseMatrixNoB'][ampName] = nanMatrix
769 else:
770 inDict['noiseMatrix'][ampName] = record['NOISE_MATRIX']
771 inDict['noiseMatrixNoB'][ampName] = record['NOISE_MATRIX_NO_B']
772 if calibVersion < 1.5:
773 # Matched to `COV_MATRIX_SIDE`. Same for all amps.
774 inDict['covMatrixSideFullCovFit'] = inDict['covMatrixSide']
775 else:
776 inDict['covMatrixSideFullCovFit'] = record['COV_MATRIX_SIDE_FULL_COV_FIT']
777 if calibVersion < 1.6:
778 inDict['rowMeanVariance'][ampName] = np.full((len(inDict['expIdMask'][ampName]),), np.nan)
779 else:
780 inDict['rowMeanVariance'][ampName] = record['ROW_MEAN_VARIANCE']
781 if calibVersion < 1.7:
782 inDict['noiseList'][ampName] = np.full_like(inDict['rawMeans'][ampName], np.nan)
783 else:
784 inDict['noiseList'][ampName] = record['NOISE_LIST']
785 if calibVersion < 1.8:
786 inDict['ptcTurnoffSamplingError'][ampName] = np.nan
787 else:
788 inDict['ptcTurnoffSamplingError'][ampName] = record['PTC_TURNOFF_SAMPLING_ERROR']
789 if calibVersion < 1.9 and inDict['ptcFitType'] == "FULLCOVARIANCE":
790 # Before version 1.9, the noise stored in the PTC was in
791 # units of electron^2 only if ptcFitType == FULLCOVARIANCE.
792 # After version 1.9, we standardized the
793 # PhotonTransferCurveDataset noise units to electron to fix
794 # this bug. If a user tries to use an earlier version of
795 # PTC with this fit type, we must be sure to do the
796 # calculations properly. More information about this noise
797 # issue can be found in DM-45976.
798 if ampName == inDict['ampNames'][0]:
799 cls().log.info(f"Input PTC VERSION ({calibVersion}) < 1.9 and"
800 " ptcFitType == FULLCOVARIANCE. Applying fix for"
801 f" the DM-45976 noise issue.")
802 # The noiseErr calculation was accidentally correct in the
803 # previous version, so we only need to upday the noise
804 # attribute.
805 inDict['noise'][ampName] = np.sqrt(record['noise'][ampName])
806 if calibVersion < 2.0:
807 inDict['ampOffsets'][ampName] = np.full_like(inDict['rawMeans'][ampName], np.nan)
808 inDict['gainUnadjusted'][ampName] = record['GAIN']
809 inDict['gainList'][ampName] = np.full_like(inDict['rawMeans'][ampName], np.nan)
810 else:
811 inDict['ampOffsets'][ampName] = record['AMP_OFFSETS']
812 inDict['gainUnadjusted'][ampName] = record['GAIN_UNADJUSTED']
813 inDict['gainList'][ampName] = record['GAIN_LIST']
814
815 inDict['auxValues'] = {}
816 record = ptcTable[0]
817 for col in record.columns.keys():
818 if col.startswith('PTCAUX_'):
819 parts = col.split('PTCAUX_')
820 if isinstance(record[col][0], np.bytes_):
821 # Convert to a unicode string because astropy fits doesn't
822 # round-trip properly
823 inDict['auxValues'][parts[1]] = record[col].astype(np.str_)
824 else:
825 inDict['auxValues'][parts[1]] = record[col]
826
827 return cls().fromDict(inDict)
828
829 def toTable(self):
830 """Construct a list of tables containing the information in this
831 calibration.
832
833 The list of tables should create an identical calibration
834 after being passed to this class's fromTable method.
835
836 Returns
837 -------
838 tableList : `list` [`astropy.table.Table`]
839 List of tables containing the linearity calibration
840 information.
841 """
842 tableList = []
844
845 badAmps = np.array(self.badAmps) if len(self.badAmps) else np.array([], dtype="U3")
846
847 catalogList = []
848 for ampName in self.ampNames:
849 ampDict = {
850 'AMPLIFIER_NAME': ampName,
851 'PTC_FIT_TYPE': self.ptcFitType,
852 'COV_MATRIX_SIDE': self.covMatrixSide,
853 'COV_MATRIX_SIDE_FULL_COV_FIT': self.covMatrixSideFullCovFit,
854 'INPUT_EXP_ID_PAIRS': self.inputExpIdPairs[ampName],
855 'EXP_ID_MASK': self.expIdMask[ampName],
856 'RAW_EXP_TIMES': self.rawExpTimes[ampName],
857 'RAW_MEANS': self.rawMeans[ampName],
858 'RAW_VARS': self.rawVars[ampName],
859 'ROW_MEAN_VARIANCE': self.rowMeanVariance[ampName],
860 'GAIN': self.gain[ampName],
861 'GAIN_ERR': self.gainErr[ampName],
862 'GAIN_UNADJUSTED': self.gainUnadjusted[ampName],
863 'GAIN_LIST': self.gainList[ampName],
864 'NOISE_LIST': self.noiseList[ampName],
865 'NOISE': self.noise[ampName],
866 'NOISE_ERR': self.noiseErr[ampName],
867 'HIST_VARS': self.histVars[ampName],
868 'HIST_CHI2_DOFS': self.histChi2Dofs[ampName],
869 'KS_PVALUES': self.kspValues[ampName],
870 'PTC_FIT_PARS': np.array(self.ptcFitPars[ampName]),
871 'PTC_FIT_PARS_ERROR': np.array(self.ptcFitParsError[ampName]),
872 'PTC_FIT_CHI_SQ': self.ptcFitChiSq[ampName],
873 'PTC_TURNOFF': self.ptcTurnoff[ampName],
874 'PTC_TURNOFF_SAMPLING_ERROR': self.ptcTurnoffSamplingError[ampName],
875 'A_MATRIX': self.aMatrix[ampName].ravel(),
876 'B_MATRIX': self.bMatrix[ampName].ravel(),
877 'A_MATRIX_NO_B': self.aMatrixNoB[ampName].ravel(),
878 'NOISE_MATRIX': self.noiseMatrix[ampName].ravel(),
879 'NOISE_MATRIX_NO_B': self.noiseMatrixNoB[ampName].ravel(),
880 'BAD_AMPS': badAmps,
881 'PHOTO_CHARGE': self.photoCharges[ampName],
882 'AMP_OFFSETS': self.ampOffsets[ampName],
883 'COVARIANCES': self.covariances[ampName].ravel(),
884 'COVARIANCES_MODEL': self.covariancesModel[ampName].ravel(),
885 'COVARIANCES_SQRT_WEIGHTS': self.covariancesSqrtWeights[ampName].ravel(),
886 'COVARIANCES_MODEL_NO_B': self.covariancesModelNoB[ampName].ravel(),
887 'FINAL_VARS': self.finalVars[ampName],
888 'FINAL_MODEL_VARS': self.finalModelVars[ampName],
889 'FINAL_MEANS': self.finalMeans[ampName],
890 }
891
892 if self.auxValues:
893 for key, value in self.auxValues.items():
894 ampDict[f"PTCAUX_{key}"] = value
895
896 catalogList.append(ampDict)
897
898 catalog = Table(catalogList)
899
900 inMeta = self.getMetadata().toDict()
901 outMeta = {k: v for k, v in inMeta.items() if v is not None}
902 outMeta.update({k: "" for k, v in inMeta.items() if v is None})
903 catalog.meta = outMeta
904 tableList.append(catalog)
905
906 return tableList
907
908 def fromDetector(self, detector):
909 """Read metadata parameters from a detector.
910
911 Parameters
912 ----------
913 detector : `lsst.afw.cameraGeom.detector`
914 Input detector with parameters to use.
915
916 Returns
917 -------
918 calib : `lsst.ip.isr.PhotonTransferCurveDataset`
919 The calibration constructed from the detector.
920 """
921
922 pass
923
924 def appendPartialPtc(self, partialPtc):
925 """Append a partial PTC dataset to this dataset.
926
927 Parameters
928 ----------
929 partialPtc : `lsst.ip.isr.PhotonTransferCurveDataset`
930 Partial PTC to append. Should only have one element.
931 """
932 if self.ampNames != partialPtc.ampNames:
933 raise ValueError("partialPtc has mis-matched amps.")
934 if len(partialPtc.rawMeans[self.ampNames[0]]) != 1 or partialPtc.ptcFitType != "PARTIAL":
935 raise ValueError("partialPtc does not appear to be the correct format.")
936
937 # Record the initial length of the PTC, for checking auxValues.
938 initialLength = len(self.expIdMask[self.ampNames[0]])
939
940 for key, value in partialPtc.auxValues.items():
941 if key in self.auxValues:
942 self.auxValues[key] = np.append(self.auxValues[key], value)
943 elif initialLength == 0:
944 # This is the first partial, so we can set the dict key.
945 self.auxValues[key] = value
946 else:
947 raise ValueError(f"partialPtc has mismatched auxValue key {key}.")
948
949 for ampName in self.ampNames:
950 # The partial dataset consists of lists of values for each
951 # quantity. In the case of the input exposure pairs, this is a
952 # list of tuples. In all cases we only want the first
953 # (and only) element of the list.
954 self.inputExpIdPairs[ampName].append(partialPtc.inputExpIdPairs[ampName][0])
955 self.expIdMask[ampName] = np.append(self.expIdMask[ampName],
956 partialPtc.expIdMask[ampName][0])
957 self.rawExpTimes[ampName] = np.append(self.rawExpTimes[ampName],
958 partialPtc.rawExpTimes[ampName][0])
959 self.rawMeans[ampName] = np.append(self.rawMeans[ampName],
960 partialPtc.rawMeans[ampName][0])
961 self.rawVars[ampName] = np.append(self.rawVars[ampName],
962 partialPtc.rawVars[ampName][0])
963 self.rowMeanVariance[ampName] = np.append(self.rowMeanVariance[ampName],
964 partialPtc.rowMeanVariance[ampName][0])
965 self.photoCharges[ampName] = np.append(self.photoCharges[ampName],
966 partialPtc.photoCharges[ampName][0])
967 self.ampOffsets[ampName] = np.append(self.ampOffsets[ampName],
968 partialPtc.ampOffsets[ampName][0])
969 self.histVars[ampName] = np.append(self.histVars[ampName],
970 partialPtc.histVars[ampName][0])
971 self.histChi2Dofs[ampName] = np.append(self.histChi2Dofs[ampName],
972 partialPtc.histChi2Dofs[ampName][0])
973 self.kspValues[ampName] = np.append(self.kspValues[ampName],
974 partialPtc.kspValues[ampName][0])
975 self.gainList[ampName] = np.append(self.gainList[ampName],
976 partialPtc.gain[ampName])
977 self.noiseList[ampName] = np.append(self.noiseList[ampName],
978 partialPtc.noise[ampName])
979 self.finalVars[ampName] = np.append(self.finalVars[ampName],
980 partialPtc.finalVars[ampName][0])
981 self.finalModelVars[ampName] = np.append(self.finalModelVars[ampName],
982 partialPtc.finalModelVars[ampName][0])
983 self.finalMeans[ampName] = np.append(self.finalMeans[ampName],
984 partialPtc.finalMeans[ampName][0])
985
986 self.covariances[ampName] = np.append(
987 self.covariances[ampName].ravel(),
988 partialPtc.covariances[ampName].ravel()
989 ).reshape(
990 (
991 len(self.rawExpTimes[ampName]),
992 self.covMatrixSide,
993 self.covMatrixSide,
994 )
995 )
996 self.covariancesSqrtWeights[ampName] = np.append(
997 self.covariancesSqrtWeights[ampName].ravel(),
998 partialPtc.covariancesSqrtWeights[ampName].ravel()
999 ).reshape(
1000 (
1001 len(self.rawExpTimes[ampName]),
1002 self.covMatrixSide,
1003 self.covMatrixSide,
1004 )
1005 )
1006 self.covariancesModel[ampName] = np.append(
1007 self.covariancesModel[ampName].ravel(),
1008 partialPtc.covariancesModel[ampName].ravel()
1009 ).reshape(
1010 (
1011 len(self.rawExpTimes[ampName]),
1012 self.covMatrixSide,
1013 self.covMatrixSide,
1014 )
1015 )
1016 self.covariancesModelNoB[ampName] = np.append(
1017 self.covariancesModelNoB[ampName].ravel(),
1018 partialPtc.covariancesModelNoB[ampName].ravel()
1019 ).reshape(
1020 (
1021 len(self.rawExpTimes[ampName]),
1022 self.covMatrixSide,
1023 self.covMatrixSide,
1024 )
1025 )
1026
1027 def sort(self, sortIndex):
1028 """Sort the components of the PTC by a given sort index.
1029
1030 The PTC is sorted in-place.
1031
1032 Parameters
1033 ----------
1034 sortIndex : `list` or `np.ndarray`
1035 The sorting index, which must be the same length as
1036 the number of elements of the PTC.
1037 """
1038 index = np.atleast_1d(sortIndex)
1039
1040 # First confirm everything matches.
1041 for ampName in self.ampNames:
1042 if len(index) != len(self.rawExpTimes[ampName]):
1043 raise ValueError(
1044 f"Length of sortIndex ({len(index)}) does not match number of PTC "
1045 f"elements ({len(self.rawExpTimes[ampName])})",
1046 )
1047
1048 # Note that gain, gainUnadjusted, gainErr, noise, noiseErr,
1049 # ptcTurnoff, ptcTurnoffSamplingError, and the full covariance fit
1050 # parameters are global and not sorted by input pair.
1051
1052 for ampName in self.ampNames:
1053 self.inputExpIdPairs[ampName] = np.array(
1054 self.inputExpIdPairs[ampName]
1055 )[index].tolist()
1056
1057 self.expIdMask[ampName] = self.expIdMask[ampName][index]
1058 self.rawExpTimes[ampName] = self.rawExpTimes[ampName][index]
1059 self.rawMeans[ampName] = self.rawMeans[ampName][index]
1060 self.rawVars[ampName] = self.rawVars[ampName][index]
1061 self.rowMeanVariance[ampName] = self.rowMeanVariance[ampName][index]
1062 self.photoCharges[ampName] = self.photoCharges[ampName][index]
1063 self.ampOffsets[ampName] = self.ampOffsets[ampName][index]
1064
1065 self.gainList[ampName] = self.gainList[ampName][index]
1066 self.noiseList[ampName] = self.noiseList[ampName][index]
1067
1068 self.histVars[ampName] = self.histVars[ampName][index]
1069 self.histChi2Dofs[ampName] = self.histChi2Dofs[ampName][index]
1070 self.kspValues[ampName] = self.kspValues[ampName][index]
1071
1072 self.covariances[ampName] = self.covariances[ampName][index]
1073 self.covariancesSqrtWeights[ampName] = self.covariancesSqrtWeights[ampName][index]
1074 self.covariancesModel[ampName] = self.covariancesModel[ampName][index]
1075 self.covariancesModelNoB[ampName] = self.covariancesModelNoB[ampName][index]
1076
1077 self.finalVars[ampName] = self.finalVars[ampName][index]
1078 self.finalModelVars[ampName] = self.finalModelVars[ampName][index]
1079 self.finalMeans[ampName] = self.finalMeans[ampName][index]
1080
1081 # Sort the auxiliary values which are not stored per-amp.
1082 for key, value in self.auxValues.items():
1083 self.auxValues[key] = value[index]
1084
1085 def getExpIdsUsed(self, ampName):
1086 """Get the exposures used, i.e. not discarded, for a given amp.
1087 If no mask has been created yet, all exposures are returned.
1088
1089 Parameters
1090 ----------
1091 ampName : `str`
1092
1093 Returns
1094 -------
1095 expIdsUsed : `list` [`tuple`]
1096 List of pairs of exposure ids used in PTC.
1097 """
1098 if len(self.expIdMask[ampName]) == 0:
1099 return self.inputExpIdPairs[ampName]
1100
1101 # if the mask exists it had better be the same length as the expIdPairs
1102 assert len(self.expIdMask[ampName]) == len(self.inputExpIdPairs[ampName])
1103
1104 pairs = self.inputExpIdPairs[ampName]
1105 mask = self.expIdMask[ampName]
1106 # cast to bool required because numpy
1107 try:
1108 expIdsUsed = [(exp1, exp2) for ((exp1, exp2), m) in zip(pairs, mask) if m]
1109 except ValueError:
1110 self.log.warning("The PTC file was written incorrectly; you should rerun the "
1111 "PTC solve task if possible.")
1112 expIdsUsed = []
1113 for pairList, m in zip(pairs, mask):
1114 if m:
1115 expIdsUsed.append(pairList[0])
1116
1117 return expIdsUsed
1118
1119 def getGoodAmps(self):
1120 """Get the good amps from this PTC."""
1121 return [amp for amp in self.ampNames if amp not in self.badAmps]
1122
1123 def getGoodPoints(self, ampName):
1124 """Get the good points used for a given amp in the PTC.
1125
1126 Parameters
1127 ----------
1128 ampName : `str`
1129 Amplifier's name.
1130
1131 Returns
1132 -------
1133 goodPoints : `np.ndarray`
1134 Boolean array of good points used in PTC.
1135 """
1136 return self.expIdMask[ampName]
1137
1138 def validateGainNoiseTurnoffValues(self, ampName, doWarn=False):
1139 """Ensure the gain, read noise, and PTC turnoff have
1140 sensible values.
1141
1142 Parameters
1143 ----------
1144 ampName : `str`
1145 Amplifier's name.
1146 """
1147
1148 gain = self.gain[ampName]
1149 noise = self.noise[ampName]
1150 ptcTurnoff = self.ptcTurnoff[ampName]
1151
1152 # Check if gain is not positive or is np.nan
1153 if not (isinstance(gain, (int, float)) and gain > 0) or math.isnan(gain):
1154 if doWarn:
1155 self.log.warning(f"Invalid gain value {gain}"
1156 " Setting to default: Gain=1")
1157 gain = 1
1158
1159 # Check if noise is not positive or is np.nan
1160 if not (isinstance(noise, (int, float)) and noise > 0) or math.isnan(noise):
1161 if doWarn:
1162 self.log.warning(f"Invalid noise value: {noise}"
1163 " Setting to default: Noise=1")
1164 noise = 1
1165
1166 # Check if ptcTurnoff is not positive or is np.nan
1167 if not (isinstance(ptcTurnoff, (int, float)) and ptcTurnoff > 0) or math.isnan(ptcTurnoff):
1168 if doWarn:
1169 self.log.warning(f"Invalid PTC turnoff value: {ptcTurnoff}"
1170 " Setting to default: PTC Turnoff=2e19")
1171 ptcTurnoff = 2e19
1172
1173 self.gain[ampName] = gain
1174 self.noise[ampName] = noise
1175 self.ptcTurnoff[ampName] = ptcTurnoff
1176
1177 def evalPtcModel(self, mu, setBtoZero=False):
1178 """Computes the covariance model at specific signal levels.
1179
1180 Parameters
1181 ----------
1182 mu : `numpy.array`, (N,)
1183 List of mean signals in ADU.
1184 setBtoZero: `bool`, optional
1185 Set "b" parameter in full model (see Astier+19) to zero.
1186 This parameter is ignored if ptcFitType != FULLCOVARIANCE.
1187
1188 Raises
1189 ------
1190 RuntimeError
1191 Raised if ptcFitType is invalid.
1192
1193 Returns
1194 -------
1195 covModel : `numpy.array`, (N, M, M)
1196 Covariances model at mu (in ADU^2).
1197
1198 Notes
1199 -----
1200 Computes the covModel for all mu, and it returns
1201 cov[N, M, M], where the variance model is cov[:,0,0].
1202 Both mu and cov are in ADUs and ADUs squared. This
1203 routine evaulates the n-degree polynomial model (defined
1204 by polynomialFitDegree) if self.ptcFitType == POLYNOMIAL,
1205 the approximation in Eq. 16 of Astier+19 (1905.08677)
1206 if self.ptcFitType == EXPAPPROXIMATION, and Eq. 20 of
1207 Astier+19 if self.ptcFitType == FULLCOVARIANCE.
1208
1209 The POLYNOMIAL model and the EXPAPPROXIMATION model
1210 (Eq. 16 of Astier+19) are only approximations for the
1211 variance (cov[0,0]), so the function returns covModel
1212 of shape (N,), representing an array of [C_{00}]
1213 if self.ptcFitType == EXPAPPROXIMATION or
1214 self.ptcFitType == POLYNOMAIL.
1215
1216 Note that the PhotoTransferCurveDataset does not store
1217 the gain fit parameter for FULLCOVARIANCE with b=0, so
1218 we can't recompute the covariance model with
1219 setBtoZero=True exactly.
1220 """
1221
1222 ampNames = self.ampNames
1223 covModel = {ampName: np.array([]) for ampName in ampNames}
1224
1225 if self.ptcFitType == "POLYNOMIAL":
1226 pars = self.ptcFitPars
1227
1228 for ampName in ampNames:
1229 c00 = poly.polyval(mu, [*pars[ampName]])
1230 covModel[ampName] = c00
1231
1232 elif self.ptcFitType == "EXPAPPROXIMATION":
1233 pars = self.ptcFitPars
1234
1235 for ampName in ampNames:
1236 a00, gain, noise = pars[ampName]
1237 f1 = 0.5/(a00*gain*gain)*(np.exp(2*a00*mu*gain)-1)
1238 f2 = noise/(gain*gain)
1239 c00 = f1 + f2
1240 covModel[ampName] = c00
1241
1242 elif self.ptcFitType == "FULLCOVARIANCE":
1243 for ampName in ampNames:
1244 noiseMatrix = self.noiseMatrix[ampName]
1245 gain = self.gain[ampName]
1246 aMatrix = self.aMatrix[ampName]
1247 bMatrix = self.bMatrix[ampName]
1248 cMatrix = aMatrix*bMatrix
1249
1250 if setBtoZero:
1251 # Note that the PhotoTransferCurveDataset does not store
1252 # the gain fit parameter for FULLCOVARIANCE with b=0, so
1253 # we can't recompute the covariance model with
1254 # setBtoZero=True.
1255 raise NotImplementedError("Cannot evaulate the PTC model"
1256 "with b=0 for FULLCOVARIANCE.")
1257
1258 matrixSideFit = self.covMatrixSideFullCovFit
1259 sa = (matrixSideFit, matrixSideFit)
1260
1261 # pad a with zeros and symmetrize
1262 aEnlarged = np.zeros((int(sa[0]*1.5)+1, int(sa[1]*1.5)+1))
1263 aEnlarged[0:sa[0], 0:sa[1]] = aMatrix
1264 aSym = symmetrize(aEnlarged)
1265
1266 # pad c with zeros and symmetrize
1267 cEnlarged = np.zeros((int(sa[0]*1.5)+1, int(sa[1]*1.5)+1))
1268 cEnlarged[0:sa[0], 0:sa[1]] = cMatrix
1269
1270 cSym = symmetrize(cEnlarged)
1271 a2 = fftconvolve(aSym, aSym, mode='same')
1272 a3 = fftconvolve(a2, aSym, mode='same')
1273 ac = fftconvolve(aSym, cSym, mode='same')
1274 (xc, yc) = np.unravel_index(np.abs(aSym).argmax(), a2.shape)
1275
1276 a1 = aMatrix[np.newaxis, :, :]
1277 a2 = a2[np.newaxis, xc:xc + matrixSideFit, yc:yc + matrixSideFit]
1278 a3 = a3[np.newaxis, xc:xc + matrixSideFit, yc:yc + matrixSideFit]
1279 ac = ac[np.newaxis, xc:xc + matrixSideFit, yc:yc + matrixSideFit]
1280 c1 = cMatrix[np.newaxis, ::]
1281
1282 # assumes that mu is 1d
1283 bigMu = mu[:, np.newaxis, np.newaxis]*gain
1284 # c(=a*b in Astier+19) also has a contribution to the last
1285 # term, that is absent for now.
1286 if setBtoZero:
1287 c1 = np.zeros_like(c1)
1288 ac = np.zeros_like(ac)
1289
1290 covModel[ampName] = (bigMu/(gain*gain)*(a1*bigMu+2./3.*(bigMu*bigMu)*(a2 + c1)
1291 + (1./3.*a3 + 5./6.*ac)*(bigMu*bigMu*bigMu))
1292 + noiseMatrix[np.newaxis, :, :]/gain**2)
1293
1294 # add the Poisson term, and the read out noise (variance)
1295 covModel[ampName][:, 0, 0] += mu/gain
1296 else:
1297 raise RuntimeError("Cannot compute PTC model for "
1298 "ptcFitType %s." % self.ptcFitType)
1299
1300 return covModel
1301
1303 """Ensure covMatrixSideFullCovFit <= covMatrixSide."""
1305 self.log.warning("covMatrixSideFullCovFit > covMatrixSide "
1306 f"({self.covMatrixSideFullCovFit} > {self.covMatrixSide})."
1307 "Setting the former to the latter.")
std::vector< SchemaItem< Flag > > * items
updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition calibType.py:207
validateGainNoiseTurnoffValues(self, ampName, doWarn=False)
__init__(self, ampNames=[], ptcFitType=None, covMatrixSide=1, covMatrixSideFullCovFit=None, **kwargs)
setAmpValuesPartialDataset(self, ampName, inputExpIdPair=(-1, -1), rawExpTime=np.nan, rawMean=np.nan, rawVar=np.nan, rowMeanVariance=np.nan, photoCharge=np.nan, ampOffset=np.nan, expIdMask=False, covariance=None, covSqrtWeights=None, gain=np.nan, noise=np.nan, histVar=np.nan, histChi2Dof=np.nan, kspValue=0.0)