Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
70b3a4c
detect overlap; commandeer fnirs functions
harrisonritz Mar 3, 2025
7f936b0
start to merge channels
harrisonritz Mar 4, 2025
8297974
opm tests
harrisonritz Mar 5, 2025
75e40c0
fix selection of radial channels; better selection of OPM sensors; su…
harrisonritz Mar 5, 2025
ab419e3
always use Z-axis orientation (now matches UCL's *-TAN sensors)
harrisonritz Mar 6, 2025
109e1de
update test_topomap
harrisonritz Mar 10, 2025
b86351e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 10, 2025
0f92da6
update testing data
harrisonritz Mar 10, 2025
dcf3e2e
update testing data hash
harrisonritz Mar 10, 2025
8067e4d
inst.info --> info
harrisonritz Mar 15, 2025
953e098
add `-ave` suffix
harrisonritz Mar 15, 2025
0483ad1
update dataset config
harrisonritz Mar 17, 2025
5460d71
properly load evoked
harrisonritz Mar 17, 2025
72822a4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 17, 2025
b751d3e
Merge branch 'main' into colocated_topo
harrisonritz Apr 22, 2025
a72b7f8
Update mne/channels/layout.py
harrisonritz Apr 22, 2025
a65961c
Update mne/channels/layout.py
harrisonritz Apr 22, 2025
7343021
append with MERGE_REMOVE. use FIFF constants
harrisonritz Apr 22, 2025
217c579
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 22, 2025
0a02fb9
new append
harrisonritz Apr 22, 2025
bfb6152
merge-remove
harrisonritz Apr 23, 2025
32b90b9
Merge branch 'main' into colocated_topo
harrisonritz Apr 23, 2025
798d730
add changelog
harrisonritz Apr 28, 2025
7743397
update docs, include topo in opm preproc tutorial
harrisonritz Apr 28, 2025
64d3d26
Merge branch 'main' into colocated_topo
harrisonritz Apr 28, 2025
972e24d
literal instead of link
harrisonritz Apr 28, 2025
eee01b0
Merge branch 'main' into colocated_topo
harrisonritz Apr 28, 2025
6bbc071
Apply suggestions from code review
larsoner Apr 29, 2025
13387a4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 29, 2025
b541bf5
Merge branch 'main' into colocated_topo
larsoner Apr 29, 2025
988e8f6
MAINT: Smoke test
larsoner Apr 29, 2025
f073d0c
FIX: Test
larsoner Apr 29, 2025
723809d
FIX: Scope
larsoner Apr 29, 2025
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/devel/13144.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow for ``topomap`` plotting of optically pumped MEG (OPM) sensors with overlapping channel locations. When channel locations overlap, plot the most radially oriented channel. By :newcontrib:`Harrison Ritz`.
1 change: 1 addition & 0 deletions doc/changes/names.inc
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
.. _Hamid Maymandi: https://github.com/HamidMandi
.. _Hamza Abdelhedi: https://github.com/BabaSanfour
.. _Hari Bharadwaj: https://github.com/haribharadwaj
.. _Harrison Ritz: https://github.com/harrisonritz
.. _Hasrat Ali Arzoo: https://github.com/hasrat17
.. _Henrich Kolkhorst: https://github.com/hekolk
.. _Hongjiang Ye: https://github.com/hongjiang-ye
Expand Down
45 changes: 41 additions & 4 deletions mne/channels/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,7 @@ def _auto_topomap_coords(info, picks, ignore_overlap, to_sphere, sphere):
# Use channel locations if available
locs3d = np.array([ch["loc"][:3] for ch in chs])

# If electrode locations are not available, use digization points
# If electrode locations are not available, use digitization points
if not _check_ch_locs(info=info, picks=picks):
logging.warning(
"Did not find any electrode locations (in the info "
Expand Down Expand Up @@ -1089,7 +1089,7 @@ def _pair_grad_sensors(
return picks


def _merge_ch_data(data, ch_type, names, method="rms"):
def _merge_ch_data(data, ch_type, names, method="rms", *, modality="opm"):
"""Merge data from channel pairs.

Parameters
Expand All @@ -1102,6 +1102,8 @@ def _merge_ch_data(data, ch_type, names, method="rms"):
List of channel names.
method : str
Can be 'rms' or 'mean'.
modality : str
The modality of the data, either 'grad', 'fnirs', or 'opm'

Returns
-------
Expand All @@ -1112,9 +1114,13 @@ def _merge_ch_data(data, ch_type, names, method="rms"):
"""
if ch_type == "grad":
data = _merge_grad_data(data, method)
else:
assert ch_type in _FNIRS_CH_TYPES_SPLIT
elif modality == "fnirs" or ch_type in _FNIRS_CH_TYPES_SPLIT:
data, names = _merge_nirs_data(data, names)
elif modality == "opm" and ch_type == "mag":
data, names = _merge_opm_data(data, names)
else:
raise ValueError(f"Unknown modality {modality} for channel type {ch_type}")

return data, names


Expand Down Expand Up @@ -1180,6 +1186,37 @@ def _merge_nirs_data(data, merged_names):
return data, merged_names


def _merge_opm_data(data, merged_names):
"""Merge data from multiple opm channel by just using the radial component.

Channel names that end in "MERGE_REMOVE" (ie non-radial channels) will be
removed. Only the the radial channel is kept.

Parameters
----------
data : array, shape = (n_channels, ..., n_times)
Data for channels.
merged_names : list
List of strings containing the channel names. Channels that are to be
removed end in "MERGE_REMOVE".

Returns
-------
data : array
Data for channels with requested channels merged. Channels used in the
merge are removed from the array.
"""
to_remove = np.empty(0, dtype=np.int32)
for idx, ch in enumerate(merged_names):
if ch.endswith("MERGE-REMOVE"):
to_remove = np.append(to_remove, idx)
to_remove = np.unique(to_remove)
for rem in sorted(to_remove, reverse=True):
del merged_names[rem]
data = np.delete(data, to_remove, axis=0)
return data, merged_names


def generate_2d_layout(
xy,
w=0.07,
Expand Down
4 changes: 2 additions & 2 deletions mne/datasets/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
# update the checksum in the MNE_DATASETS dict below, and change version
# here: ↓↓↓↓↓↓↓↓
RELEASES = dict(
testing="0.156",
testing="0.161",
misc="0.27",
phantom_kit="0.2",
ucl_opm_auditory="0.2",
Expand Down Expand Up @@ -115,7 +115,7 @@
# Testing and misc are at the top as they're updated most often
MNE_DATASETS["testing"] = dict(
archive_name=f"{TESTING_VERSIONED}.tar.gz",
hash="md5:d94fe9f3abe949a507eaeb865fb84a3f",
hash="md5:a32cfb9e098dec39a5f3ed6c0833580d",
url=(
"https://codeload.github.com/mne-tools/mne-testing-data/"
f"tar.gz/{RELEASES['testing']}"
Expand Down
1 change: 0 additions & 1 deletion mne/preprocessing/tests/test_ica.py
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,6 @@ def f(x, y):

def test_get_explained_variance_ratio(tmp_path, short_raw_epochs):
"""Test ICA.get_explained_variance_ratio()."""
pytest.importorskip("sklearn")
raw, epochs, _ = short_raw_epochs
ica = ICA(max_iter=1)

Expand Down
19 changes: 18 additions & 1 deletion mne/viz/tests/test_ica.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
pick_types,
read_cov,
read_events,
read_evokeds,
)
from mne.io import read_raw_fif
from mne.datasets import testing
from mne.io import RawArray, read_raw_fif
from mne.preprocessing import ICA, create_ecg_epochs, create_eog_epochs
from mne.utils import _record_warnings, catch_logging
from mne.viz.ica import _create_properties_layout, plot_ica_properties
Expand All @@ -32,6 +34,9 @@
event_id, tmin, tmax = 1, -0.1, 0.2
raw_ctf_fname = base_dir / "test_ctf_raw.fif"

testing_path = testing.data_path(download=False)
opm_fname = testing_path / "OPM" / "opm-evoked-ave.fif"

pytest.importorskip("sklearn")


Expand Down Expand Up @@ -526,3 +531,15 @@ def test_plot_instance_components(browser_backend):
fig._fake_click((x, y), xform="data")
fig._click_ch_name(ch_index=0, button=1)
fig._fake_keypress("escape")


@pytest.mark.slowtest
@pytest.mark.filterwarnings("ignore:.*did not converge.*:")
@testing.requires_testing_data
def test_plot_components_opm():
"""Test for gh-12934."""
evoked = read_evokeds(opm_fname, kind="average")[0]
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
15 changes: 15 additions & 0 deletions mne/viz/tests/test_topomap.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
subjects_dir = data_dir / "subjects"
ecg_fname = data_dir / "MEG" / "sample" / "sample_audvis_ecg-proj.fif"
triux_fname = data_dir / "SSS" / "TRIUX" / "triux_bmlhus_erm_raw.fif"
opm_fname = data_dir / "OPM" / "opm-evoked-ave.fif"


base_dir = Path(__file__).parents[2] / "io" / "tests" / "data"
evoked_fname = base_dir / "test-ave.fif"
Expand Down Expand Up @@ -776,6 +778,19 @@ def test_plot_topomap_bads_grad():
plot_topomap(data, info, res=8)


@testing.requires_testing_data
def test_plot_topomap_opm():
"""Test plotting topomap with OPM data."""
# load data
evoked = read_evokeds(opm_fname, kind="average")[0]

# plot evoked topomap
fig_evoked = evoked.plot_topomap(
times=[-0.1, 0, 0.1, 0.2], ch_type="mag", show=False
)
assert len(fig_evoked.axes) == 5


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