Source code for pyomo.core.base.expression

#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2024
#  National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

import sys
import logging
from weakref import ref as weakref_ref
from pyomo.common.pyomo_typing import overload

from pyomo.common.log import is_debug_set
from pyomo.common.deprecation import RenamedClass
from pyomo.common.modeling import NOTSET
from pyomo.common.formatting import tabular_writer
from pyomo.common.timing import ConstructionTimer
from pyomo.common.numeric_types import (
    native_types,
    native_numeric_types,
    check_if_numeric_type,
)

import pyomo.core.expr as EXPR
import pyomo.core.expr.numeric_expr as numeric_expr
from pyomo.core.base.component import ComponentData, ModelComponentFactory
from pyomo.core.base.global_set import UnindexedComponent_index
from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set
from pyomo.core.expr.numvalue import as_numeric
from pyomo.core.base.initializer import Initializer

logger = logging.getLogger('pyomo.core')


[docs] class NamedExpressionData(numeric_expr.NumericValue): """An object that defines a generic "named expression". This is the base class for both :class:`ExpressionData` and :class:`ObjectiveData`. """ # Note: derived classes are expected to declare the _args_ slot __slots__ = () EXPRESSION_SYSTEM = EXPR.ExpressionType.NUMERIC PRECEDENCE = 0 ASSOCIATIVITY = EXPR.OperatorAssociativity.NON_ASSOCIATIVE def __call__(self, exception=True): """Compute the value of this expression.""" (arg,) = self.args if arg.__class__ in native_types: # Note: native_types includes NoneType return arg return arg(exception=exception)
[docs] def create_node_with_local_data(self, values, classtype=None): """ Construct a simple expression after constructing the contained expression. This class provides a consistent interface for constructing a node, which is used in tree visitor scripts. """ if classtype is None: classtype = self.parent_component()._ComponentDataClass obj = classtype() obj._args_ = values return obj
[docs] def is_named_expression_type(self): """A boolean indicating whether this in a named expression.""" return True
[docs] def is_expression_type(self, expression_system=None): """A boolean indicating whether this in an expression.""" return expression_system is None or expression_system == self.EXPRESSION_SYSTEM
def arg(self, index): if index != 0: raise KeyError("Invalid index for expression argument: %d" % index) return self.args[0] @property def args(self): return self._args_ def nargs(self): return 1 def _to_string(self, values, verbose, smap): if verbose: return "%s{%s}" % (str(self), values[0]) if self.args[0] is None: return "%s{None}" % str(self) return values[0]
[docs] def clone(self): """Return a clone of this expression (no-op).""" return self
def _apply_operation(self, result): # This "expression" is a no-op wrapper, so just return the inner # result return result[0]
[docs] def polynomial_degree(self): """A tuple of subexpressions involved in this expressions operation.""" if self.args[0] is None: return None return self.expr.polynomial_degree()
def _compute_polynomial_degree(self, result): return result[0] def _is_fixed(self, values): return values[0] # NamedExpressionData should never return False because # they can store subexpressions that contain variables
[docs] def is_potentially_variable(self): return True
@property def expr(self): (arg,) = self.args if arg is None: return None return as_numeric(arg) @expr.setter def expr(self, value): self.set_value(value)
[docs] def set_value(self, expr): """Set the expression on this expression.""" if expr is None or expr.__class__ in native_numeric_types: self._args_ = (expr,) return try: if expr.is_numeric_type(): self._args_ = (expr,) return except AttributeError: if check_if_numeric_type(expr): self._args_ = (expr,) return raise ValueError( f"Cannot assign {expr.__class__.__name__} to " f"'{self.name}': {self.__class__.__name__} components only " "allow numeric expression types." )
[docs] def is_constant(self): """A boolean indicating whether this expression is constant.""" # The underlying expression can always be changed # so this should never evaluate as constant return False
[docs] def is_fixed(self): """A boolean indicating whether this expression is fixed.""" (e,) = self.args return e.__class__ in native_types or e.is_fixed()
# Override the in-place operators here so that we can redirect the # dispatcher based on the current contained expression type and not # this Expression object (which would map to "other") def __iadd__(self, other): (e,) = self.args return numeric_expr._add_dispatcher[e.__class__, other.__class__](e, other) # Note: the default implementation of __isub__ leverages __iadd__ # and doesn't need to be reimplemented here def __imul__(self, other): (e,) = self.args return numeric_expr._mul_dispatcher[e.__class__, other.__class__](e, other) def __idiv__(self, other): (e,) = self.args return numeric_expr._div_dispatcher[e.__class__, other.__class__](e, other) def __itruediv__(self, other): (e,) = self.args return numeric_expr._div_dispatcher[e.__class__, other.__class__](e, other) def __ipow__(self, other): (e,) = self.args return numeric_expr._pow_dispatcher[e.__class__, other.__class__](e, other)
class _ExpressionData(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData __renamed__version__ = '6.7.2' class _GeneralExpressionDataImpl(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData __renamed__version__ = '6.7.2'
[docs] class ExpressionData(NamedExpressionData, ComponentData): """An object that defines an expression that is never cloned Parameters ---------- expr : NumericValue The Pyomo expression stored in this expression. component : Expression The Expression object that owns this data. """ __slots__ = ('_args_',)
[docs] def __init__(self, expr=None, component=None): self._args_ = (expr,) self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET
class _GeneralExpressionData(metaclass=RenamedClass): __renamed__new_class__ = ExpressionData __renamed__version__ = '6.7.2'
[docs] @ModelComponentFactory.register( "Named expressions that can be used in other expressions." ) class Expression(IndexedComponent): """A shared expression container, which may be defined over an index. Parameters ---------- rule : ~.Initializer The source to use to initialize the expression(s) in this component. See :func:`.Initializer` for accepted argument types. initialize : A synonym for `rule` expr : A synonym for `rule` name : str Name of this component; will be overridden if this is assigned to a Block. doc : str Text describing this component. """ _ComponentDataClass = ExpressionData # This seems like a copy-paste error, and should be renamed/removed NoConstraint = IndexedComponent.Skip def __new__(cls, *args, **kwds): if cls != Expression: return super(Expression, cls).__new__(cls) if not args or (args[0] is UnindexedComponent_set and len(args) == 1): return ScalarExpression.__new__(ScalarExpression) else: return IndexedExpression.__new__(IndexedExpression) @overload def __init__( self, *indexes, rule=None, expr=None, initialize=None, name=None, doc=None ): ...
[docs] def __init__(self, *args, **kwds): _init = self._pop_from_kwargs( 'Expression', kwds, ('rule', 'expr', 'initialize'), None ) # Historically, Expression objects were dense (but None): # setting arg_not_specified causes Initializer to recognize # _init==None as a constant initializer returning None # # To initialize a completely empty Expression, pass either # initialize={} (to require explicit setitem before a getitem), # or initialize=NOTSET (to allow getitem before setitem) self._rule = Initializer(_init, arg_not_specified=NOTSET) kwds.setdefault('ctype', Expression) IndexedComponent.__init__(self, *args, **kwds)
def _pprint(self): return ( [ ('Size', len(self)), ('Index', None if (not self.is_indexed()) else self._index_set), ], self.items(), ("Expression",), lambda k, v: ["Undefined" if v.expr is None else v.expr], )
[docs] def display(self, prefix="", ostream=None): """TODO""" if not self.active: return if ostream is None: ostream = sys.stdout tab = " " ostream.write(prefix + self.local_name + " : ") ostream.write("Size=" + str(len(self))) ostream.write("\n") tabular_writer( ostream, prefix + tab, ((k, v) for k, v in self._data.items()), ("Value",), lambda k, v: ["Undefined" if v.expr is None else v()], )
# # A utility to extract all index-value pairs defining this # expression, returned as a dictionary. useful in many contexts, # in which key iteration and repeated __getitem__ calls are too # expensive to extract the contents of an expression. # def extract_values(self): return {key: expression_data.expr for key, expression_data in self.items()} # # takes as input a (index, value) dictionary for updating this # Expression. if check=True, then both the index and value are # checked through the __getitem__ method of this class. # def store_values(self, new_values): if (self.is_indexed() is False) and (not None in new_values): raise KeyError( "Cannot store value for scalar Expression" "=" + self.name + "; no value with index " "None in input new values map." ) for index, new_value in new_values.items(): self._data[index].set_value(new_value) def _getitem_when_not_present(self, idx): if self._rule is None: _init = None # TBD: Is this desired behavior? I can see implicitly setting # an Expression if it was not originally defined, but I am less # convinced that implicitly creating an Expression (like what # works with a Var) makes sense. [JDS 25 Nov 17] # raise KeyError(idx) else: _init = self._rule(self.parent_block(), idx) if _init is Expression.Skip: raise KeyError(idx) return self._setitem_when_not_present(idx, _init)
[docs] def construct(self, data=None): """Apply the rule to construct values in this set""" if self._constructed: return self._constructed = True timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug( "Constructing Expression, name=%s, from data=%s" % (self.name, str(data)) ) if self._anonymous_sets is not None: for _set in self._anonymous_sets: _set.construct() try: # We do not (currently) accept data for constructing Constraints assert data is None self._construct_from_rule_using_setitem() finally: timer.report()
[docs] class ScalarExpression(ExpressionData, Expression):
[docs] def __init__(self, *args, **kwds): ExpressionData.__init__(self, expr=None, component=self) Expression.__init__(self, *args, **kwds) self._index = UnindexedComponent_index
# # Override abstract interface methods to first check for # construction # def __call__(self, exception=True): """Return expression on this expression.""" if self._constructed: return super().__call__(exception) raise ValueError( "Evaluating the expression of Expression '%s' " "before the Expression has been constructed (there " "is currently no value to return)." % (self.name) ) @property def expr(self): """Return expression on this expression.""" if self._constructed: return ExpressionData.expr.fget(self) raise ValueError( "Accessing the expression of Expression '%s' " "before the Expression has been constructed (there " "is currently no value to return)." % (self.name) ) @expr.setter def expr(self, expr): """Set the expression on this expression.""" self.set_value(expr)
[docs] def clear(self): self._data = {}
[docs] def set_value(self, expr): """Set the expression on this expression.""" if self._constructed: return ExpressionData.set_value(self, expr) raise ValueError( "Setting the expression of Expression '%s' " "before the Expression has been constructed (there " "is currently no object to set)." % (self.name) )
[docs] def is_constant(self): """A boolean indicating whether this expression is constant.""" if self._constructed: return ExpressionData.is_constant(self) raise ValueError( "Accessing the is_constant flag of Expression '%s' " "before the Expression has been constructed (there " "is currently no value to return)." % (self.name) )
[docs] def is_fixed(self): """A boolean indicating whether this expression is fixed.""" if self._constructed: return ExpressionData.is_fixed(self) raise ValueError( "Accessing the is_fixed flag of Expression '%s' " "before the Expression has been constructed (there " "is currently no value to return)." % (self.name) )
# # Leaving this method for backward compatibility reasons. # (probably should be removed) #
[docs] def add(self, index, expr): """Add an expression with a given index.""" if index is not None: raise KeyError( "ScalarExpression object '%s' does not accept " "index values other than None. Invalid value: %s" % (self.name, index) ) if (type(expr) is tuple) and (expr == Expression.Skip): raise ValueError( "Expression.Skip can not be assigned " "to an Expression that is not indexed: %s" % (self.name) ) self.set_value(expr) return self
[docs] class SimpleExpression(metaclass=RenamedClass): __renamed__new_class__ = ScalarExpression __renamed__version__ = '6.0'
[docs] class IndexedExpression(Expression): # # Leaving this method for backward compatibility reasons # Note: It allows adding members outside of self._index_set. # This has always been the case. Not sure there is # any reason to maintain a reference to a separate # index set if we allow this. #
[docs] def add(self, index, expr): """Add an expression with a given index.""" if (type(expr) is tuple) and (expr == Expression.Skip): return None cdata = ExpressionData(expr, component=self) self._data[index] = cdata return cdata