3 from scipy.spatial
import cKDTree
10 from .matchOptimisticBTask
import MatchTolerance
12 from .pessimistic_pattern_matcher_b_3D
import PessimisticPatternMatcherB
14 __all__ = [
"MatchPessimisticBTask",
"MatchPessimisticBConfig",
15 "MatchTolerancePessimistic"]
19 """Stores match tolerances for use in AstrometryTask and later
20 iterations of the matcher.
22 MatchPessimisticBTask relies on several state variables to be
23 preserved over different iterations in the
24 AstrometryTask.matchAndFitWcs loop of AstrometryTask.
28 maxMatchDist : `lsst.geom.Angle`
29 Maximum distance to consider a match from the previous match/fit
31 autoMaxMatchDist : `lsst.geom.Angle`
32 Automated estimation of the maxMatchDist from the sky statistics of the
33 source and reference catalogs.
34 maxShift : `lsst.geom.Angle`
35 Maximum shift found in the previous match/fit cycle.
36 lastMatchedPattern : `int`
37 Index of the last source pattern that was matched into the reference
39 failedPatternList : `list` of `int`
40 Previous matches were found to be false positives.
41 PPMbObj : `lsst.meas.astrom.PessimisticPatternMatcherB`
42 Initialized Pessimistic pattern matcher object. Storing this prevents
43 the need for recalculation of the searchable distances in the PPMB.
46 def __init__(self, maxMatchDist=None, autoMaxMatchDist=None,
47 maxShift=None, lastMatchedPattern=None,
48 failedPatternList=None, PPMbObj=None):
54 if failedPatternList
is None:
61 """Configuration for MatchPessimisticBTask
63 numBrightStars = pexConfig.RangeField(
64 doc=
"Number of bright stars to use. Sets the max number of patterns "
65 "that can be tested.",
70 minMatchedPairs = pexConfig.RangeField(
71 doc=
"Minimum number of matched pairs; see also minFracMatchedPairs.",
76 minFracMatchedPairs = pexConfig.RangeField(
77 doc=
"Minimum number of matched pairs as a fraction of the smaller of "
78 "the number of reference stars or the number of good sources; "
79 "the actual minimum is the smaller of this value or "
86 matcherIterations = pexConfig.RangeField(
87 doc=
"Number of softening iterations in matcher.",
92 maxOffsetPix = pexConfig.RangeField(
93 doc=
"Maximum allowed shift of WCS, due to matching (pixel). "
94 "When changing this value, the "
95 "LoadReferenceObjectsConfig.pixelMargin should also be updated.",
100 maxRotationDeg = pexConfig.RangeField(
101 doc=
"Rotation angle allowed between sources and position reference "
102 "objects (degrees).",
107 numPointsForShape = pexConfig.Field(
108 doc=
"Number of points to define a shape for matching.",
112 numPointsForShapeAttempt = pexConfig.Field(
113 doc=
"Number of points to try for creating a shape. This value should "
114 "be greater than or equal to numPointsForShape. Besides "
115 "loosening the signal to noise cut in the 'matcher' SourceSelector, "
116 "increasing this number will solve CCDs where no match was found.",
120 minMatchDistPixels = pexConfig.RangeField(
121 doc=
"Distance in units of pixels to always consider a source-"
122 "reference pair a match. This prevents the astrometric fitter "
123 "from over-fitting and removing stars that should be matched and "
124 "allows for inclusion of new matches as the wcs improves.",
130 numPatternConsensus = pexConfig.Field(
131 doc=
"Number of implied shift/rotations from patterns that must agree "
132 "before it a given shift/rotation is accepted. This is only used "
133 "after the first softening iteration fails and if both the "
134 "number of reference and source objects is greater than "
139 numRefRequireConsensus = pexConfig.Field(
140 doc=
"If the available reference objects exceeds this number, "
141 "consensus/pessimistic mode will enforced regardless of the "
142 "number of available sources. Below this optimistic mode ("
143 "exit at first match rather than requiring numPatternConsensus to "
144 "be matched) can be used. If more sources are required to match, "
145 "decrease the signal to noise cut in the sourceSelector.",
149 maxRefObjects = pexConfig.RangeField(
150 doc=
"Maximum number of reference objects to use for the matcher. The "
151 "absolute maximum allowed for is 2 ** 16 for memory reasons.",
159 pexConfig.Config.validate(self)
161 raise ValueError(
"numPointsForShapeAttempt must be greater than "
162 "or equal to numPointsForShape.")
164 raise ValueError(
"numBrightStars must be greater than "
165 "numPointsForShape.")
178 """Match sources to reference objects.
181 ConfigClass = MatchPessimisticBConfig
182 _DefaultName =
"matchObjectsToSources"
185 pipeBase.Task.__init__(self, **kwargs)
189 match_tolerance=None):
190 """Match sources to position reference stars
192 refCat : `lsst.afw.table.SimpleCatalog`
193 catalog of reference objects that overlap the exposure; reads
197 - the specified flux field
199 sourceCat : `lsst.afw.table.SourceCatalog`
200 Catalog of sources found on an exposure. This should already be
201 down-selected to "good"/"usable" sources in the calling Task.
202 wcs : `lsst.afw.geom.SkyWcs`
204 sourceFluxField: `str`
205 field of sourceCat to use for flux
207 field of refCat to use for flux
208 match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic`
209 is a MatchTolerance class object or `None`. This this class is used
210 to communicate state between AstrometryTask and MatcherTask.
211 AstrometryTask will also set the MatchTolerance class variable
212 maxMatchDist based on the scatter AstrometryTask has found after
217 result : `lsst.pipe.base.Struct`
218 Result struct with components:
220 - ``matches`` : source to reference matches found (`list` of
221 `lsst.afw.table.ReferenceMatch`)
222 - ``usableSourceCat`` : a catalog of sources potentially usable for
223 matching and WCS fitting (`lsst.afw.table.SourceCatalog`).
224 - ``match_tolerance`` : a MatchTolerance object containing the
225 resulting state variables from the match
226 (`lsst.meas.astrom.MatchTolerancePessimistic`).
233 if match_tolerance
is None:
238 goodSourceCat = sourceCat
240 numUsableSources = len(goodSourceCat)
242 if len(goodSourceCat) == 0:
243 raise pipeBase.TaskError(
"No sources are good")
245 minMatchedPairs =
min(self.config.minMatchedPairs,
246 int(self.config.minFracMatchedPairs
247 *
min([len(refCat), len(goodSourceCat)])))
249 if len(refCat) > self.config.maxRefObjects:
251 "WARNING: Reference catalog larger that maximum allowed. "
252 "Trimming to %i" % self.config.maxRefObjects)
253 trimmedRefCat = self.
_filterRefCat_filterRefCat(refCat, refFluxField)
255 trimmedRefCat = refCat
257 doMatchReturn = self.
_doMatch_doMatch(
258 refCat=trimmedRefCat,
259 sourceCat=goodSourceCat,
261 refFluxField=refFluxField,
262 numUsableSources=numUsableSources,
263 minMatchedPairs=minMatchedPairs,
264 match_tolerance=match_tolerance,
265 sourceFluxField=sourceFluxField,
266 verbose=debug.verbose,
268 matches = doMatchReturn.matches
269 match_tolerance = doMatchReturn.match_tolerance
271 if len(matches) == 0:
272 raise RuntimeError(
"Unable to match sources")
274 self.log.
info(
"Matched %d sources" % len(matches))
275 if len(matches) < minMatchedPairs:
276 self.log.
warn(
"Number of matches is smaller than request")
278 return pipeBase.Struct(
280 usableSourceCat=goodSourceCat,
281 match_tolerance=match_tolerance,
284 def _filterRefCat(self, refCat, refFluxField):
285 """Sub-select a number of reference objects starting from the brightest
286 and maxing out at the number specified by maxRefObjects in the config.
288 No trimming is done if len(refCat) > config.maxRefObjects.
292 refCat : `lsst.afw.table.SimpleCatalog`
293 Catalog of reference objects to trim.
295 field of refCat to use for flux
298 outCat : `lsst.afw.table.SimpleCatalog`
299 Catalog trimmed to the number set in the task config from the
303 if len(refCat) <= self.config.maxRefObjects:
305 fluxArray = refCat.get(refFluxField)
306 sortedFluxArray = fluxArray[fluxArray.argsort()]
307 minFlux = sortedFluxArray[-(self.config.maxRefObjects + 1)]
309 selected = (refCat.get(refFluxField) > minFlux)
312 outCat.reserve(self.config.maxRefObjects)
313 outCat.extend(refCat[selected])
318 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
319 minMatchedPairs, match_tolerance, sourceFluxField, verbose):
320 """Implementation of matching sources to position reference objects
322 Unlike matchObjectsToSources, this method does not check if the sources
327 refCat : `lsst.afw.table.SimpleCatalog`
328 catalog of position reference objects that overlap an exposure
329 sourceCat : `lsst.afw.table.SourceCatalog`
330 catalog of sources found on the exposure
331 wcs : `lsst.afw.geom.SkyWcs`
332 estimated WCS of exposure
334 field of refCat to use for flux
335 numUsableSources : `int`
336 number of usable sources (sources with known centroid that are not
337 near the edge, but may be saturated)
338 minMatchedPairs : `int`
339 minimum number of matches
340 match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic`
341 a MatchTolerance object containing variables specifying matcher
342 tolerances and state from possible previous runs.
343 sourceFluxField : `str`
344 Name of the flux field in the source catalog.
346 Set true to print diagnostic information to std::cout
351 Results struct with components:
353 - ``matches`` : a list the matches found
354 (`list` of `lsst.afw.table.ReferenceMatch`).
355 - ``match_tolerance`` : MatchTolerance containing updated values from
356 this fit iteration (`lsst.meas.astrom.MatchTolerancePessimistic`)
365 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
366 for src_idx, srcObj
in enumerate(sourceCat):
367 coord = wcs.pixelToSky(srcObj.getCentroid())
368 theta = np.pi / 2 - coord.getLatitude().asRadians()
369 phi = coord.getLongitude().asRadians()
370 flux = srcObj[sourceFluxField]
371 src_array[src_idx, :] = \
374 if match_tolerance.PPMbObj
is None or \
375 match_tolerance.autoMaxMatchDist
is None:
379 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
380 for ref_idx, refObj
in enumerate(refCat):
381 theta = np.pi / 2 - refObj.getDec().asRadians()
382 phi = refObj.getRa().asRadians()
383 flux = refObj[refFluxField]
384 ref_array[ref_idx, :] = \
388 ref_array[:, :3], self.log)
389 self.log.
debug(
"Computing source statistics...")
392 self.log.
debug(
"Computing reference statistics...")
395 maxMatchDistArcSec = np.max((
396 self.config.minMatchDistPixels
397 * wcs.getPixelScale().asArcseconds(),
398 np.min((maxMatchDistArcSecSrc,
399 maxMatchDistArcSecRef))))
400 match_tolerance.autoMaxMatchDist =
geom.Angle(
401 maxMatchDistArcSec, geom.arcseconds)
405 if match_tolerance.maxShift
is None:
406 maxShiftArcseconds = (self.config.maxOffsetPix
407 * wcs.getPixelScale().asArcseconds())
411 maxShiftArcseconds = np.max(
412 (match_tolerance.maxShift.asArcseconds(),
413 self.config.minMatchDistPixels
414 * wcs.getPixelScale().asArcseconds()))
420 if match_tolerance.maxMatchDist
is None:
421 match_tolerance.maxMatchDist = match_tolerance.autoMaxMatchDist
423 maxMatchDistArcSec = np.max(
424 (self.config.minMatchDistPixels
425 * wcs.getPixelScale().asArcseconds(),
426 np.min((match_tolerance.maxMatchDist.asArcseconds(),
427 match_tolerance.autoMaxMatchDist.asArcseconds()))))
433 numConsensus = self.config.numPatternConsensus
434 if len(refCat) < self.config.numRefRequireConsensus:
435 minObjectsForConsensus = \
436 self.config.numBrightStars + \
437 self.config.numPointsForShapeAttempt
438 if len(refCat) < minObjectsForConsensus
or \
439 len(sourceCat) < minObjectsForConsensus:
442 self.log.
debug(
"Current tol maxDist: %.4f arcsec" %
444 self.log.
debug(
"Current shift: %.4f arcsec" %
449 for soften_dist
in range(self.config.matcherIterations):
450 if soften_dist == 0
and \
451 match_tolerance.lastMatchedPattern
is not None:
460 run_n_consent = numConsensus
463 matcher_struct = match_tolerance.PPMbObj.match(
464 source_array=src_array,
465 n_check=self.config.numPointsForShapeAttempt,
466 n_match=self.config.numPointsForShape,
467 n_agree=run_n_consent,
468 max_n_patterns=self.config.numBrightStars,
469 max_shift=maxShiftArcseconds,
470 max_rotation=self.config.maxRotationDeg,
471 max_dist=maxMatchDistArcSec * 2. ** soften_dist,
472 min_matches=minMatchedPairs,
473 pattern_skip_array=np.array(
474 match_tolerance.failedPatternList)
477 if soften_dist == 0
and \
478 len(matcher_struct.match_ids) == 0
and \
479 match_tolerance.lastMatchedPattern
is not None:
486 match_tolerance.failedPatternList.append(
487 match_tolerance.lastMatchedPattern)
488 match_tolerance.lastMatchedPattern =
None
489 maxShiftArcseconds = \
490 self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds()
491 elif len(matcher_struct.match_ids) > 0:
494 match_tolerance.maxShift = \
495 matcher_struct.shift * geom.arcseconds
496 match_tolerance.lastMatchedPattern = \
497 matcher_struct.pattern_idx
503 return pipeBase.Struct(
505 match_tolerance=match_tolerance,
517 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
518 dist_cut_arcsec = np.max(
519 (np.degrees(matcher_struct.max_dist_rad) * 3600,
520 self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds()))
525 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
527 if dist_arcsec < dist_cut_arcsec:
529 match.first = refCat[int(match_id_pair[1])]
530 match.second = sourceCat[int(match_id_pair[0])]
534 match.distance = match.first.getCoord().separation(
535 match.second.getCoord()).asArcseconds()
536 matches.append(match)
538 return pipeBase.Struct(
540 match_tolerance=match_tolerance,
543 def _latlong_flux_to_xyz_mag(self, theta, phi, flux):
544 """Convert angles theta and phi and a flux into unit sphere
545 x, y, z, and a relative magnitude.
547 Takes in a afw catalog object and converts the catalog object RA, DECs
548 to points on the unit sphere. Also converts the flux into a simple,
549 non-zero-pointed magnitude for relative sorting.
554 Angle from the north pole (z axis) of the sphere
556 Rotation around the sphere
560 output_array : `numpy.ndarray`, (N, 4)
561 Spherical unit vector x, y, z with flux.
563 output_array = np.empty(4, dtype=np.float64)
564 output_array[0] = np.sin(theta)*np.cos(phi)
565 output_array[1] = np.sin(theta)*np.sin(phi)
566 output_array[2] = np.cos(theta)
568 output_array[3] = -2.5 * np.log10(flux)
572 output_array[3] = 99.
576 def _get_pair_pattern_statistics(self, cat_array):
577 """ Compute the tolerances for the matcher automatically by comparing
578 pinwheel patterns as we would in the matcher.
580 We test how similar the patterns we can create from a given set of
581 objects by computing the spoke lengths for each pattern and sorting
582 them from smallest to largest. The match tolerance is the average
583 distance per spoke between the closest two patterns in the sorted
588 cat_array : `numpy.ndarray`, (N, 3)
589 array of 3 vectors representing the x, y, z position of catalog
590 objects on the unit sphere.
595 Suggested max match tolerance distance calculated from comparisons
596 between pinwheel patterns used in optimistic/pessimistic pattern
600 self.log.
debug(
"Starting automated tolerance calculation...")
604 pattern_array = np.empty(
605 (cat_array.shape[0] - self.config.numPointsForShape,
606 self.config.numPointsForShape - 1))
607 flux_args_array = np.argsort(cat_array[:, -1])
610 tmp_sort_array = cat_array[flux_args_array]
613 for start_idx
in range(cat_array.shape[0]
614 - self.config.numPointsForShape):
615 pattern_points = tmp_sort_array[start_idx:start_idx
616 + self.config.numPointsForShape, :-1]
617 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
618 pattern_array[start_idx, :] = np.sqrt(
619 pattern_delta[:, 0] ** 2
620 + pattern_delta[:, 1] ** 2
621 + pattern_delta[:, 2] ** 2)
626 pattern_array[start_idx, :] = pattern_array[
627 start_idx, np.argsort(pattern_array[start_idx, :])]
633 pattern_array[:, :(self.config.numPointsForShape - 1)])
634 dist_nearest_array, ids = dist_tree.query(
635 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
636 dist_nearest_array = dist_nearest_array[:, 1]
637 dist_nearest_array.sort()
641 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600.
642 / (self.config.numPointsForShape - 1.))
644 self.log.
debug(
"Automated tolerance")
645 self.log.
debug(
"\tdistance/match tol: %.4f [arcsec]" % dist_tol)
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
A class representing an angle.
def _get_pair_pattern_statistics(self, cat_array)
def _latlong_flux_to_xyz_mag(self, theta, phi, flux)
def matchObjectsToSources(self, refCat, sourceCat, wcs, sourceFluxField, refFluxField, match_tolerance=None)
def _filterRefCat(self, refCat, refFluxField)
def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, match_tolerance, sourceFluxField, verbose)
def __init__(self, **kwargs)
def __init__(self, maxMatchDist=None, autoMaxMatchDist=None, maxShift=None, lastMatchedPattern=None, failedPatternList=None, PPMbObj=None)
Lightweight representation of a geometric match between two records.