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