@@ -422,6 +422,16 @@ def to_dict(self, **kwargs):
422422 "sampling_rate" : self .sampling_rate ,
423423 "name" : self .name ,
424424 "controlled_objects_name" : getattr (self , "controlled_objects_name" , None ),
425+ # Hash(es) identifying the controlled object(s), so Rocket
426+ # deserialization can reconnect the controller to the rocket's own
427+ # reconstructed objects (see Rocket.from_dict).
428+ "controlled_objects_hash" : self ._controlled_objects_hash (),
429+ # Which expensive simulation values the controller reads; needed so a
430+ # re-simulated flight computes them (e.g. state_history). Stored as a
431+ # list because JSON has no set type.
432+ "controller_needs" : (
433+ None if self .controller_needs is None else list (self .controller_needs )
434+ ),
425435 "context" : self .context .copy (), # Preserve context state
426436 "enabled" : self .enabled ,
427437 "disable_on" : disable_on ,
@@ -430,6 +440,24 @@ def to_dict(self, **kwargs):
430440 # object reference matching in Rocket deserialization
431441 }
432442
443+ def _controlled_objects_hash (self ):
444+ """Return the identity hash of the controlled object(s), matching the
445+ shape of ``controlled_objects`` (a single hash for a single object, a
446+ list of hashes for a list). These hashes match the ones the encoder
447+ stores in each object's signature, letting Rocket deserialization find
448+ the reconstructed objects. Returns ``None`` for anything unhashable."""
449+
450+ def safe_hash (obj ):
451+ try :
452+ return hash (obj )
453+ except TypeError :
454+ return None
455+
456+ controlled_objects = self .controlled_objects
457+ if isinstance (controlled_objects , (list , tuple )):
458+ return [safe_hash (obj ) for obj in controlled_objects ]
459+ return safe_hash (controlled_objects )
460+
433461 @classmethod
434462 def from_dict (cls , data , controlled_objects = None ):
435463 """Reconstruct controller from dictionary.
@@ -455,6 +483,7 @@ def from_dict(cls, data, controlled_objects=None):
455483 enabled = data .get ("enabled" , True )
456484 disable_on = data .get ("disable_on" )
457485 enable_on = data .get ("enable_on" )
486+ controller_needs = data .get ("controller_needs" )
458487
459488 try :
460489 controller_function = from_hex_decode (controller_function )
@@ -476,7 +505,7 @@ def from_dict(cls, data, controlled_objects=None):
476505 if controlled_objects is None :
477506 controlled_objects = []
478507
479- return cls (
508+ controller = cls (
480509 controller_function = controller_function ,
481510 controlled_objects = controlled_objects ,
482511 sampling_rate = sampling_rate ,
@@ -486,4 +515,28 @@ def from_dict(cls, data, controlled_objects=None):
486515 enabled = enabled ,
487516 disable_on = disable_on ,
488517 enable_on = enable_on ,
518+ controller_needs = controller_needs ,
519+ )
520+ # Stash the serialized controlled-object hash(es) so Rocket.from_dict
521+ # can reconnect the controller to the rocket's reconstructed objects.
522+ controller ._serialized_controlled_objects_hash = data .get (
523+ "controlled_objects_hash"
489524 )
525+ return controller
526+
527+ def rebind_controlled_objects (self , controlled_objects ):
528+ """Point the controller at reconstructed controlled object(s) and
529+ refresh the callback name bindings.
530+
531+ Used when a rocket is loaded from a file: the controller is rebuilt
532+ without its controlled objects (they are separate objects in the saved
533+ data), so this reconnects it to the rocket's own objects, ensuring the
534+ controller mutates them rather than orphaned copies.
535+
536+ Parameters
537+ ----------
538+ controlled_objects : object or list of object
539+ The reconstructed object(s) the controller should control.
540+ """
541+ self .controlled_objects = controlled_objects
542+ self ._controlled_objects_bindings = self .__verify_controlled_objects_name ()
0 commit comments