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