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

View file

@ -26,129 +26,29 @@ from pathlib import Path
import numpy as np
import pandas as pd
from .tasks import Task, make_tasks, verify, _normalise
from .tasks import Task, make_tasks, _normalise
# ---------------------------------------------------------------------------- pure operators
def consensus_answers(outputs: list[list[str]]) -> list[str]:
"""The population's modal (normalised) answer per prompt.
Args:
outputs (list[list[str]]): ``outputs[i][p]`` = agent i's raw answer to prompt p.
Returns:
list[str]: per-prompt modal normalised answer (ties broken lexicographically
deterministic).
"""
n_prompts = len(outputs[0])
cons = []
for p in range(n_prompts):
votes: dict[str, int] = {}
for out in outputs:
key = _normalise(out[p])
votes[key] = votes.get(key, 0) + 1
# Reason: max count, then lexicographically smallest key, so consensus is deterministic.
top = max(votes.values())
cons.append(min(k for k, v in votes.items() if v == top))
return cons
def conformity_scores(outputs: list[list[str]], consensus: list[str]) -> np.ndarray:
"""Each agent's agreement rate with the population consensus (the E11 conformity signal)."""
return np.array([np.mean([_normalise(o) == c for o, c in zip(out, consensus)])
for out in outputs], dtype=float)
def behavioural_distance(outputs: list[list[str]]) -> np.ndarray:
"""Pairwise disagreement rate between agents' normalised answers (verifier-free diversity)."""
n = len(outputs)
norm = [[_normalise(o) for o in out] for out in outputs]
d = np.zeros((n, n))
for i in range(n):
for j in range(i + 1, n):
d[i, j] = d[j, i] = float(np.mean([a != b for a, b in zip(norm[i], norm[j])]))
return d
def select_parents(scores: np.ndarray, dist: np.ndarray, k: int, *, diversity: bool,
lam: float = 0.3) -> list[int]:
"""Select ``k`` parents by score, optionally with quality-diversity preservation.
Diversity ON: greedy take the top-scoring agent, then repeatedly add the agent maximising
``score + lam * (mean behavioural distance to those already selected)``. Diversity OFF: plain
top-k by score.
Args:
scores (np.ndarray): per-agent selection score ``g*fitness + (1-g)*conformity``.
dist (np.ndarray): pairwise behavioural-distance matrix.
k (int): number of parents.
diversity (bool): quality-diversity (True) or greedy (False).
lam (float): diversity weight.
Returns:
list[int]: selected agent indices (deterministic).
"""
if not diversity:
return list(np.argsort(-scores, kind="stable")[:k])
chosen = [int(np.argmax(scores))]
while len(chosen) < k:
best_i, best_v = -1, -np.inf
for i in range(len(scores)):
if i in chosen:
continue
v = scores[i] + lam * float(np.mean([dist[i, j] for j in chosen]))
if v > best_v:
best_i, best_v = i, v
chosen.append(best_i)
return chosen
def complementary_pairs(parents: list[int], dist: np.ndarray, n_children: int) -> list[tuple[int, int]]:
"""Mating plan: pairs of parents in descending behavioural distance, cycled to fill the slots.
Directed mate choice (E10): the most complementary (most-disagreeing) pairs breed first; the pair
list cycles until ``n_children`` slots are filled.
"""
pairs = sorted(((a, b) for i, a in enumerate(parents) for b in parents[i + 1:]),
key=lambda ab: -dist[ab[0], ab[1]])
if not pairs: # single parent: self-pair (degenerate)
pairs = [(parents[0], parents[0])]
return [pairs[i % len(pairs)] for i in range(n_children)]
# Pure operators live in society_ops (shared with the v2 loop); re-exported here so the v1 code
# path and its tests are unchanged.
from .society_ops import ( # noqa: E402,F401
answer_of as _answer_of, arm_settings as _arm_settings_v2, behavioural_distance,
complementary_pairs, conformity_scores, consensus_answers, fitness_of as _fitness,
select_parents,
)
def arm_settings(arm: str, g: float) -> dict:
"""Resolve an arm name to its operator switches (the four-arm ablation)."""
if arm == "full":
return {"g": g, "sex": True, "diversity": True}
if arm == "no_grounding":
return {"g": 0.0, "sex": True, "diversity": True}
if arm == "no_sex":
return {"g": g, "sex": False, "diversity": True}
if arm == "no_diversity":
return {"g": g, "sex": True, "diversity": False}
raise ValueError(f"unknown arm {arm!r} (full|no_grounding|no_sex|no_diversity)")
"""v1 four-arm switches (boolean ``sex``), resolved from the shared table."""
s = _arm_settings_v2(arm, g)
if s["sex"] == "linear":
raise ValueError("sex_linear is a v2 arm (kind: llm_society_v2)")
return {"g": s["g"], "sex": s["sex"] is not None, "diversity": s["diversity"]}
# ---------------------------------------------------------------------------- the GPU loop
def _fitness(outs: list[str], tasks: list[Task], fams: list[str]) -> dict:
"""Overall / per-family / worst-family verifier accuracy from precomputed outputs."""
corr = np.array([verify(o, t) for o, t in zip(outs, tasks)])
fam = np.array([t.family for t in tasks])
acc = {"overall": float(corr.mean())}
acc.update({f: float(corr[fam == f].mean()) for f in fams})
acc["worst_family"] = min(acc[f] for f in fams)
return acc
def _answer_of(raw: str) -> str:
"""Trim a raw completion to a single short answer line for the inheritance data."""
return raw.strip().split("\n")[0][:64].strip()
def run_society_experiment(cfg: dict) -> pd.DataFrame:
"""Run the society loop for every configured arm; return tidy long-form rows.