| """Correctness gates: re-derive quantities the paper (or classical theory) |
| already establishes, and assert them before interpreting anything else.""" |
| import json |
| import numpy as np |
| import rdpg |
|
|
| OUT = {} |
|
|
|
|
| def gate_eig_residual(rng): |
| """LAPACK eigendecomposition of A is exact to machine precision.""" |
| n, r = 800, 5 |
| A, X = rdpg.weighted_rdpg(n, r, rng) |
| s, U = rdpg.full_spectrum(A) |
| res = float(np.max(np.abs(A - (U * s) @ U.T))) |
| orth = float(np.max(np.abs(U.T @ U - np.eye(n)))) |
| return {"max_abs_recon_residual": res, "max_abs_orthonormality_residual": orth, |
| "n": n, "pass": res < 1e-9 and orth < 1e-9} |
|
|
|
|
| def gate_dirichlet_moments(rng): |
| """For X_i ~ Dir(1,1,1,1,1), M = E[x x^T] = (I + J)/30 exactly, so the |
| non-zero eigenvalues of P = X X^T obey s_1/n -> 1/5, s_2..s_5/n -> 1/30. |
| This pins the constant in Assumption A4 (s_j asymp rho_n n) and shows that |
| the paper's own simulation design has a 4-fold degenerate population |
| eigenvalue (Assumptions A3/A6 hold only through O(sqrt(n)) fluctuations).""" |
| r = 5 |
| M_theory = (np.eye(r) + np.ones((r, r))) / 30.0 |
| ev_theory = np.sort(np.linalg.eigvalsh(M_theory))[::-1] |
| rows = [] |
| for n in [2000, 20000, 200000]: |
| X = rdpg.dirichlet_latent(n, r, rng) |
| M_emp = X.T @ X / n |
| ev_emp = np.sort(np.linalg.eigvalsh(M_emp))[::-1] |
| rows.append({"n": n, |
| "max_abs_moment_err": float(np.max(np.abs(M_emp - M_theory))), |
| "eigs_emp": ev_emp.tolist()}) |
| return {"M_theory_eigs": ev_theory.tolist(), "rows": rows, |
| "degenerate_multiplicity": 4, |
| "pass": rows[-1]["max_abs_moment_err"] < 5e-3} |
|
|
|
|
| def gate_eckart_young(rng): |
| """Lemma 2.1 (k<0 branch) is an Eckart-Young statement. With E = 0 the |
| d-dimensional ASE IS the rank-d truncated eigendecomposition of P, so |
| min_W ||Xhat^o W - rho^{1/2}X||_F must EQUAL sqrt(sum_{j>d} s_j) exactly, |
| and the (2,inf) lower bound of Lemma 2.1 must hold.""" |
| n, r, rho = 1500, 5, 1.0 |
| X = rdpg.dirichlet_latent(n, r, rng) |
| P = rho * (X @ X.T) |
| s, U = rdpg.full_spectrum(P) |
| s_nz = np.sort(np.linalg.eigvalsh(rho * (X.T @ X)))[::-1] |
| rows = [] |
| for d in [1, 2, 3, 4]: |
| Xh = rdpg.ase_from_spectrum(s, U, d) |
| e2i, D = rdpg.err_2inf(Xh, np.sqrt(rho) * X) |
| fro = float(np.linalg.norm(D)) |
| fro_theory = float(np.sqrt(s_nz[d:].sum())) |
| bound = float(np.sqrt(s_nz[d:].sum() / n)) |
| rows.append({"d": d, "k": d - r, "frob_measured": fro, |
| "frob_eckart_young": fro_theory, |
| "rel_frob_residual": abs(fro - fro_theory) / fro_theory, |
| "err_2inf": e2i, "lemma21_lower_bound": bound, |
| "ratio_2inf_over_bound": e2i / bound}) |
| return {"n": n, "rows": rows, |
| "max_rel_frob_residual": max(x["rel_frob_residual"] for x in rows), |
| "min_ratio": min(x["ratio_2inf_over_bound"] for x in rows), |
| "pass": max(x["rel_frob_residual"] for x in rows) < 1e-10 |
| and min(x["ratio_2inf_over_bound"] for x in rows) >= 1.0} |
|
|
|
|
| def gate_procrustes(rng): |
| """W = U V^T from SVD(Xhat^T X) solves min_{W in O} ||Xhat W - X||_F. |
| Verified by (a) first-order optimality: W^T Xhat^T X is symmetric PSD, |
| (b) beating 20000 random rotations.""" |
| n, r, d = 400, 5, 7 |
| A, X = rdpg.weighted_rdpg(n, r, rng) |
| s, U = rdpg.full_spectrum(A) |
| Xh = rdpg.ase_from_spectrum(s, U, d) |
| Xp = np.hstack([X, np.zeros((n, d - r))]) |
| Uu, sv, Vt = np.linalg.svd(Xh.T @ Xp) |
| W = Uu @ Vt |
| best = float(np.linalg.norm(Xh @ W - Xp)) |
| Msym = W.T @ (Xh.T @ Xp) |
| sym_err = float(np.max(np.abs(Msym - Msym.T))) |
| psd_min = float(np.min(np.linalg.eigvalsh((Msym + Msym.T) / 2))) |
| worst = 0.0 |
| for _ in range(20000): |
| Q, _ = np.linalg.qr(rng.standard_normal((d, d))) |
| worst = max(worst, best - float(np.linalg.norm(Xh @ Q - Xp))) |
| return {"frob_at_procrustes": best, "symmetry_residual": sym_err, |
| "min_eig_of_WtXhatTX": psd_min, |
| "best_random_rotation_improvement": worst, |
| "pass": sym_err < 1e-9 and psd_min > -1e-9 and worst <= 1e-9} |
|
|
|
|
| def gate_semicircle_edge(rng): |
| """||E|| / (2 sigma sqrt(n)) -> 1 (Wigner edge). The paper bounds the |
| trailing ASE eigenvalues by ||E|| (Sec. 2.1), so this constant matters.""" |
| rows = [] |
| for n in [500, 1000, 2000, 4000]: |
| vals = [] |
| for _ in range(3): |
| E = rdpg.sym_noise(n, rng, "normal", 1.0) |
| vals.append(float(np.linalg.norm(E, 2))) |
| rows.append({"n": n, "opnorm": float(np.mean(vals)), |
| "ratio_to_2sqrtn": float(np.mean(vals)) / (2 * np.sqrt(n))}) |
| return {"rows": rows, "final_ratio": rows[-1]["ratio_to_2sqrtn"], |
| "pass": abs(rows[-1]["ratio_to_2sqrtn"] - 1.0) < 0.05} |
|
|
|
|
| def gate_signal_incoherence(rng): |
| """Corollary B.3.1 / Assumption A1: ||U_{1:r}||_{2,inf} <~ 1/sqrt(n). |
| We report the constant sqrt(n)*||U_{1:r}||_{2,inf}, which must not grow.""" |
| rows = [] |
| for n in [500, 1000, 2000, 4000, 8000]: |
| X = rdpg.dirichlet_latent(n, 5, rng) |
| P = X @ X.T |
| s, U = rdpg.full_spectrum(P) |
| c = rdpg.two_inf(U[:, :5]) * np.sqrt(n) |
| rows.append({"n": n, "sqrtn_times_2inf": float(c)}) |
| return {"rows": rows, |
| "drift": rows[-1]["sqrtn_times_2inf"] / rows[0]["sqrtn_times_2inf"], |
| "pass": rows[-1]["sqrtn_times_2inf"] < 3 * rows[0]["sqrtn_times_2inf"]} |
|
|
|
|
| def jsonable(o): |
| if isinstance(o, dict): |
| return {k: jsonable(v) for k, v in o.items()} |
| if isinstance(o, (list, tuple)): |
| return [jsonable(v) for v in o] |
| if isinstance(o, (np.bool_, bool)): |
| return bool(o) |
| if isinstance(o, (np.integer,)): |
| return int(o) |
| if isinstance(o, (np.floating,)): |
| return float(o) |
| return o |
|
|
|
|
| if __name__ == "__main__": |
| rng = np.random.default_rng(20260728) |
| OUT["matmul_sanity"] = rdpg.matmul_sanity() |
| OUT["eig_residual"] = gate_eig_residual(rng) |
| OUT["dirichlet_moments"] = gate_dirichlet_moments(rng) |
| OUT["eckart_young"] = gate_eckart_young(rng) |
| OUT["procrustes"] = gate_procrustes(rng) |
| OUT["semicircle_edge"] = gate_semicircle_edge(rng) |
| OUT["signal_incoherence"] = gate_signal_incoherence(rng) |
| OUT["all_pass"] = bool(all(v["pass"] for v in OUT.values() if isinstance(v, dict))) |
| OUT = jsonable(OUT) |
| with open("outputs/gates.json", "w") as f: |
| json.dump(OUT, f, indent=1) |
| print(json.dumps(OUT, indent=1)) |
|
|