syd 0.2.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syd/__init__.py +1 -1
- syd/flask_deployment/__init__.py +0 -6
- syd/flask_deployment/deployer.py +446 -460
- syd/flask_deployment/static/css/styles.css +50 -16
- syd/flask_deployment/static/js/viewer.js +154 -67
- syd/flask_deployment/templates/index.html +1 -1
- syd/notebook_deployment/__init__.py +0 -1
- syd/notebook_deployment/deployer.py +119 -218
- syd/notebook_deployment/widgets.py +2 -2
- syd/parameters.py +69 -104
- syd/support.py +27 -0
- syd/viewer.py +10 -6
- syd-1.0.0.dist-info/METADATA +219 -0
- syd-1.0.0.dist-info/RECORD +19 -0
- syd-0.2.0.dist-info/METADATA +0 -126
- syd-0.2.0.dist-info/RECORD +0 -19
- {syd-0.2.0.dist-info → syd-1.0.0.dist-info}/WHEEL +0 -0
- {syd-0.2.0.dist-info → syd-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,27 +1,13 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Literal, Optional
|
|
2
2
|
import warnings
|
|
3
|
-
from functools import wraps
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from contextlib import contextmanager
|
|
6
|
-
from time import time
|
|
7
|
-
|
|
8
3
|
import ipywidgets as widgets
|
|
9
4
|
from IPython.display import display
|
|
10
5
|
import matplotlib as mpl
|
|
11
6
|
import matplotlib.pyplot as plt
|
|
12
7
|
|
|
13
|
-
from ..
|
|
8
|
+
from ..support import ParameterUpdateWarning, plot_context
|
|
14
9
|
from ..viewer import Viewer
|
|
15
|
-
from .widgets import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@contextmanager
|
|
19
|
-
def _plot_context():
|
|
20
|
-
plt.ioff()
|
|
21
|
-
try:
|
|
22
|
-
yield
|
|
23
|
-
finally:
|
|
24
|
-
plt.ion()
|
|
10
|
+
from .widgets import create_widget, BaseWidget
|
|
25
11
|
|
|
26
12
|
|
|
27
13
|
def get_backend_type():
|
|
@@ -40,47 +26,6 @@ def get_backend_type():
|
|
|
40
26
|
return "other"
|
|
41
27
|
|
|
42
28
|
|
|
43
|
-
def debounce(wait_time):
|
|
44
|
-
"""
|
|
45
|
-
Decorator to prevent a function from being called more than once every wait_time seconds.
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
def decorator(fn):
|
|
49
|
-
last_called = [0.0] # Using list to maintain state in closure
|
|
50
|
-
|
|
51
|
-
@wraps(fn)
|
|
52
|
-
def debounced(*args, **kwargs):
|
|
53
|
-
current_time = time()
|
|
54
|
-
if current_time - last_called[0] >= wait_time:
|
|
55
|
-
fn(*args, **kwargs)
|
|
56
|
-
last_called[0] = current_time
|
|
57
|
-
|
|
58
|
-
return debounced
|
|
59
|
-
|
|
60
|
-
return decorator
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@dataclass
|
|
64
|
-
class LayoutConfig:
|
|
65
|
-
"""Configuration for the viewer layout."""
|
|
66
|
-
|
|
67
|
-
controls_position: str = "left" # Options are: 'left', 'top', 'right', 'bottom'
|
|
68
|
-
figure_width: float = 8.0
|
|
69
|
-
figure_height: float = 6.0
|
|
70
|
-
controls_width_percent: int = 20
|
|
71
|
-
|
|
72
|
-
def __post_init__(self):
|
|
73
|
-
valid_positions = ["left", "top", "right", "bottom"]
|
|
74
|
-
if self.controls_position not in valid_positions:
|
|
75
|
-
raise ValueError(
|
|
76
|
-
f"Invalid controls position: {self.controls_position}. Must be one of {valid_positions}"
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
@property
|
|
80
|
-
def is_horizontal(self) -> bool:
|
|
81
|
-
return self.controls_position == "left" or self.controls_position == "right"
|
|
82
|
-
|
|
83
|
-
|
|
84
29
|
class NotebookDeployer:
|
|
85
30
|
"""
|
|
86
31
|
A deployment system for Viewer in Jupyter notebooks using ipywidgets.
|
|
@@ -90,50 +35,54 @@ class NotebookDeployer:
|
|
|
90
35
|
def __init__(
|
|
91
36
|
self,
|
|
92
37
|
viewer: Viewer,
|
|
93
|
-
controls_position:
|
|
94
|
-
figure_width: float = 8.0,
|
|
95
|
-
figure_height: float = 6.0,
|
|
38
|
+
controls_position: Literal["left", "top", "right", "bottom"] = "left",
|
|
96
39
|
controls_width_percent: int = 20,
|
|
97
40
|
continuous: bool = False,
|
|
98
|
-
suppress_warnings: bool =
|
|
41
|
+
suppress_warnings: bool = True,
|
|
99
42
|
):
|
|
100
43
|
self.viewer = viewer
|
|
101
|
-
self.
|
|
102
|
-
controls_position=controls_position,
|
|
103
|
-
figure_width=figure_width,
|
|
104
|
-
figure_height=figure_height,
|
|
105
|
-
controls_width_percent=controls_width_percent,
|
|
106
|
-
)
|
|
107
|
-
self.continuous = continuous
|
|
44
|
+
self.components: dict[str, BaseWidget] = {}
|
|
108
45
|
self.suppress_warnings = suppress_warnings
|
|
46
|
+
self._updating = False # Flag to check circular updates
|
|
47
|
+
self.controls_position = controls_position
|
|
48
|
+
self.controls_width_percent = controls_width_percent
|
|
49
|
+
self.continuous = continuous
|
|
109
50
|
|
|
110
51
|
# Initialize containers
|
|
111
52
|
self.backend_type = get_backend_type()
|
|
112
53
|
if self.backend_type not in ["inline", "widget"]:
|
|
113
54
|
warnings.warn(
|
|
114
55
|
f"The current backend ({self.backend_type}) is not supported. Please use %matplotlib widget or %matplotlib inline.\n"
|
|
115
|
-
"The behavior of the viewer will almost definitely not work as expected
|
|
56
|
+
"The behavior of the viewer will almost definitely not work as expected!"
|
|
116
57
|
)
|
|
117
|
-
self.
|
|
118
|
-
self.plot_output = widgets.Output()
|
|
58
|
+
self._last_figure = None
|
|
119
59
|
|
|
120
|
-
|
|
121
|
-
|
|
60
|
+
def deploy(self) -> None:
|
|
61
|
+
"""Deploy the viewer."""
|
|
62
|
+
self.build_components()
|
|
63
|
+
self.build_layout()
|
|
64
|
+
self.backend_type = get_backend_type()
|
|
65
|
+
display(self.layout)
|
|
66
|
+
self.update_plot()
|
|
122
67
|
|
|
123
|
-
|
|
124
|
-
|
|
68
|
+
def build_components(self) -> None:
|
|
69
|
+
"""Create widget instances for all parameters and equip callbacks."""
|
|
70
|
+
for name, param in self.viewer.parameters.items():
|
|
71
|
+
widget = create_widget(param, continuous=self.continuous)
|
|
72
|
+
self.components[name] = widget
|
|
73
|
+
callback = lambda _, n=name: self.handle_component_engagement(n)
|
|
74
|
+
widget.observe(callback)
|
|
125
75
|
|
|
126
|
-
|
|
127
|
-
|
|
76
|
+
def build_layout(self) -> None:
|
|
77
|
+
"""Create the main layout combining controls and plot."""
|
|
128
78
|
|
|
129
|
-
|
|
130
|
-
"""Create widgets for controlling the layout."""
|
|
131
|
-
controls: Dict[str, widgets.Widget] = {}
|
|
79
|
+
self.plot_output = widgets.Output()
|
|
132
80
|
|
|
133
81
|
# Controls width slider for horizontal layouts
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
82
|
+
self.controls = {}
|
|
83
|
+
if self.controls_position in ["left", "right"]:
|
|
84
|
+
self.controls["controls_width"] = widgets.IntSlider(
|
|
85
|
+
value=self.controls_width_percent,
|
|
137
86
|
min=10,
|
|
138
87
|
max=50,
|
|
139
88
|
description="Controls Width %",
|
|
@@ -142,22 +91,71 @@ class NotebookDeployer:
|
|
|
142
91
|
style={"description_width": "initial"},
|
|
143
92
|
)
|
|
144
93
|
|
|
145
|
-
|
|
94
|
+
# Create parameter controls section
|
|
95
|
+
param_box = widgets.VBox(
|
|
96
|
+
[widgets.HTML("<b>Parameters</b>")]
|
|
97
|
+
+ [w.widget for w in self.components.values()],
|
|
98
|
+
layout=widgets.Layout(margin="10px 0px"),
|
|
99
|
+
)
|
|
146
100
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
101
|
+
# Combine all controls
|
|
102
|
+
if self.controls_position in ["left", "right"]:
|
|
103
|
+
# Create layout controls section if horizontal (might include for vertical later when we have more permanent controls...)
|
|
104
|
+
layout_box = widgets.VBox(
|
|
105
|
+
[widgets.HTML("<b>Syd Controls</b>")] + list(self.controls.values()),
|
|
106
|
+
layout=widgets.Layout(margin="10px 0px"),
|
|
153
107
|
)
|
|
154
108
|
|
|
155
|
-
#
|
|
156
|
-
self.
|
|
109
|
+
# Register the controls_width slider's observer
|
|
110
|
+
if "controls_width" in self.controls:
|
|
111
|
+
self.controls["controls_width"].observe(
|
|
112
|
+
self._handle_container_width_change, names="value"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
widgets_elements = [param_box, layout_box]
|
|
116
|
+
else:
|
|
117
|
+
widgets_elements = [param_box]
|
|
118
|
+
|
|
119
|
+
self.widgets_container = widgets.VBox(
|
|
120
|
+
widgets_elements,
|
|
121
|
+
layout=widgets.Layout(
|
|
122
|
+
width=(
|
|
123
|
+
f"{self.controls_width_percent}%"
|
|
124
|
+
if self.controls_position in ["left", "right"]
|
|
125
|
+
else "100%"
|
|
126
|
+
),
|
|
127
|
+
padding="10px",
|
|
128
|
+
overflow_y="scroll",
|
|
129
|
+
border="1px solid #e5e7eb",
|
|
130
|
+
border_radius="4px 4px 0px 0px",
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Create plot container
|
|
135
|
+
self.plot_container = widgets.VBox(
|
|
136
|
+
[self.plot_output],
|
|
137
|
+
layout=widgets.Layout(
|
|
138
|
+
width=(
|
|
139
|
+
f"{100 - self.controls_width_percent}%"
|
|
140
|
+
if self.controls_position in ["left", "right"]
|
|
141
|
+
else "100%"
|
|
142
|
+
),
|
|
143
|
+
padding="10px",
|
|
144
|
+
),
|
|
145
|
+
)
|
|
157
146
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
147
|
+
# Create final layout based on configuration
|
|
148
|
+
if self.controls_position == "left":
|
|
149
|
+
self.layout = widgets.HBox([self.widgets_container, self.plot_container])
|
|
150
|
+
elif self.controls_position == "right":
|
|
151
|
+
self.layout = widgets.HBox([self.plot_container, self.widgets_container])
|
|
152
|
+
elif self.controls_position == "bottom":
|
|
153
|
+
self.layout = widgets.VBox([self.plot_container, self.widgets_container])
|
|
154
|
+
else:
|
|
155
|
+
self.layout = widgets.VBox([self.widgets_container, self.plot_container])
|
|
156
|
+
|
|
157
|
+
def handle_component_engagement(self, name: str) -> None:
|
|
158
|
+
"""Handle engagement with an interactive component."""
|
|
161
159
|
if self._updating:
|
|
162
160
|
print(
|
|
163
161
|
"Already updating -- there's a circular dependency!"
|
|
@@ -174,64 +172,46 @@ class NotebookDeployer:
|
|
|
174
172
|
if self.suppress_warnings:
|
|
175
173
|
warnings.filterwarnings("ignore", category=ParameterUpdateWarning)
|
|
176
174
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if
|
|
175
|
+
# Get the component
|
|
176
|
+
component = self.components[name]
|
|
177
|
+
if component.is_action:
|
|
178
|
+
# If the component is an action, call the callback
|
|
180
179
|
parameter = self.viewer.parameters[name]
|
|
181
180
|
parameter.callback(self.viewer.state)
|
|
182
181
|
else:
|
|
183
|
-
|
|
182
|
+
# Otherwise, update the parameter value
|
|
183
|
+
self.viewer.set_parameter_value(name, component.value)
|
|
184
184
|
|
|
185
|
-
# Update any
|
|
186
|
-
self.
|
|
185
|
+
# Update any components that changed due to dependencies
|
|
186
|
+
self.sync_components_with_state()
|
|
187
187
|
|
|
188
188
|
# Update the plot
|
|
189
|
-
self.
|
|
189
|
+
self.update_plot()
|
|
190
190
|
|
|
191
191
|
finally:
|
|
192
192
|
self._updating = False
|
|
193
193
|
|
|
194
|
-
def
|
|
195
|
-
"""
|
|
196
|
-
|
|
197
|
-
def _sync_widgets_with_state(self, exclude: Optional[str] = None) -> None:
|
|
198
|
-
"""Sync widget values with viewer state."""
|
|
194
|
+
def sync_components_with_state(self, exclude: Optional[str] = None) -> None:
|
|
195
|
+
"""Sync component values with viewer state."""
|
|
199
196
|
for name, parameter in self.viewer.parameters.items():
|
|
200
197
|
if name == exclude:
|
|
201
198
|
continue
|
|
202
199
|
|
|
203
|
-
|
|
204
|
-
if not
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def _handle_figure_size_change(self, change: Dict[str, Any]) -> None:
|
|
208
|
-
"""Handle changes to figure dimensions."""
|
|
209
|
-
if self._current_figure is None:
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
self._redraw_plot()
|
|
213
|
-
|
|
214
|
-
def _handle_container_width_change(self, change: Dict[str, Any]) -> None:
|
|
215
|
-
"""Handle changes to container width proportions."""
|
|
216
|
-
width_percent = self.layout_widgets["controls_width"].value
|
|
217
|
-
self.config.controls_width_percent = width_percent
|
|
218
|
-
|
|
219
|
-
# Update container widths
|
|
220
|
-
self.widgets_container.layout.width = f"{width_percent}%"
|
|
221
|
-
self.plot_container.layout.width = f"{100 - width_percent}%"
|
|
200
|
+
component = self.components[name]
|
|
201
|
+
if not component.matches_parameter(parameter):
|
|
202
|
+
component.update_from_parameter(parameter)
|
|
222
203
|
|
|
223
|
-
def
|
|
204
|
+
def update_plot(self) -> None:
|
|
224
205
|
"""Update the plot with current state."""
|
|
225
206
|
state = self.viewer.state
|
|
226
207
|
|
|
227
|
-
with
|
|
208
|
+
with plot_context():
|
|
228
209
|
figure = self.viewer.plot(state)
|
|
229
210
|
|
|
230
|
-
# Update
|
|
231
|
-
self.
|
|
211
|
+
# Update components if plot function updated a parameter
|
|
212
|
+
self.sync_components_with_state()
|
|
232
213
|
|
|
233
214
|
# Close the last figure if it exists to keep matplotlib clean
|
|
234
|
-
# (just moved this from after clear_output.... noting!)
|
|
235
215
|
if self._last_figure is not None:
|
|
236
216
|
plt.close(self._last_figure)
|
|
237
217
|
|
|
@@ -251,90 +231,11 @@ class NotebookDeployer:
|
|
|
251
231
|
|
|
252
232
|
self._last_figure = figure
|
|
253
233
|
|
|
254
|
-
def
|
|
255
|
-
"""
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
widget.observe(lambda change, n=name: self._handle_widget_engagement(n))
|
|
259
|
-
|
|
260
|
-
# Create parameter controls section
|
|
261
|
-
param_box = widgets.VBox(
|
|
262
|
-
[widgets.HTML("<b>Parameters</b>")]
|
|
263
|
-
+ [w.widget for w in self.parameter_widgets.values()],
|
|
264
|
-
layout=widgets.Layout(margin="10px 0px"),
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
# Combine all controls
|
|
268
|
-
if self.config.is_horizontal:
|
|
269
|
-
# Create layout controls section if horizontal (might include for vertical later when we have more permanent controls...)
|
|
270
|
-
layout_box = widgets.VBox(
|
|
271
|
-
[widgets.HTML("<b>Layout Controls</b>")]
|
|
272
|
-
+ list(self.layout_widgets.values()),
|
|
273
|
-
layout=widgets.Layout(margin="10px 0px"),
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
# Register the controls_width slider's observer
|
|
277
|
-
if "controls_width" in self.layout_widgets:
|
|
278
|
-
self.layout_widgets["controls_width"].observe(
|
|
279
|
-
self._handle_container_width_change, names="value"
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
widgets_elements = [param_box, layout_box]
|
|
283
|
-
else:
|
|
284
|
-
widgets_elements = [param_box]
|
|
285
|
-
|
|
286
|
-
self.widgets_container = widgets.VBox(
|
|
287
|
-
widgets_elements,
|
|
288
|
-
layout=widgets.Layout(
|
|
289
|
-
width=(
|
|
290
|
-
f"{self.config.controls_width_percent}%"
|
|
291
|
-
if self.config.is_horizontal
|
|
292
|
-
else "100%"
|
|
293
|
-
),
|
|
294
|
-
padding="10px",
|
|
295
|
-
overflow_y="scroll",
|
|
296
|
-
border="1px solid #e5e7eb",
|
|
297
|
-
border_radius="4px 4px 0px 0px",
|
|
298
|
-
),
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
# Create plot container
|
|
302
|
-
self.plot_container = widgets.VBox(
|
|
303
|
-
[self.plot_output],
|
|
304
|
-
layout=widgets.Layout(
|
|
305
|
-
width=(
|
|
306
|
-
f"{100 - self.config.controls_width_percent}%"
|
|
307
|
-
if self.config.is_horizontal
|
|
308
|
-
else "100%"
|
|
309
|
-
),
|
|
310
|
-
padding="10px",
|
|
311
|
-
),
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
# Create final layout based on configuration
|
|
315
|
-
if self.config.controls_position == "left":
|
|
316
|
-
return widgets.HBox([self.widgets_container, self.plot_container])
|
|
317
|
-
elif self.config.controls_position == "right":
|
|
318
|
-
return widgets.HBox([self.plot_container, self.widgets_container])
|
|
319
|
-
elif self.config.controls_position == "bottom":
|
|
320
|
-
return widgets.VBox([self.plot_container, self.widgets_container])
|
|
321
|
-
else:
|
|
322
|
-
return widgets.VBox([self.widgets_container, self.plot_container])
|
|
323
|
-
|
|
324
|
-
def deploy(self) -> None:
|
|
325
|
-
"""Deploy the interactive viewer with proper state management."""
|
|
326
|
-
self.backend_type = get_backend_type()
|
|
327
|
-
|
|
328
|
-
# We used to use the deploy_app context, but notebook deployment works
|
|
329
|
-
# differently because it's asynchronous and this doesn't really behave
|
|
330
|
-
# as intended. (e.g. with self.viewer._deploy_app() ...)
|
|
331
|
-
|
|
332
|
-
# Create widgets
|
|
333
|
-
self._create_parameter_widgets()
|
|
334
|
-
|
|
335
|
-
# Create and display layout
|
|
336
|
-
self.layout = self._create_layout()
|
|
337
|
-
display(self.layout)
|
|
234
|
+
def _handle_container_width_change(self, _) -> None:
|
|
235
|
+
"""Handle changes to container width proportions."""
|
|
236
|
+
width_percent = self.controls["controls_width"].value
|
|
237
|
+
self.controls_width_percent = width_percent
|
|
338
238
|
|
|
339
|
-
#
|
|
340
|
-
self.
|
|
239
|
+
# Update container widths
|
|
240
|
+
self.widgets_container.layout.width = f"{width_percent}%"
|
|
241
|
+
self.plot_container.layout.width = f"{100 - width_percent}%"
|
|
@@ -31,7 +31,7 @@ class BaseWidget(Generic[T, W], ABC):
|
|
|
31
31
|
|
|
32
32
|
_widget: W
|
|
33
33
|
_callbacks: List[Dict[str, Union[Callable, Union[str, List[str]]]]]
|
|
34
|
-
|
|
34
|
+
is_action: bool = False
|
|
35
35
|
|
|
36
36
|
def __init__(
|
|
37
37
|
self,
|
|
@@ -465,7 +465,7 @@ class UnboundedFloatWidget(BaseWidget[UnboundedFloatParameter, widgets.FloatText
|
|
|
465
465
|
class ButtonWidget(BaseWidget[ButtonAction, widgets.Button]):
|
|
466
466
|
"""Widget for button parameters."""
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
is_action: bool = True
|
|
469
469
|
|
|
470
470
|
def _create_widget(
|
|
471
471
|
self,
|