Source code for pyomo.core.expr.relational_expr

# -*- coding: utf-8 -*-
#  ___________________________________________________________________________
#
#  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 operator

from pyomo.common.deprecation import deprecated
from pyomo.common.errors import PyomoException, DeveloperError
from pyomo.common.numeric_types import (
    native_numeric_types,
    check_if_numeric_type,
    value,
)

from .base import ExpressionBase
from .boolean_value import BooleanValue
from .expr_common import _lt, _le, _eq, ExpressionType
from .numvalue import is_potentially_variable, is_constant
from .visitor import polynomial_degree

# -------------------------------------------------------
#
# Expression classes
#
# -------------------------------------------------------


[docs] class RelationalExpression(ExpressionBase, BooleanValue): __slots__ = ('_args_',) EXPRESSION_SYSTEM = ExpressionType.RELATIONAL
[docs] def __init__(self, args): self._args_ = args
def __bool__(self): if self.is_constant(): return bool(self()) raise PyomoException( """ Cannot convert non-constant Pyomo expression (%s) to bool. This error is usually caused by using a Var, unit, or mutable Param in a Boolean context such as an "if" statement, or when checking container membership or equality. For example, >>> m.x = Var() >>> if m.x >= 1: ... pass and >>> m.y = Var() >>> if m.y in [m.x, m.y]: ... pass would both cause this exception.""".strip() % (self,) ) @property def args(self): """ Return the child nodes Returns: Either a list or tuple (depending on the node storage model) containing only the child nodes of this node """ return self._args_[: self.nargs()]
[docs] @deprecated( "is_relational() is deprecated in favor of " "is_expression_type(ExpressionType.RELATIONAL)", version='6.4.3', ) def is_relational(self): return self.is_expression_type(ExpressionType.RELATIONAL)
[docs] def is_potentially_variable(self): return any(is_potentially_variable(arg) for arg in self._args_)
[docs] def polynomial_degree(self): """ Return the polynomial degree of the expression. Returns: A non-negative integer that is the polynomial degree if the expression is polynomial, or :const:`None` otherwise. """ return polynomial_degree(self)
def _compute_polynomial_degree(self, result): # NB: We can't use max() here because None (non-polynomial) # overrides a numeric value (and max() just ignores it) ans = 0 for x in result: if x is None: return None elif ans < x: ans = x return ans def __eq__(self, other): """ Equal to operator This method is called when Python processes statements of the form:: self == other other == self """ return _generate_relational_expression(_eq, self, other) def __lt__(self, other): """ Less than operator This method is called when Python processes statements of the form:: self < other other > self """ return _generate_relational_expression(_lt, self, other) def __gt__(self, other): """ Greater than operator This method is called when Python processes statements of the form:: self > other other < self """ return _generate_relational_expression(_lt, other, self) def __le__(self, other): """ Less than or equal operator This method is called when Python processes statements of the form:: self <= other other >= self """ return _generate_relational_expression(_le, self, other) def __ge__(self, other): """ Greater than or equal operator This method is called when Python processes statements of the form:: self >= other other <= self """ return _generate_relational_expression(_le, other, self)
[docs] class RangedExpression(RelationalExpression): """ Ranged expressions, which define relations with a lower and upper bound:: x < y < z x <= y <= z args: args (tuple): child nodes strict (tuple): flags that indicate whether the inequalities are strict """ __slots__ = ('_strict',) PRECEDENCE = 9 # Shared tuples for the most common RangedExpression objects encountered # in math programming. Creating a single (shared) tuple saves memory STRICT = { False: (False, False), True: (True, True), (True, True): (True, True), (False, False): (False, False), (True, False): (True, False), (False, True): (False, True), }
[docs] def __init__(self, args, strict): super(RangedExpression, self).__init__(args) self._strict = RangedExpression.STRICT[strict]
[docs] def nargs(self): return 3
[docs] def create_node_with_local_data(self, args): return self.__class__(args, self._strict)
def _apply_operation(self, result): _l, _b, _r = result if not self._strict[0]: if not self._strict[1]: return _l <= _b and _b <= _r else: return _l <= _b and _b < _r elif not self._strict[1]: return _l < _b and _b <= _r else: return _l < _b and _b < _r def _to_string(self, values, verbose, smap): return "%s %s %s %s %s" % ( values[0], "<="[: 2 - self._strict[0]], values[1], "<="[: 2 - self._strict[1]], values[2], ) @property def strict(self): return self._strict
[docs] class InequalityExpression(RelationalExpression): """ Inequality expressions, which define less-than or less-than-or-equal relations:: x < y x <= y args: args (tuple): child nodes strict (bool): a flag that indicates whether the inequality is strict """ __slots__ = ('_strict',) PRECEDENCE = 9
[docs] def __init__(self, args, strict): super().__init__(args) self._strict = strict
[docs] def nargs(self): return 2
[docs] def create_node_with_local_data(self, args): return self.__class__(args, self._strict)
def _apply_operation(self, result): _l, _r = result if self._strict: return _l < _r return _l <= _r def _to_string(self, values, verbose, smap): return "%s %s %s" % (values[0], "<="[: 2 - self._strict], values[1]) @property def strict(self): return self._strict
[docs] def inequality(lower=None, body=None, upper=None, strict=False): """ A utility function that can be used to declare inequality and ranged inequality expressions. The expression:: inequality(2, model.x) is equivalent to the expression:: 2 <= model.x The expression:: inequality(2, model.x, 3) is equivalent to the expression:: 2 <= model.x <= 3 .. note:: This ranged inequality syntax is deprecated in Pyomo. This function provides a mechanism for expressing ranged inequalities without chained inequalities. args: lower: an expression defines a lower bound body: an expression defines the body of a ranged constraint upper: an expression defines an upper bound strict (bool): A boolean value that indicates whether the inequality is strict. Default is :const:`False`. Returns: A relational expression. The expression is an inequality if any of the values :attr:`lower`, :attr:`body` or :attr:`upper` is :const:`None`. Otherwise, the expression is a ranged inequality. """ if lower is None: if body is None or upper is None: raise ValueError("Invalid inequality expression.") return InequalityExpression((body, upper), strict) if body is None: if lower is None or upper is None: raise ValueError("Invalid inequality expression.") return InequalityExpression((lower, upper), strict) if upper is None: return InequalityExpression((lower, body), strict) return RangedExpression((lower, body, upper), strict)
[docs] class EqualityExpression(RelationalExpression): """ Equality expression:: x == y """ __slots__ = () PRECEDENCE = 9
[docs] def nargs(self): return 2
def __bool__(self): lhs, rhs = self.args if lhs is rhs: return True return super().__bool__() def _apply_operation(self, result): _l, _r = result return _l == _r def _to_string(self, values, verbose, smap): return "%s == %s" % (values[0], values[1])
[docs] class NotEqualExpression(RelationalExpression): """ Not-equal expression:: x != y """ __slots__ = ()
[docs] def nargs(self): return 2
def __bool__(self): lhs, rhs = self.args if lhs is not rhs: return True return super().__bool__() def _apply_operation(self, result): _l, _r = result return _l != _r def _to_string(self, values, verbose, smap): return "%s != %s" % (values[0], values[1])
_relational_op = { _eq: (operator.eq, '==', None), _le: (operator.le, '<=', False), _lt: (operator.lt, '<', True), } def _process_nonnumeric_arg(obj): if hasattr(obj, 'as_numeric'): # We assume non-numeric types that have an as_numeric method # are instances of AutoLinkedBooleanVar. Calling as_numeric # will return a valid Binary Var (and issue the appropriate # deprecation warning) obj = obj.as_numeric() elif check_if_numeric_type(obj): return obj else: # User assistance: provide a helpful exception when using an # indexed object in an expression if obj.is_component_type() and obj.is_indexed(): raise TypeError( "Argument for expression is an indexed numeric " "value\nspecified without an index:\n\t%s\nIs this " "value defined over an index that you did not specify?" % (obj.name,) ) raise TypeError( "Attempting to use a non-numeric type (%s) in a " "numeric expression context." % (obj.__class__.__name__,) ) def _process_relational_arg(arg, n): try: _numeric = arg.is_numeric_type() except AttributeError: _numeric = False if _numeric: if arg.is_constant(): arg = value(arg) else: _process_relational_arg.constant = False else: if arg.__class__ is InequalityExpression: _process_relational_arg.relational += n _process_relational_arg.constant = False else: arg = _process_nonnumeric_arg(arg) if arg.__class__ not in native_numeric_types: _process_relational_arg.constant = False return arg def _generate_relational_expression(etype, lhs, rhs): # Note that the use of "global" state flags is fast, but not # thread-safe. This should not be an issue because the GIL # effectively prevents parallel model construction. If we ever need # to revisit this design, we can pass in a "state" to # _process_relational_arg() - at the cost of creating/destroying the # state and an extra function argument. _process_relational_arg.relational = 0 _process_relational_arg.constant = True if lhs.__class__ not in native_numeric_types: lhs = _process_relational_arg(lhs, 1) if rhs.__class__ not in native_numeric_types: rhs = _process_relational_arg(rhs, 2) if _process_relational_arg.constant: return _relational_op[etype][0](value(lhs), value(rhs)) if etype == _eq: if _process_relational_arg.relational: raise TypeError( "Cannot create an EqualityExpression where one of the " "sub-expressions is a relational expression:\n" " %s\n {==}\n %s" % (lhs, rhs) ) return EqualityExpression((lhs, rhs)) elif _process_relational_arg.relational: if _process_relational_arg.relational == 1: return RangedExpression( lhs._args_ + (rhs,), (lhs._strict, _relational_op[etype][2]) ) elif _process_relational_arg.relational == 2: return RangedExpression( (lhs,) + rhs._args_, (_relational_op[etype][2], rhs._strict) ) else: # _process_relational_arg.relational == 3 raise TypeError( "Cannot create an InequalityExpression where both " "sub-expressions are relational expressions:\n" " %s\n {%s}\n %s" % (lhs, _relational_op[etype][1], rhs) ) else: return InequalityExpression((lhs, rhs), _relational_op[etype][2])
[docs] def tuple_to_relational_expr(args): if len(args) == 2: return EqualityExpression(args) else: return inequality(*args)