Primal-Dual Active-Set Optimizer Filter / run_experiment.py
Mechanism confirmed, baseline not beaten
1import json, time
2import numpy as np
3from scipy.optimize import minimize
4from pdas_filter import solve_active_set, kkt_residual
5
6np.set_printoptions(precision=5, suppress=True)
7
8# Strictly convex QP: min 1/2 ||v-v_nom||^2, A v >= b.
9def make_problem(v_nom, alpha=1.0, h=None, A=None):
10 n = len(v_nom)
11 if A is None:
12 A = np.vstack([np.eye(n), -np.eye(n)])
13 if h is None:
14 h = -np.ones(A.shape[0])
15 return np.eye(n), -np.asarray(v_nom), np.asarray(A, float), -alpha*np.asarray(h)
16
17
18def projected_gradient(v_nom, A, b, steps=250, lr=.12):
19 # Generic approximate baseline: gradient step followed by sequential projections.
20 v = np.zeros_like(v_nom)
21 for _ in range(steps):
22 v -= lr * (v - v_nom)
23 for a, bj in zip(A, b):
24 gap = bj - a @ v
25 if gap > 0:
26 v += gap * a / (a @ a)
27 return v
28
29# Prediction 1: for one constraint v >= alpha*q, activity changes at alpha=v_nom/q.
30v_nom = np.array([0.37]); q = 0.8
31alphas = np.linspace(0., 1.2, 25)
32active = []
33for alpha in alphas:
34 H,c,A,b = make_problem(v_nom, alpha, np.array([-q]), np.array([[1.]]))
35 _,_,act,_,_ = solve_active_set(H,c,A,b)
36 active.append(bool(act))
37pred_boundary = v_nom[0]/q
38changes = np.where(np.diff(np.array(active, dtype=int)) != 0)[0]
39obs_boundary = alphas[changes[0]+1] if len(changes) else np.nan
40
41# Prediction 2: after activation, mu = alpha*q-v_nom, slope dmu/dalpha=q.
42alpha_fit = np.linspace(pred_boundary + .08, 1.6, 12)
43mus = []
44for alpha in alpha_fit:
45 H,c,A,b = make_problem(v_nom, alpha, np.array([-q]), np.array([[1.]]))
46 _,mu,_,_,_ = solve_active_set(H,c,A,b)
47 mus.append(mu[0])
48slope = np.polyfit(alpha_fit, mus, 1)[0]
49
50# Prediction 3: smoothly moving nominal velocities preserve active sets and warm starts
51# converge in one solve iteration after the first transition.
52A = np.array([[1., 0.], [0., 1.], [-1., 0.]])
53h = np.array([-0.42, -0.25, 1.0])
54trajectory = np.column_stack([np.linspace(.1, .8, 40), np.linspace(.1, .35, 40)])
55prev = None; iterations = []; changes_count = 0; prior_act = None; max_kkt = 0.
56for vn in trajectory:
57 H,c,A2,b = make_problem(vn, 1., h, A)
58 v,mu,act,it,_ = solve_active_set(H,c,A2,b,active_init=prev)
59 iterations.append(it)
60 changes_count += int(prior_act is not None and act != prior_act)
61 prior_act = act; prev = act
62 max_kkt = max(max_kkt, kkt_residual(H,c,A2,b,v,mu))
63
64# Same small collection of changing QPs: exact PDAS versus projected-gradient approximation.
65rng = np.random.default_rng(2265)
66A4 = np.eye(4)
67h4 = -np.ones(4)*.55
68pdas_t = []; pg_t = []; pdas_err = []; pg_err = []; violations = []
69for vn in rng.normal(0, .5, size=(60,4)):
70 H,c,A5,b = make_problem(vn, 1., h4, A4)
71 t=time.perf_counter(); v,mu,act,it,_=solve_active_set(H,c,A5,b); pdas_t.append(time.perf_counter()-t)
72 t=time.perf_counter(); vp=projected_gradient(vn,A5,b); pg_t.append(time.perf_counter()-t)
73 pdas_err.append(np.linalg.norm(v-vn)**2/2); pg_err.append(np.linalg.norm(vp-vn)**2/2)
74 violations.append(max(0., np.max(b-A5@vp)))
75
76result = {
77 "prediction_boundary": {"predicted_alpha": float(pred_boundary), "observed_grid_alpha": float(obs_boundary), "grid_error": float(abs(obs_boundary-pred_boundary))},
78 "prediction_dual_slope": {"predicted_dmu_dalpha": float(q), "observed_slope": float(slope), "relative_error": float(abs(slope-q)/q)},
79 "prediction_warm_start": {"mean_iterations": float(np.mean(iterations)), "median_iterations": float(np.median(iterations)), "fraction_one_iteration": float(np.mean(np.array(iterations)==1)), "active_set_changes": int(changes_count), "max_kkt_residual": float(max_kkt)},
80 "baseline_comparison": {"pdas_mean_objective": float(np.mean(pdas_err)), "projected_gradient_mean_objective": float(np.mean(pg_err)), "pdas_mean_ms": float(1000*np.mean(pdas_t)), "projected_gradient_mean_ms": float(1000*np.mean(pg_t)), "projected_gradient_max_violation": float(max(violations))},
81 "all_iterations": iterations
82}
83print(json.dumps(result, indent=2))
84with open("results.json", "w") as f: json.dump(result, f, indent=2)