Repository: mne-tools/mne-python

Issue:
Misalignment of annotations after cropping raw
Hi,

I have an EEG dataset of one hour length covering several experiments. To mark the pauses between particular experiments as bad, I detect event codes marking their start and end point. After getting the corresponding time, I add it to the annotations variable and save it for further processing. The code is quite the same as in the [example](https://martinos.org/mne/dev/auto_tutorials/plot_artifacts_correction_rejection.html?highlight=blink%20annotation#marking-bad-raw-segments-with-annotations).

```
    raw = mne.io.read_raw_brainvision(raw_fname, montage=montage, preload=False)
    events = mne.find_events(raw, stim_channel='STI 014')

#%% Marking pauses as bad
    onset = np.array([events[idx,0] for idx in range(len(events[:, 2])) if events[idx, 2] > 80 and events[idx, 2] < 90])+1
    onset = np.insert(onset, 0, 0)
    end = np.array([events[idx,0] for idx in range(len(events[:, 2])) if events[idx, 2] > 70 and events[idx, 2] < 80])-1
    if events[0,2] < 70:
        end = np.concatenate([[events[0,0]-1], end, [len(raw)]])
    else:
        end = np.concatenate([end, [len(raw)]])

    duration = (end-onset)/raw.info['sfreq']
    onset = onset/raw.info['sfreq']
    description = ['bad']*len(onset)

    raw.annotations = mne.Annotations(onset, duration, description,
                                      orig_time=raw.info['meas_date'])

    raw.save(raw_fname+'.raw.fif.gz')
```

In a subsequent step, I load the EEG data, crop it and concatenate it. That results in a continous stream containing only the occurences of a certain experiment. 

```
    raw = mne.io.read_raw_fif(filename, preload=False)
    events = mne.find_events(raw, stim_channel='STI 014')

#%% cropping - assessing the beginning of particular experiments
    event_id = experiments['resting']['event_id']
    color = experiments['resting']['color']

# indices of the start and end triggercodes are detected, +1 and -1 to take a neighboring sample
    times_start = events[:, 0][np.where(events[:, 2] == event_id['start'])]-1
    times_end = events[:, 0][np.where(events[:, 2] == event_id['end'])]+1

    old_raw = raw.copy()
    new_raw = raw.copy().crop(raw.times[times_start[0]],
                              raw.times[times_end[0]])
    for idx in range(1, len(times_start)):
        mne.concatenate_raws([new_raw,
                              raw.copy().crop(raw.times[times_start[idx]],
                                              raw.times[times_end[idx]])])
# data is loaded, now the samples are loaded to memory
    raw = new_raw
    new_raw.close()
    raw.load_data()
    events = mne.find_events(raw, stim_channel='STI 014')
```

That is probably not the most elegant way of doing it.

The main problem is that after cropping the annotations are not where they are supposed to be. The following pictures show what I mean. 

![issue_annotations_oldraw](https://user-images.githubusercontent.com/20129752/31943259-78364100-b8c8-11e7-93ed-2a47d7ea589e.PNG)

![issue_annotations_newraw](https://user-images.githubusercontent.com/20129752/31943276-8a3cd530-b8c8-11e7-9695-75cb3b2829a0.PNG)

The annotations appear now at a totally different position which does not even fit to the onset values in the annotations variable:

```
old_raw.annotations.onset
Out[52]: 
array([    0.        ,   165.36399841,   609.80700684,   684.23101807,
        1065.14794922,  1497.01794434,  1589.45605469,  1963.6159668 ,
        2370.41699219,  2510.62597656,  2877.16308594,  3300.84399414,
        3431.56201172,  3873.2409668 ,  3942.5090332 ])

old_raw.first_samp
Out[53]: 0

new_raw.annotations.onset
Out[54]: array([ 437.35700684,  511.78101807])

new_raw.first_samp
Out[55]: 172450
```

I am not sure were the mistake happens. Probably, I misunderstood how the `orig_time` parameter works.
What is your suggestion?




Discussion:
> The main problem is that after cropping the annotations are not where they are supposed to be. 

If you are able to 1) add annotations to a raw object, 2) have them be at the correct times, 3) crop the data, and 4) have them be at the wrong times (shifted relative to the data the originally covered) -- and it sounds like this is what you're describing -- then it seems like a bug.

Can you try to simplify this down to a minimal example with the `sample` data that just adds a single annotation, plots it, crops, plots again showing a shift?
i think it's related to first_samp that should be adjusted when using crop.

Recently I also ran into problems with that pesky `first_samp` attribute that nobody seems to be aware of but MNE is still using sometimes. Time to start a new discussion to get rid of it?
the purpose of first_samp if is to have an absolute axis of time so if you
crop you know when it was in the full record.

makes sense?

I know what the purpose is, but this only works if you crop from the beginning of the file. What if I crop in the middle or end? What if I concatenate several cropped files? This is something we routinely do and `first_samp` doesn't help.
<img width="1134" alt="screen shot 2017-10-25 at 10 17 27" src="https://user-images.githubusercontent.com/161052/31987963-c2e4c83e-b96d-11e7-8cd9-5f80c54da662.png">

crop updates first_samp cf image. Do you see a potential problem with this?
now if you concatenate data from different runs you're indeed screwed as you don't know anymore more the absolute time of recording.
Plus it only works with .fif files. An option to get rid of first_samp would be nice (which would magically perform everything that makes a file start at time zero, including events).
just set first_samp to 0 for non fif files? why is this not enough?

Because people don't know about this behavior.

Maybe we could add a note to `Raw.append` saying that if the individual `raw` objects have a `first_samp` > 0 everything gets mangled, therefore setting `first_samp=0` is recommended in such a case.
+1 for adding a big warning when merging raws that don't have first_samp ==
0

... or maybe more completely, that don't all have `first_samp == 0` or `first_samp[1] == last_samp[0]`, etc. As this should still work `concatenate_raws([raw.copy().crop(0, 1-1./sfreq), raw.copy().crop(1, None)])` and be the same as `raw`.
Did I misunderstand something? Setting `first_samp` to `0` does not work.

```
new_raw.first_samp
Out[32]: 172450

new_raw.first_samp = 0
Traceback (most recent call last):

  File "<ipython-input-33-d0c36293b1ef>", line 1, in <module>
    new_raw.first_samp = 0

AttributeError: can't set attribute
```
we would need a setter for this.

it should be done in the reader.

I tried putting a setter to the reader, but did not succeed, would need some help or at least more explanation to do that.

What also works:

```
raw.first_samp
Out[4]: 240

raw._first_samps[0] = 0

raw.first_samp
Out[6]: 0
```
Here is a minimal example using the sample data:
```
import mne
import numpy as np
from mne.datasets import sample

print(__doc__)

data_path = sample.data_path()

fname = data_path + '/MEG/sample/sample_audvis_raw.fif'

raw = mne.io.read_raw_fif(fname, preload=False)

events = mne.find_events(raw, stim_channel='STI 014')


#%% Marking pauses as bad
onset = np.array([events[idx,0] for idx in range(len(events[:, 2])) if events[idx, 2] == 5])+1
end = np.array([events[idx,0] for idx in range(len(events[:, 2])) if events[idx, 2] == 32])-1
end = np.delete(end, -1)

duration = (end-onset)/raw.info['sfreq']
onset = onset/raw.info['sfreq']
description = ['bad']*len(onset)

raw.annotations = mne.Annotations(onset, duration, description,
                                  orig_time=raw.info['meas_date'])

#%% cropping - getting indices of where to crop
times_start = np.array([0, len(raw.times)*1/4, len(raw.times)*2/4, len(raw.times)*3/4])
times_end = np.array([len(raw.times)*1/4, len(raw.times)*2/4, len(raw.times)*3/4, len(raw.times)])-1

old_raw = raw.copy()
new_raw = raw.copy().crop(raw.times[times_start[0]],
                          raw.times[times_end[0]])

for idx in range(1, len(times_start)):
    cropped_raw = raw.copy().crop(raw.times[times_start[idx]],
                                  raw.times[times_end[idx]])
    mne.concatenate_raws([new_raw, cropped_raw])

old_raw.plot()
new_raw.plot()
```

First, I automatically set annotations according to event codes in `old_raw`. Then, I cut the `old_raw` data into four pieces which are subsequently concatenated. Intuitively, I would guess that the resulting `new_raw` should look the same as `old_raw`. That is not the case.

Resolving patch:
diff --git a/mne/annotations.py b/mne/annotations.py
--- a/mne/annotations.py
+++ b/mne/annotations.py
@@ -174,36 +174,33 @@ def save(self, fname):
             _write_annotations(fid, self)
 
 
-def _combine_annotations(annotations, last_samps, first_samps, sfreq,
-                         meas_date):
+def _combine_annotations(one, two, one_n_samples, one_first_samp,
+                         two_first_samp, sfreq, meas_date):
     """Combine a tuple of annotations."""
-    if not any(annotations):
+    if one is None and two is None:
         return None
-    elif annotations[1] is None:
-        return annotations[0]
-    elif annotations[0] is None:
-        old_onset = list()
-        old_duration = list()
-        old_description = list()
-        old_orig_time = None
-    else:
-        old_onset = annotations[0].onset
-        old_duration = annotations[0].duration
-        old_description = annotations[0].description
-        old_orig_time = annotations[0].orig_time
-
-    extra_samps = len(first_samps)  # Account for sample 0
-    if old_orig_time is not None and annotations[1].orig_time is None:
-        meas_date = _handle_meas_date(meas_date)
-        extra_samps += sfreq * (meas_date - old_orig_time) + first_samps[0]
-
-    onset = annotations[1].onset + (np.sum(last_samps) + extra_samps -
-                                    np.sum(first_samps)) / sfreq
-
-    onset = np.concatenate([old_onset, onset])
-    duration = np.concatenate([old_duration, annotations[1].duration])
-    description = np.concatenate([old_description, annotations[1].description])
-    return Annotations(onset, duration, description, old_orig_time)
+    elif two is None:
+        return one
+    elif one is None:
+        one = Annotations([], [], [], None)
+
+    # Compute the shift necessary for alignment:
+    # 1. The shift (in time) due to concatenation
+    shift = one_n_samples / sfreq
+    meas_date = _handle_meas_date(meas_date)
+    # 2. Shift by the difference in meas_date and one.orig_time
+    if one.orig_time is not None:
+        shift += one_first_samp / sfreq
+        shift += meas_date - one.orig_time
+    # 3. Shift by the difference in meas_date and two.orig_time
+    if two.orig_time is not None:
+        shift -= two_first_samp / sfreq
+        shift -= meas_date - two.orig_time
+
+    onset = np.concatenate([one.onset, two.onset + shift])
+    duration = np.concatenate([one.duration, two.duration])
+    description = np.concatenate([one.description, two.description])
+    return Annotations(onset, duration, description, one.orig_time)
 
 
 def _handle_meas_date(meas_date):
@@ -222,12 +219,10 @@ def _sync_onset(raw, onset, inverse=False):
     """Adjust onsets in relation to raw data."""
     meas_date = _handle_meas_date(raw.info['meas_date'])
     if raw.annotations.orig_time is None:
-        orig_time = meas_date
+        annot_start = onset
     else:
         offset = -raw._first_time if inverse else raw._first_time
-        orig_time = raw.annotations.orig_time - offset
-
-    annot_start = orig_time - meas_date + onset
+        annot_start = (raw.annotations.orig_time - meas_date) - offset + onset
     return annot_start
 
 
diff --git a/mne/io/base.py b/mne/io/base.py
--- a/mne/io/base.py
+++ b/mne/io/base.py
@@ -28,7 +28,8 @@
                     write_complex64, write_complex128, write_int,
                     write_id, write_string, _get_split_size)
 
-from ..annotations import _annotations_starts_stops, _write_annotations
+from ..annotations import (_annotations_starts_stops, _write_annotations,
+                           _handle_meas_date)
 from ..filter import (filter_data, notch_filter, resample, next_fast_len,
                       _resample_stim_channels, _filt_check_picks,
                       _filt_update_info)
@@ -676,12 +677,7 @@ def annotations(self, annotations, emit_warning=True):
             if not isinstance(annotations, Annotations):
                 raise ValueError('Annotations must be an instance of '
                                  'mne.Annotations. Got %s.' % annotations)
-            meas_date = self.info['meas_date']
-            if meas_date is None:
-                meas_date = 0
-            elif not np.isscalar(meas_date):
-                if len(meas_date) > 1:
-                    meas_date = meas_date[0] + meas_date[1] / 1000000.
+            meas_date = _handle_meas_date(self.info['meas_date'])
             if annotations.orig_time is not None:
                 offset = (annotations.orig_time - meas_date -
                           self.first_samp / self.info['sfreq'])
@@ -694,10 +690,14 @@ def annotations(self, annotations, emit_warning=True):
                 if onset > self.times[-1]:
                     omitted += 1
                     omit_ind.append(ind)
+                    logger.debug('Omitting %d @ %s > %s'
+                                 % (ind, onset, self.times[-1]))
                 elif onset < self.times[0]:
                     if onset + annotations.duration[ind] < self.times[0]:
                         omitted += 1
                         omit_ind.append(ind)
+                        logger.debug('Omitting %d @ %s < %s'
+                                     % (ind, onset, self.times[0]))
                     else:
                         limited += 1
                         duration = annotations.duration[ind] + onset
@@ -1607,7 +1607,9 @@ def crop(self, tmin=0.0, tmax=None):
 
         if self.annotations is not None:
             annotations = self.annotations
-            annotations.onset -= tmin
+            # XXX there might be a cleaner way to do this someday
+            if self.annotations.orig_time is None:
+                self.annotations.onset -= tmin
             BaseRaw.annotations.fset(self, annotations, emit_warning=False)
 
         return self
@@ -2015,18 +2017,17 @@ def append(self, raws, preload=None):
         annotations = self.annotations
         edge_samps = list()
         for ri, r in enumerate(raws):
-            annotations = _combine_annotations((annotations, r.annotations),
-                                               self._last_samps,
-                                               self._first_samps,
-                                               self.info['sfreq'],
-                                               self.info['meas_date'])
+            n_samples = self.last_samp - self.first_samp + 1
+            annotations = _combine_annotations(
+                annotations, r.annotations, n_samples,
+                self.first_samp, r.first_samp,
+                self.info['sfreq'], self.info['meas_date'])
             edge_samps.append(sum(self._last_samps) -
                               sum(self._first_samps) + (ri + 1))
             self._first_samps = np.r_[self._first_samps, r._first_samps]
             self._last_samps = np.r_[self._last_samps, r._last_samps]
             self._raw_extras += r._raw_extras
             self._filenames += r._filenames
-
         self._update_times()
         if annotations is None:
             annotations = Annotations([], [], [])
@@ -2459,7 +2460,8 @@ def _check_raw_compatibility(raw):
         raw[0].orig_format = 'unknown'
 
 
-def concatenate_raws(raws, preload=None, events_list=None):
+@verbose
+def concatenate_raws(raws, preload=None, events_list=None, verbose=None):
     """Concatenate raw instances as if they were continuous.
 
     .. note:: ``raws[0]`` is modified in-place to achieve the concatenation.
@@ -2478,6 +2480,9 @@ def concatenate_raws(raws, preload=None, events_list=None):
         have or not have data preloaded.
     events_list : None | list
         The events to concatenate. Defaults to None.
+    verbose : bool, str, int, or None
+        If not None, override default verbose level (see :func:`mne.verbose`
+        and :ref:`Logging documentation <tut_logging>` for more).
 
     Returns
     -------
diff --git a/mne/io/fiff/raw.py b/mne/io/fiff/raw.py
--- a/mne/io/fiff/raw.py
+++ b/mne/io/fiff/raw.py
@@ -106,17 +106,14 @@ def __init__(self, fname, allow_maxshield=False, preload=False,
         # combine annotations
         BaseRaw.annotations.fset(self, raws[0].annotations, False)
         if any([r.annotations for r in raws[1:]]):
-            first_samps = self._first_samps
-            last_samps = self._last_samps
+            n_samples = np.sum(self._last_samps - self._first_samps + 1)
             for r in raws:
-                annotations = _combine_annotations((self.annotations,
-                                                    r.annotations),
-                                                   last_samps, first_samps,
-                                                   r.info['sfreq'],
-                                                   self.info['meas_date'])
+                annotations = _combine_annotations(
+                    self.annotations, r.annotations,
+                    n_samples, self.first_samp, r.first_samp,
+                    r.info['sfreq'], self.info['meas_date'])
                 BaseRaw.annotations.fset(self, annotations, False)
-                first_samps = np.r_[first_samps, r.first_samp]
-                last_samps = np.r_[last_samps, r.last_samp]
+                n_samples += r.last_samp - r.first_samp + 1
 
         # Add annotations for in-data skips
         offsets = [0] + self._raw_lengths[:-1]


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
@@ -6,12 +6,13 @@
 import os.path as op
 import warnings
 
-from nose.tools import assert_raises, assert_true
+import pytest
 from numpy.testing import (assert_equal, assert_array_equal,
                            assert_array_almost_equal, assert_allclose)
 
 import numpy as np
 
+import mne
 from mne import create_info, Epochs, read_annotations
 from mne.utils import run_tests_if_main, _TempDir
 from mne.io import read_raw_fif, RawArray, concatenate_raws
@@ -20,15 +21,15 @@
 from mne.datasets import testing
 
 data_dir = op.join(testing.data_path(download=False), 'MEG', 'sample')
-fif_fname = op.join(data_dir, 'sample_audvis_trunc_raw.fif')
+fif_fname = op.join(op.dirname(__file__), '..', 'io', 'tests', 'data',
+                    'test_raw.fif')
 
 
-@testing.requires_testing_data
-def test_annotations():
+def test_basics():
     """Test annotation class."""
     raw = read_raw_fif(fif_fname)
     assert raw.annotations is None
-    assert_raises(IOError, read_annotations, fif_fname)
+    pytest.raises(IOError, read_annotations, fif_fname)
     onset = np.array(range(10))
     duration = np.ones(10)
     description = np.repeat('test', 10)
@@ -38,22 +39,24 @@ def test_annotations():
     for orig_time in [None, dt, meas_date[0], meas_date]:
         annot = Annotations(onset, duration, description, orig_time)
 
-    assert_raises(ValueError, Annotations, onset, duration, description[:9])
-    assert_raises(ValueError, Annotations, [onset, 1], duration, description)
-    assert_raises(ValueError, Annotations, onset, [duration, 1], description)
+    pytest.raises(ValueError, Annotations, onset, duration, description[:9])
+    pytest.raises(ValueError, Annotations, [onset, 1], duration, description)
+    pytest.raises(ValueError, Annotations, onset, [duration, 1], description)
 
     # Test combining annotations with concatenate_raws
     raw2 = raw.copy()
-    orig_time = (meas_date[0] + meas_date[1] * 0.000001 +
+    delta = raw.times[-1] + 1. / raw.info['sfreq']
+    orig_time = (meas_date[0] + meas_date[1] * 1e-6 +
                  raw2.first_samp / raw2.info['sfreq'])
     annot = Annotations(onset, duration, description, orig_time)
-    assert_true(' segments' in repr(annot))
+    assert ' segments' in repr(annot)
     raw2.annotations = annot
     assert_array_equal(raw2.annotations.onset, onset)
     concatenate_raws([raw, raw2])
     raw.annotations.delete(-1)  # remove boundary annotations
     raw.annotations.delete(-1)
-    assert_array_almost_equal(onset + 20., raw.annotations.onset, decimal=2)
+
+    assert_allclose(onset + delta, raw.annotations.onset, rtol=1e-5)
     assert_array_equal(annot.duration, raw.annotations.duration)
     assert_array_equal(raw.annotations.description, np.repeat('test', 10))
 
@@ -62,22 +65,22 @@ def test_annotations():
     sfreq = 100.
     info = create_info(ch_names=['MEG1', 'MEG2'], ch_types=['grad'] * 2,
                        sfreq=sfreq)
-    info['meas_date'] = 0
+    info['meas_date'] = np.pi
     raws = []
-    for i, fs in enumerate([12300, 100, 12]):
-        raw = RawArray(data.copy(), info, first_samp=fs)
-        ants = Annotations([1., 2.], [.5, .5], 'x', fs / sfreq)
+    for first_samp in [12300, 100, 12]:
+        raw = RawArray(data.copy(), info, first_samp=first_samp)
+        ants = Annotations([1., 2.], [.5, .5], 'x', np.pi + first_samp / sfreq)
         raw.annotations = ants
         raws.append(raw)
     raw = RawArray(data.copy(), info)
     raw.annotations = Annotations([1.], [.5], 'x', None)
     raws.append(raw)
-    raw = concatenate_raws(raws)
+    raw = concatenate_raws(raws, verbose='debug')
     boundary_idx = np.where(raw.annotations.description == 'BAD boundary')[0]
-    assert_equal(len(boundary_idx), 3)
+    assert len(boundary_idx) == 3
     raw.annotations.delete(boundary_idx)
     boundary_idx = np.where(raw.annotations.description == 'EDGE boundary')[0]
-    assert_equal(len(boundary_idx), 3)
+    assert len(boundary_idx) == 3
     raw.annotations.delete(boundary_idx)
     assert_array_equal(raw.annotations.onset, [1., 2., 11., 12., 21., 22.,
                                                31.])
@@ -89,8 +92,51 @@ def test_annotations():
     assert_array_equal(raw.annotations.description, ['x', 'x', 'x', 'x', 'x',
                                                      'x', 'y'])
 
-    # Test concatenating annotations with and without orig_time.
+
+def test_crop():
+    """Test cropping with annotations."""
     raw = read_raw_fif(fif_fname)
+    events = mne.find_events(raw)
+    onset = events[events[:, 2] == 1, 0] / raw.info['sfreq']
+    duration = np.full_like(onset, 0.5)
+    description = ['bad %d' % k for k in range(len(onset))]
+    raw.annotations = mne.Annotations(onset, duration, description,
+                                      orig_time=raw.info['meas_date'])
+
+    split_time = raw.times[-1] / 2. + 2.
+    split_idx = len(onset) // 2 + 1
+    raw_cropped_left = raw.copy().crop(0., split_time - 1. / raw.info['sfreq'])
+    assert_array_equal(raw_cropped_left.annotations.description,
+                       raw.annotations.description[:split_idx])
+    assert_allclose(raw_cropped_left.annotations.duration,
+                    raw.annotations.duration[:split_idx])
+    assert_allclose(raw_cropped_left.annotations.onset,
+                    raw.annotations.onset[:split_idx])
+    raw_cropped_right = raw.copy().crop(split_time, None)
+    assert_array_equal(raw_cropped_right.annotations.description,
+                       raw.annotations.description[split_idx:])
+    assert_allclose(raw_cropped_right.annotations.duration,
+                    raw.annotations.duration[split_idx:])
+    assert_allclose(raw_cropped_right.annotations.onset,
+                    raw.annotations.onset[split_idx:])
+    raw_concat = mne.concatenate_raws([raw_cropped_left, raw_cropped_right],
+                                      verbose='debug')
+    assert_allclose(raw_concat.times, raw.times)
+    assert_allclose(raw_concat[:][0], raw[:][0], atol=1e-20)
+    # Get rid of the boundary events
+    raw_concat.annotations.delete(-1)
+    raw_concat.annotations.delete(-1)
+    # Ensure we annotations survive round-trip crop->concat
+    assert_array_equal(raw_concat.annotations.description,
+                       raw.annotations.description)
+    for attr in ('onset', 'duration'):
+        assert_allclose(getattr(raw_concat.annotations, attr),
+                        getattr(raw.annotations, attr),
+                        err_msg='Failed for %s:' % (attr,))
+
+    raw.annotations = None  # undo
+
+    # Test concatenating annotations with and without orig_time.
     last_time = raw.last_samp / raw.info['sfreq']
     raw2 = raw.copy()
     raw.annotations = Annotations([45.], [3], 'test', raw.info['meas_date'])
@@ -112,7 +158,7 @@ def test_annotations():
     assert_array_equal(annot_read.description, raw.annotations.description)
     annot = Annotations((), (), ())
     annot.save(fname)
-    assert_raises(IOError, read_annotations, fif_fname)  # none in old raw
+    pytest.raises(IOError, read_annotations, fif_fname)  # none in old raw
     annot = read_annotations(fname)
     assert isinstance(annot, Annotations)
     assert len(annot) == 0
@@ -129,6 +175,41 @@ def test_annotations():
     assert raw_read.annotations is None
 
 
+def test_crop_more():
+    """Test more cropping."""
+    raw = mne.io.read_raw_fif(fif_fname).crop(0, 11).load_data()
+    raw._data[:] = np.random.RandomState(0).randn(*raw._data.shape)
+    onset = np.array([0.47058824, 2.49773765, 6.67873287, 9.15837097])
+    duration = np.array([0.89592767, 1.13574672, 1.09954739, 0.48868752])
+    annotations = mne.Annotations(onset, duration, 'BAD')
+    raw.annotations = annotations
+    assert len(raw.annotations) == 4
+    delta = 1. / raw.info['sfreq']
+    raw_concat = mne.concatenate_raws(
+        [raw.copy().crop(0, 4 - delta),
+         raw.copy().crop(4, 8 - delta),
+         raw.copy().crop(8, None)])
+    assert_allclose(raw_concat.times, raw.times)
+    assert_allclose(raw_concat[:][0], raw[:][0])
+    assert raw_concat.first_samp == raw.first_samp
+    boundary_idx = np.where(
+        raw_concat.annotations.description == 'BAD boundary')[0]
+    assert len(boundary_idx) == 2
+    raw_concat.annotations.delete(boundary_idx)
+    boundary_idx = np.where(
+        raw_concat.annotations.description == 'EDGE boundary')[0]
+    assert len(boundary_idx) == 2
+    raw_concat.annotations.delete(boundary_idx)
+    assert len(raw_concat.annotations) == 4
+    assert_array_equal(raw_concat.annotations.description,
+                       raw.annotations.description)
+    assert_allclose(raw.annotations.duration, duration)
+    assert_allclose(raw_concat.annotations.duration, duration)
+    assert_allclose(raw.annotations.onset, onset)
+    assert_allclose(raw_concat.annotations.onset, onset,
+                    atol=1. / raw.info['sfreq'])
+
+
 @testing.requires_testing_data
 def test_read_brainstorm_annotations():
     """Test reading for Brainstorm events file"""
@@ -145,7 +226,6 @@ def test_read_brainstorm_annotations():
     assert annot.orig_time == orig_time_as_scalar
 
 
-@testing.requires_testing_data
 def test_raw_reject():
     """Test raw data getter with annotation reject."""
     sfreq = 100.
@@ -163,19 +243,27 @@ def test_raw_reject():
 
     # with orig_time and complete overlap
     raw = read_raw_fif(fif_fname)
-    raw.annotations = Annotations([44, 47, 48], [1, 3, 1], 'BAD',
-                                  raw.info['meas_date'])
-    data, times = raw.get_data(range(10), 0, 6000, 'omit', True)
-    assert_array_equal(data.shape, (10, 4799))
-    assert_equal(times[-1], raw.times[5999])
-    assert_array_equal(data[:, -100:], raw[:10, 5900:6000][0])
-
-    data, times = raw.get_data(range(10), 0, 6000, 'NaN', True)
-    assert_array_equal(data.shape, (10, 6000))
-    assert_equal(times[-1], raw.times[5999])
-    assert_true(np.isnan(data[:, 314:613]).all())  # 1s -2s
-    assert_true(not np.isnan(data[:, 614].any()))
-    assert_array_equal(data[:, -100:], raw[:10, 5900:6000][0])
+    t_0 = raw.first_samp / raw.info['sfreq']
+    raw.annotations = Annotations([t_0 + 1, t_0 + 4, t_0 + 5], [1, 3, 1],
+                                  'BAD', raw.info['meas_date'])
+    t_stop = 18.
+    assert raw.times[-1] > t_stop
+    n_stop = int(round(t_stop * raw.info['sfreq']))
+    n_drop = int(round(4 * raw.info['sfreq']))
+    assert len(raw.times) >= n_stop
+    data, times = raw.get_data(range(10), 0, n_stop, 'omit', True)
+    assert data.shape == (10, n_stop - n_drop)
+    assert times[-1] == raw.times[n_stop - 1]
+    assert_array_equal(data[:, -100:], raw[:10, n_stop - 100:n_stop][0])
+
+    data, times = raw.get_data(range(10), 0, n_stop, 'NaN', True)
+    assert_array_equal(data.shape, (10, n_stop))
+    assert times[-1] == raw.times[n_stop - 1]
+    t_1, t_2 = raw.time_as_index([1, 2], use_rounding=True)
+    assert np.isnan(data[:, t_1:t_2]).all()  # 1s -2s
+    assert not np.isnan(data[:, :t_1].any())
+    assert not np.isnan(data[:, t_2:].any())
+    assert_array_equal(data[:, -100:], raw[:10, n_stop - 100:n_stop][0])
     assert_array_equal(raw.get_data(), raw[:][0])
 
     # Test _sync_onset
@@ -272,7 +360,7 @@ def test_annotation_omit():
                                        reject_by_annotation='omit')
                           for start, stop in ((0, 1000), (1000, 2000))], -1)
     assert_allclose(got, expected)
-    assert_raises(ValueError, raw.get_data, reject_by_annotation='foo')
+    pytest.raises(ValueError, raw.get_data, reject_by_annotation='foo')
 
 
 def test_annotation_epoching():