lsst.meas.algorithms  16.0-20-g21842373+6
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 
26 import abc
27 
28 import astropy.time
29 import astropy.units
30 import numpy
31 
32 import lsst.geom
33 import lsst.afw.table as afwTable
34 import lsst.pex.config as pexConfig
35 import lsst.pipe.base as pipeBase
36 import lsst.pex.exceptions as pexExcept
37 from lsst.daf.base import PropertyList
38 
39 
40 def getRefFluxField(schema, filterName=None):
41  """Get the name of a flux field from a schema.
42 
43  if filterName is specified:
44  return *filterName*_camFlux if present
45  else return *filterName*_flux if present (camera filter name matches reference filter name)
46  else throw RuntimeError
47  else:
48  return camFlux, if present,
49  else throw RuntimeError
50 
51  Parameters
52  ----------
53  schema : `lsst.afw.table.Schema`
54  Reference catalog schema.
55  filterName : `str`
56  Name of camera filter.
57 
58  Returns
59  -------
60  fluxFieldName : `str`
61  Name of flux field.
62 
63  Raises
64  ------
65  RuntimeError
66  If an appropriate field is not found.
67  """
68  if not isinstance(schema, afwTable.Schema):
69  raise RuntimeError("schema=%s is not a schema" % (schema,))
70  if filterName:
71  fluxFieldList = [filterName + "_camFlux", filterName + "_flux"]
72  else:
73  fluxFieldList = ["camFlux"]
74  for fluxField in fluxFieldList:
75  if fluxField in schema:
76  return fluxField
77 
78  raise RuntimeError("Could not find flux field(s) %s" % (", ".join(fluxFieldList)))
79 
80 
81 def getRefFluxKeys(schema, filterName=None):
82  """Return keys for flux and flux error.
83 
84  Parameters
85  ----------
86  schema : `lsst.afw.table.Schema`
87  Reference catalog schema.
88  filterName : `str`
89  Name of camera filter.
90 
91  Returns
92  -------
93  keys : `tuple` of (`lsst.afw.table.Key`, `lsst.afw.table.Key`)
94  Two keys:
95 
96  - flux key
97  - flux error key, if present, else None
98 
99  Raises
100  ------
101  RuntimeError
102  If flux field not found.
103  """
104  fluxField = getRefFluxField(schema, filterName)
105  fluxErrField = fluxField + "Err"
106  fluxKey = schema[fluxField].asKey()
107  try:
108  fluxErrKey = schema[fluxErrField].asKey()
109  except Exception:
110  fluxErrKey = None
111  return (fluxKey, fluxErrKey)
112 
113 
114 class LoadReferenceObjectsConfig(pexConfig.Config):
115  pixelMargin = pexConfig.RangeField(
116  doc="Padding to add to 4 all edges of the bounding box (pixels)",
117  dtype=int,
118  default=300,
119  min=0,
120  )
121  defaultFilter = pexConfig.Field(
122  doc="Default reference catalog filter to use if filter not specified in exposure; "
123  "if blank then filter must be specified in exposure",
124  dtype=str,
125  default="",
126  )
127  filterMap = pexConfig.DictField(
128  doc="Mapping of camera filter name: reference catalog filter name; "
129  "each reference filter must exist",
130  keytype=str,
131  itemtype=str,
132  default={},
133  )
134  requireProperMotion = pexConfig.Field(
135  doc="Require that the fields needed to correct proper motion "
136  "(epoch, pm_ra and pm_dec) are present?",
137  dtype=bool,
138  default=False,
139  )
140 
141 # The following comment block adds a link to this task from the Task Documentation page.
142 
148 
149 
150 class LoadReferenceObjectsTask(pipeBase.Task, metaclass=abc.ABCMeta):
151  r"""!Abstract base class to load objects from reference catalogs
152 
153  @anchor LoadReferenceObjectsTask_
154 
155  @section meas_algorithms_loadReferenceObjects_Contents Contents
156 
157  - @ref meas_algorithms_loadReferenceObjects_Purpose
158  - @ref meas_algorithms_loadReferenceObjects_Initialize
159  - @ref meas_algorithms_loadReferenceObjects_IO
160  - @ref meas_algorithms_loadReferenceObjects_Schema
161  - @ref meas_algorithms_loadReferenceObjects_Config
162 
163  @section meas_algorithms_loadReferenceObjects_Purpose Description
164 
165  Abstract base class for tasks that load objects from a reference catalog
166  in a particular region of the sky.
167 
168  Implementations must subclass this class, override the loadSkyCircle method,
169  and will typically override the value of ConfigClass with a task-specific config class.
170 
171  @section meas_algorithms_loadReferenceObjects_Initialize Task initialisation
172 
173  @copydoc \_\_init\_\_
174 
175  @section meas_algorithms_loadReferenceObjects_IO Invoking the Task
176 
177  @copydoc loadPixelBox
178 
179  @section meas_algorithms_loadReferenceObjects_Schema Schema of the reference object catalog
180 
181  Reference object catalogs are instances of lsst.afw.table.SimpleCatalog with the following schema
182  (other fields may also be present).
183  The units use astropy quantity conventions, so a 2 suffix means squared.
184  See also makeMinimalSchema.
185  - coord: ICRS position of star on sky (an lsst.geom.SpherePoint)
186  - centroid: position of star on an exposure, if relevant (an lsst.afw.Point2D)
187  - hasCentroid: is centroid usable? (a Flag)
188  - *referenceFilterName*_flux: brightness in the specified reference catalog filter (Jy)
189  Note: the function lsst.afw.image.abMagFromFlux will convert flux in Jy to AB Magnitude.
190  - *referenceFilterName*_fluxErr (optional): brightness standard deviation (Jy);
191  omitted if no data is available; possibly nan if data is available for some objects but not others
192  - camFlux: brightness in default camera filter (Jy); omitted if defaultFilter not specified
193  - camFluxErr: brightness standard deviation for default camera filter;
194  omitted if defaultFilter not specified or standard deviation not available that filter
195  - *cameraFilterName*_camFlux: brightness in specified camera filter (Jy)
196  - *cameraFilterName*_camFluxErr (optional): brightness standard deviation
197  in specified camera filter (Jy); omitted if no data is available;
198  possibly nan if data is available for some objects but not others
199  - photometric (optional): is the object usable for photometric calibration? (a Flag)
200  - resolved (optional): is the object spatially resolved? (a Flag)
201  - variable (optional): does the object have variable brightness? (a Flag)
202  - coord_raErr: uncertainty in `coord` along the direction of right ascension (radian, an Angle)
203  = uncertainty in ra * cos(dec); nan if unknown
204  - coord_decErr: uncertainty in `coord` along the direction of declination (radian, an Angle);
205  nan if unknown
206 
207  The following are optional; fields should only be present if the
208  information is available for at least some objects.
209  Numeric values are `nan` if unknown:
210  - epoch: date of observation as TAI MJD (day)
211 
212  - pm_ra: proper motion along the direction of right ascension (rad/year, an Angle) = dra/dt * cos(dec)
213  - pm_dec: proper motion along the direction of declination (rad/year, and Angle)
214  - pm_raErr: uncertainty in `pm_ra` (rad/year)
215  - pm_decErr: uncertainty in `pm_dec` (rad/year)
216  - pm_ra_dec_Cov: covariance between pm_ra and pm_dec (rad2/year2)
217  - pm_flag: set if proper motion, error or covariance is bad
218 
219  - parallax: parallax (rad, an Angle)
220  - parallaxErr: uncertainty in `parallax` (rad)
221  - parallax_flag: set if parallax value or parallaxErr is bad
222 
223  - coord_ra_pm_ra_Cov: covariance between coord_ra and pm_ra (rad2/year)
224  - coord_ra_pm_dec_Cov: covariance between coord_ra and pm_dec (rad2/year)
225  - coord_ra_parallax_Cov: covariance between coord_ra and parallax (rad2/year)
226  - coord_dec_pm_ra_Cov: covariance between coord_dec and pm_ra (rad2/year)
227  - coord_dec_pm_dec_Cov: covariance between coord_dec and pm_dec (rad2/year)
228  - coord_dec_parallax_Cov: covariance between coord_dec and parallax (rad2/year)
229  - pm_ra_parallax_Cov: covariance between pm_ra and parallax (rad2/year)
230  - pm_dec_parallax_Cov: covariance between pm_dec and parallax (rad2/year)
231 
232  @section meas_algorithms_loadReferenceObjects_Config Configuration parameters
233 
234  See @ref LoadReferenceObjectsConfig for a base set of configuration parameters.
235  Most subclasses will add configuration variables.
236  """
237  ConfigClass = LoadReferenceObjectsConfig
238  _DefaultName = "LoadReferenceObjects"
239 
240  def __init__(self, butler=None, *args, **kwargs):
241  """Construct a LoadReferenceObjectsTask
242 
243  Parameters
244  ----------
245  butler : `lsst.daf.persistence.Butler`
246  Data butler, for access reference catalogs.
247  """
248  pipeBase.Task.__init__(self, *args, **kwargs)
249  self.butler = butler
250 
251  @pipeBase.timeMethod
252  def loadPixelBox(self, bbox, wcs, filterName=None, calib=None, epoch=None):
253  """Load reference objects that overlap a rectangular pixel region.
254 
255  Parameters
256  ----------
257  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
258  Bounding box for pixels.
259  wcs : `lsst.afw.geom.SkyWcs`
260  WCS; used to convert pixel positions to sky coordinates
261  and vice-versa.
262  filterName : `str`
263  Name of filter, or `None` or `""` for the default filter.
264  This is used for flux values in case we have flux limits
265  (which are not yet implemented).
266  calib : `lsst.afw.image.Calib` (optional)
267  Calibration, or `None` if unknown.
268  epoch : `astropy.time.Time` (optional)
269  Epoch to which to correct proper motion and parallax,
270  or None to not apply such corrections.
271 
272  Returns
273  -------
274  results : `lsst.pipe.base.Struct`
275  A Struct containing the following fields:
276  refCat : `lsst.afw.catalog.SimpleCatalog`
277  A catalog of reference objects with the standard
278  schema, as documented in the main doc string for
279  `LoadReferenceObjects`.
280  The catalog is guaranteed to be contiguous.
281  fluxField : `str`
282  Name of flux field for specified `filterName`.
283 
284  Notes
285  -----
286  The search algorithm works by searching in a region in sky
287  coordinates whose center is the center of the bbox and radius
288  is large enough to just include all 4 corners of the bbox.
289  Stars that lie outside the bbox are then trimmed from the list.
290  """
291  circle = self._calculateCircle(bbox, wcs)
292 
293  # find objects in circle
294  self.log.info("Loading reference objects using center %s and radius %s deg" %
295  (circle.coord, circle.radius.asDegrees()))
296  loadRes = self.loadSkyCircle(circle.coord, circle.radius, filterName)
297  refCat = loadRes.refCat
298  numFound = len(refCat)
299 
300  # trim objects outside bbox
301  refCat = self._trimToBBox(refCat=refCat, bbox=circle.bbox, wcs=wcs)
302  numTrimmed = numFound - len(refCat)
303  self.log.debug("trimmed %d out-of-bbox objects, leaving %d", numTrimmed, len(refCat))
304  self.log.info("Loaded %d reference objects", len(refCat))
305 
306  # make sure catalog is contiguous
307  if not refCat.isContiguous():
308  loadRes.refCat = refCat.copy(deep=True)
309 
310  return loadRes
311 
312  @abc.abstractmethod
313  def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None):
314  """Load reference objects that overlap a circular sky region.
315 
316  Parameters
317  ----------
318  ctrCoord : `lsst.geom.SpherePoint`
319  ICRS center of search region.
320  radius : `lsst.geom.Angle`
321  Radius of search region.
322  filterName : `str` (optional)
323  Name of filter, or `None` or `""` for the default filter.
324  This is used for flux values in case we have flux limits
325  (which are not yet implemented).
326  epoch : `astropy.time.Time` (optional)
327  Epoch to which to correct proper motion and parallax,
328  or None to not apply such corrections.
329 
330  Returns
331  -------
332  results : `lsst.pipe.base.Struct`
333  A Struct containing the following fields:
334  refCat : `lsst.afw.catalog.SimpleCatalog`
335  A catalog of reference objects with the standard
336  schema, as documented in the main doc string for
337  `LoadReferenceObjects`.
338  The catalog is guaranteed to be contiguous.
339  fluxField : `str`
340  Name of flux field for specified `filterName`.
341 
342  Notes
343  -----
344  Note that subclasses are responsible for performing the proper motion
345  correction, since this is the lowest-level interface for retrieving
346  the catalog.
347  """
348  return
349 
350  @staticmethod
351  def _trimToBBox(refCat, bbox, wcs):
352  """Remove objects outside a given pixel bounding box and set
353  centroid and hasCentroid fields.
354 
355  Parameters
356  ----------
357  refCat : `lsst.afw.table.SimpleCatalog`
358  A catalog of objects. The schema must include fields
359  "coord", "centroid" and "hasCentroid".
360  The "coord" field is read.
361  The "centroid" and "hasCentroid" fields are set.
362  bbox : `lsst.geom.Box2D`
363  Pixel region
364  wcs : `lsst.afw.geom.SkyWcs`
365  WCS; used to convert sky coordinates to pixel positions.
366 
367  @return a catalog of reference objects in bbox, with centroid and hasCentroid fields set
368  """
369  afwTable.updateRefCentroids(wcs, refCat)
370  centroidKey = afwTable.Point2DKey(refCat.schema["centroid"])
371  retStarCat = type(refCat)(refCat.table)
372  for star in refCat:
373  point = star.get(centroidKey)
374  if bbox.contains(point):
375  retStarCat.append(star)
376  return retStarCat
377 
378  def _addFluxAliases(self, schema):
379  """Add aliases for camera filter fluxes to the schema.
380 
381  If self.config.defaultFilter then adds these aliases:
382  camFlux: <defaultFilter>_flux
383  camFluxErr: <defaultFilter>_fluxErr, if the latter exists
384 
385  For each camFilter: refFilter in self.config.filterMap adds these aliases:
386  <camFilter>_camFlux: <refFilter>_flux
387  <camFilter>_camFluxErr: <refFilter>_fluxErr, if the latter exists
388 
389  Parameters
390  ----------
391  schema : `lsst.afw.table.Schema`
392  Schema for reference catalog.
393 
394  Throws
395  ------
396  RuntimeError
397  If any reference flux field is missing from the schema.
398  """
399  aliasMap = schema.getAliasMap()
400 
401  def addAliasesForOneFilter(filterName, refFilterName):
402  """Add aliases for a single filter
403 
404  Parameters
405  ----------
406  filterName : `str` (optional)
407  Camera filter name. The resulting alias name is
408  <filterName>_camFlux, or simply "camFlux" if `filterName`
409  is `None` or `""`.
410  refFilterName : `str`
411  Reference catalog filter name; the field
412  <refFilterName>_flux must exist.
413  """
414  camFluxName = filterName + "_camFlux" if filterName is not None else "camFlux"
415  refFluxName = refFilterName + "_flux"
416  if refFluxName not in schema:
417  raise RuntimeError("Unknown reference filter %s" % (refFluxName,))
418  aliasMap.set(camFluxName, refFluxName)
419  refFluxErrName = refFluxName + "Err"
420  if refFluxErrName in schema:
421  camFluxErrName = camFluxName + "Err"
422  aliasMap.set(camFluxErrName, refFluxErrName)
423 
424  if self.config.defaultFilter:
425  addAliasesForOneFilter(None, self.config.defaultFilter)
426 
427  for filterName, refFilterName in self.config.filterMap.items():
428  addAliasesForOneFilter(filterName, refFilterName)
429 
430  @staticmethod
431  def makeMinimalSchema(filterNameList, *, addCentroid=True,
432  addIsPhotometric=False, addIsResolved=False,
433  addIsVariable=False, coordErrDim=2,
434  addProperMotion=False, properMotionErrDim=2,
435  addParallax=False, addParallaxErr=True):
436  """Make a standard schema for reference object catalogs.
437 
438  Parameters
439  ----------
440  filterNameList : `list` of `str`
441  List of filter names. Used to create <filterName>_flux fields.
442  addIsPhotometric : `bool`
443  If True then add field "photometric".
444  addIsResolved : `bool`
445  If True then add field "resolved".
446  addIsVariable : `bool`
447  If True then add field "variable".
448  coordErrDim : `int`
449  Number of coord error fields; must be one of 0, 2, 3:
450 
451  - If 2 or 3: add fields "coord_raErr" and "coord_decErr".
452  - If 3: also add field "coord_radecErr".
453  addProperMotion : `bool`
454  If True add fields "epoch", "pm_ra", "pm_dec" and "pm_flag".
455  properMotionErrDim : `int`
456  Number of proper motion error fields; must be one of 0, 2, 3;
457  ignored if addProperMotion false:
458  - If 2 or 3: add fields "pm_raErr" and "pm_decErr".
459  - If 3: also add field "pm_radecErr".
460  addParallax : `bool`
461  If True add fields "epoch", "parallax", "parallaxErr"
462  and "parallax_flag".
463  addParallaxErr : `bool`
464  If True add field "parallaxErr"; ignored if addParallax false.
465 
466  Returns
467  -------
468  schema : `lsst.afw.table.Schema`
469  Schema for reference catalog, an
470  `lsst.afw.table.SimpleCatalog`.
471 
472  Notes
473  -----
474  Reference catalogs support additional covariances, such as
475  covariance between RA and proper motion in declination,
476  that are not supported by this method, but can be added after
477  calling this method.
478  """
479  schema = afwTable.SimpleTable.makeMinimalSchema()
480  if addCentroid:
481  afwTable.Point2DKey.addFields(
482  schema,
483  "centroid",
484  "centroid on an exposure, if relevant",
485  "pixel",
486  )
487  schema.addField(
488  field="hasCentroid",
489  type="Flag",
490  doc="is position known?",
491  )
492  for filterName in filterNameList:
493  schema.addField(
494  field="%s_flux" % (filterName,),
495  type=numpy.float64,
496  doc="flux in filter %s" % (filterName,),
497  units="Jy",
498  )
499  for filterName in filterNameList:
500  schema.addField(
501  field="%s_fluxErr" % (filterName,),
502  type=numpy.float64,
503  doc="flux uncertainty in filter %s" % (filterName,),
504  units="Jy",
505  )
506  if addIsPhotometric:
507  schema.addField(
508  field="photometric",
509  type="Flag",
510  doc="set if the object can be used for photometric calibration",
511  )
512  if addIsResolved:
513  schema.addField(
514  field="resolved",
515  type="Flag",
516  doc="set if the object is spatially resolved",
517  )
518  if addIsVariable:
519  schema.addField(
520  field="variable",
521  type="Flag",
522  doc="set if the object has variable brightness",
523  )
524  if coordErrDim not in (0, 2, 3):
525  raise ValueError("coordErrDim={}; must be (0, 2, 3)".format(coordErrDim))
526  if coordErrDim > 0:
527  afwTable.CovarianceMatrix2fKey.addFields(
528  schema=schema,
529  prefix="coord",
530  names=["ra", "dec"],
531  units=["rad", "rad"],
532  diagonalOnly=(coordErrDim == 2),
533  )
534 
535  if addProperMotion or addParallax:
536  schema.addField(
537  field="epoch",
538  type=numpy.float64,
539  doc="date of observation (TAI, MJD)",
540  units="day",
541  )
542 
543  if addProperMotion:
544  schema.addField(
545  field="pm_ra",
546  type="Angle",
547  doc="proper motion in the right ascension direction = dra/dt * cos(dec)",
548  units="rad/year",
549  )
550  schema.addField(
551  field="pm_dec",
552  type="Angle",
553  doc="proper motion in the declination direction",
554  units="rad/year",
555  )
556  if properMotionErrDim not in (0, 2, 3):
557  raise ValueError("properMotionErrDim={}; must be (0, 2, 3)".format(properMotionErrDim))
558  if properMotionErrDim > 0:
559  afwTable.CovarianceMatrix2fKey.addFields(
560  schema=schema,
561  prefix="pm",
562  names=["ra", "dec"],
563  units=["rad/year", "rad/year"],
564  diagonalOnly=(properMotionErrDim == 2),
565  )
566  schema.addField(
567  field="pm_flag",
568  type="Flag",
569  doc="Set if proper motion or proper motion error is bad",
570  )
571 
572  if addParallax:
573  schema.addField(
574  field="parallax",
575  type=numpy.float64,
576  doc="parallax",
577  units="rad",
578  )
579  if addParallaxErr:
580  schema.addField(
581  field="parallaxErr",
582  type=numpy.float64,
583  doc="uncertainty in parallax",
584  units="rad",
585  )
586  schema.addField(
587  field="parallax_flag",
588  type="Flag",
589  doc="Set if parallax or parallax error is bad",
590  )
591  return schema
592 
593  def _calculateCircle(self, bbox, wcs):
594  """Compute on-sky center and radius of search region.
595 
596  Parameters
597  ----------
598  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
599  Pixel bounding box.
600  wcs : `lsst.afw.geom.SkyWcs`
601  WCS; used to convert pixel positions to sky coordinates.
602 
603  Returns
604  -------
605  results : `lsst.pipe.base.Struct`
606  A Struct containing:
607 
608  - coord : `lsst.geom.SpherePoint`
609  ICRS center of the search region.
610  - radius : `lsst.geom.Angle`
611  Radius of the search region.
612  - bbox : `lsst.afw.geom.Box2D`
613  Bounding box used to compute the circle.
614  """
615  bbox = lsst.geom.Box2D(bbox) # make sure bbox is double and that we have a copy
616  bbox.grow(self.config.pixelMargin)
617  coord = wcs.pixelToSky(bbox.getCenter())
618  radius = max(coord.separation(wcs.pixelToSky(pp)) for pp in bbox.getCorners())
619  return pipeBase.Struct(coord=coord, radius=radius, bbox=bbox)
620 
621  def getMetadataBox(self, bbox, wcs, filterName=None, calib=None, epoch=None):
622  """Return metadata about the load.
623 
624  This metadata is used for reloading the catalog (e.g., for
625  reconstituting a normalised match list.
626 
627  Parameters
628  ----------
629  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
630  Pixel bounding box.
631  wcs : `lsst.afw.geom.SkyWcs`
632  WCS; used to convert pixel positions to sky coordinates.
633  filterName : `str`
634  Name of camera filter, or `None` or `""` for the default
635  filter.
636  calib : `lsst.afw.image.Calib` (optional)
637  Calibration, or `None` if unknown.
638  epoch : `astropy.time.Time` (optional)
639  Epoch to which to correct proper motion and parallax,
640  or None to not apply such corrections.
641 
642  Returns
643  -------
644  metadata : lsst.daf.base.PropertyList
645  Metadata about the load.
646  """
647  circle = self._calculateCircle(bbox, wcs)
648  return self.getMetadataCircle(circle.coord, circle.radius, filterName, calib)
649 
650  def getMetadataCircle(self, coord, radius, filterName, calib=None, epoch=None):
651  """Return metadata about the load.
652 
653  This metadata is used for reloading the catalog (e.g., for
654  reconstituting a normalised match list.
655 
656  Parameters
657  ----------
658  coord : `lsst.geom.SpherePoint`
659  ICRS center of the search region.
660  radius : `lsst.geom.Angle`
661  Radius of the search region.
662  filterName : `str`
663  Name of camera filter, or `None` or `""` for the default
664  filter.
665  calib : `lsst.afw.image.Calib` (optional)
666  Calibration, or `None` if unknown.
667  epoch : `astropy.time.Time` (optional)
668  Epoch to which to correct proper motion and parallax,
669  or None to not apply such corrections.
670 
671  Returns
672  -------
673  metadata : lsst.daf.base.PropertyList
674  Metadata about the load
675  """
676  md = PropertyList()
677  md.add('RA', coord.getRa().asDegrees(), 'field center in degrees')
678  md.add('DEC', coord.getDec().asDegrees(), 'field center in degrees')
679  md.add('RADIUS', radius.asDegrees(), 'field radius in degrees, minimum')
680  md.add('SMATCHV', 1, 'SourceMatchVector version number')
681  filterName = "UNKNOWN" if filterName is None else str(filterName)
682  md.add('FILTER', filterName, 'filter name for photometric data')
683  md.add('EPOCH', "NONE" if epoch is None else epoch, 'Epoch (TAI MJD) for catalog')
684  return md
685 
686  def joinMatchListWithCatalog(self, matchCat, sourceCat):
687  """Relink an unpersisted match list to sources and reference
688  objects.
689 
690  A match list is persisted and unpersisted as a catalog of IDs
691  produced by afw.table.packMatches(), with match metadata
692  (as returned by the astrometry tasks) in the catalog's metadata
693  attribute. This method converts such a match catalog into a match
694  list, with links to source records and reference object records.
695 
696  Parameters
697  ----------
698  matchCat : `lsst.afw.table.BaseCatalog`
699  Unperisted packed match list.
700  ``matchCat.table.getMetadata()`` must contain match metadata,
701  as returned by the astrometry tasks.
702  sourceCat : `lsst.afw.table.SourceCatalog`
703  Source catalog. As a side effect, the catalog will be sorted
704  by ID.
705 
706  Returns
707  -------
708  matchList : `lsst.afw.table.ReferenceMatchVector`
709  Match list.
710  """
711  matchmeta = matchCat.table.getMetadata()
712  version = matchmeta.getInt('SMATCHV')
713  if version != 1:
714  raise ValueError('SourceMatchVector version number is %i, not 1.' % version)
715  filterName = matchmeta.getString('FILTER').strip()
716  ctrCoord = lsst.geom.SpherePoint(matchmeta.getDouble('RA'),
717  matchmeta.getDouble('DEC'), lsst.geom.degrees)
718  rad = matchmeta.getDouble('RADIUS') * lsst.geom.degrees
719  try:
720  epoch = matchmeta.getDouble('EPOCH')
721  except (pexExcept.NotFoundError, pexExcept.TypeError):
722  epoch = None # Not present, or not correct type means it's not set
723  refCat = self.loadSkyCircle(ctrCoord, rad, filterName, epoch=epoch).refCat
724  refCat.sort()
725  sourceCat.sort()
726  return afwTable.unpackMatches(matchCat, refCat, sourceCat)
727 
728  def applyProperMotions(self, catalog, epoch):
729  """Apply proper motion correction to a reference catalog.
730 
731  Adjust position and position error in the ``catalog``
732  for proper motion to the specified ``epoch``,
733  modifying the catalong in place.
734 
735  Parameters
736  ----------
737  catalog : `lsst.afw.table.SimpleCatalog`
738  Catalog of positions, containing:
739 
740  - Coordinates, retrieved by the table's coordinate key.
741  - ``coord_raErr`` : Error in Right Ascension (rad).
742  - ``coord_decErr`` : Error in Declination (rad).
743  - ``pm_ra`` : Proper motion in Right Ascension (rad/yr,
744  East positive)
745  - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
746  - ``pm_dec`` : Proper motion in Declination (rad/yr,
747  North positive)
748  - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional.
749  - ``epoch`` : Mean epoch of object (an astropy.time.Time)
750  epoch : `astropy.time.Time` (optional)
751  Epoch to which to correct proper motion and parallax,
752  or None to not apply such corrections.
753  """
754  if ("epoch" not in catalog.schema or "pm_ra" not in catalog.schema or "pm_dec" not in catalog.schema):
755  if self.config.requireProperMotion:
756  raise RuntimeError("Proper motion correction required but not available from catalog")
757  self.log.warn("Proper motion correction not available from catalog")
758  return
759  if not catalog.isContiguous():
760  raise RuntimeError("Catalog must be contiguous")
761  catEpoch = astropy.time.Time(catalog["epoch"], scale="tai", format="mjd")
762  self.log.debug("Correcting reference catalog for proper motion to %r", epoch)
763  # Use `epoch.tai` to make sure the time difference is in TAI
764  timeDiffsYears = (epoch.tai - catEpoch).to(astropy.units.yr).value
765  coordKey = catalog.table.getCoordKey()
766  # Compute the offset of each object due to proper motion
767  # as components of the arc of a great circle along RA and Dec
768  pmRaRad = catalog["pm_ra"]
769  pmDecRad = catalog["pm_dec"]
770  offsetsRaRad = pmRaRad*timeDiffsYears
771  offsetsDecRad = pmDecRad*timeDiffsYears
772  # Compute the corresponding bearing and arc length of each offset
773  # due to proper motion, and apply the offset
774  # The factor of 1e6 for computing bearing is intended as
775  # a reasonable scale for typical values of proper motion
776  # in order to avoid large errors for small values of proper motion;
777  # using the offsets is another option, but it can give
778  # needlessly large errors for short duration
779  offsetBearingsRad = numpy.arctan2(pmDecRad*1e6, pmRaRad*1e6)
780  offsetAmountsRad = numpy.hypot(offsetsRaRad, offsetsDecRad)
781  for record, bearingRad, amountRad in zip(catalog, offsetBearingsRad, offsetAmountsRad):
782  record.set(coordKey,
783  record.get(coordKey).offset(bearing=bearingRad*lsst.geom.radians,
784  amount=amountRad*lsst.geom.radians))
785  # Increase error in RA and Dec based on error in proper motion
786  if "coord_raErr" in catalog.schema:
787  catalog["coord_raErr"] = numpy.hypot(catalog["coord_raErr"],
788  catalog["pm_raErr"]*timeDiffsYears)
789  if "coord_decErr" in catalog.schema:
790  catalog["coord_decErr"] = numpy.hypot(catalog["coord_decErr"],
791  catalog["pm_decErr"]*timeDiffsYears)
def loadPixelBox(self, bbox, wcs, filterName=None, calib=None, epoch=None)
def getMetadataBox(self, bbox, wcs, filterName=None, calib=None, epoch=None)
Abstract base class to load objects from reference catalogs.
def makeMinimalSchema(filterNameList, addCentroid=True, addIsPhotometric=False, addIsResolved=False, addIsVariable=False, coordErrDim=2, addProperMotion=False, properMotionErrDim=2, addParallax=False, addParallaxErr=True)
def getMetadataCircle(self, coord, radius, filterName, calib=None, epoch=None)
def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None)