Source code for pyomo.core.expr.logical_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 types
from itertools import islice

import logging
import traceback

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

from pyomo.common.errors import PyomoException, DeveloperError
from pyomo.common.deprecation import (
    deprecation_warning,
    RenamedClass,
    relocated_module_attribute,
)
from .numvalue import (
    native_types,
    native_numeric_types,
    as_numeric,
    native_logical_types,
    value,
    is_potentially_variable,
)
from .base import ExpressionBase
from .boolean_value import BooleanValue, BooleanConstant
from .expr_common import _and, _or, _equiv, _inv, _xor, _impl, ExpressionType
from .numeric_expr import NumericExpression

import operator

relocated_module_attribute(
    'EqualityExpression',
    'pyomo.core.expr.relational_expr.EqualityExpression',
    version='6.4.3',
    f_globals=globals(),
)
relocated_module_attribute(
    'InequalityExpression',
    'pyomo.core.expr.relational_expr.InequalityExpression',
    version='6.4.3',
    f_globals=globals(),
)
relocated_module_attribute(
    'RangedExpression',
    'pyomo.core.expr.relational_expr.RangedExpression',
    version='6.4.3',
    f_globals=globals(),
)
relocated_module_attribute(
    'inequality',
    'pyomo.core.expr.relational_expr.inequality',
    version='6.4.3',
    f_globals=globals(),
)


def _generate_logical_proposition(etype, lhs, rhs):
    if (
        lhs.__class__ in native_types and lhs.__class__ not in native_logical_types
    ) and not isinstance(lhs, BooleanValue):
        return NotImplemented
    if (
        (rhs.__class__ in native_types and rhs.__class__ not in native_logical_types)
        and not isinstance(rhs, BooleanValue)
        and not (rhs is None and etype == _inv)
    ):
        return NotImplemented

    if etype == _equiv:
        return EquivalenceExpression((lhs, rhs))
    elif etype == _inv:
        assert rhs is None
        return NotExpression((lhs,))
    elif etype == _xor:
        return XorExpression((lhs, rhs))
    elif etype == _impl:
        return ImplicationExpression((lhs, rhs))
    elif etype == _and:
        return land(lhs, rhs)
    elif etype == _or:
        return lor(lhs, rhs)
    else:
        raise ValueError(
            "Unknown logical proposition type '%s'" % etype
        )  # pragma: no cover


[docs] class BooleanExpression(ExpressionBase, BooleanValue): """ Logical expression base class. This class is used to define nodes in an expression tree. Abstract args: args (list or tuple): Children of this node. """ __slots__ = ('_args_',) EXPRESSION_SYSTEM = ExpressionType.LOGICAL PRECEDENCE = 0
[docs] def __init__(self, args): self._args_ = args
@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] class BooleanExpressionBase(metaclass=RenamedClass): __renamed__new_class__ = BooleanExpression __renamed__version__ = '6.4.3'
""" ---------------------------******************-------------------- The following methods are static methods for nodes creator. Those should do the exact same thing as the class methods as well as overloaded operators. """
[docs] def lnot(Y): """ Construct a NotExpression for the passed BooleanValue. """ return NotExpression((Y,))
[docs] def equivalent(Y1, Y2): """ Construct an EquivalenceExpression Y1 == Y2 """ return EquivalenceExpression((Y1, Y2))
[docs] def xor(Y1, Y2): """ Construct an XorExpression Y1 xor Y2 """ return XorExpression((Y1, Y2))
[docs] def implies(Y1, Y2): """ Construct an Implication using function, where Y1 implies Y2 """ return ImplicationExpression((Y1, Y2))
def _flattened(args): """Flatten any potentially indexed arguments.""" for arg in args: if arg.__class__ in native_types: yield arg else: if isinstance(arg, (types.GeneratorType, list)): for _argdata in arg: yield _argdata elif arg.is_indexed(): for _argdata in arg.values(): yield _argdata else: yield arg def _flattened_boolean_args(args): """Flatten any potentially indexed arguments and check that they are Boolean-valued.""" for arg in args: if arg.__class__ in native_types: myiter = (arg,) elif isinstance(arg, (types.GeneratorType, list)): myiter = arg elif arg.is_indexed(): myiter = arg.values() else: myiter = (arg,) for _argdata in myiter: if _argdata.__class__ in native_logical_types: yield _argdata elif hasattr(_argdata, 'is_logical_type') and _argdata.is_logical_type(): yield _argdata elif isinstance(_argdata, BooleanValue): yield _argdata else: raise ValueError( "Non-Boolean-valued argument '%s' encountered when constructing " "expression of Boolean arguments" % arg ) def _flattened_numeric_args(args): """Flatten any potentially indexed arguments and check that they are numeric.""" for arg in args: if arg.__class__ in native_types: myiter = (arg,) elif isinstance(arg, (types.GeneratorType, list)): myiter = arg elif arg.is_indexed(): myiter = arg.values() else: myiter = (arg,) for _argdata in myiter: if _argdata.__class__ in native_numeric_types: yield _argdata elif hasattr(_argdata, 'is_numeric_type') and _argdata.is_numeric_type(): yield _argdata else: raise ValueError( "Non-numeric argument '%s' encountered when constructing " "expression with numeric arguments" % arg )
[docs] def land(*args): """ Construct an AndExpression between passed arguments. """ result = AndExpression([]) for argdata in _flattened_boolean_args(args): result = result.add(argdata) return result
[docs] def lor(*args): """ Construct an OrExpression between passed arguments. """ result = OrExpression([]) for argdata in _flattened_boolean_args(args): result = result.add(argdata) return result
[docs] def exactly(n, *args): """Creates a new ExactlyExpression Require exactly n arguments to be True, to make the expression True Usage: exactly(2, m.Y1, m.Y2, m.Y3, ...) """ result = ExactlyExpression([n] + list(_flattened_boolean_args(args))) return result
[docs] def atmost(n, *args): """Creates a new AtMostExpression Require at most n arguments to be True, to make the expression True Usage: atmost(2, m.Y1, m.Y2, m.Y3, ...) """ result = AtMostExpression([n] + list(_flattened_boolean_args(args))) return result
[docs] def atleast(n, *args): """Creates a new AtLeastExpression Require at least n arguments to be True, to make the expression True Usage: atleast(2, m.Y1, m.Y2, m.Y3, ...) """ result = AtLeastExpression([n] + list(_flattened_boolean_args(args))) return result
[docs] def all_different(*args): """Creates a new AllDifferentExpression Requires all of the arguments to take on a different value Usage: all_different(m.X1, m.X2, ...) """ return AllDifferentExpression(list(_flattened_numeric_args(args)))
[docs] def count_if(*args): """Creates a new CountIfExpression Counts the number of True-valued arguments Usage: count_if(m.Y1, m.Y2, ...) """ return CountIfExpression(list(_flattened_boolean_args(args)))
[docs] class UnaryBooleanExpression(BooleanExpression): """ Abstract class for single-argument logical expressions. """
[docs] def nargs(self): """ Returns number of arguments in expression """ return 1
[docs] class NotExpression(UnaryBooleanExpression): """ This is the node for a NotExpression, this node should have exactly one child """ PRECEDENCE = 2
[docs] def getname(self, *arg, **kwd): return 'Logical Negation'
def _to_string(self, values, verbose, smap): return "~%s" % values[0] def _apply_operation(self, result): return not result[0]
[docs] class BinaryBooleanExpression(BooleanExpression): """ Abstract class for binary logical expressions. """
[docs] def nargs(self): """ Return the number of argument the expression has """ return 2
[docs] class EquivalenceExpression(BinaryBooleanExpression): """ Logical equivalence statement: Y_1 iff Y_2. """ __slots__ = () PRECEDENCE = 6
[docs] def getname(self, *arg, **kwd): return 'iff'
def _to_string(self, values, verbose, smap): return " iff ".join(values) def _apply_operation(self, result): return result[0] == result[1]
[docs] class XorExpression(BinaryBooleanExpression): """ Logical Exclusive OR statement: Y_1 ⊻ Y_2 """ __slots__ = () PRECEDENCE = 4
[docs] def getname(self, *arg, **kwd): return 'xor'
def _to_string(self, values, verbose, smap): return " ⊻ ".join(values) def _apply_operation(self, result): return operator.xor(result[0], result[1])
[docs] class ImplicationExpression(BinaryBooleanExpression): """ Logical Implication statement: Y_1 --> Y_2. """ __slots__ = () PRECEDENCE = 6
[docs] def getname(self, *arg, **kwd): return 'implies'
def _to_string(self, values, verbose, smap): return " --> ".join(values) def _apply_operation(self, result): return (not result[0]) or result[1]
[docs] class NaryBooleanExpression(BooleanExpression): """ The abstract class for NaryBooleanExpression. This class should never be initialized. """ __slots__ = ('_nargs',)
[docs] def __init__(self, args): self._args_ = args self._nargs = len(self._args_)
[docs] def nargs(self): """ Return the number of expression arguments """ return self._nargs
[docs] def getname(self, *arg, **kwd): return 'NaryBooleanExpression'
def _add_to_and_or_expression(orig_expr, new_arg): """ Since AND and OR are Nary expressions, we extend the existing expression instead of creating a nested expression object if the types are compatible. """ # Clone 'self', because AndExpression/OrExpression are immutable if new_arg.__class__ is orig_expr.__class__: # adding new AndExpression/OrExpression on the right new_expr = orig_expr.__class__(orig_expr._args_) new_expr._args_.extend(islice(new_arg._args_, new_arg._nargs)) else: # adding new singleton on the right new_expr = orig_expr.__class__(orig_expr._args_) new_expr._args_.append(new_arg) # TODO set up id()-based scheme for avoiding duplicate entries new_expr._nargs = len(new_expr._args_) return new_expr
[docs] class AndExpression(NaryBooleanExpression): """ This is the node for AndExpression. """ __slots__ = () PRECEDENCE = 3
[docs] def getname(self, *arg, **kwd): return 'and'
def _to_string(self, values, verbose, smap): return " ∧ ".join(values) def _apply_operation(self, result): return all(result) def add(self, new_arg): if new_arg.__class__ in native_logical_types: if new_arg is False: return BooleanConstant(False) elif new_arg is True: return self return _add_to_and_or_expression(self, new_arg)
[docs] class OrExpression(NaryBooleanExpression): """ This is the node for OrExpression. """ __slots__ = () PRECEDENCE = 5
[docs] def getname(self, *arg, **kwd): return 'or'
def _to_string(self, values, verbose, smap): return " ∨ ".join(values) def _apply_operation(self, result): return any(result) def add(self, new_arg): if new_arg.__class__ in native_logical_types: if new_arg is False: return self elif new_arg is True: return BooleanConstant(True) return _add_to_and_or_expression(self, new_arg)
[docs] class ExactlyExpression(NaryBooleanExpression): """ Logical constraint that exactly N child statements are True. The first argument N is expected to be a numeric non-negative integer. Subsequent arguments are expected to be Boolean. Usage: exactly(1, True, False, False) --> True """ __slots__ = () PRECEDENCE = 9
[docs] def getname(self, *arg, **kwd): return 'exactly'
def _to_string(self, values, verbose, smap): return "exactly(%s: [%s])" % (values[0], ", ".join(values[1:])) def _apply_operation(self, result): return sum(result[1:]) == result[0]
[docs] class AtMostExpression(NaryBooleanExpression): """ Logical constraint that at most N child statements are True. The first argument N is expected to be a numeric non-negative integer. Subsequent arguments are expected to be Boolean. Usage: atmost(1, True, False, False) --> True """ __slots__ = () PRECEDENCE = 9
[docs] def getname(self, *arg, **kwd): return 'atmost'
def _to_string(self, values, verbose, smap): return "atmost(%s: [%s])" % (values[0], ", ".join(values[1:])) def _apply_operation(self, result): return sum(result[1:]) <= result[0]
[docs] class AtLeastExpression(NaryBooleanExpression): """ Logical constraint that at least N child statements are True. The first argument N is expected to be a numeric non-negative integer. Subsequent arguments are expected to be Boolean. Usage: atleast(1, True, False, False) --> True """ __slots__ = () PRECEDENCE = 9
[docs] def getname(self, *arg, **kwd): return 'atleast'
def _to_string(self, values, verbose, smap): return "atleast(%s: [%s])" % (values[0], ", ".join(values[1:])) def _apply_operation(self, result): return sum(result[1:]) >= result[0]
[docs] class AllDifferentExpression(NaryBooleanExpression): """ Logical expression that all of the N child statements have different values. All arguments are expected to be discrete-valued. """ __slots__ = () PRECEDENCE = None
[docs] def getname(self, *arg, **kwd): return 'all_different'
def _to_string(self, values, verbose, smap): return "all_different(%s)" % (", ".join(values)) def _apply_operation(self, result): last = None # we know these are integer-valued, so we can just sort them an make # sure that no adjacent pairs have the same value. for val in sorted(result): if last == val: return False last = val return True
[docs] class CountIfExpression(NumericExpression): """ Logical expression that returns the number of True child statements. All arguments are expected to be Boolean-valued. """ __slots__ = () PRECEDENCE = None # NumericExpression assumes binary operator, so we have to override.
[docs] def nargs(self): return len(self._args_)
[docs] def getname(self, *arg, **kwd): return 'count_if'
def _to_string(self, values, verbose, smap): return "count_if(%s)" % (", ".join(values)) def _apply_operation(self, result): return sum(value(r) for r in result)
special_boolean_atom_types = {ExactlyExpression, AtMostExpression, AtLeastExpression}