Coverage for picos/expressions/uncertain/uexp_rand_pwl.py: 70.25%
121 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) 2020 Maximilian Stahlberg
3#
4# This file is part of PICOS.
5#
6# PICOS is free software: you can redistribute it and/or modify it under the
7# terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
12# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17# ------------------------------------------------------------------------------
19"""Implements :class:`RandomExtremumAffine`."""
21import operator
22from abc import abstractmethod
23from collections import namedtuple
25import cvxopt
27from ...apidoc import api_end, api_start
28from ...caching import cached_unary_operator
29from ...constraints import (Constraint,
30 MomentAmbiguousExtremumAffineConstraint,
31 WassersteinAmbiguousExtremumAffineConstraint)
32from ...formatting import arguments
33from ..data import convert_operands
34from ..exp_affine import AffineExpression, Constant
35from ..exp_extremum import (ExtremumBase, MaximumBase, MaximumConvex,
36 MinimumBase, MinimumConcave)
37from ..expression import Expression, refine_operands, validate_prediction
38from .pert_moment import MomentAmbiguitySet
39from .pert_wasserstein import WassersteinAmbiguitySet
40from .uexp_affine import UncertainAffineExpression
41from .uexpression import UncertainExpression
43_API_START = api_start(globals())
44# -------------------------------
47class RandomExtremumAffine(ExtremumBase, UncertainExpression, Expression):
48 """Base class for random convex or concave piecewise linear expressions.
50 .. note::
52 Unlike other uncertain expression types, this class is limited to
53 uncertainty of stochastic nature, where using the expression in a
54 constraint or as an objective function implicitly takes the (worst-case)
55 expectation of the expression. Non-stochastic uncertainty is handled
56 within :class:`~picos.expressions.MaximumConvex` and
57 :class:`~picos.expressions.MinimumConcave` as their behavior, although
58 designed for certain expression types, already encodes the worst-case
59 approach of the robust optimization paradigm.
60 """
62 # --------------------------------------------------------------------------
63 # Additional abstract methods extending (in spirit) ExtremumBase.
64 # --------------------------------------------------------------------------
66 @property
67 @abstractmethod
68 def _certain_class(self):
69 pass
71 # --------------------------------------------------------------------------
72 # Initialization and factory methods.
73 # --------------------------------------------------------------------------
75 def __init__(self, expressions):
76 """Construct a :class:`RandomExtremumAffine`.
78 :param expressions:
79 A collection of uncertain affine expressions whose uncertainty is of
80 stochastic nature.
81 """
82 # Load constant data and refine expressions.
83 expressions = tuple(
84 x.refined if isinstance(x, Expression) else Constant(x)
85 for x in expressions)
87 # Check expression types.
88 if not all(isinstance(x, (AffineExpression, UncertainAffineExpression))
89 for x in expressions):
90 raise TypeError("{} can only denote the extremum of (uncertain) "
91 "affine expressions.".format(self.__class__.__name__))
93 # Check expression dimension.
94 if not all(x.scalar for x in expressions):
95 raise TypeError("{} can only denote the extremum of scalar "
96 "expressions.".format(self.__class__.__name__))
98 perturbations = tuple(set(
99 x.perturbation for x in expressions if x.uncertain))
101 # Check for a unique perturbation parameter.
102 if len(perturbations) > 1:
103 raise ValueError("{} can only denote the extremum of uncertain "
104 "affine expressions that depend on at most one perturbation "
105 "parameter, found {}."
106 .format(self.__class__.__name__, len(perturbations)))
108 perturbation = perturbations[0] if perturbations else None
109 universe = perturbation.universe if perturbation else None
111 # Check for a supported perturbation type.
112 if universe and not universe.distributional:
113 raise TypeError("{} can only represent uncertainty parameterized by"
114 " a distribution or distributional ambiguity set, not {}."
115 .format(self.__class__.__name__, universe.__class__.__name__))
117 typeStr = "{} Uncertain Piecewise Linear Function".format(
118 self._property_word.title())
120 symbStr = self._extremum_glyph(
121 arguments([x.string for x in expressions]))
123 Expression.__init__(self, typeStr, symbStr)
125 self._expressions = expressions
126 self._perturbation = perturbation
128 # --------------------------------------------------------------------------
129 # Abstract method implementations for ExtremumBase.
130 # --------------------------------------------------------------------------
132 @property
133 def expressions(self):
134 """The expressions under the extremum."""
135 return self._expressions
137 # --------------------------------------------------------------------------
138 # Method overridings for UncertainExpression.
139 # --------------------------------------------------------------------------
141 @property
142 def perturbation(self):
143 """Fast override for :class:`~.uexpression.UncertainExpression`."""
144 return self._perturbation
146 # --------------------------------------------------------------------------
147 # Abstract method implementations for Expression, except _predict.
148 # --------------------------------------------------------------------------
150 @cached_unary_operator
151 def _get_refined(self):
152 """Implement :meth:`~.expression.Expression._get_refined`."""
153 if len(self._expressions) == 1:
154 return self._expressions[0]
155 elif all(x.constant for x in self._expressions):
156 return self._extremum(self._expressions, key=lambda x: x.safe_value)
157 elif all(x.certain for x in self._expressions):
158 return self._certain_class(x.refined for x in self._expressions)
159 else:
160 return self
162 Subtype = namedtuple("Subtype", ("argnum", "universe_type"))
164 def _get_subtype(self):
165 """Implement :meth:`~.expression.Expression._get_subtype`."""
166 return self.Subtype(self.argnum, self.universe.type)
168 def _get_value(self):
169 return cvxopt.matrix(self._extremum(
170 x.safe_value for x in self._expressions))
172 # --------------------------------------------------------------------------
173 # Constraint-creating operators and _predict.
174 # --------------------------------------------------------------------------
176 @classmethod
177 def _predict(cls, subtype, relation, other):
178 assert isinstance(subtype, cls.Subtype)
180 convex = issubclass(cls, RandomMaximumAffine)
181 concave = issubclass(cls, RandomMinimumAffine)
183 if relation == operator.__le__:
184 if not convex:
185 return NotImplemented
186 elif relation == operator.__ge__:
187 if not concave:
188 return NotImplemented
189 else:
190 return NotImplemented
192 if not issubclass(other.clstype, AffineExpression) \
193 or other.subtype.dim != 1:
194 return NotImplemented
196 if issubclass(subtype.universe_type.clstype, MomentAmbiguitySet):
197 return MomentAmbiguousExtremumAffineConstraint.make_type(
198 extremum_argnum=subtype.argnum,
199 universe_subtype=subtype.universe_type.subtype)
200 elif issubclass(subtype.universe_type.clstype, WassersteinAmbiguitySet):
201 return WassersteinAmbiguousExtremumAffineConstraint.make_type(
202 extremum_argnum=subtype.argnum,
203 universe_subtype=subtype.universe_type.subtype)
205 return NotImplemented
207 @convert_operands(scalarRHS=True)
208 @validate_prediction
209 @refine_operands()
210 def __le__(self, other):
211 if not self.convex:
212 raise TypeError("Cannot upper-bound the nonconvex expression {}."
213 .format(self.string))
215 if not isinstance(other, AffineExpression):
216 return NotImplemented
218 if isinstance(self.universe, MomentAmbiguitySet):
219 return MomentAmbiguousExtremumAffineConstraint(
220 self, Constraint.LE, other)
221 elif isinstance(self.universe, WassersteinAmbiguitySet):
222 return WassersteinAmbiguousExtremumAffineConstraint(
223 self, Constraint.LE, other)
225 return NotImplemented
227 @convert_operands(scalarRHS=True)
228 @validate_prediction
229 @refine_operands()
230 def __ge__(self, other):
231 if not self.concave:
232 raise TypeError("Cannot lower-bound the nonconcave expression {}."
233 .format(self.string))
235 if not isinstance(other, AffineExpression):
236 return NotImplemented
238 if isinstance(self.universe, MomentAmbiguitySet):
239 return MomentAmbiguousExtremumAffineConstraint(
240 self, Constraint.GE, other)
241 elif isinstance(self.universe, WassersteinAmbiguitySet):
242 return WassersteinAmbiguousExtremumAffineConstraint(
243 self, Constraint.GE, other)
245 return NotImplemented
248class RandomMaximumAffine(MaximumBase, RandomExtremumAffine):
249 """The maximum over a set of random affine expressions."""
251 # --------------------------------------------------------------------------
252 # Abstract method implementations for ExtremumBase.
253 # --------------------------------------------------------------------------
255 @property
256 def _other_class(self):
257 return RandomMinimumAffine
259 # --------------------------------------------------------------------------
260 # Abstract method implementations for RandomExtremumAffine.
261 # --------------------------------------------------------------------------
263 @property
264 def _certain_class(self):
265 return MaximumConvex
268class RandomMinimumAffine(MinimumBase, RandomExtremumAffine):
269 """The minimum over a set of random affine expressions."""
271 # --------------------------------------------------------------------------
272 # Abstract method implementations for ExtremumBase.
273 # --------------------------------------------------------------------------
275 @property
276 def _other_class(self):
277 return RandomMaximumAffine
279 # --------------------------------------------------------------------------
280 # Abstract method implementations for RandomExtremumAffine.
281 # --------------------------------------------------------------------------
283 @property
284 def _certain_class(self):
285 return MinimumConcave
288# --------------------------------------
289__all__ = api_end(_API_START, globals())