Skip to content
Merged
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
54 changes: 47 additions & 7 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,14 +121,17 @@ 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)
new_flight.plots = _FlightPlots(new_flight)
set_minimal_flight_attributes(new_flight, obj)
if hash_ is not None:
setattr(new_flight, "__rpy_hash", hash_)
return new_flight
return rebuild_minimal_flight(class_, obj, hash_)
elif hasattr(class_, "from_dict"):
new_obj = class_.from_dict(obj)
if hash_ is not None:
Expand Down Expand Up @@ -146,6 +160,32 @@ def object_hook(self, obj):
return obj


def rebuild_minimal_flight(class_, obj, hash_):
"""Rebuild a Flight from stored data without resimulating it.

Parameters
----------
class_ : type
The Flight class resolved from the stored signature.
obj : dict
The decoded data of the Flight object.
hash_ : str or None
The stored hash, when the encoder recorded one.

Returns
-------
Flight
The Flight object with its minimal attributes restored.
"""
new_flight = class_.__new__(class_)
new_flight.prints = _FlightPrints(new_flight)
new_flight.plots = _FlightPlots(new_flight)
set_minimal_flight_attributes(new_flight, obj)
if hash_ is not None:
setattr(new_flight, "__rpy_hash", hash_)
return new_flight


def set_minimal_flight_attributes(flight, obj):
attributes = (
"rocket",
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
Loading