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

262 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 tasks.""" 

62 

63 def setUp(self): 

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

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

66 

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

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

69 

70 self.flatMean = 2000 

71 self.readNoiseAdu = 10 

72 mockImageConfig = isrMock.IsrMock.ConfigClass() 

73 

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

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

76 mockImageConfig.flatDrop = 0.99999 

77 mockImageConfig.isTrimmed = True 

78 

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

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

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

82 

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

84 

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

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

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

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

89 

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

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

92 

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

94 self.flux = 1000. # ADU/sec 

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

96 self.k2NonLinearity = -5e-6 

97 # quadratic signal-chain non-linearity 

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

99 self.gain = 1.5 # e-/ADU 

100 self.c1 = 1./self.gain 

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

102 self.a00 = -1.2e-6 

103 self.c2 = -1.5e-6 

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

105 

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

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

108 

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

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

111 self.dataset.rawMeans[ampName] = muVec 

112 

113 def test_covAstier(self): 

114 """Test to check getCovariancesAstier 

115 

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

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

118 MeasurePhotonTransferCurveTask when doCovariancesAstier=True) 

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

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

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

122 

123 """ 

124 extractConfig = self.defaultConfigExtract 

125 extractConfig.minNumberGoodPixelsForCovariance = 5000 

126 extractConfig.detectorMeasurementRegion = 'FULL' 

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

128 

129 solveConfig = self.defaultConfigSolve 

130 solveConfig.ptcFitType = 'FULLCOVARIANCE' 

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

132 

133 inputGain = 0.75 

134 

135 muStandard, varStandard = {}, {} 

136 expDict = {} 

137 expIds = [] 

138 idCounter = 0 

139 for expTime in self.timeVec: 

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

141 readNoiseElectrons=3, expId1=idCounter, 

142 expId2=idCounter+1) 

143 mockExpRef1 = PretendRef(mockExp1) 

144 mockExpRef2 = PretendRef(mockExp2) 

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

146 expIds.append(idCounter) 

147 expIds.append(idCounter+1) 

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

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

150 muDiff, varDiff, covAstier = extractTask.measureMeanVarCov(mockExp1, mockExp2) 

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

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

153 idCounter += 2 

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

155 resultsSolve = solveTask.run(resultsExtract.outputCovariances) 

156 

157 for amp in self.ampNames: 

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

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

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

161 

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

163 localDataset = copy.copy(self.dataset) 

164 localDataset.ptcFitType = fitType 

165 configSolve = copy.copy(self.defaultConfigSolve) 

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

167 placesTests = 6 

168 if doFitBootstrap: 

169 configSolve.doFitBootstrap = True 

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

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

172 # error. 

173 placesTests = 3 

174 

175 if fitType == 'POLYNOMIAL': 

176 if order not in [2, 3]: 

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

178 if order == 2: 

179 for ampName in self.ampNames: 

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

181 mu in localDataset.rawMeans[ampName]] 

182 configSolve.polynomialFitDegree = 2 

183 if order == 3: 

184 for ampName in self.ampNames: 

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

186 for mu in localDataset.rawMeans[ampName]] 

187 configSolve.polynomialFitDegree = 3 

188 elif fitType == 'EXPAPPROXIMATION': 

189 g = self.gain 

190 for ampName in self.ampNames: 

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

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

193 for mu in localDataset.rawMeans[ampName]] 

194 else: 

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

196 

197 for ampName in self.ampNames: 

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

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

200 configLin.maxLinearAdu = 100000 

201 configLin.minLinearAdu = 50000 

202 if doTableArray: 

203 configLin.linearityType = "LookupTable" 

204 else: 

205 configLin.linearityType = "Polynomial" 

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

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

208 

209 if doTableArray: 

210 # Non-linearity 

211 numberAmps = len(self.ampNames) 

212 # localDataset: PTC dataset 

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

214 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

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

216 linDataset = linearityTask.run(localDataset, 

217 dummy=[1.0], 

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

219 inputDims={'detector': 0}) 

220 linDataset = linDataset.outputLinearizer 

221 else: 

222 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

223 linDataset = linearityTask.run(localDataset, 

224 dummy=[1.0], 

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

226 inputDims={'detector': 0}) 

227 linDataset = linDataset.outputLinearizer 

228 if doTableArray: 

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

230 for i in np.arange(numberAmps): 

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

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

233 signalIdeal = timeRange*self.flux 

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

235 timeRange) 

236 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

240 places=placesTests) 

241 else: 

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

243 for ampName in self.ampNames: 

244 maskAmp = localDataset.expIdMask[ampName] 

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

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

247 linearPart = self.flux*finalTimeVec 

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

249 self.assertEqual(fitType, localDataset.ptcFitType) 

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

251 if fitType == 'POLYNOMIAL': 

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

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

254 if fitType == 'EXPAPPROXIMATION': 

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

256 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

258 

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

260 for ampName in self.ampNames: 

261 maskAmp = localDataset.expIdMask[ampName] 

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

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

264 linearPart = self.flux*finalTimeVec 

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

266 

267 # Nonlinearity fit parameters 

268 # Polynomial fits are now normalized to unit flux scaling 

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

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

271 places=5) 

272 

273 # Non-linearity coefficient for linearizer 

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

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

276 places=placesTests) 

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

278 places=placesTests) 

279 

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

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

282 # Fractional nonlinearity residuals 

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

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

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

286 

287 def test_ptcFit(self): 

288 for createArray in [True, False]: 

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

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

291 

292 def test_meanVarMeasurement(self): 

293 task = self.defaultTaskExtract 

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

295 

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

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

298 

299 def test_meanVarMeasurementWithNans(self): 

300 task = self.defaultTaskExtract 

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

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

303 

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

305 

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

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

308 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

309 

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

311 im1 = self.flatExp1.maskedImage 

312 im2 = self.flatExp2.maskedImage 

313 

314 temp = im2.clone() 

315 temp *= expectedMu1 

316 diffIm = im1.clone() 

317 diffIm *= expectedMu2 

318 diffIm -= temp 

319 diffIm /= expectedMu 

320 

321 # Divide by two as it is what measureMeanVarCov returns 

322 # (variance of difference) 

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

324 

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

326 # less than 1 ADU 

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

328 self.assertLess(expectedMu - mu, 1) 

329 

330 def test_meanVarMeasurementAllNan(self): 

331 task = self.defaultTaskExtract 

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

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

334 

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

336 

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

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

339 self.assertTrue(covDiff is None) 

340 

341 def test_makeZeroSafe(self): 

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

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

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

345 

346 substituteValue = 1e-10 

347 

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

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

350 

351 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

352 substituteValue=substituteValue) 

353 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

354 substituteValue=substituteValue) 

355 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

356 substituteValue=substituteValue) 

357 

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

359 self.assertEqual(exp, meas) 

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

361 self.assertEqual(exp, meas) 

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

363 self.assertEqual(exp, meas) 

364 

365 def test_getInitialGoodPoints(self): 

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

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

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

369 consecutivePointsVarDecreases=2) 

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

371 

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

373 ys[5] = 6 

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

375 consecutivePointsVarDecreases=2) 

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

377 

378 def test_getExpIdsUsed(self): 

379 localDataset = copy.copy(self.dataset) 

380 

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

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

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

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

385 

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

387 with self.assertRaises(AssertionError): 

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

389 

390 def test_getGoodAmps(self): 

391 dataset = self.dataset 

392 

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

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

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

396 

397 

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

399 def setUp(self): 

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

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

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

403 

404 def test_generalBehaviour(self): 

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

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

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

408 

409 

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

411 pass 

412 

413 

414def setup_module(module): 

415 lsst.utils.tests.init() 

416 

417 

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

419 lsst.utils.tests.init() 

420 unittest.main()