From a9c163b490f6ae3b42eb8d3359cd705b0ffd0d6a Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Mon, 22 Sep 2025 16:57:34 -0700 Subject: [PATCH 01/15] FIX, TST, WIP: Try to get test_export_epochs_eeeglab passing again Needs https://github.com/jackz314/eeglabio/pull/18 --- mne/epochs.py | 13 +++++++++++-- mne/export/_eeglab.py | 10 ++++++++-- mne/export/_export.py | 6 ++++-- mne/export/tests/test_export.py | 10 ++++++++-- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index c042715e6ae..12dbf96eeab 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2326,7 +2326,9 @@ def save( return split_fnames @verbose - def export(self, fname, fmt="auto", *, overwrite=False, verbose=None): + def export( + self, fname, fmt="auto", *, export_kwargs=None, overwrite=False, verbose=None + ): """Export Epochs to external formats. %(export_fmt_support_epochs)s @@ -2351,7 +2353,14 @@ def export(self, fname, fmt="auto", *, overwrite=False, verbose=None): """ from .export import export_epochs - export_epochs(fname, self, fmt, overwrite=overwrite, verbose=verbose) + export_epochs( + fname, + self, + fmt, + export_kwargs=export_kwargs, + overwrite=overwrite, + verbose=verbose, + ) @fill_doc def equalize_event_counts( diff --git a/mne/export/_eeglab.py b/mne/export/_eeglab.py index 459207f0616..fe6d8108410 100644 --- a/mne/export/_eeglab.py +++ b/mne/export/_eeglab.py @@ -5,7 +5,7 @@ import numpy as np from ..annotations import _sync_onset -from ..utils import _check_eeglabio_installed +from ..utils import _check_eeglabio_installed, _validate_type _check_eeglabio_installed() import eeglabio.epochs # noqa: E402 @@ -45,8 +45,13 @@ def _export_raw(fname, raw): ) -def _export_epochs(fname, epochs): +def _export_epochs(fname, epochs, *, export_kwargs=None): _check_eeglabio_installed() + + if export_kwargs is None: + export_kwargs = {} + _validate_type(export_kwargs, dict, "export_kwargs") + # load data first epochs.load_data() @@ -75,6 +80,7 @@ def _export_epochs(fname, epochs): event_id=epochs.event_id, ch_locs=cart_coords, annotations=annot, + **export_kwargs, ) diff --git a/mne/export/_export.py b/mne/export/_export.py index 4b93fda917e..b8610e1bf73 100644 --- a/mne/export/_export.py +++ b/mne/export/_export.py @@ -88,7 +88,9 @@ def export_raw( @verbose -def export_epochs(fname, epochs, fmt="auto", *, overwrite=False, verbose=None): +def export_epochs( + fname, epochs, fmt="auto", *, export_kwargs=None, overwrite=False, verbose=None +): """Export Epochs to external formats. %(export_fmt_support_epochs)s @@ -129,7 +131,7 @@ def export_epochs(fname, epochs, fmt="auto", *, overwrite=False, verbose=None): if fmt == "eeglab": from ._eeglab import _export_epochs - _export_epochs(fname, epochs) + _export_epochs(fname, epochs, export_kwargs=export_kwargs) @verbose diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 6f712923c7d..d7b7541e90a 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -534,12 +534,12 @@ def test_export_raw_edf_does_not_fail_on_empty_header_fields(tmp_path): raw.export(tmp_path / "test.edf", add_ch_type=True) -@pytest.mark.xfail(reason="eeglabio (usage?) bugs that should be fixed") @pytest.mark.parametrize("preload", (True, False)) def test_export_epochs_eeglab(tmp_path, preload): """Test saving an Epochs instance to EEGLAB's set format.""" eeglabio = pytest.importorskip("eeglabio") raw, events = _get_data()[:2] + raw = raw.copy().resample(600) raw.load_data() epochs = Epochs(raw, events, preload=preload) temp_fname = tmp_path / "test.set" @@ -549,7 +549,13 @@ def test_export_epochs_eeglab(tmp_path, preload): else: ctx = nullcontext with ctx(): - epochs.export(temp_fname) + epochs.export( + temp_fname, + export_kwargs={ + "first_samp": raw.first_samp, + "n_trials": len(epochs.drop_log), + }, + ) epochs.drop_channels([ch for ch in ["epoc", "STI 014"] if ch in epochs.ch_names]) epochs_read = read_epochs_eeglab(temp_fname, verbose="error") # head radius assert epochs.ch_names == epochs_read.ch_names From 5ed2655b7adfc85c9444adc05d36969dfd2741aa Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 08:44:19 -0700 Subject: [PATCH 02/15] WIP, FIX: refresh events --- mne/export/tests/test_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index d7b7541e90a..0e1e48afa49 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -539,7 +539,7 @@ def test_export_epochs_eeglab(tmp_path, preload): """Test saving an Epochs instance to EEGLAB's set format.""" eeglabio = pytest.importorskip("eeglabio") raw, events = _get_data()[:2] - raw = raw.copy().resample(600) + raw, events = raw.copy().resample(600, events=events) raw.load_data() epochs = Epochs(raw, events, preload=preload) temp_fname = tmp_path / "test.set" From 40b464baf2ec46cefeb5d3260d5437642b8272e7 Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 08:47:00 -0700 Subject: [PATCH 03/15] DOC: changelog --- doc/changes/dev/134238.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/dev/134238.bugfix.rst diff --git a/doc/changes/dev/134238.bugfix.rst b/doc/changes/dev/134238.bugfix.rst new file mode 100644 index 00000000000..6f050017c62 --- /dev/null +++ b/doc/changes/dev/134238.bugfix.rst @@ -0,0 +1 @@ +Allow `first_samp` and true number of events (before dropping epochs) to be considered when exporting to EEGLAB format. by `Scott Huberty`_ \ No newline at end of file From 1bd1ad63df5cd91869dc356c48e6d8127d0752e7 Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 09:14:19 -0700 Subject: [PATCH 04/15] TMP: fix eeglabio install to my fork/branch with fix --- environment.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 30036dc8187..5f916bec670 100644 --- a/environment.yml +++ b/environment.yml @@ -10,7 +10,6 @@ dependencies: - defusedxml - dipy - edfio >=0.2.1 - - eeglabio - filelock >=3.18.0 - h5io >=0.2.4 - h5py @@ -62,3 +61,5 @@ dependencies: - trame-vuetify - vtk >=9.2 - xlrd + - pip: + - eeglabio @ git+https://github.com/scott-huberty/eeglabio.git@export_epochs From 0ea034500ad3fa6bb9156726f83693642a120827 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:14:43 +0000 Subject: [PATCH 05/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- environment.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 5f916bec670..30036dc8187 100644 --- a/environment.yml +++ b/environment.yml @@ -10,6 +10,7 @@ dependencies: - defusedxml - dipy - edfio >=0.2.1 + - eeglabio - filelock >=3.18.0 - h5io >=0.2.4 - h5py @@ -61,5 +62,3 @@ dependencies: - trame-vuetify - vtk >=9.2 - xlrd - - pip: - - eeglabio @ git+https://github.com/scott-huberty/eeglabio.git@export_epochs From 51cff197e30d662548412ba0456d02834fdaa4db Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 09:35:57 -0700 Subject: [PATCH 06/15] workaround to install eeglabio dev branch.. --- tools/github_actions_dependencies.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 70fe84509ab..a543161d279 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -34,4 +34,9 @@ echo "" echo "::group::Installing test dependencies using pip" python -m pip install $STD_ARGS $INSTALL_ARGS .[$INSTALL_KIND] -echo "::endgroup::" +# TODO: Temporary work around to help us confirm this PR's fix. Remove before merging. +# Installs branch used in https://github.com/jackz314/eeglabio/pull/18 +if [[ "$MNE_CI_KIND" == "conda" || "$MNE_CI_KIND" == "mamba" ]]; then + python -m pip install $STD_ARGS "eeglabio @ git+https://github.com/scott-huberty/eeglabio.git@export_epochs" +fi +echo "::endgroup::" \ No newline at end of file From 3d502a326f42331a33816e52faa5aff37f4d78be Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 09:38:49 -0700 Subject: [PATCH 07/15] DOC, FIX: file name.. --- doc/changes/dev/{134238.bugfix.rst => 13428.bugfix.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changes/dev/{134238.bugfix.rst => 13428.bugfix.rst} (100%) diff --git a/doc/changes/dev/134238.bugfix.rst b/doc/changes/dev/13428.bugfix.rst similarity index 100% rename from doc/changes/dev/134238.bugfix.rst rename to doc/changes/dev/13428.bugfix.rst From ab4cc8d32988cea7227b9694d65653fc5e61f245 Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 16:22:14 -0700 Subject: [PATCH 08/15] Manual revert of "FIX, TST, WIP: Try to get test_export_epochs_eeeglab passing again" AND "WIP, FIX: refresh events" Manual revert of commits 5ed2655b7adfc85c9444adc05d36969dfd2741aa a9c163b490f6ae3b42eb8d3359cd705b0ffd0d6a Taking a second stab at this. We might not need to thread any kwargs to eeglabio from the public API after all... --- mne/epochs.py | 13 ++----------- mne/export/_eeglab.py | 9 ++------- mne/export/_export.py | 2 +- mne/export/tests/test_export.py | 9 +-------- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 12dbf96eeab..c042715e6ae 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2326,9 +2326,7 @@ def save( return split_fnames @verbose - def export( - self, fname, fmt="auto", *, export_kwargs=None, overwrite=False, verbose=None - ): + def export(self, fname, fmt="auto", *, overwrite=False, verbose=None): """Export Epochs to external formats. %(export_fmt_support_epochs)s @@ -2353,14 +2351,7 @@ def export( """ from .export import export_epochs - export_epochs( - fname, - self, - fmt, - export_kwargs=export_kwargs, - overwrite=overwrite, - verbose=verbose, - ) + export_epochs(fname, self, fmt, overwrite=overwrite, verbose=verbose) @fill_doc def equalize_event_counts( diff --git a/mne/export/_eeglab.py b/mne/export/_eeglab.py index fe6d8108410..3bebacd34b8 100644 --- a/mne/export/_eeglab.py +++ b/mne/export/_eeglab.py @@ -5,7 +5,7 @@ import numpy as np from ..annotations import _sync_onset -from ..utils import _check_eeglabio_installed, _validate_type +from ..utils import _check_eeglabio_installed _check_eeglabio_installed() import eeglabio.epochs # noqa: E402 @@ -45,13 +45,9 @@ def _export_raw(fname, raw): ) -def _export_epochs(fname, epochs, *, export_kwargs=None): +def _export_epochs(fname, epochs): _check_eeglabio_installed() - if export_kwargs is None: - export_kwargs = {} - _validate_type(export_kwargs, dict, "export_kwargs") - # load data first epochs.load_data() @@ -80,7 +76,6 @@ def _export_epochs(fname, epochs, *, export_kwargs=None): event_id=epochs.event_id, ch_locs=cart_coords, annotations=annot, - **export_kwargs, ) diff --git a/mne/export/_export.py b/mne/export/_export.py index b8610e1bf73..28ba9e9439f 100644 --- a/mne/export/_export.py +++ b/mne/export/_export.py @@ -131,7 +131,7 @@ def export_epochs( if fmt == "eeglab": from ._eeglab import _export_epochs - _export_epochs(fname, epochs, export_kwargs=export_kwargs) + _export_epochs(fname, epochs) @verbose diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 0e1e48afa49..be38d8c28bf 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -539,7 +539,6 @@ def test_export_epochs_eeglab(tmp_path, preload): """Test saving an Epochs instance to EEGLAB's set format.""" eeglabio = pytest.importorskip("eeglabio") raw, events = _get_data()[:2] - raw, events = raw.copy().resample(600, events=events) raw.load_data() epochs = Epochs(raw, events, preload=preload) temp_fname = tmp_path / "test.set" @@ -549,13 +548,7 @@ def test_export_epochs_eeglab(tmp_path, preload): else: ctx = nullcontext with ctx(): - epochs.export( - temp_fname, - export_kwargs={ - "first_samp": raw.first_samp, - "n_trials": len(epochs.drop_log), - }, - ) + 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, verbose="error") # head radius assert epochs.ch_names == epochs_read.ch_names From ac0ece9a5bdc69295fa09710bca4ab4e4afdf957 Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 16:28:11 -0700 Subject: [PATCH 09/15] FIX: Adjust for MATLAB's 1-based indexing of sample number If https://github.com/jackz314/eeglabio/blob/f496ba45b5db716360e05c24efd89339f7bd2434/eeglabio/epochs.py#L101 is Correct, then MATLAB stores sample number starting at 1 and not at 0 --- mne/io/eeglab/eeglab.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index b5b57588acd..83148666ffa 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -631,13 +631,14 @@ def __init__( event_type = "/".join([str(et) for et in ep.eventtype]) event_name.append(event_type) # store latency of only first event - event_latencies.append(events[ev_idx].latency) + # -1 to account for Matlab 1-based indexing of samples + event_latencies.append(events[ev_idx].latency - 1) ev_idx += len(ep.eventtype) warn_multiple_events = True else: event_type = ep.eventtype event_name.append(ep.eventtype) - event_latencies.append(events[ev_idx].latency) + event_latencies.append(events[ev_idx].latency - 1) ev_idx += 1 if event_type not in unique_ev: From 2caa3883e21a42d5dba1802d2fedf6aedf6fff69 Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 16:30:50 -0700 Subject: [PATCH 10/15] Take 2: Explicitly pass in epoch indices to eeglabio Hand in Hand with https://github.com/jackz314/eeglabio/pull/18 I am not sure if this is the right way to go but at least our test will pass now.. --- mne/export/_eeglab.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/export/_eeglab.py b/mne/export/_eeglab.py index 3bebacd34b8..2fd1b69914e 100644 --- a/mne/export/_eeglab.py +++ b/mne/export/_eeglab.py @@ -76,6 +76,7 @@ def _export_epochs(fname, epochs): event_id=epochs.event_id, ch_locs=cart_coords, annotations=annot, + epoch_indices=epochs.selection, ) From 14df67b84fa483b0122cd40a7aa90cc9f6d33a89 Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 16:36:19 -0700 Subject: [PATCH 11/15] remove cruft newline --- mne/export/_eeglab.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/export/_eeglab.py b/mne/export/_eeglab.py index 2fd1b69914e..17d9020ad76 100644 --- a/mne/export/_eeglab.py +++ b/mne/export/_eeglab.py @@ -47,7 +47,6 @@ def _export_raw(fname, raw): def _export_epochs(fname, epochs): _check_eeglabio_installed() - # load data first epochs.load_data() From 38191cbe2a9e15ef89e26ca9009d195b5504adc8 Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Tue, 23 Sep 2025 16:38:45 -0700 Subject: [PATCH 12/15] remove stale export_kwargs param --- mne/export/_export.py | 4 +--- tools/github_actions_dependencies.sh | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mne/export/_export.py b/mne/export/_export.py index 28ba9e9439f..4b93fda917e 100644 --- a/mne/export/_export.py +++ b/mne/export/_export.py @@ -88,9 +88,7 @@ def export_raw( @verbose -def export_epochs( - fname, epochs, fmt="auto", *, export_kwargs=None, overwrite=False, verbose=None -): +def export_epochs(fname, epochs, fmt="auto", *, overwrite=False, verbose=None): """Export Epochs to external formats. %(export_fmt_support_epochs)s diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index a543161d279..74f0bd128af 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -39,4 +39,4 @@ python -m pip install $STD_ARGS $INSTALL_ARGS .[$INSTALL_KIND] if [[ "$MNE_CI_KIND" == "conda" || "$MNE_CI_KIND" == "mamba" ]]; then python -m pip install $STD_ARGS "eeglabio @ git+https://github.com/scott-huberty/eeglabio.git@export_epochs" fi -echo "::endgroup::" \ No newline at end of file +echo "::endgroup::" From 39e52a3980132fd249eeaaa56e5508a71d252b4d Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Wed, 24 Sep 2025 13:42:12 -0700 Subject: [PATCH 13/15] Incorporate @larsoner suggestions Co-authored-by: Eric Larson --- mne/export/_eeglab.py | 9 ++++++++- mne/export/tests/test_export.py | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mne/export/_eeglab.py b/mne/export/_eeglab.py index 17d9020ad76..c73243b0311 100644 --- a/mne/export/_eeglab.py +++ b/mne/export/_eeglab.py @@ -2,6 +2,8 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +from inspect import getfullargspec + import numpy as np from ..annotations import _sync_onset @@ -64,6 +66,11 @@ def _export_epochs(fname, epochs): else: annot = None + # https://github.com/jackz314/eeglabio/pull/18 + kwargs = dict() + if "epoch_indices" in getfullargspec(eeglabio.epochs.export_set).kwonlyargs: + kwargs["epoch_indices"] = epochs.selection + eeglabio.epochs.export_set( fname, data=epochs.get_data(picks=ch_names), @@ -75,7 +82,7 @@ def _export_epochs(fname, epochs): event_id=epochs.event_id, ch_locs=cart_coords, annotations=annot, - epoch_indices=epochs.selection, + **kwargs, ) diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index be38d8c28bf..743491f26c9 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -35,6 +35,7 @@ _check_edfio_installed, _record_warnings, _resource_path, + check_version, object_diff, ) @@ -534,6 +535,7 @@ def test_export_raw_edf_does_not_fail_on_empty_header_fields(tmp_path): raw.export(tmp_path / "test.edf", add_ch_type=True) +@pytest.mark.skipif(not check_version("eeglabio", "0.1.2"), reason="fixed by 0.1.2") @pytest.mark.parametrize("preload", (True, False)) def test_export_epochs_eeglab(tmp_path, preload): """Test saving an Epochs instance to EEGLAB's set format.""" From d72b8efafa6398e9cee8808803a5b3ccbd769fb0 Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Wed, 24 Sep 2025 13:46:12 -0700 Subject: [PATCH 14/15] Revert "workaround to install eeglabio dev branch.." This reverts commit 51cff197e30d662548412ba0456d02834fdaa4db. --- tools/github_actions_dependencies.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 74f0bd128af..70fe84509ab 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -34,9 +34,4 @@ echo "" echo "::group::Installing test dependencies using pip" python -m pip install $STD_ARGS $INSTALL_ARGS .[$INSTALL_KIND] -# TODO: Temporary work around to help us confirm this PR's fix. Remove before merging. -# Installs branch used in https://github.com/jackz314/eeglabio/pull/18 -if [[ "$MNE_CI_KIND" == "conda" || "$MNE_CI_KIND" == "mamba" ]]; then - python -m pip install $STD_ARGS "eeglabio @ git+https://github.com/scott-huberty/eeglabio.git@export_epochs" -fi echo "::endgroup::" From 80136c58fb47792c6d36d2866c0cec03456c6b57 Mon Sep 17 00:00:00 2001 From: Scott Huberty Date: Wed, 24 Sep 2025 14:03:41 -0700 Subject: [PATCH 15/15] DOC: update changelog description --- doc/changes/dev/13428.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/13428.bugfix.rst b/doc/changes/dev/13428.bugfix.rst index 6f050017c62..513bbcf697b 100644 --- a/doc/changes/dev/13428.bugfix.rst +++ b/doc/changes/dev/13428.bugfix.rst @@ -1 +1 @@ -Allow `first_samp` and true number of events (before dropping epochs) to be considered when exporting to EEGLAB format. by `Scott Huberty`_ \ No newline at end of file +Preserve event-to-epoch mapping when exporting EEGLAB .set files by `Scott Huberty`_ \ No newline at end of file