Coverage for tests/test_ptc.py: 10%
307 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-09 04:21 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-09 04:21 -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 elif component == 'metadata':
59 return self.exp.getMetadata()
60 else:
61 return self.exp
64class MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase):
65 """A test case for the PTC tasks."""
67 def setUp(self):
68 self.defaultConfigExtract = cpPipe.ptc.PhotonTransferCurveExtractTask.ConfigClass()
69 self.defaultTaskExtract = cpPipe.ptc.PhotonTransferCurveExtractTask(config=self.defaultConfigExtract)
71 self.defaultConfigSolve = cpPipe.ptc.PhotonTransferCurveSolveTask.ConfigClass()
72 self.defaultTaskSolve = cpPipe.ptc.PhotonTransferCurveSolveTask(config=self.defaultConfigSolve)
74 self.flatMean = 2000
75 self.readNoiseAdu = 10
76 mockImageConfig = isrMock.IsrMock.ConfigClass()
78 # flatDrop is not really relevant as we replace the data
79 # but good to note it in case we change how this image is made
80 mockImageConfig.flatDrop = 0.99999
81 mockImageConfig.isTrimmed = True
83 self.flatExp1 = isrMock.FlatMock(config=mockImageConfig).run()
84 self.flatExp2 = self.flatExp1.clone()
85 (shapeY, shapeX) = self.flatExp1.getDimensions()
87 self.flatWidth = np.sqrt(self.flatMean) + self.readNoiseAdu
89 self.rng1 = np.random.RandomState(1984)
90 flatData1 = self.rng1.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
91 self.rng2 = np.random.RandomState(666)
92 flatData2 = self.rng2.normal(self.flatMean, self.flatWidth, (shapeX, shapeY))
94 self.flatExp1.image.array[:] = flatData1
95 self.flatExp2.image.array[:] = flatData2
97 # create fake PTC data to see if fit works, for one amp ('amp')
98 self.flux = 1000. # ADU/sec
99 self.timeVec = np.arange(1., 101., 5)
100 self.k2NonLinearity = -5e-6
101 # quadratic signal-chain non-linearity
102 muVec = self.flux*self.timeVec + self.k2NonLinearity*self.timeVec**2
103 self.gain = 0.75 # e-/ADU
104 self.c1 = 1./self.gain
105 self.noiseSq = 2*self.gain # 7.5 (e-)^2
106 self.a00 = -1.2e-6
107 self.c2 = -1.5e-6
108 self.c3 = -4.7e-12 # tuned so that it turns over for 200k mean
110 self.ampNames = [amp.getName() for amp in self.flatExp1.getDetector().getAmplifiers()]
111 self.dataset = PhotonTransferCurveDataset(self.ampNames, " ") # pack raw data for fitting
112 self.covariancesSqrtWeights = {}
113 for ampName in self.ampNames: # just the expTimes and means here - vars vary per function
114 self.dataset.rawExpTimes[ampName] = self.timeVec
115 self.dataset.rawMeans[ampName] = muVec
116 self.covariancesSqrtWeights[ampName] = []
118 # ISR metadata
119 self.metadataContents = TaskMetadata()
120 self.metadataContents["isr"] = {}
121 # Overscan readout noise [in ADU]
122 for amp in self.ampNames:
123 self.metadataContents["isr"][f"RESIDUAL STDEV {amp}"] = np.sqrt(self.noiseSq)/self.gain
125 def test_covAstier(self):
126 """Test to check getCovariancesAstier
128 We check that the gain is the same as the imput gain from the
129 mock data, that the covariances via FFT (as it is in
130 MeasurePhotonTransferCurveTask when doCovariancesAstier=True)
131 are the same as calculated in real space, and that Cov[0, 0]
132 (i.e., the variances) are similar to the variances calculated
133 with the standard method (when doCovariancesAstier=false),
135 """
136 extractConfig = self.defaultConfigExtract
137 extractConfig.minNumberGoodPixelsForCovariance = 5000
138 extractConfig.detectorMeasurementRegion = 'FULL'
139 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig)
141 solveConfig = self.defaultConfigSolve
142 solveConfig.ptcFitType = 'FULLCOVARIANCE'
143 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=solveConfig)
145 inputGain = self.gain
147 muStandard, varStandard = {}, {}
148 expDict = {}
149 expIds = []
150 idCounter = 0
151 for expTime in self.timeVec:
152 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain,
153 readNoiseElectrons=3,
154 expId1=idCounter, expId2=idCounter+1)
155 mockExpRef1 = PretendRef(mockExp1)
156 mockExpRef2 = PretendRef(mockExp2)
157 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1))
158 expIds.append(idCounter)
159 expIds.append(idCounter+1)
160 for ampNumber, ampName in enumerate(self.ampNames):
161 # cov has (i, j, var, cov, npix)
162 im1Area, im2Area, imStatsCtrl, mu1, mu2 = extractTask.getImageAreasMasksStats(mockExp1,
163 mockExp2)
164 muDiff, varDiff, covAstier = extractTask.measureMeanVarCov(im1Area, im2Area, imStatsCtrl,
165 mu1, mu2)
166 muStandard.setdefault(ampName, []).append(muDiff)
167 varStandard.setdefault(ampName, []).append(varDiff)
168 idCounter += 2
170 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds,
171 taskMetadata=[self.metadataContents for x in expIds])
173 # Force the last PTC dataset to have a NaN, and ensure that the
174 # task runs (DM-38029). This is a minor perturbation and does not
175 # affect the output comparison.
176 resultsExtract.outputCovariances[-2].rawMeans['C:0,0'] = [np.nan]
177 resultsExtract.outputCovariances[-2].rawVars['C:0,0'] = [np.nan]
179 resultsSolve = solveTask.run(resultsExtract.outputCovariances,
180 camera=FakeCamera([self.flatExp1.getDetector()]))
182 for amp in self.ampNames:
183 self.assertAlmostEqual(resultsSolve.outputPtcDataset.gain[amp], inputGain, places=2)
184 for v1, v2 in zip(varStandard[amp], resultsSolve.outputPtcDataset.finalVars[amp]):
185 self.assertAlmostEqual(v1/v2, 1.0, places=1)
187 def ptcFitAndCheckPtc(self, order=None, fitType=None, doTableArray=False, doFitBootstrap=False):
188 localDataset = copy.deepcopy(self.dataset)
189 localDataset.ptcFitType = fitType
190 configSolve = copy.copy(self.defaultConfigSolve)
191 configLin = cpPipe.linearity.LinearitySolveTask.ConfigClass()
192 placesTests = 6
193 if doFitBootstrap:
194 configSolve.doFitBootstrap = True
195 # Bootstrap method in cp_pipe/utils.py does multiple fits
196 # in the precense of noise. Allow for more margin of
197 # error.
198 placesTests = 3
200 if fitType == 'POLYNOMIAL':
201 if order not in [2, 3]:
202 RuntimeError("Enter a valid polynomial order for this test: 2 or 3")
203 if order == 2:
204 for ampName in self.ampNames:
205 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for
206 mu in localDataset.rawMeans[ampName]]
207 configSolve.polynomialFitDegree = 2
208 if order == 3:
209 for ampName in self.ampNames:
210 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3
211 for mu in localDataset.rawMeans[ampName]]
212 configSolve.polynomialFitDegree = 3
213 elif fitType == 'EXPAPPROXIMATION':
214 g = self.gain
215 for ampName in self.ampNames:
216 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1)
217 + self.noiseSq/(g*g))
218 for mu in localDataset.rawMeans[ampName]]
219 else:
220 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'")
222 # Initialize mask and covariance weights that will be used in fits.
223 # Covariance weights values empirically determined from one of
224 # the cases in test_covAstier.
225 matrixSize = localDataset.covMatrixSide
226 maskLength = len(localDataset.rawMeans[ampName])
227 for ampName in self.ampNames:
228 localDataset.expIdMask[ampName] = np.repeat(True, maskLength)
229 localDataset.covariancesSqrtWeights[ampName] = np.repeat(np.ones((matrixSize, matrixSize)),
230 maskLength).reshape((maskLength,
231 matrixSize,
232 matrixSize))
233 localDataset.covariancesSqrtWeights[ampName][:, 0, 0] = [0.07980188, 0.01339653, 0.0073118,
234 0.00502802, 0.00383132, 0.00309475,
235 0.00259572, 0.00223528, 0.00196273,
236 0.00174943, 0.00157794, 0.00143707,
237 0.00131929, 0.00121935, 0.0011334,
238 0.00105893, 0.00099357, 0.0009358,
239 0.00088439, 0.00083833]
240 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats
241 configLin.maxLinearAdu = 100000
242 configLin.minLinearAdu = 50000
243 if doTableArray:
244 configLin.linearityType = "LookupTable"
245 else:
246 configLin.linearityType = "Polynomial"
247 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve)
248 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin)
250 if doTableArray:
251 # Non-linearity
252 numberAmps = len(self.ampNames)
253 # localDataset: PTC dataset
254 # (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`)
255 localDataset = solveTask.fitMeasurementsToModel(localDataset)
256 # linDataset here is a lsst.pipe.base.Struct
257 linDataset = linearityTask.run(localDataset,
258 dummy=[1.0],
259 camera=FakeCamera([self.flatExp1.getDetector()]),
260 inputPhotodiodeData={},
261 inputDims={'detector': 0})
262 linDataset = linDataset.outputLinearizer
263 else:
264 localDataset = solveTask.fitMeasurementsToModel(localDataset)
265 linDataset = linearityTask.run(localDataset,
266 dummy=[1.0],
267 camera=FakeCamera([self.flatExp1.getDetector()]),
268 inputPhotodiodeData={},
269 inputDims={'detector': 0})
270 linDataset = linDataset.outputLinearizer
271 if doTableArray:
272 # check that the linearizer table has been filled out properly
273 for i in np.arange(numberAmps):
274 tMax = (configLin.maxLookupTableAdu)/self.flux
275 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu)
276 signalIdeal = timeRange*self.flux
277 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]),
278 timeRange)
279 linearizerTableRow = signalIdeal - signalUncorrected
280 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :]))
281 for j in np.arange(len(linearizerTableRow)):
282 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j],
283 places=placesTests)
284 else:
285 # check entries in localDataset, which was modified by the function
286 for ampName in self.ampNames:
287 maskAmp = localDataset.expIdMask[ampName]
288 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
289 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
290 linearPart = self.flux*finalTimeVec
291 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
292 self.assertEqual(fitType, localDataset.ptcFitType)
293 self.assertAlmostEqual(self.gain, localDataset.gain[ampName])
294 if fitType == 'POLYNOMIAL':
295 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1])
296 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName])
297 if fitType == 'EXPAPPROXIMATION':
298 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0])
299 # noise already in electrons for 'EXPAPPROXIMATION' fit
300 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName])
302 # check entries in returned dataset (a dict of , for nonlinearity)
303 for ampName in self.ampNames:
304 maskAmp = localDataset.expIdMask[ampName]
305 finalMuVec = localDataset.rawMeans[ampName][maskAmp]
306 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp]
307 linearPart = self.flux*finalTimeVec
308 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart
310 # Nonlinearity fit parameters
311 # Polynomial fits are now normalized to unit flux scaling
312 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1)
313 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1],
314 places=5)
316 # Non-linearity coefficient for linearizer
317 squaredCoeff = self.k2NonLinearity/(self.flux**2)
318 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2],
319 places=placesTests)
320 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2],
321 places=placesTests)
323 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux
324 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel
325 # Fractional nonlinearity residuals
326 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals))
327 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals):
328 self.assertAlmostEqual(calc, truth, places=3)
330 def test_ptcFit(self):
331 for createArray in [True, False]:
332 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]:
333 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray)
335 def test_meanVarMeasurement(self):
336 task = self.defaultTaskExtract
337 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
338 self.flatExp2)
339 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
341 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1)
342 self.assertLess(self.flatMean - mu, 1)
344 def test_meanVarMeasurementWithNans(self):
345 task = self.defaultTaskExtract
346 self.flatExp1.image.array[20:30, :] = np.nan
347 self.flatExp2.image.array[20:30, :] = np.nan
349 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
350 self.flatExp2)
351 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
353 expectedMu1 = np.nanmean(self.flatExp1.image.array)
354 expectedMu2 = np.nanmean(self.flatExp2.image.array)
355 expectedMu = 0.5*(expectedMu1 + expectedMu2)
357 # Now the variance of the difference. First, create the diff image.
358 im1 = self.flatExp1.maskedImage
359 im2 = self.flatExp2.maskedImage
361 temp = im2.clone()
362 temp *= expectedMu1
363 diffIm = im1.clone()
364 diffIm *= expectedMu2
365 diffIm -= temp
366 diffIm /= expectedMu
368 # Divide by two as it is what measureMeanVarCov returns
369 # (variance of difference)
370 expectedVar = 0.5*np.nanvar(diffIm.image.array)
372 # Check that the standard deviations and the emans agree to
373 # less than 1 ADU
374 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1)
375 self.assertLess(expectedMu - mu, 1)
377 def test_meanVarMeasurementAllNan(self):
378 task = self.defaultTaskExtract
379 self.flatExp1.image.array[:, :] = np.nan
380 self.flatExp2.image.array[:, :] = np.nan
382 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1,
383 self.flatExp2)
384 mu, varDiff, covDiff = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2)
386 self.assertTrue(np.isnan(mu))
387 self.assertTrue(np.isnan(varDiff))
388 self.assertTrue(covDiff is None)
390 def test_makeZeroSafe(self):
391 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8]
392 someZerosArray = [1., 20, 0, 0, 90, 879, 0]
393 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0]
395 substituteValue = 1e-10
397 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue]
398 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray))
400 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray,
401 substituteValue=substituteValue)
402 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray,
403 substituteValue=substituteValue)
404 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray,
405 substituteValue=substituteValue)
407 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray):
408 self.assertEqual(exp, meas)
409 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray):
410 self.assertEqual(exp, meas)
411 for exp, meas in zip(noZerosArray, measuredNoZerosArray):
412 self.assertEqual(exp, meas)
414 def test_getInitialGoodPoints(self):
415 xs = [1, 2, 3, 4, 5, 6]
416 ys = [2*x for x in xs]
417 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
418 consecutivePointsVarDecreases=2)
419 assert np.all(points) == np.all(np.array([True for x in xs]))
421 ys[4] = 7 # Variance decreases in two consecutive points after ys[3]=8
422 ys[5] = 6
423 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0.,
424 consecutivePointsVarDecreases=2)
425 assert np.all(points) == np.all(np.array([True, True, True, True, False]))
427 def test_getExpIdsUsed(self):
428 localDataset = copy.copy(self.dataset)
430 for pair in [(12, 34), (56, 78), (90, 10)]:
431 localDataset.inputExpIdPairs["C:0,0"].append(pair)
432 localDataset.expIdMask["C:0,0"] = np.array([True, False, True])
433 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)]))
435 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now
436 with self.assertRaises(AssertionError):
437 localDataset.getExpIdsUsed("C:0,0")
439 def test_getGoodAmps(self):
440 dataset = self.dataset
442 self.assertTrue(dataset.ampNames == self.ampNames)
443 dataset.badAmps.append("C:0,1")
444 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"])
446 def runGetGainFromFlatPair(self, correctionType='NONE'):
447 extractConfig = self.defaultConfigExtract
448 extractConfig.gainCorrectionType = correctionType
449 extractConfig.minNumberGoodPixelsForCovariance = 5000
450 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig)
452 expDict = {}
453 expIds = []
454 idCounter = 0
455 inputGain = self.gain # 1.5 e/ADU
456 for expTime in self.timeVec:
457 # Approximation works better at low flux, e.g., < 10000 ADU
458 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain,
459 readNoiseElectrons=np.sqrt(self.noiseSq),
460 fluxElectrons=100,
461 expId1=idCounter, expId2=idCounter+1)
462 mockExpRef1 = PretendRef(mockExp1)
463 mockExpRef2 = PretendRef(mockExp2)
464 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1))
465 expIds.append(idCounter)
466 expIds.append(idCounter+1)
467 idCounter += 2
469 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds,
470 taskMetadata=[self.metadataContents for x in expIds])
471 for exposurePair in resultsExtract.outputCovariances:
472 for ampName in self.ampNames:
473 if exposurePair.gain[ampName] is np.nan:
474 continue
475 self.assertAlmostEqual(exposurePair.gain[ampName], inputGain, delta=0.04)
477 def test_getGainFromFlatPair(self):
478 for gainCorrectionType in ['NONE', 'SIMPLE', 'FULL', ]:
479 self.runGetGainFromFlatPair(gainCorrectionType)
482class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase):
483 def setUp(self):
484 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ")
485 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
486 'C01': [(123, 234), (345, 456), (567, 678)]}
488 def test_generalBehaviour(self):
489 test = PhotonTransferCurveDataset(['C00', 'C01'], " ")
490 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)],
491 'C01': [(123, 234), (345, 456), (567, 678)]}
494class TestMemory(lsst.utils.tests.MemoryTestCase):
495 pass
498def setup_module(module):
499 lsst.utils.tests.init()
502if __name__ == "__main__": 502 ↛ 503line 502 didn't jump to line 503, because the condition on line 502 was never true
503 lsst.utils.tests.init()
504 unittest.main()