Coverage for python/lsst/meas/algorithms/loadReferenceObjects.py: 13%

380 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-31 04:35 -0700

1# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2017 AURA/LSST. 

5# 

6# This product includes software developed by the 

7# LSST Project (http://www.lsst.org/). 

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 LSST License Statement and 

20# the GNU General Public License along with this program. If not, 

21# see <https://www.lsstcorp.org/LegalNotices/>. 

22# 

23 

24__all__ = ["getRefFluxField", "getRefFluxKeys", "LoadReferenceObjectsTask", "LoadReferenceObjectsConfig", 

25 "ReferenceObjectLoader", "ReferenceObjectLoaderBase"] 

26 

27import abc 

28import logging 

29import warnings 

30from deprecated.sphinx import deprecated 

31 

32import astropy.time 

33import astropy.units 

34import numpy 

35 

36import lsst.geom as geom 

37import lsst.afw.table as afwTable 

38import lsst.pex.config as pexConfig 

39import lsst.pipe.base as pipeBase 

40from lsst import sphgeom 

41from lsst.daf.base import PropertyList 

42 

43 

44# TODO DM-34793: remove this function 

45def isOldFluxField(name, units): 

46 """Return True if this name/units combination corresponds to an 

47 "old-style" reference catalog flux field. 

48 """ 

49 unitsCheck = units != 'nJy' # (units == 'Jy' or units == '' or units == '?') 

50 isFlux = name.endswith('_flux') 

51 isFluxSigma = name.endswith('_fluxSigma') 

52 isFluxErr = name.endswith('_fluxErr') 

53 return (isFlux or isFluxSigma or isFluxErr) and unitsCheck 

54 

55 

56# TODO DM-34793: remove this function 

57def hasNanojanskyFluxUnits(schema): 

58 """Return True if the units of all flux and fluxErr are correct (nJy). 

59 """ 

60 for field in schema: 

61 if isOldFluxField(field.field.getName(), field.field.getUnits()): 

62 return False 

63 return True 

64 

65 

66def getFormatVersionFromRefCat(refCat): 

67 """"Return the format version stored in a reference catalog header. 

68 

69 Parameters 

70 ---------- 

71 refCat : `lsst.afw.table.SimpleCatalog` 

72 Reference catalog to inspect. 

73 

74 Returns 

75 ------- 

76 version : `int` 

77 Format verison integer. Returns `0` if the catalog has no metadata 

78 or the metadata does not include a "REFCAT_FORMAT_VERSION" key. 

79 """ 

80 # TODO DM-34793: change to "Version 0 refcats are no longer supported: refcat fluxes must have nJy units." 

81 # TODO DM-34793: and raise an exception instead of returning 0. 

82 deprecation_msg = "Support for version 0 refcats (pre-nJy fluxes) will be removed after v25." 

83 md = refCat.getMetadata() 

84 if md is None: 

85 warnings.warn(deprecation_msg) 

86 return 0 

87 try: 

88 return md.getScalar("REFCAT_FORMAT_VERSION") 

89 except KeyError: 

90 warnings.warn(deprecation_msg) 

91 return 0 

92 

93 

94# TODO DM-34793: remove this function 

95@deprecated(reason="Support for version 0 refcats (pre-nJy fluxes) will be removed after v25.", 

96 version="v24.0", category=FutureWarning) 

97def convertToNanojansky(catalog, log, doConvert=True): 

98 """Convert fluxes in a catalog from jansky to nanojansky. 

99 

100 Parameters 

101 ---------- 

102 catalog : `lsst.afw.table.SimpleCatalog` 

103 The catalog to convert. 

104 log : `lsst.log.Log` or `logging.Logger` 

105 Log to send messages to. 

106 doConvert : `bool`, optional 

107 Return a converted catalog, or just identify the fields that need to be converted? 

108 This supports the "write=False" mode of `bin/convert_to_nJy.py`. 

109 

110 Returns 

111 ------- 

112 catalog : `lsst.afw.table.SimpleCatalog` or None 

113 The converted catalog, or None if ``doConvert`` is False. 

114 

115 Notes 

116 ----- 

117 Support for old units in reference catalogs will be removed after the 

118 release of late calendar year 2019. 

119 Use `meas_algorithms/bin/convert_to_nJy.py` to update your reference catalog. 

120 """ 

121 # Do not share the AliasMap: for refcats, that gets created when the 

122 # catalog is read from disk and should not be propagated. 

123 mapper = afwTable.SchemaMapper(catalog.schema, shareAliasMap=False) 

124 mapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema()) 

125 input_fields = [] 

126 output_fields = [] 

127 for field in catalog.schema: 

128 oldName = field.field.getName() 

129 oldUnits = field.field.getUnits() 

130 if isOldFluxField(oldName, oldUnits): 

131 units = 'nJy' 

132 # remap Sigma flux fields to Err, so we can drop the alias 

133 if oldName.endswith('_fluxSigma'): 

134 name = oldName.replace('_fluxSigma', '_fluxErr') 

135 else: 

136 name = oldName 

137 newField = afwTable.Field[field.dtype](name, field.field.getDoc(), units) 

138 mapper.addMapping(field.getKey(), newField) 

139 input_fields.append(field.field) 

140 output_fields.append(newField) 

141 else: 

142 mapper.addMapping(field.getKey()) 

143 

144 fluxFieldsStr = '; '.join("(%s, '%s')" % (field.getName(), field.getUnits()) for field in input_fields) 

145 

146 if doConvert: 

147 newSchema = mapper.getOutputSchema() 

148 output = afwTable.SimpleCatalog(newSchema) 

149 output.reserve(len(catalog)) 

150 output.extend(catalog, mapper=mapper) 

151 for field in output_fields: 

152 output[field.getName()] *= 1e9 

153 log.info("Converted refcat flux fields to nJy (name, units): %s", fluxFieldsStr) 

154 return output 

155 else: 

156 log.info("Found old-style refcat flux fields (name, units): %s", fluxFieldsStr) 

157 return None 

158 

159 

160class _FilterCatalog: 

161 """This is a private helper class which filters catalogs by 

162 row based on the row being inside the region used to initialize 

163 the class. 

164 

165 Parameters 

166 ---------- 

167 region : `lsst.sphgeom.Region` 

168 The spatial region which all objects should lie within 

169 """ 

170 def __init__(self, region): 

171 self.region = region 

172 

173 def __call__(self, refCat, catRegion): 

174 """This call method on an instance of this class takes in a reference 

175 catalog, and the region from which the catalog was generated. 

176 

177 If the catalog region is entirely contained within the region used to 

178 initialize this class, then all the entries in the catalog must be 

179 within the region and so the whole catalog is returned. 

180 

181 If the catalog region is not entirely contained, then the location for 

182 each record is tested against the region used to initialize the class. 

183 Records which fall inside this region are added to a new catalog, and 

184 this catalog is then returned. 

185 

186 Parameters 

187 --------- 

188 refCat : `lsst.afw.table.SourceCatalog` 

189 SourceCatalog to be filtered. 

190 catRegion : `lsst.sphgeom.Region` 

191 Region in which the catalog was created 

192 """ 

193 if catRegion.isWithin(self.region): 

194 # no filtering needed, region completely contains refcat 

195 return refCat 

196 

197 filteredRefCat = type(refCat)(refCat.table) 

198 for record in refCat: 

199 if self.region.contains(record.getCoord().getVector()): 

200 filteredRefCat.append(record) 

201 return filteredRefCat 

202 

203 

204class LoadReferenceObjectsConfig(pexConfig.Config): 

205 pixelMargin = pexConfig.RangeField( 

206 doc="Padding to add to 4 all edges of the bounding box (pixels)", 

207 dtype=int, 

208 default=250, 

209 min=0, 

210 ) 

211 anyFilterMapsToThis = pexConfig.Field( 

212 doc=("Always use this reference catalog filter, no matter whether or what filter name is " 

213 "supplied to the loader. Effectively a trivial filterMap: map all filter names to this filter." 

214 " This can be set for purely-astrometric catalogs (e.g. Gaia DR2) where there is only one " 

215 "reasonable choice for every camera filter->refcat mapping, but not for refcats used for " 

216 "photometry, which need a filterMap and/or colorterms/transmission corrections."), 

217 dtype=str, 

218 default=None, 

219 optional=True 

220 ) 

221 filterMap = pexConfig.DictField( 

222 doc=("Mapping of camera filter name: reference catalog filter name; " 

223 "each reference filter must exist in the refcat." 

224 " Note that this does not perform any bandpass corrections: it is just a lookup."), 

225 keytype=str, 

226 itemtype=str, 

227 default={}, 

228 ) 

229 requireProperMotion = pexConfig.Field( 

230 doc="Require that the fields needed to correct proper motion " 

231 "(epoch, pm_ra and pm_dec) are present?", 

232 dtype=bool, 

233 default=False, 

234 ) 

235 ref_dataset_name = pexConfig.Field( 

236 doc="Deprecated; do not use. Added for easier transition from LoadIndexedReferenceObjectsConfig to " 

237 "LoadReferenceObjectsConfig", 

238 dtype=str, 

239 default='', 

240 deprecated='This field is not used. It will be removed after v25.', 

241 ) 

242 

243 def validate(self): 

244 super().validate() 

245 if self.filterMap != {} and self.anyFilterMapsToThis is not None: 

246 msg = "`filterMap` and `anyFilterMapsToThis` are mutually exclusive" 

247 raise pexConfig.FieldValidationError(LoadReferenceObjectsConfig.anyFilterMapsToThis, 

248 self, msg) 

249 

250 

251class ReferenceObjectLoader: 

252 """This class facilitates loading reference catalogs. 

253 

254 The QuantumGraph generation will create a list of datasets that may 

255 possibly overlap a given region. These datasets are then used to construct 

256 an instance of this class. The class instance should then be passed into 

257 a task which needs reference catalogs. These tasks should then determine 

258 the exact region of the sky reference catalogs will be loaded for, and 

259 call a corresponding method to load the reference objects. 

260 

261 Parameters 

262 ---------- 

263 dataIds : iterable of `lsst.daf.butler.DataCoordinate` 

264 An iterable object of data IDs that point to reference catalogs. 

265 refCats : iterable of `lsst.daf.butler.DeferredDatasetHandle` 

266 Handles to load refCats on demand. 

267 name : `str`, optional 

268 The name of the refcat that this object will load. This name is used 

269 for applying colorterms, for example. 

270 config : `LoadReferenceObjectsConfig` 

271 Configuration of this reference loader. 

272 log : `lsst.log.Log`, `logging.Logger` or `None`, optional 

273 Logger object used to write out messages. If `None` a default 

274 logger will be used. 

275 """ 

276 ConfigClass = LoadReferenceObjectsConfig 

277 

278 def __init__(self, dataIds, refCats, name=None, log=None, config=None, **kwargs): 

279 if kwargs: 

280 warnings.warn("Instantiating ReferenceObjectLoader with additional kwargs is deprecated " 

281 "and will be removed after v25.0", FutureWarning, stacklevel=2) 

282 

283 if config is None: 

284 config = self.ConfigClass() 

285 self.config = config 

286 self.dataIds = dataIds 

287 self.refCats = refCats 

288 self.name = name 

289 self.log = log or logging.getLogger(__name__).getChild("ReferenceObjectLoader") 

290 

291 def applyProperMotions(self, catalog, epoch): 

292 """Apply proper motion correction to a reference catalog. 

293 

294 Adjust position and position error in the ``catalog`` 

295 for proper motion to the specified ``epoch``, 

296 modifying the catalog in place. 

297 

298 Parameters 

299 ---------- 

300 catalog : `lsst.afw.table.SimpleCatalog` 

301 Catalog of positions, containing at least these fields: 

302 

303 - Coordinates, retrieved by the table's coordinate key. 

304 - ``coord_raErr`` : Error in Right Ascension (rad). 

305 - ``coord_decErr`` : Error in Declination (rad). 

306 - ``pm_ra`` : Proper motion in Right Ascension (rad/yr, 

307 East positive) 

308 - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional. 

309 - ``pm_dec`` : Proper motion in Declination (rad/yr, 

310 North positive) 

311 - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional. 

312 - ``epoch`` : Mean epoch of object (an astropy.time.Time) 

313 epoch : `astropy.time.Time` 

314 Epoch to which to correct proper motion. 

315 If None, do not apply PM corrections or raise if 

316 ``config.requireProperMotion`` is True. 

317 

318 Raises 

319 ------ 

320 RuntimeError 

321 Raised if ``config.requireProperMotion`` is set but we cannot 

322 apply the proper motion correction for some reason. 

323 """ 

324 if epoch is None: 

325 if self.config.requireProperMotion: 

326 raise RuntimeError("requireProperMotion=True but epoch not provided to loader.") 

327 else: 

328 self.log.debug("No epoch provided: not applying proper motion corrections to refcat.") 

329 return 

330 

331 # Warn/raise for a catalog in an incorrect format, if epoch was specified. 

332 if ("pm_ra" in catalog.schema 

333 and not isinstance(catalog.schema["pm_ra"].asKey(), afwTable.KeyAngle)): 

334 if self.config.requireProperMotion: 

335 raise RuntimeError("requireProperMotion=True but refcat pm_ra field is not an Angle.") 

336 else: 

337 self.log.warning("Reference catalog pm_ra field is not an Angle; cannot apply proper motion.") 

338 return 

339 

340 if ("epoch" not in catalog.schema or "pm_ra" not in catalog.schema): 

341 if self.config.requireProperMotion: 

342 raise RuntimeError("requireProperMotion=True but PM data not available from catalog.") 

343 else: 

344 self.log.warning("Proper motion correction not available for this reference catalog.") 

345 return 

346 

347 applyProperMotionsImpl(self.log, catalog, epoch) 

348 

349 @staticmethod 

350 def makeMinimalSchema(filterNameList, *, addCentroid=False, 

351 addIsPhotometric=False, addIsResolved=False, 

352 addIsVariable=False, coordErrDim=2, 

353 addProperMotion=False, properMotionErrDim=2, 

354 addParallax=False): 

355 """Make a standard schema for reference object catalogs. 

356 

357 Parameters 

358 ---------- 

359 filterNameList : `list` of `str` 

360 List of filter names. Used to create <filterName>_flux fields. 

361 addIsPhotometric : `bool` 

362 If True then add field "photometric". 

363 addIsResolved : `bool` 

364 If True then add field "resolved". 

365 addIsVariable : `bool` 

366 If True then add field "variable". 

367 coordErrDim : `int` 

368 Number of coord error fields; must be one of 0, 2, 3: 

369 

370 - If 2 or 3: add fields "coord_raErr" and "coord_decErr". 

371 - If 3: also add field "coord_radecErr". 

372 addProperMotion : `bool` 

373 If True add fields "epoch", "pm_ra", "pm_dec" and "pm_flag". 

374 properMotionErrDim : `int` 

375 Number of proper motion error fields; must be one of 0, 2, 3; 

376 ignored if addProperMotion false: 

377 - If 2 or 3: add fields "pm_raErr" and "pm_decErr". 

378 - If 3: also add field "pm_radecErr". 

379 addParallax : `bool` 

380 If True add fields "epoch", "parallax", "parallaxErr" 

381 and "parallax_flag". 

382 

383 Returns 

384 ------- 

385 schema : `lsst.afw.table.Schema` 

386 Schema for reference catalog, an 

387 `lsst.afw.table.SimpleCatalog`. 

388 

389 Notes 

390 ----- 

391 Reference catalogs support additional covariances, such as 

392 covariance between RA and proper motion in declination, 

393 that are not supported by this method, but can be added after 

394 calling this method. 

395 """ 

396 schema = afwTable.SimpleTable.makeMinimalSchema() 

397 if addCentroid: 

398 afwTable.Point2DKey.addFields( 

399 schema, 

400 "centroid", 

401 "centroid on an exposure, if relevant", 

402 "pixel", 

403 ) 

404 schema.addField( 

405 field="hasCentroid", 

406 type="Flag", 

407 doc="is position known?", 

408 ) 

409 for filterName in filterNameList: 

410 schema.addField( 

411 field="%s_flux" % (filterName,), 

412 type=numpy.float64, 

413 doc="flux in filter %s" % (filterName,), 

414 units="nJy", 

415 ) 

416 for filterName in filterNameList: 

417 schema.addField( 

418 field="%s_fluxErr" % (filterName,), 

419 type=numpy.float64, 

420 doc="flux uncertainty in filter %s" % (filterName,), 

421 units="nJy", 

422 ) 

423 if addIsPhotometric: 

424 schema.addField( 

425 field="photometric", 

426 type="Flag", 

427 doc="set if the object can be used for photometric calibration", 

428 ) 

429 if addIsResolved: 

430 schema.addField( 

431 field="resolved", 

432 type="Flag", 

433 doc="set if the object is spatially resolved", 

434 ) 

435 if addIsVariable: 

436 schema.addField( 

437 field="variable", 

438 type="Flag", 

439 doc="set if the object has variable brightness", 

440 ) 

441 if coordErrDim not in (0, 2, 3): 

442 raise ValueError("coordErrDim={}; must be (0, 2, 3)".format(coordErrDim)) 

443 if coordErrDim > 0: 

444 afwTable.CovarianceMatrix2fKey.addFields( 

445 schema=schema, 

446 prefix="coord", 

447 names=["ra", "dec"], 

448 units=["rad", "rad"], 

449 diagonalOnly=(coordErrDim == 2), 

450 ) 

451 

452 if addProperMotion or addParallax: 

453 schema.addField( 

454 field="epoch", 

455 type=numpy.float64, 

456 doc="date of observation (TAI, MJD)", 

457 units="day", 

458 ) 

459 

460 if addProperMotion: 

461 schema.addField( 

462 field="pm_ra", 

463 type="Angle", 

464 doc="proper motion in the right ascension direction = dra/dt * cos(dec)", 

465 units="rad/year", 

466 ) 

467 schema.addField( 

468 field="pm_dec", 

469 type="Angle", 

470 doc="proper motion in the declination direction", 

471 units="rad/year", 

472 ) 

473 if properMotionErrDim not in (0, 2, 3): 

474 raise ValueError("properMotionErrDim={}; must be (0, 2, 3)".format(properMotionErrDim)) 

475 if properMotionErrDim > 0: 

476 afwTable.CovarianceMatrix2fKey.addFields( 

477 schema=schema, 

478 prefix="pm", 

479 names=["ra", "dec"], 

480 units=["rad/year", "rad/year"], 

481 diagonalOnly=(properMotionErrDim == 2), 

482 ) 

483 schema.addField( 

484 field="pm_flag", 

485 type="Flag", 

486 doc="Set if proper motion or proper motion error is bad", 

487 ) 

488 

489 if addParallax: 

490 schema.addField( 

491 field="parallax", 

492 type="Angle", 

493 doc="parallax", 

494 units="rad", 

495 ) 

496 schema.addField( 

497 field="parallaxErr", 

498 type="Angle", 

499 doc="uncertainty in parallax", 

500 units="rad", 

501 ) 

502 schema.addField( 

503 field="parallax_flag", 

504 type="Flag", 

505 doc="Set if parallax or parallax error is bad", 

506 ) 

507 return schema 

508 

509 @staticmethod 

510 def _remapReferenceCatalogSchema(refCat, *, anyFilterMapsToThis=None, 

511 filterMap=None, centroids=False): 

512 """This function takes in a reference catalog and returns a new catalog 

513 with additional columns defined from the remaining function arguments. 

514 

515 Parameters 

516 ---------- 

517 refCat : `lsst.afw.table.SimpleCatalog` 

518 Reference catalog to map to new catalog 

519 anyFilterMapsToThis : `str`, optional 

520 Always use this reference catalog filter. 

521 Mutually exclusive with `filterMap` 

522 filterMap : `dict` [`str`,`str`], optional 

523 Mapping of camera filter name: reference catalog filter name. 

524 centroids : `bool`, optional 

525 Add centroid fields to the loaded Schema. ``loadPixelBox`` expects 

526 these fields to exist. 

527 

528 Returns 

529 ------- 

530 expandedCat : `lsst.afw.table.SimpleCatalog` 

531 Deep copy of input reference catalog with additional columns added 

532 """ 

533 if anyFilterMapsToThis or filterMap: 

534 ReferenceObjectLoader._addFluxAliases(refCat.schema, anyFilterMapsToThis, filterMap) 

535 

536 mapper = afwTable.SchemaMapper(refCat.schema, True) 

537 mapper.addMinimalSchema(refCat.schema, True) 

538 mapper.editOutputSchema().disconnectAliases() 

539 

540 if centroids: 

541 # Add and initialize centroid and hasCentroid fields (these are 

542 # added after loading to avoid wasting space in the saved catalogs). 

543 # The new fields are automatically initialized to (nan, nan) and 

544 # False so no need to set them explicitly. 

545 mapper.editOutputSchema().addField("centroid_x", type=float, doReplace=True) 

546 mapper.editOutputSchema().addField("centroid_y", type=float, doReplace=True) 

547 mapper.editOutputSchema().addField("hasCentroid", type="Flag", doReplace=True) 

548 mapper.editOutputSchema().getAliasMap().set("slot_Centroid", "centroid") 

549 

550 expandedCat = afwTable.SimpleCatalog(mapper.getOutputSchema()) 

551 expandedCat.setMetadata(refCat.getMetadata()) 

552 expandedCat.extend(refCat, mapper=mapper) 

553 

554 return expandedCat 

555 

556 @staticmethod 

557 def _addFluxAliases(schema, anyFilterMapsToThis=None, filterMap=None): 

558 """Add aliases for camera filter fluxes to the schema. 

559 

560 For each camFilter: refFilter in filterMap, adds these aliases: 

561 <camFilter>_camFlux: <refFilter>_flux 

562 <camFilter>_camFluxErr: <refFilter>_fluxErr, if the latter exists 

563 or sets `anyFilterMapsToThis` in the schema. 

564 

565 Parameters 

566 ---------- 

567 schema : `lsst.afw.table.Schema` 

568 Schema for reference catalog. 

569 anyFilterMapsToThis : `str`, optional 

570 Always use this reference catalog filter. 

571 Mutually exclusive with `filterMap`. 

572 filterMap : `dict` [`str`,`str`], optional 

573 Mapping of camera filter name: reference catalog filter name. 

574 Mutually exclusive with `anyFilterMapsToThis`. 

575 

576 Raises 

577 ------ 

578 RuntimeError 

579 Raised if any required reference flux field is missing from the 

580 schema. 

581 """ 

582 # Fail on any truthy value for either of these. 

583 if anyFilterMapsToThis and filterMap: 

584 raise ValueError("anyFilterMapsToThis and filterMap are mutually exclusive!") 

585 

586 aliasMap = schema.getAliasMap() 

587 

588 if anyFilterMapsToThis is not None: 

589 refFluxName = anyFilterMapsToThis + "_flux" 

590 if refFluxName not in schema: 

591 msg = f"Unknown reference filter for anyFilterMapsToThis='{refFluxName}'" 

592 raise RuntimeError(msg) 

593 aliasMap.set("anyFilterMapsToThis", refFluxName) 

594 return # this is mutually exclusive with filterMap 

595 

596 def addAliasesForOneFilter(filterName, refFilterName): 

597 """Add aliases for a single filter 

598 

599 Parameters 

600 ---------- 

601 filterName : `str` (optional) 

602 Camera filter name. The resulting alias name is 

603 <filterName>_camFlux 

604 refFilterName : `str` 

605 Reference catalog filter name; the field 

606 <refFilterName>_flux must exist. 

607 """ 

608 camFluxName = filterName + "_camFlux" 

609 refFluxName = refFilterName + "_flux" 

610 if refFluxName not in schema: 

611 raise RuntimeError("Unknown reference filter %s" % (refFluxName,)) 

612 aliasMap.set(camFluxName, refFluxName) 

613 refFluxErrName = refFluxName + "Err" 

614 if refFluxErrName in schema: 

615 camFluxErrName = camFluxName + "Err" 

616 aliasMap.set(camFluxErrName, refFluxErrName) 

617 

618 if filterMap is not None: 

619 for filterName, refFilterName in filterMap.items(): 

620 addAliasesForOneFilter(filterName, refFilterName) 

621 

622 @staticmethod 

623 def _makeBoxRegion(BBox, wcs, BBoxPadding): 

624 outerLocalBBox = geom.Box2D(BBox) 

625 innerLocalBBox = geom.Box2D(BBox) 

626 

627 # Grow the bounding box to allow for effects not fully captured by the 

628 # wcs provided (which represents the current best-guess wcs solution 

629 # associated with the dataset for which the calibration is to be 

630 # computed using the loaded and trimmed reference catalog being defined 

631 # here). These effects could include pointing errors and/or an 

632 # insufficient optical distorition model for the instrument. The idea 

633 # is to ensure the spherical geometric region created contains the 

634 # entire region covered by the bbox. 

635 # Also create an inner region that is sure to be inside the bbox. 

636 outerLocalBBox.grow(BBoxPadding) 

637 innerLocalBBox.grow(-1*BBoxPadding) 

638 

639 # Handle the case where the inner bounding box shrank to a zero sized 

640 # region (which will be the case if the shrunken size of either 

641 # dimension is less than or equal to zero). In this case, the inner 

642 # bounding box is set to the original input bounding box. This is 

643 # probably not the best way to handle an empty inner bounding box, but 

644 # it is what the calling code currently expects. 

645 if innerLocalBBox.getDimensions() == geom.Extent2D(0, 0): 

646 innerLocalBBox = geom.Box2D(BBox) 

647 

648 # Convert the corners of the bounding boxes to sky coordinates. 

649 innerBoxCorners = innerLocalBBox.getCorners() 

650 innerSphCorners = [wcs.pixelToSky(corner).getVector() for corner in innerBoxCorners] 

651 innerSkyRegion = sphgeom.ConvexPolygon(innerSphCorners) 

652 

653 outerBoxCorners = outerLocalBBox.getCorners() 

654 outerSphCorners = [wcs.pixelToSky(corner).getVector() for corner in outerBoxCorners] 

655 outerSkyRegion = sphgeom.ConvexPolygon(outerSphCorners) 

656 

657 return innerSkyRegion, outerSkyRegion, innerSphCorners, outerSphCorners 

658 

659 @staticmethod 

660 def _calculateCircle(bbox, wcs, pixelMargin): 

661 """Compute on-sky center and radius of search region. 

662 

663 Parameters 

664 ---------- 

665 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D` 

666 Pixel bounding box. 

667 wcs : `lsst.afw.geom.SkyWcs` 

668 WCS; used to convert pixel positions to sky coordinates. 

669 pixelMargin : `int` 

670 Padding to add to 4 all edges of the bounding box (pixels). 

671 

672 Returns 

673 ------- 

674 results : `lsst.pipe.base.Struct` 

675 A Struct containing: 

676 

677 - coord : `lsst.geom.SpherePoint` 

678 ICRS center of the search region. 

679 - radius : `lsst.geom.Angle` 

680 Radius of the search region. 

681 - bbox : `lsst.geom.Box2D` 

682 Bounding box used to compute the circle. 

683 """ 

684 bbox = geom.Box2D(bbox) # we modify the box, so use a copy 

685 bbox.grow(pixelMargin) 

686 coord = wcs.pixelToSky(bbox.getCenter()) 

687 radius = max(coord.separation(wcs.pixelToSky(pp)) for pp in bbox.getCorners()) 

688 return pipeBase.Struct(coord=coord, radius=radius, bbox=bbox) 

689 

690 @staticmethod 

691 def getMetadataCircle(coord, radius, filterName, epoch=None): 

692 """Return metadata about the reference catalog being loaded. 

693 

694 This metadata is used for reloading the catalog (e.g. for 

695 reconstituting a normalized match list). 

696 

697 Parameters 

698 ---------- 

699 coord : `lsst.geom.SpherePoint` 

700 ICRS center of the search region. 

701 radius : `lsst.geom.Angle` 

702 Radius of the search region. 

703 filterName : `str` 

704 Name of the camera filter. 

705 epoch : `astropy.time.Time` or `None`, optional 

706 Epoch to which to correct proper motion and parallax, or `None` to 

707 not apply such corrections. 

708 

709 Returns 

710 ------- 

711 md : `lsst.daf.base.PropertyList` 

712 Metadata about the catalog. 

713 """ 

714 md = PropertyList() 

715 md.add('RA', coord.getRa().asDegrees(), 'field center in degrees') 

716 md.add('DEC', coord.getDec().asDegrees(), 'field center in degrees') 

717 md.add('RADIUS', radius.asDegrees(), 'field radius in degrees, minimum') 

718 md.add('SMATCHV', 1, 'SourceMatchVector version number') 

719 md.add('FILTER', filterName, 'filter name for photometric data') 

720 md.add('EPOCH', "NONE" if epoch is None else epoch.mjd, 'Epoch (TAI MJD) for catalog') 

721 return md 

722 

723 def getMetadataBox(self, bbox, wcs, filterName, epoch=None, 

724 bboxToSpherePadding=100): 

725 """Return metadata about the load 

726 

727 This metadata is used for reloading the catalog (e.g., for 

728 reconstituting a normalised match list). 

729 

730 Parameters 

731 ---------- 

732 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D` 

733 Bounding box for the pixels. 

734 wcs : `lsst.afw.geom.SkyWcs` 

735 The WCS object associated with ``bbox``. 

736 filterName : `str` 

737 Name of the camera filter. 

738 epoch : `astropy.time.Time` or `None`, optional 

739 Epoch to which to correct proper motion and parallax, or `None` to 

740 not apply such corrections. 

741 bboxToSpherePadding : `int`, optional 

742 Padding in pixels to account for translating a set of corners into 

743 a spherical (convex) boundary that is certain to encompass the 

744 enitre area covered by the box. 

745 

746 Returns 

747 ------- 

748 md : `lsst.daf.base.PropertyList` 

749 The metadata detailing the search parameters used for this 

750 dataset. 

751 """ 

752 circle = self._calculateCircle(bbox, wcs, self.config.pixelMargin) 

753 md = self.getMetadataCircle(circle.coord, circle.radius, filterName, epoch=epoch) 

754 

755 paddedBbox = circle.bbox 

756 _, _, innerCorners, outerCorners = self._makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding) 

757 for box, corners in zip(("INNER", "OUTER"), (innerCorners, outerCorners)): 

758 for (name, corner) in zip(("UPPER_LEFT", "UPPER_RIGHT", "LOWER_LEFT", "LOWER_RIGHT"), 

759 corners): 

760 md.add(f"{box}_{name}_RA", geom.SpherePoint(corner).getRa().asDegrees(), f"{box}_corner") 

761 md.add(f"{box}_{name}_DEC", geom.SpherePoint(corner).getDec().asDegrees(), f"{box}_corner") 

762 return md 

763 

764 def joinMatchListWithCatalog(self, matchCat, sourceCat): 

765 """Relink an unpersisted match list to sources and reference objects. 

766 

767 A match list is persisted and unpersisted as a catalog of IDs 

768 produced by afw.table.packMatches(), with match metadata 

769 (as returned by the astrometry tasks) in the catalog's metadata 

770 attribute. This method converts such a match catalog into a match 

771 list, with links to source records and reference object records. 

772 

773 Parameters 

774 ---------- 

775 matchCat : `lsst.afw.table.BaseCatalog` 

776 Unpersisted packed match list. 

777 ``matchCat.table.getMetadata()`` must contain match metadata, 

778 as returned by the astrometry tasks. 

779 sourceCat : `lsst.afw.table.SourceCatalog` 

780 Source catalog. As a side effect, the catalog will be sorted 

781 by ID. 

782 

783 Returns 

784 ------- 

785 matchList : `lsst.afw.table.ReferenceMatchVector` 

786 Match list. 

787 """ 

788 return joinMatchListWithCatalogImpl(self, matchCat, sourceCat) 

789 

790 def loadPixelBox(self, bbox, wcs, filterName, epoch=None, 

791 bboxToSpherePadding=100): 

792 """Load reference objects that are within a pixel-based rectangular 

793 region. 

794 

795 This algorithm works by creating a spherical box whose corners 

796 correspond to the WCS converted corners of the input bounding box 

797 (possibly padded). It then defines a filtering function which looks at 

798 the pixel position of the reference objects and accepts only those that 

799 lie within the specified bounding box. 

800 

801 The spherical box region and filtering function are passed to the 

802 generic loadRegion method which loads and filters the reference objects 

803 from the datastore and returns a single catalog containing the filtered 

804 set of reference objects. 

805 

806 Parameters 

807 ---------- 

808 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D` 

809 Box which bounds a region in pixel space. 

810 wcs : `lsst.afw.geom.SkyWcs` 

811 Wcs object defining the pixel to sky (and inverse) transform for 

812 the supplied ``bbox``. 

813 filterName : `str` 

814 Name of camera filter. 

815 epoch : `astropy.time.Time` or `None`, optional 

816 Epoch to which to correct proper motion and parallax, or `None` 

817 to not apply such corrections. 

818 bboxToSpherePadding : `int`, optional 

819 Padding to account for translating a set of corners into a 

820 spherical (convex) boundary that is certain to encompase the 

821 enitre area covered by the box. 

822 

823 Returns 

824 ------- 

825 output : `lsst.pipe.base.Struct` 

826 Results struct with attributes: 

827 

828 ``refCat`` 

829 Catalog containing reference objects inside the specified 

830 bounding box (padded by self.config.pixelMargin). 

831 ``fluxField`` 

832 Name of the field containing the flux associated with 

833 ``filterName``. 

834 

835 Raises 

836 ------ 

837 RuntimeError 

838 Raised if no reference catalogs could be found for the specified 

839 region. 

840 TypeError 

841 Raised if the loaded reference catalogs do not have matching 

842 schemas. 

843 """ 

844 paddedBbox = geom.Box2D(bbox) 

845 paddedBbox.grow(self.config.pixelMargin) 

846 innerSkyRegion, outerSkyRegion, _, _ = self._makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding) 

847 

848 def _filterFunction(refCat, region): 

849 # Perform an initial "pre filter" step based on the refCat coords 

850 # and the outerSkyRegion created from the self.config.pixelMargin- 

851 # paddedBbox plus an "extra" padding of bboxToSpherePadding and the 

852 # raw wcs. This should ensure a large enough projected area on the 

853 # sky that accounts for any projection/distortion issues, but small 

854 # enough to filter out loaded reference objects that lie well 

855 # beyond the projected detector of interest. This step is required 

856 # due to the very local nature of the wcs available for the 

857 # sky <--> pixel conversions. 

858 preFiltFunc = _FilterCatalog(outerSkyRegion) 

859 refCat = preFiltFunc(refCat, region) 

860 

861 # Add columns to the pre-filtered reference catalog relating their 

862 # coordinates to equivalent pixel positions for the wcs provided 

863 # and use to populate those columns. 

864 refCat = self._remapReferenceCatalogSchema(refCat, centroids=True) 

865 afwTable.updateRefCentroids(wcs, refCat) 

866 # No need to filter the catalog if it is entirely contained in the 

867 # region defined by the inner sky region. 

868 if innerSkyRegion.contains(region): 

869 return refCat 

870 # Create a new reference catalog, and populate it only with records 

871 # that fall inside the padded bbox. 

872 filteredRefCat = type(refCat)(refCat.table) 

873 centroidKey = afwTable.Point2DKey(refCat.schema['centroid']) 

874 for record in refCat: 

875 pixCoords = record[centroidKey] 

876 if paddedBbox.contains(geom.Point2D(pixCoords)): 

877 filteredRefCat.append(record) 

878 return filteredRefCat 

879 return self.loadRegion(outerSkyRegion, filterName, filtFunc=_filterFunction, epoch=epoch) 

880 

881 def loadRegion(self, region, filterName, filtFunc=None, epoch=None): 

882 """Load reference objects within a specified region. 

883 

884 This function loads the DataIds used to construct an instance of this 

885 class which intersect or are contained within the specified region. The 

886 reference catalogs which intersect but are not fully contained within 

887 the input region are further filtered by the specified filter function. 

888 This function returns a single source catalog containing all reference 

889 objects inside the specified region. 

890 

891 Parameters 

892 ---------- 

893 region : `lsst.sphgeom.Region` 

894 This can be any type that is derived from `lsst.sphgeom.Region` and 

895 should define the spatial region for which reference objects are to 

896 be loaded. 

897 filtFunc : callable or `None`, optional 

898 This optional parameter should be a callable object that takes a 

899 reference catalog and its corresponding region as parameters, 

900 filters the catalog by some criteria and returns the filtered 

901 reference catalog. If `None`, an internal filter function is used 

902 which filters according to if a reference object falls within the 

903 input region. 

904 filterName : `str` 

905 Name of camera filter. 

906 epoch : `astropy.time.Time` or `None`, optional 

907 Epoch to which to correct proper motion and parallax, or `None` to 

908 not apply such corrections. 

909 

910 Returns 

911 ------- 

912 output : `lsst.pipe.base.Struct` 

913 Results struct with attributes: 

914 

915 ``refCat`` 

916 Catalog containing reference objects which intersect the 

917 input region, filtered by the specified filter function. 

918 ``fluxField`` 

919 Name of the field containing the flux associated with 

920 ``filterName``. 

921 

922 Raises 

923 ------ 

924 RuntimeError 

925 Raised if no reference catalogs could be found for the specified 

926 region. 

927 TypeError 

928 Raised if the loaded reference catalogs do not have matching 

929 schemas. 

930 """ 

931 regionLat = region.getBoundingBox().getLat() 

932 regionLon = region.getBoundingBox().getLon() 

933 self.log.info("Loading reference objects from %s in region bounded by " 

934 "[%.8f, %.8f], [%.8f, %.8f] RA Dec", 

935 # Name of refcat we're loading from is the datasetType. 

936 self.refCats[0].ref.datasetType.name, 

937 regionLon.getA().asDegrees(), regionLon.getB().asDegrees(), 

938 regionLat.getA().asDegrees(), regionLat.getB().asDegrees()) 

939 if filtFunc is None: 

940 filtFunc = _FilterCatalog(region) 

941 # filter out all the regions supplied by the constructor that do not overlap 

942 overlapList = [] 

943 for dataId, refCat in zip(self.dataIds, self.refCats): 

944 # SphGeom supports some objects intersecting others, but is not symmetric, 

945 # try the intersect operation in both directions 

946 try: 

947 intersects = dataId.region.intersects(region) 

948 except TypeError: 

949 intersects = region.intersects(dataId.region) 

950 

951 if intersects: 

952 overlapList.append((dataId, refCat)) 

953 

954 if len(overlapList) == 0: 

955 raise RuntimeError("No reference tables could be found for input region") 

956 

957 firstCat = overlapList[0][1].get() 

958 refCat = filtFunc(firstCat, overlapList[0][0].region) 

959 trimmedAmount = len(firstCat) - len(refCat) 

960 

961 # Load in the remaining catalogs 

962 for dataId, inputRefCat in overlapList[1:]: 

963 tmpCat = inputRefCat.get() 

964 

965 if tmpCat.schema != firstCat.schema: 

966 raise TypeError("Reference catalogs have mismatching schemas") 

967 

968 filteredCat = filtFunc(tmpCat, dataId.region) 

969 refCat.extend(filteredCat) 

970 trimmedAmount += len(tmpCat) - len(filteredCat) 

971 

972 self.log.debug("Trimmed %d refCat objects lying outside padded region, leaving %d", 

973 trimmedAmount, len(refCat)) 

974 self.log.info("Loaded %d reference objects", len(refCat)) 

975 

976 # Ensure that the loaded reference catalog is continuous in memory 

977 if not refCat.isContiguous(): 

978 refCat = refCat.copy(deep=True) 

979 

980 self.applyProperMotions(refCat, epoch) 

981 

982 # TODO DM-34793: remove this entire if block. 

983 # Verify the schema is in the correct units and has the correct version; automatically convert 

984 # it with a warning if this is not the case. 

985 if not hasNanojanskyFluxUnits(refCat.schema) or not getFormatVersionFromRefCat(refCat) >= 1: 

986 self.log.warning("Found version 0 reference catalog with old style units in schema.") 

987 self.log.warning("run `meas_algorithms/bin/convert_refcat_to_nJy.py` to convert fluxes to nJy.") 

988 self.log.warning("See RFC-575 for more details.") 

989 refCat = convertToNanojansky(refCat, self.log) 

990 

991 expandedCat = self._remapReferenceCatalogSchema(refCat, 

992 anyFilterMapsToThis=self.config.anyFilterMapsToThis, 

993 filterMap=self.config.filterMap) 

994 

995 # Ensure that the returned reference catalog is continuous in memory 

996 if not expandedCat.isContiguous(): 

997 expandedCat = expandedCat.copy(deep=True) 

998 

999 fluxField = getRefFluxField(expandedCat.schema, filterName) 

1000 return pipeBase.Struct(refCat=expandedCat, fluxField=fluxField) 

1001 

1002 def loadSkyCircle(self, ctrCoord, radius, filterName, epoch=None): 

1003 """Load reference objects that lie within a circular region on the sky. 

1004 

1005 This method constructs a circular region from an input center and 

1006 angular radius, loads reference catalogs which are contained in or 

1007 intersect the circle, and filters reference catalogs which intersect 

1008 down to objects which lie within the defined circle. 

1009 

1010 Parameters 

1011 ---------- 

1012 ctrCoord : `lsst.geom.SpherePoint` 

1013 Point defining the center of the circular region. 

1014 radius : `lsst.geom.Angle` 

1015 Defines the angular radius of the circular region. 

1016 filterName : `str` 

1017 Name of camera filter. 

1018 epoch : `astropy.time.Time` or `None`, optional 

1019 Epoch to which to correct proper motion and parallax, or `None` to 

1020 not apply such corrections. 

1021 

1022 Returns 

1023 ------- 

1024 output : `lsst.pipe.base.Struct` 

1025 Results struct with attributes: 

1026 

1027 ``refCat`` 

1028 Catalog containing reference objects inside the specified 

1029 search circle. 

1030 ``fluxField`` 

1031 Name of the field containing the flux associated with 

1032 ``filterName``. 

1033 """ 

1034 centerVector = ctrCoord.getVector() 

1035 sphRadius = sphgeom.Angle(radius.asRadians()) 

1036 circularRegion = sphgeom.Circle(centerVector, sphRadius) 

1037 return self.loadRegion(circularRegion, filterName, epoch=epoch) 

1038 

1039 

1040def getRefFluxField(schema, filterName): 

1041 """Get the name of a flux field from a schema. 

1042 

1043 Parameters 

1044 ---------- 

1045 schema : `lsst.afw.table.Schema` 

1046 Reference catalog schema. 

1047 filterName : `str` 

1048 Name of camera filter. 

1049 

1050 Returns 

1051 ------- 

1052 fluxFieldName : `str` 

1053 Name of flux field. 

1054 

1055 Notes 

1056 ----- 

1057 Return the alias of ``anyFilterMapsToThis``, if present 

1058 else, return ``*filterName*_camFlux`` if present, 

1059 else, return ``*filterName*_flux`` if present (camera filter name 

1060 matches reference filter name), else raise an exception. 

1061 

1062 Raises 

1063 ------ 

1064 RuntimeError 

1065 Raised if an appropriate field is not found. 

1066 """ 

1067 if not isinstance(schema, afwTable.Schema): 

1068 raise RuntimeError("schema=%s is not a schema" % (schema,)) 

1069 try: 

1070 return schema.getAliasMap().get("anyFilterMapsToThis") 

1071 except LookupError: 

1072 pass # try the filterMap next 

1073 

1074 fluxFieldList = [filterName + "_camFlux", filterName + "_flux"] 

1075 for fluxField in fluxFieldList: 

1076 if fluxField in schema: 

1077 return fluxField 

1078 

1079 raise RuntimeError("Could not find flux field(s) %s" % (", ".join(fluxFieldList))) 

1080 

1081 

1082def getRefFluxKeys(schema, filterName): 

1083 """Return keys for flux and flux error. 

1084 

1085 Parameters 

1086 ---------- 

1087 schema : `lsst.afw.table.Schema` 

1088 Reference catalog schema. 

1089 filterName : `str` 

1090 Name of camera filter. 

1091 

1092 Returns 

1093 ------- 

1094 keys : `tuple` of (`lsst.afw.table.Key`, `lsst.afw.table.Key`) 

1095 Two keys: 

1096 

1097 - flux key 

1098 - flux error key, if present, else None 

1099 

1100 Raises 

1101 ------ 

1102 RuntimeError 

1103 If flux field not found. 

1104 """ 

1105 fluxField = getRefFluxField(schema, filterName) 

1106 fluxErrField = fluxField + "Err" 

1107 fluxKey = schema[fluxField].asKey() 

1108 try: 

1109 fluxErrKey = schema[fluxErrField].asKey() 

1110 except Exception: 

1111 fluxErrKey = None 

1112 return (fluxKey, fluxErrKey) 

1113 

1114 

1115@deprecated(reason=("This task is used in gen2 only; it will be removed after v25. " 

1116 "See DM-35671 for details on updating code to avoid this warning."), 

1117 version="v25.0", category=FutureWarning) 

1118class LoadReferenceObjectsTask(pipeBase.Task, ReferenceObjectLoader, metaclass=abc.ABCMeta): 

1119 """Abstract gen2 base class to load objects from reference catalogs. 

1120 """ 

1121 _DefaultName = "LoadReferenceObjects" 

1122 

1123 @abc.abstractmethod 

1124 def loadSkyCircle(self, ctrCoord, radius, filterName, epoch=None, centroids=False): 

1125 """Load reference objects that overlap a circular sky region. 

1126 

1127 Parameters 

1128 ---------- 

1129 ctrCoord : `lsst.geom.SpherePoint` 

1130 ICRS center of search region. 

1131 radius : `lsst.geom.Angle` 

1132 Radius of search region. 

1133 filterName : `str` 

1134 Name of filter. This can be used for flux limit comparisons. 

1135 epoch : `astropy.time.Time` or `None`, optional 

1136 Epoch to which to correct proper motion and parallax, or `None` to 

1137 not apply such corrections. 

1138 centroids : `bool`, optional 

1139 Add centroid fields to the loaded Schema. ``loadPixelBox`` expects 

1140 these fields to exist. 

1141 

1142 Returns 

1143 ------- 

1144 results : `lsst.pipe.base.Struct` 

1145 A `~lsst.pipe.base.Struct` containing the following fields: 

1146 

1147 ``refCat`` 

1148 A catalog of reference objects with the standard 

1149 schema, as documented in the main doc string for 

1150 `LoadReferenceObjects`. 

1151 The catalog is guaranteed to be contiguous. 

1152 (`lsst.afw.catalog.SimpleCatalog`) 

1153 ``fluxField`` 

1154 Name of flux field for specified `filterName`. (`str`) 

1155 

1156 Notes 

1157 ----- 

1158 Note that subclasses are responsible for performing the proper motion 

1159 correction, since this is the lowest-level interface for retrieving 

1160 the catalog. 

1161 """ 

1162 return 

1163 

1164 

1165def joinMatchListWithCatalogImpl(refObjLoader, matchCat, sourceCat): 

1166 """Relink an unpersisted match list to sources and reference 

1167 objects. 

1168 

1169 A match list is persisted and unpersisted as a catalog of IDs 

1170 produced by afw.table.packMatches(), with match metadata 

1171 (as returned by the astrometry tasks) in the catalog's metadata 

1172 attribute. This method converts such a match catalog into a match 

1173 list, with links to source records and reference object records. 

1174 

1175 Parameters 

1176 ---------- 

1177 refObjLoader 

1178 Reference object loader to use in getting reference objects 

1179 matchCat : `lsst.afw.table.BaseCatalog` 

1180 Unperisted packed match list. 

1181 ``matchCat.table.getMetadata()`` must contain match metadata, 

1182 as returned by the astrometry tasks. 

1183 sourceCat : `lsst.afw.table.SourceCatalog` 

1184 Source catalog. As a side effect, the catalog will be sorted 

1185 by ID. 

1186 

1187 Returns 

1188 ------- 

1189 matchList : `lsst.afw.table.ReferenceMatchVector` 

1190 Match list. 

1191 """ 

1192 matchmeta = matchCat.table.getMetadata() 

1193 version = matchmeta.getInt('SMATCHV') 

1194 if version != 1: 

1195 raise ValueError('SourceMatchVector version number is %i, not 1.' % version) 

1196 filterName = matchmeta.getString('FILTER').strip() 

1197 try: 

1198 epoch = matchmeta.getDouble('EPOCH') 

1199 except (LookupError, TypeError): 

1200 epoch = None # Not present, or not correct type means it's not set 

1201 if 'RADIUS' in matchmeta: 

1202 # This is a circle style metadata, call loadSkyCircle 

1203 ctrCoord = geom.SpherePoint(matchmeta.getDouble('RA'), 

1204 matchmeta.getDouble('DEC'), geom.degrees) 

1205 rad = matchmeta.getDouble('RADIUS')*geom.degrees 

1206 refCat = refObjLoader.loadSkyCircle(ctrCoord, rad, filterName, epoch=epoch).refCat 

1207 elif "INNER_UPPER_LEFT_RA" in matchmeta: 

1208 # This is the sky box type (only triggers in the LoadReferenceObject class, not task) 

1209 # Only the outer box is required to be loaded to get the maximum region, all filtering 

1210 # will be done by the unpackMatches function, and no spatial filtering needs to be done 

1211 # by the refObjLoader 

1212 box = [] 

1213 for place in ("UPPER_LEFT", "UPPER_RIGHT", "LOWER_LEFT", "LOWER_RIGHT"): 

1214 coord = geom.SpherePoint(matchmeta.getDouble(f"OUTER_{place}_RA"), 

1215 matchmeta.getDouble(f"OUTER_{place}_DEC"), 

1216 geom.degrees).getVector() 

1217 box.append(coord) 

1218 outerBox = sphgeom.ConvexPolygon(box) 

1219 refCat = refObjLoader.loadRegion(outerBox, filterName, epoch=epoch).refCat 

1220 

1221 refCat.sort() 

1222 sourceCat.sort() 

1223 return afwTable.unpackMatches(matchCat, refCat, sourceCat) 

1224 

1225 

1226@deprecated(reason="Base class only used for gen2 interface, and will be removed after v25.0. " 

1227 "Please use ReferenceObjectLoader directly.", 

1228 version="v25.0", category=FutureWarning) 

1229class ReferenceObjectLoaderBase(ReferenceObjectLoader): 

1230 """Stub of a deprecated class. 

1231 

1232 Parameters 

1233 ---------- 

1234 config : `lsst.pex.config.Config` 

1235 Configuration for the loader. 

1236 """ 

1237 def __init__(self, config=None, *args, **kwargs): 

1238 pass 

1239 

1240 

1241def applyProperMotionsImpl(log, catalog, epoch): 

1242 """Apply proper motion correction to a reference catalog. 

1243 

1244 Adjust position and position error in the ``catalog`` 

1245 for proper motion to the specified ``epoch``, 

1246 modifying the catalog in place. 

1247 

1248 Parameters 

1249 ---------- 

1250 log : `lsst.log.Log` or `logging.getLogger` 

1251 Log object to write to. 

1252 catalog : `lsst.afw.table.SimpleCatalog` 

1253 Catalog of positions, containing: 

1254 

1255 - Coordinates, retrieved by the table's coordinate key. 

1256 - ``coord_raErr`` : Error in Right Ascension (rad). 

1257 - ``coord_decErr`` : Error in Declination (rad). 

1258 - ``pm_ra`` : Proper motion in Right Ascension (rad/yr, 

1259 East positive) 

1260 - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional. 

1261 - ``pm_dec`` : Proper motion in Declination (rad/yr, 

1262 North positive) 

1263 - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional. 

1264 - ``epoch`` : Mean epoch of object (an astropy.time.Time) 

1265 epoch : `astropy.time.Time` 

1266 Epoch to which to correct proper motion. 

1267 """ 

1268 if "epoch" not in catalog.schema or "pm_ra" not in catalog.schema or "pm_dec" not in catalog.schema: 

1269 log.warning("Proper motion correction not available from catalog") 

1270 return 

1271 if not catalog.isContiguous(): 

1272 raise RuntimeError("Catalog must be contiguous") 

1273 catEpoch = astropy.time.Time(catalog["epoch"], scale="tai", format="mjd") 

1274 log.info("Correcting reference catalog for proper motion to %r", epoch) 

1275 # Use `epoch.tai` to make sure the time difference is in TAI 

1276 timeDiffsYears = (epoch.tai - catEpoch).to(astropy.units.yr).value 

1277 coordKey = catalog.table.getCoordKey() 

1278 # Compute the offset of each object due to proper motion 

1279 # as components of the arc of a great circle along RA and Dec 

1280 pmRaRad = catalog["pm_ra"] 

1281 pmDecRad = catalog["pm_dec"] 

1282 offsetsRaRad = pmRaRad*timeDiffsYears 

1283 offsetsDecRad = pmDecRad*timeDiffsYears 

1284 # Compute the corresponding bearing and arc length of each offset 

1285 # due to proper motion, and apply the offset 

1286 # The factor of 1e6 for computing bearing is intended as 

1287 # a reasonable scale for typical values of proper motion 

1288 # in order to avoid large errors for small values of proper motion; 

1289 # using the offsets is another option, but it can give 

1290 # needlessly large errors for short duration 

1291 offsetBearingsRad = numpy.arctan2(pmDecRad*1e6, pmRaRad*1e6) 

1292 offsetAmountsRad = numpy.hypot(offsetsRaRad, offsetsDecRad) 

1293 for record, bearingRad, amountRad in zip(catalog, offsetBearingsRad, offsetAmountsRad): 

1294 record.set(coordKey, 

1295 record.get(coordKey).offset(bearing=bearingRad*geom.radians, 

1296 amount=amountRad*geom.radians)) 

1297 # Increase error in RA and Dec based on error in proper motion 

1298 if "coord_raErr" in catalog.schema: 

1299 catalog["coord_raErr"] = numpy.hypot(catalog["coord_raErr"], 

1300 catalog["pm_raErr"]*timeDiffsYears) 

1301 if "coord_decErr" in catalog.schema: 

1302 catalog["coord_decErr"] = numpy.hypot(catalog["coord_decErr"], 

1303 catalog["pm_decErr"]*timeDiffsYears)