Coverage for tests/test_ptc.py: 12%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

293 statements  

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, expId1=idCounter, 

151 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 muDiff, varDiff, covAstier = extractTask.measureMeanVarCov(mockExp1, mockExp2) 

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

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

162 idCounter += 2 

163 

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

165 taskMetadata=[self.metadataContents]) 

166 resultsSolve = solveTask.run(resultsExtract.outputCovariances) 

167 

168 for amp in self.ampNames: 

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

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

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

172 

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

174 localDataset = copy.copy(self.dataset) 

175 localDataset.ptcFitType = fitType 

176 configSolve = copy.copy(self.defaultConfigSolve) 

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

178 placesTests = 6 

179 if doFitBootstrap: 

180 configSolve.doFitBootstrap = True 

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

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

183 # error. 

184 placesTests = 3 

185 

186 if fitType == 'POLYNOMIAL': 

187 if order not in [2, 3]: 

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

189 if order == 2: 

190 for ampName in self.ampNames: 

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

192 mu in localDataset.rawMeans[ampName]] 

193 configSolve.polynomialFitDegree = 2 

194 if order == 3: 

195 for ampName in self.ampNames: 

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

197 for mu in localDataset.rawMeans[ampName]] 

198 configSolve.polynomialFitDegree = 3 

199 elif fitType == 'EXPAPPROXIMATION': 

200 g = self.gain 

201 for ampName in self.ampNames: 

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

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

204 for mu in localDataset.rawMeans[ampName]] 

205 else: 

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

207 

208 for ampName in self.ampNames: 

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

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

211 configLin.maxLinearAdu = 100000 

212 configLin.minLinearAdu = 50000 

213 if doTableArray: 

214 configLin.linearityType = "LookupTable" 

215 else: 

216 configLin.linearityType = "Polynomial" 

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

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

219 

220 if doTableArray: 

221 # Non-linearity 

222 numberAmps = len(self.ampNames) 

223 # localDataset: PTC dataset 

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

225 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

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

227 linDataset = linearityTask.run(localDataset, 

228 dummy=[1.0], 

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

230 inputDims={'detector': 0}) 

231 linDataset = linDataset.outputLinearizer 

232 else: 

233 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

234 linDataset = linearityTask.run(localDataset, 

235 dummy=[1.0], 

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

237 inputDims={'detector': 0}) 

238 linDataset = linDataset.outputLinearizer 

239 if doTableArray: 

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

241 for i in np.arange(numberAmps): 

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

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

244 signalIdeal = timeRange*self.flux 

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

246 timeRange) 

247 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

251 places=placesTests) 

252 else: 

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

254 for ampName in self.ampNames: 

255 maskAmp = localDataset.expIdMask[ampName] 

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

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

258 linearPart = self.flux*finalTimeVec 

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

260 self.assertEqual(fitType, localDataset.ptcFitType) 

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

262 if fitType == 'POLYNOMIAL': 

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

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

265 if fitType == 'EXPAPPROXIMATION': 

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

267 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

269 

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

271 for ampName in self.ampNames: 

272 maskAmp = localDataset.expIdMask[ampName] 

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

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

275 linearPart = self.flux*finalTimeVec 

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

277 

278 # Nonlinearity fit parameters 

279 # Polynomial fits are now normalized to unit flux scaling 

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

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

282 places=5) 

283 

284 # Non-linearity coefficient for linearizer 

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

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

287 places=placesTests) 

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

289 places=placesTests) 

290 

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

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

293 # Fractional nonlinearity residuals 

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

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

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

297 

298 def test_ptcFit(self): 

299 for createArray in [True, False]: 

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

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

302 

303 def test_meanVarMeasurement(self): 

304 task = self.defaultTaskExtract 

305 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2) 

306 

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

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

309 

310 def test_meanVarMeasurementWithNans(self): 

311 task = self.defaultTaskExtract 

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

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

314 

315 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2) 

316 

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

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

319 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

320 

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

322 im1 = self.flatExp1.maskedImage 

323 im2 = self.flatExp2.maskedImage 

324 

325 temp = im2.clone() 

326 temp *= expectedMu1 

327 diffIm = im1.clone() 

328 diffIm *= expectedMu2 

329 diffIm -= temp 

330 diffIm /= expectedMu 

331 

332 # Divide by two as it is what measureMeanVarCov returns 

333 # (variance of difference) 

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

335 

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

337 # less than 1 ADU 

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

339 self.assertLess(expectedMu - mu, 1) 

340 

341 def test_meanVarMeasurementAllNan(self): 

342 task = self.defaultTaskExtract 

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

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

345 

346 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2) 

347 

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

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

350 self.assertTrue(covDiff is None) 

351 

352 def test_makeZeroSafe(self): 

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

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

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

356 

357 substituteValue = 1e-10 

358 

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

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

361 

362 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

363 substituteValue=substituteValue) 

364 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

365 substituteValue=substituteValue) 

366 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

367 substituteValue=substituteValue) 

368 

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

370 self.assertEqual(exp, meas) 

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

372 self.assertEqual(exp, meas) 

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

374 self.assertEqual(exp, meas) 

375 

376 def test_getInitialGoodPoints(self): 

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

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

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

380 consecutivePointsVarDecreases=2) 

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

382 

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

384 ys[5] = 6 

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

386 consecutivePointsVarDecreases=2) 

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

388 

389 def test_getExpIdsUsed(self): 

390 localDataset = copy.copy(self.dataset) 

391 

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

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

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

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

396 

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

398 with self.assertRaises(AssertionError): 

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

400 

401 def test_getGoodAmps(self): 

402 dataset = self.dataset 

403 

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

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

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

407 

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

409 extractConfig = self.defaultConfigExtract 

410 extractConfig.gainCorrectionType = correctionType 

411 extractConfig.minNumberGoodPixelsForCovariance = 5000 

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

413 

414 expDict = {} 

415 expIds = [] 

416 idCounter = 0 

417 inputGain = self.gain # 1.5 e/ADU 

418 for expTime in self.timeVec: 

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

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

421 expId1=idCounter, expId2=idCounter+1) 

422 mockExpRef1 = PretendRef(mockExp1) 

423 mockExpRef2 = PretendRef(mockExp2) 

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

425 expIds.append(idCounter) 

426 expIds.append(idCounter+1) 

427 idCounter += 2 

428 

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

430 taskMetadata=[self.metadataContents]) 

431 

432 for exposurePair in resultsExtract.outputCovariances: 

433 for ampName in self.ampNames: 

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

435 continue 

436 self.assertAlmostEqual(exposurePair.gain[ampName], inputGain, delta=0.075) 

437 

438 def test_getGainFromFlatPair(self): 

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

440 self.runGetGainFromFlatPair(gainCorrectionType) 

441 

442 

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

444 def setUp(self): 

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

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

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

448 

449 def test_generalBehaviour(self): 

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

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

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

453 

454 

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

456 pass 

457 

458 

459def setup_module(module): 

460 lsst.utils.tests.init() 

461 

462 

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

464 lsst.utils.tests.init() 

465 unittest.main()