| """viz.py — matplotlib rendering for the Clutch demo (headless / Agg backend).""" |
|
|
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| from matplotlib.figure import Figure |
| from matplotlib.backends.backend_agg import FigureCanvasAgg |
| import numpy as np |
| import imageio.v2 as imageio |
| import tempfile, os |
|
|
| from nav import W, H, A, B |
|
|
| HABIT = "#2e9e5b" |
| COG = "#d64545" |
| INK = "#1b1b1b" |
| GRID_BG = "#f4f1ea" |
|
|
|
|
| def _render_nav_frame(walls, pos, mode, cum_expanded, target, step, plan_calls): |
| fig = Figure(figsize=(4.2, 4.2), dpi=96) |
| ax = fig.add_subplot(111) |
| ax.set_facecolor(GRID_BG) |
| img = np.ones((H, W, 3)) |
| img[walls] = [0.12, 0.12, 0.14] |
| ax.imshow(img, origin="lower", interpolation="nearest") |
| ax.scatter([A[0]], [A[1]], s=60, marker="s", c="#3a6ea5", zorder=3) |
| ax.scatter([B[0]], [B[1]], s=60, marker="s", c="#3a6ea5", zorder=3) |
| tcol = "#e8a33d" |
| ax.scatter([target[0]], [target[1]], s=180, marker="*", c=tcol, zorder=4, |
| edgecolors=INK, linewidths=0.5) |
| col = HABIT if mode == "HABITUAL" else COG |
| ax.scatter([pos[0]], [pos[1]], s=90, c=col, zorder=5, |
| edgecolors=INK, linewidths=0.6) |
| ax.set_xlim(-1, W); ax.set_ylim(-1, H) |
| ax.set_xticks([]); ax.set_yticks([]) |
| label = "REPLAN (expensive)" if mode == "COGNITIVE" else "cached step (cheap)" |
| ax.set_title(f"step {step} {label}", fontsize=10, color=col, fontweight="bold") |
| ax.text(0.5, -0.06, f"BFS cells expanded: {cum_expanded:,} plan calls: {plan_calls}", |
| transform=ax.transAxes, ha="center", va="top", fontsize=8.5, color=INK) |
| fig.tight_layout() |
| canvas = FigureCanvasAgg(fig) |
| canvas.draw() |
| buf = np.asarray(canvas.buffer_rgba())[..., :3].copy() |
| plt.close(fig) |
| return buf |
|
|
|
|
| def make_nav_gif(frames, max_frames=90, fps=12): |
| """frames: list of (walls, pos, mode, cum_expanded, target). Returns gif path.""" |
| if not frames: |
| return None |
| n = len(frames) |
| idxs = np.linspace(0, n - 1, min(max_frames, n)).astype(int) |
| plan_calls = 0 |
| prev_mode = None |
| imgs = [] |
| |
| calls_at = [] |
| pc = 0 |
| for (_, _, mode, _, _) in frames: |
| if mode == "COGNITIVE": |
| pc += 1 |
| calls_at.append(pc) |
| for i in idxs: |
| walls, pos, mode, cum, target = frames[i] |
| img = _render_nav_frame(walls, pos, mode, cum, target, i, calls_at[i]) |
| imgs.append(img) |
| |
| imgs += [imgs[-1]] * 6 |
| path = os.path.join(tempfile.gettempdir(), f"nav_{np.random.randint(1e9)}.gif") |
| imageio.mimsave(path, imgs, fps=fps, loop=0) |
| return path |
|
|
|
|
| def make_compute_plot(clutch_frames, cog_frames, clutch_label="CLUTCH"): |
| fig = Figure(figsize=(5.2, 3.4), dpi=96) |
| ax = fig.add_subplot(111) |
| cc = [f[3] for f in clutch_frames] |
| gc = [f[3] for f in cog_frames] |
| ax.plot(range(len(gc)), gc, color=COG, lw=2, label="ALWAYS_COGNITIVE (replan every step)") |
| ax.plot(range(len(cc)), cc, color=HABIT, lw=2, label=clutch_label) |
| ax.set_xlabel("step"); ax.set_ylabel("cumulative BFS cells expanded") |
| ax.set_title("Compute spent over time", fontsize=11) |
| ax.legend(fontsize=8, loc="upper left") |
| ax.grid(alpha=0.25) |
| if gc and cc: |
| ratio = cc[-1] / gc[-1] * 100 if gc[-1] else 0 |
| ax.text(0.98, 0.05, f"clutch = {ratio:.1f}% of always-replan", |
| transform=ax.transAxes, ha="right", fontsize=9, |
| color=HABIT, fontweight="bold") |
| fig.tight_layout() |
| return fig |
|
|
|
|
| def make_drift_plot(y, change_pts, result, window): |
| fig = Figure(figsize=(6.4, 3.6), dpi=96) |
| ax = fig.add_subplot(111) |
| t = np.arange(len(y)) |
| ax.plot(t, y, color="#9aa0a6", lw=1.0, label="true signal", zorder=1) |
| ax.plot(t, result["pred"], color=HABIT, lw=1.6, label="clutch prediction", zorder=2) |
| for i, cp in enumerate(sorted(change_pts)): |
| ax.axvline(cp, color="#c9b358", ls=":", lw=1.0, |
| label="regime change" if i == 0 else None) |
| for i, tt in enumerate(result["trip_times"]): |
| ax.axvline(tt, color=COG, ls="-", lw=0.9, alpha=0.7, |
| label="gate trip -> REFIT" if i == 0 else None) |
| ax.set_xlabel("time step"); ax.set_ylabel("value") |
| ax.set_title(f"Drift-gated retraining — {result['refits']} refits, " |
| f"MAE {result['mae']:.2f}", fontsize=11) |
| ax.legend(fontsize=8, loc="best") |
| ax.grid(alpha=0.25) |
| fig.tight_layout() |
| return fig |
|
|
|
|
| def make_pareto_plot(sw, best, fallback, front): |
| """Accuracy-vs-compute scatter of every swept config + Pareto frontier + winner.""" |
| fig = Figure(figsize=(6.2, 3.8), dpi=96) |
| ax = fig.add_subplot(111) |
| ref = sw["ref"] |
| mags = [r for r in sw["rows"] if r["gate"] == "MagnitudeGate"] |
| accs = [r for r in sw["rows"] if r["gate"] == "AcceleratorGate"] |
| ax.scatter([r["samples"] for r in mags], [r["mae"] for r in mags], |
| s=22, c=HABIT, alpha=0.55, label="MagnitudeGate configs") |
| ax.scatter([r["samples"] for r in accs], [r["mae"] for r in accs], |
| s=22, c="#7a5fb5", alpha=0.55, label="AcceleratorGate configs") |
| fx = [r["samples"] for r in front]; fy = [r["mae"] for r in front] |
| ax.plot(fx, fy, color=INK, lw=1.2, ls="--", alpha=0.7, label="Pareto frontier") |
| ax.axhline(ref["mae"], color=COG, lw=1.2, ls=":", |
| label=f"refit-every-step MAE ({ref['mae']:.3g})") |
| ax.axvline(ref["refit_samples"], color=COG, lw=1.0, ls=":", alpha=0.5) |
| win = best or fallback |
| if win: |
| ax.scatter([win["samples"]], [win["mae"]], s=170, marker="*", |
| c="#e8a33d", edgecolors=INK, linewidths=0.8, zorder=5, |
| label="chosen config") |
| ax.set_xlabel("training samples spent (compute)") |
| ax.set_ylabel("prediction MAE") |
| ax.set_title("Every gate config on YOUR data — down-left is better", fontsize=11) |
| ax.legend(fontsize=7.5, loc="best") |
| ax.grid(alpha=0.25) |
| fig.tight_layout() |
| return fig |
|
|