Coverage for picos/modeling/problem.py: 76.24%
766 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +0000
1# ------------------------------------------------------------------------------
2# Copyright (C) 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 complex = [
522 constraints.ComplexAffineConstraint,
523 constraints.ComplexLMIConstraint]
525 if objective is None:
526 if not C:
527 base = "Empty Problem"
528 elif C.issubset(set(linear)):
529 base = "Linear Feasibility Problem"
530 else:
531 base = "Feasibility Problem"
532 elif isinstance(objective, expressions.AffineExpression):
533 if not C:
534 if objective.constant:
535 base = "Constant Problem"
536 else:
537 base = "Linear Program" # Could have variable bounds.
538 elif C.issubset(set(linear)):
539 base = "Linear Program"
540 elif C.issubset(set(linear + quadconic)):
541 base = "Second Order Cone Program"
542 elif C.issubset(set(linear + sdp)):
543 base = "Semidefinite Program"
544 elif C.issubset(set(linear + [constraints.LogSumExpConstraint])):
545 base = "Geometric Program"
546 elif C.issubset(set(linear + exponential)):
547 base = "Exponential Program"
548 elif C.issubset(set(linear + quadratic)):
549 base = "Quadratically Constrained Program"
550 elif isinstance(objective, expressions.QuadraticExpression):
551 if C.issubset(set(linear)):
552 base = "Quadratic Program"
553 elif C.issubset(set(linear + quadratic)):
554 base = "Quadratically Constrained Quadratic Program"
555 elif isinstance(objective, expressions.LogSumExp):
556 if C.issubset(set(linear + [constraints.LogSumExpConstraint])):
557 base = "Geometric Program"
559 if self.continuous:
560 integrality = ""
561 elif self.pure_integer:
562 integrality = "Integer "
563 else:
564 integrality = "Mixed-Integer "
566 if any(c in complex for c in C):
567 complexity = "Complex "
568 else:
569 complexity = ""
571 return "{}{}{}".format(complexity, integrality, base)
573 @property
574 def dual(self):
575 """The Lagrangian dual problem of the standardized problem.
577 More precisely, this property invokes the following:
579 1. The primal problem is posed as an equivalent conic standard form
580 minimization problem, with variable bounds expressed as additional
581 constraints.
582 2. The Lagrangian dual problem of the reposed primal is computed.
583 3. The optimization direction and objective function sign of the dual
584 are adjusted such that, given strong duality and primal feasibility,
585 the optimal values of both problems are equal. In particular, if the
586 primal problem is a minimization or a maximization problem, the dual
587 problem returned will be the respective other.
589 :raises ~picos.modeling.strategy.NoStrategyFound:
590 If no reformulation strategy was found.
592 .. note::
594 This property is intended for educational purposes.
595 If you want to solve the primal problem via its dual, use the
596 :ref:`dualize <option_dualize>` option instead.
597 """
598 from ..reforms import Dualization
599 return self.reformulated(Dualization.SUPPORTED, dualize=True)
601 @property
602 def conic_form(self):
603 """The problem in conic form.
605 Reformulates the problem such that the objective is affine and all
606 constraints are :class:`~.constraints.ConicConstraint` instances.
608 :raises ~picos.modeling.strategy.NoStrategyFound:
609 If no reformulation strategy was found.
611 :Example:
613 >>> from picos import Problem, RealVariable
614 >>> x = RealVariable("x", 2)
615 >>> P = Problem()
616 >>> P.set_objective("min", abs(x)**2)
617 >>> print(P)
618 Quadratic Program
619 minimize ‖x‖²
620 over
621 2×1 real variable x
622 >>> print(P.conic_form)# doctest: +ELLIPSIS
623 Second Order Cone Program
624 minimize __..._t
625 over
626 1×1 real variable __..._t
627 2×1 real variable x
628 subject to
629 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0
631 .. note::
633 This property is intended for educational purposes.
634 You do not need to use it when solving a problem as PICOS will
635 perform the necessary reformulations automatically.
636 """
637 return self.reformulated(self.CONIC_FORM)
639 # --------------------------------------------------------------------------
640 # Python special methods, except __init__.
641 # --------------------------------------------------------------------------
643 @property
644 def _var_groups(self):
645 """Support :meth:`__str__`."""
646 vars_by_type = {}
647 for var in self._variables.values():
648 vtype = type(var).__name__
649 shape = var.shape
650 bound = tuple(bool(bound) for bound in var.bound_dicts)
651 index = (vtype, shape, bound)
653 vars_by_type.setdefault(index, set())
654 vars_by_type[index].add(var)
656 groups = []
657 for index in sorted(vars_by_type.keys()):
658 groups.append(natsorted(vars_by_type[index], key=lambda v: v.name))
660 return groups
662 @property
663 def _prm_groups(self):
664 """Support :meth:`__str__`."""
665 prms_by_type = {}
666 for prm in self._parameters.values():
667 vtype = type(prm).__name__
668 shape = prm.shape
669 index = (vtype, shape)
671 prms_by_type.setdefault(index, set())
672 prms_by_type[index].add(prm)
674 groups = []
675 for index in sorted(prms_by_type.keys()):
676 groups.append(natsorted(prms_by_type[index], key=lambda v: v.name))
678 return groups
680 @lru_cache()
681 def _mtb_group_string(self, group):
682 """Support :meth:`__str__`."""
683 if len(group) == 0:
684 return "[no mutables]"
686 if len(group) == 1:
687 return group[0].long_string
689 try:
690 template, data = parameterized_string(
691 [mtb.long_string for mtb in group])
692 except ValueError:
693 # HACK: Use the plural of the type string (e.g. "real variables").
694 type_string = group[0]._get_type_string_base().lower()
695 base_string = group[0].long_string.replace(
696 type_string, type_string + "s")
698 # HACK: Move any bound string to the end.
699 match = re.match(r"([^(]*)( \([^)]*\))", base_string)
700 if match:
701 base_string = match[1]
702 bound_string = match[2]
703 else:
704 bound_string = ""
706 return base_string \
707 + ", " + ", ".join([v.name for v in group[1:]]) + bound_string
708 else:
709 return glyphs.forall(template, data)
711 @lru_cache()
712 def _con_group_string(self, group):
713 """Support :meth:`__str__`."""
714 if len(group) == 0:
715 return "[no constraints]"
717 if len(group) == 1:
718 return str(group[0])
720 try:
721 template, data = parameterized_string([str(con) for con in group])
722 except ValueError:
723 return "[{} constraints (1st: {})]".format(len(group), group[0])
724 else:
725 return glyphs.forall(template, data)
727 def __repr__(self):
728 if self._name:
729 return glyphs.repr2(self.type, self._name)
730 else:
731 return glyphs.repr1(self.type)
733 def __str__(self):
734 # Print problem name (if available) and type.
735 if self._name:
736 string = "{} ({})\n".format(self._name, self.type)
737 else:
738 string = "{}\n".format(self.type)
740 # Print objective.
741 string += " {}\n".format(self._objective)
743 wrapper = TextWrapper(
744 initial_indent=" "*4,
745 subsequent_indent=" "*6,
746 break_long_words=False,
747 break_on_hyphens=False)
749 # Print variables.
750 if self._variables:
751 string += " {}\n".format(
752 "for" if self._objective.direction == "find" else "over")
753 for group in self._var_groups:
754 string += wrapper.fill(self._mtb_group_string(tuple(group)))
755 string += "\n"
757 # Print constraints.
758 if self._constraints:
759 string += " subject to\n"
760 for index, group in enumerate(self._con_groups):
761 string += wrapper.fill(self._con_group_string(tuple(group)))
762 string += "\n"
764 # Print parameters.
765 if self._parameters:
766 string += " given\n"
767 for group in self._prm_groups:
768 string += wrapper.fill(self._mtb_group_string(tuple(group)))
769 string += "\n"
771 return string.rstrip("\n")
773 def __iadd__(self, constraints):
774 """See :meth:`require`."""
775 if isinstance(constraints, tuple):
776 self.require(*constraints)
777 else:
778 self.require(constraints)
780 return self
782 # --------------------------------------------------------------------------
783 # Bookkeeping methods.
784 # --------------------------------------------------------------------------
786 def _register_mutables(self, mtbs):
787 """Register the mutables of an objective function or constraint."""
788 # Register every mutable at most once per call.
789 if not isinstance(mtbs, (set, frozenset)):
790 raise TypeError("Mutable registry can (un)register a mutable "
791 "only once per call, so the argument must be a set type.")
793 # Retrieve old and new mutables as mapping from name to object.
794 old_mtbs = self._mutables
795 new_mtbs = OrderedDict(
796 (mtb.name, mtb) for mtb in sorted(mtbs, key=(lambda m: m.name)))
797 new_vars = OrderedDict((name, mtb) for name, mtb in new_mtbs.items()
798 if isinstance(mtb, BaseVariable))
799 new_prms = OrderedDict((name, mtb) for name, mtb in new_mtbs.items()
800 if not isinstance(mtb, BaseVariable))
802 # Check for mutable name clashes within the new set.
803 if len(new_mtbs) != len(mtbs):
804 raise ValueError(
805 "The object you are trying to add to a problem contains "
806 "multiple mutables of the same name. This is not allowed.")
808 # Check for mutable name clashes with existing mutables.
809 for name in set(old_mtbs).intersection(set(new_mtbs)):
810 if old_mtbs[name] is not new_mtbs[name]:
811 raise ValueError("Cannot register the mutable {} with the "
812 "problem because it already tracks another mutable with "
813 "the same name.".format(name))
815 # Keep track of new mutables.
816 self._mutables.update(new_mtbs)
817 self._variables.update(new_vars)
818 self._parameters.update(new_prms)
820 # Count up the mutable references.
821 for mtb in mtbs:
822 self._mtb_count.setdefault(mtb, 0)
823 self._mtb_count[mtb] += 1
825 def _unregister_mutables(self, mtbs):
826 """Unregister the mutables of an objective function or constraint."""
827 # Unregister every mutable at most once per call.
828 if not isinstance(mtbs, (set, frozenset)):
829 raise TypeError("Mutable registry can (un)register a mutable "
830 "only once per call, so the argument must be a set type.")
832 for mtb in mtbs:
833 name = mtb.name
835 # Make sure the mutable is properly registered.
836 assert name in self._mutables and mtb in self._mtb_count, \
837 "Tried to unregister a mutable that is not registered."
838 assert self._mtb_count[mtb] >= 1, \
839 "Found a nonpostive mutable count."
841 # Count down the mutable references.
842 self._mtb_count[mtb] -= 1
844 # Remove a mutable with a reference count of zero.
845 if not self._mtb_count[mtb]:
846 self._mtb_count.pop(mtb)
847 self._mutables.pop(name)
849 if isinstance(mtb, BaseVariable):
850 self._variables.pop(name)
851 else:
852 self._parameters.pop(name)
854 # --------------------------------------------------------------------------
855 # Methods to manipulate the objective function and its direction.
856 # --------------------------------------------------------------------------
858 def set_objective(self, direction=None, expression=None):
859 """Set the optimization direction and objective function of the problem.
861 :param str direction:
862 Case insensitive search direction string. One of
864 - ``"min"`` or ``"minimize"``,
865 - ``"max"`` or ``"maximize"``,
866 - ``"find"`` or :obj:`None` (for a feasibility problem).
868 :param ~picos.expressions.Expression expression:
869 The objective function. Must be :obj:`None` for a feasibility
870 problem.
871 """
872 self.objective = direction, expression
874 # --------------------------------------------------------------------------
875 # Methods to add, retrieve and remove constraints.
876 # --------------------------------------------------------------------------
878 def _lookup_constraint(self, idOrIndOrCon):
879 """Look for a constraint with the given identifier.
881 Given a constraint object or ID or offset or a constraint group index or
882 index pair, returns a matching (list of) constraint ID(s) that is (are)
883 part of the problem.
884 """
885 if isinstance(idOrIndOrCon, int):
886 if idOrIndOrCon in self._constraints:
887 # A valid ID.
888 return idOrIndOrCon
889 elif idOrIndOrCon < len(self._constraints):
890 # An offset.
891 return list(self._constraints.keys())[idOrIndOrCon]
892 else:
893 raise LookupError(
894 "The problem has no constraint with ID or offset {}."
895 .format(idOrIndOrCon))
896 elif isinstance(idOrIndOrCon, constraints.Constraint):
897 # A constraint object.
898 id = idOrIndOrCon.id
899 if id in self._constraints:
900 return id
901 else:
902 raise KeyError("The constraint '{}' is not part of the problem."
903 .format(idOrIndOrCon))
904 elif isinstance(idOrIndOrCon, tuple) or isinstance(idOrIndOrCon, list):
905 if len(idOrIndOrCon) == 1:
906 groupIndex = idOrIndOrCon[0]
907 if groupIndex < len(self._con_groups):
908 return [c.id for c in self._con_groups[groupIndex]]
909 else:
910 raise IndexError("Constraint group index out of range.")
911 elif len(idOrIndOrCon) == 2:
912 groupIndex, groupOffset = idOrIndOrCon
913 if groupIndex < len(self._con_groups):
914 group = self._con_groups[groupIndex]
915 if groupOffset < len(group):
916 return group[groupOffset].id
917 else:
918 raise IndexError(
919 "Constraint group offset out of range.")
920 else:
921 raise IndexError("Constraint group index out of range.")
922 else:
923 raise TypeError("If looking up constraints by group, the index "
924 "must be a tuple or list of length at most two.")
925 else:
926 raise TypeError("Argument of type '{}' not supported when looking "
927 "up constraints".format(type(idOrIndOrCon)))
929 def get_constraint(self, idOrIndOrCon):
930 """Return a (list of) constraint(s) of the problem.
932 :param idOrIndOrCon: One of the following:
934 * A constraint object. It will be returned when the constraint is
935 part of the problem, otherwise a KeyError is raised.
936 * The integer ID of the constraint.
937 * The integer offset of the constraint in the list of all
938 constraints that are part of the problem, in the order that they
939 were added.
940 * A list or tuple of length 1. Its only element is the index of a
941 constraint group (of constraints that were added together), where
942 groups are indexed in the order that they were added to the
943 problem. The whole group is returned as a list of constraints.
944 That list has the constraints in the order that they were added.
945 * A list or tuple of length 2. The first element is a constraint
946 group offset as above, the second an offset within that list.
948 :type idOrIndOrCon: picos.constraints.Constraint or int or tuple or list
950 :returns: A :class:`constraint <picos.constraints.Constraint>` or a list
951 thereof.
953 :Example:
955 >>> import picos as pic
956 >>> import cvxopt as cvx
957 >>> from pprint import pprint
958 >>> prob=pic.Problem()
959 >>> x=[prob.add_variable('x[{0}]'.format(i),2) for i in range(5)]
960 >>> y=prob.add_variable('y',5)
961 >>> Cx=prob.add_list_of_constraints([(1|x[i]) < y[i] for i in range(5)])
962 >>> Cy=prob.add_constraint(y>0)
963 >>> print(prob)
964 Linear Feasibility Problem
965 find an assignment
966 for
967 2×1 real variable x[i] ∀ i ∈ [0…4]
968 5×1 real variable y
969 subject to
970 ∑(x[i]) ≤ y[i] ∀ i ∈ [0…4]
971 y ≥ 0
972 >>> # Retrieve the second constraint, indexed from zero:
973 >>> prob.get_constraint(1)
974 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>
975 >>> # Retrieve the fourth consraint from the first group:
976 >>> prob.get_constraint((0,3))
977 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>
978 >>> # Retrieve the whole first group of constraints:
979 >>> pprint(prob.get_constraint((0,)))
980 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
981 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>,
982 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
983 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
984 <1×1 Affine Constraint: ∑(x[4]) ≤ y[4]>]
985 >>> # Retrieve the second "group", containing just one constraint:
986 >>> prob.get_constraint((1,))
987 [<5×1 Affine Constraint: y ≥ 0>]
988 """
989 idOrIds = self._lookup_constraint(idOrIndOrCon)
991 if isinstance(idOrIds, list):
992 return [self._constraints[id] for id in idOrIds]
993 else:
994 return self._constraints[idOrIds]
996 def add_constraint(self, constraint, key=None):
997 """Add a single constraint to the problem and return it.
999 :param constraint:
1000 The constraint to be added.
1001 :type constraint:
1002 :class:`Constraint <picos.constraints.Constraint>`
1004 :param key: DEPRECATED
1006 :returns:
1007 The constraint that was added to the problem.
1009 .. note::
1011 This method is superseded by the more compact and more flexible
1012 :meth:`require` method or, at your preference, the ``+=`` operator.
1013 """
1014 # Handle deprecated 'key' parameter.
1015 if key is not None:
1016 throw_deprecation_warning(
1017 "Naming constraints is currently not supported.")
1019 # Register the constraint.
1020 self._constraints[constraint.id] = constraint
1021 self._con_groups.append([constraint])
1023 # Register the constraint's mutables.
1024 self._register_mutables(constraint.mutables)
1026 return constraint
1028 def add_list_of_constraints(self, lst, it=None, indices=None, key=None):
1029 """Add constraints from an iterable to the problem.
1031 :param lst:
1032 Iterable of constraints to add.
1034 :param it: DEPRECATED
1035 :param indices: DEPRECATED
1036 :param key: DEPRECATED
1038 :returns:
1039 A list of all constraints that were added.
1041 :Example:
1043 >>> import picos as pic
1044 >>> import cvxopt as cvx
1045 >>> from pprint import pprint
1046 >>> prob=pic.Problem()
1047 >>> x=[prob.add_variable('x[{0}]'.format(i),2) for i in range(5)]
1048 >>> pprint(x)
1049 [<2×1 Real Variable: x[0]>,
1050 <2×1 Real Variable: x[1]>,
1051 <2×1 Real Variable: x[2]>,
1052 <2×1 Real Variable: x[3]>,
1053 <2×1 Real Variable: x[4]>]
1054 >>> y=prob.add_variable('y',5)
1055 >>> IJ=[(1,2),(2,0),(4,2)]
1056 >>> w={}
1057 >>> for ij in IJ:
1058 ... w[ij]=prob.add_variable('w[{},{}]'.format(*ij),3)
1059 ...
1060 >>> u=pic.new_param('u',cvx.matrix([2,5]))
1061 >>> C1=prob.add_list_of_constraints([u.T*x[i] < y[i] for i in range(5)])
1062 >>> C2=prob.add_list_of_constraints([abs(w[i,j])<y[j] for (i,j) in IJ])
1063 >>> C3=prob.add_list_of_constraints([y[t] > y[t+1] for t in range(4)])
1064 >>> print(prob)
1065 Feasibility Problem
1066 find an assignment
1067 for
1068 2×1 real variable x[i] ∀ i ∈ [0…4]
1069 3×1 real variable w[i,j] ∀ (i,j) ∈ zip([1,2,4],[2,0,2])
1070 5×1 real variable y
1071 subject to
1072 uᵀ·x[i] ≤ y[i] ∀ i ∈ [0…4]
1073 ‖w[i,j]‖ ≤ y[j] ∀ (i,j) ∈ zip([1,2,4],[2,0,2])
1074 y[i] ≥ y[i+1] ∀ i ∈ [0…3]
1076 .. note::
1078 This method is superseded by the more compact and more flexible
1079 :meth:`require` method or, at your preference, the ``+=`` operator.
1080 """
1081 if it is not None or indices is not None or key is not None:
1082 # Deprecated as of 2.0.
1083 throw_deprecation_warning("Arguments 'it', 'indices' and 'key' to "
1084 "add_list_of_constraints are deprecated and ignored.")
1086 added = []
1087 for constraint in lst:
1088 added.append(self.add_constraint(constraint))
1089 self._con_groups.pop()
1091 if added:
1092 self._con_groups.append(added)
1094 return added
1096 def require(self, *constraints, ret=False):
1097 """Add constraints to the problem.
1099 :param constraints:
1100 A sequence of constraints or constraint groups (iterables yielding
1101 constraints) or a mix thereof.
1103 :param bool ret:
1104 Whether to return the added constraints.
1106 :returns:
1107 When ``ret=True``, returns either the single constraint that was
1108 added, the single group of constraint that was added in the form of
1109 a :class:`list` or, when multiple arguments are given, a list of
1110 constraints or constraint groups represented as above. When
1111 ``ret=False``, returns nothing.
1113 :Example:
1115 >>> from picos import Problem, RealVariable
1116 >>> x = RealVariable("x", 5)
1117 >>> P = Problem()
1118 >>> P.require(x >= -1, x <= 1) # Add individual constraints.
1119 >>> P.require([x[i] <= x[i+1] for i in range(4)]) # Add groups.
1120 >>> print(P)
1121 Linear Feasibility Problem
1122 find an assignment
1123 for
1124 5×1 real variable x
1125 subject to
1126 x ≥ [-1]
1127 x ≤ [1]
1128 x[i] ≤ x[i+1] ∀ i ∈ [0…3]
1130 .. note::
1132 For a single constraint ``C``, ``P.require(C)`` may also be written
1133 as ``P += C``. For multiple constraints, ``P.require([C1, C2])`` can
1134 be abbreviated ``P += [C1, C2]`` while ``P.require(C1, C2)`` can be
1135 written as either ``P += (C1, C2)`` or just ``P += C1, C2``.
1136 """
1137 from ..constraints import Constraint
1139 added = []
1140 for constraint in constraints:
1141 if isinstance(constraint, Constraint):
1142 added.append(self.add_constraint(constraint))
1143 else:
1144 try:
1145 if not all(isinstance(c, Constraint) for c in constraint):
1146 raise TypeError
1147 except TypeError:
1148 raise TypeError(
1149 "An argument is neither a constraint nor an iterable "
1150 "yielding constraints.") from None
1151 else:
1152 added.append(self.add_list_of_constraints(constraint))
1154 if ret:
1155 return added[0] if len(added) == 1 else added
1157 def _con_group_index(self, conOrConID):
1158 """Support :meth:`remove_constraint`."""
1159 if isinstance(conOrConID, int):
1160 constraint = self._constraints[conOrConID]
1161 else:
1162 constraint = conOrConID
1164 for i, group in enumerate(self._con_groups):
1165 for j, candidate in enumerate(group):
1166 if candidate is constraint:
1167 return i, j
1169 if constraint in self._constraints.values():
1170 raise RuntimeError("The problem's constraint and constraint group "
1171 "registries are out of sync.")
1172 else:
1173 raise KeyError("The constraint is not part of the problem.")
1175 def remove_constraint(self, idOrIndOrCon):
1176 """Delete a constraint from the problem.
1178 :param idOrIndOrCon: See :meth:`get_constraint`.
1180 :Example:
1182 >>> import picos
1183 >>> from pprint import pprint
1184 >>> P = picos.Problem()
1185 >>> x = [P.add_variable('x[{0}]'.format(i), 2) for i in range(4)]
1186 >>> y = P.add_variable('y', 4)
1187 >>> Cxy = P.add_list_of_constraints(
1188 ... [(1 | x[i]) <= y[i] for i in range(4)])
1189 >>> Cy = P.add_constraint(y >= 0)
1190 >>> Cx0to2 = P.add_list_of_constraints([x[i] <= 2 for i in range(3)])
1191 >>> Cx3 = P.add_constraint(x[3] <= 1)
1192 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1193 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1194 <1×1 Affine Constraint: ∑(x[1]) ≤ y[1]>,
1195 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1196 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1197 <4×1 Affine Constraint: y ≥ 0>,
1198 <2×1 Affine Constraint: x[0] ≤ [2]>,
1199 <2×1 Affine Constraint: x[1] ≤ [2]>,
1200 <2×1 Affine Constraint: x[2] ≤ [2]>,
1201 <2×1 Affine Constraint: x[3] ≤ [1]>]
1202 >>> # Delete the 2nd constraint (counted from 0):
1203 >>> P.remove_constraint(1)
1204 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1205 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1206 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1207 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1208 <4×1 Affine Constraint: y ≥ 0>,
1209 <2×1 Affine Constraint: x[0] ≤ [2]>,
1210 <2×1 Affine Constraint: x[1] ≤ [2]>,
1211 <2×1 Affine Constraint: x[2] ≤ [2]>,
1212 <2×1 Affine Constraint: x[3] ≤ [1]>]
1213 >>> # Delete the 2nd group of constraints, i.e. the constraint y > 0:
1214 >>> P.remove_constraint((1,))
1215 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1216 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1217 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1218 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1219 <2×1 Affine Constraint: x[0] ≤ [2]>,
1220 <2×1 Affine Constraint: x[1] ≤ [2]>,
1221 <2×1 Affine Constraint: x[2] ≤ [2]>,
1222 <2×1 Affine Constraint: x[3] ≤ [1]>]
1223 >>> # Delete the 3rd remaining group of constraints, i.e. x[3] < [1]:
1224 >>> P.remove_constraint((2,))
1225 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1226 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1227 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1228 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1229 <2×1 Affine Constraint: x[0] ≤ [2]>,
1230 <2×1 Affine Constraint: x[1] ≤ [2]>,
1231 <2×1 Affine Constraint: x[2] ≤ [2]>]
1232 >>> # Delete 2nd constraint of the 2nd remaining group, i.e. x[1] < |2|:
1233 >>> P.remove_constraint((1,1))
1234 >>> pprint(list(P.constraints.values()))#doctest: +NORMALIZE_WHITESPACE
1235 [<1×1 Affine Constraint: ∑(x[0]) ≤ y[0]>,
1236 <1×1 Affine Constraint: ∑(x[2]) ≤ y[2]>,
1237 <1×1 Affine Constraint: ∑(x[3]) ≤ y[3]>,
1238 <2×1 Affine Constraint: x[0] ≤ [2]>,
1239 <2×1 Affine Constraint: x[2] ≤ [2]>]
1240 """
1241 idOrIds = self._lookup_constraint(idOrIndOrCon)
1243 removedCons = []
1245 if isinstance(idOrIds, list):
1246 assert idOrIds, "There is an empty constraint group."
1247 groupIndex, _ = self._con_group_index(idOrIds[0])
1248 self._con_groups.pop(groupIndex)
1249 for id in idOrIds:
1250 removedCons.append(self._constraints.pop(id))
1251 else:
1252 constraint = self._constraints.pop(idOrIds)
1253 removedCons.append(constraint)
1254 groupIndex, groupOffset = self._con_group_index(constraint)
1255 group = self._con_groups[groupIndex]
1256 group.pop(groupOffset)
1257 if not group:
1258 self._con_groups.pop(groupIndex)
1260 # Unregister the mutables added by the removed constraints.
1261 for con in removedCons:
1262 self._unregister_mutables(con.mutables)
1264 def remove_all_constraints(self):
1265 """Remove all constraints from the problem.
1267 .. note::
1269 This method does not remove bounds set directly on variables.
1270 """
1271 del self.constraints
1273 # --------------------------------------------------------------------------
1274 # Borderline legacy methods to deal with variables.
1275 # --------------------------------------------------------------------------
1277 _PARAMETERIZED_VARIABLE_REGEX = re.compile(r"^([^[]+)\[([^\]]+)\]$")
1279 def get_variable(self, name):
1280 """Retrieve variables referenced by the problem.
1282 Retrieves either a single variable with the given name or a group of
1283 variables all named ``name[param]`` with different values for ``param``.
1284 If the values for ``param`` are the integers from zero to the size of
1285 the group minus one, then the group is returned as a :obj:`list` ordered
1286 by ``param``, otherwise it is returned as a :obj:`dict` with the values
1287 of ``param`` as keys.
1289 .. note::
1291 Since PICOS 2.0, variables are independent of problems and only
1292 appear in a problem for as long as they are referenced by the
1293 problem's objective function or constraints.
1295 :param str name:
1296 The name of a variable, or the base name of a group of variables.
1298 :returns:
1299 A :class:`variable <picos.expressions.BaseVariable>` or a
1300 :class:`list` or :class:`dict` thereof.
1302 :Example:
1304 >>> from picos import Problem, RealVariable
1305 >>> from pprint import pprint
1306 >>> # Create a number of variables with structured names.
1307 >>> vars = [RealVariable("x")]
1308 >>> for i in range(4):
1309 ... vars.append(RealVariable("y[{}]".format(i)))
1310 >>> for key in ["alice", "bob", "carol"]:
1311 ... vars.append(RealVariable("z[{}]".format(key)))
1312 >>> # Make the variables appear in a problem.
1313 >>> P = Problem()
1314 >>> P.set_objective("min", sum([var for var in vars]))
1315 >>> print(P)
1316 Linear Program
1317 minimize x + y[0] + y[1] + y[2] + y[3] + z[alice] + z[bob] + z[carol]
1318 over
1319 1×1 real variables x, y[0], y[1], y[2], y[3], z[alice], z[bob],
1320 z[carol]
1321 >>> # Retrieve the variables from the problem.
1322 >>> P.get_variable("x")
1323 <1×1 Real Variable: x>
1324 >>> pprint(P.get_variable("y"))
1325 [<1×1 Real Variable: y[0]>,
1326 <1×1 Real Variable: y[1]>,
1327 <1×1 Real Variable: y[2]>,
1328 <1×1 Real Variable: y[3]>]
1329 >>> pprint(P.get_variable("z"))
1330 {'alice': <1×1 Real Variable: z[alice]>,
1331 'bob': <1×1 Real Variable: z[bob]>,
1332 'carol': <1×1 Real Variable: z[carol]>}
1333 >>> P.get_variable("z")["alice"] is P.get_variable("z[alice]")
1334 True
1335 """
1336 if name in self._variables:
1337 return self._variables[name]
1338 else:
1339 # Check if the name is really just a basename.
1340 params = []
1341 for otherName in sorted(self._variables.keys()):
1342 match = self._PARAMETERIZED_VARIABLE_REGEX.match(otherName)
1343 if not match:
1344 continue
1345 base, param = match.groups()
1346 if name == base:
1347 params.append(param)
1349 if params:
1350 # Return a list if the parameters are a range.
1351 try:
1352 intParams = sorted([int(p) for p in params])
1353 except ValueError:
1354 pass
1355 else:
1356 if intParams == list(range(len(intParams))):
1357 return [self._variables["{}[{}]".format(name, param)]
1358 for param in intParams]
1360 # Otherwise return a dict.
1361 return {param: self._variables["{}[{}]".format(name, param)]
1362 for param in params}
1363 else:
1364 raise KeyError("The problem references no variable or group of "
1365 "variables named '{}'.".format(name))
1367 def get_valued_variable(self, name):
1368 """Retrieve values of variables referenced by the problem.
1370 This method works the same :meth:`get_variable` but it returns the
1371 variable's :attr:`values <.valuable.Valuable.value>` instead of the
1372 variable objects.
1374 :raises ~picos.expressions.NotValued:
1375 If any of the selected variables is not valued.
1376 """
1377 exp = self.get_variable(name)
1378 if isinstance(exp, list):
1379 for i in range(len(exp)):
1380 exp[i] = exp[i].value
1381 elif isinstance(exp, dict):
1382 for i in exp:
1383 exp[i] = exp[i].value
1384 else:
1385 exp = exp.value
1386 return exp
1388 # --------------------------------------------------------------------------
1389 # Methods to create copies of the problem.
1390 # --------------------------------------------------------------------------
1392 def copy(self):
1393 """Create a deep copy of the problem, using new mutables."""
1394 the_copy = Problem(copyOptions=self._options)
1396 # Duplicate the mutables.
1397 new_mtbs = {mtb: mtb.copy() for name, mtb in self._mutables.items()}
1399 # Make copies of constraints on top of the new mutables.
1400 for group in self._con_groups:
1401 the_copy.add_list_of_constraints(
1402 constraint.replace_mutables(new_mtbs) for constraint in group)
1404 # Make a copy of the objective on top of the new mutables.
1405 direction, function = self._objective
1406 if function is not None:
1407 the_copy.objective = direction, function.replace_mutables(new_mtbs)
1409 return the_copy
1411 def continuous_relaxation(self, copy_other_mutables=True):
1412 """Return a continuous relaxation of the problem.
1414 This is done by replacing integer variables with continuous ones.
1416 :param bool copy_other_mutables:
1417 Whether variables that are already continuous as well as parameters
1418 should be copied. If this is :obj:`False`, then the relxation shares
1419 these mutables with the original problem.
1420 """
1421 the_copy = Problem(copyOptions=self._options)
1423 # Relax integral variables and copy other mutables if requested.
1424 new_mtbs = {}
1425 for name, var in self._mutables.items():
1426 if isinstance(var, expressions.IntegerVariable):
1427 new_mtbs[name] = expressions.RealVariable(
1428 name, var.shape, var._lower, var._upper)
1429 elif isinstance(var, expressions.BinaryVariable):
1430 new_mtbs[name] = expressions.RealVariable(name, var.shape, 0, 1)
1431 else:
1432 if copy_other_mutables:
1433 new_mtbs[name] = var.copy()
1434 else:
1435 new_mtbs[name] = var
1437 # Make copies of constraints on top of the new mutables.
1438 for group in self._con_groups:
1439 the_copy.add_list_of_constraints(
1440 constraint.replace_mutables(new_mtbs) for constraint in group)
1442 # Make a copy of the objective on top of the new mutables.
1443 direction, function = self._objective
1444 if function is not None:
1445 the_copy.objective = direction, function.replace_mutables(new_mtbs)
1447 return the_copy
1449 def clone(self, copyOptions=True):
1450 """Create a semi-deep copy of the problem.
1452 The copy is constrained by the same constraint objects and has the same
1453 objective function and thereby references the existing variables and
1454 parameters that appear in these objects.
1456 The clone can be modified to describe a new problem but when its
1457 variables and parameters are valued, in particular when a solution is
1458 applied to the new problem, then the same values are found in the
1459 corresponding variables and parameters of the old problem. If this is
1460 not a problem to you, then cloning can be much faster than copying.
1462 :param bool copyOptions:
1463 Whether to make an independent copy of the problem's options.
1464 Disabling this will apply any option changes to the original problem
1465 as well but yields a (very small) reduction in cloning time.
1466 """
1467 # Start with a shallow copy of self.
1468 # TODO: Consider adding Problem.__new__ to speed this up further.
1469 theClone = pycopy.copy(self)
1471 # Make the constraint registry independent.
1472 theClone._constraints = self._constraints.copy()
1473 theClone._con_groups = []
1474 for group in self._con_groups:
1475 theClone._con_groups.append(pycopy.copy(group))
1477 # Make the mutable registry independent.
1478 theClone._mtb_count = self._mtb_count.copy()
1479 theClone._mutables = self._mutables.copy()
1480 theClone._variables = self._variables.copy()
1481 theClone._parameters = self._parameters.copy()
1483 # Reset the clone's solution strategy and last solution.
1484 theClone._strategy = None
1486 # Make the solver options independent, if requested.
1487 if copyOptions:
1488 theClone._options = self._options.copy()
1490 # NOTE: No need to change the following attributes:
1491 # - objective: Is immutable as a tuple.
1492 # - _last_solution: Remains as valid as it is.
1494 return theClone
1496 # --------------------------------------------------------------------------
1497 # Methods to solve or export the problem.
1498 # --------------------------------------------------------------------------
1500 def prepared(self, steps=None, **extra_options):
1501 """Perform a dry-run returning the reformulated (prepared) problem.
1503 This behaves like :meth:`solve` in that it takes a number of additional
1504 temporary options, finds a solution strategy matching the problem and
1505 options, and performs the strategy's reformulations in turn to obtain
1506 modified problems. However, it stops after the given number of steps and
1507 never hands the reformulated problem to a solver. Instead of a solution,
1508 :meth:`prepared` then returns the last reformulated problem.
1510 Unless this method returns the problem itself, the special attributes
1511 ``prepared_strategy`` and ``prepared_steps`` are added to the returned
1512 problem. They then contain the (partially) executed solution strategy
1513 and the number of performed reformulations, respectively.
1515 :param int steps:
1516 Number of reformulations to perform. :obj:`None` means as many as
1517 there are. If this parameter is :math:`0`, then the problem itself
1518 is returned. If it is :math:`1`, then only the implicit first
1519 reformulation :class:`~.reform_options.ExtraOptions` is executed,
1520 which may also output the problem itself, depending on
1521 ``extra_options``.
1523 :param extra_options:
1524 Additional solver options to use with this dry-run only.
1526 :returns:
1527 The reformulated problem, with ``extra_options`` set unless they
1528 were "consumed" by a reformulation (e.g.
1529 :ref:`option_dualize <option_dualize>`).
1531 :raises ~picos.modeling.strategy.NoStrategyFound:
1532 If no solution strategy was found.
1534 :raises ValueError:
1535 If there are not as many reformulation steps as requested.
1537 :Example:
1539 >>> from picos import Problem, RealVariable
1540 >>> x = RealVariable("x", 2)
1541 >>> P = Problem()
1542 >>> P.set_objective("min", abs(x)**2)
1543 >>> Q = P.prepared(solver = "cvxopt")
1544 >>> print(Q.prepared_strategy) # Show prepared reformulation steps.
1545 1. ExtraOptions
1546 2. EpigraphReformulation
1547 3. SquaredNormToConicReformulation
1548 4. CVXOPTSolver
1549 >>> Q.prepared_steps # Check how many steps have been performed.
1550 3
1551 >>> print(P)
1552 Quadratic Program
1553 minimize ‖x‖²
1554 over
1555 2×1 real variable x
1556 >>> print(Q)# doctest: +ELLIPSIS
1557 Second Order Cone Program
1558 minimize __..._t
1559 over
1560 1×1 real variable __..._t
1561 2×1 real variable x
1562 subject to
1563 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0
1564 """
1565 from .strategy import Strategy
1567 # Produce a strategy for the clone.
1568 strategy = Strategy.from_problem(self, **extra_options)
1569 numReforms = len(strategy.reforms)
1571 if steps is None:
1572 steps = numReforms
1574 if steps == 0:
1575 return self
1576 elif steps > numReforms:
1577 raise ValueError("The pipeline {} has only {} reformulation steps "
1578 "to choose from.".format(strategy, numReforms))
1580 # Replace the successor of the last reformulation with a dummy solver.
1581 lastReform = strategy.reforms[steps - 1]
1582 oldSuccessor = lastReform.successor
1583 lastReform.successor = type("DummySolver", (), {
1584 "execute": lambda self: Solution(
1585 {}, solver="dummy", vectorizedPrimals=True)})()
1587 # Execute the cut-short strategy.
1588 strategy.execute(**extra_options)
1590 # Repair the last reformulation.
1591 lastReform.successor = oldSuccessor
1593 # Retrieve and augment the output problem (unless it's self).
1594 output = lastReform.output
1595 if output is not self:
1596 output.prepared_strategy = strategy
1597 output.prepared_steps = steps
1599 return output
1601 def reformulated(self, specification, **extra_options):
1602 r"""Return the problem reformulated to match a specification.
1604 Internally this creates a dummy solver accepting problems of the desired
1605 form and then calls :meth:`prepared` with the dummy solver passed via
1606 :ref:`option_ad_hoc_solver <option_ad_hoc_solver>`. See meth:`prepared`
1607 for more details.
1609 :param specification:
1610 A problem class that the resulting problem must be a member of.
1611 :type specification:
1612 ~picos.modeling.Specification
1614 :param extra_options:
1615 Additional solver options to use with this reformulation only.
1617 :returns:
1618 The reformulated problem, with ``extra_options`` set unless they
1619 were "consumed" by a reformulation (e.g.
1620 :ref:`dualize <option_dualize>`).
1622 :raises ~picos.modeling.strategy.NoStrategyFound:
1623 If no reformulation strategy was found.
1625 :Example:
1627 >>> from picos import Problem, RealVariable
1628 >>> from picos.modeling import Specification
1629 >>> from picos.expressions import AffineExpression
1630 >>> from picos.constraints import (
1631 ... AffineConstraint, SOCConstraint, RSOCConstraint)
1632 >>> # Define the class/specification of second order conic problems:
1633 >>> S = Specification(objectives=[AffineExpression],
1634 ... constraints=[AffineConstraint, SOCConstraint, RSOCConstraint])
1635 >>> # Define a quadratic program and reformulate it:
1636 >>> x = RealVariable("x", 2)
1637 >>> P = Problem()
1638 >>> P.set_objective("min", abs(x)**2)
1639 >>> Q = P.reformulated(S)
1640 >>> print(P)
1641 Quadratic Program
1642 minimize ‖x‖²
1643 over
1644 2×1 real variable x
1645 >>> print(Q)# doctest: +ELLIPSIS
1646 Second Order Cone Program
1647 minimize __..._t
1648 over
1649 1×1 real variable __..._t
1650 2×1 real variable x
1651 subject to
1652 ‖fullroot(‖x‖²)‖² ≤ __..._t ∧ __..._t ≥ 0
1654 .. note::
1656 This method is intended for educational purposes.
1657 You do not need to use it when solving a problem as PICOS will
1658 perform the necessary reformulations automatically.
1659 """
1660 if not isinstance(specification, Specification):
1661 raise TypeError("The desired problem type must be given as a "
1662 "Specification object.")
1664 # Create a placeholder function for abstract methods of a dummy solver.
1665 def placeholder(the_self):
1666 raise RuntimeError("The dummy solver created by "
1667 "Problem.reformulated must not be executed.")
1669 # Declare a dummy solver that accepts specified problems.
1670 DummySolver = type("DummySolver", (Solver,), {
1671 # Abstract class methods.
1672 "supports": classmethod(lambda cls, footprint:
1673 Solver.supports(footprint) and footprint in specification),
1674 "default_penalty": classmethod(lambda cls: 0),
1675 "test_availability": classmethod(lambda cls: None),
1676 "names": classmethod(lambda cls: ("Dummy Solver", "DummySolver",
1677 "Dummy Solver accepting {}".format(specification), None)),
1678 "is_free": classmethod(lambda cls: True),
1680 # Additional class methods needed for an ad-hoc solver.
1681 "penalty": classmethod(lambda cls, options: 0),
1683 # Abstract instance methods.
1684 "reset_problem": lambda self: placeholder(self),
1685 "_import_problem": lambda self: placeholder(self),
1686 "_update_problem": lambda self: placeholder(self),
1687 "_solve": lambda self: placeholder(self)
1688 })
1690 # Ad-hoc the dummy solver and prepare the problem for it.
1691 oldAdHocSolver = self.options.ad_hoc_solver
1692 extra_options["ad_hoc_solver"] = DummySolver
1693 problem = self.prepared(**extra_options)
1695 # Restore the ad_hoc_solver option of the original problem.
1696 problem.options.ad_hoc_solver = oldAdHocSolver
1698 return problem
1700 def solve(self, **extra_options):
1701 """Hand the problem to a solver.
1703 You can select the solver manually with the ``solver`` option. Otherwise
1704 a suitable solver will be selected among those that are available on the
1705 platform.
1707 The default behavior (options ``primals=True``, ``duals=None``) is to
1708 raise a :exc:`~picos.SolutionFailure` when the primal solution is not
1709 found optimal by the solver, while the dual solution is allowed to be
1710 missing or incomplete.
1712 When this method succeeds and unless ``apply_solution=False``, you can
1713 access the solution as follows:
1715 - The problem's :attr:`value` denotes the objective function value.
1716 - The variables' :attr:`~.valuable.Valuable.value` is set according
1717 to the primal solution. You can in fact query the value of any
1718 expression involving valued variables like this.
1719 - The constraints' :attr:`~.constraint.Constraint.dual` is set
1720 according to the dual solution.
1721 - The value of any parameter involved in the problem may have
1722 changed, depending on the parameter.
1724 :param extra_options:
1725 A sequence of additional solver options to use with this solution
1726 search only. In particular, this lets you
1728 - select a solver via the ``solver`` option,
1729 - obtain non-optimal primal solutions by setting ``primals=None``,
1730 - require a complete and optimal dual solution with ``duals=True``,
1731 and
1732 - skip valuing variables or constraints with
1733 ``apply_solution=False``.
1735 :returns ~picos.Solution or list(~picos.Solution):
1736 A solution object or list thereof.
1738 :raises ~picos.SolutionFailure:
1739 In the following cases:
1741 1. No solution strategy was found.
1742 2. Multiple solutions were requested but none were returned.
1743 3. A primal solution was explicitly requested (``primals=True``) but
1744 the primal solution is missing/incomplete or not claimed optimal.
1745 4. A dual solution was explicitly requested (``duals=True``) but
1746 the dual solution is missing/incomplete or not claimed optimal.
1748 The case number is stored in the ``code`` attribute of the
1749 exception.
1750 """
1751 from .strategy import NoStrategyFound, Strategy
1753 startTime = time.time()
1755 extra_options = map_legacy_options(**extra_options)
1756 options = self.options.self_or_updated(**extra_options)
1757 verbose = options.verbosity > 0
1759 with picos_box(show=verbose):
1760 if verbose:
1761 print("Problem type: {}.".format(self.type))
1763 # Reset an outdated strategy.
1764 if self._strategy and not self._strategy.valid(**extra_options):
1765 if verbose:
1766 print("Strategy outdated:\n{}.".format(self._strategy))
1768 self._strategy = None
1770 # Find a new solution strategy, if necessary.
1771 if not self._strategy:
1772 if verbose:
1773 if options.ad_hoc_solver:
1774 solverName = options.ad_hoc_solver.get_via_name()
1775 elif options.solver:
1776 solverName = get_solver(options.solver).get_via_name()
1777 else:
1778 solverName = None
1780 print("Searching a solution strategy{}.".format(
1781 " for {}".format(solverName) if solverName else ""))
1783 try:
1784 self._strategy = Strategy.from_problem(
1785 self, **extra_options)
1786 except NoStrategyFound as error:
1787 s = str(error)
1789 if verbose:
1790 print(s, flush=True)
1792 raise SolutionFailure(1, "No solution strategy found.") \
1793 from error
1795 if verbose:
1796 print("Solution strategy:\n {}".format(
1797 "\n ".join(str(self._strategy).splitlines())))
1798 else:
1799 if verbose:
1800 print("Reusing strategy:\n {}".format(
1801 "\n ".join(str(self._strategy).splitlines())))
1803 # Execute the strategy to obtain one or more solutions.
1804 solutions = self._strategy.execute(**extra_options)
1806 # Report how many solutions were obtained, select the first.
1807 if isinstance(solutions, list):
1808 assert all(isinstance(s, Solution) for s in solutions)
1810 if not solutions:
1811 raise SolutionFailure(
1812 2, "The solver returned an empty list of solutions.")
1814 solution = solutions[0]
1816 if verbose:
1817 print("Selecting the first of {} solutions obtained for "
1818 "processing.".format(len(solutions)))
1819 else:
1820 assert isinstance(solutions, Solution)
1821 solution = solutions
1823 # Report claimed solution state.
1824 if verbose:
1825 print("Solver claims {} solution for {} problem.".format(
1826 solution.claimedStatus, solution.problemStatus))
1828 # Validate the primal solution.
1829 if options.primals:
1830 vars_ = self._variables.values()
1831 if solution.primalStatus != SS_OPTIMAL:
1832 raise SolutionFailure(3, "Primal solution state claimed {} "
1833 "but optimality is required (primals=True)."
1834 .format(solution.primalStatus))
1835 elif None in solution.primals.values() \
1836 or any(var not in solution.primals for var in vars_):
1837 raise SolutionFailure(3, "The primal solution is incomplete"
1838 " but full primals are required (primals=True).")
1840 # Validate the dual solution.
1841 if options.duals:
1842 cons = self._constraints.values()
1843 if solution.dualStatus != SS_OPTIMAL:
1844 raise SolutionFailure(4, "Dual solution state claimed {} "
1845 "but optimality is required (duals=True).".format(
1846 solution.dualStatus))
1847 elif None in solution.duals.values() \
1848 or any(con not in solution.duals for con in cons):
1849 raise SolutionFailure(4, "The dual solution is incomplete "
1850 "but full duals are required (duals=True).")
1852 if options.apply_solution:
1853 if verbose:
1854 print("Applying the solution.")
1856 # Apply the (first) solution.
1857 solution.apply(snapshotStatus=True)
1859 # Store all solutions produced by the solver.
1860 self._last_solution = solutions
1862 # Report verified solution state.
1863 if verbose:
1864 print("Applied solution is {}.".format(solution.lastStatus))
1866 endTime = time.time()
1867 solveTime = endTime - startTime
1868 searchTime = solution.searchTime
1870 if searchTime:
1871 overhead = (solveTime - searchTime) / searchTime
1872 else:
1873 overhead = float("inf")
1875 if verbose:
1876 print("Search {:.1e}s, solve {:.1e}s, overhead {:.0%}."
1877 .format(searchTime, solveTime, overhead))
1879 if settings.RETURN_SOLUTION:
1880 return solutions
1882 def write_to_file(self, filename, writer="picos"):
1883 """See :func:`picos.modeling.file_out.write`."""
1884 write(self, filename, writer)
1886 # --------------------------------------------------------------------------
1887 # Methods to query the problem.
1888 # TODO: Document removal of is_complex, is_real (also for constraints).
1889 # TODO: Revisit #14: "Interfaces to get primal/dual objective values and
1890 # primal/dual feasiblity (amount of violation).""
1891 # --------------------------------------------------------------------------
1893 def check_current_value_feasibility(self, tol=1e-5, inttol=None):
1894 """Check if the problem is feasibly valued.
1896 Checks whether all variables that appear in constraints are valued and
1897 satisfy both their bounds and the constraints up to the given tolerance.
1899 :param float tol:
1900 Largest tolerated absolute violation of a constraint or variable
1901 bound. If ``None``, then the ``abs_prim_fsb_tol`` solver option is
1902 used.
1904 :param inttol:
1905 DEPRECATED
1907 :returns:
1908 A tuple ``(feasible, violation)`` where ``feasible`` is a bool
1909 stating whether the solution is feasible and ``violation`` is either
1910 ``None``, if ``feasible == True``, or the amount of violation,
1911 otherwise.
1913 :raises picos.uncertain.IntractableWorstCase:
1914 When computing the worst-case (expected) value of the constrained
1915 expression is not supported.
1916 """
1917 if inttol is not None:
1918 throw_deprecation_warning("Variable integrality is now ensured on "
1919 "assignment of a value, so it does not need to be checked via "
1920 "check_current_value_feasibility's old 'inttol' parameter.")
1922 if tol is None:
1923 tol = self._options.abs_prim_fsb_tol
1925 all_cons = list(self._constraints.values())
1926 all_cons += [
1927 variable.bound_constraint for variable in self._variables.values()
1928 if variable.bound_constraint]
1930 largest_violation = 0.0
1932 for constraint in all_cons:
1933 try:
1934 slack = constraint.slack
1935 except IntractableWorstCase as error:
1936 raise IntractableWorstCase("Failed to check worst-case or "
1937 "expected feasibility of {}: {}".format(constraint, error))\
1938 from None
1940 assert isinstance(slack, (float, cvx.matrix, cvx.spmatrix))
1941 if isinstance(slack, (float, cvx.spmatrix)):
1942 slack = cvx.matrix(slack) # Allow min, max.
1944 # HACK: The following works around the fact that the slack of an
1945 # uncertain conic constraint is returned as a vector, even
1946 # when the cone is that of the positive semidefinite matrices,
1947 # in which case the vectorization used is nontrivial (svec).
1948 # FIXME: A similar issue should arise when a linear matrix
1949 # inequality is integrated in a product cone; The product
1950 # cone's slack can then have negative entries but still be
1951 # feasible and declared infeasible here.
1952 # TODO: Add a "violation" interface to Constraint that replaces all
1953 # the logic below.
1954 from ..expressions import Constant, PositiveSemidefiniteCone
1955 if isinstance(constraint,
1956 constraints.uncertain.ScenarioUncertainConicConstraint) \
1957 and isinstance(constraint.cone, PositiveSemidefiniteCone):
1958 hack = True
1959 slack = Constant(slack).desvec.safe_value
1960 else:
1961 hack = False
1963 if isinstance(constraint, constraints.LMIConstraint) or hack:
1964 # Check hermitian-ness of slack.
1965 violation = float(max(abs(slack - slack.H)))
1966 if violation > tol:
1967 largest_violation = max(largest_violation, violation)
1969 # Check positive semidefiniteness of slack.
1970 violation = -float(min(np.linalg.eigvalsh(cvx2np(slack))))
1971 if violation > tol:
1972 largest_violation = max(largest_violation, violation)
1973 else:
1974 violation = -float(min(slack))
1975 if violation > tol:
1976 largest_violation = max(largest_violation, violation)
1978 return (not largest_violation, largest_violation)
1980 # --------------------------------------------------------------------------
1981 # Abstract method implementations for the Valuable base class.
1982 # --------------------------------------------------------------------------
1984 def _get_valuable_string(self):
1985 return "problem with {}".format(self._objective._get_valuable_string())
1987 def _get_value(self):
1988 return self._objective._get_value()
1990 # --------------------------------------------------------------------------
1991 # Legacy methods and properties.
1992 # --------------------------------------------------------------------------
1994 _LEGACY_PROPERTY_REASON = "Still used internally by legacy code; will be " \
1995 "removed together with that code."
1997 @property
1998 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
1999 def countVar(self):
2000 """The same as :func:`len` applied to :attr:`variables`."""
2001 return len(self._variables)
2003 @property
2004 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2005 def countCons(self):
2006 """The same as :func:`len` applied to :attr:`constraints`."""
2007 return len(self._variables)
2009 @property
2010 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2011 def numberOfVars(self):
2012 """The sum of the dimensions of all referenced variables."""
2013 return sum(variable.dim for variable in self._variables.values())
2015 @property
2016 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2017 def numberLSEConstraints(self):
2018 """Number of :class:`~picos.constraints.LogSumExpConstraint` stored."""
2019 return len([c for c in self._constraints.values()
2020 if isinstance(c, constraints.LogSumExpConstraint)])
2022 @property
2023 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2024 def numberSDPConstraints(self):
2025 """Number of :class:`~picos.constraints.LMIConstraint` stored."""
2026 return len([c for c in self._constraints.values()
2027 if isinstance(c, constraints.LMIConstraint)])
2029 @property
2030 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2031 def numberQuadConstraints(self):
2032 """Number of quadratic constraints stored."""
2033 return len([c for c in self._constraints.values() if isinstance(c, (
2034 constraints.ConvexQuadraticConstraint,
2035 constraints.ConicQuadraticConstraint,
2036 constraints.NonconvexQuadraticConstraint))])
2038 @property
2039 @deprecated("2.0", reason=_LEGACY_PROPERTY_REASON)
2040 def numberConeConstraints(self):
2041 """Number of quadratic conic constraints stored."""
2042 return len([c for c in self._constraints.values() if isinstance(
2043 c, (constraints.SOCConstraint, constraints.RSOCConstraint))])
2045 @deprecated("2.0", useInstead="value")
2046 def obj_value(self):
2047 """Objective function value.
2049 :raises AttributeError:
2050 If the problem is a feasibility problem or if the objective function
2051 is not valued. This is legacy behavior. Note that :attr:`value` just
2052 returns :obj:`None` while functions that **do** raise an exception
2053 to denote an unvalued expression would raise
2054 :exc:`~picos.expressions.NotValued` instead.
2055 """
2056 if self._objective.feasibility:
2057 raise AttributeError(
2058 "A feasibility problem has no objective value.")
2060 value = self.value
2062 if self.value is None:
2063 raise AttributeError("The objective {} is not fully valued."
2064 .format(self._objective.function.string))
2065 else:
2066 return value
2068 @deprecated("2.0", useInstead="continuous")
2069 def is_continuous(self):
2070 """Whether all variables are of continuous types."""
2071 return self.continuous
2073 @deprecated("2.0", useInstead="pure_integer")
2074 def is_pure_integer(self):
2075 """Whether all variables are of integral types."""
2076 return self.pure_integer
2078 @deprecated("2.0", useInstead="Problem.options")
2079 def set_all_options_to_default(self):
2080 """Set all solver options to their default value."""
2081 self._options.reset()
2083 @deprecated("2.0", useInstead="Problem.options")
2084 def set_option(self, key, val):
2085 """Set a single solver option to the given value.
2087 :param str key: String name of the option, see below for a list.
2088 :param val: New value for the option.
2089 """
2090 key, val = map_legacy_options({key: val}).popitem()
2091 self._options[key] = val
2093 @deprecated("2.0", useInstead="Problem.options")
2094 def update_options(self, **options):
2095 """Set multiple solver options at once.
2097 :param options: A parameter sequence of options to set.
2098 """
2099 options = map_legacy_options(**options)
2100 for key, val in options.items():
2101 self._options[key] = val
2103 @deprecated("2.0", useInstead="Problem.options")
2104 def verbosity(self):
2105 """Return the problem's current verbosity level."""
2106 return self._options.verbosity
2108 @deprecated("2.0", reason="Variables can now be created independent of "
2109 "problems, and do not need to be added to any problem explicitly.")
2110 def add_variable(
2111 self, name, size=1, vtype='continuous', lower=None, upper=None):
2112 r"""Legacy method to create a PICOS variable.
2114 :param str name: The name of the variable.
2116 :param size:
2117 The shape of the variable.
2118 :type size:
2119 anything recognized by :func:`~picos.expressions.data.load_shape`
2121 :param str vtype:
2122 Domain of the variable. Can be any of
2124 - ``'continuous'`` -- real valued,
2125 - ``'binary'`` -- either zero or one,
2126 - ``'integer'`` -- integer valued,
2127 - ``'symmetric'`` -- symmetric matrix,
2128 - ``'antisym'`` or ``'skewsym'`` -- skew-symmetric matrix,
2129 - ``'complex'`` -- complex matrix,
2130 - ``'hermitian'`` -- complex hermitian matrix.
2132 :param lower:
2133 A lower bound on the variable.
2134 :type lower:
2135 anything recognized by :func:`~picos.expressions.data.load_data`
2137 :param upper:
2138 An upper bound on the variable.
2139 :type upper:
2140 anything recognized by :func:`~picos.expressions.data.load_data`
2142 :returns:
2143 A :class:`~picos.expressions.BaseVariable` instance.
2145 :Example:
2147 >>> from picos import Problem
2148 >>> P = Problem()
2149 >>> x = P.add_variable("x", 3)
2150 >>> x
2151 <3×1 Real Variable: x>
2152 >>> # Variable are not stored inside the problem any more:
2153 >>> P.variables
2154 mappingproxy(OrderedDict())
2155 >>> # They are only part of the problem if they actually appear:
2156 >>> P.set_objective("min", abs(x)**2)
2157 >>> P.variables
2158 mappingproxy(OrderedDict([('x', <3×1 Real Variable: x>)]))
2159 """
2160 if vtype == "continuous":
2161 return expressions.RealVariable(name, size, lower, upper)
2162 elif vtype == "binary":
2163 return expressions.BinaryVariable(name, size)
2164 elif vtype == "integer":
2165 return expressions.IntegerVariable(name, size, lower, upper)
2166 elif vtype == "symmetric":
2167 return expressions.SymmetricVariable(name, size, lower, upper)
2168 elif vtype in ("antisym", "skewsym"):
2169 return expressions.SkewSymmetricVariable(name, size, lower, upper)
2170 elif vtype == "complex":
2171 return expressions.ComplexVariable(name, size)
2172 elif vtype == "hermitian":
2173 return expressions.HermitianVariable(name, size)
2174 elif vtype in ("semiint", "semicont"):
2175 raise NotImplementedError("Variables with legacy types 'semiint' "
2176 "and 'semicont' are not supported anymore as of PICOS 2.0. "
2177 "If you need this functionality back, please open an issue.")
2178 else:
2179 raise ValueError("Unknown legacy variable type '{}'.".format(vtype))
2181 @deprecated("2.0", reason="Whether a problem references a variable is now"
2182 " determined dynamically, so this method has no effect.")
2183 def remove_variable(self, name):
2184 """Does nothing."""
2185 pass
2187 @deprecated("2.0", useInstead="variables")
2188 def set_var_value(self, name, value):
2189 """Set the :attr:`~.valuable.Valuable.value` of a variable.
2191 For a :class:`Problem` ``P``, this is the same as
2192 ``P.variables[name] = value``.
2194 :param str name:
2195 Name of the variable to be valued.
2197 :param value:
2198 The value to be set.
2199 :type value:
2200 anything recognized by :func:`~picos.expressions.data.load_data`
2201 """
2202 try:
2203 variable = self._variables[name]
2204 except KeyError:
2205 raise KeyError("The problem references no variable named '{}'."
2206 .format(name)) from None
2207 else:
2208 variable.value = value
2210 @deprecated("2.0", useInstead="dual")
2211 def as_dual(self):
2212 """Return the Lagrangian dual problem of the standardized problem."""
2213 return self.dual
2216# --------------------------------------
2217__all__ = api_end(_API_START, globals())