Repository: mne-tools/mne-python

Issue:
`mne.events_from_annotations` returns events that barely overlap with annotation
I noticed that `mne.events_from_annotations` sometimes returns events that barely even overlap with an actual annotation when `chunk_duration` is used. I think this might have been the actual reason behind the issue https://github.com/mne-tools/mne-python/issues/5684#issuecomment-438211200.

The issue seems to arise when an annotation has a duration that is only slightly over `chunk_duration`. A first event will cover almost the entire annotation, but a second annotation will be created that might start within the annotation, but will barely overlap with it. Because of that, in some rare cases, the second event is identical to the next annotation - causing `mne.Epochs` to throw an error due to duplicate events.

#### Steps and/or code to reproduce

```Python
import mne
import numpy as np


# Create fake data with annotations
data = np.random.randn(2, 16000)
info = mne.create_info(
    ch_names=['EEG1', 'EEG2'], ch_types=['eeg', 'eeg'], sfreq=256)
raw = mne.io.RawArray(data, info)

onset = [0, 30]
duration = [30.1, 30]
description = ['0', '0']
annots = mne.Annotations(onset, duration, description)
raw = raw.set_annotations(annots)

# Extract epochs
events, event_ids = mne.events_from_annotations(raw, chunk_duration=30)
epochs = mne.Epochs(
    raw, events, tmin=0, tmax=30, baseline=None, on_missing='warning')
```

#### Expected results
I would have expected `events` to contain two events only: one for the first annotation [0, 30.1] and one for the second [30, 60]. An option would be to have an argument that lets the user decide whether events need to completely overlap with the annotation to be returned.

#### Actual results

`events` is:
```
array([[   0,    0,    1],
       [7680,    0,    1],
       [7680,    0,    1]])
```

Because of the duplicate row, `mne.Epochs` returns `RuntimeError: Event time samples were not unique`.

#### Additional information
```
Platform:      Linux-4.17.13-041713-generic-x86_64-with-debian-buster-sid
Python:        3.6.6 |Anaconda, Inc.| (default, Jun 28 2018, 17:14:51)  [GCC 7.2.0]
Executable:    /home/hubert/miniconda3/envs/dl-eeg/bin/python
CPU:           x86_64: 4 cores
Memory:        15.6 GB

mne:           0.18.dev0
numpy:         1.16.0 {blas=openblas, lapack=openblas}
scipy:         1.1.0
matplotlib:    2.2.3 {backend=TkAgg}

sklearn:       0.20.2
nibabel:       Not found
mayavi:        4.6.2 {qt_api=pyqt5, PyQt5=5.11.3}
cupy:          Not found
pandas:        0.23.4
dipy:          Not found
```



Discussion:
@massich can you have a look? thx
I see how that is counter-intuitive and even wrong when one uses the events for epoching using `chunk_duration` as epoch length. The general question is: what degree of error for the chunks do we allow by default. If none then some may also find it surprising that having an annotations of duration 29.9 and requesting chunk duration of 30. they get no events.
The simplest scenario is to add something like `min_chunk_duration` and after this:
https://github.com/mne-tools/mne-python/blob/master/mne/annotations.py#L870
check the last event distance from annotation onset + duration. This would have to be `<= min_chunk_duration` to be retained.


Resolving patch:
diff --git a/mne/annotations.py b/mne/annotations.py
--- a/mne/annotations.py
+++ b/mne/annotations.py
@@ -836,11 +836,12 @@ def events_from_annotations(raw, event_id=None, regexp=None, use_rounding=True,
         If True, use rounding (instead of truncation) when converting
         times to indices. This can help avoid non-unique indices.
     chunk_duration: float | None
-        If chunk_duration parameter in events_from_annotations is None, events
-        correspond to the annotation onsets.
+        Chunk duration in seconds. If ``chunk_duration`` is set to None
+        (default), generated events correspond to the annotation onsets.
         If not, :func:`mne.events_from_annotations` returns as many events as
         they fit within the annotation duration spaced according to
-        `chunk_duration`, which is given in seconds.
+        ``chunk_duration``. As a consequence annotations with duration shorter
+        than ``chunk_duration`` will not contribute events.
     %(verbose)s
 
     Returns
@@ -867,18 +868,21 @@ def events_from_annotations(raw, event_id=None, regexp=None, use_rounding=True,
     else:
         inds = values = np.array([]).astype(int)
         for annot in annotations[event_sel]:
-            _onsets = np.arange(start=annot['onset'],
-                                stop=(annot['onset'] + annot['duration']),
+            annot_offset = annot['onset'] + annot['duration']
+            _onsets = np.arange(start=annot['onset'], stop=annot_offset,
                                 step=chunk_duration)
-            _inds = raw.time_as_index(_onsets,
-                                      use_rounding=use_rounding,
-                                      origin=annotations.orig_time)
-            _inds += raw.first_samp
-            inds = np.append(inds, _inds)
-            _values = np.full(shape=len(_inds),
-                              fill_value=event_id_[annot['description']],
-                              dtype=int)
-            values = np.append(values, _values)
+            good_events = annot_offset - _onsets >= chunk_duration
+            if good_events.any():
+                _onsets = _onsets[good_events]
+                _inds = raw.time_as_index(_onsets,
+                                          use_rounding=use_rounding,
+                                          origin=annotations.orig_time)
+                _inds += raw.first_samp
+                inds = np.append(inds, _inds)
+                _values = np.full(shape=len(_inds),
+                                  fill_value=event_id_[annot['description']],
+                                  dtype=int)
+                values = np.append(values, _values)
 
     events = np.c_[inds, np.zeros(len(inds)), values].astype(int)
 


Test patch:
diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py
--- a/mne/tests/test_annotations.py
+++ b/mne/tests/test_annotations.py
@@ -210,6 +210,13 @@ def test_chunk_duration():
                                                 use_rounding=False)
     assert_array_equal(events, expected_events)
 
+    # test chunk durations that do not fit equally in annotation duration
+    expected_events = np.zeros((3, 3))
+    expected_events[:, -1] = 1
+    expected_events[:, 0] = np.arange(0, 9, step=3)
+    events, events_id = events_from_annotations(raw, chunk_duration=3.)
+    assert_array_equal(events, expected_events)
+
 
 def test_crop_more():
     """Test more cropping."""