184 lines
7.3 KiB
Python
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)")
|