|
| 1 | +""" |
| 2 | +EPIC 2 "Professors' Wall" — Experiment 2: THE FREE-PROBABILITY CALCULATOR |
| 3 | +======================================================================== |
| 4 | +
|
| 5 | +CLAIM under test |
| 6 | +---------------- |
| 7 | +Given two large operators A, B that are *asymptotically free*, predict the FULL |
| 8 | +spectrum of |
| 9 | +
|
| 10 | + A ⊞ B (free additive sum) and A ⊠ B (free mult. product) |
| 11 | +
|
| 12 | +PURELY from the two individual spectra — never forming A+B or A·B, never |
| 13 | +diagonalizing the composite — via free convolution, and match the TRUE |
| 14 | +(dense-formed) composite spectrum to ~1%. |
| 15 | +
|
| 16 | +AND: when A, B are NOT free, the certificate `freeness_defect` must blow up AND |
| 17 | +the free-convolution prediction must degrade — so the calculator KNOWS its |
| 18 | +domain. |
| 19 | +
|
| 20 | +What is "pure measure-level" here (the honest, strict reading): |
| 21 | + - ADDITIVE: s.boxplus(t) -> moments of A⊞B from κ_n(A)+κ_n(B). |
| 22 | + This touches ONLY the two harvested measures (nodes/weights). |
| 23 | + It does NOT call a joint matvec. (s + t, by contrast, |
| 24 | + re-probes the real sum operator Ax+Bx — exact but it DOES |
| 25 | + apply the composite action; we report it as a cross-check.) |
| 26 | + - MULTIPLICATIVE: S_{A⊠B}(w) = S_A(w)·S_B(w) (resona.lift.s_transform), then |
| 27 | + reconstruct the moments of A⊠B from the product S-transform. |
| 28 | + Again ONLY the two measures are used. |
| 29 | +
|
| 30 | +GROUND TRUTH (dense, ONLY for verification): form A+B, A·B at N≈3000, eigvalsh, |
| 31 | +compare predicted moments / density / edges. |
| 32 | +
|
| 33 | +Run: PYTHONPATH=/home/dima/resona python experiments/exp2_free_probability_calculator.py |
| 34 | +""" |
| 35 | +import sys, os |
| 36 | +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) |
| 37 | +import numpy as np |
| 38 | +import resona |
| 39 | +from resona.lift import s_transform |
| 40 | + |
| 41 | + |
| 42 | +# ───────────────────────── operator builders (matrix-free matvecs) ────────── |
| 43 | +def diag_matvec(d): |
| 44 | + return lambda x: d[:, None] * x if x.ndim == 2 else d * x |
| 45 | + |
| 46 | + |
| 47 | +def dense_sym_matvec(M): |
| 48 | + return lambda x: M @ x |
| 49 | + |
| 50 | + |
| 51 | +def haar_orthogonal(N, rng): |
| 52 | + """A Haar-distributed orthogonal N×N (QR of a Gaussian, sign-fixed).""" |
| 53 | + Z = rng.standard_normal((N, N)) |
| 54 | + Q, R = np.linalg.qr(Z) |
| 55 | + return Q * np.sign(np.diag(R))[None, :] |
| 56 | + |
| 57 | + |
| 58 | +# ───────────────────── multiplicative reconstruction (measure-level) ──────── |
| 59 | +def _series_inverse(c, order): |
| 60 | + """Compositional inverse of χ(w) = Σ_{k>=1} c[k] w^k (c[0]=0, c[1]!=0). |
| 61 | +
|
| 62 | + Returns ψ(z) = Σ_{k>=1} b[k] z^k with χ(ψ(z)) = z up to `order` |
| 63 | + (Lagrange inversion, done iteratively / exactly in float).""" |
| 64 | + c = np.asarray(c, float) |
| 65 | + b = np.zeros(order + 1) |
| 66 | + b[1] = 1.0 / c[1] |
| 67 | + # solve χ(ψ(z)) = z order by order: [z^n] Σ_k c_k ψ^k = δ_{n,1} |
| 68 | + psi_pows = {1: b.copy()} # ψ^1 coeffs (updated as b grows) |
| 69 | + |
| 70 | + def poly_mul(a, d): |
| 71 | + out = np.zeros(order + 1) |
| 72 | + for i in range(order + 1): |
| 73 | + if a[i] == 0: |
| 74 | + continue |
| 75 | + for j in range(order + 1 - i): |
| 76 | + out[i + j] += a[i] * d[j] |
| 77 | + return out |
| 78 | + |
| 79 | + for n in range(2, order + 1): |
| 80 | + # contribution to [z^n] from all c_k ψ^k with current b[1..n-1] known; |
| 81 | + # b[n] enters linearly through c_1 · ([z^n] ψ) = c_1 · b[n]. |
| 82 | + # Build ψ with b[n]=0, get residual, then set b[n] = -residual / c_1. |
| 83 | + psi = b.copy(); psi[n] = 0.0 |
| 84 | + acc = np.zeros(order + 1) |
| 85 | + pk = np.zeros(order + 1); pk[0] = 1.0 # ψ^0 |
| 86 | + for k in range(1, n + 1): |
| 87 | + pk = poly_mul(pk, psi) |
| 88 | + acc += c[k] * pk |
| 89 | + residual = acc[n] # [z^n] with b[n]=0 |
| 90 | + b[n] = -residual / c[1] |
| 91 | + return b[1:order + 1] |
| 92 | + |
| 93 | + |
| 94 | +def moments_from_product_S(sA, sB, order=4, w_grid=None): |
| 95 | + """Moments m_1..m_order of A ⊠ B from S_{A⊠B}(w) = S_A(w)·S_B(w). |
| 96 | +
|
| 97 | + Uses ONLY the two spectra (via resona.lift.s_transform). Reconstruction: |
| 98 | +
|
| 99 | + S(w) = (1+w)/w · χ(w), χ = ψ^{-1}, ψ(z) = Σ_{k>=1} m_k z^k . |
| 100 | +
|
| 101 | + So χ(w) = w/(1+w) · S(w). We: |
| 102 | + 1. fit the Taylor coefficients c_k of χ(w) = Σ c_k w^k on a SMALL-w grid |
| 103 | + (well-conditioned for low k near 0), |
| 104 | + 2. compositionally invert χ -> ψ (Lagrange inversion), |
| 105 | + 3. read the moments m_k = [z^k] ψ(z). |
| 106 | +
|
| 107 | + Series inversion is exact in arithmetic; the only error is the χ-coeff fit, |
| 108 | + which is accurate at low order on a small grid (truncation ~ w^{order+1}). |
| 109 | + """ |
| 110 | + if w_grid is None: |
| 111 | + w_grid = np.linspace(0.002, 0.05, 30) # small w: low z, series valid |
| 112 | + S_AB = s_transform(sA, w_grid) * s_transform(sB, w_grid) |
| 113 | + chi = w_grid / (1.0 + w_grid) * S_AB # χ(w) |
| 114 | + # fit χ(w) = Σ_{k=1..order+1} c_k w^k (no constant; χ(0)=0) |
| 115 | + V = np.vstack([w_grid ** k for k in range(1, order + 2)]).T |
| 116 | + coef, *_ = np.linalg.lstsq(V, chi, rcond=None) |
| 117 | + c = np.concatenate([[0.0], coef]) # c[0]=0, c[1..order+1] |
| 118 | + return _series_inverse(c, order) # m_1..m_order |
| 119 | + |
| 120 | + |
| 121 | +# ───────────────────────────── ground-truth metrics ───────────────────────── |
| 122 | +def empirical_moments(eigs, order): |
| 123 | + return [float(np.mean(eigs ** p)) for p in range(1, order + 1)] |
| 124 | + |
| 125 | + |
| 126 | +def density_hist(eigs, edges): |
| 127 | + h, _ = np.histogram(eigs, bins=edges, density=True) |
| 128 | + return h |
| 129 | + |
| 130 | + |
| 131 | +def density_from_spectral(s, centers, eta): |
| 132 | + rho = s.density(centers, eta=eta) |
| 133 | + # normalize to a probability density on the grid |
| 134 | + dx = centers[1] - centers[0] |
| 135 | + return rho / (rho.sum() * dx) |
| 136 | + |
| 137 | + |
| 138 | +def report_block(title, pred_m, true_m, edges_pred, edges_true, l1=None): |
| 139 | + print(f"\n {title}") |
| 140 | + print(f" moments p : predicted true |Δ|/|true|") |
| 141 | + worst = 0.0 |
| 142 | + for p, (pm, tm) in enumerate(zip(pred_m, true_m), start=1): |
| 143 | + rel = abs(pm - tm) / max(abs(tm), 1e-12) |
| 144 | + worst = max(worst, rel) |
| 145 | + print(f" m_{p} : {pm:12.5f} {tm:12.5f} {rel*100:7.3f}%") |
| 146 | + print(f" edges : pred [{edges_pred[0]:.3f}, {edges_pred[1]:.3f}]" |
| 147 | + f" true [{edges_true[0]:.3f}, {edges_true[1]:.3f}]") |
| 148 | + edge_err = max(abs(edges_pred[0] - edges_true[0]), |
| 149 | + abs(edges_pred[1] - edges_true[1])) / (edges_true[1] - edges_true[0]) |
| 150 | + print(f" edge err : {edge_err*100:.2f}% of span") |
| 151 | + if l1 is not None: |
| 152 | + print(f" density L1: {l1:.4f}") |
| 153 | + print(f" >>> worst moment rel-error: {worst*100:.3f}%") |
| 154 | + return worst, edge_err |
| 155 | + |
| 156 | + |
| 157 | +# ════════════════════════════════════════════════════════════════════════════ |
| 158 | +def main(): |
| 159 | + N = 3000 |
| 160 | + ORDER = 4 |
| 161 | + rng = np.random.default_rng(7) |
| 162 | + print("=" * 78) |
| 163 | + print("EXP 2 — THE FREE-PROBABILITY CALCULATOR (N = %d)" % N) |
| 164 | + print("=" * 78) |
| 165 | + |
| 166 | + # ---- Build two operators that ARE asymptotically free ------------------- |
| 167 | + # A : a fixed deterministic spectrum (uniform on [0, 2] -> shifted so it has |
| 168 | + # a non-trivial, asymmetric distribution). Diagonal. |
| 169 | + # B : U A' U^T with U Haar-orthogonal, A' a DIFFERENT fixed spectrum |
| 170 | + # (uniform on [0.5, 1.5]). Rotating one operator's eigenbasis by an |
| 171 | + # independent Haar U makes A and B asymptotically free (Voiculescu). |
| 172 | + dA = np.linspace(0.2, 2.2, N) # A spectrum |
| 173 | + dAp = np.linspace(0.5, 1.5, N) # B's "own" spectrum |
| 174 | + U = haar_orthogonal(N, rng) |
| 175 | + B_dense = (U * dAp[None, :]) @ U.T # = U diag(dAp) U^T |
| 176 | + B_dense = 0.5 * (B_dense + B_dense.T) # symmetrize (kill fp drift) |
| 177 | + |
| 178 | + mvA = diag_matvec(dA) |
| 179 | + mvB = dense_sym_matvec(B_dense) |
| 180 | + |
| 181 | + # ---- PROBE each operator (matrix-free) ---------------------------------- |
| 182 | + sA = resona.of(mvA, N, k=64, probes=16, seed=1) |
| 183 | + sB = resona.of(mvB, N, k=64, probes=16, seed=2) |
| 184 | + print("\nIndividual spectra harvested (matrix-free Lanczos):") |
| 185 | + print(f" A: edges {sA.extreme()} moments {[round(sA.moment(p)/N,4) for p in range(1,4)]}") |
| 186 | + print(f" B: edges {sB.extreme()} moments {[round(sB.moment(p)/N,4) for p in range(1,4)]}") |
| 187 | + |
| 188 | + # ========================================================================= |
| 189 | + # (1) ADDITIVE: predict A⊞B from spectra alone (boxplus, no joint matvec) |
| 190 | + # ========================================================================= |
| 191 | + pred_add_m = sA.boxplus(sB, order=ORDER) # measure-level |
| 192 | + s_add_pred = sA.boxplus(sB, order=ORDER, as_spectral=True) # quadrature meas. |
| 193 | + # cross-check predictor that re-probes the REAL sum (exact, but uses A+B action) |
| 194 | + s_sum_reprobe = sA + sB |
| 195 | + |
| 196 | + # GROUND TRUTH: form A+B densely and diagonalize |
| 197 | + A_dense = np.diag(dA) |
| 198 | + eigs_sum = np.linalg.eigvalsh(A_dense + B_dense) |
| 199 | + true_add_m = empirical_moments(eigs_sum, ORDER) |
| 200 | + |
| 201 | + # density L1 between predicted (reprobe sum) and true |
| 202 | + lo, hi = eigs_sum.min(), eigs_sum.max() |
| 203 | + centers = np.linspace(lo, hi, 200) |
| 204 | + edges = np.linspace(lo, hi, 201) |
| 205 | + rho_true = density_hist(eigs_sum, edges) |
| 206 | + rho_pred = density_from_spectral(s_sum_reprobe, centers, eta=0.05) |
| 207 | + dx = centers[1] - centers[0] |
| 208 | + l1_add = float(np.sum(np.abs(rho_pred - rho_true)) * dx) |
| 209 | + |
| 210 | + # edges: boxplus as_spectral UNDERSHOOTS (inner nodes) by construction; |
| 211 | + # report both the measure predictor edges and the reprobe-sum edges. |
| 212 | + edges_pred_add = s_sum_reprobe.extreme() |
| 213 | + worst_add, eerr_add = report_block( |
| 214 | + "A ⊞ B (additive free convolution)", |
| 215 | + pred_add_m, true_add_m, edges_pred_add, (lo, hi), l1=l1_add) |
| 216 | + |
| 217 | + # ========================================================================= |
| 218 | + # (2) MULTIPLICATIVE: predict A⊠B from spectra alone (S_A·S_B) |
| 219 | + # ========================================================================= |
| 220 | + pred_mul_m = moments_from_product_S(sA, sB, order=ORDER) |
| 221 | + s_prod_reprobe = sA @ sB # re-probes A·B |
| 222 | + |
| 223 | + # GROUND TRUTH: form A·B. A·B is not symmetric, but its eigenvalues equal |
| 224 | + # those of the symmetric A^{1/2} B A^{1/2} (A,B PSD here) -> real, positive. |
| 225 | + Ah = np.diag(np.sqrt(dA)) |
| 226 | + sym_prod = Ah @ B_dense @ Ah |
| 227 | + sym_prod = 0.5 * (sym_prod + sym_prod.T) |
| 228 | + eigs_prod = np.linalg.eigvalsh(sym_prod) |
| 229 | + true_mul_m = empirical_moments(eigs_prod, ORDER) |
| 230 | + |
| 231 | + loP, hiP = eigs_prod.min(), eigs_prod.max() |
| 232 | + centersP = np.linspace(loP, hiP, 200) |
| 233 | + edgesP = np.linspace(loP, hiP, 201) |
| 234 | + rho_trueP = density_hist(eigs_prod, edgesP) |
| 235 | + rho_predP = density_from_spectral(s_prod_reprobe, centersP, eta=0.05) |
| 236 | + dxP = centersP[1] - centersP[0] |
| 237 | + l1_mul = float(np.sum(np.abs(rho_predP - rho_trueP)) * dxP) |
| 238 | + |
| 239 | + worst_mul, eerr_mul = report_block( |
| 240 | + "A ⊠ B (multiplicative free convolution, via S_A·S_B)", |
| 241 | + pred_mul_m, true_mul_m, s_prod_reprobe.extreme(), (loP, hiP), l1=l1_mul) |
| 242 | + |
| 243 | + # ========================================================================= |
| 244 | + # (3) FREENESS SELF-CERTIFICATION |
| 245 | + # free pair (A, B) vs NON-free pair (A, A_perm sharing eigenbasis) |
| 246 | + # ========================================================================= |
| 247 | + print("\n" + "-" * 78) |
| 248 | + print("FREENESS SELF-CERTIFICATION (does the calculator know when it is valid?)") |
| 249 | + print("-" * 78) |
| 250 | + |
| 251 | + # Non-free pair: C shares A's eigenbasis (both diagonal) -> they COMMUTE, |
| 252 | + # maximally non-free. C = diag of a shuffled-but-same-basis spectrum. |
| 253 | + dC = np.linspace(0.5, 1.5, N) # diagonal -> commutes with diagonal A |
| 254 | + mvC = diag_matvec(dC) |
| 255 | + sC = resona.of(mvC, N, k=64, probes=16, seed=3) |
| 256 | + |
| 257 | + defect_free = resona.free.freeness_defect(mvA, mvB, N, word="ABAB", |
| 258 | + probes=64, seed=11) |
| 259 | + defect_nonfree = resona.free.freeness_defect(mvA, mvC, N, word="ABAB", |
| 260 | + probes=64, seed=11) |
| 261 | + print(f"\n freeness_defect |τ(ÅB̊ÅB̊)| :") |
| 262 | + print(f" FREE pair (A, U·U^T) : {defect_free:.5f}") |
| 263 | + print(f" NON-free (A, C) commuting : {defect_nonfree:.5f}") |
| 264 | + print(f" contrast ratio : {defect_nonfree/max(defect_free,1e-9):.1f}×") |
| 265 | + |
| 266 | + # Now show the PREDICTION degrades on the non-free pair. |
| 267 | + # Predict A⊞C via boxplus (assumes freeness); compare to TRUE A+C. |
| 268 | + pred_add_nf = sA.boxplus(sC, order=ORDER) |
| 269 | + eigs_sum_nf = np.linalg.eigvalsh(np.diag(dA) + np.diag(dC)) # = dA+dC (commute) |
| 270 | + true_add_nf = empirical_moments(eigs_sum_nf, ORDER) |
| 271 | + worst_nf = max(abs(p - t) / max(abs(t), 1e-12) |
| 272 | + for p, t in zip(pred_add_nf, true_add_nf)) |
| 273 | + |
| 274 | + print(f"\n ⊞-prediction error (worst moment rel-err), free vs non-free:") |
| 275 | + print(f" FREE (A ⊞ B) : {worst_add*100:7.3f}% <- valid") |
| 276 | + print(f" NON-free (A ⊞ C) : {worst_nf*100:7.3f}% <- DEGRADES " |
| 277 | + f"(free-conv assumption violated)") |
| 278 | + print(f" degradation : {worst_nf/max(worst_add,1e-9):.1f}× worse") |
| 279 | + |
| 280 | + # ========================================================================= |
| 281 | + # VERDICT |
| 282 | + # ========================================================================= |
| 283 | + print("\n" + "=" * 78) |
| 284 | + print("VERDICT") |
| 285 | + print("=" * 78) |
| 286 | + add_ok = worst_add < 0.03 |
| 287 | + mul_ok = worst_mul < 0.05 |
| 288 | + cert_ok = (defect_nonfree > 10 * defect_free) and (worst_nf > 3 * worst_add) |
| 289 | + print(f" additive ⊞ : worst rel-err {worst_add*100:.2f}%, edge {eerr_add*100:.2f}%, " |
| 290 | + f"L1 {l1_add:.3f} -> {'OK' if add_ok else 'FAIL'}") |
| 291 | + print(f" mult. ⊠ : worst rel-err {worst_mul*100:.2f}%, edge {eerr_mul*100:.2f}%, " |
| 292 | + f"L1 {l1_mul:.3f} -> {'OK' if mul_ok else 'FAIL'}") |
| 293 | + print(f" self-cert : defect {defect_nonfree/max(defect_free,1e-9):.0f}× , " |
| 294 | + f"pred degrades {worst_nf/max(worst_add,1e-9):.0f}× " |
| 295 | + f"-> {'OK' if cert_ok else 'FAIL'}") |
| 296 | + |
| 297 | + if add_ok and mul_ok and cert_ok: |
| 298 | + verdict = "GREEN" |
| 299 | + elif (add_ok or mul_ok) and cert_ok: |
| 300 | + verdict = "YELLOW" |
| 301 | + else: |
| 302 | + verdict = "RED" |
| 303 | + print(f"\n OVERALL: {verdict}") |
| 304 | + return verdict |
| 305 | + |
| 306 | + |
| 307 | +if __name__ == "__main__": |
| 308 | + main() |
0 commit comments