Skip to content

Map Constraint.Feasible/Infeasible to concrete constraints #3546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 42 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0e04951
Map Constraint.Infeasible/Feasible to trivial inequalities
jsiirola Mar 28, 2025
dc53dbc
Rework set_value to remove duplicate exception
jsiirola Mar 28, 2025
d5cdeec
Clarify reference set name
jsiirola Mar 28, 2025
fe3ccb5
Use more modern super() / type() syntax
jsiirola Mar 28, 2025
5ddf0b6
Fix typo
jsiirola Mar 28, 2025
537cd7b
Port constraint rework to logical_constraint
jsiirola Mar 28, 2025
efa4381
Support passing Feasible/Infeasible in Disjunction expression lists
jsiirola Mar 28, 2025
57c3a27
Support BooleanConstant in logical_to_disjunctive
jsiirola Mar 28, 2025
9e05fa9
Handle constant expressions in logical_to_disjunctive
jsiirola Mar 28, 2025
8943ebb
Support constant expressions in logical_to_linear
jsiirola Mar 28, 2025
ccffb16
Update tests for trivial feasible/infeasible constraints
jsiirola Mar 28, 2025
ceca444
NFC: justify using inequalities over equalities
jsiirola Mar 28, 2025
f2c0147
set_value() should not change the constraint data when raising an exc…
jsiirola Mar 30, 2025
869ef48
Add 'expr' to disable_methods, mab body through expr
jsiirola Mar 30, 2025
40787a9
Use __name__ for logger scope
jsiirola Mar 30, 2025
e8b3219
Remove unreachable code
jsiirola Mar 30, 2025
935b443
Leverage is_logical_type for detecting logical expressions/vars
jsiirola Mar 30, 2025
0faf9b1
fix construction log message
jsiirola Mar 30, 2025
592d2fa
Support starting_index for LogicalConstraintList
jsiirola Mar 30, 2025
3247246
Make as_boolean behave similar to as_numeric
jsiirola Mar 30, 2025
583593c
Improve LogicalConstraint test coverage
jsiirola Mar 30, 2025
fd98977
NFC: apply black
jsiirola Mar 30, 2025
eab8828
NFC: fix typo
jsiirola Mar 30, 2025
847dc5a
Resolve test failures due to message change
jsiirola Mar 30, 2025
9151e93
Relax LogicalConstraint expression validation.
jsiirola Mar 30, 2025
5a821ae
Merge branch 'main' into feas-infeas
jsiirola Mar 31, 2025
3d31076
Merge branch 'main' into feas-infeas
jsiirola Apr 1, 2025
3e188e8
Fix bad merge
jsiirola Apr 1, 2025
d3ff6d4
Merge branch 'main' into feas-infeas
jsiirola Apr 2, 2025
3084cf8
Remove references to LogicalStatement
jsiirola May 13, 2025
1c99111
NFC: fix typos
jsiirola May 13, 2025
e2927ed
Merge branch 'main' into feas-infeas
jsiirola May 13, 2025
506888b
Update Constraint.Feasible/Infeasible from type to singleton
jsiirola May 13, 2025
f7cc5dc
Make BooleanConstants singletons
jsiirola May 13, 2025
329ee05
Move LogicalConstraint.Feasible/Infeasible from type to BooleanConstant
jsiirola May 13, 2025
a995d74
Relax (slightly) the simplification in Or/And.add()
jsiirola May 13, 2025
73054ec
Update test baseline (new string output)
jsiirola May 13, 2025
0e21fea
Update tests to reflect change in Feasible/Infeasible
jsiirola May 20, 2025
b8d1de7
Calling methods on a class can raise a TypeError; catch it
jsiirola May 20, 2025
1e6e15b
Update constraint processing to track change in Constraint.(In)Feasible
jsiirola May 20, 2025
c750576
Merge branch 'main' into feas-infeas
mrmundt May 20, 2025
a75e382
Merge pull request #13 from jsiirola/singleton-feas-infeas
jsiirola May 20, 2025
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
3 changes: 2 additions & 1 deletion pyomo/common/numeric_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@ def value(obj, exception=True):
tmp = obj(exception=True)
if tmp is None:
raise ValueError(
"No value for uninitialized NumericValue object %s" % (obj.name,)
"No value for uninitialized %s object %s"
% (type(obj).__name__, obj.name)
)
return tmp
except TemplateExpressionError:
Expand Down
11 changes: 10 additions & 1 deletion pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction


def _dispatch_boolean_const(visitor, node):
return False, 1 if node.value else 0


def _dispatch_boolean_var(visitor, node):
if node not in visitor.boolean_to_binary_map:
binary = node.get_associated_binary()
Expand Down Expand Up @@ -197,6 +201,7 @@ def _dispatch_atmost(visitor, node, *args):
_operator_dispatcher[EXPR.AtMostExpression] = _dispatch_atmost

_before_child_dispatcher = {}
_before_child_dispatcher[EXPR.BooleanConstant] = _dispatch_boolean_const
_before_child_dispatcher[BV.ScalarBooleanVar] = _dispatch_boolean_var
_before_child_dispatcher[BV.BooleanVarData] = _dispatch_boolean_var
_before_child_dispatcher[AutoLinkedBooleanVar] = _dispatch_boolean_var
Expand Down Expand Up @@ -264,5 +269,9 @@ def finalizeResult(self, result):
# This LogicalExpression must evaluate to True (but note that we cannot
# fix this variable to 1 since this logical expression could be living
# on a Disjunct and later need to be relaxed.)
self.constraints.add(result >= 1)
expr = result >= 1
if expr.__class__ is bool:
self.constraints.add(Constraint.Feasible if expr else Constraint.Infeasible)
else:
self.constraints.add(expr)
return result
2 changes: 1 addition & 1 deletion pyomo/contrib/incidence_analysis/incidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def _get_incident_via_standard_repn(
except ValueError as err:
# Catch error evaluating expression with uninitialized variables
# TODO: Suppress logged error?
if "No value for uninitialized NumericValue" not in str(err):
if "No value for uninitialized VarData" not in str(err):
raise err
value = None
if value != 0:
Expand Down
2 changes: 1 addition & 1 deletion pyomo/contrib/incidence_analysis/tests/test_incidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_uninitialized_value_error_message(self):
m = pyo.ConcreteModel()
m.x = pyo.Var([1, 2])
m.x[1].set_value(5)
msg = "No value for uninitialized NumericValue"
msg = "No value for uninitialized VarData"
with self.assertRaisesRegex(ValueError, msg):
pyo.value(1 + m.x[1] * m.x[2])

Expand Down
68 changes: 28 additions & 40 deletions pyomo/core/base/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
RangedExpression,
)
from pyomo.core.expr.expr_common import _type_check_exception_arg
from pyomo.core.expr.relational_expr import TrivialRelationalExpression
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 All @@ -63,10 +64,11 @@

_inf = float('inf')
_nonfinite_values = {_inf, -_inf}
_known_relational_expressions = {
_known_relational_expression_types = {
EqualityExpression,
InequalityExpression,
RangedExpression,
TrivialRelationalExpression,
}
_strict_relational_exprs = {True, (False, True), (True, False), (True, True)}
_rule_returned_none_error = """Constraint '%s': rule returned None.
Expand Down Expand Up @@ -382,9 +384,7 @@ def get_value(self):

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 expr.__class__ in _known_relational_expression_types:
if getattr(expr, 'strict', False) in _strict_relational_exprs:
raise ValueError(
"Constraint '%s' encountered a strict "
Expand All @@ -393,6 +393,7 @@ 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:
Expand All @@ -407,7 +408,7 @@ def set_value(self, expr):
"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__)
% (self.name, expr, type(arg).__name__)
)
if len(expr) == 2:
#
Expand All @@ -420,6 +421,7 @@ def set_value(self, expr):
"cannot contain None [received %s]" % (self.name, expr)
)
self._expr = EqualityExpression(expr)
return
elif len(expr) == 3:
#
# Form (ranged) inequality expression
Expand All @@ -430,6 +432,7 @@ def set_value(self, expr):
self._expr = InequalityExpression(expr[:2], False)
else:
self._expr = RangedExpression(expr, False)
return
else:
raise ValueError(
"Constraint '%s' does not have a proper value. "
Expand All @@ -442,25 +445,9 @@ def set_value(self, expr):
#
# Ignore an 'empty' constraint
#
elif expr.__class__ is type:
if expr is Constraint.Skip:
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))
)
return

elif expr is None:
raise ValueError(_rule_returned_none_error % (self.name,))
Expand All @@ -479,17 +466,18 @@ def set_value(self, expr):
try:
if expr.is_expression_type(ExpressionType.RELATIONAL):
self._expr = expr
return
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)

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 Expand Up @@ -619,10 +607,9 @@ class Constraint(ActiveIndexedComponent):

_ComponentDataClass = ConstraintData

class Infeasible(object):
pass
Infeasible = TrivialRelationalExpression('Infeasible', (1, 0))
Feasible = TrivialRelationalExpression('Feasible', (0, 0))

Feasible = ActiveIndexedComponent.Skip
NoConstraint = ActiveIndexedComponent.Skip
Violated = Infeasible
Satisfied = Feasible
Expand All @@ -640,11 +627,11 @@ def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: .

def __new__(cls, *args, **kwds):
if cls != Constraint:
return super(Constraint, cls).__new__(cls)
return super().__new__(cls)
if not args or (args[0] is UnindexedComponent_set and len(args) == 1):
return super(Constraint, cls).__new__(AbstractScalarConstraint)
return super().__new__(AbstractScalarConstraint)
else:
return super(Constraint, cls).__new__(IndexedConstraint)
return super().__new__(IndexedConstraint)

@overload
def __init__(self, *indexes, expr=None, rule=None, name=None, doc=None): ...
Expand Down Expand Up @@ -901,7 +888,7 @@ 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)
return super().set_value(expr)

#
# Leaving this method for backward compatibility reasons.
Expand All @@ -928,6 +915,7 @@ class SimpleConstraint(metaclass=RenamedClass):
'add',
'set_value',
'to_bounded_expression',
'expr',
'body',
'lower',
'upper',
Expand Down Expand Up @@ -981,7 +969,7 @@ def __init__(self, **kwargs):
_rule = kwargs.pop('rule', None)
self._starting_index = kwargs.pop('starting_index', 1)

super(ConstraintList, self).__init__(Set(dimen=1), **kwargs)
super().__init__(Set(dimen=1), **kwargs)

self.rule = Initializer(
_rule, treat_sequences_as_mappings=False, allow_generators=True
Expand Down
Loading
Loading