Coverage for tests/test_ptc.py: 13%
Shortcuts 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
Shortcuts 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.utils import (funcPolynomial, makeMockFlats)
41class FakeCamera(list):
42 def getName(self):
43 return "FakeCam"
46class PretendRef():
47 "A class to act as a mock exposure reference"
48 def __init__(self, exposure):
49 self.exp = exposure
51 def get(self, component=None):
52 if component == 'visitInfo':
53 return self.exp.getVisitInfo()
54 elif component == 'detector':
55 return self.exp.getDetector()
56 else:
57 return self.exp
60class MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase):
61 """A test case for the PTC task."""
63 def setUp(self):
64 self.defaultConfig = cpPipe.ptc.MeasurePhotonTransferCurveTask.ConfigClass()
65 self.defaultTask = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=self.defaultConfig)
67 self.defaultConfigExtract = cpPipe.ptc.PhotonTransferCurveExtractTask.ConfigClass()
68 self.defaultTaskExtract = cpPipe.ptc.PhotonTransferCurveExtractTask(config=self.defaultConfigExtract)
70 self.defaultConfigSolve = cpPipe.ptc.PhotonTransferCurveSolveTask.ConfigClass()
71 self.defaultTaskSolve = cpPipe.ptc.PhotonTransferCurveSolveTask(config=self.defaultConfigSolve)
73 self.flatMean = 2000
74 self.readNoiseAdu = 10
75 mockImageConfig = isrMock.IsrMock.ConfigClass()
77 # flatDrop is not really relevant as we replace the data
78 # but good to note it in case we change how this image is made
79 mockImageConfig.flatDrop = 0.99999
80 mockImageConfig.isTrimmed = True
82 self.flatExp1 = isrMock.FlatMock(config=mockImageConfig).run()
83 self.flatExp2 = self.flatExp1.clone()
84 (shapeY, shapeX) = self.flatExp1.getDimensions()
86 self.flatWidth = np.sqrt(self.flatMean) + self.readNoiseAdu
88 self.rng1 = np.random.RandomState(1984)
89 flatData1 = self.rng1.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
90 self.rng2 = np.random.RandomState(666)
91 flatData2 = self.rng2.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
93 self.flatExp1.image.array[:] = flatData1
94 self.flatExp2.image.array[:] = flatData2
96 # create fake PTC data to see if fit works, for one amp ('amp')
97 self.flux = 1000. # ADU/sec
98 self.timeVec = np.arange(1., 101., 5)
99 self.k2NonLinearity = -5e-6
100 # quadratic signal-chain non-linearity
101 muVec = self.flux*self.timeVec + self.k2NonLinearity*self.timeVec**2
102 self.gain = 1.5 # e-/ADU
103 self.c1 = 1./self.gain
104 self.noiseSq = 5*self.gain # 7.5 (e-)^2
105 self.a00 = -1.2e-6
106 self.c2 = -1.5e-6
107 self.c3 = -4.7e-12 # tuned so that it turns over for 200k mean
109 self.ampNames = [amp.getName() for amp in self.flatExp1.getDetector().getAmplifiers()]
110 self.dataset = PhotonTransferCurveDataset(self.ampNames, " ") # pack raw data for fitting
112 for ampName in self.ampNames: # just the expTimes and means here - vars vary per function
113 self.dataset.rawExpTimes[ampName] = self.timeVec
114 self.dataset.rawMeans[ampName] = muVec
116 def test_covAstier(self):
117 """Test to check getCovariancesAstier
119 We check that the gain is the same as the imput gain from the
120 mock data, that the covariances via FFT (as it is in
121 MeasurePhotonTransferCurveTask when doCovariancesAstier=True)
122 are the same as calculated in real space, and that Cov[0, 0]
123 (i.e., the variances) are similar to the variances calculated
124 with the standard method (when doCovariancesAstier=false),
126 """
127 task = self.defaultTask
128 extractConfig = self.defaultConfigExtract
129 extractConfig.minNumberGoodPixelsForCovariance = 5000
130 extractConfig.detectorMeasurementRegion = 'FULL'
131 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig)
133 solveConfig = self.defaultConfigSolve
134 solveConfig.ptcFitType = 'FULLCOVARIANCE'
135 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=solveConfig)
137 inputGain = 0.75
139 muStandard, varStandard = {}, {}
140 expDict = {}
141 expIds = []
142 idCounter = 0
143 for expTime in self.timeVec:
144 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain,
145 readNoiseElectrons=3, expId1=idCounter,
146 expId2=idCounter+1)
147 mockExpRef1 = PretendRef(mockExp1)
148 mockExpRef2 = PretendRef(mockExp2)
149 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1))
150 expIds.append(idCounter)
151 expIds.append(idCounter+1)
152 for ampNumber, ampName in enumerate(self.ampNames):
153 # cov has (i, j, var, cov, npix)
154 muDiff, varDiff, covAstier = task.extract.measureMeanVarCov(mockExp1, mockExp2)
155 muStandard.setdefault(ampName, []).append(muDiff)
156 varStandard.setdefault(ampName, []).append(varDiff)
157 idCounter += 2
158 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds)
159 resultsSolve = solveTask.run(resultsExtract.outputCovariances)
161 for amp in self.ampNames:
162 self.assertAlmostEqual(resultsSolve.outputPtcDataset.gain[amp], inputGain, places=2)
163 for v1, v2 in zip(varStandard[amp], resultsSolve.outputPtcDataset.finalVars[amp]):
164 self.assertAlmostEqual(v1/v2, 1.0, places=1)
166 def ptcFitAndCheckPtc(self, order=None, fitType=None, doTableArray=False, doFitBootstrap=False):
167 localDataset = copy.copy(self.dataset)
168 localDataset.ptcFitType = fitType
169 configSolve = copy.copy(self.defaultConfigSolve)
170 configLin = cpPipe.linearity.LinearitySolveTask.ConfigClass()
171 placesTests = 6
172 if doFitBootstrap:
173 configSolve.doFitBootstrap = True
174 # Bootstrap method in cp_pipe/utils.py does multiple fits
175 # in the precense of noise. Allow for more margin of
176 # error.
177 placesTests = 3
179 if fitType == 'POLYNOMIAL':
180 if order not in [2, 3]:
181 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
182 if order == 2:
183 for ampName in self.ampNames:
184 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
185 mu in localDataset.rawMeans[ampName]]
186 configSolve.polynomialFitDegree = 2
187 if order == 3:
188 for ampName in self.ampNames:
189 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
190 for mu in localDataset.rawMeans[ampName]]
191 configSolve.polynomialFitDegree = 3
192 elif fitType == 'EXPAPPROXIMATION':
193 g = self.gain
194 for ampName in self.ampNames:
195 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1)
196 + self.noiseSq/(g*g))
197 for mu in localDataset.rawMeans[ampName]]
198 else:
199 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
201 for ampName in self.ampNames:
202 localDataset.expIdMask[ampName] = np.repeat(True, len(localDataset.rawMeans[ampName]))
203 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats
204 configLin.maxLinearAdu = 100000
205 configLin.minLinearAdu = 50000
206 if doTableArray:
207 configLin.linearityType = "LookupTable"
208 else:
209 configLin.linearityType = "Polynomial"
210 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve)
211 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin)
213 if doTableArray:
214 # Non-linearity
215 numberAmps = len(self.ampNames)
216 # localDataset: PTC dataset
217 # (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`)
218 localDataset = solveTask.fitMeasurementsToModel(localDataset)
219 # linDataset here is a lsst.pipe.base.Struct
220 linDataset = linearityTask.run(localDataset,
221 dummy=[1.0],
222 camera=FakeCamera([self.flatExp1.getDetector()]),
223 inputDims={'detector': 0})
224 linDataset = linDataset.outputLinearizer
225 else:
226 localDataset = solveTask.fitMeasurementsToModel(localDataset)
227 linDataset = linearityTask.run(localDataset,
228 dummy=[1.0],
229 camera=FakeCamera([self.flatExp1.getDetector()]),
230 inputDims={'detector': 0})
231 linDataset = linDataset.outputLinearizer
232 if doTableArray:
233 # check that the linearizer table has been filled out properly
234 for i in np.arange(numberAmps):
235 tMax = (configLin.maxLookupTableAdu)/self.flux
236 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
237 signalIdeal = timeRange*self.flux
238 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
239 timeRange)
240 linearizerTableRow = signalIdeal - signalUncorrected
241 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
242 for j in np.arange(len(linearizerTableRow)):
243 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
244 places=placesTests)
245 else:
246 # check entries in localDataset, which was modified by the function
247 for ampName in self.ampNames:
248 maskAmp = localDataset.expIdMask[ampName]
249 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
250 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
251 linearPart = self.flux*finalTimeVec
252 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
253 self.assertEqual(fitType, localDataset.ptcFitType)
254 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
255 if fitType == 'POLYNOMIAL':
256 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
257 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
258 if fitType == 'EXPAPPROXIMATION':
259 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
260 # noise already in electrons for 'EXPAPPROXIMATION' fit
261 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
263 # check entries in returned dataset (a dict of , for nonlinearity)
264 for ampName in self.ampNames:
265 maskAmp = localDataset.expIdMask[ampName]
266 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
267 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
268 linearPart = self.flux*finalTimeVec
269 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
271 # Nonlinearity fit parameters
272 # Polynomial fits are now normalized to unit flux scaling
273 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
274 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
275 places=5)
277 # Non-linearity coefficient for linearizer
278 squaredCoeff = self.k2NonLinearity/(self.flux**2)
279 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
280 places=placesTests)
281 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
282 places=placesTests)
284 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
285 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
286 # Fractional nonlinearity residuals
287 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
288 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
289 self.assertAlmostEqual(calc, truth, places=3)
291 def test_ptcFit(self):
292 for createArray in [True, False]:
293 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
294 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
296 def test_meanVarMeasurement(self):
297 task = self.defaultTaskExtract
298 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
300 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
301 self.assertLess(self.flatMean - mu, 1)
303 def test_meanVarMeasurementWithNans(self):
304 task = self.defaultTaskExtract
305 self.flatExp1.image.array[20:30, :] = np.nan
306 self.flatExp2.image.array[20:30, :] = np.nan
308 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
310 expectedMu1 = np.nanmean(self.flatExp1.image.array)
311 expectedMu2 = np.nanmean(self.flatExp2.image.array)
312 expectedMu = 0.5*(expectedMu1 + expectedMu2)
314 # Now the variance of the difference. First, create the diff image.
315 im1 = self.flatExp1.maskedImage
316 im2 = self.flatExp2.maskedImage
318 temp = im2.clone()
319 temp *= expectedMu1
320 diffIm = im1.clone()
321 diffIm *= expectedMu2
322 diffIm -= temp
323 diffIm /= expectedMu
325 # Divide by two as it is what measureMeanVarCov returns
326 # (variance of difference)
327 expectedVar = 0.5*np.nanvar(diffIm.image.array)
329 # Check that the standard deviations and the emans agree to
330 # less than 1 ADU
331 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
332 self.assertLess(expectedMu - mu, 1)
334 def test_meanVarMeasurementAllNan(self):
335 task = self.defaultTaskExtract
336 self.flatExp1.image.array[:, :] = np.nan
337 self.flatExp2.image.array[:, :] = np.nan
339 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
341 self.assertTrue(np.isnan(mu))
342 self.assertTrue(np.isnan(varDiff))
343 self.assertTrue(covDiff is None)
345 def test_makeZeroSafe(self):
346 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
347 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
348 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
350 substituteValue = 1e-10
352 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
353 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
355 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
356 substituteValue=substituteValue)
357 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
358 substituteValue=substituteValue)
359 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
360 substituteValue=substituteValue)
362 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
363 self.assertEqual(exp, meas)
364 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
365 self.assertEqual(exp, meas)
366 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
367 self.assertEqual(exp, meas)
369 def test_getInitialGoodPoints(self):
370 xs = [1, 2, 3, 4, 5, 6]
371 ys = [2*x for x in xs]
372 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
373 consecutivePointsVarDecreases=2)
374 assert np.all(points) == np.all(np.array([True for x in xs]))
376 ys[4] = 7 # Variance decreases in two consecutive points after ys[3]=8
377 ys[5] = 6
378 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
379 consecutivePointsVarDecreases=2)
380 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
382 def test_getExpIdsUsed(self):
383 localDataset = copy.copy(self.dataset)
385 for pair in [(12, 34), (56, 78), (90, 10)]:
386 localDataset.inputExpIdPairs["C:0,0"].append(pair)
387 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
388 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
390 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
391 with self.assertRaises(AssertionError):
392 localDataset.getExpIdsUsed("C:0,0")
394 def test_getGoodAmps(self):
395 dataset = self.dataset
397 self.assertTrue(dataset.ampNames == self.ampNames)
398 dataset.badAmps.append("C:0,1")
399 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
402class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
403 def setUp(self):
404 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
405 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
406 'C01': [(123, 234), (345, 456), (567, 678)]}
408 def test_generalBehaviour(self):
409 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
410 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
411 'C01': [(123, 234), (345, 456), (567, 678)]}
414class TestMemory(lsst.utils.tests.MemoryTestCase):
415 pass
418def setup_module(module):
419 lsst.utils.tests.init()
422if __name__ == "__main__": 422 ↛ 423line 422 didn't jump to line 423, because the condition on line 422 was never true
423 lsst.utils.tests.init()
424 unittest.main()