""" dicke_block_collapse.py Reproduces the numerical claims of Proposition (multinomial block collapse) and Example (Dicke-state scaling): for a Dicke state |D_n^k>, the ambient 3^m x 3^l shadow-map block M_{S->S^c}(rho) has EXACTLY the same singular values (hence the same nuclear norm) as a much smaller multinomial-weighted matrix C_hat of size C(m+2,2) x C(l+2,2). This lets ||M_{S->S^c}||_* be computed exactly for cluster sizes far beyond what the ambient matrix could ever be built at. Two things are verified/produced: (1) Exact-arithmetic closed-form Dicke correlator, checked against brute-force dense simulation for small n. (2) Exact match between the ambient matrix M and the reduced matrix C_hat (singular values, nuclear norm), then a scaling table pushing m, l, n far beyond brute-force reach. Run: python3 dicke_block_collapse.py """ import time from itertools import combinations, product from math import comb, factorial import numpy as np # ---------------------------------------------------------------------- # Part 0: brute-force reference (only used for small-n sanity checks) # ---------------------------------------------------------------------- _I = np.eye(2, dtype=complex) _X = np.array([[0, 1], [1, 0]], dtype=complex) _Y = np.array([[0, -1j], [1j, 0]], dtype=complex) _Z = np.array([[1, 0], [0, -1]], dtype=complex) _PAULI = {"i": _I, "x": _X, "y": _Y, "z": _Z} def _kron_list(ops): out = ops[0] for o in ops[1:]: out = np.kron(out, o) return out def dicke_state_vector(n, k): """Dense state vector of the n-qubit weight-k Dicke state (small n only).""" dim = 2 ** n psi = np.zeros(dim, dtype=complex) for bits in combinations(range(n), k): idx = 0 for b in bits: idx |= 1 << (n - 1 - b) psi[idx] = 1.0 psi /= np.linalg.norm(psi) return psi def brute_force_expectation(n, k, labels): """ by dense simulation. labels: length-n tuple in 'ixyz'.""" psi = dicke_state_vector(n, k) op = _kron_list([_PAULI[c] for c in labels]) return psi.conj() @ op @ psi # ---------------------------------------------------------------------- # Part 1: closed-form Dicke correlator (Lemma: closed-form Dicke correlator) # ---------------------------------------------------------------------- def dicke_correlator(n, k, n_I, n_X, n_Y, n_Z): """ for a Pauli-string TYPE (n_I identities, n_X X's, n_Y Y's, n_Z Z's; n_I+n_X+n_Y+n_Z = n), via Eq. (dicke-correlator). All intermediate sums are kept as exact Python integers to avoid catastrophic cancellation between huge binomial coefficients; only the final division by C(n,k) is converted to a float (Python performs a correctly-rounded true division even for arbitrary-size integers). """ assert n_I + n_X + n_Y + n_Z == n m_xy = n_X + n_Y if m_xy % 2 != 0: return 0.0 half = m_xy // 2 target_iz = k - half if target_iz < 0 or target_iz > n_I + n_Z: return 0.0 total = 0 # exact integer accumulator for a_x in range(max(0, half - n_Y), min(n_X, half) + 1): a_y = half - a_x w_xy = comb(n_X, a_x) * comb(n_Y, a_y) * (-1) ** a_y for a_i in range(max(0, target_iz - n_Z), min(n_I, target_iz) + 1): a_z = target_iz - a_i w_iz = comb(n_I, a_i) * comb(n_Z, a_z) * (-1) ** a_z total += w_xy * w_iz denom = comb(n, k) return (1j ** n_Y) * (total / denom) # ---------------------------------------------------------------------- # Part 2: ambient block matrix vs. multinomial-reduced matrix # ---------------------------------------------------------------------- def multinomial(n, counts): r = factorial(n) for c in counts: r //= factorial(c) return r def active_types(size): """All (a_x, a_y, a_z) with a_x+a_y+a_z == size (the 'fully active' sector).""" return [ (ax, ay, size - ax - ay) for ax in range(size + 1) for ay in range(size + 1 - ax) ] def ambient_block_matrix(n, k, m, l): """The full 3^m x 3^l block M_{S->S^c}(rho) in the raw Pauli-string basis.""" labels_s = list(product("xyz", repeat=m)) labels_t = list(product("xyz", repeat=l)) rest = n - m - l M = np.zeros((len(labels_s), len(labels_t)), dtype=complex) for i, ls in enumerate(labels_s): cs = {c: ls.count(c) for c in "xyz"} for j, lt in enumerate(labels_t): ct = {c: lt.count(c) for c in "xyz"} M[i, j] = dicke_correlator( n, k, rest, cs["x"] + ct["x"], cs["y"] + ct["y"], cs["z"] + ct["z"] ) return M def reduced_block_matrix(n, k, m, l): """ The multinomial-weighted reduced matrix C_hat of Eq. (reduced-dicke-matrix), size C(m+2,2) x C(l+2,2), with the SAME singular values as the ambient 3^m x 3^l block (Proposition: multinomial block collapse). """ src_types = active_types(m) tgt_types = active_types(l) rest = n - m - l C = np.zeros((len(src_types), len(tgt_types)), dtype=complex) for i, (sx, sy, sz) in enumerate(src_types): w_s = multinomial(m, [sx, sy, sz]) for j, (tx, ty, tz) in enumerate(tgt_types): w_t = multinomial(l, [tx, ty, tz]) val = dicke_correlator(n, k, rest, sx + tx, sy + ty, sz + tz) C[i, j] = float(np.sqrt(float(w_s * w_t))) * val return C # ---------------------------------------------------------------------- # Part 3: checks and scaling table # ---------------------------------------------------------------------- def check_formula_against_brute_force(n=8, k=3, trials=30, seed=0): rng = np.random.default_rng(seed) max_err = 0.0 for _ in range(trials): labels = rng.choice(list("ixyz"), size=n) counts = {c: int((labels == c).sum()) for c in "ixyz"} ref = brute_force_expectation(n, k, tuple(labels)) val = dicke_correlator(n, k, counts["i"], counts["x"], counts["y"], counts["z"]) max_err = max(max_err, abs(ref - val)) print(f"[check 1] closed-form vs. brute force (n={n}, k={k}, {trials} random " f"Pauli strings): max error = {max_err:.2e}") def check_ambient_vs_reduced(n=9, k=4, m=4, l=3): M = ambient_block_matrix(n, k, m, l) C = reduced_block_matrix(n, k, m, l) sv_full = np.sort(np.linalg.svd(M, compute_uv=False))[::-1] sv_red = np.sort(np.linalg.svd(C, compute_uv=False))[::-1] print(f"[check 2] ambient {M.shape} vs. reduced {C.shape} (n={n}, k={k}, " f"m={m}, l={l})") print(f" ||M||_* = {sv_full.sum().real:.10f}") print(f" ||C||_* = {sv_red.sum().real:.10f}") print(f" max |sv_full - sv_red| (top {min(6, len(sv_red))}) = " f"{np.max(np.abs(sv_full[:len(sv_red)][:6] - sv_red[:6])):.2e}") def scaling_table(k=3, cases=((10, 4, 4), (30, 10, 8), (60, 20, 15), (100, 30, 25), (200, 40, 35))): print(f"[scaling table] fixed weight k={k}, reduced matrix only " f"(ambient matrix is never built)") header = f"{'n':>5}{'m':>5}{'l':>5} {'C_hat shape':>14} {'ambient 3^m x 3^l':>26} {'||C_hat||_*':>13} {'time':>8}" print(header) for n, m, l in cases: t0 = time.time() C = reduced_block_matrix(n, k, m, l) sv = np.linalg.svd(C, compute_uv=False) dt = time.time() - t0 ambient = f"{3.0**m:.2e} x {3.0**l:.2e}" print(f"{n:>5}{m:>5}{l:>5} {str(C.shape):>14} {ambient:>26} " f"{sv.sum().real:>13.6f} {dt:>7.3f}s") if __name__ == "__main__": check_formula_against_brute_force() print() check_ambient_vs_reduced() print() scaling_table()