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.linearity.maxLookupTableAdu = 200000 # Max ADU in input mock flats 

171 if doTableArray: 

172 config.linearity.linearityType = "LookupTable" 

173 else: 

174 config.linearity.linearityType = "Polynomial" 

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

176 

177 if doTableArray: 

178 # Non-linearity 

179 numberAmps = len(self.ampNames) 

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

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

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

183 linDataset = task.linearity.run(localDataset, 

184 camera=[self.flatExp1.getDetector()], 

185 inputDims={'detector': 0}) 

186 linDataset = linDataset.outputLinearizer 

187 else: 

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

189 linDataset = task.linearity.run(localDataset, 

190 camera=[self.flatExp1.getDetector()], 

191 inputDims={'detector': 0}) 

192 linDataset = linDataset.outputLinearizer 

193 

194 if doTableArray: 

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

196 for i in np.arange(numberAmps): 

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

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

199 signalIdeal = timeRange*self.flux 

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

201 timeRange) 

202 linearizerTableRow = signalIdeal - signalUncorrected 

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

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

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

206 places=placesTests) 

207 else: 

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

209 for ampName in self.ampNames: 

210 maskAmp = localDataset.expIdMask[ampName] 

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

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

213 linearPart = self.flux*finalTimeVec 

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

215 self.assertEqual(fitType, localDataset.ptcFitType) 

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

217 if fitType == 'POLYNOMIAL': 

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

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

220 if fitType == 'EXPAPPROXIMATION': 

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

222 # noise already in electrons for 'EXPAPPROXIMATION' fit 

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

224 

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

226 for ampName in self.ampNames: 

227 maskAmp = localDataset.expIdMask[ampName] 

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

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

230 linearPart = self.flux*finalTimeVec 

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

232 

233 # Nonlinearity fit parameters 

234 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0]) 

235 self.assertAlmostEqual(self.flux, linDataset.fitParams[ampName][1], 

236 places=placesTests) 

237 self.assertAlmostEqual(self.k2NonLinearity, linDataset.fitParams[ampName][2], 

238 places=placesTests) 

239 

240 # Non-linearity coefficient for linearizer 

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

242 linDataset.linearityCoeffs[ampName][0], 

243 places=placesTests) 

244 

245 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec 

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

247 # Fractional nonlinearity residuals 

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

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

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

251 

252 def test_ptcFit(self): 

253 for createArray in [True, False]: 

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

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

256 

257 def test_meanVarMeasurement(self): 

258 task = self.defaultTask 

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

260 

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

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

263 

264 def test_meanVarMeasurementWithNans(self): 

265 task = self.defaultTask 

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

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

268 

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

270 

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

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

273 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

274 

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

276 im1 = self.flatExp1.maskedImage 

277 im2 = self.flatExp2.maskedImage 

278 

279 temp = im2.clone() 

280 temp *= expectedMu1 

281 diffIm = im1.clone() 

282 diffIm *= expectedMu2 

283 diffIm -= temp 

284 diffIm /= expectedMu 

285 

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

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

288 

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

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

291 self.assertLess(expectedMu - mu, 1) 

292 

293 def test_meanVarMeasurementAllNan(self): 

294 task = self.defaultTask 

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

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

297 

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

299 

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

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

302 self.assertTrue(covDiff is None) 

303 

304 def test_getInitialGoodPoints(self): 

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

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

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

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

309 

310 ys[-1] = 30 

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

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

313 

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

315 newYs = copy.copy(ys) 

316 results = [False, True, True, False, False] 

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

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

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

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

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

322 

323 def test_getExpIdsUsed(self): 

324 localDataset = copy.copy(self.dataset) 

325 

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

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

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

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

330 

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

332 with self.assertRaises(AssertionError): 

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

334 

335 def test_getGoodAmps(self): 

336 dataset = self.dataset 

337 

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

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

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

341 

342 

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

344 def setUp(self): 

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

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

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

348 

349 def test_generalBehaviour(self): 

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

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

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

353 

354 with self.assertRaises(AttributeError): 

355 test.newItem = 1 

356 

357 

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

359 pass 

360 

361 

362def setup_module(module): 

363 lsst.utils.tests.init() 

364 

365 

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

367 lsst.utils.tests.init() 

368 unittest.main()