1 from __future__
import absolute_import, division, print_function
4 from scipy.spatial
import cKDTree
7 import lsst.pipe.base
as pipeBase
10 from lsst.meas.algorithms.sourceSelector
import sourceSelectorRegistry
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. 26 maxMatchDist : lsst.afw.geom.Angle 27 autoMaxMatchDist : lsst.afw.geom.Angle 28 maxShift : lsst.afw.geom.Angle 29 lastMatchedPattern : int 30 failedPatternList : list of ints 33 def __init__(self, maxMatchDist=None, autoMaxMatchDist=None,
34 maxShift=None, lastMatchedPattern=None,
35 failedPatternList=None):
36 """Construct a MatchPessimisticTolerance 38 MatchPessimisticBTask relies on several state variables to be 39 preserved over different iterations in the 40 AstrometryTask.matchAndFitWcs loop of AstrometryTask. 44 maxMatchDist : afw.geom.Angle 45 Current 2 sigma scatter from the previous matched wcs (if it 46 exists. It is None if this is the first iteration.) 47 autoMatxMatchDist : afw.geom.Angle 48 Result of the automated match tolerance generation. 49 maxShift : afw.geom.Angle 50 None for the first iteration or is the magnitude of the previous 51 iteration's wcs shift. 52 lastMatchedPattern : int 53 Reference to the position in themagnitude sorted source array where 54 a successful pattern match was found. 55 failedPatternList : list of ints 56 List of ints specifying indicies in the magnitude sourced source 57 array to skip. These are skipped are previous iterations that are 58 likely false positives due to the code having to soften after a 65 if failedPatternList
is None:
70 """Configuration for MatchPessimisticBTask 72 numBrightStars = pexConfig.RangeField(
73 doc=
"Number of bright stars to use. Sets the max number of patterns " 74 "that can be tested.",
79 minMatchedPairs = pexConfig.RangeField(
80 doc=
"Minimum number of matched pairs; see also minFracMatchedPairs.",
85 minFracMatchedPairs = pexConfig.RangeField(
86 doc=
"Minimum number of matched pairs as a fraction of the smaller of " 87 "the number of reference stars or the number of good sources; " 88 "the actual minimum is the smaller of this value or " 95 matcherIterations = pexConfig.RangeField(
96 doc=
"Number of softening iterations in matcher.",
101 maxOffsetPix = pexConfig.RangeField(
102 doc=
"Maximum allowed shift of WCS, due to matching (pixel). " 103 "When changing this value, the LoadReferenceObjectsConfig.pixelMargin should also be updated.",
108 maxRotationDeg = pexConfig.RangeField(
109 doc=
"Rotation angle allowed between sources and position reference " 110 "objects (degrees).",
115 numPointsForShape = pexConfig.Field(
116 doc=
"Number of points to define a shape for matching.",
120 numPointsForShapeAttempt = pexConfig.Field(
121 doc=
"Number of points to try for creating a shape. This value should " 122 "be greater than or equal to numPointsForShape.",
126 numPatternConsensus = pexConfig.Field(
127 doc=
"Number of implied shift/rotations from patterns that must agree " 128 "before it a given shift/rotation is accepted. This is only used " 129 "after the first softening iteration fails and if both the " 130 "number of reference and source objects is greater than " 135 sourceSelector = sourceSelectorRegistry.makeField(
136 doc=
"How to select sources for cross-matching. The default " 137 "matcherSourceSelector removes objects with low S/N, bad " 138 "saturated objects, edge objects, and interpolated objects.",
139 default=
"matcherPessimistic" 144 sourceSelector.setDefaults()
147 pexConfig.Config.validate(self)
149 raise ValueError(
"numPointsForShapeAttempt must be greater than " 150 "or equal to numPointsForShape.")
163 """!Match sources to reference objects 165 @anchor MatchPessimisticBTask_ 167 @section meas_astrom_MatchPessimisticB_Contents Contents 169 - @ref meas_astrom_MatchPessimisticB_Purpose 170 - @ref meas_astrom_MatchPessimisticB_Initialize 171 - @ref meas_astrom_MatchPessimisticB_IO 172 - @ref meas_astrom_MatchPessimisticB_Config 173 - @ref meas_astrom_MatchPessimisticB_Example 174 - @ref meas_astrom_MatchPessimisticB_Debug 176 @section meas_astrom_MatchPessimisticB_Purpose Description 178 Match sources to reference objects. This is often done as a preliminary 179 step to fitting an astrometric or photometric solution. For details about 180 the matching algorithm see pessimistic_pattern_matcher_b_3D.py 182 @section meas_astrom_MatchPessimisticB_Initialize Task initialisation 184 @copydoc \_\_init\_\_ 186 @section meas_astrom_MatchPessimisticB_IO Invoking the Task 188 @copydoc matchObjectsToSources 190 @section meas_astrom_MatchPessimisticB_Config Configuration 193 See @ref MatchPessimisticBConfig 195 To modify the tests for good sources for matching, create a new 196 sourceSelector class in meas_algorithms and use it in the config. 198 @section meas_astrom_MatchPessimisticB_Example A complete example of 199 using MatchPessimisticBTask 201 MatchPessimisticBTask is a subtask of AstrometryTask, which is called by 202 PhotoCalTask. See \ref meas_photocal_photocal_Example. 204 @section meas_astrom_MatchPessimisticB_Debug Debug variables 206 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink 207 interface supports a flag @c -d to import @b debug.py from your 208 @c PYTHONPATH; see @ref baseDebug for more about @b debug.py files. 210 The available variables in MatchPessimisticBTask are: 212 <DT> @c verbose (bool) 213 <DD> If True then the matcher prints debug messages to stdout 216 To investigate the @ref meas_astrom_MatchPessimisticB_Debug, put something 221 # N.b. lsstDebug.Info(name) would call us recursively 222 debug = lsstDebug.getInfo(name) 223 if name == "lsst.pipe.tasks.astrometry": 228 lsstDebug.Info = DebugInfo 230 into your debug.py file and run this task with the @c --debug flag. 233 ConfigClass = MatchPessimisticBConfig
234 _DefaultName =
"matchObjectsToSources" 237 pipeBase.Task.__init__(self, **kwargs)
238 self.makeSubtask(
"sourceSelector")
242 match_tolerance=None):
243 """!Match sources to position reference stars 245 @param[in] refCat catalog of reference objects that overlap the 246 exposure; reads fields for: 248 - the specified flux field 249 @param[in] sourceCat catalog of sources found on an exposure; 250 Please check the required fields of your specified source selector 251 that the correct flags are present. 252 @param[in] wcs estimated WCS 253 @param[in] refFluxField field of refCat to use for flux 254 @param[in] match_tolerance is a MatchTolerance class object or None. 255 This this class is used to comunicate state between AstrometryTask 256 and MatcherTask. AstrometryTask will also set the MatchTolerance 257 class variable maxMatchDist based on the scatter AstrometryTask has 258 found after fitting for the wcs. 259 @return an lsst.pipe.base.Struct with fields: 260 - matches a list of matches, each instance of 261 lsst.afw.table.ReferenceMatch 262 - usableSourcCat a catalog of sources potentially usable for 264 - match_tolerance a MatchTolerance object containing the resulting 265 state variables from the match. 272 if match_tolerance
is None:
276 numSources = len(sourceCat)
277 selectedSources = self.sourceSelector.selectSources(sourceCat)
278 goodSourceCat = selectedSources.sourceCat
279 numUsableSources = len(goodSourceCat)
280 self.log.info(
"Purged %d sources, leaving %d good sources" %
281 (numSources - numUsableSources, numUsableSources))
283 if len(goodSourceCat) == 0:
284 raise pipeBase.TaskError(
"No sources are good")
289 minMatchedPairs = min(self.config.minMatchedPairs,
290 int(self.config.minFracMatchedPairs *
291 min([len(refCat), len(goodSourceCat)])))
295 sourceCat=goodSourceCat,
297 refFluxField=refFluxField,
298 numUsableSources=numUsableSources,
299 minMatchedPairs=minMatchedPairs,
300 match_tolerance=match_tolerance,
301 sourceFluxField=self.sourceSelector.fluxField,
302 verbose=debug.verbose,
304 matches = doMatchReturn.matches
305 match_tolerance = doMatchReturn.match_tolerance
307 if len(matches) == 0:
308 raise RuntimeError(
"Unable to match sources")
310 self.log.info(
"Matched %d sources" % len(matches))
311 if len(matches) < minMatchedPairs:
312 self.log.warn(
"Number of matches is smaller than request")
314 return pipeBase.Struct(
316 usableSourceCat=goodSourceCat,
317 match_tolerance=match_tolerance,
321 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
322 minMatchedPairs, match_tolerance, sourceFluxField, verbose):
323 """!Implementation of matching sources to position reference stars 325 Unlike matchObjectsToSources, this method does not check if the sources 328 @param[in] refCat catalog of position reference stars that overlap an 330 @param[in] sourceCat catalog of sources found on the exposure 331 @param[in] wcs estimated WCS of exposure 332 @param[in] refFluxField field of refCat to use for flux 333 @param[in] numUsableSources number of usable sources (sources with 334 known centroid that are not near the edge, but may be saturated) 335 @param[in] minMatchedPairs minimum number of matches 336 @param[in] match_tolerance a MatchTolerance object containing 337 variables specifying matcher tolerances and state from possible 339 @param[in] sourceInfo SourceInfo for the sourceCat 340 @param[in] verbose true to print diagnostic information to std::cout 342 @return a list of matches, an instance of 343 lsst.afw.table.ReferenceMatch, a MatchTolerance object 352 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
353 for ref_idx, refObj
in enumerate(refCat):
354 theta = np.pi / 2 - refObj.getDec().asRadians()
355 phi = refObj.getRa().asRadians()
356 flux = refObj[refFluxField]
357 ref_array[ref_idx, :] = \
360 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
361 for src_idx, srcObj
in enumerate(sourceCat):
362 coord = wcs.pixelToSky(srcObj.getCentroid())
363 theta = np.pi / 2 - coord.getLatitude().asRadians()
364 phi = coord.getLongitude().asRadians()
365 flux = srcObj.getPsfFlux()
366 src_array[src_idx, :] = \
371 if match_tolerance.maxShift
is None:
372 maxShiftArcseconds = (self.config.maxOffsetPix *
373 wcs.pixelScale().asArcseconds())
377 maxShiftArcseconds = np.max(
378 (match_tolerance.maxShift.asArcseconds(),
379 wcs.pixelScale().asArcseconds()))
385 if match_tolerance.maxMatchDist
is None:
386 self.log.debug(
"Computing source statistics...")
389 self.log.debug(
"Computing reference statistics...")
392 maxMatchDistArcSec = np.min((maxMatchDistArcSecSrc,
393 maxMatchDistArcSecRef))
394 match_tolerance.autoMaxDist = afwgeom.Angle(maxMatchDistArcSec,
397 maxMatchDistArcSec = np.max(
398 (wcs.pixelScale().asArcseconds(),
399 np.min((match_tolerance.maxMatchDist.asArcseconds(),
400 match_tolerance.autoMaxDist.asArcseconds()))))
405 numConsensus = self.config.numPatternConsensus
406 minObjectsForConsensus = \
407 self.config.numBrightStars + self.config.numPointsForShapeAttempt
408 if ref_array.shape[0] < minObjectsForConsensus
or \
409 src_array.shape[0] < minObjectsForConsensus:
412 self.log.debug(
"Current tol maxDist: %.4f arcsec" %
414 self.log.debug(
"Current shift: %.4f arcsec" %
421 for try_idx
in range(self.config.matcherIterations):
422 if try_idx == 0
and \
423 match_tolerance.lastMatchedPattern
is not None:
428 matcher_struct = pyPPMb.match(
429 source_array=src_array,
430 n_check=self.config.numPointsForShapeAttempt,
431 n_match=self.config.numPointsForShape,
433 max_n_patterns=self.config.numBrightStars,
434 max_shift=maxShiftArcseconds,
435 max_rotation=self.config.maxRotationDeg,
436 max_dist=maxMatchDistArcSec,
437 min_matches=minMatchedPairs,
438 pattern_skip_array=np.array(
439 match_tolerance.failedPatternList)
447 matcher_struct = pyPPMb.match(
448 source_array=src_array,
449 n_check=self.config.numPointsForShapeAttempt + 2 * try_idx,
450 n_match=self.config.numPointsForShape + try_idx,
451 n_agree=numConsensus,
452 max_n_patterns=self.config.numBrightStars,
453 max_shift=(self.config.maxOffsetPix *
454 wcs.pixelScale().asArcseconds()),
455 max_rotation=self.config.maxRotationDeg,
456 max_dist=maxMatchDistArcSec * 2. ** try_idx,
457 min_matches=minMatchedPairs,
458 pattern_skip_array=np.array(
459 match_tolerance.failedPatternList)
461 if try_idx == 0
and \
462 len(matcher_struct.match_ids) == 0
and \
463 match_tolerance.lastMatchedPattern
is not None:
470 match_tolerance.failedPatternList.append(
471 match_tolerance.lastMatchedPattern)
472 match_tolerance.lastMatchedPattern =
None 473 elif len(matcher_struct.match_ids) > 0:
476 match_tolerance.maxShift = afwgeom.Angle(matcher_struct.shift,
478 match_tolerance.lastMatchedPattern = \
479 matcher_struct.pattern_idx
486 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
495 values=distances_arcsec, n_sigma=2,
496 input_cut=np.max((10 * wcs.pixelScale().asArcseconds(),
498 maxShiftArcseconds)))
504 dist_cut_arcsec = np.max(
505 (2 * wcs.pixelScale().asArcseconds(),
506 clipped_dist_arcsec, maxMatchDistArcSec))
511 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
513 if dist_arcsec < dist_cut_arcsec:
515 match.first = refCat[match_id_pair[1]]
516 match.second = sourceCat[match_id_pair[0]]
520 match.distance = match.first.getCoord().angularSeparation(
521 match.second.getCoord()).asArcseconds()
522 matches.append(match)
524 return pipeBase.Struct(
526 match_tolerance=match_tolerance,
529 def _sigma_clip(self, values, n_sigma, input_cut=None):
531 Find the distance cut by recursively cliping by n_sigma. 533 The algorithm converges once the length of the array on the 534 nth iteration is equal to the the length of the array on the 540 float array of values to caculate the cut on. 542 Number of sigma to use for the recersive clipping. 544 Optional starting cut for the clipping. 549 Value at which the clipping algorithm converged or the 550 value after a maximum of 100 iterations. 555 if input_cut
is None:
556 hold_cut = np.max(values)
560 for clip_idx
in range(100):
562 mask = (values < hold_cut)
563 total = len(values[mask])
566 if total == hold_total:
571 hold_cut = (np.mean(values[mask]) +
572 n_sigma * np.std(values[mask], ddof=1))
575 def _latlong_flux_to_xyz_mag(self, theta, phi, flux):
576 r"""Convert angles theta and phi plux a flux into unit sphere 577 x, y, z, and a relative magnitude. 579 Takes in a afw catalog objet and converts the catalog object RA, DECs 580 to points on the unit sphere. Also convets the flux into a simple, 581 non-zeropointed magnitude for relative sorting. 586 Angle from the north pole (z axis) of the sphere 588 Rotation around the sphere 593 Spherical unit vector x, y, z on the unitsphere. 595 output_array = np.empty(4, dtype=np.float64)
596 output_array[0] = np.sin(theta)*np.cos(phi)
597 output_array[1] = np.sin(theta)*np.sin(phi)
598 output_array[2] = np.cos(theta)
600 output_array[3] = -2.5 * np.log10(flux)
604 output_array[3] = 99.
608 def _get_pair_pattern_statistics(self, cat_array):
609 """ Compute the tolerances for the matcher automatically by comparing 610 pinwheel patterns as we would in the matcher. 612 We test how simplar the patterns we can create from a given set of 613 objects by computing the spoke lengths for each pattern and sorting 614 them from smallest to largest. The match tolerance is the average 615 distance per spoke between the closest two patterns in the sorted 620 cat_array : float array 621 array of 3 vectors representing the x, y, z position of catalog 622 objedts on the unit sphere. 627 Suggested max match tolerance distance calculated from comparisons 628 between pinwheel patterns used in optimistic/pessimsitic pattern 632 self.log.debug(
"Starting automated tolerance calculation...")
636 pattern_array = np.empty(
637 (cat_array.shape[0] - self.config.numPointsForShape,
638 self.config.numPointsForShape - 1))
639 flux_args_array = np.argsort(cat_array[:, -1])
642 tmp_sort_array = cat_array[flux_args_array]
645 for start_idx
in range(cat_array.shape[0] -
646 self.config.numPointsForShape):
647 pattern_points = tmp_sort_array[start_idx:start_idx +
648 self.config.numPointsForShape, :-1]
649 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
650 pattern_array[start_idx, :] = np.sqrt(
651 pattern_delta[:, 0] ** 2 +
652 pattern_delta[:, 1] ** 2 +
653 pattern_delta[:, 2] ** 2)
658 pattern_array[start_idx, :] = pattern_array[
659 start_idx, np.argsort(pattern_array[start_idx, :])]
665 pattern_array[:, :(self.config.numPointsForShape - 1)])
666 dist_nearest_array, ids = dist_tree.query(
667 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
668 dist_nearest_array = dist_nearest_array[:, 1]
669 dist_nearest_array.sort()
673 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600. /
674 (self.config.numPointsForShape - 1.))
676 self.log.debug(
"Automated tolerance")
677 self.log.debug(
"\tdistance/match tol: %.4f [arcsec]" % dist_tol)
Match sources to reference objects.
def __init__(self, kwargs)
def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, match_tolerance, sourceFluxField, verbose)
Implementation of matching sources to position reference stars.
def __init__(self, maxMatchDist=None, autoMaxMatchDist=None, maxShift=None, lastMatchedPattern=None, failedPatternList=None)
def _sigma_clip(self, values, n_sigma, input_cut=None)
def matchObjectsToSources(self, refCat, sourceCat, wcs, refFluxField, match_tolerance=None)
Match sources to position reference stars.
def _get_pair_pattern_statistics(self, cat_array)
def _latlong_flux_to_xyz_mag(self, theta, phi, flux)