Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

implemenet @block_reentrance decorator #744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 5, 2025
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
114 changes: 114 additions & 0 deletions 114 examples/selection_tools/unit_circle.py
Original file line number Diff line number Diff line change
@@ -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'
kushalkolar marked this conversation as resolved.
Show resolved Hide resolved


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))
kushalkolar marked this conversation as resolved.
Show resolved Hide resolved

# 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()
33 changes: 33 additions & 0 deletions 33 fastplotlib/graphics/_features/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ 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
def value(self) -> Any:
"""Graphic Feature value, must be implemented in subclass"""
Expand Down Expand Up @@ -316,3 +319,33 @@ 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 graphs
def set_value_wrapper(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()
"""
# 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 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 set_value_wrapper
7 changes: 6 additions & 1 deletion 7 fastplotlib/graphics/_features/_common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import numpy as np

from ._base import GraphicFeature, FeatureEvent
from ._base import GraphicFeature, FeatureEvent, block_reentrance


class Name(GraphicFeature):
Expand All @@ -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 <str>")
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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})
Expand Down
8 changes: 7 additions & 1 deletion 8 fastplotlib/graphics/_features/_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
11 changes: 10 additions & 1 deletion 11 fastplotlib/graphics/_features/_positions_graphics.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, List
from typing import Any

import numpy as np
import pygfx
Expand All @@ -11,6 +11,7 @@
BufferManager,
FeatureEvent,
to_gpu_supported_dtype,
block_reentrance,
)
from .utils import parse_colors

Expand Down Expand Up @@ -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, ...],
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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, ...],
Expand Down Expand Up @@ -318,6 +324,7 @@ def _fix_sizes(

return sizes

@block_reentrance
def __setitem__(
self,
key: int | slice | np.ndarray[int | bool] | list[int | bool],
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 5 additions & 2 deletions 7 fastplotlib/graphics/_features/_selection_features.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.