24__all__ = [
"getRefFluxField",
"getRefFluxKeys",
"LoadReferenceObjectsTask",
"LoadReferenceObjectsConfig",
25 "ReferenceObjectLoader"]
37import lsst.pipe.base
as pipeBase
38from lsst
import sphgeom
40from lsst.utils.timer
import timeMethod
44 """Return True if this name/units combination corresponds to an
45 "old-style" reference catalog flux field.
47 unitsCheck = units != 'nJy'
48 isFlux = name.endswith(
'_flux')
49 isFluxSigma = name.endswith(
'_fluxSigma')
50 isFluxErr = name.endswith(
'_fluxErr')
51 return (isFlux
or isFluxSigma
or isFluxErr)
and unitsCheck
55 """Return True if the units of all flux and fluxErr are correct (nJy).
64 """"Return the format version stored in a reference catalog header.
69 Reference catalog to inspect.
74 Format verison integer. Returns `0` if the catalog has no metadata
75 or the metadata does
not include a
"REFCAT_FORMAT_VERSION" key.
77 md = refCat.getMetadata()
81 return md.getScalar(
"REFCAT_FORMAT_VERSION")
87 """Convert fluxes in a catalog from jansky to nanojansky.
92 The catalog to convert.
94 Log to send messages to.
95 doConvert : `bool`, optional
96 Return a converted catalog,
or just identify the fields that need to be converted?
97 This supports the
"write=False" mode of `bin/convert_to_nJy.py`.
102 The converted catalog,
or None if ``doConvert``
is False.
106 Support
for old units
in reference catalogs will be removed after the
107 release of late calendar year 2019.
108 Use `meas_algorithms/bin/convert_to_nJy.py` to update your reference catalog.
112 mapper = afwTable.SchemaMapper(catalog.schema, shareAliasMap=
False)
113 mapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
116 for field
in catalog.schema:
117 oldName = field.field.getName()
118 oldUnits = field.field.getUnits()
122 if oldName.endswith(
'_fluxSigma'):
123 name = oldName.replace(
'_fluxSigma',
'_fluxErr')
126 newField = afwTable.Field[field.dtype](name, field.field.getDoc(), units)
127 mapper.addMapping(field.getKey(), newField)
128 input_fields.append(field.field)
129 output_fields.append(newField)
131 mapper.addMapping(field.getKey())
133 fluxFieldsStr =
'; '.join(
"(%s, '%s')" % (field.getName(), field.getUnits())
for field
in input_fields)
136 newSchema = mapper.getOutputSchema()
137 output = afwTable.SimpleCatalog(newSchema)
138 output.extend(catalog, mapper=mapper)
139 for field
in output_fields:
140 output[field.getName()] *= 1e9
141 log.info(
"Converted refcat flux fields to nJy (name, units): %s", fluxFieldsStr)
144 log.info(
"Found old-style refcat flux fields (name, units): %s", fluxFieldsStr)
149 """This is a private helper class which filters catalogs by
150 row based on the row being inside the region used to initialize
156 The spatial region which all objects should lie within
162 """This call method on an instance of this class takes in a reference
163 catalog, and the region
from which the catalog was generated.
165 If the catalog region
is entirely contained within the region used to
166 initialize this
class, then all the entries
in the catalog must be
167 within the region
and so the whole catalog
is returned.
169 If the catalog region
is not entirely contained, then the location
for
170 each record
is tested against the region used to initialize the
class.
171 Records which fall inside this region are added to a new catalog,
and
172 this catalog
is then returned.
177 SourceCatalog to be filtered.
179 Region
in which the catalog was created
181 if catRegion.isWithin(self.
regionregion):
185 filteredRefCat = type(refCat)(refCat.table)
186 for record
in refCat:
187 if self.
regionregion.contains(record.getCoord().getVector()):
188 filteredRefCat.append(record)
189 return filteredRefCat
193 pixelMargin = pexConfig.RangeField(
194 doc=
"Padding to add to 4 all edges of the bounding box (pixels)",
199 anyFilterMapsToThis = pexConfig.Field(
200 doc=(
"Always use this reference catalog filter, no matter whether or what filter name is "
201 "supplied to the loader. Effectively a trivial filterMap: map all filter names to this filter."
202 " This can be set for purely-astrometric catalogs (e.g. Gaia DR2) where there is only one "
203 "reasonable choice for every camera filter->refcat mapping, but not for refcats used for "
204 "photometry, which need a filterMap and/or colorterms/transmission corrections."),
209 filterMap = pexConfig.DictField(
210 doc=(
"Mapping of camera filter name: reference catalog filter name; "
211 "each reference filter must exist in the refcat."
212 " Note that this does not perform any bandpass corrections: it is just a lookup."),
217 requireProperMotion = pexConfig.Field(
218 doc=
"Require that the fields needed to correct proper motion "
219 "(epoch, pm_ra and pm_dec) are present?",
227 msg =
"`filterMap` and `anyFilterMapsToThis` are mutually exclusive"
228 raise pexConfig.FieldValidationError(LoadReferenceObjectsConfig.anyFilterMapsToThis,
233 """Base class for reference object loaders, to facilitate gen2/gen3 code
236 ConfigClass = LoadReferenceObjectsConfig
239 """Apply proper motion correction to a reference catalog.
241 Adjust position and position error
in the ``catalog``
242 for proper motion to the specified ``epoch``,
243 modifying the catalog
in place.
248 Catalog of positions, containing at least these fields:
250 - Coordinates, retrieved by the table
's coordinate key.
251 - ``coord_raErr`` : Error in Right Ascension (rad).
252 - ``coord_decErr`` : Error
in Declination (rad).
253 - ``pm_ra`` : Proper motion
in Right Ascension (rad/yr,
255 - ``pm_raErr`` : Error
in ``pm_ra`` (rad/yr), optional.
256 - ``pm_dec`` : Proper motion
in Declination (rad/yr,
258 - ``pm_decErr`` : Error
in ``pm_dec`` (rad/yr), optional.
259 - ``epoch`` : Mean epoch of object (an astropy.time.Time)
260 epoch : `astropy.time.Time`
261 Epoch to which to correct proper motion.
262 If
None, do
not apply PM corrections
or raise if
263 ``config.requireProperMotion``
is True.
268 Raised
if ``config.requireProperMotion``
is set but we cannot
269 apply the proper motion correction
for some reason.
272 if self.config.requireProperMotion:
273 raise RuntimeError(
"requireProperMotion=True but epoch not provided to loader.")
275 self.log.debug(
"No epoch provided: not applying proper motion corrections to refcat.")
279 if (
"pm_ra" in catalog.schema
280 and not isinstance(catalog.schema[
"pm_ra"].asKey(), afwTable.KeyAngle)):
281 if self.config.requireProperMotion:
282 raise RuntimeError(
"requireProperMotion=True but refcat pm_ra field is not an Angle.")
284 self.log.warning(
"Reference catalog pm_ra field is not an Angle; cannot apply proper motion.")
287 if (
"epoch" not in catalog.schema
or "pm_ra" not in catalog.schema):
288 if self.config.requireProperMotion:
289 raise RuntimeError(
"requireProperMotion=True but PM data not available from catalog.")
291 self.log.warning(
"Proper motion correction not available for this reference catalog.")
298 """This class facilitates loading reference catalogs with gen 3 middleware
300 The middleware preflight solver will create a list of datarefs that may
301 possibly overlap a given region. These datarefs are then used to construct
302 and instance of this
class. The
class instance should then be passed into
303 a task which needs reference catalogs. These tasks should then determine
304 the exact region of the sky reference catalogs will be loaded
for,
and
305 call a corresponding method to load the reference objects.
307 def __init__(self, dataIds, refCats, config, log=None):
308 """ Constructs an instance of ReferenceObjectLoader
312 dataIds : iterable of `lsst.daf.butler.DataIds`
313 An iterable object of DataSetRefs which point to reference catalogs
314 in a gen 3 repository.
315 refCats : iterable of `lsst.daf.butler.DeferedDatasetHandle`
316 Handles to load refCats on demand
318 Configuration
for the loader.
319 log : `
lsst.log.Log`, `logging.Logger`
or `
None`, optional
320 Logger object used to write out messages. If `
None` a default
325 self.loglog = log or logging.getLogger(__name__).getChild(
"ReferenceObjectLoader")
329 def _makeBoxRegion(BBox, wcs, BBoxPadding):
342 outerLocalBBox.grow(BBoxPadding)
343 innerLocalBBox.grow(-1*BBoxPadding)
355 innerBoxCorners = innerLocalBBox.getCorners()
356 innerSphCorners = [wcs.pixelToSky(corner).getVector()
for corner
in innerBoxCorners]
359 outerBoxCorners = outerLocalBBox.getCorners()
360 outerSphCorners = [wcs.pixelToSky(corner).getVector()
for corner
in outerBoxCorners]
363 return innerSkyRegion, outerSkyRegion, innerSphCorners, outerSphCorners
365 def loadPixelBox(self, bbox, wcs, filterName, epoch=None, photoCalib=None,
366 bboxToSpherePadding=100):
367 """Load reference objects that are within a pixel-based rectangular
370 This algorithm works by creating a spherical box whose corners
371 correspond to the WCS converted corners of the input bounding box
372 (possibly padded). It then defines a filtering function which looks at
373 the pixel position of the reference objects and accepts only those that
374 lie within the specified bounding box.
376 The spherical box region
and filtering function are passed to the
377 generic loadRegion method which loads
and filters the reference objects
378 from the datastore
and returns a single catalog containing the filtered
379 set of reference objects.
384 Box which bounds a region
in pixel space.
386 Wcs object defining the pixel to sky (
and inverse) transform
for
387 the supplied ``bbox``.
389 Name of camera filter.
390 epoch : `astropy.time.Time`
or `
None`, optional
391 Epoch to which to correct proper motion
and parallax,
or `
None`
392 to
not apply such corrections.
394 Deprecated
and ignored, only included
for api compatibility.
395 bboxToSpherePadding : `int`, optional
396 Padding to account
for translating a set of corners into a
397 spherical (convex) boundary that
is certain to encompase the
398 enitre area covered by the box.
403 Catalog containing reference objects inside the specified bounding
404 box (padded by self.
configconfig.pixelMargin).
409 Raised
if no reference catalogs could be found
for the specified
412 Raised
if the loaded reference catalogs do
not have matching
416 paddedBbox.grow(self.configconfig.pixelMargin)
417 innerSkyRegion, outerSkyRegion, _, _ = self._makeBoxRegion_makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding)
419 def _filterFunction(refCat, region):
430 refCat = preFiltFunc(refCat, region)
436 afwTable.updateRefCentroids(wcs, refCat)
439 if innerSkyRegion.contains(region):
443 filteredRefCat = type(refCat)(refCat.table)
444 centroidKey = afwTable.Point2DKey(refCat.schema[
'centroid'])
445 for record
in refCat:
446 pixCoords = record[centroidKey]
448 filteredRefCat.append(record)
449 return filteredRefCat
450 return self.
loadRegionloadRegion(outerSkyRegion, filterName, filtFunc=_filterFunction, epoch=epoch)
452 def loadRegion(self, region, filterName, filtFunc=None, epoch=None):
453 """Load reference objects within a specified region.
455 This function loads the DataIds used to construct an instance of this
456 class which intersect or are contained within the specified region. The
457 reference catalogs which intersect but are
not fully contained within
458 the input region are further filtered by the specified filter function.
459 This function returns a single source catalog containing all reference
460 objects inside the specified region.
466 should define the spatial region
for which reference objects are to
468 filtFunc : callable
or `
None`, optional
469 This optional parameter should be a callable object that takes a
470 reference catalog
and its corresponding region
as parameters,
471 filters the catalog by some criteria
and returns the filtered
472 reference catalog. If `
None`, an internal filter function
is used
473 which filters according to
if a reference object falls within the
476 Name of camera filter.
477 epoch : `astropy.time.Time`
or `
None`, optional
478 Epoch to which to correct proper motion
and parallax,
or `
None` to
479 not apply such corrections.
484 Catalog containing reference objects which intersect the input region,
485 filtered by the specified filter function.
490 Raised
if no reference catalogs could be found
for the specified
493 Raised
if the loaded reference catalogs do
not have matching
496 regionLat = region.getBoundingBox().getLat()
497 regionLon = region.getBoundingBox().getLon()
498 self.loglog.info("Loading reference objects from %s in region bounded by "
499 "[%.8f, %.8f], [%.8f, %.8f] RA Dec",
501 self.
refCatsrefCats[0].ref.datasetType.name,
502 regionLon.getA().asDegrees(), regionLon.getB().asDegrees(),
503 regionLat.getA().asDegrees(), regionLat.getB().asDegrees())
508 for dataId, refCat
in zip(self.
dataIdsdataIds, self.
refCatsrefCats):
512 intersects = dataId.region.intersects(region)
514 intersects = region.intersects(dataId.region)
517 overlapList.append((dataId, refCat))
519 if len(overlapList) == 0:
520 raise RuntimeError(
"No reference tables could be found for input region")
522 firstCat = overlapList[0][1].get()
523 refCat = filtFunc(firstCat, overlapList[0][0].region)
524 trimmedAmount = len(firstCat) - len(refCat)
527 for dataId, inputRefCat
in overlapList[1:]:
528 tmpCat = inputRefCat.get()
530 if tmpCat.schema != firstCat.schema:
531 raise TypeError(
"Reference catalogs have mismatching schemas")
533 filteredCat = filtFunc(tmpCat, dataId.region)
534 refCat.extend(filteredCat)
535 trimmedAmount += len(tmpCat) - len(filteredCat)
537 self.
loglog.debug(
"Trimmed %d refCat objects lying outside padded region, leaving %d",
538 trimmedAmount, len(refCat))
539 self.
loglog.info(
"Loaded %d reference objects", len(refCat))
542 if not refCat.isContiguous():
543 refCat = refCat.copy(deep=
True)
550 self.
loglog.warning(
"Found version 0 reference catalog with old style units in schema.")
551 self.
loglog.warning(
"run `meas_algorithms/bin/convert_refcat_to_nJy.py` to convert fluxes to nJy.")
552 self.
loglog.warning(
"See RFC-575 for more details.")
561 if not expandedCat.isContiguous():
562 expandedCat = expandedCat.copy(deep=
True)
565 return pipeBase.Struct(refCat=expandedCat, fluxField=fluxField)
568 """Load reference objects that lie within a circular region on the sky.
570 This method constructs a circular region from an input center
and
571 angular radius, loads reference catalogs which are contained
in or
572 intersect the circle,
and filters reference catalogs which intersect
573 down to objects which lie within the defined circle.
578 Point defining the center of the circular region.
580 Defines the angular radius of the circular region.
582 Name of camera filter.
583 epoch : `astropy.time.Time`
or `
None`, optional
584 Epoch to which to correct proper motion
and parallax,
or `
None` to
585 not apply such corrections.
590 Catalog containing reference objects inside the specified search
593 centerVector = ctrCoord.getVector()
596 return self.
loadRegionloadRegion(circularRegion, filterName, epoch=epoch)
599 """Relink an unpersisted match list to sources and reference objects.
601 A match list is persisted
and unpersisted
as a catalog of IDs
602 produced by afw.table.packMatches(),
with match metadata
603 (
as returned by the astrometry tasks)
in the catalog
's metadata
604 attribute. This method converts such a match catalog into a match
605 list, with links to source records
and reference object records.
610 Unpersisted packed match list.
611 ``matchCat.table.getMetadata()`` must contain match metadata,
612 as returned by the astrometry tasks.
614 Source catalog. As a side effect, the catalog will be sorted
625 bboxToSpherePadding=100):
626 """Return metadata about the load
628 This metadata is used
for reloading the catalog (e.g.,
for
629 reconstituting a normalised match list.)
634 Bounding box
for the pixels.
636 The WCS object associated
with ``bbox``.
638 Name of the camera filter.
640 Deprecated, only included
for api compatibility.
641 epoch : `astropy.time.Time`
or `
None`, optional
642 Epoch to which to correct proper motion
and parallax,
or `
None` to
643 not apply such corrections.
644 bboxToSpherePadding : `int`, optional
645 Padding to account
for translating a set of corners into a
646 spherical (convex) boundary that
is certain to encompase the
647 enitre area covered by the box.
652 The metadata detailing the search parameters used
for this
656 paddedBbox.grow(self.configconfig.pixelMargin)
657 _, _, innerCorners, outerCorners = self._makeBoxRegion_makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding)
659 for box, corners
in zip((
"INNER",
"OUTER"), (innerCorners, outerCorners)):
660 for (name, corner)
in zip((
"UPPER_LEFT",
"UPPER_RIGHT",
"LOWER_LEFT",
"LOWER_RIGHT"),
662 md.add(f
"{box}_{name}_RA",
geom.SpherePoint(corner).getRa().asDegrees(), f
"{box}_corner")
663 md.add(f
"{box}_{name}_DEC",
geom.SpherePoint(corner).getDec().asDegrees(), f
"{box}_corner")
664 md.add(
"SMATCHV", 1,
'SourceMatchVector version number')
665 md.add(
'FILTER', filterName,
'filter name for photometric data')
666 md.add(
'EPOCH',
"NONE" if epoch
is None else epoch.mjd,
'Epoch (TAI MJD) for catalog')
671 """Return metadata about the load.
673 This metadata is used
for reloading the catalog (e.g.
for
674 reconstituting a normalized match list.)
679 ICRS center of the search region.
681 Radius of the search region.
683 Name of the camera filter.
685 Deprecated, only included
for api compatibility.
686 epoch : `astropy.time.Time`
or `
None`, optional
687 Epoch to which to correct proper motion
and parallax,
or `
None` to
688 not apply such corrections.
695 md.add('RA', coord.getRa().asDegrees(),
'field center in degrees')
696 md.add(
'DEC', coord.getDec().asDegrees(),
'field center in degrees')
697 md.add(
'RADIUS', radius.asDegrees(),
'field radius in degrees, minimum')
698 md.add(
'SMATCHV', 1,
'SourceMatchVector version number')
699 md.add(
'FILTER', filterName,
'filter name for photometric data')
700 md.add(
'EPOCH',
"NONE" if epoch
is None else epoch.mjd,
'Epoch (TAI MJD) for catalog')
705 """Add flux columns and aliases for camera to reference mapping.
707 Creates a new catalog containing the information of the input refCat
708 as well
as added flux columns
and aliases between camera
and reference
714 Catalog of reference objects
715 filterReferenceMap : `dict` of `str`
716 Dictionary
with keys corresponding to a filter name
and values
717 which correspond to the name of the reference filter.
722 Reference catalog
with columns added to track reference filters.
727 If the specified reference filter name
is not specifed
as a
728 key
in the reference filter map.
730 refCat = ReferenceObjectLoader.remapReferenceCatalogSchema(refCat,
731 filterNameList=filterReferenceMap.keys())
732 aliasMap = refCat.schema.getAliasMap()
733 if filterReferenceMap
is None:
734 filterReferenceMap = {}
735 for filterName, refFilterName
in filterReferenceMap.items():
737 camFluxName = filterName +
"_camFlux"
738 refFluxName = refFilterName +
"_flux"
739 if refFluxName
not in refCat.schema:
740 raise RuntimeError(
"Unknown reference filter %s" % (refFluxName,))
741 aliasMap.set(camFluxName, refFluxName)
743 refFluxErrName = refFluxName +
"Err"
744 camFluxErrName = camFluxName +
"Err"
745 aliasMap.set(camFluxErrName, refFluxErrName)
751 """This function takes in a reference catalog and creates a new catalog with additional
752 columns defined the remaining function arguments.
757 Reference catalog to map to new catalog
762 Deep copy of input reference catalog with additional columns added
764 mapper = afwTable.SchemaMapper(refCat.schema, True)
765 mapper.addMinimalSchema(refCat.schema,
True)
766 mapper.editOutputSchema().disconnectAliases()
768 for filterName
in filterNameList:
769 mapper.editOutputSchema().addField(f
"{filterName}_flux",
771 doc=f
"flux in filter {filterName}",
774 mapper.editOutputSchema().addField(f
"{filterName}_fluxErr",
776 doc=f
"flux uncertanty in filter {filterName}",
781 mapper.editOutputSchema().addField(
"centroid_x", type=float, doReplace=
True)
782 mapper.editOutputSchema().addField(
"centroid_y", type=float, doReplace=
True)
783 mapper.editOutputSchema().addField(
"hasCentroid", type=
"Flag", doReplace=
True)
784 mapper.editOutputSchema().getAliasMap().set(
"slot_Centroid",
"centroid")
787 mapper.editOutputSchema().addField(
"photometric",
789 doc=
"set if the object can be used for photometric"
792 mapper.editOutputSchema().addField(
"resolved",
794 doc=
"set if the object is spatially resolved"
796 mapper.editOutputSchema().addField(
"variable",
798 doc=
"set if the object has variable brightness"
801 expandedCat = afwTable.SimpleCatalog(mapper.getOutputSchema())
802 expandedCat.setMetadata(refCat.getMetadata())
803 expandedCat.extend(refCat, mapper=mapper)
809 """Get the name of a flux field from a schema.
811 return the alias of
"anyFilterMapsToThis",
if present
813 return "*filterName*_camFlux" if present
814 else return "*filterName*_flux" if present (camera filter name
815 matches reference filter name)
816 else throw RuntimeError
821 Reference catalog schema.
823 Name of camera filter.
827 fluxFieldName : `str`
833 If an appropriate field
is not found.
835 if not isinstance(schema, afwTable.Schema):
836 raise RuntimeError(
"schema=%s is not a schema" % (schema,))
838 return schema.getAliasMap().get(
"anyFilterMapsToThis")
842 fluxFieldList = [filterName +
"_camFlux", filterName +
"_flux"]
843 for fluxField
in fluxFieldList:
844 if fluxField
in schema:
847 raise RuntimeError(
"Could not find flux field(s) %s" % (
", ".join(fluxFieldList)))
851 """Return keys for flux and flux error.
856 Reference catalog schema.
858 Name of camera filter.
866 - flux error key, if present,
else None
871 If flux field
not found.
874 fluxErrField = fluxField + "Err"
875 fluxKey = schema[fluxField].asKey()
877 fluxErrKey = schema[fluxErrField].asKey()
880 return (fluxKey, fluxErrKey)
884 """Abstract base class to load objects from reference catalogs.
886 _DefaultName = "LoadReferenceObjects"
889 """Construct a LoadReferenceObjectsTask
893 butler : `lsst.daf.persistence.Butler`
894 Data butler, for access reference catalogs.
896 pipeBase.Task.__init__(self, *args, **kwargs)
900 def loadPixelBox(self, bbox, wcs, filterName, photoCalib=None, epoch=None):
901 """Load reference objects that overlap a rectangular pixel region.
906 Bounding box
for pixels.
908 WCS; used to convert pixel positions to sky coordinates
911 Name of filter. This can be used
for flux limit comparisons.
913 Deprecated, only included
for api compatibility.
914 epoch : `astropy.time.Time`
or `
None`, optional
915 Epoch to which to correct proper motion
and parallax,
or `
None` to
916 not apply such corrections.
920 results : `lsst.pipe.base.Struct`
921 A Struct containing the following fields:
922 refCat : `lsst.afw.catalog.SimpleCatalog`
923 A catalog of reference objects
with the standard
924 schema,
as documented
in the main doc string
for
925 `LoadReferenceObjects`.
926 The catalog
is guaranteed to be contiguous.
928 Name of flux field
for specified `filterName`.
932 The search algorithm works by searching
in a region
in sky
933 coordinates whose center
is the center of the bbox
and radius
934 is large enough to just include all 4 corners of the bbox.
935 Stars that lie outside the bbox are then trimmed
from the list.
940 self.log.info(
"Loading reference objects from %s using center %s and radius %s deg",
941 self.config.ref_dataset_name, circle.coord, circle.radius.asDegrees())
942 loadRes = self.
loadSkyCircleloadSkyCircle(circle.coord, circle.radius, filterName, epoch=epoch,
944 refCat = loadRes.refCat
945 numFound = len(refCat)
948 refCat = self.
_trimToBBox_trimToBBox(refCat=refCat, bbox=circle.bbox, wcs=wcs)
949 numTrimmed = numFound - len(refCat)
950 self.log.debug(
"trimmed %d out-of-bbox objects, leaving %d", numTrimmed, len(refCat))
951 self.log.info(
"Loaded %d reference objects", len(refCat))
954 if not refCat.isContiguous():
955 loadRes.refCat = refCat.copy(deep=
True)
960 def loadSkyCircle(self, ctrCoord, radius, filterName, epoch=None, centroids=False):
961 """Load reference objects that overlap a circular sky region.
966 ICRS center of search region.
968 Radius of search region.
970 Name of filter. This can be used for flux limit comparisons.
971 epoch : `astropy.time.Time`
or `
None`, optional
972 Epoch to which to correct proper motion
and parallax,
or `
None` to
973 not apply such corrections.
974 centroids : `bool`, optional
975 Add centroid fields to the loaded Schema. ``loadPixelBox`` expects
976 these fields to exist.
980 results : `lsst.pipe.base.Struct`
981 A Struct containing the following fields:
982 refCat : `lsst.afw.catalog.SimpleCatalog`
983 A catalog of reference objects
with the standard
984 schema,
as documented
in the main doc string
for
985 `LoadReferenceObjects`.
986 The catalog
is guaranteed to be contiguous.
988 Name of flux field
for specified `filterName`.
992 Note that subclasses are responsible
for performing the proper motion
993 correction, since this
is the lowest-level interface
for retrieving
999 def _trimToBBox(refCat, bbox, wcs):
1000 """Remove objects outside a given pixel bounding box and set
1001 centroid and hasCentroid fields.
1006 A catalog of objects. The schema must include fields
1007 "coord",
"centroid" and "hasCentroid".
1008 The
"coord" field
is read.
1009 The
"centroid" and "hasCentroid" fields are set.
1013 WCS; used to convert sky coordinates to pixel positions.
1018 Reference objects
in the bbox,
with centroid
and
1019 hasCentroid fields set.
1021 afwTable.updateRefCentroids(wcs, refCat)
1022 centroidKey = afwTable.Point2DKey(refCat.schema["centroid"])
1023 retStarCat = type(refCat)(refCat.table)
1025 point = star.get(centroidKey)
1026 if bbox.contains(point):
1027 retStarCat.append(star)
1030 def _addFluxAliases(self, schema):
1031 """Add aliases for camera filter fluxes to the schema.
1033 For each camFilter: refFilter in self.config.filterMap adds these aliases:
1034 <camFilter>_camFlux: <refFilter>_flux
1035 <camFilter>_camFluxErr: <refFilter>_fluxErr,
if the latter exists
1040 Schema
for reference catalog.
1045 If any reference flux field
is missing
from the schema.
1047 aliasMap = schema.getAliasMap()
1049 if self.config.anyFilterMapsToThis
is not None:
1050 refFluxName = self.config.anyFilterMapsToThis +
"_flux"
1051 if refFluxName
not in schema:
1052 msg = f
"Unknown reference filter for anyFilterMapsToThis='{refFluxName}'"
1053 raise RuntimeError(msg)
1054 aliasMap.set(
"anyFilterMapsToThis", refFluxName)
1057 def addAliasesForOneFilter(filterName, refFilterName):
1058 """Add aliases for a single filter
1062 filterName : `str` (optional)
1063 Camera filter name. The resulting alias name is
1064 <filterName>_camFlux
1065 refFilterName : `str`
1066 Reference catalog filter name; the field
1067 <refFilterName>_flux must exist.
1069 camFluxName = filterName + "_camFlux"
1070 refFluxName = refFilterName +
"_flux"
1071 if refFluxName
not in schema:
1072 raise RuntimeError(
"Unknown reference filter %s" % (refFluxName,))
1073 aliasMap.set(camFluxName, refFluxName)
1074 refFluxErrName = refFluxName +
"Err"
1075 if refFluxErrName
in schema:
1076 camFluxErrName = camFluxName +
"Err"
1077 aliasMap.set(camFluxErrName, refFluxErrName)
1079 for filterName, refFilterName
in self.config.filterMap.items():
1080 addAliasesForOneFilter(filterName, refFilterName)
1084 addIsPhotometric=False, addIsResolved=False,
1085 addIsVariable=False, coordErrDim=2,
1086 addProperMotion=False, properMotionErrDim=2,
1088 """Make a standard schema for reference object catalogs.
1092 filterNameList : `list` of `str`
1093 List of filter names. Used to create <filterName>_flux fields.
1094 addIsPhotometric : `bool`
1095 If True then add field
"photometric".
1096 addIsResolved : `bool`
1097 If
True then add field
"resolved".
1098 addIsVariable : `bool`
1099 If
True then add field
"variable".
1101 Number of coord error fields; must be one of 0, 2, 3:
1103 - If 2
or 3: add fields
"coord_raErr" and "coord_decErr".
1104 - If 3: also add field
"coord_radecErr".
1105 addProperMotion : `bool`
1106 If
True add fields
"epoch",
"pm_ra",
"pm_dec" and "pm_flag".
1107 properMotionErrDim : `int`
1108 Number of proper motion error fields; must be one of 0, 2, 3;
1109 ignored
if addProperMotion false:
1110 - If 2
or 3: add fields
"pm_raErr" and "pm_decErr".
1111 - If 3: also add field
"pm_radecErr".
1112 addParallax : `bool`
1113 If
True add fields
"epoch",
"parallax",
"parallaxErr"
1114 and "parallax_flag".
1119 Schema
for reference catalog, an
1124 Reference catalogs support additional covariances, such
as
1125 covariance between RA
and proper motion
in declination,
1126 that are
not supported by this method, but can be added after
1127 calling this method.
1129 schema = afwTable.SimpleTable.makeMinimalSchema()
1131 afwTable.Point2DKey.addFields(
1134 "centroid on an exposure, if relevant",
1138 field=
"hasCentroid",
1140 doc=
"is position known?",
1142 for filterName
in filterNameList:
1144 field=
"%s_flux" % (filterName,),
1146 doc=
"flux in filter %s" % (filterName,),
1149 for filterName
in filterNameList:
1151 field=
"%s_fluxErr" % (filterName,),
1153 doc=
"flux uncertainty in filter %s" % (filterName,),
1156 if addIsPhotometric:
1158 field=
"photometric",
1160 doc=
"set if the object can be used for photometric calibration",
1166 doc=
"set if the object is spatially resolved",
1172 doc=
"set if the object has variable brightness",
1174 if coordErrDim
not in (0, 2, 3):
1175 raise ValueError(
"coordErrDim={}; must be (0, 2, 3)".format(coordErrDim))
1177 afwTable.CovarianceMatrix2fKey.addFields(
1180 names=[
"ra",
"dec"],
1181 units=[
"rad",
"rad"],
1182 diagonalOnly=(coordErrDim == 2),
1185 if addProperMotion
or addParallax:
1189 doc=
"date of observation (TAI, MJD)",
1197 doc=
"proper motion in the right ascension direction = dra/dt * cos(dec)",
1203 doc=
"proper motion in the declination direction",
1206 if properMotionErrDim
not in (0, 2, 3):
1207 raise ValueError(
"properMotionErrDim={}; must be (0, 2, 3)".format(properMotionErrDim))
1208 if properMotionErrDim > 0:
1209 afwTable.CovarianceMatrix2fKey.addFields(
1212 names=[
"ra",
"dec"],
1213 units=[
"rad/year",
"rad/year"],
1214 diagonalOnly=(properMotionErrDim == 2),
1219 doc=
"Set if proper motion or proper motion error is bad",
1230 field=
"parallaxErr",
1232 doc=
"uncertainty in parallax",
1236 field=
"parallax_flag",
1238 doc=
"Set if parallax or parallax error is bad",
1242 def _calculateCircle(self, bbox, wcs):
1243 """Compute on-sky center and radius of search region.
1250 WCS; used to convert pixel positions to sky coordinates.
1254 results : `lsst.pipe.base.Struct`
1255 A Struct containing:
1258 ICRS center of the search region.
1260 Radius of the search region.
1262 Bounding box used to compute the circle.
1265 bbox.grow(self.config.pixelMargin)
1266 coord = wcs.pixelToSky(bbox.getCenter())
1267 radius = max(coord.separation(wcs.pixelToSky(pp))
for pp
in bbox.getCorners())
1268 return pipeBase.Struct(coord=coord, radius=radius, bbox=bbox)
1271 """Return metadata about the load.
1273 This metadata is used
for reloading the catalog (e.g.,
for
1274 reconstituting a normalised match list.
1281 WCS; used to convert pixel positions to sky coordinates.
1283 Name of camera filter.
1285 Deprecated, only included
for api compatibility.
1286 epoch : `astropy.time.Time`
or `
None`, optional
1287 Epoch to which to correct proper motion
and parallax,
1288 or None to
not apply such corrections.
1293 Metadata about the load.
1296 return self.
getMetadataCirclegetMetadataCircle(circle.coord, circle.radius, filterName, epoch=epoch)
1299 """Return metadata about the load.
1301 This metadata is used
for reloading the catalog (e.g.,
for
1302 reconstituting a normalised match list.
1307 ICRS center of the search region.
1309 Radius of the search region.
1311 Name of camera filter.
1313 Deprecated, only included
for api compatibility.
1314 epoch : `astropy.time.Time` (optional)
1315 Epoch to which to correct proper motion
and parallax,
or `
None` to
1316 not apply such corrections.
1321 Metadata about the load
1324 md.add('RA', coord.getRa().asDegrees(),
'field center in degrees')
1325 md.add(
'DEC', coord.getDec().asDegrees(),
'field center in degrees')
1326 md.add(
'RADIUS', radius.asDegrees(),
'field radius in degrees, minimum')
1327 md.add(
'SMATCHV', 1,
'SourceMatchVector version number')
1328 md.add(
'FILTER', filterName,
'filter name for photometric data')
1329 md.add(
'EPOCH',
"NONE" if epoch
is None else epoch.mjd,
'Epoch (TAI MJD) for catalog')
1333 """Relink an unpersisted match list to sources and reference
1336 A match list is persisted
and unpersisted
as a catalog of IDs
1337 produced by afw.table.packMatches(),
with match metadata
1338 (
as returned by the astrometry tasks)
in the catalog
's metadata
1339 attribute. This method converts such a match catalog into a match
1340 list, with links to source records
and reference object records.
1345 Unperisted packed match list.
1346 ``matchCat.table.getMetadata()`` must contain match metadata,
1347 as returned by the astrometry tasks.
1349 Source catalog. As a side effect, the catalog will be sorted
1361 """Relink an unpersisted match list to sources and reference
1364 A match list is persisted
and unpersisted
as a catalog of IDs
1365 produced by afw.table.packMatches(),
with match metadata
1366 (
as returned by the astrometry tasks)
in the catalog
's metadata
1367 attribute. This method converts such a match catalog into a match
1368 list, with links to source records
and reference object records.
1373 Reference object loader to use
in getting reference objects
1375 Unperisted packed match list.
1376 ``matchCat.table.getMetadata()`` must contain match metadata,
1377 as returned by the astrometry tasks.
1379 Source catalog. As a side effect, the catalog will be sorted
1387 matchmeta = matchCat.table.getMetadata()
1388 version = matchmeta.getInt('SMATCHV')
1390 raise ValueError(
'SourceMatchVector version number is %i, not 1.' % version)
1391 filterName = matchmeta.getString(
'FILTER').strip()
1393 epoch = matchmeta.getDouble(
'EPOCH')
1394 except (LookupError, TypeError):
1396 if 'RADIUS' in matchmeta:
1399 matchmeta.getDouble(
'DEC'), geom.degrees)
1400 rad = matchmeta.getDouble(
'RADIUS')*geom.degrees
1401 refCat = refObjLoader.loadSkyCircle(ctrCoord, rad, filterName, epoch=epoch).refCat
1402 elif "INNER_UPPER_LEFT_RA" in matchmeta:
1408 for place
in (
"UPPER_LEFT",
"UPPER_RIGHT",
"LOWER_LEFT",
"LOWER_RIGHT"):
1410 matchmeta.getDouble(f
"OUTER_{place}_DEC"),
1411 geom.degrees).getVector()
1414 refCat = refObjLoader.loadRegion(outerBox, filterName, epoch=epoch).refCat
1418 return afwTable.unpackMatches(matchCat, refCat, sourceCat)
1422 """Apply proper motion correction to a reference catalog.
1424 Adjust position and position error
in the ``catalog``
1425 for proper motion to the specified ``epoch``,
1426 modifying the catalog
in place.
1431 Log object to write to.
1433 Catalog of positions, containing:
1435 - Coordinates, retrieved by the table
's coordinate key.
1436 - ``coord_raErr`` : Error in Right Ascension (rad).
1437 - ``coord_decErr`` : Error
in Declination (rad).
1438 - ``pm_ra`` : Proper motion
in Right Ascension (rad/yr,
1440 - ``pm_raErr`` : Error
in ``pm_ra`` (rad/yr), optional.
1441 - ``pm_dec`` : Proper motion
in Declination (rad/yr,
1443 - ``pm_decErr`` : Error
in ``pm_dec`` (rad/yr), optional.
1444 - ``epoch`` : Mean epoch of object (an astropy.time.Time)
1445 epoch : `astropy.time.Time`
1446 Epoch to which to correct proper motion.
1448 if "epoch" not in catalog.schema
or "pm_ra" not in catalog.schema
or "pm_dec" not in catalog.schema:
1449 log.warning(
"Proper motion correction not available from catalog")
1451 if not catalog.isContiguous():
1452 raise RuntimeError(
"Catalog must be contiguous")
1453 catEpoch = astropy.time.Time(catalog[
"epoch"], scale=
"tai", format=
"mjd")
1454 log.info(
"Correcting reference catalog for proper motion to %r", epoch)
1456 timeDiffsYears = (epoch.tai - catEpoch).to(astropy.units.yr).value
1457 coordKey = catalog.table.getCoordKey()
1460 pmRaRad = catalog[
"pm_ra"]
1461 pmDecRad = catalog[
"pm_dec"]
1462 offsetsRaRad = pmRaRad*timeDiffsYears
1463 offsetsDecRad = pmDecRad*timeDiffsYears
1471 offsetBearingsRad = numpy.arctan2(pmDecRad*1e6, pmRaRad*1e6)
1472 offsetAmountsRad = numpy.hypot(offsetsRaRad, offsetsDecRad)
1473 for record, bearingRad, amountRad
in zip(catalog, offsetBearingsRad, offsetAmountsRad):
1474 record.set(coordKey,
1475 record.get(coordKey).offset(bearing=bearingRad*geom.radians,
1476 amount=amountRad*geom.radians))
1478 if "coord_raErr" in catalog.schema:
1479 catalog[
"coord_raErr"] = numpy.hypot(catalog[
"coord_raErr"],
1480 catalog[
"pm_raErr"]*timeDiffsYears)
1481 if "coord_decErr" in catalog.schema:
1482 catalog[
"coord_decErr"] = numpy.hypot(catalog[
"coord_decErr"],
1483 catalog[
"pm_decErr"]*timeDiffsYears)
def __call__(self, refCat, catRegion)
def __init__(self, region)
def _trimToBBox(refCat, bbox, wcs)
def joinMatchListWithCatalog(self, matchCat, sourceCat)
def loadSkyCircle(self, ctrCoord, radius, filterName, epoch=None, centroids=False)
def getMetadataBox(self, bbox, wcs, filterName, photoCalib=None, epoch=None)
def loadPixelBox(self, bbox, wcs, filterName, photoCalib=None, epoch=None)
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 applyProperMotions(self, catalog, epoch)
def getMetadataCircle(coord, radius, filterName, photoCalib=None, epoch=None)
def _makeBoxRegion(BBox, wcs, BBoxPadding)
def remapReferenceCatalogSchema(refCat, *filterNameList=None, position=False, photometric=False)
def loadSkyCircle(self, ctrCoord, radius, filterName, epoch=None)
def loadRegion(self, region, filterName, filtFunc=None, epoch=None)
def loadPixelBox(self, bbox, wcs, filterName, epoch=None, photoCalib=None, bboxToSpherePadding=100)
def joinMatchListWithCatalog(self, matchCat, sourceCat)
def getMetadataBox(self, bbox, wcs, filterName, photoCalib=None, epoch=None, bboxToSpherePadding=100)
def __init__(self, dataIds, refCats, config, log=None)
def addFluxAliases(refCat, filterReferenceMap)
def hasNanojanskyFluxUnits(schema)
def getRefFluxKeys(schema, filterName)
def convertToNanojansky(catalog, log, doConvert=True)
def joinMatchListWithCatalogImpl(refObjLoader, matchCat, sourceCat)
def isOldFluxField(name, units)
def getRefFluxField(schema, filterName)
def getFormatVersionFromRefCat(refCat)
def applyProperMotionsImpl(log, catalog, epoch)