3 from scipy.spatial
import cKDTree
4 from scipy.stats
import sigmaclip
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 the magnitude sorted source array 54 where 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 " 104 "LoadReferenceObjectsConfig.pixelMargin should also be updated.",
109 maxRotationDeg = pexConfig.RangeField(
110 doc=
"Rotation angle allowed between sources and position reference " 111 "objects (degrees).",
116 numPointsForShape = pexConfig.Field(
117 doc=
"Number of points to define a shape for matching.",
121 numPointsForShapeAttempt = pexConfig.Field(
122 doc=
"Number of points to try for creating a shape. This value should " 123 "be greater than or equal to numPointsForShape. Besides " 124 "loosening the signal to noise cut in the matcherSourceSelector, " 125 "increasing this number will solve CCDs where no match was found.",
129 minMatchDistPixels = pexConfig.RangeField(
130 doc=
"Distance in units of pixels to always consider a source-" 131 "reference pair a match. This prevents the astrometric fitter " 132 "from over-fitting and removing stars that should be matched and " 133 "allows for inclusion of new matches as the wcs improves.",
139 numPatternConsensus = pexConfig.Field(
140 doc=
"Number of implied shift/rotations from patterns that must agree " 141 "before it a given shift/rotation is accepted. This is only used " 142 "after the first softening iteration fails and if both the " 143 "number of reference and source objects is greater than " 148 sourceSelector = sourceSelectorRegistry.makeField(
149 doc=
"How to select sources for cross-matching. The default " 150 "matcherSourceSelector removes objects with low S/N, bad " 151 "saturated objects, edge objects, and interpolated objects.",
152 default=
"matcherPessimistic" 157 sourceSelector.setDefaults()
160 pexConfig.Config.validate(self)
162 raise ValueError(
"numPointsForShapeAttempt must be greater than " 163 "or equal to numPointsForShape.")
176 r"""!Match sources to reference objects 178 @anchor MatchPessimisticBTask_ 180 @section meas_astrom_MatchPessimisticB_Contents Contents 182 - @ref meas_astrom_MatchPessimisticB_Purpose 183 - @ref meas_astrom_MatchPessimisticB_Initialize 184 - @ref meas_astrom_MatchPessimisticB_IO 185 - @ref meas_astrom_MatchPessimisticB_Config 186 - @ref meas_astrom_MatchPessimisticB_Example 187 - @ref meas_astrom_MatchPessimisticB_Debug 189 @section meas_astrom_MatchPessimisticB_Purpose Description 191 Match sources to reference objects. This is often done as a preliminary 192 step to fitting an astrometric or photometric solution. For details about 193 the matching algorithm see pessimistic_pattern_matcher_b_3D.py 195 @section meas_astrom_MatchPessimisticB_Initialize Task initialization 197 @copydoc \_\_init\_\_ 199 @section meas_astrom_MatchPessimisticB_IO Invoking the Task 201 @copydoc matchObjectsToSources 203 @section meas_astrom_MatchPessimisticB_Config Configuration 206 See @ref MatchPessimisticBConfig 208 To modify the tests for good sources for matching, create a new 209 sourceSelector class in meas_algorithms and use it in the config. 211 @section meas_astrom_MatchPessimisticB_Example A complete example of 212 using MatchPessimisticBTask 214 MatchPessimisticBTask is a subtask of AstrometryTask, which is called by 215 PhotoCalTask. See \ref meas_photocal_photocal_Example. 217 @section meas_astrom_MatchPessimisticB_Debug Debug variables 219 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink 220 interface supports a flag @c -d to import @b debug.py from your 221 @c PYTHONPATH; see @ref baseDebug for more about @b debug.py files. 223 The available variables in MatchPessimisticBTask are: 225 <DT> @c verbose (bool) 226 <DD> If True then the matcher prints debug messages to stdout 229 To investigate the @ref meas_astrom_MatchPessimisticB_Debug, put something 234 # N.b. lsstDebug.Info(name) would call us recursively 235 debug = lsstDebug.getInfo(name) 236 if name == "lsst.pipe.tasks.astrometry": 241 lsstDebug.Info = DebugInfo 243 into your debug.py file and run this task with the @c --debug flag. 246 ConfigClass = MatchPessimisticBConfig
247 _DefaultName =
"matchObjectsToSources" 250 pipeBase.Task.__init__(self, **kwargs)
251 self.makeSubtask(
"sourceSelector")
255 match_tolerance=None):
256 """!Match sources to position reference stars 258 @param[in] refCat catalog of reference objects that overlap the 259 exposure; reads fields for: 261 - the specified flux field 262 @param[in] sourceCat catalog of sources found on an exposure; 263 Please check the required fields of your specified source selector 264 that the correct flags are present. 265 @param[in] wcs estimated WCS 266 @param[in] refFluxField field of refCat to use for flux 267 @param[in] match_tolerance is a MatchTolerance class object or None. 268 This this class is used to communicate state between AstrometryTask 269 and MatcherTask. AstrometryTask will also set the MatchTolerance 270 class variable maxMatchDist based on the scatter AstrometryTask has 271 found after fitting for the wcs. 272 @return an lsst.pipe.base.Struct with fields: 273 - matches a list of matches, each instance of 274 lsst.afw.table.ReferenceMatch 275 - usableSourcCat a catalog of sources potentially usable for 277 - match_tolerance a MatchTolerance object containing the resulting 278 state variables from the match. 285 if match_tolerance
is None:
289 numSources = len(sourceCat)
290 selectedSources = self.sourceSelector.run(sourceCat)
291 goodSourceCat = selectedSources.sourceCat
292 numUsableSources = len(goodSourceCat)
293 self.log.info(
"Purged %d sources, leaving %d good sources" %
294 (numSources - numUsableSources, numUsableSources))
296 if len(goodSourceCat) == 0:
297 raise pipeBase.TaskError(
"No sources are good")
302 minMatchedPairs = min(self.config.minMatchedPairs,
303 int(self.config.minFracMatchedPairs *
304 min([len(refCat), len(goodSourceCat)])))
308 sourceCat=goodSourceCat,
310 refFluxField=refFluxField,
311 numUsableSources=numUsableSources,
312 minMatchedPairs=minMatchedPairs,
313 match_tolerance=match_tolerance,
314 sourceFluxField=self.sourceSelector.fluxField,
315 verbose=debug.verbose,
317 matches = doMatchReturn.matches
318 match_tolerance = doMatchReturn.match_tolerance
320 if len(matches) == 0:
321 raise RuntimeError(
"Unable to match sources")
323 self.log.info(
"Matched %d sources" % len(matches))
324 if len(matches) < minMatchedPairs:
325 self.log.warn(
"Number of matches is smaller than request")
327 return pipeBase.Struct(
329 usableSourceCat=goodSourceCat,
330 match_tolerance=match_tolerance,
334 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
335 minMatchedPairs, match_tolerance, sourceFluxField, verbose):
336 """!Implementation of matching sources to position reference stars 338 Unlike matchObjectsToSources, this method does not check if the sources 341 @param[in] refCat catalog of position reference stars that overlap an 343 @param[in] sourceCat catalog of sources found on the exposure 344 @param[in] wcs estimated WCS of exposure 345 @param[in] refFluxField field of refCat to use for flux 346 @param[in] numUsableSources number of usable sources (sources with 347 known centroid that are not near the edge, but may be saturated) 348 @param[in] minMatchedPairs minimum number of matches 349 @param[in] match_tolerance a MatchTolerance object containing 350 variables specifying matcher tolerances and state from possible 352 @param[in] sourceInfo SourceInfo for the sourceCat 353 @param[in] verbose true to print diagnostic information to std::cout 355 @return a list of matches, an instance of 356 lsst.afw.table.ReferenceMatch, a MatchTolerance object 365 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
366 for ref_idx, refObj
in enumerate(refCat):
367 theta = np.pi / 2 - refObj.getDec().asRadians()
368 phi = refObj.getRa().asRadians()
369 flux = refObj[refFluxField]
370 ref_array[ref_idx, :] = \
373 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
374 for src_idx, srcObj
in enumerate(sourceCat):
375 coord = wcs.pixelToSky(srcObj.getCentroid())
376 theta = np.pi / 2 - coord.getLatitude().asRadians()
377 phi = coord.getLongitude().asRadians()
378 flux = srcObj.getPsfInstFlux()
379 src_array[src_idx, :] = \
384 if match_tolerance.maxShift
is None:
385 maxShiftArcseconds = (self.config.maxOffsetPix *
386 wcs.getPixelScale().asArcseconds())
390 maxShiftArcseconds = np.max(
391 (match_tolerance.maxShift.asArcseconds(),
392 self.config.minMatchDistPixels *
393 wcs.getPixelScale().asArcseconds()))
399 if match_tolerance.maxMatchDist
is None:
400 self.log.debug(
"Computing source statistics...")
403 self.log.debug(
"Computing reference statistics...")
406 maxMatchDistArcSec = np.min((maxMatchDistArcSecSrc,
407 maxMatchDistArcSecRef))
408 match_tolerance.autoMaxDist = afwgeom.Angle(maxMatchDistArcSec,
411 maxMatchDistArcSec = np.max(
412 (self.config.minMatchDistPixels *
413 wcs.getPixelScale().asArcseconds(),
414 np.min((match_tolerance.maxMatchDist.asArcseconds(),
415 match_tolerance.autoMaxDist.asArcseconds()))))
420 numConsensus = self.config.numPatternConsensus
421 minObjectsForConsensus = \
422 self.config.numBrightStars + self.config.numPointsForShapeAttempt
423 if ref_array.shape[0] < minObjectsForConsensus
or \
424 src_array.shape[0] < minObjectsForConsensus:
427 self.log.debug(
"Current tol maxDist: %.4f arcsec" %
429 self.log.debug(
"Current shift: %.4f arcsec" %
437 for soften_pattern
in range(self.config.matcherIterations):
438 for soften_dist
in range(self.config.matcherIterations):
439 if soften_pattern == 0
and soften_dist == 0
and \
440 match_tolerance.lastMatchedPattern
is not None:
449 run_n_consent = numConsensus
452 matcher_struct = pyPPMb.match(
453 source_array=src_array,
454 n_check=self.config.numPointsForShapeAttempt + soften_pattern,
455 n_match=self.config.numPointsForShape,
456 n_agree=run_n_consent,
457 max_n_patterns=self.config.numBrightStars,
458 max_shift=maxShiftArcseconds,
459 max_rotation=self.config.maxRotationDeg,
460 max_dist=maxMatchDistArcSec * 2. ** soften_dist,
461 min_matches=minMatchedPairs,
462 pattern_skip_array=np.array(
463 match_tolerance.failedPatternList)
466 if soften_pattern == 0
and soften_dist == 0
and \
467 len(matcher_struct.match_ids) == 0
and \
468 match_tolerance.lastMatchedPattern
is not None:
475 match_tolerance.failedPatternList.append(
476 match_tolerance.lastMatchedPattern)
477 match_tolerance.lastMatchedPattern =
None 478 maxShiftArcseconds = \
479 self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds()
480 elif len(matcher_struct.match_ids) > 0:
483 match_tolerance.maxShift = \
484 matcher_struct.shift * afwgeom.arcseconds
485 match_tolerance.lastMatchedPattern = \
486 matcher_struct.pattern_idx
494 return pipeBase.Struct(
496 match_tolerance=match_tolerance,
508 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
509 clip_max_dist = np.max(
510 (sigmaclip(distances_arcsec, low=100, high=2)[-1],
511 self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds())
516 if not np.isfinite(clip_max_dist):
517 clip_max_dist = maxMatchDistArcSec
519 if clip_max_dist < maxMatchDistArcSec
and \
520 len(distances_arcsec[distances_arcsec < clip_max_dist]) < \
522 dist_cut_arcsec = maxMatchDistArcSec
524 dist_cut_arcsec = np.min((clip_max_dist, maxMatchDistArcSec))
529 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
531 if dist_arcsec < dist_cut_arcsec:
533 match.first = refCat[match_id_pair[1]]
534 match.second = sourceCat[match_id_pair[0]]
538 match.distance = match.first.getCoord().separation(
539 match.second.getCoord()).asArcseconds()
540 matches.append(match)
542 return pipeBase.Struct(
544 match_tolerance=match_tolerance,
547 def _latlong_flux_to_xyz_mag(self, theta, phi, flux):
548 r"""Convert angles theta and phi and a flux into unit sphere 549 x, y, z, and a relative magnitude. 551 Takes in a afw catalog object and converts the catalog object RA, DECs 552 to points on the unit sphere. Also converts the flux into a simple, 553 non-zero-pointed magnitude for relative sorting. 558 Angle from the north pole (z axis) of the sphere 560 Rotation around the sphere 565 Spherical unit vector x, y, z on the unit-sphere. 567 output_array = np.empty(4, dtype=np.float64)
568 output_array[0] = np.sin(theta)*np.cos(phi)
569 output_array[1] = np.sin(theta)*np.sin(phi)
570 output_array[2] = np.cos(theta)
572 output_array[3] = -2.5 * np.log10(flux)
576 output_array[3] = 99.
580 def _get_pair_pattern_statistics(self, cat_array):
581 """ Compute the tolerances for the matcher automatically by comparing 582 pinwheel patterns as we would in the matcher. 584 We test how similar the patterns we can create from a given set of 585 objects by computing the spoke lengths for each pattern and sorting 586 them from smallest to largest. The match tolerance is the average 587 distance per spoke between the closest two patterns in the sorted 592 cat_array : float array 593 array of 3 vectors representing the x, y, z position of catalog 594 objects on the unit sphere. 599 Suggested max match tolerance distance calculated from comparisons 600 between pinwheel patterns used in optimistic/pessimistic pattern 604 self.log.debug(
"Starting automated tolerance calculation...")
608 pattern_array = np.empty(
609 (cat_array.shape[0] - self.config.numPointsForShape,
610 self.config.numPointsForShape - 1))
611 flux_args_array = np.argsort(cat_array[:, -1])
614 tmp_sort_array = cat_array[flux_args_array]
617 for start_idx
in range(cat_array.shape[0] -
618 self.config.numPointsForShape):
619 pattern_points = tmp_sort_array[start_idx:start_idx +
620 self.config.numPointsForShape, :-1]
621 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
622 pattern_array[start_idx, :] = np.sqrt(
623 pattern_delta[:, 0] ** 2 +
624 pattern_delta[:, 1] ** 2 +
625 pattern_delta[:, 2] ** 2)
630 pattern_array[start_idx, :] = pattern_array[
631 start_idx, np.argsort(pattern_array[start_idx, :])]
637 pattern_array[:, :(self.config.numPointsForShape - 1)])
638 dist_nearest_array, ids = dist_tree.query(
639 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
640 dist_nearest_array = dist_nearest_array[:, 1]
641 dist_nearest_array.sort()
645 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600. /
646 (self.config.numPointsForShape - 1.))
648 self.log.debug(
"Automated tolerance")
649 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 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)