diff options
Diffstat (limited to 'toy/toy_transient_chaos.py')
| -rw-r--r-- | toy/toy_transient_chaos.py | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/toy/toy_transient_chaos.py b/toy/toy_transient_chaos.py new file mode 100644 index 0000000..7a6c6bd --- /dev/null +++ b/toy/toy_transient_chaos.py @@ -0,0 +1,112 @@ +"""Minimal analytically-grounded toy reproducing 'failure trajectories are more chaotic'. + +Framing: recursive reasoning = chaotic SEARCH of latent space until the trajectory lands in the +solution basin, then it converges (the answer is a stable fixed point). At a fixed readout time T: + - captured by T => SUCCESS, finite-time Lyapunov exponent (FTLE) is low (chaotic search for a + while, then contraction). + - not captured => FAILURE, FTLE stays at the chaotic-saddle value. + +Map on [0,1] (input s = solution location, drawn per 'puzzle'): + search phase: x <- 4 x (1-x) (fully chaotic, lambda = ln 2 ~ +0.693) + capture: if |x - s| < eps -> converging phase: x <- s + 0.5 (x - s) (lambda = ln 0.5 < 0) + +This is a textbook chaotic-saddle / transient-chaos system: escape (capture) times are ~geometric, +and FTLE over [0,T] = (ln2 * t_search + ln0.5 * (T - t_search)) / T. So FTLE separates outcome +PURELY as a finite-time artifact of capture time -- matching our real-model findings: + (i) failure more chaotic, (ii) separation is concurrent/finite-time (vanishes as T->inf since + everyone eventually captures), (iii) it is convergence detection, not a deep correctness signal. + +Run: python toy_transient_chaos.py (CPU, seconds). Produces toy_transient_chaos.png + stats. +""" +from __future__ import annotations +from pathlib import Path +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +HERE = Path(__file__).resolve().parent +rng = np.random.default_rng(0) + + +def run(n=20000, T=16, eps=0.04, seed=0): + rg = np.random.default_rng(seed) + s = rg.uniform(0.15, 0.85, n) # 'puzzle': solution location + x = rg.uniform(0, 1, n) # initial latent + captured = np.zeros(n, bool) + cap_time = np.full(n, T, int) + log_deriv_sum = np.zeros(n) + drift = np.zeros((n, T)) + for t in range(T): + x_prev = x.copy() + search = ~captured + # log|f'| this step + ld = np.empty(n) + ld[search] = np.log(np.abs(4 * (1 - 2 * x[search])) + 1e-12) + ld[~search] = np.log(0.5) + # update + x_new = x.copy() + x_new[search] = 4 * x[search] * (1 - x[search]) + x_new[~search] = s[~search] + 0.5 * (x[~search] - s[~search]) + # capture check (enter basin during search) + newly = search & (np.abs(x_new - s) < eps) + captured |= newly + cap_time[newly] = t + 1 + x = x_new + log_deriv_sum += ld + drift[:, t] = np.abs(x - x_prev) + ftle = log_deriv_sum / T + success = captured & (np.abs(x - s) < 0.05) # near solution at readout + return dict(ftle=ftle, success=success, cap_time=cap_time, drift=drift, T=T, eps=eps) + + +def auc(score, y): + p, n = score[y == 1], score[y == 0] + if len(p) == 0 or len(n) == 0: + return float("nan") + a = np.concatenate([p, n]); o = np.argsort(a); r = np.empty(len(a)); r[o] = np.arange(1, len(a) + 1) + return float((r[:len(p)].sum() - len(p) * (len(p) + 1) / 2) / (len(p) * len(n))) + + +def main(): + d = run(T=16) + y = d["success"].astype(int); ftle = d["ftle"] + print(f"=== Toy transient-chaos (T=16, eps={d['eps']}) ===") + print(f"success rate = {y.mean():.3f}") + print(f"FTLE: success median={np.median(ftle[y==1]):+.3f} failure median={np.median(ftle[y==0]):+.3f}") + print(f"AUC(-FTLE -> success) = {auc(-ftle, y):.3f} (>0.9 => reproduces 'failure more chaotic')") + late_drift = d["drift"][:, -4:].mean(1) + print(f"late drift: success median={np.median(late_drift[y==1]):.3f} failure median={np.median(late_drift[y==0]):.3f}") + print(f"AUC(-late_drift -> success) = {auc(-late_drift, y):.3f}") + + # FTLE separation vs readout time T (the finite-time / concurrent-not-antecedent prediction) + Ts = [4, 8, 16, 32, 64, 128, 256] + seps, accs = [], [] + for T in Ts: + dd = run(T=T) + yy = dd["success"].astype(int) + seps.append(auc(-dd["ftle"], yy)); accs.append(yy.mean()) + print("\nFTLE-AUC and success-rate vs readout time T (separation is a finite-time effect):") + for T, a, ac in zip(Ts, seps, accs): + print(f" T={T:>3}: AUC(-FTLE->success)={a:.3f} success_rate={ac:.3f}") + + fig, ax = plt.subplots(1, 3, figsize=(15, 4.2)) + bins = np.linspace(-0.5, 0.75, 50) + ax[0].hist(ftle[y == 1], bins=bins, alpha=0.6, color="tab:green", density=True, label=f"success (n={int(y.sum())})") + ax[0].hist(ftle[y == 0], bins=bins, alpha=0.6, color="tab:red", density=True, label=f"failure (n={int((1-y).sum())})") + ax[0].axvline(0, color="gray", lw=0.6); ax[0].set_xlabel("finite-time Lyapunov exponent (T=16)") + ax[0].set_title(f"Toy: failure more chaotic\nAUC={auc(-ftle,y):.3f}"); ax[0].legend(fontsize=8) + ax[1].hist(d["cap_time"][d["cap_time"] < 16], bins=range(0, 17), color="tab:blue", alpha=0.8) + ax[1].set_xlabel("capture (escape) time"); ax[1].set_ylabel("count") + ax[1].set_title("escape-time distribution (~geometric:\nchaotic-saddle signature)") + ax[2].plot(Ts, seps, "o-", label="AUC(-FTLE->success)") + ax[2].plot(Ts, accs, "s--", color="tab:purple", label="success rate") + ax[2].set_xscale("log"); ax[2].axhline(0.5, color="gray", lw=0.5) + ax[2].set_xlabel("readout time T"); ax[2].set_title("separation is FINITE-TIME:\nvanishes as T->inf (all escape)") + ax[2].legend(fontsize=8) + fig.tight_layout(); fig.savefig(HERE / "toy_transient_chaos.png", dpi=140) + print("\nsaved toy_transient_chaos.png") + + +if __name__ == "__main__": + main() |
