Coverage for tests/test_ptc.py: 12%
297 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-23 03:40 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-23 03:40 -0700
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,
151 expId1=idCounter, 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 im1Area, im2Area, imStatsCtrl, mu1, mu2 = extractTask.getImageAreasMasksStats(mockExp1,
160 mockExp2)
161 muDiff, varDiff, covAstier = extractTask.measureMeanVarCov(im1Area, im2Area, imStatsCtrl,
162 mu1, mu2)
163 muStandard.setdefault(ampName, []).append(muDiff)
164 varStandard.setdefault(ampName, []).append(varDiff)
165 idCounter += 2
167 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds,
168 taskMetadata=[self.metadataContents])
169 resultsSolve = solveTask.run(resultsExtract.outputCovariances,
170 camera=FakeCamera([self.flatExp1.getDetector()]))
172 for amp in self.ampNames:
173 self.assertAlmostEqual(resultsSolve.outputPtcDataset.gain[amp], inputGain, places=2)
174 for v1, v2 in zip(varStandard[amp], resultsSolve.outputPtcDataset.finalVars[amp]):
175 self.assertAlmostEqual(v1/v2, 1.0, places=1)
177 def ptcFitAndCheckPtc(self, order=None, fitType=None, doTableArray=False, doFitBootstrap=False):
178 localDataset = copy.copy(self.dataset)
179 localDataset.ptcFitType = fitType
180 configSolve = copy.copy(self.defaultConfigSolve)
181 configLin = cpPipe.linearity.LinearitySolveTask.ConfigClass()
182 placesTests = 6
183 if doFitBootstrap:
184 configSolve.doFitBootstrap = True
185 # Bootstrap method in cp_pipe/utils.py does multiple fits
186 # in the precense of noise. Allow for more margin of
187 # error.
188 placesTests = 3
190 if fitType == 'POLYNOMIAL':
191 if order not in [2, 3]:
192 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
193 if order == 2:
194 for ampName in self.ampNames:
195 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
196 mu in localDataset.rawMeans[ampName]]
197 configSolve.polynomialFitDegree = 2
198 if order == 3:
199 for ampName in self.ampNames:
200 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
201 for mu in localDataset.rawMeans[ampName]]
202 configSolve.polynomialFitDegree = 3
203 elif fitType == 'EXPAPPROXIMATION':
204 g = self.gain
205 for ampName in self.ampNames:
206 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1)
207 + self.noiseSq/(g*g))
208 for mu in localDataset.rawMeans[ampName]]
209 else:
210 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
212 for ampName in self.ampNames:
213 localDataset.expIdMask[ampName] = np.repeat(True, len(localDataset.rawMeans[ampName]))
214 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats
215 configLin.maxLinearAdu = 100000
216 configLin.minLinearAdu = 50000
217 if doTableArray:
218 configLin.linearityType = "LookupTable"
219 else:
220 configLin.linearityType = "Polynomial"
221 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve)
222 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin)
224 if doTableArray:
225 # Non-linearity
226 numberAmps = len(self.ampNames)
227 # localDataset: PTC dataset
228 # (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`)
229 localDataset = solveTask.fitMeasurementsToModel(localDataset)
230 # linDataset here is a lsst.pipe.base.Struct
231 linDataset = linearityTask.run(localDataset,
232 dummy=[1.0],
233 camera=FakeCamera([self.flatExp1.getDetector()]),
234 inputDims={'detector': 0})
235 linDataset = linDataset.outputLinearizer
236 else:
237 localDataset = solveTask.fitMeasurementsToModel(localDataset)
238 linDataset = linearityTask.run(localDataset,
239 dummy=[1.0],
240 camera=FakeCamera([self.flatExp1.getDetector()]),
241 inputDims={'detector': 0})
242 linDataset = linDataset.outputLinearizer
243 if doTableArray:
244 # check that the linearizer table has been filled out properly
245 for i in np.arange(numberAmps):
246 tMax = (configLin.maxLookupTableAdu)/self.flux
247 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
248 signalIdeal = timeRange*self.flux
249 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
250 timeRange)
251 linearizerTableRow = signalIdeal - signalUncorrected
252 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
253 for j in np.arange(len(linearizerTableRow)):
254 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
255 places=placesTests)
256 else:
257 # check entries in localDataset, which was modified by the function
258 for ampName in self.ampNames:
259 maskAmp = localDataset.expIdMask[ampName]
260 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
261 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
262 linearPart = self.flux*finalTimeVec
263 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
264 self.assertEqual(fitType, localDataset.ptcFitType)
265 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
266 if fitType == 'POLYNOMIAL':
267 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
268 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
269 if fitType == 'EXPAPPROXIMATION':
270 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
271 # noise already in electrons for 'EXPAPPROXIMATION' fit
272 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
274 # check entries in returned dataset (a dict of , for nonlinearity)
275 for ampName in self.ampNames:
276 maskAmp = localDataset.expIdMask[ampName]
277 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
278 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
279 linearPart = self.flux*finalTimeVec
280 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
282 # Nonlinearity fit parameters
283 # Polynomial fits are now normalized to unit flux scaling
284 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
285 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
286 places=5)
288 # Non-linearity coefficient for linearizer
289 squaredCoeff = self.k2NonLinearity/(self.flux**2)
290 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
291 places=placesTests)
292 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
293 places=placesTests)
295 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
296 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
297 # Fractional nonlinearity residuals
298 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
299 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
300 self.assertAlmostEqual(calc, truth, places=3)
302 def test_ptcFit(self):
303 for createArray in [True, False]:
304 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
305 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
307 def test_meanVarMeasurement(self):
308 task = self.defaultTaskExtract
309 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
310 self.flatExp2)
311 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
313 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
314 self.assertLess(self.flatMean - mu, 1)
316 def test_meanVarMeasurementWithNans(self):
317 task = self.defaultTaskExtract
318 self.flatExp1.image.array[20:30, :] = np.nan
319 self.flatExp2.image.array[20:30, :] = np.nan
321 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
322 self.flatExp2)
323 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
325 expectedMu1 = np.nanmean(self.flatExp1.image.array)
326 expectedMu2 = np.nanmean(self.flatExp2.image.array)
327 expectedMu = 0.5*(expectedMu1 + expectedMu2)
329 # Now the variance of the difference. First, create the diff image.
330 im1 = self.flatExp1.maskedImage
331 im2 = self.flatExp2.maskedImage
333 temp = im2.clone()
334 temp *= expectedMu1
335 diffIm = im1.clone()
336 diffIm *= expectedMu2
337 diffIm -= temp
338 diffIm /= expectedMu
340 # Divide by two as it is what measureMeanVarCov returns
341 # (variance of difference)
342 expectedVar = 0.5*np.nanvar(diffIm.image.array)
344 # Check that the standard deviations and the emans agree to
345 # less than 1 ADU
346 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
347 self.assertLess(expectedMu - mu, 1)
349 def test_meanVarMeasurementAllNan(self):
350 task = self.defaultTaskExtract
351 self.flatExp1.image.array[:, :] = np.nan
352 self.flatExp2.image.array[:, :] = np.nan
354 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
355 self.flatExp2)
356 mu, varDiff, covDiff = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
358 self.assertTrue(np.isnan(mu))
359 self.assertTrue(np.isnan(varDiff))
360 self.assertTrue(covDiff is None)
362 def test_makeZeroSafe(self):
363 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
364 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
365 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
367 substituteValue = 1e-10
369 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
370 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
372 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
373 substituteValue=substituteValue)
374 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
375 substituteValue=substituteValue)
376 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
377 substituteValue=substituteValue)
379 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
380 self.assertEqual(exp, meas)
381 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
382 self.assertEqual(exp, meas)
383 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
384 self.assertEqual(exp, meas)
386 def test_getInitialGoodPoints(self):
387 xs = [1, 2, 3, 4, 5, 6]
388 ys = [2*x for x in xs]
389 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
390 consecutivePointsVarDecreases=2)
391 assert np.all(points) == np.all(np.array([True for x in xs]))
393 ys[4] = 7 # Variance decreases in two consecutive points after ys[3]=8
394 ys[5] = 6
395 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
396 consecutivePointsVarDecreases=2)
397 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
399 def test_getExpIdsUsed(self):
400 localDataset = copy.copy(self.dataset)
402 for pair in [(12, 34), (56, 78), (90, 10)]:
403 localDataset.inputExpIdPairs["C:0,0"].append(pair)
404 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
405 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
407 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
408 with self.assertRaises(AssertionError):
409 localDataset.getExpIdsUsed("C:0,0")
411 def test_getGoodAmps(self):
412 dataset = self.dataset
414 self.assertTrue(dataset.ampNames == self.ampNames)
415 dataset.badAmps.append("C:0,1")
416 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
418 def runGetGainFromFlatPair(self, correctionType='NONE'):
419 extractConfig = self.defaultConfigExtract
420 extractConfig.gainCorrectionType = correctionType
421 extractConfig.minNumberGoodPixelsForCovariance = 5000
422 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig)
424 expDict = {}
425 expIds = []
426 idCounter = 0
427 inputGain = self.gain # 1.5 e/ADU
428 for expTime in self.timeVec:
429 # Approximation works better at low flux, e.g., < 10000 ADU
430 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain,
431 readNoiseElectrons=np.sqrt(self.noiseSq),
432 fluxElectrons=100,
433 expId1=idCounter, expId2=idCounter+1)
434 mockExpRef1 = PretendRef(mockExp1)
435 mockExpRef2 = PretendRef(mockExp2)
436 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1))
437 expIds.append(idCounter)
438 expIds.append(idCounter+1)
439 idCounter += 2
441 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds,
442 taskMetadata=[self.metadataContents])
444 for exposurePair in resultsExtract.outputCovariances:
445 for ampName in self.ampNames:
446 if exposurePair.gain[ampName] is np.nan:
447 continue
448 self.assertAlmostEqual(exposurePair.gain[ampName], inputGain, delta=0.16)
450 def test_getGainFromFlatPair(self):
451 for gainCorrectionType in ['NONE', 'SIMPLE', 'FULL', ]:
452 self.runGetGainFromFlatPair(gainCorrectionType)
455class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
456 def setUp(self):
457 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
458 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
459 'C01': [(123, 234), (345, 456), (567, 678)]}
461 def test_generalBehaviour(self):
462 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
463 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
464 'C01': [(123, 234), (345, 456), (567, 678)]}
467class TestMemory(lsst.utils.tests.MemoryTestCase):
468 pass
471def setup_module(module):
472 lsst.utils.tests.init()
475if __name__ == "__main__": 475 ↛ 476line 475 didn't jump to line 476, because the condition on line 475 was never true
476 lsst.utils.tests.init()
477 unittest.main()