Skip to content

Commit

Permalink
fixed BinnedSpikeTrain.bin_edges property (NeuralEnsemble#257)
Browse files Browse the repository at this point in the history
* fixed BinnedSpikeTrain.bin_edges property

* bin_edges: include t_stop if spiketrain duration is divisible by binsize

* BinnedSpikeTrain: warn if binning discards the spikes in the last (excluded) bin

* reuse is_binary func from utility.py
  • Loading branch information
dizcza committed Nov 11, 2019
1 parent bd684f5 commit 82f307f
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 74 deletions.
99 changes: 45 additions & 54 deletions elephant/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@

from __future__ import division, print_function

import warnings

import neo
import scipy
import scipy.sparse as sps
import numpy as np
import quantities as pq
import scipy
import scipy.sparse as sps

from elephant.utils import is_binary


def binarize(spiketrain, sampling_rate=None, t_start=None, t_stop=None,
Expand Down Expand Up @@ -429,7 +433,7 @@ def __init__(self, spiketrains, binsize=None, num_bins=None, t_start=None,
self._sparse_mat_u = None
# Check all parameter, set also missing values
if self.is_binned:
self.num_bins = np.array(spiketrains).shape[1]
self.num_bins = np.shape(spiketrains)[1]
self._calc_start_stop(spiketrains)
self._check_init_params(
self.binsize, self.num_bins, self.t_start, self.t_stop)
Expand All @@ -438,6 +442,14 @@ def __init__(self, spiketrains, binsize=None, num_bins=None, t_start=None,
# Now create sparse matrix
self._convert_to_binned(spiketrains)

if self.is_spiketrain:
n_spikes = sum(map(len, spiketrains))
n_spikes_binned = sum(map(len, self.spike_indices))
if n_spikes != n_spikes_binned:
warnings.warn("Binning discarded {n} last spike(s) in the "
"input spiketrain.".format(
n=n_spikes - n_spikes_binned))

# =========================================================================
# There are four cases the given parameters must fulfill
# Each parameter must be a combination of following order or it will raise
Expand Down Expand Up @@ -546,7 +558,7 @@ def _check_consistency(self, spiketrains, binsize, num_bins, t_start,
"at least one of the parameter which are "
"None.\n"
"t_start: %s, t_stop: %s, binsize: %s, "
"numb_bins: %s" % (
"num_bins: %s" % (
self.t_start,
self.t_stop,
self.binsize,
Expand Down Expand Up @@ -584,21 +596,25 @@ def bin_edges(self):
"""
Returns all time edges with :attr:`num_bins` bins as a quantity array.
The borders of all time steps between start and stop [start, stop]
with:attr:`num_bins` bins are regarded as edges.
The border of the last bin is included.
The borders of all time steps between :attr:`t_start` and
:attr:`t_stop` with a step :attr:`binsize`. It is crucial for many
analyses that all bins have the same size, so if
:attr:`t_stop` - :attr:`t_start` is not divisible by :attr:`binsize`,
there will be some leftover time at the end
(see https://github.com/NeuralEnsemble/elephant/issues/255).
The length of the returned array should match :attr:`num_bins`.
Returns
-------
bin_edges : quantities.Quantity array
All edges in interval [start, stop] with :attr:`num_bins` bins
are returned as a quantity array.
bin_edges : pq.Quantity
All edges in interval [:attr:`t_start`, :attr:`t_stop`] with
:attr:`num_bins` bins are returned as a quantity array.
"""
return pq.Quantity(np.linspace(self.t_start.rescale('s').magnitude,
self.t_stop.rescale('s').magnitude,
self.num_bins + 1, endpoint=True),
units='s').rescale(self.binsize.units)
t_start = self.t_start.rescale(self.binsize.units).magnitude
bin_edges = np.linspace(t_start, t_start + self.num_bins *
self.binsize.magnitude,
num=self.num_bins + 1, endpoint=True)
return pq.Quantity(bin_edges, units=self.binsize.units)

@property
def bin_centers(self):
Expand Down Expand Up @@ -699,7 +715,7 @@ def is_binary(self):
sparse (i.e. only one spike per bin at maximum).
"""

return _check_binary_matrix(self.lst_input)
return is_binary(self.lst_input)

def to_bool_array(self):
"""
Expand Down Expand Up @@ -817,9 +833,6 @@ def _convert_to_binned(self, spiketrains):
indices = []
# data
counts = []
# to be downwards compatible compare numpy versions, if the used
# version is smaller than v1.9 use different functions
smaller_version = StrictVersion(np.__version__) < '1.9.0'
for idx, elem in enumerate(spiketrains):
ev = elem.view(pq.Quantity)
scale = np.array(((ev - self.t_start).rescale(
Expand All @@ -828,11 +841,7 @@ def _convert_to_binned(self, spiketrains):
ev <= self.t_stop.rescale(self.binsize.units))
filled_tmp = scale[la]
filled_tmp = filled_tmp[filled_tmp < self.num_bins]
if smaller_version:
f = np.unique(filled_tmp)
c = np.bincount(f.searchsorted(filled_tmp))
else:
f, c = np.unique(filled_tmp, return_counts=True)
f, c = np.unique(filled_tmp, return_counts=True)
filled.extend(f)
counts.extend(c)
indices.extend([idx] * len(f))
Expand All @@ -843,18 +852,6 @@ def _convert_to_binned(self, spiketrains):
self._sparse_mat_u = csr_matrix


def _check_binary_matrix(binary_matrix):
""" Checks if given matrix is binary """
# Convert to numpy array if binary_matrix is a list
if not isinstance(binary_matrix, np.ndarray):
binary_matrix = np.array(binary_matrix)
# Check for binary rows
for row in binary_matrix:
if not len(np.where(row == 1)[0]) == np.count_nonzero(row):
return False
return True


def _check_neo_spiketrain(matrix):
"""
Checks if given input contains neo spiketrain objects
Expand All @@ -863,30 +860,24 @@ def _check_neo_spiketrain(matrix):
if isinstance(matrix, neo.SpikeTrain):
return True
# Check for list or tuple
elif isinstance(matrix, list) or isinstance(matrix, tuple):
ts = True
for m in matrix:
if not isinstance(m, neo.SpikeTrain):
return False
return ts
elif isinstance(matrix, (list, tuple)):
return all(map(_check_neo_spiketrain, matrix))
else:
return False


def _check_binned_array(matrix):
"""
Checks if given input is a binned array
"""
# Try to convert to numpy array
if isinstance(matrix, list):
matrix = np.array(matrix)
# Check for numpy arrays
if isinstance(matrix, np.ndarray):
# Check for proper dimension MxN
if matrix.ndim == 2:
return True
elif matrix.dtype == np.dtype('O') or matrix.ndim != 2:
raise TypeError('Please check the dimensions of the input, '
'it should be an MxN array, '
'the input has the shape: {}'.format(matrix.shape))
matrix = np.asarray(matrix)
# Check for proper dimension MxN
if matrix.ndim == 2:
return True
elif matrix.dtype == np.dtype('O'):
raise TypeError('Please check the dimensions of the input, '
'it should be an MxN array, '
'the input has the shape: {}'.format(matrix.shape))
else:
# Otherwise not supported
return TypeError('Input not supported. Please check again')
19 changes: 17 additions & 2 deletions elephant/test/test_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
:license: Modified BSD, see LICENSE.txt for details.
"""

import sys
import unittest

import neo
import numpy as np
from numpy.testing.utils import assert_array_almost_equal
import quantities as pq
from numpy.testing.utils import assert_array_almost_equal, assert_array_equal

import elephant.conversion as cv

python_version_major = sys.version_info.major


def get_nearest(times, time):
return (np.abs(times - time)).argmin()
Expand Down Expand Up @@ -173,6 +176,16 @@ def test_binariz_without_sampling_rate_valueerror(self):
t_start=0., t_stop=pq.Quantity(10, 'ms'))
self.assertRaises(ValueError, cv.binarize, st1)

@unittest.skipUnless(python_version_major == 3, "assertWarns requires 3.2")
def test_bin_edges(self):
st = neo.SpikeTrain(times=np.array([2.5]) * pq.s, t_start=0 * pq.s,
t_stop=3 * pq.s)
with self.assertWarns(UserWarning):
bst = cv.BinnedSpikeTrain(st, binsize=2 * pq.s, t_start=0 * pq.s,
t_stop=3 * pq.s)
assert_array_equal(bst.bin_edges, [0., 2.] * pq.s)
assert_array_equal(bst.spike_indices, [[]]) # no binned spikes


class TimeHistogramTestCase(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -236,6 +249,7 @@ def test_binned_spiketrain_neg_times(self):
np.array([1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0])]
self.assertTrue(np.array_equal(x.to_bool_array(), y))

@unittest.skipUnless(python_version_major == 3, "assertWarns requires 3.2")
def test_binned_spiketrain_neg_times_list(self):
a = neo.SpikeTrain(
[-6.5, 0.5, 0.7, 1.2, 3.1, 4.3, 5.5, 6.7] * pq.s,
Expand All @@ -246,7 +260,8 @@ def test_binned_spiketrain_neg_times_list(self):
c = [a, b]

binsize = self.binsize
x_bool = cv.BinnedSpikeTrain(c, binsize=binsize)
with self.assertWarns(UserWarning):
x_bool = cv.BinnedSpikeTrain(c, binsize=binsize)
y_bool = [[0, 1, 1, 0, 1, 1, 1, 1],
[1, 0, 1, 1, 0, 1, 1, 0]]

Expand Down
22 changes: 4 additions & 18 deletions elephant/unitary_event_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import scipy

import elephant.conversion as conv
from elephant.utils import is_binary


def decorate_deprecated_N(func):
Expand All @@ -48,21 +49,6 @@ def decorated_func(*args, **kwargs):
return decorated_func


def _is_binary(array):
"""
Parameters
----------
array: np.ndarray
Returns
-------
bool
Whether the input array is binary or nor.
"""
return ((array == 0) | (array == 1)).all()


@decorate_deprecated_N
def hash_from_pattern(m, base=2):
"""
Expand Down Expand Up @@ -116,7 +102,7 @@ def hash_from_pattern(m, base=2):
n_neurons = m.shape[0]

# check the entries of the matrix
if not _is_binary(m):
if not is_binary(m):
raise ValueError('Patterns should be binary: 0 or 1')

# generate the representation
Expand Down Expand Up @@ -234,7 +220,7 @@ def n_emp_mat(mat, pattern_hash, base=2):
[array([]), array([0, 3])]
"""
# check if the mat is zero-one matrix
if not _is_binary(mat):
if not is_binary(mat):
raise ValueError("entries of mat should be either one or zero")
h = hash_from_pattern(mat, base=base)
N_emp = np.zeros(len(pattern_hash))
Expand Down Expand Up @@ -306,7 +292,7 @@ def n_emp_mat_sum_trial(mat, pattern_hash):

idx_trials = []
# check if the mat is zero-one matrix
if not _is_binary(mat):
if not is_binary(mat):
raise ValueError("entries of mat should be either one or zero")

for mat_tr in mat:
Expand Down
17 changes: 17 additions & 0 deletions elephant/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import numpy as np


def is_binary(array):
"""
Parameters
----------
array: np.ndarray or list
Returns
-------
bool
Whether the input array is binary or nor.
"""
array = np.asarray(array)
return ((array == 0) | (array == 1)).all()

0 comments on commit 82f307f

Please sign in to comment.