Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MRG] PCA flip for volumetric source estimates #13092

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

Shrecki
Copy link

@Shrecki Shrecki commented Jan 30, 2025

What does this implement/fix?

This PR implements PCA flip in volumetric source estimates.
To do so, it relies on using the already existing _pca_flip method. _pca_flip has been slighly modified to check if at least two vertices are available within a label. If that is not the case, it returns trivially the original signal, as PCA is ill-defined in this case.

Additional information

Feel free to have a look and - of course - double-check for correctness!

Extension of PCA flip in volumetric setting. Underlying logic is pretty much identical to cortical case with respect to flip vector creation and PCA itself
Added a check in PCA flip in case the flip is None or number of vertices is below 2, in which case PCA will return a trivial estimate of the signal
Complete with its own unit test case
@Shrecki Shrecki requested a review from larsoner as a code owner January 31, 2025 09:56
@Shrecki Shrecki changed the title [FEAT] PCA flip for volumetric source estimates [MRG] PCA flip for volumetric source estimates Jan 31, 2025
@Shrecki
Copy link
Author

Shrecki commented Jan 31, 2025

I belive this PR is ready for review :) (although I do not know why pip pre is failing on Windows)

Copy link
Member

@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

Found some cruft, can you take a look at the diff on GitHub and make sure I found all of it?

Also with enhancements it's nice to see the effect in some example. Maybe you could modify this:

https://mne.tools/1.8/auto_examples/inverse/compute_mne_inverse_epochs_in_label.html

which comes from examples/inverse/compute_mne_inverse_epochs_in_label.py could be modified to add a volume example to show its flip modes as well? Or maybe some other example like tutorials/inverse/60_visualize_stc.py or examples/inverse/label_source_activations.py?

Comment on lines +3443 to +3445
# logger.info("Selected mode: " + mode)
# print("Entering _prepare_label_extraction")
# print("Selected mode: " + mode)
Copy link
Member

Choose a reason for hiding this comment

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

Cruft, should be removed. But really to help next time:

Suggested change
# logger.info("Selected mode: " + mode)
# print("Entering _prepare_label_extraction")
# print("Selected mode: " + mode)
logger.debug(f"Selected mode: {mode}")

then if you run with verbose='debug' you will see the statement

@@ -3445,6 +3456,7 @@ def _prepare_label_extraction(stc, labels, src, mode, allow_empty, use_sparse):

bad_labels = list()
for li, label in enumerate(labels):
# print("Mode: " + mode + " li: " + str(li) + " label: " + str(label))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# print("Mode: " + mode + " li: " + str(li) + " label: " + str(label))

Comment on lines +3525 to +3527
# print(f"src: {src[:2]}")
# print(f"len(src): {len(src[:2])}")

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# print(f"src: {src[:2]}")
# print(f"len(src): {len(src[:2])}")

@@ -1460,22 +1460,47 @@ def label_sign_flip(label, src):
flip : array
Sign flip vector (contains 1 or -1).
"""
if len(src) != 2:
raise ValueError("Only source spaces with 2 hemisphers are accepted")
if len(src) > 2 or len(src) == 0:
Copy link
Member

Choose a reason for hiding this comment

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

A better / more modern check would be something like:

_validate_type(src, SourceSpaces, "src")
_check_option("source space kind", src.kind, ("volume", "surface"))
if src.kind == "volume" and len(src) != 1:
    raise ValueError("Only single-segment volumes, are supported, got labelized volume source space")

And incidentally I think eventually we could add support for segmented volume source spaces, as well as mixed source spaces (once surface + volume are fully supported, mixed isn't too bad after that). But probably not as part of this PR!

Comment on lines +1481 to +1492
if isbi_hemi:
lh_id = 0
rh_id = 1
lh_vertno = src[0]["vertno"]
rh_vertno = src[1]["vertno"]
elif label.hemi == "lh":
lh_vertno = src[0]["vertno"]
elif label.hemi == "rh":
rh_id = 0
rh_vertno = src[0]["vertno"]
else:
raise Exception(f'Unknown hemisphere type "{label.hemi}"')
Copy link
Member

Choose a reason for hiding this comment

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

I don't totally follow this logic. It's a surface source space if and only if it's bihemi, and it's volume source space if and only if it's a single-element list. I think it would be cleaner to triage based on source space type probably...

Comment on lines +3752 to +3755
# if flip is None: # Happens if fewer than 2 vertices in the label
# if this_data.shape[]
# label_tc[i] = 0
# else:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# if flip is None: # Happens if fewer than 2 vertices in the label
# if this_data.shape[]
# label_tc[i] = 0
# else:

@@ -1469,7 +1613,7 @@ def objective(x):
assert_allclose(directions, want_nn, atol=2e-6)


@testing.requires_testing_data
# @testing.requires_testing_data
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# @testing.requires_testing_data
@testing.requires_testing_data

Comment on lines +1089 to +1090
modes = ("mean", "max") if vector else ("mean", "max")
for mode in modes:
Copy link
Member

Choose a reason for hiding this comment

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

Revert?

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.

2 participants