Skip to content
Open
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/14004.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When creating :class:`mne.Epochs`, a warning is now emitted if any ``events`` have sample numbers outside the recorded data, since the corresponding epochs are otherwise silently dropped; this is configurable via the new ``on_outside`` parameter (``'raise'`` | ``'warn'`` | ``'ignore'``), by `Cedric Conday`_.
49 changes: 49 additions & 0 deletions mne/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,13 @@ class BaseEpochs(

.. versionadded:: 0.16
%(event_repeated_epochs)s
on_outside : 'raise' | 'warn' | 'ignore'
What to do if an event's sample number falls outside the recorded data
range. Such events yield no epoch and are dropped. Can be ``'raise'``
to raise an error, ``'warn'`` (default) to emit a warning, or
``'ignore'`` to do nothing.

.. versionadded:: 1.13
%(raw_sfreq)s
annotations : instance of mne.Annotations | None
Annotations to set.
Expand Down Expand Up @@ -462,6 +469,7 @@ def __init__(
filename=None,
metadata=None,
event_repeated="error",
on_outside="warn",
*,
raw_sfreq=None,
annotations=None,
Expand Down Expand Up @@ -575,6 +583,7 @@ def __init__(
self.detrend = detrend

self._raw = raw
self._oob_check(on_outside)
info._check_consistency()
self.picks = _picks_to_idx(
info, picks, none="all", exclude=(), allow_empty=False
Expand Down Expand Up @@ -691,6 +700,27 @@ def __init__(
self._check_consistency()
self.set_annotations(annotations, on_missing="ignore")

def _oob_check(self, on_outside):
"""Warn when event sample numbers fall outside recorded data (gh-12989)."""
if (
self._raw is not None
and len(self.events) > 0
and hasattr(self._raw, "first_samp")
):
lo = self._raw.first_samp
hi = lo + self._raw.n_times
n_oob = int(((self.events[:, 0] < lo) | (self.events[:, 0] >= hi)).sum())
if n_oob:
_on_missing(
on_outside,
f"{n_oob} event{_pl(n_oob)} {'has' if n_oob == 1 else 'have'} a "
"sample number outside the recorded data; the corresponding "
f"epoch{_pl(n_oob)} will be dropped. This can happen if the "
"events were created at a different sampling frequency, or "
"contain sample numbers before first_samp.",
name="on_outside",
)

def _check_consistency(self):
"""Check invariants of epochs object."""
if hasattr(self, "events"):
Expand Down Expand Up @@ -3487,6 +3517,13 @@ class Epochs(BaseEpochs):

.. versionadded:: 0.16
%(event_repeated_epochs)s
on_outside : 'raise' | 'warn' | 'ignore'
What to do if an event's sample number falls outside the recorded data
range. Such events yield no epoch and are dropped. Can be ``'raise'``
to raise an error, ``'warn'`` (default) to emit a warning, or
``'ignore'`` to do nothing.

.. versionadded:: 1.13
%(verbose)s

Attributes
Expand Down Expand Up @@ -3587,6 +3624,8 @@ def __init__(
reject_by_annotation=True,
metadata=None,
event_repeated="error",
*,
on_outside="warn",
verbose=None,
Comment on lines 3626 to 3629

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
event_repeated="error",
on_outside="warn",
verbose=None,
event_repeated="error",
*,
on_outside="warn",
verbose=None,

Should make sure this new param is kwarg only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise, when the param is added to EpochsArray, it should be as kwarg-only (like some others there).

):
from .io import BaseRaw
Expand Down Expand Up @@ -3646,6 +3685,7 @@ def __init__(
on_missing=on_missing,
preload_at_end=preload,
event_repeated=event_repeated,
on_outside=on_outside,
verbose=verbose,
raw_sfreq=raw_sfreq,
annotations=annotations,
Expand Down Expand Up @@ -3728,6 +3768,13 @@ class EpochsArray(BaseEpochs):

.. versionadded:: 0.16
%(selection)s
on_outside : 'raise' | 'warn' | 'ignore'
What to do if an event's sample number falls outside the recorded data
range. Such events yield no epoch and are dropped. Can be ``'raise'``
to raise an error, ``'warn'`` (default) to emit a warning, or
``'ignore'`` to do nothing.

.. versionadded:: 1.13
%(drop_log)s

.. versionadded:: 1.3
Expand Down Expand Up @@ -3777,6 +3824,7 @@ def __init__(
metadata=None,
selection=None,
*,
on_outside="warn",
drop_log=None,
raw_sfreq=None,
verbose=None,
Expand Down Expand Up @@ -3813,6 +3861,7 @@ def __init__(
selection=selection,
proj=proj,
on_missing=on_missing,
on_outside=on_outside,
drop_log=drop_log,
raw_sfreq=raw_sfreq,
verbose=verbose,
Expand Down
5 changes: 4 additions & 1 deletion mne/preprocessing/tests/test_ica.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ def test_warnings():
"""Test that ICA warns on certain input data conditions."""
raw = read_raw_fif(raw_fname).crop(0, 5).load_data()
events = read_events(event_name)
epochs = Epochs(raw, events=events, baseline=None, preload=True)
epochs = Epochs(
raw, events=events, baseline=None, preload=True, on_outside="ignore"
)
ica = ICA(n_components=2, max_iter=1, method="infomax", random_state=0)

# not high-passed
Expand Down Expand Up @@ -1348,6 +1350,7 @@ def test_eog_channel(method):
baseline=None,
preload=True,
proj=False,
on_outside="ignore",
)
n_components = 0.9
ica = ICA(n_components=n_components, method=method)
Expand Down
23 changes: 23 additions & 0 deletions mne/tests/test_epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Copyright the MNE-Python contributors.

import pickle
import warnings
from copy import deepcopy
from datetime import timedelta
from functools import partial
Expand Down Expand Up @@ -5278,3 +5279,25 @@ def test_empty_error(method, epochs_empty):
pytest.importorskip("pandas")
with pytest.raises(RuntimeError, match="is empty."):
getattr(epochs_empty.copy(), method[0])(**method[1])


def test_epochs_warn_out_of_bounds_events():
"""Warn when event sample numbers fall outside the recorded data (gh-12989)."""
sfreq = 100.0
info = create_info(3, sfreq, "eeg")
raw = RawArray(np.random.default_rng(0).standard_normal((3, 1000)), info)
oob = np.array([[2000, 0, 1]])
# event sample 2000 is past the 1000-sample recording -> warn (default)
with pytest.warns(RuntimeWarning, match="outside the recorded data"):
mne.Epochs(raw, oob, tmin=-0.2, tmax=0.5, baseline=None)
# on_outside="raise" turns it into an error
with pytest.raises(ValueError, match="outside the recorded data"):
mne.Epochs(raw, oob, tmin=-0.2, tmax=0.5, baseline=None, on_outside="raise")
# on_outside="ignore" stays silent
with warnings.catch_warnings():
warnings.simplefilter("error")
mne.Epochs(raw, oob, tmin=-0.2, tmax=0.5, baseline=None, on_outside="ignore")
# an in-range event whose epoch window merely clips the edge must NOT warn
with warnings.catch_warnings():
warnings.simplefilter("error")
mne.Epochs(raw, np.array([[990, 0, 1]]), tmin=0, tmax=0.5, baseline=None)
Comment on lines +5297 to +5303

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warnings count as errors in our tests, so we don't need to catch them.

Loading