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):
140 localDataset = copy.copy(self.dataset)
141 config = copy.copy(self.defaultConfig)
142 if fitType == 'POLYNOMIAL':
143 if order not in [2, 3]:
144 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
145 if order == 2:
146 for ampName in self.ampNames:
147 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
148 mu in localDataset.rawMeans[ampName]]
149 config.polynomialFitDegree = 2
150 if order == 3:
151 for ampName in self.ampNames:
152 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
153 for mu in localDataset.rawMeans[ampName]]
154 config.polynomialFitDegree = 3
155 elif fitType == 'EXPAPPROXIMATION':
156 g = self.gain
157 for ampName in self.ampNames:
158 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1) +
159 self.noiseSq/(g*g)) for mu in localDataset.rawMeans[ampName]]
160 else:
161 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
163 config.maxAduForLookupTableLinearizer = 200000 # Max ADU in input mock flats
164 task = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=config)
166 if doTableArray:
167 # Non-linearity
168 numberAmps = len(self.ampNames)
169 numberAduValues = config.maxAduForLookupTableLinearizer
170 lookupTableArray = np.zeros((numberAmps, numberAduValues), dtype=np.float32)
171 # localDataset: PTC dataset (lsst.cp.pipe.ptc.PhotonTransferCurveDataset)
172 localDataset = task.fitPtc(localDataset, ptcFitType=fitType)
173 # linDataset: Dictionary of `lsst.cp.pipe.ptc.LinearityResidualsAndLinearizersDataset`
174 linDataset = task.fitNonLinearity(localDataset, tableArray=lookupTableArray)
175 else:
176 localDataset = task.fitPtc(localDataset, ptcFitType=fitType)
177 linDataset = task.fitNonLinearity(localDataset)
179 if doTableArray:
180 # check that the linearizer table has been filled out properly
181 for i in np.arange(numberAmps):
182 tMax = (config.maxAduForLookupTableLinearizer)/self.flux
183 timeRange = np.linspace(0., tMax, config.maxAduForLookupTableLinearizer)
184 signalIdeal = timeRange*self.flux
185 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
186 timeRange)
187 linearizerTableRow = signalIdeal - signalUncorrected
188 self.assertEqual(len(linearizerTableRow), len(lookupTableArray[i, :]))
189 for j in np.arange(len(linearizerTableRow)):
190 self.assertAlmostEqual(linearizerTableRow[j], lookupTableArray[i, :][j], places=6)
192 # check entries in localDataset, which was modified by the function
193 for ampName in self.ampNames:
194 maskAmp = localDataset.visitMask[ampName]
195 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
196 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
197 linearPart = self.flux*finalTimeVec
198 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
199 self.assertEqual(fitType, localDataset.ptcFitType)
200 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
201 if fitType == 'POLYNOMIAL':
202 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
203 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
204 if fitType == 'EXPAPPROXIMATION':
205 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
206 # noise already in electrons for 'EXPAPPROXIMATION' fit
207 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
209 # check entries in returned dataset (a dict of , for nonlinearity)
210 for ampName in self.ampNames:
211 maskAmp = localDataset.visitMask[ampName]
212 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
213 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
214 linearPart = self.flux*finalTimeVec
215 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
217 # Nonlinearity fit parameters
218 self.assertAlmostEqual(0.0, linDataset[ampName].meanSignalVsTimePolyFitPars[0])
219 self.assertAlmostEqual(self.flux, linDataset[ampName].meanSignalVsTimePolyFitPars[1])
220 self.assertAlmostEqual(self.k2NonLinearity, linDataset[ampName].meanSignalVsTimePolyFitPars[2])
222 # Non-linearity coefficient for linearizer
223 self.assertAlmostEqual(-self.k2NonLinearity/(self.flux**2),
224 linDataset[ampName].quadraticPolynomialLinearizerCoefficient)
226 linearPartModel = linDataset[ampName].meanSignalVsTimePolyFitPars[1]*finalTimeVec
227 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
228 # Fractional nonlinearity residuals
229 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
230 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
231 self.assertAlmostEqual(calc, truth)
233 # check calls to calculateLinearityResidualAndLinearizers
234 datasetLinResAndLinearizers = task.calculateLinearityResidualAndLinearizers(
235 localDataset.rawExpTimes[ampName], localDataset.rawMeans[ampName])
237 self.assertAlmostEqual(-self.k2NonLinearity/(self.flux**2),
238 datasetLinResAndLinearizers.quadraticPolynomialLinearizerCoefficient)
239 self.assertAlmostEqual(0.0, datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[0])
240 self.assertAlmostEqual(self.flux, datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[1])
241 self.assertAlmostEqual(self.k2NonLinearity,
242 datasetLinResAndLinearizers.meanSignalVsTimePolyFitPars[2])
244 def test_ptcFit(self):
245 for createArray in [True, False]:
246 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
247 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
249 def test_meanVarMeasurement(self):
250 task = self.defaultTask
251 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
253 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
254 self.assertLess(self.flatMean - mu, 1)
256 def test_meanVarMeasurementWithNans(self):
257 task = self.defaultTask
258 self.flatExp1.image.array[20:30, :] = np.nan
259 self.flatExp2.image.array[20:30, :] = np.nan
261 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
263 expectedMu1 = np.nanmean(self.flatExp1.image.array)
264 expectedMu2 = np.nanmean(self.flatExp2.image.array)
265 expectedMu = 0.5*(expectedMu1 + expectedMu2)
267 # Now the variance of the difference. First, create the diff image.
268 im1 = self.flatExp1.maskedImage
269 im2 = self.flatExp2.maskedImage
271 temp = im2.clone()
272 temp *= expectedMu1
273 diffIm = im1.clone()
274 diffIm *= expectedMu2
275 diffIm -= temp
276 diffIm /= expectedMu
278 # Dive by two as it is what measureMeanVarCov returns (variance of difference)
279 expectedVar = 0.5*np.nanvar(diffIm.image.array)
281 # Check that the standard deviations and the emans agree to less than 1 ADU
282 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
283 self.assertLess(expectedMu - mu, 1)
285 def test_meanVarMeasurementAllNan(self):
286 task = self.defaultTask
287 self.flatExp1.image.array[:, :] = np.nan
288 self.flatExp2.image.array[:, :] = np.nan
290 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
292 self.assertTrue(np.isnan(mu))
293 self.assertTrue(np.isnan(varDiff))
294 self.assertTrue(covDiff is None)
296 def test_getInitialGoodPoints(self):
297 xs = [1, 2, 3, 4, 5, 6]
298 ys = [2*x for x in xs]
299 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25)
300 assert np.all(points) == np.all(np.array([True for x in xs]))
302 ys[-1] = 30
303 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25)
304 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
306 ys = [2*x for x in xs]
307 newYs = copy.copy(ys)
308 results = [False, True, True, False, False]
309 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
310 newYs[-1] = ys[-1] + (factor*ys[-1])
311 points = self.defaultTask._getInitialGoodPoints(xs, newYs, 0.05, 0.25)
312 assert (np.all(points[0:-1]) == True) # noqa: E712 - flake8 is wrong here because of numpy.bool
313 assert points[-1] == results[i]
315 def test_getVisitsUsed(self):
316 localDataset = copy.copy(self.dataset)
318 for pair in [(12, 34), (56, 78), (90, 10)]:
319 localDataset.inputVisitPairs["C:0,0"].append(pair)
320 localDataset.visitMask["C:0,0"] = np.array([True, False, True])
321 self.assertTrue(np.all(localDataset.getVisitsUsed("C:0,0") == [(12, 34), (90, 10)]))
323 localDataset.visitMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
324 with self.assertRaises(AssertionError):
325 localDataset.getVisitsUsed("C:0,0")
327 def test_getGoodAmps(self):
328 dataset = self.dataset
330 self.assertTrue(dataset.ampNames == self.ampNames)
331 dataset.badAmps.append("C:0,1")
332 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
335class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
336 def setUp(self):
337 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
338 self.ptcData.inputVisitPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
339 'C01': [(123, 234), (345, 456), (567, 678)]}
341 def test_generalBehaviour(self):
342 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
343 test.inputVisitPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
344 'C01': [(123, 234), (345, 456), (567, 678)]}
346 with self.assertRaises(AttributeError):
347 test.newItem = 1
350class TestMemory(lsst.utils.tests.MemoryTestCase):
351 pass
354def setup_module(module):
355 lsst.utils.tests.init()
358if __name__ == "__main__": 358 ↛ 359line 358 didn't jump to line 359, because the condition on line 358 was never true
359 lsst.utils.tests.init()
360 unittest.main()