Source code for picos.expressions.uncertain.uexpression

# ------------------------------------------------------------------------------
# Copyright (C) 2020 Maximilian Stahlberg
#
# This file is part of PICOS.
#
# PICOS is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------

"""Implements the :class:`UncertainExpression` base class."""

import cvxopt

from ... import glyphs
from ...apidoc import api_end, api_start
from ...caching import cached_property
from ..expression import NotValued

_API_START = api_start(globals())
# -------------------------------


[docs]class IntractableWorstCase(RuntimeError): """Computing a worst-case (expected) value is hard and not supported. Raised by :meth:`~.uexpression.UncertainExpression.worst_case` and methods that depend on it. """
[docs]class UncertainExpression: """Primary base class for uncertainty affected expression types. The secondary base class must be :class:`~.expression.Expression` or a subclass thereof. Uncertain expressions have a distinct behavior when used to form a constraint or when posed as an objective function. The exact behavior depends on the type of uncertainty involved. If the perturbation parameter that describes the uncertainty is confied to a perturbation set, then the worst-case realization of the parameter is assumed when determining feasibility and optimality. If the perturbation parameter is a random variable (whose distribution may itself be ambiguous), then the constraint or objective implicitly considers the expected value of the uncertain expression (under the worst-case distribution). Uncertain expressions are thus used in the contexts of robust optimization, stochastic programming and distributionally robust optimization. """
[docs] @cached_property def perturbation(self): """The parameter controlling the uncertainty, or :obj:`None`.""" from .perturbation import Perturbation perturbations = tuple( prm for prm in self.parameters if isinstance(prm, Perturbation)) if len(perturbations) > 1: raise NotImplementedError("Uncertain expressions may depend " "on at most one perturbation parameter. Found {}." .format(" and ".join(prt.name for prt in perturbations))) return perturbations[0] if perturbations else None
@property def random(self): """Whether the uncertainty is of stochastic nature. See also :attr:`~.perturbation.PerturbationUniverse.distributional`. """ if self.certain: return False else: return self.universe.distributional
[docs] @cached_property def universe(self): """Universe that the perturbation parameter lives in, or :obj:`None`. If this is not :obj:`None`, then this is the same as :attr:`perturbation`.:attr:`~.perturbation.Perturbation.universe`. """ return self.perturbation.universe if self.perturbation else None
@property def certain(self): """Whether the uncertain expression is actually certain.""" return not self.perturbation @property def uncertain(self): """Whether the uncertain expression is in fact uncertain.""" return bool(self.perturbation)
[docs] def worst_case(self, direction): """Find a worst-case realization of the uncertainty for the expression. Expressions that are affected by uncertainty are only partially valued once an optimization solution has been applied. While their decision values are populated with a robust optimal solution, the parameter that controls the uncertainty is not valued unless the user assigned it a particular realization by hand. This method computes a worst-case (expected) value of the expression and returns it together with a realization of the perturbation parameter for which the worst case is attained (or :obj:`None` in the case of stochastic uncertainty). For multidimensional expressions, this method computes the entrywise worst case and returns an attaining realization for each entry. :param str direction: Either ``"min"`` or ``"max"``, denoting the worst-case direction. :returns: A pair ``(value, realization)``. For a scalar expression, ``value`` is its worst-case (expected) value as a :obj:`float` and ``realization`` is a realization of the :attr:`perturbation` parameter that attains this worst case as a :obj:`float` or CVXOPT matrix. For a multidimensional expression, ``value`` is a CVXOPT dense matrix denoting the entrywise worst-case values and ``realization`` is a :obj:`tuple` of attaining realizations corresponding to the expression vectorized in in column-major order. Lastly, ``realization`` is :obj:`None` if the expression is :attr:`certain` or when its uncertainty is of stochastic nature. :raises picos.NotValued: When the decision variables that occur in the expression are not fully valued. :raises picos.uncertain.IntractableWorstCase: When computing the worst-case (expected) value is not supported, in particular when it would require solving a nonconvex problem. :raises RuntimeError: When the computation is supported but fails. """ if not all(var.valued for var in self.variables): raise NotValued("Not all decision variables that occur in the " "uncertain expression {} are valued, so PICOS cannot compute " "its worst-case (expected) value.".format(self.string)) if self.certain: return self.safe_value, None outcome = self.frozen(self.variables) assert outcome.mutables == set([self.perturbation]) if self.scalar: return self.universe.worst_case(outcome, direction) else: values, realizations = zip(*( self.universe.worst_case(outcome[i], direction) for i in range(len(self)))) return cvxopt.matrix(values, self.shape), realizations
[docs] def worst_case_value(self, direction): """A shorthand for the first value returned by :meth:`worst_case`.""" return self.worst_case(direction)[0]
[docs] def worst_case_string(self, direction): """A string describing the expression within a worst-case context. :param str direction: Either ``"min"`` or ``"max"``, denoting the worst-case direction. """ # NOTE: The following distinguishes only RO and DRO and needs to be # extended when SP models are supported. if self.random: over = glyphs.probdist(self.perturbation.string) base = glyphs.exparg(self.perturbation.string, self.string) else: over = self.perturbation.string base = self.string if direction == "min": return glyphs.minarg(over, base) elif direction == "max": return glyphs.maxarg(over, base) else: raise ValueError("Invalid direction.")
# -------------------------------------- __all__ = api_end(_API_START, globals())