syd 0.1.3__py3-none-any.whl → 0.1.5__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 +15 -1
- syd/interactive_viewer.py +1139 -81
- syd/notebook_deploy/__init__.py +1 -0
- syd/notebook_deploy/deployer.py +237 -0
- syd/notebook_deploy/widgets.py +471 -0
- syd/parameters.py +1135 -154
- {syd-0.1.3.dist-info → syd-0.1.5.dist-info}/METADATA +10 -2
- syd-0.1.5.dist-info/RECORD +10 -0
- syd/notebook_deploy.py +0 -277
- syd-0.1.3.dist-info/RECORD +0 -8
- {syd-0.1.3.dist-info → syd-0.1.5.dist-info}/WHEEL +0 -0
- {syd-0.1.3.dist-info → syd-0.1.5.dist-info}/licenses/LICENSE +0 -0
syd/parameters.py
CHANGED
|
@@ -1,269 +1,1250 @@
|
|
|
1
|
-
from typing import List, Any, Tuple, Generic, TypeVar,
|
|
1
|
+
from typing import List, Any, Tuple, Generic, TypeVar, Optional, Dict, Callable
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from enum import Enum
|
|
5
|
+
from copy import deepcopy
|
|
5
6
|
from warnings import warn
|
|
6
7
|
|
|
7
8
|
T = TypeVar("T")
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
# Keep original Parameter class and exceptions unchanged
|
|
12
|
+
class ParameterAddError(Exception):
|
|
13
|
+
"""
|
|
14
|
+
Exception raised when there is an error creating a new parameter.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
parameter_name : str
|
|
19
|
+
Name of the parameter that failed to be created
|
|
20
|
+
parameter_type : str
|
|
21
|
+
Type of the parameter that failed to be created
|
|
22
|
+
message : str, optional
|
|
23
|
+
Additional error details
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, parameter_name: str, parameter_type: str, message: str = None):
|
|
27
|
+
self.parameter_name = parameter_name
|
|
28
|
+
self.parameter_type = parameter_type
|
|
29
|
+
super().__init__(
|
|
30
|
+
f"Failed to create {parameter_type} parameter '{parameter_name}'"
|
|
31
|
+
+ (f": {message}" if message else "")
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ParameterUpdateError(Exception):
|
|
36
|
+
"""
|
|
37
|
+
Exception raised when there is an error updating an existing parameter.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
parameter_name : str
|
|
42
|
+
Name of the parameter that failed to update
|
|
43
|
+
parameter_type : str
|
|
44
|
+
Type of the parameter that failed to update
|
|
45
|
+
message : str, optional
|
|
46
|
+
Additional error details
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, parameter_name: str, parameter_type: str, message: str = None):
|
|
50
|
+
self.parameter_name = parameter_name
|
|
51
|
+
self.parameter_type = parameter_type
|
|
52
|
+
super().__init__(
|
|
53
|
+
f"Failed to update {parameter_type} parameter '{parameter_name}'"
|
|
54
|
+
+ (f": {message}" if message else "")
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_parameter_attributes(param_class) -> List[str]:
|
|
59
|
+
"""
|
|
60
|
+
Get all valid attributes for a parameter class.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
param_class : class
|
|
65
|
+
The parameter class to inspect
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
list of str
|
|
70
|
+
Names of all valid attributes for the parameter class
|
|
71
|
+
"""
|
|
72
|
+
attributes = []
|
|
73
|
+
|
|
74
|
+
# Walk through class hierarchy in reverse (most specific to most general)
|
|
75
|
+
for cls in reversed(param_class.__mro__):
|
|
76
|
+
if hasattr(cls, "__annotations__"):
|
|
77
|
+
# Only add annotations that haven't been specified by a more specific class
|
|
78
|
+
for name in cls.__annotations__:
|
|
79
|
+
if not name.startswith("_"):
|
|
80
|
+
attributes.append(name)
|
|
81
|
+
|
|
82
|
+
return attributes
|
|
83
|
+
|
|
84
|
+
|
|
10
85
|
@dataclass
|
|
11
86
|
class Parameter(Generic[T], ABC):
|
|
12
|
-
"""
|
|
87
|
+
"""
|
|
88
|
+
Base class for all parameter types. Parameters are the building blocks
|
|
89
|
+
for creating interactive GUI elements.
|
|
90
|
+
|
|
91
|
+
Each parameter has a name and a value, and ensures the value stays valid
|
|
92
|
+
through validation rules.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
name : str
|
|
97
|
+
The name of the parameter, used as a label in the GUI
|
|
98
|
+
value : T
|
|
99
|
+
The current value of the parameter
|
|
100
|
+
|
|
101
|
+
Notes
|
|
102
|
+
-----
|
|
103
|
+
This is an abstract base class - you should use one of the concrete parameter
|
|
104
|
+
types like TextParameter, BooleanParameter, etc. instead of using this directly.
|
|
105
|
+
"""
|
|
13
106
|
|
|
14
107
|
name: str
|
|
108
|
+
value: T
|
|
15
109
|
|
|
16
110
|
@abstractmethod
|
|
17
|
-
def __init__(self, name: str,
|
|
111
|
+
def __init__(self, name: str, value: T):
|
|
18
112
|
raise NotImplementedError("Need to define in subclass for proper IDE support")
|
|
19
113
|
|
|
20
114
|
@property
|
|
21
115
|
def value(self) -> T:
|
|
116
|
+
"""
|
|
117
|
+
Get the current value of the parameter.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
T
|
|
122
|
+
The current value
|
|
123
|
+
"""
|
|
22
124
|
return self._value
|
|
23
125
|
|
|
24
126
|
@value.setter
|
|
25
|
-
def value(self, new_value: T):
|
|
127
|
+
def value(self, new_value: T) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Set a new value for the parameter. The value will be validated before being set.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
new_value : T
|
|
134
|
+
The new value to set
|
|
135
|
+
|
|
136
|
+
Raises
|
|
137
|
+
------
|
|
138
|
+
ValueError
|
|
139
|
+
If the new value is invalid for this parameter type
|
|
140
|
+
"""
|
|
26
141
|
self._value = self._validate(new_value)
|
|
27
142
|
|
|
28
143
|
@abstractmethod
|
|
29
144
|
def _validate(self, new_value: Any) -> T:
|
|
30
145
|
raise NotImplementedError
|
|
31
146
|
|
|
147
|
+
def update(self, updates: Dict[str, Any]) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Safely update multiple parameter attributes at once.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
updates : dict
|
|
154
|
+
Dictionary of attribute names and their new values
|
|
155
|
+
|
|
156
|
+
Raises
|
|
157
|
+
------
|
|
158
|
+
ParameterUpdateError
|
|
159
|
+
If any of the updates are invalid
|
|
160
|
+
|
|
161
|
+
Examples
|
|
162
|
+
--------
|
|
163
|
+
>>> param = FloatParameter("temperature", 20.0, min_value=0, max_value=100)
|
|
164
|
+
>>> param.update({"value": 25.0, "max_value": 150})
|
|
165
|
+
"""
|
|
166
|
+
param_copy = deepcopy(self)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
param_copy._unsafe_update(updates)
|
|
170
|
+
|
|
171
|
+
for key, value in vars(param_copy).items():
|
|
172
|
+
if not key.startswith("_"):
|
|
173
|
+
setattr(self, key, value)
|
|
174
|
+
self.value = param_copy.value
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
if isinstance(e, ValueError):
|
|
178
|
+
raise ParameterUpdateError(
|
|
179
|
+
self.name, type(self).__name__, str(e)
|
|
180
|
+
) from e
|
|
181
|
+
else:
|
|
182
|
+
raise ParameterUpdateError(
|
|
183
|
+
self.name, type(self).__name__, f"Update failed: {str(e)}"
|
|
184
|
+
) from e
|
|
185
|
+
|
|
186
|
+
def _unsafe_update(self, updates: Dict[str, Any]) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Internal update method that applies changes without safety copies.
|
|
189
|
+
|
|
190
|
+
Validates attribute names but applies updates directly to instance.
|
|
191
|
+
Called by public update() method inside a deepcopy context.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
updates: Dict mapping attribute names to new values
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
ValueError: If trying to update 'name' or invalid attributes
|
|
198
|
+
"""
|
|
199
|
+
valid_attributes = get_parameter_attributes(type(self))
|
|
200
|
+
|
|
201
|
+
for key, new_value in updates.items():
|
|
202
|
+
if key == "name":
|
|
203
|
+
raise ValueError("Cannot update parameter name")
|
|
204
|
+
elif key not in valid_attributes:
|
|
205
|
+
raise ValueError(f"Update failed, {key} is not a valid attribute")
|
|
206
|
+
|
|
207
|
+
for key, new_value in updates.items():
|
|
208
|
+
if key != "value":
|
|
209
|
+
setattr(self, key, new_value)
|
|
210
|
+
|
|
211
|
+
if "value" in updates:
|
|
212
|
+
self.value = updates["value"]
|
|
213
|
+
|
|
214
|
+
self._validate_update()
|
|
215
|
+
|
|
216
|
+
def _validate_update(self) -> None:
|
|
217
|
+
"""
|
|
218
|
+
Hook for validating complete parameter state after updates.
|
|
219
|
+
|
|
220
|
+
Called at end of _unsafe_update(). Default implementation does nothing.
|
|
221
|
+
Override in subclasses to add validation logic.
|
|
222
|
+
"""
|
|
223
|
+
pass
|
|
224
|
+
|
|
32
225
|
|
|
33
226
|
@dataclass(init=False)
|
|
34
227
|
class TextParameter(Parameter[str]):
|
|
35
|
-
|
|
228
|
+
"""
|
|
229
|
+
Parameter for text input.
|
|
230
|
+
|
|
231
|
+
Creates a text box in the GUI that accepts any string input.
|
|
232
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_text` and
|
|
233
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_text` for usage.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
name : str
|
|
238
|
+
The name of the parameter
|
|
239
|
+
value : str
|
|
240
|
+
The initial text value
|
|
241
|
+
|
|
242
|
+
Examples
|
|
243
|
+
--------
|
|
244
|
+
>>> name_param = TextParameter("username", "Alice")
|
|
245
|
+
>>> name_param.value
|
|
246
|
+
'Alice'
|
|
247
|
+
>>> name_param.update({"value": "Bob"})
|
|
248
|
+
>>> name_param.value
|
|
249
|
+
'Bob'
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, name: str, value: str):
|
|
36
253
|
self.name = name
|
|
37
|
-
self.
|
|
38
|
-
self._value = self._validate(default)
|
|
254
|
+
self._value = self._validate(value)
|
|
39
255
|
|
|
40
256
|
def _validate(self, new_value: Any) -> str:
|
|
257
|
+
"""
|
|
258
|
+
Convert input to string.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
new_value: Value to convert
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
String representation of input value
|
|
265
|
+
"""
|
|
41
266
|
return str(new_value)
|
|
42
267
|
|
|
43
268
|
|
|
44
269
|
@dataclass(init=False)
|
|
45
|
-
class
|
|
270
|
+
class BooleanParameter(Parameter[bool]):
|
|
271
|
+
"""
|
|
272
|
+
Parameter for boolean values.
|
|
273
|
+
|
|
274
|
+
Creates a checkbox in the GUI that can be toggled on/off.
|
|
275
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_boolean` and
|
|
276
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_boolean` for usage.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
name : str
|
|
281
|
+
The name of the parameter
|
|
282
|
+
value : bool, optional
|
|
283
|
+
The initial state (default is True)
|
|
284
|
+
|
|
285
|
+
Examples
|
|
286
|
+
--------
|
|
287
|
+
>>> active = BooleanParameter("is_active", True)
|
|
288
|
+
>>> active.value
|
|
289
|
+
True
|
|
290
|
+
>>> active.update({"value": False})
|
|
291
|
+
>>> active.value
|
|
292
|
+
False
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
def __init__(self, name: str, value: bool = True):
|
|
296
|
+
self.name = name
|
|
297
|
+
self._value = self._validate(value)
|
|
298
|
+
|
|
299
|
+
def _validate(self, new_value: Any) -> bool:
|
|
300
|
+
"""
|
|
301
|
+
Convert input to boolean.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
new_value: Value to convert
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Boolean interpretation of input value using Python's bool() rules
|
|
308
|
+
"""
|
|
309
|
+
return bool(new_value)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass(init=False)
|
|
313
|
+
class SelectionParameter(Parameter[Any]):
|
|
314
|
+
"""
|
|
315
|
+
Parameter for single selection from a list of options.
|
|
316
|
+
|
|
317
|
+
Creates a dropdown menu in the GUI where users can select one option.
|
|
318
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_selection` and
|
|
319
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_selection` for usage.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
name : str
|
|
324
|
+
The name of the parameter
|
|
325
|
+
value : Any
|
|
326
|
+
The initially selected value (must be one of the options)
|
|
327
|
+
options : list
|
|
328
|
+
List of valid choices that can be selected
|
|
329
|
+
|
|
330
|
+
Examples
|
|
331
|
+
--------
|
|
332
|
+
>>> color = SelectionParameter("color", "red", options=["red", "green", "blue"])
|
|
333
|
+
>>> color.value
|
|
334
|
+
'red'
|
|
335
|
+
>>> color.update({"value": "blue"})
|
|
336
|
+
>>> color.value
|
|
337
|
+
'blue'
|
|
338
|
+
>>> color.update({"value": "yellow"}) # This will raise an error
|
|
339
|
+
"""
|
|
340
|
+
|
|
46
341
|
options: List[Any]
|
|
47
342
|
|
|
48
|
-
def __init__(self, name: str,
|
|
343
|
+
def __init__(self, name: str, value: Any, options: List[Any]):
|
|
49
344
|
self.name = name
|
|
50
345
|
self.options = options
|
|
51
|
-
self.
|
|
52
|
-
self._value = self._validate(self.default)
|
|
346
|
+
self._value = self._validate(value)
|
|
53
347
|
|
|
54
348
|
def _validate(self, new_value: Any) -> Any:
|
|
349
|
+
"""
|
|
350
|
+
Validate that value is one of the allowed options.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
new_value: Value to validate
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Input value if valid
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
ValueError: If value is not in options list
|
|
360
|
+
"""
|
|
55
361
|
if new_value not in self.options:
|
|
56
362
|
raise ValueError(f"Value {new_value} not in options: {self.options}")
|
|
57
363
|
return new_value
|
|
58
364
|
|
|
365
|
+
def _validate_update(self) -> None:
|
|
366
|
+
"""
|
|
367
|
+
Validate complete parameter state after updates.
|
|
368
|
+
|
|
369
|
+
Ensures options is a list/tuple and current value is valid.
|
|
370
|
+
Sets value to first option if current value becomes invalid.
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
TypeError: If options is not a list or tuple
|
|
374
|
+
"""
|
|
375
|
+
if not isinstance(self.options, (list, tuple)):
|
|
376
|
+
raise TypeError(
|
|
377
|
+
f"Options for parameter {self.name} are not a list or tuple: {self.options}"
|
|
378
|
+
)
|
|
379
|
+
if self.value not in self.options:
|
|
380
|
+
warn(
|
|
381
|
+
f"Value {self.value} not in options: {self.options}, setting to first option"
|
|
382
|
+
)
|
|
383
|
+
self.value = self.options[0]
|
|
384
|
+
|
|
59
385
|
|
|
60
386
|
@dataclass(init=False)
|
|
61
387
|
class MultipleSelectionParameter(Parameter[List[Any]]):
|
|
388
|
+
"""
|
|
389
|
+
Parameter for multiple selections from a list of options.
|
|
390
|
+
|
|
391
|
+
Creates a set of checkboxes or multi-select dropdown in the GUI.
|
|
392
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_multiple_selection` and
|
|
393
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_multiple_selection` for usage.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
name : str
|
|
398
|
+
The name of the parameter
|
|
399
|
+
value : list
|
|
400
|
+
List of initially selected values (must all be from options)
|
|
401
|
+
options : list
|
|
402
|
+
List of valid choices that can be selected
|
|
403
|
+
|
|
404
|
+
Examples
|
|
405
|
+
--------
|
|
406
|
+
>>> toppings = MultipleSelectionParameter("pizza_toppings",
|
|
407
|
+
... value=["cheese", "mushrooms"],
|
|
408
|
+
... options=["cheese", "mushrooms", "pepperoni", "olives"])
|
|
409
|
+
>>> toppings.value
|
|
410
|
+
['cheese', 'mushrooms']
|
|
411
|
+
>>> toppings.update({"value": ["cheese", "pepperoni"]})
|
|
412
|
+
>>> toppings.value
|
|
413
|
+
['cheese', 'pepperoni']
|
|
414
|
+
"""
|
|
415
|
+
|
|
62
416
|
options: List[Any]
|
|
63
417
|
|
|
64
|
-
def __init__(self, name: str,
|
|
418
|
+
def __init__(self, name: str, value: List[Any], options: List[Any]):
|
|
65
419
|
self.name = name
|
|
66
|
-
self.default = default or []
|
|
67
420
|
self.options = options
|
|
68
|
-
self._value = self._validate(
|
|
421
|
+
self._value = self._validate(value)
|
|
422
|
+
|
|
423
|
+
def _validate(self, new_value: Any) -> List[Any]:
|
|
424
|
+
"""
|
|
425
|
+
Validate list of selected values against options.
|
|
69
426
|
|
|
70
|
-
|
|
427
|
+
Ensures value is a list/tuple and all elements are in options.
|
|
428
|
+
Preserves order based on options list while removing duplicates.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
new_value: List of selected values
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Validated list of unique values in options order
|
|
435
|
+
|
|
436
|
+
Raises:
|
|
437
|
+
TypeError: If value is not a list/tuple
|
|
438
|
+
ValueError: If any value is not in options
|
|
439
|
+
"""
|
|
71
440
|
if not isinstance(new_value, (list, tuple)):
|
|
72
|
-
raise TypeError(f"
|
|
73
|
-
|
|
74
|
-
|
|
441
|
+
raise TypeError(f"Value must be a list or tuple")
|
|
442
|
+
invalid = [val for val in new_value if val not in self.options]
|
|
443
|
+
if invalid:
|
|
75
444
|
raise ValueError(f"Values {invalid} not in options: {self.options}")
|
|
76
|
-
|
|
445
|
+
# Keep only unique values while preserving order based on self.options
|
|
446
|
+
return [x for x in self.options if x in new_value]
|
|
447
|
+
|
|
448
|
+
def _validate_update(self) -> None:
|
|
449
|
+
if not isinstance(self.options, (list, tuple)):
|
|
450
|
+
raise TypeError(
|
|
451
|
+
f"Options for parameter {self.name} are not a list or tuple: {self.options}"
|
|
452
|
+
)
|
|
453
|
+
if not isinstance(self.value, (list, tuple)):
|
|
454
|
+
warn(
|
|
455
|
+
f"For parameter {self.name}, value {self.value} is not a list or tuple. Setting to empty list."
|
|
456
|
+
)
|
|
457
|
+
self.value = []
|
|
458
|
+
if not all(val in self.options for val in self.value):
|
|
459
|
+
invalid = [val for val in self.value if val not in self.options]
|
|
460
|
+
warn(
|
|
461
|
+
f"For parameter {self.name}, value {self.value} contains invalid options: {invalid}. Setting to empty list."
|
|
462
|
+
)
|
|
463
|
+
self.value = []
|
|
464
|
+
# Keep only unique values while preserving order based on self.options
|
|
465
|
+
seen = set()
|
|
466
|
+
self.options = [x for x in self.options if not (x in seen or seen.add(x))]
|
|
77
467
|
|
|
78
468
|
|
|
79
469
|
@dataclass(init=False)
|
|
80
|
-
class
|
|
81
|
-
|
|
470
|
+
class IntegerParameter(Parameter[int]):
|
|
471
|
+
"""
|
|
472
|
+
Parameter for bounded integer values.
|
|
473
|
+
|
|
474
|
+
Creates a slider in the GUI for selecting whole numbers between bounds.
|
|
475
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer` and
|
|
476
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_integer` for usage.
|
|
477
|
+
|
|
478
|
+
Parameters
|
|
479
|
+
----------
|
|
480
|
+
name : str
|
|
481
|
+
The name of the parameter
|
|
482
|
+
value : int
|
|
483
|
+
Initial value (will be clamped to fit between min_value and max_value)
|
|
484
|
+
min_value : int
|
|
485
|
+
Minimum allowed value
|
|
486
|
+
max_value : int
|
|
487
|
+
Maximum allowed value
|
|
488
|
+
|
|
489
|
+
Examples
|
|
490
|
+
--------
|
|
491
|
+
>>> age = IntegerParameter("age", value=25, min_value=0, max_value=120)
|
|
492
|
+
>>> age.value
|
|
493
|
+
25
|
|
494
|
+
>>> age.update({"value": 150}) # Will be clamped to max_value
|
|
495
|
+
>>> age.value
|
|
496
|
+
120
|
|
497
|
+
>>> age.update({"value": -10}) # Will be clamped to min_value
|
|
498
|
+
>>> age.value
|
|
499
|
+
0
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
min_value: int
|
|
503
|
+
max_value: int
|
|
504
|
+
|
|
505
|
+
def __init__(
|
|
506
|
+
self,
|
|
507
|
+
name: str,
|
|
508
|
+
value: int,
|
|
509
|
+
min_value: int,
|
|
510
|
+
max_value: int,
|
|
511
|
+
):
|
|
82
512
|
self.name = name
|
|
83
|
-
self.
|
|
84
|
-
self.
|
|
513
|
+
self.min_value = self._validate(min_value, compare_to_range=False)
|
|
514
|
+
self.max_value = self._validate(max_value, compare_to_range=False)
|
|
515
|
+
self._value = self._validate(value)
|
|
85
516
|
|
|
86
|
-
def _validate(self, new_value: Any) ->
|
|
87
|
-
|
|
517
|
+
def _validate(self, new_value: Any, compare_to_range: bool = True) -> int:
|
|
518
|
+
"""
|
|
519
|
+
Validate and convert value to integer, optionally checking bounds.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
new_value: Value to validate
|
|
523
|
+
compare_to_range: If True, clamps value to min/max bounds
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
Validated integer value
|
|
527
|
+
|
|
528
|
+
Raises:
|
|
529
|
+
ValueError: If value cannot be converted to int
|
|
530
|
+
"""
|
|
531
|
+
try:
|
|
532
|
+
new_value = int(new_value)
|
|
533
|
+
except ValueError:
|
|
534
|
+
raise ValueError(f"Value {new_value} cannot be converted to int")
|
|
535
|
+
|
|
536
|
+
if compare_to_range:
|
|
537
|
+
if new_value < self.min_value:
|
|
538
|
+
warn(f"Value {new_value} below minimum {self.min_value}, clamping")
|
|
539
|
+
new_value = self.min_value
|
|
540
|
+
if new_value > self.max_value:
|
|
541
|
+
warn(f"Value {new_value} above maximum {self.max_value}, clamping")
|
|
542
|
+
new_value = self.max_value
|
|
543
|
+
return int(new_value)
|
|
544
|
+
|
|
545
|
+
def _validate_update(self) -> None:
|
|
546
|
+
"""
|
|
547
|
+
Validate complete parameter state after updates.
|
|
548
|
+
|
|
549
|
+
Ensures min_value <= max_value, swapping if needed.
|
|
550
|
+
Re-validates current value against potentially updated bounds.
|
|
551
|
+
|
|
552
|
+
Raises:
|
|
553
|
+
ParameterUpdateError: If bounds are invalid (e.g. None when required)
|
|
554
|
+
"""
|
|
555
|
+
if self.min_value is None or self.max_value is None:
|
|
556
|
+
raise ParameterUpdateError(
|
|
557
|
+
self.name,
|
|
558
|
+
type(self).__name__,
|
|
559
|
+
"IntegerParameter must have both min_value and max_value bounds",
|
|
560
|
+
)
|
|
561
|
+
if self.min_value > self.max_value:
|
|
562
|
+
warn(f"Min value greater than max value, swapping")
|
|
563
|
+
self.min_value, self.max_value = self.max_value, self.min_value
|
|
564
|
+
self.value = self._validate(self.value)
|
|
88
565
|
|
|
89
566
|
|
|
90
567
|
@dataclass(init=False)
|
|
91
|
-
class
|
|
92
|
-
|
|
93
|
-
|
|
568
|
+
class FloatParameter(Parameter[float]):
|
|
569
|
+
"""
|
|
570
|
+
Parameter for bounded decimal numbers.
|
|
571
|
+
|
|
572
|
+
Creates a slider in the GUI for selecting numbers between bounds.
|
|
573
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_float` and
|
|
574
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_float` for usage.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
name : str
|
|
579
|
+
The name of the parameter
|
|
580
|
+
value : float
|
|
581
|
+
Initial value (will be clamped to fit between min_value and max_value)
|
|
582
|
+
min_value : float
|
|
583
|
+
Minimum allowed value
|
|
584
|
+
max_value : float
|
|
585
|
+
Maximum allowed value
|
|
586
|
+
step : float, optional
|
|
587
|
+
Size of each increment (default is 0.1)
|
|
588
|
+
|
|
589
|
+
Examples
|
|
590
|
+
--------
|
|
591
|
+
>>> temp = FloatParameter("temperature", value=98.6,
|
|
592
|
+
... min_value=95.0, max_value=105.0, step=0.1)
|
|
593
|
+
>>> temp.value
|
|
594
|
+
98.6
|
|
595
|
+
>>> temp.update({"value": 98.67}) # Will be rounded to nearest step
|
|
596
|
+
>>> temp.value
|
|
597
|
+
98.7
|
|
598
|
+
>>> temp.update({"value": 110.0}) # Will be clamped to max_value
|
|
599
|
+
>>> temp.value
|
|
600
|
+
105.0
|
|
601
|
+
|
|
602
|
+
Notes
|
|
603
|
+
-----
|
|
604
|
+
The step parameter determines how finely you can adjust the value. For example:
|
|
605
|
+
- step=0.1 allows values like 1.0, 1.1, 1.2, etc.
|
|
606
|
+
- step=0.01 allows values like 1.00, 1.01, 1.02, etc.
|
|
607
|
+
- step=5.0 allows values like 0.0, 5.0, 10.0, etc.
|
|
608
|
+
"""
|
|
94
609
|
|
|
95
|
-
|
|
610
|
+
min_value: float
|
|
611
|
+
max_value: float
|
|
612
|
+
step: float
|
|
613
|
+
|
|
614
|
+
def __init__(
|
|
615
|
+
self,
|
|
616
|
+
name: str,
|
|
617
|
+
value: float,
|
|
618
|
+
min_value: float,
|
|
619
|
+
max_value: float,
|
|
620
|
+
step: float = 0.1,
|
|
621
|
+
):
|
|
96
622
|
self.name = name
|
|
97
|
-
self.
|
|
98
|
-
self.min_value = min_value
|
|
99
|
-
self.max_value = max_value
|
|
100
|
-
self._value = self._validate(
|
|
623
|
+
self.step = step
|
|
624
|
+
self.min_value = self._validate(min_value, compare_to_range=False)
|
|
625
|
+
self.max_value = self._validate(max_value, compare_to_range=False)
|
|
626
|
+
self._value = self._validate(value)
|
|
101
627
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
628
|
+
def _validate(self, new_value: Any, compare_to_range: bool = True) -> float:
|
|
629
|
+
"""
|
|
630
|
+
Validate and convert value to float, optionally checking bounds.
|
|
631
|
+
|
|
632
|
+
Rounds value to nearest step increment before range checking.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
new_value: Value to validate
|
|
636
|
+
compare_to_range: If True, clamps value to min/max bounds
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
Validated and potentially rounded float value
|
|
640
|
+
|
|
641
|
+
Raises:
|
|
642
|
+
ValueError: If value cannot be converted to float
|
|
643
|
+
"""
|
|
644
|
+
try:
|
|
645
|
+
new_value = float(new_value)
|
|
646
|
+
except ValueError:
|
|
647
|
+
raise ValueError(f"Value {new_value} cannot be converted to float")
|
|
648
|
+
|
|
649
|
+
# Round to the nearest step
|
|
650
|
+
new_value = round(new_value / self.step) * self.step
|
|
651
|
+
|
|
652
|
+
if compare_to_range:
|
|
653
|
+
if new_value < self.min_value:
|
|
654
|
+
warn(f"Value {new_value} below minimum {self.min_value}, clamping")
|
|
655
|
+
new_value = self.min_value
|
|
656
|
+
if new_value > self.max_value:
|
|
657
|
+
warn(f"Value {new_value} above maximum {self.max_value}, clamping")
|
|
658
|
+
new_value = self.max_value
|
|
659
|
+
|
|
660
|
+
return float(new_value)
|
|
661
|
+
|
|
662
|
+
def _validate_update(self) -> None:
|
|
663
|
+
"""
|
|
664
|
+
Validate complete parameter state after updates.
|
|
665
|
+
|
|
666
|
+
Ensures min_value <= max_value, swapping if needed.
|
|
667
|
+
Re-validates current value against potentially updated bounds.
|
|
668
|
+
|
|
669
|
+
Raises:
|
|
670
|
+
ParameterUpdateError: If bounds are invalid (e.g. None when required)
|
|
671
|
+
"""
|
|
672
|
+
if self.min_value is None or self.max_value is None:
|
|
673
|
+
raise ParameterUpdateError(
|
|
674
|
+
self.name,
|
|
675
|
+
type(self).__name__,
|
|
676
|
+
"FloatParameter must have both min_value and max_value bounds",
|
|
677
|
+
)
|
|
678
|
+
if self.min_value > self.max_value:
|
|
679
|
+
warn(f"Min value greater than max value, swapping")
|
|
680
|
+
self.min_value, self.max_value = self.max_value, self.min_value
|
|
681
|
+
self.value = self._validate(self.value)
|
|
106
682
|
|
|
107
683
|
|
|
108
684
|
@dataclass(init=False)
|
|
109
|
-
class
|
|
110
|
-
|
|
685
|
+
class IntegerRangeParameter(Parameter[Tuple[int, int]]):
|
|
686
|
+
"""
|
|
687
|
+
Parameter for a range of bounded integer values.
|
|
688
|
+
|
|
689
|
+
Creates a range slider in the GUI for selecting a range of whole numbers.
|
|
690
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer_range` and
|
|
691
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_integer_range` for usage.
|
|
692
|
+
|
|
693
|
+
Parameters
|
|
694
|
+
----------
|
|
695
|
+
name : str
|
|
696
|
+
The name of the parameter
|
|
697
|
+
value : tuple[int, int]
|
|
698
|
+
Initial (low, high) values
|
|
699
|
+
min_value : int
|
|
700
|
+
Minimum allowed value for both low and high
|
|
701
|
+
max_value : int
|
|
702
|
+
Maximum allowed value for both low and high
|
|
703
|
+
|
|
704
|
+
Examples
|
|
705
|
+
--------
|
|
706
|
+
>>> age_range = IntegerRangeParameter("age_range",
|
|
707
|
+
... value=(25, 35), min_value=18, max_value=100)
|
|
708
|
+
>>> age_range.value
|
|
709
|
+
(25, 35)
|
|
710
|
+
>>> age_range.update({"value": (35, 25)}) # Values will be swapped
|
|
711
|
+
>>> age_range.value
|
|
712
|
+
(25, 35)
|
|
713
|
+
>>> age_range.update({"value": (15, 40)}) # Low will be clamped
|
|
714
|
+
>>> age_range.value
|
|
715
|
+
(18, 40)
|
|
716
|
+
"""
|
|
717
|
+
|
|
718
|
+
min_value: int
|
|
719
|
+
max_value: int
|
|
720
|
+
|
|
721
|
+
def __init__(
|
|
722
|
+
self,
|
|
723
|
+
name: str,
|
|
724
|
+
value: Tuple[int, int],
|
|
725
|
+
min_value: int,
|
|
726
|
+
max_value: int,
|
|
727
|
+
):
|
|
111
728
|
self.name = name
|
|
729
|
+
self.min_value = self._validate_single(min_value)
|
|
730
|
+
self.max_value = self._validate_single(max_value)
|
|
731
|
+
self._value = self._validate(value)
|
|
732
|
+
|
|
733
|
+
def _validate_single(self, new_value: Any) -> int:
|
|
734
|
+
"""
|
|
735
|
+
Validate and convert a single numeric value.
|
|
736
|
+
|
|
737
|
+
Used by _validate() to handle each number in the range tuple.
|
|
738
|
+
Does not perform range checking.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
new_value: Value to validate
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
Converted numeric value
|
|
745
|
+
|
|
746
|
+
Raises:
|
|
747
|
+
ValueError: If value cannot be converted to required numeric type
|
|
748
|
+
"""
|
|
112
749
|
try:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
750
|
+
return int(new_value)
|
|
751
|
+
except ValueError:
|
|
752
|
+
raise ValueError(f"Value {new_value} cannot be converted to int")
|
|
753
|
+
|
|
754
|
+
def _validate(self, new_value: Any) -> Tuple[int, int]:
|
|
755
|
+
"""
|
|
756
|
+
Validate numeric value against parameter constraints.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
new_value: Value to validate
|
|
760
|
+
compare_to_range: If True, clamps value to min/max bounds
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
Validated and potentially clamped value
|
|
764
|
+
|
|
765
|
+
Raises:
|
|
766
|
+
ValueError: If value cannot be converted to required numeric type
|
|
767
|
+
"""
|
|
768
|
+
if not isinstance(new_value, (tuple, list)) or len(new_value) != 2:
|
|
769
|
+
raise ValueError("Value must be a tuple of (low, high)")
|
|
770
|
+
|
|
771
|
+
low = self._validate_single(new_value[0])
|
|
772
|
+
high = self._validate_single(new_value[1])
|
|
773
|
+
|
|
774
|
+
if low > high:
|
|
775
|
+
warn(f"Low value {low} greater than high value {high}, swapping")
|
|
776
|
+
low, high = high, low
|
|
131
777
|
|
|
132
|
-
if self.min_value
|
|
133
|
-
value
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
778
|
+
if low < self.min_value:
|
|
779
|
+
warn(f"Low value {low} below minimum {self.min_value}, clamping")
|
|
780
|
+
low = self.min_value
|
|
781
|
+
if high > self.max_value:
|
|
782
|
+
warn(f"High value {high} above maximum {self.max_value}, clamping")
|
|
783
|
+
high = self.max_value
|
|
784
|
+
|
|
785
|
+
return (low, high)
|
|
786
|
+
|
|
787
|
+
def _validate_update(self) -> None:
|
|
788
|
+
"""
|
|
789
|
+
Validate complete parameter state after updates.
|
|
790
|
+
|
|
791
|
+
Ensures min_value <= max_value, swapping if needed.
|
|
792
|
+
Re-validates current value against potentially updated bounds.
|
|
793
|
+
|
|
794
|
+
Raises:
|
|
795
|
+
ParameterUpdateError: If bounds are invalid (e.g. None when required)
|
|
796
|
+
"""
|
|
797
|
+
if self.min_value is None or self.max_value is None:
|
|
798
|
+
raise ParameterUpdateError(
|
|
799
|
+
self.name,
|
|
800
|
+
type(self).__name__,
|
|
801
|
+
"IntegerRangeParameter must have both min_value and max_value bounds",
|
|
802
|
+
)
|
|
803
|
+
if self.min_value > self.max_value:
|
|
804
|
+
warn(f"Min value greater than max value, swapping")
|
|
805
|
+
self.min_value, self.max_value = self.max_value, self.min_value
|
|
806
|
+
self.value = self._validate(self.value)
|
|
137
807
|
|
|
138
808
|
|
|
139
809
|
@dataclass(init=False)
|
|
140
|
-
class
|
|
810
|
+
class FloatRangeParameter(Parameter[Tuple[float, float]]):
|
|
811
|
+
"""
|
|
812
|
+
Parameter for a range of bounded decimal numbers.
|
|
813
|
+
|
|
814
|
+
Creates a range slider in the GUI for selecting a range of numbers.
|
|
815
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_float_range` and
|
|
816
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_float_range` for usage.
|
|
817
|
+
|
|
818
|
+
Parameters
|
|
819
|
+
----------
|
|
820
|
+
name : str
|
|
821
|
+
The name of the parameter
|
|
822
|
+
value : tuple[float, float]
|
|
823
|
+
Initial (low, high) values
|
|
824
|
+
min_value : float
|
|
825
|
+
Minimum allowed value for both low and high
|
|
826
|
+
max_value : float
|
|
827
|
+
Maximum allowed value for both low and high
|
|
828
|
+
step : float, optional
|
|
829
|
+
Size of each increment (default is 0.1)
|
|
830
|
+
|
|
831
|
+
Examples
|
|
832
|
+
--------
|
|
833
|
+
>>> temp_range = FloatRangeParameter("temperature_range",
|
|
834
|
+
... value=(98.6, 100.4), min_value=95.0, max_value=105.0, step=0.1)
|
|
835
|
+
>>> temp_range.value
|
|
836
|
+
(98.6, 100.4)
|
|
837
|
+
>>> temp_range.update({"value": (98.67, 100.0)}) # Low will be rounded
|
|
838
|
+
>>> temp_range.value
|
|
839
|
+
(98.7, 100.0)
|
|
840
|
+
>>> temp_range.update({"value": (101.0, 99.0)}) # Values will be swapped
|
|
841
|
+
>>> temp_range.value
|
|
842
|
+
(99.0, 101.0)
|
|
843
|
+
|
|
844
|
+
Notes
|
|
845
|
+
-----
|
|
846
|
+
The step parameter determines how finely you can adjust the values. For example:
|
|
847
|
+
- step=0.1 allows values like 1.0, 1.1, 1.2, etc.
|
|
848
|
+
- step=0.01 allows values like 1.00, 1.01, 1.02, etc.
|
|
849
|
+
- step=5.0 allows values like 0.0, 5.0, 10.0, etc.
|
|
850
|
+
"""
|
|
851
|
+
|
|
852
|
+
min_value: float
|
|
853
|
+
max_value: float
|
|
141
854
|
step: float
|
|
142
855
|
|
|
143
|
-
def __init__(
|
|
856
|
+
def __init__(
|
|
857
|
+
self,
|
|
858
|
+
name: str,
|
|
859
|
+
value: Tuple[float, float],
|
|
860
|
+
min_value: float,
|
|
861
|
+
max_value: float,
|
|
862
|
+
step: float = 0.1,
|
|
863
|
+
):
|
|
144
864
|
self.name = name
|
|
145
|
-
self.default = default
|
|
146
|
-
try:
|
|
147
|
-
self.min_value = float(min_value)
|
|
148
|
-
self.max_value = float(max_value)
|
|
149
|
-
except TypeError as e:
|
|
150
|
-
raise TypeError(f"Cannot convert {min_value} and {max_value} to float") from e
|
|
151
|
-
if self.min_value is not None and self.max_value is not None:
|
|
152
|
-
if self.min_value > self.max_value:
|
|
153
|
-
raise ValueError(f"Minimum value {self.min_value} is greater than maximum value {self.max_value}")
|
|
154
865
|
self.step = step
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
866
|
+
self.min_value = self._validate_single(min_value)
|
|
867
|
+
self.max_value = self._validate_single(max_value)
|
|
868
|
+
self._value = self._validate(value)
|
|
869
|
+
|
|
870
|
+
def _validate_single(self, new_value: Any) -> float:
|
|
871
|
+
"""
|
|
872
|
+
Validate and convert a single numeric value.
|
|
873
|
+
|
|
874
|
+
Used by _validate() to handle each number in the range tuple.
|
|
875
|
+
Does not perform range checking.
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
new_value: Value to validate
|
|
160
879
|
|
|
161
|
-
|
|
880
|
+
Returns:
|
|
881
|
+
Converted numeric value
|
|
882
|
+
|
|
883
|
+
Raises:
|
|
884
|
+
ValueError: If value cannot be converted to required numeric type
|
|
885
|
+
"""
|
|
162
886
|
try:
|
|
163
|
-
|
|
164
|
-
except
|
|
165
|
-
raise
|
|
887
|
+
new_value = float(new_value)
|
|
888
|
+
except ValueError:
|
|
889
|
+
raise ValueError(f"Value {new_value} cannot be converted to float")
|
|
166
890
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
value = min(self.max_value, value)
|
|
171
|
-
return value
|
|
891
|
+
# Round to the nearest step
|
|
892
|
+
new_value = round(new_value / self.step) * self.step
|
|
893
|
+
return new_value
|
|
172
894
|
|
|
895
|
+
def _validate(self, new_value: Any) -> Tuple[float, float]:
|
|
896
|
+
"""
|
|
897
|
+
Validate numeric value against parameter constraints.
|
|
173
898
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
max_value: T
|
|
178
|
-
default: Tuple[T, T]
|
|
899
|
+
Args:
|
|
900
|
+
new_value: Value to validate
|
|
901
|
+
compare_to_range: If True, clamps value to min/max bounds
|
|
179
902
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
raise NotImplementedError("Need to define in subclass for proper IDE support")
|
|
903
|
+
Returns:
|
|
904
|
+
Validated and potentially clamped value
|
|
183
905
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
906
|
+
Raises:
|
|
907
|
+
ValueError: If value cannot be converted to required numeric type
|
|
908
|
+
"""
|
|
909
|
+
if not isinstance(new_value, (tuple, list)) or len(new_value) != 2:
|
|
910
|
+
raise ValueError("Value must be a tuple of (low, high)")
|
|
911
|
+
|
|
912
|
+
low = self._validate_single(new_value[0])
|
|
913
|
+
high = self._validate_single(new_value[1])
|
|
914
|
+
|
|
915
|
+
if low > high:
|
|
916
|
+
warn(f"Low value {low} greater than high value {high}, swapping")
|
|
917
|
+
low, high = high, low
|
|
918
|
+
|
|
919
|
+
if low < self.min_value:
|
|
920
|
+
warn(f"Low value {low} below minimum {self.min_value}, clamping")
|
|
921
|
+
low = self.min_value
|
|
922
|
+
if high > self.max_value:
|
|
923
|
+
warn(f"High value {high} above maximum {self.max_value}, clamping")
|
|
924
|
+
high = self.max_value
|
|
925
|
+
|
|
926
|
+
return (low, high)
|
|
927
|
+
|
|
928
|
+
def _validate_update(self) -> None:
|
|
929
|
+
"""
|
|
930
|
+
Validate complete parameter state after updates.
|
|
931
|
+
|
|
932
|
+
Ensures min_value <= max_value, swapping if needed.
|
|
933
|
+
Re-validates current value against potentially updated bounds.
|
|
934
|
+
|
|
935
|
+
Raises:
|
|
936
|
+
ParameterUpdateError: If bounds are invalid (e.g. None when required)
|
|
937
|
+
"""
|
|
938
|
+
if self.min_value is None or self.max_value is None:
|
|
939
|
+
raise ParameterUpdateError(
|
|
940
|
+
self.name,
|
|
941
|
+
type(self).__name__,
|
|
942
|
+
"FloatRangeParameter must have both min_value and max_value bounds",
|
|
943
|
+
)
|
|
944
|
+
if self.min_value > self.max_value:
|
|
945
|
+
warn(f"Min value greater than max value, swapping")
|
|
946
|
+
self.min_value, self.max_value = self.max_value, self.min_value
|
|
947
|
+
self.value = self._validate(self.value)
|
|
187
948
|
|
|
188
949
|
|
|
189
950
|
@dataclass(init=False)
|
|
190
|
-
class
|
|
191
|
-
|
|
951
|
+
class UnboundedIntegerParameter(Parameter[int]):
|
|
952
|
+
"""
|
|
953
|
+
Parameter for optionally bounded integer values.
|
|
954
|
+
|
|
955
|
+
Creates a text input box in the GUI for entering whole numbers.
|
|
956
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_integer` and
|
|
957
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_unbounded_integer` for usage.
|
|
958
|
+
|
|
959
|
+
Parameters
|
|
960
|
+
----------
|
|
961
|
+
name : str
|
|
962
|
+
The name of the parameter
|
|
963
|
+
value : int
|
|
964
|
+
Initial value
|
|
965
|
+
min_value : int, optional
|
|
966
|
+
Minimum allowed value (or None for no minimum)
|
|
967
|
+
max_value : int, optional
|
|
968
|
+
Maximum allowed value (or None for no maximum)
|
|
969
|
+
|
|
970
|
+
Examples
|
|
971
|
+
--------
|
|
972
|
+
>>> count = UnboundedIntegerParameter("count", value=10, min_value=0)
|
|
973
|
+
>>> count.value
|
|
974
|
+
10
|
|
975
|
+
>>> count.update({"value": -5}) # Will be clamped to min_value
|
|
976
|
+
>>> count.value
|
|
977
|
+
0
|
|
978
|
+
>>> count.update({"value": 1000000}) # No maximum, so this is allowed
|
|
979
|
+
>>> count.value
|
|
980
|
+
1000000
|
|
981
|
+
|
|
982
|
+
Notes
|
|
983
|
+
-----
|
|
984
|
+
Use this instead of IntegerParameter when you:
|
|
985
|
+
- Don't know a reasonable maximum value
|
|
986
|
+
- Only want to enforce a minimum or maximum, but not both
|
|
987
|
+
- Need to allow very large numbers that would be impractical with a slider
|
|
988
|
+
"""
|
|
989
|
+
|
|
990
|
+
min_value: Optional[int]
|
|
991
|
+
max_value: Optional[int]
|
|
992
|
+
|
|
993
|
+
def __init__(
|
|
994
|
+
self,
|
|
995
|
+
name: str,
|
|
996
|
+
value: int,
|
|
997
|
+
min_value: Optional[int] = None,
|
|
998
|
+
max_value: Optional[int] = None,
|
|
999
|
+
):
|
|
192
1000
|
self.name = name
|
|
1001
|
+
self.min_value = (
|
|
1002
|
+
self._validate(min_value, compare_to_range=False)
|
|
1003
|
+
if min_value is not None
|
|
1004
|
+
else None
|
|
1005
|
+
)
|
|
1006
|
+
self.max_value = (
|
|
1007
|
+
self._validate(max_value, compare_to_range=False)
|
|
1008
|
+
if max_value is not None
|
|
1009
|
+
else None
|
|
1010
|
+
)
|
|
1011
|
+
self._value = self._validate(value)
|
|
1012
|
+
|
|
1013
|
+
def _validate(self, new_value: Any, compare_to_range: bool = True) -> int:
|
|
1014
|
+
"""
|
|
1015
|
+
Validate and convert value to integer, optionally checking bounds.
|
|
1016
|
+
|
|
1017
|
+
Handles None min/max values by skipping those bound checks.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
new_value: Value to validate
|
|
1021
|
+
compare_to_range: If True, clamps value to any defined min/max bounds
|
|
1022
|
+
|
|
1023
|
+
Returns:
|
|
1024
|
+
Validated integer value
|
|
1025
|
+
|
|
1026
|
+
Raises:
|
|
1027
|
+
ValueError: If value cannot be converted to int
|
|
1028
|
+
"""
|
|
193
1029
|
try:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if
|
|
199
|
-
if self.min_value
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
except (TypeError, ValueError):
|
|
211
|
-
raise TypeError(f"Cannot convert {new_value} to integer pair")
|
|
1030
|
+
new_value = int(new_value)
|
|
1031
|
+
except ValueError:
|
|
1032
|
+
raise ValueError(f"Value {new_value} cannot be converted to int")
|
|
1033
|
+
|
|
1034
|
+
if compare_to_range:
|
|
1035
|
+
if self.min_value is not None and new_value < self.min_value:
|
|
1036
|
+
warn(f"Value {new_value} below minimum {self.min_value}, clamping")
|
|
1037
|
+
new_value = self.min_value
|
|
1038
|
+
if self.max_value is not None and new_value > self.max_value:
|
|
1039
|
+
warn(f"Value {new_value} above maximum {self.max_value}, clamping")
|
|
1040
|
+
new_value = self.max_value
|
|
1041
|
+
return int(new_value)
|
|
1042
|
+
|
|
1043
|
+
def _validate_update(self) -> None:
|
|
1044
|
+
"""
|
|
1045
|
+
Validate complete parameter state after updates.
|
|
212
1046
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
1047
|
+
Ensures min_value <= max_value, swapping if needed.
|
|
1048
|
+
Re-validates current value against potentially updated bounds.
|
|
1049
|
+
|
|
1050
|
+
Raises:
|
|
1051
|
+
ParameterUpdateError: If bounds are invalid (e.g. None when required)
|
|
1052
|
+
"""
|
|
1053
|
+
if (
|
|
1054
|
+
self.min_value is not None
|
|
1055
|
+
and self.max_value is not None
|
|
1056
|
+
and self.min_value > self.max_value
|
|
1057
|
+
):
|
|
1058
|
+
warn(f"Min value greater than max value, swapping")
|
|
1059
|
+
self.min_value, self.max_value = self.max_value, self.min_value
|
|
1060
|
+
self.value = self._validate(self.value)
|
|
218
1061
|
|
|
219
1062
|
|
|
220
1063
|
@dataclass(init=False)
|
|
221
|
-
class
|
|
1064
|
+
class UnboundedFloatParameter(Parameter[float]):
|
|
1065
|
+
"""
|
|
1066
|
+
Parameter for optionally bounded decimal numbers.
|
|
1067
|
+
|
|
1068
|
+
Creates a text input box in the GUI for entering numbers.
|
|
1069
|
+
See :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_float` and
|
|
1070
|
+
:meth:`~syd.interactive_viewer.InteractiveViewer.update_unbounded_float` for usage.
|
|
1071
|
+
|
|
1072
|
+
Parameters
|
|
1073
|
+
----------
|
|
1074
|
+
name : str
|
|
1075
|
+
The name of the parameter
|
|
1076
|
+
value : float
|
|
1077
|
+
Initial value
|
|
1078
|
+
min_value : float, optional
|
|
1079
|
+
Minimum allowed value (or None for no minimum)
|
|
1080
|
+
max_value : float, optional
|
|
1081
|
+
Maximum allowed value (or None for no maximum)
|
|
1082
|
+
step : float, optional
|
|
1083
|
+
Size of each increment (default is None, meaning no rounding)
|
|
1084
|
+
|
|
1085
|
+
Examples
|
|
1086
|
+
--------
|
|
1087
|
+
>>> price = UnboundedFloatParameter("price", value=19.99, min_value=0.0, step=0.01)
|
|
1088
|
+
>>> price.value
|
|
1089
|
+
19.99
|
|
1090
|
+
>>> price.update({"value": -5.0}) # Will be clamped to min_value
|
|
1091
|
+
>>> price.value
|
|
1092
|
+
0.0
|
|
1093
|
+
>>> price.update({"value": 19.987}) # Will be rounded to step
|
|
1094
|
+
>>> price.value
|
|
1095
|
+
19.99
|
|
1096
|
+
|
|
1097
|
+
Notes
|
|
1098
|
+
-----
|
|
1099
|
+
Use this instead of FloatParameter when you:
|
|
1100
|
+
- Don't know a reasonable maximum value
|
|
1101
|
+
- Only want to enforce a minimum or maximum, but not both
|
|
1102
|
+
- Need to allow very large or precise numbers that would be impractical with a slider
|
|
1103
|
+
|
|
1104
|
+
If step is provided, values will be rounded:
|
|
1105
|
+
- step=0.1 rounds to 1.0, 1.1, 1.2, etc.
|
|
1106
|
+
- step=0.01 rounds to 1.00, 1.01, 1.02, etc.
|
|
1107
|
+
- step=5.0 rounds to 0.0, 5.0, 10.0, etc.
|
|
1108
|
+
"""
|
|
1109
|
+
|
|
1110
|
+
min_value: Optional[float]
|
|
1111
|
+
max_value: Optional[float]
|
|
222
1112
|
step: float
|
|
223
1113
|
|
|
224
1114
|
def __init__(
|
|
225
1115
|
self,
|
|
226
1116
|
name: str,
|
|
227
|
-
|
|
228
|
-
min_value: float = None,
|
|
229
|
-
max_value: float = None,
|
|
230
|
-
step: float =
|
|
1117
|
+
value: float,
|
|
1118
|
+
min_value: Optional[float] = None,
|
|
1119
|
+
max_value: Optional[float] = None,
|
|
1120
|
+
step: Optional[float] = None,
|
|
231
1121
|
):
|
|
232
1122
|
self.name = name
|
|
233
|
-
try:
|
|
234
|
-
self.min_value = float(min_value)
|
|
235
|
-
self.max_value = float(max_value)
|
|
236
|
-
except TypeError as e:
|
|
237
|
-
raise TypeError(f"Cannot convert {min_value} and {max_value} to float") from e
|
|
238
|
-
if self.min_value is not None and self.max_value is not None:
|
|
239
|
-
if self.min_value > self.max_value:
|
|
240
|
-
raise ValueError(f"Minimum value {self.min_value} is greater than maximum value {self.max_value}")
|
|
241
|
-
valid_default = self._validate(default)
|
|
242
|
-
if valid_default != default:
|
|
243
|
-
warn(f"Default value {default} is not in the range [{self.min_value}, {self.max_value}]. Clamping to {valid_default}.")
|
|
244
|
-
self.default = valid_default
|
|
245
1123
|
self.step = step
|
|
246
|
-
self.
|
|
1124
|
+
self.min_value = (
|
|
1125
|
+
self._validate(min_value, compare_to_range=False)
|
|
1126
|
+
if min_value is not None
|
|
1127
|
+
else None
|
|
1128
|
+
)
|
|
1129
|
+
self.max_value = (
|
|
1130
|
+
self._validate(max_value, compare_to_range=False)
|
|
1131
|
+
if max_value is not None
|
|
1132
|
+
else None
|
|
1133
|
+
)
|
|
1134
|
+
self._value = self._validate(value)
|
|
1135
|
+
|
|
1136
|
+
def _validate(self, new_value: Any, compare_to_range: bool = True) -> float:
|
|
1137
|
+
"""
|
|
1138
|
+
Validate and convert value to float, optionally checking bounds.
|
|
1139
|
+
|
|
1140
|
+
Handles None min/max values by skipping those bound checks.
|
|
1141
|
+
Only rounds to step if step is not None.
|
|
247
1142
|
|
|
248
|
-
|
|
1143
|
+
Args:
|
|
1144
|
+
new_value: Value to validate
|
|
1145
|
+
compare_to_range: If True, clamps value to any defined min/max bounds
|
|
1146
|
+
|
|
1147
|
+
Returns:
|
|
1148
|
+
Validated and potentially rounded float value
|
|
1149
|
+
|
|
1150
|
+
Raises:
|
|
1151
|
+
ValueError: If value cannot be converted to float
|
|
1152
|
+
"""
|
|
249
1153
|
try:
|
|
250
|
-
|
|
251
|
-
except
|
|
252
|
-
raise
|
|
1154
|
+
new_value = float(new_value)
|
|
1155
|
+
except ValueError:
|
|
1156
|
+
raise ValueError(f"Value {new_value} cannot be converted to float")
|
|
1157
|
+
|
|
1158
|
+
# Round to the nearest step if step is defined
|
|
1159
|
+
if self.step is not None:
|
|
1160
|
+
new_value = round(new_value / self.step) * self.step
|
|
1161
|
+
|
|
1162
|
+
if compare_to_range:
|
|
1163
|
+
if self.min_value is not None and new_value < self.min_value:
|
|
1164
|
+
warn(f"Value {new_value} below minimum {self.min_value}, clamping")
|
|
1165
|
+
new_value = self.min_value
|
|
1166
|
+
if self.max_value is not None and new_value > self.max_value:
|
|
1167
|
+
warn(f"Value {new_value} above maximum {self.max_value}, clamping")
|
|
1168
|
+
new_value = self.max_value
|
|
1169
|
+
|
|
1170
|
+
return float(new_value)
|
|
1171
|
+
|
|
1172
|
+
def _validate_update(self) -> None:
|
|
1173
|
+
"""
|
|
1174
|
+
Validate complete parameter state after updates.
|
|
1175
|
+
|
|
1176
|
+
Ensures min_value <= max_value, swapping if needed.
|
|
1177
|
+
Re-validates current value against potentially updated bounds.
|
|
1178
|
+
|
|
1179
|
+
Raises:
|
|
1180
|
+
ParameterUpdateError: If bounds are invalid (e.g. None when required)
|
|
1181
|
+
"""
|
|
1182
|
+
if (
|
|
1183
|
+
self.min_value is not None
|
|
1184
|
+
and self.max_value is not None
|
|
1185
|
+
and self.min_value > self.max_value
|
|
1186
|
+
):
|
|
1187
|
+
warn(f"Min value greater than max value, swapping")
|
|
1188
|
+
self.min_value, self.max_value = self.max_value, self.min_value
|
|
1189
|
+
self.value = self._validate(self.value)
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
class ButtonParameter(Parameter):
|
|
1193
|
+
"""A parameter that represents a clickable button."""
|
|
253
1194
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
1195
|
+
_is_button: bool = True
|
|
1196
|
+
|
|
1197
|
+
def __init__(self, name: str, label: str, callback: Callable[[], None]):
|
|
1198
|
+
"""
|
|
1199
|
+
Initialize a button parameter.
|
|
1200
|
+
|
|
1201
|
+
Args:
|
|
1202
|
+
name: Internal name of the parameter
|
|
1203
|
+
label: Text to display on the button
|
|
1204
|
+
callback: Function to call when button is clicked
|
|
1205
|
+
"""
|
|
1206
|
+
self.name = name
|
|
1207
|
+
self.label = label
|
|
1208
|
+
self.callback = callback
|
|
1209
|
+
self._value = None # Buttons don't have a value in the traditional sense
|
|
1210
|
+
|
|
1211
|
+
def update(self, updates: Dict[str, Any]) -> None:
|
|
1212
|
+
"""Update the button's label and/or callback."""
|
|
1213
|
+
if "label" in updates:
|
|
1214
|
+
self.label = updates["label"]
|
|
1215
|
+
if "callback" in updates:
|
|
1216
|
+
self.callback = updates["callback"]
|
|
1217
|
+
|
|
1218
|
+
@property
|
|
1219
|
+
def value(self) -> None:
|
|
1220
|
+
"""Buttons don't have a value, always returns None."""
|
|
1221
|
+
return self._value
|
|
1222
|
+
|
|
1223
|
+
@value.setter
|
|
1224
|
+
def value(self, _: Any) -> None:
|
|
1225
|
+
"""Buttons don't store values."""
|
|
1226
|
+
pass
|
|
1227
|
+
|
|
1228
|
+
def _validate_update(self) -> None:
|
|
1229
|
+
"""Buttons don't need validation."""
|
|
1230
|
+
pass
|
|
1231
|
+
|
|
1232
|
+
def _validate(self, new_value: Any) -> None:
|
|
1233
|
+
"""Buttons don't need validation."""
|
|
1234
|
+
pass
|
|
259
1235
|
|
|
260
1236
|
|
|
261
1237
|
class ParameterType(Enum):
|
|
1238
|
+
"""Registry of all available parameter types."""
|
|
1239
|
+
|
|
262
1240
|
text = TextParameter
|
|
263
|
-
selection = SingleSelectionParameter
|
|
264
|
-
multiple_selection = MultipleSelectionParameter
|
|
265
1241
|
boolean = BooleanParameter
|
|
1242
|
+
selection = SelectionParameter
|
|
1243
|
+
multiple_selection = MultipleSelectionParameter
|
|
266
1244
|
integer = IntegerParameter
|
|
267
1245
|
float = FloatParameter
|
|
268
|
-
|
|
269
|
-
|
|
1246
|
+
integer_range = IntegerRangeParameter
|
|
1247
|
+
float_range = FloatRangeParameter
|
|
1248
|
+
unbounded_integer = UnboundedIntegerParameter
|
|
1249
|
+
unbounded_float = UnboundedFloatParameter
|
|
1250
|
+
button = ButtonParameter
|