Skip to content

Commit 86eab49

Browse files
authored
Remove Unique_name fallback and integrate serialization with observer… (#158)
1 parent 959f23c commit 86eab49

File tree

5 files changed

+146
-204
lines changed

5 files changed

+146
-204
lines changed

src/easyscience/global_object/map.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def is_connected(self, vertices_encountered=None, start_vertex=None) -> bool:
261261
return False
262262

263263
def _clear(self):
264-
"""Reset the map to an empty state."""
264+
"""Reset the map to an empty state. Only to be used for testing"""
265265
for vertex in self.vertices():
266266
self.prune(vertex)
267267
gc.collect()

src/easyscience/variable/descriptor_number.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import numbers
4+
import uuid
45
from typing import Any
56
from typing import Dict
67
from typing import List
@@ -50,6 +51,7 @@ def __init__(
5051
url: Optional[str] = None,
5152
display_name: Optional[str] = None,
5253
parent: Optional[Any] = None,
54+
**kwargs: Any # Additional keyword arguments (used for (de)serialization)
5355
):
5456
"""Constructor for the DescriptorNumber class
5557
@@ -65,6 +67,10 @@ def __init__(
6567
"""
6668
self._observers: List[DescriptorNumber] = []
6769

70+
# Extract dependency_id if provided during deserialization
71+
if '__dependency_id' in kwargs:
72+
self.__dependency_id = kwargs.pop('__dependency_id')
73+
6874
if not isinstance(value, numbers.Number) or isinstance(value, bool):
6975
raise TypeError(f'{value=} must be a number')
7076
if variance is not None:
@@ -112,10 +118,14 @@ def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorNumb
112118
def _attach_observer(self, observer: DescriptorNumber) -> None:
113119
"""Attach an observer to the descriptor."""
114120
self._observers.append(observer)
121+
if not hasattr(self, '_DescriptorNumber__dependency_id'):
122+
self.__dependency_id = str(uuid.uuid4())
115123

116124
def _detach_observer(self, observer: DescriptorNumber) -> None:
117125
"""Detach an observer from the descriptor."""
118126
self._observers.remove(observer)
127+
if not self._observers:
128+
del self.__dependency_id
119129

120130
def _notify_observers(self) -> None:
121131
"""Notify all observers of a change."""
@@ -323,6 +333,8 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]:
323333
raw_dict['value'] = self._scalar.value
324334
raw_dict['unit'] = str(self._scalar.unit)
325335
raw_dict['variance'] = self._scalar.variance
336+
if hasattr(self, '_DescriptorNumber__dependency_id'):
337+
raw_dict['__dependency_id'] = self.__dependency_id
326338
return raw_dict
327339

328340
def __add__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber:

src/easyscience/variable/parameter.py

Lines changed: 40 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import weakref
1212
from typing import Any
1313
from typing import Dict
14+
from typing import List
1415
from typing import Optional
1516
from typing import Union
1617

@@ -36,14 +37,6 @@ class Parameter(DescriptorNumber):
3637
# We copy the parent's _REDIRECT and modify it to avoid altering the parent's class dict
3738
_REDIRECT = DescriptorNumber._REDIRECT.copy()
3839
_REDIRECT['callback'] = None
39-
# Skip these attributes during normal serialization as they are handled specially
40-
_REDIRECT['_dependency_interpreter'] = None
41-
_REDIRECT['_clean_dependency_string'] = None
42-
# Skip the new serialization parameters - they'll be handled by _convert_to_dict
43-
_REDIRECT['_dependency_string'] = None
44-
_REDIRECT['_dependency_map_unique_names'] = None
45-
_REDIRECT['_dependency_map_dependency_ids'] = None
46-
_REDIRECT['__dependency_id'] = None
4740

4841
def __init__(
4942
self,
@@ -84,11 +77,8 @@ def __init__(
8477
""" # noqa: E501
8578
# Extract and ignore serialization-specific fields from kwargs
8679
kwargs.pop('_dependency_string', None)
87-
kwargs.pop('_dependency_map_unique_names', None)
8880
kwargs.pop('_dependency_map_dependency_ids', None)
8981
kwargs.pop('_independent', None)
90-
# Extract dependency_id if provided during deserialization
91-
provided_dependency_id = kwargs.pop('__dependency_id', None)
9282

9383
if not isinstance(min, numbers.Number):
9484
raise TypeError('`min` must be a number')
@@ -119,6 +109,7 @@ def __init__(
119109
url=url,
120110
display_name=display_name,
121111
parent=parent,
112+
**kwargs, # Additional keyword arguments (used for (de)serialization)
122113
)
123114

124115
self._callback = callback # Callback is used by interface to link to model
@@ -128,13 +119,6 @@ def __init__(
128119
# Create additional fitting elements
129120
self._initial_scalar = copy.deepcopy(self._scalar)
130121

131-
# Generate unique dependency ID for serialization/deserialization
132-
# Use provided dependency_id if available (during deserialization), otherwise generate new one
133-
if provided_dependency_id is not None:
134-
self.__dependency_id = provided_dependency_id
135-
else:
136-
import uuid
137-
self.__dependency_id = str(uuid.uuid4())
138122

139123
@classmethod
140124
def from_dependency(cls, name: str, dependency_expression: str, dependency_map: Optional[dict] = None, **kwargs) -> Parameter: # noqa: E501
@@ -147,15 +131,15 @@ def from_dependency(cls, name: str, dependency_expression: str, dependency_map:
147131
:param kwargs: Additional keyword arguments to pass to the Parameter constructor.
148132
:return: A new dependent Parameter object.
149133
""" # noqa: E501
150-
# Set default values for required parameters if not provided in kwargs
134+
# Set default values for required parameters for the constructor, they get overwritten by the dependency anyways
151135
default_kwargs = {
152136
'value': 0.0,
153137
'unit': '',
154138
'variance': 0.0,
155139
'min': -np.inf,
156140
'max': np.inf
157141
}
158-
# Update with user-provided kwargs, giving precedence to user values
142+
# Update with user-provided kwargs, to avoid errors.
159143
default_kwargs.update(kwargs)
160144
parameter = cls(name=name, **default_kwargs)
161145
parameter.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map)
@@ -331,15 +315,6 @@ def dependency_map(self) -> Dict[str, DescriptorNumber]:
331315
def dependency_map(self, new_map: Dict[str, DescriptorNumber]) -> None:
332316
raise AttributeError('Dependency map is read-only. Use `make_dependent_on` to change the dependency map.')
333317

334-
@property
335-
def dependency_id(self) -> str:
336-
"""
337-
Get the unique dependency ID of this parameter used for serialization.
338-
339-
:return: The dependency ID of this parameter.
340-
"""
341-
return self.__dependency_id
342-
343318
@property
344319
def value_no_call_back(self) -> numbers.Number:
345320
"""
@@ -553,6 +528,26 @@ def free(self) -> bool:
553528
def free(self, value: bool) -> None:
554529
self.fixed = not value
555530

531+
def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]:
532+
""" Overwrite the as_dict method to handle dependency information. """
533+
raw_dict = super().as_dict(skip=skip)
534+
535+
536+
# Add dependency information for dependent parameters
537+
if not self._independent:
538+
# Save the dependency expression
539+
raw_dict['_dependency_string'] = self._clean_dependency_string
540+
541+
# Mark that this parameter is dependent
542+
raw_dict['_independent'] = self._independent
543+
544+
# Convert dependency_map to use dependency_ids
545+
raw_dict['_dependency_map_dependency_ids'] = {}
546+
for key, obj in self._dependency_map.items():
547+
raw_dict['_dependency_map_dependency_ids'][key] = obj._DescriptorNumber__dependency_id
548+
549+
return raw_dict
550+
556551
def _revert_dependency(self, skip_detach=False) -> None:
557552
"""
558553
Revert the dependency to the old dependency. This is used when an error is raised during setting the dependency.
@@ -595,63 +590,26 @@ def _process_dependency_unique_names(self, dependency_expression: str):
595590
raise ValueError(f'The object with unique_name {stripped_name} is not a Parameter or DescriptorNumber. Please check your dependency expression.') # noqa: E501
596591
self._clean_dependency_string = clean_dependency_string
597592

598-
def _convert_to_dict(self, d: dict, encoder, skip=None, **kwargs) -> dict:
599-
"""Custom serialization to handle parameter dependencies."""
600-
if skip is None:
601-
skip = []
602-
603-
# Add dependency information for dependent parameters
604-
if not self._independent:
605-
# Save the dependency expression
606-
d['_dependency_string'] = self._dependency_string
607-
608-
# Convert dependency_map to use dependency_ids (preferred) and unique_names (fallback)
609-
d['_dependency_map_dependency_ids'] = {}
610-
d['_dependency_map_unique_names'] = {}
611-
612-
for key, dep_obj in self._dependency_map.items():
613-
# Store dependency_id if available (more reliable)
614-
if hasattr(dep_obj, '__dependency_id'):
615-
d['_dependency_map_dependency_ids'][key] = dep_obj.__dependency_id
616-
# Also store unique_name as fallback
617-
if hasattr(dep_obj, 'unique_name'):
618-
d['_dependency_map_unique_names'][key] = dep_obj.unique_name
619-
else:
620-
# This is quite impossible - throw an error
621-
raise ValueError(f'The object with unique_name {key} does not have a unique_name attribute.')
622-
623-
# Always include dependency_id for this parameter
624-
d['__dependency_id'] = self.__dependency_id
625-
626-
# Mark that this parameter is dependent
627-
d['_independent'] = self._independent
628-
629-
return d
630-
631-
@classmethod
593+
@classmethod
632594
def from_dict(cls, obj_dict: dict) -> 'Parameter':
633595
"""
634596
Custom deserialization to handle parameter dependencies.
635597
Override the parent method to handle dependency information.
636598
"""
637599
# Extract dependency information before creating the parameter
638-
d = obj_dict.copy() # Don't modify the original dict
639-
dependency_string = d.pop('_dependency_string', None)
640-
dependency_map_unique_names = d.pop('_dependency_map_unique_names', None)
641-
dependency_map_dependency_ids = d.pop('_dependency_map_dependency_ids', None)
642-
is_independent = d.pop('_independent', True)
643-
# Note: Keep __dependency_id in the dict so it gets passed to __init__
600+
raw_dict = obj_dict.copy() # Don't modify the original dict
601+
dependency_string = raw_dict.pop('_dependency_string', None)
602+
dependency_map_dependency_ids = raw_dict.pop('_dependency_map_dependency_ids', None)
603+
is_independent = raw_dict.pop('_independent', True)
604+
# Note: Keep _dependency_id in the dict so it gets passed to __init__
644605

645606
# Create the parameter using the base class method (dependency_id is now handled in __init__)
646-
param = super().from_dict(d)
607+
param = super().from_dict(raw_dict)
647608

648609
# Store dependency information for later resolution
649-
if not is_independent and dependency_string is not None:
610+
if not is_independent:
650611
param._pending_dependency_string = dependency_string
651-
if dependency_map_dependency_ids:
652-
param._pending_dependency_map_dependency_ids = dependency_map_dependency_ids
653-
if dependency_map_unique_names:
654-
param._pending_dependency_map_unique_names = dependency_map_unique_names
612+
param._pending_dependency_map_dependency_ids = dependency_map_dependency_ids
655613
# Keep parameter as independent initially - will be made dependent after all objects are loaded
656614
param._independent = True
657615

@@ -995,13 +953,12 @@ def resolve_pending_dependencies(self) -> None:
995953
"""Resolve pending dependencies after deserialization.
996954
997955
This method should be called after all parameters have been deserialized
998-
to establish dependency relationships using dependency_ids or unique_names as fallback.
956+
to establish dependency relationships using dependency_ids.
999957
"""
1000958
if hasattr(self, '_pending_dependency_string'):
1001959
dependency_string = self._pending_dependency_string
1002960
dependency_map = {}
1003961

1004-
# Try dependency IDs first (more reliable)
1005962
if hasattr(self, '_pending_dependency_map_dependency_ids'):
1006963
dependency_map_dependency_ids = self._pending_dependency_map_dependency_ids
1007964

@@ -1013,22 +970,6 @@ def resolve_pending_dependencies(self) -> None:
1013970
else:
1014971
raise ValueError(f"Cannot find parameter with dependency_id '{dependency_id}'")
1015972

1016-
# Fallback to unique_names if dependency IDs not available or incomplete
1017-
if hasattr(self, '_pending_dependency_map_unique_names'):
1018-
dependency_map_unique_names = self._pending_dependency_map_unique_names
1019-
1020-
for key, unique_name in dependency_map_unique_names.items():
1021-
if key not in dependency_map: # Only add if not already resolved by dependency_id
1022-
try:
1023-
# Look up the parameter by unique_name in the global map
1024-
dep_obj = self._global_object.map.get_item_by_key(unique_name)
1025-
if dep_obj is not None:
1026-
dependency_map[key] = dep_obj
1027-
else:
1028-
raise ValueError(f"Cannot find parameter with unique_name '{unique_name}'")
1029-
except Exception as e:
1030-
raise ValueError(f"Error resolving dependency '{key}' -> '{unique_name}': {e}")
1031-
1032973
# Establish the dependency relationship
1033974
try:
1034975
self.make_dependent_on(dependency_expression=dependency_string, dependency_map=dependency_map)
@@ -1037,14 +978,13 @@ def resolve_pending_dependencies(self) -> None:
1037978

1038979
# Clean up temporary attributes
1039980
delattr(self, '_pending_dependency_string')
1040-
if hasattr(self, '_pending_dependency_map_dependency_ids'):
1041-
delattr(self, '_pending_dependency_map_dependency_ids')
1042-
if hasattr(self, '_pending_dependency_map_unique_names'):
1043-
delattr(self, '_pending_dependency_map_unique_names')
981+
delattr(self, '_pending_dependency_map_dependency_ids')
1044982

1045-
def _find_parameter_by_dependency_id(self, dependency_id: str) -> Optional['Parameter']:
983+
def _find_parameter_by_dependency_id(self, dependency_id: str) -> Optional['DescriptorNumber']:
1046984
"""Find a parameter by its dependency_id from all parameters in the global map."""
1047985
for obj in self._global_object.map._store.values():
1048-
if isinstance(obj, Parameter) and hasattr(obj, '__dependency_id') and obj.__dependency_id == dependency_id:
1049-
return obj
986+
if isinstance(obj, DescriptorNumber) and hasattr(obj, '_DescriptorNumber__dependency_id'):
987+
if obj._DescriptorNumber__dependency_id == dependency_id:
988+
return obj
1050989
return None
990+

src/easyscience/variable/parameter_dependency_resolver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def _collect_parameters(item: Any, parameters: List[Parameter]) -> None:
6767
resolved_count += 1
6868
except Exception as e:
6969
error_count += 1
70-
dependency_id = getattr(param, '__dependency_id', 'unknown')
70+
dependency_id = getattr(param, '_DescriptorNumber__dependency_id', 'unknown')
7171
errors.append(f"Failed to resolve dependencies for parameter '{param.name}'" \
7272
f" (unique_name: '{param.unique_name}', dependency_id: '{dependency_id}'): {e}")
7373

0 commit comments

Comments
 (0)