lsst.meas.algorithms gf36ae6ace1+7de1d741e2
loadReferenceObjects.py
Go to the documentation of this file.
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
29
30import astropy.time
31import astropy.units
32import numpy
33
34import lsst.geom as geom
35import lsst.afw.table as afwTable
36import lsst.pex.config as pexConfig
37import lsst.pipe.base as pipeBase
38from lsst import sphgeom
39from lsst.daf.base import PropertyList
40from lsst.utils.timer import timeMethod
41
42
43def isOldFluxField(name, units):
44 """Return True if this name/units combination corresponds to an
45 "old-style" reference catalog flux field.
46 """
47 unitsCheck = units != 'nJy' # (units == 'Jy' or units == '' or units == '?')
48 isFlux = name.endswith('_flux')
49 isFluxSigma = name.endswith('_fluxSigma')
50 isFluxErr = name.endswith('_fluxErr')
51 return (isFlux or isFluxSigma or isFluxErr) and unitsCheck
52
53
55 """Return True if the units of all flux and fluxErr are correct (nJy).
56 """
57 for field in schema:
58 if isOldFluxField(field.field.getName(), field.field.getUnits()):
59 return False
60 return True
61
62
64 """"Return the format version stored in a reference catalog header.
65
66 Parameters
67 ----------
69 Reference catalog to inspect.
70
71 Returns
72 -------
73 version : `int`
74 Format verison integer. Returns `0` if the catalog has no metadata
75 or the metadata does not include a "REFCAT_FORMAT_VERSION" key.
76 """
77 md = refCat.getMetadata()
78 if md is None:
79 return 0
80 try:
81 return md.getScalar("REFCAT_FORMAT_VERSION")
82 except KeyError:
83 return 0
84
85
86def convertToNanojansky(catalog, log, doConvert=True):
87 """Convert fluxes in a catalog from jansky to nanojansky.
88
89 Parameters
90 ----------
92 The catalog to convert.
93 log : `lsst.log.Log` or `logging.Logger`
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`.
98
99 Returns
100 -------
101 catalog : `lsst.afw.table.SimpleCatalog` or None
102 The converted catalog, or None if ``doConvert`` is False.
103
104 Notes
105 -----
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.
109 """
110 # Do not share the AliasMap: for refcats, that gets created when the
111 # catalog is read from disk and should not be propagated.
112 mapper = afwTable.SchemaMapper(catalog.schema, shareAliasMap=False)
113 mapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
114 input_fields = []
115 output_fields = []
116 for field in catalog.schema:
117 oldName = field.field.getName()
118 oldUnits = field.field.getUnits()
119 if isOldFluxField(oldName, oldUnits):
120 units = 'nJy'
121 # remap Sigma flux fields to Err, so we can drop the alias
122 if oldName.endswith('_fluxSigma'):
123 name = oldName.replace('_fluxSigma', '_fluxErr')
124 else:
125 name = oldName
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)
130 else:
131 mapper.addMapping(field.getKey())
132
133 fluxFieldsStr = '; '.join("(%s, '%s')" % (field.getName(), field.getUnits()) for field in input_fields)
134
135 if doConvert:
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)
142 return output
143 else:
144 log.info("Found old-style refcat flux fields (name, units): %s", fluxFieldsStr)
145 return None
146
147
149 """This is a private helper class which filters catalogs by
150 row based on the row being inside the region used to initialize
151 the class.
152
153 Parameters
154 ----------
155 region : `lsst.sphgeom.Region`
156 The spatial region which all objects should lie within
157 """
158 def __init__(self, region):
159 self.regionregion = region
160
161 def __call__(self, refCat, catRegion):
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.
164
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.
168
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.
173
174 Parameters
175 ---------
177 SourceCatalog to be filtered.
178 catRegion : `lsst.sphgeom.Region`
179 Region in which the catalog was created
180 """
181 if catRegion.isWithin(self.regionregion):
182 # no filtering needed, region completely contains refcat
183 return refCat
184
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
190
191
192class LoadReferenceObjectsConfig(pexConfig.Config):
193 pixelMargin = pexConfig.RangeField(
194 doc="Padding to add to 4 all edges of the bounding box (pixels)",
195 dtype=int,
196 default=250,
197 min=0,
198 )
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."),
205 dtype=str,
206 default=None,
207 optional=True
208 )
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."),
213 keytype=str,
214 itemtype=str,
215 default={},
216 )
217 requireProperMotion = pexConfig.Field(
218 doc="Require that the fields needed to correct proper motion "
219 "(epoch, pm_ra and pm_dec) are present?",
220 dtype=bool,
221 default=False,
222 )
223
224 def validate(self):
225 super().validate()
226 if self.filterMapfilterMap != {} and self.anyFilterMapsToThisanyFilterMapsToThis is not None:
227 msg = "`filterMap` and `anyFilterMapsToThis` are mutually exclusive"
228 raise pexConfig.FieldValidationError(LoadReferenceObjectsConfig.anyFilterMapsToThis,
229 self, msg)
230
231
233 """Base class for reference object loaders, to facilitate gen2/gen3 code
234 sharing.
235
236 Parameters
237 ----------
238 config : `lsst.pex.config.Config`
239 Configuration for the loader.
240 """
241 ConfigClass = LoadReferenceObjectsConfig
242
243 def __init__(self, config=None, *args, **kwargs):
244 self.configconfig = config
245
246 def applyProperMotions(self, catalog, epoch):
247 """Apply proper motion correction to a reference catalog.
248
249 Adjust position and position error in the ``catalog``
250 for proper motion to the specified ``epoch``,
251 modifying the catalog in place.
252
253 Parameters
254 ----------
256 Catalog of positions, containing at least these fields:
257
258 - Coordinates, retrieved by the table's coordinate key.
259 - ``coord_raErr`` : Error in Right Ascension (rad).
260 - ``coord_decErr`` : Error in Declination (rad).
261 - ``pm_ra`` : Proper motion in Right Ascension (rad/yr,
262 East positive)
263 - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
264 - ``pm_dec`` : Proper motion in Declination (rad/yr,
265 North positive)
266 - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional.
267 - ``epoch`` : Mean epoch of object (an astropy.time.Time)
268 epoch : `astropy.time.Time`
269 Epoch to which to correct proper motion.
270 If None, do not apply PM corrections or raise if
271 ``config.requireProperMotion`` is True.
272
273 Raises
274 ------
275 RuntimeError
276 Raised if ``config.requireProperMotion`` is set but we cannot
277 apply the proper motion correction for some reason.
278 """
279 if epoch is None:
280 if self.configconfig.requireProperMotion:
281 raise RuntimeError("requireProperMotion=True but epoch not provided to loader.")
282 else:
283 self.log.debug("No epoch provided: not applying proper motion corrections to refcat.")
284 return
285
286 # Warn/raise for a catalog in an incorrect format, if epoch was specified.
287 if ("pm_ra" in catalog.schema
288 and not isinstance(catalog.schema["pm_ra"].asKey(), afwTable.KeyAngle)):
289 if self.configconfig.requireProperMotion:
290 raise RuntimeError("requireProperMotion=True but refcat pm_ra field is not an Angle.")
291 else:
292 self.log.warning("Reference catalog pm_ra field is not an Angle; cannot apply proper motion.")
293 return
294
295 if ("epoch" not in catalog.schema or "pm_ra" not in catalog.schema):
296 if self.configconfig.requireProperMotion:
297 raise RuntimeError("requireProperMotion=True but PM data not available from catalog.")
298 else:
299 self.log.warning("Proper motion correction not available for this reference catalog.")
300 return
301
302 applyProperMotionsImpl(self.log, catalog, epoch)
303
304 @staticmethod
305 def makeMinimalSchema(filterNameList, *, addCentroid=False,
306 addIsPhotometric=False, addIsResolved=False,
307 addIsVariable=False, coordErrDim=2,
308 addProperMotion=False, properMotionErrDim=2,
309 addParallax=False):
310 """Make a standard schema for reference object catalogs.
311
312 Parameters
313 ----------
314 filterNameList : `list` of `str`
315 List of filter names. Used to create <filterName>_flux fields.
316 addIsPhotometric : `bool`
317 If True then add field "photometric".
318 addIsResolved : `bool`
319 If True then add field "resolved".
320 addIsVariable : `bool`
321 If True then add field "variable".
322 coordErrDim : `int`
323 Number of coord error fields; must be one of 0, 2, 3:
324
325 - If 2 or 3: add fields "coord_raErr" and "coord_decErr".
326 - If 3: also add field "coord_radecErr".
327 addProperMotion : `bool`
328 If True add fields "epoch", "pm_ra", "pm_dec" and "pm_flag".
329 properMotionErrDim : `int`
330 Number of proper motion error fields; must be one of 0, 2, 3;
331 ignored if addProperMotion false:
332 - If 2 or 3: add fields "pm_raErr" and "pm_decErr".
333 - If 3: also add field "pm_radecErr".
334 addParallax : `bool`
335 If True add fields "epoch", "parallax", "parallaxErr"
336 and "parallax_flag".
337
338 Returns
339 -------
340 schema : `lsst.afw.table.Schema`
341 Schema for reference catalog, an
343
344 Notes
345 -----
346 Reference catalogs support additional covariances, such as
347 covariance between RA and proper motion in declination,
348 that are not supported by this method, but can be added after
349 calling this method.
350 """
351 schema = afwTable.SimpleTable.makeMinimalSchema()
352 if addCentroid:
353 afwTable.Point2DKey.addFields(
354 schema,
355 "centroid",
356 "centroid on an exposure, if relevant",
357 "pixel",
358 )
359 schema.addField(
360 field="hasCentroid",
361 type="Flag",
362 doc="is position known?",
363 )
364 for filterName in filterNameList:
365 schema.addField(
366 field="%s_flux" % (filterName,),
367 type=numpy.float64,
368 doc="flux in filter %s" % (filterName,),
369 units="nJy",
370 )
371 for filterName in filterNameList:
372 schema.addField(
373 field="%s_fluxErr" % (filterName,),
374 type=numpy.float64,
375 doc="flux uncertainty in filter %s" % (filterName,),
376 units="nJy",
377 )
378 if addIsPhotometric:
379 schema.addField(
380 field="photometric",
381 type="Flag",
382 doc="set if the object can be used for photometric calibration",
383 )
384 if addIsResolved:
385 schema.addField(
386 field="resolved",
387 type="Flag",
388 doc="set if the object is spatially resolved",
389 )
390 if addIsVariable:
391 schema.addField(
392 field="variable",
393 type="Flag",
394 doc="set if the object has variable brightness",
395 )
396 if coordErrDim not in (0, 2, 3):
397 raise ValueError("coordErrDim={}; must be (0, 2, 3)".format(coordErrDim))
398 if coordErrDim > 0:
399 afwTable.CovarianceMatrix2fKey.addFields(
400 schema=schema,
401 prefix="coord",
402 names=["ra", "dec"],
403 units=["rad", "rad"],
404 diagonalOnly=(coordErrDim == 2),
405 )
406
407 if addProperMotion or addParallax:
408 schema.addField(
409 field="epoch",
410 type=numpy.float64,
411 doc="date of observation (TAI, MJD)",
412 units="day",
413 )
414
415 if addProperMotion:
416 schema.addField(
417 field="pm_ra",
418 type="Angle",
419 doc="proper motion in the right ascension direction = dra/dt * cos(dec)",
420 units="rad/year",
421 )
422 schema.addField(
423 field="pm_dec",
424 type="Angle",
425 doc="proper motion in the declination direction",
426 units="rad/year",
427 )
428 if properMotionErrDim not in (0, 2, 3):
429 raise ValueError("properMotionErrDim={}; must be (0, 2, 3)".format(properMotionErrDim))
430 if properMotionErrDim > 0:
431 afwTable.CovarianceMatrix2fKey.addFields(
432 schema=schema,
433 prefix="pm",
434 names=["ra", "dec"],
435 units=["rad/year", "rad/year"],
436 diagonalOnly=(properMotionErrDim == 2),
437 )
438 schema.addField(
439 field="pm_flag",
440 type="Flag",
441 doc="Set if proper motion or proper motion error is bad",
442 )
443
444 if addParallax:
445 schema.addField(
446 field="parallax",
447 type="Angle",
448 doc="parallax",
449 units="rad",
450 )
451 schema.addField(
452 field="parallaxErr",
453 type="Angle",
454 doc="uncertainty in parallax",
455 units="rad",
456 )
457 schema.addField(
458 field="parallax_flag",
459 type="Flag",
460 doc="Set if parallax or parallax error is bad",
461 )
462 return schema
463
464 @staticmethod
465 def _remapReferenceCatalogSchema(refCat, *, anyFilterMapsToThis=None,
466 filterMap=None, centroids=False):
467 """This function takes in a reference catalog and returns a new catalog
468 with additional columns defined from the remaining function arguments.
469
470 Parameters
471 ----------
473 Reference catalog to map to new catalog
474 anyFilterMapsToThis : `str`, optional
475 Always use this reference catalog filter.
476 Mutually exclusive with `filterMap`
477 filterMap : `dict` [`str`,`str`], optional
478 Mapping of camera filter name: reference catalog filter name.
479 centroids : `bool`, optional
480 Add centroid fields to the loaded Schema. ``loadPixelBox`` expects
481 these fields to exist.
482
483 Returns
484 -------
485 expandedCat : `lsst.afw.table.SimpleCatalog`
486 Deep copy of input reference catalog with additional columns added
487 """
488 if anyFilterMapsToThis or filterMap:
489 ReferenceObjectLoaderBase._addFluxAliases(refCat.schema, anyFilterMapsToThis, filterMap)
490
491 mapper = afwTable.SchemaMapper(refCat.schema, True)
492 mapper.addMinimalSchema(refCat.schema, True)
493 mapper.editOutputSchema().disconnectAliases()
494
495 if centroids:
496 # Add and initialize centroid and hasCentroid fields (these are
497 # added after loading to avoid wasting space in the saved catalogs).
498 # The new fields are automatically initialized to (nan, nan) and
499 # False so no need to set them explicitly.
500 mapper.editOutputSchema().addField("centroid_x", type=float, doReplace=True)
501 mapper.editOutputSchema().addField("centroid_y", type=float, doReplace=True)
502 mapper.editOutputSchema().addField("hasCentroid", type="Flag", doReplace=True)
503 mapper.editOutputSchema().getAliasMap().set("slot_Centroid", "centroid")
504
505 expandedCat = afwTable.SimpleCatalog(mapper.getOutputSchema())
506 expandedCat.setMetadata(refCat.getMetadata())
507 expandedCat.extend(refCat, mapper=mapper)
508
509 return expandedCat
510
511 @staticmethod
512 def _addFluxAliases(schema, anyFilterMapsToThis=None, filterMap=None):
513 """Add aliases for camera filter fluxes to the schema.
514
515 For each camFilter: refFilter in filterMap, adds these aliases:
516 <camFilter>_camFlux: <refFilter>_flux
517 <camFilter>_camFluxErr: <refFilter>_fluxErr, if the latter exists
518 or sets `anyFilterMapsToThis` in the schema.
519
520 Parameters
521 ----------
522 schema : `lsst.afw.table.Schema`
523 Schema for reference catalog.
524 anyFilterMapsToThis : `str`, optional
525 Always use this reference catalog filter.
526 Mutually exclusive with `filterMap`.
527 filterMap : `dict` [`str`,`str`], optional
528 Mapping of camera filter name: reference catalog filter name.
529 Mutually exclusive with `anyFilterMapsToThis`.
530
531 Raises
532 ------
533 RuntimeError
534 Raised if any required reference flux field is missing from the
535 schema.
536 """
537 # Fail on any truthy value for either of these.
538 if anyFilterMapsToThis and filterMap:
539 raise ValueError("anyFilterMapsToThis and filterMap are mutually exclusive!")
540
541 aliasMap = schema.getAliasMap()
542
543 if anyFilterMapsToThis is not None:
544 refFluxName = anyFilterMapsToThis + "_flux"
545 if refFluxName not in schema:
546 msg = f"Unknown reference filter for anyFilterMapsToThis='{refFluxName}'"
547 raise RuntimeError(msg)
548 aliasMap.set("anyFilterMapsToThis", refFluxName)
549 return # this is mutually exclusive with filterMap
550
551 def addAliasesForOneFilter(filterName, refFilterName):
552 """Add aliases for a single filter
553
554 Parameters
555 ----------
556 filterName : `str` (optional)
557 Camera filter name. The resulting alias name is
558 <filterName>_camFlux
559 refFilterName : `str`
560 Reference catalog filter name; the field
561 <refFilterName>_flux must exist.
562 """
563 camFluxName = filterName + "_camFlux"
564 refFluxName = refFilterName + "_flux"
565 if refFluxName not in schema:
566 raise RuntimeError("Unknown reference filter %s" % (refFluxName,))
567 aliasMap.set(camFluxName, refFluxName)
568 refFluxErrName = refFluxName + "Err"
569 if refFluxErrName in schema:
570 camFluxErrName = camFluxName + "Err"
571 aliasMap.set(camFluxErrName, refFluxErrName)
572
573 if filterMap is not None:
574 for filterName, refFilterName in filterMap.items():
575 addAliasesForOneFilter(filterName, refFilterName)
576
577 @staticmethod
578 def _makeBoxRegion(BBox, wcs, BBoxPadding):
579 outerLocalBBox = geom.Box2D(BBox)
580 innerLocalBBox = geom.Box2D(BBox)
581
582 # Grow the bounding box to allow for effects not fully captured by the
583 # wcs provided (which represents the current best-guess wcs solution
584 # associated with the dataset for which the calibration is to be
585 # computed using the loaded and trimmed reference catalog being defined
586 # here). These effects could include pointing errors and/or an
587 # insufficient optical distorition model for the instrument. The idea
588 # is to ensure the spherical geometric region created contains the
589 # entire region covered by the bbox.
590 # Also create an inner region that is sure to be inside the bbox.
591 outerLocalBBox.grow(BBoxPadding)
592 innerLocalBBox.grow(-1*BBoxPadding)
593
594 # Handle the case where the inner bounding box shrank to a zero sized
595 # region (which will be the case if the shrunken size of either
596 # dimension is less than or equal to zero). In this case, the inner
597 # bounding box is set to the original input bounding box. This is
598 # probably not the best way to handle an empty inner bounding box, but
599 # it is what the calling code currently expects.
600 if innerLocalBBox.getDimensions() == geom.Extent2D(0, 0):
601 innerLocalBBox = geom.Box2D(BBox)
602
603 # Convert the corners of the bounding boxes to sky coordinates.
604 innerBoxCorners = innerLocalBBox.getCorners()
605 innerSphCorners = [wcs.pixelToSky(corner).getVector() for corner in innerBoxCorners]
606 innerSkyRegion = sphgeom.ConvexPolygon(innerSphCorners)
607
608 outerBoxCorners = outerLocalBBox.getCorners()
609 outerSphCorners = [wcs.pixelToSky(corner).getVector() for corner in outerBoxCorners]
610 outerSkyRegion = sphgeom.ConvexPolygon(outerSphCorners)
611
612 return innerSkyRegion, outerSkyRegion, innerSphCorners, outerSphCorners
613
614 @staticmethod
615 def _calculateCircle(bbox, wcs, pixelMargin):
616 """Compute on-sky center and radius of search region.
617
618 Parameters
619 ----------
620 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
621 Pixel bounding box.
623 WCS; used to convert pixel positions to sky coordinates.
624 pixelMargin : `int`
625 Padding to add to 4 all edges of the bounding box (pixels).
626
627 Returns
628 -------
629 results : `lsst.pipe.base.Struct`
630 A Struct containing:
631
632 - coord : `lsst.geom.SpherePoint`
633 ICRS center of the search region.
634 - radius : `lsst.geom.Angle`
635 Radius of the search region.
636 - bbox : `lsst.geom.Box2D`
637 Bounding box used to compute the circle.
638 """
639 bbox = geom.Box2D(bbox) # we modify the box, so use a copy
640 bbox.grow(pixelMargin)
641 coord = wcs.pixelToSky(bbox.getCenter())
642 radius = max(coord.separation(wcs.pixelToSky(pp)) for pp in bbox.getCorners())
643 return pipeBase.Struct(coord=coord, radius=radius, bbox=bbox)
644
645 @staticmethod
646 def getMetadataCircle(coord, radius, filterName, epoch=None):
647 """Return metadata about the reference catalog being loaded.
648
649 This metadata is used for reloading the catalog (e.g. for
650 reconstituting a normalized match list).
651
652 Parameters
653 ----------
654 coord : `lsst.geom.SpherePoint`
655 ICRS center of the search region.
656 radius : `lsst.geom.Angle`
657 Radius of the search region.
658 filterName : `str`
659 Name of the camera filter.
660 epoch : `astropy.time.Time` or `None`, optional
661 Epoch to which to correct proper motion and parallax, or `None` to
662 not apply such corrections.
663
664 Returns
665 -------
667 Metadata about the catalog.
668 """
669 md = PropertyList()
670 md.add('RA', coord.getRa().asDegrees(), 'field center in degrees')
671 md.add('DEC', coord.getDec().asDegrees(), 'field center in degrees')
672 md.add('RADIUS', radius.asDegrees(), 'field radius in degrees, minimum')
673 md.add('SMATCHV', 1, 'SourceMatchVector version number')
674 md.add('FILTER', filterName, 'filter name for photometric data')
675 md.add('EPOCH', "NONE" if epoch is None else epoch.mjd, 'Epoch (TAI MJD) for catalog')
676 return md
677
678 def getMetadataBox(self, bbox, wcs, filterName, epoch=None,
679 bboxToSpherePadding=100):
680 """Return metadata about the load
681
682 This metadata is used for reloading the catalog (e.g., for
683 reconstituting a normalised match list).
684
685 Parameters
686 ----------
687 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
688 Bounding box for the pixels.
690 The WCS object associated with ``bbox``.
691 filterName : `str`
692 Name of the camera filter.
693 epoch : `astropy.time.Time` or `None`, optional
694 Epoch to which to correct proper motion and parallax, or `None` to
695 not apply such corrections.
696 bboxToSpherePadding : `int`, optional
697 Padding in pixels to account for translating a set of corners into
698 a spherical (convex) boundary that is certain to encompass the
699 enitre area covered by the box.
700
701 Returns
702 -------
704 The metadata detailing the search parameters used for this
705 dataset.
706 """
707 circle = self._calculateCircle_calculateCircle(bbox, wcs, self.configconfig.pixelMargin)
708 md = self.getMetadataCirclegetMetadataCircle(circle.coord, circle.radius, filterName, epoch=epoch)
709
710 paddedBbox = circle.bbox
711 _, _, innerCorners, outerCorners = self._makeBoxRegion_makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding)
712 for box, corners in zip(("INNER", "OUTER"), (innerCorners, outerCorners)):
713 for (name, corner) in zip(("UPPER_LEFT", "UPPER_RIGHT", "LOWER_LEFT", "LOWER_RIGHT"),
714 corners):
715 md.add(f"{box}_{name}_RA", geom.SpherePoint(corner).getRa().asDegrees(), f"{box}_corner")
716 md.add(f"{box}_{name}_DEC", geom.SpherePoint(corner).getDec().asDegrees(), f"{box}_corner")
717 return md
718
719 def joinMatchListWithCatalog(self, matchCat, sourceCat):
720 """Relink an unpersisted match list to sources and reference objects.
721
722 A match list is persisted and unpersisted as a catalog of IDs
723 produced by afw.table.packMatches(), with match metadata
724 (as returned by the astrometry tasks) in the catalog's metadata
725 attribute. This method converts such a match catalog into a match
726 list, with links to source records and reference object records.
727
728 Parameters
729 ----------
730 matchCat : `lsst.afw.table.BaseCatalog`
731 Unpersisted packed match list.
732 ``matchCat.table.getMetadata()`` must contain match metadata,
733 as returned by the astrometry tasks.
734 sourceCat : `lsst.afw.table.SourceCatalog`
735 Source catalog. As a side effect, the catalog will be sorted
736 by ID.
737
738 Returns
739 -------
741 Match list.
742 """
743 return joinMatchListWithCatalogImpl(self, matchCat, sourceCat)
744
745
747 """This class facilitates loading reference catalogs with gen 3 middleware.
748
749 The QuantumGraph generation will create a list of datasets that may
750 possibly overlap a given region. These datasets are then used to construct
751 and instance of this class. The class instance should then be passed into
752 a task which needs reference catalogs. These tasks should then determine
753 the exact region of the sky reference catalogs will be loaded for, and
754 call a corresponding method to load the reference objects.
755
756 Parameters
757 ----------
758 dataIds : iterable of `lsst.daf.butler.DataCoordinate`
759 An iterable object of data IDs that point to reference catalogs
760 in a gen 3 repository.
761 refCats : iterable of `lsst.daf.butler.DeferedDatasetHandle`
762 Handles to load refCats on demand
763 log : `lsst.log.Log`, `logging.Logger` or `None`, optional
764 Logger object used to write out messages. If `None` a default
765 logger will be used.
766 """
767 def __init__(self, dataIds, refCats, log=None, **kwargs):
768 super().__init__(**kwargs)
769 self.dataIdsdataIds = dataIds
770 self.refCatsrefCats = refCats
771 self.loglog = log or logging.getLogger(__name__).getChild("ReferenceObjectLoader")
772
773 def loadPixelBox(self, bbox, wcs, filterName, epoch=None,
774 bboxToSpherePadding=100):
775 """Load reference objects that are within a pixel-based rectangular
776 region.
777
778 This algorithm works by creating a spherical box whose corners
779 correspond to the WCS converted corners of the input bounding box
780 (possibly padded). It then defines a filtering function which looks at
781 the pixel position of the reference objects and accepts only those that
782 lie within the specified bounding box.
783
784 The spherical box region and filtering function are passed to the
785 generic loadRegion method which loads and filters the reference objects
786 from the datastore and returns a single catalog containing the filtered
787 set of reference objects.
788
789 Parameters
790 ----------
791 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
792 Box which bounds a region in pixel space.
794 Wcs object defining the pixel to sky (and inverse) transform for
795 the supplied ``bbox``.
796 filterName : `str`
797 Name of camera filter.
798 epoch : `astropy.time.Time` or `None`, optional
799 Epoch to which to correct proper motion and parallax, or `None`
800 to not apply such corrections.
801 bboxToSpherePadding : `int`, optional
802 Padding to account for translating a set of corners into a
803 spherical (convex) boundary that is certain to encompase the
804 enitre area covered by the box.
805
806 Returns
807 -------
808 referenceCatalog : `lsst.afw.table.SimpleCatalog`
809 Catalog containing reference objects inside the specified bounding
810 box (padded by self.configconfig.pixelMargin).
811
812 Raises
813 ------
814 RuntimeError
815 Raised if no reference catalogs could be found for the specified
816 region.
817 TypeError
818 Raised if the loaded reference catalogs do not have matching
819 schemas.
820 """
821 paddedBbox = geom.Box2D(bbox)
822 paddedBbox.grow(self.configconfig.pixelMargin)
823 innerSkyRegion, outerSkyRegion, _, _ = self._makeBoxRegion_makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding)
824
825 def _filterFunction(refCat, region):
826 # Perform an initial "pre filter" step based on the refCat coords
827 # and the outerSkyRegion created from the self.config.pixelMargin-
828 # paddedBbox plus an "extra" padding of bboxToSpherePadding and the
829 # raw wcs. This should ensure a large enough projected area on the
830 # sky that accounts for any projection/distortion issues, but small
831 # enough to filter out loaded reference objects that lie well
832 # beyond the projected detector of interest. This step is required
833 # due to the very local nature of the wcs available for the
834 # sky <--> pixel conversions.
835 preFiltFunc = _FilterCatalog(outerSkyRegion)
836 refCat = preFiltFunc(refCat, region)
837
838 # Add columns to the pre-filtered reference catalog relating their
839 # coordinates to equivalent pixel positions for the wcs provided
840 # and use to populate those columns.
841 refCat = self._remapReferenceCatalogSchema_remapReferenceCatalogSchema(refCat, centroids=True)
842 afwTable.updateRefCentroids(wcs, refCat)
843 # No need to filter the catalog if it is entirely contained in the
844 # region defined by the inner sky region.
845 if innerSkyRegion.contains(region):
846 return refCat
847 # Create a new reference catalog, and populate it only with records
848 # that fall inside the padded bbox.
849 filteredRefCat = type(refCat)(refCat.table)
850 centroidKey = afwTable.Point2DKey(refCat.schema['centroid'])
851 for record in refCat:
852 pixCoords = record[centroidKey]
853 if paddedBbox.contains(geom.Point2D(pixCoords)):
854 filteredRefCat.append(record)
855 return filteredRefCat
856 return self.loadRegionloadRegion(outerSkyRegion, filterName, filtFunc=_filterFunction, epoch=epoch)
857
858 def loadRegion(self, region, filterName, filtFunc=None, epoch=None):
859 """Load reference objects within a specified region.
860
861 This function loads the DataIds used to construct an instance of this
862 class which intersect or are contained within the specified region. The
863 reference catalogs which intersect but are not fully contained within
864 the input region are further filtered by the specified filter function.
865 This function returns a single source catalog containing all reference
866 objects inside the specified region.
867
868 Parameters
869 ----------
870 region : `lsst.sphgeom.Region`
871 This can be any type that is derived from `lsst.sphgeom.Region` and
872 should define the spatial region for which reference objects are to
873 be loaded.
874 filtFunc : callable or `None`, optional
875 This optional parameter should be a callable object that takes a
876 reference catalog and its corresponding region as parameters,
877 filters the catalog by some criteria and returns the filtered
878 reference catalog. If `None`, an internal filter function is used
879 which filters according to if a reference object falls within the
880 input region.
881 filterName : `str`
882 Name of camera filter.
883 epoch : `astropy.time.Time` or `None`, optional
884 Epoch to which to correct proper motion and parallax, or `None` to
885 not apply such corrections.
886
887 Returns
888 -------
889 referenceCatalog : `lsst.afw.table.SourceCatalog`
890 Catalog containing reference objects which intersect the input region,
891 filtered by the specified filter function.
892
893 Raises
894 ------
895 RuntimeError
896 Raised if no reference catalogs could be found for the specified
897 region.
898 TypeError
899 Raised if the loaded reference catalogs do not have matching
900 schemas.
901 """
902 regionLat = region.getBoundingBox().getLat()
903 regionLon = region.getBoundingBox().getLon()
904 self.loglog.info("Loading reference objects from %s in region bounded by "
905 "[%.8f, %.8f], [%.8f, %.8f] RA Dec",
906 # Name of refcat we're loading from is the datasetType.
907 self.refCatsrefCats[0].ref.datasetType.name,
908 regionLon.getA().asDegrees(), regionLon.getB().asDegrees(),
909 regionLat.getA().asDegrees(), regionLat.getB().asDegrees())
910 if filtFunc is None:
911 filtFunc = _FilterCatalog(region)
912 # filter out all the regions supplied by the constructor that do not overlap
913 overlapList = []
914 for dataId, refCat in zip(self.dataIdsdataIds, self.refCatsrefCats):
915 # SphGeom supports some objects intersecting others, but is not symmetric,
916 # try the intersect operation in both directions
917 try:
918 intersects = dataId.region.intersects(region)
919 except TypeError:
920 intersects = region.intersects(dataId.region)
921
922 if intersects:
923 overlapList.append((dataId, refCat))
924
925 if len(overlapList) == 0:
926 raise RuntimeError("No reference tables could be found for input region")
927
928 firstCat = overlapList[0][1].get()
929 refCat = filtFunc(firstCat, overlapList[0][0].region)
930 trimmedAmount = len(firstCat) - len(refCat)
931
932 # Load in the remaining catalogs
933 for dataId, inputRefCat in overlapList[1:]:
934 tmpCat = inputRefCat.get()
935
936 if tmpCat.schema != firstCat.schema:
937 raise TypeError("Reference catalogs have mismatching schemas")
938
939 filteredCat = filtFunc(tmpCat, dataId.region)
940 refCat.extend(filteredCat)
941 trimmedAmount += len(tmpCat) - len(filteredCat)
942
943 self.loglog.debug("Trimmed %d refCat objects lying outside padded region, leaving %d",
944 trimmedAmount, len(refCat))
945 self.loglog.info("Loaded %d reference objects", len(refCat))
946
947 # Ensure that the loaded reference catalog is continuous in memory
948 if not refCat.isContiguous():
949 refCat = refCat.copy(deep=True)
950
951 self.applyProperMotionsapplyProperMotions(refCat, epoch)
952
953 # Verify the schema is in the correct units and has the correct version; automatically convert
954 # it with a warning if this is not the case.
955 if not hasNanojanskyFluxUnits(refCat.schema) or not getFormatVersionFromRefCat(refCat) >= 1:
956 self.loglog.warning("Found version 0 reference catalog with old style units in schema.")
957 self.loglog.warning("run `meas_algorithms/bin/convert_refcat_to_nJy.py` to convert fluxes to nJy.")
958 self.loglog.warning("See RFC-575 for more details.")
959 refCat = convertToNanojansky(refCat, self.loglog)
960
961 expandedCat = self._remapReferenceCatalogSchema_remapReferenceCatalogSchema(refCat,
962 anyFilterMapsToThis=self.configconfig.anyFilterMapsToThis,
963 filterMap=self.configconfig.filterMap)
964
965 # Ensure that the returned reference catalog is continuous in memory
966 if not expandedCat.isContiguous():
967 expandedCat = expandedCat.copy(deep=True)
968
969 fluxField = getRefFluxField(expandedCat.schema, filterName)
970 return pipeBase.Struct(refCat=expandedCat, fluxField=fluxField)
971
972 def loadSkyCircle(self, ctrCoord, radius, filterName, epoch=None):
973 """Load reference objects that lie within a circular region on the sky.
974
975 This method constructs a circular region from an input center and
976 angular radius, loads reference catalogs which are contained in or
977 intersect the circle, and filters reference catalogs which intersect
978 down to objects which lie within the defined circle.
979
980 Parameters
981 ----------
982 ctrCoord : `lsst.geom.SpherePoint`
983 Point defining the center of the circular region.
984 radius : `lsst.geom.Angle`
985 Defines the angular radius of the circular region.
986 filterName : `str`
987 Name of camera filter.
988 epoch : `astropy.time.Time` or `None`, optional
989 Epoch to which to correct proper motion and parallax, or `None` to
990 not apply such corrections.
991
992 Returns
993 -------
994 referenceCatalog : `lsst.afw.table.SourceCatalog`
995 Catalog containing reference objects inside the specified search
996 circle.
997 """
998 centerVector = ctrCoord.getVector()
999 sphRadius = sphgeom.Angle(radius.asRadians())
1000 circularRegion = sphgeom.Circle(centerVector, sphRadius)
1001 return self.loadRegionloadRegion(circularRegion, filterName, epoch=epoch)
1002
1003
1004def getRefFluxField(schema, filterName):
1005 """Get the name of a flux field from a schema.
1006
1007 return the alias of "anyFilterMapsToThis", if present
1008 else:
1009 return "*filterName*_camFlux" if present
1010 else return "*filterName*_flux" if present (camera filter name
1011 matches reference filter name)
1012 else throw RuntimeError
1013
1014 Parameters
1015 ----------
1016 schema : `lsst.afw.table.Schema`
1017 Reference catalog schema.
1018 filterName : `str`
1019 Name of camera filter.
1020
1021 Returns
1022 -------
1023 fluxFieldName : `str`
1024 Name of flux field.
1025
1026 Raises
1027 ------
1028 RuntimeError
1029 If an appropriate field is not found.
1030 """
1031 if not isinstance(schema, afwTable.Schema):
1032 raise RuntimeError("schema=%s is not a schema" % (schema,))
1033 try:
1034 return schema.getAliasMap().get("anyFilterMapsToThis")
1035 except LookupError:
1036 pass # try the filterMap next
1037
1038 fluxFieldList = [filterName + "_camFlux", filterName + "_flux"]
1039 for fluxField in fluxFieldList:
1040 if fluxField in schema:
1041 return fluxField
1042
1043 raise RuntimeError("Could not find flux field(s) %s" % (", ".join(fluxFieldList)))
1044
1045
1046def getRefFluxKeys(schema, filterName):
1047 """Return keys for flux and flux error.
1048
1049 Parameters
1050 ----------
1051 schema : `lsst.afw.table.Schema`
1052 Reference catalog schema.
1053 filterName : `str`
1054 Name of camera filter.
1055
1056 Returns
1057 -------
1058 keys : `tuple` of (`lsst.afw.table.Key`, `lsst.afw.table.Key`)
1059 Two keys:
1060
1061 - flux key
1062 - flux error key, if present, else None
1063
1064 Raises
1065 ------
1066 RuntimeError
1067 If flux field not found.
1068 """
1069 fluxField = getRefFluxField(schema, filterName)
1070 fluxErrField = fluxField + "Err"
1071 fluxKey = schema[fluxField].asKey()
1072 try:
1073 fluxErrKey = schema[fluxErrField].asKey()
1074 except Exception:
1075 fluxErrKey = None
1076 return (fluxKey, fluxErrKey)
1077
1078
1079class LoadReferenceObjectsTask(pipeBase.Task, ReferenceObjectLoaderBase, metaclass=abc.ABCMeta):
1080 """Abstract gen2 base class to load objects from reference catalogs.
1081 """
1082 _DefaultName = "LoadReferenceObjects"
1083
1084 def __init__(self, butler=None, *args, **kwargs):
1085 """Construct a LoadReferenceObjectsTask
1086
1087 Parameters
1088 ----------
1089 butler : `lsst.daf.persistence.Butler`
1090 Data butler, for access reference catalogs.
1091 """
1092 pipeBase.Task.__init__(self, *args, **kwargs)
1093 self.butlerbutler = butler
1094
1095 @timeMethod
1096 def loadPixelBox(self, bbox, wcs, filterName, photoCalib=None, epoch=None):
1097 """Load reference objects that overlap a rectangular pixel region.
1098
1099 Parameters
1100 ----------
1101 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
1102 Bounding box for pixels.
1103 wcs : `lsst.afw.geom.SkyWcs`
1104 WCS; used to convert pixel positions to sky coordinates
1105 and vice-versa.
1106 filterName : `str`
1107 Name of filter. This can be used for flux limit comparisons.
1108 photoCalib : `None`
1109 Deprecated, only included for api compatibility.
1110 epoch : `astropy.time.Time` or `None`, optional
1111 Epoch to which to correct proper motion and parallax, or `None` to
1112 not apply such corrections.
1113
1114 Returns
1115 -------
1116 results : `lsst.pipe.base.Struct`
1117 A Struct containing the following fields:
1118 refCat : `lsst.afw.catalog.SimpleCatalog`
1119 A catalog of reference objects with the standard
1120 schema, as documented in the main doc string for
1121 `LoadReferenceObjects`.
1122 The catalog is guaranteed to be contiguous.
1123 fluxField : `str`
1124 Name of flux field for specified `filterName`.
1125
1126 Notes
1127 -----
1128 The search algorithm works by searching in a region in sky
1129 coordinates whose center is the center of the bbox and radius
1130 is large enough to just include all 4 corners of the bbox.
1131 Stars that lie outside the bbox are then trimmed from the list.
1132 """
1133 circle = self._calculateCircle_calculateCircle(bbox, wcs, self.configconfig.pixelMargin)
1134
1135 # find objects in circle
1136 self.log.info("Loading reference objects from %s using center %s and radius %s deg",
1137 self.configconfig.ref_dataset_name, circle.coord, circle.radius.asDegrees())
1138 loadRes = self.loadSkyCircleloadSkyCircle(circle.coord, circle.radius, filterName, epoch=epoch,
1139 centroids=True)
1140 refCat = loadRes.refCat
1141 numFound = len(refCat)
1142
1143 # trim objects outside bbox
1144 refCat = self._trimToBBox_trimToBBox(refCat=refCat, bbox=circle.bbox, wcs=wcs)
1145 numTrimmed = numFound - len(refCat)
1146 self.log.debug("trimmed %d out-of-bbox objects, leaving %d", numTrimmed, len(refCat))
1147 self.log.info("Loaded %d reference objects", len(refCat))
1148
1149 # make sure catalog is contiguous
1150 if not refCat.isContiguous():
1151 loadRes.refCat = refCat.copy(deep=True)
1152
1153 return loadRes
1154
1155 @abc.abstractmethod
1156 def loadSkyCircle(self, ctrCoord, radius, filterName, epoch=None, centroids=False):
1157 """Load reference objects that overlap a circular sky region.
1158
1159 Parameters
1160 ----------
1161 ctrCoord : `lsst.geom.SpherePoint`
1162 ICRS center of search region.
1163 radius : `lsst.geom.Angle`
1164 Radius of search region.
1165 filterName : `str`
1166 Name of filter. This can be used for flux limit comparisons.
1167 epoch : `astropy.time.Time` or `None`, optional
1168 Epoch to which to correct proper motion and parallax, or `None` to
1169 not apply such corrections.
1170 centroids : `bool`, optional
1171 Add centroid fields to the loaded Schema. ``loadPixelBox`` expects
1172 these fields to exist.
1173
1174 Returns
1175 -------
1176 results : `lsst.pipe.base.Struct`
1177 A Struct containing the following fields:
1178 refCat : `lsst.afw.catalog.SimpleCatalog`
1179 A catalog of reference objects with the standard
1180 schema, as documented in the main doc string for
1181 `LoadReferenceObjects`.
1182 The catalog is guaranteed to be contiguous.
1183 fluxField : `str`
1184 Name of flux field for specified `filterName`.
1185
1186 Notes
1187 -----
1188 Note that subclasses are responsible for performing the proper motion
1189 correction, since this is the lowest-level interface for retrieving
1190 the catalog.
1191 """
1192 return
1193
1194 @staticmethod
1195 def _trimToBBox(refCat, bbox, wcs):
1196 """Remove objects outside a given pixel bounding box and set
1197 centroid and hasCentroid fields.
1198
1199 Parameters
1200 ----------
1202 A catalog of objects. The schema must include fields
1203 "coord", "centroid" and "hasCentroid".
1204 The "coord" field is read.
1205 The "centroid" and "hasCentroid" fields are set.
1206 bbox : `lsst.geom.Box2D`
1207 Pixel region
1208 wcs : `lsst.afw.geom.SkyWcs`
1209 WCS; used to convert sky coordinates to pixel positions.
1210
1211 Returns
1212 -------
1214 Reference objects in the bbox, with centroid and
1215 hasCentroid fields set.
1216 """
1217 afwTable.updateRefCentroids(wcs, refCat)
1218 centroidKey = afwTable.Point2DKey(refCat.schema["centroid"])
1219 retStarCat = type(refCat)(refCat.table)
1220 for star in refCat:
1221 point = star.get(centroidKey)
1222 if bbox.contains(point):
1223 retStarCat.append(star)
1224 return retStarCat
1225
1226
1227def joinMatchListWithCatalogImpl(refObjLoader, matchCat, sourceCat):
1228 """Relink an unpersisted match list to sources and reference
1229 objects.
1230
1231 A match list is persisted and unpersisted as a catalog of IDs
1232 produced by afw.table.packMatches(), with match metadata
1233 (as returned by the astrometry tasks) in the catalog's metadata
1234 attribute. This method converts such a match catalog into a match
1235 list, with links to source records and reference object records.
1236
1237 Parameters
1238 ----------
1239 refObjLoader
1240 Reference object loader to use in getting reference objects
1241 matchCat : `lsst.afw.table.BaseCatalog`
1242 Unperisted packed match list.
1243 ``matchCat.table.getMetadata()`` must contain match metadata,
1244 as returned by the astrometry tasks.
1245 sourceCat : `lsst.afw.table.SourceCatalog`
1246 Source catalog. As a side effect, the catalog will be sorted
1247 by ID.
1248
1249 Returns
1250 -------
1252 Match list.
1253 """
1254 matchmeta = matchCat.table.getMetadata()
1255 version = matchmeta.getInt('SMATCHV')
1256 if version != 1:
1257 raise ValueError('SourceMatchVector version number is %i, not 1.' % version)
1258 filterName = matchmeta.getString('FILTER').strip()
1259 try:
1260 epoch = matchmeta.getDouble('EPOCH')
1261 except (LookupError, TypeError):
1262 epoch = None # Not present, or not correct type means it's not set
1263 if 'RADIUS' in matchmeta:
1264 # This is a circle style metadata, call loadSkyCircle
1265 ctrCoord = geom.SpherePoint(matchmeta.getDouble('RA'),
1266 matchmeta.getDouble('DEC'), geom.degrees)
1267 rad = matchmeta.getDouble('RADIUS')*geom.degrees
1268 refCat = refObjLoader.loadSkyCircle(ctrCoord, rad, filterName, epoch=epoch).refCat
1269 elif "INNER_UPPER_LEFT_RA" in matchmeta:
1270 # This is the sky box type (only triggers in the LoadReferenceObject class, not task)
1271 # Only the outer box is required to be loaded to get the maximum region, all filtering
1272 # will be done by the unpackMatches function, and no spatial filtering needs to be done
1273 # by the refObjLoader
1274 box = []
1275 for place in ("UPPER_LEFT", "UPPER_RIGHT", "LOWER_LEFT", "LOWER_RIGHT"):
1276 coord = geom.SpherePoint(matchmeta.getDouble(f"OUTER_{place}_RA"),
1277 matchmeta.getDouble(f"OUTER_{place}_DEC"),
1278 geom.degrees).getVector()
1279 box.append(coord)
1280 outerBox = sphgeom.ConvexPolygon(box)
1281 refCat = refObjLoader.loadRegion(outerBox, filterName, epoch=epoch).refCat
1282
1283 refCat.sort()
1284 sourceCat.sort()
1285 return afwTable.unpackMatches(matchCat, refCat, sourceCat)
1286
1287
1288def applyProperMotionsImpl(log, catalog, epoch):
1289 """Apply proper motion correction to a reference catalog.
1290
1291 Adjust position and position error in the ``catalog``
1292 for proper motion to the specified ``epoch``,
1293 modifying the catalog in place.
1294
1295 Parameters
1296 ----------
1297 log : `lsst.log.Log` or `logging.getLogger`
1298 Log object to write to.
1300 Catalog of positions, containing:
1301
1302 - Coordinates, retrieved by the table's coordinate key.
1303 - ``coord_raErr`` : Error in Right Ascension (rad).
1304 - ``coord_decErr`` : Error in Declination (rad).
1305 - ``pm_ra`` : Proper motion in Right Ascension (rad/yr,
1306 East positive)
1307 - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
1308 - ``pm_dec`` : Proper motion in Declination (rad/yr,
1309 North positive)
1310 - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional.
1311 - ``epoch`` : Mean epoch of object (an astropy.time.Time)
1312 epoch : `astropy.time.Time`
1313 Epoch to which to correct proper motion.
1314 """
1315 if "epoch" not in catalog.schema or "pm_ra" not in catalog.schema or "pm_dec" not in catalog.schema:
1316 log.warning("Proper motion correction not available from catalog")
1317 return
1318 if not catalog.isContiguous():
1319 raise RuntimeError("Catalog must be contiguous")
1320 catEpoch = astropy.time.Time(catalog["epoch"], scale="tai", format="mjd")
1321 log.info("Correcting reference catalog for proper motion to %r", epoch)
1322 # Use `epoch.tai` to make sure the time difference is in TAI
1323 timeDiffsYears = (epoch.tai - catEpoch).to(astropy.units.yr).value
1324 coordKey = catalog.table.getCoordKey()
1325 # Compute the offset of each object due to proper motion
1326 # as components of the arc of a great circle along RA and Dec
1327 pmRaRad = catalog["pm_ra"]
1328 pmDecRad = catalog["pm_dec"]
1329 offsetsRaRad = pmRaRad*timeDiffsYears
1330 offsetsDecRad = pmDecRad*timeDiffsYears
1331 # Compute the corresponding bearing and arc length of each offset
1332 # due to proper motion, and apply the offset
1333 # The factor of 1e6 for computing bearing is intended as
1334 # a reasonable scale for typical values of proper motion
1335 # in order to avoid large errors for small values of proper motion;
1336 # using the offsets is another option, but it can give
1337 # needlessly large errors for short duration
1338 offsetBearingsRad = numpy.arctan2(pmDecRad*1e6, pmRaRad*1e6)
1339 offsetAmountsRad = numpy.hypot(offsetsRaRad, offsetsDecRad)
1340 for record, bearingRad, amountRad in zip(catalog, offsetBearingsRad, offsetAmountsRad):
1341 record.set(coordKey,
1342 record.get(coordKey).offset(bearing=bearingRad*geom.radians,
1343 amount=amountRad*geom.radians))
1344 # Increase error in RA and Dec based on error in proper motion
1345 if "coord_raErr" in catalog.schema:
1346 catalog["coord_raErr"] = numpy.hypot(catalog["coord_raErr"],
1347 catalog["pm_raErr"]*timeDiffsYears)
1348 if "coord_decErr" in catalog.schema:
1349 catalog["coord_decErr"] = numpy.hypot(catalog["coord_decErr"],
1350 catalog["pm_decErr"]*timeDiffsYears)
def loadSkyCircle(self, ctrCoord, radius, filterName, epoch=None, centroids=False)
def loadPixelBox(self, bbox, wcs, filterName, photoCalib=None, epoch=None)
def getMetadataBox(self, bbox, wcs, filterName, epoch=None, bboxToSpherePadding=100)
def makeMinimalSchema(filterNameList, *addCentroid=False, addIsPhotometric=False, addIsResolved=False, addIsVariable=False, coordErrDim=2, addProperMotion=False, properMotionErrDim=2, addParallax=False)
def _remapReferenceCatalogSchema(refCat, *anyFilterMapsToThis=None, filterMap=None, centroids=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, bboxToSpherePadding=100)
def __init__(self, dataIds, refCats, log=None, **kwargs)
def convertToNanojansky(catalog, log, doConvert=True)
def joinMatchListWithCatalogImpl(refObjLoader, matchCat, sourceCat)