Code / experiments/micro/expB_token_stability/benchmark.py

experiments/micro/expB_token_stability/benchmark.py 155 lines
#!/usr/bin/env python3
# =============================================================================
#  Project   : localvm-research
#  File      : experiments/micro/expB_token_stability/benchmark.py
#  Purpose   : Temporal stability of per-token important FFN block sets
#              (consumes expA's block-energy trace)
#  Author    : Simon-Pierre Boucher
#  Contact   : contact@spboucher.ai
#  Created   : 2026-08-12
#  Modified  : 2026-08-12
#  Platform  : macOS / Apple Silicon (arm64)
#  License   : All rights reserved (research code)
# =============================================================================
"""Experiment B — stability across consecutive tokens (charter §9.B).

Usage:
    .venv/bin/python benchmark.py [--trace <path/to/block_energy_trace.npz>]
        [--target 0.95]
"""

from __future__ import annotations

import argparse
import glob
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

import numpy as np

REPO_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(REPO_ROOT / "benchmarks"))
from hardware_manifest import collect_manifest  # noqa: E402

DELTAS = [1, 2, 4, 8, 16, 32]
WINDOWS = [8, 32, 128]


def top_sets(energy: np.ndarray, target: float) -> list[np.ndarray]:
    """energy (T, B) → per-position boolean mask of the smallest block set
    covering `target` of the energy."""
    T, B = energy.shape
    order = np.argsort(energy, axis=1)[:, ::-1]
    srt = np.take_along_axis(energy, order, axis=1)
    csum = np.cumsum(srt, axis=1)
    total = csum[:, -1:] + 1e-12
    kneed = np.argmax(csum / total >= target, axis=1) + 1
    masks = np.zeros((T, B), dtype=bool)
    for t in range(T):
        masks[t, order[t, : kneed[t]]] = True
    return masks


def jaccard(a: np.ndarray, b: np.ndarray) -> float:
    inter = np.logical_and(a, b).sum()
    union = np.logical_or(a, b).sum()
    return float(inter / union) if union else 1.0


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--trace", default=None)
    ap.add_argument("--target", type=float, default=0.95)
    ap.add_argument("--agg-factor", type=int, default=4,
                    help="aggregate trace blocks by this factor (16-neuron trace -> 64-neuron sets)")
    args = ap.parse_args()
    trace_path = args.trace or sorted(
        glob.glob(str(REPO_ROOT / "results/expA_weight_concentration/*/block_energy_trace.npz")))[-1]
    z = np.load(trace_path, allow_pickle=False)
    blocks = z["blocks"].astype(np.float32)  # (P, L, B)
    if args.agg_factor > 1:
        P0, L0, B0 = blocks.shape
        blocks = blocks.reshape(P0, L0, B0 // args.agg_factor, args.agg_factor).sum(axis=-1)
    domains = z["domains"]
    traj_id = z["traj_id"]
    P, L, B = blocks.shape
    print(f"trace {trace_path}: {blocks.shape}", flush=True)

    jac = {d: [] for d in DELTAS}
    jac_random = []
    union_frac = {w: [] for w in WINDOWS}
    per_layer_j1 = [[] for _ in range(L)]
    traj_masks_union = {}  # (traj, layer) -> aggregate mask, for expC-lite
    traj_domain = {}

    rng = np.random.default_rng(7)
    for ti in np.unique(traj_id):
        sel = traj_id == ti
        traj_domain[int(ti)] = str(domains[sel][0])
        for li in range(L):
            masks = top_sets(blocks[sel, li, :], args.target)
            T = masks.shape[0]
            for d in DELTAS:
                if T > d:
                    vals = [jaccard(masks[t], masks[t + d]) for t in range(T - d)]
                    jac[d].extend(vals)
                    if d == 1:
                        per_layer_j1[li].extend(vals)
            # random-set null at matched sizes (δ=1 pairs)
            sizes = masks.sum(axis=1)
            for t in range(min(T - 1, 8)):
                a = np.zeros(B, bool); a[rng.choice(B, sizes[t], replace=False)] = True
                b = np.zeros(B, bool); b[rng.choice(B, sizes[t + 1], replace=False)] = True
                jac_random.append(jaccard(a, b))
            for w in WINDOWS:
                for s in range(0, T - w + 1, w):
                    union_frac[w].append(float(masks[s:s + w].any(axis=0).mean()))
            traj_masks_union[(int(ti), li)] = masks.any(axis=0)

    # expC-lite: within- vs across-domain overlap of per-trajectory unions
    tids = sorted(traj_domain)
    within, across = [], []
    for i in range(len(tids)):
        for j in range(i + 1, len(tids)):
            v = np.mean([jaccard(traj_masks_union[(tids[i], li)], traj_masks_union[(tids[j], li)])
                         for li in range(0, L, 4)])
            (within if traj_domain[tids[i]] == traj_domain[tids[j]] else across).append(v)

    result = {
        "jaccard_by_delta": {str(d): {"mean": float(np.mean(v)), "std": float(np.std(v)), "n": len(v)}
                             for d, v in jac.items()},
        "jaccard_random_null": {"mean": float(np.mean(jac_random)), "std": float(np.std(jac_random))},
        "union_working_set_frac_by_window": {str(w): {"mean": float(np.mean(v)), "std": float(np.std(v))}
                                             for w, v in union_frac.items()},
        "per_layer_jaccard1_mean": [float(np.mean(v)) for v in per_layer_j1],
        "expC_lite_domain_locality": {
            "within_domain_union_jaccard": float(np.mean(within)),
            "across_domain_union_jaccard": float(np.mean(across)),
        },
        "mean_topset_frac": float(np.mean([m.mean() for m in
                                           [top_sets(blocks[traj_id == t, li, :], args.target)
                                            for t in np.unique(traj_id)[:4] for li in (0, L // 2, L - 1)]])),
    }
    ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    out_dir = REPO_ROOT / "results" / "expB_token_stability" / ts
    out_dir.mkdir(parents=True)
    (out_dir / "results.json").write_text(json.dumps({
        "experiment": "expB_token_stability",
        "author": "Simon-Pierre Boucher",
        "contact": "contact@spboucher.ai",
        "manifest": collect_manifest(),
        "config": {"trace": str(trace_path), "target": args.target},
        **result,
    }, indent=2))
    print(json.dumps(result["jaccard_by_delta"], indent=1))
    print("random null:", result["jaccard_random_null"])
    print("union by window:", json.dumps(result["union_working_set_frac_by_window"]))
    print("domain locality:", result["expC_lite_domain_locality"])
    print(f"\nwrote {out_dir / 'results.json'}")


if __name__ == "__main__":
    main()