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): 

140 localDataset = copy.copy(self.dataset) 

141 config = copy.copy(self.defaultConfig) 

142 if fitType == 'POLYNOMIAL': 

143 if order not in [2, 3]: 

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

145 if order == 2: 

146 for ampName in self.ampNames: 

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

148 mu in localDataset.rawMeans[ampName]] 

149 config.polynomialFitDegree = 2 

150 if order == 3: 

151 for ampName in self.ampNames: 

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

153 for mu in localDataset.rawMeans[ampName]] 

154 config.polynomialFitDegree = 3 

155 elif fitType == 'EXPAPPROXIMATION': 

156 g = self.gain 

157 for ampName in self.ampNames: 

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

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

160 else: 

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

162 

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

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

165 

166 if doTableArray: 

167 # Non-linearity 

168 numberAmps = len(self.ampNames) 

169 numberAduValues = config.maxAduForLookupTableLinearizer 

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

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

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

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

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

175 else: 

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

177 linDataset = task.fitNonLinearity(localDataset) 

178 

179 if doTableArray: 

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

181 for i in np.arange(numberAmps): 

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

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

184 signalIdeal = timeRange*self.flux 

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

186 timeRange) 

187 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

190 self.assertAlmostEqual(linearizerTableRow[j], lookupTableArray[i, :][j], places=6) 

191 

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

193 for ampName in self.ampNames: 

194 maskAmp = localDataset.visitMask[ampName] 

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

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

197 linearPart = self.flux*finalTimeVec 

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

199 self.assertEqual(fitType, localDataset.ptcFitType) 

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

201 if fitType == 'POLYNOMIAL': 

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

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

204 if fitType == 'EXPAPPROXIMATION': 

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

206 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

208 

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

210 for ampName in self.ampNames: 

211 maskAmp = localDataset.visitMask[ampName] 

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

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

214 linearPart = self.flux*finalTimeVec 

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

216 

217 # Nonlinearity fit parameters 

218 self.assertAlmostEqual(0.0, linDataset[ampName].meanSignalVsTimePolyFitPars[0]) 

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

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

221 

222 # Non-linearity coefficient for linearizer 

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

224 linDataset[ampName].quadraticPolynomialLinearizerCoefficient) 

225 

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

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

228 # Fractional nonlinearity residuals 

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

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

231 self.assertAlmostEqual(calc, truth) 

232 

233 # check calls to calculateLinearityResidualAndLinearizers 

234 datasetLinResAndLinearizers = task.calculateLinearityResidualAndLinearizers( 

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

236 

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

238 datasetLinResAndLinearizers.quadraticPolynomialLinearizerCoefficient) 

239 self.assertAlmostEqual(0.0, datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[0]) 

240 self.assertAlmostEqual(self.flux, datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[1]) 

241 self.assertAlmostEqual(self.k2NonLinearity, 

242 datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[2]) 

243 

244 def test_ptcFit(self): 

245 for createArray in [True, False]: 

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

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

248 

249 def test_meanVarMeasurement(self): 

250 task = self.defaultTask 

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

252 

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

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

255 

256 def test_meanVarMeasurementWithNans(self): 

257 task = self.defaultTask 

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

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

260 

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

262 

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

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

265 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

266 

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

268 im1 = self.flatExp1.maskedImage 

269 im2 = self.flatExp2.maskedImage 

270 

271 temp = im2.clone() 

272 temp *= expectedMu1 

273 diffIm = im1.clone() 

274 diffIm *= expectedMu2 

275 diffIm -= temp 

276 diffIm /= expectedMu 

277 

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

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

280 

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

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

283 self.assertLess(expectedMu - mu, 1) 

284 

285 def test_meanVarMeasurementAllNan(self): 

286 task = self.defaultTask 

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

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

289 

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

291 

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

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

294 self.assertTrue(covDiff is None) 

295 

296 def test_getInitialGoodPoints(self): 

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

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

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

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

301 

302 ys[-1] = 30 

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

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

305 

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

307 newYs = copy.copy(ys) 

308 results = [False, True, True, False, False] 

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

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

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

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

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

314 

315 def test_getVisitsUsed(self): 

316 localDataset = copy.copy(self.dataset) 

317 

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

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

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

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

322 

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

324 with self.assertRaises(AssertionError): 

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

326 

327 def test_getGoodAmps(self): 

328 dataset = self.dataset 

329 

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

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

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

333 

334 

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

336 def setUp(self): 

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

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

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

340 

341 def test_generalBehaviour(self): 

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

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

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

345 

346 with self.assertRaises(AttributeError): 

347 test.newItem = 1 

348 

349 

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

351 pass 

352 

353 

354def setup_module(module): 

355 lsst.utils.tests.init() 

356 

357 

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

359 lsst.utils.tests.init() 

360 unittest.main()