Skip to content

ENH: group triaxial OPM topomaps by orientation#13866

Open
PragnyaKhandelwal wants to merge 28 commits intomne-tools:mainfrom
PragnyaKhandelwal:enh-opm-grouping-final-fix
Open

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

Conversation

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor

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 rendering and connector handling in Evoked joint plots.
  • Grouped radial and tangential rendering in ICA component topomaps.
  • Regression tests updated for topomap, joint, and ICA triaxial OPM behavior.

Additional information

  • Manual visual check done for:

    • Evoked.plot_topomap grouped radial/tangential maps
    • Evoked.plot_joint grouped maps + connector lines
    • 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
  • Joint plot 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!

@larsoner
Copy link
Copy Markdown
Member

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?

I'm not convinced... the Kernel dataset is a real dataset, and the feature needs to work for real datasets way more so than synthetic ones ...

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

... So I think the check for colocation needs adjusting. What doesn't match properly for that dataset?

@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

I fixed the docs example call path in kernel_phantom.py so it no longer passes unsupported kwargs through joint->topomap.
Local pre-commit and local topo tests are passing on my branch.
I still see CI instability/failures in some required jobs (not consistently reproducible locally), including CircleCI jobs and macOS mamba jobs.
Could you please help by re-running the failed required checks once from the latest commit and advise if you see a repo-infra issue versus a code issue I should patch?

@larsoner
Copy link
Copy Markdown
Member

larsoner commented May 1, 2026

The macOS tests I'll fix in #13878 and we can ignore, I've restarted CircleCI

)
evoked = epochs.average()
t_peak = evoked.times[np.argmax(np.std(evoked.copy().pick("meg").data, axis=0))]
fig = evoked.plot_joint(picks="mag")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why remove this?

Comment thread examples/datasets/kernel_phantom.py Outdated
#
# Since Kernel OPMs are triaxial sensors (measuring Bx, By, Bz directions),
# we can visualize them as grouped topomaps showing radial and tangential
# components side-by-side when multiple colocated channels are detected:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed two critical bugs in OPM grouped topomap rendering:

1. Use symmetric distance matrix (not upper triangle) to properly detect all
   colocated channel overlaps bidirectionally.

2. Lower grouping threshold from >=3 to >=2 channels to support both biaxial
   (e.g., bx+by or by+bz) and triaxial (bx+by+bz) OPM sensors.

Real OPM hardware like Kernel phantom has biaxial pairs instead of perfect
triaxial arrays. This fix enables grouped radial/tangential visualization
for real datasets.

Changes:
- mne/viz/topomap.py: Fixed distance matrix and threshold
- mne/viz/tests/test_topomap.py: Updated test expectations
- examples/datasets/kernel_phantom.py: Updated docstring
- tutorials/preprocessing/80_opm_processing.py: Restored plot_joint call

Closes the rendering issue where only 1 plot showed instead of 2.
Tests now expect doubled axis counts due to radial+tangential grouping
for biaxial OPM pairs. Kernel phantom data now correctly triggers grouped
visualization with 20 axes for 10 ICA components and 9 axes for 4-point
topomap (4 radial + 4 tangential + 1 colorbar).
@PragnyaKhandelwal
Copy link
Copy Markdown
Contributor Author

I fixed this issue now to handle overlap grouping for biaxial OPM pairs, so grouped radial and tangential views now trigger correctly on this dataset and also updated the tests accordingly for new axis counts.
This clarified an important issue on my side: my earlier logic assumed triaxial-only overlaps, but Kernel phantom contains biaxial colocated pairs, so the grouping gate needed to be broadened.
I also restored the evoked.plot_joint line in the OPM tutorial as requested.
Thanks for the the feedback @larsoner it directly improved the logic and made it more robust.
image

ig ready to review and merge!

Comment thread examples/datasets/kernel_phantom.py Outdated
Comment on lines +114 to +118
# Kernel OPMs measure multiple magnetic field directions (Bx, By, Bz).
# We can visualize colocated channels as grouped topomaps showing radial and
# tangential components side-by-side for clearer interpretation of the data:

fig = evoked.plot_joint(times=[t_peak], topomap_args=dict(sphere=sphere))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we show radial and tangential for Neuromag data? We don't, right? So we should probably be consistent here and just show the radial.

This would be an advantage of refactoring the logic to be the same, to ensure consistency in behaviors...

At the very least, I think that changing the behavior of plot_joint should be out of scope for this PR. How those time points show up will need some discussion, thinking and testing. (And it should probably be the same behavior for Neuromag triplets and OPM triplets.)

@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!

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

2 participants