csc8114 / code / src / data / plot_cross_platform.py
plot_cross_platform.py
Raw
"""
Figure 4: Cross-platform AUPRC comparison — simulation vs Raspberry Pi.

Purpose
-------
Shows that simulation AUPRC rankings and values closely match those measured
on physical Raspberry Pi hardware, validating the simulation methodology.
Four communication strategies are compared: float32 ρ=1 (baseline),
float16 ρ=1, float32 ρ=3, and joint adaptive.

Data sources
------------
- Simulation: eval report JSONs for N01, N02, N04, H16 (paired scenarios).
- Pi: hardcoded from physical deployment results (3 seeds each, mean ± std).

Pairing rationale
-----------------
  float32 ρ=1   : P1  ↔  N01
  float16 ρ=1   : P2  ↔  N02
  float32 ρ=3   : P3  ↔  N04
  joint adaptive : P4  ↔  H16  (both converge to int8 + ρ=3)

Output
------
  results/graphics/fig4_cross_platform.pdf  (vector, for LaTeX)
  results/graphics/fig4_cross_platform.png  (raster preview, dpi=200)

Usage
-----
  uv run python src/data/plot_cross_platform.py
"""

import json
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import statistics as _st
from pathlib import Path

# --- LaTeX-compatible style -------------------------------------------------
plt.rcParams.update({
    "font.family":     "serif",
    "font.serif":      ["Times New Roman", "DejaVu Serif"],
    "font.size":       9,
    "axes.titlesize":  9,
    "axes.labelsize":  9,
    "xtick.labelsize": 8,
    "ytick.labelsize": 8,
    "legend.fontsize": 8,
    "lines.linewidth": 1.2,
    "axes.linewidth":  0.7,
    "pdf.fonttype":    42,
    "ps.fonttype":     42,
})

# --- Paths ------------------------------------------------------------------
RESULTS_DIR = Path(__file__).parent.parent.parent / "results"
SESSION     = "2026-05-10_01-43-06"
SEEDS       = [42, 52, 62]
OUT_PDF = RESULTS_DIR / "graphics" / "fig4_cross_platform.pdf"
OUT_PNG = RESULTS_DIR / "graphics" / "fig4_cross_platform.png"

# --- Strategy definitions ---------------------------------------------------
STRATEGIES = [
    ("float32, ρ=1\n(baseline)", "N01"),
    ("float16, ρ=1",             "N02"),
    ("float32, ρ=3",             "N04"),
    ("Joint\nAdaptive",          "H16"),
]

# Pi AUPRC values: (mean, std) from physical deployment, 3 seeds each
PI_AUPRC = {
    "N01": (0.6381, 0.0042),
    "N02": (0.6434, 0.0044),
    "N04": (0.6479, 0.0006),
    "H16": (0.6482, 0.0011),
}

# Colorblind-safe palette (Wong 2011)
COLOR_SIM = "#6682F5"   # periwinkle (simulation)
COLOR_PI  = "#F57582"   # coral (real hardware)


# --- Data loading -----------------------------------------------------------

def load_sim_auprc(scenario_id: str) -> tuple[float, float]:
    """Load mean and std AUPRC across seeds from eval report JSONs."""
    values = []
    for seed in SEEDS:
        f = RESULTS_DIR / SESSION / f"{scenario_id}_seed{seed}_eval_report.json"
        if not f.exists():
            continue
        d = json.loads(f.read_text())
        values.append(d["weighted_overall"]["auprc"])
    if not values:
        raise FileNotFoundError(f"No eval reports found for {scenario_id}")
    mean = _st.mean(values)
    std  = _st.stdev(values) if len(values) > 1 else 0.0
    return mean, std


# --- Drawing ----------------------------------------------------------------

def draw() -> None:
    fig, ax = plt.subplots(figsize=(3.5, 2.4))

    n = len(STRATEGIES)
    x = np.arange(n, dtype=float)
    bar_w = 0.32

    sim_means, sim_stds = [], []
    pi_means,  pi_stds  = [], []

    for label, sid in STRATEGIES:
        sm, ss = load_sim_auprc(sid)
        pm, ps = PI_AUPRC[sid]
        sim_means.append(sm); sim_stds.append(ss)
        pi_means.append(pm);  pi_stds.append(ps)
        print(f"  {sid}: sim={sm:.4f}±{ss:.4f}  pi={pm:.4f}±{ps:.4f}")

    ax.bar(x - bar_w / 2, sim_means, width=bar_w, color=COLOR_SIM,
           label="Simulation", yerr=sim_stds, capsize=2,
           error_kw={"elinewidth": 0.8, "ecolor": "#444", "capthick": 0.8},
           zorder=3)
    ax.bar(x + bar_w / 2, pi_means, width=bar_w, color=COLOR_PI,
           label="Raspberry Pi", yerr=pi_stds, capsize=2,
           error_kw={"elinewidth": 0.8, "ecolor": "#444", "capthick": 0.8},
           zorder=3)

    ax.set_xticks(x)
    ax.set_xticklabels([s for s, _ in STRATEGIES], fontsize=7.5)
    ax.set_ylabel("AUPRC")

    all_means = sim_means + pi_means
    all_stds  = sim_stds  + pi_stds
    ymin = min(m - s for m, s in zip(all_means, all_stds)) - 0.003
    ymax = max(m + s for m, s in zip(all_means, all_stds)) + 0.003
    ax.set_ylim(ymin, ymax)
    ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda v, _: f"{v:.3f}"))

    ax.grid(axis="y", linestyle=":", linewidth=0.6, alpha=0.5, zorder=0)
    ax.set_axisbelow(True)
    ax.spines[["top", "right"]].set_visible(False)

    handles = [
        mpatches.Patch(facecolor=COLOR_SIM, label="Simulation"),
        mpatches.Patch(facecolor=COLOR_PI,  label="Raspberry Pi"),
    ]
    ax.legend(handles=handles, loc="lower right", frameon=True,
              framealpha=0.9, edgecolor="#cccccc",
              handlelength=1.0, borderpad=0.4, labelspacing=0.2)

    fig.tight_layout(pad=0.5)
    fig.savefig(OUT_PDF, format="pdf", bbox_inches="tight")
    fig.savefig(OUT_PNG, dpi=200, bbox_inches="tight")
    print(f"PDF → {OUT_PDF}")
    print(f"PNG → {OUT_PNG}")


if __name__ == "__main__":
    draw()