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 camera=FakeCamera([self.flatExp1.getDetector()]),
207 inputDims={'detector': 0})
208 linDataset = linDataset.outputLinearizer
209 else:
210 localDataset = solveTask.fitPtc(localDataset)
211 linDataset = linearityTask.run(localDataset,
212 camera=FakeCamera([self.flatExp1.getDetector()]),
213 inputDims={'detector': 0})
214 linDataset = linDataset.outputLinearizer
215 if doTableArray:
216 # check that the linearizer table has been filled out properly
217 for i in np.arange(numberAmps):
218 tMax = (configLin.maxLookupTableAdu)/self.flux
219 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
220 signalIdeal = timeRange*self.flux
221 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
222 timeRange)
223 linearizerTableRow = signalIdeal - signalUncorrected
224 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
225 for j in np.arange(len(linearizerTableRow)):
226 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
227 places=placesTests)
228 else:
229 # check entries in localDataset, which was modified by the function
230 for ampName in self.ampNames:
231 maskAmp = localDataset.expIdMask[ampName]
232 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
233 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
234 linearPart = self.flux*finalTimeVec
235 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
236 self.assertEqual(fitType, localDataset.ptcFitType)
237 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
238 if fitType == 'POLYNOMIAL':
239 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
240 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
241 if fitType == 'EXPAPPROXIMATION':
242 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
243 # noise already in electrons for 'EXPAPPROXIMATION' fit
244 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
246 # check entries in returned dataset (a dict of , for nonlinearity)
247 for ampName in self.ampNames:
248 maskAmp = localDataset.expIdMask[ampName]
249 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
250 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
251 linearPart = self.flux*finalTimeVec
252 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
254 # Nonlinearity fit parameters
255 # Polynomial fits are now normalized to unit flux scaling
256 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
257 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
258 places=5)
260 # Non-linearity coefficient for linearizer
261 squaredCoeff = self.k2NonLinearity/(self.flux**2)
262 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
263 places=placesTests)
264 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
265 places=placesTests)
267 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
268 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
269 # Fractional nonlinearity residuals
270 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
271 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
272 self.assertAlmostEqual(calc, truth, places=3)
274 def test_ptcFit(self):
275 for createArray in [True, False]:
276 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
277 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
279 def test_meanVarMeasurement(self):
280 task = self.defaultTaskExtract
281 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
283 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
284 self.assertLess(self.flatMean - mu, 1)
286 def test_meanVarMeasurementWithNans(self):
287 task = self.defaultTaskExtract
288 self.flatExp1.image.array[20:30, :] = np.nan
289 self.flatExp2.image.array[20:30, :] = np.nan
291 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
293 expectedMu1 = np.nanmean(self.flatExp1.image.array)
294 expectedMu2 = np.nanmean(self.flatExp2.image.array)
295 expectedMu = 0.5*(expectedMu1 + expectedMu2)
297 # Now the variance of the difference. First, create the diff image.
298 im1 = self.flatExp1.maskedImage
299 im2 = self.flatExp2.maskedImage
301 temp = im2.clone()
302 temp *= expectedMu1
303 diffIm = im1.clone()
304 diffIm *= expectedMu2
305 diffIm -= temp
306 diffIm /= expectedMu
308 # Dive by two as it is what measureMeanVarCov returns (variance of difference)
309 expectedVar = 0.5*np.nanvar(diffIm.image.array)
311 # Check that the standard deviations and the emans agree to less than 1 ADU
312 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
313 self.assertLess(expectedMu - mu, 1)
315 def test_meanVarMeasurementAllNan(self):
316 task = self.defaultTaskExtract
317 self.flatExp1.image.array[:, :] = np.nan
318 self.flatExp2.image.array[:, :] = np.nan
320 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
322 self.assertTrue(np.isnan(mu))
323 self.assertTrue(np.isnan(varDiff))
324 self.assertTrue(covDiff is None)
326 def test_makeZeroSafe(self):
327 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
328 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
329 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
331 substituteValue = 1e-10
333 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
334 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
336 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
337 substituteValue=substituteValue)
338 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
339 substituteValue=substituteValue)
340 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
341 substituteValue=substituteValue)
343 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
344 self.assertEqual(exp, meas)
345 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
346 self.assertEqual(exp, meas)
347 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
348 self.assertEqual(exp, meas)
350 def test_getInitialGoodPoints(self):
351 xs = [1, 2, 3, 4, 5, 6]
352 ys = [2*x for x in xs]
353 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, maxDeviationPositive=0.1,
354 maxDeviationNegative=0.25,
355 minMeanRatioTest=0.,
356 minVarPivotSearch=0.)
357 assert np.all(points) == np.all(np.array([True for x in xs]))
359 ys[-1] = 30
360 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys,
361 maxDeviationPositive=0.1,
362 maxDeviationNegative=0.25,
363 minMeanRatioTest=0.,
364 minVarPivotSearch=0.)
365 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
367 ys = [2*x for x in xs]
368 newYs = copy.copy(ys)
369 results = [False, True, True, False, False]
370 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
371 newYs[-1] = ys[-1] + (factor*ys[-1])
372 points = self.defaultTaskSolve._getInitialGoodPoints(xs, newYs, maxDeviationPositive=0.05,
373 maxDeviationNegative=0.25,
374 minMeanRatioTest=0.0,
375 minVarPivotSearch=0.0)
376 assert (np.all(points[0:-2])) # noqa: E712 - flake8 is wrong here because of numpy.bool
377 assert points[-1] == results[i]
379 def test_getExpIdsUsed(self):
380 localDataset = copy.copy(self.dataset)
382 for pair in [(12, 34), (56, 78), (90, 10)]:
383 localDataset.inputExpIdPairs["C:0,0"].append(pair)
384 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
385 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
387 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
388 with self.assertRaises(AssertionError):
389 localDataset.getExpIdsUsed("C:0,0")
391 def test_getGoodAmps(self):
392 dataset = self.dataset
394 self.assertTrue(dataset.ampNames == self.ampNames)
395 dataset.badAmps.append("C:0,1")
396 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
399class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
400 def setUp(self):
401 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
402 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
403 'C01': [(123, 234), (345, 456), (567, 678)]}
405 def test_generalBehaviour(self):
406 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
407 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
408 'C01': [(123, 234), (345, 456), (567, 678)]}
411class TestMemory(lsst.utils.tests.MemoryTestCase):
412 pass
415def setup_module(module):
416 lsst.utils.tests.init()
419if __name__ == "__main__": 419 ↛ 420line 419 didn't jump to line 420, because the condition on line 419 was never true
420 lsst.utils.tests.init()
421 unittest.main()