From 1daad4eeb48e636ebdd5fa6ec8de7a34fd623851 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Mar 2025 03:30:06 -0400 Subject: [PATCH 1/5] add imgui stats overlay, add canvas_kwargs kwarg to Figure --- fastplotlib/layouts/_figure.py | 12 +++++++++++- fastplotlib/layouts/_imgui_figure.py | 25 +++++++++++++++---------- fastplotlib/layouts/_utils.py | 4 ++-- setup.py | 4 ++-- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index e1822eb64..a1bae965e 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -48,6 +48,7 @@ def __init__( controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None, canvas: str | BaseRenderCanvas | pygfx.Texture = None, renderer: pygfx.WgpuRenderer = None, + canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, ): @@ -111,6 +112,9 @@ def __init__( renderer: pygfx.Renderer, optional pygfx renderer instance + canvas_kwargs: dict, optional + kwargs to pass to the canvas + size: (int, int), optional starting size of canvas in absolute pixels, default (500, 300) @@ -163,8 +167,14 @@ def __init__( else: subplot_names = None + if canvas_kwargs is not None: + if size not in canvas_kwargs.keys(): + canvas_kwargs["size"] = size + else: + canvas_kwargs = {"size": size, "max_fps": 60.0, "vsync": True} + canvas, renderer = make_canvas_and_renderer( - canvas, renderer, canvas_kwargs={"size": size} + canvas, renderer, canvas_kwargs=canvas_kwargs ) canvas.add_event_handler(self._fpl_reset_layout, "resize") diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index f6d3da20f..40145fe50 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -6,13 +6,12 @@ import imgui_bundle from imgui_bundle import imgui, icons_fontawesome_6 as fa -from wgpu.utils.imgui import ImguiRenderer +from wgpu.utils.imgui import ImguiRenderer, Stats from rendercanvas import BaseRenderCanvas import pygfx from ._figure import Figure -from ._utils import make_canvas_and_renderer from ..ui import EdgeWindow, SubplotToolbar, StandardRightClickMenu, Popup, GUI_EDGES from ..ui import ColormapPicker @@ -21,8 +20,8 @@ class ImguiFigure(Figure): def __init__( self, shape: tuple[int, int] = (1, 1), - rects=None, - extents=None, + rects: list[tuple | np.ndarray] = None, + extents: list[tuple | np.ndarray] = None, cameras: ( Literal["2d", "3d"] | Iterable[Iterable[Literal["2d", "3d"]]] @@ -42,16 +41,12 @@ def __init__( controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None, canvas: str | BaseRenderCanvas | pygfx.Texture = None, renderer: pygfx.WgpuRenderer = None, + canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, ): self._guis: dict[str, EdgeWindow] = {k: None for k in GUI_EDGES} - canvas, renderer = make_canvas_and_renderer( - canvas, renderer, canvas_kwargs={"size": size} - ) - self._imgui_renderer = ImguiRenderer(renderer.device, canvas) - super().__init__( shape=shape, rects=rects, @@ -62,10 +57,13 @@ def __init__( controllers=controllers, canvas=canvas, renderer=renderer, + canvas_kwargs=canvas_kwargs, size=size, names=names, ) + self._imgui_renderer = ImguiRenderer(self.renderer.device, self.canvas) + fronts_path = str( Path(imgui_bundle.__file__).parent.joinpath( "assets", "fonts", "Font_Awesome_6_Free-Solid-900.otf" @@ -97,6 +95,9 @@ def __init__( self._popups: dict[str, Popup] = {} + self.imgui_show_fps = False + self._stats = Stats(self.renderer.device, self.canvas) + self.register_popup(ColormapPicker) @property @@ -110,7 +111,11 @@ def imgui_renderer(self) -> ImguiRenderer: return self._imgui_renderer def _render(self, draw=False): - super()._render(draw) + if self.imgui_show_fps: + with self._stats: + super()._render(draw) + else: + super()._render(draw) self.imgui_renderer.render() diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 866c26aa3..98a6268f1 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -31,12 +31,12 @@ def make_canvas_and_renderer( """ if canvas is None: - canvas = RenderCanvas(max_fps=60, **canvas_kwargs) + canvas = RenderCanvas(**canvas_kwargs) elif isinstance(canvas, str): import rendercanvas m = importlib.import_module("rendercanvas." + canvas) - canvas = m.RenderCanvas(max_fps=60, **canvas_kwargs) + canvas = m.RenderCanvas(**canvas_kwargs) elif not isinstance(canvas, (BaseRenderCanvas, Texture)): raise TypeError( f"canvas option must either be a valid BaseRenderCanvas implementation, a pygfx Texture" diff --git a/setup.py b/setup.py index 9834884aa..3fb5368d5 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ install_requires = [ "numpy>=1.23.0", - "pygfx>=0.7.0", - "wgpu>=0.18.1", + "pygfx>=0.8.0", + "wgpu>=0.20.0", "cmap>=0.1.3", ] From 1e9731b2e7662d3802c4f9e764d0581aa3746c8b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Mar 2025 03:46:16 -0400 Subject: [PATCH 2/5] update spiral example --- examples/scatter/spinning_spiral.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/scatter/spinning_spiral.py b/examples/scatter/spinning_spiral.py index c032fc1c8..545e726a3 100644 --- a/examples/scatter/spinning_spiral.py +++ b/examples/scatter/spinning_spiral.py @@ -2,7 +2,7 @@ Spinning spiral scatter ======================= -Example of a spinning spiral scatter +Example of a spinning spiral scatter. Runs at 125 fps on an AMD RX 570. """ # test_example = false @@ -12,7 +12,7 @@ import fastplotlib as fpl # number of points -n = 100_000 +n = 1_000_000 # create data in the shape of a spiral phi = np.linspace(0, 30, n) @@ -23,16 +23,27 @@ data = np.column_stack([xs, ys, zs]) -figure = fpl.Figure(cameras="3d", size=(700, 560)) +figure = fpl.Figure( + cameras="3d", + size=(700, 560), + canvas_kwargs={"max_fps": 500, "vsync": False} +) spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", alpha=0.8) +jitter = np.random.normal(scale=0.01, size=n * 3).reshape((n, 3)) + def update(): # rotate around y axis spiral.rotate(0.005, axis="y") + # add small jitter - spiral.data[:] += np.random.normal(scale=0.01, size=n * 3).reshape((n, 3)) + spiral.data[:] += jitter + # shift array to provide a random-sampling effect + # without re-running a random generator on each iteration + jitter[1000:] = jitter[:-1000] + jitter[:1000] = jitter[-1000:] figure.add_animations(update) @@ -51,10 +62,16 @@ def update(): 'maintain_aspect': True, 'depth_range': None } + figure[0, 0].camera.set_state(camera_state) figure[0, 0].axes.visible = False +if fpl.IMGUI: + # show fps with imgui overlay + figure.imgui_show_fps = True + + # NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively # please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": From 1ba4108c9e4b2cfda23124d0725bbf3798a1c2ed Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Mar 2025 04:00:03 -0400 Subject: [PATCH 3/5] random sizes for scatter points --- examples/scatter/spinning_spiral.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/scatter/spinning_spiral.py b/examples/scatter/spinning_spiral.py index 545e726a3..d6e84a6d3 100644 --- a/examples/scatter/spinning_spiral.py +++ b/examples/scatter/spinning_spiral.py @@ -23,14 +23,18 @@ data = np.column_stack([xs, ys, zs]) +# generate some random sizes for the points +sizes = np.abs(np.random.normal(loc=0, scale=1, size=n)) + figure = fpl.Figure( cameras="3d", size=(700, 560), canvas_kwargs={"max_fps": 500, "vsync": False} ) -spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", alpha=0.8) +spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", alpha=0.1, sizes=sizes) +# pre-generate normally distributed data to jitter the points before each render jitter = np.random.normal(scale=0.01, size=n * 3).reshape((n, 3)) @@ -42,6 +46,7 @@ def update(): spiral.data[:] += jitter # shift array to provide a random-sampling effect # without re-running a random generator on each iteration + # generating 1 million normally distributed points takes ~50ms even with SFC64 jitter[1000:] = jitter[:-1000] jitter[:1000] = jitter[-1000:] From ab2f68b91b5c91996a8de4ecc1bfa4b7327e17b5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Mar 2025 04:25:58 -0400 Subject: [PATCH 4/5] reduce n --- examples/scatter/spinning_spiral.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/scatter/spinning_spiral.py b/examples/scatter/spinning_spiral.py index d6e84a6d3..8d77a77ae 100644 --- a/examples/scatter/spinning_spiral.py +++ b/examples/scatter/spinning_spiral.py @@ -2,17 +2,19 @@ Spinning spiral scatter ======================= -Example of a spinning spiral scatter. Runs at 125 fps on an AMD RX 570. +Example of a spinning spiral scatter. + +This example with 1 million points runs at 125 fps on an AMD RX 570. """ # test_example = false -# sphinx_gallery_pygfx_docs = 'animate 10s' +# sphinx_gallery_pygfx_docs = 'animate 15s' import numpy as np import fastplotlib as fpl # number of points -n = 1_000_000 +n = 100_000 # create data in the shape of a spiral phi = np.linspace(0, 30, n) @@ -29,18 +31,18 @@ figure = fpl.Figure( cameras="3d", size=(700, 560), - canvas_kwargs={"max_fps": 500, "vsync": False} + canvas_kwargs={"max_fps": 600, "vsync": False} ) -spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", alpha=0.1, sizes=sizes) +spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", alpha=0.5, sizes=sizes) # pre-generate normally distributed data to jitter the points before each render -jitter = np.random.normal(scale=0.01, size=n * 3).reshape((n, 3)) +jitter = np.random.normal(scale=0.001, size=n * 3).reshape((n, 3)) def update(): # rotate around y axis - spiral.rotate(0.005, axis="y") + spiral.rotate(0.0005, axis="y") # add small jitter spiral.data[:] += jitter From 8a4cab8a350d95a073e0a064c1e0df82b8358a02 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Mar 2025 04:42:06 -0400 Subject: [PATCH 5/5] better spin rate for docs gallery --- examples/scatter/spinning_spiral.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/scatter/spinning_spiral.py b/examples/scatter/spinning_spiral.py index 8d77a77ae..56cdcb906 100644 --- a/examples/scatter/spinning_spiral.py +++ b/examples/scatter/spinning_spiral.py @@ -31,7 +31,7 @@ figure = fpl.Figure( cameras="3d", size=(700, 560), - canvas_kwargs={"max_fps": 600, "vsync": False} + canvas_kwargs={"max_fps": 500, "vsync": False} ) spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", alpha=0.5, sizes=sizes) @@ -42,7 +42,7 @@ def update(): # rotate around y axis - spiral.rotate(0.0005, axis="y") + spiral.rotate(0.005, axis="y") # add small jitter spiral.data[:] += jitter