Skip to content

Commit a8d0bfa

Browse files
authored
Merge pull request #3546 from jsiirola/feas-infeas
Map Constraint.Feasible/Infeasible to concrete constraints
2 parents 1409ee2 + a75e382 commit a8d0bfa

20 files changed

+752
-354
lines changed

pyomo/common/numeric_types.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,8 @@ def value(obj, exception=True):
382382
tmp = obj(exception=True)
383383
if tmp is None:
384384
raise ValueError(
385-
"No value for uninitialized NumericValue object %s" % (obj.name,)
385+
"No value for uninitialized %s object %s"
386+
% (type(obj).__name__, obj.name)
386387
)
387388
return tmp
388389
except TemplateExpressionError:

pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction
3030

3131

32+
def _dispatch_boolean_const(visitor, node):
33+
return False, 1 if node.value else 0
34+
35+
3236
def _dispatch_boolean_var(visitor, node):
3337
if node not in visitor.boolean_to_binary_map:
3438
binary = node.get_associated_binary()
@@ -197,6 +201,7 @@ def _dispatch_atmost(visitor, node, *args):
197201
_operator_dispatcher[EXPR.AtMostExpression] = _dispatch_atmost
198202

199203
_before_child_dispatcher = {}
204+
_before_child_dispatcher[EXPR.BooleanConstant] = _dispatch_boolean_const
200205
_before_child_dispatcher[BV.ScalarBooleanVar] = _dispatch_boolean_var
201206
_before_child_dispatcher[BV.BooleanVarData] = _dispatch_boolean_var
202207
_before_child_dispatcher[AutoLinkedBooleanVar] = _dispatch_boolean_var
@@ -264,5 +269,9 @@ def finalizeResult(self, result):
264269
# This LogicalExpression must evaluate to True (but note that we cannot
265270
# fix this variable to 1 since this logical expression could be living
266271
# on a Disjunct and later need to be relaxed.)
267-
self.constraints.add(result >= 1)
272+
expr = result >= 1
273+
if expr.__class__ is bool:
274+
self.constraints.add(Constraint.Feasible if expr else Constraint.Infeasible)
275+
else:
276+
self.constraints.add(expr)
268277
return result

pyomo/contrib/incidence_analysis/incidence.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def _get_incident_via_standard_repn(
5858
except ValueError as err:
5959
# Catch error evaluating expression with uninitialized variables
6060
# TODO: Suppress logged error?
61-
if "No value for uninitialized NumericValue" not in str(err):
61+
if "No value for uninitialized VarData" not in str(err):
6262
raise err
6363
value = None
6464
if value != 0:

pyomo/contrib/incidence_analysis/tests/test_incidence.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_uninitialized_value_error_message(self):
3333
m = pyo.ConcreteModel()
3434
m.x = pyo.Var([1, 2])
3535
m.x[1].set_value(5)
36-
msg = "No value for uninitialized NumericValue"
36+
msg = "No value for uninitialized VarData"
3737
with self.assertRaisesRegex(ValueError, msg):
3838
pyo.value(1 + m.x[1] * m.x[2])
3939

pyomo/core/base/constraint.py

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
RangedExpression,
4040
)
4141
from pyomo.core.expr.expr_common import _type_check_exception_arg
42+
from pyomo.core.expr.relational_expr import TrivialRelationalExpression
4243
from pyomo.core.expr.template_expr import templatize_constraint
4344
from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory
4445
from pyomo.core.base.global_set import UnindexedComponent_index
@@ -63,10 +64,11 @@
6364

6465
_inf = float('inf')
6566
_nonfinite_values = {_inf, -_inf}
66-
_known_relational_expressions = {
67+
_known_relational_expression_types = {
6768
EqualityExpression,
6869
InequalityExpression,
6970
RangedExpression,
71+
TrivialRelationalExpression,
7072
}
7173
_strict_relational_exprs = {True, (False, True), (True, False), (True, True)}
7274
_rule_returned_none_error = """Constraint '%s': rule returned None.
@@ -382,9 +384,7 @@ def get_value(self):
382384

383385
def set_value(self, expr):
384386
"""Set the expression on this constraint."""
385-
# Clear any previously-cached normalized constraint
386-
self._expr = None
387-
if expr.__class__ in _known_relational_expressions:
387+
if expr.__class__ in _known_relational_expression_types:
388388
if getattr(expr, 'strict', False) in _strict_relational_exprs:
389389
raise ValueError(
390390
"Constraint '%s' encountered a strict "
@@ -393,6 +393,7 @@ def set_value(self, expr):
393393
"using '<=', '>=', or '=='." % (self.name,)
394394
)
395395
self._expr = expr
396+
return
396397

397398
elif expr.__class__ is tuple: # or expr_type is list:
398399
for arg in expr:
@@ -407,7 +408,7 @@ def set_value(self, expr):
407408
"Constraint expressions expressed as tuples must "
408409
"contain native numeric types or Pyomo NumericValue "
409410
"objects. Tuple %s contained invalid type, %s"
410-
% (self.name, expr, arg.__class__.__name__)
411+
% (self.name, expr, type(arg).__name__)
411412
)
412413
if len(expr) == 2:
413414
#
@@ -420,6 +421,7 @@ def set_value(self, expr):
420421
"cannot contain None [received %s]" % (self.name, expr)
421422
)
422423
self._expr = EqualityExpression(expr)
424+
return
423425
elif len(expr) == 3:
424426
#
425427
# Form (ranged) inequality expression
@@ -430,6 +432,7 @@ def set_value(self, expr):
430432
self._expr = InequalityExpression(expr[:2], False)
431433
else:
432434
self._expr = RangedExpression(expr, False)
435+
return
433436
else:
434437
raise ValueError(
435438
"Constraint '%s' does not have a proper value. "
@@ -442,25 +445,9 @@ def set_value(self, expr):
442445
#
443446
# Ignore an 'empty' constraint
444447
#
445-
elif expr.__class__ is type:
448+
if expr is Constraint.Skip:
446449
del self.parent_component()[self.index()]
447-
if expr is Constraint.Skip:
448-
return
449-
elif expr is Constraint.Infeasible:
450-
# TODO: create a trivial infeasible constraint. This
451-
# could be useful in the case of GDP where certain
452-
# disjuncts are trivially infeasible, but we would still
453-
# like to express the disjunction.
454-
# del self.parent_component()[self.index()]
455-
raise ValueError("Constraint '%s' is always infeasible" % (self.name,))
456-
else:
457-
raise ValueError(
458-
"Constraint '%s' does not have a proper "
459-
"value. Found '%s'\nExpecting a tuple or "
460-
"relational expression. Examples:"
461-
"\n sum(model.costs) == model.income"
462-
"\n (0, model.price[item], 50)" % (self.name, str(expr))
463-
)
450+
return
464451

465452
elif expr is None:
466453
raise ValueError(_rule_returned_none_error % (self.name,))
@@ -479,17 +466,18 @@ def set_value(self, expr):
479466
try:
480467
if expr.is_expression_type(ExpressionType.RELATIONAL):
481468
self._expr = expr
469+
return
482470
except AttributeError:
483471
pass
484-
if self._expr is None:
485-
msg = (
486-
"Constraint '%s' does not have a proper "
487-
"value. Found '%s'\nExpecting a tuple or "
488-
"relational expression. Examples:"
489-
"\n sum(model.costs) == model.income"
490-
"\n (0, model.price[item], 50)" % (self.name, str(expr))
491-
)
492-
raise ValueError(msg)
472+
473+
raise ValueError(
474+
"Constraint '%s' does not have a proper "
475+
"value. Found %s '%s'\nExpecting a tuple or "
476+
"relational expression. Examples:"
477+
"\n sum(model.costs) == model.income"
478+
"\n (0, model.price[item], 50)"
479+
% (self.name, type(expr).__name__, str(expr))
480+
)
493481

494482
def lslack(self):
495483
"""
@@ -619,10 +607,9 @@ class Constraint(ActiveIndexedComponent):
619607

620608
_ComponentDataClass = ConstraintData
621609

622-
class Infeasible(object):
623-
pass
610+
Infeasible = TrivialRelationalExpression('Infeasible', (1, 0))
611+
Feasible = TrivialRelationalExpression('Feasible', (0, 0))
624612

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

641628
def __new__(cls, *args, **kwds):
642629
if cls != Constraint:
643-
return super(Constraint, cls).__new__(cls)
630+
return super().__new__(cls)
644631
if not args or (args[0] is UnindexedComponent_set and len(args) == 1):
645-
return super(Constraint, cls).__new__(AbstractScalarConstraint)
632+
return super().__new__(AbstractScalarConstraint)
646633
else:
647-
return super(Constraint, cls).__new__(IndexedConstraint)
634+
return super().__new__(IndexedConstraint)
648635

649636
@overload
650637
def __init__(self, *indexes, expr=None, rule=None, name=None, doc=None): ...
@@ -901,7 +888,7 @@ def set_value(self, expr):
901888
"""Set the expression on this constraint."""
902889
if not self._data:
903890
self._data[None] = self
904-
return super(ScalarConstraint, self).set_value(expr)
891+
return super().set_value(expr)
905892

906893
#
907894
# Leaving this method for backward compatibility reasons.
@@ -928,6 +915,7 @@ class SimpleConstraint(metaclass=RenamedClass):
928915
'add',
929916
'set_value',
930917
'to_bounded_expression',
918+
'expr',
931919
'body',
932920
'lower',
933921
'upper',
@@ -981,7 +969,7 @@ def __init__(self, **kwargs):
981969
_rule = kwargs.pop('rule', None)
982970
self._starting_index = kwargs.pop('starting_index', 1)
983971

984-
super(ConstraintList, self).__init__(Set(dimen=1), **kwargs)
972+
super().__init__(Set(dimen=1), **kwargs)
985973

986974
self.rule = Initializer(
987975
_rule, treat_sequences_as_mappings=False, allow_generators=True

0 commit comments

Comments
 (0)