Coverage for picos/expressions/algebra.py: 82.73%
330 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-26 07:46 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-26 07:46 +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_sumexp import SumExponentials
47from .exp_sumxtr import SumExtremes
48from .expression import Expression
49from .set_ball import Ball
50from .set_simplex import Simplex
51from .uncertain.uexp_affine import UncertainAffineExpression
52from .uncertain.uexp_rand_pwl import RandomMaximumAffine, RandomMinimumAffine
54_API_START = api_start(globals())
55# -------------------------------
58# ------------------------------------------------------------------------------
59# Algebraic functions with a logic of their own.
60# ------------------------------------------------------------------------------
63@functools.lru_cache()
64def O(rows=1, cols=1): # noqa
65 """Create a zero matrix.
67 :Example:
69 >>> from picos import O
70 >>> print(O(2, 3))
71 [0 0 0]
72 [0 0 0]
73 """
74 return AffineExpression.zero((rows, cols))
77@functools.lru_cache()
78def I(size=1): # noqa
79 """Create an identity matrix.
81 :Example:
83 >>> from picos import I
84 >>> print(I(3))
85 [ 1.00e+00 0 0 ]
86 [ 0 1.00e+00 0 ]
87 [ 0 0 1.00e+00]
88 """
89 return AffineExpression.from_constant("I", (size, size))
92@functools.lru_cache()
93def J(rows=1, cols=1):
94 """Create a matrix of all ones.
96 :Example:
98 >>> from picos import J
99 >>> print(J(2, 3))
100 [ 1.00e+00 1.00e+00 1.00e+00]
101 [ 1.00e+00 1.00e+00 1.00e+00]
102 """
103 return AffineExpression.from_constant(1, (rows, cols))
106def sum(lst, axis=None):
107 """Sum PICOS expressions and give the result a meaningful description.
109 This is a replacement for Python's :func:`sum` that produces sensible string
110 representations, and in some cases a speedup, when summing over multiple
111 PICOS expressions. Additionally, this can be used to denote the (complete,
112 column- or row-) sum over a single matrix expression.
114 :param None or int axis:
115 If summing over a single matrix expression, this is the axis over which
116 the sum is performed: :obj:`None` denotes the sum over all elements,
117 ``0`` denotes the sum of the rows as a row vector and ``1`` denotes the
118 sum of the columns as a column vector. If summing multiple expressions,
119 any value other than :obj:`None` raises a :exc:`ValueError`.
121 :Example:
123 >>> import builtins, picos, numpy
124 >>> x = picos.RealVariable("x", 5)
125 >>> e = [x[i]*x[i+1] for i in range(len(x) - 1)]
126 >>> builtins.sum(e)
127 <Quadratic Expression: x[0]·x[1] + x[1]·x[2] + x[2]·x[3] + x[3]·x[4]>
128 >>> picos.sum(e)
129 <Quadratic Expression: ∑(x[i]·x[i+1] : i ∈ [0…3])>
130 >>> picos.sum(x) # The same as x.sum or (x|1).
131 <1×1 Real Linear Expression: ∑(x)>
132 >>> A = picos.Constant("A", range(20), (4, 5))
133 >>> picos.sum(A, axis=0) # The same as A.rowsum
134 <1×5 Real Constant: [1]ᵀ·A>
135 >>> picos.sum(A, axis=1) # The same as A.colsum
136 <4×1 Real Constant: A·[1]>
137 >>> numpy.allclose( # Same axis convention as NumPy.
138 ... numpy.sum(A.np, axis=0), picos.sum(A, axis=0).np)
139 True
140 """
141 if isinstance(lst, Expression):
142 if isinstance(lst, BiaffineExpression):
143 if axis is None:
144 return lst.sum
145 elif not isinstance(axis, (int, numpy.integer)):
146 raise TypeError(
147 "Axis must be an integer or None. (To sum multiple "
148 "expressions, provide an iterable as the first argument.)")
149 elif axis == 0:
150 return lst.rowsum
151 elif axis == 1:
152 return lst.colsum
153 else:
154 raise ValueError("Bad axis: {}.".format(axis))
155 else:
156 raise TypeError(
157 "PICOS doesn't know how to sum over a single {}."
158 .format(type(lst).__name__))
160 if axis is not None:
161 raise ValueError(
162 "No axis may be given when summing multiple expressions.")
164 # Allow passing also an iterator instead of an iterable. (The conversion is
165 # necessary as otherwise the check below would use up the iterator.)
166 if isinstance(lst, Iterator):
167 lst = tuple(lst)
169 # Resort to Python's built-in sum when no summand is a PICOS expression.
170 if not any(isinstance(expression, Expression) for expression in lst):
171 return builtins.sum(lst)
173 # If at least one summand is a PICOS expression, attempt to convert others.
174 try:
175 lst = [Constant(x) if not isinstance(x, Expression) else x for x in lst]
176 except Exception as error:
177 raise TypeError("Failed to convert some non-expression argument to a "
178 "PICOS constant.") from error
180 # Handle sums with at most two summands.
181 if len(lst) == 0:
182 return O()
183 elif len(lst) == 1:
184 return lst[0]
185 elif len(lst) == 2:
186 return lst[0] + lst[1]
188 # Find a suitable string description.
189 string = glyphs.sum(arguments([exp.string for exp in lst]))
191 # Handle (large) sums of only affine expressions efficiently.
192 if all(isinstance(expression, BiaffineExpression) for expression in lst):
193 # Determine resulting shape.
194 shapes = set(expression.shape for expression in lst)
195 if len(shapes) != 1:
196 raise TypeError("The shapes of summands do not match.")
197 else:
198 shape = shapes.pop()
200 # Determine resulting type.
201 try:
202 basetype = functools.reduce(
203 lambda A, B: A._common_basetype(B), lst, AffineExpression)
204 except Exception as error:
205 raise TypeError("Could not find a common base type for the given "
206 "(bi-)affine expressions.") from error
208 # Sum all coefficient matrices.
209 # NOTE: BiaffineExpression.__init__ will order mutable pairs and merge
210 # their coefficient matrices.
211 coefs = {}
212 byref = set()
213 for expression in lst:
214 for mtbs, coef in expression._coefs.items():
215 if mtbs in coefs:
216 if mtbs in byref:
217 # Make a copy of the coefficient so we may modify it.
218 coefs[mtbs] = load_data(coefs[mtbs], alwaysCopy=True)[0]
219 byref.remove(mtbs)
221 try:
222 coefs[mtbs] += coef
223 except TypeError:
224 # No in-place addition for sparse and dense types.
225 coefs[mtbs] = coefs[mtbs] + coef
226 else:
227 # Store the first coefficient by reference.
228 coefs[mtbs] = coef
229 byref.add(mtbs)
231 return basetype(string, shape, coefs)
233 theSum = lst[0]
234 for expression in lst[1:]:
235 theSum += expression
237 theSum._symbStr = string
239 return theSum
242def block(nested, shapes=None, name=None):
243 """Create an affine block matrix expression.
245 Given a two-level nested iterable container (e.g. a list of lists) of PICOS
246 affine expressions or constant data values or a mix thereof, this creates an
247 affine block matrix where each inner container represents one block row and
248 each expression or constant represents one block.
250 Blocks that are given as PICOS expressions are never reshaped or
251 broadcasted. Their shapes must already be consistent. Blocks that are given
252 as constant data values are reshaped or broadcasted as necessary **to match
253 existing PICOS expressions**. This means you can specify blocks as e.g.
254 ``"I"`` or ``0`` and PICOS will load them as matrices with the smallest
255 shape that is consistent with other blocks given as PICOS expressions.
257 Since constant data values are not reshaped or broadcasted with respect to
258 each other, the ``shapes`` parameter allows a manual clarification of block
259 shapes. It must be consistent with the shapes of blocks given as PICOS
260 expressions (they are still not reshaped or broadcasted).
262 :param nested:
263 The blocks.
264 :type nested:
265 tuple(tuple) or list(list)
267 :param shapes:
268 A pair ``(rows, columns)`` where ``rows`` defines the number of rows for
269 each block row and ``columns`` defines the number of columns for each
270 block column. You can put a ``0`` or :obj:`None` as a wildcard.
271 :type shapes:
272 tuple(tuple) or list(list)
274 :param str name:
275 Name or string description of the resulting block matrix. If
276 :obj:`None`, a descriptive string will be generated.
278 :Example:
280 >>> from picos import block, Constant, RealVariable
281 >>> C = Constant("C", range(6), (3, 2))
282 >>> d = Constant("d", 0.5, 2)
283 >>> x = RealVariable("x", 3)
284 >>> A = block([[ C, x ],
285 ... ["I", d ]]); A
286 <5×3 Real Affine Expression: [C, x; I, d]>
287 >>> x.value = [60, 70, 80]
288 >>> print(A)
289 [ 0.00e+00 3.00e+00 6.00e+01]
290 [ 1.00e+00 4.00e+00 7.00e+01]
291 [ 2.00e+00 5.00e+00 8.00e+01]
292 [ 1.00e+00 0.00e+00 5.00e-01]
293 [ 0.00e+00 1.00e+00 5.00e-01]
294 >>> B = block([[ C, x ], # With a shape hint.
295 ... ["I", 0 ]], shapes=((3, 2), (2, 1))); B
296 <5×3 Real Affine Expression: [C, x; I, 0]>
297 >>> print(B)
298 [ 0.00e+00 3.00e+00 6.00e+01]
299 [ 1.00e+00 4.00e+00 7.00e+01]
300 [ 2.00e+00 5.00e+00 8.00e+01]
301 [ 1.00e+00 0.00e+00 0.00e+00]
302 [ 0.00e+00 1.00e+00 0.00e+00]
303 """
304 # In a first stage, determine and validate fixed shapes from PICOS
305 # expressions, then load the remaining data to be consistent with the fixed
306 # shapes. In a second stage, validate also the shapes of the loaded data.
307 for stage in range(2):
308 R = numpy.array([[ # The row count for each block.
309 x.shape[0] if isinstance(x, BiaffineExpression) else 0 for x in row]
310 for row in nested], dtype=int)
312 M = [] # The row count for each block row.
313 for i, Ri in enumerate(R):
314 m = set(int(x) for x in Ri[numpy.nonzero(Ri)])
316 if shapes and shapes[0][i]:
317 m.add(shapes[0][i])
319 if len(m) > 1:
320 raise TypeError(
321 "Inconsistent number of rows in block row {}: {}."
322 .format(i + 1, m))
323 elif len(m) == 1:
324 M.append(int(m.pop()))
325 else:
326 assert stage == 0, "All blocks should have a shape by now."
327 M.append(None)
329 C = numpy.array([[ # The column count for each block.
330 x.shape[1] if isinstance(x, BiaffineExpression) else 0 for x in row]
331 for row in nested], dtype=int)
333 N = [] # The column count for each block column.
334 for j, Cj in enumerate(C.T):
335 n = set(int(x) for x in Cj[numpy.nonzero(Cj)])
337 if shapes and shapes[1][j]:
338 n.add(shapes[1][j])
340 if len(n) > 1:
341 raise TypeError(
342 "Inconsistent number of columns in block column {}: {}."
343 .format(j + 1, n))
344 elif len(n) == 1:
345 N.append(n.pop())
346 else:
347 assert stage == 0, "All blocks should have a shape by now."
348 N.append(None)
350 if stage == 0:
351 nested = [[
352 x if isinstance(x, BiaffineExpression)
353 else Constant(x, shape=(M[i], N[j]))
354 for j, x in enumerate(row)] for i, row in enumerate(nested)]
356 # List the blocks in block-row-major order.
357 blocks = [block for blockRow in nested for block in blockRow]
359 # Find the common base type of all expressions.
360 basetype = functools.reduce(BiaffineExpression._common_basetype.__func__,
361 (block.__class__ for block in blocks), AffineExpression)
362 typecode = basetype._get_typecode()
364 # Allow constant time random access to the block dimensions.
365 M, N = tuple(M), tuple(N)
367 # Compute the row (column) offsets for each block row (block column).
368 MOffsets = tuple(int(x) for x in numpy.cumsum((0,) + M))
369 NOffsets = tuple(int(x) for x in numpy.cumsum((0,) + N))
371 # Compute the full matrix dimensions.
372 m = builtins.sum(M)
373 n = builtins.sum(N)
374 mn = m*n
376 # Compute row and column offsets for each block in block-row-major-order.
377 MLen = len(N)
378 blockIndices = tuple(divmod(k, MLen) for k in range(len(blocks)))
379 blockOffsets = tuple(
380 (MOffsets[blockIndices[k][0]], NOffsets[blockIndices[k][1]])
381 for k in range(len(blocks)))
383 # Helper function to compute the matrix T (see below).
384 def _I():
385 for k, block in enumerate(blocks):
386 rows, cols = block.shape
387 i, j = blockOffsets[k]
388 for c in range(cols):
389 columnOffset = (j + c)*m + i
390 yield range(columnOffset, columnOffset + rows)
392 # Compute a sparse linear operator matrix T that transforms the stacked
393 # column-major vectorizations of the blocks in block-row-major order to the
394 # column-major vectorization of the full matrix.
395 V = tuple(itertools.repeat(1, mn))
396 I = tuple(itertools.chain(*_I()))
397 J = range(mn)
398 T = cvxopt.spmatrix(V, I, J, (mn, mn), typecode)
400 # Obtain all coefficient keys.
401 keys = set(key for block in blocks for key in block._coefs.keys())
403 # Stack all coefficient matrices in block-row-major order and apply T.
404 coefs = {}
405 for mtbs in keys:
406 dim = functools.reduce(lambda x, y:
407 (x if isinstance(x, int) else x.dim)*y.dim, mtbs, 1)
409 coefs[mtbs] = T*cvxopt_vcat([
410 block._coefs[mtbs] if mtbs in block._coefs
411 else cvxopt.spmatrix([], [], [], (len(block), dim), typecode)
412 for block in blocks])
414 # Build the string description.
415 if name:
416 string = str(name)
417 elif len(blocks) > 9:
418 string = glyphs.matrix(glyphs.shape((m, n)))
419 else:
420 string = functools.reduce(
421 lambda x, y: glyphs.matrix_cat(x, y, False),
422 (functools.reduce(
423 lambda x, y: glyphs.matrix_cat(x, y, True),
424 (block.string for block in blockRow))
425 for blockRow in nested))
427 return basetype(string, (m, n), coefs)
430def max(lst):
431 """Denote the maximum over a collection of convex scalar expressions.
433 If instead of a collection of expressions only a single multidimensional
434 affine expression is given, this denotes its largest element instead.
436 If some individual expressions are uncertain and their uncertainty is not of
437 stochastic but of worst-case nature (robust optimization), then the maximum
438 implicitly goes over their perturbation parameters as well.
440 :param lst:
441 A list of convex expressions or a single affine expression.
442 :type lst:
443 list or tuple or ~picos.expressions.AffineExpression
445 :Example:
447 >>> from picos import RealVariable, max, sum
448 >>> x = RealVariable("x", 5)
449 >>> max(x)
450 <Largest Element: max(x)>
451 >>> max(x) <= 2 # The same as x <= 2.
452 <Largest Element Constraint: max(x) ≤ 2>
453 >>> max([sum(x), abs(x)])
454 <Maximum of Convex Functions: max(∑(x), ‖x‖)>
455 >>> max([sum(x), abs(x)]) <= 2 # Both must be <= 2.
456 <Maximum of Convex Functions Constraint: max(∑(x), ‖x‖) ≤ 2>
457 >>> from picos.uncertain import UnitBallPerturbationSet
458 >>> z = UnitBallPerturbationSet("z", 5).parameter
459 >>> max([sum(x), x.T*z]) # Also maximize over z.
460 <Maximum of Convex Functions: max(∑(x), max_z xᵀ·z)>
461 """
462 UAE = UncertainAffineExpression
464 if isinstance(lst, AffineExpression):
465 return SumExtremes(lst, 1, largest=True, eigenvalues=False)
466 elif isinstance(lst, Expression):
467 raise TypeError("May only denote the maximum of a single affine "
468 "expression or of multiple (convex) expressions.")
470 try:
471 lst = [Constant(x) if not isinstance(x, Expression) else x for x in lst]
472 except Exception as error:
473 raise TypeError("Failed to convert some non-expression argument to a "
474 "PICOS constant.") from error
476 if any(isinstance(x, UAE) for x in lst) \
477 and all(isinstance(x, (AffineExpression, UAE)) for x in lst) \
478 and all(x.certain or x.universe.distributional for x in lst):
479 return RandomMaximumAffine(lst)
480 else:
481 return MaximumConvex(lst)
484def min(lst):
485 """Denote the minimum over a collection of concave scalar expressions.
487 If instead of a collection of expressions only a single multidimensional
488 affine expression is given, this denotes its smallest element instead.
490 If some individual expressions are uncertain and their uncertainty is not of
491 stochastic but of worst-case nature (robust optimization), then the minimum
492 implicitly goes over their perturbation parameters as well.
494 :param lst:
495 A list of concave expressions or a single affine expression.
496 :type lst:
497 list or tuple or ~picos.expressions.AffineExpression
499 :Example:
501 >>> from picos import RealVariable, min, sum
502 >>> x = RealVariable("x", 5)
503 >>> min(x)
504 <Smallest Element: min(x)>
505 >>> min(x) >= 2 # The same as x >= 2.
506 <Smallest Element Constraint: min(x) ≥ 2>
507 >>> min([sum(x), -x[0]**2])
508 <Minimum of Concave Functions: min(∑(x), -x[0]²)>
509 >>> min([sum(x), -x[0]**2]) >= 2 # Both must be >= 2.
510 <Minimum of Concave Functions Constraint: min(∑(x), -x[0]²) ≥ 2>
511 >>> from picos.uncertain import UnitBallPerturbationSet
512 >>> z = UnitBallPerturbationSet("z", 5).parameter
513 >>> min([sum(x), x.T*z]) # Also minimize over z.
514 <Minimum of Concave Functions: min(∑(x), min_z xᵀ·z)>
515 """
516 UAE = UncertainAffineExpression
518 if isinstance(lst, AffineExpression):
519 return SumExtremes(lst, 1, largest=False, eigenvalues=False)
520 elif isinstance(lst, Expression):
521 raise TypeError("May only denote the minimum of a single affine "
522 "expression or of multiple (concave) expressions.")
524 try:
525 lst = [Constant(x) if not isinstance(x, Expression) else x for x in lst]
526 except Exception as error:
527 raise TypeError("Failed to convert some non-expression argument to a "
528 "PICOS constant.") from error
530 if any(isinstance(x, UAE) for x in lst) \
531 and all(isinstance(x, (AffineExpression, UAE)) for x in lst) \
532 and all(x.certain or x.universe.distributional for x in lst):
533 return RandomMinimumAffine(lst)
534 else:
535 return MinimumConcave(lst)
538# ------------------------------------------------------------------------------
539# Functions that call expression methods.
540# ------------------------------------------------------------------------------
543def _error_on_none(func):
544 """Raise a :exc:`TypeError` if the function returns :obj:`None`."""
545 @functools.wraps(func)
546 def wrapper(*args, **kwargs):
547 result = func(*args, **kwargs)
549 if result is None:
550 raise TypeError("PICOS does not have a representation for {}({})."
551 .format(func.__qualname__ if hasattr(func, "__qualname__") else
552 func.__name__, ", ".join([type(x).__name__ for x in args] +
553 ["{}={}".format(k, type(x).__name__) for k, x in kwargs.items()]
554 )))
556 return result
557 return wrapper
560@_error_on_none
561@convert_and_refine_arguments("x")
562def exp(x):
563 """Denote the exponential."""
564 if hasattr(x, "exp"):
565 return x.exp
568@_error_on_none
569@convert_and_refine_arguments("x")
570def log(x):
571 """Denote the natural logarithm."""
572 if hasattr(x, "log"):
573 return x.log
576@deprecated("2.2", "Ensure that one operand is a PICOS expression and use infix"
577 " @ instead.")
578@_error_on_none
579@convert_and_refine_arguments("x", "y")
580def kron(x, y):
581 """Denote the kronecker product."""
582 if hasattr(x, "kron"):
583 return x @ y
586@_error_on_none
587@convert_and_refine_arguments("x")
588def diag(x, n=1):
589 r"""Form a diagonal matrix from the column-major vectorization of :math:`x`.
591 If :math:`n \neq 1`, then the vectorization is repeated :math:`n` times.
592 """
593 if hasattr(x, "diag") and n == 1:
594 return x.diag
595 elif hasattr(x, "dupdiag"):
596 return x.dupdiag(n)
599@_error_on_none
600@convert_and_refine_arguments("x")
601def maindiag(x):
602 """Extract the diagonal of :math:`x` as a column vector."""
603 if hasattr(x, "maindiag"):
604 return x.maindiag
607@_error_on_none
608@convert_and_refine_arguments("x")
609def trace(x):
610 """Denote the trace of a square matrix."""
611 if hasattr(x, "tr"):
612 return x.tr
615@_error_on_none
616@convert_and_refine_arguments("x")
617def partial_trace(x, subsystems=0, dimensions=2, k=None, dim=None):
618 """See :meth:`.exp_biaffine.BiaffineExpression.partial_trace`.
620 The parameters `k` and `dim` are for backwards compatibility.
621 """
622 if k is not None:
623 throw_deprecation_warning("Argument 'k' to partial_trace is "
624 "deprecated: Use 'subsystems' instead.", decoratorLevel=2)
625 subsystems = k
627 if dim is not None:
628 throw_deprecation_warning("Argument 'dim' to partial_trace is "
629 "deprecated: Use 'dimensions' instead.", decoratorLevel=2)
630 dimensions = dim
632 if isinstance(x, BiaffineExpression):
633 return x.partial_trace(subsystems, dimensions)
636@_error_on_none
637@convert_and_refine_arguments("x")
638def partial_transpose(x, subsystems=0, dimensions=2, k=None, dim=None):
639 """See :meth:`.exp_biaffine.BiaffineExpression.partial_transpose`.
641 The parameters `k` and `dim` are for backwards compatibility.
642 """
643 if k is not None:
644 throw_deprecation_warning("Argument 'k' to partial_transpose is "
645 "deprecated: Use 'subsystems' instead.", decoratorLevel=2)
646 subsystems = k
648 if dim is not None:
649 throw_deprecation_warning("Argument 'dim' to partial_transpose is "
650 "deprecated: Use 'dimensions' instead.", decoratorLevel=2)
651 dimensions = dim
653 if isinstance(x, BiaffineExpression):
654 return x.partial_transpose(subsystems, dimensions)
657# ------------------------------------------------------------------------------
658# Alias functions for expression classes meant to be instanciated by the user.
659# ------------------------------------------------------------------------------
662def _shorthand(name, cls):
663 def shorthand(*args, **kwargs):
664 return cls(*args, **kwargs)
666 shorthand.__doc__ = "Shorthand for :class:`{1} <{0}.{1}>`.".format(
667 cls.__module__, cls.__qualname__)
668 shorthand.__name__ = name
669 shorthand.__qualname__ = name
671 return shorthand
674expcone = _shorthand("expcone", ExponentialCone)
675geomean = _shorthand("geomean", GeometricMean)
676kldiv = _shorthand("kldiv", NegativeEntropy)
677lse = _shorthand("lse", LogSumExp)
678rsoc = _shorthand("rsoc", RotatedSecondOrderCone)
679soc = _shorthand("soc", SecondOrderCone)
680sumexp = _shorthand("sumexp", SumExponentials)
683def sum_k_largest(x, k):
684 """Wrapper for :class:`~picos.SumExtremes`.
686 Sets ``largest = True`` and ``eigenvalues = False``.
688 :Example:
690 >>> from picos import RealVariable, sum_k_largest
691 >>> x = RealVariable("x", 5)
692 >>> sum_k_largest(x, 2)
693 <Sum of Largest Elements: sum_2_largest(x)>
694 >>> sum_k_largest(x, 2) <= 2
695 <Sum of Largest Elements Constraint: sum_2_largest(x) ≤ 2>
696 """
697 return SumExtremes(x, k, largest=True, eigenvalues=False)
700def sum_k_smallest(x, k):
701 """Wrapper for :class:`~picos.SumExtremes`.
703 Sets ``largest = False`` and ``eigenvalues = False``.
705 :Example:
707 >>> from picos import RealVariable, sum_k_smallest
708 >>> x = RealVariable("x", 5)
709 >>> sum_k_smallest(x, 2)
710 <Sum of Smallest Elements: sum_2_smallest(x)>
711 >>> sum_k_smallest(x, 2) >= 2
712 <Sum of Smallest Elements Constraint: sum_2_smallest(x) ≥ 2>
713 """
714 return SumExtremes(x, k, largest=False, eigenvalues=False)
717def lambda_max(x):
718 """Wrapper for :class:`~picos.SumExtremes`.
720 Sets ``k = 1``, ``largest = True`` and ``eigenvalues = True``.
722 :Example:
724 >>> from picos import SymmetricVariable, lambda_max
725 >>> X = SymmetricVariable("X", 5)
726 >>> lambda_max(X)
727 <Largest Eigenvalue: λ_max(X)>
728 >>> lambda_max(X) <= 2
729 <Largest Eigenvalue Constraint: λ_max(X) ≤ 2>
730 """
731 return SumExtremes(x, 1, largest=True, eigenvalues=True)
734def lambda_min(x):
735 """Wrapper for :class:`~picos.SumExtremes`.
737 Sets ``k = 1``, ``largest = False`` and ``eigenvalues = True``.
739 :Example:
741 >>> from picos import SymmetricVariable, lambda_min
742 >>> X = SymmetricVariable("X", 5)
743 >>> lambda_min(X)
744 <Smallest Eigenvalue: λ_min(X)>
745 >>> lambda_min(X) >= 2
746 <Smallest Eigenvalue Constraint: λ_min(X) ≥ 2>
747 """
748 return SumExtremes(x, 1, largest=False, eigenvalues=True)
751def sum_k_largest_lambda(x, k):
752 """Wrapper for :class:`~picos.SumExtremes`.
754 Sets ``largest = True`` and ``eigenvalues = True``.
756 :Example:
758 >>> from picos import SymmetricVariable, sum_k_largest_lambda
759 >>> X = SymmetricVariable("X", 5)
760 >>> sum_k_largest_lambda(X, 2)
761 <Sum of Largest Eigenvalues: sum_2_largest_λ(X)>
762 >>> sum_k_largest_lambda(X, 2) <= 2
763 <Sum of Largest Eigenvalues Constraint: sum_2_largest_λ(X) ≤ 2>
764 """
765 return SumExtremes(x, k, largest=True, eigenvalues=True)
768def sum_k_smallest_lambda(x, k):
769 """Wrapper for :class:`~picos.SumExtremes`.
771 Sets ``largest = False`` and ``eigenvalues = True``.
773 :Example:
775 >>> from picos import SymmetricVariable, sum_k_smallest_lambda
776 >>> X = SymmetricVariable("X", 5)
777 >>> sum_k_smallest_lambda(X, 2)
778 <Sum of Smallest Eigenvalues: sum_2_smallest_λ(X)>
779 >>> sum_k_smallest_lambda(X, 2) >= 2
780 <Sum of Smallest Eigenvalues Constraint: sum_2_smallest_λ(X) ≥ 2>
781 """
782 return SumExtremes(x, k, largest=False, eigenvalues=True)
785# ------------------------------------------------------------------------------
786# Legacy algebraic functions for backwards compatibility.
787# ------------------------------------------------------------------------------
790def _deprecated_shorthand(name, cls, new_shorthand=None):
791 uiRef = "picos.{}".format(new_shorthand if new_shorthand else cls.__name__)
793 # FIXME: Warning doesn't show the name of the deprecated shorthand function.
794 @deprecated("2.0", useInstead=uiRef)
795 def shorthand(*args, **kwargs):
796 """|PLACEHOLDER|""" # noqa
797 return cls(*args, **kwargs)
799 shorthand.__doc__ = shorthand.__doc__.replace("|PLACEHOLDER|",
800 "Legacy shorthand for :class:`{1} <{0}.{1}>`.".format(
801 cls.__module__, cls.__qualname__))
802 shorthand.__name__ = name
803 shorthand.__qualname__ = name
805 return shorthand
808ball = _deprecated_shorthand("ball", Ball)
809detrootn = _deprecated_shorthand("detrootn", DetRootN)
810kullback_leibler = _deprecated_shorthand(
811 "kullback_leibler", NegativeEntropy, "kldiv")
812logsumexp = _deprecated_shorthand("logsumexp", LogSumExp, "lse")
813norm = _deprecated_shorthand("norm", Norm)
816@deprecated("2.0", useInstead="picos.PowerTrace")
817def tracepow(exp, num=1, denom=1, coef=None):
818 """Legacy shorthand for :class:`~picos.PowerTrace`."""
819 return PowerTrace(exp, num / denom, coef)
822@deprecated("2.0", useInstead="picos.Constant")
823def new_param(name, value):
824 """Create a constant or a list or dict or tuple thereof."""
825 if isinstance(value, list):
826 # Handle a vector.
827 try:
828 for x in value:
829 complex(x)
830 except Exception:
831 pass
832 else:
833 return Constant(name, value)
835 # Handle a matrix.
836 if all(isinstance(x, list) for x in value) \
837 and all(len(x) == len(value[0]) for x in value):
838 try:
839 for x in value:
840 for y in x:
841 complex(y)
842 except Exception:
843 pass
844 else:
845 return Constant(name, value)
847 # Return a list of constants.
848 return [Constant(glyphs.slice(name, i), x) for i, x in enumerate(value)]
849 elif isinstance(value, tuple):
850 # Return a list of constants.
851 # NOTE: This is very inconsistent, but legacy behavior.
852 return [Constant(glyphs.slice(name, i), x) for i, x in enumerate(value)]
853 elif isinstance(value, dict):
854 return {k: Constant(glyphs.slice(name, k), x) for k, x in value.items()}
855 else:
856 return Constant(name, value)
859@deprecated("2.0", useInstead="picos.FlowConstraint")
860def flow_Constraint(*args, **kwargs):
861 """Legacy shorthand for :class:`~picos.FlowConstraint`."""
862 from ..constraints.con_flow import FlowConstraint
863 return FlowConstraint(*args, **kwargs)
866@deprecated("2.0", useInstead="picos.maindiag")
867def diag_vect(x):
868 """Extract the diagonal of :math:`x` as a column vector."""
869 return maindiag(x)
872@deprecated("2.0", useInstead="picos.Simplex")
873def simplex(gamma):
874 r"""Create a standard simplex of radius :math:`\gamma`."""
875 return Simplex(gamma, truncated=False, symmetrized=False)
878@deprecated("2.0", useInstead="picos.Simplex")
879def truncated_simplex(gamma, sym=False):
880 r"""Create a truncated simplex of radius :math:`\gamma`."""
881 return Simplex(gamma, truncated=True, symmetrized=sym)
884# --------------------------------------
885__all__ = api_end(_API_START, globals())