3 from scipy.spatial 
import cKDTree
 
    5 import lsst.pex.config 
as pexConfig
 
   10 from .matchOptimisticBTask 
import MatchTolerance
 
   12 from .pessimistic_pattern_matcher_b_3D 
import PessimisticPatternMatcherB
 
   14 __all__ = [
"MatchPessimisticBTask", 
"MatchPessimisticBConfig",
 
   15            "MatchTolerancePessimistic"]
 
   19     """Stores match tolerances for use in AstrometryTask and later 
   20     iterations of the matcher. 
   22     MatchPessimisticBTask relies on several state variables to be 
   23     preserved over different iterations in the 
   24     AstrometryTask.matchAndFitWcs loop of AstrometryTask. 
   28     maxMatchDist : `lsst.geom.Angle` 
   29         Maximum distance to consider a match from the previous match/fit 
   31     autoMaxMatchDist : `lsst.geom.Angle` 
   32         Automated estimation of the maxMatchDist from the sky statistics of the 
   33         source and reference catalogs. 
   34     maxShift : `lsst.geom.Angle` 
   35         Maximum shift found in the previous match/fit cycle. 
   36     lastMatchedPattern : `int` 
   37         Index of the last source pattern that was matched into the reference 
   39     failedPatternList : `list` of `int` 
   40         Previous matches were found to be false positives. 
   41     PPMbObj : `lsst.meas.astrom.PessimisticPatternMatcherB` 
   42         Initialized Pessimistic pattern matcher object. Storing this prevents 
   43         the need for recalculation of the searchable distances in the PPMB. 
   46     def __init__(self, maxMatchDist=None, autoMaxMatchDist=None,
 
   47                  maxShift=None, lastMatchedPattern=None,
 
   48                  failedPatternList=None, PPMbObj=None):
 
   54         if failedPatternList 
is None:
 
   61     """Configuration for MatchPessimisticBTask 
   63     numBrightStars = pexConfig.RangeField(
 
   64         doc=
"Number of bright stars to use. Sets the max number of patterns " 
   65             "that can be tested.",
 
   70     minMatchedPairs = pexConfig.RangeField(
 
   71         doc=
"Minimum number of matched pairs; see also minFracMatchedPairs.",
 
   76     minFracMatchedPairs = pexConfig.RangeField(
 
   77         doc=
"Minimum number of matched pairs as a fraction of the smaller of " 
   78             "the number of reference stars or the number of good sources; " 
   79             "the actual minimum is the smaller of this value or " 
   86     matcherIterations = pexConfig.RangeField(
 
   87         doc=
"Number of softening iterations in matcher.",
 
   92     maxOffsetPix = pexConfig.RangeField(
 
   93         doc=
"Maximum allowed shift of WCS, due to matching (pixel). " 
   94             "When changing this value, the " 
   95             "LoadReferenceObjectsConfig.pixelMargin should also be updated.",
 
  100     maxRotationDeg = pexConfig.RangeField(
 
  101         doc=
"Rotation angle allowed between sources and position reference " 
  102             "objects (degrees).",
 
  107     numPointsForShape = pexConfig.Field(
 
  108         doc=
"Number of points to define a shape for matching.",
 
  112     numPointsForShapeAttempt = pexConfig.Field(
 
  113         doc=
"Number of points to try for creating a shape. This value should " 
  114             "be greater than or equal to numPointsForShape. Besides " 
  115             "loosening the signal to noise cut in the 'matcher' SourceSelector, " 
  116             "increasing this number will solve CCDs where no match was found.",
 
  120     minMatchDistPixels = pexConfig.RangeField(
 
  121         doc=
"Distance in units of pixels to always consider a source-" 
  122             "reference pair a match. This prevents the astrometric fitter " 
  123             "from over-fitting and removing stars that should be matched and " 
  124             "allows for inclusion of new matches as the wcs improves.",
 
  130     numPatternConsensus = pexConfig.Field(
 
  131         doc=
"Number of implied shift/rotations from patterns that must agree " 
  132             "before it a given shift/rotation is accepted. This is only used " 
  133             "after the first softening iteration fails and if both the " 
  134             "number of reference and source objects is greater than " 
  139     numRefRequireConsensus = pexConfig.Field(
 
  140         doc=
"If the available reference objects exceeds this number, " 
  141             "consensus/pessimistic mode will enforced regardless of the " 
  142             "number of available sources. Below this optimistic mode (" 
  143             "exit at first match rather than requiring numPatternConsensus to " 
  144             "be matched) can be used. If more sources are required to match, " 
  145             "decrease the signal to noise cut in the sourceSelector.",
 
  149     maxRefObjects = pexConfig.RangeField(
 
  150         doc=
"Maximum number of reference objects to use for the matcher. The " 
  151             "absolute maximum allowed for is 2 ** 16 for memory reasons.",
 
  159         pexConfig.Config.validate(self)
 
  161             raise ValueError(
"numPointsForShapeAttempt must be greater than " 
  162                              "or equal to numPointsForShape.")
 
  164             raise ValueError(
"numBrightStars must be greater than " 
  165                              "numPointsForShape.")
 
  178     """Match sources to reference objects. 
  181     ConfigClass = MatchPessimisticBConfig
 
  182     _DefaultName = 
"matchObjectsToSources" 
  185         pipeBase.Task.__init__(self, **kwargs)
 
  189                               match_tolerance=None):
 
  190         """Match sources to position reference stars 
  192         refCat : `lsst.afw.table.SimpleCatalog` 
  193             catalog of reference objects that overlap the exposure; reads 
  197             - the specified flux field 
  199         sourceCat : `lsst.afw.table.SourceCatalog` 
  200             Catalog of sources found on an exposure.  This should already be 
  201             down-selected to "good"/"usable" sources in the calling Task. 
  202         wcs : `lsst.afw.geom.SkyWcs` 
  204         sourceFluxField: `str` 
  205             field of sourceCat to use for flux 
  207             field of refCat to use for flux 
  208         match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic` 
  209             is a MatchTolerance class object or `None`. This this class is used 
  210             to communicate state between AstrometryTask and MatcherTask. 
  211             AstrometryTask will also set the MatchTolerance class variable 
  212             maxMatchDist based on the scatter AstrometryTask has found after 
  217         result : `lsst.pipe.base.Struct` 
  218             Result struct with components: 
  220             - ``matches`` : source to reference matches found (`list` of 
  221               `lsst.afw.table.ReferenceMatch`) 
  222             - ``usableSourceCat`` : a catalog of sources potentially usable for 
  223               matching and WCS fitting (`lsst.afw.table.SourceCatalog`). 
  224             - ``match_tolerance`` : a MatchTolerance object containing the 
  225               resulting state variables from the match 
  226               (`lsst.meas.astrom.MatchTolerancePessimistic`). 
  233         if match_tolerance 
is None:
 
  238         goodSourceCat = sourceCat
 
  240         numUsableSources = len(goodSourceCat)
 
  242         if len(goodSourceCat) == 0:
 
  243             raise pipeBase.TaskError(
"No sources are good")
 
  245         minMatchedPairs = 
min(self.config.minMatchedPairs,
 
  246                               int(self.config.minFracMatchedPairs
 
  247                                   * 
min([len(refCat), len(goodSourceCat)])))
 
  249         if len(refCat) > self.config.maxRefObjects:
 
  251                 "WARNING: Reference catalog larger that maximum allowed. " 
  252                 "Trimming to %i" % self.config.maxRefObjects)
 
  255             trimmedRefCat = refCat
 
  258             refCat=trimmedRefCat,
 
  259             sourceCat=goodSourceCat,
 
  261             refFluxField=refFluxField,
 
  262             numUsableSources=numUsableSources,
 
  263             minMatchedPairs=minMatchedPairs,
 
  264             match_tolerance=match_tolerance,
 
  265             sourceFluxField=sourceFluxField,
 
  266             verbose=debug.verbose,
 
  268         matches = doMatchReturn.matches
 
  269         match_tolerance = doMatchReturn.match_tolerance
 
  271         if len(matches) == 0:
 
  272             raise RuntimeError(
"Unable to match sources")
 
  274         self.log.
info(
"Matched %d sources" % len(matches))
 
  275         if len(matches) < minMatchedPairs:
 
  276             self.log.
warn(
"Number of matches is smaller than request")
 
  278         return pipeBase.Struct(
 
  280             usableSourceCat=goodSourceCat,
 
  281             match_tolerance=match_tolerance,
 
  284     def _filterRefCat(self, refCat, refFluxField):
 
  285         """Sub-select a number of reference objects starting from the brightest 
  286         and maxing out at the number specified by maxRefObjects in the config. 
  288         No trimming is done if len(refCat) > config.maxRefObjects. 
  292         refCat : `lsst.afw.table.SimpleCatalog` 
  293             Catalog of reference objects to trim. 
  295             field of refCat to use for flux 
  298         outCat : `lsst.afw.table.SimpleCatalog` 
  299             Catalog trimmed to the number set in the task config from the 
  303         if len(refCat) <= self.config.maxRefObjects:
 
  305         fluxArray = refCat.get(refFluxField)
 
  306         sortedFluxArray = fluxArray[fluxArray.argsort()]
 
  307         minFlux = sortedFluxArray[-(self.config.maxRefObjects + 1)]
 
  309         selected = (refCat.get(refFluxField) > minFlux)
 
  312         outCat.reserve(self.config.maxRefObjects)
 
  313         outCat.extend(refCat[selected])
 
  318     def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
 
  319                  minMatchedPairs, match_tolerance, sourceFluxField, verbose):
 
  320         """Implementation of matching sources to position reference objects 
  322         Unlike matchObjectsToSources, this method does not check if the sources 
  327         refCat : `lsst.afw.table.SimpleCatalog` 
  328             catalog of position reference objects that overlap an exposure 
  329         sourceCat : `lsst.afw.table.SourceCatalog` 
  330             catalog of sources found on the exposure 
  331         wcs : `lsst.afw.geom.SkyWcs` 
  332             estimated WCS of exposure 
  334             field of refCat to use for flux 
  335         numUsableSources : `int` 
  336             number of usable sources (sources with known centroid that are not 
  337             near the edge, but may be saturated) 
  338         minMatchedPairs : `int` 
  339             minimum number of matches 
  340         match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic` 
  341             a MatchTolerance object containing variables specifying matcher 
  342             tolerances and state from possible previous runs. 
  343         sourceFluxField : `str` 
  344             Name of the flux field in the source catalog. 
  346             Set true to print diagnostic information to std::cout 
  351             Results struct with components: 
  353             - ``matches`` : a list the matches found 
  354               (`list` of `lsst.afw.table.ReferenceMatch`). 
  355             - ``match_tolerance`` : MatchTolerance containing updated values from 
  356               this fit iteration (`lsst.meas.astrom.MatchTolerancePessimistic`) 
  365         src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
 
  366         for src_idx, srcObj 
in enumerate(sourceCat):
 
  367             coord = wcs.pixelToSky(srcObj.getCentroid())
 
  368             theta = np.pi / 2 - coord.getLatitude().asRadians()
 
  369             phi = coord.getLongitude().asRadians()
 
  370             flux = srcObj[sourceFluxField]
 
  371             src_array[src_idx, :] = \
 
  374         if match_tolerance.PPMbObj 
is None or \
 
  375            match_tolerance.autoMaxMatchDist 
is None:
 
  379             ref_array = np.empty((len(refCat), 4), dtype=np.float64)
 
  380             for ref_idx, refObj 
in enumerate(refCat):
 
  381                 theta = np.pi / 2 - refObj.getDec().asRadians()
 
  382                 phi = refObj.getRa().asRadians()
 
  383                 flux = refObj[refFluxField]
 
  384                 ref_array[ref_idx, :] = \
 
  388                 ref_array[:, :3], self.log)
 
  389             self.log.
debug(
"Computing source statistics...")
 
  392             self.log.
debug(
"Computing reference statistics...")
 
  395             maxMatchDistArcSec = np.max((
 
  396                 self.config.minMatchDistPixels
 
  397                 * wcs.getPixelScale().asArcseconds(),
 
  398                 np.min((maxMatchDistArcSecSrc,
 
  399                         maxMatchDistArcSecRef))))
 
  400             match_tolerance.autoMaxMatchDist = 
geom.Angle(
 
  401                 maxMatchDistArcSec, geom.arcseconds)
 
  405         if match_tolerance.maxShift 
is None:
 
  406             maxShiftArcseconds = (self.config.maxOffsetPix
 
  407                                   * wcs.getPixelScale().asArcseconds())
 
  411             maxShiftArcseconds = np.max(
 
  412                 (match_tolerance.maxShift.asArcseconds(),
 
  413                  self.config.minMatchDistPixels
 
  414                  * wcs.getPixelScale().asArcseconds()))
 
  420         if match_tolerance.maxMatchDist 
is None:
 
  421             match_tolerance.maxMatchDist = match_tolerance.autoMaxMatchDist
 
  423             maxMatchDistArcSec = np.max(
 
  424                 (self.config.minMatchDistPixels
 
  425                  * wcs.getPixelScale().asArcseconds(),
 
  426                  np.min((match_tolerance.maxMatchDist.asArcseconds(),
 
  427                          match_tolerance.autoMaxMatchDist.asArcseconds()))))
 
  433         numConsensus = self.config.numPatternConsensus
 
  434         if len(refCat) < self.config.numRefRequireConsensus:
 
  435             minObjectsForConsensus = \
 
  436                 self.config.numBrightStars + \
 
  437                 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             if soften_dist == 0 
and \
 
  451                match_tolerance.lastMatchedPattern 
is not None:
 
  460                 run_n_consent = numConsensus
 
  463             matcher_struct = match_tolerance.PPMbObj.match(
 
  464                 source_array=src_array,
 
  465                 n_check=self.config.numPointsForShapeAttempt,
 
  466                 n_match=self.config.numPointsForShape,
 
  467                 n_agree=run_n_consent,
 
  468                 max_n_patterns=self.config.numBrightStars,
 
  469                 max_shift=maxShiftArcseconds,
 
  470                 max_rotation=self.config.maxRotationDeg,
 
  471                 max_dist=maxMatchDistArcSec * 2. ** soften_dist,
 
  472                 min_matches=minMatchedPairs,
 
  473                 pattern_skip_array=np.array(
 
  474                     match_tolerance.failedPatternList)
 
  477             if soften_dist == 0 
and \
 
  478                len(matcher_struct.match_ids) == 0 
and \
 
  479                match_tolerance.lastMatchedPattern 
is not None:
 
  486                 match_tolerance.failedPatternList.append(
 
  487                     match_tolerance.lastMatchedPattern)
 
  488                 match_tolerance.lastMatchedPattern = 
None 
  489                 maxShiftArcseconds = \
 
  490                     self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds()
 
  491             elif len(matcher_struct.match_ids) > 0:
 
  494                 match_tolerance.maxShift = \
 
  495                     matcher_struct.shift * geom.arcseconds
 
  496                 match_tolerance.lastMatchedPattern = \
 
  497                     matcher_struct.pattern_idx
 
  503             return pipeBase.Struct(
 
  505                 match_tolerance=match_tolerance,
 
  517         distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
 
  518         dist_cut_arcsec = np.max(
 
  519             (np.degrees(matcher_struct.max_dist_rad) * 3600,
 
  520              self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds()))
 
  525         for match_id_pair, dist_arcsec 
in zip(matcher_struct.match_ids,
 
  527             if dist_arcsec < dist_cut_arcsec:
 
  529                 match.first = refCat[int(match_id_pair[1])]
 
  530                 match.second = sourceCat[int(match_id_pair[0])]
 
  534                 match.distance = match.first.getCoord().separation(
 
  535                     match.second.getCoord()).asArcseconds()
 
  536                 matches.append(match)
 
  538         return pipeBase.Struct(
 
  540             match_tolerance=match_tolerance,
 
  543     def _latlong_flux_to_xyz_mag(self, theta, phi, flux):
 
  544         """Convert angles theta and phi and a flux into unit sphere 
  545         x, y, z, and a relative magnitude. 
  547         Takes in a afw catalog object and converts the catalog object RA, DECs 
  548         to points on the unit sphere. Also converts the flux into a simple, 
  549         non-zero-pointed magnitude for relative sorting. 
  554             Angle from the north pole (z axis) of the sphere 
  556             Rotation around the sphere 
  560         output_array : `numpy.ndarray`, (N, 4) 
  561             Spherical unit vector x, y, z  with flux. 
  563         output_array = np.empty(4, dtype=np.float64)
 
  564         output_array[0] = np.sin(theta)*np.cos(phi)
 
  565         output_array[1] = np.sin(theta)*np.sin(phi)
 
  566         output_array[2] = np.cos(theta)
 
  568             output_array[3] = -2.5 * np.log10(flux)
 
  572             output_array[3] = 99.
 
  576     def _get_pair_pattern_statistics(self, cat_array):
 
  577         """ Compute the tolerances for the matcher automatically by comparing 
  578         pinwheel patterns as we would in the matcher. 
  580         We test how similar the patterns we can create from a given set of 
  581         objects by computing the spoke lengths for each pattern and sorting 
  582         them from smallest to largest. The match tolerance is the average 
  583         distance per spoke between the closest two patterns in the sorted 
  588         cat_array : `numpy.ndarray`, (N, 3) 
  589             array of 3 vectors representing the x, y, z position of catalog 
  590             objects on the unit sphere. 
  595             Suggested max match tolerance distance calculated from comparisons 
  596             between pinwheel patterns used in optimistic/pessimistic pattern 
  600         self.log.
debug(
"Starting automated tolerance calculation...")
 
  604         pattern_array = np.empty(
 
  605             (cat_array.shape[0] - self.config.numPointsForShape,
 
  606              self.config.numPointsForShape - 1))
 
  607         flux_args_array = np.argsort(cat_array[:, -1])
 
  610         tmp_sort_array = cat_array[flux_args_array]
 
  613         for start_idx 
in range(cat_array.shape[0]
 
  614                                - self.config.numPointsForShape):
 
  615             pattern_points = tmp_sort_array[start_idx:start_idx
 
  616                                             + self.config.numPointsForShape, :-1]
 
  617             pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
 
  618             pattern_array[start_idx, :] = np.sqrt(
 
  619                 pattern_delta[:, 0] ** 2
 
  620                 + pattern_delta[:, 1] ** 2
 
  621                 + pattern_delta[:, 2] ** 2)
 
  626             pattern_array[start_idx, :] = pattern_array[
 
  627                 start_idx, np.argsort(pattern_array[start_idx, :])]
 
  633             pattern_array[:, :(self.config.numPointsForShape - 1)])
 
  634         dist_nearest_array, ids = dist_tree.query(
 
  635             pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
 
  636         dist_nearest_array = dist_nearest_array[:, 1]
 
  637         dist_nearest_array.sort()
 
  641         dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600.
 
  642                     / (self.config.numPointsForShape - 1.))
 
  644         self.log.
debug(
"Automated tolerance")
 
  645         self.log.
debug(
"\tdistance/match tol: %.4f [arcsec]" % dist_tol)