Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into export-evokeds-mff
Browse files Browse the repository at this point in the history
* upstream/main:
  [MRG] change utils.logger.warning -> utils.warn (mne-tools#9434)
  FIX : rank computation from info now uses SSS proc history if only grad or mag are present (mne-tools#9435)
  MRG: Enable interpolation for all fNIRS types (mne-tools#9431)
  FIX: brain save_movie (mne-tools#9426)
  ENH: Add mne.export (mne-tools#9427)
  ENH: Test more on pre [skip circle] (mne-tools#9423)
  MRG, ENH: Speed up brain test (mne-tools#9422)
  MAINT: Update URL [ci skip]
  MNT: Reduce number of calls to _update (mne-tools#9407)
  MRG: Tutorial improvements (mne-tools#9416)
  • Loading branch information
larsoner committed Jun 2, 2021
2 parents 6a2ff0c + e6a0298 commit e50f4f0
Show file tree
Hide file tree
Showing 42 changed files with 557 additions and 361 deletions.
6 changes: 6 additions & 0 deletions doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,16 @@ Enhancements

- New function :func:`mne.label.find_pos_in_annot` to get atlas label for MRI coordinates. (:gh:`9376` by **by new contributor** |Marian Dovgialo|_)

- New namespace `mne.export` created to contain functions (such as `mne.export.export_raw` and `mne.export.export_epochs`) for exporting data to non-FIF formats (:gh:`9427` by `Eric Larson`_)

- Add support for Hitachi fNIRS devices in `mne.io.read_raw_hitachi` (:gh:`9391` by `Eric Larson`_)

- Add support for ``picks`` in :func:`mne.stc_near_sensors` (:gh:`9396` by `Eric Larson`_)

- Add projections when printing a :class:`mne.Info` in the notebook (:gh:`9403` by `Alex Gramfort`_)

- Add support for interpolating oxy and deoxyhaemoglobin data types (:gh:`9431` by `Robert Luke`_)

Bugs
~~~~
- Fix bug with :meth:`mne.Epochs.crop` and :meth:`mne.Evoked.crop` when ``include_tmax=False``, where the last sample was always cut off, even when ``tmax > epo.times[-1]`` (:gh:`9378` **by new contributor** |Jan Sosulski|_)
Expand All @@ -55,6 +59,8 @@ Bugs

- Fix bug when printing a :class:`mne.io.RawArray` in the notebook (:gh:`9404` by `Alex Gramfort`_)

- Fix bug when computing rank from info for SSS data with only gradiometers or magnetometers (:gh:`9435` by `Alex Gramfort`_)

API changes
~~~~~~~~~~~
- Nothing yet
17 changes: 17 additions & 0 deletions doc/export.rst
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
2 changes: 1 addition & 1 deletion doc/overview/governance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ Acknowledgements

Substantial portions of this document were adapted from the
`SciPy project's governance document
<https://github.com/scipy/scipy/blob/master/doc/source/governance/governance.rst>`_,
<https://github.com/scipy/scipy/blob/master/doc/source/dev/governance/governance.rst>`_,
which in turn was adapted from
`Jupyter/IPython project's governance document
<https://github.com/jupyter/governance/blob/master/governance.md>`_ and
Expand Down
1 change: 1 addition & 0 deletions doc/python_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ directly from a terminal, see :ref:`python_commands`.
reading_raw_data
file_io
creating_from_arrays
export
datasets
visualization
preprocessing
Expand Down
1 change: 1 addition & 0 deletions mne/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
from . import time_frequency
from . import viz
from . import decoding
from . import export

# initialize logging
set_log_level(None, False)
Expand Down
6 changes: 2 additions & 4 deletions mne/channels/interpolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,10 @@ def _interpolate_bads_meeg(inst, mode='accurate', origin=(0., 0., 0.04),
@verbose
def _interpolate_bads_nirs(inst, method='nearest', exclude=(), verbose=None):
from scipy.spatial.distance import pdist, squareform
from mne.preprocessing.nirs import _channel_frequencies,\
_check_channels_ordered
from mne.preprocessing.nirs import _validate_nirs_info

# Returns pick of all nirs and ensures channels are correctly ordered
freqs = np.unique(_channel_frequencies(inst.info))
picks_nirs = _check_channels_ordered(inst.info, freqs)
picks_nirs = _validate_nirs_info(inst.info)
if len(picks_nirs) == 0:
return

Expand Down
8 changes: 7 additions & 1 deletion mne/channels/tests/test_interpolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from mne import io, pick_types, pick_channels, read_events, Epochs
from mne.channels.interpolation import _make_interpolation_matrix
from mne.datasets import testing
from mne.preprocessing.nirs import optical_density, scalp_coupling_index
from mne.preprocessing.nirs import (optical_density, scalp_coupling_index,
beer_lambert_law)
from mne.datasets.testing import data_path
from mne.io import read_raw_nirx
from mne.io.proj import _has_eeg_average_ref_proj
Expand Down Expand Up @@ -303,3 +304,8 @@ def test_interpolation_nirs():
raw_od.interpolate_bads()
assert raw_od.info['bads'] == []
assert bad_0_std_pre_interp > np.std(raw_od._data[bad_0])
raw_haemo = beer_lambert_law(raw_od)
raw_haemo.info['bads'] = raw_haemo.ch_names[2:4]
assert raw_haemo.info['bads'] == ['S1_D2 hbo', 'S1_D2 hbr']
raw_haemo.interpolate_bads()
assert raw_haemo.info['bads'] == []
3 changes: 3 additions & 0 deletions mne/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
fname_trans = op.join(s_path, 'sample_audvis_trunc-trans.fif')


collect_ignore = ['export/_eeglab.py']


def pytest_configure(config):
"""Configure pytest options."""
# Markers
Expand Down
39 changes: 4 additions & 35 deletions mne/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import numpy as np

from .io.utils import _get_als_coords_from_chs
from .io.write import (start_file, start_block, end_file, end_block,
write_int, write_float, write_float_matrix,
write_double_matrix, write_complex_float_matrix,
Expand Down Expand Up @@ -52,15 +51,14 @@
from .utils import (_check_fname, check_fname, logger, verbose,
_time_mask, check_random_state, warn, _pl,
sizeof_fmt, SizeMixin, copy_function_doc_to_method_doc,
_check_pandas_installed, _check_eeglabio_installed,
_check_pandas_installed,
_check_preload, GetEpochsMixin,
_prepare_read_metadata, _prepare_write_metadata,
_check_event_id, _gen_events, _check_option,
_check_combine, ShiftTimeMixin, _build_data_frame,
_check_pandas_index_arguments, _convert_times,
_scale_dataframe_data, _check_time_format, object_size,
_on_missing, _validate_type, _ensure_events,
_infer_check_export_fmt)
_on_missing, _validate_type, _ensure_events)
from .utils.docs import fill_doc
from .data.html_templates import epochs_template

Expand Down Expand Up @@ -1836,37 +1834,8 @@ def export(self, fname, fmt='auto', verbose=None):
-----
%(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':
_check_eeglabio_installed()
import eeglabio.epochs
# load data first
self.load_data()

# remove extra epoc and STI channels
drop_chs = ['epoc', 'STI 014']
ch_names = [ch for ch in self.ch_names if ch not in drop_chs]
cart_coords = _get_als_coords_from_chs(self.info['chs'],
drop_chs)

eeglabio.epochs.export_set(fname,
data=self.get_data(picks=ch_names),
sfreq=self.info['sfreq'],
events=self.events,
tmin=self.tmin, tmax=self.tmax,
ch_names=ch_names,
event_id=self.event_id,
ch_locs=cart_coords)
elif fmt == 'edf':
raise NotImplementedError('Export to EDF format not implemented.')
elif fmt == 'brainvision':
raise NotImplementedError('Export to BrainVision not implemented.')
from .export import export_epochs
export_epochs(fname, self, fmt, verbose)

def equalize_event_counts(self, event_ids=None, method='mintime'):
"""Equalize the number of trials in each condition.
Expand Down
1 change: 1 addition & 0 deletions mne/export/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ._export import export_raw, export_epochs
69 changes: 69 additions & 0 deletions mne/export/_eeglab.py
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
121 changes: 121 additions & 0 deletions mne/export/_export.py
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
Loading

0 comments on commit e50f4f0

Please sign in to comment.