Download training/scripts/watch_gait.py from kenpeter123/twoleg: direct link, hf CLI and curl.
- Browser
- Download file 4.5 kB
-
https://huggingface.co/kenpeter123/twoleg/resolve/main/training/scripts/watch_gait.py
- Command line
-
hf download hf://kenpeter123/twoleg/training/scripts/watch_gait.py
-
curl -L -o watch_gait.py https://huggingface.co/kenpeter123/twoleg/resolve/main/training/scripts/watch_gait.py
4.5 kB
| """Live gait progress for the newest (or a named) TwoLeg training run. | |
| Reads the run's TensorBoard event file and prints the terms that decide whether | |
| the policy walks, not the terms that go up for free. Refreshes until Ctrl-C. | |
| uv run python scripts/watch_gait.py | |
| uv run python scripts/watch_gait.py --run 2026-09-28_21-53-46_velocity | |
| uv run python scripts/watch_gait.py --watch-videos | |
| Targets, from microduck_rl's own velocity run at iteration 2499: | |
| track_linear_velocity 1.184, air_time 0.837, error_vel_xy 0.440, upright 1.621. | |
| The pre-alignment TwoLeg run plateaued at error_vel_xy 0.46 with no air_time | |
| term at all, which is the failure this script exists to make visible. | |
| """ | |
| import argparse | |
| import glob | |
| import os | |
| import time | |
| from tensorboard.backend.event_processing import event_accumulator | |
| LOG_ROOT = os.path.join( | |
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))), | |
| "logs", | |
| "rsl_rl", | |
| "velocity", | |
| ) | |
| TERMS = ( | |
| ("Episode_Reward/track_linear_velocity", "1.0"), | |
| ("Episode_Reward/air_time", "0.5"), | |
| ("Episode_Reward/upright", "1.5"), | |
| ("Episode_Reward/pose", "0.5"), | |
| ("Episode_Reward/foot_clearance", "< 0.05"), | |
| ("Episode_Reward/foot_slip", "< 0.05"), | |
| ("Metrics/twist/error_vel_xy", "< 0.40"), | |
| ("Metrics/air_time_mean", "> 0.05"), | |
| ("Metrics/slip_velocity_mean", "< 0.05"), | |
| ("Episode_Termination/fell_over", "< 5"), | |
| ("Episode_Termination/nan_state", "0"), | |
| ("Train/mean_episode_length", "> 800"), | |
| ("Train/mean_reward", "-"), | |
| ) | |
| def newest_run(root): | |
| runs = sorted( | |
| (d for d in glob.glob(os.path.join(root, "*")) if os.path.isdir(d)), | |
| key=os.path.getmtime, | |
| ) | |
| if not runs: | |
| raise SystemExit(f"no runs under {root}") | |
| return runs[-1] | |
| def event_file(run): | |
| found = glob.glob(os.path.join(run, "events.out.tfevents.*")) | |
| if not found: | |
| raise SystemExit(f"no event file in {run}") | |
| return found[0] | |
| def tail_of(path): | |
| ea = event_accumulator.EventAccumulator(path, size_guidance={"scalars": 0}) | |
| ea.Reload() | |
| tags = set(ea.Tags()["scalars"]) | |
| out = {} | |
| for tag, target in TERMS: | |
| if tag not in tags: | |
| out[tag] = (None, target) | |
| continue | |
| series = ea.Scalars(tag) | |
| values = [s.value for s in series] | |
| window = values[-max(1, len(values) // 20) :] or values[-1:] | |
| out[tag] = (sum(window) / len(window), target) | |
| return out, (series[-1].step if series else 0) | |
| def satisfied(value, target): | |
| if value is None or target == "-": | |
| return None | |
| if target.startswith("<"): | |
| return value < float(target[1:]) | |
| if target.startswith(">"): | |
| return value > float(target[1:]) | |
| return value >= float(target) | |
| def render(run, rows, step): | |
| print(f"\n{os.path.basename(run.rstrip('/'))} iteration {step}") | |
| print(f"{'term':40s} {'last 5%':>10s} {'target':>9s}") | |
| print("-" * 63) | |
| for tag, target in TERMS: | |
| value, tgt = rows[tag] | |
| shown = "absent" if value is None else f"{value:10.4g}" | |
| flag = "" | |
| if value is not None and tgt != "-": | |
| flag = "ok " if satisfied(value, tgt) else " -" | |
| print(f"{tag:40s} {shown:>10s} {tgt:>9s} {flag}") | |
| def watch_videos(run): | |
| videos = sorted( | |
| glob.glob(os.path.join(run, "videos", "**", "*.mp4"), recursive=True) | |
| ) | |
| if not videos: | |
| print("no videos rendered yet") | |
| return | |
| latest = videos[-1] | |
| print(f"latest training video: {latest}") | |
| for path in videos[-5:]: | |
| print(f" {os.path.relpath(path, run)}") | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--run", help="run directory name under logs/rsl_rl/velocity") | |
| parser.add_argument("--interval", type=float, default=20.0) | |
| parser.add_argument("--once", action="store_true") | |
| parser.add_argument("--watch-videos", action="store_true") | |
| args = parser.parse_args() | |
| run = ( | |
| os.path.join(LOG_ROOT, args.run) | |
| if args.run | |
| else newest_run(LOG_ROOT) | |
| ) | |
| print(f"watching {run}") | |
| while True: | |
| try: | |
| rows, step = tail_of(event_file(run)) | |
| render(run, rows, step) | |
| if args.watch_videos: | |
| watch_videos(run) | |
| except Exception as exc: # the run may still be writing | |
| print(f"waiting for scalars: {exc}") | |
| if args.once: | |
| return | |
| time.sleep(args.interval) | |
| if __name__ == "__main__": | |
| main() | |