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
« 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# ------------------------------------------------------------------------------
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 Solver, all_solvers
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 ("qics_params", dict, {}, """
465 A dictionary of QICS parameters to be set after general options are
466 passed and before the search is started.
468 See the `list of QICS parameters
469 <https://qics.readthedocs.io/en/stable/guide/reference.html#solving
470 #solving>`_."""),
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.
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.
481Each entry is a tuple representing a single solver option. The tuple's entries
482are, in order:
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"""
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.
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())))
511del name, solver
513OPTIONS = sorted(OPTIONS)
516class Option():
517 """Optimization solver option.
519 A single option that affects how a :class:`~.problem.Problem` is solved.
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 """
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)
530 def __init__(self, name, argType, default, description,
531 check=(lambda x: x is not None)):
532 """Initialize an :class:`Option`.
534 See :data:`OPTIONS`.
535 """
536 assert default is None or isinstance(default, argType)
537 assert check is None or check(default)
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
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])
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)
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)))
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)))
576 self._value = value
578 value = property(lambda self: self._value, _set_value)
580 def reset(self):
581 """Reset the option to its default value."""
582 self.value = self.default
584 def is_default(self):
585 """Whether the option has its default value."""
586 return self.value == self.default
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
600OPTION_OBJS = tuple(Option(*args) for args in OPTIONS)
601"""The initial solver options as :class:`Option` objects."""
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()))
617def _jumplabel(option):
618 """Return a reStructuredText jumplabel describing an :class:`Option`."""
619 return ":ref:`{0} <option_{0}>`".format(option.name)
622class Options():
623 """Collection of optimization solver options.
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.
629 The options can be accessed as an attribute or as an item. The latter
630 approach supports Unix shell-style wildcard characters:
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
639 There are two corresponding ways to reset an option to its default value:
641 >>> del P.options.verbosity
642 >>> P.options.reset("primals", "*_tol")
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:
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 """
656 # Document the individual options.
657 __doc__ += \
658 """
659 .. rubric:: Available Options
661 Jump to option: ➥\xa0{}
663 .. list-table::
664 :header-rows: 1
665 :widths: 10 10 80
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)
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
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()
690 self.update(**options)
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))
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())
703 string = ""
705 if modified:
706 defaultLength = max(len(str(o.default)) for o in modified)
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)))
715 if defaults:
716 if modified:
717 string += "\n\n"
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)))
724 return string
726 def __eq__(self, other):
727 """Report whether two sets of options are equal."""
728 if self is other:
729 return True
731 for name in self._options:
732 if self._options[name].value != other._options[name].value:
733 return False
735 return True
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)
744 if not matching:
745 raise LookupError("No option matches '{}'."
746 .format(pattern))
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
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))
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))
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))
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))
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))
797 def __contains__(self, name):
798 return name in self._options
800 def __dir__(self):
801 optionNames = [name for name in self._options.keys()]
802 list_ = super(Options, self).__dir__() + optionNames
803 return sorted(list_)
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
812 def update(self, **options):
813 """Set multiple options at once.
815 This method is called with the keyword arguments supplied to the
816 :class:`Options` constructor, so the following two are the same:
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
825 :param options: A parameter sequence of options to set.
826 """
827 for key, val in options.items():
828 self[key] = val
830 def updated(self, **options):
831 """Return a modified copy."""
832 theCopy = self.copy()
833 if options:
834 theCopy.update(**options)
835 return theCopy
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
846 @_fuzzy(False)
847 def _reset_single(self, name):
848 self._options[name].reset()
850 def reset(self, *options):
851 """Reset all or a selection of options to their default values.
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()
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()))
873 def help(self, *options):
874 """Print text describing selected options.
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()))
889 @property
890 def nondefaults(self):
891 """A dictionary mapping option names to nondefault values.
893 :Example:
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}
907# --------------------------------------
908__all__ = api_end(_API_START, globals())