Coverage for picos/modeling/problem.py : 76.74%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# ------------------------------------------------------------------------------
2# Copyright (C) 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
34from ..apidoc import api_end, api_start
35from ..caching import cached_property
36from ..expressions.data import cvx2np
37from ..expressions.uncertain import IntractableWorstCase, UncertainExpression
38from ..expressions.variables import BaseVariable
39from ..formatting import natsorted, parameterized_string, picos_box
40from ..legacy import deprecated, map_legacy_options, throw_deprecation_warning
41from ..solvers import Solver, get_solver
42from .file_out import write
43from .footprint import Footprint, Specification
44from .options import Options
45from .solution import SS_OPTIMAL, Solution
47_API_START = api_start(globals())
48# -------------------------------
51class SolutionFailure(RuntimeError):
52 """Solving the problem failed."""
54 def __init__(self, code, message):
55 """Construct a :exc:`SolutionFailure`.
57 :param int code:
58 Status code, as defined in :meth:`Problem.solve`.
60 :param str message:
61 Text description of the failure.
62 """
63 #: Status code, as defined in :meth:`Problem.solve`.
64 self.code = code
66 #: Text description of the failure.
67 self.message = message
69 def __str__(self):
70 return "Code {}: {}".format(self.code, self.message)
73class Objective:
74 """An optimization objective composed of search direction and function."""
76 #: Short string denoting a feasibility problem.
77 FIND = "find"
79 #: Short string denoting a minimization problem.
80 MIN = "min"
82 #: Short string denoting a maximization problem.
83 MAX = "max"
85 def __init__(self, direction=None, function=None):
86 """Construct an optimization objective.
88 :param str direction:
89 Case insensitive search direction string. One of
91 - ``"min"`` or ``"minimize"``,
92 - ``"max"`` or ``"maximize"``,
93 - ``"find"`` or :obj:`None` (for a feasibility problem).
95 :param ~picos.expressions.Expression function:
96 The objective function. Must be :obj:`None` for a feasibility
97 problem.
98 """
99 if direction is None:
100 direction = self.FIND
101 else:
102 if not isinstance(direction, str):
103 raise TypeError("Search direction must be given as a string.")
105 lower = direction.lower()
106 if lower == "find":
107 direction = self.FIND
108 elif lower.startswith("min"):
109 direction = self.MIN
110 elif lower.startswith("max"):
111 direction = self.MAX
112 else:
113 raise ValueError(
114 "Invalid search direction '{}'.".format(direction))
116 if function is None:
117 if direction != self.FIND:
118 raise ValueError("Missing an objective function.")
119 else:
120 if direction == self.FIND:
121 raise ValueError("May not specify an objective function for a "
122 "feasiblity problem.")
124 if not isinstance(function, expressions.Expression):
125 raise TypeError(
126 "Objective function must be a PICOS expression.")
128 if len(function) != 1:
129 raise TypeError("Objective function must be scalar.")
131 function = function.refined
133 if isinstance(function, expressions.ComplexAffineExpression) \
134 and function.complex:
135 raise TypeError("Objective function may not be complex.")
137 self._direction = direction
138 self._function = function
140 def __str__(self):
141 if self._function is None:
142 return "find an assignment"
143 else:
144 minimize = self._direction == self.MIN
145 dir_str = "minimize" if minimize else "maximize"
147 if self._function.uncertain:
148 obj_str = self._function.worst_case_string(
149 "max" if minimize else "min")
150 else:
151 obj_str = self._function.string
153 return "{} {}".format(dir_str, obj_str)
155 def __repr__(self):
156 return glyphs.repr1("Objective: {}".format(self))
158 def __iter__(self):
159 yield self._direction
160 yield self._function
162 def __eq__(self, other):
163 if not isinstance(other, Objective):
164 return False
166 if self._direction != other._direction:
167 return False
169 if self._direction == self.FIND:
170 return True
172 try:
173 return self._function.equals(other._function)
174 except AttributeError:
175 # TODO: Allow all expressions to be equality-checked?
176 return self._function is other._function
178 @property
179 def feasibility(self):
180 """Whether the objective is "find an assignment"."""
181 return self._function is None
183 @property
184 def pair(self):
185 """Search direction and objective function as a pair."""
186 return self._direction, self._objective
188 @property
189 def direction(self):
190 """Search direction as a short string."""
191 return self._direction
193 @property
194 def function(self):
195 """Objective function."""
196 return self._function
198 @cached_property
199 def normalized(self):
200 """The objective but with feasiblity posed as "minimize 0".
202 >>> from picos import Objective
203 >>> obj = Objective(); obj
204 <Objective: find an assignment>
205 >>> obj.normalized
206 <Objective: minimize 0>
207 """
208 if self._function is None:
209 return Objective(self.MIN, expressions.AffineExpression.zero())
210 else:
211 return self
213 @property
214 def value(self):
215 """Value of the objective function.
217 In the case of an uncertain objective, this is the worst-case (expected)
218 objective value.
220 :raises picos.uncertain.IntractableWorstCase:
221 When computing the worst-case (expected) value of an uncertain
222 objective is not supported.
223 """
224 if self._function is None:
225 return None
226 elif isinstance(self._function, UncertainExpression):
227 if self._direction == self.MIN:
228 bad_direction = self.MAX
229 elif self._direction == self.MAX:
230 bad_direction = self.MIN
231 else:
232 bad_direction = self.FIND
234 try:
235 return self._function.worst_case_value(bad_direction)
236 except IntractableWorstCase as error:
237 raise IntractableWorstCase("Failed to compute the worst-case "
238 "value of the objective function {}: {} Maybe evaluate the "
239 "nominal objective function instead?"
240 .format(self._function.string, error)) from None
241 else:
242 return self._function.value
244 def __index__(self):
245 if self._function is None:
246 raise TypeError("A feasiblity objective cannot be used as an index "
247 "because there is no objective function to take the value of.")
249 value = self.value
251 if value is None:
252 raise expressions.NotValued(
253 "Cannot use unvalued objective function {} as an index."
254 .format(self._function.string))
256 assert isinstance(value, (float, int))
258 if not value.is_integer():
259 raise ValueError(
260 "Cannot use the objective function {} as an index as its value "
261 "of {} is not integral.".format(self._function.string, value))
263 return int(value)
265 def _casting_helper(self, theType):
266 assert theType in (int, float, complex)
268 if self._function is None:
269 raise TypeError("A feasiblity objective cannot be cast as {} "
270 "because there is no objective function to take the value of."
271 .format(theType.__name__))
273 value = self.value
275 if value is None:
276 raise expressions.NotValued(
277 "Cannot cast unvalued objective function {} as {}."
278 .format(self._function.string, theType.__name__))
280 return theType(value)
282 def __int__(self):
283 return self._casting_helper(int)
285 def __float__(self):
286 return self._casting_helper(float)
288 def __complex__(self):
289 return self._casting_helper(complex)
291 def __round__(self, ndigits=None):
292 return round(float(self), ndigits)
295class Problem():
296 """PICOS' representation of an optimization problem.
298 :Example:
300 >>> from picos import Problem, RealVariable
301 >>> X = RealVariable("X", (2,2), lower = 0)
302 >>> P = Problem()
303 >>> P.set_objective("max", X.tr)
304 >>> C1 = P.add_constraint(X.sum <= 10)
305 >>> C2 = P.add_constraint(X[0,0] == 1)
306 >>> print(P)
307 Linear Program
308 maximize tr(X)
309 over
310 2×2 real variable X (bounded below)
311 subject to
312 ∑(X) ≤ 10
313 X[0,0] = 1
314 >>> # PICOS will select a suitable solver if you don't specify one.
315 >>> solution = P.solve(solver = "cvxopt")
316 >>> solution.claimedStatus
317 'optimal'
318 >>> solution.searchTime #doctest: +SKIP
319 0.002137422561645508
320 >>> round(P, 1)
321 10.0
322 >>> print(X) #doctest: +SKIP
323 [ 1.00e+00 4.89e-10]
324 [ 4.89e-10 9.00e+00]
325 >>> round(C1.dual, 1)
326 1.0
327 """
329 #: The specification for problems returned by :meth:`conic_form`.
330 CONIC_FORM = Specification(
331 objectives=[expressions.AffineExpression],
332 constraints=[C for C in
333 (getattr(constraints, Cname) for Cname in constraints.__all__)
334 if issubclass(C, constraints.ConicConstraint)
335 and C is not constraints.ConicConstraint])
337 # --------------------------------------------------------------------------
338 # Initialization and reset methods.
339 # --------------------------------------------------------------------------
341 def __init__(self, copyOptions=None, useOptions=None, **extra_options):
342 """Create an empty problem and optionally set initial solver options.
344 :param copyOptions:
345 An :class:`Options <picos.Options>` object to copy instead of using
346 the default options.
348 :param useOptions: An :class:`Options <picos.Options>` object to use
349 (without making a copy) instead of using the default options.
351 :param extra_options:
352 A sequence of additional solver options to apply on top of the
353 default options or those given by ``copyOptions`` or ``useOptions``.
354 """
355 if copyOptions and useOptions:
356 raise ValueError(
357 "Can only copy or use existing solver options, not both.")
359 extra_options = map_legacy_options(**extra_options)
361 if copyOptions:
362 self._options = copyOptions.copy()
363 self._options.update(**extra_options)
364 elif useOptions:
365 self._options = useOptions
366 self._options.update(**extra_options)
367 else:
368 self._options = Options(**extra_options)
370 #: The optimization objective.
371 self._objective = Objective()
373 #: Maps constraint IDs to constraints.
374 self._constraints = OrderedDict()
376 #: Contains lists of constraints added together, all in order.
377 self._con_groups = []
379 #: Maps mutables to number of occurences in objective or constraints.
380 self._mtb_count = {}
382 #: Maps mutable names to mutables.
383 self._mutables = OrderedDict()
385 #: Maps variable names to variables.
386 self._variables = OrderedDict()
388 #: Maps parameter names to parameters.
389 self._parameters = OrderedDict()
391 #: Current solution strategy.
392 self._strategy = None
394 #: The last :class:`Solution` applied to the problem.
395 self._last_solution = None # Set by Solution.apply.
397 def _reset_mutable_registry(self):
398 self._mtb_count.clear()
399 self._mutables.clear()
400 self._variables.clear()
401 self._parameters.clear()
403 def reset(self, resetOptions=False):
404 """Reset the problem instance to its initial empty state.
406 :param bool resetOptions:
407 Whether also solver options should be reset to their default values.
408 """
409 # Reset options if requested.
410 if resetOptions:
411 self._options.reset()
413 # Reset objective to "find an assignment".
414 del self.objective
416 # Reset constraint registry.
417 self._constraints.clear()
418 self._con_groups.clear()
420 # Reset mutable registry.
421 self._reset_mutable_registry()
423 # Reset strategy and solution data.
424 self._strategy = None
425 self._last_solution = None
427 # --------------------------------------------------------------------------
428 # Properties.
429 # --------------------------------------------------------------------------
431 @property
432 def mutables(self):
433 """Maps names to variables and parameters in use by the problem.
435 :returns:
436 A read-only view to an :class:`~collections.OrderedDict`. The order
437 is deterministic and depends on the order of operations performed on
438 the :class:`Problem` instance as well as on the mutables' names.
439 """
440 return MappingProxyType(self._mutables)
442 @property
443 def variables(self):
444 """Maps names to variables in use by the problem.
446 :returns:
447 See :attr:`mutables`.
448 """
449 return MappingProxyType(self._variables)
451 @property
452 def parameters(self):
453 """Maps names to parameters in use by the problem.
455 :returns:
456 See :attr:`mutables`.
457 """
458 return MappingProxyType(self._parameters)
460 @property
461 def constraints(self):
462 """Maps constraint IDs to constraints that are part of the problem.
464 :returns:
465 A read-only view to an :class:`~collections.OrderedDict`. The order
466 is that in which constraints were added.
467 """
468 return MappingProxyType(self._constraints)
470 @constraints.deleter
471 def constraints(self):
472 # Clear constraint registry.
473 self._constraints.clear()
474 self._con_groups.clear()
476 # Update mutable registry.
477 self._reset_mutable_registry()
478 self._register_mutables(self.no.function.mutables)
480 @property
481 def objective(self):
482 """Optimization objective as an :class:`~picos.Objective` instance."""
483 return self._objective
485 @objective.setter
486 def objective(self, value):
487 self._unregister_mutables(self.no.function.mutables)
489 try:
490 if isinstance(value, Objective):
491 self._objective = value
492 else:
493 direction, function = value
494 self._objective = Objective(direction, function)
495 finally:
496 self._register_mutables(self.no.function.mutables)
498 @objective.deleter
499 def objective(self):
500 self._unregister_mutables(self.no.function.mutables)
502 self._objective = Objective()
504 @property
505 def no(self):
506 """Normalized objective as an :class:`Objective` instance.
508 Either a minimization or a maximization objective, with feasibility
509 posed as "minimize 0".
511 The same as the :attr:`~Objective.normalized` attribute of the
512 :attr:`objective`.
513 """
514 return self._objective.normalized
516 @property
517 def value(self):
518 """Objective function value.
520 If all mutables that appear in the objective function are valued, in
521 particular after a successful solution search, this is the numeric value
522 of the objective function. If the objective function is not fully valued
523 or if the problem is a feasiblity problem without an objective function,
524 this is :obj:`None`.
526 In the case of an uncertain objective, this is the worst-case (expected)
527 objective value.
529 :raises picos.uncertain.IntractableWorstCase:
530 When computing the worst-case (expected) value of an uncertain
531 objective is not supported.
533 .. note::
535 The Python special functions :class:`int`, :class:`float`,
536 :class:`complex` and :func:`round` as well as the special method
537 ``__index__`` make use of this value when applied to a
538 :class:`Problem`.
539 """
540 return self._objective.value
542 @property
543 def options(self):
544 """Solution search parameters as an :class:`~picos.Options` object."""
545 return self._options
547 @options.setter
548 def options(self, value):
549 if not isinstance(value, Options):
550 raise TypeError("Cannot assign an object of type {} as a problem's "
551 " options.".format(type(value).__name__))
553 self._options = value
555 @options.deleter
556 def options(self, value):
557 self._options.reset()
559 @property
560 def strategy(self):
561 """Solution strategy as a :class:`~picos.modeling.Strategy` object.
563 A strategy is available once you order the problem to be solved and it
564 will be reused for successive solution attempts (of a modified problem)
565 while it remains valid with respect to the problem's :attr:`footprint`.
567 When a strategy is reused, modifications to the objective and
568 constraints of a problem are passed step by step through the strategy's
569 reformulation pipeline while existing reformulation work is not
570 repeated. If the solver also supports these kinds of updates, then
571 modifying and re-solving a problem can be much faster than solving the
572 problem from scratch.
574 :Example:
576 >>> from picos import Problem, RealVariable
577 >>> x = RealVariable("x", 2)
578 >>> P = Problem()
579 >>> P.set_objective("min", abs(x)**2)
580 >>> print(P.strategy)
581 None
582 >>> sol = P.solve(solver = "cvxopt") # Creates a solution strategy.
583 >>> print(P.strategy)
584 1. ExtraOptions
585 2. EpigraphReformulation
586 3. SquaredNormToConicReformulation
587 4. CVXOPTSolver
588 >>> # Add another constraint handled by SquaredNormToConicReformulation:
589 >>> P.add_constraint(abs(x - 2)**2 <= 1)
590 <Squared Norm Constraint: ‖x - [2]‖² ≤ 1>
591 >>> P.strategy.valid(solver = "cvxopt")
592 True
593 >>> P.strategy.valid(solver = "glpk")
594 False
595 >>> sol = P.solve(solver = "cvxopt") # Reuses the strategy.
597 It's also possible to create a startegy from scratch:
599 >>> from picos.modeling import Strategy
600 >>> from picos.reforms import (EpigraphReformulation,
601 ... ConvexQuadraticToConicReformulation)
602 >>> from picos.solvers import CVXOPTSolver
603 >>> # Mimic what solve() does when no strategy exists:
604 >>> P.strategy = Strategy(P, CVXOPTSolver, EpigraphReformulation,
605 ... ConvexQuadraticToConicReformulation)
606 """
607 return self._strategy
609 @strategy.setter
610 def strategy(self, value):
611 from .strategy import Strategy
613 if not isinstance(value, Strategy):
614 raise TypeError(
615 "Cannot assign an object of type {} as a solution strategy."
616 .format(type(value).__name__))
618 if value.problem is not self:
619 raise ValueError("The solution strategy was constructed for a "
620 "different problem.")
622 self._strategy = value
624 @strategy.deleter
625 def strategy(self):
626 self._strategy = None
628 @property
629 def last_solution(self):
630 """The last :class:`~picos.Solution` applied to the problem."""
631 return self._last_solution
633 @property
634 def status(self):
635 """The solution status string as claimed by :attr:`last_solution`."""
636 if not self._last_solution:
637 return "unsolved"
638 else:
639 return self._last_solution.claimedStatus
641 @property
642 def footprint(self):
643 """Problem footprint as a :class:`~picos.modeling.Footprint` object."""
644 return Footprint.from_problem(self)
646 @property
647 def continuous(self):
648 """Whether all variables are of continuous types."""
649 return all(
650 isinstance(variable, expressions.CONTINUOUS_VARTYPES)
651 for variable in self._variables.values())
653 @property
654 def pure_integer(self):
655 """Whether all variables are of integral types."""
656 return not any(
657 isinstance(variable, expressions.CONTINUOUS_VARTYPES)
658 for variable in self._variables.values())
660 @property
661 def type(self):
662 """The problem type as a string, such as "Linear Program"."""
663 C = set(type(c) for c in self._constraints.values())
664 objective = self._objective.function
665 base = "Optimization Problem"
667 linear = [
668 constraints.AffineConstraint,
669 constraints.ComplexAffineConstraint,
670 constraints.AbsoluteValueConstraint,
671 constraints.SimplexConstraint,
672 constraints.FlowConstraint]
673 sdp = [
674 constraints.LMIConstraint,
675 constraints.ComplexLMIConstraint]
676 quadratic = [
677 constraints.ConvexQuadraticConstraint,
678 constraints.ConicQuadraticConstraint,
679 constraints.NonconvexQuadraticConstraint]
680 quadconic = [
681 constraints.SOCConstraint,
682 constraints.RSOCConstraint]
683 exponential = [
684 constraints.ExpConeConstraint,
685 constraints.SumExponentialsConstraint,
686 constraints.LogSumExpConstraint,
687 constraints.LogConstraint,
688 constraints.KullbackLeiblerConstraint]
689 complex = [
690 constraints.ComplexAffineConstraint,
691 constraints.ComplexLMIConstraint]
693 if objective is None:
694 if not C:
695 base = "Empty Problem"
696 elif C.issubset(set(linear)):
697 base = "Linear Feasibility Problem"
698 else:
699 base = "Feasibility Problem"
700 elif isinstance(objective, expressions.AffineExpression):
701 if not C:
702 if objective.constant:
703 base = "Constant Problem"
704 else:
705 base = "Linear Program" # Could have variable bounds.
706 elif C.issubset(set(linear)):
707 base = "Linear Program"
708 elif C.issubset(set(linear + quadconic)):
709 base = "Second Order Cone Program"
710 elif C.issubset(set(linear + sdp)):
711 base = "Semidefinite Program"
712 elif C.issubset(set(linear + [constraints.LogSumExpConstraint])):
713 base = "Geometric Program"
714 elif C.issubset(set(linear + exponential)):
715 base = "Exponential Program"
716 elif C.issubset(set(linear + quadratic)):
717 base = "Quadratically Constrained Program"
718 elif isinstance(objective, expressions.QuadraticExpression):
719 if C.issubset(set(linear)):
720 base = "Quadratic Program"
721 elif C.issubset(set(linear + quadratic)):
722 base = "Quadratically Constrained Quadratic Program"
723 elif isinstance(objective, expressions.LogSumExp):
724 if C.issubset(set(linear + [constraints.LogSumExpConstraint])):
725 base = "Geometric Program"
727 if self.continuous:
728 integrality = ""
729 elif self.pure_integer:
730 integrality = "Integer "
731 else:
732 integrality = "Mixed-Integer "
734 if any(c in complex for c in C):
735 complexity = "Complex "
736 else:
737 complexity = ""
739 return "{}{}{}".format(complexity, integrality, base)
741 @property
742 def dual(self):
743 """The Lagrangian dual problem of the standardized problem.
745 More precisely, this property invokes the following:
747 1. The primal problem is posed as an equivalent conic standard form
748 minimization problem, with variable bounds expressed as additional
749 constraints.
750 2. The Lagrangian dual problem of the reposed primal is computed.
751 3. The optimization direction and objective function sign of the dual
752 are adjusted such that, given strong duality and primal feasibility,
753 the optimal values of both problems are equal. In particular, if the
754 primal problem is a minimization or a maximization problem, the dual
755 problem returned will be the respective other.
757 :raises ~picos.modeling.strategy.NoStrategyFound:
758 If no reformulation strategy was found.
760 .. note::
762 This property is intended for educational purposes.
763 If you want to solve the primal problem via its dual, use the
764 :ref:`dualize <option_dualize>` option instead.
765 """
766 from ..reforms import Dualization
767 return self.reformulated(Dualization.SUPPORTED, dualize=True)
769 @property
770 def conic_form(self):
771 """The problem in conic form.
773 Reformulates the problem such that the objective is affine and all
774 constraints are :class:`~.constraints.ConicConstraint` instances.
776 :raises ~picos.modeling.strategy.NoStrategyFound:
777 If no reformulation strategy was found.
779 :Example:
781 >>> from picos import Problem, RealVariable
782 >>> x = RealVariable("x", 2)
783 >>> P = Problem()
784 >>> P.set_objective("min", abs(x)**2)
785 >>> print(P)
786 Quadratic Program
787 minimize ‖x‖²
788 over
789 2×1 real variable x
790 >>> print(P.conic_form)# doctest: +ELLIPSIS
791 Second Order Cone Program
792 minimize __..._t
793 over
794 1×1 real variable __..._t
795 2×1 real variable x
796 subject to
797 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0
799 .. note::
801 This property is intended for educational purposes.
802 You do not need to use it when solving a problem as PICOS will
803 perform the necessary reformulations automatically.
804 """
805 return self.reformulated(self.CONIC_FORM)
807 # --------------------------------------------------------------------------
808 # Python special methods, except __init__.
809 # --------------------------------------------------------------------------
811 @property
812 def _var_groups(self):
813 """Support :meth:`__str__`."""
814 vars_by_type = {}
815 for var in self._variables.values():
816 vtype = type(var).__name__
817 shape = var.shape
818 bound = tuple(bool(bound) for bound in var.bound_dicts)
819 index = (vtype, shape, bound)
821 vars_by_type.setdefault(index, set())
822 vars_by_type[index].add(var)
824 groups = []
825 for index in sorted(vars_by_type.keys()):
826 groups.append(natsorted(vars_by_type[index], key=lambda v: v.name))
828 return groups
830 @property
831 def _prm_groups(self):
832 """Support :meth:`__str__`."""
833 prms_by_type = {}
834 for prm in self._parameters.values():
835 vtype = type(prm).__name__
836 shape = prm.shape
837 index = (vtype, shape)
839 prms_by_type.setdefault(index, set())
840 prms_by_type[index].add(prm)
842 groups = []
843 for index in sorted(prms_by_type.keys()):
844 groups.append(natsorted(prms_by_type[index], key=lambda v: v.name))
846 return groups
848 @lru_cache()
849 def _mtb_group_string(self, group):
850 """Support :meth:`__str__`."""
851 if len(group) == 0:
852 return "[no mutables]"
854 if len(group) == 1:
855 return group[0].long_string
857 try:
858 template, data = parameterized_string(
859 [mtb.long_string for mtb in group])
860 except ValueError:
861 return group[0].long_string \
862 + ", " + ", ".join([v.name for v in group[1:]])
863 else:
864 return glyphs.forall(template, data)
866 @lru_cache()
867 def _con_group_string(self, group):
868 """Support :meth:`__str__`."""
869 if len(group) == 0:
870 return "[no constraints]"
872 if len(group) == 1:
873 return str(group[0])
875 try:
876 template, data = parameterized_string([str(con) for con in group])
877 except ValueError:
878 return "[{} constraints (1st: {})]".format(len(group), group[0])
879 else:
880 return glyphs.forall(template, data)
882 def __repr__(self):
883 return glyphs.repr1(self.type)
885 def __str__(self):
886 # Print problem type.
887 string = "{}\n".format(self.type)
889 # Print objective.
890 string += " {}\n".format(self._objective)
892 wrapper = TextWrapper(
893 initial_indent=" "*4,
894 subsequent_indent=" "*6,
895 break_long_words=False,
896 break_on_hyphens=False)
898 # Print variables.
899 if self._variables:
900 string += " {}\n".format(
901 "for" if self._objective.direction == "find" else "over")
902 for group in self._var_groups:
903 string += wrapper.fill(self._mtb_group_string(tuple(group)))
904 string += "\n"
906 # Print constraints.
907 if self._constraints:
908 string += " subject to\n"
909 for index, group in enumerate(self._con_groups):
910 string += wrapper.fill(self._con_group_string(tuple(group)))
911 string += "\n"
913 # Print parameters.
914 if self._parameters:
915 string += " given\n"
916 for group in self._prm_groups:
917 string += wrapper.fill(self._mtb_group_string(tuple(group)))
918 string += "\n"
920 return string.rstrip("\n")
922 def __index__(self):
923 return self._objective.__index__()
925 def __int__(self):
926 return self._objective.__int__()
928 def __float__(self):
929 return self._objective.__float__()
931 def __complex__(self):
932 return self._objective.__complex__()
934 def __round__(self, ndigits=None):
935 return self._objective.__round__(ndigits)
937 # --------------------------------------------------------------------------
938 # Bookkeeping methods.
939 # --------------------------------------------------------------------------
941 def _register_mutables(self, mtbs):
942 """Register the mutables of an objective function or constraint."""
943 # Register every mutable at most once per call.
944 if not isinstance(mtbs, (set, frozenset)):
945 raise TypeError("Mutable registry can (un)register a mutable "
946 "only once per call, so the argument must be a set type.")
948 # Retrieve old and new mutables as mapping from name to object.
949 old_mtbs = self._mutables
950 new_mtbs = OrderedDict(
951 (mtb.name, mtb) for mtb in sorted(mtbs, key=(lambda m: m.name)))
952 new_vars = OrderedDict((name, mtb) for name, mtb in new_mtbs.items()
953 if isinstance(mtb, BaseVariable))
954 new_prms = OrderedDict((name, mtb) for name, mtb in new_mtbs.items()
955 if not isinstance(mtb, BaseVariable))
957 # Check for mutable name clashes within the new set.
958 if len(new_mtbs) != len(mtbs):
959 raise ValueError(
960 "The object you are trying to add to a problem contains "
961 "multiple mutables of the same name. This is not allowed.")
963 # Check for mutable name clashes with existing mutables.
964 for name in set(old_mtbs).intersection(set(new_mtbs)):
965 if old_mtbs[name] is not new_mtbs[name]:
966 raise ValueError("Cannot register the mutable {} with the "
967 "problem because it already tracks another mutable with "
968 "the same name.".format(name))
970 # Keep track of new mutables.
971 self._mutables.update(new_mtbs)
972 self._variables.update(new_vars)
973 self._parameters.update(new_prms)
975 # Count up the mutable references.
976 for mtb in mtbs:
977 self._mtb_count.setdefault(mtb, 0)
978 self._mtb_count[mtb] += 1
980 def _unregister_mutables(self, mtbs):
981 """Unregister the mutables of an objective function or constraint."""
982 # Unregister every mutable at most once per call.
983 if not isinstance(mtbs, (set, frozenset)):
984 raise TypeError("Mutable registry can (un)register a mutable "
985 "only once per call, so the argument must be a set type.")
987 for mtb in mtbs:
988 name = mtb.name
990 # Make sure the mutable is properly registered.
991 assert name in self._mutables and mtb in self._mtb_count, \
992 "Tried to unregister a mutable that is not registered."
993 assert self._mtb_count[mtb] >= 1, \
994 "Found a nonpostive mutable count."
996 # Count down the mutable references.
997 self._mtb_count[mtb] -= 1
999 # Remove a mutable with a reference count of zero.
1000 if not self._mtb_count[mtb]:
1001 self._mtb_count.pop(mtb)
1002 self._mutables.pop(name)
1004 if isinstance(mtb, BaseVariable):
1005 self._variables.pop(name)
1006 else:
1007 self._parameters.pop(name)
1009 # --------------------------------------------------------------------------
1010 # Methods to manipulate the objective function and its direction.
1011 # --------------------------------------------------------------------------
1013 def set_objective(self, direction=None, expression=None):
1014 """Set the optimization direction and objective function of the problem.
1016 :param str direction:
1017 Case insensitive search direction string. One of
1019 - ``"min"`` or ``"minimize"``,
1020 - ``"max"`` or ``"maximize"``,
1021 - ``"find"`` or :obj:`None` (for a feasibility problem).
1023 :param ~picos.expressions.Expression expression:
1024 The objective function. Must be :obj:`None` for a feasibility
1025 problem.
1026 """
1027 self.objective = direction, expression
1029 # --------------------------------------------------------------------------
1030 # Methods to add, retrieve and remove constraints.
1031 # --------------------------------------------------------------------------
1033 def _lookup_constraint(self, idOrIndOrCon):
1034 """Look for a constraint with the given identifier.
1036 Given a constraint object or ID or offset or a constraint group index or
1037 index pair, returns a matching (list of) constraint ID(s) that is (are)
1038 part of the problem.
1039 """
1040 if isinstance(idOrIndOrCon, int):
1041 if idOrIndOrCon in self._constraints:
1042 # A valid ID.
1043 return idOrIndOrCon
1044 elif idOrIndOrCon < len(self._constraints):
1045 # An offset.
1046 return list(self._constraints.keys())[idOrIndOrCon]
1047 else:
1048 raise LookupError(
1049 "The problem has no constraint with ID or offset {}."
1050 .format(idOrIndOrCon))
1051 elif isinstance(idOrIndOrCon, constraints.Constraint):
1052 # A constraint object.
1053 id = idOrIndOrCon.id
1054 if id in self._constraints:
1055 return id
1056 else:
1057 raise KeyError("The constraint '{}' is not part of the problem."
1058 .format(idOrIndOrCon))
1059 elif isinstance(idOrIndOrCon, tuple) or isinstance(idOrIndOrCon, list):
1060 if len(idOrIndOrCon) == 1:
1061 groupIndex = idOrIndOrCon[0]
1062 if groupIndex < len(self._con_groups):
1063 return [c.id for c in self._con_groups[groupIndex]]
1064 else:
1065 raise IndexError("Constraint group index out of range.")
1066 elif len(idOrIndOrCon) == 2:
1067 groupIndex, groupOffset = idOrIndOrCon
1068 if groupIndex < len(self._con_groups):
1069 group = self._con_groups[groupIndex]
1070 if groupOffset < len(group):
1071 return group[groupOffset].id
1072 else:
1073 raise IndexError(
1074 "Constraint group offset out of range.")
1075 else:
1076 raise IndexError("Constraint group index out of range.")
1077 else:
1078 raise TypeError("If looking up constraints by group, the index "
1079 "must be a tuple or list of length at most two.")
1080 else:
1081 raise TypeError("Argument of type '{}' not supported when looking "
1082 "up constraints".format(type(idOrIndOrCon)))
1084 def get_constraint(self, idOrIndOrCon):
1085 """Return a (list of) constraint(s) of the problem.
1087 :param idOrIndOrCon: One of the following:
1089 * A constraint object. It will be returned when the constraint is
1090 part of the problem, otherwise a KeyError is raised.
1091 * The integer ID of the constraint.
1092 * The integer offset of the constraint in the list of all
1093 constraints that are part of the problem, in the order that they
1094 were added.
1095 * A list or tuple of length 1. Its only element is the index of a
1096 constraint group (of constraints that were added together), where
1097 groups are indexed in the order that they were added to the
1098 problem. The whole group is returned as a list of constraints.
1099 That list has the constraints in the order that they were added.
1100 * A list or tuple of length 2. The first element is a constraint
1101 group offset as above, the second an offset within that list.
1103 :type idOrIndOrCon: picos.constraints.Constraint or int or tuple or list
1105 :returns: A :class:`constraint <picos.constraints.Constraint>` or a list
1106 thereof.
1108 :Example:
1110 >>> import picos as pic
1111 >>> import cvxopt as cvx
1112 >>> from pprint import pprint
1113 >>> prob=pic.Problem()
1114 >>> x=[prob.add_variable('x[{0}]'.format(i),2) for i in range(5)]
1115 >>> y=prob.add_variable('y',5)
1116 >>> Cx=prob.add_list_of_constraints([(1|x[i]) < y[i] for i in range(5)])
1117 >>> Cy=prob.add_constraint(y>0)
1118 >>> print(prob)
1119 Linear Feasibility Problem
1120 find an assignment
1121 for
1122 2×1 real variable x[i] ∀ i ∈ [0…4]
1123 5×1 real variable y
1124 subject to
1125 ∑(x[i]) ≤ y[i] ∀ i ∈ [0…4]
1126 y ≥ 0
1127 >>> # Retrieve the second constraint, indexed from zero:
1128 >>> prob.get_constraint(1)
1129 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>
1130 >>> # Retrieve the fourth consraint from the first group:
1131 >>> prob.get_constraint((0,3))
1132 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>
1133 >>> # Retrieve the whole first group of constraints:
1134 >>> pprint(prob.get_constraint((0,)))
1135 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1136 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>,
1137 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1138 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1139 <1×1 Affine Constraint: ∑(x[4]) ≤ y[4]>]
1140 >>> # Retrieve the second "group", containing just one constraint:
1141 >>> prob.get_constraint((1,))
1142 [<5×1 Affine Constraint: y ≥ 0>]
1143 """
1144 idOrIds = self._lookup_constraint(idOrIndOrCon)
1146 if isinstance(idOrIds, list):
1147 return [self._constraints[id] for id in idOrIds]
1148 else:
1149 return self._constraints[idOrIds]
1151 def add_constraint(self, constraint, key=None):
1152 """Add a constraint to the problem.
1154 :param constraint: The constraint to be added.
1155 :type constraint: :class:`Constraint <picos.constraints.Constraint>`
1156 :param str key: Optional name of the constraint.
1157 :returns: The constraint that was added to the problem.
1158 """
1159 # Handle deprecated 'key' parameter.
1160 if key is not None:
1161 throw_deprecation_warning(
1162 "Naming constraints is currently not supported.")
1164 # Register the constraint.
1165 self._constraints[constraint.id] = constraint
1166 self._con_groups.append([constraint])
1168 # Register the constraint's mutables.
1169 self._register_mutables(constraint.mutables)
1171 return constraint
1173 def add_list_of_constraints(self, lst, it=None, indices=None, key=None):
1174 """Add a list of constraints to the problem.
1176 Adds a list of constraints to the problem, enabling the use of
1177 Python list comprehensions (see the example below).
1179 :param list(picos.constraints.Constraint) lst: Constraints to add.
1180 :param it: DEPRECATED
1181 :param indices: DEPRECATED
1182 :param key: DEPRECATED
1184 :returns: A list of all constraints that were added.
1186 :Example:
1188 >>> import picos as pic
1189 >>> import cvxopt as cvx
1190 >>> from pprint import pprint
1191 >>> prob=pic.Problem()
1192 >>> x=[prob.add_variable('x[{0}]'.format(i),2) for i in range(5)]
1193 >>> pprint(x)
1194 [<2×1 Real Variable: x[0]>,
1195 <2×1 Real Variable: x[1]>,
1196 <2×1 Real Variable: x[2]>,
1197 <2×1 Real Variable: x[3]>,
1198 <2×1 Real Variable: x[4]>]
1199 >>> y=prob.add_variable('y',5)
1200 >>> IJ=[(1,2),(2,0),(4,2)]
1201 >>> w={}
1202 >>> for ij in IJ:
1203 ... w[ij]=prob.add_variable('w[{},{}]'.format(*ij),3)
1204 ...
1205 >>> u=pic.new_param('u',cvx.matrix([2,5]))
1206 >>> C1=prob.add_list_of_constraints([u.T*x[i] < y[i] for i in range(5)])
1207 >>> C2=prob.add_list_of_constraints([abs(w[i,j])<y[j] for (i,j) in IJ])
1208 >>> C3=prob.add_list_of_constraints([y[t] > y[t+1] for t in range(4)])
1209 >>> print(prob)
1210 Feasibility Problem
1211 find an assignment
1212 for
1213 2×1 real variable x[i] ∀ i ∈ [0…4]
1214 3×1 real variable w[i,j] ∀ (i,j) ∈ zip([1,2,4],[2,0,2])
1215 5×1 real variable y
1216 subject to
1217 uᵀ·x[i] ≤ y[i] ∀ i ∈ [0…4]
1218 ‖w[i,j]‖ ≤ y[j] ∀ (i,j) ∈ zip([1,2,4],[2,0,2])
1219 y[i] ≥ y[i+1] ∀ i ∈ [0…3]
1220 """
1221 if it is not None or indices is not None or key is not None:
1222 # Deprecated as of 2.0.
1223 throw_deprecation_warning("Arguments 'it', 'indices' and 'key' to "
1224 "add_list_of_constraints are deprecated and ignored.")
1226 added = []
1227 for constraint in lst:
1228 added.append(self.add_constraint(constraint))
1229 self._con_groups.pop()
1231 if added:
1232 self._con_groups.append(added)
1234 return added
1236 # TODO: Add Problem.require replacing add_constraint and
1237 # add_list_of_constraints?
1239 def _con_group_index(self, conOrConID):
1240 """Support :meth:`remove_constraint`."""
1241 if isinstance(conOrConID, int):
1242 constraint = self._constraints[conOrConID]
1243 else:
1244 constraint = conOrConID
1246 for i, group in enumerate(self._con_groups):
1247 for j, candidate in enumerate(group):
1248 if candidate is constraint:
1249 return i, j
1251 if constraint in self._constraints.values():
1252 raise RuntimeError("The problem's constraint and constraint group "
1253 "registries are out of sync.")
1254 else:
1255 raise KeyError("The constraint is not part of the problem.")
1257 def remove_constraint(self, idOrIndOrCon):
1258 """Delete a constraint from the problem.
1260 :param idOrIndOrCon: See :meth:`get_constraint`.
1262 :Example:
1264 >>> import picos
1265 >>> from pprint import pprint
1266 >>> P = picos.Problem()
1267 >>> x = [P.add_variable('x[{0}]'.format(i), 2) for i in range(4)]
1268 >>> y = P.add_variable('y', 4)
1269 >>> Cxy = P.add_list_of_constraints(
1270 ... [(1 | x[i]) <= y[i] for i in range(4)])
1271 >>> Cy = P.add_constraint(y >= 0)
1272 >>> Cx0to2 = P.add_list_of_constraints([x[i] <= 2 for i in range(3)])
1273 >>> Cx3 = P.add_constraint(x[3] <= 1)
1274 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1275 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1276 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>,
1277 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1278 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1279 <4×1 Affine Constraint: y ≥ 0>,
1280 <2×1 Affine Constraint: x[0] ≤ [2]>,
1281 <2×1 Affine Constraint: x[1] ≤ [2]>,
1282 <2×1 Affine Constraint: x[2] ≤ [2]>,
1283 <2×1 Affine Constraint: x[3] ≤ [1]>]
1284 >>> # Delete the 2nd constraint (counted from 0):
1285 >>> P.remove_constraint(1)
1286 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1287 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1288 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1289 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1290 <4×1 Affine Constraint: y ≥ 0>,
1291 <2×1 Affine Constraint: x[0] ≤ [2]>,
1292 <2×1 Affine Constraint: x[1] ≤ [2]>,
1293 <2×1 Affine Constraint: x[2] ≤ [2]>,
1294 <2×1 Affine Constraint: x[3] ≤ [1]>]
1295 >>> # Delete the 2nd group of constraints, i.e. the constraint y > 0:
1296 >>> P.remove_constraint((1,))
1297 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1298 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1299 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1300 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1301 <2×1 Affine Constraint: x[0] ≤ [2]>,
1302 <2×1 Affine Constraint: x[1] ≤ [2]>,
1303 <2×1 Affine Constraint: x[2] ≤ [2]>,
1304 <2×1 Affine Constraint: x[3] ≤ [1]>]
1305 >>> # Delete the 3rd remaining group of constraints, i.e. x[3] < [1]:
1306 >>> P.remove_constraint((2,))
1307 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1308 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1309 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1310 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1311 <2×1 Affine Constraint: x[0] ≤ [2]>,
1312 <2×1 Affine Constraint: x[1] ≤ [2]>,
1313 <2×1 Affine Constraint: x[2] ≤ [2]>]
1314 >>> # Delete 2nd constraint of the 2nd remaining group, i.e. x[1] < |2|:
1315 >>> P.remove_constraint((1,1))
1316 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1317 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1318 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1319 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1320 <2×1 Affine Constraint: x[0] ≤ [2]>,
1321 <2×1 Affine Constraint: x[2] ≤ [2]>]
1322 """
1323 idOrIds = self._lookup_constraint(idOrIndOrCon)
1325 removedCons = []
1327 if isinstance(idOrIds, list):
1328 assert idOrIds, "There is an empty constraint group."
1329 groupIndex, _ = self._con_group_index(idOrIds[0])
1330 self._con_groups.pop(groupIndex)
1331 for id in idOrIds:
1332 removedCons.append(self._constraints.pop(id))
1333 else:
1334 constraint = self._constraints.pop(idOrIds)
1335 removedCons.append(constraint)
1336 groupIndex, groupOffset = self._con_group_index(constraint)
1337 group = self._con_groups[groupIndex]
1338 group.pop(groupOffset)
1339 if not group:
1340 self._con_groups.pop(groupIndex)
1342 # Unregister the mutables added by the removed constraints.
1343 for con in removedCons:
1344 self._unregister_mutables(con.mutables)
1346 def remove_all_constraints(self):
1347 """Remove all constraints from the problem.
1349 .. note::
1351 This method does not remove bounds set directly on variables.
1352 """
1353 del self.constraints
1355 # --------------------------------------------------------------------------
1356 # Borderline legacy methods to deal with variables.
1357 # --------------------------------------------------------------------------
1359 _PARAMETERIZED_VARIABLE_REGEX = re.compile(r"^([^[]+)\[([^\]]+)\]$")
1361 def get_variable(self, name):
1362 """Retrieve variables referenced by the problem.
1364 Retrieves either a single variable with the given name or a group of
1365 variables all named ``name[param]`` with different values for ``param``.
1366 If the values for ``param`` are the integers from zero to the size of
1367 the group minus one, then the group is returned as a :obj:`list` ordered
1368 by ``param``, otherwise it is returned as a :obj:`dict` with the values
1369 of ``param`` as keys.
1371 .. note::
1373 Since PICOS 2.0, variables are independent of problems and only
1374 appear in a problem for as long as they are referenced by the
1375 problem's objective function or constraints.
1377 :param str name:
1378 The name of a variable, or the base name of a group of variables.
1380 :returns:
1381 A :class:`variable <picos.expressions.BaseVariable>` or a
1382 :class:`list` or :class:`dict` thereof.
1384 :Example:
1386 >>> from picos import Problem, RealVariable
1387 >>> from pprint import pprint
1388 >>> # Create a number of variables with structured names.
1389 >>> vars = [RealVariable("x")]
1390 >>> for i in range(4):
1391 ... vars.append(RealVariable("y[{}]".format(i)))
1392 >>> for key in ["alice", "bob", "carol"]:
1393 ... vars.append(RealVariable("z[{}]".format(key)))
1394 >>> # Make the variables appear in a problem.
1395 >>> P = Problem()
1396 >>> P.set_objective("min", sum([var for var in vars]))
1397 >>> print(P)
1398 Linear Program
1399 minimize x + y[0] + y[1] + y[2] + y[3] + z[alice] + z[bob] + z[carol]
1400 over
1401 1×1 real variable x, y[0], y[1], y[2], y[3], z[alice], z[bob],
1402 z[carol]
1403 >>> # Retrieve the variables from the problem.
1404 >>> P.get_variable("x")
1405 <1×1 Real Variable: x>
1406 >>> pprint(P.get_variable("y"))
1407 [<1×1 Real Variable: y[0]>,
1408 <1×1 Real Variable: y[1]>,
1409 <1×1 Real Variable: y[2]>,
1410 <1×1 Real Variable: y[3]>]
1411 >>> pprint(P.get_variable("z"))
1412 {'alice': <1×1 Real Variable: z[alice]>,
1413 'bob': <1×1 Real Variable: z[bob]>,
1414 'carol': <1×1 Real Variable: z[carol]>}
1415 >>> P.get_variable("z")["alice"] is P.get_variable("z[alice]")
1416 True
1417 """
1418 if name in self._variables:
1419 return self._variables[name]
1420 else:
1421 # Check if the name is really just a basename.
1422 params = []
1423 for otherName in sorted(self._variables.keys()):
1424 match = self._PARAMETERIZED_VARIABLE_REGEX.match(otherName)
1425 if not match:
1426 continue
1427 base, param = match.groups()
1428 if name == base:
1429 params.append(param)
1431 if params:
1432 # Return a list if the parameters are a range.
1433 try:
1434 intParams = sorted([int(p) for p in params])
1435 except ValueError:
1436 pass
1437 else:
1438 if intParams == list(range(len(intParams))):
1439 return [self._variables["{}[{}]".format(name, param)]
1440 for param in intParams]
1442 # Otherwise return a dict.
1443 return {param: self._variables["{}[{}]".format(name, param)]
1444 for param in params}
1445 else:
1446 raise KeyError("The problem references no variable or group of "
1447 "variables named '{}'.".format(name))
1449 def get_valued_variable(self, name):
1450 """Retrieve values of variables referenced by the problem.
1452 This method works the same :meth:`get_variable` but it returns the
1453 variable's :attr:`values <.expression.Expression.value>` instead of the
1454 variable objects.
1456 :raises ~picos.expressions.NotValued:
1457 If any of the selected variables is not valued.
1458 """
1459 exp = self.get_variable(name)
1460 if isinstance(exp, list):
1461 for i in range(len(exp)):
1462 exp[i] = exp[i].value
1463 elif isinstance(exp, dict):
1464 for i in exp:
1465 exp[i] = exp[i].value
1466 else:
1467 exp = exp.value
1468 return exp
1470 # --------------------------------------------------------------------------
1471 # Methods to create copies of the problem.
1472 # --------------------------------------------------------------------------
1474 def copy(self):
1475 """Create a deep copy of the problem, using new mutables."""
1476 the_copy = Problem(copyOptions=self._options)
1478 # Duplicate the mutables.
1479 new_mtbs = {mtb: mtb.copy() for name, mtb in self._mutables.items()}
1481 # Make copies of constraints on top of the new mutables.
1482 for group in self._con_groups:
1483 the_copy.add_list_of_constraints(
1484 constraint.replace_mutables(new_mtbs) for constraint in group)
1486 # Make a copy of the objective on top of the new mutables.
1487 direction, function = self._objective
1488 if function is not None:
1489 the_copy.objective = direction, function.replace_mutables(new_mtbs)
1491 return the_copy
1493 def continuous_relaxation(self, copy_other_mutables=True):
1494 """Return a continuous relaxation of the problem.
1496 This is done by replacing integer variables with continuous ones.
1498 :param bool copy_other_mutables:
1499 Whether variables that are already continuous as well as parameters
1500 should be copied. If this is :obj:`False`, then the relxation shares
1501 these mutables with the original problem.
1502 """
1503 the_copy = Problem(copyOptions=self._options)
1505 # Relax integral variables and copy other mutables if requested.
1506 new_mtbs = {}
1507 for name, var in self._mutables.items():
1508 if isinstance(var, expressions.IntegerVariable):
1509 new_mtbs[name] = expressions.RealVariable(
1510 name, var.shape, var._lower, var._upper)
1511 elif isinstance(var, expressions.BinaryVariable):
1512 new_mtbs[name] = expressions.RealVariable(name, var.shape, 0, 1)
1513 else:
1514 if copy_other_mutables:
1515 new_mtbs[name] = var.copy()
1516 else:
1517 new_mtbs[name] = var
1519 # Make copies of constraints on top of the new mutables.
1520 for group in self._con_groups:
1521 the_copy.add_list_of_constraints(
1522 constraint.replace_mutables(new_mtbs) for constraint in group)
1524 # Make a copy of the objective on top of the new mutables.
1525 direction, function = self._objective
1526 if function is not None:
1527 the_copy.objective = direction, function.replace_mutables(new_mtbs)
1529 return the_copy
1531 def clone(self, copyOptions=True):
1532 """Create a semi-deep copy of the problem.
1534 The copy is constrained by the same constraint objects and has the same
1535 objective function and thereby references the existing variables and
1536 parameters that appear in these objects.
1538 The clone can be modified to describe a new problem but when its
1539 variables and parameters are valued, in particular when a solution is
1540 applied to the new problem, then the same values are found in the
1541 corresponding variables and parameters of the old problem. If this is
1542 not a problem to you, then cloning can be much faster than copying.
1544 :param bool copyOptions:
1545 Whether to make an independent copy of the problem's options.
1546 Disabling this will apply any option changes to the original problem
1547 as well but yields a (very small) reduction in cloning time.
1548 """
1549 # Start with a shallow copy of self.
1550 # TODO: Consider adding Problem.__new__ to speed this up further.
1551 theClone = pycopy.copy(self)
1553 # Make the constraint registry independent.
1554 theClone._constraints = self._constraints.copy()
1555 theClone._con_groups = []
1556 for group in self._con_groups:
1557 theClone._con_groups.append(pycopy.copy(group))
1559 # Make the mutable registry independent.
1560 theClone._mtb_count = self._mtb_count.copy()
1561 theClone._mutables = self._mutables.copy()
1562 theClone._variables = self._variables.copy()
1563 theClone._parameters = self._parameters.copy()
1565 # Reset the clone's solution strategy and last solution.
1566 theClone._strategy = None
1568 # Make the solver options independent, if requested.
1569 if copyOptions:
1570 theClone._options = self._options.copy()
1572 # NOTE: No need to change the following attributes:
1573 # - objective: Is immutable as a tuple.
1574 # - _last_solution: Remains as valid as it is.
1576 return theClone
1578 # --------------------------------------------------------------------------
1579 # Methods to solve or export the problem.
1580 # --------------------------------------------------------------------------
1582 def prepared(self, steps=None, **extra_options):
1583 """Perform a dry-run returning the reformulated (prepared) problem.
1585 This behaves like :meth:`solve` in that it takes a number of additional
1586 temporary options, finds a solution strategy matching the problem and
1587 options, and performs the strategy's reformulations in turn to obtain
1588 modified problems. However, it stops after the given number of steps and
1589 never hands the reformulated problem to a solver. Instead of a solution,
1590 :meth:`prepared` then returns the last reformulated problem.
1592 Unless this method returns the problem itself, the special attributes
1593 ``prepared_strategy`` and ``prepared_steps`` are added to the returned
1594 problem. They then contain the (partially) executed solution strategy
1595 and the number of performed reformulations, respectively.
1597 :param int steps:
1598 Number of reformulations to perform. :obj:`None` means as many as
1599 there are. If this parameter is :math:`0`, then the problem itself
1600 is returned. If it is :math:`1`, then only the implicit first
1601 reformulation :class:`~.reform_options.ExtraOptions` is executed,
1602 which may also output the problem itself, depending on
1603 ``extra_options``.
1605 :param extra_options:
1606 Additional solver options to use with this dry-run only.
1608 :returns:
1609 The reformulated problem, with ``extra_options`` set unless they
1610 were "consumed" by a reformulation (e.g.
1611 :ref:`option_dualize <option_dualize>`).
1613 :raises ~picos.modeling.strategy.NoStrategyFound:
1614 If no solution strategy was found.
1616 :raises ValueError:
1617 If there are not as many reformulation steps as requested.
1619 :Example:
1621 >>> from picos import Problem, RealVariable
1622 >>> x = RealVariable("x", 2)
1623 >>> P = Problem()
1624 >>> P.set_objective("min", abs(x)**2)
1625 >>> Q = P.prepared(solver = "cvxopt")
1626 >>> print(Q.prepared_strategy) # Show prepared reformulation steps.
1627 1. ExtraOptions
1628 2. EpigraphReformulation
1629 3. SquaredNormToConicReformulation
1630 4. CVXOPTSolver
1631 >>> Q.prepared_steps # Check how many steps have been performed.
1632 3
1633 >>> print(P)
1634 Quadratic Program
1635 minimize ‖x‖²
1636 over
1637 2×1 real variable x
1638 >>> print(Q)# doctest: +ELLIPSIS
1639 Second Order Cone Program
1640 minimize __..._t
1641 over
1642 1×1 real variable __..._t
1643 2×1 real variable x
1644 subject to
1645 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0
1646 """
1647 from .strategy import Strategy
1649 # Produce a strategy for the clone.
1650 strategy = Strategy.from_problem(self, **extra_options)
1651 numReforms = len(strategy.reforms)
1653 if steps is None:
1654 steps = numReforms
1656 if steps == 0:
1657 return self
1658 elif steps > numReforms:
1659 raise ValueError("The pipeline {} has only {} reformulation steps "
1660 "to choose from.".format(strategy, numReforms))
1662 # Replace the successor of the last reformulation with a dummy solver.
1663 lastReform = strategy.reforms[steps - 1]
1664 oldSuccessor = lastReform.successor
1665 lastReform.successor = type("DummySolver", (), {
1666 "execute": lambda self: Solution(
1667 {}, solver="dummy", vectorizedPrimals=True)})()
1669 # Execute the cut-short strategy.
1670 strategy.execute(**extra_options)
1672 # Repair the last reformulation.
1673 lastReform.successor = oldSuccessor
1675 # Retrieve and augment the output problem (unless it's self).
1676 output = lastReform.output
1677 if output is not self:
1678 output.prepared_strategy = strategy
1679 output.prepared_steps = steps
1681 return output
1683 def reformulated(self, specification, **extra_options):
1684 r"""Return the problem reformulated to match a specification.
1686 Internally this creates a dummy solver accepting problems of the desired
1687 form and then calls :meth:`prepared` with the dummy solver passed via
1688 :ref:`option_ad_hoc_solver <option_ad_hoc_solver>`. See meth:`prepared`
1689 for more details.
1691 :param specification:
1692 A problem class that the resulting problem must be a member of.
1693 :type specification:
1694 ~picos.modeling.Specification
1696 :param extra_options:
1697 Additional solver options to use with this reformulation only.
1699 :returns:
1700 The reformulated problem, with ``extra_options`` set unless they
1701 were "consumed" by a reformulation (e.g.
1702 :ref:`dualize <option_dualize>`).
1704 :raises ~picos.modeling.strategy.NoStrategyFound:
1705 If no reformulation strategy was found.
1707 :Example:
1709 >>> from picos import Problem, RealVariable
1710 >>> from picos.modeling import Specification
1711 >>> from picos.expressions import AffineExpression
1712 >>> from picos.constraints import (
1713 ... AffineConstraint, SOCConstraint, RSOCConstraint)
1714 >>> # Define the class/specification of second order conic problems:
1715 >>> S = Specification(objectives=[AffineExpression],
1716 ... constraints=[AffineConstraint, SOCConstraint, RSOCConstraint])
1717 >>> # Define a quadratic program and reformulate it:
1718 >>> x = RealVariable("x", 2)
1719 >>> P = Problem()
1720 >>> P.set_objective("min", abs(x)**2)
1721 >>> Q = P.reformulated(S)
1722 >>> print(P)
1723 Quadratic Program
1724 minimize ‖x‖²
1725 over
1726 2×1 real variable x
1727 >>> print(Q)# doctest: +ELLIPSIS
1728 Second Order Cone Program
1729 minimize __..._t
1730 over
1731 1×1 real variable __..._t
1732 2×1 real variable x
1733 subject to
1734 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0
1736 .. note::
1738 This method is intended for educational purposes.
1739 You do not need to use it when solving a problem as PICOS will
1740 perform the necessary reformulations automatically.
1741 """
1742 if not isinstance(specification, Specification):
1743 raise TypeError("The desired problem type must be given as a "
1744 "Specification object.")
1746 # Create a placeholder function for abstract methods of a dummy solver.
1747 def placeholder(the_self):
1748 raise RuntimeError("The dummy solver created by "
1749 "Problem.reformulated must not be executed.")
1751 # Declare a dummy solver that accepts specified problems.
1752 DummySolver = type("DummySolver", (Solver,), {
1753 # Abstract class methods.
1754 "supports": classmethod(lambda cls, footprint:
1755 Solver.supports(footprint) and footprint in specification),
1756 "default_penalty": classmethod(lambda cls: 0),
1757 "test_availability": classmethod(lambda cls: None),
1758 "names": classmethod(lambda cls: ("Dummy Solver", "DummySolver",
1759 "Dummy Solver accepting {}".format(specification))),
1760 "is_free": classmethod(lambda cls: True),
1762 # Additional class methods needed for an ad-hoc solver.
1763 "penalty": classmethod(lambda cls, options: 0),
1765 # Abstract instance methods.
1766 "reset_problem": lambda self: placeholder(self),
1767 "_import_problem": lambda self: placeholder(self),
1768 "_update_problem": lambda self: placeholder(self),
1769 "_solve": lambda self: placeholder(self)
1770 })
1772 # Ad-hoc the dummy solver and prepare the problem for it.
1773 oldAdHocSolver = self.options.ad_hoc_solver
1774 extra_options["ad_hoc_solver"] = DummySolver
1775 problem = self.prepared(**extra_options)
1777 # Restore the ad_hoc_solver option of the original problem.
1778 problem.options.ad_hoc_solver = oldAdHocSolver
1780 return problem
1782 def solve(self, **extra_options):
1783 """Hand the problem to a solver.
1785 You can select the solver manually with the ``solver`` option. Otherwise
1786 a suitable solver will be selected among those that are available on the
1787 platform.
1789 The default behavior (options ``primals=True``, ``duals=None``) is to
1790 raise a :exc:`~picos.SolutionFailure` when the primal solution is not
1791 found optimal by the solver, while the dual solution is allowed to be
1792 missing or incomplete.
1794 When this method succeeds and unless ``apply_solution=False``, you can
1795 access the solution as follows:
1797 - The problem's :attr:`value` denotes the objective function value.
1798 - The variables' :attr:`~.expression.Expression.value` is set
1799 according to the primal solution. You can in fact query the value
1800 of any expression involving valued variables like this.
1801 - The constraints' :attr:`~.constraint.Constraint.dual` is set
1802 according to the dual solution.
1803 - The value of any parameter involved in the problem may have
1804 changed, depending on the parameter.
1806 :param extra_options:
1807 A sequence of additional solver options to use with this solution
1808 search only. In particular, this lets you
1810 - select a solver via the ``solver`` option,
1811 - obtain non-optimal primal solutions by setting ``primals=None``,
1812 - require a complete and optimal dual solution with ``duals=True``,
1813 and
1814 - skip valuing variables or constraints with
1815 ``apply_solution=False``.
1817 :returns ~picos.Solution or list(~picos.Solution):
1818 A solution object or list thereof.
1820 :raises ~picos.SolutionFailure:
1821 In the following cases:
1823 1. No solution strategy was found.
1824 2. Multiple solutions were requested but none were returned.
1825 3. A primal solution was explicitly requested (``primals=True``) but
1826 the primal solution is missing/incomplete or not claimed optimal.
1827 4. A dual solution was explicitly requested (``duals=True``) but
1828 the dual solution is missing/incomplete or not claimed optimal.
1830 The case number is stored in the ``code`` attribute of the
1831 exception.
1832 """
1833 from .strategy import NoStrategyFound, Strategy
1835 startTime = time.time()
1837 extra_options = map_legacy_options(**extra_options)
1838 options = self.options.self_or_updated(**extra_options)
1839 verbose = options.verbosity > 0
1841 with picos_box(show=verbose):
1842 if verbose:
1843 print("Problem type: {}.".format(self.type))
1845 # Reset an outdated strategy.
1846 if self._strategy and not self._strategy.valid(**extra_options):
1847 if verbose:
1848 print("Strategy outdated:\n{}.".format(self._strategy))
1850 self._strategy = None
1852 # Find a new solution strategy, if necessary.
1853 if not self._strategy:
1854 if verbose:
1855 if options.ad_hoc_solver:
1856 solverName = options.ad_hoc_solver.names()[1]
1857 elif options.solver:
1858 solverName = get_solver(options.solver).names()[1]
1859 else:
1860 solverName = None
1862 print("Searching a solution strategy{}.".format(
1863 " for {}".format(solverName) if solverName else ""))
1865 try:
1866 self._strategy = Strategy.from_problem(
1867 self, **extra_options)
1868 except NoStrategyFound as error:
1869 s = str(error)
1871 if verbose:
1872 print(s, flush=True)
1874 raise SolutionFailure(1, "No solution strategy found.") \
1875 from error
1877 if verbose:
1878 print("Solution strategy:\n {}".format(
1879 "\n ".join(str(self._strategy).splitlines())))
1880 else:
1881 if verbose:
1882 print("Reusing strategy:\n {}".format(
1883 "\n ".join(str(self._strategy).splitlines())))
1885 # Execute the strategy to obtain one or more solutions.
1886 solutions = self._strategy.execute(**extra_options)
1888 # Report how many solutions were obtained, select the first.
1889 if isinstance(solutions, list):
1890 assert all(isinstance(s, Solution) for s in solutions)
1892 if not solutions:
1893 raise SolutionFailure(
1894 2, "The solver returned an empty list of solutions.")
1896 solution = solutions[0]
1898 if verbose:
1899 print("Selecting the first of {} solutions obtained for "
1900 "processing.".format(len(solutions)))
1901 else:
1902 assert isinstance(solutions, Solution)
1903 solution = solutions
1905 # Report claimed solution state.
1906 if verbose:
1907 print("Solver claims {} solution for {} problem.".format(
1908 solution.claimedStatus, solution.problemStatus))
1910 # Validate the primal solution.
1911 if options.primals:
1912 if solution.primalStatus != SS_OPTIMAL:
1913 raise SolutionFailure(3, "Primal solution state claimed {} "
1914 "but optimality is required (primals=True)."
1915 .format(solution.primalStatus))
1916 elif None in solution.primals.values():
1917 raise SolutionFailure(3, "The primal solution is incomplete"
1918 " but full primals are required (primals=True).")
1920 # Validate the dual solution.
1921 if options.duals:
1922 if solution.dualStatus != SS_OPTIMAL:
1923 raise SolutionFailure(4, "Dual solution state claimed {} "
1924 "but optimality is required (duals=True).".format(
1925 solution.dualStatus))
1926 elif None in solution.duals.values():
1927 raise SolutionFailure(4, "The dual solution is incomplete "
1928 "but full duals are required (duals=True).")
1930 if options.apply_solution:
1931 if verbose:
1932 print("Applying the solution.")
1934 # Apply the (first) solution.
1935 solution.apply(snapshotStatus=True)
1937 # Store all solutions produced by the solver.
1938 self._last_solution = solutions
1940 # Report verified solution state.
1941 if verbose:
1942 print("Applied solution is {}.".format(solution.lastStatus))
1944 endTime = time.time()
1945 solveTime = endTime - startTime
1946 searchTime = solution.searchTime
1948 if searchTime:
1949 overhead = (solveTime - searchTime) / searchTime
1950 else:
1951 overhead = float("inf")
1953 if verbose:
1954 print("Search {:.1e}s, solve {:.1e}s, overhead {:.0%}."
1955 .format(searchTime, solveTime, overhead))
1957 return solutions
1959 @deprecated("2.0", reason="Misleading semantics. Maybe "
1960 ":func:`picos.minimize` is what you want.")
1961 def minimize(self, obj, **extra_options):
1962 """Look for a minimizing solution.
1964 Sets the objective to minimize the given objective function and calls
1965 the solver with the given additional options.
1967 :param obj: The objective function to minimize.
1968 :type obj: :class:`~picos.expressions.Expression`
1969 :param extra_options: A sequence of additional solver options.
1971 :returns: A dictionary, see :meth:`~Problem.solve`.
1973 .. warning::
1975 This is equivalent to :meth:`~Problem.set_objective`
1976 followed by :meth:`~Problem.solve` and will thus override
1977 any existing objective function and direction.
1978 """
1979 self.objective = "min", obj
1980 return self.solve(**extra_options)
1982 @deprecated("2.0", reason="Misleading semantics. Maybe "
1983 ":func:`picos.maximize` is what you want.")
1984 def maximize(self, obj, **extra_options):
1985 """Look for a maximization solution.
1987 Sets the objective to maximize the given objective function and calls
1988 the solver with the given additional options.
1990 :param obj: The objective function to maximize.
1991 :type obj: :class:`~picos.expressions.Expression`
1992 :param extra_options: A sequence of additional solver options.
1994 :returns: A dictionary, see :meth:`~Problem.solve`.
1996 .. warning::
1998 This is equivalent to :meth:`~Problem.set_objective`
1999 followed by :meth:`~Problem.solve` and will thus override
2000 any existing objective function and direction.
2001 """
2002 self.objective = "max", obj
2003 return self.solve(**extra_options)
2005 def write_to_file(self, filename, writer="picos"):
2006 """See :func:`picos.modeling.file_out.write`."""
2007 write(self, filename, writer)
2009 # --------------------------------------------------------------------------
2010 # Methods to query the problem.
2011 # TODO: Document removal of is_complex, is_real (also for constraints).
2012 # TODO: Revisit #14: "Interfaces to get primal/dual objective values and
2013 # primal/dual feasiblity (amount of violation).""
2014 # --------------------------------------------------------------------------
2016 def check_current_value_feasibility(self, tol=1e-5, inttol=None):
2017 """Check if the problem is feasibly valued.
2019 Checks whether all variables that appear in constraints are valued and
2020 satisfy both their bounds and the constraints up to the given tolerance.
2022 :param float tol:
2023 Largest tolerated absolute violation of a constraint or variable
2024 bound. If ``None``, then the ``abs_prim_fsb_tol`` solver option is
2025 used.
2027 :param inttol:
2028 DEPRECATED
2030 :returns:
2031 A tuple ``(feasible, violation)`` where ``feasible`` is a bool
2032 stating whether the solution is feasible and ``violation`` is either
2033 ``None``, if ``feasible == True``, or the amount of violation,
2034 otherwise.
2036 :raises picos.uncertain.IntractableWorstCase:
2037 When computing the worst-case (expected) value of the constrained
2038 expression is not supported.
2039 """
2040 if inttol is not None:
2041 throw_deprecation_warning("Variable integrality is now ensured on "
2042 "assignment of a value, so it does not need to be checked via "
2043 "check_current_value_feasibility's old 'inttol' parameter.")
2045 if tol is None:
2046 tol = self._options.abs_prim_fsb_tol
2048 all_cons = list(self._constraints.values())
2049 all_cons += [
2050 variable.bound_constraint for variable in self._variables.values()
2051 if variable.bound_constraint]
2053 largest_violation = 0.0
2055 for constraint in all_cons:
2056 try:
2057 slack = constraint.slack
2058 except IntractableWorstCase as error:
2059 raise IntractableWorstCase("Failed to check worst-case or "
2060 "expected feasibility of {}: {}".format(constraint, error))\
2061 from None
2063 assert isinstance(slack, (float, cvx.matrix, cvx.spmatrix))
2064 if isinstance(slack, (float, cvx.spmatrix)):
2065 slack = cvx.matrix(slack) # Allow min, max.
2067 # HACK: The following works around the fact that the slack of an
2068 # uncertain conic constraint is returned as a vector, even
2069 # when the cone is that of the positive semidefinite matrices,
2070 # in which case the vectorization used is nontrivial (svec).
2071 # FIXME: A similar issue should arise when a linear matrix
2072 # inequality is integrated in a product cone; The product
2073 # cone's slack can then have negative entries but still be
2074 # feasible and declared infeasible here.
2075 # TODO: Add a "violation" interface to Constraint that replaces all
2076 # the logic below.
2077 from ..expressions import Constant, PositiveSemidefiniteCone
2078 if isinstance(constraint,
2079 constraints.uncertain.ScenarioUncertainConicConstraint) \
2080 and isinstance(constraint.cone, PositiveSemidefiniteCone):
2081 hack = True
2082 slack = Constant(slack).desvec.safe_value
2083 else:
2084 hack = False
2086 if isinstance(constraint, constraints.LMIConstraint) or hack:
2087 # Check hermitian-ness of slack.
2088 violation = float(max(abs(slack - slack.H)))
2089 if violation > tol:
2090 largest_violation = max(largest_violation, violation)
2092 # Check positive semidefiniteness of slack.
2093 violation = -float(min(np.linalg.eigvalsh(cvx2np(slack))))
2094 if violation > tol:
2095 largest_violation = max(largest_violation, violation)
2096 else:
2097 violation = -float(min(slack))
2098 if violation > tol:
2099 largest_violation = max(largest_violation, violation)
2101 return (not largest_violation, largest_violation)
2103 # --------------------------------------------------------------------------
2104 # Legacy methods and properties.
2105 # --------------------------------------------------------------------------
2107 _LEGACY_PROPERTY_REASON = "Still used internally by legacy code; will be " \
2108 "removed together with that code."
2110 @property
2111 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2112 def countVar(self):
2113 """The same as :func:`len` applied to :attr:`variables`."""
2114 return len(self._variables)
2116 @property
2117 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2118 def countCons(self):
2119 """The same as :func:`len` applied to :attr:`constraints`."""
2120 return len(self._variables)
2122 @property
2123 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2124 def numberOfVars(self):
2125 """The sum of the dimensions of all referenced variables."""
2126 return sum(variable.dim for variable in self._variables.values())
2128 @property
2129 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2130 def numberLSEConstraints(self):
2131 """Number of :class:`~picos.constraints.LogSumExpConstraint` stored."""
2132 return len([c for c in self._constraints.values()
2133 if isinstance(c, constraints.LogSumExpConstraint)])
2135 @property
2136 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2137 def numberSDPConstraints(self):
2138 """Number of :class:`~picos.constraints.LMIConstraint` stored."""
2139 return len([c for c in self._constraints.values()
2140 if isinstance(c, constraints.LMIConstraint)])
2142 @property
2143 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2144 def numberQuadConstraints(self):
2145 """Number of quadratic constraints stored."""
2146 return len([c for c in self._constraints.values() if isinstance(c, (
2147 constraints.ConvexQuadraticConstraint,
2148 constraints.ConicQuadraticConstraint,
2149 constraints.NonconvexQuadraticConstraint))])
2151 @property
2152 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2153 def numberConeConstraints(self):
2154 """Number of quadratic conic constraints stored."""
2155 return len([c for c in self._constraints.values() if isinstance(
2156 c, (constraints.SOCConstraint, constraints.RSOCConstraint))])
2158 @deprecated("2.0", useInstead="value")
2159 def obj_value(self):
2160 """Objective function value.
2162 :raises AttributeError:
2163 If the problem is a feasibility problem or if the objective function
2164 is not valued. This is legacy behavior. Note that :attr:`value` just
2165 returns :obj:`None` while functions that **do** raise an exception
2166 to denote an unvalued expression would raise
2167 :exc:`~picos.expressions.NotValued` instead.
2168 """
2169 if self._objective.feasibility:
2170 raise AttributeError(
2171 "A feasibility problem has no objective value.")
2173 value = self.value
2175 if self.value is None:
2176 raise AttributeError("The objective {} is not fully valued."
2177 .format(self._objective.function.string))
2178 else:
2179 return value
2181 @deprecated("2.0", useInstead="continuous")
2182 def is_continuous(self):
2183 """Whether all variables are of continuous types."""
2184 return self.continuous
2186 @deprecated("2.0", useInstead="pure_integer")
2187 def is_pure_integer(self):
2188 """Whether all variables are of integral types."""
2189 return self.pure_integer
2191 @deprecated("2.0", useInstead="Problem.options")
2192 def set_all_options_to_default(self):
2193 """Set all solver options to their default value."""
2194 self._options.reset()
2196 @deprecated("2.0", useInstead="Problem.options")
2197 def set_option(self, key, val):
2198 """Set a single solver option to the given value.
2200 :param str key: String name of the option, see below for a list.
2201 :param val: New value for the option.
2202 """
2203 key, val = map_legacy_options({key: val}).popitem()
2204 self._options[key] = val
2206 @deprecated("2.0", useInstead="Problem.options")
2207 def update_options(self, **options):
2208 """Set multiple solver options at once.
2210 :param options: A parameter sequence of options to set.
2211 """
2212 options = map_legacy_options(**options)
2213 for key, val in options.items():
2214 self._options[key] = val
2216 @deprecated("2.0", useInstead="Problem.options")
2217 def verbosity(self):
2218 """Return the problem's current verbosity level."""
2219 return self._options.verbosity
2221 @deprecated("2.0", reason="Variables can now be created independent of "
2222 "problems, and do not need to be added to any problem explicitly.")
2223 def add_variable(
2224 self, name, size=1, vtype='continuous', lower=None, upper=None):
2225 r"""Legacy method to create a PICOS variable.
2227 :param str name: The name of the variable.
2229 :param size:
2230 The shape of the variable.
2231 :type size:
2232 anything recognized by :func:`~picos.expressions.data.load_shape`
2234 :param str vtype:
2235 Domain of the variable. Can be any of
2237 - ``'continuous'`` -- real valued,
2238 - ``'binary'`` -- either zero or one,
2239 - ``'integer'`` -- integer valued,
2240 - ``'symmetric'`` -- symmetric matrix,
2241 - ``'antisym'`` or ``'skewsym'`` -- skew-symmetric matrix,
2242 - ``'complex'`` -- complex matrix,
2243 - ``'hermitian'`` -- complex hermitian matrix.
2245 :param lower:
2246 A lower bound on the variable.
2247 :type lower:
2248 anything recognized by :func:`~picos.expressions.data.load_data`
2250 :param upper:
2251 An upper bound on the variable.
2252 :type upper:
2253 anything recognized by :func:`~picos.expressions.data.load_data`
2255 :returns:
2256 A :class:`~picos.expressions.BaseVariable` instance.
2258 :Example:
2260 >>> from picos import Problem
2261 >>> P = Problem()
2262 >>> x = P.add_variable("x", 3)
2263 >>> x
2264 <3×1 Real Variable: x>
2265 >>> # Variable are not stored inside the problem any more:
2266 >>> P.variables
2267 mappingproxy(OrderedDict())
2268 >>> # They are only part of the problem if they actually appear:
2269 >>> P.set_objective("min", abs(x)**2)
2270 >>> P.variables
2271 mappingproxy(OrderedDict([('x', <3×1 Real Variable: x>)]))
2272 """
2273 if vtype == "continuous":
2274 return expressions.RealVariable(name, size, lower, upper)
2275 elif vtype == "binary":
2276 return expressions.BinaryVariable(name, size)
2277 elif vtype == "integer":
2278 return expressions.IntegerVariable(name, size, lower, upper)
2279 elif vtype == "symmetric":
2280 return expressions.SymmetricVariable(name, size, lower, upper)
2281 elif vtype in ("antisym", "skewsym"):
2282 return expressions.SkewSymmetricVariable(name, size, lower, upper)
2283 elif vtype == "complex":
2284 return expressions.ComplexVariable(name, size)
2285 elif vtype == "hermitian":
2286 return expressions.HermitianVariable(name, size)
2287 elif vtype in ("semiint", "semicont"):
2288 raise NotImplementedError("Variables with legacy types 'semiint' "
2289 "and 'semicont' are not supported anymore as of PICOS 2.0. "
2290 "If you need this functionality back, please open an issue.")
2291 else:
2292 raise ValueError("Unknown legacy variable type '{}'.".format(vtype))
2294 @deprecated("2.0", reason="Whether a problem references a variable is now"
2295 " determined dynamically, so this method has no effect.")
2296 def remove_variable(self, name):
2297 """Does nothing."""
2298 pass
2300 @deprecated("2.0", useInstead="variables")
2301 def set_var_value(self, name, value):
2302 """Set the :attr:`value <.expression.Expression.value>` of a variable.
2304 For a :class:`Problem` ``P``, this is the same as
2305 ``P.variables[name] = value``.
2307 :param str name:
2308 Name of the variable to be valued.
2310 :param value:
2311 The value to be set.
2312 :type value:
2313 anything recognized by :func:`~picos.expressions.data.load_data`
2314 """
2315 try:
2316 variable = self._variables[name]
2317 except KeyError:
2318 raise KeyError("The problem references no variable named '{}'."
2319 .format(name)) from None
2320 else:
2321 variable.value = value
2323 @deprecated("2.0", useInstead="dual")
2324 def as_dual(self):
2325 """Return the Lagrangian dual problem of the standardized problem."""
2326 return self.dual
2329# --------------------------------------
2330__all__ = api_end(_API_START, globals())