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