Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

2# Copyright (C) 2012-2017 Guillaume Sagnol 

3# Copyright (C) 2017-2020 Maximilian Stahlberg 

4# 

5# This file is part of PICOS. 

6# 

7# PICOS is free software: you can redistribute it and/or modify it under the 

8# terms of the GNU General Public License as published by the Free Software 

9# Foundation, either version 3 of the License, or (at your option) any later 

10# version. 

11# 

12# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY 

13# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 

14# A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

15# 

16# You should have received a copy of the GNU General Public License along with 

17# this program. If not, see <http://www.gnu.org/licenses/>. 

18# ------------------------------------------------------------------------------ 

19 

20"""Implementation of :class:`Problem`.""" 

21 

22import copy as pycopy 

23import re 

24import time 

25from collections import OrderedDict 

26from functools import lru_cache 

27from textwrap import TextWrapper 

28from types import MappingProxyType 

29 

30import cvxopt as cvx 

31import numpy as np 

32 

33from .. import constraints, expressions, glyphs 

34from ..apidoc import api_end, api_start 

35from ..caching import cached_property 

36from ..expressions.data import cvx2np 

37from ..expressions.uncertain import IntractableWorstCase, UncertainExpression 

38from ..expressions.variables import BaseVariable 

39from ..formatting import natsorted, parameterized_string, picos_box 

40from ..legacy import deprecated, map_legacy_options, throw_deprecation_warning 

41from ..solvers import Solver, get_solver 

42from .file_out import write 

43from .footprint import Footprint, Specification 

44from .options import Options 

45from .solution import SS_OPTIMAL, Solution 

46 

47_API_START = api_start(globals()) 

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

49 

50 

51class SolutionFailure(RuntimeError): 

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

53 

54 def __init__(self, code, message): 

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

56 

57 :param int code: 

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

59 

60 :param str message: 

61 Text description of the failure. 

62 """ 

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

64 self.code = code 

65 

66 #: Text description of the failure. 

67 self.message = message 

68 

69 def __str__(self): 

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

71 

72 

73class Objective: 

74 """An optimization objective composed of search direction and function.""" 

75 

76 #: Short string denoting a feasibility problem. 

77 FIND = "find" 

78 

79 #: Short string denoting a minimization problem. 

80 MIN = "min" 

81 

82 #: Short string denoting a maximization problem. 

83 MAX = "max" 

84 

85 def __init__(self, direction=None, function=None): 

86 """Construct an optimization objective. 

87 

88 :param str direction: 

89 Case insensitive search direction string. One of 

90 

91 - ``"min"`` or ``"minimize"``, 

92 - ``"max"`` or ``"maximize"``, 

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

94 

95 :param ~picos.expressions.Expression function: 

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

97 problem. 

98 """ 

99 if direction is None: 

100 direction = self.FIND 

101 else: 

102 if not isinstance(direction, str): 

103 raise TypeError("Search direction must be given as a string.") 

104 

105 lower = direction.lower() 

106 if lower == "find": 

107 direction = self.FIND 

108 elif lower.startswith("min"): 

109 direction = self.MIN 

110 elif lower.startswith("max"): 

111 direction = self.MAX 

112 else: 

113 raise ValueError( 

114 "Invalid search direction '{}'.".format(direction)) 

115 

116 if function is None: 

117 if direction != self.FIND: 

118 raise ValueError("Missing an objective function.") 

119 else: 

120 if direction == self.FIND: 

121 raise ValueError("May not specify an objective function for a " 

122 "feasiblity problem.") 

123 

124 if not isinstance(function, expressions.Expression): 

125 raise TypeError( 

126 "Objective function must be a PICOS expression.") 

127 

128 if len(function) != 1: 

129 raise TypeError("Objective function must be scalar.") 

130 

131 function = function.refined 

132 

133 if isinstance(function, expressions.ComplexAffineExpression) \ 

134 and function.complex: 

135 raise TypeError("Objective function may not be complex.") 

136 

137 self._direction = direction 

138 self._function = function 

139 

140 def __str__(self): 

141 if self._function is None: 

142 return "find an assignment" 

143 else: 

144 minimize = self._direction == self.MIN 

145 dir_str = "minimize" if minimize else "maximize" 

146 

147 if self._function.uncertain: 

148 obj_str = self._function.worst_case_string( 

149 "max" if minimize else "min") 

150 else: 

151 obj_str = self._function.string 

152 

153 return "{} {}".format(dir_str, obj_str) 

154 

155 def __repr__(self): 

156 return glyphs.repr1("Objective: {}".format(self)) 

157 

158 def __iter__(self): 

159 yield self._direction 

160 yield self._function 

161 

162 def __eq__(self, other): 

163 if not isinstance(other, Objective): 

164 return False 

165 

166 if self._direction != other._direction: 

167 return False 

168 

169 if self._direction == self.FIND: 

170 return True 

171 

172 try: 

173 return self._function.equals(other._function) 

174 except AttributeError: 

175 # TODO: Allow all expressions to be equality-checked? 

176 return self._function is other._function 

177 

178 @property 

179 def feasibility(self): 

180 """Whether the objective is "find an assignment".""" 

181 return self._function is None 

182 

183 @property 

184 def pair(self): 

185 """Search direction and objective function as a pair.""" 

186 return self._direction, self._objective 

187 

188 @property 

189 def direction(self): 

190 """Search direction as a short string.""" 

191 return self._direction 

192 

193 @property 

194 def function(self): 

195 """Objective function.""" 

196 return self._function 

197 

198 @cached_property 

199 def normalized(self): 

200 """The objective but with feasiblity posed as "minimize 0". 

201 

202 >>> from picos import Objective 

203 >>> obj = Objective(); obj 

204 <Objective: find an assignment> 

205 >>> obj.normalized 

206 <Objective: minimize 0> 

207 """ 

208 if self._function is None: 

209 return Objective(self.MIN, expressions.AffineExpression.zero()) 

210 else: 

211 return self 

212 

213 @property 

214 def value(self): 

215 """Value of the objective function. 

216 

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

218 objective value. 

219 

220 :raises picos.uncertain.IntractableWorstCase: 

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

222 objective is not supported. 

223 """ 

224 if self._function is None: 

225 return None 

226 elif isinstance(self._function, UncertainExpression): 

227 if self._direction == self.MIN: 

228 bad_direction = self.MAX 

229 elif self._direction == self.MAX: 

230 bad_direction = self.MIN 

231 else: 

232 bad_direction = self.FIND 

233 

234 try: 

235 return self._function.worst_case_value(bad_direction) 

236 except IntractableWorstCase as error: 

237 raise IntractableWorstCase("Failed to compute the worst-case " 

238 "value of the objective function {}: {} Maybe evaluate the " 

239 "nominal objective function instead?" 

240 .format(self._function.string, error)) from None 

241 else: 

242 return self._function.value 

243 

244 def __index__(self): 

245 if self._function is None: 

246 raise TypeError("A feasiblity objective cannot be used as an index " 

247 "because there is no objective function to take the value of.") 

248 

249 value = self.value 

250 

251 if value is None: 

252 raise expressions.NotValued( 

253 "Cannot use unvalued objective function {} as an index." 

254 .format(self._function.string)) 

255 

256 assert isinstance(value, (float, int)) 

257 

258 if not value.is_integer(): 

259 raise ValueError( 

260 "Cannot use the objective function {} as an index as its value " 

261 "of {} is not integral.".format(self._function.string, value)) 

262 

263 return int(value) 

264 

265 def _casting_helper(self, theType): 

266 assert theType in (int, float, complex) 

267 

268 if self._function is None: 

269 raise TypeError("A feasiblity objective cannot be cast as {} " 

270 "because there is no objective function to take the value of." 

271 .format(theType.__name__)) 

272 

273 value = self.value 

274 

275 if value is None: 

276 raise expressions.NotValued( 

277 "Cannot cast unvalued objective function {} as {}." 

278 .format(self._function.string, theType.__name__)) 

279 

280 return theType(value) 

281 

282 def __int__(self): 

283 return self._casting_helper(int) 

284 

285 def __float__(self): 

286 return self._casting_helper(float) 

287 

288 def __complex__(self): 

289 return self._casting_helper(complex) 

290 

291 def __round__(self, ndigits=None): 

292 return round(float(self), ndigits) 

293 

294 

295class Problem(): 

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

297 

298 :Example: 

299 

300 >>> from picos import Problem, RealVariable 

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

302 >>> P = Problem() 

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

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

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

306 >>> print(P) 

307 Linear Program 

308 maximize tr(X) 

309 over 

310 2×2 real variable X (bounded below) 

311 subject to 

312 ∑(X) ≤ 10 

313 X[0,0] = 1 

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

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

316 >>> solution.claimedStatus 

317 'optimal' 

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

319 0.002137422561645508 

320 >>> round(P, 1) 

321 10.0 

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

323 [ 1.00e+00 4.89e-10] 

324 [ 4.89e-10 9.00e+00] 

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

326 1.0 

327 """ 

328 

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

330 CONIC_FORM = Specification( 

331 objectives=[expressions.AffineExpression], 

332 constraints=[C for C in 

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

334 if issubclass(C, constraints.ConicConstraint) 

335 and C is not constraints.ConicConstraint]) 

336 

337 # -------------------------------------------------------------------------- 

338 # Initialization and reset methods. 

339 # -------------------------------------------------------------------------- 

340 

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

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

343 

344 :param copyOptions: 

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

346 the default options. 

347 

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

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

350 

351 :param extra_options: 

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

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

354 """ 

355 if copyOptions and useOptions: 

356 raise ValueError( 

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

358 

359 extra_options = map_legacy_options(**extra_options) 

360 

361 if copyOptions: 

362 self._options = copyOptions.copy() 

363 self._options.update(**extra_options) 

364 elif useOptions: 

365 self._options = useOptions 

366 self._options.update(**extra_options) 

367 else: 

368 self._options = Options(**extra_options) 

369 

370 #: The optimization objective. 

371 self._objective = Objective() 

372 

373 #: Maps constraint IDs to constraints. 

374 self._constraints = OrderedDict() 

375 

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

377 self._con_groups = [] 

378 

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

380 self._mtb_count = {} 

381 

382 #: Maps mutable names to mutables. 

383 self._mutables = OrderedDict() 

384 

385 #: Maps variable names to variables. 

386 self._variables = OrderedDict() 

387 

388 #: Maps parameter names to parameters. 

389 self._parameters = OrderedDict() 

390 

391 #: Current solution strategy. 

392 self._strategy = None 

393 

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

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

396 

397 def _reset_mutable_registry(self): 

398 self._mtb_count.clear() 

399 self._mutables.clear() 

400 self._variables.clear() 

401 self._parameters.clear() 

402 

403 def reset(self, resetOptions=False): 

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

405 

406 :param bool resetOptions: 

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

408 """ 

409 # Reset options if requested. 

410 if resetOptions: 

411 self._options.reset() 

412 

413 # Reset objective to "find an assignment". 

414 del self.objective 

415 

416 # Reset constraint registry. 

417 self._constraints.clear() 

418 self._con_groups.clear() 

419 

420 # Reset mutable registry. 

421 self._reset_mutable_registry() 

422 

423 # Reset strategy and solution data. 

424 self._strategy = None 

425 self._last_solution = None 

426 

427 # -------------------------------------------------------------------------- 

428 # Properties. 

429 # -------------------------------------------------------------------------- 

430 

431 @property 

432 def mutables(self): 

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

434 

435 :returns: 

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

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

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

439 """ 

440 return MappingProxyType(self._mutables) 

441 

442 @property 

443 def variables(self): 

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

445 

446 :returns: 

447 See :attr:`mutables`. 

448 """ 

449 return MappingProxyType(self._variables) 

450 

451 @property 

452 def parameters(self): 

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

454 

455 :returns: 

456 See :attr:`mutables`. 

457 """ 

458 return MappingProxyType(self._parameters) 

459 

460 @property 

461 def constraints(self): 

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

463 

464 :returns: 

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

466 is that in which constraints were added. 

467 """ 

468 return MappingProxyType(self._constraints) 

469 

470 @constraints.deleter 

471 def constraints(self): 

472 # Clear constraint registry. 

473 self._constraints.clear() 

474 self._con_groups.clear() 

475 

476 # Update mutable registry. 

477 self._reset_mutable_registry() 

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

479 

480 @property 

481 def objective(self): 

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

483 return self._objective 

484 

485 @objective.setter 

486 def objective(self, value): 

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

488 

489 try: 

490 if isinstance(value, Objective): 

491 self._objective = value 

492 else: 

493 direction, function = value 

494 self._objective = Objective(direction, function) 

495 finally: 

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

497 

498 @objective.deleter 

499 def objective(self): 

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

501 

502 self._objective = Objective() 

503 

504 @property 

505 def no(self): 

506 """Normalized objective as an :class:`Objective` instance. 

507 

508 Either a minimization or a maximization objective, with feasibility 

509 posed as "minimize 0". 

510 

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

512 :attr:`objective`. 

513 """ 

514 return self._objective.normalized 

515 

516 @property 

517 def value(self): 

518 """Objective function value. 

519 

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

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

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

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

524 this is :obj:`None`. 

525 

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

527 objective value. 

528 

529 :raises picos.uncertain.IntractableWorstCase: 

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

531 objective is not supported. 

532 

533 .. note:: 

534 

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

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

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

538 :class:`Problem`. 

539 """ 

540 return self._objective.value 

541 

542 @property 

543 def options(self): 

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

545 return self._options 

546 

547 @options.setter 

548 def options(self, value): 

549 if not isinstance(value, Options): 

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

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

552 

553 self._options = value 

554 

555 @options.deleter 

556 def options(self, value): 

557 self._options.reset() 

558 

559 @property 

560 def strategy(self): 

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

562 

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

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

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

566 

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

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

569 reformulation pipeline while existing reformulation work is not 

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

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

572 problem from scratch. 

573 

574 :Example: 

575 

576 >>> from picos import Problem, RealVariable 

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

578 >>> P = Problem() 

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

580 >>> print(P.strategy) 

581 None 

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

583 >>> print(P.strategy) 

584 1. ExtraOptions 

585 2. EpigraphReformulation 

586 3. SquaredNormToConicReformulation 

587 4. CVXOPTSolver 

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

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

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

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

592 True 

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

594 False 

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

596 

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

598 

599 >>> from picos.modeling import Strategy 

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

601 ... ConvexQuadraticToConicReformulation) 

602 >>> from picos.solvers import CVXOPTSolver 

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

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

605 ... ConvexQuadraticToConicReformulation) 

606 """ 

607 return self._strategy 

608 

609 @strategy.setter 

610 def strategy(self, value): 

611 from .strategy import Strategy 

612 

613 if not isinstance(value, Strategy): 

614 raise TypeError( 

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

616 .format(type(value).__name__)) 

617 

618 if value.problem is not self: 

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

620 "different problem.") 

621 

622 self._strategy = value 

623 

624 @strategy.deleter 

625 def strategy(self): 

626 self._strategy = None 

627 

628 @property 

629 def last_solution(self): 

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

631 return self._last_solution 

632 

633 @property 

634 def status(self): 

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

636 if not self._last_solution: 

637 return "unsolved" 

638 else: 

639 return self._last_solution.claimedStatus 

640 

641 @property 

642 def footprint(self): 

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

644 return Footprint.from_problem(self) 

645 

646 @property 

647 def continuous(self): 

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

649 return all( 

650 isinstance(variable, expressions.CONTINUOUS_VARTYPES) 

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

652 

653 @property 

654 def pure_integer(self): 

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

656 return not any( 

657 isinstance(variable, expressions.CONTINUOUS_VARTYPES) 

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

659 

660 @property 

661 def type(self): 

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

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

664 objective = self._objective.function 

665 base = "Optimization Problem" 

666 

667 linear = [ 

668 constraints.AffineConstraint, 

669 constraints.ComplexAffineConstraint, 

670 constraints.AbsoluteValueConstraint, 

671 constraints.SimplexConstraint, 

672 constraints.FlowConstraint] 

673 sdp = [ 

674 constraints.LMIConstraint, 

675 constraints.ComplexLMIConstraint] 

676 quadratic = [ 

677 constraints.ConvexQuadraticConstraint, 

678 constraints.ConicQuadraticConstraint, 

679 constraints.NonconvexQuadraticConstraint] 

680 quadconic = [ 

681 constraints.SOCConstraint, 

682 constraints.RSOCConstraint] 

683 exponential = [ 

684 constraints.ExpConeConstraint, 

685 constraints.SumExponentialsConstraint, 

686 constraints.LogSumExpConstraint, 

687 constraints.LogConstraint, 

688 constraints.KullbackLeiblerConstraint] 

689 complex = [ 

690 constraints.ComplexAffineConstraint, 

691 constraints.ComplexLMIConstraint] 

692 

693 if objective is None: 

694 if not C: 

695 base = "Empty Problem" 

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

697 base = "Linear Feasibility Problem" 

698 else: 

699 base = "Feasibility Problem" 

700 elif isinstance(objective, expressions.AffineExpression): 

701 if not C: 

702 if objective.constant: 

703 base = "Constant Problem" 

704 else: 

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

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

707 base = "Linear Program" 

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

709 base = "Second Order Cone Program" 

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

711 base = "Semidefinite Program" 

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

713 base = "Geometric Program" 

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

715 base = "Exponential Program" 

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

717 base = "Quadratically Constrained Program" 

718 elif isinstance(objective, expressions.QuadraticExpression): 

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

720 base = "Quadratic Program" 

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

722 base = "Quadratically Constrained Quadratic Program" 

723 elif isinstance(objective, expressions.LogSumExp): 

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

725 base = "Geometric Program" 

726 

727 if self.continuous: 

728 integrality = "" 

729 elif self.pure_integer: 

730 integrality = "Integer " 

731 else: 

732 integrality = "Mixed-Integer " 

733 

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

735 complexity = "Complex " 

736 else: 

737 complexity = "" 

738 

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

740 

741 @property 

742 def dual(self): 

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

744 

745 More precisely, this property invokes the following: 

746 

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

748 minimization problem, with variable bounds expressed as additional 

749 constraints. 

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

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

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

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

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

755 problem returned will be the respective other. 

756 

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

758 If no reformulation strategy was found. 

759 

760 .. note:: 

761 

762 This property is intended for educational purposes. 

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

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

765 """ 

766 from ..reforms import Dualization 

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

768 

769 @property 

770 def conic_form(self): 

771 """The problem in conic form. 

772 

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

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

775 

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

777 If no reformulation strategy was found. 

778 

779 :Example: 

780 

781 >>> from picos import Problem, RealVariable 

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

783 >>> P = Problem() 

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

785 >>> print(P) 

786 Quadratic Program 

787 minimize ‖x‖² 

788 over 

789 2×1 real variable x 

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

791 Second Order Cone Program 

792 minimize __..._t 

793 over 

794 1×1 real variable __..._t 

795 2×1 real variable x 

796 subject to 

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

798 

799 .. note:: 

800 

801 This property is intended for educational purposes. 

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

803 perform the necessary reformulations automatically. 

804 """ 

805 return self.reformulated(self.CONIC_FORM) 

806 

807 # -------------------------------------------------------------------------- 

808 # Python special methods, except __init__. 

809 # -------------------------------------------------------------------------- 

810 

811 @property 

812 def _var_groups(self): 

813 """Support :meth:`__str__`.""" 

814 vars_by_type = {} 

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

816 vtype = type(var).__name__ 

817 shape = var.shape 

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

819 index = (vtype, shape, bound) 

820 

821 vars_by_type.setdefault(index, set()) 

822 vars_by_type[index].add(var) 

823 

824 groups = [] 

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

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

827 

828 return groups 

829 

830 @property 

831 def _prm_groups(self): 

832 """Support :meth:`__str__`.""" 

833 prms_by_type = {} 

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

835 vtype = type(prm).__name__ 

836 shape = prm.shape 

837 index = (vtype, shape) 

838 

839 prms_by_type.setdefault(index, set()) 

840 prms_by_type[index].add(prm) 

841 

842 groups = [] 

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

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

845 

846 return groups 

847 

848 @lru_cache() 

849 def _mtb_group_string(self, group): 

850 """Support :meth:`__str__`.""" 

851 if len(group) == 0: 

852 return "[no mutables]" 

853 

854 if len(group) == 1: 

855 return group[0].long_string 

856 

857 try: 

858 template, data = parameterized_string( 

859 [mtb.long_string for mtb in group]) 

860 except ValueError: 

861 return group[0].long_string \ 

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

863 else: 

864 return glyphs.forall(template, data) 

865 

866 @lru_cache() 

867 def _con_group_string(self, group): 

868 """Support :meth:`__str__`.""" 

869 if len(group) == 0: 

870 return "[no constraints]" 

871 

872 if len(group) == 1: 

873 return str(group[0]) 

874 

875 try: 

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

877 except ValueError: 

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

879 else: 

880 return glyphs.forall(template, data) 

881 

882 def __repr__(self): 

883 return glyphs.repr1(self.type) 

884 

885 def __str__(self): 

886 # Print problem type. 

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

888 

889 # Print objective. 

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

891 

892 wrapper = TextWrapper( 

893 initial_indent=" "*4, 

894 subsequent_indent=" "*6, 

895 break_long_words=False, 

896 break_on_hyphens=False) 

897 

898 # Print variables. 

899 if self._variables: 

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

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

902 for group in self._var_groups: 

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

904 string += "\n" 

905 

906 # Print constraints. 

907 if self._constraints: 

908 string += " subject to\n" 

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

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

911 string += "\n" 

912 

913 # Print parameters. 

914 if self._parameters: 

915 string += " given\n" 

916 for group in self._prm_groups: 

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

918 string += "\n" 

919 

920 return string.rstrip("\n") 

921 

922 def __index__(self): 

923 return self._objective.__index__() 

924 

925 def __int__(self): 

926 return self._objective.__int__() 

927 

928 def __float__(self): 

929 return self._objective.__float__() 

930 

931 def __complex__(self): 

932 return self._objective.__complex__() 

933 

934 def __round__(self, ndigits=None): 

935 return self._objective.__round__(ndigits) 

936 

937 # -------------------------------------------------------------------------- 

938 # Bookkeeping methods. 

939 # -------------------------------------------------------------------------- 

940 

941 def _register_mutables(self, mtbs): 

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

943 # Register every mutable at most once per call. 

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

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

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

947 

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

949 old_mtbs = self._mutables 

950 new_mtbs = OrderedDict( 

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

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

953 if isinstance(mtb, BaseVariable)) 

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

955 if not isinstance(mtb, BaseVariable)) 

956 

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

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

959 raise ValueError( 

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

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

962 

963 # Check for mutable name clashes with existing mutables. 

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

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

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

967 "problem because it already tracks another mutable with " 

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

969 

970 # Keep track of new mutables. 

971 self._mutables.update(new_mtbs) 

972 self._variables.update(new_vars) 

973 self._parameters.update(new_prms) 

974 

975 # Count up the mutable references. 

976 for mtb in mtbs: 

977 self._mtb_count.setdefault(mtb, 0) 

978 self._mtb_count[mtb] += 1 

979 

980 def _unregister_mutables(self, mtbs): 

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

982 # Unregister every mutable at most once per call. 

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

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

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

986 

987 for mtb in mtbs: 

988 name = mtb.name 

989 

990 # Make sure the mutable is properly registered. 

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

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

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

994 "Found a nonpostive mutable count." 

995 

996 # Count down the mutable references. 

997 self._mtb_count[mtb] -= 1 

998 

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

1000 if not self._mtb_count[mtb]: 

1001 self._mtb_count.pop(mtb) 

1002 self._mutables.pop(name) 

1003 

1004 if isinstance(mtb, BaseVariable): 

1005 self._variables.pop(name) 

1006 else: 

1007 self._parameters.pop(name) 

1008 

1009 # -------------------------------------------------------------------------- 

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

1011 # -------------------------------------------------------------------------- 

1012 

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

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

1015 

1016 :param str direction: 

1017 Case insensitive search direction string. One of 

1018 

1019 - ``"min"`` or ``"minimize"``, 

1020 - ``"max"`` or ``"maximize"``, 

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

1022 

1023 :param ~picos.expressions.Expression expression: 

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

1025 problem. 

1026 """ 

1027 self.objective = direction, expression 

1028 

1029 # -------------------------------------------------------------------------- 

1030 # Methods to add, retrieve and remove constraints. 

1031 # -------------------------------------------------------------------------- 

1032 

1033 def _lookup_constraint(self, idOrIndOrCon): 

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

1035 

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

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

1038 part of the problem. 

1039 """ 

1040 if isinstance(idOrIndOrCon, int): 

1041 if idOrIndOrCon in self._constraints: 

1042 # A valid ID. 

1043 return idOrIndOrCon 

1044 elif idOrIndOrCon < len(self._constraints): 

1045 # An offset. 

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

1047 else: 

1048 raise LookupError( 

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

1050 .format(idOrIndOrCon)) 

1051 elif isinstance(idOrIndOrCon, constraints.Constraint): 

1052 # A constraint object. 

1053 id = idOrIndOrCon.id 

1054 if id in self._constraints: 

1055 return id 

1056 else: 

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

1058 .format(idOrIndOrCon)) 

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

1060 if len(idOrIndOrCon) == 1: 

1061 groupIndex = idOrIndOrCon[0] 

1062 if groupIndex < len(self._con_groups): 

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

1064 else: 

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

1066 elif len(idOrIndOrCon) == 2: 

1067 groupIndex, groupOffset = idOrIndOrCon 

1068 if groupIndex < len(self._con_groups): 

1069 group = self._con_groups[groupIndex] 

1070 if groupOffset < len(group): 

1071 return group[groupOffset].id 

1072 else: 

1073 raise IndexError( 

1074 "Constraint group offset out of range.") 

1075 else: 

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

1077 else: 

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

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

1080 else: 

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

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

1083 

1084 def get_constraint(self, idOrIndOrCon): 

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

1086 

1087 :param idOrIndOrCon: One of the following: 

1088 

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

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

1091 * The integer ID of the constraint. 

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

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

1094 were added. 

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

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

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

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

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

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

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

1102 

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

1104 

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

1106 thereof. 

1107 

1108 :Example: 

1109 

1110 >>> import picos as pic 

1111 >>> import cvxopt as cvx 

1112 >>> from pprint import pprint 

1113 >>> prob=pic.Problem() 

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

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

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

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

1118 >>> print(prob) 

1119 Linear Feasibility Problem 

1120 find an assignment 

1121 for 

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

1123 5×1 real variable y 

1124 subject to 

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

1126 y ≥ 0 

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

1128 >>> prob.get_constraint(1) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1143 """ 

1144 idOrIds = self._lookup_constraint(idOrIndOrCon) 

1145 

1146 if isinstance(idOrIds, list): 

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

1148 else: 

1149 return self._constraints[idOrIds] 

1150 

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

1152 """Add a constraint to the problem. 

1153 

1154 :param constraint: The constraint to be added. 

1155 :type constraint: :class:`Constraint <picos.constraints.Constraint>` 

1156 :param str key: Optional name of the constraint. 

1157 :returns: The constraint that was added to the problem. 

1158 """ 

1159 # Handle deprecated 'key' parameter. 

1160 if key is not None: 

1161 throw_deprecation_warning( 

1162 "Naming constraints is currently not supported.") 

1163 

1164 # Register the constraint. 

1165 self._constraints[constraint.id] = constraint 

1166 self._con_groups.append([constraint]) 

1167 

1168 # Register the constraint's mutables. 

1169 self._register_mutables(constraint.mutables) 

1170 

1171 return constraint 

1172 

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

1174 """Add a list of constraints to the problem. 

1175 

1176 Adds a list of constraints to the problem, enabling the use of 

1177 Python list comprehensions (see the example below). 

1178 

1179 :param list(picos.constraints.Constraint) lst: Constraints to add. 

1180 :param it: DEPRECATED 

1181 :param indices: DEPRECATED 

1182 :param key: DEPRECATED 

1183 

1184 :returns: A list of all constraints that were added. 

1185 

1186 :Example: 

1187 

1188 >>> import picos as pic 

1189 >>> import cvxopt as cvx 

1190 >>> from pprint import pprint 

1191 >>> prob=pic.Problem() 

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

1193 >>> pprint(x) 

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

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

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

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

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

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

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

1201 >>> w={} 

1202 >>> for ij in IJ: 

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

1204 ... 

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

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

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

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

1209 >>> print(prob) 

1210 Feasibility Problem 

1211 find an assignment 

1212 for 

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

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

1215 5×1 real variable y 

1216 subject to 

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

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

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

1220 """ 

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

1222 # Deprecated as of 2.0. 

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

1224 "add_list_of_constraints are deprecated and ignored.") 

1225 

1226 added = [] 

1227 for constraint in lst: 

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

1229 self._con_groups.pop() 

1230 

1231 if added: 

1232 self._con_groups.append(added) 

1233 

1234 return added 

1235 

1236 # TODO: Add Problem.require replacing add_constraint and 

1237 # add_list_of_constraints? 

1238 

1239 def _con_group_index(self, conOrConID): 

1240 """Support :meth:`remove_constraint`.""" 

1241 if isinstance(conOrConID, int): 

1242 constraint = self._constraints[conOrConID] 

1243 else: 

1244 constraint = conOrConID 

1245 

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

1247 for j, candidate in enumerate(group): 

1248 if candidate is constraint: 

1249 return i, j 

1250 

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

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

1253 "registries are out of sync.") 

1254 else: 

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

1256 

1257 def remove_constraint(self, idOrIndOrCon): 

1258 """Delete a constraint from the problem. 

1259 

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

1261 

1262 :Example: 

1263 

1264 >>> import picos 

1265 >>> from pprint import pprint 

1266 >>> P = picos.Problem() 

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

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

1269 >>> Cxy = P.add_list_of_constraints( 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1285 >>> P.remove_constraint(1) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1322 """ 

1323 idOrIds = self._lookup_constraint(idOrIndOrCon) 

1324 

1325 removedCons = [] 

1326 

1327 if isinstance(idOrIds, list): 

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

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

1330 self._con_groups.pop(groupIndex) 

1331 for id in idOrIds: 

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

1333 else: 

1334 constraint = self._constraints.pop(idOrIds) 

1335 removedCons.append(constraint) 

1336 groupIndex, groupOffset = self._con_group_index(constraint) 

1337 group = self._con_groups[groupIndex] 

1338 group.pop(groupOffset) 

1339 if not group: 

1340 self._con_groups.pop(groupIndex) 

1341 

1342 # Unregister the mutables added by the removed constraints. 

1343 for con in removedCons: 

1344 self._unregister_mutables(con.mutables) 

1345 

1346 def remove_all_constraints(self): 

1347 """Remove all constraints from the problem. 

1348 

1349 .. note:: 

1350 

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

1352 """ 

1353 del self.constraints 

1354 

1355 # -------------------------------------------------------------------------- 

1356 # Borderline legacy methods to deal with variables. 

1357 # -------------------------------------------------------------------------- 

1358 

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

1360 

1361 def get_variable(self, name): 

1362 """Retrieve variables referenced by the problem. 

1363 

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

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

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

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

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

1369 of ``param`` as keys. 

1370 

1371 .. note:: 

1372 

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

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

1375 problem's objective function or constraints. 

1376 

1377 :param str name: 

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

1379 

1380 :returns: 

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

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

1383 

1384 :Example: 

1385 

1386 >>> from picos import Problem, RealVariable 

1387 >>> from pprint import pprint 

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

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

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

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

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

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

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

1395 >>> P = Problem() 

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

1397 >>> print(P) 

1398 Linear Program 

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

1400 over 

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

1402 z[carol] 

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

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

1405 <1×1 Real Variable: x> 

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

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

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

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

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

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

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

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

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

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

1416 True 

1417 """ 

1418 if name in self._variables: 

1419 return self._variables[name] 

1420 else: 

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

1422 params = [] 

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

1424 match = self._PARAMETERIZED_VARIABLE_REGEX.match(otherName) 

1425 if not match: 

1426 continue 

1427 base, param = match.groups() 

1428 if name == base: 

1429 params.append(param) 

1430 

1431 if params: 

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

1433 try: 

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

1435 except ValueError: 

1436 pass 

1437 else: 

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

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

1440 for param in intParams] 

1441 

1442 # Otherwise return a dict. 

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

1444 for param in params} 

1445 else: 

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

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

1448 

1449 def get_valued_variable(self, name): 

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

1451 

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

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

1454 variable objects. 

1455 

1456 :raises ~picos.expressions.NotValued: 

1457 If any of the selected variables is not valued. 

1458 """ 

1459 exp = self.get_variable(name) 

1460 if isinstance(exp, list): 

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

1462 exp[i] = exp[i].value 

1463 elif isinstance(exp, dict): 

1464 for i in exp: 

1465 exp[i] = exp[i].value 

1466 else: 

1467 exp = exp.value 

1468 return exp 

1469 

1470 # -------------------------------------------------------------------------- 

1471 # Methods to create copies of the problem. 

1472 # -------------------------------------------------------------------------- 

1473 

1474 def copy(self): 

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

1476 the_copy = Problem(copyOptions=self._options) 

1477 

1478 # Duplicate the mutables. 

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

1480 

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

1482 for group in self._con_groups: 

1483 the_copy.add_list_of_constraints( 

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

1485 

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

1487 direction, function = self._objective 

1488 if function is not None: 

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

1490 

1491 return the_copy 

1492 

1493 def continuous_relaxation(self, copy_other_mutables=True): 

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

1495 

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

1497 

1498 :param bool copy_other_mutables: 

1499 Whether variables that are already continuous as well as parameters 

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

1501 these mutables with the original problem. 

1502 """ 

1503 the_copy = Problem(copyOptions=self._options) 

1504 

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

1506 new_mtbs = {} 

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

1508 if isinstance(var, expressions.IntegerVariable): 

1509 new_mtbs[name] = expressions.RealVariable( 

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

1511 elif isinstance(var, expressions.BinaryVariable): 

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

1513 else: 

1514 if copy_other_mutables: 

1515 new_mtbs[name] = var.copy() 

1516 else: 

1517 new_mtbs[name] = var 

1518 

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

1520 for group in self._con_groups: 

1521 the_copy.add_list_of_constraints( 

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

1523 

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

1525 direction, function = self._objective 

1526 if function is not None: 

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

1528 

1529 return the_copy 

1530 

1531 def clone(self, copyOptions=True): 

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

1533 

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

1535 objective function and thereby references the existing variables and 

1536 parameters that appear in these objects. 

1537 

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

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

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

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

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

1543 

1544 :param bool copyOptions: 

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

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

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

1548 """ 

1549 # Start with a shallow copy of self. 

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

1551 theClone = pycopy.copy(self) 

1552 

1553 # Make the constraint registry independent. 

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

1555 theClone._con_groups = [] 

1556 for group in self._con_groups: 

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

1558 

1559 # Make the mutable registry independent. 

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

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

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

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

1564 

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

1566 theClone._strategy = None 

1567 

1568 # Make the solver options independent, if requested. 

1569 if copyOptions: 

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

1571 

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

1573 # - objective: Is immutable as a tuple. 

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

1575 

1576 return theClone 

1577 

1578 # -------------------------------------------------------------------------- 

1579 # Methods to solve or export the problem. 

1580 # -------------------------------------------------------------------------- 

1581 

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

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

1584 

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

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

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

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

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

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

1591 

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

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

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

1595 and the number of performed reformulations, respectively. 

1596 

1597 :param int steps: 

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

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

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

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

1602 which may also output the problem itself, depending on 

1603 ``extra_options``. 

1604 

1605 :param extra_options: 

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

1607 

1608 :returns: 

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

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

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

1612 

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

1614 If no solution strategy was found. 

1615 

1616 :raises ValueError: 

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

1618 

1619 :Example: 

1620 

1621 >>> from picos import Problem, RealVariable 

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

1623 >>> P = Problem() 

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

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

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

1627 1. ExtraOptions 

1628 2. EpigraphReformulation 

1629 3. SquaredNormToConicReformulation 

1630 4. CVXOPTSolver 

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

1632 3 

1633 >>> print(P) 

1634 Quadratic Program 

1635 minimize ‖x‖² 

1636 over 

1637 2×1 real variable x 

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

1639 Second Order Cone Program 

1640 minimize __..._t 

1641 over 

1642 1×1 real variable __..._t 

1643 2×1 real variable x 

1644 subject to 

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

1646 """ 

1647 from .strategy import Strategy 

1648 

1649 # Produce a strategy for the clone. 

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

1651 numReforms = len(strategy.reforms) 

1652 

1653 if steps is None: 

1654 steps = numReforms 

1655 

1656 if steps == 0: 

1657 return self 

1658 elif steps > numReforms: 

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

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

1661 

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

1663 lastReform = strategy.reforms[steps - 1] 

1664 oldSuccessor = lastReform.successor 

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

1666 "execute": lambda self: Solution( 

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

1668 

1669 # Execute the cut-short strategy. 

1670 strategy.execute(**extra_options) 

1671 

1672 # Repair the last reformulation. 

1673 lastReform.successor = oldSuccessor 

1674 

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

1676 output = lastReform.output 

1677 if output is not self: 

1678 output.prepared_strategy = strategy 

1679 output.prepared_steps = steps 

1680 

1681 return output 

1682 

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

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

1685 

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

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

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

1689 for more details. 

1690 

1691 :param specification: 

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

1693 :type specification: 

1694 ~picos.modeling.Specification 

1695 

1696 :param extra_options: 

1697 Additional solver options to use with this reformulation only. 

1698 

1699 :returns: 

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

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

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

1703 

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

1705 If no reformulation strategy was found. 

1706 

1707 :Example: 

1708 

1709 >>> from picos import Problem, RealVariable 

1710 >>> from picos.modeling import Specification 

1711 >>> from picos.expressions import AffineExpression 

1712 >>> from picos.constraints import ( 

1713 ... AffineConstraint, SOCConstraint, RSOCConstraint) 

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

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

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

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

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

1719 >>> P = Problem() 

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

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

1722 >>> print(P) 

1723 Quadratic Program 

1724 minimize ‖x‖² 

1725 over 

1726 2×1 real variable x 

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

1728 Second Order Cone Program 

1729 minimize __..._t 

1730 over 

1731 1×1 real variable __..._t 

1732 2×1 real variable x 

1733 subject to 

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

1735 

1736 .. note:: 

1737 

1738 This method is intended for educational purposes. 

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

1740 perform the necessary reformulations automatically. 

1741 """ 

1742 if not isinstance(specification, Specification): 

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

1744 "Specification object.") 

1745 

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

1747 def placeholder(the_self): 

1748 raise RuntimeError("The dummy solver created by " 

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

1750 

1751 # Declare a dummy solver that accepts specified problems. 

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

1753 # Abstract class methods. 

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

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

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

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

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

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

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

1761 

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

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

1764 

1765 # Abstract instance methods. 

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

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

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

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

1770 }) 

1771 

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

1773 oldAdHocSolver = self.options.ad_hoc_solver 

1774 extra_options["ad_hoc_solver"] = DummySolver 

1775 problem = self.prepared(**extra_options) 

1776 

1777 # Restore the ad_hoc_solver option of the original problem. 

1778 problem.options.ad_hoc_solver = oldAdHocSolver 

1779 

1780 return problem 

1781 

1782 def solve(self, **extra_options): 

1783 """Hand the problem to a solver. 

1784 

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

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

1787 platform. 

1788 

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

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

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

1792 missing or incomplete. 

1793 

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

1795 access the solution as follows: 

1796 

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

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

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

1800 of any expression involving valued variables like this. 

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

1802 according to the dual solution. 

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

1804 changed, depending on the parameter. 

1805 

1806 :param extra_options: 

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

1808 search only. In particular, this lets you 

1809 

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

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

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

1813 and 

1814 - skip valuing variables or constraints with 

1815 ``apply_solution=False``. 

1816 

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

1818 A solution object or list thereof. 

1819 

1820 :raises ~picos.SolutionFailure: 

1821 In the following cases: 

1822 

1823 1. No solution strategy was found. 

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

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

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

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

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

1829 

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

1831 exception. 

1832 """ 

1833 from .strategy import NoStrategyFound, Strategy 

1834 

1835 startTime = time.time() 

1836 

1837 extra_options = map_legacy_options(**extra_options) 

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

1839 verbose = options.verbosity > 0 

1840 

1841 with picos_box(show=verbose): 

1842 if verbose: 

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

1844 

1845 # Reset an outdated strategy. 

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

1847 if verbose: 

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

1849 

1850 self._strategy = None 

1851 

1852 # Find a new solution strategy, if necessary. 

1853 if not self._strategy: 

1854 if verbose: 

1855 if options.ad_hoc_solver: 

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

1857 elif options.solver: 

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

1859 else: 

1860 solverName = None 

1861 

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

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

1864 

1865 try: 

1866 self._strategy = Strategy.from_problem( 

1867 self, **extra_options) 

1868 except NoStrategyFound as error: 

1869 s = str(error) 

1870 

1871 if verbose: 

1872 print(s, flush=True) 

1873 

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

1875 from error 

1876 

1877 if verbose: 

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

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

1880 else: 

1881 if verbose: 

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

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

1884 

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

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

1887 

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

1889 if isinstance(solutions, list): 

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

1891 

1892 if not solutions: 

1893 raise SolutionFailure( 

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

1895 

1896 solution = solutions[0] 

1897 

1898 if verbose: 

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

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

1901 else: 

1902 assert isinstance(solutions, Solution) 

1903 solution = solutions 

1904 

1905 # Report claimed solution state. 

1906 if verbose: 

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

1908 solution.claimedStatus, solution.problemStatus)) 

1909 

1910 # Validate the primal solution. 

1911 if options.primals: 

1912 if solution.primalStatus != SS_OPTIMAL: 

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

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

1915 .format(solution.primalStatus)) 

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

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

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

1919 

1920 # Validate the dual solution. 

1921 if options.duals: 

1922 if solution.dualStatus != SS_OPTIMAL: 

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

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

1925 solution.dualStatus)) 

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

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

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

1929 

1930 if options.apply_solution: 

1931 if verbose: 

1932 print("Applying the solution.") 

1933 

1934 # Apply the (first) solution. 

1935 solution.apply(snapshotStatus=True) 

1936 

1937 # Store all solutions produced by the solver. 

1938 self._last_solution = solutions 

1939 

1940 # Report verified solution state. 

1941 if verbose: 

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

1943 

1944 endTime = time.time() 

1945 solveTime = endTime - startTime 

1946 searchTime = solution.searchTime 

1947 

1948 if searchTime: 

1949 overhead = (solveTime - searchTime) / searchTime 

1950 else: 

1951 overhead = float("inf") 

1952 

1953 if verbose: 

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

1955 .format(searchTime, solveTime, overhead)) 

1956 

1957 return solutions 

1958 

1959 @deprecated("2.0", reason="Misleading semantics. Maybe " 

1960 ":func:`picos.minimize` is what you want.") 

1961 def minimize(self, obj, **extra_options): 

1962 """Look for a minimizing solution. 

1963 

1964 Sets the objective to minimize the given objective function and calls 

1965 the solver with the given additional options. 

1966 

1967 :param obj: The objective function to minimize. 

1968 :type obj: :class:`~picos.expressions.Expression` 

1969 :param extra_options: A sequence of additional solver options. 

1970 

1971 :returns: A dictionary, see :meth:`~Problem.solve`. 

1972 

1973 .. warning:: 

1974 

1975 This is equivalent to :meth:`~Problem.set_objective` 

1976 followed by :meth:`~Problem.solve` and will thus override 

1977 any existing objective function and direction. 

1978 """ 

1979 self.objective = "min", obj 

1980 return self.solve(**extra_options) 

1981 

1982 @deprecated("2.0", reason="Misleading semantics. Maybe " 

1983 ":func:`picos.maximize` is what you want.") 

1984 def maximize(self, obj, **extra_options): 

1985 """Look for a maximization solution. 

1986 

1987 Sets the objective to maximize the given objective function and calls 

1988 the solver with the given additional options. 

1989 

1990 :param obj: The objective function to maximize. 

1991 :type obj: :class:`~picos.expressions.Expression` 

1992 :param extra_options: A sequence of additional solver options. 

1993 

1994 :returns: A dictionary, see :meth:`~Problem.solve`. 

1995 

1996 .. warning:: 

1997 

1998 This is equivalent to :meth:`~Problem.set_objective` 

1999 followed by :meth:`~Problem.solve` and will thus override 

2000 any existing objective function and direction. 

2001 """ 

2002 self.objective = "max", obj 

2003 return self.solve(**extra_options) 

2004 

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

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

2007 write(self, filename, writer) 

2008 

2009 # -------------------------------------------------------------------------- 

2010 # Methods to query the problem. 

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

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

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

2014 # -------------------------------------------------------------------------- 

2015 

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

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

2018 

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

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

2021 

2022 :param float tol: 

2023 Largest tolerated absolute violation of a constraint or variable 

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

2025 used. 

2026 

2027 :param inttol: 

2028 DEPRECATED 

2029 

2030 :returns: 

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

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

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

2034 otherwise. 

2035 

2036 :raises picos.uncertain.IntractableWorstCase: 

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

2038 expression is not supported. 

2039 """ 

2040 if inttol is not None: 

2041 throw_deprecation_warning("Variable integrality is now ensured on " 

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

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

2044 

2045 if tol is None: 

2046 tol = self._options.abs_prim_fsb_tol 

2047 

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

2049 all_cons += [ 

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

2051 if variable.bound_constraint] 

2052 

2053 largest_violation = 0.0 

2054 

2055 for constraint in all_cons: 

2056 try: 

2057 slack = constraint.slack 

2058 except IntractableWorstCase as error: 

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

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

2061 from None 

2062 

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

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

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

2066 

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

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

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

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

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

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

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

2074 # feasible and declared infeasible here. 

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

2076 # the logic below. 

2077 from ..expressions import Constant, PositiveSemidefiniteCone 

2078 if isinstance(constraint, 

2079 constraints.uncertain.ScenarioUncertainConicConstraint) \ 

2080 and isinstance(constraint.cone, PositiveSemidefiniteCone): 

2081 hack = True 

2082 slack = Constant(slack).desvec.safe_value 

2083 else: 

2084 hack = False 

2085 

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

2087 # Check hermitian-ness of slack. 

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

2089 if violation > tol: 

2090 largest_violation = max(largest_violation, violation) 

2091 

2092 # Check positive semidefiniteness of slack. 

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

2094 if violation > tol: 

2095 largest_violation = max(largest_violation, violation) 

2096 else: 

2097 violation = -float(min(slack)) 

2098 if violation > tol: 

2099 largest_violation = max(largest_violation, violation) 

2100 

2101 return (not largest_violation, largest_violation) 

2102 

2103 # -------------------------------------------------------------------------- 

2104 # Legacy methods and properties. 

2105 # -------------------------------------------------------------------------- 

2106 

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

2108 "removed together with that code." 

2109 

2110 @property 

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

2112 def countVar(self): 

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

2114 return len(self._variables) 

2115 

2116 @property 

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

2118 def countCons(self): 

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

2120 return len(self._variables) 

2121 

2122 @property 

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

2124 def numberOfVars(self): 

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

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

2127 

2128 @property 

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

2130 def numberLSEConstraints(self): 

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

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

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

2134 

2135 @property 

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

2137 def numberSDPConstraints(self): 

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

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

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

2141 

2142 @property 

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

2144 def numberQuadConstraints(self): 

2145 """Number of quadratic constraints stored.""" 

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

2147 constraints.ConvexQuadraticConstraint, 

2148 constraints.ConicQuadraticConstraint, 

2149 constraints.NonconvexQuadraticConstraint))]) 

2150 

2151 @property 

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

2153 def numberConeConstraints(self): 

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

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

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

2157 

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

2159 def obj_value(self): 

2160 """Objective function value. 

2161 

2162 :raises AttributeError: 

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

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

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

2166 to denote an unvalued expression would raise 

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

2168 """ 

2169 if self._objective.feasibility: 

2170 raise AttributeError( 

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

2172 

2173 value = self.value 

2174 

2175 if self.value is None: 

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

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

2178 else: 

2179 return value 

2180 

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

2182 def is_continuous(self): 

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

2184 return self.continuous 

2185 

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

2187 def is_pure_integer(self): 

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

2189 return self.pure_integer 

2190 

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

2192 def set_all_options_to_default(self): 

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

2194 self._options.reset() 

2195 

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

2197 def set_option(self, key, val): 

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

2199 

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

2201 :param val: New value for the option. 

2202 """ 

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

2204 self._options[key] = val 

2205 

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

2207 def update_options(self, **options): 

2208 """Set multiple solver options at once. 

2209 

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

2211 """ 

2212 options = map_legacy_options(**options) 

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

2214 self._options[key] = val 

2215 

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

2217 def verbosity(self): 

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

2219 return self._options.verbosity 

2220 

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

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

2223 def add_variable( 

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

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

2226 

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

2228 

2229 :param size: 

2230 The shape of the variable. 

2231 :type size: 

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

2233 

2234 :param str vtype: 

2235 Domain of the variable. Can be any of 

2236 

2237 - ``'continuous'`` -- real valued, 

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

2239 - ``'integer'`` -- integer valued, 

2240 - ``'symmetric'`` -- symmetric matrix, 

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

2242 - ``'complex'`` -- complex matrix, 

2243 - ``'hermitian'`` -- complex hermitian matrix. 

2244 

2245 :param lower: 

2246 A lower bound on the variable. 

2247 :type lower: 

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

2249 

2250 :param upper: 

2251 An upper bound on the variable. 

2252 :type upper: 

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

2254 

2255 :returns: 

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

2257 

2258 :Example: 

2259 

2260 >>> from picos import Problem 

2261 >>> P = Problem() 

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

2263 >>> x 

2264 <3×1 Real Variable: x> 

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

2266 >>> P.variables 

2267 mappingproxy(OrderedDict()) 

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

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

2270 >>> P.variables 

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

2272 """ 

2273 if vtype == "continuous": 

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

2275 elif vtype == "binary": 

2276 return expressions.BinaryVariable(name, size) 

2277 elif vtype == "integer": 

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

2279 elif vtype == "symmetric": 

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

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

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

2283 elif vtype == "complex": 

2284 return expressions.ComplexVariable(name, size) 

2285 elif vtype == "hermitian": 

2286 return expressions.HermitianVariable(name, size) 

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

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

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

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

2291 else: 

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

2293 

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

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

2296 def remove_variable(self, name): 

2297 """Does nothing.""" 

2298 pass 

2299 

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

2301 def set_var_value(self, name, value): 

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

2303 

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

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

2306 

2307 :param str name: 

2308 Name of the variable to be valued. 

2309 

2310 :param value: 

2311 The value to be set. 

2312 :type value: 

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

2314 """ 

2315 try: 

2316 variable = self._variables[name] 

2317 except KeyError: 

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

2319 .format(name)) from None 

2320 else: 

2321 variable.value = value 

2322 

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

2324 def as_dual(self): 

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

2326 return self.dual 

2327 

2328 

2329# -------------------------------------- 

2330__all__ = api_end(_API_START, globals())