Coverage for picos/expressions/algebra.py: 83.18%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

327 statements  

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# ------------------------------------------------------------------------------ 

19 

20"""Implements functions that create or modify algebraic expressions.""" 

21 

22import builtins 

23import functools 

24import itertools 

25from collections.abc import Iterator 

26 

27import cvxopt 

28import numpy 

29 

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 

53 

54_API_START = api_start(globals()) 

55# ------------------------------- 

56 

57 

58# ------------------------------------------------------------------------------ 

59# Algebraic functions with a logic of their own. 

60# ------------------------------------------------------------------------------ 

61 

62 

63@functools.lru_cache() 

64def O(rows=1, cols=1): # noqa 

65 """Create a zero matrix. 

66 

67 :Example: 

68 

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

75 

76 

77@functools.lru_cache() 

78def I(size=1): # noqa 

79 """Create an identity matrix. 

80 

81 :Example: 

82 

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

90 

91 

92@functools.lru_cache() 

93def J(rows=1, cols=1): 

94 """Create a matrix of all ones. 

95 

96 :Example: 

97 

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

104 

105 

106def sum(lst, axis=None): 

107 """Sum PICOS expressions and give the result a meaningful description. 

108 

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. 

113 

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`. 

120 

121 :Example: 

122 

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

159 

160 if axis is not None: 

161 raise ValueError( 

162 "No axis may be given when summing multiple expressions.") 

163 

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) 

168 

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) 

172 

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 

179 

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] 

187 

188 # Find a suitable string description. 

189 string = glyphs.sum(arguments([exp.string for exp in lst])) 

190 

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

199 

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 

207 

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) 

220 

221 coefs[mtbs] += coef 

222 else: 

223 # Store the first coefficient by reference. 

224 coefs[mtbs] = coef 

225 byref.add(mtbs) 

226 

227 return basetype(string, shape, coefs) 

228 

229 theSum = lst[0] 

230 for expression in lst[1:]: 

231 theSum += expression 

232 

233 theSum._symbStr = string 

234 

235 return theSum 

236 

237 

238def block(nested, shapes=None, name=None): 

239 """Create an affine block matrix expression. 

240 

241 Given a two-level nested iterable container (e.g. a list of lists) of PICOS 

242 affine expressions or constant data values or a mix thereof, this creates an 

243 affine block matrix where each inner container represents one block row and 

244 each expression or constant represents one block. 

245 

246 Blocks that are given as PICOS expressions are never reshaped or 

247 broadcasted. Their shapes must already be consistent. Blocks that are given 

248 as constant data values are reshaped or broadcasted as necessary **to match 

249 existing PICOS expressions**. This means you can specify blocks as e.g. 

250 ``"I"`` or ``0`` and PICOS will load them as matrices with the smallest 

251 shape that is consistent with other blocks given as PICOS expressions. 

252 

253 Since constant data values are not reshaped or broadcasted with respect to 

254 each other, the ``shapes`` parameter allows a manual clarification of block 

255 shapes. It must be consistent with the shapes of blocks given as PICOS 

256 expressions (they are still not reshaped or broadcasted). 

257 

258 :param nested: 

259 The blocks. 

260 :type nested: 

261 tuple(tuple) or list(list) 

262 

263 :param shapes: 

264 A pair ``(rows, columns)`` where ``rows`` defines the number of rows for 

265 each block row and ``columns`` defines the number of columns for each 

266 block column. You can put a ``0`` or :obj:`None` as a wildcard. 

267 :type shapes: 

268 tuple(tuple) or list(list) 

269 

270 :param str name: 

271 Name or string description of the resulting block matrix. If 

272 :obj:`None`, a descriptive string will be generated. 

273 

274 :Example: 

275 

276 >>> from picos import block, Constant, RealVariable 

277 >>> C = Constant("C", range(6), (3, 2)) 

278 >>> d = Constant("d", 0.5, 2) 

279 >>> x = RealVariable("x", 3) 

280 >>> A = block([[ C, x ], 

281 ... ["I", d ]]); A 

282 <5×3 Real Affine Expression: [C, x; I, d]> 

283 >>> x.value = [60, 70, 80] 

284 >>> print(A) 

285 [ 0.00e+00 3.00e+00 6.00e+01] 

286 [ 1.00e+00 4.00e+00 7.00e+01] 

287 [ 2.00e+00 5.00e+00 8.00e+01] 

288 [ 1.00e+00 0.00e+00 5.00e-01] 

289 [ 0.00e+00 1.00e+00 5.00e-01] 

290 >>> B = block([[ C, x ], # With a shape hint. 

291 ... ["I", 0 ]], shapes=((3, 2), (2, 1))); B 

292 <5×3 Real Affine Expression: [C, x; I, 0]> 

293 >>> print(B) 

294 [ 0.00e+00 3.00e+00 6.00e+01] 

295 [ 1.00e+00 4.00e+00 7.00e+01] 

296 [ 2.00e+00 5.00e+00 8.00e+01] 

297 [ 1.00e+00 0.00e+00 0.00e+00] 

298 [ 0.00e+00 1.00e+00 0.00e+00] 

299 """ 

300 # In a first stage, determine and validate fixed shapes from PICOS 

301 # expressions, then load the remaining data to be consistent with the fixed 

302 # shapes. In a second stage, validate also the shapes of the loaded data. 

303 for stage in range(2): 

304 R = numpy.array([[ # The row count for each block. 

305 x.shape[0] if isinstance(x, BiaffineExpression) else 0 for x in row] 

306 for row in nested], dtype=int) 

307 

308 M = [] # The row count for each block row. 

309 for i, Ri in enumerate(R): 

310 m = set(int(x) for x in Ri[numpy.nonzero(Ri)]) 

311 

312 if shapes and shapes[0][i]: 

313 m.add(shapes[0][i]) 

314 

315 if len(m) > 1: 

316 raise TypeError( 

317 "Inconsistent number of rows in block row {}: {}." 

318 .format(i + 1, m)) 

319 elif len(m) == 1: 

320 M.append(int(m.pop())) 

321 else: 

322 assert stage == 0, "All blocks should have a shape by now." 

323 M.append(None) 

324 

325 C = numpy.array([[ # The column count for each block. 

326 x.shape[1] if isinstance(x, BiaffineExpression) else 0 for x in row] 

327 for row in nested], dtype=int) 

328 

329 N = [] # The column count for each block column. 

330 for j, Cj in enumerate(C.T): 

331 n = set(int(x) for x in Cj[numpy.nonzero(Cj)]) 

332 

333 if shapes and shapes[1][j]: 

334 n.add(shapes[1][j]) 

335 

336 if len(n) > 1: 

337 raise TypeError( 

338 "Inconsistent number of columns in block column {}: {}." 

339 .format(j + 1, n)) 

340 elif len(n) == 1: 

341 N.append(n.pop()) 

342 else: 

343 assert stage == 0, "All blocks should have a shape by now." 

344 N.append(None) 

345 

346 if stage == 0: 

347 nested = [[ 

348 x if isinstance(x, BiaffineExpression) 

349 else Constant(x, shape=(M[i], N[j])) 

350 for j, x in enumerate(row)] for i, row in enumerate(nested)] 

351 

352 # List the blocks in block-row-major order. 

353 blocks = [block for blockRow in nested for block in blockRow] 

354 

355 # Find the common base type of all expressions. 

356 basetype = functools.reduce(BiaffineExpression._common_basetype.__func__, 

357 (block.__class__ for block in blocks), AffineExpression) 

358 typecode = basetype._get_typecode() 

359 

360 # Allow constant time random access to the block dimensions. 

361 M, N = tuple(M), tuple(N) 

362 

363 # Compute the row (column) offsets for each block row (block column). 

364 MOffsets = tuple(int(x) for x in numpy.cumsum((0,) + M)) 

365 NOffsets = tuple(int(x) for x in numpy.cumsum((0,) + N)) 

366 

367 # Compute the full matrix dimensions. 

368 m = builtins.sum(M) 

369 n = builtins.sum(N) 

370 mn = m*n 

371 

372 # Compute row and column offsets for each block in block-row-major-order. 

373 MLen = len(N) 

374 blockIndices = tuple(divmod(k, MLen) for k in range(len(blocks))) 

375 blockOffsets = tuple( 

376 (MOffsets[blockIndices[k][0]], NOffsets[blockIndices[k][1]]) 

377 for k in range(len(blocks))) 

378 

379 # Helper function to compute the matrix T (see below). 

380 def _I(): 

381 for k, block in enumerate(blocks): 

382 rows, cols = block.shape 

383 i, j = blockOffsets[k] 

384 for c in range(cols): 

385 columnOffset = (j + c)*m + i 

386 yield range(columnOffset, columnOffset + rows) 

387 

388 # Compute a sparse linear operator matrix T that transforms the stacked 

389 # column-major vectorizations of the blocks in block-row-major order to the 

390 # column-major vectorization of the full matrix. 

391 V = tuple(itertools.repeat(1, mn)) 

392 I = tuple(itertools.chain(*_I())) 

393 J = range(mn) 

394 T = cvxopt.spmatrix(V, I, J, (mn, mn), typecode) 

395 

396 # Obtain all coefficient keys. 

397 keys = set(key for block in blocks for key in block._coefs.keys()) 

398 

399 # Stack all coefficient matrices in block-row-major order and apply T. 

400 coefs = {} 

401 for mtbs in keys: 

402 dim = functools.reduce(lambda x, y: 

403 (x if isinstance(x, int) else x.dim)*y.dim, mtbs, 1) 

404 

405 coefs[mtbs] = T*cvxopt_vcat([ 

406 block._coefs[mtbs] if mtbs in block._coefs 

407 else cvxopt.spmatrix([], [], [], (len(block), dim), typecode) 

408 for block in blocks]) 

409 

410 # Build the string description. 

411 if name: 

412 string = str(name) 

413 elif len(blocks) > 9: 

414 string = glyphs.matrix(glyphs.shape((m, n))) 

415 else: 

416 string = functools.reduce( 

417 lambda x, y: glyphs.matrix_cat(x, y, False), 

418 (functools.reduce( 

419 lambda x, y: glyphs.matrix_cat(x, y, True), 

420 (block.string for block in blockRow)) 

421 for blockRow in nested)) 

422 

423 return basetype(string, (m, n), coefs) 

424 

425 

426def max(lst): 

427 """Denote the maximum over a collection of convex scalar expressions. 

428 

429 If instead of a collection of expressions only a single multidimensional 

430 affine expression is given, this denotes its largest element instead. 

431 

432 If some individual expressions are uncertain and their uncertainty is not of 

433 stochastic but of worst-case nature (robust optimization), then the maximum 

434 implicitly goes over their perturbation parameters as well. 

435 

436 :param lst: 

437 A list of convex expressions or a single affine expression. 

438 :type lst: 

439 list or tuple or ~picos.expressions.AffineExpression 

440 

441 :Example: 

442 

443 >>> from picos import RealVariable, max, sum 

444 >>> x = RealVariable("x", 5) 

445 >>> max(x) 

446 <Largest Element: max(x)> 

447 >>> max(x) <= 2 # The same as x <= 2. 

448 <Largest Element Constraint: max(x) ≤ 2> 

449 >>> max([sum(x), abs(x)]) 

450 <Maximum of Convex Functions: max(∑(x), ‖x‖)> 

451 >>> max([sum(x), abs(x)]) <= 2 # Both must be <= 2. 

452 <Maximum of Convex Functions Constraint: max(∑(x), ‖x‖) ≤ 2> 

453 >>> from picos.uncertain import UnitBallPerturbationSet 

454 >>> z = UnitBallPerturbationSet("z", 5).parameter 

455 >>> max([sum(x), x.T*z]) # Also maximize over z. 

456 <Maximum of Convex Functions: max(∑(x), max_z xᵀ·z)> 

457 """ 

458 UAE = UncertainAffineExpression 

459 

460 if isinstance(lst, AffineExpression): 

461 return SumExtremes(lst, 1, largest=True, eigenvalues=False) 

462 elif isinstance(lst, Expression): 

463 raise TypeError("May only denote the maximum of a single affine " 

464 "expression or of multiple (convex) expressions.") 

465 

466 try: 

467 lst = [Constant(x) if not isinstance(x, Expression) else x for x in lst] 

468 except Exception as error: 

469 raise TypeError("Failed to convert some non-expression argument to a " 

470 "PICOS constant.") from error 

471 

472 if any(isinstance(x, UAE) for x in lst) \ 

473 and all(isinstance(x, (AffineExpression, UAE)) for x in lst) \ 

474 and all(x.certain or x.universe.distributional for x in lst): 

475 return RandomMaximumAffine(lst) 

476 else: 

477 return MaximumConvex(lst) 

478 

479 

480def min(lst): 

481 """Denote the minimum over a collection of concave scalar expressions. 

482 

483 If instead of a collection of expressions only a single multidimensional 

484 affine expression is given, this denotes its smallest element instead. 

485 

486 If some individual expressions are uncertain and their uncertainty is not of 

487 stochastic but of worst-case nature (robust optimization), then the minimum 

488 implicitly goes over their perturbation parameters as well. 

489 

490 :param lst: 

491 A list of concave expressions or a single affine expression. 

492 :type lst: 

493 list or tuple or ~picos.expressions.AffineExpression 

494 

495 :Example: 

496 

497 >>> from picos import RealVariable, min, sum 

498 >>> x = RealVariable("x", 5) 

499 >>> min(x) 

500 <Smallest Element: min(x)> 

501 >>> min(x) >= 2 # The same as x >= 2. 

502 <Smallest Element Constraint: min(x) ≥ 2> 

503 >>> min([sum(x), -x[0]**2]) 

504 <Minimum of Concave Functions: min(∑(x), -x[0]²)> 

505 >>> min([sum(x), -x[0]**2]) >= 2 # Both must be >= 2. 

506 <Minimum of Concave Functions Constraint: min(∑(x), -x[0]²) ≥ 2> 

507 >>> from picos.uncertain import UnitBallPerturbationSet 

508 >>> z = UnitBallPerturbationSet("z", 5).parameter 

509 >>> min([sum(x), x.T*z]) # Also minimize over z. 

510 <Minimum of Concave Functions: min(∑(x), min_z xᵀ·z)> 

511 """ 

512 UAE = UncertainAffineExpression 

513 

514 if isinstance(lst, AffineExpression): 

515 return SumExtremes(lst, 1, largest=False, eigenvalues=False) 

516 elif isinstance(lst, Expression): 

517 raise TypeError("May only denote the minimum of a single affine " 

518 "expression or of multiple (concave) expressions.") 

519 

520 try: 

521 lst = [Constant(x) if not isinstance(x, Expression) else x for x in lst] 

522 except Exception as error: 

523 raise TypeError("Failed to convert some non-expression argument to a " 

524 "PICOS constant.") from error 

525 

526 if any(isinstance(x, UAE) for x in lst) \ 

527 and all(isinstance(x, (AffineExpression, UAE)) for x in lst) \ 

528 and all(x.certain or x.universe.distributional for x in lst): 

529 return RandomMinimumAffine(lst) 

530 else: 

531 return MinimumConcave(lst) 

532 

533 

534# ------------------------------------------------------------------------------ 

535# Functions that call expression methods. 

536# ------------------------------------------------------------------------------ 

537 

538 

539def _error_on_none(func): 

540 """Raise a :exc:`TypeError` if the function returns :obj:`None`.""" 

541 @functools.wraps(func) 

542 def wrapper(*args, **kwargs): 

543 result = func(*args, **kwargs) 

544 

545 if result is None: 

546 raise TypeError("PICOS does not have a representation for {}({})." 

547 .format(func.__qualname__ if hasattr(func, "__qualname__") else 

548 func.__name__, ", ".join([type(x).__name__ for x in args] + 

549 ["{}={}".format(k, type(x).__name__) for k, x in kwargs.items()] 

550 ))) 

551 

552 return result 

553 return wrapper 

554 

555 

556@_error_on_none 

557@convert_and_refine_arguments("x") 

558def exp(x): 

559 """Denote the exponential.""" 

560 if hasattr(x, "exp"): 

561 return x.exp 

562 

563 

564@_error_on_none 

565@convert_and_refine_arguments("x") 

566def log(x): 

567 """Denote the natural logarithm.""" 

568 if hasattr(x, "log"): 

569 return x.log 

570 

571 

572@deprecated("2.2", "Ensure that one operand is a PICOS expression and use infix" 

573 " @ instead.") 

574@_error_on_none 

575@convert_and_refine_arguments("x", "y") 

576def kron(x, y): 

577 """Denote the kronecker product.""" 

578 if hasattr(x, "kron"): 

579 return x @ y 

580 

581 

582@_error_on_none 

583@convert_and_refine_arguments("x") 

584def diag(x, n=1): 

585 r"""Form a diagonal matrix from the column-major vectorization of :math:`x`. 

586 

587 If :math:`n \neq 1`, then the vectorization is repeated :math:`n` times. 

588 """ 

589 if hasattr(x, "diag") and n == 1: 

590 return x.diag 

591 elif hasattr(x, "dupdiag"): 

592 return x.dupdiag(n) 

593 

594 

595@_error_on_none 

596@convert_and_refine_arguments("x") 

597def maindiag(x): 

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

599 if hasattr(x, "maindiag"): 

600 return x.maindiag 

601 

602 

603@_error_on_none 

604@convert_and_refine_arguments("x") 

605def trace(x): 

606 """Denote the trace of a square matrix.""" 

607 if hasattr(x, "tr"): 

608 return x.tr 

609 

610 

611@_error_on_none 

612@convert_and_refine_arguments("x") 

613def partial_trace(x, subsystems=0, dimensions=2, k=None, dim=None): 

614 """See :meth:`.exp_biaffine.BiaffineExpression.partial_trace`. 

615 

616 The parameters `k` and `dim` are for backwards compatibility. 

617 """ 

618 if k is not None: 

619 throw_deprecation_warning("Argument 'k' to partial_trace is " 

620 "deprecated: Use 'subsystems' instead.", decoratorLevel=2) 

621 subsystems = k 

622 

623 if dim is not None: 

624 throw_deprecation_warning("Argument 'dim' to partial_trace is " 

625 "deprecated: Use 'dimensions' instead.", decoratorLevel=2) 

626 dimensions = dim 

627 

628 if isinstance(x, BiaffineExpression): 

629 return x.partial_trace(subsystems, dimensions) 

630 

631 

632@_error_on_none 

633@convert_and_refine_arguments("x") 

634def partial_transpose(x, subsystems=0, dimensions=2, k=None, dim=None): 

635 """See :meth:`.exp_biaffine.BiaffineExpression.partial_transpose`. 

636 

637 The parameters `k` and `dim` are for backwards compatibility. 

638 """ 

639 if k is not None: 

640 throw_deprecation_warning("Argument 'k' to partial_transpose is " 

641 "deprecated: Use 'subsystems' instead.", decoratorLevel=2) 

642 subsystems = k 

643 

644 if dim is not None: 

645 throw_deprecation_warning("Argument 'dim' to partial_transpose is " 

646 "deprecated: Use 'dimensions' instead.", decoratorLevel=2) 

647 dimensions = dim 

648 

649 if isinstance(x, BiaffineExpression): 

650 return x.partial_transpose(subsystems, dimensions) 

651 

652 

653# ------------------------------------------------------------------------------ 

654# Alias functions for expression classes meant to be instanciated by the user. 

655# ------------------------------------------------------------------------------ 

656 

657 

658def _shorthand(name, cls): 

659 def shorthand(*args, **kwargs): 

660 return cls(*args, **kwargs) 

661 

662 shorthand.__doc__ = "Shorthand for :class:`{1} <{0}.{1}>`.".format( 

663 cls.__module__, cls.__qualname__) 

664 shorthand.__name__ = name 

665 shorthand.__qualname__ = name 

666 

667 return shorthand 

668 

669 

670expcone = _shorthand("expcone", ExponentialCone) 

671geomean = _shorthand("geomean", GeometricMean) 

672kldiv = _shorthand("kldiv", NegativeEntropy) 

673lse = _shorthand("lse", LogSumExp) 

674rsoc = _shorthand("rsoc", RotatedSecondOrderCone) 

675soc = _shorthand("soc", SecondOrderCone) 

676sumexp = _shorthand("sumexp", SumExponentials) 

677 

678 

679def sum_k_largest(x, k): 

680 """Wrapper for :class:`~picos.SumExtremes`. 

681 

682 Sets ``largest = True`` and ``eigenvalues = False``. 

683 

684 :Example: 

685 

686 >>> from picos import RealVariable, sum_k_largest 

687 >>> x = RealVariable("x", 5) 

688 >>> sum_k_largest(x, 2) 

689 <Sum of Largest Elements: sum_2_largest(x)> 

690 >>> sum_k_largest(x, 2) <= 2 

691 <Sum of Largest Elements Constraint: sum_2_largest(x) ≤ 2> 

692 """ 

693 return SumExtremes(x, k, largest=True, eigenvalues=False) 

694 

695 

696def sum_k_smallest(x, k): 

697 """Wrapper for :class:`~picos.SumExtremes`. 

698 

699 Sets ``largest = False`` and ``eigenvalues = False``. 

700 

701 :Example: 

702 

703 >>> from picos import RealVariable, sum_k_smallest 

704 >>> x = RealVariable("x", 5) 

705 >>> sum_k_smallest(x, 2) 

706 <Sum of Smallest Elements: sum_2_smallest(x)> 

707 >>> sum_k_smallest(x, 2) >= 2 

708 <Sum of Smallest Elements Constraint: sum_2_smallest(x) ≥ 2> 

709 """ 

710 return SumExtremes(x, k, largest=False, eigenvalues=False) 

711 

712 

713def lambda_max(x): 

714 """Wrapper for :class:`~picos.SumExtremes`. 

715 

716 Sets ``k = 1``, ``largest = True`` and ``eigenvalues = True``. 

717 

718 :Example: 

719 

720 >>> from picos import SymmetricVariable, lambda_max 

721 >>> X = SymmetricVariable("X", 5) 

722 >>> lambda_max(X) 

723 <Largest Eigenvalue: λ_max(X)> 

724 >>> lambda_max(X) <= 2 

725 <Largest Eigenvalue Constraint: λ_max(X) ≤ 2> 

726 """ 

727 return SumExtremes(x, 1, largest=True, eigenvalues=True) 

728 

729 

730def lambda_min(x): 

731 """Wrapper for :class:`~picos.SumExtremes`. 

732 

733 Sets ``k = 1``, ``largest = False`` and ``eigenvalues = True``. 

734 

735 :Example: 

736 

737 >>> from picos import SymmetricVariable, lambda_min 

738 >>> X = SymmetricVariable("X", 5) 

739 >>> lambda_min(X) 

740 <Smallest Eigenvalue: λ_min(X)> 

741 >>> lambda_min(X) >= 2 

742 <Smallest Eigenvalue Constraint: λ_min(X) ≥ 2> 

743 """ 

744 return SumExtremes(x, 1, largest=False, eigenvalues=True) 

745 

746 

747def sum_k_largest_lambda(x, k): 

748 """Wrapper for :class:`~picos.SumExtremes`. 

749 

750 Sets ``largest = True`` and ``eigenvalues = True``. 

751 

752 :Example: 

753 

754 >>> from picos import SymmetricVariable, sum_k_largest_lambda 

755 >>> X = SymmetricVariable("X", 5) 

756 >>> sum_k_largest_lambda(X, 2) 

757 <Sum of Largest Eigenvalues: sum_2_largest_λ(X)> 

758 >>> sum_k_largest_lambda(X, 2) <= 2 

759 <Sum of Largest Eigenvalues Constraint: sum_2_largest_λ(X) ≤ 2> 

760 """ 

761 return SumExtremes(x, k, largest=True, eigenvalues=True) 

762 

763 

764def sum_k_smallest_lambda(x, k): 

765 """Wrapper for :class:`~picos.SumExtremes`. 

766 

767 Sets ``largest = False`` and ``eigenvalues = True``. 

768 

769 :Example: 

770 

771 >>> from picos import SymmetricVariable, sum_k_smallest_lambda 

772 >>> X = SymmetricVariable("X", 5) 

773 >>> sum_k_smallest_lambda(X, 2) 

774 <Sum of Smallest Eigenvalues: sum_2_smallest_λ(X)> 

775 >>> sum_k_smallest_lambda(X, 2) >= 2 

776 <Sum of Smallest Eigenvalues Constraint: sum_2_smallest_λ(X) ≥ 2> 

777 """ 

778 return SumExtremes(x, k, largest=False, eigenvalues=True) 

779 

780 

781# ------------------------------------------------------------------------------ 

782# Legacy algebraic functions for backwards compatibility. 

783# ------------------------------------------------------------------------------ 

784 

785 

786def _deprecated_shorthand(name, cls, new_shorthand=None): 

787 uiRef = "picos.{}".format(new_shorthand if new_shorthand else cls.__name__) 

788 

789 # FIXME: Warning doesn't show the name of the deprecated shorthand function. 

790 @deprecated("2.0", useInstead=uiRef) 

791 def shorthand(*args, **kwargs): 

792 """|PLACEHOLDER|""" # noqa 

793 return cls(*args, **kwargs) 

794 

795 shorthand.__doc__ = shorthand.__doc__.replace("|PLACEHOLDER|", 

796 "Legacy shorthand for :class:`{1} <{0}.{1}>`.".format( 

797 cls.__module__, cls.__qualname__)) 

798 shorthand.__name__ = name 

799 shorthand.__qualname__ = name 

800 

801 return shorthand 

802 

803 

804ball = _deprecated_shorthand("ball", Ball) 

805detrootn = _deprecated_shorthand("detrootn", DetRootN) 

806kullback_leibler = _deprecated_shorthand( 

807 "kullback_leibler", NegativeEntropy, "kldiv") 

808logsumexp = _deprecated_shorthand("logsumexp", LogSumExp, "lse") 

809norm = _deprecated_shorthand("norm", Norm) 

810 

811 

812@deprecated("2.0", useInstead="picos.PowerTrace") 

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

814 """Legacy shorthand for :class:`~picos.PowerTrace`.""" 

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

816 

817 

818@deprecated("2.0", useInstead="picos.Constant") 

819def new_param(name, value): 

820 """Create a constant or a list or dict or tuple thereof.""" 

821 if isinstance(value, list): 

822 # Handle a vector. 

823 try: 

824 for x in value: 

825 complex(x) 

826 except Exception: 

827 pass 

828 else: 

829 return Constant(name, value) 

830 

831 # Handle a matrix. 

832 if all(isinstance(x, list) for x in value) \ 

833 and all(len(x) == len(value[0]) for x in value): 

834 try: 

835 for x in value: 

836 for y in x: 

837 complex(y) 

838 except Exception: 

839 pass 

840 else: 

841 return Constant(name, value) 

842 

843 # Return a list of constants. 

844 return [Constant(glyphs.slice(name, i), x) for i, x in enumerate(value)] 

845 elif isinstance(value, tuple): 

846 # Return a list of constants. 

847 # NOTE: This is very inconsistent, but legacy behavior. 

848 return [Constant(glyphs.slice(name, i), x) for i, x in enumerate(value)] 

849 elif isinstance(value, dict): 

850 return {k: Constant(glyphs.slice(name, k), x) for k, x in value.items()} 

851 else: 

852 return Constant(name, value) 

853 

854 

855@deprecated("2.0", useInstead="picos.FlowConstraint") 

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

857 """Legacy shorthand for :class:`~picos.FlowConstraint`.""" 

858 from ..constraints.con_flow import FlowConstraint 

859 return FlowConstraint(*args, **kwargs) 

860 

861 

862@deprecated("2.0", useInstead="picos.maindiag") 

863def diag_vect(x): 

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

865 return maindiag(x) 

866 

867 

868@deprecated("2.0", useInstead="picos.Simplex") 

869def simplex(gamma): 

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

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

872 

873 

874@deprecated("2.0", useInstead="picos.Simplex") 

875def truncated_simplex(gamma, sym=False): 

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

877 return Simplex(gamma, truncated=True, symmetrized=sym) 

878 

879 

880# -------------------------------------- 

881__all__ = api_end(_API_START, globals())