lsst.meas.algorithms  16.0-16-ga49ff433+4
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  """!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, *, addFluxErr=False, 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  addFluxErr : `bool`
443  If True then include flux sigma fields.
444  addIsPhotometric : `bool`
445  If True then add field "photometric".
446  addIsResolved : `bool`
447  If True then add field "resolved".
448  addIsVariable : `bool`
449  If True then add field "variable".
450  coordErrDim : `int`
451  Number of coord error fields; must be one of 0, 2, 3:
452 
453  - If 2 or 3: add fields "coord_raErr" and "coord_decErr".
454  - If 3: also add field "coord_radecErr".
455  addProperMotion : `bool`
456  If True add fields "epoch", "pm_ra", "pm_dec" and "pm_flag".
457  properMotionErrDim : `int`
458  Number of proper motion error fields; must be one of 0, 2, 3;
459  ignored if addProperMotion false:
460  - If 2 or 3: add fields "pm_raErr" and "pm_decErr".
461  - If 3: also add field "pm_radecErr".
462  addParallax : `bool`
463  If True add fields "epoch", "parallax", "parallaxErr"
464  and "parallax_flag".
465  addParallaxErr : `bool`
466  If True add field "parallaxErr"; ignored if addParallax false.
467 
468  Returns
469  -------
470  schema : `lsst.afw.table.Schema`
471  Schema for reference catalog, an
472  `lsst.afw.table.SimpleCatalog`.
473 
474  Notes
475  -----
476  Reference catalogs support additional covariances, such as
477  covariance between RA and proper motion in declination,
478  that are not supported by this method, but can be added after
479  calling this method.
480  """
481  schema = afwTable.SimpleTable.makeMinimalSchema()
482  if addCentroid:
483  afwTable.Point2DKey.addFields(
484  schema,
485  "centroid",
486  "centroid on an exposure, if relevant",
487  "pixel",
488  )
489  schema.addField(
490  field="hasCentroid",
491  type="Flag",
492  doc="is position known?",
493  )
494  for filterName in filterNameList:
495  schema.addField(
496  field="%s_flux" % (filterName,),
497  type=numpy.float64,
498  doc="flux in filter %s" % (filterName,),
499  units="Jy",
500  )
501  if addFluxErr:
502  for filterName in filterNameList:
503  schema.addField(
504  field="%s_fluxErr" % (filterName,),
505  type=numpy.float64,
506  doc="flux uncertainty in filter %s" % (filterName,),
507  units="Jy",
508  )
509  if addIsPhotometric:
510  schema.addField(
511  field="photometric",
512  type="Flag",
513  doc="set if the object can be used for photometric calibration",
514  )
515  if addIsResolved:
516  schema.addField(
517  field="resolved",
518  type="Flag",
519  doc="set if the object is spatially resolved",
520  )
521  if addIsVariable:
522  schema.addField(
523  field="variable",
524  type="Flag",
525  doc="set if the object has variable brightness",
526  )
527  if coordErrDim not in (0, 2, 3):
528  raise ValueError("coordErrDim={}; must be (0, 2, 3)".format(coordErrDim))
529  if coordErrDim > 0:
530  afwTable.CovarianceMatrix2fKey.addFields(
531  schema=schema,
532  prefix="coord",
533  names=["ra", "dec"],
534  units=["rad", "rad"],
535  diagonalOnly=(coordErrDim == 2),
536  )
537 
538  if addProperMotion or addParallax:
539  schema.addField(
540  field="epoch",
541  type=numpy.float64,
542  doc="date of observation (TAI, MJD)",
543  units="day",
544  )
545 
546  if addProperMotion:
547  schema.addField(
548  field="pm_ra",
549  type="Angle",
550  doc="proper motion in the right ascension direction = dra/dt * cos(dec)",
551  units="rad/year",
552  )
553  schema.addField(
554  field="pm_dec",
555  type="Angle",
556  doc="proper motion in the declination direction",
557  units="rad/year",
558  )
559  if properMotionErrDim not in (0, 2, 3):
560  raise ValueError("properMotionErrDim={}; must be (0, 2, 3)".format(properMotionErrDim))
561  if properMotionErrDim > 0:
562  afwTable.CovarianceMatrix2fKey.addFields(
563  schema=schema,
564  prefix="pm",
565  names=["ra", "dec"],
566  units=["rad/year", "rad/year"],
567  diagonalOnly=(properMotionErrDim == 2),
568  )
569  schema.addField(
570  field="pm_flag",
571  type="Flag",
572  doc="Set if proper motion or proper motion error is bad",
573  )
574 
575  if addParallax:
576  schema.addField(
577  field="parallax",
578  type=numpy.float64,
579  doc="parallax",
580  units="rad",
581  )
582  if addParallaxErr:
583  schema.addField(
584  field="parallaxErr",
585  type=numpy.float64,
586  doc="uncertainty in parallax",
587  units="rad",
588  )
589  schema.addField(
590  field="parallax_flag",
591  type="Flag",
592  doc="Set if parallax or parallax error is bad",
593  )
594  return schema
595 
596  def _calculateCircle(self, bbox, wcs):
597  """Compute on-sky center and radius of search region.
598 
599  Parameters
600  ----------
601  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
602  Pixel bounding box.
603  wcs : `lsst.afw.geom.SkyWcs`
604  WCS; used to convert pixel positions to sky coordinates.
605 
606  Returns
607  -------
608  results : `lsst.pipe.base.Struct`
609  A Struct containing:
610 
611  - coord : `lsst.geom.SpherePoint`
612  ICRS center of the search region.
613  - radius : `lsst.geom.Angle`
614  Radius of the search region.
615  - bbox : `lsst.afw.geom.Box2D`
616  Bounding box used to compute the circle.
617  """
618  bbox = lsst.geom.Box2D(bbox) # make sure bbox is double and that we have a copy
619  bbox.grow(self.config.pixelMargin)
620  coord = wcs.pixelToSky(bbox.getCenter())
621  radius = max(coord.separation(wcs.pixelToSky(pp)) for pp in bbox.getCorners())
622  return pipeBase.Struct(coord=coord, radius=radius, bbox=bbox)
623 
624  def getMetadataBox(self, bbox, wcs, filterName=None, calib=None, epoch=None):
625  """Return metadata about the load.
626 
627  This metadata is used for reloading the catalog (e.g., for
628  reconstituting a normalised match list.
629 
630  Parameters
631  ----------
632  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
633  Pixel bounding box.
634  wcs : `lsst.afw.geom.SkyWcs`
635  WCS; used to convert pixel positions to sky coordinates.
636  filterName : `str`
637  Name of camera filter, or `None` or `""` for the default
638  filter.
639  calib : `lsst.afw.image.Calib` (optional)
640  Calibration, or `None` if unknown.
641  epoch : `astropy.time.Time` (optional)
642  Epoch to which to correct proper motion and parallax,
643  or None to not apply such corrections.
644 
645  Returns
646  -------
647  metadata : lsst.daf.base.PropertyList
648  Metadata about the load.
649  """
650  circle = self._calculateCircle(bbox, wcs)
651  return self.getMetadataCircle(circle.coord, circle.radius, filterName, calib)
652 
653  def getMetadataCircle(self, coord, radius, filterName, calib=None, epoch=None):
654  """Return metadata about the load.
655 
656  This metadata is used for reloading the catalog (e.g., for
657  reconstituting a normalised match list.
658 
659  Parameters
660  ----------
661  coord : `lsst.geom.SpherePoint`
662  ICRS center of the search region.
663  radius : `lsst.geom.Angle`
664  Radius of the search region.
665  filterName : `str`
666  Name of camera filter, or `None` or `""` for the default
667  filter.
668  calib : `lsst.afw.image.Calib` (optional)
669  Calibration, or `None` if unknown.
670  epoch : `astropy.time.Time` (optional)
671  Epoch to which to correct proper motion and parallax,
672  or None to not apply such corrections.
673 
674  Returns
675  -------
676  metadata : lsst.daf.base.PropertyList
677  Metadata about the load
678  """
679  md = PropertyList()
680  md.add('RA', coord.getRa().asDegrees(), 'field center in degrees')
681  md.add('DEC', coord.getDec().asDegrees(), 'field center in degrees')
682  md.add('RADIUS', radius.asDegrees(), 'field radius in degrees, minimum')
683  md.add('SMATCHV', 1, 'SourceMatchVector version number')
684  filterName = "UNKNOWN" if filterName is None else str(filterName)
685  md.add('FILTER', filterName, 'filter name for photometric data')
686  md.add('EPOCH', "NONE" if epoch is None else epoch, 'Epoch (TAI MJD) for catalog')
687  return md
688 
689  def joinMatchListWithCatalog(self, matchCat, sourceCat):
690  """Relink an unpersisted match list to sources and reference
691  objects.
692 
693  A match list is persisted and unpersisted as a catalog of IDs
694  produced by afw.table.packMatches(), with match metadata
695  (as returned by the astrometry tasks) in the catalog's metadata
696  attribute. This method converts such a match catalog into a match
697  list, with links to source records and reference object records.
698 
699  Parameters
700  ----------
701  matchCat : `lsst.afw.table.BaseCatalog`
702  Unperisted packed match list.
703  ``matchCat.table.getMetadata()`` must contain match metadata,
704  as returned by the astrometry tasks.
705  sourceCat : `lsst.afw.table.SourceCatalog`
706  Source catalog. As a side effect, the catalog will be sorted
707  by ID.
708 
709  Returns
710  -------
711  matchList : `lsst.afw.table.ReferenceMatchVector`
712  Match list.
713  """
714  matchmeta = matchCat.table.getMetadata()
715  version = matchmeta.getInt('SMATCHV')
716  if version != 1:
717  raise ValueError('SourceMatchVector version number is %i, not 1.' % version)
718  filterName = matchmeta.getString('FILTER').strip()
719  ctrCoord = lsst.geom.SpherePoint(matchmeta.getDouble('RA'),
720  matchmeta.getDouble('DEC'), lsst.geom.degrees)
721  rad = matchmeta.getDouble('RADIUS') * lsst.geom.degrees
722  try:
723  epoch = matchmeta.getDouble('EPOCH')
724  except (pexExcept.NotFoundError, pexExcept.TypeError):
725  epoch = None # Not present, or not correct type means it's not set
726  refCat = self.loadSkyCircle(ctrCoord, rad, filterName, epoch=epoch).refCat
727  refCat.sort()
728  sourceCat.sort()
729  return afwTable.unpackMatches(matchCat, refCat, sourceCat)
730 
731  def applyProperMotions(self, catalog, epoch):
732  """Apply proper motion correction to a reference catalog.
733 
734  Adjust position and position error in the ``catalog``
735  for proper motion to the specified ``epoch``,
736  modifying the catalong in place.
737 
738  Parameters
739  ----------
740  catalog : `lsst.afw.table.SimpleCatalog`
741  Catalog of positions, containing:
742 
743  - Coordinates, retrieved by the table's coordinate key.
744  - ``coord_raErr`` : Error in Right Ascension (rad).
745  - ``coord_decErr`` : Error in Declination (rad).
746  - ``pm_ra`` : Proper motion in Right Ascension (rad/yr,
747  East positive)
748  - ``pm_raErr`` : Error in ``pm_ra`` (rad/yr), optional.
749  - ``pm_dec`` : Proper motion in Declination (rad/yr,
750  North positive)
751  - ``pm_decErr`` : Error in ``pm_dec`` (rad/yr), optional.
752  - ``epoch`` : Mean epoch of object (an astropy.time.Time)
753  epoch : `astropy.time.Time` (optional)
754  Epoch to which to correct proper motion and parallax,
755  or None to not apply such corrections.
756  """
757  if ("epoch" not in catalog.schema or "pm_ra" not in catalog.schema or "pm_dec" not in catalog.schema):
758  if self.config.requireProperMotion:
759  raise RuntimeError("Proper motion correction required but not available from catalog")
760  self.log.warn("Proper motion correction not available from catalog")
761  return
762  if not catalog.isContiguous():
763  raise RuntimeError("Catalog must be contiguous")
764  catEpoch = astropy.time.Time(catalog["epoch"], scale="tai", format="mjd")
765  self.log.debug("Correcting reference catalog for proper motion to %r", epoch)
766  # Use `epoch.tai` to make sure the time difference is in TAI
767  timeDiffsYears = (epoch.tai - catEpoch).to(astropy.units.yr).value
768  coordKey = catalog.table.getCoordKey()
769  # Compute the offset of each object due to proper motion
770  # as components of the arc of a great circle along RA and Dec
771  pmRaRad = catalog["pm_ra"]
772  pmDecRad = catalog["pm_dec"]
773  offsetsRaRad = pmRaRad*timeDiffsYears
774  offsetsDecRad = pmDecRad*timeDiffsYears
775  # Compute the corresponding bearing and arc length of each offset
776  # due to proper motion, and apply the offset
777  # The factor of 1e6 for computing bearing is intended as
778  # a reasonable scale for typical values of proper motion
779  # in order to avoid large errors for small values of proper motion;
780  # using the offsets is another option, but it can give
781  # needlessly large errors for short duration
782  offsetBearingsRad = numpy.arctan2(pmDecRad*1e6, pmRaRad*1e6)
783  offsetAmountsRad = numpy.hypot(offsetsRaRad, offsetsDecRad)
784  for record, bearingRad, amountRad in zip(catalog, offsetBearingsRad, offsetAmountsRad):
785  record.set(coordKey,
786  record.get(coordKey).offset(bearing=bearingRad*lsst.geom.radians,
787  amount=amountRad*lsst.geom.radians))
788  # Increase error in RA and Dec based on error in proper motion
789  if "coord_raErr" in catalog.schema:
790  catalog["coord_raErr"] = numpy.hypot(catalog["coord_raErr"],
791  catalog["pm_raErr"]*timeDiffsYears)
792  if "coord_decErr" in catalog.schema:
793  catalog["coord_decErr"] = numpy.hypot(catalog["coord_decErr"],
794  catalog["pm_decErr"]*timeDiffsYears)
def loadPixelBox(self, bbox, wcs, filterName=None, calib=None, epoch=None)
def makeMinimalSchema(filterNameList, addFluxErr=False, addCentroid=True, addIsPhotometric=False, addIsResolved=False, addIsVariable=False, coordErrDim=2, addProperMotion=False, properMotionErrDim=2, addParallax=False, addParallaxErr=True)
def getMetadataBox(self, bbox, wcs, filterName=None, calib=None, epoch=None)
Abstract base class to load objects from reference catalogs.
def getMetadataCircle(self, coord, radius, filterName, calib=None, epoch=None)
def loadSkyCircle(self, ctrCoord, radius, filterName=None, epoch=None)