Flight.step_simulation() can return without advancing t, and a caller driving its own clock has no way to tell.
Two of its exits do bookkeeping and return rather than integrating:
flight.py:926, return # Move to next phase on next call, when the current phase has no time nodes left;
flight.py:884, which sets finished and returns once the phases run out.
Both are reasonable internally. The problem is for a caller stepping the flight alongside something else: it has no signal that the call was a no-op, so it advances its own clock while flight.t stands still, and the two drift apart.
What I measured
BalloonPoppingChallenge steps the flight once per environment step and advances its balloon positions on its own counter, so it is a direct case of this. Scenario 0 with the shipped example agent, 0.01 s steps:
|
|
| environment steps |
6012 |
calls that returned without advancing t |
2 |
| where |
steps 6011 and 6012, the last two |
final world_time - flight.t |
0.0232 s, or 2.3 steps |
| popped count |
10, matching the stored baseline |
The important part is the second row against the third: nothing stalled mid-flight. The out-of-rail transition, which I expected to consume a call, did not. Both stalls landed at the end, after the flight had already reached the ground, so the collision sweeps that decide the score were correctly aligned throughout. A run with a rocket that never leaves the pad shows the same shape, 2 stalls out of 43 steps, both at the end.
So this is not currently costing anyone a score. I am raising it as a contract question rather than a bug report.
The check
while not flight._step_state["finished"]:
previous_t = flight.t
flight.step_simulation()
assert flight._step_state["finished"] or flight.t > previous_t
Two ways to close it
Document that a call may return without advancing time, so callers know they have to read flight.t rather than count their own steps. That costs nothing and puts the burden where the caller can act on it.
Or have step_simulation() loop until it has either advanced or finished, which makes one call mean one step of simulated time and leaves existing callers correct without changes. This is the stronger guarantee but it changes how much work a single call can do.
I am happy to send either one. The first is a docstring, the second is a small loop plus a test that no call is a no-op unless the flight is finished. My preference is the second, since the whole point of a stepping API is that the caller drives the clock, but it is your call which contract you want to commit to.
Flight.step_simulation()can return without advancingt, and a caller driving its own clock has no way to tell.Two of its exits do bookkeeping and return rather than integrating:
flight.py:926,return # Move to next phase on next call, when the current phase has no time nodes left;flight.py:884, which setsfinishedand returns once the phases run out.Both are reasonable internally. The problem is for a caller stepping the flight alongside something else: it has no signal that the call was a no-op, so it advances its own clock while
flight.tstands still, and the two drift apart.What I measured
BalloonPoppingChallenge steps the flight once per environment step and advances its balloon positions on its own counter, so it is a direct case of this. Scenario 0 with the shipped example agent, 0.01 s steps:
tworld_time - flight.tThe important part is the second row against the third: nothing stalled mid-flight. The out-of-rail transition, which I expected to consume a call, did not. Both stalls landed at the end, after the flight had already reached the ground, so the collision sweeps that decide the score were correctly aligned throughout. A run with a rocket that never leaves the pad shows the same shape, 2 stalls out of 43 steps, both at the end.
So this is not currently costing anyone a score. I am raising it as a contract question rather than a bug report.
The check
Two ways to close it
Document that a call may return without advancing time, so callers know they have to read
flight.trather than count their own steps. That costs nothing and puts the burden where the caller can act on it.Or have
step_simulation()loop until it has either advanced or finished, which makes one call mean one step of simulated time and leaves existing callers correct without changes. This is the stronger guarantee but it changes how much work a single call can do.I am happy to send either one. The first is a docstring, the second is a small loop plus a test that no call is a no-op unless the flight is finished. My preference is the second, since the whole point of a stepping API is that the caller drives the clock, but it is your call which contract you want to commit to.