dropbear-locomotion / scripts /analyze_convergence.py
cudabenchmarktest's picture
Release Dropbear locomotion v0.1.0 (model_8745)
de3e3f2 verified
Raw
History Blame Contribute Delete
4.12 kB
#!/usr/bin/env python3
"""Analyze sustained Dropbear learning trends from TensorBoard events."""
from __future__ import annotations
import argparse
from pathlib import Path
import numpy as np
from tensorboard.backend.event_processing.event_accumulator import EventAccumulator
WORKSPACE_ROOT = Path(__file__).resolve().parents[1]
LOG_ROOT = WORKSPACE_ROOT / "logs" / "rsl_rl" / "dropbear_velocity"
TAGS = {
"reward": "Train/mean_reward",
"ep_len": "Train/mean_episode_length",
"lin_track": "Episode_Reward/track_lin_vel_xy",
"vel_err": "Metrics/base_velocity/error_vel_xy",
"gait": "Episode_Reward/gait",
"cmd_lvl": "Curriculum/lin_vel_cmd_levels",
"act_std": "Policy/mean_std",
"bad_orient": "Episode_Termination/bad_orientation",
"fps": "Perf/total_fps",
}
def scalar_series(accumulator: EventAccumulator, tag: str) -> tuple[np.ndarray, np.ndarray]:
events = accumulator.Scalars(tag)
steps = np.asarray([event.step for event in events], dtype=np.float64)
values = np.asarray([event.value for event in events], dtype=np.float64)
return steps, values
def summarize(values: np.ndarray, steps: np.ndarray, window: int) -> tuple[float, float, float]:
count = min(window, values.size)
recent_values = values[-count:]
recent_steps = steps[-count:]
mean = float(np.mean(recent_values))
std = float(np.std(recent_values))
if count < 2 or np.ptp(recent_steps) == 0:
slope_per_100 = 0.0
else:
slope_per_100 = float(np.polyfit(recent_steps, recent_values, 1)[0] * 100.0)
return mean, std, slope_per_100
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("run", help="Run directory or its basename")
parser.add_argument("--window", type=int, default=25, help="Recent iterations to summarize")
args = parser.parse_args()
run_dir = Path(args.run)
if not run_dir.is_dir():
run_dir = LOG_ROOT / args.run
event_files = sorted(run_dir.glob("events.out*"), key=lambda path: path.stat().st_mtime)
if not event_files:
raise SystemExit(f"No TensorBoard event file in {run_dir}")
accumulator = EventAccumulator(str(event_files[-1]), size_guidance={"scalars": 0})
accumulator.Reload()
scalar_tags = set(accumulator.Tags().get("scalars", []))
print(f"run: {run_dir.name}")
print(f"window: last {args.window} iterations")
print("metric latest mean std slope/100")
print("----------- -------- -------- -------- ---------")
history = 0
summaries: dict[str, tuple[float, float, float, float]] = {}
for label, tag in TAGS.items():
if tag not in scalar_tags:
continue
steps, values = scalar_series(accumulator, tag)
history = max(history, values.size)
mean, std, slope = summarize(values, steps, args.window)
latest = float(values[-1])
summaries[label] = (latest, mean, std, slope)
print(f"{label:11} {latest:8.3f} {mean:8.3f} {std:8.3f} {slope:9.3f}")
print(f"\nhistory: {history} logged iterations")
if history < max(100, args.window * 4):
print("assessment: EARLY — insufficient history for a convergence claim")
return
command = summaries.get("cmd_lvl", (0.0, 0.0, 0.0, 0.0))[1]
tracking = summaries.get("lin_track", (0.0, 0.0, 0.0, 0.0))[1]
episode_length = summaries.get("ep_len", (0.0, 0.0, 0.0, 0.0))[1]
bad_orientation = summaries.get("bad_orient", (1.0, 1.0, 0.0, 0.0))[1]
reward_slope = summaries.get("reward", (0.0, 0.0, 0.0, 1.0))[3]
gates = {
"full command curriculum": command >= 1.0,
"linear tracking >= 0.65": tracking >= 0.65,
"episode length >= 700": episode_length >= 700.0,
"bad orientation <= 0.30": bad_orientation <= 0.30,
"reward plateau |slope/100| <= 1": abs(reward_slope) <= 1.0,
}
for name, passed in gates.items():
print(f"{'PASS' if passed else 'WAIT'}: {name}")
print(f"assessment: {'CONVERGED CANDIDATE' if all(gates.values()) else 'LEARNING'}")
if __name__ == "__main__":
main()