Source: scripts/explore_v2_discovery.py · Plain text

scripts/explore_v2_discovery.py

"""Post-hoc discovery diagnostics; never evaluate media or modify frozen V2."""

import hashlib
import json
from collections import Counter, defaultdict
from pathlib import Path

import numpy as np
from scipy.stats import spearmanr

from mcss.vision_probe.v2_statistics import TRAJECTORIES, selector_summary

ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "docs/experiments/EXP-2D-opportunity-selector-v2-20260926T015728+0800"
OUT = ROOT / "docs/experiments/EXP-2D-followup-20260927"


def crossfit_global(rows):
    """Choose a shared path using other sequences only, then apply to held pairs."""
    seqs = sorted({r["sequence"] for r in rows})
    if len(seqs) < 2:
        raise ValueError("Need at least two sequences")
    groups = defaultdict(dict)
    for r in rows:
        key = (r["sequence"], r["target_slot"])
        path = tuple(r["trajectory"])
        if path in groups[key]:
            raise ValueError("Duplicate candidate")
        groups[key][path] = r["J"]
    if any(set(g) != set(TRAJECTORIES) for g in groups.values()):
        raise ValueError("Every pair must include all16 candidates")
    means = {
        s: np.mean(
            [[g[t] for t in TRAJECTORIES] for key, g in groups.items() if key[0] == s], axis=0
        )
        for s in seqs
    }
    decisions = []
    for held in seqs:
        train = [s for s in seqs if s != held]
        scores = np.mean([means[s] for s in train], axis=0)
        winner = next(i for i, v in enumerate(scores) if scores.max() - v <= 1e-12)
        for sequence, slot in sorted(groups):
            if sequence == held:
                decisions.append(
                    {
                        "sequence": held,
                        "target_slot": slot,
                        "trajectory": list(TRAJECTORIES[winner]),
                        "training_sequences": train,
                        "training_score": float(scores[winner]),
                    }
                )
    return decisions


def main():
    OUT.mkdir(exist_ok=True)
    inputs = ["discovery_rows.jsonl", "nested_cv_results.json", "selectors.json"]
    hashes = {name: hashlib.sha256((SOURCE / name).read_bytes()).hexdigest() for name in inputs}
    rows = [json.loads(x) for x in (SOURCE / inputs[0]).read_text().splitlines()]
    if {r["split"] for r in rows} != {"discovery"}:
        raise ValueError("Only discovery is permitted")
    nested = json.loads((SOURCE / inputs[1]).read_text())
    methods = {
        "LOSO_global16": crossfit_global(rows),
        **{name: data["outer_oof_decisions"] for name, data in nested.items()},
    }
    summaries = {
        name: selector_summary(rows, decisions, seed=20260927, draws=10000)
        for name, decisions in methods.items()
    }
    seqs = sorted({r["sequence"] for r in rows})
    sequence_j = {
        name: {r["sequence"]: r["J"] for r in summary["per_sequence"]}
        for name, summary in summaries.items()
    }
    rng = np.random.default_rng(20260927)
    indices = rng.integers(0, len(seqs), (10000, len(seqs)))
    comparisons = {}
    for left, right in [
        ("CycleGate", "LOSO_global16"),
        ("GateOnly", "LOSO_global16"),
        ("CycleGate", "GateOnly"),
    ]:
        values = np.array([sequence_j[left][s] - sequence_j[right][s] for s in seqs])
        comparisons[left + "-" + right] = {
            "mean": float(values.mean()),
            "ci95": np.quantile(values[indices].mean(1), [0.025, 0.975]).tolist(),
            "per_sequence": dict(zip(seqs, values.tolist(), strict=True)),
            "scope": (
                "post-hoc descriptive interval conditional on saved OOF policies; "
                "no retraining in bootstrap"
            ),
        }
    pairs = defaultdict(list)
    for r in rows:
        pairs[(r["sequence"], r["target_slot"])].append(r)
    signals = []
    for (s, slot), group in sorted(pairs.items()):
        off = next(r["J"] for r in group if tuple(r["trajectory"]) == ("OFF", "OFF"))
        nonoff = [r for r in group if tuple(r["trajectory"]) != ("OFF", "OFF")]
        feature = np.array([r["visible"]["f_cycle"] for r in nonoff])
        reward = np.array([r["J"] - off for r in nonoff])
        correlation = (
            float(spearmanr(feature, reward).statistic)
            if np.ptp(feature) > 1e-12 and np.ptp(reward) > 1e-12
            else None
        )
        signals.append(
            {
                "sequence": s,
                "target_slot": slot,
                "candidate_spearman": correlation,
                "f_cycle_range": float(np.ptp(feature)),
                "reward_range": float(np.ptp(reward)),
                "positive_cycle_but_harmful_candidates": int(
                    ((feature > 1e-12) & (reward < -1e-12)).sum()
                ),
                "positive_cycle_candidates": int((feature > 1e-12).sum()),
            }
        )
    folds = []
    for fold in nested["CycleGate"]["outer_folds"]:
        model = fold["model"]
        folds.append(
            {
                "held_out": fold["held_out_sequences"],
                "cycle_standardized_coefficient": None
                if model is None
                else model["coefficients"][model["feature_names"].index("f_cycle")],
            }
        )
    result = {
        "scope": "POST_HOC_DISCOVERY_ONLY_NOT_INDEPENDENT_CONFIRMATION",
        "seed": 20260927,
        "source_hashes": hashes,
        "methods": summaries,
        "comparisons": comparisons,
        "LOSO_global_decisions": methods["LOSO_global16"],
        "LOSO_global_winners": dict(
            Counter("/".join(r["trajectory"]) for r in methods["LOSO_global16"][::2])
        ),
        "cycle_within_pair_diagnostics": signals,
        "cycle_outer_coefficients": folds,
        "confirmation_status": "BLOCKED_NO_INDEPENDENT_DATA",
        "media_reads": 0,
        "target_gt_file_reads": 0,
        "v2_modified": False,
    }
    for name, digest in hashes.items():
        assert hashlib.sha256((SOURCE / name).read_bytes()).hexdigest() == digest
    (OUT / "discovery_diagnostics.json").write_text(
        json.dumps(result, indent=2, allow_nan=False) + "\n"
    )
    print(
        json.dumps(
            {
                "comparisons": {
                    k: {x: v[x] for x in ["mean", "ci95"]} for k, v in comparisons.items()
                },
                "LOSO_global_winners": result["LOSO_global_winners"],
                "cycle_positive_but_harmful": sum(
                    x["positive_cycle_but_harmful_candidates"] for x in signals
                ),
                "positive_cycle": sum(x["positive_cycle_candidates"] for x in signals),
            },
            indent=2,
        )
    )


if __name__ == "__main__":
    main()