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