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