"""Core harness: RDPG generation, adjacency spectral embedding, (2,inf) error. Everything here follows the paper "On the Effect of Misspecifying the Embedding Dimension in Low-rank Network Models" (Taing & Levin, arXiv:2601.06014), Definitions 1-2, Lemma 2.1, Theorems 3.1-3.2 and Section 4. """ import warnings import numpy as np def matmul_sanity(): """numpy on Apple Accelerate emits spurious divide/overflow/invalid RuntimeWarnings from matmul on perfectly finite inputs. Before silencing them we check a matmul against an independent reference (einsum + lstsq).""" rng = np.random.default_rng(0) A = rng.standard_normal((300, 120)) B = rng.standard_normal((120, 90)) with warnings.catch_warnings(): warnings.simplefilter("ignore", RuntimeWarning) C = A @ B ref = np.einsum("ij,jk->ik", A, B) err_einsum = float(np.max(np.abs(C - ref)) / np.max(np.abs(ref))) # independent route: solve A z = C column-wise, must recover B z, *_ = np.linalg.lstsq(A, C, rcond=None) err_lstsq = float(np.max(np.abs(z - B))) finite = bool(np.all(np.isfinite(C))) return {"rel_err_vs_einsum": err_einsum, "max_err_vs_lstsq": err_lstsq, "all_finite": finite, "pass": bool(err_einsum < 1e-12 and err_lstsq < 1e-9 and finite)} warnings.filterwarnings("ignore", category=RuntimeWarning, message=".*encountered in matmul.*") # ---------------------------------------------------------------- generation def dirichlet_latent(n, r, rng, alpha=1.0): """Rows of X iid Dir(alpha,...,alpha). Paper Sec. 4.1 uses alpha=(1,...,1), r=5.""" return rng.dirichlet(np.full(r, alpha), size=n) def sym_noise(n, rng, kind="normal", scale=1.0): """Symmetric mean-zero noise, entries iid up to symmetry (paper Sec. 4.1).""" if kind == "normal": Z = rng.standard_normal((n, n)) * scale elif kind == "laplace": # Paper (b) writes "E_ij + 1 ~ Laplace(0,1)", but immediately after states # that only settings (c) and (d) are re-centred to have mean zero. A # Laplace(0,1) is already mean zero, so we read (b) as E_ij ~ Laplace(0,1). Z = rng.laplace(0.0, 1.0, (n, n)) * scale elif kind == "exponential": # E_ij + 1 ~ Exp(1) Z = (rng.exponential(1.0, (n, n)) - 1.0) * scale elif kind == "poisson": # E_ij + 1 ~ Pois(1) Z = (rng.poisson(1.0, (n, n)) - 1.0).astype(float) * scale elif kind == "cauchy": # violates Assumption A7 (no 2nd moment) Z = rng.standard_cauchy((n, n)) * scale else: raise ValueError(kind) E = np.triu(Z) E = E + np.triu(Z, 1).T return E def weighted_rdpg(n, r, rng, rho=1.0, kind="normal", scale=1.0, X=None): """A = rho X X^T + E (paper Eq. 14 / 16). Returns A, rho^{1/2} X.""" if X is None: X = dirichlet_latent(n, r, rng) A = rho * (X @ X.T) + sym_noise(n, rng, kind, scale) return A, np.sqrt(rho) * X def binary_rdpg(n, r, rng, rho=1.0, X=None): """A_ij ~ Bern(rho x_i^T x_j), i0 (pad Xtrue).""" n, a = Xhat.shape b = Xtrue.shape[1] m = max(a, b) if a < m: Xhat = np.hstack([Xhat, np.zeros((n, m - a))]) if b < m: Xtrue = np.hstack([Xtrue, np.zeros((n, m - b))]) Uu, _, Vt = np.linalg.svd(Xhat.T @ Xtrue) W = Uu @ Vt D = Xhat @ W - Xtrue return float(np.max(np.linalg.norm(D, axis=1))), D def two_inf(M): return float(np.max(np.linalg.norm(M, axis=1))) # -------------------------------------------------------------------- fitting def loglog_slope(x, y): """Least-squares slope of log y on log x, with R^2 and stderr.""" x = np.asarray(x, float) y = np.asarray(y, float) lx, ly = np.log(x), np.log(y) A = np.vstack([lx, np.ones_like(lx)]).T coef, *_ = np.linalg.lstsq(A, ly, rcond=None) pred = A @ coef ss_res = float(((ly - pred) ** 2).sum()) ss_tot = float(((ly - ly.mean()) ** 2).sum()) r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else float("nan") dof = max(len(x) - 2, 1) se = float(np.sqrt(ss_res / dof / ((lx - lx.mean()) ** 2).sum())) return float(coef[0]), float(coef[1]), r2, se