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 filterMapDict = dict(tempTask.config.physicalFilterMap) 

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

114 filterMapDict) 

115 

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

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

118 

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

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

121 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

136 indices) 

137 

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

139 

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

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

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

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

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

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

146 indices) 

147 

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

149 

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

151 """ 

152 Test running of FgcmBuildStarsTableTask 

153 

154 Parameters 

155 ---------- 

156 visits: `list` 

157 List of visits to calibrate 

158 nStar: `int` 

159 Number of stars expected 

160 nObs: `int` 

161 Number of observations of stars expected 

162 """ 

163 

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

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

166 '--doraise'] 

167 if len(self.configfiles) > 0: 

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

169 args.extend(self.otherArgs) 

170 

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

172 self._checkResult(result) 

173 

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

175 

176 visitCat = butler.get('fgcmVisitCatalog') 

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

178 

179 starIds = butler.get('fgcmStarIds') 

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

181 

182 starObs = butler.get('fgcmStarObservations') 

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

184 

185 def _testFgcmBuildStarsAndCompare(self, visits): 

186 """ 

187 Test running of FgcmBuildStarsTask and compare to Table run 

188 

189 Parameters 

190 ---------- 

191 visits: `list` 

192 List of visits to calibrate 

193 """ 

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

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

196 '--doraise'] 

197 if len(self.configfiles) > 0: 

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

199 args.extend(self.otherArgs) 

200 

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

202 self._checkResult(result) 

203 

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

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

206 

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

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

209 # order (hence the stars are rearranged). 

210 self._compareBuildStars(butlerSrc, butlerTable) 

211 

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

213 """ 

214 Test running of FgcmFitCycleTask 

215 

216 Parameters 

217 ---------- 

218 nZp: `int` 

219 Number of zeropoints created by the task 

220 nGoodZp: `int` 

221 Number of good (photometric) zeropoints created 

222 nOkZp: `int` 

223 Number of constrained zeropoints (photometric or not) 

224 nBadZp: `int` 

225 Number of unconstrained (bad) zeropoints 

226 nStdStars: `int` 

227 Number of standard stars produced 

228 nPlots: `int` 

229 Number of plots produced 

230 skipChecks: `bool`, optional 

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

232 Default is False. 

233 """ 

234 

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

236 '--doraise'] 

237 if len(self.configfiles) > 0: 

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

239 args.extend(self.otherArgs) 

240 

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

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

243 # non-data output such as plots. 

244 cwd = os.getcwd() 

245 os.chdir(self.testDir) 

246 

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

248 self._checkResult(result) 

249 

250 # Move back to the previous directory 

251 os.chdir(cwd) 

252 

253 if skipChecks: 

254 return 

255 

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

257 

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

259 

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

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

262 

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

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

265 

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

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

268 

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

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

271 

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

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

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

275 

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

277 

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

279 

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

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

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

283 + '*.png')) 

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

285 

286 def _testFgcmOutputProducts(self, visitDataRefName, ccdDataRefName, 

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

288 """ 

289 Test running of FgcmOutputProductsTask 

290 

291 Parameters 

292 ---------- 

293 visitDataRefName: `str` 

294 Name of column in dataRef to get the visit 

295 ccdDataRefName: `str` 

296 Name of column in dataRef to get the ccd 

297 zpOffsets: `np.array` 

298 Zeropoint offsets expected 

299 testVisit: `int` 

300 Visit id to check for round-trip computations 

301 testCcd: `int` 

302 Ccd id to check for round-trip computations 

303 testFilter: `str` 

304 Filtername for testVisit/testCcd 

305 testBandIndex: `int` 

306 Band index for testVisit/testCcd 

307 """ 

308 

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

310 '--doraise'] 

311 if len(self.configfiles) > 0: 

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

313 args.extend(self.otherArgs) 

314 

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

316 doReturnResults=True) 

317 self._checkResult(result) 

318 

319 # Extract the offsets from the results 

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

321 

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

323 

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

325 

326 # Test the reference catalog stars 

327 

328 # Read in the raw stars... 

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

330 

331 # Read in the new reference catalog... 

332 config = LoadIndexedReferenceObjectsConfig() 

333 config.ref_dataset_name = 'fgcm_stars' 

334 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

335 

336 # Read in a giant radius to get them all 

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

338 filterName='r') 

339 

340 # Make sure all the stars are there 

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

342 

343 # And make sure the numbers are consistent 

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

345 

346 # Perform math on numpy arrays to maintain datatypes 

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

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

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

350 # Only check the first one 

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

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

353 

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

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

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

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

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

359 

360 # Test the fgcm_photoCalib output 

361 

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

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

364 

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

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

367 for rec in zptCat[selected]: 

368 testCal = butler.get('fgcm_photoCalib', 

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

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

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

372 self.assertIsNotNone(testCal) 

373 

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

375 testCal = butler.get('fgcm_photoCalib', 

376 dataId={visitDataRefName: int(testVisit), 

377 ccdDataRefName: int(testCcd), 

378 'filter': testFilter}) 

379 self.assertIsNotNone(testCal) 

380 

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

382 ccdDataRefName: int(testCcd)}) 

383 

384 # Only test sources with positive flux 

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

386 

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

388 # and doesn't know about that yet) 

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

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

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

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

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

394 

395 if self.config.doComposeWcsJacobian: 

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

397 camera = butler.get('camera') 

398 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera) 

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

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

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

402 

403 # This is the magnitude through the mean calibration 

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

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

406 photoCalMags = np.zeros_like(photoCalMeanCalMags) 

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

408 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags) 

409 

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

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

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

413 rec.getCentroid()) 

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

415 

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

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

418 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

419 zptMeanCalMags, rtol=1e-6) 

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

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

422 # wrong. 

423 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

424 photoCalMags, rtol=1e-2) 

425 

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

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

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

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

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

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

432 # offsets used in the tests. 

433 

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

435 # (multiple ccds) 

436 

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

438 

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

440 

441 st = np.argsort(matchMag) 

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

443 # deltaMagBkgOffsetPercentile, we want to ensure that these stars 

444 # match on average. 

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

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

447 

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

449 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(), 

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

451 

452 # Test the transmission output 

453 

454 visitCatalog = butler.get('fgcmVisitCatalog') 

455 lutCat = butler.get('fgcmLookUpTable') 

456 

457 testTrans = butler.get('transmission_atmosphere_fgcm', 

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

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

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

461 

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

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

464 # these output atmospheres and the standard is the different 

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

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

467 # testing. 

468 

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

470 # we only care about the shape 

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

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

473 

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

475 # difference so they aren't identical. 

476 testTrans2 = butler.get('transmission_atmosphere_fgcm', 

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

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

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

480 

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

482 ratio = np.median(testResp/testResp2) 

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

484 

485 def _testFgcmCalibrateTract(self, visits, tract, 

486 rawRepeatability, filterNCalibMap): 

487 """ 

488 Test running of FgcmCalibrateTractTask 

489 

490 Parameters 

491 ---------- 

492 visits: `list` 

493 List of visits to calibrate 

494 tract: `int` 

495 Tract number 

496 rawRepeatability: `np.array` 

497 Expected raw repeatability after convergence. 

498 Length should be number of bands. 

499 filterNCalibMap: `dict` 

500 Mapping from filter name to number of photoCalibs created. 

501 """ 

502 

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

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

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

506 '--doraise'] 

507 if len(self.configfiles) > 0: 

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

509 args.extend(self.otherArgs) 

510 

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

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

513 # non-data output such as plots. 

514 cwd = os.getcwd() 

515 os.chdir(self.testDir) 

516 

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

518 doReturnResults=True) 

519 self._checkResult(result) 

520 

521 # Move back to the previous directory 

522 os.chdir(cwd) 

523 

524 # Check that the converged repeatability is what we expect 

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

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

527 

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

529 

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

531 for filterName in filterNCalibMap.keys(): 

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

533 tot = 0 

534 for dataRef in subset: 

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

536 tot += 1 

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

538 

539 # Check that every visit got a transmission 

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

541 for visit in visits: 

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

543 tract=tract, visit=visit)) 

544 

545 # Check that we got the reference catalog output. 

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

547 config = LoadIndexedReferenceObjectsConfig() 

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

549 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

550 

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

552 

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

554 

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

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

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

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

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

560 

561 # Test that temporary files aren't stored 

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

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

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

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

566 

567 def _compareBuildStars(self, butler1, butler2): 

568 """ 

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

570 repos. 

571 

572 Parameters 

573 ---------- 

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

575 """ 

576 # Check the visit catalogs are identical 

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

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

579 

580 for col in visitCat1.columns: 

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

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

583 else: 

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

585 

586 # Check that the observation catalogs have the same length 

587 # Detailed comparisons of the contents are below. 

588 starObs1 = butler1.get('fgcmStarObservations') 

589 starObs2 = butler2.get('fgcmStarObservations') 

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

591 

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

593 starIds1 = butler1.get('fgcmStarIds') 

594 starIds2 = butler2.get('fgcmStarIds') 

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

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

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

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

599 

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

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

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

603 

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

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

606 # are constructed in a different order. 

607 starIndices1 = butler1.get('fgcmStarIndices') 

608 starIndices2 = butler2.get('fgcmStarIndices') 

609 

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

611 ('dec', 'f8'), 

612 ('x', 'f8'), 

613 ('y', 'f8'), 

614 ('psf_candidate', 'b1'), 

615 ('visit', 'i4'), 

616 ('ccd', 'i4'), 

617 ('instMag', 'f4'), 

618 ('instMagErr', 'f4'), 

619 ('jacobian', 'f4')]) 

620 test2 = np.zeros_like(test1) 

621 

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

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

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

625 counter = 0 

626 obsIndex = starIndices1['obsIndex'] 

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

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

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

630 for name in test1.dtype.names: 

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

632 counter += nObs 

633 

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

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

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

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

638 counter = 0 

639 obsIndex = starIndices2['obsIndex'] 

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

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

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

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

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

645 for name in test2.dtype.names: 

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

647 counter += nObs 

648 

649 for name in test1.dtype.names: 

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

651 

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

653 """ 

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

655 

656 Parameters 

657 ---------- 

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

659 Fgcm standard stars 

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

661 Data references for source catalogs to match 

662 bandIndex : `int` 

663 Index of the band for the source catalogs 

664 offsets : `np.ndarray` 

665 Testing calibration offsets to apply to rawStars 

666 

667 Returns 

668 ------- 

669 matchMag : `np.ndarray` 

670 Array of matched magnitudes 

671 matchDelta : `np.ndarray` 

672 Array of matched deltas between src and standard stars. 

673 """ 

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

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

676 

677 matchDelta = None 

678 for dataRef in dataRefs: 

679 src = dataRef.get() 

680 photoCal = dataRef.get('fgcm_photoCalib') 

681 src = photoCal.calibrateCatalog(src) 

682 

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

684 

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

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

687 1./3600., maxmatch=1) 

688 

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

690 # Apply offset here to the catalog mag 

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

692 delta = srcMag - catMag 

693 if matchDelta is None: 

694 matchDelta = delta 

695 matchMag = catMag 

696 else: 

697 matchDelta = np.append(matchDelta, delta) 

698 matchMag = np.append(matchMag, catMag) 

699 

700 return matchMag, matchDelta 

701 

702 def _checkResult(self, result): 

703 """ 

704 Check the result output from the task 

705 

706 Parameters 

707 ---------- 

708 result: `pipeBase.struct` 

709 Result structure output from a task 

710 """ 

711 

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

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

714 

715 def tearDown(self): 

716 """ 

717 Tear down and clear directories 

718 """ 

719 

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

721 del self.config 

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

723 shutil.rmtree(self.testDir, True)