510 lines
20 KiB
Python
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()
|