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.astierCovPtcUtils import fitData 

39from lsst.cp.pipe.utils import (funcPolynomial, makeMockFlats) 

40 

41 

42class FakeCamera(list): 

43 def getName(self): 

44 return "FakeCam" 

45 

46 

47class MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase): 

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

49 

50 def setUp(self): 

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

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

53 

54 self.flatMean = 2000 

55 self.readNoiseAdu = 10 

56 mockImageConfig = isrMock.IsrMock.ConfigClass() 

57 

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

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

60 mockImageConfig.flatDrop = 0.99999 

61 mockImageConfig.isTrimmed = True 

62 

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

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

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

66 

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

68 

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

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

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

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

73 

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

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

76 

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

78 self.flux = 1000. # ADU/sec 

79 timeVec = np.arange(1., 201.) 

80 self.k2NonLinearity = -5e-6 

81 muVec = self.flux*timeVec + self.k2NonLinearity*timeVec**2 # quadratic signal-chain non-linearity 

82 self.gain = 1.5 # e-/ADU 

83 self.c1 = 1./self.gain 

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

85 self.a00 = -1.2e-6 

86 self.c2 = -1.5e-6 

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

88 

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

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

91 

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

93 self.dataset.rawExpTimes[ampName] = timeVec 

94 self.dataset.rawMeans[ampName] = muVec 

95 

96 def test_covAstier(self): 

97 """Test to check getCovariancesAstier 

98 

99 We check that the gain is the same as the imput gain from the mock data, that 

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

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

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

103 method (when doCovariancesAstier=false), 

104 """ 

105 localDataset = copy.copy(self.dataset) 

106 config = copy.copy(self.defaultConfig) 

107 task = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=config) 

108 

109 expTimes = np.arange(5, 170, 5) 

110 tupleRecords = [] 

111 allTags = [] 

112 muStandard, varStandard = {}, {} 

113 for expTime in expTimes: 

114 mockExp1, mockExp2 = makeMockFlats(expTime, gain=0.75) 

115 tupleRows = [] 

116 

117 for ampNumber, amp in enumerate(self.ampNames): 

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

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

120 muStandard.setdefault(amp, []).append(muDiff) 

121 varStandard.setdefault(amp, []).append(varDiff) 

122 # Calculate covariances in an independent way: direct space 

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

124 

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

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

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

128 self.assertAlmostEqual(a, b) 

129 tupleRows += [(muDiff, ) + covRow + (ampNumber, expTime, amp) for covRow in covAstier] 

130 tags = ['mu', 'i', 'j', 'var', 'cov', 'npix', 'ext', 'expTime', 'ampName'] 

131 allTags += tags 

132 tupleRecords += tupleRows 

133 covariancesWithTags = np.core.records.fromrecords(tupleRecords, names=allTags) 

134 

135 expIdMask = {ampName: np.repeat(True, len(expTimes)) for ampName in self.ampNames} 

136 covFits, covFitsNoB = fitData(covariancesWithTags, expIdMask) 

137 localDataset = task.getOutputPtcDataCovAstier(localDataset, covFits, covFitsNoB) 

138 # Chek the gain and that the ratio of the variance caclulated via cov Astier (FFT) and 

139 # that calculated with the standard PTC calculation (afw) is close to 1. 

140 for amp in self.ampNames: 

141 self.assertAlmostEqual(localDataset.gain[amp], 0.75, places=2) 

142 for v1, v2 in zip(varStandard[amp], localDataset.finalVars[amp]): 

143 v2 *= (0.75**2) # convert to electrons 

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

145 

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

147 localDataset = copy.copy(self.dataset) 

148 config = copy.copy(self.defaultConfig) 

149 placesTests = 6 

150 if doFitBootstrap: 

151 config.doFitBootstrap = True 

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

153 # Allow for more margin of error. 

154 placesTests = 3 

155 

156 if fitType == 'POLYNOMIAL': 

157 if order not in [2, 3]: 

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

159 if order == 2: 

160 for ampName in self.ampNames: 

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

162 mu in localDataset.rawMeans[ampName]] 

163 config.polynomialFitDegree = 2 

164 if order == 3: 

165 for ampName in self.ampNames: 

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

167 for mu in localDataset.rawMeans[ampName]] 

168 config.polynomialFitDegree = 3 

169 elif fitType == 'EXPAPPROXIMATION': 

170 g = self.gain 

171 for ampName in self.ampNames: 

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

173 self.noiseSq/(g*g)) for mu in localDataset.rawMeans[ampName]] 

174 else: 

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

176 

177 config.linearity.maxLookupTableAdu = 200000 # Max ADU in input mock flats 

178 config.linearity.maxLinearAdu = 100000 

179 config.linearity.minLinearAdu = 50000 

180 if doTableArray: 

181 config.linearity.linearityType = "LookupTable" 

182 else: 

183 config.linearity.linearityType = "Polynomial" 

184 task = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=config) 

185 

186 if doTableArray: 

187 # Non-linearity 

188 numberAmps = len(self.ampNames) 

189 # localDataset: PTC dataset (lsst.cp.pipe.ptc.PhotonTransferCurveDataset) 

190 localDataset = task.fitPtc(localDataset, ptcFitType=fitType) 

191 # linDataset: Dictionary of `lsst.cp.pipe.ptc.LinearityResidualsAndLinearizersDataset` 

192 linDataset = task.linearity.run(localDataset, 

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

194 inputDims={'detector': 0}) 

195 linDataset = linDataset.outputLinearizer 

196 else: 

197 localDataset = task.fitPtc(localDataset, ptcFitType=fitType) 

198 linDataset = task.linearity.run(localDataset, 

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

200 inputDims={'detector': 0}) 

201 linDataset = linDataset.outputLinearizer 

202 

203 if doTableArray: 

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

205 for i in np.arange(numberAmps): 

206 tMax = (config.linearity.maxLookupTableAdu)/self.flux 

207 timeRange = np.linspace(0., tMax, config.linearity.maxLookupTableAdu) 

208 signalIdeal = timeRange*self.flux 

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

210 timeRange) 

211 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

215 places=placesTests) 

216 else: 

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

218 for ampName in self.ampNames: 

219 maskAmp = localDataset.expIdMask[ampName] 

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

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

222 linearPart = self.flux*finalTimeVec 

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

224 self.assertEqual(fitType, localDataset.ptcFitType) 

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

226 if fitType == 'POLYNOMIAL': 

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

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

229 if fitType == 'EXPAPPROXIMATION': 

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

231 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

233 

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

235 for ampName in self.ampNames: 

236 maskAmp = localDataset.expIdMask[ampName] 

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

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

239 linearPart = self.flux*finalTimeVec 

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

241 

242 # Nonlinearity fit parameters 

243 # Polynomial fits are now normalized to unit flux scaling 

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

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

246 places=5) 

247 

248 # Non-linearity coefficient for linearizer 

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

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

251 places=placesTests) 

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

253 places=placesTests) 

254 

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

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

257 # Fractional nonlinearity residuals 

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

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

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

261 

262 def test_ptcFit(self): 

263 for createArray in [True, False]: 

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

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

266 

267 def test_meanVarMeasurement(self): 

268 task = self.defaultTask 

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

270 

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

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

273 

274 def test_meanVarMeasurementWithNans(self): 

275 task = self.defaultTask 

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

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

278 

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

280 

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

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

283 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

284 

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

286 im1 = self.flatExp1.maskedImage 

287 im2 = self.flatExp2.maskedImage 

288 

289 temp = im2.clone() 

290 temp *= expectedMu1 

291 diffIm = im1.clone() 

292 diffIm *= expectedMu2 

293 diffIm -= temp 

294 diffIm /= expectedMu 

295 

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

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

298 

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

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

301 self.assertLess(expectedMu - mu, 1) 

302 

303 def test_meanVarMeasurementAllNan(self): 

304 task = self.defaultTask 

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

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

307 

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

309 

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

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

312 self.assertTrue(covDiff is None) 

313 

314 def test_getInitialGoodPoints(self): 

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

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

317 points = self.defaultTask._getInitialGoodPoints(xs, ys, maxDeviationPositive=0.1, 

318 maxDeviationNegative=0.25, 

319 minMeanRatioTest=0., 

320 minVarPivotSearch=0.) 

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

322 

323 ys[-1] = 30 

324 points = self.defaultTask._getInitialGoodPoints(xs, ys, 

325 maxDeviationPositive=0.1, 

326 maxDeviationNegative=0.25, 

327 minMeanRatioTest=0., 

328 minVarPivotSearch=0.) 

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

330 

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

332 newYs = copy.copy(ys) 

333 results = [False, True, True, False, False] 

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

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

336 points = self.defaultTask._getInitialGoodPoints(xs, newYs, maxDeviationPositive=0.05, 

337 maxDeviationNegative=0.25, 

338 minMeanRatioTest=0.0, 

339 minVarPivotSearch=0.0) 

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

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

342 

343 def test_getExpIdsUsed(self): 

344 localDataset = copy.copy(self.dataset) 

345 

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

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

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

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

350 

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

352 with self.assertRaises(AssertionError): 

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

354 

355 def test_getGoodAmps(self): 

356 dataset = self.dataset 

357 

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

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

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

361 

362 

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

364 def setUp(self): 

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

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

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

368 

369 def test_generalBehaviour(self): 

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

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

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

373 

374 

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

376 pass 

377 

378 

379def setup_module(module): 

380 lsst.utils.tests.init() 

381 

382 

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

384 lsst.utils.tests.init() 

385 unittest.main()