LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
LSSTDataManagementBasePackage
matchOptimisticB.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 from builtins import range
3 from builtins import object
4 import math
5 
6 import numpy as np
7 
8 import lsst.afw.table as afwTable
9 import lsst.pex.config as pexConfig
10 import lsst.pipe.base as pipeBase
11 from .setMatchDistance import setMatchDistance
12 from .astromLib import matchOptimisticB, MatchOptimisticBControl
13 
14 __all__ = ["MatchOptimisticBTask", "MatchOptimisticBConfig", "SourceInfo"]
15 
16 
17 class MatchOptimisticBConfig(pexConfig.Config):
18  """Configuration for MatchOptimisticBTask
19  """
20  sourceFluxType = pexConfig.Field(
21  doc="Type of source flux; typically one of Ap or Psf",
22  dtype=str,
23  default="Ap",
24  )
25  maxMatchDistArcSec = pexConfig.RangeField(
26  doc="Maximum separation between reference objects and sources "
27  "beyond which they will not be considered a match (arcsec)",
28  dtype=float,
29  default=3,
30  min=0,
31  )
32  numBrightStars = pexConfig.RangeField(
33  doc="Number of bright stars to use",
34  dtype=int,
35  default=50,
36  min=2,
37  )
38  minMatchedPairs = pexConfig.RangeField(
39  doc="Minimum number of matched pairs; see also minFracMatchedPairs",
40  dtype=int,
41  default=30,
42  min=2,
43  )
44  minFracMatchedPairs = pexConfig.RangeField(
45  doc="Minimum number of matched pairs as a fraction of the smaller of "
46  "the number of reference stars or the number of good sources; "
47  "the actual minimum is the smaller of this value or minMatchedPairs",
48  dtype=float,
49  default=0.3,
50  min=0,
51  max=1,
52  )
53  maxOffsetPix = pexConfig.RangeField(
54  doc="Maximum allowed shift of WCS, due to matching (pixel)",
55  dtype=int,
56  default=300,
57  max=4000,
58  )
59  maxRotationDeg = pexConfig.RangeField(
60  doc="Rotation angle allowed between sources and position reference objects (degrees)",
61  dtype=float,
62  default=1.0,
63  max=6.0,
64  )
65  allowedNonperpDeg = pexConfig.RangeField(
66  doc="Allowed non-perpendicularity of x and y (degree)",
67  dtype=float,
68  default=3.0,
69  max=45.0,
70  )
71  numPointsForShape = pexConfig.Field(
72  doc="number of points to define a shape for matching",
73  dtype=int,
74  default=6,
75  )
76  maxDeterminant = pexConfig.Field(
77  doc="maximum determinant of linear transformation matrix for a usable solution",
78  dtype=float,
79  default=0.02,
80  )
81  minSnr = pexConfig.Field(
82  dtype=float,
83  doc="Minimum allowed signal-to-noise ratio for sources used for matching "
84  "(in the flux specified by sourceFluxType); <=0 for no limit",
85  default=40,
86  )
87 
88 
89 class SourceInfo(object):
90  """Provide usability tests and catalog keys for sources in a source catalog
91 
92  Fields set include:
93  - centroidKey key for centroid
94  - centroidFlagKey key for flag that is True if centroid is valid
95  - edgeKey key for field that is True if source is near an edge
96  - saturatedKey key for field that is True if source has any saturated pixels
97  - interpolatedCenterKey key for field that is True if center pixels have interpolated values;
98  interpolation is triggered by saturation, cosmic rays and bad pixels, and possibly other reasons
99  - fluxField name of flux field
100 
101  @throw RuntimeError if schema version unsupported or a needed field is not found
102  """
103 
104  def __init__(self, schema, fluxType="Ap", minSnr=50):
105  """Construct a SourceInfo
106 
107  @param[in] schema source catalog schema
108  @param[in] fluxType flux type: typically one of "Ap" or "Psf"
109  @param[in] minSnr minimum allowed signal-to-noise ratio for sources used for matching
110  (in the flux specified by fluxType); <=0 for no limit
111 
112  @throw RuntimeError if the flux field is not found
113  """
114  self.centroidKey = afwTable.Point2DKey(schema["slot_Centroid"])
115  self.centroidFlagKey = schema["slot_Centroid_flag"].asKey()
116  self.edgeKey = schema["base_PixelFlags_flag_edge"].asKey()
117  self.saturatedKey = schema["base_PixelFlags_flag_saturated"].asKey()
118  fluxPrefix = "slot_%sFlux_" % (fluxType,)
119  self.fluxField = fluxPrefix + "flux"
120  self.fluxKey = schema[fluxPrefix + "flux"].asKey()
121  self.fluxFlagKey = schema[fluxPrefix + "flag"].asKey()
122  self.fluxSigmaKey = schema[fluxPrefix + "fluxSigma"].asKey()
123  self.interpolatedCenterKey = schema["base_PixelFlags_flag_interpolatedCenter"].asKey()
124  self.parentKey = schema["parent"].asKey()
125  self.minSnr = float(minSnr)
126 
127  if self.fluxField not in schema:
128  raise RuntimeError("Could not find flux field %s in source schema" % (self.fluxField,))
129 
130  def _isMultiple(self, source):
131  """Return True if source is likely multiple sources
132  """
133  if source.get(self.parentKey) != 0:
134  return True
135  footprint = source.getFootprint()
136  return footprint is not None and len(footprint.getPeaks()) > 1
137 
138  def hasCentroid(self, source):
139  """Return True if the source has a valid centroid
140  """
141  centroid = source.get(self.centroidKey)
142  return np.all(np.isfinite(centroid)) and not source.getCentroidFlag()
143 
144  def isUsable(self, source):
145  """Return True if the source is usable for matching, even if it may have a poor centroid
146 
147  For a source to be usable it must:
148  - have a valid centroid
149  - not be deblended
150  - have a valid flux (of the type specified in this object's constructor)
151  - have adequate signal-to-noise
152  """
153  return self.hasCentroid(source) \
154  and source.get(self.parentKey) == 0 \
155  and not source.get(self.fluxFlagKey) \
156  and (self.minSnr <= 0
157  or (source.get(self.fluxKey)/source.get(self.fluxSigmaKey) > self.minSnr))
158 
159  def isGood(self, source):
160  """Return True if source is usable for matching (as per isUsable) and likely has a good centroid
161 
162  The additional tests for a good centroid, beyond isUsable, are:
163  - not interpolated in the center (this includes saturated sources,
164  so we don't test separately for that)
165  - not near the edge
166  """
167  return self.isUsable(source) \
168  and not source.get(self.interpolatedCenterKey) \
169  and not source.get(self.edgeKey)
170 
171 
172 # The following block adds links to this task from the Task Documentation page.
173 # \addtogroup LSST_task_documentation
174 # \{
175 # \page measAstrom_matchOptimisticBTask
176 # \ref MatchOptimisticBTask "MatchOptimisticBTask"
177 # Match sources to reference objects
178 # \}
179 
180 class MatchOptimisticBTask(pipeBase.Task):
181  """!Match sources to reference objects
182 
183  @anchor MatchOptimisticBTask_
184 
185  @section meas_astrom_matchOptimisticB_Contents Contents
186 
187  - @ref meas_astrom_matchOptimisticB_Purpose
188  - @ref meas_astrom_matchOptimisticB_Initialize
189  - @ref meas_astrom_matchOptimisticB_IO
190  - @ref meas_astrom_matchOptimisticB_Config
191  - @ref meas_astrom_matchOptimisticB_Example
192  - @ref meas_astrom_matchOptimisticB_Debug
193 
194  @section meas_astrom_matchOptimisticB_Purpose Description
195 
196  Match sources to reference objects. This is often done as a preliminary step to fitting an astrometric
197  or photometric solution. For details about the matching algorithm see matchOptimisticB.h
198 
199  @section meas_astrom_matchOptimisticB_Initialize Task initialisation
200 
201  @copydoc \_\_init\_\_
202 
203  @section meas_astrom_matchOptimisticB_IO Invoking the Task
204 
205  @copydoc matchObjectsToSources
206 
207  @section meas_astrom_matchOptimisticB_Config Configuration parameters
208 
209  See @ref MatchOptimisticBConfig
210 
211  To modify the tests for usable sources and good sources, subclass SourceInfo and
212  set MatchOptimisticBTask.SourceInfoClass to your subclass.
213 
214  @section meas_astrom_matchOptimisticB_Example A complete example of using MatchOptimisticBTask
215 
216  MatchOptimisticBTask is a subtask of AstrometryTask, which is called by PhotoCalTask.
217  See \ref meas_photocal_photocal_Example.
218 
219  @section meas_astrom_matchOptimisticB_Debug Debug variables
220 
221  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
222  flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about
223  @b debug.py files.
224 
225  The available variables in MatchOptimisticBTask are:
226  <DL>
227  <DT> @c verbose (bool)
228  <DD> If True then the matcher prints debug messages to stdout
229  </DL>
230 
231  To investigate the @ref meas_astrom_matchOptimisticB_Debug, put something like
232  @code{.py}
233  import lsstDebug
234  def DebugInfo(name):
235  debug = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
236  if name == "lsst.pipe.tasks.astrometry":
237  debug.verbose = True
238 
239  return debug
240 
241  lsstDebug.Info = DebugInfo
242  @endcode
243  into your debug.py file and run this task with the @c --debug flag.
244  """
245  ConfigClass = MatchOptimisticBConfig
246  _DefaultName = "matchObjectsToSources"
247  SourceInfoClass = SourceInfo
248 
249  def filterStars(self, refCat):
250  """Extra filtering pass; subclass if desired
251  """
252  return refCat
253 
254  @pipeBase.timeMethod
255  def matchObjectsToSources(self, refCat, sourceCat, wcs, refFluxField, maxMatchDist=None):
256  """!Match sources to position reference stars
257 
258  @param[in] refCat catalog of reference objects that overlap the exposure; reads fields for:
259  - coord
260  - the specified flux field
261  @param[in] sourceCat catalog of sources found on an exposure; reads fields for:
262  - centroid
263  - centroid flag
264  - edge flag
265  - saturated flag
266  - aperture flux, if found, else PSF flux
267  @param[in] wcs estimated WCS
268  @param[in] refFluxField field of refCat to use for flux
269  @param[in] maxMatchDist maximum on-sky distance between reference objects and sources
270  (an lsst.afw.geom.Angle); if specified then the smaller of config.maxMatchDistArcSec or
271  maxMatchDist is used; if None then config.maxMatchDistArcSec is used
272  @return an lsst.pipe.base.Struct with fields:
273  - matches a list of matches, each instance of lsst.afw.table.ReferenceMatch
274  - usableSourcCat a catalog of sources potentially usable for matching.
275  For this fitter usable sources include unresolved sources not too near the edge.
276  It includes saturated sources, even those these are removed from the final match list,
277  because saturated sources may be used to determine the match list.
278  """
279  import lsstDebug
280  debug = lsstDebug.Info(__name__)
281 
282  preNumObj = len(refCat)
283  refCat = self.filterStars(refCat)
284  numRefObj = len(refCat)
285 
286  if self.log:
287  self.log.info("filterStars purged %d reference stars, leaving %d stars" %
288  (preNumObj - numRefObj, numRefObj))
289 
290  sourceInfo = self.SourceInfoClass(
291  schema=sourceCat.schema,
292  fluxType=self.config.sourceFluxType,
293  minSnr=self.config.minSnr,
294  )
295 
296  # usableSourceCat: sources that are good but may be saturated
297  numSources = len(sourceCat)
298  usableSourceCat = afwTable.SourceCatalog(sourceCat.table)
299  usableSourceCat.extend(s for s in sourceCat if sourceInfo.isUsable(s))
300  numUsableSources = len(usableSourceCat)
301  self.log.info("Purged %d unusable sources, leaving %d usable sources" %
302  (numSources - numUsableSources, numUsableSources))
303 
304  if len(usableSourceCat) == 0:
305  raise pipeBase.TaskError("No sources are usable")
306 
307  del sourceCat # avoid accidentally using sourceCat; use usableSourceCat or goodSourceCat from now on
308 
309  minMatchedPairs = min(self.config.minMatchedPairs,
310  int(self.config.minFracMatchedPairs * min([len(refCat), len(usableSourceCat)])))
311 
312  # match usable (possibly saturated) sources and then purge saturated sources from the match list
313  usableMatches = self._doMatch(
314  refCat=refCat,
315  sourceCat=usableSourceCat,
316  wcs=wcs,
317  refFluxField=refFluxField,
318  numUsableSources=numUsableSources,
319  minMatchedPairs=minMatchedPairs,
320  maxMatchDist=maxMatchDist,
321  sourceInfo=sourceInfo,
322  verbose=debug.verbose,
323  )
324 
325  # cull non-good sources
326  matches = []
327  for match in usableMatches:
328  if sourceInfo.isGood(match.second):
329  matches.append(match)
330 
331  self.log.debug("Found %d usable matches, of which %d had good sources",
332  len(usableMatches), len(matches))
333 
334  if len(matches) == 0:
335  raise RuntimeError("Unable to match sources")
336 
337  self.log.info("Matched %d sources" % len(matches))
338  if len(matches) < minMatchedPairs:
339  self.log.warn("Number of matches is smaller than request")
340 
341  return pipeBase.Struct(
342  matches=matches,
343  usableSourceCat=usableSourceCat,
344  )
345 
346  @pipeBase.timeMethod
347  def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs,
348  maxMatchDist, sourceInfo, verbose):
349  """!Implementation of matching sources to position reference stars
350 
351  Unlike matchObjectsToSources, this method does not check if the sources are suitable.
352 
353  @param[in] refCat catalog of position reference stars that overlap an exposure
354  @param[in] sourceCat catalog of sources found on the exposure
355  @param[in] wcs estimated WCS of exposure
356  @param[in] refFluxField field of refCat to use for flux
357  @param[in] numUsableSources number of usable sources (sources with known centroid
358  that are not near the edge, but may be saturated)
359  @param[in] minMatchedPairs minimum number of matches
360  @param[in] maxMatchDist maximum on-sky distance between reference objects and sources
361  (an lsst.afw.geom.Angle); if specified then the smaller of config.maxMatchDistArcSec or
362  maxMatchDist is used; if None then config.maxMatchDistArcSec is used
363  @param[in] sourceInfo SourceInfo for the sourceCat
364  @param[in] verbose true to print diagnostic information to std::cout
365 
366  @return a list of matches, an instance of lsst.afw.table.ReferenceMatch
367  """
368  numSources = len(sourceCat)
369  posRefBegInd = numUsableSources - numSources
370  if maxMatchDist is None:
371  maxMatchDistArcSec = self.config.maxMatchDistArcSec
372  else:
373  maxMatchDistArcSec = min(maxMatchDist.asArcseconds(), self.config.maxMatchDistArcSec)
374  configMatchDistPix = maxMatchDistArcSec/wcs.pixelScale().asArcseconds()
375 
376  matchControl = MatchOptimisticBControl()
377  matchControl.refFluxField = refFluxField
378  matchControl.sourceFluxField = sourceInfo.fluxField
379  matchControl.numBrightStars = self.config.numBrightStars
380  matchControl.minMatchedPairs = self.config.minMatchedPairs
381  matchControl.maxOffsetPix = self.config.maxOffsetPix
382  matchControl.numPointsForShape = self.config.numPointsForShape
383  matchControl.maxDeterminant = self.config.maxDeterminant
384 
385  for maxRotInd in range(4):
386  matchControl.maxRotationDeg = self.config.maxRotationDeg * math.pow(2.0, 0.5*maxRotInd)
387  for matchRadInd in range(3):
388  matchControl.matchingAllowancePix = configMatchDistPix * math.pow(1.25, matchRadInd)
389 
390  for angleDiffInd in range(3):
391  matchControl.allowedNonperpDeg = self.config.allowedNonperpDeg*(angleDiffInd+1)
392  matches = matchOptimisticB(
393  refCat,
394  sourceCat,
395  matchControl,
396  wcs,
397  posRefBegInd,
398  verbose,
399  )
400  if matches is not None and len(matches) > 0:
401  setMatchDistance(matches)
402  return matches
403  return matches
def matchObjectsToSources
Match sources to position reference stars.
def _doMatch
Implementation of matching sources to position reference stars.
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:55
afwTable::ReferenceMatchVector matchOptimisticB(afwTable::SimpleCatalog const &posRefCat, afwTable::SourceCatalog const &sourceCat, MatchOptimisticBControl const &control, afw::image::Wcs const &wcs, int posRefBegInd, bool verbose)