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.cp.pipe.ptc import PhotonTransferCurveDataset
38from lsst.cp.pipe.astierCovPtcUtils import fitData
39from lsst.cp.pipe.utils import (funcPolynomial, makeMockFlats)
42class MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase):
43 """A test case for the PTC task."""
45 def setUp(self):
46 self.defaultConfig = cpPipe.ptc.MeasurePhotonTransferCurveTask.ConfigClass()
47 self.defaultTask = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=self.defaultConfig)
49 self.flatMean = 2000
50 self.readNoiseAdu = 10
51 mockImageConfig = isrMock.IsrMock.ConfigClass()
53 # flatDrop is not really relevant as we replace the data
54 # but good to note it in case we change how this image is made
55 mockImageConfig.flatDrop = 0.99999
56 mockImageConfig.isTrimmed = True
58 self.flatExp1 = isrMock.FlatMock(config=mockImageConfig).run()
59 self.flatExp2 = self.flatExp1.clone()
60 (shapeY, shapeX) = self.flatExp1.getDimensions()
62 self.flatWidth = np.sqrt(self.flatMean) + self.readNoiseAdu
64 self.rng1 = np.random.RandomState(1984)
65 flatData1 = self.rng1.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
66 self.rng2 = np.random.RandomState(666)
67 flatData2 = self.rng2.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
69 self.flatExp1.image.array[:] = flatData1
70 self.flatExp2.image.array[:] = flatData2
72 # create fake PTC data to see if fit works, for one amp ('amp')
73 self.flux = 1000. # ADU/sec
74 timeVec = np.arange(1., 201.)
75 self.k2NonLinearity = -5e-6
76 muVec = self.flux*timeVec + self.k2NonLinearity*timeVec**2 # quadratic signal-chain non-linearity
77 self.gain = 1.5 # e-/ADU
78 self.c1 = 1./self.gain
79 self.noiseSq = 5*self.gain # 7.5 (e-)^2
80 self.a00 = -1.2e-6
81 self.c2 = -1.5e-6
82 self.c3 = -4.7e-12 # tuned so that it turns over for 200k mean
84 self.ampNames = [amp.getName() for amp in self.flatExp1.getDetector().getAmplifiers()]
85 self.dataset = PhotonTransferCurveDataset(self.ampNames, " ") # pack raw data for fitting
87 for ampName in self.ampNames: # just the expTimes and means here - vars vary per function
88 self.dataset.rawExpTimes[ampName] = timeVec
89 self.dataset.rawMeans[ampName] = muVec
91 def test_covAstier(self):
92 """Test to check getCovariancesAstier
94 We check that the gain is the same as the imput gain from the mock data, that
95 the covariances via FFT (as it is in MeasurePhotonTransferCurveTask when
96 doCovariancesAstier=True) are the same as calculated in real space, and that
97 Cov[0, 0] (i.e., the variances) are similar to the variances calculated with the standard
98 method (when doCovariancesAstier=false),
99 """
100 localDataset = copy.copy(self.dataset)
101 config = copy.copy(self.defaultConfig)
102 task = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=config)
104 expTimes = np.arange(5, 170, 5)
105 tupleRecords = []
106 allTags = []
107 muStandard, varStandard = {}, {}
108 for expTime in expTimes:
109 mockExp1, mockExp2 = makeMockFlats(expTime, gain=0.75)
110 tupleRows = []
112 for ampNumber, amp in enumerate(self.ampNames):
113 # cov has (i, j, var, cov, npix)
114 muDiff, varDiff, covAstier = task.measureMeanVarCov(mockExp1, mockExp2)
115 muStandard.setdefault(amp, []).append(muDiff)
116 varStandard.setdefault(amp, []).append(varDiff)
117 # Calculate covariances in an independent way: direct space
118 _, _, covsDirect = task.measureMeanVarCov(mockExp1, mockExp2, covAstierRealSpace=True)
120 # Test that the arrays "covs" (FFT) and "covDirect" (direct space) are the same
121 for row1, row2 in zip(covAstier, covsDirect):
122 for a, b in zip(row1, row2):
123 self.assertAlmostEqual(a, b)
124 tupleRows += [(muDiff, ) + covRow + (ampNumber, expTime, amp) for covRow in covAstier]
125 tags = ['mu', 'i', 'j', 'var', 'cov', 'npix', 'ext', 'expTime', 'ampName']
126 allTags += tags
127 tupleRecords += tupleRows
128 covariancesWithTags = np.core.records.fromrecords(tupleRecords, names=allTags)
129 covFits, _ = fitData(covariancesWithTags)
130 localDataset = task.getOutputPtcDataCovAstier(localDataset, covFits)
131 # Chek the gain and that the ratio of the variance caclulated via cov Astier (FFT) and
132 # that calculated with the standard PTC calculation (afw) is close to 1.
133 for amp in self.ampNames:
134 self.assertAlmostEqual(localDataset.gain[amp], 0.75, places=2)
135 for v1, v2 in zip(varStandard[amp], localDataset.finalVars[amp][0]):
136 v2 *= (0.75**2) # convert to electrons
137 self.assertAlmostEqual(v1/v2, 1.0, places=1)
139 def ptcFitAndCheckPtc(self, order=None, fitType='', doTableArray=False, doFitBootstrap=False):
140 localDataset = copy.copy(self.dataset)
141 config = copy.copy(self.defaultConfig)
142 placesTests = 6
143 if doFitBootstrap:
144 config.doFitBootstrap = True
145 # Bootstrap method in cp_pipe/utils.py does multiple fits in the precense of noise.
146 # Allow for more margin of error.
147 placesTests = 3
149 if fitType == 'POLYNOMIAL':
150 if order not in [2, 3]:
151 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
152 if order == 2:
153 for ampName in self.ampNames:
154 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
155 mu in localDataset.rawMeans[ampName]]
156 config.polynomialFitDegree = 2
157 if order == 3:
158 for ampName in self.ampNames:
159 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
160 for mu in localDataset.rawMeans[ampName]]
161 config.polynomialFitDegree = 3
162 elif fitType == 'EXPAPPROXIMATION':
163 g = self.gain
164 for ampName in self.ampNames:
165 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1) +
166 self.noiseSq/(g*g)) for mu in localDataset.rawMeans[ampName]]
167 else:
168 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
170 config.maxAduForLookupTableLinearizer = 200000 # Max ADU in input mock flats
171 task = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=config)
173 if doTableArray:
174 # Non-linearity
175 numberAmps = len(self.ampNames)
176 numberAduValues = config.maxAduForLookupTableLinearizer
177 lookupTableArray = np.zeros((numberAmps, numberAduValues), dtype=np.float32)
178 # localDataset: PTC dataset (lsst.cp.pipe.ptc.PhotonTransferCurveDataset)
179 localDataset = task.fitPtc(localDataset, ptcFitType=fitType)
180 # linDataset: Dictionary of `lsst.cp.pipe.ptc.LinearityResidualsAndLinearizersDataset`
181 linDataset = task.fitNonLinearity(localDataset, tableArray=lookupTableArray)
182 else:
183 localDataset = task.fitPtc(localDataset, ptcFitType=fitType)
184 linDataset = task.fitNonLinearity(localDataset)
186 if doTableArray:
187 # check that the linearizer table has been filled out properly
188 for i in np.arange(numberAmps):
189 tMax = (config.maxAduForLookupTableLinearizer)/self.flux
190 timeRange = np.linspace(0., tMax, config.maxAduForLookupTableLinearizer)
191 signalIdeal = timeRange*self.flux
192 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
193 timeRange)
194 linearizerTableRow = signalIdeal - signalUncorrected
195 self.assertEqual(len(linearizerTableRow), len(lookupTableArray[i, :]))
196 for j in np.arange(len(linearizerTableRow)):
197 self.assertAlmostEqual(linearizerTableRow[j], lookupTableArray[i, :][j],
198 places=placesTests)
200 # check entries in localDataset, which was modified by the function
201 for ampName in self.ampNames:
202 maskAmp = localDataset.visitMask[ampName]
203 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
204 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
205 linearPart = self.flux*finalTimeVec
206 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
207 self.assertEqual(fitType, localDataset.ptcFitType)
208 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
209 if fitType == 'POLYNOMIAL':
210 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
211 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
212 if fitType == 'EXPAPPROXIMATION':
213 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
214 # noise already in electrons for 'EXPAPPROXIMATION' fit
215 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
217 # check entries in returned dataset (a dict of , for nonlinearity)
218 for ampName in self.ampNames:
219 maskAmp = localDataset.visitMask[ampName]
220 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
221 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
222 linearPart = self.flux*finalTimeVec
223 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
225 # Nonlinearity fit parameters
226 self.assertLess(np.fabs(linDataset[ampName].meanSignalVsTimePolyFitPars[0]), 0.01)
227 self.assertAlmostEqual(self.flux, linDataset[ampName].meanSignalVsTimePolyFitPars[1],
228 places=placesTests)
229 self.assertAlmostEqual(self.k2NonLinearity, linDataset[ampName].meanSignalVsTimePolyFitPars[2],
230 places=placesTests)
232 # Non-linearity coefficient for linearizer
233 self.assertAlmostEqual(-self.k2NonLinearity/(self.flux**2),
234 linDataset[ampName].quadraticPolynomialLinearizerCoefficient,
235 places=placesTests)
237 linearPartModel = linDataset[ampName].meanSignalVsTimePolyFitPars[1]*finalTimeVec
238 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
239 # Fractional nonlinearity residuals
240 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
241 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
242 self.assertAlmostEqual(calc, truth, places=placesTests)
244 # check calls to calculateLinearityResidualAndLinearizers
245 datasetLinResAndLinearizers = task.calculateLinearityResidualAndLinearizers(
246 localDataset.rawExpTimes[ampName], localDataset.rawMeans[ampName])
248 self.assertAlmostEqual(-self.k2NonLinearity/(self.flux**2),
249 datasetLinResAndLinearizers.quadraticPolynomialLinearizerCoefficient,
250 places=placesTests)
251 self.assertAlmostEqual(0.0, datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[0],
252 places=placesTests)
253 self.assertAlmostEqual(self.flux, datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[1],
254 places=placesTests)
255 self.assertAlmostEqual(self.k2NonLinearity,
256 datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[2],
257 places=placesTests)
259 def test_ptcFitBootstrap(self):
260 """Test the bootstrap fit option for the PTC"""
261 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
262 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=False, doFitBootstrap=True)
264 def test_ptcFit(self):
265 for createArray in [True, False]:
266 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
267 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
269 def test_meanVarMeasurement(self):
270 task = self.defaultTask
271 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
273 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
274 self.assertLess(self.flatMean - mu, 1)
276 def test_meanVarMeasurementWithNans(self):
277 task = self.defaultTask
278 self.flatExp1.image.array[20:30, :] = np.nan
279 self.flatExp2.image.array[20:30, :] = np.nan
281 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
283 expectedMu1 = np.nanmean(self.flatExp1.image.array)
284 expectedMu2 = np.nanmean(self.flatExp2.image.array)
285 expectedMu = 0.5*(expectedMu1 + expectedMu2)
287 # Now the variance of the difference. First, create the diff image.
288 im1 = self.flatExp1.maskedImage
289 im2 = self.flatExp2.maskedImage
291 temp = im2.clone()
292 temp *= expectedMu1
293 diffIm = im1.clone()
294 diffIm *= expectedMu2
295 diffIm -= temp
296 diffIm /= expectedMu
298 # Dive by two as it is what measureMeanVarCov returns (variance of difference)
299 expectedVar = 0.5*np.nanvar(diffIm.image.array)
301 # Check that the standard deviations and the emans agree to less than 1 ADU
302 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
303 self.assertLess(expectedMu - mu, 1)
305 def test_meanVarMeasurementAllNan(self):
306 task = self.defaultTask
307 self.flatExp1.image.array[:, :] = np.nan
308 self.flatExp2.image.array[:, :] = np.nan
310 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
312 self.assertTrue(np.isnan(mu))
313 self.assertTrue(np.isnan(varDiff))
314 self.assertTrue(covDiff is None)
316 def test_getInitialGoodPoints(self):
317 xs = [1, 2, 3, 4, 5, 6]
318 ys = [2*x for x in xs]
319 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25)
320 assert np.all(points) == np.all(np.array([True for x in xs]))
322 ys[-1] = 30
323 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25)
324 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
326 ys = [2*x for x in xs]
327 newYs = copy.copy(ys)
328 results = [False, True, True, False, False]
329 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
330 newYs[-1] = ys[-1] + (factor*ys[-1])
331 points = self.defaultTask._getInitialGoodPoints(xs, newYs, 0.05, 0.25)
332 assert (np.all(points[0:-1]) == True) # noqa: E712 - flake8 is wrong here because of numpy.bool
333 assert points[-1] == results[i]
335 def test_getVisitsUsed(self):
336 localDataset = copy.copy(self.dataset)
338 for pair in [(12, 34), (56, 78), (90, 10)]:
339 localDataset.inputVisitPairs["C:0,0"].append(pair)
340 localDataset.visitMask["C:0,0"] = np.array([True, False, True])
341 self.assertTrue(np.all(localDataset.getVisitsUsed("C:0,0") == [(12, 34), (90, 10)]))
343 localDataset.visitMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
344 with self.assertRaises(AssertionError):
345 localDataset.getVisitsUsed("C:0,0")
347 def test_getGoodAmps(self):
348 dataset = self.dataset
350 self.assertTrue(dataset.ampNames == self.ampNames)
351 dataset.badAmps.append("C:0,1")
352 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
355class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
356 def setUp(self):
357 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
358 self.ptcData.inputVisitPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
359 'C01': [(123, 234), (345, 456), (567, 678)]}
361 def test_generalBehaviour(self):
362 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
363 test.inputVisitPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
364 'C01': [(123, 234), (345, 456), (567, 678)]}
366 with self.assertRaises(AttributeError):
367 test.newItem = 1
370class TestMemory(lsst.utils.tests.MemoryTestCase):
371 pass
374def setup_module(module):
375 lsst.utils.tests.init()
378if __name__ == "__main__": 378 ↛ 379line 378 didn't jump to line 379, because the condition on line 378 was never true
379 lsst.utils.tests.init()
380 unittest.main()