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.astierCovPtcUtils import fitData
39from lsst.cp.pipe.utils import (funcPolynomial, makeMockFlats)
42class FakeCamera(list):
43 def getName(self):
44 return "FakeCam"
47class MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase):
48 """A test case for the PTC task."""
50 def setUp(self):
51 self.defaultConfig = cpPipe.ptc.MeasurePhotonTransferCurveTask.ConfigClass()
52 self.defaultTask = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=self.defaultConfig)
54 self.flatMean = 2000
55 self.readNoiseAdu = 10
56 mockImageConfig = isrMock.IsrMock.ConfigClass()
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
63 self.flatExp1 = isrMock.FlatMock(config=mockImageConfig).run()
64 self.flatExp2 = self.flatExp1.clone()
65 (shapeY, shapeX) = self.flatExp1.getDimensions()
67 self.flatWidth = np.sqrt(self.flatMean) + self.readNoiseAdu
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))
74 self.flatExp1.image.array[:] = flatData1
75 self.flatExp2.image.array[:] = flatData2
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
89 self.ampNames = [amp.getName() for amp in self.flatExp1.getDetector().getAmplifiers()]
90 self.dataset = PhotonTransferCurveDataset(self.ampNames, " ") # pack raw data for fitting
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
96 def test_covAstier(self):
97 """Test to check getCovariancesAstier
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)
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 = []
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)
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)
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)
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
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'")
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)
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
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])
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
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)
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)
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)
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)
267 def test_meanVarMeasurement(self):
268 task = self.defaultTask
269 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
271 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
272 self.assertLess(self.flatMean - mu, 1)
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
279 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
281 expectedMu1 = np.nanmean(self.flatExp1.image.array)
282 expectedMu2 = np.nanmean(self.flatExp2.image.array)
283 expectedMu = 0.5*(expectedMu1 + expectedMu2)
285 # Now the variance of the difference. First, create the diff image.
286 im1 = self.flatExp1.maskedImage
287 im2 = self.flatExp2.maskedImage
289 temp = im2.clone()
290 temp *= expectedMu1
291 diffIm = im1.clone()
292 diffIm *= expectedMu2
293 diffIm -= temp
294 diffIm /= expectedMu
296 # Dive by two as it is what measureMeanVarCov returns (variance of difference)
297 expectedVar = 0.5*np.nanvar(diffIm.image.array)
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)
303 def test_meanVarMeasurementAllNan(self):
304 task = self.defaultTask
305 self.flatExp1.image.array[:, :] = np.nan
306 self.flatExp2.image.array[:, :] = np.nan
308 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
310 self.assertTrue(np.isnan(mu))
311 self.assertTrue(np.isnan(varDiff))
312 self.assertTrue(covDiff is None)
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, maxDeviationPositive=0.1,
318 maxDeviationNegative=0.25,
319 minMeanRatioTest=0.,
320 minVarPivotSearch=0.)
321 assert np.all(points) == np.all(np.array([True for x in xs]))
323 ys[-1] = 30
324 points = self.defaultTask._getInitialGoodPoints(xs, ys,
325 maxDeviationPositive=0.1,
326 maxDeviationNegative=0.25,
327 minMeanRatioTest=0.,
328 minVarPivotSearch=0.)
329 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
331 ys = [2*x for x in xs]
332 newYs = copy.copy(ys)
333 results = [False, True, True, False, False]
334 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
335 newYs[-1] = ys[-1] + (factor*ys[-1])
336 points = self.defaultTask._getInitialGoodPoints(xs, newYs, maxDeviationPositive=0.05,
337 maxDeviationNegative=0.25,
338 minMeanRatioTest=0.0,
339 minVarPivotSearch=0.0)
340 assert (np.all(points[0:-2])) # noqa: E712 - flake8 is wrong here because of numpy.bool
341 assert points[-1] == results[i]
343 def test_getExpIdsUsed(self):
344 localDataset = copy.copy(self.dataset)
346 for pair in [(12, 34), (56, 78), (90, 10)]:
347 localDataset.inputExpIdPairs["C:0,0"].append(pair)
348 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
349 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
351 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
352 with self.assertRaises(AssertionError):
353 localDataset.getExpIdsUsed("C:0,0")
355 def test_getGoodAmps(self):
356 dataset = self.dataset
358 self.assertTrue(dataset.ampNames == self.ampNames)
359 dataset.badAmps.append("C:0,1")
360 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
363class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
364 def setUp(self):
365 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
366 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
367 'C01': [(123, 234), (345, 456), (567, 678)]}
369 def test_generalBehaviour(self):
370 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
371 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
372 'C01': [(123, 234), (345, 456), (567, 678)]}
375class TestMemory(lsst.utils.tests.MemoryTestCase):
376 pass
379def setup_module(module):
380 lsst.utils.tests.init()
383if __name__ == "__main__": 383 ↛ 384line 383 didn't jump to line 384, because the condition on line 383 was never true
384 lsst.utils.tests.init()
385 unittest.main()