Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ef2ed66
Do not use to compare native type bounds for determining equality
jsiirola Mar 25, 2026
51c2ba8
Rework inequality() to build on the relational expression dispatchers
jsiirola Mar 25, 2026
2962b32
rework tuple_to_relational_expression
jsiirola Mar 25, 2026
17468cc
Switch Constraint to use tuple_to_relational_expression to resolve tu…
jsiirola Mar 25, 2026
43b12e2
Skip adding bounds constraints on unbounded variables
jsiirola Mar 25, 2026
2546168
Update tests to reflect changed exceptions
jsiirola Mar 25, 2026
df56571
Update tests to reflect relaxed equality detection
jsiirola Mar 25, 2026
3c26e64
NFC: fix typo
jsiirola Mar 25, 2026
58eb6a7
Update LP_trivial_constraints because tuple notation no longer genera…
jsiirola Mar 25, 2026
f92213f
Update LP_compiled because tuple notation no longer generates trivial…
jsiirola Mar 26, 2026
6963e2f
Update baseline to reflect improved constant expression resolution
jsiirola Mar 26, 2026
16f20b3
Patch ComplimentarityData: unbounded tuples are resolved to numeric e…
jsiirola Mar 26, 2026
9ece6a3
Fix string excaping
jsiirola Mar 27, 2026
96aaf60
Merge branch 'main' into constraint-equality
jsiirola Mar 31, 2026
49db495
Merge branch 'main' into constraint-equality
jsiirola Apr 2, 2026
807f21c
Improve resolution of trivial relational expressions in Ranged contexts
jsiirola Apr 13, 2026
67521a7
Fix error where inequality(0,0,None) returned 0 instead of True
jsiirola Apr 14, 2026
a140381
inequality(): check trivial bounds feasibility
jsiirola Apr 14, 2026
2787c86
Expand inequality() documentation
jsiirola Apr 14, 2026
5152012
Expand inequality / relational operator testing
jsiirola Apr 14, 2026
4e22431
Merge branch 'main' into constraint-equality
jsiirola Apr 14, 2026
3d91f18
NFC: fix typos
jsiirola Apr 14, 2026
3af761c
Update tests for more aggressive trivial infeasibility detection
jsiirola Apr 14, 2026
ae23c61
Doc cleanup
jsiirola Apr 14, 2026
8df5c4f
Fix my own nitpick
mrmundt Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 40 additions & 40 deletions examples/pyomobook/blocks-ch/blocks_gen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
2 Var Declarations
Power : Size=5, Index=TIME
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : 120.0 : 500.0 : False : False : Reals
1 : 0 : 145.0 : 500.0 : False : False : Reals
2 : 0 : 119.0 : 500.0 : False : False : Reals
3 : 0 : 42.0 : 500.0 : False : False : Reals
4 : 0 : 190.0 : 500.0 : False : False : Reals
0 : 0 : 120.0 : 500 : False : False : Reals
1 : 0 : 145.0 : 500 : False : False : Reals
2 : 0 : 119.0 : 500 : False : False : Reals
3 : 0 : 42.0 : 500 : False : False : Reals
4 : 0 : 190.0 : 500 : False : False : Reals
UnitOn : Size=5, Index=TIME
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : None : 1 : False : True : Binary
Expand All @@ -46,11 +46,11 @@

1 Constraint Declarations
limit_ramp : Size=4, Index=TIME, Active=True
Key : Lower : Body : Upper : Active
1 : -50.0 : Generator[G_EAST].Power[1] - Generator[G_EAST].Power[0] : Generator[G_EAST].RampLimit : True
2 : -50.0 : Generator[G_EAST].Power[2] - Generator[G_EAST].Power[1] : Generator[G_EAST].RampLimit : True
3 : -50.0 : Generator[G_EAST].Power[3] - Generator[G_EAST].Power[2] : Generator[G_EAST].RampLimit : True
4 : -50.0 : Generator[G_EAST].Power[4] - Generator[G_EAST].Power[3] : Generator[G_EAST].RampLimit : True
Key : Lower : Body : Upper : Active
1 : -50.0 : Generator[G_EAST].Power[1] - Generator[G_EAST].Power[0] : 50.0 : True
2 : -50.0 : Generator[G_EAST].Power[2] - Generator[G_EAST].Power[1] : 50.0 : True
3 : -50.0 : Generator[G_EAST].Power[3] - Generator[G_EAST].Power[2] : 50.0 : True
4 : -50.0 : Generator[G_EAST].Power[4] - Generator[G_EAST].Power[3] : 50.0 : True

7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost
Generator[G_MAIN] : Active=True
Expand All @@ -67,11 +67,11 @@
2 Var Declarations
Power : Size=5, Index=TIME
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : 120.0 : 500.0 : False : False : Reals
1 : 0 : 145.0 : 500.0 : False : False : Reals
2 : 0 : 119.0 : 500.0 : False : False : Reals
3 : 0 : 42.0 : 500.0 : False : False : Reals
4 : 0 : 190.0 : 500.0 : False : False : Reals
0 : 0 : 120.0 : 500 : False : False : Reals
1 : 0 : 145.0 : 500 : False : False : Reals
2 : 0 : 119.0 : 500 : False : False : Reals
3 : 0 : 42.0 : 500 : False : False : Reals
4 : 0 : 190.0 : 500 : False : False : Reals
UnitOn : Size=5, Index=TIME
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : None : 1 : False : True : Binary
Expand All @@ -91,11 +91,11 @@

1 Constraint Declarations
limit_ramp : Size=4, Index=TIME, Active=True
Key : Lower : Body : Upper : Active
1 : -50.0 : Generator[G_MAIN].Power[1] - Generator[G_MAIN].Power[0] : Generator[G_MAIN].RampLimit : True
2 : -50.0 : Generator[G_MAIN].Power[2] - Generator[G_MAIN].Power[1] : Generator[G_MAIN].RampLimit : True
3 : -50.0 : Generator[G_MAIN].Power[3] - Generator[G_MAIN].Power[2] : Generator[G_MAIN].RampLimit : True
4 : -50.0 : Generator[G_MAIN].Power[4] - Generator[G_MAIN].Power[3] : Generator[G_MAIN].RampLimit : True
Key : Lower : Body : Upper : Active
1 : -50.0 : Generator[G_MAIN].Power[1] - Generator[G_MAIN].Power[0] : 50.0 : True
2 : -50.0 : Generator[G_MAIN].Power[2] - Generator[G_MAIN].Power[1] : 50.0 : True
3 : -50.0 : Generator[G_MAIN].Power[3] - Generator[G_MAIN].Power[2] : 50.0 : True
4 : -50.0 : Generator[G_MAIN].Power[4] - Generator[G_MAIN].Power[3] : 50.0 : True

7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost

Expand Down Expand Up @@ -124,11 +124,11 @@
2 Var Declarations
Power : Size=5, Index=TIME
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : 120.0 : 500.0 : False : False : Reals
1 : 0 : 145.0 : 500.0 : False : False : Reals
2 : 0 : 119.0 : 500.0 : False : False : Reals
3 : 0 : 42.0 : 500.0 : False : False : Reals
4 : 0 : 190.0 : 500.0 : False : False : Reals
0 : 0 : 120.0 : 500 : False : False : Reals
1 : 0 : 145.0 : 500 : False : False : Reals
2 : 0 : 119.0 : 500 : False : False : Reals
3 : 0 : 42.0 : 500 : False : False : Reals
4 : 0 : 190.0 : 500 : False : False : Reals
UnitOn : Size=5, Index=TIME
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : None : 1 : False : True : Binary
Expand All @@ -148,11 +148,11 @@

1 Constraint Declarations
limit_ramp : Size=4, Index=TIME, Active=True
Key : Lower : Body : Upper : Active
1 : -50.0 : Generator[G_EAST].Power[1] - Generator[G_EAST].Power[0] : Generator[G_EAST].RampLimit : True
2 : -50.0 : Generator[G_EAST].Power[2] - Generator[G_EAST].Power[1] : Generator[G_EAST].RampLimit : True
3 : -50.0 : Generator[G_EAST].Power[3] - Generator[G_EAST].Power[2] : Generator[G_EAST].RampLimit : True
4 : -50.0 : Generator[G_EAST].Power[4] - Generator[G_EAST].Power[3] : Generator[G_EAST].RampLimit : True
Key : Lower : Body : Upper : Active
1 : -50.0 : Generator[G_EAST].Power[1] - Generator[G_EAST].Power[0] : 50.0 : True
2 : -50.0 : Generator[G_EAST].Power[2] - Generator[G_EAST].Power[1] : 50.0 : True
3 : -50.0 : Generator[G_EAST].Power[3] - Generator[G_EAST].Power[2] : 50.0 : True
4 : -50.0 : Generator[G_EAST].Power[4] - Generator[G_EAST].Power[3] : 50.0 : True

7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost
Generator[G_MAIN] : Active=True
Expand All @@ -169,11 +169,11 @@
2 Var Declarations
Power : Size=5, Index=TIME
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : 120.0 : 500.0 : False : False : Reals
1 : 0 : 145.0 : 500.0 : False : False : Reals
2 : 0 : 119.0 : 500.0 : False : False : Reals
3 : 0 : 42.0 : 500.0 : False : False : Reals
4 : 0 : 190.0 : 500.0 : False : False : Reals
0 : 0 : 120.0 : 500 : False : False : Reals
1 : 0 : 145.0 : 500 : False : False : Reals
2 : 0 : 119.0 : 500 : False : False : Reals
3 : 0 : 42.0 : 500 : False : False : Reals
4 : 0 : 190.0 : 500 : False : False : Reals
UnitOn : Size=5, Index=TIME
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : None : 1 : False : True : Binary
Expand All @@ -193,11 +193,11 @@

1 Constraint Declarations
limit_ramp : Size=4, Index=TIME, Active=True
Key : Lower : Body : Upper : Active
1 : -50.0 : Generator[G_MAIN].Power[1] - Generator[G_MAIN].Power[0] : Generator[G_MAIN].RampLimit : True
2 : -50.0 : Generator[G_MAIN].Power[2] - Generator[G_MAIN].Power[1] : Generator[G_MAIN].RampLimit : True
3 : -50.0 : Generator[G_MAIN].Power[3] - Generator[G_MAIN].Power[2] : Generator[G_MAIN].RampLimit : True
4 : -50.0 : Generator[G_MAIN].Power[4] - Generator[G_MAIN].Power[3] : Generator[G_MAIN].RampLimit : True
Key : Lower : Body : Upper : Active
1 : -50.0 : Generator[G_MAIN].Power[1] - Generator[G_MAIN].Power[0] : 50.0 : True
2 : -50.0 : Generator[G_MAIN].Power[2] - Generator[G_MAIN].Power[1] : 50.0 : True
3 : -50.0 : Generator[G_MAIN].Power[3] - Generator[G_MAIN].Power[2] : 50.0 : True
4 : -50.0 : Generator[G_MAIN].Power[4] - Generator[G_MAIN].Power[3] : 50.0 : True

7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost

Expand Down
99 changes: 35 additions & 64 deletions pyomo/core/base/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
RangedExpression,
)
from pyomo.core.expr.expr_common import _type_check_exception_arg
from pyomo.core.expr.relational_expr import TrivialRelationalExpression
from pyomo.core.expr.relational_expr import (
TrivialRelationalExpression,
tuple_to_relational_expr,
)
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
Expand Down Expand Up @@ -355,10 +358,24 @@ def equality(self):
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
if lb is not None:
# Note that checking native_types is sufficient:
# constant expressions should have already been
# simplified by the expression system. If the user
# explicitly created relational expressions with
# constant arguments, then we assume they knew what they
# were doing.
if lb.__class__ in native_types:
ub = expr.arg(2)
if ub.__class__ in native_types:
return lb == ub
else:
# TBD: this is a very restrictive form of structural
# equality. In the future it might be "nice" to
# look for mathematical equivalence - but that is
# expensive and likely not worth the effort.
return lb is expr.arg(2)
return False

@property
Expand Down Expand Up @@ -401,61 +418,15 @@ def set_value(self, expr):
"using '<=', '>=', or '=='." % (self.name,)
)
self._expr = expr
return

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, type(arg).__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)
return
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)
return
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
#
if expr is Constraint.Skip:
self.set_value(tuple_to_relational_expr(expr))

elif expr is Constraint.Skip:
#
# Ignore (and remove) an 'empty' constraint
#
del self.parent_component()[self.index()]
return

elif expr is None:
raise ValueError(_rule_returned_none_error % (self.name,))
Expand All @@ -478,14 +449,14 @@ def set_value(self, expr):
except AttributeError:
pass

raise ValueError(
"Constraint '%s' does not have a proper "
"value. Found %s '%s'\nExpecting a tuple or "
"relational expression. Examples:"
"\n sum(model.costs) == model.income"
"\n (0, model.price[item], 50)"
% (self.name, type(expr).__name__, str(expr))
)
raise ValueError(
"Constraint '%s' does not have a proper "
"value. Found %s '%s'\nExpecting a tuple or "
"relational expression. Examples:"
"\n sum(model.costs) == model.income"
"\n (0, model.price[item], 50)"
% (self.name, type(expr).__name__, str(expr))
)

def lslack(self):
"""
Expand Down
Loading
Loading