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.cp.pipe.ptc import PhotonTransferCurveDataset 

38from lsst.cp.pipe.astierCovPtcUtils import fitData 

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

40 

41 

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

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

44 

45 def setUp(self): 

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

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

48 

49 self.flatMean = 2000 

50 self.readNoiseAdu = 10 

51 mockImageConfig = isrMock.IsrMock.ConfigClass() 

52 

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

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

55 mockImageConfig.flatDrop = 0.99999 

56 mockImageConfig.isTrimmed = True 

57 

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

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

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

61 

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

63 

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

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

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

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

68 

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

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

71 

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

73 self.flux = 1000. # ADU/sec 

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

75 self.k2NonLinearity = -5e-6 

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

77 self.gain = 1.5 # e-/ADU 

78 self.c1 = 1./self.gain 

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

80 self.a00 = -1.2e-6 

81 self.c2 = -1.5e-6 

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

83 

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

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

86 

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

88 self.dataset.rawExpTimes[ampName] = timeVec 

89 self.dataset.rawMeans[ampName] = muVec 

90 

91 def test_covAstier(self): 

92 """Test to check getCovariancesAstier 

93 

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

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

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

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

98 method (when doCovariancesAstier=false), 

99 """ 

100 localDataset = copy.copy(self.dataset) 

101 config = copy.copy(self.defaultConfig) 

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

103 

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

105 tupleRecords = [] 

106 allTags = [] 

107 muStandard, varStandard = {}, {} 

108 for expTime in expTimes: 

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

110 tupleRows = [] 

111 

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

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

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

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

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

117 # Calculate covariances in an independent way: direct space 

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

119 

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

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

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

123 self.assertAlmostEqual(a, b) 

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

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

126 allTags += tags 

127 tupleRecords += tupleRows 

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

129 covFits, _ = fitData(covariancesWithTags) 

130 localDataset = task.getOutputPtcDataCovAstier(localDataset, covFits) 

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

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

133 for amp in self.ampNames: 

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

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

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

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

138 

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

140 localDataset = copy.copy(self.dataset) 

141 config = copy.copy(self.defaultConfig) 

142 placesTests = 6 

143 if doFitBootstrap: 

144 config.doFitBootstrap = True 

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

146 # Allow for more margin of error. 

147 placesTests = 3 

148 

149 if fitType == 'POLYNOMIAL': 

150 if order not in [2, 3]: 

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

152 if order == 2: 

153 for ampName in self.ampNames: 

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

155 mu in localDataset.rawMeans[ampName]] 

156 config.polynomialFitDegree = 2 

157 if order == 3: 

158 for ampName in self.ampNames: 

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

160 for mu in localDataset.rawMeans[ampName]] 

161 config.polynomialFitDegree = 3 

162 elif fitType == 'EXPAPPROXIMATION': 

163 g = self.gain 

164 for ampName in self.ampNames: 

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

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

167 else: 

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

169 

170 config.maxAduForLookupTableLinearizer = 200000 # Max ADU in input mock flats 

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

172 

173 if doTableArray: 

174 # Non-linearity 

175 numberAmps = len(self.ampNames) 

176 numberAduValues = config.maxAduForLookupTableLinearizer 

177 lookupTableArray = np.zeros((numberAmps, numberAduValues), dtype=np.float32) 

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

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

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

181 linDataset = task.fitNonLinearity(localDataset, tableArray=lookupTableArray) 

182 else: 

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

184 linDataset = task.fitNonLinearity(localDataset) 

185 

186 if doTableArray: 

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

188 for i in np.arange(numberAmps): 

189 tMax = (config.maxAduForLookupTableLinearizer)/self.flux 

190 timeRange = np.linspace(0., tMax, config.maxAduForLookupTableLinearizer) 

191 signalIdeal = timeRange*self.flux 

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

193 timeRange) 

194 linearizerTableRow = signalIdeal - signalUncorrected 

195 self.assertEqual(len(linearizerTableRow), len(lookupTableArray[i, :])) 

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

197 self.assertAlmostEqual(linearizerTableRow[j], lookupTableArray[i, :][j], 

198 places=placesTests) 

199 

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

201 for ampName in self.ampNames: 

202 maskAmp = localDataset.visitMask[ampName] 

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

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

205 linearPart = self.flux*finalTimeVec 

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

207 self.assertEqual(fitType, localDataset.ptcFitType) 

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

209 if fitType == 'POLYNOMIAL': 

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

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

212 if fitType == 'EXPAPPROXIMATION': 

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

214 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

216 

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

218 for ampName in self.ampNames: 

219 maskAmp = localDataset.visitMask[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 

225 # Nonlinearity fit parameters 

226 self.assertLess(np.fabs(linDataset[ampName].meanSignalVsTimePolyFitPars[0]), 0.01) 

227 self.assertAlmostEqual(self.flux, linDataset[ampName].meanSignalVsTimePolyFitPars[1], 

228 places=placesTests) 

229 self.assertAlmostEqual(self.k2NonLinearity, linDataset[ampName].meanSignalVsTimePolyFitPars[2], 

230 places=placesTests) 

231 

232 # Non-linearity coefficient for linearizer 

233 self.assertAlmostEqual(-self.k2NonLinearity/(self.flux**2), 

234 linDataset[ampName].quadraticPolynomialLinearizerCoefficient, 

235 places=placesTests) 

236 

237 linearPartModel = linDataset[ampName].meanSignalVsTimePolyFitPars[1]*finalTimeVec 

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

239 # Fractional nonlinearity residuals 

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

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

242 self.assertAlmostEqual(calc, truth, places=placesTests) 

243 

244 # check calls to calculateLinearityResidualAndLinearizers 

245 datasetLinResAndLinearizers = task.calculateLinearityResidualAndLinearizers( 

246 localDataset.rawExpTimes[ampName], localDataset.rawMeans[ampName]) 

247 

248 self.assertAlmostEqual(-self.k2NonLinearity/(self.flux**2), 

249 datasetLinResAndLinearizers.quadraticPolynomialLinearizerCoefficient, 

250 places=placesTests) 

251 self.assertAlmostEqual(0.0, datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[0], 

252 places=placesTests) 

253 self.assertAlmostEqual(self.flux, datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[1], 

254 places=placesTests) 

255 self.assertAlmostEqual(self.k2NonLinearity, 

256 datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[2], 

257 places=placesTests) 

258 

259 def test_ptcFitBootstrap(self): 

260 """Test the bootstrap fit option for the PTC""" 

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

262 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=False, doFitBootstrap=True) 

263 

264 def test_ptcFit(self): 

265 for createArray in [True, False]: 

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

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

268 

269 def test_meanVarMeasurement(self): 

270 task = self.defaultTask 

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

272 

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

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

275 

276 def test_meanVarMeasurementWithNans(self): 

277 task = self.defaultTask 

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

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

280 

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

282 

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

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

285 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

286 

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

288 im1 = self.flatExp1.maskedImage 

289 im2 = self.flatExp2.maskedImage 

290 

291 temp = im2.clone() 

292 temp *= expectedMu1 

293 diffIm = im1.clone() 

294 diffIm *= expectedMu2 

295 diffIm -= temp 

296 diffIm /= expectedMu 

297 

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

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

300 

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

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

303 self.assertLess(expectedMu - mu, 1) 

304 

305 def test_meanVarMeasurementAllNan(self): 

306 task = self.defaultTask 

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

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

309 

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

311 

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

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

314 self.assertTrue(covDiff is None) 

315 

316 def test_getInitialGoodPoints(self): 

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

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

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

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

321 

322 ys[-1] = 30 

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

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

325 

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

327 newYs = copy.copy(ys) 

328 results = [False, True, True, False, False] 

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

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

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

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

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

334 

335 def test_getVisitsUsed(self): 

336 localDataset = copy.copy(self.dataset) 

337 

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

339 localDataset.inputVisitPairs["C:0,0"].append(pair) 

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

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

342 

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

344 with self.assertRaises(AssertionError): 

345 localDataset.getVisitsUsed("C:0,0") 

346 

347 def test_getGoodAmps(self): 

348 dataset = self.dataset 

349 

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

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

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

353 

354 

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

356 def setUp(self): 

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

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

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

360 

361 def test_generalBehaviour(self): 

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

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

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

365 

366 with self.assertRaises(AttributeError): 

367 test.newItem = 1 

368 

369 

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

371 pass 

372 

373 

374def setup_module(module): 

375 lsst.utils.tests.init() 

376 

377 

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

379 lsst.utils.tests.init() 

380 unittest.main()