Files
resonance-engine/inject_variable_isolation.py
T
2026-06-06 21:34:30 +07:00

510 lines
20 KiB
Python

"""inject_variable_isolation.py — per-variable signal characterisation instrument.
This is the experiment Claude Desktop specified, with two physical-coherence fixes:
1. Equal strength + same location = 6 identical injections with different labels.
So strength is derived from each variable's own distribution at fixed
quantiles (p99, p50, p01). "Characterise variable" then means: when this
database column says an extreme reading, how much energy does it naturally
deliver, and what's the lattice response shape?
2. Calibration runs added: raw +/- 0.5, 0.25, 0.1 with no variable identity.
This is the lattice's raw impulse response curve. Every variable response
is compared against it. If mid_price's p99 response matches a raw +/-0.05
calibration scaled, mid_price has no special signature beyond amplitude.
Baseline: stability-based (verified low-variance plateau), NOT the engineered
12.0-13.0 band. The lattice is parked at the asym=33 plateau due to accumulated
forcing; we can't reach 12.x without a daemon restart (RESONANCE-owned, forbidden).
What matters: clean steady state, whatever the level, with known sigma.
Output: /mnt/d/Resonance_Engine/traj/varisol_<RUN_ID>/
meta.json
baseline_clean_stats.json
injections.jsonl (one record per injection, see below)
progress.log
"""
from __future__ import annotations
import glob, json, threading, time
from collections import deque
from pathlib import Path
import numpy as np
import pandas as pd
import pyarrow.parquet as pq
import zmq
# ─────── config ───────
DATA_DAY_WSL = "/mnt/d/PaperTrader/research/hl_data/minutes/20260601"
COIN = "BTC"
VARIABLES = [
"signed_flow_usd", # directional pressure
"wallet_entropy", # participation structure
"vwap_drift", # price vs flow agreement
"trade_count", # activity volume
"large_print_cnt", # whale activity
"mid_price", # price level (control — expected small natural strength)
]
# Per-variable injection: pick three quantile points from this var's
# full-day distribution; convert to strength via z/3 * STR_CAP, same rule
# v3 used (so results are commensurable across experiments).
QUANTILES = [0.01, 0.50, 0.99] # p1 / p50 / p99
STR_CAP = 0.30
# Raw calibration: lattice impulse response at fixed amplitudes, no var label
CALIBRATION_STRENGTHS = [-0.50, -0.25, -0.10, +0.10, +0.25, +0.50]
# Per-injection: 3 repeats each
N_REPEATS = 3
# Baseline (stability-based)
STABILITY_PROBE_S = 1800 # 30 min stability window for initial baseline
STABILITY_ASYM_STD = 0.6
STABILITY_ASYM_SLOPE = 0.005 # asym change per second
MAX_WAIT_S = 6 * 3600
BASELINE_MEASURE_S = 600 # 10 min of clean stats once verified stable
# Decay gate (between injections — return to baseline plateau)
DECAY_PROBE_S = 300 # 5 min stability check between injections
DECAY_ASYM_STD = 0.8 # slightly looser than initial
DECAY_ASYM_TOL = 1.5 # allow plateau drift of +/-1.5 from initial baseline mean
DECAY_TIMEOUT_S = 600 # 10 min hard cap per decay wait
# Per-injection telemetry windows
PRE_RECORD_S = 7 # ~50 frames at ~7.5 Hz
POST_RECORD_S = 40 # ~300 frames at ~7.5 Hz
# Injection point (fixed for all — only strength varies)
INJECT_X = 512.0
INJECT_Y = 512.0
INJECT_SIG = 32.0
TEL_ADDR = "tcp://127.0.0.1:5556"
CMD_ADDR = "tcp://127.0.0.1:5557"
CHANNELS = ["asymmetry", "coherence", "stress_xx", "stress_yy", "stress_xy",
"vorticity_mean", "vel_mean", "vel_max", "vel_var"]
RUN_ID = time.strftime("%Y%m%dT%H%M%S")
OUT_DIR = Path(f"/mnt/d/Resonance_Engine/traj/varisol_{RUN_ID}")
OUT_DIR.mkdir(parents=True, exist_ok=True)
PROGRESS = OUT_DIR / "progress.log"
JSONL = OUT_DIR / "injections.jsonl"
def log(msg: str) -> None:
line = f"[{time.strftime('%Y-%m-%dT%H:%M:%S')}] {msg}"
print(line, flush=True)
with PROGRESS.open("a") as f:
f.write(line + "\n")
# ─────── ZMQ telemetry subscriber ───────
class TelSub:
def __init__(self, addr: str):
self.ctx = zmq.Context.instance()
self.sock = self.ctx.socket(zmq.SUB)
self.sock.connect(addr)
self.sock.setsockopt(zmq.SUBSCRIBE, b"")
self.sock.setsockopt(zmq.RCVHWM, 5000)
self.buf: deque = deque(maxlen=10000)
self._stop = threading.Event()
self._t = threading.Thread(target=self._run, daemon=True)
self._t.start()
def _run(self):
while not self._stop.is_set():
try:
if self.sock.poll(200):
raw = self.sock.recv_string(zmq.NOBLOCK)
msg = json.loads(raw)
msg["_recv_wall"] = time.time()
self.buf.append(msg)
except zmq.Again:
continue
except Exception:
continue
def since(self, t0: float) -> list[dict]:
return [m for m in list(self.buf) if m.get("_recv_wall", 0) >= t0]
def stop(self):
self._stop.set()
# ─────── data loading ───────
def load_btc_day() -> pd.DataFrame:
files = sorted(glob.glob(str(Path(DATA_DAY_WSL) / "*.parquet")))
if not files:
raise RuntimeError(f"no parquet files in {DATA_DAY_WSL}")
dfs = [pd.read_parquet(f) for f in files]
df = pd.concat(dfs, ignore_index=True)
df = df[df.coin == COIN].sort_values("minute").reset_index(drop=True)
log(f"loaded {len(df)} minutes of {COIN} data from {len(files)} parquet shards")
return df
def quantile_strengths(df: pd.DataFrame, var: str) -> dict[str, float]:
"""For this var, find the value at each quantile, convert to strength
via z-score / 3 * STR_CAP (consistent with v1/v3 encoding so cross-experiment
comparisons are valid). Returns {label: strength}."""
s = df[var].astype(float)
mu = float(s.mean())
sd = float(s.std())
out = {}
for q in QUANTILES:
val = float(s.quantile(q))
if sd == 0:
strength = 0.0
else:
z = (val - mu) / sd
z_clip = float(np.clip(z, -3.0, 3.0))
strength = (z_clip / 3.0) * STR_CAP
label = f"q{int(q*100):02d}"
out[label] = {
"quantile": q,
"raw_value": val,
"mean": mu,
"std": sd,
"z_clipped": z_clip if sd > 0 else 0.0,
"strength": strength,
}
return out
# ─────── stability detection ───────
def wait_for_stability(tel: TelSub, probe_s: float, std_thresh: float,
slope_thresh: float, timeout_s: float,
label: str, anchor_mean: float | None = None,
anchor_tol: float | None = None) -> dict | None:
"""Wait until the asym signal is stable over `probe_s` window.
Returns dict with baseline stats, or None on timeout.
If anchor_mean+anchor_tol given, also requires |current_mean - anchor| <= tol."""
start = time.time()
last_status = 0.0
while True:
now = time.time()
if now - start > timeout_s:
log(f" TIMEOUT: {label} after {now-start:.0f}s")
return None
probe = tel.since(now - probe_s)
if len(probe) < 30:
time.sleep(5)
continue
ts = np.array([m["_recv_wall"] for m in probe])
a = np.array([m["asymmetry"] for m in probe])
a_mean = float(a.mean())
a_std = float(a.std())
rel_ts = ts - ts[0]
slope = float(np.polyfit(rel_ts, a, 1)[0]) if rel_ts[-1] > 0 else 0.0
stable = (a_std < std_thresh) and (abs(slope) < slope_thresh)
if anchor_mean is not None and anchor_tol is not None:
stable = stable and (abs(a_mean - anchor_mean) <= anchor_tol)
if stable:
log(f" {label} STABLE — asym mean={a_mean:.3f} std={a_std:.3f} "
f"slope={slope:+.5f}/s waited={now-start:.0f}s")
return {
"asym_mean": a_mean,
"asym_std": a_std,
"asym_slope_per_s": slope,
"wait_s": now - start,
}
if now - last_status > 60:
extra = ""
if anchor_mean is not None:
extra = f" delta_anchor={a_mean-anchor_mean:+.2f}"
log(f" {label} not stable: asym mean={a_mean:.3f} std={a_std:.3f} "
f"slope={slope:+.5f}/s waited={now-start:.0f}s{extra}")
last_status = now
time.sleep(10)
# ─────── window stats ───────
def window_stats(samples: list[dict]) -> dict:
if not samples:
return {}
out = {}
for ch in CHANNELS:
vals = [m[ch] for m in samples if ch in m]
if not vals:
continue
arr = np.array(vals, dtype=float)
out[f"{ch}_mean"] = float(arr.mean())
out[f"{ch}_std"] = float(arr.std())
out[f"{ch}_min"] = float(arr.min())
out[f"{ch}_max"] = float(arr.max())
out["n"] = len(samples)
return out
def response_signature(pre_stats: dict, post_samples: list[dict],
pre_anchor_t: float) -> dict:
"""From pre baseline stats + post samples, derive the signature."""
sig = {}
if not post_samples:
return sig
# per-channel: peak abs delta from pre_mean, time-of-peak, halflife
for ch in ["asymmetry", "coherence", "stress_xx", "stress_yy", "stress_xy",
"vel_mean", "vorticity_mean"]:
if f"{ch}_mean" not in pre_stats:
continue
base = pre_stats[f"{ch}_mean"]
sd = max(pre_stats.get(f"{ch}_std", 0.0), 1e-9)
ts_rel = []
deltas = []
for m in post_samples:
if ch in m:
deltas.append(float(m[ch]) - base)
ts_rel.append(m["_recv_wall"] - pre_anchor_t)
if not deltas:
continue
deltas_arr = np.array(deltas)
ts_arr = np.array(ts_rel)
abs_d = np.abs(deltas_arr)
idx_peak = int(np.argmax(abs_d))
peak_delta = float(deltas_arr[idx_peak])
peak_time_s = float(ts_arr[idx_peak])
sn = abs(peak_delta) / sd
# halflife: first time after peak where |delta| <= peak/2
half_time_s = None
target = abs(peak_delta) / 2.0
for i in range(idx_peak, len(deltas_arr)):
if abs(deltas_arr[i]) <= target:
half_time_s = float(ts_arr[i] - peak_time_s)
break
sig[f"{ch}_peak_delta"] = peak_delta
sig[f"{ch}_peak_time_s"] = peak_time_s
sig[f"{ch}_sn"] = sn
sig[f"{ch}_halflife_s"] = half_time_s
sig[f"{ch}_end_delta"] = float(deltas_arr[-1])
# stress symmetry: stress_xx vs stress_yy peak delta ratio
if "stress_xx_peak_delta" in sig and "stress_yy_peak_delta" in sig:
xx = abs(sig["stress_xx_peak_delta"])
yy = abs(sig["stress_yy_peak_delta"])
sig["stress_iso_ratio"] = (min(xx, yy) / max(xx, yy)) if max(xx, yy) > 0 else 1.0
return sig
# ─────── injection ───────
def fire_injection(pub: zmq.Socket, strength: float) -> dict:
payload = {
"cmd": "inject_density",
"x": INJECT_X,
"y": INJECT_Y,
"sigma": INJECT_SIG,
"strength": float(strength),
}
wall_send = time.time()
pub.send_string(json.dumps(payload))
return {"wall_send": wall_send, "payload": payload}
def run_one_injection(tel: TelSub, pub: zmq.Socket, label: dict,
strength: float, baseline_anchor: float) -> dict:
log(f" INJECT {label} strength={strength:+.4f}")
# PRE
pre_start = time.time()
while time.time() - pre_start < PRE_RECORD_S:
time.sleep(0.5)
pre_samples = tel.since(pre_start)
pre_stats = window_stats(pre_samples)
log(f" pre n={pre_stats.get('n',0)} asym mean={pre_stats.get('asymmetry_mean',float('nan')):.3f} "
f"std={pre_stats.get('asymmetry_std',float('nan')):.3f}")
# INJECT
fire = fire_injection(pub, strength)
inject_wall = fire["wall_send"]
# POST
while time.time() - inject_wall < POST_RECORD_S:
time.sleep(0.5)
post_samples = tel.since(inject_wall)
post_stats = window_stats(post_samples)
sig = response_signature(pre_stats, post_samples, inject_wall)
log(f" post n={post_stats.get('n',0)} "
f"asym peak_delta={sig.get('asymmetry_peak_delta',float('nan')):+.3f} "
f"sn={sig.get('asymmetry_sn',float('nan')):.2f} "
f"halflife={sig.get('asymmetry_halflife_s')}s "
f"stress_xy peak_delta={sig.get('stress_xy_peak_delta',float('nan')):+.6f}")
record = {
"run_id": RUN_ID,
"label": label,
"strength": float(strength),
"wall_inject": inject_wall,
"iso_inject": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(inject_wall)),
"pre_stats": pre_stats,
"post_stats": post_stats,
"signature": sig,
"inject_xy_sigma": [INJECT_X, INJECT_Y, INJECT_SIG],
"baseline_anchor": baseline_anchor,
}
# also persist the post-window samples (trimmed to channels we care about)
record["post_trace"] = [
{**{k: m.get(k) for k in CHANNELS if k in m},
"rel_t": m["_recv_wall"] - inject_wall}
for m in post_samples
]
with JSONL.open("a") as f:
f.write(json.dumps(record) + "\n")
return record
# ─────── main ───────
def main():
log(f"=== inject_variable_isolation run_id={RUN_ID} ===")
log(f"out_dir={OUT_DIR}")
# build the injection plan
df_day = load_btc_day()
per_var_q = {v: quantile_strengths(df_day, v) for v in VARIABLES}
log("per-variable quantile strengths:")
for v in VARIABLES:
for ql, info in per_var_q[v].items():
log(f" {v:18s} {ql} raw={info['raw_value']:+.6f} "
f"z_clip={info['z_clipped']:+.3f} strength={info['strength']:+.5f}")
# plan: list of (label_dict, strength)
plan = []
# variable runs
for v in VARIABLES:
for ql, info in per_var_q[v].items():
for rep in range(N_REPEATS):
plan.append((
{"kind": "variable", "var": v, "quantile_label": ql,
"quantile": info["quantile"], "raw_value": info["raw_value"],
"z_clipped": info["z_clipped"], "rep": rep},
info["strength"],
))
# calibration runs
for cs in CALIBRATION_STRENGTHS:
for rep in range(N_REPEATS):
plan.append((
{"kind": "calibration", "rep": rep},
cs,
))
# randomize order so position effects (drift) don't favor any variable
rng = np.random.default_rng(20260606)
order = rng.permutation(len(plan))
plan = [plan[i] for i in order]
log(f"plan: {len(plan)} injections (randomized order, seed=20260606)")
# write meta
meta = {
"run_id": RUN_ID,
"coin": COIN,
"variables": VARIABLES,
"quantiles": QUANTILES,
"str_cap": STR_CAP,
"calibration_strengths": CALIBRATION_STRENGTHS,
"n_repeats": N_REPEATS,
"inject_xy_sigma": [INJECT_X, INJECT_Y, INJECT_SIG],
"pre_record_s": PRE_RECORD_S,
"post_record_s": POST_RECORD_S,
"decay_probe_s": DECAY_PROBE_S,
"decay_asym_tol": DECAY_ASYM_TOL,
"decay_asym_std": DECAY_ASYM_STD,
"decay_timeout_s": DECAY_TIMEOUT_S,
"stability_probe_s": STABILITY_PROBE_S,
"stability_asym_std": STABILITY_ASYM_STD,
"stability_asym_slope": STABILITY_ASYM_SLOPE,
"max_wait_s": MAX_WAIT_S,
"baseline_measure_s": BASELINE_MEASURE_S,
"n_total_injections": len(plan),
"plan_order": [p[0] for p in plan],
"plan_strengths": [p[1] for p in plan],
"wall_iso_start": time.strftime("%Y-%m-%dT%H:%M:%S"),
"notes": (
"Baseline is stability-based, NOT engineered 12-13 band. Lattice "
"currently parked at asym~33 plateau from accumulated forcing; "
"daemon restart is forbidden (RESONANCE-owned). What matters is a "
"verified-stable low-variance baseline, whatever the level."
),
}
(OUT_DIR / "meta.json").write_text(json.dumps(meta, indent=2, default=str))
# dump quantile info
(OUT_DIR / "per_var_quantiles.json").write_text(json.dumps(per_var_q, indent=2))
# ZMQ
tel = TelSub(TEL_ADDR)
ctx = zmq.Context.instance()
pub = ctx.socket(zmq.PUB)
pub.connect(CMD_ADDR)
time.sleep(0.7) # let PUB connect
# wait for telemetry
t0 = time.time()
while not tel.buf and time.time() - t0 < 15:
time.sleep(0.3)
if not tel.buf:
log("FATAL: no telemetry on 5556")
return
log(f"telemetry up, {len(tel.buf)} initial samples")
# ── PHASE 0: initial stability gate ──
log(f"phase 0: waiting for stable baseline (asym std < {STABILITY_ASYM_STD} "
f"and |slope| < {STABILITY_ASYM_SLOPE}/s over {STABILITY_PROBE_S}s)")
base = wait_for_stability(
tel, STABILITY_PROBE_S, STABILITY_ASYM_STD, STABILITY_ASYM_SLOPE,
MAX_WAIT_S, label="phase0_init",
)
if base is None:
log("FATAL: never reached stable baseline; aborting")
return
baseline_anchor = base["asym_mean"]
log(f"baseline ANCHOR set: asym_mean={baseline_anchor:.3f} (decay returns must hit this +/- {DECAY_ASYM_TOL})")
# measure clean baseline for BASELINE_MEASURE_S
log(f"phase 0b: measuring clean baseline for {BASELINE_MEASURE_S}s")
base_start = time.time()
while time.time() - base_start < BASELINE_MEASURE_S:
time.sleep(5)
base_samples = tel.since(base_start)
base_stats = window_stats(base_samples)
base_stats["baseline_anchor_asym"] = baseline_anchor
base_stats["measured_wall_start_iso"] = time.strftime("%Y-%m-%dT%H:%M:%S",
time.localtime(base_start))
(OUT_DIR / "baseline_clean_stats.json").write_text(json.dumps(base_stats, indent=2))
log(f"baseline stats: asym mean={base_stats.get('asymmetry_mean',float('nan')):.3f} "
f"std={base_stats.get('asymmetry_std',float('nan')):.4f} n={base_stats.get('n',0)}")
# ── PHASES 1-5 LOOP ──
log(f"\nbeginning injection loop: {len(plan)} total injections")
for i, (label, strength) in enumerate(plan, 1):
log(f"\n=== injection {i}/{len(plan)}: {label} strength={strength:+.5f} ===")
# decay gate before each injection (except the first, which followed phase 0b).
# We do NOT require return to the ORIGINAL baseline_anchor because the field
# naturally drifts between attractors. We only require LOCAL stability so the
# pre-injection snapshot is on a settled field. Each injection's response is
# measured against its own pre_stats anyway.
if i > 1:
log(f" decay gate: waiting for local stability "
f"(std<{DECAY_ASYM_STD}, |slope|<{STABILITY_ASYM_SLOPE*2}/s over {DECAY_PROBE_S}s)")
decay_ok = wait_for_stability(
tel, DECAY_PROBE_S, DECAY_ASYM_STD, STABILITY_ASYM_SLOPE * 2,
DECAY_TIMEOUT_S, label="decay_gate",
)
if decay_ok is None:
log(f" WARN: local stability not reached within {DECAY_TIMEOUT_S}s; "
f"proceeding anyway with field as-is")
cur_probe = tel.since(time.time() - 30)
cur_stats = window_stats(cur_probe)
label = {**label, "decay_timeout": True,
"field_at_inject_asym": cur_stats.get("asymmetry_mean")}
run_one_injection(tel, pub, label, strength, baseline_anchor)
log("\n=== ALL INJECTIONS COMPLETE ===")
log(f"output: {OUT_DIR}")
tel.stop()
if __name__ == "__main__":
main()