1# ___________________________________________________________________________ 2# 3# Pyomo: Python Optimization Modeling Objects 4# Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC 5# Under the terms of Contract DE-NA0003525 with National Technology and 6# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 7# rights in this software. 8# This software is distributed under the 3-clause BSD License. 9# ___________________________________________________________________________ 10 11__all__ = ('Objective', 12 'simple_objective_rule', 13 '_ObjectiveData', 14 'minimize', 15 'maximize', 16 'simple_objectivelist_rule', 17 'ObjectiveList') 18 19import sys 20import logging 21from weakref import ref as weakref_ref 22 23from pyomo.common.log import is_debug_set 24from pyomo.common.deprecation import deprecated, RenamedClass 25from pyomo.common.formatting import tabular_writer 26from pyomo.common.timing import ConstructionTimer 27from pyomo.core.expr.numvalue import value 28from pyomo.core.base.component import ( 29 ActiveComponentData, ModelComponentFactory, 30) 31from pyomo.core.base.indexed_component import ( 32 ActiveIndexedComponent, UnindexedComponent_set, rule_wrapper, 33 _get_indexed_component_data_name, 34) 35from pyomo.core.base.expression import (_ExpressionData, 36 _GeneralExpressionDataImpl) 37from pyomo.core.base.misc import apply_indexed_rule 38from pyomo.core.base.set import Set 39from pyomo.core.base.initializer import ( 40 Initializer, IndexedCallInitializer, CountedCallInitializer, 41) 42from pyomo.core.base import minimize, maximize 43 44logger = logging.getLogger('pyomo.core') 45 46_rule_returned_none_error = """Objective '%s': rule returned None. 47 48Objective rules must return either a valid expression, numeric value, or 49Objective.Skip. The most common cause of this error is forgetting to 50include the "return" statement at the end of your rule. 51""" 52 53def simple_objective_rule(rule): 54 """ 55 This is a decorator that translates None into Objective.Skip. 56 This supports a simpler syntax in objective rules, though these 57 can be more difficult to debug when errors occur. 58 59 Example use: 60 61 @simple_objective_rule 62 def O_rule(model, i, j): 63 ... 64 65 model.o = Objective(rule=simple_objective_rule(...)) 66 """ 67 return rule_wrapper(rule, {None: Objective.Skip}) 68 69def simple_objectivelist_rule(rule): 70 """ 71 This is a decorator that translates None into ObjectiveList.End. 72 This supports a simpler syntax in objective rules, though these 73 can be more difficult to debug when errors occur. 74 75 Example use: 76 77 @simple_objectivelist_rule 78 def O_rule(model, i, j): 79 ... 80 81 model.o = ObjectiveList(expr=simple_objectivelist_rule(...)) 82 """ 83 return rule_wrapper(rule, {None: ObjectiveList.End}) 84 85# 86# This class is a pure interface 87# 88 89class _ObjectiveData(_ExpressionData): 90 """ 91 This class defines the data for a single objective. 92 93 Public class attributes: 94 expr The Pyomo expression for this objective 95 sense The direction for this objective. 96 """ 97 98 __slots__ = () 99 100 # 101 # Interface 102 # 103 104 def is_minimizing(self): 105 """Return True if this is a minimization objective.""" 106 return self.sense == minimize 107 108 # 109 # Abstract Interface 110 # 111 112 @property 113 def sense(self): 114 """Access sense (direction) of this objective.""" 115 raise NotImplementedError 116 117 def set_sense(self, sense): 118 """Set the sense (direction) of this objective.""" 119 raise NotImplementedError 120 121class _GeneralObjectiveData(_GeneralExpressionDataImpl, 122 _ObjectiveData, 123 ActiveComponentData): 124 """ 125 This class defines the data for a single objective. 126 127 Note that this is a subclass of NumericValue to allow 128 objectives to be used as part of expressions. 129 130 Constructor arguments: 131 expr The Pyomo expression stored in this objective. 132 sense The direction for this objective. 133 component The Objective object that owns this data. 134 135 Public class attributes: 136 expr The Pyomo expression for this objective 137 active A boolean that is true if this objective is active 138 in the model. 139 sense The direction for this objective. 140 141 Private class attributes: 142 _component The objective component. 143 _active A boolean that indicates whether this data is active 144 """ 145 146 __pickle_slots__ = ("_sense",) 147 __slots__ = __pickle_slots__ + _GeneralExpressionDataImpl.__pickle_slots__ 148 149 def __init__(self, expr=None, sense=minimize, component=None): 150 _GeneralExpressionDataImpl.__init__(self, expr) 151 # Inlining ActiveComponentData.__init__ 152 self._component = weakref_ref(component) if (component is not None) \ 153 else None 154 self._active = True 155 self._sense = sense 156 157 if (self._sense != minimize) and \ 158 (self._sense != maximize): 159 raise ValueError("Objective sense must be set to one of " 160 "'minimize' (%s) or 'maximize' (%s). Invalid " 161 "value: %s'" % (minimize, maximize, sense)) 162 163 def __getstate__(self): 164 """ 165 This method must be defined because this class uses slots. 166 """ 167 state = _GeneralExpressionDataImpl.__getstate__(self) 168 for i in _GeneralObjectiveData.__pickle_slots__: 169 state[i] = getattr(self,i) 170 return state 171 172 # Note: because NONE of the slots on this class need to be edited, 173 # we don't need to implement a specialized __setstate__ 174 # method. 175 176 def set_value(self, expr): 177 if expr is None: 178 raise ValueError(_rule_returned_none_error % (self.name,)) 179 return super().set_value(expr) 180 181 # 182 # Abstract Interface 183 # 184 185 @property 186 def sense(self): 187 """Access sense (direction) of this objective.""" 188 return self._sense 189 @sense.setter 190 def sense(self, sense): 191 """Set the sense (direction) of this objective.""" 192 self.set_sense(sense) 193 194 def set_sense(self, sense): 195 """Set the sense (direction) of this objective.""" 196 if sense in {minimize, maximize}: 197 self._sense = sense 198 else: 199 raise ValueError("Objective sense must be set to one of " 200 "'minimize' (%s) or 'maximize' (%s). Invalid " 201 "value: %s'" % (minimize, maximize, sense)) 202 203@ModelComponentFactory.register("Expressions that are minimized or maximized.") 204class Objective(ActiveIndexedComponent): 205 """ 206 This modeling component defines an objective expression. 207 208 Note that this is a subclass of NumericValue to allow 209 objectives to be used as part of expressions. 210 211 Constructor arguments: 212 expr 213 A Pyomo expression for this objective 214 rule 215 A function that is used to construct objective expressions 216 sense 217 Indicate whether minimizing (the default) or maximizing 218 doc 219 A text string describing this component 220 name 221 A name for this component 222 223 Public class attributes: 224 doc 225 A text string describing this component 226 name 227 A name for this component 228 active 229 A boolean that is true if this component will be used to construct 230 a model instance 231 rule 232 The rule used to initialize the objective(s) 233 sense 234 The objective sense 235 236 Private class attributes: 237 _constructed 238 A boolean that is true if this component has been constructed 239 _data 240 A dictionary from the index set to component data objects 241 _index 242 The set of valid indices 243 _implicit_subsets 244 A tuple of set objects that represents the index set 245 _model 246 A weakref to the model that owns this component 247 _parent 248 A weakref to the parent block that owns this component 249 _type 250 The class type for the derived subclass 251 """ 252 253 _ComponentDataClass = _GeneralObjectiveData 254 NoObjective = ActiveIndexedComponent.Skip 255 256 def __new__(cls, *args, **kwds): 257 if cls != Objective: 258 return super(Objective, cls).__new__(cls) 259 if not args or (args[0] is UnindexedComponent_set and len(args)==1): 260 return ScalarObjective.__new__(ScalarObjective) 261 else: 262 return IndexedObjective.__new__(IndexedObjective) 263 264 def __init__(self, *args, **kwargs): 265 _sense = kwargs.pop('sense', minimize) 266 _init = tuple( _arg for _arg in ( 267 kwargs.pop('rule', None), kwargs.pop('expr', None) 268 ) if _arg is not None ) 269 if len(_init) == 1: 270 _init = _init[0] 271 elif not _init: 272 _init = None 273 else: 274 raise ValueError("Duplicate initialization: Objective() only " 275 "accepts one of 'rule=' and 'expr='") 276 277 kwargs.setdefault('ctype', Objective) 278 ActiveIndexedComponent.__init__(self, *args, **kwargs) 279 280 self.rule = Initializer(_init) 281 self._init_sense = Initializer(_sense) 282 283 def construct(self, data=None): 284 """ 285 Construct the expression(s) for this objective. 286 """ 287 if self._constructed: 288 return 289 self._constructed = True 290 291 timer = ConstructionTimer(self) 292 if is_debug_set(logger): 293 logger.debug("Constructing objective %s" % (self.name)) 294 295 rule = self.rule 296 try: 297 # We do not (currently) accept data for constructing Objectives 298 index = None 299 assert data is None 300 301 if rule is None: 302 # If there is no rule, then we are immediately done. 303 return 304 305 if rule.constant() and self.is_indexed(): 306 raise IndexError( 307 "Objective '%s': Cannot initialize multiple indices " 308 "of an objective with a single expression" % 309 (self.name,) ) 310 311 block = self.parent_block() 312 if rule.contains_indices(): 313 # The index is coming in externally; we need to validate it 314 for index in rule.indices(): 315 ans = self.__setitem__(index, rule(block, index)) 316 if ans is not None: 317 self[index].set_sense(self._init_sense(block, index)) 318 elif not self.index_set().isfinite(): 319 # If the index is not finite, then we cannot iterate 320 # over it. Since the rule doesn't provide explicit 321 # indices, then there is nothing we can do (the 322 # assumption is that the user will trigger specific 323 # indices to be created at a later time). 324 pass 325 else: 326 # Bypass the index validation and create the member directly 327 for index in self.index_set(): 328 ans = self._setitem_when_not_present( 329 index, rule(block, index)) 330 if ans is not None: 331 ans.set_sense(self._init_sense(block, index)) 332 except Exception: 333 err = sys.exc_info()[1] 334 logger.error( 335 "Rule failed when generating expression for " 336 "Objective %s with index %s:\n%s: %s" 337 % (self.name, 338 str(index), 339 type(err).__name__, 340 err)) 341 raise 342 finally: 343 timer.report() 344 345 def _getitem_when_not_present(self, index): 346 if self.rule is None: 347 raise KeyError(index) 348 obj = self._setitem_when_not_present( 349 index, self.rule(self.parent_block(), index)) 350 if obj is None: 351 raise KeyError(index) 352 else: 353 obj.set_sense(self._init_sense(block, index)) 354 return obj 355 356 def _pprint(self): 357 """ 358 Return data that will be printed for this component. 359 """ 360 return ( 361 [("Size", len(self)), 362 ("Index", self._index if self.is_indexed() else None), 363 ("Active", self.active) 364 ], 365 self._data.items(), 366 ( "Active","Sense","Expression"), 367 lambda k, v: [ v.active, 368 ("minimize" if (v.sense == minimize) else "maximize"), 369 v.expr 370 ] 371 ) 372 373 def display(self, prefix="", ostream=None): 374 """Provide a verbose display of this object""" 375 if not self.active: 376 return 377 tab = " " 378 if ostream is None: 379 ostream = sys.stdout 380 ostream.write(prefix+self.local_name+" : ") 381 ostream.write(", ".join("%s=%s" % (k,v) for k,v in [ 382 ("Size", len(self)), 383 ("Index", self._index if self.is_indexed() else None), 384 ("Active", self.active), 385 ] )) 386 387 ostream.write("\n") 388 tabular_writer( ostream, prefix+tab, 389 ((k,v) for k,v in self._data.items() if v.active), 390 ( "Active","Value" ), 391 lambda k, v: [ v.active, value(v), ] ) 392 393 394class ScalarObjective(_GeneralObjectiveData, Objective): 395 """ 396 ScalarObjective is the implementation representing a single, 397 non-indexed objective. 398 """ 399 400 def __init__(self, *args, **kwd): 401 _GeneralObjectiveData.__init__(self, expr=None, component=self) 402 Objective.__init__(self, *args, **kwd) 403 404 # 405 # Since this class derives from Component and 406 # Component.__getstate__ just packs up the entire __dict__ into 407 # the state dict, we do not need to define the __getstate__ or 408 # __setstate__ methods. We just defer to the super() get/set 409 # state. Since all of our get/set state methods rely on super() 410 # to traverse the MRO, this will automatically pick up both the 411 # Component and Data base classes. 412 # 413 414 # 415 # Override abstract interface methods to first check for 416 # construction 417 # 418 419 @property 420 def expr(self): 421 """Access the expression of this objective.""" 422 if self._constructed: 423 if len(self._data) == 0: 424 raise ValueError( 425 "Accessing the expression of ScalarObjective " 426 "'%s' before the Objective has been assigned " 427 "a sense or expression. There is currently " 428 "nothing to access." % (self.name)) 429 return _GeneralObjectiveData.expr.fget(self) 430 raise ValueError( 431 "Accessing the expression of objective '%s' " 432 "before the Objective has been constructed (there " 433 "is currently no value to return)." 434 % (self.name)) 435 @expr.setter 436 def expr(self, expr): 437 """Set the expression of this objective.""" 438 self.set_value(expr) 439 440 # for backwards compatibility reasons 441 @property 442 @deprecated("The .value property getter on ScalarObjective is deprecated. " 443 "Use the .expr property getter instead", version='4.3.11323') 444 def value(self): 445 return self.expr 446 447 @value.setter 448 @deprecated("The .value property setter on ScalarObjective is deprecated. " 449 "Use the set_value(expr) method instead", version='4.3.11323') 450 def value(self, expr): 451 self.set_value(expr) 452 453 @property 454 def sense(self): 455 """Access sense (direction) of this objective.""" 456 if self._constructed: 457 if len(self._data) == 0: 458 raise ValueError( 459 "Accessing the sense of ScalarObjective " 460 "'%s' before the Objective has been assigned " 461 "a sense or expression. There is currently " 462 "nothing to access." % (self.name)) 463 return _GeneralObjectiveData.sense.fget(self) 464 raise ValueError( 465 "Accessing the sense of objective '%s' " 466 "before the Objective has been constructed (there " 467 "is currently no value to return)." 468 % (self.name)) 469 @sense.setter 470 def sense(self, sense): 471 """Set the sense (direction) of this objective.""" 472 self.set_sense(sense) 473 474 # 475 # Singleton objectives are strange in that we want them to be 476 # both be constructed but have len() == 0 when not initialized with 477 # anything (at least according to the unit tests that are 478 # currently in place). So during initialization only, we will 479 # treat them as "indexed" objects where things like 480 # Objective.Skip are managed. But after that they will behave 481 # like _ObjectiveData objects where set_value does not handle 482 # Objective.Skip but expects a valid expression or None 483 # 484 485 def clear(self): 486 self._data = {} 487 488 def set_value(self, expr): 489 """Set the expression of this objective.""" 490 if not self._constructed: 491 raise ValueError( 492 "Setting the value of objective '%s' " 493 "before the Objective has been constructed (there " 494 "is currently no object to set)." 495 % (self.name)) 496 if not self._data: 497 self._data[None] = self 498 return super().set_value(expr) 499 500 def set_sense(self, sense): 501 """Set the sense (direction) of this objective.""" 502 if self._constructed: 503 if len(self._data) == 0: 504 self._data[None] = self 505 return _GeneralObjectiveData.set_sense(self, sense) 506 raise ValueError( 507 "Setting the sense of objective '%s' " 508 "before the Objective has been constructed (there " 509 "is currently no object to set)." 510 % (self.name)) 511 512 # 513 # Leaving this method for backward compatibility reasons. 514 # (probably should be removed) 515 # 516 def add(self, index, expr): 517 """Add an expression with a given index.""" 518 if index is not None: 519 raise ValueError( 520 "ScalarObjective object '%s' does not accept " 521 "index values other than None. Invalid value: %s" 522 % (self.name, index)) 523 self.set_value(expr) 524 return self 525 526 527class SimpleObjective(metaclass=RenamedClass): 528 __renamed__new_class__ = ScalarObjective 529 __renamed__version__ = '6.0' 530 531 532class IndexedObjective(Objective): 533 534 # 535 # Leaving this method for backward compatibility reasons 536 # 537 # Note: Beginning after Pyomo 5.2 this method will now validate that 538 # the index is in the underlying index set (through 5.2 the index 539 # was not checked). 540 # 541 def add(self, index, expr): 542 """Add an objective with a given index.""" 543 return self.__setitem__(index, expr) 544 545 546@ModelComponentFactory.register("A list of objective expressions.") 547class ObjectiveList(IndexedObjective): 548 """ 549 An objective component that represents a list of objectives. 550 Objectives can be indexed by their index, but when they are added 551 an index value is not specified. 552 """ 553 554 class End(object): pass 555 556 def __init__(self, **kwargs): 557 """Constructor""" 558 if 'expr' in kwargs: 559 raise ValueError( 560 "ObjectiveList does not accept the 'expr' keyword") 561 _rule = kwargs.pop('rule', None) 562 563 args = (Set(dimen=1),) 564 super().__init__(*args, **kwargs) 565 566 self.rule = Initializer(_rule, allow_generators=True) 567 # HACK to make the "counted call" syntax work. We wait until 568 # after the base class is set up so that is_indexed() is 569 # reliable. 570 if self.rule is not None and type(self.rule) is IndexedCallInitializer: 571 self.rule = CountedCallInitializer(self, self.rule) 572 573 def construct(self, data=None): 574 """ 575 Construct the expression(s) for this objective. 576 """ 577 if self._constructed: 578 return 579 self._constructed=True 580 581 if is_debug_set(logger): 582 logger.debug("Constructing objective list %s" 583 % (self.name)) 584 585 self.index_set().construct() 586 587 if self.rule is not None: 588 _rule = self.rule(self.parent_block(), ()) 589 for cc in iter(_rule): 590 if cc is ObjectiveList.End: 591 break 592 if cc is Objective.Skip: 593 continue 594 self.add(cc, sense=self._init_sense) 595 596 def add(self, expr, sense=minimize): 597 """Add an objective to the list.""" 598 next_idx = len(self._index) + 1 599 self._index.add(next_idx) 600 ans = self.__setitem__(next_idx, expr) 601 if ans is not None: 602 if sense not in {minimize, maximize}: 603 sense = sense(self.parent_block(), next_idx) 604 ans.set_sense(sense) 605 return ans 606 607