Coverage for tests/test_ptc.py : 10%

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
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."""
27from __future__ import absolute_import, division, print_function
28import unittest
29import numpy as np
30import copy
32import lsst.utils
33import lsst.utils.tests
35import lsst.cp.pipe as cpPipe
36import lsst.ip.isr.isrMock as isrMock
37from lsst.ip.isr import PhotonTransferCurveDataset
38from lsst.cp.pipe.utils import (funcPolynomial, makeMockFlats)
41class FakeCamera(list):
42 def getName(self):
43 return "FakeCam"
46class MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase):
47 """A test case for the PTC task."""
49 def setUp(self):
50 self.defaultConfig = cpPipe.ptc.MeasurePhotonTransferCurveTask.ConfigClass()
51 self.defaultTask = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=self.defaultConfig)
53 self.defaultConfigExtract = cpPipe.ptc.PhotonTransferCurveExtractTask.ConfigClass()
54 self.defaultTaskExtract = cpPipe.ptc.PhotonTransferCurveExtractTask(config=self.defaultConfigExtract)
56 self.defaultConfigSolve = cpPipe.ptc.PhotonTransferCurveSolveTask.ConfigClass()
57 self.defaultTaskSolve = cpPipe.ptc.PhotonTransferCurveSolveTask(config=self.defaultConfigSolve)
59 self.flatMean = 2000
60 self.readNoiseAdu = 10
61 mockImageConfig = isrMock.IsrMock.ConfigClass()
63 # flatDrop is not really relevant as we replace the data
64 # but good to note it in case we change how this image is made
65 mockImageConfig.flatDrop = 0.99999
66 mockImageConfig.isTrimmed = True
68 self.flatExp1 = isrMock.FlatMock(config=mockImageConfig).run()
69 self.flatExp2 = self.flatExp1.clone()
70 (shapeY, shapeX) = self.flatExp1.getDimensions()
72 self.flatWidth = np.sqrt(self.flatMean) + self.readNoiseAdu
74 self.rng1 = np.random.RandomState(1984)
75 flatData1 = self.rng1.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
76 self.rng2 = np.random.RandomState(666)
77 flatData2 = self.rng2.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
79 self.flatExp1.image.array[:] = flatData1
80 self.flatExp2.image.array[:] = flatData2
82 # create fake PTC data to see if fit works, for one amp ('amp')
83 self.flux = 1000. # ADU/sec
84 self.timeVec = np.arange(1., 101., 5)
85 self.k2NonLinearity = -5e-6
86 # quadratic signal-chain non-linearity
87 muVec = self.flux*self.timeVec + self.k2NonLinearity*self.timeVec**2
88 self.gain = 1.5 # e-/ADU
89 self.c1 = 1./self.gain
90 self.noiseSq = 5*self.gain # 7.5 (e-)^2
91 self.a00 = -1.2e-6
92 self.c2 = -1.5e-6
93 self.c3 = -4.7e-12 # tuned so that it turns over for 200k mean
95 self.ampNames = [amp.getName() for amp in self.flatExp1.getDetector().getAmplifiers()]
96 self.dataset = PhotonTransferCurveDataset(self.ampNames, " ") # pack raw data for fitting
98 for ampName in self.ampNames: # just the expTimes and means here - vars vary per function
99 self.dataset.rawExpTimes[ampName] = self.timeVec
100 self.dataset.rawMeans[ampName] = muVec
102 def test_covAstier(self):
103 """Test to check getCovariancesAstier
105 We check that the gain is the same as the imput gain from the mock data, that
106 the covariances via FFT (as it is in MeasurePhotonTransferCurveTask when
107 doCovariancesAstier=True) are the same as calculated in real space, and that
108 Cov[0, 0] (i.e., the variances) are similar to the variances calculated with the standard
109 method (when doCovariancesAstier=false),
110 """
111 task = self.defaultTask
112 extractConfig = self.defaultConfigExtract
113 extractConfig.minNumberGoodPixelsForCovariance = 5000
114 extractConfig.detectorMeasurementRegion = 'FULL'
115 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig)
117 solveConfig = self.defaultConfigSolve
118 solveConfig.ptcFitType = 'FULLCOVARIANCE'
119 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=solveConfig)
121 inputGain = 0.75
123 muStandard, varStandard = {}, {}
124 expDict = {}
125 expIds = []
126 idCounter = 0
127 for expTime in self.timeVec:
128 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain,
129 readNoiseElectrons=3, expId1=idCounter,
130 expId2=idCounter+1)
131 expDict[expTime] = ((mockExp1, idCounter), (mockExp2, idCounter+1))
132 expIds.append(idCounter)
133 expIds.append(idCounter+1)
134 for ampNumber, ampName in enumerate(self.ampNames):
135 # cov has (i, j, var, cov, npix)
136 muDiff, varDiff, covAstier = task.extract.measureMeanVarCov(mockExp1, mockExp2)
137 muStandard.setdefault(ampName, []).append(muDiff)
138 varStandard.setdefault(ampName, []).append(varDiff)
139 # Calculate covariances in an independent way: direct space
140 _, _, covsDirect = task.extract.measureMeanVarCov(mockExp1, mockExp2, covAstierRealSpace=True)
142 # Test that the arrays "covs" (FFT) and "covDirect" (direct space) are the same
143 for row1, row2 in zip(covAstier, covsDirect):
144 for a, b in zip(row1, row2):
145 self.assertAlmostEqual(a, b)
146 idCounter += 2
147 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds)
148 resultsSolve = solveTask.run(resultsExtract.outputCovariances)
150 for amp in self.ampNames:
151 self.assertAlmostEqual(resultsSolve.outputPtcDataset.gain[amp], inputGain, places=2)
152 for v1, v2 in zip(varStandard[amp], resultsSolve.outputPtcDataset.finalVars[amp]):
153 self.assertAlmostEqual(v1/v2, 1.0, places=1)
155 def ptcFitAndCheckPtc(self, order=None, fitType=None, doTableArray=False, doFitBootstrap=False):
156 localDataset = copy.copy(self.dataset)
157 localDataset.ptcFitType = fitType
158 configSolve = copy.copy(self.defaultConfigSolve)
159 configLin = cpPipe.linearity.LinearitySolveTask.ConfigClass()
160 placesTests = 6
161 if doFitBootstrap:
162 configSolve.doFitBootstrap = True
163 # Bootstrap method in cp_pipe/utils.py does multiple fits in the precense of noise.
164 # Allow for more margin of error.
165 placesTests = 3
167 if fitType == 'POLYNOMIAL':
168 if order not in [2, 3]:
169 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
170 if order == 2:
171 for ampName in self.ampNames:
172 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
173 mu in localDataset.rawMeans[ampName]]
174 configSolve.polynomialFitDegree = 2
175 if order == 3:
176 for ampName in self.ampNames:
177 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
178 for mu in localDataset.rawMeans[ampName]]
179 configSolve.polynomialFitDegree = 3
180 elif fitType == 'EXPAPPROXIMATION':
181 g = self.gain
182 for ampName in self.ampNames:
183 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1)
184 + self.noiseSq/(g*g))
185 for mu in localDataset.rawMeans[ampName]]
186 else:
187 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
189 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats
190 configLin.maxLinearAdu = 100000
191 configLin.minLinearAdu = 50000
192 if doTableArray:
193 configLin.linearityType = "LookupTable"
194 else:
195 configLin.linearityType = "Polynomial"
196 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve)
197 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin)
199 if doTableArray:
200 # Non-linearity
201 numberAmps = len(self.ampNames)
202 # localDataset: PTC dataset (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`)
203 localDataset = solveTask.fitPtc(localDataset)
204 # linDataset here is a lsst.pipe.base.Struct
205 linDataset = linearityTask.run(localDataset,
206 dummy=[1.0],
207 camera=FakeCamera([self.flatExp1.getDetector()]),
208 inputDims={'detector': 0})
209 linDataset = linDataset.outputLinearizer
210 else:
211 localDataset = solveTask.fitPtc(localDataset)
212 linDataset = linearityTask.run(localDataset,
213 dummy=[1.0],
214 camera=FakeCamera([self.flatExp1.getDetector()]),
215 inputDims={'detector': 0})
216 linDataset = linDataset.outputLinearizer
217 if doTableArray:
218 # check that the linearizer table has been filled out properly
219 for i in np.arange(numberAmps):
220 tMax = (configLin.maxLookupTableAdu)/self.flux
221 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
222 signalIdeal = timeRange*self.flux
223 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
224 timeRange)
225 linearizerTableRow = signalIdeal - signalUncorrected
226 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
227 for j in np.arange(len(linearizerTableRow)):
228 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
229 places=placesTests)
230 else:
231 # check entries in localDataset, which was modified by the function
232 for ampName in self.ampNames:
233 maskAmp = localDataset.expIdMask[ampName]
234 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
235 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
236 linearPart = self.flux*finalTimeVec
237 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
238 self.assertEqual(fitType, localDataset.ptcFitType)
239 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
240 if fitType == 'POLYNOMIAL':
241 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
242 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
243 if fitType == 'EXPAPPROXIMATION':
244 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
245 # noise already in electrons for 'EXPAPPROXIMATION' fit
246 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
248 # check entries in returned dataset (a dict of , for nonlinearity)
249 for ampName in self.ampNames:
250 maskAmp = localDataset.expIdMask[ampName]
251 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
252 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
253 linearPart = self.flux*finalTimeVec
254 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
256 # Nonlinearity fit parameters
257 # Polynomial fits are now normalized to unit flux scaling
258 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
259 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
260 places=5)
262 # Non-linearity coefficient for linearizer
263 squaredCoeff = self.k2NonLinearity/(self.flux**2)
264 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
265 places=placesTests)
266 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
267 places=placesTests)
269 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
270 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
271 # Fractional nonlinearity residuals
272 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
273 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
274 self.assertAlmostEqual(calc, truth, places=3)
276 def test_ptcFit(self):
277 for createArray in [True, False]:
278 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
279 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
281 def test_meanVarMeasurement(self):
282 task = self.defaultTaskExtract
283 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
285 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
286 self.assertLess(self.flatMean - mu, 1)
288 def test_meanVarMeasurementWithNans(self):
289 task = self.defaultTaskExtract
290 self.flatExp1.image.array[20:30, :] = np.nan
291 self.flatExp2.image.array[20:30, :] = np.nan
293 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
295 expectedMu1 = np.nanmean(self.flatExp1.image.array)
296 expectedMu2 = np.nanmean(self.flatExp2.image.array)
297 expectedMu = 0.5*(expectedMu1 + expectedMu2)
299 # Now the variance of the difference. First, create the diff image.
300 im1 = self.flatExp1.maskedImage
301 im2 = self.flatExp2.maskedImage
303 temp = im2.clone()
304 temp *= expectedMu1
305 diffIm = im1.clone()
306 diffIm *= expectedMu2
307 diffIm -= temp
308 diffIm /= expectedMu
310 # Dive by two as it is what measureMeanVarCov returns (variance of difference)
311 expectedVar = 0.5*np.nanvar(diffIm.image.array)
313 # Check that the standard deviations and the emans agree to less than 1 ADU
314 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
315 self.assertLess(expectedMu - mu, 1)
317 def test_meanVarMeasurementAllNan(self):
318 task = self.defaultTaskExtract
319 self.flatExp1.image.array[:, :] = np.nan
320 self.flatExp2.image.array[:, :] = np.nan
322 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
324 self.assertTrue(np.isnan(mu))
325 self.assertTrue(np.isnan(varDiff))
326 self.assertTrue(covDiff is None)
328 def test_makeZeroSafe(self):
329 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
330 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
331 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
333 substituteValue = 1e-10
335 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
336 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
338 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
339 substituteValue=substituteValue)
340 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
341 substituteValue=substituteValue)
342 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
343 substituteValue=substituteValue)
345 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
346 self.assertEqual(exp, meas)
347 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
348 self.assertEqual(exp, meas)
349 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
350 self.assertEqual(exp, meas)
352 def test_getInitialGoodPoints(self):
353 xs = [1, 2, 3, 4, 5, 6]
354 ys = [2*x for x in xs]
355 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, maxDeviationPositive=0.1,
356 maxDeviationNegative=0.25,
357 minMeanRatioTest=0.,
358 minVarPivotSearch=0.)
359 assert np.all(points) == np.all(np.array([True for x in xs]))
361 ys[-1] = 30
362 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys,
363 maxDeviationPositive=0.1,
364 maxDeviationNegative=0.25,
365 minMeanRatioTest=0.,
366 minVarPivotSearch=0.)
367 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
369 ys = [2*x for x in xs]
370 newYs = copy.copy(ys)
371 results = [False, True, True, False, False]
372 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
373 newYs[-1] = ys[-1] + (factor*ys[-1])
374 points = self.defaultTaskSolve._getInitialGoodPoints(xs, newYs, maxDeviationPositive=0.05,
375 maxDeviationNegative=0.25,
376 minMeanRatioTest=0.0,
377 minVarPivotSearch=0.0)
378 assert (np.all(points[0:-2])) # noqa: E712 - flake8 is wrong here because of numpy.bool
379 assert points[-1] == results[i]
381 def test_getExpIdsUsed(self):
382 localDataset = copy.copy(self.dataset)
384 for pair in [(12, 34), (56, 78), (90, 10)]:
385 localDataset.inputExpIdPairs["C:0,0"].append(pair)
386 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
387 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
389 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
390 with self.assertRaises(AssertionError):
391 localDataset.getExpIdsUsed("C:0,0")
393 def test_getGoodAmps(self):
394 dataset = self.dataset
396 self.assertTrue(dataset.ampNames == self.ampNames)
397 dataset.badAmps.append("C:0,1")
398 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
401class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
402 def setUp(self):
403 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
404 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
405 'C01': [(123, 234), (345, 456), (567, 678)]}
407 def test_generalBehaviour(self):
408 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
409 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
410 'C01': [(123, 234), (345, 456), (567, 678)]}
413class TestMemory(lsst.utils.tests.MemoryTestCase):
414 pass
417def setup_module(module):
418 lsst.utils.tests.init()
421if __name__ == "__main__": 421 ↛ 422line 421 didn't jump to line 422, because the condition on line 421 was never true
422 lsst.utils.tests.init()
423 unittest.main()