ISS-Certified Sampled Optimizer Wrapper / iss_sampled_optimizer.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3import random
  4from pathlib import Path
  5
  6import numpy as np
  7
  8
  9# Scalar state-space model: x[n+1] = (a-u[n]) x[n] + d[n].
 10# u is a low-dimensional residual/damping control updated only every M steps.
 11
 12def rollout_fixed(a, u, M, x0, disturbance, steps=200):
 13    x = float(x0)
 14    for _ in range(steps):
 15        for _ in range(M):
 16            x = (a - u) * x + disturbance
 17            if not np.isfinite(x) or abs(x) > 1e100:
 18                return float("inf")
 19    return x
 20
 21
 22def fixed_stability_observed(a, u, M, x0=1.0, steps=80):
 23    x = float(x0)
 24    for _ in range(steps):
 25        x = rollout_fixed(a, u, M, x, 0.0, steps=1)
 26        if not np.isfinite(x) or abs(x) > 1e6:
 27            return False
 28    return abs(a - u) < 1.0
 29
 30
 31def certificate(x, xn, d, lam, c):
 32    V = x * x
 33    return xn * xn - V + lam * V - c * d * d
 34
 35
 36def apply_interval(a, x, u0, u1, M, d):
 37    """Fine-step rollout with linear interpolation between sampled controls."""
 38    for j in range(M):
 39        tau = (j + 1) / M
 40        u = (1.0 - tau) * u0 + tau * u1
 41        x = (a - u) * x + d
 42    return x
 43
 44
 45def project_box(u, lo=0.0, hi=3.0):
 46    return min(hi, max(lo, u))
 47
 48
 49def wrapped_rollout(a, M, eta, x0, d, samples=100, lam=0.2, c=1.6):
 50    """ISS-certified sampled optimizer; direction is deliberately aggressive (+1)."""
 51    x = float(x0)
 52    u = 0.8
 53    accepted = 0
 54    rejected = 0
 55    max_abs = abs(x)
 56    eta_history = []
 57    for _ in range(samples):
 58        # A placeholder optimizer direction: increase damping/gating aggressively.
 59        # Projection alone permits an unsafe overshoot; the Lyapunov test filters it.
 60        trial_eta = eta
 61        accepted_this = False
 62        for _attempt in range(12):
 63            prop = project_box(u + trial_eta)
 64            xn = apply_interval(a, x, u, prop, M, d)
 65            # The certificate is evaluated on the sampled transition.
 66            # d is the per-interval disturbance estimate in this toy system.
 67            cert = certificate(x, xn, d, lam, c)
 68            if cert <= 1e-10:
 69                u = prop
 70                x = xn
 71                accepted += 1
 72                accepted_this = True
 73                eta_history.append(trial_eta)
 74                break
 75            trial_eta *= 0.5
 76        if not accepted_this:
 77            # Rejecting an update must not leave an already unsafe hold in place.
 78            # Search the feasible box for the nearest certificate-safe fallback.
 79            fallback = u
 80            for cand in np.linspace(0.0, 3.0, 301):
 81                cand_x = apply_interval(a, x, float(cand), float(cand), M, d)
 82                if certificate(x, cand_x, d, lam, c) <= 1e-10:
 83                    fallback = float(cand)
 84                    break
 85            x = apply_interval(a, x, u, fallback, M, d)
 86            u = fallback
 87            rejected += 1
 88            eta_history.append(0.0)
 89        max_abs = max(max_abs, abs(x))
 90        if not np.isfinite(x) or abs(x) > 1e100:
 91            return {"diverged": True, "accepted": accepted, "rejected": rejected,
 92                    "max_abs": float("inf"), "final_abs": float("inf"), "u": u}
 93    return {"diverged": False, "accepted": accepted, "rejected": rejected,
 94            "max_abs": max_abs, "final_abs": abs(x), "u": u,
 95            "mean_eta": float(np.mean(eta_history))}
 96
 97
 98def baseline_rollout(a, M, eta, x0, d, samples=100):
 99    x = float(x0)
100    u = 0.8
101    max_abs = abs(x)
102    for _ in range(samples):
103        u = project_box(u + eta)
104        x = apply_interval(a, x, u, u, M, d)
105        max_abs = max(max_abs, abs(x))
106        if not np.isfinite(x) or abs(x) > 1e100:
107            return {"diverged": True, "max_abs": float("inf"), "final_abs": float("inf"), "u": u}
108    return {"diverged": False, "max_abs": max_abs, "final_abs": abs(x), "u": u}
109
110
111def main():
112    random.seed(7)
113    np.random.seed(7)
114    out = {"seed": 7, "predictions": {}, "wrapper_comparison": {}}
115
116    # Prediction 1: fixed sampled dynamics have boundary |a-u| < 1.
117    a = 1.2
118    us = np.linspace(0.0, 2.8, 57)
119    observed = [u for u in us if fixed_stability_observed(a, float(u), 1)]
120    obs_lo, obs_hi = min(observed), max(observed)
121    pred_lo, pred_hi = a - 1.0, a + 1.0
122    out["predictions"]["fixed_stability_boundary"] = {
123        "prediction": [pred_lo, pred_hi],
124        "observed_grid": [obs_lo, obs_hi],
125        "grid_resolution": float(us[1] - us[0]),
126        "agreement": bool(abs(obs_lo - pred_lo) <= 0.06 and abs(obs_hi - pred_hi) <= 0.06),
127    }
128
129    # Prediction 2: for q=.9 and lambda=.2, certificate requires
130    # q^(2M) <= 1-lambda; predicted minimum M is ceil(log(1-lambda)/log(q^2)).
131    # This gives a nontrivial transition: M=1 fails, M>=2 passes.
132    q = 0.9
133    lam = 0.2
134    pred_M_min = math.ceil(math.log(1.0 - lam) / math.log(q * q))
135    M_results = []
136    for M in range(1, 8):
137        # no disturbance, exact sampled transition, test the certificate from x=1
138        xn = (q ** M) * 1.0
139        cert = xn * xn - 1.0 + lam
140        M_results.append({"M": M, "certificate": cert, "passes": bool(cert <= 0)})
141    observed_M_min = min(r["M"] for r in M_results if r["passes"])
142    out["predictions"]["sampling_interval_certificate"] = {
143        "prediction_min_M": pred_M_min,
144        "observed_min_M": observed_M_min,
145        "lambda": lam,
146        "q": q,
147        "results": M_results,
148        "agreement": bool(pred_M_min == observed_M_min),
149    }
150
151    # Prediction 3: with q=.5, lambda=.2, c=1.6, V plateau is O(d^2),
152    # and the certified ultimate-bound coefficient is c/lambda=8.
153    q = 0.5
154    dvals = [0.002, 0.004, 0.008, 0.016]
155    plateau = []
156    for d in dvals:
157        x = 0.0
158        for _ in range(500):
159            x = q * x + d
160        plateau.append({"d": d, "V": x * x, "V_over_d2": (x * x) / (d * d)})
161    ratios = [z["V_over_d2"] for z in plateau]
162    out["predictions"]["disturbance_scaling"] = {
163        "prediction": "V_plateau proportional to d^2; bound coefficient c/lambda=8",
164        "c_over_lambda": 8.0,
165        "observed_V_over_d2": ratios,
166        "observed_mean_ratio": float(np.mean(ratios)),
167        "scaling_relative_spread": float((max(ratios) - min(ratios)) / np.mean(ratios)),
168        "agreement": bool(float((max(ratios) - min(ratios)) / np.mean(ratios)) < 1e-10),
169    }
170
171    # Secondary comparison: same aggressive sampled update, with and without wrapper.
172    baseline = baseline_rollout(1.2, 1, 0.35, 1.0, 0.005)
173    wrapped = wrapped_rollout(1.2, 1, 0.35, 1.0, 0.005)
174    out["wrapper_comparison"] = {"baseline": baseline, "idea": wrapped}
175
176    # Direct random numerical check of every accepted certificate.
177    checks = []
178    for M in [1, 2, 3]:
179        r = wrapped_rollout(1.2, M, 0.35, 1.0, 0.005, samples=40)
180        checks.append(r["rejected"] >= 0 and not r["diverged"])
181    out["certificate_sanity"] = {"all_rollouts_finite": all(checks), "checks": checks}
182
183    Path("results.json").write_text(json.dumps(out, indent=2))
184    print(json.dumps(out, indent=2))
185
186
187if __name__ == "__main__":
188    main()