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.linearity.maxLookupTableAdu = 200000 # Max ADU in input mock flats
171 if doTableArray:
172 config.linearity.linearityType = "LookupTable"
173 else:
174 config.linearity.linearityType = "Polynomial"
175 task = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=config)
177 if doTableArray:
178 # Non-linearity
179 numberAmps = len(self.ampNames)
180 # localDataset: PTC dataset (lsst.cp.pipe.ptc.PhotonTransferCurveDataset)
181 localDataset = task.fitPtc(localDataset, ptcFitType=fitType)
182 # linDataset: Dictionary of `lsst.cp.pipe.ptc.LinearityResidualsAndLinearizersDataset`
183 linDataset = task.linearity.run(localDataset,
184 camera=[self.flatExp1.getDetector()],
185 inputDims={'detector': 0})
186 linDataset = linDataset.outputLinearizer
187 else:
188 localDataset = task.fitPtc(localDataset, ptcFitType=fitType)
189 linDataset = task.linearity.run(localDataset,
190 camera=[self.flatExp1.getDetector()],
191 inputDims={'detector': 0})
192 linDataset = linDataset.outputLinearizer
194 if doTableArray:
195 # check that the linearizer table has been filled out properly
196 for i in np.arange(numberAmps):
197 tMax = (config.linearity.maxLookupTableAdu)/self.flux
198 timeRange = np.linspace(0., tMax, config.linearity.maxLookupTableAdu)
199 signalIdeal = timeRange*self.flux
200 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
201 timeRange)
202 linearizerTableRow = signalIdeal - signalUncorrected
203 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
204 for j in np.arange(len(linearizerTableRow)):
205 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
206 places=placesTests)
207 else:
208 # check entries in localDataset, which was modified by the function
209 for ampName in self.ampNames:
210 maskAmp = localDataset.expIdMask[ampName]
211 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
212 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
213 linearPart = self.flux*finalTimeVec
214 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
215 self.assertEqual(fitType, localDataset.ptcFitType)
216 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
217 if fitType == 'POLYNOMIAL':
218 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
219 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
220 if fitType == 'EXPAPPROXIMATION':
221 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
222 # noise already in electrons for 'EXPAPPROXIMATION' fit
223 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
225 # check entries in returned dataset (a dict of , for nonlinearity)
226 for ampName in self.ampNames:
227 maskAmp = localDataset.expIdMask[ampName]
228 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
229 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
230 linearPart = self.flux*finalTimeVec
231 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
233 # Nonlinearity fit parameters
234 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0])
235 self.assertAlmostEqual(self.flux, linDataset.fitParams[ampName][1],
236 places=placesTests)
237 self.assertAlmostEqual(self.k2NonLinearity, linDataset.fitParams[ampName][2],
238 places=placesTests)
240 # Non-linearity coefficient for linearizer
241 self.assertAlmostEqual(-self.k2NonLinearity/(self.flux**2),
242 linDataset.linearityCoeffs[ampName][0],
243 places=placesTests)
245 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec
246 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
247 # Fractional nonlinearity residuals
248 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
249 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
250 self.assertAlmostEqual(calc, truth, places=placesTests)
252 def test_ptcFit(self):
253 for createArray in [True, False]:
254 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
255 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
257 def test_meanVarMeasurement(self):
258 task = self.defaultTask
259 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
261 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
262 self.assertLess(self.flatMean - mu, 1)
264 def test_meanVarMeasurementWithNans(self):
265 task = self.defaultTask
266 self.flatExp1.image.array[20:30, :] = np.nan
267 self.flatExp2.image.array[20:30, :] = np.nan
269 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
271 expectedMu1 = np.nanmean(self.flatExp1.image.array)
272 expectedMu2 = np.nanmean(self.flatExp2.image.array)
273 expectedMu = 0.5*(expectedMu1 + expectedMu2)
275 # Now the variance of the difference. First, create the diff image.
276 im1 = self.flatExp1.maskedImage
277 im2 = self.flatExp2.maskedImage
279 temp = im2.clone()
280 temp *= expectedMu1
281 diffIm = im1.clone()
282 diffIm *= expectedMu2
283 diffIm -= temp
284 diffIm /= expectedMu
286 # Dive by two as it is what measureMeanVarCov returns (variance of difference)
287 expectedVar = 0.5*np.nanvar(diffIm.image.array)
289 # Check that the standard deviations and the emans agree to less than 1 ADU
290 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
291 self.assertLess(expectedMu - mu, 1)
293 def test_meanVarMeasurementAllNan(self):
294 task = self.defaultTask
295 self.flatExp1.image.array[:, :] = np.nan
296 self.flatExp2.image.array[:, :] = np.nan
298 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
300 self.assertTrue(np.isnan(mu))
301 self.assertTrue(np.isnan(varDiff))
302 self.assertTrue(covDiff is None)
304 def test_getInitialGoodPoints(self):
305 xs = [1, 2, 3, 4, 5, 6]
306 ys = [2*x for x in xs]
307 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25)
308 assert np.all(points) == np.all(np.array([True for x in xs]))
310 ys[-1] = 30
311 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25)
312 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
314 ys = [2*x for x in xs]
315 newYs = copy.copy(ys)
316 results = [False, True, True, False, False]
317 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
318 newYs[-1] = ys[-1] + (factor*ys[-1])
319 points = self.defaultTask._getInitialGoodPoints(xs, newYs, 0.05, 0.25)
320 assert (np.all(points[0:-1]) == True) # noqa: E712 - flake8 is wrong here because of numpy.bool
321 assert points[-1] == results[i]
323 def test_getExpIdsUsed(self):
324 localDataset = copy.copy(self.dataset)
326 for pair in [(12, 34), (56, 78), (90, 10)]:
327 localDataset.inputExpIdPairs["C:0,0"].append(pair)
328 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
329 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
331 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
332 with self.assertRaises(AssertionError):
333 localDataset.getExpIdsUsed("C:0,0")
335 def test_getGoodAmps(self):
336 dataset = self.dataset
338 self.assertTrue(dataset.ampNames == self.ampNames)
339 dataset.badAmps.append("C:0,1")
340 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
343class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
344 def setUp(self):
345 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
346 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
347 'C01': [(123, 234), (345, 456), (567, 678)]}
349 def test_generalBehaviour(self):
350 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
351 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
352 'C01': [(123, 234), (345, 456), (567, 678)]}
354 with self.assertRaises(AttributeError):
355 test.newItem = 1
358class TestMemory(lsst.utils.tests.MemoryTestCase):
359 pass
362def setup_module(module):
363 lsst.utils.tests.init()
366if __name__ == "__main__": 366 ↛ 367line 366 didn't jump to line 367, because the condition on line 366 was never true
367 lsst.utils.tests.init()
368 unittest.main()