1 from __future__
import absolute_import, division, print_function
4 from scipy.spatial
import cKDTree
6 import lsst.pex.config
as pexConfig
7 import lsst.pipe.base
as pipeBase
8 import lsst.afw.geom
as afwgeom
9 import lsst.afw.table
as afwTable
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).",
107 maxRotationDeg = pexConfig.RangeField(
108 doc=
"Rotation angle allowed between sources and position reference " 109 "objects (degrees).",
114 numPointsForShape = pexConfig.Field(
115 doc=
"Number of points to define a shape for matching.",
119 numPointsForShapeAttempt = pexConfig.Field(
120 doc=
"Number of points to try for creating a shape. This value should " 121 "be greater than or equal to numPointsForShape.",
125 numPatternConsensus = pexConfig.Field(
126 doc=
"Number of implied shift/rotations from patterns that must agree " 127 "before it a given shift/rotation is accepted. This is only used " 128 "after the first softening iteration fails and if both the " 129 "number of reference and source objects is greater than " 134 sourceSelector = sourceSelectorRegistry.makeField(
135 doc=
"How to select sources for cross-matching. The default " 136 "matcherSourceSelector removes objects with low S/N, bad " 137 "saturated objects, edge objects, and interpolated objects.",
138 default=
"matcherPessimistic" 143 sourceSelector.setDefaults()
146 pexConfig.Config.validate(self)
148 raise ValueError(
"numPointsForShapeAttempt must be greater than " 149 "or equal to numPointsForShape.")
162 """!Match sources to reference objects 164 @anchor MatchPessimisticBTask_ 166 @section meas_astrom_MatchPessimisticB_Contents Contents 168 - @ref meas_astrom_MatchPessimisticB_Purpose 169 - @ref meas_astrom_MatchPessimisticB_Initialize 170 - @ref meas_astrom_MatchPessimisticB_IO 171 - @ref meas_astrom_MatchPessimisticB_Config 172 - @ref meas_astrom_MatchPessimisticB_Example 173 - @ref meas_astrom_MatchPessimisticB_Debug 175 @section meas_astrom_MatchPessimisticB_Purpose Description 177 Match sources to reference objects. This is often done as a preliminary 178 step to fitting an astrometric or photometric solution. For details about 179 the matching algorithm see pessimistic_pattern_matcher_b_3D.py 181 @section meas_astrom_MatchPessimisticB_Initialize Task initialisation 183 @copydoc \_\_init\_\_ 185 @section meas_astrom_MatchPessimisticB_IO Invoking the Task 187 @copydoc matchObjectsToSources 189 @section meas_astrom_MatchPessimisticB_Config Configuration 192 See @ref MatchPessimisticBConfig 194 To modify the tests for good sources for matching, create a new 195 sourceSelector class in meas_algorithms and use it in the config. 197 @section meas_astrom_MatchPessimisticB_Example A complete example of 198 using MatchPessimisticBTask 200 MatchPessimisticBTask is a subtask of AstrometryTask, which is called by 201 PhotoCalTask. See \ref meas_photocal_photocal_Example. 203 @section meas_astrom_MatchPessimisticB_Debug Debug variables 205 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink 206 interface supports a flag @c -d to import @b debug.py from your 207 @c PYTHONPATH; see @ref baseDebug for more about @b debug.py files. 209 The available variables in MatchPessimisticBTask are: 211 <DT> @c verbose (bool) 212 <DD> If True then the matcher prints debug messages to stdout 215 To investigate the @ref meas_astrom_MatchPessimisticB_Debug, put something 220 # N.b. lsstDebug.Info(name) would call us recursively 221 debug = lsstDebug.getInfo(name) 222 if name == "lsst.pipe.tasks.astrometry": 227 lsstDebug.Info = DebugInfo 229 into your debug.py file and run this task with the @c --debug flag. 232 ConfigClass = MatchPessimisticBConfig
233 _DefaultName =
"matchObjectsToSources" 236 pipeBase.Task.__init__(self, **kwargs)
237 self.makeSubtask(
"sourceSelector")
241 match_tolerance=None):
242 """!Match sources to position reference stars 244 @param[in] refCat catalog of reference objects that overlap the 245 exposure; reads fields for: 247 - the specified flux field 248 @param[in] sourceCat catalog of sources found on an exposure; 249 Please check the required fields of your specified source selector 250 that the correct flags are present. 251 @param[in] wcs estimated WCS 252 @param[in] refFluxField field of refCat to use for flux 253 @param[in] match_tolerance is a MatchTolerance class object or None. 254 This this class is used to comunicate state between AstrometryTask 255 and MatcherTask. AstrometryTask will also set the MatchTolerance 256 class variable maxMatchDist based on the scatter AstrometryTask has 257 found after fitting for the wcs. 258 @return an lsst.pipe.base.Struct with fields: 259 - matches a list of matches, each instance of 260 lsst.afw.table.ReferenceMatch 261 - usableSourcCat a catalog of sources potentially usable for 263 - match_tolerance a MatchTolerance object containing the resulting 264 state variables from the match. 267 debug = lsstDebug.Info(__name__)
271 if match_tolerance
is None:
275 numSources = len(sourceCat)
276 selectedSources = self.sourceSelector.selectSources(sourceCat)
277 goodSourceCat = selectedSources.sourceCat
278 numUsableSources = len(goodSourceCat)
279 self.log.info(
"Purged %d sources, leaving %d good sources" %
280 (numSources - numUsableSources, numUsableSources))
282 if len(goodSourceCat) == 0:
283 raise pipeBase.TaskError(
"No sources are good")
288 minMatchedPairs = min(self.config.minMatchedPairs,
289 int(self.config.minFracMatchedPairs *
290 min([len(refCat), len(goodSourceCat)])))
294 sourceCat=goodSourceCat,
296 refFluxField=refFluxField,
297 numUsableSources=numUsableSources,
298 minMatchedPairs=minMatchedPairs,
299 match_tolerance=match_tolerance,
300 sourceFluxField=self.sourceSelector.fluxField,
301 verbose=debug.verbose,
303 matches = doMatchReturn.matches
304 match_tolerance = doMatchReturn.match_tolerance
306 if len(matches) == 0:
307 raise RuntimeError(
"Unable to match sources")
309 self.log.info(
"Matched %d sources" % len(matches))
310 if len(matches) < minMatchedPairs:
311 self.log.warn(
"Number of matches is smaller than request")
313 return pipeBase.Struct(
315 usableSourceCat=goodSourceCat,
316 match_tolerance=match_tolerance,
320 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
321 minMatchedPairs, match_tolerance, sourceFluxField, verbose):
322 """!Implementation of matching sources to position reference stars 324 Unlike matchObjectsToSources, this method does not check if the sources 327 @param[in] refCat catalog of position reference stars that overlap an 329 @param[in] sourceCat catalog of sources found on the exposure 330 @param[in] wcs estimated WCS of exposure 331 @param[in] refFluxField field of refCat to use for flux 332 @param[in] numUsableSources number of usable sources (sources with 333 known centroid that are not near the edge, but may be saturated) 334 @param[in] minMatchedPairs minimum number of matches 335 @param[in] match_tolerance a MatchTolerance object containing 336 variables specifying matcher tolerances and state from possible 338 @param[in] sourceInfo SourceInfo for the sourceCat 339 @param[in] verbose true to print diagnostic information to std::cout 341 @return a list of matches, an instance of 342 lsst.afw.table.ReferenceMatch, a MatchTolerance object 351 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
352 for ref_idx, refObj
in enumerate(refCat):
353 theta = np.pi / 2 - refObj.getDec().asRadians()
354 phi = refObj.getRa().asRadians()
355 flux = refObj[refFluxField]
356 ref_array[ref_idx, :] = \
359 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
360 for src_idx, srcObj
in enumerate(sourceCat):
361 coord = wcs.pixelToSky(srcObj.getCentroid())
362 theta = np.pi / 2 - coord.getLatitude().asRadians()
363 phi = coord.getLongitude().asRadians()
364 flux = srcObj.getPsfFlux()
365 src_array[src_idx, :] = \
370 if match_tolerance.maxShift
is None:
371 maxShiftArcseconds = (self.config.maxOffsetPix *
372 wcs.pixelScale().asArcseconds())
376 maxShiftArcseconds = np.max(
377 (match_tolerance.maxShift.asArcseconds(),
378 wcs.pixelScale().asArcseconds()))
384 if match_tolerance.maxMatchDist
is None:
385 self.log.debug(
"Computing source statistics...")
388 self.log.debug(
"Computing reference statistics...")
391 maxMatchDistArcSec = np.min((maxMatchDistArcSecSrc,
392 maxMatchDistArcSecRef))
393 match_tolerance.autoMaxDist = afwgeom.Angle(maxMatchDistArcSec,
396 maxMatchDistArcSec = np.max(
397 (wcs.pixelScale().asArcseconds(),
398 np.min((match_tolerance.maxMatchDist.asArcseconds(),
399 match_tolerance.autoMaxDist.asArcseconds()))))
404 numConsensus = self.config.numPatternConsensus
405 minObjectsForConsensus = \
406 self.config.numBrightStars + self.config.numPointsForShapeAttempt
407 if ref_array.shape[0] < minObjectsForConsensus
or \
408 src_array.shape[0] < minObjectsForConsensus:
411 self.log.debug(
"Current tol maxDist: %.4f arcsec" %
413 self.log.debug(
"Current shift: %.4f arcsec" %
420 for try_idx
in range(self.config.matcherIterations):
421 if try_idx == 0
and \
422 match_tolerance.lastMatchedPattern
is not None:
427 matcher_struct = pyPPMb.match(
428 source_array=src_array,
429 n_check=self.config.numPointsForShapeAttempt,
430 n_match=self.config.numPointsForShape,
432 max_n_patterns=self.config.numBrightStars,
433 max_shift=maxShiftArcseconds,
434 max_rotation=self.config.maxRotationDeg,
435 max_dist=maxMatchDistArcSec,
436 min_matches=minMatchedPairs,
437 pattern_skip_array=np.array(
438 match_tolerance.failedPatternList)
446 matcher_struct = pyPPMb.match(
447 source_array=src_array,
448 n_check=self.config.numPointsForShapeAttempt + 2 * try_idx,
449 n_match=self.config.numPointsForShape + try_idx,
450 n_agree=numConsensus,
451 max_n_patterns=self.config.numBrightStars,
452 max_shift=(self.config.maxOffsetPix *
453 wcs.pixelScale().asArcseconds()),
454 max_rotation=self.config.maxRotationDeg,
455 max_dist=maxMatchDistArcSec * 2. ** try_idx,
456 min_matches=minMatchedPairs,
457 pattern_skip_array=np.array(
458 match_tolerance.failedPatternList)
460 if try_idx == 0
and \
461 len(matcher_struct.match_ids) == 0
and \
462 match_tolerance.lastMatchedPattern
is not None:
469 match_tolerance.failedPatternList.append(
470 match_tolerance.lastMatchedPattern)
471 match_tolerance.lastMatchedPattern =
None 472 elif len(matcher_struct.match_ids) > 0:
475 match_tolerance.maxShift = afwgeom.Angle(matcher_struct.shift,
477 match_tolerance.lastMatchedPattern = \
478 matcher_struct.pattern_idx
485 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
494 values=distances_arcsec, n_sigma=2,
495 input_cut=np.max((10 * wcs.pixelScale().asArcseconds(),
497 maxShiftArcseconds)))
503 dist_cut_arcsec = np.max(
504 (2 * wcs.pixelScale().asArcseconds(),
505 clipped_dist_arcsec, maxMatchDistArcSec))
510 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
512 if dist_arcsec < dist_cut_arcsec:
513 match = afwTable.ReferenceMatch()
514 match.first = refCat[match_id_pair[1]]
515 match.second = sourceCat[match_id_pair[0]]
519 match.distance = match.first.getCoord().angularSeparation(
520 match.second.getCoord()).asArcseconds()
521 matches.append(match)
523 return pipeBase.Struct(
525 match_tolerance=match_tolerance,
528 def _sigma_clip(self, values, n_sigma, input_cut=None):
530 Find the distance cut by recursively cliping by n_sigma. 532 The algorithm converges once the length of the array on the 533 nth iteration is equal to the the length of the array on the 539 float array of values to caculate the cut on. 541 Number of sigma to use for the recersive clipping. 543 Optional starting cut for the clipping. 548 Value at which the clipping algorithm converged or the 549 value after a maximum of 100 iterations. 554 if input_cut
is None:
555 hold_cut = np.max(values)
559 for clip_idx
in range(100):
561 mask = (values < hold_cut)
562 total = len(values[mask])
565 if total == hold_total:
570 hold_cut = (np.mean(values[mask]) +
571 n_sigma * np.std(values[mask], ddof=1))
574 def _latlong_flux_to_xyz_mag(self, theta, phi, flux):
575 r"""Convert angles theta and phi plux a flux into unit sphere 576 x, y, z, and a relative magnitude. 578 Takes in a afw catalog objet and converts the catalog object RA, DECs 579 to points on the unit sphere. Also convets the flux into a simple, 580 non-zeropointed magnitude for relative sorting. 585 Angle from the north pole (z axis) of the sphere 587 Rotation around the sphere 592 Spherical unit vector x, y, z on the unitsphere. 594 output_array = np.empty(4, dtype=np.float64)
595 output_array[0] = np.sin(theta)*np.cos(phi)
596 output_array[1] = np.sin(theta)*np.sin(phi)
597 output_array[2] = np.cos(theta)
599 output_array[3] = -2.5 * np.log10(flux)
603 output_array[3] = 99.
607 def _get_pair_pattern_statistics(self, cat_array):
608 """ Compute the tolerances for the matcher automatically by comparing 609 pinwheel patterns as we would in the matcher. 611 We test how simplar the patterns we can create from a given set of 612 objects by computing the spoke lengths for each pattern and sorting 613 them from smallest to largest. The match tolerance is the average 614 distance per spoke between the closest two patterns in the sorted 619 cat_array : float array 620 array of 3 vectors representing the x, y, z position of catalog 621 objedts on the unit sphere. 626 Suggested max match tolerance distance calculated from comparisons 627 between pinwheel patterns used in optimistic/pessimsitic pattern 631 self.log.debug(
"Starting automated tolerance calculation...")
635 pattern_array = np.empty(
636 (cat_array.shape[0] - self.config.numPointsForShape,
637 self.config.numPointsForShape - 1))
638 flux_args_array = np.argsort(cat_array[:, -1])
641 tmp_sort_array = cat_array[flux_args_array]
644 for start_idx
in range(cat_array.shape[0] -
645 self.config.numPointsForShape):
646 pattern_points = tmp_sort_array[start_idx:start_idx +
647 self.config.numPointsForShape, :-1]
648 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
649 pattern_array[start_idx, :] = np.sqrt(
650 pattern_delta[:, 0] ** 2 +
651 pattern_delta[:, 1] ** 2 +
652 pattern_delta[:, 2] ** 2)
657 pattern_array[start_idx, :] = pattern_array[
658 start_idx, np.argsort(pattern_array[start_idx, :])]
664 pattern_array[:, :(self.config.numPointsForShape - 1)])
665 dist_nearest_array, ids = dist_tree.query(
666 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
667 dist_nearest_array = dist_nearest_array[:, 1]
668 dist_nearest_array.sort()
672 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600. /
673 (self.config.numPointsForShape - 1.))
675 self.log.debug(
"Automated tolerance")
676 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)