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 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
45 changes: 44 additions & 1 deletion mne/_fiff/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,46 @@ 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 that ref_dict channel exist in instance:
ref_dict_channels = list(
set(
list(ref_dict.keys())
+ [item for sublist in ref_dict.values() for item in sublist]
qian-chu marked this conversation as resolved.
Show resolved Hide resolved
)
)
assert all([ref_ch in inst.ch_names for ref_ch in ref_dict_channels]), (
"The custom referencing dictionary "
"contains channels which are not in the "
"instance!"
)
# Raise a warning if one of the reference channel is bad:
if any([ch in ref_dict_channels for ch in inst.info["bads"]]):
msg = "Channels in the reference scheme are marked as bad!"
_on_missing("warns", msg)

# Copy the data instance to re-reference:
ref_to_data = inst.copy()._data
if len(ref_dict) > 0:
# Loop through each channel to re-reference:
for ch in ref_dict.keys():
assert len(ref_dict[ch]) > 0, f"No channel to re-reference ch-{ch}"
# Get indices of the channels to use as reference
ref_from = pick_channels(inst.ch_names, ref_dict[ch], ordered=True)
# Get indice of channel to re.reference:
ref_to = pick_channels(inst.ch_names, ch, ordered=True)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can add a warning around here if ref_from and ref_to belong to different channel types

# Compute the reference data:
ref_data = inst._data[..., ref_from, :].mean(-2, keepdims=True)
# Subtract the reference data to the channel to re-reference:
ref_to_data[..., ref_to, :] -= ref_data
# Add the data back to the instance:
inst._data = ref_to_data
# Set that custom reference was applied:
inst.info["custom_ref_applied"] = FIFF.FIFFV_MNE_CUSTOM_REF_ON
return inst


@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 @@ -430,7 +470,10 @@ def set_eeg_reference(
"reference."
)

return _apply_reference(inst, ref_channels, ch_sel, forward, ch_type=ch_type)
if isinstance(ref_channels, dict):
return _apply_dict_reference(inst, ref_channels, ch_type=ch_type)
else:
return _apply_reference(inst, ref_channels, ch_sel, forward, ch_type=ch_type)


def _get_ch_type(inst, ch_type):
Expand Down
11 changes: 9 additions & 2 deletions mne/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3660,13 +3660,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