From acc56e558383eed45eee5819d9c9b0119d4b1577 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Wed, 15 Jul 2020 18:38:10 +0100 Subject: [PATCH 01/22] Add method to combine channels --- mne/channels/channels.py | 116 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 552d3cce9c9..781e603aeec 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -10,6 +10,8 @@ import os import os.path as op import sys +from copy import deepcopy +from functools import partial import numpy as np from scipy import sparse @@ -19,7 +21,7 @@ _check_preload, _validate_type, fill_doc, _check_option) from ..io.compensator import get_current_comp from ..io.constants import FIFF -from ..io.meas_info import anonymize_info, Info, MontageMixin +from ..io.meas_info import anonymize_info, Info, MontageMixin, create_info from ..io.pick import (channel_type, pick_info, pick_types, _picks_by_type, _check_excludes_includes, _contains_ch_type, channel_indices_by_type, pick_channels, _picks_to_idx, @@ -987,6 +989,118 @@ def add_channels(self, add_list, force_update_info=False): assert all(len(r) == self.info['nchan'] for r in self._read_picks) return self + def combine_channels(self, group_by, method='mean'): + """Combine channels based on regions of interest (ROIs). + + Parameters + ---------- + group_by : dict + Specifies which channels are aggregated into a single channel, with + aggregation method determined by the ``method`` parameter. One new + pseudo-channel is made per dict entry; the dict values must be + lists of picks (integer indices of ``ch_names``). For example:: + + group_by=dict(Left_ROI=[1, 2, 3, 4], Right_ROI=[5, 6, 7, 8]) + + Note that within a dict entry all channels must have the same type. + %(method)s + If callable, the callable must accept one positional input (data of + shape ``(n_channels, n_times)``, ``(n_epochs, n_channels, + n_times)``, or ``(n_evokeds, n_channels, n_time)``) and return an + :class:`array ` of shape ``(n_times)``, ``(n_epochs, + n_times)``, or ``(n_evokeds, n_times)``. For example with an + instance of Raw:: + + method = lambda data: np.median(data, axis=0) + + Defaults to ``mean``. + + Returns + ------- + inst : instance of Raw, Epochs, or Evoked + A new instance. + """ + from ..io import BaseRaw, RawArray + from ..epochs import BaseEpochs + from ..evoked import Evoked + from .. import EpochsArray, EvokedArray + + # Check to see if data is preloaded + _check_preload(self, 'Combining channels') + + ch_axis = len(self._data.shape) - 2 # idx of the channel axis + ch_idx = _picks_to_idx(self.info, None) + ch_types = _get_channel_types(self.info) + group_by = deepcopy(group_by) + + # Convert string values of ``method`` into callables + if isinstance(method, str): + method_dict = {key: partial(getattr(np, key), axis=ch_axis) + for key in ('mean', 'median', 'std')} + try: + method = method_dict[method] + except KeyError: + raise ValueError('"method" must be a callable, or one of ' + '"mean", "median", or "std"; got {}' + ''.format(method)) + + # Check correctness of combinations + for this_group, these_picks in group_by.items(): + # Check if channel indices are out of bounds + if not all(idx in ch_idx for idx in these_picks): + raise ValueError('Some channel indices are out of bounds.') + # Check if heterogeneous sensor type combinations + this_ch_type = np.array(ch_types)[these_picks] + if len(set(this_ch_type)) > 1: + types = ', '.join(set(this_ch_type)) + raise ValueError('Cannot combine sensors of different types; ' + '"{}" contains types {}.' + ''.format(this_group, types)) + # Check if combining less than 2 channel + if len(these_picks) < 2: + raise ValueError('Less than 2 channels in group "{}"; cannot ' + 'combine by method "{}".' + ''.format(this_group, method)) + # If all good create more detailed dict with channel type + group_by[this_group] = dict(picks=these_picks, + ch_type=this_ch_type[0]) + + try: + # Extract stim channels if any + inst = self.copy().pick_types(meg=False, stim=True) + # Create clean info to avoid inconsistencies + inst.info = create_info(sfreq=self.info['sfreq'], + ch_names=inst.info['ch_names'], + ch_types=_get_channel_types(inst.info)) + except ValueError: + inst = None + + # Combine channels and add them to the new instance + for this_group, this_group_dict in group_by.items(): + these_picks = this_group_dict['picks'] + this_ch_type = this_group_dict['ch_type'] + this_data = np.take(self._data, these_picks, axis=ch_axis) + this_group_data = np.expand_dims(method(this_data), axis=ch_axis) + this_info = create_info(sfreq=self.info['sfreq'], + ch_names=[this_group], + ch_types=[this_ch_type]) + this_inst = None + if isinstance(self, BaseRaw): + this_inst = RawArray(this_group_data, this_info, + first_samp=self.first_samp) + elif isinstance(self, BaseEpochs): + this_inst = EpochsArray(this_group_data, this_info, + tmin=self.times[0]) + elif isinstance(self, Evoked): + this_inst = EvokedArray(this_group_data, this_info, + tmin=self.times[0]) + if inst is None: + inst = this_inst + else: + inst.add_channels([this_inst]) + + return inst + class InterpolationMixin(object): """Mixin class for Raw, Evoked, Epochs.""" From e0189a75f479f1c3026bca0757e82694fc348b73 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 16 Jul 2020 12:06:24 +0100 Subject: [PATCH 02/22] Use f-string in combine_channels Co-authored-by: Daniel McCloy --- mne/channels/channels.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 781e603aeec..2dd5ff73902 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1054,8 +1054,7 @@ def combine_channels(self, group_by, method='mean'): if len(set(this_ch_type)) > 1: types = ', '.join(set(this_ch_type)) raise ValueError('Cannot combine sensors of different types; ' - '"{}" contains types {}.' - ''.format(this_group, types)) + f'"{this_group}" contains types {types}.') # Check if combining less than 2 channel if len(these_picks) < 2: raise ValueError('Less than 2 channels in group "{}"; cannot ' From c7da6405b46b721209f410623b73bc7dd95d7107 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 16 Jul 2020 18:13:57 +0100 Subject: [PATCH 03/22] Give the option to keep stim channel or not --- mne/channels/channels.py | 59 +++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 2dd5ff73902..d81f7558f90 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -989,22 +989,24 @@ def add_channels(self, add_list, force_update_info=False): assert all(len(r) == self.info['nchan'] for r in self._read_picks) return self - def combine_channels(self, group_by, method='mean'): + def combine_channels(self, groups, method='mean', keep_stim=False): """Combine channels based on regions of interest (ROIs). Parameters ---------- - group_by : dict + groups : dict Specifies which channels are aggregated into a single channel, with aggregation method determined by the ``method`` parameter. One new pseudo-channel is made per dict entry; the dict values must be lists of picks (integer indices of ``ch_names``). For example:: - group_by=dict(Left_ROI=[1, 2, 3, 4], Right_ROI=[5, 6, 7, 8]) + groups=dict(Left_ROI=[1, 2, 3, 4], Right_ROI=[5, 6, 7, 8]) Note that within a dict entry all channels must have the same type. - %(method)s - If callable, the callable must accept one positional input (data of + method : str | callable + Which method to use to combine channels. If a :class:`str`, must be + one of 'mean', 'median', or 'std' (standard deviation). If + callable, the callable must accept one positional input (data of shape ``(n_channels, n_times)``, ``(n_epochs, n_channels, n_times)``, or ``(n_evokeds, n_channels, n_time)``) and return an :class:`array ` of shape ``(n_times)``, ``(n_epochs, @@ -1014,6 +1016,8 @@ def combine_channels(self, group_by, method='mean'): method = lambda data: np.median(data, axis=0) Defaults to ``mean``. + keep_stim : bool + If True (default False), keep stimulus channels. Returns ------- @@ -1031,7 +1035,7 @@ def combine_channels(self, group_by, method='mean'): ch_axis = len(self._data.shape) - 2 # idx of the channel axis ch_idx = _picks_to_idx(self.info, None) ch_types = _get_channel_types(self.info) - group_by = deepcopy(group_by) + groups = deepcopy(groups) # Convert string values of ``method`` into callables if isinstance(method, str): @@ -1041,11 +1045,26 @@ def combine_channels(self, group_by, method='mean'): method = method_dict[method] except KeyError: raise ValueError('"method" must be a callable, or one of ' - '"mean", "median", or "std"; got {}' - ''.format(method)) + f'"mean", "median", or "std"; got {method}.') + + # Create new instance + if not isinstance(keep_stim, bool): + raise ValueError('"keep_stim" must be of type bool, not ' + f'{type(keep_stim)}.') + if keep_stim is True: + try: + inst = self.copy().pick_types(meg=False, stim=True) + # Create clean info to avoid inconsistencies + inst.info = create_info(sfreq=self.info['sfreq'], + ch_names=inst.info['ch_names'], + ch_types=_get_channel_types(inst.info)) + except ValueError: + raise ValueError('Could not find stimulus channels.') + else: + inst = None # Check correctness of combinations - for this_group, these_picks in group_by.items(): + for this_group, these_picks in groups.items(): # Check if channel indices are out of bounds if not all(idx in ch_idx for idx in these_picks): raise ValueError('Some channel indices are out of bounds.') @@ -1057,25 +1076,15 @@ def combine_channels(self, group_by, method='mean'): f'"{this_group}" contains types {types}.') # Check if combining less than 2 channel if len(these_picks) < 2: - raise ValueError('Less than 2 channels in group "{}"; cannot ' - 'combine by method "{}".' - ''.format(this_group, method)) + raise ValueError('Less than 2 channels in group ' + f'"{this_group}"; cannot combine by method ' + f'"{method}".') # If all good create more detailed dict with channel type - group_by[this_group] = dict(picks=these_picks, - ch_type=this_ch_type[0]) - - try: - # Extract stim channels if any - inst = self.copy().pick_types(meg=False, stim=True) - # Create clean info to avoid inconsistencies - inst.info = create_info(sfreq=self.info['sfreq'], - ch_names=inst.info['ch_names'], - ch_types=_get_channel_types(inst.info)) - except ValueError: - inst = None + groups[this_group] = dict(picks=these_picks, + ch_type=this_ch_type[0]) # Combine channels and add them to the new instance - for this_group, this_group_dict in group_by.items(): + for this_group, this_group_dict in groups.items(): these_picks = this_group_dict['picks'] this_ch_type = this_group_dict['ch_type'] this_data = np.take(self._data, these_picks, axis=ch_axis) From a5166170d8e4a4ff433bc6906c1840370d3214d8 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 16 Jul 2020 19:28:35 +0100 Subject: [PATCH 04/22] Convert method into function --- mne/channels/__init__.py | 4 +- mne/channels/channels.py | 246 ++++++++++++++-------------- mne/channels/tests/test_channels.py | 5 + 3 files changed, 133 insertions(+), 122 deletions(-) diff --git a/mne/channels/__init__.py b/mne/channels/__init__.py index 771a3628d18..afa26af4818 100644 --- a/mne/channels/__init__.py +++ b/mne/channels/__init__.py @@ -15,8 +15,8 @@ read_custom_montage, read_dig_hpts, compute_native_head_t) from .channels import (equalize_channels, rename_channels, fix_mag_coil_types, - read_ch_adjacency, _get_ch_type, - find_ch_adjacency, make_1020_channel_selections) + read_ch_adjacency, _get_ch_type, find_ch_adjacency, + make_1020_channel_selections, combine_channels) from ..utils import deprecated_alias deprecated_alias('read_ch_connectivity', read_ch_adjacency) deprecated_alias('find_ch_connectivity', find_ch_adjacency) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index d81f7558f90..3415ea1875f 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -989,126 +989,6 @@ def add_channels(self, add_list, force_update_info=False): assert all(len(r) == self.info['nchan'] for r in self._read_picks) return self - def combine_channels(self, groups, method='mean', keep_stim=False): - """Combine channels based on regions of interest (ROIs). - - Parameters - ---------- - groups : dict - Specifies which channels are aggregated into a single channel, with - aggregation method determined by the ``method`` parameter. One new - pseudo-channel is made per dict entry; the dict values must be - lists of picks (integer indices of ``ch_names``). For example:: - - groups=dict(Left_ROI=[1, 2, 3, 4], Right_ROI=[5, 6, 7, 8]) - - Note that within a dict entry all channels must have the same type. - method : str | callable - Which method to use to combine channels. If a :class:`str`, must be - one of 'mean', 'median', or 'std' (standard deviation). If - callable, the callable must accept one positional input (data of - shape ``(n_channels, n_times)``, ``(n_epochs, n_channels, - n_times)``, or ``(n_evokeds, n_channels, n_time)``) and return an - :class:`array ` of shape ``(n_times)``, ``(n_epochs, - n_times)``, or ``(n_evokeds, n_times)``. For example with an - instance of Raw:: - - method = lambda data: np.median(data, axis=0) - - Defaults to ``mean``. - keep_stim : bool - If True (default False), keep stimulus channels. - - Returns - ------- - inst : instance of Raw, Epochs, or Evoked - A new instance. - """ - from ..io import BaseRaw, RawArray - from ..epochs import BaseEpochs - from ..evoked import Evoked - from .. import EpochsArray, EvokedArray - - # Check to see if data is preloaded - _check_preload(self, 'Combining channels') - - ch_axis = len(self._data.shape) - 2 # idx of the channel axis - ch_idx = _picks_to_idx(self.info, None) - ch_types = _get_channel_types(self.info) - groups = deepcopy(groups) - - # Convert string values of ``method`` into callables - if isinstance(method, str): - method_dict = {key: partial(getattr(np, key), axis=ch_axis) - for key in ('mean', 'median', 'std')} - try: - method = method_dict[method] - except KeyError: - raise ValueError('"method" must be a callable, or one of ' - f'"mean", "median", or "std"; got {method}.') - - # Create new instance - if not isinstance(keep_stim, bool): - raise ValueError('"keep_stim" must be of type bool, not ' - f'{type(keep_stim)}.') - if keep_stim is True: - try: - inst = self.copy().pick_types(meg=False, stim=True) - # Create clean info to avoid inconsistencies - inst.info = create_info(sfreq=self.info['sfreq'], - ch_names=inst.info['ch_names'], - ch_types=_get_channel_types(inst.info)) - except ValueError: - raise ValueError('Could not find stimulus channels.') - else: - inst = None - - # Check correctness of combinations - for this_group, these_picks in groups.items(): - # Check if channel indices are out of bounds - if not all(idx in ch_idx for idx in these_picks): - raise ValueError('Some channel indices are out of bounds.') - # Check if heterogeneous sensor type combinations - this_ch_type = np.array(ch_types)[these_picks] - if len(set(this_ch_type)) > 1: - types = ', '.join(set(this_ch_type)) - raise ValueError('Cannot combine sensors of different types; ' - f'"{this_group}" contains types {types}.') - # Check if combining less than 2 channel - if len(these_picks) < 2: - raise ValueError('Less than 2 channels in group ' - f'"{this_group}"; cannot combine by method ' - f'"{method}".') - # If all good create more detailed dict with channel type - groups[this_group] = dict(picks=these_picks, - ch_type=this_ch_type[0]) - - # Combine channels and add them to the new instance - for this_group, this_group_dict in groups.items(): - these_picks = this_group_dict['picks'] - this_ch_type = this_group_dict['ch_type'] - this_data = np.take(self._data, these_picks, axis=ch_axis) - this_group_data = np.expand_dims(method(this_data), axis=ch_axis) - this_info = create_info(sfreq=self.info['sfreq'], - ch_names=[this_group], - ch_types=[this_ch_type]) - this_inst = None - if isinstance(self, BaseRaw): - this_inst = RawArray(this_group_data, this_info, - first_samp=self.first_samp) - elif isinstance(self, BaseEpochs): - this_inst = EpochsArray(this_group_data, this_info, - tmin=self.times[0]) - elif isinstance(self, Evoked): - this_inst = EvokedArray(this_group_data, this_info, - tmin=self.times[0]) - if inst is None: - inst = this_inst - else: - inst.add_channels([this_inst]) - - return inst - class InterpolationMixin(object): """Mixin class for Raw, Evoked, Epochs.""" @@ -1641,3 +1521,129 @@ def make_1020_channel_selections(info, midline="z"): for selection, picks in selections.items()} return selections + + +def combine_channels(instance, groups, method='mean', keep_stim=False): + """Combine channels based on regions of interest (ROIs). + + Parameters + ---------- + instance : instance of Raw, Epochs, or Evoked + An MNE-Python object to combine the channels for. The object can be of + type Raw, Epochs, or Evoked. + groups : dict + Specifies which channels are aggregated into a single channel, with + aggregation method determined by the ``method`` parameter. One new + pseudo-channel is made per dict entry; the dict values must be lists of + picks (integer indices of ``ch_names``). For example:: + + groups=dict(Left_ROI=[1, 2, 3, 4], Right_ROI=[5, 6, 7, 8]) + + Note that within a dict entry all channels must have the same type. + method : str | callable + Which method to use to combine channels. If a :class:`str`, must be one + of 'mean', 'median', or 'std' (standard deviation). If callable, the + callable must accept one positional input (data of shape ``(n_channels, + n_times)``, ``(n_epochs, n_channels, n_times)``, or ``(n_evokeds, + n_channels, n_time)``) and return an :class:`array ` of + shape ``(n_times)``, ``(n_epochs, n_times)``, or ``(n_evokeds, + n_times)``. For example with an instance of Raw:: + + method = lambda data: np.median(data, axis=0) + + Defaults to ``mean``. + keep_stim : bool + If True (default False), keep stimulus channels. + + Returns + ------- + combined_inst : instance of Raw, Epochs, or Evoked + An MNE-Python object with channels combined for each ROI defined in the + ``groups`` parameter. + """ + from ..io import BaseRaw, RawArray + from ..epochs import BaseEpochs + from ..evoked import Evoked + from .. import EpochsArray, EvokedArray + + # Check to see if data is preloaded + _check_preload(instance, 'Combining channels') + + ch_axis = len(instance._data.shape) - 2 # idx of the channel axis + ch_idx = _picks_to_idx(instance.info, None) + ch_types = _get_channel_types(instance.info) + groups = deepcopy(groups) + + # Convert string values of ``method`` into callables + # XXX Possibly de-duplicate with _make_combine_callable of mne/viz/utils.py + if isinstance(method, str): + method_dict = {key: partial(getattr(np, key), axis=ch_axis) + for key in ('mean', 'median', 'std')} + try: + method = method_dict[method] + except KeyError: + raise ValueError('"method" must be a callable, or one of ' + f'"mean", "median", or "std"; got "{method}".') + + # Create new instance + if not isinstance(keep_stim, bool): + raise ValueError('"keep_stim" must be of type bool, not ' + f'{type(keep_stim)}.') + if keep_stim is True: + try: + combined_inst = instance.copy().pick_types(meg=False, stim=True) + # Create clean info to avoid inconsistencies + stim_ch_names = combined_inst.info['ch_names'] + stim_ch_types = _get_channel_types(combined_inst.info) + combined_inst.info = create_info(sfreq=instance.info['sfreq'], + ch_names=stim_ch_names, + ch_types=stim_ch_types) + except ValueError: + raise ValueError('Could not find stimulus channels.') + else: + combined_inst = None + + # Check correctness of combinations + for this_group, these_picks in groups.items(): + # Check if channel indices are out of bounds + if not all(idx in ch_idx for idx in these_picks): + raise ValueError('Some channel indices are out of bounds.') + # Check if heterogeneous sensor type combinations + this_ch_type = np.array(ch_types)[these_picks] + if len(set(this_ch_type)) > 1: + types = ', '.join(set(this_ch_type)) + raise ValueError('Cannot combine sensors of different types; ' + f'"{this_group}" contains types {types}.') + # Check if combining less than 2 channel + if len(set(these_picks)) < 2: + raise ValueError('Cannot combine less than 2 different channels ' + f'in group "{this_group}".') + # If all good create more detailed dict with channel type + groups[this_group] = dict(picks=these_picks, + ch_type=this_ch_type[0]) + + # Combine channels and add them to the new instance + for this_group, this_group_dict in groups.items(): + these_picks = this_group_dict['picks'] + this_ch_type = this_group_dict['ch_type'] + this_data = np.take(instance._data, these_picks, axis=ch_axis) + this_group_data = np.expand_dims(method(this_data), axis=ch_axis) + this_info = create_info(sfreq=instance.info['sfreq'], + ch_names=[this_group], + ch_types=[this_ch_type]) + this_inst = None + if isinstance(instance, BaseRaw): + this_inst = RawArray(this_group_data, this_info, + first_samp=instance.first_samp) + elif isinstance(instance, BaseEpochs): + this_inst = EpochsArray(this_group_data, this_info, + tmin=instance.times[0]) + elif isinstance(instance, Evoked): + this_inst = EvokedArray(this_group_data, this_info, + tmin=instance.times[0]) + if combined_inst is None: + combined_inst = this_inst + else: + combined_inst.add_channels([this_inst]) + + return combined_inst diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index d013d880a45..c29f7f429e9 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -360,4 +360,9 @@ def test_equalize_channels(): assert epochs is epochs2 +def test_combine_channels(): + # TODO + pass + + run_tests_if_main() From af3e80fbf0a9fb23ba0b0323530938eebaeef341 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 17 Jul 2020 09:48:36 +0100 Subject: [PATCH 05/22] Create a single new instance --- mne/channels/channels.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 3415ea1875f..7995855942d 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1623,27 +1623,28 @@ def combine_channels(instance, groups, method='mean', keep_stim=False): ch_type=this_ch_type[0]) # Combine channels and add them to the new instance + group_ch_names, group_ch_types, group_data = [], [], [] for this_group, this_group_dict in groups.items(): + group_ch_names.append(this_group) + group_ch_types.append(this_group_dict['ch_type']) these_picks = this_group_dict['picks'] - this_ch_type = this_group_dict['ch_type'] this_data = np.take(instance._data, these_picks, axis=ch_axis) - this_group_data = np.expand_dims(method(this_data), axis=ch_axis) - this_info = create_info(sfreq=instance.info['sfreq'], - ch_names=[this_group], - ch_types=[this_ch_type]) - this_inst = None - if isinstance(instance, BaseRaw): - this_inst = RawArray(this_group_data, this_info, - first_samp=instance.first_samp) - elif isinstance(instance, BaseEpochs): - this_inst = EpochsArray(this_group_data, this_info, - tmin=instance.times[0]) - elif isinstance(instance, Evoked): - this_inst = EvokedArray(this_group_data, this_info, - tmin=instance.times[0]) - if combined_inst is None: - combined_inst = this_inst - else: - combined_inst.add_channels([this_inst]) + group_data.append(method(this_data)) + group_data = np.swapaxes(group_data, 0, ch_axis) + info = create_info(sfreq=instance.info['sfreq'], ch_names=group_ch_names, + ch_types=group_ch_types) + if isinstance(instance, BaseRaw): + new_inst = RawArray(group_data, info, first_samp=instance.first_samp, + verbose=instance.verbose) + elif isinstance(instance, BaseEpochs): + new_inst = EpochsArray(group_data, info, tmin=instance.times[0], + verbose=instance.verbose) + elif isinstance(instance, Evoked): + new_inst = EvokedArray(group_data, info, tmin=instance.times[0], + verbose=instance.verbose) + if combined_inst is None: + combined_inst = new_inst + else: + combined_inst.add_channels([new_inst]) return combined_inst From 64970b47b217b6d903c43a2162133afaa410dacc Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 17 Jul 2020 14:06:54 +0100 Subject: [PATCH 06/22] Add tests for combine_channels --- mne/channels/channels.py | 4 +-- mne/channels/tests/test_channels.py | 42 ++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 7995855942d..373272d7a77 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1587,8 +1587,8 @@ def combine_channels(instance, groups, method='mean', keep_stim=False): # Create new instance if not isinstance(keep_stim, bool): - raise ValueError('"keep_stim" must be of type bool, not ' - f'{type(keep_stim)}.') + raise TypeError('"keep_stim" must be of type bool, not ' + f'{type(keep_stim)}.') if keep_stim is True: try: combined_inst = instance.copy().pick_types(meg=False, stim=True) diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index c29f7f429e9..ea8c7038ea5 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -13,7 +13,7 @@ from scipy.io import savemat from numpy.testing import assert_array_equal, assert_equal -from mne.channels import (rename_channels, read_ch_adjacency, +from mne.channels import (rename_channels, read_ch_adjacency, combine_channels, find_ch_adjacency, make_1020_channel_selections, read_custom_montage, equalize_channels) from mne.channels.channels import (_ch_neighbor_adjacency, @@ -23,12 +23,13 @@ from mne.io.constants import FIFF from mne.utils import _TempDir, run_tests_if_main from mne import (pick_types, pick_channels, EpochsArray, EvokedArray, - make_ad_hoc_cov, create_info) + make_ad_hoc_cov, create_info, read_events, Epochs) from mne.datasets import testing io_dir = op.join(op.dirname(__file__), '..', '..', 'io') base_dir = op.join(io_dir, 'tests', 'data') raw_fname = op.join(base_dir, 'test_raw.fif') +eve_fname = op .join(base_dir, 'test-eve.fif') fname_kit_157 = op.join(io_dir, 'kit', 'tests', 'data', 'test.sqd') @@ -361,8 +362,41 @@ def test_equalize_channels(): def test_combine_channels(): - # TODO - pass + """Test channel combination on Raw, Epochs, and Evoked""" + raw = read_raw_fif(raw_fname, preload=True) + epochs = Epochs(raw, read_events(eve_fname), preload=True) + evoked = epochs.average() + good = dict(foo=[0, 1], bar=[5, 2]) # good grad and mag + + # Test good cases + combine_channels(raw, good) + combine_channels(epochs, good) + combine_channels(evoked, good) + combine_channels(raw, good, keep_stim=True) + + # Test result with one ROI + good_single = dict(foo=[0, 1]) # good grad + combined_data = combine_channels(raw, good_single)._data + foo_mean = np.mean(raw._data[good_single['foo']], axis=0) + assert np.array_equal(combined_data, np.expand_dims(foo_mean, axis=0)) + + # Test bad cases + raw_no_preload = read_raw_fif(raw_fname, preload=False) + raw_no_stim = read_raw_fif(raw_fname, preload=True) + raw_no_stim.pick_types(meg=True, stim=False) + bad1 = dict(foo=[0, 376], bar=[5, 2]) # out of bounds + bad2 = dict(foo=[0, 2], bar=[5, 2]) # type mix in same group + bad3 = dict(foo=[0, 0], bar=[5, 2]) # same channel in same group + bad4 = dict(foo=[0], bar=[5, 2]) # one channel + pytest.raises(RuntimeError, combine_channels, raw_no_preload, good) + pytest.raises(ValueError, combine_channels, raw, good, method='bad_method') + pytest.raises(TypeError, combine_channels, raw, good, keep_stim='bad_type') + pytest.raises(ValueError, combine_channels, raw_no_stim, good, + keep_stim=True) + pytest.raises(ValueError, combine_channels, raw, bad1) + pytest.raises(ValueError, combine_channels, raw, bad2) + pytest.raises(ValueError, combine_channels, raw, bad3) + pytest.raises(ValueError, combine_channels, raw, bad4) run_tests_if_main() From f0e138af8e35003d02929be15ee7b5401de610a6 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 17 Jul 2020 14:12:17 +0100 Subject: [PATCH 07/22] Update changelog --- doc/changes/latest.inc | 2 ++ doc/changes/names.inc | 2 ++ 2 files changed, 4 insertions(+) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 211b844719d..b4d984c935f 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -125,6 +125,8 @@ Changelog - BrainVision data format files are now parsed for EEG impedance values in :func:`mne.io.read_raw_brainvision` and provided as a ``.impedances`` attribute of ``raw`` by `Stefan Appelhoff`_ and `Jan Sedivy`_ +- Add function :func:`mne.channels.combine_channels` to combine channels from Raw, Epochs, or Evoked according to ROIs (combinations including mean, median, or standard deviation; can also use a callable) by `Johann Benerradi`_ + Bug ~~~ diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 3e9471b7b5b..7f027dec383 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -309,3 +309,5 @@ .. _Ezequiel Mikulan: https://github.com/ezemikulan .. _Jan Sedivy: https://github.com/honzaseda + +.. _Johann Benerradi: https://github.com/HanBnrd From 15c2fed46d72b85860fe8613c95cd3d7e2d96ede Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 17 Jul 2020 15:12:26 +0100 Subject: [PATCH 08/22] Fix pydocstyle --- mne/channels/tests/test_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index ea8c7038ea5..bf60e5df1f0 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -362,7 +362,7 @@ def test_equalize_channels(): def test_combine_channels(): - """Test channel combination on Raw, Epochs, and Evoked""" + """Test channel combination on Raw, Epochs, and Evoked.""" raw = read_raw_fif(raw_fname, preload=True) epochs = Epochs(raw, read_events(eve_fname), preload=True) evoked = epochs.average() From 780a978455fdbc8eb0ac78d4b88ad39f6eb76b5b Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sun, 19 Jul 2020 11:37:28 +0100 Subject: [PATCH 09/22] Replace error by warning for single channels --- mne/channels/channels.py | 4 ++-- mne/channels/tests/test_channels.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 35754b48ecc..e421e67ca88 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1633,8 +1633,8 @@ def combine_channels(instance, groups, method='mean', keep_stim=False): f'"{this_group}" contains types {types}.') # Check if combining less than 2 channel if len(set(these_picks)) < 2: - raise ValueError('Cannot combine less than 2 different channels ' - f'in group "{this_group}".') + warn(f'Only one channel in group "{this_group}"; cannot combine ' + f'by method "{method}".') # If all good create more detailed dict with channel type groups[this_group] = dict(picks=these_picks, ch_type=this_ch_type[0]) diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index bf60e5df1f0..42096472856 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -386,8 +386,6 @@ def test_combine_channels(): raw_no_stim.pick_types(meg=True, stim=False) bad1 = dict(foo=[0, 376], bar=[5, 2]) # out of bounds bad2 = dict(foo=[0, 2], bar=[5, 2]) # type mix in same group - bad3 = dict(foo=[0, 0], bar=[5, 2]) # same channel in same group - bad4 = dict(foo=[0], bar=[5, 2]) # one channel pytest.raises(RuntimeError, combine_channels, raw_no_preload, good) pytest.raises(ValueError, combine_channels, raw, good, method='bad_method') pytest.raises(TypeError, combine_channels, raw, good, keep_stim='bad_type') @@ -395,8 +393,12 @@ def test_combine_channels(): keep_stim=True) pytest.raises(ValueError, combine_channels, raw, bad1) pytest.raises(ValueError, combine_channels, raw, bad2) - pytest.raises(ValueError, combine_channels, raw, bad3) - pytest.raises(ValueError, combine_channels, raw, bad4) + + # Test warnings + warn1 = dict(foo=[0, 0], bar=[5, 2]) # same channel in same group + warn2 = dict(foo=[0], bar=[5, 2]) # one channel + pytest.warns(RuntimeWarning, combine_channels, raw, warn1) + pytest.warns(RuntimeWarning, combine_channels, raw, warn2) run_tests_if_main() From 7eeebc71241f3aad1bc62aa2bdebf9ce93f5776d Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sun, 19 Jul 2020 13:00:54 +0100 Subject: [PATCH 10/22] Fix bug out of bounds due to stim and bad channels --- mne/channels/channels.py | 2 +- mne/channels/tests/test_channels.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index e421e67ca88..23d56bdbd64 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1587,7 +1587,7 @@ def combine_channels(instance, groups, method='mean', keep_stim=False): _check_preload(instance, 'Combining channels') ch_axis = len(instance._data.shape) - 2 # idx of the channel axis - ch_idx = _picks_to_idx(instance.info, None) + ch_idx = list(range(instance.info['nchan'])) ch_types = _get_channel_types(instance.info) groups = deepcopy(groups) diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 42096472856..5d6ac49a504 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -395,8 +395,8 @@ def test_combine_channels(): pytest.raises(ValueError, combine_channels, raw, bad2) # Test warnings - warn1 = dict(foo=[0, 0], bar=[5, 2]) # same channel in same group - warn2 = dict(foo=[0], bar=[5, 2]) # one channel + warn1 = dict(foo=[375, 375], bar=[5, 2]) # same channel in same group + warn2 = dict(foo=[375], bar=[5, 2]) # one channel (last channel) pytest.warns(RuntimeWarning, combine_channels, raw, warn1) pytest.warns(RuntimeWarning, combine_channels, raw, warn2) From 3b5241a7ec0df6c1aa08adaac15add5cd418da64 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sun, 19 Jul 2020 13:57:42 +0100 Subject: [PATCH 11/22] Add more tests --- mne/channels/tests/test_channels.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 5d6ac49a504..e65d4c834bb 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -366,7 +366,7 @@ def test_combine_channels(): raw = read_raw_fif(raw_fname, preload=True) epochs = Epochs(raw, read_events(eve_fname), preload=True) evoked = epochs.average() - good = dict(foo=[0, 1], bar=[5, 2]) # good grad and mag + good = dict(foo=[0, 1, 3, 4], bar=[5, 2]) # good grad and mag # Test good cases combine_channels(raw, good) @@ -375,10 +375,16 @@ def test_combine_channels(): combine_channels(raw, good, keep_stim=True) # Test result with one ROI - good_single = dict(foo=[0, 1]) # good grad - combined_data = combine_channels(raw, good_single)._data + good_single = dict(foo=[0, 1, 3, 4]) # good grad + combined_mean = combine_channels(raw, good_single, method='mean')._data + combined_median = combine_channels(raw, good_single, method='median')._data + combined_std = combine_channels(raw, good_single, method='std')._data foo_mean = np.mean(raw._data[good_single['foo']], axis=0) - assert np.array_equal(combined_data, np.expand_dims(foo_mean, axis=0)) + foo_median = np.median(raw._data[good_single['foo']], axis=0) + foo_std = np.std(raw._data[good_single['foo']], axis=0) + assert np.array_equal(combined_mean, np.expand_dims(foo_mean, axis=0)) + assert np.array_equal(combined_median, np.expand_dims(foo_median, axis=0)) + assert np.array_equal(combined_std, np.expand_dims(foo_std, axis=0)) # Test bad cases raw_no_preload = read_raw_fif(raw_fname, preload=False) From 0ead668ec9d883a360c71f548e270f6bb6f997fc Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sun, 19 Jul 2020 18:52:41 +0100 Subject: [PATCH 12/22] Add drop_bad parameter and use OrderedDict --- mne/channels/channels.py | 35 +++++++++++++++++++++++------ mne/channels/tests/test_channels.py | 6 +++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 23d56bdbd64..957f336951e 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -10,6 +10,7 @@ import os import os.path as op import sys +from collections import OrderedDict from copy import deepcopy from functools import partial @@ -1540,7 +1541,8 @@ def make_1020_channel_selections(info, midline="z"): return selections -def combine_channels(instance, groups, method='mean', keep_stim=False): +def combine_channels(instance, groups, method='mean', keep_stim=False, + drop_bad=False): """Combine channels based on regions of interest (ROIs). Parameters @@ -1571,6 +1573,8 @@ def combine_channels(instance, groups, method='mean', keep_stim=False): Defaults to ``mean``. keep_stim : bool If True (default False), keep stimulus channels. + drop_bad : bool + If True (default False), drop channels marked as bad. Returns ------- @@ -1589,7 +1593,7 @@ def combine_channels(instance, groups, method='mean', keep_stim=False): ch_axis = len(instance._data.shape) - 2 # idx of the channel axis ch_idx = list(range(instance.info['nchan'])) ch_types = _get_channel_types(instance.info) - groups = deepcopy(groups) + groups = OrderedDict(deepcopy(groups)) # Convert string values of ``method`` into callables # XXX Possibly de-duplicate with _make_combine_callable of mne/viz/utils.py @@ -1620,6 +1624,18 @@ def combine_channels(instance, groups, method='mean', keep_stim=False): else: combined_inst = None + # Get indices of bad channels + if not isinstance(drop_bad, bool): + raise TypeError('"drop_bad" must be of type bool, not ' + f'{type(drop_bad)}.') + if drop_bad is False: + ch_idx_bad = [] + elif not instance.info['bads']: + ch_idx_bad = [] + else: + ch_idx_bad = pick_channels(instance.info['ch_names'], + instance.info['bads']) + # Check correctness of combinations for this_group, these_picks in groups.items(): # Check if channel indices are out of bounds @@ -1631,13 +1647,18 @@ def combine_channels(instance, groups, method='mean', keep_stim=False): types = ', '.join(set(this_ch_type)) raise ValueError('Cannot combine sensors of different types; ' f'"{this_group}" contains types {types}.') + # Remove bad channels + these_bads = [idx for idx in these_picks if idx in ch_idx_bad] + these_picks = [idx for idx in these_picks if idx not in ch_idx_bad] + if these_bads: + logger.info('Dropped the following channels in group ' + f'{this_group}: {these_bads}') # Check if combining less than 2 channel if len(set(these_picks)) < 2: - warn(f'Only one channel in group "{this_group}"; cannot combine ' - f'by method "{method}".') - # If all good create more detailed dict with channel type - groups[this_group] = dict(picks=these_picks, - ch_type=this_ch_type[0]) + warn(f'Less than 2 channels in group "{this_group}"; cannot ' + f'combine by method "{method}".') + # If all good create more detailed dict without bad channels + groups[this_group] = dict(picks=these_picks, ch_type=this_ch_type[0]) # Combine channels and add them to the new instance group_ch_names, group_ch_types, group_data = [], [], [] diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index e65d4c834bb..30669f7a563 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -364,6 +364,8 @@ def test_equalize_channels(): def test_combine_channels(): """Test channel combination on Raw, Epochs, and Evoked.""" raw = read_raw_fif(raw_fname, preload=True) + raw_ch_bad = read_raw_fif(raw_fname, preload=True) + raw_ch_bad.info['bads'] = ['MEG 0113', 'MEG 0112'] epochs = Epochs(raw, read_events(eve_fname), preload=True) evoked = epochs.average() good = dict(foo=[0, 1, 3, 4], bar=[5, 2]) # good grad and mag @@ -373,6 +375,7 @@ def test_combine_channels(): combine_channels(epochs, good) combine_channels(evoked, good) combine_channels(raw, good, keep_stim=True) + combine_channels(raw_ch_bad, good, drop_bad=True) # Test result with one ROI good_single = dict(foo=[0, 1, 3, 4]) # good grad @@ -403,8 +406,11 @@ def test_combine_channels(): # Test warnings warn1 = dict(foo=[375, 375], bar=[5, 2]) # same channel in same group warn2 = dict(foo=[375], bar=[5, 2]) # one channel (last channel) + warn3 = dict(foo=[0, 4], bar=[5, 2]) # one good channel left pytest.warns(RuntimeWarning, combine_channels, raw, warn1) pytest.warns(RuntimeWarning, combine_channels, raw, warn2) + pytest.warns(RuntimeWarning, combine_channels, raw_ch_bad, warn3, + drop_bad=True) run_tests_if_main() From 42511d5708a9550d865a080770d49b0a723eb913 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sun, 19 Jul 2020 20:03:41 +0100 Subject: [PATCH 13/22] Add missing tests --- mne/channels/tests/test_channels.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 30669f7a563..248a622ebc3 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -375,6 +375,7 @@ def test_combine_channels(): combine_channels(epochs, good) combine_channels(evoked, good) combine_channels(raw, good, keep_stim=True) + combine_channels(raw, good, drop_bad=True) combine_channels(raw_ch_bad, good, drop_bad=True) # Test result with one ROI @@ -398,6 +399,7 @@ def test_combine_channels(): pytest.raises(RuntimeError, combine_channels, raw_no_preload, good) pytest.raises(ValueError, combine_channels, raw, good, method='bad_method') pytest.raises(TypeError, combine_channels, raw, good, keep_stim='bad_type') + pytest.raises(TypeError, combine_channels, raw, good, drop_bad='bad_type') pytest.raises(ValueError, combine_channels, raw_no_stim, good, keep_stim=True) pytest.raises(ValueError, combine_channels, raw, bad1) From 200f3ccac444805bc6f6c1b5504f7740235e0983 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Mon, 20 Jul 2020 08:51:58 +0100 Subject: [PATCH 14/22] Modify wording in docstring Co-authored-by: Robert Luke <748691+rob-luke@users.noreply.github.com> --- mne/channels/channels.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 957f336951e..f2bf2e6e974 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1543,7 +1543,7 @@ def make_1020_channel_selections(info, midline="z"): def combine_channels(instance, groups, method='mean', keep_stim=False, drop_bad=False): - """Combine channels based on regions of interest (ROIs). + """Combine channels based on specified channel grouping. Parameters ---------- @@ -1556,7 +1556,7 @@ def combine_channels(instance, groups, method='mean', keep_stim=False, pseudo-channel is made per dict entry; the dict values must be lists of picks (integer indices of ``ch_names``). For example:: - groups=dict(Left_ROI=[1, 2, 3, 4], Right_ROI=[5, 6, 7, 8]) + groups=dict(Left=[1, 2, 3, 4], Right=[5, 6, 7, 8]) Note that within a dict entry all channels must have the same type. method : str | callable @@ -1579,8 +1579,8 @@ def combine_channels(instance, groups, method='mean', keep_stim=False, Returns ------- combined_inst : instance of Raw, Epochs, or Evoked - An MNE-Python object with channels combined for each ROI defined in the - ``groups`` parameter. + An MNE-Python object with channels combined for each group defined in + the ``groups`` parameter. """ from ..io import BaseRaw, RawArray from ..epochs import BaseEpochs From 922d54d6d6e8f760b5ee9c1972b7b06977fca368 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Mon, 20 Jul 2020 09:01:29 +0100 Subject: [PATCH 15/22] Add combine_channels to python_reference.rst --- doc/python_reference.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/python_reference.rst b/doc/python_reference.rst index c74edbdd512..8e273e73243 100644 --- a/doc/python_reference.rst +++ b/doc/python_reference.rst @@ -338,6 +338,7 @@ Projections: rename_channels generate_2d_layout make_1020_channel_selections + combine_channels :py:mod:`mne.preprocessing`: From 0fbe53d0cde4d04f76001252b5e79625545fee9e Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Mon, 20 Jul 2020 18:55:18 +0100 Subject: [PATCH 16/22] Apply suggestions from code review Co-authored-by: Alexandre Gramfort Co-authored-by: Daniel McCloy --- mne/channels/channels.py | 102 ++++++++++++++-------------- mne/channels/tests/test_channels.py | 51 ++++++++------ 2 files changed, 80 insertions(+), 73 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index f2bf2e6e974..67721dc8c57 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1541,13 +1541,13 @@ def make_1020_channel_selections(info, midline="z"): return selections -def combine_channels(instance, groups, method='mean', keep_stim=False, +def combine_channels(inst, groups, method='mean', keep_stim=False, drop_bad=False): """Combine channels based on specified channel grouping. Parameters ---------- - instance : instance of Raw, Epochs, or Evoked + inst : instance of Raw, Epochs, or Evoked An MNE-Python object to combine the channels for. The object can be of type Raw, Epochs, or Evoked. groups : dict @@ -1563,36 +1563,37 @@ def combine_channels(instance, groups, method='mean', keep_stim=False, Which method to use to combine channels. If a :class:`str`, must be one of 'mean', 'median', or 'std' (standard deviation). If callable, the callable must accept one positional input (data of shape ``(n_channels, - n_times)``, ``(n_epochs, n_channels, n_times)``, or ``(n_evokeds, - n_channels, n_time)``) and return an :class:`array ` of - shape ``(n_times)``, ``(n_epochs, n_times)``, or ``(n_evokeds, - n_times)``. For example with an instance of Raw:: + n_times)``, or ``(n_epochs, n_channels, n_times)``) and return an + :class:`array ` of shape ``(n_times,)``, or ``(n_epochs, + n_times)``. For example with an instance of Raw or Evoked:: - method = lambda data: np.median(data, axis=0) + method = lambda data: np.mean(data, axis=0) - Defaults to ``mean``. + Another example with an instance of Epochs:: + + method = lambda data: np.median(data, axis=1) + + Defaults to ``'mean'``. keep_stim : bool - If True (default False), keep stimulus channels. + If ``True``, include stimulus channels in the resulting object. + Defaults to ``False``. drop_bad : bool - If True (default False), drop channels marked as bad. + If ``True``, drop channels marked as bad before combining. Defaults to + ``False``. Returns ------- combined_inst : instance of Raw, Epochs, or Evoked - An MNE-Python object with channels combined for each group defined in - the ``groups`` parameter. + An MNE-Python object of the same type as the input ``inst``, containing + one virtual channel for each group in ``groups`` (and, if ``keep_stim`` + is ``True``, also containing stimulus channels). """ from ..io import BaseRaw, RawArray - from ..epochs import BaseEpochs - from ..evoked import Evoked - from .. import EpochsArray, EvokedArray + from .. import BaseEpochs, EpochsArray, Evoked, EvokedArray - # Check to see if data is preloaded - _check_preload(instance, 'Combining channels') - - ch_axis = len(instance._data.shape) - 2 # idx of the channel axis - ch_idx = list(range(instance.info['nchan'])) - ch_types = _get_channel_types(instance.info) + ch_axis = 1 if isinstance(inst, BaseEpochs) else 0 + ch_idx = list(range(inst.info['nchan'])) + ch_types = _get_channel_types(inst.info) groups = OrderedDict(deepcopy(groups)) # Convert string values of ``method`` into callables @@ -1610,13 +1611,13 @@ def combine_channels(instance, groups, method='mean', keep_stim=False, if not isinstance(keep_stim, bool): raise TypeError('"keep_stim" must be of type bool, not ' f'{type(keep_stim)}.') - if keep_stim is True: + if keep_stim: try: - combined_inst = instance.copy().pick_types(meg=False, stim=True) + combined_inst = inst.copy().pick_types(meg=False, stim=True) # Create clean info to avoid inconsistencies stim_ch_names = combined_inst.info['ch_names'] stim_ch_types = _get_channel_types(combined_inst.info) - combined_inst.info = create_info(sfreq=instance.info['sfreq'], + combined_inst.info = create_info(sfreq=inst.info['sfreq'], ch_names=stim_ch_names, ch_types=stim_ch_types) except ValueError: @@ -1628,58 +1629,57 @@ def combine_channels(instance, groups, method='mean', keep_stim=False, if not isinstance(drop_bad, bool): raise TypeError('"drop_bad" must be of type bool, not ' f'{type(drop_bad)}.') - if drop_bad is False: - ch_idx_bad = [] - elif not instance.info['bads']: - ch_idx_bad = [] + if drop_bad and inst.info['bads']: + ch_idx_bad = pick_channels(inst.info['ch_names'], + inst.info['bads']) else: - ch_idx_bad = pick_channels(instance.info['ch_names'], - instance.info['bads']) + ch_idx_bad = [] # Check correctness of combinations - for this_group, these_picks in groups.items(): + for this_group, this_picks in groups.items(): # Check if channel indices are out of bounds - if not all(idx in ch_idx for idx in these_picks): + if not all(idx in ch_idx for idx in this_picks): raise ValueError('Some channel indices are out of bounds.') # Check if heterogeneous sensor type combinations - this_ch_type = np.array(ch_types)[these_picks] + this_ch_type = np.array(ch_types)[this_picks] if len(set(this_ch_type)) > 1: types = ', '.join(set(this_ch_type)) raise ValueError('Cannot combine sensors of different types; ' f'"{this_group}" contains types {types}.') # Remove bad channels - these_bads = [idx for idx in these_picks if idx in ch_idx_bad] - these_picks = [idx for idx in these_picks if idx not in ch_idx_bad] + these_bads = [idx for idx in this_picks if idx in ch_idx_bad] + this_picks = [idx for idx in this_picks if idx not in ch_idx_bad] if these_bads: logger.info('Dropped the following channels in group ' f'{this_group}: {these_bads}') # Check if combining less than 2 channel - if len(set(these_picks)) < 2: - warn(f'Less than 2 channels in group "{this_group}"; cannot ' - f'combine by method "{method}".') + if len(set(this_picks)) < 2: + warn(f'Less than 2 channels in group "{this_group}" when ' + f'combining by method "{method}".') # If all good create more detailed dict without bad channels - groups[this_group] = dict(picks=these_picks, ch_type=this_ch_type[0]) + groups[this_group] = dict(picks=this_picks, ch_type=this_ch_type[0]) # Combine channels and add them to the new instance + inst_data = inst.data if isinstance(inst, Evoked) else inst.get_data() group_ch_names, group_ch_types, group_data = [], [], [] for this_group, this_group_dict in groups.items(): group_ch_names.append(this_group) group_ch_types.append(this_group_dict['ch_type']) - these_picks = this_group_dict['picks'] - this_data = np.take(instance._data, these_picks, axis=ch_axis) + this_picks = this_group_dict['picks'] + this_data = np.take(inst_data, this_picks, axis=ch_axis) group_data.append(method(this_data)) group_data = np.swapaxes(group_data, 0, ch_axis) - info = create_info(sfreq=instance.info['sfreq'], ch_names=group_ch_names, + info = create_info(sfreq=inst.info['sfreq'], ch_names=group_ch_names, ch_types=group_ch_types) - if isinstance(instance, BaseRaw): - new_inst = RawArray(group_data, info, first_samp=instance.first_samp, - verbose=instance.verbose) - elif isinstance(instance, BaseEpochs): - new_inst = EpochsArray(group_data, info, tmin=instance.times[0], - verbose=instance.verbose) - elif isinstance(instance, Evoked): - new_inst = EvokedArray(group_data, info, tmin=instance.times[0], - verbose=instance.verbose) + if isinstance(inst, BaseRaw): + new_inst = RawArray(group_data, info, first_samp=inst.first_samp, + verbose=inst.verbose) + elif isinstance(inst, BaseEpochs): + new_inst = EpochsArray(group_data, info, tmin=inst.times[0], + verbose=inst.verbose) + elif isinstance(inst, Evoked): + new_inst = EvokedArray(group_data, info, tmin=inst.times[0], + verbose=inst.verbose) if combined_inst is None: combined_inst = new_inst else: diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 248a622ebc3..516d0db199b 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -380,39 +380,46 @@ def test_combine_channels(): # Test result with one ROI good_single = dict(foo=[0, 1, 3, 4]) # good grad - combined_mean = combine_channels(raw, good_single, method='mean')._data - combined_median = combine_channels(raw, good_single, method='median')._data - combined_std = combine_channels(raw, good_single, method='std')._data - foo_mean = np.mean(raw._data[good_single['foo']], axis=0) - foo_median = np.median(raw._data[good_single['foo']], axis=0) - foo_std = np.std(raw._data[good_single['foo']], axis=0) - assert np.array_equal(combined_mean, np.expand_dims(foo_mean, axis=0)) - assert np.array_equal(combined_median, np.expand_dims(foo_median, axis=0)) - assert np.array_equal(combined_std, np.expand_dims(foo_std, axis=0)) + combined_mean = combine_channels(raw, good_single, method='mean') + combined_median = combine_channels(raw, good_single, method='median') + combined_std = combine_channels(raw, good_single, method='std') + foo_mean = np.mean(raw.get_data()[good_single['foo']], axis=0) + foo_median = np.median(raw.get_data()[good_single['foo']], axis=0) + foo_std = np.std(raw.get_data()[good_single['foo']], axis=0) + assert np.array_equal(combined_mean.get_data(), + np.expand_dims(foo_mean, axis=0)) + assert np.array_equal(combined_median.get_data(), + np.expand_dims(foo_median, axis=0)) + assert np.array_equal(combined_std.get_data(), + np.expand_dims(foo_std, axis=0)) # Test bad cases - raw_no_preload = read_raw_fif(raw_fname, preload=False) raw_no_stim = read_raw_fif(raw_fname, preload=True) raw_no_stim.pick_types(meg=True, stim=False) bad1 = dict(foo=[0, 376], bar=[5, 2]) # out of bounds bad2 = dict(foo=[0, 2], bar=[5, 2]) # type mix in same group - pytest.raises(RuntimeError, combine_channels, raw_no_preload, good) - pytest.raises(ValueError, combine_channels, raw, good, method='bad_method') - pytest.raises(TypeError, combine_channels, raw, good, keep_stim='bad_type') - pytest.raises(TypeError, combine_channels, raw, good, drop_bad='bad_type') - pytest.raises(ValueError, combine_channels, raw_no_stim, good, - keep_stim=True) - pytest.raises(ValueError, combine_channels, raw, bad1) - pytest.raises(ValueError, combine_channels, raw, bad2) + with pytest.raises(ValueError, match='"method" must be a callable, or'): + combine_channels(raw, good, method='bad_method') + with pytest.raises(TypeError, match='"keep_stim" must be of type bool'): + combine_channels(raw, good, keep_stim='bad_type') + with pytest.raises(TypeError, match='"drop_bad" must be of type bool'): + combine_channels(raw, good, drop_bad='bad_type') + with pytest.raises(ValueError, match='Could not find stimulus'): + combine_channels(raw_no_stim, good, keep_stim=True) + with pytest.raises(ValueError, match='Some channel indices are out of'): + combine_channels(raw, bad1) + with pytest.raises(ValueError, match='Cannot combine sensors of diff'): + combine_channels(raw, bad2) # Test warnings warn1 = dict(foo=[375, 375], bar=[5, 2]) # same channel in same group warn2 = dict(foo=[375], bar=[5, 2]) # one channel (last channel) warn3 = dict(foo=[0, 4], bar=[5, 2]) # one good channel left - pytest.warns(RuntimeWarning, combine_channels, raw, warn1) - pytest.warns(RuntimeWarning, combine_channels, raw, warn2) - pytest.warns(RuntimeWarning, combine_channels, raw_ch_bad, warn3, - drop_bad=True) + with pytest.warns(RuntimeWarning, match='Less than 2 channels') as record: + combine_channels(raw, warn1) + combine_channels(raw, warn2) + combine_channels(raw_ch_bad, warn3, drop_bad=True) + assert len(record) == 3 run_tests_if_main() From 6a77c7d1b8a30e1229f22a68f2317017965dffdf Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Mon, 20 Jul 2020 21:50:26 +0100 Subject: [PATCH 17/22] No copy for stim --- mne/channels/channels.py | 63 +++++++++++++---------------- mne/channels/tests/test_channels.py | 20 +++++---- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 67721dc8c57..cdc41fdf635 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1593,7 +1593,9 @@ def combine_channels(inst, groups, method='mean', keep_stim=False, ch_axis = 1 if isinstance(inst, BaseEpochs) else 0 ch_idx = list(range(inst.info['nchan'])) - ch_types = _get_channel_types(inst.info) + ch_names = inst.info['ch_names'] + ch_types = inst.get_channel_types() + inst_data = inst.data if isinstance(inst, Evoked) else inst.get_data() groups = OrderedDict(deepcopy(groups)) # Convert string values of ``method`` into callables @@ -1607,33 +1609,28 @@ def combine_channels(inst, groups, method='mean', keep_stim=False, raise ValueError('"method" must be a callable, or one of ' f'"mean", "median", or "std"; got "{method}".') - # Create new instance + # Instantiate data and channel info + new_ch_names, new_ch_types, new_data = [], [], [] if not isinstance(keep_stim, bool): raise TypeError('"keep_stim" must be of type bool, not ' f'{type(keep_stim)}.') if keep_stim: - try: - combined_inst = inst.copy().pick_types(meg=False, stim=True) - # Create clean info to avoid inconsistencies - stim_ch_names = combined_inst.info['ch_names'] - stim_ch_types = _get_channel_types(combined_inst.info) - combined_inst.info = create_info(sfreq=inst.info['sfreq'], - ch_names=stim_ch_names, - ch_types=stim_ch_types) - except ValueError: - raise ValueError('Could not find stimulus channels.') - else: - combined_inst = None + stim_ch_idx = list(pick_types(inst.info, meg=False, stim=True)) + if stim_ch_idx: + new_ch_names = [ch_names[idx] for idx in stim_ch_idx] + new_ch_types = [ch_types[idx] for idx in stim_ch_idx] + new_data = [np.take(inst_data, idx, axis=ch_axis) + for idx in stim_ch_idx] + else: + warn('Could not find stimulus channels.') # Get indices of bad channels + ch_idx_bad = [] if not isinstance(drop_bad, bool): raise TypeError('"drop_bad" must be of type bool, not ' f'{type(drop_bad)}.') if drop_bad and inst.info['bads']: - ch_idx_bad = pick_channels(inst.info['ch_names'], - inst.info['bads']) - else: - ch_idx_bad = [] + ch_idx_bad = pick_channels(ch_names, inst.info['bads']) # Check correctness of combinations for this_group, this_picks in groups.items(): @@ -1660,29 +1657,23 @@ def combine_channels(inst, groups, method='mean', keep_stim=False, groups[this_group] = dict(picks=this_picks, ch_type=this_ch_type[0]) # Combine channels and add them to the new instance - inst_data = inst.data if isinstance(inst, Evoked) else inst.get_data() - group_ch_names, group_ch_types, group_data = [], [], [] for this_group, this_group_dict in groups.items(): - group_ch_names.append(this_group) - group_ch_types.append(this_group_dict['ch_type']) + new_ch_names.append(this_group) + new_ch_types.append(this_group_dict['ch_type']) this_picks = this_group_dict['picks'] this_data = np.take(inst_data, this_picks, axis=ch_axis) - group_data.append(method(this_data)) - group_data = np.swapaxes(group_data, 0, ch_axis) - info = create_info(sfreq=inst.info['sfreq'], ch_names=group_ch_names, - ch_types=group_ch_types) + new_data.append(method(this_data)) + new_data = np.swapaxes(new_data, 0, ch_axis) + info = create_info(sfreq=inst.info['sfreq'], ch_names=new_ch_names, + ch_types=new_ch_types) if isinstance(inst, BaseRaw): - new_inst = RawArray(group_data, info, first_samp=inst.first_samp, - verbose=inst.verbose) + combined_inst = RawArray(new_data, info, first_samp=inst.first_samp, + verbose=inst.verbose) elif isinstance(inst, BaseEpochs): - new_inst = EpochsArray(group_data, info, tmin=inst.times[0], - verbose=inst.verbose) + combined_inst = EpochsArray(new_data, info, tmin=inst.times[0], + verbose=inst.verbose) elif isinstance(inst, Evoked): - new_inst = EvokedArray(group_data, info, tmin=inst.times[0], - verbose=inst.verbose) - if combined_inst is None: - combined_inst = new_inst - else: - combined_inst.add_channels([new_inst]) + combined_inst = EvokedArray(new_data, info, tmin=inst.times[0], + verbose=inst.verbose) return combined_inst diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 516d0db199b..12a9c1deaed 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -363,10 +363,10 @@ def test_equalize_channels(): def test_combine_channels(): """Test channel combination on Raw, Epochs, and Evoked.""" - raw = read_raw_fif(raw_fname, preload=True) - raw_ch_bad = read_raw_fif(raw_fname, preload=True) + raw = read_raw_fif(raw_fname) + raw_ch_bad = read_raw_fif(raw_fname) raw_ch_bad.info['bads'] = ['MEG 0113', 'MEG 0112'] - epochs = Epochs(raw, read_events(eve_fname), preload=True) + epochs = Epochs(raw, read_events(eve_fname)) evoked = epochs.average() good = dict(foo=[0, 1, 3, 4], bar=[5, 2]) # good grad and mag @@ -374,10 +374,14 @@ def test_combine_channels(): combine_channels(raw, good) combine_channels(epochs, good) combine_channels(evoked, good) - combine_channels(raw, good, keep_stim=True) combine_channels(raw, good, drop_bad=True) combine_channels(raw_ch_bad, good, drop_bad=True) + # Test with stimulus channels + combine_stim = combine_channels(raw, good, keep_stim=True) + target_nchan = len(good) + len(pick_types(raw.info, meg=False, stim=True)) + assert combine_stim.info['nchan'] == target_nchan + # Test result with one ROI good_single = dict(foo=[0, 1, 3, 4]) # good grad combined_mean = combine_channels(raw, good_single, method='mean') @@ -394,8 +398,6 @@ def test_combine_channels(): np.expand_dims(foo_std, axis=0)) # Test bad cases - raw_no_stim = read_raw_fif(raw_fname, preload=True) - raw_no_stim.pick_types(meg=True, stim=False) bad1 = dict(foo=[0, 376], bar=[5, 2]) # out of bounds bad2 = dict(foo=[0, 2], bar=[5, 2]) # type mix in same group with pytest.raises(ValueError, match='"method" must be a callable, or'): @@ -404,17 +406,19 @@ def test_combine_channels(): combine_channels(raw, good, keep_stim='bad_type') with pytest.raises(TypeError, match='"drop_bad" must be of type bool'): combine_channels(raw, good, drop_bad='bad_type') - with pytest.raises(ValueError, match='Could not find stimulus'): - combine_channels(raw_no_stim, good, keep_stim=True) with pytest.raises(ValueError, match='Some channel indices are out of'): combine_channels(raw, bad1) with pytest.raises(ValueError, match='Cannot combine sensors of diff'): combine_channels(raw, bad2) # Test warnings + raw_no_stim = read_raw_fif(raw_fname) + raw_no_stim.pick_types(meg=True, stim=False) warn1 = dict(foo=[375, 375], bar=[5, 2]) # same channel in same group warn2 = dict(foo=[375], bar=[5, 2]) # one channel (last channel) warn3 = dict(foo=[0, 4], bar=[5, 2]) # one good channel left + with pytest.warns(RuntimeWarning, match='Could not find stimulus'): + combine_channels(raw_no_stim, good, keep_stim=True) with pytest.warns(RuntimeWarning, match='Less than 2 channels') as record: combine_channels(raw, warn1) combine_channels(raw, warn2) From e5e303f764390c3268caa15da07b7d5478a20a29 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Tue, 21 Jul 2020 22:28:39 +0100 Subject: [PATCH 18/22] Speed up tests with preload on Raw --- mne/channels/channels.py | 6 +++--- mne/channels/tests/test_channels.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index cdc41fdf635..7c69db6cddb 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1606,10 +1606,10 @@ def combine_channels(inst, groups, method='mean', keep_stim=False, try: method = method_dict[method] except KeyError: - raise ValueError('"method" must be a callable, or one of ' - f'"mean", "median", or "std"; got "{method}".') + raise ValueError('"method" must be a callable, or one of "mean", ' + f'"median", or "std"; got "{method}".') - # Instantiate data and channel info + # Instantiate channel info and data new_ch_names, new_ch_types, new_data = [], [], [] if not isinstance(keep_stim, bool): raise TypeError('"keep_stim" must be of type bool, not ' diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 12a9c1deaed..e4f9b917777 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -363,8 +363,8 @@ def test_equalize_channels(): def test_combine_channels(): """Test channel combination on Raw, Epochs, and Evoked.""" - raw = read_raw_fif(raw_fname) - raw_ch_bad = read_raw_fif(raw_fname) + raw = read_raw_fif(raw_fname, preload=True) + raw_ch_bad = read_raw_fif(raw_fname, preload=True) raw_ch_bad.info['bads'] = ['MEG 0113', 'MEG 0112'] epochs = Epochs(raw, read_events(eve_fname)) evoked = epochs.average() @@ -382,7 +382,7 @@ def test_combine_channels(): target_nchan = len(good) + len(pick_types(raw.info, meg=False, stim=True)) assert combine_stim.info['nchan'] == target_nchan - # Test result with one ROI + # Test results with one ROI good_single = dict(foo=[0, 1, 3, 4]) # good grad combined_mean = combine_channels(raw, good_single, method='mean') combined_median = combine_channels(raw, good_single, method='median') @@ -412,7 +412,7 @@ def test_combine_channels(): combine_channels(raw, bad2) # Test warnings - raw_no_stim = read_raw_fif(raw_fname) + raw_no_stim = read_raw_fif(raw_fname, preload=True) raw_no_stim.pick_types(meg=True, stim=False) warn1 = dict(foo=[375, 375], bar=[5, 2]) # same channel in same group warn2 = dict(foo=[375], bar=[5, 2]) # one channel (last channel) From 80b4bba555324e00e7ffb5d1e59e55e26107b12a Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 23 Jul 2020 15:48:27 +0100 Subject: [PATCH 19/22] Add draft tutorial for combine_channels --- tutorials/evoked/plot_eeg_erp.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tutorials/evoked/plot_eeg_erp.py b/tutorials/evoked/plot_eeg_erp.py index 8ffbdbac88a..fc702fbfe51 100644 --- a/tutorials/evoked/plot_eeg_erp.py +++ b/tutorials/evoked/plot_eeg_erp.py @@ -12,6 +12,7 @@ import mne from mne.datasets import sample +from mne.channels import combine_channels ############################################################################### # Setup for reading the raw data @@ -107,6 +108,28 @@ evoked_custom.plot(titles=dict(eeg=title), time_unit='s') evoked_custom.plot_topomap(times=[0.1], size=3., title=title, time_unit='s') +############################################################################### +# Evoked response averaged accross channels by ROI +# ------------------------------------------------ +# +# It is possible to average channels by region of interest (for example left +# and right) when studying the response to this left auditory stimulus. +raw_car, _ = mne.set_eeg_reference(raw, 'average', projection=True) +evoked_car = mne.Epochs(raw_car, **epochs_params).average() +evoked_car.pick_types(meg=False, eeg=True, eog=False) +del raw_car # save memory + +roi_dict = {'Left': [], 'Right': []} +for idx, ch in enumerate(raw.info['chs']): + if ch['loc'][0] < 0: + roi_dict['Left'].append(idx) + elif ch['loc'][0] > 0: + roi_dict['Right'].append(idx) +evoked_combined = combine_channels(evoked_car, roi_dict, method='mean') + +title = 'Evoked response averaged by side' +evoked_combined.plot(titles=dict(eeg=title), time_unit='s') + ############################################################################### # Evoked arithmetic (e.g. differences) # ------------------------------------ From aa864e3712a06f75c33f9b41563a755bc5eed218 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 23 Jul 2020 16:14:40 +0100 Subject: [PATCH 20/22] Fix typo --- tutorials/evoked/plot_eeg_erp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/evoked/plot_eeg_erp.py b/tutorials/evoked/plot_eeg_erp.py index fc702fbfe51..c7c8c702ee5 100644 --- a/tutorials/evoked/plot_eeg_erp.py +++ b/tutorials/evoked/plot_eeg_erp.py @@ -109,8 +109,8 @@ evoked_custom.plot_topomap(times=[0.1], size=3., title=title, time_unit='s') ############################################################################### -# Evoked response averaged accross channels by ROI -# ------------------------------------------------ +# Evoked response averaged across channels by ROI +# ----------------------------------------------- # # It is possible to average channels by region of interest (for example left # and right) when studying the response to this left auditory stimulus. From 4a0a40d0ea0c6a31d833a8058c2250734f0a7a41 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 23 Jul 2020 19:53:19 +0100 Subject: [PATCH 21/22] Fix warning in doc --- tutorials/evoked/plot_eeg_erp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tutorials/evoked/plot_eeg_erp.py b/tutorials/evoked/plot_eeg_erp.py index c7c8c702ee5..ff0bad19305 100644 --- a/tutorials/evoked/plot_eeg_erp.py +++ b/tutorials/evoked/plot_eeg_erp.py @@ -113,11 +113,11 @@ # ----------------------------------------------- # # It is possible to average channels by region of interest (for example left -# and right) when studying the response to this left auditory stimulus. -raw_car, _ = mne.set_eeg_reference(raw, 'average', projection=True) -evoked_car = mne.Epochs(raw_car, **epochs_params).average() -evoked_car.pick_types(meg=False, eeg=True, eog=False) -del raw_car # save memory +# and right) when studying the response to this left auditory stimulus. Here we +# use our Raw object on which the average reference projection has been added +# back. +evoked = mne.Epochs(raw, **epochs_params).average() +evoked.pick_types(meg=False, eeg=True, eog=False) roi_dict = {'Left': [], 'Right': []} for idx, ch in enumerate(raw.info['chs']): From d0017164dc622e8051cf6cad22818e4c38a4d774 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 24 Jul 2020 20:56:55 +0100 Subject: [PATCH 22/22] Modify ROIs for tutorial --- tutorials/evoked/plot_eeg_erp.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tutorials/evoked/plot_eeg_erp.py b/tutorials/evoked/plot_eeg_erp.py index ff0bad19305..d18f4d31171 100644 --- a/tutorials/evoked/plot_eeg_erp.py +++ b/tutorials/evoked/plot_eeg_erp.py @@ -117,15 +117,13 @@ # use our Raw object on which the average reference projection has been added # back. evoked = mne.Epochs(raw, **epochs_params).average() -evoked.pick_types(meg=False, eeg=True, eog=False) - -roi_dict = {'Left': [], 'Right': []} -for idx, ch in enumerate(raw.info['chs']): - if ch['loc'][0] < 0: - roi_dict['Left'].append(idx) - elif ch['loc'][0] > 0: - roi_dict['Right'].append(idx) -evoked_combined = combine_channels(evoked_car, roi_dict, method='mean') + +left_idx = mne.pick_channels(evoked.info['ch_names'], + ['EEG 017', 'EEG 018', 'EEG 025', 'EEG 026']) +right_idx = mne.pick_channels(evoked.info['ch_names'], + ['EEG 023', 'EEG 024', 'EEG 034', 'EEG 035']) +roi_dict = dict(Left=left_idx, Right=right_idx) +evoked_combined = combine_channels(evoked, roi_dict, method='mean') title = 'Evoked response averaged by side' evoked_combined.plot(titles=dict(eeg=title), time_unit='s')