Coverage for tests/test_ptc.py: 12%
303 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-03 04:28 -0700
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-03 04:28 -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 = 0.75 # 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
110 self.covariancesSqrtWeights = {}
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
114 self.covariancesSqrtWeights[ampName] = []
116 # ISR metadata
117 self.metadataContents = TaskMetadata()
118 self.metadataContents["isr"] = {}
119 # Overscan readout noise [in ADU]
120 for amp in self.ampNames:
121 self.metadataContents["isr"][f"RESIDUAL STDEV {amp}"] = np.sqrt(self.noiseSq)/self.gain
123 def test_covAstier(self):
124 """Test to check getCovariancesAstier
126 We check that the gain is the same as the imput gain from the
127 mock data, that the covariances via FFT (as it is in
128 MeasurePhotonTransferCurveTask when doCovariancesAstier=True)
129 are the same as calculated in real space, and that Cov[0, 0]
130 (i.e., the variances) are similar to the variances calculated
131 with the standard method (when doCovariancesAstier=false),
133 """
134 extractConfig = self.defaultConfigExtract
135 extractConfig.minNumberGoodPixelsForCovariance = 5000
136 extractConfig.detectorMeasurementRegion = 'FULL'
137 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig)
139 solveConfig = self.defaultConfigSolve
140 solveConfig.ptcFitType = 'FULLCOVARIANCE'
141 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=solveConfig)
143 inputGain = self.gain
145 muStandard, varStandard = {}, {}
146 expDict = {}
147 expIds = []
148 idCounter = 0
149 for expTime in self.timeVec:
150 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain,
151 readNoiseElectrons=3,
152 expId1=idCounter, expId2=idCounter+1)
153 mockExpRef1 = PretendRef(mockExp1)
154 mockExpRef2 = PretendRef(mockExp2)
155 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1))
156 expIds.append(idCounter)
157 expIds.append(idCounter+1)
158 for ampNumber, ampName in enumerate(self.ampNames):
159 # cov has (i, j, var, cov, npix)
160 im1Area, im2Area, imStatsCtrl, mu1, mu2 = extractTask.getImageAreasMasksStats(mockExp1,
161 mockExp2)
162 muDiff, varDiff, covAstier = extractTask.measureMeanVarCov(im1Area, im2Area, imStatsCtrl,
163 mu1, mu2)
164 muStandard.setdefault(ampName, []).append(muDiff)
165 varStandard.setdefault(ampName, []).append(varDiff)
166 idCounter += 2
168 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds,
169 taskMetadata=[self.metadataContents])
170 resultsSolve = solveTask.run(resultsExtract.outputCovariances,
171 camera=FakeCamera([self.flatExp1.getDetector()]))
173 for amp in self.ampNames:
174 self.assertAlmostEqual(resultsSolve.outputPtcDataset.gain[amp], inputGain, places=2)
175 for v1, v2 in zip(varStandard[amp], resultsSolve.outputPtcDataset.finalVars[amp]):
176 self.assertAlmostEqual(v1/v2, 1.0, places=1)
178 def ptcFitAndCheckPtc(self, order=None, fitType=None, doTableArray=False, doFitBootstrap=False):
179 localDataset = copy.deepcopy(self.dataset)
180 localDataset.ptcFitType = fitType
181 configSolve = copy.copy(self.defaultConfigSolve)
182 configLin = cpPipe.linearity.LinearitySolveTask.ConfigClass()
183 placesTests = 6
184 if doFitBootstrap:
185 configSolve.doFitBootstrap = True
186 # Bootstrap method in cp_pipe/utils.py does multiple fits
187 # in the precense of noise. Allow for more margin of
188 # error.
189 placesTests = 3
191 if fitType == 'POLYNOMIAL':
192 if order not in [2, 3]:
193 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
194 if order == 2:
195 for ampName in self.ampNames:
196 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
197 mu in localDataset.rawMeans[ampName]]
198 configSolve.polynomialFitDegree = 2
199 if order == 3:
200 for ampName in self.ampNames:
201 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
202 for mu in localDataset.rawMeans[ampName]]
203 configSolve.polynomialFitDegree = 3
204 elif fitType == 'EXPAPPROXIMATION':
205 g = self.gain
206 for ampName in self.ampNames:
207 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1)
208 + self.noiseSq/(g*g))
209 for mu in localDataset.rawMeans[ampName]]
210 else:
211 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
213 # Initialize mask and covariance weights that will be used in fits.
214 # Covariance weights values empirically determined from one of
215 # the cases in test_covAstier.
216 matrixSize = localDataset.covMatrixSide
217 maskLength = len(localDataset.rawMeans[ampName])
218 for ampName in self.ampNames:
219 localDataset.expIdMask[ampName] = np.repeat(True, maskLength)
220 localDataset.covariancesSqrtWeights[ampName] = np.repeat(np.ones((matrixSize, matrixSize)),
221 maskLength).reshape((maskLength,
222 matrixSize,
223 matrixSize))
224 localDataset.covariancesSqrtWeights[ampName][:, 0, 0] = [0.07980188, 0.01339653, 0.0073118,
225 0.00502802, 0.00383132, 0.00309475,
226 0.00259572, 0.00223528, 0.00196273,
227 0.00174943, 0.00157794, 0.00143707,
228 0.00131929, 0.00121935, 0.0011334,
229 0.00105893, 0.00099357, 0.0009358,
230 0.00088439, 0.00083833]
231 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats
232 configLin.maxLinearAdu = 100000
233 configLin.minLinearAdu = 50000
234 if doTableArray:
235 configLin.linearityType = "LookupTable"
236 else:
237 configLin.linearityType = "Polynomial"
238 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve)
239 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin)
241 if doTableArray:
242 # Non-linearity
243 numberAmps = len(self.ampNames)
244 # localDataset: PTC dataset
245 # (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`)
246 localDataset = solveTask.fitMeasurementsToModel(localDataset)
247 # linDataset here is a lsst.pipe.base.Struct
248 linDataset = linearityTask.run(localDataset,
249 dummy=[1.0],
250 camera=FakeCamera([self.flatExp1.getDetector()]),
251 inputDims={'detector': 0})
252 linDataset = linDataset.outputLinearizer
253 else:
254 localDataset = solveTask.fitMeasurementsToModel(localDataset)
255 linDataset = linearityTask.run(localDataset,
256 dummy=[1.0],
257 camera=FakeCamera([self.flatExp1.getDetector()]),
258 inputDims={'detector': 0})
259 linDataset = linDataset.outputLinearizer
260 if doTableArray:
261 # check that the linearizer table has been filled out properly
262 for i in np.arange(numberAmps):
263 tMax = (configLin.maxLookupTableAdu)/self.flux
264 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
265 signalIdeal = timeRange*self.flux
266 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
267 timeRange)
268 linearizerTableRow = signalIdeal - signalUncorrected
269 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
270 for j in np.arange(len(linearizerTableRow)):
271 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
272 places=placesTests)
273 else:
274 # check entries in localDataset, which was modified by the function
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
281 self.assertEqual(fitType, localDataset.ptcFitType)
282 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
283 if fitType == 'POLYNOMIAL':
284 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
285 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
286 if fitType == 'EXPAPPROXIMATION':
287 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
288 # noise already in electrons for 'EXPAPPROXIMATION' fit
289 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
291 # check entries in returned dataset (a dict of , for nonlinearity)
292 for ampName in self.ampNames:
293 maskAmp = localDataset.expIdMask[ampName]
294 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
295 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
296 linearPart = self.flux*finalTimeVec
297 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
299 # Nonlinearity fit parameters
300 # Polynomial fits are now normalized to unit flux scaling
301 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
302 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
303 places=5)
305 # Non-linearity coefficient for linearizer
306 squaredCoeff = self.k2NonLinearity/(self.flux**2)
307 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
308 places=placesTests)
309 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
310 places=placesTests)
312 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
313 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
314 # Fractional nonlinearity residuals
315 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
316 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
317 self.assertAlmostEqual(calc, truth, places=3)
319 def test_ptcFit(self):
320 for createArray in [True, False]:
321 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
322 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
324 def test_meanVarMeasurement(self):
325 task = self.defaultTaskExtract
326 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
327 self.flatExp2)
328 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
330 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
331 self.assertLess(self.flatMean - mu, 1)
333 def test_meanVarMeasurementWithNans(self):
334 task = self.defaultTaskExtract
335 self.flatExp1.image.array[20:30, :] = np.nan
336 self.flatExp2.image.array[20:30, :] = np.nan
338 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
339 self.flatExp2)
340 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
342 expectedMu1 = np.nanmean(self.flatExp1.image.array)
343 expectedMu2 = np.nanmean(self.flatExp2.image.array)
344 expectedMu = 0.5*(expectedMu1 + expectedMu2)
346 # Now the variance of the difference. First, create the diff image.
347 im1 = self.flatExp1.maskedImage
348 im2 = self.flatExp2.maskedImage
350 temp = im2.clone()
351 temp *= expectedMu1
352 diffIm = im1.clone()
353 diffIm *= expectedMu2
354 diffIm -= temp
355 diffIm /= expectedMu
357 # Divide by two as it is what measureMeanVarCov returns
358 # (variance of difference)
359 expectedVar = 0.5*np.nanvar(diffIm.image.array)
361 # Check that the standard deviations and the emans agree to
362 # less than 1 ADU
363 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
364 self.assertLess(expectedMu - mu, 1)
366 def test_meanVarMeasurementAllNan(self):
367 task = self.defaultTaskExtract
368 self.flatExp1.image.array[:, :] = np.nan
369 self.flatExp2.image.array[:, :] = np.nan
371 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
372 self.flatExp2)
373 mu, varDiff, covDiff = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
375 self.assertTrue(np.isnan(mu))
376 self.assertTrue(np.isnan(varDiff))
377 self.assertTrue(covDiff is None)
379 def test_makeZeroSafe(self):
380 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
381 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
382 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
384 substituteValue = 1e-10
386 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
387 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
389 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
390 substituteValue=substituteValue)
391 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
392 substituteValue=substituteValue)
393 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
394 substituteValue=substituteValue)
396 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
397 self.assertEqual(exp, meas)
398 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
399 self.assertEqual(exp, meas)
400 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
401 self.assertEqual(exp, meas)
403 def test_getInitialGoodPoints(self):
404 xs = [1, 2, 3, 4, 5, 6]
405 ys = [2*x for x in xs]
406 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
407 consecutivePointsVarDecreases=2)
408 assert np.all(points) == np.all(np.array([True for x in xs]))
410 ys[4] = 7 # Variance decreases in two consecutive points after ys[3]=8
411 ys[5] = 6
412 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
413 consecutivePointsVarDecreases=2)
414 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
416 def test_getExpIdsUsed(self):
417 localDataset = copy.copy(self.dataset)
419 for pair in [(12, 34), (56, 78), (90, 10)]:
420 localDataset.inputExpIdPairs["C:0,0"].append(pair)
421 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
422 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
424 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
425 with self.assertRaises(AssertionError):
426 localDataset.getExpIdsUsed("C:0,0")
428 def test_getGoodAmps(self):
429 dataset = self.dataset
431 self.assertTrue(dataset.ampNames == self.ampNames)
432 dataset.badAmps.append("C:0,1")
433 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
435 def runGetGainFromFlatPair(self, correctionType='NONE'):
436 extractConfig = self.defaultConfigExtract
437 extractConfig.gainCorrectionType = correctionType
438 extractConfig.minNumberGoodPixelsForCovariance = 5000
439 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig)
441 expDict = {}
442 expIds = []
443 idCounter = 0
444 inputGain = self.gain # 1.5 e/ADU
445 for expTime in self.timeVec:
446 # Approximation works better at low flux, e.g., < 10000 ADU
447 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain,
448 readNoiseElectrons=np.sqrt(self.noiseSq),
449 fluxElectrons=100,
450 expId1=idCounter, expId2=idCounter+1)
451 mockExpRef1 = PretendRef(mockExp1)
452 mockExpRef2 = PretendRef(mockExp2)
453 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1))
454 expIds.append(idCounter)
455 expIds.append(idCounter+1)
456 idCounter += 2
458 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds,
459 taskMetadata=[self.metadataContents])
460 for exposurePair in resultsExtract.outputCovariances:
461 for ampName in self.ampNames:
462 if exposurePair.gain[ampName] is np.nan:
463 continue
464 self.assertAlmostEqual(exposurePair.gain[ampName], inputGain, delta=0.04)
466 def test_getGainFromFlatPair(self):
467 for gainCorrectionType in ['NONE', 'SIMPLE', 'FULL', ]:
468 self.runGetGainFromFlatPair(gainCorrectionType)
471class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
472 def setUp(self):
473 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
474 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
475 'C01': [(123, 234), (345, 456), (567, 678)]}
477 def test_generalBehaviour(self):
478 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
479 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
480 'C01': [(123, 234), (345, 456), (567, 678)]}
483class TestMemory(lsst.utils.tests.MemoryTestCase):
484 pass
487def setup_module(module):
488 lsst.utils.tests.init()
491if __name__ == "__main__": 491 ↛ 492line 491 didn't jump to line 492, because the condition on line 491 was never true
492 lsst.utils.tests.init()
493 unittest.main()