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

345 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-12 07:53 +0000

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_quantcondentr import QuantumConditionalEntropy 

47from .exp_quantentr import QuantumEntropy, NegativeQuantumEntropy 

48from .exp_quantkeydist import QuantumKeyDistribution 

49from .exp_renyientr import (RenyiEntropy, QuasiEntropy, SandRenyiEntropy, 

50 SandQuasiEntropy) 

51from .exp_oprelentr import OperatorRelativeEntropy 

52from .exp_mtxgeomean import MatrixGeometricMean 

53from .exp_sumexp import SumExponentials 

54from .exp_sumxtr import SumExtremes 

55from .expression import Expression 

56from .set_ball import Ball 

57from .set_simplex import Simplex 

58from .uncertain.uexp_affine import UncertainAffineExpression 

59from .uncertain.uexp_rand_pwl import RandomMaximumAffine, RandomMinimumAffine 

60 

61_API_START = api_start(globals()) 

62# ------------------------------- 

63 

64 

65# ------------------------------------------------------------------------------ 

66# Algebraic functions with a logic of their own. 

67# ------------------------------------------------------------------------------ 

68 

69 

70@functools.lru_cache() 

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

72 """Create a zero matrix. 

73 

74 :Example: 

75 

76 >>> from picos import O 

77 >>> print(O(2, 3)) 

78 [0 0 0] 

79 [0 0 0] 

80 """ 

81 return AffineExpression.zero((rows, cols)) 

82 

83 

84@functools.lru_cache() 

85def I(size=1): # noqa 

86 """Create an identity matrix. 

87 

88 :Example: 

89 

90 >>> from picos import I 

91 >>> print(I(3)) 

92 [ 1.00e+00 0 0 ] 

93 [ 0 1.00e+00 0 ] 

94 [ 0 0 1.00e+00] 

95 """ 

96 return AffineExpression.from_constant("I", (size, size)) 

97 

98 

99@functools.lru_cache() 

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

101 """Create a matrix of all ones. 

102 

103 :Example: 

104 

105 >>> from picos import J 

106 >>> print(J(2, 3)) 

107 [ 1.00e+00 1.00e+00 1.00e+00] 

108 [ 1.00e+00 1.00e+00 1.00e+00] 

109 """ 

110 return AffineExpression.from_constant(1, (rows, cols)) 

111 

112 

113def sum(lst, axis=None): 

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

115 

116 This is a replacement for Python's :func:`sum` that produces sensible string 

117 representations, and in some cases a speedup, when summing over multiple 

118 PICOS expressions. Additionally, this can be used to denote the (complete, 

119 column- or row-) sum over a single matrix expression. 

120 

121 :param None or int axis: 

122 If summing over a single matrix expression, this is the axis over which 

123 the sum is performed: :obj:`None` denotes the sum over all elements, 

124 ``0`` denotes the sum of the rows as a row vector and ``1`` denotes the 

125 sum of the columns as a column vector. If summing multiple expressions, 

126 any value other than :obj:`None` raises a :exc:`ValueError`. 

127 

128 :Example: 

129 

130 >>> import builtins, picos, numpy 

131 >>> x = picos.RealVariable("x", 5) 

132 >>> e = [x[i]*x[i+1] for i in range(len(x) - 1)] 

133 >>> builtins.sum(e) 

134 <Quadratic Expression: x[0]·x[1] + x[1]·x[2] + x[2]·x[3] + x[3]·x[4]> 

135 >>> picos.sum(e) 

136 <Quadratic Expression: ∑(x[i]·x[i+1] : i ∈ [0…3])> 

137 >>> picos.sum(x) # The same as x.sum or (1|x). 

138 <1×1 Real Linear Expression: ∑(x)> 

139 >>> A = picos.Constant("A", range(20), (4, 5)) 

140 >>> picos.sum(A, axis=0) # The same as A.rowsum 

141 <1×5 Real Constant: [1]ᵀ·A> 

142 >>> picos.sum(A, axis=1) # The same as A.colsum 

143 <4×1 Real Constant: A·[1]> 

144 >>> numpy.allclose( # Same axis convention as NumPy. 

145 ... numpy.sum(A.np, axis=0), picos.sum(A, axis=0).np) 

146 True 

147 """ 

148 if isinstance(lst, Expression): 

149 if isinstance(lst, BiaffineExpression): 

150 if axis is None: 

151 return lst.sum 

152 elif not isinstance(axis, (int, numpy.integer)): 

153 raise TypeError( 

154 "Axis must be an integer or None. (To sum multiple " 

155 "expressions, provide an iterable as the first argument.)") 

156 elif axis == 0: 

157 return lst.rowsum 

158 elif axis == 1: 

159 return lst.colsum 

160 else: 

161 raise ValueError("Bad axis: {}.".format(axis)) 

162 else: 

163 raise TypeError( 

164 "PICOS doesn't know how to sum over a single {}." 

165 .format(type(lst).__name__)) 

166 

167 if axis is not None: 

168 raise ValueError( 

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

170 

171 # Allow passing also an iterator instead of an iterable. (The conversion is 

172 # necessary as otherwise the check below would use up the iterator.) 

173 if isinstance(lst, Iterator): 

174 lst = tuple(lst) 

175 

176 # Resort to Python's built-in sum when no summand is a PICOS expression. 

177 if not any(isinstance(expression, Expression) for expression in lst): 

178 return builtins.sum(lst) 

179 

180 # If at least one summand is a PICOS expression, attempt to convert others. 

181 try: 

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

183 except Exception as error: 

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

185 "PICOS constant.") from error 

186 

187 # Handle sums with at most two summands. 

188 if len(lst) == 0: 

189 return O() 

190 elif len(lst) == 1: 

191 return lst[0] 

192 elif len(lst) == 2: 

193 return lst[0] + lst[1] 

194 

195 # Find a suitable string description. 

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

197 

198 # Handle (large) sums of only affine expressions efficiently. 

199 if all(isinstance(expression, BiaffineExpression) for expression in lst): 

200 # Determine resulting shape. 

201 shapes = set(expression.shape for expression in lst) 

202 if len(shapes) != 1: 

203 raise TypeError("The shapes of summands do not match.") 

204 else: 

205 shape = shapes.pop() 

206 

207 # Determine resulting type. 

208 try: 

209 basetype = functools.reduce( 

210 lambda A, B: A._common_basetype(B), lst, AffineExpression) 

211 except Exception as error: 

212 raise TypeError("Could not find a common base type for the given " 

213 "(bi-)affine expressions.") from error 

214 

215 # Sum all coefficient matrices. 

216 # NOTE: BiaffineExpression.__init__ will order mutable pairs and merge 

217 # their coefficient matrices. 

218 coefs = {} 

219 byref = set() 

220 for expression in lst: 

221 for mtbs, coef in expression._coefs.items(): 

222 if mtbs in coefs: 

223 if mtbs in byref: 

224 # Make a copy of the coefficient so we may modify it. 

225 coefs[mtbs] = load_data(coefs[mtbs], alwaysCopy=True)[0] 

226 byref.remove(mtbs) 

227 

228 try: 

229 coefs[mtbs] += coef 

230 except TypeError: 

231 # No in-place addition for sparse and dense types. 

232 coefs[mtbs] = coefs[mtbs] + coef 

233 else: 

234 # Store the first coefficient by reference. 

235 coefs[mtbs] = coef 

236 byref.add(mtbs) 

237 

238 return basetype(string, shape, coefs) 

239 

240 theSum = lst[0] 

241 for expression in lst[1:]: 

242 theSum += expression 

243 

244 theSum._symbStr = string 

245 

246 return theSum 

247 

248 

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

250 """Create an affine block matrix expression. 

251 

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

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

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

255 each expression or constant represents one block. 

256 

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

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

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

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

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

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

263 

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

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

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

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

268 

269 :param nested: 

270 The blocks. 

271 :type nested: 

272 tuple(tuple) or list(list) 

273 

274 :param shapes: 

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

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

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

278 :type shapes: 

279 tuple(tuple) or list(list) 

280 

281 :param str name: 

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

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

284 

285 :Example: 

286 

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

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

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

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

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

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

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

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

295 >>> print(A) 

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

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

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

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

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

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

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

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

304 >>> print(B) 

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

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

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

308 [ 1.00e+00 0.00e+00 0.00e+00] 

309 [ 0.00e+00 1.00e+00 0.00e+00] 

310 """ 

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

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

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

314 for stage in range(2): 

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

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

317 for row in nested], dtype=int) 

318 

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

320 for i, Ri in enumerate(R): 

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

322 

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

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

325 

326 if len(m) > 1: 

327 raise TypeError( 

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

329 .format(i + 1, m)) 

330 elif len(m) == 1: 

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

332 else: 

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

334 M.append(None) 

335 

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

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

338 for row in nested], dtype=int) 

339 

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

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

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

343 

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

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

346 

347 if len(n) > 1: 

348 raise TypeError( 

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

350 .format(j + 1, n)) 

351 elif len(n) == 1: 

352 N.append(n.pop()) 

353 else: 

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

355 N.append(None) 

356 

357 if stage == 0: 

358 nested = [[ 

359 x if isinstance(x, BiaffineExpression) 

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

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

362 

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

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

365 

366 # Find the common base type of all expressions. 

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

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

369 typecode = basetype._get_typecode() 

370 

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

372 M, N = tuple(M), tuple(N) 

373 

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

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

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

377 

378 # Compute the full matrix dimensions. 

379 m = builtins.sum(M) 

380 n = builtins.sum(N) 

381 mn = m*n 

382 

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

384 MLen = len(N) 

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

386 blockOffsets = tuple( 

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

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

389 

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

391 def _I(): 

392 for k, block in enumerate(blocks): 

393 rows, cols = block.shape 

394 i, j = blockOffsets[k] 

395 for c in range(cols): 

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

397 yield range(columnOffset, columnOffset + rows) 

398 

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

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

401 # column-major vectorization of the full matrix. 

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

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

404 J = range(mn) 

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

406 

407 # Obtain all coefficient keys. 

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

409 

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

411 coefs = {} 

412 for mtbs in keys: 

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

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

415 

416 coefs[mtbs] = T*cvxopt_vcat([ 

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

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

419 for block in blocks]) 

420 

421 # Build the string description. 

422 if name: 

423 string = str(name) 

424 elif len(blocks) > 9: 

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

426 else: 

427 string = functools.reduce( 

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

429 (functools.reduce( 

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

431 (block.string for block in blockRow)) 

432 for blockRow in nested)) 

433 

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

435 

436 

437def max(lst): 

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

439 

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

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

442 

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

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

445 implicitly goes over their perturbation parameters as well. 

446 

447 :param lst: 

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

449 :type lst: 

450 list or tuple or ~picos.expressions.AffineExpression 

451 

452 :Example: 

453 

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

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

456 >>> max(x) 

457 <Largest Element: max(x)> 

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

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

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

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

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

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

464 >>> from picos.uncertain import UnitBallPerturbationSet 

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

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

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

468 """ 

469 UAE = UncertainAffineExpression 

470 

471 if isinstance(lst, AffineExpression): 

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

473 elif isinstance(lst, Expression): 

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

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

476 

477 try: 

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

479 except Exception as error: 

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

481 "PICOS constant.") from error 

482 

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

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

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

486 return RandomMaximumAffine(lst) 

487 else: 

488 return MaximumConvex(lst) 

489 

490 

491def min(lst): 

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

493 

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

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

496 

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

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

499 implicitly goes over their perturbation parameters as well. 

500 

501 :param lst: 

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

503 :type lst: 

504 list or tuple or ~picos.expressions.AffineExpression 

505 

506 :Example: 

507 

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

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

510 >>> min(x) 

511 <Smallest Element: min(x)> 

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

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

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

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

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

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

518 >>> from picos.uncertain import UnitBallPerturbationSet 

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

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

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

522 """ 

523 UAE = UncertainAffineExpression 

524 

525 if isinstance(lst, AffineExpression): 

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

527 elif isinstance(lst, Expression): 

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

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

530 

531 try: 

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

533 except Exception as error: 

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

535 "PICOS constant.") from error 

536 

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

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

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

540 return RandomMinimumAffine(lst) 

541 else: 

542 return MinimumConcave(lst) 

543 

544 

545# ------------------------------------------------------------------------------ 

546# Functions that call expression methods. 

547# ------------------------------------------------------------------------------ 

548 

549 

550def _error_on_none(func): 

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

552 @functools.wraps(func) 

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

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

555 

556 if result is None: 

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

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

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

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

561 ))) 

562 

563 return result 

564 return wrapper 

565 

566 

567@_error_on_none 

568@convert_and_refine_arguments("x") 

569def exp(x): 

570 """Denote the exponential.""" 

571 if hasattr(x, "exp"): 

572 return x.exp 

573 

574 

575@_error_on_none 

576@convert_and_refine_arguments("x") 

577def log(x): 

578 """Denote the natural logarithm.""" 

579 if hasattr(x, "log"): 

580 return x.log 

581 

582 

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

584 " @ instead.") 

585@_error_on_none 

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

587def kron(x, y): 

588 """Denote the kronecker product.""" 

589 if hasattr(x, "kron"): 

590 return x @ y 

591 

592 

593@_error_on_none 

594@convert_and_refine_arguments("x") 

595def diag(x, n=1): 

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

597 

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

599 """ 

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

601 return x.diag 

602 elif hasattr(x, "dupdiag"): 

603 return x.dupdiag(n) 

604 

605 

606@_error_on_none 

607@convert_and_refine_arguments("x") 

608def maindiag(x): 

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

610 if hasattr(x, "maindiag"): 

611 return x.maindiag 

612 

613 

614@_error_on_none 

615@convert_and_refine_arguments("x") 

616def trace(x): 

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

618 if hasattr(x, "tr"): 

619 return x.tr 

620 

621 

622@_error_on_none 

623@convert_and_refine_arguments("x") 

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

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

626 

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

628 """ 

629 if k is not None: 

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

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

632 subsystems = k 

633 

634 if dim is not None: 

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

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

637 dimensions = dim 

638 

639 if isinstance(x, BiaffineExpression): 

640 return x.partial_trace(subsystems, dimensions) 

641 

642 

643@_error_on_none 

644@convert_and_refine_arguments("x") 

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

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

647 

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

649 """ 

650 if k is not None: 

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

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

653 subsystems = k 

654 

655 if dim is not None: 

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

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

658 dimensions = dim 

659 

660 if isinstance(x, BiaffineExpression): 

661 return x.partial_transpose(subsystems, dimensions) 

662 

663 

664# ------------------------------------------------------------------------------ 

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

666# ------------------------------------------------------------------------------ 

667 

668 

669def _shorthand(name, cls): 

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

671 return cls(*args, **kwargs) 

672 

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

674 cls.__module__, cls.__qualname__) 

675 shorthand.__name__ = name 

676 shorthand.__qualname__ = name 

677 

678 return shorthand 

679 

680 

681expcone = _shorthand("expcone", ExponentialCone) 

682geomean = _shorthand("geomean", GeometricMean) 

683kldiv = _shorthand("kldiv", NegativeEntropy) 

684lse = _shorthand("lse", LogSumExp) 

685rsoc = _shorthand("rsoc", RotatedSecondOrderCone) 

686soc = _shorthand("soc", SecondOrderCone) 

687sumexp = _shorthand("sumexp", SumExponentials) 

688 

689quantentr = _shorthand("quantentr", QuantumEntropy) 

690quantrelentr = _shorthand("quantrelentr", NegativeQuantumEntropy) 

691quantcondentr = _shorthand("quantcondentr", QuantumConditionalEntropy) 

692quantkeydist = _shorthand("quantkeydist", QuantumKeyDistribution) 

693oprelentr = _shorthand("oprelentr", OperatorRelativeEntropy) 

694mtxgeomean = _shorthand("oprelentr", MatrixGeometricMean) 

695 

696renyientr = _shorthand("renyientr", RenyiEntropy) 

697quasientr = _shorthand("quasientr", QuasiEntropy) 

698sandrenyientr = _shorthand("sandrenyientr", SandRenyiEntropy) 

699sandquasientr = _shorthand("sandquasientr", SandQuasiEntropy) 

700 

701def sum_k_largest(x, k): 

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

703 

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

705 

706 :Example: 

707 

708 >>> from picos import RealVariable, sum_k_largest 

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

710 >>> sum_k_largest(x, 2) 

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

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

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

714 """ 

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

716 

717 

718def sum_k_smallest(x, k): 

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

720 

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

722 

723 :Example: 

724 

725 >>> from picos import RealVariable, sum_k_smallest 

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

727 >>> sum_k_smallest(x, 2) 

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

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

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

731 """ 

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

733 

734 

735def lambda_max(x): 

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

737 

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

739 

740 :Example: 

741 

742 >>> from picos import SymmetricVariable, lambda_max 

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

744 >>> lambda_max(X) 

745 <Largest Eigenvalue: λ_max(X)> 

746 >>> lambda_max(X) <= 2 

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

748 """ 

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

750 

751 

752def lambda_min(x): 

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

754 

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

756 

757 :Example: 

758 

759 >>> from picos import SymmetricVariable, lambda_min 

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

761 >>> lambda_min(X) 

762 <Smallest Eigenvalue: λ_min(X)> 

763 >>> lambda_min(X) >= 2 

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

765 """ 

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

767 

768 

769def sum_k_largest_lambda(x, k): 

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

771 

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

773 

774 :Example: 

775 

776 >>> from picos import SymmetricVariable, sum_k_largest_lambda 

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

778 >>> sum_k_largest_lambda(X, 2) 

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

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

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

782 """ 

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

784 

785 

786def sum_k_smallest_lambda(x, k): 

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

788 

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

790 

791 :Example: 

792 

793 >>> from picos import SymmetricVariable, sum_k_smallest_lambda 

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

795 >>> sum_k_smallest_lambda(X, 2) 

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

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

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

799 """ 

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

801 

802 

803# ------------------------------------------------------------------------------ 

804# Legacy algebraic functions for backwards compatibility. 

805# ------------------------------------------------------------------------------ 

806 

807 

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

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

810 

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

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

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

814 """|PLACEHOLDER|""" # noqa 

815 return cls(*args, **kwargs) 

816 

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

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

819 cls.__module__, cls.__qualname__)) 

820 shorthand.__name__ = name 

821 shorthand.__qualname__ = name 

822 

823 return shorthand 

824 

825 

826ball = _deprecated_shorthand("ball", Ball) 

827detrootn = _deprecated_shorthand("detrootn", DetRootN) 

828kullback_leibler = _deprecated_shorthand( 

829 "kullback_leibler", NegativeEntropy, "kldiv") 

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

831norm = _deprecated_shorthand("norm", Norm) 

832 

833 

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

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

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

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

838 

839 

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

841def new_param(name, value): 

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

843 if isinstance(value, list): 

844 # Handle a vector. 

845 try: 

846 for x in value: 

847 complex(x) 

848 except Exception: 

849 pass 

850 else: 

851 return Constant(name, value) 

852 

853 # Handle a matrix. 

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

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

856 try: 

857 for x in value: 

858 for y in x: 

859 complex(y) 

860 except Exception: 

861 pass 

862 else: 

863 return Constant(name, value) 

864 

865 # Return a list of constants. 

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

867 elif isinstance(value, tuple): 

868 # Return a list of constants. 

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

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

871 elif isinstance(value, dict): 

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

873 else: 

874 return Constant(name, value) 

875 

876 

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

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

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

880 from ..constraints.con_flow import FlowConstraint 

881 return FlowConstraint(*args, **kwargs) 

882 

883 

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

885def diag_vect(x): 

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

887 return maindiag(x) 

888 

889 

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

891def simplex(gamma): 

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

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

894 

895 

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

897def truncated_simplex(gamma, sym=False): 

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

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

900 

901 

902# -------------------------------------- 

903__all__ = api_end(_API_START, globals())