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 # Calculate covariances in an independent way: direct space
158 _, _, covsDirect = task.extract.measureMeanVarCov(mockExp1, mockExp2, covAstierRealSpace=True)
160 # Test that the arrays "covs" (FFT) and "covDirect"
161 # (direct space) are the same
162 for row1, row2 in zip(covAstier, covsDirect):
163 for a, b in zip(row1, row2):
164 self.assertAlmostEqual(a, b)
165 idCounter += 2
166 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds)
167 resultsSolve = solveTask.run(resultsExtract.outputCovariances)
169 for amp in self.ampNames:
170 self.assertAlmostEqual(resultsSolve.outputPtcDataset.gain[amp], inputGain, places=2)
171 for v1, v2 in zip(varStandard[amp], resultsSolve.outputPtcDataset.finalVars[amp]):
172 self.assertAlmostEqual(v1/v2, 1.0, places=1)
174 def ptcFitAndCheckPtc(self, order=None, fitType=None, doTableArray=False, doFitBootstrap=False):
175 localDataset = copy.copy(self.dataset)
176 localDataset.ptcFitType = fitType
177 configSolve = copy.copy(self.defaultConfigSolve)
178 configLin = cpPipe.linearity.LinearitySolveTask.ConfigClass()
179 placesTests = 6
180 if doFitBootstrap:
181 configSolve.doFitBootstrap = True
182 # Bootstrap method in cp_pipe/utils.py does multiple fits
183 # in the precense of noise. Allow for more margin of
184 # error.
185 placesTests = 3
187 if fitType == 'POLYNOMIAL':
188 if order not in [2, 3]:
189 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
190 if order == 2:
191 for ampName in self.ampNames:
192 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
193 mu in localDataset.rawMeans[ampName]]
194 configSolve.polynomialFitDegree = 2
195 if order == 3:
196 for ampName in self.ampNames:
197 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
198 for mu in localDataset.rawMeans[ampName]]
199 configSolve.polynomialFitDegree = 3
200 elif fitType == 'EXPAPPROXIMATION':
201 g = self.gain
202 for ampName in self.ampNames:
203 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1)
204 + self.noiseSq/(g*g))
205 for mu in localDataset.rawMeans[ampName]]
206 else:
207 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
209 for ampName in self.ampNames:
210 localDataset.expIdMask[ampName] = np.repeat(True, len(localDataset.rawMeans[ampName]))
211 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats
212 configLin.maxLinearAdu = 100000
213 configLin.minLinearAdu = 50000
214 if doTableArray:
215 configLin.linearityType = "LookupTable"
216 else:
217 configLin.linearityType = "Polynomial"
218 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve)
219 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin)
221 if doTableArray:
222 # Non-linearity
223 numberAmps = len(self.ampNames)
224 # localDataset: PTC dataset
225 # (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`)
226 localDataset = solveTask.fitPtc(localDataset)
227 # linDataset here is a lsst.pipe.base.Struct
228 linDataset = linearityTask.run(localDataset,
229 dummy=[1.0],
230 camera=FakeCamera([self.flatExp1.getDetector()]),
231 inputDims={'detector': 0})
232 linDataset = linDataset.outputLinearizer
233 else:
234 localDataset = solveTask.fitPtc(localDataset)
235 linDataset = linearityTask.run(localDataset,
236 dummy=[1.0],
237 camera=FakeCamera([self.flatExp1.getDetector()]),
238 inputDims={'detector': 0})
239 linDataset = linDataset.outputLinearizer
240 if doTableArray:
241 # check that the linearizer table has been filled out properly
242 for i in np.arange(numberAmps):
243 tMax = (configLin.maxLookupTableAdu)/self.flux
244 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
245 signalIdeal = timeRange*self.flux
246 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
247 timeRange)
248 linearizerTableRow = signalIdeal - signalUncorrected
249 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
250 for j in np.arange(len(linearizerTableRow)):
251 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
252 places=placesTests)
253 else:
254 # check entries in localDataset, which was modified by the function
255 for ampName in self.ampNames:
256 maskAmp = localDataset.expIdMask[ampName]
257 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
258 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
259 linearPart = self.flux*finalTimeVec
260 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
261 self.assertEqual(fitType, localDataset.ptcFitType)
262 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
263 if fitType == 'POLYNOMIAL':
264 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
265 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
266 if fitType == 'EXPAPPROXIMATION':
267 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
268 # noise already in electrons for 'EXPAPPROXIMATION' fit
269 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
271 # check entries in returned dataset (a dict of , for nonlinearity)
272 for ampName in self.ampNames:
273 maskAmp = localDataset.expIdMask[ampName]
274 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
275 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
276 linearPart = self.flux*finalTimeVec
277 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
279 # Nonlinearity fit parameters
280 # Polynomial fits are now normalized to unit flux scaling
281 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
282 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
283 places=5)
285 # Non-linearity coefficient for linearizer
286 squaredCoeff = self.k2NonLinearity/(self.flux**2)
287 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
288 places=placesTests)
289 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
290 places=placesTests)
292 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
293 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
294 # Fractional nonlinearity residuals
295 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
296 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
297 self.assertAlmostEqual(calc, truth, places=3)
299 def test_ptcFit(self):
300 for createArray in [True, False]:
301 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
302 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
304 def test_meanVarMeasurement(self):
305 task = self.defaultTaskExtract
306 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
308 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
309 self.assertLess(self.flatMean - mu, 1)
311 def test_meanVarMeasurementWithNans(self):
312 task = self.defaultTaskExtract
313 self.flatExp1.image.array[20:30, :] = np.nan
314 self.flatExp2.image.array[20:30, :] = np.nan
316 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
318 expectedMu1 = np.nanmean(self.flatExp1.image.array)
319 expectedMu2 = np.nanmean(self.flatExp2.image.array)
320 expectedMu = 0.5*(expectedMu1 + expectedMu2)
322 # Now the variance of the difference. First, create the diff image.
323 im1 = self.flatExp1.maskedImage
324 im2 = self.flatExp2.maskedImage
326 temp = im2.clone()
327 temp *= expectedMu1
328 diffIm = im1.clone()
329 diffIm *= expectedMu2
330 diffIm -= temp
331 diffIm /= expectedMu
333 # Divide by two as it is what measureMeanVarCov returns
334 # (variance of difference)
335 expectedVar = 0.5*np.nanvar(diffIm.image.array)
337 # Check that the standard deviations and the emans agree to
338 # less than 1 ADU
339 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
340 self.assertLess(expectedMu - mu, 1)
342 def test_meanVarMeasurementAllNan(self):
343 task = self.defaultTaskExtract
344 self.flatExp1.image.array[:, :] = np.nan
345 self.flatExp2.image.array[:, :] = np.nan
347 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
349 self.assertTrue(np.isnan(mu))
350 self.assertTrue(np.isnan(varDiff))
351 self.assertTrue(covDiff is None)
353 def test_makeZeroSafe(self):
354 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
355 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
356 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
358 substituteValue = 1e-10
360 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
361 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
363 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
364 substituteValue=substituteValue)
365 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
366 substituteValue=substituteValue)
367 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
368 substituteValue=substituteValue)
370 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
371 self.assertEqual(exp, meas)
372 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
373 self.assertEqual(exp, meas)
374 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
375 self.assertEqual(exp, meas)
377 def test_getInitialGoodPoints(self):
378 xs = [1, 2, 3, 4, 5, 6]
379 ys = [2*x for x in xs]
380 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.)
381 assert np.all(points) == np.all(np.array([True for x in xs]))
383 ys[4] = 7 # Variance decreases in two consecutive points after ys[3]=8
384 ys[5] = 6
385 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.)
386 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
388 def test_getExpIdsUsed(self):
389 localDataset = copy.copy(self.dataset)
391 for pair in [(12, 34), (56, 78), (90, 10)]:
392 localDataset.inputExpIdPairs["C:0,0"].append(pair)
393 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
394 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
396 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
397 with self.assertRaises(AssertionError):
398 localDataset.getExpIdsUsed("C:0,0")
400 def test_getGoodAmps(self):
401 dataset = self.dataset
403 self.assertTrue(dataset.ampNames == self.ampNames)
404 dataset.badAmps.append("C:0,1")
405 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
408class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
409 def setUp(self):
410 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
411 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
412 'C01': [(123, 234), (345, 456), (567, 678)]}
414 def test_generalBehaviour(self):
415 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
416 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
417 'C01': [(123, 234), (345, 456), (567, 678)]}
420class TestMemory(lsst.utils.tests.MemoryTestCase):
421 pass
424def setup_module(module):
425 lsst.utils.tests.init()
428if __name__ == "__main__": 428 ↛ 429line 428 didn't jump to line 429, because the condition on line 428 was never true
429 lsst.utils.tests.init()
430 unittest.main()