1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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()
|