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 covFits, covFitsNoB = fitData(covariancesWithTags) 

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

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

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

138 for amp in self.ampNames: 

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

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

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

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

143 

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

145 localDataset = copy.copy(self.dataset) 

146 config = copy.copy(self.defaultConfig) 

147 placesTests = 6 

148 if doFitBootstrap: 

149 config.doFitBootstrap = True 

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

151 # Allow for more margin of error. 

152 placesTests = 3 

153 

154 if fitType == 'POLYNOMIAL': 

155 if order not in [2, 3]: 

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

157 if order == 2: 

158 for ampName in self.ampNames: 

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

160 mu in localDataset.rawMeans[ampName]] 

161 config.polynomialFitDegree = 2 

162 if order == 3: 

163 for ampName in self.ampNames: 

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

165 for mu in localDataset.rawMeans[ampName]] 

166 config.polynomialFitDegree = 3 

167 elif fitType == 'EXPAPPROXIMATION': 

168 g = self.gain 

169 for ampName in self.ampNames: 

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

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

172 else: 

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

174 

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

176 config.linearity.maxLinearAdu = 100000 

177 config.linearity.minLinearAdu = 50000 

178 if doTableArray: 

179 config.linearity.linearityType = "LookupTable" 

180 else: 

181 config.linearity.linearityType = "Polynomial" 

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

183 

184 if doTableArray: 

185 # Non-linearity 

186 numberAmps = len(self.ampNames) 

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

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

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

190 linDataset = task.linearity.run(localDataset, 

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

192 inputDims={'detector': 0}) 

193 linDataset = linDataset.outputLinearizer 

194 else: 

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

196 linDataset = task.linearity.run(localDataset, 

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

198 inputDims={'detector': 0}) 

199 linDataset = linDataset.outputLinearizer 

200 

201 if doTableArray: 

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

203 for i in np.arange(numberAmps): 

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

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

206 signalIdeal = timeRange*self.flux 

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

208 timeRange) 

209 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

213 places=placesTests) 

214 else: 

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

216 for ampName in self.ampNames: 

217 maskAmp = localDataset.expIdMask[ampName] 

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

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

220 linearPart = self.flux*finalTimeVec 

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

222 self.assertEqual(fitType, localDataset.ptcFitType) 

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

224 if fitType == 'POLYNOMIAL': 

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

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

227 if fitType == 'EXPAPPROXIMATION': 

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

229 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

231 

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

233 for ampName in self.ampNames: 

234 maskAmp = localDataset.expIdMask[ampName] 

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

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

237 linearPart = self.flux*finalTimeVec 

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

239 

240 # Nonlinearity fit parameters 

241 # Polynomial fits are now normalized to unit flux scaling 

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

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

244 places=5) 

245 

246 # Non-linearity coefficient for linearizer 

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

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

249 places=placesTests) 

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

251 places=placesTests) 

252 

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

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

255 # Fractional nonlinearity residuals 

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

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

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

259 

260 def test_ptcFit(self): 

261 for createArray in [True, False]: 

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

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

264 

265 def test_meanVarMeasurement(self): 

266 task = self.defaultTask 

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

268 

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

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

271 

272 def test_meanVarMeasurementWithNans(self): 

273 task = self.defaultTask 

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

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

276 

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

278 

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

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

281 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

282 

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

284 im1 = self.flatExp1.maskedImage 

285 im2 = self.flatExp2.maskedImage 

286 

287 temp = im2.clone() 

288 temp *= expectedMu1 

289 diffIm = im1.clone() 

290 diffIm *= expectedMu2 

291 diffIm -= temp 

292 diffIm /= expectedMu 

293 

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

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

296 

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

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

299 self.assertLess(expectedMu - mu, 1) 

300 

301 def test_meanVarMeasurementAllNan(self): 

302 task = self.defaultTask 

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

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

305 

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

307 

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

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

310 self.assertTrue(covDiff is None) 

311 

312 def test_getInitialGoodPoints(self): 

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

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

315 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25) 

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

317 

318 ys[-1] = 30 

319 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25) 

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

321 

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

323 newYs = copy.copy(ys) 

324 results = [False, True, True, False, False] 

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

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

327 points = self.defaultTask._getInitialGoodPoints(xs, newYs, 0.05, 0.25) 

328 assert (np.all(points[0:-1]) == True) # noqa: E712 - flake8 is wrong here because of numpy.bool 

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

330 

331 def test_getExpIdsUsed(self): 

332 localDataset = copy.copy(self.dataset) 

333 

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

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

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

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

338 

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

340 with self.assertRaises(AssertionError): 

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

342 

343 def test_getGoodAmps(self): 

344 dataset = self.dataset 

345 

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

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

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

349 

350 

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

352 def setUp(self): 

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

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

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

356 

357 def test_generalBehaviour(self): 

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

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

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

361 

362 

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

364 pass 

365 

366 

367def setup_module(module): 

368 lsst.utils.tests.init() 

369 

370 

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

372 lsst.utils.tests.init() 

373 unittest.main()