Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# See COPYRIGHT file at the top of the source tree. 

2# 

3# This file is part of fgcmcal. 

4# 

5# Developed for the LSST Data Management System. 

6# This product includes software developed by the LSST Project 

7# (https://www.lsst.org). 

8# See the COPYRIGHT file at the top-level directory of this distribution 

9# for details of code ownership. 

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 GNU General Public License 

22# along with this program. If not, see <https://www.gnu.org/licenses/>. 

23"""General fgcmcal testing class. 

24 

25This class is used as the basis for individual obs package tests using 

26data from testdata_jointcal. 

27""" 

28 

29import os 

30import shutil 

31import numpy as np 

32import glob 

33 

34import lsst.daf.persistence as dafPersist 

35import lsst.geom as geom 

36import lsst.log 

37from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask, LoadIndexedReferenceObjectsConfig 

38from astropy import units 

39 

40import lsst.fgcmcal as fgcmcal 

41 

42 

43class FgcmcalTestBase(object): 

44 """ 

45 Base class for fgcmcal tests, to genericize some test running and setup. 

46 

47 Derive from this first, then from TestCase. 

48 """ 

49 

50 def setUp_base(self, inputDir=None, testDir=None, logLevel=None, otherArgs=[]): 

51 """ 

52 Call from your child class's setUp() to get variables built. 

53 

54 Parameters 

55 ---------- 

56 inputDir: `str`, optional 

57 Input directory 

58 testDir: `str`, optional 

59 Test directory 

60 logLevel: `str`, optional 

61 Override loglevel for command-line tasks 

62 otherArgs: `list`, default=[] 

63 List of additional arguments to send to command-line tasks 

64 """ 

65 

66 self.inputDir = inputDir 

67 self.testDir = testDir 

68 self.logLevel = logLevel 

69 self.otherArgs = otherArgs 

70 

71 self.config = None 

72 self.configfiles = [] 

73 

74 lsst.log.setLevel("daf.persistence.butler", lsst.log.FATAL) 

75 lsst.log.setLevel("CameraMapper", lsst.log.FATAL) 

76 

77 if self.logLevel is not None: 

78 self.otherArgs.extend(['--loglevel', 'fgcmcal=%s'%self.logLevel]) 

79 

80 def _testFgcmMakeLut(self, nBand, i0Std, i0Recon, i10Std, i10Recon): 

81 """ 

82 Test running of FgcmMakeLutTask 

83 

84 Parameters 

85 ---------- 

86 nBand: `int` 

87 Number of bands tested 

88 i0Std: `np.array', size nBand 

89 Values of i0Std to compare to 

90 i10Std: `np.array`, size nBand 

91 Values of i10Std to compare to 

92 i0Recon: `np.array`, size nBand 

93 Values of reconstructed i0 to compare to 

94 i10Recon: `np.array`, size nBand 

95 Values of reconsntructed i10 to compare to 

96 

97 Raises 

98 ------ 

99 Exceptions on test failures 

100 """ 

101 

102 args = [self.inputDir, '--output', self.testDir, 

103 '--doraise'] 

104 if len(self.configfiles) > 0: 

105 args.extend(['--configfile', *self.configfiles]) 

106 args.extend(self.otherArgs) 

107 

108 result = fgcmcal.FgcmMakeLutTask.parseAndRun(args=args, config=self.config) 

109 self._checkResult(result) 

110 

111 butler = dafPersist.butler.Butler(self.testDir) 

112 tempTask = fgcmcal.FgcmFitCycleTask() 

113 lutCat = butler.get('fgcmLookUpTable') 

114 fgcmLut, lutIndexVals, lutStd = fgcmcal.utilities.translateFgcmLut(lutCat, 

115 dict(tempTask.config.filterMap)) 

116 

117 # Check that we got the requested number of bands... 

118 self.assertEqual(nBand, len(lutIndexVals[0]['FILTERNAMES'])) 

119 

120 self.assertFloatsAlmostEqual(i0Std, lutStd[0]['I0STD'], msg='I0Std', rtol=1e-5) 

121 self.assertFloatsAlmostEqual(i10Std, lutStd[0]['I10STD'], msg='I10Std', rtol=1e-5) 

122 

123 indices = fgcmLut.getIndices(np.arange(nBand, dtype=np.int32), 

124 np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']), 

125 np.zeros(nBand) + lutStd[0]['O3STD'], 

126 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']), 

127 np.zeros(nBand) + lutStd[0]['ALPHASTD'], 

128 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])), 

129 np.zeros(nBand, dtype=np.int32), 

130 np.zeros(nBand) + lutStd[0]['PMBSTD']) 

131 i0 = fgcmLut.computeI0(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']), 

132 np.zeros(nBand) + lutStd[0]['O3STD'], 

133 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']), 

134 np.zeros(nBand) + lutStd[0]['ALPHASTD'], 

135 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])), 

136 np.zeros(nBand) + lutStd[0]['PMBSTD'], 

137 indices) 

138 

139 self.assertFloatsAlmostEqual(i0Recon, i0, msg='i0Recon', rtol=1e-5) 

140 

141 i1 = fgcmLut.computeI1(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']), 

142 np.zeros(nBand) + lutStd[0]['O3STD'], 

143 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']), 

144 np.zeros(nBand) + lutStd[0]['ALPHASTD'], 

145 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])), 

146 np.zeros(nBand) + lutStd[0]['PMBSTD'], 

147 indices) 

148 

149 self.assertFloatsAlmostEqual(i10Recon, i1/i0, msg='i10Recon', rtol=1e-5) 

150 

151 def _testFgcmBuildStars(self, visits, nStar, nObs): 

152 """ 

153 Test running of FgcmBuildStarsTask 

154 

155 Parameters 

156 ---------- 

157 visits: `list` 

158 List of visits to calibrate 

159 nStar: `int` 

160 Number of stars expected 

161 nObs: `int` 

162 Number of observations of stars expected 

163 

164 Raises 

165 ------ 

166 Exceptions on test failures 

167 """ 

168 

169 args = [self.inputDir, '--output', self.testDir, 

170 '--id', 'visit='+'^'.join([str(visit) for visit in visits]), 

171 '--doraise'] 

172 if len(self.configfiles) > 0: 

173 args.extend(['--configfile', *self.configfiles]) 

174 args.extend(self.otherArgs) 

175 

176 result = fgcmcal.FgcmBuildStarsTask.parseAndRun(args=args, config=self.config) 

177 self._checkResult(result) 

178 

179 butler = dafPersist.butler.Butler(self.testDir) 

180 

181 visitCat = butler.get('fgcmVisitCatalog') 

182 self.assertEqual(len(visits), len(visitCat)) 

183 

184 starIds = butler.get('fgcmStarIds') 

185 self.assertEqual(nStar, len(starIds)) 

186 

187 starObs = butler.get('fgcmStarObservations') 

188 self.assertEqual(nObs, len(starObs)) 

189 

190 def _testFgcmFitCycle(self, nZp, nGoodZp, nOkZp, nBadZp, nStdStars, nPlots, skipChecks=False): 

191 """ 

192 Test running of FgcmFitCycleTask 

193 

194 Parameters 

195 ---------- 

196 nZp: `int` 

197 Number of zeropoints created by the task 

198 nGoodZp: `int` 

199 Number of good (photometric) zeropoints created 

200 nOkZp: `int` 

201 Number of constrained zeropoints (photometric or not) 

202 nBadZp: `int` 

203 Number of unconstrained (bad) zeropoints 

204 nStdStars: `int` 

205 Number of standard stars produced 

206 nPlots: `int` 

207 Number of plots produced 

208 skipChecks: `bool`, optional 

209 Skip number checks, when running less-than-final cycle. 

210 Default is False. 

211 """ 

212 

213 args = [self.inputDir, '--output', self.testDir, 

214 '--doraise'] 

215 if len(self.configfiles) > 0: 

216 args.extend(['--configfile', *self.configfiles]) 

217 args.extend(self.otherArgs) 

218 

219 # Move into the test directory so the plots will get cleaned in tearDown 

220 # In the future, with Gen3, we will probably have a better way of managing 

221 # non-data output such as plots. 

222 cwd = os.getcwd() 

223 os.chdir(self.testDir) 

224 

225 result = fgcmcal.FgcmFitCycleTask.parseAndRun(args=args, config=self.config) 

226 self._checkResult(result) 

227 

228 # Move back to the previous directory 

229 os.chdir(cwd) 

230 

231 if skipChecks: 

232 return 

233 

234 # Check that the expected number of plots are there. 

235 plots = glob.glob(os.path.join(self.testDir, self.config.outfileBase + 

236 '_cycle%02d_plots/' % (self.config.cycleNumber) + 

237 '*.png')) 

238 self.assertEqual(nPlots, len(plots)) 

239 

240 butler = dafPersist.butler.Butler(self.testDir) 

241 

242 zps = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber) 

243 

244 # Check the numbers of zeropoints in all, good, okay, and bad 

245 self.assertEqual(len(zps), nZp) 

246 

247 gd, = np.where(zps['fgcmFlag'] == 1) 

248 self.assertEqual(len(gd), nGoodZp) 

249 

250 ok, = np.where(zps['fgcmFlag'] < 16) 

251 self.assertEqual(len(ok), nOkZp) 

252 

253 bd, = np.where(zps['fgcmFlag'] >= 16) 

254 self.assertEqual(len(bd), nBadZp) 

255 

256 # Check that there are no illegal values with the ok zeropoints 

257 test, = np.where(zps['fgcmZpt'][gd] < -9000.0) 

258 self.assertEqual(len(test), 0) 

259 

260 stds = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber) 

261 

262 self.assertEqual(len(stds), nStdStars) 

263 

264 def _testFgcmOutputProducts(self, visitDataRefName, ccdDataRefName, filterMapping, 

265 zpOffsets, testVisit, testCcd, testFilter, testBandIndex): 

266 """ 

267 Test running of FgcmOutputProductsTask 

268 

269 Parameters 

270 ---------- 

271 visitDataRefName: `str` 

272 Name of column in dataRef to get the visit 

273 ccdDataRefName: `str` 

274 Name of column in dataRef to get the ccd 

275 filterMapping: `dict` 

276 Mapping of filterName to dataRef filter names 

277 zpOffsets: `np.array` 

278 Zeropoint offsets expected 

279 testVisit: `int` 

280 Visit id to check for round-trip computations 

281 testCcd: `int` 

282 Ccd id to check for round-trip computations 

283 testFilter: `str` 

284 Filtername for testVisit/testCcd 

285 testBandIndex: `int` 

286 Band index for testVisit/testCcd 

287 """ 

288 

289 args = [self.inputDir, '--output', self.testDir, 

290 '--doraise'] 

291 if len(self.configfiles) > 0: 

292 args.extend(['--configfile', *self.configfiles]) 

293 args.extend(self.otherArgs) 

294 

295 result = fgcmcal.FgcmOutputProductsTask.parseAndRun(args=args, config=self.config, 

296 doReturnResults=True) 

297 self._checkResult(result) 

298 

299 # Extract the offsets from the results 

300 offsets = result.resultList[0].results.offsets 

301 

302 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6) 

303 

304 butler = dafPersist.butler.Butler(self.testDir) 

305 

306 # Test the reference catalog stars 

307 

308 # Read in the raw stars... 

309 rawStars = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber) 

310 

311 # Read in the new reference catalog... 

312 config = LoadIndexedReferenceObjectsConfig() 

313 config.ref_dataset_name = 'fgcm_stars' 

314 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

315 

316 # Read in a giant radius to get them all 

317 refStruct = task.loadSkyCircle(rawStars[0].getCoord(), 5.0*geom.degrees, 

318 filterName='r') 

319 

320 # Make sure all the stars are there 

321 self.assertEqual(len(rawStars), len(refStruct.refCat)) 

322 

323 # And make sure the numbers are consistent 

324 test, = np.where(rawStars['id'][0] == refStruct.refCat['id']) 

325 

326 # Perform math on numpy arrays to maintain datatypes 

327 mags = rawStars['mag_std_noabs'][:, 0].astype(np.float64) + offsets[0] 

328 fluxes = (mags*units.ABmag).to_value(units.nJy) 

329 fluxErrs = (np.log(10.)/2.5)*fluxes*rawStars['magErr_std'][:, 0].astype(np.float64) 

330 # Only check the first one 

331 self.assertFloatsAlmostEqual(fluxes[0], refStruct.refCat['r_flux'][test[0]]) 

332 self.assertFloatsAlmostEqual(fluxErrs[0], refStruct.refCat['r_fluxErr'][test[0]]) 

333 

334 # Test the psf candidate counting, ratio should be between 0.0 and 1.0 

335 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64) / 

336 refStruct.refCat['r_nTotal'].astype(np.float64)) 

337 self.assertFloatsAlmostEqual(candRatio.min(), 0.0) 

338 self.assertFloatsAlmostEqual(candRatio.max(), 1.0) 

339 

340 # Test the fgcm_photoCalib output 

341 

342 zptCat = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber) 

343 selected = (zptCat['fgcmFlag'] < 16) 

344 

345 # Read in all the calibrations, these should all be there 

346 # This test is simply to ensure that all the photoCalib files exist 

347 for rec in zptCat[selected]: 

348 testCal = butler.get('fgcm_photoCalib', 

349 dataId={visitDataRefName: int(rec['visit']), 

350 ccdDataRefName: int(rec['ccd']), 

351 'filter': filterMapping[rec['filtername']]}) 

352 self.assertIsNotNone(testCal) 

353 

354 # We do round-trip value checking on just the final one (chosen arbitrarily) 

355 testCal = butler.get('fgcm_photoCalib', 

356 dataId={visitDataRefName: int(testVisit), 

357 ccdDataRefName: int(testCcd), 

358 'filter': filterMapping[testFilter]}) 

359 self.assertIsNotNone(testCal) 

360 

361 src = butler.get('src', dataId={visitDataRefName: int(testVisit), 

362 ccdDataRefName: int(testCcd)}) 

363 

364 # Only test sources with positive flux 

365 gdSrc = (src['slot_CalibFlux_flux'] > 0.0) 

366 

367 # We need to apply the calibration offset to the fgcmzpt (which is internal 

368 # and doesn't know about that yet) 

369 testZpInd, = np.where((zptCat['visit'] == testVisit) & 

370 (zptCat['ccd'] == testCcd)) 

371 fgcmZpt = zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex] 

372 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd]) 

373 

374 if self.config.doComposeWcsJacobian: 

375 # The raw zeropoint needs to be modified to know about the wcs jacobian 

376 camera = butler.get('camera') 

377 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera) 

378 center = approxPixelAreaFields[testCcd].getBBox().getCenter() 

379 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center) 

380 fgcmZpt += -2.5*np.log10(pixAreaCorr) 

381 

382 # This is the magnitude through the mean calibration 

383 photoCalMeanCalMags = np.zeros(gdSrc.sum()) 

384 # This is the magnitude through the full focal-plane variable mags 

385 photoCalMags = np.zeros_like(photoCalMeanCalMags) 

386 # This is the magnitude with the FGCM (central-ccd) zeropoint 

387 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags) 

388 

389 for i, rec in enumerate(src[gdSrc]): 

390 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_flux']) 

391 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_flux'], 

392 rec.getCentroid()) 

393 zptMeanCalMags[i] = fgcmZpt - 2.5*np.log10(rec['slot_CalibFlux_flux']) 

394 

395 # These should be very close but some tiny differences because the fgcm value 

396 # is defined at the center of the bbox, and the photoCal is the mean over the box 

397 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

398 zptMeanCalMags, rtol=1e-6) 

399 # These should be roughly equal, but not precisely because of the focal-plane 

400 # variation. However, this is a useful sanity check for something going totally 

401 # wrong. 

402 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

403 photoCalMags, rtol=1e-2) 

404 

405 # And the photoCal error is just the zeropoint gray error 

406 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(), 

407 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr) 

408 

409 # Test the transmission output 

410 

411 visitCatalog = butler.get('fgcmVisitCatalog') 

412 lutCat = butler.get('fgcmLookUpTable') 

413 

414 testTrans = butler.get('transmission_atmosphere_fgcm', 

415 dataId={visitDataRefName: visitCatalog[0]['visit']}) 

416 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0), 

417 wavelengths=lutCat[0]['atmLambda']) 

418 

419 # The test fit is performed with the atmosphere parameters frozen 

420 # (freezeStdAtmosphere = True). Thus the only difference between 

421 # these output atmospheres and the standard is the different 

422 # airmass. Furthermore, this is a very rough comparison because 

423 # the look-up table is computed with very coarse sampling for faster 

424 # testing. 

425 # Therefore, this rough comparison can only be seen as a sanity check 

426 # and is not high precision. 

427 self.assertFloatsAlmostEqual(testResp, lutCat[0]['atmStdTrans'], atol=0.06) 

428 

429 # The second should be close to the first, but there is the airmass 

430 # difference so they aren't identical 

431 testTrans2 = butler.get('transmission_atmosphere_fgcm', 

432 dataId={visitDataRefName: visitCatalog[1]['visit']}) 

433 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0), 

434 wavelengths=lutCat[0]['atmLambda']) 

435 self.assertFloatsAlmostEqual(testResp, testResp2, atol=1e-4) 

436 

437 def _testFgcmCalibrateTract(self, visits, tract, 

438 rawRepeatability, filterNCalibMap): 

439 """ 

440 Test running of FgcmCalibrateTractTask 

441 

442 Parameters 

443 ---------- 

444 visits: `list` 

445 List of visits to calibrate 

446 tract: `int` 

447 Tract number 

448 rawRepeatability: `np.array` 

449 Expected raw repeatability after convergence. 

450 Length should be number of bands. 

451 filterNCalibMap: `dict` 

452 Mapping from filter name to number of photoCalibs created. 

453 """ 

454 

455 args = [self.inputDir, '--output', self.testDir, 

456 '--id', 'visit='+'^'.join([str(visit) for visit in visits]), 

457 'tract=%d' % (tract), 

458 '--doraise'] 

459 if len(self.configfiles) > 0: 

460 args.extend(['--configfile', *self.configfiles]) 

461 args.extend(self.otherArgs) 

462 

463 # Move into the test directory so the plots will get cleaned in tearDown 

464 # In the future, with Gen3, we will probably have a better way of managing 

465 # non-data output such as plots. 

466 cwd = os.getcwd() 

467 os.chdir(self.testDir) 

468 

469 result = fgcmcal.FgcmCalibrateTractTask.parseAndRun(args=args, config=self.config, 

470 doReturnResults=True) 

471 self._checkResult(result) 

472 

473 # Move back to the previous directory 

474 os.chdir(cwd) 

475 

476 # Check that the converged repeatability is what we expect 

477 repeatability = result.resultList[0].results.repeatability 

478 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=1e-5) 

479 

480 butler = dafPersist.butler.Butler(self.testDir) 

481 

482 # Check that the number of photoCalib objects in each filter are what we expect 

483 for filterName in filterNCalibMap.keys(): 

484 subset = butler.subset('fgcm_tract_photoCalib', tract=tract, filter=filterName) 

485 tot = 0 

486 for dataRef in subset: 

487 if butler.datasetExists('fgcm_tract_photoCalib', dataId=dataRef.dataId): 

488 tot += 1 

489 self.assertEqual(tot, filterNCalibMap[filterName]) 

490 

491 # Check that every visit got a transmission 

492 visits = butler.queryMetadata('fgcm_tract_photoCalib', ('visit'), tract=tract) 

493 for visit in visits: 

494 self.assertTrue(butler.datasetExists('transmission_atmosphere_fgcm_tract', 

495 tract=tract, visit=visit)) 

496 

497 # Check that we got the reference catalog output. 

498 # This will raise an exception if the catalog is not there. 

499 config = LoadIndexedReferenceObjectsConfig() 

500 config.ref_dataset_name = 'fgcm_stars_%d' % (tract) 

501 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

502 

503 coord = geom.SpherePoint(320.0*geom.degrees, 0.0*geom.degrees) 

504 

505 refStruct = task.loadSkyCircle(coord, 5.0*geom.degrees, filterName='r') 

506 

507 # Test the psf candidate counting, ratio should be between 0.0 and 1.0 

508 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64) / 

509 refStruct.refCat['r_nTotal'].astype(np.float64)) 

510 self.assertFloatsAlmostEqual(candRatio.min(), 0.0) 

511 self.assertFloatsAlmostEqual(candRatio.max(), 1.0) 

512 

513 # Test that temporary files aren't stored 

514 self.assertFalse(butler.datasetExists('fgcmVisitCatalog')) 

515 self.assertFalse(butler.datasetExists('fgcmStarObservations')) 

516 self.assertFalse(butler.datasetExists('fgcmStarIndices')) 

517 self.assertFalse(butler.datasetExists('fgcmReferenceStars')) 

518 

519 def _checkResult(self, result): 

520 """ 

521 Check the result output from the task 

522 

523 Parameters 

524 ---------- 

525 result: `pipeBase.struct` 

526 Result structure output from a task 

527 

528 Raises 

529 ------ 

530 Exceptions on test failures 

531 """ 

532 

533 self.assertNotEqual(result.resultList, [], 'resultList should not be empty') 

534 self.assertEqual(result.resultList[0].exitStatus, 0) 

535 

536 def tearDown(self): 

537 """ 

538 Tear down and clear directories 

539 """ 

540 

541 if getattr(self, 'config', None) is not None: 

542 del self.config 

543 

544 if os.path.exists(self.testDir): 

545 shutil.rmtree(self.testDir, True)