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