271 lines
12 KiB
Python
271 lines
12 KiB
Python
"""_analyze_varisol.py — analyse inject_variable_isolation output.
|
|
|
|
Reads /mnt/d/Resonance_Engine/traj/varisol_<RUN_ID>/injections.jsonl
|
|
Outputs the four tests Claude Desktop specified:
|
|
1. S/N test — peak_delta_asym / pre_std_asym per (var, quantile)
|
|
2. Directionality — does sign(response) flip with sign(strength)?
|
|
3. Stress symmetry — stress_xx vs yy isotropic vs anisotropic
|
|
4. Control test — mid_price near-zero check
|
|
Plus per-variable signature table aggregated over reps.
|
|
|
|
Plus the new test enabled by adding calibration runs:
|
|
5. Calibration regression — is each variable response explained by
|
|
amplitude alone (does it fall on the raw-strength response curve)?
|
|
If yes → variable is "just amplitude". If no → variable has structure.
|
|
|
|
Usage:
|
|
python3 _analyze_varisol.py latest
|
|
python3 _analyze_varisol.py <RUN_ID>
|
|
"""
|
|
from __future__ import annotations
|
|
import json, sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
ROOT = Path("/mnt/d/Resonance_Engine/traj")
|
|
|
|
|
|
def find_run(arg: str) -> Path:
|
|
if arg == "latest":
|
|
runs = sorted([p for p in ROOT.glob("varisol_*") if p.is_dir()])
|
|
if not runs:
|
|
sys.exit("no varisol_* runs in " + str(ROOT))
|
|
return runs[-1]
|
|
p = ROOT / (arg if arg.startswith("varisol_") else f"varisol_{arg}")
|
|
if not p.exists():
|
|
sys.exit(f"missing: {p}")
|
|
return p
|
|
|
|
|
|
def load_records(run_dir: Path) -> list[dict]:
|
|
jp = run_dir / "injections.jsonl"
|
|
if not jp.exists():
|
|
sys.exit(f"no injections.jsonl in {run_dir}")
|
|
out = []
|
|
with jp.open() as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line:
|
|
out.append(json.loads(line))
|
|
return out
|
|
|
|
|
|
def fmt(v, w=8, p=3, plus=False):
|
|
if v is None:
|
|
return f"{'--':>{w}}"
|
|
try:
|
|
s = f"{v:+.{p}f}" if plus else f"{v:.{p}f}"
|
|
except Exception:
|
|
return f"{str(v):>{w}}"
|
|
return f"{s:>{w}}"
|
|
|
|
|
|
def aggregate(recs: list[dict]) -> pd.DataFrame:
|
|
rows = []
|
|
for r in recs:
|
|
lbl = r["label"]
|
|
row = {
|
|
"kind": lbl.get("kind"),
|
|
"var": lbl.get("var", "—"),
|
|
"quantile": lbl.get("quantile_label", "—"),
|
|
"rep": lbl.get("rep"),
|
|
"strength": r["strength"],
|
|
"pre_asym_mean": r.get("pre_stats", {}).get("asymmetry_mean"),
|
|
"pre_asym_std": r.get("pre_stats", {}).get("asymmetry_std"),
|
|
"pre_coh_mean": r.get("pre_stats", {}).get("coherence_mean"),
|
|
}
|
|
sig = r.get("signature", {})
|
|
for ch in ["asymmetry", "coherence", "stress_xx", "stress_yy",
|
|
"stress_xy", "vel_mean", "vorticity_mean"]:
|
|
row[f"{ch}_peak_delta"] = sig.get(f"{ch}_peak_delta")
|
|
row[f"{ch}_sn"] = sig.get(f"{ch}_sn")
|
|
row[f"{ch}_halflife_s"] = sig.get(f"{ch}_halflife_s")
|
|
row[f"{ch}_peak_time_s"] = sig.get(f"{ch}_peak_time_s")
|
|
row["stress_iso_ratio"] = sig.get("stress_iso_ratio")
|
|
rows.append(row)
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
def main():
|
|
arg = sys.argv[1] if len(sys.argv) > 1 else "latest"
|
|
run = find_run(arg)
|
|
print(f"=== analyzing {run.name} ===\n")
|
|
|
|
meta = json.loads((run / "meta.json").read_text())
|
|
print(f"run_id={meta['run_id']} started={meta.get('wall_iso_start')}")
|
|
print(f"variables={meta['variables']}")
|
|
print(f"quantiles={meta['quantiles']} str_cap={meta['str_cap']}")
|
|
print(f"calibration_strengths={meta['calibration_strengths']}")
|
|
print(f"n_repeats={meta['n_repeats']} total={meta['n_total_injections']}")
|
|
print()
|
|
|
|
base_p = run / "baseline_clean_stats.json"
|
|
if base_p.exists():
|
|
b = json.loads(base_p.read_text())
|
|
print("=== verified-stable baseline ===")
|
|
print(f" baseline_anchor_asym = {b.get('baseline_anchor_asym'):.3f}")
|
|
print(f" asymmetry: mean={b.get('asymmetry_mean'):.3f} std={b.get('asymmetry_std'):.4f}")
|
|
print(f" coherence: mean={b.get('coherence_mean'):.4f} std={b.get('coherence_std'):.4f}")
|
|
print(f" stress_xy: mean={b.get('stress_xy_mean'):.6f} std={b.get('stress_xy_std'):.6f}")
|
|
print(f" vel_mean : mean={b.get('vel_mean_mean'):.4f} std={b.get('vel_mean_std'):.4f}")
|
|
print(f" n={b.get('n')}")
|
|
print()
|
|
|
|
recs = load_records(run)
|
|
df = aggregate(recs)
|
|
print(f"loaded {len(df)} injection records "
|
|
f"({(df['kind']=='variable').sum()} variable, {(df['kind']=='calibration').sum()} calibration)\n")
|
|
|
|
var_df = df[df["kind"] == "variable"].copy()
|
|
cal_df = df[df["kind"] == "calibration"].copy()
|
|
|
|
# ─── TEST 1: S/N per (variable, quantile) ───
|
|
print("=== TEST 1: SIGNAL/NOISE (peak_asym_delta / pre_asym_std) ===")
|
|
print(f"{'variable':<18}{'quantile':>10}{'strength':>12}{'sn(mean3reps)':>17}{'peak_Δasym':>14}{'halflife_s':>13}")
|
|
grp = var_df.groupby(["var", "quantile", "strength"]).agg(
|
|
sn_mean=("asymmetry_sn", "mean"),
|
|
peak_mean=("asymmetry_peak_delta", "mean"),
|
|
halflife_mean=("asymmetry_halflife_s", "mean"),
|
|
).reset_index().sort_values(["var", "quantile"])
|
|
for _, r in grp.iterrows():
|
|
sn_str = "BELOW NOISE" if (r["sn_mean"] is not None and r["sn_mean"] < 2.0) else ""
|
|
print(f"{r['var']:<18}{r['quantile']:>10}{r['strength']:>+12.5f}"
|
|
f"{r['sn_mean']:>17.3f}{r['peak_mean']:>+14.4f}"
|
|
f"{r['halflife_mean']!s:>13} {sn_str}")
|
|
print(" (S/N < 2.0 = below the noise floor; that variable at that quantile is poison.)\n")
|
|
|
|
# ─── TEST 2: Directionality ───
|
|
print("=== TEST 2: DIRECTIONALITY (does response sign flip with strength sign?) ===")
|
|
print(f"{'variable':<18}{'q01_strength':>14}{'q01_peakΔ':>14}{'q99_strength':>14}{'q99_peakΔ':>14}{'verdict':>20}")
|
|
for v in meta["variables"]:
|
|
sub = var_df[var_df["var"] == v]
|
|
q01 = sub[sub["quantile"] == "q01"]
|
|
q99 = sub[sub["quantile"] == "q99"]
|
|
if q01.empty or q99.empty:
|
|
continue
|
|
s01 = float(q01["strength"].iloc[0]); s99 = float(q99["strength"].iloc[0])
|
|
p01 = float(q01["asymmetry_peak_delta"].mean()); p99 = float(q99["asymmetry_peak_delta"].mean())
|
|
verdict = "directional" if (np.sign(p01) != np.sign(p99) and np.sign(s01) != np.sign(s99)) else "scalar/null"
|
|
print(f"{v:<18}{s01:>+14.4f}{p01:>+14.4f}{s99:>+14.4f}{p99:>+14.4f}{verdict:>20}")
|
|
print()
|
|
|
|
# ─── TEST 3: Stress symmetry (isotropic vs anisotropic) ───
|
|
print("=== TEST 3: STRESS SYMMETRY (xx vs yy peak — isotropic if ratio→1) ===")
|
|
print(f"{'variable':<18}{'quantile':>10}{'iso_ratio(mean)':>18}{'stress_xy_peak':>17}{'verdict':>18}")
|
|
for v in meta["variables"]:
|
|
sub = var_df[var_df["var"] == v]
|
|
for q in ["q01", "q50", "q99"]:
|
|
ss = sub[sub["quantile"] == q]
|
|
if ss.empty:
|
|
continue
|
|
iso = float(ss["stress_iso_ratio"].mean())
|
|
xy = float(ss["stress_xy_peak_delta"].mean())
|
|
if iso > 0.8:
|
|
verdict = "isotropic"
|
|
elif iso < 0.5:
|
|
verdict = "anisotropic"
|
|
else:
|
|
verdict = "mixed"
|
|
print(f"{v:<18}{q:>10}{iso:>18.3f}{xy:>+17.6f}{verdict:>18}")
|
|
print()
|
|
|
|
# ─── TEST 4: Control (mid_price should be near-zero) ───
|
|
print("=== TEST 4: CONTROL (mid_price should produce near-zero response) ===")
|
|
sub = var_df[var_df["var"] == "mid_price"]
|
|
if not sub.empty:
|
|
for q in ["q01", "q50", "q99"]:
|
|
ss = sub[sub["quantile"] == q]
|
|
if ss.empty:
|
|
continue
|
|
strength = float(ss["strength"].iloc[0])
|
|
sn = float(ss["asymmetry_sn"].mean())
|
|
peak = float(ss["asymmetry_peak_delta"].mean())
|
|
print(f" mid_price {q}: strength={strength:+.5f} sn={sn:.3f} peakΔ={peak:+.4f}")
|
|
print(" Interpretation: SN should track strength (small strength → small SN). If "
|
|
"mid_price q50 has SN>>2.0, the encoding is wrong.\n")
|
|
|
|
# ─── TEST 5: Calibration regression ───
|
|
print("=== TEST 5: CALIBRATION REGRESSION (lattice impulse response vs amplitude) ===")
|
|
if not cal_df.empty:
|
|
cal_grp = cal_df.groupby("strength").agg(
|
|
asym_peak=("asymmetry_peak_delta", "mean"),
|
|
asym_sn =("asymmetry_sn", "mean"),
|
|
xy_peak =("stress_xy_peak_delta", "mean"),
|
|
half =("asymmetry_halflife_s", "mean"),
|
|
).reset_index().sort_values("strength")
|
|
print(f"{'strength':>12}{'asym_peakΔ':>14}{'asym_sn':>12}{'xy_peakΔ':>14}{'halflife_s':>13}")
|
|
for _, r in cal_grp.iterrows():
|
|
print(f"{r['strength']:>+12.4f}{r['asym_peak']:>+14.4f}{r['asym_sn']:>12.3f}"
|
|
f"{r['xy_peak']:>+14.6f}{r['half']!s:>13}")
|
|
|
|
# Fit asym_peak ~ strength (linear)
|
|
s = cal_grp["strength"].values
|
|
p = cal_grp["asym_peak"].values
|
|
if len(s) >= 3:
|
|
coef = np.polyfit(s, p, 1)
|
|
r2_num = np.sum((p - (coef[0]*s + coef[1]))**2)
|
|
r2_den = np.sum((p - p.mean())**2)
|
|
r2 = 1 - (r2_num / r2_den) if r2_den > 0 else 0
|
|
print(f"\n asym_peak = {coef[0]:+.4f} * strength + {coef[1]:+.4f} R²={r2:.4f}")
|
|
print(f" → calibration slope: {coef[0]:+.4f} (asym units per unit strength)")
|
|
print()
|
|
|
|
# For each variable, predict expected peak from strength * cal_slope,
|
|
# compare to observed. Residual = variable-specific structure.
|
|
if len(s) >= 3:
|
|
slope, intercept = coef
|
|
print(" PER-VARIABLE EXCESS RESPONSE (observed_peak - calibration_predicted):")
|
|
print(f" {'variable':<18}{'quantile':>10}{'strength':>12}{'predicted':>12}"
|
|
f"{'observed':>12}{'residual':>12}{'rel_excess%':>13}")
|
|
for v in meta["variables"]:
|
|
sub = var_df[var_df["var"] == v]
|
|
for q in ["q01", "q50", "q99"]:
|
|
ss = sub[sub["quantile"] == q]
|
|
if ss.empty:
|
|
continue
|
|
st = float(ss["strength"].iloc[0])
|
|
pred = slope * st + intercept
|
|
obs = float(ss["asymmetry_peak_delta"].mean())
|
|
resid = obs - pred
|
|
rel = (resid / abs(pred) * 100) if abs(pred) > 1e-6 else float("nan")
|
|
print(f" {v:<18}{q:>10}{st:>+12.5f}{pred:>+12.4f}"
|
|
f"{obs:>+12.4f}{resid:>+12.4f}{rel:>+13.1f}")
|
|
print("\n Interpretation: |relative excess| < 20% → variable response is "
|
|
"explained by amplitude alone (no special structure). |excess| > 50% → "
|
|
"variable has a signature beyond pure amplitude (interesting/worth using).")
|
|
else:
|
|
print(" no calibration records found\n")
|
|
|
|
# ─── Per-variable summary table ───
|
|
print("\n=== PER-VARIABLE SUMMARY (q99 vs q01, mean of reps) ===")
|
|
print(f"{'variable':<18}{'q99_strength':>14}{'q99_peakΔ':>12}{'q99_sn':>10}"
|
|
f"{'q01_strength':>14}{'q01_peakΔ':>12}{'q01_sn':>10}{'directional':>13}")
|
|
summary_rows = []
|
|
for v in meta["variables"]:
|
|
sub = var_df[var_df["var"] == v]
|
|
q01 = sub[sub["quantile"] == "q01"]
|
|
q99 = sub[sub["quantile"] == "q99"]
|
|
if q01.empty or q99.empty:
|
|
continue
|
|
s01, s99 = float(q01["strength"].iloc[0]), float(q99["strength"].iloc[0])
|
|
p01, p99 = float(q01["asymmetry_peak_delta"].mean()), float(q99["asymmetry_peak_delta"].mean())
|
|
sn01, sn99 = float(q01["asymmetry_sn"].mean()), float(q99["asymmetry_sn"].mean())
|
|
direc = "yes" if np.sign(p01) != np.sign(p99) and np.sign(s01) != np.sign(s99) else "no"
|
|
print(f"{v:<18}{s99:>+14.5f}{p99:>+12.4f}{sn99:>10.2f}"
|
|
f"{s01:>+14.5f}{p01:>+12.4f}{sn01:>10.2f}{direc:>13}")
|
|
summary_rows.append({"var": v, "s99": s99, "p99": p99, "sn99": sn99,
|
|
"s01": s01, "p01": p01, "sn01": sn01, "directional": direc})
|
|
print()
|
|
|
|
# save aggregated CSV for further work
|
|
df.to_csv(run / "_aggregated.csv", index=False)
|
|
print(f"aggregated CSV: {run/'_aggregated.csv'}")
|
|
print("\n=== DONE ===")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|