Coverage for picos/modeling/problem.py: 75.13%
776 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) 2012-2017 Guillaume Sagnol
3# Copyright (C) 2017-2020 Maximilian Stahlberg
4#
5# This file is part of PICOS.
6#
7# PICOS is free software: you can redistribute it and/or modify it under the
8# terms of the GNU General Public License as published by the Free Software
9# Foundation, either version 3 of the License, or (at your option) any later
10# version.
11#
12# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
13# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# ------------------------------------------------------------------------------
20"""Implementation of :class:`Problem`."""
22import copy as pycopy
23import re
24import time
25from collections import OrderedDict
26from functools import lru_cache
27from textwrap import TextWrapper
28from types import MappingProxyType
30import cvxopt as cvx
31import numpy as np
33from .. import constraints, expressions, glyphs, settings
34from ..apidoc import api_end, api_start
35from ..expressions.data import cvx2np
36from ..expressions.uncertain import IntractableWorstCase
37from ..expressions.variables import BaseVariable
38from ..formatting import natsorted, parameterized_string, picos_box
39from ..legacy import deprecated, map_legacy_options, throw_deprecation_warning
40from ..solvers import Solver, get_solver
41from ..valuable import Valuable
42from .file_out import write
43from .footprint import Footprint, Specification
44from .objective import Objective
45from .options import Options
46from .solution import SS_OPTIMAL, Solution
48_API_START = api_start(globals())
49# -------------------------------
52class SolutionFailure(RuntimeError):
53 """Solving the problem failed."""
55 def __init__(self, code, message):
56 """Construct a :exc:`SolutionFailure`.
58 :param int code:
59 Status code, as defined in :meth:`Problem.solve`.
61 :param str message:
62 Text description of the failure.
63 """
64 #: Status code, as defined in :meth:`Problem.solve`.
65 self.code = code
67 #: Text description of the failure.
68 self.message = message
70 def __str__(self):
71 return "Code {}: {}".format(self.code, self.message)
74class Problem(Valuable):
75 """PICOS' representation of an optimization problem.
77 :Example:
79 >>> from picos import Problem, RealVariable
80 >>> X = RealVariable("X", (2,2), lower = 0)
81 >>> P = Problem("Example")
82 >>> P.maximize = X.tr
83 >>> C = X.sum <= 10
84 >>> P += C, X[0,0] == 1
85 >>> print(P)
86 Example (Linear Program)
87 maximize tr(X)
88 over
89 2×2 real variable X (bounded below)
90 subject to
91 ∑(X) ≤ 10
92 X[0,0] = 1
93 >>> # PICOS will select a suitable solver if you don't specify one.
94 >>> solution = P.solve(solver = "cvxopt")
95 >>> solution.claimedStatus
96 'optimal'
97 >>> solution.searchTime #doctest: +SKIP
98 0.002137422561645508
99 >>> round(P, 1)
100 10.0
101 >>> print(X) #doctest: +SKIP
102 [ 1.00e+00 4.89e-10]
103 [ 4.89e-10 9.00e+00]
104 >>> round(C.dual, 1)
105 1.0
106 """
108 #: The specification for problems returned by :meth:`conic_form`.
109 CONIC_FORM = Specification(
110 objectives=[expressions.AffineExpression],
111 constraints=[C for C in
112 (getattr(constraints, Cname) for Cname in constraints.__all__)
113 if issubclass(C, constraints.ConicConstraint)
114 and C is not constraints.ConicConstraint])
116 # --------------------------------------------------------------------------
117 # Initialization and reset methods.
118 # --------------------------------------------------------------------------
120 def __init__(
121 self, name=None, *, copyOptions=None, useOptions=None,
122 **extra_options
123 ):
124 """Create an empty problem and optionally set initial solver options.
126 :param str name:
127 A name or title to give to the optimization problem.
129 :param copyOptions:
130 An :class:`Options <picos.Options>` object to copy instead of using
131 the default options.
133 :param useOptions: An :class:`Options <picos.Options>` object to use
134 (without making a copy) instead of using the default options.
136 :param extra_options:
137 A sequence of additional solver options to apply on top of the
138 default options or those given by ``copyOptions`` or ``useOptions``.
139 """
140 if name and not isinstance(name, str):
141 raise TypeError(
142 "The first positional argument denotes the name of the problem "
143 "and must be a string.")
145 if copyOptions and useOptions:
146 raise ValueError(
147 "Can only copy or use existing solver options, not both.")
149 extra_options = map_legacy_options(**extra_options)
151 if copyOptions:
152 self._options = copyOptions.copy()
153 self._options.update(**extra_options)
154 elif useOptions:
155 self._options = useOptions
156 self._options.update(**extra_options)
157 else:
158 self._options = Options(**extra_options)
160 #: Explicit name for the problem.
161 self._name = name
163 #: The optimization objective.
164 self._objective = Objective()
166 #: Maps constraint IDs to constraints.
167 self._constraints = OrderedDict()
169 #: Contains lists of constraints added together, all in order.
170 self._con_groups = []
172 #: Maps mutables to number of occurences in objective or constraints.
173 self._mtb_count = {}
175 #: Maps mutable names to mutables.
176 self._mutables = OrderedDict()
178 #: Maps variable names to variables.
179 self._variables = OrderedDict()
181 #: Maps parameter names to parameters.
182 self._parameters = OrderedDict()
184 #: Current solution strategy.
185 self._strategy = None
187 #: The last :class:`Solution` applied to the problem.
188 self._last_solution = None # Set by Solution.apply.
190 def _reset_mutable_registry(self):
191 self._mtb_count.clear()
192 self._mutables.clear()
193 self._variables.clear()
194 self._parameters.clear()
196 def reset(self, resetOptions=False):
197 """Reset the problem instance to its initial empty state.
199 :param bool resetOptions:
200 Whether also solver options should be reset to their default values.
201 """
202 # Reset options if requested.
203 if resetOptions:
204 self._options.reset()
206 # Reset objective to "find an assignment".
207 del self.objective
209 # Reset constraint registry.
210 self._constraints.clear()
211 self._con_groups.clear()
213 # Reset mutable registry.
214 self._reset_mutable_registry()
216 # Reset strategy and solution data.
217 self._strategy = None
218 self._last_solution = None
220 # --------------------------------------------------------------------------
221 # Properties.
222 # --------------------------------------------------------------------------
224 @property
225 def name(self):
226 """Name or title of the problem."""
227 return self._name
229 @name.setter
230 def name(self, value):
231 if value and not isinstance(value, str):
232 raise TypeError("The problem name must be a string.")
234 if not value:
235 self._name = None
236 else:
237 self._name = value
239 @name.deleter
240 def name(self):
241 self._name = None
243 @property
244 def mutables(self):
245 """Maps names to variables and parameters in use by the problem.
247 :returns:
248 A read-only view to an :class:`~collections.OrderedDict`. The order
249 is deterministic and depends on the order of operations performed on
250 the :class:`Problem` instance as well as on the mutables' names.
251 """
252 return MappingProxyType(self._mutables)
254 @property
255 def variables(self):
256 """Maps names to variables in use by the problem.
258 :returns:
259 See :attr:`mutables`.
260 """
261 return MappingProxyType(self._variables)
263 @property
264 def parameters(self):
265 """Maps names to parameters in use by the problem.
267 :returns:
268 See :attr:`mutables`.
269 """
270 return MappingProxyType(self._parameters)
272 @property
273 def constraints(self):
274 """Maps constraint IDs to constraints that are part of the problem.
276 :returns:
277 A read-only view to an :class:`~collections.OrderedDict`. The order
278 is that in which constraints were added.
279 """
280 return MappingProxyType(self._constraints)
282 @constraints.deleter
283 def constraints(self):
284 # Clear constraint registry.
285 self._constraints.clear()
286 self._con_groups.clear()
288 # Update mutable registry.
289 self._reset_mutable_registry()
290 self._register_mutables(self.no.function.mutables)
292 @property
293 def objective(self):
294 """Optimization objective as an :class:`~picos.Objective` instance."""
295 return self._objective
297 @objective.setter
298 def objective(self, value):
299 self._unregister_mutables(self.no.function.mutables)
301 try:
302 if isinstance(value, Objective):
303 self._objective = value
304 else:
305 direction, function = value
306 self._objective = Objective(direction, function)
307 finally:
308 self._register_mutables(self.no.function.mutables)
310 @objective.deleter
311 def objective(self):
312 self._unregister_mutables(self.no.function.mutables)
314 self._objective = Objective()
316 @property
317 def no(self):
318 """Normalized objective as an :class:`~picos.Objective` instance.
320 Either a minimization or a maximization objective, with feasibility
321 posed as "minimize 0".
323 The same as the :attr:`~.objective.Objective.normalized` attribute of
324 the :attr:`objective`.
325 """
326 return self._objective.normalized
328 @property
329 def minimize(self):
330 """Minimization objective as an :class:`~.expression.Expression`.
332 This can be used to set a minimization objective. For querying the
333 objective, it is recommended to use :attr:`objective` instead.
334 """
335 if self._objective.direction == Objective.MIN:
336 return self._objective.function
337 else:
338 raise ValueError("Objective direction is not minimize.")
340 @minimize.setter
341 def minimize(self, value):
342 self.objective = "min", value
344 @minimize.deleter
345 def minimize(self):
346 if self._objective.direction == Objective.MIN:
347 del self.objective
348 else:
349 raise ValueError("Objective direction is not minimize.")
351 @property
352 def maximize(self):
353 """Maximization objective as an :class:`~.expression.Expression`.
355 This can be used to set a maximization objective. For querying the
356 objective, it is recommended to use :attr:`objective` instead.
357 """
358 if self._objective.direction == Objective.MAX:
359 return self._objective.function
360 else:
361 raise ValueError("Objective direction is not maximize.")
363 @maximize.setter
364 def maximize(self, value):
365 self.objective = "max", value
367 @maximize.deleter
368 def maximize(self):
369 if self._objective.direction == Objective.MAX:
370 del self.objective
371 else:
372 raise ValueError("Objective direction is not maximize.")
374 @property
375 def options(self):
376 """Solution search parameters as an :class:`~picos.Options` object."""
377 return self._options
379 @options.setter
380 def options(self, value):
381 if not isinstance(value, Options):
382 raise TypeError("Cannot assign an object of type {} as a problem's "
383 " options.".format(type(value).__name__))
385 self._options = value
387 @options.deleter
388 def options(self, value):
389 self._options.reset()
391 @property
392 def strategy(self):
393 """Solution strategy as a :class:`~picos.modeling.Strategy` object.
395 A strategy is available once you order the problem to be solved and it
396 will be reused for successive solution attempts (of a modified problem)
397 while it remains valid with respect to the problem's :attr:`footprint`.
399 When a strategy is reused, modifications to the objective and
400 constraints of a problem are passed step by step through the strategy's
401 reformulation pipeline while existing reformulation work is not
402 repeated. If the solver also supports these kinds of updates, then
403 modifying and re-solving a problem can be much faster than solving the
404 problem from scratch.
406 :Example:
408 >>> from picos import Problem, RealVariable
409 >>> x = RealVariable("x", 2)
410 >>> P = Problem()
411 >>> P.set_objective("min", abs(x)**2)
412 >>> print(P.strategy)
413 None
414 >>> sol = P.solve(solver = "cvxopt") # Creates a solution strategy.
415 >>> print(P.strategy)
416 1. ExtraOptions
417 2. EpigraphReformulation
418 3. SquaredNormToConicReformulation
419 4. CVXOPTSolver
420 >>> # Add another constraint handled by SquaredNormToConicReformulation:
421 >>> P.add_constraint(abs(x - 2)**2 <= 1)
422 <Squared Norm Constraint: ‖x - [2]‖² ≤ 1>
423 >>> P.strategy.valid(solver = "cvxopt")
424 True
425 >>> P.strategy.valid(solver = "glpk")
426 False
427 >>> sol = P.solve(solver = "cvxopt") # Reuses the strategy.
429 It's also possible to create a startegy from scratch:
431 >>> from picos.modeling import Strategy
432 >>> from picos.reforms import (EpigraphReformulation,
433 ... ConvexQuadraticToConicReformulation)
434 >>> from picos.solvers import CVXOPTSolver
435 >>> # Mimic what solve() does when no strategy exists:
436 >>> P.strategy = Strategy(P, CVXOPTSolver, EpigraphReformulation,
437 ... ConvexQuadraticToConicReformulation)
438 """
439 return self._strategy
441 @strategy.setter
442 def strategy(self, value):
443 from .strategy import Strategy
445 if not isinstance(value, Strategy):
446 raise TypeError(
447 "Cannot assign an object of type {} as a solution strategy."
448 .format(type(value).__name__))
450 if value.problem is not self:
451 raise ValueError("The solution strategy was constructed for a "
452 "different problem.")
454 self._strategy = value
456 @strategy.deleter
457 def strategy(self):
458 self._strategy = None
460 @property
461 def last_solution(self):
462 """The last :class:`~picos.Solution` applied to the problem."""
463 return self._last_solution
465 @property
466 def status(self):
467 """The solution status string as claimed by :attr:`last_solution`."""
468 if not self._last_solution:
469 return "unsolved"
470 else:
471 return self._last_solution.claimedStatus
473 @property
474 def footprint(self):
475 """Problem footprint as a :class:`~picos.modeling.Footprint` object."""
476 return Footprint.from_problem(self)
478 @property
479 def continuous(self):
480 """Whether all variables are of continuous types."""
481 return all(
482 isinstance(variable, expressions.CONTINUOUS_VARTYPES)
483 for variable in self._variables.values())
485 @property
486 def pure_integer(self):
487 """Whether all variables are of integral types."""
488 return not any(
489 isinstance(variable, expressions.CONTINUOUS_VARTYPES)
490 for variable in self._variables.values())
492 @property
493 def type(self):
494 """The problem type as a string, such as "Linear Program"."""
495 C = set(type(c) for c in self._constraints.values())
496 objective = self._objective.function
497 base = "Optimization Problem"
499 linear = [
500 constraints.AffineConstraint,
501 constraints.ComplexAffineConstraint,
502 constraints.AbsoluteValueConstraint,
503 constraints.SimplexConstraint,
504 constraints.FlowConstraint]
505 sdp = [
506 constraints.LMIConstraint,
507 constraints.ComplexLMIConstraint]
508 quadratic = [
509 constraints.ConvexQuadraticConstraint,
510 constraints.ConicQuadraticConstraint,
511 constraints.NonconvexQuadraticConstraint]
512 quadconic = [
513 constraints.SOCConstraint,
514 constraints.RSOCConstraint]
515 exponential = [
516 constraints.ExpConeConstraint,
517 constraints.SumExponentialsConstraint,
518 constraints.LogSumExpConstraint,
519 constraints.LogConstraint,
520 constraints.KullbackLeiblerConstraint]
521 quantum = [
522 expressions.QuantumEntropy,
523 expressions.NegativeQuantumEntropy,
524 expressions.QuantumConditionalEntropy,
525 expressions.QuantumKeyDistribution,
526 constraints.QuantRelEntropyConstraint,
527 constraints.QuantCondEntropyConstraint,
528 constraints.QuantKeyDistributionConstraint]
529 complex = [
530 constraints.ComplexAffineConstraint,
531 constraints.ComplexLMIConstraint]
533 if objective is None:
534 if not C:
535 base = "Empty Problem"
536 elif C.issubset(set(linear)):
537 base = "Linear Feasibility Problem"
538 else:
539 base = "Feasibility Problem"
540 elif isinstance(objective, expressions.AffineExpression):
541 if not C:
542 if objective.constant:
543 base = "Constant Problem"
544 else:
545 base = "Linear Program" # Could have variable bounds.
546 elif C.issubset(set(linear)):
547 base = "Linear Program"
548 elif C.issubset(set(linear + quadconic)):
549 base = "Second Order Cone Program"
550 elif C.issubset(set(linear + sdp)):
551 base = "Semidefinite Program"
552 elif C.issubset(set(linear + [constraints.LogSumExpConstraint])):
553 base = "Geometric Program"
554 elif C.issubset(set(linear + exponential)):
555 base = "Exponential Program"
556 elif C.issubset(set(linear + quadratic)):
557 base = "Quadratically Constrained Program"
558 elif C.issubset(set(linear + sdp + quantum)):
559 base = "Quantum Relative Entropy Program"
560 elif isinstance(objective, expressions.QuadraticExpression):
561 if C.issubset(set(linear)):
562 base = "Quadratic Program"
563 elif C.issubset(set(linear + quadratic)):
564 base = "Quadratically Constrained Quadratic Program"
565 elif isinstance(objective, expressions.LogSumExp):
566 if C.issubset(set(linear + [constraints.LogSumExpConstraint])):
567 base = "Geometric Program"
568 elif isinstance(objective, tuple(quantum)):
569 if C.issubset(set(linear + sdp + quantum)):
570 base = "Quantum Relative Entropy Program"
571 elif isinstance(objective, expressions.WeightedSum):
572 if all(isinstance(expr, tuple(linear + quantum))
573 for expr in objective.expressions):
574 if C.issubset(set(linear + sdp + quantum)):
575 base = "Quantum Relative Entropy Program"
577 if self.continuous:
578 integrality = ""
579 elif self.pure_integer:
580 integrality = "Integer "
581 else:
582 integrality = "Mixed-Integer "
584 if any(c in complex for c in C):
585 complexity = "Complex "
586 else:
587 complexity = ""
589 return "{}{}{}".format(complexity, integrality, base)
591 @property
592 def dual(self):
593 """The Lagrangian dual problem of the standardized problem.
595 More precisely, this property invokes the following:
597 1. The primal problem is posed as an equivalent conic standard form
598 minimization problem, with variable bounds expressed as additional
599 constraints.
600 2. The Lagrangian dual problem of the reposed primal is computed.
601 3. The optimization direction and objective function sign of the dual
602 are adjusted such that, given strong duality and primal feasibility,
603 the optimal values of both problems are equal. In particular, if the
604 primal problem is a minimization or a maximization problem, the dual
605 problem returned will be the respective other.
607 :raises ~picos.modeling.strategy.NoStrategyFound:
608 If no reformulation strategy was found.
610 .. note::
612 This property is intended for educational purposes.
613 If you want to solve the primal problem via its dual, use the
614 :ref:`dualize <option_dualize>` option instead.
615 """
616 from ..reforms import Dualization
617 return self.reformulated(Dualization.SUPPORTED, dualize=True)
619 @property
620 def conic_form(self):
621 """The problem in conic form.
623 Reformulates the problem such that the objective is affine and all
624 constraints are :class:`~.constraints.ConicConstraint` instances.
626 :raises ~picos.modeling.strategy.NoStrategyFound:
627 If no reformulation strategy was found.
629 :Example:
631 >>> from picos import Problem, RealVariable
632 >>> x = RealVariable("x", 2)
633 >>> P = Problem()
634 >>> P.set_objective("min", abs(x)**2)
635 >>> print(P)
636 Quadratic Program
637 minimize ‖x‖²
638 over
639 2×1 real variable x
640 >>> print(P.conic_form)# doctest: +ELLIPSIS
641 Second Order Cone Program
642 minimize __..._t
643 over
644 1×1 real variable __..._t
645 2×1 real variable x
646 subject to
647 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0
649 .. note::
651 This property is intended for educational purposes.
652 You do not need to use it when solving a problem as PICOS will
653 perform the necessary reformulations automatically.
654 """
655 return self.reformulated(self.CONIC_FORM)
657 # --------------------------------------------------------------------------
658 # Python special methods, except __init__.
659 # --------------------------------------------------------------------------
661 @property
662 def _var_groups(self):
663 """Support :meth:`__str__`."""
664 vars_by_type = {}
665 for var in self._variables.values():
666 vtype = type(var).__name__
667 shape = var.shape
668 bound = tuple(bool(bound) for bound in var.bound_dicts)
669 index = (vtype, shape, bound)
671 vars_by_type.setdefault(index, set())
672 vars_by_type[index].add(var)
674 groups = []
675 for index in sorted(vars_by_type.keys()):
676 groups.append(natsorted(vars_by_type[index], key=lambda v: v.name))
678 return groups
680 @property
681 def _prm_groups(self):
682 """Support :meth:`__str__`."""
683 prms_by_type = {}
684 for prm in self._parameters.values():
685 vtype = type(prm).__name__
686 shape = prm.shape
687 index = (vtype, shape)
689 prms_by_type.setdefault(index, set())
690 prms_by_type[index].add(prm)
692 groups = []
693 for index in sorted(prms_by_type.keys()):
694 groups.append(natsorted(prms_by_type[index], key=lambda v: v.name))
696 return groups
698 @lru_cache()
699 def _mtb_group_string(self, group):
700 """Support :meth:`__str__`."""
701 if len(group) == 0:
702 return "[no mutables]"
704 if len(group) == 1:
705 return group[0].long_string
707 try:
708 template, data = parameterized_string(
709 [mtb.long_string for mtb in group])
710 except ValueError:
711 # HACK: Use the plural of the type string (e.g. "real variables").
712 type_string = group[0]._get_type_string_base().lower()
713 base_string = group[0].long_string.replace(
714 type_string, type_string + "s")
716 # HACK: Move any bound string to the end.
717 match = re.match(r"([^(]*)( \([^)]*\))", base_string)
718 if match:
719 base_string = match[1]
720 bound_string = match[2]
721 else:
722 bound_string = ""
724 return base_string \
725 + ", " + ", ".join([v.name for v in group[1:]]) + bound_string
726 else:
727 return glyphs.forall(template, data)
729 @lru_cache()
730 def _con_group_string(self, group):
731 """Support :meth:`__str__`."""
732 if len(group) == 0:
733 return "[no constraints]"
735 if len(group) == 1:
736 return str(group[0])
738 try:
739 template, data = parameterized_string([str(con) for con in group])
740 except ValueError:
741 return "[{} constraints (1st: {})]".format(len(group), group[0])
742 else:
743 return glyphs.forall(template, data)
745 def __repr__(self):
746 if self._name:
747 return glyphs.repr2(self.type, self._name)
748 else:
749 return glyphs.repr1(self.type)
751 def __str__(self):
752 # Print problem name (if available) and type.
753 if self._name:
754 string = "{} ({})\n".format(self._name, self.type)
755 else:
756 string = "{}\n".format(self.type)
758 # Print objective.
759 string += " {}\n".format(self._objective)
761 wrapper = TextWrapper(
762 initial_indent=" "*4,
763 subsequent_indent=" "*6,
764 break_long_words=False,
765 break_on_hyphens=False)
767 # Print variables.
768 if self._variables:
769 string += " {}\n".format(
770 "for" if self._objective.direction == "find" else "over")
771 for group in self._var_groups:
772 string += wrapper.fill(self._mtb_group_string(tuple(group)))
773 string += "\n"
775 # Print constraints.
776 if self._constraints:
777 string += " subject to\n"
778 for index, group in enumerate(self._con_groups):
779 string += wrapper.fill(self._con_group_string(tuple(group)))
780 string += "\n"
782 # Print parameters.
783 if self._parameters:
784 string += " given\n"
785 for group in self._prm_groups:
786 string += wrapper.fill(self._mtb_group_string(tuple(group)))
787 string += "\n"
789 return string.rstrip("\n")
791 def __iadd__(self, constraints):
792 """See :meth:`require`."""
793 if isinstance(constraints, tuple):
794 self.require(*constraints)
795 else:
796 self.require(constraints)
798 return self
800 # --------------------------------------------------------------------------
801 # Bookkeeping methods.
802 # --------------------------------------------------------------------------
804 def _register_mutables(self, mtbs):
805 """Register the mutables of an objective function or constraint."""
806 # Register every mutable at most once per call.
807 if not isinstance(mtbs, (set, frozenset)):
808 raise TypeError("Mutable registry can (un)register a mutable "
809 "only once per call, so the argument must be a set type.")
811 # Retrieve old and new mutables as mapping from name to object.
812 old_mtbs = self._mutables
813 new_mtbs = OrderedDict(
814 (mtb.name, mtb) for mtb in sorted(mtbs, key=(lambda m: m.name)))
815 new_vars = OrderedDict((name, mtb) for name, mtb in new_mtbs.items()
816 if isinstance(mtb, BaseVariable))
817 new_prms = OrderedDict((name, mtb) for name, mtb in new_mtbs.items()
818 if not isinstance(mtb, BaseVariable))
820 # Check for mutable name clashes within the new set.
821 if len(new_mtbs) != len(mtbs):
822 raise ValueError(
823 "The object you are trying to add to a problem contains "
824 "multiple mutables of the same name. This is not allowed.")
826 # Check for mutable name clashes with existing mutables.
827 for name in set(old_mtbs).intersection(set(new_mtbs)):
828 if old_mtbs[name] is not new_mtbs[name]:
829 raise ValueError("Cannot register the mutable {} with the "
830 "problem because it already tracks another mutable with "
831 "the same name.".format(name))
833 # Keep track of new mutables.
834 self._mutables.update(new_mtbs)
835 self._variables.update(new_vars)
836 self._parameters.update(new_prms)
838 # Count up the mutable references.
839 for mtb in mtbs:
840 self._mtb_count.setdefault(mtb, 0)
841 self._mtb_count[mtb] += 1
843 def _unregister_mutables(self, mtbs):
844 """Unregister the mutables of an objective function or constraint."""
845 # Unregister every mutable at most once per call.
846 if not isinstance(mtbs, (set, frozenset)):
847 raise TypeError("Mutable registry can (un)register a mutable "
848 "only once per call, so the argument must be a set type.")
850 for mtb in mtbs:
851 name = mtb.name
853 # Make sure the mutable is properly registered.
854 assert name in self._mutables and mtb in self._mtb_count, \
855 "Tried to unregister a mutable that is not registered."
856 assert self._mtb_count[mtb] >= 1, \
857 "Found a nonpostive mutable count."
859 # Count down the mutable references.
860 self._mtb_count[mtb] -= 1
862 # Remove a mutable with a reference count of zero.
863 if not self._mtb_count[mtb]:
864 self._mtb_count.pop(mtb)
865 self._mutables.pop(name)
867 if isinstance(mtb, BaseVariable):
868 self._variables.pop(name)
869 else:
870 self._parameters.pop(name)
872 # --------------------------------------------------------------------------
873 # Methods to manipulate the objective function and its direction.
874 # --------------------------------------------------------------------------
876 def set_objective(self, direction=None, expression=None):
877 """Set the optimization direction and objective function of the problem.
879 :param str direction:
880 Case insensitive search direction string. One of
882 - ``"min"`` or ``"minimize"``,
883 - ``"max"`` or ``"maximize"``,
884 - ``"find"`` or :obj:`None` (for a feasibility problem).
886 :param ~picos.expressions.Expression expression:
887 The objective function. Must be :obj:`None` for a feasibility
888 problem.
889 """
890 self.objective = direction, expression
892 # --------------------------------------------------------------------------
893 # Methods to add, retrieve and remove constraints.
894 # --------------------------------------------------------------------------
896 def _lookup_constraint(self, idOrIndOrCon):
897 """Look for a constraint with the given identifier.
899 Given a constraint object or ID or offset or a constraint group index or
900 index pair, returns a matching (list of) constraint ID(s) that is (are)
901 part of the problem.
902 """
903 if isinstance(idOrIndOrCon, int):
904 if idOrIndOrCon in self._constraints:
905 # A valid ID.
906 return idOrIndOrCon
907 elif idOrIndOrCon < len(self._constraints):
908 # An offset.
909 return list(self._constraints.keys())[idOrIndOrCon]
910 else:
911 raise LookupError(
912 "The problem has no constraint with ID or offset {}."
913 .format(idOrIndOrCon))
914 elif isinstance(idOrIndOrCon, constraints.Constraint):
915 # A constraint object.
916 id = idOrIndOrCon.id
917 if id in self._constraints:
918 return id
919 else:
920 raise KeyError("The constraint '{}' is not part of the problem."
921 .format(idOrIndOrCon))
922 elif isinstance(idOrIndOrCon, tuple) or isinstance(idOrIndOrCon, list):
923 if len(idOrIndOrCon) == 1:
924 groupIndex = idOrIndOrCon[0]
925 if groupIndex < len(self._con_groups):
926 return [c.id for c in self._con_groups[groupIndex]]
927 else:
928 raise IndexError("Constraint group index out of range.")
929 elif len(idOrIndOrCon) == 2:
930 groupIndex, groupOffset = idOrIndOrCon
931 if groupIndex < len(self._con_groups):
932 group = self._con_groups[groupIndex]
933 if groupOffset < len(group):
934 return group[groupOffset].id
935 else:
936 raise IndexError(
937 "Constraint group offset out of range.")
938 else:
939 raise IndexError("Constraint group index out of range.")
940 else:
941 raise TypeError("If looking up constraints by group, the index "
942 "must be a tuple or list of length at most two.")
943 else:
944 raise TypeError("Argument of type '{}' not supported when looking "
945 "up constraints".format(type(idOrIndOrCon)))
947 def get_constraint(self, idOrIndOrCon):
948 """Return a (list of) constraint(s) of the problem.
950 :param idOrIndOrCon: One of the following:
952 * A constraint object. It will be returned when the constraint is
953 part of the problem, otherwise a KeyError is raised.
954 * The integer ID of the constraint.
955 * The integer offset of the constraint in the list of all
956 constraints that are part of the problem, in the order that they
957 were added.
958 * A list or tuple of length 1. Its only element is the index of a
959 constraint group (of constraints that were added together), where
960 groups are indexed in the order that they were added to the
961 problem. The whole group is returned as a list of constraints.
962 That list has the constraints in the order that they were added.
963 * A list or tuple of length 2. The first element is a constraint
964 group offset as above, the second an offset within that list.
966 :type idOrIndOrCon: picos.constraints.Constraint or int or tuple or list
968 :returns: A :class:`constraint <picos.constraints.Constraint>` or a list
969 thereof.
971 :Example:
973 >>> import picos as pic
974 >>> import cvxopt as cvx
975 >>> from pprint import pprint
976 >>> prob=pic.Problem()
977 >>> x=[pic.RealVariable('x[{0}]'.format(i),2) for i in range(5)]
978 >>> y=pic.RealVariable('y',5)
979 >>> Cx=prob.add_list_of_constraints([x[i].sum < y[i] for i in range(5)])
980 >>> Cy=prob.add_constraint(y>0)
981 >>> print(prob)
982 Linear Feasibility Problem
983 find an assignment
984 for
985 2×1 real variable x[i] ∀ i ∈ [0…4]
986 5×1 real variable y
987 subject to
988 ∑(x[i]) ≤ y[i] ∀ i ∈ [0…4]
989 y ≥ 0
990 >>> # Retrieve the second constraint, indexed from zero:
991 >>> prob.get_constraint(1)
992 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>
993 >>> # Retrieve the fourth consraint from the first group:
994 >>> prob.get_constraint((0,3))
995 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>
996 >>> # Retrieve the whole first group of constraints:
997 >>> pprint(prob.get_constraint((0,)))
998 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
999 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>,
1000 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1001 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1002 <1×1 Affine Constraint: ∑(x[4]) ≤ y[4]>]
1003 >>> # Retrieve the second "group", containing just one constraint:
1004 >>> prob.get_constraint((1,))
1005 [<5×1 Affine Constraint: y ≥ 0>]
1006 """
1007 idOrIds = self._lookup_constraint(idOrIndOrCon)
1009 if isinstance(idOrIds, list):
1010 return [self._constraints[id] for id in idOrIds]
1011 else:
1012 return self._constraints[idOrIds]
1014 def add_constraint(self, constraint, key=None):
1015 """Add a single constraint to the problem and return it.
1017 :param constraint:
1018 The constraint to be added.
1019 :type constraint:
1020 :class:`Constraint <picos.constraints.Constraint>`
1022 :param key: DEPRECATED
1024 :returns:
1025 The constraint that was added to the problem.
1027 .. note::
1029 This method is superseded by the more compact and more flexible
1030 :meth:`require` method or, at your preference, the ``+=`` operator.
1031 """
1032 # Handle deprecated 'key' parameter.
1033 if key is not None:
1034 throw_deprecation_warning(
1035 "Naming constraints is currently not supported.")
1037 # Register the constraint.
1038 self._constraints[constraint.id] = constraint
1039 self._con_groups.append([constraint])
1041 # Register the constraint's mutables.
1042 self._register_mutables(constraint.mutables)
1044 return constraint
1046 def add_list_of_constraints(self, lst, it=None, indices=None, key=None):
1047 """Add constraints from an iterable to the problem.
1049 :param lst:
1050 Iterable of constraints to add.
1052 :param it: DEPRECATED
1053 :param indices: DEPRECATED
1054 :param key: DEPRECATED
1056 :returns:
1057 A list of all constraints that were added.
1059 :Example:
1061 >>> import picos as pic
1062 >>> import cvxopt as cvx
1063 >>> from pprint import pprint
1064 >>> prob=pic.Problem()
1065 >>> x=[pic.RealVariable('x[{0}]'.format(i),2) for i in range(5)]
1066 >>> pprint(x)
1067 [<2×1 Real Variable: x[0]>,
1068 <2×1 Real Variable: x[1]>,
1069 <2×1 Real Variable: x[2]>,
1070 <2×1 Real Variable: x[3]>,
1071 <2×1 Real Variable: x[4]>]
1072 >>> y=pic.RealVariable('y',5)
1073 >>> IJ=[(1,2),(2,0),(4,2)]
1074 >>> w={}
1075 >>> for ij in IJ:
1076 ... w[ij]=pic.RealVariable('w[{},{}]'.format(*ij),3)
1077 ...
1078 >>> u=pic.new_param('u',cvx.matrix([2,5]))
1079 >>> C1=prob.add_list_of_constraints([u.T*x[i] < y[i] for i in range(5)])
1080 >>> C2=prob.add_list_of_constraints([abs(w[i,j])<y[j] for (i,j) in IJ])
1081 >>> C3=prob.add_list_of_constraints([y[t] > y[t+1] for t in range(4)])
1082 >>> print(prob)
1083 Feasibility Problem
1084 find an assignment
1085 for
1086 2×1 real variable x[i] ∀ i ∈ [0…4]
1087 3×1 real variable w[i,j] ∀ (i,j) ∈ zip([1,2,4],[2,0,2])
1088 5×1 real variable y
1089 subject to
1090 uᵀ·x[i] ≤ y[i] ∀ i ∈ [0…4]
1091 ‖w[i,j]‖ ≤ y[j] ∀ (i,j) ∈ zip([1,2,4],[2,0,2])
1092 y[i] ≥ y[i+1] ∀ i ∈ [0…3]
1094 .. note::
1096 This method is superseded by the more compact and more flexible
1097 :meth:`require` method or, at your preference, the ``+=`` operator.
1098 """
1099 if it is not None or indices is not None or key is not None:
1100 # Deprecated as of 2.0.
1101 throw_deprecation_warning("Arguments 'it', 'indices' and 'key' to "
1102 "add_list_of_constraints are deprecated and ignored.")
1104 added = []
1105 for constraint in lst:
1106 added.append(self.add_constraint(constraint))
1107 self._con_groups.pop()
1109 if added:
1110 self._con_groups.append(added)
1112 return added
1114 def require(self, *constraints, ret=False):
1115 """Add constraints to the problem.
1117 :param constraints:
1118 A sequence of constraints or constraint groups (iterables yielding
1119 constraints) or a mix thereof.
1121 :param bool ret:
1122 Whether to return the added constraints.
1124 :returns:
1125 When ``ret=True``, returns either the single constraint that was
1126 added, the single group of constraint that was added in the form of
1127 a :class:`list` or, when multiple arguments are given, a list of
1128 constraints or constraint groups represented as above. When
1129 ``ret=False``, returns nothing.
1131 :Example:
1133 >>> from picos import Problem, RealVariable
1134 >>> x = RealVariable("x", 5)
1135 >>> P = Problem()
1136 >>> P.require(x >= -1, x <= 1) # Add individual constraints.
1137 >>> P.require([x[i] <= x[i+1] for i in range(4)]) # Add groups.
1138 >>> print(P)
1139 Linear Feasibility Problem
1140 find an assignment
1141 for
1142 5×1 real variable x
1143 subject to
1144 x ≥ [-1]
1145 x ≤ [1]
1146 x[i] ≤ x[i+1] ∀ i ∈ [0…3]
1148 .. note::
1150 For a single constraint ``C``, ``P.require(C)`` may also be written
1151 as ``P += C``. For multiple constraints, ``P.require([C1, C2])`` can
1152 be abbreviated ``P += [C1, C2]`` while ``P.require(C1, C2)`` can be
1153 written as either ``P += (C1, C2)`` or just ``P += C1, C2``.
1154 """
1155 from ..constraints import Constraint
1157 added = []
1158 for constraint in constraints:
1159 if isinstance(constraint, Constraint):
1160 added.append(self.add_constraint(constraint))
1161 else:
1162 try:
1163 if not all(isinstance(c, Constraint) for c in constraint):
1164 raise TypeError
1165 except TypeError:
1166 raise TypeError(
1167 "An argument is neither a constraint nor an iterable "
1168 "yielding constraints.") from None
1169 else:
1170 added.append(self.add_list_of_constraints(constraint))
1172 if ret:
1173 return added[0] if len(added) == 1 else added
1175 def _con_group_index(self, conOrConID):
1176 """Support :meth:`remove_constraint`."""
1177 if isinstance(conOrConID, int):
1178 constraint = self._constraints[conOrConID]
1179 else:
1180 constraint = conOrConID
1182 for i, group in enumerate(self._con_groups):
1183 for j, candidate in enumerate(group):
1184 if candidate is constraint:
1185 return i, j
1187 if constraint in self._constraints.values():
1188 raise RuntimeError("The problem's constraint and constraint group "
1189 "registries are out of sync.")
1190 else:
1191 raise KeyError("The constraint is not part of the problem.")
1193 def remove_constraint(self, idOrIndOrCon):
1194 """Delete a constraint from the problem.
1196 :param idOrIndOrCon: See :meth:`get_constraint`.
1198 :Example:
1200 >>> import picos
1201 >>> from pprint import pprint
1202 >>> P = picos.Problem()
1203 >>> x = [picos.RealVariable('x[{0}]'.format(i), 2) for i in range(4)]
1204 >>> y = picos.RealVariable('y', 4)
1205 >>> Cxy = P.add_list_of_constraints(
1206 ... [x[i].sum <= y[i] for i in range(4)])
1207 >>> Cy = P.add_constraint(y >= 0)
1208 >>> Cx0to2 = P.add_list_of_constraints([x[i] <= 2 for i in range(3)])
1209 >>> Cx3 = P.add_constraint(x[3] <= 1)
1210 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1211 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1212 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>,
1213 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1214 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1215 <4×1 Affine Constraint: y ≥ 0>,
1216 <2×1 Affine Constraint: x[0] ≤ [2]>,
1217 <2×1 Affine Constraint: x[1] ≤ [2]>,
1218 <2×1 Affine Constraint: x[2] ≤ [2]>,
1219 <2×1 Affine Constraint: x[3] ≤ [1]>]
1220 >>> # Delete the 2nd constraint (counted from 0):
1221 >>> P.remove_constraint(1)
1222 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1223 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1224 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1225 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1226 <4×1 Affine Constraint: y ≥ 0>,
1227 <2×1 Affine Constraint: x[0] ≤ [2]>,
1228 <2×1 Affine Constraint: x[1] ≤ [2]>,
1229 <2×1 Affine Constraint: x[2] ≤ [2]>,
1230 <2×1 Affine Constraint: x[3] ≤ [1]>]
1231 >>> # Delete the 2nd group of constraints, i.e. the constraint y > 0:
1232 >>> P.remove_constraint((1,))
1233 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1234 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1235 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1236 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1237 <2×1 Affine Constraint: x[0] ≤ [2]>,
1238 <2×1 Affine Constraint: x[1] ≤ [2]>,
1239 <2×1 Affine Constraint: x[2] ≤ [2]>,
1240 <2×1 Affine Constraint: x[3] ≤ [1]>]
1241 >>> # Delete the 3rd remaining group of constraints, i.e. x[3] < [1]:
1242 >>> P.remove_constraint((2,))
1243 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1244 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1245 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1246 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1247 <2×1 Affine Constraint: x[0] ≤ [2]>,
1248 <2×1 Affine Constraint: x[1] ≤ [2]>,
1249 <2×1 Affine Constraint: x[2] ≤ [2]>]
1250 >>> # Delete 2nd constraint of the 2nd remaining group, i.e. x[1] < |2|:
1251 >>> P.remove_constraint((1,1))
1252 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1253 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1254 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1255 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1256 <2×1 Affine Constraint: x[0] ≤ [2]>,
1257 <2×1 Affine Constraint: x[2] ≤ [2]>]
1258 """
1259 idOrIds = self._lookup_constraint(idOrIndOrCon)
1261 removedCons = []
1263 if isinstance(idOrIds, list):
1264 assert idOrIds, "There is an empty constraint group."
1265 groupIndex, _ = self._con_group_index(idOrIds[0])
1266 self._con_groups.pop(groupIndex)
1267 for id in idOrIds:
1268 removedCons.append(self._constraints.pop(id))
1269 else:
1270 constraint = self._constraints.pop(idOrIds)
1271 removedCons.append(constraint)
1272 groupIndex, groupOffset = self._con_group_index(constraint)
1273 group = self._con_groups[groupIndex]
1274 group.pop(groupOffset)
1275 if not group:
1276 self._con_groups.pop(groupIndex)
1278 # Unregister the mutables added by the removed constraints.
1279 for con in removedCons:
1280 self._unregister_mutables(con.mutables)
1282 def remove_all_constraints(self):
1283 """Remove all constraints from the problem.
1285 .. note::
1287 This method does not remove bounds set directly on variables.
1288 """
1289 del self.constraints
1291 # --------------------------------------------------------------------------
1292 # Borderline legacy methods to deal with variables.
1293 # --------------------------------------------------------------------------
1295 _PARAMETERIZED_VARIABLE_REGEX = re.compile(r"^([^[]+)\[([^\]]+)\]$")
1297 def get_variable(self, name):
1298 """Retrieve variables referenced by the problem.
1300 Retrieves either a single variable with the given name or a group of
1301 variables all named ``name[param]`` with different values for ``param``.
1302 If the values for ``param`` are the integers from zero to the size of
1303 the group minus one, then the group is returned as a :obj:`list` ordered
1304 by ``param``, otherwise it is returned as a :obj:`dict` with the values
1305 of ``param`` as keys.
1307 .. note::
1309 Since PICOS 2.0, variables are independent of problems and only
1310 appear in a problem for as long as they are referenced by the
1311 problem's objective function or constraints.
1313 :param str name:
1314 The name of a variable, or the base name of a group of variables.
1316 :returns:
1317 A :class:`variable <picos.expressions.BaseVariable>` or a
1318 :class:`list` or :class:`dict` thereof.
1320 :Example:
1322 >>> from picos import Problem, RealVariable
1323 >>> from pprint import pprint
1324 >>> # Create a number of variables with structured names.
1325 >>> vars = [RealVariable("x")]
1326 >>> for i in range(4):
1327 ... vars.append(RealVariable("y[{}]".format(i)))
1328 >>> for key in ["alice", "bob", "carol"]:
1329 ... vars.append(RealVariable("z[{}]".format(key)))
1330 >>> # Make the variables appear in a problem.
1331 >>> P = Problem()
1332 >>> P.set_objective("min", sum([var for var in vars]))
1333 >>> print(P)
1334 Linear Program
1335 minimize x + y[0] + y[1] + y[2] + y[3] + z[alice] + z[bob] + z[carol]
1336 over
1337 1×1 real variables x, y[0], y[1], y[2], y[3], z[alice], z[bob],
1338 z[carol]
1339 >>> # Retrieve the variables from the problem.
1340 >>> P.get_variable("x")
1341 <1×1 Real Variable: x>
1342 >>> pprint(P.get_variable("y"))
1343 [<1×1 Real Variable: y[0]>,
1344 <1×1 Real Variable: y[1]>,
1345 <1×1 Real Variable: y[2]>,
1346 <1×1 Real Variable: y[3]>]
1347 >>> pprint(P.get_variable("z"))
1348 {'alice': <1×1 Real Variable: z[alice]>,
1349 'bob': <1×1 Real Variable: z[bob]>,
1350 'carol': <1×1 Real Variable: z[carol]>}
1351 >>> P.get_variable("z")["alice"] is P.get_variable("z[alice]")
1352 True
1353 """
1354 if name in self._variables:
1355 return self._variables[name]
1356 else:
1357 # Check if the name is really just a basename.
1358 params = []
1359 for otherName in sorted(self._variables.keys()):
1360 match = self._PARAMETERIZED_VARIABLE_REGEX.match(otherName)
1361 if not match:
1362 continue
1363 base, param = match.groups()
1364 if name == base:
1365 params.append(param)
1367 if params:
1368 # Return a list if the parameters are a range.
1369 try:
1370 intParams = sorted([int(p) for p in params])
1371 except ValueError:
1372 pass
1373 else:
1374 if intParams == list(range(len(intParams))):
1375 return [self._variables["{}[{}]".format(name, param)]
1376 for param in intParams]
1378 # Otherwise return a dict.
1379 return {param: self._variables["{}[{}]".format(name, param)]
1380 for param in params}
1381 else:
1382 raise KeyError("The problem references no variable or group of "
1383 "variables named '{}'.".format(name))
1385 def get_valued_variable(self, name):
1386 """Retrieve values of variables referenced by the problem.
1388 This method works the same :meth:`get_variable` but it returns the
1389 variable's :attr:`values <.valuable.Valuable.value>` instead of the
1390 variable objects.
1392 :raises ~picos.expressions.NotValued:
1393 If any of the selected variables is not valued.
1394 """
1395 exp = self.get_variable(name)
1396 if isinstance(exp, list):
1397 for i in range(len(exp)):
1398 exp[i] = exp[i].value
1399 elif isinstance(exp, dict):
1400 for i in exp:
1401 exp[i] = exp[i].value
1402 else:
1403 exp = exp.value
1404 return exp
1406 # --------------------------------------------------------------------------
1407 # Methods to create copies of the problem.
1408 # --------------------------------------------------------------------------
1410 def copy(self):
1411 """Create a deep copy of the problem, using new mutables."""
1412 the_copy = Problem(copyOptions=self._options)
1414 # Duplicate the mutables.
1415 new_mtbs = {mtb: mtb.copy() for name, mtb in self._mutables.items()}
1417 # Make copies of constraints on top of the new mutables.
1418 for group in self._con_groups:
1419 the_copy.add_list_of_constraints(
1420 constraint.replace_mutables(new_mtbs) for constraint in group)
1422 # Make a copy of the objective on top of the new mutables.
1423 direction, function = self._objective
1424 if function is not None:
1425 the_copy.objective = direction, function.replace_mutables(new_mtbs)
1427 return the_copy
1429 def continuous_relaxation(self, copy_other_mutables=True):
1430 """Return a continuous relaxation of the problem.
1432 This is done by replacing integer variables with continuous ones.
1434 :param bool copy_other_mutables:
1435 Whether variables that are already continuous as well as parameters
1436 should be copied. If this is :obj:`False`, then the relxation shares
1437 these mutables with the original problem.
1438 """
1439 the_copy = Problem(copyOptions=self._options)
1441 # Relax integral variables and copy other mutables if requested.
1442 new_mtbs = {}
1443 for name, var in self._mutables.items():
1444 if isinstance(var, expressions.IntegerVariable):
1445 new_mtbs[name] = expressions.RealVariable(
1446 name, var.shape, var._lower, var._upper)
1447 elif isinstance(var, expressions.BinaryVariable):
1448 new_mtbs[name] = expressions.RealVariable(name, var.shape, 0, 1)
1449 else:
1450 if copy_other_mutables:
1451 new_mtbs[name] = var.copy()
1452 else:
1453 new_mtbs[name] = var
1455 # Make copies of constraints on top of the new mutables.
1456 for group in self._con_groups:
1457 the_copy.add_list_of_constraints(
1458 constraint.replace_mutables(new_mtbs) for constraint in group)
1460 # Make a copy of the objective on top of the new mutables.
1461 direction, function = self._objective
1462 if function is not None:
1463 the_copy.objective = direction, function.replace_mutables(new_mtbs)
1465 return the_copy
1467 def clone(self, copyOptions=True):
1468 """Create a semi-deep copy of the problem.
1470 The copy is constrained by the same constraint objects and has the same
1471 objective function and thereby references the existing variables and
1472 parameters that appear in these objects.
1474 The clone can be modified to describe a new problem but when its
1475 variables and parameters are valued, in particular when a solution is
1476 applied to the new problem, then the same values are found in the
1477 corresponding variables and parameters of the old problem. If this is
1478 not a problem to you, then cloning can be much faster than copying.
1480 :param bool copyOptions:
1481 Whether to make an independent copy of the problem's options.
1482 Disabling this will apply any option changes to the original problem
1483 as well but yields a (very small) reduction in cloning time.
1484 """
1485 # Start with a shallow copy of self.
1486 # TODO: Consider adding Problem.__new__ to speed this up further.
1487 theClone = pycopy.copy(self)
1489 # Make the constraint registry independent.
1490 theClone._constraints = self._constraints.copy()
1491 theClone._con_groups = []
1492 for group in self._con_groups:
1493 theClone._con_groups.append(pycopy.copy(group))
1495 # Make the mutable registry independent.
1496 theClone._mtb_count = self._mtb_count.copy()
1497 theClone._mutables = self._mutables.copy()
1498 theClone._variables = self._variables.copy()
1499 theClone._parameters = self._parameters.copy()
1501 # Reset the clone's solution strategy and last solution.
1502 theClone._strategy = None
1504 # Make the solver options independent, if requested.
1505 if copyOptions:
1506 theClone._options = self._options.copy()
1508 # NOTE: No need to change the following attributes:
1509 # - objective: Is immutable as a tuple.
1510 # - _last_solution: Remains as valid as it is.
1512 return theClone
1514 # --------------------------------------------------------------------------
1515 # Methods to solve or export the problem.
1516 # --------------------------------------------------------------------------
1518 def prepared(self, steps=None, **extra_options):
1519 """Perform a dry-run returning the reformulated (prepared) problem.
1521 This behaves like :meth:`solve` in that it takes a number of additional
1522 temporary options, finds a solution strategy matching the problem and
1523 options, and performs the strategy's reformulations in turn to obtain
1524 modified problems. However, it stops after the given number of steps and
1525 never hands the reformulated problem to a solver. Instead of a solution,
1526 :meth:`prepared` then returns the last reformulated problem.
1528 Unless this method returns the problem itself, the special attributes
1529 ``prepared_strategy`` and ``prepared_steps`` are added to the returned
1530 problem. They then contain the (partially) executed solution strategy
1531 and the number of performed reformulations, respectively.
1533 :param int steps:
1534 Number of reformulations to perform. :obj:`None` means as many as
1535 there are. If this parameter is :math:`0`, then the problem itself
1536 is returned. If it is :math:`1`, then only the implicit first
1537 reformulation :class:`~.reform_options.ExtraOptions` is executed,
1538 which may also output the problem itself, depending on
1539 ``extra_options``.
1541 :param extra_options:
1542 Additional solver options to use with this dry-run only.
1544 :returns:
1545 The reformulated problem, with ``extra_options`` set unless they
1546 were "consumed" by a reformulation (e.g.
1547 :ref:`option_dualize <option_dualize>`).
1549 :raises ~picos.modeling.strategy.NoStrategyFound:
1550 If no solution strategy was found.
1552 :raises ValueError:
1553 If there are not as many reformulation steps as requested.
1555 :Example:
1557 >>> from picos import Problem, RealVariable
1558 >>> x = RealVariable("x", 2)
1559 >>> P = Problem()
1560 >>> P.set_objective("min", abs(x)**2)
1561 >>> Q = P.prepared(solver = "cvxopt")
1562 >>> print(Q.prepared_strategy) # Show prepared reformulation steps.
1563 1. ExtraOptions
1564 2. EpigraphReformulation
1565 3. SquaredNormToConicReformulation
1566 4. CVXOPTSolver
1567 >>> Q.prepared_steps # Check how many steps have been performed.
1568 3
1569 >>> print(P)
1570 Quadratic Program
1571 minimize ‖x‖²
1572 over
1573 2×1 real variable x
1574 >>> print(Q)# doctest: +ELLIPSIS
1575 Second Order Cone Program
1576 minimize __..._t
1577 over
1578 1×1 real variable __..._t
1579 2×1 real variable x
1580 subject to
1581 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0
1582 """
1583 from .strategy import Strategy
1585 # Produce a strategy for the clone.
1586 strategy = Strategy.from_problem(self, **extra_options)
1587 numReforms = len(strategy.reforms)
1589 if steps is None:
1590 steps = numReforms
1592 if steps == 0:
1593 return self
1594 elif steps > numReforms:
1595 raise ValueError("The pipeline {} has only {} reformulation steps "
1596 "to choose from.".format(strategy, numReforms))
1598 # Replace the successor of the last reformulation with a dummy solver.
1599 lastReform = strategy.reforms[steps - 1]
1600 oldSuccessor = lastReform.successor
1601 lastReform.successor = type("DummySolver", (), {
1602 "execute": lambda self: Solution(
1603 {}, solver="dummy", vectorizedPrimals=True)})()
1605 # Execute the cut-short strategy.
1606 strategy.execute(**extra_options)
1608 # Repair the last reformulation.
1609 lastReform.successor = oldSuccessor
1611 # Retrieve and augment the output problem (unless it's self).
1612 output = lastReform.output
1613 if output is not self:
1614 output.prepared_strategy = strategy
1615 output.prepared_steps = steps
1617 return output
1619 def reformulated(self, specification, **extra_options):
1620 r"""Return the problem reformulated to match a specification.
1622 Internally this creates a dummy solver accepting problems of the desired
1623 form and then calls :meth:`prepared` with the dummy solver passed via
1624 :ref:`option_ad_hoc_solver <option_ad_hoc_solver>`. See meth:`prepared`
1625 for more details.
1627 :param specification:
1628 A problem class that the resulting problem must be a member of.
1629 :type specification:
1630 ~picos.modeling.Specification
1632 :param extra_options:
1633 Additional solver options to use with this reformulation only.
1635 :returns:
1636 The reformulated problem, with ``extra_options`` set unless they
1637 were "consumed" by a reformulation (e.g.
1638 :ref:`dualize <option_dualize>`).
1640 :raises ~picos.modeling.strategy.NoStrategyFound:
1641 If no reformulation strategy was found.
1643 :Example:
1645 >>> from picos import Problem, RealVariable
1646 >>> from picos.modeling import Specification
1647 >>> from picos.expressions import AffineExpression
1648 >>> from picos.constraints import (
1649 ... AffineConstraint, SOCConstraint, RSOCConstraint)
1650 >>> # Define the class/specification of second order conic problems:
1651 >>> S = Specification(objectives=[AffineExpression],
1652 ... constraints=[AffineConstraint, SOCConstraint, RSOCConstraint])
1653 >>> # Define a quadratic program and reformulate it:
1654 >>> x = RealVariable("x", 2)
1655 >>> P = Problem()
1656 >>> P.set_objective("min", abs(x)**2)
1657 >>> Q = P.reformulated(S)
1658 >>> print(P)
1659 Quadratic Program
1660 minimize ‖x‖²
1661 over
1662 2×1 real variable x
1663 >>> print(Q)# doctest: +ELLIPSIS
1664 Second Order Cone Program
1665 minimize __..._t
1666 over
1667 1×1 real variable __..._t
1668 2×1 real variable x
1669 subject to
1670 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0
1672 .. note::
1674 This method is intended for educational purposes.
1675 You do not need to use it when solving a problem as PICOS will
1676 perform the necessary reformulations automatically.
1677 """
1678 if not isinstance(specification, Specification):
1679 raise TypeError("The desired problem type must be given as a "
1680 "Specification object.")
1682 # Create a placeholder function for abstract methods of a dummy solver.
1683 def placeholder(the_self):
1684 raise RuntimeError("The dummy solver created by "
1685 "Problem.reformulated must not be executed.")
1687 # Declare a dummy solver that accepts specified problems.
1688 DummySolver = type("DummySolver", (Solver,), {
1689 # Abstract class methods.
1690 "supports": classmethod(lambda cls, footprint:
1691 Solver.supports(footprint) and footprint in specification),
1692 "default_penalty": classmethod(lambda cls: 0),
1693 "test_availability": classmethod(lambda cls: None),
1694 "names": classmethod(lambda cls: ("Dummy Solver", "DummySolver",
1695 "Dummy Solver accepting {}".format(specification), None)),
1696 "is_free": classmethod(lambda cls: True),
1698 # Additional class methods needed for an ad-hoc solver.
1699 "penalty": classmethod(lambda cls, options: 0),
1701 # Abstract instance methods.
1702 "reset_problem": lambda self: placeholder(self),
1703 "_import_problem": lambda self: placeholder(self),
1704 "_update_problem": lambda self: placeholder(self),
1705 "_solve": lambda self: placeholder(self)
1706 })
1708 # Ad-hoc the dummy solver and prepare the problem for it.
1709 oldAdHocSolver = self.options.ad_hoc_solver
1710 extra_options["ad_hoc_solver"] = DummySolver
1711 problem = self.prepared(**extra_options)
1713 # Restore the ad_hoc_solver option of the original problem.
1714 problem.options.ad_hoc_solver = oldAdHocSolver
1716 return problem
1718 def solve(self, **extra_options):
1719 """Hand the problem to a solver.
1721 You can select the solver manually with the ``solver`` option. Otherwise
1722 a suitable solver will be selected among those that are available on the
1723 platform.
1725 The default behavior (options ``primals=True``, ``duals=None``) is to
1726 raise a :exc:`~picos.SolutionFailure` when the primal solution is not
1727 found optimal by the solver, while the dual solution is allowed to be
1728 missing or incomplete.
1730 When this method succeeds and unless ``apply_solution=False``, you can
1731 access the solution as follows:
1733 - The problem's :attr:`value` denotes the objective function value.
1734 - The variables' :attr:`~.valuable.Valuable.value` is set according
1735 to the primal solution. You can in fact query the value of any
1736 expression involving valued variables like this.
1737 - The constraints' :attr:`~.constraint.Constraint.dual` is set
1738 according to the dual solution.
1739 - The value of any parameter involved in the problem may have
1740 changed, depending on the parameter.
1742 :param extra_options:
1743 A sequence of additional solver options to use with this solution
1744 search only. In particular, this lets you
1746 - select a solver via the ``solver`` option,
1747 - obtain non-optimal primal solutions by setting ``primals=None``,
1748 - require a complete and optimal dual solution with ``duals=True``,
1749 and
1750 - skip valuing variables or constraints with
1751 ``apply_solution=False``.
1753 :returns ~picos.Solution or list(~picos.Solution):
1754 A solution object or list thereof.
1756 :raises ~picos.SolutionFailure:
1757 In the following cases:
1759 1. No solution strategy was found.
1760 2. Multiple solutions were requested but none were returned.
1761 3. A primal solution was explicitly requested (``primals=True``) but
1762 the primal solution is missing/incomplete or not claimed optimal.
1763 4. A dual solution was explicitly requested (``duals=True``) but
1764 the dual solution is missing/incomplete or not claimed optimal.
1766 The case number is stored in the ``code`` attribute of the
1767 exception.
1768 """
1769 from .strategy import NoStrategyFound, Strategy
1771 startTime = time.time()
1773 extra_options = map_legacy_options(**extra_options)
1774 options = self.options.self_or_updated(**extra_options)
1775 verbose = options.verbosity > 0
1777 with picos_box(show=verbose):
1778 if verbose:
1779 print("Problem type: {}.".format(self.type))
1781 # Reset an outdated strategy.
1782 if self._strategy and not self._strategy.valid(**extra_options):
1783 if verbose:
1784 print("Strategy outdated:\n{}.".format(self._strategy))
1786 self._strategy = None
1788 # Find a new solution strategy, if necessary.
1789 if not self._strategy:
1790 if verbose:
1791 if options.ad_hoc_solver:
1792 solverName = options.ad_hoc_solver.get_via_name()
1793 elif options.solver:
1794 solverName = get_solver(options.solver).get_via_name()
1795 else:
1796 solverName = None
1798 print("Searching a solution strategy{}.".format(
1799 " for {}".format(solverName) if solverName else ""))
1801 try:
1802 self._strategy = Strategy.from_problem(
1803 self, **extra_options)
1804 except NoStrategyFound as error:
1805 s = str(error)
1807 if verbose:
1808 print(s, flush=True)
1810 raise SolutionFailure(1, "No solution strategy found.") \
1811 from error
1813 if verbose:
1814 print("Solution strategy:\n {}".format(
1815 "\n ".join(str(self._strategy).splitlines())))
1816 else:
1817 if verbose:
1818 print("Reusing strategy:\n {}".format(
1819 "\n ".join(str(self._strategy).splitlines())))
1821 # Execute the strategy to obtain one or more solutions.
1822 solutions = self._strategy.execute(**extra_options)
1824 # Report how many solutions were obtained, select the first.
1825 if isinstance(solutions, list):
1826 assert all(isinstance(s, Solution) for s in solutions)
1828 if not solutions:
1829 raise SolutionFailure(
1830 2, "The solver returned an empty list of solutions.")
1832 solution = solutions[0]
1834 if verbose:
1835 print("Selecting the first of {} solutions obtained for "
1836 "processing.".format(len(solutions)))
1837 else:
1838 assert isinstance(solutions, Solution)
1839 solution = solutions
1841 # Report claimed solution state.
1842 if verbose:
1843 print("Solver claims {} solution for {} problem.".format(
1844 solution.claimedStatus, solution.problemStatus))
1846 # Validate the primal solution.
1847 if options.primals:
1848 vars_ = self._variables.values()
1849 if solution.primalStatus != SS_OPTIMAL:
1850 raise SolutionFailure(3, "Primal solution state claimed {} "
1851 "but optimality is required (primals=True)."
1852 .format(solution.primalStatus))
1853 elif None in solution.primals.values() \
1854 or any(var not in solution.primals for var in vars_):
1855 raise SolutionFailure(3, "The primal solution is incomplete"
1856 " but full primals are required (primals=True).")
1858 # Validate the dual solution.
1859 if options.duals:
1860 cons = self._constraints.values()
1861 if solution.dualStatus != SS_OPTIMAL:
1862 raise SolutionFailure(4, "Dual solution state claimed {} "
1863 "but optimality is required (duals=True).".format(
1864 solution.dualStatus))
1865 elif None in solution.duals.values() \
1866 or any(con not in solution.duals for con in cons):
1867 raise SolutionFailure(4, "The dual solution is incomplete "
1868 "but full duals are required (duals=True).")
1870 if options.apply_solution:
1871 if verbose:
1872 print("Applying the solution.")
1874 # Apply the (first) solution.
1875 solution.apply(snapshotStatus=True)
1877 # Store all solutions produced by the solver.
1878 self._last_solution = solutions
1880 # Report verified solution state.
1881 if verbose:
1882 print("Applied solution is {}.".format(solution.lastStatus))
1884 endTime = time.time()
1885 solveTime = endTime - startTime
1886 searchTime = solution.searchTime
1888 if searchTime:
1889 overhead = (solveTime - searchTime) / searchTime
1890 else:
1891 overhead = float("inf")
1893 if verbose:
1894 print("Search {:.1e}s, solve {:.1e}s, overhead {:.0%}."
1895 .format(searchTime, solveTime, overhead))
1897 if settings.RETURN_SOLUTION:
1898 return solutions
1900 def write_to_file(self, filename, writer="picos"):
1901 """See :func:`picos.modeling.file_out.write`."""
1902 write(self, filename, writer)
1904 # --------------------------------------------------------------------------
1905 # Methods to query the problem.
1906 # TODO: Document removal of is_complex, is_real (also for constraints).
1907 # TODO: Revisit #14: "Interfaces to get primal/dual objective values and
1908 # primal/dual feasiblity (amount of violation).""
1909 # --------------------------------------------------------------------------
1911 def check_current_value_feasibility(self, tol=1e-5, inttol=None):
1912 """Check if the problem is feasibly valued.
1914 Checks whether all variables that appear in constraints are valued and
1915 satisfy both their bounds and the constraints up to the given tolerance.
1917 :param float tol:
1918 Largest tolerated absolute violation of a constraint or variable
1919 bound. If ``None``, then the ``abs_prim_fsb_tol`` solver option is
1920 used.
1922 :param inttol:
1923 DEPRECATED
1925 :returns:
1926 A tuple ``(feasible, violation)`` where ``feasible`` is a bool
1927 stating whether the solution is feasible and ``violation`` is either
1928 ``None``, if ``feasible == True``, or the amount of violation,
1929 otherwise.
1931 :raises picos.uncertain.IntractableWorstCase:
1932 When computing the worst-case (expected) value of the constrained
1933 expression is not supported.
1934 """
1935 if inttol is not None:
1936 throw_deprecation_warning("Variable integrality is now ensured on "
1937 "assignment of a value, so it does not need to be checked via "
1938 "check_current_value_feasibility's old 'inttol' parameter.")
1940 if tol is None:
1941 tol = self._options.abs_prim_fsb_tol
1943 all_cons = list(self._constraints.values())
1944 all_cons += [
1945 variable.bound_constraint for variable in self._variables.values()
1946 if variable.bound_constraint]
1948 largest_violation = 0.0
1950 for constraint in all_cons:
1951 try:
1952 slack = constraint.slack
1953 except IntractableWorstCase as error:
1954 raise IntractableWorstCase("Failed to check worst-case or "
1955 "expected feasibility of {}: {}".format(constraint, error))\
1956 from None
1958 assert isinstance(slack, (float, cvx.matrix, cvx.spmatrix))
1959 if isinstance(slack, (float, cvx.spmatrix)):
1960 slack = cvx.matrix(slack) # Allow min, max.
1962 # HACK: The following works around the fact that the slack of an
1963 # uncertain conic constraint is returned as a vector, even
1964 # when the cone is that of the positive semidefinite matrices,
1965 # in which case the vectorization used is nontrivial (svec).
1966 # FIXME: A similar issue should arise when a linear matrix
1967 # inequality is integrated in a product cone; The product
1968 # cone's slack can then have negative entries but still be
1969 # feasible and declared infeasible here.
1970 # TODO: Add a "violation" interface to Constraint that replaces all
1971 # the logic below.
1972 from ..expressions import Constant, PositiveSemidefiniteCone
1973 if isinstance(constraint,
1974 constraints.uncertain.ScenarioUncertainConicConstraint) \
1975 and isinstance(constraint.cone, PositiveSemidefiniteCone):
1976 hack = True
1977 slack = Constant(slack).desvec.safe_value
1978 else:
1979 hack = False
1981 psd_constraints = (
1982 constraints.LMIConstraint,
1983 constraints.OpRelEntropyConstraint,
1984 constraints.MatrixGeoMeanEpiConstraint,
1985 constraints.MatrixGeoMeanHypoConstraint,
1986 )
1988 if isinstance(constraint, psd_constraints) or hack:
1989 # Check hermitian-ness of slack.
1990 violation = float(max(abs(slack - slack.H)))
1991 if violation > tol:
1992 largest_violation = max(largest_violation, violation)
1994 # Check positive semidefiniteness of slack.
1995 violation = -float(min(np.linalg.eigvalsh(cvx2np(slack))))
1996 if violation > tol:
1997 largest_violation = max(largest_violation, violation)
1998 else:
1999 violation = -float(min(slack))
2000 if violation > tol:
2001 largest_violation = max(largest_violation, violation)
2003 return (not largest_violation, largest_violation)
2005 # --------------------------------------------------------------------------
2006 # Abstract method implementations for the Valuable base class.
2007 # --------------------------------------------------------------------------
2009 def _get_valuable_string(self):
2010 return "problem with {}".format(self._objective._get_valuable_string())
2012 def _get_value(self):
2013 return self._objective._get_value()
2015 # --------------------------------------------------------------------------
2016 # Legacy methods and properties.
2017 # --------------------------------------------------------------------------
2019 _LEGACY_PROPERTY_REASON = "Still used internally by legacy code; will be " \
2020 "removed together with that code."
2022 @property
2023 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2024 def countVar(self):
2025 """The same as :func:`len` applied to :attr:`variables`."""
2026 return len(self._variables)
2028 @property
2029 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2030 def countCons(self):
2031 """The same as :func:`len` applied to :attr:`constraints`."""
2032 return len(self._variables)
2034 @property
2035 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2036 def numberOfVars(self):
2037 """The sum of the dimensions of all referenced variables."""
2038 return sum(variable.dim for variable in self._variables.values())
2040 @property
2041 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2042 def numberLSEConstraints(self):
2043 """Number of :class:`~picos.constraints.LogSumExpConstraint` stored."""
2044 return len([c for c in self._constraints.values()
2045 if isinstance(c, constraints.LogSumExpConstraint)])
2047 @property
2048 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2049 def numberSDPConstraints(self):
2050 """Number of :class:`~picos.constraints.LMIConstraint` stored."""
2051 return len([c for c in self._constraints.values()
2052 if isinstance(c, constraints.LMIConstraint)])
2054 @property
2055 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2056 def numberQuadConstraints(self):
2057 """Number of quadratic constraints stored."""
2058 return len([c for c in self._constraints.values() if isinstance(c, (
2059 constraints.ConvexQuadraticConstraint,
2060 constraints.ConicQuadraticConstraint,
2061 constraints.NonconvexQuadraticConstraint))])
2063 @property
2064 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2065 def numberConeConstraints(self):
2066 """Number of quadratic conic constraints stored."""
2067 return len([c for c in self._constraints.values() if isinstance(
2068 c, (constraints.SOCConstraint, constraints.RSOCConstraint))])
2070 @deprecated("2.0", useInstead="value")
2071 def obj_value(self):
2072 """Objective function value.
2074 :raises AttributeError:
2075 If the problem is a feasibility problem or if the objective function
2076 is not valued. This is legacy behavior. Note that :attr:`value` just
2077 returns :obj:`None` while functions that **do** raise an exception
2078 to denote an unvalued expression would raise
2079 :exc:`~picos.expressions.NotValued` instead.
2080 """
2081 if self._objective.feasibility:
2082 raise AttributeError(
2083 "A feasibility problem has no objective value.")
2085 value = self.value
2087 if self.value is None:
2088 raise AttributeError("The objective {} is not fully valued."
2089 .format(self._objective.function.string))
2090 else:
2091 return value
2093 @deprecated("2.0", useInstead="continuous")
2094 def is_continuous(self):
2095 """Whether all variables are of continuous types."""
2096 return self.continuous
2098 @deprecated("2.0", useInstead="pure_integer")
2099 def is_pure_integer(self):
2100 """Whether all variables are of integral types."""
2101 return self.pure_integer
2103 @deprecated("2.0", useInstead="Problem.options")
2104 def set_all_options_to_default(self):
2105 """Set all solver options to their default value."""
2106 self._options.reset()
2108 @deprecated("2.0", useInstead="Problem.options")
2109 def set_option(self, key, val):
2110 """Set a single solver option to the given value.
2112 :param str key: String name of the option, see below for a list.
2113 :param val: New value for the option.
2114 """
2115 key, val = map_legacy_options({key: val}).popitem()
2116 self._options[key] = val
2118 @deprecated("2.0", useInstead="Problem.options")
2119 def update_options(self, **options):
2120 """Set multiple solver options at once.
2122 :param options: A parameter sequence of options to set.
2123 """
2124 options = map_legacy_options(**options)
2125 for key, val in options.items():
2126 self._options[key] = val
2128 @deprecated("2.0", useInstead="Problem.options")
2129 def verbosity(self):
2130 """Return the problem's current verbosity level."""
2131 return self._options.verbosity
2133 @deprecated("2.0", reason="Variables can now be created independent of "
2134 "problems, and do not need to be added to any problem explicitly.")
2135 def add_variable(
2136 self, name, size=1, vtype='continuous', lower=None, upper=None):
2137 r"""Legacy method to create a PICOS variable.
2139 :param str name: The name of the variable.
2141 :param size:
2142 The shape of the variable.
2143 :type size:
2144 anything recognized by :func:`~picos.expressions.data.load_shape`
2146 :param str vtype:
2147 Domain of the variable. Can be any of
2149 - ``'continuous'`` -- real valued,
2150 - ``'binary'`` -- either zero or one,
2151 - ``'integer'`` -- integer valued,
2152 - ``'symmetric'`` -- symmetric matrix,
2153 - ``'antisym'`` or ``'skewsym'`` -- skew-symmetric matrix,
2154 - ``'complex'`` -- complex matrix,
2155 - ``'hermitian'`` -- complex hermitian matrix.
2157 :param lower:
2158 A lower bound on the variable.
2159 :type lower:
2160 anything recognized by :func:`~picos.expressions.data.load_data`
2162 :param upper:
2163 An upper bound on the variable.
2164 :type upper:
2165 anything recognized by :func:`~picos.expressions.data.load_data`
2167 :returns:
2168 A :class:`~picos.expressions.BaseVariable` instance.
2170 :Example:
2172 >>> from picos import Problem, RealVariable
2173 >>> P = Problem()
2174 >>> x = RealVariable("x", 3)
2175 >>> x
2176 <3×1 Real Variable: x>
2177 >>> # Variables exist independently of problems:
2178 >>> P.variables
2179 mappingproxy(OrderedDict())
2180 >>> # They are only part of the problem if they actually appear:
2181 >>> P.set_objective("min", abs(x)**2)
2182 >>> P.variables
2183 mappingproxy(OrderedDict({'x': <3×1 Real Variable: x>}))
2184 """
2185 if vtype == "continuous":
2186 return expressions.RealVariable(name, size, lower, upper)
2187 elif vtype == "binary":
2188 return expressions.BinaryVariable(name, size)
2189 elif vtype == "integer":
2190 return expressions.IntegerVariable(name, size, lower, upper)
2191 elif vtype == "symmetric":
2192 return expressions.SymmetricVariable(name, size, lower, upper)
2193 elif vtype in ("antisym", "skewsym"):
2194 return expressions.SkewSymmetricVariable(name, size, lower, upper)
2195 elif vtype == "complex":
2196 return expressions.ComplexVariable(name, size)
2197 elif vtype == "hermitian":
2198 return expressions.HermitianVariable(name, size)
2199 elif vtype in ("semiint", "semicont"):
2200 raise NotImplementedError("Variables with legacy types 'semiint' "
2201 "and 'semicont' are not supported anymore as of PICOS 2.0. "
2202 "If you need this functionality back, please open an issue.")
2203 else:
2204 raise ValueError("Unknown legacy variable type '{}'.".format(vtype))
2206 @deprecated("2.0", reason="Whether a problem references a variable is now"
2207 " determined dynamically, so this method has no effect.")
2208 def remove_variable(self, name):
2209 """Does nothing."""
2210 pass
2212 @deprecated("2.0", useInstead="variables")
2213 def set_var_value(self, name, value):
2214 """Set the :attr:`~.valuable.Valuable.value` of a variable.
2216 For a :class:`Problem` ``P``, this is the same as
2217 ``P.variables[name] = value``.
2219 :param str name:
2220 Name of the variable to be valued.
2222 :param value:
2223 The value to be set.
2224 :type value:
2225 anything recognized by :func:`~picos.expressions.data.load_data`
2226 """
2227 try:
2228 variable = self._variables[name]
2229 except KeyError:
2230 raise KeyError("The problem references no variable named '{}'."
2231 .format(name)) from None
2232 else:
2233 variable.value = value
2235 @deprecated("2.0", useInstead="dual")
2236 def as_dual(self):
2237 """Return the Lagrangian dual problem of the standardized problem."""
2238 return self.dual
2241# --------------------------------------
2242__all__ = api_end(_API_START, globals())