Coverage for tests/test_ptc.py: 10%
303 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-14 11:00 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-14 11:00 +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)
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 inputPhotodiodeData={},
252 inputDims={'detector': 0})
253 linDataset = linDataset.outputLinearizer
254 else:
255 localDataset = solveTask.fitMeasurementsToModel(localDataset)
256 linDataset = linearityTask.run(localDataset,
257 dummy=[1.0],
258 camera=FakeCamera([self.flatExp1.getDetector()]),
259 inputPhotodiodeData={},
260 inputDims={'detector': 0})
261 linDataset = linDataset.outputLinearizer
262 if doTableArray:
263 # check that the linearizer table has been filled out properly
264 for i in np.arange(numberAmps):
265 tMax = (configLin.maxLookupTableAdu)/self.flux
266 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
267 signalIdeal = timeRange*self.flux
268 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
269 timeRange)
270 linearizerTableRow = signalIdeal - signalUncorrected
271 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
272 for j in np.arange(len(linearizerTableRow)):
273 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
274 places=placesTests)
275 else:
276 # check entries in localDataset, which was modified by the function
277 for ampName in self.ampNames:
278 maskAmp = localDataset.expIdMask[ampName]
279 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
280 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
281 linearPart = self.flux*finalTimeVec
282 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
283 self.assertEqual(fitType, localDataset.ptcFitType)
284 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
285 if fitType == 'POLYNOMIAL':
286 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
287 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
288 if fitType == 'EXPAPPROXIMATION':
289 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
290 # noise already in electrons for 'EXPAPPROXIMATION' fit
291 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
293 # check entries in returned dataset (a dict of , for nonlinearity)
294 for ampName in self.ampNames:
295 maskAmp = localDataset.expIdMask[ampName]
296 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
297 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
298 linearPart = self.flux*finalTimeVec
299 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
301 # Nonlinearity fit parameters
302 # Polynomial fits are now normalized to unit flux scaling
303 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
304 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
305 places=5)
307 # Non-linearity coefficient for linearizer
308 squaredCoeff = self.k2NonLinearity/(self.flux**2)
309 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
310 places=placesTests)
311 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
312 places=placesTests)
314 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
315 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
316 # Fractional nonlinearity residuals
317 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
318 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
319 self.assertAlmostEqual(calc, truth, places=3)
321 def test_ptcFit(self):
322 for createArray in [True, False]:
323 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
324 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
326 def test_meanVarMeasurement(self):
327 task = self.defaultTaskExtract
328 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
329 self.flatExp2)
330 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
332 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
333 self.assertLess(self.flatMean - mu, 1)
335 def test_meanVarMeasurementWithNans(self):
336 task = self.defaultTaskExtract
337 self.flatExp1.image.array[20:30, :] = np.nan
338 self.flatExp2.image.array[20:30, :] = np.nan
340 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
341 self.flatExp2)
342 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
344 expectedMu1 = np.nanmean(self.flatExp1.image.array)
345 expectedMu2 = np.nanmean(self.flatExp2.image.array)
346 expectedMu = 0.5*(expectedMu1 + expectedMu2)
348 # Now the variance of the difference. First, create the diff image.
349 im1 = self.flatExp1.maskedImage
350 im2 = self.flatExp2.maskedImage
352 temp = im2.clone()
353 temp *= expectedMu1
354 diffIm = im1.clone()
355 diffIm *= expectedMu2
356 diffIm -= temp
357 diffIm /= expectedMu
359 # Divide by two as it is what measureMeanVarCov returns
360 # (variance of difference)
361 expectedVar = 0.5*np.nanvar(diffIm.image.array)
363 # Check that the standard deviations and the emans agree to
364 # less than 1 ADU
365 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
366 self.assertLess(expectedMu - mu, 1)
368 def test_meanVarMeasurementAllNan(self):
369 task = self.defaultTaskExtract
370 self.flatExp1.image.array[:, :] = np.nan
371 self.flatExp2.image.array[:, :] = np.nan
373 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
374 self.flatExp2)
375 mu, varDiff, covDiff = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
377 self.assertTrue(np.isnan(mu))
378 self.assertTrue(np.isnan(varDiff))
379 self.assertTrue(covDiff is None)
381 def test_makeZeroSafe(self):
382 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
383 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
384 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
386 substituteValue = 1e-10
388 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
389 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
391 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
392 substituteValue=substituteValue)
393 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
394 substituteValue=substituteValue)
395 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
396 substituteValue=substituteValue)
398 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
399 self.assertEqual(exp, meas)
400 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
401 self.assertEqual(exp, meas)
402 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
403 self.assertEqual(exp, meas)
405 def test_getInitialGoodPoints(self):
406 xs = [1, 2, 3, 4, 5, 6]
407 ys = [2*x for x in xs]
408 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
409 consecutivePointsVarDecreases=2)
410 assert np.all(points) == np.all(np.array([True for x in xs]))
412 ys[4] = 7 # Variance decreases in two consecutive points after ys[3]=8
413 ys[5] = 6
414 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
415 consecutivePointsVarDecreases=2)
416 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
418 def test_getExpIdsUsed(self):
419 localDataset = copy.copy(self.dataset)
421 for pair in [(12, 34), (56, 78), (90, 10)]:
422 localDataset.inputExpIdPairs["C:0,0"].append(pair)
423 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
424 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
426 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
427 with self.assertRaises(AssertionError):
428 localDataset.getExpIdsUsed("C:0,0")
430 def test_getGoodAmps(self):
431 dataset = self.dataset
433 self.assertTrue(dataset.ampNames == self.ampNames)
434 dataset.badAmps.append("C:0,1")
435 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
437 def runGetGainFromFlatPair(self, correctionType='NONE'):
438 extractConfig = self.defaultConfigExtract
439 extractConfig.gainCorrectionType = correctionType
440 extractConfig.minNumberGoodPixelsForCovariance = 5000
441 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig)
443 expDict = {}
444 expIds = []
445 idCounter = 0
446 inputGain = self.gain # 1.5 e/ADU
447 for expTime in self.timeVec:
448 # Approximation works better at low flux, e.g., < 10000 ADU
449 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain,
450 readNoiseElectrons=np.sqrt(self.noiseSq),
451 fluxElectrons=100,
452 expId1=idCounter, expId2=idCounter+1)
453 mockExpRef1 = PretendRef(mockExp1)
454 mockExpRef2 = PretendRef(mockExp2)
455 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1))
456 expIds.append(idCounter)
457 expIds.append(idCounter+1)
458 idCounter += 2
460 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds,
461 taskMetadata=[self.metadataContents])
462 for exposurePair in resultsExtract.outputCovariances:
463 for ampName in self.ampNames:
464 if exposurePair.gain[ampName] is np.nan:
465 continue
466 self.assertAlmostEqual(exposurePair.gain[ampName], inputGain, delta=0.04)
468 def test_getGainFromFlatPair(self):
469 for gainCorrectionType in ['NONE', 'SIMPLE', 'FULL', ]:
470 self.runGetGainFromFlatPair(gainCorrectionType)
473class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
474 def setUp(self):
475 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
476 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
477 'C01': [(123, 234), (345, 456), (567, 678)]}
479 def test_generalBehaviour(self):
480 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
481 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
482 'C01': [(123, 234), (345, 456), (567, 678)]}
485class TestMemory(lsst.utils.tests.MemoryTestCase):
486 pass
489def setup_module(module):
490 lsst.utils.tests.init()
493if __name__ == "__main__": 493 ↛ 494line 493 didn't jump to line 494, because the condition on line 493 was never true
494 lsst.utils.tests.init()
495 unittest.main()