fix(training): always emit flight-telemetry info keys, not just when nonzero

VecMonitor's info_keywords does a bare info[key] lookup on a completed
episode's terminal info dict and raises KeyError -- crashing the whole
training run -- if a key is ever absent. get_info() was only including
airborne_fraction/mean_altitude/vertical_thrust_mean when _telemetry_ticks
was nonzero and air_touch_fraction when _touches was nonzero; an episode
with zero ball touches (common, especially early in training) crashed on
the very first rollout in a smoke-test run. All four now always default to
0.0 rather than being conditionally present.

Found via TRAINING.md's Generation 4 validation ladder (rung 2, a 60k-step
smoke run) -- confirmed fixed by rerunning the same smoke run clean, then
export/evaluate parity (rungs 2-3) against Game/bots/promoted/easy.json.
This commit is contained in:
Josh Creek
2026-08-04 23:35:17 +01:00
parent 0e046aa9f1
commit cab2fa6391
+12 -6
View File
@@ -180,16 +180,22 @@ func get_reward() -> float:
# TRAINING.md). Flight telemetry fields are leading indicators for
# generation 4's core hypothesis (see train.py's FlightTelemetryCallback).
func get_info() -> Dictionary:
# The four telemetry keys must ALWAYS be present (not just when their
# denominator is nonzero) — VecMonitor's info_keywords does a bare
# info[key] lookup on whatever info dict is attached to a completed
# episode's terminal step (see train.py's VecMonitor(...,
# info_keywords=(...))) and raises KeyError, crashing the whole training
# run, if a key is ever missing. 0.0 is a reasonable default for "no
# touches/no ticks yet" (in practice _telemetry_ticks is >0 by the time
# any episode ends; _touches often legitimately is 0).
var info := {"goal_scored": goal_scored_this_episode}
if truncated_this_episode:
info["truncated"] = true
info["terminal_obs"] = terminal_obs
if _telemetry_ticks > 0:
info["airborne_fraction"] = float(_airborne_ticks) / _telemetry_ticks
info["mean_altitude"] = _altitude_sum / _telemetry_ticks
info["vertical_thrust_mean"] = _thrust_y_sum / _telemetry_ticks
if _touches > 0:
info["air_touch_fraction"] = float(_air_touches) / _touches
info["airborne_fraction"] = float(_airborne_ticks) / _telemetry_ticks if _telemetry_ticks > 0 else 0.0
info["mean_altitude"] = _altitude_sum / _telemetry_ticks if _telemetry_ticks > 0 else 0.0
info["vertical_thrust_mean"] = _thrust_y_sum / _telemetry_ticks if _telemetry_ticks > 0 else 0.0
info["air_touch_fraction"] = float(_air_touches) / _touches if _touches > 0 else 0.0
return info