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

238 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-21 10:51 +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 

36from .utilities import computeReferencePixelScale, countDetectors 

37 

38import fgcm 

39 

40REFSTARS_FORMAT_VERSION = 1 

41 

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

43 

44 

45class FgcmBuildStarsConfigBase(pexConfig.Config): 

46 """Base config for FgcmBuildStars tasks""" 

47 

48 instFluxField = pexConfig.Field( 

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

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

51 dtype=str, 

52 default='slot_CalibFlux_instFlux', 

53 ) 

54 minPerBand = pexConfig.Field( 

55 doc="Minimum observations per band", 

56 dtype=int, 

57 default=2, 

58 ) 

59 matchRadius = pexConfig.Field( 

60 doc="Match radius (arcseconds)", 

61 dtype=float, 

62 default=1.0, 

63 ) 

64 isolationRadius = pexConfig.Field( 

65 doc="Isolation radius (arcseconds)", 

66 dtype=float, 

67 default=2.0, 

68 ) 

69 densityCutNside = pexConfig.Field( 

70 doc="Density cut healpix nside", 

71 dtype=int, 

72 default=128, 

73 ) 

74 densityCutMaxPerPixel = pexConfig.Field( 

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

76 dtype=int, 

77 default=1000, 

78 ) 

79 randomSeed = pexConfig.Field( 

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

81 dtype=int, 

82 default=123456, 

83 optional=True, 

84 ) 

85 matchNside = pexConfig.Field( 

86 doc="Healpix Nside for matching", 

87 dtype=int, 

88 default=4096, 

89 ) 

90 coarseNside = pexConfig.Field( 

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

92 dtype=int, 

93 default=8, 

94 ) 

95 physicalFilterMap = pexConfig.DictField( 

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

97 keytype=str, 

98 itemtype=str, 

99 default={}, 

100 ) 

101 requiredBands = pexConfig.ListField( 

102 doc="Bands required for each star", 

103 dtype=str, 

104 default=(), 

105 ) 

106 primaryBands = pexConfig.ListField( 

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

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

109 "as a calibration star."), 

110 dtype=str, 

111 default=None 

112 ) 

113 doApplyWcsJacobian = pexConfig.Field( 

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

115 dtype=bool, 

116 default=True 

117 ) 

118 doModelErrorsWithBackground = pexConfig.Field( 

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

120 dtype=bool, 

121 default=True 

122 ) 

123 psfCandidateName = pexConfig.Field( 

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

125 dtype=str, 

126 default="calib_psf_candidate" 

127 ) 

128 doSubtractLocalBackground = pexConfig.Field( 

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

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

131 dtype=bool, 

132 default=False 

133 ) 

134 localBackgroundFluxField = pexConfig.Field( 

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

136 dtype=str, 

137 default='base_LocalBackground_instFlux' 

138 ) 

139 sourceSelector = sourceSelectorRegistry.makeField( 

140 doc="How to select sources", 

141 default="science" 

142 ) 

143 apertureInnerInstFluxField = pexConfig.Field( 

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

145 "flux for aperture correction proxy"), 

146 dtype=str, 

147 default='base_CircularApertureFlux_12_0_instFlux' 

148 ) 

149 apertureOuterInstFluxField = pexConfig.Field( 

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

151 "flux for aperture correction proxy"), 

152 dtype=str, 

153 default='base_CircularApertureFlux_17_0_instFlux' 

154 ) 

155 doReferenceMatches = pexConfig.Field( 

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

157 dtype=bool, 

158 default=True, 

159 ) 

160 fgcmLoadReferenceCatalog = pexConfig.ConfigurableField( 

161 target=FgcmLoadReferenceCatalogTask, 

162 doc="FGCM reference object loader", 

163 ) 

164 nVisitsPerCheckpoint = pexConfig.Field( 

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

166 dtype=int, 

167 default=500, 

168 ) 

169 

170 def setDefaults(self): 

171 sourceSelector = self.sourceSelector["science"] 

172 sourceSelector.setDefaults() 

173 

174 sourceSelector.doFlags = True 

175 sourceSelector.doUnresolved = True 

176 sourceSelector.doSignalToNoise = True 

177 sourceSelector.doIsolated = True 

178 sourceSelector.doRequireFiniteRaDec = True 

179 

180 sourceSelector.signalToNoise.minimum = 10.0 

181 sourceSelector.signalToNoise.maximum = 1000.0 

182 

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

184 # appropriate for the current base_ClassificationExtendedness 

185 sourceSelector.unresolved.maximum = 0.5 

186 

187 

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

189 """ 

190 Base task to build stars for FGCM global calibration 

191 """ 

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

193 super().__init__(**kwargs) 

194 

195 self.makeSubtask("sourceSelector") 

196 # Only log warning and fatal errors from the sourceSelector 

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

198 

199 def fgcmMakeAllStarObservations(self, groupedHandles, visitCat, 

200 sourceSchema, 

201 camera, 

202 calibFluxApertureRadius=None): 

203 """ 

204 Compile all good star observations from visits in visitCat. 

205 

206 Parameters 

207 ---------- 

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

209 Dataset handles, grouped by visit. 

210 visitCat : `afw.table.BaseCatalog` 

211 Catalog with visit data for FGCM 

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

213 Schema for the input src catalogs. 

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

215 calibFluxApertureRadius : `float`, optional 

216 Aperture radius for calibration flux. 

217 inStarObsCat : `afw.table.BaseCatalog` 

218 Input observation catalog. If this is incomplete, observations 

219 will be appended from when it was cut off. 

220 

221 Returns 

222 ------- 

223 fgcmStarObservations : `afw.table.BaseCatalog` 

224 Full catalog of good observations. 

225 

226 Raises 

227 ------ 

228 RuntimeError: Raised if doSubtractLocalBackground is True and 

229 calibFluxApertureRadius is not set. 

230 """ 

231 raise NotImplementedError("fgcmMakeAllStarObservations not implemented.") 

232 

233 def fgcmMakeVisitCatalog(self, camera, groupedHandles, useScienceDetectors=False): 

234 """ 

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

236 

237 Parameters 

238 ---------- 

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

240 Camera from the butler 

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

242 Dataset handles, grouped by visit. 

243 useScienceDetectors : `bool`, optional 

244 Limit to science detectors? 

245 

246 Returns 

247 ------- 

248 visitCat: `afw.table.BaseCatalog` 

249 """ 

250 

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

252 

253 nCcd = countDetectors(camera, useScienceDetectors) 

254 

255 schema = self._makeFgcmVisitSchema(nCcd) 

256 

257 visitCat = afwTable.BaseCatalog(schema) 

258 visitCat.reserve(len(groupedHandles)) 

259 visitCat.resize(len(groupedHandles)) 

260 

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

262 visitCat['used'] = 0 

263 visitCat['sources_read'] = False 

264 

265 defaultPixelScale = computeReferencePixelScale(camera, useScienceDetectors=useScienceDetectors) 

266 

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

268 # already read. 

269 self._fillVisitCatalog(visitCat, groupedHandles, defaultPixelScale) 

270 

271 return visitCat 

272 

273 def _fillVisitCatalog(self, visitCat, groupedHandles, defaultPixelScale): 

274 """ 

275 Fill the visit catalog with visit metadata 

276 

277 Parameters 

278 ---------- 

279 visitCat : `afw.table.BaseCatalog` 

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

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

282 Dataset handles, grouped by visit. 

283 defaultPixelScale : `float` 

284 Default pixel scale to use if not in visit summary (arcsecond/pixel). 

285 """ 

286 

287 # Guarantee that these are sorted. 

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

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

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

291 

292 handle = groupedHandles[visit][0] 

293 summary = handle.get() 

294 

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

296 if summaryRow is None: 

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

298 summaryRow = summary[0] 

299 

300 visitInfo = summaryRow.getVisitInfo() 

301 physicalFilter = summaryRow['physical_filter'] 

302 # Compute the median psf sigma and fwhm if possible. 

303 if 'pixelScale' in summary.schema: 

304 # This is not available in the older test summaries 

305 pixelScales = summary['pixelScale'] 

306 else: 

307 pixelScales = np.full(len(summary['psfSigma']), defaultPixelScale) 

308 psfSigmas = summary['psfSigma'] 

309 psfFwhms = psfSigmas * pixelScales * np.sqrt(8.*np.log(2.)) 

310 goodSigma = ((np.nan_to_num(psfSigmas) > 0) & (np.nan_to_num(pixelScales) > 0)) 

311 psfSigmas[~goodSigma] = -9999.0 

312 psfFwhms[~goodSigma] = -9999.0 

313 if goodSigma.size > 2: 

314 psfSigma = np.median(psfSigmas[goodSigma]) 

315 psfFwhm = np.median(psfFwhms[goodSigma]) 

316 elif goodSigma.size > 0: 

317 psfSigma = psfSigmas[goodSigma[0]] 

318 psfFwhm = psfFwhms[goodSigma[0]] 

319 else: 

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

321 psfSigma = 0.0 

322 psfFwhm = 0.0 

323 # Compute median background if possible 

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

325 if goodBackground.size > 2: 

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

327 elif goodBackground.size > 0: 

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

329 else: 

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

331 skyBackground = -1.0 

332 

333 rec = visitCat[i] 

334 rec['visit'] = visit 

335 rec['physicalFilter'] = physicalFilter 

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

337 radec = visitInfo.getBoresightRaDec() 

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

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

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

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

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

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

344 # convert from Pa to millibar 

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

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

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

348 rec['deepFlag'] = 0 

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

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

351 # Median delta aperture, to be measured from stars 

352 rec['deltaAper'] = -9999.0 

353 rec['deltaAperDetector'][:] = -9999.0 

354 rec['psfSigma'] = psfSigma.item() 

355 rec['psfFwhm'] = psfFwhm.item() 

356 # This is keyed by detector. 

357 rec['psfFwhmDetector'][summary["id"]] = psfFwhms 

358 rec['skyBackground'] = skyBackground 

359 rec['used'] = 1 

360 

361 def _makeSourceMapper(self, sourceSchema): 

362 """ 

363 Make a schema mapper for fgcm sources 

364 

365 Parameters 

366 ---------- 

367 sourceSchema: `afwTable.Schema` 

368 Default source schema from the butler 

369 

370 Returns 

371 ------- 

372 sourceMapper: `afwTable.schemaMapper` 

373 Mapper to the FGCM source schema 

374 """ 

375 

376 # create a mapper to the preferred output 

377 sourceMapper = afwTable.SchemaMapper(sourceSchema) 

378 

379 # map to ra/dec 

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

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

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

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

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

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

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

387 # to collate if available. 

388 try: 

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

390 'psf_candidate') 

391 except LookupError: 

392 sourceMapper.editOutputSchema().addField( 

393 "psf_candidate", type='Flag', 

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

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

396 

397 # and add the fields we want 

398 sourceMapper.editOutputSchema().addField( 

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

400 sourceMapper.editOutputSchema().addField( 

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

402 sourceMapper.editOutputSchema().addField( 

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

404 sourceMapper.editOutputSchema().addField( 

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

406 sourceMapper.editOutputSchema().addField( 

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

408 sourceMapper.editOutputSchema().addField( 

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

410 sourceMapper.editOutputSchema().addField( 

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

412 

413 return sourceMapper 

414 

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

416 """ 

417 Use FGCM code to match observations into unique stars. 

418 

419 Parameters 

420 ---------- 

421 visitCat: `afw.table.BaseCatalog` 

422 Catalog with visit data for fgcm 

423 obsCat: `afw.table.BaseCatalog` 

424 Full catalog of star observations for fgcm 

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

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

427 

428 Returns 

429 ------- 

430 fgcmStarIdCat: `afw.table.BaseCatalog` 

431 Catalog of unique star identifiers and index keys 

432 fgcmStarIndicesCat: `afwTable.BaseCatalog` 

433 Catalog of unique star indices 

434 fgcmRefCat: `afw.table.BaseCatalog` 

435 Catalog of matched reference stars. 

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

437 """ 

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

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

440 visitFilterNames = np.zeros(len(visitCat), dtype='S30') 

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

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

443 

444 # match to put filterNames with observations 

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

446 obsCat['visit']) 

447 

448 obsFilterNames = visitFilterNames[visitIndex] 

449 

450 if self.config.doReferenceMatches: 

451 # Get the reference filter names, using the LUT 

452 lutCat = lutHandle.get() 

453 

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

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

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

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

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

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

460 

461 del lutCat 

462 

463 referenceFilterNames = self._getReferenceFilterNames(visitCat, 

464 stdFilterDict, 

465 stdLambdaDict) 

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

467 (', '.join(referenceFilterNames))) 

468 

469 else: 

470 # This should be an empty list 

471 referenceFilterNames = [] 

472 

473 # make the fgcm starConfig dict 

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

475 'useHtm': True, 

476 'filterToBand': self.config.physicalFilterMap, 

477 'requiredBands': self.config.requiredBands, 

478 'minPerBand': self.config.minPerBand, 

479 'matchRadius': self.config.matchRadius, 

480 'isolationRadius': self.config.isolationRadius, 

481 'matchNSide': self.config.matchNside, 

482 'coarseNSide': self.config.coarseNside, 

483 'densNSide': self.config.densityCutNside, 

484 'densMaxPerPixel': self.config.densityCutMaxPerPixel, 

485 'randomSeed': self.config.randomSeed, 

486 'primaryBands': self.config.primaryBands, 

487 'referenceFilterNames': referenceFilterNames} 

488 

489 # initialize the FgcmMakeStars object 

490 fgcmMakeStars = fgcm.FgcmMakeStars(starConfig) 

491 

492 # make the primary stars 

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

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

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

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

497 # be approximately 600x slower. 

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

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

500 obsCat['dec'] * conv, 

501 filterNameArray=obsFilterNames, 

502 bandSelected=False) 

503 

504 # and match all the stars 

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

506 obsCat['dec'] * conv, 

507 obsFilterNames) 

508 

509 if self.config.doReferenceMatches: 

510 fgcmMakeStars.makeReferenceMatches(self.fgcmLoadReferenceCatalog) 

511 

512 # now persist 

513 

514 objSchema = self._makeFgcmObjSchema() 

515 

516 # make catalog and records 

517 fgcmStarIdCat = afwTable.BaseCatalog(objSchema) 

518 fgcmStarIdCat.reserve(fgcmMakeStars.objIndexCat.size) 

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

520 fgcmStarIdCat.addNew() 

521 

522 # fill the catalog 

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

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

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

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

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

528 

529 obsSchema = self._makeFgcmObsSchema() 

530 

531 fgcmStarIndicesCat = afwTable.BaseCatalog(obsSchema) 

532 fgcmStarIndicesCat.reserve(fgcmMakeStars.obsIndexCat.size) 

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

534 fgcmStarIndicesCat.addNew() 

535 

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

537 

538 if self.config.doReferenceMatches: 

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

540 

541 fgcmRefCat = afwTable.BaseCatalog(refSchema) 

542 fgcmRefCat.reserve(fgcmMakeStars.referenceCat.size) 

543 

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

545 fgcmRefCat.addNew() 

546 

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

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

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

550 

551 md = PropertyList() 

552 md.set("REFSTARS_FORMAT_VERSION", REFSTARS_FORMAT_VERSION) 

553 md.set("FILTERNAMES", referenceFilterNames) 

554 fgcmRefCat.setMetadata(md) 

555 

556 else: 

557 fgcmRefCat = None 

558 

559 return fgcmStarIdCat, fgcmStarIndicesCat, fgcmRefCat 

560 

561 def _makeFgcmVisitSchema(self, nCcd): 

562 """ 

563 Make a schema for an fgcmVisitCatalog 

564 

565 Parameters 

566 ---------- 

567 nCcd: `int` 

568 Number of CCDs in the camera 

569 

570 Returns 

571 ------- 

572 schema: `afwTable.Schema` 

573 """ 

574 

575 schema = afwTable.Schema() 

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

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

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

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

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

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

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

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

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

585 schema.addField('psfSigma', type=np.float32, doc="PSF sigma (median); pixels") 

586 schema.addField('psfFwhm', type=np.float32, doc="PSF FWHM (median); arcseconds") 

587 schema.addField('psfFwhmDetector', type='ArrayF', doc="PSF FWHM per detector; arcseconds", size=nCcd) 

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

589 schema.addField('deltaAperDetector', type='ArrayF', doc='Delta-aperture per detector', size=nCcd) 

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

591 # the following field is not used yet 

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

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

594 size=nCcd) 

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

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

597 

598 return schema 

599 

600 def _makeFgcmObjSchema(self): 

601 """ 

602 Make a schema for the objIndexCat from fgcmMakeStars 

603 

604 Returns 

605 ------- 

606 schema: `afwTable.Schema` 

607 """ 

608 

609 objSchema = afwTable.Schema() 

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

611 # Will investigate making these angles... 

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

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

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

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

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

617 

618 return objSchema 

619 

620 def _makeFgcmObsSchema(self): 

621 """ 

622 Make a schema for the obsIndexCat from fgcmMakeStars 

623 

624 Returns 

625 ------- 

626 schema: `afwTable.Schema` 

627 """ 

628 

629 obsSchema = afwTable.Schema() 

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

631 

632 return obsSchema 

633 

634 def _makeFgcmRefSchema(self, nReferenceBands): 

635 """ 

636 Make a schema for the referenceCat from fgcmMakeStars 

637 

638 Parameters 

639 ---------- 

640 nReferenceBands: `int` 

641 Number of reference bands 

642 

643 Returns 

644 ------- 

645 schema: `afwTable.Schema` 

646 """ 

647 

648 refSchema = afwTable.Schema() 

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

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

651 size=nReferenceBands) 

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

653 size=nReferenceBands) 

654 

655 return refSchema 

656 

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

658 """ 

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

660 information from the look-up-table. 

661 

662 Parameters 

663 ---------- 

664 visitCat: `afw.table.BaseCatalog` 

665 Catalog with visit data for FGCM 

666 stdFilterDict: `dict` 

667 Mapping of filterName to stdFilterName from LUT 

668 stdLambdaDict: `dict` 

669 Mapping of stdFilterName to stdLambda from LUT 

670 

671 Returns 

672 ------- 

673 referenceFilterNames: `list` 

674 Wavelength-ordered list of reference filter names 

675 """ 

676 

677 # Find the unique list of filter names in visitCat 

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

679 

680 # Find the unique list of "standard" filters 

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

682 

683 # And sort these by wavelength 

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

685 

686 return referenceFilterNames