Coverage for picos/modeling/objective.py: 66.86%

169 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-26 07:46 +0000

1# ------------------------------------------------------------------------------ 

2# Copyright (C) 2019-2021 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"""Implementation of :class:`Objective`.""" 

20 

21import cvxopt 

22 

23from .. import expressions, glyphs 

24from ..apidoc import api_end, api_start 

25from ..caching import cached_property 

26from ..expressions.uncertain import IntractableWorstCase, UncertainExpression 

27from ..valuable import NotValued, Valuable 

28 

29_API_START = api_start(globals()) 

30# ------------------------------- 

31 

32 

33class Objective(Valuable): 

34 """An optimization objective composed of search direction and function. 

35 

36 :Example: 

37 

38 >>> from picos import Objective, RealVariable 

39 >>> x = RealVariable("x") 

40 >>> obj = Objective("min", x); obj 

41 <Objective: minimize x> 

42 >>> obj + x**2 # Add a term to the objective function. 

43 <Objective: minimize x + x²> 

44 >>> obj/2 + 2*obj # Scale and combine two objectives. 

45 <Objective: minimize x/2 + 2·x> 

46 >>> -obj # Flip the optimization direction. 

47 <Objective: maximize -x> 

48 """ 

49 

50 #: Short string denoting a feasibility problem. 

51 FIND = "find" 

52 

53 #: Short string denoting a minimization problem. 

54 MIN = "min" 

55 

56 #: Short string denoting a maximization problem. 

57 MAX = "max" 

58 

59 def __init__(self, direction=None, function=None): 

60 """Construct an optimization objective. 

61 

62 :param str direction: 

63 Case insensitive search direction string. One of 

64 

65 - ``"min"`` or ``"minimize"``, 

66 - ``"max"`` or ``"maximize"``, 

67 - ``"find"`` or :obj:`None` (for a feasibility problem). 

68 

69 :param ~picos.expressions.Expression function: 

70 The objective function. Must be :obj:`None` for a feasibility 

71 problem. 

72 """ 

73 if direction is None: 

74 direction = self.FIND 

75 else: 

76 if not isinstance(direction, str): 

77 raise TypeError("Search direction must be given as a string.") 

78 

79 lower = direction.lower() 

80 if lower == "find": 

81 direction = self.FIND 

82 elif lower.startswith("min"): 

83 direction = self.MIN 

84 elif lower.startswith("max"): 

85 direction = self.MAX 

86 else: 

87 raise ValueError( 

88 "Invalid search direction '{}'.".format(direction)) 

89 

90 if function is None: 

91 if direction != self.FIND: 

92 raise ValueError("Missing an objective function.") 

93 else: 

94 if direction == self.FIND: 

95 raise ValueError("May not specify an objective function for a " 

96 "feasiblity problem.") 

97 

98 if not isinstance(function, expressions.Expression): 

99 raise TypeError( 

100 "Objective function must be a PICOS expression.") 

101 

102 if len(function) != 1: 

103 raise TypeError("Objective function must be scalar.") 

104 

105 function = function.refined 

106 

107 if isinstance(function, expressions.ComplexAffineExpression) \ 

108 and function.complex: 

109 raise TypeError("Objective function may not be complex.") 

110 

111 self._direction = direction 

112 self._function = function 

113 

114 def __str__(self): 

115 if self._function is None: 

116 return "find an assignment" 

117 else: 

118 minimize = self._direction == self.MIN 

119 dir_str = "minimize" if minimize else "maximize" 

120 

121 if self._function.uncertain: 

122 obj_str = self._function.worst_case_string( 

123 "max" if minimize else "min") 

124 else: 

125 obj_str = self._function.string 

126 

127 return "{} {}".format(dir_str, obj_str) 

128 

129 def __repr__(self): 

130 return glyphs.repr1("Objective: {}".format(self)) 

131 

132 def __iter__(self): 

133 yield self._direction 

134 yield self._function 

135 

136 def __eq__(self, other): 

137 """Report whether two objectives are the same.""" 

138 if not isinstance(other, Objective): 

139 return False 

140 

141 if self._direction != other._direction: 

142 return False 

143 

144 if self._direction == self.FIND: 

145 return True 

146 

147 try: 

148 return self._function.equals(other._function) 

149 except AttributeError: 

150 # TODO: Allow all expressions to be equality-checked? 

151 return self._function is other._function 

152 

153 def __pos__(self): 

154 """Return the objective as-is.""" 

155 return self 

156 

157 def __neg__(self): 

158 """Return the negated objective with the search direction flipped.""" 

159 if self._direction == self.FIND: 

160 return self 

161 elif self._direction == self.MIN: 

162 return Objective(self.MAX, -self._function) 

163 else: 

164 return Objective(self.MIN, -self._function) 

165 

166 def __add__(self, other): 

167 """Denote the sum of two compatible objectives.""" 

168 if self.feasibility: 

169 if isinstance(other, Objective): 

170 return other 

171 else: 

172 raise TypeError( 

173 "May only add another objective to a feasiblity objective.") 

174 elif isinstance(other, Objective): 

175 if other.feasibility: 

176 return self 

177 elif self._direction == other._direction: 

178 return self + other._function 

179 else: 

180 return self - (-other._function) 

181 else: 

182 try: 

183 function = self._function + other 

184 except TypeError as error: 

185 raise TypeError("Failed to add to objective.") from error 

186 else: 

187 return Objective(self._direction, function) 

188 

189 def __sub__(self, other): 

190 """Denote the difference of two compatible objectives.""" 

191 if self.feasibility: 

192 if isinstance(other, Objective): 

193 return -other 

194 else: 

195 raise TypeError("May only subtract another objective from a " 

196 "feasiblity objective.") 

197 elif isinstance(other, Objective): 

198 if other.feasibility: 

199 return self 

200 elif self._direction == other._direction: 

201 return self - other._function 

202 else: 

203 return self + (-other._function) 

204 else: 

205 try: 

206 function = self._function - other 

207 except TypeError as error: 

208 raise TypeError("Failed to subtract from objective.") from error 

209 else: 

210 return Objective(self._direction, function) 

211 

212 def _mul(self, other, reverse): 

213 if self.feasibility: 

214 return self 

215 elif isinstance(other, Objective): 

216 raise TypeError("You may only add or subtract two objectives, not " 

217 "multiply or divide them.") 

218 else: 

219 try: 

220 if reverse: 

221 function = other * self._function 

222 else: 

223 function = self._function * other 

224 except TypeError as error: 

225 raise TypeError("Failed to multiply objective.") from error 

226 else: 

227 return Objective(self._direction, function) 

228 

229 def __mul__(self, other): 

230 """Denote the product of the objective with an expression.""" 

231 return self._mul(other, False) 

232 

233 def __rmul__(self, other): 

234 """Denote the product of the objective with an expression.""" 

235 return self._mul(other, True) 

236 

237 def __truediv__(self, other): 

238 """Denote division of the objective by an expression.""" 

239 if self.feasibility: 

240 return self 

241 elif isinstance(other, Objective): 

242 raise TypeError("You may only add or subtract two objectives, not " 

243 "multiply or divide them.") 

244 else: 

245 try: 

246 function = self._function / other 

247 except TypeError as error: 

248 raise TypeError("Failed to divide objective.") from error 

249 else: 

250 return Objective(self._direction, function) 

251 

252 @property 

253 def feasibility(self): 

254 """Whether the objective is "find an assignment".""" 

255 return self._function is None 

256 

257 @property 

258 def pair(self): 

259 """Search direction and objective function as a pair.""" 

260 return self._direction, self._objective 

261 

262 @property 

263 def direction(self): 

264 """Search direction as a short string.""" 

265 return self._direction 

266 

267 @property 

268 def function(self): 

269 """Objective function.""" 

270 return self._function 

271 

272 @cached_property 

273 def normalized(self): 

274 """The objective but with feasiblity posed as "minimize 0". 

275 

276 >>> from picos import Objective 

277 >>> obj = Objective(); obj 

278 <Objective: find an assignment> 

279 >>> obj.normalized 

280 <Objective: minimize 0> 

281 """ 

282 if self._function is None: 

283 return Objective(self.MIN, expressions.AffineExpression.zero()) 

284 else: 

285 return self 

286 

287 # -------------------------------------------------------------------------- 

288 # Abstract method implementations for the Valuable base class. 

289 # -------------------------------------------------------------------------- 

290 

291 def _get_valuable_string(self): 

292 return "objective {}".format(self) 

293 

294 def _get_value(self): 

295 if self._function is None: 

296 raise NotValued("A feasibility objective has no value.") 

297 elif isinstance(self._function, UncertainExpression): 

298 if self._direction == self.MIN: 

299 bad_direction = self.MAX 

300 elif self._direction == self.MAX: 

301 bad_direction = self.MIN 

302 else: 

303 bad_direction = self.FIND 

304 

305 try: 

306 value = self._function.worst_case_value(bad_direction) 

307 except IntractableWorstCase as error: 

308 raise IntractableWorstCase("Failed to compute the worst-case " 

309 "value of the objective function {}: {} Maybe evaluate the " 

310 "nominal objective function instead?" 

311 .format(self._function.string, error)) from None 

312 else: 

313 return cvxopt.matrix(value) 

314 else: 

315 return self._function._get_value() 

316 

317 def _set_value(self, value): 

318 if self._function is None: 

319 raise TypeError("Cannot set the value of a feasibility objective.") 

320 else: 

321 self._function.value = value 

322 

323 

324# -------------------------------------- 

325__all__ = api_end(_API_START, globals())