Hessian-Coupled Event-Triggered Preconditioner / event_triggered_preconditioner.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, os
  2import numpy as np
  3from scipy.linalg import eigvalsh, solve_discrete_lyapunov
  4
  5SEED = 2075
  6rng = np.random.default_rng(SEED)
  7
  8
  9def make_hessians():
 10    # Two non-commuting positive definite curvature vertices.
 11    r = np.array([[math.cos(.42), -math.sin(.42)], [math.sin(.42), math.cos(.42)]])
 12    h1 = np.diag([1.0, 8.0])
 13    h2 = r @ np.diag([1.5, 5.5]) @ r.T
 14    return [h1, h2]
 15
 16
 17def lmi_margin(Hs, K, alpha, sigma, P=None, lam=1.0):
 18    """Return largest eigenvalue of supplied S-procedure matrices.
 19    Negative means the fixed-P certificate is strict. P is normalized if absent.
 20    """
 21    n = Hs[0].shape[0]
 22    if P is None:
 23        # A valid Lyapunov candidate for the no-staleness closed loop.
 24        Abar = -K @ sum(Hs) / len(Hs)
 25        P = solve_discrete_lyapunov(np.eye(n) + .02*Abar, np.eye(n))
 26        P = P / np.trace(P) * n
 27    worst = -np.inf
 28    for H in Hs:
 29        A = -K @ H
 30        B = A.copy()
 31        tl = A.T@P + P@A + 2*alpha*P + lam*sigma*sigma*np.eye(n)
 32        M = np.block([[tl, P@B], [B.T@P, -lam*np.eye(n)]])
 33        worst = max(worst, eigvalsh((M+M.T)/2).max())
 34    return float(worst), P
 35
 36
 37def run(h, K, sigma, steps=2500, seed=0, max_interval=10):
 38    rng = np.random.default_rng(seed)
 39    Hs = make_hessians()
 40    x = np.array([2.0, -1.5])
 41    last = x.copy()
 42    total = 0.0
 43    events = 0
 44    max_norm = 0.0
 45    losses = []
 46    intervals = []
 47    since = 0
 48    # Alternating vertex is a deliberately adversarial but bounded curvature sequence.
 49    for t in range(steps):
 50        H = Hs[t % len(Hs)]
 51        g_stale = H @ last
 52        x = x - h * (K @ g_stale)
 53        since += 1
 54        # e proxy is x itself because x*=0, matching the proposed trigger proxy.
 55        d = last - x
 56        if (d @ d > sigma*sigma*(x @ x + 1e-12)) or since >= max_interval:
 57            last = x.copy()
 58            events += 1
 59            intervals.append(since)
 60            since = 0
 61        q = .5*x @ H @ x
 62        total += q
 63        losses.append(q)
 64        max_norm = max(max_norm, float(np.linalg.norm(x)))
 65        if not np.isfinite(q) or max_norm > 1e12:
 66            return dict(stable=False, final_loss=float('inf'), events=events,
 67                        rate=events/steps, mean_interval=np.mean(intervals) if intervals else steps,
 68                        max_norm=max_norm, losses=losses)
 69    return dict(stable=True, final_loss=float(losses[-1]), events=events,
 70                rate=events/steps, mean_interval=np.mean(intervals) if intervals else steps,
 71                max_norm=max_norm, losses=losses)
 72
 73
 74def main():
 75    Hs = make_hessians()
 76    # Dense K is aligned to average Hessian; diagonal baseline has same trace/average scale.
 77    Hbar = sum(Hs)/len(Hs)
 78    K_dense = np.linalg.inv(Hbar) * 0.72
 79    K_diag = np.diag(0.72/np.diag(Hbar))
 80    # Prediction 1: no-stale discrete quadratic stability boundary h*rho(KH)<2.
 81    print('HESSIANS', [h.tolist() for h in Hs])
 82    for name,K in [('diag',K_diag),('dense',K_dense)]:
 83        rho=max(abs(np.linalg.eigvals(K@H)) .max() for H in Hs)
 84        predicted=2/rho
 85        rows=[]
 86        for mult in [0.90, 1.02]:
 87            h=mult*predicted
 88            rr=run(h,K,0.0,steps=160,seed=1,max_interval=1)
 89            rows.append({'mult':mult,'h':h,'stable':rr['stable'],'max_norm':rr['max_norm']})
 90        print('STABILITY',name,json.dumps({'rho':float(rho),'predicted_h_boundary':predicted,'observed':rows}))
 91    # Prediction 2: trigger threshold zero gives stale-free updates; larger sigma permits longer intervals
 92    # and should increase error relative to a synchronized reference.
 93    h=.16
 94    print('TRIGGER_SWEEP')
 95    for name,K in [('diag',K_diag),('dense',K_dense)]:
 96        ref=run(h,K,0.0,steps=800,seed=3,max_interval=1)
 97        for sigma in [0.0,.01,.03,.08,.16,.30]:
 98            rr=run(h,K,sigma,steps=800,seed=3,max_interval=50)
 99            # average loss over final 20%, normalized to stale-free final loss floor
100            tail=float(np.mean(rr['losses'][-160:]))
101            ref_tail=float(np.mean(ref['losses'][-160:]))
102            print(json.dumps({'kind':'trigger','method':name,'sigma':sigma,
103                'events':rr['events'],'event_fraction':rr['rate'],'mean_interval':rr['mean_interval'],
104                'final_tail_loss':tail,'loss_ratio_vs_sync':tail/(ref_tail+1e-30),
105                'stable':rr['stable']}))
106    # Prediction 3: local first-order communication interval scales approximately sigma/(h ||KH||)
107    # for small sigma, so event fraction should scale approximately linearly with h/sigma.
108    print('SCALING_SWEEP')
109    K=K_dense
110    for h in [.04,.08,.16]:
111        for sigma in [.02,.04,.08]:
112            rr=run(h,K,sigma,steps=1000,seed=4,max_interval=1000)
113            print(json.dumps({'kind':'scaling','h':h,'sigma':sigma,'events':rr['events'],
114                'event_fraction':rr['rate'],'mean_interval':rr['mean_interval'],'stable':rr['stable']}))
115    # Fixed-K LMI sanity check: report monotone margin as sigma increases.
116    print('LMI_SWEEP')
117    for name,K in [('diag',K_diag),('dense',K_dense)]:
118        for sigma in [0.0,.02,.05,.1,.2]:
119            margin,_=lmi_margin(Hs,K,alpha=.02,sigma=sigma,lam=1.0)
120            print(json.dumps({'kind':'lmi','method':name,'sigma':sigma,'worst_eigenvalue':margin,'certified':margin<0}))
121
122if __name__ == '__main__':
123    main()