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, 0.1, 0.25) 

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

319 

320 ys[-1] = 30 

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

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

323 

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

325 newYs = copy.copy(ys) 

326 results = [False, True, True, False, False] 

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

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

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

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

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

332 

333 def test_getExpIdsUsed(self): 

334 localDataset = copy.copy(self.dataset) 

335 

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

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

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

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

340 

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

342 with self.assertRaises(AssertionError): 

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

344 

345 def test_getGoodAmps(self): 

346 dataset = self.dataset 

347 

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

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

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

351 

352 

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

354 def setUp(self): 

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

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

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

358 

359 def test_generalBehaviour(self): 

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

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

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

363 

364 

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

366 pass 

367 

368 

369def setup_module(module): 

370 lsst.utils.tests.init() 

371 

372 

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

374 lsst.utils.tests.init() 

375 unittest.main()