Coverage for tests/test_ptc.py: 10%
263 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-12 11:11 +0000
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-12 11:11 +0000
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 MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase):
47 """A test case for the PTC task."""
49 def setUp(self):
50 self.defaultConfig = cpPipe.ptc.MeasurePhotonTransferCurveTask.ConfigClass()
51 self.defaultTask = cpPipe.ptc.MeasurePhotonTransferCurveTask(config=self.defaultConfig)
53 self.defaultConfigExtract = cpPipe.ptc.PhotonTransferCurveExtractTask.ConfigClass()
54 self.defaultTaskExtract = cpPipe.ptc.PhotonTransferCurveExtractTask(config=self.defaultConfigExtract)
56 self.defaultConfigSolve = cpPipe.ptc.PhotonTransferCurveSolveTask.ConfigClass()
57 self.defaultTaskSolve = cpPipe.ptc.PhotonTransferCurveSolveTask(config=self.defaultConfigSolve)
59 self.flatMean = 2000
60 self.readNoiseAdu = 10
61 mockImageConfig = isrMock.IsrMock.ConfigClass()
63 # flatDrop is not really relevant as we replace the data
64 # but good to note it in case we change how this image is made
65 mockImageConfig.flatDrop = 0.99999
66 mockImageConfig.isTrimmed = True
68 self.flatExp1 = isrMock.FlatMock(config=mockImageConfig).run()
69 self.flatExp2 = self.flatExp1.clone()
70 (shapeY, shapeX) = self.flatExp1.getDimensions()
72 self.flatWidth = np.sqrt(self.flatMean) + self.readNoiseAdu
74 self.rng1 = np.random.RandomState(1984)
75 flatData1 = self.rng1.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
76 self.rng2 = np.random.RandomState(666)
77 flatData2 = self.rng2.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
79 self.flatExp1.image.array[:] = flatData1
80 self.flatExp2.image.array[:] = flatData2
82 # create fake PTC data to see if fit works, for one amp ('amp')
83 self.flux = 1000. # ADU/sec
84 self.timeVec = np.arange(1., 101., 5)
85 self.k2NonLinearity = -5e-6
86 # quadratic signal-chain non-linearity
87 muVec = self.flux*self.timeVec + self.k2NonLinearity*self.timeVec**2
88 self.gain = 1.5 # e-/ADU
89 self.c1 = 1./self.gain
90 self.noiseSq = 5*self.gain # 7.5 (e-)^2
91 self.a00 = -1.2e-6
92 self.c2 = -1.5e-6
93 self.c3 = -4.7e-12 # tuned so that it turns over for 200k mean
95 self.ampNames = [amp.getName() for amp in self.flatExp1.getDetector().getAmplifiers()]
96 self.dataset = PhotonTransferCurveDataset(self.ampNames, " ") # pack raw data for fitting
98 for ampName in self.ampNames: # just the expTimes and means here - vars vary per function
99 self.dataset.rawExpTimes[ampName] = self.timeVec
100 self.dataset.rawMeans[ampName] = muVec
102 def test_covAstier(self):
103 """Test to check getCovariancesAstier
105 We check that the gain is the same as the imput gain from the
106 mock data, that the covariances via FFT (as it is in
107 MeasurePhotonTransferCurveTask when doCovariancesAstier=True)
108 are the same as calculated in real space, and that Cov[0, 0]
109 (i.e., the variances) are similar to the variances calculated
110 with the standard method (when doCovariancesAstier=false),
112 """
113 task = self.defaultTask
114 extractConfig = self.defaultConfigExtract
115 extractConfig.minNumberGoodPixelsForCovariance = 5000
116 extractConfig.detectorMeasurementRegion = 'FULL'
117 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig)
119 solveConfig = self.defaultConfigSolve
120 solveConfig.ptcFitType = 'FULLCOVARIANCE'
121 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=solveConfig)
123 inputGain = 0.75
125 muStandard, varStandard = {}, {}
126 expDict = {}
127 expIds = []
128 idCounter = 0
129 for expTime in self.timeVec:
130 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain,
131 readNoiseElectrons=3, expId1=idCounter,
132 expId2=idCounter+1)
133 expDict[expTime] = ((mockExp1, idCounter), (mockExp2, idCounter+1))
134 expIds.append(idCounter)
135 expIds.append(idCounter+1)
136 for ampNumber, ampName in enumerate(self.ampNames):
137 # cov has (i, j, var, cov, npix)
138 muDiff, varDiff, covAstier = task.extract.measureMeanVarCov(mockExp1, mockExp2)
139 muStandard.setdefault(ampName, []).append(muDiff)
140 varStandard.setdefault(ampName, []).append(varDiff)
141 # Calculate covariances in an independent way: direct space
142 _, _, covsDirect = task.extract.measureMeanVarCov(mockExp1, mockExp2, covAstierRealSpace=True)
144 # Test that the arrays "covs" (FFT) and "covDirect"
145 # (direct space) are the same
146 for row1, row2 in zip(covAstier, covsDirect):
147 for a, b in zip(row1, row2):
148 self.assertAlmostEqual(a, b)
149 idCounter += 2
150 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds)
151 resultsSolve = solveTask.run(resultsExtract.outputCovariances)
153 for amp in self.ampNames:
154 self.assertAlmostEqual(resultsSolve.outputPtcDataset.gain[amp], inputGain, places=2)
155 for v1, v2 in zip(varStandard[amp], resultsSolve.outputPtcDataset.finalVars[amp]):
156 self.assertAlmostEqual(v1/v2, 1.0, places=1)
158 def ptcFitAndCheckPtc(self, order=None, fitType=None, doTableArray=False, doFitBootstrap=False):
159 localDataset = copy.copy(self.dataset)
160 localDataset.ptcFitType = fitType
161 configSolve = copy.copy(self.defaultConfigSolve)
162 configLin = cpPipe.linearity.LinearitySolveTask.ConfigClass()
163 placesTests = 6
164 if doFitBootstrap:
165 configSolve.doFitBootstrap = True
166 # Bootstrap method in cp_pipe/utils.py does multiple fits
167 # in the precense of noise. Allow for more margin of
168 # error.
169 placesTests = 3
171 if fitType == 'POLYNOMIAL':
172 if order not in [2, 3]:
173 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
174 if order == 2:
175 for ampName in self.ampNames:
176 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
177 mu in localDataset.rawMeans[ampName]]
178 configSolve.polynomialFitDegree = 2
179 if order == 3:
180 for ampName in self.ampNames:
181 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
182 for mu in localDataset.rawMeans[ampName]]
183 configSolve.polynomialFitDegree = 3
184 elif fitType == 'EXPAPPROXIMATION':
185 g = self.gain
186 for ampName in self.ampNames:
187 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1)
188 + self.noiseSq/(g*g))
189 for mu in localDataset.rawMeans[ampName]]
190 else:
191 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
193 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats
194 configLin.maxLinearAdu = 100000
195 configLin.minLinearAdu = 50000
196 if doTableArray:
197 configLin.linearityType = "LookupTable"
198 else:
199 configLin.linearityType = "Polynomial"
200 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve)
201 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin)
203 if doTableArray:
204 # Non-linearity
205 numberAmps = len(self.ampNames)
206 # localDataset: PTC dataset
207 # (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`)
208 localDataset = solveTask.fitPtc(localDataset)
209 # linDataset here is a lsst.pipe.base.Struct
210 linDataset = linearityTask.run(localDataset,
211 dummy=[1.0],
212 camera=FakeCamera([self.flatExp1.getDetector()]),
213 inputDims={'detector': 0})
214 linDataset = linDataset.outputLinearizer
215 else:
216 localDataset = solveTask.fitPtc(localDataset)
217 linDataset = linearityTask.run(localDataset,
218 dummy=[1.0],
219 camera=FakeCamera([self.flatExp1.getDetector()]),
220 inputDims={'detector': 0})
221 linDataset = linDataset.outputLinearizer
222 if doTableArray:
223 # check that the linearizer table has been filled out properly
224 for i in np.arange(numberAmps):
225 tMax = (configLin.maxLookupTableAdu)/self.flux
226 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
227 signalIdeal = timeRange*self.flux
228 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
229 timeRange)
230 linearizerTableRow = signalIdeal - signalUncorrected
231 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
232 for j in np.arange(len(linearizerTableRow)):
233 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
234 places=placesTests)
235 else:
236 # check entries in localDataset, which was modified by the function
237 for ampName in self.ampNames:
238 maskAmp = localDataset.expIdMask[ampName]
239 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
240 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
241 linearPart = self.flux*finalTimeVec
242 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
243 self.assertEqual(fitType, localDataset.ptcFitType)
244 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
245 if fitType == 'POLYNOMIAL':
246 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
247 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
248 if fitType == 'EXPAPPROXIMATION':
249 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
250 # noise already in electrons for 'EXPAPPROXIMATION' fit
251 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
253 # check entries in returned dataset (a dict of , for nonlinearity)
254 for ampName in self.ampNames:
255 maskAmp = localDataset.expIdMask[ampName]
256 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
257 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
258 linearPart = self.flux*finalTimeVec
259 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
261 # Nonlinearity fit parameters
262 # Polynomial fits are now normalized to unit flux scaling
263 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
264 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
265 places=5)
267 # Non-linearity coefficient for linearizer
268 squaredCoeff = self.k2NonLinearity/(self.flux**2)
269 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
270 places=placesTests)
271 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
272 places=placesTests)
274 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
275 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
276 # Fractional nonlinearity residuals
277 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
278 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
279 self.assertAlmostEqual(calc, truth, places=3)
281 def test_ptcFit(self):
282 for createArray in [True, False]:
283 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
284 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
286 def test_meanVarMeasurement(self):
287 task = self.defaultTaskExtract
288 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
290 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
291 self.assertLess(self.flatMean - mu, 1)
293 def test_meanVarMeasurementWithNans(self):
294 task = self.defaultTaskExtract
295 self.flatExp1.image.array[20:30, :] = np.nan
296 self.flatExp2.image.array[20:30, :] = np.nan
298 mu, varDiff, _ = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
300 expectedMu1 = np.nanmean(self.flatExp1.image.array)
301 expectedMu2 = np.nanmean(self.flatExp2.image.array)
302 expectedMu = 0.5*(expectedMu1 + expectedMu2)
304 # Now the variance of the difference. First, create the diff image.
305 im1 = self.flatExp1.maskedImage
306 im2 = self.flatExp2.maskedImage
308 temp = im2.clone()
309 temp *= expectedMu1
310 diffIm = im1.clone()
311 diffIm *= expectedMu2
312 diffIm -= temp
313 diffIm /= expectedMu
315 # Divide by two as it is what measureMeanVarCov returns
316 # (variance of difference)
317 expectedVar = 0.5*np.nanvar(diffIm.image.array)
319 # Check that the standard deviations and the emans agree to
320 # less than 1 ADU
321 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
322 self.assertLess(expectedMu - mu, 1)
324 def test_meanVarMeasurementAllNan(self):
325 task = self.defaultTaskExtract
326 self.flatExp1.image.array[:, :] = np.nan
327 self.flatExp2.image.array[:, :] = np.nan
329 mu, varDiff, covDiff = task.measureMeanVarCov(self.flatExp1, self.flatExp2)
331 self.assertTrue(np.isnan(mu))
332 self.assertTrue(np.isnan(varDiff))
333 self.assertTrue(covDiff is None)
335 def test_makeZeroSafe(self):
336 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
337 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
338 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
340 substituteValue = 1e-10
342 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
343 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
345 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
346 substituteValue=substituteValue)
347 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
348 substituteValue=substituteValue)
349 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
350 substituteValue=substituteValue)
352 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
353 self.assertEqual(exp, meas)
354 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
355 self.assertEqual(exp, meas)
356 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
357 self.assertEqual(exp, meas)
359 def test_getInitialGoodPoints(self):
360 xs = [1, 2, 3, 4, 5, 6]
361 ys = [2*x for x in xs]
362 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, maxDeviationPositive=0.1,
363 maxDeviationNegative=0.25,
364 minMeanRatioTest=0.,
365 minVarPivotSearch=0.)
366 assert np.all(points) == np.all(np.array([True for x in xs]))
368 ys[-1] = 30
369 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys,
370 maxDeviationPositive=0.1,
371 maxDeviationNegative=0.25,
372 minMeanRatioTest=0.,
373 minVarPivotSearch=0.)
374 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
376 ys = [2*x for x in xs]
377 newYs = copy.copy(ys)
378 results = [False, True, True, False, False]
379 for i, factor in enumerate([-0.5, -0.1, 0, 0.1, 0.5]):
380 newYs[-1] = ys[-1] + (factor*ys[-1])
381 points = self.defaultTaskSolve._getInitialGoodPoints(xs, newYs, maxDeviationPositive=0.05,
382 maxDeviationNegative=0.25,
383 minMeanRatioTest=0.0,
384 minVarPivotSearch=0.0)
385 assert (np.all(points[0:-2])) # noqa: E712 - flake8 is wrong here because of numpy.bool
386 assert points[-1] == results[i]
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()