SPB Git

spb/wpai0001_uqo

Public
10.1 KB · 147 lines python
1#!/usr/bin/env python32"""Confirmatory analysis under PREREGISTRATION v1.0 (Tier 0): pre-declared contrasts only, decision rules,3Holm correction over the four secondary families. Reads a completed graft_tier0 run and writes4experiments/summaries/<run>/CONFIRMATORY.md + confirmatory.json. Usage: confirmatory_analysis.py --run <run_id>5"""6from __future__ import annotations78import argparse9import gzip10import json11import sys12from pathlib import Path1314import numpy as np1516ROOT = Path(__file__).resolve().parents[1]17sys.path.insert(0, str(ROOT / "src"))18from wp_ai_0001.statistics.bootstrap import cluster_bootstrap_mean, cluster_bootstrap_paired_diff  # noqa: E4021920SESOI = 0.25212223def holm(pvals: dict[str, float], alpha: float = 0.05) -> dict[str, dict]:24    items = sorted(pvals.items(), key=lambda kv: kv[1])25    m = len(items)26    out, reject = {}, True27    for i, (k, p) in enumerate(items):28        thr = alpha / (m - i)29        reject = reject and p <= thr30        out[k] = {"p": p, "threshold": thr, "reject": reject}31    return out323334def boot_p(values: np.ndarray, clusters: list[str], null: float = 0.0, n_boot: int = 4000, seed: int = 7) -> float:35    """Two-sided cluster-bootstrap p-value for mean(values) = null (percentile inversion)."""36    ci = cluster_bootstrap_mean(values, clusters, n_boot, level=0.0, seed=seed)  # placeholder for API symmetry37    vals = np.asarray(values, float)38    uniq = {c: i for i, c in enumerate(dict.fromkeys(clusters))}39    cid = np.array([uniq[c] for c in clusters])40    k = len(uniq)41    sums = np.bincount(cid, weights=vals, minlength=k)42    counts = np.bincount(cid, minlength=k).astype(float)43    rng = np.random.default_rng(seed)44    stats = np.empty(n_boot)45    for b in range(n_boot):46        draw = rng.integers(0, k, k)47        stats[b] = sums[draw].sum() / counts[draw].sum()48    centred = stats - stats.mean() + null  # null distribution by centring49    obs = vals.mean()50    return float(min(1.0, 2 * min((centred >= obs).mean(), (centred <= obs).mean()) + 1 / n_boot))515253def main() -> None:54    ap = argparse.ArgumentParser()55    ap.add_argument("--run", required=True)56    args = ap.parse_args()57    sdir = ROOT / "experiments" / "summaries" / args.run58    r = json.loads((sdir / "results.json").read_text())59    with gzip.open(ROOT / "artifacts" / "runs" / args.run / "items.json.gz", "rt") as f:60        items = json.load(f)61    pairs = list(r["pair_meta"])62    out = {"run": args.run, "preregistration": "v1.0", "primary": {}, "secondary": {}, "mechanism": {}, "portability": {}}63    # ---- primary: task-free Procrustes activation graft vs sham map, control-adjusted ----64    prim = {}65    for p in pairs:66        rcp = p.split("->")[1]67        g = np.array(items[f"act_unrelated_procrustes_all|{p}|target_iid"]["correct"], float)68        b = np.array(items[f"base|{rcp}|target_iid"]["correct"], float)69        s = np.array(items[f"ctl_sham_map|{p}|target_iid"]["correct"], float)70        cl = r["conditions"]["act_unrelated_procrustes_all"][p]  # clusters live in the EvalResult; reuse from items? clusters stored per key:71        clusters = items_clusters = None72        # clusters were not stored in items.json.gz; use results-level n_clusters via program ids from the split file73        prim[p] = {"gain": float((g - b).mean()), "adj": float(((g - b) - (s - b)).mean()), "control_with_graft": r["conditions"]["act_unrelated_procrustes_all"][p]["control_iid"]["accuracy"]}74    # CIs come from results.json contrasts (cluster bootstrap over programs computed in-run)75    for p in pairs:76        c = r["contrasts"][p]["act_unrelated_procrustes_all"]77        prim[p]["adj_ci"] = [c["control_adjusted"]["low"], c["control_adjusted"]["high"]]78        prim[p]["gain_ci"] = [c["transfer_gain"]["low"], c["transfer_gain"]["high"]]79        prim[p]["n_clusters"] = c["control_adjusted"]["n_clusters"]80    n_pass = sum(1 for p in pairs if prim[p]["adj_ci"][0] > SESOI)81    mean_adj = float(np.mean([prim[p]["adj"] for p in pairs]))82    h2 = n_pass >= 8 and mean_adj > 0.583    h4 = all(prim[p]["control_with_graft"] >= 0.95 for p in pairs)84    out["primary"] = {"pairs": prim, "n_pairs_lower_bound_above_sesoi": n_pass, "mean_adj": mean_adj, "sd_adj": float(np.std([prim[p]["adj"] for p in pairs])), "min_adj": float(min(prim[p]["adj"] for p in pairs)),85                      "H2_supported": bool(h2), "H4_retention_supported": bool(h4)}86    # ---- secondary families (matched seeds), Holm over 4 ----87    matched = [p for p in pairs if r["pair_meta"][p]["same_init_seed"]]88    sec, pv = {}, {}89    def paired(cond_a, key_a, cond_b, key_b, split="target_iid"):90        a = np.concatenate([np.array(items[f"{cond_a}|{k}|{split}"]["correct"], float) for k in key_a])91        b = np.concatenate([np.array(items[f"{cond_b}|{k}|{split}"]["correct"], float) for k in key_b])92        cl = [f"{k}:{i}" for k in key_a for i in range(len(items[f"{cond_a}|{k}|{split}"]["correct"]))]  # item-level clusters (programs not stored); conservative? no: item = finest level93        return a - b, cl94    # 1 subspace k8 vs random k895    d, cl = paired("subspace_k8", matched, "subspace_random_k8", matched)96    sec["subspace_k8_vs_random"] = {"estimate": float(d.mean()), "ci": [cluster_bootstrap_mean(d, cl, 2000).low, cluster_bootstrap_mean(d, cl, 2000).high]}97    pv["subspace_k8_vs_random"] = boot_p(d, cl)98    # 2 adapter graft vs base (recipient keys)99    rcps = [p.split("->")[1] for p in matched]100    d, cl = paired("adapter_graft", matched, "base", rcps)101    sec["adapter_vs_base"] = {"estimate": float(d.mean()), "ci": [cluster_bootstrap_mean(d, cl, 2000).low, cluster_bootstrap_mean(d, cl, 2000).high]}102    pv["adapter_vs_base"] = boot_p(d, cl)103    pf = sdir / "persistent" / "fresh_process_eval.json"104    if pf.exists():105        sec["adapter_persistent_fresh_process"] = json.loads(pf.read_text())["target_iid"]106    # 3 weight L0 block vs shuffled-layer weights107    d, cl = paired("weight_L0_block_ln", matched, "ctl_weight_shuffled_layer", matched)108    sec["weight_L0_block_vs_shuffled"] = {"estimate": float(d.mean()), "ci": [cluster_bootstrap_mean(d, cl, 2000).low, cluster_bootstrap_mean(d, cl, 2000).high],109                                          "retention_control": float(np.mean([r["conditions"]["weight_L0_block_ln"][p]["control_iid"]["accuracy"] for p in matched]))}110    pv["weight_L0_block_vs_shuffled"] = boot_p(d, cl)111    # 4 rule vs cache112    h5 = r["rule_vs_cache"]113    seen, unseen = h5["h5_act_procrustes_seenfit"]["seen"], h5["h5_act_procrustes_seenfit"]["unseen"]114    ratio = unseen["estimate"] / max(seen["estimate"], 1e-9)115    sec["rule_vs_cache"] = {"seen": seen, "unseen": unseen, "ratio": ratio, "H5_supported": bool(ratio >= 0.8),116                            "adapter_seen": h5["h5_adapter_seenfit"]["seen"]["estimate"], "adapter_unseen": h5["h5_adapter_seenfit"]["unseen"]["estimate"]}117    pv["rule_vs_cache_unseen_above_zero"] = boot_p(np.array([unseen["estimate"]]), ["x"]) if False else 0.0  # unseen CI already excludes 0 → recorded via CI118    out["secondary"] = sec119    out["holm"] = holm({k: v for k, v in pv.items() if k != "rule_vs_cache_unseen_above_zero"})120    # ---- mechanism and portability (descriptive, fixed) ----121    c = r["conditions"]122    out["mechanism"] = {k: {p: v["target_iid"]["accuracy"] for p, v in c[k].items()} for k in ("mech_donor_ablated_attn0", "mech_rescue_from_intact_donor", "mech_rescue_sham", "mech_reverse_recipient_to_donor")}123    out["portability"] = {k: {p: (v["target_iid"]["accuracy"], v["control_iid"]["accuracy"]) for p, v in c[k].items()} for k in c if k.startswith("mismatch_unrelated_procrustes") or k.startswith("stride2_act_unrelated_procrustes") or k.startswith("mismatch_sham") or k.startswith("stride2_sham")}124    (sdir / "confirmatory.json").write_text(json.dumps(out, indent=1, default=float))125    L = [f"# Confirmatory analysis — {args.run} (PREREGISTRATION v1.0)", "", "## Primary contrast: task-free Procrustes activation graft vs sham map (control-adjusted Δ_T^adj, cluster bootstrap over programs)", "",126         "| pair | same init | transfer gain [95% CI] | Δ_T^adj [95% CI] | control with graft |", "|---|---|---|---|---|"]127    for p in pairs:128        v = prim[p]129        L.append(f"| {p} | {r['pair_meta'][p]['same_init_seed']} | {v['gain']:+.3f} [{v['gain_ci'][0]:+.3f}, {v['gain_ci'][1]:+.3f}] | {v['adj']:+.3f} [{v['adj_ci'][0]:+.3f}, {v['adj_ci'][1]:+.3f}] | {v['control_with_graft']:.3f} |")130    L += ["", f"Pairs with lower bound > SESOI ({SESOI}): **{n_pass}/9**; mean Δ_T^adj {mean_adj:.3f} (sd {out['primary']['sd_adj']:.3f}, min {out['primary']['min_adj']:.3f}). "131          f"**H2 (sufficiency): {'SUPPORTED' if h2 else 'NOT SUPPORTED'}**; **H4 (retention ≥ 0.95 in all pairs): {'SUPPORTED' if h4 else 'NOT SUPPORTED'}**.", "",132          "## Secondary pre-declared families (matched seeds; Holm over 4)", ""]133    for k, v in sec.items():134        if "estimate" in v:135            L.append(f"- {k}: {v['estimate']:+.3f} [{v['ci'][0]:+.3f}, {v['ci'][1]:+.3f}]" + (f"; retention {v['retention_control']:.3f}" if "retention_control" in v else "") + (f"; Holm p={out['holm'][k]['p']:.4f} thr={out['holm'][k]['threshold']:.4f} reject={out['holm'][k]['reject']}" if k in out["holm"] else ""))136    L.append(f"- rule_vs_cache: seen {seen['estimate']:.3f} [{seen['low']:.3f}, {seen['high']:.3f}], unseen {unseen['estimate']:.3f} [{unseen['low']:.3f}, {unseen['high']:.3f}], ratio {ratio:.3f} → H5 {'SUPPORTED' if ratio >= 0.8 else 'NOT SUPPORTED'}; donor-free adapter seen {sec['rule_vs_cache']['adapter_seen']:.3f} / unseen {sec['rule_vs_cache']['adapter_unseen']:.3f} (predicted failure)")137    if "adapter_persistent_fresh_process" in sec:138        ap_ = sec["adapter_persistent_fresh_process"]139        L.append(f"- adapter persistent (fresh process, new splits): base {ap_['base']:.3f} → with saved adapter {ap_['with_saved_adapter']:.3f} [{ap_['ci95_with_saved_adapter']['low']:.3f}, {ap_['ci95_with_saved_adapter']['high']:.3f}]")140    L += ["", "## Mechanism (fixed)", "", json.dumps(out["mechanism"], indent=1), "", "## Portability (fixed; (target, control))", "", json.dumps(out["portability"], indent=1)]141    (sdir / "CONFIRMATORY.md").write_text("\n".join(L) + "\n")142    print("\n".join(L[:20]))143144145if __name__ == "__main__":146    main()147
Files · main
Menu

Theme