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