Skip to content

removed valueError for nonzero eigenvalues#112

Closed
jordandekraker wants to merge 1 commit into
MICA-MNI:masterfrom
jordandekraker:master
Closed

removed valueError for nonzero eigenvalues#112
jordandekraker wants to merge 1 commit into
MICA-MNI:masterfrom
jordandekraker:master

Conversation

@jordandekraker

Copy link
Copy Markdown
Contributor

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.

@ReinderVosDeWael

ReinderVosDeWael commented Jan 29, 2024

Copy link
Copy Markdown
Collaborator

I'd be more curious as to why this is happening in the first place. The MSM manuscript is not ambiguous:

A set of n observations with a full-rank spatial weights matrix W of size n × n will result in n – 1 orthogonal and uncorrelated (Griffith 2000) eigenvectors Vk associated with eigenvalues λk, while a single eigenvector with zero eigenvalue is dropped.

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.

@jordandekraker

Copy link
Copy Markdown
Contributor Author

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

@ReinderVosDeWael

Copy link
Copy Markdown
Collaborator

@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

@OualidBenkarim

Copy link
Copy Markdown
Contributor

There's the tol argument to control that. Maybe increasing the tolerance might fix the issue

@zihuaihuai zihuaihuai requested review from Copilot and removed request for OualidBenkarim February 9, 2026 15:34
@zihuaihuai zihuaihuai self-assigned this Feb 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ValueError previously raised when n_zero == 0 (no eigenvalues below tol).

💡 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)

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
Comment on lines 97 to 101
# Remove zero eigen-value/vector
ev_abs = np.abs(ev)
mask_zero = ev_abs < tol
n_zero = np.count_nonzero(mask_zero)

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@zihuaihuai zihuaihuai removed the request for review from ReinderVosDeWael February 9, 2026 17:52

@zihuaihuai zihuaihuai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 False
  • mask_zero.argmax() returns 0 when nothing is True
  • idx_zero = 0
  • ev[idx_zero:-1] = ev[idx_zero+1:] → drops ev[0], the largest eigenvalue (not a zero one)
  • compute_mem returns 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.

zihuaihuai added a commit that referenced this pull request May 4, 2026
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>
@zihuaihuai

Copy link
Copy Markdown
Collaborator

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.

@zihuaihuai

Copy link
Copy Markdown
Collaborator

Closing — your bug report was correct and the fix has already landed in #157, where it's branch-aware:

  • spectrum='nonzero' now accepts kernel-free weight matrices (your hippocampal-subfield use case works)
  • spectrum='all' still raises, but with a clearer error pointing at spectrum='nonzero'

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants