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