Coverage for picos/expressions/uncertain/uexp_affine.py: 89.74%
156 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:`UncertainAffineExpression`."""
21import operator
22from collections import namedtuple
24from ...apidoc import api_end, api_start
25from ...caching import cached_property, cached_unary_operator
26from ...constraints.uncertain import (ConicallyUncertainAffineConstraint,
27 ScenarioUncertainConicConstraint)
28from ..data import convert_operands, cvxopt_K
29from ..exp_affine import AffineExpression
30from ..exp_biaffine import BiaffineExpression
31from ..expression import ExpressionType, refine_operands, validate_prediction
32from ..variables import BaseVariable
33from .uexpression import UncertainExpression
35# NOTE: May not import ConicPerturbationSet from .pert_conic here because the
36# latter imports from .perturbation which in turn needs to import
37# UncertainAffineExpression as a base class of Perturbation from here.
40_API_START = api_start(globals())
41# -------------------------------
44class UncertainAffineExpression(UncertainExpression, BiaffineExpression):
45 r"""A multidimensional uncertain affine expression.
47 This expression has the form
49 .. math::
51 A(x,\theta) = B(x,\theta) + P(x) + Q(\theta) + C
53 where :math:`B`, :math:`P`, :math:`Q`, :math:`C` and :math:`x` are defined
54 as for the :class:`~.exp_biaffine.BiaffineExpression` base class and
55 :math:`\theta` is an uncertain perturbation parameter confined to
56 (distributed according to) a perturbation set (distribution) :math:`\Theta`.
58 If no coefficient matrices defining :math:`B` and :math:`P` are provided,
59 then this expression represents uncertain data confined to an uncertainty
60 set :math:`\{Q(\theta) + C \mid \theta \in \Theta\}` (distributed according
61 to :math:`Q(\Theta) + C`) where :math:`C` can be understood as a nominal
62 data value while :math:`Q(\theta)` quantifies the uncertainty on the data.
63 """
65 # --------------------------------------------------------------------------
66 # Abstract method implementations for Expression, except _predict.
67 # --------------------------------------------------------------------------
69 Subtype = namedtuple("Subtype", ("shape", "universe_type"))
70 Subtype.dim = property(lambda self: self.shape[0] * self.shape[1])
72 def _get_subtype(self):
73 """Implement :meth:`~.expression.Expression._get_subtype`."""
74 return self.Subtype(self._shape, self.universe.type)
76 # --------------------------------------------------------------------------
77 # Method overridings for Expression.
78 # --------------------------------------------------------------------------
80 def _get_refined(self):
81 """Implement :meth:`~.expression.Expression._get_refined`."""
82 if self.certain:
83 return AffineExpression(self.string, self.shape, self._coefs)
84 else:
85 return self
87 # --------------------------------------------------------------------------
88 # Abstract method implementations for BiaffineExpression.
89 # --------------------------------------------------------------------------
91 @classmethod
92 def _get_bilinear_terms_allowed(cls):
93 """Implement for :class:`~.exp_biaffine.BiaffineExpression`."""
94 return True
96 @classmethod
97 def _get_parameters_allowed(cls):
98 """Implement for :class:`~.exp_biaffine.BiaffineExpression`."""
99 return True
101 @classmethod
102 def _get_basetype(cls):
103 """Implement :meth:`~.exp_biaffine.BiaffineExpression._get_basetype`."""
104 return UncertainAffineExpression
106 @classmethod
107 def _get_typecode(cls):
108 """Implement :meth:`~.exp_biaffine.BiaffineExpression._get_typecode`."""
109 return "d"
111 # --------------------------------------------------------------------------
112 # Method overridings for BiaffineExpression.
113 # --------------------------------------------------------------------------
115 @classmethod
116 def _get_type_string_base(cls):
117 """Override for :class:`~.exp_biaffine.BiaffineExpression`."""
118 # TODO: Allow the strings "Uncertain (Linear Expression|Constant)".
119 return "Uncertain {}".format("Affine Expression")
121 def __init__(self, string, shape=(1, 1), coefficients={}):
122 """Construct an :class:`UncertainAffineExpression`.
124 Extends :meth:`.exp_biaffine.BiaffineExpression.__init__`.
126 This constructor is meant for internal use. As a user, you will want to
127 first define a universe (e.g.
128 :class:`~.pert_conic.ConicPerturbationSet`) for a
129 :class:`perturbation parameter <.perturbation.Perturbation>` and use
130 that parameter as a building block to create more complex uncertain
131 expressions.
132 """
133 from .perturbation import Perturbation
135 BiaffineExpression.__init__(self, string, shape, coefficients)
137 if not all(isinstance(prm, Perturbation) for prm in self.parameters):
138 raise TypeError("Uncertain affine expressions may not depend on "
139 "parameters other than perturbation parameters.")
141 for pair in self._bilinear_coefs:
142 x, y = pair
144 d = sum(isinstance(var, BaseVariable) for var in pair)
145 p = sum(isinstance(var, Perturbation) for var in pair)
147 # Forbid a quadratic part.
148 if d > 1:
149 raise TypeError("Tried to create an uncertain affine "
150 "expression that is {}.".format(
151 "quadratic in {}".format(x.string) if x is y else
152 "biaffine in {} and {}".format(x.string, y.string)))
154 # Forbid quadratic dependence on the perturbation parameter.
155 if p > 1:
156 assert x is y
157 raise NotImplementedError("Uncertain affine expressions may "
158 "only depend affinely on the perturbation parameter. Tried "
159 "to create one that is quadratic in {}.".format(x.string))
161 assert d == 1 and p == 1
163 @classmethod
164 def _common_basetype(cls, other, reverse=False):
165 from ..exp_affine import AffineExpression
167 # HACK: AffineExpression is not a subclass of UncertainAffineExpression
168 # but we can treat it as one when it comes to basetype detection.
169 if issubclass(other._get_basetype(), AffineExpression):
170 return UncertainAffineExpression
171 else:
172 return BiaffineExpression._common_basetype.__func__(
173 cls, other, reverse)
175 def _is_convex(self):
176 return True
178 def _is_concave(self):
179 return True
181 # --------------------------------------------------------------------------
182 # Class-specific properties.
183 # --------------------------------------------------------------------------
185 @cached_property
186 def _sorted_bilinear_coefs(self):
187 """Bilinear part coefficients with perturbation on the right side."""
188 from .perturbation import Perturbation
190 coefs = {}
191 for mtbs, coef in self._bilinear_coefs.items():
192 x, y = mtbs
194 if isinstance(x, Perturbation):
195 # Obtain a fitting commutation matrix.
196 K = cvxopt_K(y.dim, x.dim, self._typecode)
198 # Make coef apply to vec(y*x.T) instead of vec(x*y.T).
199 coef = coef * K
201 # Swap x and y.
202 x, y = y, x
204 assert isinstance(x, BaseVariable)
205 assert isinstance(y, Perturbation)
207 coefs[x, y] = coef
209 return coefs
211 # --------------------------------------------------------------------------
212 # Expression-creating operators.
213 # --------------------------------------------------------------------------
215 @cached_unary_operator
216 def __abs__(self):
217 """Denote the Euclidean or Frobenius norm of the expression."""
218 from .uexp_norm import UncertainNorm
220 return UncertainNorm(self)
222 # --------------------------------------------------------------------------
223 # Constraint-creating operators and _predict.
224 # --------------------------------------------------------------------------
226 @classmethod
227 def _predict(cls, subtype, relation, other):
228 from ..cone_nno import NonnegativeOrthant
229 from ..cone_psd import PositiveSemidefiniteCone
230 from ..set import Set
231 from .pert_conic import ConicPerturbationSet
232 from .pert_scenario import ScenarioPerturbationSet
234 assert isinstance(subtype, cls.Subtype)
236 universe = subtype.universe_type
238 if relation in (operator.__le__, operator.__ge__):
239 if issubclass(universe.clstype, ConicPerturbationSet):
240 if issubclass(other.clstype,
241 (AffineExpression, UncertainAffineExpression)) \
242 and other.subtype.shape == subtype.shape:
243 return ConicallyUncertainAffineConstraint.make_type(
244 dim=subtype.dim,
245 universe_subtype=universe.subtype)
246 elif issubclass(universe.clstype, ScenarioPerturbationSet):
247 if issubclass(other.clstype,
248 (AffineExpression, UncertainAffineExpression)) \
249 and other.subtype.shape == subtype.shape:
250 return ScenarioUncertainConicConstraint.make_type(
251 dim=subtype.dim,
252 scenario_count=universe.subtype.scenario_count,
253 cone_type=NonnegativeOrthant.make_type(subtype.dim))
254 elif relation in (operator.__lshift__, operator.__rshift__):
255 if relation == operator.__lshift__ \
256 and issubclass(other.clstype, Set):
257 own_type = ExpressionType(cls, subtype)
258 return other.predict(operator.__rshift__, own_type)
260 if issubclass(other.clstype,
261 (AffineExpression, UncertainAffineExpression)) \
262 and other.subtype.shape == subtype.shape:
263 if subtype.shape[0] != subtype.shape[1]:
264 return NotImplemented
266 self = ExpressionType(cls, subtype)
268 return self.predict(operator.__lshift__,
269 PositiveSemidefiniteCone.make_type(dim=None))
271 return NotImplemented
273 @convert_operands(sameShape=True)
274 @validate_prediction
275 @refine_operands()
276 def __le__(self, other):
277 from ..cone_nno import NonnegativeOrthant
278 from .pert_conic import ConicPerturbationSet
279 from .pert_scenario import ScenarioPerturbationSet
281 if isinstance(self.perturbation.universe, ConicPerturbationSet):
282 if isinstance(other, (AffineExpression, UncertainAffineExpression)):
283 return ConicallyUncertainAffineConstraint(self - other)
284 elif isinstance(self.perturbation.universe, ScenarioPerturbationSet):
285 if isinstance(other, (AffineExpression, UncertainAffineExpression)):
286 return ScenarioUncertainConicConstraint(
287 other - self, NonnegativeOrthant(len(self)))
288 else:
289 raise NotImplementedError("Uncertain affine constraints "
290 "parameterized by {} are not supported.".format(
291 self.perturbation.universe.__class__.__name__))
293 return NotImplemented
295 @convert_operands(sameShape=True)
296 @validate_prediction
297 @refine_operands()
298 def __ge__(self, other):
299 from ..cone_nno import NonnegativeOrthant
300 from .pert_conic import ConicPerturbationSet
301 from .pert_scenario import ScenarioPerturbationSet
303 if isinstance(self.perturbation.universe, ConicPerturbationSet):
304 if isinstance(other, (AffineExpression, UncertainAffineExpression)):
305 return ConicallyUncertainAffineConstraint(other - self)
306 elif isinstance(self.perturbation.universe, ScenarioPerturbationSet):
307 if isinstance(other, (AffineExpression, UncertainAffineExpression)):
308 return ScenarioUncertainConicConstraint(
309 self - other, NonnegativeOrthant(len(self)))
310 else:
311 raise NotImplementedError("Uncertain affine constraints "
312 "parameterized by {} are not supported.".format(
313 self.perturbation.universe.__class__.__name__))
315 return NotImplemented
317 @staticmethod
318 def _lmi_helper(lower, greater):
319 from ..cone_psd import PositiveSemidefiniteCone
321 if isinstance(lower, UncertainAffineExpression) \
322 and isinstance(greater, UncertainAffineExpression) \
323 and lower.perturbation is not greater.perturbation:
324 # NOTE: This failure cannot be predicted.
325 raise ValueError("Can only form a linear matrix inequality if one "
326 "side is certain or both sides depend on the same uncertainty.")
328 diff = greater - lower
330 if not diff.square:
331 raise TypeError("Can only form a linear matrix inequality from "
332 "square matrices.")
334 if not diff.hermitian:
335 # NOTE: This failure cannot be predicted.
336 raise TypeError("Can only form a linear matrix inequality from "
337 "hermitian matrices.")
339 return diff << PositiveSemidefiniteCone()
341 def _lshift_implementation(self, other):
342 if isinstance(other, (AffineExpression, UncertainAffineExpression)):
343 return self._lmi_helper(self, other)
345 return NotImplemented
347 def _rshift_implementation(self, other):
348 if isinstance(other, (AffineExpression, UncertainAffineExpression)):
349 return self._lmi_helper(other, self)
351 return NotImplemented
354# --------------------------------------
355__all__ = api_end(_API_START, globals())