From 9919b556357069db3e31af256d7ad199264dcaeb Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 13 Mar 2024 14:10:34 +0100 Subject: [PATCH 1/8] Improved GUI selection wip --- fastplotlib/__init__.py | 2 +- fastplotlib/layouts/_frame/_frame.py | 40 ++------------- fastplotlib/layouts/_gridplot.py | 12 ++--- fastplotlib/layouts/_plot.py | 4 +- fastplotlib/layouts/_plot_area.py | 6 +-- fastplotlib/layouts/_subplot.py | 4 +- fastplotlib/layouts/_utils.py | 74 +++------------------------- fastplotlib/utils/gui.py | 49 ++++++++++++++++++ fastplotlib/widgets/image.py | 16 +++--- 9 files changed, 81 insertions(+), 126 deletions(-) create mode 100644 fastplotlib/utils/gui.py diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 27545f0ad..06a9b00f5 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -6,8 +6,8 @@ from .legends import * from .widgets import ImageWidget from .utils import _notebook_print_banner, config +from .utils.gui import run -from wgpu.gui.auto import run from wgpu.backends.wgpu_native import enumerate_adapters diff --git a/fastplotlib/layouts/_frame/_frame.py b/fastplotlib/layouts/_frame/_frame.py index 2b76b8124..219a59082 100644 --- a/fastplotlib/layouts/_frame/_frame.py +++ b/fastplotlib/layouts/_frame/_frame.py @@ -1,41 +1,7 @@ import os -from ._toolbar import ToolBar - from ...graphics import ImageGraphic - -from .._utils import CANVAS_OPTIONS_AVAILABLE - - -class UnavailableOutputContext: - # called when a requested output context is not available - # ex: if trying to force jupyter_rfb canvas but jupyter_rfb is not installed - def __init__(self, context_name, msg): - self.context_name = context_name - self.msg = msg - - def __call__(self, *args, **kwargs): - raise ModuleNotFoundError( - f"The following output context is not available: {self.context_name}\n{self.msg}" - ) - - -# TODO: potentially put all output context and toolbars in their own module and have this determination done at import -if CANVAS_OPTIONS_AVAILABLE["jupyter"]: - from ._jupyter_output import JupyterOutputContext -else: - JupyterOutputContext = UnavailableOutputContext( - "Jupyter", - "You must install fastplotlib using the `'notebook'` option to use this context:\n" - 'pip install "fastplotlib[notebook]"', - ) - -if CANVAS_OPTIONS_AVAILABLE["qt"]: - from ._qt_output import QOutputContext -else: - QtOutput = UnavailableOutputContext( - "Qt", "You must install `PyQt6` to use this output context" - ) +from ._toolbar import ToolBar class Frame: @@ -158,6 +124,8 @@ def show( # return the appropriate OutputContext based on the current canvas if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": + from ._jupyter_output import JupyterOutputContext # noqa - inline import + self._output = JupyterOutputContext( frame=self, make_toolbar=toolbar, @@ -167,6 +135,8 @@ def show( ) elif self.canvas.__class__.__name__ == "QWgpuCanvas": + from ._qt_output import QOutputContext # noqa - inline import + self._output = QOutputContext( frame=self, make_toolbar=toolbar, add_widgets=add_widgets ) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 04046cd01..fa987b661 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -6,7 +6,7 @@ import pygfx -from wgpu.gui.auto import WgpuCanvas +from wgpu.gui import WgpuCanvasBase from ._frame import Frame from ._utils import make_canvas_and_renderer, create_controller, create_camera @@ -22,7 +22,7 @@ def __init__( cameras: Union[str, list, np.ndarray] = "2d", controller_types: Union[str, list, np.ndarray] = None, controller_ids: Union[str, list, np.ndarray] = None, - canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, + canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None, renderer: pygfx.WgpuRenderer = None, size: Tuple[int, int] = (500, 300), names: Union[list, np.ndarray] = None, @@ -219,12 +219,6 @@ def __init__( for cam in cams[1:]: _controller.add_camera(cam) - if canvas is None: - canvas = WgpuCanvas() - - if renderer is None: - renderer = pygfx.renderers.WgpuRenderer(canvas) - self._canvas = canvas self._renderer = renderer @@ -266,7 +260,7 @@ def __init__( Frame.__init__(self) @property - def canvas(self) -> WgpuCanvas: + def canvas(self) -> WgpuCanvasBase: """The canvas associated to this GridPlot""" return self._canvas diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 34027a276..44a880132 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -1,7 +1,7 @@ from typing import * import pygfx -from wgpu.gui.auto import WgpuCanvas +from wgpu.gui import WgpuCanvasBase from ._subplot import Subplot from ._frame import Frame @@ -11,7 +11,7 @@ class Plot(Subplot, Frame, RecordMixin): def __init__( self, - canvas: Union[str, WgpuCanvas] = None, + canvas: Union[str, WgpuCanvasBase] = None, renderer: pygfx.WgpuRenderer = None, camera: Union[str, pygfx.PerspectiveCamera] = "2d", controller: Union[str, pygfx.Controller] = None, diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 08a09baa7..2c93d7e9e 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -7,7 +7,7 @@ import pygfx from pylinalg import vec_transform, vec_unproject -from wgpu.gui.auto import WgpuCanvas +from wgpu.gui import WgpuCanvasBase from ._utils import create_camera, create_controller from ..graphics._base import Graphic @@ -29,7 +29,7 @@ def __init__( camera: Union[pygfx.PerspectiveCamera], controller: Union[pygfx.Controller], scene: pygfx.Scene, - canvas: WgpuCanvas, + canvas: WgpuCanvasBase, renderer: pygfx.WgpuRenderer, name: str = None, ): @@ -122,7 +122,7 @@ def scene(self) -> pygfx.Scene: return self._scene @property - def canvas(self) -> WgpuCanvas: + def canvas(self) -> WgpuCanvasBase: """Canvas associated to the plot area""" return self._canvas diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index e776fddb6..53d2a1400 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -4,7 +4,7 @@ import pygfx -from wgpu.gui.auto import WgpuCanvas +from wgpu.gui import WgpuCanvasBase from ..graphics import TextGraphic from ._utils import make_canvas_and_renderer, create_camera, create_controller @@ -20,7 +20,7 @@ def __init__( parent_dims: Tuple[int, int] = None, camera: Union[str, pygfx.PerspectiveCamera] = "2d", controller: Union[str, pygfx.Controller] = None, - canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, + canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None, renderer: pygfx.WgpuRenderer = None, name: str = None, ): diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 1662f00c5..5ee930b67 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -1,64 +1,15 @@ from typing import * +import importlib import pygfx from pygfx import WgpuRenderer, Texture +from wgpu.gui import WgpuCanvasBase -# default auto-determined canvas -from wgpu.gui.auto import WgpuCanvas -from wgpu.gui.base import WgpuCanvasBase - - -# TODO: this determination can be better -try: - from wgpu.gui.jupyter import JupyterWgpuCanvas -except ImportError: - JupyterWgpuCanvas = False - -try: - import PyQt6 - from wgpu.gui.qt import QWgpuCanvas -except ImportError: - QWgpuCanvas = False - -try: - from wgpu.gui.glfw import GlfwWgpuCanvas -except ImportError: - GlfwWgpuCanvas = False - - -CANVAS_OPTIONS = ["jupyter", "glfw", "qt"] -CANVAS_OPTIONS_AVAILABLE = { - "jupyter": JupyterWgpuCanvas, - "glfw": GlfwWgpuCanvas, - "qt": QWgpuCanvas, -} - - -def auto_determine_canvas(): - try: - ip = get_ipython() - if ip.has_trait("kernel"): - if hasattr(ip.kernel, "app"): - if ip.kernel.app.__class__.__name__ == "QApplication": - return QWgpuCanvas - else: - return JupyterWgpuCanvas - except NameError: - pass - - else: - if CANVAS_OPTIONS_AVAILABLE["qt"]: - return QWgpuCanvas - elif CANVAS_OPTIONS_AVAILABLE["glfw"]: - return GlfwWgpuCanvas - - # We go with the wgpu auto guess - # for example, offscreen canvas etc. - return WgpuCanvas +from ..utils import gui def make_canvas_and_renderer( - canvas: Union[str, WgpuCanvas, Texture, None], renderer: [WgpuRenderer, None] + canvas: Union[str, WgpuCanvasBase, Texture, None], renderer: [WgpuRenderer, None] ): """ Parses arguments and returns the appropriate canvas and renderer instances @@ -66,23 +17,14 @@ def make_canvas_and_renderer( """ if canvas is None: - Canvas = auto_determine_canvas() - canvas = Canvas(max_fps=60) - + canvas = gui.WgpuCanvas(max_fps=60) elif isinstance(canvas, str): - if canvas not in CANVAS_OPTIONS: - raise ValueError(f"str canvas argument must be one of: {CANVAS_OPTIONS}") - elif not CANVAS_OPTIONS_AVAILABLE[canvas]: - raise ImportError( - f"The {canvas} framework is not installed for using this canvas" - ) - else: - canvas = CANVAS_OPTIONS_AVAILABLE[canvas](max_fps=60) - + m = importlib.import_module("wgpu.gui." + canvas) + canvas = m.WgpuCanvas(max_fps=60) elif not isinstance(canvas, (WgpuCanvasBase, Texture)): raise ValueError( f"canvas option must either be a valid WgpuCanvas implementation, a pygfx Texture" - f" or a str from the following options: {CANVAS_OPTIONS}" + f" or a str with the wgpu gui backend name." ) if renderer is None: diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py new file mode 100644 index 000000000..1675e5088 --- /dev/null +++ b/fastplotlib/utils/gui.py @@ -0,0 +1,49 @@ +import sys +import importlib + + +def _is_jupyter(): + """Determine whether the user is executing in a Jupyter Notebook / Lab.""" + try: + ip = get_ipython() + if ip.has_trait("kernel"): + return True + else: + return False + except NameError: + return False + + +IS_JUPYTER = _is_jupyter() + +# Triage. Ultimately, we let wgpu-py decide, but we can prime things a bit to +# create our own preferred order. If we're in Jupyter, wgpu will prefer and +# select Jupyter, so no no action needed. If one of the GUI backends is already +# imported, this is likely because the user want to use that one, so we should +# honor that. Otherwise, we try importing the GUI backends in the order that +# fastplotlib prefers. When wgpu-py loads the gui.auto, it will see that the +# respective lib has been imported and then prefer the corresponding backend. + +# A list of wgpu GUI backend libs, in the order preferred by fpl +gui_backend_libs = ["PyQt6", "PySide6", "PyQt6", "PySide2", "PyQt5", "glfw"] + +already_imported = [libname for libname in gui_backend_libs if libname in sys.modules] +if not IS_JUPYTER and not already_imported: + for libname in gui_backend_libs: + try: + importlib.import_module(libname) + except Exception: + pass + else: + break + +from wgpu.gui.auto import WgpuCanvas, run + +# Get the name of the backend ('qt', 'glfw', 'jupyter') +GUI_BACKEND = WgpuCanvas.__module__.split(".")[-1] + +if GUI_BACKEND == "qt": + # create and store ref to qt app + from wgpu.gui.qt import get_app + + _qt_app = get_app() diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 0da1bb520..9412f7cc5 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -8,14 +8,6 @@ from ..graphics import ImageGraphic from ..utils import calculate_gridshape from .histogram_lut import HistogramLUT -from ..layouts._utils import CANVAS_OPTIONS_AVAILABLE - - -if CANVAS_OPTIONS_AVAILABLE["jupyter"]: - from ..layouts._frame._ipywidget_toolbar import IpywidgetImageWidgetToolbar - -if CANVAS_OPTIONS_AVAILABLE["qt"]: - from ..layouts._frame._qt_toolbar import QToolbarImageWidget DEFAULT_DIMS_ORDER = { @@ -927,9 +919,17 @@ def show( ImageWidget just uses the Gridplot output context """ if self.gridplot.canvas.__class__.__name__ == "JupyterWgpuCanvas": + from ..layouts._frame._ipywidget_toolbar import ( + IpywidgetImageWidgetToolbar, + ) # noqa - inline import + self._image_widget_toolbar = IpywidgetImageWidgetToolbar(self) elif self.gridplot.canvas.__class__.__name__ == "QWgpuCanvas": + from ..layouts._frame._qt_toolbar import ( + QToolbarImageWidget, + ) # noqa - inline import + self._image_widget_toolbar = QToolbarImageWidget(self) self._output = self.gridplot.show( From 38369d8bd48833a623c12fa4f6d8d3c9e751738a Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 13 Mar 2024 14:34:51 +0100 Subject: [PATCH 2/8] Support all modern qt libs --- fastplotlib/layouts/_frame/_qt_output.py | 3 +-- fastplotlib/layouts/_frame/_qt_toolbar.py | 3 +-- fastplotlib/layouts/_frame/_qtoolbar_template.py | 5 ++--- fastplotlib/utils/gui.py | 11 ++++++++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/fastplotlib/layouts/_frame/_qt_output.py b/fastplotlib/layouts/_frame/_qt_output.py index e8be2d050..d7e7f2612 100644 --- a/fastplotlib/layouts/_frame/_qt_output.py +++ b/fastplotlib/layouts/_frame/_qt_output.py @@ -1,5 +1,4 @@ -from PyQt6 import QtWidgets - +from ...utils.gui import QtWidgets from ._qt_toolbar import QToolbar diff --git a/fastplotlib/layouts/_frame/_qt_toolbar.py b/fastplotlib/layouts/_frame/_qt_toolbar.py index 4ee073701..d62994c2d 100644 --- a/fastplotlib/layouts/_frame/_qt_toolbar.py +++ b/fastplotlib/layouts/_frame/_qt_toolbar.py @@ -4,8 +4,7 @@ import traceback from typing import * -from PyQt6 import QtWidgets, QtCore - +from ...utils.gui import QtCore, QtWidgets from ...graphics.selectors import PolygonSelector from ._toolbar import ToolBar from ._qtoolbar_template import Ui_QToolbar diff --git a/fastplotlib/layouts/_frame/_qtoolbar_template.py b/fastplotlib/layouts/_frame/_qtoolbar_template.py index a8a1c6f86..d2311c595 100644 --- a/fastplotlib/layouts/_frame/_qtoolbar_template.py +++ b/fastplotlib/layouts/_frame/_qtoolbar_template.py @@ -5,8 +5,7 @@ # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. - -from PyQt6 import QtCore, QtGui, QtWidgets +from ...utils.gui import QtGui, QtCore, QtWidgets class Ui_QToolbar(object): @@ -30,7 +29,7 @@ def setupUi(self, QToolbar): self.maintain_aspect_button = QtWidgets.QPushButton(parent=QToolbar) font = QtGui.QFont() font.setBold(True) - font.setWeight(75) + font.setWeight(QtGui.QFont.Weight.Bold) self.maintain_aspect_button.setFont(font) self.maintain_aspect_button.setCheckable(True) self.maintain_aspect_button.setObjectName("maintain_aspect_button") diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py index 1675e5088..9630edeef 100644 --- a/fastplotlib/utils/gui.py +++ b/fastplotlib/utils/gui.py @@ -25,7 +25,7 @@ def _is_jupyter(): # respective lib has been imported and then prefer the corresponding backend. # A list of wgpu GUI backend libs, in the order preferred by fpl -gui_backend_libs = ["PyQt6", "PySide6", "PyQt6", "PySide2", "PyQt5", "glfw"] +gui_backend_libs = ["PySide6", "PyQt6", "PySide2", "PyQt5", "glfw"] already_imported = [libname for libname in gui_backend_libs if libname in sys.modules] if not IS_JUPYTER and not already_imported: @@ -43,7 +43,12 @@ def _is_jupyter(): GUI_BACKEND = WgpuCanvas.__module__.split(".")[-1] if GUI_BACKEND == "qt": - # create and store ref to qt app - from wgpu.gui.qt import get_app + from wgpu.gui.qt import get_app, libname + # create and store ref to qt app _qt_app = get_app() + + # Import submodules of PySide6/PyQt6/PySid2/PyQt5 + QtCore = importlib.import_module(".QtCore", libname) + QtGui = importlib.import_module(".QtGui", libname) + QtWidgets = importlib.import_module(".QtWidgets", libname) From 777cd0e6cbce94d172245766aecdd4a19d4e015f Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 13 Mar 2024 14:37:30 +0100 Subject: [PATCH 3/8] comment --- fastplotlib/utils/gui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py index 9630edeef..059385978 100644 --- a/fastplotlib/utils/gui.py +++ b/fastplotlib/utils/gui.py @@ -49,6 +49,8 @@ def _is_jupyter(): _qt_app = get_app() # Import submodules of PySide6/PyQt6/PySid2/PyQt5 + # For the way that fpl uses Qt, the supported Qt libs seems compatible enough. + # If necessary we can do some qtpy-like monkey-patching here. QtCore = importlib.import_module(".QtCore", libname) QtGui = importlib.import_module(".QtGui", libname) QtWidgets = importlib.import_module(".QtWidgets", libname) From f961fad3cf6a043eb0f1eed60d305a83d383968a Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 13 Mar 2024 15:03:06 +0100 Subject: [PATCH 4/8] make the ipywidgets imports conditional --- fastplotlib/graphics/selectors/_linear.py | 16 +++++++--------- .../graphics/selectors/_linear_region.py | 18 ++++++++---------- fastplotlib/utils/_gpu_info.py | 16 ++++++---------- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index ff617c5e3..886ccbaaf 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -3,21 +3,19 @@ from numbers import Real import numpy as np - import pygfx -try: - import ipywidgets - - HAS_IPYWIDGETS = True -except (ImportError, ModuleNotFoundError): - HAS_IPYWIDGETS = False - +from ...utils.gui import IS_JUPYTER from .._base import Graphic, GraphicCollection from .._features._selection_features import LinearSelectionFeature from ._base_selector import BaseSelector +if IS_JUPYTER: + # If using the jupyter backend, user has jupyter_rfb, and thus also ipywidgets + import ipywidgets + + class LinearSelector(BaseSelector): @property def limits(self) -> Tuple[float, float]: @@ -240,7 +238,7 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): """ - if not HAS_IPYWIDGETS: + if not IS_JUPYTER: raise ImportError( "Must installed `ipywidgets` to use `make_ipywidget_slider()`" ) diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 4ffbd2cc2..b88174ddb 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -1,20 +1,18 @@ from typing import * from numbers import Real -try: - import ipywidgets - - HAS_IPYWIDGETS = True -except (ImportError, ModuleNotFoundError): - HAS_IPYWIDGETS = False - import numpy as np - import pygfx +from ...utils.gui import IS_JUPYTER from .._base import Graphic, GraphicCollection -from ._base_selector import BaseSelector from .._features._selection_features import LinearRegionSelectionFeature +from ._base_selector import BaseSelector + + +if IS_JUPYTER: + # If using the jupyter backend, user has jupyter_rfb, and thus also ipywidgets + import ipywidgets class LinearRegionSelector(BaseSelector): @@ -390,7 +388,7 @@ def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs): """ - if not HAS_IPYWIDGETS: + if not IS_JUPYTER: raise ImportError( "Must installed `ipywidgets` to use `make_ipywidget_slider()`" ) diff --git a/fastplotlib/utils/_gpu_info.py b/fastplotlib/utils/_gpu_info.py index 93e95d281..5e55e6de6 100644 --- a/fastplotlib/utils/_gpu_info.py +++ b/fastplotlib/utils/_gpu_info.py @@ -3,19 +3,15 @@ from wgpu.backends.wgpu_native import enumerate_adapters from wgpu.utils import get_default_device -try: - ip = get_ipython() +from .gui import IS_JUPYTER + + +NOTEBOOK = False +if IS_JUPYTER: from ipywidgets import Image - from wgpu.gui.jupyter import JupyterWgpuCanvas -except (NameError, ModuleNotFoundError, ImportError): - NOTEBOOK = False -else: from IPython.display import display - if ip.has_trait("kernel") and (JupyterWgpuCanvas is not False): - NOTEBOOK = True - else: - NOTEBOOK = False + NOTEBOOK = get_ipython().has_trait("kernel") def _notebook_print_banner(): From eba37515021f23d953355d02a1509355e75aa9a0 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 29 Mar 2024 12:18:38 +0100 Subject: [PATCH 5/8] Adjust for new wgpu --- docs/source/user_guide/gpu.rst | 39 ++++---------- fastplotlib/__init__.py | 7 ++- fastplotlib/utils/__init__.py | 1 - fastplotlib/utils/_gpu_info.py | 62 ---------------------- fastplotlib/utils/gui.py | 96 ++++++++++++++++++++++++---------- 5 files changed, 81 insertions(+), 124 deletions(-) delete mode 100644 fastplotlib/utils/_gpu_info.py diff --git a/docs/source/user_guide/gpu.rst b/docs/source/user_guide/gpu.rst index 006f78872..18fb81cdf 100644 --- a/docs/source/user_guide/gpu.rst +++ b/docs/source/user_guide/gpu.rst @@ -33,39 +33,18 @@ View available GPU You can view all GPUs that are available to ``WGPU`` like this:: - from wgpu.backends.wgpu_native import enumerate_adapters - from pprint import pprint + import wgpu - for adapter in enumerate_adapters(): - pprint(adapter.request_adapter_info()) + for adapter in wgpu.gpu.enumerate_adapters(): + print(adapter.summary) For example, on a Thinkpad AMD laptop with a dedicated nvidia GPU this returns:: - {'adapter_type': 'IntegratedGPU', - 'architecture': '', - 'backend_type': 'Vulkan', - 'description': 'Mesa 22.3.6', - 'device': 'AMD Radeon Graphics (RADV REMBRANDT)', - 'vendor': 'radv'} - {'adapter_type': 'DiscreteGPU', - 'architecture': '', - 'backend_type': 'Vulkan', - 'description': '535.129.03', - 'device': 'NVIDIA T1200 Laptop GPU', - 'vendor': 'NVIDIA'} - {'adapter_type': 'CPU', - 'architecture': '', - 'backend_type': 'Vulkan', - 'description': 'Mesa 22.3.6 (LLVM 15.0.6)', - 'device': 'llvmpipe (LLVM 15.0.6, 256 bits)', - 'vendor': 'llvmpipe'} - {'adapter_type': 'Unknown', - 'architecture': '', - 'backend_type': 'OpenGL', - 'description': '', - 'device': 'AMD Radeon Graphics (rembrandt, LLVM 15.0.6, DRM 3.52, ' - '6.4.0-0.deb12.2-amd64)', - 'vendor': ''} + AMD Radeon Graphics (RADV REMBRANDT) (IntegratedGPU) on Vulkan + NVIDIA T1200 Laptop GPU (DiscreteGPU) on Vulkan + llvmpipe (LLVM 15.0.6, 256 bits) (CPU) on Vulkan + AMD Radeon Graphics (rembrandt, LLVM 15.0.6, DRM 3.52, 6.4.0-0.deb12.2-amd64) (Unknown) on OpenGL + GPU currently in use -------------------- @@ -78,5 +57,5 @@ If you want to know the GPU that a current plot is using you can check the adapt plot.show() # GPU that is currently in use by the renderer - plot.renderer.device.adapter.request_adapter_info() + print(plot.renderer.device.adapter.summary) diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 06a9b00f5..33db8c79d 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -5,21 +5,20 @@ from .graphics.selectors import * from .legends import * from .widgets import ImageWidget -from .utils import _notebook_print_banner, config +from .utils import config from .utils.gui import run -from wgpu.backends.wgpu_native import enumerate_adapters +import wgpu with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: __version__ = f.read().split("\n")[0] -adapters = [a.request_adapter_info() for a in enumerate_adapters()] +adapters = [a.summary for a in wgpu.gpu.enumerate_adapters()] if len(adapters) < 1: raise IndexError("No WGPU adapters found, fastplotlib will not work.") -_notebook_print_banner() __all__ = [ "Plot", diff --git a/fastplotlib/utils/__init__.py b/fastplotlib/utils/__init__.py index 305af90a8..6759b2497 100644 --- a/fastplotlib/utils/__init__.py +++ b/fastplotlib/utils/__init__.py @@ -2,7 +2,6 @@ from .functions import * -from ._gpu_info import _notebook_print_banner @dataclass diff --git a/fastplotlib/utils/_gpu_info.py b/fastplotlib/utils/_gpu_info.py deleted file mode 100644 index 5e55e6de6..000000000 --- a/fastplotlib/utils/_gpu_info.py +++ /dev/null @@ -1,62 +0,0 @@ -from pathlib import Path - -from wgpu.backends.wgpu_native import enumerate_adapters -from wgpu.utils import get_default_device - -from .gui import IS_JUPYTER - - -NOTEBOOK = False -if IS_JUPYTER: - from ipywidgets import Image - from IPython.display import display - - NOTEBOOK = get_ipython().has_trait("kernel") - - -def _notebook_print_banner(): - if NOTEBOOK is False: - return - - logo_path = Path(__file__).parent.parent.joinpath( - "assets", "fastplotlib_face_logo.png" - ) - - with open(logo_path, "rb") as f: - logo_data = f.read() - - image = Image(value=logo_data, format="png", width=300, height=55) - - display(image) - - # print logo and adapter info - adapters = [a for a in enumerate_adapters()] - adapters_info = [a.request_adapter_info() for a in adapters] - - ix_default = adapters_info.index( - get_default_device().adapter.request_adapter_info() - ) - - if len(adapters) > 0: - print("Available devices:") - - for ix, adapter in enumerate(adapters_info): - atype = adapter["adapter_type"] - backend = adapter["backend_type"] - driver = adapter["description"] - device = adapter["device"] - - if atype == "DiscreteGPU" and backend != "OpenGL": - charactor = chr(0x2705) - elif atype == "IntegratedGPU" and backend != "OpenGL": - charactor = chr(0x0001FBC4) - else: - charactor = chr(0x2757) - - if ix == ix_default: - default = " (default) " - else: - default = " " - - output_str = f"{charactor}{default}| {device} | {atype} | {backend} | {driver}" - print(output_str) diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py index 059385978..792f8f7bc 100644 --- a/fastplotlib/utils/gui.py +++ b/fastplotlib/utils/gui.py @@ -1,48 +1,45 @@ import sys import importlib +from pathlib import Path +import wgpu -def _is_jupyter(): - """Determine whether the user is executing in a Jupyter Notebook / Lab.""" - try: - ip = get_ipython() - if ip.has_trait("kernel"): - return True - else: - return False - except NameError: - return False - +from . import _notebook_print_banner -IS_JUPYTER = _is_jupyter() # Triage. Ultimately, we let wgpu-py decide, but we can prime things a bit to -# create our own preferred order. If we're in Jupyter, wgpu will prefer and -# select Jupyter, so no no action needed. If one of the GUI backends is already -# imported, this is likely because the user want to use that one, so we should -# honor that. Otherwise, we try importing the GUI backends in the order that -# fastplotlib prefers. When wgpu-py loads the gui.auto, it will see that the -# respective lib has been imported and then prefer the corresponding backend. - -# A list of wgpu GUI backend libs, in the order preferred by fpl -gui_backend_libs = ["PySide6", "PyQt6", "PySide2", "PyQt5", "glfw"] - -already_imported = [libname for libname in gui_backend_libs if libname in sys.modules] -if not IS_JUPYTER and not already_imported: - for libname in gui_backend_libs: +# create our own preferred order, by importing a Qt lib. But we only do this +# if no GUI has been imported yet. + +# Qt libs that we will try to import +qt_libs = ["PySide6", "PyQt6", "PySide2", "PyQt5"] + +# Other known libs that, if imported, we should probably not try to force qt +other_libs = ["glfw", "wx", "ipykernel"] + +already_imported = [name for name in (qt_libs + other_libs) if name in sys.modules] +if not already_imported: + for name in qt_libs: try: - importlib.import_module(libname) + importlib.import_module(name) except Exception: pass else: break +# Let wgpu do the auto gui selection from wgpu.gui.auto import WgpuCanvas, run # Get the name of the backend ('qt', 'glfw', 'jupyter') GUI_BACKEND = WgpuCanvas.__module__.split(".")[-1] -if GUI_BACKEND == "qt": +IS_JUPYTER = False + +if GUI_BACKEND == "jupyter": + IS_JUPYTER = True + _notebook_print_banner() + +elif GUI_BACKEND == "qt": from wgpu.gui.qt import get_app, libname # create and store ref to qt app @@ -54,3 +51,48 @@ def _is_jupyter(): QtCore = importlib.import_module(".QtCore", libname) QtGui = importlib.import_module(".QtGui", libname) QtWidgets = importlib.import_module(".QtWidgets", libname) + + +def _notebook_print_banner(): + + from ipywidgets import Image + from IPython.display import display + + logo_path = Path(__file__).parent.parent.joinpath( + "assets", "fastplotlib_face_logo.png" + ) + + with open(logo_path, "rb") as f: + logo_data = f.read() + + image = Image(value=logo_data, format="png", width=300, height=55) + + display(image) + + # print logo and adapter info + adapters = [a for a in wgpu.gui.enumerate_adapters()] + adapters_info = [a.request_adapter_info() for a in adapters] + + if len(adapters) > 0: + print("Available devices:") + + for ix, adapter in enumerate(adapters_info): + atype = adapter["adapter_type"] + backend = adapter["backend_type"] + driver = adapter["description"] + device = adapter["device"] + + if atype == "DiscreteGPU" and backend != "OpenGL": + charactor = chr(0x2705) + elif atype == "IntegratedGPU" and backend != "OpenGL": + charactor = chr(0x0001FBC4) + else: + charactor = chr(0x2757) + + if ix == 0: + default = " (default) " + else: + default = " " + + output_str = f"{charactor}{default}| {device} | {atype} | {backend} | {driver}" + print(output_str) From 8f4a4bc8619da8fac182691cbfc459877e3b25a4 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 29 Mar 2024 12:20:00 +0100 Subject: [PATCH 6/8] set min wgpu version --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a06a879a5..e8f2613d9 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ install_requires = [ "numpy>=1.23.0", + "wgpu>=0.15.1", "pygfx>=0.1.14", ] From f21f7aab58a55882937a45644526328fe6a5e7a1 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 29 Mar 2024 12:27:57 +0100 Subject: [PATCH 7/8] fixes --- fastplotlib/utils/gui.py | 51 +++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py index 792f8f7bc..a9f153061 100644 --- a/fastplotlib/utils/gui.py +++ b/fastplotlib/utils/gui.py @@ -4,12 +4,13 @@ import wgpu -from . import _notebook_print_banner +# --- Prepare -# Triage. Ultimately, we let wgpu-py decide, but we can prime things a bit to -# create our own preferred order, by importing a Qt lib. But we only do this -# if no GUI has been imported yet. + +# Ultimately, we let wgpu-py decide, but we can prime things a bit to create our +# own preferred order, by importing a Qt lib. But we only do this if no GUI has +# been imported yet. # Qt libs that we will try to import qt_libs = ["PySide6", "PyQt6", "PySide2", "PyQt5"] @@ -27,30 +28,19 @@ else: break + +# --- Triage + + # Let wgpu do the auto gui selection from wgpu.gui.auto import WgpuCanvas, run # Get the name of the backend ('qt', 'glfw', 'jupyter') GUI_BACKEND = WgpuCanvas.__module__.split(".")[-1] +IS_JUPYTER = GUI_BACKEND == "jupyter" -IS_JUPYTER = False - -if GUI_BACKEND == "jupyter": - IS_JUPYTER = True - _notebook_print_banner() - -elif GUI_BACKEND == "qt": - from wgpu.gui.qt import get_app, libname - - # create and store ref to qt app - _qt_app = get_app() - # Import submodules of PySide6/PyQt6/PySid2/PyQt5 - # For the way that fpl uses Qt, the supported Qt libs seems compatible enough. - # If necessary we can do some qtpy-like monkey-patching here. - QtCore = importlib.import_module(".QtCore", libname) - QtGui = importlib.import_module(".QtGui", libname) - QtWidgets = importlib.import_module(".QtWidgets", libname) +# --- Some backend-specific preparations def _notebook_print_banner(): @@ -70,7 +60,7 @@ def _notebook_print_banner(): display(image) # print logo and adapter info - adapters = [a for a in wgpu.gui.enumerate_adapters()] + adapters = [a for a in wgpu.gpu.enumerate_adapters()] adapters_info = [a.request_adapter_info() for a in adapters] if len(adapters) > 0: @@ -96,3 +86,20 @@ def _notebook_print_banner(): output_str = f"{charactor}{default}| {device} | {atype} | {backend} | {driver}" print(output_str) + + +if GUI_BACKEND == "jupyter": + _notebook_print_banner() + +elif GUI_BACKEND == "qt": + from wgpu.gui.qt import get_app, libname + + # create and store ref to qt app + _qt_app = get_app() + + # Import submodules of PySide6/PyQt6/PySid2/PyQt5 + # For the way that fpl uses Qt, the supported Qt libs seems compatible enough. + # If necessary we can do some qtpy-like monkey-patching here. + QtCore = importlib.import_module(".QtCore", libname) + QtGui = importlib.import_module(".QtGui", libname) + QtWidgets = importlib.import_module(".QtWidgets", libname) From 45fe01d7bfbe5147bc5c3db88795861bd759c2f6 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 29 Mar 2024 12:34:14 +0100 Subject: [PATCH 8/8] top might not necessarily the default --- fastplotlib/utils/gui.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py index a9f153061..b59c7799b 100644 --- a/fastplotlib/utils/gui.py +++ b/fastplotlib/utils/gui.py @@ -63,6 +63,9 @@ def _notebook_print_banner(): adapters = [a for a in wgpu.gpu.enumerate_adapters()] adapters_info = [a.request_adapter_info() for a in adapters] + default_adapter_info = wgpu.gpu.request_adapter().request_adapter_info() + default_ix = adapters_info.index(default_adapter_info) + if len(adapters) > 0: print("Available devices:") @@ -79,7 +82,7 @@ def _notebook_print_banner(): else: charactor = chr(0x2757) - if ix == 0: + if ix == default_ix: default = " (default) " else: default = " "