Repository: mne-tools/mne-bids

Issue:
Export to EEGLAB format?
I try to write a raw object using 
```
raw = mne.io.read_raw_eeglab(input_fname=file, preload=False, verbose=True)
write_raw_bids(raw, bids_basename, allow_preload=False, overwrite=False, verbose=False)
```                 
and I got the following error: 
```
Cannot write file - The data has already been loaded from disk. To write it to BIDS, pass "allow_preload=True" and the "format" parameter.
```

When I set allow_preload to True, I then get
`For preloaded data, you must set the "format" parameter to one of: BrainVision, EDF, or FIF
`

The issue is I don't see an option of EEGLAB/set file. 

Is this a know issue? Am I doing something wrong? 



Discussion:
I don't think we support writing to EEGLAB. You can try to write to BrainVision instead 
what is the content of bids_basename and bids_basename.format ?

> Message ID: ***@***.***>
>

> For preloaded data, you must set the "format" parameter to one of: BrainVision, EDF, or FIF

You will have to pick one of those. No EEGLAB support so far. Sorry!
We can consider adding support for exporting to EEGLAB, using this library: https://github.com/jackz314/eeglabio
Thanks @sappelhoff  
Full support for .SET would help us make open-source BIDS converter tools like [EEG2BIDS](https://github.com/aces/eeg2bids) accessible for many more BIDS-compliant cases, especially for researchers looking to move away from manufacturer-driven data formats like BV. 

(Let us know if there's a separate issue to follow up on for this, my colleague @laemtl is raising this somewhat related also to #1151 )
> Full support for .SET would help us make open-source BIDS converter tools like [EEG2BIDS](https://github.com/aces/eeg2bids) accessible for many more BIDS-compliant cases, especially for researchers looking to move away from manufacturer-driven data formats like BV.

3 comments on this:

1. The EEGLAB data format is not "RECOMMENDED" under BIDS, it's merely "accepted" based on its wide distribution
2. it's a MATLAB file in disguise
3. The mne-bids devs (to my knowledge) don't work actively with the EEGLAB format themselves

that maybe explains why so far there have been only little efforts to support this file format.

Having that said, we'd welcome contributions in the form of PRs (perhaps by @laemtl) to allow export to EEGLAB.

What do you think @hoechenberger @agramfort ?
Isn't Matlab simply HDF5 these days? Hence I don't see why (modern) EEGLAB shouldn't be fully supported by BIDS (and hence MNE-BIDS)
@christinerogers @laemtl @jeffersoncasimir

Please see this issue as a list of To Dos as to improve EEGLAB functionality of mne-bids: 

- https://github.com/mne-tools/mne-bids/issues/654

We'd be happy to review PRs to improve the state of the art. And perhaps working on these "smaller" items can be done in parallel to the present issue of also **exporting** to the EEGLAB format.


FYI, I have created a project board for all EEGLAB related issues: https://github.com/mne-tools/mne-bids/projects/10#card-89935503


Resolving patch:
diff --git a/mne_bids/config.py b/mne_bids/config.py
--- a/mne_bids/config.py
+++ b/mne_bids/config.py
@@ -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"""
 
diff --git a/mne_bids/write.py b/mne_bids/write.py
--- a/mne_bids/write.py
+++ b/mne_bids/write.py
@@ -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(
     *,
@@ -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
@@ -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:
@@ -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")
@@ -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")


Test patch:
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -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__)'
diff --git a/mne_bids/tests/test_dig.py b/mne_bids/tests/test_dig.py
--- a/mne_bids/tests/test_dig.py
+++ b/mne_bids/tests/test_dig.py
@@ -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})
 
@@ -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()
@@ -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={
@@ -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)
diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py
--- a/mne_bids/tests/test_write.py
+++ b/mne_bids/tests/test_write.py
@@ -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"
@@ -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",
@@ -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)
@@ -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
 
@@ -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
     )
@@ -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"]
@@ -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
 
@@ -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():
diff --git a/test_requirements.txt b/test_requirements.txt
--- a/test_requirements.txt
+++ b/test_requirements.txt
@@ -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