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 

373 if self.config.doComposeWcsJacobian: 

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

375 camera = butler.get('camera') 

376 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera) 

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

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

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

380 

381 # This is the magnitude through the mean calibration 

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

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

384 photoCalMags = np.zeros_like(photoCalMeanCalMags) 

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

386 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags) 

387 

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

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

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

391 rec.getCentroid()) 

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

393 

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

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

396 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

397 zptMeanCalMags, rtol=1e-6) 

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

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

400 # wrong. 

401 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

402 photoCalMags, rtol=1e-2) 

403 

404 # Test the transmission output 

405 

406 visitCatalog = butler.get('fgcmVisitCatalog') 

407 lutCat = butler.get('fgcmLookUpTable') 

408 

409 testTrans = butler.get('transmission_atmosphere_fgcm', 

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

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

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

413 

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

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

416 # these output atmospheres and the standard is the different 

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

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

419 # testing. 

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

421 # and is not high precision. 

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

423 

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

425 # difference so they aren't identical 

426 testTrans2 = butler.get('transmission_atmosphere_fgcm', 

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

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

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

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

431 

432 def _testFgcmCalibrateTract(self, visits, tract, 

433 rawRepeatability, filterNCalibMap): 

434 """ 

435 Test running of FgcmCalibrateTractTask 

436 

437 Parameters 

438 ---------- 

439 visits: `list` 

440 List of visits to calibrate 

441 tract: `int` 

442 Tract number 

443 rawRepeatability: `np.array` 

444 Expected raw repeatability after convergence. 

445 Length should be number of bands. 

446 filterNCalibMap: `dict` 

447 Mapping from filter name to number of photoCalibs created. 

448 """ 

449 

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

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

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

453 '--doraise'] 

454 if len(self.configfiles) > 0: 

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

456 args.extend(self.otherArgs) 

457 

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

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

460 # non-data output such as plots. 

461 cwd = os.getcwd() 

462 os.chdir(self.testDir) 

463 

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

465 doReturnResults=True) 

466 self._checkResult(result) 

467 

468 # Move back to the previous directory 

469 os.chdir(cwd) 

470 

471 # Check that the converged repeatability is what we expect 

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

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

474 

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

476 

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

478 for filterName in filterNCalibMap.keys(): 

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

480 tot = 0 

481 for dataRef in subset: 

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

483 tot += 1 

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

485 

486 # Check that every visit got a transmission 

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

488 for visit in visits: 

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

490 tract=tract, visit=visit)) 

491 

492 # Check that we got the reference catalog output. 

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

494 config = LoadIndexedReferenceObjectsConfig() 

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

496 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

497 

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

499 

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

501 

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

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

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

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

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

507 

508 def _checkResult(self, result): 

509 """ 

510 Check the result output from the task 

511 

512 Parameters 

513 ---------- 

514 result: `pipeBase.struct` 

515 Result structure output from a task 

516 

517 Raises 

518 ------ 

519 Exceptions on test failures 

520 """ 

521 

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

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

524 

525 def tearDown(self): 

526 """ 

527 Tear down and clear directories 

528 """ 

529 

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

531 del self.config 

532 

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

534 shutil.rmtree(self.testDir, True)