From 9e0e3a81cf43d3bc2b957413b2660001433dc3e3 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Mar 2025 14:49:47 -0500 Subject: [PATCH 1/4] implemenet block_reentrance decorator --- fastplotlib/graphics/_features/_base.py | 29 +++++++++++++++++++ fastplotlib/graphics/_features/_common.py | 7 ++++- fastplotlib/graphics/_features/_image.py | 8 ++++- .../graphics/_features/_positions_graphics.py | 11 ++++++- .../graphics/_features/_selection_features.py | 7 +++-- fastplotlib/graphics/_features/_text.py | 7 ++++- 6 files changed, 63 insertions(+), 6 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 1612414a1..cca75cac2 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -1,5 +1,6 @@ from warnings import warn from typing import Any, Literal +from traceback import format_exc import numpy as np from numpy.typing import NDArray @@ -53,6 +54,8 @@ def __init__(self, **kwargs): self._event_handlers = list() self._block_events = False + self._reentrant_block: bool = False + @property def value(self) -> Any: """Graphic Feature value, must be implemented in subclass""" @@ -316,3 +319,29 @@ def __len__(self): def __repr__(self): return f"{self.__class__.__name__} buffer data:\n" f"{self.value.__repr__()}" + + +def block_reentrance(set_value): + # decorator to block re-entrant set_value methods + # useful when creating complex, circular, bidirectional event graphics + def wrap(self: GraphicFeature, graphic_or_key, value): + """ + wraps GraphicFeature.set_value + + self: GraphicFeature instance + + graphic_or_key: graphic, or key if a BufferManager + + value: the value passed to set_value() + """ + if self._reentrant_block: + return + try: + self._reentrant_block = True + set_value(self, graphic_or_key, value) + except Exception as e: + raise e(format_exc()) + finally: + self._reentrant_block = False + + return wrap diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/_features/_common.py index fe32a485f..e9c49a475 100644 --- a/fastplotlib/graphics/_features/_common.py +++ b/fastplotlib/graphics/_features/_common.py @@ -1,6 +1,6 @@ import numpy as np -from ._base import GraphicFeature, FeatureEvent +from ._base import GraphicFeature, FeatureEvent, block_reentrance class Name(GraphicFeature): @@ -14,6 +14,7 @@ def __init__(self, value: str): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): if not isinstance(value, str): raise TypeError("`Graphic` name must be of type ") @@ -44,6 +45,7 @@ def _validate(self, value): def value(self) -> np.ndarray: return self._value + @block_reentrance def set_value(self, graphic, value: np.ndarray | list | tuple): self._validate(value) @@ -74,6 +76,7 @@ def _validate(self, value): def value(self) -> np.ndarray: return self._value + @block_reentrance def set_value(self, graphic, value: np.ndarray | list | tuple): self._validate(value) @@ -96,6 +99,7 @@ def __init__(self, value: bool): def value(self) -> bool: return self._value + @block_reentrance def set_value(self, graphic, value: bool): graphic.world_object.visible = value self._value = value @@ -117,6 +121,7 @@ def __init__(self, value: bool): def value(self) -> bool: return self._value + @block_reentrance def set_value(self, graphic, value: bool): self._value = value event = FeatureEvent(type="deleted", info={"value": value}) diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index b67bf1cd4..c0e2b28d2 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -5,7 +5,7 @@ import numpy as np import pygfx -from ._base import GraphicFeature, FeatureEvent +from ._base import GraphicFeature, FeatureEvent, block_reentrance from ...utils import ( make_colors, @@ -135,6 +135,7 @@ def __next__(self) -> tuple[pygfx.Texture, tuple[int, int], tuple[slice, slice]] def __getitem__(self, item): return self.value[item] + @block_reentrance def __setitem__(self, key, value): self.value[key] = value @@ -159,6 +160,7 @@ def __init__(self, value: float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float): vmax = graphic._material.clim[1] graphic._material.clim = (value, vmax) @@ -179,6 +181,7 @@ def __init__(self, value: float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float): vmin = graphic._material.clim[0] graphic._material.clim = (vmin, value) @@ -200,6 +203,7 @@ def __init__(self, value: str): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): new_colors = make_colors(256, value) graphic._material.map.texture.data[:] = new_colors @@ -226,6 +230,7 @@ def _validate(self, value): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): self._validate(value) @@ -254,6 +259,7 @@ def _validate(self, value): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): self._validate(value) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index c4e153a31..78e53f545 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any import numpy as np import pygfx @@ -11,6 +11,7 @@ BufferManager, FeatureEvent, to_gpu_supported_dtype, + block_reentrance, ) from .utils import parse_colors @@ -58,6 +59,7 @@ def __init__( super().__init__(data=data, isolated_buffer=isolated_buffer) + @block_reentrance def __setitem__( self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], @@ -155,6 +157,7 @@ def __init__( def value(self) -> pygfx.Color: return self._value + @block_reentrance def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Color): value = pygfx.Color(value) graphic.world_object.material.color = value @@ -174,6 +177,7 @@ def __init__(self, value: int | float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float | int): graphic.world_object.material.size = float(value) self._value = value @@ -192,6 +196,7 @@ def __init__(self, value: str): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): if "Line" in graphic.world_object.material.__class__.__name__: graphic.world_object.material.thickness_space = value @@ -243,6 +248,7 @@ def _fix_data(self, data): return to_gpu_supported_dtype(data) + @block_reentrance def __setitem__( self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], @@ -318,6 +324,7 @@ def _fix_sizes( return sizes + @block_reentrance def __setitem__( self, key: int | slice | np.ndarray[int | bool] | list[int | bool], @@ -344,6 +351,7 @@ def __init__(self, value: float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float): graphic.world_object.material.thickness = value self._value = value @@ -392,6 +400,7 @@ def __init__( # set vertex colors from cmap self._vertex_colors[:] = colors + @block_reentrance def __setitem__(self, key: slice, cmap_name): if not isinstance(key, slice): raise TypeError( diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index c385f820f..c157023b4 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -1,9 +1,9 @@ -from typing import Sequence, Tuple +from typing import Sequence import numpy as np from ...utils import mesh_masks -from ._base import GraphicFeature, FeatureEvent +from ._base import GraphicFeature, FeatureEvent, block_reentrance class LinearSelectionFeature(GraphicFeature): @@ -54,6 +54,7 @@ def value(self) -> np.float32: """ return self._value + @block_reentrance def set_value(self, selector, value: float): # clip value between limits value = np.clip(value, self._limits[0], self._limits[1], dtype=np.float32) @@ -117,6 +118,7 @@ def axis(self) -> str: """one of "x" | "y" """ return self._axis + @block_reentrance def set_value(self, selector, value: Sequence[float]): """ Set start, stop range of selector @@ -231,6 +233,7 @@ def value(self) -> np.ndarray[float]: """ return self._value + @block_reentrance def set_value(self, selector, value: Sequence[float]): """ Set the selection of the rectangle selector. diff --git a/fastplotlib/graphics/_features/_text.py b/fastplotlib/graphics/_features/_text.py index baa2734d5..90af7c719 100644 --- a/fastplotlib/graphics/_features/_text.py +++ b/fastplotlib/graphics/_features/_text.py @@ -2,7 +2,7 @@ import pygfx -from ._base import GraphicFeature, FeatureEvent +from ._base import GraphicFeature, FeatureEvent, block_reentrance class TextData(GraphicFeature): @@ -14,6 +14,7 @@ def __init__(self, value: str): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): graphic.world_object.geometry.set_text(value) self._value = value @@ -31,6 +32,7 @@ def __init__(self, value: float | int): def value(self) -> float | int: return self._value + @block_reentrance def set_value(self, graphic, value: float | int): graphic.world_object.geometry.font_size = value self._value = graphic.world_object.geometry.font_size @@ -48,6 +50,7 @@ def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): def value(self) -> pygfx.Color: return self._value + @block_reentrance def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): value = pygfx.Color(value) graphic.world_object.material.color = value @@ -66,6 +69,7 @@ def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): def value(self) -> pygfx.Color: return self._value + @block_reentrance def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): value = pygfx.Color(value) graphic.world_object.material.outline_color = value @@ -84,6 +88,7 @@ def __init__(self, value: float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float): graphic.world_object.material.outline_thickness = value self._value = graphic.world_object.material.outline_thickness From 21db04966f614315241522d5ff50accbaf20536b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Mar 2025 22:52:00 -0500 Subject: [PATCH 2/4] add unit circle example --- examples/selection_tools/unit_circle.py | 114 ++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 examples/selection_tools/unit_circle.py diff --git a/examples/selection_tools/unit_circle.py b/examples/selection_tools/unit_circle.py new file mode 100644 index 000000000..76f6a207c --- /dev/null +++ b/examples/selection_tools/unit_circle.py @@ -0,0 +1,114 @@ +""" +Unit circle +=========== + +Example with linear selectors on a sine and cosine function that demonstrates the unit circle. + +This shows how fastplotlib supports bidirectional events, drag the linear selector on the sine +or cosine function and they will both move together. + +Click on the sine or cosine function to set the colormap transform to illustrate the sine or +cosine function output values on the unit circle. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + + +import numpy as np +import fastplotlib as fpl + + +# helper function to make a cirlce +def make_circle(center, radius: float, n_points: int) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.cos(theta) + ys = radius * np.sin(theta) + + return np.column_stack([xs, ys]) + center + + +# create a figure with 3 subplots +figure = fpl.Figure((3, 1), names=["unit circle", "sin(x)", "cos(x)"], size=(700, 1024)) + +# set the axes to intersect at (0, 0, 0) to better illustrate the unit circle +for subplot in figure: + subplot.axes.intersection = (0, 0, 0) + +figure["sin(x)"].camera.maintain_aspect = False +figure["cos(x)"].camera.maintain_aspect = False + +# create sine and cosine data +xs = np.linspace(0, 2 * np.pi, 360) +sine = np.sin(xs) +cosine = np.cos(xs) + +# circle data +circle_data = make_circle(center=(0, 0), radius=1, n_points=360) + +# make the circle line graphic, set the cmap transform using the sine function +circle_graphic = figure["unit circle"].add_line( + circle_data, thickness=4, cmap="bwr", cmap_transform=sine +) + +# line to show the circle radius +# use it to indicate the current position of the sine and cosine selctors (below) +radius_data = np.array([[0, 0, 0], [*circle_data[0], 0]]) +circle_radius = figure["unit circle"].add_line( + radius_data, thickness=6, colors="magenta" +) + +# sine line graphic, cmap transform set from the sine function +sine_graphic = figure["sin(x)"].add_line( + sine, thickness=10, cmap="bwr", cmap_transform=sine +) + +# cosine line graphic, cmap transform set from the sine function +# illustrates the sine function values on the cosine graphic +cosine_graphic = figure["cos(x)"].add_line( + cosine, thickness=10, cmap="bwr", cmap_transform=sine +) + +# add linear selectors to the sine and cosine line graphics +sine_selector = sine_graphic.add_linear_selector() +cosine_selector = cosine_graphic.add_linear_selector() + +def set_circle_cmap(ev): + # sets the cmap transforms + + cmap_transform = ev.graphic.data[:, 1] # y-val data of the sine or cosine graphic + for g in [sine_graphic, cosine_graphic]: + g.cmap.transform = cmap_transform + + # set circle cmap transform + circle_graphic.cmap.transform = cmap_transform + +# when the sine or cosine graphic is clicked, the cmap_transform +# of the sine, cosine and circle line graphics are all set from +# the y-values of the clicked line +sine_graphic.add_event_handler(set_circle_cmap, "click") +cosine_graphic.add_event_handler(set_circle_cmap, "click") + + +def set_x_val(ev): + # used to sync the two selectors + value = ev.info["value"] + index = ev.get_selected_index() + + sine_selector.selection = value + cosine_selector.selection = value + + circle_radius.data[1, :-1] = circle_data[index] + +# add same event handler to both graphics +sine_selector.add_event_handler(set_x_val, "selection") +cosine_selector.add_event_handler(set_x_val, "selection") + +figure.show() + + +# 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__": + print(__doc__) + fpl.loop.run() From c563a0609c75cf61cb13a6726f1f67ff55426a31 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Mar 2025 23:13:12 -0500 Subject: [PATCH 3/4] raise original exception correctly, comments --- fastplotlib/graphics/_features/_base.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index cca75cac2..b39ed1b2b 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -323,8 +323,8 @@ def __repr__(self): def block_reentrance(set_value): # decorator to block re-entrant set_value methods - # useful when creating complex, circular, bidirectional event graphics - def wrap(self: GraphicFeature, graphic_or_key, value): + # useful when creating complex, circular, bidirectional event graphs + def set_value_wrapper(self: GraphicFeature, graphic_or_key, value): """ wraps GraphicFeature.set_value @@ -334,14 +334,18 @@ def wrap(self: GraphicFeature, graphic_or_key, value): value: the value passed to set_value() """ + # set_value is already in the middle of an execution, block re-entrance if self._reentrant_block: return try: + # block re-execution of set_value until it has *fully* finished executing self._reentrant_block = True set_value(self, graphic_or_key, value) - except Exception as e: - raise e(format_exc()) + except Exception as exc: + # raise original exception + raise exc # set_value has raised. The line above and the lines 2+ steps below are probably more relevant! finally: + # set_value has finished executing, now allow future executions self._reentrant_block = False - return wrap + return set_value_wrapper From 31ebc34b261e4193def56f7ec76bc557dc4da17a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Mar 2025 23:25:19 -0500 Subject: [PATCH 4/4] cleanup, comments --- fastplotlib/graphics/_features/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index b39ed1b2b..1088dc005 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -1,6 +1,5 @@ from warnings import warn from typing import Any, Literal -from traceback import format_exc import numpy as np from numpy.typing import NDArray @@ -54,6 +53,7 @@ def __init__(self, **kwargs): self._event_handlers = list() self._block_events = False + # used by @block_reentrance decorator to block re-entrance into set_value functions self._reentrant_block: bool = False @property pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy