From 8be92abf35738881274845567b89b9506e53220d Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 16 Apr 2019 16:20:57 +0200 Subject: [PATCH 01/22] Add plot_montage (that uses the same code as plot_eeg_no_mri) --- examples/visualization/plot_montage.py | 62 ++++++++++++++++++++++++++ mne/datasets/utils.py | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 examples/visualization/plot_montage.py diff --git a/examples/visualization/plot_montage.py b/examples/visualization/plot_montage.py new file mode 100644 index 00000000000..6ab594c4081 --- /dev/null +++ b/examples/visualization/plot_montage.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +""" +Plotting sensor layouts of EEG Systems +====================================== + +Show sensor layouts of different EEG systems. +""" # noqa +# Authors: Alexandre Gramfort +# Joan Massich +# +# License: BSD Style. + +from mayavi import mlab +import os.path as op + +import mne +from mne.channels.montage import get_builtin_montages +from mne.viz import plot_alignment + +# print(__doc__) + +subjects_dir = op.join(mne.datasets.sample.data_path(), 'subjects') + +############################################################################### +# check all montages +# + +fix_units = {'EGI_256': 'cm', 'GSN-HydroCel-128': 'cm', + 'GSN-HydroCel-129': 'cm', 'GSN-HydroCel-256': 'cm', + 'GSN-HydroCel-257': 'cm', 'GSN-HydroCel-32': 'cm', + 'GSN-HydroCel-64_1.0': 'cm', 'GSN-HydroCel-65_1.0': 'cm', + 'biosemi128': 'mm', 'biosemi16': 'mm', 'biosemi160': 'mm', + 'biosemi256': 'mm', 'biosemi32': 'mm', 'biosemi64': 'mm', + 'easycap-M1': 'mm', 'easycap-M10': 'mm', + 'mgh60': 'm', 'mgh70': 'm', + 'standard_1005': 'm', 'standard_1020': 'm', + 'standard_alphabetic': 'm', + 'standard_postfixed': 'm', + 'standard_prefixed': 'm', + 'standard_primed': 'm'} + +############################################################################### +# check all montages +# + +for current_montage in get_builtin_montages(): + montage = mne.channels.read_montage(current_montage, + unit=fix_units[current_montage], + transform=False) + + info = mne.create_info(ch_names=montage.ch_names, + sfreq=1, + ch_types='eeg', + montage=montage) + + fig = plot_alignment(info, trans=None, + subject='fsaverage', + subjects_dir=subjects_dir, + eeg=['original', 'projected'], + ) + mlab.view(135, 80) + mlab.title(montage.kind, figure=fig) diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index 3f20a83bf2e..4fad7873084 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -252,7 +252,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True, 'datasets/foo.tgz', misc='https://codeload.github.com/mne-tools/mne-misc-data/' 'tar.gz/%s' % releases['misc'], - sample="https://osf.io/86qa2/download", + sample="https://osf.io/j4ms3/download", somato='https://osf.io/tp4sg/download', spm='https://osf.io/je4s8/download', testing='https://codeload.github.com/mne-tools/mne-testing-data/' From 22c2b41b16dc305e359d5ae342e6423cd6551e47 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 23 Apr 2019 15:01:05 +0200 Subject: [PATCH 02/22] MAINT: Use version-based permalink for download (#6161) --- mne/datasets/utils.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index 4fad7873084..b484bd62aab 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -243,29 +243,29 @@ def _data_path(path=None, force_update=False, update_path=True, download=True, # try to match url->archive_name->folder_name urls = dict( # the URLs to use brainstorm=dict( - bst_auditory='https://osf.io/5t9n8/download', - bst_phantom_ctf='https://osf.io/sxr8y/download', - bst_phantom_elekta='https://osf.io/dpcku/download', - bst_raw='https://osf.io/9675n/download', - bst_resting='https://osf.io/m7bd3/download'), + bst_auditory='https://osf.io/5t9n8/download?version=1', + bst_phantom_ctf='https://osf.io/sxr8y/download?version=1', + bst_phantom_elekta='https://osf.io/dpcku/download?version=1', + bst_raw='https://osf.io/9675n/download?version=2', + bst_resting='https://osf.io/m7bd3/download?version=3'), fake='https://github.com/mne-tools/mne-testing-data/raw/master/' 'datasets/foo.tgz', misc='https://codeload.github.com/mne-tools/mne-misc-data/' 'tar.gz/%s' % releases['misc'], - sample="https://osf.io/j4ms3/download", - somato='https://osf.io/tp4sg/download', - spm='https://osf.io/je4s8/download', + sample="https://osf.io/86qa2/download?version=4", + somato='https://osf.io/tp4sg/download?version=2', + spm='https://osf.io/je4s8/download?version=2', testing='https://codeload.github.com/mne-tools/mne-testing-data/' 'tar.gz/%s' % releases['testing'], multimodal='https://ndownloader.figshare.com/files/5999598', - opm='https://osf.io/p6ae7/download', + opm='https://osf.io/p6ae7/download?version=2', visual_92_categories=[ - 'https://osf.io/8ejrs/download', - 'https://osf.io/t4yjp/download'], - mtrf='https://osf.io/h85s2/download', - kiloword='https://osf.io/qkvf9/download', - fieldtrip_cmc='https://osf.io/j9b6s/download', - phantom_4dbti='https://osf.io/v2brw/download', + 'https://osf.io/8ejrs/download?version=1', + 'https://osf.io/t4yjp/download?version=1'], + mtrf='https://osf.io/h85s2/download?version=1', + kiloword='https://osf.io/qkvf9/download?version=1', + fieldtrip_cmc='https://osf.io/j9b6s/download?version=1', + phantom_4dbti='https://osf.io/v2brw/download?version=1', ) # filename of the resulting downloaded archive (only needed if the URL # name does not match resulting filename) From 79fb4dc60b20063dc3b9d37727720e2ecbebfe93 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Thu, 18 Apr 2019 14:17:37 +0200 Subject: [PATCH 03/22] use unit='auto' --- examples/visualization/plot_montage.py | 27 +++++++++++++++++++++++--- mne/channels/montage.py | 18 ++++++++++------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/examples/visualization/plot_montage.py b/examples/visualization/plot_montage.py index 6ab594c4081..4ef44f24987 100644 --- a/examples/visualization/plot_montage.py +++ b/examples/visualization/plot_montage.py @@ -39,14 +39,35 @@ 'standard_prefixed': 'm', 'standard_primed': 'm'} +for current_montage in get_builtin_montages(): + montage = mne.channels.read_montage(current_montage, + unit=fix_units[current_montage], + transform=False) + + info = mne.create_info(ch_names=montage.ch_names, + sfreq=1, + ch_types='eeg', + montage=montage) + + fig = plot_alignment(info, trans=None, + subject='fsaverage', + subjects_dir=subjects_dir, + eeg=['original', 'projected'], + ) + mlab.view(135, 80) + mlab.title(montage.kind, figure=fig) + ############################################################################### -# check all montages +# check all montages using 'auto' everywhere # for current_montage in get_builtin_montages(): + + cant_transform = ['EGI_256', 'easycap-M1', 'easycap-M10'] + transform = False if current_montage in cant_transform else True montage = mne.channels.read_montage(current_montage, - unit=fix_units[current_montage], - transform=False) + unit='auto', + transform=transform) info = mne.create_info(ch_names=montage.ch_names, sfreq=1, diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 28775b5a8b0..b565e651a57 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -26,7 +26,7 @@ from ..io.open import fiff_open from ..io.constants import FIFF from ..utils import (_check_fname, warn, copy_function_doc_to_method_doc, - _clean_names) + _clean_names, _check_option) from .layout import _pol_to_cart, _cart_to_sph @@ -132,9 +132,8 @@ def read_montage(kind, ch_names=None, path=None, unit='m', transform=False): path : str | None The path of the folder containing the montage file. Defaults to the mne/channels/data/montages folder in your mne-python installation. - unit : 'm' | 'cm' | 'mm' - Unit of the input file. If not 'm' (default), coordinates will be - rescaled to 'm'. + unit : 'm' | 'cm' | 'mm' | 'auto' + Unit of the input file. Defaults to 'auto'. transform : bool If True, points will be transformed to Neuromag space. The fidicuals, 'nasion', 'lpa', 'rpa' must be specified in the montage file. Useful @@ -209,6 +208,8 @@ def read_montage(kind, ch_names=None, path=None, unit='m', transform=False): .. versionadded:: 0.9.0 """ + _check_option('unit', unit, ['mm', 'cm', 'm', 'auto']) + if path is None: path = op.join(op.dirname(__file__), 'data', 'montages') if not op.isabs(kind): @@ -332,12 +333,15 @@ def read_montage(kind, ch_names=None, path=None, unit='m', transform=False): kind) selection = np.arange(len(pos)) - if unit == 'mm': + if unit == 'auto': # rescale to 0.085 + pos = 0.085 * (pos / np.linalg.norm(pos, axis=1).mean()) + elif unit == 'mm': pos /= 1e3 elif unit == 'cm': pos /= 1e2 - elif unit != 'm': - raise ValueError("'unit' should be either 'm', 'cm', or 'mm'.") + elif unit == 'm': # montage is supposed to be in m + pass + names_lower = [name.lower() for name in list(ch_names_)] fids = {key: pos[names_lower.index(fid_names[ii])] if fid_names[ii] in names_lower else None From 9b01b71a2fd230edb0eb22afb590a9319ea20b2c Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Thu, 18 Apr 2019 15:48:43 +0200 Subject: [PATCH 04/22] use auto and transform=False --- examples/visualization/plot_montage.py | 42 ++------------------------ mne/channels/montage.py | 1 + 2 files changed, 3 insertions(+), 40 deletions(-) diff --git a/examples/visualization/plot_montage.py b/examples/visualization/plot_montage.py index 4ef44f24987..30dbd72c81e 100644 --- a/examples/visualization/plot_montage.py +++ b/examples/visualization/plot_montage.py @@ -25,49 +25,11 @@ # check all montages # -fix_units = {'EGI_256': 'cm', 'GSN-HydroCel-128': 'cm', - 'GSN-HydroCel-129': 'cm', 'GSN-HydroCel-256': 'cm', - 'GSN-HydroCel-257': 'cm', 'GSN-HydroCel-32': 'cm', - 'GSN-HydroCel-64_1.0': 'cm', 'GSN-HydroCel-65_1.0': 'cm', - 'biosemi128': 'mm', 'biosemi16': 'mm', 'biosemi160': 'mm', - 'biosemi256': 'mm', 'biosemi32': 'mm', 'biosemi64': 'mm', - 'easycap-M1': 'mm', 'easycap-M10': 'mm', - 'mgh60': 'm', 'mgh70': 'm', - 'standard_1005': 'm', 'standard_1020': 'm', - 'standard_alphabetic': 'm', - 'standard_postfixed': 'm', - 'standard_prefixed': 'm', - 'standard_primed': 'm'} - for current_montage in get_builtin_montages(): - montage = mne.channels.read_montage(current_montage, - unit=fix_units[current_montage], - transform=False) - info = mne.create_info(ch_names=montage.ch_names, - sfreq=1, - ch_types='eeg', - montage=montage) - - fig = plot_alignment(info, trans=None, - subject='fsaverage', - subjects_dir=subjects_dir, - eeg=['original', 'projected'], - ) - mlab.view(135, 80) - mlab.title(montage.kind, figure=fig) - -############################################################################### -# check all montages using 'auto' everywhere -# - -for current_montage in get_builtin_montages(): - - cant_transform = ['EGI_256', 'easycap-M1', 'easycap-M10'] - transform = False if current_montage in cant_transform else True montage = mne.channels.read_montage(current_montage, unit='auto', - transform=transform) + transform=False) info = mne.create_info(ch_names=montage.ch_names, sfreq=1, @@ -77,7 +39,7 @@ fig = plot_alignment(info, trans=None, subject='fsaverage', subjects_dir=subjects_dir, - eeg=['original', 'projected'], + eeg=['projected'], ) mlab.view(135, 80) mlab.title(montage.kind, figure=fig) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index b565e651a57..adfd3d7f4a5 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -334,6 +334,7 @@ def read_montage(kind, ch_names=None, path=None, unit='m', transform=False): selection = np.arange(len(pos)) if unit == 'auto': # rescale to 0.085 + pos -= np.average(pos, axis=0) pos = 0.085 * (pos / np.linalg.norm(pos, axis=1).mean()) elif unit == 'mm': pos /= 1e3 From fac9c015f8bf719dfa41b1af5ed46e86538f0258 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Thu, 18 Apr 2019 16:46:47 +0200 Subject: [PATCH 05/22] fix --- mne/channels/montage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index adfd3d7f4a5..a10488b8677 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -334,7 +334,7 @@ def read_montage(kind, ch_names=None, path=None, unit='m', transform=False): selection = np.arange(len(pos)) if unit == 'auto': # rescale to 0.085 - pos -= np.average(pos, axis=0) + pos -= np.mean(pos, axis=0) pos = 0.085 * (pos / np.linalg.norm(pos, axis=1).mean()) elif unit == 'mm': pos /= 1e3 From 96d107059cfee96c73967f295eab80696ab5a58a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 17 Apr 2019 14:40:36 -0400 Subject: [PATCH 06/22] MRG: Fix inconsistencies with compute_proj_raw (#6160) * FIX: Fix inconsistencies with compute_proj_raw * BUG: SciPy req --- doc/whats_new.rst | 2 ++ mne/proj.py | 7 +++++-- mne/tests/test_proj.py | 41 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 6d964c44d01..0cd81593de8 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -154,6 +154,8 @@ Bug - Fix issue with bad channels ignored in :func:`mne.beamformer.make_lcmv` and :func:`mne.beamformer.make_dics` by `Alex Gramfort`_ +- Fix :func:`mne.compute_proj_raw` when ``duration != None`` not to apply existing proj and to avoid using duplicate raw data samples by `Eric Larson`_ + - Fix ``reject_by_annotation`` not being passed internally by :func:`mne.preprocessing.create_ecg_epochs` and :ref:`mne clean_eog_ecg ` to :func:`mne.preprocessing.find_ecg_events` by `Eric Larson`_ - Fix :func:`mne.io.read_raw_edf` failing when EDF header fields (such as patient name) contained special characters, by `Clemens Brunner`_ diff --git a/mne/proj.py b/mne/proj.py index 9d731e82aff..7dd6dae5e19 100644 --- a/mne/proj.py +++ b/mne/proj.py @@ -297,11 +297,14 @@ def compute_proj_raw(raw, start=0, stop=None, duration=1, n_grad=2, n_mag=2, compute_proj_epochs, compute_proj_evoked """ if duration is not None: + duration = np.round(duration * raw.info['sfreq']) / raw.info['sfreq'] events = make_fixed_length_events(raw, 999, start, stop, duration) picks = pick_types(raw.info, meg=True, eeg=True, eog=True, ecg=True, emg=True, exclude='bads') - epochs = Epochs(raw, events, None, tmin=0., tmax=duration, - picks=picks, reject=reject, flat=flat) + epochs = Epochs(raw, events, None, tmin=0., + tmax=duration - 1. / raw.info['sfreq'], + picks=picks, reject=reject, flat=flat, + baseline=None, proj=False) data = _compute_cov_epochs(epochs, n_jobs) info = epochs.info if not stop: diff --git a/mne/tests/test_proj.py b/mne/tests/test_proj.py index 192ab96b462..666ed08d117 100644 --- a/mne/tests/test_proj.py +++ b/mne/tests/test_proj.py @@ -1,26 +1,26 @@ +import copy as cp import os.path as op import numpy as np from numpy.testing import (assert_array_almost_equal, assert_allclose, assert_equal) import pytest - -import copy as cp +from scipy import linalg from mne import (compute_proj_epochs, compute_proj_evoked, compute_proj_raw, pick_types, read_events, Epochs, sensitivity_map, - read_source_estimate, compute_raw_covariance, + read_source_estimate, compute_raw_covariance, create_info, read_forward_solution, convert_forward_solution) from mne.cov import regularize, compute_whitener from mne.datasets import testing -from mne.io import read_raw_fif +from mne.io import read_raw_fif, RawArray from mne.io.proj import (make_projector, activate_proj, _needs_eeg_average_ref_proj) from mne.preprocessing import maxwell_filter from mne.proj import (read_proj, write_proj, make_eeg_average_ref_proj, _has_eeg_average_ref_proj) from mne.rank import _compute_rank_int -from mne.utils import _TempDir, run_tests_if_main +from mne.utils import _TempDir, run_tests_if_main, requires_version base_dir = op.join(op.dirname(__file__), '..', 'io', 'tests', 'data') raw_fname = op.join(base_dir, 'test_raw.fif') @@ -280,6 +280,37 @@ def test_compute_proj_raw(): assert_array_almost_equal(proj, np.eye(len(raw.ch_names))) +@requires_version('scipy', '1.0') +@pytest.mark.parametrize('duration', [1, np.pi / 2.]) +@pytest.mark.parametrize('sfreq', [600.614990234375, 1000.]) +def test_proj_raw_duration(duration, sfreq): + """Test equivalence of `duration` options.""" + n_ch, n_dim = 30, 3 + rng = np.random.RandomState(0) + signals = rng.randn(n_dim, 10000) + mixing = rng.randn(n_ch, n_dim) + [0, 1, 2] + data = np.dot(mixing, signals) + raw = RawArray(data, create_info(n_ch, sfreq, 'eeg')) + raw.set_eeg_reference(projection=True) + n_eff = int(round(raw.info['sfreq'] * duration)) + # crop to an even "duration" number of epochs + stop = ((len(raw.times) // n_eff) * n_eff - 1) / raw.info['sfreq'] + raw.crop(0, stop) + proj_def = compute_proj_raw(raw, n_eeg=n_dim) + proj_dur = compute_proj_raw(raw, duration=duration, n_eeg=n_dim) + proj_none = compute_proj_raw(raw, duration=None, n_eeg=n_dim) + assert len(proj_dur) == len(proj_none) == len(proj_def) == n_dim + # proj_def is not in here because it does not necessarily evenly divide + # the signal length: + for pu, pn in zip(proj_dur, proj_none): + assert_allclose(pu['data']['data'], pn['data']['data']) + # but we can test it here since it should still be a small subspace angle: + for proj in (proj_dur, proj_none, proj_def): + computed = np.concatenate([p['data']['data'] for p in proj], 0) + angle = np.rad2deg(linalg.subspace_angles(computed.T, mixing)[0]) + assert angle < 1e-5 + + def test_make_eeg_average_ref_proj(): """Test EEG average reference projection.""" raw = read_raw_fif(raw_fname, preload=True) From cce5cec1c14675584c252fc0656cce0d5a04aba1 Mon Sep 17 00:00:00 2001 From: Nichalas Date: Wed, 17 Apr 2019 22:16:58 +0200 Subject: [PATCH 07/22] ENH: Add the ids='all' as an option for mne.event.shift_time_events (#6143) --- doc/python_reference.rst | 1 + doc/whats_new.rst | 4 ++++ mne/event.py | 10 +++++++--- mne/tests/test_event.py | 24 +++++++++++++++++++----- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/doc/python_reference.rst b/doc/python_reference.rst index 167b7ee7a1c..d62ca061432 100644 --- a/doc/python_reference.rst +++ b/doc/python_reference.rst @@ -440,6 +440,7 @@ Events :toctree: generated/ define_target_events + shift_time_events :py:mod:`mne.epochs`: diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 0cd81593de8..b06a34b1daf 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -97,6 +97,8 @@ Changelog - Add :class:`mne.realtime.LSLClient` for realtime data acquisition with LSL streams of data by `Teon Brooks`_ and `Mainak Jas`_ +- Add option ``ids = None`` in :func:`mne.event.shift_time_events` for considering all events by `Nikolas Chalas`_ and `Joan Massich`_ + Bug ~~~ - Fix filtering functions (e.g., :meth:`mne.io.Raw.filter`) to properly take into account the two elements in ``n_pad`` parameter by `Bruno Nicenboim`_ @@ -3275,3 +3277,5 @@ of commits): .. _Katarina Slama: https://katarinaslama.github.io .. _Bruno Nicenboim: http://nicenboim.org + +.. _Nikolas Chalas: https://github.com/Nichalas diff --git a/mne/event.py b/mne/event.py index 36418c87481..36390692d1f 100644 --- a/mne/event.py +++ b/mne/event.py @@ -800,7 +800,7 @@ def shift_time_events(events, ids, tshift, sfreq): ---------- events : array, shape=(n_events, 3) The events - ids : array int + ids : ndarray of int | None The ids of events to shift. tshift : float Time-shift event. Use positive value tshift for forward shifting @@ -814,8 +814,12 @@ def shift_time_events(events, ids, tshift, sfreq): The new events. """ events = events.copy() - for ii in ids: - events[events[:, 2] == ii, 0] += int(tshift * sfreq) + if ids is None: + mask = slice(None) + else: + mask = np.in1d(events[:, 2], ids) + events[mask, 0] += int(tshift * sfreq) + return events diff --git a/mne/tests/test_event.py b/mne/tests/test_event.py index 0c63f028726..7e074497b5f 100644 --- a/mne/tests/test_event.py +++ b/mne/tests/test_event.py @@ -11,7 +11,8 @@ read_evokeds, Epochs, create_info, compute_raw_covariance) from mne.io import read_raw_fif, RawArray from mne.utils import _TempDir, run_tests_if_main -from mne.event import define_target_events, merge_events, AcqParserFIF +from mne.event import (define_target_events, merge_events, AcqParserFIF, + shift_time_events) from mne.datasets import testing base_dir = op.join(op.dirname(__file__), '..', 'io', 'tests', 'data') @@ -219,16 +220,16 @@ def test_find_events(): [[1, 0, 1], [2, 1, 2], [3, 2, 3]]) # testing with mask_type = 'and' assert_array_equal(find_events(raw, shortest_event=1, mask=1, - mask_type='and'), + mask_type='and'), [[1, 0, 1], [3, 0, 1]]) assert_array_equal(find_events(raw, shortest_event=1, mask=2, - mask_type='and'), + mask_type='and'), [[2, 0, 2]]) assert_array_equal(find_events(raw, shortest_event=1, mask=3, - mask_type='and'), + mask_type='and'), [[1, 0, 1], [2, 1, 2], [3, 2, 3]]) assert_array_equal(find_events(raw, shortest_event=1, mask=4, - mask_type='and'), + mask_type='and'), [[4, 0, 4]]) # test empty events channel @@ -542,4 +543,17 @@ def test_acqparser_averaging(): rtol=0, atol=1e-13) # tol = 1 fT/cm +def test_shift_time_events(): + """Test events latency shift by a given amount.""" + events = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]]) + EXPECTED = [1, 2, 3] + new_events = shift_time_events(events, ids=None, tshift=1, sfreq=1) + assert all(new_events[:, 0] == EXPECTED) + + events = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]]) + EXPECTED = [0, 2, 3] + new_events = shift_time_events(events, ids=[1, 2], tshift=1, sfreq=1) + assert all(new_events[:, 0] == EXPECTED) + + run_tests_if_main() From 1690d14ba20f98ca7d26765cb6c39e55ad5d58c1 Mon Sep 17 00:00:00 2001 From: Alexandre Gramfort Date: Thu, 18 Apr 2019 10:00:04 +0200 Subject: [PATCH 08/22] nilearn was updated --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0841f16fc30..35f7ccc7604 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,7 +60,6 @@ jobs: pip install --user -q --upgrade pip numpy vtk pip install --user --progress-bar off -r requirements.txt pip install --user --progress-bar off ipython sphinx_fontawesome sphinx_bootstrap_theme "https://api.github.com/repos/sphinx-gallery/sphinx-gallery/zipball/master" memory_profiler "https://api.github.com/repos/nipy/PySurfer/zipball/master" - pip install --user --upgrade --progress-bar off "https://api.github.com/repos/nilearn/nilearn/zipball/master" - save_cache: key: pip-cache From fa08e936263d98743976260106eb6f3064c1b150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dirk=20G=C3=BCtlin?= <34550289+DiGyt@users.noreply.github.com> Date: Sat, 20 Apr 2019 09:57:05 +0200 Subject: [PATCH 09/22] Raise ValueError if TFR freqs <= 0 (#6169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added import _freq_mask Enable import the function _freq_mask from .numerics * Added _freq_mask function Added _freq_mask function equivalent to _time_mask function, but for frequency * Added tests for _freq_mask Added tests for _freq_mask * Added frequency cropping args to BaseTFR class Enables frequency cropping with tfr.crop(fmin, fmax) for TFR objects * Enhanced test_crop Enhanced test_crop to also test for frequency cropping. * updated documentation for BaseTFR.crop function, fixed floating point issues in utils.numerics._freq_mask * Updated docstring for BaseTFR.crop Updated docstring for BaseTFR.crop to show fmin/fmax params * Fixed _freq_mask fixed floating point error in _freq_mask * Adapted test_freq_mask to function changes Adapted test_freq_mask to error fix in freq_mask * Adapted docstring BaseTFR.crop Adapted docstring for version in BaseTFR.crop * Removed Debugging notes Removed Debugging notes from test_crop * correct spacing correct spacing * Fixed code format Fixed some code for flake8 * Updated whats_new.rst Added changes to whats_new.rst * Updated whats_new.rst Fixed update to whats_new.rst * Eliminated Conflicts with master eliminated merge conflicts in mne.utils.__init__.py Also edited test_freq_mask and test_time_mask mne.utils.tests.test_numerics to find errors more explicitely. * fixed flake8 probs * changed _time/_freq_mask code style changed code format and error message. * Removed sfreq=None from _freq_mask -Removed sfreq=None argument default from mne/utils/numerics.py::_freq_mask -Adapted relevant tests and methods accordingly * Removed support for sfreq=None from _freq_mask -Introduced raise(ValueError) When sfreq=None -Introduced tests for sfreq=None case * Made _BaseTFR.crop omit mask if not defined. - _time_mask or _freq_mask are only called, if they were defined in crop() - _time_mask, _freq_mask were changed back to save time & mem * corrected new merge conflicts once again corrected merge conflicts with mne/utils/__init__.py * corrected conditions in _BaseTFR.crop corrected ```_time_mask``` and ```_freq_mask``` conditions in ```_BaseTFR.crop()``` * Make TFR raise value error for freqs <= 0 Make multitaper and morlet raise a value error if freqs include a frequency <= 0. Signed-off-by: Dirk Gütlin * convert freqs in morlet and _make_dpss to np.array convert freqs in morlet and _make_dpss to np.array for faster computation Signed-off-by: Dirk Gütlin * changed error statements changed error statements for invalid frequencies passed in mne/time_frequency/tfr.py ::morlet and ::_make_dpss Signed-off-by: Dirk Gütlin --- mne/time_frequency/tests/test_tfr.py | 12 ++++++++++++ mne/time_frequency/tfr.py | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index b26b9b9ccd9..ddcd1255bcd 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -232,6 +232,12 @@ def test_time_frequency(): for mode in ['same', 'valid', 'full']: cwt(data[0], Ws, use_fft=use_fft, mode=mode) + # Test invalid frequency arguments + with pytest.raises(ValueError, match=" 'freqs' must be greater than 0"): + tfr_morlet(epochs, freqs=np.arange(0, 3), n_cycles=7) + with pytest.raises(ValueError, match=" 'freqs' must be greater than 0"): + tfr_morlet(epochs, freqs=np.arange(-4, -1), n_cycles=7) + # Test decim parameter checks pytest.raises(TypeError, tfr_morlet, epochs, freqs=freqs, n_cycles=n_cycles, use_fft=True, return_itc=True, @@ -352,6 +358,12 @@ def test_tfr_multitaper(): pytest.raises(ValueError, tfr_multitaper, epochs, freqs=freqs, n_cycles=1000, time_bandwidth=4.0) + # Test invalid frequency arguments + with pytest.raises(ValueError, match=" 'freqs' must be greater than 0"): + tfr_multitaper(epochs, freqs=np.arange(0, 3), n_cycles=7) + with pytest.raises(ValueError, match=" 'freqs' must be greater than 0"): + tfr_multitaper(epochs, freqs=np.arange(-4, -1), n_cycles=7) + def test_crop(): """Test TFR cropping.""" diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 1501af906e4..f4ed613972e 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -66,6 +66,11 @@ def morlet(sfreq, freqs, n_cycles=7.0, sigma=None, zero_mean=False): Ws = list() n_cycles = np.atleast_1d(n_cycles) + freqs = np.array(freqs) + if np.any(freqs <= 0): + raise ValueError("all frequencies in 'freqs' must be " + "greater than 0.") + if (n_cycles.size != 1) and (n_cycles.size != len(freqs)): raise ValueError("n_cycles should be fixed or defined for " "each frequency.") @@ -120,6 +125,12 @@ def _make_dpss(sfreq, freqs, n_cycles=7., time_bandwidth=4.0, zero_mean=False): The wavelets time series. """ Ws = list() + + freqs = np.array(freqs) + if np.any(freqs <= 0): + raise ValueError("all frequencies in 'freqs' must be " + "greater than 0.") + if time_bandwidth < 2.0: raise ValueError("time_bandwidth should be >= 2.0 for good tapers") n_taps = int(np.floor(time_bandwidth - 1)) From 8dc00bfbf07100a344e6dc0fd04efb68546ea3b6 Mon Sep 17 00:00:00 2001 From: jona-sassenhagen Date: Mon, 22 Apr 2019 23:17:51 +0200 Subject: [PATCH 10/22] init (#6181) --- mne/viz/evoked.py | 2 +- mne/viz/topo.py | 2 +- mne/viz/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 75719125f0e..6a6baf1aaf9 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -579,7 +579,7 @@ def _plot_image(data, ax, this_type, picks, cmap, unit, units, scalings, times, _check_if_nan(data) im, t_end = _plot_masked_image( - ax, data, times, mask, picks=None, yvals=None, cmap=cmap[0], + ax, data, times, mask, yvals=None, cmap=cmap[0], vmin=vmin, vmax=vmax, mask_style=mask_style, mask_alpha=mask_alpha, mask_cmap=mask_cmap) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 3ff72dde7d7..f2e7ce65c72 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -297,7 +297,7 @@ def _imshow_tfr(ax, ch_idx, tmin, tmax, vmin, vmax, onselect, ylim=None, times = np.linspace(tmin, tmax, num=tfr[ch_idx].shape[1]) img, t_end = _plot_masked_image( - ax, tfr[ch_idx], times, mask, picks=None, yvals=freq, cmap=cmap, + ax, tfr[ch_idx], times, mask, yvals=freq, cmap=cmap, vmin=vmin, vmax=vmax, mask_style=mask_style, mask_alpha=mask_alpha, mask_cmap=mask_cmap, yscale=yscale) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 5c04bf8838a..c2c62b6f640 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2576,7 +2576,7 @@ def _check_time_unit(time_unit, times): return time_unit, times -def _plot_masked_image(ax, data, times, mask=None, picks=None, yvals=None, +def _plot_masked_image(ax, data, times, mask=None, yvals=None, cmap="RdBu_r", vmin=None, vmax=None, ylim=None, mask_style="both", mask_alpha=.25, mask_cmap="Greys", yscale="linear"): From fd1d2276ca3bfbd0ac529fbbf55926da5c4fed5c Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Mon, 22 Apr 2019 13:32:07 -0800 Subject: [PATCH 11/22] MRG+1: better title in plot_compare_evokeds (closes #6165) (#6173) * better title in plot_compare_evokeds (closes #6165) * use logger.info instead of warn * fix stupidity * simplify --- mne/viz/evoked.py | 2 ++ mne/viz/utils.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 6a6baf1aaf9..3d96540caf5 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1875,6 +1875,8 @@ def plot_compare_evokeds(evokeds, picks=None, gfp=False, colors=None, _validate_type(vlines, (list, tuple), "vlines", "list or tuple") picks = [] if picks is None else picks + if title is None and picks in _DATA_CH_TYPES_SPLIT: + title = _handle_default('titles')[picks] picks = _picks_to_idx(info, picks, allow_empty=True) if len(picks) == 0: logger.info("No picks, plotting the GFP ...") diff --git a/mne/viz/utils.py b/mne/viz/utils.py index c2c62b6f640..45c92c8aee7 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -32,7 +32,8 @@ pick_info, _picks_by_type, pick_channels_cov) from ..rank import compute_rank from ..io.proj import setup_proj -from ..utils import verbose, set_config, warn, _check_ch_locs, _check_option +from ..utils import (verbose, set_config, warn, _check_ch_locs, _check_option, + logger) from ..selection import (read_selection, _SELECTIONS, _EEG_SELECTIONS, _divide_to_regions) @@ -2557,7 +2558,7 @@ def _set_title_multiple_electrodes(title, combine, ch_names, max_chans=6, title = "{} of {} {}".format( combine, len(ch_names), ch_type) elif len(ch_names) > max_chans and combine != "gfp": - warn("More than {} channels, truncating title ...".format( + logger.info("More than {} channels, truncating title ...".format( max_chans)) title += ", ...\n({} of {} {})".format( combine, len(ch_names), ch_type,) From 09eeb1e1b92f4fc12823460448c823eea0e36257 Mon Sep 17 00:00:00 2001 From: Ivana Kojcic <44771055+ikojcic@users.noreply.github.com> Date: Mon, 22 Apr 2019 23:38:36 +0200 Subject: [PATCH 12/22] This commit adds additional explanation to the contributing guide. (#6182) --- doc/contributing.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/contributing.rst b/doc/contributing.rst index e4f0c9e7d0c..15faa7e05c8 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -33,10 +33,12 @@ you can follow those steps: $ git clone git@github.com:mne-tools/mne-python.git $ cd mne-python - $ conda env create -f environment.yml - $ conda activate mne - $ pip install -e . + $ conda env create -f environment.yml -n mne-dev + $ conda activate mne-dev + $ pip install -e . # or python setup.py develop +Note: -n flag provides the name for the new (development) environment, and +overrides any name specified in the .yml file. To check the installation, you can enter the following commands: .. code-block:: console From 2e0d87085e50f68d7fcc3fc33a72af4dc398c584 Mon Sep 17 00:00:00 2001 From: Alexandre Gramfort Date: Tue, 23 Apr 2019 09:04:42 +0200 Subject: [PATCH 13/22] DOC: make sure you get a warning when reading onsets which are not timestamps (#6184) * DOC: make sure you get a warning when reading onsets which are not timestamps in .csv * non-empty match --- mne/annotations.py | 14 ++++++++++++++ mne/tests/test_annotations.py | 20 +++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/mne/annotations.py b/mne/annotations.py index a7a97fc1c57..311e56dbdb7 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -575,6 +575,12 @@ def read_annotations(fname, sfreq='auto', uint16_codec=None): ------- annot : instance of Annotations | None The annotations. + + Notes + ----- + The annotations stored in a .csv require the onset columns to be + timestamps. If you have onsets as floats (in seconds), you should use the + .txt extension. """ from .io.brainvision.brainvision import _read_annotations_brainvision from .io.eeglab.eeglab import _read_annotations_eeglab @@ -648,6 +654,14 @@ def _read_annotations_csv(fname): description = df['description'].values if orig_time == 0: orig_time = None + + if onset_dt.unique().size != onset.unique().size: + warn("The number of onsets in the '.csv' file (%d) does not match " + "the number of onsets in the Annotations created (%d). If " + "this happens because you did not encode your onsets as " + "timestamps you should save your files as '.txt'." % + (onset_dt.unique().size, onset.unique().size)) + return Annotations(onset, duration, description, orig_time) diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 9e1f3385282..37ec0f3767c 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -686,8 +686,22 @@ def dummy_annotation_csv_file(tmpdir_factory): return fname +@pytest.fixture(scope='session') +def dummy_broken_annotation_csv_file(tmpdir_factory): + """Create csv file for testing.""" + content = ("onset,duration,description\n" + "1.,1.0,AA\n" + "3.,2.425,BB") + + fname = tmpdir_factory.mktemp('data').join('annotations_broken.csv') + fname.write(content) + return fname + + @requires_version('pandas', '0.16') -def test_io_annotation_csv(dummy_annotation_csv_file, tmpdir_factory): +def test_io_annotation_csv(dummy_annotation_csv_file, + dummy_broken_annotation_csv_file, + tmpdir_factory): """Test CSV input/output.""" annot = read_annotations(str(dummy_annotation_csv_file)) assert annot.orig_time == 1038942071.7201 @@ -707,6 +721,10 @@ def test_io_annotation_csv(dummy_annotation_csv_file, tmpdir_factory): annot2 = read_annotations(fname) _assert_annotations_equal(annot, annot2) + # Test broken .csv that does not use timestamps + with pytest.warns(RuntimeWarning, match='The number of onsets in the'): + annot2 = read_annotations(str(dummy_broken_annotation_csv_file)) + # Test for IO with .txt files From 18a45871824c26de62fe6ea5493a7d714e45cc81 Mon Sep 17 00:00:00 2001 From: Teon L Brooks Date: Tue, 23 Apr 2019 11:22:38 +0200 Subject: [PATCH 14/22] [MRG] RealTime client refactor (#6141) * Add an example using the LSLClient n_chan --> n_channels * Refactor FTClient; Add MockLSLStream, refactor test to use mock stream * update reference and whats new * fixing some errors * update style * temp * improvements to the realtime module currently the test is breaking when it comes to using the RtEpochs object. * minor fix * move the RtEpochs testing to separate PR * cleanup * fix the way super is called * updated the MockLSLStream to take raw instance * add time dilation factor, cleanup * add more info on lsl identifier * address ci * skip running test with multiprocessing on windows Windows runs into a problem with multiprocessing: 'https://stackoverflow.com/questions/50079165/' * cleaned up the windows check * update the pylsl requirement to 1.12 this is compatible across platforms --- .circleci/config.yml | 1 + README.rst | 2 +- doc/python_reference.rst | 1 + doc/whats_new.rst | 2 + environment.yml | 2 +- examples/realtime/plot_lslclient_rt.py | 55 +++++ mne/realtime/__init__.py | 4 +- mne/realtime/base_client.py | 19 +- mne/realtime/fieldtrip_client.py | 293 ++++++++----------------- mne/realtime/lsl_client.py | 10 +- mne/realtime/mock_lsl_stream.py | 83 +++++++ mne/realtime/tests/test_lsl_client.py | 68 ++---- requirements.txt | 2 +- 13 files changed, 286 insertions(+), 256 deletions(-) create mode 100644 examples/realtime/plot_lslclient_rt.py create mode 100644 mne/realtime/mock_lsl_stream.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 35f7ccc7604..1368b4cd08e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -59,6 +59,7 @@ jobs: command: | pip install --user -q --upgrade pip numpy vtk pip install --user --progress-bar off -r requirements.txt + pip install --user pylsl pip install --user --progress-bar off ipython sphinx_fontawesome sphinx_bootstrap_theme "https://api.github.com/repos/sphinx-gallery/sphinx-gallery/zipball/master" memory_profiler "https://api.github.com/repos/nipy/PySurfer/zipball/master" - save_cache: diff --git a/README.rst b/README.rst index a3a69888841..6e56ab92e27 100644 --- a/README.rst +++ b/README.rst @@ -93,7 +93,7 @@ For full functionality, some functions require: - Picard >= 0.3 - CuPy >= 4.0 (for NVIDIA CUDA acceleration) - DIPY >= 0.10.1 -- PyLSL >= 1.13.1 +- PyLSL >= 1.12 Contributing to MNE-Python ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/python_reference.rst b/doc/python_reference.rst index d62ca061432..21f973206cd 100644 --- a/doc/python_reference.rst +++ b/doc/python_reference.rst @@ -954,6 +954,7 @@ Realtime FieldTripClient LSLClient + MockLSLStream MockRtClient RtEpochs RtClient diff --git a/doc/whats_new.rst b/doc/whats_new.rst index b06a34b1daf..677132b24c6 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -99,6 +99,8 @@ Changelog - Add option ``ids = None`` in :func:`mne.event.shift_time_events` for considering all events by `Nikolas Chalas`_ and `Joan Massich`_ +- Add :class:`mne.realtime.MockLSLStream` to simulate an LSL stream for testing and examples by `Teon Brooks`_ + Bug ~~~ - Fix filtering functions (e.g., :meth:`mne.io.Raw.filter`) to properly take into account the two elements in ``n_pad`` parameter by `Bruno Nicenboim`_ diff --git a/environment.yml b/environment.yml index 4fbbc8778fc..27eb095b86d 100644 --- a/environment.yml +++ b/environment.yml @@ -45,4 +45,4 @@ dependencies: - pydocstyle - codespell - python-picard -# - pylsl>=1.13.1 Needs https://github.com/labstreaminglayer/liblsl-Python/issues/6 + - pylsl>=1.12 diff --git a/examples/realtime/plot_lslclient_rt.py b/examples/realtime/plot_lslclient_rt.py new file mode 100644 index 00000000000..14de3462c4d --- /dev/null +++ b/examples/realtime/plot_lslclient_rt.py @@ -0,0 +1,55 @@ +""" +============================================================== +Plot real-time epoch data with LSL client +============================================================== + +This example demonstrates how to use the LSL client to plot real-time +collection of event data from an LSL stream. +For the purposes of demo, a mock LSL stream is constructed. You can +replace this with the stream of your choice by changing the host id to +the desired stream. + +""" +# Author: Teon Brooks +# +# License: BSD (3-clause) +import matplotlib.pyplot as plt + +from mne.realtime import LSLClient, MockLSLStream +from mne.datasets import sample +from mne.io import read_raw_fif + +print(__doc__) + +# this is the host id that identifies your stream on LSL +host = 'mne_stream' +# this is the max wait time in seconds until client connection +wait_max = 5 + + +# Load a file to stream raw data +data_path = sample.data_path() +raw_fname = data_path + '/MEG/sample/sample_audvis_filt-0-40_raw.fif' +raw = read_raw_fif(raw_fname, preload=True).pick('eeg') + +# For this example, let's use the mock LSL stream. +stream = MockLSLStream(host, raw, 'eeg') +stream.start() + +# Let's observe it +plt.ion() # make plot interactive +_, ax = plt.subplots(1) +with LSLClient(info=raw.info, host=host, wait_max=wait_max) as client: + client_info = client.get_measurement_info() + sfreq = int(client_info['sfreq']) + print(client_info) + + # let's observe ten seconds of data + for ii in range(10): + plt.cla() + epoch = client.get_data_as_epoch(n_samples=sfreq) + epoch.average().plot(axes=ax) + plt.pause(1) +plt.draw() +# Let's terminate the mock LSL stream +stream.stop() diff --git a/mne/realtime/__init__.py b/mne/realtime/__init__.py index 86181107e1c..6145d97ae49 100644 --- a/mne/realtime/__init__.py +++ b/mne/realtime/__init__.py @@ -2,14 +2,16 @@ # Authors: Christoph Dinh # Martin Luessi -# Mainak Jas +# Mainak Jas # Matti Hamalainen +# Teon Brooks # # License: BSD (3-clause) from .client import RtClient from .epochs import RtEpochs from .lsl_client import LSLClient +from .mock_lsl_stream import MockLSLStream from .mockclient import MockRtClient from .fieldtrip_client import FieldTripClient from .stim_server_client import StimServer, StimClient diff --git a/mne/realtime/base_client.py b/mne/realtime/base_client.py index 46f014de68c..9dc4b733d43 100644 --- a/mne/realtime/base_client.py +++ b/mne/realtime/base_client.py @@ -21,6 +21,7 @@ def _buffer_recv_worker(client): print('Buffer receive thread stopped: %s' % err) +@fill_doc class _BaseClient(object): """Base Realtime Client. @@ -42,9 +43,7 @@ class _BaseClient(object): Time instant to stop receiving buffers. buffer_size : int Size of each buffer in terms of number of samples. - verbose : bool, str, int, or None - If not None, override default verbose level (see :func:`mne.verbose` - and :ref:`Logging documentation ` for more). + %(verbose)s """ def __init__(self, info=None, host='localhost', port=None, @@ -58,6 +57,8 @@ def __init__(self, info=None, host='localhost', port=None, self.tmax = tmax self.buffer_size = buffer_size self.verbose = verbose + self._recv_thread = None + self._recv_callbacks = list() def __enter__(self): # noqa: D105 @@ -132,6 +133,12 @@ def register_receive_callback(self, callback): if callback not in self._recv_callbacks: self._recv_callbacks.append(callback) + def start(self): + """Start the client.""" + self.__enter__() + + return self + def start_receive_thread(self, nchan): """Start the receive thread. @@ -149,6 +156,12 @@ def start_receive_thread(self, nchan): self._recv_thread.daemon = True self._recv_thread.start() + def stop(self): + """Stop the client.""" + self._disconnect() + + return self + def stop_receive_thread(self, stop_measurement=False): """Stop the receive thread. diff --git a/mne/realtime/fieldtrip_client.py b/mne/realtime/fieldtrip_client.py index 9ea12e0f2dc..28d2e5bced4 100644 --- a/mne/realtime/fieldtrip_client.py +++ b/mne/realtime/fieldtrip_client.py @@ -9,6 +9,7 @@ import numpy as np +from .base_client import _BaseClient from ..io import _empty_info from ..io.pick import _picks_to_idx, pick_info from ..io.constants import FIFF @@ -17,19 +18,8 @@ from ..externals.FieldTrip import Client as FtClient -def _buffer_recv_worker(ft_client): - """Worker thread that constantly receives buffers.""" - try: - for raw_buffer in ft_client.iter_raw_buffers(): - ft_client._push_raw_buffer(raw_buffer) - except RuntimeError as err: - # something is wrong, the server stopped (or something) - ft_client._recv_thread = None - logger.error('Buffer receive thread stopped: %s' % err) - - @fill_doc -class FieldTripClient(object): +class FieldTripClient(_BaseClient): """Realtime FieldTrip client. Parameters @@ -54,81 +44,96 @@ class FieldTripClient(object): """ def __init__(self, info=None, host='localhost', port=1972, wait_max=30, - tmin=None, tmax=np.inf, buffer_size=1000, - verbose=None): # noqa: D102 - self.verbose = verbose - - self.info = info - self.wait_max = wait_max - self.tmin = tmin - self.tmax = tmax - self.buffer_size = buffer_size - - self.host = host - self.port = port - - self._recv_thread = None - self._recv_callbacks = list() - - def __enter__(self): # noqa: D105 - # instantiate Fieldtrip client and connect - self.ft_client = FtClient() - - # connect to FieldTrip buffer - logger.info("FieldTripClient: Waiting for server to start") - start_time, current_time = time.time(), time.time() - success = False - while current_time < (start_time + self.wait_max): - try: - self.ft_client.connect(self.host, self.port) - logger.info("FieldTripClient: Connected") - success = True + tmin=None, tmax=np.inf, buffer_size=1000, verbose=None): + super().__init__(info, host, port, wait_max, tmin, tmax, + buffer_size, verbose) + + def __exit__(self, type, value, traceback): # noqa: D105 + self.client.disconnect() + + return self + + @fill_doc + def get_data_as_epoch(self, n_samples=1024, picks=None): + """Return last n_samples from current time. + + Parameters + ---------- + n_samples : int + Number of samples to fetch. + %(picks_all)s + + Returns + ------- + epoch : instance of Epochs + The samples fetched as an Epochs object. + + See Also + -------- + mne.Epochs.iter_evoked + """ + ft_header = self.client.getHeader() + last_samp = ft_header.nSamples - 1 + start = last_samp - n_samples + 1 + stop = last_samp + events = np.expand_dims(np.array([start, 1, 1]), axis=0) + + # get the data + data = self.client.getData([start, stop]).transpose() + + # create epoch from data + picks = _picks_to_idx(self.info, picks, 'all', exclude=()) + info = pick_info(self.info, picks) + return EpochsArray(data[picks][np.newaxis], info, events) + + def iter_raw_buffers(self): + """Return an iterator over raw buffers. + + Returns + ------- + raw_buffer : generator + Generator for iteration over raw buffers. + """ + # self.tmax_samp should be included + iter_times = list(zip( + list(range(self.tmin_samp, self.tmax_samp, self.buffer_size)), + list(range(self.tmin_samp + self.buffer_size, + self.tmax_samp + 1, self.buffer_size)))) + last_iter_sample = iter_times[-1][1] if iter_times else self.tmin_samp + if last_iter_sample < self.tmax_samp + 1: + iter_times.append((last_iter_sample, self.tmax_samp + 1)) + + for ii, (start, stop) in enumerate(iter_times): + + # wait for correct number of samples to be available + self.client.wait(stop, np.iinfo(np.uint32).max, + np.iinfo(np.uint32).max) + + # get the samples (stop index is inclusive) + raw_buffer = self.client.getData([start, stop - 1]).transpose() + + yield raw_buffer + + if self._recv_thread != threading.current_thread(): + # stop_receive_thread has been called break - except Exception: - current_time = time.time() - time.sleep(0.1) - if not success: - raise RuntimeError('Could not connect to FieldTrip Buffer') + def _connect(self): + self.client = FtClient() + self.client.connect(self.host, self.port) # retrieve header logger.info("FieldTripClient: Retrieving header") - start_time, current_time = time.time(), time.time() - while current_time < (start_time + self.wait_max): - self.ft_header = self.ft_client.getHeader() + while True: + self.ft_header = self.client.getHeader() if self.ft_header is None: - current_time = time.time() time.sleep(0.1) else: break - - if self.ft_header is None: - raise RuntimeError('Failed to retrieve Fieldtrip header!') - else: - logger.info("FieldTripClient: Header retrieved") - - self.info = self._create_info() - self.ch_names = self.ft_header.labels - - # find start and end samples - - sfreq = self.info['sfreq'] - - if self.tmin is None: - self.tmin_samp = max(0, self.ft_header.nSamples - 1) - else: - self.tmin_samp = int(round(sfreq * self.tmin)) - - if self.tmax != np.inf: - self.tmax_samp = int(round(sfreq * self.tmax)) - else: - self.tmax_samp = np.iinfo(np.uint32).max + logger.info("FieldTripClient: Header retrieved") return self - def __exit__(self, type, value, traceback): # noqa: D105 - self.ft_client.disconnect() - def _create_info(self): """Create a minimal Info dictionary for epoching, averaging, etc.""" if self.info is None: @@ -232,132 +237,20 @@ def _create_info(self): return info - def get_measurement_info(self): - """Return the measurement info. - - Returns - ------- - self.info : dict - The measurement info. - """ - return self.info - - @fill_doc - def get_data_as_epoch(self, n_samples=1024, picks=None): - """Return last n_samples from current time. - - Parameters - ---------- - n_samples : int - Number of samples to fetch. - %(picks_all)s - - Returns - ------- - epoch : instance of Epochs - The samples fetched as an Epochs object. - - See Also - -------- - mne.Epochs.iter_evoked - """ - ft_header = self.ft_client.getHeader() - last_samp = ft_header.nSamples - 1 - start = last_samp - n_samples + 1 - stop = last_samp - events = np.expand_dims(np.array([start, 1, 1]), axis=0) - - # get the data - data = self.ft_client.getData([start, stop]).transpose() - - # create epoch from data - picks = _picks_to_idx(self.info, picks, 'all', exclude=()) - info = pick_info(self.info, picks) - return EpochsArray(data[picks][np.newaxis], info, events) - - def register_receive_callback(self, callback): - """Register a raw buffer receive callback. - - Parameters - ---------- - callback : callable - The callback. The raw buffer is passed as the first parameter - to callback. - """ - if callback not in self._recv_callbacks: - self._recv_callbacks.append(callback) - - def unregister_receive_callback(self, callback): - """Unregister a raw buffer receive callback. - - Parameters - ---------- - callback : callable - The callback to unregister. - """ - if callback in self._recv_callbacks: - self._recv_callbacks.remove(callback) - - def _push_raw_buffer(self, raw_buffer): - """Push raw buffer to clients using callbacks.""" - for callback in self._recv_callbacks: - callback(raw_buffer) - - def start_receive_thread(self, nchan): - """Start the receive thread. - - If the measurement has not been started, it will also be started. - - Parameters - ---------- - nchan : int - The number of channels in the data. - """ - if self._recv_thread is None: - - self._recv_thread = threading.Thread(target=_buffer_recv_worker, - args=(self, )) - self._recv_thread.daemon = True - self._recv_thread.start() - - def stop_receive_thread(self, stop_measurement=False): - """Stop the receive thread. - - Parameters - ---------- - stop_measurement : bool - unused, for compatibility. - """ - self._recv_thread = None - - def iter_raw_buffers(self): - """Return an iterator over raw buffers. - - Returns - ------- - raw_buffer : generator - Generator for iteration over raw buffers. - """ - # self.tmax_samp should be included - iter_times = list(zip( - list(range(self.tmin_samp, self.tmax_samp, self.buffer_size)), - list(range(self.tmin_samp + self.buffer_size, - self.tmax_samp + 1, self.buffer_size)))) - last_iter_sample = iter_times[-1][1] if iter_times else self.tmin_samp - if last_iter_sample < self.tmax_samp + 1: - iter_times.append((last_iter_sample, self.tmax_samp + 1)) - - for ii, (start, stop) in enumerate(iter_times): + def _enter_extra(self): + self.ch_names = self.ft_header.labels - # wait for correct number of samples to be available - self.ft_client.wait(stop, np.iinfo(np.uint32).max, - np.iinfo(np.uint32).max) + # find start and end samples + sfreq = self.info['sfreq'] - # get the samples (stop index is inclusive) - raw_buffer = self.ft_client.getData([start, stop - 1]).transpose() + if self.tmin is None: + self.tmin_samp = max(0, self.ft_header.nSamples - 1) + else: + self.tmin_samp = int(round(sfreq * self.tmin)) - yield raw_buffer + if self.tmax != np.inf: + self.tmax_samp = int(round(sfreq * self.tmax)) + else: + self.tmax_samp = np.iinfo(np.uint32).max - if self._recv_thread != threading.current_thread(): - # stop_receive_thread has been called - break + return self diff --git a/mne/realtime/lsl_client.py b/mne/realtime/lsl_client.py index 480f7ccea82..9e1dabd2455 100644 --- a/mne/realtime/lsl_client.py +++ b/mne/realtime/lsl_client.py @@ -23,7 +23,10 @@ class LSLClient(_BaseClient): the channel type is EEG, the `standard_1005` montage is used for electrode location. host : str - The LSL identifier of the server. + The LSL identifier of the server. This is the source_id designated + when the LSL stream was created. Make sure the source_id is unique on + the LSL subnet. For more information on LSL, please check the + docstrings on `StreamInfo` and `StreamInlet` in the pylsl. port : int | None Port to use for the connection. wait_max : float @@ -74,11 +77,8 @@ def get_data_as_epoch(self, n_samples=1024, picks=None): def iter_raw_buffers(self): """Return an iterator over raw buffers.""" - pylsl = _check_pylsl_installed(strict=True) - inlet = pylsl.StreamInlet(self.client) - while True: - samples, _ = inlet.pull_chunk(max_samples=self.buffer_size) + samples, _ = self.client.pull_chunk(max_samples=self.buffer_size) yield np.vstack(samples).T diff --git a/mne/realtime/mock_lsl_stream.py b/mne/realtime/mock_lsl_stream.py new file mode 100644 index 00000000000..405f7904b41 --- /dev/null +++ b/mne/realtime/mock_lsl_stream.py @@ -0,0 +1,83 @@ +# Authors: Teon Brooks +# +# License: BSD (3-clause) +import time +from multiprocessing import Process + +from ..utils import _check_pylsl_installed +from ..io import constants + + +class MockLSLStream: + """Mock LSL Stream. + + Parameters + ---------- + host : str + The LSL identifier of the server. + raw : instance of Raw object + An instance of Raw object to be streamed. + ch_type : str + The type of data that is being streamed. + time_dilation : int + A scale factor to speed up or slow down the rate of + the data being streamed. + """ + + def __init__(self, host, raw, ch_type, time_dilation=1): + self._host = host + self._ch_type = ch_type + self._time_dilation = time_dilation + + raw.load_data().pick(ch_type) + self._raw = raw + self._sfreq = int(self._raw.info['sfreq']) + + def start(self): + """Start a mock LSL stream.""" + print("now sending data...") + self.process = Process(target=self._initiate_stream) + self.process.daemon = True + self.process.start() + + return self + + def stop(self): + """Stop a mock LSL stream.""" + self._streaming = False + self.process.terminate() + + print("Stopping stream...") + + return self + + def _initiate_stream(self): + # outlet needs to be made on the same process + pylsl = _check_pylsl_installed(strict=True) + self._streaming = True + info = pylsl.StreamInfo(name='MNE', type=self._ch_type.upper(), + channel_count=self._raw.info['nchan'], + nominal_srate=self._sfreq, + channel_format='float32', source_id=self._host) + info.desc().append_child_value("manufacturer", "MNE") + channels = info.desc().append_child("channels") + for ch in self._raw.info['chs']: + unit = ch['unit'] + keys, values = zip(*list(constants.FIFF.items())) + unit = keys[values.index(unit)] + channels.append_child("channel") \ + .append_child_value("label", ch['ch_name']) \ + .append_child_value("type", self._ch_type.lower()) \ + .append_child_value("unit", unit) + + # next make an outlet + outlet = pylsl.StreamOutlet(info) + + # let's make some data + counter = 0 + while self._streaming: + mysample = self._raw[:, counter][0].ravel().tolist() + # now send it and wait for a bit + outlet.push_sample(mysample) + counter += 1 + time.sleep(self._time_dilation / self._sfreq) diff --git a/mne/realtime/tests/test_lsl_client.py b/mne/realtime/tests/test_lsl_client.py index 7be4d674bc6..83fa751cf36 100644 --- a/mne/realtime/tests/test_lsl_client.py +++ b/mne/realtime/tests/test_lsl_client.py @@ -1,67 +1,47 @@ # Author: Teon Brooks # # License: BSD (3-clause) -from multiprocessing import Process -import time -from random import random as rand +from os import getenv, path as op +import pytest -from mne.realtime import LSLClient +from mne.realtime import LSLClient, MockLSLStream from mne.utils import run_tests_if_main, requires_pylsl +from mne.io import read_raw_fif +from mne.datasets import testing host = 'myuid34234' - - -def _start_mock_lsl_stream(host): - """Start a mock LSL stream to test LSLClient.""" - from pylsl import StreamInfo, StreamOutlet - - n_channels = 8 - sfreq = 100 - info = StreamInfo('MNE', 'EEG', n_channels, sfreq, 'float32', host) - info.desc().append_child_value("manufacturer", "MNE") - channels = info.desc().append_child("channels") - for c_id in range(1, n_channels + 1): - channels.append_child("channel") \ - .append_child_value("label", "MNE {:03d}".format(c_id)) \ - .append_child_value("type", "eeg") \ - .append_child_value("unit", "microvolts") - - # next make an outlet - outlet = StreamOutlet(info) - - print("now sending data...") - while True: - mysample = [rand(), rand(), rand(), rand(), - rand(), rand(), rand(), rand()] - mysample = [x * 1e-6 for x in mysample] - # now send it and wait for a bit - outlet.push_sample(mysample) - time.sleep(0.01) +base_dir = op.join(op.dirname(__file__), '..', '..', 'io', 'tests', 'data') +raw_fname = op.join(base_dir, 'test_raw.fif') @requires_pylsl +@testing.requires_testing_data +@pytest.mark.skipif(getenv('AZURE_CI_WINDOWS', 'false').lower() == 'true', + reason=('Running multiprocessing on Windows ' + + 'creates a BrokenPipeError, see ' + + 'https://stackoverflow.com/questions/50079165/')) def test_lsl_client(): """Test the LSLClient for connection and data retrieval.""" - n_channels = 8 - n_samples = 5 wait_max = 10 - process = Process(target=_start_mock_lsl_stream, args=(host,)) - process.daemon = True - process.start() + raw = read_raw_fif(raw_fname) + raw_info = raw.info + sfreq = raw_info['sfreq'] + stream = MockLSLStream(host, raw, ch_type='eeg') + stream.start() - with LSLClient(info=None, host=host, wait_max=wait_max) as client: + with LSLClient(info=raw_info, host=host, wait_max=wait_max) as client: client_info = client.get_measurement_info() + epoch = client.get_data_as_epoch(n_samples=sfreq) - assert ([ch["ch_name"] for ch in client_info["chs"]] == - ["MNE {:03d}".format(ch_id) for ch_id in - range(1, n_channels + 1)]) + assert client_info['nchan'] == raw_info['nchan'] + assert ([ch["ch_name"] for ch in client_info["chs"]] == + [ch_name for ch_name in raw_info['ch_names']]) - epoch = client.get_data_as_epoch(n_samples=n_samples) - assert n_channels, n_samples == epoch.get_data().shape[1:] + assert raw_info['nchan'], sfreq == epoch.get_data().shape[1:] - process.terminate() + stream.stop() run_tests_if_main() diff --git a/requirements.txt b/requirements.txt index f0a5fc9e925..04d1b22fb11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,4 +28,4 @@ neo xlrd pydocstyle flake8 -# pylsl>=1.13.1 Needs https://github.com/labstreaminglayer/liblsl-Python/issues/6 +pylsl>=1.12 From 748ff1949e66033968f5464b04b13e8c973ed632 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 23 Apr 2019 12:18:26 +0200 Subject: [PATCH 15/22] ENH: Add MNE-fsaverage (#6174) * Keep eric's fsaverage * make set_montage_coreg_path as private * stip check manifest out from fsaverage * ENH: better parameter name * move the fsaverage info to fsaverage * FIX: docstring * [skip ci] move fsaverage files * rename * update the fsaverage manifests * FIX: use the new zip files * FIX: Fix test * FIX: manifests * FIX: Manifest again * FIX: Doc * FIX: call montage setter * ENH: Simplify * FIX: string * fix * Skip the test for 3.5, since zipfiles cannot be written --- .circleci/config.yml | 3 + MANIFEST.in | 2 + doc/manual/datasets_index.rst | 7 ++ doc/python_reference.rst | 1 + doc/whats_new.rst | 2 + mne/datasets/__init__.py | 9 ++ mne/datasets/_fsaverage/__init__.py | 0 mne/datasets/_fsaverage/base.py | 146 +++++++++++++++++++++++ mne/datasets/_fsaverage/bem.txt | 11 ++ mne/datasets/_fsaverage/root.txt | 179 ++++++++++++++++++++++++++++ mne/datasets/tests/test_datasets.py | 85 ++++++++++--- mne/datasets/utils.py | 36 +++++- mne/utils/_testing.py | 8 +- 13 files changed, 468 insertions(+), 21 deletions(-) create mode 100644 mne/datasets/_fsaverage/__init__.py create mode 100644 mne/datasets/_fsaverage/base.py create mode 100644 mne/datasets/_fsaverage/bem.txt create mode 100644 mne/datasets/_fsaverage/root.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index 1368b4cd08e..aa9a885db4b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -107,6 +107,9 @@ jobs: if [[ $(cat $FNAME | grep -x ".*datasets.*sample.*" | wc -l) -gt 0 ]]; then python -c "import mne; print(mne.datasets.sample.data_path(update_path=True))"; fi; + if [[ $(cat $FNAME | grep -x ".*datasets.*fetch_fsaverage.*" | wc -l) -gt 0 ]]; then + python -c "import mne; print(mne.datasets.fetch_fsaverage(verbose=True))"; + fi; if [[ $(cat $FNAME | grep -x ".*datasets.*spm_face.*" | wc -l) -gt 0 ]]; then python -c "import mne; print(mne.datasets.spm_face.data_path(update_path=True))"; fi; diff --git a/MANIFEST.in b/MANIFEST.in index 83f4b6f46a3..16932231741 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,6 +14,8 @@ recursive-include mne/data * recursive-include mne/data/helmets * recursive-include mne/data/image * recursive-include mne/data/fsaverage * +include mne/datasets/_fsaverage/root.txt +include mne/datasets/_fsaverage/bem.txt recursive-include mne/channels/data/layouts * recursive-include mne/channels/data/montages * diff --git a/doc/manual/datasets_index.rst b/doc/manual/datasets_index.rst index 624b592c5ca..8433a32c39c 100644 --- a/doc/manual/datasets_index.rst +++ b/doc/manual/datasets_index.rst @@ -25,6 +25,13 @@ as soon as possible after the appearance of the face. Once the ``data_path`` is known, its contents can be examined using :ref:`IO functions `. +fsaverage +========= +:func:`mne.datasets.fetch_fsaverage` + +For convenience, we provide a function to separately download and extract the +(or update an existing) fsaverage subject. + Brainstorm ========== Dataset fetchers for three Brainstorm tutorials are available. Users must agree to the diff --git a/doc/python_reference.rst b/doc/python_reference.rst index 21f973206cd..9f68605a3c0 100644 --- a/doc/python_reference.rst +++ b/doc/python_reference.rst @@ -183,6 +183,7 @@ Datasets brainstorm.bst_raw.data_path eegbci.load_data fetch_aparc_sub_parcellation + fetch_fsaverage fetch_hcp_mmp_parcellation hf_sef.data_path kiloword.data_path diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 677132b24c6..ac8ced90a72 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -19,6 +19,8 @@ Current Changelog ~~~~~~~~~ +- Add convenience ``fsaverage`` subject dataset fetcher / updater :func:`mne.datasets.fetch_fsaverage` by `Eric Larson`_ + - Add ``fmin`` and ``fmax`` argument to :meth:`mne.time_frequency.AverageTFR.crop` and to :meth:`mne.time_frequency.EpochsTFR.crop` to crop TFR objects along frequency axis by `Dirk Gütlin`_ - Add support to :func:`mne.read_annotations` to read CNT formats by `Joan Massich`_ diff --git a/mne/datasets/__init__.py b/mne/datasets/__init__.py index 29f164be7fc..0f0c5a989d5 100644 --- a/mne/datasets/__init__.py +++ b/mne/datasets/__init__.py @@ -23,3 +23,12 @@ from . import sleep_physionet from .utils import (_download_all_example_data, fetch_hcp_mmp_parcellation, fetch_aparc_sub_parcellation) +from ._fsaverage.base import fetch_fsaverage + +__all__ = [ + '_download_all_example_data', '_fake', 'brainstorm', 'eegbci', + 'fetch_aparc_sub_parcellation', 'fetch_fsaverage', + 'fetch_hcp_mmp_parcellation', 'fieldtrip_cmc', 'hf_sef', 'kiloword', + 'megsim', 'misc', 'mtrf', 'multimodal', 'opm', 'phantom_4dbti', 'sample', + 'sleep_physionet', 'somato', 'spm_face', 'testing', 'visual_92_categories', +] diff --git a/mne/datasets/_fsaverage/__init__.py b/mne/datasets/_fsaverage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mne/datasets/_fsaverage/base.py b/mne/datasets/_fsaverage/base.py new file mode 100644 index 00000000000..a642e03e221 --- /dev/null +++ b/mne/datasets/_fsaverage/base.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Authors: Eric Larson +# License: BSD Style. + +import os +import os.path as op + + +from ...utils import (verbose, get_subjects_dir, set_config) + +FSAVERAGE_MANIFEST_PATH = op.dirname(__file__) + + +@verbose +def fetch_fsaverage(subjects_dir=None, verbose=None): + """Fetch and update fsaverage. + + Parameters + ---------- + subjects_dir : str | None + The path to use as the subjects directory in the MNE-Python + config file. None will use the existing config variable (i.e., + will not change anything), and if it does not exist, will use + ``~/mne_data/MNE-fsaverage-data``. + %(verbose)s + + Returns + ------- + fs_dir : str + The fsaverage directory. + (essentially ``subjects_dir + '/fsaverage'``) + + Notes + ----- + This function is designed to provide + + 1. All modern (Freesurfer 6) fsaverage subject files + 2. All MNE fsaverage parcellations + 3. fsaverage head surface, fiducials, head<->MRI trans, 1- and 3-layer + BEMs (and surfaces) + + This function will compare the contents of ``subjects_dir/fsaverage`` + to the ones provided in the remote zip file. If any are missing, + the zip file is downloaded and files are updated. No files will + be overwritten. + + .. versionadded:: 0.18 + """ + # Code used to create the BEM (other files taken from MNE-sample-data): + # + # $ mne watershed_bem -s fsaverage -d $PWD --verbose info --copy + # $ python + # >>> bem = mne.make_bem_model('fsaverage', subjects_dir='.', verbose=True) + # >>> mne.write_bem_surfaces( + # ... 'fsaverage/bem/fsaverage-5120-5120-5120-bem.fif', bem) + # >>> sol = mne.make_bem_solution(bem, verbose=True) + # >>> mne.write_bem_solution( + # ... 'fsaverage/bem/fsaverage-5120-5120-5120-bem-sol.fif', sol) + # >>> import os + # >>> import os.path as op + # >>> names = sorted(op.join(r, f) + # ... for r, d, files in os.walk('fsaverage') + # ... for f in files) + # with open('fsaverage.txt', 'w') as fid: + # fid.write('\n'.join(names)) + # + from ..utils import _manifest_check_download + subjects_dir = _set_montage_coreg_path(subjects_dir) + subjects_dir = op.abspath(subjects_dir) + fs_dir = op.join(subjects_dir, 'fsaverage') + os.makedirs(fs_dir, exist_ok=True) + + fsaverage_data_parts = { + 'root.zip': dict( + url='https://osf.io/3bxqt/download?revision=1', + hash_='98fd27539b7a2b02e3d98398179ae378', + manifest=op.join(FSAVERAGE_MANIFEST_PATH, 'root.txt'), + destination=op.join(subjects_dir), + ), + 'bem.zip': dict( + url='https://osf.io/7ve8g/download?revision=1', + hash_='07c3ccde63121f5e82d1fc20e3194497', + manifest=op.join(FSAVERAGE_MANIFEST_PATH, 'bem.txt'), + destination=op.join(subjects_dir, 'fsaverage'), + ), + } + for fname, data in fsaverage_data_parts.items(): + _manifest_check_download( + destination=data['destination'], + manifest_path=data['manifest'], + url=data['url'], + hash_=data['hash_'], + ) + return fs_dir + + +def _get_create_subjects_dir(subjects_dir): + from ..utils import _get_path + subjects_dir = get_subjects_dir(subjects_dir, raise_error=False) + if subjects_dir is None: + subjects_dir = _get_path(None, '', 'montage coregistration') + subjects_dir = op.join(subjects_dir, 'MNE-fsaverage-data') + os.makedirs(subjects_dir, exist_ok=True) + return subjects_dir + + +def _set_montage_coreg_path(subjects_dir=None): + """Set a subject directory suitable for montage(-only) coregistration. + + Parameters + ---------- + subjects_dir : str | None + The path to use as the subjects directory in the MNE-Python + config file. None will use the existing config variable (i.e., + will not change anything), and if it does not exist, will use + ``~/mne_data/MNE-fsaverage-data``. + + Returns + ------- + subjects_dir : str + The subjects directory that was used. + + See Also + -------- + mne.datasets.fetch_fsaverage + mne.get_config + mne.set_config + + Notes + ----- + If you plan to only do EEG-montage based coregistrations with fsaverage + without any MRI warping, this function can facilitate the process. + Essentially it sets the default value for ``subjects_dir`` in MNE + functions to be ``~/mne_data/MNE-fsaverage-data`` (assuming it has + not already been set to some other value). + + .. versionadded:: 0.18 + """ + subjects_dir = _get_create_subjects_dir(subjects_dir) + old_subjects_dir = get_subjects_dir(None, raise_error=False) + if old_subjects_dir is not None and old_subjects_dir != subjects_dir: + raise ValueError('The subjects dir is already set to %r, which does ' + 'not match the provided subjects_dir=%r' + % (old_subjects_dir, subjects_dir)) + set_config('SUBJECTS_DIR', subjects_dir) + return subjects_dir diff --git a/mne/datasets/_fsaverage/bem.txt b/mne/datasets/_fsaverage/bem.txt new file mode 100644 index 00000000000..5e1f8c2fa3d --- /dev/null +++ b/mne/datasets/_fsaverage/bem.txt @@ -0,0 +1,11 @@ +bem/fsaverage-fiducials.fif +bem/fsaverage-5120-5120-5120-bem.fif +bem/fsaverage-head.fif +bem/outer_skin.surf +bem/brain.surf +bem/fsaverage-trans.fif +bem/fsaverage-ico-5-src.fif +bem/outer_skull.surf +bem/inner_skull.surf +bem/fsaverage-5120-5120-5120-bem-sol.fif +bem/fsaverage-inner_skull-bem.fif diff --git a/mne/datasets/_fsaverage/root.txt b/mne/datasets/_fsaverage/root.txt new file mode 100644 index 00000000000..a6d32810cf5 --- /dev/null +++ b/mne/datasets/_fsaverage/root.txt @@ -0,0 +1,179 @@ +fsaverage/bem/fsaverage-head-dense.fif +fsaverage/bem/fsaverage-head-medium.fif +fsaverage/bem/fsaverage-head.fif +fsaverage/bem/fsaverage-ico-5-src.fif +fsaverage/label/lh.BA1.label +fsaverage/label/lh.BA2.label +fsaverage/label/lh.BA3a.label +fsaverage/label/lh.BA3b.label +fsaverage/label/lh.BA44.label +fsaverage/label/lh.BA45.label +fsaverage/label/lh.BA4a.label +fsaverage/label/lh.BA4p.label +fsaverage/label/lh.BA6.label +fsaverage/label/lh.HCPMMP1.annot +fsaverage/label/lh.HCPMMP1_combined.annot +fsaverage/label/lh.MT.label +fsaverage/label/lh.Medial_wall.label +fsaverage/label/lh.PALS_B12.labels.gii +fsaverage/label/lh.PALS_B12_Brodmann.annot +fsaverage/label/lh.PALS_B12_Lobes.annot +fsaverage/label/lh.PALS_B12_OrbitoFrontal.annot +fsaverage/label/lh.PALS_B12_Visuotopic.annot +fsaverage/label/lh.V1.label +fsaverage/label/lh.V2.label +fsaverage/label/lh.Yeo2011_17Networks_N1000.annot +fsaverage/label/lh.Yeo2011_7Networks_N1000.annot +fsaverage/label/lh.aparc.a2005s.annot +fsaverage/label/lh.aparc.a2009s.annot +fsaverage/label/lh.aparc.annot +fsaverage/label/lh.aparc.label +fsaverage/label/lh.aparc_sub.annot +fsaverage/label/lh.cortex.label +fsaverage/label/lh.entorhinal.label +fsaverage/label/lh.oasis.chubs.annot +fsaverage/label/rh.BA1.label +fsaverage/label/rh.BA2.label +fsaverage/label/rh.BA3a.label +fsaverage/label/rh.BA3b.label +fsaverage/label/rh.BA44.label +fsaverage/label/rh.BA45.label +fsaverage/label/rh.BA4a.label +fsaverage/label/rh.BA4p.label +fsaverage/label/rh.BA6.label +fsaverage/label/rh.HCPMMP1.annot +fsaverage/label/rh.HCPMMP1_combined.annot +fsaverage/label/rh.MT.label +fsaverage/label/rh.Medial_wall.label +fsaverage/label/rh.PALS_B12.labels.gii +fsaverage/label/rh.PALS_B12_Brodmann.annot +fsaverage/label/rh.PALS_B12_Lobes.annot +fsaverage/label/rh.PALS_B12_OrbitoFrontal.annot +fsaverage/label/rh.PALS_B12_Visuotopic.annot +fsaverage/label/rh.V1.label +fsaverage/label/rh.V2.label +fsaverage/label/rh.Yeo2011_17Networks_N1000.annot +fsaverage/label/rh.Yeo2011_7Networks_N1000.annot +fsaverage/label/rh.aparc.a2005s.annot +fsaverage/label/rh.aparc.a2009s.annot +fsaverage/label/rh.aparc.annot +fsaverage/label/rh.aparc.label +fsaverage/label/rh.aparc_sub.annot +fsaverage/label/rh.cortex.label +fsaverage/label/rh.entorhinal.label +fsaverage/label/rh.oasis.chubs.annot +fsaverage/mri.2mm/README +fsaverage/mri.2mm/T1.mgz +fsaverage/mri.2mm/aseg.mgz +fsaverage/mri.2mm/brain.mgz +fsaverage/mri.2mm/brainmask.mgz +fsaverage/mri.2mm/mni305.cor.mgz +fsaverage/mri.2mm/orig.mgz +fsaverage/mri.2mm/reg.2mm.dat +fsaverage/mri.2mm/reg.2mm.mni152.dat +fsaverage/mri.2mm/subcort.mask.mgz +fsaverage/mri.2mm/subcort.prob.mgz +fsaverage/mri/T1.mgz +fsaverage/mri/aparc+aseg.mgz +fsaverage/mri/aparc.a2005s+aseg.mgz +fsaverage/mri/aparc.a2009s+aseg.mgz +fsaverage/mri/aseg.mgz +fsaverage/mri/brain.mgz +fsaverage/mri/brainmask.mgz +fsaverage/mri/lh.ribbon.mgz +fsaverage/mri/mni305.cor.mgz +fsaverage/mri/orig.mgz +fsaverage/mri/p.aseg.mgz +fsaverage/mri/rh.ribbon.mgz +fsaverage/mri/ribbon.mgz +fsaverage/mri/seghead.mgz +fsaverage/mri/subcort.prob.log +fsaverage/mri/subcort.prob.mgz +fsaverage/mri/transforms/reg.mni152.2mm.dat +fsaverage/mri/transforms/talairach.xfm +fsaverage/scripts/build-stamp.txt +fsaverage/scripts/csurfdir +fsaverage/scripts/make_average_surface.log +fsaverage/scripts/make_average_volume.log +fsaverage/scripts/mkheadsurf.log +fsaverage/scripts/mris_inflate.log +fsaverage/scripts/mris_inflate_lh.log +fsaverage/scripts/mris_inflate_rh.log +fsaverage/scripts/recon-all-status.log +fsaverage/scripts/recon-all.cmd +fsaverage/scripts/recon-all.done +fsaverage/scripts/recon-all.env +fsaverage/scripts/recon-all.env.bak +fsaverage/scripts/recon-all.local-copy +fsaverage/scripts/recon-all.log +fsaverage/surf/lh.area +fsaverage/surf/lh.area.seghead +fsaverage/surf/lh.avg_curv +fsaverage/surf/lh.avg_sulc +fsaverage/surf/lh.avg_thickness +fsaverage/surf/lh.cortex.patch.3d +fsaverage/surf/lh.cortex.patch.flat +fsaverage/surf/lh.curv +fsaverage/surf/lh.curv.seghead +fsaverage/surf/lh.fsaverage_sym.sphere.reg +fsaverage/surf/lh.inflated +fsaverage/surf/lh.inflated.H +fsaverage/surf/lh.inflated.K +fsaverage/surf/lh.inflated_avg +fsaverage/surf/lh.inflated_pre +fsaverage/surf/lh.orig +fsaverage/surf/lh.orig.avg.area.mgh +fsaverage/surf/lh.orig_avg +fsaverage/surf/lh.pial +fsaverage/surf/lh.pial.avg.area.mgh +fsaverage/surf/lh.pial_avg +fsaverage/surf/lh.pial_semi_inflated +fsaverage/surf/lh.seghead +fsaverage/surf/lh.seghead.inflated +fsaverage/surf/lh.smoothwm +fsaverage/surf/lh.sphere +fsaverage/surf/lh.sphere.left_right +fsaverage/surf/lh.sphere.reg +fsaverage/surf/lh.sphere.reg.avg +fsaverage/surf/lh.sulc +fsaverage/surf/lh.sulc.seghead +fsaverage/surf/lh.thickness +fsaverage/surf/lh.white +fsaverage/surf/lh.white.avg.area.mgh +fsaverage/surf/lh.white_avg +fsaverage/surf/lh.white_avg.H +fsaverage/surf/lh.white_avg.K +fsaverage/surf/mris_preproc.surface.lh.log +fsaverage/surf/mris_preproc.surface.rh.log +fsaverage/surf/rh.area +fsaverage/surf/rh.avg_curv +fsaverage/surf/rh.avg_sulc +fsaverage/surf/rh.avg_thickness +fsaverage/surf/rh.cortex.patch.3d +fsaverage/surf/rh.cortex.patch.flat +fsaverage/surf/rh.curv +fsaverage/surf/rh.fsaverage_sym.sphere.reg +fsaverage/surf/rh.inflated +fsaverage/surf/rh.inflated.H +fsaverage/surf/rh.inflated.K +fsaverage/surf/rh.inflated_avg +fsaverage/surf/rh.inflated_pre +fsaverage/surf/rh.orig +fsaverage/surf/rh.orig.avg.area.mgh +fsaverage/surf/rh.orig_avg +fsaverage/surf/rh.pial +fsaverage/surf/rh.pial.avg.area.mgh +fsaverage/surf/rh.pial_avg +fsaverage/surf/rh.pial_semi_inflated +fsaverage/surf/rh.smoothwm +fsaverage/surf/rh.sphere +fsaverage/surf/rh.sphere.left_right +fsaverage/surf/rh.sphere.reg +fsaverage/surf/rh.sphere.reg.avg +fsaverage/surf/rh.sulc +fsaverage/surf/rh.thickness +fsaverage/surf/rh.white +fsaverage/surf/rh.white.avg.area.mgh +fsaverage/surf/rh.white_avg +fsaverage/surf/rh.white_avg.H +fsaverage/surf/rh.white_avg.K diff --git a/mne/datasets/tests/test_datasets.py b/mne/datasets/tests/test_datasets.py index 5cdfc803e05..756480d17d0 100644 --- a/mne/datasets/tests/test_datasets.py +++ b/mne/datasets/tests/test_datasets.py @@ -1,19 +1,25 @@ import os from os import path as op import shutil +import zipfile import sys import pytest from mne import datasets from mne.datasets import testing -from mne.utils import _TempDir, run_tests_if_main, requires_good_network +from mne.datasets._fsaverage.base import _set_montage_coreg_path +from mne.datasets.utils import _manifest_check_download + +from mne.utils import (run_tests_if_main, requires_good_network, modified_env, + get_subjects_dir, ArgvSetter, _pl, use_log_level, + catch_logging) subjects_dir = op.join(testing.data_path(download=False), 'subjects') -def test_datasets(): +def test_datasets_basic(tmpdir): """Test simple dataset functions.""" # XXX 'hf_sef' and 'misc' do not conform to these standards for dname in ('sample', 'somato', 'spm_face', 'testing', 'opm', @@ -34,33 +40,32 @@ def test_datasets(): assert dataset.get_version() is None assert not datasets.utils.has_dataset(check_name) print('%s: %s' % (dname, datasets.utils.has_dataset(check_name))) - tempdir = _TempDir() + tempdir = str(tmpdir) # don't let it read from the config file to get the directory, # force it to look for the default - os.environ['_MNE_FAKE_HOME_DIR'] = tempdir - try: + with modified_env(**{'_MNE_FAKE_HOME_DIR': tempdir, 'SUBJECTS_DIR': None}): assert (datasets.utils._get_path(None, 'foo', 'bar') == op.join(tempdir, 'mne_data')) - finally: - del os.environ['_MNE_FAKE_HOME_DIR'] + assert get_subjects_dir(None) is None + _set_montage_coreg_path() + sd = get_subjects_dir() + assert sd.endswith('MNE-fsaverage-data') @requires_good_network -def test_megsim(): +def test_megsim(tmpdir): """Test MEGSIM URL handling.""" - data_dir = _TempDir() paths = datasets.megsim.load_data( - 'index', 'text', 'text', path=data_dir, update_path=False) + 'index', 'text', 'text', path=str(tmpdir), update_path=False) assert len(paths) == 1 assert paths[0].endswith('index.html') @requires_good_network -def test_downloads(): +def test_downloads(tmpdir): """Test dataset URL handling.""" # Try actually downloading a dataset - data_dir = _TempDir() - path = datasets._fake.data_path(path=data_dir, update_path=False) + path = datasets._fake.data_path(path=str(tmpdir), update_path=False) assert op.isfile(op.join(path, 'bar')) assert datasets._fake.get_version() is None @@ -83,14 +88,58 @@ def test_fetch_parcellations(tmpdir): 'lh.aparc_sub.annot'), 'wb'): pass datasets.fetch_aparc_sub_parcellation(subjects_dir=this_subjects_dir) - try: - sys.argv.append('--accept-hcpmmp-license') + with ArgvSetter(('--accept-hcpmmp-license',)): datasets.fetch_hcp_mmp_parcellation(subjects_dir=this_subjects_dir) - finally: - sys.argv.pop(-1) for hemi in ('lh', 'rh'): assert op.isfile(op.join(this_subjects_dir, 'fsaverage', 'label', - '%s.aparc_sub.annot' % hemi)) + '%s.aparc_sub.annot' % hemi)) + + +_zip_fnames = ['foo/foo.txt', 'foo/bar.txt', 'foo/baz.txt'] + + +def _fake_zip_fetch(url, fname, hash_): + with zipfile.ZipFile(fname, 'w') as zipf: + for fname in _zip_fnames: + with zipf.open(fname, 'w'): + pass + +@pytest.mark.skipif(sys.version_info < (3, 6), + reason="writing zip files requires python3.6 or higher") +@pytest.mark.parametrize('n_have', range(len(_zip_fnames))) +def test_manifest_check_download(tmpdir, n_have, monkeypatch): + """Test our manifest downloader.""" + monkeypatch.setattr(datasets.utils, '_fetch_file', _fake_zip_fetch) + destination = op.join(str(tmpdir), 'empty') + manifest_path = op.join(str(tmpdir), 'manifest.txt') + with open(manifest_path, 'w') as fid: + for fname in _zip_fnames: + fid.write('%s\n' % fname) + assert n_have in range(len(_zip_fnames) + 1) + assert not op.isdir(destination) + if n_have > 0: + os.makedirs(op.join(destination, 'foo')) + assert op.isdir(op.join(destination, 'foo')) + for fname in _zip_fnames: + assert not op.isfile(op.join(destination, fname)) + for fname in _zip_fnames[:n_have]: + with open(op.join(destination, fname), 'w'): + pass + with catch_logging() as log: + with use_log_level(True): + url = hash_ = '' # we mock the _fetch_file so these are not used + _manifest_check_download(manifest_path, destination, url, hash_) + log = log.getvalue() + n_missing = 3 - n_have + assert ('%d file%s missing from' % (n_missing, _pl(n_missing))) in log + for want in ('Extracting missing', 'Successfully '): + if n_missing > 0: + assert want in log + else: + assert want not in log + assert op.isdir(destination) + for fname in _zip_fnames: + assert op.isfile(op.join(destination, fname)) run_tests_if_main() diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index b484bd62aab..171cca2ac25 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -12,14 +12,16 @@ import stat import sys import zipfile +import tempfile from distutils.version import LooseVersion import numpy as np +from ._fsaverage.base import fetch_fsaverage from .. import __version__ as mne_version from ..label import read_labels_from_annot, Label, write_labels_to_annot from ..utils import (get_config, set_config, _fetch_file, logger, warn, - verbose, get_subjects_dir, hashfunc) + verbose, get_subjects_dir, hashfunc, _pl) from ..utils.docs import docdict from ..externals.doccer import docformat @@ -583,6 +585,9 @@ def _download_all_example_data(verbose=True): megsim.load_data(condition='visual', data_format='evoked', data_type='simulation', update_path=True) eegbci.load_data(1, [6, 10, 14], update_path=True) + # If the user has SUBJECTS_DIR, respect it, if not, set it to the EEG one + # (probably on CircleCI, or otherwise advanced user) + fetch_fsaverage(None) sys.argv += ['--accept-hcpmmp-license'] try: fetch_hcp_mmp_parcellation() @@ -766,3 +771,32 @@ def fetch_hcp_mmp_parcellation(subjects_dir=None, combine=True, verbose=None): assert len(labels_out) == 46 write_labels_to_annot(labels_out, 'fsaverage', 'HCPMMP1_combined', hemi='both', subjects_dir=subjects_dir) + + +def _manifest_check_download(manifest_path, destination, url, hash_): + with open(manifest_path, 'r') as fid: + names = [name.strip() for name in fid.readlines()] + need = list() + for name in names: + if not op.isfile(op.join(destination, name)): + need.append(name) + logger.info('%d file%s missing from %s in %s' + % (len(need), _pl(need), manifest_path, destination)) + if len(need) > 0: + with tempfile.TemporaryDirectory() as path: + logger.info('Downloading missing files remotely') + + fname_path = op.join(path, 'temp.zip') + _fetch_file(url, fname_path, hash_=hash_) + logger.info('Extracting missing file%s' % (_pl(need),)) + with zipfile.ZipFile(fname_path, 'r') as ff: + members = set(f for f in ff.namelist() + if not f.endswith(op.sep)) + missing = sorted(members.symmetric_difference(set(names))) + if len(missing): + raise RuntimeError('Zip file did not have correct names:' + '\n%s' % ('\n'.join(missing))) + for name in need: + ff.extract(name, path=destination) + logger.info('Successfully extracted %d file%s' + % (len(need), _pl(need))) diff --git a/mne/utils/_testing.py b/mne/utils/_testing.py index 7d12f3091af..9befc307e59 100644 --- a/mne/utils/_testing.py +++ b/mne/utils/_testing.py @@ -547,10 +547,14 @@ def modified_env(**d): orig_env = dict() for key, val in d.items(): orig_env[key] = os.getenv(key) - os.environ[key] = val + if val is not None: + assert isinstance(val, str) + os.environ[key] = val + elif key in os.environ: + del os.environ[key] yield for key, val in orig_env.items(): if val is not None: os.environ[key] = val - else: + elif key in os.environ: del os.environ[key] From 79ccebca1d31ff36de20926b3eebaf333550e081 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 23 Apr 2019 15:59:00 +0200 Subject: [PATCH 16/22] update example to use fetch_fsaverage --- examples/visualization/plot_montage.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/visualization/plot_montage.py b/examples/visualization/plot_montage.py index 30dbd72c81e..a219a0f253c 100644 --- a/examples/visualization/plot_montage.py +++ b/examples/visualization/plot_montage.py @@ -4,7 +4,7 @@ ====================================== Show sensor layouts of different EEG systems. -""" # noqa +""" # noqa: D205, D400 # Authors: Alexandre Gramfort # Joan Massich # @@ -15,11 +15,10 @@ import mne from mne.channels.montage import get_builtin_montages +from mne.datasets import fetch_fsaverage from mne.viz import plot_alignment -# print(__doc__) - -subjects_dir = op.join(mne.datasets.sample.data_path(), 'subjects') +subjects_dir = op.dirname(fetch_fsaverage()) ############################################################################### # check all montages From c9f75ae9f0e4bd49cce470654109a7aa640cfe6e Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 23 Apr 2019 17:59:38 +0200 Subject: [PATCH 17/22] whats new + couple words --- doc/whats_new.rst | 2 ++ examples/visualization/plot_montage.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index ac8ced90a72..1275d1320ab 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -19,6 +19,8 @@ Current Changelog ~~~~~~~~~ +- Add example on how to load standard montage :ref:`sphx_glr_auto_examples_datasets_plot_montage` by `Joan Massich`_ + - Add convenience ``fsaverage`` subject dataset fetcher / updater :func:`mne.datasets.fetch_fsaverage` by `Eric Larson`_ - Add ``fmin`` and ``fmax`` argument to :meth:`mne.time_frequency.AverageTFR.crop` and to :meth:`mne.time_frequency.EpochsTFR.crop` to crop TFR objects along frequency axis by `Dirk Gütlin`_ diff --git a/examples/visualization/plot_montage.py b/examples/visualization/plot_montage.py index a219a0f253c..ed7e6461bbc 100644 --- a/examples/visualization/plot_montage.py +++ b/examples/visualization/plot_montage.py @@ -3,7 +3,8 @@ Plotting sensor layouts of EEG Systems ====================================== -Show sensor layouts of different EEG systems. +This example illustrates how to load all the EEG system montages +shipped in MNE-python, and display it on fsaverage template. """ # noqa: D205, D400 # Authors: Alexandre Gramfort # Joan Massich From 72c12e05dbbc6699ec87ab7cd5cae51e29c78f6b Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 23 Apr 2019 18:17:08 +0200 Subject: [PATCH 18/22] document 'auto' --- mne/channels/montage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index a10488b8677..c0c809087f1 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -133,7 +133,8 @@ def read_montage(kind, ch_names=None, path=None, unit='m', transform=False): The path of the folder containing the montage file. Defaults to the mne/channels/data/montages folder in your mne-python installation. unit : 'm' | 'cm' | 'mm' | 'auto' - Unit of the input file. Defaults to 'auto'. + Unit of the input file. When 'auto' the montage is normalized to + a sphere of radius equal to the average brain size. Defaults to 'auto'. transform : bool If True, points will be transformed to Neuromag space. The fidicuals, 'nasion', 'lpa', 'rpa' must be specified in the montage file. Useful From d4613b8b9f1045d90b0d87b83fae1a26d30685cf Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 23 Apr 2019 18:20:24 +0200 Subject: [PATCH 19/22] Cross reference the example --- tutorials/plot_eeg_no_mri.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tutorials/plot_eeg_no_mri.py b/tutorials/plot_eeg_no_mri.py index 7125eedfac6..16aebc09cc7 100644 --- a/tutorials/plot_eeg_no_mri.py +++ b/tutorials/plot_eeg_no_mri.py @@ -10,6 +10,9 @@ subject will be less accurate. Do not over interpret activity locations which can be off by multiple centimeters. +.. note:: :ref:`sphx_glr_auto_examples_datasets_plot_montage` show all the + standard montages in MNE-Phython. + .. contents:: This tutorial covers: :local: :depth: 2 From f5553d4de7a5eceda3a25662007ab8d34efcbd10 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 23 Apr 2019 18:44:42 +0200 Subject: [PATCH 20/22] fix cross references --- doc/whats_new.rst | 4 ++-- examples/visualization/plot_montage.py | 2 ++ tutorials/plot_eeg_no_mri.py | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 41b0f18d5f3..58fde222f18 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -19,9 +19,9 @@ Current Changelog ~~~~~~~~~ -- Add example on how to load standard montage :ref:`sphx_glr_auto_examples_datasets_plot_montage` by `Joan Massich`_ +- Add example on how to load standard montage :ref:`plot_montage` by `Joan Massich`_ -- Add new tutorial on :ref:`sphx_glr_auto_tutorials_plot_eeg_no_mri.py` by `Alex Gramfort`_, and `Joan Massich`_ +- Add new tutorial on :ref:`plot_eeg_no_mri` by `Alex Gramfort`_, and `Joan Massich`_ - Add convenience ``fsaverage`` subject dataset fetcher / updater :func:`mne.datasets.fetch_fsaverage` by `Eric Larson`_ diff --git a/examples/visualization/plot_montage.py b/examples/visualization/plot_montage.py index ed7e6461bbc..3af9779a554 100644 --- a/examples/visualization/plot_montage.py +++ b/examples/visualization/plot_montage.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- """ +.. _plot_montage: + Plotting sensor layouts of EEG Systems ====================================== diff --git a/tutorials/plot_eeg_no_mri.py b/tutorials/plot_eeg_no_mri.py index 16aebc09cc7..ed2ae81260d 100644 --- a/tutorials/plot_eeg_no_mri.py +++ b/tutorials/plot_eeg_no_mri.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- """ +.. _plot_eeg_no_mri: + EEG forward operator with a template MRI ======================================== From 8f1613580692c709e9e2cdd7d49fa48e28f92db2 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 23 Apr 2019 18:50:34 +0200 Subject: [PATCH 21/22] I forgot one --- tutorials/plot_eeg_no_mri.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tutorials/plot_eeg_no_mri.py b/tutorials/plot_eeg_no_mri.py index ed2ae81260d..b7718038cb3 100644 --- a/tutorials/plot_eeg_no_mri.py +++ b/tutorials/plot_eeg_no_mri.py @@ -12,8 +12,7 @@ subject will be less accurate. Do not over interpret activity locations which can be off by multiple centimeters. -.. note:: :ref:`sphx_glr_auto_examples_datasets_plot_montage` show all the - standard montages in MNE-Phython. +.. note:: :ref:`plot_montage` show all the standard montages in MNE-Phython. .. contents:: This tutorial covers: :local: From f0044f3dc14b2603aecb22f81d930080ffd0eb5c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 24 Apr 2019 00:27:54 +0200 Subject: [PATCH 22/22] FIX: Spelling [ci skip] --- tutorials/plot_eeg_no_mri.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/plot_eeg_no_mri.py b/tutorials/plot_eeg_no_mri.py index b7718038cb3..bf9dd0d6850 100644 --- a/tutorials/plot_eeg_no_mri.py +++ b/tutorials/plot_eeg_no_mri.py @@ -12,7 +12,7 @@ subject will be less accurate. Do not over interpret activity locations which can be off by multiple centimeters. -.. note:: :ref:`plot_montage` show all the standard montages in MNE-Phython. +.. note:: :ref:`plot_montage` show all the standard montages in MNE-Python. .. contents:: This tutorial covers: :local: