lsst.meas.astrom  13.0-14-g9415442+40
 All Classes Namespaces Files Functions Variables Typedefs Friends Macros Groups Pages
matchOptimisticBContinued.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 
3 __all__ = ["matchOptimisticB", "MatchOptimisticBTask", "MatchOptimisticBConfig",
4  "MatchTolerance"]
5 
6 from builtins import range
7 from builtins import object
8 import math
9 
10 import numpy as np
11 
12 from lsst.afw.table import Point2DKey
13 import lsst.pex.config as pexConfig
14 import lsst.pipe.base as pipeBase
15 from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry
16 
17 from ..setMatchDistance import setMatchDistance
18 from . import matchOptimisticB, MatchOptimisticBControl
19 
20 
21 class MatchTolerance(object):
22  """ Stores match tolerances for use in AstrometryTask and later
23  iterations of the matcher.
24 
25  Attributes
26  ----------
27  maxMatchDist : lsst.afw.geom.Angle
28  """
29 
30  def __init__(self, maxMatchDist=None):
31  """ MatchOptimsiticBTask relies on a maximum distance for matching
32  set by either the default in MatchOptimisticBConfig or the 2 sigma
33  scatter found after AstrometryTask has fit for a wcs.
34  """
35  self.maxMatchDist = maxMatchDist
36 
37 
38 class MatchOptimisticBConfig(pexConfig.Config):
39  """Configuration for MatchOptimisticBTask
40  """
41  maxMatchDistArcSec = pexConfig.RangeField(
42  doc="Maximum separation between reference objects and sources "
43  "beyond which they will not be considered a match (arcsec)",
44  dtype=float,
45  default=3,
46  min=0,
47  )
48  numBrightStars = pexConfig.RangeField(
49  doc="Number of bright stars to use",
50  dtype=int,
51  default=50,
52  min=2,
53  )
54  minMatchedPairs = pexConfig.RangeField(
55  doc="Minimum number of matched pairs; see also minFracMatchedPairs",
56  dtype=int,
57  default=30,
58  min=2,
59  )
60  minFracMatchedPairs = pexConfig.RangeField(
61  doc="Minimum number of matched pairs as a fraction of the smaller of "
62  "the number of reference stars or the number of good sources; "
63  "the actual minimum is the smaller of this value or minMatchedPairs",
64  dtype=float,
65  default=0.3,
66  min=0,
67  max=1,
68  )
69  maxOffsetPix = pexConfig.RangeField(
70  doc="Maximum allowed shift of WCS, due to matching (pixel)",
71  dtype=int,
72  default=300,
73  max=4000,
74  )
75  maxRotationDeg = pexConfig.RangeField(
76  doc="Rotation angle allowed between sources and position reference objects (degrees)",
77  dtype=float,
78  default=1.0,
79  max=6.0,
80  )
81  allowedNonperpDeg = pexConfig.RangeField(
82  doc="Allowed non-perpendicularity of x and y (degree)",
83  dtype=float,
84  default=3.0,
85  max=45.0,
86  )
87  numPointsForShape = pexConfig.Field(
88  doc="number of points to define a shape for matching",
89  dtype=int,
90  default=6,
91  )
92  maxDeterminant = pexConfig.Field(
93  doc="maximum determinant of linear transformation matrix for a usable solution",
94  dtype=float,
95  default=0.02,
96  )
97  sourceSelector = sourceSelectorRegistry.makeField(
98  doc="How to select sources for cross-matching",
99  default="matcher"
100  )
101 
102  def setDefaults(self):
103  sourceSelector = self.sourceSelector["matcher"]
104  sourceSelector.setDefaults()
105 
106 
107 # The following block adds links to this task from the Task Documentation page.
108 # \addtogroup LSST_task_documentation
109 # \{
110 # \page measAstrom_matchOptimisticBTask
111 # \ref MatchOptimisticBTask "MatchOptimisticBTask"
112 # Match sources to reference objects
113 # \}
114 
115 
116 class MatchOptimisticBTask(pipeBase.Task):
117  """!Match sources to reference objects
118 
119  @anchor MatchOptimisticBTask_
120 
121  @section meas_astrom_matchOptimisticB_Contents Contents
122 
123  - @ref meas_astrom_matchOptimisticB_Purpose
124  - @ref meas_astrom_matchOptimisticB_Initialize
125  - @ref meas_astrom_matchOptimisticB_IO
126  - @ref meas_astrom_matchOptimisticB_Config
127  - @ref meas_astrom_matchOptimisticB_Example
128  - @ref meas_astrom_matchOptimisticB_Debug
129 
130  @section meas_astrom_matchOptimisticB_Purpose Description
131 
132  Match sources to reference objects. This is often done as a preliminary step to fitting an astrometric
133  or photometric solution. For details about the matching algorithm see matchOptimisticB.h
134 
135  @section meas_astrom_matchOptimisticB_Initialize Task initialisation
136 
137  @copydoc \_\_init\_\_
138 
139  @section meas_astrom_matchOptimisticB_IO Invoking the Task
140 
141  @copydoc matchObjectsToSources
142 
143  @section meas_astrom_matchOptimisticB_Config Configuration parameters
144 
145  See @ref MatchOptimisticBConfig
146 
147  To modify how usable sources are selected, specify a different source
148  selector in `config.sourceSelector`.
149 
150  @section meas_astrom_matchOptimisticB_Example A complete example of using MatchOptimisticBTask
151 
152  MatchOptimisticBTask is a subtask of AstrometryTask, which is called by PhotoCalTask.
153  See \ref pipe_tasks_photocal_Example.
154 
155  @section meas_astrom_matchOptimisticB_Debug Debug variables
156 
157  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
158  flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about
159  @b debug.py files.
160 
161  The available variables in MatchOptimisticBTask are:
162  <DL>
163  <DT> @c verbose (bool)
164  <DD> If True then the matcher prints debug messages to stdout
165  </DL>
166 
167  To investigate the @ref meas_astrom_matchOptimisticB_Debug, put something like
168  @code{.py}
169  import lsstDebug
170  def DebugInfo(name):
171  debug = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
172  if name == "lsst.pipe.tasks.astrometry":
173  debug.verbose = True
174 
175  return debug
176 
177  lsstDebug.Info = DebugInfo
178  @endcode
179  into your debug.py file and run this task with the @c --debug flag.
180  """
181  ConfigClass = MatchOptimisticBConfig
182  _DefaultName = "matchObjectsToSources"
183 
184  def __init__(self, **kwargs):
185  pipeBase.Task.__init__(self, **kwargs)
186  self.makeSubtask("sourceSelector")
187 
188  def filterStars(self, refCat):
189  """Extra filtering pass; subclass if desired
190  """
191  return refCat
192 
193  @pipeBase.timeMethod
194  def matchObjectsToSources(self, refCat, sourceCat, wcs, refFluxField,
195  match_tolerance=None):
196  """!Match sources to position reference stars
197 
198  @param[in] refCat catalog of reference objects that overlap the exposure; reads fields for:
199  - coord
200  - the specified flux field
201  @param[in] sourceCat catalog of sources found on an exposure; reads fields for:
202  - centroid
203  - centroid flag
204  - edge flag
205  - saturated flag
206  - aperture flux, if found, else PSF flux
207  @param[in] wcs estimated WCS
208  @param[in] refFluxField field of refCat to use for flux
209  @param[in] match_tolerance a MatchTolerance object for specifying
210  tolerances. Must at minimum contain a lsst.afw.geom.Angle
211  called maxMatchDist that communicates state between AstrometryTask
212  and the matcher Task.
213  @return an lsst.pipe.base.Struct with fields:
214  - matches a list of matches, each instance of lsst.afw.table.ReferenceMatch
215  - usableSourcCat a catalog of sources potentially usable for matching.
216  For this fitter usable sources include unresolved sources not too near the edge.
217  It includes saturated sources, even those these are removed from the final match list,
218  because saturated sources may be used to determine the match list.
219  """
220  import lsstDebug
221  debug = lsstDebug.Info(__name__)
222 
223  preNumObj = len(refCat)
224  refCat = self.filterStars(refCat)
225  numRefObj = len(refCat)
226 
227  if self.log:
228  self.log.info("filterStars purged %d reference stars, leaving %d stars" %
229  (preNumObj - numRefObj, numRefObj))
230 
231  if match_tolerance is None:
232  match_tolerance = MatchTolerance()
233 
234  # usableSourceCat: sources that are good but may be saturated
235  numSources = len(sourceCat)
236  selectedSources = self.sourceSelector.selectSources(sourceCat)
237  usableSourceCat = selectedSources.sourceCat
238  numUsableSources = len(usableSourceCat)
239  self.log.info("Purged %d unusable sources, leaving %d usable sources" %
240  (numSources - numUsableSources, numUsableSources))
241 
242  if len(usableSourceCat) == 0:
243  raise pipeBase.TaskError("No sources are usable")
244 
245  del sourceCat # avoid accidentally using sourceCat; use usableSourceCat or goodSourceCat from now on
246 
247  minMatchedPairs = min(self.config.minMatchedPairs,
248  int(self.config.minFracMatchedPairs * min([len(refCat), len(usableSourceCat)])))
249 
250  # match usable (possibly saturated) sources and then purge saturated sources from the match list
251  usableMatches = self._doMatch(
252  refCat=refCat,
253  sourceCat=usableSourceCat,
254  wcs=wcs,
255  refFluxField=refFluxField,
256  numUsableSources=numUsableSources,
257  minMatchedPairs=minMatchedPairs,
258  maxMatchDist=match_tolerance.maxMatchDist,
259  sourceFluxField=self.sourceSelector.fluxField,
260  verbose=debug.verbose,
261  )
262 
263  # cull non-good sources
264  matches = []
265  self._getIsGoodKeys(usableSourceCat.schema)
266  for match in usableMatches:
267  if self._isGoodTest(match.second):
268  # Append the isGood match.
269  matches.append(match)
270 
271  self.log.debug("Found %d usable matches, of which %d had good sources",
272  len(usableMatches), len(matches))
273 
274  if len(matches) == 0:
275  raise RuntimeError("Unable to match sources")
276 
277  self.log.info("Matched %d sources" % len(matches))
278  if len(matches) < minMatchedPairs:
279  self.log.warn("Number of matches is smaller than request")
280 
281  return pipeBase.Struct(
282  matches=matches,
283  usableSourceCat=usableSourceCat,
284  match_tolerance=match_tolerance,
285  )
286 
287  def _getIsGoodKeys(self, schema):
288  self.edgeKey = schema["base_PixelFlags_flag_edge"].asKey()
289  self.interpolatedCenterKey = schema["base_PixelFlags_flag_interpolatedCenter"].asKey()
290  self.saturatedKey = schema["base_PixelFlags_flag_saturated"].asKey()
291 
292  def _isGoodTest(self, source):
293  """
294  This is a hard coded version of the isGood flag from the old SourceInfo class that used to be
295  part of this class. This is done current as the API for sourceSelector does not currently
296  support matchLists.
297  """
298  return (not source.get(self.edgeKey) and
299  not source.get(self.interpolatedCenterKey) and
300  not source.get(self.saturatedKey))
301 
302  @pipeBase.timeMethod
303  def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs,
304  maxMatchDist, sourceFluxField, verbose):
305  """!Implementation of matching sources to position reference stars
306 
307  Unlike matchObjectsToSources, this method does not check if the sources are suitable.
308 
309  @param[in] refCat catalog of position reference stars that overlap an exposure
310  @param[in] sourceCat catalog of sources found on the exposure
311  @param[in] wcs estimated WCS of exposure
312  @param[in] refFluxField field of refCat to use for flux
313  @param[in] numUsableSources number of usable sources (sources with known centroid
314  that are not near the edge, but may be saturated)
315  @param[in] minMatchedPairs minimum number of matches
316  @param[in] maxMatchDist maximum on-sky distance between reference objects and sources
317  (an lsst.afw.geom.Angle); if specified then the smaller of config.maxMatchDistArcSec or
318  maxMatchDist is used; if None then config.maxMatchDistArcSec is used
319  @param[in] sourceFluxField Name of flux field in source catalog
320  @param[in] verbose true to print diagnostic information to std::cout
321 
322  @return a list of matches, an instance of lsst.afw.table.ReferenceMatch
323  """
324  numSources = len(sourceCat)
325  posRefBegInd = numUsableSources - numSources
326  if maxMatchDist is None:
327  maxMatchDistArcSec = self.config.maxMatchDistArcSec
328  else:
329  maxMatchDistArcSec = min(maxMatchDist.asArcseconds(), self.config.maxMatchDistArcSec)
330  configMatchDistPix = maxMatchDistArcSec/wcs.pixelScale().asArcseconds()
331 
332  matchControl = MatchOptimisticBControl()
333  matchControl.refFluxField = refFluxField
334  matchControl.sourceFluxField = sourceFluxField
335  matchControl.numBrightStars = self.config.numBrightStars
336  matchControl.minMatchedPairs = self.config.minMatchedPairs
337  matchControl.maxOffsetPix = self.config.maxOffsetPix
338  matchControl.numPointsForShape = self.config.numPointsForShape
339  matchControl.maxDeterminant = self.config.maxDeterminant
340 
341  for maxRotInd in range(4):
342  matchControl.maxRotationDeg = self.config.maxRotationDeg * math.pow(2.0, 0.5*maxRotInd)
343  for matchRadInd in range(3):
344  matchControl.matchingAllowancePix = configMatchDistPix * math.pow(1.25, matchRadInd)
345 
346  for angleDiffInd in range(3):
347  matchControl.allowedNonperpDeg = self.config.allowedNonperpDeg*(angleDiffInd+1)
348  matches = matchOptimisticB(
349  refCat,
350  sourceCat,
351  matchControl,
352  wcs,
353  posRefBegInd,
354  verbose,
355  )
356  if matches is not None and len(matches) > 0:
357  setMatchDistance(matches)
358  return matches
359  return matches
def _doMatch
Implementation of matching sources to position reference stars.
lsst::afw::table::ReferenceMatchVector matchOptimisticB(lsst::afw::table::SimpleCatalog const &posRefCat, lsst::afw::table::SourceCatalog const &sourceCat, MatchOptimisticBControl const &control, afw::image::Wcs const &wcs, int posRefBegInd=0, bool verbose=false)
Match sources to stars in a position reference catalog using optimistic pattern matching B...