Repository: mne-tools/mne-python

Issue:
mark_flats results in annotations that are out of data range
#### Describe the bug
`mne.preprocessing.mark_flats` appends flat segments to `raw.annotations` that are out of data range.

This probably happens because `mne.preprocessing.mark_flats` adds annotations relative to the first sample while `set_annotations` has changed in `0.17` so that `orig_time` matches `info['meas_date']` and not the first sample.

#### Steps and/or code to reproduce

```Python
import numpy as np
import mne

# Data
sfreq = 1000  # Sampling frequency
times = np.arange(0, 10, 0.001)  # Use 10000 samples (10s)

sin = np.sin(times * 10)  # Multiplied by 10 for shorter cycles
flat = np.zeros(20)

data = np.atleast_2d(np.array(np.r_[sin, flat, sin]))

# Info
ch_types = ['mag']
ch_names = ['sin_flat_sin']

info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
info['meas_date'] = (1545655654, 71165)

# Raw
raw = mne.io.RawArray(data, info, first_samp=50000)

# Mark flats
mne.preprocessing.flat.mark_flat(raw)

# The annotation is omitted though it should not be
raw.set_annotations(raw.annotations)
```


#### Expected results
Nothing changes when I run `raw.set_annotations(raw.annotations)`


#### Actual results
The segment is omitted due to being out of data range.

```
<stdin>:1: RuntimeWarning: Omitted 1 annotation(s) that were outside data range.
```


#### Additional information
```
Platform:      Windows-7-6.1.7601-SP1
Python:        3.7.3 (default, Apr 24 2019, 15:29:51) [MSC v.1915 64 bit (AMD64)
]
Executable:    D:\Windows_software\Miniconda3\envs\mne_dev_test\python.exe
CPU:           Intel64 Family 6 Model 69 Stepping 1, GenuineIntel: 4 cores
Memory:        Unavailable (requires "psutil" package)
mne:           0.18.dev0
numpy:         1.16.3 {blas=mkl_rt, lapack=mkl_rt}
scipy:         1.2.1
matplotlib:    Not found

sklearn:       Not found
nibabel:       Not found
mayavi:        Not found
cupy:          Not found
pandas:        Not found
dipy:          Not found
```



Discussion:
@kalenkovich thanks for the bug report. Do you want to try to fix it yourself with a pull request?
@agramfort I'll do it tomorrow.

Resolving patch:
diff --git a/mne/preprocessing/flat.py b/mne/preprocessing/flat.py
--- a/mne/preprocessing/flat.py
+++ b/mne/preprocessing/flat.py
@@ -93,7 +93,7 @@ def mark_flat(raw, bad_percent=5., min_duration=0.005, picks=None,
     raw.info['bads'] = list(raw.info['bads']) + add_bads
     if len(starts) > 0:
         starts, stops = np.array(starts), np.array(stops)
-        onsets = starts / raw.info['sfreq']
+        onsets = (starts + raw.first_samp) / raw.info['sfreq']
         durations = (stops - starts) / raw.info['sfreq']
         raw.annotations.append(onsets, durations, ['BAD_flat'] * len(onsets))
     return raw


Test patch:
diff --git a/mne/preprocessing/tests/test_flat.py b/mne/preprocessing/tests/test_flat.py
--- a/mne/preprocessing/tests/test_flat.py
+++ b/mne/preprocessing/tests/test_flat.py
@@ -10,14 +10,17 @@
 from mne.preprocessing import mark_flat
 
 
-def test_mark_flat():
+@pytest.mark.parametrize('first_samp', (0, 10000))
+def test_mark_flat(first_samp):
     """Test marking flat segments."""
     # Test if ECG analysis will work on data that is not preloaded
     n_ch, n_times = 11, 1000
     data = np.random.RandomState(0).randn(n_ch, n_times)
     assert not (np.diff(data, axis=-1) == 0).any()  # nothing flat at first
     info = create_info(n_ch, 1000., 'eeg')
-    raw = RawArray(data, info)
+    info['meas_date'] = (1, 2)
+    # test first_samp != for gh-6295
+    raw = RawArray(data, info, first_samp=first_samp)
     raw.info['bads'] = [raw.ch_names[-1]]
 
     #
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
@@ -31,6 +31,8 @@
 fif_fname = op.join(op.dirname(__file__), '..', 'io', 'tests', 'data',
                     'test_raw.fif')
 
+first_samps = pytest.mark.parametrize('first_samp', (0, 10000))
+
 
 def test_basics():
     """Test annotation class."""
@@ -67,7 +69,9 @@ def test_basics():
     assert_array_equal(annot.duration, raw.annotations.duration)
     assert_array_equal(raw.annotations.description, np.repeat('test', 10))
 
-    # Test combining with RawArray and orig_times
+
+def test_raw_array_orig_times():
+    """Test combining with RawArray and orig_times."""
     data = np.random.randn(2, 1000) * 10e-12
     sfreq = 100.
     info = create_info(ch_names=['MEG1', 'MEG2'], ch_types=['grad'] * 2,
@@ -190,12 +194,13 @@ def test_crop():
     assert len(raw_read.annotations.onset) == 0  # XXX to be fixed in #5416
 
 
-def test_chunk_duration():
+@first_samps
+def test_chunk_duration(first_samp):
     """Test chunk_duration."""
     # create dummy raw
     raw = RawArray(data=np.empty([10, 10], dtype=np.float64),
                    info=create_info(ch_names=10, sfreq=1.),
-                   first_samp=0)
+                   first_samp=first_samp)
     raw.info['meas_date'] = 0
     raw.set_annotations(Annotations(description='foo', onset=[0],
                                     duration=[10], orig_time=None))
@@ -205,6 +210,7 @@ def test_chunk_duration():
     expected_events = np.atleast_2d(np.repeat(range(10), repeats=2)).T
     expected_events = np.insert(expected_events, 1, 0, axis=1)
     expected_events = np.insert(expected_events, 2, 1, axis=1)
+    expected_events[:, 0] += first_samp
 
     events, events_id = events_from_annotations(raw, chunk_duration=.5,
                                                 use_rounding=False)
@@ -213,7 +219,7 @@ def test_chunk_duration():
     # 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)
+    expected_events[:, 0] = np.arange(0, 9, step=3) + first_samp
     events, events_id = events_from_annotations(raw, chunk_duration=3.)
     assert_array_equal(events, expected_events)
 
@@ -257,11 +263,12 @@ def test_read_brainstorm_annotations():
     assert np.unique(annot.description).size == 5
 
 
-def test_raw_reject():
+@first_samps
+def test_raw_reject(first_samp):
     """Test raw data getter with annotation reject."""
     sfreq = 100.
     info = create_info(['a', 'b', 'c', 'd', 'e'], sfreq, ch_types='eeg')
-    raw = RawArray(np.ones((5, 15000)), info)
+    raw = RawArray(np.ones((5, 15000)), info, first_samp=first_samp)
     with pytest.warns(RuntimeWarning, match='outside the data range'):
         raw.set_annotations(Annotations([2, 100, 105, 148],
                                         [2, 8, 5, 8], 'BAD'))
@@ -307,12 +314,14 @@ def test_raw_reject():
     assert_array_almost_equal(times, _sync_onset(raw, onsets, True))
 
 
-def test_annotation_filtering():
+@first_samps
+def test_annotation_filtering(first_samp):
     """Test that annotations work properly with filtering."""
     # Create data with just a DC component
     data = np.ones((1, 1000))
     info = create_info(1, 1000., 'eeg')
-    raws = [RawArray(data * (ii + 1), info) for ii in range(4)]
+    raws = [RawArray(data * (ii + 1), info, first_samp=first_samp)
+            for ii in range(4)]
     kwargs_pass = dict(l_freq=None, h_freq=50., fir_design='firwin')
     kwargs_stop = dict(l_freq=50., h_freq=None, fir_design='firwin')
     # lowpass filter, which should not modify the data
@@ -380,11 +389,12 @@ def test_annotation_filtering():
     assert_allclose(raw_filt[:][0], expected, atol=1e-14)
 
 
-def test_annotation_omit():
+@first_samps
+def test_annotation_omit(first_samp):
     """Test raw.get_data with annotations."""
     data = np.concatenate([np.ones((1, 1000)), 2 * np.ones((1, 1000))], -1)
     info = create_info(1, 1000., 'eeg')
-    raw = RawArray(data, info)
+    raw = RawArray(data, info, first_samp=first_samp)
     raw.set_annotations(Annotations([0.5], [1], ['bad']))
     expected = raw[0][0]
     assert_allclose(raw.get_data(reject_by_annotation=None), expected)