Clarity pass over the main text (36-item audit), Discussion rewrite and cut, acknowledgements, Souly et al. as ref 62, lettered SI panels, model section moved under Results; plus the untracked curriculum/society/compose/smol configs, runners, figures, stats and tests that the SI already cites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y64o8FKP7rCuXzC48pxpMm
115 lines
5.7 KiB
Python
115 lines
5.7 KiB
Python
"""v2 society figure — E11's three panels at the language-model tier, plus the competence genotype.
|
||
|
||
Pre-registered layout (tasks/prereg-llm-society-v2.md §8), written before unblinding and run on the
|
||
smoke bundle first. Reads only committed bundles: a single bundle directory, or a campaign directory
|
||
whose sub-directories ``s{seed}_{arm}/`` each hold a bundle (the PBS array writes one per element).
|
||
|
||
(A) Best-agent overall test accuracy per arm over generations (solid) with the best *newborn* of each
|
||
generation (dotted) — a climb carried by a surviving founder is visible as such; B₀ (best founder
|
||
at gen 0) dashed. Mean ± 95% CI over seeds.
|
||
(B) Behavioural diversity of the population (mean pairwise disagreement).
|
||
(C) The self-consumption signature: mean conformity − mean true accuracy.
|
||
(D) Competence genotype of the ``full`` arm's best agent: per-family test accuracy × generation, mean
|
||
over seeds — E8's "a genotype no parent had", if it happens.
|
||
|
||
Usage: python figures/plot_llm_society.py [results/llm_society_v2 | results/llm_society_v2_smoke]
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
import pandas as pd
|
||
import matplotlib.pyplot as plt
|
||
import yaml
|
||
|
||
sys.path.insert(0, str(Path(__file__).parent))
|
||
from _figlib import mean_ci, savefig # noqa: E402
|
||
|
||
_ARMS = [("full", "#2ca02c", "full society"),
|
||
("no_sex", "#ff7f0e", "no sex (no recombination)"),
|
||
("no_diversity", "#9467bd", "no diversity (greedy)"),
|
||
("no_grounding", "#d62728", "no grounding (self-consumption)"),
|
||
("sex_linear", "#1f77b4", "sex by linear blend (H2 control)")]
|
||
|
||
|
||
def load_any(results_dir: Path) -> tuple[pd.DataFrame, list[str]]:
|
||
"""One bundle, or every ``*/results.parquet`` below the directory (the campaign layout)."""
|
||
if (results_dir / "results.parquet").exists():
|
||
paths = [results_dir]
|
||
else:
|
||
paths = sorted(p.parent for p in results_dir.glob("*/results.parquet"))
|
||
if not paths:
|
||
raise SystemExit(f"no results.parquet under {results_dir}")
|
||
df = pd.concat([pd.read_parquet(p / "results.parquet") for p in paths], ignore_index=True)
|
||
fams = yaml.safe_load((paths[0] / "resolved_config.yaml").read_text())["source_config"]["families"]
|
||
return df, list(fams)
|
||
|
||
|
||
def main(results_dir: str = "results/llm_society_v2") -> None:
|
||
rd = Path(results_dir)
|
||
df, fams = load_any(rd)
|
||
pop = df[df.role == "population"]
|
||
summ = df[df.role == "summary"]
|
||
arms = [a for a in _ARMS if a[0] in set(df.arm.unique())]
|
||
|
||
best = (pop[pop.metric == "test_overall"].groupby(["arm", "seed", "generation"]).value.max()
|
||
.rename("best").reset_index())
|
||
newborn = summ[summ.metric == "best_newborn_overall"][["arm", "seed", "generation", "value"]]
|
||
b0 = best[best.generation == 0].groupby("seed").best.mean().mean()
|
||
|
||
fig, axes = plt.subplots(1, 4, figsize=(21, 4.8))
|
||
|
||
def traj(ax, frame, col, title, ylabel, style="-", label_suffix=""):
|
||
for arm, color, label in arms:
|
||
sub = frame[frame.arm == arm]
|
||
if sub.empty:
|
||
continue
|
||
x, m, h = mean_ci(sub, "generation", col)
|
||
ax.plot(x, m, style, color=color, label=(label + label_suffix) if style == "-" else None, lw=2)
|
||
if style == "-":
|
||
ax.fill_between(x, m - h, m + h, color=color, alpha=0.15, lw=0)
|
||
if title: # overlay calls pass "" and must not wipe labels
|
||
ax.set_title(title, fontsize=10); ax.set_xlabel("generation"); ax.set_ylabel(ylabel)
|
||
|
||
traj(axes[0], best, "best", "(A) best agent (solid) and best newborn (dotted)\nB₀ = best founder, dashed",
|
||
"overall test accuracy")
|
||
traj(axes[0], newborn.rename(columns={"value": "best"}), "best", "", "", style=":")
|
||
axes[0].axhline(b0, color="k", ls="--", lw=1, label=f"B₀ = {b0:.2f}")
|
||
axes[0].legend(fontsize=8, loc="best")
|
||
|
||
div = summ[summ.metric == "diversity_behav"]
|
||
traj(axes[1], div, "value", "(B) population diversity\n(mean pairwise disagreement)", "diversity")
|
||
gap = summ[summ.metric == "gap_conformity_minus_truth"]
|
||
traj(axes[2], gap, "value", "(C) self-consumption signature\nconformity − true accuracy", "gap")
|
||
axes[2].axhline(0, color="k", lw=0.8)
|
||
|
||
# (D) competence genotype of the full arm's best agent, families × generations, mean over seeds
|
||
full = pop[pop.arm == ("full" if "full" in set(pop.arm) else arms[0][0])]
|
||
fam_cols = [f"test_{f}" for f in fams]
|
||
idx = full[full.metric == "test_overall"].sort_values("value").groupby(["seed", "generation"]).tail(1)
|
||
keyed = full.set_index(["seed", "generation", "agent", "metric"]).value
|
||
gens = sorted(full.generation.unique())
|
||
heat = np.full((len(fams), len(gens)), np.nan)
|
||
for gi, g in enumerate(gens):
|
||
rows = idx[idx.generation == g]
|
||
vals = np.array([[keyed.get((r.seed, g, r.agent, c), np.nan) for c in fam_cols] for r in rows.itertuples()])
|
||
if len(vals):
|
||
heat[:, gi] = np.nanmean(vals, axis=0)
|
||
im = axes[3].imshow(heat, aspect="auto", cmap="viridis", vmin=0, vmax=1)
|
||
axes[3].set_yticks(range(len(fams))); axes[3].set_yticklabels(fams, fontsize=8)
|
||
axes[3].set_xticks(range(len(gens))); axes[3].set_xticklabels(gens, fontsize=8)
|
||
axes[3].set_xlabel("generation"); axes[3].set_title("(D) competence genotype of the best agent\n(full arm; per-family accuracy)", fontsize=10)
|
||
fig.colorbar(im, ax=axes[3], fraction=0.046, pad=0.02)
|
||
|
||
n_seeds = df.seed.nunique()
|
||
fig.suptitle(f"llm_society_v2 — the composed society at LLM scale ({n_seeds} seed{'s' if n_seeds != 1 else ''}, "
|
||
f"L={len(fams)} families, mean ± 95% CI)", y=1.02)
|
||
fig.tight_layout()
|
||
savefig(fig, rd, "llm_society_v2")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main(*sys.argv[1:])
|