Disorder-Controlled Basin Merging / disorder_basin.py
Mechanism confirmed, baseline not beaten
1"""MVP verification of disorder-controlled basin merging.
2
3The RFIM-like K=Delta=H=0 zero-temperature mean-field equation is
4 F_R(m) = erf(m / (sqrt(2)*R)).
5For relaxed updates m <- (1-alpha)m + alpha F_R(m), the local multiplier at
6zero is lambda = 1-alpha+alpha*sqrt(2/pi)/R. Thus Rc=sqrt(2/pi).
7"""
8import json, math
9from pathlib import Path
10import numpy as np
11from scipy.special import erf
12
13SEED = 1016
14RC_PRED = math.sqrt(2.0 / math.pi)
15
16
17def rf_map(m, R):
18 return erf(m / (math.sqrt(2.0) * R))
19
20
21def relaxed_iter(m0, R, alpha=1.0, steps=3000):
22 m = float(m0)
23 trace = []
24 for _ in range(steps):
25 m = (1-alpha)*m + alpha*rf_map(m, R)
26 trace.append(m)
27 return np.asarray(trace)
28
29
30def tanh_iter(m0, coupling=1.4, alpha=1.0, steps=3000):
31 # Standard continuously saturated mean-field recurrent update, no disorder.
32 m = float(m0)
33 trace = []
34 for _ in range(steps):
35 m = (1-alpha)*m + alpha*math.tanh(coupling*m)
36 trace.append(m)
37 return np.asarray(trace)
38
39
40def estimate_derivative(R, eps=1e-6):
41 return (rf_map(eps, R) - rf_map(-eps, R)) / (2*eps)
42
43
44def main():
45 rng = np.random.default_rng(SEED)
46 Rs = np.array([0.45, 0.60, 0.70, 0.78, 0.80, 0.90, 1.10, 1.40])
47 alpha = 0.35
48 rows = []
49 for R in Rs:
50 slope = estimate_derivative(R)
51 predicted_lambda = 1-alpha + alpha*slope
52 starts = np.array([-1.0, -0.4, -0.05, 0.05, 0.4, 1.0])
53 finals = np.array([relaxed_iter(x, R, alpha)[-1] for x in starts])
54 # Pairwise late-state spread is the basin-merging metric.
55 pairwise = float(np.mean((finals[:, None] - finals[None, :])**2))
56 # Empirical contraction from two nearby positive initial conditions.
57 # Measure the Jacobian at the zero fixed point, where the analytic
58 # contraction prediction is defined (rather than at a nonzero basin).
59 eps = 1e-7
60 def update_zero(x):
61 return (1-alpha)*x + alpha*rf_map(x, R)
62 measured_lambda = float((update_zero(eps)-update_zero(-eps))/(2*eps))
63 rows.append({"R":float(R), "slope_measured":float(slope),
64 "slope_predicted":float(RC_PRED/R),
65 "lambda_measured":measured_lambda,
66 "lambda_predicted":float(predicted_lambda),
67 "finals":finals.tolist(), "pairwise_msd":pairwise})
68
69 # Locate the bifurcation by bisection on the positive fixed point.
70 lo, hi = 0.55, 1.05
71 for _ in range(45):
72 mid = (lo+hi)/2
73 pos = relaxed_iter(1.0, mid, 0.8, 5000)[-1]
74 if pos > 1e-5: lo = mid
75 else: hi = mid
76 rc_obs = (lo+hi)/2
77
78 # A finite-N annealed ternary implementation: p(s) softmax and fresh fields.
79 # This checks that the advertised ternary probabilities produce the same map.
80 def ternary_relax(R, N=200000, steps=100, beta=80.0, alpha=1.0):
81 # Fresh fields are redrawn each relaxation step, as in the proposal.
82 p = np.zeros((N, 3)); p[:, 2] = 1.0
83 means = []
84 states = np.array([-1., 0., 1.])
85 for _ in range(steps):
86 m = float(np.mean(p @ states))
87 xi = rng.normal(0., R, N)
88 energies = -(m + xi)[:, None] * states[None, :]
89 z = -beta * energies
90 z -= z.max(axis=1, keepdims=True)
91 peq = np.exp(z); peq /= peq.sum(axis=1, keepdims=True)
92 p = (1-alpha)*p + alpha*peq
93 means.append(float(np.mean(p @ states)))
94 return means[-1]
95 ternary_check = {str(R): ternary_relax(float(R), N=30000, steps=80)
96 for R in [0.6, 1.0]}
97
98 # Baseline: saturated tanh has two persistent basins at coupling 1.4;
99 # idea: disorder above Rc merges them to zero.
100 baseline = np.array([tanh_iter(x, 1.4)[-1] for x in [-1., 1.]])
101 idea_high = np.array([relaxed_iter(x, 1.0, alpha)[-1] for x in [-1., 1.]])
102 summary = {
103 "seed": SEED, "Rc_predicted": RC_PRED, "Rc_observed_bisection": rc_obs,
104 "alpha": alpha, "rows": rows,
105 "ternary_finite_N_final_m": ternary_check,
106 "baseline_tanh_final_m": baseline.tolist(),
107 "idea_R1_final_m": idea_high.tolist(),
108 "prediction_checks": {
109 "linear_slope_max_relative_error": float(max(abs(r['slope_measured']-r['slope_predicted'])/r['slope_predicted'] for r in rows)),
110 "critical_point_absolute_error": abs(rc_obs-RC_PRED),
111 "relaxation_multiplier_max_absolute_error": float(max(abs(r['lambda_measured']-r['lambda_predicted']) for r in rows))
112 }
113 }
114 Path("results.json").write_text(json.dumps(summary, indent=2))
115 print(json.dumps(summary, indent=2))
116
117if __name__ == "__main__":
118 main()