22__all__ = [
"MatchPessimisticBTask",
"MatchPessimisticBConfig",
23 "MatchTolerancePessimistic"]
26from scipy.spatial
import cKDTree
32from lsst.utils.timer
import timeMethod
34from .matchOptimisticBTask
import MatchTolerance
36from .pessimistic_pattern_matcher_b_3D
import PessimisticPatternMatcherB
40 """Stores match tolerances for use in AstrometryTask and later
41 iterations of the matcher.
43 MatchPessimisticBTask relies on several state variables to be
44 preserved over different iterations in the
45 AstrometryTask.matchAndFitWcs loop of AstrometryTask.
50 Maximum distance to consider a match
from the previous match/fit
53 Automated estimation of the maxMatchDist
from the sky statistics of the
54 source
and reference catalogs.
56 Maximum shift found
in the previous match/fit cycle.
57 lastMatchedPattern : `int`
58 Index of the last source pattern that was matched into the reference
60 failedPatternList : `list` of `int`
61 Previous matches were found to be false positives.
63 Initialized Pessimistic pattern matcher object. Storing this prevents
64 the need
for recalculation of the searchable distances
in the PPMB.
67 def __init__(self, maxMatchDist=None, autoMaxMatchDist=None,
68 maxShift=None, lastMatchedPattern=None,
69 failedPatternList=None, PPMbObj=None):
75 if failedPatternList
is None:
82 """Configuration for MatchPessimisticBTask
84 numBrightStars = pexConfig.RangeField(
85 doc="Number of bright stars to use. Sets the max number of patterns "
86 "that can be tested.",
91 minMatchedPairs = pexConfig.RangeField(
92 doc=
"Minimum number of matched pairs; see also minFracMatchedPairs.",
97 minFracMatchedPairs = pexConfig.RangeField(
98 doc=
"Minimum number of matched pairs as a fraction of the smaller of "
99 "the number of reference stars or the number of good sources; "
100 "the actual minimum is the smaller of this value or "
107 matcherIterations = pexConfig.RangeField(
108 doc=
"Number of softening iterations in matcher.",
113 maxOffsetPix = pexConfig.RangeField(
114 doc=
"Maximum allowed shift of WCS, due to matching (pixel). "
115 "When changing this value, the "
116 "LoadReferenceObjectsConfig.pixelMargin should also be updated.",
121 maxRotationDeg = pexConfig.RangeField(
122 doc=
"Rotation angle allowed between sources and position reference "
123 "objects (degrees).",
128 numPointsForShape = pexConfig.Field(
129 doc=
"Number of points to define a shape for matching.",
133 numPointsForShapeAttempt = pexConfig.Field(
134 doc=
"Number of points to try for creating a shape. This value should "
135 "be greater than or equal to numPointsForShape. Besides "
136 "loosening the signal to noise cut in the 'matcher' SourceSelector, "
137 "increasing this number will solve CCDs where no match was found.",
141 minMatchDistPixels = pexConfig.RangeField(
142 doc=
"Distance in units of pixels to always consider a source-"
143 "reference pair a match. This prevents the astrometric fitter "
144 "from over-fitting and removing stars that should be matched and "
145 "allows for inclusion of new matches as the wcs improves.",
151 numPatternConsensus = pexConfig.Field(
152 doc=
"Number of implied shift/rotations from patterns that must agree "
153 "before it a given shift/rotation is accepted. This is only used "
154 "after the first softening iteration fails and if both the "
155 "number of reference and source objects is greater than "
160 numRefRequireConsensus = pexConfig.Field(
161 doc=
"If the available reference objects exceeds this number, "
162 "consensus/pessimistic mode will enforced regardless of the "
163 "number of available sources. Below this optimistic mode ("
164 "exit at first match rather than requiring numPatternConsensus to "
165 "be matched) can be used. If more sources are required to match, "
166 "decrease the signal to noise cut in the sourceSelector.",
170 maxRefObjects = pexConfig.RangeField(
171 doc=
"Maximum number of reference objects to use for the matcher. The "
172 "absolute maximum allowed for is 2 ** 16 for memory reasons.",
180 pexConfig.Config.validate(self)
182 raise ValueError(
"numPointsForShapeAttempt must be greater than "
183 "or equal to numPointsForShape.")
185 raise ValueError(
"numBrightStars must be greater than "
186 "numPointsForShape.")
199 """Match sources to reference objects.
202 ConfigClass = MatchPessimisticBConfig
203 _DefaultName = "matchPessimisticB"
206 pipeBase.Task.__init__(self, **kwargs)
210 match_tolerance=None):
211 """Match sources to position reference stars
214 catalog of reference objects that overlap the exposure; reads
218 - the specified flux field
221 Catalog of sources found on an exposure. This should already be
222 down-selected to
"good"/
"usable" sources
in the calling Task.
225 sourceFluxField: `str`
226 field of sourceCat to use
for flux
228 field of refCat to use
for flux
230 is a MatchTolerance
class object or `
None`. This this
class is used
231 to communicate state between AstrometryTask
and MatcherTask.
232 AstrometryTask will also set the MatchTolerance
class variable
233 maxMatchDist based on the scatter AstrometryTask has found after
238 result : `lsst.pipe.base.Struct`
239 Result struct
with components:
241 - ``matches`` : source to reference matches found (`list` of
243 - ``usableSourceCat`` : a catalog of sources potentially usable
for
245 - ``match_tolerance`` : a MatchTolerance object containing the
246 resulting state variables
from the match
254 if match_tolerance
is None:
259 goodSourceCat = sourceCat
261 numUsableSources = len(goodSourceCat)
263 if len(goodSourceCat) == 0:
264 raise pipeBase.TaskError(
"No sources are good")
266 minMatchedPairs =
min(self.config.minMatchedPairs,
267 int(self.config.minFracMatchedPairs
268 *
min([len(refCat), len(goodSourceCat)])))
270 if len(goodSourceCat) <= self.config.numPointsForShape:
271 msg = (f
"Not enough catalog objects ({len(goodSourceCat)}) to make a "
272 f
"shape for the matcher (need {self.config.numPointsForShape}).")
273 raise RuntimeError(msg)
274 if len(refCat) <= self.config.numPointsForShape:
275 msg = (f
"Not enough refcat objects ({len(refCat)}) to make a "
276 f
"shape for the matcher (need {self.config.numPointsForShape}).")
277 raise RuntimeError(msg)
279 if len(refCat) > self.config.maxRefObjects:
281 "WARNING: Reference catalog larger that maximum allowed. "
282 "Trimming to %i", self.config.maxRefObjects)
285 trimmedRefCat = refCat
288 refCat=trimmedRefCat,
289 sourceCat=goodSourceCat,
291 refFluxField=refFluxField,
292 numUsableSources=numUsableSources,
293 minMatchedPairs=minMatchedPairs,
294 match_tolerance=match_tolerance,
295 sourceFluxField=sourceFluxField,
296 verbose=debug.verbose,
298 matches = doMatchReturn.matches
299 match_tolerance = doMatchReturn.match_tolerance
301 if len(matches) == 0:
302 raise RuntimeError(
"Unable to match sources")
304 self.
log.info(
"Matched %d sources", len(matches))
305 if len(matches) < minMatchedPairs:
306 self.
log.warning(
"Number of matches is smaller than request")
308 return pipeBase.Struct(
310 usableSourceCat=goodSourceCat,
311 match_tolerance=match_tolerance,
315 """Sub-select a number of reference objects starting from the brightest
316 and maxing out at the number specified by maxRefObjects
in the config.
318 No trimming
is done
if len(refCat) > config.maxRefObjects.
323 Catalog of reference objects to trim.
325 field of refCat to use
for flux
329 Catalog trimmed to the number set
in the task config
from the
333 if len(refCat) <= self.config.maxRefObjects:
335 fluxArray = refCat.get(refFluxField)
336 sortedFluxArray = fluxArray[fluxArray.argsort()]
337 minFlux = sortedFluxArray[-(self.config.maxRefObjects + 1)]
339 selected = (refCat.get(refFluxField) > minFlux)
342 outCat.reserve(self.config.maxRefObjects)
343 outCat.extend(refCat[selected])
348 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
349 minMatchedPairs, match_tolerance, sourceFluxField, verbose):
350 """Implementation of matching sources to position reference objects
352 Unlike matchObjectsToSources, this method does not check
if the sources
358 catalog of position reference objects that overlap an exposure
360 catalog of sources found on the exposure
362 estimated WCS of exposure
364 field of refCat to use
for flux
365 numUsableSources : `int`
366 number of usable sources (sources
with known centroid that are
not
367 near the edge, but may be saturated)
368 minMatchedPairs : `int`
369 minimum number of matches
371 a MatchTolerance object containing variables specifying matcher
372 tolerances
and state
from possible previous runs.
373 sourceFluxField : `str`
374 Name of the flux field
in the source catalog.
376 Set true to
print diagnostic information to std::cout
381 Results struct
with components:
383 - ``matches`` : a list the matches found
385 - ``match_tolerance`` : MatchTolerance containing updated values
from
395 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
396 for src_idx, srcObj
in enumerate(sourceCat):
397 coord = wcs.pixelToSky(srcObj.getCentroid())
398 theta = np.pi / 2 - coord.getLatitude().asRadians()
399 phi = coord.getLongitude().asRadians()
400 flux = srcObj[sourceFluxField]
401 src_array[src_idx, :] = \
404 if match_tolerance.PPMbObj
is None or \
405 match_tolerance.autoMaxMatchDist
is None:
409 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
410 for ref_idx, refObj
in enumerate(refCat):
411 theta = np.pi / 2 - refObj.getDec().asRadians()
412 phi = refObj.getRa().asRadians()
413 flux = refObj[refFluxField]
414 ref_array[ref_idx, :] = \
418 ref_array[:, :3], self.
log)
419 self.
log.debug(
"Computing source statistics...")
422 self.
log.debug(
"Computing reference statistics...")
425 maxMatchDistArcSec = np.max((
426 self.config.minMatchDistPixels
427 * wcs.getPixelScale().asArcseconds(),
428 np.min((maxMatchDistArcSecSrc,
429 maxMatchDistArcSecRef))))
430 match_tolerance.autoMaxMatchDist =
geom.Angle(
431 maxMatchDistArcSec, geom.arcseconds)
435 if match_tolerance.maxShift
is None:
436 maxShiftArcseconds = (self.config.maxOffsetPix
437 * wcs.getPixelScale().asArcseconds())
441 maxShiftArcseconds = np.max(
442 (match_tolerance.maxShift.asArcseconds(),
443 self.config.minMatchDistPixels
444 * wcs.getPixelScale().asArcseconds()))
450 if match_tolerance.maxMatchDist
is None:
451 match_tolerance.maxMatchDist = match_tolerance.autoMaxMatchDist
453 maxMatchDistArcSec = np.max(
454 (self.config.minMatchDistPixels
455 * wcs.getPixelScale().asArcseconds(),
456 np.min((match_tolerance.maxMatchDist.asArcseconds(),
457 match_tolerance.autoMaxMatchDist.asArcseconds()))))
463 numConsensus = self.config.numPatternConsensus
464 if len(refCat) < self.config.numRefRequireConsensus:
465 minObjectsForConsensus = \
466 self.config.numBrightStars + \
467 self.config.numPointsForShapeAttempt
468 if len(refCat) < minObjectsForConsensus
or \
469 len(sourceCat) < minObjectsForConsensus:
472 self.
log.debug(
"Current tol maxDist: %.4f arcsec",
474 self.
log.debug(
"Current shift: %.4f arcsec",
479 for soften_dist
in range(self.config.matcherIterations):
480 if soften_dist == 0
and \
481 match_tolerance.lastMatchedPattern
is not None:
490 run_n_consent = numConsensus
493 matcher_struct = match_tolerance.PPMbObj.match(
494 source_array=src_array,
495 n_check=self.config.numPointsForShapeAttempt,
496 n_match=self.config.numPointsForShape,
497 n_agree=run_n_consent,
498 max_n_patterns=self.config.numBrightStars,
499 max_shift=maxShiftArcseconds,
500 max_rotation=self.config.maxRotationDeg,
501 max_dist=maxMatchDistArcSec * 2. ** soften_dist,
502 min_matches=minMatchedPairs,
503 pattern_skip_array=np.array(
504 match_tolerance.failedPatternList)
507 if soften_dist == 0
and \
508 len(matcher_struct.match_ids) == 0
and \
509 match_tolerance.lastMatchedPattern
is not None:
516 match_tolerance.failedPatternList.append(
517 match_tolerance.lastMatchedPattern)
518 match_tolerance.lastMatchedPattern =
None
519 maxShiftArcseconds = \
520 self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds()
521 elif len(matcher_struct.match_ids) > 0:
524 match_tolerance.maxShift = \
525 matcher_struct.shift * geom.arcseconds
526 match_tolerance.lastMatchedPattern = \
527 matcher_struct.pattern_idx
533 return pipeBase.Struct(
535 match_tolerance=match_tolerance,
547 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
548 dist_cut_arcsec = np.max(
549 (np.degrees(matcher_struct.max_dist_rad) * 3600,
550 self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds()))
555 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
557 if dist_arcsec < dist_cut_arcsec:
559 match.first = refCat[int(match_id_pair[1])]
560 match.second = sourceCat[int(match_id_pair[0])]
564 match.distance = match.first.getCoord().separation(
565 match.second.getCoord()).asArcseconds()
566 matches.append(match)
568 return pipeBase.Struct(
570 match_tolerance=match_tolerance,
574 """Convert angles theta and phi and a flux into unit sphere
575 x, y, z, and a relative magnitude.
577 Takes
in a afw catalog object
and converts the catalog object RA, DECs
578 to points on the unit sphere. Also converts the flux into a simple,
579 non-zero-pointed magnitude
for relative sorting.
584 Angle
from the north pole (z axis) of the sphere
586 Rotation around the sphere
590 output_array : `numpy.ndarray`, (N, 4)
591 Spherical unit vector x, y, z
with flux.
593 output_array = np.empty(4, dtype=np.float64)
594 output_array[0] = np.sin(theta)*np.cos(phi)
595 output_array[1] = np.sin(theta)*np.sin(phi)
596 output_array[2] = np.cos(theta)
598 output_array[3] = -2.5 * np.log10(flux)
602 output_array[3] = 99.
607 """ Compute the tolerances for the matcher automatically by comparing
608 pinwheel patterns as we would
in the matcher.
610 We test how similar the patterns we can create
from a given set of
611 objects by computing the spoke lengths
for each pattern
and sorting
612 them
from smallest to largest. The match tolerance
is the average
613 distance per spoke between the closest two patterns
in the sorted
618 cat_array : `numpy.ndarray`, (N, 3)
619 array of 3 vectors representing the x, y, z position of catalog
620 objects on the unit sphere.
625 Suggested max match tolerance distance calculated
from comparisons
626 between pinwheel patterns used
in optimistic/pessimistic pattern
630 self.log.debug("Starting automated tolerance calculation...")
634 pattern_array = np.empty(
635 (cat_array.shape[0] - self.config.numPointsForShape,
636 self.config.numPointsForShape - 1))
637 flux_args_array = np.argsort(cat_array[:, -1])
640 tmp_sort_array = cat_array[flux_args_array]
643 for start_idx
in range(cat_array.shape[0]
644 - self.config.numPointsForShape):
645 pattern_points = tmp_sort_array[start_idx:start_idx
646 + self.config.numPointsForShape, :-1]
647 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
648 pattern_array[start_idx, :] = np.sqrt(
649 pattern_delta[:, 0] ** 2
650 + pattern_delta[:, 1] ** 2
651 + pattern_delta[:, 2] ** 2)
656 pattern_array[start_idx, :] = pattern_array[
657 start_idx, np.argsort(pattern_array[start_idx, :])]
663 pattern_array[:, :(self.config.numPointsForShape - 1)])
664 dist_nearest_array, ids = dist_tree.query(
665 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
666 dist_nearest_array = dist_nearest_array[:, 1]
667 dist_nearest_array.sort()
671 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600.
672 / (self.config.numPointsForShape - 1.))
674 self.
log.debug(
"Automated tolerance")
675 self.
log.debug(
"\tdistance/match tol: %.4f [arcsec]", dist_tol)
table::Key< std::string > object
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
A class representing an angle.
_get_pair_pattern_statistics(self, cat_array)
_latlong_flux_to_xyz_mag(self, theta, phi, flux)
_doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, match_tolerance, sourceFluxField, verbose)
_filterRefCat(self, refCat, refFluxField)
matchObjectsToSources(self, refCat, sourceCat, wcs, sourceFluxField, refFluxField, match_tolerance=None)
__init__(self, maxMatchDist=None, autoMaxMatchDist=None, maxShift=None, lastMatchedPattern=None, failedPatternList=None, PPMbObj=None)
Lightweight representation of a geometric match between two records.