Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for dictionary-type ref_channels in set_eeg_reference() #12366

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1bce965
init the PR draft
qian-chu Jan 16, 2024
f42d5fb
Merge branch 'main' into dict_ref
qian-chu Jan 16, 2024
278fdf3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 16, 2024
f3cff5a
Create 12366.newfeature.rst
qian-chu Jan 16, 2024
b7b5c0c
Update 12366.newfeature.rst
qian-chu Jan 17, 2024
7ece510
Merge branch 'mne-tools:main' into dict_ref
qian-chu May 2, 2024
8d4516d
Add custom reference based on dict
May 3, 2024
42f45b8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 3, 2024
95a1434
BF: use isintance to check if dict
May 3, 2024
c921d69
BF: remove extra copy of data
May 3, 2024
b0c91d2
Add custom reference
May 3, 2024
78a5c7e
change doc (add Alex)
May 3, 2024
ca6908c
Add warning if bad channels in re-referencing scheme
May 3, 2024
6ac7bed
Merge branch 'dict_ref' of https://github.com/qian-chu/mne-python int…
May 3, 2024
b1165b9
add _check_before_dict_reference and enrich set_eeg_reference_see_als…
qian-chu May 3, 2024
ed56c97
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 3, 2024
d27ce12
Update reference.py
qian-chu May 31, 2024
073ca9d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 31, 2024
8d744ca
Update test_reference.py
qian-chu May 31, 2024
9e9507d
Update reference.py
qian-chu Jun 3, 2024
24d16a5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 3, 2024
2a6db40
Update test_reference.py
qian-chu Jun 3, 2024
6699de0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 3, 2024
8a5232a
formatting
qian-chu Jun 3, 2024
73ad30d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 3, 2024
3474370
dict does not accept repeated keys, no need to test
qian-chu Jun 5, 2024
cd213ee
add test for warnings and raises
qian-chu Jun 5, 2024
6c79a60
Update test_reference.py
qian-chu Jun 5, 2024
e36ddd2
Update docs.py
qian-chu Jun 5, 2024
41893f3
Merge branch 'mne-tools:main' into dict_ref
qian-chu Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/devel/12366.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for `dict` type argument ``ref_channels`` to :func:`mne.set_eeg_reference`, to allow flexible re-referencing (e.g. ``raw.set_eeg_reference(ref_channels={'A1': ['A2', 'A3']})`` will set the new A1 data to be ``A1 - (A2 + A3)/2``), by :newcontrib:`Alex Lepauvre` and `Qian Chu`_
2 changes: 2 additions & 0 deletions doc/changes/names.inc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

.. _Alex Kiefer: https://home.alexk101.dev

.. _Alex Lepauvre: https://github.com/AlexLepauvre

.. _Alex Rockhill: https://github.com/alexrockhill/

.. _Alexander Rudiuk: https://github.com/ARudiuk
Expand Down
81 changes: 81 additions & 0 deletions mne/_fiff/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,63 @@ def _check_before_reference(inst, ref_from, ref_to, ch_type):
return ref_to


def _check_before_dict_reference(inst, ref_dict):
"""Prepare instance for dict-based referencing."""
# Check to see that data is preloaded
_check_preload(inst, "Applying a reference")

def check_value_str(inst, value, key_ch_type):
# Check that value is in ch_names
assert (
value in inst.ch_names
), f"Channel {value} in ref_channels is not in the instance"
# If value is a bad channel, issue a warning
if value in inst.info["bads"]:
msg = f"Channel {value} in ref_channels is marked as bad!"
_on_missing("warn", msg)
# If key and value are of different channel types, issue a warning
value_pick = pick_channels(inst.ch_names, [value], ordered=True)
value_ch_type = inst.get_channel_types(picks=value_pick)[0]
if key_ch_type != value_ch_type:
msg = (
f"Channel {key} is of type {DEFAULTS['titles'][key_ch_type]}, "
f"but reference channel {value} is of type "
f"{DEFAULTS['titles'][value_ch_type]}."
)
_on_missing("warn", msg)

for key, value in ref_dict.items():
# Check that keys are strings
assert isinstance(key, str), (
"Keys in dict-type ref_channels must be strings. You provided "
f"{type(key)}."
)
# Check that keys are in ch_names
assert (
key in inst.ch_names
), f"Channel {key} in ref_channels is not in the instance"
key_pick = pick_channels(inst.ch_names, [key], ordered=True)
key_ch_type = inst.get_channel_types(picks=key_pick)[0]
# Check values
if isinstance(value, str):
check_value_str(inst, value, key_ch_type)
elif isinstance(value, list):
for val in value:
# Check that values are strings
assert isinstance(val, str), (
"Values in dict-type ref_channels must be strings or "
"lists of strings. You provided a list of "
f"{type(val)}"
)
check_value_str(inst, val, key_ch_type)
else:
raise ValueError(
"Values in dict-type ref_channels must be strings or "
"lists of strings. You provided "
f"{type(value)}"
)


def _apply_reference(inst, ref_from, ref_to=None, forward=None, ch_type="auto"):
"""Apply a custom EEG referencing scheme."""
ref_to = _check_before_reference(inst, ref_from, ref_to, ch_type)
Expand Down Expand Up @@ -128,6 +185,26 @@ def _apply_reference(inst, ref_from, ref_to=None, forward=None, ch_type="auto"):
return inst, ref_data


def _apply_dict_reference(inst, ref_dict):
"""Apply a dict-based custom EEG referencing scheme."""
_check_before_dict_reference(inst, ref_dict)

orig_data = inst.copy()._data
data = inst._data
if len(ref_dict) > 0:
for key, value in ref_dict.items():
if isinstance(value, str):
value = [value] # pick_channels expects a list
ref_from = pick_channels(inst.ch_names, value, ordered=True)
ref_to = pick_channels(inst.ch_names, [key], ordered=True)
ref_data = orig_data[..., ref_from, :].mean(-2, keepdims=True)
data[..., ref_to, :] -= ref_data

with inst.info._unlock():
inst.info["custom_ref_applied"] = FIFF.FIFFV_MNE_CUSTOM_REF_ON
return inst, data


@fill_doc
def add_reference_channels(inst, ref_channels, copy=True):
"""Add reference channels to data that consists of all zeros.
Expand Down Expand Up @@ -331,6 +408,10 @@ def set_eeg_reference(

_check_can_reref(inst)

if isinstance(ref_channels, dict):
logger.info("Applying a custom dict-based reference.")
return _apply_dict_reference(inst, ref_channels)

ch_type = _get_ch_type(inst, ch_type)

if projection: # average reference projector
Expand Down
74 changes: 74 additions & 0 deletions mne/_fiff/tests/test_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,80 @@ def test_set_eeg_reference_rest():
assert 0.995 < exp_var <= 1


@testing.requires_testing_data
@pytest.mark.parametrize(
"ref_channels, expectation",
[
(
{2: "EEG 001"},
pytest.raises(
AssertionError, match=f"Keys in dict-type.*You provided {int}"
),
),
(
{"EEG 001": (1, 2)},
pytest.raises(
ValueError, match=f"Values in dict-type.*You provided {type((1,2))}"
),
),
(
{"EEG 001": [1, 2]},
pytest.raises(
AssertionError,
match="Values in dict-type.*You provided a list of <class 'int'>",
),
),
(
{"EEG 999": "EEG 001"},
pytest.raises(
AssertionError,
match="Channel EEG 999 in ref_channels is not in the instance",
),
),
(
{"EEG 001": "EEG 999"},
pytest.raises(
AssertionError,
match="Channel EEG 999 in ref_channels is not in the instance",
),
),
(
{"EEG 001": "EEG 057"},
pytest.warns(
RuntimeWarning,
match="Channel EEG 057 in ref_channels is marked as bad!",
),
),
(
{"EEG 001": "STI 001"},
pytest.warns(
RuntimeWarning,
match="Channel EEG 001 is of type EEG, but reference channel STI 001 is of type Stimulus.",
),
),
(
{"EEG 001": "EEG 002", "EEG 002": "EEG 002", "EEG 003": "EEG 005"},
nullcontext(),
),
],
)
def test_set_eeg_reference_dict(ref_channels, expectation):
"""Test setting dict-based reference."""
raw = read_raw_fif(fif_fname).crop(0, 1).pick(picks=["eeg", "stim"])
with pytest.raises(
RuntimeError,
match="By default, MNE does not load data.*Applying a reference requires.*",
):
raw.set_eeg_reference(ref_channels=ref_channels)
raw.load_data()
raw.info["bads"] = ["EEG 057"]
with expectation:
raw.set_eeg_reference(ref_channels=ref_channels)

# Data testing goes below here @Alex
# if expectation == nullcontext():


@testing.requires_testing_data
@pytest.mark.parametrize("inst_type", ("raw", "epochs", "evoked"))
def test_set_bipolar_reference(inst_type):
Expand Down
21 changes: 19 additions & 2 deletions mne/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3665,13 +3665,20 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
"""

docdict["ref_channels_set_eeg_reference"] = """
ref_channels : list of str | str
ref_channels : list of str | str | dict
Can be:

- The name(s) of the channel(s) used to construct the reference.
- The name(s) of the channel(s) used to construct the reference for
every channel of ``ch_type``.
- ``'average'`` to apply an average reference (default)
- ``'REST'`` to use the Reference Electrode Standardization Technique
infinity reference :footcite:`Yao2001`.
- A dictionary mapping names of channels to be referenced to (a list of)
names of channels to use as reference. This is the most flexible
re-referencing approaching. For example, {'A1': 'A3'} would replace the
data in channel 'A1' with the difference between 'A1' and 'A3'. To take
the average of multiple channels as reference, supply a list of channel
names as the dictionary value, e.g. {'A1': ['A2', 'A3']}.
- An empty list, in which case MNE will not attempt any re-referencing of
the data
"""
Expand Down Expand Up @@ -3999,6 +4006,16 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
The given EEG electrodes are referenced to a point at infinity using the
lead fields in ``forward``, which helps standardize the signals.

- Different references for different channels
Set ``ref_channels`` to a dictionary mapping source channel names (str)
to the reference channel names (str or list of str). Unlike the other
approaches where the same reference is applied globally, you can set
different references for different channels with this method. For example,
to re-reference channel 'A1' to 'A2' and 'B1' to the average of 'B2' and
'B3', set ``ref_channels={'A1': 'A2', 'B1': ['B2', 'B3']}``. Warnings are
issued when a bad channel is used as a reference or when a mapping involves
channels of different types.

1. If a reference is requested that is not the average reference, this
function removes any pre-existing average reference projections.

Expand Down