Coverage for tests/test_ptc.py: 12%

297 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-13 03:52 -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 

27from __future__ import absolute_import, division, print_function 

28import unittest 

29import numpy as np 

30import copy 

31 

32import lsst.utils 

33import lsst.utils.tests 

34 

35import lsst.cp.pipe as cpPipe 

36import lsst.ip.isr.isrMock as isrMock 

37from lsst.ip.isr import PhotonTransferCurveDataset 

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

39 

40from lsst.pipe.base import TaskMetadata 

41 

42 

43class FakeCamera(list): 

44 def getName(self): 

45 return "FakeCam" 

46 

47 

48class PretendRef(): 

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

50 def __init__(self, exposure): 

51 self.exp = exposure 

52 

53 def get(self, component=None): 

54 if component == 'visitInfo': 

55 return self.exp.getVisitInfo() 

56 elif component == 'detector': 

57 return self.exp.getDetector() 

58 else: 

59 return self.exp 

60 

61 

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

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

64 

65 def setUp(self): 

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

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

68 

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

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

71 

72 self.flatMean = 2000 

73 self.readNoiseAdu = 10 

74 mockImageConfig = isrMock.IsrMock.ConfigClass() 

75 

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

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

78 mockImageConfig.flatDrop = 0.99999 

79 mockImageConfig.isTrimmed = True 

80 

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

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

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

84 

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

86 

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

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

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

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

91 

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

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

94 

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

96 self.flux = 1000. # ADU/sec 

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

98 self.k2NonLinearity = -5e-6 

99 # quadratic signal-chain non-linearity 

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

101 self.gain = 1.5 # e-/ADU 

102 self.c1 = 1./self.gain 

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

104 self.a00 = -1.2e-6 

105 self.c2 = -1.5e-6 

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

107 

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

109 self.dataset = PhotonTransferCurveDataset(self.ampNames, " ") # pack raw data for fitting 

110 

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

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

113 self.dataset.rawMeans[ampName] = muVec 

114 

115 # ISR metadata 

116 self.metadataContents = TaskMetadata() 

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

118 # Overscan readout noise [in ADU] 

119 for amp in self.ampNames: 

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

121 

122 def test_covAstier(self): 

123 """Test to check getCovariancesAstier 

124 

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

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

127 MeasurePhotonTransferCurveTask when doCovariancesAstier=True) 

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

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

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

131 

132 """ 

133 extractConfig = self.defaultConfigExtract 

134 extractConfig.minNumberGoodPixelsForCovariance = 5000 

135 extractConfig.detectorMeasurementRegion = 'FULL' 

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

137 

138 solveConfig = self.defaultConfigSolve 

139 solveConfig.ptcFitType = 'FULLCOVARIANCE' 

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

141 

142 inputGain = 0.75 

143 

144 muStandard, varStandard = {}, {} 

145 expDict = {} 

146 expIds = [] 

147 idCounter = 0 

148 for expTime in self.timeVec: 

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

150 readNoiseElectrons=3, 

151 expId1=idCounter, expId2=idCounter+1) 

152 mockExpRef1 = PretendRef(mockExp1) 

153 mockExpRef2 = PretendRef(mockExp2) 

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

155 expIds.append(idCounter) 

156 expIds.append(idCounter+1) 

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

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

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

160 mockExp2) 

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

162 mu1, mu2) 

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

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

165 idCounter += 2 

166 

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

168 taskMetadata=[self.metadataContents]) 

169 resultsSolve = solveTask.run(resultsExtract.outputCovariances) 

170 

171 for amp in self.ampNames: 

172 self.assertAlmostEqual(resultsSolve.outputPtcDataset.gain[amp], inputGain, places=2) 

173 for v1, v2 in zip(varStandard[amp], resultsSolve.outputPtcDataset.finalVars[amp]): 

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

175 

176 def ptcFitAndCheckPtc(self, order=None, fitType=None, doTableArray=False, doFitBootstrap=False): 

177 localDataset = copy.copy(self.dataset) 

178 localDataset.ptcFitType = fitType 

179 configSolve = copy.copy(self.defaultConfigSolve) 

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

181 placesTests = 6 

182 if doFitBootstrap: 

183 configSolve.doFitBootstrap = True 

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

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

186 # error. 

187 placesTests = 3 

188 

189 if fitType == 'POLYNOMIAL': 

190 if order not in [2, 3]: 

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

192 if order == 2: 

193 for ampName in self.ampNames: 

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

195 mu in localDataset.rawMeans[ampName]] 

196 configSolve.polynomialFitDegree = 2 

197 if order == 3: 

198 for ampName in self.ampNames: 

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

200 for mu in localDataset.rawMeans[ampName]] 

201 configSolve.polynomialFitDegree = 3 

202 elif fitType == 'EXPAPPROXIMATION': 

203 g = self.gain 

204 for ampName in self.ampNames: 

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

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

207 for mu in localDataset.rawMeans[ampName]] 

208 else: 

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

210 

211 for ampName in self.ampNames: 

212 localDataset.expIdMask[ampName] = np.repeat(True, len(localDataset.rawMeans[ampName])) 

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

214 configLin.maxLinearAdu = 100000 

215 configLin.minLinearAdu = 50000 

216 if doTableArray: 

217 configLin.linearityType = "LookupTable" 

218 else: 

219 configLin.linearityType = "Polynomial" 

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

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

222 

223 if doTableArray: 

224 # Non-linearity 

225 numberAmps = len(self.ampNames) 

226 # localDataset: PTC dataset 

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

228 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

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

230 linDataset = linearityTask.run(localDataset, 

231 dummy=[1.0], 

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

233 inputDims={'detector': 0}) 

234 linDataset = linDataset.outputLinearizer 

235 else: 

236 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

237 linDataset = linearityTask.run(localDataset, 

238 dummy=[1.0], 

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

240 inputDims={'detector': 0}) 

241 linDataset = linDataset.outputLinearizer 

242 if doTableArray: 

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

244 for i in np.arange(numberAmps): 

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

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

247 signalIdeal = timeRange*self.flux 

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

249 timeRange) 

250 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

254 places=placesTests) 

255 else: 

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

257 for ampName in self.ampNames: 

258 maskAmp = localDataset.expIdMask[ampName] 

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

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

261 linearPart = self.flux*finalTimeVec 

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

263 self.assertEqual(fitType, localDataset.ptcFitType) 

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

265 if fitType == 'POLYNOMIAL': 

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

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

268 if fitType == 'EXPAPPROXIMATION': 

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

270 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

272 

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

274 for ampName in self.ampNames: 

275 maskAmp = localDataset.expIdMask[ampName] 

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

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

278 linearPart = self.flux*finalTimeVec 

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

280 

281 # Nonlinearity fit parameters 

282 # Polynomial fits are now normalized to unit flux scaling 

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

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

285 places=5) 

286 

287 # Non-linearity coefficient for linearizer 

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

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

290 places=placesTests) 

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

292 places=placesTests) 

293 

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

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

296 # Fractional nonlinearity residuals 

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

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

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

300 

301 def test_ptcFit(self): 

302 for createArray in [True, False]: 

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

304 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray) 

305 

306 def test_meanVarMeasurement(self): 

307 task = self.defaultTaskExtract 

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

309 self.flatExp2) 

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

311 

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

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

314 

315 def test_meanVarMeasurementWithNans(self): 

316 task = self.defaultTaskExtract 

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

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

319 

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

321 self.flatExp2) 

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

323 

324 expectedMu1 = np.nanmean(self.flatExp1.image.array) 

325 expectedMu2 = np.nanmean(self.flatExp2.image.array) 

326 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

327 

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

329 im1 = self.flatExp1.maskedImage 

330 im2 = self.flatExp2.maskedImage 

331 

332 temp = im2.clone() 

333 temp *= expectedMu1 

334 diffIm = im1.clone() 

335 diffIm *= expectedMu2 

336 diffIm -= temp 

337 diffIm /= expectedMu 

338 

339 # Divide by two as it is what measureMeanVarCov returns 

340 # (variance of difference) 

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

342 

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

344 # less than 1 ADU 

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

346 self.assertLess(expectedMu - mu, 1) 

347 

348 def test_meanVarMeasurementAllNan(self): 

349 task = self.defaultTaskExtract 

350 self.flatExp1.image.array[:, :] = np.nan 

351 self.flatExp2.image.array[:, :] = np.nan 

352 

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

354 self.flatExp2) 

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

356 

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

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

359 self.assertTrue(covDiff is None) 

360 

361 def test_makeZeroSafe(self): 

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

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

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

365 

366 substituteValue = 1e-10 

367 

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

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

370 

371 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

372 substituteValue=substituteValue) 

373 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

374 substituteValue=substituteValue) 

375 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

376 substituteValue=substituteValue) 

377 

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

379 self.assertEqual(exp, meas) 

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

381 self.assertEqual(exp, meas) 

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

383 self.assertEqual(exp, meas) 

384 

385 def test_getInitialGoodPoints(self): 

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

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

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

389 consecutivePointsVarDecreases=2) 

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

391 

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

393 ys[5] = 6 

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

395 consecutivePointsVarDecreases=2) 

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

397 

398 def test_getExpIdsUsed(self): 

399 localDataset = copy.copy(self.dataset) 

400 

401 for pair in [(12, 34), (56, 78), (90, 10)]: 

402 localDataset.inputExpIdPairs["C:0,0"].append(pair) 

403 localDataset.expIdMask["C:0,0"] = np.array([True, False, True]) 

404 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)])) 

405 

406 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now 

407 with self.assertRaises(AssertionError): 

408 localDataset.getExpIdsUsed("C:0,0") 

409 

410 def test_getGoodAmps(self): 

411 dataset = self.dataset 

412 

413 self.assertTrue(dataset.ampNames == self.ampNames) 

414 dataset.badAmps.append("C:0,1") 

415 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"]) 

416 

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

418 extractConfig = self.defaultConfigExtract 

419 extractConfig.gainCorrectionType = correctionType 

420 extractConfig.minNumberGoodPixelsForCovariance = 5000 

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

422 

423 expDict = {} 

424 expIds = [] 

425 idCounter = 0 

426 inputGain = self.gain # 1.5 e/ADU 

427 for expTime in self.timeVec: 

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

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

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

431 fluxElectrons=100, 

432 expId1=idCounter, expId2=idCounter+1) 

433 mockExpRef1 = PretendRef(mockExp1) 

434 mockExpRef2 = PretendRef(mockExp2) 

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

436 expIds.append(idCounter) 

437 expIds.append(idCounter+1) 

438 idCounter += 2 

439 

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

441 taskMetadata=[self.metadataContents]) 

442 

443 for exposurePair in resultsExtract.outputCovariances: 

444 for ampName in self.ampNames: 

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

446 continue 

447 self.assertAlmostEqual(exposurePair.gain[ampName], inputGain, delta=0.16) 

448 

449 def test_getGainFromFlatPair(self): 

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

451 self.runGetGainFromFlatPair(gainCorrectionType) 

452 

453 

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

455 def setUp(self): 

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

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

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

459 

460 def test_generalBehaviour(self): 

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

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

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

464 

465 

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

467 pass 

468 

469 

470def setup_module(module): 

471 lsst.utils.tests.init() 

472 

473 

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

475 lsst.utils.tests.init() 

476 unittest.main()