226 lines
9.2 KiB
Python
226 lines
9.2 KiB
Python
|
|
"""
|
||
|
|
SDP-based sharpening of the biseparable threshold for min(||M_AB||_*, ||M_AC||_*, ||M_AD||_*)
|
||
|
|
on 4 qubits, via the PPT-mixture relaxation (Jungnitsch-Moroder-Guehne 2011 style SDP).
|
||
|
|
|
||
|
|
WHY NOT A ONE-SHOT SDP
|
||
|
|
-----------------------
|
||
|
|
||M_S(rho)||_* is CONVEX in rho. Maximizing a convex function over a convex set is itself
|
||
|
|
a non-convex problem -- no SDP solver can do this directly.
|
||
|
|
|
||
|
|
THE FIX -- alternating (Frank-Wolfe) scheme using the dual (support-function) form of the
|
||
|
|
nuclear norm:
|
||
|
|
||X||_* = max_{||O||_op <= 1} tr(O^T X)
|
||
|
|
For FIXED O_AB, O_AC, O_AD (each with operator norm <= 1), min_S tr(O_S^T M_S(rho)) is a
|
||
|
|
min of THREE LINEAR functions of rho, hence CONCAVE, hence
|
||
|
|
max_{rho in PPT-mixtures} min_S tr(O_S^T M_S(rho))
|
||
|
|
IS a legitimate concave-maximization problem -> a genuine SDP.
|
||
|
|
|
||
|
|
Loop:
|
||
|
|
1) fix O's -> solve the SDP -> get rho*
|
||
|
|
2) at rho*, compute the TRUE nuclear norms and their exact dual witnesses
|
||
|
|
O_S = U_S V_S^T (from the SVD of M_S(rho*)) -> update O's
|
||
|
|
3) repeat
|
||
|
|
|
||
|
|
This is a heuristic (finds a local stationary point of a genuinely non-convex problem),
|
||
|
|
but it searches the FULL convex PPT-mixture body (a strict superset of the biseparable
|
||
|
|
states), not just a hand-picked family of pure-state mixtures -- a much stronger stress
|
||
|
|
test of the conjectured biseparable supremum (~3.0) than black-box optimization over a
|
||
|
|
parametrized ansatz.
|
||
|
|
|
||
|
|
Requires: pip install cvxpy numpy scipy
|
||
|
|
Every piece of linear algebra here (Pauli-tensor extraction, partial-transpose
|
||
|
|
permutation, the fast coefficient-matrix construction) was verified against a slow
|
||
|
|
reference implementation in pure numpy before being translated to cvxpy; only the
|
||
|
|
cvxpy Problem-building/solving itself is unverified in the sandbox this was written in
|
||
|
|
(no cvxpy, no network there).
|
||
|
|
"""
|
||
|
|
import numpy as np
|
||
|
|
import cvxpy as cp
|
||
|
|
|
||
|
|
# ----------------------------------------------------------------------
|
||
|
|
# 1. Pauli tensor operators, index k = i0*64 + i1*16 + i2*4 + i3
|
||
|
|
# ----------------------------------------------------------------------
|
||
|
|
I2 = 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 = [I2, X, Y, Z]
|
||
|
|
|
||
|
|
|
||
|
|
def kron4(a, b, c, d):
|
||
|
|
return np.kron(np.kron(a, b), np.kron(c, d))
|
||
|
|
|
||
|
|
|
||
|
|
PAULI_OPS = np.zeros((256, 16, 16), dtype=complex)
|
||
|
|
for i0 in range(4):
|
||
|
|
for i1 in range(4):
|
||
|
|
for i2 in range(4):
|
||
|
|
for i3 in range(4):
|
||
|
|
k = i0 * 64 + i1 * 16 + i2 * 4 + i3
|
||
|
|
PAULI_OPS[k] = kron4(PAULI[i0], PAULI[i1], PAULI[i2], PAULI[i3])
|
||
|
|
|
||
|
|
CLUSTERS = [('AB', 0, 1), ('AC', 0, 2), ('AD', 0, 3)]
|
||
|
|
|
||
|
|
|
||
|
|
def build_coeff_matrix(s0, s1):
|
||
|
|
"""(225,256) complex matrix Coeff s.t., for rho flattened row-major (vec[a*16+b]=rho[a,b]),
|
||
|
|
(Coeff @ vec).reshape(15,15).real / 3.0 == the normalized bigraduated cluster map M_S,
|
||
|
|
S = {s0,s1}, complement the other two qubits. Verified against a slow trace-based
|
||
|
|
reference (max abs diff ~2e-17)."""
|
||
|
|
others = [k for k in range(4) if k not in (s0, s1)]
|
||
|
|
c0, c1 = others
|
||
|
|
rows = [(i, j) for i in range(4) for j in range(4) if (i, j) != (0, 0)]
|
||
|
|
cols = [(i, j) for i in range(4) for j in range(4) if (i, j) != (0, 0)]
|
||
|
|
Coeff = np.zeros((225, 256), dtype=complex)
|
||
|
|
entry = 0
|
||
|
|
for (ia, ib) in rows:
|
||
|
|
for (ic, idd) in cols:
|
||
|
|
idx = [0, 0, 0, 0]
|
||
|
|
idx[s0] = ia
|
||
|
|
idx[s1] = ib
|
||
|
|
idx[c0] = ic
|
||
|
|
idx[c1] = idd
|
||
|
|
k = idx[0] * 64 + idx[1] * 16 + idx[2] * 4 + idx[3]
|
||
|
|
# trace(rho @ P_k) = sum_{a,b} rho[a,b] P_k[b,a]; row-major vec_rho[a*16+b]=rho[a,b]
|
||
|
|
Coeff[entry, :] = PAULI_OPS[k].T.flatten()
|
||
|
|
entry += 1
|
||
|
|
return Coeff
|
||
|
|
|
||
|
|
|
||
|
|
COEFF = {name: build_coeff_matrix(s0, s1) for name, s0, s1 in CLUSTERS}
|
||
|
|
|
||
|
|
|
||
|
|
# ----------------------------------------------------------------------
|
||
|
|
# 2. Partial transpose as an explicit (256,256) permutation matrix
|
||
|
|
# ----------------------------------------------------------------------
|
||
|
|
def partial_transpose_perm(T, n=4):
|
||
|
|
perm = np.zeros(4 ** n, dtype=int)
|
||
|
|
for row in range(2 ** n):
|
||
|
|
rbits = [(row >> (n - 1 - i)) & 1 for i in range(n)]
|
||
|
|
for col in range(2 ** n):
|
||
|
|
cbits = [(col >> (n - 1 - i)) & 1 for i in range(n)]
|
||
|
|
new_row = [cbits[i] if i in T else rbits[i] for i in range(n)]
|
||
|
|
new_col = [rbits[i] if i in T else cbits[i] for i in range(n)]
|
||
|
|
new_row_idx = sum(b << (n - 1 - i) for i, b in enumerate(new_row))
|
||
|
|
new_col_idx = sum(b << (n - 1 - i) for i, b in enumerate(new_col))
|
||
|
|
perm[new_row_idx * (2 ** n) + new_col_idx] = row * (2 ** n) + col
|
||
|
|
return perm
|
||
|
|
|
||
|
|
|
||
|
|
def perm_matrix(T, n=4):
|
||
|
|
perm = partial_transpose_perm(T, n)
|
||
|
|
P = np.zeros((4 ** n, 4 ** n))
|
||
|
|
for new_idx, old_idx in enumerate(perm):
|
||
|
|
P[new_idx, old_idx] = 1.0
|
||
|
|
return P
|
||
|
|
|
||
|
|
|
||
|
|
# The 7 bipartitions of {A,B,C,D}={0,1,2,3}; PT taken w.r.t. the listed (smaller) side.
|
||
|
|
# PPT is equivalent for either side of a bipartition, so this choice is arbitrary but fixed.
|
||
|
|
BIPARTITIONS = [
|
||
|
|
('AB|CD', {0, 1}), ('AC|BD', {0, 2}), ('AD|BC', {0, 3}),
|
||
|
|
('A|BCD', {0}), ('B|ACD', {1}), ('C|ABD', {2}), ('D|ABC', {3}),
|
||
|
|
]
|
||
|
|
PT_MATRIX = {name: perm_matrix(side) for name, side in BIPARTITIONS}
|
||
|
|
|
||
|
|
|
||
|
|
def cvxpy_flatten_rowmajor(rho_expr):
|
||
|
|
"""16x16 cvxpy expression -> length-256 cvxpy expression, row-major."""
|
||
|
|
return cp.hstack([rho_expr[i, :] for i in range(16)])
|
||
|
|
|
||
|
|
|
||
|
|
def cvxpy_partial_transpose(rho_expr, name):
|
||
|
|
vec = cvxpy_flatten_rowmajor(rho_expr)
|
||
|
|
pt_vec = PT_MATRIX[name] @ vec
|
||
|
|
return cp.reshape(pt_vec, (16, 16), order='C') # MUST match row-major PT_MATRIX construction
|
||
|
|
|
||
|
|
|
||
|
|
def cvxpy_cluster_maps(rho_expr):
|
||
|
|
vec = cvxpy_flatten_rowmajor(rho_expr)
|
||
|
|
out = {}
|
||
|
|
for name, s0, s1 in CLUSTERS:
|
||
|
|
flat = COEFF[name] @ vec / 3.0
|
||
|
|
out[name] = cp.real(cp.reshape(flat, (15, 15), order='C')) # MUST match row-major COEFF construction
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
# ----------------------------------------------------------------------
|
||
|
|
# 3. PPT-mixture SDP + one alternating step
|
||
|
|
# ----------------------------------------------------------------------
|
||
|
|
def solve_fixed_witness_step(O, solver=cp.SCS, verbose=False):
|
||
|
|
"""O: dict name(in {'AB','AC','AD'}) -> 15x15 real array with operator norm <= 1.
|
||
|
|
Returns (rho_value, sdp_optimal_t, dict of numeric M_S values)."""
|
||
|
|
rho_gammas = {}
|
||
|
|
constraints = []
|
||
|
|
for name, side in BIPARTITIONS:
|
||
|
|
r = cp.Variable((16, 16), hermitian=True)
|
||
|
|
constraints.append(r >> 0) # PSD
|
||
|
|
constraints.append(cvxpy_partial_transpose(r, name) >> 0) # PPT across this cut
|
||
|
|
rho_gammas[name] = r
|
||
|
|
|
||
|
|
rho = sum(rho_gammas.values())
|
||
|
|
constraints.append(cp.real(cp.trace(rho)) == 1)
|
||
|
|
|
||
|
|
M = cvxpy_cluster_maps(rho)
|
||
|
|
t = cp.Variable()
|
||
|
|
for name, _, _ in CLUSTERS:
|
||
|
|
constraints.append(t <= cp.sum(cp.multiply(O[name], M[name])))
|
||
|
|
|
||
|
|
prob = cp.Problem(cp.Maximize(t), constraints)
|
||
|
|
prob.solve(solver=solver, verbose=verbose)
|
||
|
|
|
||
|
|
if rho.value is None:
|
||
|
|
raise RuntimeError(f"SDP did not solve to a usable solution (status={prob.status}).")
|
||
|
|
|
||
|
|
M_vals = {name: M[name].value for name, _, _ in CLUSTERS}
|
||
|
|
return rho.value, prob.value, M_vals
|
||
|
|
|
||
|
|
|
||
|
|
def true_norms_and_witnesses(M_vals):
|
||
|
|
"""Exact nuclear norms of the numeric M_S matrices, plus their optimal dual witnesses
|
||
|
|
O_S = U_S V_S^T (operator norm exactly 1, achieves tr(O_S^T M_S) = ||M_S||_*)."""
|
||
|
|
norms, O_opt = {}, {}
|
||
|
|
for name, M in M_vals.items():
|
||
|
|
U, s, Vt = np.linalg.svd(M)
|
||
|
|
norms[name] = s.sum()
|
||
|
|
O_opt[name] = U @ Vt
|
||
|
|
return norms, O_opt
|
||
|
|
|
||
|
|
|
||
|
|
# ----------------------------------------------------------------------
|
||
|
|
# 4. Alternating search with multiple random restarts
|
||
|
|
# ----------------------------------------------------------------------
|
||
|
|
def alternating_search(n_restarts=5, n_iters=15, seed0=0, verbose=True):
|
||
|
|
best_min, best_rho = -np.inf, None
|
||
|
|
for r in range(n_restarts):
|
||
|
|
rng = np.random.default_rng(seed0 + r)
|
||
|
|
O = {}
|
||
|
|
for name, _, _ in CLUSTERS:
|
||
|
|
A = rng.normal(size=(15, 15))
|
||
|
|
O[name] = A / np.linalg.norm(A, ord=2) # operator norm 1
|
||
|
|
if verbose:
|
||
|
|
print(f"--- restart {r} ---")
|
||
|
|
for it in range(n_iters):
|
||
|
|
rho_val, t_val, M_vals = solve_fixed_witness_step(O)
|
||
|
|
norms, O = true_norms_and_witnesses(M_vals)
|
||
|
|
cur_min = min(norms.values())
|
||
|
|
if verbose:
|
||
|
|
nice = {k: round(v, 4) for k, v in norms.items()}
|
||
|
|
print(f" iter {it:2d}: SDP t={t_val:.4f} true norms={nice} min={cur_min:.4f}")
|
||
|
|
if cur_min > best_min:
|
||
|
|
best_min, best_rho = cur_min, rho_val
|
||
|
|
if verbose:
|
||
|
|
print()
|
||
|
|
return best_min, best_rho
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
print("Proven upper bound (pure-state extreme points + convexity of the sum): 11/3 =", 11 / 3)
|
||
|
|
print("Conjectured true biseparable / PPT-mixture supremum: ~3.0")
|
||
|
|
print()
|
||
|
|
best_min, best_rho = alternating_search(n_restarts=5, n_iters=15)
|
||
|
|
print("=" * 60)
|
||
|
|
print("Best min(||M_AB||_*, ||M_AC||_*, ||M_AD||_*) found over PPT-mixtures:", best_min)
|
||
|
|
print("- if this stays at/near 3.0 across restarts -> strong evidence 3.0 is exact")
|
||
|
|
print("- if it clearly exceeds 3.0 -> best_rho is a concrete witness state to inspect")
|