Package mvpa :: Package mappers :: Module metric

[hide private]
[frames] | no frames]

# Source Code for Module mvpa.mappers.metric

      1  # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
      2  # vi: set ft=python sts=4 ts=4 sw=4 et:
      3  ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
      4  #
      5  #   See COPYING file distributed along with the PyMVPA package for the
      6  #   copyright and license terms.
      7  #
      8  ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
      9  """Classes and functions to provide sense of distances between sample points"""
     10
     11  __docformat__ = 'restructuredtext'
     12
     13  import numpy as N
     14
     15  from mvpa.clfs.distance import cartesianDistance
     16
     17 -class Metric(object):
     18      """Abstract class for any metric.
     19
     20      Subclasses abstract a metric of a dataspace with certain properties and can
     21      be queried for structural information. Currently, this is limited to
     22      neighborhood information, i.e. identifying the surround a some coordinate in
     23      the respective dataspace.
     24
     25      At least one of the methods (getNeighbors, getNeighbor) has to be overriden
     26      in every derived class.  NOTE: derived #2 from derived class #1 has to
     27      override all methods which were overrident in class #1
     28      """
     29
     30 -    def getNeighbors(self, *args, **kwargs):
     31          """Return the list of coordinates for the neighbors.
     32
     33          By default it simply constracts the list based on
     34          the generator getNeighbor
     35          """
     36          return [ x for x in self.getNeighbor(*args, **kwargs) ]
     37
     38
     39 -    def getNeighbor(self, *args, **kwargs):
     40          """Generator to return coordinate of the neighbor.
     41
     42          Base class contains the simplest implementation, assuming that
     43          getNeighbors returns iterative structure to spit out neighbors
     44          1-by-1
     45          """
     46          for neighbor in self.getNeighbors(*args, **kwargs):
     47              yield neighbor
     48
     49
     50
     51 -class DescreteMetric(Metric):
     52      """Find neighboring points in descretized space
     53
     54      If input space is descretized and all points fill in N-dimensional cube,
     55      this finder returns list of neighboring points for a given distance.
     56
     57      For all `origin` coordinates this class exclusively operates on discretized
     58      values, not absolute coordinates (which are e.g. in mm).
     59
     60      Additionally, this metric has the notion of compatible and incompatible
     61      dataspace metrics, i.e. the descrete space might contain dimensions for
     62      which computing an overall distance is not meaningful. This could, for
     63      example, be a combined spatio-temporal space (three spatial dimension,
     64      plus the temporal one). This metric allows to define a boolean mask
     65      (`compatmask`) which dimensions share the same dataspace metrics and for
     66      which the distance function should be evaluated. If a `compatmask` is
     67      provided, all cordinates are projected into the subspace of the non-zero
     68      dimensions and distances are computed within that space.
     69
     70      However, by using a per dimension radius argument for the getNeighbor
     71      methods, it is nevertheless possible to define a neighborhood along all
     72      dimension. For all non-compatible axes the respective radius is treated
     73      as a one-dimensional distance along the respective axis.
     74      """
     75
     76 -    def __init__(self, elementsize=1, distance_function=cartesianDistance,
     77                   compatmask=None):
     78          """
     79          :Parameters:
     80            elementsize: float | sequence
     81              The extent of a dataspace element along all dimensions.
     82            distance_function: functor
     83              The distance measure used to determine distances between
     84              dataspace elements.
     85            compatmask: 1D bool array | None
     86              A mask where all non-zero elements indicate dimensions
     87              with compatible spacemetrics. If None (default) all dimensions
     88              are assumed to have compatible spacemetrics.
     89          """
     90          Metric.__init__(self)
     91          self.__filter_radius = None
     92          self.__filter_coord = None
     93          self.__distance_function = distance_function
     94          self.__elementsize = N.array(elementsize, ndmin=1)
     95          self.__Ndims = len(self.__elementsize)
     96          self.compatmask = compatmask
     97
     98
     99 -    def _expandRadius(self, radius):
    100          # expand radius to be equal along all dimensions if just scalar
    101          # is provided
    102          if N.isscalar(radius):
    103              radius = N.array([radius] * len(self.__elementsize), dtype='float')
    104          else:
    105              radius = N.array(radius, dtype='float')
    106
    107          return radius
    108
    109
    110 -    def _computeFilter(self, radius):
    111          """ (Re)compute filter_coord based on given radius
    112          """
    113          if not N.all(radius[self.__compatmask][0] == radius[self.__compatmask]):
    114              raise ValueError, \
    115                    "Currently only neighborhood spheres are supported, " \
    116                    "not ellipsoids."
    117          # store radius in compatible space
    118          compat_radius = radius[self.__compatmask][0]
    119          # compute radius in units of elementsize per axis
    120          elementradius_per_axis = radius / self.__elementsize
    121
    122          # build prototype search space
    123          filter_radiuses = N.ceil(N.abs(elementradius_per_axis)).astype('int')
    124          filter_center = filter_radiuses
    125          comp_center = filter_center[self.__compatmask] \
    126                              * self.__elementsize[self.__compatmask]
    127          filter_mask = N.ones((filter_radiuses * 2) + 1, dtype='bool')
    128
    129          # get coordinates of all elements
    130          f_coords = N.transpose(filter_mask.nonzero())
    131
    132          # but start with empty mask
    133          filter_mask[:] = False
    134
    135          # check all filter element
    136          for coord in f_coords:
    137              dist = self.__distance_function(
    138                          coord[self.__compatmask]
    139                              * self.__elementsize[self.__compatmask],
    140                          comp_center)
    141              # compare with radius
    142              if dist <= compat_radius:
    143                  # zero too distant
    144                  filter_mask[N.array(coord, ndmin=2).T.tolist()] = True
    145
    146
    147          self.__filter_coord = N.array( filter_mask.nonzero() ).T \
    148                                          - filter_center
    149          self.__filter_radius = radius
    150
    151
    152 -    def getNeighbors(self, origin, radius=0):
    153          """Returns coordinates of the neighbors which are within
    154          distance from coord.
    155
    156          :Parameters:
    157            origin: 1D array
    158              The center coordinate of the neighborhood.
    159            radius: scalar | sequence
    160              If a scalar, the radius is treated as identical along all dimensions
    161              of the dataspace. If a sequence, it defines a per dimension radius,
    162              thus has to have the same number of elements as dimensions.
    163              Currently, only spherical neighborhoods are supported. Therefore,
    164              the radius has to be equal along all dimensions flagged as having
    165              compatible dataspace metrics. It is, however, possible to define
    166              variant radii for all other dimensions.
    167          """
    168          if len(origin) != self.__Ndims:
    169              raise ValueError("Obtained coordinates [%r] which have different "
    170                               "number of dimensions (%d) from known "
    171                               "elementsize" % (origin, self.__Ndims))
    172
    173          # take care of postprocessing the radius the ensure validity of the next
    174          # conditional
    175          radius = self._expandRadius(radius)
    176          if N.any(radius != self.__filter_radius):
    177              self._computeFilter(radius)
    178
    179          # for the ease of future references, it is better to transform
    180          # coordinates into tuples
    181          return origin + self.__filter_coord
    182
    183
    184 -    def _setFilter(self, filter_coord):
    185          """Lets allow to specify some custom filter to use
    186          """
    187          self.__filter_coord = filter_coord
    188
    189
    190 -    def _getFilter(self):
    191          """Lets allow to specify some custom filter to use
    192          """
    193          return self.__filter_coord
    194
    195 -    def _setElementSize(self, v):
    196          # reset filter radius
    197          _elementsize = N.array(v, ndmin=1)
    198          # assure that it is read-only and it gets reassigned
    199          # only as a whole to trigger this house-keeping
    200          _elementsize.flags.writeable = False
    201          self.__elementsize = _elementsize
    202          self.__Ndims = len(_elementsize)
    203          self.__filter_radius = None
    204
    205
    206 -    def _getCompatMask(self):
    207          """Return compatmask
    208
    209          .. note::
    210            Don't modify in place since it would need to require to reset
    211            __filter_radius whenever changed
    212          """
    213          return self.__compatmask
    214
    215 -    def _setCompatMask(self, compatmask):
    216          """Set new value of compatmask
    217          """
    218          if compatmask is None:
    219              self.__compatmask = N.ones(self.__elementsize.shape, dtype='bool')
    220          else:
    221              self.__compatmask = N.array(compatmask, dtype='bool')
    222              if not self.__elementsize.shape == self.__compatmask.shape:
    223                  raise ValueError, '`compatmask` is of incompatible shape ' \
    224                          '(need %s, got %s)' % (`self.__elementsize.shape`,
    225                                                 `self.__compatmask.shape`)
    226          self.__filter_radius = None     # reset so filter gets recomputed
    227
    228
    229      filter_coord = property(fget=_getFilter, fset=_setFilter)
    230      elementsize = property(fget=lambda self: self.__elementsize,
    231                             fset=_setElementSize)
    232      compatmask = property(_getCompatMask, _setCompatMask)
    233
    234  # Template for future classes
    235  #
    236  # class MeshMetric(Metric):
    237  #     """Return list of neighboring points on a mesh
    238  #     """
    239  #     def getNeighbors(self, origin, distance=0):
    240  #         """Return neighbors"""
    241  #         raise NotImplementedError
    242