Coverage for picos/expressions/uncertain/uexp_sqnorm.py: 79.82%

109 statements  

« 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# ------------------------------------------------------------------------------ 

18 

19"""Implements :class:`UncertainSquaredNorm`.""" 

20 

21import operator 

22from collections import namedtuple 

23 

24import cvxopt 

25import numpy 

26 

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 

44 

45_API_START = api_start(globals()) 

46# ------------------------------- 

47 

48 

49class UncertainSquaredNorm(UncertainExpression, Expression): 

50 """Squared Euclidean or Frobenius norm of an uncertain affine expression.""" 

51 

52 # -------------------------------------------------------------------------- 

53 # Initialization and factory methods. 

54 # -------------------------------------------------------------------------- 

55 

56 def __init__(self, x): 

57 """Construct an :class:`UncertainSquaredNorm`. 

58 

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__)) 

68 

69 typeStr = "Uncertain Squared Norm" 

70 symbStr = glyphs.squared(glyphs.norm(x.string)) 

71 

72 Expression.__init__(self, typeStr, symbStr) 

73 

74 self._x = x 

75 

76 # -------------------------------------------------------------------------- 

77 # Properties. 

78 # -------------------------------------------------------------------------- 

79 

80 @property 

81 def x(self): 

82 """Uncertain affine expression under the squared norm.""" 

83 return self._x 

84 

85 # -------------------------------------------------------------------------- 

86 # Abstract method implementations for Expression, except _predict. 

87 # -------------------------------------------------------------------------- 

88 

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 

96 

97 Subtype = namedtuple("Subtype", ("argdim", "universe_type")) 

98 

99 def _get_subtype(self): 

100 """Implement :meth:`~.expression.Expression._get_subtype`.""" 

101 return self.Subtype(len(self._x), self.universe.type) 

102 

103 def _get_value(self): 

104 value = self._x._get_value() 

105 

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) 

111 

112 @cached_unary_operator 

113 def _get_mutables(self): 

114 return self._x.mutables 

115 

116 def _is_convex(self): 

117 return True 

118 

119 def _is_concave(self): 

120 return False 

121 

122 def _replace_mutables(self, mapping): 

123 return self.__class__(self._x._replace_mutables(mapping)) 

124 

125 def _freeze_mutables(self, freeze): 

126 return self.__class__(self._x._freeze_mutables(freeze)) 

127 

128 # -------------------------------------------------------------------------- 

129 # Constraint-creating operators and _predict. 

130 # -------------------------------------------------------------------------- 

131 

132 @classmethod 

133 def _predict(cls, subtype, relation, other): 

134 assert isinstance(subtype, cls.Subtype) 

135 

136 AE = AffineExpression 

137 BAE = BiaffineExpression 

138 UAE = UncertainAffineExpression 

139 MAS = MomentAmbiguitySet 

140 SPS = ScenarioPerturbationSet 

141 WAS = WassersteinAmbiguitySet 

142 

143 if issubclass(other.clstype, BAE) and other.subtype.dim != 1: 

144 return NotImplemented 

145 

146 if relation is not operator.__le__: 

147 return NotImplemented 

148 

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 

157 

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 

167 

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 

174 

175 return NotImplemented 

176 

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.") 

194 

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).") 

213 

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__)) 

219 

220 # Make sure the Python NotImplemented-triggered TypeError works. 

221 assert not isinstance(other, 

222 (AffineExpression, UncertainAffineExpression)) 

223 

224 return NotImplemented 

225 

226 

227# -------------------------------------- 

228__all__ = api_end(_API_START, globals())