Skip to content

REL: v0.1.0 - #61

Merged
thc1006 merged 195 commits into
mainfrom
develop
Jul 29, 2026
Merged

REL: v0.1.0#61
thc1006 merged 195 commits into
mainfrom
develop

Conversation

@zuorenchen

@zuorenchen zuorenchen commented Jul 26, 2026

Copy link
Copy Markdown
Member

v0.1.0

Forty-seven pull requests since v0.0.2, from @zuorenchen, @thc1006 and @chichunwang.

Closes #93.

Urgent Upgrade Notes

Read this before running an existing agent

  • Running out of time is reported as truncated, not terminated. Gymnasium keeps the two apart deliberately: terminated means the episode reached a terminal state, which here is the flight ending, and truncated means something outside it stopped the episode, which is what running out of the precomputed horizon is. v0.0.2 returned terminated=True for both, so a loop written as while not terminated: never ends on the horizon. It keeps calling step(), current_step walks past the end of the precomputed balloon flights, and the run dies indexing them. For scenario 1 the horizon is the usual ending, so this is the common path rather than an edge.

    Any agent loop needs to become while not (terminated or truncated): and keep the fourth value step() returns. evaluate.py and both example runners in doc/examples/ were updated with the change. The distinction also matters to a learning agent: an algorithm bootstraps the value of the final state when the episode was truncated and does not when it terminated, so reporting the clock as termination teaches it that running out of time is absorbing. (#94, #102, @thc1006), for #93

  • Observations and given_parameters are copies now. An agent that wrote into the arrays it was handed used to be writing into the environment's own state, which was a scoring hole rather than an API detail: writing 2 into observation["balloon_status"] and never launching returned the whole scenario. Both are detached, so an agent that relied on mutating them in place no longer affects anything. (#99, @thc1006)

  • The gimbal limit parameter was renamed. rocket.control.gimbal_range is now rocket.control.max_gimbal_angle, and there is no alias. This key is in the whitelist agents receive, so an agent that reads given_parameters["rocket"]["control"]["gimbal_range"] raises KeyError on v0.1.0. The shipped example agents do not read it, so they are unaffected. (#39, @zuorenchen)

  • The default renderer crashes on Matplotlib 3.11. render_mode: "matplotlib" is what example_eval_cfg.yaml ships with and what README describes as needing no extra. On Matplotlib 3.11 the first frame raises AttributeError: 'list' object has no attribute 'shape' and the whole evaluation is lost. It needs two things at once, and this code produces both: Line3D.draw reads _verts3d[0].shape only when a coordinate is invalid for the axis scale, which every frame before launch is because the rocket state is all-NaN, and the update passed lists rather than arrays. Nothing bounds the version: the only constraint anywhere is ActiveRocketPy's matplotlib>=3.9.0. uv sync resolves 3.10.9 from the lockfile and is unaffected, but pip install -r requirements-dev.txt, which is what CI and the README's pip path both use, resolves whatever is current and hits it. Filed as #89 and fixed in this release by #90. Kept here because anyone who ran v0.0.2 against a current pip install hit it, and because it explains why the version you get is not the version the lockfile records.

  • The command line score line gained a prefix, and the notebook stopped printing one. Diagnostics moved from print to the logging module, and only the CLI configures logging. Total reward: 7 now reads INFO:__main__:Total reward: 7, so anything reading that line by prefix breaks. The Colab notebook and run_for_evaluation in doc/examples/run_env_agent.py configure no logging at all, so they print neither the completion nor the score; the notebook's last cell renders the returned environment tuple where the score used to be. (#51, @thc1006). Fixed in this release by #88, so v0.1.0 prints the score the way v0.0.2 did. The note stays because a v0.0.2 user reading these lines from a script needs to know the format did move and came back.

  • Trajectories differ from v0.0.2. The physics engine moved to the RocketPy v1.13 line and gained actuator dynamics. Results are reproducible within a release, not across this boundary, so anything measured against v0.0.2 needs re-running. (#60, @zuorenchen), (#39, @zuorenchen)

Changes by Kind

The first sixteen pull requests, grouped. Everything that landed after these
notes were first written is under Also in this release below.

Feature

  • Actuator dynamics for the thrust vector, roll and throttle channels. Each gains a first-order lag through gimbal_time_constant, roll_torque_time_constant and throttle_time_constant; both shipped scenarios leave them null, which keeps the previous pass-through behaviour. (#39, @zuorenchen)
  • The physics engine moves to the RocketPy v1.13 line. (#60, @zuorenchen)
  • Rocket sensors are seeded from the scenario seed, so a run with a fixed seed reproduces its sensor noise instead of drawing from the process-global generator. (#54, @thc1006)
  • Diagnostics go through the logging module rather than print. (#51, @thc1006)

Bug or Regression

  • Submissions can be packed again. pack_for_submission raised TypeError on a SeedSequence sensor seed, which broke packing for every competitor. (#67, @thc1006)
  • A network failure during the evaluate.py integrity check no longer throws away a submission that has already been simulated. (#68, @thc1006)
  • An agent that never sends a launch action now reaches the timeout and ends the episode instead of raising AttributeError, and the interval between launch and the first simulated sample takes part in pop detection rather than being skipped. (#65, @thc1006)
  • The evaluate.py integrity check no longer reports a mismatch for a file that differs only by a byte-order mark or by line endings. (#55, @thc1006)
  • The default matplotlib renderer no longer crashes on Matplotlib 3.11. Line3D.draw reads a shape off the coordinates whenever one is invalid for the axis scale, which every frame before launch is, and the renderer was handing it lists. (#90, @thc1006)
  • The command line prints Total reward: N again rather than INFO:__main__:Total reward: N, and the Colab notebook and run_for_evaluation print the score instead of nothing. (#88, @thc1006)
  • Lint and pytest failures in the test suite. (#40, @zuorenchen)

Testing and CI

  • Golden-master regression tests for scenarios 0 and 1, comparing the rocket and balloon trajectories against stored baselines so a change in the physics engine cannot move a score unnoticed. (#47, #49, @thc1006)
  • Unit tests for the pop-detection geometry. (#48, @thc1006)
  • Actuator dynamics tests with non-null time constants, and two test files that could skip silently now fail loudly instead. (#66, @thc1006)
  • CI checks that uv.lock is current against the ActiveRocketPy submodule, which is what later caught a stale lockfile on develop. (#62, @thc1006)
  • ruff is pinned so a release of it cannot turn a green branch red on its own. (#50, @thc1006)
  • uv.lock relocked against the submodule. (#46, @thc1006)
  • uv.lock relocked after the version bump, and CI now also runs on pushes to develop so a red integration branch cannot go unnoticed. (#78, @thc1006)

Known limitations

Stated here rather than discovered later. None of these are new in v0.1.0.

  • Thrust vector control applies its moments without the matching lateral force. The flight equations reduce axial thrust for a gimbal deflection and apply the resulting moments, but the lateral components of that same thrust vector do not reach the translational equations. At the shipped 15 degree limit on both axes that is a missing 0.366 * T, and two independent 15 degree axes reach a combined deflection of 21.47 degrees. Every entrant runs the same model, so this does not favour anyone, but v0.1.0 should not be described as a validated thrust-vector model. Tracked upstream as ActiveRocketPy#21.
  • Throttle scales thrust, not propellant flow. Mass properties come from the maximum flow rate and the motor cuts off at burn_time whatever the throttle did, so a throttle time constant lags the thrust without lagging the mass schedule. Documented in the README and unchanged here.
  • The control fields on a launch action are not applied to that step. The launch action creates the flight; tvc, roll and throttle are first read on the following step(). The flight is not advanced on the launch step either, so nothing is simulated with the dropped values, but the contract is worth stating explicitly.

Also in this release

Everything below landed on develop after these notes were first written. Each
one names the regression it makes visible, since every one was found by a
mutation the suite as shipped did not catch.

Changes what a competitor sees

  • #99 hands the agent copies. The observation arrays and the whitelisted parameter lists were the environment's own objects, so writing 2 into the status array and never launching scored the whole scenario, and an agent could widen its own throttle range or change the rocket's moment of inertia from its constructor.
  • #70 stops the integrity check reading a partial or proxy-rewritten response as evidence that the competitor edited evaluate.py.
  • #73, #74 make the submission write atomic and drop a redundant 165 MB field.
  • #96 removes the Monte Carlo workspace instead of leaving 169 MB per run in /tmp, and keeps it when a run fails so there is something to read.
  • #101 refuses a short Monte Carlo result set. The pinned RocketPy catches a keyboard interrupt and returns what it has, and exactly one returned trajectory broadcasts into a full hundred-balloon world in which every balloon flies the same path, scoring normally against a world that was never simulated.
  • #98 decides whether two swept segments are parallel by the angle between them rather than by how long they are. A millimetre-scale pair at ninety degrees was classified as parallel and reported 1.5005 m where the real distance is 1.4995 m, which against a 1.5 m radius is a pop reported as a miss. Measured over a scenario 1 run: 56 of 9155 evaluations took that branch, all genuinely degenerate, so no score moved.
  • #88, #90 are described under Urgent Upgrade Notes above.

Closes a hole in what the suite can see

  • #83 checks scenario 1's release timing. Deleting the ground-to-released update, releasing all hundred balloons on the first step, or an off-by-one on the release condition each left all seven golden-master tests green.
  • #100 pins when scenario 0 pops each balloon and how the episode ends. Replacing pop detection with "mark every balloon popped on the first flown step" passed every test before it, because the count, the trajectory and the duration are all unchanged by it.
  • #81 writes down the pop rule settled in #63 and covers the clamping branches.
  • #71 proves the scenario's actuator time constant is the one being used.
  • #85 bounds the 3D position error absolutely. The relative bound scaled with altitude above sea level, so it exceeded the balloon radius at 108 of scenario 0's 119 sampled points and reached 48.8 m at apogee.
  • #86 restores the per-balloon render assertions #40 deleted.
  • #84 writes baselines as strict JSON, atomically, without narrowing their permissions.
  • #82 stops a test import guard turning an installed but broken simulation stack into a skip.
  • #87 pins the coordinate frame the observation is reported in. A 500 m offset on the horizontal position, latitude and longitude in place of X and Y, and X swapped with Y all passed before.
  • #77 covers the submission path #67 fixed. Reverting Give sensors JSON-safe seeds so submissions can be packed again #67 left the suite green; with Cover the submission path #67 actually fixed, and pin the seed derivation #77 it fails 8 of 11.
  • #95 normalises the console level and pins the records production emits.
  • #69 hardens the sweep origin.
  • #102, #103 finish the truncation change across every runner, and narrow what the isolation tests claim to what they establish.
  • #104 finishes it everywhere else: the log line for running out of time still said "Terminated", a truncated episode drew no final frame unless its last step happened to land on the render cadence, and the README still described one termination flag and one way for an episode to end. The README snippet is not a .py file, so the invariant test could not see it; it reads the fenced blocks now.

Tooling

  • #97 adds scripts/verify_submission.py, which regenerates a submission's balloons from the shipped scenario and its seed and asks whether the claimed pops were anywhere the rocket says it went. It does not verify the rocket trajectory, which stays the competitor's claim.
  • #75 tests the Python 3.10 floor the package claims. #76 adds CONTRIBUTING and CHANGELOG. #79 groups these notes by kind automatically from labels, and adds CITATION.cff and SECURITY.md.

Still open, and therefore not in v0.1.0

  • #58 moves submissions off pickle, paired with the leaderboard side.

Open questions rather than pending work: #57 on submission size, and #35 on the integrity warning, which fires for any checkout that is not current main and needs a release-pinned reference rather than a moving branch to close properly.

Verified as a whole

Each of these was green on its own base, which says nothing about the merged
tree, so develop was checked out clean and run through the exact CI after the
last of them landed: ruff check, ruff format, uv lock --check, and 315 tests
with 190 subtests passing with the slow suite enabled.

Contributors

Thank you to everyone whose work is in this release: @zuorenchen, @thc1006, @chichunwang.

Full Changelog: v0.0.2...v0.1.0

chichunwang and others added 27 commits May 5, 2026 23:15
* Fix evaluate's encode problem

* Update yaml encoding
* MNT: README
* MNT: fix scenario 0 balloon position info. #8
* Skip Monte Carlo balloon simulation for scenario 0

scenario 0 uses static balloons at fixed heights, but reset() ran a
full Monte Carlo balloon-flight simulation every call and immediately
overwrote the result with static positions. Move the Monte Carlo path
into the non-scenario-0 branch and build the static balloon array
directly via a new __generate_static_balloon_flights helper. The
resulting _balloon_flights array is identical to before; only the
wasted Monte Carlo run and its file writes are removed.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* Add pytest to dev optional-dependencies

The scenario 0 test suite lives in tests/; declare pytest in the dev
extra so it installs via requirements-dev.txt and the suite can be run.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

---------

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
vpython was imported at module scope in balloon_world.py and listed as
a hard dependency, so importing the environment required vpython even
for matplotlib rendering or render_mode=None (e.g. Colab). Move the
import into the vpython branch of _render_frame and expose vpython via
a `vpython` optional extra. Default installs and the matplotlib
renderer are unaffected; `pip install -e ".[vpython]"` enables it.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* Add cleanup invariant tests

AST-based static checks for redundant file.close() after with-open,
dead class-level imports, drifted example agent_kwargs, and empty
f-strings. No new dependencies beyond the standard library and PyYAML.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* Remove dead code, redundant calls, and unused imports

- evaluate.py: drop three redundant file.close() after with-open
- example_agents.py: drop dead class-level get_initial_attitude
  import inside NavigationAgent (already imported at module scope)
- balloon_world.py: drop unused rocketpy.Function and vpython.arrow
  imports; drop f-prefix from two f-strings without interpolation
- example_eval_cfg.yaml: drop dummy arg3 unused by AttitudeRateControlAgent
- envs/__init__.py: use explicit "as" re-export to satisfy F401

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* Document how to run tests and ignore .ci-venv

Add a Testing section to README pointing at "python -m unittest
discover tests", and gitignore the throwaway venv name commonly
used to install ruff locally.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* Declare PyYAML as a dependency

evaluate.py and the cleanup tests import yaml, but PyYAML was never
declared in pyproject.toml dependencies and is not pulled in
transitively. A fresh `pip install -r requirements.txt` therefore
leaves `import yaml` failing with ModuleNotFoundError. Add pyyaml to
the project dependencies.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

---------

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Co-authored-by: ZuoRen Chen <180084773+zuorenchen@users.noreply.github.com>
* Move Monte Carlo output out of the package directory

__generate_balloon_flights passed MonteCarlo a filename inside the
installed package directory (BalloonPoppingGymEnv/envs/data/), so every
non-scenario-0 reset wrote balloon_sim.{inputs,outputs,errors}.txt
there. That fails in read-only / installed environments and pollutes
the package tree. Write to tempfile.gettempdir() instead; the env only
uses the in-memory simulate() results, so the persisted files are a
pure side effect whose location does not affect behavior.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* Use a per-process Monte Carlo output filename

tempfile.gettempdir() is shared across OS users, so a fixed "balloon_sim"
name there can collide: on a shared host the second user's reset() hits
a PermissionError writing the first user's files. Suffix the name with
the process id so concurrent processes and users get distinct paths.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

---------

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* Add uv as the recommended environment setup path

uv installs the pinned Python, creates the virtual environment, and
installs locked dependencies in one step, lowering the setup barrier
for contributors. Declare activerocketpy as a path dependency via
[tool.uv.sources] so uv resolves the submodule, pin Python to 3.14
via .python-version, and commit uv.lock. The requirements.txt / pip
path is unchanged and still works for users who do not use uv. Also
drop the duplicate vpython entry left in [project.dependencies] by the
PR #7 / #15 merge -- vpython is the optional extra only.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* Switch the README uv setup to the standalone installer

The uv steps said `pip install uv` then `uv sync`, but pip places the
uv executable in a scripts directory that is frequently not on PATH,
so the bare `uv` command then fails. Document uv's standalone
installer instead, which puts `uv` on PATH, and add a troubleshooting
note covering a stale shell, PATH, and the `python -m uv` fallback.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

---------

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Add GitHub issue form templates under .github/ISSUE_TEMPLATE: a bug
report, a feature request / proposal, and a question form, plus a
config that keeps blank issues enabled. Each form collects the
context that otherwise has to be asked for in follow-up comments.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* Enhance BalloonPoppingEnv with trajectory logging and tidy up attribute in _init_ and reset

* Add save_trajectory function to save balloon and rocket trajectories into json file. Call save_trajectories in evaluate_scenario. TODO: render_trajectory
* Enhance BalloonPoppingEnv with trajectory logging and tidy up attribute in _init_ and reset

* Add save_trajectory function to save balloon and rocket trajectories into json file. Call save_trajectories in evaluate_scenario. TODO: render_trajectory

* Add balloon_status to trajectories

* ENH: pack results for leaderboard submission

* Add time to trajectory log

* Save trajectory file in UTC
* Add gust model configuration to environment parameters and implement gust handling in BalloonPoppingEnv

(cherry picked from commit 9550377)

* Refactor quasi-random gust model

* Fix rng usage. run __create_environment() after env.reset

---------

Co-authored-by: RickyRicato <rickywang44@gmail.com>
* Render every balloon in the vpython renderer

_render_frame's vpython branch created a single sphere and
positioned it from _balloon_states[0], so a scenario with N
balloons showed only the first. Create one sphere per balloon and
update them all, which matches the matplotlib branch that already
slices over every balloon. Add tests/test_render.py, which mocks
vpython and checks that a reset creates one sphere per balloon.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* Assert the vpython renderer repositions every balloon

The render test checked that num spheres are created but not that
all of them are repositioned each frame, so a regression updating
only one sphere would still pass. Add a vector() call-count
assertion that pins the per-balloon update loop.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* ENH: Add rocket and balloon colors to vpython rendering

* Add vPython option to comment

---------

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Co-authored-by: zuorenchen <zuorenchen@m110.nthu.edu.tw>
* Add GitHub Actions CI workflow

Set up .github/workflows/ci.yml with a lint job (ruff check and
format check) and a test job that installs the project with its
ActiveRocketPy submodule, runs the suite with coverage, and uploads
a coverage report. CI runs on pushes to main, pull requests, and
manual dispatch. The Codecov upload step stays dormant until a
CODECOV_TOKEN secret is configured.

Add tests/test_import_smoke.py to confirm the package and example
agents import after an install, and declare pytest-cov as a dev
dependency for coverage generation.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* Bump CI actions off the deprecated Node 20 runtime

actions/checkout@v4, actions/setup-python@v5 and
actions/upload-artifact@v4 run on Node 20, which GitHub deprecates
in June 2026. Move them to the current majors (v6, v6, v7), which
run on Node 24.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

* Probe the simulation stack with find_spec in the smoke test

The skip guard used importlib.import_module, which executes rocketpy
and so cannot tell an installed-but-broken stack apart from an absent
one -- test_import_example_agents would skip the import breakage it
exists to catch. find_spec checks installation without importing, so
the test runs and fails loudly when an installed stack is broken.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

---------

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* Add leaderboard information

* Run formatter

* Add release notes

* Update release notes
* Update ActiveRocketPy submodule to add actuator classes

* Update names to align with ActiveRocketPy actuator class updates

* Bump ActiveRocketPy version after actuator module merged to main
* Fix lint error

* Remove pytest on vpython.vector calls
Bring uv.lock in line with pyproject.toml and the pinned Python 3.14:
- add the dev extra's pytest-cov (declared in pyproject but absent from the
  lock), which pulls in coverage
- re-resolve platform markers for Python 3.14 (e.g. contextily and its deps
  are constrained to python_full_version < '3.14')

Regenerated with `uv lock` (uv 0.11.14).
A new ruff release can change lint/format rules and break CI without any code
change. Pin ruff==0.15.20 in the lint job for reproducibility.
Convert the diagnostic print() calls in the environment step()/close() and
the evaluation entry point to module-level loggers, with no change to
computed values, reward, or control flow:

- balloon_world.py: add a module logger; the two step() termination prints
  become logger.info and the close() print becomes logger.debug.
- evaluate.py: add a module logger; the completion/total-reward prints become
  logger.info with lazy %-style args, and the CLI block calls
  logging.basicConfig(stream=sys.stdout) so console output is preserved.
- tests/test_logging.py: assert close() emits through the module logger and
  that both modules expose a correctly named logger.

evaluate_scenario keeps returning (env, agent, scenario_parameters).
* test: add unit tests for balloon pop-detection geometry

Cover the scoring core, which had no functional tests:
- _segment_distance_squared_batch: parallel, intersecting, degenerate
  (point) segments, colinear endpoint clamping, and batch output shape.
- _detect_pops: released-in-path pops, distant miss, ground/popped
  balloons skipped, inclusive radius boundary, and correct index
  mapping across mixed release states.

Pure-numpy helpers; guarded with skipUnless like the other runtime tests.

* test: cover moving-balloon pop detection (swept balloon segment)

The existing _detect_pops tests use static balloons (previous == current
position). Add cases where the balloon sweeps a segment during the timestep
(the scenario #1 wind-drift case), exercising the swept balloon path:

- a balloon whose sweep crosses the rocket path is popped even though both of
  its endpoints are 5 m from the path (a static check would miss it)
- a swept closest approach within the radius is popped
- a balloon that sweeps past outside the radius is not popped
…n check (#55)

* BUG: normalize line endings and drop BOM in the evaluate.py submission check

pack_for_submission compared the local evaluate.py against the copy on main by
hashing raw bytes, so a CRLF or BOM-prefixed working tree (common on Windows)
hashed differently from GitHub's LF-served file and warned that an unmodified
evaluate.py should not be modified. Decode with utf-8-sig and normalize line
endings before hashing, so CRLF, a BOM, and a trailing-newline difference no
longer false-positive while real modifications are still detected.

Addresses #35 (the reported Windows false-positive). The separate branch-skew
case (evaluate.py legitimately differs between main and develop, e.g. the #51
logging change) is not covered here and needs its own decision.

* Extract _normalized_md5 to module level and unit-test it

The evaluate.py integrity check in pack_for_submission was a nested closure, so
the BOM/line-ending normalization had no way to be tested on its own. Hoist it to
a module-level helper (no behavior change, it captured nothing from the enclosing
scope) and add tests/test_submission_md5.py.

The tests pin both halves of #35: BOM, CRLF, classic-Mac CR, and trailing-newline
variants all hash identically, so a Windows checkout no longer false-positives,
while real edits (changed return, extra whitespace, added line, renamed function)
still change the hash. Guarded with skipUnless like the other runtime tests, since
utils imports the rocketpy stack at module top.

* Normalize the evaluate.py checksum on raw bytes

str.splitlines() splits on a superset of the physical line endings Python's tokenizer recognizes (form feed, NEL, U+2028), so an LF-to-form-feed swap changed the program while hashing the same, and decoding as UTF-8 raised on a valid non-UTF-8 source (a file with a coding: declaration), turning the integrity warning into a crash.

Normalize on raw bytes instead: drop a UTF-8 BOM, fold CRLF and CR to LF, strip a single trailing newline. That keeps the BOM/CRLF false-report fix (#35) while catching a form-feed semantic edit and never crashing on non-UTF-8 source.

Tests now gate on find_spec for rocketpy, so a renamed or removed helper fails loudly instead of skipping, plus form-feed and non-UTF-8 regression cases.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

---------

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* Add golden-master regression test for scenario #1 (issue #38)

Mirror the scenario #0 regression guard for scenario #1. Scenario #1 runs a
100-flight Monte Carlo balloon ensemble through the ECMWF ensemble atmosphere
on reset, so the balloons drift; the test therefore compares the downsampled
balloon position trajectories in addition to the rocket trajectory and the
popped-balloon count.

The env reset+step loop is driven directly (not evaluate.py) to avoid the
save_trajectories file writes and the submission-MD5 network call. Two seeded
runs are bit-identical on the same machine; tolerances (rtol 3%, atol 1 m)
absorb cross-platform float drift only. Row-count guards catch launch-failure
or early-termination regressions that the overlap clip would otherwise mask.

The test is opt-in via BPC_RUN_SLOW_TESTS (~30 s per run) so it does not bloat
the default PR CI; the project has no pytest slow-marker infrastructure yet.
A small downsampled baseline is committed with a regenerate script for
deliberate physics updates.

* ci: run the slow scenario regression tests in CI

Set BPC_RUN_SLOW_TESTS=1 in the test job so the opt-in slow regression tests
(the scenario #1 Monte Carlo golden master) run in PR CI, while local default
runs stay fast.

* test: harden the scenario #1 golden-master against false negatives

Same false-negative paths as scenario #0, plus the balloon-specific ones:

- post_launch_rocket_positions dropped every NaN row, so a rocket trajectory that
  went NaN after launch was silently truncated. It now takes the trajectory from
  the first finite row and raises on any non-finite row after launch.
- Flight duration and the downsampled row counts used a 2% tolerance, which waved
  through a full second of early termination. Both are now absolute.
- The rocket and balloon comparisons used numpy's additive atol + rtol*expected;
  they now apply a real max(1 m, 3%) floor, which a NaN also fails.
- The balloon array was downsampled 1-in-10 in the balloon index, so 90 of the
  100 balloons were never compared. It now keeps every balloon (time downsampling
  only) and the baseline regrows accordingly.
- The class skipped on any ImportError. The skip now keys off whether rocketpy
  itself imports, the BalloonPoppingGymEnv imports fail loudly when the stack is
  present but broken, and with BPC_RUN_SLOW_TESTS set a missing stack fails
  rather than skips.

Also pin the expected popped count (0), read the real scenario seed in the
regenerator, handle a future truncated step, parse the slow-gate flag strictly,
and drop the stale "does not bloat CI" note now that CI runs it.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

---------

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* test: add golden-master regression for scenario #0 (issue #38)

Re-runs scenario #0 (deterministic: static balloons, fixed seed, ideal
sensors, no Monte Carlo) with the default agent and compares against a
committed baseline, guarding against unintended physics changes when
ActiveRocketPy is updated.

- Compares popped count (exact, == 10), rocket position trajectory
  (rtol 3%, atol 1 m), and flight duration (within 2%).
- Baseline is downsampled post-launch positions; regenerate deliberately
  via tests/baselines/regenerate_scenario_0.py and review the diff.
- Side-effect free runner; guarded with skipUnless like the other runtime tests.

* test: harden the scenario #0 golden-master against false negatives

Several ways the regression could stay green while the flight was actually broken:

- post_launch_positions dropped every NaN row, not just the pre-launch ones, so a
  trajectory that went NaN after launch was silently truncated and slipped past
  the comparison. It now takes the trajectory from the first finite row and
  raises on any non-finite row after launch.
- Flight duration and the downsampled row count used a 2% tolerance, which waved
  through a full second of early termination on a ~6000-step flight. Both are now
  absolute (a couple of steps, one row of cross-platform jitter).
- The position comparison used numpy's additive atol + rtol*expected, so a small
  coordinate could drift by a metre plus another few percent. It now applies a
  real max(1 m, 3%) floor.
- The class skipped on any ImportError, which hid the real breakage it exists to
  catch (a renamed or removed ActiveRocketPy symbol). The skip now keys off
  whether rocketpy itself imports; the BalloonPoppingGymEnv imports are left to
  fail loudly when the stack is present but broken.

Also pin the expected popped count (10) so regenerating the baseline cannot bless
a regression that pops fewer, handle a future truncated step, and read the real
scenario seed in the regenerator instead of hardcoding it.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

---------

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Thread the scenario seed into the gyroscope / accelerometer / GNSS sensors
via the new ActiveRocketPy sensor seed argument (#13). Each sensor gets a
decorrelated sub-stream from SeedSequence([np_random_seed, domain]).spawn(3),
clear of the balloon Monte Carlo seed tree, so the noisy-sensor observations
are reproducible from the announced scenario seed and independent of the
global RNG. Bumps the ActiveRocketPy submodule to develop (e9ebff65), which
carries the seed support; adds tests/test_sensor_determinism.py.

Closes #52.
thc1006 added 22 commits July 29, 2026 00:25
Make the actuator tests catch a wrong sampling rate and cover all four actuators
…er-assertions

Put back the half of the render test #40 deleted, as behaviour
test: pin the coordinate frame the observations are reported in
…-gaps

Cover the submission path #67 actually fixed, and pin the seed derivation
…nventions

Group the generated release notes by kind, and add CITATION.cff and SECURITY.md
Refuse a short balloon Monte Carlo instead of building a world out of it
…ncation

The third runner still waited on terminated alone
Say what the isolation tests establish, and make the walker check it
Add a script that checks a submission's balloons against its seed
…and-changelog

# Conflicts:
#	tests/baselines/regenerate_scenario_0.py
#	tests/baselines/regenerate_scenario_1.py
Add CONTRIBUTING.md and CHANGELOG.md, and make make format match CI
Two places that still read terminated from when that one flag also
covered the clock. Splitting the two causes was right; neither of these
moved with it.

The log said "Terminated: Reached max time" for the case the flags now
deliberately call truncation. That line is the one thing an operator
reads to find out why a run stopped, so it should not name the wrong
ending.

The renderer drew a frame every 0.1 s of simulation time or when the
episode ended, and the ending it checked was terminated alone. While
that covered the clock, an episode running out of horizon always drew
its last frame. Afterwards it only did so when the final step happened
to land on the cadence. Scenario 0 truncates at step 9999 against a
cadence of 10, so it did not, and scenario 1 ends this way normally.

The existing console test pinned the old wording, so it moves too.

Verified by mutation: both the old wording and the old render gate fail.
The frame test also asserts that the final step misses the cadence,
since otherwise the old gate would have drawn it anyway and the
assertion would hold either way.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Sweeping for everything that still read terminated from when that one
flag also covered the clock turned up two more than the code.

The README described step() as returning "the new observations, reward,
termination flag, and additional info", which is four values and one
flag, and said the episode ends when the time limit is reached or the
rocket lands, as though those were the same outcome. It is the document
a competitor writes their loop from, so it was the worst place for this
to be stale. It now names both flags, says which cause each one is, and
shows the loop.

The changelog had no entry at all. Added under Changed as breaking for
every agent loop, alongside the observation copies, and the four
behaviour fixes from this wave under Fixed.

The README snippet is not a .py file, so the invariant test could not
see it. It reads the fenced python blocks out of the README and puts any
loop it finds through the same guard and unpack checks as the runners,
plus a check that it found one at all, since a snippet that stops being
tagged python would otherwise leave the check holding over nothing.

Verified by mutation: the README showing the old loop, the README
discarding truncated, and the snippet ceasing to be python all fail.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…-frame

Finish the truncation change: the log, the final frame, the README and the changelog
@thc1006

thc1006 commented Jul 29, 2026

Copy link
Copy Markdown
Member

Your order works. One step to add, and one thing I measured that says the rest is safe.

TLDR:

1. ARP #19  -> ARP develop
2. ARP develop -> ARP master        <- the step BPC actually needs
3. BPC submodule bump to that master
4. BPC #61 merge, tag v0.1.0
5. BPC #58 + leaderboard move to JSON together, release v0.1.1

Step 2 is easy to miss. BPC pins ActiveRocketPy master (435f58d), and #19 targets develop, so merging it alone changes nothing here. I checked whether that step drags anything else in: the two trees are byte-identical today, git diff master develop is empty, and master is just one release merge ahead. So after #19 lands, master picks up the actuator change and nothing else.

On whether the bump moves anyone's numbers, I pointed a scratch worktree's submodule at the #19 branch and ran it:

  • both golden masters pass unchanged, 34 passed
  • full BPC suite 315 passed
  • uv lock --check still resolves, so the bump needs no lockfile change

That holds because both shipped scenarios leave gimbal_time_constant, roll_torque_time_constant and throttle_time_constant at null, so the branch #19 touches never runs.

On the cut-off: I would leave my six open PRs (#105 to #110) out of v0.1.0
and put them in v0.1.1. None of them changes a score, the golden masters are unchanged by all six together, and the submission checker they mostly touch is maintainer tooling that is not in the release. No reason to hold the tag for them.

Happy to run 3 and 4 once you have done 1 and 2, or to do the whole thing if you would rather. Step 2 is a release, so I would rather you called that one.

Every merge on `develop` since v0.0.2 now appears. One of them is a scoring
change and was the reason to check: #69 fixed the first interval after launch
being skipped for pops, because the sweep started from an all-NaN position.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

@thc1006 thc1006 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving so this can go in.

Everything in the notes matches what is on develop: I checked each #nnn in the merge commits between main and develop against the changelog rather than reading for it.

One thing to land first, #111. The changelog was missing twenty of those pull requests, including a scoring change (#69, the first interval after launch was not checked for pops). It is a one file docs change, green, and without it v0.1.0 ships with an incomplete record.

My six open PRs (#105 to #110) are deliberately not in this cut. None of them changes a score, and the golden masters are unchanged by all six together, so there is no reason to hold the tag for them. They can go in v0.1.1 alongside the ActiveRocketPy bump.

Credit the twenty pull requests the changelog had left out
@thc1006
thc1006 merged commit 69dfbda into main Jul 29, 2026
6 checks passed
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.

Running out of time is reported as terminated instead of truncated

3 participants