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

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

230 

231 return basetype(string, shape, coefs) 

232 

233 theSum = lst[0] 

234 for expression in lst[1:]: 

235 theSum += expression 

236 

237 theSum._symbStr = string 

238 

239 return theSum 

240 

241 

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

243 """Create an affine block matrix expression. 

244 

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. 

249 

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. 

256 

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

261 

262 :param nested: 

263 The blocks. 

264 :type nested: 

265 tuple(tuple) or list(list) 

266 

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) 

273 

274 :param str name: 

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

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

277 

278 :Example: 

279 

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) 

311 

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

315 

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

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

318 

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) 

328 

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) 

332 

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

336 

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

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

339 

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) 

349 

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

355 

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

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

358 

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

363 

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

365 M, N = tuple(M), tuple(N) 

366 

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

370 

371 # Compute the full matrix dimensions. 

372 m = builtins.sum(M) 

373 n = builtins.sum(N) 

374 mn = m*n 

375 

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

382 

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) 

391 

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) 

399 

400 # Obtain all coefficient keys. 

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

402 

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) 

408 

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

413 

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

426 

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

428 

429 

430def max(lst): 

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

432 

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

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

435 

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. 

439 

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 

444 

445 :Example: 

446 

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 

463 

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

469 

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 

475 

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) 

482 

483 

484def min(lst): 

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

486 

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

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

489 

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. 

493 

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 

498 

499 :Example: 

500 

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 

517 

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

523 

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 

529 

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) 

536 

537 

538# ------------------------------------------------------------------------------ 

539# Functions that call expression methods. 

540# ------------------------------------------------------------------------------ 

541 

542 

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) 

548 

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

555 

556 return result 

557 return wrapper 

558 

559 

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 

566 

567 

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 

574 

575 

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 

584 

585 

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

590 

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) 

597 

598 

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 

605 

606 

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 

613 

614 

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

619 

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 

626 

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 

631 

632 if isinstance(x, BiaffineExpression): 

633 return x.partial_trace(subsystems, dimensions) 

634 

635 

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

640 

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 

647 

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 

652 

653 if isinstance(x, BiaffineExpression): 

654 return x.partial_transpose(subsystems, dimensions) 

655 

656 

657# ------------------------------------------------------------------------------ 

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

659# ------------------------------------------------------------------------------ 

660 

661 

662def _shorthand(name, cls): 

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

664 return cls(*args, **kwargs) 

665 

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

667 cls.__module__, cls.__qualname__) 

668 shorthand.__name__ = name 

669 shorthand.__qualname__ = name 

670 

671 return shorthand 

672 

673 

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) 

681 

682 

683def sum_k_largest(x, k): 

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

685 

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

687 

688 :Example: 

689 

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) 

698 

699 

700def sum_k_smallest(x, k): 

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

702 

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

704 

705 :Example: 

706 

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) 

715 

716 

717def lambda_max(x): 

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

719 

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

721 

722 :Example: 

723 

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) 

732 

733 

734def lambda_min(x): 

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

736 

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

738 

739 :Example: 

740 

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) 

749 

750 

751def sum_k_largest_lambda(x, k): 

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

753 

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

755 

756 :Example: 

757 

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) 

766 

767 

768def sum_k_smallest_lambda(x, k): 

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

770 

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

772 

773 :Example: 

774 

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) 

783 

784 

785# ------------------------------------------------------------------------------ 

786# Legacy algebraic functions for backwards compatibility. 

787# ------------------------------------------------------------------------------ 

788 

789 

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

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

792 

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) 

798 

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 

804 

805 return shorthand 

806 

807 

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) 

814 

815 

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) 

820 

821 

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) 

834 

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) 

846 

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) 

857 

858 

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) 

864 

865 

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) 

870 

871 

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) 

876 

877 

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) 

882 

883 

884# -------------------------------------- 

885__all__ = api_end(_API_START, globals())