""" Overlay plot: Down vs AntiDown (tabular experiments). Reads CSVs from a shared results directory and writes a PNG overlay plot. This is the canonical comparative result for the Down vs AntiDown experiment. """ from __future__ import annotations # Headless-safe plotting (Hugging Face / Linux without DISPLAY) import matplotlib matplotlib.use("Agg") import argparse import csv import os from collections import defaultdict from pathlib import Path from typing import Dict, List, Tuple import matplotlib.pyplot as plt # ---------------------------- # Utilities # ---------------------------- def _read_csv(path: Path) -> List[Dict[str, str]]: with path.open("r", newline="") as f: return list(csv.DictReader(f)) def _mean(xs: List[float]) -> float: if not xs: return float("nan") return sum(xs) / len(xs) # ---------------------------- # Series builders # ---------------------------- def _down_series(rows: List[Dict[str, str]]) -> Tuple[List[int], List[float]]: """ Down CSV fields (expected): phase, episode, agent, episode_reward, [global_episode], ... Aggregate by (phase, episode), averaging across agents. """ bucket: Dict[Tuple[int, int], List[float]] = defaultdict(list) for r in rows: try: phase = int(r["phase"]) ep = int(r["episode"]) rew = float(r["episode_reward"]) except Exception: continue bucket[(phase, ep)].append(rew) xs, ys = [], [] g = 0 for key in sorted(bucket.keys()): xs.append(g) ys.append(_mean(bucket[key])) g += 1 return xs, ys def _antidown_series(rows: List[Dict[str, str]]) -> Tuple[List[int], List[float]]: """ AntiDown CSV fields (expected): phase, episode, agent, total_reward, ... Aggregate by (phase, episode), averaging across agents. """ bucket: Dict[Tuple[int, int], List[float]] = defaultdict(list) for r in rows: try: phase = int(r["phase"]) ep = int(r["episode"]) rew = float(r["total_reward"]) except Exception: continue bucket[(phase, ep)].append(rew) xs, ys = [], [] g = 0 for key in sorted(bucket.keys()): xs.append(g) ys.append(_mean(bucket[key])) g += 1 return xs, ys # ---------------------------- # CLI # ---------------------------- def _build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(description="Plot Down vs AntiDown (dual overlay).") p.add_argument( "--results_dir", type=str, default="", help="Directory containing both CSVs. Defaults to TWOQUARKS_RESULTS_DIR or ./results.", ) p.add_argument( "--out", type=str, default="", help="Output directory for PNG. Defaults to TWOQUARKS_GRAPHICS_DIR or ./graphics.", ) p.add_argument( "--prefix", type=str, default="dual_down_antidown", help="Filename prefix for output image.", ) return p # ---------------------------- # Main # ---------------------------- def main() -> None: args = _build_parser().parse_args() root = Path(__file__).resolve().parent results_dir = Path( args.results_dir or os.environ.get("TWOQUARKS_RESULTS_DIR", (root / "results").as_posix()) ) graphics_dir = Path( args.out or os.environ.get("TWOQUARKS_GRAPHICS_DIR", (root / "graphics").as_posix()) ) graphics_dir.mkdir(parents=True, exist_ok=True) down_csv = results_dir / "down_paradox_tabular_results.csv" anti_csv = results_dir / "antidown_corrupted_valley_tabular.csv" if not down_csv.exists(): raise FileNotFoundError(f"Missing Down CSV: {down_csv}") if not anti_csv.exists(): raise FileNotFoundError(f"Missing AntiDown CSV: {anti_csv}") down_rows = _read_csv(down_csv) anti_rows = _read_csv(anti_csv) if not down_rows: raise RuntimeError("Down CSV is empty.") if not anti_rows: raise RuntimeError("AntiDown CSV is empty.") x1, y1 = _down_series(down_rows) x2, y2 = _antidown_series(anti_rows) if len(x1) != len(x2): print( f"[dual] WARNING: series length mismatch " f"(Down={len(x1)}, AntiDown={len(x2)})" ) plt.figure(figsize=(10, 5)) plt.plot(x1, y1, label="Down (mean across agents)") plt.plot(x2, y2, label="AntiDown (mean across agents)") plt.xlabel("Global episode (phase-concatenated)") plt.ylabel("Episode return") plt.title("Down vs AntiDown — Tabular returns (mean across agents)") plt.legend() plt.tight_layout() out_path = graphics_dir / f"{args.prefix}_episode_return.png" plt.savefig(out_path, dpi=160) plt.close() print(f"[dual] saved {out_path}") if __name__ == "__main__": main()