Skip to content

Commit 3362669

Browse files
authored
implemenet @block_reentrance decorator (#744)
* implemenet block_reentrance decorator * add unit circle example * raise original exception correctly, comments * cleanup, comments
1 parent bd131f9 commit 3362669

File tree

7 files changed

+181
-6
lines changed

7 files changed

+181
-6
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
Unit circle
3+
===========
4+
5+
Example with linear selectors on a sine and cosine function that demonstrates the unit circle.
6+
7+
This shows how fastplotlib supports bidirectional events, drag the linear selector on the sine
8+
or cosine function and they will both move together.
9+
10+
Click on the sine or cosine function to set the colormap transform to illustrate the sine or
11+
cosine function output values on the unit circle.
12+
"""
13+
14+
# test_example = false
15+
# sphinx_gallery_pygfx_docs = 'screenshot'
16+
17+
18+
import numpy as np
19+
import fastplotlib as fpl
20+
21+
22+
# helper function to make a cirlce
23+
def make_circle(center, radius: float, n_points: int) -> np.ndarray:
24+
theta = np.linspace(0, 2 * np.pi, n_points)
25+
xs = radius * np.cos(theta)
26+
ys = radius * np.sin(theta)
27+
28+
return np.column_stack([xs, ys]) + center
29+
30+
31+
# create a figure with 3 subplots
32+
figure = fpl.Figure((3, 1), names=["unit circle", "sin(x)", "cos(x)"], size=(700, 1024))
33+
34+
# set the axes to intersect at (0, 0, 0) to better illustrate the unit circle
35+
for subplot in figure:
36+
subplot.axes.intersection = (0, 0, 0)
37+
38+
figure["sin(x)"].camera.maintain_aspect = False
39+
figure["cos(x)"].camera.maintain_aspect = False
40+
41+
# create sine and cosine data
42+
xs = np.linspace(0, 2 * np.pi, 360)
43+
sine = np.sin(xs)
44+
cosine = np.cos(xs)
45+
46+
# circle data
47+
circle_data = make_circle(center=(0, 0), radius=1, n_points=360)
48+
49+
# make the circle line graphic, set the cmap transform using the sine function
50+
circle_graphic = figure["unit circle"].add_line(
51+
circle_data, thickness=4, cmap="bwr", cmap_transform=sine
52+
)
53+
54+
# line to show the circle radius
55+
# use it to indicate the current position of the sine and cosine selctors (below)
56+
radius_data = np.array([[0, 0, 0], [*circle_data[0], 0]])
57+
circle_radius = figure["unit circle"].add_line(
58+
radius_data, thickness=6, colors="magenta"
59+
)
60+
61+
# sine line graphic, cmap transform set from the sine function
62+
sine_graphic = figure["sin(x)"].add_line(
63+
sine, thickness=10, cmap="bwr", cmap_transform=sine
64+
)
65+
66+
# cosine line graphic, cmap transform set from the sine function
67+
# illustrates the sine function values on the cosine graphic
68+
cosine_graphic = figure["cos(x)"].add_line(
69+
cosine, thickness=10, cmap="bwr", cmap_transform=sine
70+
)
71+
72+
# add linear selectors to the sine and cosine line graphics
73+
sine_selector = sine_graphic.add_linear_selector()
74+
cosine_selector = cosine_graphic.add_linear_selector()
75+
76+
def set_circle_cmap(ev):
77+
# sets the cmap transforms
78+
79+
cmap_transform = ev.graphic.data[:, 1] # y-val data of the sine or cosine graphic
80+
for g in [sine_graphic, cosine_graphic]:
81+
g.cmap.transform = cmap_transform
82+
83+
# set circle cmap transform
84+
circle_graphic.cmap.transform = cmap_transform
85+
86+
# when the sine or cosine graphic is clicked, the cmap_transform
87+
# of the sine, cosine and circle line graphics are all set from
88+
# the y-values of the clicked line
89+
sine_graphic.add_event_handler(set_circle_cmap, "click")
90+
cosine_graphic.add_event_handler(set_circle_cmap, "click")
91+
92+
93+
def set_x_val(ev):
94+
# used to sync the two selectors
95+
value = ev.info["value"]
96+
index = ev.get_selected_index()
97+
98+
sine_selector.selection = value
99+
cosine_selector.selection = value
100+
101+
circle_radius.data[1, :-1] = circle_data[index]
102+
103+
# add same event handler to both graphics
104+
sine_selector.add_event_handler(set_x_val, "selection")
105+
cosine_selector.add_event_handler(set_x_val, "selection")
106+
107+
figure.show()
108+
109+
110+
# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
111+
# please see our docs for using fastplotlib interactively in ipython and jupyter
112+
if __name__ == "__main__":
113+
print(__doc__)
114+
fpl.loop.run()

fastplotlib/graphics/_features/_base.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def __init__(self, **kwargs):
5353
self._event_handlers = list()
5454
self._block_events = False
5555

56+
# used by @block_reentrance decorator to block re-entrance into set_value functions
57+
self._reentrant_block: bool = False
58+
5659
@property
5760
def value(self) -> Any:
5861
"""Graphic Feature value, must be implemented in subclass"""
@@ -316,3 +319,33 @@ def __len__(self):
316319

317320
def __repr__(self):
318321
return f"{self.__class__.__name__} buffer data:\n" f"{self.value.__repr__()}"
322+
323+
324+
def block_reentrance(set_value):
325+
# decorator to block re-entrant set_value methods
326+
# useful when creating complex, circular, bidirectional event graphs
327+
def set_value_wrapper(self: GraphicFeature, graphic_or_key, value):
328+
"""
329+
wraps GraphicFeature.set_value
330+
331+
self: GraphicFeature instance
332+
333+
graphic_or_key: graphic, or key if a BufferManager
334+
335+
value: the value passed to set_value()
336+
"""
337+
# set_value is already in the middle of an execution, block re-entrance
338+
if self._reentrant_block:
339+
return
340+
try:
341+
# block re-execution of set_value until it has *fully* finished executing
342+
self._reentrant_block = True
343+
set_value(self, graphic_or_key, value)
344+
except Exception as exc:
345+
# raise original exception
346+
raise exc # set_value has raised. The line above and the lines 2+ steps below are probably more relevant!
347+
finally:
348+
# set_value has finished executing, now allow future executions
349+
self._reentrant_block = False
350+
351+
return set_value_wrapper

fastplotlib/graphics/_features/_common.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import numpy as np
22

3-
from ._base import GraphicFeature, FeatureEvent
3+
from ._base import GraphicFeature, FeatureEvent, block_reentrance
44

55

66
class Name(GraphicFeature):
@@ -14,6 +14,7 @@ def __init__(self, value: str):
1414
def value(self) -> str:
1515
return self._value
1616

17+
@block_reentrance
1718
def set_value(self, graphic, value: str):
1819
if not isinstance(value, str):
1920
raise TypeError("`Graphic` name must be of type <str>")
@@ -44,6 +45,7 @@ def _validate(self, value):
4445
def value(self) -> np.ndarray:
4546
return self._value
4647

48+
@block_reentrance
4749
def set_value(self, graphic, value: np.ndarray | list | tuple):
4850
self._validate(value)
4951

@@ -74,6 +76,7 @@ def _validate(self, value):
7476
def value(self) -> np.ndarray:
7577
return self._value
7678

79+
@block_reentrance
7780
def set_value(self, graphic, value: np.ndarray | list | tuple):
7881
self._validate(value)
7982

@@ -96,6 +99,7 @@ def __init__(self, value: bool):
9699
def value(self) -> bool:
97100
return self._value
98101

102+
@block_reentrance
99103
def set_value(self, graphic, value: bool):
100104
graphic.world_object.visible = value
101105
self._value = value
@@ -117,6 +121,7 @@ def __init__(self, value: bool):
117121
def value(self) -> bool:
118122
return self._value
119123

124+
@block_reentrance
120125
def set_value(self, graphic, value: bool):
121126
self._value = value
122127
event = FeatureEvent(type="deleted", info={"value": value})

fastplotlib/graphics/_features/_image.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy as np
66

77
import pygfx
8-
from ._base import GraphicFeature, FeatureEvent
8+
from ._base import GraphicFeature, FeatureEvent, block_reentrance
99

1010
from ...utils import (
1111
make_colors,
@@ -135,6 +135,7 @@ def __next__(self) -> tuple[pygfx.Texture, tuple[int, int], tuple[slice, slice]]
135135
def __getitem__(self, item):
136136
return self.value[item]
137137

138+
@block_reentrance
138139
def __setitem__(self, key, value):
139140
self.value[key] = value
140141

@@ -159,6 +160,7 @@ def __init__(self, value: float):
159160
def value(self) -> float:
160161
return self._value
161162

163+
@block_reentrance
162164
def set_value(self, graphic, value: float):
163165
vmax = graphic._material.clim[1]
164166
graphic._material.clim = (value, vmax)
@@ -179,6 +181,7 @@ def __init__(self, value: float):
179181
def value(self) -> float:
180182
return self._value
181183

184+
@block_reentrance
182185
def set_value(self, graphic, value: float):
183186
vmin = graphic._material.clim[0]
184187
graphic._material.clim = (vmin, value)
@@ -200,6 +203,7 @@ def __init__(self, value: str):
200203
def value(self) -> str:
201204
return self._value
202205

206+
@block_reentrance
203207
def set_value(self, graphic, value: str):
204208
new_colors = make_colors(256, value)
205209
graphic._material.map.texture.data[:] = new_colors
@@ -226,6 +230,7 @@ def _validate(self, value):
226230
def value(self) -> str:
227231
return self._value
228232

233+
@block_reentrance
229234
def set_value(self, graphic, value: str):
230235
self._validate(value)
231236

@@ -254,6 +259,7 @@ def _validate(self, value):
254259
def value(self) -> str:
255260
return self._value
256261

262+
@block_reentrance
257263
def set_value(self, graphic, value: str):
258264
self._validate(value)
259265

fastplotlib/graphics/_features/_positions_graphics.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, List
1+
from typing import Any
22

33
import numpy as np
44
import pygfx
@@ -11,6 +11,7 @@
1111
BufferManager,
1212
FeatureEvent,
1313
to_gpu_supported_dtype,
14+
block_reentrance,
1415
)
1516
from .utils import parse_colors
1617

@@ -58,6 +59,7 @@ def __init__(
5859

5960
super().__init__(data=data, isolated_buffer=isolated_buffer)
6061

62+
@block_reentrance
6163
def __setitem__(
6264
self,
6365
key: int | slice | np.ndarray[int | bool] | tuple[slice, ...],
@@ -155,6 +157,7 @@ def __init__(
155157
def value(self) -> pygfx.Color:
156158
return self._value
157159

160+
@block_reentrance
158161
def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Color):
159162
value = pygfx.Color(value)
160163
graphic.world_object.material.color = value
@@ -174,6 +177,7 @@ def __init__(self, value: int | float):
174177
def value(self) -> float:
175178
return self._value
176179

180+
@block_reentrance
177181
def set_value(self, graphic, value: float | int):
178182
graphic.world_object.material.size = float(value)
179183
self._value = value
@@ -192,6 +196,7 @@ def __init__(self, value: str):
192196
def value(self) -> str:
193197
return self._value
194198

199+
@block_reentrance
195200
def set_value(self, graphic, value: str):
196201
if "Line" in graphic.world_object.material.__class__.__name__:
197202
graphic.world_object.material.thickness_space = value
@@ -243,6 +248,7 @@ def _fix_data(self, data):
243248

244249
return to_gpu_supported_dtype(data)
245250

251+
@block_reentrance
246252
def __setitem__(
247253
self,
248254
key: int | slice | np.ndarray[int | bool] | tuple[slice, ...],
@@ -318,6 +324,7 @@ def _fix_sizes(
318324

319325
return sizes
320326

327+
@block_reentrance
321328
def __setitem__(
322329
self,
323330
key: int | slice | np.ndarray[int | bool] | list[int | bool],
@@ -344,6 +351,7 @@ def __init__(self, value: float):
344351
def value(self) -> float:
345352
return self._value
346353

354+
@block_reentrance
347355
def set_value(self, graphic, value: float):
348356
graphic.world_object.material.thickness = value
349357
self._value = value
@@ -392,6 +400,7 @@ def __init__(
392400
# set vertex colors from cmap
393401
self._vertex_colors[:] = colors
394402

403+
@block_reentrance
395404
def __setitem__(self, key: slice, cmap_name):
396405
if not isinstance(key, slice):
397406
raise TypeError(

fastplotlib/graphics/_features/_selection_features.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from typing import Sequence, Tuple
1+
from typing import Sequence
22

33
import numpy as np
44

55
from ...utils import mesh_masks
6-
from ._base import GraphicFeature, FeatureEvent
6+
from ._base import GraphicFeature, FeatureEvent, block_reentrance
77

88

99
class LinearSelectionFeature(GraphicFeature):
@@ -54,6 +54,7 @@ def value(self) -> np.float32:
5454
"""
5555
return self._value
5656

57+
@block_reentrance
5758
def set_value(self, selector, value: float):
5859
# clip value between limits
5960
value = np.clip(value, self._limits[0], self._limits[1], dtype=np.float32)
@@ -117,6 +118,7 @@ def axis(self) -> str:
117118
"""one of "x" | "y" """
118119
return self._axis
119120

121+
@block_reentrance
120122
def set_value(self, selector, value: Sequence[float]):
121123
"""
122124
Set start, stop range of selector
@@ -231,6 +233,7 @@ def value(self) -> np.ndarray[float]:
231233
"""
232234
return self._value
233235

236+
@block_reentrance
234237
def set_value(self, selector, value: Sequence[float]):
235238
"""
236239
Set the selection of the rectangle selector.

0 commit comments

Comments
 (0)