Skip to content

Commit f23049f

Browse files
FIX: Support biaxial OPM sensor grouping for visualization
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.
1 parent 8fd2600 commit f23049f

4 files changed

Lines changed: 12 additions & 11 deletions

File tree

examples/datasets/kernel_phantom.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@
111111
# Grouped OPM topomap visualization
112112
# ==================================
113113
#
114-
# Since Kernel OPMs are triaxial sensors (measuring Bx, By, Bz directions),
115-
# we can visualize them as grouped topomaps showing radial and tangential
116-
# components side-by-side when multiple colocated channels are detected:
114+
# Kernel OPMs measure multiple magnetic field directions (Bx, By, Bz).
115+
# We can visualize colocated channels as grouped topomaps showing radial and
116+
# tangential components side-by-side for clearer interpretation of the data:
117117

118118
fig = evoked.plot_joint(times=[t_peak], topomap_args=dict(sphere=sphere))

mne/viz/tests/test_topomap.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ def test_split_opm_overlaps(triaxial_evoked):
852852

853853

854854
def test_should_use_opm_orientation_groups_only_for_triaxial():
855-
"""Test that OPM orientation grouping is restricted to triaxial overlaps."""
855+
"""Test that OPM orientation grouping works for biaxial and triaxial overlaps."""
856856
ch_names = [f"OPM{k:03}" for k in range(1, 7)]
857857
info = create_info(ch_names, 1000.0, ch_types="mag")
858858
with info._unlock():
@@ -869,9 +869,8 @@ def test_should_use_opm_orientation_groups_only_for_triaxial():
869869
np.array(["OPM004", "OPM005", "OPM006"]),
870870
]
871871

872-
assert not topomap._should_use_opm_orientation_groups(
873-
info, picks, pair_overlaps, "mag"
874-
)
872+
# Both biaxial and triaxial overlaps should trigger grouping
873+
assert topomap._should_use_opm_orientation_groups(info, picks, pair_overlaps, "mag")
875874
assert topomap._should_use_opm_orientation_groups(
876875
info, picks, triax_overlaps, "mag"
877876
)

mne/viz/topomap.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ def _find_overlaps(info, ch_type, sphere, modality="fnirs"):
226226
channels_to_exclude = list()
227227

228228
if len(locs3d) > 1 and np.min(dist) < 1e-10:
229-
overlapping_mask = np.triu(squareform(dist < 1e-10))
229+
# Use symmetric distance matrix to find all colocated channel groups
230+
overlapping_mask = squareform(dist < 1e-10)
230231
for chan_idx in range(overlapping_mask.shape[0]):
231232
already_overlapped = list(
232233
itertools.chain.from_iterable(overlapping_channels)
@@ -362,8 +363,8 @@ def _compute_opm_orientation_topomap_data(data, ch_names, pos, overlapping_chann
362363
def _should_use_opm_orientation_groups(info, picks, merge_channels, ch_type):
363364
"""Return whether OPM orientation grouping should be enabled.
364365
365-
Grouping is only used for OPM magnetometer channels with overlap sets that
366-
include at least 3 colocated channels (triaxial-style sensors).
366+
Grouping is used for OPM magnetometer channels with overlap sets that
367+
include at least 2 colocated channels (biaxial or triaxial sensors).
367368
"""
368369
if ch_type != "mag" or not merge_channels:
369370
return False
@@ -376,7 +377,7 @@ def _should_use_opm_orientation_groups(info, picks, merge_channels, ch_type):
376377
if not isinstance(merge_channels, (list, tuple)):
377378
return False
378379

379-
return any(len(overlap_set) >= 3 for overlap_set in merge_channels)
380+
return any(len(overlap_set) >= 2 for overlap_set in merge_channels)
380381

381382

382383
def _plot_update_evoked_topomap(params, bools):

tutorials/preprocessing/80_opm_processing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
)
244244
evoked = epochs.average()
245245
t_peak = evoked.times[np.argmax(np.std(evoked.copy().pick("meg").data, axis=0))]
246+
fig = evoked.plot_joint(picks="mag")
246247

247248
# %%
248249
# Visualizing coregistration

0 commit comments

Comments
 (0)