removed valueError for nonzero eigenvalues#112
Conversation
|
I'd be more curious as to why this is happening in the first place. The MSM manuscript is not ambiguous:
Unfortunately, this seems like the kind of deep dive that's beyond what I have time for these days. That being said, I'd see if you have duplicate data in your input. My first instinct is that that's what's going on. |
|
Yes there are likely duplicate values, but this is valid in my case. All my vertices were generated on one regular meshgrid (and then some vertices were removed, leading to appropriate face sizes in hippocampal meshes). Thus, some vertices will have identical "n_ring=1" distances (or in other words, many face sizes are identical) It looks like the next few lines are meant to handle cases with many 0 Eigenvalues anyway, so I really think this line can just be dropped. Not sure why the CI has broken |
|
@OualidBenkarim What are your thoughts; I think we could consider just changing it to a warning. This shouldn't happen, and is likely indicative of another issue, but if the end-user believes their use-case is an exception we can allow it. FYI these are my fairly uninformed thoughts coalesced in the ten minutes I could find to look at this, so don't put too much stock in my opinion on this :P |
|
There's the tol argument to control that. Maybe increasing the tolerance might fix the issue |
There was a problem hiding this comment.
Pull request overview
This PR updates Moran spectral randomization (compute_mem) to avoid raising an exception when the doubly-centered weight matrix yields no eigenvalues classified as “zero” under the provided tolerance—motivated by running on hippocampal surfaces without masked regions.
Changes:
- Removed the
ValueErrorpreviously raised whenn_zero == 0(no eigenvalues belowtol).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ev_abs = np.abs(ev) | ||
| mask_zero = ev_abs < tol | ||
| n_zero = np.count_nonzero(mask_zero) | ||
|
|
There was a problem hiding this comment.
With the ValueError removed, spectrum='nonzero' now returns all eigenvectors when n_zero == 0 (i.e., nothing is filtered by mask_zero). That contradicts the function’s contract of removing the (at least one) zero/constant mode from the doubly-centered matrix and can change downstream behavior (e.g., component count becomes n_vertices instead of the expected <= n_vertices-1). Consider treating the smallest-|eigenvalue| entry as zero when n_zero == 0 (e.g., set mask_zero[ev_abs.argmin()] = True) so the constant mode is still removed even when numerical precision/tol prevents exact zeros.
| # If no eigenvalue is below tolerance but spectrum=='nonzero', still | |
| # remove at least one mode (the one with smallest |eigenvalue|), | |
| # corresponding to the constant component of the doubly centered matrix. | |
| if spectrum == 'nonzero' and n_zero == 0: | |
| idx_min = ev_abs.argmin() | |
| mask_zero[idx_min] = True | |
| n_zero = 1 |
| # Remove zero eigen-value/vector | ||
| ev_abs = np.abs(ev) | ||
| mask_zero = ev_abs < tol | ||
| n_zero = np.count_nonzero(mask_zero) | ||
|
|
There was a problem hiding this comment.
There’s no regression test covering the scenario that motivated this change (doubly-centered wc producing no eigenvalues below tol, causing n_zero == 0). Adding a test that forces this condition (e.g., very small tol or a matrix construction that yields tiny-but-nonzero numerical eigenvalues) would help ensure compute_mem still drops the constant/near-zero mode and avoids returning n_vertices components.
zihuaihuai
left a comment
There was a problem hiding this comment.
Thanks for the report — your hippocampal-surface use case is real and the current behavior (raising even when the caller asked for spectrum='nonzero') is wrong for it.
However, simply deleting the guard introduces a worse silent-data-corruption bug for the spectrum='all' branch. Walk through with n_zero == 0:
mask_zero = ev_abs < tol→ all Falsemask_zero.argmax()returns0when nothing is Trueidx_zero = 0ev[idx_zero:-1] = ev[idx_zero+1:]→ dropsev[0], the largest eigenvalue (not a zero one)compute_memreturns garbage
Your hippocampal call almost certainly uses spectrum='nonzero' (the else branch on line 121), where mask_nonzero = ~mask_zero is all-True and the function happens to do the right thing — that's why you didn't hit the bug.
Suggested fix
Make the guard branch-aware:
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)."
)That keeps the guard where it actually matters and lets your spectrum='nonzero' call through cleanly.
A small regression test (a non-singular weight matrix → compute_mem(w, spectrum='nonzero') succeeds) would also lock this in.
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 <jordandekraker@users.noreply.github.com>
|
Superseded by #157. Your bug report was right — kept the guard branch-aware so spectrum='nonzero' now works on kernel-free matrices like yours, while spectrum='all' still raises (with a clearer message). You're credited as Co-author. Feel free to close this PR. |
|
Closing — your bug report was correct and the fix has already landed in #157, where it's branch-aware:
This PR's diff overlaps with #157, which is why it now shows a conflict. You're credited as Co-author on the squashed commit. Thanks! |
I was rtying to run this for hippocampal surfaces with no masked data (i.e. no medial wall) and received an error on this line. I believe it is an unneeded line.