Coverage for picos/modeling/problem.py: 76.24%

766 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-26 07:46 +0000

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 ..valuable import Valuable 

42from .file_out import write 

43from .footprint import Footprint, Specification 

44from .objective import Objective 

45from .options import Options 

46from .solution import SS_OPTIMAL, Solution 

47 

48_API_START = api_start(globals()) 

49# ------------------------------- 

50 

51 

52class SolutionFailure(RuntimeError): 

53 """Solving the problem failed.""" 

54 

55 def __init__(self, code, message): 

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

57 

58 :param int code: 

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

60 

61 :param str message: 

62 Text description of the failure. 

63 """ 

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

65 self.code = code 

66 

67 #: Text description of the failure. 

68 self.message = message 

69 

70 def __str__(self): 

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

72 

73 

74class Problem(Valuable): 

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

76 

77 :Example: 

78 

79 >>> from picos import Problem, RealVariable 

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

81 >>> P = Problem("Example") 

82 >>> P.maximize = X.tr 

83 >>> C = X.sum <= 10 

84 >>> P += C, X[0,0] == 1 

85 >>> print(P) 

86 Example (Linear Program) 

87 maximize tr(X) 

88 over 

89 2×2 real variable X (bounded below) 

90 subject to 

91 ∑(X) ≤ 10 

92 X[0,0] = 1 

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

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

95 >>> solution.claimedStatus 

96 'optimal' 

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

98 0.002137422561645508 

99 >>> round(P, 1) 

100 10.0 

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

102 [ 1.00e+00 4.89e-10] 

103 [ 4.89e-10 9.00e+00] 

104 >>> round(C.dual, 1) 

105 1.0 

106 """ 

107 

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

109 CONIC_FORM = Specification( 

110 objectives=[expressions.AffineExpression], 

111 constraints=[C for C in 

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

113 if issubclass(C, constraints.ConicConstraint) 

114 and C is not constraints.ConicConstraint]) 

115 

116 # -------------------------------------------------------------------------- 

117 # Initialization and reset methods. 

118 # -------------------------------------------------------------------------- 

119 

120 def __init__( 

121 self, name=None, *, copyOptions=None, useOptions=None, 

122 **extra_options 

123 ): 

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

125 

126 :param str name: 

127 A name or title to give to the optimization problem. 

128 

129 :param copyOptions: 

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

131 the default options. 

132 

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

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

135 

136 :param extra_options: 

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

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

139 """ 

140 if name and not isinstance(name, str): 

141 raise TypeError( 

142 "The first positional argument denotes the name of the problem " 

143 "and must be a string.") 

144 

145 if copyOptions and useOptions: 

146 raise ValueError( 

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

148 

149 extra_options = map_legacy_options(**extra_options) 

150 

151 if copyOptions: 

152 self._options = copyOptions.copy() 

153 self._options.update(**extra_options) 

154 elif useOptions: 

155 self._options = useOptions 

156 self._options.update(**extra_options) 

157 else: 

158 self._options = Options(**extra_options) 

159 

160 #: Explicit name for the problem. 

161 self._name = name 

162 

163 #: The optimization objective. 

164 self._objective = Objective() 

165 

166 #: Maps constraint IDs to constraints. 

167 self._constraints = OrderedDict() 

168 

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

170 self._con_groups = [] 

171 

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

173 self._mtb_count = {} 

174 

175 #: Maps mutable names to mutables. 

176 self._mutables = OrderedDict() 

177 

178 #: Maps variable names to variables. 

179 self._variables = OrderedDict() 

180 

181 #: Maps parameter names to parameters. 

182 self._parameters = OrderedDict() 

183 

184 #: Current solution strategy. 

185 self._strategy = None 

186 

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

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

189 

190 def _reset_mutable_registry(self): 

191 self._mtb_count.clear() 

192 self._mutables.clear() 

193 self._variables.clear() 

194 self._parameters.clear() 

195 

196 def reset(self, resetOptions=False): 

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

198 

199 :param bool resetOptions: 

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

201 """ 

202 # Reset options if requested. 

203 if resetOptions: 

204 self._options.reset() 

205 

206 # Reset objective to "find an assignment". 

207 del self.objective 

208 

209 # Reset constraint registry. 

210 self._constraints.clear() 

211 self._con_groups.clear() 

212 

213 # Reset mutable registry. 

214 self._reset_mutable_registry() 

215 

216 # Reset strategy and solution data. 

217 self._strategy = None 

218 self._last_solution = None 

219 

220 # -------------------------------------------------------------------------- 

221 # Properties. 

222 # -------------------------------------------------------------------------- 

223 

224 @property 

225 def name(self): 

226 """Name or title of the problem.""" 

227 return self._name 

228 

229 @name.setter 

230 def name(self, value): 

231 if value and not isinstance(value, str): 

232 raise TypeError("The problem name must be a string.") 

233 

234 if not value: 

235 self._name = None 

236 else: 

237 self._name = value 

238 

239 @name.deleter 

240 def name(self): 

241 self._name = None 

242 

243 @property 

244 def mutables(self): 

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

246 

247 :returns: 

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

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

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

251 """ 

252 return MappingProxyType(self._mutables) 

253 

254 @property 

255 def variables(self): 

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

257 

258 :returns: 

259 See :attr:`mutables`. 

260 """ 

261 return MappingProxyType(self._variables) 

262 

263 @property 

264 def parameters(self): 

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

266 

267 :returns: 

268 See :attr:`mutables`. 

269 """ 

270 return MappingProxyType(self._parameters) 

271 

272 @property 

273 def constraints(self): 

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

275 

276 :returns: 

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

278 is that in which constraints were added. 

279 """ 

280 return MappingProxyType(self._constraints) 

281 

282 @constraints.deleter 

283 def constraints(self): 

284 # Clear constraint registry. 

285 self._constraints.clear() 

286 self._con_groups.clear() 

287 

288 # Update mutable registry. 

289 self._reset_mutable_registry() 

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

291 

292 @property 

293 def objective(self): 

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

295 return self._objective 

296 

297 @objective.setter 

298 def objective(self, value): 

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

300 

301 try: 

302 if isinstance(value, Objective): 

303 self._objective = value 

304 else: 

305 direction, function = value 

306 self._objective = Objective(direction, function) 

307 finally: 

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

309 

310 @objective.deleter 

311 def objective(self): 

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

313 

314 self._objective = Objective() 

315 

316 @property 

317 def no(self): 

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

319 

320 Either a minimization or a maximization objective, with feasibility 

321 posed as "minimize 0". 

322 

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

324 the :attr:`objective`. 

325 """ 

326 return self._objective.normalized 

327 

328 @property 

329 def minimize(self): 

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

331 

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

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

334 """ 

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

336 return self._objective.function 

337 else: 

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

339 

340 @minimize.setter 

341 def minimize(self, value): 

342 self.objective = "min", value 

343 

344 @minimize.deleter 

345 def minimize(self): 

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

347 del self.objective 

348 else: 

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

350 

351 @property 

352 def maximize(self): 

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

354 

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

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

357 """ 

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

359 return self._objective.function 

360 else: 

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

362 

363 @maximize.setter 

364 def maximize(self, value): 

365 self.objective = "max", value 

366 

367 @maximize.deleter 

368 def maximize(self): 

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

370 del self.objective 

371 else: 

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

373 

374 @property 

375 def options(self): 

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

377 return self._options 

378 

379 @options.setter 

380 def options(self, value): 

381 if not isinstance(value, Options): 

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

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

384 

385 self._options = value 

386 

387 @options.deleter 

388 def options(self, value): 

389 self._options.reset() 

390 

391 @property 

392 def strategy(self): 

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

394 

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

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

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

398 

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

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

401 reformulation pipeline while existing reformulation work is not 

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

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

404 problem from scratch. 

405 

406 :Example: 

407 

408 >>> from picos import Problem, RealVariable 

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

410 >>> P = Problem() 

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

412 >>> print(P.strategy) 

413 None 

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

415 >>> print(P.strategy) 

416 1. ExtraOptions 

417 2. EpigraphReformulation 

418 3. SquaredNormToConicReformulation 

419 4. CVXOPTSolver 

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

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

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

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

424 True 

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

426 False 

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

428 

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

430 

431 >>> from picos.modeling import Strategy 

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

433 ... ConvexQuadraticToConicReformulation) 

434 >>> from picos.solvers import CVXOPTSolver 

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

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

437 ... ConvexQuadraticToConicReformulation) 

438 """ 

439 return self._strategy 

440 

441 @strategy.setter 

442 def strategy(self, value): 

443 from .strategy import Strategy 

444 

445 if not isinstance(value, Strategy): 

446 raise TypeError( 

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

448 .format(type(value).__name__)) 

449 

450 if value.problem is not self: 

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

452 "different problem.") 

453 

454 self._strategy = value 

455 

456 @strategy.deleter 

457 def strategy(self): 

458 self._strategy = None 

459 

460 @property 

461 def last_solution(self): 

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

463 return self._last_solution 

464 

465 @property 

466 def status(self): 

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

468 if not self._last_solution: 

469 return "unsolved" 

470 else: 

471 return self._last_solution.claimedStatus 

472 

473 @property 

474 def footprint(self): 

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

476 return Footprint.from_problem(self) 

477 

478 @property 

479 def continuous(self): 

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

481 return all( 

482 isinstance(variable, expressions.CONTINUOUS_VARTYPES) 

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

484 

485 @property 

486 def pure_integer(self): 

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

488 return not any( 

489 isinstance(variable, expressions.CONTINUOUS_VARTYPES) 

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

491 

492 @property 

493 def type(self): 

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

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

496 objective = self._objective.function 

497 base = "Optimization Problem" 

498 

499 linear = [ 

500 constraints.AffineConstraint, 

501 constraints.ComplexAffineConstraint, 

502 constraints.AbsoluteValueConstraint, 

503 constraints.SimplexConstraint, 

504 constraints.FlowConstraint] 

505 sdp = [ 

506 constraints.LMIConstraint, 

507 constraints.ComplexLMIConstraint] 

508 quadratic = [ 

509 constraints.ConvexQuadraticConstraint, 

510 constraints.ConicQuadraticConstraint, 

511 constraints.NonconvexQuadraticConstraint] 

512 quadconic = [ 

513 constraints.SOCConstraint, 

514 constraints.RSOCConstraint] 

515 exponential = [ 

516 constraints.ExpConeConstraint, 

517 constraints.SumExponentialsConstraint, 

518 constraints.LogSumExpConstraint, 

519 constraints.LogConstraint, 

520 constraints.KullbackLeiblerConstraint] 

521 complex = [ 

522 constraints.ComplexAffineConstraint, 

523 constraints.ComplexLMIConstraint] 

524 

525 if objective is None: 

526 if not C: 

527 base = "Empty Problem" 

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

529 base = "Linear Feasibility Problem" 

530 else: 

531 base = "Feasibility Problem" 

532 elif isinstance(objective, expressions.AffineExpression): 

533 if not C: 

534 if objective.constant: 

535 base = "Constant Problem" 

536 else: 

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

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

539 base = "Linear Program" 

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

541 base = "Second Order Cone Program" 

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

543 base = "Semidefinite Program" 

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

545 base = "Geometric Program" 

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

547 base = "Exponential Program" 

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

549 base = "Quadratically Constrained Program" 

550 elif isinstance(objective, expressions.QuadraticExpression): 

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

552 base = "Quadratic Program" 

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

554 base = "Quadratically Constrained Quadratic Program" 

555 elif isinstance(objective, expressions.LogSumExp): 

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

557 base = "Geometric Program" 

558 

559 if self.continuous: 

560 integrality = "" 

561 elif self.pure_integer: 

562 integrality = "Integer " 

563 else: 

564 integrality = "Mixed-Integer " 

565 

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

567 complexity = "Complex " 

568 else: 

569 complexity = "" 

570 

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

572 

573 @property 

574 def dual(self): 

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

576 

577 More precisely, this property invokes the following: 

578 

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

580 minimization problem, with variable bounds expressed as additional 

581 constraints. 

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

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

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

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

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

587 problem returned will be the respective other. 

588 

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

590 If no reformulation strategy was found. 

591 

592 .. note:: 

593 

594 This property is intended for educational purposes. 

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

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

597 """ 

598 from ..reforms import Dualization 

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

600 

601 @property 

602 def conic_form(self): 

603 """The problem in conic form. 

604 

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

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

607 

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

609 If no reformulation strategy was found. 

610 

611 :Example: 

612 

613 >>> from picos import Problem, RealVariable 

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

615 >>> P = Problem() 

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

617 >>> print(P) 

618 Quadratic Program 

619 minimize ‖x‖² 

620 over 

621 2×1 real variable x 

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

623 Second Order Cone Program 

624 minimize __..._t 

625 over 

626 1×1 real variable __..._t 

627 2×1 real variable x 

628 subject to 

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

630 

631 .. note:: 

632 

633 This property is intended for educational purposes. 

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

635 perform the necessary reformulations automatically. 

636 """ 

637 return self.reformulated(self.CONIC_FORM) 

638 

639 # -------------------------------------------------------------------------- 

640 # Python special methods, except __init__. 

641 # -------------------------------------------------------------------------- 

642 

643 @property 

644 def _var_groups(self): 

645 """Support :meth:`__str__`.""" 

646 vars_by_type = {} 

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

648 vtype = type(var).__name__ 

649 shape = var.shape 

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

651 index = (vtype, shape, bound) 

652 

653 vars_by_type.setdefault(index, set()) 

654 vars_by_type[index].add(var) 

655 

656 groups = [] 

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

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

659 

660 return groups 

661 

662 @property 

663 def _prm_groups(self): 

664 """Support :meth:`__str__`.""" 

665 prms_by_type = {} 

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

667 vtype = type(prm).__name__ 

668 shape = prm.shape 

669 index = (vtype, shape) 

670 

671 prms_by_type.setdefault(index, set()) 

672 prms_by_type[index].add(prm) 

673 

674 groups = [] 

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

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

677 

678 return groups 

679 

680 @lru_cache() 

681 def _mtb_group_string(self, group): 

682 """Support :meth:`__str__`.""" 

683 if len(group) == 0: 

684 return "[no mutables]" 

685 

686 if len(group) == 1: 

687 return group[0].long_string 

688 

689 try: 

690 template, data = parameterized_string( 

691 [mtb.long_string for mtb in group]) 

692 except ValueError: 

693 # HACK: Use the plural of the type string (e.g. "real variables"). 

694 type_string = group[0]._get_type_string_base().lower() 

695 base_string = group[0].long_string.replace( 

696 type_string, type_string + "s") 

697 

698 # HACK: Move any bound string to the end. 

699 match = re.match(r"([^(]*)( \([^)]*\))", base_string) 

700 if match: 

701 base_string = match[1] 

702 bound_string = match[2] 

703 else: 

704 bound_string = "" 

705 

706 return base_string \ 

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

708 else: 

709 return glyphs.forall(template, data) 

710 

711 @lru_cache() 

712 def _con_group_string(self, group): 

713 """Support :meth:`__str__`.""" 

714 if len(group) == 0: 

715 return "[no constraints]" 

716 

717 if len(group) == 1: 

718 return str(group[0]) 

719 

720 try: 

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

722 except ValueError: 

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

724 else: 

725 return glyphs.forall(template, data) 

726 

727 def __repr__(self): 

728 if self._name: 

729 return glyphs.repr2(self.type, self._name) 

730 else: 

731 return glyphs.repr1(self.type) 

732 

733 def __str__(self): 

734 # Print problem name (if available) and type. 

735 if self._name: 

736 string = "{} ({})\n".format(self._name, self.type) 

737 else: 

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

739 

740 # Print objective. 

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

742 

743 wrapper = TextWrapper( 

744 initial_indent=" "*4, 

745 subsequent_indent=" "*6, 

746 break_long_words=False, 

747 break_on_hyphens=False) 

748 

749 # Print variables. 

750 if self._variables: 

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

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

753 for group in self._var_groups: 

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

755 string += "\n" 

756 

757 # Print constraints. 

758 if self._constraints: 

759 string += " subject to\n" 

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

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

762 string += "\n" 

763 

764 # Print parameters. 

765 if self._parameters: 

766 string += " given\n" 

767 for group in self._prm_groups: 

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

769 string += "\n" 

770 

771 return string.rstrip("\n") 

772 

773 def __iadd__(self, constraints): 

774 """See :meth:`require`.""" 

775 if isinstance(constraints, tuple): 

776 self.require(*constraints) 

777 else: 

778 self.require(constraints) 

779 

780 return self 

781 

782 # -------------------------------------------------------------------------- 

783 # Bookkeeping methods. 

784 # -------------------------------------------------------------------------- 

785 

786 def _register_mutables(self, mtbs): 

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

788 # Register every mutable at most once per call. 

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

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

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

792 

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

794 old_mtbs = self._mutables 

795 new_mtbs = OrderedDict( 

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

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

798 if isinstance(mtb, BaseVariable)) 

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

800 if not isinstance(mtb, BaseVariable)) 

801 

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

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

804 raise ValueError( 

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

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

807 

808 # Check for mutable name clashes with existing mutables. 

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

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

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

812 "problem because it already tracks another mutable with " 

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

814 

815 # Keep track of new mutables. 

816 self._mutables.update(new_mtbs) 

817 self._variables.update(new_vars) 

818 self._parameters.update(new_prms) 

819 

820 # Count up the mutable references. 

821 for mtb in mtbs: 

822 self._mtb_count.setdefault(mtb, 0) 

823 self._mtb_count[mtb] += 1 

824 

825 def _unregister_mutables(self, mtbs): 

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

827 # Unregister every mutable at most once per call. 

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

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

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

831 

832 for mtb in mtbs: 

833 name = mtb.name 

834 

835 # Make sure the mutable is properly registered. 

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

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

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

839 "Found a nonpostive mutable count." 

840 

841 # Count down the mutable references. 

842 self._mtb_count[mtb] -= 1 

843 

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

845 if not self._mtb_count[mtb]: 

846 self._mtb_count.pop(mtb) 

847 self._mutables.pop(name) 

848 

849 if isinstance(mtb, BaseVariable): 

850 self._variables.pop(name) 

851 else: 

852 self._parameters.pop(name) 

853 

854 # -------------------------------------------------------------------------- 

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

856 # -------------------------------------------------------------------------- 

857 

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

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

860 

861 :param str direction: 

862 Case insensitive search direction string. One of 

863 

864 - ``"min"`` or ``"minimize"``, 

865 - ``"max"`` or ``"maximize"``, 

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

867 

868 :param ~picos.expressions.Expression expression: 

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

870 problem. 

871 """ 

872 self.objective = direction, expression 

873 

874 # -------------------------------------------------------------------------- 

875 # Methods to add, retrieve and remove constraints. 

876 # -------------------------------------------------------------------------- 

877 

878 def _lookup_constraint(self, idOrIndOrCon): 

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

880 

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

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

883 part of the problem. 

884 """ 

885 if isinstance(idOrIndOrCon, int): 

886 if idOrIndOrCon in self._constraints: 

887 # A valid ID. 

888 return idOrIndOrCon 

889 elif idOrIndOrCon < len(self._constraints): 

890 # An offset. 

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

892 else: 

893 raise LookupError( 

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

895 .format(idOrIndOrCon)) 

896 elif isinstance(idOrIndOrCon, constraints.Constraint): 

897 # A constraint object. 

898 id = idOrIndOrCon.id 

899 if id in self._constraints: 

900 return id 

901 else: 

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

903 .format(idOrIndOrCon)) 

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

905 if len(idOrIndOrCon) == 1: 

906 groupIndex = idOrIndOrCon[0] 

907 if groupIndex < len(self._con_groups): 

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

909 else: 

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

911 elif len(idOrIndOrCon) == 2: 

912 groupIndex, groupOffset = idOrIndOrCon 

913 if groupIndex < len(self._con_groups): 

914 group = self._con_groups[groupIndex] 

915 if groupOffset < len(group): 

916 return group[groupOffset].id 

917 else: 

918 raise IndexError( 

919 "Constraint group offset out of range.") 

920 else: 

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

922 else: 

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

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

925 else: 

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

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

928 

929 def get_constraint(self, idOrIndOrCon): 

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

931 

932 :param idOrIndOrCon: One of the following: 

933 

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

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

936 * The integer ID of the constraint. 

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

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

939 were added. 

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

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

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

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

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

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

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

947 

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

949 

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

951 thereof. 

952 

953 :Example: 

954 

955 >>> import picos as pic 

956 >>> import cvxopt as cvx 

957 >>> from pprint import pprint 

958 >>> prob=pic.Problem() 

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

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

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

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

963 >>> print(prob) 

964 Linear Feasibility Problem 

965 find an assignment 

966 for 

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

968 5×1 real variable y 

969 subject to 

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

971 y ≥ 0 

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

973 >>> prob.get_constraint(1) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

988 """ 

989 idOrIds = self._lookup_constraint(idOrIndOrCon) 

990 

991 if isinstance(idOrIds, list): 

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

993 else: 

994 return self._constraints[idOrIds] 

995 

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

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

998 

999 :param constraint: 

1000 The constraint to be added. 

1001 :type constraint: 

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

1003 

1004 :param key: DEPRECATED 

1005 

1006 :returns: 

1007 The constraint that was added to the problem. 

1008 

1009 .. note:: 

1010 

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

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

1013 """ 

1014 # Handle deprecated 'key' parameter. 

1015 if key is not None: 

1016 throw_deprecation_warning( 

1017 "Naming constraints is currently not supported.") 

1018 

1019 # Register the constraint. 

1020 self._constraints[constraint.id] = constraint 

1021 self._con_groups.append([constraint]) 

1022 

1023 # Register the constraint's mutables. 

1024 self._register_mutables(constraint.mutables) 

1025 

1026 return constraint 

1027 

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

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

1030 

1031 :param lst: 

1032 Iterable of constraints to add. 

1033 

1034 :param it: DEPRECATED 

1035 :param indices: DEPRECATED 

1036 :param key: DEPRECATED 

1037 

1038 :returns: 

1039 A list of all constraints that were added. 

1040 

1041 :Example: 

1042 

1043 >>> import picos as pic 

1044 >>> import cvxopt as cvx 

1045 >>> from pprint import pprint 

1046 >>> prob=pic.Problem() 

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

1048 >>> pprint(x) 

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

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

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

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

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

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

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

1056 >>> w={} 

1057 >>> for ij in IJ: 

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

1059 ... 

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

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

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

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

1064 >>> print(prob) 

1065 Feasibility Problem 

1066 find an assignment 

1067 for 

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

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

1070 5×1 real variable y 

1071 subject to 

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

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

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

1075 

1076 .. note:: 

1077 

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

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

1080 """ 

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

1082 # Deprecated as of 2.0. 

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

1084 "add_list_of_constraints are deprecated and ignored.") 

1085 

1086 added = [] 

1087 for constraint in lst: 

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

1089 self._con_groups.pop() 

1090 

1091 if added: 

1092 self._con_groups.append(added) 

1093 

1094 return added 

1095 

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

1097 """Add constraints to the problem. 

1098 

1099 :param constraints: 

1100 A sequence of constraints or constraint groups (iterables yielding 

1101 constraints) or a mix thereof. 

1102 

1103 :param bool ret: 

1104 Whether to return the added constraints. 

1105 

1106 :returns: 

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

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

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

1110 constraints or constraint groups represented as above. When 

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

1112 

1113 :Example: 

1114 

1115 >>> from picos import Problem, RealVariable 

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

1117 >>> P = Problem() 

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

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

1120 >>> print(P) 

1121 Linear Feasibility Problem 

1122 find an assignment 

1123 for 

1124 5×1 real variable x 

1125 subject to 

1126 x ≥ [-1] 

1127 x ≤ [1] 

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

1129 

1130 .. note:: 

1131 

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

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

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

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

1136 """ 

1137 from ..constraints import Constraint 

1138 

1139 added = [] 

1140 for constraint in constraints: 

1141 if isinstance(constraint, Constraint): 

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

1143 else: 

1144 try: 

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

1146 raise TypeError 

1147 except TypeError: 

1148 raise TypeError( 

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

1150 "yielding constraints.") from None 

1151 else: 

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

1153 

1154 if ret: 

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

1156 

1157 def _con_group_index(self, conOrConID): 

1158 """Support :meth:`remove_constraint`.""" 

1159 if isinstance(conOrConID, int): 

1160 constraint = self._constraints[conOrConID] 

1161 else: 

1162 constraint = conOrConID 

1163 

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

1165 for j, candidate in enumerate(group): 

1166 if candidate is constraint: 

1167 return i, j 

1168 

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

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

1171 "registries are out of sync.") 

1172 else: 

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

1174 

1175 def remove_constraint(self, idOrIndOrCon): 

1176 """Delete a constraint from the problem. 

1177 

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

1179 

1180 :Example: 

1181 

1182 >>> import picos 

1183 >>> from pprint import pprint 

1184 >>> P = picos.Problem() 

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

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

1187 >>> Cxy = P.add_list_of_constraints( 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1203 >>> P.remove_constraint(1) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1240 """ 

1241 idOrIds = self._lookup_constraint(idOrIndOrCon) 

1242 

1243 removedCons = [] 

1244 

1245 if isinstance(idOrIds, list): 

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

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

1248 self._con_groups.pop(groupIndex) 

1249 for id in idOrIds: 

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

1251 else: 

1252 constraint = self._constraints.pop(idOrIds) 

1253 removedCons.append(constraint) 

1254 groupIndex, groupOffset = self._con_group_index(constraint) 

1255 group = self._con_groups[groupIndex] 

1256 group.pop(groupOffset) 

1257 if not group: 

1258 self._con_groups.pop(groupIndex) 

1259 

1260 # Unregister the mutables added by the removed constraints. 

1261 for con in removedCons: 

1262 self._unregister_mutables(con.mutables) 

1263 

1264 def remove_all_constraints(self): 

1265 """Remove all constraints from the problem. 

1266 

1267 .. note:: 

1268 

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

1270 """ 

1271 del self.constraints 

1272 

1273 # -------------------------------------------------------------------------- 

1274 # Borderline legacy methods to deal with variables. 

1275 # -------------------------------------------------------------------------- 

1276 

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

1278 

1279 def get_variable(self, name): 

1280 """Retrieve variables referenced by the problem. 

1281 

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

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

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

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

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

1287 of ``param`` as keys. 

1288 

1289 .. note:: 

1290 

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

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

1293 problem's objective function or constraints. 

1294 

1295 :param str name: 

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

1297 

1298 :returns: 

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

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

1301 

1302 :Example: 

1303 

1304 >>> from picos import Problem, RealVariable 

1305 >>> from pprint import pprint 

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

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

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

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

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

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

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

1313 >>> P = Problem() 

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

1315 >>> print(P) 

1316 Linear Program 

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

1318 over 

1319 1×1 real variables x, y[0], y[1], y[2], y[3], z[alice], z[bob], 

1320 z[carol] 

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

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

1323 <1×1 Real Variable: x> 

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

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

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

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

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

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

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

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

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

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

1334 True 

1335 """ 

1336 if name in self._variables: 

1337 return self._variables[name] 

1338 else: 

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

1340 params = [] 

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

1342 match = self._PARAMETERIZED_VARIABLE_REGEX.match(otherName) 

1343 if not match: 

1344 continue 

1345 base, param = match.groups() 

1346 if name == base: 

1347 params.append(param) 

1348 

1349 if params: 

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

1351 try: 

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

1353 except ValueError: 

1354 pass 

1355 else: 

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

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

1358 for param in intParams] 

1359 

1360 # Otherwise return a dict. 

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

1362 for param in params} 

1363 else: 

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

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

1366 

1367 def get_valued_variable(self, name): 

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

1369 

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

1371 variable's :attr:`values <.valuable.Valuable.value>` instead of the 

1372 variable objects. 

1373 

1374 :raises ~picos.expressions.NotValued: 

1375 If any of the selected variables is not valued. 

1376 """ 

1377 exp = self.get_variable(name) 

1378 if isinstance(exp, list): 

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

1380 exp[i] = exp[i].value 

1381 elif isinstance(exp, dict): 

1382 for i in exp: 

1383 exp[i] = exp[i].value 

1384 else: 

1385 exp = exp.value 

1386 return exp 

1387 

1388 # -------------------------------------------------------------------------- 

1389 # Methods to create copies of the problem. 

1390 # -------------------------------------------------------------------------- 

1391 

1392 def copy(self): 

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

1394 the_copy = Problem(copyOptions=self._options) 

1395 

1396 # Duplicate the mutables. 

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

1398 

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

1400 for group in self._con_groups: 

1401 the_copy.add_list_of_constraints( 

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

1403 

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

1405 direction, function = self._objective 

1406 if function is not None: 

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

1408 

1409 return the_copy 

1410 

1411 def continuous_relaxation(self, copy_other_mutables=True): 

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

1413 

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

1415 

1416 :param bool copy_other_mutables: 

1417 Whether variables that are already continuous as well as parameters 

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

1419 these mutables with the original problem. 

1420 """ 

1421 the_copy = Problem(copyOptions=self._options) 

1422 

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

1424 new_mtbs = {} 

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

1426 if isinstance(var, expressions.IntegerVariable): 

1427 new_mtbs[name] = expressions.RealVariable( 

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

1429 elif isinstance(var, expressions.BinaryVariable): 

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

1431 else: 

1432 if copy_other_mutables: 

1433 new_mtbs[name] = var.copy() 

1434 else: 

1435 new_mtbs[name] = var 

1436 

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

1438 for group in self._con_groups: 

1439 the_copy.add_list_of_constraints( 

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

1441 

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

1443 direction, function = self._objective 

1444 if function is not None: 

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

1446 

1447 return the_copy 

1448 

1449 def clone(self, copyOptions=True): 

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

1451 

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

1453 objective function and thereby references the existing variables and 

1454 parameters that appear in these objects. 

1455 

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

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

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

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

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

1461 

1462 :param bool copyOptions: 

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

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

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

1466 """ 

1467 # Start with a shallow copy of self. 

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

1469 theClone = pycopy.copy(self) 

1470 

1471 # Make the constraint registry independent. 

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

1473 theClone._con_groups = [] 

1474 for group in self._con_groups: 

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

1476 

1477 # Make the mutable registry independent. 

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

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

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

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

1482 

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

1484 theClone._strategy = None 

1485 

1486 # Make the solver options independent, if requested. 

1487 if copyOptions: 

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

1489 

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

1491 # - objective: Is immutable as a tuple. 

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

1493 

1494 return theClone 

1495 

1496 # -------------------------------------------------------------------------- 

1497 # Methods to solve or export the problem. 

1498 # -------------------------------------------------------------------------- 

1499 

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

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

1502 

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

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

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

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

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

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

1509 

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

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

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

1513 and the number of performed reformulations, respectively. 

1514 

1515 :param int steps: 

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

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

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

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

1520 which may also output the problem itself, depending on 

1521 ``extra_options``. 

1522 

1523 :param extra_options: 

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

1525 

1526 :returns: 

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

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

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

1530 

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

1532 If no solution strategy was found. 

1533 

1534 :raises ValueError: 

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

1536 

1537 :Example: 

1538 

1539 >>> from picos import Problem, RealVariable 

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

1541 >>> P = Problem() 

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

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

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

1545 1. ExtraOptions 

1546 2. EpigraphReformulation 

1547 3. SquaredNormToConicReformulation 

1548 4. CVXOPTSolver 

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

1550 3 

1551 >>> print(P) 

1552 Quadratic Program 

1553 minimize ‖x‖² 

1554 over 

1555 2×1 real variable x 

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

1557 Second Order Cone Program 

1558 minimize __..._t 

1559 over 

1560 1×1 real variable __..._t 

1561 2×1 real variable x 

1562 subject to 

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

1564 """ 

1565 from .strategy import Strategy 

1566 

1567 # Produce a strategy for the clone. 

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

1569 numReforms = len(strategy.reforms) 

1570 

1571 if steps is None: 

1572 steps = numReforms 

1573 

1574 if steps == 0: 

1575 return self 

1576 elif steps > numReforms: 

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

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

1579 

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

1581 lastReform = strategy.reforms[steps - 1] 

1582 oldSuccessor = lastReform.successor 

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

1584 "execute": lambda self: Solution( 

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

1586 

1587 # Execute the cut-short strategy. 

1588 strategy.execute(**extra_options) 

1589 

1590 # Repair the last reformulation. 

1591 lastReform.successor = oldSuccessor 

1592 

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

1594 output = lastReform.output 

1595 if output is not self: 

1596 output.prepared_strategy = strategy 

1597 output.prepared_steps = steps 

1598 

1599 return output 

1600 

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

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

1603 

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

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

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

1607 for more details. 

1608 

1609 :param specification: 

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

1611 :type specification: 

1612 ~picos.modeling.Specification 

1613 

1614 :param extra_options: 

1615 Additional solver options to use with this reformulation only. 

1616 

1617 :returns: 

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

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

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

1621 

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

1623 If no reformulation strategy was found. 

1624 

1625 :Example: 

1626 

1627 >>> from picos import Problem, RealVariable 

1628 >>> from picos.modeling import Specification 

1629 >>> from picos.expressions import AffineExpression 

1630 >>> from picos.constraints import ( 

1631 ... AffineConstraint, SOCConstraint, RSOCConstraint) 

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

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

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

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

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

1637 >>> P = Problem() 

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

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

1640 >>> print(P) 

1641 Quadratic Program 

1642 minimize ‖x‖² 

1643 over 

1644 2×1 real variable x 

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

1646 Second Order Cone Program 

1647 minimize __..._t 

1648 over 

1649 1×1 real variable __..._t 

1650 2×1 real variable x 

1651 subject to 

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

1653 

1654 .. note:: 

1655 

1656 This method is intended for educational purposes. 

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

1658 perform the necessary reformulations automatically. 

1659 """ 

1660 if not isinstance(specification, Specification): 

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

1662 "Specification object.") 

1663 

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

1665 def placeholder(the_self): 

1666 raise RuntimeError("The dummy solver created by " 

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

1668 

1669 # Declare a dummy solver that accepts specified problems. 

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

1671 # Abstract class methods. 

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

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

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

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

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

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

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

1679 

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

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

1682 

1683 # Abstract instance methods. 

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

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

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

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

1688 }) 

1689 

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

1691 oldAdHocSolver = self.options.ad_hoc_solver 

1692 extra_options["ad_hoc_solver"] = DummySolver 

1693 problem = self.prepared(**extra_options) 

1694 

1695 # Restore the ad_hoc_solver option of the original problem. 

1696 problem.options.ad_hoc_solver = oldAdHocSolver 

1697 

1698 return problem 

1699 

1700 def solve(self, **extra_options): 

1701 """Hand the problem to a solver. 

1702 

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

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

1705 platform. 

1706 

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

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

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

1710 missing or incomplete. 

1711 

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

1713 access the solution as follows: 

1714 

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

1716 - The variables' :attr:`~.valuable.Valuable.value` is set according 

1717 to the primal solution. You can in fact query the value of any 

1718 expression involving valued variables like this. 

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

1720 according to the dual solution. 

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

1722 changed, depending on the parameter. 

1723 

1724 :param extra_options: 

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

1726 search only. In particular, this lets you 

1727 

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

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

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

1731 and 

1732 - skip valuing variables or constraints with 

1733 ``apply_solution=False``. 

1734 

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

1736 A solution object or list thereof. 

1737 

1738 :raises ~picos.SolutionFailure: 

1739 In the following cases: 

1740 

1741 1. No solution strategy was found. 

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

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

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

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

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

1747 

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

1749 exception. 

1750 """ 

1751 from .strategy import NoStrategyFound, Strategy 

1752 

1753 startTime = time.time() 

1754 

1755 extra_options = map_legacy_options(**extra_options) 

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

1757 verbose = options.verbosity > 0 

1758 

1759 with picos_box(show=verbose): 

1760 if verbose: 

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

1762 

1763 # Reset an outdated strategy. 

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

1765 if verbose: 

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

1767 

1768 self._strategy = None 

1769 

1770 # Find a new solution strategy, if necessary. 

1771 if not self._strategy: 

1772 if verbose: 

1773 if options.ad_hoc_solver: 

1774 solverName = options.ad_hoc_solver.get_via_name() 

1775 elif options.solver: 

1776 solverName = get_solver(options.solver).get_via_name() 

1777 else: 

1778 solverName = None 

1779 

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

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

1782 

1783 try: 

1784 self._strategy = Strategy.from_problem( 

1785 self, **extra_options) 

1786 except NoStrategyFound as error: 

1787 s = str(error) 

1788 

1789 if verbose: 

1790 print(s, flush=True) 

1791 

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

1793 from error 

1794 

1795 if verbose: 

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

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

1798 else: 

1799 if verbose: 

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

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

1802 

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

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

1805 

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

1807 if isinstance(solutions, list): 

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

1809 

1810 if not solutions: 

1811 raise SolutionFailure( 

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

1813 

1814 solution = solutions[0] 

1815 

1816 if verbose: 

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

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

1819 else: 

1820 assert isinstance(solutions, Solution) 

1821 solution = solutions 

1822 

1823 # Report claimed solution state. 

1824 if verbose: 

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

1826 solution.claimedStatus, solution.problemStatus)) 

1827 

1828 # Validate the primal solution. 

1829 if options.primals: 

1830 vars_ = self._variables.values() 

1831 if solution.primalStatus != SS_OPTIMAL: 

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

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

1834 .format(solution.primalStatus)) 

1835 elif None in solution.primals.values() \ 

1836 or any(var not in solution.primals for var in vars_): 

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

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

1839 

1840 # Validate the dual solution. 

1841 if options.duals: 

1842 cons = self._constraints.values() 

1843 if solution.dualStatus != SS_OPTIMAL: 

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

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

1846 solution.dualStatus)) 

1847 elif None in solution.duals.values() \ 

1848 or any(con not in solution.duals for con in cons): 

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

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

1851 

1852 if options.apply_solution: 

1853 if verbose: 

1854 print("Applying the solution.") 

1855 

1856 # Apply the (first) solution. 

1857 solution.apply(snapshotStatus=True) 

1858 

1859 # Store all solutions produced by the solver. 

1860 self._last_solution = solutions 

1861 

1862 # Report verified solution state. 

1863 if verbose: 

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

1865 

1866 endTime = time.time() 

1867 solveTime = endTime - startTime 

1868 searchTime = solution.searchTime 

1869 

1870 if searchTime: 

1871 overhead = (solveTime - searchTime) / searchTime 

1872 else: 

1873 overhead = float("inf") 

1874 

1875 if verbose: 

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

1877 .format(searchTime, solveTime, overhead)) 

1878 

1879 if settings.RETURN_SOLUTION: 

1880 return solutions 

1881 

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

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

1884 write(self, filename, writer) 

1885 

1886 # -------------------------------------------------------------------------- 

1887 # Methods to query the problem. 

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

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

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

1891 # -------------------------------------------------------------------------- 

1892 

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

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

1895 

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

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

1898 

1899 :param float tol: 

1900 Largest tolerated absolute violation of a constraint or variable 

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

1902 used. 

1903 

1904 :param inttol: 

1905 DEPRECATED 

1906 

1907 :returns: 

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

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

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

1911 otherwise. 

1912 

1913 :raises picos.uncertain.IntractableWorstCase: 

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

1915 expression is not supported. 

1916 """ 

1917 if inttol is not None: 

1918 throw_deprecation_warning("Variable integrality is now ensured on " 

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

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

1921 

1922 if tol is None: 

1923 tol = self._options.abs_prim_fsb_tol 

1924 

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

1926 all_cons += [ 

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

1928 if variable.bound_constraint] 

1929 

1930 largest_violation = 0.0 

1931 

1932 for constraint in all_cons: 

1933 try: 

1934 slack = constraint.slack 

1935 except IntractableWorstCase as error: 

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

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

1938 from None 

1939 

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

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

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

1943 

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

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

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

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

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

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

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

1951 # feasible and declared infeasible here. 

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

1953 # the logic below. 

1954 from ..expressions import Constant, PositiveSemidefiniteCone 

1955 if isinstance(constraint, 

1956 constraints.uncertain.ScenarioUncertainConicConstraint) \ 

1957 and isinstance(constraint.cone, PositiveSemidefiniteCone): 

1958 hack = True 

1959 slack = Constant(slack).desvec.safe_value 

1960 else: 

1961 hack = False 

1962 

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

1964 # Check hermitian-ness of slack. 

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

1966 if violation > tol: 

1967 largest_violation = max(largest_violation, violation) 

1968 

1969 # Check positive semidefiniteness of slack. 

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

1971 if violation > tol: 

1972 largest_violation = max(largest_violation, violation) 

1973 else: 

1974 violation = -float(min(slack)) 

1975 if violation > tol: 

1976 largest_violation = max(largest_violation, violation) 

1977 

1978 return (not largest_violation, largest_violation) 

1979 

1980 # -------------------------------------------------------------------------- 

1981 # Abstract method implementations for the Valuable base class. 

1982 # -------------------------------------------------------------------------- 

1983 

1984 def _get_valuable_string(self): 

1985 return "problem with {}".format(self._objective._get_valuable_string()) 

1986 

1987 def _get_value(self): 

1988 return self._objective._get_value() 

1989 

1990 # -------------------------------------------------------------------------- 

1991 # Legacy methods and properties. 

1992 # -------------------------------------------------------------------------- 

1993 

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

1995 "removed together with that code." 

1996 

1997 @property 

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

1999 def countVar(self): 

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

2001 return len(self._variables) 

2002 

2003 @property 

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

2005 def countCons(self): 

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

2007 return len(self._variables) 

2008 

2009 @property 

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

2011 def numberOfVars(self): 

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

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

2014 

2015 @property 

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

2017 def numberLSEConstraints(self): 

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

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

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

2021 

2022 @property 

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

2024 def numberSDPConstraints(self): 

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

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

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

2028 

2029 @property 

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

2031 def numberQuadConstraints(self): 

2032 """Number of quadratic constraints stored.""" 

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

2034 constraints.ConvexQuadraticConstraint, 

2035 constraints.ConicQuadraticConstraint, 

2036 constraints.NonconvexQuadraticConstraint))]) 

2037 

2038 @property 

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

2040 def numberConeConstraints(self): 

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

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

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

2044 

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

2046 def obj_value(self): 

2047 """Objective function value. 

2048 

2049 :raises AttributeError: 

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

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

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

2053 to denote an unvalued expression would raise 

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

2055 """ 

2056 if self._objective.feasibility: 

2057 raise AttributeError( 

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

2059 

2060 value = self.value 

2061 

2062 if self.value is None: 

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

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

2065 else: 

2066 return value 

2067 

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

2069 def is_continuous(self): 

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

2071 return self.continuous 

2072 

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

2074 def is_pure_integer(self): 

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

2076 return self.pure_integer 

2077 

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

2079 def set_all_options_to_default(self): 

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

2081 self._options.reset() 

2082 

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

2084 def set_option(self, key, val): 

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

2086 

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

2088 :param val: New value for the option. 

2089 """ 

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

2091 self._options[key] = val 

2092 

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

2094 def update_options(self, **options): 

2095 """Set multiple solver options at once. 

2096 

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

2098 """ 

2099 options = map_legacy_options(**options) 

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

2101 self._options[key] = val 

2102 

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

2104 def verbosity(self): 

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

2106 return self._options.verbosity 

2107 

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

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

2110 def add_variable( 

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

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

2113 

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

2115 

2116 :param size: 

2117 The shape of the variable. 

2118 :type size: 

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

2120 

2121 :param str vtype: 

2122 Domain of the variable. Can be any of 

2123 

2124 - ``'continuous'`` -- real valued, 

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

2126 - ``'integer'`` -- integer valued, 

2127 - ``'symmetric'`` -- symmetric matrix, 

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

2129 - ``'complex'`` -- complex matrix, 

2130 - ``'hermitian'`` -- complex hermitian matrix. 

2131 

2132 :param lower: 

2133 A lower bound on the variable. 

2134 :type lower: 

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

2136 

2137 :param upper: 

2138 An upper bound on the variable. 

2139 :type upper: 

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

2141 

2142 :returns: 

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

2144 

2145 :Example: 

2146 

2147 >>> from picos import Problem 

2148 >>> P = Problem() 

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

2150 >>> x 

2151 <3×1 Real Variable: x> 

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

2153 >>> P.variables 

2154 mappingproxy(OrderedDict()) 

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

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

2157 >>> P.variables 

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

2159 """ 

2160 if vtype == "continuous": 

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

2162 elif vtype == "binary": 

2163 return expressions.BinaryVariable(name, size) 

2164 elif vtype == "integer": 

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

2166 elif vtype == "symmetric": 

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

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

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

2170 elif vtype == "complex": 

2171 return expressions.ComplexVariable(name, size) 

2172 elif vtype == "hermitian": 

2173 return expressions.HermitianVariable(name, size) 

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

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

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

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

2178 else: 

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

2180 

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

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

2183 def remove_variable(self, name): 

2184 """Does nothing.""" 

2185 pass 

2186 

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

2188 def set_var_value(self, name, value): 

2189 """Set the :attr:`~.valuable.Valuable.value` of a variable. 

2190 

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

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

2193 

2194 :param str name: 

2195 Name of the variable to be valued. 

2196 

2197 :param value: 

2198 The value to be set. 

2199 :type value: 

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

2201 """ 

2202 try: 

2203 variable = self._variables[name] 

2204 except KeyError: 

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

2206 .format(name)) from None 

2207 else: 

2208 variable.value = value 

2209 

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

2211 def as_dual(self): 

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

2213 return self.dual 

2214 

2215 

2216# -------------------------------------- 

2217__all__ = api_end(_API_START, globals())