Coverage for tests/test_ptc.py: 13%

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

269 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 

40 

41class FakeCamera(list): 

42 def getName(self): 

43 return "FakeCam" 

44 

45 

46class PretendRef(): 

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

48 def __init__(self, exposure): 

49 self.exp = exposure 

50 

51 def get(self, component=None): 

52 if component == 'visitInfo': 

53 return self.exp.getVisitInfo() 

54 elif component == 'detector': 

55 return self.exp.getDetector() 

56 else: 

57 return self.exp 

58 

59 

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

61 """A test case for the PTC task.""" 

62 

63 def setUp(self): 

64 self.defaultConfig = cpPipe.ptc.MeasurePhotonTransferCurveTask.ConfigClass() 

65 self.defaultTask = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=self.defaultConfig) 

66 

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

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

69 

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

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

72 

73 self.flatMean = 2000 

74 self.readNoiseAdu = 10 

75 mockImageConfig = isrMock.IsrMock.ConfigClass() 

76 

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

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

79 mockImageConfig.flatDrop = 0.99999 

80 mockImageConfig.isTrimmed = True 

81 

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

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

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

85 

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

87 

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

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

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

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

92 

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

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

95 

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

97 self.flux = 1000. # ADU/sec 

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

99 self.k2NonLinearity = -5e-6 

100 # quadratic signal-chain non-linearity 

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

102 self.gain = 1.5 # e-/ADU 

103 self.c1 = 1./self.gain 

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

105 self.a00 = -1.2e-6 

106 self.c2 = -1.5e-6 

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

108 

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

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

111 

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

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

114 self.dataset.rawMeans[ampName] = muVec 

115 

116 def test_covAstier(self): 

117 """Test to check getCovariancesAstier 

118 

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

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

121 MeasurePhotonTransferCurveTask when doCovariancesAstier=True) 

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

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

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

125 

126 """ 

127 task = self.defaultTask 

128 extractConfig = self.defaultConfigExtract 

129 extractConfig.minNumberGoodPixelsForCovariance = 5000 

130 extractConfig.detectorMeasurementRegion = 'FULL' 

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

132 

133 solveConfig = self.defaultConfigSolve 

134 solveConfig.ptcFitType = 'FULLCOVARIANCE' 

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

136 

137 inputGain = 0.75 

138 

139 muStandard, varStandard = {}, {} 

140 expDict = {} 

141 expIds = [] 

142 idCounter = 0 

143 for expTime in self.timeVec: 

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

145 readNoiseElectrons=3, expId1=idCounter, 

146 expId2=idCounter+1) 

147 mockExpRef1 = PretendRef(mockExp1) 

148 mockExpRef2 = PretendRef(mockExp2) 

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

150 expIds.append(idCounter) 

151 expIds.append(idCounter+1) 

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

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

154 muDiff, varDiff, covAstier = task.extract.measureMeanVarCov(mockExp1, mockExp2) 

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

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

157 # Calculate covariances in an independent way: direct space 

158 _, _, covsDirect = task.extract.measureMeanVarCov(mockExp1, mockExp2, covAstierRealSpace=True) 

159 

160 # Test that the arrays "covs" (FFT) and "covDirect" 

161 # (direct space) are the same 

162 for row1, row2 in zip(covAstier, covsDirect): 

163 for a, b in zip(row1, row2): 

164 self.assertAlmostEqual(a, b) 

165 idCounter += 2 

166 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds) 

167 resultsSolve = solveTask.run(resultsExtract.outputCovariances) 

168 

169 for amp in self.ampNames: 

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

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

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

173 

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

175 localDataset = copy.copy(self.dataset) 

176 localDataset.ptcFitType = fitType 

177 configSolve = copy.copy(self.defaultConfigSolve) 

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

179 placesTests = 6 

180 if doFitBootstrap: 

181 configSolve.doFitBootstrap = True 

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

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

184 # error. 

185 placesTests = 3 

186 

187 if fitType == 'POLYNOMIAL': 

188 if order not in [2, 3]: 

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

190 if order == 2: 

191 for ampName in self.ampNames: 

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

193 mu in localDataset.rawMeans[ampName]] 

194 configSolve.polynomialFitDegree = 2 

195 if order == 3: 

196 for ampName in self.ampNames: 

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

198 for mu in localDataset.rawMeans[ampName]] 

199 configSolve.polynomialFitDegree = 3 

200 elif fitType == 'EXPAPPROXIMATION': 

201 g = self.gain 

202 for ampName in self.ampNames: 

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

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

205 for mu in localDataset.rawMeans[ampName]] 

206 else: 

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

208 

209 for ampName in self.ampNames: 

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

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

212 configLin.maxLinearAdu = 100000 

213 configLin.minLinearAdu = 50000 

214 if doTableArray: 

215 configLin.linearityType = "LookupTable" 

216 else: 

217 configLin.linearityType = "Polynomial" 

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

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

220 

221 if doTableArray: 

222 # Non-linearity 

223 numberAmps = len(self.ampNames) 

224 # localDataset: PTC dataset 

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

226 localDataset = solveTask.fitPtc(localDataset) 

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

228 linDataset = linearityTask.run(localDataset, 

229 dummy=[1.0], 

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

231 inputDims={'detector': 0}) 

232 linDataset = linDataset.outputLinearizer 

233 else: 

234 localDataset = solveTask.fitPtc(localDataset) 

235 linDataset = linearityTask.run(localDataset, 

236 dummy=[1.0], 

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

238 inputDims={'detector': 0}) 

239 linDataset = linDataset.outputLinearizer 

240 if doTableArray: 

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

242 for i in np.arange(numberAmps): 

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

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

245 signalIdeal = timeRange*self.flux 

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

247 timeRange) 

248 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

252 places=placesTests) 

253 else: 

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

255 for ampName in self.ampNames: 

256 maskAmp = localDataset.expIdMask[ampName] 

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

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

259 linearPart = self.flux*finalTimeVec 

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

261 self.assertEqual(fitType, localDataset.ptcFitType) 

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

263 if fitType == 'POLYNOMIAL': 

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

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

266 if fitType == 'EXPAPPROXIMATION': 

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

268 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

270 

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

272 for ampName in self.ampNames: 

273 maskAmp = localDataset.expIdMask[ampName] 

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

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

276 linearPart = self.flux*finalTimeVec 

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

278 

279 # Nonlinearity fit parameters 

280 # Polynomial fits are now normalized to unit flux scaling 

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

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

283 places=5) 

284 

285 # Non-linearity coefficient for linearizer 

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

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

288 places=placesTests) 

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

290 places=placesTests) 

291 

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

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

294 # Fractional nonlinearity residuals 

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

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

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

298 

299 def test_ptcFit(self): 

300 for createArray in [True, False]: 

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

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

303 

304 def test_meanVarMeasurement(self): 

305 task = self.defaultTaskExtract 

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

307 

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

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

310 

311 def test_meanVarMeasurementWithNans(self): 

312 task = self.defaultTaskExtract 

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

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

315 

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

317 

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

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

320 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

321 

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

323 im1 = self.flatExp1.maskedImage 

324 im2 = self.flatExp2.maskedImage 

325 

326 temp = im2.clone() 

327 temp *= expectedMu1 

328 diffIm = im1.clone() 

329 diffIm *= expectedMu2 

330 diffIm -= temp 

331 diffIm /= expectedMu 

332 

333 # Divide by two as it is what measureMeanVarCov returns 

334 # (variance of difference) 

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

336 

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

338 # less than 1 ADU 

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

340 self.assertLess(expectedMu - mu, 1) 

341 

342 def test_meanVarMeasurementAllNan(self): 

343 task = self.defaultTaskExtract 

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

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

346 

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

348 

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

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

351 self.assertTrue(covDiff is None) 

352 

353 def test_makeZeroSafe(self): 

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

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

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

357 

358 substituteValue = 1e-10 

359 

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

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

362 

363 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

364 substituteValue=substituteValue) 

365 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

366 substituteValue=substituteValue) 

367 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

368 substituteValue=substituteValue) 

369 

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

371 self.assertEqual(exp, meas) 

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

373 self.assertEqual(exp, meas) 

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

375 self.assertEqual(exp, meas) 

376 

377 def test_getInitialGoodPoints(self): 

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

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

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

381 consecutivePointsVarDecreases=2) 

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

383 

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

385 ys[5] = 6 

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

387 consecutivePointsVarDecreases=2) 

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

389 

390 def test_getExpIdsUsed(self): 

391 localDataset = copy.copy(self.dataset) 

392 

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

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

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

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

397 

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

399 with self.assertRaises(AssertionError): 

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

401 

402 def test_getGoodAmps(self): 

403 dataset = self.dataset 

404 

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

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

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

408 

409 

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

411 def setUp(self): 

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

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

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

415 

416 def test_generalBehaviour(self): 

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

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

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

420 

421 

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

423 pass 

424 

425 

426def setup_module(module): 

427 lsst.utils.tests.init() 

428 

429 

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

431 lsst.utils.tests.init() 

432 unittest.main()