Coverage for picos/expressions/uncertain/uexp_sqnorm.py: 79.82%
109 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:`UncertainSquaredNorm`."""
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 (MomentAmbiguousSquaredNormConstraint,
31 ScenarioUncertainConicConstraint,
32 WassersteinAmbiguousSquaredNormConstraint)
33from ..cone_rsoc import RotatedSecondOrderCone
34from ..data import convert_operands, cvx2np
35from ..exp_affine import AffineExpression
36from ..exp_biaffine import BiaffineExpression
37from ..exp_norm import Norm
38from ..expression import Expression, refine_operands, validate_prediction
39from .pert_moment import MomentAmbiguitySet
40from .pert_scenario import ScenarioPerturbationSet
41from .pert_wasserstein import WassersteinAmbiguitySet
42from .uexp_affine import UncertainAffineExpression
43from .uexpression import UncertainExpression
45_API_START = api_start(globals())
46# -------------------------------
49class UncertainSquaredNorm(UncertainExpression, Expression):
50 """Squared Euclidean or Frobenius norm of an uncertain affine expression."""
52 # --------------------------------------------------------------------------
53 # Initialization and factory methods.
54 # --------------------------------------------------------------------------
56 def __init__(self, x):
57 """Construct an :class:`UncertainSquaredNorm`.
59 :param x:
60 The uncertain affine expression to denote the squared norm of.
61 :type x:
62 ~picos.expressions.uncertain.uexp_affine.UncertainAffineExpression
63 """
64 if not isinstance(x, UncertainAffineExpression):
65 raise TypeError("Can only form the uncertain squared norm of an "
66 "uncertain affine expression, not of {}."
67 .format(type(x).__name__))
69 typeStr = "Uncertain Squared Norm"
70 symbStr = glyphs.squared(glyphs.norm(x.string))
72 Expression.__init__(self, typeStr, symbStr)
74 self._x = x
76 # --------------------------------------------------------------------------
77 # Properties.
78 # --------------------------------------------------------------------------
80 @property
81 def x(self):
82 """Uncertain affine expression under the squared norm."""
83 return self._x
85 # --------------------------------------------------------------------------
86 # Abstract method implementations for Expression, except _predict.
87 # --------------------------------------------------------------------------
89 @cached_unary_operator
90 def _get_refined(self):
91 """Implement :meth:`~.expression.Expression._get_refined`."""
92 if self.certain:
93 return Norm(self._x.refined)**2
94 else:
95 return self
97 Subtype = namedtuple("Subtype", ("argdim", "universe_type"))
99 def _get_subtype(self):
100 """Implement :meth:`~.expression.Expression._get_subtype`."""
101 return self.Subtype(len(self._x), self.universe.type)
103 def _get_value(self):
104 value = self._x._get_value()
106 if len(value) == 1:
107 return abs(value)**2
108 else:
109 return cvxopt.matrix(
110 numpy.linalg.norm(numpy.ravel(cvx2np(value)))**2)
112 @cached_unary_operator
113 def _get_mutables(self):
114 return self._x.mutables
116 def _is_convex(self):
117 return True
119 def _is_concave(self):
120 return False
122 def _replace_mutables(self, mapping):
123 return self.__class__(self._x._replace_mutables(mapping))
125 def _freeze_mutables(self, freeze):
126 return self.__class__(self._x._freeze_mutables(freeze))
128 # --------------------------------------------------------------------------
129 # Constraint-creating operators and _predict.
130 # --------------------------------------------------------------------------
132 @classmethod
133 def _predict(cls, subtype, relation, other):
134 assert isinstance(subtype, cls.Subtype)
136 AE = AffineExpression
137 BAE = BiaffineExpression
138 UAE = UncertainAffineExpression
139 MAS = MomentAmbiguitySet
140 SPS = ScenarioPerturbationSet
141 WAS = WassersteinAmbiguitySet
143 if issubclass(other.clstype, BAE) and other.subtype.dim != 1:
144 return NotImplemented
146 if relation is not operator.__le__:
147 return NotImplemented
149 if issubclass(subtype.universe_type.clstype, MAS):
150 if issubclass(other.clstype, AE):
151 return MomentAmbiguousSquaredNormConstraint.make_type(
152 sqnorm_argdim=subtype.argdim,
153 universe_subtype=subtype.universe_type.subtype)
154 elif issubclass(subtype.universe_type.clstype, WAS):
155 if subtype.universe_type.subtype.p != 2:
156 return NotImplemented
158 if issubclass(other.clstype, AE):
159 return WassersteinAmbiguousSquaredNormConstraint.make_type(
160 sqnorm_argdim=subtype.argdim,
161 universe_subtype=subtype.universe_type.subtype)
162 elif issubclass(subtype.universe_type.clstype, SPS):
163 if issubclass(other.clstype, (AE, UAE)):
164 if issubclass(other.clstype, UAE) \
165 and not issubclass(other.subtype.universe_type.clstype, SPS):
166 return NotImplemented
168 return ScenarioUncertainConicConstraint.make_type(
169 dim=(subtype.argdim + 2),
170 scenario_count=subtype.universe_type.subtype.scenario_count,
171 cone_type=RotatedSecondOrderCone.make_type(dim=None))
172 else:
173 return NotImplemented
175 return NotImplemented
177 @convert_operands(scalarRHS=True)
178 @validate_prediction
179 @refine_operands()
180 def __le__(self, other):
181 if isinstance(self._x.universe, MomentAmbiguitySet):
182 if isinstance(other, AffineExpression):
183 return MomentAmbiguousSquaredNormConstraint(self, other)
184 elif isinstance(other, UncertainAffineExpression):
185 # Raise a meaningful exception because there are other cases
186 # where upper bounding with an UncertainAffineExpression works
187 # so the default Python exception would be misleading.
188 raise TypeError("When upper-bounding a moment-ambiguous "
189 "expected squared norm, the upper bound must be certain.")
190 elif isinstance(self._x.universe, WassersteinAmbiguitySet):
191 if self._x.universe.p != 2:
192 raise ValueError("Upper-bounding an expected squared norm under"
193 " Wasserstein ambiguity requires p = 2.")
195 if isinstance(other, AffineExpression):
196 return WassersteinAmbiguousSquaredNormConstraint(self, other)
197 elif isinstance(other, UncertainAffineExpression):
198 # Raise a meaningful exception because there are other cases
199 # where upper bounding with an UncertainAffineExpression works
200 # so the default Python exception would be misleading.
201 raise TypeError("When upper-bounding a Wasserstein-ambiguous "
202 "expected squared norm, the upper bound must be certain.")
203 elif isinstance(self._x.universe, ScenarioPerturbationSet):
204 if isinstance(other, (AffineExpression, UncertainAffineExpression)):
205 # Uncertain upper bound must have equal uncertainty.
206 # NOTE: Can only be predicted up to the perturbation type.
207 if isinstance(other, UncertainAffineExpression) \
208 and self.perturbation is not other.perturbation:
209 raise ValueError("If the upper bound to a scenario "
210 "uncertain squared norm is itself uncertain, then the "
211 "uncertainty in both sides must be equal (same "
212 "perturbation parameter).")
214 return (other // 1 // self._x.vec) << RotatedSecondOrderCone()
215 else:
216 raise TypeError("Upper-bounding an uncertain squared norm whose "
217 "perturbation parameter is described by an instance of {} is "
218 "not supported.".format(self._x.universe.__class__.__name__))
220 # Make sure the Python NotImplemented-triggered TypeError works.
221 assert not isinstance(other,
222 (AffineExpression, UncertainAffineExpression))
224 return NotImplemented
227# --------------------------------------
228__all__ = api_end(_API_START, globals())