Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/dev/13921.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ensure grouped OPM tangential topomaps use unsigned RMS magnitude and per-group colormap scaling, aligning behavior with Neuromag-style grouping, by `Pragnya Khandelwal`_.
21 changes: 18 additions & 3 deletions mne/viz/evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -1954,9 +1954,24 @@ def plot_evoked_joint(
del times
_, times_ts = _check_time_unit(ts_args["time_unit"], times_sec)

# prepare axes for topomap
ch_type = ch_types.pop() # set should only contain one element
use_opm_orientation_groups = False
if ch_type == "mag":
from .topomap import _should_use_opm_orientation_groups

picks_topo, _, merge_channels, *_ = _prepare_topomap_plot(
evoked, ch_type, sphere=topomap_args.get("sphere")
)
use_opm_orientation_groups = _should_use_opm_orientation_groups(
evoked.info, picks_topo, merge_channels, ch_type
)
n_group_axes = 2 if use_opm_orientation_groups else 1

# prepare axes for topomap and butterfly plots
if not got_axes:
fig, ts_ax, map_ax = _prepare_joint_axes(len(times_sec), figsize=(8.0, 4.2))
fig, ts_ax, map_ax = _prepare_joint_axes(
len(times_sec) * n_group_axes, figsize=(8.0, 4.2)
)
cbar_ax = None
else:
ts_ax = ts_args["axes"]
Expand Down Expand Up @@ -2002,7 +2017,7 @@ def plot_evoked_joint(

# topomap
contours = topomap_args.get("contours", 6)
ch_type = ch_types.pop() # set should only contain one element

# Since the data has all the ch_types, we get the limits from the plot.
vmin, vmax = (None, None)
norm = ch_type == "grad"
Expand Down
5 changes: 3 additions & 2 deletions mne/viz/tests/test_ica.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,8 @@ def test_plot_components_opm():
ica = ICA(max_iter=1, random_state=0, n_components=10)
ica.fit(RawArray(evoked.data, evoked.info), picks="mag", verbose="error")
fig = ica.plot_components()
assert len(fig.axes) == 10
# Biaxial OPM overlaps render grouped radial+tangential maps.
assert len(fig.axes) == 20


@pytest.mark.slowtest
Expand All @@ -628,4 +629,4 @@ def test_plot_components_opm_triaxial(triaxial_raw):
ica = ICA(max_iter=1, random_state=0, n_components=3)
ica.fit(triaxial_raw, picks="mag", verbose="error")
fig = ica.plot_components()
assert len(fig.axes) == 3
assert len(fig.axes) == 6
59 changes: 58 additions & 1 deletion mne/viz/tests/test_topomap.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,9 @@ def test_plot_topomap_opm():
fig_evoked = evoked.plot_topomap(
times=[-0.1, 0, 0.1, 0.2], ch_type="mag", show=False
)
assert len(fig_evoked.axes) == 5
# Biaxial OPM pairs trigger grouped rendering
# (4 radial + 4 tangential + 2 colorbars)
assert len(fig_evoked.axes) == 10


def test_prepare_topomap_plot_opm_non_quspin_coils():
Expand Down Expand Up @@ -851,6 +853,61 @@ def test_split_opm_overlaps(triaxial_evoked):
assert tangential == ["OPM002", "OPM003", "OPM005", "OPM006"]


def test_opm_tangential_rms_unsigned(triaxial_evoked):
"""Test that tangential OPM data is RMS magnitude and unsigned."""
picks, pos, merge_channels, names, *_ = topomap._prepare_topomap_plot(
triaxial_evoked, "mag"
)
data = triaxial_evoked.data[picks]
grouped = topomap._compute_opm_orientation_topomap_data(
data, names, pos, merge_channels
)
tangential = [group for group in grouped if group[0] == "tangential"][0]
assert np.all(tangential[1] >= 0)
assert tangential[4]


def test_should_use_opm_orientation_groups_only_for_triaxial():
"""Test that OPM orientation grouping works for biaxial and triaxial overlaps."""
ch_names = [f"OPM{k:03}" for k in range(1, 7)]
info = create_info(ch_names, 1000.0, ch_types="mag")
with info._unlock():
for ch in info["chs"]:
ch["coil_type"] = FIFF.FIFFV_COIL_FIELDLINE_OPM_MAG_GEN1

picks = np.arange(len(ch_names))
pair_overlaps = [
np.array(["OPM001", "OPM002"]),
np.array(["OPM003", "OPM004"]),
]
triax_overlaps = [
np.array(["OPM001", "OPM002", "OPM003"]),
np.array(["OPM004", "OPM005", "OPM006"]),
]

# Both biaxial and triaxial overlaps should trigger grouping
assert topomap._should_use_opm_orientation_groups(info, picks, pair_overlaps, "mag")
assert topomap._should_use_opm_orientation_groups(
info, picks, triax_overlaps, "mag"
)


def test_plot_evoked_topomap_opm_triaxial_groups(triaxial_evoked):
"""Test grouped radial/tangential topomap rendering for triaxial OPM."""
fig = triaxial_evoked.plot_topomap(
times=[0.0],
ch_type="mag",
contours=0,
res=8,
sensors=False,
show=False,
)
assert len(fig.axes) == 4
titles = [ax.get_title() for ax in fig.axes]
assert any("radial" in title for title in titles)
assert any("tangential" in title for title in titles)


def test_plot_topomap_nirs_overlap(fnirs_epochs):
"""Test plotting nirs topomap with overlapping channels (gh-7414)."""
fig = fnirs_epochs["A"].average(picks="hbo").plot_topomap()
Expand Down
Loading
Loading