Delay-Shape Master-Stability Coupling / verify_delay_shape.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3import numpy as np
  4
  5SEED = 2069
  6rng = np.random.default_rng(SEED)
  7
  8
  9def companion(a, alpha, delay):
 10    """Companion matrix for x[t+1] = a*x[t] + alpha*x[t-delay]."""
 11    n = delay + 1
 12    C = np.zeros((n, n), dtype=complex if np.iscomplexobj(alpha) else float)
 13    C[0, 0] = a
 14    C[0, -1] = alpha
 15    C[1:, :-1] = np.eye(delay)
 16    return C
 17
 18
 19def rho(a, alpha, delay):
 20    return float(np.max(np.abs(np.linalg.eigvals(companion(a, alpha, delay)))))
 21
 22
 23def empirical_exponent(a, alpha, delay, steps=5000):
 24    C = companion(a, alpha, delay)
 25    v = rng.normal(size=delay + 1) + 1j * rng.normal(size=delay + 1)
 26    v /= np.linalg.norm(v)
 27    logs = []
 28    for _ in range(steps):
 29        v = C @ v
 30        n = np.linalg.norm(v)
 31        logs.append(math.log(max(n, 1e-300)))
 32        v /= max(n, 1e-300)
 33    # discard transient; renormalization makes this a Lyapunov estimate
 34    return float(np.mean(logs[500:]))
 35
 36
 37def nontrivial_eigenvalues(W):
 38    vals = np.linalg.eigvals(W)
 39    # For row-stochastic W, remove the eigenvalue nearest one.
 40    j = int(np.argmin(np.abs(vals - 1)))
 41    return np.delete(vals, j)
 42
 43
 44def disagreement_rollout(W, a, sigma, delay, steps=180):
 45    n = W.shape[0]
 46    # History is a transverse random perturbation around a nonzero synchronized value.
 47    hist = np.zeros((delay + 1, n))
 48    hist[:] = 1.0 + 0.03 * rng.normal(size=(delay + 1, n))
 49    hist -= hist.mean(axis=1, keepdims=True)
 50    ds = []
 51    for t in range(steps):
 52        x = hist[-1]
 53        delayed = hist[0]
 54        y = a * x + sigma * W @ delayed
 55        ds.append(float(np.mean((x - x.mean()) ** 2)))
 56        hist = np.concatenate([hist[1:], y[None]], axis=0)
 57    tail = np.array(ds[20:])
 58    slope = float(np.polyfit(np.arange(len(tail)), np.log(tail + 1e-30), 1)[0])
 59    return ds, slope
 60
 61
 62def make_symmetric_ring(n):
 63    W = np.zeros((n, n))
 64    for i in range(n):
 65        W[i, (i - 1) % n] = 0.5
 66        W[i, (i + 1) % n] = 0.5
 67    return W
 68
 69
 70def make_directed_heterogeneous(n, trials=3000):
 71    # Directed, nonreciprocal, heterogeneous indegrees; retain row normalization and W1=1.
 72    best, best_score = None, float('inf')
 73    for _ in range(trials):
 74        W = rng.dirichlet(0.22 * np.ones(n), size=n)
 75        vals = nontrivial_eigenvalues(W)
 76        score = float(np.max(np.abs(vals)))
 77        if score < best_score:
 78            best, best_score = W, score
 79    return best
 80
 81
 82def main():
 83    a = 0.72
 84    # Prediction 1: exact transition at rho=1 for every delay.
 85    boundary_rows = []
 86    for d in [0, 1, 2, 4, 7]:
 87        # bisection on positive real alpha
 88        lo, hi = 0.0, 2.0
 89        for _ in range(55):
 90            mid = (lo + hi) / 2
 91            if rho(a, mid, d) < 1:
 92                lo = mid
 93            else:
 94                hi = mid
 95        alpha_star = (lo + hi) / 2
 96        boundary_rows.append({"delay": d, "predicted_alpha_rho1": alpha_star,
 97                              "rho_at_boundary": rho(a, alpha_star, d)})
 98
 99    # Prediction 2: measured finite-rollout exponent equals log spectral radius.
100    exponent_rows = []
101    for d in [1, 2, 4]:
102        for alpha in [0.05, 0.15, 0.30, 0.50]:
103            r = rho(a, alpha, d)
104            e = empirical_exponent(a, alpha, d)
105            exponent_rows.append({"delay": d, "alpha": alpha, "log_rho": math.log(r),
106                                  "measured_exponent": e, "abs_error": abs(e-math.log(r))})
107
108    # Prediction 3: increasing delay shrinks the stable positive-alpha interval.
109    delay_rows = []
110    for d in range(0, 9):
111        lo, hi = 0.0, 2.0
112        for _ in range(50):
113            mid = (lo + hi) / 2
114            if rho(a, mid, d) < 1: lo = mid
115            else: hi = mid
116        delay_rows.append({"delay": d, "stable_alpha_max": (lo+hi)/2})
117
118    n = 8
119    W_base = make_symmetric_ring(n)
120    W_idea = make_directed_heterogeneous(n)
121    vals_base = nontrivial_eigenvalues(W_base)
122    vals_idea = nontrivial_eigenvalues(W_idea)
123    sigma = 0.42
124    delay = 2
125    ds_b, slope_b = disagreement_rollout(W_base, a, sigma, delay)
126    ds_i, slope_i = disagreement_rollout(W_idea, a, sigma, delay)
127    network = {
128        "a": a, "sigma": sigma, "delay": delay,
129        "baseline_max_transverse_abs_eigenvalue": float(np.max(np.abs(vals_base))),
130        "idea_max_transverse_abs_eigenvalue": float(np.max(np.abs(vals_idea))),
131        "baseline_predicted_worst_log_rho": max(math.log(rho(a, sigma*v, delay)) for v in vals_base),
132        "idea_predicted_worst_log_rho": max(math.log(rho(a, sigma*v, delay)) for v in vals_idea),
133        "baseline_disagreement_log_slope": slope_b,
134        "idea_disagreement_log_slope": slope_i,
135        "baseline_final_disagreement": ds_b[-1],
136        "idea_final_disagreement": ds_i[-1],
137        "baseline_W": W_base.tolist(), "idea_W": W_idea.tolist()
138    }
139    result = {"seed": SEED, "predictions": {
140        "boundary_rho_equals_one": boundary_rows,
141        "exponent_log_rho": exponent_rows,
142        "delay_stability_boundary": delay_rows}, "network_comparison": network}
143    with open("results.json", "w") as f: json.dump(result, f, indent=2)
144    print(json.dumps({"boundary": boundary_rows, "max_exponent_error": max(x["abs_error"] for x in exponent_rows),
145                      "delay_boundary": delay_rows, "network": {k:v for k,v in network.items() if not k.endswith("_W")}}, indent=2))
146
147if __name__ == "__main__": main()