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

776 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-12 07:53 +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 quantum = [ 

522 expressions.QuantumEntropy, 

523 expressions.NegativeQuantumEntropy, 

524 expressions.QuantumConditionalEntropy, 

525 expressions.QuantumKeyDistribution, 

526 constraints.QuantRelEntropyConstraint, 

527 constraints.QuantCondEntropyConstraint, 

528 constraints.QuantKeyDistributionConstraint] 

529 complex = [ 

530 constraints.ComplexAffineConstraint, 

531 constraints.ComplexLMIConstraint] 

532 

533 if objective is None: 

534 if not C: 

535 base = "Empty Problem" 

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

537 base = "Linear Feasibility Problem" 

538 else: 

539 base = "Feasibility Problem" 

540 elif isinstance(objective, expressions.AffineExpression): 

541 if not C: 

542 if objective.constant: 

543 base = "Constant Problem" 

544 else: 

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

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

547 base = "Linear Program" 

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

549 base = "Second Order Cone Program" 

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

551 base = "Semidefinite Program" 

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

553 base = "Geometric Program" 

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

555 base = "Exponential Program" 

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

557 base = "Quadratically Constrained Program" 

558 elif C.issubset(set(linear + sdp + quantum)): 

559 base = "Quantum Relative Entropy Program" 

560 elif isinstance(objective, expressions.QuadraticExpression): 

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

562 base = "Quadratic Program" 

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

564 base = "Quadratically Constrained Quadratic Program" 

565 elif isinstance(objective, expressions.LogSumExp): 

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

567 base = "Geometric Program" 

568 elif isinstance(objective, tuple(quantum)): 

569 if C.issubset(set(linear + sdp + quantum)): 

570 base = "Quantum Relative Entropy Program" 

571 elif isinstance(objective, expressions.WeightedSum): 

572 if all(isinstance(expr, tuple(linear + quantum)) 

573 for expr in objective.expressions): 

574 if C.issubset(set(linear + sdp + quantum)): 

575 base = "Quantum Relative Entropy Program" 

576 

577 if self.continuous: 

578 integrality = "" 

579 elif self.pure_integer: 

580 integrality = "Integer " 

581 else: 

582 integrality = "Mixed-Integer " 

583 

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

585 complexity = "Complex " 

586 else: 

587 complexity = "" 

588 

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

590 

591 @property 

592 def dual(self): 

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

594 

595 More precisely, this property invokes the following: 

596 

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

598 minimization problem, with variable bounds expressed as additional 

599 constraints. 

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

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

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

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

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

605 problem returned will be the respective other. 

606 

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

608 If no reformulation strategy was found. 

609 

610 .. note:: 

611 

612 This property is intended for educational purposes. 

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

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

615 """ 

616 from ..reforms import Dualization 

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

618 

619 @property 

620 def conic_form(self): 

621 """The problem in conic form. 

622 

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

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

625 

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

627 If no reformulation strategy was found. 

628 

629 :Example: 

630 

631 >>> from picos import Problem, RealVariable 

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

633 >>> P = Problem() 

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

635 >>> print(P) 

636 Quadratic Program 

637 minimize ‖x‖² 

638 over 

639 2×1 real variable x 

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

641 Second Order Cone Program 

642 minimize __..._t 

643 over 

644 1×1 real variable __..._t 

645 2×1 real variable x 

646 subject to 

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

648 

649 .. note:: 

650 

651 This property is intended for educational purposes. 

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

653 perform the necessary reformulations automatically. 

654 """ 

655 return self.reformulated(self.CONIC_FORM) 

656 

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

658 # Python special methods, except __init__. 

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

660 

661 @property 

662 def _var_groups(self): 

663 """Support :meth:`__str__`.""" 

664 vars_by_type = {} 

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

666 vtype = type(var).__name__ 

667 shape = var.shape 

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

669 index = (vtype, shape, bound) 

670 

671 vars_by_type.setdefault(index, set()) 

672 vars_by_type[index].add(var) 

673 

674 groups = [] 

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

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

677 

678 return groups 

679 

680 @property 

681 def _prm_groups(self): 

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

683 prms_by_type = {} 

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

685 vtype = type(prm).__name__ 

686 shape = prm.shape 

687 index = (vtype, shape) 

688 

689 prms_by_type.setdefault(index, set()) 

690 prms_by_type[index].add(prm) 

691 

692 groups = [] 

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

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

695 

696 return groups 

697 

698 @lru_cache() 

699 def _mtb_group_string(self, group): 

700 """Support :meth:`__str__`.""" 

701 if len(group) == 0: 

702 return "[no mutables]" 

703 

704 if len(group) == 1: 

705 return group[0].long_string 

706 

707 try: 

708 template, data = parameterized_string( 

709 [mtb.long_string for mtb in group]) 

710 except ValueError: 

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

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

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

714 type_string, type_string + "s") 

715 

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

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

718 if match: 

719 base_string = match[1] 

720 bound_string = match[2] 

721 else: 

722 bound_string = "" 

723 

724 return base_string \ 

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

726 else: 

727 return glyphs.forall(template, data) 

728 

729 @lru_cache() 

730 def _con_group_string(self, group): 

731 """Support :meth:`__str__`.""" 

732 if len(group) == 0: 

733 return "[no constraints]" 

734 

735 if len(group) == 1: 

736 return str(group[0]) 

737 

738 try: 

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

740 except ValueError: 

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

742 else: 

743 return glyphs.forall(template, data) 

744 

745 def __repr__(self): 

746 if self._name: 

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

748 else: 

749 return glyphs.repr1(self.type) 

750 

751 def __str__(self): 

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

753 if self._name: 

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

755 else: 

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

757 

758 # Print objective. 

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

760 

761 wrapper = TextWrapper( 

762 initial_indent=" "*4, 

763 subsequent_indent=" "*6, 

764 break_long_words=False, 

765 break_on_hyphens=False) 

766 

767 # Print variables. 

768 if self._variables: 

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

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

771 for group in self._var_groups: 

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

773 string += "\n" 

774 

775 # Print constraints. 

776 if self._constraints: 

777 string += " subject to\n" 

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

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

780 string += "\n" 

781 

782 # Print parameters. 

783 if self._parameters: 

784 string += " given\n" 

785 for group in self._prm_groups: 

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

787 string += "\n" 

788 

789 return string.rstrip("\n") 

790 

791 def __iadd__(self, constraints): 

792 """See :meth:`require`.""" 

793 if isinstance(constraints, tuple): 

794 self.require(*constraints) 

795 else: 

796 self.require(constraints) 

797 

798 return self 

799 

800 # -------------------------------------------------------------------------- 

801 # Bookkeeping methods. 

802 # -------------------------------------------------------------------------- 

803 

804 def _register_mutables(self, mtbs): 

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

806 # Register every mutable at most once per call. 

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

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

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

810 

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

812 old_mtbs = self._mutables 

813 new_mtbs = OrderedDict( 

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

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

816 if isinstance(mtb, BaseVariable)) 

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

818 if not isinstance(mtb, BaseVariable)) 

819 

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

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

822 raise ValueError( 

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

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

825 

826 # Check for mutable name clashes with existing mutables. 

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

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

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

830 "problem because it already tracks another mutable with " 

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

832 

833 # Keep track of new mutables. 

834 self._mutables.update(new_mtbs) 

835 self._variables.update(new_vars) 

836 self._parameters.update(new_prms) 

837 

838 # Count up the mutable references. 

839 for mtb in mtbs: 

840 self._mtb_count.setdefault(mtb, 0) 

841 self._mtb_count[mtb] += 1 

842 

843 def _unregister_mutables(self, mtbs): 

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

845 # Unregister every mutable at most once per call. 

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

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

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

849 

850 for mtb in mtbs: 

851 name = mtb.name 

852 

853 # Make sure the mutable is properly registered. 

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

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

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

857 "Found a nonpostive mutable count." 

858 

859 # Count down the mutable references. 

860 self._mtb_count[mtb] -= 1 

861 

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

863 if not self._mtb_count[mtb]: 

864 self._mtb_count.pop(mtb) 

865 self._mutables.pop(name) 

866 

867 if isinstance(mtb, BaseVariable): 

868 self._variables.pop(name) 

869 else: 

870 self._parameters.pop(name) 

871 

872 # -------------------------------------------------------------------------- 

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

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

875 

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

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

878 

879 :param str direction: 

880 Case insensitive search direction string. One of 

881 

882 - ``"min"`` or ``"minimize"``, 

883 - ``"max"`` or ``"maximize"``, 

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

885 

886 :param ~picos.expressions.Expression expression: 

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

888 problem. 

889 """ 

890 self.objective = direction, expression 

891 

892 # -------------------------------------------------------------------------- 

893 # Methods to add, retrieve and remove constraints. 

894 # -------------------------------------------------------------------------- 

895 

896 def _lookup_constraint(self, idOrIndOrCon): 

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

898 

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

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

901 part of the problem. 

902 """ 

903 if isinstance(idOrIndOrCon, int): 

904 if idOrIndOrCon in self._constraints: 

905 # A valid ID. 

906 return idOrIndOrCon 

907 elif idOrIndOrCon < len(self._constraints): 

908 # An offset. 

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

910 else: 

911 raise LookupError( 

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

913 .format(idOrIndOrCon)) 

914 elif isinstance(idOrIndOrCon, constraints.Constraint): 

915 # A constraint object. 

916 id = idOrIndOrCon.id 

917 if id in self._constraints: 

918 return id 

919 else: 

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

921 .format(idOrIndOrCon)) 

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

923 if len(idOrIndOrCon) == 1: 

924 groupIndex = idOrIndOrCon[0] 

925 if groupIndex < len(self._con_groups): 

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

927 else: 

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

929 elif len(idOrIndOrCon) == 2: 

930 groupIndex, groupOffset = idOrIndOrCon 

931 if groupIndex < len(self._con_groups): 

932 group = self._con_groups[groupIndex] 

933 if groupOffset < len(group): 

934 return group[groupOffset].id 

935 else: 

936 raise IndexError( 

937 "Constraint group offset out of range.") 

938 else: 

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

940 else: 

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

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

943 else: 

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

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

946 

947 def get_constraint(self, idOrIndOrCon): 

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

949 

950 :param idOrIndOrCon: One of the following: 

951 

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

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

954 * The integer ID of the constraint. 

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

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

957 were added. 

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

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

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

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

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

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

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

965 

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

967 

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

969 thereof. 

970 

971 :Example: 

972 

973 >>> import picos as pic 

974 >>> import cvxopt as cvx 

975 >>> from pprint import pprint 

976 >>> prob=pic.Problem() 

977 >>> x=[pic.RealVariable('x[{0}]'.format(i),2) for i in range(5)] 

978 >>> y=pic.RealVariable('y',5) 

979 >>> Cx=prob.add_list_of_constraints([x[i].sum < y[i] for i in range(5)]) 

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

981 >>> print(prob) 

982 Linear Feasibility Problem 

983 find an assignment 

984 for 

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

986 5×1 real variable y 

987 subject to 

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

989 y ≥ 0 

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

991 >>> prob.get_constraint(1) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1006 """ 

1007 idOrIds = self._lookup_constraint(idOrIndOrCon) 

1008 

1009 if isinstance(idOrIds, list): 

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

1011 else: 

1012 return self._constraints[idOrIds] 

1013 

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

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

1016 

1017 :param constraint: 

1018 The constraint to be added. 

1019 :type constraint: 

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

1021 

1022 :param key: DEPRECATED 

1023 

1024 :returns: 

1025 The constraint that was added to the problem. 

1026 

1027 .. note:: 

1028 

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

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

1031 """ 

1032 # Handle deprecated 'key' parameter. 

1033 if key is not None: 

1034 throw_deprecation_warning( 

1035 "Naming constraints is currently not supported.") 

1036 

1037 # Register the constraint. 

1038 self._constraints[constraint.id] = constraint 

1039 self._con_groups.append([constraint]) 

1040 

1041 # Register the constraint's mutables. 

1042 self._register_mutables(constraint.mutables) 

1043 

1044 return constraint 

1045 

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

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

1048 

1049 :param lst: 

1050 Iterable of constraints to add. 

1051 

1052 :param it: DEPRECATED 

1053 :param indices: DEPRECATED 

1054 :param key: DEPRECATED 

1055 

1056 :returns: 

1057 A list of all constraints that were added. 

1058 

1059 :Example: 

1060 

1061 >>> import picos as pic 

1062 >>> import cvxopt as cvx 

1063 >>> from pprint import pprint 

1064 >>> prob=pic.Problem() 

1065 >>> x=[pic.RealVariable('x[{0}]'.format(i),2) for i in range(5)] 

1066 >>> pprint(x) 

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

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

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

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

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

1072 >>> y=pic.RealVariable('y',5) 

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

1074 >>> w={} 

1075 >>> for ij in IJ: 

1076 ... w[ij]=pic.RealVariable('w[{},{}]'.format(*ij),3) 

1077 ... 

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

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

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

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

1082 >>> print(prob) 

1083 Feasibility Problem 

1084 find an assignment 

1085 for 

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

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

1088 5×1 real variable y 

1089 subject to 

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

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

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

1093 

1094 .. note:: 

1095 

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

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

1098 """ 

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

1100 # Deprecated as of 2.0. 

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

1102 "add_list_of_constraints are deprecated and ignored.") 

1103 

1104 added = [] 

1105 for constraint in lst: 

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

1107 self._con_groups.pop() 

1108 

1109 if added: 

1110 self._con_groups.append(added) 

1111 

1112 return added 

1113 

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

1115 """Add constraints to the problem. 

1116 

1117 :param constraints: 

1118 A sequence of constraints or constraint groups (iterables yielding 

1119 constraints) or a mix thereof. 

1120 

1121 :param bool ret: 

1122 Whether to return the added constraints. 

1123 

1124 :returns: 

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

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

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

1128 constraints or constraint groups represented as above. When 

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

1130 

1131 :Example: 

1132 

1133 >>> from picos import Problem, RealVariable 

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

1135 >>> P = Problem() 

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

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

1138 >>> print(P) 

1139 Linear Feasibility Problem 

1140 find an assignment 

1141 for 

1142 5×1 real variable x 

1143 subject to 

1144 x ≥ [-1] 

1145 x ≤ [1] 

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

1147 

1148 .. note:: 

1149 

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

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

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

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

1154 """ 

1155 from ..constraints import Constraint 

1156 

1157 added = [] 

1158 for constraint in constraints: 

1159 if isinstance(constraint, Constraint): 

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

1161 else: 

1162 try: 

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

1164 raise TypeError 

1165 except TypeError: 

1166 raise TypeError( 

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

1168 "yielding constraints.") from None 

1169 else: 

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

1171 

1172 if ret: 

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

1174 

1175 def _con_group_index(self, conOrConID): 

1176 """Support :meth:`remove_constraint`.""" 

1177 if isinstance(conOrConID, int): 

1178 constraint = self._constraints[conOrConID] 

1179 else: 

1180 constraint = conOrConID 

1181 

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

1183 for j, candidate in enumerate(group): 

1184 if candidate is constraint: 

1185 return i, j 

1186 

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

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

1189 "registries are out of sync.") 

1190 else: 

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

1192 

1193 def remove_constraint(self, idOrIndOrCon): 

1194 """Delete a constraint from the problem. 

1195 

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

1197 

1198 :Example: 

1199 

1200 >>> import picos 

1201 >>> from pprint import pprint 

1202 >>> P = picos.Problem() 

1203 >>> x = [picos.RealVariable('x[{0}]'.format(i), 2) for i in range(4)] 

1204 >>> y = picos.RealVariable('y', 4) 

1205 >>> Cxy = P.add_list_of_constraints( 

1206 ... [x[i].sum <= y[i] for i in range(4)]) 

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

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

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

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

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

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

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

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

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

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 <2×1 Affine Constraint: x[3] ≤ [1]>] 

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

1221 >>> P.remove_constraint(1) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1258 """ 

1259 idOrIds = self._lookup_constraint(idOrIndOrCon) 

1260 

1261 removedCons = [] 

1262 

1263 if isinstance(idOrIds, list): 

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

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

1266 self._con_groups.pop(groupIndex) 

1267 for id in idOrIds: 

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

1269 else: 

1270 constraint = self._constraints.pop(idOrIds) 

1271 removedCons.append(constraint) 

1272 groupIndex, groupOffset = self._con_group_index(constraint) 

1273 group = self._con_groups[groupIndex] 

1274 group.pop(groupOffset) 

1275 if not group: 

1276 self._con_groups.pop(groupIndex) 

1277 

1278 # Unregister the mutables added by the removed constraints. 

1279 for con in removedCons: 

1280 self._unregister_mutables(con.mutables) 

1281 

1282 def remove_all_constraints(self): 

1283 """Remove all constraints from the problem. 

1284 

1285 .. note:: 

1286 

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

1288 """ 

1289 del self.constraints 

1290 

1291 # -------------------------------------------------------------------------- 

1292 # Borderline legacy methods to deal with variables. 

1293 # -------------------------------------------------------------------------- 

1294 

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

1296 

1297 def get_variable(self, name): 

1298 """Retrieve variables referenced by the problem. 

1299 

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

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

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

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

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

1305 of ``param`` as keys. 

1306 

1307 .. note:: 

1308 

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

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

1311 problem's objective function or constraints. 

1312 

1313 :param str name: 

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

1315 

1316 :returns: 

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

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

1319 

1320 :Example: 

1321 

1322 >>> from picos import Problem, RealVariable 

1323 >>> from pprint import pprint 

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

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

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

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

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

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

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

1331 >>> P = Problem() 

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

1333 >>> print(P) 

1334 Linear Program 

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

1336 over 

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

1338 z[carol] 

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

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

1341 <1×1 Real Variable: x> 

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

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

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

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

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

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

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

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

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

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

1352 True 

1353 """ 

1354 if name in self._variables: 

1355 return self._variables[name] 

1356 else: 

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

1358 params = [] 

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

1360 match = self._PARAMETERIZED_VARIABLE_REGEX.match(otherName) 

1361 if not match: 

1362 continue 

1363 base, param = match.groups() 

1364 if name == base: 

1365 params.append(param) 

1366 

1367 if params: 

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

1369 try: 

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

1371 except ValueError: 

1372 pass 

1373 else: 

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

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

1376 for param in intParams] 

1377 

1378 # Otherwise return a dict. 

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

1380 for param in params} 

1381 else: 

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

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

1384 

1385 def get_valued_variable(self, name): 

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

1387 

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

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

1390 variable objects. 

1391 

1392 :raises ~picos.expressions.NotValued: 

1393 If any of the selected variables is not valued. 

1394 """ 

1395 exp = self.get_variable(name) 

1396 if isinstance(exp, list): 

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

1398 exp[i] = exp[i].value 

1399 elif isinstance(exp, dict): 

1400 for i in exp: 

1401 exp[i] = exp[i].value 

1402 else: 

1403 exp = exp.value 

1404 return exp 

1405 

1406 # -------------------------------------------------------------------------- 

1407 # Methods to create copies of the problem. 

1408 # -------------------------------------------------------------------------- 

1409 

1410 def copy(self): 

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

1412 the_copy = Problem(copyOptions=self._options) 

1413 

1414 # Duplicate the mutables. 

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

1416 

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

1418 for group in self._con_groups: 

1419 the_copy.add_list_of_constraints( 

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

1421 

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

1423 direction, function = self._objective 

1424 if function is not None: 

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

1426 

1427 return the_copy 

1428 

1429 def continuous_relaxation(self, copy_other_mutables=True): 

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

1431 

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

1433 

1434 :param bool copy_other_mutables: 

1435 Whether variables that are already continuous as well as parameters 

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

1437 these mutables with the original problem. 

1438 """ 

1439 the_copy = Problem(copyOptions=self._options) 

1440 

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

1442 new_mtbs = {} 

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

1444 if isinstance(var, expressions.IntegerVariable): 

1445 new_mtbs[name] = expressions.RealVariable( 

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

1447 elif isinstance(var, expressions.BinaryVariable): 

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

1449 else: 

1450 if copy_other_mutables: 

1451 new_mtbs[name] = var.copy() 

1452 else: 

1453 new_mtbs[name] = var 

1454 

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

1456 for group in self._con_groups: 

1457 the_copy.add_list_of_constraints( 

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

1459 

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

1461 direction, function = self._objective 

1462 if function is not None: 

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

1464 

1465 return the_copy 

1466 

1467 def clone(self, copyOptions=True): 

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

1469 

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

1471 objective function and thereby references the existing variables and 

1472 parameters that appear in these objects. 

1473 

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

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

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

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

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

1479 

1480 :param bool copyOptions: 

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

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

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

1484 """ 

1485 # Start with a shallow copy of self. 

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

1487 theClone = pycopy.copy(self) 

1488 

1489 # Make the constraint registry independent. 

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

1491 theClone._con_groups = [] 

1492 for group in self._con_groups: 

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

1494 

1495 # Make the mutable registry independent. 

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

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

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

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

1500 

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

1502 theClone._strategy = None 

1503 

1504 # Make the solver options independent, if requested. 

1505 if copyOptions: 

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

1507 

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

1509 # - objective: Is immutable as a tuple. 

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

1511 

1512 return theClone 

1513 

1514 # -------------------------------------------------------------------------- 

1515 # Methods to solve or export the problem. 

1516 # -------------------------------------------------------------------------- 

1517 

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

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

1520 

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

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

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

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

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

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

1527 

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

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

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

1531 and the number of performed reformulations, respectively. 

1532 

1533 :param int steps: 

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

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

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

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

1538 which may also output the problem itself, depending on 

1539 ``extra_options``. 

1540 

1541 :param extra_options: 

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

1543 

1544 :returns: 

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

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

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

1548 

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

1550 If no solution strategy was found. 

1551 

1552 :raises ValueError: 

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

1554 

1555 :Example: 

1556 

1557 >>> from picos import Problem, RealVariable 

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

1559 >>> P = Problem() 

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

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

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

1563 1. ExtraOptions 

1564 2. EpigraphReformulation 

1565 3. SquaredNormToConicReformulation 

1566 4. CVXOPTSolver 

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

1568 3 

1569 >>> print(P) 

1570 Quadratic Program 

1571 minimize ‖x‖² 

1572 over 

1573 2×1 real variable x 

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

1575 Second Order Cone Program 

1576 minimize __..._t 

1577 over 

1578 1×1 real variable __..._t 

1579 2×1 real variable x 

1580 subject to 

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

1582 """ 

1583 from .strategy import Strategy 

1584 

1585 # Produce a strategy for the clone. 

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

1587 numReforms = len(strategy.reforms) 

1588 

1589 if steps is None: 

1590 steps = numReforms 

1591 

1592 if steps == 0: 

1593 return self 

1594 elif steps > numReforms: 

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

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

1597 

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

1599 lastReform = strategy.reforms[steps - 1] 

1600 oldSuccessor = lastReform.successor 

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

1602 "execute": lambda self: Solution( 

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

1604 

1605 # Execute the cut-short strategy. 

1606 strategy.execute(**extra_options) 

1607 

1608 # Repair the last reformulation. 

1609 lastReform.successor = oldSuccessor 

1610 

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

1612 output = lastReform.output 

1613 if output is not self: 

1614 output.prepared_strategy = strategy 

1615 output.prepared_steps = steps 

1616 

1617 return output 

1618 

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

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

1621 

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

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

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

1625 for more details. 

1626 

1627 :param specification: 

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

1629 :type specification: 

1630 ~picos.modeling.Specification 

1631 

1632 :param extra_options: 

1633 Additional solver options to use with this reformulation only. 

1634 

1635 :returns: 

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

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

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

1639 

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

1641 If no reformulation strategy was found. 

1642 

1643 :Example: 

1644 

1645 >>> from picos import Problem, RealVariable 

1646 >>> from picos.modeling import Specification 

1647 >>> from picos.expressions import AffineExpression 

1648 >>> from picos.constraints import ( 

1649 ... AffineConstraint, SOCConstraint, RSOCConstraint) 

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

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

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

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

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

1655 >>> P = Problem() 

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

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

1658 >>> print(P) 

1659 Quadratic Program 

1660 minimize ‖x‖² 

1661 over 

1662 2×1 real variable x 

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

1664 Second Order Cone Program 

1665 minimize __..._t 

1666 over 

1667 1×1 real variable __..._t 

1668 2×1 real variable x 

1669 subject to 

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

1671 

1672 .. note:: 

1673 

1674 This method is intended for educational purposes. 

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

1676 perform the necessary reformulations automatically. 

1677 """ 

1678 if not isinstance(specification, Specification): 

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

1680 "Specification object.") 

1681 

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

1683 def placeholder(the_self): 

1684 raise RuntimeError("The dummy solver created by " 

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

1686 

1687 # Declare a dummy solver that accepts specified problems. 

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

1689 # Abstract class methods. 

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

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

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

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

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

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

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

1697 

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

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

1700 

1701 # Abstract instance methods. 

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

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

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

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

1706 }) 

1707 

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

1709 oldAdHocSolver = self.options.ad_hoc_solver 

1710 extra_options["ad_hoc_solver"] = DummySolver 

1711 problem = self.prepared(**extra_options) 

1712 

1713 # Restore the ad_hoc_solver option of the original problem. 

1714 problem.options.ad_hoc_solver = oldAdHocSolver 

1715 

1716 return problem 

1717 

1718 def solve(self, **extra_options): 

1719 """Hand the problem to a solver. 

1720 

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

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

1723 platform. 

1724 

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

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

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

1728 missing or incomplete. 

1729 

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

1731 access the solution as follows: 

1732 

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

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

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

1736 expression involving valued variables like this. 

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

1738 according to the dual solution. 

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

1740 changed, depending on the parameter. 

1741 

1742 :param extra_options: 

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

1744 search only. In particular, this lets you 

1745 

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

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

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

1749 and 

1750 - skip valuing variables or constraints with 

1751 ``apply_solution=False``. 

1752 

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

1754 A solution object or list thereof. 

1755 

1756 :raises ~picos.SolutionFailure: 

1757 In the following cases: 

1758 

1759 1. No solution strategy was found. 

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

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

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

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

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

1765 

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

1767 exception. 

1768 """ 

1769 from .strategy import NoStrategyFound, Strategy 

1770 

1771 startTime = time.time() 

1772 

1773 extra_options = map_legacy_options(**extra_options) 

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

1775 verbose = options.verbosity > 0 

1776 

1777 with picos_box(show=verbose): 

1778 if verbose: 

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

1780 

1781 # Reset an outdated strategy. 

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

1783 if verbose: 

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

1785 

1786 self._strategy = None 

1787 

1788 # Find a new solution strategy, if necessary. 

1789 if not self._strategy: 

1790 if verbose: 

1791 if options.ad_hoc_solver: 

1792 solverName = options.ad_hoc_solver.get_via_name() 

1793 elif options.solver: 

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

1795 else: 

1796 solverName = None 

1797 

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

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

1800 

1801 try: 

1802 self._strategy = Strategy.from_problem( 

1803 self, **extra_options) 

1804 except NoStrategyFound as error: 

1805 s = str(error) 

1806 

1807 if verbose: 

1808 print(s, flush=True) 

1809 

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

1811 from error 

1812 

1813 if verbose: 

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

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

1816 else: 

1817 if verbose: 

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

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

1820 

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

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

1823 

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

1825 if isinstance(solutions, list): 

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

1827 

1828 if not solutions: 

1829 raise SolutionFailure( 

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

1831 

1832 solution = solutions[0] 

1833 

1834 if verbose: 

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

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

1837 else: 

1838 assert isinstance(solutions, Solution) 

1839 solution = solutions 

1840 

1841 # Report claimed solution state. 

1842 if verbose: 

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

1844 solution.claimedStatus, solution.problemStatus)) 

1845 

1846 # Validate the primal solution. 

1847 if options.primals: 

1848 vars_ = self._variables.values() 

1849 if solution.primalStatus != SS_OPTIMAL: 

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

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

1852 .format(solution.primalStatus)) 

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

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

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

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

1857 

1858 # Validate the dual solution. 

1859 if options.duals: 

1860 cons = self._constraints.values() 

1861 if solution.dualStatus != SS_OPTIMAL: 

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

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

1864 solution.dualStatus)) 

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

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

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

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

1869 

1870 if options.apply_solution: 

1871 if verbose: 

1872 print("Applying the solution.") 

1873 

1874 # Apply the (first) solution. 

1875 solution.apply(snapshotStatus=True) 

1876 

1877 # Store all solutions produced by the solver. 

1878 self._last_solution = solutions 

1879 

1880 # Report verified solution state. 

1881 if verbose: 

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

1883 

1884 endTime = time.time() 

1885 solveTime = endTime - startTime 

1886 searchTime = solution.searchTime 

1887 

1888 if searchTime: 

1889 overhead = (solveTime - searchTime) / searchTime 

1890 else: 

1891 overhead = float("inf") 

1892 

1893 if verbose: 

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

1895 .format(searchTime, solveTime, overhead)) 

1896 

1897 if settings.RETURN_SOLUTION: 

1898 return solutions 

1899 

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

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

1902 write(self, filename, writer) 

1903 

1904 # -------------------------------------------------------------------------- 

1905 # Methods to query the problem. 

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

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

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

1909 # -------------------------------------------------------------------------- 

1910 

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

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

1913 

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

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

1916 

1917 :param float tol: 

1918 Largest tolerated absolute violation of a constraint or variable 

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

1920 used. 

1921 

1922 :param inttol: 

1923 DEPRECATED 

1924 

1925 :returns: 

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

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

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

1929 otherwise. 

1930 

1931 :raises picos.uncertain.IntractableWorstCase: 

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

1933 expression is not supported. 

1934 """ 

1935 if inttol is not None: 

1936 throw_deprecation_warning("Variable integrality is now ensured on " 

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

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

1939 

1940 if tol is None: 

1941 tol = self._options.abs_prim_fsb_tol 

1942 

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

1944 all_cons += [ 

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

1946 if variable.bound_constraint] 

1947 

1948 largest_violation = 0.0 

1949 

1950 for constraint in all_cons: 

1951 try: 

1952 slack = constraint.slack 

1953 except IntractableWorstCase as error: 

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

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

1956 from None 

1957 

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

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

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

1961 

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

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

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

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

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

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

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

1969 # feasible and declared infeasible here. 

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

1971 # the logic below. 

1972 from ..expressions import Constant, PositiveSemidefiniteCone 

1973 if isinstance(constraint, 

1974 constraints.uncertain.ScenarioUncertainConicConstraint) \ 

1975 and isinstance(constraint.cone, PositiveSemidefiniteCone): 

1976 hack = True 

1977 slack = Constant(slack).desvec.safe_value 

1978 else: 

1979 hack = False 

1980 

1981 psd_constraints = ( 

1982 constraints.LMIConstraint, 

1983 constraints.OpRelEntropyConstraint, 

1984 constraints.MatrixGeoMeanEpiConstraint, 

1985 constraints.MatrixGeoMeanHypoConstraint, 

1986 ) 

1987 

1988 if isinstance(constraint, psd_constraints) or hack: 

1989 # Check hermitian-ness of slack. 

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

1991 if violation > tol: 

1992 largest_violation = max(largest_violation, violation) 

1993 

1994 # Check positive semidefiniteness of slack. 

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

1996 if violation > tol: 

1997 largest_violation = max(largest_violation, violation) 

1998 else: 

1999 violation = -float(min(slack)) 

2000 if violation > tol: 

2001 largest_violation = max(largest_violation, violation) 

2002 

2003 return (not largest_violation, largest_violation) 

2004 

2005 # -------------------------------------------------------------------------- 

2006 # Abstract method implementations for the Valuable base class. 

2007 # -------------------------------------------------------------------------- 

2008 

2009 def _get_valuable_string(self): 

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

2011 

2012 def _get_value(self): 

2013 return self._objective._get_value() 

2014 

2015 # -------------------------------------------------------------------------- 

2016 # Legacy methods and properties. 

2017 # -------------------------------------------------------------------------- 

2018 

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

2020 "removed together with that code." 

2021 

2022 @property 

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

2024 def countVar(self): 

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

2026 return len(self._variables) 

2027 

2028 @property 

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

2030 def countCons(self): 

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

2032 return len(self._variables) 

2033 

2034 @property 

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

2036 def numberOfVars(self): 

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

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

2039 

2040 @property 

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

2042 def numberLSEConstraints(self): 

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

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

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

2046 

2047 @property 

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

2049 def numberSDPConstraints(self): 

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

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

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

2053 

2054 @property 

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

2056 def numberQuadConstraints(self): 

2057 """Number of quadratic constraints stored.""" 

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

2059 constraints.ConvexQuadraticConstraint, 

2060 constraints.ConicQuadraticConstraint, 

2061 constraints.NonconvexQuadraticConstraint))]) 

2062 

2063 @property 

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

2065 def numberConeConstraints(self): 

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

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

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

2069 

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

2071 def obj_value(self): 

2072 """Objective function value. 

2073 

2074 :raises AttributeError: 

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

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

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

2078 to denote an unvalued expression would raise 

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

2080 """ 

2081 if self._objective.feasibility: 

2082 raise AttributeError( 

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

2084 

2085 value = self.value 

2086 

2087 if self.value is None: 

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

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

2090 else: 

2091 return value 

2092 

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

2094 def is_continuous(self): 

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

2096 return self.continuous 

2097 

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

2099 def is_pure_integer(self): 

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

2101 return self.pure_integer 

2102 

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

2104 def set_all_options_to_default(self): 

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

2106 self._options.reset() 

2107 

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

2109 def set_option(self, key, val): 

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

2111 

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

2113 :param val: New value for the option. 

2114 """ 

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

2116 self._options[key] = val 

2117 

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

2119 def update_options(self, **options): 

2120 """Set multiple solver options at once. 

2121 

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

2123 """ 

2124 options = map_legacy_options(**options) 

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

2126 self._options[key] = val 

2127 

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

2129 def verbosity(self): 

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

2131 return self._options.verbosity 

2132 

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

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

2135 def add_variable( 

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

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

2138 

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

2140 

2141 :param size: 

2142 The shape of the variable. 

2143 :type size: 

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

2145 

2146 :param str vtype: 

2147 Domain of the variable. Can be any of 

2148 

2149 - ``'continuous'`` -- real valued, 

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

2151 - ``'integer'`` -- integer valued, 

2152 - ``'symmetric'`` -- symmetric matrix, 

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

2154 - ``'complex'`` -- complex matrix, 

2155 - ``'hermitian'`` -- complex hermitian matrix. 

2156 

2157 :param lower: 

2158 A lower bound on the variable. 

2159 :type lower: 

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

2161 

2162 :param upper: 

2163 An upper bound on the variable. 

2164 :type upper: 

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

2166 

2167 :returns: 

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

2169 

2170 :Example: 

2171 

2172 >>> from picos import Problem, RealVariable 

2173 >>> P = Problem() 

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

2175 >>> x 

2176 <3×1 Real Variable: x> 

2177 >>> # Variables exist independently of problems: 

2178 >>> P.variables 

2179 mappingproxy(OrderedDict()) 

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

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

2182 >>> P.variables 

2183 mappingproxy(OrderedDict({'x': <3×1 Real Variable: x>})) 

2184 """ 

2185 if vtype == "continuous": 

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

2187 elif vtype == "binary": 

2188 return expressions.BinaryVariable(name, size) 

2189 elif vtype == "integer": 

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

2191 elif vtype == "symmetric": 

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

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

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

2195 elif vtype == "complex": 

2196 return expressions.ComplexVariable(name, size) 

2197 elif vtype == "hermitian": 

2198 return expressions.HermitianVariable(name, size) 

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

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

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

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

2203 else: 

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

2205 

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

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

2208 def remove_variable(self, name): 

2209 """Does nothing.""" 

2210 pass 

2211 

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

2213 def set_var_value(self, name, value): 

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

2215 

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

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

2218 

2219 :param str name: 

2220 Name of the variable to be valued. 

2221 

2222 :param value: 

2223 The value to be set. 

2224 :type value: 

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

2226 """ 

2227 try: 

2228 variable = self._variables[name] 

2229 except KeyError: 

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

2231 .format(name)) from None 

2232 else: 

2233 variable.value = value 

2234 

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

2236 def as_dual(self): 

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

2238 return self.dual 

2239 

2240 

2241# -------------------------------------- 

2242__all__ = api_end(_API_START, globals())