File size: 8,218 Bytes
2a87ede
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""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`."""