Coverage for picos/expressions/algebra.py: 85.80%
345 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-12 07:53 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-12 07:53 +0000
1# ------------------------------------------------------------------------------
2# Copyright (C) 2019-2020 Maximilian Stahlberg
3# Based in parts on the tools module by Guillaume Sagnol.
4#
5# This file is part of PICOS.
6#
7# PICOS is free software: you can redistribute it and/or modify it under the
8# terms of the GNU General Public License as published by the Free Software
9# Foundation, either version 3 of the License, or (at your option) any later
10# version.
11#
12# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
13# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# ------------------------------------------------------------------------------
20"""Implements functions that create or modify algebraic expressions."""
22import builtins
23import functools
24import itertools
25from collections.abc import Iterator
27import cvxopt
28import numpy
30from .. import glyphs
31from ..apidoc import api_end, api_start
32from ..formatting import arguments
33from ..legacy import deprecated, throw_deprecation_warning
34from .cone_expcone import ExponentialCone
35from .cone_rsoc import RotatedSecondOrderCone
36from .cone_soc import SecondOrderCone
37from .data import convert_and_refine_arguments, cvxopt_vcat, load_data
38from .exp_affine import AffineExpression, BiaffineExpression, Constant
39from .exp_detrootn import DetRootN
40from .exp_entropy import NegativeEntropy
41from .exp_extremum import MaximumConvex, MinimumConcave
42from .exp_geomean import GeometricMean
43from .exp_logsumexp import LogSumExp
44from .exp_norm import Norm
45from .exp_powtrace import PowerTrace
46from .exp_quantcondentr import QuantumConditionalEntropy
47from .exp_quantentr import QuantumEntropy, NegativeQuantumEntropy
48from .exp_quantkeydist import QuantumKeyDistribution
49from .exp_renyientr import (RenyiEntropy, QuasiEntropy, SandRenyiEntropy,
50 SandQuasiEntropy)
51from .exp_oprelentr import OperatorRelativeEntropy
52from .exp_mtxgeomean import MatrixGeometricMean
53from .exp_sumexp import SumExponentials
54from .exp_sumxtr import SumExtremes
55from .expression import Expression
56from .set_ball import Ball
57from .set_simplex import Simplex
58from .uncertain.uexp_affine import UncertainAffineExpression
59from .uncertain.uexp_rand_pwl import RandomMaximumAffine, RandomMinimumAffine
61_API_START = api_start(globals())
62# -------------------------------
65# ------------------------------------------------------------------------------
66# Algebraic functions with a logic of their own.
67# ------------------------------------------------------------------------------
70@functools.lru_cache()
71def O(rows=1, cols=1): # noqa
72 """Create a zero matrix.
74 :Example:
76 >>> from picos import O
77 >>> print(O(2, 3))
78 [0 0 0]
79 [0 0 0]
80 """
81 return AffineExpression.zero((rows, cols))
84@functools.lru_cache()
85def I(size=1): # noqa
86 """Create an identity matrix.
88 :Example:
90 >>> from picos import I
91 >>> print(I(3))
92 [ 1.00e+00 0 0 ]
93 [ 0 1.00e+00 0 ]
94 [ 0 0 1.00e+00]
95 """
96 return AffineExpression.from_constant("I", (size, size))
99@functools.lru_cache()
100def J(rows=1, cols=1):
101 """Create a matrix of all ones.
103 :Example:
105 >>> from picos import J
106 >>> print(J(2, 3))
107 [ 1.00e+00 1.00e+00 1.00e+00]
108 [ 1.00e+00 1.00e+00 1.00e+00]
109 """
110 return AffineExpression.from_constant(1, (rows, cols))
113def sum(lst, axis=None):
114 """Sum PICOS expressions and give the result a meaningful description.
116 This is a replacement for Python's :func:`sum` that produces sensible string
117 representations, and in some cases a speedup, when summing over multiple
118 PICOS expressions. Additionally, this can be used to denote the (complete,
119 column- or row-) sum over a single matrix expression.
121 :param None or int axis:
122 If summing over a single matrix expression, this is the axis over which
123 the sum is performed: :obj:`None` denotes the sum over all elements,
124 ``0`` denotes the sum of the rows as a row vector and ``1`` denotes the
125 sum of the columns as a column vector. If summing multiple expressions,
126 any value other than :obj:`None` raises a :exc:`ValueError`.
128 :Example:
130 >>> import builtins, picos, numpy
131 >>> x = picos.RealVariable("x", 5)
132 >>> e = [x[i]*x[i+1] for i in range(len(x) - 1)]
133 >>> builtins.sum(e)
134 <Quadratic Expression: x[0]·x[1] + x[1]·x[2] + x[2]·x[3] + x[3]·x[4]>
135 >>> picos.sum(e)
136 <Quadratic Expression: ∑(x[i]·x[i+1] : i ∈ [0…3])>
137 >>> picos.sum(x) # The same as x.sum or (1|x).
138 <1×1 Real Linear Expression: ∑(x)>
139 >>> A = picos.Constant("A", range(20), (4, 5))
140 >>> picos.sum(A, axis=0) # The same as A.rowsum
141 <1×5 Real Constant: [1]ᵀ·A>
142 >>> picos.sum(A, axis=1) # The same as A.colsum
143 <4×1 Real Constant: A·[1]>
144 >>> numpy.allclose( # Same axis convention as NumPy.
145 ... numpy.sum(A.np, axis=0), picos.sum(A, axis=0).np)
146 True
147 """
148 if isinstance(lst, Expression):
149 if isinstance(lst, BiaffineExpression):
150 if axis is None:
151 return lst.sum
152 elif not isinstance(axis, (int, numpy.integer)):
153 raise TypeError(
154 "Axis must be an integer or None. (To sum multiple "
155 "expressions, provide an iterable as the first argument.)")
156 elif axis == 0:
157 return lst.rowsum
158 elif axis == 1:
159 return lst.colsum
160 else:
161 raise ValueError("Bad axis: {}.".format(axis))
162 else:
163 raise TypeError(
164 "PICOS doesn't know how to sum over a single {}."
165 .format(type(lst).__name__))
167 if axis is not None:
168 raise ValueError(
169 "No axis may be given when summing multiple expressions.")
171 # Allow passing also an iterator instead of an iterable. (The conversion is
172 # necessary as otherwise the check below would use up the iterator.)
173 if isinstance(lst, Iterator):
174 lst = tuple(lst)
176 # Resort to Python's built-in sum when no summand is a PICOS expression.
177 if not any(isinstance(expression, Expression) for expression in lst):
178 return builtins.sum(lst)
180 # If at least one summand is a PICOS expression, attempt to convert others.
181 try:
182 lst = [Constant(x) if not isinstance(x, Expression) else x for x in lst]
183 except Exception as error:
184 raise TypeError("Failed to convert some non-expression argument to a "
185 "PICOS constant.") from error
187 # Handle sums with at most two summands.
188 if len(lst) == 0:
189 return O()
190 elif len(lst) == 1:
191 return lst[0]
192 elif len(lst) == 2:
193 return lst[0] + lst[1]
195 # Find a suitable string description.
196 string = glyphs.sum(arguments([exp.string for exp in lst]))
198 # Handle (large) sums of only affine expressions efficiently.
199 if all(isinstance(expression, BiaffineExpression) for expression in lst):
200 # Determine resulting shape.
201 shapes = set(expression.shape for expression in lst)
202 if len(shapes) != 1:
203 raise TypeError("The shapes of summands do not match.")
204 else:
205 shape = shapes.pop()
207 # Determine resulting type.
208 try:
209 basetype = functools.reduce(
210 lambda A, B: A._common_basetype(B), lst, AffineExpression)
211 except Exception as error:
212 raise TypeError("Could not find a common base type for the given "
213 "(bi-)affine expressions.") from error
215 # Sum all coefficient matrices.
216 # NOTE: BiaffineExpression.__init__ will order mutable pairs and merge
217 # their coefficient matrices.
218 coefs = {}
219 byref = set()
220 for expression in lst:
221 for mtbs, coef in expression._coefs.items():
222 if mtbs in coefs:
223 if mtbs in byref:
224 # Make a copy of the coefficient so we may modify it.
225 coefs[mtbs] = load_data(coefs[mtbs], alwaysCopy=True)[0]
226 byref.remove(mtbs)
228 try:
229 coefs[mtbs] += coef
230 except TypeError:
231 # No in-place addition for sparse and dense types.
232 coefs[mtbs] = coefs[mtbs] + coef
233 else:
234 # Store the first coefficient by reference.
235 coefs[mtbs] = coef
236 byref.add(mtbs)
238 return basetype(string, shape, coefs)
240 theSum = lst[0]
241 for expression in lst[1:]:
242 theSum += expression
244 theSum._symbStr = string
246 return theSum
249def block(nested, shapes=None, name=None):
250 """Create an affine block matrix expression.
252 Given a two-level nested iterable container (e.g. a list of lists) of PICOS
253 affine expressions or constant data values or a mix thereof, this creates an
254 affine block matrix where each inner container represents one block row and
255 each expression or constant represents one block.
257 Blocks that are given as PICOS expressions are never reshaped or
258 broadcasted. Their shapes must already be consistent. Blocks that are given
259 as constant data values are reshaped or broadcasted as necessary **to match
260 existing PICOS expressions**. This means you can specify blocks as e.g.
261 ``"I"`` or ``0`` and PICOS will load them as matrices with the smallest
262 shape that is consistent with other blocks given as PICOS expressions.
264 Since constant data values are not reshaped or broadcasted with respect to
265 each other, the ``shapes`` parameter allows a manual clarification of block
266 shapes. It must be consistent with the shapes of blocks given as PICOS
267 expressions (they are still not reshaped or broadcasted).
269 :param nested:
270 The blocks.
271 :type nested:
272 tuple(tuple) or list(list)
274 :param shapes:
275 A pair ``(rows, columns)`` where ``rows`` defines the number of rows for
276 each block row and ``columns`` defines the number of columns for each
277 block column. You can put a ``0`` or :obj:`None` as a wildcard.
278 :type shapes:
279 tuple(tuple) or list(list)
281 :param str name:
282 Name or string description of the resulting block matrix. If
283 :obj:`None`, a descriptive string will be generated.
285 :Example:
287 >>> from picos import block, Constant, RealVariable
288 >>> C = Constant("C", range(6), (3, 2))
289 >>> d = Constant("d", 0.5, 2)
290 >>> x = RealVariable("x", 3)
291 >>> A = block([[ C, x ],
292 ... ["I", d ]]); A
293 <5×3 Real Affine Expression: [C, x; I, d]>
294 >>> x.value = [60, 70, 80]
295 >>> print(A)
296 [ 0.00e+00 3.00e+00 6.00e+01]
297 [ 1.00e+00 4.00e+00 7.00e+01]
298 [ 2.00e+00 5.00e+00 8.00e+01]
299 [ 1.00e+00 0.00e+00 5.00e-01]
300 [ 0.00e+00 1.00e+00 5.00e-01]
301 >>> B = block([[ C, x ], # With a shape hint.
302 ... ["I", 0 ]], shapes=((3, 2), (2, 1))); B
303 <5×3 Real Affine Expression: [C, x; I, 0]>
304 >>> print(B)
305 [ 0.00e+00 3.00e+00 6.00e+01]
306 [ 1.00e+00 4.00e+00 7.00e+01]
307 [ 2.00e+00 5.00e+00 8.00e+01]
308 [ 1.00e+00 0.00e+00 0.00e+00]
309 [ 0.00e+00 1.00e+00 0.00e+00]
310 """
311 # In a first stage, determine and validate fixed shapes from PICOS
312 # expressions, then load the remaining data to be consistent with the fixed
313 # shapes. In a second stage, validate also the shapes of the loaded data.
314 for stage in range(2):
315 R = numpy.array([[ # The row count for each block.
316 x.shape[0] if isinstance(x, BiaffineExpression) else 0 for x in row]
317 for row in nested], dtype=int)
319 M = [] # The row count for each block row.
320 for i, Ri in enumerate(R):
321 m = set(int(x) for x in Ri[numpy.nonzero(Ri)])
323 if shapes and shapes[0][i]:
324 m.add(shapes[0][i])
326 if len(m) > 1:
327 raise TypeError(
328 "Inconsistent number of rows in block row {}: {}."
329 .format(i + 1, m))
330 elif len(m) == 1:
331 M.append(int(m.pop()))
332 else:
333 assert stage == 0, "All blocks should have a shape by now."
334 M.append(None)
336 C = numpy.array([[ # The column count for each block.
337 x.shape[1] if isinstance(x, BiaffineExpression) else 0 for x in row]
338 for row in nested], dtype=int)
340 N = [] # The column count for each block column.
341 for j, Cj in enumerate(C.T):
342 n = set(int(x) for x in Cj[numpy.nonzero(Cj)])
344 if shapes and shapes[1][j]:
345 n.add(shapes[1][j])
347 if len(n) > 1:
348 raise TypeError(
349 "Inconsistent number of columns in block column {}: {}."
350 .format(j + 1, n))
351 elif len(n) == 1:
352 N.append(n.pop())
353 else:
354 assert stage == 0, "All blocks should have a shape by now."
355 N.append(None)
357 if stage == 0:
358 nested = [[
359 x if isinstance(x, BiaffineExpression)
360 else Constant(x, shape=(M[i], N[j]))
361 for j, x in enumerate(row)] for i, row in enumerate(nested)]
363 # List the blocks in block-row-major order.
364 blocks = [block for blockRow in nested for block in blockRow]
366 # Find the common base type of all expressions.
367 basetype = functools.reduce(BiaffineExpression._common_basetype.__func__,
368 (block.__class__ for block in blocks), AffineExpression)
369 typecode = basetype._get_typecode()
371 # Allow constant time random access to the block dimensions.
372 M, N = tuple(M), tuple(N)
374 # Compute the row (column) offsets for each block row (block column).
375 MOffsets = tuple(int(x) for x in numpy.cumsum((0,) + M))
376 NOffsets = tuple(int(x) for x in numpy.cumsum((0,) + N))
378 # Compute the full matrix dimensions.
379 m = builtins.sum(M)
380 n = builtins.sum(N)
381 mn = m*n
383 # Compute row and column offsets for each block in block-row-major-order.
384 MLen = len(N)
385 blockIndices = tuple(divmod(k, MLen) for k in range(len(blocks)))
386 blockOffsets = tuple(
387 (MOffsets[blockIndices[k][0]], NOffsets[blockIndices[k][1]])
388 for k in range(len(blocks)))
390 # Helper function to compute the matrix T (see below).
391 def _I():
392 for k, block in enumerate(blocks):
393 rows, cols = block.shape
394 i, j = blockOffsets[k]
395 for c in range(cols):
396 columnOffset = (j + c)*m + i
397 yield range(columnOffset, columnOffset + rows)
399 # Compute a sparse linear operator matrix T that transforms the stacked
400 # column-major vectorizations of the blocks in block-row-major order to the
401 # column-major vectorization of the full matrix.
402 V = tuple(itertools.repeat(1, mn))
403 I = tuple(itertools.chain(*_I()))
404 J = range(mn)
405 T = cvxopt.spmatrix(V, I, J, (mn, mn), typecode)
407 # Obtain all coefficient keys.
408 keys = set(key for block in blocks for key in block._coefs.keys())
410 # Stack all coefficient matrices in block-row-major order and apply T.
411 coefs = {}
412 for mtbs in keys:
413 dim = functools.reduce(lambda x, y:
414 (x if isinstance(x, int) else x.dim)*y.dim, mtbs, 1)
416 coefs[mtbs] = T*cvxopt_vcat([
417 block._coefs[mtbs] if mtbs in block._coefs
418 else cvxopt.spmatrix([], [], [], (len(block), dim), typecode)
419 for block in blocks])
421 # Build the string description.
422 if name:
423 string = str(name)
424 elif len(blocks) > 9:
425 string = glyphs.matrix(glyphs.shape((m, n)))
426 else:
427 string = functools.reduce(
428 lambda x, y: glyphs.matrix_cat(x, y, False),
429 (functools.reduce(
430 lambda x, y: glyphs.matrix_cat(x, y, True),
431 (block.string for block in blockRow))
432 for blockRow in nested))
434 return basetype(string, (m, n), coefs)
437def max(lst):
438 """Denote the maximum over a collection of convex scalar expressions.
440 If instead of a collection of expressions only a single multidimensional
441 affine expression is given, this denotes its largest element instead.
443 If some individual expressions are uncertain and their uncertainty is not of
444 stochastic but of worst-case nature (robust optimization), then the maximum
445 implicitly goes over their perturbation parameters as well.
447 :param lst:
448 A list of convex expressions or a single affine expression.
449 :type lst:
450 list or tuple or ~picos.expressions.AffineExpression
452 :Example:
454 >>> from picos import RealVariable, max, sum
455 >>> x = RealVariable("x", 5)
456 >>> max(x)
457 <Largest Element: max(x)>
458 >>> max(x) <= 2 # The same as x <= 2.
459 <Largest Element Constraint: max(x) ≤ 2>
460 >>> max([sum(x), abs(x)])
461 <Maximum of Convex Functions: max(∑(x), ‖x‖)>
462 >>> max([sum(x), abs(x)]) <= 2 # Both must be <= 2.
463 <Maximum of Convex Functions Constraint: max(∑(x), ‖x‖) ≤ 2>
464 >>> from picos.uncertain import UnitBallPerturbationSet
465 >>> z = UnitBallPerturbationSet("z", 5).parameter
466 >>> max([sum(x), x.T*z]) # Also maximize over z.
467 <Maximum of Convex Functions: max(∑(x), max_z xᵀ·z)>
468 """
469 UAE = UncertainAffineExpression
471 if isinstance(lst, AffineExpression):
472 return SumExtremes(lst, 1, largest=True, eigenvalues=False)
473 elif isinstance(lst, Expression):
474 raise TypeError("May only denote the maximum of a single affine "
475 "expression or of multiple (convex) expressions.")
477 try:
478 lst = [Constant(x) if not isinstance(x, Expression) else x for x in lst]
479 except Exception as error:
480 raise TypeError("Failed to convert some non-expression argument to a "
481 "PICOS constant.") from error
483 if any(isinstance(x, UAE) for x in lst) \
484 and all(isinstance(x, (AffineExpression, UAE)) for x in lst) \
485 and all(x.certain or x.universe.distributional for x in lst):
486 return RandomMaximumAffine(lst)
487 else:
488 return MaximumConvex(lst)
491def min(lst):
492 """Denote the minimum over a collection of concave scalar expressions.
494 If instead of a collection of expressions only a single multidimensional
495 affine expression is given, this denotes its smallest element instead.
497 If some individual expressions are uncertain and their uncertainty is not of
498 stochastic but of worst-case nature (robust optimization), then the minimum
499 implicitly goes over their perturbation parameters as well.
501 :param lst:
502 A list of concave expressions or a single affine expression.
503 :type lst:
504 list or tuple or ~picos.expressions.AffineExpression
506 :Example:
508 >>> from picos import RealVariable, min, sum
509 >>> x = RealVariable("x", 5)
510 >>> min(x)
511 <Smallest Element: min(x)>
512 >>> min(x) >= 2 # The same as x >= 2.
513 <Smallest Element Constraint: min(x) ≥ 2>
514 >>> min([sum(x), -x[0]**2])
515 <Minimum of Concave Functions: min(∑(x), -x[0]²)>
516 >>> min([sum(x), -x[0]**2]) >= 2 # Both must be >= 2.
517 <Minimum of Concave Functions Constraint: min(∑(x), -x[0]²) ≥ 2>
518 >>> from picos.uncertain import UnitBallPerturbationSet
519 >>> z = UnitBallPerturbationSet("z", 5).parameter
520 >>> min([sum(x), x.T*z]) # Also minimize over z.
521 <Minimum of Concave Functions: min(∑(x), min_z xᵀ·z)>
522 """
523 UAE = UncertainAffineExpression
525 if isinstance(lst, AffineExpression):
526 return SumExtremes(lst, 1, largest=False, eigenvalues=False)
527 elif isinstance(lst, Expression):
528 raise TypeError("May only denote the minimum of a single affine "
529 "expression or of multiple (concave) expressions.")
531 try:
532 lst = [Constant(x) if not isinstance(x, Expression) else x for x in lst]
533 except Exception as error:
534 raise TypeError("Failed to convert some non-expression argument to a "
535 "PICOS constant.") from error
537 if any(isinstance(x, UAE) for x in lst) \
538 and all(isinstance(x, (AffineExpression, UAE)) for x in lst) \
539 and all(x.certain or x.universe.distributional for x in lst):
540 return RandomMinimumAffine(lst)
541 else:
542 return MinimumConcave(lst)
545# ------------------------------------------------------------------------------
546# Functions that call expression methods.
547# ------------------------------------------------------------------------------
550def _error_on_none(func):
551 """Raise a :exc:`TypeError` if the function returns :obj:`None`."""
552 @functools.wraps(func)
553 def wrapper(*args, **kwargs):
554 result = func(*args, **kwargs)
556 if result is None:
557 raise TypeError("PICOS does not have a representation for {}({})."
558 .format(func.__qualname__ if hasattr(func, "__qualname__") else
559 func.__name__, ", ".join([type(x).__name__ for x in args] +
560 ["{}={}".format(k, type(x).__name__) for k, x in kwargs.items()]
561 )))
563 return result
564 return wrapper
567@_error_on_none
568@convert_and_refine_arguments("x")
569def exp(x):
570 """Denote the exponential."""
571 if hasattr(x, "exp"):
572 return x.exp
575@_error_on_none
576@convert_and_refine_arguments("x")
577def log(x):
578 """Denote the natural logarithm."""
579 if hasattr(x, "log"):
580 return x.log
583@deprecated("2.2", "Ensure that one operand is a PICOS expression and use infix"
584 " @ instead.")
585@_error_on_none
586@convert_and_refine_arguments("x", "y")
587def kron(x, y):
588 """Denote the kronecker product."""
589 if hasattr(x, "kron"):
590 return x @ y
593@_error_on_none
594@convert_and_refine_arguments("x")
595def diag(x, n=1):
596 r"""Form a diagonal matrix from the column-major vectorization of :math:`x`.
598 If :math:`n \neq 1`, then the vectorization is repeated :math:`n` times.
599 """
600 if hasattr(x, "diag") and n == 1:
601 return x.diag
602 elif hasattr(x, "dupdiag"):
603 return x.dupdiag(n)
606@_error_on_none
607@convert_and_refine_arguments("x")
608def maindiag(x):
609 """Extract the diagonal of :math:`x` as a column vector."""
610 if hasattr(x, "maindiag"):
611 return x.maindiag
614@_error_on_none
615@convert_and_refine_arguments("x")
616def trace(x):
617 """Denote the trace of a square matrix."""
618 if hasattr(x, "tr"):
619 return x.tr
622@_error_on_none
623@convert_and_refine_arguments("x")
624def partial_trace(x, subsystems=0, dimensions=2, k=None, dim=None):
625 """See :meth:`.exp_biaffine.BiaffineExpression.partial_trace`.
627 The parameters `k` and `dim` are for backwards compatibility.
628 """
629 if k is not None:
630 throw_deprecation_warning("Argument 'k' to partial_trace is "
631 "deprecated: Use 'subsystems' instead.", decoratorLevel=2)
632 subsystems = k
634 if dim is not None:
635 throw_deprecation_warning("Argument 'dim' to partial_trace is "
636 "deprecated: Use 'dimensions' instead.", decoratorLevel=2)
637 dimensions = dim
639 if isinstance(x, BiaffineExpression):
640 return x.partial_trace(subsystems, dimensions)
643@_error_on_none
644@convert_and_refine_arguments("x")
645def partial_transpose(x, subsystems=0, dimensions=2, k=None, dim=None):
646 """See :meth:`.exp_biaffine.BiaffineExpression.partial_transpose`.
648 The parameters `k` and `dim` are for backwards compatibility.
649 """
650 if k is not None:
651 throw_deprecation_warning("Argument 'k' to partial_transpose is "
652 "deprecated: Use 'subsystems' instead.", decoratorLevel=2)
653 subsystems = k
655 if dim is not None:
656 throw_deprecation_warning("Argument 'dim' to partial_transpose is "
657 "deprecated: Use 'dimensions' instead.", decoratorLevel=2)
658 dimensions = dim
660 if isinstance(x, BiaffineExpression):
661 return x.partial_transpose(subsystems, dimensions)
664# ------------------------------------------------------------------------------
665# Alias functions for expression classes meant to be instanciated by the user.
666# ------------------------------------------------------------------------------
669def _shorthand(name, cls):
670 def shorthand(*args, **kwargs):
671 return cls(*args, **kwargs)
673 shorthand.__doc__ = "Shorthand for :class:`{1} <{0}.{1}>`.".format(
674 cls.__module__, cls.__qualname__)
675 shorthand.__name__ = name
676 shorthand.__qualname__ = name
678 return shorthand
681expcone = _shorthand("expcone", ExponentialCone)
682geomean = _shorthand("geomean", GeometricMean)
683kldiv = _shorthand("kldiv", NegativeEntropy)
684lse = _shorthand("lse", LogSumExp)
685rsoc = _shorthand("rsoc", RotatedSecondOrderCone)
686soc = _shorthand("soc", SecondOrderCone)
687sumexp = _shorthand("sumexp", SumExponentials)
689quantentr = _shorthand("quantentr", QuantumEntropy)
690quantrelentr = _shorthand("quantrelentr", NegativeQuantumEntropy)
691quantcondentr = _shorthand("quantcondentr", QuantumConditionalEntropy)
692quantkeydist = _shorthand("quantkeydist", QuantumKeyDistribution)
693oprelentr = _shorthand("oprelentr", OperatorRelativeEntropy)
694mtxgeomean = _shorthand("oprelentr", MatrixGeometricMean)
696renyientr = _shorthand("renyientr", RenyiEntropy)
697quasientr = _shorthand("quasientr", QuasiEntropy)
698sandrenyientr = _shorthand("sandrenyientr", SandRenyiEntropy)
699sandquasientr = _shorthand("sandquasientr", SandQuasiEntropy)
701def sum_k_largest(x, k):
702 """Wrapper for :class:`~picos.SumExtremes`.
704 Sets ``largest = True`` and ``eigenvalues = False``.
706 :Example:
708 >>> from picos import RealVariable, sum_k_largest
709 >>> x = RealVariable("x", 5)
710 >>> sum_k_largest(x, 2)
711 <Sum of Largest Elements: sum_2_largest(x)>
712 >>> sum_k_largest(x, 2) <= 2
713 <Sum of Largest Elements Constraint: sum_2_largest(x) ≤ 2>
714 """
715 return SumExtremes(x, k, largest=True, eigenvalues=False)
718def sum_k_smallest(x, k):
719 """Wrapper for :class:`~picos.SumExtremes`.
721 Sets ``largest = False`` and ``eigenvalues = False``.
723 :Example:
725 >>> from picos import RealVariable, sum_k_smallest
726 >>> x = RealVariable("x", 5)
727 >>> sum_k_smallest(x, 2)
728 <Sum of Smallest Elements: sum_2_smallest(x)>
729 >>> sum_k_smallest(x, 2) >= 2
730 <Sum of Smallest Elements Constraint: sum_2_smallest(x) ≥ 2>
731 """
732 return SumExtremes(x, k, largest=False, eigenvalues=False)
735def lambda_max(x):
736 """Wrapper for :class:`~picos.SumExtremes`.
738 Sets ``k = 1``, ``largest = True`` and ``eigenvalues = True``.
740 :Example:
742 >>> from picos import SymmetricVariable, lambda_max
743 >>> X = SymmetricVariable("X", 5)
744 >>> lambda_max(X)
745 <Largest Eigenvalue: λ_max(X)>
746 >>> lambda_max(X) <= 2
747 <Largest Eigenvalue Constraint: λ_max(X) ≤ 2>
748 """
749 return SumExtremes(x, 1, largest=True, eigenvalues=True)
752def lambda_min(x):
753 """Wrapper for :class:`~picos.SumExtremes`.
755 Sets ``k = 1``, ``largest = False`` and ``eigenvalues = True``.
757 :Example:
759 >>> from picos import SymmetricVariable, lambda_min
760 >>> X = SymmetricVariable("X", 5)
761 >>> lambda_min(X)
762 <Smallest Eigenvalue: λ_min(X)>
763 >>> lambda_min(X) >= 2
764 <Smallest Eigenvalue Constraint: λ_min(X) ≥ 2>
765 """
766 return SumExtremes(x, 1, largest=False, eigenvalues=True)
769def sum_k_largest_lambda(x, k):
770 """Wrapper for :class:`~picos.SumExtremes`.
772 Sets ``largest = True`` and ``eigenvalues = True``.
774 :Example:
776 >>> from picos import SymmetricVariable, sum_k_largest_lambda
777 >>> X = SymmetricVariable("X", 5)
778 >>> sum_k_largest_lambda(X, 2)
779 <Sum of Largest Eigenvalues: sum_2_largest_λ(X)>
780 >>> sum_k_largest_lambda(X, 2) <= 2
781 <Sum of Largest Eigenvalues Constraint: sum_2_largest_λ(X) ≤ 2>
782 """
783 return SumExtremes(x, k, largest=True, eigenvalues=True)
786def sum_k_smallest_lambda(x, k):
787 """Wrapper for :class:`~picos.SumExtremes`.
789 Sets ``largest = False`` and ``eigenvalues = True``.
791 :Example:
793 >>> from picos import SymmetricVariable, sum_k_smallest_lambda
794 >>> X = SymmetricVariable("X", 5)
795 >>> sum_k_smallest_lambda(X, 2)
796 <Sum of Smallest Eigenvalues: sum_2_smallest_λ(X)>
797 >>> sum_k_smallest_lambda(X, 2) >= 2
798 <Sum of Smallest Eigenvalues Constraint: sum_2_smallest_λ(X) ≥ 2>
799 """
800 return SumExtremes(x, k, largest=False, eigenvalues=True)
803# ------------------------------------------------------------------------------
804# Legacy algebraic functions for backwards compatibility.
805# ------------------------------------------------------------------------------
808def _deprecated_shorthand(name, cls, new_shorthand=None):
809 uiRef = "picos.{}".format(new_shorthand if new_shorthand else cls.__name__)
811 # FIXME: Warning doesn't show the name of the deprecated shorthand function.
812 @deprecated("2.0", useInstead=uiRef)
813 def shorthand(*args, **kwargs):
814 """|PLACEHOLDER|""" # noqa
815 return cls(*args, **kwargs)
817 shorthand.__doc__ = shorthand.__doc__.replace("|PLACEHOLDER|",
818 "Legacy shorthand for :class:`{1} <{0}.{1}>`.".format(
819 cls.__module__, cls.__qualname__))
820 shorthand.__name__ = name
821 shorthand.__qualname__ = name
823 return shorthand
826ball = _deprecated_shorthand("ball", Ball)
827detrootn = _deprecated_shorthand("detrootn", DetRootN)
828kullback_leibler = _deprecated_shorthand(
829 "kullback_leibler", NegativeEntropy, "kldiv")
830logsumexp = _deprecated_shorthand("logsumexp", LogSumExp, "lse")
831norm = _deprecated_shorthand("norm", Norm)
834@deprecated("2.0", useInstead="picos.PowerTrace")
835def tracepow(exp, num=1, denom=1, coef=None):
836 """Legacy shorthand for :class:`~picos.PowerTrace`."""
837 return PowerTrace(exp, num / denom, coef)
840@deprecated("2.0", useInstead="picos.Constant")
841def new_param(name, value):
842 """Create a constant or a list or dict or tuple thereof."""
843 if isinstance(value, list):
844 # Handle a vector.
845 try:
846 for x in value:
847 complex(x)
848 except Exception:
849 pass
850 else:
851 return Constant(name, value)
853 # Handle a matrix.
854 if all(isinstance(x, list) for x in value) \
855 and all(len(x) == len(value[0]) for x in value):
856 try:
857 for x in value:
858 for y in x:
859 complex(y)
860 except Exception:
861 pass
862 else:
863 return Constant(name, value)
865 # Return a list of constants.
866 return [Constant(glyphs.slice(name, i), x) for i, x in enumerate(value)]
867 elif isinstance(value, tuple):
868 # Return a list of constants.
869 # NOTE: This is very inconsistent, but legacy behavior.
870 return [Constant(glyphs.slice(name, i), x) for i, x in enumerate(value)]
871 elif isinstance(value, dict):
872 return {k: Constant(glyphs.slice(name, k), x) for k, x in value.items()}
873 else:
874 return Constant(name, value)
877@deprecated("2.0", useInstead="picos.FlowConstraint")
878def flow_Constraint(*args, **kwargs):
879 """Legacy shorthand for :class:`~picos.FlowConstraint`."""
880 from ..constraints.con_flow import FlowConstraint
881 return FlowConstraint(*args, **kwargs)
884@deprecated("2.0", useInstead="picos.maindiag")
885def diag_vect(x):
886 """Extract the diagonal of :math:`x` as a column vector."""
887 return maindiag(x)
890@deprecated("2.0", useInstead="picos.Simplex")
891def simplex(gamma):
892 r"""Create a standard simplex of radius :math:`\gamma`."""
893 return Simplex(gamma, truncated=False, symmetrized=False)
896@deprecated("2.0", useInstead="picos.Simplex")
897def truncated_simplex(gamma, sym=False):
898 r"""Create a truncated simplex of radius :math:`\gamma`."""
899 return Simplex(gamma, truncated=True, symmetrized=sym)
902# --------------------------------------
903__all__ = api_end(_API_START, globals())