Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ authors:
family-names: Aristimunha
affiliation: 'Université Paris-Saclay, LISN, Inria TAU/INRIA Nerv, France'
orcid: 'https://orcid.org/0000-0001-5258-2995'
- given-names: Kalle
family-names: Mäkelä
affiliation: 'University of Helsinki, Helsinki, Finland'
orcid: 'https://orcid.org/0009-0005-5706-0842'
- given-names: Alexandre
family-names: Gramfort
affiliation: 'Université Paris-Saclay, Inria, CEA, Palaiseau, France'
Expand Down
1 change: 1 addition & 0 deletions doc/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@
.. _William Turner: https://bootstrapbill.github.io/
.. _Yorguin Mantilla: https://github.com/yjmantilla
.. _Julius Welzel: https://github.com/JuliusWelzel
.. _Kalle Mäkelä: https://github.com/Kallemakela
2 changes: 2 additions & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The following authors contributed for the first time. Thank you so much! 🤩
* `Julius Welzel`_
* `Alex Lopez Marquez`_
* `Bruno Aristimunha`_
* `Kalle Mäkelä`_

The following authors had contributed before. Thank you for sticking around! 🤘

Expand All @@ -34,6 +35,7 @@ Detailed list of changes
🚀 Enhancements
^^^^^^^^^^^^^^^

- TODO
- :func:`mne_bids.write_raw_bids()` has a new parameter `electrodes_tsv_task` which allows adding the `task` entity to the `electrodes.tsv` filepath, by `Alex Lopez Marquez`_ (:gh:`1424`)
- Extended the configuration to recognise `motion` as a valid BIDS datatype by `Julius Welzel`_ (:gh:`1430`)
- Better control of verbosity in several functions, by `Bruno Aristimunha`_ (:gh:`1449`)
Expand Down
39 changes: 35 additions & 4 deletions mne_bids/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ def _get_bads_from_tsv_data(tsv_data):
return bads


def _handle_channels_reading(channels_fname, raw):
def _handle_channels_reading(channels_fname, raw, ch_name_mismatch="raise"):
"""Read associated channels.tsv and populate raw.

Updates status (bad) and types of channels.
Expand Down Expand Up @@ -847,7 +847,23 @@ def _handle_channels_reading(channels_fname, raw):
f"set channel names."
)
else:
raw.rename_channels(dict(zip(raw.ch_names, ch_names_tsv)))
orig_names = list(raw.ch_names)
if orig_names != ch_names_tsv:
if ch_name_mismatch == "raise":
raise RuntimeError(
f"Channel mismatch between {channels_fname} and the raw data file detected."
)
warn(
f"Channel mismatch between {channels_fname} and the raw data file detected. Using mismatch strategy: {ch_name_mismatch}."
)
if ch_name_mismatch == "reorder":
raw.reorder_channels(ch_names_tsv)
elif ch_name_mismatch == "rename":
raw.rename_channels(dict(zip(raw.ch_names, ch_names_tsv)))
else:
raise ValueError(
"ch_name_mismatch must be one of {'reorder','raise','rename'}"
)

# Set the channel types in the raw data according to channels.tsv
channel_type_bids_mne_map_available_channels = {
Expand Down Expand Up @@ -887,7 +903,12 @@ def _handle_channels_reading(channels_fname, raw):

@verbose
def read_raw_bids(
bids_path, extra_params=None, *, return_event_dict=False, verbose=None
bids_path,
extra_params=None,
*,
return_event_dict=False,
ch_name_mismatch="raise",
verbose=None,
):
"""Read BIDS compatible data.

Expand Down Expand Up @@ -918,6 +939,12 @@ def read_raw_bids(
event IDs, in addition to the :class:`~mne.io.Raw` object. If a ``value`` column
is present in the ``*_events.tsv`` file, it will be used as the source of the
integer event ID values (events with ``value="n/a"`` will be omitted).
ch_name_mismatch : str
What to do if the channel names in the channels.tsv file do not match the channel names in the raw data file.
Must be one of {'raise', 'reorder', 'rename'}. Default is 'raise'.
- 'raise' will raise a RuntimeError if there is a channel mismatch.
- 'reorder' will reorder the channels in the raw data file to match the channel names in the channels.tsv file.
- 'rename' will rename the channels in the raw data file to match the channel names in the channels.tsv file.
%(verbose)s

Returns
Expand All @@ -944,6 +971,8 @@ def read_raw_bids(
ValueError
If the specified ``datatype`` cannot be found in the dataset.

RuntimeError
If channels.tsv and raw data file have a channel name mismatch and ch_name_mismatch is 'raise'.
"""
if not isinstance(bids_path, BIDSPath):
raise RuntimeError(
Expand Down Expand Up @@ -1082,7 +1111,9 @@ def read_raw_bids(
bids_path, suffix="channels", extension=".tsv", on_error="warn"
)
if channels_fname is not None:
raw = _handle_channels_reading(channels_fname, raw)
raw = _handle_channels_reading(
channels_fname, raw, ch_name_mismatch=ch_name_mismatch
)

# Try to find an associated electrodes.tsv and coordsystem.json
# to get information about the status and type of present channels
Expand Down
112 changes: 111 additions & 1 deletion mne_bids/tests/test_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
)
from mne_bids.path import _find_matching_sidecar
from mne_bids.read import (
_handle_channels_reading,
_handle_events_reading,
_handle_scans_reading,
_read_raw,
Expand Down Expand Up @@ -1463,7 +1464,13 @@ def test_channels_tsv_raw_mismatch(tmp_path):
raw.reorder_channels(ch_names_new)
raw.save(raw_path, overwrite=True)

raw = read_raw_bids(bids_path)
with (
pytest.warns(
RuntimeWarning,
match=r"Channel mismatch between .*channels\.tsv and the raw data file detected\. Using mismatch strategy: reorder\.",
),
):
raw = read_raw_bids(bids_path, ch_name_mismatch="reorder")
assert raw.ch_names == ch_names_orig


Expand Down Expand Up @@ -1509,6 +1516,109 @@ def test_gsr_and_temp_reading():
assert raw.get_channel_types(["Temperature"]) == ["temperature"]


def _setup_nirs_channel_mismatch(tmp_path):
ch_order_snirf = ["S1_D1 760", "S1_D2 760", "S1_D1 850", "S1_D2 850"]
ch_types = ["fnirs_cw_amplitude"] * len(ch_order_snirf)
info = mne.create_info(ch_order_snirf, sfreq=10, ch_types=ch_types)
data = np.arange(len(ch_order_snirf) * 10.0).reshape(len(ch_order_snirf), 10)
raw = mne.io.RawArray(data, info)

for i, ch_name in enumerate(raw.ch_names):
loc = np.zeros(12)
if "S1" in ch_name:
loc[3:6] = np.array([0, 0, 0])
if "D1" in ch_name:
loc[6:9] = np.array([1, 0, 0])
elif "D2" in ch_name:
loc[6:9] = np.array([0, 1, 0])
loc[9] = int(ch_name.split(" ")[1])
loc[0:3] = (loc[3:6] + loc[6:9]) / 2
raw.info["chs"][i]["loc"] = loc

orig_name_to_loc = {
name: raw.info["chs"][i]["loc"].copy() for i, name in enumerate(raw.ch_names)
}
orig_name_to_data = {
name: raw.get_data(picks=i).copy() for i, name in enumerate(raw.ch_names)
}

ch_order_bids = ["S1_D1 760", "S1_D1 850", "S1_D2 760", "S1_D2 850"]
ch_types_bids = ["NIRSCWAMPLITUDE"] * len(ch_order_bids)
channels_dict = OrderedDict([("name", ch_order_bids), ("type", ch_types_bids)])
channels_fname = tmp_path / "channels.tsv"
_to_tsv(channels_dict, channels_fname)

return (
raw,
ch_order_snirf,
ch_order_bids,
channels_fname,
orig_name_to_loc,
orig_name_to_data,
)


def test_channel_mismatch_raise(tmp_path):
raw, _, _, channels_fname, _, _ = _setup_nirs_channel_mismatch(tmp_path)
with pytest.raises(
RuntimeError,
match=r"Channel mismatch between .*channels\.tsv and the raw data file detected\.",
):
_handle_channels_reading(channels_fname, raw.copy(), ch_name_mismatch="raise")


def test_channel_mismatch_reorder(tmp_path):
raw, _, ch_order_bids, channels_fname, orig_name_to_loc, orig_name_to_data = (
_setup_nirs_channel_mismatch(tmp_path)
)
with (
pytest.warns(
RuntimeWarning,
match=r"Channel mismatch between .*channels\.tsv and the raw data file detected\. Using mismatch strategy: reorder\.",
),
):
raw_out = _handle_channels_reading(
channels_fname, raw, ch_name_mismatch="reorder"
)
assert raw_out.ch_names == ch_order_bids
for i, new_name in enumerate(raw_out.ch_names):
np.testing.assert_allclose(
raw_out.info["chs"][i]["loc"], orig_name_to_loc[new_name]
)
np.testing.assert_allclose(
raw_out.get_data(picks=i), orig_name_to_data[new_name]
)


def test_channel_mismatch_rename(tmp_path):
(
raw,
ch_order_snirf,
ch_order_bids,
channels_fname,
orig_name_to_loc,
orig_name_to_data,
) = _setup_nirs_channel_mismatch(tmp_path)
with (
pytest.warns(
RuntimeWarning,
match=r"Channel mismatch between .*channels\.tsv and the raw data file detected\. Using mismatch strategy: rename\.",
),
):
raw_out_rename = _handle_channels_reading(
channels_fname, raw.copy(), ch_name_mismatch="rename"
)
assert raw_out_rename.ch_names == ch_order_bids
for i in range(len(ch_order_bids)):
orig_name_at_i = ch_order_snirf[i]
np.testing.assert_allclose(
raw_out_rename.info["chs"][i]["loc"], orig_name_to_loc[orig_name_at_i]
)
np.testing.assert_allclose(
raw_out_rename.get_data(picks=i), orig_name_to_data[orig_name_at_i]
)


def test_events_file_to_annotation_kwargs(tmp_path):
"""Test that events file is read correctly."""
bids_path = BIDSPath(
Expand Down
4 changes: 3 additions & 1 deletion mne_bids/tests/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
no_hand=r"ignore:Unable to map.*\n.*subject handedness.*:RuntimeWarning:mne",
no_montage=r"ignore:Not setting position of.*channel found in "
r"montage.*:RuntimeWarning:mne",
channel_mismatch="ignore:Channel mismatch between .*channels\\.tsv and the raw data file detected\\.:RuntimeWarning:mne",
)


Expand Down Expand Up @@ -1398,6 +1399,7 @@ def test_vhdr(_bids_validate, tmp_path):
warning_str["cnt_warning2"],
warning_str["no_hand"],
warning_str["no_montage"],
warning_str["channel_mismatch"],
)
@testing.requires_testing_data
def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
Expand Down Expand Up @@ -1488,7 +1490,7 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
# Reading the file back should still work, even though we've renamed
# some channels (there's now a mismatch between BIDS and Raw channel
# names, and BIDS should take precedence)
raw_read = read_raw_bids(bids_path=bids_path)
raw_read = read_raw_bids(bids_path=bids_path, ch_name_mismatch="rename")
assert raw_read.ch_names[0] == "EOGtest"
assert raw_read.ch_names[1] == "EMG"

Expand Down
Loading