3 from scipy.spatial
import cKDTree
6 import lsst.pipe.base
as pipeBase
9 from lsst.meas.algorithms.sourceSelector
import sourceSelectorRegistry
11 from .matchOptimisticBTask
import MatchTolerance
13 from .pessimistic_pattern_matcher_b_3D
import PessimisticPatternMatcherB
15 __all__ = [
"MatchPessimisticBTask",
"MatchPessimisticBConfig",
16 "MatchTolerancePessimistic"]
20 """Stores match tolerances for use in AstrometryTask and later 21 iterations of the matcher. 23 MatchPessimisticBTask relies on several state variables to be 24 preserved over different iterations in the 25 AstrometryTask.matchAndFitWcs loop of AstrometryTask. 29 maxMatchDist : `lsst.geom.Angle` 30 Maximum distance to consider a match from the previous match/fit 32 autoMaxMatchDist : `lsst.geom.Angle` 33 Automated estimation of the maxMatchDist from the sky statistics of the 34 source and reference catalogs. 35 maxShift : `lsst.geom.Angle` 36 Maximum shift found in the previous match/fit cycle. 37 lastMatchedPattern : `int` 38 Index of the last source pattern that was matched into the reference 40 failedPatternList : `list` of `int` 41 Previous matches were found to be false positives. 42 PPMbObj : `lsst.meas.astrom.PessimisticPatternMatcherB` 43 Initialized Pessimistic pattern matcher object. Storing this prevents 44 the need for recalculation of the searchable distances in the PPMB. 47 def __init__(self, maxMatchDist=None, autoMaxMatchDist=None,
48 maxShift=None, lastMatchedPattern=None,
49 failedPatternList=None, PPMbObj=None):
55 if failedPatternList
is None:
62 """Configuration for MatchPessimisticBTask 64 numBrightStars = pexConfig.RangeField(
65 doc=
"Number of bright stars to use. Sets the max number of patterns " 66 "that can be tested.",
71 minMatchedPairs = pexConfig.RangeField(
72 doc=
"Minimum number of matched pairs; see also minFracMatchedPairs.",
77 minFracMatchedPairs = pexConfig.RangeField(
78 doc=
"Minimum number of matched pairs as a fraction of the smaller of " 79 "the number of reference stars or the number of good sources; " 80 "the actual minimum is the smaller of this value or " 87 matcherIterations = pexConfig.RangeField(
88 doc=
"Number of softening iterations in matcher.",
93 maxOffsetPix = pexConfig.RangeField(
94 doc=
"Maximum allowed shift of WCS, due to matching (pixel). " 95 "When changing this value, the " 96 "LoadReferenceObjectsConfig.pixelMargin should also be updated.",
101 maxRotationDeg = pexConfig.RangeField(
102 doc=
"Rotation angle allowed between sources and position reference " 103 "objects (degrees).",
108 numPointsForShape = pexConfig.Field(
109 doc=
"Number of points to define a shape for matching.",
113 numPointsForShapeAttempt = pexConfig.Field(
114 doc=
"Number of points to try for creating a shape. This value should " 115 "be greater than or equal to numPointsForShape. Besides " 116 "loosening the signal to noise cut in the matcherSourceSelector, " 117 "increasing this number will solve CCDs where no match was found.",
121 minMatchDistPixels = pexConfig.RangeField(
122 doc=
"Distance in units of pixels to always consider a source-" 123 "reference pair a match. This prevents the astrometric fitter " 124 "from over-fitting and removing stars that should be matched and " 125 "allows for inclusion of new matches as the wcs improves.",
131 numPatternConsensus = pexConfig.Field(
132 doc=
"Number of implied shift/rotations from patterns that must agree " 133 "before it a given shift/rotation is accepted. This is only used " 134 "after the first softening iteration fails and if both the " 135 "number of reference and source objects is greater than " 140 numRefRequireConsensus = pexConfig.Field(
141 doc=
"If the available reference objects exceeds this number, " 142 "consensus/pessimistic mode will enforced regardless of the " 143 "number of available sources. Below this optimistic mode (" 144 "exit at first match rather than requiring numPatternConsensus to " 145 "be matched) can be used. If more sources are required to match, " 146 "decrease the signal to noise cut in the sourceSelector.",
150 maxRefObjects = pexConfig.RangeField(
151 doc=
"Maximum number of reference objects to use for the matcher. The " 152 "absolute maximum allowed for is 2 ** 16 for memory reasons.",
158 sourceSelector = sourceSelectorRegistry.makeField(
159 doc=
"How to select sources for cross-matching. The default " 160 "matcherSourceSelector removes objects with low S/N, bad " 161 "saturated objects, edge objects, and interpolated objects.",
162 default=
"matcherPessimistic" 167 sourceSelector.setDefaults()
170 pexConfig.Config.validate(self)
172 raise ValueError(
"numPointsForShapeAttempt must be greater than " 173 "or equal to numPointsForShape.")
175 raise ValueError(
"numBrightStars must be greater than " 176 "numPointsForShape.")
189 """Match sources to reference objects. 192 ConfigClass = MatchPessimisticBConfig
193 _DefaultName =
"matchObjectsToSources" 196 pipeBase.Task.__init__(self, **kwargs)
197 self.makeSubtask(
"sourceSelector")
201 match_tolerance=None):
202 """Match sources to position reference stars 204 refCat : `lsst.afw.table.SimpleCatalog` 205 catalog of reference objects that overlap the exposure; reads 209 - the specified flux field 211 sourceCat : `lsst.afw.table.SourceCatalog` 212 catalog of sources found on an exposure; Please check the required 213 fields of your specified source selector that the correct flags are present. 214 wcs : `lsst.afw.geom.SkyWcs` 217 field of refCat to use for flux 218 match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic` 219 is a MatchTolerance class object or `None`. This this class is used 220 to communicate state between AstrometryTask and MatcherTask. 221 AstrometryTask will also set the MatchTolerance class variable 222 maxMatchDist based on the scatter AstrometryTask has found after 227 result : `lsst.pipe.base.Struct` 228 Result struct with components: 230 - ``matches`` : source to reference matches found (`list` of 231 `lsst.afw.table.ReferenceMatch`) 232 - ``usableSourcCat`` : a catalog of sources potentially usable for 233 matching and WCS fitting (`lsst.afw.table.SourceCatalog`). 234 - ``match_tolerance`` : a MatchTolerance object containing the 235 resulting state variables from the match 236 (`lsst.meas.astrom.MatchTolerancePessimistic`). 243 if match_tolerance
is None:
247 numSources = len(sourceCat)
248 selectedSources = self.sourceSelector.run(sourceCat)
249 goodSourceCat = selectedSources.sourceCat
250 numUsableSources = len(goodSourceCat)
251 self.log.info(
"Purged %d sources, leaving %d good sources" %
252 (numSources - numUsableSources, numUsableSources))
254 if len(goodSourceCat) == 0:
255 raise pipeBase.TaskError(
"No sources are good")
260 minMatchedPairs = min(self.config.minMatchedPairs,
261 int(self.config.minFracMatchedPairs *
262 min([len(refCat), len(goodSourceCat)])))
264 if len(refCat) > self.config.maxRefObjects:
266 "WARNING: Reference catalog larger that maximum allowed. " 267 "Trimming to %i" % self.config.maxRefObjects)
270 trimedRefCat = refCat
274 sourceCat=goodSourceCat,
276 refFluxField=refFluxField,
277 numUsableSources=numUsableSources,
278 minMatchedPairs=minMatchedPairs,
279 match_tolerance=match_tolerance,
280 sourceFluxField=self.sourceSelector.fluxField,
281 verbose=debug.verbose,
283 matches = doMatchReturn.matches
284 match_tolerance = doMatchReturn.match_tolerance
286 if len(matches) == 0:
287 raise RuntimeError(
"Unable to match sources")
289 self.log.info(
"Matched %d sources" % len(matches))
290 if len(matches) < minMatchedPairs:
291 self.log.warn(
"Number of matches is smaller than request")
293 return pipeBase.Struct(
295 usableSourceCat=goodSourceCat,
296 match_tolerance=match_tolerance,
299 def _filterRefCat(self, refCat, refFluxField):
300 """Sub-select a number of reference objects starting from the brightest 301 and maxing out at the number specified by maxRefObjects in the config. 303 No trimming is done if len(refCat) > config.maxRefObjects. 307 refCat : `lsst.afw.table.SimpleCatalog` 308 Catalog of reference objects to trim. 310 field of refCat to use for flux 313 outCat : `lsst.afw.table.SimpleCatalog` 314 Catalog trimmed to the number set in the task config from the 318 if len(refCat) <= self.config.maxRefObjects:
320 fluxArray = refCat.get(refFluxField)
321 sortedFluxArray = fluxArray[fluxArray.argsort()]
322 minFlux = sortedFluxArray[-(self.config.maxRefObjects + 1)]
324 selected = (refCat.get(refFluxField) > minFlux)
327 outCat.reserve(self.config.maxRefObjects)
328 outCat.extend(refCat[selected])
333 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
334 minMatchedPairs, match_tolerance, sourceFluxField, verbose):
335 """Implementation of matching sources to position reference objects 337 Unlike matchObjectsToSources, this method does not check if the sources 342 refCat : `lsst.afw.table.SimpleCatalog` 343 catalog of position reference objects that overlap an exposure 344 sourceCat : `lsst.afw.table.SourceCatalog` 345 catalog of sources found on the exposure 346 wcs : `lsst.afw.geom.SkyWcs` 347 estimated WCS of exposure 349 field of refCat to use for flux 350 numUsableSources : `int` 351 number of usable sources (sources with known centroid that are not 352 near the edge, but may be saturated) 353 minMatchedPairs : `int` 354 minimum number of matches 355 match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic` 356 a MatchTolerance object containing variables specifying matcher 357 tolerances and state from possible previous runs. 358 sourceFluxField : `str` 359 Name of the flux field in the source catalog. 361 Set true to print diagnostic information to std::cout 366 Results struct with components: 368 - ``matches`` : a list the matches found 369 (`list` of `lsst.afw.table.ReferenceMatch`). 370 - ``match_tolerance`` : MatchTolerance containing updated values from 371 this fit iteration (`lsst.meas.astrom.MatchTolerancePessimistic`) 380 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
381 for src_idx, srcObj
in enumerate(sourceCat):
382 coord = wcs.pixelToSky(srcObj.getCentroid())
383 theta = np.pi / 2 - coord.getLatitude().asRadians()
384 phi = coord.getLongitude().asRadians()
385 flux = srcObj.getPsfInstFlux()
386 src_array[src_idx, :] = \
389 if match_tolerance.PPMbObj
is None or \
390 match_tolerance.autoMaxMatchDist
is None:
394 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
395 for ref_idx, refObj
in enumerate(refCat):
396 theta = np.pi / 2 - refObj.getDec().asRadians()
397 phi = refObj.getRa().asRadians()
398 flux = refObj[refFluxField]
399 ref_array[ref_idx, :] = \
403 ref_array[:, :3], self.log)
404 self.log.debug(
"Computing source statistics...")
407 self.log.debug(
"Computing reference statistics...")
410 maxMatchDistArcSec = np.max((
411 self.config.minMatchDistPixels *
412 wcs.getPixelScale().asArcseconds(),
413 np.min((maxMatchDistArcSecSrc,
414 maxMatchDistArcSecRef))))
415 match_tolerance.autoMaxMatchDist = afwgeom.Angle(
416 maxMatchDistArcSec, afwgeom.arcseconds)
420 if match_tolerance.maxShift
is None:
421 maxShiftArcseconds = (self.config.maxOffsetPix *
422 wcs.getPixelScale().asArcseconds())
426 maxShiftArcseconds = np.max(
427 (match_tolerance.maxShift.asArcseconds(),
428 self.config.minMatchDistPixels *
429 wcs.getPixelScale().asArcseconds()))
435 if match_tolerance.maxMatchDist
is None:
436 match_tolerance.maxMatchDist = match_tolerance.autoMaxMatchDist
438 maxMatchDistArcSec = np.max(
439 (self.config.minMatchDistPixels *
440 wcs.getPixelScale().asArcseconds(),
441 np.min((match_tolerance.maxMatchDist.asArcseconds(),
442 match_tolerance.autoMaxMatchDist.asArcseconds()))))
448 numConsensus = self.config.numPatternConsensus
449 if len(refCat) < self.config.numRefRequireConsensus:
450 minObjectsForConsensus = \
451 self.config.numBrightStars + \
452 self.config.numPointsForShapeAttempt
453 if len(refCat) < minObjectsForConsensus
or \
454 len(sourceCat) < minObjectsForConsensus:
457 self.log.debug(
"Current tol maxDist: %.4f arcsec" %
459 self.log.debug(
"Current shift: %.4f arcsec" %
464 for soften_dist
in range(self.config.matcherIterations):
465 if soften_dist == 0
and \
466 match_tolerance.lastMatchedPattern
is not None:
475 run_n_consent = numConsensus
478 matcher_struct = match_tolerance.PPMbObj.match(
479 source_array=src_array,
480 n_check=self.config.numPointsForShapeAttempt,
481 n_match=self.config.numPointsForShape,
482 n_agree=run_n_consent,
483 max_n_patterns=self.config.numBrightStars,
484 max_shift=maxShiftArcseconds,
485 max_rotation=self.config.maxRotationDeg,
486 max_dist=maxMatchDistArcSec * 2. ** soften_dist,
487 min_matches=minMatchedPairs,
488 pattern_skip_array=np.array(
489 match_tolerance.failedPatternList)
492 if soften_dist == 0
and \
493 len(matcher_struct.match_ids) == 0
and \
494 match_tolerance.lastMatchedPattern
is not None:
501 match_tolerance.failedPatternList.append(
502 match_tolerance.lastMatchedPattern)
503 match_tolerance.lastMatchedPattern =
None 504 maxShiftArcseconds = \
505 self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds()
506 elif len(matcher_struct.match_ids) > 0:
509 match_tolerance.maxShift = \
510 matcher_struct.shift * afwgeom.arcseconds
511 match_tolerance.lastMatchedPattern = \
512 matcher_struct.pattern_idx
518 return pipeBase.Struct(
520 match_tolerance=match_tolerance,
532 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
533 dist_cut_arcsec = np.max(
534 (np.degrees(matcher_struct.max_dist_rad) * 3600,
535 self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds()))
540 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
542 if dist_arcsec < dist_cut_arcsec:
544 match.first = refCat[int(match_id_pair[1])]
545 match.second = sourceCat[int(match_id_pair[0])]
549 match.distance = match.first.getCoord().separation(
550 match.second.getCoord()).asArcseconds()
551 matches.append(match)
553 return pipeBase.Struct(
555 match_tolerance=match_tolerance,
558 def _latlong_flux_to_xyz_mag(self, theta, phi, flux):
559 """Convert angles theta and phi and a flux into unit sphere 560 x, y, z, and a relative magnitude. 562 Takes in a afw catalog object and converts the catalog object RA, DECs 563 to points on the unit sphere. Also converts the flux into a simple, 564 non-zero-pointed magnitude for relative sorting. 569 Angle from the north pole (z axis) of the sphere 571 Rotation around the sphere 575 output_array : `numpy.ndarray`, (N, 4) 576 Spherical unit vector x, y, z with flux. 578 output_array = np.empty(4, dtype=np.float64)
579 output_array[0] = np.sin(theta)*np.cos(phi)
580 output_array[1] = np.sin(theta)*np.sin(phi)
581 output_array[2] = np.cos(theta)
583 output_array[3] = -2.5 * np.log10(flux)
587 output_array[3] = 99.
591 def _get_pair_pattern_statistics(self, cat_array):
592 """ Compute the tolerances for the matcher automatically by comparing 593 pinwheel patterns as we would in the matcher. 595 We test how similar the patterns we can create from a given set of 596 objects by computing the spoke lengths for each pattern and sorting 597 them from smallest to largest. The match tolerance is the average 598 distance per spoke between the closest two patterns in the sorted 603 cat_array : `numpy.ndarray`, (N, 3) 604 array of 3 vectors representing the x, y, z position of catalog 605 objects on the unit sphere. 610 Suggested max match tolerance distance calculated from comparisons 611 between pinwheel patterns used in optimistic/pessimistic pattern 615 self.log.debug(
"Starting automated tolerance calculation...")
619 pattern_array = np.empty(
620 (cat_array.shape[0] - self.config.numPointsForShape,
621 self.config.numPointsForShape - 1))
622 flux_args_array = np.argsort(cat_array[:, -1])
625 tmp_sort_array = cat_array[flux_args_array]
628 for start_idx
in range(cat_array.shape[0] -
629 self.config.numPointsForShape):
630 pattern_points = tmp_sort_array[start_idx:start_idx +
631 self.config.numPointsForShape, :-1]
632 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
633 pattern_array[start_idx, :] = np.sqrt(
634 pattern_delta[:, 0] ** 2 +
635 pattern_delta[:, 1] ** 2 +
636 pattern_delta[:, 2] ** 2)
641 pattern_array[start_idx, :] = pattern_array[
642 start_idx, np.argsort(pattern_array[start_idx, :])]
648 pattern_array[:, :(self.config.numPointsForShape - 1)])
649 dist_nearest_array, ids = dist_tree.query(
650 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
651 dist_nearest_array = dist_nearest_array[:, 1]
652 dist_nearest_array.sort()
656 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600. /
657 (self.config.numPointsForShape - 1.))
659 self.log.debug(
"Automated tolerance")
660 self.log.debug(
"\tdistance/match tol: %.4f [arcsec]" % dist_tol)
def __init__(self, kwargs)
def __init__(self, maxMatchDist=None, autoMaxMatchDist=None, maxShift=None, lastMatchedPattern=None, failedPatternList=None, PPMbObj=None)
def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, match_tolerance, sourceFluxField, verbose)
def _filterRefCat(self, refCat, refFluxField)
def matchObjectsToSources(self, refCat, sourceCat, wcs, refFluxField, match_tolerance=None)
def _get_pair_pattern_statistics(self, cat_array)
def _latlong_flux_to_xyz_mag(self, theta, phi, flux)