Skip to content

Commit acaf355

Browse files
authored
feat(cones): composable cones for tsallis, conditional entropy, and quadrature/integral relative entropy (#1868)
Add composable CVXPY cone builders for the remaining value functions: tsallis_entropy (hypo), tsallis_relative_entropy (epi), quantum_conditional_entropy (hypo), relative_entropy_quadrature (epi), and integral_relative_entropy (lower + upper). The tsallis and conditional-entropy builders compose existing cones; the quadrature/integral builders factor the SDP constraints out of the former internal solve, reproducing it exactly. cones stays a strict leaf (the conditional-entropy builder uses cvxpy.partial_trace, no matrix_ops import). BREAKING CHANGE: relative_entropy_quadrature no longer accepts the m and k parameters; they moved to relative_entropy_quadrature_epi_cone. Closes #1846. Closes #1867.
1 parent ce380e3 commit acaf355

25 files changed

Lines changed: 1850 additions & 404 deletions

toqito/channel_metrics/tests/test_channel_relative_entropy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def failing_eigh(*args, **kwargs):
3838
raise LinAlgError("singular pencil")
3939

4040
monkeypatch.setattr(
41-
"toqito.state_props.integral_relative_entropy._generalized_eigenvalues",
41+
"toqito.cones._integral_relative_entropy_helpers._generalized_eigenvalues",
4242
failing_eigh,
4343
)
4444

toqito/cones/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,27 @@
77

88
from toqito.cones.geometric_mean_epi_cone import geometric_mean_epi_cone
99
from toqito.cones.geometric_mean_hypo_cone import geometric_mean_hypo_cone
10+
from toqito.cones.integral_relative_entropy_lower_cone import (
11+
integral_relative_entropy_lower_cone,
12+
)
13+
from toqito.cones.integral_relative_entropy_upper_cone import (
14+
integral_relative_entropy_upper_cone,
15+
)
1016
from toqito.cones.lieb_ando_epi_cone import lieb_ando_epi_cone
1117
from toqito.cones.lieb_ando_hypo_cone import lieb_ando_hypo_cone
1218
from toqito.cones.ln_quantum_entropy_hypo_cone import ln_quantum_entropy_hypo_cone
1319
from toqito.cones.operator_relative_entropy_epi_cone import operator_relative_entropy_epi_cone
20+
from toqito.cones.quantum_conditional_entropy_hypo_cone import (
21+
quantum_conditional_entropy_hypo_cone,
22+
)
1423
from toqito.cones.quantum_relative_entropy_epi_cone import quantum_relative_entropy_epi_cone
24+
from toqito.cones.relative_entropy_quadrature_epi_cone import (
25+
relative_entropy_quadrature_epi_cone,
26+
)
1527
from toqito.cones.trace_matrix_log_hypo_cone import trace_matrix_log_hypo_cone
1628
from toqito.cones.trace_matrix_power_epi_cone import trace_matrix_power_epi_cone
1729
from toqito.cones.trace_matrix_power_hypo_cone import trace_matrix_power_hypo_cone
30+
from toqito.cones.tsallis_entropy_hypo_cone import tsallis_entropy_hypo_cone
31+
from toqito.cones.tsallis_relative_entropy_epi_cone import (
32+
tsallis_relative_entropy_epi_cone,
33+
)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Numeric helpers for integral relative-entropy sandwich grids."""
2+
3+
import cvxpy
4+
import numpy as np
5+
from scipy.linalg import LinAlgError, eig, eigh
6+
7+
8+
def _generalized_eigenvalues(a: np.ndarray, b: np.ndarray) -> np.ndarray:
9+
"""Return real generalized eigenvalues for the pencil ``(a, b)``."""
10+
try:
11+
return np.real(eigh(a, b, check_finite=False)[0])
12+
except LinAlgError:
13+
return np.real(eig(a, b, left=False, right=False)[0])
14+
15+
16+
def _sandwich_parameters(rho: np.ndarray, sigma: np.ndarray) -> tuple[float, float]:
17+
r"""Return sandwich bounds \(\mu\) and \(\lambda\) for PSD matrices \(X\) and \(Y\)."""
18+
try:
19+
w_xy = _generalized_eigenvalues(rho, sigma)
20+
w_yx = _generalized_eigenvalues(sigma, rho)
21+
except LinAlgError as exc:
22+
raise ValueError("Failed to compute sandwich parameters from generalized eigenvalues.") from exc
23+
finite_xy = w_xy[np.isfinite(w_xy)]
24+
finite_yx = w_yx[np.isfinite(w_yx)]
25+
if finite_xy.size == 0 or finite_yx.size == 0:
26+
raise ValueError("Failed to compute sandwich parameters from generalized eigenvalues.")
27+
lam = float(np.max(finite_xy))
28+
mu = float(np.min(finite_yx))
29+
return mu, lam
30+
31+
32+
def _make_grid(mu: float, lam: float, epsilon: float) -> np.ndarray:
33+
r"""Make a grid of points for the integral representation of the relative entropy.
34+
35+
The first point of the grid is set to \(\mu\). For the k-th point \(t_k\), where
36+
\(k \gt 1\), \(t_k = t_{k-1} + \sqrt{8 \epsilon t_{k-1}}\).
37+
38+
This formula yields \(O(\sqrt{\lambda/\epsilon})\) points in the grid.
39+
"""
40+
grid = [mu]
41+
curr = mu + np.sqrt(epsilon * mu * 8)
42+
while curr < lam:
43+
grid.append(curr)
44+
curr = curr + np.sqrt(epsilon * 8 * curr)
45+
return np.array(grid + [lam])
46+
47+
48+
def _make_delta(t: np.ndarray) -> np.ndarray:
49+
r"""Make the delta coefficients for the integral representation."""
50+
delta = np.zeros(len(t))
51+
delta[0] = t[0] * ((1 + t[0] / (t[1] - t[0])) * np.log(t[1] / t[0]) - 1)
52+
delta[-1] = t[-1] * (1 - (np.log(t[-1] / t[-2]) * t[-2] / (t[-1] - t[-2])))
53+
for i in range(1, len(t) - 1):
54+
delta[i] = t[i] * (
55+
(1 + t[i] / (t[i + 1] - t[i])) * np.log(t[i + 1] / t[i])
56+
- t[i - 1] * np.log(t[i] / t[i - 1]) / (t[i] - t[i - 1])
57+
)
58+
return delta
59+
60+
61+
def _make_gamma(t: np.ndarray) -> np.ndarray:
62+
r"""Make the gamma coefficients for the integral representation."""
63+
gamma = np.zeros(len(t))
64+
gamma[0] = -1 * ((1 + t[0] / (t[1] - t[0])) * np.log(t[1] / t[0]) - 1)
65+
gamma[-1] = -1 * (1 - t[-2] * np.log(t[-1] / t[-2]) / (t[-1] - t[-2]))
66+
for i in range(1, len(t) - 1):
67+
gamma[i] = -1 * (
68+
(1 + t[i] / (t[i + 1] - t[i])) * np.log(t[i + 1] / t[i])
69+
- np.log(t[i] / t[i - 1]) * t[i - 1] / (t[i] - t[i - 1])
70+
)
71+
return gamma
72+
73+
74+
def _integral_correction(lam: float) -> float:
75+
return float(np.log(lam) + 1 - lam)
76+
77+
78+
def _require_valid_sandwich(mu: float, lam: float) -> None:
79+
if mu <= 0 or lam <= mu:
80+
raise ValueError(
81+
"The integral representation requires 0 < mu < lambda. "
82+
"This typically means the matrices are too close for the bound "
83+
"(support of X may not be contained in support of Y)."
84+
)
85+
86+
87+
def _numeric_pair_for_sandwich(
88+
mat_x: cvxpy.Expression,
89+
mat_y: cvxpy.Expression,
90+
) -> tuple[np.ndarray, np.ndarray]:
91+
if mat_x.value is None or mat_y.value is None:
92+
raise ValueError(
93+
"Sandwich parameters require numeric `.value` on mat_x and mat_y, or pass mu and lam explicitly."
94+
)
95+
return np.asarray(mat_x.value), np.asarray(mat_y.value)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""CVXPY constraints for the integral lower approximation of quantum relative entropy."""
2+
3+
import cvxpy
4+
import numpy as np
5+
6+
from toqito.cones._integral_relative_entropy_helpers import (
7+
_integral_correction,
8+
_make_grid,
9+
_numeric_pair_for_sandwich,
10+
_require_valid_sandwich,
11+
_sandwich_parameters,
12+
)
13+
from toqito.cones._utils import _require_square_2d, _symmetric_like_variable
14+
15+
16+
def integral_relative_entropy_lower_cone(
17+
mat_x: cvxpy.Expression,
18+
mat_y: cvxpy.Expression,
19+
t: cvxpy.Expression,
20+
*,
21+
epsilon_dec: float = 1e-2,
22+
mu: float | None = None,
23+
lam: float | None = None,
24+
hermitian: bool = False,
25+
) -> list[cvxpy.Constraint]:
26+
r"""Return CVXPY constraints for the integral lower approximation of \(D(X\|Y)\).
27+
28+
Discretizes the integral representation of quantum relative entropy
29+
[@kossmann2024optimisingrelativeentropy] and enforces
30+
31+
\[
32+
t \geqslant L_{\varepsilon}(X, Y),
33+
\]
34+
35+
where \(L_{\varepsilon}\) is the lower SDP bound (with \(n \times n\) auxiliaries,
36+
not the \(n^2\) Kronecker lift of ``quantum_relative_entropy_epi_cone``).
37+
38+
Sandwich endpoints \(\mu, \lambda\) are taken from ``mu`` / ``lam`` when provided;
39+
otherwise they are computed from ``mat_x.value`` and ``mat_y.value``.
40+
41+
Args:
42+
mat_x: A CVXPY expression for an ``n x n`` PSD matrix \(X\).
43+
mat_y: A CVXPY expression for an ``n x n`` PSD matrix \(Y\).
44+
t: A CVXPY scalar (or ``1 x 1``) epigraph variable.
45+
epsilon_dec: Grid refinement parameter \(\varepsilon\).
46+
mu: Optional sandwich lower endpoint.
47+
lam: Optional sandwich upper endpoint.
48+
hermitian: Whether the matrices are Hermitian or symmetric.
49+
50+
Raises:
51+
ValueError: If ``mat_x`` or ``mat_y`` is not square 2D.
52+
ValueError: If shapes differ, sandwich is degenerate, or numeric values
53+
are missing when ``mu`` / ``lam`` are omitted.
54+
55+
Returns:
56+
A list of CVXPY constraints.
57+
58+
"""
59+
_require_square_2d(mat_x, "mat_x")
60+
_require_square_2d(mat_y, "mat_y")
61+
if mat_x.shape != mat_y.shape:
62+
raise ValueError("mat_x and mat_y must have the same shape")
63+
64+
if mu is None or lam is None:
65+
if mu is not None or lam is not None:
66+
raise ValueError("mu and lam must both be provided or both omitted")
67+
x_val, y_val = _numeric_pair_for_sandwich(mat_x, mat_y)
68+
mu, lam = _sandwich_parameters(x_val, y_val)
69+
_require_valid_sandwich(mu, lam)
70+
71+
grid = _make_grid(mu, lam, epsilon_dec)
72+
r = len(grid)
73+
alpha = [float(np.log(grid[k] / grid[k + 1])) for k in range(r - 1)]
74+
beta = [float(grid[k + 1] - grid[k]) for k in range(r - 1)]
75+
76+
n = int(mat_x.shape[0])
77+
mu_vars = [_symmetric_like_variable(n, hermitian=hermitian) for _ in range(r - 1)]
78+
constraints: list[cvxpy.Constraint] = [mu_vars[k] >> 0 for k in range(r - 1)]
79+
constraints.extend(mu_vars[k] - alpha[k] * mat_x - beta[k] * mat_y >> 0 for k in range(r - 1))
80+
bound = cvxpy.sum([cvxpy.trace(mu_vars[k]) for k in range(r - 1)]) + _integral_correction(lam)
81+
if hermitian:
82+
bound = cvxpy.real(bound)
83+
constraints.append(t >= bound)
84+
return constraints
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""CVXPY constraints for the integral upper approximation of quantum relative entropy."""
2+
3+
import cvxpy
4+
5+
from toqito.cones._integral_relative_entropy_helpers import (
6+
_integral_correction,
7+
_make_delta,
8+
_make_gamma,
9+
_make_grid,
10+
_numeric_pair_for_sandwich,
11+
_require_valid_sandwich,
12+
_sandwich_parameters,
13+
)
14+
from toqito.cones._utils import _require_square_2d, _symmetric_like_variable
15+
16+
17+
def integral_relative_entropy_upper_cone(
18+
mat_x: cvxpy.Expression,
19+
mat_y: cvxpy.Expression,
20+
t: cvxpy.Expression,
21+
*,
22+
epsilon_dec: float = 1e-2,
23+
mu: float | None = None,
24+
lam: float | None = None,
25+
hermitian: bool = False,
26+
) -> list[cvxpy.Constraint]:
27+
r"""Return CVXPY constraints for the integral upper approximation of \(D(X\|Y)\).
28+
29+
Discretizes the integral representation of quantum relative entropy
30+
[@kossmann2024optimisingrelativeentropy] and enforces
31+
32+
\[
33+
t \geqslant U_{\varepsilon}(X, Y),
34+
\]
35+
36+
where \(U_{\varepsilon}\) is the upper SDP bound (with \(n \times n\) auxiliaries,
37+
not the \(n^2\) Kronecker lift of ``quantum_relative_entropy_epi_cone``).
38+
39+
Sandwich endpoints \(\mu, \lambda\) are taken from ``mu`` / ``lam`` when provided;
40+
otherwise they are computed from ``mat_x.value`` and ``mat_y.value``.
41+
42+
Args:
43+
mat_x: A CVXPY expression for an ``n x n`` PSD matrix \(X\).
44+
mat_y: A CVXPY expression for an ``n x n`` PSD matrix \(Y\).
45+
t: A CVXPY scalar (or ``1 x 1``) epigraph variable.
46+
epsilon_dec: Grid refinement parameter \(\varepsilon\).
47+
mu: Optional sandwich lower endpoint.
48+
lam: Optional sandwich upper endpoint.
49+
hermitian: Whether the matrices are Hermitian or symmetric.
50+
51+
Raises:
52+
ValueError: If ``mat_x`` or ``mat_y`` is not square 2D.
53+
ValueError: If shapes differ, sandwich is degenerate, or numeric values
54+
are missing when ``mu`` / ``lam`` are omitted.
55+
56+
Returns:
57+
A list of CVXPY constraints.
58+
59+
"""
60+
_require_square_2d(mat_x, "mat_x")
61+
_require_square_2d(mat_y, "mat_y")
62+
if mat_x.shape != mat_y.shape:
63+
raise ValueError("mat_x and mat_y must have the same shape")
64+
65+
if mu is None or lam is None:
66+
if mu is not None or lam is not None:
67+
raise ValueError("mu and lam must both be provided or both omitted")
68+
x_val, y_val = _numeric_pair_for_sandwich(mat_x, mat_y)
69+
mu, lam = _sandwich_parameters(x_val, y_val)
70+
_require_valid_sandwich(mu, lam)
71+
72+
grid = _make_grid(mu, lam, epsilon_dec)
73+
gamma = _make_gamma(grid)
74+
delta = _make_delta(grid)
75+
r = len(grid)
76+
77+
n = int(mat_x.shape[0])
78+
nu_vars = [_symmetric_like_variable(n, hermitian=hermitian) for _ in range(r)]
79+
constraints: list[cvxpy.Constraint] = [nu_vars[k] >> 0 for k in range(r)]
80+
constraints.extend(nu_vars[k] - float(gamma[k]) * mat_x - float(delta[k]) * mat_y >> 0 for k in range(r))
81+
bound = cvxpy.sum([cvxpy.trace(nu_vars[k]) for k in range(r)]) + _integral_correction(lam)
82+
if hermitian:
83+
bound = cvxpy.real(bound)
84+
constraints.append(t >= bound)
85+
return constraints

0 commit comments

Comments
 (0)