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

222 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-07 11:20 +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 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 

190 sourceSelector.signalToNoise.minimum = 10.0 

191 sourceSelector.signalToNoise.maximum = 1000.0 

192 

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

194 # appropriate for the current base_ClassificationExtendedness 

195 sourceSelector.unresolved.maximum = 0.5 

196 

197 

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

199 """ 

200 Base task to build stars for FGCM global calibration 

201 """ 

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

203 super().__init__(**kwargs) 

204 

205 self.makeSubtask("sourceSelector") 

206 # Only log warning and fatal errors from the sourceSelector 

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

208 

209 @abc.abstractmethod 

210 def fgcmMakeAllStarObservations(self, groupedHandles, visitCat, 

211 sourceSchema, 

212 camera, 

213 calibFluxApertureRadius=None): 

214 """ 

215 Compile all good star observations from visits in visitCat. 

216 

217 Parameters 

218 ---------- 

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

220 Dataset handles, grouped by visit. 

221 visitCat : `afw.table.BaseCatalog` 

222 Catalog with visit data for FGCM 

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

224 Schema for the input src catalogs. 

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

226 calibFluxApertureRadius : `float`, optional 

227 Aperture radius for calibration flux. 

228 inStarObsCat : `afw.table.BaseCatalog` 

229 Input observation catalog. If this is incomplete, observations 

230 will be appended from when it was cut off. 

231 

232 Returns 

233 ------- 

234 fgcmStarObservations : `afw.table.BaseCatalog` 

235 Full catalog of good observations. 

236 

237 Raises 

238 ------ 

239 RuntimeError: Raised if doSubtractLocalBackground is True and 

240 calibFluxApertureRadius is not set. 

241 """ 

242 raise NotImplementedError("fgcmMakeAllStarObservations not implemented.") 

243 

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

245 """ 

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

247 

248 Parameters 

249 ---------- 

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

251 Camera from the butler 

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

253 Dataset handles, grouped by visit. 

254 bkgHandleDict: `dict`, optional 

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

256 

257 Returns 

258 ------- 

259 visitCat: `afw.table.BaseCatalog` 

260 """ 

261 

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

263 

264 nCcd = len(camera) 

265 

266 schema = self._makeFgcmVisitSchema(nCcd) 

267 

268 visitCat = afwTable.BaseCatalog(schema) 

269 visitCat.reserve(len(groupedHandles)) 

270 visitCat.resize(len(groupedHandles)) 

271 

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

273 visitCat['used'] = 0 

274 visitCat['sources_read'] = False 

275 

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

277 # already read. 

278 self._fillVisitCatalog(visitCat, groupedHandles, 

279 bkgHandleDict=bkgHandleDict) 

280 

281 return visitCat 

282 

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

284 """ 

285 Fill the visit catalog with visit metadata 

286 

287 Parameters 

288 ---------- 

289 visitCat : `afw.table.BaseCatalog` 

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

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

292 Dataset handles, grouped by visit. 

293 bkgHandleDict : `dict`, optional 

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

295 for background info. 

296 """ 

297 for i, visit in enumerate(groupedHandles): 

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

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

300 

301 handle = groupedHandles[visit][0] 

302 summary = handle.get() 

303 

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

305 if summaryRow is None: 

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

307 summaryRow = summary[0] 

308 

309 summaryDetector = summaryRow['id'] 

310 visitInfo = summaryRow.getVisitInfo() 

311 physicalFilter = summaryRow['physical_filter'] 

312 # Compute the median psf sigma if possible 

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

314 if goodSigma.size > 2: 

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

316 elif goodSigma.size > 0: 

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

318 else: 

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

320 psfSigma = 0.0 

321 

322 rec = visitCat[i] 

323 rec['visit'] = visit 

324 rec['physicalFilter'] = physicalFilter 

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

326 radec = visitInfo.getBoresightRaDec() 

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

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

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

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

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

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

333 # convert from Pa to millibar 

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

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

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

337 rec['deepFlag'] = 0 

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

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

340 # Median delta aperture, to be measured from stars 

341 rec['deltaAper'] = 0.0 

342 rec['psfSigma'] = psfSigma 

343 

344 if self.config.doModelErrorsWithBackground: 

345 # Use the same detector used from the summary. 

346 bkgHandle = bkgHandleDict[(visit, summaryDetector)] 

347 bgList = bkgHandle.get() 

348 

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

350 for bg in bgList) 

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

352 else: 

353 rec['skyBackground'] = -1.0 

354 

355 rec['used'] = 1 

356 

357 def _makeSourceMapper(self, sourceSchema): 

358 """ 

359 Make a schema mapper for fgcm sources 

360 

361 Parameters 

362 ---------- 

363 sourceSchema: `afwTable.Schema` 

364 Default source schema from the butler 

365 

366 Returns 

367 ------- 

368 sourceMapper: `afwTable.schemaMapper` 

369 Mapper to the FGCM source schema 

370 """ 

371 

372 # create a mapper to the preferred output 

373 sourceMapper = afwTable.SchemaMapper(sourceSchema) 

374 

375 # map to ra/dec 

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

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

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

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

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

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

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

383 # to collate if available. 

384 try: 

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

386 'psf_candidate') 

387 except LookupError: 

388 sourceMapper.editOutputSchema().addField( 

389 "psf_candidate", type='Flag', 

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

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

392 

393 # and add the fields we want 

394 sourceMapper.editOutputSchema().addField( 

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

396 sourceMapper.editOutputSchema().addField( 

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

398 sourceMapper.editOutputSchema().addField( 

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

400 sourceMapper.editOutputSchema().addField( 

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

402 sourceMapper.editOutputSchema().addField( 

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

404 sourceMapper.editOutputSchema().addField( 

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

406 sourceMapper.editOutputSchema().addField( 

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

408 

409 return sourceMapper 

410 

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

412 """ 

413 Use FGCM code to match observations into unique stars. 

414 

415 Parameters 

416 ---------- 

417 visitCat: `afw.table.BaseCatalog` 

418 Catalog with visit data for fgcm 

419 obsCat: `afw.table.BaseCatalog` 

420 Full catalog of star observations for fgcm 

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

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

423 

424 Returns 

425 ------- 

426 fgcmStarIdCat: `afw.table.BaseCatalog` 

427 Catalog of unique star identifiers and index keys 

428 fgcmStarIndicesCat: `afwTable.BaseCatalog` 

429 Catalog of unique star indices 

430 fgcmRefCat: `afw.table.BaseCatalog` 

431 Catalog of matched reference stars. 

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

433 """ 

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

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

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

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

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

439 

440 # match to put filterNames with observations 

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

442 obsCat['visit']) 

443 

444 obsFilterNames = visitFilterNames[visitIndex] 

445 

446 if self.config.doReferenceMatches: 

447 # Get the reference filter names, using the LUT 

448 lutCat = lutHandle.get() 

449 

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

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

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

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

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

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

456 

457 del lutCat 

458 

459 referenceFilterNames = self._getReferenceFilterNames(visitCat, 

460 stdFilterDict, 

461 stdLambdaDict) 

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

463 (', '.join(referenceFilterNames))) 

464 

465 else: 

466 # This should be an empty list 

467 referenceFilterNames = [] 

468 

469 # make the fgcm starConfig dict 

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

471 'useHtm': True, 

472 'filterToBand': self.config.physicalFilterMap, 

473 'requiredBands': self.config.requiredBands, 

474 'minPerBand': self.config.minPerBand, 

475 'matchRadius': self.config.matchRadius, 

476 'isolationRadius': self.config.isolationRadius, 

477 'matchNSide': self.config.matchNside, 

478 'coarseNSide': self.config.coarseNside, 

479 'densNSide': self.config.densityCutNside, 

480 'densMaxPerPixel': self.config.densityCutMaxPerPixel, 

481 'randomSeed': self.config.randomSeed, 

482 'primaryBands': self.config.primaryBands, 

483 'referenceFilterNames': referenceFilterNames} 

484 

485 # initialize the FgcmMakeStars object 

486 fgcmMakeStars = fgcm.FgcmMakeStars(starConfig) 

487 

488 # make the primary stars 

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

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

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

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

493 # be approximately 600x slower. 

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

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

496 obsCat['dec'] * conv, 

497 filterNameArray=obsFilterNames, 

498 bandSelected=False) 

499 

500 # and match all the stars 

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

502 obsCat['dec'] * conv, 

503 obsFilterNames) 

504 

505 if self.config.doReferenceMatches: 

506 fgcmMakeStars.makeReferenceMatches(self.fgcmLoadReferenceCatalog) 

507 

508 # now persist 

509 

510 objSchema = self._makeFgcmObjSchema() 

511 

512 # make catalog and records 

513 fgcmStarIdCat = afwTable.BaseCatalog(objSchema) 

514 fgcmStarIdCat.reserve(fgcmMakeStars.objIndexCat.size) 

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

516 fgcmStarIdCat.addNew() 

517 

518 # fill the catalog 

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

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

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

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

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

524 

525 obsSchema = self._makeFgcmObsSchema() 

526 

527 fgcmStarIndicesCat = afwTable.BaseCatalog(obsSchema) 

528 fgcmStarIndicesCat.reserve(fgcmMakeStars.obsIndexCat.size) 

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

530 fgcmStarIndicesCat.addNew() 

531 

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

533 

534 if self.config.doReferenceMatches: 

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

536 

537 fgcmRefCat = afwTable.BaseCatalog(refSchema) 

538 fgcmRefCat.reserve(fgcmMakeStars.referenceCat.size) 

539 

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

541 fgcmRefCat.addNew() 

542 

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

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

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

546 

547 md = PropertyList() 

548 md.set("REFSTARS_FORMAT_VERSION", REFSTARS_FORMAT_VERSION) 

549 md.set("FILTERNAMES", referenceFilterNames) 

550 fgcmRefCat.setMetadata(md) 

551 

552 else: 

553 fgcmRefCat = None 

554 

555 return fgcmStarIdCat, fgcmStarIndicesCat, fgcmRefCat 

556 

557 def _makeFgcmVisitSchema(self, nCcd): 

558 """ 

559 Make a schema for an fgcmVisitCatalog 

560 

561 Parameters 

562 ---------- 

563 nCcd: `int` 

564 Number of CCDs in the camera 

565 

566 Returns 

567 ------- 

568 schema: `afwTable.Schema` 

569 """ 

570 

571 schema = afwTable.Schema() 

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

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

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

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

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

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

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

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

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

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

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

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

584 # the following field is not used yet 

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

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

587 size=nCcd) 

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

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

590 

591 return schema 

592 

593 def _makeFgcmObjSchema(self): 

594 """ 

595 Make a schema for the objIndexCat from fgcmMakeStars 

596 

597 Returns 

598 ------- 

599 schema: `afwTable.Schema` 

600 """ 

601 

602 objSchema = afwTable.Schema() 

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

604 # Will investigate making these angles... 

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

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

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

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

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

610 

611 return objSchema 

612 

613 def _makeFgcmObsSchema(self): 

614 """ 

615 Make a schema for the obsIndexCat from fgcmMakeStars 

616 

617 Returns 

618 ------- 

619 schema: `afwTable.Schema` 

620 """ 

621 

622 obsSchema = afwTable.Schema() 

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

624 

625 return obsSchema 

626 

627 def _makeFgcmRefSchema(self, nReferenceBands): 

628 """ 

629 Make a schema for the referenceCat from fgcmMakeStars 

630 

631 Parameters 

632 ---------- 

633 nReferenceBands: `int` 

634 Number of reference bands 

635 

636 Returns 

637 ------- 

638 schema: `afwTable.Schema` 

639 """ 

640 

641 refSchema = afwTable.Schema() 

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

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

644 size=nReferenceBands) 

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

646 size=nReferenceBands) 

647 

648 return refSchema 

649 

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

651 """ 

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

653 information from the look-up-table. 

654 

655 Parameters 

656 ---------- 

657 visitCat: `afw.table.BaseCatalog` 

658 Catalog with visit data for FGCM 

659 stdFilterDict: `dict` 

660 Mapping of filterName to stdFilterName from LUT 

661 stdLambdaDict: `dict` 

662 Mapping of stdFilterName to stdLambda from LUT 

663 

664 Returns 

665 ------- 

666 referenceFilterNames: `list` 

667 Wavelength-ordered list of reference filter names 

668 """ 

669 

670 # Find the unique list of filter names in visitCat 

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

672 

673 # Find the unique list of "standard" filters 

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

675 

676 # And sort these by wavelength 

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

678 

679 return referenceFilterNames