Skip to content

BUG: serialize numpy SeedSequence for sensor seeds (#1087) - #1124

Merged
Gui-FernandesBR merged 2 commits into
RocketPy-Team:developfrom
thatrandomasiandev:bug/1087-sensor-seedsequence-serialize
Aug 15, 2026
Merged

Gui-FernandesBR merged 2 commits into
RocketPy-Team:developfrom
thatrandomasiandev:bug/1087-sensor-seedsequence-serialize

Conversation

@thatrandomasiandev

Copy link
Copy Markdown

Summary

  • Teach RocketPyEncoder / RocketPyDecoder to round-trip numpy.random.SeedSequence via its reconstructible state (entropy, spawn_key, n_children_spawned, pool_size) plus a class signature.
  • Fixes TypeError: Object of type SeedSequence is not JSON serializable when dumping sensors (or any object) that hold a SeedSequence seed, including spawned child sequences used for parallel Monte Carlo.

Fixes #1087

Test plan

  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONPATH=. pytest -p pytest_cov tests/unit/sensors/test_sensor_seeding.py -q (8 passed)
  • Regression: Accelerometer(..., seed=SeedSequence(0).spawn(1)[0]) then json.dumps(sensor.to_dict(), cls=RocketPyEncoder) succeeds and decoder restores matching .state

@thatrandomasiandev
thatrandomasiandev requested a review from a team as a code owner August 11, 2026 01:33
@Gui-FernandesBR
Gui-FernandesBR force-pushed the bug/1087-sensor-seedsequence-serialize branch from c586fd2 to 118d866 Compare August 11, 2026 01:48
@Gui-FernandesBR Gui-FernandesBR linked an issue Aug 15, 2026 that may be closed by this pull request
@Gui-FernandesBR
Gui-FernandesBR force-pushed the bug/1087-sensor-seedsequence-serialize branch from 118d866 to 34679a2 Compare August 15, 2026 01:50
@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.33%. Comparing base (76cab5a) to head (db9e68e).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #1124   +/-   ##
========================================
  Coverage    84.33%   84.33%           
========================================
  Files          130      130           
  Lines        17258    17266    +8     
========================================
+ Hits         14554    14562    +8     
  Misses        2704     2704           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Gui-FernandesBR
Gui-FernandesBR force-pushed the bug/1087-sensor-seedsequence-serialize branch from 34679a2 to ee699b6 Compare August 15, 2026 02:28
The SeedSequence branch pushed object_hook to 26 statements, one over
pylint's max-statements, failing the Linters job. Extract the Flight
rebuild into a module-level helper next to set_minimal_flight_attributes,
matching how that path is already factored. Pure code move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Gui-FernandesBR
Gui-FernandesBR merged commit 2ce0ad6 into RocketPy-Team:develop Aug 15, 2026
9 checks passed
myungjunlee added a commit to myungjunlee/RocketPy that referenced this pull request Aug 16, 2026
`Sensor.__init__` passes the seed straight to `numpy.random.default_rng`,
which also accepts `Generator` and `BitGenerator` objects. The sensor then
constructs successfully and stores the object on `self._seed`, where
`to_dict()` emits it verbatim, so the failure only surfaces later at
`json.dumps()`, far from the call that caused it.

RocketPy-Team#1124 closed the `SeedSequence` case in RocketPy-Team#1087 by teaching `RocketPyEncoder`
to write one out. That works because a `SeedSequence` is defined by its
entropy and spawn key, so it still describes the stream after a round trip.
A `Generator` has no such description: its state advances on every draw, so
whatever `to_dict()` wrote would depend on when it ran, and restoring it
would not reproduce the stream the sensor actually used.

Reject those two in the constructor instead, so the failure stays at the
call that caused it. Ints, numpy ints, `SeedSequence` and `None` are
untouched, as are the sequences of ints `default_rng` accepts and the
encoder already serializes, so no seed that works today is rejected.

Annotate `seed` on every constructor that takes one, with the type the
issue itself names, so the contract is stated where the argument is
declared rather than only in the docstring.
Gui-FernandesBR pushed a commit to myungjunlee/RocketPy that referenced this pull request Sep 9, 2026
`Sensor.__init__` passes the seed straight to `numpy.random.default_rng`,
which also accepts `Generator` and `BitGenerator` objects. The sensor then
constructs successfully and stores the object on `self._seed`, where
`to_dict()` emits it verbatim, so the failure only surfaces later at
`json.dumps()`, far from the call that caused it.

RocketPy-Team#1124 closed the `SeedSequence` case in RocketPy-Team#1087 by teaching `RocketPyEncoder`
to write one out. That works because a `SeedSequence` is defined by its
entropy and spawn key, so it still describes the stream after a round trip.
A `Generator` has no such description: its state advances on every draw, so
whatever `to_dict()` wrote would depend on when it ran, and restoring it
would not reproduce the stream the sensor actually used.

Reject those two in the constructor instead, so the failure stays at the
call that caused it. Ints, numpy ints, `SeedSequence` and `None` are
untouched, as are the sequences of ints `default_rng` accepts and the
encoder already serializes, so no seed that works today is rejected.

Annotate `seed` on every constructor that takes one, with the type the
issue itself names, so the contract is stated where the argument is
declared rather than only in the docstring.
Gui-FernandesBR pushed a commit that referenced this pull request Sep 9, 2026
* BUG: reject live RNG objects as Sensor seeds

`Sensor.__init__` passes the seed straight to `numpy.random.default_rng`,
which also accepts `Generator` and `BitGenerator` objects. The sensor then
constructs successfully and stores the object on `self._seed`, where
`to_dict()` emits it verbatim, so the failure only surfaces later at
`json.dumps()`, far from the call that caused it.

#1124 closed the `SeedSequence` case in #1087 by teaching `RocketPyEncoder`
to write one out. That works because a `SeedSequence` is defined by its
entropy and spawn key, so it still describes the stream after a round trip.
A `Generator` has no such description: its state advances on every draw, so
whatever `to_dict()` wrote would depend on when it ran, and restoring it
would not reproduce the stream the sensor actually used.

Reject those two in the constructor instead, so the failure stays at the
call that caused it. Ints, numpy ints, `SeedSequence` and `None` are
untouched, as are the sequences of ints `default_rng` accepts and the
encoder already serializes, so no seed that works today is rejected.

Annotate `seed` on every constructor that takes one, with the type the
issue itself names, so the contract is stated where the argument is
declared rather than only in the docstring.

* BUG: reject RandomState and other non-descriptor Sensor seeds

default_rng also accepts RandomState from NumPy 2.2 on, and RocketPy pins
no upper bound on numpy, so the previous isinstance list let it through to
the same late TypeError at json.dumps() that #1087 reported.

Check the stable half of the contract instead of enumerating the live types:
accept ints, array_like of ints and SeedSequence, and refuse the rest. A
seed kind numpy starts accepting later is now refused at construction rather
than reaching serialization.

Widen the annotation to the array_like integer contract the check actually
takes. It goes through a SeedLike union so the seven signatures stay inside
the line limit while help() and inspect.signature() still expand the members.

* TST: compare the noise stream across a seed round trip

The existing round-trip tests assert on the stored seed value, which would
still pass for a seed that survives JSON without naming the stream the
original sensor used. Draw from the restored sensor instead and compare it
against a fresh one built from the same seed, across the four descriptor
kinds the constructor accepts.

* BUG: refuse the seed shapes SeedSequence cannot take either

The seed check accepted a nested sequence of ints, on the stated grounds
that "numpy accepts as entropy just the same". It does not: SeedSequence
raises TypeError for a nested sequence and ValueError for an array of two
or more dimensions. Older NumPy let the nested form through, which is why
this passed under Python 3.10 and failed under 3.14, where a newer NumPy is
resolved -- the acceptance was never portable, and RocketPy sets no upper
bound on the dependency.

So _is_int_array_like now takes a flat sequence only, and an ndarray only
at ndim <= 1. Both refusals name the seed, where the NumPy messages they
replace name neither it nor the argument that carried it.

The nested case moves out of test_int_array_like_seeds_are_accepted and
into a rejection test alongside a 2-D array, which the check would have
admitted for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Gui-FernandesBR <guilherme.fernandes@maggu.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sensor accepts a SeedSequence seed but cannot serialize it

2 participants