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

223 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-15 01:51 -0700

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 visitDataRefName = pexConfig.Field( 

113 doc="dataRef name for the 'visit' field, usually 'visit'.", 

114 dtype=str, 

115 default="visit", 

116 deprecated="The visitDataRefname was only used for gen2; this config will be removed after v24." 

117 ) 

118 ccdDataRefName = pexConfig.Field( 

119 doc="dataRef name for the 'ccd' field, usually 'ccd' or 'detector'.", 

120 dtype=str, 

121 default="ccd", 

122 deprecated="The ccdDataRefname was only used for gen2; this config will be removed after v24." 

123 ) 

124 doApplyWcsJacobian = pexConfig.Field( 

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

126 dtype=bool, 

127 default=True 

128 ) 

129 doModelErrorsWithBackground = pexConfig.Field( 

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

131 dtype=bool, 

132 default=True 

133 ) 

134 psfCandidateName = pexConfig.Field( 

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

136 dtype=str, 

137 default="calib_psf_candidate" 

138 ) 

139 doSubtractLocalBackground = pexConfig.Field( 

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

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

142 dtype=bool, 

143 default=False 

144 ) 

145 localBackgroundFluxField = pexConfig.Field( 

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

147 dtype=str, 

148 default='base_LocalBackground_instFlux' 

149 ) 

150 sourceSelector = sourceSelectorRegistry.makeField( 

151 doc="How to select sources", 

152 default="science" 

153 ) 

154 apertureInnerInstFluxField = pexConfig.Field( 

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

156 "flux for aperture correction proxy"), 

157 dtype=str, 

158 default='base_CircularApertureFlux_12_0_instFlux' 

159 ) 

160 apertureOuterInstFluxField = pexConfig.Field( 

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

162 "flux for aperture correction proxy"), 

163 dtype=str, 

164 default='base_CircularApertureFlux_17_0_instFlux' 

165 ) 

166 doReferenceMatches = pexConfig.Field( 

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

168 dtype=bool, 

169 default=True, 

170 ) 

171 fgcmLoadReferenceCatalog = pexConfig.ConfigurableField( 

172 target=FgcmLoadReferenceCatalogTask, 

173 doc="FGCM reference object loader", 

174 ) 

175 nVisitsPerCheckpoint = pexConfig.Field( 

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

177 dtype=int, 

178 default=500, 

179 ) 

180 

181 def setDefaults(self): 

182 sourceSelector = self.sourceSelector["science"] 

183 sourceSelector.setDefaults() 

184 

185 sourceSelector.doFlags = True 

186 sourceSelector.doUnresolved = True 

187 sourceSelector.doSignalToNoise = True 

188 sourceSelector.doIsolated = True 

189 sourceSelector.doRequireFiniteRaDec = True 

190 

191 sourceSelector.signalToNoise.minimum = 10.0 

192 sourceSelector.signalToNoise.maximum = 1000.0 

193 

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

195 # appropriate for the current base_ClassificationExtendedness 

196 sourceSelector.unresolved.maximum = 0.5 

197 

198 

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

200 """ 

201 Base task to build stars for FGCM global calibration 

202 """ 

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

204 super().__init__(**kwargs) 

205 

206 self.makeSubtask("sourceSelector") 

207 # Only log warning and fatal errors from the sourceSelector 

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

209 

210 @abc.abstractmethod 

211 def fgcmMakeAllStarObservations(self, groupedHandles, visitCat, 

212 sourceSchema, 

213 camera, 

214 calibFluxApertureRadius=None): 

215 """ 

216 Compile all good star observations from visits in visitCat. 

217 

218 Parameters 

219 ---------- 

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

221 Dataset handles, grouped by visit. 

222 visitCat : `afw.table.BaseCatalog` 

223 Catalog with visit data for FGCM 

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

225 Schema for the input src catalogs. 

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

227 calibFluxApertureRadius : `float`, optional 

228 Aperture radius for calibration flux. 

229 inStarObsCat : `afw.table.BaseCatalog` 

230 Input observation catalog. If this is incomplete, observations 

231 will be appended from when it was cut off. 

232 

233 Returns 

234 ------- 

235 fgcmStarObservations : `afw.table.BaseCatalog` 

236 Full catalog of good observations. 

237 

238 Raises 

239 ------ 

240 RuntimeError: Raised if doSubtractLocalBackground is True and 

241 calibFluxApertureRadius is not set. 

242 """ 

243 raise NotImplementedError("fgcmMakeAllStarObservations not implemented.") 

244 

245 def fgcmMakeVisitCatalog(self, camera, groupedHandles, bkgHandleDict=None): 

246 """ 

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

248 

249 Parameters 

250 ---------- 

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

252 Camera from the butler 

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

254 Dataset handles, grouped by visit. 

255 bkgHandleDict: `dict`, optional 

256 Dictionary of `lsst.daf.butler.DeferredDatasetHandle` for background info. 

257 

258 Returns 

259 ------- 

260 visitCat: `afw.table.BaseCatalog` 

261 """ 

262 

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

264 

265 nCcd = len(camera) 

266 

267 schema = self._makeFgcmVisitSchema(nCcd) 

268 

269 visitCat = afwTable.BaseCatalog(schema) 

270 visitCat.reserve(len(groupedHandles)) 

271 visitCat.resize(len(groupedHandles)) 

272 

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

274 visitCat['used'] = 0 

275 visitCat['sources_read'] = False 

276 

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

278 # already read. 

279 self._fillVisitCatalog(visitCat, groupedHandles, 

280 bkgHandleDict=bkgHandleDict) 

281 

282 return visitCat 

283 

284 def _fillVisitCatalog(self, visitCat, groupedHandles, bkgHandleDict=None): 

285 """ 

286 Fill the visit catalog with visit metadata 

287 

288 Parameters 

289 ---------- 

290 visitCat : `afw.table.BaseCatalog` 

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

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

293 Dataset handles, grouped by visit. 

294 bkgHandleDict : `dict`, optional 

295 Dictionary of `lsst.daf.butler.DeferredDatasetHandle` 

296 for background info. 

297 """ 

298 for i, visit in enumerate(groupedHandles): 

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

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

301 

302 handle = groupedHandles[visit][0] 

303 summary = handle.get() 

304 

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

306 if summaryRow is None: 

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

308 summaryRow = summary[0] 

309 

310 summaryDetector = summaryRow['id'] 

311 visitInfo = summaryRow.getVisitInfo() 

312 physicalFilter = summaryRow['physical_filter'] 

313 # Compute the median psf sigma if possible 

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

315 if goodSigma.size > 2: 

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

317 elif goodSigma.size > 0: 

318 psfSigma = np.mean(summary['psfSigma'][goodSigma]) 

319 else: 

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

321 psfSigma = 0.0 

322 

323 rec = visitCat[i] 

324 rec['visit'] = visit 

325 rec['physicalFilter'] = physicalFilter 

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

327 radec = visitInfo.getBoresightRaDec() 

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

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

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

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

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

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

334 # convert from Pa to millibar 

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

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

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

338 rec['deepFlag'] = 0 

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

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

341 # Median delta aperture, to be measured from stars 

342 rec['deltaAper'] = 0.0 

343 rec['psfSigma'] = psfSigma 

344 

345 if self.config.doModelErrorsWithBackground: 

346 # Use the same detector used from the summary. 

347 bkgHandle = bkgHandleDict[(visit, summaryDetector)] 

348 bgList = bkgHandle.get() 

349 

350 bgStats = (bg[0].getStatsImage().getImage().array 

351 for bg in bgList) 

352 rec['skyBackground'] = sum(np.median(bg[np.isfinite(bg)]) for bg in bgStats) 

353 else: 

354 rec['skyBackground'] = -1.0 

355 

356 rec['used'] = 1 

357 

358 def _makeSourceMapper(self, sourceSchema): 

359 """ 

360 Make a schema mapper for fgcm sources 

361 

362 Parameters 

363 ---------- 

364 sourceSchema: `afwTable.Schema` 

365 Default source schema from the butler 

366 

367 Returns 

368 ------- 

369 sourceMapper: `afwTable.schemaMapper` 

370 Mapper to the FGCM source schema 

371 """ 

372 

373 # create a mapper to the preferred output 

374 sourceMapper = afwTable.SchemaMapper(sourceSchema) 

375 

376 # map to ra/dec 

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

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

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

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

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

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

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

384 # to collate if available. 

385 try: 

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

387 'psf_candidate') 

388 except LookupError: 

389 sourceMapper.editOutputSchema().addField( 

390 "psf_candidate", type='Flag', 

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

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

393 

394 # and add the fields we want 

395 sourceMapper.editOutputSchema().addField( 

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

397 sourceMapper.editOutputSchema().addField( 

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

399 sourceMapper.editOutputSchema().addField( 

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

401 sourceMapper.editOutputSchema().addField( 

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

403 sourceMapper.editOutputSchema().addField( 

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

405 sourceMapper.editOutputSchema().addField( 

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

407 sourceMapper.editOutputSchema().addField( 

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

409 

410 return sourceMapper 

411 

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

413 """ 

414 Use FGCM code to match observations into unique stars. 

415 

416 Parameters 

417 ---------- 

418 visitCat: `afw.table.BaseCatalog` 

419 Catalog with visit data for fgcm 

420 obsCat: `afw.table.BaseCatalog` 

421 Full catalog of star observations for fgcm 

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

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

424 

425 Returns 

426 ------- 

427 fgcmStarIdCat: `afw.table.BaseCatalog` 

428 Catalog of unique star identifiers and index keys 

429 fgcmStarIndicesCat: `afwTable.BaseCatalog` 

430 Catalog of unique star indices 

431 fgcmRefCat: `afw.table.BaseCatalog` 

432 Catalog of matched reference stars. 

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

434 """ 

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

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

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

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

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

440 

441 # match to put filterNames with observations 

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

443 obsCat['visit']) 

444 

445 obsFilterNames = visitFilterNames[visitIndex] 

446 

447 if self.config.doReferenceMatches: 

448 # Get the reference filter names, using the LUT 

449 lutCat = lutHandle.get() 

450 

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

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

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

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

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

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

457 

458 del lutCat 

459 

460 referenceFilterNames = self._getReferenceFilterNames(visitCat, 

461 stdFilterDict, 

462 stdLambdaDict) 

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

464 (', '.join(referenceFilterNames))) 

465 

466 else: 

467 # This should be an empty list 

468 referenceFilterNames = [] 

469 

470 # make the fgcm starConfig dict 

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

472 'useHtm': True, 

473 'filterToBand': self.config.physicalFilterMap, 

474 'requiredBands': self.config.requiredBands, 

475 'minPerBand': self.config.minPerBand, 

476 'matchRadius': self.config.matchRadius, 

477 'isolationRadius': self.config.isolationRadius, 

478 'matchNSide': self.config.matchNside, 

479 'coarseNSide': self.config.coarseNside, 

480 'densNSide': self.config.densityCutNside, 

481 'densMaxPerPixel': self.config.densityCutMaxPerPixel, 

482 'randomSeed': self.config.randomSeed, 

483 'primaryBands': self.config.primaryBands, 

484 'referenceFilterNames': referenceFilterNames} 

485 

486 # initialize the FgcmMakeStars object 

487 fgcmMakeStars = fgcm.FgcmMakeStars(starConfig) 

488 

489 # make the primary stars 

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

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

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

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

494 # be approximately 600x slower. 

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

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

497 obsCat['dec'] * conv, 

498 filterNameArray=obsFilterNames, 

499 bandSelected=False) 

500 

501 # and match all the stars 

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

503 obsCat['dec'] * conv, 

504 obsFilterNames) 

505 

506 if self.config.doReferenceMatches: 

507 fgcmMakeStars.makeReferenceMatches(self.fgcmLoadReferenceCatalog) 

508 

509 # now persist 

510 

511 objSchema = self._makeFgcmObjSchema() 

512 

513 # make catalog and records 

514 fgcmStarIdCat = afwTable.BaseCatalog(objSchema) 

515 fgcmStarIdCat.reserve(fgcmMakeStars.objIndexCat.size) 

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

517 fgcmStarIdCat.addNew() 

518 

519 # fill the catalog 

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

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

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

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

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

525 

526 obsSchema = self._makeFgcmObsSchema() 

527 

528 fgcmStarIndicesCat = afwTable.BaseCatalog(obsSchema) 

529 fgcmStarIndicesCat.reserve(fgcmMakeStars.obsIndexCat.size) 

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

531 fgcmStarIndicesCat.addNew() 

532 

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

534 

535 if self.config.doReferenceMatches: 

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

537 

538 fgcmRefCat = afwTable.BaseCatalog(refSchema) 

539 fgcmRefCat.reserve(fgcmMakeStars.referenceCat.size) 

540 

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

542 fgcmRefCat.addNew() 

543 

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

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

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

547 

548 md = PropertyList() 

549 md.set("REFSTARS_FORMAT_VERSION", REFSTARS_FORMAT_VERSION) 

550 md.set("FILTERNAMES", referenceFilterNames) 

551 fgcmRefCat.setMetadata(md) 

552 

553 else: 

554 fgcmRefCat = None 

555 

556 return fgcmStarIdCat, fgcmStarIndicesCat, fgcmRefCat 

557 

558 def _makeFgcmVisitSchema(self, nCcd): 

559 """ 

560 Make a schema for an fgcmVisitCatalog 

561 

562 Parameters 

563 ---------- 

564 nCcd: `int` 

565 Number of CCDs in the camera 

566 

567 Returns 

568 ------- 

569 schema: `afwTable.Schema` 

570 """ 

571 

572 schema = afwTable.Schema() 

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

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

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

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

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

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

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

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

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

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

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

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

585 # the following field is not used yet 

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

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

588 size=nCcd) 

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

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

591 

592 return schema 

593 

594 def _makeFgcmObjSchema(self): 

595 """ 

596 Make a schema for the objIndexCat from fgcmMakeStars 

597 

598 Returns 

599 ------- 

600 schema: `afwTable.Schema` 

601 """ 

602 

603 objSchema = afwTable.Schema() 

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

605 # Will investigate making these angles... 

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

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

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

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

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

611 

612 return objSchema 

613 

614 def _makeFgcmObsSchema(self): 

615 """ 

616 Make a schema for the obsIndexCat from fgcmMakeStars 

617 

618 Returns 

619 ------- 

620 schema: `afwTable.Schema` 

621 """ 

622 

623 obsSchema = afwTable.Schema() 

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

625 

626 return obsSchema 

627 

628 def _makeFgcmRefSchema(self, nReferenceBands): 

629 """ 

630 Make a schema for the referenceCat from fgcmMakeStars 

631 

632 Parameters 

633 ---------- 

634 nReferenceBands: `int` 

635 Number of reference bands 

636 

637 Returns 

638 ------- 

639 schema: `afwTable.Schema` 

640 """ 

641 

642 refSchema = afwTable.Schema() 

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

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

645 size=nReferenceBands) 

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

647 size=nReferenceBands) 

648 

649 return refSchema 

650 

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

652 """ 

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

654 information from the look-up-table. 

655 

656 Parameters 

657 ---------- 

658 visitCat: `afw.table.BaseCatalog` 

659 Catalog with visit data for FGCM 

660 stdFilterDict: `dict` 

661 Mapping of filterName to stdFilterName from LUT 

662 stdLambdaDict: `dict` 

663 Mapping of stdFilterName to stdLambda from LUT 

664 

665 Returns 

666 ------- 

667 referenceFilterNames: `list` 

668 Wavelength-ordered list of reference filter names 

669 """ 

670 

671 # Find the unique list of filter names in visitCat 

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

673 

674 # Find the unique list of "standard" filters 

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

676 

677 # And sort these by wavelength 

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

679 

680 return referenceFilterNames