Skip to content

Commit ba3af17

Browse files
committed
Remove Unique_name fallback and integrate serialization with observer pattern.
1 parent 959f23c commit ba3af17

File tree

5 files changed

+142
-251
lines changed

5 files changed

+142
-251
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, '_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, '_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: 88 additions & 149 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,90 @@ 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._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._dependency_id
548+
549+
return raw_dict
550+
551+
@classmethod
552+
def from_dict(cls, obj_dict: dict) -> 'Parameter':
553+
"""
554+
Custom deserialization to handle parameter dependencies.
555+
Override the parent method to handle dependency information.
556+
"""
557+
# Extract dependency information before creating the parameter
558+
raw_dict = obj_dict.copy() # Don't modify the original dict
559+
dependency_string = raw_dict.pop('_dependency_string', None)
560+
dependency_map_dependency_ids = raw_dict.pop('_dependency_map_dependency_ids', None)
561+
is_independent = raw_dict.pop('_independent', True)
562+
# Note: Keep _dependency_id in the dict so it gets passed to __init__
563+
564+
# Create the parameter using the base class method (dependency_id is now handled in __init__)
565+
param = super().from_dict(raw_dict)
566+
567+
# Store dependency information for later resolution
568+
if not is_independent:
569+
param._pending_dependency_string = dependency_string
570+
param._pending_dependency_map_dependency_ids = dependency_map_dependency_ids
571+
# Keep parameter as independent initially - will be made dependent after all objects are loaded
572+
param._independent = True
573+
574+
return param
575+
576+
def resolve_pending_dependencies(self) -> None:
577+
"""Resolve pending dependencies after deserialization.
578+
579+
This method should be called after all parameters have been deserialized
580+
to establish dependency relationships using dependency_ids.
581+
"""
582+
if hasattr(self, '_pending_dependency_string'):
583+
dependency_string = self._pending_dependency_string
584+
dependency_map = {}
585+
586+
if hasattr(self, '_pending_dependency_map_dependency_ids'):
587+
dependency_map_dependency_ids = self._pending_dependency_map_dependency_ids
588+
589+
# Build dependency_map by looking up objects by dependency_id
590+
for key, dependency_id in dependency_map_dependency_ids.items():
591+
dep_obj = self._find_parameter_by_dependency_id(dependency_id)
592+
if dep_obj is not None:
593+
dependency_map[key] = dep_obj
594+
else:
595+
raise ValueError(f"Cannot find parameter with dependency_id '{dependency_id}'")
596+
597+
# Establish the dependency relationship
598+
try:
599+
self.make_dependent_on(dependency_expression=dependency_string, dependency_map=dependency_map)
600+
except Exception as e:
601+
raise ValueError(f"Error establishing dependency '{dependency_string}': {e}")
602+
603+
# Clean up temporary attributes
604+
delattr(self, '_pending_dependency_string')
605+
delattr(self, '_pending_dependency_map_dependency_ids')
606+
607+
def _find_parameter_by_dependency_id(self, dependency_id: str) -> Optional['DescriptorNumber']:
608+
"""Find a parameter by its dependency_id from all parameters in the global map."""
609+
for obj in self._global_object.map._store.values():
610+
if isinstance(obj, DescriptorNumber) and hasattr(obj, '_dependency_id') and obj._dependency_id == dependency_id:
611+
return obj
612+
return None
613+
614+
556615
def _revert_dependency(self, skip_detach=False) -> None:
557616
"""
558617
Revert the dependency to the old dependency. This is used when an error is raised during setting the dependency.
@@ -595,68 +654,6 @@ def _process_dependency_unique_names(self, dependency_expression: str):
595654
raise ValueError(f'The object with unique_name {stripped_name} is not a Parameter or DescriptorNumber. Please check your dependency expression.') # noqa: E501
596655
self._clean_dependency_string = clean_dependency_string
597656

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
632-
def from_dict(cls, obj_dict: dict) -> 'Parameter':
633-
"""
634-
Custom deserialization to handle parameter dependencies.
635-
Override the parent method to handle dependency information.
636-
"""
637-
# 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__
644-
645-
# Create the parameter using the base class method (dependency_id is now handled in __init__)
646-
param = super().from_dict(d)
647-
648-
# Store dependency information for later resolution
649-
if not is_independent and dependency_string is not None:
650-
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
655-
# Keep parameter as independent initially - will be made dependent after all objects are loaded
656-
param._independent = True
657-
658-
return param
659-
660657
def __copy__(self) -> Parameter:
661658
new_obj = super().__copy__()
662659
new_obj._callback = property()
@@ -990,61 +987,3 @@ def __abs__(self) -> Parameter:
990987
parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value)
991988
parameter.name = parameter.unique_name
992989
return parameter
993-
994-
def resolve_pending_dependencies(self) -> None:
995-
"""Resolve pending dependencies after deserialization.
996-
997-
This method should be called after all parameters have been deserialized
998-
to establish dependency relationships using dependency_ids or unique_names as fallback.
999-
"""
1000-
if hasattr(self, '_pending_dependency_string'):
1001-
dependency_string = self._pending_dependency_string
1002-
dependency_map = {}
1003-
1004-
# Try dependency IDs first (more reliable)
1005-
if hasattr(self, '_pending_dependency_map_dependency_ids'):
1006-
dependency_map_dependency_ids = self._pending_dependency_map_dependency_ids
1007-
1008-
# Build dependency_map by looking up objects by dependency_id
1009-
for key, dependency_id in dependency_map_dependency_ids.items():
1010-
dep_obj = self._find_parameter_by_dependency_id(dependency_id)
1011-
if dep_obj is not None:
1012-
dependency_map[key] = dep_obj
1013-
else:
1014-
raise ValueError(f"Cannot find parameter with dependency_id '{dependency_id}'")
1015-
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-
1032-
# Establish the dependency relationship
1033-
try:
1034-
self.make_dependent_on(dependency_expression=dependency_string, dependency_map=dependency_map)
1035-
except Exception as e:
1036-
raise ValueError(f"Error establishing dependency '{dependency_string}': {e}")
1037-
1038-
# Clean up temporary attributes
1039-
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')
1044-
1045-
def _find_parameter_by_dependency_id(self, dependency_id: str) -> Optional['Parameter']:
1046-
"""Find a parameter by its dependency_id from all parameters in the global map."""
1047-
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
1050-
return None

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, '_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)