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 mock data, that 

106 the covariances via FFT (as it is in MeasurePhotonTransferCurveTask when 

107 doCovariancesAstier=True) are the same as calculated in real space, and that 

108 Cov[0, 0] (i.e., the variances) are similar to the variances calculated with the standard 

109 method (when doCovariancesAstier=false), 

110 """ 

111 task = self.defaultTask 

112 extractConfig = self.defaultConfigExtract 

113 extractConfig.minNumberGoodPixelsForCovariance = 5000 

114 extractConfig.detectorMeasurementRegion = 'FULL' 

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

116 

117 solveConfig = self.defaultConfigSolve 

118 solveConfig.ptcFitType = 'FULLCOVARIANCE' 

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

120 

121 inputGain = 0.75 

122 

123 muStandard, varStandard = {}, {} 

124 expDict = {} 

125 expIds = [] 

126 idCounter = 0 

127 for expTime in self.timeVec: 

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

129 readNoiseElectrons=3, expId1=idCounter, 

130 expId2=idCounter+1) 

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

132 expIds.append(idCounter) 

133 expIds.append(idCounter+1) 

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

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

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

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

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

139 # Calculate covariances in an independent way: direct space 

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

141 

142 # Test that the arrays "covs" (FFT) and "covDirect" (direct space) are the same 

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

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

145 self.assertAlmostEqual(a, b) 

146 idCounter += 2 

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

148 resultsSolve = solveTask.run(resultsExtract.outputCovariances) 

149 

150 for amp in self.ampNames: 

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

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

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

154 

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

156 localDataset = copy.copy(self.dataset) 

157 localDataset.ptcFitType = fitType 

158 configSolve = copy.copy(self.defaultConfigSolve) 

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

160 placesTests = 6 

161 if doFitBootstrap: 

162 configSolve.doFitBootstrap = True 

163 # Bootstrap method in cp_pipe/utils.py does multiple fits in the precense of noise. 

164 # Allow for more margin of error. 

165 placesTests = 3 

166 

167 if fitType == 'POLYNOMIAL': 

168 if order not in [2, 3]: 

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

170 if order == 2: 

171 for ampName in self.ampNames: 

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

173 mu in localDataset.rawMeans[ampName]] 

174 configSolve.polynomialFitDegree = 2 

175 if order == 3: 

176 for ampName in self.ampNames: 

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

178 for mu in localDataset.rawMeans[ampName]] 

179 configSolve.polynomialFitDegree = 3 

180 elif fitType == 'EXPAPPROXIMATION': 

181 g = self.gain 

182 for ampName in self.ampNames: 

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

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

185 for mu in localDataset.rawMeans[ampName]] 

186 else: 

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

188 

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

190 configLin.maxLinearAdu = 100000 

191 configLin.minLinearAdu = 50000 

192 if doTableArray: 

193 configLin.linearityType = "LookupTable" 

194 else: 

195 configLin.linearityType = "Polynomial" 

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

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

198 

199 if doTableArray: 

200 # Non-linearity 

201 numberAmps = len(self.ampNames) 

202 # localDataset: PTC dataset (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`) 

203 localDataset = solveTask.fitPtc(localDataset) 

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

205 linDataset = linearityTask.run(localDataset, 

206 dummy=[1.0], 

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

208 inputDims={'detector': 0}) 

209 linDataset = linDataset.outputLinearizer 

210 else: 

211 localDataset = solveTask.fitPtc(localDataset) 

212 linDataset = linearityTask.run(localDataset, 

213 dummy=[1.0], 

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

215 inputDims={'detector': 0}) 

216 linDataset = linDataset.outputLinearizer 

217 if doTableArray: 

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

219 for i in np.arange(numberAmps): 

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

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

222 signalIdeal = timeRange*self.flux 

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

224 timeRange) 

225 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

229 places=placesTests) 

230 else: 

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

232 for ampName in self.ampNames: 

233 maskAmp = localDataset.expIdMask[ampName] 

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

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

236 linearPart = self.flux*finalTimeVec 

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

238 self.assertEqual(fitType, localDataset.ptcFitType) 

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

240 if fitType == 'POLYNOMIAL': 

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

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

243 if fitType == 'EXPAPPROXIMATION': 

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

245 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

247 

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

249 for ampName in self.ampNames: 

250 maskAmp = localDataset.expIdMask[ampName] 

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

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

253 linearPart = self.flux*finalTimeVec 

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

255 

256 # Nonlinearity fit parameters 

257 # Polynomial fits are now normalized to unit flux scaling 

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

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

260 places=5) 

261 

262 # Non-linearity coefficient for linearizer 

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

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

265 places=placesTests) 

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

267 places=placesTests) 

268 

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

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

271 # Fractional nonlinearity residuals 

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

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

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

275 

276 def test_ptcFit(self): 

277 for createArray in [True, False]: 

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

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

280 

281 def test_meanVarMeasurement(self): 

282 task = self.defaultTaskExtract 

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

284 

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

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

287 

288 def test_meanVarMeasurementWithNans(self): 

289 task = self.defaultTaskExtract 

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

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

292 

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

294 

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

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

297 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

298 

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

300 im1 = self.flatExp1.maskedImage 

301 im2 = self.flatExp2.maskedImage 

302 

303 temp = im2.clone() 

304 temp *= expectedMu1 

305 diffIm = im1.clone() 

306 diffIm *= expectedMu2 

307 diffIm -= temp 

308 diffIm /= expectedMu 

309 

310 # Dive by two as it is what measureMeanVarCov returns (variance of difference) 

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

312 

313 # Check that the standard deviations and the emans agree to less than 1 ADU 

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

315 self.assertLess(expectedMu - mu, 1) 

316 

317 def test_meanVarMeasurementAllNan(self): 

318 task = self.defaultTaskExtract 

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

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

321 

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

323 

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

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

326 self.assertTrue(covDiff is None) 

327 

328 def test_makeZeroSafe(self): 

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

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

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

332 

333 substituteValue = 1e-10 

334 

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

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

337 

338 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

339 substituteValue=substituteValue) 

340 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

341 substituteValue=substituteValue) 

342 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

343 substituteValue=substituteValue) 

344 

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

346 self.assertEqual(exp, meas) 

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

348 self.assertEqual(exp, meas) 

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

350 self.assertEqual(exp, meas) 

351 

352 def test_getInitialGoodPoints(self): 

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

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

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

356 maxDeviationNegative=0.25, 

357 minMeanRatioTest=0., 

358 minVarPivotSearch=0.) 

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

360 

361 ys[-1] = 30 

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

363 maxDeviationPositive=0.1, 

364 maxDeviationNegative=0.25, 

365 minMeanRatioTest=0., 

366 minVarPivotSearch=0.) 

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

368 

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

370 newYs = copy.copy(ys) 

371 results = [False, True, True, False, False] 

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

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

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

375 maxDeviationNegative=0.25, 

376 minMeanRatioTest=0.0, 

377 minVarPivotSearch=0.0) 

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

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

380 

381 def test_getExpIdsUsed(self): 

382 localDataset = copy.copy(self.dataset) 

383 

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

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

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

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

388 

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

390 with self.assertRaises(AssertionError): 

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

392 

393 def test_getGoodAmps(self): 

394 dataset = self.dataset 

395 

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

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

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

399 

400 

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

402 def setUp(self): 

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

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

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

406 

407 def test_generalBehaviour(self): 

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

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

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

411 

412 

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

414 pass 

415 

416 

417def setup_module(module): 

418 lsst.utils.tests.init() 

419 

420 

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

422 lsst.utils.tests.init() 

423 unittest.main()