Coverage for python/lsst/cp/verify/verifyPtc.py: 16%
118 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-19 02:48 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-19 02:48 -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 def setDefaults(self):
35 super().setDefaults()
37 gainThreshold = pexConfig.Field(
38 dtype=float,
39 doc="Maximum percentage difference between PTC gain and nominal amplifier gain.",
40 default=5.0,
41 )
43 noiseThreshold = pexConfig.Field(
44 dtype=float,
45 doc="Maximum percentage difference between PTC readout noise and nominal "
46 "amplifier readout noise.",
47 default=5.0,
48 )
50 turnoffThreshold = pexConfig.Field(
51 dtype=float,
52 doc="Minimun full well requirement (in electrons). To be compared with the "
53 "reported PTC turnoff per amplifier.",
54 default=90000,
55 )
57 a00MinITL = pexConfig.Field(
58 dtype=float,
59 doc="Minimum a00 (c.f., Astier+19) for ITL CCDs.",
60 default=-4.56e-6,
61 )
63 a00MaxITL = pexConfig.Field(
64 dtype=float,
65 doc="Maximum a00 (c.f., Astier+19) for ITL CCDs.",
66 default=6.91e-7,
67 )
69 a00MinE2V = pexConfig.Field(
70 dtype=float,
71 doc="Minimum a00 (c.f., Astier+19) for E2V CCDs.",
72 default=-3.52e-6,
73 )
75 a00MaxE2V = pexConfig.Field(
76 dtype=float,
77 doc="Maximum a00 (c.f., Astier+19) for E2V CCDs.",
78 default=-2.61e-6,
79 )
82def linearModel(x, m, b):
83 """A linear model.
84 """
85 return m*x + b
88def modelResidual(p, x, y):
89 """Model residual for fit below.
90 """
91 return y - linearModel(x, *p)
94class CpVerifyPtcTask(CpVerifyCalibTask):
95 """PTC verification sub-class, implementing the verify method.
96 """
97 ConfigClass = CpVerifyPtcConfig
98 _DefaultName = 'cpVerifyPtc'
100 def detectorStatistics(self, inputCalib, camera=None):
101 """Calculate detector level statistics from the calibration.
103 Parameters
104 ----------
105 inputCalib : `lsst.ip.isr.IsrCalib`
106 The calibration to verify.
107 camera : `lsst.afw.cameraGeom.Camera`, optional
108 Input camera to get detectors from.
110 Returns
111 -------
112 outputStatistics : `dict` [`str`, scalar]
113 A dictionary of the statistics measured and their values.
114 """
115 return {}
117 def amplifierStatistics(self, inputCalib, camera=None):
118 """Calculate detector level statistics from the calibration.
120 Parameters
121 ----------
122 inputCalib : `lsst.ip.isr.IsrCalib`
123 The calibration to verify.
124 camera : `lsst.afw.cameraGeom.Camera`, optional
125 Input camera to get detectors from.
127 Returns
128 -------
129 outputStatistics : `dict` [`str`, scalar]
130 A dictionary of the statistics measured and their values.
131 """
132 calibMetadata = inputCalib.getMetadata().toDict()
133 detId = calibMetadata['DETECTOR']
134 detector = camera[detId]
135 ptcFitType = calibMetadata['PTC_FIT_TYPE']
136 outputStatistics = {amp.getName(): {} for amp in detector}
137 for amp in detector:
138 ampName = amp.getName()
139 calibGain = inputCalib.gain[ampName]
140 outputStatistics[ampName]['PTC_GAIN'] = calibGain
141 outputStatistics[ampName]['AMP_GAIN'] = amp.getGain()
142 outputStatistics[ampName]['PTC_NOISE'] = inputCalib.noise[ampName]
143 outputStatistics[ampName]['AMP_NOISE'] = amp.getReadNoise()
144 outputStatistics[ampName]['PTC_TURNOFF'] = inputCalib.ptcTurnoff[ampName]
145 outputStatistics[ampName]['PTC_FIT_TYPE'] = ptcFitType
146 outputStatistics[ampName]['PTC_ROW_MEAN_VARIANCE'] = inputCalib.rowMeanVariance[ampName].tolist()
147 outputStatistics[ampName]['PTC_MAX_RAW_MEANS'] = float(np.nanmax(inputCalib.rawMeans[ampName]))
148 # To plot Covs[ij] vs flux
149 rawFlux = inputCalib.rawMeans[ampName].tolist()
150 outputStatistics[ampName]['PTC_RAW_MEANS'] = rawFlux
151 mask = inputCalib.expIdMask[ampName].tolist()
152 outputStatistics[ampName]['PTC_EXP_ID_MASK'] = mask
153 covs = inputCalib.covariances[ampName]
154 outputStatistics[ampName]['PTC_COV_10'] = covs[:, 1, 0].tolist()
155 outputStatistics[ampName]['PTC_COV_01'] = covs[:, 0, 1].tolist()
156 outputStatistics[ampName]['PTC_COV_11'] = covs[:, 1, 1].tolist()
157 outputStatistics[ampName]['PTC_COV_20'] = covs[:, 2, 0].tolist()
158 outputStatistics[ampName]['PTC_COV_02'] = covs[:, 0, 2].tolist()
159 # Calculate and save the slopes and offsets from Covs[ij] vs flux
160 keys = ['PTC_COV_10', 'PTC_COV_01', 'PTC_COV_11', 'PTC_COV_20',
161 'PTC_COV_02']
162 maskedFlux = np.array(rawFlux)[mask]
163 for key in keys:
164 maskedCov = np.array(outputStatistics[ampName][key])[mask]
165 linearFit = least_squares(modelResidual, [1., 0.0],
166 args=(np.array(maskedFlux), np.array(maskedCov)),
167 loss='cauchy')
168 slopeKey = key + '_FIT_SLOPE'
169 offsetKey = key + '_FIT_OFFSET'
170 successKey = key + '_FIT_SUCCESS'
171 outputStatistics[ampName][slopeKey] = float(linearFit.x[0])
172 outputStatistics[ampName][offsetKey] = float(linearFit.x[1])
173 outputStatistics[ampName][successKey] = linearFit.success
175 if ptcFitType == 'EXPAPPROXIMATION':
176 outputStatistics[ampName]['PTC_BFE_A00'] = float(inputCalib.ptcFitPars[ampName][0])
177 if ptcFitType == 'FULLCOVARIANCE':
178 outputStatistics[ampName]['PTC_BFE_A00'] = float(inputCalib.aMatrix[ampName][0][0])
180 # Test from eo_pipe: github.com/lsst-camera-dh/eo-pipe;
181 # ptcPlotTask.py
182 # Slope of [variance of means of rows](electrons^2)
183 # vs [2*signal(electrons)/numCols]
184 numCols = amp.getBBox().width
185 mask = inputCalib.expIdMask[ampName]
186 rowMeanVar = inputCalib.rowMeanVariance[ampName][mask]*calibGain**2
187 signal = inputCalib.rawMeans[ampName][mask]*calibGain
188 try:
189 slope = sum(rowMeanVar) / sum(2.*signal/numCols)
190 except ZeroDivisionError:
191 slope = np.nan
192 outputStatistics[ampName]['PTC_ROW_MEAN_VARIANCE_SLOPE'] = float(slope)
194 return outputStatistics
196 def verify(self, calib, statisticsDict, camera=None):
197 """Verify that the calibration meets the verification criteria.
199 Parameters
200 ----------
201 inputCalib : `lsst.ip.isr.IsrCalib`
202 The calibration to verify.
203 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
204 Dictionary of measured statistics. The inner dictionary
205 should have keys that are statistic names (`str`) with
206 values that are some sort of scalar (`int` or `float` are
207 the mostly likely types).
208 camera : `lsst.afw.cameraGeom.Camera`, optional
209 Input camera to get detectors from.
211 Returns
212 -------
213 outputStatistics : `dict` [`str`, `dict` [`str`, `bool`]]
214 A dictionary indexed by the amplifier name, containing
215 dictionaries of the verification criteria.
216 success : `bool`
217 A boolean indicating whether all tests have passed.
218 """
219 verifyStats = {}
220 success = True
221 calibMetadata = calib.getMetadata().toDict()
222 detId = calibMetadata['DETECTOR']
223 detector = camera[detId]
224 ptcFitType = calibMetadata['PTC_FIT_TYPE']
225 # 'DET_SER' is of the form 'ITL-3800C-229'
226 detVendor = calibMetadata['DET_SER'].split('-')[0]
228 for amp in detector:
229 verify = {}
230 ampName = amp.getName()
231 calibGain = calib.gain[ampName]
233 diffGain = (np.abs(calibGain - amp.getGain()) / amp.getGain())*100
234 diffNoise = (np.abs(calib.noise[ampName] - amp.getReadNoise()) / amp.getReadNoise())*100
236 # DMTN-101: 16.1 and 16.2
237 # The fractional relative difference between the fitted PTC and the
238 # nominal amplifier gain and readout noise values should be less
239 # than a certain threshold (default: 5%).
240 verify['PTC_GAIN'] = bool(diffGain < self.config.gainThreshold)
241 verify['PTC_NOISE'] = bool(diffNoise < self.config.noiseThreshold)
243 # Check that the noises measured in cpPtcExtract do not evolve
244 # as a function of flux.
245 # We check that the reduced chi squared statistic between the
246 # noises and the mean of the noises less than 1.25 sigmas
247 mask = calib.expIdMask[ampName]
248 noiseList = calib.noiseList[ampName][mask]
249 expectedNoiseList = np.zeros_like(noiseList) + np.mean(noiseList)
250 chiSquared = np.sum((noiseList - expectedNoiseList)**2 / np.std(noiseList))
251 reducedChiSquared = chiSquared / len(noiseList)
252 verify['NOISE_SIGNAL_INDEPENDENCE'] = bool(reducedChiSquared < 1.25)
254 # DMTN-101: 16.3
255 # Check that the measured PTC turnoff is at least greater than the
256 # full-well requirement of 90k e-.
257 turnoffCut = self.config.turnoffThreshold
258 verify['PTC_TURNOFF'] = bool(calib.ptcTurnoff[ampName]*calibGain > turnoffCut)
259 # DMTN-101: 16.4
260 # Check the a00 value (brighter-fatter effect).
261 # This is a purely electrostatic parameter that should not change
262 # unless voltages are changed (e.g., parallel, bias voltages).
263 # Check that the fitted a00 parameter per CCD vendor is within a
264 # range motivated by measurements on data (DM-30171).
265 if ptcFitType in ['EXPAPPROXIMATION', 'FULLCOVARIANCE']:
266 # a00 is a fit parameter from these models.
267 if ptcFitType == 'EXPAPPROXIMATION':
268 a00 = calib.ptcFitPars[ampName][0]
269 else:
270 a00 = calib.aMatrix[ampName][0][0]
271 if detVendor == 'ITL':
272 a00Max = self.config.a00MaxITL
273 a00Min = self.config.a00MinITL
274 verify['PTC_BFE_A00'] = bool(a00 > a00Min and a00 < a00Max)
275 elif detVendor == 'E2V':
276 a00Max = self.config.a00MaxE2V
277 a00Min = self.config.a00MinE2V
278 verify['PTC_BFE_A00'] = bool(a00 > a00Min and a00 < a00Max)
279 else:
280 raise RuntimeError(f"Detector type {detVendor} not one of 'ITL' or 'E2V'")
282 # Overall success among all tests for this amp.
283 verify['SUCCESS'] = bool(np.all(list(verify.values())))
284 if verify['SUCCESS'] is False:
285 success = False
287 verifyStats[ampName] = verify
289 return {'AMP': verifyStats}, bool(success)