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.)
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.minNumberGoodPixelsForFft = 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, mockExp2)
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)) for mu in localDataset.rawMeans[ampName]]
185 else:
186 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
188 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats
189 configLin.maxLinearAdu = 100000
190 configLin.minLinearAdu = 50000
191 if doTableArray:
192 configLin.linearityType = "LookupTable"
193 else:
194 configLin.linearityType = "Polynomial"
195 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve)
196 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin)
198 if doTableArray:
199 # Non-linearity
200 numberAmps = len(self.ampNames)
201 # localDataset: PTC dataset (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`)
202 localDataset = solveTask.fitPtc(localDataset)
203 # linDataset here is a lsst.pipe.base.Struct
204 linDataset = linearityTask.run(localDataset,
205 camera=FakeCamera([self.flatExp1.getDetector()]),
206 inputDims={'detector': 0})
207 linDataset = linDataset.outputLinearizer
208 else:
209 localDataset = solveTask.fitPtc(localDataset)
210 linDataset = linearityTask.run(localDataset,
211 camera=FakeCamera([self.flatExp1.getDetector()]),
212 inputDims={'detector': 0})
213 linDataset = linDataset.outputLinearizer
214 if doTableArray:
215 # check that the linearizer table has been filled out properly
216 for i in np.arange(numberAmps):
217 tMax = (configLin.maxLookupTableAdu)/self.flux
218 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
219 signalIdeal = timeRange*self.flux
220 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
221 timeRange)
222 linearizerTableRow = signalIdeal - signalUncorrected
223 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
224 for j in np.arange(len(linearizerTableRow)):
225 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
226 places=placesTests)
227 else:
228 # check entries in localDataset, which was modified by the function
229 for ampName in self.ampNames:
230 maskAmp = localDataset.expIdMask[ampName]
231 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
232 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
233 linearPart = self.flux*finalTimeVec
234 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
235 self.assertEqual(fitType, localDataset.ptcFitType)
236 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
237 if fitType == 'POLYNOMIAL':
238 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
239 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
240 if fitType == 'EXPAPPROXIMATION':
241 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
242 # noise already in electrons for 'EXPAPPROXIMATION' fit
243 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
245 # check entries in returned dataset (a dict of , for nonlinearity)
246 for ampName in self.ampNames:
247 maskAmp = localDataset.expIdMask[ampName]
248 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
249 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
250 linearPart = self.flux*finalTimeVec
251 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
253 # Nonlinearity fit parameters
254 # Polynomial fits are now normalized to unit flux scaling
255 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
256 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
257 places=5)
259 # Non-linearity coefficient for linearizer
260 squaredCoeff = self.k2NonLinearity/(self.flux**2)
261 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
262 places=placesTests)
263 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
264 places=placesTests)
266 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
267 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
268 # Fractional nonlinearity residuals
269 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
270 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
271 self.assertAlmostEqual(calc, truth, places=3)
273 def test_ptcFit(self):
274 for createArray in [True, False]:
275 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
276 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
278 def test_meanVarMeasurement(self):
279 task = self.defaultTaskExtract
280 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
282 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
283 self.assertLess(self.flatMean - mu, 1)
285 def test_meanVarMeasurementWithNans(self):
286 task = self.defaultTaskExtract
287 self.flatExp1.image.array[20:30, :] = np.nan
288 self.flatExp2.image.array[20:30, :] = np.nan
290 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
292 expectedMu1 = np.nanmean(self.flatExp1.image.array)
293 expectedMu2 = np.nanmean(self.flatExp2.image.array)
294 expectedMu = 0.5*(expectedMu1 + expectedMu2)
296 # Now the variance of the difference. First, create the diff image.
297 im1 = self.flatExp1.maskedImage
298 im2 = self.flatExp2.maskedImage
300 temp = im2.clone()
301 temp *= expectedMu1
302 diffIm = im1.clone()
303 diffIm *= expectedMu2
304 diffIm -= temp
305 diffIm /= expectedMu
307 # Dive by two as it is what measureMeanVarCov returns (variance of difference)
308 expectedVar = 0.5*np.nanvar(diffIm.image.array)
310 # Check that the standard deviations and the emans agree to less than 1 ADU
311 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
312 self.assertLess(expectedMu - mu, 1)
314 def test_meanVarMeasurementAllNan(self):
315 task = self.defaultTaskExtract
316 self.flatExp1.image.array[:, :] = np.nan
317 self.flatExp2.image.array[:, :] = np.nan
319 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
321 self.assertTrue(np.isnan(mu))
322 self.assertTrue(np.isnan(varDiff))
323 self.assertTrue(covDiff is None)
325 def test_makeZeroSafe(self):
326 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
327 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
328 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
330 substituteValue = 1e-10
332 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
333 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
335 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
336 substituteValue=substituteValue)
337 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
338 substituteValue=substituteValue)
339 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
340 substituteValue=substituteValue)
342 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
343 self.assertEqual(exp, meas)
344 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
345 self.assertEqual(exp, meas)
346 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
347 self.assertEqual(exp, meas)
349 def test_getInitialGoodPoints(self):
350 xs = [1, 2, 3, 4, 5, 6]
351 ys = [2*x for x in xs]
352 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, maxDeviationPositive=0.1,
353 maxDeviationNegative=0.25,
354 minMeanRatioTest=0.,
355 minVarPivotSearch=0.)
356 assert np.all(points) == np.all(np.array([True for x in xs]))
358 ys[-1] = 30
359 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys,
360 maxDeviationPositive=0.1,
361 maxDeviationNegative=0.25,
362 minMeanRatioTest=0.,
363 minVarPivotSearch=0.)
364 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
366 ys = [2*x for x in xs]
367 newYs = copy.copy(ys)
368 results = [False, True, True, False, False]
369 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
370 newYs[-1] = ys[-1] + (factor*ys[-1])
371 points = self.defaultTaskSolve._getInitialGoodPoints(xs, newYs, maxDeviationPositive=0.05,
372 maxDeviationNegative=0.25,
373 minMeanRatioTest=0.0,
374 minVarPivotSearch=0.0)
375 assert (np.all(points[0:-2])) # noqa: E712 - flake8 is wrong here because of numpy.bool
376 assert points[-1] == results[i]
378 def test_getExpIdsUsed(self):
379 localDataset = copy.copy(self.dataset)
381 for pair in [(12, 34), (56, 78), (90, 10)]:
382 localDataset.inputExpIdPairs["C:0,0"].append(pair)
383 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
384 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
386 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
387 with self.assertRaises(AssertionError):
388 localDataset.getExpIdsUsed("C:0,0")
390 def test_getGoodAmps(self):
391 dataset = self.dataset
393 self.assertTrue(dataset.ampNames == self.ampNames)
394 dataset.badAmps.append("C:0,1")
395 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
398class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
399 def setUp(self):
400 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
401 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
402 'C01': [(123, 234), (345, 456), (567, 678)]}
404 def test_generalBehaviour(self):
405 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
406 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
407 'C01': [(123, 234), (345, 456), (567, 678)]}
410class TestMemory(lsst.utils.tests.MemoryTestCase):
411 pass
414def setup_module(module):
415 lsst.utils.tests.init()
418if __name__ == "__main__": 418 ↛ 419line 418 didn't jump to line 419, because the condition on line 418 was never true
419 lsst.utils.tests.init()
420 unittest.main()