Coverage for tests/fgcmcalTestBaseGen2.py: 8%

Shortcuts 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

277 statements  

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 

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

365 bad = (zptCat['fgcmFlag'] >= 16) 

366 

367 # Check that all the good photocalibs are output. 

368 for rec in zptCat[good]: 

369 testCal = None 

370 try: 

371 testCal = butler.get('fgcm_photoCalib', 

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

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

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

375 except dafPersist.NoResults: 

376 pass 

377 

378 self.assertIsNotNone(testCal) 

379 

380 # Check that none of the bad photocalibs are output. 

381 for rec in zptCat[bad]: 

382 testCal = None 

383 try: 

384 testCal = butler.get('fgcm_photoCalib', 

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

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

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

388 except dafPersist.NoResults: 

389 pass 

390 self.assertIsNone(testCal) 

391 

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

393 testCal = butler.get('fgcm_photoCalib', 

394 dataId={visitDataRefName: int(testVisit), 

395 ccdDataRefName: int(testCcd), 

396 'filter': testFilter}) 

397 self.assertIsNotNone(testCal) 

398 

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

400 ccdDataRefName: int(testCcd)}) 

401 

402 # Only test sources with positive flux 

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

404 

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

406 # and doesn't know about that yet) 

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

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

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

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

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

412 

413 if self.config.doComposeWcsJacobian: 

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

415 camera = butler.get('camera') 

416 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera) 

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

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

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

420 

421 # This is the magnitude through the mean calibration 

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

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

424 photoCalMags = np.zeros_like(photoCalMeanCalMags) 

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

426 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags) 

427 

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

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

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

431 rec.getCentroid()) 

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

433 

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

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

436 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

437 zptMeanCalMags, rtol=1e-6) 

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

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

440 # wrong. 

441 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

442 photoCalMags, rtol=1e-2) 

443 

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

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

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

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

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

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

450 # offsets used in the tests. 

451 

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

453 # (multiple ccds) 

454 

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

456 

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

458 

459 st = np.argsort(matchMag) 

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

461 # deltaMagBkgOffsetPercentile, we want to ensure that these stars 

462 # match on average. 

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

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

465 

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

467 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(), 

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

469 

470 # Test the transmission output 

471 

472 visitCatalog = butler.get('fgcmVisitCatalog') 

473 lutCat = butler.get('fgcmLookUpTable') 

474 

475 testTrans = butler.get('transmission_atmosphere_fgcm', 

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

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

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

479 

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

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

482 # these output atmospheres and the standard is the different 

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

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

485 # testing. 

486 

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

488 # we only care about the shape 

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

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

491 

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

493 # difference so they aren't identical. 

494 testTrans2 = butler.get('transmission_atmosphere_fgcm', 

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

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

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

498 

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

500 ratio = np.median(testResp/testResp2) 

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

502 

503 def _testFgcmCalibrateTract(self, visits, tract, 

504 rawRepeatability, filterNCalibMap): 

505 """ 

506 Test running of FgcmCalibrateTractTask 

507 

508 Parameters 

509 ---------- 

510 visits: `list` 

511 List of visits to calibrate 

512 tract: `int` 

513 Tract number 

514 rawRepeatability: `np.array` 

515 Expected raw repeatability after convergence. 

516 Length should be number of bands. 

517 filterNCalibMap: `dict` 

518 Mapping from filter name to number of photoCalibs created. 

519 """ 

520 

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

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

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

524 '--doraise'] 

525 if len(self.configfiles) > 0: 

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

527 args.extend(self.otherArgs) 

528 

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

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

531 # non-data output such as plots. 

532 cwd = os.getcwd() 

533 os.chdir(self.testDir) 

534 

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

536 doReturnResults=True) 

537 self._checkResult(result) 

538 

539 # Move back to the previous directory 

540 os.chdir(cwd) 

541 

542 # Check that the converged repeatability is what we expect 

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

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

545 

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

547 

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

549 for filterName in filterNCalibMap.keys(): 

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

551 tot = 0 

552 for dataRef in subset: 

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

554 tot += 1 

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

556 

557 # Check that every visit got a transmission 

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

559 for visit in visits: 

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

561 tract=tract, visit=visit)) 

562 

563 # Check that we got the reference catalog output. 

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

565 config = LoadIndexedReferenceObjectsConfig() 

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

567 task = LoadIndexedReferenceObjectsTask(butler, config=config) 

568 

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

570 

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

572 

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

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

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

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

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

578 

579 # Test that temporary files aren't stored 

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

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

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

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

584 

585 def _compareBuildStars(self, butler1, butler2): 

586 """ 

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

588 repos. 

589 

590 Parameters 

591 ---------- 

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

593 """ 

594 # Check the visit catalogs are identical 

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

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

597 

598 for col in visitCat1.columns: 

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

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

601 else: 

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

603 

604 # Check that the observation catalogs have the same length 

605 # Detailed comparisons of the contents are below. 

606 starObs1 = butler1.get('fgcmStarObservations') 

607 starObs2 = butler2.get('fgcmStarObservations') 

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

609 

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

611 starIds1 = butler1.get('fgcmStarIds') 

612 starIds2 = butler2.get('fgcmStarIds') 

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

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

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

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

617 

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

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

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

621 

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

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

624 # are constructed in a different order. 

625 starIndices1 = butler1.get('fgcmStarIndices') 

626 starIndices2 = butler2.get('fgcmStarIndices') 

627 

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

629 ('dec', 'f8'), 

630 ('x', 'f8'), 

631 ('y', 'f8'), 

632 ('psf_candidate', 'b1'), 

633 ('visit', 'i4'), 

634 ('ccd', 'i4'), 

635 ('instMag', 'f4'), 

636 ('instMagErr', 'f4'), 

637 ('jacobian', 'f4')]) 

638 test2 = np.zeros_like(test1) 

639 

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

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

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

643 counter = 0 

644 obsIndex = starIndices1['obsIndex'] 

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

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

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

648 for name in test1.dtype.names: 

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

650 counter += nObs 

651 

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

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

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

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

656 counter = 0 

657 obsIndex = starIndices2['obsIndex'] 

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

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

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

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

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

663 for name in test2.dtype.names: 

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

665 counter += nObs 

666 

667 for name in test1.dtype.names: 

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

669 

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

671 """ 

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

673 

674 Parameters 

675 ---------- 

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

677 Fgcm standard stars 

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

679 Data references for source catalogs to match 

680 bandIndex : `int` 

681 Index of the band for the source catalogs 

682 offsets : `np.ndarray` 

683 Testing calibration offsets to apply to rawStars 

684 

685 Returns 

686 ------- 

687 matchMag : `np.ndarray` 

688 Array of matched magnitudes 

689 matchDelta : `np.ndarray` 

690 Array of matched deltas between src and standard stars. 

691 """ 

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

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

694 

695 matchDelta = None 

696 for dataRef in dataRefs: 

697 src = dataRef.get() 

698 photoCal = dataRef.get('fgcm_photoCalib') 

699 src = photoCal.calibrateCatalog(src) 

700 

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

702 

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

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

705 1./3600., maxmatch=1) 

706 

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

708 # Apply offset here to the catalog mag 

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

710 delta = srcMag - catMag 

711 if matchDelta is None: 

712 matchDelta = delta 

713 matchMag = catMag 

714 else: 

715 matchDelta = np.append(matchDelta, delta) 

716 matchMag = np.append(matchMag, catMag) 

717 

718 return matchMag, matchDelta 

719 

720 def _checkResult(self, result): 

721 """ 

722 Check the result output from the task 

723 

724 Parameters 

725 ---------- 

726 result: `pipeBase.struct` 

727 Result structure output from a task 

728 """ 

729 

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

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

732 

733 def tearDown(self): 

734 """ 

735 Tear down and clear directories 

736 """ 

737 

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

739 del self.config 

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

741 shutil.rmtree(self.testDir, True)