From 92faefcc2450748d1b209c1b2cf17e36dfa3cfdf Mon Sep 17 00:00:00 2001 From: Enning Yang Date: Mon, 4 May 2026 10:47:16 -0400 Subject: [PATCH] Allow spectrum='nonzero' with no zero eigenvalue (#112) The previous unconditional `if n_zero == 0: raise ValueError` blocked legitimate use cases like hippocampal-subfield analyses where the weight matrix has no kernel (no medial wall). Simply removing the guard, as proposed in #112, would silently corrupt the spectrum='all' branch (it would drop the largest eigenvalue instead of a zero one). Make the guard branch-aware: spectrum='all' still requires a zero eigenvalue and raises a clearer error pointing at spectrum='nonzero'; spectrum='nonzero' now accepts kernel-free matrices and returns all eigenvalues. Adds two regression tests covering both branches. Co-authored-by: Jordan DeKraker --- brainspace/null_models/moran.py | 8 ++++++-- brainspace/tests/test_null_models.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/brainspace/null_models/moran.py b/brainspace/null_models/moran.py index 54da5d90..9c722b9e 100644 --- a/brainspace/null_models/moran.py +++ b/brainspace/null_models/moran.py @@ -99,8 +99,12 @@ def compute_mem(w, n_ring=1, spectrum='nonzero', tol=1e-10): mask_zero = ev_abs < tol n_zero = np.count_nonzero(mask_zero) - if n_zero == 0: - raise ValueError('Weight matrix has no zero eigenvalue.') + if spectrum == 'all' and n_zero == 0: + raise ValueError( + "Weight matrix has no zero eigenvalue; spectrum='all' " + "requires at least one. Use spectrum='nonzero' for matrices " + "without a kernel (e.g., surfaces with no medial wall)." + ) # Multiple zero eigenvalues if spectrum == 'all': diff --git a/brainspace/tests/test_null_models.py b/brainspace/tests/test_null_models.py index 0219d3aa..078db38e 100644 --- a/brainspace/tests/test_null_models.py +++ b/brainspace/tests/test_null_models.py @@ -74,6 +74,31 @@ def test_moran(): assert np.allclose(r2, msr.randomize(feats)) +def test_compute_mem_no_zero_eigvals_nonzero_branch(): + """spectrum='nonzero' must accept matrices with no zero eigenvalues (#112). + + Surfaces with no medial wall (e.g. hippocampal subfields) produce weight + matrices with no kernel; the previous unconditional ValueError blocked + that legitimate use case. + """ + rs = np.random.RandomState(0) + a = rs.randn(20, 20) + w = a @ a.T # positive definite -> no zero eigenvalues + mem, ev = compute_mem(w, spectrum='nonzero', tol=1e-10) + assert mem.shape[0] == 20 + assert ev.shape[0] == mem.shape[1] + assert np.all(np.abs(ev) > 1e-10) + + +def test_compute_mem_no_zero_eigvals_all_branch_still_raises(): + """spectrum='all' still requires a zero eigenvalue (#112).""" + rs = np.random.RandomState(1) + a = rs.randn(20, 20) + w = a @ a.T + with pytest.raises(ValueError, match="spectrum='all'"): + compute_mem(w, spectrum='all', tol=1e-10) + + def test_spin(): # Create dummy spheres or left and right hemispheres sphere_lh = wrap_vtk(vtk.vtkSphereSource, radius=20, thetaResolution=10,