diff --git a/.circleci/config.yml b/.circleci/config.yml index 697c3e8c761..b40e56dc33a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,12 +11,6 @@ version: 2.1 -_xvfb: &xvfb - name: Start Xvfb virtual framebuffer - command: | - echo "export DISPLAY=:99" >> $BASH_ENV - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1280x1024x24 -ac +extension GLX +render -noreset -nolisten tcp -nolisten unix - jobs: build_docs: parameters: @@ -24,9 +18,7 @@ jobs: type: string default: "false" docker: - # Use 18.04 rather than 20.04 because MESA 20.0.8 on 18.04 has working - # transparency but 21.0.3 on 20.04 does not! - - image: cimg/base:stable-18.04 + - image: cimg/base:stable-20.04 steps: - restore_cache: keys: @@ -95,13 +87,8 @@ jobs: name: Set BASH_ENV command: | set -e - sudo apt update -qq - sudo apt install -qq libosmesa6 libglx-mesa0 libopengl0 libglx0 libdbus-1-3 \ - libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 \ - libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 \ - graphviz optipng \ - python3.8-venv python3-venv \ - xvfb libxft2 ffmpeg + ./tools/setup_xvfb.sh + sudo apt install -qq graphviz optipng python3.8-venv python3-venv libxft2 ffmpeg python3.8 -m venv ~/python_env echo "set -e" >> $BASH_ENV echo "export OPENBLAS_NUM_THREADS=4" >> $BASH_ENV @@ -109,9 +96,11 @@ jobs: echo "export MNE_FULL_DATE=true" >> $BASH_ENV source tools/get_minimal_commands.sh echo "export MNE_3D_BACKEND=pyvistaqt" >> $BASH_ENV + echo "export MNE_3D_OPTION_MULTI_SAMPLES=1" >> $BASH_ENV echo "export MNE_BROWSER_BACKEND=qt" >> $BASH_ENV echo "export MNE_BROWSER_PRECOMPUTE=false" >> $BASH_ENV echo "export PATH=~/.local/bin/:$PATH" >> $BASH_ENV + echo "export DISPLAY=:99" >> $BASH_ENV echo "source ~/python_env/bin/activate" >> $BASH_ENV mkdir -p ~/.local/bin ln -s ~/python_env/bin/python ~/.local/bin/python @@ -124,9 +113,6 @@ jobs: command: | neuromag2ft --version - - run: - <<: *xvfb - - run: name: Install fonts needed for diagrams command: | @@ -160,8 +146,8 @@ jobs: - ~/.local/bin - run: - name: Check PyQt5 - command: LD_DEBUG=libs python -c "from PyQt5.QtWidgets import QApplication, QWidget; app = QApplication([])" + name: Check Qt + command: LD_DEBUG=libs python -c "from PySide6.QtWidgets import QApplication, QWidget; app = QApplication([])" # Load tiny cache so that ~/.mne does not need to be created below - restore_cache: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c6013127a98..f52cc53e0ad 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -89,7 +89,9 @@ stages: OPENBLAS_NUM_THREADS: '1' steps: - bash: | - sudo apt install libxkbcommon-x11-0 xvfb tcsh libxcb* + set -e + ./tools/setup_xvfb.sh + sudo apt install -yq tcsh displayName: 'Install Ubuntu dependencies' - bash: | source tools/get_minimal_commands.sh @@ -105,17 +107,17 @@ stages: architecture: 'x64' addToPath: true displayName: 'Get Python' - - bash: | - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render -noreset; - displayName: 'Spin up Xvfb' - bash: | set -e python -m pip install --progress-bar off --upgrade pip setuptools wheel codecov - python -m pip install --progress-bar off mne-qt-browser[opengl] vtk scikit-learn pytest-error-for-skips python-picard + python -m pip install --progress-bar off mne-qt-browser[opengl] vtk scikit-learn pytest-error-for-skips python-picard PySide6 qtpy python -m pip uninstall -yq mne python -m pip install --progress-bar off --upgrade -e .[test] displayName: 'Install dependencies with pip' - - script: mne sys_info -pd + - bash: | + set -e + mne sys_info -pd + mne sys_info -pd | grep "qtpy: .*{PySide6=.*}$" displayName: Print config - bash: source tools/get_testing_version.sh displayName: 'Get testing version' @@ -152,10 +154,7 @@ stages: OPENBLAS_NUM_THREADS: '1' TEST_OPTIONS: "--tb=short --cov=mne --cov-report=xml --cov-report=html --cov-append -vv mne/viz/_brain mne/viz/backends mne/viz/tests/test_evoked.py mne/gui" steps: - - bash: | - set -e - ./tools/setup_xvfb.sh - sudo apt install libegl1 + - bash: ./tools/setup_xvfb.sh displayName: 'Install Ubuntu dependencies' - task: UsePythonVersion@0 inputs: @@ -191,10 +190,10 @@ stages: - bash: | set -e mne sys_info -pd - mne sys_info -pd | grep "^qtpy: .*{PyQt5=.*}$" + mne sys_info -pd | grep "qtpy: .*{PySide6=.*}$" pytest -m "not slowtest" ${TEST_OPTIONS} - python -m pip uninstall -yq PyQt5 PyQt5-sip PyQt5-Qt5 - displayName: 'PyQt5' + python -m pip uninstall -yq PySide6 + displayName: 'PySide6' - bash: | set -e python -m pip install PyQt6 @@ -211,14 +210,15 @@ stages: pytest -m "not slowtest" ${TEST_OPTIONS} python -m pip uninstall -yq PySide2 displayName: 'PySide2' + # PyQt5 leaves cruft behind, so run it last - bash: | set -e - python -m pip install PySide6 + python -m pip install PyQt5 mne sys_info -pd - mne sys_info -pd | grep "qtpy: .*{PySide6=.*}$" + mne sys_info -pd | grep "^qtpy: .*{PyQt5=.*}$" pytest -m "not slowtest" ${TEST_OPTIONS} - python -m pip uninstall -yq PySide6 - displayName: 'PySide6' + python -m pip uninstall -yq PyQt5 PyQt5-sip PyQt5-Qt5 + displayName: 'PyQt5' # Coverage - bash: bash <(curl -s https://codecov.io/bash) displayName: 'Codecov' @@ -257,9 +257,9 @@ stages: PLATFORM: 'x86-64' TEST_MODE: 'conda' PYTHON_VERSION: '3.9' - 3.7 pip: + 3.9 pip: TEST_MODE: 'pip' - PYTHON_VERSION: '3.7' + PYTHON_VERSION: '3.9' 3.10 pip pre: TEST_MODE: 'pip-pre' PYTHON_VERSION: '3.10' diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 40eaa40b446..41c71edb2e8 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -33,6 +33,8 @@ Bugs - Fix bug where ``theme`` was not handled properly in :meth:`mne.io.Raw.plot` (:gh:`10487`, :gh:`10500` by `Mathieu Scheltienne`_ and `Eric Larson`_) +- Rendering issues with recent MESA releases can be avoided by setting the new environment variable``MNE_3D_OPTION_MULTI_SAMPLES=1`` or using :func:`mne.viz.set_3d_options` (:gh:`10513` by `Eric Larson`_) + - Fix behavior for the ``pyvista`` 3D renderer's ``quiver3D`` function so that default arguments plot a glyph in ``arrow`` mode (:gh:`10493` by `Alex Rockhill`_) - Retain epochs metadata when using :func:`mne.channels.combine_channels` (:gh:`10504` by `Clemens Brunner`_) diff --git a/doc/conf.py b/doc/conf.py index ef40a3457d4..25436bb9ef8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -33,6 +33,7 @@ os.environ['_MNE_BROWSER_NO_BLOCK'] = 'true' os.environ['MNE_BROWSER_OVERVIEW_MODE'] = 'hidden' os.environ['MNE_BROWSER_THEME'] = 'light' +os.environ['MNE_3D_OPTION_THEME'] = 'light' # -- Path setup -------------------------------------------------------------- @@ -311,9 +312,9 @@ def __call__(self, gallery_conf, fname, when): except ImportError: vtkPolyData = None # noqa try: - from mne_qt_browser._pg_figure import PyQtGraphBrowser + from mne_qt_browser._pg_figure import MNEQtBrowser except ImportError: - PyQtGraphBrowser = None + MNEQtBrowser = None from mne.viz.backends.renderer import backend _Renderer = backend._Renderer if backend is not None else None reset_warnings(gallery_conf, fname) @@ -332,7 +333,7 @@ def __call__(self, gallery_conf, fname, when): when = f'mne/conf.py:Resetter.__call__:{when}:{fname}' # Support stuff like # MNE_SKIP_INSTANCE_ASSERTIONS="Brain,Plotter,BackgroundPlotter,vtkPolyData,_Renderer" make html_dev-memory # noqa: E501 - # to just test PyQtGraphBrowser + # to just test MNEQtBrowser skips = os.getenv('MNE_SKIP_INSTANCE_ASSERTIONS', '').lower() prefix = '' if skips not in ('true', '1', 'all'): @@ -349,15 +350,15 @@ def __call__(self, gallery_conf, fname, when): _assert_no_instances(vtkPolyData, when) if '_renderer' not in skips: _assert_no_instances(_Renderer, when) - if PyQtGraphBrowser is not None and \ - 'pyqtgraphbrowser' not in skips: + if MNEQtBrowser is not None and \ + 'mneqtbrowser' not in skips: # Ensure any manual fig.close() events get properly handled from mne_qt_browser._pg_figure import QApplication inst = QApplication.instance() if inst is not None: for _ in range(2): inst.processEvents() - _assert_no_instances(PyQtGraphBrowser, when) + _assert_no_instances(MNEQtBrowser, when) # This will overwrite some Sphinx printing but it's useful # for memory timestamps if os.getenv('SG_STAMP_STARTS', '').lower() == 'true': @@ -394,7 +395,7 @@ def __call__(self, gallery_conf, fname, when): import mne_qt_browser _min_ver = _compare_version(mne_qt_browser.__version__, '>=', '0.2') if mne.viz.get_browser_backend() == 'qt' and _min_ver: - scrapers += (mne.viz._scraper._PyQtGraphScraper(),) + scrapers += (mne.viz._scraper._MNEQtBrowserScraper(),) except ImportError: pass @@ -892,6 +893,9 @@ def reset_warnings(gallery_conf, fname): warnings.filterwarnings( 'ignore', message=r'numpy\.ndarray size changed.*', category=RuntimeWarning) + warnings.filterwarnings( + 'ignore', message=r'.*Setting theme=.*6 in qdarkstyle.*', + category=RuntimeWarning) # In case we use np.set_printoptions in any tutorials, we only # want it to affect those: diff --git a/doc/install/advanced.rst b/doc/install/advanced.rst index 45389eb2e13..d5e6bc3de81 100644 --- a/doc/install/advanced.rst +++ b/doc/install/advanced.rst @@ -191,6 +191,12 @@ to force MESA to use modern OpenGL by using this before executing Also, it's possible that different software rending backends might perform better than others, such as using the ``llvmpipe`` backend rather than ``swr``. +In newer MESA (21+), rendering can be incorrect when using MSAA, so consider +setting: + +.. code-block:: console + + export MNE_3D_OPTION_MULTI_SAMPLES=1 MESA also can have trouble with full-screen antialiasing, which you can disable with: diff --git a/doc/install/check_installation.rst b/doc/install/check_installation.rst index b429cdf5595..725867074a2 100644 --- a/doc/install/check_installation.rst +++ b/doc/install/check_installation.rst @@ -27,7 +27,7 @@ MNE-Python and its dependencies. Typical output looks like this:: mne: 0.21.dev0 numpy: 1.19.0.dev0+8dfaa4a {blas=openblas, lapack=openblas} scipy: 1.5.0.dev0+f614064 - matplotlib: 3.2.1 {backend=Qt5Agg} + matplotlib: 3.2.1 {backend=QtAgg} sklearn: 0.22.2.post1 numba: 0.49.0 @@ -37,7 +37,7 @@ MNE-Python and its dependencies. Typical output looks like this:: dipy: 1.1.1 pyvista: 0.25.2 {pyvistaqt=0.1.0} vtk: 9.0.0 - qtpy: 2.0.1 {PyQt5=5.14.1} + qtpy: 2.0.1 {PySide6=6.2.4} .. collapse:: |hand-paper| If you get an error... diff --git a/mne/conftest.py b/mne/conftest.py index fc2c81802ed..df3b4b63f8d 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -440,8 +440,7 @@ def _check_pyqtgraph(request): pytest.skip('Requires mne_qt_browser') else: ver = mne_qt_browser.__version__ - req_pyqt5 = _compare_version(ver, '<=', '0.2.6') - if api != 'PyQt5' and req_pyqt5: + if api != 'PyQt5' and _compare_version(ver, '<=', '0.2.6'): pytest.skip(f'mne_qt_browser {ver} requires PyQt5, API is {api}') diff --git a/mne/utils/config.py b/mne/utils/config.py index 145c55bdd36..d36d3937390 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -70,6 +70,7 @@ def set_memmap_min_size(memmap_min_size): known_config_types = ( 'MNE_3D_OPTION_ANTIALIAS', 'MNE_3D_OPTION_DEPTH_PEELING', + 'MNE_3D_OPTION_MULTI_SAMPLES', 'MNE_3D_OPTION_SMOOTH_SHADING', 'MNE_3D_OPTION_THEME', 'MNE_BROWSE_RAW_SIZE', diff --git a/mne/utils/misc.py b/mne/utils/misc.py index cf0295695d2..5a071b1b89c 100644 --- a/mne/utils/misc.py +++ b/mne/utils/misc.py @@ -366,6 +366,12 @@ def _assert_no_instances(cls, when=''): del r_ else: rep = repr(r)[:100].replace('\n', ' ') + # If it's a __closure__, get more information + if rep.startswith(' # # License: Simplified BSD + +import weakref + from ...utils import logger @@ -54,15 +57,18 @@ class UpdateLUT(object): """Update the LUT.""" def __init__(self, brain=None): - self.brain = brain - self.widgets = {key: list() for key in self.brain.keys} + self.brain = weakref.ref(brain) + self.widgets = {key: list() for key in brain.keys} def __call__(self, fmin=None, fmid=None, fmax=None): """Update the colorbar sliders.""" - self.brain.update_lut(fmin=fmin, fmid=fmid, fmax=fmax) - with self.brain._no_lut_update(f'UpdateLUT {fmin} {fmid} {fmax}'): + brain = self.brain() + if brain is None: + return + brain.update_lut(fmin=fmin, fmid=fmid, fmax=fmax) + with brain._no_lut_update(f'UpdateLUT {fmin} {fmid} {fmax}'): for key in ('fmin', 'fmid', 'fmax'): - value = self.brain._data[key] + value = brain._data[key] logger.debug(f'Updating {key} = {value}') for widget in self.widgets[key]: widget.set_value(value) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 99ca4a21289..6e279496294 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -838,6 +838,35 @@ def test_brain_traces(renderer_interactive_pyvistaqt, hemi, src, tmp_path, assert img.shape[:2] == screenshot_all.shape[:2] +@testing.requires_testing_data +def test_brain_scraper(renderer_interactive_pyvistaqt, brain_gc, tmp_path): + """Test a simple scraping example.""" + pytest.importorskip('sphinx_gallery') + stc = read_source_estimate(fname_stc, subject='sample') + size = (600, 300) + brain = stc.plot(subjects_dir=subjects_dir, + time_viewer=True, show_traces=True, + hemi='split', size=size, views='lat') + fnames = [str(tmp_path / f'temp_{ii}.png') for ii in range(2)] + block_vars = dict(image_path_iterator=iter(fnames), + example_globals=dict(brain=brain)) + block = ('code', '', 1) + gallery_conf = dict(src_dir=str(tmp_path), compress_images=[]) + scraper = _BrainScraper() + rst = scraper(block, block_vars, gallery_conf) + assert brain.plotter is None # closed + assert brain._cleaned + del brain + fname = fnames[0] + assert op.basename(fname) in rst + assert op.isfile(fname) + img = image.imread(fname) + w = img.shape[1] + w0 = size[0] + assert np.isclose(w, w0, atol=10) or \ + np.isclose(w, w0 * 2, atol=10), f'w ∉ {{{w0}, {2 * w0}}}' # HiDPI + + @testing.requires_testing_data @pytest.mark.slowtest def test_brain_linkviewer(renderer_interactive_pyvistaqt, brain_gc): diff --git a/mne/viz/_scraper.py b/mne/viz/_scraper.py index bff5013bf2b..7a8a1d6ce6f 100644 --- a/mne/viz/_scraper.py +++ b/mne/viz/_scraper.py @@ -8,10 +8,10 @@ from .backends._utils import _pixmap_to_ndarray -class _PyQtGraphScraper: +class _MNEQtBrowserScraper: def __repr__(self): - return '' + return '' def __call__(self, block, block_vars, gallery_conf): import mne_qt_browser diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 92d3512f022..ceb6e703573 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -57,7 +57,7 @@ def __init__(self): def _init(self, plotter=None, show=False, title='PyVista Scene', size=(600, 600), shape=(1, 1), background_color='black', smooth_shading=True, off_screen=False, notebook=False, - splash=False): + splash=False, multi_samples=None): self._plotter = plotter self.display = None self.background_color = background_color @@ -71,8 +71,7 @@ def _init(self, plotter=None, show=False, title='PyVista Scene', self.store['shape'] = shape self.store['off_screen'] = off_screen self.store['border'] = False - # multi_samples > 1 is broken on macOS + Intel Iris + volume rendering - self.store['multi_samples'] = 1 if sys.platform == 'darwin' else 4 + self.store['multi_samples'] = multi_samples if not self.notebook: self.store['show'] = show @@ -155,14 +154,20 @@ class _PyVistaRenderer(_AbstractRenderer): def __init__(self, fig=None, size=(600, 600), bgcolor='black', name="PyVista Scene", show=False, shape=(1, 1), - notebook=None, smooth_shading=True, splash=False): + notebook=None, smooth_shading=True, splash=False, + multi_samples=None): from .._3d import _get_3d_option _require_version('pyvista', 'use 3D rendering', '0.32') + multi_samples = _get_3d_option('multi_samples') + # multi_samples > 1 is broken on macOS + Intel Iris + volume rendering + if sys.platform == 'darwin': + multi_samples = 1 figure = PyVistaFigure() figure._init( show=show, title=name, size=size, shape=shape, background_color=bgcolor, notebook=notebook, - smooth_shading=smooth_shading, splash=splash) + smooth_shading=smooth_shading, splash=splash, + multi_samples=multi_samples) self.font_family = "arial" self.tube_n_sides = 20 self.antialias = _get_3d_option('antialias') @@ -676,12 +681,7 @@ def _enable_antialias(self): bad_system = ( sys.platform == 'darwin' or os.getenv('AZURE_CI_WINDOWS', 'false').lower() == 'true') - # MESA (could use GPUInfo / _get_gpu_info here, but it takes - # > 700 ms to make a new window + report capabilities!) - # CircleCI's is: "Mesa 20.0.8 via llvmpipe (LLVM 10.0.0, 256 bits)" - gpu_info = self.plotter.ren_win.ReportCapabilities() - gpu_info = re.findall("OpenGL renderer string:(.+)\n", gpu_info) - bad_system |= 'mesa' in ' '.join(gpu_info).lower().split() + bad_system |= _is_mesa(self.plotter) if not bad_system: for renderer in self._all_renderers: renderer.enable_anti_aliasing() @@ -1151,3 +1151,12 @@ def _disabled_depth_peeling(): yield finally: depth_peeling["enabled"] = depth_peeling_enabled + + +def _is_mesa(plotter): + # MESA (could use GPUInfo / _get_gpu_info here, but it takes + # > 700 ms to make a new window + report capabilities!) + # CircleCI's is: "Mesa 20.0.8 via llvmpipe (LLVM 10.0.0, 256 bits)" + gpu_info = plotter.ren_win.ReportCapabilities() + gpu_info = re.findall("OpenGL renderer string:(.+)\n", gpu_info) + return ' mesa ' in ' '.join(gpu_info).lower().split() diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 3c3f26b40f2..bf63ecf4ce1 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -6,6 +6,7 @@ # License: Simplified BSD from contextlib import contextmanager +import weakref import pyvista from pyvistaqt.plotting import FileDialog @@ -417,9 +418,14 @@ def _tool_bar_add_spacer(self): self._tool_bar.addWidget(spacer) def _tool_bar_add_file_button(self, name, desc, func, *, shortcut=None): - def callback(): + weakself = weakref.ref(self) + + def callback(weakself=weakself): + weakself = weakself() + if weakself is None: + return return FileDialog( - self.plotter.app_window, + weakself.app_window, callback=func, ) diff --git a/mne/viz/backends/tests/test_utils.py b/mne/viz/backends/tests/test_utils.py index 9574ad74217..92f8cce01af 100644 --- a/mne/viz/backends/tests/test_utils.py +++ b/mne/viz/backends/tests/test_utils.py @@ -6,6 +6,7 @@ # License: Simplified BSD from colorsys import rgb_to_hls +from contextlib import nullcontext import numpy as np import pytest @@ -14,6 +15,7 @@ from mne.io import RawArray from mne.viz.backends._utils import (_get_colormap_from_array, _check_color, _qt_is_dark, _pixmap_to_ndarray) +from mne.utils import _check_qt_version def test_get_colormap_from_array(): @@ -54,8 +56,20 @@ def test_theme_colors(pg_backend, theme, monkeypatch, tmp_path): darkdetect = pytest.importorskip('darkdetect') monkeypatch.setenv('_MNE_FAKE_HOME_DIR', str(tmp_path)) monkeypatch.delenv('MNE_BROWSER_THEME', raising=False) + # make it seem like the system is always in light mode + monkeypatch.setattr(darkdetect, 'theme', lambda: 'light') raw = RawArray(np.zeros((1, 1000)), create_info(1, 1000., 'eeg')) - fig = raw.plot(theme=theme) + _, api = _check_qt_version(return_api=True) + if api in ('PyQt6', 'PySide6') and theme == 'dark': + ctx = pytest.warns(RuntimeWarning, match='not yet supported') + return_early = True + else: + ctx = nullcontext() + return_early = False + with ctx: + fig = raw.plot(theme=theme) + if return_early: + return # we could add a ton of conditionals below, but KISS is_dark = _qt_is_dark(fig) if theme == 'dark': assert is_dark, theme diff --git a/mne/viz/tests/test_scraper.py b/mne/viz/tests/test_scraper.py index b4bd3a898a5..2bd5a2e9073 100644 --- a/mne/viz/tests/test_scraper.py +++ b/mne/viz/tests/test_scraper.py @@ -9,7 +9,7 @@ @requires_version('sphinx_gallery') -def test_pg_scraper(raw, pg_backend, tmp_path): +def test_qt_scraper(raw, pg_backend, tmp_path): """Test sphinx-gallery scraping of the browser.""" # make sure there is only one to scrape from old tests fig = raw.plot(group_by='selection') @@ -22,6 +22,6 @@ def test_pg_scraper(raw, pg_backend, tmp_path): image_path_iterator=iter(image_paths)) assert not any(op.isfile(image_path) for image_path in image_paths) assert not getattr(fig, '_scraped', False) - mne.viz._scraper._PyQtGraphScraper()(None, block_vars, gallery_conf) + mne.viz._scraper._MNEQtBrowserScraper()(None, block_vars, gallery_conf) assert all(op.isfile(image_path) for image_path in image_paths) assert fig._scraped diff --git a/mne/viz/tests/test_utils.py b/mne/viz/tests/test_utils.py index c71346c0f37..f63575e5be5 100644 --- a/mne/viz/tests/test_utils.py +++ b/mne/viz/tests/test_utils.py @@ -12,7 +12,7 @@ from mne.viz.utils import (compare_fiff, _fake_click, _compute_scalings, _validate_if_list_of_axes, _get_color_list, _setup_vmin_vmax, centers_to_edges, - _make_event_color_dict) + _make_event_color_dict, concatenate_images) from mne.viz import ClickableImage, add_background_image, mne_analyze_colormap from mne.io import read_raw_fif from mne.event import read_events @@ -180,3 +180,20 @@ def test_event_color_dict(): # test error with pytest.raises(KeyError, match='must be strictly positive, or -1'): _ = _make_event_color_dict({-2: 'r', -1: 'b'}) + + +@pytest.mark.parametrize('axis', (0, 1)) +@pytest.mark.parametrize('b_h', (2, 4)) +@pytest.mark.parametrize('b_w', (3, 5)) +@pytest.mark.parametrize('a_h', (2, 4)) +@pytest.mark.parametrize('a_w', (3, 5)) +def test_concatenate_images(a_w, a_h, b_w, b_h, axis): + """Test that concat with arbitrary sizes works.""" + a = np.zeros((a_h, a_w, 3)) + b = np.zeros((b_h, b_w, 3)) + img = concatenate_images([a, b], axis=axis) + if axis == 0: + want_shape = (a_h + b_h, max(a_w, b_w), 3) + else: + want_shape = (max(a_h, b_h), a_w + b_w, 3) + assert img.shape == want_shape diff --git a/mne/viz/utils.py b/mne/viz/utils.py index c26565707f6..fb90b302717 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2363,6 +2363,8 @@ def concatenate_images(images, axis=0, bgcolor='black', centered=True, The concatenated image. """ n_channels = _ensure_int(n_channels, 'n_channels') + axis = _ensure_int(axis) + _check_option('axis', axis, (0, 1)) _check_option('n_channels', n_channels, (3, 4)) alpha = True if n_channels == 4 else False bgcolor = _to_rgb(bgcolor, name='bgcolor', alpha=alpha) @@ -2378,7 +2380,7 @@ def concatenate_images(images, axis=0, bgcolor='black', centered=True, sec = np.array([0 == axis, 1 == axis]).astype(int) for image in images: shape = image.shape[:-1] - dec = ptr + dec = ptr.copy() dec += ((ret_shape - shape) // 2) * (1 - sec) if centered else 0 ret[dec[0]:dec[0] + shape[0], dec[1]:dec[1] + shape[1], :] = image ptr += shape * sec diff --git a/requirements.txt b/requirements.txt index 59992313e8a..4bc69d9e8e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,10 +9,7 @@ h5io packaging pymatreader qtpy -pyqt5>=5.10,<5.14; platform_system == "Darwin" -pyqt5>=5.10,!=5.15.2,!=5.15.3; platform_system == "Linux" -pyqt5>=5.10,!=5.15.3; platform_system == "Windows" -pyqt5-sip +pyside6 pyobjc-framework-Cocoa>=5.2.0; platform_system=="Darwin" sip scikit-learn diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 588bea0cc89..0f5c855c40a 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -10,7 +10,7 @@ if [ "${TEST_MODE}" == "pip" ]; then elif [ "${TEST_MODE}" == "pip-pre" ]; then python -m pip install --progress-bar off --upgrade pip setuptools wheel python -m pip install --progress-bar off --upgrade --pre --only-binary ":all:" python-dateutil pytz joblib threadpoolctl six cycler kiwisolver pyparsing patsy - python -m pip install --progress-bar off --upgrade --pre --only-binary ":all:" --no-deps --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt5 PyQt5-sip PyQt5-Qt5 + python -m pip install --progress-bar off --upgrade --pre --only-binary ":all:" --no-deps --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 PyQt6-sip PyQt6-Qt6 # SciPy Windows build is missing from conda nightly builds python -m pip install --progress-bar off --upgrade --pre --only-binary ":all:" --no-deps -i "https://pypi.anaconda.org/scipy-wheels-nightly/simple" numpy python -m pip install --progress-bar off --upgrade --pre --only-binary ":all:" --no-deps scipy diff --git a/tools/circleci_dependencies.sh b/tools/circleci_dependencies.sh index d983c84bdd5..564f0576ad0 100755 --- a/tools/circleci_dependencies.sh +++ b/tools/circleci_dependencies.sh @@ -1,17 +1,5 @@ #!/bin/bash -ef -echo "Working around PyQt5 bugs" -# https://github.com/ContinuumIO/anaconda-issues/issues/9190#issuecomment-386508136 -# https://github.com/golemfactory/golem/issues/1019 -sudo apt update -sudo apt install libosmesa6 libglx-mesa0 libopengl0 libglx0 libdbus-1-3 \ - libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 \ - libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 \ - graphviz optipng -if [ ! -f /usr/lib/x86_64-linux-gnu/libxcb-util.so.1 ]; then - sudo ln -s /usr/lib/x86_64-linux-gnu/libxcb-util.so.0 /usr/lib/x86_64-linux-gnu/libxcb-util.so.1 -fi - echo "Installing setuptools and sphinx" python -m pip install --upgrade "pip!=20.3.0" python -m pip install --upgrade --progress-bar off setuptools wheel @@ -25,7 +13,7 @@ else # standard doc build python -m pip install --upgrade --progress-bar off --only-binary "numpy,scipy,matplotlib,pandas,statsmodels" -r requirements.txt -r requirements_testing.txt -r requirements_doc.txt python -m pip uninstall -yq sphinx-gallery mne-qt-browser # TODO: Revert to upstream/main once https://github.com/mne-tools/mne-qt-browser/pull/105 is merged - python -m pip install --upgrade --progress-bar off https://github.com/larsoner/mne-qt-browser/zipball/overview https://github.com/sphinx-gallery/sphinx-gallery/zipball/master https://github.com/pyvista/pyvista/zipball/main https://github.com/pyvista/pyvistaqt/zipball/main + python -m pip install --upgrade --progress-bar off https://github.com/mne-tools/mne-qt-browser/zipball/main https://github.com/sphinx-gallery/sphinx-gallery/zipball/master https://github.com/pyvista/pyvista/zipball/main https://github.com/pyvista/pyvistaqt/zipball/main # deal with comparisons and escapes (https://app.circleci.com/pipelines/github/mne-tools/mne-python/9686/workflows/3fd32b47-3254-4812-8b9a-8bab0d646d18/jobs/32934) python -m pip install --upgrade quantities fi diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 1ebe047e9cd..39e6850176c 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -14,8 +14,8 @@ else echo "Date utils" # https://pip.pypa.io/en/latest/user_guide/#possible-ways-to-reduce-backtracking-occurring pip install $STD_ARGS --pre --only-binary ":all:" python-dateutil pytz joblib threadpoolctl six - echo "PyQt5" - pip install $STD_ARGS --pre --only-binary ":all:" --no-deps --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt5 PyQt5-sip PyQt5-Qt5 + echo "PyQt6" + pip install $STD_ARGS --pre --only-binary ":all:" --no-deps --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 PyQt6-sip PyQt6-Qt6 echo "NumPy/SciPy/pandas etc." pip install $STD_ARGS --pre --only-binary ":all:" --no-deps -i "https://pypi.anaconda.org/scipy-wheels-nightly/simple" numpy scipy pandas scikit-learn statsmodels dipy echo "H5py, pillow, matplotlib" diff --git a/tools/setup_xvfb.sh b/tools/setup_xvfb.sh index 3709c5f2f8d..67d3dc83d01 100755 --- a/tools/setup_xvfb.sh +++ b/tools/setup_xvfb.sh @@ -9,6 +9,7 @@ for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file done +# This also includes the libraries necessary for PyQt5/PyQt6 sudo apt update -sudo apt install -yqq libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libopengl0 +sudo apt install -yqq xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libopengl0 libegl1 /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render -noreset diff --git a/tutorials/inverse/50_beamformer_lcmv.py b/tutorials/inverse/50_beamformer_lcmv.py index 41eed1d3284..4d6a620edca 100644 --- a/tutorials/inverse/50_beamformer_lcmv.py +++ b/tutorials/inverse/50_beamformer_lcmv.py @@ -6,8 +6,8 @@ Source reconstruction using an LCMV beamformer ============================================== -This tutorial gives an overview of the beamformer method -and shows how to reconstruct source activity using an LCMV beamformer. +This tutorial gives an overview of the beamformer method and shows how to +reconstruct source activity using an LCMV beamformer. """ # Authors: Britta Westner # Eric Larson