Source code for pyomo.core.base.boolean_var

#  ___________________________________________________________________________
#
#  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 logging
from weakref import ref as weakref_ref, ReferenceType

from pyomo.common.deprecation import deprecation_warning, RenamedClass
from pyomo.common.log import is_debug_set
from pyomo.common.modeling import unique_component_name, NOTSET
from pyomo.common.timing import ConstructionTimer
from pyomo.core.staleflag import StaleFlagManager
from pyomo.core.expr.boolean_value import BooleanValue
from pyomo.core.expr import GetItemExpression
from pyomo.core.expr.numvalue import value
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.base.misc import apply_indexed_rule
from pyomo.core.base.set import Set, BooleanSet, Binary
from pyomo.core.base.util import is_functor
from pyomo.core.base.var import Var


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

_logical_var_types = {bool, type(None)}


class _DeprecatedImplicitAssociatedBinaryVariable(object):
    __slots__ = ('_boolvar',)

    def __init__(self, boolvar):
        self._boolvar = weakref_ref(boolvar)

    def __call__(self):
        deprecation_warning(
            "Relying on core.logical_to_linear to transform "
            "BooleanVars that do not appear in LogicalConstraints "
            "is deprecated. Please associate your own binaries if "
            "you have BooleanVars not used in logical expressions.",
            version='6.2',
        )

        parent_block = self._boolvar().parent_block()
        new_var = Var(domain=Binary)
        parent_block.add_component(
            unique_component_name(
                parent_block, self._boolvar().local_name + "_asbinary"
            ),
            new_var,
        )
        self._boolvar()._associated_binary = None
        self._boolvar().associate_binary_var(new_var)
        return new_var

    def __getstate__(self):
        return self._boolvar()

    def __setstate__(self, state):
        self._boolvar = weakref_ref(state)


def _associated_binary_mapper(encode, val):
    if val is None:
        return None
    if encode:
        if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable:
            return val()
    else:
        if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable:
            return weakref_ref(val)
    return val


[docs] class BooleanVarData(ComponentData, BooleanValue): """This class defines the data for a single Boolean variable. Parameters ---------- component: Component The BooleanVar object that owns this data. Attributes ---------- fixed: bool If True, then this variable is treated as a fixed constant in the model. """ __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') __autoslot_mappers__ = { '_associated_binary': _associated_binary_mapper, '_stale': StaleFlagManager.stale_mapper, }
[docs] def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: # - BooleanVarData # - ComponentData # - BooleanValue self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET self._value = None self.fixed = False self._stale = 0 # True self._associated_binary = None
[docs] def is_fixed(self): """Returns True if this variable is fixed, otherwise returns False.""" return self.fixed
[docs] def is_constant(self): """Returns False because this is not a constant in an expression.""" return False
[docs] def is_variable_type(self): """Returns True because this is a variable.""" return True
[docs] def is_potentially_variable(self): """Returns True because this is a variable.""" return True
[docs] def set_value(self, val, skip_validation=False): """ Set the value of this numeric object, after validating its value. If the 'valid' flag is True, then the validation step is skipped. """ # Note that it is basically as fast to check the type as it is # to check the skip_validation flag. Considering that we expect # the flag to always be False, we will just ignore it in the # name of efficiency. if val.__class__ not in _logical_var_types: if not skip_validation: logger.warning( "implicitly casting '%s' value %s to bool" % (self.name, val) ) val = bool(val) self._value = val self._stale = StaleFlagManager.get_flag(self._stale)
def clear(self): self.value = None def __call__(self, exception=True): """Compute the value of this variable.""" return self.value @property def value(self): """bool : the current value for this variable.""" return self._value @value.setter def value(self, val): self.set_value(val) @property def domain(self): """BooleanSet : the domain for this variable.""" return BooleanSet @property def stale(self): """ bool : A Boolean indicating whether the value of this variable is Consistent with the most recent solve. `True` indicates that this variable's value was set prior to the most recent solve and was not updated by the results returned by the solve. """ return StaleFlagManager.is_stale(self._stale) @stale.setter def stale(self, val): if val: self._stale = 0 else: self._stale = StaleFlagManager.get_flag(0)
[docs] def get_associated_binary(self): """Get the binary VarData associated with this BooleanVarData""" return ( self._associated_binary() if self._associated_binary is not None else None )
[docs] def associate_binary_var(self, binary_var): """Associate a binary VarData to this BooleanVarData""" if ( self._associated_binary is not None and type(self._associated_binary) is not _DeprecatedImplicitAssociatedBinaryVariable ): raise RuntimeError( "Reassociating BooleanVar '%s' (currently associated " "with '%s') with '%s' is not allowed" % ( self.name, ( self._associated_binary().name if self._associated_binary is not None else None ), binary_var.name if binary_var is not None else None, ) ) if binary_var is not None: self._associated_binary = weakref_ref(binary_var)
[docs] def fix(self, value=NOTSET, skip_validation=False): """Fix the value of this variable (treat as nonvariable) This sets the `fixed` indicator to True. If ``value`` is provided, the value (and the ``skip_validation`` flag) are first passed to :py:meth:`set_value()`. """ self.fixed = True if value is not NOTSET: self.set_value(value, skip_validation)
[docs] def unfix(self): """Unfix this variable (treat as variable) This sets the `fixed` indicator to False. """ self.fixed = False
[docs] def free(self): """Alias for :py:meth:`unfix`""" return self.unfix()
class _BooleanVarData(metaclass=RenamedClass): __renamed__new_class__ = BooleanVarData __renamed__version__ = '6.7.2' class _GeneralBooleanVarData(metaclass=RenamedClass): __renamed__new_class__ = BooleanVarData __renamed__version__ = '6.7.2'
[docs] @ModelComponentFactory.register("Logical decision variables.") class BooleanVar(IndexedComponent): """A logical variable, which may be defined over an index. Args: initialize (float or function, optional): The initial value for the variable, or a rule that returns initial values. rule (float or function, optional): An alias for `initialize`. dense (bool, optional): Instantiate all elements from `index_set()` when constructing the Var (True) or just the variables returned by `initialize`/`rule` (False). Defaults to True. """ _ComponentDataClass = BooleanVarData def __new__(cls, *args, **kwds): if cls != BooleanVar: return super(BooleanVar, cls).__new__(cls) if not args or (args[0] is UnindexedComponent_set and len(args) == 1): return ScalarBooleanVar.__new__(ScalarBooleanVar) else: return IndexedBooleanVar.__new__(IndexedBooleanVar)
[docs] def __init__(self, *args, **kwd): initialize = kwd.pop('initialize', None) initialize = kwd.pop('rule', initialize) self._dense = kwd.pop('dense', True) kwd.setdefault('ctype', BooleanVar) IndexedComponent.__init__(self, *args, **kwd) # Allow for functions or functors for value initialization, # without confusing with Params, etc (which have a __call__ method). # self._value_init_value = None self._value_init_rule = None if is_functor(initialize) and (not isinstance(initialize, BooleanValue)): self._value_init_rule = initialize else: self._value_init_value = initialize
[docs] def flag_as_stale(self): """ Set the 'stale' attribute of every variable data object to True. """ for boolvar_data in self._data.values(): boolvar_data._stale = 0
[docs] def get_values(self, include_fixed_values=True): """ Return a dictionary of index-value pairs. """ if include_fixed_values: return dict((idx, vardata.value) for idx, vardata in self._data.items()) return dict( (idx, vardata.value) for idx, vardata in self._data.items() if not vardata.fixed )
extract_values = get_values
[docs] def set_values(self, new_values, skip_validation=False): """ Set data values from a dictionary. The default behavior is to validate the values in the dictionary. """ for index, new_value in new_values.items(): self[index].set_value(new_value, skip_validation)
[docs] def construct(self, data=None): """Construct this component.""" if is_debug_set(logger): # pragma:nocover try: name = str(self.name) except: name = type(self) logger.debug( "Constructing Variable, name=%s, from data=%s" % (name, str(data)) ) if self._constructed: return timer = ConstructionTimer(self) self._constructed = True if self._anonymous_sets is not None: for _set in self._anonymous_sets: _set.construct() # # Construct BooleanVarData objects for all index values # if not self.is_indexed(): self._data[None] = self self._initialize_members((None,)) elif self._dense: # This loop is optimized for speed with pypy. # Calling dict.update((...) for ...) is roughly # 30% slower self_weakref = weakref_ref(self) for ndx in self._index_set: cdata = self._ComponentDataClass(component=None) cdata._component = self_weakref self._data[ndx] = cdata # NOTE: This is a special case where a key, value pair is added # to the _data dictionary without calling # _getitem_when_not_present, which is why we need to set the # index here. cdata._index = ndx self._initialize_members(self._index_set) timer.report()
[docs] def add(self, index): """Add a variable with a particular index.""" return self[index]
# # This method must be defined on subclasses of # IndexedComponent that support implicit definition # def _getitem_when_not_present(self, index): """Returns the default component data value.""" if index is None and not self.is_indexed(): obj = self._data[index] = self else: obj = self._data[index] = self._ComponentDataClass(component=self) self._initialize_members((index,)) obj._index = index return obj def _setitem_when_not_present(self, index, value): """Perform the fundamental component item creation and storage. BooleanVar overrides the default implementation from IndexedComponent to enforce the call to _initialize_members. """ obj = self._getitem_when_not_present(index) try: return obj.set_value(value) except: del self._data[index] raise def _initialize_members(self, init_set): """Initialize variable data for all indices in a set.""" # # Initialize values # if self._value_init_rule is not None: # # Initialize values with a rule # if self.is_indexed(): for key in init_set: vardata = self._data[key] val = apply_indexed_rule( self, self._value_init_rule, self._parent(), key ) val = value(val) vardata.set_value(val) else: val = self._value_init_rule(self._parent()) val = value(val) self.set_value(val) elif self._value_init_value is not None: # # Initialize values with a value if self._value_init_value.__class__ is dict: for key in init_set: # Skip indices that are not in the # dictionary. This arises when # initializing VarList objects with a # dictionary. # What does this continue do here? if not key in self._value_init_value: continue val = self._value_init_value[key] vardata = self._data[key] vardata.set_value(val) else: val = value(self._value_init_value) for key in init_set: vardata = self._data[key] vardata.set_value(val) def _pprint(self): """ Print component information. """ return ( [ ("Size", len(self)), ("Index", self._index_set if self.is_indexed() else None), ], self._data.items(), ("Value", "Fixed", "Stale"), lambda k, v: [v.value, v.fixed, v.stale], )
[docs] class ScalarBooleanVar(BooleanVarData, BooleanVar): """A single variable."""
[docs] def __init__(self, *args, **kwd): BooleanVarData.__init__(self, component=self) BooleanVar.__init__(self, *args, **kwd) self._index = UnindexedComponent_index
# # Override abstract interface methods to first check for # construction # # NOTE: that we can't provide these errors for # fixed and stale because they are attributes @property def value(self): """bool : the current value of this variable.""" if self._constructed: return BooleanVarData.value.fget(self) raise ValueError( "Accessing the value of variable '%s' " "before the Var has been constructed (there " "is currently no value to return)." % (self.name) ) @value.setter def value(self, val): if self._constructed: return BooleanVarData.value.fset(self, val) raise ValueError( "Setting the value of variable '%s' " "before the Var has been constructed (there " "is currently nothing to set." % (self.name) ) @property def domain(self): """BooleanSet : the domain for this variable.""" return BooleanVarData.domain.fget(self)
[docs] def fix(self, value=NOTSET, skip_validation=False): """ Set the fixed indicator to True. Value argument is optional, indicating the variable should be fixed at its current value. """ if self._constructed: return BooleanVarData.fix(self, value, skip_validation) raise ValueError( "Fixing variable '%s' " "before the Var has been constructed (there " "is currently nothing to set)." % (self.name) )
[docs] def unfix(self): """Sets the fixed indicator to False.""" if self._constructed: return BooleanVarData.unfix(self) raise ValueError( "Freeing variable '%s' " "before the Var has been constructed (there " "is currently nothing to set)." % (self.name) )
[docs] class SimpleBooleanVar(metaclass=RenamedClass): __renamed__new_class__ = ScalarBooleanVar __renamed__version__ = '6.0'
[docs] class IndexedBooleanVar(BooleanVar): """An array of variables."""
[docs] def fix(self, value=NOTSET, skip_validation=False): """Fix all variables in this IndexedBooleanVar (treat as nonvariable) This sets the `fixed` indicator to True for every variable in this IndexedBooleanVar. If ``value`` is provided, the value (and the ``skip_validation`` flag) are first passed to :py:meth:`set_value()`. """ for boolean_vardata in self.values(): boolean_vardata.fix(value, skip_validation)
[docs] def unfix(self): """Unfix all variables in this IndexedBooleanVar (treat as variable) This sets the `fixed` indicator to False for every variable in this IndexedBooleanVar. """ for boolean_vardata in self.values(): boolean_vardata.unfix()
[docs] def free(self): """Alias for :py:meth:`unfix`""" return self.unfix()
@property def domain(self): """BooleanSet : the domain for this variable.""" return BooleanSet # Because Emma wants crazy things... (Where crazy things are the ability to # index BooleanVars by other (integer) Vars and integer-valued # expressions--a thing you can do in Constraint Programming.) def __getitem__(self, args): tmp = args if args.__class__ is tuple else (args,) if any( hasattr(arg, 'is_potentially_variable') and arg.is_potentially_variable() for arg in tmp ): return GetItemExpression((self,) + tmp) return super().__getitem__(args)
[docs] @ModelComponentFactory.register("List of logical decision variables.") class BooleanVarList(IndexedBooleanVar): """ Variable-length indexed variable objects used to construct Pyomo models. """
[docs] def __init__(self, **kwargs): self._starting_index = kwargs.pop('starting_index', 1) args = (Set(),) IndexedBooleanVar.__init__(self, *args, **kwargs)
[docs] def construct(self, data=None): """Construct this component.""" if is_debug_set(logger): logger.debug("Constructing variable list %s", self.name) # We need to ensure that the indices needed for initialization are # added to the underlying implicit set. We *could* verify that the # indices in the initialization dict are all sequential integers, # OR we can just add the correct number of sequential integers and # then let _validate_index complain when we set the value. if self._value_init_value.__class__ is dict: for i in range(len(self._value_init_value)): self._index_set.add(i + self._starting_index) super(BooleanVarList, self).construct(data) # Note that the current Var initializer silently ignores # initialization data that is not in the underlying index set. To # ensure that at least here all initialization data is added to the # VarList (so we get potential domain errors), we will re-set # everything. if self._value_init_value.__class__ is dict: for k, v in self._value_init_value.items(): self[k] = v
[docs] def add(self): """Add a variable to this list.""" next_idx = len(self._index_set) + self._starting_index self._index_set.add(next_idx) return self[next_idx]