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__ = ['Expression', '_ExpressionData'] 12 13import sys 14import logging 15from weakref import ref as weakref_ref 16 17from pyomo.common.log import is_debug_set 18from pyomo.common.deprecation import deprecated, RenamedClass 19from pyomo.common.modeling import NOTSET 20from pyomo.common.formatting import tabular_writer 21from pyomo.common.timing import ConstructionTimer 22 23from pyomo.core.base.component import ComponentData, ModelComponentFactory 24from pyomo.core.base.indexed_component import ( 25 IndexedComponent, 26 UnindexedComponent_set, ) 27from pyomo.core.base.misc import apply_indexed_rule 28from pyomo.core.base.numvalue import (NumericValue, 29 as_numeric) 30from pyomo.core.base.initializer import Initializer 31 32logger = logging.getLogger('pyomo.core') 33 34 35class _ExpressionData(NumericValue): 36 """ 37 An object that defines a named expression. 38 39 Public Class Attributes 40 expr The expression owned by this data. 41 """ 42 43 __slots__ = () 44 45 # 46 # Interface 47 # 48 49 def __call__(self, exception=True): 50 """Compute the value of this expression.""" 51 if self.expr is None: 52 return None 53 return self.expr(exception=exception) 54 55 def is_named_expression_type(self): 56 """A boolean indicating whether this in a named expression.""" 57 return True 58 59 def is_expression_type(self): 60 """A boolean indicating whether this in an expression.""" 61 return True 62 63 def arg(self, index): 64 if index < 0 or index >= 1: 65 raise KeyError("Invalid index for expression argument: %d" % index) 66 return self.expr 67 68 @property 69 def _args_(self): 70 return (self.expr,) 71 72 @property 73 def args(self): 74 return (self.expr,) 75 76 def nargs(self): 77 return 1 78 79 def _precedence(self): 80 return 0 81 82 def _associativity(self): 83 return 0 84 85 def _to_string(self, values, verbose, smap, compute_values): 86 if verbose: 87 return "%s{%s}" % (str(self), values[0]) 88 if self.expr is None: 89 return "%s{None}" % str(self) 90 return values[0] 91 92 def clone(self): 93 """Return a clone of this expression (no-op).""" 94 return self 95 96 def _apply_operation(self, result): 97 # This "expression" is a no-op wrapper, so just return the inner 98 # result 99 return result[0] 100 101 def polynomial_degree(self): 102 """A tuple of subexpressions involved in this expressions operation.""" 103 return self.expr.polynomial_degree() 104 105 def _compute_polynomial_degree(self, result): 106 return result[0] 107 108 def _is_fixed(self, values): 109 return values[0] 110 111 # 112 # Abstract Interface 113 # 114 115 @property 116 def expr(self): 117 """Return expression on this expression.""" 118 raise NotImplementedError 119 120 def set_value(self, expr): 121 """Set the expression on this expression.""" 122 raise NotImplementedError 123 124 def is_constant(self): 125 """A boolean indicating whether this expression is constant.""" 126 raise NotImplementedError 127 128 def is_fixed(self): 129 """A boolean indicating whether this expression is fixed.""" 130 raise NotImplementedError 131 132 # _ExpressionData should never return False because 133 # they can store subexpressions that contain variables 134 def is_potentially_variable(self): 135 return True 136 137 138class _GeneralExpressionDataImpl(_ExpressionData): 139 """ 140 An object that defines an expression that is never cloned 141 142 Constructor Arguments 143 expr The Pyomo expression stored in this expression. 144 component The Expression object that owns this data. 145 146 Public Class Attributes 147 expr The expression owned by this data. 148 """ 149 150 __pickle_slots__ = ('_expr',) 151 152 __slots__ = () 153 154 def __init__(self, expr=None): 155 self._expr = as_numeric(expr) if (expr is not None) else None 156 157 def create_node_with_local_data(self, values): 158 """ 159 Construct a simple expression after constructing the 160 contained expression. 161 162 This class provides a consistent interface for constructing a 163 node, which is used in tree visitor scripts. 164 """ 165 obj = ScalarExpression() 166 obj.construct() 167 obj.expr = values[0] 168 return obj 169 170 def __getstate__(self): 171 state = super(_GeneralExpressionDataImpl, self).__getstate__() 172 for i in _GeneralExpressionDataImpl.__pickle_slots__: 173 state[i] = getattr(self, i) 174 return state 175 176 # Note: because NONE of the slots on this class need to be edited, 177 # we don't need to implement a specialized __setstate__ 178 # method. 179 180 # 181 # Abstract Interface 182 # 183 184 @property 185 def expr(self): 186 """Return expression on this expression.""" 187 return self._expr 188 @expr.setter 189 def expr(self, expr): 190 self.set_value(expr) 191 192 # for backwards compatibility reasons 193 @property 194 @deprecated("The .value property getter on _GeneralExpressionDataImpl " 195 "is deprecated. Use the .expr property getter instead", 196 version='4.3.11323') 197 def value(self): 198 return self._expr 199 200 @value.setter 201 @deprecated("The .value property setter on _GeneralExpressionDataImpl " 202 "is deprecated. Use the set_value(expr) method instead", 203 version='4.3.11323') 204 def value(self, expr): 205 self.set_value(expr) 206 207 def set_value(self, expr): 208 """Set the expression on this expression.""" 209 self._expr = as_numeric(expr) if (expr is not None) else None 210 211 def is_constant(self): 212 """A boolean indicating whether this expression is constant.""" 213 # The underlying expression can always be changed 214 # so this should never evaluate as constant 215 return False 216 217 def is_fixed(self): 218 """A boolean indicating whether this expression is fixed.""" 219 return self._expr.is_fixed() 220 221class _GeneralExpressionData(_GeneralExpressionDataImpl, 222 ComponentData): 223 """ 224 An object that defines an expression that is never cloned 225 226 Constructor Arguments 227 expr The Pyomo expression stored in this expression. 228 component The Expression object that owns this data. 229 230 Public Class Attributes 231 expr The expression owned by this data. 232 233 Private class attributes: 234 _component The expression component. 235 """ 236 237 __slots__ = _GeneralExpressionDataImpl.__pickle_slots__ 238 239 def __init__(self, expr=None, component=None): 240 _GeneralExpressionDataImpl.__init__(self, expr) 241 # Inlining ComponentData.__init__ 242 self._component = weakref_ref(component) if (component is not None) \ 243 else None 244 245 246@ModelComponentFactory.register( 247 "Named expressions that can be used in other expressions.") 248class Expression(IndexedComponent): 249 """ 250 A shared expression container, which may be defined over a index. 251 252 Constructor Arguments: 253 initialize A Pyomo expression or dictionary of expressions 254 used to initialize this object. 255 expr A synonym for initialize. 256 rule A rule function used to initialize this object. 257 """ 258 259 _ComponentDataClass = _GeneralExpressionData 260 # This seems like a copy-paste error, and should be renamed/removed 261 NoConstraint = IndexedComponent.Skip 262 263 def __new__(cls, *args, **kwds): 264 if cls != Expression: 265 return super(Expression, cls).__new__(cls) 266 if not args or (args[0] is UnindexedComponent_set and len(args)==1): 267 return ScalarExpression.__new__(ScalarExpression) 268 else: 269 return IndexedExpression.__new__(IndexedExpression) 270 271 def __init__(self, *args, **kwds): 272 _init = tuple( 273 arg for arg in 274 (kwds.pop(_arg, None) for _arg in ('rule', 'expr', 'initialize')) 275 if arg is not None 276 ) 277 if len(_init) == 1: 278 _init = _init[0] 279 elif not _init: 280 _init = None 281 else: 282 raise ValueError( 283 "Duplicate initialization: Expression() only " 284 "accepts one of 'rule=', 'expr=', and 'initialize='") 285 286 kwds.setdefault('ctype', Expression) 287 IndexedComponent.__init__(self, *args, **kwds) 288 289 # Historically, Expression objects were dense (but None): 290 # setting arg_not_specified causes Initializer to recognize 291 # _init==None as a constant initializer returning None 292 self._rule = Initializer(_init, arg_not_specified=NOTSET) 293 294 def _pprint(self): 295 return ( 296 [('Size', len(self)), 297 ('Index', None if (not self.is_indexed()) 298 else self._index) 299 ], 300 self.items(), 301 ("Expression",), 302 lambda k,v: \ 303 ["Undefined" if v.expr is None else v.expr] 304 ) 305 306 def display(self, prefix="", ostream=None): 307 """TODO""" 308 if not self.active: 309 return 310 if ostream is None: 311 ostream = sys.stdout 312 tab=" " 313 ostream.write(prefix+self.local_name+" : ") 314 ostream.write("Size="+str(len(self))) 315 316 ostream.write("\n") 317 tabular_writer( 318 ostream, 319 prefix+tab, 320 ((k,v) for k,v in self._data.items()), 321 ( "Value", ), 322 lambda k, v: \ 323 ["Undefined" if v.expr is None else v()]) 324 325 # 326 # A utility to extract all index-value pairs defining this 327 # expression, returned as a dictionary. useful in many contexts, 328 # in which key iteration and repeated __getitem__ calls are too 329 # expensive to extract the contents of an expression. 330 # 331 def extract_values(self): 332 return {key:expression_data.expr 333 for key, expression_data in self.items()} 334 335 # 336 # takes as input a (index, value) dictionary for updating this 337 # Expression. if check=True, then both the index and value are 338 # checked through the __getitem__ method of this class. 339 # 340 def store_values(self, new_values): 341 342 if (self.is_indexed() is False) and \ 343 (not None in new_values): 344 raise KeyError( 345 "Cannot store value for scalar Expression" 346 "="+self.name+"; no value with index " 347 "None in input new values map.") 348 349 for index, new_value in new_values.items(): 350 self._data[index].set_value(new_value) 351 352 def _getitem_when_not_present(self, idx): 353 if self._rule is None: 354 _init = None 355 # TBD: Is this desired behavior? I can see implicitly setting 356 # an Expression if it was not originally defined, but I am less 357 # convinced that implicitly creating an Expression (like what 358 # works with a Var) makes sense. [JDS 25 Nov 17] 359 #raise KeyError(idx) 360 else: 361 _init = self._rule(self.parent_block(), idx) 362 obj = self._setitem_when_not_present(idx, _init) 363 #if obj is None: 364 # raise KeyError(idx) 365 return obj 366 367 def construct(self, data=None): 368 """ Apply the rule to construct values in this set """ 369 if self._constructed: 370 return 371 self._constructed = True 372 373 timer = ConstructionTimer(self) 374 if is_debug_set(logger): 375 logger.debug( 376 "Constructing Expression, name=%s, from data=%s" 377 % (self.name, str(data))) 378 379 try: 380 # We do not (currently) accept data for constructing Constraints 381 assert data is None 382 self._construct_from_rule_using_setitem() 383 finally: 384 timer.report() 385 386 387class ScalarExpression(_GeneralExpressionData, Expression): 388 389 def __init__(self, *args, **kwds): 390 _GeneralExpressionData.__init__(self, expr=None, component=self) 391 Expression.__init__(self, *args, **kwds) 392 393 # 394 # Since this class derives from Component and 395 # Component.__getstate__ just packs up the entire __dict__ into 396 # the state dict, we do not need to define the __getstate__ or 397 # __setstate__ methods. We just defer to the super() get/set 398 # state. Since all of our get/set state methods rely on super() 399 # to traverse the MRO, this will automatically pick up both the 400 # Component and Data base classes. 401 # 402 403 # 404 # Override abstract interface methods to first check for 405 # construction 406 # 407 408 @property 409 def expr(self): 410 """Return expression on this expression.""" 411 if self._constructed: 412 return _GeneralExpressionData.expr.fget(self) 413 raise ValueError( 414 "Accessing the expression of expression '%s' " 415 "before the Expression has been constructed (there " 416 "is currently no value to return)." 417 % (self.name)) 418 @expr.setter 419 def expr(self, expr): 420 """Set the expression on this expression.""" 421 self.set_value(expr) 422 423 # for backwards compatibility reasons 424 @property 425 @deprecated("The .value property getter on ScalarExpression " 426 "is deprecated. Use the .expr property getter instead", 427 version='4.3.11323') 428 def value(self): 429 return self.expr 430 431 @value.setter 432 @deprecated("The .value property setter on ScalarExpression " 433 "is deprecated. Use the set_value(expr) method instead", 434 version='4.3.11323') 435 def value(self, expr): 436 self.set_value(expr) 437 438 def clear(self): 439 self._data = {} 440 441 def set_value(self, expr): 442 """Set the expression on this expression.""" 443 if self._constructed: 444 return _GeneralExpressionData.set_value(self, expr) 445 raise ValueError( 446 "Setting the expression of expression '%s' " 447 "before the Expression has been constructed (there " 448 "is currently no object to set)." 449 % (self.name)) 450 451 def is_constant(self): 452 """A boolean indicating whether this expression is constant.""" 453 if self._constructed: 454 return _GeneralExpressionData.is_constant(self) 455 raise ValueError( 456 "Accessing the is_constant flag of expression '%s' " 457 "before the Expression has been constructed (there " 458 "is currently no value to return)." 459 % (self.name)) 460 461 def is_fixed(self): 462 """A boolean indicating whether this expression is fixed.""" 463 if self._constructed: 464 return _GeneralExpressionData.is_fixed(self) 465 raise ValueError( 466 "Accessing the is_fixed flag of expression '%s' " 467 "before the Expression has been constructed (there " 468 "is currently no value to return)." 469 % (self.name)) 470 471 # 472 # Leaving this method for backward compatibility reasons. 473 # (probably should be removed) 474 # 475 def add(self, index, expr): 476 """Add an expression with a given index.""" 477 if index is not None: 478 raise KeyError( 479 "ScalarExpression object '%s' does not accept " 480 "index values other than None. Invalid value: %s" 481 % (self.name, index)) 482 if (type(expr) is tuple) and \ 483 (expr == Expression.Skip): 484 raise ValueError( 485 "Expression.Skip can not be assigned " 486 "to an Expression that is not indexed: %s" 487 % (self.name)) 488 self.set_value(expr) 489 return self 490 491 492class SimpleExpression(metaclass=RenamedClass): 493 __renamed__new_class__ = ScalarExpression 494 __renamed__version__ = '6.0' 495 496 497class IndexedExpression(Expression): 498 499 # 500 # Leaving this method for backward compatibility reasons 501 # Note: It allows adding members outside of self._index. 502 # This has always been the case. Not sure there is 503 # any reason to maintain a reference to a separate 504 # index set if we allow this. 505 # 506 def add(self, index, expr): 507 """Add an expression with a given index.""" 508 if (type(expr) is tuple) and \ 509 (expr == Expression.Skip): 510 return None 511 cdata = _GeneralExpressionData(expr, component=self) 512 self._data[index] = cdata 513 return cdata 514 515