# ___________________________________________________________________________
#
# 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 collections import namedtuple
from pyomo.common.deprecation import RenamedClass
from pyomo.common.log import is_debug_set
from pyomo.common.timing import ConstructionTimer
import pyomo.core.expr as EXPR
from pyomo.core.expr.numvalue import ZeroConstant, native_numeric_types, as_numeric
from pyomo.core import Constraint, Var, Block, Set
from pyomo.core.base.component import ModelComponentFactory
from pyomo.core.base.global_set import UnindexedComponent_index
from pyomo.core.base.block import BlockData
from pyomo.core.base.disable_methods import disable_methods
from pyomo.core.base.initializer import (
Initializer,
IndexedCallInitializer,
CountedCallInitializer,
)
import logging
logger = logging.getLogger('pyomo.core')
#
# A named 2-tuple that minimizes error checking
#
ComplementarityTuple = namedtuple('ComplementarityTuple', ('arg0', 'arg1'))
[docs]
def complements(a, b):
"""Return a named 2-tuple"""
return ComplementarityTuple(a, b)
[docs]
class ComplementarityData(BlockData):
def _canonical_expression(self, e):
# Note: as the complimentarity component maintains references to
# the original expression (e), it is NOT safe or valid to bypass
# the clone checks: bypassing the check can result in corrupting
# the original expressions and will result in mind-boggling
# pprint output.
e_ = None
if e.__class__ is EXPR.EqualityExpression:
if e.arg(1).__class__ in native_numeric_types or e.arg(1).is_fixed():
_e = (e.arg(1), e.arg(0))
#
# The first argument of an equality is never fixed
#
else:
_e = (ZeroConstant, e.arg(0) - e.arg(1))
elif e.__class__ is EXPR.InequalityExpression:
if e.arg(1).__class__ in native_numeric_types or e.arg(1).is_fixed():
_e = (None, e.arg(0), e.arg(1))
elif e.arg(0).__class__ in native_numeric_types or e.arg(0).is_fixed():
_e = (e.arg(0), e.arg(1), None)
else:
_e = (ZeroConstant, e.arg(1) - e.arg(0), None)
elif e.__class__ is EXPR.RangedExpression:
_e = (e.arg(0), e.arg(1), e.arg(2))
else:
_e = (None, e, None)
return _e
def to_standard_form(self):
#
# Add auxiliary variables and constraints that ensure
# a monotone transformation of general complementary constraints to
# the form:
# l1 <= v1 <= u1 OR l2 <= v2 <= u2
#
# Note that this transformation creates more variables and
# constraints than are strictly necessary. However, we don't
# have a complete list of the variables used in a model's
# complementarity conditions when adding a single condition, so
# we add additional variables.
#
# This has the form:
#
# e: l1 <= expression <= l2
# v: l3 <= var <= l4
#
# where exactly two of l1, l2, l3 and l4 are finite, and with the
# equality constraint:
#
# c: v == expression
#
_e1 = self._canonical_expression(self._args[0])
_e2 = self._canonical_expression(self._args[1])
if len(_e1) == 2:
# Ignore _e2; _e1 is an equality constraint
self.c = Constraint(expr=_e1)
return
if len(_e2) == 2:
# Ignore _e1; _e2 is an equality constraint
self.c = Constraint(expr=_e2)
return
#
if (_e1[0] is None) + (_e1[2] is None) + (_e2[0] is None) + (
_e2[2] is None
) != 2:
raise RuntimeError(
"Complementarity condition %s must have exactly two finite bounds"
% self.name
)
#
if _e1[0] is None and _e1[2] is None:
# Only e2 will be an unconstrained expression
_e1, _e2 = _e2, _e1
#
if _e2[0] is None and _e2[2] is None:
self.c = Constraint(expr=(None, _e2[1], None))
self.c._complementarity_type = 3
elif _e2[2] is None:
self.c = Constraint(expr=_e2[0] <= _e2[1])
self.c._complementarity_type = 1
elif _e2[0] is None:
self.c = Constraint(expr=-_e2[2] <= -_e2[1])
self.c._complementarity_type = 1
#
if not _e1[0] is None and not _e1[2] is None:
if not (_e1[0].__class__ in native_numeric_types or _e1[0].is_constant()):
raise RuntimeError(
"Cannot express a complementarity problem of the form L < v < U _|_ g(x) where L is not a constant value"
)
if not (_e1[2].__class__ in native_numeric_types or _e1[2].is_constant()):
raise RuntimeError(
"Cannot express a complementarity problem of the form L < v < U _|_ g(x) where U is not a constant value"
)
self.v = Var(bounds=(_e1[0], _e1[2]))
self.ve = Constraint(expr=self.v == _e1[1])
elif _e1[2] is None:
self.v = Var(bounds=(0, None))
self.ve = Constraint(expr=self.v == _e1[1] - _e1[0])
else:
# _e1[0] is None:
self.v = Var(bounds=(0, None))
self.ve = Constraint(expr=self.v == _e1[2] - _e1[1])
[docs]
def set_value(self, cc):
"""
Add a complementarity condition with a specified index.
"""
if cc.__class__ is ComplementarityTuple:
#
# The ComplementarityTuple has a fixed length, so we initialize
# the _args component and return
#
self._args = (cc.arg0, cc.arg1)
#
elif cc.__class__ is tuple:
if len(cc) != 2:
raise ValueError(
"Invalid tuple for Complementarity %s (expected 2-tuple):"
"\n\t%s" % (self.name, cc)
)
self._args = cc
elif cc is Complementarity.Skip:
del self.parent_component()[self.index()]
elif cc.__class__ is list:
#
# Call set_value() recursively to apply the error same error
# checks.
#
return self.set_value(tuple(cc))
else:
raise ValueError(
"Unexpected value for Complementarity %s:\n\t%s" % (self.name, cc)
)
class _ComplementarityData(metaclass=RenamedClass):
__renamed__new_class__ = ComplementarityData
__renamed__version__ = '6.7.2'
[docs]
@ModelComponentFactory.register("Complementarity conditions.")
class Complementarity(Block):
_ComponentDataClass = ComplementarityData
def __new__(cls, *args, **kwds):
if cls != Complementarity:
return super(Complementarity, cls).__new__(cls)
if args == ():
return super(Complementarity, cls).__new__(AbstractScalarComplementarity)
else:
return super(Complementarity, cls).__new__(IndexedComplementarity)
@staticmethod
def _complementarity_rule(b, *idx):
_rule = b.parent_component()._init_rule
if _rule is None:
return
cc = _rule(b.parent_block(), idx)
if cc is None:
raise ValueError(
"""
Invalid complementarity condition. The complementarity condition
is None instead of a 2-tuple. Please modify your rule to return
Complementarity.Skip instead of None.
Error thrown for Complementarity "%s"."""
% (b.name,)
)
b.set_value(cc)
[docs]
def __init__(self, *args, **kwargs):
kwargs.setdefault('ctype', Complementarity)
kwargs.setdefault('dense', False)
_init = tuple(
_arg
for _arg in (
kwargs.pop('initialize', None),
kwargs.pop('rule', None),
kwargs.pop('expr', None),
)
if _arg is not None
)
if len(_init) > 1:
raise ValueError(
"Duplicate initialization: Complementarity() only accepts "
"one of 'initialize=', 'rule=', and 'expr='"
)
elif _init:
_init = _init[0]
else:
_init = None
self._init_rule = Initializer(
_init, treat_sequences_as_mappings=False, allow_generators=True
)
if self._init_rule is not None:
kwargs['rule'] = Complementarity._complementarity_rule
Block.__init__(self, *args, **kwargs)
# HACK to make the "counted call" syntax work. We wait until
# after the base class is set up so that is_indexed() is
# reliable.
if (
self._init_rule is not None
and self._init_rule.__class__ is IndexedCallInitializer
):
self._init_rule = CountedCallInitializer(self, self._init_rule)
[docs]
def add(self, index, cc):
"""
Add a complementarity condition with a specified index.
"""
if cc is Complementarity.Skip:
return
_block = self[index]
_block.set_value(cc)
return _block
def _pprint(self):
"""
Return data that will be printed for this component.
"""
_table_data = lambda k, v: [v._args[0], v._args[1], v.active]
# This is a bit weird, but is being implemented to preserve
# backwards compatibility. The Complementarity transformation
# is "in place", in that modeling components are added to this
# block. If the transformation has been executed (or if any
# components have been added to the block), we want to output
# the block components as well as the normal complementarity
# table.
#
# TODO: In the future we should probably reconsider how
# Complementarity is implemented and move away from this
# paradigm.
#
# FIXME: remove the _transformed check and only invoke
# _pprint_callback if there are components (requires baseline
# updates and a check that we do not break anything in the
# Book).
_transformed = not issubclass(self.ctype, Complementarity)
def _conditional_block_printer(ostream, idx, data):
if _transformed or len(data.component_map()):
self._pprint_callback(ostream, idx, data)
return (
[
("Size", len(self)),
("Index", self._index_set if self.is_indexed() else None),
("Active", self.active),
],
self._data.items(),
("Arg0", "Arg1", "Active"),
(_table_data, _conditional_block_printer),
)
[docs]
class ScalarComplementarity(ComplementarityData, Complementarity):
[docs]
def __init__(self, *args, **kwds):
ComplementarityData.__init__(self, self)
Complementarity.__init__(self, *args, **kwds)
self._data[None] = self
self._index = UnindexedComponent_index
[docs]
class SimpleComplementarity(metaclass=RenamedClass):
__renamed__new_class__ = ScalarComplementarity
__renamed__version__ = '6.0'
[docs]
@disable_methods({'add', 'set_value', 'to_standard_form'})
class AbstractScalarComplementarity(ScalarComplementarity):
pass
[docs]
class AbstractSimpleComplementarity(metaclass=RenamedClass):
__renamed__new_class__ = AbstractScalarComplementarity
__renamed__version__ = '6.0'
[docs]
class IndexedComplementarity(Complementarity):
pass
[docs]
@ModelComponentFactory.register("A list of complementarity conditions.")
class ComplementarityList(IndexedComplementarity):
"""
A complementarity component that represents a list of complementarity
conditions. Each condition can be indexed by its index, but when added
an index value is not specified.
"""
End = (1003,)
[docs]
def __init__(self, **kwargs):
"""Constructor"""
args = (Set(),)
self._nconditions = 0
Complementarity.__init__(self, *args, **kwargs)
# disable the implicit rule; construct will exhaust the
# user-provided rule, and then subsequent attempts to add a CC
# will bypass the rule
self._rule = None
[docs]
def add(self, expr):
"""
Add a complementarity condition with an implicit index.
"""
self._nconditions += 1
self._index_set.add(self._nconditions)
return Complementarity.add(self, self._nconditions, expr)
[docs]
def construct(self, data=None):
"""
Construct the expression(s) for this complementarity condition.
"""
if self._constructed:
return
self._constructed = True
timer = ConstructionTimer(self)
if is_debug_set(logger):
logger.debug("Constructing complementarity list %s", self.name)
if self._anonymous_sets is not None:
for _set in self._anonymous_sets:
_set.construct()
if self._init_rule is not None:
_init = self._init_rule(self.parent_block(), ())
for cc in iter(_init):
if cc is ComplementarityList.End:
break
if cc is Complementarity.Skip:
continue
self.add(cc)
timer.report()