Coverage for picos/expressions/uncertain/uexp_norm.py: 78.05%
123 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-26 07:46 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-26 07:46 +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:`UncertainNorm`."""
21import operator
22from collections import namedtuple
24import cvxopt
25import numpy
27from ... import glyphs
28from ...apidoc import api_end, api_start
29from ...caching import cached_unary_operator
30from ...constraints.uncertain import (BallUncertainNormConstraint,
31 ScenarioUncertainConicConstraint)
32from ..cone_soc import SecondOrderCone
33from ..data import convert_operands, cvx2np
34from ..exp_affine import AffineExpression
35from ..exp_biaffine import BiaffineExpression
36from ..exp_norm import Norm
37from ..expression import Expression, refine_operands, validate_prediction
38from .pert_conic import ConicPerturbationSet, UnitBallPerturbationSet
39from .pert_scenario import ScenarioPerturbationSet
40from .uexp_affine import UncertainAffineExpression
41from .uexp_sqnorm import UncertainSquaredNorm
42from .uexpression import UncertainExpression
44_API_START = api_start(globals())
45# -------------------------------
48class UncertainNorm(UncertainExpression, Expression):
49 """Euclidean or Frobenius norm of an uncertain affine expression."""
51 # --------------------------------------------------------------------------
52 # Initialization and factory methods.
53 # --------------------------------------------------------------------------
55 def __init__(self, x):
56 """Construct an :class:`UncertainNorm`.
58 :param x:
59 The uncertain affine expression to denote the norm of.
60 :type x:
61 ~picos.expressions.uncertain.uexp_affine.UncertainAffineExpression
62 """
63 if not isinstance(x, UncertainAffineExpression):
64 raise TypeError("Can only form the uncertain norm of an uncertain "
65 "affine expression, not of {}.".format(type(x).__name__))
67 # Refine perturbation set from ellipsoidal to unit ball.
68 if x.uncertain and isinstance(x.universe, ConicPerturbationSet) \
69 and x.universe.ellipsoidal:
70 x = x.replace_mutables(x.universe.unit_ball_form[1])
71 assert isinstance(x.universe, UnitBallPerturbationSet)
73 if len(x) == 1:
74 typeStr = "Uncertain Absolute Value"
75 symbStr = glyphs.abs(x.string)
76 else:
77 typeStr = "Uncertain {} Norm".format(
78 "Euclidean" if 1 in x.shape else "Frobenius")
79 symbStr = glyphs.norm(x.string)
81 Expression.__init__(self, typeStr, symbStr)
83 self._x = x
85 # --------------------------------------------------------------------------
86 # Properties.
87 # --------------------------------------------------------------------------
89 @property
90 def x(self):
91 """Uncertain affine expression under the norm."""
92 return self._x
94 # --------------------------------------------------------------------------
95 # Abstract method implementations for Expression, except _predict.
96 # --------------------------------------------------------------------------
98 @cached_unary_operator
99 def _get_refined(self):
100 """Implement :meth:`~.expression.Expression._get_refined`."""
101 if self.certain:
102 return Norm(self._x.refined)
103 else:
104 return self
106 Subtype = namedtuple("Subtype", ("argdim", "universe_type"))
108 def _get_subtype(self):
109 """Implement :meth:`~.expression.Expression._get_subtype`."""
110 return self.Subtype(len(self._x), self.universe.type)
112 def _get_value(self):
113 value = self._x._get_value()
115 if len(value) == 1:
116 return abs(value)
117 else:
118 return cvxopt.matrix(numpy.linalg.norm(numpy.ravel(cvx2np(value))))
120 @cached_unary_operator
121 def _get_mutables(self):
122 return self._x.mutables
124 def _is_convex(self):
125 return True
127 def _is_concave(self):
128 return False
130 def _replace_mutables(self, mapping):
131 return self.__class__(self._x._replace_mutables(mapping))
133 def _freeze_mutables(self, freeze):
134 return self.__class__(self._x._freeze_mutables(freeze))
136 # --------------------------------------------------------------------------
137 # Python special method implementations, except constraint-creating ones.
138 # --------------------------------------------------------------------------
140 @convert_operands(scalarRHS=True)
141 @refine_operands()
142 def __pow__(self, other):
143 if isinstance(other, AffineExpression):
144 if not other.constant or other.value != 2:
145 raise NotImplementedError(
146 "You may only take an uncertain norm to the power of two.")
148 return UncertainSquaredNorm(self._x)
149 else:
150 return NotImplemented
152 # --------------------------------------------------------------------------
153 # Constraint-creating operators and _predict.
154 # --------------------------------------------------------------------------
156 @classmethod
157 def _predict(cls, subtype, relation, other):
158 assert isinstance(subtype, cls.Subtype)
160 AE = AffineExpression
161 BAE = BiaffineExpression
162 UAE = UncertainAffineExpression
163 CPS = ConicPerturbationSet
164 UBPS = UnitBallPerturbationSet
165 SPS = ScenarioPerturbationSet
167 if issubclass(other.clstype, BAE) and other.subtype.dim != 1:
168 return NotImplemented
170 if relation is not operator.__le__:
171 return NotImplemented
173 if issubclass(subtype.universe_type.clstype, UBPS):
174 if issubclass(other.clstype, (AE, UAE)):
175 if issubclass(other.clstype, UAE) \
176 and not issubclass(other.subtype.universe_type.clstype, CPS):
177 return NotImplemented
179 if issubclass(other.clstype, UAE):
180 bound_universe_subtype = other.subtype.universe_type.subtype
181 else:
182 bound_universe_subtype = None
184 return BallUncertainNormConstraint.make_type(
185 dim=subtype.argdim,
186 norm_universe_subtype=subtype.universe_type.subtype,
187 bound_universe_subtype=bound_universe_subtype)
188 elif issubclass(subtype.universe_type.clstype, SPS):
189 if issubclass(other.clstype, (AE, UAE)):
190 if issubclass(other.clstype, UAE) \
191 and not issubclass(other.subtype.universe_type.clstype, SPS):
192 return NotImplemented
194 return ScenarioUncertainConicConstraint.make_type(
195 dim=(subtype.argdim + 1),
196 scenario_count=subtype.universe_type.subtype.scenario_count,
197 cone_type=SecondOrderCone.make_type(dim=None))
198 else:
199 return NotImplemented
201 return NotImplemented
203 @convert_operands(scalarRHS=True)
204 @validate_prediction
205 @refine_operands()
206 def __le__(self, other):
207 if isinstance(self._x.universe, UnitBallPerturbationSet):
208 if isinstance(other, (AffineExpression, UncertainAffineExpression)):
209 # Upper bound must be certain or conically uncertain.
210 if isinstance(other, UncertainAffineExpression) \
211 and not isinstance(other.universe, ConicPerturbationSet):
212 raise TypeError(
213 "May only upper bound a conically uncertain norm with a"
214 " certain or another conically uncertain expression.")
216 # Uncertain upper bound must have independent uncertainty.
217 # NOTE: Can only be predicted up to the perturbation type.
218 if isinstance(other, UncertainAffineExpression) \
219 and self.perturbation is other.perturbation:
220 raise ValueError("If the upper bound to a conically "
221 "uncertain norm is itself uncertain, then the "
222 "uncertainty in both sides must be independent "
223 "(distinct perturbation parameters).")
225 return BallUncertainNormConstraint(self, other)
226 elif isinstance(self._x.universe, ScenarioPerturbationSet):
227 if isinstance(other, (AffineExpression, UncertainAffineExpression)):
228 # Upper bound must be certain or scenario uncertain.
229 if isinstance(other, UncertainAffineExpression) \
230 and not isinstance(other.universe, ScenarioPerturbationSet):
231 raise TypeError(
232 "May only upper bound a scenario uncertain norm with a"
233 " certain or another scenario uncertain expression.")
235 # Uncertain upper bound must have equal uncertainty.
236 # NOTE: Can only be predicted up to the perturbation type.
237 if isinstance(other, UncertainAffineExpression) \
238 and self.perturbation is not other.perturbation:
239 raise ValueError(
240 "If the upper bound to a scenario uncertain norm is "
241 "itself uncertain, then the uncertainty in both sides "
242 "must be equal (same perturbation parameter).")
244 return (other // self._x.vec) << SecondOrderCone()
245 elif isinstance(self._x.universe, ConicPerturbationSet):
246 # The universe could not be refined to a UnitBallPerturbationSet.
247 assert not self._x.universe.ellipsoidal
248 raise TypeError("Upper-bounding an uncertain norm whose "
249 "perturbation parameter lives in a conic perturbation set is "
250 "only supported if the perturbation set is an ellipsoid.")
251 else:
252 raise TypeError("Upper-bounding an uncertain norm whose "
253 "perturbation parameter is described by an instance of {} is "
254 "not supported.".format(self._x.universe.__class__.__name__))
256 # Make sure the Python NotImplemented-triggered TypeError works.
257 assert not isinstance(other,
258 (AffineExpression, UncertainAffineExpression))
260 return NotImplemented
263# --------------------------------------
264__all__ = api_end(_API_START, globals())