Raw ⬇ ZIP
  1import json
  2from pathlib import Path
  3import numpy as np
  4
  5SEED = 2698
  6rng = np.random.default_rng(SEED)
  7
  8
  9def fit_dynamics(h, z, ridge=1e-6):
 10    # h[t+1] = A h[t] + B z[t] + e[t]
 11    X = np.concatenate([h[:-1], z], axis=1)
 12    Y = h[1:]
 13    W = Y.T @ X @ np.linalg.inv(X.T @ X + ridge*np.eye(X.shape[1]))
 14    d, p = h.shape[1], z.shape[1]
 15    return W[:, :d], W[:, d:], Y - X @ W.T
 16
 17
 18def q_factor(F, P):
 19    # Similarity-invariant generalized contraction factor.
 20    Pm = np.linalg.inv(np.linalg.cholesky(P))
 21    M = Pm @ F @ P @ F.T @ Pm.T
 22    return float(np.sqrt(max(np.linalg.eigvalsh(M).max(), 0)))
 23
 24
 25def scalar_sweep():
 26    # h+=q h + bounded disturbance. Exact worst-case radius is sigma/(1-q).
 27    qs = np.array([0.50, 0.70, 0.85, 0.95, 0.99, 1.01, 1.10])
 28    sigma = 0.1
 29    rows = []
 30    for q in qs:
 31        # adversarial disturbance maximizes absolute state and exposes the formula.
 32        h = 0.0
 33        vals = []
 34        for _ in range(600):
 35            e = sigma if h >= 0 else -sigma
 36            h = q*h + e
 37            vals.append(abs(h))
 38        if q < 1:
 39            predicted = sigma/(1-q)
 40            observed = float(np.mean(vals[-50:]))
 41            relerr = abs(observed-predicted)/predicted
 42        else:
 43            predicted = float('inf')
 44            observed = float(np.mean(vals[-20:]))
 45            relerr = None
 46        growth = (float(np.log(max(vals[-1], 1e-300)/max(vals[-21], 1e-300))/20) if q > 1 else None)
 47        predicted_growth = float(np.log(q)) if q > 1 else None
 48        rows.append(dict(q=float(q), predicted_radius=predicted,
 49                         observed_tail_radius=observed, relative_error=relerr,
 50                         diverged=bool(max(vals)>1e6), observed_log_growth_per_step=growth,
 51                         predicted_log_growth_per_step=predicted_growth))
 52    return rows
 53
 54
 55def boundary_sweep():
 56    # Random bounded disturbances: classify boundedness across a fine q sweep.
 57    qs = np.linspace(.85, 1.15, 31)
 58    sigma = .02
 59    rows=[]
 60    for q in qs:
 61        h=0.; maxabs=0.
 62        for _ in range(400):
 63            h=q*h+rng.uniform(-sigma,sigma)
 64            maxabs=max(maxabs,abs(h))
 65        rows.append((float(q), float(maxabs)))
 66    # operational divergence threshold is first q whose final state exceeds 10x stable q=.99 scale
 67    threshold = next((q for q,m in rows if m > 2.0), None)
 68    return rows, threshold
 69
 70
 71def data_fit_and_projection():
 72    # Offline trajectory fit, covariance ellipsoid, and same forcing with/without projection.
 73    d=3; p=2
 74    A=np.array([[.72,.08,0],[-.04,.64,.05],[0,.03,.58]])
 75    B=np.array([[.35,.0],[0,.28],[.05,.12]])
 76    T=5000
 77    z=rng.normal(size=(T,p))*0.3
 78    h=np.zeros((T+1,d))
 79    for t in range(T):
 80        h[t+1]=A@h[t]+B@z[t]+rng.normal(size=d)*.006
 81    Ah,Bh,res=fit_dynamics(h,z)
 82    # diagonal inflated residual ellipsoid (99.5% absolute coordinate containment)
 83    rad=np.quantile(np.abs(res), .995, axis=0)*1.05
 84    P=np.diag((rad/(1-.72))**2) # conservative axis box-to-ellipsoid scale
 85    fit_err=float(np.linalg.norm(A-Ah)/np.linalg.norm(A))
 86    # Deliberately unstable learned/recurrent matrix to make projection effect visible.
 87    F=Ah*1.55
 88    qp=q_factor(F,P)
 89    x=np.zeros(d); xp=np.zeros(d); raw=[]; proj=[]
 90    for _ in range(300):
 91        noise=rng.normal(size=d)*rad
 92        x=F@x+noise
 93        xp=F@xp+noise
 94        energy=float(xp@np.linalg.inv(P)@xp)
 95        if energy>1: xp=xp/np.sqrt(energy)
 96        raw.append(float(x@np.linalg.inv(P)@x)); proj.append(float(xp@np.linalg.inv(P)@xp))
 97    return {
 98        'fit_relative_A_error':fit_err, 'residual_axis_radii':rad.tolist(),
 99        'q_of_test_matrix':qp, 'raw_violation_rate':float(np.mean(np.array(raw)>1)),
100        'projected_violation_rate':float(np.mean(np.array(proj)>1+1e-10)),
101        'raw_max_ellipsoid_energy':float(max(raw)), 'projected_max_ellipsoid_energy':float(max(proj)),
102        'projected_violation_tolerance':1e-10}
103
104
105def main():
106    radii=scalar_sweep()
107    b_rows,boundary=boundary_sweep()
108    fit=data_fit_and_projection()
109    stable=[r for r in radii if r['q']<1]
110    mean_err=float(np.mean([r['relative_error'] for r in stable]))
111    report={
112      'seed':SEED,
113      'predictions':{
114        'P1_boundary_predicted_q':1.0,
115        'P1_observed_operational_boundary_q':boundary,
116        'P2_radius_formula_mean_relative_error':mean_err,
117        'P2_radius_sweep':radii,
118        'P3_projection_predicted_zero_violation':True,
119        'P3_projection_result':fit
120      },
121      'boundary_sweep': [{'q':q,'max_abs_state':m} for q,m in b_rows]
122    }
123    Path('results.json').write_text(json.dumps(report, indent=2))
124    print(json.dumps(report, indent=2))
125
126if __name__=='__main__': main()