Skip to content

Commit

Permalink
MRG: selection.read_selection() ➡️ channels.read_vectorview_selection…
Browse files Browse the repository at this point in the history
…() (mne-tools#8870)

* Move read_selection() to channels.read_neuromag_selection()

Fixes mne-tools#8865

* Fix Sphinx warning

* read_neuromag_selection -> read_vectorview_selection

* Use pytest context manager

* Add API reference

* Fix changelog

* Update docstring

* Update mne/channels/channels.py

Co-authored-by: Eric Larson <[email protected]>

* Docstring, import

* fix changelog [skip azp][skip github]

Co-authored-by: Eric Larson <[email protected]>
  • Loading branch information
hoechenberger and larsoner authored Feb 18, 2021
1 parent 24377ad commit 53f1b85
Show file tree
Hide file tree
Showing 16 changed files with 213 additions and 224 deletions.
2 changes: 1 addition & 1 deletion doc/changes/0.12.inc
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Changelog
- Add epoch rejection based on annotated segments by `Jaakko Leppakangas`_
- Add option to use new-style MEG channel names in :func:`mne.read_selection` by `Eric Larson`_
- Add option to use new-style MEG channel names in ``mne.read_selection`` by `Eric Larson`_
- Add option for ``proj`` in :class:`mne.EpochsArray` by `Eric Larson`_
Expand Down
5 changes: 3 additions & 2 deletions doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Enhancements

- Add :func:`mne.chpi.extract_chpi_locs_kit` to read cHPI coil locations from KIT/Yokogawa data (:gh:`` **by new contributor** |Matt Sanderson|_, `Robert Seymour`_, and `Eric Larson`_)

- Add ``match_alias`` parameter to :meth:`mne.io.Raw.set_montage` and related functions to match unrecognized channel location names to known aliases (:gh`8799` **by new contributor** |Zhi Zhang|_)

- Update the ``notebook`` 3d backend to use ``ipyvtk_simple`` for a better integration within ``Jupyter`` (:gh:`8503` by `Guillaume Favelier`_)

- Add toggle-all button to :class:`mne.Report` HTML and ``width`` argument to :meth:`mne.Report.add_bem_to_section` (:gh:`8723` by `Eric Larson`_)
Expand Down Expand Up @@ -144,5 +146,4 @@ Bugs

API changes
~~~~~~~~~~~

- Add ``match_alias`` parameter to :meth:`mne.io.Raw.set_montage` and related functions to match unrecognized channel location names to known aliases (:gh`8799` **by new contributor** |Zhi Zhang|_)
- ``mne.read_selection`` has been deprecated in favor of `mne.read_vectorview_selection`. ``mne.read_selection`` will be removed in MNE-Python 0.24 (:gh:`8870` by `Richard Höchenberger`_)
2 changes: 1 addition & 1 deletion doc/sensor_space.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Sensor Space Data
pick_info
read_epochs
read_reject_parameters
read_selection
read_vectorview_selection
rename_channels

:py:mod:`mne.baseline`:
Expand Down
2 changes: 1 addition & 1 deletion examples/inverse/plot_tf_dics.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
# Pick a selection of magnetometer channels. A subset of all channels was used
# to speed up the example. For a solution based on all MEG channels use
# meg=True, selection=None and add mag=4e-12 to the reject dictionary.
left_temporal_channels = mne.read_selection('Left-temporal')
left_temporal_channels = mne.read_vectorview_selection('Left-temporal')
picks = mne.pick_types(raw.info, meg='mag', eeg=False, eog=False,
stim=False, exclude='bads',
selection=left_temporal_channels)
Expand Down
8 changes: 6 additions & 2 deletions mne/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@
transform_surface_to, Transform)
from .proj import (read_proj, write_proj, compute_proj_epochs,
compute_proj_evoked, compute_proj_raw, sensitivity_map)
from .selection import read_selection
from .dipole import read_dipole, Dipole, DipoleFixed, fit_dipole
from .channels import equalize_channels, rename_channels, find_layout
from .channels import (equalize_channels, rename_channels, find_layout,
read_vectorview_selection)
from .report import Report, open_report

from .io import read_epochs_fieldtrip, read_evoked_fieldtrip, read_evokeds_mff
Expand Down Expand Up @@ -122,6 +122,10 @@
from . import viz
from . import decoding

# deprecations
from .utils import deprecated_alias
deprecated_alias('read_selection', read_vectorview_selection)

# initialize logging
set_log_level(None, False)
set_log_file()
4 changes: 2 additions & 2 deletions mne/beamformer/tests/test_lcmv.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from mne.transforms import apply_trans, invert_transform
from mne import (convert_forward_solution, read_forward_solution, compute_rank,
VolVectorSourceEstimate, VolSourceEstimate, EvokedArray,
pick_channels_cov)
pick_channels_cov, read_vectorview_selection)
from mne.beamformer import (make_lcmv, apply_lcmv, apply_lcmv_epochs,
apply_lcmv_raw, Beamformer,
read_beamformer, apply_lcmv_cov, make_dics)
Expand Down Expand Up @@ -70,7 +70,7 @@ def _get_data(tmin=-0.1, tmax=0.15, all_forward=True, epochs=True,
# Setup for reading the raw data
raw.info['bads'] = ['MEG 2443', 'EEG 053'] # 2 bad channels
# Set up pick list: MEG - bad channels
left_temporal_channels = mne.read_selection('Left-temporal')
left_temporal_channels = read_vectorview_selection('Left-temporal')
picks = mne.pick_types(raw.info, meg=True,
selection=left_temporal_channels)
picks = picks[::2] # decimate for speed
Expand Down
5 changes: 4 additions & 1 deletion mne/channels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
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, combine_channels)
make_1020_channel_selections, combine_channels,
read_vectorview_selection, _SELECTIONS, _EEG_SELECTIONS,
_divide_to_regions)

__all__ = [
# Data Structures
Expand All @@ -35,6 +37,7 @@
'rename_channels', 'make_1020_channel_selections',
'_get_ch_type', 'equalize_channels', 'find_ch_adjacency', 'find_layout',
'fix_mag_coil_types', 'generate_2d_layout', 'get_builtin_montages',
'combine_channels', 'read_vectorview_selection',

# Other
'compute_dev_head_t', 'compute_native_head_t',
Expand Down
168 changes: 166 additions & 2 deletions mne/channels/channels.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Authors: Alexandre Gramfort <[email protected]>
# Matti Hämäläinen <[email protected]>
# Martin Luessi <[email protected]>
# Denis Engemann <[email protected]>
# Andrew Dykstra <[email protected]>
# Teon Brooks <[email protected]>
# Daniel McCloy <[email protected]>
#
# License: BSD (3-clause)


import os
import os.path as op
import sys
Expand All @@ -19,14 +21,16 @@
from ..defaults import HEAD_SIZE_DEFAULT, _handle_default
from ..transforms import _frame_to_str
from ..utils import (verbose, logger, warn,
_check_preload, _validate_type, fill_doc, _check_option)
_check_preload, _validate_type, fill_doc, _check_option,
_get_stim_channel, _check_fname)
from ..io.compensator import get_current_comp
from ..io.constants import FIFF
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,
_get_channel_types, get_channel_type_constants)
_get_channel_types, get_channel_type_constants,
_pick_data_channels)
from ..io.write import DATE_NONE
from ..io._digitization import _get_data_as_dict_from_dig

Expand Down Expand Up @@ -1762,3 +1766,163 @@ def combine_channels(inst, groups, method='mean', keep_stim=False,
verbose=inst.verbose)

return combined_inst


# NeuroMag channel groupings
_SELECTIONS = ['Vertex', 'Left-temporal', 'Right-temporal', 'Left-parietal',
'Right-parietal', 'Left-occipital', 'Right-occipital',
'Left-frontal', 'Right-frontal']
_EEG_SELECTIONS = ['EEG 1-32', 'EEG 33-64', 'EEG 65-96', 'EEG 97-128']


def _divide_to_regions(info, add_stim=True):
"""Divide channels to regions by positions."""
from scipy.stats import zscore
picks = _pick_data_channels(info, exclude=[])
chs_in_lobe = len(picks) // 4
pos = np.array([ch['loc'][:3] for ch in info['chs']])
x, y, z = pos.T

frontal = picks[np.argsort(y[picks])[-chs_in_lobe:]]
picks = np.setdiff1d(picks, frontal)

occipital = picks[np.argsort(y[picks])[:chs_in_lobe]]
picks = np.setdiff1d(picks, occipital)

temporal = picks[np.argsort(z[picks])[:chs_in_lobe]]
picks = np.setdiff1d(picks, temporal)

lt, rt = _divide_side(temporal, x)
lf, rf = _divide_side(frontal, x)
lo, ro = _divide_side(occipital, x)
lp, rp = _divide_side(picks, x) # Parietal lobe from the remaining picks.

# Because of the way the sides are divided, there may be outliers in the
# temporal lobes. Here we switch the sides for these outliers. For other
# lobes it is not a big problem because of the vicinity of the lobes.
with np.errstate(invalid='ignore'): # invalid division, greater compare
zs = np.abs(zscore(x[rt]))
outliers = np.array(rt)[np.where(zs > 2.)[0]]
rt = list(np.setdiff1d(rt, outliers))

with np.errstate(invalid='ignore'): # invalid division, greater compare
zs = np.abs(zscore(x[lt]))
outliers = np.append(outliers, (np.array(lt)[np.where(zs > 2.)[0]]))
lt = list(np.setdiff1d(lt, outliers))

l_mean = np.mean(x[lt])
r_mean = np.mean(x[rt])
for outlier in outliers:
if abs(l_mean - x[outlier]) < abs(r_mean - x[outlier]):
lt.append(outlier)
else:
rt.append(outlier)

if add_stim:
stim_ch = _get_stim_channel(None, info, raise_error=False)
if len(stim_ch) > 0:
for region in [lf, rf, lo, ro, lp, rp, lt, rt]:
region.append(info['ch_names'].index(stim_ch[0]))
return OrderedDict([('Left-frontal', lf), ('Right-frontal', rf),
('Left-parietal', lp), ('Right-parietal', rp),
('Left-occipital', lo), ('Right-occipital', ro),
('Left-temporal', lt), ('Right-temporal', rt)])


def _divide_side(lobe, x):
"""Make a separation between left and right lobe evenly."""
lobe = np.asarray(lobe)
median = np.median(x[lobe])

left = lobe[np.where(x[lobe] < median)[0]]
right = lobe[np.where(x[lobe] > median)[0]]
medians = np.where(x[lobe] == median)[0]

left = np.sort(np.concatenate([left, lobe[medians[1::2]]]))
right = np.sort(np.concatenate([right, lobe[medians[::2]]]))
return list(left), list(right)


@verbose
def read_vectorview_selection(name, fname=None, info=None, verbose=None):
"""Read Neuromag Vector View channel selection from a file.
Parameters
----------
name : str | list of str
Name of the selection. If a list, the selections are combined.
Supported selections are: ``'Vertex'``, ``'Left-temporal'``,
``'Right-temporal'``, ``'Left-parietal'``, ``'Right-parietal'``,
``'Left-occipital'``, ``'Right-occipital'``, ``'Left-frontal'`` and
``'Right-frontal'``. Selections can also be matched and combined by
spcecifying common substrings. For example, ``name='temporal`` will
produce a combination of ``'Left-temporal'`` and ``'Right-temporal'``.
fname : str
Filename of the selection file (if ``None``, built-in selections are
used).
info : instance of Info
Measurement info file, which will be used to determine the spacing
of channel names to return, e.g. ``'MEG 0111'`` for old Neuromag
systems and ``'MEG0111'`` for new ones.
%(verbose)s
Returns
-------
sel : list of str
List with channel names in the selection.
"""
# convert name to list of string
if not isinstance(name, (list, tuple)):
name = [name]
if isinstance(info, Info):
picks = pick_types(info, meg=True, exclude=())
if len(picks) > 0 and ' ' not in info['ch_names'][picks[0]]:
spacing = 'new'
else:
spacing = 'old'
elif info is not None:
raise TypeError('info must be an instance of Info or None, not %s'
% (type(info),))
else: # info is None
spacing = 'old'

# use built-in selections by default
if fname is None:
fname = op.join(op.dirname(__file__), '..', 'data', 'mne_analyze.sel')

fname = _check_fname(fname, must_exist=True, overwrite='read')

# use this to make sure we find at least one match for each name
name_found = {n: False for n in name}
with open(fname, 'r') as fid:
sel = []
for line in fid:
line = line.strip()
# skip blank lines and comments
if len(line) == 0 or line[0] == '#':
continue
# get the name of the selection in the file
pos = line.find(':')
if pos < 0:
logger.info('":" delimiter not found in selections file, '
'skipping line')
continue
sel_name_file = line[:pos]
# search for substring match with name provided
for n in name:
if sel_name_file.find(n) >= 0:
sel.extend(line[pos + 1:].split('|'))
name_found[n] = True
break

# make sure we found at least one match for each name
for n, found in name_found.items():
if not found:
raise ValueError('No match for selection name "%s" found' % n)

# make the selection a sorted list with unique elements
sel = list(set(sel))
sel.sort()
if spacing == 'new': # "new" or "old" by now, "old" is default
sel = [s.replace('MEG ', 'MEG') for s in sel]
return sel
Loading

0 comments on commit 53f1b85

Please sign in to comment.