diff --git a/rocketpy/_encoders.py b/rocketpy/_encoders.py index b6730f65c..e9fbd6ed6 100644 --- a/rocketpy/_encoders.py +++ b/rocketpy/_encoders.py @@ -56,6 +56,17 @@ def default(self, o): return o.item() elif isinstance(o, np.ndarray): return o.tolist() + elif isinstance(o, np.random.SeedSequence): + # Sensor seeds (and other RNGs) may hold a SeedSequence. Encode its + # reconstructible state so JSON dump does not raise TypeError. + encoding = { + "entropy": o.entropy, + "spawn_key": list(o.spawn_key), + "n_children_spawned": int(o.n_children_spawned), + "pool_size": int(o.pool_size), + } + encoding["signature"] = get_class_signature(o) + return encoding elif isinstance(o, datetime): return [o.year, o.month, o.day, o.hour] elif hasattr(o, "__iter__") and not isinstance(o, str): @@ -110,6 +121,15 @@ def object_hook(self, obj): class_ = get_class_from_signature(signature) hash_ = signature.get("hash", None) + if class_ is np.random.SeedSequence: + # Cython __init__ has no __code__, so the generic kwargs + # path cannot rebuild SeedSequence; restore from state. + return np.random.SeedSequence( + entropy=obj.get("entropy"), + spawn_key=tuple(obj.get("spawn_key", ())), + pool_size=obj.get("pool_size", 4), + n_children_spawned=obj.get("n_children_spawned", 0), + ) if class_.__name__ == "Flight" and not self.resimulate: new_flight = class_.__new__(class_) new_flight.prints = _FlightPrints(new_flight) diff --git a/tests/unit/sensors/test_sensor_seeding.py b/tests/unit/sensors/test_sensor_seeding.py index d8474d317..73b4c664b 100644 --- a/tests/unit/sensors/test_sensor_seeding.py +++ b/tests/unit/sensors/test_sensor_seeding.py @@ -16,7 +16,7 @@ import numpy as np -from rocketpy._encoders import RocketPyEncoder +from rocketpy._encoders import RocketPyDecoder, RocketPyEncoder from rocketpy.mathutils.vector_matrix import Vector from rocketpy.sensors.accelerometer import Accelerometer from rocketpy.sensors.barometer import Barometer @@ -139,3 +139,17 @@ def test_from_dict_defaults_seed_to_none_when_absent(): ).to_dict() del data["seed"] assert GnssReceiver.from_dict(data).to_dict()["seed"] is None + + +def test_seedsequence_sensor_seed_is_json_serializable(): + """SeedSequence seeds must serialize through RocketPyEncoder (#1087).""" + seed = np.random.SeedSequence(0).spawn(1)[0] + sensor = Accelerometer(sampling_rate=100, seed=seed) + + encoded = json.dumps(sensor.to_dict(), cls=RocketPyEncoder) + decoded = json.loads(encoded, cls=RocketPyDecoder) + + assert isinstance(decoded["seed"], np.random.SeedSequence) + assert decoded["seed"].state == seed.state + restored = Accelerometer.from_dict(decoded) + assert restored.to_dict()["seed"].state == seed.state