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

265 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 idCounter += 2 

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

159 resultsSolve = solveTask.run(resultsExtract.outputCovariances) 

160 

161 for amp in self.ampNames: 

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

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

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

165 

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

167 localDataset = copy.copy(self.dataset) 

168 localDataset.ptcFitType = fitType 

169 configSolve = copy.copy(self.defaultConfigSolve) 

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

171 placesTests = 6 

172 if doFitBootstrap: 

173 configSolve.doFitBootstrap = True 

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

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

176 # error. 

177 placesTests = 3 

178 

179 if fitType == 'POLYNOMIAL': 

180 if order not in [2, 3]: 

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

182 if order == 2: 

183 for ampName in self.ampNames: 

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

185 mu in localDataset.rawMeans[ampName]] 

186 configSolve.polynomialFitDegree = 2 

187 if order == 3: 

188 for ampName in self.ampNames: 

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

190 for mu in localDataset.rawMeans[ampName]] 

191 configSolve.polynomialFitDegree = 3 

192 elif fitType == 'EXPAPPROXIMATION': 

193 g = self.gain 

194 for ampName in self.ampNames: 

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

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

197 for mu in localDataset.rawMeans[ampName]] 

198 else: 

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

200 

201 for ampName in self.ampNames: 

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

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

204 configLin.maxLinearAdu = 100000 

205 configLin.minLinearAdu = 50000 

206 if doTableArray: 

207 configLin.linearityType = "LookupTable" 

208 else: 

209 configLin.linearityType = "Polynomial" 

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

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

212 

213 if doTableArray: 

214 # Non-linearity 

215 numberAmps = len(self.ampNames) 

216 # localDataset: PTC dataset 

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

218 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

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

220 linDataset = linearityTask.run(localDataset, 

221 dummy=[1.0], 

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

223 inputDims={'detector': 0}) 

224 linDataset = linDataset.outputLinearizer 

225 else: 

226 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

227 linDataset = linearityTask.run(localDataset, 

228 dummy=[1.0], 

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

230 inputDims={'detector': 0}) 

231 linDataset = linDataset.outputLinearizer 

232 if doTableArray: 

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

234 for i in np.arange(numberAmps): 

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

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

237 signalIdeal = timeRange*self.flux 

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

239 timeRange) 

240 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

244 places=placesTests) 

245 else: 

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

247 for ampName in self.ampNames: 

248 maskAmp = localDataset.expIdMask[ampName] 

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

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

251 linearPart = self.flux*finalTimeVec 

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

253 self.assertEqual(fitType, localDataset.ptcFitType) 

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

255 if fitType == 'POLYNOMIAL': 

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

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

258 if fitType == 'EXPAPPROXIMATION': 

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

260 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

262 

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

264 for ampName in self.ampNames: 

265 maskAmp = localDataset.expIdMask[ampName] 

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

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

268 linearPart = self.flux*finalTimeVec 

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

270 

271 # Nonlinearity fit parameters 

272 # Polynomial fits are now normalized to unit flux scaling 

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

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

275 places=5) 

276 

277 # Non-linearity coefficient for linearizer 

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

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

280 places=placesTests) 

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

282 places=placesTests) 

283 

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

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

286 # Fractional nonlinearity residuals 

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

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

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

290 

291 def test_ptcFit(self): 

292 for createArray in [True, False]: 

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

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

295 

296 def test_meanVarMeasurement(self): 

297 task = self.defaultTaskExtract 

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

299 

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

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

302 

303 def test_meanVarMeasurementWithNans(self): 

304 task = self.defaultTaskExtract 

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

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

307 

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

309 

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

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

312 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

313 

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

315 im1 = self.flatExp1.maskedImage 

316 im2 = self.flatExp2.maskedImage 

317 

318 temp = im2.clone() 

319 temp *= expectedMu1 

320 diffIm = im1.clone() 

321 diffIm *= expectedMu2 

322 diffIm -= temp 

323 diffIm /= expectedMu 

324 

325 # Divide by two as it is what measureMeanVarCov returns 

326 # (variance of difference) 

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

328 

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

330 # less than 1 ADU 

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

332 self.assertLess(expectedMu - mu, 1) 

333 

334 def test_meanVarMeasurementAllNan(self): 

335 task = self.defaultTaskExtract 

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

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

338 

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

340 

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

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

343 self.assertTrue(covDiff is None) 

344 

345 def test_makeZeroSafe(self): 

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

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

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

349 

350 substituteValue = 1e-10 

351 

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

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

354 

355 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

356 substituteValue=substituteValue) 

357 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

358 substituteValue=substituteValue) 

359 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

360 substituteValue=substituteValue) 

361 

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

363 self.assertEqual(exp, meas) 

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

365 self.assertEqual(exp, meas) 

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

367 self.assertEqual(exp, meas) 

368 

369 def test_getInitialGoodPoints(self): 

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

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

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

373 consecutivePointsVarDecreases=2) 

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

375 

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

377 ys[5] = 6 

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

379 consecutivePointsVarDecreases=2) 

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

381 

382 def test_getExpIdsUsed(self): 

383 localDataset = copy.copy(self.dataset) 

384 

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

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

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

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

389 

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

391 with self.assertRaises(AssertionError): 

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

393 

394 def test_getGoodAmps(self): 

395 dataset = self.dataset 

396 

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

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

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

400 

401 

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

403 def setUp(self): 

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

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

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

407 

408 def test_generalBehaviour(self): 

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

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

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

412 

413 

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

415 pass 

416 

417 

418def setup_module(module): 

419 lsst.utils.tests.init() 

420 

421 

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

423 lsst.utils.tests.init() 

424 unittest.main()