| """Targeted sweeps and negative controls. |
| |
| ksweep - paper Fig. 2: error vs k at fixed n (predicted sqrt(k) growth) |
| sigma - sigma-dependence of the over-specification term (Theorem 3.2) |
| rho - rho-dependence of the under-specification floor (Theorem 3.2, k<0) |
| ktrue - |k|-dependence of the floor vs the EXACT Lemma 2.1 bound |
| rankctrl - negative control: same d, but the discarded direction carries no signal |
| sparse - sparse binary RDPG, rho_n = n^{-gamma} (Conjecture 1 / Sec. 4.2) |
| hubctrl - negative control for Conjecture 1: variance condition c <= E|E|^2 broken |
| """ |
| import json |
| import sys |
| import numpy as np |
| import rdpg |
|
|
| R = 5 |
|
|
|
|
| def _ase_err(A, Xt, dims, r=R): |
| s, U = rdpg.full_spectrum(A) |
| out = {} |
| for d in dims: |
| Xh = rdpg.ase_from_spectrum(s, U, d) |
| e, _ = rdpg.err_2inf(Xh, Xt) |
| out[d] = {"err": e} |
| if d > r: |
| out[d]["trail"] = rdpg.two_inf(Xh[:, r:d]) |
| out[d]["U_trail_2inf"] = rdpg.two_inf(U[:, r:d]) |
| out[d]["s_hat_d"] = float(abs(s[d - 1])) |
| return out, s, U |
|
|
|
|
| def ksweep(rng, reps=8): |
| """Paper Fig. 2: exponential noise, error as a function of embedding dim.""" |
| dims = [3, 4, 5, 6, 7, 8, 10, 14, 20, 30, 40, 60, 80] |
| res = {} |
| for n in [1000, 2000, 4000]: |
| acc = {d: [] for d in dims} |
| tacc = {d: [] for d in dims if d > R} |
| for _ in range(reps): |
| A, Xt = rdpg.weighted_rdpg(n, R, rng, kind="exponential", scale=0.1) |
| o, _, _ = _ase_err(A, Xt, dims) |
| for d in dims: |
| acc[d].append(o[d]["err"]) |
| if d > R: |
| tacc[d].append(o[d]["trail"]) |
| res[n] = {"dims": dims, |
| "err_mean": [float(np.mean(acc[d])) for d in dims], |
| "err_se": [float(np.std(acc[d], ddof=1) / np.sqrt(reps)) for d in dims], |
| "trail_mean": {str(d): float(np.mean(tacc[d])) for d in dims if d > R}} |
| ks = [d - R for d in dims if d > R] |
| res[n]["k_slope_err"] = rdpg.loglog_slope( |
| ks, [np.mean(acc[d]) for d in dims if d > R]) |
| res[n]["k_slope_trail"] = rdpg.loglog_slope( |
| ks, [np.mean(tacc[d]) for d in dims if d > R]) |
| res[n]["argmin_dim"] = int(dims[int(np.argmin([np.mean(acc[d]) for d in dims]))]) |
| return res |
|
|
|
|
| def sigma(rng, reps=8): |
| """Theorem 3.2 writes the over-specification term as sqrt(sigma^2 k)(...)/n^{1/4}, |
| i.e. sigma^1. Lemma 2.1 builds that term as ||Uhat_{r+1:r+k}||_{2,inf}*||E||^{1/2}, |
| and ||E|| ~ 2 sigma sqrt(n), which predicts sigma^{1/2}. We measure the exponent.""" |
| n, d = 2000, 10 |
| sigmas = [0.025, 0.05, 0.1, 0.2, 0.4, 0.8] |
| T, EV, SV, EX = [], [], [], [] |
| for sg in sigmas: |
| t, ev, sv, ex = [], [], [], [] |
| for _ in range(reps): |
| A, Xt = rdpg.weighted_rdpg(n, R, rng, kind="normal", scale=sg) |
| o, s, U = _ase_err(A, Xt, [R, d]) |
| t.append(o[d]["trail"]) |
| ev.append(o[d]["U_trail_2inf"]) |
| sv.append(np.sqrt(o[d]["s_hat_d"])) |
| ex.append(o[d]["err"] - o[R]["err"]) |
| T.append(float(np.mean(t))); EV.append(float(np.mean(ev))) |
| SV.append(float(np.mean(sv))); EX.append(float(np.mean(ex))) |
| return {"n": n, "d": d, "k": d - R, "sigmas": sigmas, |
| "trail_mean": T, "U_trail_2inf_mean": EV, "sqrt_s_hat_mean": SV, |
| "excess_err_mean": EX, |
| "exp_trail": rdpg.loglog_slope(sigmas, T), |
| "exp_U": rdpg.loglog_slope(sigmas, EV), |
| "exp_sqrt_s": rdpg.loglog_slope(sigmas, SV), |
| "exp_excess": rdpg.loglog_slope(sigmas, EX), |
| "bbp_ratio": [float(n / 30 / (sg * np.sqrt(n))) for sg in sigmas]} |
|
|
|
|
| def rho(rng, reps=8): |
| """Theorem 3.2 (k<0): floor >~ sqrt(|k| rho_n). Sweep rho at fixed n, |k|=2.""" |
| n, d = 4000, 3 |
| rhos = [1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125] |
| E, B = [], [] |
| for rh in rhos: |
| e, b = [], [] |
| for _ in range(reps): |
| X = rdpg.dirichlet_latent(n, R, rng) |
| A, Xt = rdpg.weighted_rdpg(n, R, rng, rho=rh, kind="normal", |
| scale=0.02, X=X) |
| o, _, _ = _ase_err(A, Xt, [d]) |
| s_nz = np.sort(np.linalg.eigvalsh(rh * (X.T @ X)))[::-1] |
| e.append(o[d]["err"]) |
| b.append(float(np.sqrt(s_nz[d:].sum() / n))) |
| E.append(float(np.mean(e))); B.append(float(np.mean(b))) |
| return {"n": n, "d": d, "k": d - R, "rhos": rhos, "floor_mean": E, |
| "lemma21_bound_mean": B, |
| "ratio": [float(a / b) for a, b in zip(E, B)], |
| "sqrt_k_rho": [float(np.sqrt(2 * rh)) for rh in rhos], |
| "exp_floor": rdpg.loglog_slope(rhos, E), |
| "exp_bound": rdpg.loglog_slope(rhos, B)} |
|
|
|
|
| def ktrue(rng, reps=8): |
| """|k|-dependence of the under-specification floor vs the exact Lemma 2.1 bound.""" |
| n = 4000 |
| dims = [1, 2, 3, 4] |
| E = {d: [] for d in dims} |
| B = {d: [] for d in dims} |
| for _ in range(reps): |
| X = rdpg.dirichlet_latent(n, R, rng) |
| A, Xt = rdpg.weighted_rdpg(n, R, rng, kind="normal", scale=0.1, X=X) |
| o, _, _ = _ase_err(A, Xt, dims) |
| s_nz = np.sort(np.linalg.eigvalsh(X.T @ X))[::-1] |
| for d in dims: |
| E[d].append(o[d]["err"]) |
| B[d].append(float(np.sqrt(s_nz[d:].sum() / n))) |
| return {"n": n, "dims": dims, |
| "floor_mean": [float(np.mean(E[d])) for d in dims], |
| "lemma21_bound_mean": [float(np.mean(B[d])) for d in dims], |
| "ratio": [float(np.mean(E[d]) / np.mean(B[d])) for d in dims], |
| "sqrt_k_rho": [float(np.sqrt(R - d)) for d in dims]} |
|
|
|
|
| def rankctrl(rng, reps=8): |
| """NEGATIVE CONTROL for the k<0 lower bound. At the SAME embedding |
| dimension d=4 we compare (i) a true rank-5 P (one signal direction is |
| discarded -> floor) with (ii) a true rank-4 P (nothing is discarded -> |
| no floor). If a floor appeared in (ii) as well, the effect would be an |
| artefact of the dimension count rather than of discarded signal.""" |
| dims = [4] |
| out = {"n": [], "rank5_d4": [], "rank4_d4": [], "rank5_d5": []} |
| for n in [500, 1000, 2000, 4000]: |
| a, b, c = [], [], [] |
| for _ in range(reps): |
| X5 = rdpg.dirichlet_latent(n, 5, rng) |
| A5, Xt5 = rdpg.weighted_rdpg(n, 5, rng, kind="normal", scale=0.1, X=X5) |
| o5, _, _ = _ase_err(A5, Xt5, [4, 5]) |
| X4 = rdpg.dirichlet_latent(n, 4, rng) |
| A4, Xt4 = rdpg.weighted_rdpg(n, 4, rng, kind="normal", scale=0.1, X=X4) |
| o4, _, _ = _ase_err(A4, Xt4, [4], r=4) |
| a.append(o5[4]["err"]); b.append(o4[4]["err"]); c.append(o5[5]["err"]) |
| out["n"].append(n) |
| out["rank5_d4"].append(float(np.mean(a))) |
| out["rank4_d4"].append(float(np.mean(b))) |
| out["rank5_d5"].append(float(np.mean(c))) |
| for key in ["rank5_d4", "rank4_d4", "rank5_d5"]: |
| out["slope_" + key] = rdpg.loglog_slope(out["n"], out[key]) |
| return out |
|
|
|
|
| def sparse(rng, reps=6): |
| """Sparse binary RDPG, rho_n = n^{-gamma} (paper Eq. 18). The k<0 floor is |
| predicted to be sqrt(|k| rho_n) ~ n^{-gamma/2}: a *decaying* floor whose |
| slope is set by gamma. This is a sharp, falsifiable prediction.""" |
| dims = [3, 4, 5, 6, 7, 10, 20] |
| ngrid = [500, 1000, 2000, 4000, 8000] |
| res = {} |
| for gam in [0.0, 0.25, 0.5]: |
| per = {d: [] for d in dims} |
| bnd, dl = [], [] |
| for n in ngrid: |
| acc = {d: [] for d in dims} |
| bb, dd = [], [] |
| for _ in range(reps): |
| X = rdpg.dirichlet_latent(n, R, rng) |
| rh = n ** (-gam) |
| A, Xt = rdpg.binary_rdpg(n, R, rng, rho=rh, X=X) |
| o, s, U = _ase_err(A, Xt, dims) |
| for d in dims: |
| acc[d].append(o[d]["err"]) |
| s_nz = np.sort(np.linalg.eigvalsh(rh * (X.T @ X)))[::-1] |
| bb.append(float(np.sqrt(s_nz[3:].sum() / n))) |
| dd.append(float(np.abs(U[:, R]).max())) |
| for d in dims: |
| per[d].append(float(np.mean(acc[d]))) |
| bnd.append(float(np.mean(bb))) |
| dl.append(float(np.mean(dd))) |
| res[str(gam)] = {"ngrid": ngrid, |
| "err": {str(d): per[d] for d in dims}, |
| "lemma21_bound_d3": bnd, |
| "deloc_rp1": dl, |
| "slopes": {str(d): rdpg.loglog_slope(ngrid, per[d]) for d in dims}, |
| "slope_bound_d3": rdpg.loglog_slope(ngrid, bnd), |
| "slope_deloc": rdpg.loglog_slope(ngrid, dl), |
| "predicted_floor_slope": -gam / 2} |
| return res |
|
|
|
|
| def hubctrl(rng, reps=6): |
| """NEGATIVE CONTROL for Conjecture 1. The conjecture relaxes Assumption A7 |
| to c <= E|E_ij|^2 <= C, i.e. entry variances bounded AWAY FROM ZERO. A |
| bounded-degree binary graph (rho_n = c/n) violates the lower bound c, and |
| there eigenvector localisation is expected. We compare the delocalisation |
| statistic in the dense (conjecture-compliant) and bounded-degree regimes.""" |
| ngrid = [1000, 2000, 4000] |
| out = {"ngrid": ngrid, "dense": [], "bounded_degree": [], |
| "dense_ipr": [], "bounded_degree_ipr": []} |
| for n in ngrid: |
| a, b, ai, bi = [], [], [], [] |
| for _ in range(reps): |
| A, _ = rdpg.binary_rdpg(n, R, rng, rho=1.0) |
| s, U = rdpg.full_spectrum(A) |
| a.append(float(np.abs(U[:, R]).max())); ai.append(float((U[:, R] ** 4).sum())) |
| A, _ = rdpg.binary_rdpg(n, R, rng, rho=12.0 / n) |
| s, U = rdpg.full_spectrum(A) |
| b.append(float(np.abs(U[:, R]).max())); bi.append(float((U[:, R] ** 4).sum())) |
| out["dense"].append(float(np.mean(a))) |
| out["bounded_degree"].append(float(np.mean(b))) |
| out["dense_ipr"].append(float(np.mean(ai))) |
| out["bounded_degree_ipr"].append(float(np.mean(bi))) |
| out["slope_dense"] = rdpg.loglog_slope(ngrid, out["dense"]) |
| out["slope_bounded_degree"] = rdpg.loglog_slope(ngrid, out["bounded_degree"]) |
| out["slope_dense_ipr"] = rdpg.loglog_slope(ngrid, out["dense_ipr"]) |
| out["slope_bd_ipr"] = rdpg.loglog_slope(ngrid, out["bounded_degree_ipr"]) |
| return out |
|
|
|
|
| def decompctrl(rng, reps=8): |
| """NEGATIVE CONTROL for the two-term decomposition of Lemma 2.1. We rebuild |
| the d-dimensional embedding with its trailing columns SET TO ZERO, |
| [Xhat_{1:r} | 0], and re-solve the same O_d Procrustes problem. If the |
| n^{-1/4} degradation really comes from the trailing block, this ablated |
| embedding must fall back exactly onto the n^{-1/2} base curve.""" |
| dims = [10, 20] |
| out = {"ngrid": [], "base": [], "full": {str(d): [] for d in dims}, |
| "ablated": {str(d): [] for d in dims}} |
| for n in [500, 1000, 2000, 4000]: |
| b, f, a = [], {d: [] for d in dims}, {d: [] for d in dims} |
| for _ in range(reps): |
| A, Xt = rdpg.weighted_rdpg(n, R, rng, kind="normal", scale=0.1) |
| s, U = rdpg.full_spectrum(A) |
| Xr = rdpg.ase_from_spectrum(s, U, R) |
| b.append(rdpg.err_2inf(Xr, Xt)[0]) |
| for d in dims: |
| Xh = rdpg.ase_from_spectrum(s, U, d) |
| f[d].append(rdpg.err_2inf(Xh, Xt)[0]) |
| Xz = Xh.copy() |
| Xz[:, R:] = 0.0 |
| a[d].append(rdpg.err_2inf(Xz, Xt)[0]) |
| out["ngrid"].append(n) |
| out["base"].append(float(np.mean(b))) |
| for d in dims: |
| out["full"][str(d)].append(float(np.mean(f[d]))) |
| out["ablated"][str(d)].append(float(np.mean(a[d]))) |
| out["slope_base"] = rdpg.loglog_slope(out["ngrid"], out["base"]) |
| out["slope_full"] = {k: rdpg.loglog_slope(out["ngrid"], v) for k, v in out["full"].items()} |
| out["slope_ablated"] = {k: rdpg.loglog_slope(out["ngrid"], v) for k, v in out["ablated"].items()} |
| return out |
|
|
|
|
| if __name__ == "__main__": |
| which = sys.argv[1] |
| rng = np.random.default_rng(hash(which) % (2 ** 31)) |
| fn = {"ksweep": ksweep, "sigma": sigma, "rho": rho, "ktrue": ktrue, |
| "rankctrl": rankctrl, "sparse": sparse, "hubctrl": hubctrl, |
| "decompctrl": decompctrl}[which] |
| res = fn(rng) |
| with open(f"outputs/extra_{which}.json", "w") as f: |
| json.dump(res, f, indent=1) |
| print(which, "done") |
|
|