| |
| """Render a compact release report from one RSL-RL TensorBoard event file.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| from pathlib import Path |
|
|
| import matplotlib.pyplot as plt |
| import numpy as np |
| from tensorboard.backend.event_processing.event_accumulator import ( |
| EventAccumulator, |
| ) |
|
|
|
|
| TAGS = { |
| "reward": "Train/mean_reward", |
| "episode_length": "Train/mean_episode_length", |
| "velocity_error": "Metrics/base_velocity/error_vel_xy", |
| "bad_orientation": "Episode_Termination/bad_orientation", |
| "timeout": "Episode_Termination/time_out", |
| "value_loss": "Loss/value", |
| "surrogate_loss": "Loss/surrogate", |
| } |
|
|
|
|
| def load_series( |
| accumulator: EventAccumulator, |
| tag: str, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| events = accumulator.Scalars(tag) |
| steps = np.asarray([event.step for event in events]) |
| values = np.asarray([event.value for event in events]) |
| return steps, values |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser() |
| parser.add_argument("event_file", type=Path) |
| parser.add_argument("output", type=Path) |
| parser.add_argument("--title", default="Dropbear Stage 46") |
| args = parser.parse_args() |
|
|
| accumulator = EventAccumulator( |
| str(args.event_file), |
| size_guidance={"scalars": 0}, |
| ) |
| accumulator.Reload() |
| available = set(accumulator.Tags()["scalars"]) |
| missing = [tag for tag in TAGS.values() if tag not in available] |
| if missing: |
| raise RuntimeError(f"Missing TensorBoard tags: {missing}") |
| data = { |
| name: load_series(accumulator, tag) |
| for name, tag in TAGS.items() |
| } |
|
|
| plt.style.use("dark_background") |
| fig, axes = plt.subplots(2, 2, figsize=(13, 8), sharex=True) |
| fig.patch.set_facecolor("#07111f") |
| for axis in axes.flat: |
| axis.set_facecolor("#0c1727") |
| axis.grid(color="#334155", alpha=0.28) |
| axis.spines[["top", "right"]].set_visible(False) |
|
|
| axes[0, 0].plot(*data["reward"], color="#5eead4", linewidth=2) |
| axes[0, 0].set_title("Mean episode reward") |
| axes[0, 0].set_ylabel("reward") |
|
|
| axes[0, 1].plot( |
| *data["episode_length"], |
| color="#60a5fa", |
| linewidth=2, |
| label="episode length", |
| ) |
| axes[0, 1].axhline( |
| 1000, |
| color="#a7f3d0", |
| linestyle=":", |
| linewidth=1, |
| label="full horizon", |
| ) |
| axes[0, 1].set_title("Survival recovery") |
| axes[0, 1].set_ylabel("steps") |
| axes[0, 1].legend(frameon=False, fontsize=8) |
|
|
| axes[1, 0].plot( |
| *data["velocity_error"], |
| color="#fbbf24", |
| linewidth=2, |
| ) |
| axes[1, 0].set_title("Planar velocity tracking error") |
| axes[1, 0].set_ylabel("m/s") |
| axes[1, 0].set_xlabel("checkpoint iteration") |
|
|
| axes[1, 1].plot( |
| *data["bad_orientation"], |
| color="#fb7185", |
| linewidth=2, |
| label="bad orientation", |
| ) |
| axes[1, 1].plot( |
| *data["timeout"], |
| color="#4ade80", |
| linewidth=2, |
| label="time-out", |
| ) |
| axes[1, 1].set_title("Episode completion") |
| axes[1, 1].set_ylabel("fraction") |
| axes[1, 1].set_xlabel("checkpoint iteration") |
| axes[1, 1].legend(frameon=False, fontsize=8) |
|
|
| start = int(data["reward"][0][0]) |
| end = int(data["reward"][0][-1]) |
| fig.suptitle( |
| f"{args.title} · iterations {start}–{end}", |
| x=0.065, |
| ha="left", |
| fontsize=17, |
| fontweight="bold", |
| ) |
| fig.text( |
| 0.985, |
| 0.015, |
| "RSL-RL PPO continuation · 512 parallel environments", |
| ha="right", |
| color="#94a3b8", |
| fontsize=8, |
| ) |
| fig.tight_layout(rect=(0.04, 0.04, 0.99, 0.93)) |
| args.output.parent.mkdir(parents=True, exist_ok=True) |
| fig.savefig(args.output, dpi=180, facecolor=fig.get_facecolor()) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|