Source code for pyomo.core.base.constraint

#  ___________________________________________________________________________
#
#  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 __future__ import annotations
import sys
import logging
from weakref import ref as weakref_ref
from pyomo.common.pyomo_typing import overload
from typing import Union, Type

from pyomo.common.deprecation import RenamedClass
from pyomo.common.errors import DeveloperError, TemplateExpressionError
from pyomo.common.formatting import tabular_writer
from pyomo.common.log import is_debug_set
from pyomo.common.modeling import NOTSET
from pyomo.common.timing import ConstructionTimer

from pyomo.core.expr.numvalue import (
    NumericValue,
    value,
    as_numeric,
    is_fixed,
    native_numeric_types,
    native_logical_types,
    native_types,
)
from pyomo.core.expr import (
    ExpressionType,
    EqualityExpression,
    InequalityExpression,
    RangedExpression,
)
from pyomo.core.expr.template_expr import templatize_constraint
from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory
from pyomo.core.base.global_set import UnindexedComponent_index
from pyomo.core.base.indexed_component import (
    ActiveIndexedComponent,
    UnindexedComponent_set,
    rule_wrapper,
    IndexedComponent,
)
from pyomo.core.base.set import Set
from pyomo.core.base.disable_methods import disable_methods
from pyomo.core.base.initializer import (
    Initializer,
    IndexedCallInitializer,
    CountedCallInitializer,
)


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

TEMPLATIZE_CONSTRAINTS = False

_inf = float('inf')
_nonfinite_values = {_inf, -_inf}
_known_relational_expressions = {
    EqualityExpression,
    InequalityExpression,
    RangedExpression,
}
_strict_relational_exprs = {True, (False, True), (True, False), (True, True)}
_rule_returned_none_error = """Constraint '%s': rule returned None.

Constraint rules must return either a valid expression, a 2- or 3-member
tuple, or one of Constraint.Skip, Constraint.Feasible, or
Constraint.Infeasible.  The most common cause of this error is
forgetting to include the "return" statement at the end of your rule.
"""


[docs] def simple_constraint_rule(rule): """ This is a decorator that translates None/True/False return values into Constraint.Skip/Constraint.Feasible/Constraint.Infeasible. This supports a simpler syntax in constraint rules, though these can be more difficult to debug when errors occur. Example use: .. code:: @simple_constraint_rule def C_rule(model, i, j): # ... model.c = Constraint(rule=simple_constraint_rule(...)) """ map_types = set([type(None)]) | native_logical_types result_map = {None: Constraint.Skip} for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible result_map[l_type(False)] = Constraint.Infeasible # Note: some logical types hash the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map return rule_wrapper(rule, result_map, map_types=map_types)
[docs] def simple_constraintlist_rule(rule): """ This is a decorator that translates None/True/False return values into ConstraintList.End/Constraint.Feasible/Constraint.Infeasible. This supports a simpler syntax in constraint rules, though these can be more difficult to debug when errors occur. Example use: .. code:: @simple_constraintlist_rule def C_rule(model, i, j): # ... model.c = ConstraintList(expr=simple_constraintlist_rule(...)) """ map_types = set([type(None)]) | native_logical_types result_map = {None: ConstraintList.End} for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible result_map[l_type(False)] = Constraint.Infeasible # Note: some logical types hash the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map return rule_wrapper(rule, result_map, map_types=map_types)
[docs] class ConstraintData(ActiveComponentData): """This class defines the data for a single algebraic constraint. Parameters ---------- expr : ExpressionBase The Pyomo expression stored in this constraint. component : Constraint The Constraint object that owns this data. """ __slots__ = ('_expr',) # Set to true when a constraint class stores its expression # in linear canonical form _linear_canonical_form = False
[docs] def __init__(self, expr=None, component=None): # # These lines represent in-lining of the # following constructors: # - ConstraintData # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None self._active = True self._expr = None if expr is not None: self.set_value(expr)
def __call__(self, exception=True): """Compute the value of the body of this constraint.""" body = self.to_bounded_expression()[1] if body.__class__ not in native_numeric_types: body = value(self.body, exception=exception) return body
[docs] def to_bounded_expression(self, evaluate_bounds=False): """Convert this constraint to a tuple of 3 expressions (lb, body, ub) This method "standardizes" the expression into a 3-tuple of expressions: (`lower_bound`, `body`, `upper_bound`). Upon conversion, `lower_bound` and `upper_bound` are guaranteed to be `None`, numeric constants, or fixed (not necessarily constant) expressions. Note ---- As this method operates on the *current state* of the expression, any required expression manipulations (and by extension, the result) can change after fixing / unfixing :py:class:`Var` objects. Parameters ---------- evaluate_bounds: bool If True, then the lower and upper bounds will be evaluated to a finite numeric constant or None. Raises ------ ValueError: Raised if the expression cannot be mapped to this form (i.e., :py:class:`RangedExpression` constraints with variable lower or upper bounds. """ expr = self._expr if expr.__class__ is RangedExpression: lb, body, ub = ans = expr.args if ( lb.__class__ not in native_types and lb.is_potentially_variable() and not lb.is_fixed() ): raise ValueError( f"Constraint '{self.name}' is a Ranged Inequality with a " "variable lower bound. Cannot normalize the " "constraint or send it to a solver." ) if ( ub.__class__ not in native_types and ub.is_potentially_variable() and not ub.is_fixed() ): raise ValueError( f"Constraint '{self.name}' is a Ranged Inequality with a " "variable upper bound. Cannot normalize the " "constraint or send it to a solver." ) elif expr is None: ans = None, None, None else: lhs, rhs = expr.args if rhs.__class__ in native_types or not rhs.is_potentially_variable(): ans = rhs if expr.__class__ is EqualityExpression else None, lhs, rhs elif lhs.__class__ in native_types or not lhs.is_potentially_variable(): ans = lhs, rhs, lhs if expr.__class__ is EqualityExpression else None else: ans = 0 if expr.__class__ is EqualityExpression else None, lhs - rhs, 0 if evaluate_bounds: lb, body, ub = ans return self._evaluate_bound(lb, True), body, self._evaluate_bound(ub, False) return ans
def _evaluate_bound(self, bound, is_lb): if bound is None: return None if bound.__class__ not in native_numeric_types: bound = float(value(bound)) # Note that "bound != bound" catches float('nan') if bound in _nonfinite_values or bound != bound: if bound == (-_inf if is_lb else _inf): return None raise ValueError( f"Constraint '{self.name}' created with an invalid non-finite " f"{'lower' if is_lb else 'upper'} bound ({bound})." ) return bound @property def body(self): """The body (variable portion) of a constraint expression.""" try: ans = self.to_bounded_expression()[1] except ValueError: # It is possible that the expression is not currently valid # (i.e., a ranged expression with a non-fixed bound). We # will catch that exception here and - if this actually *is* # a RangedExpression - return the body. if self._expr.__class__ is RangedExpression: _, ans, _ = self._expr.args else: raise if ans.__class__ in native_types and ans is not None: # Historically, constraint.lower was guaranteed to return a type # derived from Pyomo NumericValue (or None). Replicate that. # # [JDS 6/2024: it would be nice to remove this behavior, # although possibly unnecessary, as people should use # to_bounded_expression() instead] return as_numeric(ans) return ans @property def lower(self): """The lower bound of a constraint expression. This is the fixed lower bound of a Constraint as a Pyomo expression. This may contain potentially variable terms that are currently fixed. If there is no lower bound, this will return `None`. """ ans = self.to_bounded_expression()[0] if ans.__class__ in native_types and ans is not None: # Historically, constraint.lower was guaranteed to return a type # derived from Pyomo NumericValue (or None). Replicate that # functionality, although clients should in almost all cases # move to using ConstraintData.lb instead of accessing # lower/body/upper to avoid the unnecessary creation (and # inevitable destruction) of the NumericConstant wrappers. return as_numeric(ans) return ans @property def upper(self): """Access the upper bound of a constraint expression. This is the fixed upper bound of a Constraint as a Pyomo expression. This may contain potentially variable terms that are currently fixed. If there is no upper bound, this will return `None`. """ ans = self.to_bounded_expression()[2] if ans.__class__ in native_types and ans is not None: # Historically, constraint.upper was guaranteed to return a type # derived from Pyomo NumericValue (or None). Replicate that # functionality, although clients should in almost all cases # move to using ConstraintData.lb instead of accessing # lower/body/upper to avoid the unnecessary creation (and # inevitable destruction) of the NumericConstant wrappers. return as_numeric(ans) return ans @property def lb(self): """float : the value of the lower bound of a constraint expression.""" return self._evaluate_bound(self.to_bounded_expression()[0], True) @property def ub(self): """float : the value of the upper bound of a constraint expression.""" return self._evaluate_bound(self.to_bounded_expression()[2], False) @property def equality(self): """bool : True if this is an equality constraint.""" expr = self.expr if expr.__class__ is EqualityExpression: return True elif expr.__class__ is RangedExpression: # TODO: this is a very restrictive form of structural equality. lb = expr.arg(0) if lb is not None and lb is expr.arg(2): return True return False @property def strict_lower(self): """bool : True if this constraint has a strict lower bound.""" return False @property def strict_upper(self): """bool : True if this constraint has a strict upper bound.""" return False
[docs] def has_lb(self): """Returns :const:`False` when the lower bound is :const:`None` or negative infinity""" return self.lb is not None
[docs] def has_ub(self): """Returns :const:`False` when the upper bound is :const:`None` or positive infinity""" return self.ub is not None
@property def expr(self): """Return the expression associated with this constraint.""" return self._expr
[docs] def get_value(self): """Get the expression on this constraint.""" return self.expr
[docs] def set_value(self, expr): """Set the expression on this constraint.""" # Clear any previously-cached normalized constraint self._expr = None if expr.__class__ in _known_relational_expressions: if getattr(expr, 'strict', False) in _strict_relational_exprs: raise ValueError( "Constraint '%s' encountered a strict " "inequality expression ('>' or '<'). All " "constraints must be formulated using " "using '<=', '>=', or '=='." % (self.name,) ) self._expr = expr elif expr.__class__ is tuple: # or expr_type is list: for arg in expr: if ( arg is None or arg.__class__ in native_numeric_types or isinstance(arg, NumericValue) ): continue raise ValueError( "Constraint '%s' does not have a proper value. " "Constraint expressions expressed as tuples must " "contain native numeric types or Pyomo NumericValue " "objects. Tuple %s contained invalid type, %s" % (self.name, expr, arg.__class__.__name__) ) if len(expr) == 2: # # Form equality expression # if expr[0] is None or expr[1] is None: raise ValueError( "Constraint '%s' does not have a proper value. " "Equality Constraints expressed as 2-tuples " "cannot contain None [received %s]" % (self.name, expr) ) self._expr = EqualityExpression(expr) elif len(expr) == 3: # # Form (ranged) inequality expression # if expr[0] is None: self._expr = InequalityExpression(expr[1:], False) elif expr[2] is None: self._expr = InequalityExpression(expr[:2], False) else: self._expr = RangedExpression(expr, False) else: raise ValueError( "Constraint '%s' does not have a proper value. " "Found a tuple of length %d. Expecting a tuple of " "length 2 or 3:\n" " Equality: (left, right)\n" " Inequality: (lower, expression, upper)" % (self.name, len(expr)) ) # # Ignore an 'empty' constraint # elif expr.__class__ is type: del self.parent_component()[self.index()] if expr is Constraint.Skip: return elif expr is Constraint.Infeasible: # TODO: create a trivial infeasible constraint. This # could be useful in the case of GDP where certain # disjuncts are trivially infeasible, but we would still # like to express the disjunction. # del self.parent_component()[self.index()] raise ValueError("Constraint '%s' is always infeasible" % (self.name,)) else: raise ValueError( "Constraint '%s' does not have a proper " "value. Found '%s'\nExpecting a tuple or " "relational expression. Examples:" "\n sum(model.costs) == model.income" "\n (0, model.price[item], 50)" % (self.name, str(expr)) ) elif expr is None: raise ValueError(_rule_returned_none_error % (self.name,)) elif expr.__class__ is bool: raise ValueError( "Invalid constraint expression. The constraint " "expression resolved to a trivial Boolean (%s) " "instead of a Pyomo object. Please modify your " "rule to return Constraint.%s instead of %s." "\n\nError thrown for Constraint '%s'" % (expr, "Feasible" if expr else "Infeasible", expr, self.name) ) else: try: if expr.is_expression_type(ExpressionType.RELATIONAL): self._expr = expr except AttributeError: pass if self._expr is None: msg = ( "Constraint '%s' does not have a proper " "value. Found '%s'\nExpecting a tuple or " "relational expression. Examples:" "\n sum(model.costs) == model.income" "\n (0, model.price[item], 50)" % (self.name, str(expr)) ) raise ValueError(msg)
[docs] def lslack(self): """ Returns the value of f(x)-L for constraints of the form: L <= f(x) (<= U) (U >=) f(x) >= L """ lb = self.lb if lb is None: return _inf else: return value(self.body) - lb
[docs] def uslack(self): """ Returns the value of U-f(x) for constraints of the form: (L <=) f(x) <= U U >= f(x) (>= L) """ ub = self.ub if ub is None: return _inf else: return ub - value(self.body)
[docs] def slack(self): """ Returns the smaller of lslack and uslack values """ lb = self.lb ub = self.ub body = value(self.body) if lb is None: return ub - body elif ub is None: return body - lb return min(ub - body, body - lb)
class _ConstraintData(metaclass=RenamedClass): __renamed__new_class__ = ConstraintData __renamed__version__ = '6.7.2' class _GeneralConstraintData(metaclass=RenamedClass): __renamed__new_class__ = ConstraintData __renamed__version__ = '6.7.2'
[docs] class TemplateConstraintData(ConstraintData): __slots__ = ()
[docs] def __init__(self, template_info, component, index): # These lines represent in-lining of the # following constructors: # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = component self._active = True self._index = index self._expr = template_info
@property def expr(self): # Note that it is faster to just generate the expression from # scratch than it is to clone it and replace the IndexTemplate objects self.set_value(self.parent_component().rule(self.parent_block(), self.index())) return self.expr def template_expr(self): return self._expr
[docs] def set_value(self, expr): self.__class__ = ConstraintData return self.set_value(expr)
[docs] def to_bounded_expression(self): tmp, self._expr = self._expr, self._expr[0] try: return super().to_bounded_expression() finally: self._expr = tmp
[docs] @ModelComponentFactory.register("General constraint expressions.") class Constraint(ActiveIndexedComponent): """ This modeling component defines a constraint expression using a rule function. Constructor arguments: expr A Pyomo expression for this constraint rule A function that is used to construct constraint expressions name A name for this component doc A text string describing this component Public class attributes: doc A text string describing this component name A name for this component active A boolean that is true if this component will be used to construct a model instance rule The rule used to initialize the constraint(s) Private class attributes: _constructed A boolean that is true if this component has been constructed _data A dictionary from the index set to component data objects _index The set of valid indices _model A weakref to the model that owns this component _parent A weakref to the parent block that owns this component _type The class type for the derived subclass """ _ComponentDataClass = ConstraintData class Infeasible(object): pass Feasible = ActiveIndexedComponent.Skip NoConstraint = ActiveIndexedComponent.Skip Violated = Infeasible Satisfied = Feasible @overload def __new__( cls: Type[Constraint], *args, **kwds ) -> Union[ScalarConstraint, IndexedConstraint]: ... @overload def __new__(cls: Type[ScalarConstraint], *args, **kwds) -> ScalarConstraint: ... @overload def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: ... def __new__(cls, *args, **kwds): if cls != Constraint: return super(Constraint, cls).__new__(cls) if not args or (args[0] is UnindexedComponent_set and len(args) == 1): return super(Constraint, cls).__new__(AbstractScalarConstraint) else: return super(Constraint, cls).__new__(IndexedConstraint) @overload def __init__(self, *indexes, expr=None, rule=None, name=None, doc=None): ...
[docs] def __init__(self, *args, **kwargs): _init = self._pop_from_kwargs('Constraint', kwargs, ('rule', 'expr'), None) # Special case: we accept 2- and 3-tuples as constraints if type(_init) is tuple: self.rule = Initializer(_init, treat_sequences_as_mappings=False) else: self.rule = Initializer(_init) kwargs.setdefault('ctype', Constraint) ActiveIndexedComponent.__init__(self, *args, **kwargs)
[docs] def construct(self, data=None): """ Construct the expression(s) for this constraint. """ if self._constructed: return self._constructed = True timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug("Constructing constraint %s" % (self.name)) if self._anonymous_sets is not None: for _set in self._anonymous_sets: _set.construct() rule = self.rule try: # We do not (currently) accept data for constructing Constraints index = None assert data is None if rule is None: # If there is no rule, then we are immediately done. return if rule.constant() and self.is_indexed(): raise IndexError( "Constraint '%s': Cannot initialize multiple indices " "of a constraint with a single expression" % (self.name,) ) block = self.parent_block() if rule.contains_indices(): # The index is coming in externally; we need to validate it for index in rule.indices(): self[index] = rule(block, index) elif not self.index_set().isfinite(): # If the index is not finite, then we cannot iterate # over it. Since the rule doesn't provide explicit # indices, then there is nothing we can do (the # assumption is that the user will trigger specific # indices to be created at a later time). pass else: if TEMPLATIZE_CONSTRAINTS: try: template_info = templatize_constraint(self) comp = weakref_ref(self) self._data = { idx: TemplateConstraintData(template_info, comp, idx) for idx in self.index_set() } return except TemplateExpressionError: pass # Bypass the index validation and create the member directly for index in self.index_set(): self._setitem_when_not_present(index, rule(block, index)) except Exception: err = sys.exc_info()[1] logger.error( "Rule failed when generating expression for " "Constraint %s with index %s:\n%s: %s" % (self.name, str(index), type(err).__name__, err) ) raise finally: timer.report()
def _getitem_when_not_present(self, idx): if self.rule is None: raise KeyError(idx) con = self._setitem_when_not_present(idx, self.rule(self.parent_block(), idx)) if con is None: raise KeyError(idx) return con def _pprint(self): """ Return data that will be printed for this component. """ return ( [ ("Size", len(self)), ("Index", self._index_set if self.is_indexed() else None), ("Active", self.active), ], self.items(), ("Lower", "Body", "Upper", "Active"), lambda k, v: [ "-Inf" if v.lower is None else v.lower, v.body, "+Inf" if v.upper is None else v.upper, v.active, ], )
[docs] def display(self, prefix="", ostream=None): """ Print component state information This duplicates logic in Component.pprint() """ if not self.active: return if ostream is None: ostream = sys.stdout tab = " " ostream.write(prefix + self.local_name + " : ") ostream.write("Size=" + str(len(self))) ostream.write("\n") tabular_writer( ostream, prefix + tab, ((k, v) for k, v in self._data.items() if v.active), ("Lower", "Body", "Upper"), lambda k, v: [ value(v.lower, exception=False), value(v.body, exception=False), value(v.upper, exception=False), ], )
[docs] class ScalarConstraint(ConstraintData, Constraint): """ ScalarConstraint is the implementation representing a single, non-indexed constraint. """
[docs] def __init__(self, *args, **kwds): ConstraintData.__init__(self, component=self, expr=None) Constraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index
# # Singleton constraints are strange in that we want them to be # both be constructed but have len() == 0 when not initialized with # anything (at least according to the unit tests that are # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Constraint.Skip are managed. But after that they will behave # like ConstraintData objects where set_value does not handle # Constraint.Skip but expects a valid expression or None. # @property def body(self): """The body (variable portion) of a constraint expression.""" if not self._data: raise ValueError( "Accessing the body of ScalarConstraint " "'%s' before the Constraint has been assigned " "an expression. There is currently " "nothing to access." % (self.name) ) return ConstraintData.body.fget(self) @property def lower(self): """The lower bound of a constraint expression. This is the fixed lower bound of a Constraint as a Pyomo expression. This may contain potentially variable terms that are currently fixed. If there is no lower bound, this will return `None`. """ if not self._data: raise ValueError( "Accessing the lower bound of ScalarConstraint " "'%s' before the Constraint has been assigned " "an expression. There is currently " "nothing to access." % (self.name) ) return ConstraintData.lower.fget(self) @property def upper(self): """Access the upper bound of a constraint expression. This is the fixed upper bound of a Constraint as a Pyomo expression. This may contain potentially variable terms that are currently fixed. If there is no upper bound, this will return `None`. """ if not self._data: raise ValueError( "Accessing the upper bound of ScalarConstraint " "'%s' before the Constraint has been assigned " "an expression. There is currently " "nothing to access." % (self.name) ) return ConstraintData.upper.fget(self) @property def equality(self): """bool : True if this is an equality constraint.""" if not self._data: raise ValueError( "Accessing the equality flag of ScalarConstraint " "'%s' before the Constraint has been assigned " "an expression. There is currently " "nothing to access." % (self.name) ) return ConstraintData.equality.fget(self) @property def strict_lower(self): """bool : True if this constraint has a strict lower bound.""" if not self._data: raise ValueError( "Accessing the strict_lower flag of ScalarConstraint " "'%s' before the Constraint has been assigned " "an expression. There is currently " "nothing to access." % (self.name) ) return ConstraintData.strict_lower.fget(self) @property def strict_upper(self): """bool : True if this constraint has a strict upper bound.""" if not self._data: raise ValueError( "Accessing the strict_upper flag of ScalarConstraint " "'%s' before the Constraint has been assigned " "an expression. There is currently " "nothing to access." % (self.name) ) return ConstraintData.strict_upper.fget(self)
[docs] def clear(self): self._data = {}
[docs] def set_value(self, expr): """Set the expression on this constraint.""" if not self._data: self._data[None] = self return super(ScalarConstraint, self).set_value(expr)
# # Leaving this method for backward compatibility reasons. # (probably should be removed) #
[docs] def add(self, index, expr): """Add a constraint with a given index.""" if index is not None: raise ValueError( "ScalarConstraint object '%s' does not accept " "index values other than None. Invalid value: %s" % (self.name, index) ) self.set_value(expr) return self
[docs] class SimpleConstraint(metaclass=RenamedClass): __renamed__new_class__ = ScalarConstraint __renamed__version__ = '6.0'
[docs] @disable_methods( { 'add', 'set_value', 'to_bounded_expression', 'body', 'lower', 'upper', 'equality', 'strict_lower', 'strict_upper', } ) class AbstractScalarConstraint(ScalarConstraint): pass
[docs] class AbstractSimpleConstraint(metaclass=RenamedClass): __renamed__new_class__ = AbstractScalarConstraint __renamed__version__ = '6.0'
[docs] class IndexedConstraint(Constraint): # # Leaving this method for backward compatibility reasons # # Note: Beginning after Pyomo 5.2 this method will now validate that # the index is in the underlying index set (through 5.2 the index # was not checked). #
[docs] def add(self, index, expr): """Add a constraint with a given index.""" return self.__setitem__(index, expr)
@overload def __getitem__(self, index) -> ConstraintData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore
[docs] @ModelComponentFactory.register("A list of constraint expressions.") class ConstraintList(IndexedConstraint): """ A constraint component that represents a list of constraints. Constraints can be indexed by their index, but when they are added an index value is not specified. """ class End(object): pass
[docs] def __init__(self, **kwargs): """Constructor""" if 'expr' in kwargs: raise ValueError("ConstraintList does not accept the 'expr' keyword") _rule = kwargs.pop('rule', None) self._starting_index = kwargs.pop('starting_index', 1) super(ConstraintList, self).__init__(Set(dimen=1), **kwargs) self.rule = Initializer( _rule, treat_sequences_as_mappings=False, allow_generators=True ) # 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.rule is not None and type(self.rule) is IndexedCallInitializer: self.rule = CountedCallInitializer(self, self.rule, self._starting_index)
[docs] def construct(self, data=None): """ Construct the expression(s) for this constraint. """ if self._constructed: return self._constructed = True if is_debug_set(logger): logger.debug("Constructing constraint list %s" % (self.name)) if self._anonymous_sets is not None: for _set in self._anonymous_sets: _set.construct() if self.rule is not None: _rule = self.rule(self.parent_block(), ()) for cc in iter(_rule): if cc is ConstraintList.End: break if cc is Constraint.Skip: continue self.add(cc)
[docs] def add(self, expr): """Add a constraint with an implicit index.""" next_idx = len(self._index_set) + self._starting_index self._index_set.add(next_idx) return self.__setitem__(next_idx, expr)