Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# ------------------------------------------------------------------------------ 

2# Copyright (C) 2012-2017 Guillaume Sagnol 

3# Copyright (C) 2017-2020 Maximilian Stahlberg 

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"""Implementation of :class:`Problem`.""" 

21 

22import copy as pycopy 

23import re 

24import time 

25from collections import OrderedDict 

26from functools import lru_cache 

27from textwrap import TextWrapper 

28from types import MappingProxyType 

29 

30import cvxopt as cvx 

31import numpy as np 

32 

33from .. import constraints, expressions, glyphs, settings 

34from ..apidoc import api_end, api_start 

35from ..expressions.data import cvx2np 

36from ..expressions.uncertain import IntractableWorstCase 

37from ..expressions.variables import BaseVariable 

38from ..formatting import natsorted, parameterized_string, picos_box 

39from ..legacy import deprecated, map_legacy_options, throw_deprecation_warning 

40from ..solvers import Solver, get_solver 

41from .file_out import write 

42from .footprint import Footprint, Specification 

43from .objective import Objective 

44from .options import Options 

45from .solution import SS_OPTIMAL, Solution 

46 

47_API_START = api_start(globals()) 

48# ------------------------------- 

49 

50 

51class SolutionFailure(RuntimeError): 

52 """Solving the problem failed.""" 

53 

54 def __init__(self, code, message): 

55 """Construct a :exc:`SolutionFailure`. 

56 

57 :param int code: 

58 Status code, as defined in :meth:`Problem.solve`. 

59 

60 :param str message: 

61 Text description of the failure. 

62 """ 

63 #: Status code, as defined in :meth:`Problem.solve`. 

64 self.code = code 

65 

66 #: Text description of the failure. 

67 self.message = message 

68 

69 def __str__(self): 

70 return "Code {}: {}".format(self.code, self.message) 

71 

72 

73class Problem(): 

74 """PICOS' representation of an optimization problem. 

75 

76 :Example: 

77 

78 >>> from picos import Problem, RealVariable 

79 >>> X = RealVariable("X", (2,2), lower = 0) 

80 >>> P = Problem() 

81 >>> P.set_objective("max", X.tr) 

82 >>> C1 = P.add_constraint(X.sum <= 10) 

83 >>> C2 = P.add_constraint(X[0,0] == 1) 

84 >>> print(P) 

85 Linear Program 

86 maximize tr(X) 

87 over 

88 2×2 real variable X (bounded below) 

89 subject to 

90 ∑(X) ≤ 10 

91 X[0,0] = 1 

92 >>> # PICOS will select a suitable solver if you don't specify one. 

93 >>> solution = P.solve(solver = "cvxopt") 

94 >>> solution.claimedStatus 

95 'optimal' 

96 >>> solution.searchTime #doctest: +SKIP 

97 0.002137422561645508 

98 >>> round(P, 1) 

99 10.0 

100 >>> print(X) #doctest: +SKIP 

101 [ 1.00e+00 4.89e-10] 

102 [ 4.89e-10 9.00e+00] 

103 >>> round(C1.dual, 1) 

104 1.0 

105 """ 

106 

107 #: The specification for problems returned by :meth:`conic_form`. 

108 CONIC_FORM = Specification( 

109 objectives=[expressions.AffineExpression], 

110 constraints=[C for C in 

111 (getattr(constraints, Cname) for Cname in constraints.__all__) 

112 if issubclass(C, constraints.ConicConstraint) 

113 and C is not constraints.ConicConstraint]) 

114 

115 # -------------------------------------------------------------------------- 

116 # Initialization and reset methods. 

117 # -------------------------------------------------------------------------- 

118 

119 def __init__(self, copyOptions=None, useOptions=None, **extra_options): 

120 """Create an empty problem and optionally set initial solver options. 

121 

122 :param copyOptions: 

123 An :class:`Options <picos.Options>` object to copy instead of using 

124 the default options. 

125 

126 :param useOptions: An :class:`Options <picos.Options>` object to use 

127 (without making a copy) instead of using the default options. 

128 

129 :param extra_options: 

130 A sequence of additional solver options to apply on top of the 

131 default options or those given by ``copyOptions`` or ``useOptions``. 

132 """ 

133 if copyOptions and useOptions: 

134 raise ValueError( 

135 "Can only copy or use existing solver options, not both.") 

136 

137 extra_options = map_legacy_options(**extra_options) 

138 

139 if copyOptions: 

140 self._options = copyOptions.copy() 

141 self._options.update(**extra_options) 

142 elif useOptions: 

143 self._options = useOptions 

144 self._options.update(**extra_options) 

145 else: 

146 self._options = Options(**extra_options) 

147 

148 #: The optimization objective. 

149 self._objective = Objective() 

150 

151 #: Maps constraint IDs to constraints. 

152 self._constraints = OrderedDict() 

153 

154 #: Contains lists of constraints added together, all in order. 

155 self._con_groups = [] 

156 

157 #: Maps mutables to number of occurences in objective or constraints. 

158 self._mtb_count = {} 

159 

160 #: Maps mutable names to mutables. 

161 self._mutables = OrderedDict() 

162 

163 #: Maps variable names to variables. 

164 self._variables = OrderedDict() 

165 

166 #: Maps parameter names to parameters. 

167 self._parameters = OrderedDict() 

168 

169 #: Current solution strategy. 

170 self._strategy = None 

171 

172 #: The last :class:`Solution` applied to the problem. 

173 self._last_solution = None # Set by Solution.apply. 

174 

175 def _reset_mutable_registry(self): 

176 self._mtb_count.clear() 

177 self._mutables.clear() 

178 self._variables.clear() 

179 self._parameters.clear() 

180 

181 def reset(self, resetOptions=False): 

182 """Reset the problem instance to its initial empty state. 

183 

184 :param bool resetOptions: 

185 Whether also solver options should be reset to their default values. 

186 """ 

187 # Reset options if requested. 

188 if resetOptions: 

189 self._options.reset() 

190 

191 # Reset objective to "find an assignment". 

192 del self.objective 

193 

194 # Reset constraint registry. 

195 self._constraints.clear() 

196 self._con_groups.clear() 

197 

198 # Reset mutable registry. 

199 self._reset_mutable_registry() 

200 

201 # Reset strategy and solution data. 

202 self._strategy = None 

203 self._last_solution = None 

204 

205 # -------------------------------------------------------------------------- 

206 # Properties. 

207 # -------------------------------------------------------------------------- 

208 

209 @property 

210 def mutables(self): 

211 """Maps names to variables and parameters in use by the problem. 

212 

213 :returns: 

214 A read-only view to an :class:`~collections.OrderedDict`. The order 

215 is deterministic and depends on the order of operations performed on 

216 the :class:`Problem` instance as well as on the mutables' names. 

217 """ 

218 return MappingProxyType(self._mutables) 

219 

220 @property 

221 def variables(self): 

222 """Maps names to variables in use by the problem. 

223 

224 :returns: 

225 See :attr:`mutables`. 

226 """ 

227 return MappingProxyType(self._variables) 

228 

229 @property 

230 def parameters(self): 

231 """Maps names to parameters in use by the problem. 

232 

233 :returns: 

234 See :attr:`mutables`. 

235 """ 

236 return MappingProxyType(self._parameters) 

237 

238 @property 

239 def constraints(self): 

240 """Maps constraint IDs to constraints that are part of the problem. 

241 

242 :returns: 

243 A read-only view to an :class:`~collections.OrderedDict`. The order 

244 is that in which constraints were added. 

245 """ 

246 return MappingProxyType(self._constraints) 

247 

248 @constraints.deleter 

249 def constraints(self): 

250 # Clear constraint registry. 

251 self._constraints.clear() 

252 self._con_groups.clear() 

253 

254 # Update mutable registry. 

255 self._reset_mutable_registry() 

256 self._register_mutables(self.no.function.mutables) 

257 

258 @property 

259 def objective(self): 

260 """Optimization objective as an :class:`~picos.Objective` instance.""" 

261 return self._objective 

262 

263 @objective.setter 

264 def objective(self, value): 

265 self._unregister_mutables(self.no.function.mutables) 

266 

267 try: 

268 if isinstance(value, Objective): 

269 self._objective = value 

270 else: 

271 direction, function = value 

272 self._objective = Objective(direction, function) 

273 finally: 

274 self._register_mutables(self.no.function.mutables) 

275 

276 @objective.deleter 

277 def objective(self): 

278 self._unregister_mutables(self.no.function.mutables) 

279 

280 self._objective = Objective() 

281 

282 @property 

283 def no(self): 

284 """Normalized objective as an :class:`~picos.Objective` instance. 

285 

286 Either a minimization or a maximization objective, with feasibility 

287 posed as "minimize 0". 

288 

289 The same as the :attr:`~.objective.Objective.normalized` attribute of 

290 the :attr:`objective`. 

291 """ 

292 return self._objective.normalized 

293 

294 @property 

295 def minimize(self): 

296 """Minimization objective as an :class:`~.expression.Expression`. 

297 

298 This can be used to set a minimization objective. For querying the 

299 objective, it is recommended to use :attr:`objective` instead. 

300 """ 

301 if self._objective.direction == Objective.MIN: 

302 return self._objective.function 

303 else: 

304 raise ValueError("Objective direction is not minimize.") 

305 

306 @minimize.setter 

307 def minimize(self, value): 

308 self.objective = "min", value 

309 

310 @minimize.deleter 

311 def minimize(self): 

312 if self._objective.direction == Objective.MIN: 

313 del self.objective 

314 else: 

315 raise ValueError("Objective direction is not minimize.") 

316 

317 @property 

318 def maximize(self): 

319 """Maximization objective as an :class:`~.expression.Expression`. 

320 

321 This can be used to set a maximization objective. For querying the 

322 objective, it is recommended to use :attr:`objective` instead. 

323 """ 

324 if self._objective.direction == Objective.MAX: 

325 return self._objective.function 

326 else: 

327 raise ValueError("Objective direction is not maximize.") 

328 

329 @maximize.setter 

330 def maximize(self, value): 

331 self.objective = "max", value 

332 

333 @maximize.deleter 

334 def maximize(self): 

335 if self._objective.direction == Objective.MAX: 

336 del self.objective 

337 else: 

338 raise ValueError("Objective direction is not maximize.") 

339 

340 @property 

341 def value(self): 

342 """Objective function value. 

343 

344 If all mutables that appear in the objective function are valued, in 

345 particular after a successful solution search, this is the numeric value 

346 of the objective function. If the objective function is not fully valued 

347 or if the problem is a feasiblity problem without an objective function, 

348 this is :obj:`None`. 

349 

350 In the case of an uncertain objective, this is the worst-case (expected) 

351 objective value. 

352 

353 :raises picos.uncertain.IntractableWorstCase: 

354 When computing the worst-case (expected) value of an uncertain 

355 objective is not supported. 

356 

357 .. note:: 

358 

359 The Python special functions :class:`int`, :class:`float`, 

360 :class:`complex` and :func:`round` as well as the special method 

361 ``__index__`` make use of this value when applied to a 

362 :class:`Problem`. 

363 """ 

364 return self._objective.value 

365 

366 @property 

367 def options(self): 

368 """Solution search parameters as an :class:`~picos.Options` object.""" 

369 return self._options 

370 

371 @options.setter 

372 def options(self, value): 

373 if not isinstance(value, Options): 

374 raise TypeError("Cannot assign an object of type {} as a problem's " 

375 " options.".format(type(value).__name__)) 

376 

377 self._options = value 

378 

379 @options.deleter 

380 def options(self, value): 

381 self._options.reset() 

382 

383 @property 

384 def strategy(self): 

385 """Solution strategy as a :class:`~picos.modeling.Strategy` object. 

386 

387 A strategy is available once you order the problem to be solved and it 

388 will be reused for successive solution attempts (of a modified problem) 

389 while it remains valid with respect to the problem's :attr:`footprint`. 

390 

391 When a strategy is reused, modifications to the objective and 

392 constraints of a problem are passed step by step through the strategy's 

393 reformulation pipeline while existing reformulation work is not 

394 repeated. If the solver also supports these kinds of updates, then 

395 modifying and re-solving a problem can be much faster than solving the 

396 problem from scratch. 

397 

398 :Example: 

399 

400 >>> from picos import Problem, RealVariable 

401 >>> x = RealVariable("x", 2) 

402 >>> P = Problem() 

403 >>> P.set_objective("min", abs(x)**2) 

404 >>> print(P.strategy) 

405 None 

406 >>> sol = P.solve(solver = "cvxopt") # Creates a solution strategy. 

407 >>> print(P.strategy) 

408 1. ExtraOptions 

409 2. EpigraphReformulation 

410 3. SquaredNormToConicReformulation 

411 4. CVXOPTSolver 

412 >>> # Add another constraint handled by SquaredNormToConicReformulation: 

413 >>> P.add_constraint(abs(x - 2)**2 <= 1) 

414 <Squared Norm Constraint: ‖x - [2]‖² ≤ 1> 

415 >>> P.strategy.valid(solver = "cvxopt") 

416 True 

417 >>> P.strategy.valid(solver = "glpk") 

418 False 

419 >>> sol = P.solve(solver = "cvxopt") # Reuses the strategy. 

420 

421 It's also possible to create a startegy from scratch: 

422 

423 >>> from picos.modeling import Strategy 

424 >>> from picos.reforms import (EpigraphReformulation, 

425 ... ConvexQuadraticToConicReformulation) 

426 >>> from picos.solvers import CVXOPTSolver 

427 >>> # Mimic what solve() does when no strategy exists: 

428 >>> P.strategy = Strategy(P, CVXOPTSolver, EpigraphReformulation, 

429 ... ConvexQuadraticToConicReformulation) 

430 """ 

431 return self._strategy 

432 

433 @strategy.setter 

434 def strategy(self, value): 

435 from .strategy import Strategy 

436 

437 if not isinstance(value, Strategy): 

438 raise TypeError( 

439 "Cannot assign an object of type {} as a solution strategy." 

440 .format(type(value).__name__)) 

441 

442 if value.problem is not self: 

443 raise ValueError("The solution strategy was constructed for a " 

444 "different problem.") 

445 

446 self._strategy = value 

447 

448 @strategy.deleter 

449 def strategy(self): 

450 self._strategy = None 

451 

452 @property 

453 def last_solution(self): 

454 """The last :class:`~picos.Solution` applied to the problem.""" 

455 return self._last_solution 

456 

457 @property 

458 def status(self): 

459 """The solution status string as claimed by :attr:`last_solution`.""" 

460 if not self._last_solution: 

461 return "unsolved" 

462 else: 

463 return self._last_solution.claimedStatus 

464 

465 @property 

466 def footprint(self): 

467 """Problem footprint as a :class:`~picos.modeling.Footprint` object.""" 

468 return Footprint.from_problem(self) 

469 

470 @property 

471 def continuous(self): 

472 """Whether all variables are of continuous types.""" 

473 return all( 

474 isinstance(variable, expressions.CONTINUOUS_VARTYPES) 

475 for variable in self._variables.values()) 

476 

477 @property 

478 def pure_integer(self): 

479 """Whether all variables are of integral types.""" 

480 return not any( 

481 isinstance(variable, expressions.CONTINUOUS_VARTYPES) 

482 for variable in self._variables.values()) 

483 

484 @property 

485 def type(self): 

486 """The problem type as a string, such as "Linear Program".""" 

487 C = set(type(c) for c in self._constraints.values()) 

488 objective = self._objective.function 

489 base = "Optimization Problem" 

490 

491 linear = [ 

492 constraints.AffineConstraint, 

493 constraints.ComplexAffineConstraint, 

494 constraints.AbsoluteValueConstraint, 

495 constraints.SimplexConstraint, 

496 constraints.FlowConstraint] 

497 sdp = [ 

498 constraints.LMIConstraint, 

499 constraints.ComplexLMIConstraint] 

500 quadratic = [ 

501 constraints.ConvexQuadraticConstraint, 

502 constraints.ConicQuadraticConstraint, 

503 constraints.NonconvexQuadraticConstraint] 

504 quadconic = [ 

505 constraints.SOCConstraint, 

506 constraints.RSOCConstraint] 

507 exponential = [ 

508 constraints.ExpConeConstraint, 

509 constraints.SumExponentialsConstraint, 

510 constraints.LogSumExpConstraint, 

511 constraints.LogConstraint, 

512 constraints.KullbackLeiblerConstraint] 

513 complex = [ 

514 constraints.ComplexAffineConstraint, 

515 constraints.ComplexLMIConstraint] 

516 

517 if objective is None: 

518 if not C: 

519 base = "Empty Problem" 

520 elif C.issubset(set(linear)): 

521 base = "Linear Feasibility Problem" 

522 else: 

523 base = "Feasibility Problem" 

524 elif isinstance(objective, expressions.AffineExpression): 

525 if not C: 

526 if objective.constant: 

527 base = "Constant Problem" 

528 else: 

529 base = "Linear Program" # Could have variable bounds. 

530 elif C.issubset(set(linear)): 

531 base = "Linear Program" 

532 elif C.issubset(set(linear + quadconic)): 

533 base = "Second Order Cone Program" 

534 elif C.issubset(set(linear + sdp)): 

535 base = "Semidefinite Program" 

536 elif C.issubset(set(linear + [constraints.LogSumExpConstraint])): 

537 base = "Geometric Program" 

538 elif C.issubset(set(linear + exponential)): 

539 base = "Exponential Program" 

540 elif C.issubset(set(linear + quadratic)): 

541 base = "Quadratically Constrained Program" 

542 elif isinstance(objective, expressions.QuadraticExpression): 

543 if C.issubset(set(linear)): 

544 base = "Quadratic Program" 

545 elif C.issubset(set(linear + quadratic)): 

546 base = "Quadratically Constrained Quadratic Program" 

547 elif isinstance(objective, expressions.LogSumExp): 

548 if C.issubset(set(linear + [constraints.LogSumExpConstraint])): 

549 base = "Geometric Program" 

550 

551 if self.continuous: 

552 integrality = "" 

553 elif self.pure_integer: 

554 integrality = "Integer " 

555 else: 

556 integrality = "Mixed-Integer " 

557 

558 if any(c in complex for c in C): 

559 complexity = "Complex " 

560 else: 

561 complexity = "" 

562 

563 return "{}{}{}".format(complexity, integrality, base) 

564 

565 @property 

566 def dual(self): 

567 """The Lagrangian dual problem of the standardized problem. 

568 

569 More precisely, this property invokes the following: 

570 

571 1. The primal problem is posed as an equivalent conic standard form 

572 minimization problem, with variable bounds expressed as additional 

573 constraints. 

574 2. The Lagrangian dual problem of the reposed primal is computed. 

575 3. The optimization direction and objective function sign of the dual 

576 are adjusted such that, given strong duality and primal feasibility, 

577 the optimal values of both problems are equal. In particular, if the 

578 primal problem is a minimization or a maximization problem, the dual 

579 problem returned will be the respective other. 

580 

581 :raises ~picos.modeling.strategy.NoStrategyFound: 

582 If no reformulation strategy was found. 

583 

584 .. note:: 

585 

586 This property is intended for educational purposes. 

587 If you want to solve the primal problem via its dual, use the 

588 :ref:`dualize <option_dualize>` option instead. 

589 """ 

590 from ..reforms import Dualization 

591 return self.reformulated(Dualization.SUPPORTED, dualize=True) 

592 

593 @property 

594 def conic_form(self): 

595 """The problem in conic form. 

596 

597 Reformulates the problem such that the objective is affine and all 

598 constraints are :class:`~.constraints.ConicConstraint` instances. 

599 

600 :raises ~picos.modeling.strategy.NoStrategyFound: 

601 If no reformulation strategy was found. 

602 

603 :Example: 

604 

605 >>> from picos import Problem, RealVariable 

606 >>> x = RealVariable("x", 2) 

607 >>> P = Problem() 

608 >>> P.set_objective("min", abs(x)**2) 

609 >>> print(P) 

610 Quadratic Program 

611 minimize ‖x‖² 

612 over 

613 2×1 real variable x 

614 >>> print(P.conic_form)# doctest: +ELLIPSIS 

615 Second Order Cone Program 

616 minimize __..._t 

617 over 

618 1×1 real variable __..._t 

619 2×1 real variable x 

620 subject to 

621 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0 

622 

623 .. note:: 

624 

625 This property is intended for educational purposes. 

626 You do not need to use it when solving a problem as PICOS will 

627 perform the necessary reformulations automatically. 

628 """ 

629 return self.reformulated(self.CONIC_FORM) 

630 

631 # -------------------------------------------------------------------------- 

632 # Python special methods, except __init__. 

633 # -------------------------------------------------------------------------- 

634 

635 @property 

636 def _var_groups(self): 

637 """Support :meth:`__str__`.""" 

638 vars_by_type = {} 

639 for var in self._variables.values(): 

640 vtype = type(var).__name__ 

641 shape = var.shape 

642 bound = tuple(bool(bound) for bound in var.bound_dicts) 

643 index = (vtype, shape, bound) 

644 

645 vars_by_type.setdefault(index, set()) 

646 vars_by_type[index].add(var) 

647 

648 groups = [] 

649 for index in sorted(vars_by_type.keys()): 

650 groups.append(natsorted(vars_by_type[index], key=lambda v: v.name)) 

651 

652 return groups 

653 

654 @property 

655 def _prm_groups(self): 

656 """Support :meth:`__str__`.""" 

657 prms_by_type = {} 

658 for prm in self._parameters.values(): 

659 vtype = type(prm).__name__ 

660 shape = prm.shape 

661 index = (vtype, shape) 

662 

663 prms_by_type.setdefault(index, set()) 

664 prms_by_type[index].add(prm) 

665 

666 groups = [] 

667 for index in sorted(prms_by_type.keys()): 

668 groups.append(natsorted(prms_by_type[index], key=lambda v: v.name)) 

669 

670 return groups 

671 

672 @lru_cache() 

673 def _mtb_group_string(self, group): 

674 """Support :meth:`__str__`.""" 

675 if len(group) == 0: 

676 return "[no mutables]" 

677 

678 if len(group) == 1: 

679 return group[0].long_string 

680 

681 try: 

682 template, data = parameterized_string( 

683 [mtb.long_string for mtb in group]) 

684 except ValueError: 

685 return group[0].long_string \ 

686 + ", " + ", ".join([v.name for v in group[1:]]) 

687 else: 

688 return glyphs.forall(template, data) 

689 

690 @lru_cache() 

691 def _con_group_string(self, group): 

692 """Support :meth:`__str__`.""" 

693 if len(group) == 0: 

694 return "[no constraints]" 

695 

696 if len(group) == 1: 

697 return str(group[0]) 

698 

699 try: 

700 template, data = parameterized_string([str(con) for con in group]) 

701 except ValueError: 

702 return "[{} constraints (1st: {})]".format(len(group), group[0]) 

703 else: 

704 return glyphs.forall(template, data) 

705 

706 def __repr__(self): 

707 return glyphs.repr1(self.type) 

708 

709 def __str__(self): 

710 # Print problem type. 

711 string = "{}\n".format(self.type) 

712 

713 # Print objective. 

714 string += " {}\n".format(self._objective) 

715 

716 wrapper = TextWrapper( 

717 initial_indent=" "*4, 

718 subsequent_indent=" "*6, 

719 break_long_words=False, 

720 break_on_hyphens=False) 

721 

722 # Print variables. 

723 if self._variables: 

724 string += " {}\n".format( 

725 "for" if self._objective.direction == "find" else "over") 

726 for group in self._var_groups: 

727 string += wrapper.fill(self._mtb_group_string(tuple(group))) 

728 string += "\n" 

729 

730 # Print constraints. 

731 if self._constraints: 

732 string += " subject to\n" 

733 for index, group in enumerate(self._con_groups): 

734 string += wrapper.fill(self._con_group_string(tuple(group))) 

735 string += "\n" 

736 

737 # Print parameters. 

738 if self._parameters: 

739 string += " given\n" 

740 for group in self._prm_groups: 

741 string += wrapper.fill(self._mtb_group_string(tuple(group))) 

742 string += "\n" 

743 

744 return string.rstrip("\n") 

745 

746 def __index__(self): 

747 return self._objective.__index__() 

748 

749 def __int__(self): 

750 return self._objective.__int__() 

751 

752 def __float__(self): 

753 return self._objective.__float__() 

754 

755 def __complex__(self): 

756 return self._objective.__complex__() 

757 

758 def __round__(self, ndigits=None): 

759 return self._objective.__round__(ndigits) 

760 

761 def __iadd__(self, constraints): 

762 if isinstance(constraints, tuple): 

763 self.require(*constraints) 

764 else: 

765 self.require(constraints) 

766 

767 return self 

768 

769 # -------------------------------------------------------------------------- 

770 # Bookkeeping methods. 

771 # -------------------------------------------------------------------------- 

772 

773 def _register_mutables(self, mtbs): 

774 """Register the mutables of an objective function or constraint.""" 

775 # Register every mutable at most once per call. 

776 if not isinstance(mtbs, (set, frozenset)): 

777 raise TypeError("Mutable registry can (un)register a mutable " 

778 "only once per call, so the argument must be a set type.") 

779 

780 # Retrieve old and new mutables as mapping from name to object. 

781 old_mtbs = self._mutables 

782 new_mtbs = OrderedDict( 

783 (mtb.name, mtb) for mtb in sorted(mtbs, key=(lambda m: m.name))) 

784 new_vars = OrderedDict((name, mtb) for name, mtb in new_mtbs.items() 

785 if isinstance(mtb, BaseVariable)) 

786 new_prms = OrderedDict((name, mtb) for name, mtb in new_mtbs.items() 

787 if not isinstance(mtb, BaseVariable)) 

788 

789 # Check for mutable name clashes within the new set. 

790 if len(new_mtbs) != len(mtbs): 

791 raise ValueError( 

792 "The object you are trying to add to a problem contains " 

793 "multiple mutables of the same name. This is not allowed.") 

794 

795 # Check for mutable name clashes with existing mutables. 

796 for name in set(old_mtbs).intersection(set(new_mtbs)): 

797 if old_mtbs[name] is not new_mtbs[name]: 

798 raise ValueError("Cannot register the mutable {} with the " 

799 "problem because it already tracks another mutable with " 

800 "the same name.".format(name)) 

801 

802 # Keep track of new mutables. 

803 self._mutables.update(new_mtbs) 

804 self._variables.update(new_vars) 

805 self._parameters.update(new_prms) 

806 

807 # Count up the mutable references. 

808 for mtb in mtbs: 

809 self._mtb_count.setdefault(mtb, 0) 

810 self._mtb_count[mtb] += 1 

811 

812 def _unregister_mutables(self, mtbs): 

813 """Unregister the mutables of an objective function or constraint.""" 

814 # Unregister every mutable at most once per call. 

815 if not isinstance(mtbs, (set, frozenset)): 

816 raise TypeError("Mutable registry can (un)register a mutable " 

817 "only once per call, so the argument must be a set type.") 

818 

819 for mtb in mtbs: 

820 name = mtb.name 

821 

822 # Make sure the mutable is properly registered. 

823 assert name in self._mutables and mtb in self._mtb_count, \ 

824 "Tried to unregister a mutable that is not registered." 

825 assert self._mtb_count[mtb] >= 1, \ 

826 "Found a nonpostive mutable count." 

827 

828 # Count down the mutable references. 

829 self._mtb_count[mtb] -= 1 

830 

831 # Remove a mutable with a reference count of zero. 

832 if not self._mtb_count[mtb]: 

833 self._mtb_count.pop(mtb) 

834 self._mutables.pop(name) 

835 

836 if isinstance(mtb, BaseVariable): 

837 self._variables.pop(name) 

838 else: 

839 self._parameters.pop(name) 

840 

841 # -------------------------------------------------------------------------- 

842 # Methods to manipulate the objective function and its direction. 

843 # -------------------------------------------------------------------------- 

844 

845 def set_objective(self, direction=None, expression=None): 

846 """Set the optimization direction and objective function of the problem. 

847 

848 :param str direction: 

849 Case insensitive search direction string. One of 

850 

851 - ``"min"`` or ``"minimize"``, 

852 - ``"max"`` or ``"maximize"``, 

853 - ``"find"`` or :obj:`None` (for a feasibility problem). 

854 

855 :param ~picos.expressions.Expression expression: 

856 The objective function. Must be :obj:`None` for a feasibility 

857 problem. 

858 """ 

859 self.objective = direction, expression 

860 

861 # -------------------------------------------------------------------------- 

862 # Methods to add, retrieve and remove constraints. 

863 # -------------------------------------------------------------------------- 

864 

865 def _lookup_constraint(self, idOrIndOrCon): 

866 """Look for a constraint with the given identifier. 

867 

868 Given a constraint object or ID or offset or a constraint group index or 

869 index pair, returns a matching (list of) constraint ID(s) that is (are) 

870 part of the problem. 

871 """ 

872 if isinstance(idOrIndOrCon, int): 

873 if idOrIndOrCon in self._constraints: 

874 # A valid ID. 

875 return idOrIndOrCon 

876 elif idOrIndOrCon < len(self._constraints): 

877 # An offset. 

878 return list(self._constraints.keys())[idOrIndOrCon] 

879 else: 

880 raise LookupError( 

881 "The problem has no constraint with ID or offset {}." 

882 .format(idOrIndOrCon)) 

883 elif isinstance(idOrIndOrCon, constraints.Constraint): 

884 # A constraint object. 

885 id = idOrIndOrCon.id 

886 if id in self._constraints: 

887 return id 

888 else: 

889 raise KeyError("The constraint '{}' is not part of the problem." 

890 .format(idOrIndOrCon)) 

891 elif isinstance(idOrIndOrCon, tuple) or isinstance(idOrIndOrCon, list): 

892 if len(idOrIndOrCon) == 1: 

893 groupIndex = idOrIndOrCon[0] 

894 if groupIndex < len(self._con_groups): 

895 return [c.id for c in self._con_groups[groupIndex]] 

896 else: 

897 raise IndexError("Constraint group index out of range.") 

898 elif len(idOrIndOrCon) == 2: 

899 groupIndex, groupOffset = idOrIndOrCon 

900 if groupIndex < len(self._con_groups): 

901 group = self._con_groups[groupIndex] 

902 if groupOffset < len(group): 

903 return group[groupOffset].id 

904 else: 

905 raise IndexError( 

906 "Constraint group offset out of range.") 

907 else: 

908 raise IndexError("Constraint group index out of range.") 

909 else: 

910 raise TypeError("If looking up constraints by group, the index " 

911 "must be a tuple or list of length at most two.") 

912 else: 

913 raise TypeError("Argument of type '{}' not supported when looking " 

914 "up constraints".format(type(idOrIndOrCon))) 

915 

916 def get_constraint(self, idOrIndOrCon): 

917 """Return a (list of) constraint(s) of the problem. 

918 

919 :param idOrIndOrCon: One of the following: 

920 

921 * A constraint object. It will be returned when the constraint is 

922 part of the problem, otherwise a KeyError is raised. 

923 * The integer ID of the constraint. 

924 * The integer offset of the constraint in the list of all 

925 constraints that are part of the problem, in the order that they 

926 were added. 

927 * A list or tuple of length 1. Its only element is the index of a 

928 constraint group (of constraints that were added together), where 

929 groups are indexed in the order that they were added to the 

930 problem. The whole group is returned as a list of constraints. 

931 That list has the constraints in the order that they were added. 

932 * A list or tuple of length 2. The first element is a constraint 

933 group offset as above, the second an offset within that list. 

934 

935 :type idOrIndOrCon: picos.constraints.Constraint or int or tuple or list 

936 

937 :returns: A :class:`constraint <picos.constraints.Constraint>` or a list 

938 thereof. 

939 

940 :Example: 

941 

942 >>> import picos as pic 

943 >>> import cvxopt as cvx 

944 >>> from pprint import pprint 

945 >>> prob=pic.Problem() 

946 >>> x=[prob.add_variable('x[{0}]'.format(i),2) for i in range(5)] 

947 >>> y=prob.add_variable('y',5) 

948 >>> Cx=prob.add_list_of_constraints([(1|x[i]) < y[i] for i in range(5)]) 

949 >>> Cy=prob.add_constraint(y>0) 

950 >>> print(prob) 

951 Linear Feasibility Problem 

952 find an assignment 

953 for 

954 2×1 real variable x[i] ∀ i ∈ [0…4] 

955 5×1 real variable y 

956 subject to 

957 ∑(x[i]) ≤ y[i] ∀ i ∈ [0…4] 

958 y ≥ 0 

959 >>> # Retrieve the second constraint, indexed from zero: 

960 >>> prob.get_constraint(1) 

961 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]> 

962 >>> # Retrieve the fourth consraint from the first group: 

963 >>> prob.get_constraint((0,3)) 

964 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]> 

965 >>> # Retrieve the whole first group of constraints: 

966 >>> pprint(prob.get_constraint((0,))) 

967 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>, 

968 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>, 

969 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>, 

970 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>, 

971 <1×1 Affine Constraint: ∑(x[4]) ≤ y[4]>] 

972 >>> # Retrieve the second "group", containing just one constraint: 

973 >>> prob.get_constraint((1,)) 

974 [<5×1 Affine Constraint: y ≥ 0>] 

975 """ 

976 idOrIds = self._lookup_constraint(idOrIndOrCon) 

977 

978 if isinstance(idOrIds, list): 

979 return [self._constraints[id] for id in idOrIds] 

980 else: 

981 return self._constraints[idOrIds] 

982 

983 def add_constraint(self, constraint, key=None): 

984 """Add a single constraint to the problem and return it. 

985 

986 :param constraint: 

987 The constraint to be added. 

988 :type constraint: 

989 :class:`Constraint <picos.constraints.Constraint>` 

990 

991 :param key: DEPRECATED 

992 

993 :returns: 

994 The constraint that was added to the problem. 

995 

996 .. note:: 

997 

998 This method is superseded by the more compact and more flexible 

999 :meth:`require` method or, at your preference, the ``+=`` operator. 

1000 """ 

1001 # Handle deprecated 'key' parameter. 

1002 if key is not None: 

1003 throw_deprecation_warning( 

1004 "Naming constraints is currently not supported.") 

1005 

1006 # Register the constraint. 

1007 self._constraints[constraint.id] = constraint 

1008 self._con_groups.append([constraint]) 

1009 

1010 # Register the constraint's mutables. 

1011 self._register_mutables(constraint.mutables) 

1012 

1013 return constraint 

1014 

1015 def add_list_of_constraints(self, lst, it=None, indices=None, key=None): 

1016 """Add constraints from an iterable to the problem. 

1017 

1018 :param lst: 

1019 Iterable of constraints to add. 

1020 

1021 :param it: DEPRECATED 

1022 :param indices: DEPRECATED 

1023 :param key: DEPRECATED 

1024 

1025 :returns: 

1026 A list of all constraints that were added. 

1027 

1028 :Example: 

1029 

1030 >>> import picos as pic 

1031 >>> import cvxopt as cvx 

1032 >>> from pprint import pprint 

1033 >>> prob=pic.Problem() 

1034 >>> x=[prob.add_variable('x[{0}]'.format(i),2) for i in range(5)] 

1035 >>> pprint(x) 

1036 [<2×1 Real Variable: x[0]>, 

1037 <2×1 Real Variable: x[1]>, 

1038 <2×1 Real Variable: x[2]>, 

1039 <2×1 Real Variable: x[3]>, 

1040 <2×1 Real Variable: x[4]>] 

1041 >>> y=prob.add_variable('y',5) 

1042 >>> IJ=[(1,2),(2,0),(4,2)] 

1043 >>> w={} 

1044 >>> for ij in IJ: 

1045 ... w[ij]=prob.add_variable('w[{},{}]'.format(*ij),3) 

1046 ... 

1047 >>> u=pic.new_param('u',cvx.matrix([2,5])) 

1048 >>> C1=prob.add_list_of_constraints([u.T*x[i] < y[i] for i in range(5)]) 

1049 >>> C2=prob.add_list_of_constraints([abs(w[i,j])<y[j] for (i,j) in IJ]) 

1050 >>> C3=prob.add_list_of_constraints([y[t] > y[t+1] for t in range(4)]) 

1051 >>> print(prob) 

1052 Feasibility Problem 

1053 find an assignment 

1054 for 

1055 2×1 real variable x[i] ∀ i ∈ [0…4] 

1056 3×1 real variable w[i,j] ∀ (i,j) ∈ zip([1,2,4],[2,0,2]) 

1057 5×1 real variable y 

1058 subject to 

1059 uᵀ·x[i] ≤ y[i] ∀ i ∈ [0…4] 

1060 ‖w[i,j]‖ ≤ y[j] ∀ (i,j) ∈ zip([1,2,4],[2,0,2]) 

1061 y[i] ≥ y[i+1] ∀ i ∈ [0…3] 

1062 

1063 .. note:: 

1064 

1065 This method is superseded by the more compact and more flexible 

1066 :meth:`require` method or, at your preference, the ``+=`` operator. 

1067 """ 

1068 if it is not None or indices is not None or key is not None: 

1069 # Deprecated as of 2.0. 

1070 throw_deprecation_warning("Arguments 'it', 'indices' and 'key' to " 

1071 "add_list_of_constraints are deprecated and ignored.") 

1072 

1073 added = [] 

1074 for constraint in lst: 

1075 added.append(self.add_constraint(constraint)) 

1076 self._con_groups.pop() 

1077 

1078 if added: 

1079 self._con_groups.append(added) 

1080 

1081 return added 

1082 

1083 def require(self, *constraints, ret=False): 

1084 """Add constraints to the problem. 

1085 

1086 :param constraints: 

1087 A sequence of constraints or constraint groups (iterables yielding 

1088 constraints) or a mix thereof. 

1089 

1090 :param bool ret: 

1091 Whether to return the added constraints. 

1092 

1093 :returns: 

1094 When ``ret=True``, returns either the single constraint that was 

1095 added, the single group of constraint that was added in the form of 

1096 a :class:`list` or, when multiple arguments are given, a list of 

1097 constraints or constraint groups represented as above. When 

1098 ``ret=False``, returns nothing. 

1099 

1100 :Example: 

1101 

1102 >>> from picos import Problem, RealVariable 

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

1104 >>> P = Problem() 

1105 >>> P.require(x >= -1, x <= 1) # Add individual constraints. 

1106 >>> P.require([x[i] <= x[i+1] for i in range(4)]) # Add groups. 

1107 >>> print(P) 

1108 Linear Feasibility Problem 

1109 find an assignment 

1110 for 

1111 5×1 real variable x 

1112 subject to 

1113 x ≥ [-1] 

1114 x ≤ [1] 

1115 x[i] ≤ x[i+1] ∀ i ∈ [0…3] 

1116 

1117 .. note:: 

1118 

1119 For a single constraint ``C``, ``P.require(C)`` may also be written 

1120 as ``P += C``. For multiple constraints, ``P.require([C1, C2])`` can 

1121 be abbreviated ``P += [C1, C2]`` while ``P.require(C1, C2)`` can be 

1122 written as either ``P += (C1, C2)`` or just ``P += C1, C2``. 

1123 """ 

1124 from ..constraints import Constraint 

1125 

1126 added = [] 

1127 for constraint in constraints: 

1128 if isinstance(constraint, Constraint): 

1129 added.append(self.add_constraint(constraint)) 

1130 else: 

1131 try: 

1132 if not all(isinstance(c, Constraint) for c in constraint): 

1133 raise TypeError 

1134 except TypeError: 

1135 raise TypeError( 

1136 "An argument is neither a constraint nor an iterable " 

1137 "yielding constraints.") from None 

1138 else: 

1139 added.append(self.add_list_of_constraints(constraint)) 

1140 

1141 if ret: 

1142 return added[0] if len(added) == 1 else added 

1143 

1144 def _con_group_index(self, conOrConID): 

1145 """Support :meth:`remove_constraint`.""" 

1146 if isinstance(conOrConID, int): 

1147 constraint = self._constraints[conOrConID] 

1148 else: 

1149 constraint = conOrConID 

1150 

1151 for i, group in enumerate(self._con_groups): 

1152 for j, candidate in enumerate(group): 

1153 if candidate is constraint: 

1154 return i, j 

1155 

1156 if constraint in self._constraints.values(): 

1157 raise RuntimeError("The problem's constraint and constraint group " 

1158 "registries are out of sync.") 

1159 else: 

1160 raise KeyError("The constraint is not part of the problem.") 

1161 

1162 def remove_constraint(self, idOrIndOrCon): 

1163 """Delete a constraint from the problem. 

1164 

1165 :param idOrIndOrCon: See :meth:`get_constraint`. 

1166 

1167 :Example: 

1168 

1169 >>> import picos 

1170 >>> from pprint import pprint 

1171 >>> P = picos.Problem() 

1172 >>> x = [P.add_variable('x[{0}]'.format(i), 2) for i in range(4)] 

1173 >>> y = P.add_variable('y', 4) 

1174 >>> Cxy = P.add_list_of_constraints( 

1175 ... [(1 | x[i]) <= y[i] for i in range(4)]) 

1176 >>> Cy = P.add_constraint(y >= 0) 

1177 >>> Cx0to2 = P.add_list_of_constraints([x[i] <= 2 for i in range(3)]) 

1178 >>> Cx3 = P.add_constraint(x[3] <= 1) 

1179 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE 

1180 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>, 

1181 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>, 

1182 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>, 

1183 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>, 

1184 <4×1 Affine Constraint: y ≥ 0>, 

1185 <2×1 Affine Constraint: x[0] ≤ [2]>, 

1186 <2×1 Affine Constraint: x[1] ≤ [2]>, 

1187 <2×1 Affine Constraint: x[2] ≤ [2]>, 

1188 <2×1 Affine Constraint: x[3] ≤ [1]>] 

1189 >>> # Delete the 2nd constraint (counted from 0): 

1190 >>> P.remove_constraint(1) 

1191 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE 

1192 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>, 

1193 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>, 

1194 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>, 

1195 <4×1 Affine Constraint: y ≥ 0>, 

1196 <2×1 Affine Constraint: x[0] ≤ [2]>, 

1197 <2×1 Affine Constraint: x[1] ≤ [2]>, 

1198 <2×1 Affine Constraint: x[2] ≤ [2]>, 

1199 <2×1 Affine Constraint: x[3] ≤ [1]>] 

1200 >>> # Delete the 2nd group of constraints, i.e. the constraint y > 0: 

1201 >>> P.remove_constraint((1,)) 

1202 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE 

1203 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>, 

1204 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>, 

1205 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>, 

1206 <2×1 Affine Constraint: x[0] ≤ [2]>, 

1207 <2×1 Affine Constraint: x[1] ≤ [2]>, 

1208 <2×1 Affine Constraint: x[2] ≤ [2]>, 

1209 <2×1 Affine Constraint: x[3] ≤ [1]>] 

1210 >>> # Delete the 3rd remaining group of constraints, i.e. x[3] < [1]: 

1211 >>> P.remove_constraint((2,)) 

1212 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE 

1213 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>, 

1214 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>, 

1215 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>, 

1216 <2×1 Affine Constraint: x[0] ≤ [2]>, 

1217 <2×1 Affine Constraint: x[1] ≤ [2]>, 

1218 <2×1 Affine Constraint: x[2] ≤ [2]>] 

1219 >>> # Delete 2nd constraint of the 2nd remaining group, i.e. x[1] < |2|: 

1220 >>> P.remove_constraint((1,1)) 

1221 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE 

1222 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>, 

1223 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>, 

1224 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>, 

1225 <2×1 Affine Constraint: x[0] ≤ [2]>, 

1226 <2×1 Affine Constraint: x[2] ≤ [2]>] 

1227 """ 

1228 idOrIds = self._lookup_constraint(idOrIndOrCon) 

1229 

1230 removedCons = [] 

1231 

1232 if isinstance(idOrIds, list): 

1233 assert idOrIds, "There is an empty constraint group." 

1234 groupIndex, _ = self._con_group_index(idOrIds[0]) 

1235 self._con_groups.pop(groupIndex) 

1236 for id in idOrIds: 

1237 removedCons.append(self._constraints.pop(id)) 

1238 else: 

1239 constraint = self._constraints.pop(idOrIds) 

1240 removedCons.append(constraint) 

1241 groupIndex, groupOffset = self._con_group_index(constraint) 

1242 group = self._con_groups[groupIndex] 

1243 group.pop(groupOffset) 

1244 if not group: 

1245 self._con_groups.pop(groupIndex) 

1246 

1247 # Unregister the mutables added by the removed constraints. 

1248 for con in removedCons: 

1249 self._unregister_mutables(con.mutables) 

1250 

1251 def remove_all_constraints(self): 

1252 """Remove all constraints from the problem. 

1253 

1254 .. note:: 

1255 

1256 This method does not remove bounds set directly on variables. 

1257 """ 

1258 del self.constraints 

1259 

1260 # -------------------------------------------------------------------------- 

1261 # Borderline legacy methods to deal with variables. 

1262 # -------------------------------------------------------------------------- 

1263 

1264 _PARAMETERIZED_VARIABLE_REGEX = re.compile(r"^([^[]+)\[([^\]]+)\]$") 

1265 

1266 def get_variable(self, name): 

1267 """Retrieve variables referenced by the problem. 

1268 

1269 Retrieves either a single variable with the given name or a group of 

1270 variables all named ``name[param]`` with different values for ``param``. 

1271 If the values for ``param`` are the integers from zero to the size of 

1272 the group minus one, then the group is returned as a :obj:`list` ordered 

1273 by ``param``, otherwise it is returned as a :obj:`dict` with the values 

1274 of ``param`` as keys. 

1275 

1276 .. note:: 

1277 

1278 Since PICOS 2.0, variables are independent of problems and only 

1279 appear in a problem for as long as they are referenced by the 

1280 problem's objective function or constraints. 

1281 

1282 :param str name: 

1283 The name of a variable, or the base name of a group of variables. 

1284 

1285 :returns: 

1286 A :class:`variable <picos.expressions.BaseVariable>` or a 

1287 :class:`list` or :class:`dict` thereof. 

1288 

1289 :Example: 

1290 

1291 >>> from picos import Problem, RealVariable 

1292 >>> from pprint import pprint 

1293 >>> # Create a number of variables with structured names. 

1294 >>> vars = [RealVariable("x")] 

1295 >>> for i in range(4): 

1296 ... vars.append(RealVariable("y[{}]".format(i))) 

1297 >>> for key in ["alice", "bob", "carol"]: 

1298 ... vars.append(RealVariable("z[{}]".format(key))) 

1299 >>> # Make the variables appear in a problem. 

1300 >>> P = Problem() 

1301 >>> P.set_objective("min", sum([var for var in vars])) 

1302 >>> print(P) 

1303 Linear Program 

1304 minimize x + y[0] + y[1] + y[2] + y[3] + z[alice] + z[bob] + z[carol] 

1305 over 

1306 1×1 real variable x, y[0], y[1], y[2], y[3], z[alice], z[bob], 

1307 z[carol] 

1308 >>> # Retrieve the variables from the problem. 

1309 >>> P.get_variable("x") 

1310 <1×1 Real Variable: x> 

1311 >>> pprint(P.get_variable("y")) 

1312 [<1×1 Real Variable: y[0]>, 

1313 <1×1 Real Variable: y[1]>, 

1314 <1×1 Real Variable: y[2]>, 

1315 <1×1 Real Variable: y[3]>] 

1316 >>> pprint(P.get_variable("z")) 

1317 {'alice': <1×1 Real Variable: z[alice]>, 

1318 'bob': <1×1 Real Variable: z[bob]>, 

1319 'carol': <1×1 Real Variable: z[carol]>} 

1320 >>> P.get_variable("z")["alice"] is P.get_variable("z[alice]") 

1321 True 

1322 """ 

1323 if name in self._variables: 

1324 return self._variables[name] 

1325 else: 

1326 # Check if the name is really just a basename. 

1327 params = [] 

1328 for otherName in sorted(self._variables.keys()): 

1329 match = self._PARAMETERIZED_VARIABLE_REGEX.match(otherName) 

1330 if not match: 

1331 continue 

1332 base, param = match.groups() 

1333 if name == base: 

1334 params.append(param) 

1335 

1336 if params: 

1337 # Return a list if the parameters are a range. 

1338 try: 

1339 intParams = sorted([int(p) for p in params]) 

1340 except ValueError: 

1341 pass 

1342 else: 

1343 if intParams == list(range(len(intParams))): 

1344 return [self._variables["{}[{}]".format(name, param)] 

1345 for param in intParams] 

1346 

1347 # Otherwise return a dict. 

1348 return {param: self._variables["{}[{}]".format(name, param)] 

1349 for param in params} 

1350 else: 

1351 raise KeyError("The problem references no variable or group of " 

1352 "variables named '{}'.".format(name)) 

1353 

1354 def get_valued_variable(self, name): 

1355 """Retrieve values of variables referenced by the problem. 

1356 

1357 This method works the same :meth:`get_variable` but it returns the 

1358 variable's :attr:`values <.expression.Expression.value>` instead of the 

1359 variable objects. 

1360 

1361 :raises ~picos.expressions.NotValued: 

1362 If any of the selected variables is not valued. 

1363 """ 

1364 exp = self.get_variable(name) 

1365 if isinstance(exp, list): 

1366 for i in range(len(exp)): 

1367 exp[i] = exp[i].value 

1368 elif isinstance(exp, dict): 

1369 for i in exp: 

1370 exp[i] = exp[i].value 

1371 else: 

1372 exp = exp.value 

1373 return exp 

1374 

1375 # -------------------------------------------------------------------------- 

1376 # Methods to create copies of the problem. 

1377 # -------------------------------------------------------------------------- 

1378 

1379 def copy(self): 

1380 """Create a deep copy of the problem, using new mutables.""" 

1381 the_copy = Problem(copyOptions=self._options) 

1382 

1383 # Duplicate the mutables. 

1384 new_mtbs = {mtb: mtb.copy() for name, mtb in self._mutables.items()} 

1385 

1386 # Make copies of constraints on top of the new mutables. 

1387 for group in self._con_groups: 

1388 the_copy.add_list_of_constraints( 

1389 constraint.replace_mutables(new_mtbs) for constraint in group) 

1390 

1391 # Make a copy of the objective on top of the new mutables. 

1392 direction, function = self._objective 

1393 if function is not None: 

1394 the_copy.objective = direction, function.replace_mutables(new_mtbs) 

1395 

1396 return the_copy 

1397 

1398 def continuous_relaxation(self, copy_other_mutables=True): 

1399 """Return a continuous relaxation of the problem. 

1400 

1401 This is done by replacing integer variables with continuous ones. 

1402 

1403 :param bool copy_other_mutables: 

1404 Whether variables that are already continuous as well as parameters 

1405 should be copied. If this is :obj:`False`, then the relxation shares 

1406 these mutables with the original problem. 

1407 """ 

1408 the_copy = Problem(copyOptions=self._options) 

1409 

1410 # Relax integral variables and copy other mutables if requested. 

1411 new_mtbs = {} 

1412 for name, var in self._mutables.items(): 

1413 if isinstance(var, expressions.IntegerVariable): 

1414 new_mtbs[name] = expressions.RealVariable( 

1415 name, var.shape, var._lower, var._upper) 

1416 elif isinstance(var, expressions.BinaryVariable): 

1417 new_mtbs[name] = expressions.RealVariable(name, var.shape, 0, 1) 

1418 else: 

1419 if copy_other_mutables: 

1420 new_mtbs[name] = var.copy() 

1421 else: 

1422 new_mtbs[name] = var 

1423 

1424 # Make copies of constraints on top of the new mutables. 

1425 for group in self._con_groups: 

1426 the_copy.add_list_of_constraints( 

1427 constraint.replace_mutables(new_mtbs) for constraint in group) 

1428 

1429 # Make a copy of the objective on top of the new mutables. 

1430 direction, function = self._objective 

1431 if function is not None: 

1432 the_copy.objective = direction, function.replace_mutables(new_mtbs) 

1433 

1434 return the_copy 

1435 

1436 def clone(self, copyOptions=True): 

1437 """Create a semi-deep copy of the problem. 

1438 

1439 The copy is constrained by the same constraint objects and has the same 

1440 objective function and thereby references the existing variables and 

1441 parameters that appear in these objects. 

1442 

1443 The clone can be modified to describe a new problem but when its 

1444 variables and parameters are valued, in particular when a solution is 

1445 applied to the new problem, then the same values are found in the 

1446 corresponding variables and parameters of the old problem. If this is 

1447 not a problem to you, then cloning can be much faster than copying. 

1448 

1449 :param bool copyOptions: 

1450 Whether to make an independent copy of the problem's options. 

1451 Disabling this will apply any option changes to the original problem 

1452 as well but yields a (very small) reduction in cloning time. 

1453 """ 

1454 # Start with a shallow copy of self. 

1455 # TODO: Consider adding Problem.__new__ to speed this up further. 

1456 theClone = pycopy.copy(self) 

1457 

1458 # Make the constraint registry independent. 

1459 theClone._constraints = self._constraints.copy() 

1460 theClone._con_groups = [] 

1461 for group in self._con_groups: 

1462 theClone._con_groups.append(pycopy.copy(group)) 

1463 

1464 # Make the mutable registry independent. 

1465 theClone._mtb_count = self._mtb_count.copy() 

1466 theClone._mutables = self._mutables.copy() 

1467 theClone._variables = self._variables.copy() 

1468 theClone._parameters = self._parameters.copy() 

1469 

1470 # Reset the clone's solution strategy and last solution. 

1471 theClone._strategy = None 

1472 

1473 # Make the solver options independent, if requested. 

1474 if copyOptions: 

1475 theClone._options = self._options.copy() 

1476 

1477 # NOTE: No need to change the following attributes: 

1478 # - objective: Is immutable as a tuple. 

1479 # - _last_solution: Remains as valid as it is. 

1480 

1481 return theClone 

1482 

1483 # -------------------------------------------------------------------------- 

1484 # Methods to solve or export the problem. 

1485 # -------------------------------------------------------------------------- 

1486 

1487 def prepared(self, steps=None, **extra_options): 

1488 """Perform a dry-run returning the reformulated (prepared) problem. 

1489 

1490 This behaves like :meth:`solve` in that it takes a number of additional 

1491 temporary options, finds a solution strategy matching the problem and 

1492 options, and performs the strategy's reformulations in turn to obtain 

1493 modified problems. However, it stops after the given number of steps and 

1494 never hands the reformulated problem to a solver. Instead of a solution, 

1495 :meth:`prepared` then returns the last reformulated problem. 

1496 

1497 Unless this method returns the problem itself, the special attributes 

1498 ``prepared_strategy`` and ``prepared_steps`` are added to the returned 

1499 problem. They then contain the (partially) executed solution strategy 

1500 and the number of performed reformulations, respectively. 

1501 

1502 :param int steps: 

1503 Number of reformulations to perform. :obj:`None` means as many as 

1504 there are. If this parameter is :math:`0`, then the problem itself 

1505 is returned. If it is :math:`1`, then only the implicit first 

1506 reformulation :class:`~.reform_options.ExtraOptions` is executed, 

1507 which may also output the problem itself, depending on 

1508 ``extra_options``. 

1509 

1510 :param extra_options: 

1511 Additional solver options to use with this dry-run only. 

1512 

1513 :returns: 

1514 The reformulated problem, with ``extra_options`` set unless they 

1515 were "consumed" by a reformulation (e.g. 

1516 :ref:`option_dualize <option_dualize>`). 

1517 

1518 :raises ~picos.modeling.strategy.NoStrategyFound: 

1519 If no solution strategy was found. 

1520 

1521 :raises ValueError: 

1522 If there are not as many reformulation steps as requested. 

1523 

1524 :Example: 

1525 

1526 >>> from picos import Problem, RealVariable 

1527 >>> x = RealVariable("x", 2) 

1528 >>> P = Problem() 

1529 >>> P.set_objective("min", abs(x)**2) 

1530 >>> Q = P.prepared(solver = "cvxopt") 

1531 >>> print(Q.prepared_strategy) # Show prepared reformulation steps. 

1532 1. ExtraOptions 

1533 2. EpigraphReformulation 

1534 3. SquaredNormToConicReformulation 

1535 4. CVXOPTSolver 

1536 >>> Q.prepared_steps # Check how many steps have been performed. 

1537 3 

1538 >>> print(P) 

1539 Quadratic Program 

1540 minimize ‖x‖² 

1541 over 

1542 2×1 real variable x 

1543 >>> print(Q)# doctest: +ELLIPSIS 

1544 Second Order Cone Program 

1545 minimize __..._t 

1546 over 

1547 1×1 real variable __..._t 

1548 2×1 real variable x 

1549 subject to 

1550 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0 

1551 """ 

1552 from .strategy import Strategy 

1553 

1554 # Produce a strategy for the clone. 

1555 strategy = Strategy.from_problem(self, **extra_options) 

1556 numReforms = len(strategy.reforms) 

1557 

1558 if steps is None: 

1559 steps = numReforms 

1560 

1561 if steps == 0: 

1562 return self 

1563 elif steps > numReforms: 

1564 raise ValueError("The pipeline {} has only {} reformulation steps " 

1565 "to choose from.".format(strategy, numReforms)) 

1566 

1567 # Replace the successor of the last reformulation with a dummy solver. 

1568 lastReform = strategy.reforms[steps - 1] 

1569 oldSuccessor = lastReform.successor 

1570 lastReform.successor = type("DummySolver", (), { 

1571 "execute": lambda self: Solution( 

1572 {}, solver="dummy", vectorizedPrimals=True)})() 

1573 

1574 # Execute the cut-short strategy. 

1575 strategy.execute(**extra_options) 

1576 

1577 # Repair the last reformulation. 

1578 lastReform.successor = oldSuccessor 

1579 

1580 # Retrieve and augment the output problem (unless it's self). 

1581 output = lastReform.output 

1582 if output is not self: 

1583 output.prepared_strategy = strategy 

1584 output.prepared_steps = steps 

1585 

1586 return output 

1587 

1588 def reformulated(self, specification, **extra_options): 

1589 r"""Return the problem reformulated to match a specification. 

1590 

1591 Internally this creates a dummy solver accepting problems of the desired 

1592 form and then calls :meth:`prepared` with the dummy solver passed via 

1593 :ref:`option_ad_hoc_solver <option_ad_hoc_solver>`. See meth:`prepared` 

1594 for more details. 

1595 

1596 :param specification: 

1597 A problem class that the resulting problem must be a member of. 

1598 :type specification: 

1599 ~picos.modeling.Specification 

1600 

1601 :param extra_options: 

1602 Additional solver options to use with this reformulation only. 

1603 

1604 :returns: 

1605 The reformulated problem, with ``extra_options`` set unless they 

1606 were "consumed" by a reformulation (e.g. 

1607 :ref:`dualize <option_dualize>`). 

1608 

1609 :raises ~picos.modeling.strategy.NoStrategyFound: 

1610 If no reformulation strategy was found. 

1611 

1612 :Example: 

1613 

1614 >>> from picos import Problem, RealVariable 

1615 >>> from picos.modeling import Specification 

1616 >>> from picos.expressions import AffineExpression 

1617 >>> from picos.constraints import ( 

1618 ... AffineConstraint, SOCConstraint, RSOCConstraint) 

1619 >>> # Define the class/specification of second order conic problems: 

1620 >>> S = Specification(objectives=[AffineExpression], 

1621 ... constraints=[AffineConstraint, SOCConstraint, RSOCConstraint]) 

1622 >>> # Define a quadratic program and reformulate it: 

1623 >>> x = RealVariable("x", 2) 

1624 >>> P = Problem() 

1625 >>> P.set_objective("min", abs(x)**2) 

1626 >>> Q = P.reformulated(S) 

1627 >>> print(P) 

1628 Quadratic Program 

1629 minimize ‖x‖² 

1630 over 

1631 2×1 real variable x 

1632 >>> print(Q)# doctest: +ELLIPSIS 

1633 Second Order Cone Program 

1634 minimize __..._t 

1635 over 

1636 1×1 real variable __..._t 

1637 2×1 real variable x 

1638 subject to 

1639 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0 

1640 

1641 .. note:: 

1642 

1643 This method is intended for educational purposes. 

1644 You do not need to use it when solving a problem as PICOS will 

1645 perform the necessary reformulations automatically. 

1646 """ 

1647 if not isinstance(specification, Specification): 

1648 raise TypeError("The desired problem type must be given as a " 

1649 "Specification object.") 

1650 

1651 # Create a placeholder function for abstract methods of a dummy solver. 

1652 def placeholder(the_self): 

1653 raise RuntimeError("The dummy solver created by " 

1654 "Problem.reformulated must not be executed.") 

1655 

1656 # Declare a dummy solver that accepts specified problems. 

1657 DummySolver = type("DummySolver", (Solver,), { 

1658 # Abstract class methods. 

1659 "supports": classmethod(lambda cls, footprint: 

1660 Solver.supports(footprint) and footprint in specification), 

1661 "default_penalty": classmethod(lambda cls: 0), 

1662 "test_availability": classmethod(lambda cls: None), 

1663 "names": classmethod(lambda cls: ("Dummy Solver", "DummySolver", 

1664 "Dummy Solver accepting {}".format(specification))), 

1665 "is_free": classmethod(lambda cls: True), 

1666 

1667 # Additional class methods needed for an ad-hoc solver. 

1668 "penalty": classmethod(lambda cls, options: 0), 

1669 

1670 # Abstract instance methods. 

1671 "reset_problem": lambda self: placeholder(self), 

1672 "_import_problem": lambda self: placeholder(self), 

1673 "_update_problem": lambda self: placeholder(self), 

1674 "_solve": lambda self: placeholder(self) 

1675 }) 

1676 

1677 # Ad-hoc the dummy solver and prepare the problem for it. 

1678 oldAdHocSolver = self.options.ad_hoc_solver 

1679 extra_options["ad_hoc_solver"] = DummySolver 

1680 problem = self.prepared(**extra_options) 

1681 

1682 # Restore the ad_hoc_solver option of the original problem. 

1683 problem.options.ad_hoc_solver = oldAdHocSolver 

1684 

1685 return problem 

1686 

1687 def solve(self, **extra_options): 

1688 """Hand the problem to a solver. 

1689 

1690 You can select the solver manually with the ``solver`` option. Otherwise 

1691 a suitable solver will be selected among those that are available on the 

1692 platform. 

1693 

1694 The default behavior (options ``primals=True``, ``duals=None``) is to 

1695 raise a :exc:`~picos.SolutionFailure` when the primal solution is not 

1696 found optimal by the solver, while the dual solution is allowed to be 

1697 missing or incomplete. 

1698 

1699 When this method succeeds and unless ``apply_solution=False``, you can 

1700 access the solution as follows: 

1701 

1702 - The problem's :attr:`value` denotes the objective function value. 

1703 - The variables' :attr:`~.expression.Expression.value` is set 

1704 according to the primal solution. You can in fact query the value 

1705 of any expression involving valued variables like this. 

1706 - The constraints' :attr:`~.constraint.Constraint.dual` is set 

1707 according to the dual solution. 

1708 - The value of any parameter involved in the problem may have 

1709 changed, depending on the parameter. 

1710 

1711 :param extra_options: 

1712 A sequence of additional solver options to use with this solution 

1713 search only. In particular, this lets you 

1714 

1715 - select a solver via the ``solver`` option, 

1716 - obtain non-optimal primal solutions by setting ``primals=None``, 

1717 - require a complete and optimal dual solution with ``duals=True``, 

1718 and 

1719 - skip valuing variables or constraints with 

1720 ``apply_solution=False``. 

1721 

1722 :returns ~picos.Solution or list(~picos.Solution): 

1723 A solution object or list thereof. 

1724 

1725 :raises ~picos.SolutionFailure: 

1726 In the following cases: 

1727 

1728 1. No solution strategy was found. 

1729 2. Multiple solutions were requested but none were returned. 

1730 3. A primal solution was explicitly requested (``primals=True``) but 

1731 the primal solution is missing/incomplete or not claimed optimal. 

1732 4. A dual solution was explicitly requested (``duals=True``) but 

1733 the dual solution is missing/incomplete or not claimed optimal. 

1734 

1735 The case number is stored in the ``code`` attribute of the 

1736 exception. 

1737 """ 

1738 from .strategy import NoStrategyFound, Strategy 

1739 

1740 startTime = time.time() 

1741 

1742 extra_options = map_legacy_options(**extra_options) 

1743 options = self.options.self_or_updated(**extra_options) 

1744 verbose = options.verbosity > 0 

1745 

1746 with picos_box(show=verbose): 

1747 if verbose: 

1748 print("Problem type: {}.".format(self.type)) 

1749 

1750 # Reset an outdated strategy. 

1751 if self._strategy and not self._strategy.valid(**extra_options): 

1752 if verbose: 

1753 print("Strategy outdated:\n{}.".format(self._strategy)) 

1754 

1755 self._strategy = None 

1756 

1757 # Find a new solution strategy, if necessary. 

1758 if not self._strategy: 

1759 if verbose: 

1760 if options.ad_hoc_solver: 

1761 solverName = options.ad_hoc_solver.names()[1] 

1762 elif options.solver: 

1763 solverName = get_solver(options.solver).names()[1] 

1764 else: 

1765 solverName = None 

1766 

1767 print("Searching a solution strategy{}.".format( 

1768 " for {}".format(solverName) if solverName else "")) 

1769 

1770 try: 

1771 self._strategy = Strategy.from_problem( 

1772 self, **extra_options) 

1773 except NoStrategyFound as error: 

1774 s = str(error) 

1775 

1776 if verbose: 

1777 print(s, flush=True) 

1778 

1779 raise SolutionFailure(1, "No solution strategy found.") \ 

1780 from error 

1781 

1782 if verbose: 

1783 print("Solution strategy:\n {}".format( 

1784 "\n ".join(str(self._strategy).splitlines()))) 

1785 else: 

1786 if verbose: 

1787 print("Reusing strategy:\n {}".format( 

1788 "\n ".join(str(self._strategy).splitlines()))) 

1789 

1790 # Execute the strategy to obtain one or more solutions. 

1791 solutions = self._strategy.execute(**extra_options) 

1792 

1793 # Report how many solutions were obtained, select the first. 

1794 if isinstance(solutions, list): 

1795 assert all(isinstance(s, Solution) for s in solutions) 

1796 

1797 if not solutions: 

1798 raise SolutionFailure( 

1799 2, "The solver returned an empty list of solutions.") 

1800 

1801 solution = solutions[0] 

1802 

1803 if verbose: 

1804 print("Selecting the first of {} solutions obtained for " 

1805 "processing.".format(len(solutions))) 

1806 else: 

1807 assert isinstance(solutions, Solution) 

1808 solution = solutions 

1809 

1810 # Report claimed solution state. 

1811 if verbose: 

1812 print("Solver claims {} solution for {} problem.".format( 

1813 solution.claimedStatus, solution.problemStatus)) 

1814 

1815 # Validate the primal solution. 

1816 if options.primals: 

1817 if solution.primalStatus != SS_OPTIMAL: 

1818 raise SolutionFailure(3, "Primal solution state claimed {} " 

1819 "but optimality is required (primals=True)." 

1820 .format(solution.primalStatus)) 

1821 elif None in solution.primals.values(): 

1822 raise SolutionFailure(3, "The primal solution is incomplete" 

1823 " but full primals are required (primals=True).") 

1824 

1825 # Validate the dual solution. 

1826 if options.duals: 

1827 if solution.dualStatus != SS_OPTIMAL: 

1828 raise SolutionFailure(4, "Dual solution state claimed {} " 

1829 "but optimality is required (duals=True).".format( 

1830 solution.dualStatus)) 

1831 elif None in solution.duals.values(): 

1832 raise SolutionFailure(4, "The dual solution is incomplete " 

1833 "but full duals are required (duals=True).") 

1834 

1835 if options.apply_solution: 

1836 if verbose: 

1837 print("Applying the solution.") 

1838 

1839 # Apply the (first) solution. 

1840 solution.apply(snapshotStatus=True) 

1841 

1842 # Store all solutions produced by the solver. 

1843 self._last_solution = solutions 

1844 

1845 # Report verified solution state. 

1846 if verbose: 

1847 print("Applied solution is {}.".format(solution.lastStatus)) 

1848 

1849 endTime = time.time() 

1850 solveTime = endTime - startTime 

1851 searchTime = solution.searchTime 

1852 

1853 if searchTime: 

1854 overhead = (solveTime - searchTime) / searchTime 

1855 else: 

1856 overhead = float("inf") 

1857 

1858 if verbose: 

1859 print("Search {:.1e}s, solve {:.1e}s, overhead {:.0%}." 

1860 .format(searchTime, solveTime, overhead)) 

1861 

1862 if settings.RETURN_SOLUTION: 

1863 return solutions 

1864 

1865 def write_to_file(self, filename, writer="picos"): 

1866 """See :func:`picos.modeling.file_out.write`.""" 

1867 write(self, filename, writer) 

1868 

1869 # -------------------------------------------------------------------------- 

1870 # Methods to query the problem. 

1871 # TODO: Document removal of is_complex, is_real (also for constraints). 

1872 # TODO: Revisit #14: "Interfaces to get primal/dual objective values and 

1873 # primal/dual feasiblity (amount of violation)."" 

1874 # -------------------------------------------------------------------------- 

1875 

1876 def check_current_value_feasibility(self, tol=1e-5, inttol=None): 

1877 """Check if the problem is feasibly valued. 

1878 

1879 Checks whether all variables that appear in constraints are valued and 

1880 satisfy both their bounds and the constraints up to the given tolerance. 

1881 

1882 :param float tol: 

1883 Largest tolerated absolute violation of a constraint or variable 

1884 bound. If ``None``, then the ``abs_prim_fsb_tol`` solver option is 

1885 used. 

1886 

1887 :param inttol: 

1888 DEPRECATED 

1889 

1890 :returns: 

1891 A tuple ``(feasible, violation)`` where ``feasible`` is a bool 

1892 stating whether the solution is feasible and ``violation`` is either 

1893 ``None``, if ``feasible == True``, or the amount of violation, 

1894 otherwise. 

1895 

1896 :raises picos.uncertain.IntractableWorstCase: 

1897 When computing the worst-case (expected) value of the constrained 

1898 expression is not supported. 

1899 """ 

1900 if inttol is not None: 

1901 throw_deprecation_warning("Variable integrality is now ensured on " 

1902 "assignment of a value, so it does not need to be checked via " 

1903 "check_current_value_feasibility's old 'inttol' parameter.") 

1904 

1905 if tol is None: 

1906 tol = self._options.abs_prim_fsb_tol 

1907 

1908 all_cons = list(self._constraints.values()) 

1909 all_cons += [ 

1910 variable.bound_constraint for variable in self._variables.values() 

1911 if variable.bound_constraint] 

1912 

1913 largest_violation = 0.0 

1914 

1915 for constraint in all_cons: 

1916 try: 

1917 slack = constraint.slack 

1918 except IntractableWorstCase as error: 

1919 raise IntractableWorstCase("Failed to check worst-case or " 

1920 "expected feasibility of {}: {}".format(constraint, error))\ 

1921 from None 

1922 

1923 assert isinstance(slack, (float, cvx.matrix, cvx.spmatrix)) 

1924 if isinstance(slack, (float, cvx.spmatrix)): 

1925 slack = cvx.matrix(slack) # Allow min, max. 

1926 

1927 # HACK: The following works around the fact that the slack of an 

1928 # uncertain conic constraint is returned as a vector, even 

1929 # when the cone is that of the positive semidefinite matrices, 

1930 # in which case the vectorization used is nontrivial (svec). 

1931 # FIXME: A similar issue should arise when a linear matrix 

1932 # inequality is integrated in a product cone; The product 

1933 # cone's slack can then have negative entries but still be 

1934 # feasible and declared infeasible here. 

1935 # TODO: Add a "violation" interface to Constraint that replaces all 

1936 # the logic below. 

1937 from ..expressions import Constant, PositiveSemidefiniteCone 

1938 if isinstance(constraint, 

1939 constraints.uncertain.ScenarioUncertainConicConstraint) \ 

1940 and isinstance(constraint.cone, PositiveSemidefiniteCone): 

1941 hack = True 

1942 slack = Constant(slack).desvec.safe_value 

1943 else: 

1944 hack = False 

1945 

1946 if isinstance(constraint, constraints.LMIConstraint) or hack: 

1947 # Check hermitian-ness of slack. 

1948 violation = float(max(abs(slack - slack.H))) 

1949 if violation > tol: 

1950 largest_violation = max(largest_violation, violation) 

1951 

1952 # Check positive semidefiniteness of slack. 

1953 violation = -float(min(np.linalg.eigvalsh(cvx2np(slack)))) 

1954 if violation > tol: 

1955 largest_violation = max(largest_violation, violation) 

1956 else: 

1957 violation = -float(min(slack)) 

1958 if violation > tol: 

1959 largest_violation = max(largest_violation, violation) 

1960 

1961 return (not largest_violation, largest_violation) 

1962 

1963 # -------------------------------------------------------------------------- 

1964 # Legacy methods and properties. 

1965 # -------------------------------------------------------------------------- 

1966 

1967 _LEGACY_PROPERTY_REASON = "Still used internally by legacy code; will be " \ 

1968 "removed together with that code." 

1969 

1970 @property 

1971 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON) 

1972 def countVar(self): 

1973 """The same as :func:`len` applied to :attr:`variables`.""" 

1974 return len(self._variables) 

1975 

1976 @property 

1977 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON) 

1978 def countCons(self): 

1979 """The same as :func:`len` applied to :attr:`constraints`.""" 

1980 return len(self._variables) 

1981 

1982 @property 

1983 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON) 

1984 def numberOfVars(self): 

1985 """The sum of the dimensions of all referenced variables.""" 

1986 return sum(variable.dim for variable in self._variables.values()) 

1987 

1988 @property 

1989 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON) 

1990 def numberLSEConstraints(self): 

1991 """Number of :class:`~picos.constraints.LogSumExpConstraint` stored.""" 

1992 return len([c for c in self._constraints.values() 

1993 if isinstance(c, constraints.LogSumExpConstraint)]) 

1994 

1995 @property 

1996 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON) 

1997 def numberSDPConstraints(self): 

1998 """Number of :class:`~picos.constraints.LMIConstraint` stored.""" 

1999 return len([c for c in self._constraints.values() 

2000 if isinstance(c, constraints.LMIConstraint)]) 

2001 

2002 @property 

2003 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON) 

2004 def numberQuadConstraints(self): 

2005 """Number of quadratic constraints stored.""" 

2006 return len([c for c in self._constraints.values() if isinstance(c, ( 

2007 constraints.ConvexQuadraticConstraint, 

2008 constraints.ConicQuadraticConstraint, 

2009 constraints.NonconvexQuadraticConstraint))]) 

2010 

2011 @property 

2012 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON) 

2013 def numberConeConstraints(self): 

2014 """Number of quadratic conic constraints stored.""" 

2015 return len([c for c in self._constraints.values() if isinstance( 

2016 c, (constraints.SOCConstraint, constraints.RSOCConstraint))]) 

2017 

2018 @deprecated("2.0", useInstead="value") 

2019 def obj_value(self): 

2020 """Objective function value. 

2021 

2022 :raises AttributeError: 

2023 If the problem is a feasibility problem or if the objective function 

2024 is not valued. This is legacy behavior. Note that :attr:`value` just 

2025 returns :obj:`None` while functions that **do** raise an exception 

2026 to denote an unvalued expression would raise 

2027 :exc:`~picos.expressions.NotValued` instead. 

2028 """ 

2029 if self._objective.feasibility: 

2030 raise AttributeError( 

2031 "A feasibility problem has no objective value.") 

2032 

2033 value = self.value 

2034 

2035 if self.value is None: 

2036 raise AttributeError("The objective {} is not fully valued." 

2037 .format(self._objective.function.string)) 

2038 else: 

2039 return value 

2040 

2041 @deprecated("2.0", useInstead="continuous") 

2042 def is_continuous(self): 

2043 """Whether all variables are of continuous types.""" 

2044 return self.continuous 

2045 

2046 @deprecated("2.0", useInstead="pure_integer") 

2047 def is_pure_integer(self): 

2048 """Whether all variables are of integral types.""" 

2049 return self.pure_integer 

2050 

2051 @deprecated("2.0", useInstead="Problem.options") 

2052 def set_all_options_to_default(self): 

2053 """Set all solver options to their default value.""" 

2054 self._options.reset() 

2055 

2056 @deprecated("2.0", useInstead="Problem.options") 

2057 def set_option(self, key, val): 

2058 """Set a single solver option to the given value. 

2059 

2060 :param str key: String name of the option, see below for a list. 

2061 :param val: New value for the option. 

2062 """ 

2063 key, val = map_legacy_options({key: val}).popitem() 

2064 self._options[key] = val 

2065 

2066 @deprecated("2.0", useInstead="Problem.options") 

2067 def update_options(self, **options): 

2068 """Set multiple solver options at once. 

2069 

2070 :param options: A parameter sequence of options to set. 

2071 """ 

2072 options = map_legacy_options(**options) 

2073 for key, val in options.items(): 

2074 self._options[key] = val 

2075 

2076 @deprecated("2.0", useInstead="Problem.options") 

2077 def verbosity(self): 

2078 """Return the problem's current verbosity level.""" 

2079 return self._options.verbosity 

2080 

2081 @deprecated("2.0", reason="Variables can now be created independent of " 

2082 "problems, and do not need to be added to any problem explicitly.") 

2083 def add_variable( 

2084 self, name, size=1, vtype='continuous', lower=None, upper=None): 

2085 r"""Legacy method to create a PICOS variable. 

2086 

2087 :param str name: The name of the variable. 

2088 

2089 :param size: 

2090 The shape of the variable. 

2091 :type size: 

2092 anything recognized by :func:`~picos.expressions.data.load_shape` 

2093 

2094 :param str vtype: 

2095 Domain of the variable. Can be any of 

2096 

2097 - ``'continuous'`` -- real valued, 

2098 - ``'binary'`` -- either zero or one, 

2099 - ``'integer'`` -- integer valued, 

2100 - ``'symmetric'`` -- symmetric matrix, 

2101 - ``'antisym'`` or ``'skewsym'`` -- skew-symmetric matrix, 

2102 - ``'complex'`` -- complex matrix, 

2103 - ``'hermitian'`` -- complex hermitian matrix. 

2104 

2105 :param lower: 

2106 A lower bound on the variable. 

2107 :type lower: 

2108 anything recognized by :func:`~picos.expressions.data.load_data` 

2109 

2110 :param upper: 

2111 An upper bound on the variable. 

2112 :type upper: 

2113 anything recognized by :func:`~picos.expressions.data.load_data` 

2114 

2115 :returns: 

2116 A :class:`~picos.expressions.BaseVariable` instance. 

2117 

2118 :Example: 

2119 

2120 >>> from picos import Problem 

2121 >>> P = Problem() 

2122 >>> x = P.add_variable("x", 3) 

2123 >>> x 

2124 <3×1 Real Variable: x> 

2125 >>> # Variable are not stored inside the problem any more: 

2126 >>> P.variables 

2127 mappingproxy(OrderedDict()) 

2128 >>> # They are only part of the problem if they actually appear: 

2129 >>> P.set_objective("min", abs(x)**2) 

2130 >>> P.variables 

2131 mappingproxy(OrderedDict([('x', <3×1 Real Variable: x>)])) 

2132 """ 

2133 if vtype == "continuous": 

2134 return expressions.RealVariable(name, size, lower, upper) 

2135 elif vtype == "binary": 

2136 return expressions.BinaryVariable(name, size) 

2137 elif vtype == "integer": 

2138 return expressions.IntegerVariable(name, size, lower, upper) 

2139 elif vtype == "symmetric": 

2140 return expressions.SymmetricVariable(name, size, lower, upper) 

2141 elif vtype in ("antisym", "skewsym"): 

2142 return expressions.SkewSymmetricVariable(name, size, lower, upper) 

2143 elif vtype == "complex": 

2144 return expressions.ComplexVariable(name, size) 

2145 elif vtype == "hermitian": 

2146 return expressions.HermitianVariable(name, size) 

2147 elif vtype in ("semiint", "semicont"): 

2148 raise NotImplementedError("Variables with legacy types 'semiint' " 

2149 "and 'semicont' are not supported anymore as of PICOS 2.0. " 

2150 "If you need this functionality back, please open an issue.") 

2151 else: 

2152 raise ValueError("Unknown legacy variable type '{}'.".format(vtype)) 

2153 

2154 @deprecated("2.0", reason="Whether a problem references a variable is now" 

2155 " determined dynamically, so this method has no effect.") 

2156 def remove_variable(self, name): 

2157 """Does nothing.""" 

2158 pass 

2159 

2160 @deprecated("2.0", useInstead="variables") 

2161 def set_var_value(self, name, value): 

2162 """Set the :attr:`value <.expression.Expression.value>` of a variable. 

2163 

2164 For a :class:`Problem` ``P``, this is the same as 

2165 ``P.variables[name] = value``. 

2166 

2167 :param str name: 

2168 Name of the variable to be valued. 

2169 

2170 :param value: 

2171 The value to be set. 

2172 :type value: 

2173 anything recognized by :func:`~picos.expressions.data.load_data` 

2174 """ 

2175 try: 

2176 variable = self._variables[name] 

2177 except KeyError: 

2178 raise KeyError("The problem references no variable named '{}'." 

2179 .format(name)) from None 

2180 else: 

2181 variable.value = value 

2182 

2183 @deprecated("2.0", useInstead="dual") 

2184 def as_dual(self): 

2185 """Return the Lagrangian dual problem of the standardized problem.""" 

2186 return self.dual 

2187 

2188 

2189# -------------------------------------- 

2190__all__ = api_end(_API_START, globals())