Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

21from .. import expressions, glyphs 

22from ..apidoc import api_end, api_start 

23from ..caching import cached_property 

24from ..expressions.uncertain import IntractableWorstCase, UncertainExpression 

25 

26_API_START = api_start(globals()) 

27# ------------------------------- 

28 

29 

30class Objective: 

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

32 

33 :Example: 

34 

35 >>> from picos import Objective, RealVariable 

36 >>> x = RealVariable("x") 

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

38 <Objective: minimize x> 

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

40 <Objective: minimize x + x²> 

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

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

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

44 <Objective: maximize -x> 

45 """ 

46 

47 #: Short string denoting a feasibility problem. 

48 FIND = "find" 

49 

50 #: Short string denoting a minimization problem. 

51 MIN = "min" 

52 

53 #: Short string denoting a maximization problem. 

54 MAX = "max" 

55 

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

57 """Construct an optimization objective. 

58 

59 :param str direction: 

60 Case insensitive search direction string. One of 

61 

62 - ``"min"`` or ``"minimize"``, 

63 - ``"max"`` or ``"maximize"``, 

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

65 

66 :param ~picos.expressions.Expression function: 

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

68 problem. 

69 """ 

70 if direction is None: 

71 direction = self.FIND 

72 else: 

73 if not isinstance(direction, str): 

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

75 

76 lower = direction.lower() 

77 if lower == "find": 

78 direction = self.FIND 

79 elif lower.startswith("min"): 

80 direction = self.MIN 

81 elif lower.startswith("max"): 

82 direction = self.MAX 

83 else: 

84 raise ValueError( 

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

86 

87 if function is None: 

88 if direction != self.FIND: 

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

90 else: 

91 if direction == self.FIND: 

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

93 "feasiblity problem.") 

94 

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

96 raise TypeError( 

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

98 

99 if len(function) != 1: 

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

101 

102 function = function.refined 

103 

104 if isinstance(function, expressions.ComplexAffineExpression) \ 

105 and function.complex: 

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

107 

108 self._direction = direction 

109 self._function = function 

110 

111 def __str__(self): 

112 if self._function is None: 

113 return "find an assignment" 

114 else: 

115 minimize = self._direction == self.MIN 

116 dir_str = "minimize" if minimize else "maximize" 

117 

118 if self._function.uncertain: 

119 obj_str = self._function.worst_case_string( 

120 "max" if minimize else "min") 

121 else: 

122 obj_str = self._function.string 

123 

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

125 

126 def __repr__(self): 

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

128 

129 def __iter__(self): 

130 yield self._direction 

131 yield self._function 

132 

133 def __eq__(self, other): 

134 if not isinstance(other, Objective): 

135 return False 

136 

137 if self._direction != other._direction: 

138 return False 

139 

140 if self._direction == self.FIND: 

141 return True 

142 

143 try: 

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

145 except AttributeError: 

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

147 return self._function is other._function 

148 

149 def __pos__(self): 

150 return self 

151 

152 def __neg__(self): 

153 if self._direction == self.FIND: 

154 return self 

155 elif self._direction == self.MIN: 

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

157 else: 

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

159 

160 def __add__(self, other): 

161 if self.feasibility: 

162 if isinstance(other, Objective): 

163 return other 

164 else: 

165 raise TypeError( 

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

167 elif isinstance(other, Objective): 

168 if other.feasibility: 

169 return self 

170 elif self._direction == other._direction: 

171 return self + other._function 

172 else: 

173 return self - (-other._function) 

174 else: 

175 try: 

176 function = self._function + other 

177 except TypeError as error: 

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

179 else: 

180 return Objective(self._direction, function) 

181 

182 def __sub__(self, other): 

183 if self.feasibility: 

184 if isinstance(other, Objective): 

185 return -other 

186 else: 

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

188 "feasiblity objective.") 

189 elif isinstance(other, Objective): 

190 if other.feasibility: 

191 return self 

192 elif self._direction == other._direction: 

193 return self - other._function 

194 else: 

195 return self + (-other._function) 

196 else: 

197 try: 

198 function = self._function - other 

199 except TypeError as error: 

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

201 else: 

202 return Objective(self._direction, function) 

203 

204 def _mul(self, other, reverse): 

205 if self.feasibility: 

206 return self 

207 elif isinstance(other, Objective): 

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

209 "multiply or divide them.") 

210 else: 

211 try: 

212 if reverse: 

213 function = other * self._function 

214 else: 

215 function = self._function * other 

216 except TypeError as error: 

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

218 else: 

219 return Objective(self._direction, function) 

220 

221 def __mul__(self, other): 

222 return self._mul(other, False) 

223 

224 def __rmul__(self, other): 

225 return self._mul(other, True) 

226 

227 def __truediv__(self, other): 

228 if self.feasibility: 

229 return self 

230 elif isinstance(other, Objective): 

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

232 "multiply or divide them.") 

233 else: 

234 try: 

235 function = self._function / other 

236 except TypeError as error: 

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

238 else: 

239 return Objective(self._direction, function) 

240 

241 @property 

242 def feasibility(self): 

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

244 return self._function is None 

245 

246 @property 

247 def pair(self): 

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

249 return self._direction, self._objective 

250 

251 @property 

252 def direction(self): 

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

254 return self._direction 

255 

256 @property 

257 def function(self): 

258 """Objective function.""" 

259 return self._function 

260 

261 @cached_property 

262 def normalized(self): 

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

264 

265 >>> from picos import Objective 

266 >>> obj = Objective(); obj 

267 <Objective: find an assignment> 

268 >>> obj.normalized 

269 <Objective: minimize 0> 

270 """ 

271 if self._function is None: 

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

273 else: 

274 return self 

275 

276 @property 

277 def value(self): 

278 """Value of the objective function. 

279 

280 In the case of an uncertain objective, this is the worst-case (expected) 

281 objective value. 

282 

283 :raises picos.uncertain.IntractableWorstCase: 

284 When computing the worst-case (expected) value of an uncertain 

285 objective is not supported. 

286 """ 

287 if self._function is None: 

288 return None 

289 elif isinstance(self._function, UncertainExpression): 

290 if self._direction == self.MIN: 

291 bad_direction = self.MAX 

292 elif self._direction == self.MAX: 

293 bad_direction = self.MIN 

294 else: 

295 bad_direction = self.FIND 

296 

297 try: 

298 return self._function.worst_case_value(bad_direction) 

299 except IntractableWorstCase as error: 

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

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

302 "nominal objective function instead?" 

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

304 else: 

305 return self._function.value 

306 

307 def __index__(self): 

308 if self._function is None: 

309 raise TypeError("A feasiblity objective cannot be used as an index " 

310 "because there is no objective function to take the value of.") 

311 

312 value = self.value 

313 

314 if value is None: 

315 raise expressions.NotValued( 

316 "Cannot use unvalued objective function {} as an index." 

317 .format(self._function.string)) 

318 

319 assert isinstance(value, (float, int)) 

320 

321 if not value.is_integer(): 

322 raise ValueError( 

323 "Cannot use the objective function {} as an index as its value " 

324 "of {} is not integral.".format(self._function.string, value)) 

325 

326 return int(value) 

327 

328 def _casting_helper(self, theType): 

329 assert theType in (int, float, complex) 

330 

331 if self._function is None: 

332 raise TypeError("A feasiblity objective cannot be cast as {} " 

333 "because there is no objective function to take the value of." 

334 .format(theType.__name__)) 

335 

336 value = self.value 

337 

338 if value is None: 

339 raise expressions.NotValued( 

340 "Cannot cast unvalued objective function {} as {}." 

341 .format(self._function.string, theType.__name__)) 

342 

343 return theType(value) 

344 

345 def __int__(self): 

346 return self._casting_helper(int) 

347 

348 def __float__(self): 

349 return self._casting_helper(float) 

350 

351 def __complex__(self): 

352 return self._casting_helper(complex) 

353 

354 def __round__(self, ndigits=None): 

355 return round(float(self), ndigits) 

356 

357 

358# -------------------------------------- 

359__all__ = api_end(_API_START, globals())