Coverage for python/lsst/cp/verify/verifyPtc.py: 13%
139 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-11 05:14 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-11 05:14 -0700
1# This file is part of cp_verify.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21import numpy as np
22import lsst.pex.config as pexConfig
23from scipy.optimize import least_squares
25from .verifyCalib import CpVerifyCalibConfig, CpVerifyCalibTask, CpVerifyCalibConnections
27__all__ = ['CpVerifyPtcConfig', 'CpVerifyPtcTask']
30class CpVerifyPtcConfig(CpVerifyCalibConfig,
31 pipelineConnections=CpVerifyCalibConnections):
32 """Inherits from base CpVerifyCalibConfig."""
34 gainThreshold = pexConfig.Field(
35 dtype=float,
36 doc="Maximum percentage difference between PTC gain and nominal amplifier gain.",
37 default=5.0,
38 )
40 noiseThreshold = pexConfig.Field(
41 dtype=float,
42 doc="Maximum percentage difference between PTC readout noise and nominal "
43 "amplifier readout noise.",
44 default=5.0,
45 )
47 turnoffThreshold = pexConfig.Field(
48 dtype=float,
49 doc="Minimun full well requirement (in electrons). To be compared with the "
50 "reported PTC turnoff per amplifier.",
51 default=90000,
52 )
54 a00MinITL = pexConfig.Field(
55 dtype=float,
56 doc="Minimum a00 (c.f., Astier+19) for ITL CCDs.",
57 default=-4.56e-6,
58 )
60 a00MaxITL = pexConfig.Field(
61 dtype=float,
62 doc="Maximum a00 (c.f., Astier+19) for ITL CCDs.",
63 default=6.91e-7,
64 )
66 a00MinE2V = pexConfig.Field(
67 dtype=float,
68 doc="Minimum a00 (c.f., Astier+19) for E2V CCDs.",
69 default=-3.52e-6,
70 )
72 a00MaxE2V = pexConfig.Field(
73 dtype=float,
74 doc="Maximum a00 (c.f., Astier+19) for E2V CCDs.",
75 default=-2.61e-6,
76 )
78 def setDefaults(self):
79 super().setDefaults()
80 self.stageName = 'PTC'
83def linearModel(x, m, b):
84 """A linear model.
85 """
86 return m*x + b
89def modelResidual(p, x, y):
90 """Model residual for fit below.
91 """
92 return y - linearModel(x, *p)
95class CpVerifyPtcTask(CpVerifyCalibTask):
96 """PTC verification sub-class, implementing the verify method.
97 """
98 ConfigClass = CpVerifyPtcConfig
99 _DefaultName = 'cpVerifyPtc'
101 def detectorStatistics(self, inputCalib, camera=None):
102 """Calculate detector level statistics from the calibration.
104 Parameters
105 ----------
106 inputCalib : `lsst.ip.isr.IsrCalib`
107 The calibration to verify.
108 camera : `lsst.afw.cameraGeom.Camera`, optional
109 Input camera to get detectors from.
111 Returns
112 -------
113 outputStatistics : `dict` [`str`, scalar]
114 A dictionary of the statistics measured and their values.
115 """
116 return {}
118 def amplifierStatistics(self, inputCalib, camera=None):
119 """Calculate detector level statistics from the calibration.
121 Parameters
122 ----------
123 inputCalib : `lsst.ip.isr.IsrCalib`
124 The calibration to verify.
125 camera : `lsst.afw.cameraGeom.Camera`, optional
126 Input camera to get detectors from.
128 Returns
129 -------
130 outputStatistics : `dict` [`str`, scalar]
131 A dictionary of the statistics measured and their values.
132 """
133 calibMetadata = inputCalib.getMetadata().toDict()
134 detId = calibMetadata['DETECTOR']
135 detector = camera[detId]
136 ptcFitType = calibMetadata['PTC_FIT_TYPE']
137 outputStatistics = {amp.getName(): {} for amp in detector}
138 for amp in detector:
139 ampName = amp.getName()
140 calibGain = inputCalib.gain[ampName]
141 outputStatistics[ampName]['PTC_GAIN'] = calibGain
142 outputStatistics[ampName]['AMP_GAIN'] = amp.getGain()
143 outputStatistics[ampName]['PTC_NOISE'] = inputCalib.noise[ampName]
144 outputStatistics[ampName]['AMP_NOISE'] = amp.getReadNoise()
145 outputStatistics[ampName]['PTC_TURNOFF'] = inputCalib.ptcTurnoff[ampName]
146 outputStatistics[ampName]['PTC_FIT_TYPE'] = ptcFitType
147 outputStatistics[ampName]['PTC_ROW_MEAN_VARIANCE'] = inputCalib.rowMeanVariance[ampName].tolist()
148 outputStatistics[ampName]['PTC_MAX_RAW_MEANS'] = float(np.nanmax(inputCalib.rawMeans[ampName]))
149 # To plot Covs[ij] vs flux
150 rawFlux = inputCalib.rawMeans[ampName].tolist()
151 outputStatistics[ampName]['PTC_RAW_MEANS'] = rawFlux
152 mask = inputCalib.expIdMask[ampName].tolist()
153 outputStatistics[ampName]['PTC_EXP_ID_MASK'] = mask
154 covs = inputCalib.covariances[ampName]
155 outputStatistics[ampName]['PTC_COV_10'] = covs[:, 1, 0].tolist()
156 outputStatistics[ampName]['PTC_COV_01'] = covs[:, 0, 1].tolist()
157 outputStatistics[ampName]['PTC_COV_11'] = covs[:, 1, 1].tolist()
158 outputStatistics[ampName]['PTC_COV_20'] = covs[:, 2, 0].tolist()
159 outputStatistics[ampName]['PTC_COV_02'] = covs[:, 0, 2].tolist()
160 # Calculate and save the slopes and offsets from Covs[ij] vs flux
161 keys = ['PTC_COV_10', 'PTC_COV_01', 'PTC_COV_11', 'PTC_COV_20',
162 'PTC_COV_02']
163 maskedFlux = np.array(rawFlux)[mask]
164 for key in keys:
165 maskedCov = np.array(outputStatistics[ampName][key])[mask]
166 linearFit = least_squares(modelResidual, [1., 0.0],
167 args=(np.array(maskedFlux), np.array(maskedCov)),
168 loss='cauchy')
169 slopeKey = key + '_FIT_SLOPE'
170 offsetKey = key + '_FIT_OFFSET'
171 successKey = key + '_FIT_SUCCESS'
172 outputStatistics[ampName][slopeKey] = float(linearFit.x[0])
173 outputStatistics[ampName][offsetKey] = float(linearFit.x[1])
174 outputStatistics[ampName][successKey] = linearFit.success
176 if ptcFitType == 'EXPAPPROXIMATION':
177 outputStatistics[ampName]['PTC_BFE_A00'] = float(inputCalib.ptcFitPars[ampName][0])
178 if ptcFitType == 'FULLCOVARIANCE':
179 outputStatistics[ampName]['PTC_BFE_A00'] = float(inputCalib.aMatrix[ampName][0][0])
181 # Test from eo_pipe: github.com/lsst-camera-dh/eo-pipe;
182 # ptcPlotTask.py
183 # Slope of [variance of means of rows](electrons^2)
184 # vs [2*signal(electrons)/numCols]
185 numCols = amp.getBBox().width
186 mask = inputCalib.expIdMask[ampName]
187 rowMeanVar = inputCalib.rowMeanVariance[ampName][mask]*calibGain**2
188 signal = inputCalib.rawMeans[ampName][mask]*calibGain
189 try:
190 slope = sum(rowMeanVar) / sum(2.*signal/numCols)
191 except ZeroDivisionError:
192 slope = np.nan
193 outputStatistics[ampName]['PTC_ROW_MEAN_VARIANCE_SLOPE'] = float(slope)
195 return outputStatistics
197 def verify(self, calib, statisticsDict, camera=None):
198 """Verify that the calibration meets the verification criteria.
200 Parameters
201 ----------
202 inputCalib : `lsst.ip.isr.IsrCalib`
203 The calibration to verify.
204 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
205 Dictionary of measured statistics. The inner dictionary
206 should have keys that are statistic names (`str`) with
207 values that are some sort of scalar (`int` or `float` are
208 the mostly likely types).
209 camera : `lsst.afw.cameraGeom.Camera`, optional
210 Input camera to get detectors from.
212 Returns
213 -------
214 outputStatistics : `dict` [`str`, `dict` [`str`, `bool`]]
215 A dictionary indexed by the amplifier name, containing
216 dictionaries of the verification criteria.
217 success : `bool`
218 A boolean indicating whether all tests have passed.
219 """
220 verifyStats = {}
221 success = True
222 calibMetadata = calib.getMetadata().toDict()
223 detId = calibMetadata['DETECTOR']
224 detector = camera[detId]
225 ptcFitType = calibMetadata['PTC_FIT_TYPE']
226 # 'DET_SER' is of the form 'ITL-3800C-229'
227 detVendor = calibMetadata['DET_SER'].split('-')[0]
229 for amp in detector:
230 verify = {}
231 ampName = amp.getName()
232 calibGain = calib.gain[ampName]
234 diffGain = (np.abs(calibGain - amp.getGain()) / amp.getGain())*100
235 diffNoise = (np.abs(calib.noise[ampName] - amp.getReadNoise()) / amp.getReadNoise())*100
237 # DMTN-101: 16.1 and 16.2
238 # The fractional relative difference between the fitted PTC and the
239 # nominal amplifier gain and readout noise values should be less
240 # than a certain threshold (default: 5%).
241 verify['PTC_GAIN'] = bool(diffGain < self.config.gainThreshold)
242 verify['PTC_NOISE'] = bool(diffNoise < self.config.noiseThreshold)
244 # Check that the noises measured in cpPtcExtract do not evolve
245 # as a function of flux.
246 # We check that the reduced chi squared statistic between the
247 # noises and the mean of the noises less than 1.25 sigmas
248 mask = calib.expIdMask[ampName]
249 noiseList = calib.noiseList[ampName][mask]
250 expectedNoiseList = np.zeros_like(noiseList) + np.mean(noiseList)
251 chiSquared = np.sum((noiseList - expectedNoiseList)**2 / np.std(noiseList))
252 reducedChiSquared = chiSquared / len(noiseList)
253 verify['NOISE_SIGNAL_INDEPENDENCE'] = bool(reducedChiSquared < 1.25)
255 # DMTN-101: 16.3
256 # Check that the measured PTC turnoff is at least greater than the
257 # full-well requirement of 90k e-.
258 turnoffCut = self.config.turnoffThreshold
259 verify['PTC_TURNOFF'] = bool(calib.ptcTurnoff[ampName]*calibGain > turnoffCut)
260 # DMTN-101: 16.4
261 # Check the a00 value (brighter-fatter effect).
262 # This is a purely electrostatic parameter that should not change
263 # unless voltages are changed (e.g., parallel, bias voltages).
264 # Check that the fitted a00 parameter per CCD vendor is within a
265 # range motivated by measurements on data (DM-30171).
266 if ptcFitType in ['EXPAPPROXIMATION', 'FULLCOVARIANCE']:
267 # a00 is a fit parameter from these models.
268 if ptcFitType == 'EXPAPPROXIMATION':
269 a00 = calib.ptcFitPars[ampName][0]
270 else:
271 a00 = calib.aMatrix[ampName][0][0]
272 if detVendor == 'ITL':
273 a00Max = self.config.a00MaxITL
274 a00Min = self.config.a00MinITL
275 verify['PTC_BFE_A00'] = bool(a00 > a00Min and a00 < a00Max)
276 elif detVendor == 'E2V':
277 a00Max = self.config.a00MaxE2V
278 a00Min = self.config.a00MinE2V
279 verify['PTC_BFE_A00'] = bool(a00 > a00Min and a00 < a00Max)
280 else:
281 raise RuntimeError(f"Detector type {detVendor} not one of 'ITL' or 'E2V'")
283 # Overall success among all tests for this amp.
284 verify['SUCCESS'] = bool(np.all(list(verify.values())))
285 if verify['SUCCESS'] is False:
286 success = False
288 verifyStats[ampName] = verify
290 return {'AMP': verifyStats}, bool(success)
292 def repackStats(self, statisticsDict, dimensions):
293 # docstring inherited
294 rows = {}
295 rowList = []
296 matrixRowList = None
298 if self.config.useIsrStatistics:
299 mjd = statisticsDict["ISR"]["MJD"]
300 else:
301 mjd = np.nan
303 rowBase = {
304 "instrument": dimensions["instrument"],
305 "detector": dimensions["detector"],
306 "mjd": mjd,
307 }
309 # AMP results:
310 for ampName, stats in statisticsDict["AMP"].items():
311 rows[ampName] = {}
312 rows[ampName].update(rowBase)
313 rows[ampName]["amplifier"] = ampName
314 for key, value in stats.items():
315 rows[ampName][f"{self.config.stageName}_{key}"] = value
317 # VERIFY results
318 for ampName, stats in statisticsDict["VERIFY"]["AMP"].items():
319 for key, value in stats.items():
320 rows[ampName][f"{self.config.stageName}_VERIFY_{key}"] = value
322 # pack final list
323 for ampName, stats in rows.items():
324 rowList.append(stats)
326 return rowList, matrixRowList