Coverage for python/lsst/fgcmcal/fgcmBuildStarsBase.py: 20%

221 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-11 11:27 +0000

1# This file is part of fgcmcal. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21"""Base class for BuildStars using src tables or sourceTable_visit tables. 

22""" 

23 

24import abc 

25 

26import numpy as np 

27 

28import lsst.pex.config as pexConfig 

29import lsst.pipe.base as pipeBase 

30import lsst.afw.table as afwTable 

31from lsst.daf.base import PropertyList 

32from lsst.daf.base.dateTime import DateTime 

33from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry 

34 

35from .fgcmLoadReferenceCatalog import FgcmLoadReferenceCatalogTask 

36 

37import fgcm 

38 

39REFSTARS_FORMAT_VERSION = 1 

40 

41__all__ = ['FgcmBuildStarsConfigBase', 'FgcmBuildStarsBaseTask'] 

42 

43 

44class FgcmBuildStarsConfigBase(pexConfig.Config): 

45 """Base config for FgcmBuildStars tasks""" 

46 

47 instFluxField = pexConfig.Field( 

48 doc=("Faull name of the source instFlux field to use, including 'instFlux'. " 

49 "The associated flag will be implicitly included in badFlags"), 

50 dtype=str, 

51 default='slot_CalibFlux_instFlux', 

52 ) 

53 minPerBand = pexConfig.Field( 

54 doc="Minimum observations per band", 

55 dtype=int, 

56 default=2, 

57 ) 

58 matchRadius = pexConfig.Field( 

59 doc="Match radius (arcseconds)", 

60 dtype=float, 

61 default=1.0, 

62 ) 

63 isolationRadius = pexConfig.Field( 

64 doc="Isolation radius (arcseconds)", 

65 dtype=float, 

66 default=2.0, 

67 ) 

68 densityCutNside = pexConfig.Field( 

69 doc="Density cut healpix nside", 

70 dtype=int, 

71 default=128, 

72 ) 

73 densityCutMaxPerPixel = pexConfig.Field( 

74 doc="Density cut number of stars per pixel", 

75 dtype=int, 

76 default=1000, 

77 ) 

78 randomSeed = pexConfig.Field( 

79 doc="Random seed for high density down-sampling.", 

80 dtype=int, 

81 default=None, 

82 optional=True, 

83 ) 

84 matchNside = pexConfig.Field( 

85 doc="Healpix Nside for matching", 

86 dtype=int, 

87 default=4096, 

88 ) 

89 coarseNside = pexConfig.Field( 

90 doc="Healpix coarse Nside for partitioning matches", 

91 dtype=int, 

92 default=8, 

93 ) 

94 physicalFilterMap = pexConfig.DictField( 

95 doc="Mapping from 'physicalFilter' to band.", 

96 keytype=str, 

97 itemtype=str, 

98 default={}, 

99 ) 

100 requiredBands = pexConfig.ListField( 

101 doc="Bands required for each star", 

102 dtype=str, 

103 default=(), 

104 ) 

105 primaryBands = pexConfig.ListField( 

106 doc=("Bands for 'primary' star matches. " 

107 "A star must be observed in one of these bands to be considered " 

108 "as a calibration star."), 

109 dtype=str, 

110 default=None 

111 ) 

112 doApplyWcsJacobian = pexConfig.Field( 

113 doc="Apply the jacobian of the WCS to the star observations prior to fit?", 

114 dtype=bool, 

115 default=True 

116 ) 

117 doModelErrorsWithBackground = pexConfig.Field( 

118 doc="Model flux errors with background term?", 

119 dtype=bool, 

120 default=True 

121 ) 

122 psfCandidateName = pexConfig.Field( 

123 doc="Name of field with psf candidate flag for propagation", 

124 dtype=str, 

125 default="calib_psf_candidate" 

126 ) 

127 doSubtractLocalBackground = pexConfig.Field( 

128 doc=("Subtract the local background before performing calibration? " 

129 "This is only supported for circular aperture calibration fluxes."), 

130 dtype=bool, 

131 default=False 

132 ) 

133 localBackgroundFluxField = pexConfig.Field( 

134 doc="Full name of the local background instFlux field to use.", 

135 dtype=str, 

136 default='base_LocalBackground_instFlux' 

137 ) 

138 sourceSelector = sourceSelectorRegistry.makeField( 

139 doc="How to select sources", 

140 default="science" 

141 ) 

142 apertureInnerInstFluxField = pexConfig.Field( 

143 doc=("Full name of instFlux field that contains inner aperture " 

144 "flux for aperture correction proxy"), 

145 dtype=str, 

146 default='base_CircularApertureFlux_12_0_instFlux' 

147 ) 

148 apertureOuterInstFluxField = pexConfig.Field( 

149 doc=("Full name of instFlux field that contains outer aperture " 

150 "flux for aperture correction proxy"), 

151 dtype=str, 

152 default='base_CircularApertureFlux_17_0_instFlux' 

153 ) 

154 doReferenceMatches = pexConfig.Field( 

155 doc="Match reference catalog as additional constraint on calibration", 

156 dtype=bool, 

157 default=True, 

158 ) 

159 fgcmLoadReferenceCatalog = pexConfig.ConfigurableField( 

160 target=FgcmLoadReferenceCatalogTask, 

161 doc="FGCM reference object loader", 

162 ) 

163 nVisitsPerCheckpoint = pexConfig.Field( 

164 doc="Number of visits read between checkpoints", 

165 dtype=int, 

166 default=500, 

167 ) 

168 

169 def setDefaults(self): 

170 sourceSelector = self.sourceSelector["science"] 

171 sourceSelector.setDefaults() 

172 

173 sourceSelector.doFlags = True 

174 sourceSelector.doUnresolved = True 

175 sourceSelector.doSignalToNoise = True 

176 sourceSelector.doIsolated = True 

177 sourceSelector.doRequireFiniteRaDec = True 

178 

179 sourceSelector.signalToNoise.minimum = 10.0 

180 sourceSelector.signalToNoise.maximum = 1000.0 

181 

182 # FGCM operates on unresolved sources, and this setting is 

183 # appropriate for the current base_ClassificationExtendedness 

184 sourceSelector.unresolved.maximum = 0.5 

185 

186 

187class FgcmBuildStarsBaseTask(pipeBase.PipelineTask, abc.ABC): 

188 """ 

189 Base task to build stars for FGCM global calibration 

190 """ 

191 def __init__(self, initInputs=None, **kwargs): 

192 super().__init__(**kwargs) 

193 

194 self.makeSubtask("sourceSelector") 

195 # Only log warning and fatal errors from the sourceSelector 

196 self.sourceSelector.log.setLevel(self.sourceSelector.log.WARN) 

197 

198 def fgcmMakeAllStarObservations(self, groupedHandles, visitCat, 

199 sourceSchema, 

200 camera, 

201 calibFluxApertureRadius=None): 

202 """ 

203 Compile all good star observations from visits in visitCat. 

204 

205 Parameters 

206 ---------- 

207 groupedHandles : `dict` [`list` [`lsst.daf.butler.DeferredDatasetHandle`]] 

208 Dataset handles, grouped by visit. 

209 visitCat : `afw.table.BaseCatalog` 

210 Catalog with visit data for FGCM 

211 sourceSchema : `lsst.afw.table.Schema` 

212 Schema for the input src catalogs. 

213 camera : `lsst.afw.cameraGeom.Camera` 

214 calibFluxApertureRadius : `float`, optional 

215 Aperture radius for calibration flux. 

216 inStarObsCat : `afw.table.BaseCatalog` 

217 Input observation catalog. If this is incomplete, observations 

218 will be appended from when it was cut off. 

219 

220 Returns 

221 ------- 

222 fgcmStarObservations : `afw.table.BaseCatalog` 

223 Full catalog of good observations. 

224 

225 Raises 

226 ------ 

227 RuntimeError: Raised if doSubtractLocalBackground is True and 

228 calibFluxApertureRadius is not set. 

229 """ 

230 raise NotImplementedError("fgcmMakeAllStarObservations not implemented.") 

231 

232 def fgcmMakeVisitCatalog(self, camera, groupedHandles): 

233 """ 

234 Make a visit catalog with all the keys from each visit 

235 

236 Parameters 

237 ---------- 

238 camera: `lsst.afw.cameraGeom.Camera` 

239 Camera from the butler 

240 groupedHandles: `dict` [`list` [`lsst.daf.butler.DeferredDatasetHandle`]] 

241 Dataset handles, grouped by visit. 

242 

243 Returns 

244 ------- 

245 visitCat: `afw.table.BaseCatalog` 

246 """ 

247 

248 self.log.info("Assembling visitCatalog from %d visits", len(groupedHandles)) 

249 

250 nCcd = len(camera) 

251 

252 schema = self._makeFgcmVisitSchema(nCcd) 

253 

254 visitCat = afwTable.BaseCatalog(schema) 

255 visitCat.reserve(len(groupedHandles)) 

256 visitCat.resize(len(groupedHandles)) 

257 

258 visitCat['visit'] = list(groupedHandles.keys()) 

259 visitCat['used'] = 0 

260 visitCat['sources_read'] = False 

261 

262 # No matter what, fill the catalog. This will check if it was 

263 # already read. 

264 self._fillVisitCatalog(visitCat, groupedHandles) 

265 

266 return visitCat 

267 

268 def _fillVisitCatalog(self, visitCat, groupedHandles): 

269 """ 

270 Fill the visit catalog with visit metadata 

271 

272 Parameters 

273 ---------- 

274 visitCat : `afw.table.BaseCatalog` 

275 Visit catalog. See _makeFgcmVisitSchema() for schema definition. 

276 groupedHandles : `dict` [`list` [`lsst.daf.butler.DeferredDatasetHandle`]] 

277 Dataset handles, grouped by visit. 

278 """ 

279 

280 # Guarantee that these are sorted. 

281 for i, visit in enumerate(sorted(groupedHandles)): 

282 if (i % self.config.nVisitsPerCheckpoint) == 0: 

283 self.log.info("Retrieving metadata for visit %d (%d/%d)", visit, i, len(groupedHandles)) 

284 

285 handle = groupedHandles[visit][0] 

286 summary = handle.get() 

287 

288 summaryRow = summary.find(self.config.referenceCCD) 

289 if summaryRow is None: 

290 # Take the first available ccd if reference isn't available 

291 summaryRow = summary[0] 

292 

293 visitInfo = summaryRow.getVisitInfo() 

294 physicalFilter = summaryRow['physical_filter'] 

295 # Compute the median psf sigma if possible 

296 goodSigma, = np.where(summary['psfSigma'] > 0) 

297 if goodSigma.size > 2: 

298 psfSigma = np.median(summary['psfSigma'][goodSigma]) 

299 elif goodSigma.size > 0: 

300 psfSigma = summary['psfSigma'][goodSigma[0]] 

301 else: 

302 self.log.warning("Could not find any good summary psfSigma for visit %d", visit) 

303 psfSigma = 0.0 

304 # Compute median background if possible 

305 goodBackground, = np.where(np.nan_to_num(summary['skyBg']) > 0.0) 

306 if goodBackground.size > 2: 

307 skyBackground = np.median(summary['skyBg'][goodBackground]) 

308 elif goodBackground.size > 0: 

309 skyBackground = summary['skyBg'][goodBackground[0]] 

310 else: 

311 self.log.warning('Could not find any good summary skyBg for visit %d', visit) 

312 skyBackground = -1.0 

313 

314 rec = visitCat[i] 

315 rec['visit'] = visit 

316 rec['physicalFilter'] = physicalFilter 

317 # TODO DM-26991: Use the wcs to refine the focal-plane center. 

318 radec = visitInfo.getBoresightRaDec() 

319 rec['telra'] = radec.getRa().asDegrees() 

320 rec['teldec'] = radec.getDec().asDegrees() 

321 rec['telha'] = visitInfo.getBoresightHourAngle().asDegrees() 

322 rec['telrot'] = visitInfo.getBoresightRotAngle().asDegrees() 

323 rec['mjd'] = visitInfo.getDate().get(system=DateTime.MJD) 

324 rec['exptime'] = visitInfo.getExposureTime() 

325 # convert from Pa to millibar 

326 # Note that I don't know if this unit will need to be per-camera config 

327 rec['pmb'] = visitInfo.getWeather().getAirPressure() / 100 

328 # Flag to signify if this is a "deep" field. Not currently used 

329 rec['deepFlag'] = 0 

330 # Relative flat scaling (1.0 means no relative scaling) 

331 rec['scaling'][:] = 1.0 

332 # Median delta aperture, to be measured from stars 

333 rec['deltaAper'] = 0.0 

334 rec['psfSigma'] = psfSigma 

335 rec['skyBackground'] = skyBackground 

336 rec['used'] = 1 

337 

338 def _makeSourceMapper(self, sourceSchema): 

339 """ 

340 Make a schema mapper for fgcm sources 

341 

342 Parameters 

343 ---------- 

344 sourceSchema: `afwTable.Schema` 

345 Default source schema from the butler 

346 

347 Returns 

348 ------- 

349 sourceMapper: `afwTable.schemaMapper` 

350 Mapper to the FGCM source schema 

351 """ 

352 

353 # create a mapper to the preferred output 

354 sourceMapper = afwTable.SchemaMapper(sourceSchema) 

355 

356 # map to ra/dec 

357 sourceMapper.addMapping(sourceSchema['coord_ra'].asKey(), 'ra') 

358 sourceMapper.addMapping(sourceSchema['coord_dec'].asKey(), 'dec') 

359 sourceMapper.addMapping(sourceSchema['slot_Centroid_x'].asKey(), 'x') 

360 sourceMapper.addMapping(sourceSchema['slot_Centroid_y'].asKey(), 'y') 

361 # Add the mapping if the field exists in the input catalog. 

362 # If the field does not exist, simply add it (set to False). 

363 # This field is not required for calibration, but is useful 

364 # to collate if available. 

365 try: 

366 sourceMapper.addMapping(sourceSchema[self.config.psfCandidateName].asKey(), 

367 'psf_candidate') 

368 except LookupError: 

369 sourceMapper.editOutputSchema().addField( 

370 "psf_candidate", type='Flag', 

371 doc=("Flag set if the source was a candidate for PSF determination, " 

372 "as determined by the star selector.")) 

373 

374 # and add the fields we want 

375 sourceMapper.editOutputSchema().addField( 

376 "visit", type=np.int64, doc="Visit number") 

377 sourceMapper.editOutputSchema().addField( 

378 "ccd", type=np.int32, doc="CCD number") 

379 sourceMapper.editOutputSchema().addField( 

380 "instMag", type=np.float32, doc="Instrumental magnitude") 

381 sourceMapper.editOutputSchema().addField( 

382 "instMagErr", type=np.float32, doc="Instrumental magnitude error") 

383 sourceMapper.editOutputSchema().addField( 

384 "jacobian", type=np.float32, doc="Relative pixel scale from wcs jacobian") 

385 sourceMapper.editOutputSchema().addField( 

386 "deltaMagBkg", type=np.float32, doc="Change in magnitude due to local background offset") 

387 sourceMapper.editOutputSchema().addField( 

388 "deltaMagAper", type=np.float32, doc="Change in magnitude from larger to smaller aperture") 

389 

390 return sourceMapper 

391 

392 def fgcmMatchStars(self, visitCat, obsCat, lutHandle=None): 

393 """ 

394 Use FGCM code to match observations into unique stars. 

395 

396 Parameters 

397 ---------- 

398 visitCat: `afw.table.BaseCatalog` 

399 Catalog with visit data for fgcm 

400 obsCat: `afw.table.BaseCatalog` 

401 Full catalog of star observations for fgcm 

402 lutHandle: `lsst.daf.butler.DeferredDatasetHandle`, optional 

403 Data reference to fgcm look-up table (used if matching reference stars). 

404 

405 Returns 

406 ------- 

407 fgcmStarIdCat: `afw.table.BaseCatalog` 

408 Catalog of unique star identifiers and index keys 

409 fgcmStarIndicesCat: `afwTable.BaseCatalog` 

410 Catalog of unique star indices 

411 fgcmRefCat: `afw.table.BaseCatalog` 

412 Catalog of matched reference stars. 

413 Will be None if `config.doReferenceMatches` is False. 

414 """ 

415 # get filter names into a numpy array... 

416 # This is the type that is expected by the fgcm code 

417 visitFilterNames = np.zeros(len(visitCat), dtype='a30') 

418 for i in range(len(visitCat)): 

419 visitFilterNames[i] = visitCat[i]['physicalFilter'] 

420 

421 # match to put filterNames with observations 

422 visitIndex = np.searchsorted(visitCat['visit'], 

423 obsCat['visit']) 

424 

425 obsFilterNames = visitFilterNames[visitIndex] 

426 

427 if self.config.doReferenceMatches: 

428 # Get the reference filter names, using the LUT 

429 lutCat = lutHandle.get() 

430 

431 stdFilterDict = {filterName: stdFilter for (filterName, stdFilter) in 

432 zip(lutCat[0]['physicalFilters'].split(','), 

433 lutCat[0]['stdPhysicalFilters'].split(','))} 

434 stdLambdaDict = {stdFilter: stdLambda for (stdFilter, stdLambda) in 

435 zip(lutCat[0]['stdPhysicalFilters'].split(','), 

436 lutCat[0]['lambdaStdFilter'])} 

437 

438 del lutCat 

439 

440 referenceFilterNames = self._getReferenceFilterNames(visitCat, 

441 stdFilterDict, 

442 stdLambdaDict) 

443 self.log.info("Using the following reference filters: %s" % 

444 (', '.join(referenceFilterNames))) 

445 

446 else: 

447 # This should be an empty list 

448 referenceFilterNames = [] 

449 

450 # make the fgcm starConfig dict 

451 starConfig = {'logger': self.log, 

452 'useHtm': True, 

453 'filterToBand': self.config.physicalFilterMap, 

454 'requiredBands': self.config.requiredBands, 

455 'minPerBand': self.config.minPerBand, 

456 'matchRadius': self.config.matchRadius, 

457 'isolationRadius': self.config.isolationRadius, 

458 'matchNSide': self.config.matchNside, 

459 'coarseNSide': self.config.coarseNside, 

460 'densNSide': self.config.densityCutNside, 

461 'densMaxPerPixel': self.config.densityCutMaxPerPixel, 

462 'randomSeed': self.config.randomSeed, 

463 'primaryBands': self.config.primaryBands, 

464 'referenceFilterNames': referenceFilterNames} 

465 

466 # initialize the FgcmMakeStars object 

467 fgcmMakeStars = fgcm.FgcmMakeStars(starConfig) 

468 

469 # make the primary stars 

470 # note that the ra/dec native Angle format is radians 

471 # We determine the conversion from the native units (typically 

472 # radians) to degrees for the first observation. This allows us 

473 # to treate ra/dec as numpy arrays rather than Angles, which would 

474 # be approximately 600x slower. 

475 conv = obsCat[0]['ra'].asDegrees() / float(obsCat[0]['ra']) 

476 fgcmMakeStars.makePrimaryStars(obsCat['ra'] * conv, 

477 obsCat['dec'] * conv, 

478 filterNameArray=obsFilterNames, 

479 bandSelected=False) 

480 

481 # and match all the stars 

482 fgcmMakeStars.makeMatchedStars(obsCat['ra'] * conv, 

483 obsCat['dec'] * conv, 

484 obsFilterNames) 

485 

486 if self.config.doReferenceMatches: 

487 fgcmMakeStars.makeReferenceMatches(self.fgcmLoadReferenceCatalog) 

488 

489 # now persist 

490 

491 objSchema = self._makeFgcmObjSchema() 

492 

493 # make catalog and records 

494 fgcmStarIdCat = afwTable.BaseCatalog(objSchema) 

495 fgcmStarIdCat.reserve(fgcmMakeStars.objIndexCat.size) 

496 for i in range(fgcmMakeStars.objIndexCat.size): 

497 fgcmStarIdCat.addNew() 

498 

499 # fill the catalog 

500 fgcmStarIdCat['fgcm_id'][:] = fgcmMakeStars.objIndexCat['fgcm_id'] 

501 fgcmStarIdCat['ra'][:] = fgcmMakeStars.objIndexCat['ra'] 

502 fgcmStarIdCat['dec'][:] = fgcmMakeStars.objIndexCat['dec'] 

503 fgcmStarIdCat['obsArrIndex'][:] = fgcmMakeStars.objIndexCat['obsarrindex'] 

504 fgcmStarIdCat['nObs'][:] = fgcmMakeStars.objIndexCat['nobs'] 

505 

506 obsSchema = self._makeFgcmObsSchema() 

507 

508 fgcmStarIndicesCat = afwTable.BaseCatalog(obsSchema) 

509 fgcmStarIndicesCat.reserve(fgcmMakeStars.obsIndexCat.size) 

510 for i in range(fgcmMakeStars.obsIndexCat.size): 

511 fgcmStarIndicesCat.addNew() 

512 

513 fgcmStarIndicesCat['obsIndex'][:] = fgcmMakeStars.obsIndexCat['obsindex'] 

514 

515 if self.config.doReferenceMatches: 

516 refSchema = self._makeFgcmRefSchema(len(referenceFilterNames)) 

517 

518 fgcmRefCat = afwTable.BaseCatalog(refSchema) 

519 fgcmRefCat.reserve(fgcmMakeStars.referenceCat.size) 

520 

521 for i in range(fgcmMakeStars.referenceCat.size): 

522 fgcmRefCat.addNew() 

523 

524 fgcmRefCat['fgcm_id'][:] = fgcmMakeStars.referenceCat['fgcm_id'] 

525 fgcmRefCat['refMag'][:, :] = fgcmMakeStars.referenceCat['refMag'] 

526 fgcmRefCat['refMagErr'][:, :] = fgcmMakeStars.referenceCat['refMagErr'] 

527 

528 md = PropertyList() 

529 md.set("REFSTARS_FORMAT_VERSION", REFSTARS_FORMAT_VERSION) 

530 md.set("FILTERNAMES", referenceFilterNames) 

531 fgcmRefCat.setMetadata(md) 

532 

533 else: 

534 fgcmRefCat = None 

535 

536 return fgcmStarIdCat, fgcmStarIndicesCat, fgcmRefCat 

537 

538 def _makeFgcmVisitSchema(self, nCcd): 

539 """ 

540 Make a schema for an fgcmVisitCatalog 

541 

542 Parameters 

543 ---------- 

544 nCcd: `int` 

545 Number of CCDs in the camera 

546 

547 Returns 

548 ------- 

549 schema: `afwTable.Schema` 

550 """ 

551 

552 schema = afwTable.Schema() 

553 schema.addField('visit', type=np.int64, doc="Visit number") 

554 schema.addField('physicalFilter', type=str, size=30, doc="Physical filter") 

555 schema.addField('telra', type=np.float64, doc="Pointing RA (deg)") 

556 schema.addField('teldec', type=np.float64, doc="Pointing Dec (deg)") 

557 schema.addField('telha', type=np.float64, doc="Pointing Hour Angle (deg)") 

558 schema.addField('telrot', type=np.float64, doc="Camera rotation (deg)") 

559 schema.addField('mjd', type=np.float64, doc="MJD of visit") 

560 schema.addField('exptime', type=np.float32, doc="Exposure time") 

561 schema.addField('pmb', type=np.float32, doc="Pressure (millibar)") 

562 schema.addField('psfSigma', type=np.float32, doc="PSF sigma (reference CCD)") 

563 schema.addField('deltaAper', type=np.float32, doc="Delta-aperture") 

564 schema.addField('skyBackground', type=np.float32, doc="Sky background (ADU) (reference CCD)") 

565 # the following field is not used yet 

566 schema.addField('deepFlag', type=np.int32, doc="Deep observation") 

567 schema.addField('scaling', type='ArrayD', doc="Scaling applied due to flat adjustment", 

568 size=nCcd) 

569 schema.addField('used', type=np.int32, doc="This visit has been ingested.") 

570 schema.addField('sources_read', type='Flag', doc="This visit had sources read.") 

571 

572 return schema 

573 

574 def _makeFgcmObjSchema(self): 

575 """ 

576 Make a schema for the objIndexCat from fgcmMakeStars 

577 

578 Returns 

579 ------- 

580 schema: `afwTable.Schema` 

581 """ 

582 

583 objSchema = afwTable.Schema() 

584 objSchema.addField('fgcm_id', type=np.int32, doc='FGCM Unique ID') 

585 # Will investigate making these angles... 

586 objSchema.addField('ra', type=np.float64, doc='Mean object RA (deg)') 

587 objSchema.addField('dec', type=np.float64, doc='Mean object Dec (deg)') 

588 objSchema.addField('obsArrIndex', type=np.int32, 

589 doc='Index in obsIndexTable for first observation') 

590 objSchema.addField('nObs', type=np.int32, doc='Total number of observations') 

591 

592 return objSchema 

593 

594 def _makeFgcmObsSchema(self): 

595 """ 

596 Make a schema for the obsIndexCat from fgcmMakeStars 

597 

598 Returns 

599 ------- 

600 schema: `afwTable.Schema` 

601 """ 

602 

603 obsSchema = afwTable.Schema() 

604 obsSchema.addField('obsIndex', type=np.int32, doc='Index in observation table') 

605 

606 return obsSchema 

607 

608 def _makeFgcmRefSchema(self, nReferenceBands): 

609 """ 

610 Make a schema for the referenceCat from fgcmMakeStars 

611 

612 Parameters 

613 ---------- 

614 nReferenceBands: `int` 

615 Number of reference bands 

616 

617 Returns 

618 ------- 

619 schema: `afwTable.Schema` 

620 """ 

621 

622 refSchema = afwTable.Schema() 

623 refSchema.addField('fgcm_id', type=np.int32, doc='FGCM Unique ID') 

624 refSchema.addField('refMag', type='ArrayF', doc='Reference magnitude array (AB)', 

625 size=nReferenceBands) 

626 refSchema.addField('refMagErr', type='ArrayF', doc='Reference magnitude error array', 

627 size=nReferenceBands) 

628 

629 return refSchema 

630 

631 def _getReferenceFilterNames(self, visitCat, stdFilterDict, stdLambdaDict): 

632 """ 

633 Get the reference filter names, in wavelength order, from the visitCat and 

634 information from the look-up-table. 

635 

636 Parameters 

637 ---------- 

638 visitCat: `afw.table.BaseCatalog` 

639 Catalog with visit data for FGCM 

640 stdFilterDict: `dict` 

641 Mapping of filterName to stdFilterName from LUT 

642 stdLambdaDict: `dict` 

643 Mapping of stdFilterName to stdLambda from LUT 

644 

645 Returns 

646 ------- 

647 referenceFilterNames: `list` 

648 Wavelength-ordered list of reference filter names 

649 """ 

650 

651 # Find the unique list of filter names in visitCat 

652 filterNames = np.unique(visitCat.asAstropy()['physicalFilter']) 

653 

654 # Find the unique list of "standard" filters 

655 stdFilterNames = {stdFilterDict[filterName] for filterName in filterNames} 

656 

657 # And sort these by wavelength 

658 referenceFilterNames = sorted(stdFilterNames, key=stdLambdaDict.get) 

659 

660 return referenceFilterNames