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 numpy.testing as testing 

33import glob 

34import esutil 

35 

36import lsst.daf.persistence as dafPersist 

37import lsst.geom as geom 

38import lsst.log 

39from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask, LoadIndexedReferenceObjectsConfig 

40from astropy import units 

41 

42import lsst.fgcmcal as fgcmcal 

43 

44 

45class FgcmcalTestBase(object): 

46 """ 

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

48 

49 Derive from this first, then from TestCase. 

50 """ 

51 

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

53 """ 

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

55 

56 Parameters 

57 ---------- 

58 inputDir: `str`, optional 

59 Input directory 

60 testDir: `str`, optional 

61 Test directory 

62 logLevel: `str`, optional 

63 Override loglevel for command-line tasks 

64 otherArgs: `list`, default=[] 

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

66 """ 

67 

68 self.inputDir = inputDir 

69 self.testDir = testDir 

70 self.logLevel = logLevel 

71 self.otherArgs = otherArgs 

72 

73 self.config = None 

74 self.configfiles = [] 

75 

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

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

78 

79 if self.logLevel is not None: 

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

81 

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

83 """ 

84 Test running of FgcmMakeLutTask 

85 

86 Parameters 

87 ---------- 

88 nBand: `int` 

89 Number of bands tested 

90 i0Std: `np.array', size nBand 

91 Values of i0Std to compare to 

92 i10Std: `np.array`, size nBand 

93 Values of i10Std to compare to 

94 i0Recon: `np.array`, size nBand 

95 Values of reconstructed i0 to compare to 

96 i10Recon: `np.array`, size nBand 

97 Values of reconsntructed i10 to compare to 

98 """ 

99 

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

101 '--doraise'] 

102 if len(self.configfiles) > 0: 

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

104 args.extend(self.otherArgs) 

105 

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

107 self._checkResult(result) 

108 

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

110 tempTask = fgcmcal.FgcmFitCycleTask() 

111 lutCat = butler.get('fgcmLookUpTable') 

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

113 dict(tempTask.config.filterMap)) 

114 

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

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

117 

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

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

120 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

135 indices) 

136 

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

138 

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

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

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

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

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

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

145 indices) 

146 

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

148 

149 def _testFgcmBuildStarsTable(self, visits, nStar, nObs): 

150 """ 

151 Test running of FgcmBuildStarsTableTask 

152 

153 Parameters 

154 ---------- 

155 visits: `list` 

156 List of visits to calibrate 

157 nStar: `int` 

158 Number of stars expected 

159 nObs: `int` 

160 Number of observations of stars expected 

161 """ 

162 

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

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

165 '--doraise'] 

166 if len(self.configfiles) > 0: 

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

168 args.extend(self.otherArgs) 

169 

170 result = fgcmcal.FgcmBuildStarsTableTask.parseAndRun(args=args, config=self.config) 

171 self._checkResult(result) 

172 

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

174 

175 visitCat = butler.get('fgcmVisitCatalog') 

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

177 

178 starIds = butler.get('fgcmStarIds') 

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

180 

181 starObs = butler.get('fgcmStarObservations') 

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

183 

184 def _testFgcmBuildStarsAndCompare(self, visits): 

185 """ 

186 Test running of FgcmBuildStarsTask and compare to Table run 

187 

188 Parameters 

189 ---------- 

190 visits: `list` 

191 List of visits to calibrate 

192 """ 

193 args = [self.testDir, '--output', os.path.join(self.testDir, 'rerun', 'src'), 

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

195 '--doraise'] 

196 if len(self.configfiles) > 0: 

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

198 args.extend(self.otherArgs) 

199 

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

201 self._checkResult(result) 

202 

203 butlerSrc = dafPersist.Butler(os.path.join(self.testDir, 'rerun', 'src')) 

204 butlerTable = dafPersist.Butler(os.path.join(self.testDir)) 

205 

206 # We compare the two catalogs to ensure they contain the same data. They will 

207 # not be identical in ordering because the input data was ingested in a different 

208 # order (hence the stars are rearranged). 

209 self._compareBuildStars(butlerSrc, butlerTable) 

210 

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

212 """ 

213 Test running of FgcmFitCycleTask 

214 

215 Parameters 

216 ---------- 

217 nZp: `int` 

218 Number of zeropoints created by the task 

219 nGoodZp: `int` 

220 Number of good (photometric) zeropoints created 

221 nOkZp: `int` 

222 Number of constrained zeropoints (photometric or not) 

223 nBadZp: `int` 

224 Number of unconstrained (bad) zeropoints 

225 nStdStars: `int` 

226 Number of standard stars produced 

227 nPlots: `int` 

228 Number of plots produced 

229 skipChecks: `bool`, optional 

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

231 Default is False. 

232 """ 

233 

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

235 '--doraise'] 

236 if len(self.configfiles) > 0: 

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

238 args.extend(self.otherArgs) 

239 

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

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

242 # non-data output such as plots. 

243 cwd = os.getcwd() 

244 os.chdir(self.testDir) 

245 

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

247 self._checkResult(result) 

248 

249 # Move back to the previous directory 

250 os.chdir(cwd) 

251 

252 if skipChecks: 

253 return 

254 

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

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

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

258 '*.png')) 

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

260 

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

262 

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

264 

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

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

267 

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

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

270 

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

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

273 

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

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

276 

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

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

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

280 

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

282 

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

284 

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

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

287 """ 

288 Test running of FgcmOutputProductsTask 

289 

290 Parameters 

291 ---------- 

292 visitDataRefName: `str` 

293 Name of column in dataRef to get the visit 

294 ccdDataRefName: `str` 

295 Name of column in dataRef to get the ccd 

296 filterMapping: `dict` 

297 Mapping of filterName to dataRef filter names 

298 zpOffsets: `np.array` 

299 Zeropoint offsets expected 

300 testVisit: `int` 

301 Visit id to check for round-trip computations 

302 testCcd: `int` 

303 Ccd id to check for round-trip computations 

304 testFilter: `str` 

305 Filtername for testVisit/testCcd 

306 testBandIndex: `int` 

307 Band index for testVisit/testCcd 

308 """ 

309 

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

311 '--doraise'] 

312 if len(self.configfiles) > 0: 

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

314 args.extend(self.otherArgs) 

315 

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

317 doReturnResults=True) 

318 self._checkResult(result) 

319 

320 # Extract the offsets from the results 

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

322 

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

324 

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

326 

327 # Test the reference catalog stars 

328 

329 # Read in the raw stars... 

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

331 

332 # Read in the new reference catalog... 

333 config = LoadIndexedReferenceObjectsConfig() 

334 config.ref_dataset_name = 'fgcm_stars' 

335 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

336 

337 # Read in a giant radius to get them all 

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

339 filterName='r') 

340 

341 # Make sure all the stars are there 

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

343 

344 # And make sure the numbers are consistent 

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

346 

347 # Perform math on numpy arrays to maintain datatypes 

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

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

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

351 # Only check the first one 

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

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

354 

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

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

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

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

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

360 

361 # Test the fgcm_photoCalib output 

362 

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

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

365 

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

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

368 for rec in zptCat[selected]: 

369 testCal = butler.get('fgcm_photoCalib', 

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

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

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

373 self.assertIsNotNone(testCal) 

374 

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

376 testCal = butler.get('fgcm_photoCalib', 

377 dataId={visitDataRefName: int(testVisit), 

378 ccdDataRefName: int(testCcd), 

379 'filter': filterMapping[testFilter]}) 

380 self.assertIsNotNone(testCal) 

381 

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

383 ccdDataRefName: int(testCcd)}) 

384 

385 # Only test sources with positive flux 

386 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0) 

387 

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

389 # and doesn't know about that yet) 

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

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

392 fgcmZpt = (zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex] + 

393 zptCat['fgcmDeltaChrom'][testZpInd]) 

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

395 

396 if self.config.doComposeWcsJacobian: 

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

398 camera = butler.get('camera') 

399 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera) 

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

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

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

403 

404 # This is the magnitude through the mean calibration 

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

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

407 photoCalMags = np.zeros_like(photoCalMeanCalMags) 

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

409 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags) 

410 

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

412 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux']) 

413 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'], 

414 rec.getCentroid()) 

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

416 

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

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

419 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

420 zptMeanCalMags, rtol=1e-6) 

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

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

423 # wrong. 

424 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

425 photoCalMags, rtol=1e-2) 

426 

427 # The next test compares the "FGCM standard magnitudes" (which are output 

428 # from the fgcm code itself) to the "calibrated magnitudes" that are 

429 # obtained from running photoCalib.calibrateCatalog() on the original 

430 # src catalogs. This summary comparison ensures that using photoCalibs 

431 # yields the same results as what FGCM is computing internally. 

432 # Note that we additionally need to take into account the post-processing 

433 # offsets used in the tests. 

434 

435 # For decent statistics, we are matching all the sources from one visit 

436 # (multiple ccds) 

437 

438 subset = butler.subset('src', dataId={visitDataRefName: int(testVisit)}) 

439 

440 matchMag, matchDelta = self._getMatchedVisitCat(rawStars, subset, testBandIndex, offsets) 

441 

442 st = np.argsort(matchMag) 

443 # Compare the brightest 25% of stars. No matter the setting of 

444 # deltaMagBkgOffsetPercentile, we want to ensure that these stars 

445 # match on average. 

446 brightest, = np.where(matchMag < matchMag[st[int(0.25*st.size)]]) 

447 self.assertFloatsAlmostEqual(np.median(matchDelta[brightest]), 0.0, atol=0.002) 

448 

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

450 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(), 

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

452 

453 # Test the transmission output 

454 

455 visitCatalog = butler.get('fgcmVisitCatalog') 

456 lutCat = butler.get('fgcmLookUpTable') 

457 

458 testTrans = butler.get('transmission_atmosphere_fgcm', 

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

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

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

462 

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

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

465 # these output atmospheres and the standard is the different 

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

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

468 # testing. 

469 

470 # To account for overall throughput changes, we scale by the median ratio, 

471 # we only care about the shape 

472 ratio = np.median(testResp/lutCat[0]['atmStdTrans']) 

473 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.04) 

474 

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

476 # difference so they aren't identical. 

477 testTrans2 = butler.get('transmission_atmosphere_fgcm', 

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

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

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

481 

482 # As above, we scale by the ratio to compare the shape of the curve. 

483 ratio = np.median(testResp/testResp2) 

484 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04) 

485 

486 def _testFgcmCalibrateTract(self, visits, tract, 

487 rawRepeatability, filterNCalibMap): 

488 """ 

489 Test running of FgcmCalibrateTractTask 

490 

491 Parameters 

492 ---------- 

493 visits: `list` 

494 List of visits to calibrate 

495 tract: `int` 

496 Tract number 

497 rawRepeatability: `np.array` 

498 Expected raw repeatability after convergence. 

499 Length should be number of bands. 

500 filterNCalibMap: `dict` 

501 Mapping from filter name to number of photoCalibs created. 

502 """ 

503 

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

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

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

507 '--doraise'] 

508 if len(self.configfiles) > 0: 

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

510 args.extend(self.otherArgs) 

511 

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

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

514 # non-data output such as plots. 

515 cwd = os.getcwd() 

516 os.chdir(self.testDir) 

517 

518 result = fgcmcal.FgcmCalibrateTractTableTask.parseAndRun(args=args, config=self.config, 

519 doReturnResults=True) 

520 self._checkResult(result) 

521 

522 # Move back to the previous directory 

523 os.chdir(cwd) 

524 

525 # Check that the converged repeatability is what we expect 

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

527 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=4e-6) 

528 

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

530 

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

532 for filterName in filterNCalibMap.keys(): 

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

534 tot = 0 

535 for dataRef in subset: 

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

537 tot += 1 

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

539 

540 # Check that every visit got a transmission 

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

542 for visit in visits: 

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

544 tract=tract, visit=visit)) 

545 

546 # Check that we got the reference catalog output. 

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

548 config = LoadIndexedReferenceObjectsConfig() 

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

550 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

551 

552 coord = geom.SpherePoint(337.656174*geom.degrees, 0.823595*geom.degrees) 

553 

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

555 

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

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

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

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

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

561 

562 # Test that temporary files aren't stored 

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

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

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

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

567 

568 def _compareBuildStars(self, butler1, butler2): 

569 """ 

570 Compare the full set of BuildStars outputs with files from two 

571 repos. 

572 

573 Parameters 

574 ---------- 

575 butler1, butler2 : `lsst.daf.persistence.Butler` 

576 """ 

577 # Check the visit catalogs are identical 

578 visitCat1 = butler1.get('fgcmVisitCatalog').asAstropy() 

579 visitCat2 = butler2.get('fgcmVisitCatalog').asAstropy() 

580 

581 for col in visitCat1.columns: 

582 if isinstance(visitCat1[col][0], str): 

583 testing.assert_array_equal(visitCat1[col], visitCat2[col]) 

584 else: 

585 testing.assert_array_almost_equal(visitCat1[col], visitCat2[col]) 

586 

587 # Check that the observation catalogs have the same length 

588 # Detailed comparisons of the contents are below. 

589 starObs1 = butler1.get('fgcmStarObservations') 

590 starObs2 = butler2.get('fgcmStarObservations') 

591 self.assertEqual(len(starObs1), len(starObs2)) 

592 

593 # Check that the number of stars is the same and all match. 

594 starIds1 = butler1.get('fgcmStarIds') 

595 starIds2 = butler2.get('fgcmStarIds') 

596 self.assertEqual(len(starIds1), len(starIds2)) 

597 matcher = esutil.htm.Matcher(11, starIds1['ra'], starIds1['dec']) 

598 matches = matcher.match(starIds2['ra'], starIds2['dec'], 1./3600., maxmatch=1) 

599 self.assertEqual(len(matches[0]), len(starIds1)) 

600 

601 # Check that the number of observations of each star is the same. 

602 testing.assert_array_equal(starIds1['nObs'][matches[1]], 

603 starIds2['nObs'][matches[0]]) 

604 

605 # And to test the contents, we need to unravel the observations and make 

606 # sure that they are matched individually, because the two catalogs 

607 # are constructed in a different order. 

608 starIndices1 = butler1.get('fgcmStarIndices') 

609 starIndices2 = butler2.get('fgcmStarIndices') 

610 

611 test1 = np.zeros(len(starIndices1), dtype=[('ra', 'f8'), 

612 ('dec', 'f8'), 

613 ('x', 'f8'), 

614 ('y', 'f8'), 

615 ('psf_candidate', 'b1'), 

616 ('visit', 'i4'), 

617 ('ccd', 'i4'), 

618 ('instMag', 'f4'), 

619 ('instMagErr', 'f4'), 

620 ('jacobian', 'f4')]) 

621 test2 = np.zeros_like(test1) 

622 

623 # Fill the test1 numpy recarray with sorted and unpacked data from starObs1. 

624 # Note that each star has a different number of observations, leading to 

625 # a "ragged" array that is packed in here. 

626 counter = 0 

627 obsIndex = starIndices1['obsIndex'] 

628 for i in range(len(starIds1)): 

629 ind = starIds1['obsArrIndex'][matches[1][i]] 

630 nObs = starIds1['nObs'][matches[1][i]] 

631 for name in test1.dtype.names: 

632 test1[name][counter: counter + nObs] = starObs1[name][obsIndex][ind: ind + nObs] 

633 counter += nObs 

634 

635 # Fill the test2 numpy recarray with sorted and unpacked data from starObs2. 

636 # Note that we have to match these observations per star by matching "visit" 

637 # (implicitly assuming each star is observed only once per visit) to ensure 

638 # that the observations in test2 are in the same order as test1. 

639 counter = 0 

640 obsIndex = starIndices2['obsIndex'] 

641 for i in range(len(starIds2)): 

642 ind = starIds2['obsArrIndex'][matches[0][i]] 

643 nObs = starIds2['nObs'][matches[0][i]] 

644 a, b = esutil.numpy_util.match(test1['visit'][counter: counter + nObs], 

645 starObs2['visit'][obsIndex][ind: ind + nObs]) 

646 for name in test2.dtype.names: 

647 test2[name][counter: counter + nObs][a] = starObs2[name][obsIndex][ind: ind + nObs][b] 

648 counter += nObs 

649 

650 for name in test1.dtype.names: 

651 testing.assert_array_almost_equal(test1[name], test2[name]) 

652 

653 def _getMatchedVisitCat(self, rawStars, dataRefs, bandIndex, offsets): 

654 """ 

655 Get a list of matched magnitudes and deltas from calibrated src catalogs. 

656 

657 Parameters 

658 ---------- 

659 rawStars : `lsst.afw.table.SourceCatalog` 

660 Fgcm standard stars 

661 dataRefs : `list` or `lsst.daf.persist.ButlerSubset` 

662 Data references for source catalogs to match 

663 bandIndex : `int` 

664 Index of the band for the source catalogs 

665 offsets : `np.ndarray` 

666 Testing calibration offsets to apply to rawStars 

667 

668 Returns 

669 ------- 

670 matchMag : `np.ndarray` 

671 Array of matched magnitudes 

672 matchDelta : `np.ndarray` 

673 Array of matched deltas between src and standard stars. 

674 """ 

675 matcher = esutil.htm.Matcher(11, np.rad2deg(rawStars['coord_ra']), 

676 np.rad2deg(rawStars['coord_dec'])) 

677 

678 matchDelta = None 

679 for dataRef in dataRefs: 

680 src = dataRef.get() 

681 photoCal = dataRef.get('fgcm_photoCalib') 

682 src = photoCal.calibrateCatalog(src) 

683 

684 gdSrc, = np.where(np.nan_to_num(src['slot_CalibFlux_flux']) > 0.0) 

685 

686 matches = matcher.match(np.rad2deg(src['coord_ra'][gdSrc]), 

687 np.rad2deg(src['coord_dec'][gdSrc]), 

688 1./3600., maxmatch=1) 

689 

690 srcMag = src['slot_CalibFlux_mag'][gdSrc][matches[0]] 

691 # Apply offset here to the catalog mag 

692 catMag = rawStars['mag_std_noabs'][matches[1]][:, bandIndex] + offsets[bandIndex] 

693 delta = srcMag - catMag 

694 if matchDelta is None: 

695 matchDelta = delta 

696 matchMag = catMag 

697 else: 

698 matchDelta = np.append(matchDelta, delta) 

699 matchMag = np.append(matchMag, catMag) 

700 

701 return matchMag, matchDelta 

702 

703 def _checkResult(self, result): 

704 """ 

705 Check the result output from the task 

706 

707 Parameters 

708 ---------- 

709 result: `pipeBase.struct` 

710 Result structure output from a task 

711 """ 

712 

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

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

715 

716 def tearDown(self): 

717 """ 

718 Tear down and clear directories 

719 """ 

720 

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

722 del self.config 

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

724 shutil.rmtree(self.testDir, True)