24 __all__ = [
"getRefFluxField",
"getRefFluxKeys",
"LoadReferenceObjectsTask",
"LoadReferenceObjectsConfig",
25 "ReferenceObjectLoader"]
39 from lsst
import sphgeom
41 from lsst.utils.timer
import timeMethod
45 """Return True if this name/units combination corresponds to an
46 "old-style" reference catalog flux field.
48 unitsCheck = units !=
'nJy'
49 isFlux = name.endswith(
'_flux')
50 isFluxSigma = name.endswith(
'_fluxSigma')
51 isFluxErr = name.endswith(
'_fluxErr')
52 return (isFlux
or isFluxSigma
or isFluxErr)
and unitsCheck
56 """Return True if the units of all flux and fluxErr are correct (nJy).
65 """"Return the format version stored in a reference catalog header.
69 refCat : `lsst.afw.table.SimpleCatalog`
70 Reference catalog to inspect.
75 Format verison integer. Returns `0` if the catalog has no metadata
76 or the metadata does not include a "REFCAT_FORMAT_VERSION" key.
78 md = refCat.getMetadata()
82 return md.getScalar(
"REFCAT_FORMAT_VERSION")
88 """Convert fluxes in a catalog from jansky to nanojansky.
92 catalog : `lsst.afw.table.SimpleCatalog`
93 The catalog to convert.
94 log : `lsst.log.Log` or `logging.Logger`
95 Log to send messages to.
96 doConvert : `bool`, optional
97 Return a converted catalog, or just identify the fields that need to be converted?
98 This supports the "write=False" mode of `bin/convert_to_nJy.py`.
102 catalog : `lsst.afw.table.SimpleCatalog` or None
103 The converted catalog, or None if ``doConvert`` is False.
107 Support for old units in reference catalogs will be removed after the
108 release of late calendar year 2019.
109 Use `meas_algorithms/bin/convert_to_nJy.py` to update your reference catalog.
114 mapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
117 for field
in catalog.schema:
118 oldName = field.field.getName()
119 oldUnits = field.field.getUnits()
123 if oldName.endswith(
'_fluxSigma'):
124 name = oldName.replace(
'_fluxSigma',
'_fluxErr')
127 newField =
afwTable.Field[field.dtype](name, field.field.getDoc(), units)
128 mapper.addMapping(field.getKey(), newField)
129 input_fields.append(field.field)
130 output_fields.append(newField)
132 mapper.addMapping(field.getKey())
134 fluxFieldsStr =
'; '.join(
"(%s, '%s')" % (field.getName(), field.getUnits())
for field
in input_fields)
137 newSchema = mapper.getOutputSchema()
139 output.extend(catalog, mapper=mapper)
140 for field
in output_fields:
141 output[field.getName()] *= 1e9
142 log.info(
"Converted refcat flux fields to nJy (name, units): %s", fluxFieldsStr)
145 log.info(
"Found old-style refcat flux fields (name, units): %s", fluxFieldsStr)
150 """This is a private helper class which filters catalogs by
151 row based on the row being inside the region used to initialize
156 region : `lsst.sphgeom.Region`
157 The spatial region which all objects should lie within
163 """This call method on an instance of this class takes in a reference
164 catalog, and the region from which the catalog was generated.
166 If the catalog region is entirely contained within the region used to
167 initialize this class, then all the entries in the catalog must be
168 within the region and so the whole catalog is returned.
170 If the catalog region is not entirely contained, then the location for
171 each record is tested against the region used to initialize the class.
172 Records which fall inside this region are added to a new catalog, and
173 this catalog is then returned.
177 refCat : `lsst.afw.table.SourceCatalog`
178 SourceCatalog to be filtered.
179 catRegion : `lsst.sphgeom.Region`
180 Region in which the catalog was created
182 if catRegion.isWithin(self.
regionregion):
186 filteredRefCat =
type(refCat)(refCat.table)
187 for record
in refCat:
189 filteredRefCat.append(record)
190 return filteredRefCat
194 """Base class for reference object loaders, to facilitate gen2/gen3 code
198 """Apply proper motion correction to a reference catalog.
200 Adjust position and position error in the ``catalog``
201 for proper motion to the specified ``epoch``,
202 modifying the catalog in place.
206 catalog : `lsst.afw.table.SimpleCatalog`
207 Catalog of positions, containing at least these fields:
209 - Coordinates, retrieved by the table's coordinate key.
210 - ``coord_raErr`` : Error in Right Ascension (rad).
211 - ``coord_decErr`` : Error in Declination (rad).
212 - ``pm_ra`` : Proper motion in Right Ascension (rad/yr,
214 - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
215 - ``pm_dec`` : Proper motion in Declination (rad/yr,
217 - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional.
218 - ``epoch`` : Mean epoch of object (an astropy.time.Time)
219 epoch : `astropy.time.Time`
220 Epoch to which to correct proper motion.
221 If None, do not apply PM corrections or raise if
222 ``config.requireProperMotion`` is True.
227 Raised if ``config.requireProperMotion`` is set but we cannot
228 apply the proper motion correction for some reason.
231 if self.config.requireProperMotion:
232 raise RuntimeError(
"requireProperMotion=True but epoch not provided to loader.")
234 self.log.
debug(
"No epoch provided: not applying proper motion corrections to refcat.")
238 if (
"pm_ra" in catalog.schema
239 and not isinstance(catalog.schema[
"pm_ra"].asKey(), afwTable.KeyAngle)):
240 if self.config.requireProperMotion:
241 raise RuntimeError(
"requireProperMotion=True but refcat pm_ra field is not an Angle.")
243 self.log.
warning(
"Reference catalog pm_ra field is not an Angle; cannot apply proper motion.")
246 if (
"epoch" not in catalog.schema
or "pm_ra" not in catalog.schema):
247 if self.config.requireProperMotion:
248 raise RuntimeError(
"requireProperMotion=True but PM data not available from catalog.")
250 self.log.
warning(
"Proper motion correction not available for this reference catalog.")
257 """This class facilitates loading reference catalogs with gen 3 middleware
259 The middleware preflight solver will create a list of datarefs that may
260 possibly overlap a given region. These datarefs are then used to construct
261 and instance of this class. The class instance should then be passed into
262 a task which needs reference catalogs. These tasks should then determine
263 the exact region of the sky reference catalogs will be loaded for, and
264 call a corresponding method to load the reference objects.
266 def __init__(self, dataIds, refCats, config, log=None):
267 """ Constructs an instance of ReferenceObjectLoader
271 dataIds : iterable of `lsst.daf.butler.DataIds`
272 An iterable object of DataSetRefs which point to reference catalogs
273 in a gen 3 repository.
274 refCats : iterable of `lsst.daf.butler.DeferedDatasetHandle`
275 Handles to load refCats on demand
276 config : `lsst.pex.config.configurableField`
277 Configuration for the loader.
278 log : `lsst.log.Log`, `logging.Logger` or `None`, optional
279 Logger object used to write out messages. If `None` a default
284 self.
loglog = log
or logging.getLogger(__name__).getChild(
"ReferenceObjectLoader")
288 def _makeBoxRegion(BBox, wcs, BBoxPadding):
301 outerLocalBBox.grow(BBoxPadding)
302 innerLocalBBox.grow(-1*BBoxPadding)
314 innerBoxCorners = innerLocalBBox.getCorners()
315 innerSphCorners = [wcs.pixelToSky(corner).getVector()
for corner
in innerBoxCorners]
318 outerBoxCorners = outerLocalBBox.getCorners()
319 outerSphCorners = [wcs.pixelToSky(corner).getVector()
for corner
in outerBoxCorners]
322 return innerSkyRegion, outerSkyRegion, innerSphCorners, outerSphCorners
324 def loadPixelBox(self, bbox, wcs, filterName=None, epoch=None, photoCalib=None,
325 bboxToSpherePadding=100):
326 """Load reference objects that are within a pixel-based rectangular
329 This algorithm works by creating a spherical box whose corners
330 correspond to the WCS converted corners of the input bounding box
331 (possibly padded). It then defines a filtering function which looks at
332 the pixel position of the reference objects and accepts only those that
333 lie within the specified bounding box.
335 The spherical box region and filtering function are passed to the
336 generic loadRegion method which loads and filters the reference objects
337 from the datastore and returns a single catalog containing the filtered
338 set of reference objects.
342 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
343 Box which bounds a region in pixel space.
344 wcs : `lsst.afw.geom.SkyWcs`
345 Wcs object defining the pixel to sky (and inverse) transform for
346 the supplied ``bbox``.
347 filterName : `str` or `None`, optional
348 Name of camera filter, or `None` or blank for the default filter.
349 epoch : `astropy.time.Time` or `None`, optional
350 Epoch to which to correct proper motion and parallax, or `None`
351 to not apply such corrections.
353 Deprecated and ignored, only included for api compatibility.
354 bboxToSpherePadding : `int`, optional
355 Padding to account for translating a set of corners into a
356 spherical (convex) boundary that is certain to encompase the
357 enitre area covered by the box.
361 referenceCatalog : `lsst.afw.table.SimpleCatalog`
362 Catalog containing reference objects inside the specified bounding
363 box (padded by self.config.pixelMargin).
368 Raised if no reference catalogs could be found for the specified
371 Raised if the loaded reference catalogs do not have matching
375 paddedBbox.grow(self.
configconfig.pixelMargin)
376 innerSkyRegion, outerSkyRegion, _, _ = self.
_makeBoxRegion_makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding)
378 def _filterFunction(refCat, region):
389 refCat = preFiltFunc(refCat, region)
398 if innerSkyRegion.contains(region):
402 filteredRefCat =
type(refCat)(refCat.table)
404 for record
in refCat:
405 pixCoords = record[centroidKey]
407 filteredRefCat.append(record)
408 return filteredRefCat
409 return self.
loadRegionloadRegion(outerSkyRegion, filtFunc=_filterFunction, epoch=epoch, filterName=filterName)
411 def loadRegion(self, region, filtFunc=None, filterName=None, epoch=None):
412 """Load reference objects within a specified region.
414 This function loads the DataIds used to construct an instance of this
415 class which intersect or are contained within the specified region. The
416 reference catalogs which intersect but are not fully contained within
417 the input region are further filtered by the specified filter function.
418 This function returns a single source catalog containing all reference
419 objects inside the specified region.
423 region : `lsst.sphgeom.Region`
424 This can be any type that is derived from `lsst.sphgeom.Region` and
425 should define the spatial region for which reference objects are to
427 filtFunc : callable or `None`, optional
428 This optional parameter should be a callable object that takes a
429 reference catalog and its corresponding region as parameters,
430 filters the catalog by some criteria and returns the filtered
431 reference catalog. If `None`, an internal filter function is used
432 which filters according to if a reference object falls within the
434 filterName : `str` or `None`, optional
435 Name of camera filter, or `None` or blank for the default filter.
436 epoch : `astropy.time.Time` or `None`, optional
437 Epoch to which to correct proper motion and parallax, or `None` to
438 not apply such corrections.
442 referenceCatalog : `lsst.afw.table.SourceCatalog`
443 Catalog containing reference objects which intersect the input region,
444 filtered by the specified filter function.
449 Raised if no reference catalogs could be found for the specified
452 Raised if the loaded reference catalogs do not have matching
455 regionLat = region.getBoundingBox().getLat()
456 regionLon = region.getBoundingBox().getLon()
457 self.
loglog.
info(
"Loading reference objects from region bounded by "
458 "[%.8f, %.8f], [%.8f, %.8f] RA Dec",
459 regionLon.getA().asDegrees(), regionLon.getB().asDegrees(),
460 regionLat.getA().asDegrees(), regionLat.getB().asDegrees())
465 for dataId, refCat
in zip(self.
dataIdsdataIds, self.
refCatsrefCats):
469 intersects = dataId.region.intersects(region)
471 intersects = region.intersects(dataId.region)
474 overlapList.append((dataId, refCat))
476 if len(overlapList) == 0:
477 raise RuntimeError(
"No reference tables could be found for input region")
479 firstCat = overlapList[0][1].get()
480 refCat = filtFunc(firstCat, overlapList[0][0].region)
481 trimmedAmount = len(firstCat) - len(refCat)
484 for dataId, inputRefCat
in overlapList[1:]:
485 tmpCat = inputRefCat.get()
487 if tmpCat.schema != firstCat.schema:
488 raise TypeError(
"Reference catalogs have mismatching schemas")
490 filteredCat = filtFunc(tmpCat, dataId.region)
491 refCat.extend(filteredCat)
492 trimmedAmount += len(tmpCat) - len(filteredCat)
494 self.
loglog.
debug(
"Trimmed %d refCat objects lying outside padded region, leaving %d",
495 trimmedAmount, len(refCat))
496 self.
loglog.
info(
"Loaded %d reference objects", len(refCat))
499 if not refCat.isContiguous():
500 refCat = refCat.copy(deep=
True)
507 self.
loglog.
warning(
"Found version 0 reference catalog with old style units in schema.")
508 self.
loglog.
warning(
"run `meas_algorithms/bin/convert_refcat_to_nJy.py` to convert fluxes to nJy.")
509 self.
loglog.
warning(
"See RFC-575 for more details.")
518 if not expandedCat.isContiguous():
519 expandedCat = expandedCat.copy(deep=
True)
521 fluxField =
getRefFluxField(schema=expandedCat.schema, filterName=filterName)
522 return pipeBase.Struct(refCat=expandedCat, fluxField=fluxField)
525 """Load reference objects that lie within a circular region on the sky.
527 This method constructs a circular region from an input center and
528 angular radius, loads reference catalogs which are contained in or
529 intersect the circle, and filters reference catalogs which intersect
530 down to objects which lie within the defined circle.
534 ctrCoord : `lsst.geom.SpherePoint`
535 Point defining the center of the circular region.
536 radius : `lsst.geom.Angle`
537 Defines the angular radius of the circular region.
538 filterName : `str` or `None`, optional
539 Name of camera filter, or `None` or blank for the default filter.
540 epoch : `astropy.time.Time` or `None`, optional
541 Epoch to which to correct proper motion and parallax, or `None` to
542 not apply such corrections.
546 referenceCatalog : `lsst.afw.table.SourceCatalog`
547 Catalog containing reference objects inside the specified search
550 centerVector = ctrCoord.getVector()
553 return self.
loadRegionloadRegion(circularRegion, filterName=filterName, epoch=epoch)
556 """Relink an unpersisted match list to sources and reference objects.
558 A match list is persisted and unpersisted as a catalog of IDs
559 produced by afw.table.packMatches(), with match metadata
560 (as returned by the astrometry tasks) in the catalog's metadata
561 attribute. This method converts such a match catalog into a match
562 list, with links to source records and reference object records.
566 matchCat : `lsst.afw.table.BaseCatalog`
567 Unpersisted packed match list.
568 ``matchCat.table.getMetadata()`` must contain match metadata,
569 as returned by the astrometry tasks.
570 sourceCat : `lsst.afw.table.SourceCatalog`
571 Source catalog. As a side effect, the catalog will be sorted
576 matchList : `lsst.afw.table.ReferenceMatchVector`
581 def getMetadataBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None,
582 bboxToSpherePadding=100):
583 """Return metadata about the load
585 This metadata is used for reloading the catalog (e.g., for
586 reconstituting a normalised match list.)
590 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
591 Bounding box for the pixels.
592 wcs : `lsst.afw.geom.SkyWcs`
593 The WCS object associated with ``bbox``.
594 filterName : `str` or `None`, optional
595 Name of the camera filter, or `None` or blank for the default
598 Deprecated, only included for api compatibility.
599 epoch : `astropy.time.Time` or `None`, optional
600 Epoch to which to correct proper motion and parallax, or `None` to
601 not apply such corrections.
602 bboxToSpherePadding : `int`, optional
603 Padding to account for translating a set of corners into a
604 spherical (convex) boundary that is certain to encompase the
605 enitre area covered by the box.
609 md : `lsst.daf.base.PropertyList`
610 The metadata detailing the search parameters used for this
614 paddedBbox.grow(self.
configconfig.pixelMargin)
615 _, _, innerCorners, outerCorners = self.
_makeBoxRegion_makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding)
617 for box, corners
in zip((
"INNER",
"OUTER"), (innerCorners, outerCorners)):
618 for (name, corner)
in zip((
"UPPER_LEFT",
"UPPER_RIGHT",
"LOWER_LEFT",
"LOWER_RIGHT"),
620 md.add(f
"{box}_{name}_RA",
geom.SpherePoint(corner).getRa().asDegrees(), f
"{box}_corner")
621 md.add(f
"{box}_{name}_DEC",
geom.SpherePoint(corner).getDec().asDegrees(), f
"{box}_corner")
622 md.add(
"SMATCHV", 1,
'SourceMatchVector version number')
623 filterName =
"UNKNOWN" if filterName
is None else str(filterName)
624 md.add(
'FILTER', filterName,
'filter name for photometric data')
625 md.add(
'EPOCH',
"NONE" if epoch
is None else epoch.mjd,
'Epoch (TAI MJD) for catalog')
630 """Return metadata about the load.
632 This metadata is used for reloading the catalog (e.g. for
633 reconstituting a normalized match list.)
637 coord : `lsst.geom.SpherePoint`
638 ICRS center of the search region.
639 radius : `lsst.geom.Angle`
640 Radius of the search region.
641 filterName : `str` or `None`
642 Name of the camera filter, or `None` or blank for the default
645 Deprecated, only included for api compatibility.
646 epoch : `astropy.time.Time` or `None`, optional
647 Epoch to which to correct proper motion and parallax, or `None` to
648 not apply such corrections.
652 md : `lsst.daf.base.PropertyList`
655 md.add(
'RA', coord.getRa().asDegrees(),
'field center in degrees')
656 md.add(
'DEC', coord.getDec().asDegrees(),
'field center in degrees')
657 md.add(
'RADIUS', radius.asDegrees(),
'field radius in degrees, minimum')
658 md.add(
'SMATCHV', 1,
'SourceMatchVector version number')
659 filterName =
"UNKNOWN" if filterName
is None else str(filterName)
660 md.add(
'FILTER', filterName,
'filter name for photometric data')
661 md.add(
'EPOCH',
"NONE" if epoch
is None else epoch.mjd,
'Epoch (TAI MJD) for catalog')
666 """Add flux columns and aliases for camera to reference mapping.
668 Creates a new catalog containing the information of the input refCat
669 as well as added flux columns and aliases between camera and reference
674 refCat : `lsst.afw.table.SimpleCatalog`
675 Catalog of reference objects
676 defaultFilter : `str`
677 Name of the default reference filter
678 filterReferenceMap : `dict` of `str`
679 Dictionary with keys corresponding to a filter name and values
680 which correspond to the name of the reference filter.
684 refCat : `lsst.afw.table.SimpleCatalog`
685 Reference catalog with columns added to track reference filters.
690 If the specified reference filter name is not specifed as a
691 key in the reference filter map.
693 refCat = ReferenceObjectLoader.remapReferenceCatalogSchema(refCat,
694 filterNameList=filterReferenceMap.keys())
695 aliasMap = refCat.schema.getAliasMap()
696 if filterReferenceMap
is None:
697 filterReferenceMap = {}
698 for filterName, refFilterName
in itertools.chain([(
None, defaultFilter)],
699 filterReferenceMap.items()):
701 camFluxName = filterName +
"_camFlux" if filterName
is not None else "camFlux"
702 refFluxName = refFilterName +
"_flux"
703 if refFluxName
not in refCat.schema:
704 raise RuntimeError(
"Unknown reference filter %s" % (refFluxName,))
705 aliasMap.set(camFluxName, refFluxName)
707 refFluxErrName = refFluxName +
"Err"
708 camFluxErrName = camFluxName +
"Err"
709 aliasMap.set(camFluxErrName, refFluxErrName)
715 """This function takes in a reference catalog and creates a new catalog with additional
716 columns defined the remaining function arguments.
720 refCat : `lsst.afw.table.SimpleCatalog`
721 Reference catalog to map to new catalog
725 expandedCat : `lsst.afw.table.SimpleCatalog`
726 Deep copy of input reference catalog with additional columns added
729 mapper.addMinimalSchema(refCat.schema,
True)
730 mapper.editOutputSchema().disconnectAliases()
732 for filterName
in filterNameList:
733 mapper.editOutputSchema().addField(f
"{filterName}_flux",
735 doc=f
"flux in filter {filterName}",
738 mapper.editOutputSchema().addField(f
"{filterName}_fluxErr",
740 doc=f
"flux uncertanty in filter {filterName}",
745 mapper.editOutputSchema().addField(
"centroid_x", type=float, doReplace=
True)
746 mapper.editOutputSchema().addField(
"centroid_y", type=float, doReplace=
True)
747 mapper.editOutputSchema().addField(
"hasCentroid", type=
"Flag", doReplace=
True)
748 mapper.editOutputSchema().getAliasMap().
set(
"slot_Centroid",
"centroid")
751 mapper.editOutputSchema().addField(
"photometric",
753 doc=
"set if the object can be used for photometric"
756 mapper.editOutputSchema().addField(
"resolved",
758 doc=
"set if the object is spatially resolved"
760 mapper.editOutputSchema().addField(
"variable",
762 doc=
"set if the object has variable brightness"
766 expandedCat.setMetadata(refCat.getMetadata())
767 expandedCat.extend(refCat, mapper=mapper)
773 """Get the name of a flux field from a schema.
775 return the alias of "anyFilterMapsToThis", if present
776 else if filterName is specified:
777 return "*filterName*_camFlux" if present
778 else return "*filterName*_flux" if present (camera filter name
779 matches reference filter name)
780 else throw RuntimeError
782 return "camFlux", if present,
783 else throw RuntimeError
787 schema : `lsst.afw.table.Schema`
788 Reference catalog schema.
789 filterName : `str`, optional
790 Name of camera filter. If not specified, ``defaultFilter`` needs to be
791 set in the refcat loader config.
795 fluxFieldName : `str`
801 If an appropriate field is not found.
804 raise RuntimeError(
"schema=%s is not a schema" % (schema,))
806 return schema.getAliasMap().get(
"anyFilterMapsToThis")
811 fluxFieldList = [filterName +
"_camFlux", filterName +
"_flux"]
813 fluxFieldList = [
"camFlux"]
814 for fluxField
in fluxFieldList:
815 if fluxField
in schema:
818 raise RuntimeError(
"Could not find flux field(s) %s" % (
", ".join(fluxFieldList)))
822 """Return keys for flux and flux error.
826 schema : `lsst.afw.table.Schema`
827 Reference catalog schema.
829 Name of camera filter.
833 keys : `tuple` of (`lsst.afw.table.Key`, `lsst.afw.table.Key`)
837 - flux error key, if present, else None
842 If flux field not found.
845 fluxErrField = fluxField +
"Err"
846 fluxKey = schema[fluxField].asKey()
848 fluxErrKey = schema[fluxErrField].asKey()
851 return (fluxKey, fluxErrKey)
855 pixelMargin = pexConfig.RangeField(
856 doc=
"Padding to add to 4 all edges of the bounding box (pixels)",
861 defaultFilter = pexConfig.Field(
862 doc=(
"Default reference catalog filter to use if filter not specified in exposure;"
863 " if blank then filter must be specified in exposure."),
866 deprecated=
"defaultFilter is deprecated by RFC-716. Will be removed after v22."
868 anyFilterMapsToThis = pexConfig.Field(
869 doc=(
"Always use this reference catalog filter, no matter whether or what filter name is "
870 "supplied to the loader. Effectively a trivial filterMap: map all filter names to this filter."
871 " This can be set for purely-astrometric catalogs (e.g. Gaia DR2) where there is only one "
872 "reasonable choice for every camera filter->refcat mapping, but not for refcats used for "
873 "photometry, which need a filterMap and/or colorterms/transmission corrections."),
878 filterMap = pexConfig.DictField(
879 doc=(
"Mapping of camera filter name: reference catalog filter name; "
880 "each reference filter must exist in the refcat."
881 " Note that this does not perform any bandpass corrections: it is just a lookup."),
886 requireProperMotion = pexConfig.Field(
887 doc=
"Require that the fields needed to correct proper motion "
888 "(epoch, pm_ra and pm_dec) are present?",
896 msg =
"`filterMap` and `anyFilterMapsToThis` are mutually exclusive"
897 raise pexConfig.FieldValidationError(LoadReferenceObjectsConfig.anyFilterMapsToThis,
902 """Abstract base class to load objects from reference catalogs.
904 ConfigClass = LoadReferenceObjectsConfig
905 _DefaultName =
"LoadReferenceObjects"
908 """Construct a LoadReferenceObjectsTask
912 butler : `lsst.daf.persistence.Butler`
913 Data butler, for access reference catalogs.
915 pipeBase.Task.__init__(self, *args, **kwargs)
919 def loadPixelBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None):
920 """Load reference objects that overlap a rectangular pixel region.
924 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
925 Bounding box for pixels.
926 wcs : `lsst.afw.geom.SkyWcs`
927 WCS; used to convert pixel positions to sky coordinates
929 filterName : `str` or `None`, optional
930 Name of filter, or `None` or `""` for the default filter.
931 This is used for flux values in case we have flux limits
932 (which are not yet implemented).
934 Deprecated, only included for api compatibility.
935 epoch : `astropy.time.Time` or `None`, optional
936 Epoch to which to correct proper motion and parallax, or `None` to
937 not apply such corrections.
941 results : `lsst.pipe.base.Struct`
942 A Struct containing the following fields:
943 refCat : `lsst.afw.catalog.SimpleCatalog`
944 A catalog of reference objects with the standard
945 schema, as documented in the main doc string for
946 `LoadReferenceObjects`.
947 The catalog is guaranteed to be contiguous.
949 Name of flux field for specified `filterName`.
953 The search algorithm works by searching in a region in sky
954 coordinates whose center is the center of the bbox and radius
955 is large enough to just include all 4 corners of the bbox.
956 Stars that lie outside the bbox are then trimmed from the list.
961 self.log.
info(
"Loading reference objects using center %s and radius %s deg",
962 circle.coord, circle.radius.asDegrees())
963 loadRes = self.
loadSkyCircleloadSkyCircle(circle.coord, circle.radius, filterName=filterName, epoch=epoch,
965 refCat = loadRes.refCat
966 numFound = len(refCat)
969 refCat = self.
_trimToBBox_trimToBBox(refCat=refCat, bbox=circle.bbox, wcs=wcs)
970 numTrimmed = numFound - len(refCat)
971 self.log.
debug(
"trimmed %d out-of-bbox objects, leaving %d", numTrimmed, len(refCat))
972 self.log.
info(
"Loaded %d reference objects", len(refCat))
975 if not refCat.isContiguous():
976 loadRes.refCat = refCat.copy(deep=
True)
981 def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None, centroids=False):
982 """Load reference objects that overlap a circular sky region.
986 ctrCoord : `lsst.geom.SpherePoint`
987 ICRS center of search region.
988 radius : `lsst.geom.Angle`
989 Radius of search region.
990 filterName : `str` or `None`, optional
991 Name of filter, or `None` or `""` for the default filter.
992 This is used for flux values in case we have flux limits
993 (which are not yet implemented).
994 epoch : `astropy.time.Time` or `None`, optional
995 Epoch to which to correct proper motion and parallax, or `None` to
996 not apply such corrections.
997 centroids : `bool`, optional
998 Add centroid fields to the loaded Schema. ``loadPixelBox`` expects
999 these fields to exist.
1003 results : `lsst.pipe.base.Struct`
1004 A Struct containing the following fields:
1005 refCat : `lsst.afw.catalog.SimpleCatalog`
1006 A catalog of reference objects with the standard
1007 schema, as documented in the main doc string for
1008 `LoadReferenceObjects`.
1009 The catalog is guaranteed to be contiguous.
1011 Name of flux field for specified `filterName`.
1015 Note that subclasses are responsible for performing the proper motion
1016 correction, since this is the lowest-level interface for retrieving
1022 def _trimToBBox(refCat, bbox, wcs):
1023 """Remove objects outside a given pixel bounding box and set
1024 centroid and hasCentroid fields.
1028 refCat : `lsst.afw.table.SimpleCatalog`
1029 A catalog of objects. The schema must include fields
1030 "coord", "centroid" and "hasCentroid".
1031 The "coord" field is read.
1032 The "centroid" and "hasCentroid" fields are set.
1033 bbox : `lsst.geom.Box2D`
1035 wcs : `lsst.afw.geom.SkyWcs`
1036 WCS; used to convert sky coordinates to pixel positions.
1040 catalog : `lsst.afw.table.SimpleCatalog`
1041 Reference objects in the bbox, with centroid and
1042 hasCentroid fields set.
1046 retStarCat =
type(refCat)(refCat.table)
1048 point = star.get(centroidKey)
1049 if bbox.contains(point):
1050 retStarCat.append(star)
1053 def _addFluxAliases(self, schema):
1054 """Add aliases for camera filter fluxes to the schema.
1056 If self.config.defaultFilter then adds these aliases:
1057 camFlux: <defaultFilter>_flux
1058 camFluxErr: <defaultFilter>_fluxErr, if the latter exists
1060 For each camFilter: refFilter in self.config.filterMap adds these aliases:
1061 <camFilter>_camFlux: <refFilter>_flux
1062 <camFilter>_camFluxErr: <refFilter>_fluxErr, if the latter exists
1066 schema : `lsst.afw.table.Schema`
1067 Schema for reference catalog.
1072 If any reference flux field is missing from the schema.
1074 aliasMap = schema.getAliasMap()
1076 if self.config.anyFilterMapsToThis
is not None:
1077 refFluxName = self.config.anyFilterMapsToThis +
"_flux"
1078 if refFluxName
not in schema:
1079 msg = f
"Unknown reference filter for anyFilterMapsToThis='{refFluxName}'"
1080 raise RuntimeError(msg)
1081 aliasMap.set(
"anyFilterMapsToThis", refFluxName)
1084 def addAliasesForOneFilter(filterName, refFilterName):
1085 """Add aliases for a single filter
1089 filterName : `str` (optional)
1090 Camera filter name. The resulting alias name is
1091 <filterName>_camFlux, or simply "camFlux" if `filterName`
1093 refFilterName : `str`
1094 Reference catalog filter name; the field
1095 <refFilterName>_flux must exist.
1097 camFluxName = filterName +
"_camFlux" if filterName
is not None else "camFlux"
1098 refFluxName = refFilterName +
"_flux"
1099 if refFluxName
not in schema:
1100 raise RuntimeError(
"Unknown reference filter %s" % (refFluxName,))
1101 aliasMap.set(camFluxName, refFluxName)
1102 refFluxErrName = refFluxName +
"Err"
1103 if refFluxErrName
in schema:
1104 camFluxErrName = camFluxName +
"Err"
1105 aliasMap.set(camFluxErrName, refFluxErrName)
1107 if self.config.defaultFilter:
1108 addAliasesForOneFilter(
None, self.config.defaultFilter)
1110 for filterName, refFilterName
in self.config.filterMap.items():
1111 addAliasesForOneFilter(filterName, refFilterName)
1115 addIsPhotometric=False, addIsResolved=False,
1116 addIsVariable=False, coordErrDim=2,
1117 addProperMotion=False, properMotionErrDim=2,
1119 """Make a standard schema for reference object catalogs.
1123 filterNameList : `list` of `str`
1124 List of filter names. Used to create <filterName>_flux fields.
1125 addIsPhotometric : `bool`
1126 If True then add field "photometric".
1127 addIsResolved : `bool`
1128 If True then add field "resolved".
1129 addIsVariable : `bool`
1130 If True then add field "variable".
1132 Number of coord error fields; must be one of 0, 2, 3:
1134 - If 2 or 3: add fields "coord_raErr" and "coord_decErr".
1135 - If 3: also add field "coord_radecErr".
1136 addProperMotion : `bool`
1137 If True add fields "epoch", "pm_ra", "pm_dec" and "pm_flag".
1138 properMotionErrDim : `int`
1139 Number of proper motion error fields; must be one of 0, 2, 3;
1140 ignored if addProperMotion false:
1141 - If 2 or 3: add fields "pm_raErr" and "pm_decErr".
1142 - If 3: also add field "pm_radecErr".
1143 addParallax : `bool`
1144 If True add fields "epoch", "parallax", "parallaxErr"
1145 and "parallax_flag".
1149 schema : `lsst.afw.table.Schema`
1150 Schema for reference catalog, an
1151 `lsst.afw.table.SimpleCatalog`.
1155 Reference catalogs support additional covariances, such as
1156 covariance between RA and proper motion in declination,
1157 that are not supported by this method, but can be added after
1158 calling this method.
1160 schema = afwTable.SimpleTable.makeMinimalSchema()
1162 afwTable.Point2DKey.addFields(
1165 "centroid on an exposure, if relevant",
1169 field=
"hasCentroid",
1171 doc=
"is position known?",
1173 for filterName
in filterNameList:
1175 field=
"%s_flux" % (filterName,),
1177 doc=
"flux in filter %s" % (filterName,),
1180 for filterName
in filterNameList:
1182 field=
"%s_fluxErr" % (filterName,),
1184 doc=
"flux uncertainty in filter %s" % (filterName,),
1187 if addIsPhotometric:
1189 field=
"photometric",
1191 doc=
"set if the object can be used for photometric calibration",
1197 doc=
"set if the object is spatially resolved",
1203 doc=
"set if the object has variable brightness",
1205 if coordErrDim
not in (0, 2, 3):
1206 raise ValueError(
"coordErrDim={}; must be (0, 2, 3)".
format(coordErrDim))
1208 afwTable.CovarianceMatrix2fKey.addFields(
1211 names=[
"ra",
"dec"],
1212 units=[
"rad",
"rad"],
1213 diagonalOnly=(coordErrDim == 2),
1216 if addProperMotion
or addParallax:
1220 doc=
"date of observation (TAI, MJD)",
1228 doc=
"proper motion in the right ascension direction = dra/dt * cos(dec)",
1234 doc=
"proper motion in the declination direction",
1237 if properMotionErrDim
not in (0, 2, 3):
1238 raise ValueError(
"properMotionErrDim={}; must be (0, 2, 3)".
format(properMotionErrDim))
1239 if properMotionErrDim > 0:
1240 afwTable.CovarianceMatrix2fKey.addFields(
1243 names=[
"ra",
"dec"],
1244 units=[
"rad/year",
"rad/year"],
1245 diagonalOnly=(properMotionErrDim == 2),
1250 doc=
"Set if proper motion or proper motion error is bad",
1261 field=
"parallaxErr",
1263 doc=
"uncertainty in parallax",
1267 field=
"parallax_flag",
1269 doc=
"Set if parallax or parallax error is bad",
1273 def _calculateCircle(self, bbox, wcs):
1274 """Compute on-sky center and radius of search region.
1278 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
1280 wcs : `lsst.afw.geom.SkyWcs`
1281 WCS; used to convert pixel positions to sky coordinates.
1285 results : `lsst.pipe.base.Struct`
1286 A Struct containing:
1288 - coord : `lsst.geom.SpherePoint`
1289 ICRS center of the search region.
1290 - radius : `lsst.geom.Angle`
1291 Radius of the search region.
1292 - bbox : `lsst.geom.Box2D`
1293 Bounding box used to compute the circle.
1296 bbox.grow(self.config.pixelMargin)
1297 coord = wcs.pixelToSky(bbox.getCenter())
1298 radius =
max(coord.separation(wcs.pixelToSky(pp))
for pp
in bbox.getCorners())
1299 return pipeBase.Struct(coord=coord, radius=radius, bbox=bbox)
1302 """Return metadata about the load.
1304 This metadata is used for reloading the catalog (e.g., for
1305 reconstituting a normalised match list.
1309 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
1311 wcs : `lsst.afw.geom.SkyWcs`
1312 WCS; used to convert pixel positions to sky coordinates.
1313 filterName : `str` or `None`, optional
1314 Name of camera filter, or `None` or `""` for the default
1317 Deprecated, only included for api compatibility.
1318 epoch : `astropy.time.Time` or `None`, optional
1319 Epoch to which to correct proper motion and parallax,
1320 or None to not apply such corrections.
1324 metadata : `lsst.daf.base.PropertyList`
1325 Metadata about the load.
1328 return self.
getMetadataCirclegetMetadataCircle(circle.coord, circle.radius, filterName, epoch=epoch)
1331 """Return metadata about the load.
1333 This metadata is used for reloading the catalog (e.g., for
1334 reconstituting a normalised match list.
1338 coord : `lsst.geom.SpherePoint`
1339 ICRS center of the search region.
1340 radius : `lsst.geom.Angle`
1341 Radius of the search region.
1343 Name of camera filter, or `None` or `""` for the default
1346 Deprecated, only included for api compatibility.
1347 epoch : `astropy.time.Time` (optional)
1348 Epoch to which to correct proper motion and parallax, or `None` to
1349 not apply such corrections.
1353 metadata : lsst.daf.base.PropertyList
1354 Metadata about the load
1357 md.add(
'RA', coord.getRa().asDegrees(),
'field center in degrees')
1358 md.add(
'DEC', coord.getDec().asDegrees(),
'field center in degrees')
1359 md.add(
'RADIUS', radius.asDegrees(),
'field radius in degrees, minimum')
1360 md.add(
'SMATCHV', 1,
'SourceMatchVector version number')
1361 filterName =
"UNKNOWN" if filterName
is None else str(filterName)
1362 md.add(
'FILTER', filterName,
'filter name for photometric data')
1363 md.add(
'EPOCH',
"NONE" if epoch
is None else epoch.mjd,
'Epoch (TAI MJD) for catalog')
1367 """Relink an unpersisted match list to sources and reference
1370 A match list is persisted and unpersisted as a catalog of IDs
1371 produced by afw.table.packMatches(), with match metadata
1372 (as returned by the astrometry tasks) in the catalog's metadata
1373 attribute. This method converts such a match catalog into a match
1374 list, with links to source records and reference object records.
1378 matchCat : `lsst.afw.table.BaseCatalog`
1379 Unperisted packed match list.
1380 ``matchCat.table.getMetadata()`` must contain match metadata,
1381 as returned by the astrometry tasks.
1382 sourceCat : `lsst.afw.table.SourceCatalog`
1383 Source catalog. As a side effect, the catalog will be sorted
1388 matchList : `lsst.afw.table.ReferenceMatchVector`
1395 """Relink an unpersisted match list to sources and reference
1398 A match list is persisted and unpersisted as a catalog of IDs
1399 produced by afw.table.packMatches(), with match metadata
1400 (as returned by the astrometry tasks) in the catalog's metadata
1401 attribute. This method converts such a match catalog into a match
1402 list, with links to source records and reference object records.
1407 Reference object loader to use in getting reference objects
1408 matchCat : `lsst.afw.table.BaseCatalog`
1409 Unperisted packed match list.
1410 ``matchCat.table.getMetadata()`` must contain match metadata,
1411 as returned by the astrometry tasks.
1412 sourceCat : `lsst.afw.table.SourceCatalog`
1413 Source catalog. As a side effect, the catalog will be sorted
1418 matchList : `lsst.afw.table.ReferenceMatchVector`
1421 matchmeta = matchCat.table.getMetadata()
1422 version = matchmeta.getInt(
'SMATCHV')
1424 raise ValueError(
'SourceMatchVector version number is %i, not 1.' % version)
1425 filterName = matchmeta.getString(
'FILTER').
strip()
1427 epoch = matchmeta.getDouble(
'EPOCH')
1428 except (LookupError, TypeError):
1430 if 'RADIUS' in matchmeta:
1433 matchmeta.getDouble(
'DEC'), geom.degrees)
1434 rad = matchmeta.getDouble(
'RADIUS')*geom.degrees
1435 refCat = refObjLoader.loadSkyCircle(ctrCoord, rad, filterName, epoch=epoch).refCat
1436 elif "INNER_UPPER_LEFT_RA" in matchmeta:
1442 for place
in (
"UPPER_LEFT",
"UPPER_RIGHT",
"LOWER_LEFT",
"LOWER_RIGHT"):
1444 matchmeta.getDouble(f
"OUTER_{place}_DEC"),
1445 geom.degrees).getVector()
1448 refCat = refObjLoader.loadRegion(outerBox, filterName=filterName, epoch=epoch).refCat
1456 """Apply proper motion correction to a reference catalog.
1458 Adjust position and position error in the ``catalog``
1459 for proper motion to the specified ``epoch``,
1460 modifying the catalog in place.
1464 log : `lsst.log.Log` or `logging.getLogger`
1465 Log object to write to.
1466 catalog : `lsst.afw.table.SimpleCatalog`
1467 Catalog of positions, containing:
1469 - Coordinates, retrieved by the table's coordinate key.
1470 - ``coord_raErr`` : Error in Right Ascension (rad).
1471 - ``coord_decErr`` : Error in Declination (rad).
1472 - ``pm_ra`` : Proper motion in Right Ascension (rad/yr,
1474 - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
1475 - ``pm_dec`` : Proper motion in Declination (rad/yr,
1477 - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional.
1478 - ``epoch`` : Mean epoch of object (an astropy.time.Time)
1479 epoch : `astropy.time.Time`
1480 Epoch to which to correct proper motion.
1482 if "epoch" not in catalog.schema
or "pm_ra" not in catalog.schema
or "pm_dec" not in catalog.schema:
1483 log.warning(
"Proper motion correction not available from catalog")
1485 if not catalog.isContiguous():
1486 raise RuntimeError(
"Catalog must be contiguous")
1487 catEpoch = astropy.time.Time(catalog[
"epoch"], scale=
"tai", format=
"mjd")
1488 log.info(
"Correcting reference catalog for proper motion to %r", epoch)
1490 timeDiffsYears = (epoch.tai - catEpoch).
to(astropy.units.yr).value
1491 coordKey = catalog.table.getCoordKey()
1494 pmRaRad = catalog[
"pm_ra"]
1495 pmDecRad = catalog[
"pm_dec"]
1496 offsetsRaRad = pmRaRad*timeDiffsYears
1497 offsetsDecRad = pmDecRad*timeDiffsYears
1505 offsetBearingsRad = numpy.arctan2(pmDecRad*1e6, pmRaRad*1e6)
1506 offsetAmountsRad = numpy.hypot(offsetsRaRad, offsetsDecRad)
1507 for record, bearingRad, amountRad
in zip(catalog, offsetBearingsRad, offsetAmountsRad):
1508 record.set(coordKey,
1509 record.get(coordKey).offset(bearing=bearingRad*geom.radians,
1510 amount=amountRad*geom.radians))
1512 if "coord_raErr" in catalog.schema:
1513 catalog[
"coord_raErr"] = numpy.hypot(catalog[
"coord_raErr"],
1514 catalog[
"pm_raErr"]*timeDiffsYears)
1515 if "coord_decErr" in catalog.schema:
1516 catalog[
"coord_decErr"] = numpy.hypot(catalog[
"coord_decErr"],
1517 catalog[
"pm_decErr"]*timeDiffsYears)
Defines the fields and offsets for a table.
A mapping between the keys of two Schemas, used to copy data between them.
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Class for storing ordered metadata with comments.
A floating-point coordinate rectangle geometry.
Point in an unspecified spherical coordinate system.
def __call__(self, refCat, catRegion)
def __init__(self, region)
def _trimToBBox(refCat, bbox, wcs)
def joinMatchListWithCatalog(self, matchCat, sourceCat)
def makeMinimalSchema(filterNameList, *addCentroid=False, addIsPhotometric=False, addIsResolved=False, addIsVariable=False, coordErrDim=2, addProperMotion=False, properMotionErrDim=2, addParallax=False)
def getMetadataCircle(self, coord, radius, filterName, photoCalib=None, epoch=None)
def __init__(self, butler=None, *args, **kwargs)
def _calculateCircle(self, bbox, wcs)
def loadPixelBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None)
def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None, centroids=False)
def getMetadataBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None)
def applyProperMotions(self, catalog, epoch)
def loadRegion(self, region, filtFunc=None, filterName=None, epoch=None)
def getMetadataCircle(coord, radius, filterName, photoCalib=None, epoch=None)
def _makeBoxRegion(BBox, wcs, BBoxPadding)
def remapReferenceCatalogSchema(refCat, *filterNameList=None, position=False, photometric=False)
def addFluxAliases(refCat, defaultFilter, filterReferenceMap)
def getMetadataBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None, bboxToSpherePadding=100)
def loadPixelBox(self, bbox, wcs, filterName=None, epoch=None, photoCalib=None, bboxToSpherePadding=100)
def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None)
def joinMatchListWithCatalog(self, matchCat, sourceCat)
def __init__(self, dataIds, refCats, config, log=None)
Angle represents an angle in radians.
Circle is a circular region on the unit sphere that contains its boundary.
ConvexPolygon is a closed convex polygon on the unit sphere.
daf::base::PropertySet * set
std::vector< Match< typename Cat1::Record, typename Cat2::Record > > unpackMatches(BaseCatalog const &matches, Cat1 const &cat1, Cat2 const &cat2)
Reconstruct a MatchVector from a BaseCatalog representation of the matches and a pair of catalogs.
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
Update centroids in a collection of reference objects.
def getRefFluxKeys(schema, filterName=None)
def hasNanojanskyFluxUnits(schema)
def getRefFluxField(schema, filterName=None)
def convertToNanojansky(catalog, log, doConvert=True)
def joinMatchListWithCatalogImpl(refObjLoader, matchCat, sourceCat)
def isOldFluxField(name, units)
def getFormatVersionFromRefCat(refCat)
def applyProperMotionsImpl(log, catalog, epoch)
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
A description of a field in a table.