# Coverage for picos/expressions/algebra.py: 82.73%

## 330 statements

, 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

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:

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.

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

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]:

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]:

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()

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"

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.

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)

817def tracepow(exp, num=1, denom=1, coef=None):

818 """Legacy shorthand for :class:~picos.PowerTrace."""

819 return PowerTrace(exp, num / denom, coef)

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)

860def flow_Constraint(*args, **kwargs):

861 """Legacy shorthand for :class:~picos.FlowConstraint."""

862 from ..constraints.con_flow import FlowConstraint

863 return FlowConstraint(*args, **kwargs)

867def diag_vect(x):

868 """Extract the diagonal of :math:x as a column vector."""

869 return maindiag(x)

873def simplex(gamma):

874 r"""Create a standard simplex of radius :math:\gamma."""

875 return Simplex(gamma, truncated=False, symmetrized=False)

880 r"""Create a truncated simplex of radius :math:\gamma."""