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

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) 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_lwr_bnd_limit", float, None, """
374 Tells CPLEX to stop MIP optimization if a lower bound below this value
375 is found.
376 """, None),
378 ("cplex_upr_bnd_limit", float, None, """
379 Tells CPLEX to stop MIP optimization if an upper bound above this value
380 is found.
381 """, None),
383 ("cplex_bnd_monitor", bool, False, """
384 Tells CPLEX to store information about the evolution of the bounds
385 during the MIP solution search process. At the end of the computation, a
386 list of triples ``(time, lowerbound, upperbound)`` will be provided in
387 the field ``bounds_monitor`` of the dictionary returned by
388 :meth:`~.problem.Problem.solve`.
389 """),
391 ("cvxopt_kktsolver", (str, types.FunctionType), "ldl", """
392 The KKT solver used by CVXOPT internally.
394 See `CVXOPT's guide on exploiting structure
395 <https://cvxopt.org/userguide/coneprog.html#exploiting-structure>`_.
397 :obj:`None` denotes CVXOPT's default KKT solver.
398 """, None),
400 ("cvxopt_kktreg", float, 1e-9, """
401 The KKT solver regularization term used by CVXOPT internally.
403 This is an undocumented feature of CVXOPT, see `here
404 <https://github.com/cvxopt/cvxopt/issues/36#issuecomment-125165634>`_.
406 End of 2020, this option only affected the LDL KKT solver.
408 :obj:`None` denotes CVXOPT's default value.
409 """, None),
411 ("gurobi_params", dict, {}, """
412 A dictionary of Gurobi parameters to be set after general options are
413 passed and before the search is started.
415 For example, ``{"NodeLimit": 25}`` limits the number of nodes visited by
416 the MIP optimizer to :math:`25`."""),
418 ("mosek_params", dict, {}, """
419 A dictionary of MOSEK (Optimizer) parameters to be set after general
420 options are passed and before the search is started.
422 See the `list of MOSEK (Optimizer) 8.1 parameters
423 <https://docs.mosek.com/8.1/pythonapi/parameters.html>`_."""),
425 ("mskfsn_params", dict, {}, """
426 A dictionary of MOSEK (Fusion) parameters to be set after general
427 options are passed and before the search is started.
429 See the `list of MOSEK (Fusion) 8.1 parameters
430 <https://docs.mosek.com/8.1/pythonfusion/parameters.html>`_."""),
432 ("scip_params", dict, {}, """
433 A dictionary of SCIP parameters to be set after general options are
434 passed and before the search is started.
436 For example, ``{"lp/threads": 4}`` sets the number of threads to solve
437 LPs with to :math:`4`."""),
438]
439"""The table of available solver options.
441Each entry is a tuple representing a single solver option. The tuple's entries
442are, in order:
444- Name of the option. Must be a valid Python attribute name.
445- The option's argument type. Will be cast on any argument that is not already
446 an instance of the type, except for :obj:`None`.
447- The option's default value. Must already be of the proper type, or
448 :obj:`None`, and must pass the optional check.
449- The option's description, which is used as part of the docstring of
450 :class:`Options`. In the case of a multi-line text, leading and trailing
451 empty lines as well as the overall indentation are ignored.
452- Optional: A boolean function used on every argument that passes the type
453 conversion (so either an argument of the proper type, or :obj:`None`). If the
454 function returns ``False``, then the argument is rejected. The default
455 function rejects exactly :obj:`None`. Supplying :obj:`None` instead of a
456 function accepts all arguments (in particular, accepts :obj:`None`).
457"""
459# Add per-solver options.
460for name, solver in all_solvers().items():
461 OPTIONS.append(("penalty_{}".format(name), float, solver.default_penalty(),
462 """
463 Penalty for using the {} solver.
465 If solver :math:`A` has a penalty of :math:`p` and solver :math:`B` has
466 a larger penality of :math:`p + x`, then :math:`B` is be chosen over
467 :math:`A` only if the problem as passed to :math:`A` would be
468 :math:`10^x` times larger as when passed to :math:`B`.
469 """.format(name.upper())))
471del name, solver
473OPTIONS = sorted(OPTIONS)
476class Option():
477 """Optimization solver option.
479 A single option that affects how a :class:`~.problem.Problem` is solved.
481 An initial instance of this class is built from each entry of the
482 :data:`OPTIONS` table to obtain the :data:`OPTION_OBJS` tuple.
483 """
485 # Define __new__ in addition to __init__ so that copy can bypass __init__.
486 def __new__(cls, *args, **kwargs):
487 """Create a blank :class:`Option` to be filled in by :meth:`copy`."""
488 return super(Option, cls).__new__(cls)
490 def __init__(self, name, argType, default, description,
491 check=(lambda x: x is not None)):
492 """Initialize an :class:`Option`.
494 See :data:`OPTIONS`.
495 """
496 assert default is None or isinstance(default, argType)
497 assert check is None or check(default)
499 self.name = name
500 self.argType = argType
501 self.default = default
502 self._value = default
503 self.description = self._normalize_description(description)
504 self.check = check
506 def _normalize_description(self, description):
507 lines = description.splitlines()
508 notSpace = [n for n, line in enumerate(lines) if line.strip()]
509 if not notSpace:
510 return ""
511 first, last = min(notSpace), max(notSpace)
512 i = len(lines[first]) - len(lines[first].lstrip())
513 return "\n".join(line[i:].rstrip() for line in lines[first:last+1])
515 def _set_value(self, value):
516 if value is not None and not isinstance(value, self.argType):
517 if isinstance(self.argType, type):
518 try:
519 value = self.argType(value)
520 except Exception as error:
521 raise TypeError("Failed to convert argument {} to option "
522 "'{}' to type {}.".format(repr(value), self.name,
523 self.argType.__name__)) from error
524 else:
525 assert isinstance(self.argType, (tuple, list))
526 assert all(isinstance(t, type) for t in self.argType)
528 raise TypeError("Argument {} to option '{}' does not match "
529 "permissible types {}.".format(repr(value), self.name,
530 ", ".join(t.__name__ for t in self.argType)))
532 if self.check is not None and not self.check(value):
533 raise ValueError("The option '{}' does not accept the value {}."
534 .format(self.name, repr(value)))
536 self._value = value
538 value = property(lambda self: self._value, _set_value)
540 def reset(self):
541 """Reset the option to its default value."""
542 self.value = self.default
544 def is_default(self):
545 """Whether the option has its default value."""
546 return self.value == self.default
548 def copy(self):
549 """Return an independent copy of the option."""
550 theCopy = self.__class__.__new__(self.__class__)
551 theCopy.name = self.name
552 theCopy.argType = self.argType
553 theCopy.default = self.default
554 theCopy._value = self._value
555 theCopy.description = self.description
556 theCopy.check = self.check
557 return theCopy
560OPTION_OBJS = tuple(Option(*args) for args in OPTIONS)
561"""The initial solver options as :class:`Option` objects."""
564def _tablerow(option, indentaion=0):
565 """Return a reST list-table row describing an :class:`Option`."""
566 spaces = " "*indentaion
567 return (
568 "{}- * {{0}}\n"
569 "{} * ``{{1}}``\n"
570 "{} * .. _option_{{0}}:\n\n"
571 "{} {{2}}"
572 ).format(
573 *(4*(spaces,))).format(option.name, repr(option.default),
574 "\n{} ".format(spaces).join(option.description.splitlines()))
577def _jumplabel(option):
578 """Return a reStructuredText jumplabel describing an :class:`Option`."""
579 return ":ref:`{0} <option_{0}>`".format(option.name)
582class Options():
583 """Collection of optimization solver options.
585 A collection of options that affect how a :class:`~.problem.Problem` is
586 solved. :attr:`Problem.options <.problem.Problem.options>` is an instance of
587 this class.
589 The options can be accessed as an attribute or as an item. The latter
590 approach supports Unix shell-style wildcard characters:
592 >>> import picos
593 >>> P = picos.Problem()
594 >>> P.options.verbosity = 2
595 >>> P.options["primals"] = False
596 >>> # Set all absolute tolerances at once.
597 >>> P.options["abs_*_tol"] = 1e-6
599 There are two corresponding ways to reset an option to its default value:
601 >>> del P.options.verbosity
602 >>> P.options.reset("primals", "*_tol")
604 Options can also be passed as a keyword argument sequence when the
605 :class:`Problem <picos.Problem>` is created and whenever a solution is
606 searched:
608 >>> # Use default options except for verbosity.
609 >>> P = picos.Problem(verbosity = 1)
610 >>> x = P.add_variable("x", lower = 0); P.set_objective("min", x)
611 >>> # Only for the next search: Don't be verbose anyway.
612 >>> solution = P.solve(solver = "cvxopt", verbosity = 0)
613 """
615 # Document the individual options.
616 __doc__ += \
617 """
618 .. rubric:: Available Options
620 Jump to option: ➥\xa0{}
622 .. list-table::
623 :header-rows: 1
624 :widths: 10 10 80
626 - * Option
627 * Default
628 * Description
629 """.format(" ➥\xa0".join(_jumplabel(option) for option in OPTION_OBJS)) \
630 .rstrip() + "\n" + "\n".join(_tablerow(option, 6) for option in OPTION_OBJS)
632 # Define __new__ in addition to __init__ so that
633 # 1. __init__ does not take the static default options as an argument,
634 # hiding them from the user and the documentation while
635 # 2. Options.copy can still bypass copying the default options (by bypassing
636 # __init__) so that options aren't copied twice.
637 def __new__(cls, *args, **kwargs):
638 """Create an empty options set."""
639 instance = super(Options, cls).__new__(cls)
640 # Options overwrites __setattr__, so we need to call object.__setattr__.
641 super(Options, cls).__setattr__(instance, "_options", {})
642 return instance
644 def __init__(self, **options):
645 """Create a default option set and set the given options on top."""
646 for option in OPTION_OBJS:
647 self._options[option.name] = option.copy()
649 self.update(**options)
651 def __str__(self):
652 defaults = sorted(
653 (o for o in self._options.values() if o.is_default()),
654 key=(lambda o: o.name))
655 modified = sorted(
656 (o for o in self._options.values() if not o.is_default()),
657 key=(lambda o: o.name))
659 nameLength = max(len(o.name) for o in self._options.values())
660 valueLength = max(len(str(o.value)) for o in self._options.values())
662 string = ""
664 if modified:
665 defaultLength = max(len(str(o.default)) for o in modified)
667 string += "Modified solver options:\n" + "\n".join((
668 " {{:{}}} = {{:{}}} (default: {{:{}}})".format(
669 nameLength, valueLength, defaultLength
670 ).format(
671 option.name, str(option.value), str(option.default))
672 for num, option in enumerate(modified)))
674 if defaults:
675 if modified:
676 string += "\n\n"
678 string += "Default solver options:\n" + "\n".join((
679 " {{:{}}} = {{}}".format(nameLength).format(
680 option.name, str(option.value))
681 for num, option in enumerate(defaults)))
683 return string
685 def __eq__(self, other):
686 if self is other:
687 return True
689 for name in self._options:
690 if self._options[name].value != other._options[name].value:
691 return False
693 return True
695 def _fuzzy(returnsSomething):
696 """Allow wildcards in option names."""
697 def decorator(method):
698 def wrapper(self, pattern, *extraArgs):
699 if any(char in pattern for char in "*?[!]"):
700 matching = fnmatch.filter(self._options.keys(), pattern)
702 if not matching:
703 raise LookupError("No option matches '{}'."
704 .format(pattern))
706 if returnsSomething:
707 return {name: method(self, name, *extraArgs)
708 for name in matching}
709 else:
710 for name in matching:
711 method(self, name, *extraArgs)
712 else:
713 if returnsSomething:
714 return method(self, pattern, *extraArgs)
715 else:
716 method(self, pattern, *extraArgs)
717 return wrapper
718 return decorator
720 @_fuzzy(True)
721 def __getattr__(self, name):
722 if name in self._options:
723 return self._options[name].value
724 else:
725 raise AttributeError("Unknown option '{}'.".format(name))
727 @_fuzzy(False)
728 def __setattr__(self, name, value):
729 if name in self._options:
730 self._options[name].value = value
731 else:
732 raise AttributeError("Unknown option '{}'.".format(name))
734 @_fuzzy(False)
735 def __delattr__(self, name):
736 if name in self._options:
737 self._options[name].reset()
738 else:
739 raise AttributeError("Unknown option '{}'.".format(name))
741 @_fuzzy(True)
742 def __getitem__(self, name):
743 if name in self._options:
744 return self._options[name].value
745 else:
746 raise LookupError("Unknown option '{}'.".format(name))
748 @_fuzzy(False)
749 def __setitem__(self, name, value):
750 if name in self._options:
751 self._options[name].value = value
752 else:
753 raise LookupError("Unknown option '{}'.".format(name))
755 def __contains__(self, name):
756 return name in self._options
758 def __dir__(self):
759 optionNames = [name for name in self._options.keys()]
760 list_ = super(Options, self).__dir__() + optionNames
761 return sorted(list_)
763 def copy(self):
764 """Return an independent copy of the current options set."""
765 theCopy = self.__class__.__new__(self.__class__)
766 for option in self._options.values():
767 theCopy._options[option.name] = option.copy()
768 return theCopy
770 def update(self, **options):
771 """Set multiple options at once.
773 This method is called with the keyword arguments supplied to the
774 :class:`Options` constructor, so the following two are the same:
776 >>> import picos
777 >>> a = picos.Options(verbosity = 1, primals = False)
778 >>> b = picos.Options()
779 >>> b.update(verbosity = 1, primals = False)
780 >>> a == b
781 True
783 :param options: A parameter sequence of options to set.
784 """
785 for key, val in options.items():
786 self[key] = val
788 def updated(self, **options):
789 """Return a modified copy."""
790 theCopy = self.copy()
791 if options:
792 theCopy.update(**options)
793 return theCopy
795 def self_or_updated(self, **options):
796 """Return either a modified copy or self, depending on given options."""
797 if options:
798 theCopy = self.copy()
799 theCopy.update(**options)
800 return theCopy
801 else:
802 return self
804 @_fuzzy(False)
805 def _reset_single(self, name):
806 self._options[name].reset()
808 def reset(self, *options):
809 """Reset all or a selection of options to their default values.
811 :param options: The names of the options to reset, may contain wildcard
812 characters. If no name is given, all options are reset.
813 """
814 if options:
815 for name in options:
816 self._reset_single(name)
817 else:
818 for option in self._options.values():
819 option.reset()
821 @_fuzzy(True)
822 def _help_single(self, name):
823 option = self._options[name]
824 return (
825 "Option: {}\n"
826 "Default: {}\n"
827 "\n {}"
828 ).format(option.name, str(option.default),
829 "\n ".join(option.description.splitlines()))
831 def help(self, *options):
832 """Print text describing selected options.
834 :param options: The names of the options to describe, may contain
835 wildcard characters.
836 """
837 for i, name in enumerate(options):
838 if i != 0:
839 print("\n\n")
840 retval = self._help_single(name)
841 if isinstance(retval, str):
842 print(retval)
843 else:
844 assert isinstance(retval, dict)
845 print("\n\n".join(retval.values()))
847 @property
848 def nondefaults(self):
849 """A dictionary mapping option names to nondefault values.
851 :Example:
853 >>> from picos import Options
854 >>> o = Options()
855 >>> o.verbosity = 2
856 >>> o.nondefaults
857 {'verbosity': 2}
858 >>> Options(**o.nondefaults) == o
859 True
860 """
861 return {name: option._value for name, option in self._options.items()
862 if option._value != option.default}
865# --------------------------------------
866__all__ = api_end(_API_START, globals())