diff --git a/doc/changes/dev/14163.bugfix.rst b/doc/changes/dev/14163.bugfix.rst new file mode 100644 index 00000000000..dcef83de471 --- /dev/null +++ b/doc/changes/dev/14163.bugfix.rst @@ -0,0 +1 @@ +Allow :func:`mne.read_epochs_eeglab` to read epoched ``.set`` files without event information, by `Daria Agafonova`_. diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index d975175f984..93137388526 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -662,49 +662,65 @@ def __init__( event_name, event_latencies, unique_ev = list(), list(), list() ev_idx = 0 warn_multiple_events = False - epochs = _bunchify(eeg.epoch) - events = _bunchify(eeg.event) - for ep in epochs: - if isinstance(ep.eventtype, int | float): - ep.eventtype = str(ep.eventtype) - if not isinstance(ep.eventtype, str): - event_type = "/".join([str(et) for et in ep.eventtype]) - event_name.append(event_type) - # store latency of only first event - # -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 - 1) - ev_idx += 1 - - if event_type not in unique_ev: - unique_ev.append(event_type) - - # invent event dict but use id > 0 so you know its a trigger - event_id = {ev: idx + 1 for idx, ev in enumerate(unique_ev)} - - # warn about multiple events in epoch if necessary - if warn_multiple_events: + epochs = _bunchify(eeg.get("epoch", [])) + eeg_events = _bunchify(eeg.get("event", [])) + if len(epochs) == 0 or len(eeg_events) == 0: warn( - "At least one epoch has multiple events. Only the latency" - " of the first event will be retained." + "The EEGLAB file contains no event information. All epochs " + "will be assigned to a single 'unknown' event." ) + event_id = {"unknown": 1} + events = np.column_stack( + ( + np.arange(eeg.trials), + np.zeros(eeg.trials, dtype=int), + np.ones(eeg.trials, dtype=int), + ) + ) + else: + for ep in epochs: + if isinstance(ep.eventtype, int | float): + ep.eventtype = str(ep.eventtype) + if not isinstance(ep.eventtype, str): + event_type = "/".join([str(et) for et in ep.eventtype]) + event_name.append(event_type) + # store latency of only first event + # -1 to account for Matlab 1-based indexing of samples + event_latencies.append(eeg_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(eeg_events[ev_idx].latency - 1) + ev_idx += 1 + + if event_type not in unique_ev: + unique_ev.append(event_type) + + # invent event dict but use id > 0 so you know its a trigger + event_id = {ev: idx + 1 for idx, ev in enumerate(unique_ev)} + + # warn about multiple events in epoch if necessary + if warn_multiple_events: + warn( + "At least one epoch has multiple events. Only the latency" + " of the first event will be retained." + ) - # now fill up the event array - events = np.zeros((eeg.trials, 3), dtype=int) - assert event_id is not None - for idx in range(0, eeg.trials): - if idx == 0: - prev_stim = 0 - elif idx > 0 and event_latencies[idx] - event_latencies[idx - 1] == 1: - prev_stim = event_id[event_name[idx - 1]] - events[idx, 0] = event_latencies[idx] - events[idx, 1] = prev_stim - events[idx, 2] = event_id[event_name[idx]] + # now fill up the event array + events = np.zeros((eeg.trials, 3), dtype=int) + assert event_id is not None + for idx in range(0, eeg.trials): + if idx == 0: + prev_stim = 0 + elif ( + idx > 0 and event_latencies[idx] - event_latencies[idx - 1] == 1 + ): + prev_stim = event_id[event_name[idx - 1]] + events[idx, 0] = event_latencies[idx] + events[idx, 1] = prev_stim + events[idx, 2] = event_id[event_name[idx]] elif isinstance(events, str | Path | PathLike): events = read_events(events) diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py index fb58c9c3b51..8435b0386e7 100644 --- a/mne/io/eeglab/tests/test_eeglab.py +++ b/mne/io/eeglab/tests/test_eeglab.py @@ -394,6 +394,45 @@ def test_io_set_epochs_events(tmp_path): pytest.raises(ValueError, read_epochs_eeglab, epochs_fname_mat, epochs.events, None) +@pytest.mark.parametrize("include_event_fields", (True, False)) +def test_io_set_epochs_without_events(tmp_path, include_event_fields): + """Read epoched EEGLAB files that have no event information.""" + n_epochs, n_channels, n_times = 3, 2, 20 + data = np.arange(n_channels * n_times * n_epochs, dtype=float).reshape( + n_channels, n_times, n_epochs + ) + fname = tmp_path / "no-events.set" + eeg = { + "trials": n_epochs, + "nbchan": n_channels, + "pnts": n_times, + "srate": 100.0, + "xmin": -0.1, + "xmax": 0.09, + "data": data, + "chanlocs": np.array( + [{"labels": "EEG 001"}, {"labels": "EEG 002"}], dtype=object + ), + } + if include_event_fields: + eeg.update(epoch=np.array([], dtype=object), event=np.array([], dtype=object)) + io.savemat(fname, {"EEG": eeg}, appendmat=False) + + with pytest.warns(RuntimeWarning, match="contains no event information"): + epochs = read_epochs_eeglab(fname) + + expected_events = np.column_stack( + ( + np.arange(n_epochs), + np.zeros(n_epochs, dtype=int), + np.ones(n_epochs, dtype=int), + ) + ) + assert epochs.event_id == {"unknown": 1} + assert_array_equal(epochs.events, expected_events) + assert_allclose(epochs.get_data(copy=False), data.transpose(2, 0, 1) * 1e-6) + + @testing.requires_testing_data @pytest.mark.filterwarnings("ignore:At least one epoch has multiple events") @pytest.mark.filterwarnings("ignore:The data contains 'boundary' events")