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