spb/wpai0001_uqo
Public
1#!/usr/bin/env python32"""Sprint 1-2 pilot (Tier 0, micro-world A / OPSEQ).34What this script establishes, with artifacts:5 1. Donor/recipient capability gap on the target operator (Gate B), repeated over seeds.6 2. Hook no-op guarantee on trained models (Gate C): instrumented == uninstrumented, bitwise.7 3. Within-model causal localization of the target operator in the donor (activation patching,8 zero-ablation, head patching), with a sham baseline and reproducibility checks (RQ1).910No transplant is performed here. Nothing in this script claims transfer.1112Usage: python scripts/run_pilot.py [--seeds 0 1 2] [--steps 3000] [--device auto] [--quick] [--dry-run]13"""14from __future__ import annotations1516import argparse17import gzip18import json19import sys20import time21from pathlib import Path2223import numpy as np24import torch2526ROOT = Path(__file__).resolve().parents[1]27sys.path.insert(0, str(ROOT / "src"))2829from wp_ai_0001.config import ModelRoleConfig, OpSeqTaskConfig, PilotConfig, ToyModelConfig, TrainConfig, config_hash # noqa: E40230from wp_ai_0001.datasets import opseq # noqa: E40231from wp_ai_0001.datasets.common import Split, assert_disjoint, encode_split # noqa: E40232from wp_ai_0001.evaluation.metrics import chance_level, evaluate # noqa: E40233from wp_ai_0001.instrumentation.provenance import Manifest, make_run_id, utc_now # noqa: E40234from wp_ai_0001.localization.patching import ablation_sweep, head_patch_sweep, patch_sweep # noqa: E40235from wp_ai_0001.models.hooks import Capture, Zero # noqa: E40236from wp_ai_0001.models.toy_transformer import resolve_device # noqa: E40237from wp_ai_0001.orchestration import registry # noqa: E40238from wp_ai_0001.statistics.bootstrap import cluster_bootstrap_mean, cluster_bootstrap_paired_diff, seeds_summary # noqa: E40239from wp_ai_0001.training.train_toy import load as load_model # noqa: E40240from wp_ai_0001.training.train_toy import train # noqa: E4024142OMEGA = "qua"43MAX_SEQ = 24444546def build_splits(cfg_donor: OpSeqTaskConfig, cfg_recip: OpSeqTaskConfig, n_train: int, n_dev: int, n_eval: int, base_seed: int) -> dict[str, Split]:47 """All splits from distinct seeds; evaluation splits are disjoint from every training split."""48 s = base_seed49 sp: dict[str, Split] = {}50 sp["donor_train"] = opseq.generate(cfg_donor, n_train, s + 1, "donor_train", mode="train")51 sp["recipient_train"] = opseq.generate(cfg_recip, n_train, s + 2, "recipient_train", mode="train")52 used = sp["donor_train"].item_hashes() | sp["recipient_train"].item_hashes()53 sp["donor_dev"] = opseq.generate(cfg_donor, n_dev, s + 3, "donor_dev", mode="train", exclude_hashes=used)54 used |= sp["donor_dev"].item_hashes()55 sp["recipient_dev"] = opseq.generate(cfg_recip, n_dev, s + 4, "recipient_dev", mode="train", exclude_hashes=used)56 used |= sp["recipient_dev"].item_hashes()57 # evaluation strata (shared by all models)58 sp["target_iid"] = opseq.generate(cfg_donor, n_eval, s + 10, "target_iid", mode="train", require_operator=OMEGA, exclude_hashes=used)59 used |= sp["target_iid"].item_hashes()60 sp["control_iid"] = opseq.generate(cfg_donor, n_eval, s + 11, "control_iid", mode="train", forbid_operator=OMEGA, exclude_hashes=used)61 used |= sp["control_iid"].item_hashes()62 sp["comp_heldout"] = opseq.generate(cfg_donor, n_eval, s + 12, "comp_heldout", mode="comp", min_len=2, exclude_hashes=used)63 used |= sp["comp_heldout"].item_hashes()64 sp["len_ood"] = opseq.generate(cfg_donor, max(200, n_eval // 2), s + 13, "len_ood", mode="any", min_len=6, max_len=7, exclude_hashes=used)65 for k in ("donor_train", "donor_dev", "target_iid", "control_iid", "comp_heldout", "len_ood"):66 opseq.validate(sp[k], cfg_donor)67 opseq.validate(sp["recipient_train"], cfg_recip)68 opseq.validate(sp["recipient_dev"], cfg_recip)69 assert_disjoint(sp["donor_train"], sp["donor_dev"], sp["target_iid"], sp["control_iid"], sp["comp_heldout"], sp["len_ood"])70 assert_disjoint(sp["recipient_train"], sp["recipient_dev"], sp["target_iid"], sp["control_iid"], sp["comp_heldout"], sp["len_ood"])71 return sp727374def omega_step_accuracy(split: Split, predictions: list[list[str]]) -> np.ndarray:75 """Per item: 1 if the scratchpad token at the first Ω step is correct (items without Ω: NaN)."""76 out = np.full(len(split.examples), np.nan)77 for i, (e, pred) in enumerate(zip(split.examples, predictions)):78 ops = list(e.prompt[2:-1])79 if OMEGA in ops:80 j = ops.index(OMEGA)81 out[i] = float(j < len(pred) and pred[j] == e.answers[j])82 return out838485def localization_set(cfg: OpSeqTaskConfig, corrupt_op: str = "zib") -> tuple[Split, Split]:86 """Length-3 programs with Ω at index 1; corrupted twin replaces Ω by ``corrupt_op`` so that r_2 differs."""87 others = [o for o in cfg.operators if o != OMEGA]88 clean, corrupt = [], []89 for a in others:90 for b in others:91 for v0 in range(cfg.modulus):92 ec = opseq.make_example(v0, [a, OMEGA, b], cfg.modulus)93 ex = opseq.make_example(v0, [a, corrupt_op, b], cfg.modulus)94 if ec.answers[1] != ex.answers[1]:95 clean.append(ec)96 corrupt.append(ex)97 return (Split("loc_clean", clean, "opseq", opseq.GENERATOR_VERSION, 0, cfg.model_dump()),98 Split("loc_corrupt", corrupt, "opseq", opseq.GENERATOR_VERSION, 0, cfg.model_dump()))99100101def noop_check(model, enc, device) -> dict:102 ids = torch.from_numpy(enc["ids"][:256]).to(device)103 pos = torch.from_numpy(enc["prompt_len"][:256] - 1).to(device)104 model.hooks.clear()105 with torch.no_grad():106 ref = model(ids, pos)107 again = model(ids, pos)108 cap = Capture()109 for n in model.hook_names():110 model.hooks.add(n, cap)111 with_capture = model(ids, pos)112 model.hooks.add("resid_post.0", Zero())113 with model.hooks.disabled():114 disabled = model(ids, pos)115 enabled = model(ids, pos)116 model.hooks.clear()117 return {"deterministic_repeat": bool(torch.equal(ref, again)), "capture_all_bitwise_identity": bool(torch.equal(ref, with_capture)),118 "disabled_modifying_hook_bitwise_identity": bool(torch.equal(ref, disabled)),119 "enabled_zero_hook_changes_output": bool(not torch.equal(ref, enabled)), "n_hook_points": len(model.hook_names()), "n_items": int(ids.shape[0])}120121122def main() -> None:123 ap = argparse.ArgumentParser()124 ap.add_argument("--seeds", type=int, nargs="+", default=[0, 1, 2])125 ap.add_argument("--steps", type=int, default=3000)126 ap.add_argument("--device", default="auto")127 ap.add_argument("--quick", action="store_true", help="tiny settings for a CPU smoke test")128 ap.add_argument("--dry-run", action="store_true", help="build splits and manifest only")129 ap.add_argument("--n-boot", type=int, default=2000)130 args = ap.parse_args()131132 n_train, n_dev, n_eval, steps = (3000, 300, 300, 200) if args.quick else (40_000, 1_000, 1_000, args.steps)133 task_d = OpSeqTaskConfig()134 task_r = OpSeqTaskConfig(excluded_operators=[OMEGA])135 model_cfg = ToyModelConfig(max_seq_len=MAX_SEQ) if not args.quick else ToyModelConfig(d_model=64, n_layers=3, n_heads=2, d_ff=128, max_seq_len=MAX_SEQ)136 cfg = PilotConfig(137 experiment_id="pilot_tier0_opseq" + ("_quick" if args.quick else ""), seeds=args.seeds, target_operator=OMEGA, n_eval=n_eval, n_bootstrap=args.n_boot,138 donor=ModelRoleConfig(role="donor", name="donor", model=model_cfg, task=task_d, train=TrainConfig(steps=steps, device=args.device, n_train=n_train, n_dev=n_dev)),139 recipient=ModelRoleConfig(role="recipient", name="recipient", model=model_cfg, task=task_r, train=TrainConfig(steps=steps, device=args.device, n_train=n_train, n_dev=n_dev)),140 )141 chash = config_hash(cfg)142 run_id = make_run_id(cfg.experiment_id, chash)143 out_dir = ROOT / "experiments" / "summaries" / run_id144 art_dir = ROOT / "artifacts" / "runs" / run_id145 models_dir = ROOT / "artifacts" / "models" / run_id146 fig_dir = ROOT / "artifacts" / "figures" / run_id147 for d in (out_dir, art_dir, fig_dir):148 d.mkdir(parents=True, exist_ok=True)149 man = Manifest(run_id, cfg.experiment_id, cfg.model_dump(), ROOT / "experiments" / "manifests")150 registry.record(run_id, cfg.experiment_id, "planned", config_hash=chash)151 device = resolve_device(args.device)152 t_start = time.time()153 print(f"[{utc_now()}] run_id={run_id} device={device}")154155 # ---------------- data ----------------156 splits = build_splits(task_d, task_r, n_train, n_dev, n_eval, base_seed=20261109)157 data_dir = art_dir / "data"158 for name, sp in splits.items():159 sp.save(data_dir / f"{name}.json")160 man.data["data_hashes"][name] = {"hash": sp.hash, "n": len(sp), "seed": sp.seed, "generator": f"{sp.generator}@{sp.generator_version}"}161 man.save()162 vocab = opseq.vocab_for(task_d)163 chance = {k: chance_level(splits[k]) for k in ("target_iid", "control_iid", "comp_heldout", "len_ood")}164 print("splits:", {k: (len(v), v.hash) for k, v in splits.items()})165 if args.dry_run:166 man.set_status("planned", "dry run: splits and manifest only")167 print("dry run complete")168 return169170 man.set_status("running")171 registry.record(run_id, cfg.experiment_id, "running")172 results: dict = {"run_id": run_id, "config_hash": chash, "device": str(device), "chance_level": chance, "seeds": {}, "noop": {}, "localization": {}}173 try:174 # ---------------- training + evaluation ----------------175 eval_names = ["target_iid", "control_iid", "comp_heldout", "len_ood"]176 per_item: dict[str, dict[str, np.ndarray]] = {}177 for seed in cfg.seeds:178 results["seeds"][seed] = {}179 for role in (cfg.donor, cfg.recipient):180 tcfg = role.train.model_copy(update={"seed": seed})181 mdir = models_dir / f"{role.name}_s{seed}"182 t0 = time.time()183 if (mdir / "model.pt").exists():184 model, _ = load_model(mdir, device)185 hist = json.loads((mdir / "history.json").read_text())186 print(f" loaded {role.name} s{seed}")187 else:188 hist = []189 model = train(role.model, tcfg, splits[f"{role.name}_train"], splits[f"{role.name}_dev"], vocab, mdir, hist)190 print(f" trained {role.name} s{seed} in {time.time()-t0:.0f}s dev_acc={hist[-1]['dev_acc']:.3f}")191 model.eval()192 rec = {"n_params": model.n_params, "train_seconds": time.time() - t0, "final_dev": hist[-1], "eval": {}}193 for en in eval_names:194 r = evaluate(model, splits[en], vocab, device)195 ci = cluster_bootstrap_mean(r.correct.astype(float), r.clusters, cfg.n_bootstrap, seed=seed)196 om = omega_step_accuracy(splits[en], r.predictions)197 om_ci = cluster_bootstrap_mean(om[~np.isnan(om)], [c for c, o in zip(r.clusters, om) if not np.isnan(o)], cfg.n_bootstrap, seed=seed) if (~np.isnan(om)).any() else None198 rec["eval"][en] = {**r.to_summary(), "ci95": ci.to_dict(), "omega_step_acc": om_ci.to_dict() if om_ci else None, "chance": chance[en]}199 per_item[f"{role.name}_s{seed}_{en}"] = {"correct": r.correct, "trace_correct": r.trace_correct, "omega": om}200 with gzip.open(art_dir / f"items_{role.name}_s{seed}_{en}.json.gz", "wt") as f:201 json.dump({"split_hash": r.split_hash, "predictions": r.predictions, "answers": r.answers, "clusters": r.clusters}, f)202 enc_dev = encode_split(splits["donor_dev"], vocab, MAX_SEQ)203 results["noop"][f"{role.name}_s{seed}"] = noop_check(model, enc_dev, device)204 results["seeds"][seed][role.name] = rec205 # paired donor-recipient difference on identical items206 results["seeds"][seed]["gap"] = {}207 for en in eval_names:208 d, r_ = per_item[f"donor_s{seed}_{en}"], per_item[f"recipient_s{seed}_{en}"]209 ci = cluster_bootstrap_paired_diff(d["correct"].astype(float), r_["correct"].astype(float), splits[en].examples and [e.cluster for e in splits[en].examples], cfg.n_bootstrap, seed=seed)210 results["seeds"][seed]["gap"][en] = ci.to_dict()211 man.save()212213 results["across_seeds"] = {en: {role: seeds_summary([results["seeds"][s][role]["eval"][en]["accuracy"] for s in cfg.seeds]) for role in ("donor", "recipient")} for en in eval_names}214 results["across_seeds_omega_step"] = {role: seeds_summary([results["seeds"][s][role]["eval"]["target_iid"]["omega_step_acc"]["estimate"] for s in cfg.seeds]) for role in ("donor", "recipient")}215216 # ---------------- localization (donor models) ----------------217 clean, corrupt = localization_set(task_d)218 enc_c, enc_x = encode_split(clean, vocab, MAX_SEQ), encode_split(corrupt, vocab, MAX_SEQ)219 read_pos = int(enc_c["prompt_len"][0]) # index of r_1; predicts r_2 (the Ω step)220 assert (enc_c["prompt_len"] == read_pos).all() and (enc_x["prompt_len"] == read_pos).all()221 ids_c = torch.from_numpy(enc_c["ids"]).to(device)222 ids_x = torch.from_numpy(enc_x["ids"]).to(device)223 pos = torch.full((len(clean),), read_pos, dtype=torch.long, device=device)224 ans_c = torch.from_numpy(enc_c["answers"][:, 1]).to(device)225 ans_x = torch.from_numpy(enc_x["answers"][:, 1]).to(device)226 positions = list(range(read_pos + 1))227 pos_labels = ["<bos>", "v0", "op1", "Ω", "op3", "=", "r1"]228 loc: dict = {"n_items": len(clean), "read_position": read_pos, "positions": pos_labels, "families": {}, "ablation": {}, "heads": {}, "reproducibility": {}}229 heat_by_seed: dict[str, list[np.ndarray]] = {}230 for seed in cfg.seeds:231 model, _ = load_model(models_dir / f"donor_s{seed}", device)232 with torch.no_grad():233 base_c = (model.answer_logits(ids_c, pos).argmax(-1) == ans_c).float().mean().item()234 base_x = (model.answer_logits(ids_x, pos).argmax(-1) == ans_c).float().mean().item()235 loc.setdefault("baselines", {})[f"s{seed}"] = {"clean_acc_on_clean_answer": base_c, "corrupt_run_acc_on_clean_answer(sham)": base_x}236 for fam in ("resid_post", "attn_out", "mlp_out"):237 pr = patch_sweep(model, ids_c, ids_x, pos, ans_c, ans_x, family=fam, positions=positions)238 pr2 = patch_sweep(model, ids_c, ids_x, pos, ans_c, ans_x, family=fam, positions=positions)239 loc["families"].setdefault(fam, {})[f"s{seed}"] = {"flip_rate": pr.flip_rate.tolist(), "logit_recovery": pr.logit_recovery.tolist()}240 loc["reproducibility"][f"{fam}_s{seed}_repeat_max_abs_diff"] = float(np.abs(pr.flip_rate - pr2.flip_rate).max())241 heat_by_seed.setdefault(fam, []).append(pr.flip_rate)242 abl = ablation_sweep(model, ids_c, pos, ans_c, family=fam, positions=positions, mode="zero")243 loc["ablation"].setdefault(fam, {})[f"s{seed}"] = abl.tolist()244 for p_name, p_idx in (("omega_token", 3), ("read_position", read_pos)):245 loc["heads"].setdefault(p_name, {})[f"s{seed}"] = head_patch_sweep(model, ids_c, ids_x, pos, ans_c, ans_x, p_idx).tolist()246 print(f" localization donor s{seed} done")247 for fam, mats in heat_by_seed.items():248 if len(mats) > 1:249 flat = np.stack([m.ravel() for m in mats])250 cc = np.corrcoef(flat)251 loc["reproducibility"][f"{fam}_across_seed_pearson_min"] = float(cc[np.triu_indices(len(mats), 1)].min())252 loc["families"][fam]["mean_flip_rate"] = np.mean(mats, axis=0).tolist()253 results["localization"] = loc254255 # ---------------- figures ----------------256 make_figures(results, fig_dir, pos_labels, cfg)257 results["elapsed_seconds"] = time.time() - t_start258 (out_dir / "results.json").write_text(json.dumps(results, indent=2, default=float))259 write_summary(results, out_dir, cfg, fig_dir)260 man.data["outputs"] = {"results": str(out_dir.relative_to(ROOT) / "results.json"), "figures": str(fig_dir.relative_to(ROOT)), "models": str(models_dir.relative_to(ROOT)), "items": str(art_dir.relative_to(ROOT))}261 man.set_status("completed")262 registry.record(run_id, cfg.experiment_id, "completed", results=str(out_dir.relative_to(ROOT) / "results.json"))263 print(f"[{utc_now()}] completed in {results['elapsed_seconds']:.0f}s -> {out_dir}")264 except Exception as exc: # noqa: BLE001265 man.set_status("failed", repr(exc))266 registry.record(run_id, cfg.experiment_id, "failed", error=repr(exc))267 raise268269270def make_figures(results: dict, fig_dir: Path, pos_labels: list[str], cfg: PilotConfig) -> None:271 import matplotlib.pyplot as plt272273 from wp_ai_0001.visualization import DIVERGING_CMAP, ENTITY_COLORS, SEQUENTIAL_CMAP, apply_style, heatmap274 from wp_ai_0001.visualization.style import DOUBLE_WIDTH_IN, TEXT_SECONDARY275276 apply_style()277 seeds = {str(k): v for k, v in results["seeds"].items()} # JSON round-trips keys as strings278 eval_names = ["target_iid", "control_iid", "comp_heldout", "len_ood"]279 # Figure 1: capability gap, per split, donor vs recipient, mean over seeds with per-seed points and CI of seed 0280 fig, ax = plt.subplots(figsize=(DOUBLE_WIDTH_IN * 0.6, 2.2))281 x = np.arange(len(eval_names))282 w = 0.36283 for k, role in enumerate(("donor", "recipient")):284 means = [results["across_seeds"][en][role]["mean"] for en in eval_names]285 ax.bar(x + (k - 0.5) * w, means, width=w - 0.04, color=ENTITY_COLORS[role], label=role, linewidth=0)286 for s in cfg.seeds:287 ax.scatter(x + (k - 0.5) * w, [seeds[str(s)][role]["eval"][en]["accuracy"] for en in eval_names], s=8, color="#ffffff", edgecolor=ENTITY_COLORS[role], linewidth=0.6, zorder=3)288 ax.scatter(x, [results["chance_level"][en] for en in eval_names], marker="_", s=120, color=ENTITY_COLORS["chance"], label="majority-class chance", zorder=4)289 ax.set_xticks(x)290 ax.set_xticklabels(["target (Ω)", "control (no Ω)", "held-out comp.", "length OOD"])291 ax.set_ylabel("final-answer exact match")292 ax.set_ylim(0, 1.02)293 ax.legend(loc="upper right", ncol=1)294 fig.savefig(fig_dir / "fig_gap_tier0.pdf")295 plt.close(fig)296 # Figure 2: localization heatmaps (mean flip rate over seeds) for three families297 fams = ["resid_post", "attn_out", "mlp_out"]298 fig, axes = plt.subplots(1, 3, figsize=(DOUBLE_WIDTH_IN, 2.1), sharey=True)299 for ax, fam in zip(axes, fams):300 m = np.array(results["localization"]["families"][fam]["mean_flip_rate"])301 heatmap(ax, m, [f"L{l}" for l in range(m.shape[0])], pos_labels, cmap=SEQUENTIAL_CMAP, vmin=0, vmax=1, cbar_label="restored-answer rate", title=fam, annotate=True)302 ax.set_xlabel("patched token position")303 axes[0].set_ylabel("layer")304 fig.savefig(fig_dir / "fig_localization_patching_tier0.pdf")305 plt.close(fig)306 # Figure 3: zero-ablation (mean over seeds) accuracy307 fig, axes = plt.subplots(1, 3, figsize=(DOUBLE_WIDTH_IN, 2.1), sharey=True)308 for ax, fam in zip(axes, fams):309 m = np.mean([np.array(v) for v in results["localization"]["ablation"][fam].values()], axis=0)310 heatmap(ax, m, [f"L{l}" for l in range(m.shape[0])], pos_labels, cmap=SEQUENTIAL_CMAP, vmin=0, vmax=1, cbar_label="accuracy after zero-ablation", title=fam, annotate=True)311 ax.set_xlabel("ablated token position")312 axes[0].set_ylabel("layer")313 fig.savefig(fig_dir / "fig_localization_ablation_tier0.pdf")314 plt.close(fig)315 # Figure 4: head-level patching at the Ω token and at the read position316 fig, axes = plt.subplots(1, 2, figsize=(DOUBLE_WIDTH_IN * 0.75, 2.1), sharey=True)317 for ax, key in zip(axes, ("omega_token", "read_position")):318 m = np.mean([np.array(v) for v in results["localization"]["heads"][key].values()], axis=0)319 heatmap(ax, m, [f"L{l}" for l in range(m.shape[0])], [f"h{h}" for h in range(m.shape[1])], cmap=SEQUENTIAL_CMAP, vmin=0, vmax=1, cbar_label="restored-answer rate", title=f"head patch @ {key.replace('_', ' ')}", annotate=True)320 axes[0].set_ylabel("layer")321 fig.savefig(fig_dir / "fig_localization_heads_tier0.pdf")322 plt.close(fig)323 fig.text(0, 0, "", color=TEXT_SECONDARY)324325326def write_summary(results: dict, out_dir: Path, cfg: PilotConfig, fig_dir: Path) -> None:327 seeds = {str(k): v for k, v in results["seeds"].items()}328 L = ["# Pilot summary — " + results["run_id"], "", f"Generated {utc_now()} on device `{results['device']}`; config hash `{results['config_hash']}`; seeds {cfg.seeds}.", "",329 "## Gate B — donor/recipient gap (final-answer exact match, mean over seeds; CI = cluster bootstrap on programs)", "",330 "| split | chance | donor mean (sd) | recipient mean (sd) | paired gap seed 0 [95% CI] |", "|---|---|---|---|---|"]331 for en in ("target_iid", "control_iid", "comp_heldout", "len_ood"):332 d, r = results["across_seeds"][en]["donor"], results["across_seeds"][en]["recipient"]333 g = seeds[str(cfg.seeds[0])]["gap"][en]334 L.append(f"| {en} | {results['chance_level'][en]:.3f} | {d['mean']:.3f} ({(d['sd'] or 0):.3f}) | {r['mean']:.3f} ({(r['sd'] or 0):.3f}) | {g['estimate']:+.3f} [{g['low']:+.3f}, {g['high']:+.3f}] |")335 om = results["across_seeds_omega_step"]336 L += ["", f"Ω-step accuracy on target_iid (token emitted at the first Ω step): donor {om['donor']['mean']:.3f}, recipient {om['recipient']['mean']:.3f}.", "",337 "## Gate C — hook no-op checks (bitwise equality of logits on 256 dev items)", "", "| model | repeat | capture-all | disabled hook | enabled zero hook changes output |", "|---|---|---|---|---|"]338 for k, v in results["noop"].items():339 L.append(f"| {k} | {v['deterministic_repeat']} | {v['capture_all_bitwise_identity']} | {v['disabled_modifying_hook_bitwise_identity']} | {v['enabled_zero_hook_changes_output']} |")340 loc = results["localization"]341 L += ["", f"## RQ1 — within-donor localization of Ω (n = {loc['n_items']} clean/corrupt pairs, read position = r1, predicting r2)", "",342 "Baselines: " + "; ".join(f"s{k[1:]}: clean {v['clean_acc_on_clean_answer']:.3f}, sham (corrupt run scored on clean answer) {v['corrupt_run_acc_on_clean_answer(sham)']:.3f}" for k, v in loc["baselines"].items()), "",343 "Mean restored-answer rate after patching the clean activation into the corrupted run (rows = layers, columns = positions " + ", ".join(loc["positions"]) + "):", ""]344 for fam in ("resid_post", "attn_out", "mlp_out"):345 m = np.array(loc["families"][fam]["mean_flip_rate"])346 L.append(f"- `{fam}`: " + " | ".join(f"L{i}: " + " ".join(f"{v:.2f}" for v in row) for i, row in enumerate(m)))347 L += ["", "Reproducibility: " + "; ".join(f"{k} = {v:.4f}" for k, v in loc["reproducibility"].items()), "",348 "Figures: " + ", ".join(f"`{p.name}`" for p in sorted(fig_dir.glob("*.pdf"))), "",349 "## Interpretation (observed vs. speculative)", "",350 "Observed: the numbers above are produced by the artifacts listed in the manifest. Any statement about *transfer* is out of scope for this run: no transplant was performed.",351 "Speculative: whether the localized positions/layers constitute a transferable 'organ' is the object of the next experiment (activation graft with sham/random controls)."]352 (out_dir / "SUMMARY.md").write_text("\n".join(L) + "\n")353354355if __name__ == "__main__":356 main()357