Coverage for tests/test_ptc.py: 12%

297 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-05 09:09 +0000

1#!/usr/bin/env python 

2 

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.""" 

26 

27from __future__ import absolute_import, division, print_function 

28import unittest 

29import numpy as np 

30import copy 

31 

32import lsst.utils 

33import lsst.utils.tests 

34 

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) 

39 

40from lsst.pipe.base import TaskMetadata 

41 

42 

43class FakeCamera(list): 

44 def getName(self): 

45 return "FakeCam" 

46 

47 

48class PretendRef(): 

49 "A class to act as a mock exposure reference" 

50 def __init__(self, exposure): 

51 self.exp = exposure 

52 

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 

60 

61 

62class MeasurePhotonTransferCurveTaskTestCase(lsst.utils.tests.TestCase): 

63 """A test case for the PTC tasks.""" 

64 

65 def setUp(self): 

66 self.defaultConfigExtract = cpPipe.ptc.PhotonTransferCurveExtractTask.ConfigClass() 

67 self.defaultTaskExtract = cpPipe.ptc.PhotonTransferCurveExtractTask(config=self.defaultConfigExtract) 

68 

69 self.defaultConfigSolve = cpPipe.ptc.PhotonTransferCurveSolveTask.ConfigClass() 

70 self.defaultTaskSolve = cpPipe.ptc.PhotonTransferCurveSolveTask(config=self.defaultConfigSolve) 

71 

72 self.flatMean = 2000 

73 self.readNoiseAdu = 10 

74 mockImageConfig = isrMock.IsrMock.ConfigClass() 

75 

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 

80 

81 self.flatExp1 = isrMock.FlatMock(config=mockImageConfig).run() 

82 self.flatExp2 = self.flatExp1.clone() 

83 (shapeY, shapeX) = self.flatExp1.getDimensions() 

84 

85 self.flatWidth = np.sqrt(self.flatMean) + self.readNoiseAdu 

86 

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)) 

91 

92 self.flatExp1.image.array[:] = flatData1 

93 self.flatExp2.image.array[:] = flatData2 

94 

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 

107 

108 self.ampNames = [amp.getName() for amp in self.flatExp1.getDetector().getAmplifiers()] 

109 self.dataset = PhotonTransferCurveDataset(self.ampNames, " ") # pack raw data for fitting 

110 

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 

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 

121 

122 def test_covAstier(self): 

123 """Test to check getCovariancesAstier 

124 

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), 

131 

132 """ 

133 extractConfig = self.defaultConfigExtract 

134 extractConfig.minNumberGoodPixelsForCovariance = 5000 

135 extractConfig.detectorMeasurementRegion = 'FULL' 

136 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig) 

137 

138 solveConfig = self.defaultConfigSolve 

139 solveConfig.ptcFitType = 'FULLCOVARIANCE' 

140 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=solveConfig) 

141 

142 inputGain = 0.75 

143 

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 

166 

167 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds, 

168 taskMetadata=[self.metadataContents]) 

169 resultsSolve = solveTask.run(resultsExtract.outputCovariances, 

170 camera=FakeCamera([self.flatExp1.getDetector()])) 

171 

172 for amp in self.ampNames: 

173 self.assertAlmostEqual(resultsSolve.outputPtcDataset.gain[amp], inputGain, places=2) 

174 for v1, v2 in zip(varStandard[amp], resultsSolve.outputPtcDataset.finalVars[amp]): 

175 self.assertAlmostEqual(v1/v2, 1.0, places=1) 

176 

177 def ptcFitAndCheckPtc(self, order=None, fitType=None, doTableArray=False, doFitBootstrap=False): 

178 localDataset = copy.copy(self.dataset) 

179 localDataset.ptcFitType = fitType 

180 configSolve = copy.copy(self.defaultConfigSolve) 

181 configLin = cpPipe.linearity.LinearitySolveTask.ConfigClass() 

182 placesTests = 6 

183 if doFitBootstrap: 

184 configSolve.doFitBootstrap = True 

185 # Bootstrap method in cp_pipe/utils.py does multiple fits 

186 # in the precense of noise. Allow for more margin of 

187 # error. 

188 placesTests = 3 

189 

190 if fitType == 'POLYNOMIAL': 

191 if order not in [2, 3]: 

192 RuntimeError("Enter a valid polynomial order for this test: 2 or 3") 

193 if order == 2: 

194 for ampName in self.ampNames: 

195 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 for 

196 mu in localDataset.rawMeans[ampName]] 

197 configSolve.polynomialFitDegree = 2 

198 if order == 3: 

199 for ampName in self.ampNames: 

200 localDataset.rawVars[ampName] = [self.noiseSq + self.c1*mu + self.c2*mu**2 + self.c3*mu**3 

201 for mu in localDataset.rawMeans[ampName]] 

202 configSolve.polynomialFitDegree = 3 

203 elif fitType == 'EXPAPPROXIMATION': 

204 g = self.gain 

205 for ampName in self.ampNames: 

206 localDataset.rawVars[ampName] = [(0.5/(self.a00*g**2)*(np.exp(2*self.a00*mu*g)-1) 

207 + self.noiseSq/(g*g)) 

208 for mu in localDataset.rawMeans[ampName]] 

209 else: 

210 RuntimeError("Enter a fit function type: 'POLYNOMIAL' or 'EXPAPPROXIMATION'") 

211 

212 for ampName in self.ampNames: 

213 localDataset.expIdMask[ampName] = np.repeat(True, len(localDataset.rawMeans[ampName])) 

214 configLin.maxLookupTableAdu = 200000 # Max ADU in input mock flats 

215 configLin.maxLinearAdu = 100000 

216 configLin.minLinearAdu = 50000 

217 if doTableArray: 

218 configLin.linearityType = "LookupTable" 

219 else: 

220 configLin.linearityType = "Polynomial" 

221 solveTask = cpPipe.ptc.PhotonTransferCurveSolveTask(config=configSolve) 

222 linearityTask = cpPipe.linearity.LinearitySolveTask(config=configLin) 

223 

224 if doTableArray: 

225 # Non-linearity 

226 numberAmps = len(self.ampNames) 

227 # localDataset: PTC dataset 

228 # (`lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset`) 

229 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

230 # linDataset here is a lsst.pipe.base.Struct 

231 linDataset = linearityTask.run(localDataset, 

232 dummy=[1.0], 

233 camera=FakeCamera([self.flatExp1.getDetector()]), 

234 inputDims={'detector': 0}) 

235 linDataset = linDataset.outputLinearizer 

236 else: 

237 localDataset = solveTask.fitMeasurementsToModel(localDataset) 

238 linDataset = linearityTask.run(localDataset, 

239 dummy=[1.0], 

240 camera=FakeCamera([self.flatExp1.getDetector()]), 

241 inputDims={'detector': 0}) 

242 linDataset = linDataset.outputLinearizer 

243 if doTableArray: 

244 # check that the linearizer table has been filled out properly 

245 for i in np.arange(numberAmps): 

246 tMax = (configLin.maxLookupTableAdu)/self.flux 

247 timeRange = np.linspace(0., tMax, configLin.maxLookupTableAdu) 

248 signalIdeal = timeRange*self.flux 

249 signalUncorrected = funcPolynomial(np.array([0.0, self.flux, self.k2NonLinearity]), 

250 timeRange) 

251 linearizerTableRow = signalIdeal - signalUncorrected 

252 self.assertEqual(len(linearizerTableRow), len(linDataset.tableData[i, :])) 

253 for j in np.arange(len(linearizerTableRow)): 

254 self.assertAlmostEqual(linearizerTableRow[j], linDataset.tableData[i, :][j], 

255 places=placesTests) 

256 else: 

257 # check entries in localDataset, which was modified by the function 

258 for ampName in self.ampNames: 

259 maskAmp = localDataset.expIdMask[ampName] 

260 finalMuVec = localDataset.rawMeans[ampName][maskAmp] 

261 finalTimeVec = localDataset.rawExpTimes[ampName][maskAmp] 

262 linearPart = self.flux*finalTimeVec 

263 inputFracNonLinearityResiduals = 100*(linearPart - finalMuVec)/linearPart 

264 self.assertEqual(fitType, localDataset.ptcFitType) 

265 self.assertAlmostEqual(self.gain, localDataset.gain[ampName]) 

266 if fitType == 'POLYNOMIAL': 

267 self.assertAlmostEqual(self.c1, localDataset.ptcFitPars[ampName][1]) 

268 self.assertAlmostEqual(np.sqrt(self.noiseSq)*self.gain, localDataset.noise[ampName]) 

269 if fitType == 'EXPAPPROXIMATION': 

270 self.assertAlmostEqual(self.a00, localDataset.ptcFitPars[ampName][0]) 

271 # noise already in electrons for 'EXPAPPROXIMATION' fit 

272 self.assertAlmostEqual(np.sqrt(self.noiseSq), localDataset.noise[ampName]) 

273 

274 # check entries in returned dataset (a dict of , for nonlinearity) 

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 

282 # Nonlinearity fit parameters 

283 # Polynomial fits are now normalized to unit flux scaling 

284 self.assertAlmostEqual(0.0, linDataset.fitParams[ampName][0], places=1) 

285 self.assertAlmostEqual(1.0, linDataset.fitParams[ampName][1], 

286 places=5) 

287 

288 # Non-linearity coefficient for linearizer 

289 squaredCoeff = self.k2NonLinearity/(self.flux**2) 

290 self.assertAlmostEqual(squaredCoeff, linDataset.fitParams[ampName][2], 

291 places=placesTests) 

292 self.assertAlmostEqual(-squaredCoeff, linDataset.linearityCoeffs[ampName][2], 

293 places=placesTests) 

294 

295 linearPartModel = linDataset.fitParams[ampName][1]*finalTimeVec*self.flux 

296 outputFracNonLinearityResiduals = 100*(linearPartModel - finalMuVec)/linearPartModel 

297 # Fractional nonlinearity residuals 

298 self.assertEqual(len(outputFracNonLinearityResiduals), len(inputFracNonLinearityResiduals)) 

299 for calc, truth in zip(outputFracNonLinearityResiduals, inputFracNonLinearityResiduals): 

300 self.assertAlmostEqual(calc, truth, places=3) 

301 

302 def test_ptcFit(self): 

303 for createArray in [True, False]: 

304 for (fitType, order) in [('POLYNOMIAL', 2), ('POLYNOMIAL', 3), ('EXPAPPROXIMATION', None)]: 

305 self.ptcFitAndCheckPtc(fitType=fitType, order=order, doTableArray=createArray) 

306 

307 def test_meanVarMeasurement(self): 

308 task = self.defaultTaskExtract 

309 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1, 

310 self.flatExp2) 

311 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2) 

312 

313 self.assertLess(self.flatWidth - np.sqrt(varDiff), 1) 

314 self.assertLess(self.flatMean - mu, 1) 

315 

316 def test_meanVarMeasurementWithNans(self): 

317 task = self.defaultTaskExtract 

318 self.flatExp1.image.array[20:30, :] = np.nan 

319 self.flatExp2.image.array[20:30, :] = np.nan 

320 

321 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1, 

322 self.flatExp2) 

323 mu, varDiff, _ = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2) 

324 

325 expectedMu1 = np.nanmean(self.flatExp1.image.array) 

326 expectedMu2 = np.nanmean(self.flatExp2.image.array) 

327 expectedMu = 0.5*(expectedMu1 + expectedMu2) 

328 

329 # Now the variance of the difference. First, create the diff image. 

330 im1 = self.flatExp1.maskedImage 

331 im2 = self.flatExp2.maskedImage 

332 

333 temp = im2.clone() 

334 temp *= expectedMu1 

335 diffIm = im1.clone() 

336 diffIm *= expectedMu2 

337 diffIm -= temp 

338 diffIm /= expectedMu 

339 

340 # Divide by two as it is what measureMeanVarCov returns 

341 # (variance of difference) 

342 expectedVar = 0.5*np.nanvar(diffIm.image.array) 

343 

344 # Check that the standard deviations and the emans agree to 

345 # less than 1 ADU 

346 self.assertLess(np.sqrt(expectedVar) - np.sqrt(varDiff), 1) 

347 self.assertLess(expectedMu - mu, 1) 

348 

349 def test_meanVarMeasurementAllNan(self): 

350 task = self.defaultTaskExtract 

351 self.flatExp1.image.array[:, :] = np.nan 

352 self.flatExp2.image.array[:, :] = np.nan 

353 

354 im1Area, im2Area, imStatsCtrl, mu1, mu2 = task.getImageAreasMasksStats(self.flatExp1, 

355 self.flatExp2) 

356 mu, varDiff, covDiff = task.measureMeanVarCov(im1Area, im2Area, imStatsCtrl, mu1, mu2) 

357 

358 self.assertTrue(np.isnan(mu)) 

359 self.assertTrue(np.isnan(varDiff)) 

360 self.assertTrue(covDiff is None) 

361 

362 def test_makeZeroSafe(self): 

363 noZerosArray = [1., 20, -35, 45578.98, 90.0, 897, 659.8] 

364 someZerosArray = [1., 20, 0, 0, 90, 879, 0] 

365 allZerosArray = [0., 0.0, 0, 0, 0.0, 0, 0] 

366 

367 substituteValue = 1e-10 

368 

369 expectedSomeZerosArray = [1., 20, substituteValue, substituteValue, 90, 879, substituteValue] 

370 expectedAllZerosArray = np.repeat(substituteValue, len(allZerosArray)) 

371 

372 measuredSomeZerosArray = self.defaultTaskSolve._makeZeroSafe(someZerosArray, 

373 substituteValue=substituteValue) 

374 measuredAllZerosArray = self.defaultTaskSolve._makeZeroSafe(allZerosArray, 

375 substituteValue=substituteValue) 

376 measuredNoZerosArray = self.defaultTaskSolve._makeZeroSafe(noZerosArray, 

377 substituteValue=substituteValue) 

378 

379 for exp, meas in zip(expectedSomeZerosArray, measuredSomeZerosArray): 

380 self.assertEqual(exp, meas) 

381 for exp, meas in zip(expectedAllZerosArray, measuredAllZerosArray): 

382 self.assertEqual(exp, meas) 

383 for exp, meas in zip(noZerosArray, measuredNoZerosArray): 

384 self.assertEqual(exp, meas) 

385 

386 def test_getInitialGoodPoints(self): 

387 xs = [1, 2, 3, 4, 5, 6] 

388 ys = [2*x for x in xs] 

389 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0., 

390 consecutivePointsVarDecreases=2) 

391 assert np.all(points) == np.all(np.array([True for x in xs])) 

392 

393 ys[4] = 7 # Variance decreases in two consecutive points after ys[3]=8 

394 ys[5] = 6 

395 points = self.defaultTaskSolve._getInitialGoodPoints(xs, ys, minVarPivotSearch=0., 

396 consecutivePointsVarDecreases=2) 

397 assert np.all(points) == np.all(np.array([True, True, True, True, False])) 

398 

399 def test_getExpIdsUsed(self): 

400 localDataset = copy.copy(self.dataset) 

401 

402 for pair in [(12, 34), (56, 78), (90, 10)]: 

403 localDataset.inputExpIdPairs["C:0,0"].append(pair) 

404 localDataset.expIdMask["C:0,0"] = np.array([True, False, True]) 

405 self.assertTrue(np.all(localDataset.getExpIdsUsed("C:0,0") == [(12, 34), (90, 10)])) 

406 

407 localDataset.expIdMask["C:0,0"] = np.array([True, False, True, True]) # wrong length now 

408 with self.assertRaises(AssertionError): 

409 localDataset.getExpIdsUsed("C:0,0") 

410 

411 def test_getGoodAmps(self): 

412 dataset = self.dataset 

413 

414 self.assertTrue(dataset.ampNames == self.ampNames) 

415 dataset.badAmps.append("C:0,1") 

416 self.assertTrue(dataset.getGoodAmps() == [amp for amp in self.ampNames if amp != "C:0,1"]) 

417 

418 def runGetGainFromFlatPair(self, correctionType='NONE'): 

419 extractConfig = self.defaultConfigExtract 

420 extractConfig.gainCorrectionType = correctionType 

421 extractConfig.minNumberGoodPixelsForCovariance = 5000 

422 extractTask = cpPipe.ptc.PhotonTransferCurveExtractTask(config=extractConfig) 

423 

424 expDict = {} 

425 expIds = [] 

426 idCounter = 0 

427 inputGain = self.gain # 1.5 e/ADU 

428 for expTime in self.timeVec: 

429 # Approximation works better at low flux, e.g., < 10000 ADU 

430 mockExp1, mockExp2 = makeMockFlats(expTime, gain=inputGain, 

431 readNoiseElectrons=np.sqrt(self.noiseSq), 

432 fluxElectrons=100, 

433 expId1=idCounter, expId2=idCounter+1) 

434 mockExpRef1 = PretendRef(mockExp1) 

435 mockExpRef2 = PretendRef(mockExp2) 

436 expDict[expTime] = ((mockExpRef1, idCounter), (mockExpRef2, idCounter+1)) 

437 expIds.append(idCounter) 

438 expIds.append(idCounter+1) 

439 idCounter += 2 

440 

441 resultsExtract = extractTask.run(inputExp=expDict, inputDims=expIds, 

442 taskMetadata=[self.metadataContents]) 

443 

444 for exposurePair in resultsExtract.outputCovariances: 

445 for ampName in self.ampNames: 

446 if exposurePair.gain[ampName] is np.nan: 

447 continue 

448 self.assertAlmostEqual(exposurePair.gain[ampName], inputGain, delta=0.16) 

449 

450 def test_getGainFromFlatPair(self): 

451 for gainCorrectionType in ['NONE', 'SIMPLE', 'FULL', ]: 

452 self.runGetGainFromFlatPair(gainCorrectionType) 

453 

454 

455class MeasurePhotonTransferCurveDatasetTestCase(lsst.utils.tests.TestCase): 

456 def setUp(self): 

457 self.ptcData = PhotonTransferCurveDataset(['C00', 'C01'], " ") 

458 self.ptcData.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)], 

459 'C01': [(123, 234), (345, 456), (567, 678)]} 

460 

461 def test_generalBehaviour(self): 

462 test = PhotonTransferCurveDataset(['C00', 'C01'], " ") 

463 test.inputExpIdPairs = {'C00': [(123, 234), (345, 456), (567, 678)], 

464 'C01': [(123, 234), (345, 456), (567, 678)]} 

465 

466 

467class TestMemory(lsst.utils.tests.MemoryTestCase): 

468 pass 

469 

470 

471def setup_module(module): 

472 lsst.utils.tests.init() 

473 

474 

475if __name__ == "__main__": 475 ↛ 476line 475 didn't jump to line 476, because the condition on line 475 was never true

476 lsst.utils.tests.init() 

477 unittest.main()