elephant.unitary_event_analysis — Elephant 0.5.0 documentation

# Source code for elephant.unitary_event_analysis

    # -*- coding: utf-8 -*-
    """

    Synopsis
    --------

    Unitary Event (UE) analysis is a statistical method that
    analyzes excess spike correlation between simultaneously recorded neurons in a
    time resolved manner by comparing the empirical spike coincidences to the
    expected number based on the firing rates of the neurons see [#Gruen99]_.

    Background
    ----------

    It has been proposed that cortical neurons organize dynamically into functional
    groups (“cell assemblies”) by the temporal structure of their joint spiking
    activity. The Unitary Events analysis method detects conspicuous patterns of
    synchronous spike activity among simultaneously recorded single neurons. The
    statistical significance of a pattern is evaluated by comparing the empirical
    number of occurrences to the number expected given the firing rates of the
    neurons. Key elements of the method are the proper formulation of the null
    hypothesis and the derivation of the corresponding count distribution of
    synchronous spike events used in the significance test. The analysis is
    performed in a sliding window manner and yields a time-resolved measure of
    significant spike synchrony.

    Examples
    --------

    :ref:`Tutorial on using Unitary Events <ipynb/unitary_events.ipynb>`

    Run tutorial interactively:

    .. image:: https://mybinder.org/badge.svg
       :target: https://mybinder.org/v2/gh/INM-6/elephant/enh/clean_ue?filepath=doc/ipynb/unitary_events.ipynb

    References
    ----------

    .. [#Gruen99] Gruen S, Diesmann M, Grammont F, Riehle A, Aertsen A (1999) TITLE
       J Neurosci Methods, 94(1): 67-79.
    .. [#Gruen02a] Gruen S, Diesmann M, Aertsen A (2002) TITLE Neural Comput,
       14(1): 43-80.
    .. [#Gruen02b] Gruen S, Diesmann M, Aertsen A (2002) TITLE Neural Comput,
       14(1): 81-19.
    .. [#Gruen03] Gruen S, Riehle A, and Diesmann M (2003) Effect of cross-trial
       nonstationarity on joint-spike events. Biological Cybernetics 88(5):335-351
    .. [#Gruen09] Gruen S (2009) Data-driven significance estimation of precise
       spike correlation. J Neurophysiology 101:1126-1140 (invited review).

    Author Contributions
    --------------------

     - Vahid Rostami (VH)
     - Sonja Gruen (SG)
     - Markus Diesmann (MD)
    VH implemented the method, SG and MD provided input

    Functions
    ---------

    :copyright: Copyright 2015-2018 by the Elephant team, see AUTHORS.txt.
    :license: Modified BSD, see LICENSE.txt for details.
    """

    from elephant.conversion import BinnedSpikeTrain
    import warnings
    import numpy as np
    import scipy
    import neo
    import quantities as pq

    def hash_from_pattern(m, N, base=2):
        """
        Calculate for a spike pattern or a matrix of spike patterns
        (provide each pattern as a column) composed of N neurons a
        unique number.

        Parameters:
        -----------
        m: 2-dim ndarray
               spike patterns represented as a binary matrix (i.e., matrix of 0's and 1's).
               Rows and columns correspond to patterns and neurons, respectively.
        N: integer
               number of neurons is required to be equal to the number
               of rows
        base: integer
               base for calculation of hash values from binary
               sequences (= pattern).
               Default is 2

        Returns:
        --------
        list of integers:
               An array containing the hash values of each pattern,
               shape: (number of patterns)

        Raises:
        -------
           ValueError: if matrix m has wrong orientation

        Examples:
        ---------
        descriptive example:
        m = [0
             1
             1]
        N = 3
        base = 2
        hash = 0*2^2 + 1*2^1 + 1*2^0 = 3

        second example:
        >>> import numpy as np
        >>> m = np.array([[0, 1, 0, 0, 1, 1, 0, 1],
                             [0, 0, 1, 0, 1, 0, 1, 1],
                             [0, 0, 0, 1, 0, 1, 1, 1]])

        >>> hash_from_pattern(m,N=3)
            array([0, 4, 2, 1, 6, 5, 3, 7])
        """
        # check the consistency between shape of m and number neurons N
        if N != np.shape(m)[0]:
            raise ValueError('patterns in the matrix should be column entries')

        # check the entries of the matrix
        if not np.all((np.array(m) == 0) + (np.array(m) == 1)):
            raise ValueError('patterns should be zero or one')

        # generate the representation
        v = np.array([base ** x for x in range(N)])
        # reverse the order
        v = v[np.argsort(-v)]
        # calculate the binary number by use of scalar product
        return np.dot(v, m)

    def inverse_hash_from_pattern(h, N, base=2):
        """
        Calculate the 0-1 spike patterns (matrix) from hash values

        Parameters:
        -----------
        h: list of integers
               list or array of hash values, length: number of patterns
        N: integer
               number of neurons
        base: integer
               base for calculation of the number from binary
               sequences (= pattern).
               Default is 2

        Raises:
        -------
           ValueError: if the hash is not compatible with the number
           of neurons hash value should not be larger than the biggest
           possible hash number with given number of neurons
           (e.g. for N = 2, max(hash) = 2^1 + 2^0 = 3
                , or for N = 4, max(hash) = 2^3 + 2^2 + 2^1 + 2^0 = 15)

        Returns:
        --------
           numpy.array:
               A matrix of shape: (N, number of patterns)

        Examples
        ---------
        >>> import numpy as np
        >>> h = np.array([3,7])
        >>> N = 4
        >>> inverse_hash_from_pattern(h,N)
            array([[1, 1],
                   [1, 1],
                   [0, 1],
                   [0, 0]])
        """

        # check if the hash values are not greater than the greatest possible
        # value for N neurons with the given base
        if np.any(h > np.sum([base ** x for x in range(N)])):
            raise ValueError(
                "hash value is not compatible with the number of neurons N")
        # check if the hash values are integer
        if not np.all(np.int64(h) == h):
            raise ValueError("hash values are not integers")

        m = np.zeros((N, len(h)), dtype=int)
        for j, hh in enumerate(h):
            i = N - 1
            while i >= 0 and hh != 0:
                m[i, j] = hh % base
                hh /= base
                i -= 1
        return m

    def n_emp_mat(mat, N, pattern_hash, base=2):
        """
        Count the occurrences of spike coincidence patterns
        in the given spike trains.

        Parameters:
        -----------
        mat: 2-dim ndarray
               binned spike trains of N neurons. Rows and columns correspond
               to neurons and temporal bins, respectively.
        N: integer
               number of neurons
        pattern_hash: list of integers
               hash values representing the spike coincidence patterns
               of which occurrences are counted.
        base: integer
               Base which was used to generate the hash values.
               Default is 2

        Returns:
        --------
        N_emp: list of integers
               number of occurrences of the given patterns in the given spike trains
        indices: list of lists of integers
               indices indexing the bins where the given spike patterns are found
               in `mat`. Same length as `pattern_hash`
               indices[i] = N_emp[i] = pattern_hash[i]

        Raises:
        -------
           ValueError: if mat is not zero-one matrix

        Examples:
        ---------
        >>> mat = np.array([[1, 0, 0, 1, 1],
                            [1, 0, 0, 1, 0]])
        >>> pattern_hash = np.array([1,3])
        >>> n_emp, n_emp_indices = N_emp_mat(mat, N,pattern_hash)
        >>> print n_emp
        [ 0.  2.]
        >>> print n_emp_indices
        [array([]), array([0, 3])]
        """
        # check if the mat is zero-one matrix
        if not np.all((np.array(mat) == 0) + (np.array(mat) == 1)):
            raise ValueError("entries of mat should be either one or zero")
        h = hash_from_pattern(mat, N, base=base)
        N_emp = np.zeros(len(pattern_hash))
        indices = []
        for idx_ph, ph in enumerate(pattern_hash):
            indices_tmp = np.where(h == ph)[0]
            indices.append(indices_tmp)
            N_emp[idx_ph] = len(indices_tmp)
        return N_emp, indices

    def n_emp_mat_sum_trial(mat, N, pattern_hash):
        """
        Calculates empirical number of observed patterns summed across trials

        Parameters:
        -----------
        mat: 3d numpy array or elephant BinnedSpikeTrain object
               Binned spike trains represented as a binary matrix (i.e., matrix of 0's and 1's),
               segmented into trials. Trials should contain an identical number of neurons and
               an identical number of time bins.
                the entries are zero or one
                0-axis --> trials
                1-axis --> neurons
                2-axis --> time bins
        N: integer
                number of neurons
        pattern_hash: list of integers
                array of hash values, length: number of patterns

        Returns:
        --------
        N_emp: list of integers
               numbers of occurences of the given spike patterns in the given spike trains,
               summed across trials. Same length as `pattern_hash`.
        idx_trials: list of lists of integers
               list of indices of mat for each trial in which
               the specific pattern has been observed.
               0-axis --> trial
               1-axis --> list of indices for the chosen trial per
               entry of `pattern_hash`

        Raises:
        -------
           ValueError: if matrix mat has wrong orientation
           ValueError: if mat is not zero-one matrix

        Examples:
        ---------
        >>> mat = np.array([[[1, 1, 1, 1, 0],
                           [0, 1, 1, 1, 0],
                           [0, 1, 1, 0, 1]],

                           [[1, 1, 1, 1, 1],
                            [0, 1, 1, 1, 1],
                            [1, 1, 0, 1, 0]]])

        >>> pattern_hash = np.array([4,6])
        >>> N = 3
        >>> n_emp_sum_trial, n_emp_sum_trial_idx =
                                 n_emp_mat_sum_trial(mat, N,pattern_hash)
        >>> n_emp_sum_trial
            array([ 1.,  3.])
        >>> n_emp_sum_trial_idx
            [[array([0]), array([3])], [array([], dtype=int64), array([2, 4])]]
        """
        # check the consistency between shape of m and number neurons N
        if N != np.shape(mat)[1]:
            raise ValueError('the entries of mat should be a list of a'
                             'list where 0-axis is trials and 1-axis is neurons')

        num_patt = len(pattern_hash)
        N_emp = np.zeros(num_patt)

        idx_trials = []
        # check if the mat is zero-one matrix
        if not np.all((np.array(mat) == 0) + (np.array(mat) == 1)):
            raise ValueError("entries of mat should be either one or zero")

        for mat_tr in mat:
            N_emp_tmp, indices_tmp = n_emp_mat(mat_tr, N, pattern_hash, base=2)
            idx_trials.append(indices_tmp)
            N_emp += N_emp_tmp

        return N_emp, idx_trials

    def _n_exp_mat_analytic(mat, N, pattern_hash):
        """
        Calculates the expected joint probability for each spike pattern analyticaly
        """
        marg_prob = np.mean(mat, 1, dtype=float)
        # marg_prob needs to be a column vector, so we
        # build a two dimensional array with 1 column
        # and len(marg_prob) rows
        marg_prob = np.reshape(marg_prob, (len(marg_prob), 1))
        m = inverse_hash_from_pattern(pattern_hash, N)
        nrep = np.shape(m)[1]
        # multipyling the marginal probability of neurons with regard to the
        # pattern
        pmat = np.multiply(m, np.tile(marg_prob, (1, nrep))) +\
            np.multiply(1 - m, np.tile(1 - marg_prob, (1, nrep)))
        return np.prod(pmat, axis=0) * float(np.shape(mat)[1])

    def _n_exp_mat_surrogate(mat, N, pattern_hash, n_surr=1):
        """
        Calculates the expected joint probability for each spike pattern with spike
        time randomization surrogate
        """
        if len(pattern_hash) > 1:
            raise ValueError('surrogate method works only for one pattern!')
        N_exp_array = np.zeros(n_surr)
        for rz_idx, rz in enumerate(np.arange(n_surr)):
            # shuffling all elements of zero-one matrix
            mat_surr = np.array(mat)
            [np.random.shuffle(i) for i in mat_surr]
            N_exp_array[rz_idx] = n_emp_mat(mat_surr, N, pattern_hash)[0][0]
        return N_exp_array

    def n_exp_mat(mat, N, pattern_hash, method='analytic', n_surr=1):
        """
        Calculates the expected joint probability for each spike pattern

        Parameters:
        -----------
        mat: 2d numpy array
                the entries are zero or one
                0-axis --> neurons
                1-axis --> time bins
        pattern_hash: list of integers
                array of hash values, length: number of patterns
        method: string
                method with which the expectency should be caculated
                'analytic' -- > analytically
                'surr' -- > with surrogates (spike time randomization)
                Default is 'analytic'
        n_surr: integer
                number of surrogates for constructing the distribution of expected joint probability.
                Default is 1 and this number is needed only when method = 'surr'

        kwargs:
        -------

        Raises:
        -------
           ValueError: if matrix m has wrong orientation

        Returns:
        --------
        if method is analytic:
            numpy.array:
               An array containing the expected joint probability of each pattern,
               shape: (number of patterns,)
        if method is surr:
            numpy.ndarray, 0-axis --> different realizations,
                           length = number of surrogates
                           1-axis --> patterns

        Examples:
        ---------
        >>> mat = np.array([[1, 1, 1, 1],
                           [0, 1, 0, 1],
                           [0, 0, 1, 0]])
        >>> pattern_hash = np.array([5,6])
        >>> N = 3
        >>> n_exp_anal = n_exp_mat(mat,N, pattern_hash, method = 'analytic')
        >>> n_exp_anal
            [ 0.5 1.5 ]
        >>>
        >>>
        >>> n_exp_surr = n_exp_mat(
                      mat, N,pattern_hash, method = 'surr', n_surr = 5000)
        >>> print n_exp_surr
        [[ 1.  1.]
         [ 2.  0.]
         [ 2.  0.]
         ...,
         [ 2.  0.]
         [ 2.  0.]
         [ 1.  1.]]

        """
        # check if the mat is zero-one matrix
        if np.any(mat > 1) or np.any(mat < 0):
            raise ValueError("entries of mat should be either one or zero")

        if method == 'analytic':
            return _n_exp_mat_analytic(mat, N, pattern_hash)
        if method == 'surr':
            return _n_exp_mat_surrogate(mat, N, pattern_hash, n_surr)

    def n_exp_mat_sum_trial(
            mat, N, pattern_hash, method='analytic_TrialByTrial', **kwargs):
        """
        Calculates the expected joint probability
        for each spike pattern sum over trials

        Parameters:
        -----------
        mat: 3d numpy array or elephant BinnedSpikeTrain object
               Binned spike trains represented as a binary matrix (i.e., matrix of 0's and 1's),
               segmented into trials. Trials should contain an identical number of neurons and
               an identical number of time bins.
                the entries are zero or one
                0-axis --> trials
                1-axis --> neurons
                2-axis --> time bins
        N: integer
               number of neurons
        pattern_hash: list of integers
                array of hash values, length: number of patterns
        method: string
                method with which the unitary events whould be computed
                'analytic_TrialByTrial' -- > calculate the expectency
                (analytically) on each trial, then sum over all trials.
                'analytic_TrialAverage' -- > calculate the expectency
                by averaging over trials.
                (cf. Gruen et al. 2003)
                'surrogate_TrialByTrial' -- > calculate the distribution
                of expected coincidences by spike time randomzation in
                each trial and sum over trials.
                Default is 'analytic_trialByTrial'

        kwargs:
        -------
        n_surr: integer
                number of surrogate to be used
                Default is 1

        Returns:
        --------
        numpy.array:
           An array containing the expected joint probability of
           each pattern summed over trials,shape: (number of patterns,)

        Examples:
        --------
        >>> mat = np.array([[[1, 1, 1, 1, 0],
                           [0, 1, 1, 1, 0],
                           [0, 1, 1, 0, 1]],

                           [[1, 1, 1, 1, 1],
                            [0, 1, 1, 1, 1],
                            [1, 1, 0, 1, 0]]])

        >>> pattern_hash = np.array([5,6])
        >>> N = 3
        >>> n_exp_anal = n_exp_mat_sum_trial(mat, N, pattern_hash)
        >>> print n_exp_anal
            array([ 1.56,  2.56])
        """
        # check the consistency between shape of m and number neurons N
        if N != np.shape(mat)[1]:
            raise ValueError('the entries of mat should be a list of a'
                             'list where 0-axis is trials and 1-axis is neurons')

        if method == 'analytic_TrialByTrial':
            n_exp = np.zeros(len(pattern_hash))
            for mat_tr in mat:
                n_exp += n_exp_mat(mat_tr, N, pattern_hash, method='analytic')
        elif method == 'analytic_TrialAverage':
            n_exp = n_exp_mat(
                np.mean(mat, 0), N, pattern_hash, method='analytic') * np.shape(mat)[0]
        elif method == 'surrogate_TrialByTrial':
            if 'n_surr' in kwargs:
                n_surr = kwargs['n_surr']
            else:
                n_surr = 1.
            n_exp = np.zeros(n_surr)
            for mat_tr in mat:
                n_exp += n_exp_mat(mat_tr, N, pattern_hash,
                                   method='surr', n_surr=n_surr)
        else:
            raise ValueError(
                "The method only works on the zero_one matrix at the moment")
        return n_exp

    def gen_pval_anal(
            mat, N, pattern_hash, method='analytic_TrialByTrial', **kwargs):
        """
        computes the expected coincidences and a function to calculate
        p-value for given empirical coincidences

        this function generate a poisson distribution with the expected
        value calculated by mat. it returns a function which gets
        the empirical coincidences, `n_emp`,  and calculates a p-value
        as the area under the poisson distribution from `n_emp` to infinity

        Parameters:
        -----------
        mat: 3d numpy array or elephant BinnedSpikeTrain object
               Binned spike trains represented as a binary matrix (i.e., matrix of 0's and 1's),
               segmented into trials. Trials should contain an identical number of neurons and
               an identical number of time bins.
                the entries are zero or one
                0-axis --> trials
                1-axis --> neurons
                2-axis --> time bins
        N: integer
               number of neurons
        pattern_hash: list of integers
                array of hash values, length: number of patterns
        method: string
                method with which the unitary events whould be computed
                'analytic_TrialByTrial' -- > calculate the expectency
                (analytically) on each trial, then sum over all trials.
                ''analytic_TrialAverage' -- > calculate the expectency
                by averaging over trials.
                Default is 'analytic_trialByTrial'
                (cf. Gruen et al. 2003)

        kwargs:
        -------
        n_surr: integer
                number of surrogate to be used
                Default is 1

        Returns:
        --------
        pval_anal:
                a function which calculates the p-value for
                the given empirical coincidences
        n_exp: list of floats
                expected coincidences

        Examples:
        --------
        >>> mat = np.array([[[1, 1, 1, 1, 0],
                           [0, 1, 1, 1, 0],
                           [0, 1, 1, 0, 1]],

                           [[1, 1, 1, 1, 1],
                            [0, 1, 1, 1, 1],
                            [1, 1, 0, 1, 0]]])

        >>> pattern_hash = np.array([5,6])
        >>> N = 3
        >>> pval_anal,n_exp = gen_pval_anal(mat, N,pattern_hash)
        >>> n_exp
            array([ 1.56,  2.56])
        """
        if method == 'analytic_TrialByTrial' or method == 'analytic_TrialAverage':
            n_exp = n_exp_mat_sum_trial(mat, N, pattern_hash, method=method)

            def pval(n_emp):
                p = 1. - scipy.special.gammaincc(n_emp, n_exp)
                return p
        elif method == 'surrogate_TrialByTrial':
            if 'n_surr' in kwargs:
                n_surr = kwargs['n_surr']
            else:
                n_surr = 1.
            n_exp = n_exp_mat_sum_trial(
                mat, N, pattern_hash, method=method, n_surr=n_surr)

            def pval(n_emp):
                hist = np.bincount(np.int64(n_exp))
                exp_dist = hist / float(np.sum(hist))
                if len(n_emp) > 1:
                    raise ValueError(
                        'in surrogate method the p_value can be calculated only for one pattern!')
                return np.sum(exp_dist[int(n_emp[0]):])

        return pval, n_exp

    def jointJ(p_val):
        """Surprise measurement

        logarithmic transformation of joint-p-value into surprise measure
        for better visualization as the highly significant events are
        indicated by very low joint-p-values

        Parameters:
        -----------
        p_val: list of floats
            p-values of statistical tests for different pattern.

        Returns:
        --------
        J: list of floats
            list of surprise measure

        Examples:
        ---------
        >>> p_val = np.array([0.31271072,  0.01175031])
        >>> jointJ(p_val)
            array([0.3419968 ,  1.92481736])
        """
        p_arr = np.array(p_val)

        try:
            Js = np.log10(1 - p_arr) - np.log10(p_arr)
        except RuntimeWarning:
            pass
        return Js

    def _rate_mat_avg_trial(mat):
        """
        calculates the average firing rate of each neurons across trials
        """
        num_tr, N, nbins = np.shape(mat)
        psth = np.zeros(N)
        for tr, mat_tr in enumerate(mat):
            psth += np.sum(mat_tr, axis=1)
        return psth / float(nbins) / float(num_tr)

    def _bintime(t, binsize):
        """
        change the real time to bintime
        """
        t_dl = t.rescale('ms').magnitude
        binsize_dl = binsize.rescale('ms').magnitude
        return np.floor(np.array(t_dl) / binsize_dl).astype(int)

    def _winpos(t_start, t_stop, winsize, winstep, position='left-edge'):
        """
        Calculates the position of the analysis window
        """
        t_start_dl = t_start.rescale('ms').magnitude
        t_stop_dl = t_stop.rescale('ms').magnitude
        winsize_dl = winsize.rescale('ms').magnitude
        winstep_dl = winstep.rescale('ms').magnitude

        # left side of the window time
        if position == 'left-edge':
            ts_winpos = np.arange(
                t_start_dl, t_stop_dl - winsize_dl + winstep_dl, winstep_dl) * pq.ms
        else:
            raise ValueError(
                'the current version only returns left-edge of the window')
        return ts_winpos

    def _UE(mat, N, pattern_hash, method='analytic_TrialByTrial', **kwargs):
        """
        returns the default results of unitary events analysis
        (Surprise, empirical coincidences and index of where it happened
        in the given mat, n_exp and average rate of neurons)
        """
        rate_avg = _rate_mat_avg_trial(mat)
        n_emp, indices = n_emp_mat_sum_trial(mat, N, pattern_hash)
        if method == 'surrogate_TrialByTrial':
            if 'n_surr' in kwargs:
                n_surr = kwargs['n_surr']
            else:
                n_surr = 1
            dist_exp, n_exp = gen_pval_anal(
                mat, N, pattern_hash, method, n_surr=n_surr)
            n_exp = np.mean(n_exp)
        elif method == 'analytic_TrialByTrial' or method == 'analytic_TrialAverage':
            dist_exp, n_exp = gen_pval_anal(mat, N, pattern_hash, method)
        pval = dist_exp(n_emp)
        Js = jointJ(pval)
        return Js, rate_avg, n_exp, n_emp, indices

    def unitary_event_analysis(
            data, bin_size, window_size, window_step, pattern_hash,
            method='analytic_TrialByTrial', t_start=None,
            t_stop=None, binary=True, **kwargs):
        """
        Performs the Unitary Event Analysis in a sliding window fashion.

        #TODO: Describe the method

        Parameters:
        ----------
        data: list of lists of `SpikeTrain` objects
            Contains the spike data to be analyzed. `data` is constructed such
            that `data[t][n]` contains the `SpikeTrain` object that refers to
            neuron `n` in trial `t`. For any trial `t` and any neuron `n`, the
            `SpikeTrain` objects are assumed to share a common time axis.
        bin_size: `Quantity` scalar (unit: time)
               Size of bins for discretizing spike trains.
        window_size: `Quantity` scalar (unit: time)
               Size of the sliding analysis window.
        window_step: `Quantity` scalar (unit: time)
               Stepsize by the window is moved.
        pattern_hash: list of integers
               List of patterns to include in the analysis. Patterns are identified
               by their hash values as returned by the function
               `hash_from_pattern` (see also function `inverse_hash_from_pattern`).
        method: string
            Method to calculate the Unitary Events.

            * 'analytic_TrialByTrial': calculate the expectancy (analytically) on
              each trial, then sum over all trials.
            * 'analytic_TrialAverage': calculate the expectancy by averaging over
              trials (cf. Gruen et al. 2003).
            * 'surrogate_TrialByTrial': calculate the distribution of expected
              coincidences by spike time randomization in each trial and sum over
              trials.

            Default: 'analytic_trialByTrial'
        t_start: None or Quantity scalar (unit time)
        	The start time of the analysis. #TODO position of window?
            If **None**, retrieved from the `t_start` attribute of the
            `SpikeTrain` object.

            Default: None.

        t_stop: None or Quantity scalar, optional
                The stop time of the analysis.
                If **None**, retrieved from the `t_stop` attribute of the
                `SpikeTrain` object.

                Default: None

        kwargs:
        -------
        n_surrogates: integer
                Number of surrogates to be used

                Default: 100

        Returns:
        --------
        result: dictionary
            Dictionary containing a summary of analysis results. Each key 'k' in
            the dictionary stores a list of lists such that result['k'][p][w]
            contains the resulting measurement k for the pattern `pattern_hash[p]`
            calculated at window position `w`. The dictionary contains the
            following keys:
            Js: list of list of float
                TODO: is this really a list of list?
            indices: list of list of integers
                indices of pattern within each window TODO: ???
            n_emp: list of integers
                 Empirical number of occurrences of the pattern.
            n_exp: list of floats
                 Expected number of occurrences of the pattern.
            rate_avg: list of floats
                 Average firing rate of each neuron.
        See also:
        ---------
        hash_from_pattern
        inverse_hash_from_pattern
        """
        if not isinstance(data[0][0], neo.SpikeTrain):
            raise ValueError(
                "structure of the data is not correct: 0-axis should be trials, 1-axis units and 2-axis neo spike trains")

        # TODO: Handling of t_start and t_stop not adequate, consistency check needed
        if t_start is None:
            t_start = data[0][0].t_start.rescale('ms')
        if t_stop is None:
            t_stop = data[0][0].t_stop.rescale('ms')

        # position of all windows (left edges)
        t_winpos = _winpos(t_start, t_stop, window_size, window_step, position='left-edge')
        t_winpos_bintime = _bintime(t_winpos, bin_size)

        winsize_bintime = _bintime(window_size, bin_size)
        winstep_bintime = _bintime(window_step, bin_size)

        if winsize_bintime * bin_size != window_size:
            warnings.warn(
                "ratio between window_size and bin_size is not integer -- "
                "the actual number for window size is " + str(winsize_bintime * bin_size))

        if winstep_bintime * bin_size != window_step:
            warnings.warn(
                "ratio between window_size and bin_size is not integer -- "
                "the actual number for window size is" + str(winstep_bintime * bin_size))

        num_tr, N = np.shape(data)[:2]

        n_bins = int((t_stop - t_start) / bin_size)

        mat_tr_unit_spt = np.zeros((len(data), N, n_bins))
        for tr, sts in enumerate(data):
            bs = BinnedSpikeTrain(
                sts, t_start=t_start, t_stop=t_stop, binsize=bin_size)
            if binary is True:
                mat = bs.to_bool_array()
            else:
                raise ValueError(
                    "The method only works on the zero_one matrix at the moment")
            mat_tr_unit_spt[tr] = mat

        num_win = len(t_winpos)
        Js_win, n_exp_win, n_emp_win = (np.zeros(num_win) for _ in range(3))
        rate_avg = np.zeros((num_win, N))
        indices_win = {}
        for i in range(num_tr):
            indices_win['trial' + str(i)] = []

        for i, win_pos in enumerate(t_winpos_bintime):
            mat_win = mat_tr_unit_spt[:, :, win_pos:win_pos + winsize_bintime]
            if method == 'surrogate_TrialByTrial':
                if 'n_surrogates' in kwargs:
                    n_surrogates = kwargs['n_surrogates']
                else:

                    n_surrogates = 100
                Js_win[i], rate_avg[i], n_exp_win[i], n_emp_win[i], indices_lst = _UE(
                    mat_win, N, pattern_hash, method, n_surr=n_surrogates)
            else:
                Js_win[i], rate_avg[i], n_exp_win[i], n_emp_win[
                    i], indices_lst = _UE(mat_win, N, pattern_hash, method)
            for j in range(num_tr):
                if len(indices_lst[j][0]) > 0:
                    indices_win[
                        'trial' + str(j)] = np.append(indices_win['trial' + str(j)], indices_lst[j][0] + win_pos)
        return {'Js': Js_win, 'indices': indices_win, 'n_emp': n_emp_win, 'n_exp': n_exp_win, 'rate_avg': rate_avg / bin_size}