Coverage for python/lsst/cp/pipe/ptc/cpSolvePtcTask.py: 10%

402 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-18 09:40 +0000

1# This file is part of cp_pipe. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://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 <https://www.gnu.org/licenses/>. 

21# 

22import numpy as np 

23from collections import Counter 

24 

25import lsst.pex.config as pexConfig 

26import lsst.pipe.base as pipeBase 

27from lsst.cp.pipe.utils import (fitLeastSq, fitBootstrap, funcPolynomial, funcAstier, symmetrize) 

28 

29from scipy.signal import fftconvolve 

30from scipy.optimize import least_squares 

31from itertools import groupby 

32from operator import itemgetter 

33 

34import lsst.pipe.base.connectionTypes as cT 

35 

36from lsst.ip.isr import PhotonTransferCurveDataset 

37 

38from lsst.cp.pipe._lookupStaticCalibration import lookupStaticCalibration 

39 

40import copy 

41 

42 

43__all__ = ['PhotonTransferCurveSolveConfig', 'PhotonTransferCurveSolveTask'] 

44 

45 

46class PhotonTransferCurveSolveConnections(pipeBase.PipelineTaskConnections, 

47 dimensions=("instrument", "detector")): 

48 inputCovariances = cT.Input( 

49 name="ptcCovariances", 

50 doc="Tuple with measured covariances from flats.", 

51 storageClass="PhotonTransferCurveDataset", 

52 dimensions=("instrument", "exposure", "detector"), 

53 isCalibration=True, 

54 multiple=True, 

55 ) 

56 camera = cT.PrerequisiteInput( 

57 name="camera", 

58 doc="Camera the input data comes from.", 

59 storageClass="Camera", 

60 dimensions=("instrument",), 

61 isCalibration=True, 

62 lookupFunction=lookupStaticCalibration, 

63 ) 

64 outputPtcDataset = cT.Output( 

65 name="ptcDatsetProposal", 

66 doc="Output proposed ptc dataset.", 

67 storageClass="PhotonTransferCurveDataset", 

68 dimensions=("instrument", "detector"), 

69 multiple=False, 

70 isCalibration=True, 

71 ) 

72 

73 

74class PhotonTransferCurveSolveConfig(pipeBase.PipelineTaskConfig, 

75 pipelineConnections=PhotonTransferCurveSolveConnections): 

76 """Configuration for fitting measured covariances. 

77 """ 

78 

79 ptcFitType = pexConfig.ChoiceField( 

80 dtype=str, 

81 doc="Fit PTC to Eq. 16, Eq. 20 in Astier+19, or to a polynomial.", 

82 default="POLYNOMIAL", 

83 allowed={ 

84 "POLYNOMIAL": "n-degree polynomial (use 'polynomialFitDegree' to set 'n').", 

85 "EXPAPPROXIMATION": "Approximation in Astier+19 (Eq. 16).", 

86 "FULLCOVARIANCE": "Full covariances model in Astier+19 (Eq. 20)" 

87 } 

88 ) 

89 maximumRangeCovariancesAstier = pexConfig.Field( 

90 dtype=int, 

91 doc="Maximum range of covariances as in Astier+19", 

92 default=8, 

93 ) 

94 sigmaClipFullFitCovariancesAstier = pexConfig.Field( 

95 dtype=float, 

96 doc="sigma clip for full model fit for FULLCOVARIANCE ptcFitType ", 

97 default=5.0, 

98 ) 

99 maxIterFullFitCovariancesAstier = pexConfig.Field( 

100 dtype=int, 

101 doc="Maximum number of iterations in full model fit for FULLCOVARIANCE ptcFitType", 

102 default=3, 

103 ) 

104 polynomialFitDegree = pexConfig.Field( 

105 dtype=int, 

106 doc="Degree of polynomial to fit the PTC, when 'ptcFitType'=POLYNOMIAL.", 

107 default=3, 

108 ) 

109 sigmaCutPtcOutliers = pexConfig.Field( 

110 dtype=float, 

111 doc="Sigma cut for outlier rejection in PTC.", 

112 default=5.0, 

113 ) 

114 maxIterationsPtcOutliers = pexConfig.RangeField( 

115 dtype=int, 

116 doc="Maximum number of iterations for outlier rejection in PTC.", 

117 default=2, 

118 min=0 

119 ) 

120 minVarPivotSearch = pexConfig.Field( 

121 dtype=float, 

122 doc="The code looks for a pivot signal point after which the variance starts decreasing at high-flux" 

123 " to exclude then from the PTC model fit. However, sometimes at low fluxes, the variance" 

124 " decreases slightly. Set this variable for the variance value, in ADU^2, after which the pivot " 

125 " should be sought.", 

126 default=10000, 

127 ) 

128 consecutivePointsVarDecreases = pexConfig.RangeField( 

129 dtype=int, 

130 doc="Required number of consecutive points/fluxes in the PTC where the variance " 

131 "decreases in order to find a first estimate of the PTC turn-off. ", 

132 default=2, 

133 min=2 

134 ) 

135 doFitBootstrap = pexConfig.Field( 

136 dtype=bool, 

137 doc="Use bootstrap for the PTC fit parameters and errors?.", 

138 default=False, 

139 ) 

140 binSize = pexConfig.Field( 

141 dtype=int, 

142 doc="Bin the image by this factor in both dimensions.", 

143 default=1, 

144 ) 

145 

146 

147class PhotonTransferCurveSolveTask(pipeBase.PipelineTask): 

148 """Task to fit the PTC from flat covariances. 

149 

150 The first task of the PTC measurement pipeline, 

151 ``PhotonTransferCurveMeasureTask`` (and assumed to have been run 

152 before this task), produced a list of 

153 `~lsst.ip.isr.PhotonTransferCurveDataset` objects. Each dataset 

154 contains the mean signal and covariances of the 

155 difference image of the flat-field images taken at 

156 the same exposure time. The list also contains dummy 

157 datasets (with no measurements), whose purpose is to have 

158 the input and output dimensions of ``PhotonTransferCurveMeasureTask`` 

159 match. 

160 

161 This task, ``PhotonTransferCurveSolveTask``, assembles the list 

162 of individual PTC datasets produced 

163 by ``PhotonTransferCurveMeasureTask`` into one single final PTC 

164 dataset, discarding the dummy datset as appropiate. 

165 The task fits the measured (co)variances to one of three models: 

166 a polynomial model of a given order, or the models described 

167 in equations 16 and 20 of Astier+19. These options are referred 

168 to as ``POLYNOMIAL``, ``EXPAPPROXIMATION``, and ``FULLCOVARIANCE`` 

169 in the configuration options of the task, respectively). 

170 Parameters of interest such as the gain and noise are derived 

171 from the fits. The ``FULLCOVARIANCE`` model is fitted to the 

172 full covariance data (as oppossed to the other two models, which 

173 are fit to the variance vs mean measurements only). 

174 

175 Astier+19: "The Shape of the Photon Transfer Curve 

176 of CCD sensors", arXiv:1905.08677 

177 """ 

178 

179 ConfigClass = PhotonTransferCurveSolveConfig 

180 _DefaultName = 'cpPhotonTransferCurveSolve' 

181 

182 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

183 """Ensure that the input and output dimensions are passed along. 

184 

185 Parameters 

186 ---------- 

187 butlerQC : `~lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext` 

188 Butler to operate on. 

189 inputRefs : `~lsst.pipe.base.connections.InputQuantizedConnection` 

190 Input data refs to load. 

191 ouptutRefs : `~lsst.pipe.base.connections.OutputQuantizedConnection` 

192 Output data refs to persist. 

193 """ 

194 inputs = butlerQC.get(inputRefs) 

195 detId = inputRefs.inputCovariances[0].dataId['detector'] 

196 outputs = self.run(inputCovariances=inputs['inputCovariances'], camera=inputs['camera'], detId=detId) 

197 butlerQC.put(outputs, outputRefs) 

198 

199 def run(self, inputCovariances, camera=None, detId=0): 

200 """Fit measured covariances to different models. 

201 

202 Parameters 

203 ---------- 

204 inputCovariances : `list` [`lsst.ip.isr.PhotonTransferCurveDataset`] 

205 List of lsst.ip.isr.PhotonTransferCurveDataset datasets. 

206 camera : `lsst.afw.cameraGeom.Camera`, optional 

207 Input camera. 

208 detId : `int` 

209 Detector ID to locate the detector in the camera and 

210 populate the `lsst.ip.isr.PhotonTransferCurveDataset` 

211 metadata. 

212 Returns 

213 ------- 

214 results : `lsst.pipe.base.Struct` 

215 The resultins structure contains: 

216 

217 ``outputPtcDatset`` 

218 Final PTC dataset, containing information such as the 

219 means, variances, and exposure times 

220 (`lsst.ip.isr.PhotonTransferCurveDataset`). 

221 """ 

222 # Assemble individual PTC datasets into a single PTC dataset. 

223 ampNames = np.unique(inputCovariances[0].ampNames) 

224 datasetPtc = PhotonTransferCurveDataset(ampNames=ampNames, 

225 ptcFitType=self.config.ptcFitType, 

226 covMatrixSide=self.config.maximumRangeCovariancesAstier) 

227 for partialPtcDataset in inputCovariances: 

228 # Ignore dummy datasets 

229 if partialPtcDataset.ptcFitType == 'DUMMY': 

230 continue 

231 for ampName in ampNames: 

232 datasetPtc.inputExpIdPairs[ampName].append(partialPtcDataset.inputExpIdPairs[ampName]) 

233 if type(partialPtcDataset.rawExpTimes[ampName]) is list: 

234 datasetPtc.rawExpTimes[ampName].append(partialPtcDataset.rawExpTimes[ampName][0]) 

235 else: 

236 datasetPtc.rawExpTimes[ampName].append(partialPtcDataset.rawExpTimes[ampName]) 

237 if type(partialPtcDataset.rawMeans[ampName]) is list: 

238 datasetPtc.rawMeans[ampName].append(partialPtcDataset.rawMeans[ampName][0]) 

239 else: 

240 datasetPtc.rawMeans[ampName].append(partialPtcDataset.rawMeans[ampName]) 

241 if type(partialPtcDataset.rawVars[ampName]) is list: 

242 datasetPtc.rawVars[ampName].append(partialPtcDataset.rawVars[ampName][0]) 

243 else: 

244 datasetPtc.rawVars[ampName].append(partialPtcDataset.rawVars[ampName]) 

245 if type(partialPtcDataset.expIdMask[ampName]) is list: 

246 datasetPtc.expIdMask[ampName].append(partialPtcDataset.expIdMask[ampName][0]) 

247 else: 

248 datasetPtc.expIdMask[ampName].append(partialPtcDataset.expIdMask[ampName]) 

249 datasetPtc.covariances[ampName].append(np.array(partialPtcDataset.covariances[ampName][0])) 

250 datasetPtc.covariancesSqrtWeights[ampName].append( 

251 np.array(partialPtcDataset.covariancesSqrtWeights[ampName][0])) 

252 # Sort arrays that are filled so far in the final dataset by 

253 # rawMeans index 

254 for ampName in ampNames: 

255 index = np.argsort(np.ravel(np.array(datasetPtc.rawMeans[ampName]))) 

256 datasetPtc.inputExpIdPairs[ampName] = np.array(datasetPtc.inputExpIdPairs[ampName])[index] 

257 datasetPtc.rawExpTimes[ampName] = np.array(datasetPtc.rawExpTimes[ampName])[index] 

258 datasetPtc.rawMeans[ampName] = np.array(datasetPtc.rawMeans[ampName])[index] 

259 datasetPtc.rawVars[ampName] = np.array(datasetPtc.rawVars[ampName])[index] 

260 datasetPtc.expIdMask[ampName] = np.array(datasetPtc.expIdMask[ampName])[index] 

261 datasetPtc.covariances[ampName] = np.array(datasetPtc.covariances[ampName])[index] 

262 datasetPtc.covariancesSqrtWeights[ampName] = np.array( 

263 datasetPtc.covariancesSqrtWeights[ampName])[index] 

264 if self.config.ptcFitType == "FULLCOVARIANCE": 

265 # Fit the measured covariances vs mean signal to 

266 # the Astier+19 full model (Eq. 20). Before that 

267 # do a preliminary fit to the variance (C_00) vs mean 

268 # signal (mu) curve using the EXPAPPROXIMATION model 

269 # (Eq. 16 in Astier+19) in order to 

270 # get the flat pairs that are masked. The 

271 # points at these fluxes will also be masked when 

272 # calculating the other elements of the covariance 

273 # matrix, C_ij, i!=j). 

274 

275 # Preliminary fit, usign a temp dataset to get the mask 

276 tempDatasetPtc = copy.copy(datasetPtc) 

277 tempDatasetPtc.ptcFitType = "EXPAPPROXIMATION" 

278 tempDatasetPtc = self.fitMeasurementsToModel(tempDatasetPtc) 

279 

280 # "FULLCOVARIANCE", using the mask obtained from the 

281 # previous fit. 

282 for ampName in datasetPtc.ampNames: 

283 datasetPtc.expIdMask[ampName] = tempDatasetPtc.expIdMask[ampName] 

284 datasetPtc.fitType = "FULLCOVARIANCE" 

285 datasetPtc = self.fitMeasurementsToModel(datasetPtc) 

286 # The other options are: self.config.ptcFitType in 

287 # ("EXPAPPROXIMATION", "POLYNOMIAL") 

288 else: 

289 # Fit the PTC to a polynomial or to Astier+19 exponential 

290 # approximation (Eq. 16). Fill up 

291 # PhotonTransferCurveDataset object. 

292 datasetPtc = self.fitMeasurementsToModel(datasetPtc) 

293 

294 if camera: 

295 detector = camera[detId] 

296 else: 

297 detector = None 

298 datasetPtc.updateMetadataFromExposures(inputCovariances) 

299 datasetPtc.updateMetadata(setDate=True, camera=camera, detector=detector) 

300 

301 return pipeBase.Struct( 

302 outputPtcDataset=datasetPtc, 

303 ) 

304 

305 def fitMeasurementsToModel(self, dataset): 

306 """Fit the measured covariances vs mean signal to a 

307 polynomial or one of the models in Astier+19 

308 (Eq. 16 or Eq.20). 

309 

310 Parameters 

311 ---------- 

312 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

313 The dataset containing information such as the means, 

314 (co)variances, and exposure times. 

315 

316 Returns 

317 ------- 

318 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

319 This is the same dataset as the input parameter, however, 

320 it has been modified to include information such as the 

321 fit vectors and the fit parameters. See the class 

322 `PhotonTransferCurveDatase`. 

323 """ 

324 fitType = dataset.ptcFitType 

325 if fitType in ["FULLCOVARIANCE", ]: 

326 # This model uses the full covariance matrix in the fit. 

327 # The PTC is technically defined as variance vs signal, 

328 # with variance = Cov_00 

329 dataset = self.fitDataFullCovariance(dataset) 

330 elif fitType in ["POLYNOMIAL", "EXPAPPROXIMATION"]: 

331 # The PTC is technically defined as variance vs signal 

332 dataset = self.fitPtc(dataset) 

333 else: 

334 raise RuntimeError( 

335 f"Fitting option {fitType} not one of " 

336 "'POLYNOMIAL', 'EXPAPPROXIMATION', or 'FULLCOVARIANCE'" 

337 ) 

338 

339 return dataset 

340 

341 def fitDataFullCovariance(self, dataset): 

342 """Fit measured flat covariances to the full model in 

343 Astier+19 (Eq. 20). 

344 

345 Parameters 

346 ---------- 

347 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

348 The dataset containing information such as the means, 

349 (co)variances, and exposure times. 

350 

351 Returns 

352 ------- 

353 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

354 This is the same dataset as the input parameter, however, 

355 it has been modified to include information such as the 

356 fit vectors and the fit parameters. See the class 

357 `PhotonTransferCurveDatase`. 

358 

359 Notes 

360 ----- 

361 The parameters of the full model for C_ij(mu) ("C_ij" and "mu" 

362 in ADU^2 and ADU, respectively) in Astier+19 (Eq. 20) are: 

363 

364 - "a" coefficients (r by r matrix), units: 1/e 

365 - "b" coefficients (r by r matrix), units: 1/e 

366 - noise matrix (r by r matrix), units: e^2 

367 - gain, units: e/ADU 

368 

369 "b" appears in Eq. 20 only through the "ab" combination, which 

370 is defined in this code as "c=ab". 

371 

372 Total number of parameters: #entries(a) + #entries(c) + #entries(noise) 

373 + 1. This is equivalent to r^2 + r^2 + r^2 + 1, where "r" is the 

374 maximum lag considered for the covariances calculation, and the 

375 extra "1" is the gain. If "b" is 0, then "c" is 0, and len(pInit) will 

376 have r^2 fewer entries. 

377 """ 

378 matrixSide = self.config.maximumRangeCovariancesAstier 

379 lenParams = matrixSide*matrixSide 

380 

381 for ampName in dataset.ampNames: 

382 lenInputTimes = len(dataset.rawExpTimes[ampName]) 

383 # Not used when ptcFitType is 'FULLCOVARIANCE' 

384 dataset.ptcFitPars[ampName] = [np.nan] 

385 dataset.ptcFitParsError[ampName] = [np.nan] 

386 dataset.ptcFitChiSq[ampName] = np.nan 

387 

388 if ampName in dataset.badAmps: 

389 # Bad amp 

390 # Entries need to have proper dimensions so read/write 

391 # with astropy.Table works. 

392 nanMatrix = np.full((matrixSide, matrixSide), np.nan) 

393 listNanMatrix = np.full((lenInputTimes, matrixSide, matrixSide), np.nan) 

394 dataset.covariancesModel[ampName] = listNanMatrix 

395 dataset.covariancesSqrtWeights[ampName] = listNanMatrix 

396 dataset.aMatrix[ampName] = nanMatrix 

397 dataset.bMatrix[ampName] = nanMatrix 

398 dataset.covariancesModelNoB[ampName] = listNanMatrix 

399 dataset.aMatrixNoB[ampName] = nanMatrix 

400 

401 dataset.expIdMask[ampName] = np.repeat(False, lenInputTimes) 

402 dataset.gain[ampName] = np.nan 

403 dataset.gainErr[ampName] = np.nan 

404 dataset.noise[ampName] = np.nan 

405 dataset.noiseErr[ampName] = np.nan 

406 dataset.finalVars[ampName] = np.repeat(np.nan, lenInputTimes) 

407 dataset.finalModelVars[ampName] = np.repeat(np.nan, lenInputTimes) 

408 dataset.finalMeans[ampName] = np.repeat(np.nan, lenInputTimes) 

409 continue 

410 

411 muAtAmp = dataset.rawMeans[ampName] 

412 maskAtAmp = dataset.expIdMask[ampName] 

413 if len(maskAtAmp) == 0: 

414 maskAtAmp = np.repeat(True, len(muAtAmp)) 

415 

416 muAtAmp = muAtAmp[maskAtAmp] 

417 covAtAmp = np.nan_to_num(dataset.covariances[ampName])[maskAtAmp] 

418 covSqrtWeightsAtAmp = np.nan_to_num(dataset.covariancesSqrtWeights[ampName])[maskAtAmp] 

419 

420 # Initial fit, to approximate parameters, with c=0 

421 a0, c0, noise0, gain0 = self.initialFitFullCovariance(muAtAmp, covAtAmp, covSqrtWeightsAtAmp) 

422 

423 # Fit full model (Eq. 20 of Astier+19) and same model with 

424 # b=0 (c=0 in this code) 

425 pInit = np.concatenate((a0.flatten(), c0.flatten(), noise0.flatten(), np.array(gain0)), axis=None) 

426 functionsDict = {'fullModel': self.funcFullCovarianceModel, 

427 'fullModelNoB': self.funcFullCovarianceModelNoB} 

428 fitResults = {'fullModel': {'a': [], 'c': [], 'noise': [], 'gain': [], 'paramsErr': []}, 

429 'fullModelNoB': {'a': [], 'c': [], 'noise': [], 'gain': [], 'paramsErr': []}} 

430 for key in functionsDict: 

431 params, paramsErr, _ = fitLeastSq(pInit, muAtAmp, 

432 covAtAmp.flatten(), functionsDict[key], 

433 weightsY=covSqrtWeightsAtAmp.flatten()) 

434 a = params[:lenParams].reshape((matrixSide, matrixSide)) 

435 c = params[lenParams:2*lenParams].reshape((matrixSide, matrixSide)) 

436 noise = params[2*lenParams:3*lenParams].reshape((matrixSide, matrixSide)) 

437 gain = params[-1] 

438 

439 fitResults[key]['a'] = a 

440 fitResults[key]['c'] = c 

441 fitResults[key]['noise'] = noise 

442 fitResults[key]['gain'] = gain 

443 fitResults[key]['paramsErr'] = paramsErr 

444 

445 # Put the information in the PTC dataset 

446 

447 # Not used when ptcFitType is 'FULLCOVARIANCE' 

448 dataset.ptcFitPars[ampName] = [np.nan] 

449 dataset.ptcFitParsError[ampName] = [np.nan] 

450 dataset.ptcFitChiSq[ampName] = np.nan 

451 

452 # Save full covariances, covariances models, and their weights. 

453 # dataset.expIdMask is already full, but needs to be 

454 # converted to bool. 

455 dataset.expIdMask[ampName] = np.array(dataset.expIdMask[ampName], dtype=bool) 

456 dataset.covariances[ampName] = covAtAmp 

457 dataset.covariancesModel[ampName] = self.evalCovModel(muAtAmp, 

458 fitResults['fullModel']['a'], 

459 fitResults['fullModel']['c'], 

460 fitResults['fullModel']['noise'], 

461 fitResults['fullModel']['gain']) 

462 dataset.covariancesSqrtWeights[ampName] = covSqrtWeightsAtAmp 

463 dataset.aMatrix[ampName] = fitResults['fullModel']['a'] 

464 dataset.bMatrix[ampName] = fitResults['fullModel']['c']/fitResults['fullModel']['a'] 

465 dataset.covariancesModelNoB[ampName] = self.evalCovModel(muAtAmp, 

466 fitResults['fullModelNoB']['a'], 

467 fitResults['fullModelNoB']['c'], 

468 fitResults['fullModelNoB']['noise'], 

469 fitResults['fullModelNoB']['gain'], 

470 setBtoZero=True) 

471 dataset.aMatrixNoB[ampName] = fitResults['fullModelNoB']['a'] 

472 dataset.gain[ampName] = fitResults['fullModel']['gain'] 

473 dataset.gainErr[ampName] = fitResults['fullModel']['paramsErr'][-1] 

474 readoutNoise = fitResults['fullModel']['noise'][0][0] 

475 readoutNoiseSqrt = np.sqrt(np.fabs(readoutNoise)) 

476 dataset.noise[ampName] = readoutNoise 

477 readoutNoiseSigma = fitResults['fullModel']['paramsErr'][2*lenParams] 

478 dataset.noiseErr[ampName] = 0.5*(readoutNoiseSigma/np.fabs(readoutNoise))*readoutNoiseSqrt 

479 dataset.finalVars[ampName] = covAtAmp[:, 0, 0] 

480 dataset.finalModelVars[ampName] = dataset.covariancesModel[ampName][:, 0, 0] 

481 dataset.finalMeans[ampName] = muAtAmp 

482 

483 return dataset 

484 

485 def initialFitFullCovariance(self, mu, cov, sqrtW): 

486 """ Performs a crude parabolic fit of the data in order to start 

487 the full fit close to the solution, setting b=0 (c=0) in Eq. 20 

488 of Astier+19. 

489 

490 Parameters 

491 ---------- 

492 mu : `numpy.array`, (N,) 

493 Signal `mu` (ADU) 

494 cov : `numpy.array`, (N, M, M) 

495 Covariance arrays of size `(M, M)` (with 

496 `M = config.maximumRangeCovariancesAstier`), 

497 indexed by mean signal `mu`. 

498 sqrtW : `numpy.array`, (N,) 

499 Covariance weights, defined as 1./sqrt(Variances) 

500 

501 Returns 

502 ------- 

503 a : `numpy.array`, (M, M) 

504 "a" parameter per flux in Eq. 20 of Astier+19. 

505 c : `numpy.array`, (M, M) 

506 "c"="ab" parameter per flux in Eq. 20 of Astier+19. 

507 noise : `numpy.array`, (M, M) 

508 "noise" parameter per flux in Eq. 20 of Astier+19. 

509 gain : `float` 

510 Amplifier gain (e/ADU) 

511 """ 

512 matrixSide = self.config.maximumRangeCovariancesAstier 

513 

514 # Initialize fit parameters 

515 a = np.zeros((matrixSide, matrixSide)) 

516 c = np.zeros((matrixSide, matrixSide)) 

517 noise = np.zeros((matrixSide, matrixSide)) 

518 gain = 1. 

519 

520 # iterate the fit to account for higher orders 

521 # the chi2 does not necessarily go down, so one could 

522 # stop when it increases 

523 oldChi2 = 1e30 

524 for _ in range(5): 

525 model = np.nan_to_num(self.evalCovModel(mu, a, c, noise, gain, setBtoZero=True)) 

526 # loop on lags 

527 for i in range(matrixSide): 

528 for j in range(matrixSide): 

529 # fit a parabola for a given lag 

530 parsFit = np.polyfit(mu, cov[:, i, j] - model[:, i, j], 

531 2, w=sqrtW[:, i, j]) 

532 # model equation (Eq. 20) in Astier+19, with c=a*b=0: 

533 a[i, j] += parsFit[0] 

534 noise[i, j] += parsFit[2] 

535 if(i + j == 0): 

536 gain = 1./(1/gain+parsFit[1]) 

537 weightedRes = (model - cov)*sqrtW 

538 chi2 = (weightedRes.flatten()**2).sum() 

539 if chi2 > oldChi2: 

540 break 

541 oldChi2 = chi2 

542 

543 return a, c, noise, gain 

544 

545 def funcFullCovarianceModel(self, params, x): 

546 """Model to fit covariances from flat fields; Equation 20 of 

547 Astier+19. 

548 

549 Parameters 

550 ---------- 

551 params : `list` 

552 Parameters of the model: aMatrix, CMatrix, noiseMatrix, 

553 gain (e/ADU). 

554 x : `numpy.array`, (N,) 

555 Signal `mu` (ADU) 

556 

557 Returns 

558 ------- 

559 y : `numpy.array`, (N,) 

560 Covariance matrix. 

561 """ 

562 matrixSide = self.config.maximumRangeCovariancesAstier 

563 lenParams = matrixSide*matrixSide 

564 aMatrix = params[:lenParams].reshape((matrixSide, matrixSide)) 

565 cMatrix = params[lenParams:2*lenParams].reshape((matrixSide, matrixSide)) 

566 noiseMatrix = params[2*lenParams:3*lenParams].reshape((matrixSide, matrixSide)) 

567 gain = params[-1] 

568 

569 return self.evalCovModel(x, aMatrix, cMatrix, noiseMatrix, gain).flatten() 

570 

571 def funcFullCovarianceModelNoB(self, params, x): 

572 """Model to fit covariances from flat fields; Equation 20 of 

573 Astier+19, with b=0 (equivalent to c=a*b=0 in this code). 

574 

575 Parameters 

576 ---------- 

577 params : `list` 

578 Parameters of the model: aMatrix, CMatrix, noiseMatrix, 

579 gain (e/ADU). 

580 x : `numpy.array`, (N,) 

581 Signal mu (ADU) 

582 

583 Returns 

584 ------- 

585 y : `numpy.array`, (N,) 

586 Covariance matrix. 

587 """ 

588 matrixSide = self.config.maximumRangeCovariancesAstier 

589 lenParams = matrixSide*matrixSide 

590 aMatrix = params[:lenParams].reshape((matrixSide, matrixSide)) 

591 cMatrix = params[lenParams:2*lenParams].reshape((matrixSide, matrixSide)) 

592 noiseMatrix = params[2*lenParams:3*lenParams].reshape((matrixSide, matrixSide)) 

593 gain = params[-1] 

594 

595 return self.evalCovModel(x, aMatrix, cMatrix, noiseMatrix, gain, setBtoZero=True).flatten() 

596 

597 def evalCovModel(self, mu, aMatrix, cMatrix, noiseMatrix, gain, setBtoZero=False): 

598 """Computes full covariances model (Eq. 20 of Astier+19). 

599 

600 Parameters 

601 ---------- 

602 mu : `numpy.array`, (N,) 

603 List of mean signals. 

604 aMatrix : `numpy.array`, (M, M) 

605 "a" parameter per flux in Eq. 20 of Astier+19. 

606 cMatrix : `numpy.array`, (M, M) 

607 "c"="ab" parameter per flux in Eq. 20 of Astier+19. 

608 noiseMatrix : `numpy.array`, (M, M) 

609 "noise" parameter per flux in Eq. 20 of Astier+19. 

610 gain : `float` 

611 Amplifier gain (e/ADU) 

612 setBtoZero=False : `bool`, optional 

613 Set "b" parameter in full model (see Astier+19) to zero. 

614 

615 Returns 

616 ------- 

617 covModel : `numpy.array`, (N, M, M) 

618 Covariances model. 

619 

620 Notes 

621 ----- 

622 By default, computes the covModel for the mu's stored(self.mu). 

623 Returns cov[Nmu, M, M]. The variance for the PTC is 

624 cov[:, 0, 0]. mu and cov are in ADUs and ADUs squared. To use 

625 electrons for both, the gain should be set to 1. This routine 

626 implements the model in Astier+19 (1905.08677). 

627 The parameters of the full model for C_ij(mu) ("C_ij" and "mu" 

628 in ADU^2 and ADU, respectively) in Astier+19 (Eq. 20) are: 

629 

630 - "a" coefficients (M by M matrix), units: 1/e 

631 - "b" coefficients (M by M matrix), units: 1/e 

632 - noise matrix (M by M matrix), units: e^2 

633 - gain, units: e/ADU 

634 

635 "b" appears in Eq. 20 only through the "ab" combination, which 

636 is defined in this code as "c=ab". 

637 """ 

638 matrixSide = self.config.maximumRangeCovariancesAstier 

639 sa = (matrixSide, matrixSide) 

640 # pad a with zeros and symmetrize 

641 aEnlarged = np.zeros((int(sa[0]*1.5)+1, int(sa[1]*1.5)+1)) 

642 aEnlarged[0:sa[0], 0:sa[1]] = aMatrix 

643 aSym = symmetrize(aEnlarged) 

644 # pad c with zeros and symmetrize 

645 cEnlarged = np.zeros((int(sa[0]*1.5)+1, int(sa[1]*1.5)+1)) 

646 cEnlarged[0:sa[0], 0:sa[1]] = cMatrix 

647 cSym = symmetrize(cEnlarged) 

648 a2 = fftconvolve(aSym, aSym, mode='same') 

649 a3 = fftconvolve(a2, aSym, mode='same') 

650 ac = fftconvolve(aSym, cSym, mode='same') 

651 (xc, yc) = np.unravel_index(np.abs(aSym).argmax(), a2.shape) 

652 

653 a1 = aMatrix[np.newaxis, :, :] 

654 a2 = a2[np.newaxis, xc:xc + matrixSide, yc:yc + matrixSide] 

655 a3 = a3[np.newaxis, xc:xc + matrixSide, yc:yc + matrixSide] 

656 ac = ac[np.newaxis, xc:xc + matrixSide, yc:yc + matrixSide] 

657 c1 = cMatrix[np.newaxis, ::] 

658 

659 # assumes that mu is 1d 

660 bigMu = mu[:, np.newaxis, np.newaxis]*gain 

661 # c(=a*b in Astier+19) also has a contribution to the last 

662 # term, that is absent for now. 

663 if setBtoZero: 

664 c1 = np.zeros_like(c1) 

665 ac = np.zeros_like(ac) 

666 covModel = (bigMu/(gain*gain)*(a1*bigMu+2./3.*(bigMu*bigMu)*(a2 + c1) 

667 + (1./3.*a3 + 5./6.*ac)*(bigMu*bigMu*bigMu)) + noiseMatrix[np.newaxis, :, :]/gain**2) 

668 # add the Poisson term, and the read out noise (variance) 

669 covModel[:, 0, 0] += mu/gain 

670 

671 return covModel 

672 

673 # EXPAPPROXIMATION and POLYNOMIAL fit methods 

674 @staticmethod 

675 def _initialParsForPolynomial(order): 

676 assert(order >= 2) 

677 pars = np.zeros(order, dtype=float) 

678 pars[0] = 10 

679 pars[1] = 1 

680 pars[2:] = 0.0001 

681 return pars 

682 

683 @staticmethod 

684 def _boundsForPolynomial(initialPars, lowers=[], uppers=[]): 

685 if not len(lowers): 

686 lowers = [np.NINF for p in initialPars] 

687 if not len(uppers): 

688 uppers = [np.inf for p in initialPars] 

689 lowers[1] = 0 # no negative gains 

690 return (lowers, uppers) 

691 

692 @staticmethod 

693 def _boundsForAstier(initialPars, lowers=[], uppers=[]): 

694 if not len(lowers): 

695 lowers = [np.NINF for p in initialPars] 

696 if not len(uppers): 

697 uppers = [np.inf for p in initialPars] 

698 return (lowers, uppers) 

699 

700 @staticmethod 

701 def _getInitialGoodPoints(means, variances, minVarPivotSearch, consecutivePointsVarDecreases): 

702 """Return a boolean array to mask bad points. 

703 

704 Parameters 

705 ---------- 

706 means : `numpy.array` 

707 Input array with mean signal values. 

708 variances : `numpy.array` 

709 Input array with variances at each mean value. 

710 minVarPivotSearch : `float` 

711 The variance (in ADU^2), above which, the point 

712 of decreasing variance should be sought. 

713 consecutivePointsVarDecreases : `int` 

714 Required number of consecutive points/fluxes 

715 in the PTC where the variance 

716 decreases in order to find a first 

717 estimate of the PTC turn-off. 

718 

719 Returns 

720 ------ 

721 goodPoints : `numpy.array` [`bool`] 

722 Boolean array to select good (`True`) and bad (`False`) 

723 points. 

724 

725 Notes 

726 ----- 

727 Eliminate points beyond which the variance decreases. 

728 """ 

729 goodPoints = np.ones_like(means, dtype=bool) 

730 # Variances are sorted and should monotonically increase 

731 pivotList = np.where(np.array(np.diff(variances)) < 0)[0] 

732 if len(pivotList) > 0: 

733 # For small values, sometimes the variance decreases slightly 

734 # Only look when var > self.config.minVarPivotSearch 

735 pivotList = [p for p in pivotList if variances[p] > minVarPivotSearch] 

736 # Require that the varince decreases during 

737 # consecutivePointsVarDecreases 

738 # consecutive points. This will give a first 

739 # estimate of the PTC turn-off, which 

740 # may be updated (reduced) further in the code. 

741 if len(pivotList) > 1: 

742 # enumerate(pivotList) creates tuples (index, value), for 

743 # each value in pivotList. The lambda function subtracts 

744 # each value from the index. 

745 # groupby groups elements by equal key value. 

746 for k, g in groupby(enumerate(pivotList), lambda x: x[0]-x[1]): 

747 group = (map(itemgetter(1), g)) 

748 # Form groups of consecute values from pivotList 

749 group = list(map(int, group)) 

750 # values in pivotList are indices where np.diff(variances) 

751 # is negative, i.e., where the variance starts decreasing. 

752 # Find the first group of consecutive numbers when 

753 # variance decreases. 

754 if len(group) >= consecutivePointsVarDecreases: 

755 pivotIndex = np.min(group) 

756 goodPoints[pivotIndex+1:] = False 

757 break 

758 

759 # Finally, we filter out any infinities or NaNs. 

760 goodPoints[(~np.isfinite(means)) | (~np.isfinite(variances))] = False 

761 

762 return goodPoints 

763 

764 def _makeZeroSafe(self, array, substituteValue=1e-9): 

765 """""" 

766 array = np.array(array) 

767 nBad = Counter(np.ravel(array))[0] 

768 if nBad == 0: 

769 return array 

770 

771 index, = np.where(array == 0) 

772 if len(index): 

773 msg = f"Found {nBad} zeros in array at elements {index}" 

774 self.log.warning(msg) 

775 

776 array[index] = substituteValue 

777 

778 return array 

779 

780 def fitPtc(self, dataset): 

781 """Fit the photon transfer curve to a polynomial or to the 

782 Astier+19 approximation (Eq. 16). 

783 

784 Fit the photon transfer curve with either a polynomial of 

785 the order specified in the task config, or using the 

786 exponential approximation in Astier+19 (Eq. 16). 

787 

788 Sigma clipping is performed iteratively for the fit, as 

789 well as an initial clipping of data points that are more 

790 than `config.initialNonLinearityExclusionThreshold` away 

791 from lying on a straight line. This other step is necessary 

792 because the photon transfer curve turns over catastrophically 

793 at very high flux (because saturation 

794 drops the variance to ~0) and these far outliers cause the 

795 initial fit to fail, meaning the sigma cannot be calculated 

796 to perform the sigma-clipping. 

797 

798 Parameters 

799 ---------- 

800 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

801 The dataset containing the means, variances and 

802 exposure times. 

803 

804 Returns 

805 ------- 

806 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

807 This is the same dataset as the input parameter, however, 

808 it has been modified to include information such as the 

809 fit vectors and the fit parameters. See the class 

810 `PhotonTransferCurveDatase`. 

811 

812 Raises 

813 ------ 

814 RuntimeError 

815 Raised if dataset.ptcFitType is None or empty. 

816 """ 

817 if dataset.ptcFitType: 

818 ptcFitType = dataset.ptcFitType 

819 else: 

820 raise RuntimeError("ptcFitType is None of empty in PTC dataset.") 

821 matrixSide = self.config.maximumRangeCovariancesAstier 

822 nanMatrix = np.empty((matrixSide, matrixSide)) 

823 nanMatrix[:] = np.nan 

824 

825 for amp in dataset.ampNames: 

826 lenInputTimes = len(dataset.rawExpTimes[amp]) 

827 listNanMatrix = np.empty((lenInputTimes, matrixSide, matrixSide)) 

828 listNanMatrix[:] = np.nan 

829 

830 dataset.covariancesModel[amp] = listNanMatrix 

831 dataset.aMatrix[amp] = nanMatrix 

832 dataset.bMatrix[amp] = nanMatrix 

833 dataset.covariancesModelNoB[amp] = listNanMatrix 

834 dataset.aMatrixNoB[amp] = nanMatrix 

835 

836 def errFunc(p, x, y): 

837 return ptcFunc(p, x) - y 

838 

839 sigmaCutPtcOutliers = self.config.sigmaCutPtcOutliers 

840 maxIterationsPtcOutliers = self.config.maxIterationsPtcOutliers 

841 

842 for i, ampName in enumerate(dataset.ampNames): 

843 timeVecOriginal = np.ravel(np.array(dataset.rawExpTimes[ampName])) 

844 meanVecOriginal = np.ravel(np.array(dataset.rawMeans[ampName])) 

845 varVecOriginal = np.ravel(np.array(dataset.rawVars[ampName])) 

846 varVecOriginal = self._makeZeroSafe(varVecOriginal) 

847 

848 # Discard points when the variance starts to decrease after two 

849 # consecutive signal levels 

850 goodPoints = self._getInitialGoodPoints(meanVecOriginal, varVecOriginal, 

851 self.config.minVarPivotSearch, 

852 self.config.consecutivePointsVarDecreases) 

853 

854 # Check if all points are bad from the 'cpExtractPtcTask' 

855 initialExpIdMask = np.ravel(np.array(dataset.expIdMask[ampName])) 

856 

857 if not (goodPoints.any() and initialExpIdMask.any()): 

858 msg = (f"SERIOUS: All points in goodPoints: {goodPoints} or " 

859 f"in initialExpIdMask: {initialExpIdMask} are bad." 

860 f"Setting {ampName} to BAD.") 

861 self.log.warning(msg) 

862 # Fill entries with NaNs 

863 self.fillBadAmp(dataset, ptcFitType, ampName) 

864 continue 

865 

866 # Save the point where the variance starts decreasing as the 

867 # PTC turnoff point 

868 ptcTurnoff = meanVecOriginal[goodPoints][-1] 

869 dataset.ptcTurnoff[ampName] = ptcTurnoff 

870 

871 mask = goodPoints 

872 

873 if ptcFitType == 'EXPAPPROXIMATION': 

874 ptcFunc = funcAstier 

875 parsIniPtc = [-1e-9, 1.0, 10.] # a00, gain, noise^2 

876 # lowers and uppers obtained from BOT data studies by 

877 # C. Lage (UC Davis, 11/2020). 

878 if self.config.binSize > 1: 

879 bounds = self._boundsForAstier(parsIniPtc) 

880 else: 

881 bounds = self._boundsForAstier(parsIniPtc, lowers=[-1e-4, 0.5, -2000], 

882 uppers=[1e-4, 2.5, 2000]) 

883 if ptcFitType == 'POLYNOMIAL': 

884 ptcFunc = funcPolynomial 

885 parsIniPtc = self._initialParsForPolynomial(self.config.polynomialFitDegree + 1) 

886 bounds = self._boundsForPolynomial(parsIniPtc) 

887 

888 # Before bootstrap fit, do an iterative fit to get rid of outliers. 

889 # This further process of outlier rejection be skipped 

890 # if self.config.maxIterationsPtcOutliers = 0. 

891 # We already did some initial outlier rejection above in 

892 # self._getInitialGoodPoints. 

893 count = 1 

894 newMask = np.ones_like(meanVecOriginal, dtype=bool) 

895 pars = parsIniPtc 

896 while count <= maxIterationsPtcOutliers: 

897 # Note that application of the mask actually shrinks the array 

898 # to size rather than setting elements to zero (as we want) so 

899 # always update mask itself and re-apply to the original data 

900 meanTempVec = meanVecOriginal[mask] 

901 varTempVec = varVecOriginal[mask] 

902 res = least_squares(errFunc, parsIniPtc, bounds=bounds, args=(meanTempVec, varTempVec)) 

903 pars = res.x 

904 

905 # change this to the original from the temp because 

906 # the masks are ANDed meaning once a point is masked 

907 # it's always masked, and the masks must always be the 

908 # same length for broadcasting 

909 sigResids = (varVecOriginal - ptcFunc(pars, meanVecOriginal))/np.sqrt(varVecOriginal) 

910 newMask = np.array([True if np.abs(r) < sigmaCutPtcOutliers else False for r in sigResids]) 

911 mask = mask & newMask 

912 if not (mask.any() and newMask.any()): 

913 msg = (f"SERIOUS: All points in either mask: {mask} or newMask: {newMask} are bad. " 

914 f"Setting {ampName} to BAD.") 

915 self.log.warning(msg) 

916 # Fill entries with NaNs 

917 self.fillBadAmp(dataset, ptcFitType, ampName) 

918 break 

919 nDroppedTotal = Counter(mask)[False] 

920 self.log.debug("Iteration %d: discarded %d points in total for %s", 

921 count, nDroppedTotal, ampName) 

922 count += 1 

923 # objects should never shrink 

924 assert (len(mask) == len(timeVecOriginal) == len(meanVecOriginal) == len(varVecOriginal)) 

925 if not (mask.any() and newMask.any()): 

926 continue 

927 dataset.expIdMask[ampName] = np.array(dataset.expIdMask[ampName]) 

928 # store the final mask 

929 if len(dataset.expIdMask[ampName]): 

930 dataset.expIdMask[ampName] &= mask # bitwise_and if there is already a mask 

931 else: 

932 dataset.expIdMask[ampName] = mask 

933 # In case there was a previous mask stored 

934 mask = dataset.expIdMask[ampName] 

935 parsIniPtc = pars 

936 meanVecFinal = meanVecOriginal[mask] 

937 varVecFinal = varVecOriginal[mask] 

938 

939 if Counter(mask)[False] > 0: 

940 self.log.info("Number of points discarded in PTC of amplifier %s:" 

941 " %d out of %d", ampName, Counter(mask)[False], len(meanVecOriginal)) 

942 

943 if (len(meanVecFinal) < len(parsIniPtc)): 

944 msg = (f"SERIOUS: Not enough data points ({len(meanVecFinal)}) compared to the number of " 

945 f"parameters of the PTC model({len(parsIniPtc)}). Setting {ampName} to BAD.") 

946 self.log.warning(msg) 

947 # Fill entries with NaNs 

948 self.fillBadAmp(dataset, ptcFitType, ampName) 

949 continue 

950 # Fit the PTC. 

951 # The variance of the variance is Var(v)=2*v^2/Npix. This is 

952 # already calculated in `makeCovArray` of CpPtcExtract. 

953 # dataset.covariancesSqrtWeights[ampName][:,0,0] 

954 # has 1/sqrt(Var(v)). 

955 weightsY = dataset.covariancesSqrtWeights[ampName][:, 0, 0][mask] 

956 if self.config.doFitBootstrap: 

957 parsFit, parsFitErr, reducedChiSqPtc = fitBootstrap(parsIniPtc, meanVecFinal, 

958 varVecFinal, ptcFunc, 

959 weightsY=weightsY) 

960 else: 

961 parsFit, parsFitErr, reducedChiSqPtc = fitLeastSq(parsIniPtc, meanVecFinal, 

962 varVecFinal, ptcFunc, 

963 weightsY=weightsY) 

964 dataset.ptcFitPars[ampName] = parsFit 

965 dataset.ptcFitParsError[ampName] = parsFitErr 

966 dataset.ptcFitChiSq[ampName] = reducedChiSqPtc 

967 # Masked variances (measured and modeled) and means. Need 

968 # to pad the array so astropy.Table does not crash (the 

969 # mask may vary per amp). 

970 padLength = len(dataset.rawExpTimes[ampName]) - len(varVecFinal) 

971 dataset.finalVars[ampName] = np.pad(varVecFinal, (0, padLength), 'constant', 

972 constant_values=np.nan) 

973 dataset.finalModelVars[ampName] = np.pad(ptcFunc(parsFit, meanVecFinal), (0, padLength), 

974 'constant', constant_values=np.nan) 

975 dataset.finalMeans[ampName] = np.pad(meanVecFinal, (0, padLength), 'constant', 

976 constant_values=np.nan) 

977 if ptcFitType == 'EXPAPPROXIMATION': 

978 ptcGain = parsFit[1] 

979 ptcGainErr = parsFitErr[1] 

980 ptcNoise = np.sqrt(np.fabs(parsFit[2])) 

981 ptcNoiseErr = 0.5*(parsFitErr[2]/np.fabs(parsFit[2]))*np.sqrt(np.fabs(parsFit[2])) 

982 if ptcFitType == 'POLYNOMIAL': 

983 ptcGain = 1./parsFit[1] 

984 ptcGainErr = np.fabs(1./parsFit[1])*(parsFitErr[1]/parsFit[1]) 

985 ptcNoise = np.sqrt(np.fabs(parsFit[0]))*ptcGain 

986 ptcNoiseErr = (0.5*(parsFitErr[0]/np.fabs(parsFit[0]))*(np.sqrt(np.fabs(parsFit[0]))))*ptcGain 

987 dataset.gain[ampName] = ptcGain 

988 dataset.gainErr[ampName] = ptcGainErr 

989 dataset.noise[ampName] = ptcNoise 

990 dataset.noiseErr[ampName] = ptcNoiseErr 

991 

992 if not len(dataset.ptcFitType) == 0: 

993 dataset.ptcFitType = ptcFitType 

994 if len(dataset.badAmps) == 0: 

995 dataset.badAmps = np.repeat(np.nan, len(list(dataset.rawExpTimes.values())[0])) 

996 

997 return dataset 

998 

999 def fillBadAmp(self, dataset, ptcFitType, ampName): 

1000 """Fill the dataset with NaNs if there are not enough 

1001 good points. 

1002 

1003 Parameters 

1004 ---------- 

1005 dataset : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

1006 The dataset containing the means, variances and 

1007 exposure times. 

1008 ptcFitType : {'POLYNOMIAL', 'EXPAPPROXIMATION'} 

1009 Fit a 'POLYNOMIAL' (degree: 'polynomialFitDegree') or 

1010 'EXPAPPROXIMATION' (Eq. 16 of Astier+19) to the PTC. 

1011 ampName : `str` 

1012 Amplifier name. 

1013 """ 

1014 dataset.badAmps.append(ampName) 

1015 dataset.expIdMask[ampName] = np.repeat(False, len(dataset.rawExpTimes[ampName])) 

1016 dataset.gain[ampName] = np.nan 

1017 dataset.gainErr[ampName] = np.nan 

1018 dataset.noise[ampName] = np.nan 

1019 dataset.noiseErr[ampName] = np.nan 

1020 dataset.ptcFitPars[ampName] = (np.repeat(np.nan, self.config.polynomialFitDegree + 1) if 

1021 ptcFitType in ["POLYNOMIAL", ] else np.repeat(np.nan, 3)) 

1022 dataset.ptcFitParsError[ampName] = (np.repeat(np.nan, self.config.polynomialFitDegree + 1) if 

1023 ptcFitType in ["POLYNOMIAL", ] else np.repeat(np.nan, 3)) 

1024 dataset.ptcFitChiSq[ampName] = np.nan 

1025 dataset.ptcTurnoff[ampName] = np.nan 

1026 dataset.finalVars[ampName] = np.repeat(np.nan, len(dataset.rawExpTimes[ampName])) 

1027 dataset.finalModelVars[ampName] = np.repeat(np.nan, len(dataset.rawExpTimes[ampName])) 

1028 dataset.finalMeans[ampName] = np.repeat(np.nan, len(dataset.rawExpTimes[ampName])) 

1029 

1030 return