Spectral-gap adaptive halting / experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math
  2from pathlib import Path
  3import numpy as np
  4
  5SEED = 2748
  6np.random.seed(SEED)
  7
  8# Fold normal form from the paper: u_{t+1}=u+g*eps+b*u^2.
  9def fold_step(u, eps, g=1.0, b=1.0):
 10    return u + g * eps + b * u * u
 11
 12def fixed_point(eps, g=1.0, b=1.0):
 13    return -math.sqrt(-g * eps / b)
 14
 15def stable_gap(eps, g=1.0, b=1.0):
 16    u = fixed_point(eps, g, b)
 17    return 1.0 - abs(1.0 + 2.0 * b * u)
 18
 19def passage_time(eps, g=1.0, b=1.0, max_steps=10000000):
 20    u, n = 0.0, 0
 21    while u < 1.0 and n < max_steps:
 22        u = fold_step(u, eps, g, b)
 23        n += 1
 24    return n
 25
 26def log_slope(x, y):
 27    return float(np.polyfit(np.log(x), np.log(y), 1)[0])
 28
 29def jvp_scalar(v, u, eps, g=1.0, b=1.0):
 30    return (1.0 + 2.0 * b * u) * v
 31
 32def predictor(u, delta=0.01, g=1.0, b=1.0):
 33    # Exact scalar JVP, equivalent to one power iteration.
 34    lam = abs(jvp_scalar(1.0, u, 0.0, g, b))
 35    return math.pi / max(delta, 1.0 - lam), lam
 36
 37def residual(u, old):
 38    return abs(u - old) / (abs(u) + 1e-6)
 39
 40def stable_run(eps, mode, tol=1e-5, rtol=0.03, tau_threshold=25.0,
 41               max_steps=50000):
 42    """Inference on the stable branch, stopping when |u-u*| <= tol.
 43
 44    Fixed is a conservative cap, residual is the standard local early exit,
 45    and spectral additionally requires a small local relaxation forecast.
 46    """
 47    target = fixed_point(eps)
 48    u, old = 0.0, 0.0
 49    for t in range(1, max_steps + 1):
 50        old, u = u, fold_step(u, eps)
 51        r = residual(u, old)
 52        accurate = abs(u - target) <= tol
 53        if mode == 'fixed':
 54            if accurate: return t, True
 55        elif mode == 'residual':
 56            if accurate and r < rtol: return t, True
 57        else:
 58            tau, lam = predictor(u)
 59            # Stable branch has lam<1; reject critical/unstable estimates.
 60            if accurate and r < rtol and lam < 1.0 and tau < tau_threshold:
 61                return t, True
 62    return max_steps, False
 63
 64def oscillatory_fallback():
 65    # A stable rotation has oscillating coordinates; a fold scalar detector
 66    # should disable its predictor on this non-fold trajectory.
 67    z = np.array([1.0, 0.0]); theta, rho = 0.55, 0.99
 68    R = rho * np.array([[math.cos(theta), -math.sin(theta)],
 69                        [math.sin(theta), math.cos(theta)]])
 70    xs = []
 71    for _ in range(20):
 72        z = R @ z; xs.append(float(z[0]))
 73    sign_changes = sum(xs[i] * xs[i-1] < 0 for i in range(1, len(xs)))
 74    return sign_changes >= 1, sign_changes
 75
 76def main():
 77    # Prediction 1: gap ~ eps^(1/2); prediction 2: passage time ~ eps^(-1/2).
 78    eps = np.logspace(-5, -2, 12)
 79    gaps = np.array([stable_gap(-e) for e in eps])
 80    passages = np.array([passage_time(e) for e in eps], dtype=float)
 81    # Prediction 3: changing b gives opposite +/-1/2 prefactor exponents.
 82    e0 = 2e-5
 83    bs = np.array([0.25, 0.5, 1., 2., 4.])
 84    b_gaps = np.array([stable_gap(-e0, b=b) for b in bs])
 85    b_passages = np.array([passage_time(e0, b=b) for b in bs], dtype=float)
 86    products = b_gaps * b_passages
 87
 88    # Direct stable-branch test of pi/(1-lambda): actual threshold time has
 89    # the same inverse-gap scaling, while pi is an asymptotic calibration.
 90    rows = []
 91    for e in [1e-4, 3e-5, 1e-5]:
 92        target = fixed_point(-e); u = 0.0
 93        for t in range(1, 50000):
 94            u = fold_step(u, -e)
 95            if abs(u-target) <= 1e-5:
 96                tau, lam = predictor(u)
 97                rows.append({'eps':e, 'actual_steps':t,
 98                             'predicted_tau':tau, 'lambda_hat':lam,
 99                             'gap':1-lam})
100                break
101
102    workload = np.logspace(-5, -3, 20)
103    controller = {}
104    for mode in ['fixed', 'residual', 'spectral']:
105        vals = [stable_run(float(-e), mode)[0] for e in workload]
106        controller[mode] = {'mean_steps':float(np.mean(vals)),
107                            'median_steps':float(np.median(vals)), 'steps':vals}
108    result = {
109      'seed':SEED,
110      'predictions':{
111        'gap_epsilon_exponent':{'predicted':0.5,'observed':log_slope(eps,gaps)},
112        'passage_epsilon_exponent':{'predicted':-0.5,'observed':log_slope(eps,passages)},
113        'gap_b_exponent':{'predicted':0.5,'observed':log_slope(bs,b_gaps)},
114        'passage_b_exponent':{'predicted':-0.5,'observed':log_slope(bs,b_passages)},
115        'Pi':{'predicted':math.pi,'observed':products.tolist(),
116              'mean':float(np.mean(products)),
117              'relative_error':float(abs(np.mean(products)-math.pi)/math.pi)}},
118      'stable_predictor_samples':rows, 'controller':controller,
119      'oscillatory_fallback':dict(detected=oscillatory_fallback()[0],
120                                  sign_changes=oscillatory_fallback()[1]),
121      'settings':{'delta':0.01,'residual_tol':0.03,'accuracy_tol':1e-5,
122                  'tau_threshold':25.0}}
123    Path('results.json').write_text(json.dumps(result,indent=2))
124    print(json.dumps(result,indent=2))
125
126if __name__ == '__main__': main()