Files
resonance-engine/_analyze_traj.py
T
2026-06-06 20:34:31 +07:00

184 lines
7.3 KiB
Python

"""Analyze trajectory experiment: are the 6 variables distinguishable in lattice trajectory-space?
For each variable: load its trace.parquet, restrict to t in [PRE, PRE+SEQ], i.e. while pulses were active.
Compute trajectory in (asymmetry, coherence, stress_xy, stress_xx, stress_yy, vorticity_mean) space.
Compare pairwise distances between variable trajectories vs. self-consistency.
Output:
- per-variable summary table (mean/std/range during pulse window)
- pairwise trajectory distance matrix
- decision: are trajectories distinguishable above field noise?
"""
import json
from pathlib import Path
import numpy as np
import pandas as pd
OUT_DIR = Path("/mnt/d/Resonance_Engine/traj/20260606T193626")
META = json.loads((OUT_DIR / "meta.json").read_text())
PRE = META["pre_record_s"]
N = META["n_pulses"]
SPACING = META["pulse_spacing_s"]
SEQ_END = PRE + N * SPACING # end of pulse window in rel_t
VARS = META["vars"]
CHANNELS = ["asymmetry", "coherence", "stress_xx", "stress_yy", "stress_xy",
"vorticity_mean", "vel_mean", "vel_var"]
print(f"=== run {META['run_id']} pulse window [{PRE:.0f}s .. {SEQ_END:.0f}s] ===\n")
# load all traces
traces = {}
for v in VARS:
df = pd.read_parquet(OUT_DIR / f"{v}.parquet")
traces[v] = df
# helper: select pulse-active window
def pulse_window(df):
return df[(df.rel_t >= PRE) & (df.rel_t <= SEQ_END)]
def pre_window(df):
return df[df.rel_t < PRE]
# ---- per-variable stats during pulse window ----
print("=== per-variable telemetry during pulse window ===")
print(f"{'var':18s} {'n':>4} {'asym_mean':>10} {'asym_std':>9} {'asym_max':>9} "
f"{'sxy_mean':>10} {'sxy_std':>10} {'sxy_amp':>10} "
f"{'coh_mean':>9} {'coh_min':>9} {'vort_mean':>10}")
print("-" * 130)
summary = {}
for v in VARS:
pw = pulse_window(traces[v])
s = {
"n": len(pw),
"asym_mean": pw.asymmetry.mean(),
"asym_std": pw.asymmetry.std(),
"asym_max": pw.asymmetry.max(),
"asym_min": pw.asymmetry.min(),
"sxy_mean": pw.stress_xy.mean(),
"sxy_std": pw.stress_xy.std(),
"sxy_amp": pw.stress_xy.max() - pw.stress_xy.min(),
"sxx_mean": pw.stress_xx.mean(),
"syy_mean": pw.stress_yy.mean(),
"coh_mean": pw.coherence.mean(),
"coh_min": pw.coherence.min(),
"vort_mean": pw.vorticity_mean.mean(),
"vort_std": pw.vorticity_mean.std(),
"vel_var_mean": pw.vel_var.mean(),
}
summary[v] = s
print(f"{v:18s} {s['n']:>4d} {s['asym_mean']:>10.4f} {s['asym_std']:>9.4f} "
f"{s['asym_max']:>9.4f} "
f"{s['sxy_mean']:>+10.6f} {s['sxy_std']:>10.6f} {s['sxy_amp']:>10.6f} "
f"{s['coh_mean']:>9.5f} {s['coh_min']:>9.5f} {s['vort_mean']:>10.5f}")
# Compute spread across variables (between-variable variance)
print("\n=== spread ACROSS variables (between-variable std / within-variable std) ===")
between = {}
within = {}
for ch in ["asymmetry", "stress_xy", "stress_xx", "stress_yy", "coherence",
"vorticity_mean", "vel_var"]:
var_means = [pulse_window(traces[v])[ch].mean() for v in VARS]
var_stds = [pulse_window(traces[v])[ch].std() for v in VARS]
between[ch] = np.std(var_means)
within[ch] = np.mean(var_stds)
ratio = between[ch] / within[ch] if within[ch] > 0 else 0
print(f" {ch:18s} between_std={between[ch]:.6f} within_std={within[ch]:.6f} "
f"ratio={ratio:.3f}")
# Pairwise distance in normalized multi-channel space
print("\n=== pairwise trajectory distance ===")
# Build a feature vector per variable: time series of each channel during pulse window,
# resampled to common length, then concatenated and normalized per channel by the
# WITHIN-variable std (so we measure cross-variable difference in units of own noise)
NRESAMPLE = 100
feature_vecs = {}
for v in VARS:
pw = pulse_window(traces[v]).sort_values("rel_t")
# resample to NRESAMPLE points uniformly across [PRE, SEQ_END]
times = np.linspace(PRE, SEQ_END, NRESAMPLE)
vec = []
for ch in ["asymmetry", "stress_xy", "stress_xx", "stress_yy", "coherence",
"vorticity_mean", "vel_var"]:
resampled = np.interp(times, pw.rel_t.values, pw[ch].values)
# normalize by within-channel std across all variables
if within[ch] > 0:
resampled = (resampled - resampled.mean()) / within[ch]
vec.append(resampled)
feature_vecs[v] = np.concatenate(vec)
print(f"{'':18s} " + " ".join(f"{v[:12]:>12s}" for v in VARS))
for v1 in VARS:
row = []
for v2 in VARS:
d = np.linalg.norm(feature_vecs[v1] - feature_vecs[v2])
row.append(d)
print(f" {v1:16s} " + " ".join(f"{d:>12.2f}" for d in row))
# Are the distances large enough to call distinguishable?
off_diag = []
for i, v1 in enumerate(VARS):
for j, v2 in enumerate(VARS):
if i < j:
off_diag.append(np.linalg.norm(feature_vecs[v1] - feature_vecs[v2]))
print(f"\n pairwise distances: min={min(off_diag):.2f} max={max(off_diag):.2f} "
f"mean={np.mean(off_diag):.2f}")
# Sanity baseline: distance between two halves of the SAME variable's trace
print("\n=== sanity: distance within same variable (split halves of pulse window) ===")
for v in VARS:
pw = pulse_window(traces[v]).sort_values("rel_t")
mid = (PRE + SEQ_END) / 2
a = pw[pw.rel_t < mid]
b = pw[pw.rel_t >= mid]
if len(a) < 5 or len(b) < 5: continue
times_a = np.linspace(PRE, mid, NRESAMPLE)
times_b = np.linspace(mid, SEQ_END, NRESAMPLE)
vec_a, vec_b = [], []
for ch in ["asymmetry", "stress_xy", "stress_xx", "stress_yy", "coherence",
"vorticity_mean", "vel_var"]:
ra = np.interp(times_a, a.rel_t.values, a[ch].values)
rb = np.interp(times_b, b.rel_t.values, b[ch].values)
if within[ch] > 0:
ra = (ra - ra.mean()) / within[ch]
rb = (rb - rb.mean()) / within[ch]
vec_a.append(ra)
vec_b.append(rb)
d_self = np.linalg.norm(np.concatenate(vec_a) - np.concatenate(vec_b))
print(f" {v:18s} self_half_distance={d_self:.2f}")
print()
print("=== INTERPRETATION ===")
mean_cross = np.mean(off_diag)
# clean within-variable half-distance
half_dists = []
for v in VARS:
pw = pulse_window(traces[v]).sort_values("rel_t")
mid = (PRE + SEQ_END) / 2
a = pw[pw.rel_t < mid]
b = pw[pw.rel_t >= mid]
if len(a) < 5 or len(b) < 5:
continue
times_a = np.linspace(PRE, mid, NRESAMPLE)
times_b = np.linspace(mid, SEQ_END, NRESAMPLE)
vec_a, vec_b = [], []
for ch in ["asymmetry", "stress_xy", "stress_xx", "stress_yy", "coherence",
"vorticity_mean", "vel_var"]:
ra = np.interp(times_a, a.rel_t.values, a[ch].values)
rb = np.interp(times_b, b.rel_t.values, b[ch].values)
if within[ch] > 0:
ra = (ra - ra.mean()) / within[ch]
rb = (rb - rb.mean()) / within[ch]
vec_a.append(ra)
vec_b.append(rb)
half_dists.append(np.linalg.norm(np.concatenate(vec_a) - np.concatenate(vec_b)))
mean_self = np.mean(half_dists)
print(f" mean cross-variable distance: {mean_cross:.2f}")
print(f" mean within-variable half-distance: {mean_self:.2f}")
ratio = mean_cross / mean_self if mean_self > 0 else float('inf')
print(f" RATIO: {ratio:.2f}x (>1 = variables more different from each other")
print(f" than each is from itself between halves)")