Coverage for tests/test_ptc.py: 8%

369 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-18 03:03 -0700

1#!/usr/bin/env python 

2 

3# 

4# LSST Data Management System 

5# 

6# Copyright 2008-2017 AURA/LSST. 

7# 

8# This product includes software developed by the 

9# LSST Project (http://www.lsst.org/). 

10# 

11# This program is free software: you can redistribute it and/or modify 

12# it under the terms of the GNU General Public License as published by 

13# the Free Software Foundation, either version 3 of the License, or 

14# (at your option) any later version. 

15# 

16# This program is distributed in the hope that it will be useful, 

17# but WITHOUT ANY WARRANTY; without even the implied warranty of 

18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19# GNU General Public License for more details. 

20# 

21# You should have received a copy of the LSST License Statement and 

22# the GNU General Public License along with this program. If not, 

23# see <https://www.lsstcorp.org/LegalNotices/>. 

24# 

25"""Test cases for cp_pipe.""" 

26 

27import unittest 

28import numpy as np 

29import copy 

30import tempfile 

31import logging 

32 

33import lsst.utils 

34import lsst.utils.tests 

35 

36import lsst.cp.pipe as cpPipe 

37import lsst.ip.isr.isrMock as isrMock 

38from lsst.ip.isr import PhotonTransferCurveDataset 

39from lsst.cp.pipe.utils import (funcPolynomial, makeMockFlats) 

40 

41from lsst.pipe.base import TaskMetadata 

42 

43 

44class FakeCamera(list): 

45 def getName(self): 

46 return "FakeCam" 

47 

48 

49class PretendRef(): 

50 "A class to act as a mock exposure reference" 

51 def __init__(self, exposure): 

52 self.exp = exposure 

53 

54 def get(self, component=None): 

55 if component == 'visitInfo': 

56 return self.exp.getVisitInfo() 

57 elif component == 'detector': 

58 return self.exp.getDetector() 

59 elif component == 'metadata': 

60 return self.exp.getMetadata() 

61 else: 

62 return self.exp 

63 

64 

65class MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase): 

66 """A test case for the PTC tasks.""" 

67 

68 def setUp(self): 

69 self.defaultConfigExtract = cpPipe.ptc.PhotonTransferCurveExtractTask.ConfigClass() 

70 self.defaultTaskExtract = cpPipe.ptc.PhotonTransferCurveExtractTask(config=self.defaultConfigExtract) 

71 

72 self.defaultConfigSolve = cpPipe.ptc.PhotonTransferCurveSolveTask.ConfigClass() 

73 self.defaultTaskSolve = cpPipe.ptc.PhotonTransferCurveSolveTask(config=self.defaultConfigSolve) 

74 

75 self.flatMean = 2000 

76 self.readNoiseAdu = 10 

77 mockImageConfig = isrMock.IsrMock.ConfigClass() 

78 

79 # flatDrop is not really relevant as we replace the data 

80 # but good to note it in case we change how this image is made 

81 mockImageConfig.flatDrop = 0.99999 

82 mockImageConfig.isTrimmed = True 

83 

84 self.flatExp1 = isrMock.FlatMock(config=mockImageConfig).run() 

85 self.flatExp2 = self.flatExp1.clone() 

86 (shapeY, shapeX) = self.flatExp1.getDimensions() 

87 

88 self.flatWidth = np.sqrt(self.flatMean) + self.readNoiseAdu 

89 

90 self.rng1 = np.random.RandomState(1984) 

91 flatData1 = self.rng1.normal(self.flatMean, self.flatWidth, (shapeX, shapeY)) 

92 self.rng2 = np.random.RandomState(666) 

93 flatData2 = self.rng2.normal(self.flatMean, self.flatWidth, (shapeX, shapeY)) 

94 

95 self.flatExp1.image.array[:] = flatData1 

96 self.flatExp2.image.array[:] = flatData2 

97 

98 # create fake PTC data to see if fit works, for one amp ('amp') 

99 self.flux = 1000. # ADU/sec 

100 self.timeVec = np.arange(1., 101., 5) 

101 self.k2NonLinearity = -5e-6 

102 # quadratic signal-chain non-linearity 

103 muVec = self.flux*self.timeVec + self.k2NonLinearity*self.timeVec**2 

104 self.gain = 0.75 # e-/ADU 

105 self.c1 = 1./self.gain 

106 self.noiseSq = 2*self.gain # 7.5 (e-)^2 

107 self.a00 = -1.2e-6 

108 self.c2 = -1.5e-6 

109 self.c3 = -4.7e-12 # tuned so that it turns over for 200k mean 

110 

111 self.ampNames = [amp.getName() for amp in self.flatExp1.getDetector().getAmplifiers()] 

112 self.dataset = PhotonTransferCurveDataset(self.ampNames, ptcFitType="PARTIAL") 

113 self.covariancesSqrtWeights = {} 

114 for ampName in self.ampNames: # just the expTimes and means here - vars vary per function 

115 self.dataset.rawExpTimes[ampName] = self.timeVec 

116 self.dataset.rawMeans[ampName] = muVec 

117 self.dataset.covariancesSqrtWeights[ampName] = np.zeros((1, 

118 self.dataset.covMatrixSide, 

119 self.dataset.covMatrixSide)) 

120 

121 # ISR metadata 

122 self.metadataContents = TaskMetadata() 

123 self.metadataContents["isr"] = {} 

124 # Overscan readout noise [in ADU] 

125 for amp in self.ampNames: 

126 self.metadataContents["isr"][f"RESIDUAL STDEV {amp}"] = np.sqrt(self.noiseSq)/self.gain 

127 

128 def test_covAstier(self): 

129 """Test to check getCovariancesAstier 

130 

131 We check that the gain is the same as the imput gain from the 

132 mock data, that the covariances via FFT (as it is in 

133 MeasurePhotonTransferCurveTask when doCovariancesAstier=True) 

134 are the same as calculated in real space, and that Cov[0, 0] 

135 (i.e., the variances) are similar to the variances calculated 

136 with the standard method (when doCovariancesAstier=false), 

137 

138 """ 

139 extractConfig = self.defaultConfigExtract 

140 extractConfig.minNumberGoodPixelsForCovariance = 5000 

141 extractConfig.detectorMeasurementRegion = 'FULL' 

142 # Cut off the low-flux point which is a bad fit, and this 

143 # also exercises this functionality and makes the tests 

144 # run a lot faster. 

145 extractConfig.minMeanSignal["ALL_AMPS"] = 2000.0 

146 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig) 

147 

148 solveConfig = self.defaultConfigSolve 

149 solveConfig.ptcFitType = 'FULLCOVARIANCE' 

150 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=solveConfig) 

151 

152 inputGain = self.gain 

153 

154 muStandard, varStandard = {}, {} 

155 expDict = {} 

156 expIds = [] 

157 idCounter = 0 

158 for expTime in self.timeVec: 

159 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain, 

160 readNoiseElectrons=3, 

161 expId1=idCounter, expId2=idCounter+1) 

162 mockExpRef1 = PretendRef(mockExp1) 

163 mockExpRef2 = PretendRef(mockExp2) 

164 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1)) 

165 expIds.append(idCounter) 

166 expIds.append(idCounter+1) 

167 for ampNumber, ampName in enumerate(self.ampNames): 

168 # cov has (i, j, var, cov, npix) 

169 im1Area, im2Area, imStatsCtrl, mu1, mu2 = extractTask.getImageAreasMasksStats(mockExp1, 

170 mockExp2) 

171 muDiff, varDiff, covAstier = extractTask.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, 

172 mu1, mu2) 

173 muStandard.setdefault(ampName, []).append(muDiff) 

174 varStandard.setdefault(ampName, []).append(varDiff) 

175 idCounter += 2 

176 

177 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds, 

178 taskMetadata=[self.metadataContents for x in expIds]) 

179 

180 # Force the last PTC dataset to have a NaN, and ensure that the 

181 # task runs (DM-38029). This is a minor perturbation and does not 

182 # affect the output comparison. 

183 resultsExtract.outputCovariances[-2].rawMeans['C:0,0'] = np.array([np.nan]) 

184 resultsExtract.outputCovariances[-2].rawVars['C:0,0'] = np.array([np.nan]) 

185 

186 resultsSolve = solveTask.run(resultsExtract.outputCovariances, 

187 camera=FakeCamera([self.flatExp1.getDetector()])) 

188 

189 ptc = resultsSolve.outputPtcDataset 

190 

191 for amp in self.ampNames: 

192 self.assertAlmostEqual(ptc.gain[amp], inputGain, places=2) 

193 for v1, v2 in zip(varStandard[amp], ptc.finalVars[amp]): 

194 self.assertAlmostEqual(v1/v2, 1.0, places=1) 

195 

196 mask = ptc.getGoodPoints(amp) 

197 

198 values = ((ptc.covariancesModel[amp][mask, 0, 0] - ptc.covariances[amp][mask, 0, 0]) 

199 / ptc.covariancesModel[amp][mask, 0, 0]) 

200 np.testing.assert_array_less(np.abs(values), 2e-3) 

201 

202 values = ((ptc.covariancesModel[amp][mask, 1, 1] - ptc.covariances[amp][mask, 1, 1]) 

203 / ptc.covariancesModel[amp][mask, 1, 1]) 

204 np.testing.assert_array_less(np.abs(values), 0.2) 

205 

206 values = ((ptc.covariancesModel[amp][mask, 1, 2] - ptc.covariances[amp][mask, 1, 2]) 

207 / ptc.covariancesModel[amp][mask, 1, 2]) 

208 np.testing.assert_array_less(np.abs(values), 0.2) 

209 

210 expIdsUsed = ptc.getExpIdsUsed("C:0,0") 

211 # Check that these are the same as the inputs, paired up, with the 

212 # first two (low flux) and final two (nans) removed. 

213 self.assertTrue(np.all(expIdsUsed == np.array(expIds).reshape(len(expIds) // 2, 2)[1:-1])) 

214 

215 goodAmps = ptc.getGoodAmps() 

216 self.assertEqual(goodAmps, self.ampNames) 

217 

218 # Check that every possibly modified field has the same length. 

219 covShape = None 

220 covSqrtShape = None 

221 covModelShape = None 

222 covModelNoBShape = None 

223 

224 for ampName in self.ampNames: 

225 if covShape is None: 

226 covShape = ptc.covariances[ampName].shape 

227 covSqrtShape = ptc.covariancesSqrtWeights[ampName].shape 

228 covModelShape = ptc.covariancesModel[ampName].shape 

229 covModelNoBShape = ptc.covariancesModelNoB[ampName].shape 

230 else: 

231 self.assertEqual(ptc.covariances[ampName].shape, covShape) 

232 self.assertEqual(ptc.covariancesSqrtWeights[ampName].shape, covSqrtShape) 

233 self.assertEqual(ptc.covariancesModel[ampName].shape, covModelShape) 

234 self.assertEqual(ptc.covariancesModelNoB[ampName].shape, covModelNoBShape) 

235 

236 # And check that this is serializable 

237 with tempfile.NamedTemporaryFile(suffix=".fits") as f: 

238 usedFilename = ptc.writeFits(f.name) 

239 fromFits = PhotonTransferCurveDataset.readFits(usedFilename) 

240 self.assertEqual(fromFits, ptc) 

241 

242 def ptcFitAndCheckPtc( 

243 self, 

244 order=None, 

245 fitType=None, 

246 doTableArray=False, 

247 doFitBootstrap=False, 

248 doLegacy=False, 

249 ): 

250 localDataset = copy.deepcopy(self.dataset) 

251 localDataset.ptcFitType = fitType 

252 configSolve = copy.copy(self.defaultConfigSolve) 

253 configLin = cpPipe.linearity.LinearitySolveTask.ConfigClass() 

254 placesTests = 6 

255 if doFitBootstrap: 

256 configSolve.doFitBootstrap = True 

257 # Bootstrap method in cp_pipe/utils.py does multiple fits 

258 # in the precense of noise. Allow for more margin of 

259 # error. 

260 placesTests = 3 

261 

262 configSolve.doLegacyTurnoffAndOutlierSelection = doLegacy 

263 

264 if fitType == 'POLYNOMIAL': 

265 if order not in [2, 3]: 

266 RuntimeError("Enter a valid polynomial order for this test: 2 or 3") 

267 if order == 2: 

268 for ampName in self.ampNames: 

269 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for 

270 mu in localDataset.rawMeans[ampName]] 

271 configSolve.polynomialFitDegree = 2 

272 if order == 3: 

273 for ampName in self.ampNames: 

274 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3 

275 for mu in localDataset.rawMeans[ampName]] 

276 configSolve.polynomialFitDegree = 3 

277 elif fitType == 'EXPAPPROXIMATION': 

278 g = self.gain 

279 for ampName in self.ampNames: 

280 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1) 

281 + self.noiseSq/(g*g)) 

282 for mu in localDataset.rawMeans[ampName]] 

283 else: 

284 raise RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'") 

285 

286 # Initialize mask and covariance weights that will be used in fits. 

287 # Covariance weights values empirically determined from one of 

288 # the cases in test_covAstier. 

289 matrixSize = localDataset.covMatrixSide 

290 maskLength = len(localDataset.rawMeans[ampName]) 

291 for ampName in self.ampNames: 

292 localDataset.expIdMask[ampName] = np.repeat(True, maskLength) 

293 localDataset.covariancesSqrtWeights[ampName] = np.repeat(np.ones((matrixSize, matrixSize)), 

294 maskLength).reshape((maskLength, 

295 matrixSize, 

296 matrixSize)) 

297 localDataset.covariancesSqrtWeights[ampName][:, 0, 0] = [0.07980188, 0.01339653, 0.0073118, 

298 0.00502802, 0.00383132, 0.00309475, 

299 0.00259572, 0.00223528, 0.00196273, 

300 0.00174943, 0.00157794, 0.00143707, 

301 0.00131929, 0.00121935, 0.0011334, 

302 0.00105893, 0.00099357, 0.0009358, 

303 0.00088439, 0.00083833] 

304 

305 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats 

306 configLin.maxLinearAdu = 100000 

307 configLin.minLinearAdu = 50000 

308 if doTableArray: 

309 configLin.linearityType = "LookupTable" 

310 else: 

311 configLin.linearityType = "Polynomial" 

312 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve) 

313 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin) 

314 

315 if doTableArray: 

316 # Non-linearity 

317 numberAmps = len(self.ampNames) 

318 # localDataset: PTC dataset 

319 # (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`) 

320 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

321 # linDataset here is a lsst.pipe.base.Struct 

322 linDataset = linearityTask.run(localDataset, 

323 dummy=[1.0], 

324 camera=FakeCamera([self.flatExp1.getDetector()]), 

325 inputPhotodiodeData={}, 

326 inputDims={'detector': 0}) 

327 linDataset = linDataset.outputLinearizer 

328 else: 

329 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

330 linDataset = linearityTask.run(localDataset, 

331 dummy=[1.0], 

332 camera=FakeCamera([self.flatExp1.getDetector()]), 

333 inputPhotodiodeData={}, 

334 inputDims={'detector': 0}) 

335 linDataset = linDataset.outputLinearizer 

336 if doTableArray: 

337 # check that the linearizer table has been filled out properly 

338 for i in np.arange(numberAmps): 

339 tMax = (configLin.maxLookupTableAdu)/self.flux 

340 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu) 

341 signalIdeal = timeRange*self.flux 

342 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]), 

343 timeRange) 

344 linearizerTableRow = signalIdeal - signalUncorrected 

345 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :])) 

346 for j in np.arange(len(linearizerTableRow)): 

347 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j], 

348 places=placesTests) 

349 else: 

350 # check entries in localDataset, which was modified by the function 

351 for ampName in self.ampNames: 

352 maskAmp = localDataset.expIdMask[ampName] 

353 finalMuVec = localDataset.rawMeans[ampName][maskAmp] 

354 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp] 

355 linearPart = self.flux*finalTimeVec 

356 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart 

357 self.assertEqual(fitType, localDataset.ptcFitType) 

358 self.assertAlmostEqual(self.gain, localDataset.gain[ampName]) 

359 if fitType == 'POLYNOMIAL': 

360 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1]) 

361 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName]) 

362 if fitType == 'EXPAPPROXIMATION': 

363 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0]) 

364 # noise already in electrons for 'EXPAPPROXIMATION' fit 

365 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName]) 

366 

367 # check entries in returned dataset (a dict of , for nonlinearity) 

368 for ampName in self.ampNames: 

369 maskAmp = localDataset.expIdMask[ampName] 

370 finalMuVec = localDataset.rawMeans[ampName][maskAmp] 

371 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp] 

372 linearPart = self.flux*finalTimeVec 

373 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart 

374 

375 # Nonlinearity fit parameters 

376 # Polynomial fits are now normalized to unit flux scaling 

377 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1) 

378 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1], 

379 places=5) 

380 

381 # Non-linearity coefficient for linearizer 

382 squaredCoeff = self.k2NonLinearity/(self.flux**2) 

383 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2], 

384 places=placesTests) 

385 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2], 

386 places=placesTests) 

387 

388 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux 

389 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel 

390 # Fractional nonlinearity residuals 

391 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals)) 

392 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals): 

393 self.assertAlmostEqual(calc, truth, places=3) 

394 

395 def test_ptcFit(self): 

396 for createArray in [True, False]: 

397 for doLegacy in [False, True]: 

398 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]: 

399 self.ptcFitAndCheckPtc( 

400 fitType=fitType, 

401 order=order, 

402 doTableArray=createArray, 

403 doLegacy=doLegacy, 

404 ) 

405 

406 def test_meanVarMeasurement(self): 

407 task = self.defaultTaskExtract 

408 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1, 

409 self.flatExp2) 

410 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2) 

411 

412 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1) 

413 self.assertLess(self.flatMean - mu, 1) 

414 

415 def test_meanVarMeasurementWithNans(self): 

416 task = self.defaultTaskExtract 

417 

418 flatExp1 = self.flatExp1.clone() 

419 flatExp2 = self.flatExp2.clone() 

420 

421 flatExp1.image.array[20:30, :] = np.nan 

422 flatExp2.image.array[20:30, :] = np.nan 

423 

424 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(flatExp1, 

425 flatExp2) 

426 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2) 

427 

428 expectedMu1 = np.nanmean(flatExp1.image.array) 

429 expectedMu2 = np.nanmean(flatExp2.image.array) 

430 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

431 

432 # Now the variance of the difference. First, create the diff image. 

433 im1 = flatExp1.maskedImage 

434 im2 = flatExp2.maskedImage 

435 

436 temp = im2.clone() 

437 temp *= expectedMu1 

438 diffIm = im1.clone() 

439 diffIm *= expectedMu2 

440 diffIm -= temp 

441 diffIm /= expectedMu 

442 

443 # Divide by two as it is what measureMeanVarCov returns 

444 # (variance of difference) 

445 expectedVar = 0.5*np.nanvar(diffIm.image.array) 

446 

447 # Check that the standard deviations and the emans agree to 

448 # less than 1 ADU 

449 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1) 

450 self.assertLess(expectedMu - mu, 1) 

451 

452 def test_meanVarMeasurementAllNan(self): 

453 task = self.defaultTaskExtract 

454 flatExp1 = self.flatExp1.clone() 

455 flatExp2 = self.flatExp2.clone() 

456 

457 flatExp1.image.array[:, :] = np.nan 

458 flatExp2.image.array[:, :] = np.nan 

459 

460 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(flatExp1, 

461 flatExp2) 

462 mu, varDiff, covDiff = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2) 

463 

464 self.assertTrue(np.isnan(mu)) 

465 self.assertTrue(np.isnan(varDiff)) 

466 self.assertTrue(covDiff is None) 

467 

468 def test_meanVarMeasurementTooFewPixels(self): 

469 task = self.defaultTaskExtract 

470 flatExp1 = self.flatExp1.clone() 

471 flatExp2 = self.flatExp2.clone() 

472 

473 flatExp1.image.array[0: 190, :] = np.nan 

474 flatExp2.image.array[0: 190, :] = np.nan 

475 

476 bit = flatExp1.mask.getMaskPlaneDict()["NO_DATA"] 

477 flatExp1.mask.array[0: 190, :] &= 2**bit 

478 flatExp2.mask.array[0: 190, :] &= 2**bit 

479 

480 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(flatExp1, 

481 flatExp2) 

482 with self.assertLogs(level=logging.WARNING) as cm: 

483 mu, varDiff, covDiff = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2) 

484 self.assertIn("Number of good points", cm.output[0]) 

485 

486 self.assertTrue(np.isnan(mu)) 

487 self.assertTrue(np.isnan(varDiff)) 

488 self.assertTrue(covDiff is None) 

489 

490 def test_meanVarMeasurementTooNarrowStrip(self): 

491 # We need a new config to make sure the second covariance cut is 

492 # triggered. 

493 config = cpPipe.ptc.PhotonTransferCurveExtractTask.ConfigClass() 

494 config.minNumberGoodPixelsForCovariance = 10 

495 task = cpPipe.ptc.PhotonTransferCurveExtractTask(config=config) 

496 flatExp1 = self.flatExp1.clone() 

497 flatExp2 = self.flatExp2.clone() 

498 

499 flatExp1.image.array[0: 195, :] = np.nan 

500 flatExp2.image.array[0: 195, :] = np.nan 

501 flatExp1.image.array[:, 0: 195] = np.nan 

502 flatExp2.image.array[:, 0: 195] = np.nan 

503 

504 bit = flatExp1.mask.getMaskPlaneDict()["NO_DATA"] 

505 flatExp1.mask.array[0: 195, :] &= 2**bit 

506 flatExp2.mask.array[0: 195, :] &= 2**bit 

507 flatExp1.mask.array[:, 0: 195] &= 2**bit 

508 flatExp2.mask.array[:, 0: 195] &= 2**bit 

509 

510 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(flatExp1, 

511 flatExp2) 

512 with self.assertLogs(level=logging.WARNING) as cm: 

513 mu, varDiff, covDiff = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2) 

514 self.assertIn("Not enough pixels", cm.output[0]) 

515 

516 self.assertTrue(np.isnan(mu)) 

517 self.assertTrue(np.isnan(varDiff)) 

518 self.assertTrue(covDiff is None) 

519 

520 def test_makeZeroSafe(self): 

521 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8] 

522 someZerosArray = [1., 20, 0, 0, 90, 879, 0] 

523 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0] 

524 

525 substituteValue = 1e-10 

526 

527 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue] 

528 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray)) 

529 

530 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

531 substituteValue=substituteValue) 

532 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

533 substituteValue=substituteValue) 

534 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

535 substituteValue=substituteValue) 

536 

537 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray): 

538 self.assertEqual(exp, meas) 

539 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray): 

540 self.assertEqual(exp, meas) 

541 for exp, meas in zip(noZerosArray, measuredNoZerosArray): 

542 self.assertEqual(exp, meas) 

543 

544 def test_getInitialGoodPoints(self): 

545 xs = [1, 2, 3, 4, 5, 6] 

546 ys = [2*x for x in xs] 

547 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0., 

548 consecutivePointsVarDecreases=2) 

549 assert np.all(points) == np.all(np.array([True for x in xs])) 

550 

551 ys[4] = 7 # Variance decreases in two consecutive points after ys[3]=8 

552 ys[5] = 6 

553 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0., 

554 consecutivePointsVarDecreases=2) 

555 assert np.all(points) == np.all(np.array([True, True, True, True, False])) 

556 

557 def runGetGainFromFlatPair(self, correctionType='NONE'): 

558 extractConfig = self.defaultConfigExtract 

559 extractConfig.gainCorrectionType = correctionType 

560 extractConfig.minNumberGoodPixelsForCovariance = 5000 

561 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig) 

562 

563 expDict = {} 

564 expIds = [] 

565 idCounter = 0 

566 inputGain = self.gain # 1.5 e/ADU 

567 for expTime in self.timeVec: 

568 # Approximation works better at low flux, e.g., < 10000 ADU 

569 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain, 

570 readNoiseElectrons=np.sqrt(self.noiseSq), 

571 fluxElectrons=100, 

572 expId1=idCounter, expId2=idCounter+1) 

573 mockExpRef1 = PretendRef(mockExp1) 

574 mockExpRef2 = PretendRef(mockExp2) 

575 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1)) 

576 expIds.append(idCounter) 

577 expIds.append(idCounter+1) 

578 idCounter += 2 

579 

580 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds, 

581 taskMetadata=[self.metadataContents for x in expIds]) 

582 for exposurePair in resultsExtract.outputCovariances: 

583 for ampName in self.ampNames: 

584 if exposurePair.gain[ampName] is np.nan: 

585 continue 

586 self.assertAlmostEqual(exposurePair.gain[ampName], inputGain, delta=0.04) 

587 

588 def test_getGainFromFlatPair(self): 

589 for gainCorrectionType in ['NONE', 'SIMPLE', 'FULL', ]: 

590 self.runGetGainFromFlatPair(gainCorrectionType) 

591 

592 

593class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase): 

594 def setUp(self): 

595 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ") 

596 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)], 

597 'C01': [(123, 234), (345, 456), (567, 678)]} 

598 

599 def test_generalBehaviour(self): 

600 test = PhotonTransferCurveDataset(['C00', 'C01'], " ") 

601 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)], 

602 'C01': [(123, 234), (345, 456), (567, 678)]} 

603 

604 

605class TestMemory(lsst.utils.tests.MemoryTestCase): 

606 pass 

607 

608 

609def setup_module(module): 

610 lsst.utils.tests.init() 

611 

612 

613if __name__ == "__main__": 613 ↛ 614line 613 didn't jump to line 614, because the condition on line 613 was never true

614 lsst.utils.tests.init() 

615 unittest.main()