Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
52 changes: 48 additions & 4 deletions mne_bids/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,28 @@ def _get_bads_from_tsv_data(tsv_data):
return bads


def _handle_channels_reading(channels_fname, raw):
def _handle_channel_mismatch(raw, on_ch_mismatch, ch_names_tsv, channels_fname):
"""Handle mismatch between channels.tsv and raw channel names."""
if on_ch_mismatch == "raise":
raise RuntimeError(
f"Channel mismatch between {channels_fname} and the raw data file detected."
f"Either align channel names in channels.tsv with the raw file, or call "
f"read_raw_bids(on_ch_mismatch='reorder'|'rename') to proceed."
)
warn(
"Channel mismatch between "
f"{channels_fname} and the raw data file detected. "
f"Using mismatch strategy: {on_ch_mismatch}."
)
if on_ch_mismatch == "reorder":
raw.reorder_channels(ch_names_tsv)
elif on_ch_mismatch == "rename":
raw.rename_channels(dict(zip(raw.ch_names, ch_names_tsv)))
else:
raise ValueError("on_ch_mismatch must be one of {'reorder','raise','rename'}")


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

Updates status (bad) and types of channels.
Expand Down Expand Up @@ -847,7 +868,9 @@ 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:
_handle_channel_mismatch(raw, on_ch_mismatch, ch_names_tsv, channels_fname)

# 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 +910,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,
on_ch_mismatch="raise",
verbose=None,
):
"""Read BIDS compatible data.

Expand Down Expand Up @@ -918,6 +946,17 @@ 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).
on_ch_mismatch : str
How to handle a mismatch between channel names in channels.tsv file
and channel names in the raw data file.
Must be one of ``'raise'``, ``'reorder'``, ``'rename'`` (default ``'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 order 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 +983,9 @@ def read_raw_bids(
ValueError
If the specified ``datatype`` cannot be found in the dataset.

RuntimeError
If channels.tsv and the raw file have a channel-name mismatch
and ``on_ch_mismatch`` is 'raise'.
"""
if not isinstance(bids_path, BIDSPath):
raise RuntimeError(
Expand Down Expand Up @@ -1082,7 +1124,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, on_ch_mismatch=on_ch_mismatch
)

# Try to find an associated electrodes.tsv and coordsystem.json
# to get information about the status and type of present channels
Expand Down
126 changes: 125 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,16 @@ 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 "
r"detected\. Using mismatch strategy: reorder\."
),
),
):
raw = read_raw_bids(bids_path, on_ch_mismatch="reorder")
assert raw.ch_names == ch_names_orig


Expand Down Expand Up @@ -1509,6 +1519,120 @@ 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):
"""Raise error when ``on_ch_mismatch='raise'`` and names differ."""
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(), on_ch_mismatch="raise")


def test_channel_mismatch_reorder(tmp_path):
"""Reorder channels to match ``channels.tsv`` ordering."""
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 "
r"detected\. Using mismatch strategy: reorder\."
),
),
):
raw_out = _handle_channels_reading(
channels_fname, raw, on_ch_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):
"""Rename channels to match ``channels.tsv`` names."""
(
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 "
r"detected\. Using mismatch strategy: rename\."
),
),
):
raw_out_rename = _handle_channels_reading(
channels_fname, raw.copy(), on_ch_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
7 changes: 6 additions & 1 deletion mne_bids/tests/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
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 +1402,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 +1493,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, on_ch_mismatch="rename")
assert raw_read.ch_names[0] == "EOGtest"
assert raw_read.ch_names[1] == "EMG"

Expand Down