Coverage for picos/expressions/uncertain/uexp_rand_pwl.py: 70.25%

121 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:`RandomExtremumAffine`.""" 

20 

21import operator 

22from abc import abstractmethod 

23from collections import namedtuple 

24 

25import cvxopt 

26 

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 

42 

43_API_START = api_start(globals()) 

44# ------------------------------- 

45 

46 

47class RandomExtremumAffine(ExtremumBase, UncertainExpression, Expression): 

48 """Base class for random convex or concave piecewise linear expressions. 

49 

50 .. note:: 

51 

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

61 

62 # -------------------------------------------------------------------------- 

63 # Additional abstract methods extending (in spirit) ExtremumBase. 

64 # -------------------------------------------------------------------------- 

65 

66 @property 

67 @abstractmethod 

68 def _certain_class(self): 

69 pass 

70 

71 # -------------------------------------------------------------------------- 

72 # Initialization and factory methods. 

73 # -------------------------------------------------------------------------- 

74 

75 def __init__(self, expressions): 

76 """Construct a :class:`RandomExtremumAffine`. 

77 

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) 

86 

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

92 

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

97 

98 perturbations = tuple(set( 

99 x.perturbation for x in expressions if x.uncertain)) 

100 

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

107 

108 perturbation = perturbations[0] if perturbations else None 

109 universe = perturbation.universe if perturbation else None 

110 

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

116 

117 typeStr = "{} Uncertain Piecewise Linear Function".format( 

118 self._property_word.title()) 

119 

120 symbStr = self._extremum_glyph( 

121 arguments([x.string for x in expressions])) 

122 

123 Expression.__init__(self, typeStr, symbStr) 

124 

125 self._expressions = expressions 

126 self._perturbation = perturbation 

127 

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

129 # Abstract method implementations for ExtremumBase. 

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

131 

132 @property 

133 def expressions(self): 

134 """The expressions under the extremum.""" 

135 return self._expressions 

136 

137 # -------------------------------------------------------------------------- 

138 # Method overridings for UncertainExpression. 

139 # -------------------------------------------------------------------------- 

140 

141 @property 

142 def perturbation(self): 

143 """Fast override for :class:`~.uexpression.UncertainExpression`.""" 

144 return self._perturbation 

145 

146 # -------------------------------------------------------------------------- 

147 # Abstract method implementations for Expression, except _predict. 

148 # -------------------------------------------------------------------------- 

149 

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 

161 

162 Subtype = namedtuple("Subtype", ("argnum", "universe_type")) 

163 

164 def _get_subtype(self): 

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

166 return self.Subtype(self.argnum, self.universe.type) 

167 

168 def _get_value(self): 

169 return cvxopt.matrix(self._extremum( 

170 x.safe_value for x in self._expressions)) 

171 

172 # -------------------------------------------------------------------------- 

173 # Constraint-creating operators and _predict. 

174 # -------------------------------------------------------------------------- 

175 

176 @classmethod 

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

178 assert isinstance(subtype, cls.Subtype) 

179 

180 convex = issubclass(cls, RandomMaximumAffine) 

181 concave = issubclass(cls, RandomMinimumAffine) 

182 

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 

191 

192 if not issubclass(other.clstype, AffineExpression) \ 

193 or other.subtype.dim != 1: 

194 return NotImplemented 

195 

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) 

204 

205 return NotImplemented 

206 

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

214 

215 if not isinstance(other, AffineExpression): 

216 return NotImplemented 

217 

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) 

224 

225 return NotImplemented 

226 

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

234 

235 if not isinstance(other, AffineExpression): 

236 return NotImplemented 

237 

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) 

244 

245 return NotImplemented 

246 

247 

248class RandomMaximumAffine(MaximumBase, RandomExtremumAffine): 

249 """The maximum over a set of random affine expressions.""" 

250 

251 # -------------------------------------------------------------------------- 

252 # Abstract method implementations for ExtremumBase. 

253 # -------------------------------------------------------------------------- 

254 

255 @property 

256 def _other_class(self): 

257 return RandomMinimumAffine 

258 

259 # -------------------------------------------------------------------------- 

260 # Abstract method implementations for RandomExtremumAffine. 

261 # -------------------------------------------------------------------------- 

262 

263 @property 

264 def _certain_class(self): 

265 return MaximumConvex 

266 

267 

268class RandomMinimumAffine(MinimumBase, RandomExtremumAffine): 

269 """The minimum over a set of random affine expressions.""" 

270 

271 # -------------------------------------------------------------------------- 

272 # Abstract method implementations for ExtremumBase. 

273 # -------------------------------------------------------------------------- 

274 

275 @property 

276 def _other_class(self): 

277 return RandomMaximumAffine 

278 

279 # -------------------------------------------------------------------------- 

280 # Abstract method implementations for RandomExtremumAffine. 

281 # -------------------------------------------------------------------------- 

282 

283 @property 

284 def _certain_class(self): 

285 return MinimumConcave 

286 

287 

288# -------------------------------------- 

289__all__ = api_end(_API_START, globals())