Repository: mne-tools/mne-python

Issue:
IndexError in epochs.plot() when first epoch of next page is marked as bad and there is a bad channel
#### Description:
An IndexError occurs when the first epoch of the next page (i.e. epoch nr 21 when n_epochs=20) is marked as bad and there is a bad channel. It would be fixed by changing:

 _if bad_idx < start_idx or bad_idx __>__ end_idx:_  _(line 1275 of /mne/viz/epochs.py)_
to:
 _if bad_idx < start_idx or bad_idx __>=__ end_idx:_

However, I'm surprised that this didn't show up before (since the commit that introduced this line is from 2015), therefore I'm asking here before submitting the pull request. Should I go ahead?

#### Steps to reproduce:
import mne
import os.path as op

data_path = mne.datasets.sample.data_path()
raw = mne.io.read_raw_fif(op.join(data_path, 'MEG/sample/sample_audvis_raw.fif'))
events = mne.find_events(raw, stim_channel='STI 014')
event_id = {'Auditory/Left': 1, 'Auditory/Right': 2}
epochs = mne.Epochs(raw, events, event_id, tmin=-0.1, tmax=1, baseline=(None, 0), preload=True)
epochs.plot(block=True)

-> mark one channel as bad
-> press right arrow to go to next page
-> mark the first epoch of the new page as bad
-> press left arrow to go to the previous page

#### Expected results
No error

#### Actual results
_IndexError: index 20 is out of bounds for axis 0 with size 20_


#### Additional information
Platform:      Linux-4.15.0-45-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/eze/soft/anaconda3/envs/mne3/bin/python
CPU:           x86_64: 8 cores
Memory:        15.6 GB
mne:           0.18.dev0
numpy:         1.15.0 {blas=mkl_rt, lapack=mkl_rt}
scipy:         1.1.0
matplotlib:    2.2.2 {backend=Qt5Agg}
sklearn:       0.19.1
nibabel:       2.3.0
mayavi:        4.7.0.dev0 {qt_api=pyqt5, PyQt5=5.9.2}
cupy:          Not found
pandas:        0.23.3
dipy:          Not found




Discussion:
issue confirmed and bug fix suggestion does fix it for me. Can you open a
PR?

thanks heaps

>


Resolving patch:
diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py
--- a/mne/viz/epochs.py
+++ b/mne/viz/epochs.py
@@ -1272,7 +1272,7 @@ def _plot_traces(params):
                     ylabels[line_idx].set_color(this_color)
                 this_color = np.tile((params['bad_color']), (num_epochs, 1))
                 for bad_idx in params['bads']:
-                    if bad_idx < start_idx or bad_idx > end_idx:
+                    if bad_idx < start_idx or bad_idx >= end_idx:
                         continue
                     this_color[bad_idx - start_idx] = (1., 0., 0.)
                 lines[line_idx].set_zorder(2)


Test patch:
diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py
--- a/mne/viz/tests/test_epochs.py
+++ b/mne/viz/tests/test_epochs.py
@@ -29,7 +29,7 @@
 layout = read_layout('Vectorview-all')
 
 
-def _get_epochs():
+def _get_epochs(stop=5):
     """Get epochs."""
     raw = read_raw_fif(raw_fname)
     events = read_events(event_name)
@@ -38,15 +38,16 @@ def _get_epochs():
     # Use a subset of channels for plotting speed
     picks = np.round(np.linspace(0, len(picks) + 1, n_chan)).astype(int)
     with pytest.warns(RuntimeWarning, match='projection'):
-        epochs = Epochs(raw, events[:5], event_id, tmin, tmax, picks=picks,
+        epochs = Epochs(raw, events[:stop], event_id, tmin, tmax, picks=picks,
                         proj=False)
     epochs.info.normalize_proj()  # avoid warnings
     return epochs
 
 
-def test_plot_epochs():
+def test_plot_epochs(capsys):
     """Test epoch plotting."""
-    epochs = _get_epochs()
+    epochs = _get_epochs().load_data()
+    assert len(epochs.events) == 1
     epochs.info['lowpass'] = 10.  # allow heavy decim during plotting
     epochs.plot(scalings=None, title='Epochs')
     plt.close('all')
@@ -104,9 +105,9 @@ def test_plot_epochs():
     plt.close('all')
     fig = epochs.plot(events=epochs.events)
     # test mouse clicks
-    x = fig.get_axes()[0].get_xlim()[1] / 2
-    y = fig.get_axes()[0].get_ylim()[0] / 2
     data_ax = fig.get_axes()[0]
+    x = data_ax.get_xlim()[1] / 2
+    y = data_ax.get_ylim()[0] / 2
     n_epochs = len(epochs)
     _fake_click(fig, data_ax, [x, y], xform='data')  # mark a bad epoch
     _fake_click(fig, data_ax, [x, y], xform='data')  # unmark a bad epoch
@@ -120,6 +121,26 @@ def test_plot_epochs():
     plt.close('all')
     epochs.plot_sensors()  # Test plot_sensors
     plt.close('all')
+    # gh-5906
+    epochs = _get_epochs(None).load_data()
+    epochs.load_data()
+    assert len(epochs) == 7
+    epochs.info['bads'] = [epochs.ch_names[0]]
+    capsys.readouterr()
+    fig = epochs.plot(n_epochs=3)
+    data_ax = fig.get_axes()[0]
+    _fake_click(fig, data_ax, [-0.1, 0.9])  # click on y-label
+    fig.canvas.key_press_event('right')  # move right
+    x = fig.get_axes()[0].get_xlim()[1] / 6.
+    y = fig.get_axes()[0].get_ylim()[0] / 2
+    _fake_click(fig, data_ax, [x, y], xform='data')  # mark a bad epoch
+    fig.canvas.key_press_event('left')  # move back
+    out, err = capsys.readouterr()
+    assert 'out of bounds' not in out
+    assert 'out of bounds' not in err
+    fig.canvas.close_event()
+    assert len(epochs) == 6
+    plt.close('all')
 
 
 def test_plot_epochs_image():