-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
MRG, ENH: Add mne.export #9427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
MRG, ENH: Add mne.export #9427
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
|
|
||
| Exporting | ||
| ================ | ||
|
|
||
| :py:mod:`mne.export`: | ||
|
|
||
| .. automodule:: mne.export | ||
| :no-members: | ||
| :no-inherited-members: | ||
|
|
||
| .. currentmodule:: mne.export | ||
|
|
||
| .. autosummary:: | ||
| :toctree: generated/ | ||
|
|
||
| export_epochs | ||
| export_raw |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from ._export import export_raw, export_epochs |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # -*- coding: utf-8 -*- | ||
| # Authors: MNE Developers | ||
| # | ||
| # License: BSD (3-clause) | ||
|
|
||
| import numpy as np | ||
|
|
||
| from ..utils import _check_eeglabio_installed | ||
| _check_eeglabio_installed() | ||
| import eeglabio.raw # noqa: E402 | ||
| import eeglabio.epochs # noqa: E402 | ||
|
|
||
|
|
||
| def _export_raw(fname, raw): | ||
| # load data first | ||
| raw.load_data() | ||
|
|
||
| # remove extra epoc and STI channels | ||
| drop_chs = ['epoc'] | ||
| if not (raw.filenames[0].endswith('.fif')): | ||
| drop_chs.append('STI 014') | ||
|
|
||
| ch_names = [ch for ch in raw.ch_names if ch not in drop_chs] | ||
| cart_coords = _get_als_coords_from_chs(raw.info['chs'], drop_chs) | ||
|
|
||
| annotations = [raw.annotations.description, | ||
| raw.annotations.onset, | ||
| raw.annotations.duration] | ||
| eeglabio.raw.export_set( | ||
| fname, data=raw.get_data(picks=ch_names), sfreq=raw.info['sfreq'], | ||
| ch_names=ch_names, ch_locs=cart_coords, annotations=annotations) | ||
|
|
||
|
|
||
| def _export_epochs(fname, epochs): | ||
| _check_eeglabio_installed() | ||
| # load data first | ||
| epochs.load_data() | ||
|
|
||
| # remove extra epoc and STI channels | ||
| drop_chs = ['epoc', 'STI 014'] | ||
| ch_names = [ch for ch in epochs.ch_names if ch not in drop_chs] | ||
| cart_coords = _get_als_coords_from_chs(epochs.info['chs'], drop_chs) | ||
|
|
||
| eeglabio.epochs.export_set( | ||
| fname, data=epochs.get_data(picks=ch_names), | ||
| sfreq=epochs.info['sfreq'], events=epochs.events, | ||
| tmin=epochs.tmin, tmax=epochs.tmax, ch_names=ch_names, | ||
| event_id=epochs.event_id, ch_locs=cart_coords) | ||
|
|
||
|
|
||
| def _get_als_coords_from_chs(chs, drop_chs=None): | ||
| """Extract channel locations in ALS format (x, y, z) from a chs instance. | ||
|
|
||
| Returns | ||
| ------- | ||
| None if no valid coordinates are found (all zeros) | ||
| """ | ||
| if drop_chs is None: | ||
| drop_chs = [] | ||
| cart_coords = np.array([d['loc'][:3] for d in chs | ||
| if d['ch_name'] not in drop_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 | ||
| return cart_coords |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| # -*- coding: utf-8 -*- | ||
| # Authors: MNE Developers | ||
| # | ||
| # License: BSD (3-clause) | ||
|
|
||
| import os.path as op | ||
|
|
||
| from ..utils import verbose, _validate_type | ||
|
|
||
|
|
||
| @verbose | ||
| def export_raw(fname, raw, fmt='auto', verbose=None): | ||
| """Export Raw to external formats. | ||
|
|
||
| Supported formats: EEGLAB (set, uses :mod:`eeglabio`) | ||
| %(export_warning)s | ||
|
|
||
| Parameters | ||
| ---------- | ||
| %(export_params_fname)s | ||
| raw : instance of Raw | ||
| The raw instance to export. | ||
| %(export_params_fmt)s | ||
| %(verbose)s | ||
|
|
||
| Notes | ||
| ----- | ||
| %(export_eeglab_note)s | ||
| """ | ||
| supported_export_formats = { # format : extensions | ||
| 'eeglab': ('set',), | ||
| 'edf': ('edf',), | ||
| 'brainvision': ('eeg', 'vmrk', 'vhdr',) | ||
| } | ||
| fmt = _infer_check_export_fmt(fmt, fname, supported_export_formats) | ||
|
|
||
| if fmt == 'eeglab': | ||
| from ._eeglab import _export_raw | ||
| _export_raw(fname, raw) | ||
| elif fmt == 'edf': | ||
| raise NotImplementedError('Export to EDF format not implemented.') | ||
| elif fmt == 'brainvision': | ||
| raise NotImplementedError('Export to BrainVision not implemented.') | ||
|
|
||
|
|
||
| @verbose | ||
| def export_epochs(fname, epochs, fmt='auto', verbose=None): | ||
| """Export Epochs to external formats. | ||
|
|
||
| Supported formats: EEGLAB (set, uses :mod:`eeglabio`) | ||
| %(export_warning)s | ||
|
|
||
| Parameters | ||
| ---------- | ||
| %(export_params_fname)s | ||
| epochs : instance of Epochs | ||
| The epochs to export. | ||
| %(export_params_fmt)s | ||
| %(verbose)s | ||
|
|
||
| Notes | ||
| ----- | ||
| %(export_eeglab_note)s | ||
| """ | ||
| supported_export_formats = { | ||
| 'eeglab': ('set',), | ||
| 'edf': ('edf',), | ||
| 'brainvision': ('eeg', 'vmrk', 'vhdr',) | ||
| } | ||
| fmt = _infer_check_export_fmt(fmt, fname, supported_export_formats) | ||
|
|
||
| if fmt == 'eeglab': | ||
| from ._eeglab import _export_epochs | ||
| _export_epochs(fname, epochs) | ||
| elif fmt == 'edf': | ||
| raise NotImplementedError('Export to EDF format not implemented.') | ||
| elif fmt == 'brainvision': | ||
| raise NotImplementedError('Export to BrainVision not implemented.') | ||
|
|
||
|
|
||
| def _infer_check_export_fmt(fmt, fname, supported_formats): | ||
| """Infer export format from filename extension if auto. | ||
|
|
||
| Raises error if fmt is auto and no file extension found, | ||
| then checks format against supported formats, raises error if format is not | ||
| supported. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| fmt : str | ||
| Format of the export, will only infer the format from filename if fmt | ||
| is auto. | ||
| fname : str | ||
| Name of the target export file, only used when fmt is auto. | ||
| supported_formats : dict of str : tuple/list | ||
| Dictionary containing supported formats (as keys) and each format's | ||
| corresponding file extensions in a tuple/list (e.g. 'eeglab': ('set',)) | ||
| """ | ||
| _validate_type(fmt, str, 'fmt') | ||
| fmt = fmt.lower() | ||
| if fmt == "auto": | ||
| fmt = op.splitext(fname)[1] | ||
| if fmt: | ||
| fmt = fmt[1:].lower() | ||
| # find fmt in supported formats dict's tuples | ||
| fmt = next((k for k, v in supported_formats.items() if fmt in v), | ||
| fmt) # default to original fmt for raising error later | ||
| else: | ||
| raise ValueError(f"Couldn't infer format from filename {fname}" | ||
| " (no extension found)") | ||
|
|
||
| if fmt not in supported_formats: | ||
| supported = [] | ||
| for format, extensions in supported_formats.items(): | ||
| ext_str = ', '.join(f'*.{ext}' for ext in extensions) | ||
| supported.append(f'{format} ({ext_str})') | ||
|
|
||
| supported_str = ', '.join(supported) | ||
| raise ValueError(f"Format '{fmt}' is not supported. " | ||
| f"Supported formats are {supported_str}.") | ||
| return fmt | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """Test exporting functions.""" | ||
| # Authors: MNE Developers | ||
| # | ||
| # License: BSD (3-clause) | ||
|
|
||
| from pathlib import Path | ||
| import os.path as op | ||
|
|
||
| import pytest | ||
| import numpy as np | ||
| from numpy.testing import assert_allclose, assert_array_equal | ||
|
|
||
| from mne import read_epochs_eeglab, Epochs | ||
| from mne.tests.test_epochs import _get_data | ||
| from mne.io import read_raw_fif, read_raw_eeglab | ||
| from mne.utils import _check_eeglabio_installed | ||
|
|
||
|
|
||
| @pytest.mark.skipif(not _check_eeglabio_installed(strict=False), | ||
| reason='eeglabio not installed') | ||
| def test_export_raw_eeglab(tmpdir): | ||
| """Test saving a Raw instance to EEGLAB's set format.""" | ||
| fname = (Path(__file__).parent.parent.parent / | ||
| "io" / "tests" / "data" / "test_raw.fif") | ||
| raw = read_raw_fif(fname) | ||
| raw.load_data() | ||
| temp_fname = op.join(str(tmpdir), 'test.set') | ||
| raw.export(temp_fname) | ||
| raw.drop_channels([ch for ch in ['epoc'] | ||
| if ch in raw.ch_names]) | ||
| 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 | ||
| cart_coords_read = np.array([d['loc'][:3] for d in raw_read.info['chs']]) | ||
| assert_allclose(cart_coords, cart_coords_read) | ||
| assert_allclose(raw.times, raw_read.times) | ||
| assert_allclose(raw.get_data(), raw_read.get_data()) | ||
|
|
||
|
|
||
| @pytest.mark.skipif(not _check_eeglabio_installed(strict=False), | ||
| reason='eeglabio not installed') | ||
| @pytest.mark.parametrize('preload', (True, False)) | ||
| def test_export_epochs_eeglab(tmpdir, preload): | ||
| """Test saving an Epochs instance to EEGLAB's set format.""" | ||
| raw, events = _get_data()[:2] | ||
| raw.load_data() | ||
| epochs = Epochs(raw, events, preload=preload) | ||
| temp_fname = op.join(str(tmpdir), 'test.set') | ||
| epochs.export(temp_fname) | ||
| epochs.drop_channels([ch for ch in ['epoc', 'STI 014'] | ||
| if ch in epochs.ch_names]) | ||
| epochs_read = read_epochs_eeglab(temp_fname) | ||
| assert epochs.ch_names == epochs_read.ch_names | ||
| cart_coords = np.array([d['loc'][:3] | ||
| for d in epochs.info['chs']]) # just xyz | ||
| cart_coords_read = np.array([d['loc'][:3] | ||
| for d in epochs_read.info['chs']]) | ||
| assert_allclose(cart_coords, cart_coords_read) | ||
| assert_array_equal(epochs.events[:, 0], | ||
| epochs_read.events[:, 0]) # latency | ||
| assert epochs.event_id.keys() == epochs_read.event_id.keys() # just keys | ||
| assert_allclose(epochs.times, epochs_read.times) | ||
| assert_allclose(epochs.get_data(), epochs_read.get_data()) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we have both BaseRaw.export and Epochs.export? it seems quite redundant here no?
so maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My idea is to expose a uniform interface
mne.export.export_*to people for all exporting functions (raw, epochs, and evoked), as it's nice to expose all exporting methods there. It is redundant withmne.Epochs.export(...), but so aremne.Epochs.plot/mne.viz.plot_epochs,mne.Covariance.save/mne.write_cov, etc. -- I think of this as a similar method-to-function mapping.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I can live without it though if you're not convinced)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@drammock you decide :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't object to the function/method redundancy.
Question about Evokeds: the class method on
Evokedwould pass[self]to themne.export.export_evokedsright? So the function can handle multiple instances, but the class method only one instance? That's how I would expect it to work anyway.I don't see the class-method versions in this changeset? Is that in a separate PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean
Epochs.export(...)andRaw.export(...)? Those are already inmain.Evoked.exportwill be part of #9406Yep, that's the plan
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind, I just wasn't reading the diff carefully enough. I see now the nested
from .export import export_epochs