from typing import List, Any, Tuple, Generic, TypeVar, Optional, Dict, Callable, Union
from dataclasses import dataclass, field
from abc import ABC, abstractmethod
from enum import Enum
from copy import deepcopy
from .support import (
NoInitialValue,
ParameterMeta,
ParameterUpdateError,
get_parameter_attributes,
warn_parameter_update,
)
T = TypeVar("T")
[docs]
@dataclass
class Parameter(Generic[T], ABC, metaclass=ParameterMeta):
"""
Base class for all parameter types. Parameters are the building blocks
for creating interactive GUI elements.
Each parameter has a name and a value, and ensures the value stays valid
through validation rules.
Parameters
----------
name : str
The name of the parameter, used as a label in the GUI
value : T
The current value of the parameter
Notes
-----
This is an abstract base class - you should use one of the concrete parameter
types like TextParameter, BooleanParameter, etc. instead of using this directly.
"""
name: str
value: T
_is_action: bool = False
[docs]
@abstractmethod
def __init__(self, name: str, value: T):
raise NotImplementedError("Need to define in subclass for proper IDE support")
@property
def value(self) -> T:
"""
Get the current value of the parameter.
Returns
-------
T
The current value
"""
return self._value
@value.setter
def value(self, new_value: T) -> None:
"""
Set a new value for the parameter. The value will be validated before being set.
Parameters
----------
new_value : T
The new value to set
Raises
------
ValueError
If the new value is invalid for this parameter type
"""
self._value = self._validate(new_value)
@abstractmethod
def _validate(self, new_value: Any) -> T:
raise NotImplementedError
[docs]
def update(self, updates: Dict[str, Any]) -> None:
"""
Safely update multiple parameter attributes at once.
Parameters
----------
updates : dict
Dictionary of attribute names and their new values
Raises
------
ParameterUpdateError
If any of the updates are invalid
Examples
--------
>>> param = FloatParameter("temperature", 20.0, min=0, max=100)
>>> param.update({"value": 25.0, "max": 150})
"""
param_copy = deepcopy(self)
try:
param_copy._unsafe_update(updates)
for key, value in vars(param_copy).items():
if not key.startswith("_"):
setattr(self, key, value)
self.value = param_copy.value
except Exception as e:
if isinstance(e, ValueError):
raise ParameterUpdateError(
self.name, type(self).__name__, str(e)
) from e
else:
raise ParameterUpdateError(
self.name, type(self).__name__, f"Update failed: {str(e)}"
) from e
def _unsafe_update(self, updates: Dict[str, Any]) -> None:
"""
Internal update method that applies changes without safety copies.
Validates attribute names but applies updates directly to instance.
Called by public update() method inside a deepcopy context.
Args:
updates: Dict mapping attribute names to new values
Raises:
ValueError: If trying to update 'name' or invalid attributes
"""
valid_attributes = get_parameter_attributes(type(self))
for key, new_value in updates.items():
if key == "name":
raise ValueError("Cannot update parameter name")
elif key not in valid_attributes:
raise ValueError(f"Update failed, {key} is not a valid attribute")
for key, new_value in updates.items():
if key != "value":
setattr(self, key, new_value)
if "value" in updates:
self.value = updates["value"]
self._validate_update()
def _validate_update(self) -> None:
"""
Hook for validating complete parameter state after updates.
Called at end of _unsafe_update(). Default implementation does nothing.
Override in subclasses to add validation logic.
"""
pass
[docs]
@dataclass(init=False)
class TextParameter(Parameter[str]):
"""
Parameter for text input.
Creates a text box in the GUI that accepts any string input.
See :meth:`~syd.viewer.Viewer.add_text` and
:meth:`~syd.viewer.Viewer.update_text` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[str, NoInitialValue]
The initial text value
Examples
--------
>>> name_param = TextParameter("username", "Alice")
>>> name_param.value
'Alice'
>>> name_param.update({"value": "Bob"})
>>> name_param.value
'Bob'
"""
def __init__(self, name: str, value: Union[str, NoInitialValue]):
self.name = name
if isinstance(value, NoInitialValue):
value = ""
self._value = self._validate(value)
def _validate(self, new_value: Any) -> str:
"""
Convert input to string.
Args:
new_value: Value to convert
Returns:
String representation of input value
"""
return str(new_value)
[docs]
@dataclass(init=False)
class BooleanParameter(Parameter[bool]):
"""
Parameter for boolean values.
Creates a checkbox in the GUI that can be toggled on/off.
See :meth:`~syd.viewer.Viewer.add_boolean` and
:meth:`~syd.viewer.Viewer.update_boolean` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[bool, NoInitialValue]
The initial state (default is True)
Examples
--------
>>> active = BooleanParameter("is_active", True)
>>> active.value
True
>>> active.update({"value": False})
>>> active.value
False
"""
def __init__(self, name: str, value: Union[bool, NoInitialValue]):
self.name = name
if isinstance(value, NoInitialValue):
value = True
self._value = self._validate(value)
def _validate(self, new_value: Any) -> bool:
"""
Convert input to boolean.
Args:
new_value: Value to convert
Returns:
Boolean interpretation of input value using Python's bool() rules
"""
return bool(new_value)
[docs]
@dataclass(init=False)
class SelectionParameter(Parameter[Any]):
"""
Parameter for single selection from a list of options.
Creates a dropdown menu in the GUI where users can select one option.
See :meth:`~syd.viewer.Viewer.add_selection` and
:meth:`~syd.viewer.Viewer.update_selection` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[Any, NoInitialValue]
The initially selected value (must be one of the options)
options : sequence
List, tuple, or 1D numpy array of valid choices that can be selected
Examples
--------
>>> color = SelectionParameter("color", "red", options=["red", "green", "blue"])
>>> color.value
'red'
>>> color.update({"value": "blue"})
>>> color.value
'blue'
>>> color.update({"value": "yellow"}) # This will raise an error
>>> # With numpy array
>>> import numpy as np
>>> numbers = SelectionParameter("number", 1, options=np.array([1, 2, 3]))
>>> numbers.value
1
"""
options: List[Any]
def __init__(
self, name: str, value: Union[Any, NoInitialValue], options: Union[List, Tuple]
):
self.name = name
self.options = self._validate_options(options)
if isinstance(value, NoInitialValue):
value = self.options[0]
self._value = self._validate(value)
def _validate_options(self, options: Any) -> List[Any]:
"""
Validate options and convert to list if necessary.
Parameters
----------
options : list or tuple
The options to validate
Returns
-------
list
Validated list of options
Raises
------
TypeError
If options is not a list or tuple
ValueError
If any option is not hashable
"""
if not isinstance(options, (list, tuple)):
raise TypeError(
f"Options for parameter {self.name} must be a list or tuple"
)
if not options:
raise ValueError(f"Options for parameter {self.name} must not be empty")
# Verify all options are hashable (needed for comparison)
try:
for opt in options:
hash(opt)
except TypeError as e:
raise ValueError(
f"All options for parameter {self.name} must be hashable: {str(e)}"
)
return list(options)
def _validate(self, new_value: Any) -> Any:
"""
Validate that value is one of the allowed options.
Args:
new_value: Value to validate
Returns:
Input value if valid
Raises:
ValueError: If value is not in options list
"""
# Direct check for non-float values or when new_value is exactly in options
if new_value in self.options:
return new_value
# Special handling for numeric values to account for type mismatches
if isinstance(new_value, (int, float)):
for option in self.options:
# For numeric options, compare as floats
if (
isinstance(option, (int, float))
and abs(float(new_value) - float(option)) < 1e-10
):
return option
# Also try string conversion for numeric strings
elif isinstance(option, str):
try:
if abs(float(new_value) - float(option)) < 1e-10:
return option
except ValueError:
pass
# Handle string conversion - when new_value is a string but options might be numeric
if isinstance(new_value, str):
try:
# Try to convert to float if possible
float_value = float(new_value)
for option in self.options:
if (
isinstance(option, (int, float))
and abs(float_value - float(option)) < 1e-10
):
return option
except ValueError:
pass
raise ValueError(f"Value {new_value} not in options: {self.options}")
def _validate_update(self) -> None:
"""
Validate complete parameter state after updates.
Ensures options is a list/tuple and current value is valid.
Sets value to first option if current value becomes invalid.
Raises:
TypeError: If options is not a list or tuple
"""
self.options = self._validate_options(self.options)
# Check if value is directly in options
if self.value in self.options:
return
# For numeric values, try flexible comparison
value_found = False
if isinstance(self.value, (int, float)):
for option in self.options:
if (
isinstance(option, (int, float))
and abs(float(self.value) - float(option)) < 1e-10
):
# Don't update self.value here as we want to keep the original type if possible
value_found = True
break
elif isinstance(option, str):
try:
if abs(float(self.value) - float(option)) < 1e-10:
value_found = True
break
except ValueError:
pass
# For string values that might be numeric
if not value_found and isinstance(self.value, str):
try:
float_value = float(self.value)
for option in self.options:
if (
isinstance(option, (int, float))
and abs(float_value - float(option)) < 1e-10
):
value_found = True
break
except ValueError:
pass
# If value is not found after all checks, reset to first option
if not value_found:
warn_parameter_update(
self.name,
type(self).__name__,
f"Value {self.value} not in options, setting to first option ({self.options[0]})",
)
self.value = self.options[0]
[docs]
@dataclass(init=False)
class MultipleSelectionParameter(Parameter[List[Any]]):
"""
Parameter for multiple selections from a list of options.
Creates a set of checkboxes or multi-select dropdown in the GUI.
See :meth:`~syd.viewer.Viewer.add_multiple_selection` and
:meth:`~syd.viewer.Viewer.update_multiple_selection` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[List[Any], NoInitialValue]
List of initially selected values (must all be from options, can be empty)
options : sequence
List, tuple, or 1D numpy array of valid choices that can be selected
Examples
--------
>>> toppings = MultipleSelectionParameter("pizza_toppings",
... value=["cheese", "mushrooms"],
... options=["cheese", "mushrooms", "pepperoni", "olives"])
>>> toppings.value
['cheese', 'mushrooms']
>>> # With numpy array
>>> import numpy as np
>>> numbers = MultipleSelectionParameter("numbers",
... value=[1, 3],
... options=np.array([1, 2, 3, 4]))
>>> numbers.value
[1, 3]
"""
options: List[Any]
def __init__(
self,
name: str,
value: Union[List[Any], NoInitialValue],
options: Union[List, Tuple],
):
self.name = name
self.options = self._validate_options(options)
if isinstance(value, NoInitialValue):
value = []
self._value = self._validate(value)
def _validate_options(self, options: Any) -> List[Any]:
"""
Validate options and convert to list if necessary.
Parameters
----------
options : list or tuple
The options to validate
Returns
-------
list
Validated list of options
Raises
------
TypeError
If options is not a list or tuple
ValueError
If any option is not hashable
"""
if not isinstance(options, (list, tuple)):
raise TypeError(
f"Options for parameter {self.name} must be a list or tuple, received {type(options)}"
)
if not options:
raise ValueError(f"Options for parameter {self.name} must not be empty")
# Verify all options are hashable (needed for comparison)
try:
for opt in options:
hash(opt)
except TypeError as e:
raise ValueError(
f"All options for parameter {self.name} must be hashable: {str(e)}"
)
return list(options)
def _validate(self, new_value: Any) -> List[Any]:
"""
Validate list of selected values against options.
Ensures value is a list/tuple and all elements are in options.
Preserves order based on options list while removing duplicates.
Args:
new_value: List of selected values
Returns:
Validated list of unique values in options order
Raises:
TypeError: If value is not a list/tuple
ValueError: If any value is not in options
"""
if not isinstance(new_value, (list, tuple)):
raise TypeError(f"Value must be a list or tuple")
invalid = [val for val in new_value if val not in self.options]
if invalid:
raise ValueError(f"Values {invalid} not in options: {self.options}")
# Keep only unique values while preserving order based on self.options
return [x for x in self.options if x in new_value]
def _validate_update(self) -> None:
self.options = self._validate_options(self.options)
if not isinstance(self.value, (list, tuple)):
warn_parameter_update(
self.name,
type(self).__name__,
f"For parameter {self.name}, value {self.value} is not a list or tuple. Setting to empty list.",
)
self.value = []
if not all(val in self.options for val in self.value):
invalid = [val for val in self.value if val not in self.options]
warn_parameter_update(
self.name,
type(self).__name__,
f"For parameter {self.name}, value {self.value} contains invalid selections: {invalid}. Setting to empty list.",
)
self.value = []
# Keep only unique values while preserving order based on self.options
seen = set()
self.options = [x for x in self.options if not (x in seen or seen.add(x))]
[docs]
@dataclass(init=False)
class IntegerParameter(Parameter[int]):
"""
Parameter for bounded integer values.
Creates a slider in the GUI for selecting whole numbers between bounds.
See :meth:`~syd.viewer.Viewer.add_integer` and
:meth:`~syd.viewer.Viewer.update_integer` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[int, NoInitialValue]
Initial value (will be clamped to fit between min and max)
min : int
Minimum allowed value
max : int
Maximum allowed value
Examples
--------
>>> age = IntegerParameter("age", value=25, min=0, max=120)
>>> age.value
25
>>> age.update({"value": 150}) # Will be clamped to max
>>> age.value
120
>>> age.update({"value": -10}) # Will be clamped to min
>>> age.value
0
"""
min: int
max: int
def __init__(
self,
name: str,
value: Union[int, NoInitialValue],
min: int,
max: int,
):
self.name = name
self.min = self._validate(min, compare_to_range=False)
self.max = self._validate(max, compare_to_range=False)
if isinstance(value, NoInitialValue):
value = self.min
self._value = self._validate(value)
def _validate(self, new_value: Any, compare_to_range: bool = True) -> int:
"""
Validate and convert value to integer, optionally checking bounds.
Args:
new_value: Value to validate
compare_to_range: If True, clamps value to min/max bounds
Returns:
Validated integer value
Raises:
ValueError: If value cannot be converted to int
"""
try:
new_value = int(new_value)
except ValueError:
raise ValueError(f"Value {new_value} cannot be converted to int")
if compare_to_range:
if new_value < self.min:
warn_parameter_update(
self.name,
type(self).__name__,
f"Value {new_value} below minimum {self.min}, clamping",
)
new_value = self.min
if new_value > self.max:
warn_parameter_update(
self.name,
type(self).__name__,
f"Value {new_value} above maximum {self.max}, clamping",
)
new_value = self.max
return int(new_value)
def _validate_update(self) -> None:
"""
Validate complete parameter state after updates.
Ensures min <= max, swapping if needed.
Re-validates current value against potentially updated bounds.
Raises:
ParameterUpdateError: If bounds are invalid (e.g. None when required)
"""
if self.min is None or self.max is None:
raise ParameterUpdateError(
self.name,
type(self).__name__,
"IntegerParameter must have both min and max bounds",
)
if self.min > self.max:
warn_parameter_update(
self.name,
type(self).__name__,
f"Min value greater than max value, swapping",
)
self.min, self.max = self.max, self.min
self.value = self._validate(self.value)
[docs]
@dataclass(init=False)
class FloatParameter(Parameter[float]):
"""
Parameter for bounded decimal numbers.
Creates a slider in the GUI for selecting numbers between bounds.
See :meth:`~syd.viewer.Viewer.add_float` and
:meth:`~syd.viewer.Viewer.update_float` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[float, NoInitialValue]
Initial value (will be clamped to fit between min and max)
min : float
Minimum allowed value
max : float
Maximum allowed value
step : float, optional
Size of each increment (default is 0.001)
Examples
--------
>>> temp = FloatParameter("temperature", value=98.6,
... min=95.0, max=105.0, step=0.1)
>>> temp.value
98.6
>>> temp.update({"value": 98.67}) # Will be rounded to nearest step
>>> temp.value
98.7
>>> temp.update({"value": 110.0}) # Will be clamped to max
>>> temp.value
105.0
Notes
-----
The step parameter determines how finely you can adjust the value. For example:
- step=0.1 allows values like 1.0, 1.1, 1.2, etc.
- step=0.01 allows values like 1.00, 1.01, 1.02, etc.
- step=5.0 allows values like 0.0, 5.0, 10.0, etc.
"""
min: float
max: float
step: float
def __init__(
self,
name: str,
value: Union[float, NoInitialValue],
min: float,
max: float,
step: float = 0.001,
):
self.name = name
self.step = step
self.min = self._validate(min, compare_to_range=False)
self.max = self._validate(max, compare_to_range=False)
if isinstance(value, NoInitialValue):
value = self.min
self._value = self._validate(value)
def _validate(self, new_value: Any, compare_to_range: bool = True) -> float:
"""
Validate and convert value to float, optionally checking bounds.
Rounds value to nearest step increment before range checking.
Args:
new_value: Value to validate
compare_to_range: If True, clamps value to min/max bounds
Returns:
Validated and potentially rounded float value
Raises:
ValueError: If value cannot be converted to float
"""
try:
new_value = float(new_value)
except ValueError:
raise ValueError(f"Value {new_value} cannot be converted to float")
# Round to the nearest step
new_value = round(new_value / self.step) * self.step
if compare_to_range:
if new_value < self.min:
warn_parameter_update(
self.name,
type(self).__name__,
f"Value {new_value} below minimum {self.min}, clamping",
)
new_value = self.min
if new_value > self.max:
warn_parameter_update(
self.name,
type(self).__name__,
f"Value {new_value} above maximum {self.max}, clamping",
)
new_value = self.max
return float(new_value)
def _validate_update(self) -> None:
"""
Validate complete parameter state after updates.
Ensures min <= max, swapping if needed.
Re-validates current value against potentially updated bounds.
Raises:
ParameterUpdateError: If bounds are invalid (e.g. None when required)
"""
if self.min is None or self.max is None:
raise ParameterUpdateError(
self.name,
type(self).__name__,
"FloatParameter must have both min and max bounds",
)
if self.min > self.max:
warn_parameter_update(
self.name,
type(self).__name__,
f"Min value greater than max value, swapping",
)
self.min, self.max = self.max, self.min
self.value = self._validate(self.value)
[docs]
@dataclass(init=False)
class IntegerRangeParameter(Parameter[Tuple[int, int]]):
"""
Parameter for a range of bounded integer values.
Creates a range slider in the GUI for selecting a range of whole numbers.
See :meth:`~syd.viewer.Viewer.add_integer_range` and
:meth:`~syd.viewer.Viewer.update_integer_range` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[Tuple[int, int], NoInitialValue]
Initial (low, high) values
min : int
Minimum allowed value for both low and high
max : int
Maximum allowed value for both low and high
Examples
--------
>>> age_range = IntegerRangeParameter("age_range",
... value=(25, 35), min=18, max=100)
>>> age_range.value
(25, 35)
>>> age_range.update({"value": (35, 25)}) # Values will be swapped
>>> age_range.value
(25, 35)
>>> age_range.update({"value": (15, 40)}) # Low will be clamped
>>> age_range.value
(18, 40)
"""
min: int
max: int
def __init__(
self,
name: str,
value: Union[Tuple[int, int], NoInitialValue],
min: int,
max: int,
):
self.name = name
self.min = self._validate_single(min, context="min")
self.max = self._validate_single(max, context="max")
if isinstance(value, NoInitialValue):
value = (self.min, self.max)
self._value = self._validate(value)
def _validate_single(self, new_value: Any, context: Optional[str] = None) -> int:
"""
Validate and convert a single numeric value.
Used by _validate() to handle each number in the range tuple.
Does not perform range checking.
Args:
new_value: Value to validate
Returns:
Converted numeric value
Raises:
ValueError: If value cannot be converted to required numeric type
"""
try:
return int(new_value)
except Exception:
msg = f"Value {new_value} cannot be converted to int"
if context:
msg += f" for {context}"
raise ValueError(msg)
def _validate(self, new_value: Any) -> Tuple[int, int]:
"""
Validate numeric value against parameter constraints.
Args:
new_value: Value to validate
compare_to_range: If True, clamps value to min/max bounds
Returns:
Validated and potentially clamped value
Raises:
ValueError: If value cannot be converted to required numeric type
"""
if not isinstance(new_value, (tuple, list)) or len(new_value) != 2:
raise ValueError("Value must be a tuple of (low, high)")
low = self._validate_single(new_value[0])
high = self._validate_single(new_value[1])
if low > high:
warn_parameter_update(
self.name,
type(self).__name__,
f"Low value {low} greater than high value {high}, swapping",
)
low, high = high, low
if low < self.min:
warn_parameter_update(
self.name,
type(self).__name__,
f"Low value {low} below minimum {self.min}, clamping",
)
low = self.min
if high > self.max:
warn_parameter_update(
self.name,
type(self).__name__,
f"High value {high} above maximum {self.max}, clamping",
)
high = self.max
return (low, high)
def _validate_update(self) -> None:
"""
Validate complete parameter state after updates.
Ensures min <= max, swapping if needed.
Re-validates current value against potentially updated bounds.
Raises:
ParameterUpdateError: If bounds are invalid (e.g. None when required)
"""
if self.min is None or self.max is None:
raise ParameterUpdateError(
self.name,
type(self).__name__,
"IntegerRangeParameter must have both min and max bounds",
)
if self.min > self.max:
warn_parameter_update(
self.name,
type(self).__name__,
f"Min value greater than max value, swapping",
)
self.min, self.max = self.max, self.min
self.value = self._validate(self.value)
[docs]
@dataclass(init=False)
class FloatRangeParameter(Parameter[Tuple[float, float]]):
"""
Parameter for a range of bounded decimal numbers.
Creates a range slider in the GUI for selecting a range of numbers.
See :meth:`~syd.viewer.Viewer.add_float_range` and
:meth:`~syd.viewer.Viewer.update_float_range` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[Tuple[float, float], NoInitialValue]
Initial (low, high) values
min : float
Minimum allowed value for both low and high
max : float
Maximum allowed value for both low and high
step : float, optional
Size of each increment (default is 0.001)
Examples
--------
>>> temp_range = FloatRangeParameter("temperature_range",
... value=(98.6, 100.4), min=95.0, max=105.0, step=0.1)
>>> temp_range.value
(98.6, 100.4)
>>> temp_range.update({"value": (98.67, 100.0)}) # Low will be rounded
>>> temp_range.value
(98.7, 100.0)
>>> temp_range.update({"value": (101.0, 99.0)}) # Values will be swapped
>>> temp_range.value
(99.0, 101.0)
Notes
-----
The step parameter determines how finely you can adjust the values. For example:
- step=0.1 allows values like 1.0, 1.1, 1.2, etc.
- step=0.01 allows values like 1.00, 1.01, 1.02, etc.
- step=5.0 allows values like 0.0, 5.0, 10.0, etc.
"""
min: float
max: float
step: float
def __init__(
self,
name: str,
value: Union[Tuple[float, float], NoInitialValue],
min: float,
max: float,
step: float = 0.001,
):
self.name = name
self.step = step
self.min = self._validate_single(min, context="min")
self.max = self._validate_single(max, context="max")
if isinstance(value, NoInitialValue):
value = (self.min, self.max)
self._value = self._validate(value)
def _validate_single(self, new_value: Any, context: Optional[str] = None) -> float:
"""
Validate and convert a single numeric value.
Used by _validate() to handle each number in the range tuple.
Does not perform range checking.
Args:
new_value: Value to validate
Returns:
Converted numeric value
Raises:
ValueError: If value cannot be converted to required numeric type
"""
try:
new_value = float(new_value)
except Exception:
msg = f"Value {new_value} cannot be converted to float"
if context:
msg += f" for {context}"
raise ValueError(msg)
# Round to the nearest step
new_value = round(new_value / self.step) * self.step
return new_value
def _validate(self, new_value: Any) -> Tuple[float, float]:
"""
Validate numeric value against parameter constraints.
Args:
new_value: Value to validate
compare_to_range: If True, clamps value to min/max bounds
Returns:
Validated and potentially clamped value
Raises:
ValueError: If value cannot be converted to required numeric type
"""
if not isinstance(new_value, (tuple, list)) or len(new_value) != 2:
raise ValueError("Value must be a tuple of (low, high)")
low = self._validate_single(new_value[0])
high = self._validate_single(new_value[1])
if low > high:
warn_parameter_update(
self.name,
type(self).__name__,
f"Low value {low} greater than high value {high}, swapping",
)
low, high = high, low
if low < self.min:
warn_parameter_update(
self.name,
type(self).__name__,
f"Low value {low} below minimum {self.min}, clamping",
)
low = self.min
if high > self.max:
warn_parameter_update(
self.name,
type(self).__name__,
f"High value {high} above maximum {self.max}, clamping",
)
high = self.max
return (low, high)
def _validate_update(self) -> None:
"""
Validate complete parameter state after updates.
Ensures min <= max, swapping if needed.
Re-validates current value against potentially updated bounds.
Raises:
ParameterUpdateError: If bounds are invalid (e.g. None when required)
"""
if self.min is None or self.max is None:
raise ParameterUpdateError(
self.name,
type(self).__name__,
"FloatRangeParameter must have both min and max bounds",
)
if self.min > self.max:
warn_parameter_update(
self.name,
type(self).__name__,
f"Min value greater than max value, swapping",
)
self.min, self.max = self.max, self.min
self.value = self._validate(self.value)
[docs]
@dataclass(init=False)
class UnboundedIntegerParameter(Parameter[int]):
"""
Parameter for optionally bounded integer values.
Creates a text input box in the GUI for entering whole numbers.
See :meth:`~syd.viewer.Viewer.add_unbounded_integer` and
:meth:`~syd.viewer.Viewer.update_unbounded_integer` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[int, NoInitialValue]
Initial value
Examples
--------
>>> count = UnboundedIntegerParameter("count", value=10)
>>> count.value
10
>>> count.update({"value": 1000000}) # No maximum, so this is allowed
>>> count.value
1000000
Notes
-----
Use this instead of IntegerParameter when you:
- Don't have any reason to bound the value
- Need to allow very large numbers that would be impractical with a slider
"""
def __init__(
self,
name: str,
value: Union[int, NoInitialValue],
):
self.name = name
if isinstance(value, NoInitialValue):
value = 0
self._value = self._validate(value)
def _validate(self, new_value: Any) -> int:
"""
Validate and convert value to integer.
Args:
new_value: Value to validate
Returns:
Validated integer value
Raises:
ValueError: If value cannot be converted to int
"""
try:
new_value = int(new_value)
except ValueError:
raise ValueError(f"Value {new_value} cannot be converted to int")
return int(new_value)
def _validate_update(self) -> None:
"""
Validate complete parameter state after updates.
Raises:
ParameterUpdateError: If bounds are invalid (e.g. None when required)
"""
self.value = self._validate(self.value)
[docs]
@dataclass(init=False)
class UnboundedFloatParameter(Parameter[float]):
"""
Parameter for optionally bounded decimal numbers.
Creates a text input box in the GUI for entering numbers.
See :meth:`~syd.viewer.Viewer.add_unbounded_float` and
:meth:`~syd.viewer.Viewer.update_unbounded_float` for usage.
Parameters
----------
name : str
The name of the parameter
value : Union[float, NoInitialValue]
Initial value
step : float, optional
Size of each increment (default is None, meaning no rounding)
Examples
--------
>>> price = UnboundedFloatParameter("price", value=19.99)
>>> price.value
19.99
>>> price.update({"value": 19.987}) # Will be rounded to step
>>> price.value
19.99
Notes
-----
Use this instead of FloatParameter when you:
- Don't know a reasonable maximum value
- Need to allow very large or precise numbers that would be impractical with a slider
If step is provided, values will be rounded:
- step=0.1 rounds to 1.0, 1.1, 1.2, etc.
- step=0.01 rounds to 1.00, 1.01, 1.02, etc.
- step=5.0 rounds to 0.0, 5.0, 10.0, etc.
"""
step: Optional[float]
def __init__(
self,
name: str,
value: Union[float, NoInitialValue],
step: Optional[float] = None,
):
self.name = name
self.step = step
if isinstance(value, NoInitialValue):
value = 0
self._value = self._validate(value)
def _validate(self, new_value: Any) -> float:
"""
Validate and convert value to float.
Only rounds to step if step is not None.
Args:
new_value: Value to validate
Returns:
Validated and potentially rounded float value
Raises:
ValueError: If value cannot be converted to float
"""
try:
new_value = float(new_value)
except ValueError:
raise ValueError(f"Value {new_value} cannot be converted to float")
# Round to the nearest step if step is defined
if self.step is not None:
new_value = round(new_value / self.step) * self.step
return float(new_value)
def _validate_update(self) -> None:
"""
Validate complete parameter state after updates.
Ensures min <= max, swapping if needed.
Re-validates current value against potentially updated bounds.
Raises:
ParameterUpdateError: If bounds are invalid (e.g. None when required)
"""
self.value = self._validate(self.value)
class ParameterType(Enum):
"""Registry of all available parameter types."""
text = TextParameter
boolean = BooleanParameter
selection = SelectionParameter
multiple_selection = MultipleSelectionParameter
integer = IntegerParameter
float = FloatParameter
integer_range = IntegerRangeParameter
float_range = FloatRangeParameter
unbounded_integer = UnboundedIntegerParameter
unbounded_float = UnboundedFloatParameter
class ActionType(Enum):
button = ButtonAction