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

274 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 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats 

210 configLin.maxLinearAdu = 100000 

211 configLin.minLinearAdu = 50000 

212 if doTableArray: 

213 configLin.linearityType = "LookupTable" 

214 else: 

215 configLin.linearityType = "Polynomial" 

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

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

218 

219 if doTableArray: 

220 # Non-linearity 

221 numberAmps = len(self.ampNames) 

222 # localDataset: PTC dataset 

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

224 localDataset = solveTask.fitPtc(localDataset) 

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

226 linDataset = linearityTask.run(localDataset, 

227 dummy=[1.0], 

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

229 inputDims={'detector': 0}) 

230 linDataset = linDataset.outputLinearizer 

231 else: 

232 localDataset = solveTask.fitPtc(localDataset) 

233 linDataset = linearityTask.run(localDataset, 

234 dummy=[1.0], 

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

236 inputDims={'detector': 0}) 

237 linDataset = linDataset.outputLinearizer 

238 if doTableArray: 

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

240 for i in np.arange(numberAmps): 

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

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

243 signalIdeal = timeRange*self.flux 

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

245 timeRange) 

246 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

250 places=placesTests) 

251 else: 

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

253 for ampName in self.ampNames: 

254 maskAmp = localDataset.expIdMask[ampName] 

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

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

257 linearPart = self.flux*finalTimeVec 

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

259 self.assertEqual(fitType, localDataset.ptcFitType) 

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

261 if fitType == 'POLYNOMIAL': 

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

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

264 if fitType == 'EXPAPPROXIMATION': 

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

266 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

268 

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

270 for ampName in self.ampNames: 

271 maskAmp = localDataset.expIdMask[ampName] 

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

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

274 linearPart = self.flux*finalTimeVec 

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

276 

277 # Nonlinearity fit parameters 

278 # Polynomial fits are now normalized to unit flux scaling 

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

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

281 places=5) 

282 

283 # Non-linearity coefficient for linearizer 

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

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

286 places=placesTests) 

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

288 places=placesTests) 

289 

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

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

292 # Fractional nonlinearity residuals 

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

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

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

296 

297 def test_ptcFit(self): 

298 for createArray in [True, False]: 

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

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

301 

302 def test_meanVarMeasurement(self): 

303 task = self.defaultTaskExtract 

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

305 

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

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

308 

309 def test_meanVarMeasurementWithNans(self): 

310 task = self.defaultTaskExtract 

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

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

313 

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

315 

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

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

318 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

319 

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

321 im1 = self.flatExp1.maskedImage 

322 im2 = self.flatExp2.maskedImage 

323 

324 temp = im2.clone() 

325 temp *= expectedMu1 

326 diffIm = im1.clone() 

327 diffIm *= expectedMu2 

328 diffIm -= temp 

329 diffIm /= expectedMu 

330 

331 # Divide by two as it is what measureMeanVarCov returns 

332 # (variance of difference) 

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

334 

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

336 # less than 1 ADU 

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

338 self.assertLess(expectedMu - mu, 1) 

339 

340 def test_meanVarMeasurementAllNan(self): 

341 task = self.defaultTaskExtract 

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

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

344 

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

346 

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

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

349 self.assertTrue(covDiff is None) 

350 

351 def test_makeZeroSafe(self): 

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

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

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

355 

356 substituteValue = 1e-10 

357 

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

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

360 

361 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

362 substituteValue=substituteValue) 

363 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

364 substituteValue=substituteValue) 

365 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

366 substituteValue=substituteValue) 

367 

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

369 self.assertEqual(exp, meas) 

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

371 self.assertEqual(exp, meas) 

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

373 self.assertEqual(exp, meas) 

374 

375 def test_getInitialGoodPoints(self): 

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

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

378 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, maxDeviationPositive=0.1, 

379 maxDeviationNegative=0.25, 

380 minMeanRatioTest=0., 

381 minVarPivotSearch=0.) 

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

383 

384 ys[-1] = 30 

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

386 maxDeviationPositive=0.1, 

387 maxDeviationNegative=0.25, 

388 minMeanRatioTest=0., 

389 minVarPivotSearch=0.) 

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

391 

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

393 newYs = copy.copy(ys) 

394 results = [False, True, True, False, False] 

395 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]): 

396 newYs[-1] = ys[-1] + (factor*ys[-1]) 

397 points = self.defaultTaskSolve._getInitialGoodPoints(xs, newYs, maxDeviationPositive=0.05, 

398 maxDeviationNegative=0.25, 

399 minMeanRatioTest=0.0, 

400 minVarPivotSearch=0.0) 

401 assert (np.all(points[0:-2])) # noqa: E712 - flake8 is wrong here because of numpy.bool 

402 assert points[-1] == results[i] 

403 

404 def test_getExpIdsUsed(self): 

405 localDataset = copy.copy(self.dataset) 

406 

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

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

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

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

411 

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

413 with self.assertRaises(AssertionError): 

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

415 

416 def test_getGoodAmps(self): 

417 dataset = self.dataset 

418 

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

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

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

422 

423 

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

425 def setUp(self): 

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

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

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

429 

430 def test_generalBehaviour(self): 

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

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

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

434 

435 

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

437 pass 

438 

439 

440def setup_module(module): 

441 lsst.utils.tests.init() 

442 

443 

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

445 lsst.utils.tests.init() 

446 unittest.main()