1111import weakref
1212from typing import Any
1313from typing import Dict
14+ from typing import List
1415from typing import Optional
1516from 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
0 commit comments