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