lsst.meas.algorithms  15.0-3-gd5b9ff95+1
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 from __future__ import absolute_import, division, print_function
24 
25 __all__ = ["getRefFluxField", "getRefFluxKeys", "LoadReferenceObjectsTask", "LoadReferenceObjectsConfig"]
26 
27 import abc
28 
29 import numpy
30 
31 import lsst.afw.geom as afwGeom
32 import lsst.afw.table as afwTable
33 import lsst.pex.config as pexConfig
34 import lsst.pipe.base as pipeBase
35 from lsst.daf.base import PropertyList
36 from future.utils import with_metaclass
37 
38 
39 def getRefFluxField(schema, filterName=None):
40  """!Get name of flux field in schema
41 
42  if filterName is specified:
43  return *filterName*_camFlux if present
44  else return *filterName*_flux if present (camera filter name matches reference filter name)
45  else throw RuntimeError
46  else:
47  return camFlux, if present,
48  else throw RuntimeError
49 
50  @param[in] schema reference catalog schema
51  @param[in] filterName name of camera filter
52  @return flux field name
53  @throw RuntimeError if appropriate field is not found
54  """
55  if not isinstance(schema, afwTable.Schema):
56  raise RuntimeError("schema=%s is not a schema" % (schema,))
57  if filterName:
58  fluxFieldList = [filterName + "_camFlux", filterName + "_flux"]
59  else:
60  fluxFieldList = ["camFlux"]
61  for fluxField in fluxFieldList:
62  if fluxField in schema:
63  return fluxField
64 
65  raise RuntimeError("Could not find flux field(s) %s" % (", ".join(fluxFieldList)))
66 
67 
68 def getRefFluxKeys(schema, filterName=None):
69  """!Return flux and flux error keys
70 
71  @param[in] schema reference catalog schema
72  @param[in] filterName name of camera filter
73  @return a pair of keys:
74  flux key
75  flux error key, if present, else None
76  @throw RuntimeError if flux field not found
77  """
78  fluxField = getRefFluxField(schema, filterName)
79  fluxErrField = fluxField + "Sigma"
80  fluxKey = schema[fluxField].asKey()
81  try:
82  fluxErrKey = schema[fluxErrField].asKey()
83  except Exception:
84  fluxErrKey = None
85  return (fluxKey, fluxErrKey)
86 
87 
88 class LoadReferenceObjectsConfig(pexConfig.Config):
89  pixelMargin = pexConfig.RangeField(
90  doc="Padding to add to 4 all edges of the bounding box (pixels)",
91  dtype=int,
92  default=300,
93  min=0,
94  )
95  defaultFilter = pexConfig.Field(
96  doc="Default reference catalog filter to use if filter not specified in exposure; " +
97  "if blank then filter must be specified in exposure",
98  dtype=str,
99  default="",
100  )
101  filterMap = pexConfig.DictField(
102  doc="Mapping of camera filter name: reference catalog filter name; " +
103  "each reference filter must exist",
104  keytype=str,
105  itemtype=str,
106  default={},
107  )
108 
109 # The following comment block adds a link to this task from the Task Documentation page.
110 
116 
117 
118 class LoadReferenceObjectsTask(with_metaclass(abc.ABCMeta, pipeBase.Task)):
119  """!Abstract base class to load objects from reference catalogs
120 
121  @anchor LoadReferenceObjectsTask_
122 
123  @section meas_algorithms_loadReferenceObjects_Contents Contents
124 
125  - @ref meas_algorithms_loadReferenceObjects_Purpose
126  - @ref meas_algorithms_loadReferenceObjects_Initialize
127  - @ref meas_algorithms_loadReferenceObjects_IO
128  - @ref meas_algorithms_loadReferenceObjects_Schema
129  - @ref meas_algorithms_loadReferenceObjects_Config
130 
131  @section meas_algorithms_loadReferenceObjects_Purpose Description
132 
133  Abstract base class for tasks that load objects from a reference catalog
134  in a particular region of the sky.
135 
136  Implementations must subclass this class, override the loadSkyCircle method,
137  and will typically override the value of ConfigClass with a task-specific config class.
138 
139  @section meas_algorithms_loadReferenceObjects_Initialize Task initialisation
140 
141  @copydoc \_\_init\_\_
142 
143  @section meas_algorithms_loadReferenceObjects_IO Invoking the Task
144 
145  @copydoc loadObjectsInBBox
146 
147  @section meas_algorithms_loadReferenceObjects_Schema Schema of the reference object catalog
148 
149  Reference object catalogs are instances of lsst.afw.table.SimpleCatalog with the following schema
150  (other fields may also be present):
151  - coord: ICRS position of star on sky (an lsst.afw.geom.SpherePoint)
152  - centroid: position of star on an exposure, if relevant (an lsst.afw.Point2D)
153  - hasCentroid: is centroid usable?
154  - *referenceFilterName*_flux: brightness in the specified reference catalog filter (Jy)
155  Note: the function lsst.afw.image.abMagFromFlux will convert flux in Jy to AB Magnitude.
156  - *referenceFilterName*_fluxSigma (optional): brightness standard deviation (Jy);
157  omitted if no data is available; possibly nan if data is available for some objects but not others
158  - camFlux: brightness in default camera filter (Jy); omitted if defaultFilter not specified
159  - camFluxSigma: brightness standard deviation for default camera filter;
160  omitted if defaultFilter not specified or standard deviation not available that filter
161  - *cameraFilterName*_camFlux: brightness in specified camera filter (Jy)
162  - *cameraFilterName*_camFluxSigma (optional): brightness standard deviation
163  in specified camera filter (Jy); omitted if no data is available;
164  possibly nan if data is available for some objects but not others
165  - photometric (optional): is the object usable for photometric calibration?
166  - resolved (optional): is the object spatially resolved?
167  - variable (optional): does the object have variable brightness?
168 
169  @section meas_algorithms_loadReferenceObjects_Config Configuration parameters
170 
171  See @ref LoadReferenceObjectsConfig for a base set of configuration parameters.
172  Most subclasses will add configuration variables.
173  """
174  ConfigClass = LoadReferenceObjectsConfig
175  _DefaultName = "LoadReferenceObjects"
176 
177  def __init__(self, butler=None, *args, **kwargs):
178  """!Construct a LoadReferenceObjectsTask
179 
180  @param[in] butler A daf.persistence.Butler object. This allows subclasses to use the butler to
181  access reference catalog files using the stack I/O abstraction scheme.
182  """
183  pipeBase.Task.__init__(self, *args, **kwargs)
184  self.butler = butler
185 
186  @pipeBase.timeMethod
187  def loadPixelBox(self, bbox, wcs, filterName=None, calib=None):
188  """!Load reference objects that overlap a pixel-based rectangular region
189 
190  The search algorithm works by searching in a region in sky coordinates whose center is the center
191  of the bbox and radius is large enough to just include all 4 corners of the bbox.
192  Stars that lie outside the bbox are then trimmed from the list.
193 
194  @param[in] bbox bounding box for pixels (an lsst.afw.geom.Box2I or Box2D)
195  @param[in] wcs WCS (an lsst.afw.geom.SkyWcs)
196  @param[in] filterName name of camera filter, or None or blank for the default filter
197  @param[in] calib calibration, or None if unknown
198 
199  @return an lsst.pipe.base.Struct containing:
200  - refCat a catalog of reference objects with the
201  \link meas_algorithms_loadReferenceObjects_Schema standard schema \endlink
202  as documented in LoadReferenceObjects, including photometric, resolved and variable;
203  hasCentroid is False for all objects.
204  - fluxField = name of flux field for specified filterName
205  """
206  circle = self._calculateCircle(bbox, wcs)
207 
208  # find objects in circle
209  self.log.info("Loading reference objects using center %s and radius %s deg" %
210  (circle.coord, circle.radius.asDegrees()))
211  loadRes = self.loadSkyCircle(circle.coord, circle.radius, filterName)
212  refCat = loadRes.refCat
213  numFound = len(refCat)
214 
215  # trim objects outside bbox
216  refCat = self._trimToBBox(refCat=refCat, bbox=circle.bbox, wcs=wcs)
217  numTrimmed = numFound - len(refCat)
218  self.log.debug("trimmed %d out-of-bbox objects, leaving %d", numTrimmed, len(refCat))
219  self.log.info("Loaded %d reference objects", len(refCat))
220 
221  loadRes.refCat = refCat # should be a no-op, but just in case
222  return loadRes
223 
224  @abc.abstractmethod
225  def loadSkyCircle(self, ctrCoord, radius, filterName=None):
226  """!Load reference objects that overlap a circular sky region
227 
228  @param[in] ctrCoord ICRS center of search region (an lsst.afw.geom.SpherePoint)
229  @param[in] radius radius of search region (an lsst.afw.geom.Angle)
230  @param[in] filterName name of filter, or None for the default filter;
231  used for flux values in case we have flux limits (which are not yet implemented)
232 
233  @return an lsst.pipe.base.Struct containing:
234  - refCat a catalog of reference objects with the
235  \link meas_algorithms_loadReferenceObjects_Schema standard schema \endlink
236  as documented in LoadReferenceObjects, including photometric, resolved and variable;
237  hasCentroid is False for all objects.
238  - fluxField = name of flux field for specified filterName
239  """
240  return
241 
242  @staticmethod
243  def _trimToBBox(refCat, bbox, wcs):
244  """!Remove objects outside a given pixel-based bbox and set centroid and hasCentroid fields
245 
246  @param[in,out] refCat a catalog of objects (an lsst.afw.table.SimpleCatalog,
247  or other table type that has fields "coord", "centroid" and "hasCentroid").
248  The "coord" field is read.
249  The "centroid" and "hasCentroid" fields are set.
250  @param[in] bbox pixel region (an afwImage.Box2D)
251  @param[in] wcs WCS used to convert sky position to pixel position (an lsst.afw.math.WCS)
252 
253  @return a catalog of reference objects in bbox, with centroid and hasCentroid fields set
254  """
255  afwTable.updateRefCentroids(wcs, refCat)
256  centroidKey = afwTable.Point2DKey(refCat.schema["centroid"])
257  retStarCat = type(refCat)(refCat.table)
258  for star in refCat:
259  point = star.get(centroidKey)
260  if bbox.contains(point):
261  retStarCat.append(star)
262  return retStarCat
263 
264  def _addFluxAliases(self, schema):
265  """Add aliases for camera filter fluxes to the schema
266 
267  If self.config.defaultFilter then adds these aliases:
268  camFlux: <defaultFilter>_flux
269  camFluxSigma: <defaultFilter>_fluxSigma, if the latter exists
270 
271  For each camFilter: refFilter in self.config.filterMap adds these aliases:
272  <camFilter>_camFlux: <refFilter>_flux
273  <camFilter>_camFluxSigma: <refFilter>_fluxSigma, if the latter exists
274 
275  @throw RuntimeError if any reference flux field is missing from the schema
276  """
277  aliasMap = schema.getAliasMap()
278 
279  def addAliasesForOneFilter(filterName, refFilterName):
280  """Add aliases for a single filter
281 
282  @param[in] filterName camera filter name, or ""
283  the name is <filterName>_camFlux or camFlux if filterName is None
284  @param[in] refFilterName reference filter name; <refFilterName>_flux must exist
285  """
286  camFluxName = filterName + "_camFlux" if filterName is not None else "camFlux"
287  refFluxName = refFilterName + "_flux"
288  if refFluxName not in schema:
289  raise RuntimeError("Unknown reference filter %s" % (refFluxName,))
290  aliasMap.set(camFluxName, refFluxName)
291  refFluxErrName = refFluxName + "Sigma"
292  if refFluxErrName in schema:
293  camFluxErrName = camFluxName + "Sigma"
294  aliasMap.set(camFluxErrName, refFluxErrName)
295 
296  if self.config.defaultFilter:
297  addAliasesForOneFilter(None, self.config.defaultFilter)
298 
299  for filterName, refFilterName in self.config.filterMap.items():
300  addAliasesForOneFilter(filterName, refFilterName)
301 
302  @staticmethod
303  def makeMinimalSchema(filterNameList, addFluxSigma=False,
304  addIsPhotometric=False, addIsResolved=False, addIsVariable=False):
305  """!Make the standard schema for reference object catalogs
306 
307  @param[in] filterNameList list of filter names; used to create *filterName*_flux fields
308  @param[in] addFluxSigma if True then include flux sigma fields
309  @param[in] addIsPhotometric if True add field "photometric"
310  @param[in] addIsResolved if True add field "resolved"
311  @param[in] addIsVariable if True add field "variable"
312  """
313  schema = afwTable.SimpleTable.makeMinimalSchema()
314  afwTable.Point2DKey.addFields(
315  schema,
316  "centroid",
317  "centroid on an exposure, if relevant",
318  "pixel",
319  )
320  schema.addField(
321  field="hasCentroid",
322  type="Flag",
323  doc="is position known?",
324  )
325  for filterName in filterNameList:
326  schema.addField(
327  field="%s_flux" % (filterName,),
328  type=numpy.float64,
329  doc="flux in filter %s" % (filterName,),
330  units="Jy",
331  )
332  if addFluxSigma:
333  for filterName in filterNameList:
334  schema.addField(
335  field="%s_fluxSigma" % (filterName,),
336  type=numpy.float64,
337  doc="flux uncertainty in filter %s" % (filterName,),
338  units="Jy",
339  )
340  if addIsPhotometric:
341  schema.addField(
342  field="photometric",
343  type="Flag",
344  doc="set if the object can be used for photometric calibration",
345  )
346  if addIsResolved:
347  schema.addField(
348  field="resolved",
349  type="Flag",
350  doc="set if the object is spatially resolved",
351  )
352  if addIsVariable:
353  schema.addField(
354  field="variable",
355  type="Flag",
356  doc="set if the object has variable brightness",
357  )
358  return schema
359 
360  def _calculateCircle(self, bbox, wcs):
361  """!Compute on-sky center and radius of search region
362 
363  @param[in] bbox bounding box for pixels (an lsst.afw.geom.Box2I or Box2D)
364  @param[in] wcs WCS (an lsst.afw.geom.SkyWcs)
365  @return an lsst.pipe.base.Struct containing:
366  - coord: ICRS center of the search region (lsst.afw.geom.SpherePoint)
367  - radius: the radius of the search region (lsst.afw.geom.Angle)
368  - bbox: the bounding box used to compute the circle (lsst.afw.geom.Box2D)
369  """
370  bbox = afwGeom.Box2D(bbox) # make sure bbox is double and that we have a copy
371  bbox.grow(self.config.pixelMargin)
372  coord = wcs.pixelToSky(bbox.getCenter())
373  radius = max(coord.separation(wcs.pixelToSky(pp)) for pp in bbox.getCorners())
374  return pipeBase.Struct(coord=coord, radius=radius, bbox=bbox)
375 
376  def getMetadataBox(self, bbox, wcs, filterName=None, calib=None):
377  """!Return metadata about the load
378 
379  This metadata is used for reloading the catalog (e.g., for
380  reconstituting a normalised match list.
381 
382  @param[in] bbox bounding box for pixels (an lsst.afw.geom.Box2I or Box2D)
383  @param[in] wcs WCS (an lsst.afw.geom.SkyWcs)
384  @param[in] filterName name of camera filter, or None or blank for the default filter
385  @param[in] calib calibration, or None if unknown
386  @return metadata (lsst.daf.base.PropertyList)
387  """
388  circle = self._calculateCircle(bbox, wcs)
389  return self.getMetadataCircle(circle.coord, circle.radius, filterName, calib)
390 
391  def getMetadataCircle(self, coord, radius, filterName, calib=None):
392  """!Return metadata about the load
393 
394  This metadata is used for reloading the catalog (e.g., for
395  reconstituting a normalised match list.
396 
397  @param[in] coord ICRS centr of circle (lsst.afw.geom.SpherePoint)
398  @param[in] radius radius of circle (lsst.afw.geom.Angle)
399  @param[in] filterName name of camera filter, or None or blank for the default filter
400  @param[in] calib calibration, or None if unknown
401  @return metadata (lsst.daf.base.PropertyList)
402  """
403  md = PropertyList()
404  md.add('RA', coord.getRa().asDegrees(), 'field center in degrees')
405  md.add('DEC', coord.getDec().asDegrees(), 'field center in degrees')
406  md.add('RADIUS', radius.asDegrees(), 'field radius in degrees, minimum')
407  md.add('SMATCHV', 1, 'SourceMatchVector version number')
408  filterName = "UNKNOWN" if filterName is None else str(filterName)
409  md.add('FILTER', filterName, 'filter name for photometric data')
410  return md
411 
412  def joinMatchListWithCatalog(self, matchCat, sourceCat):
413  """!Relink an unpersisted match list to sources and reference objects
414 
415  A match list is persisted and unpersisted as a catalog of IDs produced by
416  afw.table.packMatches(), with match metadata (as returned by the astrometry tasks)
417  in the catalog's metadata attribute. This method converts such a match catalog
418  into a match list (an lsst.afw.table.ReferenceMatchVector) with links to source
419  records and reference object records.
420 
421  @param[in] matchCat Unperisted packed match list (an lsst.afw.table.BaseCatalog).
422  matchCat.table.getMetadata() must contain match metadata,
423  as returned by the astrometry tasks.
424  @param[in,out] sourceCat Source catalog (an lsst.afw.table.SourceCatalog).
425  As a side effect, the catalog will be sorted by ID.
426 
427  @return the match list (an lsst.afw.table.ReferenceMatchVector)
428  """
429  matchmeta = matchCat.table.getMetadata()
430  version = matchmeta.getInt('SMATCHV')
431  if version != 1:
432  raise ValueError('SourceMatchVector version number is %i, not 1.' % version)
433  filterName = matchmeta.getString('FILTER').strip()
434  ctrCoord = afwGeom.SpherePoint(matchmeta.getDouble('RA'),
435  matchmeta.getDouble('DEC'), afwGeom.degrees)
436  rad = matchmeta.getDouble('RADIUS') * afwGeom.degrees
437  refCat = self.loadSkyCircle(ctrCoord, rad, filterName).refCat
438  refCat.sort()
439  sourceCat.sort()
440  return afwTable.unpackMatches(matchCat, refCat, sourceCat)
def joinMatchListWithCatalog(self, matchCat, sourceCat)
Relink an unpersisted match list to sources and reference objects.
def __init__(self, butler=None, args, kwargs)
Construct a LoadReferenceObjectsTask.
def _trimToBBox(refCat, bbox, wcs)
Remove objects outside a given pixel-based bbox and set centroid and hasCentroid fields.
def loadPixelBox(self, bbox, wcs, filterName=None, calib=None)
Load reference objects that overlap a pixel-based rectangular region.
def getRefFluxField(schema, filterName=None)
Get name of flux field in schema.
def getMetadataCircle(self, coord, radius, filterName, calib=None)
Return metadata about the load.
def getRefFluxKeys(schema, filterName=None)
Return flux and flux error keys.
def makeMinimalSchema(filterNameList, addFluxSigma=False, addIsPhotometric=False, addIsResolved=False, addIsVariable=False)
Make the standard schema for reference object catalogs.
Abstract base class to load objects from reference catalogs.
def loadSkyCircle(self, ctrCoord, radius, filterName=None)
Load reference objects that overlap a circular sky region.
def _calculateCircle(self, bbox, wcs)
Compute on-sky center and radius of search region.
def getMetadataBox(self, bbox, wcs, filterName=None, calib=None)
Return metadata about the load.