Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ The backend is based on [pyqtgraph](https://github.com/pyqtgraph/pyqtgraph)
which uses Qt's [Graphics View Framework](https://doc.qt.io/qt-5/graphicsview.html)
for the plotting.
Development started as a [2021's Google Summer of Code Project](https://github.com/marsipu/gsoc2021).
Currently, only `Raw.plot()` is supported. For the future support for Epochs
and ICA-Sources is planned.
For supported features look [here](https://mne.tools/stable/generated/mne.viz.set_browser_backend.html)

## Installation
Install **full MNE-Python** with the instructions provided [here](https://mne.tools/stable/install/mne_python.html#d-plotting-and-source-analysis) or install **minimal MNE-Python** with
Expand Down
81 changes: 44 additions & 37 deletions mne_qt_browser/_pg_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ def update_value(self, value):

def update_nchan(self):
"""Update bar size."""
if self.mne.group_by in ['position', 'selection']:
if getattr(self.mne, 'group_by', None) in ['position', 'selection']:
self.setPageStep(1)
self.setMaximum(len(self.mne.ch_selections) - 1)
else:
Expand Down Expand Up @@ -876,7 +876,8 @@ def update_bad_epochs(self):

def update_events(self):
"""Update representation of events."""
if self.mne.event_nums is not None and self.mne.events_visible:
if getattr(self.mne, 'event_nums', None) is not None \
and self.mne.events_visible:
for ev_t, ev_id in zip(self.mne.event_times, self.mne.event_nums):
color_name = self.mne.event_color_dict[ev_id]
color = _get_color(color_name)
Expand Down Expand Up @@ -1384,6 +1385,9 @@ def _get_ypos(self):
ch_name not in self.mne.whitened_ch_names:
self.ypos = self.mne.ch_start + idx + 1
break
# Consider all indices bad
if self.ypos is None:
self.ypos = self.mne.ch_start + ch_type_idxs[0] + 1

def update_x_position(self):
"""Update x-position of Scalebar."""
Expand Down Expand Up @@ -2552,8 +2556,6 @@ def __init__(self, **kwargs):
# Initialize attributes which are only used by pyqtgraph, not by
# matplotlib and add them to MNEBrowseParams.

# Blocks concurrent scolling to avoid segmentation faults
self.is_scrolling = False
# Exactly one MessageBox for messages to facilitate testing/debugging
self.msg_box = QMessageBox(self)
# MessageBox modality needs to be adapted for tests
Expand Down Expand Up @@ -2705,7 +2707,7 @@ def __init__(self, **kwargs):
plt.addItem(grid_line)

# Add events
if self.mne.event_nums is not None:
if getattr(self.mne, 'event_nums', None) is not None:
self.mne.events_visible = True
for ev_time, ev_id in zip(self.mne.event_times,
self.mne.event_nums):
Expand Down Expand Up @@ -2749,7 +2751,10 @@ def __init__(self, **kwargs):
# Initialize BrowserView (inherits QGraphicsView)
view = BrowserView(plt, useOpenGL=self.mne.use_opengl)
if hasattr(self.mne, 'bgcolor'):
view.setBackground(_get_color(self.mne.bgcolor))
bgcolor = self.mne.bgcolor
else:
bgcolor = 'w'
view.setBackground(_get_color(bgcolor))
layout.addWidget(view, 0, 0)

# Initialize Scroll-Bars
Expand Down Expand Up @@ -2812,6 +2817,10 @@ def __init__(self, **kwargs):
widget.setLayout(layout)
self.setCentralWidget(widget)

# Initialize Selection-Dialog
if getattr(self.mne, 'group_by', None) in ['position', 'selection']:
self._create_selection_fig()

# Initialize Toolbar
toolbar = self.addToolBar('Tools')
toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
Expand Down Expand Up @@ -2905,31 +2914,31 @@ def __init__(self, **kwargs):
'modifier': [None, 'Shift'],
'slot': [self.hscroll],
'parameter': [-40, '-full'],
'description': [f'Move left ({hscroll_type})',
'Move left (full page)']
'description': [f'Scroll left ({hscroll_type})',
'Scroll left (full page)']
},
'right': {
'alias': '→',
'qt_key': Qt.Key_Right,
'modifier': [None, 'Shift'],
'slot': [self.hscroll],
'parameter': [40, '+full'],
'description': [f'Move right ({hscroll_type})',
'Move right (full page)']
'description': [f'Scroll right ({hscroll_type})',
'Scroll right (full page)']
},
'up': {
'alias': '↑',
'qt_key': Qt.Key_Up,
'slot': [self.vscroll],
'parameter': ['-full'],
'description': ['Move up (full page)']
'description': ['Scroll up (full page)']
},
'down': {
'alias': '↓',
'qt_key': Qt.Key_Down,
'slot': [self.vscroll],
'parameter': ['+full'],
'description': ['Move down (full page)']
'description': ['Scroll down (full page)']
},
'home': {
'alias': dur_keys[0],
Expand Down Expand Up @@ -3104,6 +3113,8 @@ def _add_scalebars(self):
self.mne.scalebar_texts[ch_type] = scale_bar_text
self.mne.plt.addItem(scale_bar_text)

self._set_scalebars_visible(self.mne.scalebars_visible)

def _update_scalebar_x_positions(self):
if self.mne.scalebars_visible:
for scalebar in self.mne.scalebars.values():
Expand Down Expand Up @@ -3161,11 +3172,6 @@ def scale_all(self, step):

def hscroll(self, step):
"""Scroll horizontally by step."""
if self.is_scrolling:
return

self.is_scrolling = True

if step == '+full':
rel_step = self.mne.duration
elif step == '-full':
Expand All @@ -3189,11 +3195,6 @@ def hscroll(self, step):

def vscroll(self, step):
"""Scroll vertically by step."""
if self.is_scrolling:
return

self.is_scrolling = True

if self.mne.fig_selection is not None:
if step == '+full':
step = 1
Expand Down Expand Up @@ -3353,12 +3354,20 @@ def _mouse_moved(self, pos):
tr.ypos - 0.5 < y < tr.ypos + 0.5]
if len(trace) == 1:
trace = trace[0]
idx = np.argmin(np.abs(trace.xData - x))
yshown = trace.get_ydata()[idx]

idx = np.searchsorted(self.mne.times, x)
if self.mne.data_precomputed:
data = self.mne.data[trace.order_idx]
else:
data = self.mne.data[trace.range_idx]
yvalue = data[idx]
yshown = yvalue + trace.ypos
self.mne.crosshair.set_data(x, yshown)

yvalue = yshown - trace.ypos
# relative x for epochs
if self.mne.is_epochs:
rel_idx = idx % len(self.mne.inst.times)
x = self.mne.inst.times[rel_idx]

# negative because plot is inverted for Y
scaler = -1 if self.mne.butterfly else -2
inv_norm = (scaler *
Expand Down Expand Up @@ -3424,9 +3433,6 @@ def _xrange_changed(self, _, xrange):
# Update Scalebars
self._update_scalebar_x_positions()

# Relieve Scrolling-Block
self.is_scrolling = False

def _update_events_xrange(self, xrange):
"""Add or remove event-lines depending on view-range.

Expand Down Expand Up @@ -3521,9 +3527,6 @@ def _yrange_changed(self, _, yrange):
trace.update_color()
trace.update_data()

# Relieve Scrolling-Block
self.is_scrolling = False

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# DATA HANDLING
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
Expand Down Expand Up @@ -4029,7 +4032,8 @@ def _set_butterfly(self, butterfly):
self._draw_traces()

def _toggle_butterfly(self):
self._set_butterfly(not self.mne.butterfly)
if self.mne.instance_type != 'ica':
self._set_butterfly(not self.mne.butterfly)

def _toggle_dc(self):
self.mne.remove_dc = not self.mne.remove_dc
Expand Down Expand Up @@ -4135,11 +4139,10 @@ def _toggle_epoch_histogramm(self):
if fig is not None:
self._get_dlg_from_mpl(fig)

def _update_trace_offsets(self):
pass

def _create_selection_fig(self):
SelectionDialog(self)
if not any([isinstance(fig, SelectionDialog) for
fig in self.mne.child_figs]):
SelectionDialog(self)

def message_box(self, text, info_text=None, buttons=None,
default_button=None, icon=None, modal=True):
Expand Down Expand Up @@ -4329,6 +4332,10 @@ def _click_ch_name(self, ch_index, button):
self._fake_click((x, y), fig=self.mne.view, button=button,
xform='none')

def _update_trace_offsets(self):
"""legacy method for mne<1.0"""
pass

def _resize_by_factor(self, factor):
pass

Expand Down
3 changes: 2 additions & 1 deletion mne_qt_browser/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from mne.conftest import (raw_orig, pg_backend, garbage_collect) # noqa: F401

_store = {'Raw': {},
'Epochs': {}}
'Epochs_unicolor': {},
'Epochs_multicolor': {}}


def pytest_configure(config):
Expand Down
66 changes: 63 additions & 3 deletions mne_qt_browser/tests/test_speed.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ def _initiate_hscroll(pg_fig, store, request, timer):

h_mean_fps = 1 / np.median(hscroll_diffs)
v_mean_fps = 1 / np.median(vscroll_diffs)
type_key = 'Epochs' if pg_fig.mne.is_epochs else 'Raw'
if pg_fig.mne.is_epochs:
if pg_fig.mne.epoch_colors is None:
type_key = 'Epochs_unicolor'
else:
type_key = 'Epochs_multicolor'
else:
type_key = 'Raw'
store[type_key][request.node.callspec.id] = dict(h=h_mean_fps,
v=v_mean_fps)
pg_fig.close()
Expand Down Expand Up @@ -147,6 +153,13 @@ def test_scroll_speed_raw(raw_orig, benchmark_param, store,
sys.exit(app.exec())


def _check_epochs_version():
import mne
from packaging.version import parse
if parse(mne.__version__) < parse('1.0'):
pytest.skip('Epochs-Test were skipped because of mne < 1.0!')


@pytest.mark.benchmark
@pytest.mark.parametrize('benchmark_param', [
pytest.param({'use_opengl': False, 'precompute': False},
Expand All @@ -159,11 +172,57 @@ def test_scroll_speed_raw(raw_orig, benchmark_param, store,
id='precompute=True'),
pytest.param({}, id='defaults'),
])
def test_scroll_speed_epochs(raw_orig, benchmark_param, store,
pg_backend, request):
def test_scroll_speed_epochs_unicolor(raw_orig, benchmark_param, store,
pg_backend, request):
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication
_check_epochs_version()
_reinit_bm_values()

app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)

events = np.full((50, 3), [0, 0, 1])
events[:, 0] = np.arange(0, len(raw_orig), len(raw_orig) / 50) \
+ raw_orig.first_samp
epochs = mne.Epochs(raw_orig, events, preload=True)
# Prevent problems with info's locked-stated
epochs.info._unlocked = True

fig = epochs.plot(show=False, block=False, **benchmark_param)

# # Wait max. 10 s for precomputed data to load
if fig.load_thread.isRunning():
fig.load_thread.wait(10000)

timer = QTimer()
timer.timeout.connect(partial(_initiate_hscroll, fig, store,
request, timer))
timer.start(0)

fig.show()
with pytest.raises(SystemExit):
sys.exit(app.exec())


@pytest.mark.benchmark
@pytest.mark.parametrize('benchmark_param', [
pytest.param({'use_opengl': False, 'precompute': False},
id='use_opengl=False'),
pytest.param({'use_opengl': True, 'precompute': False},
id='use_opengl=True', marks=gl_mark),
pytest.param({'precompute': False, 'use_opengl': False},
id='precompute=False'),
pytest.param({'precompute': True, 'use_opengl': False},
id='precompute=True'),
pytest.param({}, id='defaults'),
])
def test_scroll_speed_epochs_multicolor(raw_orig, benchmark_param, store,
pg_backend, request):
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication
_check_epochs_version()
_reinit_bm_values()

app = QApplication.instance()
Expand Down Expand Up @@ -191,6 +250,7 @@ def test_scroll_speed_epochs(raw_orig, benchmark_param, store,
epoch_colors[2::3] = epoch_col3
epoch_colors = epoch_colors.tolist()

# Multicolored Epochs might be unstable without OpenGL on macOS
if sys.platform == 'darwin':
benchmark_param['use_opengl'] = True

Expand Down
1 change: 1 addition & 0 deletions requirements_testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pytest-timeout
pooch
pyvista # for mne sys_info to tell us about OpenGL
tqdm
sklearn # for testing ICA