Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of Block Covariance estimation #154

Merged
merged 27 commits into from Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d27e99e
fix for new sklearn version
Mar 29, 2021
5ca7137
fetch upstream
gabelstein Nov 19, 2021
53cf78e
Added Tests and all for block covariance estimation.
gabelstein Dec 8, 2021
93a3398
Merge branch 'master' of https://github.com/pyRiemann/pyRiemann into …
gabelstein Dec 8, 2021
111baaf
Apply suggestions from code review
gabelstein Dec 10, 2021
0716fdb
flake8
gabelstein Dec 10, 2021
3ec030b
Update whatsnew.rst
gabelstein Dec 10, 2021
e571aba
Merge branch 'block_cov' of https://github.com/gabelstein/pyRiemann i…
gabelstein Dec 10, 2021
081eb80
Update plot_classify_ssvep_mdm.py
gabelstein Dec 10, 2021
0f1abae
Merge branch 'master' into block_cov
gabelstein Dec 13, 2021
7d035dd
CI
gabelstein Dec 13, 2021
fd6d793
Apply suggestions from code review
gabelstein Dec 13, 2021
8ecafac
Fix Docstrings
gabelstein Dec 13, 2021
0b189b8
flake8
gabelstein Dec 13, 2021
7505318
Extended Docstrings
gabelstein Dec 14, 2021
b8945dd
set whatsnew in the chronological order
qbarthelemy Dec 14, 2021
5e41262
Apply suggestions from code review
gabelstein Dec 15, 2021
713fbb3
Move valid block size checking to block_covariances
gabelstein Dec 15, 2021
7f6fea5
Delete workspace.xml
gabelstein Dec 15, 2021
ebf00fc
suggestions
gabelstein Dec 16, 2021
006edfa
flake8
gabelstein Dec 17, 2021
455e040
Update pyriemann/utils/covariance.py
gabelstein Dec 17, 2021
75821a9
Merge branch 'block_cov' of https://github.com/gabelstein/pyRiemann i…
gabelstein Dec 17, 2021
c836d1e
Update test_estimation.py
gabelstein Dec 17, 2021
7be9818
move contribution and tests
qbarthelemy Dec 17, 2021
9a59c11
complete doc
qbarthelemy Dec 17, 2021
857e6df
correct flake8
qbarthelemy Dec 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
338 changes: 338 additions & 0 deletions .idea/workspace.xml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions doc/whatsnew.rst
Expand Up @@ -36,6 +36,8 @@ v0.2.8.dev

- Add masked and NaN means with Riemannian metric: :func:`pyriemann.utils.mean.maskedmean_riemann` and :func:`pyriemann.utils.mean.nanmean_riemann`

- Add block covariance matrix: :func:`pyriemann.utils.covariance.block_covariances`

- Add ``corr`` option in :func:`pyriemann.utils.covariance.normalize`, to normalize covariance into correlation matrices
qbarthelemy marked this conversation as resolved.
Show resolved Hide resolved

v0.2.7 (June 2021)
Expand Down
5 changes: 3 additions & 2 deletions examples/SSVEP/plot_classify_ssvep_mdm.py
Expand Up @@ -23,7 +23,7 @@
from mne.datasets import fetch_dataset

# pyriemann import
from pyriemann.estimation import Covariances
from pyriemann.estimation import BlockCovariances
from pyriemann.utils.mean import mean_riemann
from pyriemann.classification import MDM

Expand Down Expand Up @@ -190,7 +190,8 @@ def _bandpass_filter(signal, lowcut, highcut):
# The covariance matrices will be estimated using the Ledoit-Wolf shrinkage
# estimator on the extended signal.

cov_ext_trials = Covariances(estimator='lwf').transform(epochs.get_data())
cov_ext_trials = BlockCovariances(estimator='lwf',
block_size=8).transform(epochs.get_data())

# This plot shows an example of a covariance matrix observed for each class:

Expand Down
88 changes: 87 additions & 1 deletion pyriemann/estimation.py
Expand Up @@ -3,7 +3,7 @@

from .spatialfilters import Xdawn
from .utils.covariance import (covariances, covariances_EP, cospectrum,
coherence)
coherence, block_covariances)
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.covariance import shrunk_covariance

Expand Down Expand Up @@ -651,3 +651,89 @@ def transform(self, X):
covmats[ii] = shrunk_covariance(x, self.shrinkage)

return covmats


class BlockCovariances(BaseEstimator, TransformerMixin):
"""Estimation of block covariance matrix.
Perform a block covariance matrix estimation for each given trial. The
resulting matrices are block diagonal matrices.

The blocks on the diagonal are calculated as individual covariance
matrices for a subset of channels using the given the estimator.
Varying block sized possible by passing a list to allow incorporation
of different modalities with different number of channels (e.g. EEG,
ECoG, LFP, EMG) with their own respective covariance matrices.

Parameters
gabelstein marked this conversation as resolved.
Show resolved Hide resolved
----------
estimator : string (default: 'scm')
covariance matrix estimator. For regularization consider 'lwf' or 'oas'
For a complete list of estimator, see `utils.covariance`.
block_size : list or int
Sizes of individual blocks given as int for same-size block or list for
varying block sizes.

gabelstein marked this conversation as resolved.
Show resolved Hide resolved
See Also
gabelstein marked this conversation as resolved.
Show resolved Hide resolved
--------
ERPCovariances
XdawnCovariances
CospCovariances
HankelCovariances
gabelstein marked this conversation as resolved.
Show resolved Hide resolved
"""

def __init__(self, block_size, estimator='scm'):
"""Init."""
self.estimator = estimator
self.block_size = block_size

def fit(self, X, y=None):
"""Fit.
gabelstein marked this conversation as resolved.
Show resolved Hide resolved

Do nothing. For compatibility purpose.

Parameters
gabelstein marked this conversation as resolved.
Show resolved Hide resolved
----------
X : ndarray, shape (n_matrices, n_channels, n_samples)
ndarray of matrices.
y : ndarray shape (n_matrices,)
labels corresponding to each trial, not used.

Returns
gabelstein marked this conversation as resolved.
Show resolved Hide resolved
-------
self : Covariances instance
The Covariances instance.
"""
return self

def transform(self, X):
"""Estimate block covariance matrices.

Parameters
gabelstein marked this conversation as resolved.
Show resolved Hide resolved
----------
X : ndarray, shape (n_matrices, n_channels, n_samples)
ndarray of matrices.

Returns
gabelstein marked this conversation as resolved.
Show resolved Hide resolved
-------
covmats : ndarray, shape (n_matrices, n_channels, n_channels)
ndarray of covariance matrices for each trials.
"""
n_matrices, n_channels, n_times = X.shape

if isinstance(self.block_size, int):
n_blocks = n_channels // self.block_size

if n_blocks*self.block_size != n_channels:
gabelstein marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError('block_size must be divisor '
'of number of channels of X.')

blocks = [self.block_size for b in range(n_blocks)]

else:
qbarthelemy marked this conversation as resolved.
Show resolved Hide resolved
blocks = self.block_size

if np.sum(blocks) != n_channels:
raise ValueError('Sum of individual block sizes '
'must match number of channels of X.')
qbarthelemy marked this conversation as resolved.
Show resolved Hide resolved

return block_covariances(X, blocks, self.estimator)
44 changes: 44 additions & 0 deletions pyriemann/utils/covariance.py
@@ -1,6 +1,7 @@
import warnings

import numpy as np
from scipy.linalg import block_diag

from sklearn.covariance import oas, ledoit_wolf, fast_mcd, empirical_covariance
from .test import is_square
Expand Down Expand Up @@ -444,6 +445,49 @@ def coherence(X, window=128, overlap=0.75, fmin=None, fmax=None, fs=None,
return C, freqs


def block_covariances(X, blocks, estimator='cov'):
"""Compute block diagonal covariance.

Calculates block diagonal matrices where each block is a covariance
matrix of a subset of channels.
Block sizes are passed as a list of integers and can vary. The sum
of block sizes must equal the number of channels in X.

Parameters
----------
X : ndarray, shape (n_matrices, n_channels, n_times)
Multi-channel time-series.
blocks: list of int
list of block sizes.
estimator : {'cov', 'scm', 'lwf', 'oas', 'mcd', 'sch', 'corr'} \
(default: 'scm')
Covariance matrix estimator, see
:func:`pyriemann.utils.covariance.covariances`.

Returns
-------
C : ndarray, shape (n_matrices, n_channels, n_channels)
Block diagonal covariance matrices.

Notes
-----
.. versionadded:: 0.2.8
"""
est = _check_est(estimator)
n_matrices, n_channels, n_times = X.shape

covmats = np.empty((n_matrices, n_channels, n_channels))
for i in range(n_matrices):
blockcov = []
idx_start = 0
for j in blocks:
blockcov.append(est(X[i, idx_start:idx_start+j, :]))
idx_start += j
covmats[i, :, :] = block_diag(*tuple(blockcov))
gabelstein marked this conversation as resolved.
Show resolved Hide resolved

return covmats


###############################################################################


Expand Down
45 changes: 45 additions & 0 deletions tests/test_estimation.py
Expand Up @@ -6,6 +6,7 @@
HankelCovariances,
Coherences,
Shrinkage,
BlockCovariances
)
import pytest

Expand Down Expand Up @@ -206,3 +207,47 @@ def test_shrinkage(shrinkage, rndstate):
assert sh.get_params() == dict(shrinkage=shrinkage)
assert covmats.shape == (n_trials, n_channels, n_channels)
assert is_spd(covmats)


@pytest.mark.parametrize("estimator", estim)
def test_block_covariances_est(estimator, rndstate):
"""Test Covariances"""
n_trials, n_channels, n_times = 2, 12, 100
x = rndstate.randn(n_trials, n_channels, n_times)
cov = BlockCovariances(block_size=6, estimator=estimator)
cov.fit(x)
covmats = cov.fit_transform(x)
assert cov.get_params() == dict(block_size=6, estimator=estimator)
assert covmats.shape == (n_trials, n_channels, n_channels)
assert is_spd(covmats)


@pytest.mark.parametrize("block_size", [1, 6, [4, 8]])
def test_block_covariances_blocks(block_size, rndstate):
"""Test Covariances"""
n_trials, n_channels, n_times = 2, 12, 100
x = rndstate.randn(n_trials, n_channels, n_times)
cov = BlockCovariances(block_size=block_size)
cov.fit(x)
covmats = cov.fit_transform(x)
assert cov.get_params() == dict(block_size=block_size, estimator='scm')
assert covmats.shape == (n_trials, n_channels, n_channels)
assert is_spd(covmats)


def test_block_covariances_int_value_error(rndstate):
"""Test Covariances"""
n_trials, n_channels, n_times = 2, 12, 100
x = rndstate.randn(n_trials, n_channels, n_times)
cov = BlockCovariances(block_size=5)
with pytest.raises(ValueError):
cov.fit_transform(x)


def test_block_covariances_array_value_error(rndstate):
"""Test Covariances"""
n_trials, n_channels, n_times = 2, 12, 100
x = rndstate.randn(n_trials, n_channels, n_times)
cov = BlockCovariances(block_size=[4, 4, 5])
with pytest.raises(ValueError):
cov.fit_transform(x)
44 changes: 43 additions & 1 deletion tests/test_utils_covariance.py
@@ -1,12 +1,13 @@
from numpy.testing import assert_array_almost_equal
import numpy as np
from scipy.linalg import block_diag
from scipy.signal import welch, csd, coherence as coherence_sp
import pytest

from pyriemann.utils.covariance import (
covariances, covariances_EP, covariances_X, eegtocov,
cross_spectrum, cospectrum, coherence,
normalize, get_nondiag_weight
normalize, get_nondiag_weight, block_covariances
)
from pyriemann.utils.test import is_real, is_hermitian

Expand Down Expand Up @@ -275,6 +276,47 @@ def test_covariances_coherence(coh, rndstate):
assert c[0, 3, foi] == pytest.approx(0.0)


@pytest.mark.parametrize(
'estimator', ['oas', 'lwf', 'scm', 'corr', 'mcd',
'sch', np.cov, 'truc', None]
)
def test_block_covariances_est(estimator, rndstate):
"""Test covariance for multiple estimators"""
n_matrices, n_channels, n_times = 2, 12, 100
x = rndstate.randn(n_matrices, n_channels, n_times)

if estimator is None:
cov = block_covariances(x, [4, 4, 4])
assert cov.shape == (n_matrices, n_channels, n_channels)
elif estimator == 'truc':
with pytest.raises(ValueError):
block_covariances(x, [4, 4, 4], estimator=estimator)
else:
cov = block_covariances(x, [4, 4, 4], estimator=estimator)
assert cov.shape == (n_matrices, n_channels, n_channels)


def test_block_covariances(rndstate):
"""Test block covariance"""
n_matrices, n_channels, n_times = 2, 12, 100
x = rndstate.randn(n_matrices, n_channels, n_times)

cov = block_covariances(x, [12], estimator='cov')
assert_array_almost_equal(cov, covariances(x, estimator='cov'))

cov = block_covariances(x, [6, 6], estimator='cov')
cov2 = covariances(x, estimator='cov')
covcomp = block_diag(*(cov2[0, :6, :6], cov2[0, 6:12, 6:12]))
assert_array_almost_equal(cov[0], covcomp)

cov = block_covariances(x, [3, 5, 4], estimator='cov')
cov2 = covariances(x, estimator='cov')
covcomp = block_diag(*(cov2[0, :3, :3],
cov2[0, 3:8, 3:8],
cov2[0, 8:12, 8:12]))
assert_array_almost_equal(cov[0], covcomp)


@pytest.mark.parametrize('norm', ['corr', 'trace', 'determinant'])
def test_normalize_shapes(norm, rndstate):
"""Test normalize shapes"""
Expand Down