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.
@@ -1,27 +1,13 @@
1
- from typing import Dict, Any, Optional
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 ..parameters import ParameterUpdateWarning
8
+ from ..support import ParameterUpdateWarning, plot_context
14
9
  from ..viewer import Viewer
15
- from .widgets import BaseWidget, create_widget
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: str = "left",
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 = False,
41
+ suppress_warnings: bool = True,
99
42
  ):
100
43
  self.viewer = viewer
101
- self.config = LayoutConfig(
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.parameter_widgets: Dict[str, BaseWidget] = {}
118
- self.plot_output = widgets.Output()
58
+ self._last_figure = None
119
59
 
120
- # Create layout for controls
121
- self.layout_widgets = self._create_layout_controls()
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
- # Flag to prevent circular updates
124
- self._updating = False
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
- # Last figure to close when new figures are created
127
- self._last_figure = None
76
+ def build_layout(self) -> None:
77
+ """Create the main layout combining controls and plot."""
128
78
 
129
- def _create_layout_controls(self) -> Dict[str, widgets.Widget]:
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
- if self.config.is_horizontal:
135
- controls["controls_width"] = widgets.IntSlider(
136
- value=self.config.controls_width_percent,
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
- return controls
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
- def _create_parameter_widgets(self) -> None:
148
- """Create widget instances for all parameters."""
149
- for name, param in self.viewer.parameters.items():
150
- widget = create_widget(
151
- param,
152
- continuous=self.continuous,
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
- # Store in widget dict
156
- self.parameter_widgets[name] = widget
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
- @debounce(0.1)
159
- def _handle_widget_engagement(self, name: str) -> None:
160
- """Handle engagement with an interactive widget."""
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
- widget = self.parameter_widgets[name]
178
-
179
- if widget._is_action:
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
- self.viewer.set_parameter_value(name, widget.value)
182
+ # Otherwise, update the parameter value
183
+ self.viewer.set_parameter_value(name, component.value)
184
184
 
185
- # Update any widgets that changed due to dependencies
186
- self._sync_widgets_with_state()
185
+ # Update any components that changed due to dependencies
186
+ self.sync_components_with_state()
187
187
 
188
188
  # Update the plot
189
- self._update_plot()
189
+ self.update_plot()
190
190
 
191
191
  finally:
192
192
  self._updating = False
193
193
 
194
- def _handle_action(self, name: str) -> None:
195
- """Handle actions for parameter widgets."""
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
- widget = self.parameter_widgets[name]
204
- if not widget.matches_parameter(parameter):
205
- widget.update_from_parameter(parameter)
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 _update_plot(self) -> None:
204
+ def update_plot(self) -> None:
224
205
  """Update the plot with current state."""
225
206
  state = self.viewer.state
226
207
 
227
- with _plot_context():
208
+ with plot_context():
228
209
  figure = self.viewer.plot(state)
229
210
 
230
- # Update widgets if plot function updated a parameter
231
- self._sync_widgets_with_state()
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 _create_layout(self) -> widgets.Widget:
255
- """Create the main layout combining controls and plot."""
256
- # Set up parameter widgets with their observe callbacks
257
- for name, widget in self.parameter_widgets.items():
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
- # Create initial plot
340
- self._update_plot()
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
- _is_action: bool = False
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
- _is_action: bool = True
468
+ is_action: bool = True
469
469
 
470
470
  def _create_widget(
471
471
  self,