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, 0.1, 0.25)
318 assert np.all(points) == np.all(np.array([True for x in xs]))
320 ys[-1] = 30
321 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25)
322 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
324 ys = [2*x for x in xs]
325 newYs = copy.copy(ys)
326 results = [False, True, True, False, False]
327 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
328 newYs[-1] = ys[-1] + (factor*ys[-1])
329 points = self.defaultTask._getInitialGoodPoints(xs, newYs, 0.05, 0.25)
330 assert (np.all(points[0:-2])) # noqa: E712 - flake8 is wrong here because of numpy.bool
331 assert points[-1] == results[i]
333 def test_getExpIdsUsed(self):
334 localDataset = copy.copy(self.dataset)
336 for pair in [(12, 34), (56, 78), (90, 10)]:
337 localDataset.inputExpIdPairs["C:0,0"].append(pair)
338 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
339 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
341 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
342 with self.assertRaises(AssertionError):
343 localDataset.getExpIdsUsed("C:0,0")
345 def test_getGoodAmps(self):
346 dataset = self.dataset
348 self.assertTrue(dataset.ampNames == self.ampNames)
349 dataset.badAmps.append("C:0,1")
350 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
353class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
354 def setUp(self):
355 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
356 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
357 'C01': [(123, 234), (345, 456), (567, 678)]}
359 def test_generalBehaviour(self):
360 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
361 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
362 'C01': [(123, 234), (345, 456), (567, 678)]}
365class TestMemory(lsst.utils.tests.MemoryTestCase):
366 pass
369def setup_module(module):
370 lsst.utils.tests.init()
373if __name__ == "__main__": 373 ↛ 374line 373 didn't jump to line 374, because the condition on line 373 was never true
374 lsst.utils.tests.init()
375 unittest.main()