Claim 4: run the 400x800 zero-sum comparison independently; second-order reaches gap 8.2e-14 in 1.10s vs first-order plateauing at 1.1e-5 (1.4e8x worse)
13e208e verified | """Claim 4: on 400x800 random zero-sum games, second-order (PSSN-style) methods | |
| reach a duality gap of ~1e-12 in a few seconds. | |
| The logbook checked this only as a source-reported figure. The claim has two | |
| measurable parts -- the ACHIEVABLE GAP and the TIME -- and a second-order method | |
| should separate sharply from a first-order one on the first of these. | |
| Zero-sum game: min_x max_y x^T A y over the simplices. Duality gap at (x,y): | |
| gap = max_j (A^T x)_j - min_i (A y)_i . | |
| Compared: extragradient / mirror-prox (first order) against an interior-point | |
| solve of the equivalent LP (second order, Newton steps on the KKT system). | |
| """ | |
| import json, time, numpy as np | |
| from scipy.optimize import linprog | |
| RES = {} | |
| def game(m, n, seed): | |
| return np.random.default_rng(seed).normal(size=(m, n)) | |
| def gap(A, x, y): | |
| return float((A.T @ x).max()-(A @ y).min()) | |
| def simplex_proj(v): | |
| u = np.sort(v)[::-1]; c = np.cumsum(u)-1 | |
| r = np.arange(1, len(v)+1) | |
| rho = np.nonzero(u-c/r > 0)[0][-1] | |
| return np.maximum(v-c[rho]/(rho+1), 0) | |
| def first_order(A, iters=20000): | |
| m, n = A.shape | |
| x = np.ones(m)/m; y = np.ones(n)/n | |
| L = np.linalg.norm(A, 2); eta = 0.9/max(L, 1e-12) | |
| t0 = time.perf_counter() | |
| for _ in range(iters): | |
| xh = simplex_proj(x-eta*(A @ y)); yh = simplex_proj(y+eta*(A.T @ x)) | |
| x = simplex_proj(x-eta*(A @ yh)); y = simplex_proj(y+eta*(A.T @ xh)) | |
| return gap(A, x, y), time.perf_counter()-t0 | |
| def second_order(A): | |
| """LP form solved by interior point (Newton on the KKT system).""" | |
| m, n = A.shape | |
| t0 = time.perf_counter() | |
| # min_x max_j (A^T x)_j -> min t s.t. A^T x <= t 1, sum x = 1, x >= 0 | |
| c = np.zeros(m+1); c[-1] = 1.0 | |
| Aub = np.hstack([A.T, -np.ones((n, 1))]) | |
| Aeq = np.zeros((1, m+1)); Aeq[0, :m] = 1.0 | |
| r = linprog(c, A_ub=Aub, b_ub=np.zeros(n), A_eq=Aeq, b_eq=[1.0], | |
| bounds=[(0, None)]*m+[(None, None)], method="highs-ipm") | |
| x = r.x[:m] | |
| c2 = np.zeros(n+1); c2[-1] = -1.0 | |
| Aub2 = np.hstack([-A, np.ones((m, 1))]) | |
| Aeq2 = np.zeros((1, n+1)); Aeq2[0, :n] = 1.0 | |
| r2 = linprog(c2, A_ub=Aub2, b_ub=np.zeros(m), A_eq=Aeq2, b_eq=[1.0], | |
| bounds=[(0, None)]*n+[(None, None)], method="highs-ipm") | |
| y = r2.x[:n] | |
| el = time.perf_counter()-t0 | |
| return gap(A, x, y), el | |
| def main(): | |
| rows = [] | |
| for (m, n) in ((100, 200), (200, 400), (400, 800)): | |
| for s in range(3): | |
| A = game(m, n, seed=s) | |
| g1, t1 = first_order(A) | |
| g2, t2 = second_order(A) | |
| rows.append({"m": m, "n": n, "seed": s, | |
| "first_order_gap": g1, "first_order_sec": round(t1, 3), | |
| "second_order_gap": g2, "second_order_sec": round(t2, 3)}) | |
| sub = [r for r in rows if r["m"] == m] | |
| print(" %dx%d first-order: gap=%.3e in %.2fs | second-order: gap=%.3e in %.2fs" | |
| % (m, n, np.mean([r["first_order_gap"] for r in sub]), np.mean([r["first_order_sec"] for r in sub]), | |
| np.mean([r["second_order_gap"] for r in sub]), np.mean([r["second_order_sec"] for r in sub])), flush=True) | |
| big = [r for r in rows if r["m"] == 400] | |
| RES["claim4_pssn"] = {"rows": rows, "seeds": 3, | |
| "target_size": "400x800", | |
| "second_order_gap_400x800": float(np.mean([r["second_order_gap"] for r in big])), | |
| "second_order_sec_400x800": round(float(np.mean([r["second_order_sec"] for r in big])), 3), | |
| "first_order_gap_400x800": float(np.mean([r["first_order_gap"] for r in big])), | |
| "gap_ratio": float(np.mean([r["first_order_gap"] for r in big])/max(np.mean([r["second_order_gap"] for r in big]), 1e-300)), | |
| "paper_reported": {"gap": 1e-12, "seconds": "4.3-4.4"}} | |
| R = RES["claim4_pssn"] | |
| print(" 400x800: second-order gap %.3e in %.2fs (paper: 1e-12 in 4.3-4.4s); first-order gap %.3e -> %.1e x worse" | |
| % (R["second_order_gap_400x800"], R["second_order_sec_400x800"], | |
| R["first_order_gap_400x800"], R["gap_ratio"]), flush=True) | |
| json.dump(RES, open("pssn_results.json", "w"), indent=1) | |
| if __name__ == "__main__": | |
| main(); print("DONE") | |