syd 0.1.5__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,9 +1,21 @@
1
- from typing import List, Any, Tuple, Generic, TypeVar, Optional, Dict, Callable
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
5
16
  from copy import deepcopy
6
17
  from warnings import warn
18
+ import numpy as np
7
19
 
8
20
  T = TypeVar("T")
9
21
 
@@ -55,6 +67,29 @@ class ParameterUpdateError(Exception):
55
67
  )
56
68
 
57
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
+
58
93
  def get_parameter_attributes(param_class) -> List[str]:
59
94
  """
60
95
  Get all valid attributes for a parameter class.
@@ -106,6 +141,7 @@ class Parameter(Generic[T], ABC):
106
141
 
107
142
  name: str
108
143
  value: T
144
+ _is_action: bool = False
109
145
 
110
146
  @abstractmethod
111
147
  def __init__(self, name: str, value: T):
@@ -324,8 +360,8 @@ class SelectionParameter(Parameter[Any]):
324
360
  The name of the parameter
325
361
  value : Any
326
362
  The initially selected value (must be one of the options)
327
- options : list
328
- List of valid choices that can be selected
363
+ options : sequence
364
+ List, tuple, or 1D numpy array of valid choices that can be selected
329
365
 
330
366
  Examples
331
367
  --------
@@ -336,15 +372,56 @@ class SelectionParameter(Parameter[Any]):
336
372
  >>> color.value
337
373
  'blue'
338
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
339
380
  """
340
381
 
341
382
  options: List[Any]
342
383
 
343
- def __init__(self, name: str, value: Any, options: List[Any]):
384
+ def __init__(self, name: str, value: Any, options: Union[List, Tuple]):
344
385
  self.name = name
345
- self.options = options
386
+ self.options = self._validate_options(options)
346
387
  self._value = self._validate(value)
347
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)
424
+
348
425
  def _validate(self, new_value: Any) -> Any:
349
426
  """
350
427
  Validate that value is one of the allowed options.
@@ -372,13 +449,14 @@ class SelectionParameter(Parameter[Any]):
372
449
  Raises:
373
450
  TypeError: If options is not a list or tuple
374
451
  """
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
- )
452
+ self.options = self._validate_options(self.options)
379
453
  if self.value not in self.options:
380
454
  warn(
381
- f"Value {self.value} not in options: {self.options}, setting to first option"
455
+ ParameterUpdateWarning(
456
+ self.name,
457
+ type(self).__name__,
458
+ f"Value {self.value} not in options: {self.options}, setting to first option",
459
+ )
382
460
  )
383
461
  self.value = self.options[0]
384
462
 
@@ -398,8 +476,8 @@ class MultipleSelectionParameter(Parameter[List[Any]]):
398
476
  The name of the parameter
399
477
  value : list
400
478
  List of initially selected values (must all be from options)
401
- options : list
402
- List of valid choices that can be selected
479
+ options : sequence
480
+ List, tuple, or 1D numpy array of valid choices that can be selected
403
481
 
404
482
  Examples
405
483
  --------
@@ -408,18 +486,58 @@ class MultipleSelectionParameter(Parameter[List[Any]]):
408
486
  ... options=["cheese", "mushrooms", "pepperoni", "olives"])
409
487
  >>> toppings.value
410
488
  ['cheese', 'mushrooms']
411
- >>> toppings.update({"value": ["cheese", "pepperoni"]})
412
- >>> toppings.value
413
- ['cheese', 'pepperoni']
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]
414
496
  """
415
497
 
416
498
  options: List[Any]
417
499
 
418
- def __init__(self, name: str, value: List[Any], options: List[Any]):
500
+ def __init__(self, name: str, value: List[Any], options: Union[List, Tuple]):
419
501
  self.name = name
420
- self.options = options
502
+ self.options = self._validate_options(options)
421
503
  self._value = self._validate(value)
422
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
+
423
541
  def _validate(self, new_value: Any) -> List[Any]:
424
542
  """
425
543
  Validate list of selected values against options.
@@ -446,19 +564,24 @@ class MultipleSelectionParameter(Parameter[List[Any]]):
446
564
  return [x for x in self.options if x in new_value]
447
565
 
448
566
  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
- )
567
+ self.options = self._validate_options(self.options)
453
568
  if not isinstance(self.value, (list, tuple)):
454
569
  warn(
455
- f"For parameter {self.name}, value {self.value} is not a list or tuple. Setting to empty list."
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
+ )
456
575
  )
457
576
  self.value = []
458
577
  if not all(val in self.options for val in self.value):
459
578
  invalid = [val for val in self.value if val not in self.options]
460
579
  warn(
461
- f"For parameter {self.name}, value {self.value} contains invalid options: {invalid}. Setting to empty list."
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
+ )
462
585
  )
463
586
  self.value = []
464
587
  # Keep only unique values while preserving order based on self.options
@@ -535,10 +658,22 @@ class IntegerParameter(Parameter[int]):
535
658
 
536
659
  if compare_to_range:
537
660
  if new_value < self.min_value:
538
- warn(f"Value {new_value} below minimum {self.min_value}, clamping")
661
+ warn(
662
+ ParameterUpdateWarning(
663
+ self.name,
664
+ type(self).__name__,
665
+ f"Value {new_value} below minimum {self.min_value}, clamping",
666
+ )
667
+ )
539
668
  new_value = self.min_value
540
669
  if new_value > self.max_value:
541
- warn(f"Value {new_value} above maximum {self.max_value}, clamping")
670
+ warn(
671
+ ParameterUpdateWarning(
672
+ self.name,
673
+ type(self).__name__,
674
+ f"Value {new_value} above maximum {self.max_value}, clamping",
675
+ )
676
+ )
542
677
  new_value = self.max_value
543
678
  return int(new_value)
544
679
 
@@ -559,7 +694,13 @@ class IntegerParameter(Parameter[int]):
559
694
  "IntegerParameter must have both min_value and max_value bounds",
560
695
  )
561
696
  if self.min_value > self.max_value:
562
- warn(f"Min value greater than max value, swapping")
697
+ warn(
698
+ ParameterUpdateWarning(
699
+ self.name,
700
+ type(self).__name__,
701
+ f"Min value greater than max value, swapping",
702
+ )
703
+ )
563
704
  self.min_value, self.max_value = self.max_value, self.min_value
564
705
  self.value = self._validate(self.value)
565
706
 
@@ -651,10 +792,22 @@ class FloatParameter(Parameter[float]):
651
792
 
652
793
  if compare_to_range:
653
794
  if new_value < self.min_value:
654
- warn(f"Value {new_value} below minimum {self.min_value}, clamping")
795
+ warn(
796
+ ParameterUpdateWarning(
797
+ self.name,
798
+ type(self).__name__,
799
+ f"Value {new_value} below minimum {self.min_value}, clamping",
800
+ )
801
+ )
655
802
  new_value = self.min_value
656
803
  if new_value > self.max_value:
657
- warn(f"Value {new_value} above maximum {self.max_value}, clamping")
804
+ warn(
805
+ ParameterUpdateWarning(
806
+ self.name,
807
+ type(self).__name__,
808
+ f"Value {new_value} above maximum {self.max_value}, clamping",
809
+ )
810
+ )
658
811
  new_value = self.max_value
659
812
 
660
813
  return float(new_value)
@@ -676,7 +829,13 @@ class FloatParameter(Parameter[float]):
676
829
  "FloatParameter must have both min_value and max_value bounds",
677
830
  )
678
831
  if self.min_value > self.max_value:
679
- warn(f"Min value greater than max value, swapping")
832
+ warn(
833
+ ParameterUpdateWarning(
834
+ self.name,
835
+ type(self).__name__,
836
+ f"Min value greater than max value, swapping",
837
+ )
838
+ )
680
839
  self.min_value, self.max_value = self.max_value, self.min_value
681
840
  self.value = self._validate(self.value)
682
841
 
@@ -772,14 +931,32 @@ class IntegerRangeParameter(Parameter[Tuple[int, int]]):
772
931
  high = self._validate_single(new_value[1])
773
932
 
774
933
  if low > high:
775
- warn(f"Low value {low} greater than high value {high}, swapping")
934
+ warn(
935
+ ParameterUpdateWarning(
936
+ self.name,
937
+ type(self).__name__,
938
+ f"Low value {low} greater than high value {high}, swapping",
939
+ )
940
+ )
776
941
  low, high = high, low
777
942
 
778
943
  if low < self.min_value:
779
- warn(f"Low value {low} below minimum {self.min_value}, clamping")
944
+ warn(
945
+ ParameterUpdateWarning(
946
+ self.name,
947
+ type(self).__name__,
948
+ f"Low value {low} below minimum {self.min_value}, clamping",
949
+ )
950
+ )
780
951
  low = self.min_value
781
952
  if high > self.max_value:
782
- warn(f"High value {high} above maximum {self.max_value}, clamping")
953
+ warn(
954
+ ParameterUpdateWarning(
955
+ self.name,
956
+ type(self).__name__,
957
+ f"High value {high} above maximum {self.max_value}, clamping",
958
+ )
959
+ )
783
960
  high = self.max_value
784
961
 
785
962
  return (low, high)
@@ -801,7 +978,13 @@ class IntegerRangeParameter(Parameter[Tuple[int, int]]):
801
978
  "IntegerRangeParameter must have both min_value and max_value bounds",
802
979
  )
803
980
  if self.min_value > self.max_value:
804
- warn(f"Min value greater than max value, swapping")
981
+ warn(
982
+ ParameterUpdateWarning(
983
+ self.name,
984
+ type(self).__name__,
985
+ f"Min value greater than max value, swapping",
986
+ )
987
+ )
805
988
  self.min_value, self.max_value = self.max_value, self.min_value
806
989
  self.value = self._validate(self.value)
807
990
 
@@ -913,14 +1096,32 @@ class FloatRangeParameter(Parameter[Tuple[float, float]]):
913
1096
  high = self._validate_single(new_value[1])
914
1097
 
915
1098
  if low > high:
916
- warn(f"Low value {low} greater than high value {high}, swapping")
1099
+ warn(
1100
+ ParameterUpdateWarning(
1101
+ self.name,
1102
+ type(self).__name__,
1103
+ f"Low value {low} greater than high value {high}, swapping",
1104
+ )
1105
+ )
917
1106
  low, high = high, low
918
1107
 
919
1108
  if low < self.min_value:
920
- warn(f"Low value {low} below minimum {self.min_value}, clamping")
1109
+ warn(
1110
+ ParameterUpdateWarning(
1111
+ self.name,
1112
+ type(self).__name__,
1113
+ f"Low value {low} below minimum {self.min_value}, clamping",
1114
+ )
1115
+ )
921
1116
  low = self.min_value
922
1117
  if high > self.max_value:
923
- warn(f"High value {high} above maximum {self.max_value}, clamping")
1118
+ warn(
1119
+ ParameterUpdateWarning(
1120
+ self.name,
1121
+ type(self).__name__,
1122
+ f"High value {high} above maximum {self.max_value}, clamping",
1123
+ )
1124
+ )
924
1125
  high = self.max_value
925
1126
 
926
1127
  return (low, high)
@@ -942,7 +1143,13 @@ class FloatRangeParameter(Parameter[Tuple[float, float]]):
942
1143
  "FloatRangeParameter must have both min_value and max_value bounds",
943
1144
  )
944
1145
  if self.min_value > self.max_value:
945
- warn(f"Min value greater than max value, swapping")
1146
+ warn(
1147
+ ParameterUpdateWarning(
1148
+ self.name,
1149
+ type(self).__name__,
1150
+ f"Min value greater than max value, swapping",
1151
+ )
1152
+ )
946
1153
  self.min_value, self.max_value = self.max_value, self.min_value
947
1154
  self.value = self._validate(self.value)
948
1155
 
@@ -1033,10 +1240,22 @@ class UnboundedIntegerParameter(Parameter[int]):
1033
1240
 
1034
1241
  if compare_to_range:
1035
1242
  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")
1243
+ warn(
1244
+ ParameterUpdateWarning(
1245
+ self.name,
1246
+ type(self).__name__,
1247
+ f"Value {new_value} below minimum {self.min_value}, clamping",
1248
+ )
1249
+ )
1037
1250
  new_value = self.min_value
1038
1251
  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")
1252
+ warn(
1253
+ ParameterUpdateWarning(
1254
+ self.name,
1255
+ type(self).__name__,
1256
+ f"Value {new_value} above maximum {self.max_value}, clamping",
1257
+ )
1258
+ )
1040
1259
  new_value = self.max_value
1041
1260
  return int(new_value)
1042
1261
 
@@ -1055,7 +1274,13 @@ class UnboundedIntegerParameter(Parameter[int]):
1055
1274
  and self.max_value is not None
1056
1275
  and self.min_value > self.max_value
1057
1276
  ):
1058
- warn(f"Min value greater than max value, swapping")
1277
+ warn(
1278
+ ParameterUpdateWarning(
1279
+ self.name,
1280
+ type(self).__name__,
1281
+ f"Min value greater than max value, swapping",
1282
+ )
1283
+ )
1059
1284
  self.min_value, self.max_value = self.max_value, self.min_value
1060
1285
  self.value = self._validate(self.value)
1061
1286
 
@@ -1161,10 +1386,22 @@ class UnboundedFloatParameter(Parameter[float]):
1161
1386
 
1162
1387
  if compare_to_range:
1163
1388
  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")
1389
+ warn(
1390
+ ParameterUpdateWarning(
1391
+ self.name,
1392
+ type(self).__name__,
1393
+ f"Value {new_value} below minimum {self.min_value}, clamping",
1394
+ )
1395
+ )
1165
1396
  new_value = self.min_value
1166
1397
  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")
1398
+ warn(
1399
+ ParameterUpdateWarning(
1400
+ self.name,
1401
+ type(self).__name__,
1402
+ f"Value {new_value} above maximum {self.max_value}, clamping",
1403
+ )
1404
+ )
1168
1405
  new_value = self.max_value
1169
1406
 
1170
1407
  return float(new_value)
@@ -1184,19 +1421,66 @@ class UnboundedFloatParameter(Parameter[float]):
1184
1421
  and self.max_value is not None
1185
1422
  and self.min_value > self.max_value
1186
1423
  ):
1187
- warn(f"Min value greater than max value, swapping")
1424
+ warn(
1425
+ ParameterUpdateWarning(
1426
+ self.name,
1427
+ type(self).__name__,
1428
+ f"Min value greater than max value, swapping",
1429
+ )
1430
+ )
1188
1431
  self.min_value, self.max_value = self.max_value, self.min_value
1189
1432
  self.value = self._validate(self.value)
1190
1433
 
1191
1434
 
1192
- class ButtonParameter(Parameter):
1193
- """A parameter that represents a clickable button."""
1435
+ @dataclass(init=False)
1436
+ class ButtonAction(Parameter[None]):
1437
+ """
1438
+ Parameter for creating clickable buttons with callbacks.
1439
+
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
+ """
1194
1475
 
1195
- _is_button: bool = True
1476
+ label: str
1477
+ callback: Callable
1478
+ value: None = field(default=None, repr=False)
1479
+ _is_action: bool = field(default=True, repr=False)
1196
1480
 
1197
- def __init__(self, name: str, label: str, callback: Callable[[], None]):
1481
+ def __init__(self, name: str, label: str, callback: Callable):
1198
1482
  """
1199
- Initialize a button parameter.
1483
+ Initialize a button.
1200
1484
 
1201
1485
  Args:
1202
1486
  name: Internal name of the parameter
@@ -1206,32 +1490,28 @@ class ButtonParameter(Parameter):
1206
1490
  self.name = name
1207
1491
  self.label = label
1208
1492
  self.callback = callback
1209
- self._value = None # Buttons don't have a value in the traditional sense
1493
+ self._value = None
1210
1494
 
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
1495
+ def _validate(self, new_value: Any) -> None:
1496
+ """Validate the button's value."""
1497
+ return None
1227
1498
 
1228
1499
  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
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
+ )
1235
1515
 
1236
1516
 
1237
1517
  class ParameterType(Enum):
@@ -1247,4 +1527,7 @@ class ParameterType(Enum):
1247
1527
  float_range = FloatRangeParameter
1248
1528
  unbounded_integer = UnboundedIntegerParameter
1249
1529
  unbounded_float = UnboundedFloatParameter
1250
- button = ButtonParameter
1530
+
1531
+
1532
+ class ActionType(Enum):
1533
+ button = ButtonAction