Skip to content

Commit

Permalink
Refactor tests (#136)
Browse files Browse the repository at this point in the history
* rewriting base file to get ride of numpy matrices.

* last modification to rewrite into full numpy integration

* rewriting mean with dotted notation and getting rid of numpy matrices

* continuing to rewrite the geodesic

* rewriting of the spatial filters

* small style correction in channelselection

* updating the use of numpy for all higher packages

* last rewriting of pyriemann file

* adding slack  notification

* finishing to update numpy ref and removing numpy matrices

* covering all functions of classification: TSclassifier, kNN

* PEP8 conformation for test clustering

* PEP 8 normalisation

* fixing last bugs for testing

* update test

* adding test for Potato

* end of modification of test units and coverage improvment.

* modifying and adapting travis file.

* updating travis file and correcting error in testing wasserstein mean

* adding pandas in the requierement for Travis

* ajout de la generation de matrice SPD

* Correcting the Karcher mean implementation if the gradient descent is initialized on one of the point of the set, generating zero-divide error.

* adding ALM to mean computation

* adding ref for the parametrized geodesic

* update avant merge

* uncomplete merge

* merge with upstream

* use fixture and parametrize for tests, avoid code redundancy, systematic test of dist and metric

* correct tangent fit bug

Co-authored-by: kjersbry <noreply@github.com>
Co-authored-by: sylvchev <noreply@github.com>

* add tsupdate check

* reduce complexity of the test to make them faster

* add flake8 to tests and examples, correct style in tests

* switch to triangular inequality to avoid random failure

* update coverage

* remove redundant tests, add coverage

* correct flake8

* correct F401,F811 import errors with fixtures

* correct w value

* remove assert_array_equal and correct variable names

Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>

* add is_spsd and get_labels fixture

* adding test for Schaefer estimator

* update test for spatial filters

Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>

* correct var name for inverse transform

* use get_labels fixture

* correct flake8

* add is_psd and is_semi_psd

* add independence test for fit/transform

* correct zeros -> empty

* Schaefer-Strimmer shrinkage covariance estimator (#59)

* rewriting base file to get ride of numpy matrices.

* last modification to rewrite into full numpy integration

* rewriting mean with dotted notation and getting rid of numpy matrices

* continuing to rewrite the geodesic

* rewriting of the spatial filters

* small style correction in channelselection

* updating the use of numpy for all higher packages

* last rewriting of pyriemann file

* adding slack  notification

* finishing to update numpy ref and removing numpy matrices

* covering all functions of classification: TSclassifier, kNN

* PEP8 conformation for test clustering

* PEP 8 normalisation

* fixing last bugs for testing

* update test

* adding test for Potato

* end of modification of test units and coverage improvment.

* modifying and adapting travis file.

* updating travis file and correcting error in testing wasserstein mean

* adding pandas in the requierement for Travis

* ajout de la generation de matrice SPD

* Correcting the Karcher mean implementation if the gradient descent is initialized on one of the point of the set, generating zero-divide error.

* adding ALM to mean computation

* adding ref for the parametrized geodesic

* update avant merge

* adding computation and test of Schaefer-Strimmer covariance estimator

* replacing loops with algebra

* adding an example

* correcting plots

* uncomplete merge

* pep8

* update test for tangent space

* correct docstring and bug

* converting notebook into py file

* correct style

* rewrite plots, reduce computation time

* update link and rename variables

* correct style

* correct plot layout

* adding Schaefer-Strimmer in whatsnew

* update test, docstring and docs from suggestions

Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>
Co-authored-by: Alexandre Gramfort <alexandre.gramfort@m4x.org>

* improve tests for covariance

* keep the same color for each estimator across figures

* correct example text

Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>

* raise error for covariance_EP and initialize RandomState in example

* correct import order

* Apply suggestions from code review

Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>

* sort import, correct spd

Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>

* switch zeros to np.empty

Co-authored-by: Alexandre Gramfort <alexandre.gramfort@m4x.org>

Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>
Co-authored-by: Alexandre Gramfort <alexandre.gramfort@m4x.org>

* add doc

* rebase on master

* correct tangent fit bug

Co-authored-by: kjersbry <noreply@github.com>
Co-authored-by: sylvchev <noreply@github.com>

* add tsupdate check

* reduce complexity of the test to make them faster

* add flake8 to tests and examples, correct style in tests

* switch to triangular inequality to avoid random failure

* update coverage

* remove redundant tests, add coverage

* correct flake8

* correct F401,F811 import errors with fixtures

* correct w value

* add is_spsd and get_labels fixture

* remove assert_array_equal and correct variable names

Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>

* adding test for Schaefer estimator

* correct var name for inverse transform

* update test for spatial filters

Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>

* use get_labels fixture

* correct flake8

* add is_psd and is_semi_psd

* add independence test for fit/transform

* correct zeros -> empty

* define is_spd/is_spsd fixtures

* clean code

* updating whats new

Co-authored-by: kjersbry <noreply@github.com>
Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com>
Co-authored-by: Alexandre Gramfort <alexandre.gramfort@m4x.org>
  • Loading branch information
4 people committed Sep 20, 2021
1 parent 3ac67b7 commit 9e12432
Show file tree
Hide file tree
Showing 22 changed files with 1,527 additions and 1,094 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/testing.yml
Expand Up @@ -33,7 +33,7 @@ jobs:
python -m pip install .[tests]
- name: Lint with flake8
run: |
flake8 examples pyriemann
flake8 examples tests pyriemann
- name: Test with pytest
run: |
pytest
2 changes: 2 additions & 0 deletions doc/whatsnew.rst
Expand Up @@ -22,6 +22,8 @@ v0.2.8.dev

- Add Schaefer-Strimmer covariance estimator in :func:`pyriemann.utils.covariance.covariances`, and an example to compare estimators

- Refactor tests + fix refit of :class:`pyriemann.tangentspace.TangentSpace`

v0.2.7 (June 2021)
------------------

Expand Down
11 changes: 4 additions & 7 deletions pyriemann/clustering.py
Expand Up @@ -80,15 +80,12 @@ class Kmeans(BaseEstimator, ClassifierMixin, ClusterMixin, TransformerMixin):
The generator used to initialize the centers. If an integer is
given, it fixes the seed. Defaults to the global numpy random
number generator.
init : 'k-means++', 'random' or an ndarray (default 'random')
init : 'random' or an ndarray (default 'random')
Method for initialization of centers.
'k-means++' : selects initial cluster centers for k-mean
clustering in a smart way to speed up convergence. See section
Notes in k_init for more details.
'random': choose k observations (rows) at random from data for
the initial centroids.
If an ndarray is passed, it should be of shape (n_clusters, n_features)
and gives the initial centers.
If an ndarray is passed, it should be of shape
(n_clusters, n_channels, n_channels) and gives the initial centers.
n_init : int, (default: 10)
Number of time the k-means algorithm will be run with different
centroid seeds. The final results will be the best output of
Expand Down Expand Up @@ -151,7 +148,7 @@ def fit(self, X, y=None):
self : Kmeans instance
The Kmean instance.
"""
if (self.init != 'random') | (self.n_init == 1):
if (self.init != 'random'):
# no need to iterate if init is not random
labels, inertia, mdm = _fit_single(X, y,
n_clusters=self.n_clusters,
Expand Down
6 changes: 3 additions & 3 deletions pyriemann/spatialfilters.py
Expand Up @@ -185,7 +185,7 @@ def __init__(self, filters, log=False):
self.log = log

def fit(self, X, y):
"""Train CSP spatial filters.
"""Train BilinearFilter spatial filters.
Parameters
----------
Expand All @@ -196,8 +196,8 @@ def fit(self, X, y):
Returns
-------
self : CSP instance
The CSP instance.
self : BilinearFilter instance
The BilinearFilter instance.
"""
self.filters_ = self.filters
return self
Expand Down
1 change: 0 additions & 1 deletion pyriemann/tangentspace.py
Expand Up @@ -164,7 +164,6 @@ def fit_transform(self, X, y=None, sample_weight=None):
the tangent space projection of the matrices.
"""
# compute mean covariance
self._check_reference_points(X)
self.reference_ = mean_covariance(X, metric=self.metric,
sample_weight=sample_weight)
return tangent_space(X, self.reference_)
Expand Down
176 changes: 164 additions & 12 deletions tests/conftest.py
@@ -1,4 +1,5 @@
import pytest
from pytest import approx
import numpy as np
from functools import partial

Expand All @@ -22,27 +23,178 @@ def requires_module(function, name, call=None):
requires_seaborn = partial(requires_module, name="seaborn")


def generate_cov(n_trials, n_channels):
"""Generate a set of cavariances matrices for test purpose"""
rs = np.random.RandomState(1234)
def generate_cov(n_trials, n_channels, rs, return_params=False):
"""Generate a set of covariances matrices for test purpose"""
diags = 2.0 + 0.1 * rs.randn(n_trials, n_channels)
A = 2 * rs.rand(n_channels, n_channels) - 1
A /= np.linalg.norm(A, axis=1)[:, np.newaxis]
covmats = np.empty((n_trials, n_channels, n_channels))
for i in range(n_trials):
covmats[i] = A @ np.diag(diags[i]) @ A.T
return covmats, diags, A
if return_params:
return covmats, diags, A
else:
return covmats


@pytest.fixture
def rndstate():
return np.random.RandomState(1234)


@pytest.fixture
def get_covmats(rndstate):
def _gen_cov(n_trials, n_chan):
return generate_cov(n_trials, n_chan, rndstate, return_params=False)

return _gen_cov


@pytest.fixture
def get_covmats_params(rndstate):
def _gen_cov_params(n_trials, n_chan):
return generate_cov(n_trials, n_chan, rndstate, return_params=True)

return _gen_cov_params


@pytest.fixture
def get_labels():
def _get_labels(n_trials, n_classes):
return np.arange(n_classes).repeat(n_trials // n_classes)

return _get_labels


def is_positive_semi_definite(X):
"""Check if all matrices are positive semi-definite.
Parameters
----------
X : ndarray, shape (..., n, n)
The set of square matrices, at least 2D ndarray.
Returns
-------
ret : boolean
True if all matrices are positive semi-definite.
"""
cs = X.shape[-1]
return np.all(np.linalg.eigvals(X.reshape((-1, cs, cs))) >= 0.0)


def is_positive_definite(X):
"""Check if all matrices are positive definite.
Parameters
----------
X : ndarray, shape (..., n, n)
The set of square matrices, at least 2D ndarray.
Returns
-------
ret : boolean
True if all matrices are positive definite.
"""
cs = X.shape[-1]
return np.all(np.linalg.eigvals(X.reshape((-1, cs, cs))) > 0.0)


def is_symmetric(X):
"""Check if all matrices are symmetric.
Parameters
----------
X : ndarray, shape (..., n, n)
The set of square matrices, at least 2D ndarray.
Returns
-------
ret : boolean
True if all matrices are symmetric.
"""
return X == approx(np.swapaxes(X, -2, -1))


@pytest.fixture
def covmats():
"""Generate covariance matrices for test"""
covmats, _, _ = generate_cov(6, 3)
return covmats
def is_spd():
"""Check if all matrices are symmetric positive-definite.
Parameters
----------
X : ndarray, shape (..., n, n)
The set of square matrices, at least 2D ndarray.
Returns
-------
ret : boolean
True if all matrices are symmetric positive-definite.
"""

def _is_spd(X):
return is_symmetric(X) and is_positive_definite(X)

return _is_spd


@pytest.fixture
def many_covmats():
"""Generate covariance matrices for test"""
covmats, _, _ = generate_cov(100, 3)
return covmats
def is_spsd():
"""Check if all matrices are symmetric positive semi-definite.
Parameters
----------
X : ndarray, shape (..., n, n)
The set of square matrices, at least 2D ndarray.
Returns
-------
ret : boolean
True if all matrices are symmetric positive semi-definite.
"""

def _is_spsd(X):
return is_symmetric(X) and is_positive_semi_definite(X)

return _is_spsd


def get_distances():
distances = [
"riemann",
"logeuclid",
"euclid",
"logdet",
"kullback",
"kullback_right",
"kullback_sym",
]
for dist in distances:
yield dist


def get_means():
means = [
"riemann",
"logeuclid",
"euclid",
"logdet",
"identity",
"wasserstein",
"ale",
"harmonic",
"kullback_sym",
]
for mean in means:
yield mean


def get_metrics():
metrics = [
"riemann",
"logeuclid",
"euclid",
"logdet",
"kullback_sym",
]
for met in metrics:
yield met
87 changes: 52 additions & 35 deletions tests/test_ajd.py
@@ -1,73 +1,90 @@
from numpy.testing import assert_array_equal
import numpy as np
from numpy.testing import assert_allclose
import pytest
from pytest import approx

from pyriemann.utils.ajd import rjd, ajd_pham, uwedge, _get_normalized_weight


def generate_cov(Nt, Ne):
"""Generate a set of cavariances matrices for test purpose"""
rs = np.random.RandomState(1234)
diags = 2.0 + 0.1 * rs.randn(Nt, Ne)
A = 2*rs.rand(Ne, Ne) - 1
A /= np.atleast_2d(np.sqrt(np.sum(A**2, 1))).T
covmats = np.empty((Nt, Ne, Ne))
for i in range(Nt):
covmats[i] = np.dot(np.dot(A, np.diag(diags[i])), A.T)
return covmats, diags, A


def test_get_normalized_weight():
def test_get_normalized_weight(get_covmats):
"""Test get_normalized_weight"""
Nt = 100
covmats, diags, A = generate_cov(Nt, 3)
n_trials, n_channels = 5, 3
covmats = get_covmats(n_trials, n_channels)
w = _get_normalized_weight(None, covmats)
assert np.isclose(np.sum(w), 1., atol=1e-10)
assert np.sum(w) == approx(1.0, abs=1e-10)


def test_get_normalized_weight_length(get_covmats):
n_trials, n_channels = 5, 3
covmats = get_covmats(n_trials, n_channels)
w = _get_normalized_weight(None, covmats)
with pytest.raises(ValueError): # not same length
_get_normalized_weight(w[:Nt//2], covmats)
_get_normalized_weight(w[: n_trials // 2], covmats)


def test_get_normalized_weight_pos(get_covmats):
n_trials, n_channels = 5, 3
covmats = get_covmats(n_trials, n_channels)
w = _get_normalized_weight(None, covmats)
with pytest.raises(ValueError): # not strictly positive weight
w[0] = 0
_get_normalized_weight(w, covmats)


def test_rjd():
"""Test rjd"""
covmats, _, _ = generate_cov(100, 3)
@pytest.mark.parametrize("ajd", [rjd, ajd_pham])
def test_ajd_shape(ajd, get_covmats):
n_trials, n_channels = 5, 3
covmats = get_covmats(n_trials, n_channels)
V, D = rjd(covmats)
assert V.shape == (3, 3)
assert D.shape == (100, 3, 3)
assert V.shape == (n_channels, n_channels)
assert D.shape == (n_trials, n_channels, n_channels)


def test_pham():
def test_pham(get_covmats):
"""Test pham's ajd"""
Nt = 100
covmats, diags, A = generate_cov(Nt, 3)
n_trials, n_channels, w_val = 5, 3, 2
covmats = get_covmats(n_trials, n_channels)
V, D = ajd_pham(covmats)
assert V.shape == (n_channels, n_channels)
assert D.shape == (n_trials, n_channels, n_channels)

w = 5 * np.ones(Nt)
Vw, Dw = ajd_pham(covmats, sample_weight=w)
Vw, Dw = ajd_pham(covmats, sample_weight=w_val * np.ones(n_trials))
assert_array_equal(V, Vw) # same result as ajd_pham without weight
assert_array_equal(D, Dw)


def test_pham_pos_weight(get_covmats):
# Test that weight must be strictly positive
n_trials, n_channels, w_val = 5, 3, 2
covmats = get_covmats(n_trials, n_channels)
w = w_val * np.ones(n_trials)
with pytest.raises(ValueError): # not strictly positive weight
w[0] = 0
ajd_pham(covmats, sample_weight=w)


def test_pham_zero_weight(get_covmats):
# now test that setting one weight to almost zero it's almost
# like not passing the matrix
V, D = ajd_pham(covmats[1:])
n_trials, n_channels, w_val = 5, 3, 2
covmats = get_covmats(n_trials, n_channels)
w = w_val * np.ones(n_trials)
V, D = ajd_pham(covmats[1:], sample_weight=w[1:])
w[0] = 1e-12

Vw, Dw = ajd_pham(covmats, sample_weight=w)
assert_allclose(V, Vw, rtol=1e-4, atol=1e-8)
assert_allclose(D, Dw[1:], rtol=1e-4, atol=1e-8)
assert V == approx(Vw, rel=1e-4, abs=1e-8)
assert D == approx(Dw[1:], rel=1e-4, abs=1e-8)


def test_uwedge():
@pytest.mark.parametrize("init", [True, False])
def test_uwedge(init, get_covmats_params):
"""Test uwedge."""
covmats, diags, A = generate_cov(100, 3)
V, D = uwedge(covmats)
V, D = uwedge(covmats, init=A)
n_trials, n_channels = 5, 3
covmats, _, A = get_covmats_params(n_trials, n_channels)
if init:
V, D = uwedge(covmats)
else:
V, D = uwedge(covmats, init=A)
assert V.shape == (n_channels, n_channels)
assert D.shape == (n_trials, n_channels, n_channels)

0 comments on commit 9e12432

Please sign in to comment.