syd 0.1.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 ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,213 @@
1
+ from typing import List, Any, Callable, Dict
2
+ from functools import wraps
3
+ from contextlib import contextmanager
4
+ from matplotlib.figure import Figure
5
+
6
+ from .parameters import ParameterType, Parameter
7
+
8
+
9
+ def validate_parameter_operation(operation: str, parameter_type: ParameterType) -> Callable:
10
+ """
11
+ Decorator that validates parameter operations for the InteractiveViewer class.
12
+
13
+ This decorator ensures that:
14
+ 1. The operation type matches the method name (add/update)
15
+ 2. The parameter type matches the method's intended parameter type
16
+ 3. Parameters can only be added when the app is not deployed
17
+ 4. Parameters can only be updated when the app is deployed
18
+ 5. For updates, validates that the parameter exists and is of the correct type
19
+
20
+ Args:
21
+ operation (str): The type of operation to validate. Must be either 'add' or 'update'.
22
+ parameter_type (ParameterType): The expected parameter type from the ParameterType enum.
23
+
24
+ Returns:
25
+ Callable: A decorated function that includes parameter validation.
26
+
27
+ Raises:
28
+ ValueError: If the operation type doesn't match the method name or if updating a non-existent parameter
29
+ TypeError: If updating a parameter with an incorrect type
30
+ RuntimeError: If adding parameters while deployed or updating while not deployed
31
+
32
+ Example:
33
+ @validate_parameter_operation('add', ParameterType.text)
34
+ def add_text(self, name: str, default: str = "") -> None:
35
+ ...
36
+ """
37
+
38
+ def decorator(func: Callable) -> Callable:
39
+ @wraps(func)
40
+ def wrapper(self: "InteractiveViewer", name: str, *args, **kwargs):
41
+ # Validate operation matches method name (add/update)
42
+ if not func.__name__.startswith(operation):
43
+ raise ValueError(f"Invalid operation type specified ({operation}) for method {func.__name__}")
44
+
45
+ # Validate deployment state
46
+ if operation == "add" and self._app_deployed:
47
+ raise RuntimeError("The app is currently deployed, cannot add a new parameter right now.")
48
+
49
+ # For updates, validate parameter existence and type
50
+ if operation == "update":
51
+ if name not in self.parameters:
52
+ raise ValueError(f"Parameter called {name} not found - you can only update registered parameters!")
53
+ if type(self.parameters[name]) != parameter_type:
54
+ msg = f"Parameter called {name} was found but is registered as a different parameter type ({type(self.parameters[name])})"
55
+ raise TypeError(msg)
56
+
57
+ return func(self, name, *args, **kwargs)
58
+
59
+ return wrapper
60
+
61
+ return decorator
62
+
63
+
64
+ class InteractiveViewer:
65
+ def __new__(cls):
66
+ instance = super().__new__(cls)
67
+ instance.parameters = {}
68
+ instance.callbacks = {}
69
+ instance.state = {}
70
+ instance._app_deployed = False
71
+ return instance
72
+
73
+ def __init__(self):
74
+ self.parameters: Dict[str, Parameter] = {}
75
+ self.callbacks: Dict[str, List[Callable]] = {}
76
+ self.state = {}
77
+ self._app_deployed = False
78
+
79
+ def param_dict(self) -> Dict[str, Any]:
80
+ return {name: param.value for name, param in self.parameters.items()}
81
+
82
+ def plot(self, **kwargs) -> Figure:
83
+ raise NotImplementedError("Subclasses must implement the plot method")
84
+
85
+ @contextmanager
86
+ def deploy_app(self):
87
+ """Internal context manager to control app deployment state"""
88
+ self._app_deployed = True
89
+ try:
90
+ yield
91
+ finally:
92
+ self._app_deployed = False
93
+
94
+ def perform_callbacks(self, name: str) -> bool:
95
+ """Perform callbacks for all parameters that have changed"""
96
+ if name in self.callbacks:
97
+ for callback in self.callbacks[name]:
98
+ callback(self.parameters[name].value)
99
+
100
+ def on_change(self, parameter_name: str, callback: Callable):
101
+ """Register a function to be called when a parameter changes."""
102
+ if parameter_name not in self.parameters:
103
+ raise ValueError(f"Parameter '{parameter_name}' is not registered!")
104
+ if parameter_name not in self.callbacks:
105
+ self.callbacks[parameter_name] = []
106
+ self.callbacks[parameter_name].append(callback)
107
+
108
+ def set_parameter_value(self, name: str, value: Any) -> None:
109
+ """Set a parameter value and trigger dependency updates"""
110
+ if name not in self.parameters:
111
+ raise ValueError(f"Parameter {name} not found")
112
+
113
+ # Update the parameter value
114
+ self.parameters[name].value = value
115
+
116
+ # Perform callbacks
117
+ self.perform_callbacks(name)
118
+
119
+ # -------------------- parameter registration methods --------------------
120
+ @validate_parameter_operation("add", ParameterType.text)
121
+ def add_text(self, name: str, default: str = "") -> None:
122
+ self.parameters[name] = ParameterType.text.value(name, default)
123
+
124
+ @validate_parameter_operation("add", ParameterType.selection)
125
+ def add_selection(self, name: str, options: List[Any], default: str = None) -> None:
126
+ self.parameters[name] = ParameterType.selection.value(name, options, default)
127
+
128
+ @validate_parameter_operation("add", ParameterType.multiple_selection)
129
+ def add_multiple_selection(self, name: str, options: List[Any], defaults: List[Any] = None) -> None:
130
+ self.parameters[name] = ParameterType.multiple_selection.value(name, options, defaults)
131
+
132
+ @validate_parameter_operation("add", ParameterType.boolean)
133
+ def add_boolean(self, name: str, default: bool = False) -> None:
134
+ self.parameters[name] = ParameterType.boolean.value(name, default)
135
+
136
+ @validate_parameter_operation("add", ParameterType.integer)
137
+ def add_integer(self, name: str, min_value: int, max_value: int, default: int = None) -> None:
138
+ self.parameters[name] = ParameterType.integer.value(name, min_value, max_value, default)
139
+
140
+ @validate_parameter_operation("add", ParameterType.float)
141
+ def add_float(self, name: str, min_value: float, max_value: float, step: float = 0.1, default: float = None) -> None:
142
+ self.parameters[name] = ParameterType.float.value(name, min_value, max_value, step, default)
143
+
144
+ @validate_parameter_operation("add", ParameterType.integer_pair)
145
+ def add_integer_pair(
146
+ self,
147
+ name: str,
148
+ min_value: int,
149
+ max_value: int,
150
+ default_low: int = None,
151
+ default_high: int = None,
152
+ ) -> None:
153
+ self.parameters[name] = ParameterType.integer_pair.value(name, min_value, max_value, default_low, default_high)
154
+
155
+ @validate_parameter_operation("add", ParameterType.float_pair)
156
+ def add_float_pair(
157
+ self,
158
+ name: str,
159
+ min_value: float,
160
+ max_value: float,
161
+ step: float = 0.1,
162
+ default_low: float = None,
163
+ default_high: float = None,
164
+ ) -> None:
165
+ self.parameters[name] = ParameterType.float_pair.value(name, min_value, max_value, step, default_low, default_high)
166
+
167
+ # -------------------- parameter update methods --------------------
168
+ @validate_parameter_operation("update", ParameterType.text)
169
+ def update_text(self, name: str, default: str = "") -> None:
170
+ self.parameters[name] = ParameterType.text.value(name, default)
171
+
172
+ @validate_parameter_operation("update", ParameterType.selection)
173
+ def update_selection(self, name: str, options: List[Any], default: str = None) -> None:
174
+ self.parameters[name] = ParameterType.selection.value(name, options, default)
175
+
176
+ @validate_parameter_operation("update", ParameterType.multiple_selection)
177
+ def update_multiple_selection(self, name: str, options: List[Any], defaults: List[Any] = None) -> None:
178
+ self.parameters[name] = ParameterType.multiple_selection.value(name, options, defaults)
179
+
180
+ @validate_parameter_operation("update", ParameterType.boolean)
181
+ def update_boolean(self, name: str, default: bool = False) -> None:
182
+ self.parameters[name] = ParameterType.boolean.value(name, default)
183
+
184
+ @validate_parameter_operation("update", ParameterType.integer)
185
+ def update_integer(self, name: str, min_value: int, max_value: int, default: int = None) -> None:
186
+ self.parameters[name] = ParameterType.integer.value(name, min_value, max_value, default)
187
+
188
+ @validate_parameter_operation("update", ParameterType.float)
189
+ def update_float(self, name: str, min_value: float, max_value: float, step: float = 0.1, default: float = None) -> None:
190
+ self.parameters[name] = ParameterType.float.value(name, min_value, max_value, step, default)
191
+
192
+ @validate_parameter_operation("update", ParameterType.integer_pair)
193
+ def update_integer_pair(
194
+ self,
195
+ name: str,
196
+ min_value: int,
197
+ max_value: int,
198
+ default_low: int = None,
199
+ default_high: int = None,
200
+ ) -> None:
201
+ self.parameters[name] = ParameterType.integer_pair.value(name, min_value, max_value, default_low, default_high)
202
+
203
+ @validate_parameter_operation("update", ParameterType.float_pair)
204
+ def update_float_pair(
205
+ self,
206
+ name: str,
207
+ min_value: float,
208
+ max_value: float,
209
+ step: float = 0.1,
210
+ default_low: float = None,
211
+ default_high: float = None,
212
+ ) -> None:
213
+ self.parameters[name] = ParameterType.float_pair.value(name, min_value, max_value, step, default_low, default_high)
syd/notebook_deploy.py ADDED
@@ -0,0 +1,270 @@
1
+ from typing import Dict, Any
2
+ import matplotlib.pyplot as plt
3
+ import ipywidgets as widgets
4
+ from IPython.display import display
5
+ from .parameters import (
6
+ Parameter,
7
+ TextParameter,
8
+ SingleSelectionParameter,
9
+ MultipleSelectionParameter,
10
+ BooleanParameter,
11
+ IntegerParameter,
12
+ FloatParameter,
13
+ IntegerPairParameter,
14
+ FloatPairParameter,
15
+ )
16
+ from .interactive_viewer import InteractiveViewer
17
+
18
+
19
+ class NotebookDeployment:
20
+ """
21
+ Deployment system for InteractiveViewer in Jupyter notebooks using ipywidgets.
22
+ Includes enhanced layout control and figure size management.
23
+ """
24
+
25
+ def __init__(self, viewer: InteractiveViewer):
26
+ """Initialize with an InteractiveViewer instance."""
27
+ self.viewer = viewer
28
+ self.widgets: Dict[str, widgets.Widget] = {}
29
+ self._widget_callbacks = {}
30
+ self._current_figure = None
31
+
32
+ # Default figure size
33
+ self.fig_width = 8
34
+ self.fig_height = 6
35
+
36
+ def _create_text_widget(self, param: TextParameter) -> widgets.Text:
37
+ """Create a text input widget."""
38
+ w = widgets.Text(value=param.default, description=param.name, layout=widgets.Layout(width="95%"), style={"description_width": "initial"})
39
+ return w
40
+
41
+ def _create_selection_widget(self, param: SingleSelectionParameter) -> widgets.Dropdown:
42
+ """Create a dropdown selection widget."""
43
+ w = widgets.Dropdown(
44
+ options=param.options,
45
+ value=param.default if param.default else param.options[0],
46
+ description=param.name,
47
+ layout=widgets.Layout(width="95%"),
48
+ style={"description_width": "initial"},
49
+ )
50
+ return w
51
+
52
+ def _create_multiple_selection_widget(self, param: MultipleSelectionParameter) -> widgets.SelectMultiple:
53
+ """Create a multiple selection widget."""
54
+ w = widgets.SelectMultiple(
55
+ options=param.options,
56
+ value=param.default if param.default else [],
57
+ description=param.name,
58
+ layout=widgets.Layout(width="95%"),
59
+ style={"description_width": "initial"},
60
+ )
61
+ return w
62
+
63
+ def _create_boolean_widget(self, param: BooleanParameter) -> widgets.Checkbox:
64
+ """Create a checkbox widget."""
65
+ w = widgets.Checkbox(value=param.default, description=param.name, layout=widgets.Layout(width="95%"), style={"description_width": "initial"})
66
+ return w
67
+
68
+ def _create_integer_widget(self, param: IntegerParameter) -> widgets.IntSlider:
69
+ """Create an integer slider widget."""
70
+ w = widgets.IntSlider(
71
+ value=param.default if param.default is not None else param.min_value,
72
+ min=param.min_value,
73
+ max=param.max_value,
74
+ description=param.name,
75
+ layout=widgets.Layout(width="95%"),
76
+ style={"description_width": "initial"},
77
+ )
78
+ return w
79
+
80
+ def _create_float_widget(self, param: FloatParameter) -> widgets.FloatSlider:
81
+ """Create a float slider widget."""
82
+ w = widgets.FloatSlider(
83
+ value=param.default if param.default is not None else param.min_value,
84
+ min=param.min_value,
85
+ max=param.max_value,
86
+ step=param.step,
87
+ description=param.name,
88
+ layout=widgets.Layout(width="95%"),
89
+ style={"description_width": "initial"},
90
+ )
91
+ return w
92
+
93
+ def _create_integer_pair_widget(self, param: IntegerPairParameter) -> widgets.HBox:
94
+ """Create a pair of integer input widgets."""
95
+ low = widgets.IntText(
96
+ value=param.default_low if param.default_low is not None else param.min_value,
97
+ description=f"{param.name} (low)",
98
+ layout=widgets.Layout(width="47%"),
99
+ style={"description_width": "initial"},
100
+ )
101
+ high = widgets.IntText(
102
+ value=param.default_high if param.default_high is not None else param.max_value,
103
+ description=f"{param.name} (high)",
104
+ layout=widgets.Layout(width="47%"),
105
+ style={"description_width": "initial"},
106
+ )
107
+ return widgets.HBox([low, high], layout=widgets.Layout(width="95%"))
108
+
109
+ def _create_float_pair_widget(self, param: FloatPairParameter) -> widgets.HBox:
110
+ """Create a pair of float input widgets."""
111
+ low = widgets.FloatText(
112
+ value=param.default_low if param.default_low is not None else param.min_value,
113
+ description=f"{param.name} (low)",
114
+ layout=widgets.Layout(width="47%"),
115
+ style={"description_width": "initial"},
116
+ )
117
+ high = widgets.FloatText(
118
+ value=param.default_high if param.default_high is not None else param.max_value,
119
+ description=f"{param.name} (high)",
120
+ layout=widgets.Layout(width="47%"),
121
+ style={"description_width": "initial"},
122
+ )
123
+ return widgets.HBox([low, high], layout=widgets.Layout(width="95%"))
124
+
125
+ def _create_widget_for_parameter(self, param: Parameter) -> widgets.Widget:
126
+ """Create the appropriate widget based on parameter type."""
127
+ widget_creators = {
128
+ TextParameter: self._create_text_widget,
129
+ SingleSelectionParameter: self._create_selection_widget,
130
+ MultipleSelectionParameter: self._create_multiple_selection_widget,
131
+ BooleanParameter: self._create_boolean_widget,
132
+ IntegerParameter: self._create_integer_widget,
133
+ FloatParameter: self._create_float_widget,
134
+ IntegerPairParameter: self._create_integer_pair_widget,
135
+ FloatPairParameter: self._create_float_pair_widget,
136
+ }
137
+
138
+ creator = widget_creators.get(type(param))
139
+ if not creator:
140
+ raise ValueError(f"Unsupported parameter type: {type(param)}")
141
+
142
+ return creator(param)
143
+
144
+ def _create_size_controls(self) -> widgets.VBox:
145
+ """Create controls for adjusting the figure size."""
146
+ self.width_slider = widgets.FloatSlider(
147
+ value=self.fig_width,
148
+ min=4,
149
+ max=20,
150
+ step=0.5,
151
+ description="Figure Width",
152
+ layout=widgets.Layout(width="95%"),
153
+ style={"description_width": "initial"},
154
+ )
155
+
156
+ self.height_slider = widgets.FloatSlider(
157
+ value=self.fig_height,
158
+ min=3,
159
+ max=15,
160
+ step=0.5,
161
+ description="Figure Height",
162
+ layout=widgets.Layout(width="95%"),
163
+ style={"description_width": "initial"},
164
+ )
165
+
166
+ self.container_width = widgets.IntSlider(
167
+ value=30,
168
+ min=20,
169
+ max=80,
170
+ description="Controls Width %",
171
+ layout=widgets.Layout(width="95%"),
172
+ style={"description_width": "initial"},
173
+ )
174
+
175
+ # Add callbacks for size changes
176
+ self.width_slider.observe(self._handle_size_change, names="value")
177
+ self.height_slider.observe(self._handle_size_change, names="value")
178
+ self.container_width.observe(self._handle_container_width_change, names="value")
179
+
180
+ return widgets.VBox(
181
+ [widgets.HTML("<b>Layout Controls</b>"), self.width_slider, self.height_slider, self.container_width],
182
+ layout=widgets.Layout(margin="10px 0px"),
183
+ )
184
+
185
+ def _handle_size_change(self, change: Dict[str, Any]) -> None:
186
+ """Handle changes to figure size."""
187
+ self.fig_width = self.width_slider.value
188
+ self.fig_height = self.height_slider.value
189
+ self._update_plot()
190
+
191
+ def _handle_container_width_change(self, change: Dict[str, Any]) -> None:
192
+ """Handle changes to container widths."""
193
+ controls_width = f"{self.container_width.value}%"
194
+ plot_width = f"{100 - self.container_width.value}%"
195
+
196
+ self.widgets_container.layout.width = controls_width
197
+ self.plot_container.layout.width = plot_width
198
+
199
+ def _handle_widget_change(self, name: str, change: Dict[str, Any]) -> None:
200
+ """Handle widget value changes and update the viewer parameter."""
201
+ if isinstance(self.widgets[name], widgets.HBox):
202
+ # Handle pair widgets
203
+ low_value = self.widgets[name].children[0].value
204
+ high_value = self.widgets[name].children[1].value
205
+ value = (low_value, high_value)
206
+ else:
207
+ value = change["new"]
208
+
209
+ self.viewer.set_parameter_value(name, value)
210
+ self._update_plot()
211
+
212
+ def _update_plot(self) -> None:
213
+ """Update the plot with current parameters and size."""
214
+ self._current_fig.clear()
215
+ self._current_fig.set_size_inches(self.fig_width, self.fig_height)
216
+ self._current_fig = self.viewer.plot(fig=self._current_fig, state=self.viewer.param_dict())
217
+
218
+ # Apply tight layout to remove dead space
219
+ self._current_fig.tight_layout()
220
+
221
+ self.plot_output.clear_output(wait=True)
222
+ with self.plot_output:
223
+ display(self._current_fig)
224
+
225
+ def create_widgets(self) -> None:
226
+ """Create widgets for all parameters in the viewer."""
227
+ for name, param in self.viewer.parameters.items():
228
+ widget = self._create_widget_for_parameter(param)
229
+ self.widgets[name] = widget
230
+
231
+ # Set up callback
232
+ if isinstance(widget, widgets.HBox):
233
+ # For pair widgets, observe both components
234
+ widget.children[0].observe(lambda change, n=name: self._handle_widget_change(n, change), names="value")
235
+ widget.children[1].observe(lambda change, n=name: self._handle_widget_change(n, change), names="value")
236
+ else:
237
+ widget.observe(lambda change, n=name: self._handle_widget_change(n, change), names="value")
238
+
239
+ def display_widgets(self) -> None:
240
+ """Display all widgets and plot in an organized layout."""
241
+ # Create size controls
242
+ size_controls = self._create_size_controls()
243
+
244
+ # Create widgets container with parameters and size controls
245
+ all_widgets = list(self.widgets.values()) + [size_controls]
246
+ self.widgets_container = widgets.VBox(all_widgets, layout=widgets.Layout(width="30%", padding="10px", overflow_y="auto"))
247
+
248
+ # Create output widget for plot
249
+ self.plot_output = widgets.Output()
250
+ self.plot_container = widgets.VBox([self.plot_output], layout=widgets.Layout(width="70%", padding="10px"))
251
+
252
+ # Combine widgets and plot in layout
253
+ # Make the container height dynamic
254
+ layout = widgets.HBox(
255
+ [self.widgets_container, self.plot_container], layout=widgets.Layout(width="100%", height="auto") # Dynamic height based on content
256
+ )
257
+ display(layout)
258
+
259
+ # Create initial plot
260
+ self._current_fig = plt.figure(figsize=(self.fig_width, self.fig_height))
261
+ plt.close(self._current_fig) # close the figure to prevent it from being displayed
262
+ self._current_fig = self.viewer.plot(fig=self._current_fig, state=self.viewer.param_dict())
263
+ with self.plot_output:
264
+ display(self._current_fig)
265
+
266
+ def deploy(self) -> None:
267
+ """Create and display all widgets with proper deployment state management."""
268
+ with self.viewer.deploy_app():
269
+ self.create_widgets()
270
+ self.display_widgets()