LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
loadReferenceObjects.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 #
4 # Copyright 2008-2017 AURA/LSST.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <https://www.lsstcorp.org/LegalNotices/>.
22 #
23 
24 __all__ = ["getRefFluxField", "getRefFluxKeys", "LoadReferenceObjectsTask", "LoadReferenceObjectsConfig",
25  "ReferenceObjectLoader"]
26 
27 import abc
28 import itertools
29 import logging
30 
31 import astropy.time
32 import astropy.units
33 import numpy
34 
35 import lsst.geom as geom
36 import lsst.afw.table as afwTable
37 import lsst.pex.config as pexConfig
38 import lsst.pipe.base as pipeBase
39 from lsst import sphgeom
40 from lsst.daf.base import PropertyList
41 from lsst.utils.timer import timeMethod
42 
43 
44 def isOldFluxField(name, units):
45  """Return True if this name/units combination corresponds to an
46  "old-style" reference catalog flux field.
47  """
48  unitsCheck = units != 'nJy' # (units == 'Jy' or units == '' or units == '?')
49  isFlux = name.endswith('_flux')
50  isFluxSigma = name.endswith('_fluxSigma')
51  isFluxErr = name.endswith('_fluxErr')
52  return (isFlux or isFluxSigma or isFluxErr) and unitsCheck
53 
54 
56  """Return True if the units of all flux and fluxErr are correct (nJy).
57  """
58  for field in schema:
59  if isOldFluxField(field.field.getName(), field.field.getUnits()):
60  return False
61  return True
62 
63 
65  """"Return the format version stored in a reference catalog header.
66 
67  Parameters
68  ----------
69  refCat : `lsst.afw.table.SimpleCatalog`
70  Reference catalog to inspect.
71 
72  Returns
73  -------
74  version : `int`
75  Format verison integer. Returns `0` if the catalog has no metadata
76  or the metadata does not include a "REFCAT_FORMAT_VERSION" key.
77  """
78  md = refCat.getMetadata()
79  if md is None:
80  return 0
81  try:
82  return md.getScalar("REFCAT_FORMAT_VERSION")
83  except KeyError:
84  return 0
85 
86 
87 def convertToNanojansky(catalog, log, doConvert=True):
88  """Convert fluxes in a catalog from jansky to nanojansky.
89 
90  Parameters
91  ----------
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`.
99 
100  Returns
101  -------
102  catalog : `lsst.afw.table.SimpleCatalog` or None
103  The converted catalog, or None if ``doConvert`` is False.
104 
105  Notes
106  -----
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.
110  """
111  # Do not share the AliasMap: for refcats, that gets created when the
112  # catalog is read from disk and should not be propagated.
113  mapper = afwTable.SchemaMapper(catalog.schema, shareAliasMap=False)
114  mapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
115  input_fields = []
116  output_fields = []
117  for field in catalog.schema:
118  oldName = field.field.getName()
119  oldUnits = field.field.getUnits()
120  if isOldFluxField(oldName, oldUnits):
121  units = 'nJy'
122  # remap Sigma flux fields to Err, so we can drop the alias
123  if oldName.endswith('_fluxSigma'):
124  name = oldName.replace('_fluxSigma', '_fluxErr')
125  else:
126  name = oldName
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)
131  else:
132  mapper.addMapping(field.getKey())
133 
134  fluxFieldsStr = '; '.join("(%s, '%s')" % (field.getName(), field.getUnits()) for field in input_fields)
135 
136  if doConvert:
137  newSchema = mapper.getOutputSchema()
138  output = afwTable.SimpleCatalog(newSchema)
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)
143  return output
144  else:
145  log.info("Found old-style refcat flux fields (name, units): %s", fluxFieldsStr)
146  return None
147 
148 
150  """This is a private helper class which filters catalogs by
151  row based on the row being inside the region used to initialize
152  the class.
153 
154  Parameters
155  ----------
156  region : `lsst.sphgeom.Region`
157  The spatial region which all objects should lie within
158  """
159  def __init__(self, region):
160  self.regionregion = region
161 
162  def __call__(self, refCat, catRegion):
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.
165 
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.
169 
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.
174 
175  Parameters
176  ---------
177  refCat : `lsst.afw.table.SourceCatalog`
178  SourceCatalog to be filtered.
179  catRegion : `lsst.sphgeom.Region`
180  Region in which the catalog was created
181  """
182  if catRegion.isWithin(self.regionregion):
183  # no filtering needed, region completely contains refcat
184  return refCat
185 
186  filteredRefCat = type(refCat)(refCat.table)
187  for record in refCat:
188  if self.regionregion.contains(record.getCoord().getVector()):
189  filteredRefCat.append(record)
190  return filteredRefCat
191 
192 
194  """Base class for reference object loaders, to facilitate gen2/gen3 code
195  sharing.
196  """
197  def applyProperMotions(self, catalog, epoch):
198  """Apply proper motion correction to a reference catalog.
199 
200  Adjust position and position error in the ``catalog``
201  for proper motion to the specified ``epoch``,
202  modifying the catalog in place.
203 
204  Parameters
205  ----------
206  catalog : `lsst.afw.table.SimpleCatalog`
207  Catalog of positions, containing at least these fields:
208 
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,
213  East positive)
214  - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
215  - ``pm_dec`` : Proper motion in Declination (rad/yr,
216  North positive)
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.
223 
224  Raises
225  ------
226  RuntimeError
227  Raised if ``config.requireProperMotion`` is set but we cannot
228  apply the proper motion correction for some reason.
229  """
230  if epoch is None:
231  if self.config.requireProperMotion:
232  raise RuntimeError("requireProperMotion=True but epoch not provided to loader.")
233  else:
234  self.log.debug("No epoch provided: not applying proper motion corrections to refcat.")
235  return
236 
237  # Warn/raise for a catalog in an incorrect format, if epoch was specified.
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.")
242  else:
243  self.log.warning("Reference catalog pm_ra field is not an Angle; cannot apply proper motion.")
244  return
245 
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.")
249  else:
250  self.log.warning("Proper motion correction not available for this reference catalog.")
251  return
252 
253  applyProperMotionsImpl(self.log, catalog, epoch)
254 
255 
257  """This class facilitates loading reference catalogs with gen 3 middleware
258 
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.
265  """
266  def __init__(self, dataIds, refCats, config, log=None):
267  """ Constructs an instance of ReferenceObjectLoader
268 
269  Parameters
270  ----------
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
280  logger will be used.
281  """
282  self.dataIdsdataIds = dataIds
283  self.refCatsrefCats = refCats
284  self.loglog = log or logging.getLogger(__name__).getChild("ReferenceObjectLoader")
285  self.configconfig = config
286 
287  @staticmethod
288  def _makeBoxRegion(BBox, wcs, BBoxPadding):
289  outerLocalBBox = geom.Box2D(BBox)
290  innerLocalBBox = geom.Box2D(BBox)
291 
292  # Grow the bounding box to allow for effects not fully captured by the
293  # wcs provided (which represents the current best-guess wcs solution
294  # associated with the dataset for which the calibration is to be
295  # computed using the loaded and trimmed reference catalog being defined
296  # here). These effects could include pointing errors and/or an
297  # insufficient optical distorition model for the instrument. The idea
298  # is to ensure the spherical geometric region created contains the
299  # entire region covered by the bbox.
300  # Also create an inner region that is sure to be inside the bbox.
301  outerLocalBBox.grow(BBoxPadding)
302  innerLocalBBox.grow(-1*BBoxPadding)
303 
304  # Handle the case where the inner bounding box shrank to a zero sized
305  # region (which will be the case if the shrunken size of either
306  # dimension is less than or equal to zero). In this case, the inner
307  # bounding box is set to the original input bounding box. This is
308  # probably not the best way to handle an empty inner bounding box, but
309  # it is what the calling code currently expects.
310  if innerLocalBBox.getDimensions() == geom.Extent2D(0, 0):
311  innerLocalBBox = geom.Box2D(BBox)
312 
313  # Convert the corners of the bounding boxes to sky coordinates.
314  innerBoxCorners = innerLocalBBox.getCorners()
315  innerSphCorners = [wcs.pixelToSky(corner).getVector() for corner in innerBoxCorners]
316  innerSkyRegion = sphgeom.ConvexPolygon(innerSphCorners)
317 
318  outerBoxCorners = outerLocalBBox.getCorners()
319  outerSphCorners = [wcs.pixelToSky(corner).getVector() for corner in outerBoxCorners]
320  outerSkyRegion = sphgeom.ConvexPolygon(outerSphCorners)
321 
322  return innerSkyRegion, outerSkyRegion, innerSphCorners, outerSphCorners
323 
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
327  region.
328 
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.
334 
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.
339 
340  Parameters
341  ----------
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.
352  photoCalib : `None`
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.
358 
359  Returns
360  -------
361  referenceCatalog : `lsst.afw.table.SimpleCatalog`
362  Catalog containing reference objects inside the specified bounding
363  box (padded by self.config.pixelMargin).
364 
365  Raises
366  ------
367  RuntimeError
368  Raised if no reference catalogs could be found for the specified
369  region.
370  TypeError
371  Raised if the loaded reference catalogs do not have matching
372  schemas.
373  """
374  paddedBbox = geom.Box2D(bbox)
375  paddedBbox.grow(self.configconfig.pixelMargin)
376  innerSkyRegion, outerSkyRegion, _, _ = self._makeBoxRegion_makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding)
377 
378  def _filterFunction(refCat, region):
379  # Perform an initial "pre filter" step based on the refCat coords
380  # and the outerSkyRegion created from the self.config.pixelMargin-
381  # paddedBbox plus an "extra" padding of bboxToSpherePadding and the
382  # raw wcs. This should ensure a large enough projected area on the
383  # sky that accounts for any projection/distortion issues, but small
384  # enough to filter out loaded reference objects that lie well
385  # beyond the projected detector of interest. This step is required
386  # due to the very local nature of the wcs available for the
387  # sky <--> pixel conversions.
388  preFiltFunc = _FilterCatalog(outerSkyRegion)
389  refCat = preFiltFunc(refCat, region)
390 
391  # Add columns to the pre-filtered reference catalog relating their
392  # coordinates to equivalent pixel positions for the wcs provided
393  # and use to populate those columns.
394  refCat = self.remapReferenceCatalogSchemaremapReferenceCatalogSchema(refCat, position=True)
395  afwTable.updateRefCentroids(wcs, refCat)
396  # No need to filter the catalog if it is entirely contained in the
397  # region defined by the inner sky region.
398  if innerSkyRegion.contains(region):
399  return refCat
400  # Create a new reference catalog, and populate it only with records
401  # that fall inside the padded bbox.
402  filteredRefCat = type(refCat)(refCat.table)
403  centroidKey = afwTable.Point2DKey(refCat.schema['centroid'])
404  for record in refCat:
405  pixCoords = record[centroidKey]
406  if paddedBbox.contains(geom.Point2D(pixCoords)):
407  filteredRefCat.append(record)
408  return filteredRefCat
409  return self.loadRegionloadRegion(outerSkyRegion, filtFunc=_filterFunction, epoch=epoch, filterName=filterName)
410 
411  def loadRegion(self, region, filtFunc=None, filterName=None, epoch=None):
412  """Load reference objects within a specified region.
413 
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.
420 
421  Parameters
422  ----------
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
426  be loaded.
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
433  input region.
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.
439 
440  Returns
441  -------
442  referenceCatalog : `lsst.afw.table.SourceCatalog`
443  Catalog containing reference objects which intersect the input region,
444  filtered by the specified filter function.
445 
446  Raises
447  ------
448  RuntimeError
449  Raised if no reference catalogs could be found for the specified
450  region.
451  TypeError
452  Raised if the loaded reference catalogs do not have matching
453  schemas.
454  """
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())
461  if filtFunc is None:
462  filtFunc = _FilterCatalog(region)
463  # filter out all the regions supplied by the constructor that do not overlap
464  overlapList = []
465  for dataId, refCat in zip(self.dataIdsdataIds, self.refCatsrefCats):
466  # SphGeom supports some objects intersecting others, but is not symmetric,
467  # try the intersect operation in both directions
468  try:
469  intersects = dataId.region.intersects(region)
470  except TypeError:
471  intersects = region.intersects(dataId.region)
472 
473  if intersects:
474  overlapList.append((dataId, refCat))
475 
476  if len(overlapList) == 0:
477  raise RuntimeError("No reference tables could be found for input region")
478 
479  firstCat = overlapList[0][1].get()
480  refCat = filtFunc(firstCat, overlapList[0][0].region)
481  trimmedAmount = len(firstCat) - len(refCat)
482 
483  # Load in the remaining catalogs
484  for dataId, inputRefCat in overlapList[1:]:
485  tmpCat = inputRefCat.get()
486 
487  if tmpCat.schema != firstCat.schema:
488  raise TypeError("Reference catalogs have mismatching schemas")
489 
490  filteredCat = filtFunc(tmpCat, dataId.region)
491  refCat.extend(filteredCat)
492  trimmedAmount += len(tmpCat) - len(filteredCat)
493 
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))
497 
498  # Ensure that the loaded reference catalog is continuous in memory
499  if not refCat.isContiguous():
500  refCat = refCat.copy(deep=True)
501 
502  self.applyProperMotionsapplyProperMotions(refCat, epoch)
503 
504  # Verify the schema is in the correct units and has the correct version; automatically convert
505  # it with a warning if this is not the case.
506  if not hasNanojanskyFluxUnits(refCat.schema) or not getFormatVersionFromRefCat(refCat) >= 1:
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.")
510  refCat = convertToNanojansky(refCat, self.loglog)
511 
512  expandedCat = self.remapReferenceCatalogSchemaremapReferenceCatalogSchema(refCat, position=True)
513 
514  # Add flux aliases
515  expandedCat = self.addFluxAliasesaddFluxAliases(expandedCat, self.configconfig.defaultFilter, self.configconfig.filterMap)
516 
517  # Ensure that the returned reference catalog is continuous in memory
518  if not expandedCat.isContiguous():
519  expandedCat = expandedCat.copy(deep=True)
520 
521  fluxField = getRefFluxField(schema=expandedCat.schema, filterName=filterName)
522  return pipeBase.Struct(refCat=expandedCat, fluxField=fluxField)
523 
524  def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None):
525  """Load reference objects that lie within a circular region on the sky.
526 
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.
531 
532  Parameters
533  ----------
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.
543 
544  Returns
545  -------
546  referenceCatalog : `lsst.afw.table.SourceCatalog`
547  Catalog containing reference objects inside the specified search
548  circle.
549  """
550  centerVector = ctrCoord.getVector()
551  sphRadius = sphgeom.Angle(radius.asRadians())
552  circularRegion = sphgeom.Circle(centerVector, sphRadius)
553  return self.loadRegionloadRegion(circularRegion, filterName=filterName, epoch=epoch)
554 
555  def joinMatchListWithCatalog(self, matchCat, sourceCat):
556  """Relink an unpersisted match list to sources and reference objects.
557 
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.
563 
564  Parameters
565  ----------
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
572  by ID.
573 
574  Returns
575  -------
576  matchList : `lsst.afw.table.ReferenceMatchVector`
577  Match list.
578  """
579  return joinMatchListWithCatalogImpl(self, matchCat, sourceCat)
580 
581  def getMetadataBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None,
582  bboxToSpherePadding=100):
583  """Return metadata about the load
584 
585  This metadata is used for reloading the catalog (e.g., for
586  reconstituting a normalised match list.)
587 
588  Parameters
589  ----------
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
596  filter.
597  photoCalib : `None`
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.
606 
607  Returns
608  -------
609  md : `lsst.daf.base.PropertyList`
610  The metadata detailing the search parameters used for this
611  dataset.
612  """
613  paddedBbox = geom.Box2D(bbox)
614  paddedBbox.grow(self.configconfig.pixelMargin)
615  _, _, innerCorners, outerCorners = self._makeBoxRegion_makeBoxRegion(paddedBbox, wcs, bboxToSpherePadding)
616  md = PropertyList()
617  for box, corners in zip(("INNER", "OUTER"), (innerCorners, outerCorners)):
618  for (name, corner) in zip(("UPPER_LEFT", "UPPER_RIGHT", "LOWER_LEFT", "LOWER_RIGHT"),
619  corners):
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')
626  return md
627 
628  @staticmethod
629  def getMetadataCircle(coord, radius, filterName, photoCalib=None, epoch=None):
630  """Return metadata about the load.
631 
632  This metadata is used for reloading the catalog (e.g. for
633  reconstituting a normalized match list.)
634 
635  Parameters
636  ----------
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
643  filter.
644  photoCalib : `None`
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.
649 
650  Returns
651  -------
652  md : `lsst.daf.base.PropertyList`
653  """
654  md = 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')
662  return md
663 
664  @staticmethod
665  def addFluxAliases(refCat, defaultFilter, filterReferenceMap):
666  """Add flux columns and aliases for camera to reference mapping.
667 
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
670  fluxes.
671 
672  Parameters
673  ----------
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.
681 
682  Returns
683  -------
684  refCat : `lsst.afw.table.SimpleCatalog`
685  Reference catalog with columns added to track reference filters.
686 
687  Raises
688  ------
689  `RuntimeError`
690  If the specified reference filter name is not specifed as a
691  key in the reference filter map.
692  """
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()):
700  if refFilterName:
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)
706 
707  refFluxErrName = refFluxName + "Err"
708  camFluxErrName = camFluxName + "Err"
709  aliasMap.set(camFluxErrName, refFluxErrName)
710 
711  return refCat
712 
713  @staticmethod
714  def remapReferenceCatalogSchema(refCat, *, filterNameList=None, position=False, photometric=False):
715  """This function takes in a reference catalog and creates a new catalog with additional
716  columns defined the remaining function arguments.
717 
718  Parameters
719  ----------
720  refCat : `lsst.afw.table.SimpleCatalog`
721  Reference catalog to map to new catalog
722 
723  Returns
724  -------
725  expandedCat : `lsst.afw.table.SimpleCatalog`
726  Deep copy of input reference catalog with additional columns added
727  """
728  mapper = afwTable.SchemaMapper(refCat.schema, True)
729  mapper.addMinimalSchema(refCat.schema, True)
730  mapper.editOutputSchema().disconnectAliases()
731  if filterNameList:
732  for filterName in filterNameList:
733  mapper.editOutputSchema().addField(f"{filterName}_flux",
734  type=numpy.float64,
735  doc=f"flux in filter {filterName}",
736  units="Jy"
737  )
738  mapper.editOutputSchema().addField(f"{filterName}_fluxErr",
739  type=numpy.float64,
740  doc=f"flux uncertanty in filter {filterName}",
741  units="Jy"
742  )
743 
744  if position:
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")
749 
750  if photometric:
751  mapper.editOutputSchema().addField("photometric",
752  type="Flag",
753  doc="set if the object can be used for photometric"
754  "calibration",
755  )
756  mapper.editOutputSchema().addField("resolved",
757  type="Flag",
758  doc="set if the object is spatially resolved"
759  )
760  mapper.editOutputSchema().addField("variable",
761  type="Flag",
762  doc="set if the object has variable brightness"
763  )
764 
765  expandedCat = afwTable.SimpleCatalog(mapper.getOutputSchema())
766  expandedCat.setMetadata(refCat.getMetadata())
767  expandedCat.extend(refCat, mapper=mapper)
768 
769  return expandedCat
770 
771 
772 def getRefFluxField(schema, filterName=None):
773  """Get the name of a flux field from a schema.
774 
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
781  else:
782  return "camFlux", if present,
783  else throw RuntimeError
784 
785  Parameters
786  ----------
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.
792 
793  Returns
794  -------
795  fluxFieldName : `str`
796  Name of flux field.
797 
798  Raises
799  ------
800  RuntimeError
801  If an appropriate field is not found.
802  """
803  if not isinstance(schema, afwTable.Schema):
804  raise RuntimeError("schema=%s is not a schema" % (schema,))
805  try:
806  return schema.getAliasMap().get("anyFilterMapsToThis")
807  except LookupError:
808  pass # try the filterMap next
809 
810  if filterName:
811  fluxFieldList = [filterName + "_camFlux", filterName + "_flux"]
812  else:
813  fluxFieldList = ["camFlux"]
814  for fluxField in fluxFieldList:
815  if fluxField in schema:
816  return fluxField
817 
818  raise RuntimeError("Could not find flux field(s) %s" % (", ".join(fluxFieldList)))
819 
820 
821 def getRefFluxKeys(schema, filterName=None):
822  """Return keys for flux and flux error.
823 
824  Parameters
825  ----------
826  schema : `lsst.afw.table.Schema`
827  Reference catalog schema.
828  filterName : `str`
829  Name of camera filter.
830 
831  Returns
832  -------
833  keys : `tuple` of (`lsst.afw.table.Key`, `lsst.afw.table.Key`)
834  Two keys:
835 
836  - flux key
837  - flux error key, if present, else None
838 
839  Raises
840  ------
841  RuntimeError
842  If flux field not found.
843  """
844  fluxField = getRefFluxField(schema, filterName)
845  fluxErrField = fluxField + "Err"
846  fluxKey = schema[fluxField].asKey()
847  try:
848  fluxErrKey = schema[fluxErrField].asKey()
849  except Exception:
850  fluxErrKey = None
851  return (fluxKey, fluxErrKey)
852 
853 
854 class LoadReferenceObjectsConfig(pexConfig.Config):
855  pixelMargin = pexConfig.RangeField(
856  doc="Padding to add to 4 all edges of the bounding box (pixels)",
857  dtype=int,
858  default=250,
859  min=0,
860  )
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."),
864  dtype=str,
865  default="",
866  deprecated="defaultFilter is deprecated by RFC-716. Will be removed after v22."
867  )
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."),
874  dtype=str,
875  default=None,
876  optional=True
877  )
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."),
882  keytype=str,
883  itemtype=str,
884  default={},
885  )
886  requireProperMotion = pexConfig.Field(
887  doc="Require that the fields needed to correct proper motion "
888  "(epoch, pm_ra and pm_dec) are present?",
889  dtype=bool,
890  default=False,
891  )
892 
893  def validate(self):
894  super().validate()
895  if self.filterMapfilterMap != {} and self.anyFilterMapsToThisanyFilterMapsToThis is not None:
896  msg = "`filterMap` and `anyFilterMapsToThis` are mutually exclusive"
897  raise pexConfig.FieldValidationError(LoadReferenceObjectsConfig.anyFilterMapsToThis,
898  self, msg)
899 
900 
901 class LoadReferenceObjectsTask(pipeBase.Task, ReferenceObjectLoaderBase, metaclass=abc.ABCMeta):
902  """Abstract base class to load objects from reference catalogs.
903  """
904  ConfigClass = LoadReferenceObjectsConfig
905  _DefaultName = "LoadReferenceObjects"
906 
907  def __init__(self, butler=None, *args, **kwargs):
908  """Construct a LoadReferenceObjectsTask
909 
910  Parameters
911  ----------
912  butler : `lsst.daf.persistence.Butler`
913  Data butler, for access reference catalogs.
914  """
915  pipeBase.Task.__init__(self, *args, **kwargs)
916  self.butlerbutler = butler
917 
918  @timeMethod
919  def loadPixelBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None):
920  """Load reference objects that overlap a rectangular pixel region.
921 
922  Parameters
923  ----------
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
928  and vice-versa.
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).
933  photoCalib : `None`
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.
938 
939  Returns
940  -------
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.
948  fluxField : `str`
949  Name of flux field for specified `filterName`.
950 
951  Notes
952  -----
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.
957  """
958  circle = self._calculateCircle_calculateCircle(bbox, wcs)
959 
960  # find objects in circle
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,
964  centroids=True)
965  refCat = loadRes.refCat
966  numFound = len(refCat)
967 
968  # trim objects outside bbox
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))
973 
974  # make sure catalog is contiguous
975  if not refCat.isContiguous():
976  loadRes.refCat = refCat.copy(deep=True)
977 
978  return loadRes
979 
980  @abc.abstractmethod
981  def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None, centroids=False):
982  """Load reference objects that overlap a circular sky region.
983 
984  Parameters
985  ----------
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.
1000 
1001  Returns
1002  -------
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.
1010  fluxField : `str`
1011  Name of flux field for specified `filterName`.
1012 
1013  Notes
1014  -----
1015  Note that subclasses are responsible for performing the proper motion
1016  correction, since this is the lowest-level interface for retrieving
1017  the catalog.
1018  """
1019  return
1020 
1021  @staticmethod
1022  def _trimToBBox(refCat, bbox, wcs):
1023  """Remove objects outside a given pixel bounding box and set
1024  centroid and hasCentroid fields.
1025 
1026  Parameters
1027  ----------
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`
1034  Pixel region
1035  wcs : `lsst.afw.geom.SkyWcs`
1036  WCS; used to convert sky coordinates to pixel positions.
1037 
1038  Returns
1039  -------
1040  catalog : `lsst.afw.table.SimpleCatalog`
1041  Reference objects in the bbox, with centroid and
1042  hasCentroid fields set.
1043  """
1044  afwTable.updateRefCentroids(wcs, refCat)
1045  centroidKey = afwTable.Point2DKey(refCat.schema["centroid"])
1046  retStarCat = type(refCat)(refCat.table)
1047  for star in refCat:
1048  point = star.get(centroidKey)
1049  if bbox.contains(point):
1050  retStarCat.append(star)
1051  return retStarCat
1052 
1053  def _addFluxAliases(self, schema):
1054  """Add aliases for camera filter fluxes to the schema.
1055 
1056  If self.config.defaultFilter then adds these aliases:
1057  camFlux: <defaultFilter>_flux
1058  camFluxErr: <defaultFilter>_fluxErr, if the latter exists
1059 
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
1063 
1064  Parameters
1065  ----------
1066  schema : `lsst.afw.table.Schema`
1067  Schema for reference catalog.
1068 
1069  Raises
1070  ------
1071  RuntimeError
1072  If any reference flux field is missing from the schema.
1073  """
1074  aliasMap = schema.getAliasMap()
1075 
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)
1082  return # this is mutually exclusive with filterMap
1083 
1084  def addAliasesForOneFilter(filterName, refFilterName):
1085  """Add aliases for a single filter
1086 
1087  Parameters
1088  ----------
1089  filterName : `str` (optional)
1090  Camera filter name. The resulting alias name is
1091  <filterName>_camFlux, or simply "camFlux" if `filterName`
1092  is `None` or `""`.
1093  refFilterName : `str`
1094  Reference catalog filter name; the field
1095  <refFilterName>_flux must exist.
1096  """
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)
1106 
1107  if self.config.defaultFilter:
1108  addAliasesForOneFilter(None, self.config.defaultFilter)
1109 
1110  for filterName, refFilterName in self.config.filterMap.items():
1111  addAliasesForOneFilter(filterName, refFilterName)
1112 
1113  @staticmethod
1114  def makeMinimalSchema(filterNameList, *, addCentroid=False,
1115  addIsPhotometric=False, addIsResolved=False,
1116  addIsVariable=False, coordErrDim=2,
1117  addProperMotion=False, properMotionErrDim=2,
1118  addParallax=False):
1119  """Make a standard schema for reference object catalogs.
1120 
1121  Parameters
1122  ----------
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".
1131  coordErrDim : `int`
1132  Number of coord error fields; must be one of 0, 2, 3:
1133 
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".
1146 
1147  Returns
1148  -------
1149  schema : `lsst.afw.table.Schema`
1150  Schema for reference catalog, an
1151  `lsst.afw.table.SimpleCatalog`.
1152 
1153  Notes
1154  -----
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.
1159  """
1160  schema = afwTable.SimpleTable.makeMinimalSchema()
1161  if addCentroid:
1162  afwTable.Point2DKey.addFields(
1163  schema,
1164  "centroid",
1165  "centroid on an exposure, if relevant",
1166  "pixel",
1167  )
1168  schema.addField(
1169  field="hasCentroid",
1170  type="Flag",
1171  doc="is position known?",
1172  )
1173  for filterName in filterNameList:
1174  schema.addField(
1175  field="%s_flux" % (filterName,),
1176  type=numpy.float64,
1177  doc="flux in filter %s" % (filterName,),
1178  units="nJy",
1179  )
1180  for filterName in filterNameList:
1181  schema.addField(
1182  field="%s_fluxErr" % (filterName,),
1183  type=numpy.float64,
1184  doc="flux uncertainty in filter %s" % (filterName,),
1185  units="nJy",
1186  )
1187  if addIsPhotometric:
1188  schema.addField(
1189  field="photometric",
1190  type="Flag",
1191  doc="set if the object can be used for photometric calibration",
1192  )
1193  if addIsResolved:
1194  schema.addField(
1195  field="resolved",
1196  type="Flag",
1197  doc="set if the object is spatially resolved",
1198  )
1199  if addIsVariable:
1200  schema.addField(
1201  field="variable",
1202  type="Flag",
1203  doc="set if the object has variable brightness",
1204  )
1205  if coordErrDim not in (0, 2, 3):
1206  raise ValueError("coordErrDim={}; must be (0, 2, 3)".format(coordErrDim))
1207  if coordErrDim > 0:
1208  afwTable.CovarianceMatrix2fKey.addFields(
1209  schema=schema,
1210  prefix="coord",
1211  names=["ra", "dec"],
1212  units=["rad", "rad"],
1213  diagonalOnly=(coordErrDim == 2),
1214  )
1215 
1216  if addProperMotion or addParallax:
1217  schema.addField(
1218  field="epoch",
1219  type=numpy.float64,
1220  doc="date of observation (TAI, MJD)",
1221  units="day",
1222  )
1223 
1224  if addProperMotion:
1225  schema.addField(
1226  field="pm_ra",
1227  type="Angle",
1228  doc="proper motion in the right ascension direction = dra/dt * cos(dec)",
1229  units="rad/year",
1230  )
1231  schema.addField(
1232  field="pm_dec",
1233  type="Angle",
1234  doc="proper motion in the declination direction",
1235  units="rad/year",
1236  )
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(
1241  schema=schema,
1242  prefix="pm",
1243  names=["ra", "dec"],
1244  units=["rad/year", "rad/year"],
1245  diagonalOnly=(properMotionErrDim == 2),
1246  )
1247  schema.addField(
1248  field="pm_flag",
1249  type="Flag",
1250  doc="Set if proper motion or proper motion error is bad",
1251  )
1252 
1253  if addParallax:
1254  schema.addField(
1255  field="parallax",
1256  type="Angle",
1257  doc="parallax",
1258  units="rad",
1259  )
1260  schema.addField(
1261  field="parallaxErr",
1262  type="Angle",
1263  doc="uncertainty in parallax",
1264  units="rad",
1265  )
1266  schema.addField(
1267  field="parallax_flag",
1268  type="Flag",
1269  doc="Set if parallax or parallax error is bad",
1270  )
1271  return schema
1272 
1273  def _calculateCircle(self, bbox, wcs):
1274  """Compute on-sky center and radius of search region.
1275 
1276  Parameters
1277  ----------
1278  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
1279  Pixel bounding box.
1280  wcs : `lsst.afw.geom.SkyWcs`
1281  WCS; used to convert pixel positions to sky coordinates.
1282 
1283  Returns
1284  -------
1285  results : `lsst.pipe.base.Struct`
1286  A Struct containing:
1287 
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.
1294  """
1295  bbox = geom.Box2D(bbox) # make sure bbox is double and that we have a copy
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)
1300 
1301  def getMetadataBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None):
1302  """Return metadata about the load.
1303 
1304  This metadata is used for reloading the catalog (e.g., for
1305  reconstituting a normalised match list.
1306 
1307  Parameters
1308  ----------
1309  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
1310  Pixel bounding box.
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
1315  filter.
1316  photoCalib : `None`
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.
1321 
1322  Returns
1323  -------
1324  metadata : `lsst.daf.base.PropertyList`
1325  Metadata about the load.
1326  """
1327  circle = self._calculateCircle_calculateCircle(bbox, wcs)
1328  return self.getMetadataCirclegetMetadataCircle(circle.coord, circle.radius, filterName, epoch=epoch)
1329 
1330  def getMetadataCircle(self, coord, radius, filterName, photoCalib=None, epoch=None):
1331  """Return metadata about the load.
1332 
1333  This metadata is used for reloading the catalog (e.g., for
1334  reconstituting a normalised match list.
1335 
1336  Parameters
1337  ----------
1338  coord : `lsst.geom.SpherePoint`
1339  ICRS center of the search region.
1340  radius : `lsst.geom.Angle`
1341  Radius of the search region.
1342  filterName : `str`
1343  Name of camera filter, or `None` or `""` for the default
1344  filter.
1345  photoCalib : `None`
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.
1350 
1351  Returns
1352  -------
1353  metadata : lsst.daf.base.PropertyList
1354  Metadata about the load
1355  """
1356  md = PropertyList()
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')
1364  return md
1365 
1366  def joinMatchListWithCatalog(self, matchCat, sourceCat):
1367  """Relink an unpersisted match list to sources and reference
1368  objects.
1369 
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.
1375 
1376  Parameters
1377  ----------
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
1384  by ID.
1385 
1386  Returns
1387  -------
1388  matchList : `lsst.afw.table.ReferenceMatchVector`
1389  Match list.
1390  """
1391  return joinMatchListWithCatalogImpl(self, matchCat, sourceCat)
1392 
1393 
1394 def joinMatchListWithCatalogImpl(refObjLoader, matchCat, sourceCat):
1395  """Relink an unpersisted match list to sources and reference
1396  objects.
1397 
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.
1403 
1404  Parameters
1405  ----------
1406  refObjLoader
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
1414  by ID.
1415 
1416  Returns
1417  -------
1418  matchList : `lsst.afw.table.ReferenceMatchVector`
1419  Match list.
1420  """
1421  matchmeta = matchCat.table.getMetadata()
1422  version = matchmeta.getInt('SMATCHV')
1423  if version != 1:
1424  raise ValueError('SourceMatchVector version number is %i, not 1.' % version)
1425  filterName = matchmeta.getString('FILTER').strip()
1426  try:
1427  epoch = matchmeta.getDouble('EPOCH')
1428  except (LookupError, TypeError):
1429  epoch = None # Not present, or not correct type means it's not set
1430  if 'RADIUS' in matchmeta:
1431  # This is a circle style metadata, call loadSkyCircle
1432  ctrCoord = geom.SpherePoint(matchmeta.getDouble('RA'),
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:
1437  # This is the sky box type (only triggers in the LoadReferenceObject class, not task)
1438  # Only the outer box is required to be loaded to get the maximum region, all filtering
1439  # will be done by the unpackMatches function, and no spatial filtering needs to be done
1440  # by the refObjLoader
1441  box = []
1442  for place in ("UPPER_LEFT", "UPPER_RIGHT", "LOWER_LEFT", "LOWER_RIGHT"):
1443  coord = geom.SpherePoint(matchmeta.getDouble(f"OUTER_{place}_RA"),
1444  matchmeta.getDouble(f"OUTER_{place}_DEC"),
1445  geom.degrees).getVector()
1446  box.append(coord)
1447  outerBox = sphgeom.ConvexPolygon(box)
1448  refCat = refObjLoader.loadRegion(outerBox, filterName=filterName, epoch=epoch).refCat
1449 
1450  refCat.sort()
1451  sourceCat.sort()
1452  return afwTable.unpackMatches(matchCat, refCat, sourceCat)
1453 
1454 
1455 def applyProperMotionsImpl(log, catalog, epoch):
1456  """Apply proper motion correction to a reference catalog.
1457 
1458  Adjust position and position error in the ``catalog``
1459  for proper motion to the specified ``epoch``,
1460  modifying the catalog in place.
1461 
1462  Parameters
1463  ----------
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:
1468 
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,
1473  East positive)
1474  - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
1475  - ``pm_dec`` : Proper motion in Declination (rad/yr,
1476  North positive)
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.
1481  """
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")
1484  return
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)
1489  # Use `epoch.tai` to make sure the time difference is in TAI
1490  timeDiffsYears = (epoch.tai - catEpoch).to(astropy.units.yr).value
1491  coordKey = catalog.table.getCoordKey()
1492  # Compute the offset of each object due to proper motion
1493  # as components of the arc of a great circle along RA and Dec
1494  pmRaRad = catalog["pm_ra"]
1495  pmDecRad = catalog["pm_dec"]
1496  offsetsRaRad = pmRaRad*timeDiffsYears
1497  offsetsDecRad = pmDecRad*timeDiffsYears
1498  # Compute the corresponding bearing and arc length of each offset
1499  # due to proper motion, and apply the offset
1500  # The factor of 1e6 for computing bearing is intended as
1501  # a reasonable scale for typical values of proper motion
1502  # in order to avoid large errors for small values of proper motion;
1503  # using the offsets is another option, but it can give
1504  # needlessly large errors for short duration
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))
1511  # Increase error in RA and Dec based on error in proper motion
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)
int max
table::Key< int > type
Definition: Detector.cc:163
table::Key< int > to
Defines the fields and offsets for a table.
Definition: Schema.h:51
A mapping between the keys of two Schemas, used to copy data between them.
Definition: SchemaMapper.h:21
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Definition: SortedCatalog.h:42
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
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 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 loadRegion(self, region, filtFunc=None, filterName=None, epoch=None)
def getMetadataCircle(coord, radius, filterName, photoCalib=None, epoch=None)
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)
Angle represents an angle in radians.
Definition: Angle.h:43
Circle is a circular region on the unit sphere that contains its boundary.
Definition: Circle.h:46
ConvexPolygon is a closed convex polygon on the unit sphere.
Definition: ConvexPolygon.h:57
daf::base::PropertySet * set
Definition: fits.cc:912
bool strip
Definition: fits.cc:911
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.
Definition: Match.cc:455
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
Update centroids in a collection of reference objects.
Definition: wcsUtils.cc:72
def convertToNanojansky(catalog, log, doConvert=True)
def joinMatchListWithCatalogImpl(refObjLoader, matchCat, sourceCat)
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
A description of a field in a table.
Definition: Field.h:24