Source code for pyomo.core.kernel.variable

#  ___________________________________________________________________________
#
#  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.common.modeling import NoArgumentGiven
from pyomo.core.staleflag import StaleFlagManager
from pyomo.core.expr.numvalue import NumericValue, is_numeric_data, value
from pyomo.core.kernel.base import ICategorizedObject, _abstract_readwrite_property
from pyomo.core.kernel.container_utils import define_simple_containers
from pyomo.core.kernel.set_types import RealSet, IntegerSet

_pos_inf = float('inf')
_neg_inf = float('-inf')


def _extract_domain_type_and_bounds(domain_type, domain, lb, ub):
    if domain is not None:
        if domain_type is not None:
            raise ValueError(
                "At most one of the 'domain' and "
                "'domain_type' keywords can be changed "
                "from their default value when "
                "initializing a variable."
            )
        domain_lb, domain_ub, domain_step = domain.get_interval()
        if domain_step == 0:
            domain_type = RealSet
        elif domain_step == 1:
            domain_type = IntegerSet
        # else: domain_type will remain None and generate an exception below
        if domain_lb is not None:
            if lb is not None:
                raise ValueError(
                    "The 'lb' keyword can not be used "
                    "to initialize a variable when the "
                    "domain lower bound is finite."
                )
            lb = domain_lb
        if domain_ub is not None:
            if ub is not None:
                raise ValueError(
                    "The 'ub' keyword can not be used "
                    "to initialize a variable when the "
                    "domain upper bound is finite."
                )
            ub = domain_ub
    elif domain_type is None:
        domain_type = RealSet
    if domain_type not in IVariable._valid_domain_types:
        raise ValueError(
            "Domain type '%s' is not valid. Must be "
            "one of: %s" % (domain_type, IVariable._valid_domain_types)
        )

    return domain_type, lb, ub


[docs] class IVariable(ICategorizedObject, NumericValue): """The interface for decision variables""" __slots__ = () _valid_domain_types = (RealSet, IntegerSet) # # Implementations can choose to define these # properties as using __slots__, __dict__, or # by overriding the @property method # domain_type = _abstract_readwrite_property( doc=( "The domain type of the variable " "(:class:`RealSet` or :class:`IntegerSet`)" ) ) lb = _abstract_readwrite_property(doc="The lower bound of the variable") ub = _abstract_readwrite_property(doc="The upper bound of the variable") value = _abstract_readwrite_property(doc="The value of the variable") fixed = _abstract_readwrite_property(doc="The fixed status of the variable") stale = _abstract_readwrite_property(doc="The stale status of the variable") # # Interface # @property def bounds(self): """Get/Set the bounds as a tuple (lb, ub).""" return (self.lb, self.ub) @bounds.setter def bounds(self, bounds_tuple): self.lower, self.upper = bounds_tuple @property def lb(self): """Return the numeric value of the variable lower bound.""" lb = value(self.lower) if lb == _neg_inf: return None return lb @lb.setter def lb(self, val): self.lower = val @property def ub(self): """Return the numeric value of the variable upper bound.""" ub = value(self.upper) if ub == _pos_inf: return None return ub @ub.setter def ub(self, val): self.upper = val
[docs] def fix(self, value=NoArgumentGiven): """ Fix the variable. Sets the fixed indicator to :const:`True`. An optional value argument will update the variable's value before fixing. """ if value is not NoArgumentGiven: self.value = value self.fixed = True
[docs] def unfix(self): """Free the variable. Sets the fixed indicator to :const:`False`.""" self.fixed = False
free = unfix
[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 (lb != _neg_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 (ub != _pos_inf)
@property def lslack(self): """Lower slack (value - lb). Returns :const:`None` if the variable value is :const:`None`.""" val = self.value if val is None: return None lb = self.lb if lb is None: lb = _neg_inf return val - lb @property def uslack(self): """Upper slack (ub - value). Returns :const:`None` if the variable value is :const:`None`.""" val = self.value if val is None: return None ub = self.ub if ub is None: ub = _pos_inf return ub - val @property def slack(self): """min(lslack, uslack). Returns :const:`None` if the variable value is :const:`None`.""" # this method is written so that constraint # types that build the body expression on the # fly do not have to here val = self.value if val is None: return None return min(self.lslack, self.uslack) # # Convenience methods mainly used by the solver # interfaces #
[docs] def is_continuous(self): """Returns :const:`True` when the domain type is :class:`RealSet`.""" return self.domain_type.get_interval()[2] == 0
# this could be expanded to include semi-continuous # where as is_integer would not
[docs] def is_discrete(self): """Returns :const:`True` when the domain type is :class:`IntegerSet`.""" return self.domain_type.get_interval()[2] not in (0, None)
[docs] def is_integer(self): """Returns :const:`True` when the domain type is :class:`IntegerSet`.""" return self.domain_type.get_interval()[2] == 1
[docs] def is_binary(self): """Returns :const:`True` when the domain type is :class:`IntegerSet` and the bounds are within [0,1].""" return self.domain_type.get_interval()[2] == 1 and (self.lb, self.ub) in { (0, 1), (0, 0), (1, 1), }
# TODO? # def is_semicontinuous(self): # """Returns :const:`True` when the domain class is # SemiContinuous.""" # return issubclass(self.domain_type, SemiRealSet) # def is_semiinteger(self): # """Returns :const:`True` when the domain class is # SemiInteger.""" # return issubclass(self.domain_type, SemiIntegerSet) # # Implement the NumericValue abstract methods #
[docs] def is_fixed(self): """Returns :const:`True` if this variable is fixed, otherwise returns :const:`False`.""" return self.fixed
[docs] def is_constant(self): """Returns :const:`False` because this is not a constant in an expression.""" return False
[docs] def is_parameter_type(self): """Returns :const:`False` because this is not a parameter object.""" return False
[docs] def is_variable_type(self): """Returns :const:`True` because this is a variable object.""" return True
[docs] def is_potentially_variable(self): """Returns :const:`True` because this is a variable.""" return True
[docs] def polynomial_degree(self): """Return the polynomial degree of this expression""" # If the variable is fixed, it represents a constant; # otherwise, it has degree 1. if self.fixed: return 0 return 1
def __call__(self, exception=True): """Return the value of this variable.""" if exception and (self.value is None): raise ValueError("value is None") return self.value
[docs] class variable(IVariable): """A decision variable Decision variables are used in objectives and constraints to define an optimization problem. Args: domain_type: Sets the domain type of the variable. Must be one of :const:`RealSet` or :const:`IntegerSet`. Can be updated later by assigning to the :attr:`domain_type` property. The default value of :const:`None` is equivalent to :const:`RealSet`, unless the :attr:`domain` keyword is used. domain: Sets the domain of the variable. This updates the :attr:`domain_type`, :attr:`lb`, and :attr:`ub` properties of the variable. The default value of :const:`None` implies that this keyword is ignored. This keyword can not be used in combination with the :attr:`domain_type` keyword. lb: Sets the lower bound of the variable. Can be updated later by assigning to the :attr:`lb` property on the variable. Default is :const:`None`, which is equivalent to :const:`-inf`. ub: Sets the upper bound of the variable. Can be updated later by assigning to the :attr:`ub` property on the variable. Default is :const:`None`, which is equivalent to :const:`+inf`. value: Sets the value of the variable. Can be updated later by assigning to the :attr:`value` property on the variable. Default is :const:`None`. fixed (bool): Sets the fixed status of the variable. Can be updated later by assigning to the :attr:`fixed` property or by calling the :meth:`fix` method. Default is :const:`False`. Examples: >>> import pyomo.kernel as pmo >>> # A continuous variable with infinite bounds >>> x = pmo.variable() >>> # A binary variable >>> x = pmo.variable(domain=pmo.Binary) >>> # Also a binary variable >>> x = pmo.variable(domain_type=pmo.IntegerSet, lb=0, ub=1) """ _ctype = IVariable __slots__ = ( "_parent", "_storage_key", "_domain_type", "_active", "_lb", "_ub", "_value", "_fixed", "_stale", "__weakref__", )
[docs] def __init__( self, domain_type=None, domain=None, lb=None, ub=None, value=None, fixed=False ): self._parent = None self._storage_key = None self._active = True self._domain_type = RealSet self._lb = lb self._ub = ub self._value = value self._fixed = fixed self._stale = 0 # True if (domain_type is not None) or (domain is not None): self._domain_type, self._lb, self._ub = _extract_domain_type_and_bounds( domain_type, domain, lb, ub )
@property def lower(self): """The lower bound of the variable""" return self._lb @lower.setter def lower(self, lb): if (lb is not None) and (not is_numeric_data(lb)): raise ValueError( "Variable lower bounds must be numbers or " "expressions restricted to numeric data." ) self._lb = lb @property def upper(self): """The upper bound of the variable""" return self._ub @upper.setter def upper(self, ub): if (ub is not None) and (not is_numeric_data(ub)): raise ValueError( "Variable upper bounds must be numbers or " "expressions restricted to numeric data." ) self._ub = ub @property def value(self): """The value of the variable""" return self._value @value.setter def value(self, value): self._value = value self._stale = StaleFlagManager.get_flag(self._stale) def set_value(self, value, skip_validation=True): self.value = value @property def fixed(self): """The fixed status of the variable""" return self._fixed @fixed.setter def fixed(self, fixed): self._fixed = fixed @property def stale(self): """The stale status of the variable""" return StaleFlagManager.is_stale(self._stale) @stale.setter def stale(self, stale): if stale: self._stale = 0 else: self._stale = StaleFlagManager.get_flag(0) @property def domain_type(self): """The domain type of the variable (:class:`RealSet` or :class:`IntegerSet`)""" return self._domain_type @domain_type.setter def domain_type(self, domain_type): if domain_type not in IVariable._valid_domain_types: raise ValueError( "Domain type '%s' is not valid. Must be " "one of: %s" % (self.domain_type, IVariable._valid_domain_types) ) self._domain_type = domain_type def _set_domain(self, domain): """Set the domain of the variable. This method updates the :attr:`domain_type` property and overwrites the :attr:`lb` and :attr:`ub` properties with the domain bounds.""" self.domain_type, self.lb, self.ub = _extract_domain_type_and_bounds( None, domain, None, None ) domain = property(fset=_set_domain, doc=_set_domain.__doc__)
# inserts class definitions for simple _tuple, _list, and # _dict containers into this module define_simple_containers(globals(), "variable", IVariable)