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)
134 covFits, covFitsNoB = fitData(covariancesWithTags)
135 localDataset = task.getOutputPtcDataCovAstier(localDataset, covFits, covFitsNoB)
136 # Chek the gain and that the ratio of the variance caclulated via cov Astier (FFT) and
137 # that calculated with the standard PTC calculation (afw) is close to 1.
138 for amp in self.ampNames:
139 self.assertAlmostEqual(localDataset.gain[amp], 0.75, places=2)
140 for v1, v2 in zip(varStandard[amp], localDataset.finalVars[amp]):
141 v2 *= (0.75**2) # convert to electrons
142 self.assertAlmostEqual(v1/v2, 1.0, places=1)
144 def ptcFitAndCheckPtc(self, order=None, fitType='', doTableArray=False, doFitBootstrap=False):
145 localDataset = copy.copy(self.dataset)
146 config = copy.copy(self.defaultConfig)
147 placesTests = 6
148 if doFitBootstrap:
149 config.doFitBootstrap = True
150 # Bootstrap method in cp_pipe/utils.py does multiple fits in the precense of noise.
151 # Allow for more margin of error.
152 placesTests = 3
154 if fitType == 'POLYNOMIAL':
155 if order not in [2, 3]:
156 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
157 if order == 2:
158 for ampName in self.ampNames:
159 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
160 mu in localDataset.rawMeans[ampName]]
161 config.polynomialFitDegree = 2
162 if order == 3:
163 for ampName in self.ampNames:
164 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
165 for mu in localDataset.rawMeans[ampName]]
166 config.polynomialFitDegree = 3
167 elif fitType == 'EXPAPPROXIMATION':
168 g = self.gain
169 for ampName in self.ampNames:
170 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1) +
171 self.noiseSq/(g*g)) for mu in localDataset.rawMeans[ampName]]
172 else:
173 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
175 config.linearity.maxLookupTableAdu = 200000 # Max ADU in input mock flats
176 config.linearity.maxLinearAdu = 100000
177 config.linearity.minLinearAdu = 50000
178 if doTableArray:
179 config.linearity.linearityType = "LookupTable"
180 else:
181 config.linearity.linearityType = "Polynomial"
182 task = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=config)
184 if doTableArray:
185 # Non-linearity
186 numberAmps = len(self.ampNames)
187 # localDataset: PTC dataset (lsst.cp.pipe.ptc.PhotonTransferCurveDataset)
188 localDataset = task.fitPtc(localDataset, ptcFitType=fitType)
189 # linDataset: Dictionary of `lsst.cp.pipe.ptc.LinearityResidualsAndLinearizersDataset`
190 linDataset = task.linearity.run(localDataset,
191 camera=FakeCamera([self.flatExp1.getDetector()]),
192 inputDims={'detector': 0})
193 linDataset = linDataset.outputLinearizer
194 else:
195 localDataset = task.fitPtc(localDataset, ptcFitType=fitType)
196 linDataset = task.linearity.run(localDataset,
197 camera=FakeCamera([self.flatExp1.getDetector()]),
198 inputDims={'detector': 0})
199 linDataset = linDataset.outputLinearizer
201 if doTableArray:
202 # check that the linearizer table has been filled out properly
203 for i in np.arange(numberAmps):
204 tMax = (config.linearity.maxLookupTableAdu)/self.flux
205 timeRange = np.linspace(0., tMax, config.linearity.maxLookupTableAdu)
206 signalIdeal = timeRange*self.flux
207 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
208 timeRange)
209 linearizerTableRow = signalIdeal - signalUncorrected
210 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
211 for j in np.arange(len(linearizerTableRow)):
212 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
213 places=placesTests)
214 else:
215 # check entries in localDataset, which was modified by the function
216 for ampName in self.ampNames:
217 maskAmp = localDataset.expIdMask[ampName]
218 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
219 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
220 linearPart = self.flux*finalTimeVec
221 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
222 self.assertEqual(fitType, localDataset.ptcFitType)
223 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
224 if fitType == 'POLYNOMIAL':
225 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
226 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
227 if fitType == 'EXPAPPROXIMATION':
228 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
229 # noise already in electrons for 'EXPAPPROXIMATION' fit
230 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
232 # check entries in returned dataset (a dict of , for nonlinearity)
233 for ampName in self.ampNames:
234 maskAmp = localDataset.expIdMask[ampName]
235 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
236 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
237 linearPart = self.flux*finalTimeVec
238 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
240 # Nonlinearity fit parameters
241 # Polynomial fits are now normalized to unit flux scaling
242 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
243 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
244 places=5)
246 # Non-linearity coefficient for linearizer
247 squaredCoeff = self.k2NonLinearity/(self.flux**2)
248 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
249 places=placesTests)
250 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
251 places=placesTests)
253 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
254 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
255 # Fractional nonlinearity residuals
256 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
257 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
258 self.assertAlmostEqual(calc, truth, places=3)
260 def test_ptcFit(self):
261 for createArray in [True, False]:
262 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
263 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
265 def test_meanVarMeasurement(self):
266 task = self.defaultTask
267 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
269 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
270 self.assertLess(self.flatMean - mu, 1)
272 def test_meanVarMeasurementWithNans(self):
273 task = self.defaultTask
274 self.flatExp1.image.array[20:30, :] = np.nan
275 self.flatExp2.image.array[20:30, :] = np.nan
277 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
279 expectedMu1 = np.nanmean(self.flatExp1.image.array)
280 expectedMu2 = np.nanmean(self.flatExp2.image.array)
281 expectedMu = 0.5*(expectedMu1 + expectedMu2)
283 # Now the variance of the difference. First, create the diff image.
284 im1 = self.flatExp1.maskedImage
285 im2 = self.flatExp2.maskedImage
287 temp = im2.clone()
288 temp *= expectedMu1
289 diffIm = im1.clone()
290 diffIm *= expectedMu2
291 diffIm -= temp
292 diffIm /= expectedMu
294 # Dive by two as it is what measureMeanVarCov returns (variance of difference)
295 expectedVar = 0.5*np.nanvar(diffIm.image.array)
297 # Check that the standard deviations and the emans agree to less than 1 ADU
298 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
299 self.assertLess(expectedMu - mu, 1)
301 def test_meanVarMeasurementAllNan(self):
302 task = self.defaultTask
303 self.flatExp1.image.array[:, :] = np.nan
304 self.flatExp2.image.array[:, :] = np.nan
306 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
308 self.assertTrue(np.isnan(mu))
309 self.assertTrue(np.isnan(varDiff))
310 self.assertTrue(covDiff is None)
312 def test_getInitialGoodPoints(self):
313 xs = [1, 2, 3, 4, 5, 6]
314 ys = [2*x for x in xs]
315 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25)
316 assert np.all(points) == np.all(np.array([True for x in xs]))
318 ys[-1] = 30
319 points = self.defaultTask._getInitialGoodPoints(xs, ys, 0.1, 0.25)
320 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
322 ys = [2*x for x in xs]
323 newYs = copy.copy(ys)
324 results = [False, True, True, False, False]
325 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
326 newYs[-1] = ys[-1] + (factor*ys[-1])
327 points = self.defaultTask._getInitialGoodPoints(xs, newYs, 0.05, 0.25)
328 assert (np.all(points[0:-1]) == True) # noqa: E712 - flake8 is wrong here because of numpy.bool
329 assert points[-1] == results[i]
331 def test_getExpIdsUsed(self):
332 localDataset = copy.copy(self.dataset)
334 for pair in [(12, 34), (56, 78), (90, 10)]:
335 localDataset.inputExpIdPairs["C:0,0"].append(pair)
336 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
337 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
339 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
340 with self.assertRaises(AssertionError):
341 localDataset.getExpIdsUsed("C:0,0")
343 def test_getGoodAmps(self):
344 dataset = self.dataset
346 self.assertTrue(dataset.ampNames == self.ampNames)
347 dataset.badAmps.append("C:0,1")
348 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
351class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
352 def setUp(self):
353 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
354 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
355 'C01': [(123, 234), (345, 456), (567, 678)]}
357 def test_generalBehaviour(self):
358 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
359 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
360 'C01': [(123, 234), (345, 456), (567, 678)]}
363class TestMemory(lsst.utils.tests.MemoryTestCase):
364 pass
367def setup_module(module):
368 lsst.utils.tests.init()
371if __name__ == "__main__": 371 ↛ 372line 371 didn't jump to line 372, because the condition on line 371 was never true
372 lsst.utils.tests.init()
373 unittest.main()