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