"""_analyze_overnight.py — analyze overnight_v4 results. Reads /mnt/d/Resonance_Engine/traj/overnight_/ and produces: 1. Per-variable mean/std of each channel during pulses, averaged over 3 reps. 2. CRITICAL: variable ranking PER rep. If the ranking is preserved across all 3 reps with different injection orderings -> real per-variable effect. If ranking tracks injection position -> time-drift artifact. 3. Pairwise variable trajectory distances (Euclidean over resampled trace), compared to within-variable self-distances across reps. 4. Decay shape: tail-decay (last 30s of pulses vs final 60s post) per variable. Usage: python3 _analyze_overnight.py python3 _analyze_overnight.py latest """ from __future__ import annotations import json, sys from pathlib import Path import numpy as np import pandas as pd from scipy.stats import spearmanr ROOT = Path("/mnt/d/Resonance_Engine/traj") CHANNELS = ["asymmetry", "coherence", "stress_xx", "stress_yy", "stress_xy", "vorticity_mean", "vel_mean", "vel_max", "vel_var"] VARS = ["mid_price", "signed_flow_usd", "trade_count", "large_print_cnt", "wallet_entropy", "vwap_drift"] def find_run(arg: str) -> Path: if arg == "latest": runs = sorted([p for p in ROOT.glob("overnight_*") if p.is_dir()]) if not runs: sys.exit("no overnight_* runs in " + str(ROOT)) return runs[-1] p = ROOT / f"overnight_{arg}" if not arg.startswith("overnight_") else ROOT / arg if not p.exists(): sys.exit(f"missing: {p}") return p def load_rep(rep_dir: Path) -> dict[str, pd.DataFrame]: out = {} for v in VARS: f = rep_dir / f"{v}.parquet" if f.exists(): out[v] = pd.read_parquet(f) return out def pulse_window_stats(df: pd.DataFrame, rel_t_col: str = "rel_t") -> dict: """Stats during pulse phase (rel_t between 0 and end-of-pulses) per channel.""" # exclude pre (rel_t<0) and the very-far post-tail; keep pulse + first 5s post if rel_t_col not in df.columns: return {} pulse_end = df[rel_t_col].max() - 55 # approx end of pulses mask = (df[rel_t_col] >= 0) & (df[rel_t_col] <= pulse_end) sub = df[mask] out = {} for ch in CHANNELS: if ch in sub.columns: out[ch + "_mean"] = float(sub[ch].mean()) out[ch + "_std"] = float(sub[ch].std()) out[ch + "_max"] = float(sub[ch].max()) out[ch + "_min"] = float(sub[ch].min()) return out def decay_stats(df: pd.DataFrame, rel_t_col: str = "rel_t") -> dict: """Compare last 20s of pulse phase vs last 20s of post (decay magnitude).""" if rel_t_col not in df.columns or len(df) < 10: return {} t_max = df[rel_t_col].max() pulse_end = t_max - 55 # 60s post window approx tail_pulse_mask = (df[rel_t_col] >= pulse_end - 20) & (df[rel_t_col] <= pulse_end) tail_post_mask = (df[rel_t_col] >= t_max - 20) & (df[rel_t_col] <= t_max) tp = df[tail_pulse_mask] pp = df[tail_post_mask] out = {} for ch in ["asymmetry", "coherence", "stress_xy"]: if ch in df.columns and len(tp) > 2 and len(pp) > 2: out[ch + "_decay"] = float(tp[ch].mean() - pp[ch].mean()) return out def resample_trajectory(df: pd.DataFrame, channel: str, n_pts: int = 200) -> np.ndarray: """Resample one channel on a uniform rel_t grid.""" if "rel_t" not in df.columns or channel not in df.columns: return np.full(n_pts, np.nan) d = df.dropna(subset=["rel_t", channel]).sort_values("rel_t") if len(d) < 4: return np.full(n_pts, np.nan) t0, t1 = d["rel_t"].min(), d["rel_t"].max() grid = np.linspace(t0, t1, n_pts) return np.interp(grid, d["rel_t"].values, d[channel].values) def main(): run_id_arg = sys.argv[1] if len(sys.argv) > 1 else "latest" run_dir = find_run(run_id_arg) print(f"=== analyzing {run_dir.name} ===\n") meta_p = run_dir / "meta.json" if meta_p.exists(): meta = json.loads(meta_p.read_text()) print(f"run_id={meta.get('run_id')} started={meta.get('started')}") print(f"vars={meta.get('vars')}") print(f"orderings_planned={meta.get('orderings', 'n/a')}") print() # baseline base_stats_p = run_dir / "baseline_clean_stats.json" if base_stats_p.exists(): bs = json.loads(base_stats_p.read_text()) print("=== verified-clean baseline sigma ===") for k, v in bs.items(): if isinstance(v, float): print(f" {k:30s} {v:.6f}") else: print(f" {k:30s} {v}") print() sigma = {k.replace("sigma_", ""): v for k, v in bs.items() if k.startswith("sigma_")} else: sigma = {} print("(no baseline_clean_stats.json found)\n") # ----- load all reps ----- rep_dirs = sorted([p for p in run_dir.iterdir() if p.is_dir() and p.name.startswith("rep_")]) if not rep_dirs: sys.exit("no rep_* dirs found yet") print(f"found {len(rep_dirs)} reps: {[p.name for p in rep_dirs]}\n") # per-rep, per-var stats rep_stats = {} # rep_name -> var -> stats dict rep_order = {} # rep_name -> var -> int (order in this rep) for rd in rep_dirs: rep_stats[rd.name] = {} # try to read order from a meta file order_p = rd / "rep_meta.json" if order_p.exists(): rm = json.loads(order_p.read_text()) for i, v in enumerate(rm.get("order", [])): rep_order.setdefault(rd.name, {})[v] = i reps = load_rep(rd) for v, df in reps.items(): rep_stats[rd.name][v] = pulse_window_stats(df) rep_stats[rd.name][v].update(decay_stats(df)) # ----- table 1: per-var per-rep asymmetry mean ----- print("=== ASYMMETRY mean during pulse phase (per rep, per var) ===") header = f"{'var':<20}" + "".join(f"{rd.name:>12}" for rd in rep_dirs) + f"{'mean':>10}{'std':>8}" print(header) print("-" * len(header)) rows = [] for v in VARS: vals = [] for rd in rep_dirs: s = rep_stats[rd.name].get(v, {}) vals.append(s.get("asymmetry_mean", np.nan)) m = np.nanmean(vals) sd = np.nanstd(vals) rows.append((v, vals, m, sd)) line = f"{v:<20}" + "".join(f"{x:>12.3f}" if not np.isnan(x) else f"{'--':>12}" for x in vals) line += f"{m:>10.3f}{sd:>8.3f}" print(line) print() # ----- table 2: cross-rep ranking preservation (THE critical test) ----- print("=== RANKING preservation test (variable effect vs drift artifact) ===") rankings = {} for rd in rep_dirs: valid = [(v, rep_stats[rd.name].get(v, {}).get("asymmetry_mean", np.nan)) for v in VARS] valid = [vv for vv in valid if not np.isnan(vv[1])] valid.sort(key=lambda x: x[1]) rankings[rd.name] = [vv[0] for vv in valid] print(f" {rd.name}: low->high asym = {' < '.join(rankings[rd.name])}") print() # Spearman rank correlation between all rep pairs if len(rep_dirs) >= 2: print(" Spearman rank corr between rep pairs (1.0 = perfect agreement, 0 = random):") all_vars_sorted = sorted(VARS) for i in range(len(rep_dirs)): for j in range(i + 1, len(rep_dirs)): ri = [rep_stats[rep_dirs[i].name].get(v, {}).get("asymmetry_mean", np.nan) for v in all_vars_sorted] rj = [rep_stats[rep_dirs[j].name].get(v, {}).get("asymmetry_mean", np.nan) for v in all_vars_sorted] mask = ~(np.isnan(ri) | np.isnan(rj)) if mask.sum() >= 3: corr, p = spearmanr(np.array(ri)[mask], np.array(rj)[mask]) print(f" {rep_dirs[i].name} vs {rep_dirs[j].name}: rho={corr:+.3f} p={p:.3f}") print() # Also test: does asym_mean correlate with INJECTION ORDER (drift artifact)? if rep_order: print(" ORDER-POSITION vs asymmetry_mean (high pos corr = time-drift artifact):") for rd in rep_dirs: if rd.name not in rep_order: continue pairs = [] for v in VARS: pos = rep_order[rd.name].get(v) val = rep_stats[rd.name].get(v, {}).get("asymmetry_mean", np.nan) if pos is not None and not np.isnan(val): pairs.append((pos, val)) if len(pairs) >= 3: pos = np.array([p[0] for p in pairs]) val = np.array([p[1] for p in pairs]) r, pp = spearmanr(pos, val) print(f" {rd.name}: rho(order_pos, asym_mean) = {r:+.3f} p={pp:.3f}") print() # ----- table 3: per-channel between/within variance ratio (across-rep means) ----- print("=== Between-var vs within-var std ratio (per channel, using rep-averaged values) ===") print(" ratio > 1 means variables differ from each other more than reps of same var disagree") print(f"{'channel':<22}{'between_std':>14}{'within_std':>14}{'ratio':>10}") for ch in CHANNELS: per_var_means = [] per_var_within = [] for v in VARS: vals = [rep_stats[rd.name].get(v, {}).get(ch + "_mean", np.nan) for rd in rep_dirs] vals = [x for x in vals if not np.isnan(x)] if len(vals) >= 1: per_var_means.append(np.mean(vals)) if len(vals) >= 2: per_var_within.append(np.std(vals)) if len(per_var_means) >= 2 and per_var_within: bs = float(np.std(per_var_means)) ws = float(np.mean(per_var_within)) ratio = bs / ws if ws > 1e-12 else float('inf') print(f"{ch:<22}{bs:>14.4f}{ws:>14.4f}{ratio:>10.2f}") print() # ----- table 4: pairwise variable trajectory distances on asymmetry ----- print("=== Pairwise variable trajectory distance (asymmetry, rep-averaged) ===") # Build rep-averaged trajectory per var avg_traj = {} for v in VARS: traces = [] for rd in rep_dirs: f = rd / f"{v}.parquet" if f.exists(): df = pd.read_parquet(f) t = resample_trajectory(df, "asymmetry", 200) if not np.all(np.isnan(t)): traces.append(t) if traces: avg_traj[v] = np.nanmean(np.stack(traces), axis=0) keys = list(avg_traj.keys()) if len(keys) >= 2: print(f"{'':<20}" + "".join(f"{k[:10]:>12}" for k in keys)) for i, ka in enumerate(keys): row = f"{ka:<20}" for kb in keys: if ka == kb: row += f"{'-':>12}" else: d = float(np.linalg.norm(avg_traj[ka] - avg_traj[kb])) row += f"{d:>12.3f}" print(row) print() # Self-distance: distance between rep-pair trajectories for same var print(" Self-distance (cross-rep traj distance for same var, sanity):") for v in VARS: ts = [] for rd in rep_dirs: f = rd / f"{v}.parquet" if f.exists(): df = pd.read_parquet(f) t = resample_trajectory(df, "asymmetry", 200) if not np.all(np.isnan(t)): ts.append(t) if len(ts) >= 2: ds = [] for i in range(len(ts)): for j in range(i + 1, len(ts)): ds.append(float(np.linalg.norm(ts[i] - ts[j]))) if ds: print(f" {v:<20} mean_self_dist={np.mean(ds):.3f} n_pairs={len(ds)}") print("\n=== DONE ===") if __name__ == "__main__": main()