Skip to content

Commit 5f507e6

Browse files
authored
[FIX] riemann ASR eigpair mismatch in nonlinear_eigenspace (#100)
## Summary - fix issue #99 by returning eigenvalues consistent with optimized eigenvectors in nonlinear_eigenspace - compute eigenvalues from Rayleigh quotients of the optimized basis (diag(X.T @ L @ X)) - add regression test to ensure returned eigenvalues match returned eigenvectors ## Why The previous implementation returned eigenvalues from a random-initialization matrix (S0) but eigenvectors from the optimizer solution (Xsol.point), producing mismatched eigpairs and unstable component ordering in riemann ASR. ## Validation - reproduced issue behavior before fix (riemann selecting clean channels) - after fix, reproduced expected behavior (no inverted channel selection) - tests run: - tests/test_cov.py::test_nonlinear_eigenspace_consistent_eigpairs - tests/test_asr.py with riemann-focused selection Closes #99
1 parent 72054a4 commit 5f507e6

2 files changed

Lines changed: 39 additions & 2 deletions

File tree

‎meegkit/utils/covariances.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,5 +510,15 @@ def ehess(X, U):
510510
problem = Problem(manifold=manifold, cost=cost, euclidean_gradient=egrad,
511511
euclidean_hessian=ehess)
512512
Xsol = solver.run(problem, initial_point=U0)
513-
514-
return S0, Xsol.point
513+
X = Xsol.point
514+
515+
# The Grassmann manifold only fixes the *subspace* spanned by X, so X carries
516+
# an arbitrary internal rotation. Diagonalise the k×k projected matrix to
517+
# obtain proper Ritz pairs: after rotation, X.T @ L @ X is diagonal and each
518+
# column of X is a genuine approximate eigenvector of L.
519+
T = X.T @ L @ X
520+
T = (T + T.T) / 2 # symmetrise against numerical noise
521+
S, R = linalg.eigh(T)
522+
X = X @ R
523+
524+
return np.real(S), np.real(X)

‎tests/test_cov.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
import pytest
23
from numpy.testing import assert_almost_equal
34

45
from meegkit.utils import convmtx, tscov, tsxcov
@@ -99,6 +100,32 @@ def test_convmtx():
99100
])
100101
)
101102

103+
104+
def test_nonlinear_eigenspace_consistent_eigpairs():
105+
"""Each returned eigenvalue must match the Rayleigh quotient of its column."""
106+
pytest.importorskip("pymanopt")
107+
from meegkit.utils.covariances import nonlinear_eigenspace
108+
109+
A = rng.standard_normal((6, 6))
110+
L = A.T @ A + np.eye(6) * 1e-6
111+
112+
S, X = nonlinear_eigenspace(L, 6)
113+
# X.T @ L @ X should be (near) diagonal; check the diagonal matches S
114+
# element-wise so a permutation between S and X columns cannot slip through.
115+
rayleigh = np.real(np.diag(X.T @ L @ X))
116+
117+
np.testing.assert_allclose(
118+
S,
119+
rayleigh,
120+
rtol=1e-3,
121+
atol=1e-6,
122+
)
123+
124+
# Off-diagonal entries of the projected matrix should be negligible.
125+
proj = X.T @ L @ X
126+
off_diag = proj - np.diag(np.diag(proj))
127+
assert np.max(np.abs(off_diag)) < 1e-6 * np.max(np.abs(np.diag(proj)))
128+
102129
if __name__ == "__main__":
103130
import pytest
104131
pytest.main([__file__])

0 commit comments

Comments
 (0)