Package mvpa :: Package mappers :: Module mask

[hide private]
[frames] | no frames]

# Source Code for Module mvpa.mappers.mask

      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  """Data mapper which applies mask to the data"""
     10
     11  __docformat__ = 'restructuredtext'
     12
     13  import numpy as N
     14
     15  from mvpa.mappers.base import Mapper
     16  from mvpa.base.dochelpers import enhancedDocString
     17  from mvpa.misc.support import isInVolume
     18
     19  if __debug__:
     20      from mvpa.base import debug, warning
     21      from mvpa.misc.support import isSorted
     22
     23
     24 -class MaskMapper(Mapper):
     25      """Mapper which uses a binary mask to select "Features" """
     26
     27 -    def __init__(self, mask, **kwargs):
     28          """Initialize MaskMapper
     29
     30          :Parameters:
     31            mask : array
     32              an array in the original dataspace and its nonzero elements are
     33              used to define the features included in the dataset
     34          """
     35          Mapper.__init__(self, **kwargs)
     36
     37          self.__mask = self.__maskdim = self.__masksize = \
     38                        self.__masknonzerosize = self.__forwardmap = \
     39                        self.__masknonzero = None # to make pylint happy
     40          self._initMask(mask)
     41
     42
     43      __doc__ = enhancedDocString('MaskMapper', locals(), Mapper)
     44
     45
     46 -    def __str__(self):
     47          return "MaskMapper: %d -> %d" \
     48              % (self.__masksize, self.__masknonzerosize)
     49
     50 -    def __repr__(self):
     51          s = super(MaskMapper, self).__repr__()
     52          return s.replace("(", "(mask=%s," % self.__mask, 1)
     53
     54  # XXX
     55  # XXX HAS TO TAKE CARE OF SUBCLASSES!!!
     56  # XXX
     57  #
     58  #    def __deepcopy__(self, memo=None):
     59  #        # XXX memo does not seem to be used
     60  #        if memo is None:
     61  #            memo = {}
     62  #        from mvpa.support.copy import deepcopy
     63  #        out = MaskMapper.__new__(MaskMapper)
     64  #        Mapper.__init__(out)
     65  #        out.__mask = self.__mask.copy()
     66  #        out.__maskdim = self.__maskdim
     67  #        out.__masksize = self.__masksize
     68  #        out.__masknonzero = deepcopy(self.__masknonzero)
     69  #        out.__masknonzerosize = self.__masknonzerosize
     70  #        out.__forwardmap = self.__forwardmap.copy()
     71  #
     72  #        return out
     73
     74
     75 -    def _initMask(self, mask):
     76          """Initialize internal state with mask-derived information
     77
     78          It is needed to initialize structures for the fast
     79          and reverse lookup to don't impose performance hit on any
     80          future operation
     81          """
     82          # NOTE: If any new class member are added here __deepcopy__() has to
     83          #       be adjusted accordingly!
     84
     85          self.__mask = (mask != 0)
     86          self.__maskdim = len(mask.shape)
     87          self.__masksize = N.prod(mask.shape)
     88
     89          # Following introduces space penalty but are needed
     90          # for efficient processing.
     91          # Store all coordinates for backward mapping
     92          self.__masknonzero = mask.nonzero()
     93          self.__masknonzerosize = len(self.__masknonzero[0])
     94          #from IPython.Shell import IPShellEmbed
     95          #ipshell = IPShellEmbed()
     96          #ipshell()
     97          #import pydb; pydb.debugger()
     98          # Store forward mapping (ie from coord into outId)
     99          # TODO to save space might take appropriate int type
    100          #     depending on masknonzerosize
    101          # it could be done with a dictionary, but since mask
    102          # might be relatively big, it is better to simply use
    103          # a chunk of RAM ;-)
    104          self.__forwardmap = N.zeros(mask.shape, dtype=N.int64)
    105          # under assumption that we +1 values in forwardmap so that
    106          # 0 can be used to signal outside of mask
    107
    108          self.__forwardmap[self.__masknonzero] = \
    109              N.arange(self.__masknonzerosize)
    110
    111
    112 -    def forward(self, data):
    113          """Map data from the original dataspace into featurespace.
    114          """
    115          data = N.asanyarray(data)          # assure it is an array
    116          datadim = len(data.shape)
    117          datashape = data.shape[(-1)*self.__maskdim:]
    118          if not datashape == self.__mask.shape:
    119              raise ValueError, \
    120                    "The shape of data to be mapped %s " % `datashape` \
    121                    + " does not match the mapper's mask shape %s" \
    122                      % `self.__mask.shape`
    123
    124          if self.__maskdim == datadim:
    125              # we had to select by __masknonzero if we didn't sort
    126              # Ids and wanted to preserve the order
    127              #return data[ self.__masknonzero ]
    128              return data[ self.__mask ]
    129          elif self.__maskdim+1 == datadim:
    130              # XXX XXX XXX below line should be accomodated also
    131              # to make use of self.__masknonzero instead of
    132              # plain mask if we want to preserve the (re)order
    133              return data[ :, self.__mask ]
    134          else:
    135              raise ValueError, \
    136                    "Shape of the to be mapped data, does not match the " \
    137                    "mapper mask. Only one (optional) additional dimension " \
    138                    "exceeding the mask shape is supported."
    139
    140
    141 -    def reverse(self, data):
    142          """Reverse map data from featurespace into the original dataspace.
    143          """
    144          data = N.asanyarray(data)
    145          datadim = len(data.shape)
    146          if not datadim in [1, 2]:
    147              raise ValueError, \
    148                    "Only 2d or 1d data can be reverse mapped. "\
    149                    "Got data of shape %s" % (data.shape,)
    150
    151          if datadim == 1:
    152              # Verify that we are trying to reverse data of proper dimension.
    153              # In 1D case numpy would not complain and will broadcast
    154              # the values
    155              if __debug__ and  self.nfeatures != len(data):
    156                  raise ValueError, \
    157                        "Cannot reverse map data with %d elements, whenever " \
    158                        "mask knows only %d" % (len(data), self.nfeatures)
    159              mapped = N.zeros(self.__mask.shape, dtype=data.dtype)
    160              mapped[self.__mask] = data
    161          elif datadim == 2:
    162              # Verify that we are trying to reverse data of proper dimension.
    163              # In 2D case numpy we should have matching # of features
    164              if __debug__ and  self.nfeatures != data.shape[1]:
    165                  raise ValueError, \
    166                        "Cannot reverse map data of shape %s, whenever " \
    167                        "mask knows only %d features" \
    168                        % (data.shape, self.nfeatures)
    169
    170              mapped = N.zeros(data.shape[:1] + self.__mask.shape,
    171                               dtype=data.dtype)
    172              mapped[:, self.__mask] = data
    173
    174          return mapped
    175
    176
    177 -    def getInSize(self):
    178          """InShape is a shape of original mask"""
    179          return self.__masksize
    180
    181
    182 -    def getOutSize(self):
    183          """OutSize is a number of non-0 elements in the mask"""
    184          return self.__masknonzerosize
    185
    186
    187 -    def getMask(self, copy = True):
    188          """By default returns a copy of the current mask.
    189
    190          If 'copy' is set to False a reference to the mask is returned instead.
    191          This shared mask must not be modified!
    192          """
    193          if copy:
    194              return self.__mask.copy()
    195          else:
    196              return self.__mask
    197
    198
    199 -    def getInId(self, outId):
    200          """Returns a features coordinate in the original data space
    201          for a given feature id.
    202
    203          If this method is called with a list of feature ids it returns a
    204          2d-array where the first axis corresponds the dimensions in 'In'
    205          dataspace and along the second axis are the coordinates of the features
    206          on this dimension (like the output of NumPy.array.nonzero()).
    207
    208          XXX it might become __get_item__ access method
    209
    210          """
    211          # XXX Might be improved by storing also transpose of
    212          # __masknonzero
    213          return N.array([self.__masknonzero[i][outId]
    214                          for i in xrange(self.__maskdim)])
    215
    216
    217 -    def getInIds(self):
    218          """Returns a 2d array where each row contains the coordinate of the
    219          feature with the corresponding id.
    220          """
    221          return N.transpose(self.__masknonzero)
    222
    223
    224 -    def isValidInId(self, inId):
    225          mask = self.mask
    226          return (isInVolume(inId, mask.shape) and mask[tuple(inId)] != 0)
    227
    228
    229 -    def getOutId(self, coord):
    230          """Translate a feature mask coordinate into a feature ID.
    231          """
    232          # FIXME Since lists/arrays accept negative indexes to go from
    233          # the end -- we need to check coordinates explicitely. Otherwise
    234          # we would get warping effect
    235          try:
    236              tcoord = tuple(coord)
    237              if self.__mask[tcoord] == 0:
    238                  raise ValueError, \
    239                        "The point %s didn't belong to the mask" % (`coord`)
    240              return self.__forwardmap[tcoord]
    241          except TypeError:
    242              raise ValueError, \
    243                    "Coordinates %s are of incorrect dimension. " % `coord` + \
    244                    "The mask has %d dimensions." % self.__maskdim
    245          except IndexError:
    246              raise ValueError, \
    247                    "Coordinates %s are out of mask boundary. " % `coord` + \
    248                    "The mask is of %s shape." % `self.__mask.shape`
    249
    250
    251 -    def selectOut(self, outIds):
    252          """Only listed outIds would remain.
    253
    254          *Function assumes that outIds are sorted*. In __debug__ mode selectOut
    255          would check if obtained IDs are sorted and would warn the user if they
    256          are not.
    257
    258          .. note::
    259            If you feel strongly that you need to remap features
    260            internally (ie to allow Ids with mixed order) please contact
    261            developers of mvpa to discuss your use case.
    262
    263            The function used to accept a matrix-mask as the input but now
    264            it really has to be a list of IDs
    265
    266          Feature/Bug:
    267           * Negative outIds would not raise exception - just would be
    268             treated 'from the tail'
    269          """
    270          if __debug__ and 'CHECK_SORTEDIDS' in debug.active:
    271              # per short conversation with Michael -- we should not
    272              # allow reordering since we saw no viable use case for
    273              # it. Thus -- warn user is outIds are not in sorted order
    274              # and no sorting was requested may be due to performance
    275              # considerations
    276              if not isSorted(outIds):
    277                  warning("IDs for selectOut must be provided " +
    278                          "in sorted order, otherwise .forward() would fail"+
    279                          " on the data with multiple samples")
    280
    281          # adjust mask and forwardmap
    282          discarded = N.array([ True ] * self.nfeatures)
    283          discarded[outIds] = False    # create a map of discarded Ids
    284          discardedin = tuple(self.getInId(discarded))
    285          self.__mask[discardedin] = False
    286
    287          self.__masknonzerosize = len(outIds)
    288          self.__masknonzero = [ x[outIds] for x in self.__masknonzero ]
    289
    290          # adjust/remap not discarded in forwardmap
    291          # since we merged _tent/maskmapper-init-noloop it is not necessary
    292          # to zero-out discarded entries since we anyway would check with mask
    293          # in getOutId(s)
    294          self.__forwardmap[self.__masknonzero] = \
    295              N.arange(self.__masknonzerosize)
    296
    297
    298 -    def discardOut(self, outIds):
    299          """Listed outIds would be discarded
    300
    301          """
    302
    303          # adjust mask and forwardmap
    304          discardedin = tuple(self.getInId(outIds))
    305          self.__mask[discardedin] = False
    306          # since we merged _tent/maskmapper-init-noloop it is not necessary
    307          # to zero-out discarded entries since we anyway would check with mask
    308          # in getOutId(s)
    309          # self.__forwardmap[discardedin] = 0
    310
    311          self.__masknonzerosize -= len(outIds)
    312          self.__masknonzero = [ N.delete(x, outIds)
    313                                 for x in self.__masknonzero ]
    314
    315          # adjust/remap not discarded in forwardmap
    316          self.__forwardmap[self.__masknonzero] = \
    317                                                N.arange(self.__masknonzerosize)
    318
    319          # OPT: we can adjust __forwardmap only for ids which are higher than
    320          # the smallest outId among discarded. Similar strategy could be done
    321          # for selectOut but such index has to be figured out first there
    322          #      ....
    323
    324
    325  # comment out for now... introduce when needed
    326  #    def getInEmpty(self):
    327  #        """Returns empty instance of input object"""
    328  #        raise NotImplementedError
    329  #
    330  #
    331  #    def getOutEmpty(self):
    332  #        """Returns empty instance of output object"""
    333  #        raise NotImplementedError
    334
    335
    336 -    def convertOutIds2OutMask(self, outIds):
    337          """Returns a boolean mask with all features in `outIds` selected.
    338
    339          :Parameters:
    340              outIds: list or 1d array
    341                  To be selected features ids in out-space.
    342
    343          :Returns:
    344              ndarray: dtype='bool'
    345                  All selected features are set to True; False otherwise.
    346          """
    347          fmask = N.repeat(False, self.nfeatures)
    348          fmask[outIds] = True
    349
    350          return fmask
    351
    352
    353 -    def convertOutIds2InMask(self, outIds):
    354          """Returns a boolean mask with all features in `ouIds` selected.
    355
    356          This method works exactly like Mapper.convertOutIds2OutMask(), but the
    357          feature mask is finally (reverse) mapped into in-space.
    358
    359          :Parameters:
    360              outIds: list or 1d array
    361                  To be selected features ids in out-space.
    362
    363          :Returns:
    364              ndarray: dtype='bool'
    365                  All selected features are set to True; False otherwise.
    366          """
    367          return self.reverse(self.convertOutIds2OutMask(outIds))
    368
    369
    370      # Read-only props
    371      mask = property(fget=lambda self:self.getMask(False))
    372
    373
    374      # TODO Unify tuple/array conversion of coordinates. tuples are needed
    375      #      for easy reference, arrays are needed when doing computation on
    376      #      coordinates: for some reason numpy doesn't handle casting into
    377      #      array from tuples while performing arithm operations...
    378