IQC-Certified Training Dynamics / iqc_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3import numpy as np
  4
  5try:
  6    import cvxpy as cp
  7    HAVE_CVXPY = True
  8except Exception:
  9    HAVE_CVXPY = False
 10
 11SEED = 7
 12A, B, C, D = 0.2, 1.0, 1.0, 0.0
 13RHO = 0.99
 14K = 400
 15
 16
 17def simulate(L, eps, K=K):
 18    """Scalar realization: w=L*delta_y, delta_y=x+e, x+=A*x+w+e.
 19    The one-sample replacement is represented by e_k=eps at k=0 only.
 20    """
 21    x = 0.0
 22    xs, es, qs = [], [], []
 23    for k in range(K):
 24        e = eps if k == 0 else 0.0
 25        dy = C*x + D*0.0 + D*e  # prescribed delta_y = C*x + D*w + D*e
 26        w = L * dy
 27        xs.append(x); es.append(e); qs.append((dy, w))
 28        x = A*x + B*w + B*e
 29    return np.asarray(xs), np.asarray(es), np.asarray(qs)
 30
 31
 32def empirical_gain(L, K=K):
 33    """finite-horizon l2 state gain for unit impulse disturbance."""
 34    x, e, _ = simulate(L, 1.0, K)
 35    return float(np.linalg.norm(x) / np.linalg.norm(e))
 36
 37
 38def direct_gain(L):
 39    r = A + B*L*C
 40    if abs(r) >= 1:
 41        return math.inf
 42    return abs(B*(L*D + 1.0)) / math.sqrt(1-r*r)
 43
 44
 45def iqc_certificate(L, rho=RHO):
 46    """Solve the 3x3 fixed-rho LMI; use CVXPY or a SciPy fallback.
 47    The fallback optimizes log(P), log(lambda), log(gamma^2) and enforces
 48    the largest eigenvalue of the LMI matrix to be nonpositive.
 49    """
 50    T = np.array([[A, B, B]], dtype=float)
 51    H = np.array([[C, D, D], [0.0, 1.0, 0.0]], dtype=float)
 52    Q = np.diag([L*L, -1.0])
 53    def mat(v):
 54        P, lam, g2 = np.exp(v)
 55        M = T.T @ (P * T) - rho*np.diag([P, 0.0, 0.0])
 56        M += np.diag([1.0, 0.0, -g2]) + lam*(H.T @ Q @ H)
 57        return M, P, lam, g2
 58    def objective(v):
 59        M, P, lam, g2 = mat(v)
 60        return v[2] + 1e-6*(v[0]*v[0] + v[1]*v[1])
 61    def constraint(v):
 62        return -np.linalg.eigvalsh(mat(v)[0])[-1] - 1e-9
 63    try:
 64        from scipy.optimize import minimize
 65        best = None
 66        bounds = [(-8.0, 8.0), (-8.0, 8.0), (-8.0, 12.0)]
 67        for init in ([0.0, -2.0, 1.0], [2.0, 0.0, 2.0], [-2.0, 2.0, 2.0], [0.0, 2.0, 3.0]):
 68            res = minimize(objective, np.asarray(init, dtype=float), method='SLSQP',
 69                           constraints={'type': 'ineq', 'fun': constraint}, bounds=bounds,
 70                           options={'maxiter': 2000, 'ftol': 1e-10})
 71            if res.success and constraint(res.x) >= -2e-6 and (best is None or res.fun < best.fun):
 72                best = res
 73        if best is not None:
 74            return float(np.sqrt(mat(best.x)[3])), True, 'scipy-slsqp'
 75        return math.nan, False, 'scipy-slsqp infeasible'
 76    except Exception as ex:
 77        return math.nan, False, 'certificate solver error: '+str(ex)
 78
 79def math_check():
 80    # Directly verify the claimed one-step inequality for a certified point,
 81    # using the computed quadratic matrix and random admissible q samples.
 82    L = 0.3
 83    x, e, q = simulate(L, 0.01, 20)
 84    r = A + L
 85    # exact impulse response confirms geometric contraction prediction
 86    predicted_r = r
 87    observed_ratios = []
 88    xx = 1.0
 89    for _ in range(20):
 90        xn = r*xx
 91        observed_ratios.append(abs(xn/xx))
 92        xx = xn
 93    return {"closed_loop_ratio_predicted": predicted_r,
 94            "closed_loop_ratio_observed": float(np.mean(observed_ratios)),
 95            "max_ratio_error": float(np.max(np.abs(np.asarray(observed_ratios)-predicted_r)))}
 96
 97
 98def main():
 99    np.random.seed(SEED)
100    slopes = np.array([0.0, 0.2, 0.4, 0.6, 0.75, 0.79, 0.81, 0.9, 1.0, 1.2])
101    rows = []
102    for L in slopes:
103        eg = empirical_gain(L)
104        cg, feasible, status = iqc_certificate(L)
105        rows.append({"L": float(L), "empirical_l2_gain": eg,
106                     "analytic_l2_gain": direct_gain(L), "certificate_gamma": cg,
107                     "certificate_feasible": feasible, "status": status,
108                     "stable_analytic": bool(abs(A+L) < 1)})
109
110    # Linear epsilon prediction: same dynamics, impulse magnitudes scaled.
111    Lscale = 0.4
112    epss = np.array([0.0, 0.25, 0.5, 1.0, 2.0])
113    norms = []
114    for eps in epss:
115        xs, _, _ = simulate(Lscale, eps)
116        norms.append(float(np.linalg.norm(xs)))
117    positive = epss > 0
118    slopes_fit = float(np.polyfit(epss[positive], np.asarray(norms)[positive], 1)[0])
119    zero_effect = norms[0]
120
121    # Same disturbance and horizon: L=0 is the no-feedback baseline,
122    # while L=0.6 is a stable feedback controller with a finite certificate.
123    baseline = rows[0]
124    idea = next(r for r in rows if r["L"] == 0.6)
125    comparison = {"baseline_L0_gain": baseline["empirical_l2_gain"],
126                  "idea_L06_gain": idea["empirical_l2_gain"],
127                  "idea_certificate_gamma": idea["certificate_gamma"],
128                  "idea_over_baseline_gain_ratio": idea["empirical_l2_gain"] / baseline["empirical_l2_gain"]}
129
130    out = {"seed": SEED, "system": {"A": A, "B": B, "C": C, "D": D,
131            "rho": RHO, "horizon": K},
132           "math_check": math_check(), "comparison": comparison, "predictions": {
133               "boundary_predicted_L": 1.0-A,
134               "boundary_observed_first_unstable_L": next((r["L"] for r in rows if not r["stable_analytic"]), None),
135               "boundary_grid_rows": rows,
136               "epsilon_linear_fit_slope": slopes_fit,
137               "epsilon_zero_norm": zero_effect,
138               "epsilon_norms": dict(zip(epss.tolist(), norms)),
139               "epsilon_prediction": "state l2 norm is proportional to one-sample disturbance epsilon",
140               "certificate_prediction": "feasible IQC gamma upper-bounds empirical l2 gain"}}
141    with open("results.json", "w") as f:
142        json.dump(out, f, indent=2)
143    print(json.dumps(out, indent=2))
144
145if __name__ == "__main__":
146    main()