From 7e7919cefd6848c003d14906fbf83304c8a6aef6 Mon Sep 17 00:00:00 2001 From: Jack Zhang Date: Thu, 8 Apr 2021 00:41:46 -0700 Subject: [PATCH] Remove MNE dependencies. --- README.md | 15 +++-- eeglabio/__init__.py | 2 +- eeglabio/_version.py | 2 +- eeglabio/epochs.py | 82 ++++++++++++++--------- eeglabio/raw.py | 87 +++++++++++++------------ eeglabio/tests/__init__.py | 2 +- eeglabio/tests/test_epochs.py | 9 +-- eeglabio/tests/test_raw.py | 7 +- eeglabio/utils.py | 118 +++++++++++++++++++++++++++------- requirements.txt | 1 - 10 files changed, 209 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 4dc6ffe..8ab8492 100644 --- a/README.md +++ b/README.md @@ -13,24 +13,27 @@ pip install -i https://test.pypi.org/simple/ eeglabio ### Dependencies eeglabio requires Python >= 3.6 and the following packages: -* [mne](https://github.com/mne-tools/mne-python) * [numpy](http://numpy.org/) * [scipy](https://www.scipy.org/) -### Usage +For testing, we also require the following additional packages: +* [mne](https://github.com/mne-tools/mne-python) + + +### Example Usage (with [MNE](https://github.com/mne-tools/mne-python)) Export from MNE [`Epochs`](https://mne.tools/stable/generated/mne.Epochs.html) to EEGLAB (`.set`): ```python import mne -from eeglabio.epochs import export_set +from eeglabio.utils import export_mne_epochs epochs = mne.Epochs(...) -export_set(epochs, "file_name.set") +export_mne_epochs(epochs, "file_name.set") ``` Export from MNE [`Raw`](https://mne.tools/stable/generated/mne.io.Raw.html) to EEGLAB (`.set`): ```python import mne -from eeglabio.raw import export_set +from eeglabio.utils import export_mne_raw raw = mne.io.read_raw(...) -export_set(raw, "file_name.set") +export_mne_raw(raw, "file_name.set") ``` diff --git a/eeglabio/__init__.py b/eeglabio/__init__.py index 20d0b13..469420a 100644 --- a/eeglabio/__init__.py +++ b/eeglabio/__init__.py @@ -2,4 +2,4 @@ from . import epochs from . import raw -__all__ = [epochs, raw] \ No newline at end of file +__all__ = [epochs, raw] diff --git a/eeglabio/_version.py b/eeglabio/_version.py index a2e82b4..dbc772b 100644 --- a/eeglabio/_version.py +++ b/eeglabio/_version.py @@ -1,3 +1,3 @@ """The version number.""" -__version__ = '0.0.1-1' +__version__ = '0.0.1-2' diff --git a/eeglabio/epochs.py b/eeglabio/epochs.py index c9c9d6f..5427bd6 100644 --- a/eeglabio/epochs.py +++ b/eeglabio/epochs.py @@ -1,56 +1,75 @@ import numpy as np from numpy.core.records import fromarrays from scipy.io import savemat -from .utils import _get_eeglab_full_cords +from .utils import cart_to_eeglab -def export_set(inst, fname): - """Export Epochs to EEGLAB's .set format. + +def export_set(fname, data, sfreq, events, tmin, tmax, ch_names, + event_id=None, ch_locs=None): + """Export epoch data to EEGLAB's .set format. Parameters ---------- - inst : mne.BaseEpochs - Epochs instance to save fname : str Name of the export file. + data : np.ndarray, shape (n_epochs, n_channels, n_samples) + Data array containing epochs. Follows the same format as + MNE Epochs' data array. + sfreq : int + sample frequency of data + events : np.ndarray, shape (n_events, 3) + Event array, the first column contains the event time in samples, + the second column contains the value of the stim channel immediately + before the event/step, and the third column contains the event id. + Follows the same format as MNE's event arrays. + tmin : float + Start time (seconds) before event. + tmax : float + End time (seconds) after event. + ch_names : list of str + Channel names. + event_id : dict + Names of conditions corresponding to event ids (last column of events). + If None, event names will default to string versions of the event ids. + ch_locs : np.ndarray, shape (n_channels, 3) + Array containing channel locations in Cartesian coordinates (x, y, z) Notes ----- Channel locations are expanded to the full EEGLAB format For more details see .io.utils.cart_to_eeglab_full_coords """ - # load data first - inst.load_data() - - # remove extra epoc and STI channels - chs_drop = [ch for ch in ['epoc', 'STI 014'] if ch in inst.ch_names] - inst.drop_channels(chs_drop) - data = inst.get_data() * 1e6 # convert to microvolts + data = data * 1e6 # convert to microvolts data = np.moveaxis(data, 0, 2) # convert to EEGLAB 3D format - fs = inst.info["sfreq"] - times = inst.times - trials = len(inst.events) # epoch count in EEGLAB - # get full EEGLAB coordinates to export - full_coords = _get_eeglab_full_cords(inst) + trials = len(events) # epoch count in EEGLAB - ch_names = inst.ch_names + if ch_locs is not None: + # get full EEGLAB coordinates to export + full_coords = cart_to_eeglab(ch_locs) - # convert to record arrays for MATLAB format - chanlocs = fromarrays( - [ch_names, *full_coords.T, np.repeat('', len(ch_names))], - names=["labels", "X", "Y", "Z", "sph_theta", "sph_phi", - "sph_radius", "theta", "radius", - "sph_theta_besa", "sph_phi_besa", "type"]) + # convert to record arrays for MATLAB format + chanlocs = fromarrays( + [ch_names, *full_coords.T, np.repeat('', len(ch_names))], + names=["labels", "X", "Y", "Z", "sph_theta", "sph_phi", + "sph_radius", "theta", "radius", + "sph_theta_besa", "sph_phi_besa", "type"]) + else: + chanlocs = fromarrays([ch_names], names=["labels"]) # reverse order of event type dict to look up events faster - event_type_d = dict((v, k) for k, v in inst.event_id.items()) - ev_types = [event_type_d[ev[2]] for ev in inst.events] + # name: value to value: name + if event_id: + event_type_d = dict((v, k) for k, v in event_id.items()) + ev_types = [event_type_d[ev[2]] for ev in events] + else: + ev_types = [str(ev[2]) for ev in events] # EEGLAB latency, in units of data sample points # ev_lat = [int(n) for n in self.events[:, 0]] - ev_lat = inst.events[:, 0] + ev_lat = events[:, 0] # event durations should all be 0 except boundaries which we don't have ev_dur = np.zeros((trials,), dtype=np.int64) @@ -75,14 +94,13 @@ def export_set(inst, fname): nbchan=data.shape[0], pnts=float(data.shape[1]), trials=trials, - srate=fs, - xmin=times[0], - xmax=times[-1], + srate=sfreq, + xmin=tmin, + xmax=tmax, chanlocs=chanlocs, event=events, epoch=epochs, icawinv=[], icasphere=[], icaweights=[])) - savemat(fname, eeg_d, - appendmat=False) + savemat(fname, eeg_d, appendmat=False) diff --git a/eeglabio/raw.py b/eeglabio/raw.py index c0a3c80..10dd67f 100644 --- a/eeglabio/raw.py +++ b/eeglabio/raw.py @@ -1,67 +1,66 @@ import numpy as np from numpy.core.records import fromarrays from scipy.io import savemat -from .utils import _get_eeglab_full_cords +from .utils import cart_to_eeglab -def export_set(inst, fname): - """Export Raw to EEGLAB's .set format. + +def export_set(fname, data, sfreq, ch_names, ch_locs=None, annotations=None): + """Export continuous raw data to EEGLAB's .set format. Parameters ---------- - inst : mne.io.BaseRaw - Raw instance to save fname : str Name of the export file. + data : np.ndarray, shape (n_epochs, n_channels, n_samples) + Data array containing epochs. Follows the same format as + MNE Epochs' data array. + sfreq : int + sample frequency of data + ch_names : list of str + Channel names. + ch_locs : np.ndarray, shape (n_channels, 3) + Array containing channel locations in Cartesian coordinates (x, y, z) + annotations : list, shape (3, n_annotations) + List containing three annotation subarrays: + first array (str) is description, + second array (float) is onset (starting time in seconds), + third array (float) is duration (in seconds) + This roughly follows MNE's Annotations structure. Notes ----- Channel locations are expanded to the full EEGLAB format For more details see .utils.cart_to_eeglab_full_coords """ - # load data first - inst.load_data() - # remove extra epoc and STI channels - chs_drop = [ch for ch in ['epoc'] if ch in inst.ch_names] - if 'STI 014' in inst.ch_names and \ - not (inst.filenames[0].endswith('.fif')): - chs_drop.append('STI 014') - inst.drop_channels(chs_drop) + data = data * 1e6 # convert to microvolts - data = inst.get_data() * 1e6 # convert to microvolts - fs = inst.info["sfreq"] - times = inst.times + if ch_locs is not None: + # get full EEGLAB coordinates to export + full_coords = cart_to_eeglab(ch_locs) - # convert xyz to full eeglab coordinates - full_coords = _get_eeglab_full_cords(inst) + # convert to record arrays for MATLAB format + chanlocs = fromarrays( + [ch_names, *full_coords.T, np.repeat('', len(ch_names))], + names=["labels", "X", "Y", "Z", "sph_theta", "sph_phi", + "sph_radius", "theta", "radius", + "sph_theta_besa", "sph_phi_besa", "type"]) + else: + chanlocs = fromarrays([ch_names], names=["labels"]) - ch_names = inst.ch_names + eeg = dict(data=data, setname=fname, nbchan=data.shape[0], + pnts=data.shape[1], trials=1, srate=sfreq, xmin=0, + xmax=data.shape[1] / sfreq, chanlocs=chanlocs, icawinv=[], + icasphere=[], icaweights=[]) - # convert to record arrays for MATLAB format - chanlocs = fromarrays( - [ch_names, *full_coords.T, np.repeat('', len(ch_names))], - names=["labels", "X", "Y", "Z", "sph_theta", "sph_phi", - "sph_radius", "theta", "radius", - "sph_theta_besa", "sph_phi_besa", "type"]) + if annotations is not None: + events = fromarrays([annotations[0], + annotations[1] * sfreq + 1, + annotations[2] * sfreq], + names=["type", "latency", "duration"]) + eeg['event'] = events - events = fromarrays([inst.annotations.description, - inst.annotations.onset * fs + 1, - inst.annotations.duration * fs], - names=["type", "latency", "duration"]) - eeg_d = dict(EEG=dict(data=data, - setname=fname, - nbchan=data.shape[0], - pnts=data.shape[1], - trials=1, - srate=fs, - xmin=times[0], - xmax=times[-1], - chanlocs=chanlocs, - event=events, - icawinv=[], - icasphere=[], - icaweights=[])) + eeg_d = dict(EEG=eeg) - savemat(fname, eeg_d, - appendmat=False) + savemat(fname, eeg_d, appendmat=False) diff --git a/eeglabio/tests/__init__.py b/eeglabio/tests/__init__.py index fa2e280..aba6507 100644 --- a/eeglabio/tests/__init__.py +++ b/eeglabio/tests/__init__.py @@ -1,3 +1,3 @@ import os.path as op -data_dir = op.join(op.dirname(__file__), 'data') \ No newline at end of file +data_dir = op.join(op.dirname(__file__), 'data') diff --git a/eeglabio/tests/test_epochs.py b/eeglabio/tests/test_epochs.py index 4d990b7..fe7338c 100644 --- a/eeglabio/tests/test_epochs.py +++ b/eeglabio/tests/test_epochs.py @@ -1,12 +1,13 @@ +from os import path as op from pathlib import Path + +import numpy as np import pytest from mne import read_events, pick_types, Epochs, read_epochs_eeglab from mne.io import read_raw_fif -from os import path as op -import numpy as np from numpy.testing import assert_allclose, assert_array_equal -from eeglabio.epochs import export_set +from eeglabio.utils import export_mne_epochs raw_fname = Path(__file__).parent / "data" / "test_raw.fif" event_name = Path(__file__).parent / "data" / 'test-eve.fif' @@ -30,7 +31,7 @@ def test_export_set(tmpdir, preload): raw.load_data() epochs = Epochs(raw, events, preload=preload) temp_fname = op.join(str(tmpdir), 'test_epochs.set') - export_set(epochs, temp_fname) + export_mne_epochs(epochs, temp_fname) epochs_read = read_epochs_eeglab(temp_fname) assert epochs.ch_names == epochs_read.ch_names cart_coords = np.array([d['loc'][:3] diff --git a/eeglabio/tests/test_raw.py b/eeglabio/tests/test_raw.py index 7a615a5..85c5cec 100644 --- a/eeglabio/tests/test_raw.py +++ b/eeglabio/tests/test_raw.py @@ -1,10 +1,11 @@ -from pathlib import Path from os import path as op +from pathlib import Path + import numpy as np from mne.io import read_raw_fif, read_raw_eeglab from numpy.testing import assert_allclose -from eeglabio.raw import export_set +from eeglabio.utils import export_mne_raw raw_fname = Path(__file__).parent / "data" / "test_raw.fif" @@ -14,7 +15,7 @@ def test_export_set(tmpdir): raw = read_raw_fif(raw_fname) raw.load_data() temp_fname = op.join(str(tmpdir), 'test_raw.set') - export_set(raw, temp_fname) + export_mne_raw(raw, temp_fname) raw_read = read_raw_eeglab(temp_fname, preload=True) assert raw.ch_names == raw_read.ch_names cart_coords = np.array([d['loc'][:3] for d in raw.info['chs']]) # just xyz diff --git a/eeglabio/utils.py b/eeglabio/utils.py index d601602..046e5d7 100644 --- a/eeglabio/utils.py +++ b/eeglabio/utils.py @@ -1,23 +1,23 @@ import numpy as np -def _cart_to_eeglab_full_coords_xyz(x, y, z): - """Convert Cartesian coordinates to EEGLAB full coordinates. +def _xyz_cart_to_eeglab_sph(x, y, z): + """Convert Cartesian coordinates to EEGLAB spherical coordinates. Also see https://github.com/sccn/eeglab/blob/develop/functions/sigprocfunc/convertlocs.m Parameters ---------- - x : ndarray, shape (n_points, ) + x : np.ndarray, shape (n_points, ) Array of x coordinates - y : ndarray, shape (n_points, ) + y : np.ndarray, shape (n_points, ) Array of y coordinates - z : ndarray, shape (n_points, ) + z : np.ndarray, shape (n_points, ) Array of z coordinates Returns ------- - sph_pts : ndarray, shape (n_points, 7) + sph_pts : np.ndarray, shape (n_points, 7) Array containing points in spherical coordinates (sph_theta, sph_phi, sph_radius, theta, radius, sph_theta_besa, sph_phi_besa) @@ -67,19 +67,19 @@ def topo2sph(theta, radius): return out -def _cart_to_eeglab_full_coords(cart): - """Convert Cartesian coordinates to EEGLAB full coordinates. +def _cart_to_eeglab_sph(cart): + """Convert Cartesian coordinates to EEGLAB spherical coordinates. Also see https://github.com/sccn/eeglab/blob/develop/functions/sigprocfunc/convertlocs.m Parameters ---------- - cart : ndarray, shape (n_points, 3) + cart : np.ndarray, shape (n_points, 3) Array containing points in Cartesian coordinates (x, y, z) Returns ------- - sph_pts : ndarray, shape (n_points, 7) + sph_pts : np.ndarray, shape (n_points, 7) Array containing points in spherical coordinates (sph_theta, sph_phi, sph_radius, theta, radius, sph_theta_besa, sph_phi_besa) @@ -89,28 +89,100 @@ def _cart_to_eeglab_full_coords(cart): assert cart.ndim == 2 and cart.shape[1] == 3 cart = np.atleast_2d(cart) x, y, z = cart.T - return _cart_to_eeglab_full_coords_xyz(x, y, z) + return _xyz_cart_to_eeglab_sph(x, y, z) -def _get_eeglab_full_cords(inst): - """Get full EEGLAB coords from MNE instance (Raw or Epochs) +def cart_to_eeglab(cart): + """Convert Cartesian coordinates to EEGLAB full coordinates. Parameters ---------- - inst: Epochs or Raw - Instance of epochs or raw to extract x,y,z coordinates from + cart : np.ndarray, shape (n_points, 3) + Array containing points in Cartesian coordinates (x, y, z) Returns ------- - full_coords : ndarray, shape (n_channels, 10) + full_coords : np.ndarray, shape (n_channels, 10) xyz + spherical and polar coords - see cart_to_eeglab_full_coords for more detail + see cart_to_eeglab_full_coords for more detail. + """ + return np.append(cart, _cart_to_eeglab_sph(cart), 1) # hstack + + +def export_mne_epochs(inst, fname): + """Export Epochs to EEGLAB's .set format. + Parameters + ---------- + inst : mne.BaseEpochs + Epochs instance to save + fname : str + Name of the export file. + + Notes + ----- + Channel locations are expanded to the full EEGLAB format + For more details see .io.utils.cart_to_eeglab_full_coords + """ + from .epochs import export_set + # load data first + inst.load_data() + + # remove extra epoc and STI channels + chs_drop = [ch for ch in ['epoc', 'STI 014'] if ch in inst.ch_names] + inst.drop_channels(chs_drop) + + chs = inst.info["chs"] + cart_coords = np.array([d['loc'][:3] for d in chs]) + if cart_coords.any(): # has coordinates + # (-y x z) to (x y z) + cart_coords[:, 0] = -cart_coords[:, 0] # -y to y + # swap x (1) and y (0) + cart_coords[:, [0, 1]] = cart_coords[:, [1, 0]] + else: + cart_coords = None + + export_set(fname, inst.get_data(), inst.info['sfreq'], inst.events, + inst.tmin, inst.tmax, inst.ch_names, inst.event_id, + cart_coords) + + +def export_mne_raw(inst, fname): + """Export Raw to EEGLAB's .set format. + + Parameters + ---------- + inst : mne.BaseRaw + Raw instance to save + fname : str + Name of the export file. + + Notes + ----- + Channel locations are expanded to the full EEGLAB format + For more details see pyeeglab.utils._cart_to_eeglab_full_coords """ + from .raw import export_set + # load data first + inst.load_data() + + # remove extra epoc and STI channels + chs_drop = [ch for ch in ['epoc'] if ch in inst.ch_names] + if 'STI 014' in inst.ch_names and \ + not (inst.filenames[0].endswith('.fif')): + chs_drop.append('STI 014') + inst.drop_channels(chs_drop) + chs = inst.info["chs"] cart_coords = np.array([d['loc'][:3] for d in chs]) - # (-y x z) to (x y z) - cart_coords[:, 0] = -cart_coords[:, 0] # -y to y - cart_coords[:, [0, 1]] = cart_coords[:, [1, 0]] # swap x (1) and y (0) - other_coords = _cart_to_eeglab_full_coords(cart_coords) - full_coords = np.append(cart_coords, other_coords, 1) # hstack - return full_coords + if cart_coords.any(): # has coordinates + # (-y x z) to (x y z) + cart_coords[:, 0] = -cart_coords[:, 0] # -y to y + # swap x (1) and y (0) + cart_coords[:, [0, 1]] = cart_coords[:, [1, 0]] + else: + cart_coords = None + + annotations = [inst.annotations.description, inst.annotations.onset, + inst.annotations.duration] + export_set(fname, inst.get_data(), inst.info['sfreq'], + inst.ch_names, cart_coords, annotations) diff --git a/requirements.txt b/requirements.txt index 30450e2..5576e19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -mne numpy scipy \ No newline at end of file