167 lines
5.3 KiB
Python
167 lines
5.3 KiB
Python
"""
|
|
common.py
|
|
|
|
Shared numpy utilities for the Tiles-state / DPS-hierarchy scripts (03-08).
|
|
Convention: qutrits (d=3), Gell-Mann generators scaled so that
|
|
tr(sigma_i sigma_j) = d * delta_ij = 3 * delta_ij, matching the paper's own
|
|
convention (see the Tiles benchmark in symmetric_shadow_maps_formal.tex).
|
|
"""
|
|
import numpy as np
|
|
|
|
d = 3
|
|
|
|
# --- Gell-Mann matrices, paper convention ---
|
|
_lam = [None] * 8
|
|
_lam[0] = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]], dtype=complex)
|
|
_lam[1] = np.array([[0, -1j, 0], [1j, 0, 0], [0, 0, 0]], dtype=complex)
|
|
_lam[2] = np.array([[1, 0, 0], [0, -1, 0], [0, 0, 0]], dtype=complex)
|
|
_lam[3] = np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]], dtype=complex)
|
|
_lam[4] = np.array([[0, 0, -1j], [0, 0, 0], [1j, 0, 0]], dtype=complex)
|
|
_lam[5] = np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]], dtype=complex)
|
|
_lam[6] = np.array([[0, 0, 0], [0, 0, -1j], [0, 1j, 0]], dtype=complex)
|
|
_lam[7] = (1 / np.sqrt(3)) * np.array([[1, 0, 0], [0, 1, 0], [0, 0, -2]], dtype=complex)
|
|
GELLMANN = [np.sqrt(3 / 2) * L for L in _lam]
|
|
|
|
I3 = np.eye(3, dtype=complex)
|
|
I9 = np.eye(9, dtype=complex)
|
|
|
|
|
|
def opA(P):
|
|
return np.kron(P, I3)
|
|
|
|
|
|
def opB(P):
|
|
return np.kron(I3, P)
|
|
|
|
|
|
def e(i):
|
|
v = np.zeros(3)
|
|
v[i] = 1
|
|
return v
|
|
|
|
|
|
# --- The Tiles UPB bound-entangled state (Bennett, DiVincenzo, Mor, Shor,
|
|
# Smolin, Terhal 1999), as used in the paper's own qutrit benchmark ---
|
|
def _build_tiles():
|
|
sqrt2, sqrt3 = np.sqrt(2), np.sqrt(3)
|
|
upb = [
|
|
np.kron(e(0), (e(0) - e(1)) / sqrt2),
|
|
np.kron(e(2), (e(1) - e(2)) / sqrt2),
|
|
np.kron((e(0) - e(1)) / sqrt2, e(2)),
|
|
np.kron((e(1) - e(2)) / sqrt2, e(0)),
|
|
np.kron((e(0) + e(1) + e(2)) / sqrt3, (e(0) + e(1) + e(2)) / sqrt3),
|
|
]
|
|
P_UPB = sum(np.outer(v, v) for v in upb)
|
|
return ((np.eye(9) - P_UPB) / 4).astype(complex)
|
|
|
|
|
|
RHO_TILES = _build_tiles()
|
|
|
|
|
|
def noisy_tiles(p):
|
|
"""rho(p) = p * rho_Tiles + (1-p) * I/9"""
|
|
return p * RHO_TILES + (1 - p) * I9 / 9
|
|
|
|
|
|
# --- Qutrit Werner state (antisymmetric-subspace family), Werner 1989 ---
|
|
def _swap_matrix(dim=3):
|
|
V = np.zeros((dim * dim, dim * dim))
|
|
for a in range(dim):
|
|
for b in range(dim):
|
|
V[b * dim + a, a * dim + b] = 1
|
|
return V
|
|
|
|
|
|
SWAP_3 = _swap_matrix(3)
|
|
P_ANTI = (np.eye(9) - SWAP_3) / 2
|
|
DIM_ANTI = np.trace(P_ANTI).real # = 3
|
|
|
|
|
|
def werner_qutrit(p):
|
|
"""rho(p) = p * P_anti/3 + (1-p) * I/9. Known exact separability
|
|
threshold: p = 1/(d+1) = 1/4 (Werner 1989)."""
|
|
return p * P_ANTI / DIM_ANTI + (1 - p) * I9 / 9
|
|
|
|
|
|
# --- Correlation matrix / shadow-map criterion (paper Section "tensor
|
|
# viewpoint" / Tiles benchmark) ---
|
|
def correlation_matrix(rho):
|
|
T = np.zeros((8, 8))
|
|
for i in range(8):
|
|
for j in range(8):
|
|
T[i, j] = np.trace(rho @ opA(GELLMANN[i]) @ opB(GELLMANN[j])).real
|
|
return T
|
|
|
|
|
|
def shadow_map_nuclear_norm(rho):
|
|
"""||M_A(rho)||_*, normalization sqrt((d_A-1)(d_B-1)) = 2 for qutrits.
|
|
Separable states satisfy this <= 1 (Theorem "cut-bound" in the note)."""
|
|
T = correlation_matrix(rho)
|
|
return np.linalg.svd(T / 2.0, compute_uv=False).sum()
|
|
|
|
|
|
# --- Plain PPT (Peres-Horodecki) check ---
|
|
def plain_ppt_min_eig(rho, dim=3):
|
|
rho_pt = np.zeros((dim * dim, dim * dim), dtype=complex)
|
|
for a in range(dim):
|
|
for b in range(dim):
|
|
for ap in range(dim):
|
|
for bp in range(dim):
|
|
i, j = a * dim + b, ap * dim + bp
|
|
i2, j2 = a * dim + bp, ap * dim + b
|
|
rho_pt[i2, j2] = rho[i, j]
|
|
return np.linalg.eigvalsh(rho_pt).min()
|
|
|
|
|
|
def plain_ppt_feasible(rho, dim=3, tol=1e-9):
|
|
return plain_ppt_min_eig(rho, dim) >= -tol
|
|
|
|
|
|
# --- numpy partial traces, used only by the operator-Sinkhorn filter ---
|
|
def partial_trace_B_np(X, dim=3):
|
|
T = X.reshape(dim, dim, dim, dim)
|
|
return np.einsum('ikjk->ij', T)
|
|
|
|
|
|
def partial_trace_A_np(X, dim=3):
|
|
T = X.reshape(dim, dim, dim, dim)
|
|
return np.einsum('kikj->ij', T)
|
|
|
|
|
|
def _inv_sqrt_psd(M, eps=1e-12):
|
|
w, v = np.linalg.eigh(M)
|
|
w = np.clip(w, eps, None)
|
|
return (v * (w ** -0.5)) @ v.conj().T
|
|
|
|
|
|
def operator_sinkhorn(rho, dim=3, max_iter=3000, tol=1e-11, verbose=False):
|
|
"""Local-filtering (SLOCC) normal-form algorithm: alternately rescale
|
|
each side by (reduced state)^{-1/2} until both marginals are maximally
|
|
mixed. Standard algorithm (Verstraete-Dehaene-DeMoor 2001/2003); the
|
|
resulting fixed point is the Leinaas-Myrheim-Ovrum (2006) normal form."""
|
|
X = rho.copy() / np.trace(rho).real
|
|
devA = devB = None
|
|
for it in range(max_iter):
|
|
rhoA = partial_trace_B_np(X, dim)
|
|
rhoA /= np.trace(rhoA).real
|
|
devA = np.linalg.norm(rhoA - np.eye(dim) / dim)
|
|
FA = np.kron(_inv_sqrt_psd(rhoA), np.eye(dim))
|
|
X = FA @ X @ FA.conj().T
|
|
X /= np.trace(X).real
|
|
|
|
rhoB = partial_trace_A_np(X, dim)
|
|
rhoB /= np.trace(rhoB).real
|
|
devB = np.linalg.norm(rhoB - np.eye(dim) / dim)
|
|
FB = np.kron(np.eye(dim), _inv_sqrt_psd(rhoB))
|
|
X = FB @ X @ FB.conj().T
|
|
X /= np.trace(X).real
|
|
|
|
if devA < tol and devB < tol:
|
|
if verbose:
|
|
print(f" Sinkhorn converged after {it + 1} iterations")
|
|
break
|
|
else:
|
|
if verbose:
|
|
print(f" Sinkhorn did NOT fully converge in {max_iter} iters "
|
|
f"(devA={devA:.2e}, devB={devB:.2e})")
|
|
return X
|