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

400 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-27 03:40 -0700

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 # Find the ampNames from a non-dummy ptc. 

223 ampNames = [] 

224 for partialPtcDataset in inputCovariances: 

225 if partialPtcDataset.ptcFitType != 'DUMMY': 

226 ampNames = partialPtcDataset.ampNames 

227 break 

228 

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

230 datasetPtc = PhotonTransferCurveDataset(ampNames=ampNames, 

231 ptcFitType=self.config.ptcFitType, 

232 covMatrixSide=self.config.maximumRangeCovariancesAstier) 

233 for partialPtcDataset in inputCovariances: 

234 # Ignore dummy datasets 

235 if partialPtcDataset.ptcFitType == 'DUMMY': 

236 continue 

237 for ampName in ampNames: 

238 # The partial dataset consists of lists of values for each 

239 # quantity. In the case of the input exposure pairs, this is a 

240 # list of tuples. In all cases we only want the first 

241 # (and only) element of the list. 

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

243 datasetPtc.rawExpTimes[ampName] = np.append(datasetPtc.rawExpTimes[ampName], 

244 partialPtcDataset.rawExpTimes[ampName][0]) 

245 datasetPtc.rawMeans[ampName] = np.append(datasetPtc.rawMeans[ampName], 

246 partialPtcDataset.rawMeans[ampName][0]) 

247 datasetPtc.rawVars[ampName] = np.append(datasetPtc.rawVars[ampName], 

248 partialPtcDataset.rawVars[ampName][0]) 

249 datasetPtc.expIdMask[ampName] = np.append(datasetPtc.expIdMask[ampName], 

250 partialPtcDataset.expIdMask[ampName][0]) 

251 datasetPtc.covariances[ampName] = np.append( 

252 datasetPtc.covariances[ampName].ravel(), 

253 partialPtcDataset.covariances[ampName].ravel() 

254 ).reshape( 

255 ( 

256 len(datasetPtc.rawExpTimes[ampName]), 

257 datasetPtc.covMatrixSide, 

258 datasetPtc.covMatrixSide, 

259 ) 

260 ) 

261 datasetPtc.covariancesSqrtWeights[ampName] = np.append( 

262 datasetPtc.covariancesSqrtWeights[ampName].ravel(), 

263 partialPtcDataset.covariancesSqrtWeights[ampName].ravel() 

264 ).reshape( 

265 ( 

266 len(datasetPtc.rawExpTimes[ampName]), 

267 datasetPtc.covMatrixSide, 

268 datasetPtc.covMatrixSide, 

269 ) 

270 ) 

271 

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

273 # rawMeans index 

274 for ampName in ampNames: 

275 index = np.argsort(datasetPtc.rawMeans[ampName]) 

276 datasetPtc.inputExpIdPairs[ampName] = np.array( 

277 datasetPtc.inputExpIdPairs[ampName] 

278 )[index].tolist() 

279 datasetPtc.rawExpTimes[ampName] = datasetPtc.rawExpTimes[ampName][index] 

280 datasetPtc.rawMeans[ampName] = datasetPtc.rawMeans[ampName][index] 

281 datasetPtc.rawVars[ampName] = datasetPtc.rawVars[ampName][index] 

282 datasetPtc.expIdMask[ampName] = datasetPtc.expIdMask[ampName][index] 

283 datasetPtc.covariances[ampName] = datasetPtc.covariances[ampName][index] 

284 datasetPtc.covariancesSqrtWeights[ampName] = datasetPtc.covariancesSqrtWeights[ampName][index] 

285 

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

287 # Fit the measured covariances vs mean signal to 

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

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

290 # signal (mu) curve using the EXPAPPROXIMATION model 

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

292 # get the flat pairs that are masked. The 

293 # points at these fluxes will also be masked when 

294 # calculating the other elements of the covariance 

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

296 

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

298 tempDatasetPtc = copy.copy(datasetPtc) 

299 tempDatasetPtc.ptcFitType = "EXPAPPROXIMATION" 

300 tempDatasetPtc = self.fitMeasurementsToModel(tempDatasetPtc) 

301 

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

303 # previous fit. 

304 for ampName in datasetPtc.ampNames: 

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

306 datasetPtc.fitType = "FULLCOVARIANCE" 

307 datasetPtc = self.fitMeasurementsToModel(datasetPtc) 

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

309 # ("EXPAPPROXIMATION", "POLYNOMIAL") 

310 else: 

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

312 # approximation (Eq. 16). Fill up 

313 # PhotonTransferCurveDataset object. 

314 datasetPtc = self.fitMeasurementsToModel(datasetPtc) 

315 

316 if camera: 

317 detector = camera[detId] 

318 else: 

319 detector = None 

320 datasetPtc.updateMetadataFromExposures(inputCovariances) 

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

322 

323 return pipeBase.Struct( 

324 outputPtcDataset=datasetPtc, 

325 ) 

326 

327 def fitMeasurementsToModel(self, dataset): 

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

329 polynomial or one of the models in Astier+19 

330 (Eq. 16 or Eq.20). 

331 

332 Parameters 

333 ---------- 

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

335 The dataset containing information such as the means, 

336 (co)variances, and exposure times. 

337 

338 Returns 

339 ------- 

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

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

342 it has been modified to include information such as the 

343 fit vectors and the fit parameters. See the class 

344 `PhotonTransferCurveDatase`. 

345 """ 

346 fitType = dataset.ptcFitType 

347 if fitType in ["FULLCOVARIANCE", ]: 

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

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

350 # with variance = Cov_00 

351 dataset = self.fitDataFullCovariance(dataset) 

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

353 # The PTC is technically defined as variance vs signal 

354 dataset = self.fitPtc(dataset) 

355 else: 

356 raise RuntimeError( 

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

358 "'POLYNOMIAL', 'EXPAPPROXIMATION', or 'FULLCOVARIANCE'" 

359 ) 

360 

361 return dataset 

362 

363 def fitDataFullCovariance(self, dataset): 

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

365 Astier+19 (Eq. 20). 

366 

367 Parameters 

368 ---------- 

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

370 The dataset containing information such as the means, 

371 (co)variances, and exposure times. 

372 

373 Returns 

374 ------- 

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

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

377 it has been modified to include information such as the 

378 fit vectors and the fit parameters. See the class 

379 `PhotonTransferCurveDatase`. 

380 

381 Notes 

382 ----- 

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

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

385 

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

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

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

389 - gain, units: e/ADU 

390 

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

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

393 

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

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

396 maximum lag considered for the covariances calculation, and the 

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

398 have r^2 fewer entries. 

399 """ 

400 matrixSide = self.config.maximumRangeCovariancesAstier 

401 lenParams = matrixSide*matrixSide 

402 

403 for ampName in dataset.ampNames: 

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

405 # Not used when ptcFitType is 'FULLCOVARIANCE' 

406 dataset.ptcFitPars[ampName] = np.array([np.nan]) 

407 dataset.ptcFitParsError[ampName] = np.array([np.nan]) 

408 dataset.ptcFitChiSq[ampName] = np.nan 

409 

410 if ampName in dataset.badAmps: 

411 # Bad amp 

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

413 # with astropy.Table works. 

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

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

416 dataset.covariancesModel[ampName] = listNanMatrix 

417 dataset.covariancesSqrtWeights[ampName] = listNanMatrix 

418 dataset.aMatrix[ampName] = nanMatrix 

419 dataset.bMatrix[ampName] = nanMatrix 

420 dataset.covariancesModelNoB[ampName] = listNanMatrix 

421 dataset.aMatrixNoB[ampName] = nanMatrix 

422 

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

424 dataset.gain[ampName] = np.nan 

425 dataset.gainErr[ampName] = np.nan 

426 dataset.noise[ampName] = np.nan 

427 dataset.noiseErr[ampName] = np.nan 

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

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

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

431 continue 

432 

433 muAtAmp = dataset.rawMeans[ampName] 

434 maskAtAmp = dataset.expIdMask[ampName] 

435 if len(maskAtAmp) == 0: 

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

437 

438 muAtAmpMasked = muAtAmp[maskAtAmp] 

439 covAtAmp = dataset.covariances[ampName] 

440 covAtAmpMasked = np.nan_to_num(covAtAmp)[maskAtAmp] 

441 covSqrtWeightsAtAmp = dataset.covariancesSqrtWeights[ampName] 

442 covSqrtWeightsAtAmpMasked = np.nan_to_num(covSqrtWeightsAtAmp)[maskAtAmp] 

443 

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

445 a0, c0, noise0, gain0 = self.initialFitFullCovariance( 

446 muAtAmpMasked, 

447 covAtAmpMasked, 

448 covSqrtWeightsAtAmpMasked 

449 ) 

450 

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

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

453 pInit = np.concatenate((a0.ravel(), c0.ravel(), noise0.ravel(), np.array(gain0)), axis=None) 

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

455 'fullModelNoB': self.funcFullCovarianceModelNoB} 

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

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

458 for key in functionsDict: 

459 params, paramsErr, _ = fitLeastSq(pInit, muAtAmpMasked, 

460 covAtAmpMasked.ravel(), functionsDict[key], 

461 weightsY=covSqrtWeightsAtAmpMasked.ravel()) 

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

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

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

465 gain = params[-1] 

466 

467 fitResults[key]['a'] = a 

468 fitResults[key]['c'] = c 

469 fitResults[key]['noise'] = noise 

470 fitResults[key]['gain'] = gain 

471 fitResults[key]['paramsErr'] = paramsErr 

472 

473 # Put the information in the PTC dataset 

474 

475 # Not used when ptcFitType is 'FULLCOVARIANCE' 

476 dataset.ptcFitPars[ampName] = np.array([np.nan]) 

477 dataset.ptcFitParsError[ampName] = np.array([np.nan]) 

478 dataset.ptcFitChiSq[ampName] = np.nan 

479 

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

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

482 # converted to bool. 

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

484 dataset.covariances[ampName] = covAtAmp 

485 # We evaluate the covariance model everywhere, even the 

486 # masked amps. 

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

488 fitResults['fullModel']['a'], 

489 fitResults['fullModel']['c'], 

490 fitResults['fullModel']['noise'], 

491 fitResults['fullModel']['gain']) 

492 dataset.covariancesSqrtWeights[ampName] = covSqrtWeightsAtAmp 

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

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

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

496 fitResults['fullModelNoB']['a'], 

497 fitResults['fullModelNoB']['c'], 

498 fitResults['fullModelNoB']['noise'], 

499 fitResults['fullModelNoB']['gain'], 

500 setBtoZero=True) 

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

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

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

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

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

506 dataset.noise[ampName] = readoutNoise 

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

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

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

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

511 dataset.finalMeans[ampName] = muAtAmp 

512 

513 return dataset 

514 

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

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

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

518 of Astier+19. 

519 

520 Parameters 

521 ---------- 

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

523 Signal `mu` (ADU) 

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

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

526 `M = config.maximumRangeCovariancesAstier`), 

527 indexed by mean signal `mu`. 

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

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

530 

531 Returns 

532 ------- 

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

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

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

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

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

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

539 gain : `float` 

540 Amplifier gain (e/ADU) 

541 """ 

542 matrixSide = self.config.maximumRangeCovariancesAstier 

543 

544 # Initialize fit parameters 

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

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

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

548 gain = 1. 

549 

550 # iterate the fit to account for higher orders 

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

552 # stop when it increases 

553 oldChi2 = 1e30 

554 for _ in range(5): 

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

556 # loop on lags 

557 for i in range(matrixSide): 

558 for j in range(matrixSide): 

559 # fit a parabola for a given lag 

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

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

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

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

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

565 if(i + j == 0): 

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

567 weightedRes = (model - cov)*sqrtW 

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

569 if chi2 > oldChi2: 

570 break 

571 oldChi2 = chi2 

572 

573 return a, c, noise, gain 

574 

575 def funcFullCovarianceModel(self, params, x): 

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

577 Astier+19. 

578 

579 Parameters 

580 ---------- 

581 params : `list` 

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

583 gain (e/ADU). 

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

585 Signal `mu` (ADU) 

586 

587 Returns 

588 ------- 

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

590 Covariance matrix. 

591 """ 

592 matrixSide = self.config.maximumRangeCovariancesAstier 

593 lenParams = matrixSide*matrixSide 

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

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

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

597 gain = params[-1] 

598 

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

600 

601 def funcFullCovarianceModelNoB(self, params, x): 

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

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

604 

605 Parameters 

606 ---------- 

607 params : `list` 

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

609 gain (e/ADU). 

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

611 Signal mu (ADU) 

612 

613 Returns 

614 ------- 

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

616 Covariance matrix. 

617 """ 

618 matrixSide = self.config.maximumRangeCovariancesAstier 

619 lenParams = matrixSide*matrixSide 

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

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

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

623 gain = params[-1] 

624 

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

626 

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

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

629 

630 Parameters 

631 ---------- 

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

633 List of mean signals. 

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

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

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

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

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

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

640 gain : `float` 

641 Amplifier gain (e/ADU) 

642 setBtoZero=False : `bool`, optional 

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

644 

645 Returns 

646 ------- 

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

648 Covariances model. 

649 

650 Notes 

651 ----- 

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

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

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

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

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

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

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

659 

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

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

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

663 - gain, units: e/ADU 

664 

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

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

667 """ 

668 matrixSide = self.config.maximumRangeCovariancesAstier 

669 sa = (matrixSide, matrixSide) 

670 # pad a with zeros and symmetrize 

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

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

673 aSym = symmetrize(aEnlarged) 

674 # pad c with zeros and symmetrize 

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

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

677 cSym = symmetrize(cEnlarged) 

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

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

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

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

682 

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

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

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

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

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

688 

689 # assumes that mu is 1d 

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

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

692 # term, that is absent for now. 

693 if setBtoZero: 

694 c1 = np.zeros_like(c1) 

695 ac = np.zeros_like(ac) 

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

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

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

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

700 

701 return covModel 

702 

703 # EXPAPPROXIMATION and POLYNOMIAL fit methods 

704 @staticmethod 

705 def _initialParsForPolynomial(order): 

706 assert(order >= 2) 

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

708 pars[0] = 10 

709 pars[1] = 1 

710 pars[2:] = 0.0001 

711 return pars 

712 

713 @staticmethod 

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

715 if not len(lowers): 

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

717 if not len(uppers): 

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

719 lowers[1] = 0 # no negative gains 

720 return (lowers, uppers) 

721 

722 @staticmethod 

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

724 if not len(lowers): 

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

726 if not len(uppers): 

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

728 return (lowers, uppers) 

729 

730 @staticmethod 

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

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

733 

734 Parameters 

735 ---------- 

736 means : `numpy.array` 

737 Input array with mean signal values. 

738 variances : `numpy.array` 

739 Input array with variances at each mean value. 

740 minVarPivotSearch : `float` 

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

742 of decreasing variance should be sought. 

743 consecutivePointsVarDecreases : `int` 

744 Required number of consecutive points/fluxes 

745 in the PTC where the variance 

746 decreases in order to find a first 

747 estimate of the PTC turn-off. 

748 

749 Returns 

750 ------ 

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

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

753 points. 

754 

755 Notes 

756 ----- 

757 Eliminate points beyond which the variance decreases. 

758 """ 

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

760 # Variances are sorted and should monotonically increase 

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

762 if len(pivotList) > 0: 

763 # For small values, sometimes the variance decreases slightly 

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

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

766 # Require that the varince decreases during 

767 # consecutivePointsVarDecreases 

768 # consecutive points. This will give a first 

769 # estimate of the PTC turn-off, which 

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

771 if len(pivotList) > 1: 

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

773 # each value in pivotList. The lambda function subtracts 

774 # each value from the index. 

775 # groupby groups elements by equal key value. 

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

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

778 # Form groups of consecute values from pivotList 

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

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

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

782 # Find the first group of consecutive numbers when 

783 # variance decreases. 

784 if len(group) >= consecutivePointsVarDecreases: 

785 pivotIndex = np.min(group) 

786 goodPoints[pivotIndex+1:] = False 

787 break 

788 

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

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

791 

792 return goodPoints 

793 

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

795 """""" 

796 array = np.array(array) 

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

798 if nBad == 0: 

799 return array 

800 

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

802 if len(index): 

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

804 self.log.warning(msg) 

805 

806 array[index] = substituteValue 

807 

808 return array 

809 

810 def fitPtc(self, dataset): 

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

812 Astier+19 approximation (Eq. 16). 

813 

814 Fit the photon transfer curve with either a polynomial of 

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

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

817 

818 Sigma clipping is performed iteratively for the fit, as 

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

820 than `config.initialNonLinearityExclusionThreshold` away 

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

822 because the photon transfer curve turns over catastrophically 

823 at very high flux (because saturation 

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

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

826 to perform the sigma-clipping. 

827 

828 Parameters 

829 ---------- 

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

831 The dataset containing the means, variances and 

832 exposure times. 

833 

834 Returns 

835 ------- 

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

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

838 it has been modified to include information such as the 

839 fit vectors and the fit parameters. See the class 

840 `PhotonTransferCurveDatase`. 

841 

842 Raises 

843 ------ 

844 RuntimeError 

845 Raised if dataset.ptcFitType is None or empty. 

846 """ 

847 if dataset.ptcFitType: 

848 ptcFitType = dataset.ptcFitType 

849 else: 

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

851 matrixSide = self.config.maximumRangeCovariancesAstier 

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

853 nanMatrix[:] = np.nan 

854 

855 for amp in dataset.ampNames: 

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

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

858 listNanMatrix[:] = np.nan 

859 

860 dataset.covariancesModel[amp] = listNanMatrix 

861 dataset.aMatrix[amp] = nanMatrix 

862 dataset.bMatrix[amp] = nanMatrix 

863 dataset.covariancesModelNoB[amp] = listNanMatrix 

864 dataset.aMatrixNoB[amp] = nanMatrix 

865 

866 def errFunc(p, x, y): 

867 return ptcFunc(p, x) - y 

868 

869 sigmaCutPtcOutliers = self.config.sigmaCutPtcOutliers 

870 maxIterationsPtcOutliers = self.config.maxIterationsPtcOutliers 

871 

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

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

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

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

876 varVecOriginal = self._makeZeroSafe(varVecOriginal) 

877 

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

879 # consecutive signal levels 

880 goodPoints = self._getInitialGoodPoints(meanVecOriginal, varVecOriginal, 

881 self.config.minVarPivotSearch, 

882 self.config.consecutivePointsVarDecreases) 

883 

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

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

886 

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

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

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

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

891 self.log.warning(msg) 

892 # Fill entries with NaNs 

893 self.fillBadAmp(dataset, ptcFitType, ampName) 

894 continue 

895 

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

897 # PTC turnoff point 

898 ptcTurnoff = meanVecOriginal[goodPoints][-1] 

899 dataset.ptcTurnoff[ampName] = ptcTurnoff 

900 

901 mask = goodPoints 

902 

903 if ptcFitType == 'EXPAPPROXIMATION': 

904 ptcFunc = funcAstier 

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

906 # lowers and uppers obtained from BOT data studies by 

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

908 if self.config.binSize > 1: 

909 bounds = self._boundsForAstier(parsIniPtc) 

910 else: 

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

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

913 if ptcFitType == 'POLYNOMIAL': 

914 ptcFunc = funcPolynomial 

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

916 bounds = self._boundsForPolynomial(parsIniPtc) 

917 

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

919 # This further process of outlier rejection be skipped 

920 # if self.config.maxIterationsPtcOutliers = 0. 

921 # We already did some initial outlier rejection above in 

922 # self._getInitialGoodPoints. 

923 count = 1 

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

925 pars = parsIniPtc 

926 while count <= maxIterationsPtcOutliers: 

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

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

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

930 meanTempVec = meanVecOriginal[mask] 

931 varTempVec = varVecOriginal[mask] 

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

933 pars = res.x 

934 

935 # change this to the original from the temp because 

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

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

938 # same length for broadcasting 

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

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

941 mask = mask & newMask 

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

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

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

945 self.log.warning(msg) 

946 # Fill entries with NaNs 

947 self.fillBadAmp(dataset, ptcFitType, ampName) 

948 break 

949 nDroppedTotal = Counter(mask)[False] 

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

951 count, nDroppedTotal, ampName) 

952 count += 1 

953 # objects should never shrink 

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

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

956 continue 

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

958 # store the final mask 

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

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

961 else: 

962 dataset.expIdMask[ampName] = mask 

963 # In case there was a previous mask stored 

964 mask = dataset.expIdMask[ampName] 

965 parsIniPtc = pars 

966 meanVecFinal = meanVecOriginal[mask] 

967 varVecFinal = varVecOriginal[mask] 

968 

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

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

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

972 

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

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

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

976 self.log.warning(msg) 

977 # Fill entries with NaNs 

978 self.fillBadAmp(dataset, ptcFitType, ampName) 

979 continue 

980 # Fit the PTC. 

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

982 # already calculated in `makeCovArray` of CpPtcExtract. 

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

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

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

986 if self.config.doFitBootstrap: 

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

988 varVecFinal, ptcFunc, 

989 weightsY=weightsY) 

990 else: 

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

992 varVecFinal, ptcFunc, 

993 weightsY=weightsY) 

994 dataset.ptcFitPars[ampName] = parsFit 

995 dataset.ptcFitParsError[ampName] = parsFitErr 

996 dataset.ptcFitChiSq[ampName] = reducedChiSqPtc 

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

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

999 # mask may vary per amp). 

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

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

1002 constant_values=np.nan) 

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

1004 'constant', constant_values=np.nan) 

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

1006 constant_values=np.nan) 

1007 if ptcFitType == 'EXPAPPROXIMATION': 

1008 ptcGain = parsFit[1] 

1009 ptcGainErr = parsFitErr[1] 

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

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

1012 if ptcFitType == 'POLYNOMIAL': 

1013 ptcGain = 1./parsFit[1] 

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

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

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

1017 dataset.gain[ampName] = ptcGain 

1018 dataset.gainErr[ampName] = ptcGainErr 

1019 dataset.noise[ampName] = ptcNoise 

1020 dataset.noiseErr[ampName] = ptcNoiseErr 

1021 

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

1023 dataset.ptcFitType = ptcFitType 

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

1025 dataset.badAmps = [] 

1026 

1027 return dataset 

1028 

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

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

1031 good points. 

1032 

1033 Parameters 

1034 ---------- 

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

1036 The dataset containing the means, variances and 

1037 exposure times. 

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

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

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

1041 ampName : `str` 

1042 Amplifier name. 

1043 """ 

1044 dataset.badAmps.append(ampName) 

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

1046 dataset.gain[ampName] = np.nan 

1047 dataset.gainErr[ampName] = np.nan 

1048 dataset.noise[ampName] = np.nan 

1049 dataset.noiseErr[ampName] = np.nan 

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

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

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

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

1054 dataset.ptcFitChiSq[ampName] = np.nan 

1055 dataset.ptcTurnoff[ampName] = np.nan 

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

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

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

1059 

1060 return