Skip to content

Commit a75e382

Browse files
authored
Merge pull request #13 from jsiirola/singleton-feas-infeas
Convert Feasible/Infeasible to singletons
2 parents c750576 + 1e6e15b commit a75e382

File tree

7 files changed

+114
-74
lines changed

7 files changed

+114
-74
lines changed

pyomo/core/base/constraint.py

Lines changed: 7 additions & 23 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
@@ -67,6 +68,7 @@
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.
@@ -443,24 +445,9 @@ def set_value(self, expr):
443445
#
444446
# Ignore an 'empty' constraint
445447
#
446-
elif expr.__class__ is type:
447-
if expr is Constraint.Skip:
448-
del self.parent_component()[self.index()]
449-
return
450-
elif expr is Constraint.Infeasible:
451-
# Note that an inequality is sufficient to induce
452-
# infeasibility and is simpler to relax (in the Big-M
453-
# sense) than an equality.
454-
self._expr = InequalityExpression((1, 0), False)
455-
return
456-
elif expr is Constraint.Feasible:
457-
# Note that we do not want to provide a trivial equality
458-
# constraint as that can confuse solvers like Ipopt into
459-
# believing that the model has fewer degrees of freedom
460-
# than it actually has.
461-
self._expr = InequalityExpression((0, 0), False)
462-
return
463-
# else: fall through to the ValueError below
448+
if expr is Constraint.Skip:
449+
del self.parent_component()[self.index()]
450+
return
464451

465452
elif expr is None:
466453
raise ValueError(_rule_returned_none_error % (self.name,))
@@ -620,11 +607,8 @@ class Constraint(ActiveIndexedComponent):
620607

621608
_ComponentDataClass = ConstraintData
622609

623-
class Infeasible(object):
624-
pass
625-
626-
class Feasible(object):
627-
pass
610+
Infeasible = TrivialRelationalExpression('Infeasible', (1, 0))
611+
Feasible = TrivialRelationalExpression('Feasible', (0, 0))
628612

629613
NoConstraint = ActiveIndexedComponent.Skip
630614
Violated = Infeasible

pyomo/core/base/logical_constraint.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,9 @@ def set_value(self, expr):
112112
#
113113
# Ignore an 'empty' constraint
114114
#
115-
if expr.__class__ is type:
116-
if expr is LogicalConstraint.Skip:
117-
del self.parent_component()[self.index()]
118-
return
119-
elif expr is LogicalConstraint.Infeasible:
120-
self._expr = BooleanConstant(False)
121-
return
122-
elif expr is LogicalConstraint.Feasible:
123-
self._expr = BooleanConstant(True)
124-
return
125-
# else: fall through to the ValueError below
115+
if expr is LogicalConstraint.Skip:
116+
del self.parent_component()[self.index()]
117+
return
126118

127119
elif expr.__class__ in native_logical_types:
128120
self._expr = as_boolean(expr)
@@ -148,7 +140,7 @@ def set_value(self, expr):
148140
if hasattr(expr, '_resolve_template'):
149141
self._expr = expr
150142
return
151-
except AttributeError:
143+
except (AttributeError, TypeError):
152144
pass
153145

154146
raise ValueError(
@@ -218,11 +210,8 @@ class LogicalConstraint(ActiveIndexedComponent):
218210

219211
_ComponentDataClass = LogicalConstraintData
220212

221-
class Infeasible(object):
222-
pass
223-
224-
class Feasible(object):
225-
pass
213+
Infeasible = BooleanConstant(False, 'Infeasible')
214+
Feasible = BooleanConstant(True, 'Feasible')
226215

227216
NoConstraint = ActiveIndexedComponent.Skip
228217
Violated = Infeasible

pyomo/core/expr/boolean_value.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,14 +278,32 @@ class BooleanConstant(BooleanValue):
278278
value The initial value.
279279
"""
280280

281-
__slots__ = ('value',)
282-
283-
def __init__(self, value):
284-
if value not in native_logical_values:
285-
raise TypeError(
286-
'Not a valid BooleanValue. Unable to create a logical constant'
287-
)
288-
self.value = value
281+
__slots__ = ('value', '_name')
282+
singleton = {}
283+
284+
def __new__(cls, value, name=None):
285+
if name is None:
286+
name = value
287+
if name not in cls.singleton:
288+
if value not in native_logical_values:
289+
raise TypeError(
290+
'Not a valid BooleanValue. Unable to create a logical constant'
291+
)
292+
cls.singleton[name] = super().__new__(cls)
293+
cls.singleton[name].value = value
294+
cls.singleton[name]._name = name
295+
return cls.singleton[name]
296+
297+
def __deepcopy__(self, memo):
298+
# Prevent deepcopy from duplicating this object
299+
return self
300+
301+
def __reduce__(self):
302+
return self.__class__, (self._name, self._args_)
303+
304+
def __init__(self, value, name=None):
305+
# note that the meat of __init__ is called as part of __new__ above.
306+
assert self.value == value
289307

290308
def is_constant(self):
291309
return True
@@ -297,7 +315,7 @@ def is_potentially_variable(self):
297315
return False
298316

299317
def __str__(self):
300-
return str(self.value)
318+
return str(self._name)
301319

302320
def __nonzero__(self):
303321
return self.value

pyomo/core/expr/logical_expr.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -471,11 +471,14 @@ def _apply_operation(self, result):
471471
return all(result)
472472

473473
def add(self, new_arg):
474+
# FIXME: we should probably NOT perform this simplification
475+
# here, and instead use a dispatcher-based expression generation
476+
# system
474477
if new_arg.__class__ in native_logical_types:
475-
if new_arg is False:
476-
return BooleanConstant(False)
477-
elif new_arg is True:
478+
if new_arg:
478479
return self
480+
else:
481+
return BooleanConstant(False)
479482
return _add_to_and_or_expression(self, new_arg)
480483

481484

@@ -498,11 +501,14 @@ def _apply_operation(self, result):
498501
return any(result)
499502

500503
def add(self, new_arg):
504+
# FIXME: we should probably NOT perform this simplification
505+
# here, and instead use a dispatcher-based expression generation
506+
# system
501507
if new_arg.__class__ in native_logical_types:
502-
if new_arg is False:
503-
return self
504-
elif new_arg is True:
508+
if new_arg:
505509
return BooleanConstant(True)
510+
else:
511+
return self
506512
return _add_to_and_or_expression(self, new_arg)
507513

508514

pyomo/core/expr/relational_expr.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,43 @@ def strict(self):
307307
return self._strict
308308

309309

310+
class TrivialRelationalExpression(InequalityExpression):
311+
"""A trivial relational expression
312+
313+
Note that an inequality is sufficient to induce infeasibility and is
314+
simpler to relax (in the Big-M sense) than an equality.
315+
316+
Note that we do not want to provide a trivial equality constraint as
317+
that can confuse solvers like Ipopt into believing that the model
318+
has fewer degrees of freedom than it actually has.
319+
320+
"""
321+
322+
__slots__ = ('_name',)
323+
singleton = {}
324+
325+
def __new__(cls, name, args):
326+
if name not in cls.singleton:
327+
cls.singleton[name] = super().__new__(cls)
328+
super().__init__(cls.singleton[name], args, False)
329+
cls.singleton[name]._name = name
330+
return cls.singleton[name]
331+
332+
def __init__(self, name, args):
333+
# note that the meat of __init__ is called as part of __new__ above.
334+
assert args == self.args
335+
336+
def __deepcopy__(self, memo):
337+
# Prevent deepcopy from duplicating this object
338+
return self
339+
340+
def __reduce__(self):
341+
return self.__class__, (self._name, self._args_)
342+
343+
def _to_string(self, values, verbose, smap):
344+
return self._name
345+
346+
310347
def inequality(lower=None, body=None, upper=None, strict=False):
311348
"""
312349
A utility function that can be used to declare inequality and

pyomo/core/tests/unit/test_logical_constraint.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@ def test_indexed_constructor(self):
117117
],
118118
)
119119

120-
self.assertExpressionsStructurallyEqual(m.p[0].expr, BooleanConstant(True))
121-
self.assertExpressionsStructurallyEqual(m.p[1].expr, BooleanConstant(False))
120+
self.assertExpressionsStructurallyEqual(m.p[0].expr, LogicalConstraint.Feasible)
121+
self.assertExpressionsStructurallyEqual(
122+
m.p[1].expr, LogicalConstraint.Infeasible
123+
)
122124
self.assertNotIn(2, m.p)
123125
with self.assertRaises(KeyError):
124126
m.p[2].expr
@@ -202,13 +204,13 @@ def test_pprint(self):
202204
m.p.pprint(ostream=OUT)
203205
self.assertEqual(
204206
"""p : Size=6, Index={0, 1, 2, 3, 4, 5, 6}, Active=True
205-
Key : Body : Active
206-
0 : True : True
207-
1 : False : True
208-
3 : x --> y : True
209-
4 : x : True
210-
5 : True : True
211-
6 : False : True
207+
Key : Body : Active
208+
0 : Feasible : True
209+
1 : Infeasible : True
210+
3 : x --> y : True
211+
4 : x : True
212+
5 : True : True
213+
6 : False : True
212214
""",
213215
OUT.getvalue(),
214216
)
@@ -229,8 +231,10 @@ def p_rule(m, i):
229231

230232
m.p = LogicalConstraint(NonNegativeIntegers, rule=p_rule)
231233

232-
self.assertExpressionsStructurallyEqual(m.p[0].expr, BooleanConstant(True))
233-
self.assertExpressionsStructurallyEqual(m.p[1].expr, BooleanConstant(False))
234+
self.assertExpressionsStructurallyEqual(m.p[0].expr, LogicalConstraint.Feasible)
235+
self.assertExpressionsStructurallyEqual(
236+
m.p[1].expr, LogicalConstraint.Infeasible
237+
)
234238
self.assertNotIn(2, m.p)
235239
with self.assertRaises(KeyError):
236240
m.p[2].expr
@@ -287,23 +291,23 @@ def test_set_value(self):
287291
self.assertEqual(len(m.p), 0)
288292

289293
m.p = LogicalConstraint.Feasible
290-
self.assertExpressionsStructurallyEqual(m.p.expr, BooleanConstant(True))
294+
self.assertExpressionsStructurallyEqual(m.p.expr, LogicalConstraint.Feasible)
291295

292296
m.p = LogicalConstraint.Infeasible
293-
self.assertExpressionsStructurallyEqual(m.p.expr, BooleanConstant(False))
297+
self.assertExpressionsStructurallyEqual(m.p.expr, LogicalConstraint.Infeasible)
294298

295299
with self.assertRaisesRegex(
296-
ValueError, "Assigning improper value to LogicalConstraint 'p':"
300+
ValueError, "Assigning improper value to LogicalConstraint 'p'."
297301
):
298302
m.p = LogicalConstraint
299303

300304
with self.assertRaisesRegex(
301-
ValueError, "Assigning improper value to LogicalConstraint 'p':"
305+
ValueError, "Assigning improper value to LogicalConstraint 'p'."
302306
):
303307
m.p = {}
304308

305309
with self.assertRaisesRegex(
306-
ValueError, "Assigning improper value to LogicalConstraint 'p':"
310+
ValueError, "Assigning improper value to LogicalConstraint 'p'."
307311
):
308312
m.p = Param(mutable=True) + 1
309313

pyomo/gdp/disjunct.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -588,9 +588,11 @@ def set_value(self, expr):
588588
continue
589589
except AttributeError:
590590
pass
591-
if _tmp_e in (Constraint.Feasible, Constraint.Infeasible):
592-
expressions.append(_tmp_e)
593-
continue
591+
# Note: Constraint.(In)Feasible is now a full relational
592+
# expression, so we don't need to check for it.
593+
# LogicalConstraint.(In)Feasible is a BooleanConstant,
594+
# so is not an expression and needs to be explicitly
595+
# checked for.
594596
if _tmp_e in (LogicalConstraint.Feasible, LogicalConstraint.Infeasible):
595597
propositions.append(_tmp_e)
596598
continue

0 commit comments

Comments
 (0)