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

397 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-14 03:47 -0800

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 

141 

142class PhotonTransferCurveSolveTask(pipeBase.PipelineTask): 

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

144 

145 The first task of the PTC measurement pipeline, 

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

147 before this task), produced a list of 

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

149 contains the mean signal and covariances of the 

150 difference image of the flat-field images taken at 

151 the same exposure time. The list also contains dummy 

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

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

154 match. 

155 

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

157 of individual PTC datasets produced 

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

159 dataset, discarding the dummy datset as appropiate. 

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

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

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

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

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

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

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

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

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

169 

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

171 of CCD sensors", arXiv:1905.08677 

172 """ 

173 

174 ConfigClass = PhotonTransferCurveSolveConfig 

175 _DefaultName = 'cpPhotonTransferCurveSolve' 

176 

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

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

179 

180 Parameters 

181 ---------- 

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

183 Butler to operate on. 

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

185 Input data refs to load. 

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

187 Output data refs to persist. 

188 """ 

189 inputs = butlerQC.get(inputRefs) 

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

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

192 butlerQC.put(outputs, outputRefs) 

193 

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

195 """Fit measured covariances to different models. 

196 

197 Parameters 

198 ---------- 

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

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

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

202 Input camera. 

203 detId : `int` 

204 Detector ID to locate the detector in the camera and 

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

206 metadata. 

207 Returns 

208 ------- 

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

210 The resultins structure contains: 

211 

212 ``outputPtcDatset`` 

213 Final PTC dataset, containing information such as the 

214 means, variances, and exposure times 

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

216 """ 

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

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

219 datasetPtc = PhotonTransferCurveDataset(ampNames=ampNames, 

220 ptcFitType=self.config.ptcFitType, 

221 covMatrixSide=self.config.maximumRangeCovariancesAstier) 

222 for partialPtcDataset in inputCovariances: 

223 # Ignore dummy datasets 

224 if partialPtcDataset.ptcFitType == 'DUMMY': 

225 continue 

226 for ampName in ampNames: 

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

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

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

230 else: 

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

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

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

234 else: 

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

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

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

238 else: 

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

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

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

242 else: 

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

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

245 datasetPtc.covariancesSqrtWeights[ampName].append( 

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

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

248 # rawMeans index 

249 for ampName in ampNames: 

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

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

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

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

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

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

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

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

258 datasetPtc.covariancesSqrtWeights[ampName])[index] 

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

260 # Fit the measured covariances vs mean signal to 

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

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

263 # signal (mu) curve using the EXPAPPROXIMATION model 

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

265 # get the flat pairs that are masked. The 

266 # points at these fluxes will also be masked when 

267 # calculating the other elements of the covariance 

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

269 

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

271 tempDatasetPtc = copy.copy(datasetPtc) 

272 tempDatasetPtc.ptcFitType = "EXPAPPROXIMATION" 

273 tempDatasetPtc = self.fitMeasurementsToModel(tempDatasetPtc) 

274 

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

276 # previous fit. 

277 for ampName in datasetPtc.ampNames: 

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

279 datasetPtc.fitType = "FULLCOVARIANCE" 

280 datasetPtc = self.fitMeasurementsToModel(datasetPtc) 

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

282 # ("EXPAPPROXIMATION", "POLYNOMIAL") 

283 else: 

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

285 # approximation (Eq. 16). Fill up 

286 # PhotonTransferCurveDataset object. 

287 datasetPtc = self.fitMeasurementsToModel(datasetPtc) 

288 

289 if camera: 

290 detector = camera[detId] 

291 else: 

292 detector = None 

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

294 

295 return pipeBase.Struct( 

296 outputPtcDataset=datasetPtc, 

297 ) 

298 

299 def fitMeasurementsToModel(self, dataset): 

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

301 polynomial or one of the models in Astier+19 

302 (Eq. 16 or Eq.20). 

303 

304 Parameters 

305 ---------- 

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

307 The dataset containing information such as the means, 

308 (co)variances, and exposure times. 

309 

310 Returns 

311 ------- 

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

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

314 it has been modified to include information such as the 

315 fit vectors and the fit parameters. See the class 

316 `PhotonTransferCurveDatase`. 

317 """ 

318 fitType = dataset.ptcFitType 

319 if fitType in ["FULLCOVARIANCE", ]: 

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

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

322 # with variance = Cov_00 

323 dataset = self.fitDataFullCovariance(dataset) 

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

325 # The PTC is technically defined as variance vs signal 

326 dataset = self.fitPtc(dataset) 

327 else: 

328 raise RuntimeError( 

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

330 "'POLYNOMIAL', 'EXPAPPROXIMATION', or 'FULLCOVARIANCE'" 

331 ) 

332 

333 return dataset 

334 

335 def fitDataFullCovariance(self, dataset): 

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

337 Astier+19 (Eq. 20). 

338 

339 Parameters 

340 ---------- 

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

342 The dataset containing information such as the means, 

343 (co)variances, and exposure times. 

344 

345 Returns 

346 ------- 

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

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

349 it has been modified to include information such as the 

350 fit vectors and the fit parameters. See the class 

351 `PhotonTransferCurveDatase`. 

352 

353 Notes 

354 ----- 

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

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

357 

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

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

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

361 - gain, units: e/ADU 

362 

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

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

365 

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

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

368 maximum lag considered for the covariances calculation, and the 

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

370 have r^2 fewer entries. 

371 """ 

372 matrixSide = self.config.maximumRangeCovariancesAstier 

373 lenParams = matrixSide*matrixSide 

374 

375 for ampName in dataset.ampNames: 

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

377 # Not used when ptcFitType is 'FULLCOVARIANCE' 

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

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

380 dataset.ptcFitChiSq[ampName] = np.nan 

381 

382 if ampName in dataset.badAmps: 

383 # Bad amp 

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

385 # with astropy.Table works. 

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

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

388 dataset.covariancesModel[ampName] = listNanMatrix 

389 dataset.covariancesSqrtWeights[ampName] = listNanMatrix 

390 dataset.aMatrix[ampName] = nanMatrix 

391 dataset.bMatrix[ampName] = nanMatrix 

392 dataset.covariancesModelNoB[ampName] = listNanMatrix 

393 dataset.aMatrixNoB[ampName] = nanMatrix 

394 

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

396 dataset.gain[ampName] = np.nan 

397 dataset.gainErr[ampName] = np.nan 

398 dataset.noise[ampName] = np.nan 

399 dataset.noiseErr[ampName] = np.nan 

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

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

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

403 continue 

404 

405 muAtAmp = dataset.rawMeans[ampName] 

406 maskAtAmp = dataset.expIdMask[ampName] 

407 if len(maskAtAmp) == 0: 

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

409 

410 muAtAmp = muAtAmp[maskAtAmp] 

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

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

413 

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

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

416 

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

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

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

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

421 'fullModelNoB': self.funcFullCovarianceModelNoB} 

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

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

424 for key in functionsDict: 

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

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

427 weightsY=covSqrtWeightsAtAmp.flatten()) 

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

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

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

431 gain = params[-1] 

432 

433 fitResults[key]['a'] = a 

434 fitResults[key]['c'] = c 

435 fitResults[key]['noise'] = noise 

436 fitResults[key]['gain'] = gain 

437 fitResults[key]['paramsErr'] = paramsErr 

438 

439 # Put the information in the PTC dataset 

440 

441 # Not used when ptcFitType is 'FULLCOVARIANCE' 

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

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

444 dataset.ptcFitChiSq[ampName] = np.nan 

445 

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

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

448 # converted to bool. 

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

450 dataset.covariances[ampName] = covAtAmp 

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

452 fitResults['fullModel']['a'], 

453 fitResults['fullModel']['c'], 

454 fitResults['fullModel']['noise'], 

455 fitResults['fullModel']['gain']) 

456 dataset.covariancesSqrtWeights[ampName] = covSqrtWeightsAtAmp 

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

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

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

460 fitResults['fullModelNoB']['a'], 

461 fitResults['fullModelNoB']['c'], 

462 fitResults['fullModelNoB']['noise'], 

463 fitResults['fullModelNoB']['gain'], 

464 setBtoZero=True) 

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

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

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

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

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

470 dataset.noise[ampName] = readoutNoise 

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

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

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

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

475 dataset.finalMeans[ampName] = muAtAmp 

476 

477 return dataset 

478 

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

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

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

482 of Astier+19. 

483 

484 Parameters 

485 ---------- 

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

487 Signal `mu` (ADU) 

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

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

490 `M = config.maximumRangeCovariancesAstier`), 

491 indexed by mean signal `mu`. 

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

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

494 

495 Returns 

496 ------- 

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

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

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

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

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

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

503 gain : `float` 

504 Amplifier gain (e/ADU) 

505 """ 

506 matrixSide = self.config.maximumRangeCovariancesAstier 

507 

508 # Initialize fit parameters 

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

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

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

512 gain = 1. 

513 

514 # iterate the fit to account for higher orders 

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

516 # stop when it increases 

517 oldChi2 = 1e30 

518 for _ in range(5): 

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

520 # loop on lags 

521 for i in range(matrixSide): 

522 for j in range(matrixSide): 

523 # fit a parabola for a given lag 

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

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

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

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

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

529 if(i + j == 0): 

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

531 weightedRes = (model - cov)*sqrtW 

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

533 if chi2 > oldChi2: 

534 break 

535 oldChi2 = chi2 

536 

537 return a, c, noise, gain 

538 

539 def funcFullCovarianceModel(self, params, x): 

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

541 Astier+19. 

542 

543 Parameters 

544 ---------- 

545 params : `list` 

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

547 gain (e/ADU). 

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

549 Signal `mu` (ADU) 

550 

551 Returns 

552 ------- 

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

554 Covariance matrix. 

555 """ 

556 matrixSide = self.config.maximumRangeCovariancesAstier 

557 lenParams = matrixSide*matrixSide 

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

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

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

561 gain = params[-1] 

562 

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

564 

565 def funcFullCovarianceModelNoB(self, params, x): 

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

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

568 

569 Parameters 

570 ---------- 

571 params : `list` 

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

573 gain (e/ADU). 

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

575 Signal mu (ADU) 

576 

577 Returns 

578 ------- 

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

580 Covariance matrix. 

581 """ 

582 matrixSide = self.config.maximumRangeCovariancesAstier 

583 lenParams = matrixSide*matrixSide 

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

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

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

587 gain = params[-1] 

588 

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

590 

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

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

593 

594 Parameters 

595 ---------- 

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

597 List of mean signals. 

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

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

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

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

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

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

604 gain : `float` 

605 Amplifier gain (e/ADU) 

606 setBtoZero=False : `bool`, optional 

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

608 

609 Returns 

610 ------- 

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

612 Covariances model. 

613 

614 Notes 

615 ----- 

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

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

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

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

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

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

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

623 

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

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

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

627 - gain, units: e/ADU 

628 

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

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

631 """ 

632 matrixSide = self.config.maximumRangeCovariancesAstier 

633 sa = (matrixSide, matrixSide) 

634 # pad a with zeros and symmetrize 

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

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

637 aSym = symmetrize(aEnlarged) 

638 # pad c with zeros and symmetrize 

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

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

641 cSym = symmetrize(cEnlarged) 

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

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

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

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

646 

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

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

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

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

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

652 

653 # assumes that mu is 1d 

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

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

656 # term, that is absent for now. 

657 if setBtoZero: 

658 c1 = np.zeros_like(c1) 

659 ac = np.zeros_like(ac) 

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

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

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

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

664 

665 return covModel 

666 

667 # EXPAPPROXIMATION and POLYNOMIAL fit methods 

668 @staticmethod 

669 def _initialParsForPolynomial(order): 

670 assert(order >= 2) 

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

672 pars[0] = 10 

673 pars[1] = 1 

674 pars[2:] = 0.0001 

675 return pars 

676 

677 @staticmethod 

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

679 if not len(lowers): 

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

681 if not len(uppers): 

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

683 lowers[1] = 0 # no negative gains 

684 return (lowers, uppers) 

685 

686 @staticmethod 

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

688 if not len(lowers): 

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

690 if not len(uppers): 

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

692 return (lowers, uppers) 

693 

694 @staticmethod 

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

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

697 

698 Parameters 

699 ---------- 

700 means : `numpy.array` 

701 Input array with mean signal values. 

702 variances : `numpy.array` 

703 Input array with variances at each mean value. 

704 minVarPivotSearch : `float` 

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

706 of decreasing variance should be sought. 

707 consecutivePointsVarDecreases : `int` 

708 Required number of consecutive points/fluxes 

709 in the PTC where the variance 

710 decreases in order to find a first 

711 estimate of the PTC turn-off. 

712 

713 Returns 

714 ------ 

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

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

717 points. 

718 

719 Notes 

720 ----- 

721 Eliminate points beyond which the variance decreases. 

722 """ 

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

724 # Variances are sorted and should monotonically increase 

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

726 if len(pivotList) > 0: 

727 # For small values, sometimes the variance decreases slightly 

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

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

730 # Require that the varince decreases during 

731 # consecutivePointsVarDecreases 

732 # consecutive points. This will give a first 

733 # estimate of the PTC turn-off, which 

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

735 if len(pivotList) > 1: 

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

737 # each value in pivotList. The lambda function subtracts 

738 # each value from the index. 

739 # groupby groups elements by equal key value. 

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

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

742 # Form groups of consecute values from pivotList 

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

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

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

746 # Find the first group of consecutive numbers when 

747 # variance decreases. 

748 if len(group) >= consecutivePointsVarDecreases: 

749 pivotIndex = np.min(group) 

750 goodPoints[pivotIndex+1:] = False 

751 break 

752 

753 return goodPoints 

754 

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

756 """""" 

757 array = np.array(array) 

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

759 if nBad == 0: 

760 return array 

761 

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

763 if len(index): 

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

765 self.log.warning(msg) 

766 

767 array[index] = substituteValue 

768 

769 return array 

770 

771 def fitPtc(self, dataset): 

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

773 Astier+19 approximation (Eq. 16). 

774 

775 Fit the photon transfer curve with either a polynomial of 

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

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

778 

779 Sigma clipping is performed iteratively for the fit, as 

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

781 than `config.initialNonLinearityExclusionThreshold` away 

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

783 because the photon transfer curve turns over catastrophically 

784 at very high flux (because saturation 

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

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

787 to perform the sigma-clipping. 

788 

789 Parameters 

790 ---------- 

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

792 The dataset containing the means, variances and 

793 exposure times. 

794 

795 Returns 

796 ------- 

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

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

799 it has been modified to include information such as the 

800 fit vectors and the fit parameters. See the class 

801 `PhotonTransferCurveDatase`. 

802 

803 Raises 

804 ------ 

805 RuntimeError 

806 Raised if dataset.ptcFitType is None or empty. 

807 """ 

808 if dataset.ptcFitType: 

809 ptcFitType = dataset.ptcFitType 

810 else: 

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

812 matrixSide = self.config.maximumRangeCovariancesAstier 

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

814 nanMatrix[:] = np.nan 

815 

816 for amp in dataset.ampNames: 

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

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

819 listNanMatrix[:] = np.nan 

820 

821 dataset.covariancesModel[amp] = listNanMatrix 

822 dataset.aMatrix[amp] = nanMatrix 

823 dataset.bMatrix[amp] = nanMatrix 

824 dataset.covariancesModelNoB[amp] = listNanMatrix 

825 dataset.aMatrixNoB[amp] = nanMatrix 

826 

827 def errFunc(p, x, y): 

828 return ptcFunc(p, x) - y 

829 

830 sigmaCutPtcOutliers = self.config.sigmaCutPtcOutliers 

831 maxIterationsPtcOutliers = self.config.maxIterationsPtcOutliers 

832 

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

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

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

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

837 varVecOriginal = self._makeZeroSafe(varVecOriginal) 

838 

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

840 # consecutive signal levels 

841 goodPoints = self._getInitialGoodPoints(meanVecOriginal, varVecOriginal, 

842 self.config.minVarPivotSearch, 

843 self.config.consecutivePointsVarDecreases) 

844 

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

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

847 

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

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

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

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

852 self.log.warning(msg) 

853 # Fill entries with NaNs 

854 self.fillBadAmp(dataset, ptcFitType, ampName) 

855 continue 

856 

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

858 # PTC turnoff point 

859 ptcTurnoff = meanVecOriginal[goodPoints][-1] 

860 dataset.ptcTurnoff[ampName] = ptcTurnoff 

861 

862 mask = goodPoints 

863 

864 if ptcFitType == 'EXPAPPROXIMATION': 

865 ptcFunc = funcAstier 

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

867 # lowers and uppers obtained from BOT data studies by 

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

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

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

871 if ptcFitType == 'POLYNOMIAL': 

872 ptcFunc = funcPolynomial 

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

874 bounds = self._boundsForPolynomial(parsIniPtc) 

875 

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

877 # This further process of outlier rejection be skipped 

878 # if self.config.maxIterationsPtcOutliers = 0. 

879 # We already did some initial outlier rejection above in 

880 # self._getInitialGoodPoints. 

881 count = 1 

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

883 pars = parsIniPtc 

884 while count <= maxIterationsPtcOutliers: 

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

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

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

888 meanTempVec = meanVecOriginal[mask] 

889 varTempVec = varVecOriginal[mask] 

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

891 pars = res.x 

892 

893 # change this to the original from the temp because 

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

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

896 # same length for broadcasting 

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

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

899 mask = mask & newMask 

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

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

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

903 self.log.warning(msg) 

904 # Fill entries with NaNs 

905 self.fillBadAmp(dataset, ptcFitType, ampName) 

906 break 

907 nDroppedTotal = Counter(mask)[False] 

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

909 count, nDroppedTotal, ampName) 

910 count += 1 

911 # objects should never shrink 

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

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

914 continue 

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

916 # store the final mask 

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

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

919 else: 

920 dataset.expIdMask[ampName] = mask 

921 # In case there was a previous mask stored 

922 mask = dataset.expIdMask[ampName] 

923 parsIniPtc = pars 

924 meanVecFinal = meanVecOriginal[mask] 

925 varVecFinal = varVecOriginal[mask] 

926 

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

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

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

930 

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

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

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

934 self.log.warning(msg) 

935 # Fill entries with NaNs 

936 self.fillBadAmp(dataset, ptcFitType, ampName) 

937 continue 

938 # Fit the PTC. 

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

940 # already calculated in `makeCovArray` of CpPtcExtract. 

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

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

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

944 if self.config.doFitBootstrap: 

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

946 varVecFinal, ptcFunc, 

947 weightsY=weightsY) 

948 else: 

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

950 varVecFinal, ptcFunc, 

951 weightsY=weightsY) 

952 dataset.ptcFitPars[ampName] = parsFit 

953 dataset.ptcFitParsError[ampName] = parsFitErr 

954 dataset.ptcFitChiSq[ampName] = reducedChiSqPtc 

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

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

957 # mask may vary per amp). 

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

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

960 constant_values=np.nan) 

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

962 'constant', constant_values=np.nan) 

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

964 constant_values=np.nan) 

965 if ptcFitType == 'EXPAPPROXIMATION': 

966 ptcGain = parsFit[1] 

967 ptcGainErr = parsFitErr[1] 

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

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

970 if ptcFitType == 'POLYNOMIAL': 

971 ptcGain = 1./parsFit[1] 

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

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

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

975 dataset.gain[ampName] = ptcGain 

976 dataset.gainErr[ampName] = ptcGainErr 

977 dataset.noise[ampName] = ptcNoise 

978 dataset.noiseErr[ampName] = ptcNoiseErr 

979 

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

981 dataset.ptcFitType = ptcFitType 

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

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

984 

985 return dataset 

986 

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

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

989 good points. 

990 

991 Parameters 

992 ---------- 

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

994 The dataset containing the means, variances and 

995 exposure times. 

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

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

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

999 ampName : `str` 

1000 Amplifier name. 

1001 """ 

1002 dataset.badAmps.append(ampName) 

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

1004 dataset.gain[ampName] = np.nan 

1005 dataset.gainErr[ampName] = np.nan 

1006 dataset.noise[ampName] = np.nan 

1007 dataset.noiseErr[ampName] = np.nan 

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

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

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

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

1012 dataset.ptcFitChiSq[ampName] = np.nan 

1013 dataset.ptcTurnoff[ampName] = np.nan 

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

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

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

1017 

1018 return