-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy path_ged.py
More file actions
172 lines (140 loc) · 5.7 KB
/
Copy path_ged.py
File metadata and controls
172 lines (140 loc) · 5.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import numpy as np
import scipy.linalg
from ..cov import Covariance, _smart_eigh, compute_whitener
def _handle_restr_mat(C_ref, restr_type, info, rank):
"""Get restricting matrix to C_ref rank-dimensional principal subspace.
Returns matrix of shape (rank, n_chs) used to restrict or
restrict+rescale (whiten) covariances matrices.
"""
if C_ref is None or restr_type is None:
return None
if restr_type == "whitening":
C_ref_cov = Covariance(C_ref, info.ch_names, info["bads"], info["projs"], 0)
restr_mat = compute_whitener(
C_ref_cov, info, rank=rank, pca=True, verbose="error"
)[0]
elif restr_type == "restricting":
restr_mat = _get_restr_mat(C_ref, info, rank)
else:
raise ValueError(
"restr_type should either be callable or one of "
"('whitening', 'restricting')"
)
return restr_mat
def _smart_ged(S, R, restr_mat=None, R_func=None):
"""Perform smart generalized eigenvalue decomposition (GED) of S and R.
If restr_mat is provided S and R will be restricted to the principal subspace
of a reference matrix with rank r (see _handle_restr_mat), then GED is performed
on the restricted S and R and then generalized eigenvectors are transformed back
to the original space. The g-eigenvectors matrix is of shape (n_chs, r).
If callable R_func is provided the GED will be performed on (S, R_func(S,R))
"""
# Array API Standard support for hardware acceleration (TPU/GPU)
if hasattr(S, "__array_namespace__"):
xp = S.__array_namespace__()
else:
import numpy as xp
if restr_mat is None:
if xp.__name__ == "numpy":
evals, evecs = scipy.linalg.eigh(S, R)
return evals, evecs
else:
# Hardware-accelerated Cholesky path since eigh(A,B) is not in Array API
L = xp.linalg.cholesky(R)
L_inv = xp.linalg.inv(L)
C = L_inv @ S @ L_inv.T
evals, evecs_C = xp.linalg.eigh(C)
evecs = L_inv.T @ evecs_C
return evals, evecs
S_restr = restr_mat @ S @ restr_mat.T
R_restr = restr_mat @ R @ restr_mat.T
if R_func is not None:
R_restr = R_func([S_restr, R_restr])
if xp.__name__ == "numpy":
evals, evecs_restr = scipy.linalg.eigh(S_restr, R_restr)
else:
L = xp.linalg.cholesky(R_restr)
L_inv = xp.linalg.inv(L)
C = L_inv @ S_restr @ L_inv.T
evals, evecs_C = xp.linalg.eigh(C)
evecs_restr = L_inv.T @ evecs_C
evecs = restr_mat.T @ evecs_restr
return evals, evecs
def _is_cov_symm(cov, rtol=1e-7, atol=None):
if atol is None:
atol = 1e-7 * np.max(np.abs(cov))
is_symm = scipy.linalg.issymmetric(cov, rtol=rtol, atol=atol)
return is_symm
def _get_cov_def(cov, eval_tol=None):
"""Get definiteness of symmetric cov matrix.
All evals in (-eval_tol, eval_tol) will be considered zero,
while all evals smaller than -eval_tol will be considered
negative.
"""
evals = scipy.linalg.eigvalsh(cov)
if eval_tol is None:
eval_tol = 1e-7 * np.max(np.abs(evals))
if np.all(evals > eval_tol):
return "pos_def"
elif np.all(evals >= -eval_tol):
return "pos_semidef"
else:
return "indef"
def _is_cov_pos_semidef(cov, eval_tol=None):
cov_def = _get_cov_def(cov, eval_tol=eval_tol)
return cov_def in ("pos_def", "pos_semidef")
def _is_cov_pos_def(cov, eval_tol=None):
cov_def = _get_cov_def(cov, eval_tol=eval_tol)
return cov_def == "pos_def"
def _smart_ajd(covs, restr_mat=None, weights=None):
"""Perform smart approximate joint diagonalization.
If restr_mat is provided all the cov matrices will be restricted to the
principal subspace of a reference matrix with rank r (see _handle_restr_mat),
then GED is performed on the restricted S and R and then generalized eigenvectors
are transformed back to the original space.
The matrix of generalized eigenvectors is of shape (n_chs, r).
"""
from .csp import _ajd_pham
if restr_mat is None:
are_all_pos_def = all([_is_cov_pos_def(cov) for cov in covs])
if not are_all_pos_def:
raise ValueError(
"If C_ref is not provided by covariance estimator, "
"all the covs should be positive definite"
)
evecs, D = _ajd_pham(covs)
return evecs
else:
are_all_pos_semidef = all([_is_cov_pos_semidef(cov) for cov in covs])
if not are_all_pos_semidef:
raise ValueError(
"All the covs should be positive semi-definite for "
"approximate joint diagonalization"
)
covs = np.array([restr_mat @ cov @ restr_mat.T for cov in covs], float)
evecs_restr, D = _ajd_pham(covs)
evecs = _normalize_eigenvectors(evecs_restr.T, covs, weights)
evecs = restr_mat.T @ evecs
return evecs
def _get_restr_mat(C, info, rank):
"""Get matrix restricting covariance to rank-dimensional principal subspace of C."""
_, ref_evecs, mask = _smart_eigh(
C,
info,
rank,
proj_subspace=True,
do_compute_rank=False,
log_ch_type="data",
)
restr_mat = ref_evecs[mask]
return restr_mat
def _normalize_eigenvectors(evecs, covs, sample_weights):
# Here we apply an euclidean mean. See pyRiemann for other metrics
mean_cov = np.average(covs, axis=0, weights=sample_weights)
for ii in range(evecs.shape[1]):
tmp = np.dot(np.dot(evecs[:, ii].T, mean_cov), evecs[:, ii])
evecs[:, ii] /= np.sqrt(tmp)
return evecs