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 Gen3 repos. 

27""" 

28 

29import os 

30import shutil 

31import numpy as np 

32import glob 

33import esutil 

34 

35import click.testing 

36import lsst.ctrl.mpexec.cli.pipetask 

37 

38import lsst.daf.butler as dafButler 

39import lsst.obs.base as obsBase 

40import lsst.geom as geom 

41import lsst.log 

42 

43import lsst.fgcmcal as fgcmcal 

44 

45ROOT = os.path.abspath(os.path.dirname(__file__)) 

46 

47 

48class FgcmcalTestBase(object): 

49 """Base class for gen3 fgcmcal tests, to genericize some test running and setup. 

50 

51 Derive from this first, then from TestCase. 

52 """ 

53 def setUp_base(self, testDir): 

54 """Common routines to set up tests. 

55 

56 Parameters 

57 ---------- 

58 testDir : `str` 

59 Temporary directory to run tests in. 

60 """ 

61 self.testDir = testDir 

62 

63 def _importRepository(self, instrument, exportPath, exportFile): 

64 """Import a test repository into self.testDir 

65 

66 Parameters 

67 ---------- 

68 instrument : `str` 

69 Full string name for the instrument. 

70 exportPath : `str` 

71 Path to location of repository to export. 

72 exportFile : `str` 

73 Filename of export data. 

74 """ 

75 self.repo = os.path.join(self.testDir, 'testrepo') 

76 

77 print('Importing %s into %s' % (exportFile, self.testDir)) 

78 

79 # Make the repo and retrieve a writeable Butler 

80 _ = dafButler.Butler.makeRepo(self.repo) 

81 butler = dafButler.Butler(self.repo, writeable=True) 

82 # Register the instrument 

83 instrInstance = obsBase.utils.getInstrument(instrument) 

84 instrInstance.register(butler.registry) 

85 # Import the exportFile 

86 butler.import_(directory=exportPath, filename=exportFile, 

87 transfer='symlink', 

88 skip_dimensions={'instrument', 'detector', 'physical_filter'}) 

89 

90 def _runPipeline(self, repo, pipelineFile, queryString=None, 

91 inputCollections=None, outputCollection=None, 

92 configFiles=None, configOptions=None, 

93 registerDatasetTypes=False): 

94 """Run a pipeline via pipetask. 

95 

96 Parameters 

97 ---------- 

98 repo : `str` 

99 Gen3 repository yaml file. 

100 pipelineFile : `str` 

101 Pipeline definition file. 

102 queryString : `str`, optional 

103 String to use for "-d" data query. 

104 inputCollections : `str`, optional 

105 String to use for "-i" input collections (comma delimited). 

106 outputCollection : `str`, optional 

107 String to use for "-o" output collection. 

108 configFiles : `list` [`str`], optional 

109 List of config files to use (with "-C"). 

110 configOptions : `list` [`str`], optional 

111 List of individual config options to use (with "-c"). 

112 registerDatasetTypes : `bool`, optional 

113 Set "--register-dataset-types". 

114 

115 Returns 

116 ------- 

117 exit_code : `int` 

118 Exit code for pipetask run. 

119 

120 Raises 

121 ------ 

122 RuntimeError : Raised if the "pipetask" call fails. 

123 """ 

124 pipelineArgs = ["run", 

125 "-b", repo, 

126 "-p", pipelineFile] 

127 

128 if queryString is not None: 

129 pipelineArgs.extend(["-d", queryString]) 

130 if inputCollections is not None: 

131 pipelineArgs.extend(["-i", inputCollections]) 

132 if outputCollection is not None: 

133 pipelineArgs.extend(["-o", outputCollection]) 

134 if configFiles is not None: 

135 for configFile in configFiles: 

136 pipelineArgs.extend(["-C", configFile]) 

137 if configOptions is not None: 

138 for configOption in configOptions: 

139 pipelineArgs.extend(["-c", configOption]) 

140 if registerDatasetTypes: 

141 pipelineArgs.extend(["--register-dataset-types"]) 

142 

143 # CliRunner is an unsafe workaround for DM-26239 

144 runner = click.testing.CliRunner() 

145 results = runner.invoke(lsst.ctrl.mpexec.cli.pipetask.cli, pipelineArgs) 

146 if results.exception: 

147 raise RuntimeError("Pipeline %s failed." % (pipelineFile)) from results.exception 

148 return results.exit_code 

149 

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

151 """Test running of FgcmMakeLutTask 

152 

153 Parameters 

154 ---------- 

155 instName : `str` 

156 Short name of the instrument 

157 nBand : `int` 

158 Number of bands tested 

159 i0Std : `np.ndarray' 

160 Values of i0Std to compare to 

161 i10Std : `np.ndarray` 

162 Values of i10Std to compare to 

163 i0Recon : `np.ndarray` 

164 Values of reconstructed i0 to compare to 

165 i10Recon : `np.ndarray` 

166 Values of reconsntructed i10 to compare to 

167 """ 

168 instCamel = instName.title() 

169 

170 configFile = 'fgcmMakeLut:' + os.path.join(ROOT, 

171 'config', 

172 'fgcmMakeLut%s.py' % (instCamel)) 

173 outputCollection = '%s/testfgcmcal/lut' % (instName) 

174 

175 self._runPipeline(self.repo, 

176 os.path.join(ROOT, 

177 'pipelines', 

178 'fgcmMakeLut%s.yaml' % (instCamel)), 

179 configFiles=[configFile], 

180 inputCollections='%s/calib,%s/testdata' % (instName, instName), 

181 outputCollection=outputCollection, 

182 registerDatasetTypes=True) 

183 

184 # Check output values 

185 butler = dafButler.Butler(self.repo) 

186 lutCat = butler.get('fgcmLookUpTable', 

187 collections=[outputCollection], 

188 instrument=instName) 

189 fgcmLut, lutIndexVals, lutStd = fgcmcal.utilities.translateFgcmLut(lutCat, {}) 

190 

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

192 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

207 indices) 

208 

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

210 

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

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

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

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

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

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

217 indices) 

218 

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

220 

221 def _testFgcmBuildStarsTable(self, instName, queryString, visits, nStar, nObs): 

222 """Test running of FgcmBuildStarsTableTask 

223 

224 Parameters 

225 ---------- 

226 instName : `str` 

227 Short name of the instrument 

228 queryString : `str` 

229 Query to send to the pipetask. 

230 visits : `list` 

231 List of visits to calibrate 

232 nStar : `int` 

233 Number of stars expected 

234 nObs : `int` 

235 Number of observations of stars expected 

236 """ 

237 instCamel = instName.title() 

238 

239 configFile = 'fgcmBuildStarsTable:' + os.path.join(ROOT, 

240 'config', 

241 'fgcmBuildStarsTable%s.py' % (instCamel)) 

242 outputCollection = '%s/testfgcmcal/buildstars' % (instName) 

243 

244 self._runPipeline(self.repo, 

245 os.path.join(ROOT, 

246 'pipelines', 

247 'fgcmBuildStarsTable%s.yaml' % (instCamel)), 

248 configFiles=[configFile], 

249 inputCollections='%s/testfgcmcal/lut,refcats' % (instName), 

250 outputCollection=outputCollection, 

251 configOptions=['fgcmBuildStarsTable:ccdDataRefName=detector'], 

252 queryString=queryString, 

253 registerDatasetTypes=True) 

254 

255 butler = dafButler.Butler(self.repo) 

256 

257 visitCat = butler.get('fgcmVisitCatalog', collections=[outputCollection], 

258 instrument=instName) 

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

260 

261 starIds = butler.get('fgcmStarIds', collections=[outputCollection], 

262 instrument=instName) 

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

264 

265 starObs = butler.get('fgcmStarObservations', collections=[outputCollection], 

266 instrument=instName) 

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

268 

269 def _testFgcmFitCycle(self, instName, cycleNumber, 

270 nZp, nGoodZp, nOkZp, nBadZp, nStdStars, nPlots, 

271 skipChecks=False, extraConfig=None): 

272 """Test running of FgcmFitCycleTask 

273 

274 Parameters 

275 ---------- 

276 instName : `str` 

277 Short name of the instrument 

278 cycleNumber : `int` 

279 Fit cycle number. 

280 nZp : `int` 

281 Number of zeropoints created by the task 

282 nGoodZp : `int` 

283 Number of good (photometric) zeropoints created 

284 nOkZp : `int` 

285 Number of constrained zeropoints (photometric or not) 

286 nBadZp : `int` 

287 Number of unconstrained (bad) zeropoints 

288 nStdStars : `int` 

289 Number of standard stars produced 

290 nPlots : `int` 

291 Number of plots produced 

292 skipChecks : `bool`, optional 

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

294 extraConfig : `str`, optional 

295 Name of an extra config file to apply. 

296 """ 

297 instCamel = instName.title() 

298 

299 configFiles = ['fgcmFitCycle:' + os.path.join(ROOT, 

300 'config', 

301 'fgcmFitCycle%s.py' % (instCamel))] 

302 if extraConfig is not None: 

303 configFiles.append('fgcmFitCycle:' + extraConfig) 

304 

305 outputCollection = '%s/testfgcmcal/fit' % (instName) 

306 

307 if cycleNumber == 0: 

308 inputCollections = '%s/testfgcmcal/buildstars' % (instName) 

309 else: 

310 # We are reusing the outputCollection so we can't specify the input 

311 inputCollections = None 

312 

313 cwd = os.getcwd() 

314 os.chdir(self.testDir) 

315 

316 configOptions = ['fgcmFitCycle:cycleNumber=%d' % (cycleNumber), 

317 'fgcmFitCycle:connections.previousCycleNumber=%d' % 

318 (cycleNumber - 1), 

319 'fgcmFitCycle:connections.cycleNumber=%d' % 

320 (cycleNumber)] 

321 

322 self._runPipeline(self.repo, 

323 os.path.join(ROOT, 

324 'pipelines', 

325 'fgcmFitCycle%s.yaml' % (instCamel)), 

326 configFiles=configFiles, 

327 inputCollections=inputCollections, 

328 outputCollection=outputCollection, 

329 configOptions=configOptions, 

330 registerDatasetTypes=True) 

331 

332 os.chdir(cwd) 

333 

334 if skipChecks: 

335 return 

336 

337 butler = dafButler.Butler(self.repo) 

338 

339 config = butler.get('fgcmFitCycle_config', collections=[outputCollection]) 

340 

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

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

343 + '_cycle%02d_plots/' % (cycleNumber) 

344 + '*.png')) 

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

346 

347 zps = butler.get('fgcmZeropoints%d' % (cycleNumber), 

348 collections=[outputCollection], 

349 instrument=instName) 

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

351 

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

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

354 

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

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

357 

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

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

360 

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

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

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

364 

365 stds = butler.get('fgcmStandardStars%d' % (cycleNumber), 

366 collections=[outputCollection], 

367 instrument=instName) 

368 

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

370 

371 def _testFgcmOutputProducts(self, instName, 

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

373 """Test running of FgcmOutputProductsTask. 

374 

375 Parameters 

376 ---------- 

377 instName : `str` 

378 Short name of the instrument 

379 zpOffsets : `np.ndarray` 

380 Zeropoint offsets expected 

381 testVisit : `int` 

382 Visit id to check for round-trip computations 

383 testCcd : `int` 

384 Ccd id to check for round-trip computations 

385 testFilter : `str` 

386 Filtername for testVisit/testCcd 

387 testBandIndex : `int` 

388 Band index for testVisit/testCcd 

389 """ 

390 instCamel = instName.title() 

391 

392 configFile = 'fgcmOutputProducts:' + os.path.join(ROOT, 

393 'config', 

394 'fgcmOutputProducts%s.py' % (instCamel)) 

395 inputCollection = '%s/testfgcmcal/fit' % (instName) 

396 outputCollection = '%s/testfgcmcal/fit/output' % (instName) 

397 

398 self._runPipeline(self.repo, 

399 os.path.join(ROOT, 

400 'pipelines', 

401 'fgcmOutputProducts%s.yaml' % (instCamel)), 

402 configFiles=[configFile], 

403 inputCollections=inputCollection, 

404 outputCollection=outputCollection, 

405 configOptions=['fgcmOutputProducts:doRefcatOutput=False'], 

406 registerDatasetTypes=True) 

407 

408 butler = dafButler.Butler(self.repo) 

409 offsetCat = butler.get('fgcmReferenceCalibrationOffsets', 

410 collections=[outputCollection], instrument=instName) 

411 offsets = offsetCat['offset'][:] 

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

413 

414 config = butler.get('fgcmOutputProducts_config', 

415 collections=[outputCollection], instrument=instName) 

416 

417 rawStars = butler.get('fgcmStandardStars' + config.connections.cycleNumber, 

418 collections=[inputCollection], instrument=instName) 

419 

420 candRatio = (rawStars['npsfcand'][:, 0].astype(np.float64) 

421 / rawStars['ntotal'][:, 0].astype(np.float64)) 

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

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

424 

425 # Test the fgcm_photoCalib output 

426 zptCat = butler.get('fgcmZeropoints' + config.connections.cycleNumber, 

427 collections=[inputCollection], instrument=instName) 

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

429 

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

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

432 visits = np.unique(zptCat['visit']) 

433 photoCalibDict = {} 

434 for visit in visits: 

435 expCat = butler.get('fgcmPhotoCalibCatalog', 

436 visit=visit, 

437 collections=[outputCollection], instrument=instName) 

438 for row in expCat: 

439 if row['visit'] == visit: 

440 photoCalibDict[(visit, row['detector_id'])] = row.getPhotoCalib() 

441 

442 for rec in zptCat[selected]: 

443 self.assertTrue((rec['visit'], rec['detector']) in photoCalibDict) 

444 

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

446 testCal = photoCalibDict[(testVisit, testCcd)] 

447 

448 src = butler.get('src', visit=int(testVisit), detector=int(testCcd), 

449 collections=[outputCollection], instrument=instName) 

450 

451 # Only test sources with positive flux 

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

453 

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

455 # and doesn't know about that yet) 

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

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

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

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

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

461 

462 if config.doComposeWcsJacobian: 

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

464 refs = butler.registry.queryDatasets('camera', dimensions=['instrument'], 

465 collections=...) 

466 camera = butler.getDirect(list(refs)[0]) 

467 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera) 

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

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

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

471 

472 # This is the magnitude through the mean calibration 

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

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

475 photoCalMags = np.zeros_like(photoCalMeanCalMags) 

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

477 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags) 

478 

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

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

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

482 rec.getCentroid()) 

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

484 

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

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

487 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

488 zptMeanCalMags, rtol=1e-6) 

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

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

491 # wrong. 

492 self.assertFloatsAlmostEqual(photoCalMeanCalMags, 

493 photoCalMags, rtol=1e-2) 

494 

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

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

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

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

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

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

501 # offsets used in the tests. 

502 

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

504 # (multiple ccds) 

505 whereClause = f"instrument='{instName:s}' and visit={testVisit:d}" 

506 srcRefs = butler.registry.queryDatasets('src', dimensions=['visit'], 

507 collections='%s/testdata' % (instName), 

508 where=whereClause, 

509 findFirst=True) 

510 photoCals = [] 

511 for srcRef in srcRefs: 

512 photoCals.append(photoCalibDict[(testVisit, srcRef.dataId['detector'])]) 

513 

514 matchMag, matchDelta = self._getMatchedVisitCat(butler, srcRefs, photoCals, 

515 rawStars, testBandIndex, offsets) 

516 

517 st = np.argsort(matchMag) 

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

519 # deltaMagBkgOffsetPercentile, we want to ensure that these stars 

520 # match on average. 

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

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

523 

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

525 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(), 

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

527 

528 # Test the transmission output 

529 visitCatalog = butler.get('fgcmVisitCatalog', collections=[inputCollection], 

530 instrument=instName) 

531 lutCat = butler.get('fgcmLookUpTable', collections=[inputCollection], 

532 instrument=instName) 

533 

534 testTrans = butler.get('transmission_atmosphere_fgcm', 

535 visit=visitCatalog[0]['visit'], 

536 collections=[outputCollection], instrument=instName) 

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

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

539 

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

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

542 # these output atmospheres and the standard is the different 

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

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

545 # testing. 

546 

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

548 # we only care about the shape 

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

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

551 

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

553 # difference so they aren't identical. 

554 testTrans2 = butler.get('transmission_atmosphere_fgcm', 

555 visit=visitCatalog[1]['visit'], 

556 collections=[outputCollection], instrument=instName) 

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

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

559 

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

561 ratio = np.median(testResp/testResp2) 

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

563 

564 def _getMatchedVisitCat(self, butler, srcRefs, photoCals, 

565 rawStars, bandIndex, offsets): 

566 """ 

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

568 

569 Parameters 

570 ---------- 

571 butler : `lsst.daf.butler.Butler` 

572 srcRefs : `list` 

573 dataRefs of source catalogs 

574 photoCalibRefs : `list` 

575 dataRefs of photoCalib files, matched to srcRefs. 

576 photoCals : `list` 

577 photoCalib objects, matched to srcRefs. 

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

579 Fgcm standard stars 

580 bandIndex : `int` 

581 Index of the band for the source catalogs 

582 offsets : `np.ndarray` 

583 Testing calibration offsets to apply to rawStars 

584 

585 Returns 

586 ------- 

587 matchMag : `np.ndarray` 

588 Array of matched magnitudes 

589 matchDelta : `np.ndarray` 

590 Array of matched deltas between src and standard stars. 

591 """ 

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

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

594 

595 matchDelta = None 

596 # for dataRef in dataRefs: 

597 for srcRef, photoCal in zip(srcRefs, photoCals): 

598 src = butler.getDirect(srcRef) 

599 src = photoCal.calibrateCatalog(src) 

600 

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

602 

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

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

605 1./3600., maxmatch=1) 

606 

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

608 # Apply offset here to the catalog mag 

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

610 delta = srcMag - catMag 

611 if matchDelta is None: 

612 matchDelta = delta 

613 matchMag = catMag 

614 else: 

615 matchDelta = np.append(matchDelta, delta) 

616 matchMag = np.append(matchMag, catMag) 

617 

618 return matchMag, matchDelta 

619 

620 def _testFgcmCalibrateTract(self, instName, visits, tract, skymapName, 

621 rawRepeatability, filterNCalibMap): 

622 """Test running of FgcmCalibrateTractTask 

623 

624 Parameters 

625 ---------- 

626 instName : `str` 

627 Short name of the instrument 

628 visits : `list` 

629 List of visits to calibrate 

630 tract : `int` 

631 Tract number 

632 skymapName : `str` 

633 Name of the sky map 

634 rawRepeatability : `np.array` 

635 Expected raw repeatability after convergence. 

636 Length should be number of bands. 

637 filterNCalibMap : `dict` 

638 Mapping from filter name to number of photoCalibs created. 

639 """ 

640 instCamel = instName.title() 

641 

642 configFile = os.path.join(ROOT, 

643 'config', 

644 'fgcmCalibrateTractTable%s.py' % (instCamel)) 

645 

646 configFiles = ['fgcmCalibrateTractTable:' + configFile] 

647 outputCollection = '%s/testfgcmcal/tract' % (instName) 

648 

649 inputCollections = '%s/testfgcmcal/lut,refcats' % (instName) 

650 configOption = 'fgcmCalibrateTractTable:fgcmOutputProducts.doRefcatOutput=False' 

651 

652 queryString = f"tract={tract:d} and skymap='{skymapName:s}'" 

653 

654 self._runPipeline(self.repo, 

655 os.path.join(ROOT, 

656 'pipelines', 

657 f'fgcmCalibrateTractTable{instCamel:s}.yaml'), 

658 queryString=queryString, 

659 configFiles=configFiles, 

660 inputCollections=inputCollections, 

661 outputCollection=outputCollection, 

662 configOptions=[configOption], 

663 registerDatasetTypes=True) 

664 

665 butler = dafButler.Butler(self.repo) 

666 

667 whereClause = f"instrument='{instName:s}' and tract={tract:d} and skymap='{skymapName:s}'" 

668 

669 repRefs = butler.registry.queryDatasets('fgcmRawRepeatability', 

670 dimensions=['tract'], 

671 collections=outputCollection, 

672 where=whereClause) 

673 

674 repeatabilityCat = butler.getDirect(list(repRefs)[0]) 

675 repeatability = repeatabilityCat['rawRepeatability'][:] 

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

677 

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

679 for filterName in filterNCalibMap.keys(): 

680 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and " 

681 f"physical_filter='{filterName:s}' and skymap='{skymapName:s}'") 

682 

683 refs = butler.registry.queryDatasets('fgcmPhotoCalibTractCatalog', 

684 dimensions=['tract', 'physical_filter'], 

685 collections=outputCollection, 

686 where=whereClause) 

687 

688 count = 0 

689 for ref in set(refs): 

690 expCat = butler.getDirect(ref) 

691 test, = np.where((expCat['visit'] > 0) & (expCat['detector_id'] >= 0)) 

692 count += test.size 

693 

694 self.assertEqual(count, filterNCalibMap[filterName]) 

695 

696 # Check that every visit got a transmission 

697 for visit in visits: 

698 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and " 

699 f"visit={visit:d} and skymap='{skymapName:s}'") 

700 refs = butler.registry.queryDatasets('transmission_atmosphere_fgcm_tract', 

701 dimensions=['tract', 'visit'], 

702 collections=outputCollection, 

703 where=whereClause) 

704 self.assertEqual(len(set(refs)), 1) 

705 

706 def tearDown(self): 

707 """Tear down and clear directories 

708 """ 

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

710 shutil.rmtree(self.testDir, True)