Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions rocketpy/_encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 15 additions & 1 deletion tests/unit/sensors/test_sensor_seeding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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