Tatopenn's picture
Sync mirror with GitHub main (v8.1.62): subpackage refactor, mitigation promotions, MCP server refactor
2a87ede verified
Raw History Blame Contribute Delete
8.22 kB
"""Sandwiched Quantum Renyi Divergence for full density-matrix diagnostics
(Muller-Lennert, Reeb, Wolf, Wilde, "On quantum Renyi entropies: a new
generalization and some applications", arXiv:1306.3142, Definition 1).
D_alpha(rho||sigma) = 1/(alpha-1) * log2 Tr[(sigma^e rho sigma^e)^alpha],
e = (1-alpha)/(2*alpha), with the alpha->1 limit reducing to the standard
quantum relative entropy and alpha=1/2 reducing to a fidelity-based form.
Originated from a Colab proposal with a real bug in its case_general
branch: `tr_inner = jnp.maximum(tr_inner, 1.0)` floors the inner trace at
1.0 even when the true value is < 1 (the normal case for non-commuting
rho, sigma), silently forcing every result to log2(1)=0 -- confirmed
directly in the Colab's own printed output (alpha=1.5 gave exactly
0.000000 across an entire rotation sweep). A second, deeper bug survived
the floor-value fix alone: for alpha > 1, a trace below 1 is not a
numerical artifact to clamp away, it is the genuine signature of a
support mismatch (supp(rho) not contained in supp(sigma)), which the
divergence must report as +inf, not a finite (and wrong-signed) number --
verified by hand on two different pure states: Tr[Q^1.5] = 0.6759,
matching the closed-form prediction (|<sigma|rho>|^2)^alpha exactly, and
plugging that into the naive formula gives a finite NEGATIVE divergence,
worse than the original bug's silent zero since it looks plausible
instead of visibly wrong.
Fixed and validated in Dense-Evolution-Discovery, Experiment 29
(https://tatopenn-cell.github.io/Dense-Evolution-Discovery/sandwiched_renyi_density_matrix/):
against the alpha->1 relative-entropy limit (matches an independent numpy
reference to 4 decimal places), the commuting/diagonal case (reduces
exactly to the classical Renyi divergence), and the support-violation
+inf case (verified at alpha>1, confirmed the branch does not fire
spuriously at alpha<1).
Its originally proposed use case -- replacing the JSD-based truncation
criterion in dense_evolution.mps's bond-dimension search -- was
independently disproven: on the diagonal singular-value spectrum used
there, rho and sigma commute, so a non-commuting-aware divergence induces
the exact same truncation ordering as JSD (5 benchmark configurations,
byte-identical chi_used and truncation error every time) -- nothing for
it to add in that setting. Promoted here instead for the genuinely
non-commuting full-density-matrix diagnostic use case it WAS validated
against: alongside uhlmann_fidelity, tracking a Bell state degraded by
amplitude damping, where the two metrics' noise-sensitivity curves
visibly diverge from each other.
"""
import jax
import jax.numpy as jnp
__all__ = ["sandwiched_renyi_divergence", "sandwiched_renyi_divergence_jit"]
_EPS = 1e-12
def _case_half(rho, sigma):
ev_r, ec_r = jnp.linalg.eigh(rho)
safe_ev_r = jnp.where(ev_r > _EPS, ev_r, 0.0)
sqrt_rho = (ec_r * jnp.sqrt(safe_ev_r)) @ jnp.conj(ec_r).T
uhlmann_mat = sqrt_rho @ sigma @ sqrt_rho
ev_u = jnp.linalg.eigvalsh(uhlmann_mat)
safe_ev_u = jnp.where(ev_u > _EPS, ev_u, 0.0)
fidelity = jnp.clip(jnp.sum(jnp.sqrt(safe_ev_u)), 0.0, 1.0)
return -2.0 * jnp.log2(jnp.maximum(fidelity, _EPS))
def _case_one(rho, sigma):
ev_r, ec_r = jnp.linalg.eigh(rho)
safe_ev_r = jnp.where(ev_r > _EPS, ev_r, 1.0)
log_rho = (ec_r * jnp.where(ev_r > _EPS, jnp.log2(safe_ev_r), 0.0)) @ jnp.conj(ec_r).T
ev_s, ec_s = jnp.linalg.eigh(sigma)
safe_ev_s = jnp.where(ev_s > _EPS, ev_s, 1.0)
log_sigma = (ec_s * jnp.where(ev_s > _EPS, jnp.log2(safe_ev_s), 0.0)) @ jnp.conj(ec_s).T
return jnp.trace(rho @ (log_rho - log_sigma)).real
def _case_general(rho, sigma, alpha):
exponent = (1.0 - alpha) / (2.0 * alpha)
ev_s, ec_s = jnp.linalg.eigh(sigma)
mask_s = ev_s > _EPS
safe_ev_s = jnp.where(mask_s, ev_s, 1.0)
pow_ev_s = jnp.where(mask_s, safe_ev_s ** exponent, 0.0)
sigma_pow = (ec_s * pow_ev_s) @ jnp.conj(ec_s).T
int_m = sigma_pow @ rho @ sigma_pow
v_int = jnp.linalg.eigvalsh(int_m)
mask_int = v_int > _EPS
safe_v_int = jnp.where(mask_int, v_int, 1.0)
pow_v_int = jnp.where(mask_int, safe_v_int ** alpha, 0.0)
tr_inner = jnp.sum(pow_v_int)
# For alpha > 1, D_alpha is finite only when supp(rho) subset supp(sigma);
# tr_inner < 1 there is the genuine signature of a support mismatch, not
# a numerical artifact -- see the module docstring's bug-history note.
is_support_violation = (alpha > 1.0) & (tr_inner < 1.0 - 1e-9)
tr_inner_safe = jnp.maximum(tr_inner, _EPS)
finite_result = ((1.0 / (alpha - 1.0)) * jnp.log2(tr_inner_safe)).real
return jnp.where(is_support_violation, jnp.inf, finite_result)
def _sandwiched_renyi_divergence_core(rho: jnp.ndarray, sigma: jnp.ndarray, alpha: float = 1.5) -> jnp.ndarray:
# alpha must be a jnp value, not a raw Python float, before it reaches
# _case_general: jax.lax.cond traces BOTH branches regardless of which
# one runs, and `1.0 / (alpha - 1.0)` at alpha=1.0 raises a Python
# ZeroDivisionError if alpha is still a plain float at that point (the
# case_one branch's own guard never gets a chance to skip it) -- under
# jax.jit this conversion happens automatically (all arguments get
# traced), which is why the original un-split Discovery version never
# hit this; the eager (non-jit) core path here needs it explicitly.
alpha = jnp.asarray(alpha, dtype=jnp.float64)
is_half = jnp.isclose(alpha, 0.5)
is_one = jnp.isclose(alpha, 1.0)
return jax.lax.cond(
is_half, lambda: _case_half(rho, sigma),
lambda: jax.lax.cond(is_one, lambda: _case_one(rho, sigma), lambda: _case_general(rho, sigma, alpha)),
)
def sandwiched_renyi_divergence(rho: jnp.ndarray, sigma: jnp.ndarray, alpha: float = 1.5) -> float:
"""Sandwiched quantum Renyi divergence D_alpha(rho||sigma), in bits
(log2). `rho`, `sigma` are density matrices of the same dimension;
`alpha` selects the order (0.5 -> fidelity-based, 1.0 -> standard
relative entropy, both handled as exact closed-form limits rather
than through the general formula's own alpha->0.5/1 numerical
instability).
Zero when rho == sigma at every alpha (verified). For alpha > 1,
returns `+inf` when supp(rho) is not contained in supp(sigma) --
e.g. two different pure (rank-1) states -- rather than a finite
number; see the module docstring for why this is the mathematically
correct behavior, not an edge-case failure.
KNOWN LIMITATION at exactly alpha=1.0: the same support-violation
check is NOT applied to the alpha=1 (relative-entropy) branch, which
instead clips log(0)-type contributions to 0 rather than diverging --
e.g. D_1(rho||sigma) for two different pure states returns 0.0, not
+inf, even though the true relative entropy diverges there too.
Experiment 29 validated the alpha=1 branch only against full-rank
(depolarized) inputs specifically to sidestep this exactly-singular
case (`scipy.linalg.logm` itself raises `LogmExactlySingularWarning`
on singular inputs -- an inherent ill-conditioning of relative
entropy near degenerate support, not unique to this implementation).
Do not rely on alpha=1 to correctly flag a support mismatch; use
alpha slightly above 1 (e.g. 1.001) if that matters for your use case.
Validation-only, like `uhlmann_fidelity`: meant to grade a correction
against a known reference state, not to feed into one (see
`uhlmann_fidelity`'s docstring for the full "ideal state as oracle"
argument, which applies here identically).
"""
rho = jnp.asarray(rho, dtype=jnp.complex128)
sigma = jnp.asarray(sigma, dtype=jnp.complex128)
return float(_sandwiched_renyi_divergence_core(rho, sigma, alpha))
sandwiched_renyi_divergence_jit = jax.jit(_sandwiched_renyi_divergence_core)
"""`jax.jit`-compiled entry point for `sandwiched_renyi_divergence`. `rho`/
`sigma` must already be `complex128`. Returns a jnp scalar, not a Python
`float`."""