LSSTApplications  18.0.0+106,18.0.0+50,19.0.0,19.0.0+1,19.0.0+10,19.0.0+11,19.0.0+13,19.0.0+17,19.0.0+2,19.0.0-1-g20d9b18+6,19.0.0-1-g425ff20,19.0.0-1-g5549ca4,19.0.0-1-g580fafe+6,19.0.0-1-g6fe20d0+1,19.0.0-1-g7011481+9,19.0.0-1-g8c57eb9+6,19.0.0-1-gb5175dc+11,19.0.0-1-gdc0e4a7+9,19.0.0-1-ge272bc4+6,19.0.0-1-ge3aa853,19.0.0-10-g448f008b,19.0.0-12-g6990b2c,19.0.0-2-g0d9f9cd+11,19.0.0-2-g3d9e4fb2+11,19.0.0-2-g5037de4,19.0.0-2-gb96a1c4+3,19.0.0-2-gd955cfd+15,19.0.0-3-g2d13df8,19.0.0-3-g6f3c7dc,19.0.0-4-g725f80e+11,19.0.0-4-ga671dab3b+1,19.0.0-4-gad373c5+3,19.0.0-5-ga2acb9c+2,19.0.0-5-gfe96e6c+2,w.2020.01
LSSTDataManagementBasePackage
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
35 import lsst.afw.table as afwTable
36 import lsst.pex.config as pexConfig
37 import lsst.pex.exceptions as pexExceptions
38 import lsst.pipe.base as pipeBase
39 import lsst.pex.exceptions as pexExcept
40 import lsst.log
41 from lsst import geom
42 from lsst import sphgeom
43 from lsst.daf.base import PropertyList
44 
45 
46 def isOldFluxField(name, units):
47  """Return True if this name/units combination corresponds to an
48  "old-style" reference catalog flux field.
49  """
50  unitsCheck = units != 'nJy' # (units == 'Jy' or units == '' or units == '?')
51  isFlux = name.endswith('_flux')
52  isFluxSigma = name.endswith('_fluxSigma')
53  isFluxErr = name.endswith('_fluxErr')
54  return (isFlux or isFluxSigma or isFluxErr) and unitsCheck
55 
56 
58  """Return True if the units of all flux and fluxErr are correct (nJy).
59  """
60  for field in schema:
61  if isOldFluxField(field.field.getName(), field.field.getUnits()):
62  return False
63  return True
64 
65 
67  """"Return the format version stored in a reference catalog header.
68 
69  Parameters
70  ----------
71  refCat : `lsst.afw.table.SimpleCatalog`
72  Reference catalog to inspect.
73 
74  Returns
75  -------
76  version : `int` or `None`
77  Format version integer, or `None` if the catalog has no metadata
78  or the metadata does not include a "REFCAT_FORMAT_VERSION" key.
79  """
80  md = refCat.getMetadata()
81  if md is None:
82  return None
83  try:
84  return md.getScalar("REFCAT_FORMAT_VERSION")
85  except KeyError:
86  return None
87 
88 
89 def convertToNanojansky(catalog, log, doConvert=True):
90  """Convert fluxes in a catalog from jansky to nanojansky.
91 
92  Parameters
93  ----------
94  catalog : `lsst.afw.table.SimpleCatalog`
95  The catalog to convert.
96  log : `lsst.log.Log`
97  Log to send messages to.
98  doConvert : `bool`, optional
99  Return a converted catalog, or just identify the fields that need to be converted?
100  This supports the "write=False" mode of `bin/convert_to_nJy.py`.
101 
102  Returns
103  -------
104  catalog : `lsst.afw.table.SimpleCatalog` or None
105  The converted catalog, or None if ``doConvert`` is False.
106 
107  Notes
108  -----
109  Support for old units in reference catalogs will be removed after the
110  release of late calendar year 2019.
111  Use `meas_algorithms/bin/convert_to_nJy.py` to update your reference catalog.
112  """
113  # Do not share the AliasMap: for refcats, that gets created when the
114  # catalog is read from disk and should not be propagated.
115  mapper = lsst.afw.table.SchemaMapper(catalog.schema, shareAliasMap=False)
116  mapper.addMinimalSchema(lsst.afw.table.SimpleTable.makeMinimalSchema())
117  input_fields = []
118  output_fields = []
119  for field in catalog.schema:
120  oldName = field.field.getName()
121  oldUnits = field.field.getUnits()
122  if isOldFluxField(oldName, oldUnits):
123  units = 'nJy'
124  # remap Sigma flux fields to Err, so we can drop the alias
125  if oldName.endswith('_fluxSigma'):
126  name = oldName.replace('_fluxSigma', '_fluxErr')
127  else:
128  name = oldName
129  newField = lsst.afw.table.Field[field.dtype](name, field.field.getDoc(), units)
130  mapper.addMapping(field.getKey(), newField)
131  input_fields.append(field.field)
132  output_fields.append(newField)
133  else:
134  mapper.addMapping(field.getKey())
135 
136  fluxFieldsStr = '; '.join("(%s, '%s')" % (field.getName(), field.getUnits()) for field in input_fields)
137 
138  if doConvert:
139  newSchema = mapper.getOutputSchema()
140  output = lsst.afw.table.SimpleCatalog(newSchema)
141  output.extend(catalog, mapper=mapper)
142  for field in output_fields:
143  output[field.getName()] *= 1e9
144  log.info(f"Converted refcat flux fields to nJy (name, units): {fluxFieldsStr}")
145  return output
146  else:
147  log.info(f"Found old-style refcat flux fields (name, units): {fluxFieldsStr}")
148  return None
149 
150 
152  """This is a private helper class which filters catalogs by
153  row based on the row being inside the region used to initialize
154  the class.
155 
156  Parameters
157  ----------
158  region : `lsst.sphgeom.Region`
159  The spatial region which all objects should lie within
160  """
161  def __init__(self, region):
162  self.region = region
163 
164  def __call__(self, refCat, catRegion):
165  """This call method on an instance of this class takes in a reference
166  catalog, and the region from which the catalog was generated.
167 
168  If the catalog region is entirely contained within the region used to
169  initialize this class, then all the entries in the catalog must be
170  within the region and so the whole catalog is returned.
171 
172  If the catalog region is not entirely contained, then the location for
173  each record is tested against the region used to initialize the class.
174  Records which fall inside this region are added to a new catalog, and
175  this catalog is then returned.
176 
177  Parameters
178  ---------
179  refCat : `lsst.afw.table.SourceCatalog`
180  SourceCatalog to be filtered.
181  catRegion : `lsst.sphgeom.Region`
182  Region in which the catalog was created
183  """
184  if catRegion.isWithin(self.region):
185  # no filtering needed, region completely contains refcat
186  return refCat
187 
188  filteredRefCat = type(refCat)(refCat.table)
189  for record in refCat:
190  if self.region.contains(record.getCoord().getVector()):
191  filteredRefCat.append(record)
192  return filteredRefCat
193 
194 
196  """ This class facilitates loading reference catalogs with gen 3 middleware
197 
198  The middleware preflight solver will create a list of datarefs that may
199  possibly overlap a given region. These datarefs are then used to construct
200  and instance of this class. The class instance should then be passed into
201  a task which needs reference catalogs. These tasks should then determine
202  the exact region of the sky reference catalogs will be loaded for, and
203  call a corresponding method to load the reference objects.
204  """
205  def __init__(self, dataIds, refCats, config, log=None):
206  """ Constructs an instance of ReferenceObjectLoader
207 
208  Parameters
209  ----------
210  dataIds : iterable of `lsst.daf.butler.DataIds`
211  An iterable object of DataSetRefs which point to reference catalogs
212  in a gen 3 repository
213  refCats : Iterable of `lsst.daf.butler.DeferedDatasetHandle`
214  Handles to load refCats on demand
215  log : `lsst.log.Log`
216  Logger object used to write out messages. If `None` (default) the default
217  lsst logger will be used
218 
219  """
220  self.dataIds = dataIds
221  self.refCats = refCats
223  self.config = config
224 
225  @staticmethod
226  def _makeBoxRegion(BBox, wcs, BBoxPadding):
227  outerLocalBBox = geom.Box2D(BBox)
228  innerLocalBBox = geom.Box2D(BBox)
229 
230  # Grow the bounding box to make sure the spherical geometry bbox will contain
231  # the same region, as non-padded boxes may contain different regions because of optical distortion.
232  # Also create an inner region that is sure to be inside the bbox
233  outerLocalBBox.grow(BBoxPadding)
234  innerLocalBBox.grow(-1*BBoxPadding)
235 
236  # Handle the fact that the inner bounding box shrunk to a zero sized region in at least one
237  # dimension, in which case all reference catalogs must be checked fully against the input
238  # bounding box
239  if innerLocalBBox.getDimensions() == geom.Extent2D(0, 0):
240  innerSkyRegion = sphgeom.Box()
241  else:
242  innerBoxCorners = innerLocalBBox.getCorners()
243  innerSphCorners = [wcs.pixelToSky(corner).getVector() for corner in innerBoxCorners]
244 
245  innerSkyRegion = sphgeom.ConvexPolygon(innerSphCorners)
246 
247  # Convert the corners of the box to sky coordinates
248  outerBoxCorners = outerLocalBBox.getCorners()
249  outerSphCorners = [wcs.pixelToSky(corner).getVector() for corner in outerBoxCorners]
250 
251  outerSkyRegion = sphgeom.ConvexPolygon(outerSphCorners)
252 
253  return innerSkyRegion, outerSkyRegion, innerSphCorners, outerSphCorners
254 
255  def loadPixelBox(self, bbox, wcs, filterName=None, epoch=None, photoCalib=None, bboxPadding=100):
256  """Load reference objects that are within a pixel-based rectangular region
257 
258  This algorithm works by creating a spherical box whose corners correspond
259  to the WCS converted corners of the input bounding box (possibly padded).
260  It then defines a filtering function which will look at a reference
261  objects pixel position and accept objects that lie within the specified
262  bounding box.
263 
264  The spherical box region and filtering function are passed to the generic
265  loadRegion method which will load and filter the reference objects from
266  the datastore and return a single catalog containing all reference objects
267 
268  Parameters
269  ----------
270  bbox : `lsst.geom.box2I`
271  Box which bounds a region in pixel space
272  wcs : `lsst.afw.geom.SkyWcs`
273  Wcs object defining the pixel to sky (and inverse) transform for the space
274  of pixels of the supplied bbox
275  filterName : `str`
276  Name of camera filter, or None or blank for the default filter
277  epoch : `astropy.time.Time` (optional)
278  Epoch to which to correct proper motion and parallax,
279  or None to not apply such corrections.
280  photoCalib : None
281  Deprecated and ignored, only included for api compatibility
282  bboxPadding : `int`
283  Number describing how much to pad the input bbox by (in pixels), defaults
284  to 100. This parameter is necessary because optical distortions in telescopes
285  can cause a rectangular pixel grid to map into a non "rectangular" spherical
286  region in sky coordinates. This padding is used to create a spherical
287  "rectangle", which will for sure enclose the input box. This padding is only
288  used to determine if the reference catalog for a sky patch will be loaded from
289  the data store, this function will filter out objects which lie within the
290  padded region but fall outside the input bounding box region.
291 
292  Returns
293  -------
294  referenceCatalog : `lsst.afw.table.SimpleCatalog`
295  Catalog containing reference objects inside the specified bounding box
296 
297  Raises
298  ------
299  `lsst.pex.exception.RuntimeError`
300  Raised if no reference catalogs could be found for the specified region
301 
302  `lsst.pex.exception.TypeError`
303  Raised if the loaded reference catalogs do not have matching schemas
304  """
305  innerSkyRegion, outerSkyRegion, _, _ = self._makeBoxRegion(bbox, wcs, bboxPadding)
306 
307  def _filterFunction(refCat, region):
308  # Add columns to the reference catalog relating to center positions and use afwTable
309  # to populate those columns
310  refCat = self.remapReferenceCatalogSchema(refCat, position=True)
311  afwTable.updateRefCentroids(wcs, refCat)
312  # no need to filter the catalog if it is sure that it is entirely contained in the region
313  # defined by given bbox
314  if innerSkyRegion.contains(region):
315  return refCat
316  # Create a new reference catalog, and populate it with records which fall inside the bbox
317  filteredRefCat = type(refCat)(refCat.table)
318  centroidKey = afwTable.Point2DKey(refCat.schema['centroid'])
319  for record in refCat:
320  pixCoords = record[centroidKey]
321  if bbox.contains(geom.Point2I(pixCoords)):
322  filteredRefCat.append(record)
323  return filteredRefCat
324  return self.loadRegion(outerSkyRegion, filtFunc=_filterFunction, epoch=epoch, filterName=filterName)
325 
326  def loadRegion(self, region, filtFunc=None, filterName=None, epoch=None):
327  """ Load reference objects within a specified region
328 
329  This function loads the DataIds used to construct an instance of this class
330  which intersect or are contained within the specified region. The reference
331  catalogs which intersect but are not fully contained within the input region are
332  further filtered by the specified filter function. This function will return a
333  single source catalog containing all reference objects inside the specified region.
334 
335  Parameters
336  ----------
337  region : `lsst.sphgeom.Region`
338  This can be any type that is derived from `lsst.sphgeom.region` and should
339  define the spatial region for which reference objects are to be loaded.
340  filtFunc : callable
341  This optional parameter should be a callable object that takes a reference
342  catalog and its corresponding region as parameters, filters the catalog by
343  some criteria and returns the filtered reference catalog. If the value is
344  left as the default (None) than an internal filter function is used which
345  filters according to if a reference object falls within the input region.
346  filterName : `str`
347  Name of camera filter, or None or blank for the default filter
348  epoch : `astropy.time.Time` (optional)
349  Epoch to which to correct proper motion and parallax,
350  or None to not apply such corrections.
351 
352  Returns
353  -------
354  referenceCatalog : `lsst.afw.table.SourceCatalog`
355  Catalog containing reference objects which intersect the input region,
356  filtered by the specified filter function
357 
358  Raises
359  ------
360  `lsst.pex.exception.RuntimeError`
361  Raised if no reference catalogs could be found for the specified region
362 
363  `lsst.pex.exception.TypeError`
364  Raised if the loaded reference catalogs do not have matching schemas
365 
366  """
367  regionBounding = region.getBoundingBox()
368  self.log.info("Loading reference objects from region bounded by {}, {} lat lon".format(
369  regionBounding.getLat(), regionBounding.getLon()))
370  if filtFunc is None:
371  filtFunc = _FilterCatalog(region)
372  # filter out all the regions supplied by the constructor that do not overlap
373  overlapList = []
374  for dataId, refCat in zip(self.dataIds, self.refCats):
375  # SphGeom supports some objects intersecting others, but is not symmetric,
376  # try the intersect operation in both directions
377  try:
378  intersects = dataId.region.intersects(region)
379  except TypeError:
380  intersects = region.intersects(dataId.region)
381 
382  if intersects:
383  overlapList.append((dataId, refCat))
384 
385  if len(overlapList) == 0:
386  raise pexExceptions.RuntimeError("No reference tables could be found for input region")
387 
388  firstCat = overlapList[0][1].get()
389  refCat = filtFunc(firstCat, overlapList[0][0].region)
390  trimmedAmount = len(firstCat) - len(refCat)
391 
392  # Load in the remaining catalogs
393  for dataId, inputRefCat in overlapList[1:]:
394  tmpCat = inputRefCat.get()
395 
396  if tmpCat.schema != firstCat.schema:
397  raise pexExceptions.TypeError("Reference catalogs have mismatching schemas")
398 
399  filteredCat = filtFunc(tmpCat, dataId.region)
400  refCat.extend(filteredCat)
401  trimmedAmount += len(tmpCat) - len(filteredCat)
402 
403  self.log.debug(f"Trimmed {trimmedAmount} out of region objects, leaving {len(refCat)}")
404  self.log.info(f"Loaded {len(refCat)} reference objects")
405 
406  if epoch is not None and "pm_ra" in refCat.schema:
407  # check for a catalog in a non-standard format
408  if isinstance(refCat.schema["pm_ra"].asKey(), lsst.afw.table.KeyAngle):
409  applyProperMotionsImpl(self.log, refCat, epoch)
410  else:
411  self.log.warn("Catalog pm_ra field is not an Angle; not applying proper motion")
412 
413  # Verify the schema is in the correct units and has the correct version; automatically convert
414  # it with a warning if this is not the case.
415  if not hasNanojanskyFluxUnits(refCat.schema) or not getFormatVersionFromRefCat(refCat) >= 1:
416  self.log.warn("Found version 0 reference catalog with old style units in schema.")
417  self.log.warn("run `meas_algorithms/bin/convert_refcat_to_nJy.py` to convert fluxes to nJy.")
418  self.log.warn("See RFC-575 for more details.")
419  refCat = convertToNanojansky(refCat, self.log)
420 
421  expandedCat = self.remapReferenceCatalogSchema(refCat, position=True)
422 
423  # Add flux aliases
424  expandedCat = self.addFluxAliases(expandedCat, self.config.defaultFilter, self.config.filterMap)
425 
426  # Ensure that the returned reference catalog is continuous in memory
427  if not expandedCat.isContiguous():
428  expandedCat = refCat.copy(deep=True)
429 
430  fluxField = getRefFluxField(schema=expandedCat.schema, filterName=filterName)
431  return pipeBase.Struct(refCat=expandedCat, fluxField=fluxField)
432 
433  def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None):
434  """Load reference objects that lie within a circular region on the sky
435 
436  This method constructs a circular region from an input center and angular radius,
437  loads reference catalogs which are contained in or intersect the circle, and
438  filters reference catalogs which intersect down to objects which lie within
439  the defined circle.
440 
441  Parameters
442  ----------
443  ctrCoord : `lsst.geom.SpherePoint`
444  Point defining the center of the circular region
445  radius : `lsst.geom.Angle`
446  Defines the angular radius of the circular region
447  filterName : `str`
448  Name of camera filter, or None or blank for the default filter
449  epoch : `astropy.time.Time` (optional)
450  Epoch to which to correct proper motion and parallax,
451  or None to not apply such corrections.
452 
453  Returns
454  -------
455  referenceCatalog : `lsst.afw.table.SourceCatalog`
456  Catalog containing reference objects inside the specified bounding box
457 
458  Raises
459  ------
460  `lsst.pex.exception.RuntimeError`
461  Raised if no reference catalogs could be found for the specified region
462 
463  `lsst.pex.exception.TypeError`
464  Raised if the loaded reference catalogs do not have matching schemas
465 
466  """
467  centerVector = ctrCoord.getVector()
468  sphRadius = sphgeom.Angle(radius.asRadians())
469  circularRegion = sphgeom.Circle(centerVector, sphRadius)
470  return self.loadRegion(circularRegion, filterName=filterName, epoch=None)
471 
472  def joinMatchListWithCatalog(self, matchCat, sourceCat):
473  """Relink an unpersisted match list to sources and reference
474  objects.
475 
476  A match list is persisted and unpersisted as a catalog of IDs
477  produced by afw.table.packMatches(), with match metadata
478  (as returned by the astrometry tasks) in the catalog's metadata
479  attribute. This method converts such a match catalog into a match
480  list, with links to source records and reference object records.
481 
482  Parameters
483  ----------
484  matchCat : `lsst.afw.table.BaseCatalog`
485  Unpersisted packed match list.
486  ``matchCat.table.getMetadata()`` must contain match metadata,
487  as returned by the astrometry tasks.
488  sourceCat : `lsst.afw.table.SourceCatalog`
489  Source catalog. As a side effect, the catalog will be sorted
490  by ID.
491 
492  Returns
493  -------
494  matchList : `lsst.afw.table.ReferenceMatchVector`
495  Match list.
496  """
497  return joinMatchListWithCatalogImpl(self, matchCat, sourceCat)
498 
499  @classmethod
500  def getMetadataBox(cls, bbox, wcs, filterName=None, photoCalib=None, epoch=None, bboxPadding=100):
501  """Return metadata about the load
502 
503  This metadata is used for reloading the catalog (e.g., for
504  reconstituting a normalised match list.)
505 
506  Parameters
507  ----------
508  bbox : `lsst.geom.Box2I`
509  Bounding bos for the pixels
510  wcs : `lsst.afw.geom.SkyWcs
511  WCS object
512  filterName : `str` or None
513  filterName of the camera filter, or None or blank for the default filter
514  photoCalib : None
515  Deprecated, only included for api compatibility
516  epoch : `astropy.time.Time` (optional)
517  Epoch to which to correct proper motion and parallax,
518  or None to not apply such corrections.
519  bboxPadding : `int`
520  Number describing how much to pad the input bbox by (in pixels), defaults
521  to 100. This parameter is necessary because optical distortions in telescopes
522  can cause a rectangular pixel grid to map into a non "rectangular" spherical
523  region in sky coordinates. This padding is used to create a spherical
524  "rectangle", which will for sure enclose the input box. This padding is only
525  used to determine if the reference catalog for a sky patch will be loaded from
526  the data store, this function will filter out objects which lie within the
527  padded region but fall outside the input bounding box region.
528  Returns
529  -------
530  md : `lsst.daf.base.PropertyList`
531  """
532  _, _, innerCorners, outerCorners = cls._makeBoxRegion(bbox, wcs, bboxPadding)
533  md = PropertyList()
534  for box, corners in zip(("INNER", "OUTER"), (innerCorners, outerCorners)):
535  for (name, corner) in zip(("UPPER_LEFT", "UPPER_RIGHT", "LOWER_LEFT", "LOWER_RIGHT"),
536  corners):
537  md.add(f"{box}_{name}_RA", geom.SpherePoint(corner).getRa().asDegrees(), f"{box}_corner")
538  md.add(f"{box}_{name}_DEC", geom.SpherePoint(corner).getDec().asDegrees(), f"{box}_corner")
539  md.add("SMATCHV", 1, 'SourceMatchVector version number')
540  filterName = "UNKNOWN" if filterName is None else str(filterName)
541  md.add('FILTER', filterName, 'filter name for photometric data')
542  # Calling str on the epoch is a workaround for code that never worked anyway and was not used
543  # see DM-17438
544  md.add('EPOCH', "NONE" if epoch is None else str(epoch), 'Epoch (TAI MJD) for catalog')
545  return md
546 
547  @staticmethod
548  def getMetadataCircle(coord, radius, filterName, photoCalib=None, epoch=None):
549  """Return metadata about the load
550 
551  This metadata is used for reloading the catalog (e.g. for reconstituting
552  a normalized match list.)
553 
554  Parameters
555  ----------
556  coord : `lsst.geom.SpherePoint`
557  ICRS center of a circle
558  radius : `lsst.geom.angle`
559  radius of a circle
560  filterName : `str` or None
561  filterName of the camera filter, or None or blank for the default filter
562  photoCalib : None
563  Deprecated, only included for api compatibility
564  epoch : `astropy.time.Time` (optional)
565  Epoch to which to correct proper motion and parallax,
566  or None to not apply such corrections.
567 
568  Returns
569  -------
570  md : `lsst.daf.base.PropertyList`
571  """
572  md = PropertyList()
573  md.add('RA', coord.getRa().asDegrees(), 'field center in degrees')
574  md.add('DEC', coord.getDec().asDegrees(), 'field center in degrees')
575  md.add('RADIUS', radius.asDegrees(), 'field radius in degrees, minimum')
576  md.add('SMATCHV', 1, 'SourceMatchVector version number')
577  filterName = "UNKNOWN" if filterName is None else str(filterName)
578  md.add('FILTER', filterName, 'filter name for photometric data')
579  md.add('EPOCH', "NONE" if epoch is None else epoch, 'Epoch (TAI MJD) for catalog')
580  return md
581 
582  @staticmethod
583  def addFluxAliases(refCat, defaultFilter, filterReferenceMap):
584  """This function creates a new catalog containing the information of the input refCat
585  as well as added flux columns and aliases between camera and reference flux.
586 
587  Parameters
588  ----------
589  refCat : `lsst.afw.table.SimpleCatalog`
590  Catalog of reference objects
591  defaultFilter : `str`
592  Name of the default reference filter
593  filterReferenceMap : `dict` of `str`
594  Dictionary with keys corresponding to a filter name, and values which
595  correspond to the name of the reference filter.
596 
597  Returns
598  -------
599  refCat : `lsst.afw.table.SimpleCatalog`
600  Reference catalog with columns added to track reference filters
601 
602  Raises
603  ------
604  `RuntimeError`
605  If specified reference filter name is not a filter specifed as a key in the
606  reference filter map.
607  """
608  refCat = ReferenceObjectLoader.remapReferenceCatalogSchema(refCat,
609  filterNameList=filterReferenceMap.keys())
610  aliasMap = refCat.schema.getAliasMap()
611  if filterReferenceMap is None:
612  filterReferenceMap = {}
613  for filterName, refFilterName in itertools.chain([(None, defaultFilter)],
614  filterReferenceMap.items()):
615  if refFilterName:
616  camFluxName = filterName + "_camFlux" if filterName is not None else "camFlux"
617  refFluxName = refFilterName + "_flux"
618  if refFluxName not in refCat.schema:
619  raise RuntimeError("Unknown reference filter %s" % (refFluxName,))
620  aliasMap.set(camFluxName, refFluxName)
621 
622  refFluxErrName = refFluxName + "Err"
623  camFluxErrName = camFluxName + "Err"
624  aliasMap.set(camFluxErrName, refFluxErrName)
625 
626  return refCat
627 
628  @staticmethod
629  def remapReferenceCatalogSchema(refCat, *, filterNameList=None, position=False, photometric=False):
630  """This function takes in a reference catalog and creates a new catalog with additional
631  columns defined the remaining function arguments.
632 
633  Parameters
634  ----------
635  refCat : `lsst.afw.table.SimpleCatalog`
636  Reference catalog to map to new catalog
637 
638  Returns
639  -------
640  expandedCat : `lsst.afw.table.SimpleCatalog`
641  Deep copy of input reference catalog with additional columns added
642  """
643  mapper = afwTable.SchemaMapper(refCat.schema, True)
644  mapper.addMinimalSchema(refCat.schema, True)
645  mapper.editOutputSchema().disconnectAliases()
646  if filterNameList:
647  for filterName in filterNameList:
648  mapper.editOutputSchema().addField(f"{filterName}_flux",
649  type=numpy.float64,
650  doc=f"flux in filter {filterName}",
651  units="Jy"
652  )
653  mapper.editOutputSchema().addField(f"{filterName}_fluxErr",
654  type=numpy.float64,
655  doc=f"flux uncertanty in filter {filterName}",
656  units="Jy"
657  )
658 
659  if position:
660  mapper.editOutputSchema().addField("centroid_x", type=float, doReplace=True)
661  mapper.editOutputSchema().addField("centroid_y", type=float, doReplace=True)
662  mapper.editOutputSchema().addField("hasCentroid", type="Flag", doReplace=True)
663  mapper.editOutputSchema().getAliasMap().set("slot_Centroid", "centroid")
664 
665  if photometric:
666  mapper.editOutputSchema().addField("photometric",
667  type="Flag",
668  doc="set if the object can be used for photometric" +
669  "calibration",
670  )
671  mapper.editOutputSchema().addField("resolved",
672  type="Flag",
673  doc="set if the object is spatially resolved"
674  )
675  mapper.editOutputSchema().addField("variable",
676  type="Flag",
677  doc="set if the object has variable brightness"
678  )
679 
680  expandedCat = afwTable.SimpleCatalog(mapper.getOutputSchema())
681  expandedCat.setMetadata(refCat.getMetadata())
682  expandedCat.extend(refCat, mapper=mapper)
683 
684  return expandedCat
685 
686 
687 def getRefFluxField(schema, filterName=None):
688  """Get the name of a flux field from a schema.
689 
690  if filterName is specified:
691  return *filterName*_camFlux if present
692  else return *filterName*_flux if present (camera filter name matches reference filter name)
693  else throw RuntimeError
694  else:
695  return camFlux, if present,
696  else throw RuntimeError
697 
698  Parameters
699  ----------
700  schema : `lsst.afw.table.Schema`
701  Reference catalog schema.
702  filterName : `str`
703  Name of camera filter.
704 
705  Returns
706  -------
707  fluxFieldName : `str`
708  Name of flux field.
709 
710  Raises
711  ------
712  RuntimeError
713  If an appropriate field is not found.
714  """
715  if not isinstance(schema, afwTable.Schema):
716  raise RuntimeError("schema=%s is not a schema" % (schema,))
717  if filterName:
718  fluxFieldList = [filterName + "_camFlux", filterName + "_flux"]
719  else:
720  fluxFieldList = ["camFlux"]
721  for fluxField in fluxFieldList:
722  if fluxField in schema:
723  return fluxField
724 
725  raise RuntimeError("Could not find flux field(s) %s" % (", ".join(fluxFieldList)))
726 
727 
728 def getRefFluxKeys(schema, filterName=None):
729  """Return keys for flux and flux error.
730 
731  Parameters
732  ----------
733  schema : `lsst.afw.table.Schema`
734  Reference catalog schema.
735  filterName : `str`
736  Name of camera filter.
737 
738  Returns
739  -------
740  keys : `tuple` of (`lsst.afw.table.Key`, `lsst.afw.table.Key`)
741  Two keys:
742 
743  - flux key
744  - flux error key, if present, else None
745 
746  Raises
747  ------
748  RuntimeError
749  If flux field not found.
750  """
751  fluxField = getRefFluxField(schema, filterName)
752  fluxErrField = fluxField + "Err"
753  fluxKey = schema[fluxField].asKey()
754  try:
755  fluxErrKey = schema[fluxErrField].asKey()
756  except Exception:
757  fluxErrKey = None
758  return (fluxKey, fluxErrKey)
759 
760 
761 class LoadReferenceObjectsConfig(pexConfig.Config):
762  pixelMargin = pexConfig.RangeField(
763  doc="Padding to add to 4 all edges of the bounding box (pixels)",
764  dtype=int,
765  default=300,
766  min=0,
767  )
768  defaultFilter = pexConfig.Field(
769  doc="Default reference catalog filter to use if filter not specified in exposure; "
770  "if blank then filter must be specified in exposure",
771  dtype=str,
772  default="",
773  )
774  filterMap = pexConfig.DictField(
775  doc="Mapping of camera filter name: reference catalog filter name; "
776  "each reference filter must exist",
777  keytype=str,
778  itemtype=str,
779  default={},
780  )
781  requireProperMotion = pexConfig.Field(
782  doc="Require that the fields needed to correct proper motion "
783  "(epoch, pm_ra and pm_dec) are present?",
784  dtype=bool,
785  default=False,
786  )
787 
788 # The following comment block adds a link to this task from the Task Documentation page.
789 
795 
796 
797 class LoadReferenceObjectsTask(pipeBase.Task, metaclass=abc.ABCMeta):
798  r"""!Abstract base class to load objects from reference catalogs
799 
800  @anchor LoadReferenceObjectsTask_
801 
802  @section meas_algorithms_loadReferenceObjects_Contents Contents
803 
804  - @ref meas_algorithms_loadReferenceObjects_Purpose
805  - @ref meas_algorithms_loadReferenceObjects_Initialize
806  - @ref meas_algorithms_loadReferenceObjects_IO
807  - @ref meas_algorithms_loadReferenceObjects_Schema
808  - @ref meas_algorithms_loadReferenceObjects_Config
809 
810  @section meas_algorithms_loadReferenceObjects_Purpose Description
811 
812  Abstract base class for tasks that load objects from a reference catalog
813  in a particular region of the sky.
814 
815  Implementations must subclass this class, override the loadSkyCircle method,
816  and will typically override the value of ConfigClass with a task-specific config class.
817 
818  @section meas_algorithms_loadReferenceObjects_Initialize Task initialisation
819 
820  @copydoc \_\_init\_\_
821 
822  @section meas_algorithms_loadReferenceObjects_IO Invoking the Task
823 
824  @copydoc loadPixelBox
825 
826  @section meas_algorithms_loadReferenceObjects_Schema Schema of the reference object catalog
827 
828  Reference object catalogs are instances of lsst.afw.table.SimpleCatalog with the following schema
829  (other fields may also be present).
830  The units use astropy quantity conventions, so a 2 suffix means squared.
831  See also makeMinimalSchema.
832  - coord: ICRS position of star on sky (an lsst.geom.SpherePoint)
833  - centroid: position of star on an exposure, if relevant (an lsst.afw.Point2D)
834  - hasCentroid: is centroid usable? (a Flag)
835  - *referenceFilterName*_flux: brightness in the specified reference catalog filter (nJy)
836  Note: you can use astropy.units to convert from AB Magnitude to nJy:
837  `u.Magnitude(value, u.ABmag).to_value(u.nJy)`
838  - *referenceFilterName*_fluxErr (optional): brightness standard deviation (nJy);
839  omitted if no data is available; possibly nan if data is available for some objects but not others
840  - camFlux: brightness in default camera filter (nJy); omitted if defaultFilter not specified
841  - camFluxErr: brightness standard deviation for default camera filter;
842  omitted if defaultFilter not specified or standard deviation not available that filter
843  - *cameraFilterName*_camFlux: brightness in specified camera filter (nJy)
844  - *cameraFilterName*_camFluxErr (optional): brightness standard deviation
845  in specified camera filter (nJy); omitted if no data is available;
846  possibly nan if data is available for some objects but not others
847  - photometric (optional): is the object usable for photometric calibration? (a Flag)
848  - resolved (optional): is the object spatially resolved? (a Flag)
849  - variable (optional): does the object have variable brightness? (a Flag)
850  - coord_raErr: uncertainty in `coord` along the direction of right ascension (radian, an Angle)
851  = uncertainty in ra * cos(dec); nan if unknown
852  - coord_decErr: uncertainty in `coord` along the direction of declination (radian, an Angle);
853  nan if unknown
854 
855  The following are optional; fields should only be present if the
856  information is available for at least some objects.
857  Numeric values are `nan` if unknown:
858  - epoch: date of observation as TAI MJD (day)
859 
860  - pm_ra: proper motion along the direction of right ascension (rad/year, an Angle) = dra/dt * cos(dec)
861  - pm_dec: proper motion along the direction of declination (rad/year, and Angle)
862  - pm_raErr: uncertainty in `pm_ra` (rad/year)
863  - pm_decErr: uncertainty in `pm_dec` (rad/year)
864  - pm_ra_dec_Cov: covariance between pm_ra and pm_dec (rad2/year2)
865  - pm_flag: set if proper motion, error or covariance is bad
866 
867  - parallax: parallax (rad, an Angle)
868  - parallaxErr: uncertainty in `parallax` (rad)
869  - parallax_flag: set if parallax value or parallaxErr is bad
870 
871  - coord_ra_pm_ra_Cov: covariance between coord_ra and pm_ra (rad2/year)
872  - coord_ra_pm_dec_Cov: covariance between coord_ra and pm_dec (rad2/year)
873  - coord_ra_parallax_Cov: covariance between coord_ra and parallax (rad2/year)
874  - coord_dec_pm_ra_Cov: covariance between coord_dec and pm_ra (rad2/year)
875  - coord_dec_pm_dec_Cov: covariance between coord_dec and pm_dec (rad2/year)
876  - coord_dec_parallax_Cov: covariance between coord_dec and parallax (rad2/year)
877  - pm_ra_parallax_Cov: covariance between pm_ra and parallax (rad2/year)
878  - pm_dec_parallax_Cov: covariance between pm_dec and parallax (rad2/year)
879 
880  @section meas_algorithms_loadReferenceObjects_Config Configuration parameters
881 
882  See @ref LoadReferenceObjectsConfig for a base set of configuration parameters.
883  Most subclasses will add configuration variables.
884  """
885  ConfigClass = LoadReferenceObjectsConfig
886  _DefaultName = "LoadReferenceObjects"
887 
888  def __init__(self, butler=None, *args, **kwargs):
889  """Construct a LoadReferenceObjectsTask
890 
891  Parameters
892  ----------
893  butler : `lsst.daf.persistence.Butler`
894  Data butler, for access reference catalogs.
895  """
896  pipeBase.Task.__init__(self, *args, **kwargs)
897  self.butler = butler
898 
899  @pipeBase.timeMethod
900  def loadPixelBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None):
901  """Load reference objects that overlap a rectangular pixel region.
902 
903  Parameters
904  ----------
905  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
906  Bounding box for pixels.
907  wcs : `lsst.afw.geom.SkyWcs`
908  WCS; used to convert pixel positions to sky coordinates
909  and vice-versa.
910  filterName : `str`
911  Name of filter, or `None` or `""` for the default filter.
912  This is used for flux values in case we have flux limits
913  (which are not yet implemented).
914  photoCalib : `lsst.afw.image.PhotoCalib` (optional)
915  Calibration, or `None` if unknown.
916  epoch : `astropy.time.Time` (optional)
917  Epoch to which to correct proper motion and parallax,
918  or None to not apply such corrections.
919 
920  Returns
921  -------
922  results : `lsst.pipe.base.Struct`
923  A Struct containing the following fields:
924  refCat : `lsst.afw.catalog.SimpleCatalog`
925  A catalog of reference objects with the standard
926  schema, as documented in the main doc string for
927  `LoadReferenceObjects`.
928  The catalog is guaranteed to be contiguous.
929  fluxField : `str`
930  Name of flux field for specified `filterName`.
931 
932  Notes
933  -----
934  The search algorithm works by searching in a region in sky
935  coordinates whose center is the center of the bbox and radius
936  is large enough to just include all 4 corners of the bbox.
937  Stars that lie outside the bbox are then trimmed from the list.
938  """
939  circle = self._calculateCircle(bbox, wcs)
940 
941  # find objects in circle
942  self.log.info("Loading reference objects using center %s and radius %s deg" %
943  (circle.coord, circle.radius.asDegrees()))
944  loadRes = self.loadSkyCircle(circle.coord, circle.radius, filterName, centroids=True)
945  refCat = loadRes.refCat
946  numFound = len(refCat)
947 
948  # trim objects outside bbox
949  refCat = self._trimToBBox(refCat=refCat, bbox=circle.bbox, wcs=wcs)
950  numTrimmed = numFound - len(refCat)
951  self.log.debug("trimmed %d out-of-bbox objects, leaving %d", numTrimmed, len(refCat))
952  self.log.info("Loaded %d reference objects", len(refCat))
953 
954  # make sure catalog is contiguous
955  if not refCat.isContiguous():
956  loadRes.refCat = refCat.copy(deep=True)
957 
958  return loadRes
959 
960  @abc.abstractmethod
961  def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None, centroids=False):
962  """Load reference objects that overlap a circular sky region.
963 
964  Parameters
965  ----------
966  ctrCoord : `lsst.geom.SpherePoint`
967  ICRS center of search region.
968  radius : `lsst.geom.Angle`
969  Radius of search region.
970  filterName : `str` (optional)
971  Name of filter, or `None` or `""` for the default filter.
972  This is used for flux values in case we have flux limits
973  (which are not yet implemented).
974  epoch : `astropy.time.Time` (optional)
975  Epoch to which to correct proper motion and parallax,
976  or None to not apply such corrections.
977  centroids : `bool` (optional)
978  Add centroid fields to the loaded Schema. ``loadPixelBox`` expects
979  these fields to exist.
980 
981  Returns
982  -------
983  results : `lsst.pipe.base.Struct`
984  A Struct containing the following fields:
985  refCat : `lsst.afw.catalog.SimpleCatalog`
986  A catalog of reference objects with the standard
987  schema, as documented in the main doc string for
988  `LoadReferenceObjects`.
989  The catalog is guaranteed to be contiguous.
990  fluxField : `str`
991  Name of flux field for specified `filterName`.
992 
993  Notes
994  -----
995  Note that subclasses are responsible for performing the proper motion
996  correction, since this is the lowest-level interface for retrieving
997  the catalog.
998  """
999  return
1000 
1001  @staticmethod
1002  def _trimToBBox(refCat, bbox, wcs):
1003  """Remove objects outside a given pixel bounding box and set
1004  centroid and hasCentroid fields.
1005 
1006  Parameters
1007  ----------
1008  refCat : `lsst.afw.table.SimpleCatalog`
1009  A catalog of objects. The schema must include fields
1010  "coord", "centroid" and "hasCentroid".
1011  The "coord" field is read.
1012  The "centroid" and "hasCentroid" fields are set.
1013  bbox : `lsst.geom.Box2D`
1014  Pixel region
1015  wcs : `lsst.afw.geom.SkyWcs`
1016  WCS; used to convert sky coordinates to pixel positions.
1017 
1018  @return a catalog of reference objects in bbox, with centroid and hasCentroid fields set
1019  """
1020  afwTable.updateRefCentroids(wcs, refCat)
1021  centroidKey = afwTable.Point2DKey(refCat.schema["centroid"])
1022  retStarCat = type(refCat)(refCat.table)
1023  for star in refCat:
1024  point = star.get(centroidKey)
1025  if bbox.contains(point):
1026  retStarCat.append(star)
1027  return retStarCat
1028 
1029  def _addFluxAliases(self, schema):
1030  """Add aliases for camera filter fluxes to the schema.
1031 
1032  If self.config.defaultFilter then adds these aliases:
1033  camFlux: <defaultFilter>_flux
1034  camFluxErr: <defaultFilter>_fluxErr, if the latter exists
1035 
1036  For each camFilter: refFilter in self.config.filterMap adds these aliases:
1037  <camFilter>_camFlux: <refFilter>_flux
1038  <camFilter>_camFluxErr: <refFilter>_fluxErr, if the latter exists
1039 
1040  Parameters
1041  ----------
1042  schema : `lsst.afw.table.Schema`
1043  Schema for reference catalog.
1044 
1045  Throws
1046  ------
1047  RuntimeError
1048  If any reference flux field is missing from the schema.
1049  """
1050  aliasMap = schema.getAliasMap()
1051 
1052  def addAliasesForOneFilter(filterName, refFilterName):
1053  """Add aliases for a single filter
1054 
1055  Parameters
1056  ----------
1057  filterName : `str` (optional)
1058  Camera filter name. The resulting alias name is
1059  <filterName>_camFlux, or simply "camFlux" if `filterName`
1060  is `None` or `""`.
1061  refFilterName : `str`
1062  Reference catalog filter name; the field
1063  <refFilterName>_flux must exist.
1064  """
1065  camFluxName = filterName + "_camFlux" if filterName is not None else "camFlux"
1066  refFluxName = refFilterName + "_flux"
1067  if refFluxName not in schema:
1068  raise RuntimeError("Unknown reference filter %s" % (refFluxName,))
1069  aliasMap.set(camFluxName, refFluxName)
1070  refFluxErrName = refFluxName + "Err"
1071  if refFluxErrName in schema:
1072  camFluxErrName = camFluxName + "Err"
1073  aliasMap.set(camFluxErrName, refFluxErrName)
1074 
1075  if self.config.defaultFilter:
1076  addAliasesForOneFilter(None, self.config.defaultFilter)
1077 
1078  for filterName, refFilterName in self.config.filterMap.items():
1079  addAliasesForOneFilter(filterName, refFilterName)
1080 
1081  @staticmethod
1082  def makeMinimalSchema(filterNameList, *, addCentroid=False,
1083  addIsPhotometric=False, addIsResolved=False,
1084  addIsVariable=False, coordErrDim=2,
1085  addProperMotion=False, properMotionErrDim=2,
1086  addParallax=False):
1087  """Make a standard schema for reference object catalogs.
1088 
1089  Parameters
1090  ----------
1091  filterNameList : `list` of `str`
1092  List of filter names. Used to create <filterName>_flux fields.
1093  addIsPhotometric : `bool`
1094  If True then add field "photometric".
1095  addIsResolved : `bool`
1096  If True then add field "resolved".
1097  addIsVariable : `bool`
1098  If True then add field "variable".
1099  coordErrDim : `int`
1100  Number of coord error fields; must be one of 0, 2, 3:
1101 
1102  - If 2 or 3: add fields "coord_raErr" and "coord_decErr".
1103  - If 3: also add field "coord_radecErr".
1104  addProperMotion : `bool`
1105  If True add fields "epoch", "pm_ra", "pm_dec" and "pm_flag".
1106  properMotionErrDim : `int`
1107  Number of proper motion error fields; must be one of 0, 2, 3;
1108  ignored if addProperMotion false:
1109  - If 2 or 3: add fields "pm_raErr" and "pm_decErr".
1110  - If 3: also add field "pm_radecErr".
1111  addParallax : `bool`
1112  If True add fields "epoch", "parallax", "parallaxErr"
1113  and "parallax_flag".
1114 
1115  Returns
1116  -------
1117  schema : `lsst.afw.table.Schema`
1118  Schema for reference catalog, an
1119  `lsst.afw.table.SimpleCatalog`.
1120 
1121  Notes
1122  -----
1123  Reference catalogs support additional covariances, such as
1124  covariance between RA and proper motion in declination,
1125  that are not supported by this method, but can be added after
1126  calling this method.
1127  """
1128  schema = afwTable.SimpleTable.makeMinimalSchema()
1129  if addCentroid:
1130  afwTable.Point2DKey.addFields(
1131  schema,
1132  "centroid",
1133  "centroid on an exposure, if relevant",
1134  "pixel",
1135  )
1136  schema.addField(
1137  field="hasCentroid",
1138  type="Flag",
1139  doc="is position known?",
1140  )
1141  for filterName in filterNameList:
1142  schema.addField(
1143  field="%s_flux" % (filterName,),
1144  type=numpy.float64,
1145  doc="flux in filter %s" % (filterName,),
1146  units="nJy",
1147  )
1148  for filterName in filterNameList:
1149  schema.addField(
1150  field="%s_fluxErr" % (filterName,),
1151  type=numpy.float64,
1152  doc="flux uncertainty in filter %s" % (filterName,),
1153  units="nJy",
1154  )
1155  if addIsPhotometric:
1156  schema.addField(
1157  field="photometric",
1158  type="Flag",
1159  doc="set if the object can be used for photometric calibration",
1160  )
1161  if addIsResolved:
1162  schema.addField(
1163  field="resolved",
1164  type="Flag",
1165  doc="set if the object is spatially resolved",
1166  )
1167  if addIsVariable:
1168  schema.addField(
1169  field="variable",
1170  type="Flag",
1171  doc="set if the object has variable brightness",
1172  )
1173  if coordErrDim not in (0, 2, 3):
1174  raise ValueError("coordErrDim={}; must be (0, 2, 3)".format(coordErrDim))
1175  if coordErrDim > 0:
1176  afwTable.CovarianceMatrix2fKey.addFields(
1177  schema=schema,
1178  prefix="coord",
1179  names=["ra", "dec"],
1180  units=["rad", "rad"],
1181  diagonalOnly=(coordErrDim == 2),
1182  )
1183 
1184  if addProperMotion or addParallax:
1185  schema.addField(
1186  field="epoch",
1187  type=numpy.float64,
1188  doc="date of observation (TAI, MJD)",
1189  units="day",
1190  )
1191 
1192  if addProperMotion:
1193  schema.addField(
1194  field="pm_ra",
1195  type="Angle",
1196  doc="proper motion in the right ascension direction = dra/dt * cos(dec)",
1197  units="rad/year",
1198  )
1199  schema.addField(
1200  field="pm_dec",
1201  type="Angle",
1202  doc="proper motion in the declination direction",
1203  units="rad/year",
1204  )
1205  if properMotionErrDim not in (0, 2, 3):
1206  raise ValueError("properMotionErrDim={}; must be (0, 2, 3)".format(properMotionErrDim))
1207  if properMotionErrDim > 0:
1208  afwTable.CovarianceMatrix2fKey.addFields(
1209  schema=schema,
1210  prefix="pm",
1211  names=["ra", "dec"],
1212  units=["rad/year", "rad/year"],
1213  diagonalOnly=(properMotionErrDim == 2),
1214  )
1215  schema.addField(
1216  field="pm_flag",
1217  type="Flag",
1218  doc="Set if proper motion or proper motion error is bad",
1219  )
1220 
1221  if addParallax:
1222  schema.addField(
1223  field="parallax",
1224  type="Angle",
1225  doc="parallax",
1226  units="rad",
1227  )
1228  schema.addField(
1229  field="parallaxErr",
1230  type="Angle",
1231  doc="uncertainty in parallax",
1232  units="rad",
1233  )
1234  schema.addField(
1235  field="parallax_flag",
1236  type="Flag",
1237  doc="Set if parallax or parallax error is bad",
1238  )
1239  return schema
1240 
1241  def _calculateCircle(self, bbox, wcs):
1242  """Compute on-sky center and radius of search region.
1243 
1244  Parameters
1245  ----------
1246  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
1247  Pixel bounding box.
1248  wcs : `lsst.afw.geom.SkyWcs`
1249  WCS; used to convert pixel positions to sky coordinates.
1250 
1251  Returns
1252  -------
1253  results : `lsst.pipe.base.Struct`
1254  A Struct containing:
1255 
1256  - coord : `lsst.geom.SpherePoint`
1257  ICRS center of the search region.
1258  - radius : `lsst.geom.Angle`
1259  Radius of the search region.
1260  - bbox : `lsst.geom.Box2D`
1261  Bounding box used to compute the circle.
1262  """
1263  bbox = lsst.geom.Box2D(bbox) # make sure bbox is double and that we have a copy
1264  bbox.grow(self.config.pixelMargin)
1265  coord = wcs.pixelToSky(bbox.getCenter())
1266  radius = max(coord.separation(wcs.pixelToSky(pp)) for pp in bbox.getCorners())
1267  return pipeBase.Struct(coord=coord, radius=radius, bbox=bbox)
1268 
1269  def getMetadataBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None):
1270  """Return metadata about the load.
1271 
1272  This metadata is used for reloading the catalog (e.g., for
1273  reconstituting a normalised match list.
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  filterName : `str`
1282  Name of camera filter, or `None` or `""` for the default
1283  filter.
1284  photoCalib : `lsst.afw.image.PhotoCalib` (optional)
1285  Calibration, or `None` if unknown.
1286  epoch : `astropy.time.Time` (optional)
1287  Epoch to which to correct proper motion and parallax,
1288  or None to not apply such corrections.
1289 
1290  Returns
1291  -------
1292  metadata : lsst.daf.base.PropertyList
1293  Metadata about the load.
1294  """
1295  circle = self._calculateCircle(bbox, wcs)
1296  return self.getMetadataCircle(circle.coord, circle.radius, filterName, photoCalib)
1297 
1298  def getMetadataCircle(self, coord, radius, filterName, photoCalib=None, epoch=None):
1299  """Return metadata about the load.
1300 
1301  This metadata is used for reloading the catalog (e.g., for
1302  reconstituting a normalised match list.
1303 
1304  Parameters
1305  ----------
1306  coord : `lsst.geom.SpherePoint`
1307  ICRS center of the search region.
1308  radius : `lsst.geom.Angle`
1309  Radius of the search region.
1310  filterName : `str`
1311  Name of camera filter, or `None` or `""` for the default
1312  filter.
1313  photoCalib : `lsst.afw.image.PhotoCalib` (optional)
1314  Calibration, or `None` if unknown.
1315  epoch : `astropy.time.Time` (optional)
1316  Epoch to which to correct proper motion and parallax,
1317  or None to not apply such corrections.
1318 
1319  Returns
1320  -------
1321  metadata : lsst.daf.base.PropertyList
1322  Metadata about the load
1323  """
1324  md = PropertyList()
1325  md.add('RA', coord.getRa().asDegrees(), 'field center in degrees')
1326  md.add('DEC', coord.getDec().asDegrees(), 'field center in degrees')
1327  md.add('RADIUS', radius.asDegrees(), 'field radius in degrees, minimum')
1328  md.add('SMATCHV', 1, 'SourceMatchVector version number')
1329  filterName = "UNKNOWN" if filterName is None else str(filterName)
1330  md.add('FILTER', filterName, 'filter name for photometric data')
1331  md.add('EPOCH', "NONE" if epoch is None else epoch, 'Epoch (TAI MJD) for catalog')
1332  return md
1333 
1334  def joinMatchListWithCatalog(self, matchCat, sourceCat):
1335  """Relink an unpersisted match list to sources and reference
1336  objects.
1337 
1338  A match list is persisted and unpersisted as a catalog of IDs
1339  produced by afw.table.packMatches(), with match metadata
1340  (as returned by the astrometry tasks) in the catalog's metadata
1341  attribute. This method converts such a match catalog into a match
1342  list, with links to source records and reference object records.
1343 
1344  Parameters
1345  ----------
1346  matchCat : `lsst.afw.table.BaseCatalog`
1347  Unperisted packed match list.
1348  ``matchCat.table.getMetadata()`` must contain match metadata,
1349  as returned by the astrometry tasks.
1350  sourceCat : `lsst.afw.table.SourceCatalog`
1351  Source catalog. As a side effect, the catalog will be sorted
1352  by ID.
1353 
1354  Returns
1355  -------
1356  matchList : `lsst.afw.table.ReferenceMatchVector`
1357  Match list.
1358  """
1359  return joinMatchListWithCatalogImpl(self, matchCat, sourceCat)
1360 
1361  def applyProperMotions(self, catalog, epoch):
1362  """Apply proper motion correction to a reference catalog.
1363 
1364  Adjust position and position error in the ``catalog``
1365  for proper motion to the specified ``epoch``,
1366  modifying the catalong in place.
1367 
1368  Parameters
1369  ----------
1370  catalog : `lsst.afw.table.SimpleCatalog`
1371  Catalog of positions, containing:
1372 
1373  - Coordinates, retrieved by the table's coordinate key.
1374  - ``coord_raErr`` : Error in Right Ascension (rad).
1375  - ``coord_decErr`` : Error in Declination (rad).
1376  - ``pm_ra`` : Proper motion in Right Ascension (rad/yr,
1377  East positive)
1378  - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
1379  - ``pm_dec`` : Proper motion in Declination (rad/yr,
1380  North positive)
1381  - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional.
1382  - ``epoch`` : Mean epoch of object (an astropy.time.Time)
1383  epoch : `astropy.time.Time` (optional)
1384  Epoch to which to correct proper motion and parallax,
1385  or None to not apply such corrections.
1386  """
1387  if ("epoch" not in catalog.schema or "pm_ra" not in catalog.schema or "pm_dec" not in catalog.schema):
1388  if self.config.requireProperMotion:
1389  raise RuntimeError("Proper motion correction required but not available from catalog")
1390  self.log.warn("Proper motion correction not available from catalog")
1391  return
1392  applyProperMotionsImpl(self.log, catalog, epoch)
1393 
1394 
1395 def joinMatchListWithCatalogImpl(refObjLoader, matchCat, sourceCat):
1396  """Relink an unpersisted match list to sources and reference
1397  objects.
1398 
1399  A match list is persisted and unpersisted as a catalog of IDs
1400  produced by afw.table.packMatches(), with match metadata
1401  (as returned by the astrometry tasks) in the catalog's metadata
1402  attribute. This method converts such a match catalog into a match
1403  list, with links to source records and reference object records.
1404 
1405  Parameters
1406  ----------
1407  refObjLoader
1408  Reference object loader to use in getting reference objects
1409  matchCat : `lsst.afw.table.BaseCatalog`
1410  Unperisted packed match list.
1411  ``matchCat.table.getMetadata()`` must contain match metadata,
1412  as returned by the astrometry tasks.
1413  sourceCat : `lsst.afw.table.SourceCatalog`
1414  Source catalog. As a side effect, the catalog will be sorted
1415  by ID.
1416 
1417  Returns
1418  -------
1419  matchList : `lsst.afw.table.ReferenceMatchVector`
1420  Match list.
1421  """
1422  matchmeta = matchCat.table.getMetadata()
1423  version = matchmeta.getInt('SMATCHV')
1424  if version != 1:
1425  raise ValueError('SourceMatchVector version number is %i, not 1.' % version)
1426  filterName = matchmeta.getString('FILTER').strip()
1427  try:
1428  epoch = matchmeta.getDouble('EPOCH')
1430  epoch = None # Not present, or not correct type means it's not set
1431  if 'RADIUS' in matchmeta:
1432  # This is a circle style metadata, call loadSkyCircle
1433  ctrCoord = lsst.geom.SpherePoint(matchmeta.getDouble('RA'),
1434  matchmeta.getDouble('DEC'), lsst.geom.degrees)
1435  rad = matchmeta.getDouble('RADIUS') * lsst.geom.degrees
1436  refCat = refObjLoader.loadSkyCircle(ctrCoord, rad, filterName, epoch=epoch).refCat
1437  elif "INNER_UPPER_LEFT_RA" in matchmeta:
1438  # This is the sky box type (only triggers in the LoadReferenceObject class, not task)
1439  # Only the outer box is required to be loaded to get the maximum region, all filtering
1440  # will be done by the unpackMatches function, and no spatial filtering needs to be done
1441  # by the refObjLoader
1442  box = []
1443  for place in ("UPPER_LEFT", "UPPER_RIGHT", "LOWER_LEFT", "LOWER_RIGHT"):
1444  coord = lsst.geom.SpherePoint(matchmeta.getDouble(f"OUTER_{place}_RA"),
1445  matchmeta.getDouble(f"OUTER_{place}_DEC"),
1446  lsst.geom.degrees).getVector()
1447  box.append(coord)
1448  outerBox = sphgeom.ConvexPolygon(box)
1449  refCat = refObjLoader.loadRegion(outerBox, filterName=filterName, epoch=epoch).refCat
1450 
1451  refCat.sort()
1452  sourceCat.sort()
1453  return afwTable.unpackMatches(matchCat, refCat, sourceCat)
1454 
1455 
1456 def applyProperMotionsImpl(log, catalog, epoch):
1457  """Apply proper motion correction to a reference catalog.
1458 
1459  Adjust position and position error in the ``catalog``
1460  for proper motion to the specified ``epoch``,
1461  modifying the catalong in place.
1462 
1463  Parameters
1464  ----------
1465  log : `lsst.log.log`
1466  log object to write to
1467  catalog : `lsst.afw.table.SimpleCatalog`
1468  Catalog of positions, containing:
1469 
1470  - Coordinates, retrieved by the table's coordinate key.
1471  - ``coord_raErr`` : Error in Right Ascension (rad).
1472  - ``coord_decErr`` : Error in Declination (rad).
1473  - ``pm_ra`` : Proper motion in Right Ascension (rad/yr,
1474  East positive)
1475  - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
1476  - ``pm_dec`` : Proper motion in Declination (rad/yr,
1477  North positive)
1478  - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional.
1479  - ``epoch`` : Mean epoch of object (an astropy.time.Time)
1480  epoch : `astropy.time.Time` (optional)
1481  Epoch to which to correct proper motion and parallax,
1482  or None to not apply such corrections.
1483  """
1484  if "epoch" not in catalog.schema or "pm_ra" not in catalog.schema or "pm_dec" not in catalog.schema:
1485  log.warn("Proper motion correction not available from catalog")
1486  return
1487  if not catalog.isContiguous():
1488  raise RuntimeError("Catalog must be contiguous")
1489  catEpoch = astropy.time.Time(catalog["epoch"], scale="tai", format="mjd")
1490  log.debug("Correcting reference catalog for proper motion to %r", epoch)
1491  # Use `epoch.tai` to make sure the time difference is in TAI
1492  timeDiffsYears = (epoch.tai - catEpoch).to(astropy.units.yr).value
1493  coordKey = catalog.table.getCoordKey()
1494  # Compute the offset of each object due to proper motion
1495  # as components of the arc of a great circle along RA and Dec
1496  pmRaRad = catalog["pm_ra"]
1497  pmDecRad = catalog["pm_dec"]
1498  offsetsRaRad = pmRaRad*timeDiffsYears
1499  offsetsDecRad = pmDecRad*timeDiffsYears
1500  # Compute the corresponding bearing and arc length of each offset
1501  # due to proper motion, and apply the offset
1502  # The factor of 1e6 for computing bearing is intended as
1503  # a reasonable scale for typical values of proper motion
1504  # in order to avoid large errors for small values of proper motion;
1505  # using the offsets is another option, but it can give
1506  # needlessly large errors for short duration
1507  offsetBearingsRad = numpy.arctan2(pmDecRad*1e6, pmRaRad*1e6)
1508  offsetAmountsRad = numpy.hypot(offsetsRaRad, offsetsDecRad)
1509  for record, bearingRad, amountRad in zip(catalog, offsetBearingsRad, offsetAmountsRad):
1510  record.set(coordKey,
1511  record.get(coordKey).offset(bearing=bearingRad*lsst.geom.radians,
1512  amount=amountRad*lsst.geom.radians))
1513  # Increase error in RA and Dec based on error in proper motion
1514  if "coord_raErr" in catalog.schema:
1515  catalog["coord_raErr"] = numpy.hypot(catalog["coord_raErr"],
1516  catalog["pm_raErr"]*timeDiffsYears)
1517  if "coord_decErr" in catalog.schema:
1518  catalog["coord_decErr"] = numpy.hypot(catalog["coord_decErr"],
1519  catalog["pm_decErr"]*timeDiffsYears)
def getMetadataCircle(self, coord, radius, filterName, photoCalib=None, epoch=None)
def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None, centroids=False)
static Log getDefaultLogger()
Return default logger instance, same as default constructor.
Definition: Log.h:754
Defines the fields and offsets for a table.
Definition: Schema.h:50
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
def loadPixelBox(self, bbox, wcs, filterName=None, epoch=None, photoCalib=None, bboxPadding=100)
def addFluxAliases(refCat, defaultFilter, filterReferenceMap)
static Schema makeMinimalSchema()
Return a minimal schema for Simple tables and records.
Definition: Simple.h:140
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None)
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
A mapping between the keys of two Schemas, used to copy data between them.
Definition: SchemaMapper.h:21
def getMetadataBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None)
daf::base::PropertySet * set
Definition: fits.cc:902
def getMetadataCircle(coord, radius, filterName, photoCalib=None, epoch=None)
Box represents a rectangle in spherical coordinate space that contains its boundary.
Definition: Box.h:54
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
Update centroids in a collection of reference objects.
Definition: wcsUtils.cc:73
def convertToNanojansky(catalog, log, doConvert=True)
Definition: Log.h:706
def loadPixelBox(self, bbox, wcs, filterName=None, photoCalib=None, epoch=None)
Reports attempts to access elements using an invalid key.
Definition: Runtime.h:151
table::Key< int > to
table::Key< int > type
Definition: Detector.cc:163
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:63
def loadRegion(self, region, filtFunc=None, filterName=None, epoch=None)
A description of a field in a table.
Definition: Field.h:24
Abstract base class to load objects from reference catalogs.
def makeMinimalSchema(filterNameList, addCentroid=False, addIsPhotometric=False, addIsResolved=False, addIsVariable=False, coordErrDim=2, addProperMotion=False, properMotionErrDim=2, addParallax=False)
int max
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
def joinMatchListWithCatalogImpl(refObjLoader, matchCat, sourceCat)
Angle represents an angle in radians.
Definition: Angle.h:43
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
template SourceMatchVector unpackMatches(BaseCatalog const &, SourceCatalog const &, SourceCatalog const &)
def getMetadataBox(cls, bbox, wcs, filterName=None, photoCalib=None, epoch=None, bboxPadding=100)
Reports errors from accepting an object of an unexpected or inappropriate type.
Definition: Runtime.h:167
def remapReferenceCatalogSchema(refCat, filterNameList=None, position=False, photometric=False)
bool strip
Definition: fits.cc:901
Reports errors that are due to events beyond the control of the program.
Definition: Runtime.h:104