Coverage for picos/modeling/options.py: 82.98%

188 statements  

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

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

2# Copyright (C) 2019-2020 Maximilian Stahlberg 

3# 

4# This file is part of PICOS. 

5# 

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

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

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

9# version. 

10# 

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

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

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

14# 

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

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

17# ------------------------------------------------------------------------------ 

18 

19"""Optimization solver parameter handling.""" 

20 

21# ------------------------------------------------------------------------------ 

22# NOTE: When modifying tolerance options, be sure to also modify tolerances.rst. 

23# ------------------------------------------------------------------------------ 

24 

25import fnmatch 

26import types 

27 

28from ..apidoc import api_end, api_start 

29from ..solvers import Solver, all_solvers 

30 

31_API_START = api_start(globals()) 

32# ------------------------------- 

33 

34 

35OPTIONS = [ 

36 # General options. 

37 # -------------------------------------------------------------------------- 

38 

39 ("strict_options", bool, False, """ 

40 Whether unsupported general options will raise an 

41 :class:`~.solver.UnsupportedOptionError` exception, instead of printing 

42 a warning."""), 

43 

44 ("verbosity", int, 0, """ 

45 Verbosity level. 

46 

47 - ``-1`` attempts to suppress all output, even errros. 

48 - ``0`` only generates warnings and errors. 

49 - ``1`` generates standard informative output. 

50 - ``2`` or larger prints additional information for debugging purposes. 

51 """, lambda n: -1 <= n), 

52 

53 ("license_warnings", bool, True, """ 

54 Whether solvers are allowed to ignore the :ref:`verbosity 

55 <option_verbosity>` option to print licensing related warnings. 

56 

57 See also the global setting :data:`~.settings.LICENSE_WARNINGS`. 

58 """), 

59 

60 ("solver", str, None, """ 

61 The solver to use. 

62 

63 See also the global settings :data:`~.settings.SOLVER_BLACKLIST`, 

64 :data:`~.settings.SOLVER_WHITELIST` and 

65 :data:`~.settings.NONFREE_SOLVERS`. 

66 

67 - :obj:`None` to let PICOS choose. 

68 - """ + """ 

69 - """.join('``"{0}"`` for :class:`{1} <picos.solvers.{1}>`.' 

70 .format(name, solver.__name__) 

71 for name, solver in all_solvers().items()) + """ 

72 

73 This option is ignored when :ref:`ad_hoc_solver <option_ad_hoc_solver>` 

74 is set. 

75 

76 .. note:: 

77 

78 :func:`picos.available_solvers() <picos.available_solvers>` returns 

79 a list of names of solvers that are available at runtime. 

80 """, lambda value: value is None or value in all_solvers().keys()), 

81 

82 ("ad_hoc_solver", type, None, """ 

83 The solver to use as a :class:`~.solvers.solver.Solver` subclass. 

84 

85 This allows solver implementations to be shipped independent of PICOS. 

86 

87 If set, takes precedence over :ref:`solver <option_solver>`.""", 

88 lambda value: value is None or issubclass(value, Solver)), 

89 

90 ("primals", bool, True, """ 

91 Whether to request a primal solution. 

92 

93 - :obj:`True` will raise an exception if no optimal primal solution is 

94 found. 

95 - :obj:`None` will accept and apply also incomplete, infeasible or 

96 suboptimal primal solutions. 

97 - :obj:`False` will not ask for a primal solution and throw away any 

98 primal solution returned by the solver. 

99 """, None), 

100 

101 ("duals", bool, None, """ 

102 Whether to request a dual solution. 

103 

104 - :obj:`True` will raise an exception if no optimal dual solution is 

105 found. 

106 - :obj:`None` will accept and apply also incomplete, infeasible or 

107 suboptimal dual solutions. 

108 - :obj:`False` will not ask for a dual solution and throw away any 

109 dual solution returned by the solver. 

110 """, None), 

111 

112 ("dualize", bool, False, """ 

113 Whether to dualize the problem as part of the solution strategy. 

114 

115 This can sometimes lead to a significant solution search speedup. 

116 """), 

117 

118 ("assume_conic", bool, True, r""" 

119 Determines how :class:`~picos.constraints.ConicQuadraticConstraint` 

120 instances, which correspond to nonconvex constraints of the form 

121 :math:`x^TQx + p^Tx + q \leq (a^Tx + b)(c^Tx + d)` with 

122 :math:`x^TQx + p^Tx + q` representable as a squared norm, are processed: 

123 

124 - :obj:`True` strengthens them into convex conic constraints by assuming 

125 the additional constraints :math:`a^Tx + b \geq 0` and 

126 :math:`c^Tx + d \geq 0`. 

127 - :obj:`False` takes them verbatim and also considers solutions with 

128 :math:`(a^Tx + b) < 0` or :math:`(c^Tx + d) < 0`. This requires a 

129 solver that accepts nonconvex quadratic constraints. 

130 

131 .. warning:: 

132 

133 :class:`~picos.constraints.ConicQuadraticConstraint` are also used 

134 in the case of :math:`Q = 0`. For instance, :math:`x^2 \geq 1` is 

135 effectively ransformed to :math:`x \geq 1` if this is :obj:`True`. 

136 """), 

137 

138 ("apply_solution", bool, True, """ 

139 Whether to immediately apply the solution returned by a solver to the 

140 problem's variables and constraints. 

141 

142 If multiple solutions are returned by the solver, then the first one 

143 will be applied. If this is ``False``, then solutions can be applied 

144 manually via their :meth:`~.solution.Solution.apply` method. 

145 """), 

146 

147 ("abs_prim_fsb_tol", float, 1e-8, """ 

148 Absolute primal problem feasibility tolerance. 

149 

150 A primal solution is feasible if some norm over the vector of primal 

151 constraint violations is smaller than this value. 

152 

153 :obj:`None` lets the solver use its own default value. 

154 """, lambda tol: tol is None or tol > 0.0), 

155 

156 ("rel_prim_fsb_tol", float, 1e-8, """ 

157 Relative primal problem feasibility tolerance. 

158 

159 Like :ref:`abs_prim_fsb_tol <option_abs_prim_fsb_tol>`, but the norm is 

160 divided by the norm of the constraints' right hand side vector. 

161 

162 If the norm used is some nested norm (e.g. the maximum over the norms of 

163 the equality and inequality violations), then solvers might divide the 

164 inner violation norms by the respective right hand side inner norms (see 

165 e.g. `CVXOPT 

166 <https://cvxopt.org/userguide/coneprog.html#algorithm-parameters>`__). 

167 

168 To prevent that the right hand side vector norm is zero (or small), 

169 solvers would either add some small constant or use a fixed lower bound, 

170 which may be as large as :math:`1`. 

171 

172 :obj:`None` lets the solver use its own default value. 

173 """, lambda tol: tol is None or tol > 0.0), 

174 

175 ("abs_dual_fsb_tol", float, 1e-8, """ 

176 Absolute dual problem feasibility tolerance. 

177 

178 A dual solution is feasible if some norm over the vector of dual 

179 constraint violations is smaller than this value. 

180 

181 Serves as an optimality criterion for the Simplex algorithm. 

182 

183 :obj:`None` lets the solver use its own default value. 

184 """, lambda tol: tol is None or tol > 0.0), 

185 

186 ("rel_dual_fsb_tol", float, 1e-8, """ 

187 Relative dual problem feasibility tolerance. 

188 

189 Like :ref:`abs_dual_fsb_tol <option_abs_dual_fsb_tol>`, but the norm is 

190 divided by the norm of the constraints' right hand side vector. (See 

191 :ref:`rel_prim_fsb_tol <option_rel_prim_fsb_tol>` for exceptions.) 

192 

193 Serves as an optimality criterion for the Simplex algorithm. 

194 

195 :obj:`None` lets the solver use its own default value. 

196 """, lambda tol: tol is None or tol > 0.0), 

197 

198 ("abs_ipm_opt_tol", float, 1e-8, """ 

199 Absolute optimality tolerance for interior point methods. 

200 

201 Depending on the solver, a fesible primal/dual solution pair is 

202 considered optimal if this value upper bounds either 

203 

204 - the absolute difference between the primal and dual objective values, 

205 or 

206 - the violation of the complementary slackness condition. 

207 

208 The violation is computed as some norm over the vector that contains the 

209 products of each constraint's slack with its corresponding dual value. 

210 If the norm is the 1-norm, then the two conditions are equal. Otherwise 

211 they can differ by a factor that depends on the number and type of 

212 constraints. 

213 

214 :obj:`None` lets the solver use its own default value. 

215 """, lambda tol: tol is None or tol > 0.0), 

216 

217 ("rel_ipm_opt_tol", float, 1e-8, """ 

218 Relative optimality tolerance for interior point methods. 

219 

220 Like :ref:`abs_ipm_opt_tol <option_abs_ipm_opt_tol>`, but the 

221 suboptimality measure is divided by a convex combination of the absolute 

222 primal and dual objective function values. 

223 

224 :obj:`None` lets the solver use its own default value. 

225 """, lambda tol: tol is None or tol > 0.0), 

226 

227 ("abs_bnb_opt_tol", float, 1e-6, """ 

228 Absolute optimality tolerance for branch-and-bound solution strategies 

229 to mixed integer problems. 

230 

231 A solution is optimal if the absolute difference between the objective 

232 function value of the current best integer solution and the current best 

233 bound obtained from a continuous relaxation is smaller than this value. 

234 

235 :obj:`None` lets the solver use its own default value. 

236 """, lambda tol: tol is None or tol > 0.0), 

237 

238 ("rel_bnb_opt_tol", float, 1e-4, """ 

239 Relative optimality tolerance for branch-and-bound solution strategies 

240 to mixed integer problems. 

241 

242 Like :ref:`abs_bnb_opt_tol <option_abs_bnb_opt_tol>`, but the difference 

243 is divided by a convex combination of the absolute values of the two 

244 objective function values. 

245 

246 :obj:`None` lets the solver use its own default value. 

247 """, lambda tol: tol is None or tol > 0.0), 

248 

249 ("integrality_tol", float, 1e-5, r""" 

250 Integrality tolerance. 

251 

252 A number :math:`x \in \mathbb{R}` is considered integral if 

253 :math:`\min_{z \in \mathbb{Z}}{|x - z|}` is at most this value. 

254 

255 :obj:`None` lets the solver use its own default value. 

256 """, lambda tol: tol is None or (tol > 0.0 and tol < 0.5)), 

257 

258 ("markowitz_tol", float, None, """ 

259 Markowitz threshold used in the Simplex algorithm. 

260 

261 :obj:`None` lets the solver use its own default value. 

262 """, lambda tol: tol is None or (tol > 0.0 and tol < 1.0)), 

263 

264 ("max_iterations", int, None, """ 

265 Maximum number of iterations allowed for iterative solution strategies. 

266 

267 :obj:`None` means no limit. 

268 """, None), 

269 

270 ("max_fsb_nodes", int, None, """ 

271 Maximum number of feasible solution nodes visited for branch-and-bound 

272 solution strategies. 

273 

274 :obj:`None` means no limit. 

275 

276 .. note:: 

277 

278 If you want to obtain all feasible solutions that the solver 

279 encountered, use the :ref:`pool_size <option_pool_size>` option. 

280 """, None), 

281 

282 ("timelimit", int, None, """ 

283 Maximum number of seconds spent searching for a solution. 

284 

285 :obj:`None` means no limit. 

286 """, None), 

287 

288 ("lp_root_method", str, None, """ 

289 Algorithm used to solve continuous linear problems, including the root 

290 relaxation of mixed integer problems. 

291 

292 - :obj:`None` lets PICOS or the solver select it for you. 

293 - ``"psimplex"`` for Primal Simplex. 

294 - ``"dsimplex"`` for Dual Simplex. 

295 - ``"interior"`` for the interior point method. 

296 """, lambda value: value in (None, "psimplex", "dsimplex", "interior")), 

297 

298 ("lp_node_method", str, None, """ 

299 Algorithm used to solve continuous linear problems at non-root nodes of 

300 the branching tree built when solving mixed integer programs. 

301 

302 - :obj:`None` lets PICOS or the solver select it for you. 

303 - ``"psimplex"`` for Primal Simplex. 

304 - ``"dsimplex"`` for Dual Simplex. 

305 - ``"interior"`` for the interior point method. 

306 """, lambda value: value in (None, "psimplex", "dsimplex", "interior")), 

307 

308 ("treememory", int, None, """ 

309 Bound on the memory used by the branch-and-bound tree, in Megabytes. 

310 

311 :obj:`None` means no limit. 

312 """, None), 

313 

314 ("pool_size", int, None, """ 

315 Maximum number of mixed integer feasible solutions returned. 

316 

317 If this is not :obj:`None`, :meth:`~.problem.Problem.solve` 

318 returns a list of :class:`~.solution.Solution` objects instead of just a 

319 single one. 

320 

321 :obj:`None` lets the solver return only the best solution. 

322 """, lambda value: value is None or value >= 1), 

323 

324 ("pool_rel_gap", float, None, """ 

325 Discards solutions from the :ref:`solution pool <option_pool_size>` as 

326 soon as a better solution is found that beats it by the given relative 

327 objective function gap. 

328 

329 :obj:`None` is the solver's choice, which may be *never discard*. 

330 """, None), 

331 

332 ("pool_abs_gap", float, None, """ 

333 Discards solutions from the :ref:`solution pool <option_pool_size>` as 

334 soon as a better solution is found that beats it by the given absolute 

335 objective function gap. 

336 

337 :obj:`None` is the solver's choice, which may be *never discard*. 

338 """, None), 

339 

340 ("hotstart", bool, False, """ 

341 Tells the solver to start from the (partial) solution that is stored in 

342 the :class:`variables <.variables.BaseVariable>` assigned to the 

343 problem."""), 

344 

345 ("verify_prediction", bool, True, """ 

346 Whether PICOS should validate that problem reformulations produce a 

347 problem that matches their predicted outcome. 

348 

349 If a mismatch is detected, a :class:`RuntimeError` is thrown as there is 

350 a chance that it is caused by a bug in the reformulation, which could 

351 affect the correctness of the solution. By disabling this option you are 

352 able to retrieve a correct solution given that the error is only in the 

353 prediction, and given that the solution strategy remains valid for the 

354 actual outcome."""), 

355 

356 ("max_footprints", int, 1024, """ 

357 Maximum number of different predicted problem formulations (footprints) 

358 to consider before deciding on a formulation and solver to use. 

359 

360 :obj:`None` lets PICOS exhaust all reachable problem formulations. 

361 """, None), 

362 

363 # Solver-specific options. 

364 # -------------------------------------------------------------------------- 

365 

366 ("cplex_params", dict, {}, """ 

367 A dictionary of CPLEX parameters to be set after general options are 

368 passed and before the search is started. 

369 

370 For example, ``{"mip.limits.cutpasses": 5}`` limits the number of 

371 cutting plane passes when solving the root node to :math:`5`."""), 

372 

373 ("cplex_vmconfig", str, None, """ 

374 Load a CPLEX virtual machine configuration file. 

375 """, None), 

376 

377 ("cplex_lwr_bnd_limit", float, None, """ 

378 Tells CPLEX to stop MIP optimization if a lower bound below this value 

379 is found. 

380 """, None), 

381 

382 ("cplex_upr_bnd_limit", float, None, """ 

383 Tells CPLEX to stop MIP optimization if an upper bound above this value 

384 is found. 

385 """, None), 

386 

387 ("cplex_bnd_monitor", bool, False, """ 

388 Tells CPLEX to store information about the evolution of the bounds 

389 during the MIP solution search process. At the end of the computation, a 

390 list of triples ``(time, lowerbound, upperbound)`` will be provided in 

391 the field ``bounds_monitor`` of the dictionary returned by 

392 :meth:`~.problem.Problem.solve`. 

393 """), 

394 

395 ("cvxopt_kktsolver", (str, types.FunctionType), None, """ 

396 The KKT solver used by CVXOPT internally. 

397 

398 See `CVXOPT's guide on exploiting structure 

399 <https://cvxopt.org/userguide/coneprog.html#exploiting-structure>`_. 

400 

401 :obj:`None` denotes PICOS' choice: Try first with the faster ``"chol"``, 

402 then with the more reliable ``"ldl"`` solver. 

403 """, None), 

404 

405 ("cvxopt_kktreg", float, 1e-9, """ 

406 The KKT solver regularization term used by CVXOPT internally. 

407 

408 This is an undocumented feature of CVXOPT, see `here 

409 <https://github.com/cvxopt/cvxopt/issues/36#issuecomment-125165634>`_. 

410 

411 End of 2020, this option only affected the LDL KKT solver. 

412 

413 :obj:`None` denotes CVXOPT's default value. 

414 """, None), 

415 

416 ("gurobi_params", dict, {}, """ 

417 A dictionary of Gurobi parameters to be set after general options are 

418 passed and before the search is started. 

419 

420 For example, ``{"NodeLimit": 25}`` limits the number of nodes visited by 

421 the MIP optimizer to :math:`25`."""), 

422 

423 ("gurobi_matint", bool, None, """ 

424 Whether to use Gurobi's matrix interface. 

425 

426 This requires Gurobi 9 or later and SciPy. 

427 

428 :obj:`None` with :data:`~picos.settings.PREFER_GUROBI_MATRIX_INTERFACE` 

429 enabled means *use it if possible*. :obj:`None` with that setting 

430 disabled behaves like :obj:`False`. 

431 """, None), 

432 

433 ("mosek_params", dict, {}, """ 

434 A dictionary of MOSEK (Optimizer) parameters to be set after general 

435 options are passed and before the search is started. 

436 

437 See the `list of MOSEK (Optimizer) 8.1 parameters 

438 <https://docs.mosek.com/8.1/pythonapi/parameters.html>`_."""), 

439 

440 ("mosek_server", str, None, """ 

441 Address of a MOSEK remote optimization server to use. 

442 

443 This option affects both MOSEK (Optimizer) and MOSEK (Fusion). 

444 """, None), 

445 

446 ("mosek_basic_sol", bool, False, """ 

447 Return a basic solution when solving LPs with MOSEK (Optimizer). 

448 """), 

449 

450 ("mskfsn_params", dict, {}, """ 

451 A dictionary of MOSEK (Fusion) parameters to be set after general 

452 options are passed and before the search is started. 

453 

454 See the `list of MOSEK (Fusion) 8.1 parameters 

455 <https://docs.mosek.com/8.1/pythonfusion/parameters.html>`_."""), 

456 

457 ("osqp_params", dict, {}, """ 

458 A dictionary of OSQP parameters to be set after general options are 

459 passed and before the search is started. 

460 

461 See the `list of OSQP parameters 

462 <https://osqp.org/docs/interfaces/solver_settings.html>`_."""), 

463 

464 ("qics_params", dict, {}, """ 

465 A dictionary of QICS parameters to be set after general options are 

466 passed and before the search is started. 

467 

468 See the `list of QICS parameters 

469 <https://qics.readthedocs.io/en/stable/guide/reference.html#solving 

470 #solving>`_."""), 

471 

472 ("scip_params", dict, {}, """ 

473 A dictionary of SCIP parameters to be set after general options are 

474 passed and before the search is started. 

475 

476 For example, ``{"lp/threads": 4}`` sets the number of threads to solve 

477 LPs with to :math:`4`."""), 

478] 

479"""The table of available solver options. 

480 

481Each entry is a tuple representing a single solver option. The tuple's entries 

482are, in order: 

483 

484- Name of the option. Must be a valid Python attribute name. 

485- The option's argument type. Will be cast on any argument that is not already 

486 an instance of the type, except for :obj:`None`. 

487- The option's default value. Must already be of the proper type, or 

488 :obj:`None`, and must pass the optional check. 

489- The option's description, which is used as part of the docstring of 

490 :class:`Options`. In the case of a multi-line text, leading and trailing 

491 empty lines as well as the overall indentation are ignored. 

492- Optional: A boolean function used on every argument that passes the type 

493 conversion (so either an argument of the proper type, or :obj:`None`). If the 

494 function returns ``False``, then the argument is rejected. The default 

495 function rejects exactly :obj:`None`. Supplying :obj:`None` instead of a 

496 function accepts all arguments (in particular, accepts :obj:`None`). 

497""" 

498 

499# Add per-solver options. 

500for name, solver in all_solvers().items(): 

501 OPTIONS.append(("penalty_{}".format(name), float, solver.default_penalty(), 

502 """ 

503 Penalty for using the {} solver. 

504 

505 If solver :math:`A` has a penalty of :math:`p` and solver :math:`B` has 

506 a larger penality of :math:`p + x`, then :math:`B` is be chosen over 

507 :math:`A` only if the problem as passed to :math:`A` would be 

508 :math:`10^x` times larger as when passed to :math:`B`. 

509 """.format(name.upper()))) 

510 

511del name, solver 

512 

513OPTIONS = sorted(OPTIONS) 

514 

515 

516class Option(): 

517 """Optimization solver option. 

518 

519 A single option that affects how a :class:`~.problem.Problem` is solved. 

520 

521 An initial instance of this class is built from each entry of the 

522 :data:`OPTIONS` table to obtain the :data:`OPTION_OBJS` tuple. 

523 """ 

524 

525 # Define __new__ in addition to __init__ so that copy can bypass __init__. 

526 def __new__(cls, *args, **kwargs): 

527 """Create a blank :class:`Option` to be filled in by :meth:`copy`.""" 

528 return super(Option, cls).__new__(cls) 

529 

530 def __init__(self, name, argType, default, description, 

531 check=(lambda x: x is not None)): 

532 """Initialize an :class:`Option`. 

533 

534 See :data:`OPTIONS`. 

535 """ 

536 assert default is None or isinstance(default, argType) 

537 assert check is None or check(default) 

538 

539 self.name = name 

540 self.argType = argType 

541 self.default = default 

542 self._value = default 

543 self.description = self._normalize_description(description) 

544 self.check = check 

545 

546 def _normalize_description(self, description): 

547 lines = description.splitlines() 

548 notSpace = [n for n, line in enumerate(lines) if line.strip()] 

549 if not notSpace: 

550 return "" 

551 first, last = min(notSpace), max(notSpace) 

552 i = len(lines[first]) - len(lines[first].lstrip()) 

553 return "\n".join(line[i:].rstrip() for line in lines[first:last+1]) 

554 

555 def _set_value(self, value): 

556 if value is not None and not isinstance(value, self.argType): 

557 if isinstance(self.argType, type): 

558 try: 

559 value = self.argType(value) 

560 except Exception as error: 

561 raise TypeError("Failed to convert argument {} to option " 

562 "'{}' to type {}.".format(repr(value), self.name, 

563 self.argType.__name__)) from error 

564 else: 

565 assert isinstance(self.argType, (tuple, list)) 

566 assert all(isinstance(t, type) for t in self.argType) 

567 

568 raise TypeError("Argument {} to option '{}' does not match " 

569 "permissible types {}.".format(repr(value), self.name, 

570 ", ".join(t.__name__ for t in self.argType))) 

571 

572 if self.check is not None and not self.check(value): 

573 raise ValueError("The option '{}' does not accept the value {}." 

574 .format(self.name, repr(value))) 

575 

576 self._value = value 

577 

578 value = property(lambda self: self._value, _set_value) 

579 

580 def reset(self): 

581 """Reset the option to its default value.""" 

582 self.value = self.default 

583 

584 def is_default(self): 

585 """Whether the option has its default value.""" 

586 return self.value == self.default 

587 

588 def copy(self): 

589 """Return an independent copy of the option.""" 

590 theCopy = self.__class__.__new__(self.__class__) 

591 theCopy.name = self.name 

592 theCopy.argType = self.argType 

593 theCopy.default = self.default 

594 theCopy._value = self._value 

595 theCopy.description = self.description 

596 theCopy.check = self.check 

597 return theCopy 

598 

599 

600OPTION_OBJS = tuple(Option(*args) for args in OPTIONS) 

601"""The initial solver options as :class:`Option` objects.""" 

602 

603 

604def _tablerow(option, indentaion=0): 

605 """Return a reST list-table row describing an :class:`Option`.""" 

606 spaces = " "*indentaion 

607 return ( 

608 "{}- * {{0}}\n" 

609 "{} * ``{{1}}``\n" 

610 "{} * .. _option_{{0}}:\n\n" 

611 "{} {{2}}" 

612 ).format( 

613 *(4*(spaces,))).format(option.name, repr(option.default), 

614 "\n{} ".format(spaces).join(option.description.splitlines())) 

615 

616 

617def _jumplabel(option): 

618 """Return a reStructuredText jumplabel describing an :class:`Option`.""" 

619 return ":ref:`{0} <option_{0}>`".format(option.name) 

620 

621 

622class Options(): 

623 """Collection of optimization solver options. 

624 

625 A collection of options that affect how a :class:`~.problem.Problem` is 

626 solved. :attr:`Problem.options <.problem.Problem.options>` is an instance of 

627 this class. 

628 

629 The options can be accessed as an attribute or as an item. The latter 

630 approach supports Unix shell-style wildcard characters: 

631 

632 >>> import picos 

633 >>> P = picos.Problem() 

634 >>> P.options.verbosity = 2 

635 >>> P.options["primals"] = False 

636 >>> # Set all absolute tolerances at once. 

637 >>> P.options["abs_*_tol"] = 1e-6 

638 

639 There are two corresponding ways to reset an option to its default value: 

640 

641 >>> del P.options.verbosity 

642 >>> P.options.reset("primals", "*_tol") 

643 

644 Options can also be passed as a keyword argument sequence when the 

645 :class:`Problem <picos.Problem>` is created and whenever a solution is 

646 searched: 

647 

648 >>> # Use default options except for verbosity. 

649 >>> P = picos.Problem(verbosity = 1) 

650 >>> x = picos.RealVariable("x", lower = 0) 

651 >>> P.minimize = x 

652 >>> # Only for the next search: Don't be verbose anyway. 

653 >>> solution = P.solve(solver = "cvxopt", verbosity = 0) 

654 """ 

655 

656 # Document the individual options. 

657 __doc__ += \ 

658 """ 

659 .. rubric:: Available Options 

660 

661 Jump to option: ➥\xa0{} 

662 

663 .. list-table:: 

664 :header-rows: 1 

665 :widths: 10 10 80 

666 

667 - * Option 

668 * Default 

669 * Description 

670 """.format(" ➥\xa0".join(_jumplabel(option) for option in OPTION_OBJS)) \ 

671 .rstrip() + "\n" + "\n".join(_tablerow(option, 6) for option in OPTION_OBJS) 

672 

673 # Define __new__ in addition to __init__ so that 

674 # 1. __init__ does not take the static default options as an argument, 

675 # hiding them from the user and the documentation while 

676 # 2. Options.copy can still bypass copying the default options (by bypassing 

677 # __init__) so that options aren't copied twice. 

678 def __new__(cls, *args, **kwargs): 

679 """Create an empty options set.""" 

680 instance = super(Options, cls).__new__(cls) 

681 # Options overwrites __setattr__, so we need to call object.__setattr__. 

682 super(Options, cls).__setattr__(instance, "_options", {}) 

683 return instance 

684 

685 def __init__(self, **options): 

686 """Create a default option set and set the given options on top.""" 

687 for option in OPTION_OBJS: 

688 self._options[option.name] = option.copy() 

689 

690 self.update(**options) 

691 

692 def __str__(self): 

693 defaults = sorted( 

694 (o for o in self._options.values() if o.is_default()), 

695 key=(lambda o: o.name)) 

696 modified = sorted( 

697 (o for o in self._options.values() if not o.is_default()), 

698 key=(lambda o: o.name)) 

699 

700 nameLength = max(len(o.name) for o in self._options.values()) 

701 valueLength = max(len(str(o.value)) for o in self._options.values()) 

702 

703 string = "" 

704 

705 if modified: 

706 defaultLength = max(len(str(o.default)) for o in modified) 

707 

708 string += "Modified solver options:\n" + "\n".join(( 

709 " {{:{}}} = {{:{}}} (default: {{:{}}})".format( 

710 nameLength, valueLength, defaultLength 

711 ).format( 

712 option.name, str(option.value), str(option.default)) 

713 for num, option in enumerate(modified))) 

714 

715 if defaults: 

716 if modified: 

717 string += "\n\n" 

718 

719 string += "Default solver options:\n" + "\n".join(( 

720 " {{:{}}} = {{}}".format(nameLength).format( 

721 option.name, str(option.value)) 

722 for num, option in enumerate(defaults))) 

723 

724 return string 

725 

726 def __eq__(self, other): 

727 """Report whether two sets of options are equal.""" 

728 if self is other: 

729 return True 

730 

731 for name in self._options: 

732 if self._options[name].value != other._options[name].value: 

733 return False 

734 

735 return True 

736 

737 def _fuzzy(returnsSomething): 

738 """Allow wildcards in option names.""" 

739 def decorator(method): 

740 def wrapper(self, pattern, *extraArgs): 

741 if any(char in pattern for char in "*?[!]"): 

742 matching = fnmatch.filter(self._options.keys(), pattern) 

743 

744 if not matching: 

745 raise LookupError("No option matches '{}'." 

746 .format(pattern)) 

747 

748 if returnsSomething: 

749 return {name: method(self, name, *extraArgs) 

750 for name in matching} 

751 else: 

752 for name in matching: 

753 method(self, name, *extraArgs) 

754 else: 

755 if returnsSomething: 

756 return method(self, pattern, *extraArgs) 

757 else: 

758 method(self, pattern, *extraArgs) 

759 return wrapper 

760 return decorator 

761 

762 @_fuzzy(True) 

763 def __getattr__(self, name): 

764 if name in self._options: 

765 return self._options[name].value 

766 else: 

767 raise AttributeError("Unknown option '{}'.".format(name)) 

768 

769 @_fuzzy(False) 

770 def __setattr__(self, name, value): 

771 if name in self._options: 

772 self._options[name].value = value 

773 else: 

774 raise AttributeError("Unknown option '{}'.".format(name)) 

775 

776 @_fuzzy(False) 

777 def __delattr__(self, name): 

778 if name in self._options: 

779 self._options[name].reset() 

780 else: 

781 raise AttributeError("Unknown option '{}'.".format(name)) 

782 

783 @_fuzzy(True) 

784 def __getitem__(self, name): 

785 if name in self._options: 

786 return self._options[name].value 

787 else: 

788 raise LookupError("Unknown option '{}'.".format(name)) 

789 

790 @_fuzzy(False) 

791 def __setitem__(self, name, value): 

792 if name in self._options: 

793 self._options[name].value = value 

794 else: 

795 raise LookupError("Unknown option '{}'.".format(name)) 

796 

797 def __contains__(self, name): 

798 return name in self._options 

799 

800 def __dir__(self): 

801 optionNames = [name for name in self._options.keys()] 

802 list_ = super(Options, self).__dir__() + optionNames 

803 return sorted(list_) 

804 

805 def copy(self): 

806 """Return an independent copy of the current options set.""" 

807 theCopy = self.__class__.__new__(self.__class__) 

808 for option in self._options.values(): 

809 theCopy._options[option.name] = option.copy() 

810 return theCopy 

811 

812 def update(self, **options): 

813 """Set multiple options at once. 

814 

815 This method is called with the keyword arguments supplied to the 

816 :class:`Options` constructor, so the following two are the same: 

817 

818 >>> import picos 

819 >>> a = picos.Options(verbosity = 1, primals = False) 

820 >>> b = picos.Options() 

821 >>> b.update(verbosity = 1, primals = False) 

822 >>> a == b 

823 True 

824 

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

826 """ 

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

828 self[key] = val 

829 

830 def updated(self, **options): 

831 """Return a modified copy.""" 

832 theCopy = self.copy() 

833 if options: 

834 theCopy.update(**options) 

835 return theCopy 

836 

837 def self_or_updated(self, **options): 

838 """Return either a modified copy or self, depending on given options.""" 

839 if options: 

840 theCopy = self.copy() 

841 theCopy.update(**options) 

842 return theCopy 

843 else: 

844 return self 

845 

846 @_fuzzy(False) 

847 def _reset_single(self, name): 

848 self._options[name].reset() 

849 

850 def reset(self, *options): 

851 """Reset all or a selection of options to their default values. 

852 

853 :param options: The names of the options to reset, may contain wildcard 

854 characters. If no name is given, all options are reset. 

855 """ 

856 if options: 

857 for name in options: 

858 self._reset_single(name) 

859 else: 

860 for option in self._options.values(): 

861 option.reset() 

862 

863 @_fuzzy(True) 

864 def _help_single(self, name): 

865 option = self._options[name] 

866 return ( 

867 "Option: {}\n" 

868 "Default: {}\n" 

869 "\n {}" 

870 ).format(option.name, str(option.default), 

871 "\n ".join(option.description.splitlines())) 

872 

873 def help(self, *options): 

874 """Print text describing selected options. 

875 

876 :param options: The names of the options to describe, may contain 

877 wildcard characters. 

878 """ 

879 for i, name in enumerate(options): 

880 if i != 0: 

881 print("\n\n") 

882 retval = self._help_single(name) 

883 if isinstance(retval, str): 

884 print(retval) 

885 else: 

886 assert isinstance(retval, dict) 

887 print("\n\n".join(retval.values())) 

888 

889 @property 

890 def nondefaults(self): 

891 """A dictionary mapping option names to nondefault values. 

892 

893 :Example: 

894 

895 >>> from picos import Options 

896 >>> o = Options() 

897 >>> o.verbosity = 2 

898 >>> o.nondefaults 

899 {'verbosity': 2} 

900 >>> Options(**o.nondefaults) == o 

901 True 

902 """ 

903 return {name: option._value for name, option in self._options.items() 

904 if option._value != option.default} 

905 

906 

907# -------------------------------------- 

908__all__ = api_end(_API_START, globals())