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