add script for multinomial block collapse and Dicke state scaling verification
This commit is contained in:
parent
f195204a30
commit
6ea7900b55
2 changed files with 412 additions and 1 deletions
208
scripts/dicker_block_collapse.py
Normal file
208
scripts/dicker_block_collapse.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
"""
|
||||
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):
|
||||
"""<D_n^k| P |D_n^k> 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):
|
||||
"""
|
||||
<D_n^k| P |D_n^k> 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue