Manuscript revision and pending experiment work, snapshot before restructuring

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
This commit is contained in:
Giorgio Gilestro 2026-09-13 16:54:09 +01:00
parent e4804adabc
commit 84124de143
450 changed files with 52813 additions and 1202 deletions

115
figures/plot_llm_society.py Normal file
View file

@ -0,0 +1,115 @@
"""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:])