Skip to content

Commit 4e42c4b

Browse files
committed
test: characterization tests for Flight.step_simulation()
Verify the stepped-simulation API (run_simulation=False plus repeated step_simulation() calls) reproduces a one-shot simulate(): - initial step state is unfinished at the first phase - stepping visits multiple phases and reaches the finished state - the stepped trajectory (final t and the full solution array) matches simulate() to a tight tolerance, robust to LSODA last-bit noise across platforms - post_process_simulation / initialize_prints_plots fire on finish (t_final, prints, plots present) - stepping after finished is a no-op Uses the parachute-free calisto flight (parachute triggers are not migrated into the stepping path); the twin's launch parameters are read back from the reference so it cannot drift. Scope: uncontrolled stepping only.
1 parent a6dda6a commit 4e42c4b

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Characterization tests for ``Flight.step_simulation()``.
2+
3+
The fork's stepped-simulation API (``run_simulation=False`` plus repeated
4+
``step_simulation()`` calls) must reproduce a one-shot ``simulate()`` so that
5+
callers driving the flight one node at a time -- e.g. the Balloon Popping
6+
Challenge environment, which steps it every timestep -- obtain the same
7+
trajectory. A parachute-free rocket (``flight_calisto``) is used because
8+
parachute triggers are not yet migrated into the stepping path.
9+
10+
Scope: this guards that *uncontrolled* stepping matches ``simulate()``. Stepping
11+
with live controller/actuator updates between nodes is a separate concern and is
12+
not covered here.
13+
"""
14+
15+
import numpy as np
16+
17+
from rocketpy import Flight
18+
19+
20+
def _stepped_twin(reference_flight):
21+
"""A non-simulated ``Flight`` twin of ``reference_flight``, to step by hand.
22+
23+
Launch parameters are read back from the reference so the twin cannot drift
24+
from it.
25+
"""
26+
return Flight(
27+
environment=reference_flight.env,
28+
rocket=reference_flight.rocket,
29+
rail_length=reference_flight.rail_length,
30+
inclination=reference_flight.inclination,
31+
heading=reference_flight.heading,
32+
terminate_on_apogee=reference_flight.terminate_on_apogee,
33+
run_simulation=False,
34+
)
35+
36+
37+
def _run_stepped(flight, max_steps=100000):
38+
"""Drive ``step_simulation`` to completion.
39+
40+
Returns the number of calls made and the set of phase indices visited.
41+
"""
42+
steps = 0
43+
phases_seen = {flight._step_state["phase_index"]}
44+
while not flight._step_state["finished"]:
45+
flight.step_simulation()
46+
phases_seen.add(flight._step_state["phase_index"])
47+
steps += 1
48+
assert steps < max_steps, "stepped simulation did not terminate"
49+
return steps, phases_seen
50+
51+
52+
class TestStepSimulation:
53+
"""Stepping must match a one-shot ``simulate()`` and finalise correctly."""
54+
55+
def test_initial_state_is_unfinished_at_first_phase(self, flight_calisto):
56+
stepped = _stepped_twin(flight_calisto)
57+
assert stepped._step_state["finished"] is False
58+
assert stepped._step_state["phase_index"] == 0
59+
assert stepped._step_state["node_index"] == 0
60+
61+
def test_stepping_visits_multiple_phases_then_finishes(self, flight_calisto):
62+
stepped = _stepped_twin(flight_calisto)
63+
_, phases_seen = _run_stepped(stepped)
64+
assert stepped._step_state["finished"] is True
65+
assert len(phases_seen) > 1 # at least a rail phase and a flight phase
66+
67+
def test_stepped_trajectory_matches_simulate(self, flight_calisto):
68+
stepped = _stepped_twin(flight_calisto)
69+
_run_stepped(stepped)
70+
# Stepping replays the same solver nodes as simulate(). A tight tolerance
71+
# (rather than exact equality) keeps the guard robust to last-bit noise in
72+
# the LSODA Fortran solver across platforms, while still catching any real
73+
# trajectory divergence.
74+
np.testing.assert_allclose(stepped.t, flight_calisto.t, rtol=1e-8, atol=1e-10)
75+
np.testing.assert_allclose(
76+
np.array(stepped.solution),
77+
np.array(flight_calisto.solution),
78+
rtol=1e-8,
79+
atol=1e-10,
80+
)
81+
82+
def test_post_process_artifacts_exist_after_stepping(self, flight_calisto):
83+
stepped = _stepped_twin(flight_calisto)
84+
_run_stepped(stepped)
85+
# post_process_simulation() and initialize_prints_plots() fire on finish.
86+
assert stepped.t_final == stepped.t
87+
assert stepped.prints is not None
88+
assert stepped.plots is not None
89+
90+
def test_stepping_after_finished_is_a_noop(self, flight_calisto):
91+
stepped = _stepped_twin(flight_calisto)
92+
_run_stepped(stepped)
93+
t_final, y_final = stepped.t, np.array(stepped.y_sol)
94+
stepped.step_simulation() # already finished -> must return immediately
95+
assert stepped.t == t_final
96+
np.testing.assert_array_equal(stepped.y_sol, y_final)

0 commit comments

Comments
 (0)