Skip to content

Commit

Permalink
[MRG] Add support for eeglab export (#1187)
Browse files Browse the repository at this point in the history
* Add support for eeglab export

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* changelog, test

* fix typo

* add eeglabio to optional packages

* language: SET -> EEGLAB

* fix runtime warning

* fix legacy warning

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Stefan Appelhoff <stefan.appelhoff@mailbox.org>
  • Loading branch information
3 people committed Nov 16, 2023
1 parent 5ad1db4 commit 7708a3f
Show file tree
Hide file tree
Showing 9 changed files with 57 additions and 18 deletions.
1 change: 1 addition & 0 deletions .github/workflows/unit_tests.yml
Expand Up @@ -102,6 +102,7 @@ jobs:
python -c 'import mne_bids; print(mne_bids.__version__)'
python -c 'import nibabel; print(nibabel.__version__)'
python -c 'import pybv; print(pybv.__version__)'
python -c 'import eeglabio; print(eeglabio.__version__)'
python -c 'import pymatreader; print(pymatreader.__version__)'
python -c 'import matplotlib; print(matplotlib.__version__)'
python -c 'import pandas; print(pandas.__version__)'
Expand Down
3 changes: 2 additions & 1 deletion doc/install.rst
Expand Up @@ -20,7 +20,8 @@ Optional:

* ``nibabel`` (>=3.2.1, for processing MRI data)
* ``pybv`` (>=0.7.5, for writing BrainVision data)
* ``pymatreader`` (>=0.0.30 , for operations with EEGLAB data)
* ``eeglabio`` (>=0.0.2, for writing EEGLAB data)
* ``pymatreader`` (>=0.0.30, for other operations with EEGLAB data)
* ``matplotlib`` (>=3.4.0, for using the interactive data inspector)
* ``pandas`` (>=1.2.4, for generating event statistics)
* ``EDFlib-Python`` (>=1.0.6, for writing EDF data)
Expand Down
4 changes: 2 additions & 2 deletions doc/whats_new.rst
Expand Up @@ -30,7 +30,7 @@ Detailed list of changes
🚀 Enhancements
^^^^^^^^^^^^^^^

- nothing yet
- Enable exporting to the EEGLAB data format (``.set``), by `Laetitia Fesselier`_ and `Stefan Appelhoff`_ (:gh:`1187`)

🧐 API and behavior changes
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand All @@ -40,7 +40,7 @@ Detailed list of changes
🛠 Requirements
^^^^^^^^^^^^^^^

- nothing yet
- ``eeglabio`` is now a required package if you want to export to the EEGLAB data format.

🪲 Bug fixes
^^^^^^^^^^^^
Expand Down
1 change: 1 addition & 0 deletions mne_bids/config.py
Expand Up @@ -6,6 +6,7 @@
BIDS_VERSION = "1.7.0"

PYBV_VERSION = "0.7.3"
EEGLABIO_VERSION = "0.0.2"

DOI = """https://doi.org/10.21105/joss.01896"""

Expand Down
8 changes: 4 additions & 4 deletions mne_bids/tests/test_dig.py
Expand Up @@ -109,7 +109,7 @@ def test_dig_pixels(tmp_path):
op.join(bids_root, "sub-01", "ses-01", bids_path.datatype), exist_ok=True
)
raw = _load_raw()
raw.pick_types(eeg=True)
raw.pick(["eeg"])
raw.del_proj()
raw.set_channel_types({ch: "ecog" for ch in raw.ch_names})

Expand Down Expand Up @@ -145,7 +145,7 @@ def test_dig_template(tmp_path):
bids_path = _bids_path.copy().update(root=bids_root, datatype=datatype)
for coord_frame in BIDS_STANDARD_TEMPLATE_COORDINATE_SYSTEMS:
raw = _load_raw()
raw.pick_types(eeg=True)
raw.pick(["eeg"])
bids_path.update(space=coord_frame)
montage = raw.get_montage()
pos = montage.get_positions()
Expand Down Expand Up @@ -251,7 +251,7 @@ def test_template_to_head():
# test all coordinate frames
raw = _load_raw()
raw.set_montage(None)
raw.pick_types(eeg=True)
raw.pick(["eeg"])
raw.drop_channels(raw.ch_names[3:])
montage = mne.channels.make_dig_montage(
ch_pos={
Expand Down Expand Up @@ -365,7 +365,7 @@ def test_convert_montage():
def test_electrodes_io(tmp_path):
"""Ensure only electrodes end up in *_electrodes.json."""
raw = _load_raw()
raw.pick_types(eeg=True, stim=True) # we don't need meg channels
raw.pick(["eeg", "stim"]) # we don't need meg channels
bids_root = tmp_path / "bids1"
bids_path = _bids_path.copy().update(root=bids_root, datatype="eeg")
write_raw_bids(raw=raw, bids_path=bids_path)
Expand Down
22 changes: 16 additions & 6 deletions mne_bids/tests/test_write.py
Expand Up @@ -59,7 +59,12 @@
from mne_bids.sidecar_updates import _update_sidecar, update_sidecar_json
from mne_bids.path import _find_matching_sidecar, _parse_ext
from mne_bids.pick import coil_type
from mne_bids.config import REFERENCES, BIDS_COORD_FRAME_DESCRIPTIONS, PYBV_VERSION
from mne_bids.config import (
REFERENCES,
BIDS_COORD_FRAME_DESCRIPTIONS,
PYBV_VERSION,
EEGLABIO_VERSION,
)

base_path = op.join(op.dirname(mne.__file__), "io")
subject_id = "01"
Expand Down Expand Up @@ -148,6 +153,7 @@ def fn(fname, *args, **kwargs):

# parametrization for testing converting file formats for EEG/iEEG
test_converteeg_data = [
("EEGLAB", "EEGLAB", "test_raw.set", _read_raw_eeglab), # noqa
(
"Persyst",
"BrainVision",
Expand Down Expand Up @@ -564,7 +570,7 @@ def test_fif(_bids_validate, tmp_path):
bids_path.update(root=bids_root)
raw = _read_raw_fif(raw_fname)
raw.load_data()
raw2 = raw.pick_types(meg=False, eeg=True, stim=True, eog=True, ecg=True)
raw2 = raw.pick(["eeg", "stim", "eog", "ecg"])
raw2.save(bids_root / "test-raw.fif", overwrite=True)
raw2 = mne.io.Raw(op.join(bids_root, "test-raw.fif"), preload=False)
events = mne.find_events(raw2)
Expand Down Expand Up @@ -3328,6 +3334,7 @@ def test_sidecar_encoding(_bids_validate, tmp_path):
def test_convert_eeg_formats(dir_name, format, fname, reader, tmp_path):
"""Test conversion of EEG/iEEG manufacturer fmt to BrainVision/EDF."""
pytest.importorskip("pybv", PYBV_VERSION)
pytest.importorskip("eeglabio", EEGLABIO_VERSION)
bids_root = tmp_path / format
raw_fname = data_path / dir_name / fname

Expand All @@ -3336,7 +3343,7 @@ def test_convert_eeg_formats(dir_name, format, fname, reader, tmp_path):

raw = reader(raw_fname)
# drop 'misc' type channels when exporting
raw = raw.pick_types(eeg=True)
raw = raw.pick(["eeg"])
kwargs = dict(
raw=raw, format=format, bids_path=bids_path, overwrite=True, verbose=False
)
Expand Down Expand Up @@ -3366,11 +3373,13 @@ def test_convert_eeg_formats(dir_name, format, fname, reader, tmp_path):
):
bids_output_path = write_raw_bids(**kwargs)
else:
with pytest.warns(RuntimeWarning, match="Converting data files to EDF format"):
with pytest.warns(
RuntimeWarning, match=f"Converting data files to {format} format"
):
bids_output_path = write_raw_bids(**kwargs)

# channel units should stay the same
raw2 = read_raw_bids(bids_output_path)
raw2 = read_raw_bids(bids_output_path, extra_params=dict(preload=True))
assert all(
[
ch1["unit"] == ch2["unit"]
Expand Down Expand Up @@ -3413,6 +3422,7 @@ def test_convert_eeg_formats(dir_name, format, fname, reader, tmp_path):
def test_format_conversion_overwrite(dir_name, format, fname, reader, tmp_path):
"""Test that overwrite works when format is passed to write_raw_bids."""
pytest.importorskip("pybv", PYBV_VERSION)
pytest.importorskip("eeglabio", EEGLABIO_VERSION)
bids_root = tmp_path / format
raw_fname = data_path / dir_name / fname

Expand All @@ -3421,7 +3431,7 @@ def test_format_conversion_overwrite(dir_name, format, fname, reader, tmp_path):

raw = reader(raw_fname)
# drop 'misc' type channels when exporting
raw = raw.pick_types(eeg=True)
raw = raw.pick(["eeg"])
kwargs = dict(raw=raw, format=format, bids_path=bids_path, verbose=False)

with warnings.catch_warnings():
Expand Down
34 changes: 29 additions & 5 deletions mne_bids/write.py
Expand Up @@ -1156,6 +1156,22 @@ def _write_raw_edf(raw, bids_fname, overwrite):
raw.export(bids_fname, overwrite=overwrite)


def _write_raw_eeglab(raw, bids_fname, overwrite):
"""Store data as EEGLAB.
Parameters
----------
raw : mne.io.Raw
Raw data to save.
bids_fname : str
The output filename.
overwrite : bool
Whether to overwrite an existing file or not.
"""
assert str(bids_fname).endswith(".set")
raw.export(bids_fname, overwrite=overwrite)


@verbose
def make_dataset_description(
*,
Expand Down Expand Up @@ -1467,14 +1483,14 @@ def write_raw_bids(
``source`` column of ``scans.tsv``. By default, this information
is not stored.
format : 'auto' | 'BrainVision' | 'EDF' | 'FIF'
format : 'auto' | 'BrainVision' | 'EDF' | 'FIF' | 'EEGLAB'
Controls the file format of the data after BIDS conversion. If
``'auto'``, MNE-BIDS will attempt to convert the input data to BIDS
without a change of the original file format. A conversion to a
different file format (BrainVision, EDF, or FIF) will only take place
when the original file format lacks some necessary features. Conversion
can be forced to BrainVision or EDF for (i)EEG, and to FIF for MEG
data.
different file format will then only take place if the original file
format lacks some necessary features.
Conversion may be forced to BrainVision, EDF, or EEGLAB for (i)EEG,
and to FIF for MEG data.
symlink : bool
Instead of copying the source files, only create symbolic links to
preserve storage space. This is only allowed when not anonymizing the
Expand Down Expand Up @@ -1716,6 +1732,8 @@ def write_raw_bids(
ext = ".vhdr"
elif format == "EDF":
ext = ".edf"
elif format == "EEGLAB":
ext = ".set"
elif format == "FIF":
ext = ".fif"
else:
Expand Down Expand Up @@ -2029,6 +2047,9 @@ def write_raw_bids(
elif format == "EDF" and bids_path.datatype in ["ieeg", "eeg"]:
convert = True
bids_path.update(extension=".edf")
elif format == "EEGLAB" and bids_path.datatype in ["ieeg", "eeg"]:
convert = True
bids_path.update(extension=".set")
elif format == "FIF" and bids_path.datatype == "meg":
convert = True
bids_path.update(extension=".fif")
Expand Down Expand Up @@ -2089,6 +2110,9 @@ def write_raw_bids(
elif bids_path.datatype in ["eeg", "ieeg"] and format == "EDF":
warn("Converting data files to EDF format")
_write_raw_edf(raw, bids_path.fpath, overwrite=overwrite)
elif bids_path.datatype in ["eeg", "ieeg"] and format == "EEGLAB":
warn("Converting data files to EEGLAB format")
_write_raw_eeglab(raw, bids_path.fpath, overwrite=overwrite)
else:
warn("Converting data files to BrainVision format")
bids_path.update(suffix=bids_path.datatype, extension=".vhdr")
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -44,6 +44,7 @@ include_package_data = True
full =
nibabel >= 3.2.1
pybv >= 0.7.5
eeglabio >= 0.0.2
pymatreader >= 0.0.30
matplotlib >= 3.4.0
pandas >= 1.2.4
Expand Down
1 change: 1 addition & 0 deletions test_requirements.txt
Expand Up @@ -3,6 +3,7 @@ numpy>=1.20.2
scipy>=1.6.3
nibabel>=3.2.1
pybv>=0.7.5
eeglabio>=0.0.2
pymatreader>=0.0.30
matplotlib>=3.4.0
pandas>=1.2.4
Expand Down

0 comments on commit 7708a3f

Please sign in to comment.