Spaces:
Sleeping
Sleeping
| """ | |
| UMBRA Pictorial Analysis β generates 6 charts from local log files. | |
| Run locally: python analysis.py | |
| Run in Colab: %run analysis.py | |
| """ | |
| import json, math, collections | |
| from pathlib import Path | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import matplotlib.patches as mpatches | |
| import numpy as np | |
| OUT_DIR = Path("logs/reward_graphs") | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| DARK = "#0d1117" | |
| PANEL = "#161b22" | |
| ACCENT = "#58a6ff" | |
| GREEN = "#3fb950" | |
| AMBER = "#d29922" | |
| RED = "#f85149" | |
| WHITE = "#e6edf3" | |
| GREY = "#8b949e" | |
| plt.rcParams.update({ | |
| "figure.facecolor": DARK, | |
| "axes.facecolor": PANEL, | |
| "axes.edgecolor": GREY, | |
| "axes.labelcolor": WHITE, | |
| "xtick.color": WHITE, | |
| "ytick.color": WHITE, | |
| "text.color": WHITE, | |
| "grid.color": "#21262d", | |
| "grid.linewidth": 0.8, | |
| "font.family": "monospace", | |
| }) | |
| # ββ Load data βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| episodes = [] | |
| with open("logs/episodes.jsonl") as f: | |
| for line in f: | |
| episodes.append(json.loads(line.strip())) | |
| rollouts = [] | |
| with open("logs/rollout_samples.jsonl") as f: | |
| for line in f: | |
| rollouts.append(json.loads(line.strip())) | |
| with open("results.json") as f: | |
| results = json.load(f) | |
| # ββ Chart 1: Reward progression over episodes (rollout checkpoints) βββββββββββ | |
| def chart_reward_progression(): | |
| eps = [r["ep"] for r in rollouts] | |
| rews = [r["total_reward"] for r in rollouts] | |
| # EMA smoothing | |
| alpha, ema, emas = 0.4, rews[0], [] | |
| for r in rews: | |
| ema = alpha * r + (1 - alpha) * ema | |
| emas.append(ema) | |
| fig, ax = plt.subplots(figsize=(12, 5)) | |
| fig.patch.set_facecolor(DARK) | |
| ax.fill_between(eps, rews, alpha=0.18, color=ACCENT) | |
| ax.plot(eps, rews, "o--", color=ACCENT, alpha=0.55, linewidth=1.2, | |
| markersize=6, label="Episode reward") | |
| ax.plot(eps, emas, "-", color=GREEN, linewidth=2.5, | |
| label="EMA trend (Ξ±=0.4)") | |
| # Stage boundary annotations | |
| for x, label, col in [(167, "Stage 2", AMBER), (333, "Stage 3", RED)]: | |
| ax.axvline(x, color=col, linestyle="--", linewidth=1.2, alpha=0.7) | |
| ax.text(x + 5, max(rews) * 0.92, label, color=col, fontsize=9) | |
| ax.set_xlabel("Episode", fontsize=11) | |
| ax.set_ylabel("Total Reward", fontsize=11) | |
| ax.set_title("Reward Progression Across 500 Episodes", fontsize=13, | |
| fontweight="bold", color=WHITE, pad=12) | |
| ax.legend(loc="upper left", facecolor=PANEL, edgecolor=GREY) | |
| ax.grid(True, axis="y") | |
| plt.tight_layout() | |
| path = OUT_DIR / "1_reward_progression.png" | |
| plt.savefig(path, dpi=150, bbox_inches="tight", facecolor=DARK) | |
| plt.close() | |
| print(f"β Saved: {path}") | |
| # ββ Chart 2: Performance metrics by difficulty (Easy / Medium / Hard) βββββββββ | |
| def chart_metrics_by_difficulty(): | |
| metrics = [ | |
| "Sycophancy rate", | |
| "Deception catch rate", | |
| "Calibration score", | |
| "Coalition detection rate", | |
| "Suboptimal action rate", | |
| ] | |
| difficulties = ["easy", "medium", "hard"] | |
| colors = [ACCENT, GREEN, AMBER] | |
| x = np.arange(len(metrics)) | |
| width = 0.25 | |
| fig, ax = plt.subplots(figsize=(13, 6)) | |
| fig.patch.set_facecolor(DARK) | |
| for i, (diff, col) in enumerate(zip(difficulties, colors)): | |
| vals = [results[diff][m] for m in metrics] | |
| bars = ax.bar(x + i * width, vals, width, label=diff.capitalize(), | |
| color=col, alpha=0.85) | |
| for bar, v in zip(bars, vals): | |
| ax.text(bar.get_x() + bar.get_width() / 2, | |
| bar.get_height() + 0.02, f"{v:.2f}", | |
| ha="center", va="bottom", fontsize=8, color=WHITE) | |
| ax.set_xticks(x + width) | |
| ax.set_xticklabels([m.replace(" ", "\n") for m in metrics], fontsize=9) | |
| ax.set_ylabel("Score (0β1)", fontsize=11) | |
| ax.set_ylim(0, 1.15) | |
| ax.set_title("Performance Metrics by Difficulty Level", fontsize=13, | |
| fontweight="bold", color=WHITE, pad=12) | |
| ax.legend(facecolor=PANEL, edgecolor=GREY) | |
| ax.grid(True, axis="y") | |
| plt.tight_layout() | |
| path = OUT_DIR / "2_metrics_by_difficulty.png" | |
| plt.savefig(path, dpi=150, bbox_inches="tight", facecolor=DARK) | |
| plt.close() | |
| print(f"β Saved: {path}") | |
| # ββ Chart 3: Reward distribution by curriculum stage ββββββββββββββββββββββββββ | |
| def chart_reward_by_stage(): | |
| by_stage = collections.defaultdict(list) | |
| for ep in episodes: | |
| by_stage[ep["stage"]].append(ep["total_reward"]) | |
| fig, axes = plt.subplots(1, 3, figsize=(14, 5), sharey=False) | |
| fig.patch.set_facecolor(DARK) | |
| stage_colors = {1: ACCENT, 2: AMBER, 3: RED} | |
| stage_names = {1: "Stage 1 β Agreeable NPCs", | |
| 2: "Stage 2 β Coalition NPCs", | |
| 3: "Stage 3 β Manipulator NPCs"} | |
| for ax, stage in zip(axes, [1, 2, 3]): | |
| data = by_stage[stage] | |
| col = stage_colors[stage] | |
| ax.hist(data, bins=18, color=col, alpha=0.8, edgecolor=DARK) | |
| ax.axvline(np.mean(data), color=WHITE, linestyle="--", linewidth=1.5, | |
| label=f"Mean={np.mean(data):.2f}") | |
| ax.set_title(stage_names[stage], fontsize=10, color=WHITE, pad=8) | |
| ax.set_xlabel("Episode Reward", fontsize=9) | |
| ax.set_ylabel("Count" if stage == 1 else "", fontsize=9) | |
| ax.legend(fontsize=8, facecolor=PANEL, edgecolor=GREY) | |
| ax.grid(True, axis="y") | |
| fig.suptitle("Reward Distribution by Curriculum Stage", fontsize=13, | |
| fontweight="bold", color=WHITE, y=1.02) | |
| plt.tight_layout() | |
| path = OUT_DIR / "3_reward_by_stage.png" | |
| plt.savefig(path, dpi=150, bbox_inches="tight", facecolor=DARK) | |
| plt.close() | |
| print(f"β Saved: {path}") | |
| # ββ Chart 4: NPC encounter frequency ββββββββββββββββββββββββββββββββββββββββββ | |
| def chart_npc_frequency(): | |
| npc_counts = collections.Counter() | |
| for ep in episodes: | |
| for npc in ep["active_npcs"]: | |
| npc_counts[npc] += 1 | |
| npcs = list(npc_counts.keys()) | |
| counts = [npc_counts[n] for n in npcs] | |
| cols = [ACCENT, GREEN, AMBER, RED, "#bc8cff", "#ff7b72"][:len(npcs)] | |
| fig, ax = plt.subplots(figsize=(9, 5)) | |
| fig.patch.set_facecolor(DARK) | |
| bars = ax.barh(npcs, counts, color=cols, alpha=0.85) | |
| for bar, c in zip(bars, counts): | |
| ax.text(bar.get_width() + 2, bar.get_y() + bar.get_height() / 2, | |
| str(c), va="center", fontsize=9, color=WHITE) | |
| ax.set_xlabel("Episodes", fontsize=11) | |
| ax.set_title("NPC Encounter Frequency (538 episodes)", fontsize=13, | |
| fontweight="bold", color=WHITE, pad=12) | |
| ax.grid(True, axis="x") | |
| plt.tight_layout() | |
| path = OUT_DIR / "4_npc_frequency.png" | |
| plt.savefig(path, dpi=150, bbox_inches="tight", facecolor=DARK) | |
| plt.close() | |
| print(f"β Saved: {path}") | |
| # ββ Chart 5: Action diversity (unique actions per rollout episode) βββββββββββββ | |
| def chart_action_diversity(): | |
| eps = [r["ep"] for r in rollouts] | |
| n_unique = [len(set(r["actions"])) for r in rollouts] | |
| n_total = [len(r["actions"]) for r in rollouts] | |
| entropy = [] | |
| for r in rollouts: | |
| acts = r["actions"] | |
| cnt = collections.Counter(acts) | |
| tot = len(acts) | |
| h = -sum((v / tot) * math.log2(v / tot) for v in cnt.values() if v > 0) | |
| entropy.append(round(h, 3)) | |
| fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) | |
| fig.patch.set_facecolor(DARK) | |
| ax1.bar(eps, n_unique, width=25, color=GREEN, alpha=0.8, label="Unique actions") | |
| ax1.bar(eps, n_total, width=25, color=ACCENT, alpha=0.35, label="Total actions") | |
| ax1.set_ylabel("Action count", fontsize=10) | |
| ax1.set_title("Action Diversity per Checkpoint Episode", fontsize=13, | |
| fontweight="bold", color=WHITE, pad=12) | |
| ax1.legend(facecolor=PANEL, edgecolor=GREY) | |
| ax1.grid(True, axis="y") | |
| ax2.plot(eps, entropy, "o-", color=AMBER, linewidth=2, markersize=8) | |
| ax2.axhline(1.0, color=GREEN, linestyle="--", linewidth=1, alpha=0.6, label="Healthy min (1.0)") | |
| ax2.axhline(0.3, color=RED, linestyle="--", linewidth=1, alpha=0.6, label="Collapse threshold (0.3)") | |
| ax2.fill_between(eps, entropy, alpha=0.15, color=AMBER) | |
| ax2.set_xlabel("Episode", fontsize=10) | |
| ax2.set_ylabel("Shannon Entropy (bits)", fontsize=10) | |
| ax2.set_title("Action Entropy Over Training", fontsize=11, | |
| fontweight="bold", color=WHITE, pad=8) | |
| ax2.legend(facecolor=PANEL, edgecolor=GREY, fontsize=8) | |
| ax2.grid(True, axis="y") | |
| plt.tight_layout() | |
| path = OUT_DIR / "5_action_diversity.png" | |
| plt.savefig(path, dpi=150, bbox_inches="tight", facecolor=DARK) | |
| plt.close() | |
| print(f"β Saved: {path}") | |
| # ββ Chart 6: Overall UMBRA scorecard radar ββββββββββββββββββββββββββββββββββββ | |
| def chart_scorecard_radar(): | |
| labels = [ | |
| "Deception\nResistance", "Calibration", "Coalition\nDetection", | |
| "Low\nSycophancy", "Low Suboptimal\nActions", "Adversarial\nRobustness" | |
| ] | |
| # Average across easy/medium/hard where applicable | |
| def avg(key): | |
| return sum(results[d].get(key, 0) for d in ["easy","medium","hard"]) / 3 | |
| values = [ | |
| avg("Deception catch rate"), | |
| avg("Calibration score"), | |
| avg("Coalition detection rate"), | |
| 1 - avg("Sycophancy rate"), # invert: lower syco = better | |
| 1 - avg("Suboptimal action rate"), # invert: lower suboptimal = better | |
| results["adversarial_robustness_score"], | |
| ] | |
| N = len(labels) | |
| angles = [n / float(N) * 2 * math.pi for n in range(N)] | |
| angles += angles[:1] | |
| values += values[:1] | |
| fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True)) | |
| fig.patch.set_facecolor(DARK) | |
| ax.set_facecolor(PANEL) | |
| ax.plot(angles, values, "o-", color=ACCENT, linewidth=2.5) | |
| ax.fill(angles, values, color=ACCENT, alpha=0.25) | |
| ax.set_xticks(angles[:-1]) | |
| ax.set_xticklabels(labels, fontsize=10, color=WHITE) | |
| ax.set_ylim(0, 1) | |
| ax.set_yticks([0.25, 0.5, 0.75, 1.0]) | |
| ax.set_yticklabels(["0.25", "0.5", "0.75", "1.0"], fontsize=8, color=GREY) | |
| ax.grid(color=GREY, alpha=0.4) | |
| ax.spines["polar"].set_color(GREY) | |
| # Annotate each vertex | |
| for angle, val, label in zip(angles[:-1], values[:-1], labels): | |
| ax.annotate(f"{val:.2f}", | |
| xy=(angle, val), | |
| xytext=(angle, val + 0.08), | |
| ha="center", fontsize=9, color=WHITE, fontweight="bold") | |
| ax.set_title("UMBRA Overall Scorecard", fontsize=14, | |
| fontweight="bold", color=WHITE, pad=20) | |
| plt.tight_layout() | |
| path = OUT_DIR / "6_scorecard_radar.png" | |
| plt.savefig(path, dpi=150, bbox_inches="tight", facecolor=DARK) | |
| plt.close() | |
| print(f"β Saved: {path}") | |
| # ββ Run all βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| print("Generating UMBRA pictorial analysisβ¦\n") | |
| chart_reward_progression() | |
| chart_metrics_by_difficulty() | |
| chart_reward_by_stage() | |
| chart_npc_frequency() | |
| chart_action_diversity() | |
| chart_scorecard_radar() | |
| print(f"\nβ All 6 charts saved to {OUT_DIR}/") | |