quantum-shadow-maps_v2/scripts/ghz3_shadow_map_symbolic.py

159 lines
5.9 KiB
Python
Raw Permalink Normal View History

2026-07-26 14:09:49 +02:00
"""
ghz3_shadow_map_symbolic.py
Exact symbolic (sympy) construction of the combined shadow map M_A(rho) for the
three-qubit GHZ state, with source party A and target complement {B,C}.
This reproduces, with exact algebraic numbers (no floating point), the claim
from Section "Qubit examples" of the paper:
For |GHZ_3> = (|000> + |111>)/sqrt(2), the three singular values of the
normalized combined shadow map M_A(rho) are all equal to sqrt(2/3),
i.e. ||M_A(rho)||_* = sqrt(6).
Convention (matches the .tex draft):
- Pauli generators sigma_1=X, sigma_2=Y, sigma_3=Z, normalized by
tr(sigma_i sigma_j) = 2 delta_ij (qubit case, d_a = 2).
- Target sectors for source A are T in { {B}, {C}, {B,C} }, stacked as
rows of one 15 x 3 matrix (3 from B, 3 from C, 9 from BC).
- Combined shadow map normalization: 1/sqrt((d_a-1)(d_bar_a-1))
= 1/sqrt(1*3) = 1/sqrt(3) for n=3 qubits (Eq. "combined-map" in the note).
Run: python3 ghz3_shadow_map_symbolic.py
"""
import sympy as sp
from sympy import sqrt, I, simplify, Matrix, eye, zeros, re, Rational
# ---------------------------------------------------------------------
# 1. Pauli matrices (exact, symbolic entries)
# ---------------------------------------------------------------------
X = Matrix([[0, 1], [1, 0]])
Y = Matrix([[0, -I], [I, 0]])
Z = Matrix([[1, 0], [0, -1]])
I2 = eye(2)
PAULIS = {'x': X, 'y': Y, 'z': Z}
def kron(A, B):
"""Kronecker (tensor) product of two sympy matrices, built manually
so everything stays exact/symbolic (no numeric backend needed)."""
mA, nA = A.shape
mB, nB = B.shape
out = zeros(mA * mB, nA * nB)
for i in range(mA):
for j in range(nA):
out[i * mB:(i + 1) * mB, j * nB:(j + 1) * nB] = A[i, j] * B
return out
def kron3(a, b, c):
"""Tensor product of three single-qubit operators -> 8x8 matrix."""
return kron(kron(a, b), c)
# Embeddings of a single-qubit operator P onto party A, B, or C
# within the 3-qubit Hilbert space (order A ⊗ B ⊗ C).
def op_A(P): return kron3(P, I2, I2)
def op_B(P): return kron3(I2, P, I2)
def op_C(P): return kron3(I2, I2, P)
# ---------------------------------------------------------------------
# 2. The GHZ_3 state
# ---------------------------------------------------------------------
def ghz3_state():
"""Density matrix of (|000> + |111>)/sqrt(2), as an 8x8 sympy Matrix."""
psi = zeros(8, 1)
psi[0, 0] = 1 / sqrt(2) # |000>
psi[7, 0] = 1 / sqrt(2) # |111>
rho = psi * psi.H # outer product, .H = conjugate transpose
return simplify(rho)
# ---------------------------------------------------------------------
# 3. Correlation-tensor entries and the shadow-map matrix
# ---------------------------------------------------------------------
def entry(rho, ops):
"""tr(rho * op1 * op2 * ...), simplified and forced real
(expectation values of Hermitian operators in a Hermitian state
are always real; re(...) just discards a numerically/symbolically
residual zero imaginary part)."""
M = None
for op in ops:
M = op if M is None else M * op
return simplify(re(simplify((rho * M).trace())))
def build_M(rho):
"""Unnormalized shadow-map matrix M_A(rho): 15 (target) x 3 (source A).
Row blocks, in order:
rows 0-2 : target sector T = {B} (source index x,y,z; target x,y,z)
rows 3-5 : target sector T = {C}
rows 6-14 : target sector T = {B,C} (9 = 3x3 combinations)
Column index: source generator on A, in order x,y,z.
"""
rows = []
for pb in ['x', 'y', 'z']:
rows.append([entry(rho, [op_A(PAULIS[pa]), op_B(PAULIS[pb])])
for pa in ['x', 'y', 'z']])
for pc in ['x', 'y', 'z']:
rows.append([entry(rho, [op_A(PAULIS[pa]), op_C(PAULIS[pc])])
for pa in ['x', 'y', 'z']])
for pb in ['x', 'y', 'z']:
for pc in ['x', 'y', 'z']:
rows.append([entry(rho, [op_A(PAULIS[pa]), op_B(PAULIS[pb]), op_C(PAULIS[pc])])
for pa in ['x', 'y', 'z']])
return Matrix(rows)
# ---------------------------------------------------------------------
# 4. Main: build, normalize, and diagonalize
# ---------------------------------------------------------------------
def main():
rho0 = ghz3_state()
print("tr(rho0) =", simplify(rho0.trace()), " (sanity check, should be 1)\n")
M0 = build_M(rho0)
print("Unnormalized shadow matrix M0 (15x3):")
sp.pprint(M0)
# Combined-map normalization for n=3 qubits: 1/sqrt((d_a-1)(d_bar_a-1)) = 1/sqrt(3)
norm_const = 1 / sqrt(3)
Mn0 = simplify(norm_const * M0)
# Singular values of Mn0 are sqrt(eigenvalues of the Gram matrix Mn0^T Mn0).
# This avoids sympy's (slower/less robust) generic SVD and is exact here
# because Mn0^T Mn0 is a small 3x3 symmetric matrix.
G = simplify(Mn0.T * Mn0)
print("\nGram matrix Mn0^T Mn0 =")
sp.pprint(G)
eigs = G.eigenvals() # dict: eigenvalue -> multiplicity
print("\nEigenvalues of the Gram matrix (= squared singular values):")
for ev, mult in eigs.items():
sigma = simplify(sqrt(ev))
print(f" lambda = {ev} (multiplicity {mult}) -> sigma = {sigma}"
f" = {float(sigma):.6f}")
print("\nExpected from the paper: sigma = sqrt(2/3) = sqrt(6)/3 ≈ 0.816497,"
" threefold degenerate.")
# Save U0, V0 (orthonormal bases of the degenerate singular subspace) for
# reuse in the perturbation-theory script. Since the Gram matrix is
# exactly (2/3) * I_3 here, the source space is untouched (V0 = I_3) and
# U0 is simply Mn0 rescaled to unit-norm columns.
sigma_val = sqrt(Rational(2, 3))
U0 = simplify(Mn0 / sigma_val)
V0 = eye(3)
print("\nU0 (15x3, orthonormal columns spanning the degenerate target subspace):")
sp.pprint(U0)
print("\nCheck U0^T U0 = I_3:", simplify(U0.T * U0))
return rho0, U0, V0, sigma_val
if __name__ == "__main__":
main()