feat: add numeric and symbolic scripts
This commit is contained in:
parent
6ea7900b55
commit
e80b7c3582
38 changed files with 3314 additions and 0 deletions
96
scripts/dps_hierarchy/01_werner_qubit_symbolic.py
Normal file
96
scripts/dps_hierarchy/01_werner_qubit_symbolic.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""
|
||||
01_werner_qubit_symbolic.py
|
||||
|
||||
Exact symbolic (sympy) check: the two-qubit Werner state
|
||||
|
||||
rho(p) = p |Psi-><Psi-| + (1-p) I/4, |Psi-> = (|01>-|10>)/sqrt(2)
|
||||
|
||||
is invariant under U (x) U for every U in SU(2). Since the adjoint
|
||||
representation of SU(2) on the traceless qubit Bloch space R^3 is
|
||||
irreducible (single isotype), the correlation matrix is forced to be
|
||||
proportional to the identity. We check this exactly and compare the
|
||||
resulting nuclear-norm threshold to the exact PPT/separability threshold.
|
||||
|
||||
Result: BOTH give exactly p = 1/3 -- the order-1 correlation-matrix
|
||||
criterion is exactly tight here (a low-dimensional special case, since
|
||||
PPT=separable for 2x2 systems by the Horodecki theorem).
|
||||
|
||||
Requires: sympy. Runtime: a few seconds.
|
||||
"""
|
||||
import sympy as sp
|
||||
from sympy import sqrt, I, simplify, Matrix, eye, zeros, re, symbols
|
||||
|
||||
X = Matrix([[0, 1], [1, 0]])
|
||||
Y = Matrix([[0, -I], [I, 0]])
|
||||
Z = Matrix([[1, 0], [0, -1]])
|
||||
I2 = eye(2)
|
||||
|
||||
|
||||
def kron(A, B):
|
||||
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 op_A(P):
|
||||
return kron(P, I2)
|
||||
|
||||
|
||||
def op_B(P):
|
||||
return kron(I2, P)
|
||||
|
||||
|
||||
p = symbols('p', real=True)
|
||||
|
||||
psi = zeros(4, 1)
|
||||
psi[1, 0] = 1 / sqrt(2)
|
||||
psi[2, 0] = -1 / sqrt(2)
|
||||
rho_singlet = simplify(psi * psi.H)
|
||||
|
||||
rho_p = simplify(p * rho_singlet + (1 - p) * eye(4) / 4)
|
||||
print("rho(p) =")
|
||||
sp.pprint(rho_p)
|
||||
|
||||
|
||||
def entry(rho, ops):
|
||||
M = None
|
||||
for op in ops:
|
||||
M = op if M is None else M * op
|
||||
return simplify(re(simplify((rho * M).trace())))
|
||||
|
||||
|
||||
plist = [X, Y, Z]
|
||||
T = Matrix(3, 3, lambda i, j: entry(rho_p, [op_A(plist[i]), op_B(plist[j])]))
|
||||
print("\nCorrelation matrix T(p) =")
|
||||
sp.pprint(T)
|
||||
|
||||
G = simplify(T.T * T)
|
||||
eigs = G.eigenvals()
|
||||
singular_values = []
|
||||
for ev, mult in eigs.items():
|
||||
singular_values += [simplify(sqrt(ev))] * mult
|
||||
nuclear_norm = simplify(sum(singular_values))
|
||||
print("\nNuclear norm ||M_A(rho(p))||_* =", nuclear_norm)
|
||||
print("Shadow-map threshold (||.||_* = 1):", sp.solve(sp.Eq(nuclear_norm, 1), p))
|
||||
|
||||
|
||||
def partial_transpose_B(M):
|
||||
Mpt = zeros(4, 4)
|
||||
for a in range(2):
|
||||
for b in range(2):
|
||||
for c in range(2):
|
||||
for dd in range(2):
|
||||
i, j = a * 2 + b, c * 2 + dd
|
||||
i2, j2 = a * 2 + dd, c * 2 + b
|
||||
Mpt[i2, j2] = M[i, j]
|
||||
return Mpt
|
||||
|
||||
|
||||
rho_pt = partial_transpose_B(rho_p)
|
||||
print("\nEigenvalues of the partial transpose rho(p)^{T_B}:")
|
||||
for e_ in rho_pt.eigenvals().keys():
|
||||
print(" ", simplify(e_), " = 0 at p =", sp.solve(sp.Eq(e_, 0), p))
|
||||
103
scripts/dps_hierarchy/02_werner_qutrit_symbolic.py
Normal file
103
scripts/dps_hierarchy/02_werner_qutrit_symbolic.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""
|
||||
02_werner_qutrit_symbolic.py
|
||||
|
||||
Same check as 01_werner_qubit_symbolic.py, generalized to d=3 (qutrits),
|
||||
using the sqrt(3/2)-scaled Gell-Mann convention fixed in the paper's own
|
||||
Tiles example (tr(sigma_i sigma_j) = d delta_ij = 3 delta_ij).
|
||||
|
||||
State family: rho(p) = p * P_anti/dim(P_anti) + (1-p) * I/9
|
||||
(the natural qutrit "Werner state" built from the antisymmetric subspace
|
||||
of C^3 x C^3, dimension 3), invariant under U(x)U for all U in U(3).
|
||||
|
||||
Result (the interesting part): the order-1 shadow-map/correlation-matrix
|
||||
criterion gives p_c = 1/2, but the TRUE separability threshold (Werner
|
||||
1989, p_sep = 1/(d+1)) is p_c = 1/4. Unlike the qubit case, the criterion
|
||||
is here only a valid but NOT tight sufficient condition -- symmetry forces
|
||||
"concentration" of the signal (single isotype => correlation matrix
|
||||
proportional to identity) but not "sharpening" of the threshold itself.
|
||||
|
||||
Requires: sympy. Runtime: under a minute.
|
||||
"""
|
||||
import sympy as sp
|
||||
from sympy import sqrt, I, simplify, Matrix, eye, zeros, re, symbols, Rational
|
||||
|
||||
d = 3
|
||||
|
||||
lam = [None] * 8
|
||||
lam[0] = Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 0]])
|
||||
lam[1] = Matrix([[0, -I, 0], [I, 0, 0], [0, 0, 0]])
|
||||
lam[2] = Matrix([[1, 0, 0], [0, -1, 0], [0, 0, 0]])
|
||||
lam[3] = Matrix([[0, 0, 1], [0, 0, 0], [1, 0, 0]])
|
||||
lam[4] = Matrix([[0, 0, -I], [0, 0, 0], [I, 0, 0]])
|
||||
lam[5] = Matrix([[0, 0, 0], [0, 0, 1], [0, 1, 0]])
|
||||
lam[6] = Matrix([[0, 0, 0], [0, 0, -I], [0, I, 0]])
|
||||
lam[7] = (1 / sqrt(3)) * Matrix([[1, 0, 0], [0, 1, 0], [0, 0, -2]])
|
||||
|
||||
c = sqrt(Rational(3, 2))
|
||||
sigma = [simplify(c * L) for L in lam]
|
||||
for i in range(8):
|
||||
for j in range(8):
|
||||
val = simplify((sigma[i] * sigma[j]).trace())
|
||||
assert val == (3 if i == j else 0), (i, j, val)
|
||||
|
||||
I3 = eye(3)
|
||||
|
||||
|
||||
def op_A(P):
|
||||
return sp.Matrix(sp.kronecker_product(P, I3))
|
||||
|
||||
|
||||
def op_B(P):
|
||||
return sp.Matrix(sp.kronecker_product(I3, P))
|
||||
|
||||
|
||||
V = zeros(9, 9)
|
||||
for a in range(3):
|
||||
for b in range(3):
|
||||
V[b * 3 + a, a * 3 + b] = 1
|
||||
|
||||
I9 = eye(9)
|
||||
P_anti = simplify((I9 - V) / 2)
|
||||
dim_anti = simplify(P_anti.trace()) # = 3
|
||||
|
||||
p = symbols('p', real=True)
|
||||
rho_p = simplify(p * P_anti / dim_anti + (1 - p) * I9 / 9)
|
||||
|
||||
|
||||
def entry(rho, ops):
|
||||
M = None
|
||||
for op in ops:
|
||||
M = op if M is None else M * op
|
||||
return simplify(re(simplify((rho * M).trace())))
|
||||
|
||||
|
||||
T = Matrix(8, 8, lambda i, j: entry(rho_p, [op_A(sigma[i]), op_B(sigma[j])]))
|
||||
print("Correlation matrix T(p) (should be proportional to I_8):")
|
||||
sp.pprint(T)
|
||||
|
||||
norm_const = 1 / sqrt(4) # (d_a-1)(d_bar_a-1) = 2*2 = 4
|
||||
Mn = simplify(norm_const * T)
|
||||
nuclear_norm = simplify(8 * sp.Abs(Mn[0, 0]))
|
||||
print("\nShadow-map nuclear norm:", nuclear_norm)
|
||||
print("Shadow-map threshold:", sp.solve(sp.Eq(nuclear_norm, 1), p))
|
||||
|
||||
|
||||
def partial_transpose_B_d(M, dim):
|
||||
Mpt = zeros(dim * dim, dim * dim)
|
||||
for a in range(dim):
|
||||
for b in range(dim):
|
||||
for cc in range(dim):
|
||||
for dd in range(dim):
|
||||
i, j = a * dim + b, cc * dim + dd
|
||||
i2, j2 = a * dim + dd, cc * dim + b
|
||||
Mpt[i2, j2] = M[i, j]
|
||||
return Mpt
|
||||
|
||||
|
||||
rho_pt = partial_transpose_B_d(rho_p, 3)
|
||||
print("\nPartial-transpose eigenvalues (PPT / true-separability threshold):")
|
||||
for ev in rho_pt.eigenvals().keys():
|
||||
print(" ", simplify(ev), " = 0 at p =", sp.solve(sp.Eq(ev, 0), p))
|
||||
|
||||
print("\nExpected: shadow-map threshold p=1/2 (NOT tight);"
|
||||
" true threshold (Werner 1989, p_sep=1/(d+1)) p=1/4.")
|
||||
54
scripts/dps_hierarchy/03_dps_level2_demo.py
Normal file
54
scripts/dps_hierarchy/03_dps_level2_demo.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""
|
||||
03_dps_level2_demo.py
|
||||
|
||||
Demonstrates the level-2 DPS SDP (via dps_hierarchy.build_dps_problem) on
|
||||
two test states:
|
||||
|
||||
A) the qutrit Werner state -- sanity check. PPT is already exactly
|
||||
tight for this family (p_c=1/4, see 02_werner_qutrit_symbolic.py), so
|
||||
DPS-2 cannot improve on it; well away from the boundary both should
|
||||
agree.
|
||||
B) the Tiles UPB bound-entangled state -- the interesting case. Plain
|
||||
PPT is blind (min eigenvalue ~0, "PPT to machine precision" as noted
|
||||
in the paper's own Tiles example), but DPS level 2 correctly detects
|
||||
the entanglement.
|
||||
|
||||
Expected runtime: well under a minute with SCS.
|
||||
"""
|
||||
import cvxpy as cp
|
||||
from common import RHO_TILES, werner_qutrit, plain_ppt_feasible, plain_ppt_min_eig
|
||||
from dps_hierarchy import build_dps_problem, dps_feasible
|
||||
|
||||
SOLVER = cp.SCS # swap to cp.MOSEK if you have a license -- likely much faster
|
||||
|
||||
prob, rho_param, sigma = build_dps_problem(d=3, k=2)
|
||||
print(f"DPS level-2 SDP built: sigma shape {sigma.shape}\n")
|
||||
|
||||
print("=" * 70)
|
||||
print("Sanity check: qutrit Werner state (known exact threshold p=1/4)")
|
||||
print("=" * 70)
|
||||
for p in [0.20, 0.40]:
|
||||
rho = werner_qutrit(p)
|
||||
ppt_ok = plain_ppt_feasible(rho)
|
||||
eig = plain_ppt_min_eig(rho)
|
||||
ok = dps_feasible(prob, rho_param, rho, solver=SOLVER, eps=1e-6)
|
||||
print(f"p={p:.2f}: PPT feasible={ppt_ok} (min eig {eig:+.5f}) "
|
||||
f"DPS-2 feasible={ok}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Tiles UPB bound-entangled state (the interesting case)")
|
||||
print("=" * 70)
|
||||
ppt_ok = plain_ppt_feasible(RHO_TILES)
|
||||
eig = plain_ppt_min_eig(RHO_TILES)
|
||||
print(f"Plain PPT feasible: {ppt_ok} (min eigenvalue: {eig:.10f})")
|
||||
|
||||
ok = dps_feasible(prob, rho_param, RHO_TILES, solver=SOLVER, eps=1e-6)
|
||||
print(f"DPS level-2 feasible: {ok} "
|
||||
f"({'NOT detected' if ok else 'ENTANGLEMENT DETECTED'})")
|
||||
|
||||
print("\nRobustness across solver tolerances (guards against SDP numerical "
|
||||
"artifacts near a threshold -- see the chat for a case where this "
|
||||
"mattered):")
|
||||
for eps in [1e-5, 1e-6, 1e-7, 1e-8]:
|
||||
dps_feasible(prob, rho_param, RHO_TILES, solver=SOLVER, eps=eps, max_iters=50000)
|
||||
print(f" eps={eps}: status={prob.status}")
|
||||
50
scripts/dps_hierarchy/04_tiles_noise_scan.py
Normal file
50
scripts/dps_hierarchy/04_tiles_noise_scan.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""
|
||||
04_tiles_noise_scan.py
|
||||
|
||||
Noise-robustness comparison: find the critical white-noise fraction p_c at
|
||||
which each criterion stops detecting entanglement of
|
||||
|
||||
rho(p) = p * rho_Tiles + (1-p) * I/9
|
||||
|
||||
Reproduces (see the chat for the full discussion/derivation):
|
||||
- plain shadow-map / de Vicente Bloch-representation criterion:
|
||||
p_c ~ 0.9493 (tolerance ~5.07%)
|
||||
- DPS level 2:
|
||||
p_c ~ 0.951 (tolerance ~4.9%)
|
||||
|
||||
i.e. DPS level 2 barely improves on the much cheaper order-1 correlation
|
||||
criterion for THIS state -- see 05_local_filtering.py for the much bigger
|
||||
lever (local filtering).
|
||||
|
||||
Expected runtime: seconds for the shadow-map part; a few minutes for the
|
||||
DPS-2 bisection (12-16 SDP solves).
|
||||
"""
|
||||
import cvxpy as cp
|
||||
from common import noisy_tiles, shadow_map_nuclear_norm
|
||||
from dps_hierarchy import build_dps_problem, dps_feasible
|
||||
|
||||
SOLVER = cp.SCS
|
||||
|
||||
print("Shadow-map criterion threshold:")
|
||||
lo, hi = 0.0, 1.0
|
||||
for _ in range(40):
|
||||
mid = (lo + hi) / 2
|
||||
if shadow_map_nuclear_norm(noisy_tiles(mid)) > 1:
|
||||
hi = mid
|
||||
else:
|
||||
lo = mid
|
||||
print(f" p_c = {hi:.5f} (tolerance {100 * (1 - hi):.2f}%)")
|
||||
|
||||
print("\nDPS level-2 threshold:")
|
||||
prob, rho_param, sigma = build_dps_problem(d=3, k=2)
|
||||
lo, hi = 0.0, 1.0
|
||||
for i in range(16):
|
||||
mid = (lo + hi) / 2
|
||||
ok = dps_feasible(prob, rho_param, noisy_tiles(mid), solver=SOLVER,
|
||||
eps=1e-7, warm_start=True)
|
||||
if ok:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
print(f" [{i + 1}] p={mid:.5f} feasible={ok}")
|
||||
print(f" p_c = {hi:.5f} (tolerance {100 * (1 - hi):.2f}%)")
|
||||
42
scripts/dps_hierarchy/05_local_filtering.py
Normal file
42
scripts/dps_hierarchy/05_local_filtering.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""
|
||||
05_local_filtering.py
|
||||
|
||||
Applies the operator-Sinkhorn local-filtering (SLOCC normal-form)
|
||||
algorithm to the Tiles state family, then re-evaluates the plain
|
||||
shadow-map criterion on the FILTERED state.
|
||||
|
||||
Reproduces the literature's "Filter Covariance Matrix Criterion"
|
||||
(Gittsovich, Guehne, Hyllus, Eisert, "Unifying several separability
|
||||
conditions using the covariance matrix criterion", arXiv:0803.0757,
|
||||
Proposition IV.13) threshold almost exactly:
|
||||
|
||||
filtered shadow-map (this script): p_c ~ 0.8722 (tolerance 12.78%)
|
||||
literature Filter-CMC (Prop IV.13): p_c = 0.8723 (tolerance 12.77%)
|
||||
|
||||
i.e. local filtering + the paper's OWN, already-existing order-1
|
||||
criterion reproduces a specialized literature result almost to 4 decimal
|
||||
places, with no new criterion needed -- just the right pre-processing.
|
||||
|
||||
Expected runtime: a few seconds (filtering is cheap linear algebra,
|
||||
no SDP involved here; ~20-30 Sinkhorn iterations per state).
|
||||
"""
|
||||
from common import noisy_tiles, RHO_TILES, operator_sinkhorn, shadow_map_nuclear_norm
|
||||
|
||||
print("Filtering the pure Tiles state (p=1):")
|
||||
rho_f = operator_sinkhorn(RHO_TILES, verbose=True)
|
||||
print(" shadow-map nuclear norm BEFORE filtering:", shadow_map_nuclear_norm(RHO_TILES))
|
||||
print(" shadow-map nuclear norm AFTER filtering:", shadow_map_nuclear_norm(rho_f))
|
||||
|
||||
print("\nBisection for the filtered-shadow-map threshold:")
|
||||
lo, hi = 0.80, 0.95
|
||||
for _ in range(20):
|
||||
mid = (lo + hi) / 2
|
||||
rho_pf = operator_sinkhorn(noisy_tiles(mid))
|
||||
nn = shadow_map_nuclear_norm(rho_pf)
|
||||
if nn > 1:
|
||||
hi = mid
|
||||
else:
|
||||
lo = mid
|
||||
print(f" p_c = {hi:.5f} (tolerance {100 * (1 - hi):.2f}%)")
|
||||
print(" Literature (Filter-CMC, Prop. IV.13, arXiv:0803.0757): "
|
||||
"p_c = 0.87230 (tolerance 12.77%)")
|
||||
42
scripts/dps_hierarchy/06_dps_level2_filtered.py
Normal file
42
scripts/dps_hierarchy/06_dps_level2_filtered.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""
|
||||
06_dps_level2_filtered.py
|
||||
|
||||
Applies DPS level 2 to the FILTERED Tiles state family (filtering + higher
|
||||
extension order, combined). The interesting (somewhat counter-intuitive)
|
||||
result: filtering helps DPS-2 only marginally --
|
||||
|
||||
p_c ~ 0.9426 (tolerance 5.74%)
|
||||
|
||||
-- much less than it helps the plain shadow-map criterion alone
|
||||
(p_c ~ 0.8722, tolerance 12.78%, see 05_local_filtering.py). I.e. for this
|
||||
state, "which local basis you filter into" matters far more than "how
|
||||
many extension copies you add" -- filtering and DPS-extension-order are
|
||||
not equally powerful levers here, and they don't simply stack.
|
||||
|
||||
Expected runtime: a few minutes (DPS-2 bisection with re-filtering the
|
||||
state at each bisection point).
|
||||
"""
|
||||
import cvxpy as cp
|
||||
from common import noisy_tiles, operator_sinkhorn
|
||||
from dps_hierarchy import build_dps_problem, dps_feasible
|
||||
|
||||
SOLVER = cp.SCS
|
||||
prob, rho_param, sigma = build_dps_problem(d=3, k=2)
|
||||
|
||||
print("DPS level 2 on the filtered pure Tiles state (p=1):")
|
||||
rho_f = operator_sinkhorn(noisy_tiles(1.0))
|
||||
ok = dps_feasible(prob, rho_param, rho_f, solver=SOLVER, eps=1e-7)
|
||||
print(f" feasible={ok}")
|
||||
|
||||
print("\nBisection for the filtered-DPS-2 threshold:")
|
||||
lo, hi = 0.5, 0.95
|
||||
for i in range(14):
|
||||
mid = (lo + hi) / 2
|
||||
rho_pf = operator_sinkhorn(noisy_tiles(mid))
|
||||
ok = dps_feasible(prob, rho_param, rho_pf, solver=SOLVER, eps=1e-7, warm_start=True)
|
||||
if ok:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
print(f" [{i + 1}] p={mid:.5f} feasible={ok}")
|
||||
print(f" p_c = {hi:.5f} (tolerance {100 * (1 - hi):.2f}%)")
|
||||
30
scripts/dps_hierarchy/07_dps_level3_single.py
Normal file
30
scripts/dps_hierarchy/07_dps_level3_single.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""
|
||||
07_dps_level3_single.py
|
||||
|
||||
A single DPS level-3 feasibility check on the pure Tiles state, to confirm
|
||||
the construction works and see its cost before committing to a full
|
||||
noise-threshold bisection (see 08_dps_level_k_bisection.py).
|
||||
|
||||
In the original sandbox this took ~137s with SCS (single solve, cold
|
||||
start). Expect similar or better on a modern laptop; likely far faster
|
||||
with an interior-point solver (MOSEK, if you have a license) since the
|
||||
PSD cone here (81x81 complex Hermitian) is small by modern SDP standards
|
||||
-- SCS is a first-order method tuned for large sparse problems and is not
|
||||
especially fast on small/dense feasibility problems like this one.
|
||||
|
||||
Expected result: status "infeasible" (i.e. entanglement IS detected).
|
||||
"""
|
||||
import time
|
||||
import cvxpy as cp
|
||||
from common import RHO_TILES
|
||||
from dps_hierarchy import build_dps_problem, dps_feasible
|
||||
|
||||
SOLVER = cp.SCS # try cp.MOSEK if available -- likely much faster at this size
|
||||
|
||||
prob, rho_param, sigma = build_dps_problem(d=3, k=3)
|
||||
print(f"sigma shape: {sigma.shape} "
|
||||
f"PPT-constraint (PSD cone) size: {3 * 3 ** 3} x {3 * 3 ** 3}")
|
||||
|
||||
t0 = time.time()
|
||||
ok = dps_feasible(prob, rho_param, RHO_TILES, solver=SOLVER, eps=1e-6, max_iters=20000)
|
||||
print(f"DPS level-3 feasible: {ok} status={prob.status} [{time.time() - t0:.1f}s]")
|
||||
84
scripts/dps_hierarchy/08_dps_level_k_bisection.py
Normal file
84
scripts/dps_hierarchy/08_dps_level_k_bisection.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""
|
||||
08_dps_level_k_bisection.py
|
||||
|
||||
General, RESUMABLE bisection for the DPS level-k noise-robustness
|
||||
threshold of the Tiles state family. Each run performs STEPS_PER_RUN
|
||||
bisection steps and saves progress to a JSON state file, so you can call
|
||||
it repeatedly (e.g. in a shell loop, or across separate sessions) without
|
||||
losing progress -- useful since each SDP solve can take anywhere from
|
||||
under a second (k=2) to a few minutes (k=3, with SCS) depending on k,
|
||||
your hardware, and the solver.
|
||||
|
||||
Usage:
|
||||
python3 08_dps_level_k_bisection.py
|
||||
|
||||
Configure LEVEL, SOLVER, STEPS_PER_RUN, and EPS below.
|
||||
The state file is named dps_level{LEVEL}_bisection_state.json.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
Progress already made in the original chat session for LEVEL=3 (8 SCS
|
||||
solves, ~135-227s each) is included alongside this script as
|
||||
dps_level3_bisection_state.json:
|
||||
|
||||
bracket so far: [0.90982, 0.91080] (i.e. p_c ~ 0.910-0.911)
|
||||
|
||||
Just run this script (with LEVEL=3, the default) to continue narrowing
|
||||
it -- it will pick up automatically from that saved state. Delete the
|
||||
state file to start over, or change LEVEL to try a different extension
|
||||
order (4, 5, ... but see README.md for how fast the PPT-constraint size,
|
||||
and hence the cost, grows: 3*3^k).
|
||||
--------------------------------------------------------------------
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import cvxpy as cp
|
||||
from common import noisy_tiles
|
||||
from dps_hierarchy import build_dps_problem, dps_feasible
|
||||
|
||||
LEVEL = 3
|
||||
SOLVER = cp.SCS # swap to cp.MOSEK if available -- likely much faster
|
||||
STEPS_PER_RUN = 1 # raise this if your machine/solver is fast enough
|
||||
EPS = 1e-5 # solver tolerance; tighten once you have a rough bracket
|
||||
DEFAULT_BRACKET = (0.70, 0.951) # 0.951 is a proven-safe upper bound (= DPS level-2 threshold,
|
||||
# since DPS level 3 can only detect at <= that noise level)
|
||||
|
||||
STATE_FILE = f"dps_level{LEVEL}_bisection_state.json"
|
||||
|
||||
if os.path.exists(STATE_FILE):
|
||||
with open(STATE_FILE) as f:
|
||||
state = json.load(f)
|
||||
print(f"Resuming from saved state: bracket [{state['lo']:.5f}, {state['hi']:.5f}], "
|
||||
f"{state['iter']} iterations so far.")
|
||||
else:
|
||||
state = {"lo": DEFAULT_BRACKET[0], "hi": DEFAULT_BRACKET[1], "iter": 0, "log": []}
|
||||
print(f"Starting fresh: bracket {DEFAULT_BRACKET}")
|
||||
|
||||
prob, rho_param, sigma = build_dps_problem(d=3, k=LEVEL)
|
||||
print(f"sigma shape: {sigma.shape}, PPT-constraint (PSD cone) size: "
|
||||
f"{3 * 3 ** LEVEL} x {3 * 3 ** LEVEL}\n")
|
||||
|
||||
for _ in range(STEPS_PER_RUN):
|
||||
lo, hi = state["lo"], state["hi"]
|
||||
mid = (lo + hi) / 2
|
||||
t0 = time.time()
|
||||
feasible = dps_feasible(prob, rho_param, noisy_tiles(mid), solver=SOLVER,
|
||||
eps=EPS, max_iters=20000, warm_start=True)
|
||||
dt = time.time() - t0
|
||||
if feasible:
|
||||
state["lo"] = mid
|
||||
else:
|
||||
state["hi"] = mid
|
||||
state["iter"] += 1
|
||||
state["log"].append({"iter": state["iter"], "p": mid, "feasible": feasible,
|
||||
"time_s": round(dt, 1), "status": prob.status})
|
||||
print(f"[iter {state['iter']}] p={mid:.5f} feasible={feasible} "
|
||||
f"status={prob.status} ({dt:.1f}s) "
|
||||
f"bracket now [{state['lo']:.5f}, {state['hi']:.5f}]")
|
||||
|
||||
with open(STATE_FILE, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
print(f"\nCurrent bracket: [{state['lo']:.5f}, {state['hi']:.5f}] "
|
||||
f"(width {state['hi'] - state['lo']:.5f})")
|
||||
print("Run again to continue narrowing it further (progress is saved).")
|
||||
86
scripts/dps_hierarchy/09_dps_level3_filtered_bisection.py
Normal file
86
scripts/dps_hierarchy/09_dps_level3_filtered_bisection.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""
|
||||
09_dps_level3_filtered_bisection.py
|
||||
|
||||
Combines local filtering (operator-Sinkhorn, as in 05/06) with DPS level 3
|
||||
(as in 07/08): at each candidate noise level p, first bring rho(p) to its
|
||||
local-filtering normal form, then run the DPS level-3 feasibility SDP on
|
||||
the FILTERED state.
|
||||
|
||||
Precedent from level 2 (06_dps_level2_filtered.py): filtering helped DPS-2
|
||||
only modestly (4.90% -> 5.74% tolerance), far less than it helped the
|
||||
plain order-1 shadow-map criterion alone (-> 12.78%, see 05). Expect a
|
||||
similarly modest improvement here, NOT a jump to ~13% territory.
|
||||
|
||||
Cost note: this is the most expensive script in the collection. Each
|
||||
solve costs about as much as plain DPS-3 (07/08) -- filtering itself is
|
||||
cheap, the SDP solve dominates -- so a full bisection needs roughly the
|
||||
same total time as 08's bisection, i.e. another dozen-ish solves at
|
||||
~85-140s each on hardware like yours.
|
||||
|
||||
On the starting bracket: for the level-2 case, filtering turned out to
|
||||
help (0.9426 < 0.9510), but this is NOT something proven in general here
|
||||
-- local filtering does not obviously commute with the k-extension
|
||||
structure the way it does with plain separability (which is SLOCC-
|
||||
invariant by definition). So, unlike 08's upper bound (0.951, rigorously
|
||||
justified by DPS monotonicity in k alone), the bracket below is only an
|
||||
empirically-motivated starting guess, not a proven bound. If a bisection
|
||||
step ever reports "feasible" surprisingly close to hi, that's a sign the
|
||||
true threshold may be above the assumed bracket -- widen it and restart
|
||||
if so.
|
||||
|
||||
Usage:
|
||||
python3 09_dps_level3_filtered_bisection.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import cvxpy as cp
|
||||
from common import noisy_tiles, operator_sinkhorn
|
||||
from dps_hierarchy import build_dps_problem, dps_feasible
|
||||
|
||||
LEVEL = 3
|
||||
SOLVER = cp.SCS # swap to cp.MOSEK if available -- likely much faster
|
||||
STEPS_PER_RUN = 1
|
||||
EPS = 1e-6
|
||||
DEFAULT_BRACKET = (0.8, 0.95) # empirically-motivated, NOT rigorously proven (see above)
|
||||
|
||||
STATE_FILE = f"dps_level{LEVEL}_filtered_bisection_state.json"
|
||||
|
||||
if os.path.exists(STATE_FILE):
|
||||
with open(STATE_FILE) as f:
|
||||
state = json.load(f)
|
||||
print(f"Resuming from saved state: bracket [{state['lo']:.5f}, {state['hi']:.5f}], "
|
||||
f"{state['iter']} iterations so far.")
|
||||
else:
|
||||
state = {"lo": DEFAULT_BRACKET[0], "hi": DEFAULT_BRACKET[1], "iter": 0, "log": []}
|
||||
print(f"Starting fresh: bracket {DEFAULT_BRACKET}")
|
||||
|
||||
prob, rho_param, sigma = build_dps_problem(d=3, k=LEVEL)
|
||||
print(f"sigma shape: {sigma.shape}, PPT-constraint (PSD cone) size: "
|
||||
f"{3 * 3 ** LEVEL} x {3 * 3 ** LEVEL}\n")
|
||||
|
||||
for _ in range(STEPS_PER_RUN):
|
||||
lo, hi = state["lo"], state["hi"]
|
||||
mid = (lo + hi) / 2
|
||||
t0 = time.time()
|
||||
rho_filtered = operator_sinkhorn(noisy_tiles(mid))
|
||||
feasible = dps_feasible(prob, rho_param, rho_filtered, solver=SOLVER,
|
||||
eps=EPS, max_iters=20000, warm_start=True)
|
||||
dt = time.time() - t0
|
||||
if feasible:
|
||||
state["lo"] = mid
|
||||
else:
|
||||
state["hi"] = mid
|
||||
state["iter"] += 1
|
||||
state["log"].append({"iter": state["iter"], "p": mid, "feasible": feasible,
|
||||
"time_s": round(dt, 1), "status": prob.status})
|
||||
print(f"[iter {state['iter']}] p={mid:.5f} feasible={feasible} "
|
||||
f"status={prob.status} ({dt:.1f}s) "
|
||||
f"bracket now [{state['lo']:.5f}, {state['hi']:.5f}]")
|
||||
|
||||
with open(STATE_FILE, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
print(f"\nCurrent bracket: [{state['lo']:.5f}, {state['hi']:.5f}] "
|
||||
f"(width {state['hi'] - state['lo']:.5f})")
|
||||
print("Run again to continue narrowing it further (progress is saved).")
|
||||
96
scripts/dps_hierarchy/README.md
Normal file
96
scripts/dps_hierarchy/README.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Entanglement-detection scripts from this chat
|
||||
|
||||
This is the code from a conversation that started with the "symmetric shadow
|
||||
maps" paper (`symmetric_shadow_maps_formal.tex`) and worked outward through a
|
||||
chain of entanglement-detection techniques on the two-qutrit **Tiles**
|
||||
bound-entangled state (Bennett-DiVincenzo-Mor-Shor-Smolin-Terhal UPB state,
|
||||
already used as a benchmark in the paper): symmetric-state sanity checks,
|
||||
DPS symmetric-extension SDPs, noise-robustness thresholds, local filtering,
|
||||
and (in progress) a third DPS extension level.
|
||||
|
||||
**Note: none of this has been re-run/verified after being assembled into
|
||||
this package** (per your request) -- it's a straight extraction of the
|
||||
code from the chat. The numbers quoted in each docstring/comment are what
|
||||
the sandbox actually produced during the conversation; treat them as
|
||||
"expected results to check against" rather than guaranteed.
|
||||
|
||||
## Setup
|
||||
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
`scs` is the default (open-source, first-order) SDP solver used throughout.
|
||||
If you have a MOSEK license (free for academics), it is very likely much
|
||||
faster for these problem sizes -- just change `SOLVER = cp.SCS` to
|
||||
`SOLVER = cp.MOSEK` near the top of scripts 03, 04, 06, 07, 08.
|
||||
|
||||
## Files, in the order they came up in the conversation
|
||||
|
||||
| File | What it does | Expected result | Rough runtime |
|
||||
|---|---|---|---|
|
||||
| `common.py` | Shared utilities: Gell-Mann generators (paper convention), Tiles state, qutrit Werner state, correlation-matrix/shadow-map criterion, plain PPT check, operator-Sinkhorn filter. Imported by scripts 03-06. | -- | -- |
|
||||
| `dps_hierarchy.py` | General, level-`k`-parametrized DPS symmetric-extension SDP builder (used for levels 2 and 3, and usable for higher `k`). | -- | -- |
|
||||
| `01_werner_qubit_symbolic.py` | Exact (sympy) check: 2-qubit Werner state, `SU(2)` symmetry forces the correlation matrix `∝ I`. | Shadow-map threshold *exactly* matches PPT: **p_c = 1/3** both ways. | seconds |
|
||||
| `02_werner_qutrit_symbolic.py` | Same, generalized to qutrits (antisymmetric-subspace Werner state). | Shadow-map threshold **p_c = 1/2**, but true threshold (Werner 1989) is **p_c = 1/4** -- the order-1 criterion is valid but NOT tight in d=3 (unlike d=2). | under a minute |
|
||||
| `03_dps_level2_demo.py` | DPS level 2 via `dps_hierarchy`. Sanity check on Werner qutrit; then the interesting case: Tiles state, where plain PPT is exactly blind (min eigenvalue ≈ 0) but DPS-2 detects it. | Werner: consistent with p=1/4 away from the boundary. Tiles: PPT feasible=True, DPS-2 feasible=False (detected). Robust across solver tolerances 1e-5..1e-8. | under a minute |
|
||||
| `04_tiles_noise_scan.py` | Noise-threshold bisection for (a) the plain shadow-map criterion and (b) DPS level 2, on the noisy Tiles family. | Shadow-map **p_c ≈ 0.9493** (5.07% tolerance); DPS-2 **p_c ≈ 0.951** (4.9%) -- i.e. DPS-2 barely improves on the much cheaper order-1 criterion for this state. | a few minutes (DPS-2 bisection) |
|
||||
| `05_local_filtering.py` | Operator-Sinkhorn local filtering (SLOCC normal form) + the plain shadow-map criterion on the filtered state. | **p_c ≈ 0.8722** (12.78% tolerance) -- matches the literature's "Filter Covariance Matrix Criterion" (Gittsovich, Gühne, Hyllus, Eisert, arXiv:0803.0757, Prop. IV.13: p_c = 0.8723, 12.77%) to ~4 decimal places. | seconds |
|
||||
| `06_dps_level2_filtered.py` | DPS level 2 applied to the *filtered* state (combining both levers). | **p_c ≈ 0.9426** (5.74%) -- filtering helps DPS-2 only marginally, much less than it helps the plain shadow-map (05). Filtering and DPS-extension-order are not equally powerful levers here, and don't simply stack. | a few minutes |
|
||||
| `07_dps_level3_single.py` | A single DPS level-3 feasibility check on the pure Tiles state, to confirm level 3 is tractable at all. | status = infeasible (detected). Took **~137s** with SCS in the original sandbox. | ~1-3 minutes |
|
||||
| `08_dps_level_k_bisection.py` | General, **resumable** bisection for the DPS level-`k` noise threshold, one step per invocation, progress saved to JSON. Defaults to `LEVEL=3`. | See below -- **in progress**. | ~2-4 min per step with SCS (k=3) |
|
||||
| `dps_level3_bisection_state.json` | Saved progress for the level-3 bisection from the original session (8 SCS solves already spent). | Current bracket: **[0.90982, 0.91080]**, i.e. `p_c ≈ 0.910-0.911`. | -- |
|
||||
| `09_dps_level3_filtered_bisection.py` | Combines local filtering (05) with DPS level 3 (07/08): filter the state, then run the level-3 SDP on it. Resumable, same pattern as 08. | Untested/in progress -- based on the level-2 precedent (06), expect only a modest improvement over plain level 3, not a jump to ~13%. Starting bracket is an educated guess, not a proven bound (see the script's docstring). | most expensive script here: ~85-140s per solve, ~12-16 solves for a full bisection |
|
||||
|
||||
## Where the level-3 bisection currently stands
|
||||
|
||||
```json
|
||||
{"lo": 0.90982, "hi": 0.91080, "iter": 8}
|
||||
```
|
||||
|
||||
So DPS level 3 detects entanglement for `p ≳ 0.910`, i.e. roughly
|
||||
**9.0% noise tolerance** -- already better than level 2's 4.9-5.7%, but
|
||||
still well short of the 12.77-12.78% that local filtering alone achieves.
|
||||
Just re-run `08_dps_level_k_bisection.py` (it picks up the saved state
|
||||
automatically) to narrow this further.
|
||||
|
||||
## The overall picture that emerged (for reference)
|
||||
|
||||
| Method | p_c | Noise tolerance |
|
||||
|---|---|---|
|
||||
| plain PPT | ~1.0 | ~0% (knife-edge) |
|
||||
| shadow-map / de Vicente Bloch criterion (order 1) | 0.9493 | 5.07% |
|
||||
| DPS level 2 | 0.9510 | 4.90% |
|
||||
| DPS level 2 + filtering | 0.9426 | 5.74% |
|
||||
| DPS level 3 (partial result so far) | ~0.910 | ~9.0% (narrowing) |
|
||||
| **local filtering + shadow-map (order 1)** | **0.8722** | **12.78%** |
|
||||
| literature: Filter-CMC (Prop. IV.13) | 0.8723 | 12.77% |
|
||||
| literature: best known positive map | 0.8744 | 12.56% |
|
||||
|
||||
Headline takeaway: for this particular state, **local filtering (a SLOCC
|
||||
pre-processing step) is a far bigger lever than increasing the DPS
|
||||
extension order**, and the two don't stack additively -- filtering the
|
||||
state and then applying the cheapest possible (order-1) criterion already
|
||||
matches a specialized literature result almost exactly, while adding DPS
|
||||
levels on top gives comparatively little.
|
||||
|
||||
## A performance note on why level 3+ gets slow
|
||||
|
||||
The DPS SDP *variable* is parametrized on `A ⊗ Sym^k(B)`, dimension
|
||||
`d · C(d+k-1, k)` -- polynomial in `k` (this is the "exploit the built-in
|
||||
Bose symmetry of the extension copies" trick). But the **PPT constraint**
|
||||
itself has to be checked on the full, unsymmetrized embedding
|
||||
`A ⊗ B_1 ⊗ ... ⊗ B_k`, dimension `d^(k+1)` -- exponential in `k`. Since
|
||||
SDP solver cost is governed by the size of the PSD cone (the PPT
|
||||
constraint), not by the number of free variables, this is why level 3
|
||||
(81×81 cone) is already much slower than level 2 (27×27 cone), and level 4
|
||||
(243×243) would be slower still. A proper fix would exploit
|
||||
representation-theoretic structure of the PPT constraint itself, not just
|
||||
of the extension -- that's a bigger undertaking than what's implemented
|
||||
here.
|
||||
|
||||
If you have MOSEK (or another interior-point solver): try it first for
|
||||
levels 3-4. Interior-point methods are usually much faster than SCS on
|
||||
small/medium, dense SDPs like these -- SCS is tuned for large sparse
|
||||
problems and is likely the main reason level 3 took ~137s-227s per solve
|
||||
here rather than a fraction of a second.
|
||||
167
scripts/dps_hierarchy/common.py
Normal file
167
scripts/dps_hierarchy/common.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"""
|
||||
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
|
||||
110
scripts/dps_hierarchy/dps_hierarchy.py
Normal file
110
scripts/dps_hierarchy/dps_hierarchy.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
"""
|
||||
dps_hierarchy.py
|
||||
|
||||
General DPS (Doherty-Parrilo-Spedalieri) level-k symmetric-extension SDP,
|
||||
for a bipartite qudit state rho_AB with local dimension d, extending party
|
||||
B to k Bose-symmetric copies.
|
||||
|
||||
Key design point (discussed at length in the chat this was extracted
|
||||
from): the SDP *variable* sigma is parametrized directly on
|
||||
A x Sym^k(C^d), dimension d * C(d+k-1,k) -- POLYNOMIAL in k. But the PPT
|
||||
constraint (sigma^{T_A} >= 0) must be checked on the full, unsymmetrized
|
||||
embedding A x B_1 x ... x B_k, dimension d^{k+1} -- EXPONENTIAL in k. So
|
||||
this construction saves on free parameters but NOT on the size of the
|
||||
PSD cone that actually drives SDP solve time. See the README for measured
|
||||
timings (k=2: ~27x27 cone, sub-second; k=3: ~81x81 cone, ~2-4 minutes
|
||||
with SCS in the original sandbox).
|
||||
|
||||
Requires: numpy, cvxpy.
|
||||
"""
|
||||
import math
|
||||
from itertools import permutations
|
||||
import numpy as np
|
||||
import cvxpy as cp
|
||||
|
||||
|
||||
def sym_isometry(d, k):
|
||||
"""Isometry W, shape (d**k, dim Sym^k(C^d)), spanning the totally
|
||||
symmetric subspace of (C^d)^{tensor k}. Built by brute-force averaging
|
||||
over all k! permutations of the k tensor factors -- fine for k up to
|
||||
~6-7; for larger k this construction itself becomes the bottleneck,
|
||||
independently of the SDP."""
|
||||
n = d ** k
|
||||
P = np.zeros((n, n))
|
||||
for perm in permutations(range(k)):
|
||||
M = np.zeros((n, n))
|
||||
for idx in np.ndindex(*([d] * k)):
|
||||
new_idx = tuple(idx[perm[i]] for i in range(k))
|
||||
row = 0
|
||||
col = 0
|
||||
for i in range(k):
|
||||
row = row * d + new_idx[i]
|
||||
col = col * d + idx[i]
|
||||
M[row, col] = 1
|
||||
P += M
|
||||
P /= math.factorial(k)
|
||||
eigvals, eigvecs = np.linalg.eigh(P)
|
||||
cols = [eigvecs[:, i] for i in range(n) if abs(eigvals[i] - 1) < 1e-9]
|
||||
return np.column_stack(cols)
|
||||
|
||||
|
||||
def partial_trace_keep_first_copy(full_expr, d, k):
|
||||
"""full_expr indexed by (a, b_1, ..., b_k) with combined index
|
||||
a*d**k + b_1*d**(k-1) + ... + b_k. Traces out b_2..b_k, keeping (a,b_1)
|
||||
-- i.e. returns the marginal on A x (first copy of B)."""
|
||||
rest_dim = d ** (k - 1)
|
||||
rows = []
|
||||
for a in range(d):
|
||||
for b1 in range(d):
|
||||
row = []
|
||||
for ap in range(d):
|
||||
for b1p in range(d):
|
||||
terms = [full_expr[(a * d + b1) * rest_dim + r,
|
||||
(ap * d + b1p) * rest_dim + r]
|
||||
for r in range(rest_dim)]
|
||||
row.append(sum(terms))
|
||||
rows.append(row)
|
||||
return cp.bmat(rows)
|
||||
|
||||
|
||||
def partial_transpose_first_system(full_expr, d1, d2):
|
||||
"""Partial transpose on the first (d1-dim) system of a
|
||||
(d1*d2) x (d1*d2) matrix. Has the same eigenvalues as transposing the
|
||||
second system instead (standard fact: M^{T_A} and M^{T_B} always share
|
||||
a spectrum, since M^{T_B} = (M^{T_A})^T)."""
|
||||
rows = []
|
||||
for i in range(d1):
|
||||
for kk in range(d2):
|
||||
row = []
|
||||
for j in range(d1):
|
||||
for l in range(d2):
|
||||
row.append(full_expr[j * d2 + kk, i * d2 + l])
|
||||
rows.append(row)
|
||||
return cp.bmat(rows)
|
||||
|
||||
|
||||
def build_dps_problem(d, k):
|
||||
"""Returns (prob, rho_param, sigma) for the level-k DPS feasibility
|
||||
SDP. Set rho_param.value = <(d*d)x(d*d) target state>, then call
|
||||
dps_feasible(...) or prob.solve(...) directly."""
|
||||
W = sym_isometry(d, k)
|
||||
dim_sym = W.shape[1]
|
||||
Iso = np.kron(np.eye(d), W) # d**(k+1) x (d * dim_sym)
|
||||
sigma = cp.Variable((d * dim_sym, d * dim_sym), hermitian=True)
|
||||
full = Iso @ sigma @ Iso.conj().T
|
||||
ptrace = partial_trace_keep_first_copy(full, d, k)
|
||||
pt = partial_transpose_first_system(full, d1=d, d2=d ** k)
|
||||
rho_param = cp.Parameter((d * d, d * d), hermitian=True)
|
||||
constraints = [sigma >> 0, cp.trace(sigma) == 1,
|
||||
ptrace == rho_param, pt >> 0]
|
||||
prob = cp.Problem(cp.Minimize(0), constraints)
|
||||
return prob, rho_param, sigma
|
||||
|
||||
|
||||
def dps_feasible(prob, rho_param, rho_target, solver=cp.SCS, **solve_kwargs):
|
||||
"""Solve the (already-built) DPS problem for a given target state and
|
||||
return True iff a valid extension was found (i.e. rho_target is NOT
|
||||
certified entangled at this level)."""
|
||||
rho_param.value = rho_target
|
||||
prob.solve(solver=solver, **solve_kwargs)
|
||||
return prob.status in ("optimal", "optimal_inaccurate")
|
||||
70
scripts/dps_hierarchy/dps_level3_bisection_state.json
Normal file
70
scripts/dps_hierarchy/dps_level3_bisection_state.json
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"lo": 0.9098203125,
|
||||
"hi": 0.9103105468749999,
|
||||
"iter": 9,
|
||||
"log": [
|
||||
{
|
||||
"iter": 1,
|
||||
"p": 0.8254999999999999,
|
||||
"feasible": true,
|
||||
"time_s": 144.7,
|
||||
"status": "optimal"
|
||||
},
|
||||
{
|
||||
"iter": 2,
|
||||
"p": 0.88825,
|
||||
"feasible": true,
|
||||
"time_s": 223.2,
|
||||
"status": "optimal_inaccurate"
|
||||
},
|
||||
{
|
||||
"iter": 3,
|
||||
"p": 0.9196249999999999,
|
||||
"feasible": false,
|
||||
"time_s": 135.9,
|
||||
"status": "infeasible"
|
||||
},
|
||||
{
|
||||
"iter": 4,
|
||||
"p": 0.9039375,
|
||||
"feasible": true,
|
||||
"time_s": 226.1,
|
||||
"status": "optimal_inaccurate"
|
||||
},
|
||||
{
|
||||
"iter": 5,
|
||||
"p": 0.91178125,
|
||||
"feasible": false,
|
||||
"time_s": 139.1,
|
||||
"status": "infeasible"
|
||||
},
|
||||
{
|
||||
"iter": 6,
|
||||
"p": 0.9078593749999999,
|
||||
"feasible": true,
|
||||
"time_s": 226.9,
|
||||
"status": "optimal_inaccurate"
|
||||
},
|
||||
{
|
||||
"iter": 7,
|
||||
"p": 0.9098203125,
|
||||
"feasible": true,
|
||||
"time_s": 227.1,
|
||||
"status": "optimal_inaccurate"
|
||||
},
|
||||
{
|
||||
"iter": 8,
|
||||
"p": 0.91080078125,
|
||||
"feasible": false,
|
||||
"time_s": 139.0,
|
||||
"status": "infeasible"
|
||||
},
|
||||
{
|
||||
"iter": 9,
|
||||
"p": 0.9103105468749999,
|
||||
"feasible": false,
|
||||
"time_s": 83.2,
|
||||
"status": "infeasible"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"lo": 0.90107421875,
|
||||
"hi": 0.9013671875,
|
||||
"iter": 9,
|
||||
"log": [
|
||||
{
|
||||
"iter": 1,
|
||||
"p": 0.875,
|
||||
"feasible": true,
|
||||
"time_s": 140.1,
|
||||
"status": "optimal_inaccurate"
|
||||
},
|
||||
{
|
||||
"iter": 2,
|
||||
"p": 0.9125,
|
||||
"feasible": false,
|
||||
"time_s": 83.4,
|
||||
"status": "infeasible"
|
||||
},
|
||||
{
|
||||
"iter": 3,
|
||||
"p": 0.89375,
|
||||
"feasible": true,
|
||||
"time_s": 146.0,
|
||||
"status": "optimal_inaccurate"
|
||||
},
|
||||
{
|
||||
"iter": 4,
|
||||
"p": 0.903125,
|
||||
"feasible": false,
|
||||
"time_s": 84.0,
|
||||
"status": "infeasible"
|
||||
},
|
||||
{
|
||||
"iter": 5,
|
||||
"p": 0.8984375,
|
||||
"feasible": true,
|
||||
"time_s": 149.6,
|
||||
"status": "optimal_inaccurate"
|
||||
},
|
||||
{
|
||||
"iter": 6,
|
||||
"p": 0.90078125,
|
||||
"feasible": true,
|
||||
"time_s": 148.3,
|
||||
"status": "optimal_inaccurate"
|
||||
},
|
||||
{
|
||||
"iter": 7,
|
||||
"p": 0.9019531249999999,
|
||||
"feasible": false,
|
||||
"time_s": 84.1,
|
||||
"status": "infeasible"
|
||||
},
|
||||
{
|
||||
"iter": 8,
|
||||
"p": 0.9013671875,
|
||||
"feasible": false,
|
||||
"time_s": 84.6,
|
||||
"status": "infeasible"
|
||||
},
|
||||
{
|
||||
"iter": 9,
|
||||
"p": 0.90107421875,
|
||||
"feasible": true,
|
||||
"time_s": 148.0,
|
||||
"status": "optimal_inaccurate"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
scripts/dps_hierarchy/requirements.txt
Normal file
8
scripts/dps_hierarchy/requirements.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
numpy
|
||||
sympy
|
||||
cvxpy
|
||||
scs
|
||||
|
||||
# Optional, much faster for the DPS SDPs (03, 04, 06, 07, 08) if you have
|
||||
# a license (free for academics): mosek, and set SOLVER = cp.MOSEK in
|
||||
# those scripts.
|
||||
Loading…
Add table
Add a link
Reference in a new issue