Repository: NeuralEnsemble/elephant

Issue:
[Bug] cannot run ASSET on binder
In https://github.com/NeuralEnsemble/elephant/pull/327 Alex found that binder `postBuild` should be updated in order to run asset tutorial.
Also, scikit-learn should be added in the requirements-tutorials.txt

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/31681636/badge)](https://coveralls.io/builds/31681636)

Coverage increased (+0.002%) to 89.174% when pulling **8e273ae5c4076ed4e47304bd060903cf2497f315 on INM-6:fix/asset_leftovers** into **1405966d56e859d0c22e8d486ad91a0477df235f on NeuralEnsemble:master**.


Patch:
diff --git a/elephant/__init__.py b/elephant/__init__.py
index f1d8758..e500976 100644
--- a/elephant/__init__.py
+++ b/elephant/__init__.py
@@ -31,7 +31,6 @@ from . import (statistics,
 #   parallel: avoid warns when elephant is imported
 
 try:
-    from . import pandas_bridge
     from . import asset
     from . import spade
 except ImportError:
diff --git a/elephant/asset.py b/elephant/asset.py
index b6fc9ed..cc43b72 100644
--- a/elephant/asset.py
+++ b/elephant/asset.py
@@ -99,7 +99,6 @@ The ASSET found 2 sequences of synchronous events:
 """
 from __future__ import division, print_function, unicode_literals
 
-import itertools
 import warnings
 
 import neo
@@ -400,13 +399,98 @@ def _interpolate_signals(signals, sampling_times, verbose=False):
     return interpolated_signal
 
 
-def _wrong_order(a):
-    if a[-1] > a[0]:
-        return True
-    for i in range(len(a) - 1):
-        if a[i] < a[i + 1]:
-            return True
-    return False
+def _num_iterations(n, d):
+    if d > n:
+        return 0
+    if d == 1:
+        return n
+    if d == 2:
+        # equivalent to np.sum(count_matrix)
+        return n * (n + 1) // 2 - 1
+
+    # Create square matrix with diagonal values equal to 2 to `n`.
+    # Start from row/column with index == 2 to facilitate indexing.
+    count_matrix = np.zeros((n + 1, n + 1), dtype=int)
+    np.fill_diagonal(count_matrix, np.arange(n + 1))
+    count_matrix[1, 1] = 0
+
+    # Accumulate counts of all the iterations where the first index
+    # is in the interval `d` to `n`.
+    #
+    # The counts for every level is obtained by accumulating the
+    # `count_matrix`, which is the count of iterations with the first
+    # index between `d` and `n`, when `d` == 2.
+    #
+    # For every value from 3 to `d`...
+    # 1. Define each row `n` in the count matrix as the sum of all rows
+    #    equal or above.
+    # 2. Set all rows above the current value of `d` with zeros.
+    #
+    # Example for `n` = 6 and `d` = 4:
+    #
+    #  d = 2 (start)                d = 3
+    #        count                        count
+    #  n                            n
+    #  2     2  0  0  0  0
+    #  3     0  3  0  0  0    ==>   3     2  3  0  0  0    ==>
+    #  4     0  0  4  0  0          4     2  3  4  0  0
+    #  5     0  0  0  5  0          5     2  3  4  5  0
+    #  6     0  0  0  0  6          6     2  3  4  5  6
+    #
+    #  d = 4
+    #        count
+    #  n
+    #
+    #  4     4  6  4  0  0
+    #  5     6  9  8  5  0
+    #  6     8  12 12 10 6
+    #
+    #  The total number is the sum of the `count_matrix` when `d` has
+    #  the value passed to the function.
+    #
+
+    for cur_d in range(3, d + 1):
+        for cur_n in range(n, 2, -1):
+            count_matrix[cur_n, :] = np.sum(count_matrix[:cur_n + 1, :],
+                                            axis=0)
+        # Set previous `d` level to zeros
+        count_matrix[cur_d - 1, :] = 0
+    return np.sum(count_matrix)
+
+
+def _combinations_with_replacement(n, d):
+    # Generate sequences of {a_i} such that
+    #   a_0 >= a_1 >= ... >= a_(d-1) and
+    #   d-i <= a_i <= n, for each i in [0, d-1].
+    #
+    # Almost equivalent to
+    # list(itertools.combinations_with_replacement(range(n, 0, -1), r=d))[::-1]
+    #
+    # Example:
+    #   _combinations_with_replacement(n=13, d=3) -->
+    #   (3, 2, 1), (3, 2, 2), (3, 3, 1), ... , (13, 13, 12), (13, 13, 13).
+    #
+    # The implementation follows the insertion sort algorithm:
+    #   insert a new element a_i from right to left to keep the reverse sorted
+    #   order. Now substitute increment operation for insert.
+    if d > n:
+        return
+    if d == 1:
+        for matrix_entry in range(1, n + 1):
+            yield (matrix_entry,)
+        return
+    sequence_sorted = list(range(d, 0, -1))
+    input_order = tuple(sequence_sorted)  # fixed
+    while sequence_sorted[0] != n + 1:
+        for last_element in range(1, sequence_sorted[-2] + 1):
+            sequence_sorted[-1] = last_element
+            yield tuple(sequence_sorted)
+        increment_id = d - 2
+        while increment_id > 0 and sequence_sorted[increment_id - 1] == \
+                sequence_sorted[increment_id]:
+            increment_id -= 1
+        sequence_sorted[increment_id + 1:] = input_order[increment_id + 1:]
+        sequence_sorted[increment_id] += 1
 
 
 def _jsf_uniform_orderstat_3d(u, n, verbose=False):
@@ -452,8 +536,7 @@ def _jsf_uniform_orderstat_3d(u, n, verbose=False):
 
     # Define ranges [1,...,n], [2,...,n], ..., [d,...,n] for the mute variables
     # used to compute the integral as a sum over all possibilities
-    lists = [range(j, n + 1) for j in range(d, 0, -1)]
-    it_todo = np.prod([n + 1 - j for j in range(d, 0, -1)])
+    it_todo = _num_iterations(n, d)
 
     log_1 = np.log(1.)
     # Compute the log of the integral's coefficient
@@ -485,24 +568,17 @@ def _jsf_uniform_orderstat_3d(u, n, verbose=False):
     # using matrix algebra
     # initialise probabilities to 0
     P_total = np.zeros(du.shape[0], dtype=np.float32)
-    iter_id = 0
-    for matrix_entries in tqdm(itertools.product(*lists),
-                               total=it_todo,
-                               desc="Joint survival function",
-                               disable=not verbose):
+    for iter_id, matrix_entries in enumerate(
+            tqdm(_combinations_with_replacement(n, d=d),
+                 total=it_todo,
+                 desc="Joint survival function",
+                 disable=not verbose)):
         # if we are running with MPI
         if mpi_accelerated and iter_id % size != rank:
-            iter_id += 1
-            continue
-
-        iter_id += 1
-
-        # test for valid pyramid and exit loop early
-        if _wrong_order(matrix_entries):
             continue
 
         # we only need the differences of the indices:
-        di = -np.diff(matrix_entries, prepend=n, append=0)
+        di = -np.diff((n,) + matrix_entries + (0,))
 
         # reshape the matrix to be compatible with du
         di_scratch[:, range(len(di))] = di
@@ -543,7 +619,7 @@ def _jsf_uniform_orderstat_3d(u, n, verbose=False):
     return P_total
 
 
-def _pmat_neighbors(mat, filter_shape, n_largest, verbose):
+def _pmat_neighbors(mat, filter_shape, n_largest):
     """
     Build the 3D matrix `L` of largest neighbors of elements in a 2D matrix
     `mat`.
@@ -565,8 +641,6 @@ def _pmat_neighbors(mat, filter_shape, n_largest, verbose):
         A pair of integers representing the kernel shape `(l, w)`.
     n_largest : int
         The number of largest neighbors to collect for each entry in `mat`.
-    verbose : bool
-        Show the progress bar or not.
 
     Returns
     -------
@@ -617,16 +691,16 @@ def _pmat_neighbors(mat, filter_shape, n_largest, verbose):
     # if the matrix is symmetric do not use kernel positions intersected
     # by the diagonal
     if symmetric:
-        bin_range_y = trange(l, N_bin_y - l + 1, disable=not verbose)
+        bin_range_y = range(l, N_bin_y - l + 1)
     else:
-        bin_range_y = trange(N_bin_y - l + 1, disable=not verbose)
-        bin_range_x = trange(N_bin_x - l + 1, disable=not verbose)
+        bin_range_y = range(N_bin_y - l + 1)
+        bin_range_x = range(N_bin_x - l + 1)
 
     # compute matrix of largest values
     for y in bin_range_y:
         if symmetric:
             # x range depends on y position
-            bin_range_x = trange(y - l + 1, disable=not verbose)
+            bin_range_x = range(y - l + 1)
         for x in bin_range_x:
             patch = mat[y: y + l, x: x + l]
             mskd = np.multiply(filt, patch)
@@ -1508,17 +1582,7 @@ class ASSET(object):
 
         # Compute the probability matrix obtained from imat using the Poisson
         # pdfs
-        pmat = np.zeros(imat.shape, dtype=np.float32)
-
-        for i in range(imat.shape[0]):
-            if mpi_accelerated and i % size != rank:
-                continue
-            for j in range(imat.shape[1]):
-                pmat[i, j] = scipy.stats.poisson.cdf(imat[i, j] - 1, Mu[i, j])
-
-        if mpi_accelerated:
-            for i in range(imat.shape[0]):
-                pmat[i] = comm.bcast(pmat[i], root=i % size)
+        pmat = scipy.stats.poisson.cdf(imat - 1, Mu)
 
         if symmetric:
             # Substitute 0.5 to the elements along the main diagonal
@@ -1574,8 +1638,7 @@ class ASSET(object):
         # Find for each P_ij in the probability matrix its neighbors and
         # maximize them by the maximum value 1-p_value_min
         pmat_neighb = _pmat_neighbors(
-            pmat, filter_shape=filter_shape, n_largest=n_largest,
-            verbose=self.verbose)
+            pmat, filter_shape=filter_shape, n_largest=n_largest)
 
         pmat_neighb = np.minimum(pmat_neighb, 1. - min_p_value)
 
diff --git a/elephant/spike_train_generation.py b/elephant/spike_train_generation.py
index 578be46..c8b613c 100644
--- a/elephant/spike_train_generation.py
+++ b/elephant/spike_train_generation.py
@@ -412,20 +412,6 @@ def homogeneous_poisson_process(rate, t_start=0.0 * pq.ms,
     return spiketrain
 
 
-def homogeneous_poisson_process_with_refr_period(rate,
-                                                 refr_period=2. * pq.ms,
-                                                 t_start=0.0 * pq.ms,
-                                                 t_stop=1000.0 * pq.ms,
-                                                 as_array=False):
-    warnings.warn("homogeneous_poisson_process_with_refr_period() function is "
-                  "deprecated and will be deleted in v0.8.0 release. Use "
-                  "homogeneous_poisson_process(refractory_period=...) instead",
-                  DeprecationWarning)
-    return homogeneous_poisson_process(rate=rate, t_start=t_start,
-                                       t_stop=t_stop, as_array=as_array,
-                                       refractory_period=refr_period)
-
-
 def inhomogeneous_poisson_process(rate, as_array=False,
                                   refractory_period=None):
     """
diff --git a/elephant/unitary_event_analysis.py b/elephant/unitary_event_analysis.py
index 320acb7..e7b7c6d 100644
--- a/elephant/unitary_event_analysis.py
+++ b/elephant/unitary_event_analysis.py
@@ -55,7 +55,6 @@ from __future__ import division, print_function, unicode_literals
 
 import sys
 import warnings
-from functools import wraps
 
 import neo
 import numpy as np
@@ -66,26 +65,6 @@ import elephant.conversion as conv
 from elephant.utils import is_binary
 
 
-def decorate_deprecated_N(func):
-    @wraps(func)
-    def decorated_func(*args, **kwargs):
-        N = None
-        if 'N' in kwargs:
-            N = kwargs.pop('N')
-        elif len(args) > 1 and isinstance(args[1], int):
-            args = list(args)
-            N = args.pop(1)
-        if N is not None:
-            warnings.warn("'N' is deprecated in '{func_name}' and will be "
-                          "removed in the next Elephant release. Now 'N' is "
-                          "extracted from the data shape.".format(
-                              func_name=func.__name__), DeprecationWarning)
-        return func(*args, **kwargs)
-
-    return decorated_func
-
-
-@decorate_deprecated_N
 def hash_from_pattern(m, base=2):
     """
     Calculate for a spike pattern or a matrix of spike patterns
@@ -208,7 +187,6 @@ def inverse_hash_from_pattern(h, N, base=2):
     return m
 
 
-@decorate_deprecated_N
 def n_emp_mat(mat, pattern_hash, base=2):
     """
     Count the occurrences of spike coincidence patterns in the given spike
@@ -267,7 +245,6 @@ def n_emp_mat(mat, pattern_hash, base=2):
     return N_emp, indices
 
 
-@decorate_deprecated_N
 def n_emp_mat_sum_trial(mat, pattern_hash):
     """
     Calculate empirical number of observed patterns, summed across trials.
@@ -336,7 +313,6 @@ def n_emp_mat_sum_trial(mat, pattern_hash):
     return N_emp, idx_trials
 
 
-@decorate_deprecated_N
 def _n_exp_mat_analytic(mat, pattern_hash):
     """
     Calculates the expected joint probability for each spike pattern
@@ -357,7 +333,6 @@ def _n_exp_mat_analytic(mat, pattern_hash):
     return np.prod(pmat, axis=0) * float(mat.shape[1])
 
 
-@decorate_deprecated_N
 def _n_exp_mat_surrogate(mat, pattern_hash, n_surr=1):
     """
     Calculates the expected joint probability for each spike pattern with spike
@@ -374,7 +349,6 @@ def _n_exp_mat_surrogate(mat, pattern_hash, n_surr=1):
     return N_exp_array
 
 
-@decorate_deprecated_N
 def n_exp_mat(mat, pattern_hash, method='analytic', n_surr=1):
     """
     Calculates the expected joint probability for each spike pattern.
@@ -682,7 +656,6 @@ def _winpos(t_start, t_stop, winsize, winstep, position='left-edge'):
     return ts_winpos
 
 
-@decorate_deprecated_N
 def _UE(mat, pattern_hash, method='analytic_TrialByTrial', n_surr=1):
     """
     Return the default results of unitary events analysis
diff --git a/postBuild b/postBuild
index e753732..d760c5e 100755
--- a/postBuild
+++ b/postBuild
@@ -1,4 +1,4 @@
 # Install conda version of mpi4py for mybinder service
 conda install -c conda-forge mpi4py
 
-pip install .[tutorials]
+pip install .[tutorials,extras]


Test patch:
diff --git a/elephant/test/test_asset.py b/elephant/test/test_asset.py
index 578c121..0fee8fe 100644
--- a/elephant/test/test_asset.py
+++ b/elephant/test/test_asset.py
@@ -8,6 +8,7 @@ Unit tests for the ASSET analysis.
 
 import random
 import unittest
+import itertools
 
 import neo
 import numpy as np
@@ -268,6 +269,33 @@ class AssetTestCase(unittest.TestCase):
                           spiketrains=[st1, st2], bin_size=bin_size,
                           t_stop_x=5 * pq.ms)
 
+    def test_combinations_with_replacement(self):
+        # Test that _combinations_with_replacement yields the same tuples
+        # as in the original implementation with itertools.product(*lists)
+        # and filtering by _wrong_order.
+
+        def _wrong_order(a):
+            if a[-1] > a[0]:
+                return True
+            for i in range(len(a) - 1):
+                if a[i] < a[i + 1]:
+                    return True
+            return False
+
+        for n in range(1, 15):
+            for d in range(1, 6):
+                lists = [range(j, n + 1) for j in range(d, 0, -1)]
+                matrix_entries = list(
+                    asset._combinations_with_replacement(n=n, d=d)
+                )
+                matrix_entries_correct = [
+                    indices for indices in itertools.product(*lists)
+                    if not _wrong_order(indices)
+                ]
+                it_todo = asset._num_iterations(n=n, d=d)
+                self.assertEqual(matrix_entries, matrix_entries_correct)
+                self.assertEqual(it_todo, len(matrix_entries_correct))
+
 
 @unittest.skipUnless(HAVE_SKLEARN, 'requires sklearn')
 class AssetTestIntegration(unittest.TestCase):
diff --git a/elephant/test/test_spike_train_generation.py b/elephant/test/test_spike_train_generation.py
index 5f8ca0a..c1786e6 100644
--- a/elephant/test/test_spike_train_generation.py
+++ b/elephant/test/test_spike_train_generation.py
@@ -288,25 +288,6 @@ class HomogeneousPoissonProcessTestCase(unittest.TestCase):
         # no units provided for refractory_period
         self.assertRaises(ValueError, hpp, rate=rate, refractory_period=2)
 
-    def test_deprecated_homogeneous_poisson_process_with_refr_period(self):
-        # TODO: remove in v0.8.0
-        rate = 10 * Hz
-        t_start = 17 * ms
-        t_stop = 2 * s
-        refractory_period = 3 * ms
-        np.random.seed(28)
-        spiketrain = stgen.homogeneous_poisson_process(
-                rate=rate, t_start=t_start, t_stop=t_stop,
-                refractory_period=refractory_period)
-        np.random.seed(28)
-        spiketrain_depr = stgen.homogeneous_poisson_process_with_refr_period(
-            rate=rate, refr_period=refractory_period, t_start=t_start,
-            t_stop=t_stop
-        )
-        assert_array_almost_equal(spiketrain_depr.times, spiketrain.times)
-        self.assertAlmostEqual(spiketrain_depr.t_start, spiketrain.t_start)
-        self.assertAlmostEqual(spiketrain_depr.t_stop, spiketrain.t_stop)
-
 
 class InhomogeneousPoissonProcessTestCase(unittest.TestCase):
     def setUp(self):
diff --git a/elephant/test/test_unitary_event_analysis.py b/elephant/test/test_unitary_event_analysis.py
index 29cea23..30d2484 100644
--- a/elephant/test/test_unitary_event_analysis.py
+++ b/elephant/test/test_unitary_event_analysis.py
@@ -115,17 +115,6 @@ class UETestCase(unittest.TestCase):
                                      [0, 1, 1, 1, 1],
                                      [1, 1, 0, 1, 0]]])
 
-    @unittest.skipUnless(python_version_major == 3, "assertWarns requires 3.2")
-    def test_deprecated_N(self):
-        n_patterns = 10
-        m = np.random.randint(low=0, high=2, size=(n_patterns, 2))
-        with self.assertWarns(DeprecationWarning):
-            # check *args
-            h = ue.hash_from_pattern(m, n_patterns)
-        with self.assertWarns(DeprecationWarning):
-            # check **kwargs
-            h = ue.hash_from_pattern(m, N=n_patterns)
-
     def test_hash_default(self):
         m = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 1, 0],
                       [1, 0, 1], [0, 1, 1], [1, 1, 1]])