Skip to content

ENH: group triaxial OPM topomaps by orientation#13866

Open
PragnyaKhandelwal wants to merge 44 commits into
mne-tools:mainfrom
PragnyaKhandelwal:enh-opm-grouping-final-fix
Open

ENH: group triaxial OPM topomaps by orientation#13866
PragnyaKhandelwal wants to merge 44 commits into
mne-tools:mainfrom
PragnyaKhandelwal:enh-opm-grouping-final-fix

Conversation

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor

@PragnyaKhandelwal PragnyaKhandelwal commented Apr 24, 2026

Reference issue (if any)

Closes #13781

What does this implement/fix?

Final fix for #13781: adds caller-facing grouped rendering for colocated triaxial OPM channels so orientation information is shown explicitly across visualization entry points.

Implemented:

  • Grouped radial and tangential rendering in Evoked topomap.
  • Grouped radial and tangential rendering in ICA component topomaps.
  • Regression tests updated for topomap and ICA triaxial OPM behavior.

Additional information

  • Manual visual check done for:

    • Evoked.plot_topomap grouped radial/tangential maps
    • ICA.plot_components grouped radial/tangential titles
  • A local helper script was used for manual visual smoke-checking on synthetic triaxial OPM data. I can share it if needed.

  • Visual outputs:

    • Evoked topomap grouped view:
image
  • ICA grouped components view:
image

@PragnyaKhandelwal PragnyaKhandelwal marked this pull request as ready for review April 25, 2026 09:22
@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

Hi @larsoner....Just a friendly ping on this final wrap-up for the OPM topomaps. I've included visual outputs in the PR description showing the new grouped views. Ready to review whenever you have a chance!

@larsoner
Copy link
Copy Markdown
Member

Code looks reasonable at first look. But now I'm wondering whether or not this can be combined / refactored with the mag/grad code for Neuromag systems. The problem is similar there: there are three sensors per location, one radial (magnetometer) and two tangential (gradiometers) and we plot the mags in one plot and the RMS of the gradiometers in another.

Do you think it's worth looking into that refactoring in this PR? Or would it be better to review + merge this PR as-is and then refactor afterward?

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

Thanks @larsoner! I completely agree that unifying the OPM and Neuromag mag/grad logic is the right architectural direction since the underlying radial/tangential problem is so similar.
Since this current PR is already quite dense with the OPM-specific tests and rendering logic, I think it would be cleaner to merge this as-is to close out #13781 without risking scope creep. We can then track the Neuromag unification in a fresh issue or PR.
As a quick heads-up on my end: my university final exams are starting this week and run through May 24th, so I will be stepping away from the keyboard for the next few weeks to focus on those. I would love to tackle that Neuromag refactor as my very first task once I am back! Does that sound like a good plan to you?

@larsoner
Copy link
Copy Markdown
Member

Yeah that sounds reasonable to me!

One last request, can you modify some example(s)/tutorial(s) in a way that shows this new functionality?

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

Thanks @larsoner! I've added a new example demonstrating the grouped triaxial OPM topomaps using plot_topomap() and plot_joint() as requested.

Well I initially tried using the ucl_opm_auditory dataset, but it caused the Sphinx Gallery CI build to time out, so I swapped it to generate a fast, synthetic triaxial dataset instead to keep the docs build snappy.

Ready for final review and merge once these last CI checks turn green!

@larsoner
Copy link
Copy Markdown
Member

Well I initially tried using the ucl_opm_auditory dataset, but it caused the Sphinx Gallery CI build to time out, so I swapped it to generate a fast, synthetic triaxial dataset instead to keep the docs build snappy.

Hmmm that's a problem, that's a real dataset and it should work on real data. Can you try running it with memory profiler for example to see if memory usage goes too high? It really shouldn't for evoked data... if you can push a commit that should work (or revert to one that should) I can also look

A fake triaxial dataset I think is less useful for people.

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

Agreed! Real data is definitely better for learning. To avoid stressing the CI with another heavy download, I just moved the visualization into the existing 80_opm_processing.py tutorial since it already loads the dataset. Let's see if the docs build cleanly now......if it still times out, I'll run a profiler and trim it down!

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

Ah, I see why the grouped view didn't trigger in the rendered docs!

The ucl_opm_auditory dataset primarily consists of uniaxial/radial sensors. Because my logic specifically requires colocated triaxial overlaps to group the axes (to avoid breaking standard OPM plots), it recognized the missing tangential data and safely fell back to the normal single topomap view.

Since mne.datasets doesn't currently seem to have a native triaxial dataset to showcase this, how would you prefer to handle the tutorial step? Happy to follow your lead!

@larsoner
Copy link
Copy Markdown
Member

This one is triaxial I think

https://mne.tools/stable/auto_examples/datasets/kernel_phantom.html

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

PragnyaKhandelwal commented Apr 29, 2026

Alright, I migrated the example to the kernel_phantom dataset and the docs built successfully, but it is still falling back to the standard single-plot view (as seen in the rendered docs).
I dug into why this is happening: while the Kernel hardware is triaxial, the way the channel geometries are stored in that specific .fif file doesn't perfectly satisfy the strict mathematical colocated overlap check my algorithm uses to trigger the grouping (which I added to ensure we don't accidentally split standard OPM arrays).
Since my synthetic tests prove the grouping architecture works perfectly for true triaxial overlaps (visuals in the PR description above!), I think the cleanest path forward is to simply merge this core feature as-is to close out #13781. We can then add a proper tutorial later once a clean triaxial dataset is added to mne.datasets.
How does that sound? If you agree, then I could add the synthetic dataset again and this should be ready to merge!

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

Thanks @larsoner! you're completely right that plot_joint should stay consistent with the Neuromag behavior for now.
I know I said I was already stepping away for my exams, but I really wanted to get this cleared out and (hopefully!) merged so I could start fresh after my break. Locking in for exams with zero pending PRs hanging over my head just feels much better! 😅. I just pushed the commits to revert plot_joint so it remains completely unchanged. The PR is now strictly scoped down to just Evoked.plot_topomap() and ICA.plot_components().
I will leave the final merge button in your hands once the CI finishes running. See you in a few weeks for the broader mag/grad refactor discussion!

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

Failing tests of pyvistaqt and vtk are not related!

@larsoner
Copy link
Copy Markdown
Member

larsoner commented May 6, 2026

This output is surprising:

image

I would expect signed values for the radial, but unsigned (magnitude) values for the tangential components, just like for Neuromag grads (you have to combine the two tangential directions somehow, and that's done via norm).

With these recurring bugs / differences in behavior, I think we should refactor the code to handle the Neuromag SQUID and triaxial OPM cases the same way. Doing it separately doesn't seem to be working as expected

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

Hey @larsoner, checking back in after my exams! You make a completely fair point about the tangential components needing to be unsigned magnitudes to match Neuromag. Unifying this logic is definitely the right architectural move.

Since my GSoC coding period officially kicks off this Monday (focusing heavily on the mffpy refactor), my bandwidth to tackle a major Neuromag/OPM plotting refactor is going to be a bit limited in the short term. >
I'll leave this PR open and chip away at the unified logic as a background task alongside my GSoC deliverables. I'll ping you when I have a working draft of the unified approach!

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

I opened a follow‑up draft PR for the Neuromag/OPM refactor here: #13921. It includes the unsigned tangential RMS + per‑group colormap change and a shared helper. I recommend merging #13866 first, then I’ll rebase #13921 onto main.

Copy link
Copy Markdown
Contributor

@nordme nordme left a comment

Choose a reason for hiding this comment

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

Hi @PragnyaKhandelwal !

First off, thanks for the PR! I have a comment about function design here from a future usability perspective. I think future OPM features are likely to need a generic helper function to merge tangential OPM channel data from triaxial systems. Currently, the _merge_ch_data helper function with the opm modality option uses the _merge_opm_data helper function, which does not do any merging at all, but simply discards the tangential data. Meanwhile, you've helpfully added code here for performing tangential merges, but only within the compute_opm_orientation_topomap_data function, which does not allow future authors to call the tangential channel merging logic for other tasks. I think other contributors coming along later will find this design confusing.

I suggest adding a generic helper function for combining tangential channels that the opm orientation function can call. Also, I suggest renaming _merge_opm_data to something like _restrict_opm_to_rad to distinguish it from your tangential merge function so that other contributors more easily understand the actual function behavior.

assert tangential == ["OPM002", "OPM003", "OPM005", "OPM006"]


def test_should_use_opm_orientation_groups_only_for_triaxial():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
def test_should_use_opm_orientation_groups_only_for_triaxial():
def test_should_use_opm_orientation_groups_for_tri_and_biaxial():

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

Hi @larsoner and @nordme,
I thought about both of your feedback and realized you are completely right. Leaving the OPM and Neuromag logic split across two different PRs makes the architecture confusing and harder to review.
I have consolidated the work into this single PR! I merged my follow-up refactor branch directly in here.

What this PR now includes:
The generic _compute_orientation_group_data helper.

Tangential components now use unsigned RMS magnitude, aligning with the Neuromag grads

Per-group colormaps and vlims are active for the grouped plots.

The build_docs CI just finished successfully, and the kernel_phantom example is now generating the correct split sequential/diverging colormaps for the OPM orientations.
Let me know what you think of the updated visuals and the unified architecture!

image

@larsoner
Copy link
Copy Markdown
Member

larsoner commented Jun 1, 2026

@harrisonritz did you want to look / comment / try this code?

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.

topomap plots of dual- or triaxial OPM data

3 participants