Coverage for python/lsst/meas/astrom/matchOptimisticBTask.py: 26%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
2__all__ = ["MatchOptimisticBTask", "MatchOptimisticBConfig",
3 "MatchTolerance"]
5import math
7import lsst.pex.config as pexConfig
8import lsst.pipe.base as pipeBase
10from .setMatchDistance import setMatchDistance
11from .matchOptimisticB import matchOptimisticB, MatchOptimisticBControl
14class MatchTolerance:
15 """Stores match tolerances for use in `lsst.meas.astrom.AstrometryTask` and
16 later iterations of the matcher.
18 MatchOptimsiticBTask relies on a maximum distance for matching
19 set by either the default in MatchOptimisticBConfig or the 2 sigma
20 scatter found after AstrometryTask has fit for a wcs.
22 Parameters
23 ----------
24 maxMatchDist : `lsst.geom.Angle`
25 Current maximum distance to consider a match.
26 """
28 def __init__(self, maxMatchDist=None):
29 self.maxMatchDist = maxMatchDist
32class 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=250,
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 )
94# The following block adds links to this task from the Task Documentation page.
95# \addtogroup LSST_task_documentation
96# \{
97# \page measAstrom_matchOptimisticBTask
98# \ref MatchOptimisticBTask "MatchOptimisticBTask"
99# Match sources to reference objects
100# \}
103class MatchOptimisticBTask(pipeBase.Task):
104 """Match sources to reference objects using the Optimistic Pattern Matcher
105 B algorithm of Tabur 2007.
106 """
107 ConfigClass = MatchOptimisticBConfig
108 _DefaultName = "matchObjectsToSources"
110 def __init__(self, **kwargs):
111 pipeBase.Task.__init__(self, **kwargs)
113 def filterStars(self, refCat):
114 """Extra filtering pass; subclass if desired.
116 Parameters
117 ----------
118 refCat : `lsst.afw.table.SimpleCatalog`
119 Catalog of reference objects.
121 Returns
122 -------
123 trimmedRefCat : `lsst.afw.table.SimpleCatalog`
124 Reference catalog with some filtering applied. Currently no
125 filtering is applied.
126 """
127 return refCat
129 @pipeBase.timeMethod
130 def matchObjectsToSources(self, refCat, sourceCat, wcs, sourceFluxField, refFluxField,
131 match_tolerance=None):
132 """Match sources to position reference stars.
134 Parameters
135 ----------
136 refCat : `lsst.afw.table.SimpleCatalog`
137 Reference catalog to match.
138 sourceCat : `lsst.afw.table.SourceCatalog`
139 Catalog of sources found on an exposure. This should already be
140 down-selected to "good"/"usable" sources in the calling Task.
141 wcs : `lsst.afw.geom.SkyWcs`
142 Current WCS of the exposure containing the sources.
143 sourceFluxField : `str`
144 Field of the sourceCat to use for flux
145 refFluxField : `str`
146 Field of the refCat to use for flux
147 match_tolerance : `lsst.meas.astrom.MatchTolerance`
148 Object containing information from previous
149 `lsst.meas.astrom.AstrometryTask` match/fit cycles for use in
150 matching. If `None` is config defaults.
152 Returns
153 -------
154 matchResult : `lsst.pipe.base.Struct`
155 Result struct with components
157 - ``matches`` : List of matches with distance below the maximum match
158 distance (`list` of `lsst.afw.table.ReferenceMatch`).
159 - ``useableSourceCat`` : Catalog of sources matched and suited for
160 WCS fitting (`lsst.afw.table.SourceCatalog`).
161 - ``match_tolerance`` : MatchTolerance object updated from this
162 match iteration (`lsst.meas.astrom.MatchTolerance`).
163 """
164 import lsstDebug
165 debug = lsstDebug.Info(__name__)
167 preNumObj = len(refCat)
168 refCat = self.filterStars(refCat)
169 numRefObj = len(refCat)
171 if self.log:
172 self.log.info("filterStars purged %d reference stars, leaving %d stars" %
173 (preNumObj - numRefObj, numRefObj))
175 if match_tolerance is None:
176 match_tolerance = MatchTolerance()
178 # Make a name alias here for consistency with older code, and to make
179 # it clear that this is a good/usable (cleaned) source catalog.
180 usableSourceCat = sourceCat
182 numUsableSources = len(usableSourceCat)
184 if len(usableSourceCat) == 0:
185 raise pipeBase.TaskError("No sources are usable")
187 minMatchedPairs = min(self.config.minMatchedPairs,
188 int(self.config.minFracMatchedPairs * min([len(refCat), len(usableSourceCat)])))
190 # match usable (possibly saturated) sources and then purge saturated sources from the match list
191 usableMatches = self._doMatch(
192 refCat=refCat,
193 sourceCat=usableSourceCat,
194 wcs=wcs,
195 refFluxField=refFluxField,
196 numUsableSources=numUsableSources,
197 minMatchedPairs=minMatchedPairs,
198 maxMatchDist=match_tolerance.maxMatchDist,
199 sourceFluxField=sourceFluxField,
200 verbose=debug.verbose,
201 )
203 # cull non-good sources
204 matches = []
205 self._getIsGoodKeys(usableSourceCat.schema)
206 for match in usableMatches:
207 if self._isGoodTest(match.second):
208 # Append the isGood match.
209 matches.append(match)
211 self.log.debug("Found %d usable matches, of which %d had good sources",
212 len(usableMatches), len(matches))
214 if len(matches) == 0:
215 raise RuntimeError("Unable to match sources")
217 self.log.info("Matched %d sources" % len(matches))
218 if len(matches) < minMatchedPairs:
219 self.log.warn("Number of matches is smaller than request")
221 return pipeBase.Struct(
222 matches=matches,
223 usableSourceCat=usableSourceCat,
224 match_tolerance=match_tolerance,
225 )
227 def _getIsGoodKeys(self, schema):
228 """Retrieve the keys needed for the isGoodTest from the source catalog
229 schema.
231 Parameters
232 ----------
233 schema : `lsst.afw.table.Schema`
234 Source schema to retrieve `lsst.afw.table.Key` s from.
235 """
236 self.edgeKey = schema["base_PixelFlags_flag_edge"].asKey()
237 self.interpolatedCenterKey = schema["base_PixelFlags_flag_interpolatedCenter"].asKey()
238 self.saturatedKey = schema["base_PixelFlags_flag_saturated"].asKey()
240 def _isGoodTest(self, source):
241 """Test that an object is good for use in the WCS fitter.
243 This is a hard coded version of the isGood flag from the old SourceInfo
244 class that used to be part of this class.
246 Parameters
247 ----------
248 source : `lsst.afw.table.SourceRecord`
249 Source to test.
251 Returns
252 -------
253 isGood : `bool`
254 Source passes CCD edge and saturated tests.
255 """
256 return (not source.get(self.edgeKey)
257 and not source.get(self.interpolatedCenterKey)
258 and not source.get(self.saturatedKey))
260 @pipeBase.timeMethod
261 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs,
262 maxMatchDist, sourceFluxField, verbose):
263 """Implementation of matching sources to position reference stars.
265 Unlike matchObjectsToSources, this method does not check if the sources
266 are suitable.
268 Parameters
269 ----------
270 refCat : `lsst.afw.table.SimpleCatalog`
271 Catalog of reference objects.
272 sourceCat : `lsst.afw.table.SourceCatalog`
273 Catalog of detected sources.
274 wcs : `lsst.afw.geom.SkyWcs`
275 Current best WCS of the image.
276 refFluxFioeld : `str`
277 Name of flux field in refCat to use.
278 numUsableSources : `int`
279 Total number of source usable for matching.
280 mintMatchPairs : `int`
281 Minimum number of objects to match between the refCat and sourceCat
282 to consider a valid match.
283 maxMatchDist : `lsst.geom.Angle`
284 Maximum separation to considering a reference and a source a match.
285 sourceFluxField : `str`
286 Name of source catalog flux field.
287 verbose : `bool`
288 Print diagnostic information std::cout
290 Returns
291 -------
292 matches : `list` of `lsst.afw.table.ReferenceMatch`
293 """
294 numSources = len(sourceCat)
295 posRefBegInd = numUsableSources - numSources
296 if maxMatchDist is None:
297 maxMatchDistArcSec = self.config.maxMatchDistArcSec
298 else:
299 maxMatchDistArcSec = min(maxMatchDist.asArcseconds(), self.config.maxMatchDistArcSec)
300 configMatchDistPix = maxMatchDistArcSec/wcs.getPixelScale().asArcseconds()
302 matchControl = MatchOptimisticBControl()
303 matchControl.refFluxField = refFluxField
304 matchControl.sourceFluxField = sourceFluxField
305 matchControl.numBrightStars = self.config.numBrightStars
306 matchControl.minMatchedPairs = self.config.minMatchedPairs
307 matchControl.maxOffsetPix = self.config.maxOffsetPix
308 matchControl.numPointsForShape = self.config.numPointsForShape
309 matchControl.maxDeterminant = self.config.maxDeterminant
311 for maxRotInd in range(4):
312 matchControl.maxRotationDeg = self.config.maxRotationDeg * math.pow(2.0, 0.5*maxRotInd)
313 for matchRadInd in range(3):
314 matchControl.matchingAllowancePix = configMatchDistPix * math.pow(1.25, matchRadInd)
316 for angleDiffInd in range(3):
317 matchControl.allowedNonperpDeg = self.config.allowedNonperpDeg*(angleDiffInd+1)
318 matches = matchOptimisticB(
319 refCat,
320 sourceCat,
321 matchControl,
322 wcs,
323 posRefBegInd,
324 verbose,
325 )
326 if matches is not None and len(matches) > 0:
327 setMatchDistance(matches)
328 return matches
329 return matches