Coverage for python/lsst/cp/verify/verifyPtc.py: 16%

118 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-03 02:51 -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 

24 

25from .verifyCalib import CpVerifyCalibConfig, CpVerifyCalibTask, CpVerifyCalibConnections 

26 

27__all__ = ['CpVerifyPtcConfig', 'CpVerifyPtcTask'] 

28 

29 

30class CpVerifyPtcConfig(CpVerifyCalibConfig, 

31 pipelineConnections=CpVerifyCalibConnections): 

32 """Inherits from base CpVerifyCalibConfig.""" 

33 

34 def setDefaults(self): 

35 super().setDefaults() 

36 

37 gainThreshold = pexConfig.Field( 

38 dtype=float, 

39 doc="Maximum percentage difference between PTC gain and nominal amplifier gain.", 

40 default=5.0, 

41 ) 

42 

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 ) 

49 

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 ) 

56 

57 a00MinITL = pexConfig.Field( 

58 dtype=float, 

59 doc="Minimum a00 (c.f., Astier+19) for ITL CCDs.", 

60 default=-4.56e-6, 

61 ) 

62 

63 a00MaxITL = pexConfig.Field( 

64 dtype=float, 

65 doc="Maximum a00 (c.f., Astier+19) for ITL CCDs.", 

66 default=6.91e-7, 

67 ) 

68 

69 a00MinE2V = pexConfig.Field( 

70 dtype=float, 

71 doc="Minimum a00 (c.f., Astier+19) for E2V CCDs.", 

72 default=-3.52e-6, 

73 ) 

74 

75 a00MaxE2V = pexConfig.Field( 

76 dtype=float, 

77 doc="Maximum a00 (c.f., Astier+19) for E2V CCDs.", 

78 default=-2.61e-6, 

79 ) 

80 

81 

82def linearModel(x, m, b): 

83 """A linear model. 

84 """ 

85 return m*x + b 

86 

87 

88def modelResidual(p, x, y): 

89 """Model residual for fit below. 

90 """ 

91 return y - linearModel(x, *p) 

92 

93 

94class CpVerifyPtcTask(CpVerifyCalibTask): 

95 """PTC verification sub-class, implementing the verify method. 

96 """ 

97 ConfigClass = CpVerifyPtcConfig 

98 _DefaultName = 'cpVerifyPtc' 

99 

100 def detectorStatistics(self, inputCalib, camera=None): 

101 """Calculate detector level statistics from the calibration. 

102 

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. 

109 

110 Returns 

111 ------- 

112 outputStatistics : `dict` [`str`, scalar] 

113 A dictionary of the statistics measured and their values. 

114 """ 

115 return {} 

116 

117 def amplifierStatistics(self, inputCalib, camera=None): 

118 """Calculate detector level statistics from the calibration. 

119 

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. 

126 

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 

174 

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]) 

179 

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) 

193 

194 return outputStatistics 

195 

196 def verify(self, calib, statisticsDict, camera=None): 

197 """Verify that the calibration meets the verification criteria. 

198 

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. 

210 

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] 

227 

228 for amp in detector: 

229 verify = {} 

230 ampName = amp.getName() 

231 calibGain = calib.gain[ampName] 

232 

233 diffGain = (np.abs(calibGain - amp.getGain()) / amp.getGain())*100 

234 diffNoise = (np.abs(calib.noise[ampName] - amp.getReadNoise()) / amp.getReadNoise())*100 

235 

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) 

242 

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) 

253 

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'") 

281 

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 

286 

287 verifyStats[ampName] = verify 

288 

289 return {'AMP': verifyStats}, bool(success)