3 from scipy.spatial
import cKDTree
4 from scipy.stats
import sigmaclip
12 from .matchOptimisticB
import MatchTolerance
14 from .pessimistic_pattern_matcher_b_3D
import PessimisticPatternMatcherB
16 __all__ = [
"MatchPessimisticBTask",
"MatchPessimisticBConfig",
17 "MatchTolerancePessimistic"]
21 """Stores match tolerances for use in AstrometryTask and later 22 iterations of the matcher. 24 MatchPessimisticBTask relies on several state variables to be 25 preserved over different iterations in the 26 AstrometryTask.matchAndFitWcs loop of AstrometryTask. 30 maxMatchDist : `lsst.geom.Angle` 31 Maximum distance to consider a match from the previous match/fit 33 autoMaxMatchDist : `lsst.geom.Angle` 34 Automated estimation of the maxMatchDist from the sky statistics of the 35 source and reference catalogs. 36 maxShift : `lsst.geom.Angle` 37 Maximum shift found in the previous match/fit cycle. 38 lastMatchedPattern : `int` 39 Index of the last source pattern that was matched into the reference 41 failedPatternList : `list` of `int` 42 Previous matches were found to be false positives. 43 PPMbObj : `lsst.meas.astrom.PessimisticPatternMatcherB` 44 Initialized Pessimistic pattern matcher object. Storing this prevents 45 the need for recalculation of the searchable distances in the PPMB. 48 def __init__(self, maxMatchDist=None, autoMaxMatchDist=None,
49 maxShift=None, lastMatchedPattern=None,
50 failedPatternList=None, PPMbObj=None):
56 if failedPatternList
is None:
63 """Configuration for MatchPessimisticBTask 65 numBrightStars = pexConfig.RangeField(
66 doc=
"Number of bright stars to use. Sets the max number of patterns " 67 "that can be tested.",
72 minMatchedPairs = pexConfig.RangeField(
73 doc=
"Minimum number of matched pairs; see also minFracMatchedPairs.",
78 minFracMatchedPairs = pexConfig.RangeField(
79 doc=
"Minimum number of matched pairs as a fraction of the smaller of " 80 "the number of reference stars or the number of good sources; " 81 "the actual minimum is the smaller of this value or " 88 matcherIterations = pexConfig.RangeField(
89 doc=
"Number of softening iterations in matcher.",
94 maxOffsetPix = pexConfig.RangeField(
95 doc=
"Maximum allowed shift of WCS, due to matching (pixel). " 96 "When changing this value, the " 97 "LoadReferenceObjectsConfig.pixelMargin should also be updated.",
102 maxRotationDeg = pexConfig.RangeField(
103 doc=
"Rotation angle allowed between sources and position reference " 104 "objects (degrees).",
109 numPointsForShape = pexConfig.Field(
110 doc=
"Number of points to define a shape for matching.",
114 numPointsForShapeAttempt = pexConfig.Field(
115 doc=
"Number of points to try for creating a shape. This value should " 116 "be greater than or equal to numPointsForShape. Besides " 117 "loosening the signal to noise cut in the matcherSourceSelector, " 118 "increasing this number will solve CCDs where no match was found.",
122 minMatchDistPixels = pexConfig.RangeField(
123 doc=
"Distance in units of pixels to always consider a source-" 124 "reference pair a match. This prevents the astrometric fitter " 125 "from over-fitting and removing stars that should be matched and " 126 "allows for inclusion of new matches as the wcs improves.",
132 numPatternConsensus = pexConfig.Field(
133 doc=
"Number of implied shift/rotations from patterns that must agree " 134 "before it a given shift/rotation is accepted. This is only used " 135 "after the first softening iteration fails and if both the " 136 "number of reference and source objects is greater than " 141 maxRefObjects = pexConfig.RangeField(
142 doc=
"Maximum number of reference objects to use for the matcher. The " 143 "absolute maximum allowed for is 2 ** 16 for memory reasons.",
149 sourceSelector = sourceSelectorRegistry.makeField(
150 doc=
"How to select sources for cross-matching. The default " 151 "matcherSourceSelector removes objects with low S/N, bad " 152 "saturated objects, edge objects, and interpolated objects.",
153 default=
"matcherPessimistic" 158 sourceSelector.setDefaults()
161 pexConfig.Config.validate(self)
163 raise ValueError(
"numPointsForShapeAttempt must be greater than " 164 "or equal to numPointsForShape.")
166 raise ValueError(
"numBrightStars must be greater than " 167 "numPointsForShape.")
180 """Match sources to reference objects. 183 ConfigClass = MatchPessimisticBConfig
184 _DefaultName =
"matchObjectsToSources" 187 pipeBase.Task.__init__(self, **kwargs)
188 self.makeSubtask(
"sourceSelector")
192 match_tolerance=None):
193 """Match sources to position reference stars 195 refCat : `lsst.afw.table.SimpleCatalog` 196 catalog of reference objects that overlap the exposure; reads 200 - the specified flux field 202 sourceCat : `lsst.afw.table.SourceCatalog` 203 catalog of sources found on an exposure; Please check the required 204 fields of your specified source selector that the correct flags are present. 205 wcs : `lsst.afw.geom.SkyWcs` 208 field of refCat to use for flux 209 match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic` 210 is a MatchTolerance class object or `None`. This this class is used 211 to communicate state between AstrometryTask and MatcherTask. 212 AstrometryTask will also set the MatchTolerance class variable 213 maxMatchDist based on the scatter AstrometryTask has found after 218 result : `lsst.pipe.base.Struct` 219 Result struct with components: 221 - ``matches`` : source to reference matches found (`list` of 222 `lsst.afw.table.ReferenceMatch`) 223 - ``usableSourcCat`` : a catalog of sources potentially usable for 224 matching and WCS fitting (`lsst.afw.table.SourceCatalog`). 225 - ``match_tolerance`` : a MatchTolerance object containing the 226 resulting state variables from the match 227 (`lsst.meas.astrom.MatchTolerancePessimistic`). 234 if match_tolerance
is None:
238 numSources = len(sourceCat)
239 selectedSources = self.sourceSelector.
run(sourceCat)
240 goodSourceCat = selectedSources.sourceCat
241 numUsableSources = len(goodSourceCat)
242 self.log.
info(
"Purged %d sources, leaving %d good sources" %
243 (numSources - numUsableSources, numUsableSources))
245 if len(goodSourceCat) == 0:
246 raise pipeBase.TaskError(
"No sources are good")
251 minMatchedPairs =
min(self.config.minMatchedPairs,
252 int(self.config.minFracMatchedPairs *
253 min([len(refCat), len(goodSourceCat)])))
255 if len(refCat) > self.config.maxRefObjects:
257 "WARNING: Reference catalog larger that maximum allowed. " 258 "Trimming to %i" % self.config.maxRefObjects)
261 trimedRefCat = refCat
265 sourceCat=goodSourceCat,
267 refFluxField=refFluxField,
268 numUsableSources=numUsableSources,
269 minMatchedPairs=minMatchedPairs,
270 match_tolerance=match_tolerance,
271 sourceFluxField=self.sourceSelector.fluxField,
272 verbose=debug.verbose,
274 matches = doMatchReturn.matches
275 match_tolerance = doMatchReturn.match_tolerance
277 if len(matches) == 0:
278 raise RuntimeError(
"Unable to match sources")
280 self.log.
info(
"Matched %d sources" % len(matches))
281 if len(matches) < minMatchedPairs:
282 self.log.
warn(
"Number of matches is smaller than request")
284 return pipeBase.Struct(
286 usableSourceCat=goodSourceCat,
287 match_tolerance=match_tolerance,
290 def _filterRefCat(self, refCat, refFluxField):
291 """Sub-select a number of reference objects starting from the brightest 292 and maxing out at the number specified by maxRefObjects in the config. 294 No trimming is done if len(refCat) > config.maxRefObjects. 298 refCat : `lsst.afw.table.SimpleCatalog` 299 Catalog of reference objects to trim. 301 field of refCat to use for flux 304 outCat : `lsst.afw.table.SimpleCatalog` 305 Catalog trimmed to the number set in the task config from the 309 if len(refCat) <= self.config.maxRefObjects:
311 fluxArray = refCat.get(refFluxField)
312 sortedFluxArray = fluxArray[fluxArray.argsort()]
313 minFlux = sortedFluxArray[-(self.config.maxRefObjects + 1)]
315 selected = (refCat.get(refFluxField) > minFlux)
318 outCat.reserve(self.config.maxRefObjects)
319 outCat.extend(refCat[selected])
324 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
325 minMatchedPairs, match_tolerance, sourceFluxField, verbose):
326 """Implementation of matching sources to position reference objects 328 Unlike matchObjectsToSources, this method does not check if the sources 333 refCat : `lsst.afw.table.SimpleCatalog` 334 catalog of position reference objects that overlap an exposure 335 sourceCat : `lsst.afw.table.SourceCatalog` 336 catalog of sources found on the exposure 337 wcs : `lsst.afw.geom.SkyWcs` 338 estimated WCS of exposure 340 field of refCat to use for flux 341 numUsableSources : `int` 342 number of usable sources (sources with known centroid that are not 343 near the edge, but may be saturated) 344 minMatchedPairs : `int` 345 minimum number of matches 346 match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic` 347 a MatchTolerance object containing variables specifying matcher 348 tolerances and state from possible previous runs. 349 sourceFluxField : `str` 350 Name of the flux field in the source catalog. 352 Set true to print diagnostic information to std::cout 357 Results struct with components: 359 - ``matches`` : a list the matches found 360 (`list` of `lsst.afw.table.ReferenceMatch`). 361 - ``match_tolerance`` : MatchTolerance containing updated values from 362 this fit iteration (`lsst.meas.astrom.MatchTolerancePessimistic`) 371 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
372 for src_idx, srcObj
in enumerate(sourceCat):
373 coord = wcs.pixelToSky(srcObj.getCentroid())
374 theta = np.pi / 2 - coord.getLatitude().asRadians()
375 phi = coord.getLongitude().asRadians()
376 flux = srcObj.getPsfInstFlux()
377 src_array[src_idx, :] = \
380 if match_tolerance.PPMbObj
is None or \
381 match_tolerance.autoMaxMatchDist
is None:
385 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
386 for ref_idx, refObj
in enumerate(refCat):
387 theta = np.pi / 2 - refObj.getDec().asRadians()
388 phi = refObj.getRa().asRadians()
389 flux = refObj[refFluxField]
390 ref_array[ref_idx, :] = \
394 ref_array[:, :3], self.log)
395 self.log.
debug(
"Computing source statistics...")
398 self.log.
debug(
"Computing reference statistics...")
401 maxMatchDistArcSec = np.min((maxMatchDistArcSecSrc,
402 maxMatchDistArcSecRef))
403 match_tolerance.autoMaxMatchDist = afwgeom.Angle(
404 maxMatchDistArcSec, afwgeom.arcseconds)
408 if match_tolerance.maxShift
is None:
409 maxShiftArcseconds = (self.config.maxOffsetPix *
410 wcs.getPixelScale().asArcseconds())
414 maxShiftArcseconds = np.max(
415 (match_tolerance.maxShift.asArcseconds(),
416 self.config.minMatchDistPixels *
417 wcs.getPixelScale().asArcseconds()))
423 if match_tolerance.maxMatchDist
is None:
424 match_tolerance.maxMatchDist = match_tolerance.autoMaxMatchDist
426 maxMatchDistArcSec = np.max(
427 (self.config.minMatchDistPixels *
428 wcs.getPixelScale().asArcseconds(),
429 np.min((match_tolerance.maxMatchDist.asArcseconds(),
430 match_tolerance.autoMaxMatchDist.asArcseconds()))))
435 numConsensus = self.config.numPatternConsensus
436 minObjectsForConsensus = \
437 self.config.numBrightStars + 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 for soften_pattern
in range(self.config.matcherIterations):
451 if soften_pattern == 0
and soften_dist == 0
and \
452 match_tolerance.lastMatchedPattern
is not None:
461 run_n_consent = numConsensus
464 matcher_struct = match_tolerance.PPMbObj.match(
465 source_array=src_array,
466 n_check=self.config.numPointsForShapeAttempt + soften_pattern,
467 n_match=self.config.numPointsForShape,
468 n_agree=run_n_consent,
469 max_n_patterns=self.config.numBrightStars,
470 max_shift=maxShiftArcseconds,
471 max_rotation=self.config.maxRotationDeg,
472 max_dist=maxMatchDistArcSec * 2. ** soften_dist,
473 min_matches=minMatchedPairs,
474 pattern_skip_array=np.array(
475 match_tolerance.failedPatternList)
478 if soften_pattern == 0
and soften_dist == 0
and \
479 len(matcher_struct.match_ids) == 0
and \
480 match_tolerance.lastMatchedPattern
is not None:
487 match_tolerance.failedPatternList.append(
488 match_tolerance.lastMatchedPattern)
489 match_tolerance.lastMatchedPattern =
None 490 maxShiftArcseconds = \
491 self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds()
492 elif len(matcher_struct.match_ids) > 0:
495 match_tolerance.maxShift = \
496 matcher_struct.shift * afwgeom.arcseconds
497 match_tolerance.lastMatchedPattern = \
498 matcher_struct.pattern_idx
506 return pipeBase.Struct(
508 match_tolerance=match_tolerance,
520 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
521 clip_max_dist = np.max(
522 (sigmaclip(distances_arcsec, low=100, high=2)[-1],
523 self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds())
528 if not np.isfinite(clip_max_dist):
529 clip_max_dist = maxMatchDistArcSec
531 if clip_max_dist < maxMatchDistArcSec
and \
532 len(distances_arcsec[distances_arcsec < clip_max_dist]) < \
534 dist_cut_arcsec = maxMatchDistArcSec
536 dist_cut_arcsec = np.min((clip_max_dist, maxMatchDistArcSec))
541 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
543 if dist_arcsec < dist_cut_arcsec:
545 match.first = refCat[
int(match_id_pair[1])]
546 match.second = sourceCat[
int(match_id_pair[0])]
550 match.distance = match.first.getCoord().separation(
551 match.second.getCoord()).asArcseconds()
552 matches.append(match)
554 return pipeBase.Struct(
556 match_tolerance=match_tolerance,
559 def _latlong_flux_to_xyz_mag(self, theta, phi, flux):
560 """Convert angles theta and phi and a flux into unit sphere 561 x, y, z, and a relative magnitude. 563 Takes in a afw catalog object and converts the catalog object RA, DECs 564 to points on the unit sphere. Also converts the flux into a simple, 565 non-zero-pointed magnitude for relative sorting. 570 Angle from the north pole (z axis) of the sphere 572 Rotation around the sphere 576 output_array : `numpy.ndarray`, (N, 4) 577 Spherical unit vector x, y, z with flux. 579 output_array = np.empty(4, dtype=np.float64)
580 output_array[0] = np.sin(theta)*np.cos(phi)
581 output_array[1] = np.sin(theta)*np.sin(phi)
582 output_array[2] = np.cos(theta)
584 output_array[3] = -2.5 * np.log10(flux)
588 output_array[3] = 99.
592 def _get_pair_pattern_statistics(self, cat_array):
593 """ Compute the tolerances for the matcher automatically by comparing 594 pinwheel patterns as we would in the matcher. 596 We test how similar the patterns we can create from a given set of 597 objects by computing the spoke lengths for each pattern and sorting 598 them from smallest to largest. The match tolerance is the average 599 distance per spoke between the closest two patterns in the sorted 604 cat_array : `numpy.ndarray`, (N, 3) 605 array of 3 vectors representing the x, y, z position of catalog 606 objects on the unit sphere. 611 Suggested max match tolerance distance calculated from comparisons 612 between pinwheel patterns used in optimistic/pessimistic pattern 616 self.log.
debug(
"Starting automated tolerance calculation...")
620 pattern_array = np.empty(
621 (cat_array.shape[0] - self.config.numPointsForShape,
622 self.config.numPointsForShape - 1))
623 flux_args_array = np.argsort(cat_array[:, -1])
626 tmp_sort_array = cat_array[flux_args_array]
629 for start_idx
in range(cat_array.shape[0] -
630 self.config.numPointsForShape):
631 pattern_points = tmp_sort_array[start_idx:start_idx +
632 self.config.numPointsForShape, :-1]
633 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
634 pattern_array[start_idx, :] = np.sqrt(
635 pattern_delta[:, 0] ** 2 +
636 pattern_delta[:, 1] ** 2 +
637 pattern_delta[:, 2] ** 2)
642 pattern_array[start_idx, :] = pattern_array[
643 start_idx, np.argsort(pattern_array[start_idx, :])]
649 pattern_array[:, :(self.config.numPointsForShape - 1)])
650 dist_nearest_array, ids = dist_tree.query(
651 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
652 dist_nearest_array = dist_nearest_array[:, 1]
653 dist_nearest_array.sort()
657 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600. /
658 (self.config.numPointsForShape - 1.))
660 self.log.
debug(
"Automated tolerance")
661 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)
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Lightweight representation of a geometric match between two records.
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)