Coverage for python/lsst/meas/astrom/matchOptimisticBTask.py: 27%
89 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-24 03:35 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-24 03:35 -0700
2__all__ = ["MatchOptimisticBTask", "MatchOptimisticBConfig",
3 "MatchTolerance"]
5import math
7import lsst.pex.config as pexConfig
8import lsst.pipe.base as pipeBase
9from lsst.utils.timer import timeMethod
11from .setMatchDistance import setMatchDistance
12from .matchOptimisticB import matchOptimisticB, MatchOptimisticBControl
15class MatchTolerance:
16 """Stores match tolerances for use in `lsst.meas.astrom.AstrometryTask` and
17 later iterations of the matcher.
19 MatchOptimsiticBTask relies on a maximum distance for matching
20 set by either the default in MatchOptimisticBConfig or the 2 sigma
21 scatter found after AstrometryTask has fit for a wcs.
23 Parameters
24 ----------
25 maxMatchDist : `lsst.geom.Angle`
26 Current maximum distance to consider a match.
27 """
29 def __init__(self, maxMatchDist=None):
30 self.maxMatchDist = maxMatchDist
33class MatchOptimisticBConfig(pexConfig.Config):
34 """Configuration for MatchOptimisticBTask
35 """
36 maxMatchDistArcSec = pexConfig.RangeField(
37 doc="Maximum separation between reference objects and sources "
38 "beyond which they will not be considered a match (arcsec)",
39 dtype=float,
40 default=3,
41 min=0,
42 )
43 numBrightStars = pexConfig.RangeField(
44 doc="Number of bright stars to use",
45 dtype=int,
46 default=50,
47 min=2,
48 )
49 minMatchedPairs = pexConfig.RangeField(
50 doc="Minimum number of matched pairs; see also minFracMatchedPairs",
51 dtype=int,
52 default=30,
53 min=2,
54 )
55 minFracMatchedPairs = pexConfig.RangeField(
56 doc="Minimum number of matched pairs as a fraction of the smaller of "
57 "the number of reference stars or the number of good sources; "
58 "the actual minimum is the smaller of this value or minMatchedPairs",
59 dtype=float,
60 default=0.3,
61 min=0,
62 max=1,
63 )
64 maxOffsetPix = pexConfig.RangeField(
65 doc="Maximum allowed shift of WCS, due to matching (pixel). "
66 "When changing this value, the LoadReferenceObjectsConfig.pixelMargin should also be updated.",
67 dtype=int,
68 default=250,
69 max=4000,
70 )
71 maxRotationDeg = pexConfig.RangeField(
72 doc="Rotation angle allowed between sources and position reference objects (degrees)",
73 dtype=float,
74 default=1.0,
75 max=6.0,
76 )
77 allowedNonperpDeg = pexConfig.RangeField(
78 doc="Allowed non-perpendicularity of x and y (degree)",
79 dtype=float,
80 default=3.0,
81 max=45.0,
82 )
83 numPointsForShape = pexConfig.Field(
84 doc="number of points to define a shape for matching",
85 dtype=int,
86 default=6,
87 )
88 maxDeterminant = pexConfig.Field(
89 doc="maximum determinant of linear transformation matrix for a usable solution",
90 dtype=float,
91 default=0.02,
92 )
95# The following block adds links to this task from the Task Documentation page.
96# \addtogroup LSST_task_documentation
97# \{
98# \page measAstrom_matchOptimisticBTask
99# \ref MatchOptimisticBTask "MatchOptimisticBTask"
100# Match sources to reference objects
101# \}
104class MatchOptimisticBTask(pipeBase.Task):
105 """Match sources to reference objects using the Optimistic Pattern Matcher
106 B algorithm of Tabur 2007.
107 """
108 ConfigClass = MatchOptimisticBConfig
109 _DefaultName = "matchObjectsToSources"
111 def __init__(self, **kwargs):
112 pipeBase.Task.__init__(self, **kwargs)
114 def filterStars(self, refCat):
115 """Extra filtering pass; subclass if desired.
117 Parameters
118 ----------
119 refCat : `lsst.afw.table.SimpleCatalog`
120 Catalog of reference objects.
122 Returns
123 -------
124 trimmedRefCat : `lsst.afw.table.SimpleCatalog`
125 Reference catalog with some filtering applied. Currently no
126 filtering is applied.
127 """
128 return refCat
130 @timeMethod
131 def matchObjectsToSources(self, refCat, sourceCat, wcs, sourceFluxField, refFluxField,
132 match_tolerance=None):
133 """Match sources to position reference stars.
135 Parameters
136 ----------
137 refCat : `lsst.afw.table.SimpleCatalog`
138 Reference catalog to match.
139 sourceCat : `lsst.afw.table.SourceCatalog`
140 Catalog of sources found on an exposure. This should already be
141 down-selected to "good"/"usable" sources in the calling Task.
142 wcs : `lsst.afw.geom.SkyWcs`
143 Current WCS of the exposure containing the sources.
144 sourceFluxField : `str`
145 Field of the sourceCat to use for flux
146 refFluxField : `str`
147 Field of the refCat to use for flux
148 match_tolerance : `lsst.meas.astrom.MatchTolerance`
149 Object containing information from previous
150 `lsst.meas.astrom.AstrometryTask` match/fit cycles for use in
151 matching. If `None` is config defaults.
153 Returns
154 -------
155 matchResult : `lsst.pipe.base.Struct`
156 Result struct with components
158 - ``matches`` : List of matches with distance below the maximum match
159 distance (`list` of `lsst.afw.table.ReferenceMatch`).
160 - ``useableSourceCat`` : Catalog of sources matched and suited for
161 WCS fitting (`lsst.afw.table.SourceCatalog`).
162 - ``match_tolerance`` : MatchTolerance object updated from this
163 match iteration (`lsst.meas.astrom.MatchTolerance`).
164 """
165 import lsstDebug
166 debug = lsstDebug.Info(__name__)
168 preNumObj = len(refCat)
169 refCat = self.filterStars(refCat)
170 numRefObj = len(refCat)
172 if self.log:
173 self.log.info("filterStars purged %d reference stars, leaving %d stars",
174 preNumObj - numRefObj, numRefObj)
176 if match_tolerance is None:
177 match_tolerance = MatchTolerance()
179 # Make a name alias here for consistency with older code, and to make
180 # it clear that this is a good/usable (cleaned) source catalog.
181 usableSourceCat = sourceCat
183 numUsableSources = len(usableSourceCat)
185 if len(usableSourceCat) == 0:
186 raise pipeBase.TaskError("No sources are usable")
188 minMatchedPairs = min(self.config.minMatchedPairs,
189 int(self.config.minFracMatchedPairs * min([len(refCat), len(usableSourceCat)])))
191 # match usable (possibly saturated) sources and then purge saturated sources from the match list
192 usableMatches = self._doMatch(
193 refCat=refCat,
194 sourceCat=usableSourceCat,
195 wcs=wcs,
196 refFluxField=refFluxField,
197 numUsableSources=numUsableSources,
198 minMatchedPairs=minMatchedPairs,
199 maxMatchDist=match_tolerance.maxMatchDist,
200 sourceFluxField=sourceFluxField,
201 verbose=debug.verbose,
202 )
204 # cull non-good sources
205 matches = []
206 self._getIsGoodKeys(usableSourceCat.schema)
207 for match in usableMatches:
208 if self._isGoodTest(match.second):
209 # Append the isGood match.
210 matches.append(match)
212 self.log.debug("Found %d usable matches, of which %d had good sources",
213 len(usableMatches), len(matches))
215 if len(matches) == 0:
216 raise RuntimeError("Unable to match sources")
218 self.log.info("Matched %d sources", len(matches))
219 if len(matches) < minMatchedPairs:
220 self.log.warning("Number of matches is smaller than request")
222 return pipeBase.Struct(
223 matches=matches,
224 usableSourceCat=usableSourceCat,
225 match_tolerance=match_tolerance,
226 )
228 def _getIsGoodKeys(self, schema):
229 """Retrieve the keys needed for the isGoodTest from the source catalog
230 schema.
232 Parameters
233 ----------
234 schema : `lsst.afw.table.Schema`
235 Source schema to retrieve `lsst.afw.table.Key` s from.
236 """
237 self.edgeKey = schema["base_PixelFlags_flag_edge"].asKey()
238 self.interpolatedCenterKey = schema["base_PixelFlags_flag_interpolatedCenter"].asKey()
239 self.saturatedKey = schema["base_PixelFlags_flag_saturated"].asKey()
241 def _isGoodTest(self, source):
242 """Test that an object is good for use in the WCS fitter.
244 This is a hard coded version of the isGood flag from the old SourceInfo
245 class that used to be part of this class.
247 Parameters
248 ----------
249 source : `lsst.afw.table.SourceRecord`
250 Source to test.
252 Returns
253 -------
254 isGood : `bool`
255 Source passes CCD edge and saturated tests.
256 """
257 return (not source.get(self.edgeKey)
258 and not source.get(self.interpolatedCenterKey)
259 and not source.get(self.saturatedKey))
261 @timeMethod
262 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs,
263 maxMatchDist, sourceFluxField, verbose):
264 """Implementation of matching sources to position reference stars.
266 Unlike matchObjectsToSources, this method does not check if the sources
267 are suitable.
269 Parameters
270 ----------
271 refCat : `lsst.afw.table.SimpleCatalog`
272 Catalog of reference objects.
273 sourceCat : `lsst.afw.table.SourceCatalog`
274 Catalog of detected sources.
275 wcs : `lsst.afw.geom.SkyWcs`
276 Current best WCS of the image.
277 refFluxFioeld : `str`
278 Name of flux field in refCat to use.
279 numUsableSources : `int`
280 Total number of source usable for matching.
281 mintMatchPairs : `int`
282 Minimum number of objects to match between the refCat and sourceCat
283 to consider a valid match.
284 maxMatchDist : `lsst.geom.Angle`
285 Maximum separation to considering a reference and a source a match.
286 sourceFluxField : `str`
287 Name of source catalog flux field.
288 verbose : `bool`
289 Print diagnostic information std::cout
291 Returns
292 -------
293 matches : `list` of `lsst.afw.table.ReferenceMatch`
294 """
295 numSources = len(sourceCat)
296 posRefBegInd = numUsableSources - numSources
297 if maxMatchDist is None:
298 maxMatchDistArcSec = self.config.maxMatchDistArcSec
299 else:
300 maxMatchDistArcSec = min(maxMatchDist.asArcseconds(), self.config.maxMatchDistArcSec)
301 configMatchDistPix = maxMatchDistArcSec/wcs.getPixelScale().asArcseconds()
303 matchControl = MatchOptimisticBControl()
304 matchControl.refFluxField = refFluxField
305 matchControl.sourceFluxField = sourceFluxField
306 matchControl.numBrightStars = self.config.numBrightStars
307 matchControl.minMatchedPairs = self.config.minMatchedPairs
308 matchControl.maxOffsetPix = self.config.maxOffsetPix
309 matchControl.numPointsForShape = self.config.numPointsForShape
310 matchControl.maxDeterminant = self.config.maxDeterminant
312 for maxRotInd in range(4):
313 matchControl.maxRotationDeg = self.config.maxRotationDeg * math.pow(2.0, 0.5*maxRotInd)
314 for matchRadInd in range(3):
315 matchControl.matchingAllowancePix = configMatchDistPix * math.pow(1.25, matchRadInd)
317 for angleDiffInd in range(3):
318 matchControl.allowedNonperpDeg = self.config.allowedNonperpDeg*(angleDiffInd+1)
319 matches = matchOptimisticB(
320 refCat,
321 sourceCat,
322 matchControl,
323 wcs,
324 posRefBegInd,
325 verbose,
326 )
327 if matches is not None and len(matches) > 0:
328 setMatchDistance(matches)
329 return matches
330 return matches