Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/dev/14163.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow :func:`mne.read_epochs_eeglab` to read epoched ``.set`` files without event information, by `Daria Agafonova`_.
96 changes: 56 additions & 40 deletions mne/io/eeglab/eeglab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
39 changes: 39 additions & 0 deletions mne/io/eeglab/tests/test_eeglab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading