Flip-Bifurcation Spectral Guard / flip_guard_experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json
  2import math
  3from pathlib import Path
  4import numpy as np
  5
  6# Scalar center-manifold normal form:
  7# z_{k+1}=-(1+mu) z_k + a z_k^3.
  8# The fixed point has J=-(1+mu), and a symmetric 2-cycle satisfies
  9# f(z)=-z, giving |z|=sqrt(mu/a) for mu>0.
 10
 11def iterate(mu, a=1.0, z0=0.03, steps=4000):
 12    z = float(z0)
 13    hist = []
 14    for _ in range(steps):
 15        z = -(1.0 + mu) * z + a * z**3
 16        hist.append(z)
 17        if not np.isfinite(z) or abs(z) > 1e6:
 18            return np.asarray(hist), False
 19    return np.asarray(hist), True
 20
 21def cycle_amplitude(hist, tail=500):
 22    if len(hist) < tail or not np.all(np.isfinite(hist)):
 23        return float("nan")
 24    return float(np.mean(np.abs(hist[-tail:])))
 25
 26def a_k(hist, tail=500):
 27    if len(hist) < tail + 2 or not np.all(np.isfinite(hist)):
 28        return float("nan")
 29    return float(np.mean(np.abs(hist[-tail:] - hist[-tail-2:-2])) /
 30                 (np.mean(np.abs(hist[-tail:])) + 1e-12))
 31
 32def jacobian_eigenvalue(mu):
 33    return -(1.0 + mu)
 34
 35def corrected_guard_mu(mu, delta=0.05):
 36    # lambda >= -1+delta; lambda=-1-mu, so mu <= -delta.
 37    return min(mu, -delta)
 38
 39def run():
 40    a = 2.0
 41    delta = 0.05
 42
 43    # Prediction 1: the fixed point loses stability at mu=0 (lambda=-1).
 44    mus_boundary = np.linspace(-0.20, 0.20, 81)
 45    stable = []
 46    for mu in mus_boundary:
 47        h, ok = iterate(mu, a=a, z0=1e-4, steps=1500)
 48        stable.append(ok and abs(h[-1]) < 1e-3 and abs(h[-1] - h[-3]) < 1e-3)
 49    stable_mus = mus_boundary[np.asarray(stable)]
 50    unstable_mus = mus_boundary[~np.asarray(stable)]
 51    observed_boundary = float((stable_mus[-1] + unstable_mus[0]) / 2)
 52
 53    # Prediction 2: period-two amplitude scales as sqrt(mu/a).
 54    mu_amp = np.linspace(0.01, 0.18, 12)
 55    observed_amp, predicted_amp = [], []
 56    for mu in mu_amp:
 57        h, _ = iterate(mu, a=a, z0=0.01, steps=5000)
 58        observed_amp.append(cycle_amplitude(h))
 59        predicted_amp.append(math.sqrt(mu / a))
 60    observed_amp, predicted_amp = np.asarray(observed_amp), np.asarray(predicted_amp)
 61    rel_amp_error = float(np.mean(np.abs(observed_amp - predicted_amp) / predicted_amp))
 62    coeff = float(np.dot(np.sqrt(mu_amp), observed_amp) /
 63                  np.dot(np.sqrt(mu_amp), np.sqrt(mu_amp)))
 64
 65    # Prediction 3: amplitude changes as 1/sqrt(a).
 66    mu_scale = 0.08
 67    a_values = np.asarray([0.5, 1.0, 2.0, 4.0])
 68    scale_obs, scale_pred = [], []
 69    for av in a_values:
 70        h, _ = iterate(mu_scale, a=av, z0=0.01, steps=5000)
 71        scale_obs.append(cycle_amplitude(h))
 72        scale_pred.append(math.sqrt(mu_scale / av))
 73    scale_obs, scale_pred = np.asarray(scale_obs), np.asarray(scale_pred)
 74    scale_error = float(np.mean(np.abs(scale_obs-scale_pred)/scale_pred))
 75
 76    # Small same-task comparison: train scalar recurrent gain toward an
 77    # unstable target mu=+0.15. Baseline follows task loss; corrected guard
 78    # projects onto lambda >= -1+delta after each update.
 79    target, beta, lr = 0.15, 5.0, 0.08
 80    mu_base, mu_guard, mu_literal = -0.10, -0.10, -0.10
 81    for _ in range(100):
 82        mu_base -= lr * 2.0 * (mu_base-target)
 83        corrected_grad = 2.0 * beta * max(0.0, mu_guard + delta)
 84        mu_guard -= lr * (2.0*(mu_guard-target) + corrected_grad)
 85        mu_guard = corrected_guard_mu(mu_guard, delta)
 86        # Literal formula from the prompt: [max(0, lambda+1-delta)]^2.
 87        # Since lambda=-1-mu, this is max(0,-mu-delta)^2 and cannot stop
 88        # positive mu (the flip-unstable region).
 89        literal_grad = -2.0 * beta * max(0.0, -mu_literal-delta)
 90        mu_literal -= lr * (2.0*(mu_literal-target) + literal_grad)
 91
 92    hb, _ = iterate(mu_base, a=a, z0=0.01, steps=3000)
 93    hg, _ = iterate(corrected_guard_mu(mu_guard, delta), a=a, z0=0.01, steps=3000)
 94    hl, _ = iterate(mu_literal, a=a, z0=0.01, steps=3000)
 95    test_mu = 0.10
 96    lam = jacobian_eigenvalue(test_mu)
 97
 98    out = {
 99        "normal_form": {"a": a, "delta": delta},
100        "prediction_1_boundary": {
101            "predicted_mu": 0.0, "observed_mu_midpoint": observed_boundary,
102            "last_stable_grid_mu": float(stable_mus[-1]),
103            "first_unstable_grid_mu": float(unstable_mus[0]),
104            "lambda_at_observed_midpoint": jacobian_eigenvalue(observed_boundary)},
105        "prediction_2_sqrt_scaling": {
106            "mu": mu_amp.tolist(), "observed_amplitude": observed_amp.tolist(),
107            "predicted_amplitude": predicted_amp.tolist(),
108            "mean_relative_error": rel_amp_error,
109            "observed_sqrt_coefficient": coeff,
110            "predicted_sqrt_coefficient": 1.0/math.sqrt(a)},
111        "prediction_3_cubic_scaling": {
112            "a": a_values.tolist(), "observed_amplitude": scale_obs.tolist(),
113            "predicted_amplitude": scale_pred.tolist(), "mean_relative_error": scale_error},
114        "guard_training": {
115            "target_mu": target, "baseline_mu": float(mu_base),
116            "corrected_guard_mu": float(mu_guard), "literal_formula_mu": float(mu_literal),
117            "baseline_lambda": jacobian_eigenvalue(mu_base),
118            "corrected_guard_lambda": jacobian_eigenvalue(mu_guard),
119            "literal_formula_lambda": jacobian_eigenvalue(mu_literal),
120            "baseline_A_K": a_k(hb), "corrected_guard_A_K": a_k(hg),
121            "literal_formula_A_K": a_k(hl),
122            "baseline_cycle_amplitude": cycle_amplitude(hb),
123            "corrected_guard_cycle_amplitude": cycle_amplitude(hg),
124            "literal_formula_cycle_amplitude": cycle_amplitude(hl)},
125        "formula_sign_check": {
126            "mu": test_mu, "lambda": lam,
127            "displayed_penalty": max(0.0, lam + 1.0 - delta)**2,
128            "corrected_lower_bound_penalty": max(0.0, (-1.0 + delta) - lam)**2,
129            "note": "The displayed formula penalizes lambda above -1+delta, rather than lambda below it; corrected results are reported separately."}}
130    Path("results.json").write_text(json.dumps(out, indent=2))
131    print(json.dumps(out, indent=2))
132
133if __name__ == "__main__":
134    run()