# ___________________________________________________________________________
#
# 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.
# ___________________________________________________________________________
from pyomo.core.expr.numvalue import (
ZeroConstant,
as_numeric,
is_potentially_variable,
is_numeric_data,
value,
)
from pyomo.core.expr.expr_common import ExpressionType
from pyomo.core.expr.relational_expr import (
EqualityExpression,
RangedExpression,
InequalityExpression,
)
from pyomo.core.kernel.base import ICategorizedObject, _abstract_readonly_property
from pyomo.core.kernel.container_utils import define_simple_containers
_pos_inf = float('inf')
_neg_inf = float('-inf')
_RELATIONAL = ExpressionType.RELATIONAL
[docs]
class IConstraint(ICategorizedObject):
"""The interface for constraints"""
__slots__ = ()
#
# Implementations can choose to define these
# properties as using __slots__, __dict__, or
# by overriding the @property method
#
body = _abstract_readonly_property(
doc="The expression for the body of the constraint"
)
lower = _abstract_readonly_property(
doc="The expression for the lower bound of the constraint"
)
upper = _abstract_readonly_property(
doc="The expression for the upper bound of the constraint"
)
lb = _abstract_readonly_property(
doc="The value of the lower bound of the constraint"
)
ub = _abstract_readonly_property(
doc="The value of the upper bound of the constraint"
)
rhs = _abstract_readonly_property(doc="The right-hand side of the constraint")
equality = _abstract_readonly_property(
doc=("A boolean indicating whether this is an equality constraint")
)
_linear_canonical_form = _abstract_readonly_property(
doc=(
"Indicates whether or not the class or "
"instance provides the properties that "
"define the linear canonical form of a "
"constraint"
)
)
#
# Interface
#
def __call__(self, exception=True):
"""Compute the value of the body of this constraint."""
if exception and (self.body is None):
raise ValueError("constraint body is None")
elif self.body is None:
return None
return self.body(exception=exception)
@property
def lslack(self):
"""Lower slack (body - lb). Returns :const:`None` if
a value for the body can not be computed."""
# this method is written so that constraint
# types that build the body expression on the
# fly do not have to here
body = self(exception=False)
if body is None:
return None
lb = self.lb
if lb is None:
lb = _neg_inf
else:
lb = value(lb)
return body - lb
@property
def uslack(self):
"""Upper slack (ub - body). Returns :const:`None` if
a value for the body can not be computed."""
# this method is written so that constraint
# types that build the body expression on the
# fly do not have to here
body = self(exception=False)
if body is None:
return None
ub = self.ub
if ub is None:
ub = _pos_inf
else:
ub = value(ub)
return ub - body
@property
def slack(self):
"""min(lslack, uslack). Returns :const:`None` if a
value for the body can not be computed."""
# this method is written so that constraint
# types that build the body expression on the
# fly do not have to here
body = self(exception=False)
if body is None:
return None
return min(self.lslack, self.uslack)
@property
def expr(self):
"""Get the expression on this constraint."""
body_expr = self.body
if body_expr is None:
return None
if self.equality:
return body_expr == self.rhs
else:
if self.lb is None:
return body_expr <= self.ub
elif self.ub is None:
return self.lb <= body_expr
return RangedExpression((self.lb, body_expr, self.ub), (False, False))
@property
def bounds(self):
"""The bounds of the constraint as a tuple (lb, ub)"""
return (self.lb, self.ub)
[docs]
def has_lb(self):
"""Returns :const:`False` when the lower bound is
:const:`None` or negative infinity"""
lb = self.lb
return (lb is not None) and (value(lb) != float('-inf'))
[docs]
def has_ub(self):
"""Returns :const:`False` when the upper bound is
:const:`None` or positive infinity"""
ub = self.ub
return (ub is not None) and (value(ub) != float('inf'))
def to_bounded_expression(self, evaluate_bounds=False):
if evaluate_bounds:
lb = self.lb
if lb == -float('inf'):
lb = None
ub = self.ub
if ub == float('inf'):
ub = None
return lb, self.body, ub
return self.lower, self.body, self.upper
class _MutableBoundsConstraintMixin(object):
"""
Use as a base class for IConstraint implementations
that allow adjusting the lb, ub, rhs, and equality
properties.
Assumes the derived class has _lb, _ub, and _equality
attributes that can be modified.
"""
__slots__ = ()
#
# Define some of the IConstraint abstract methods
#
@property
def lower(self):
"""The expression for the lower bound of the constraint"""
return self._lb
@lower.setter
def lower(self, lb):
if self.equality:
raise ValueError(
"The lower property can not be set "
"when the equality property is True."
)
if (lb is not None) and (not is_numeric_data(lb)):
raise TypeError(
"Constraint lower bounds must be "
"expressions restricted to numeric data."
)
self._lb = lb
@property
def upper(self):
"""The expression for the upper bound of the constraint"""
return self._ub
@upper.setter
def upper(self, ub):
if self.equality:
raise ValueError(
"The upper property can not be set "
"when the equality property is True."
)
if (ub is not None) and (not is_numeric_data(ub)):
raise TypeError(
"Constraint upper bounds must be "
"expressions restricted to numeric data."
)
self._ub = ub
@property
def lb(self):
"""The value of the lower bound of the constraint"""
lb = value(self.lower)
if lb == _neg_inf:
return None
return lb
@lb.setter
def lb(self, lb):
self.lower = lb
@property
def ub(self):
"""The value of the upper bound of the constraint"""
ub = value(self.upper)
if ub == _pos_inf:
return None
return ub
@ub.setter
def ub(self, ub):
self.upper = ub
@property
def rhs(self):
"""The right-hand side of the constraint"""
if not self.equality:
raise ValueError(
"The rhs property can not be read "
"when the equality property is False."
)
return self._lb
@rhs.setter
def rhs(self, rhs):
if rhs is None:
# None has a different meaning depending on the
# context (lb or ub), so there is no way to
# interpret this
raise ValueError(
"Constraint right-hand side can not be assigned a value of None."
)
elif not is_numeric_data(rhs):
raise TypeError(
"Constraint right-hand side must be numbers "
"or expressions restricted to data."
)
self._lb = rhs
self._ub = rhs
self._equality = True
@property
def bounds(self):
"""The bounds of the constraint as a tuple (lb, ub)"""
return super(_MutableBoundsConstraintMixin, self).bounds
@bounds.setter
def bounds(self, bounds_tuple):
self.lb, self.ub = bounds_tuple
@property
def equality(self):
"""Returns :const:`True` when this is an equality
constraint.
Disable equality by assigning
:const:`False`. Equality can only be activated by
assigning a value to the .rhs property."""
return self._equality
@equality.setter
def equality(self, equality):
if equality:
raise ValueError(
"The constraint equality flag can "
"only be set to True by assigning "
"a value to the rhs property "
"(e.g., con.rhs = con.lb)."
)
assert not equality
self._equality = False
[docs]
class constraint(_MutableBoundsConstraintMixin, IConstraint):
"""A general algebraic constraint
Algebraic constraints store relational expressions
composed of linear or nonlinear functions involving
decision variables.
Args:
expr: Sets the relational expression for the
constraint. Can be updated later by assigning to
the :attr:`expr` property on the
constraint. When this keyword is used, values
for the :attr:`body`, :attr:`lb`, :attr:`ub`,
and :attr:`rhs` attributes are automatically
determined based on the relational expression
type. Default value is :const:`None`.
body: Sets the body of the constraint. Can be
updated later by assigning to the :attr:`body`
property on the constraint. Default is
:const:`None`. This keyword should not be used
in combination with the :attr:`expr` keyword.
lb: Sets the lower bound of the constraint. Can be
updated later by assigning to the :attr:`lb`
property on the constraint. Default is
:const:`None`, which is equivalent to
:const:`-inf`. This keyword should not be used
in combination with the :attr:`expr` keyword.
ub: Sets the upper bound of the constraint. Can be
updated later by assigning to the :attr:`ub`
property on the constraint. Default is
:const:`None`, which is equivalent to
:const:`+inf`. This keyword should not be used
in combination with the :attr:`expr` keyword.
rhs: Sets the right-hand side of the constraint. Can
be updated later by assigning to the :attr:`rhs`
property on the constraint. The default value of
:const:`None` implies that this keyword is
ignored. Otherwise, use of this keyword implies
that the :attr:`equality` property is set to
:const:`True`. This keyword should not be used
in combination with the :attr:`expr` keyword.
Examples:
>>> import pyomo.kernel as pmo
>>> # A decision variable used to define constraints
>>> x = pmo.variable()
>>> # An upper bound constraint
>>> c = pmo.constraint(0.5*x <= 1)
>>> # (equivalent form)
>>> c = pmo.constraint(body=0.5*x, ub=1)
>>> # A range constraint
>>> c = pmo.constraint(lb=-1, body=0.5*x, ub=1)
>>> # An nonlinear equality constraint
>>> c = pmo.constraint(x**2 == 1)
>>> # (equivalent form)
>>> c = pmo.constraint(body=x**2, rhs=1)
"""
_ctype = IConstraint
_linear_canonical_form = False
__slots__ = (
"_parent",
"_storage_key",
"_active",
"_body",
"_lb",
"_ub",
"_equality",
"__weakref__",
)
[docs]
def __init__(self, expr=None, body=None, lb=None, ub=None, rhs=None):
self._parent = None
self._storage_key = None
self._active = True
self._body = None
self._lb = None
self._ub = None
self._equality = False
if expr is not None:
if body is not None:
raise ValueError(
"Both the 'expr' and 'body' "
"keywords can not be used to "
"initialize a constraint."
)
if lb is not None:
raise ValueError(
"Both the 'expr' and 'lb' "
"keywords can not be used to "
"initialize a constraint."
)
if ub is not None:
raise ValueError(
"Both the 'expr' and 'ub' "
"keywords can not be used to "
"initialize a constraint."
)
if rhs is not None:
raise ValueError(
"Both the 'expr' and 'rhs' "
"keywords can not be used to "
"initialize a constraint."
)
# call the setter
self.expr = expr
else:
self.body = body
if rhs is None:
self.lb = lb
self.ub = ub
else:
if (lb is not None) or (ub is not None):
raise ValueError(
"The 'rhs' keyword can not "
"be used with the 'lb' or "
"'ub' keywords to initialize"
" a constraint."
)
self.rhs = rhs
#
# Define the IConstraint abstract methods
#
@property
def body(self):
"""The body of the constraint"""
return self._body
@body.setter
def body(self, body):
if body is not None:
body = as_numeric(body)
self._body = body
#
# Extend the IConstraint interface to allow the
# expression on this constraint to be changed
# after construction.
#
@property
def expr(self):
"""Get or set the expression on this constraint."""
return super(constraint, self).expr
@expr.setter
def expr(self, expr):
self._equality = False
if expr is None:
self.body = None
self.lb = None
self.ub = None
return
_expr_type = expr.__class__
if _expr_type is tuple:
#
# Form equality expression
#
if len(expr) == 2:
arg0 = expr[0]
arg1 = expr[1]
# assigning to the rhs property
# will set the equality flag to True
if not is_potentially_variable(arg1):
self.rhs = arg1
self.body = arg0
elif not is_potentially_variable(arg0):
self.rhs = arg0
self.body = arg1
else:
self.rhs = ZeroConstant
self.body = arg0
self.body -= arg1
#
# Form inequality expression
#
elif len(expr) == 3:
arg0 = expr[0]
if arg0 is not None:
if not is_numeric_data(arg0):
raise ValueError(
"Constraint '%s' found a 3-tuple (lower,"
" expression, upper) but the lower "
"value was not numeric data or an "
"expression restricted to storage of "
"numeric data." % (self.name)
)
arg1 = expr[1]
if arg1 is not None:
arg1 = as_numeric(arg1)
arg2 = expr[2]
if arg2 is not None:
if not is_numeric_data(arg2):
raise ValueError(
"Constraint '%s' found a 3-tuple (lower,"
" expression, upper) but the upper "
"value was not numeric data or an "
"expression restricted to storage of "
"numeric data." % (self.name)
)
elif arg1 is not None and is_numeric_data(arg1):
# Special case (reflect behavior of AML): if the
# upper bound is None and the "body" is only data,
# then shift the body to the UB and the LB to the
# body
arg0, arg1, arg2 = arg2, arg0, arg1
self.lb = arg0
self.body = arg1
self.ub = arg2
else:
raise ValueError(
"Constraint '%s' assigned a tuple "
"of length %d. Expecting a tuple of "
"length 2 or 3:\n"
"Equality: (body, rhs)\n"
"Inequality: (lb, body, ub)" % (self.name, len(expr))
)
relational_expr = False
else:
try:
relational_expr = expr.is_expression_type(_RELATIONAL)
if not relational_expr:
raise ValueError(
"Constraint '%s' does not have a proper "
"value. Found '%s'\nExpecting a tuple or "
"equation. Examples:"
"\n sum_product(model.costs) == model.income"
"\n (0, model.price[item], 50)" % (self.name, str(expr))
)
except AttributeError:
msg = (
"Constraint '%s' does not have a proper "
"value. Found '%s'\nExpecting a tuple or "
"equation. Examples:"
"\n sum_product(model.costs) == model.income"
"\n (0, model.price[item], 50)" % (self.name, str(expr))
)
if type(expr) is bool:
msg += (
"\nNote: constant Boolean expressions "
"are not valid constraint expressions. "
"Some apparently non-constant compound "
"inequalities (e.g. 'expr >= 0 <= 1') "
"can return boolean values; the proper "
"form for compound inequalities is "
"always 'lb <= expr <= ub'."
)
raise ValueError(msg)
#
# Process relational expressions
# (i.e. explicit '==', '<', and '<=')
#
if relational_expr:
if _expr_type is EqualityExpression:
# assigning to the rhs property
# will set the equality flag to True
if not is_potentially_variable(expr.arg(1)):
self.rhs = expr.arg(1)
self.body = expr.arg(0)
elif not is_potentially_variable(expr.arg(0)):
self.rhs = expr.arg(0)
self.body = expr.arg(1)
else:
self.rhs = ZeroConstant
self.body = expr.arg(0)
self.body -= expr.arg(1)
elif _expr_type is InequalityExpression:
if expr._strict:
raise ValueError(
"Constraint '%s' encountered a strict "
"inequality expression ('>' or '<'). All"
" constraints must be formulated using "
"using '<=', '>=', or '=='." % (self.name)
)
if not is_potentially_variable(expr.arg(1)):
self.lb = None
self.body = expr.arg(0)
self.ub = expr.arg(1)
elif not is_potentially_variable(expr.arg(0)):
self.lb = expr.arg(0)
self.body = expr.arg(1)
self.ub = None
else:
self.lb = None
self.body = expr.arg(0)
self.body -= expr.arg(1)
self.ub = ZeroConstant
else: # RangedExpression
if any(expr._strict):
raise ValueError(
"Constraint '%s' encountered a strict "
"inequality expression ('>' or '<'). All"
" constraints must be formulated using "
"using '<=', '>=', or '=='." % (self.name)
)
if not is_numeric_data(expr.arg(0)):
raise ValueError(
"Constraint '%s' found a double-sided "
"inequality expression (lower <= "
"expression <= upper) but the lower "
"bound was not numeric data or an "
"expression restricted to storage of "
"numeric data." % (self.name)
)
if not is_numeric_data(expr.arg(2)):
raise ValueError(
"Constraint '%s' found a double-sided "
"inequality expression (lower <= "
"expression <= upper) but the upper "
"bound was not numeric data or an "
"expression restricted to storage of "
"numeric data." % (self.name)
)
self.lb = expr.arg(0)
self.body = expr.arg(1)
self.ub = expr.arg(2)
#
# Error check, to ensure that we don't have an equality
# constraint with 'infinite' RHS
#
assert not (self.equality and (self.lower is None))
assert (not self.equality) or (self.lower is self.upper)
#
# Note: This class is experimental. The implementation may
# change or it may go away.
#
[docs]
class linear_constraint(_MutableBoundsConstraintMixin, IConstraint):
"""A linear constraint
A linear constraint stores a linear relational
expression defined by a list of variables and
coefficients. This class can be used to reduce build
time and memory for an optimization model. It also
increases the speed at which the model can be output to
a solver.
Args:
variables (list): Sets the list of variables in the
linear expression defining the body of the
constraint. Can be updated later by assigning to
the :attr:`variables` property on the
constraint.
coefficients (list): Sets the list of coefficients
for the variables in the linear expression
defining the body of the constraint. Can be
updated later by assigning to the
:attr:`coefficients` property on the constraint.
terms (list): An alternative way of initializing the
:attr:`variables` and :attr:`coefficients` lists
using an iterable of (variable, coefficient)
tuples. Can be updated later by assigning to the
:attr:`terms` property on the constraint. This
keyword should not be used in combination with
the :attr:`variables` or :attr:`coefficients`
keywords.
lb: Sets the lower bound of the constraint. Can be
updated later by assigning to the :attr:`lb`
property on the constraint. Default is
:const:`None`, which is equivalent to
:const:`-inf`.
ub: Sets the upper bound of the constraint. Can be
updated later by assigning to the :attr:`ub`
property on the constraint. Default is
:const:`None`, which is equivalent to
:const:`+inf`.
rhs: Sets the right-hand side of the constraint. Can
be updated later by assigning to the :attr:`rhs`
property on the constraint. The default value of
:const:`None` implies that this keyword is
ignored. Otherwise, use of this keyword implies
that the :attr:`equality` property is set to
:const:`True`.
Examples:
>>> import pyomo.kernel as pmo
>>> # Decision variables used to define constraints
>>> x = pmo.variable()
>>> y = pmo.variable()
>>> # An upper bound constraint
>>> c = pmo.linear_constraint(variables=[x,y], coefficients=[1,2], ub=1)
>>> # (equivalent form)
>>> c = pmo.linear_constraint(terms=[(x,1), (y,2)], ub=1)
>>> # (equivalent form using a general constraint)
>>> c = pmo.constraint(x + 2*y <= 1)
"""
_ctype = IConstraint
_linear_canonical_form = True
__slots__ = (
"_parent",
"_storage_key",
"_active",
"_variables",
"_coefficients",
"_lb",
"_ub",
"_equality",
"__weakref__",
)
[docs]
def __init__(
self, variables=None, coefficients=None, terms=None, lb=None, ub=None, rhs=None
):
self._parent = None
self._storage_key = None
self._active = True
self._variables = None
self._coefficients = None
self._lb = None
self._ub = None
self._equality = False
if terms is not None:
if (variables is not None) or (coefficients is not None):
raise ValueError(
"Both the 'variables' and 'coefficients' "
"keywords must be None when the 'terms' "
"keyword is not None"
)
# use the setter method
self.terms = terms
elif (variables is not None) or (coefficients is not None):
if (variables is None) or (coefficients is None):
raise ValueError(
"Both the 'variables' and 'coefficients' "
"keywords must be set when the 'terms' "
"keyword is None"
)
self._variables = tuple(variables)
self._coefficients = tuple(coefficients)
else:
# it is okay to initialize this with nothing
self._variables = ()
self._coefficients = ()
if rhs is None:
self.lb = lb
self.ub = ub
else:
if (lb is not None) or (ub is not None):
raise ValueError(
"The 'rhs' keyword can not "
"be used with the 'lb' or "
"'ub' keywords to initialize"
" a constraint."
)
self.rhs = rhs
@property
def terms(self):
"""An iterator over the terms in the body of this
constraint as (variable, coefficient) tuples"""
return zip(self._variables, self._coefficients)
@terms.setter
def terms(self, terms):
"""Set the terms in the body of this constraint
using an iterable of (variable, coefficient) tuples"""
transpose = tuple(zip(*terms))
if len(transpose) == 2:
self._variables, self._coefficients = transpose
else:
assert transpose == ()
self._variables = ()
self._coefficients = ()
#
# Override a the default __call__ method on IConstraint
# to avoid building the body expression
#
def __call__(self, exception=True):
try:
return sum(
value(c, exception=exception) * v(exception=exception)
for v, c in self.terms
)
except (ValueError, TypeError):
if exception:
raise ValueError("one or more terms could not be evaluated")
return None
#
# Define the IConstraint abstract methods
#
@property
def body(self):
"""The body of the constraint"""
return sum(c * v for v, c in self.terms)
#
# Define methods that writers expect when the
# _linear_canonical_form flag is True
#
# inserts class definitions for simple _tuple, _list, and
# _dict containers into this module
define_simple_containers(globals(), "constraint", IConstraint)