1#  ___________________________________________________________________________
2#
3#  Pyomo: Python Optimization Modeling Objects
4#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
5#  Under the terms of Contract DE-NA0003525 with National Technology and
6#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
7#  rights in this software.
8#  This software is distributed under the 3-clause BSD License.
9#  ___________________________________________________________________________
10
11__all__ = ['Constraint', '_ConstraintData', 'ConstraintList',
12           'simple_constraint_rule', 'simple_constraintlist_rule']
13
14import io
15import sys
16import logging
17import math
18from weakref import ref as weakref_ref
19
20from pyomo.common.deprecation import RenamedClass
21from pyomo.common.errors import DeveloperError
22from pyomo.common.formatting import tabular_writer
23from pyomo.common.log import is_debug_set
24from pyomo.common.timing import ConstructionTimer
25from pyomo.core.expr import logical_expr
26from pyomo.core.expr.numvalue import (
27    NumericValue, value, as_numeric, is_fixed, native_numeric_types,
28)
29from pyomo.core.base.component import (
30    ActiveComponentData, ModelComponentFactory,
31)
32from pyomo.core.base.indexed_component import (
33    ActiveIndexedComponent, UnindexedComponent_set, rule_wrapper,
34)
35from pyomo.core.base.set import Set
36from pyomo.core.base.disable_methods import disable_methods
37from pyomo.core.base.initializer import (
38    Initializer, IndexedCallInitializer, CountedCallInitializer,
39)
40
41
42logger = logging.getLogger('pyomo.core')
43
44_inf = float('inf')
45_rule_returned_none_error = """Constraint '%s': rule returned None.
46
47Constraint rules must return either a valid expression, a 2- or 3-member
48tuple, or one of Constraint.Skip, Constraint.Feasible, or
49Constraint.Infeasible.  The most common cause of this error is
50forgetting to include the "return" statement at the end of your rule.
51"""
52
53def simple_constraint_rule(rule):
54    """
55    This is a decorator that translates None/True/False return
56    values into Constraint.Skip/Constraint.Feasible/Constraint.Infeasible.
57    This supports a simpler syntax in constraint rules, though these
58    can be more difficult to debug when errors occur.
59
60    Example use:
61
62    @simple_constraint_rule
63    def C_rule(model, i, j):
64        ...
65
66    model.c = Constraint(rule=simple_constraint_rule(...))
67    """
68    return rule_wrapper(rule, {
69        None: Constraint.Skip,
70        True: Constraint.Feasible,
71        False: Constraint.Infeasible,
72    })
73
74def simple_constraintlist_rule(rule):
75    """
76    This is a decorator that translates None/True/False return values
77    into ConstraintList.End/Constraint.Feasible/Constraint.Infeasible.
78    This supports a simpler syntax in constraint rules, though these can be
79    more difficult to debug when errors occur.
80
81    Example use:
82
83    @simple_constraintlist_rule
84    def C_rule(model, i, j):
85        ...
86
87    model.c = ConstraintList(expr=simple_constraintlist_rule(...))
88    """
89    return rule_wrapper(rule, {
90        None: ConstraintList.End,
91        True: Constraint.Feasible,
92        False: Constraint.Infeasible,
93    })
94
95#
96# This class is a pure interface
97#
98
99class _ConstraintData(ActiveComponentData):
100    """
101    This class defines the data for a single constraint.
102
103    Constructor arguments:
104        component       The Constraint object that owns this data.
105
106    Public class attributes:
107        active          A boolean that is true if this constraint is
108                            active in the model.
109        body            The Pyomo expression for this constraint
110        lower           The Pyomo expression for the lower bound
111        upper           The Pyomo expression for the upper bound
112        equality        A boolean that indicates whether this is an
113                            equality constraint
114        strict_lower    A boolean that indicates whether this
115                            constraint uses a strict lower bound
116        strict_upper    A boolean that indicates whether this
117                            constraint uses a strict upper bound
118
119    Private class attributes:
120        _component      The objective component.
121        _active         A boolean that indicates whether this data is active
122    """
123
124    __slots__ = ()
125
126    # Set to true when a constraint class stores its expression
127    # in linear canonical form
128    _linear_canonical_form = False
129
130    def __init__(self, component=None):
131        #
132        # These lines represent in-lining of the
133        # following constructors:
134        #   - _ConstraintData,
135        #   - ActiveComponentData
136        #   - ComponentData
137        self._component = weakref_ref(component) if (component is not None) \
138                          else None
139        self._active = True
140
141    #
142    # Interface
143    #
144
145    def __call__(self, exception=True):
146        """Compute the value of the body of this constraint."""
147        return value(self.body, exception=exception)
148
149    def has_lb(self):
150        """Returns :const:`False` when the lower bound is
151        :const:`None` or negative infinity"""
152        return self.lower is not None
153
154    def has_ub(self):
155        """Returns :const:`False` when the upper bound is
156        :const:`None` or positive infinity"""
157        return self.upper is not None
158
159    def lslack(self):
160        """
161        Returns the value of f(x)-L for constraints of the form:
162            L <= f(x) (<= U)
163            (U >=) f(x) >= L
164        """
165        lb = self.lb
166        if lb is None:
167            return _inf
168        else:
169            return value(self.body) - lb
170
171    def uslack(self):
172        """
173        Returns the value of U-f(x) for constraints of the form:
174            (L <=) f(x) <= U
175            U >= f(x) (>= L)
176        """
177        ub = self.ub
178        if ub is None:
179            return _inf
180        else:
181            return ub - value(self.body)
182
183    def slack(self):
184        """
185        Returns the smaller of lslack and uslack values
186        """
187        lb = self.lb
188        ub = self.ub
189        body = value(self.body)
190        if lb is None:
191            return ub - body
192        elif ub is None:
193            return body - lb
194        return min(ub - body, body - lb)
195
196    #
197    # Abstract Interface
198    #
199
200    @property
201    def body(self):
202        """Access the body of a constraint expression."""
203        raise NotImplementedError
204
205    @property
206    def lower(self):
207        """Access the lower bound of a constraint expression."""
208        raise NotImplementedError
209
210    @property
211    def upper(self):
212        """Access the upper bound of a constraint expression."""
213        raise NotImplementedError
214
215    @property
216    def lb(self):
217        """Access the value of the lower bound of a constraint expression."""
218        raise NotImplementedError
219
220    @property
221    def ub(self):
222        """Access the value of the upper bound of a constraint expression."""
223        raise NotImplementedError
224
225    @property
226    def equality(self):
227        """A boolean indicating whether this is an equality constraint."""
228        raise NotImplementedError
229
230    @property
231    def strict_lower(self):
232        """True if this constraint has a strict lower bound."""
233        raise NotImplementedError
234
235    @property
236    def strict_upper(self):
237        """True if this constraint has a strict upper bound."""
238        raise NotImplementedError
239
240    def set_value(self, expr):
241        """Set the expression on this constraint."""
242        raise NotImplementedError
243
244    def get_value(self):
245        """Get the expression on this constraint."""
246        raise NotImplementedError
247
248
249class _GeneralConstraintData(_ConstraintData):
250    """
251    This class defines the data for a single general constraint.
252
253    Constructor arguments:
254        component       The Constraint object that owns this data.
255        expr            The Pyomo expression stored in this constraint.
256
257    Public class attributes:
258        active          A boolean that is true if this constraint is
259                            active in the model.
260        body            The Pyomo expression for this constraint
261        lower           The Pyomo expression for the lower bound
262        upper           The Pyomo expression for the upper bound
263        equality        A boolean that indicates whether this is an
264                            equality constraint
265        strict_lower    A boolean that indicates whether this
266                            constraint uses a strict lower bound
267        strict_upper    A boolean that indicates whether this
268                            constraint uses a strict upper bound
269
270    Private class attributes:
271        _component      The objective component.
272        _active         A boolean that indicates whether this data is active
273    """
274
275    __slots__ = ('_body', '_lower', '_upper', '_expr')
276
277    def __init__(self,  expr=None, component=None):
278        #
279        # These lines represent in-lining of the
280        # following constructors:
281        #   - _ConstraintData,
282        #   - ActiveComponentData
283        #   - ComponentData
284        self._component = weakref_ref(component) if (component is not None) \
285                          else None
286        self._active = True
287
288        self._body = None
289        self._lower = None
290        self._upper = None
291        self._expr = None
292        if expr is not None:
293            self.set_value(expr)
294
295    def __getstate__(self):
296        """
297        This method must be defined because this class uses slots.
298        """
299        result = super(_GeneralConstraintData, self).__getstate__()
300        for i in _GeneralConstraintData.__slots__:
301            result[i] = getattr(self, i)
302        return result
303
304    # Since this class requires no special processing of the state
305    # dictionary, it does not need to implement __setstate__()
306
307    #
308    # Abstract Interface
309    #
310
311    @property
312    def body(self):
313        """Access the body of a constraint expression."""
314        if self._body is not None:
315            body = self._body
316        else:
317            # The incoming RangedInequality had a potentially variable
318            # bound.  The "body" is fine, but the bounds may not be
319            # (although the responsibility for those checks lies with the
320            # lower/upper properties)
321            body = self._expr.arg(1)
322        return as_numeric(body)
323
324    def _lb(self):
325        if self._body is not None:
326            bound = self._lower
327        elif self._expr is None:
328            return None
329        else:
330            bound = self._expr.arg(0)
331            if not is_fixed(bound):
332                raise ValueError(
333                    "Constraint '%s' is a Ranged Inequality with a "
334                    "variable %s bound.  Cannot normalize the "
335                    "constraint or send it to a solver."
336                    % (self.name, 'lower'))
337        return bound
338
339    def _ub(self):
340        if self._body is not None:
341            bound = self._upper
342        elif self._expr is None:
343            return None
344        else:
345            bound = self._expr.arg(2)
346            if not is_fixed(bound):
347                raise ValueError(
348                    "Constraint '%s' is a Ranged Inequality with a "
349                    "variable %s bound.  Cannot normalize the "
350                    "constraint or send it to a solver."
351                    % (self.name, 'upper'))
352        return bound
353
354    @property
355    def lower(self):
356        """Access the lower bound of a constraint expression."""
357        bound = self._lb()
358        # Historically, constraint.lower was guaranteed to return a type
359        # derived from Pyomo NumericValue (or None).  Replicate that
360        # functionality, although clients should in almost all cases
361        # move to using ConstraintData.lb instead of accessing
362        # lower/body/upper to avoid the unnecessary creation (and
363        # inevitable destruction) of the NumericConstant wrappers.
364        if bound is None:
365            return None
366        return as_numeric(bound)
367
368    @property
369    def upper(self):
370        """Access the upper bound of a constraint expression."""
371        bound = self._ub()
372        # Historically, constraint.upper was guaranteed to return a type
373        # derived from Pyomo NumericValue (or None).  Replicate that
374        # functionality, although clients should in almost all cases
375        # move to using ConstraintData.ub instead of accessing
376        # lower/body/upper to avoid the unnecessary creation (and
377        # inevitable destruction) of the NumericConstant wrappers.
378        if bound is None:
379            return None
380        return as_numeric(bound)
381
382    @property
383    def lb(self):
384        """Access the value of the lower bound of a constraint expression."""
385        bound = value(self._lb())
386        if bound is not None and not math.isfinite(bound):
387            if bound == -_inf:
388                bound = None
389            else:
390                raise ValueError(
391                    "Constraint '%s' created with an invalid non-finite "
392                    "lower bound (%s)." % (self.name, bound))
393        return bound
394
395    @property
396    def ub(self):
397        """Access the value of the upper bound of a constraint expression."""
398        bound = value(self._ub())
399        if bound is not None and not math.isfinite(bound):
400            if bound == _inf:
401                bound = None
402            else:
403                raise ValueError(
404                    "Constraint '%s' created with an invalid non-finite "
405                    "upper bound (%s)." % (self.name, bound))
406        return bound
407
408    @property
409    def equality(self):
410        """A boolean indicating whether this is an equality constraint."""
411        if self._expr.__class__ is logical_expr.EqualityExpression:
412            return True
413        elif self._expr.__class__ is logical_expr.RangedExpression:
414            # TODO: this is a very restrictive form of structural equality.
415            lb = self._expr.arg(0)
416            if lb is not None and lb is self._expr.arg(2):
417                return True
418        return False
419
420    @property
421    def strict_lower(self):
422        """True if this constraint has a strict lower bound."""
423        return False
424
425    @property
426    def strict_upper(self):
427        """True if this constraint has a strict upper bound."""
428        return False
429
430    @property
431    def expr(self):
432        """Return the expression associated with this constraint."""
433        return self._expr
434
435    def get_value(self):
436        """Get the expression on this constraint."""
437        return self._expr
438
439    def set_value(self, expr):
440        """Set the expression on this constraint."""
441        # Clear any previously-cached normalized constraint
442        self._lower = self._upper = self._body = self._expr = None
443
444        _expr_type = expr.__class__
445        if hasattr(expr, 'is_relational'):
446            if not expr.is_relational():
447                raise ValueError(
448                    "Constraint '%s' does not have a proper "
449                    "value. Found '%s'\nExpecting a tuple or "
450                    "equation. Examples:"
451                    "\n   sum(model.costs) == model.income"
452                    "\n   (0, model.price[item], 50)"
453                    % (self.name, str(expr)))
454            self._expr = expr
455
456        elif _expr_type is tuple: # or expr_type is list:
457            for arg in expr:
458                if arg is None or arg.__class__ in native_numeric_types \
459                   or isinstance(arg, NumericValue):
460                    continue
461                raise ValueError(
462                    "Constraint '%s' does not have a proper value. "
463                    "Constraint expressions expressed as tuples must "
464                    "contain native numeric types or Pyomo NumericValue "
465                    "objects. Tuple %s contained invalid type, %s"
466                    % (self.name, expr, arg.__class__.__name__))
467            if len(expr) == 2:
468                #
469                # Form equality expression
470                #
471                if expr[0] is None or expr[1] is None:
472                    raise ValueError(
473                        "Constraint '%s' does not have a proper value. "
474                        "Equality Constraints expressed as 2-tuples "
475                        "cannot contain None [received %s]"
476                        % (self.name, expr,))
477                self._expr = logical_expr.EqualityExpression(expr)
478            elif len(expr) == 3:
479                #
480                # Form (ranged) inequality expression
481                #
482                if expr[0] is None:
483                    self._expr = logical_expr.InequalityExpression(
484                        expr[1:], False)
485                elif expr[2] is None:
486                    self._expr = logical_expr.InequalityExpression(
487                        expr[:2], False)
488                else:
489                    self._expr = logical_expr.RangedExpression(expr, False)
490            else:
491                raise ValueError(
492                    "Constraint '%s' does not have a proper value. "
493                    "Found a tuple of length %d. Expecting a tuple of "
494                    "length 2 or 3:\n"
495                    "    Equality:   (left, right)\n"
496                    "    Inequality: (lower, expression, upper)"
497                    % (self.name, len(expr)))
498        #
499        # Ignore an 'empty' constraints
500        #
501        elif _expr_type is type:
502            del self.parent_component()[self.index()]
503            if expr is Constraint.Skip:
504                return
505            elif expr is Constraint.Infeasible:
506                # TODO: create a trivial infeasible constraint.  This
507                # could be useful in the case of GDP where certain
508                # disjuncts are trivially infeasible, but we would still
509                # like to express the disjunction.
510                #del self.parent_component()[self.index()]
511                raise ValueError(
512                    "Constraint '%s' is always infeasible"
513                    % (self.name,) )
514            else:
515                raise ValueError(
516                    "Constraint '%s' does not have a proper "
517                    "value. Found '%s'\nExpecting a tuple or "
518                    "equation. Examples:"
519                    "\n   sum(model.costs) == model.income"
520                    "\n   (0, model.price[item], 50)"
521                    % (self.name, str(expr)))
522
523        elif expr is None:
524            raise ValueError(_rule_returned_none_error % (self.name,))
525
526        elif _expr_type is bool:
527            raise ValueError(
528                "Invalid constraint expression. The constraint "
529                "expression resolved to a trivial Boolean (%s) "
530                "instead of a Pyomo object. Please modify your "
531                "rule to return Constraint.%s instead of %s."
532                "\n\nError thrown for Constraint '%s'"
533                % (expr, "Feasible" if expr else "Infeasible",
534                   expr, self.name))
535
536        else:
537            msg = ("Constraint '%s' does not have a proper "
538                   "value. Found '%s'\nExpecting a tuple or "
539                   "equation. Examples:"
540                   "\n   sum(model.costs) == model.income"
541                   "\n   (0, model.price[item], 50)"
542                   % (self.name, str(expr)))
543            raise ValueError(msg)
544        #
545        # Normalize the incoming expressions, if we can
546        #
547        args = self._expr.args
548        if self._expr.__class__ is logical_expr.InequalityExpression:
549            if self._expr.strict:
550                raise ValueError(
551                    "Constraint '%s' encountered a strict "
552                    "inequality expression ('>' or '< '). All"
553                    " constraints must be formulated using "
554                    "using '<=', '>=', or '=='."
555                    % (self.name,))
556            if args[1] is None or args[1].__class__ in native_numeric_types \
557               or not args[1].is_potentially_variable():
558                self._body = args[0]
559                self._upper = args[1]
560            elif args[0] is None or args[0].__class__ in native_numeric_types \
561               or not args[0].is_potentially_variable():
562                self._lower = args[0]
563                self._body = args[1]
564            else:
565                self._body = args[0] - args[1]
566                self._upper = 0
567        elif self._expr.__class__ is logical_expr.EqualityExpression:
568            if args[0] is None or args[1] is None:
569                # Error check: ensure equality does not have infinite RHS
570                raise ValueError(
571                    "Equality constraint '%s' defined with "
572                    "non-finite term." % (self.name))
573            if args[0].__class__ in native_numeric_types or \
574               not args[0].is_potentially_variable():
575                self._lower = self._upper = args[0]
576                self._body = args[1]
577            elif args[1].__class__ in native_numeric_types or \
578               not args[1].is_potentially_variable():
579                self._lower = self._upper = args[1]
580                self._body = args[0]
581            else:
582                self._lower = self._upper = 0
583                self._body = args[0] - args[1]
584            # The following logic is caught below when checking for
585            # invalid non-finite bounds:
586            #
587            # if self._lower.__class__ in native_numeric_types and \
588            #    not math.isfinite(self._lower):
589            #     raise ValueError(
590            #         "Equality constraint '%s' defined with "
591            #         "non-finite term." % (self.name))
592        elif self._expr.__class__ is logical_expr.RangedExpression:
593            if any(self._expr.strict):
594                raise ValueError(
595                    "Constraint '%s' encountered a strict "
596                    "inequality expression ('>' or '< '). All"
597                   " constraints must be formulated using "
598                    "using '<=', '>=', or '=='."
599                    % (self.name,))
600            if all(( arg is None or
601                     arg.__class__ in native_numeric_types or
602                     not arg.is_potentially_variable() )
603                   for arg in (args[0], args[2])):
604                self._lower, self._body, self._upper = args
605        else:
606            # Defensive programming: we currently only support three
607            # relational expression types.  This will only be hit if
608            # someone defines a fourth...
609            raise DeveloperError("Unrecognized relational expression type: %s"
610                                 % (self._expr.__class__.__name__,))
611
612        # We have historically mapped incoming inf to None
613        if self._lower.__class__ in native_numeric_types:
614            if self._lower == -_inf:
615                self._lower = None
616            elif not math.isfinite(self._lower):
617                raise ValueError(
618                    "Constraint '%s' created with an invalid non-finite "
619                    "lower bound (%s)." % (self.name, self._lower))
620        if self._upper.__class__ in native_numeric_types:
621            if self._upper == _inf:
622                self._upper = None
623            elif not math.isfinite(self._upper):
624                raise ValueError(
625                    "Constraint '%s' created with an invalid non-finite "
626                    "upper bound (%s)." % (self.name, self._upper))
627
628
629@ModelComponentFactory.register("General constraint expressions.")
630class Constraint(ActiveIndexedComponent):
631    """
632    This modeling component defines a constraint expression using a
633    rule function.
634
635    Constructor arguments:
636        expr
637            A Pyomo expression for this constraint
638        rule
639            A function that is used to construct constraint expressions
640        doc
641            A text string describing this component
642        name
643            A name for this component
644
645    Public class attributes:
646        doc
647            A text string describing this component
648        name
649            A name for this component
650        active
651            A boolean that is true if this component will be used to
652            construct a model instance
653        rule
654           The rule used to initialize the constraint(s)
655
656    Private class attributes:
657        _constructed
658            A boolean that is true if this component has been constructed
659        _data
660            A dictionary from the index set to component data objects
661        _index
662            The set of valid indices
663        _implicit_subsets
664            A tuple of set objects that represents the index set
665        _model
666            A weakref to the model that owns this component
667        _parent
668            A weakref to the parent block that owns this component
669        _type
670            The class type for the derived subclass
671    """
672
673    _ComponentDataClass = _GeneralConstraintData
674    class Infeasible(object): pass
675    Feasible = ActiveIndexedComponent.Skip
676    NoConstraint = ActiveIndexedComponent.Skip
677    Violated = Infeasible
678    Satisfied = Feasible
679
680    def __new__(cls, *args, **kwds):
681        if cls != Constraint:
682            return super(Constraint, cls).__new__(cls)
683        if not args or (args[0] is UnindexedComponent_set and len(args)==1):
684            return super(Constraint, cls).__new__(AbstractScalarConstraint)
685        else:
686            return super(Constraint, cls).__new__(IndexedConstraint)
687
688    def __init__(self, *args, **kwargs):
689        _init = tuple( _arg for _arg in (
690            kwargs.pop('rule', None),
691            kwargs.pop('expr', None) ) if _arg is not None )
692        if len(_init) == 1:
693            _init = _init[0]
694        elif not _init:
695            _init = None
696        else:
697            raise ValueError("Duplicate initialization: Constraint() only "
698                             "accepts one of 'rule=' and 'expr='")
699
700        kwargs.setdefault('ctype', Constraint)
701        ActiveIndexedComponent.__init__(self, *args, **kwargs)
702
703        # Special case: we accept 2- and 3-tuples as constraints
704        if type(_init) is tuple:
705            self.rule = Initializer(_init, treat_sequences_as_mappings=False)
706        else:
707            self.rule = Initializer(_init)
708
709    def construct(self, data=None):
710        """
711        Construct the expression(s) for this constraint.
712        """
713        if self._constructed:
714            return
715        self._constructed=True
716
717        timer = ConstructionTimer(self)
718        if is_debug_set(logger):
719            logger.debug("Constructing constraint %s" % (self.name))
720
721        rule = self.rule
722        try:
723            # We do not (currently) accept data for constructing Constraints
724            index = None
725            assert data is None
726
727            if rule is None:
728                # If there is no rule, then we are immediately done.
729                return
730
731            if rule.constant() and self.is_indexed():
732                raise IndexError(
733                    "Constraint '%s': Cannot initialize multiple indices "
734                    "of a constraint with a single expression" %
735                    (self.name,) )
736
737            block = self.parent_block()
738            if rule.contains_indices():
739                # The index is coming in externally; we need to validate it
740                for index in rule.indices():
741                    self[index] = rule(block, index)
742            elif not self.index_set().isfinite():
743                # If the index is not finite, then we cannot iterate
744                # over it.  Since the rule doesn't provide explicit
745                # indices, then there is nothing we can do (the
746                # assumption is that the user will trigger specific
747                # indices to be created at a later time).
748                pass
749            else:
750                # Bypass the index validation and create the member directly
751                for index in self.index_set():
752                    self._setitem_when_not_present(index, rule(block, index))
753        except Exception:
754            err = sys.exc_info()[1]
755            logger.error(
756                "Rule failed when generating expression for "
757                "Constraint %s with index %s:\n%s: %s"
758                % (self.name,
759                   str(index),
760                   type(err).__name__,
761                   err))
762            raise
763        finally:
764            timer.report()
765
766    def _getitem_when_not_present(self, idx):
767        if self.rule is None:
768            raise KeyError(idx)
769        con = self._setitem_when_not_present(
770            idx, self.rule(self.parent_block(), idx))
771        if con is None:
772            raise KeyError(idx)
773        return con
774
775    def _pprint(self):
776        """
777        Return data that will be printed for this component.
778        """
779        return (
780            [("Size", len(self)),
781             ("Index", self._index if self.is_indexed() else None),
782             ("Active", self.active),
783             ],
784            self.items(),
785            ( "Lower","Body","Upper","Active" ),
786            lambda k, v: [ "-Inf" if v.lower is None else v.lower,
787                           v.body,
788                           "+Inf" if v.upper is None else v.upper,
789                           v.active,
790                           ]
791            )
792
793    def display(self, prefix="", ostream=None):
794        """
795        Print component state information
796
797        This duplicates logic in Component.pprint()
798        """
799        if not self.active:
800            return
801        if ostream is None:
802            ostream = sys.stdout
803        tab="    "
804        ostream.write(prefix+self.local_name+" : ")
805        ostream.write("Size="+str(len(self)))
806
807        ostream.write("\n")
808        tabular_writer( ostream, prefix+tab,
809                        ((k,v) for k,v in self._data.items() if v.active),
810                        ( "Lower","Body","Upper" ),
811                        lambda k, v: [
812                            value(v.lower, exception=False),
813                            value(v.body, exception=False),
814                            value(v.upper, exception=False),
815                        ])
816
817
818class ScalarConstraint(_GeneralConstraintData, Constraint):
819    """
820    ScalarConstraint is the implementation representing a single,
821    non-indexed constraint.
822    """
823
824    def __init__(self, *args, **kwds):
825        _GeneralConstraintData.__init__(self, component=self, expr=None)
826        Constraint.__init__(self, *args, **kwds)
827
828    #
829    # Since this class derives from Component and
830    # Component.__getstate__ just packs up the entire __dict__ into
831    # the state dict, we do not need to define the __getstate__ or
832    # __setstate__ methods.  We just defer to the super() get/set
833    # state.  Since all of our get/set state methods rely on super()
834    # to traverse the MRO, this will automatically pick up both the
835    # Component and Data base classes.
836    #
837
838    #
839    # Singleton constraints are strange in that we want them to be
840    # both be constructed but have len() == 0 when not initialized with
841    # anything (at least according to the unit tests that are
842    # currently in place). So during initialization only, we will
843    # treat them as "indexed" objects where things like
844    # Constraint.Skip are managed. But after that they will behave
845    # like _ConstraintData objects where set_value does not handle
846    # Constraint.Skip but expects a valid expression or None.
847    #
848    @property
849    def body(self):
850        """Access the body of a constraint expression."""
851        if not self._data:
852            raise ValueError(
853                "Accessing the body of ScalarConstraint "
854                "'%s' before the Constraint has been assigned "
855                "an expression. There is currently "
856                "nothing to access." % (self.name))
857        return _GeneralConstraintData.body.fget(self)
858
859    @property
860    def lower(self):
861        """Access the lower bound of a constraint expression."""
862        if not self._data:
863            raise ValueError(
864                "Accessing the lower bound of ScalarConstraint "
865                "'%s' before the Constraint has been assigned "
866                "an expression. There is currently "
867                "nothing to access." % (self.name))
868        return _GeneralConstraintData.lower.fget(self)
869
870    @property
871    def upper(self):
872        """Access the upper bound of a constraint expression."""
873        if not self._data:
874            raise ValueError(
875                "Accessing the upper bound of ScalarConstraint "
876                "'%s' before the Constraint has been assigned "
877                "an expression. There is currently "
878                "nothing to access." % (self.name))
879        return _GeneralConstraintData.upper.fget(self)
880
881    @property
882    def equality(self):
883        """A boolean indicating whether this is an equality constraint."""
884        if not self._data:
885            raise ValueError(
886                "Accessing the equality flag of ScalarConstraint "
887                "'%s' before the Constraint has been assigned "
888                "an expression. There is currently "
889                "nothing to access." % (self.name))
890        return _GeneralConstraintData.equality.fget(self)
891
892    @property
893    def strict_lower(self):
894        """A boolean indicating whether this constraint has a strict lower bound."""
895        if not self._data:
896            raise ValueError(
897                "Accessing the strict_lower flag of ScalarConstraint "
898                "'%s' before the Constraint has been assigned "
899                "an expression. There is currently "
900                "nothing to access." % (self.name))
901        return _GeneralConstraintData.strict_lower.fget(self)
902
903    @property
904    def strict_upper(self):
905        """A boolean indicating whether this constraint has a strict upper bound."""
906        if not self._data:
907            raise ValueError(
908                "Accessing the strict_upper flag of ScalarConstraint "
909                "'%s' before the Constraint has been assigned "
910                "an expression. There is currently "
911                "nothing to access." % (self.name))
912        return _GeneralConstraintData.strict_upper.fget(self)
913
914    def clear(self):
915        self._data = {}
916
917    def set_value(self, expr):
918        """Set the expression on this constraint."""
919        if not self._data:
920            self._data[None] = self
921        return super(ScalarConstraint, self).set_value(expr)
922
923    #
924    # Leaving this method for backward compatibility reasons.
925    # (probably should be removed)
926    #
927    def add(self, index, expr):
928        """Add a constraint with a given index."""
929        if index is not None:
930            raise ValueError(
931                "ScalarConstraint object '%s' does not accept "
932                "index values other than None. Invalid value: %s"
933                % (self.name, index))
934        self.set_value(expr)
935        return self
936
937
938class SimpleConstraint(metaclass=RenamedClass):
939    __renamed__new_class__ = ScalarConstraint
940    __renamed__version__ = '6.0'
941
942
943@disable_methods({'add', 'set_value', 'body', 'lower', 'upper', 'equality',
944                  'strict_lower', 'strict_upper'})
945class AbstractScalarConstraint(ScalarConstraint):
946    pass
947
948
949class AbstractSimpleConstraint(metaclass=RenamedClass):
950    __renamed__new_class__ = AbstractScalarConstraint
951    __renamed__version__ = '6.0'
952
953
954class IndexedConstraint(Constraint):
955
956    #
957    # Leaving this method for backward compatibility reasons
958    #
959    # Note: Beginning after Pyomo 5.2 this method will now validate that
960    # the index is in the underlying index set (through 5.2 the index
961    # was not checked).
962    #
963    def add(self, index, expr):
964        """Add a constraint with a given index."""
965        return self.__setitem__(index, expr)
966
967
968@ModelComponentFactory.register("A list of constraint expressions.")
969class ConstraintList(IndexedConstraint):
970    """
971    A constraint component that represents a list of constraints.
972    Constraints can be indexed by their index, but when they are
973    added an index value is not specified.
974    """
975
976    class End(object): pass
977
978    def __init__(self, **kwargs):
979        """Constructor"""
980        if 'expr' in kwargs:
981            raise ValueError(
982                "ConstraintList does not accept the 'expr' keyword")
983        _rule = kwargs.pop('rule', None)
984
985        args = (Set(dimen=1),)
986        super(ConstraintList, self).__init__(*args, **kwargs)
987
988        self.rule = Initializer(_rule,
989                                treat_sequences_as_mappings=False,
990                                allow_generators=True)
991        # HACK to make the "counted call" syntax work.  We wait until
992        # after the base class is set up so that is_indexed() is
993        # reliable.
994        if self.rule is not None and type(self.rule) is IndexedCallInitializer:
995            self.rule = CountedCallInitializer(self, self.rule)
996
997
998    def construct(self, data=None):
999        """
1000        Construct the expression(s) for this constraint.
1001        """
1002        if self._constructed:
1003            return
1004        self._constructed=True
1005
1006        if is_debug_set(logger):
1007            logger.debug("Constructing constraint list %s"
1008                         % (self.name))
1009
1010        self.index_set().construct()
1011
1012        if self.rule is not None:
1013            _rule = self.rule(self.parent_block(), ())
1014            for cc in iter(_rule):
1015                if cc is ConstraintList.End:
1016                    break
1017                if cc is Constraint.Skip:
1018                    continue
1019                self.add(cc)
1020
1021
1022    def add(self, expr):
1023        """Add a constraint with an implicit index."""
1024        next_idx = len(self._index) + 1
1025        self._index.add(next_idx)
1026        return self.__setitem__(next_idx, expr)
1027
1028