Hide keyboard shortcuts

Hot-keys 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

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 MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase): 

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

48 

49 def setUp(self): 

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

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

52 

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

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

55 

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

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

58 

59 self.flatMean = 2000 

60 self.readNoiseAdu = 10 

61 mockImageConfig = isrMock.IsrMock.ConfigClass() 

62 

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

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

65 mockImageConfig.flatDrop = 0.99999 

66 mockImageConfig.isTrimmed = True 

67 

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

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

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

71 

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

73 

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

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

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

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

78 

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

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

81 

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

83 self.flux = 1000. # ADU/sec 

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

85 self.k2NonLinearity = -5e-6 

86 # quadratic signal-chain non-linearity 

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

88 self.gain = 1.5 # e-/ADU 

89 self.c1 = 1./self.gain 

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

91 self.a00 = -1.2e-6 

92 self.c2 = -1.5e-6 

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

94 

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

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

97 

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

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

100 self.dataset.rawMeans[ampName] = muVec 

101 

102 def test_covAstier(self): 

103 """Test to check getCovariancesAstier 

104 

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

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

107 MeasurePhotonTransferCurveTask when doCovariancesAstier=True) 

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

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

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

111 

112 """ 

113 task = self.defaultTask 

114 extractConfig = self.defaultConfigExtract 

115 extractConfig.minNumberGoodPixelsForCovariance = 5000 

116 extractConfig.detectorMeasurementRegion = 'FULL' 

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

118 

119 solveConfig = self.defaultConfigSolve 

120 solveConfig.ptcFitType = 'FULLCOVARIANCE' 

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

122 

123 inputGain = 0.75 

124 

125 muStandard, varStandard = {}, {} 

126 expDict = {} 

127 expIds = [] 

128 idCounter = 0 

129 for expTime in self.timeVec: 

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

131 readNoiseElectrons=3, expId1=idCounter, 

132 expId2=idCounter+1) 

133 expDict[expTime] = ((mockExp1, idCounter), (mockExp2, idCounter+1)) 

134 expIds.append(idCounter) 

135 expIds.append(idCounter+1) 

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

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

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

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

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

141 # Calculate covariances in an independent way: direct space 

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

143 

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

145 # (direct space) are the same 

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

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

148 self.assertAlmostEqual(a, b) 

149 idCounter += 2 

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

151 resultsSolve = solveTask.run(resultsExtract.outputCovariances) 

152 

153 for amp in self.ampNames: 

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

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

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

157 

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

159 localDataset = copy.copy(self.dataset) 

160 localDataset.ptcFitType = fitType 

161 configSolve = copy.copy(self.defaultConfigSolve) 

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

163 placesTests = 6 

164 if doFitBootstrap: 

165 configSolve.doFitBootstrap = True 

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

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

168 # error. 

169 placesTests = 3 

170 

171 if fitType == 'POLYNOMIAL': 

172 if order not in [2, 3]: 

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

174 if order == 2: 

175 for ampName in self.ampNames: 

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

177 mu in localDataset.rawMeans[ampName]] 

178 configSolve.polynomialFitDegree = 2 

179 if order == 3: 

180 for ampName in self.ampNames: 

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

182 for mu in localDataset.rawMeans[ampName]] 

183 configSolve.polynomialFitDegree = 3 

184 elif fitType == 'EXPAPPROXIMATION': 

185 g = self.gain 

186 for ampName in self.ampNames: 

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

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

189 for mu in localDataset.rawMeans[ampName]] 

190 else: 

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

192 

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

194 configLin.maxLinearAdu = 100000 

195 configLin.minLinearAdu = 50000 

196 if doTableArray: 

197 configLin.linearityType = "LookupTable" 

198 else: 

199 configLin.linearityType = "Polynomial" 

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

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

202 

203 if doTableArray: 

204 # Non-linearity 

205 numberAmps = len(self.ampNames) 

206 # localDataset: PTC dataset 

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

208 localDataset = solveTask.fitPtc(localDataset) 

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

210 linDataset = linearityTask.run(localDataset, 

211 dummy=[1.0], 

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

213 inputDims={'detector': 0}) 

214 linDataset = linDataset.outputLinearizer 

215 else: 

216 localDataset = solveTask.fitPtc(localDataset) 

217 linDataset = linearityTask.run(localDataset, 

218 dummy=[1.0], 

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

220 inputDims={'detector': 0}) 

221 linDataset = linDataset.outputLinearizer 

222 if doTableArray: 

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

224 for i in np.arange(numberAmps): 

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

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

227 signalIdeal = timeRange*self.flux 

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

229 timeRange) 

230 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

234 places=placesTests) 

235 else: 

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

237 for ampName in self.ampNames: 

238 maskAmp = localDataset.expIdMask[ampName] 

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

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

241 linearPart = self.flux*finalTimeVec 

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

243 self.assertEqual(fitType, localDataset.ptcFitType) 

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

245 if fitType == 'POLYNOMIAL': 

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

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

248 if fitType == 'EXPAPPROXIMATION': 

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

250 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

252 

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

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 

261 # Nonlinearity fit parameters 

262 # Polynomial fits are now normalized to unit flux scaling 

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

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

265 places=5) 

266 

267 # Non-linearity coefficient for linearizer 

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

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

270 places=placesTests) 

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

272 places=placesTests) 

273 

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

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

276 # Fractional nonlinearity residuals 

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

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

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

280 

281 def test_ptcFit(self): 

282 for createArray in [True, False]: 

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

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

285 

286 def test_meanVarMeasurement(self): 

287 task = self.defaultTaskExtract 

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

289 

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

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

292 

293 def test_meanVarMeasurementWithNans(self): 

294 task = self.defaultTaskExtract 

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

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

297 

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

299 

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

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

302 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

303 

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

305 im1 = self.flatExp1.maskedImage 

306 im2 = self.flatExp2.maskedImage 

307 

308 temp = im2.clone() 

309 temp *= expectedMu1 

310 diffIm = im1.clone() 

311 diffIm *= expectedMu2 

312 diffIm -= temp 

313 diffIm /= expectedMu 

314 

315 # Divide by two as it is what measureMeanVarCov returns 

316 # (variance of difference) 

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

318 

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

320 # less than 1 ADU 

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

322 self.assertLess(expectedMu - mu, 1) 

323 

324 def test_meanVarMeasurementAllNan(self): 

325 task = self.defaultTaskExtract 

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

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

328 

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

330 

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

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

333 self.assertTrue(covDiff is None) 

334 

335 def test_makeZeroSafe(self): 

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

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

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

339 

340 substituteValue = 1e-10 

341 

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

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

344 

345 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

346 substituteValue=substituteValue) 

347 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

348 substituteValue=substituteValue) 

349 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

350 substituteValue=substituteValue) 

351 

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

353 self.assertEqual(exp, meas) 

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

355 self.assertEqual(exp, meas) 

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

357 self.assertEqual(exp, meas) 

358 

359 def test_getInitialGoodPoints(self): 

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

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

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

363 maxDeviationNegative=0.25, 

364 minMeanRatioTest=0., 

365 minVarPivotSearch=0.) 

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

367 

368 ys[-1] = 30 

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

370 maxDeviationPositive=0.1, 

371 maxDeviationNegative=0.25, 

372 minMeanRatioTest=0., 

373 minVarPivotSearch=0.) 

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

375 

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

377 newYs = copy.copy(ys) 

378 results = [False, True, True, False, False] 

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

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

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

382 maxDeviationNegative=0.25, 

383 minMeanRatioTest=0.0, 

384 minVarPivotSearch=0.0) 

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

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

387 

388 def test_getExpIdsUsed(self): 

389 localDataset = copy.copy(self.dataset) 

390 

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

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

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

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

395 

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

397 with self.assertRaises(AssertionError): 

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

399 

400 def test_getGoodAmps(self): 

401 dataset = self.dataset 

402 

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

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

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

406 

407 

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

409 def setUp(self): 

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

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

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

413 

414 def test_generalBehaviour(self): 

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

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

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

418 

419 

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

421 pass 

422 

423 

424def setup_module(module): 

425 lsst.utils.tests.init() 

426 

427 

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

429 lsst.utils.tests.init() 

430 unittest.main()