Buckets:
| #!/usr/bin/env python | |
| """Plot the manipulated object's world-frame motion from exported trajectories. | |
| python scripts/plot_trajectories.py --episode <uuid> | |
| Reads the ``*_trajectory.npz`` files written by the pipeline and renders three | |
| panels that answer "how is the object moving": | |
| A. path over the workbench (world X-Y, viewed from above) | |
| B. height above the robot base (world Z) against time | |
| C. speed against time | |
| Series are capped at three clips. That is a colour constraint, not an arbitrary | |
| one: the reference categorical palette's first three slots are the set validated | |
| for all-pairs separation under colour-vision deficiency, and a fourth slot would | |
| put yellow beside orange and fail that floor. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| from pathlib import Path | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt # noqa: E402 | |
| import numpy as np # noqa: E402 | |
| REPO_ROOT = Path(__file__).resolve().parents[1] | |
| # Reference categorical palette, light mode, slots 1-3 (blue / orange / aqua). | |
| SERIES = ("#2a78d6", "#eb6834", "#1baf7a") | |
| SURFACE = "#fcfcfb" | |
| INK = "#0b0b0b" | |
| INK_SECONDARY = "#52514e" | |
| GRID = "#e4e3df" | |
| MAX_SERIES = 3 | |
| def load(path: Path) -> dict: | |
| with np.load(path, allow_pickle=False) as z: | |
| return {k: z[k] for k in z.files} | |
| def style_axes(ax: plt.Axes, title: str, xlabel: str, ylabel: str) -> None: | |
| """Recessive grid and axes; the data is the only assertive thing on screen.""" | |
| ax.set_facecolor(SURFACE) | |
| ax.set_title(title, color=INK, fontsize=11, loc="left", pad=10) | |
| ax.set_xlabel(xlabel, color=INK_SECONDARY, fontsize=9) | |
| ax.set_ylabel(ylabel, color=INK_SECONDARY, fontsize=9) | |
| ax.grid(True, color=GRID, linewidth=0.8, zorder=0) | |
| ax.set_axisbelow(True) | |
| for side in ("top", "right"): | |
| ax.spines[side].set_visible(False) | |
| for side in ("left", "bottom"): | |
| ax.spines[side].set_color(GRID) | |
| ax.tick_params(colors=INK_SECONDARY, labelsize=8, length=0) | |
| def main() -> int: | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument("--episode", required=True, help="episode uuid") | |
| ap.add_argument("--outputs", type=Path, default=REPO_ROOT / "outputs") | |
| ap.add_argument("--out", type=Path, default=None) | |
| args = ap.parse_args() | |
| ep_dir = args.outputs / args.episode | |
| files = sorted(ep_dir.glob("*_trajectory.npz")) | |
| if not files: | |
| raise SystemExit(f"no trajectory files in {ep_dir}") | |
| runs = [] | |
| for f in files: | |
| d = load(f) | |
| speed = d["object_speed_mps"] | |
| if not np.isfinite(speed).any(): | |
| continue | |
| d["_clip"] = f.stem.split("_")[1] | |
| d["_peak"] = float(np.nanmax(speed)) | |
| runs.append(d) | |
| if not runs: | |
| raise SystemExit("no clip produced a finite speed") | |
| # Show the clips where the object actually moved -- a panel of flat lines | |
| # tells the reader nothing. | |
| runs.sort(key=lambda d: d["_peak"], reverse=True) | |
| shown, dropped = runs[:MAX_SERIES], runs[MAX_SERIES:] | |
| task = str(runs[0]["task"]) | |
| fig, axes = plt.subplots(1, 3, figsize=(15, 4.6), facecolor=SURFACE) | |
| fig.suptitle( | |
| f"Object motion in the world (robot-base) frame\n{task}", | |
| color=INK, | |
| fontsize=13, | |
| x=0.011, | |
| ha="left", | |
| y=1.06, | |
| ) | |
| for i, d in enumerate(shown): | |
| color = SERIES[i] | |
| c = d["centroid_xyz_world_m"] | |
| t = d["timestamps_s"] | |
| speed = d["object_speed_mps"] | |
| ok = np.isfinite(c).all(axis=1) | |
| label = f"clip {d['_clip']}" | |
| axes[0].plot(c[ok, 0], c[ok, 1], color=color, lw=2.0, zorder=3, label=label) | |
| # Start hollow, end filled: direction is readable without an arrow legend. | |
| axes[0].plot( | |
| c[ok, 0][0], c[ok, 1][0], "o", ms=8, mfc=SURFACE, mec=color, mew=2, zorder=4 | |
| ) | |
| axes[0].plot(c[ok, 0][-1], c[ok, 1][-1], "o", ms=8, color=color, zorder=4) | |
| axes[1].plot(t[ok], c[ok, 2], color=color, lw=2.0, zorder=3, label=label) | |
| fin = np.isfinite(speed) | |
| axes[2].plot(t[fin], speed[fin], color=color, lw=2.0, zorder=3, label=label) | |
| style_axes(axes[0], "A · Path from above", "world X (m)", "world Y (m)") | |
| axes[0].set_aspect("equal", adjustable="datalim") | |
| style_axes(axes[1], "B · Height", "time (s)", "world Z (m)") | |
| style_axes(axes[2], "C · Speed", "time (s)", "speed (m/s)") | |
| axes[0].legend( | |
| frameon=False, fontsize=8, labelcolor=INK_SECONDARY, loc="best" | |
| ) | |
| note = "hollow = start, filled = end" | |
| if dropped: | |
| note += f" · {len(dropped)} slower clip(s) omitted: " + ", ".join( | |
| f"{d['_clip']} ({d['_peak']:.3f} m/s)" for d in dropped | |
| ) | |
| fig.text(0.011, -0.06, note, color=INK_SECONDARY, fontsize=8, ha="left") | |
| out = args.out or (args.outputs / f"{args.episode}_motion.png") | |
| fig.savefig(out, dpi=150, bbox_inches="tight", facecolor=SURFACE) | |
| print(f"wrote {out}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |
Xet Storage Details
- Size:
- 5.03 kB
- Xet hash:
- 72a4add8182506dda7ed2969d286b5f011cb10ad5fc269073326563f7de30c7b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.