Coverage for picos/modeling/options.py: 83.07%
189 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +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# ------------------------------------------------------------------------------
19"""Optimization solver parameter handling."""
21# ------------------------------------------------------------------------------
22# NOTE: When modifying tolerance options, be sure to also modify tolerances.rst.
23# ------------------------------------------------------------------------------
25import fnmatch
26import types
28from ..apidoc import api_end, api_start
29from ..solvers import all_solvers, Solver
31_API_START = api_start(globals())
32# -------------------------------
35OPTIONS = [
36 # General options.
37 # --------------------------------------------------------------------------
39 ("strict_options", bool, False, """
40 Whether unsupported general options will raise an
41 :class:`~.solver.UnsupportedOptionError` exception, instead of printing
42 a warning."""),
44 ("verbosity", int, 0, """
45 Verbosity level.
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),
53 ("license_warnings", bool, True, """
54 Whether solvers are allowed to ignore the :ref:`verbosity
55 <option_verbosity>` option to print licensing related warnings.
57 See also the global setting :data:`~.settings.LICENSE_WARNINGS`.
58 """),
60 ("solver", str, None, """
61 The solver to use.
63 See also the global settings :data:`~.settings.SOLVER_BLACKLIST`,
64 :data:`~.settings.SOLVER_WHITELIST` and
65 :data:`~.settings.NONFREE_SOLVERS`.
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()) + """
73 This option is ignored when :ref:`ad_hoc_solver <option_ad_hoc_solver>`
74 is set.
76 .. note::
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()),
82 ("ad_hoc_solver", type, None, """
83 The solver to use as a :class:`~.solvers.solver.Solver` subclass.
85 This allows solver implementations to be shipped independent of PICOS.
87 If set, takes precedence over :ref:`solver <option_solver>`.""",
88 lambda value: value is None or issubclass(value, Solver)),
90 ("primals", bool, True, """
91 Whether to request a primal solution.
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),
101 ("duals", bool, None, """
102 Whether to request a dual solution.
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),
112 ("dualize", bool, False, """
113 Whether to dualize the problem as part of the solution strategy.
115 This can sometimes lead to a significant solution search speedup.
116 """),
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:
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.
131 .. warning::
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 """),
138 ("apply_solution", bool, True, """
139 Whether to immediately apply the solution returned by a solver to the
140 problem's variables and constraints.
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 """),
147 ("abs_prim_fsb_tol", float, 1e-8, """
148 Absolute primal problem feasibility tolerance.
150 A primal solution is feasible if some norm over the vector of primal
151 constraint violations is smaller than this value.
153 :obj:`None` lets the solver use its own default value.
154 """, lambda tol: tol is None or tol > 0.0),
156 ("rel_prim_fsb_tol", float, 1e-8, """
157 Relative primal problem feasibility tolerance.
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.
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>`__).
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`.
172 :obj:`None` lets the solver use its own default value.
173 """, lambda tol: tol is None or tol > 0.0),
175 ("abs_dual_fsb_tol", float, 1e-8, """
176 Absolute dual problem feasibility tolerance.
178 A dual solution is feasible if some norm over the vector of dual
179 constraint violations is smaller than this value.
181 Serves as an optimality criterion for the Simplex algorithm.
183 :obj:`None` lets the solver use its own default value.
184 """, lambda tol: tol is None or tol > 0.0),
186 ("rel_dual_fsb_tol", float, 1e-8, """
187 Relative dual problem feasibility tolerance.
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.)
193 Serves as an optimality criterion for the Simplex algorithm.
195 :obj:`None` lets the solver use its own default value.
196 """, lambda tol: tol is None or tol > 0.0),
198 ("abs_ipm_opt_tol", float, 1e-8, """
199 Absolute optimality tolerance for interior point methods.
201 Depending on the solver, a fesible primal/dual solution pair is
202 considered optimal if this value upper bounds either
204 - the absolute difference between the primal and dual objective values,
205 or
206 - the violation of the complementary slackness condition.
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.
214 :obj:`None` lets the solver use its own default value.
215 """, lambda tol: tol is None or tol > 0.0),
217 ("rel_ipm_opt_tol", float, 1e-8, """
218 Relative optimality tolerance for interior point methods.
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.
224 :obj:`None` lets the solver use its own default value.
225 """, lambda tol: tol is None or tol > 0.0),
227 ("abs_bnb_opt_tol", float, 1e-6, """
228 Absolute optimality tolerance for branch-and-bound solution strategies
229 to mixed integer problems.
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.
235 :obj:`None` lets the solver use its own default value.
236 """, lambda tol: tol is None or tol > 0.0),
238 ("rel_bnb_opt_tol", float, 1e-4, """
239 Relative optimality tolerance for branch-and-bound solution strategies
240 to mixed integer problems.
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.
246 :obj:`None` lets the solver use its own default value.
247 """, lambda tol: tol is None or tol > 0.0),
249 ("integrality_tol", float, 1e-5, r"""
250 Integrality tolerance.
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.
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)),
258 ("markowitz_tol", float, None, """
259 Markowitz threshold used in the Simplex algorithm.
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)),
264 ("max_iterations", int, None, """
265 Maximum number of iterations allowed for iterative solution strategies.
267 :obj:`None` means no limit.
268 """, None),
270 ("max_fsb_nodes", int, None, """
271 Maximum number of feasible solution nodes visited for branch-and-bound
272 solution strategies.
274 :obj:`None` means no limit.
276 .. note::
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),
282 ("timelimit", int, None, """
283 Maximum number of seconds spent searching for a solution.
285 :obj:`None` means no limit.
286 """, None),
288 ("lp_root_method", str, None, """
289 Algorithm used to solve continuous linear problems, including the root
290 relaxation of mixed integer problems.
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")),
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.
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")),
308 ("treememory", int, None, """
309 Bound on the memory used by the branch-and-bound tree, in Megabytes.
311 :obj:`None` means no limit.
312 """, None),
314 ("pool_size", int, None, """
315 Maximum number of mixed integer feasible solutions returned.
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.
321 :obj:`None` lets the solver return only the best solution.
322 """, lambda value: value is None or value >= 1),
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.
329 :obj:`None` is the solver's choice, which may be *never discard*.
330 """, None),
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.
337 :obj:`None` is the solver's choice, which may be *never discard*.
338 """, None),
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."""),
345 ("verify_prediction", bool, True, """
346 Whether PICOS should validate that problem reformulations produce a
347 problem that matches their predicted outcome.
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."""),
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.
360 :obj:`None` lets PICOS exhaust all reachable problem formulations.
361 """, None),
363 # Solver-specific options.
364 # --------------------------------------------------------------------------
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.
370 For example, ``{"mip.limits.cutpasses": 5}`` limits the number of
371 cutting plane passes when solving the root node to :math:`5`."""),
373 ("cplex_vmconfig", str, None, """
374 Load a CPLEX virtual machine configuration file.
375 """, None),
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),
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),
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 """),
395 ("cvxopt_kktsolver", (str, types.FunctionType), None, """
396 The KKT solver used by CVXOPT internally.
398 See `CVXOPT's guide on exploiting structure
399 <https://cvxopt.org/userguide/coneprog.html#exploiting-structure>`_.
401 :obj:`None` denotes PICOS' choice: Try first with the faster ``"chol"``,
402 then with the more reliable ``"ldl"`` solver.
403 """, None),
405 ("cvxopt_kktreg", float, 1e-9, """
406 The KKT solver regularization term used by CVXOPT internally.
408 This is an undocumented feature of CVXOPT, see `here
409 <https://github.com/cvxopt/cvxopt/issues/36#issuecomment-125165634>`_.
411 End of 2020, this option only affected the LDL KKT solver.
413 :obj:`None` denotes CVXOPT's default value.
414 """, None),
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.
420 For example, ``{"NodeLimit": 25}`` limits the number of nodes visited by
421 the MIP optimizer to :math:`25`."""),
423 ("gurobi_matint", bool, None, """
424 Whether to use Gurobi's matrix interface.
426 This requires Gurobi 9 or later and SciPy.
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),
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.
437 See the `list of MOSEK (Optimizer) 8.1 parameters
438 <https://docs.mosek.com/8.1/pythonapi/parameters.html>`_."""),
440 ("mosek_server", str, None, """
441 Address of a MOSEK remote optimization server to use.
443 This option affects both MOSEK (Optimizer) and MOSEK (Fusion).
444 """, None),
446 ("mosek_basic_sol", bool, False, """
447 Return a basic solution when solving LPs with MOSEK (Optimizer).
448 """),
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.
454 See the `list of MOSEK (Fusion) 8.1 parameters
455 <https://docs.mosek.com/8.1/pythonfusion/parameters.html>`_."""),
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.
461 See the `list of OSQP parameters
462 <https://osqp.org/docs/interfaces/solver_settings.html>`_."""),
464 ("scip_params", dict, {}, """
465 A dictionary of SCIP parameters to be set after general options are
466 passed and before the search is started.
468 For example, ``{"lp/threads": 4}`` sets the number of threads to solve
469 LPs with to :math:`4`."""),
470]
471"""The table of available solver options.
473Each entry is a tuple representing a single solver option. The tuple's entries
474are, in order:
476- Name of the option. Must be a valid Python attribute name.
477- The option's argument type. Will be cast on any argument that is not already
478 an instance of the type, except for :obj:`None`.
479- The option's default value. Must already be of the proper type, or
480 :obj:`None`, and must pass the optional check.
481- The option's description, which is used as part of the docstring of
482 :class:`Options`. In the case of a multi-line text, leading and trailing
483 empty lines as well as the overall indentation are ignored.
484- Optional: A boolean function used on every argument that passes the type
485 conversion (so either an argument of the proper type, or :obj:`None`). If the
486 function returns ``False``, then the argument is rejected. The default
487 function rejects exactly :obj:`None`. Supplying :obj:`None` instead of a
488 function accepts all arguments (in particular, accepts :obj:`None`).
489"""
491# Add per-solver options.
492for name, solver in all_solvers().items():
493 OPTIONS.append(("penalty_{}".format(name), float, solver.default_penalty(),
494 """
495 Penalty for using the {} solver.
497 If solver :math:`A` has a penalty of :math:`p` and solver :math:`B` has
498 a larger penality of :math:`p + x`, then :math:`B` is be chosen over
499 :math:`A` only if the problem as passed to :math:`A` would be
500 :math:`10^x` times larger as when passed to :math:`B`.
501 """.format(name.upper())))
503del name, solver
505OPTIONS = sorted(OPTIONS)
508class Option():
509 """Optimization solver option.
511 A single option that affects how a :class:`~.problem.Problem` is solved.
513 An initial instance of this class is built from each entry of the
514 :data:`OPTIONS` table to obtain the :data:`OPTION_OBJS` tuple.
515 """
517 # Define __new__ in addition to __init__ so that copy can bypass __init__.
518 def __new__(cls, *args, **kwargs):
519 """Create a blank :class:`Option` to be filled in by :meth:`copy`."""
520 return super(Option, cls).__new__(cls)
522 def __init__(self, name, argType, default, description,
523 check=(lambda x: x is not None)):
524 """Initialize an :class:`Option`.
526 See :data:`OPTIONS`.
527 """
528 assert default is None or isinstance(default, argType)
529 assert check is None or check(default)
531 self.name = name
532 self.argType = argType
533 self.default = default
534 self._value = default
535 self.description = self._normalize_description(description)
536 self.check = check
538 def _normalize_description(self, description):
539 lines = description.splitlines()
540 notSpace = [n for n, line in enumerate(lines) if line.strip()]
541 if not notSpace:
542 return ""
543 first, last = min(notSpace), max(notSpace)
544 i = len(lines[first]) - len(lines[first].lstrip())
545 return "\n".join(line[i:].rstrip() for line in lines[first:last+1])
547 def _set_value(self, value):
548 if value is not None and not isinstance(value, self.argType):
549 if isinstance(self.argType, type):
550 try:
551 value = self.argType(value)
552 except Exception as error:
553 raise TypeError("Failed to convert argument {} to option "
554 "'{}' to type {}.".format(repr(value), self.name,
555 self.argType.__name__)) from error
556 else:
557 assert isinstance(self.argType, (tuple, list))
558 assert all(isinstance(t, type) for t in self.argType)
560 raise TypeError("Argument {} to option '{}' does not match "
561 "permissible types {}.".format(repr(value), self.name,
562 ", ".join(t.__name__ for t in self.argType)))
564 if self.check is not None and not self.check(value):
565 raise ValueError("The option '{}' does not accept the value {}."
566 .format(self.name, repr(value)))
568 self._value = value
570 value = property(lambda self: self._value, _set_value)
572 def reset(self):
573 """Reset the option to its default value."""
574 self.value = self.default
576 def is_default(self):
577 """Whether the option has its default value."""
578 return self.value == self.default
580 def copy(self):
581 """Return an independent copy of the option."""
582 theCopy = self.__class__.__new__(self.__class__)
583 theCopy.name = self.name
584 theCopy.argType = self.argType
585 theCopy.default = self.default
586 theCopy._value = self._value
587 theCopy.description = self.description
588 theCopy.check = self.check
589 return theCopy
592OPTION_OBJS = tuple(Option(*args) for args in OPTIONS)
593"""The initial solver options as :class:`Option` objects."""
596def _tablerow(option, indentaion=0):
597 """Return a reST list-table row describing an :class:`Option`."""
598 spaces = " "*indentaion
599 return (
600 "{}- * {{0}}\n"
601 "{} * ``{{1}}``\n"
602 "{} * .. _option_{{0}}:\n\n"
603 "{} {{2}}"
604 ).format(
605 *(4*(spaces,))).format(option.name, repr(option.default),
606 "\n{} ".format(spaces).join(option.description.splitlines()))
609def _jumplabel(option):
610 """Return a reStructuredText jumplabel describing an :class:`Option`."""
611 return ":ref:`{0} <option_{0}>`".format(option.name)
614class Options():
615 """Collection of optimization solver options.
617 A collection of options that affect how a :class:`~.problem.Problem` is
618 solved. :attr:`Problem.options <.problem.Problem.options>` is an instance of
619 this class.
621 The options can be accessed as an attribute or as an item. The latter
622 approach supports Unix shell-style wildcard characters:
624 >>> import picos
625 >>> P = picos.Problem()
626 >>> P.options.verbosity = 2
627 >>> P.options["primals"] = False
628 >>> # Set all absolute tolerances at once.
629 >>> P.options["abs_*_tol"] = 1e-6
631 There are two corresponding ways to reset an option to its default value:
633 >>> del P.options.verbosity
634 >>> P.options.reset("primals", "*_tol")
636 Options can also be passed as a keyword argument sequence when the
637 :class:`Problem <picos.Problem>` is created and whenever a solution is
638 searched:
640 >>> # Use default options except for verbosity.
641 >>> P = picos.Problem(verbosity = 1)
642 >>> x = P.add_variable("x", lower = 0); P.set_objective("min", x)
643 >>> # Only for the next search: Don't be verbose anyway.
644 >>> solution = P.solve(solver = "cvxopt", verbosity = 0)
645 """
647 # Document the individual options.
648 __doc__ += \
649 """
650 .. rubric:: Available Options
652 Jump to option: ➥\xa0{}
654 .. list-table::
655 :header-rows: 1
656 :widths: 10 10 80
658 - * Option
659 * Default
660 * Description
661 """.format(" ➥\xa0".join(_jumplabel(option) for option in OPTION_OBJS)) \
662 .rstrip() + "\n" + "\n".join(_tablerow(option, 6) for option in OPTION_OBJS)
664 # Define __new__ in addition to __init__ so that
665 # 1. __init__ does not take the static default options as an argument,
666 # hiding them from the user and the documentation while
667 # 2. Options.copy can still bypass copying the default options (by bypassing
668 # __init__) so that options aren't copied twice.
669 def __new__(cls, *args, **kwargs):
670 """Create an empty options set."""
671 instance = super(Options, cls).__new__(cls)
672 # Options overwrites __setattr__, so we need to call object.__setattr__.
673 super(Options, cls).__setattr__(instance, "_options", {})
674 return instance
676 def __init__(self, **options):
677 """Create a default option set and set the given options on top."""
678 for option in OPTION_OBJS:
679 self._options[option.name] = option.copy()
681 self.update(**options)
683 def __str__(self):
684 defaults = sorted(
685 (o for o in self._options.values() if o.is_default()),
686 key=(lambda o: o.name))
687 modified = sorted(
688 (o for o in self._options.values() if not o.is_default()),
689 key=(lambda o: o.name))
691 nameLength = max(len(o.name) for o in self._options.values())
692 valueLength = max(len(str(o.value)) for o in self._options.values())
694 string = ""
696 if modified:
697 defaultLength = max(len(str(o.default)) for o in modified)
699 string += "Modified solver options:\n" + "\n".join((
700 " {{:{}}} = {{:{}}} (default: {{:{}}})".format(
701 nameLength, valueLength, defaultLength
702 ).format(
703 option.name, str(option.value), str(option.default))
704 for num, option in enumerate(modified)))
706 if defaults:
707 if modified:
708 string += "\n\n"
710 string += "Default solver options:\n" + "\n".join((
711 " {{:{}}} = {{}}".format(nameLength).format(
712 option.name, str(option.value))
713 for num, option in enumerate(defaults)))
715 return string
717 def __eq__(self, other):
718 """Report whether two sets of options are equal."""
719 if self is other:
720 return True
722 for name in self._options:
723 if self._options[name].value != other._options[name].value:
724 return False
726 return True
728 def _fuzzy(returnsSomething):
729 """Allow wildcards in option names."""
730 def decorator(method):
731 def wrapper(self, pattern, *extraArgs):
732 if any(char in pattern for char in "*?[!]"):
733 matching = fnmatch.filter(self._options.keys(), pattern)
735 if not matching:
736 raise LookupError("No option matches '{}'."
737 .format(pattern))
739 if returnsSomething:
740 return {name: method(self, name, *extraArgs)
741 for name in matching}
742 else:
743 for name in matching:
744 method(self, name, *extraArgs)
745 else:
746 if returnsSomething:
747 return method(self, pattern, *extraArgs)
748 else:
749 method(self, pattern, *extraArgs)
750 return wrapper
751 return decorator
753 @_fuzzy(True)
754 def __getattr__(self, name):
755 if name in self._options:
756 return self._options[name].value
757 else:
758 raise AttributeError("Unknown option '{}'.".format(name))
760 @_fuzzy(False)
761 def __setattr__(self, name, value):
762 if name in self._options:
763 self._options[name].value = value
764 else:
765 raise AttributeError("Unknown option '{}'.".format(name))
767 @_fuzzy(False)
768 def __delattr__(self, name):
769 if name in self._options:
770 self._options[name].reset()
771 else:
772 raise AttributeError("Unknown option '{}'.".format(name))
774 @_fuzzy(True)
775 def __getitem__(self, name):
776 if name in self._options:
777 return self._options[name].value
778 else:
779 raise LookupError("Unknown option '{}'.".format(name))
781 @_fuzzy(False)
782 def __setitem__(self, name, value):
783 if name in self._options:
784 self._options[name].value = value
785 else:
786 raise LookupError("Unknown option '{}'.".format(name))
788 def __contains__(self, name):
789 return name in self._options
791 def __dir__(self):
792 optionNames = [name for name in self._options.keys()]
793 list_ = super(Options, self).__dir__() + optionNames
794 return sorted(list_)
796 def copy(self):
797 """Return an independent copy of the current options set."""
798 theCopy = self.__class__.__new__(self.__class__)
799 for option in self._options.values():
800 theCopy._options[option.name] = option.copy()
801 return theCopy
803 def update(self, **options):
804 """Set multiple options at once.
806 This method is called with the keyword arguments supplied to the
807 :class:`Options` constructor, so the following two are the same:
809 >>> import picos
810 >>> a = picos.Options(verbosity = 1, primals = False)
811 >>> b = picos.Options()
812 >>> b.update(verbosity = 1, primals = False)
813 >>> a == b
814 True
816 :param options: A parameter sequence of options to set.
817 """
818 for key, val in options.items():
819 self[key] = val
821 def updated(self, **options):
822 """Return a modified copy."""
823 theCopy = self.copy()
824 if options:
825 theCopy.update(**options)
826 return theCopy
828 def self_or_updated(self, **options):
829 """Return either a modified copy or self, depending on given options."""
830 if options:
831 theCopy = self.copy()
832 theCopy.update(**options)
833 return theCopy
834 else:
835 return self
837 @_fuzzy(False)
838 def _reset_single(self, name):
839 self._options[name].reset()
841 def reset(self, *options):
842 """Reset all or a selection of options to their default values.
844 :param options: The names of the options to reset, may contain wildcard
845 characters. If no name is given, all options are reset.
846 """
847 if options:
848 for name in options:
849 self._reset_single(name)
850 else:
851 for option in self._options.values():
852 option.reset()
854 @_fuzzy(True)
855 def _help_single(self, name):
856 option = self._options[name]
857 return (
858 "Option: {}\n"
859 "Default: {}\n"
860 "\n {}"
861 ).format(option.name, str(option.default),
862 "\n ".join(option.description.splitlines()))
864 def help(self, *options):
865 """Print text describing selected options.
867 :param options: The names of the options to describe, may contain
868 wildcard characters.
869 """
870 for i, name in enumerate(options):
871 if i != 0:
872 print("\n\n")
873 retval = self._help_single(name)
874 if isinstance(retval, str):
875 print(retval)
876 else:
877 assert isinstance(retval, dict)
878 print("\n\n".join(retval.values()))
880 @property
881 def nondefaults(self):
882 """A dictionary mapping option names to nondefault values.
884 :Example:
886 >>> from picos import Options
887 >>> o = Options()
888 >>> o.verbosity = 2
889 >>> o.nondefaults
890 {'verbosity': 2}
891 >>> Options(**o.nondefaults) == o
892 True
893 """
894 return {name: option._value for name, option in self._options.items()
895 if option._value != option.default}
898# --------------------------------------
899__all__ = api_end(_API_START, globals())