diff --git a/doc/changes/devel/12324.bugfix.rst b/doc/changes/devel/12324.bugfix.rst new file mode 100644 index 00000000000..ec7f2c5849d --- /dev/null +++ b/doc/changes/devel/12324.bugfix.rst @@ -0,0 +1 @@ +Add ``tol`` parameter to :meth:`mne.events_from_annotations` so that the user can specify the tolerance to ignore rounding errors of event onsets when using ``chunk_duration`` is not None (default is 1e-8), by `Michiru Kaneda`_ diff --git a/mne/annotations.py b/mne/annotations.py index f0f88783b68..a6be1f7a62d 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -1529,6 +1529,7 @@ def events_from_annotations( regexp=r"^(?![Bb][Aa][Dd]|[Ee][Dd][Gg][Ee]).*$", use_rounding=True, chunk_duration=None, + tol=1e-8, verbose=None, ): """Get :term:`events` and ``event_id`` from an Annotations object. @@ -1572,6 +1573,11 @@ def events_from_annotations( they fit within the annotation duration spaced according to ``chunk_duration``. As a consequence annotations with duration shorter than ``chunk_duration`` will not contribute events. + tol : float + The tolerance used to check if a chunk fits within an annotation when + ``chunk_duration`` is not ``None``. If the duration from a computed + chunk onset to the end of the annotation is smaller than + ``chunk_duration`` minus ``tol``, the onset will be discarded. %(verbose)s Returns @@ -1617,7 +1623,7 @@ def events_from_annotations( for annot in annotations[event_sel]: annot_offset = annot["onset"] + annot["duration"] _onsets = np.arange(annot["onset"], annot_offset, chunk_duration) - good_events = annot_offset - _onsets >= chunk_duration + good_events = annot_offset - _onsets >= chunk_duration - tol if good_events.any(): _onsets = _onsets[good_events] _inds = raw.time_as_index( diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 8be52b60a9d..4868f5dc5df 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -819,6 +819,49 @@ def test_events_from_annot_onset_alingment(): assert raw.first_samp == event_latencies[0, 0] +@pytest.mark.parametrize( + "use_rounding,tol,shape,onsets,descriptions", + [ + pytest.param(True, 0, (2, 3), [202, 402], [0, 2], id="rounding-notol"), + pytest.param(True, 1e-8, (3, 3), [202, 302, 402], [0, 1, 2], id="rounding-tol"), + pytest.param(False, 0, (2, 3), [202, 401], [0, 2], id="norounding-notol"), + pytest.param( + False, 1e-8, (3, 3), [202, 302, 401], [0, 1, 2], id="norounding-tol" + ), + pytest.param(None, None, (3, 3), [202, 302, 402], [0, 1, 2], id="default"), + ], +) +def test_events_from_annot_with_tolerance( + use_rounding, tol, shape, onsets, descriptions +): + """Test events_from_annotations w/ and w/o tolerance.""" + info = create_info(ch_names=1, sfreq=100) + raw = RawArray(data=np.empty((1, 1000)), info=info, first_samp=0) + meas_date = _handle_meas_date(0) + with raw.info._unlock(check_after=True): + raw.info["meas_date"] = meas_date + chunk_duration = 1 + annot = Annotations([2.02, 3.02, 4.02], chunk_duration, ["0", "1", "2"], 0) + raw.set_annotations(annot) + event_id = {"0": 0, "1": 1, "2": 2} + + if use_rounding is None: + events, _ = events_from_annotations( + raw, event_id=event_id, chunk_duration=chunk_duration + ) + else: + events, _ = events_from_annotations( + raw, + event_id=event_id, + chunk_duration=chunk_duration, + use_rounding=use_rounding, + tol=tol, + ) + assert events.shape == shape + assert (events[:, 0] == onsets).all() + assert (events[:, 2] == descriptions).all() + + def _create_annotation_based_on_descr( description, annotation_start_sampl=0, duration=0, orig_time=0 ):