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 for Gen2 repos. 

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 FgcmcalTestBaseGen2(object): 

46 """ 

47 Base class for gen2 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 butler = dafPersist.butler.Butler(self.testDir) 

256 

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

258 

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

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

261 

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

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

264 

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

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

267 

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

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

270 

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

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

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

274 

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

276 

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

278 

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

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

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

282 + '*.png')) 

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

284 

285 def _testFgcmOutputProducts(self, visitDataRefName, ccdDataRefName, 

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 zpOffsets: `np.array` 

297 Zeropoint offsets expected 

298 testVisit: `int` 

299 Visit id to check for round-trip computations 

300 testCcd: `int` 

301 Ccd id to check for round-trip computations 

302 testFilter: `str` 

303 Filtername for testVisit/testCcd 

304 testBandIndex: `int` 

305 Band index for testVisit/testCcd 

306 """ 

307 

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

309 '--doraise'] 

310 if len(self.configfiles) > 0: 

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

312 args.extend(self.otherArgs) 

313 

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

315 doReturnResults=True) 

316 self._checkResult(result) 

317 

318 # Extract the offsets from the results 

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

320 

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

322 

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

324 

325 # Test the reference catalog stars 

326 

327 # Read in the raw stars... 

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

329 

330 # Read in the new reference catalog... 

331 config = LoadIndexedReferenceObjectsConfig() 

332 config.ref_dataset_name = 'fgcm_stars' 

333 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

334 

335 # Read in a giant radius to get them all 

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

337 filterName='r') 

338 

339 # Make sure all the stars are there 

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

341 

342 # And make sure the numbers are consistent 

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

344 

345 # Perform math on numpy arrays to maintain datatypes 

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

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

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

349 # Only check the first one 

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

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

352 

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

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

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

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

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

358 

359 # Test the fgcm_photoCalib output 

360 

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

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

363 

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

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

366 for rec in zptCat[selected]: 

367 testCal = butler.get('fgcm_photoCalib', 

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

369 ccdDataRefName: int(rec['detector']), 

370 'filter': rec['filtername']}) 

371 self.assertIsNotNone(testCal) 

372 

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

374 testCal = butler.get('fgcm_photoCalib', 

375 dataId={visitDataRefName: int(testVisit), 

376 ccdDataRefName: int(testCcd), 

377 'filter': testFilter}) 

378 self.assertIsNotNone(testCal) 

379 

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

381 ccdDataRefName: int(testCcd)}) 

382 

383 # Only test sources with positive flux 

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

385 

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

387 # and doesn't know about that yet) 

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

389 & (zptCat['detector'] == testCcd)) 

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

391 + zptCat['fgcmDeltaChrom'][testZpInd]) 

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

393 

394 if self.config.doComposeWcsJacobian: 

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

396 camera = butler.get('camera') 

397 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera) 

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

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

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

401 

402 # This is the magnitude through the mean calibration 

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

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

405 photoCalMags = np.zeros_like(photoCalMeanCalMags) 

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

407 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags) 

408 

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

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

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

412 rec.getCentroid()) 

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

414 

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

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

417 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

418 zptMeanCalMags, rtol=1e-6) 

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

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

421 # wrong. 

422 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

423 photoCalMags, rtol=1e-2) 

424 

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

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

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

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

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

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

431 # offsets used in the tests. 

432 

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

434 # (multiple ccds) 

435 

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

437 

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

439 

440 st = np.argsort(matchMag) 

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

442 # deltaMagBkgOffsetPercentile, we want to ensure that these stars 

443 # match on average. 

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

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

446 

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

448 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(), 

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

450 

451 # Test the transmission output 

452 

453 visitCatalog = butler.get('fgcmVisitCatalog') 

454 lutCat = butler.get('fgcmLookUpTable') 

455 

456 testTrans = butler.get('transmission_atmosphere_fgcm', 

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

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

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

460 

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

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

463 # these output atmospheres and the standard is the different 

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

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

466 # testing. 

467 

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

469 # we only care about the shape 

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

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

472 

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

474 # difference so they aren't identical. 

475 testTrans2 = butler.get('transmission_atmosphere_fgcm', 

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

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

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

479 

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

481 ratio = np.median(testResp/testResp2) 

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

483 

484 def _testFgcmCalibrateTract(self, visits, tract, 

485 rawRepeatability, filterNCalibMap): 

486 """ 

487 Test running of FgcmCalibrateTractTask 

488 

489 Parameters 

490 ---------- 

491 visits: `list` 

492 List of visits to calibrate 

493 tract: `int` 

494 Tract number 

495 rawRepeatability: `np.array` 

496 Expected raw repeatability after convergence. 

497 Length should be number of bands. 

498 filterNCalibMap: `dict` 

499 Mapping from filter name to number of photoCalibs created. 

500 """ 

501 

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

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

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

505 '--doraise'] 

506 if len(self.configfiles) > 0: 

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

508 args.extend(self.otherArgs) 

509 

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

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

512 # non-data output such as plots. 

513 cwd = os.getcwd() 

514 os.chdir(self.testDir) 

515 

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

517 doReturnResults=True) 

518 self._checkResult(result) 

519 

520 # Move back to the previous directory 

521 os.chdir(cwd) 

522 

523 # Check that the converged repeatability is what we expect 

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

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

526 

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

528 

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

530 for filterName in filterNCalibMap.keys(): 

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

532 tot = 0 

533 for dataRef in subset: 

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

535 tot += 1 

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

537 

538 # Check that every visit got a transmission 

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

540 for visit in visits: 

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

542 tract=tract, visit=visit)) 

543 

544 # Check that we got the reference catalog output. 

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

546 config = LoadIndexedReferenceObjectsConfig() 

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

548 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

549 

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

551 

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

553 

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

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

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

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

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

559 

560 # Test that temporary files aren't stored 

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

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

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

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

565 

566 def _compareBuildStars(self, butler1, butler2): 

567 """ 

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

569 repos. 

570 

571 Parameters 

572 ---------- 

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

574 """ 

575 # Check the visit catalogs are identical 

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

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

578 

579 for col in visitCat1.columns: 

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

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

582 else: 

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

584 

585 # Check that the observation catalogs have the same length 

586 # Detailed comparisons of the contents are below. 

587 starObs1 = butler1.get('fgcmStarObservations') 

588 starObs2 = butler2.get('fgcmStarObservations') 

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

590 

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

592 starIds1 = butler1.get('fgcmStarIds') 

593 starIds2 = butler2.get('fgcmStarIds') 

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

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

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

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

598 

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

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

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

602 

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

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

605 # are constructed in a different order. 

606 starIndices1 = butler1.get('fgcmStarIndices') 

607 starIndices2 = butler2.get('fgcmStarIndices') 

608 

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

610 ('dec', 'f8'), 

611 ('x', 'f8'), 

612 ('y', 'f8'), 

613 ('psf_candidate', 'b1'), 

614 ('visit', 'i4'), 

615 ('ccd', 'i4'), 

616 ('instMag', 'f4'), 

617 ('instMagErr', 'f4'), 

618 ('jacobian', 'f4')]) 

619 test2 = np.zeros_like(test1) 

620 

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

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

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

624 counter = 0 

625 obsIndex = starIndices1['obsIndex'] 

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

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

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

629 for name in test1.dtype.names: 

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

631 counter += nObs 

632 

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

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

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

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

637 counter = 0 

638 obsIndex = starIndices2['obsIndex'] 

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

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

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

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

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

644 for name in test2.dtype.names: 

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

646 counter += nObs 

647 

648 for name in test1.dtype.names: 

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

650 

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

652 """ 

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

654 

655 Parameters 

656 ---------- 

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

658 Fgcm standard stars 

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

660 Data references for source catalogs to match 

661 bandIndex : `int` 

662 Index of the band for the source catalogs 

663 offsets : `np.ndarray` 

664 Testing calibration offsets to apply to rawStars 

665 

666 Returns 

667 ------- 

668 matchMag : `np.ndarray` 

669 Array of matched magnitudes 

670 matchDelta : `np.ndarray` 

671 Array of matched deltas between src and standard stars. 

672 """ 

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

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

675 

676 matchDelta = None 

677 for dataRef in dataRefs: 

678 src = dataRef.get() 

679 photoCal = dataRef.get('fgcm_photoCalib') 

680 src = photoCal.calibrateCatalog(src) 

681 

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

683 

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

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

686 1./3600., maxmatch=1) 

687 

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

689 # Apply offset here to the catalog mag 

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

691 delta = srcMag - catMag 

692 if matchDelta is None: 

693 matchDelta = delta 

694 matchMag = catMag 

695 else: 

696 matchDelta = np.append(matchDelta, delta) 

697 matchMag = np.append(matchMag, catMag) 

698 

699 return matchMag, matchDelta 

700 

701 def _checkResult(self, result): 

702 """ 

703 Check the result output from the task 

704 

705 Parameters 

706 ---------- 

707 result: `pipeBase.struct` 

708 Result structure output from a task 

709 """ 

710 

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

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

713 

714 def tearDown(self): 

715 """ 

716 Tear down and clear directories 

717 """ 

718 

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

720 del self.config 

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

722 shutil.rmtree(self.testDir, True)