Skip to content

Commit 925bf20

Browse files
authored
Doc improvements for some atoms, plus an Expression.cast function. (cvxpy#2847)
* documentation and style for quantum_cond_entr * documentation for von_neumann_entr and input checking thats consistent with other quantum information atoms * documentation and evaluation tolerance for quantum_rel_entr * remove unused function definitions; * add a hermitian_wrap * documentation, and create new Expression.cast function whose meaning is clearer than Expression.cast_to_const * make ruff happy
1 parent 88792c5 commit 925bf20

File tree

7 files changed

+134
-69
lines changed

7 files changed

+134
-69
lines changed

cvxpy/atoms/quantum_cond_entr.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Literal, Tuple
22

33
import numpy as np
44

@@ -9,12 +9,54 @@
99
from cvxpy.expressions.expression import Expression
1010

1111

12-
def quantum_cond_entr(rho: Expression , dim: list[int], sys: Optional[int]=0):
12+
def quantum_cond_entr(
13+
rho: Expression , dims: Tuple[int, int], sys: Literal[0, 1] = 0, quad_approx=(3, 3)
14+
):
15+
"""
16+
Returns (an approximation of) the quantum conditional entropy for a bipartite state,
17+
conditioning on system :math:`\\texttt{sys}.`
18+
19+
Formally, if :math:`N` is the von Neumann entropy function and
20+
:math:`\\operatorname{tr}_{\\texttt{sys}}` is the partial trace operator over subsystem
21+
:math:`\\texttt{sys},` the returned expression represents
22+
23+
.. math::
24+
N(\\rho) - N(\\operatorname{tr}_{\\texttt{sys}}(\\rho)).
25+
26+
Parameters
27+
----------
28+
rho : Expression
29+
A Hermitian matrix of order :math:`\\texttt{dims[0]}\\cdot\\texttt{dims[1]}.`
30+
31+
dims : tuple
32+
The dimensions of the two subsystems that definte :math:`\\rho` as a bipartite state.
33+
34+
sys : int
35+
The subsystem on which to condition in evaluating the conditional quantum entropy.
36+
37+
quad_approx : Tuple[int, int]
38+
quad_approx[0] is the number of quadrature nodes and quad_approx[1] is the number of scaling
39+
points in the quadrature scheme from https://arxiv.org/abs/1705.00812.
40+
41+
Notes
42+
-----
43+
This function does not assume :math:`\\operatorname{tr}(\rho)=1,` which would be required
44+
for most uses of this function in the context of quantum information theory. See
45+
https://en.wikipedia.org/wiki/Conditional_quantum_entropy for more information.
46+
"""
47+
if len(dims) != 2:
48+
err = 'This function is only defined for a tensor product of two subsystems,' + \
49+
f'but {len(dims)} subsystems were implied from the value of dims.'
50+
raise ValueError(err)
1351
if sys == 0:
14-
composite_arg = kron(np.eye(dim[0]),
15-
partial_trace(rho, dim, sys))
16-
return -quantum_rel_entr(rho, hermitian_wrap(composite_arg))
52+
composite_arg = kron(
53+
np.eye(dims[0]), partial_trace(rho, dims, sys)
54+
)
1755
elif sys == 1:
18-
composite_arg = kron(partial_trace(rho, dim, sys),
19-
np.eye(dim[1]))
20-
return -quantum_rel_entr(rho, hermitian_wrap(composite_arg))
56+
composite_arg = kron(
57+
partial_trace(rho, dims, sys), np.eye(dims[1])
58+
)
59+
else:
60+
raise ValueError(f'Argument sys must be either 0 or 1; got {sys}.')
61+
composite_arg = hermitian_wrap(composite_arg)
62+
return -quantum_rel_entr(rho, composite_arg, quad_approx)

cvxpy/atoms/quantum_rel_entr.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,40 @@
1919
from scipy import linalg as LA
2020
from scipy.stats import entropy
2121

22+
from cvxpy import settings
2223
from cvxpy.atoms.atom import Atom
2324
from cvxpy.constraints.constraint import Constraint
2425

2526

2627
class quantum_rel_entr(Atom):
2728
"""
29+
An approximation of the quantum relative entropy between systems with (possibly un-normalized)
30+
density matrices :math:`X` and :math`Y:`
31+
32+
.. math::
33+
\\operatorname{tr}\\left( X ( \\log X - \\log Y ) \\right).
34+
35+
The approximation uses a quadrature scheme described in https://arxiv.org/abs/1705.00812.
36+
2837
Parameters
2938
----------
3039
X : Expression or numeric
3140
A PSD matrix
3241
Y : Expression or numeric
3342
A PSD matrix
43+
quad_approx : Tuple[int, int]
44+
quad_approx[0] is the number of quadrature nodes and quad_approx[1] is the number of scaling
45+
points in the quadrature scheme from https://arxiv.org/abs/1705.00812.
46+
47+
Notes
48+
-----
49+
This function does not assume :math:`\\operatorname{tr}(X)=\\operatorname{tr}(Y)=1,` which
50+
would be required for most uses of this function in the context of quantum information.
3451
"""
3552

53+
EVAL_TOL = min(settings.ATOM_EVAL_TOL, 1e-6)
54+
3655
def __init__(self, X, Y, quad_approx: Tuple[int, int] = (3, 3)) -> None:
37-
# TODO: add a check that N is symmetric/Hermitian.
3856
self.quad_approx = quad_approx
3957
super(quantum_rel_entr, self).__init__(X, Y)
4058

@@ -49,12 +67,11 @@ def numeric(self, values):
4967
w1, V = LA.eigh(X)
5068
w2, W = LA.eigh(Y)
5169
u = w1.T @ np.abs(V.conj().T @ W) ** 2
52-
def func(x):
53-
assert np.all(x >= 0)
54-
x_sum = x.sum()
55-
val = -entropy(x)
56-
un_normalized = (x_sum * val + np.log(x_sum)*x_sum)
57-
return un_normalized
70+
if np.any(w1 < - self.EVAL_TOL) or np.any(w2 < -self.EVAL_TOL):
71+
return np.inf
72+
else:
73+
w1[w1 < 0] = 0
74+
w2[w2 < 0] = 0
5875
r1 = -entropy(w1)
5976
r2 = u @ np.log(w2)
6077
return (r1 - r2)

cvxpy/atoms/von_neumann_entr.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,32 +25,37 @@
2525

2626
class von_neumann_entr(Atom):
2727
"""
28-
Computes the Von Neumann Entropy of the positive-definite matrix
29-
:math:`X\\in\\mathbb{S}^n_{+}`
28+
Represents the von Neumann Entropy of the positive-definite matrix :math:`X,`
3029
3130
.. math::
32-
-\\operatorname{tr}(X \\operatorname{logm}(X))
31+
-\\operatorname{tr}(X \\log X).
3332
34-
where :math:`\\operatorname{tr}` is the trace and
35-
:math:`\\operatorname{logm}` is the matrix logarithm
36-
37-
| May alternatively be expressed as:
33+
Mathematically, this is equivalent to
3834
3935
.. math::
4036
\\texttt{von_neumann_entr}(X) = -\\textstyle\\sum_{i=1}^n \\lambda_i \\log \\lambda_i
4137
42-
| where :math:`\\lambda_{i}` are the eigenvalues of :math:`X`
43-
This atom does not enforce :math:`\\operatorname{tr}(X) = 1`
44-
as is expected in applications from quantum mechanics.
38+
where :math:`\\lambda_{i}` are the eigenvalues of :math:`X.`
4539
4640
Parameters
4741
----------
4842
X : Expression or numeric
4943
A PSD matrix
44+
45+
quad_approx : Tuple[int,...]
46+
This is either an empty tuple (default) or a 2-tuple. If a 2-tuple, then this atom
47+
is approximately canonicalized. The approximations replace ExpCone constraints with SOC
48+
constraints based on a quadrature scheme from https://arxiv.org/abs/1705.00812;
49+
quad_approx[0] is the number of quadrature nodes and quad_approx[1] is the number of
50+
scaling points in the quadrature scheme.
51+
52+
Notes
53+
-----
54+
This function does not assume :math:`\\operatorname{tr}(X)=1,` which would be required
55+
for most uses of this function in the context of quantum information theory.
5056
"""
5157

52-
def __init__(self, X, quad_approx: Tuple[int, int] = ()) -> None:
53-
# TODO: add a check that N is symmetric/Hermitian.
58+
def __init__(self, X, quad_approx: Tuple[int, ...] = ()) -> None:
5459
self.quad_approx = quad_approx
5560
super(von_neumann_entr, self).__init__(X)
5661

@@ -63,10 +68,14 @@ def numeric(self, values):
6368
return val
6469

6570
def validate_arguments(self) -> None:
66-
"""Verify that the argument is a square matrix."""
67-
if not self.args[0].ndim == 2 or self.args[0].shape[0] != self.args[0].shape[1]:
71+
"""Verify that the argument is Hermitian."""
72+
if not self.args[0].is_hermitian():
6873
raise ValueError(
69-
f"The argument {self.args[0].name()} to von_neumann_entr must be a 2-d square array"
74+
f"""
75+
The argument {self.args[0].name()} to von_neumann_entr must be a Hermitian matrix.
76+
If you know for a fact that the input is Hermitian, wrap it with the hermitian_wrap
77+
atom before calling von_neumann_entr.
78+
"""
7079
)
7180

7281
def sign_from_args(self) -> Tuple[bool, bool]:
@@ -118,7 +127,7 @@ def _grad(self, values):
118127
# derivative = 2*(L + L * logm(np.dot(L.T, L)))
119128
# TODO: have to wrap derivative around scipy CSC sparse matrices
120129
# compare to log_det atom.
121-
raise ValueError()
130+
raise NotImplementedError()
122131

123132
def _domain(self) -> List[Constraint]:
124133
"""Returns constraints describing the domain of the node.

cvxpy/constraints/exponential.py

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ class ExpCone(Cone):
5858

5959
def __init__(self, x: Expression, y: Expression, z: Expression, constr_id=None) -> None:
6060
Expression = cvxtypes.expression()
61-
self.x = Expression.cast_to_const(x)
62-
self.y = Expression.cast_to_const(y)
63-
self.z = Expression.cast_to_const(z)
61+
self.x = Expression.cast(x)
62+
self.y = Expression.cast(y)
63+
self.z = Expression.cast(z)
6464
args = [self.x, self.y, self.z]
6565
for val in args:
6666
if not (val.is_affine() and val.is_real()):
@@ -163,19 +163,15 @@ def _dual_cone(self, *args):
163163

164164

165165
class RelEntrConeQuad(Cone):
166-
"""An approximate construction of the scalar relative entropy cone
167-
168-
Definition:
166+
"""An approximation of the scalar relative entropy cone,
169167
170168
.. math::
171169
172170
K_{re}=\\text{cl}\\{(x,y,z)\\in\\mathbb{R}_{++}\\times
173-
\\mathbb{R}_{++}\\times\\mathbb{R}_{++}\\:x\\log(x/y)\\leq z\\}
174-
175-
Since the above definition is very similar to the ExpCone, we provide a conversion method.
171+
\\mathbb{R}_{++}\\times\\mathbb{R}_{++}\\:x\\log(x/y)\\leq z\\},
176172
177-
More details on the approximation can be found in Theorem-3 on page-10 in the paper:
178-
Semidefinite Approximations of the Matrix Logarithm.
173+
in terms of second order cones. The approximation uses a numerical quadrature scheme
174+
described in https://arxiv.org/abs/1705.00812.
179175
180176
Parameters
181177
----------
@@ -185,17 +181,18 @@ class RelEntrConeQuad(Cone):
185181
y in the (approximate) scalar relative entropy cone
186182
z : Expression
187183
z in the (approximate) scalar relative entropy cone
188-
m: Parameter directly related to the number of generated nodes for the quadrature
189-
approximation used in the algorithm
190-
k: Another parameter controlling the approximation
184+
m : int
185+
Number of quadrature points in the approximation.
186+
k: int
187+
Number of scaling points in the approximation.
191188
"""
192189

193190
def __init__(self, x: Expression, y: Expression, z: Expression,
194191
m: int, k: int, constr_id=None) -> None:
195192
Expression = cvxtypes.expression()
196-
self.x = Expression.cast_to_const(x)
197-
self.y = Expression.cast_to_const(y)
198-
self.z = Expression.cast_to_const(z)
193+
self.x = Expression.cast(x)
194+
self.y = Expression.cast(y)
195+
self.z = Expression.cast(z)
199196
args = [self.x, self.y, self.z]
200197
for val in args:
201198
if not (val.is_affine() and val.is_real()):
@@ -281,17 +278,15 @@ def save_dual_value(self, value) -> None:
281278

282279

283280
class OpRelEntrConeQuad(Cone):
284-
"""An approximate construction of the operator relative entropy cone
285-
286-
Definition:
281+
"""An approximate construction of the operator relative entropy cone,
287282
288283
.. math::
289284
290-
K_{re}^n=\\text{cl}\\{(X,Y,T)\\in\\mathbb{H}^n_{++}\\times
291-
\\mathbb{H}^n_{++}\\times\\mathbb{H}^n_{++}\\:D_{\\text{op}}\\succeq T\\}
285+
K_{re}^n = \\text{cl}\\{(X,Y,T)\\in\\mathbb{H}^n_{++}\\times
286+
\\mathbb{H}^n_{++}\\times\\mathbb{H}^n_{++}\\:D_{\\text{op}}(X,Y) \\succeq T\\}.
292287
293-
More details on the approximation can be found in Theorem-3 on page-10 in the paper:
294-
Semidefinite Approximations of the Matrix Logarithm.
288+
Details on the approximation can be found in Theorem-3 on page-10 of
289+
https://arxiv.org/abs/1705.00812.
295290
296291
Parameters
297292
----------
@@ -317,9 +312,9 @@ class OpRelEntrConeQuad(Cone):
317312
def __init__(self, X: Expression, Y: Expression, Z: Expression,
318313
m: int, k: int, constr_id=None) -> None:
319314
Expression = cvxtypes.expression()
320-
self.X = Expression.cast_to_const(X)
321-
self.Y = Expression.cast_to_const(Y)
322-
self.Z = Expression.cast_to_const(Z)
315+
self.X = Expression.cast(X)
316+
self.Y = Expression.cast(Y)
317+
self.Z = Expression.cast(Z)
323318
if (not X.is_hermitian()) or (not Y.is_hermitian()) or (not Z.is_hermitian()):
324319
msg = ("One of the input matrices has not explicitly been declared as symmetric or"
325320
"Hermitian. If the inputs are Variable objects, try declaring them with the"

cvxpy/expressions/expression.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,15 @@ def __rpow__(self, base: float) -> "Expression":
564564
" identity that a**x = cp.exp(cp.multiply(np"
565565
".log(a), x)).")
566566

567+
@staticmethod
568+
def cast(expr_like) -> "Expression":
569+
"""
570+
If expr_like is an Expression, return it. Otherwise, cast expr_like to a Constant.
571+
572+
This is a wrapper around the misleadingly-named `Expression.cast_to_const` function.
573+
"""
574+
return Expression.cast_to_const(expr_like)
575+
567576
# Arithmetic operators.
568577
@staticmethod
569578
def cast_to_const(expr: "Expression"):

cvxpy/tests/test_quantum_rel_entr.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from cvxpy.tests import solver_test_helpers as STH
1212

1313

14-
def applychan(chan: np.array, rho: cp.Variable, rep: str, dim: tuple[int, int]):
14+
def applychan(chan: np.ndarray, rho: cp.Variable, rep: str, dim: tuple[int, int]):
1515
dimA, dimB, dimE = None, None, None
1616
if rep == 'choi2':
1717
dimA, dimB = dim
@@ -25,14 +25,6 @@ def applychan(chan: np.array, rho: cp.Variable, rep: str, dim: tuple[int, int]):
2525
rho_out = partial_trace(chan @ rho @ chan.conj().T, [dimB, dimE], 1)
2626
return rho_out
2727

28-
def randH(n: int):
29-
A = np.random.randn(n, n) + 1j * np.random.randn(n, n)
30-
return (A + A.conj().T)/2
31-
32-
def randRho(n: int):
33-
p = 10 * randH(n)
34-
p = (p @ p.conj().T)/np.trace(p @ p.conj().T)
35-
return p
3628

3729
class TestQuantumRelEntr:
3830
"""

cvxpy/tests/test_von_neumann_entr.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,9 @@ def make_test_4():
262262
p2_expect = 0.5
263263
var_pairs = [(p1, p1_expect), (p2, p2_expect)]
264264

265-
obj = cp.Maximize((cp.von_neumann_entr(p1 * rho1 + p2 * rho2) - p1 * H1 - \
266-
p2 * H2)/np.log(2))
265+
vne_arg = cp.hermitian_wrap(p1 * rho1 + p2 * rho2)
266+
obj_arg = (cp.von_neumann_entr(vne_arg) - p1 * H1 - p2 * H2)/np.log(2)
267+
obj = cp.Maximize(obj_arg)
267268
obj_expect = 0.60088
268269
obj_pair = (obj, obj_expect)
269270

0 commit comments

Comments
 (0)