cleanup semantics of under-specified constraint in model_builder python

This commit is contained in:
Laurent Perron
2024-03-30 10:53:14 +01:00
parent 32bffd90d5
commit cc71b3ee57
2 changed files with 56 additions and 36 deletions

View File

@@ -432,7 +432,7 @@ def _add_linear_constraint_to_helper(
TypeError: If constraint is an invalid type.
"""
if isinstance(bounded_expr, bool):
c = LinearConstraint(helper)
c = LinearConstraint(helper, None, True)
if name is not None:
helper.set_constraint_name(c.index, name)
if bounded_expr:
@@ -477,7 +477,8 @@ def _add_enforced_linear_constraint_to_helper(
TypeError: If constraint is an invalid type.
"""
if isinstance(bounded_expr, bool):
c = EnforcedLinearConstraint(helper)
# TODO(user): create indicator variable assignment instead ?
c = EnforcedLinearConstraint(helper, None, True)
c.indicator_variable = var
c.indicator_value = value
if name is not None:
@@ -651,13 +652,17 @@ class LinearConstraint:
"""
def __init__(
self, helper: mbh.ModelBuilderHelper, index: Optional[IntegerT] = None
self,
helper: mbh.ModelBuilderHelper,
index: Optional[IntegerT] = None,
is_under_specified: bool = False,
):
if index is None:
self.__index = helper.add_linear_constraint()
else:
self.__index = index
self.__helper: mbh.ModelBuilderHelper = helper
self.__is_under_specified = is_under_specified
@property
def index(self) -> IntegerT:
@@ -696,12 +701,21 @@ class LinearConstraint:
def name(self, name: str) -> None:
return self.__helper.set_constraint_name(self.__index, name)
def is_always_false(self) -> bool:
"""Returns True if the constraint is always false.
@property
def is_under_specified(self) -> bool:
"""Returns True if the constraint is under specified.
Usually, it means that it was created by model.add(False)
Usually, it means that it was created by model.add(False) or model.add(True)
The effect is that modifying the constraint will raise an exception.
"""
return self.lower_bound > self.upper_bound
return self.__is_under_specified
def assert_constraint_is_well_defined(self) -> None:
"""Raises an exception if the constraint is under specified."""
if self.__is_under_specified:
raise ValueError(
f"Constraint {self.index} is under specified and cannot be modified"
)
def __str__(self):
return self.name
@@ -716,22 +730,17 @@ class LinearConstraint:
def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
"""Sets the coefficient of the variable in the constraint."""
if self.is_always_false():
raise ValueError(
f"Constraint {self.index} is always false and cannot be modified"
)
self.assert_constraint_is_well_defined()
self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)
def add_term(self, var: Variable, coeff: NumberT) -> None:
"""Adds var * coeff to the constraint."""
if self.is_always_false():
raise ValueError(
f"Constraint {self.index} is always false and cannot be modified"
)
self.assert_constraint_is_well_defined()
self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff)
def clear_terms(self) -> None:
"""Clear all terms of the constraint."""
self.assert_constraint_is_well_defined()
self.__helper.clear_constraint_terms(self.__index)
@@ -747,7 +756,10 @@ class EnforcedLinearConstraint:
"""
def __init__(
self, helper: mbh.ModelBuilderHelper, index: Optional[IntegerT] = None
self,
helper: mbh.ModelBuilderHelper,
index: Optional[IntegerT] = None,
is_under_specified: bool = False,
):
if index is None:
self.__index = helper.add_enforced_linear_constraint()
@@ -760,6 +772,7 @@ class EnforcedLinearConstraint:
self.__index = index
self.__helper: mbh.ModelBuilderHelper = helper
self.__is_under_specified = is_under_specified
@property
def index(self) -> IntegerT:
@@ -819,12 +832,21 @@ class EnforcedLinearConstraint:
def name(self, name: str) -> None:
return self.__helper.set_enforced_constraint_name(self.__index, name)
def is_always_false(self) -> bool:
"""Returns True if the constraint is always false.
@property
def is_under_specified(self) -> bool:
"""Returns True if the constraint is under specified.
Usually, it means that it was created by model.add(False)
Usually, it means that it was created by model.add(False) or model.add(True)
The effect is that modifying the constraint will raise an exception.
"""
return self.lower_bound > self.upper_bound
return self.__is_under_specified
def assert_constraint_is_well_defined(self) -> None:
"""Raises an exception if the constraint is under specified."""
if self.__is_under_specified:
raise ValueError(
f"Constraint {self.index} is under specified and cannot be modified"
)
def __str__(self):
return self.name
@@ -841,26 +863,21 @@ class EnforcedLinearConstraint:
def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
"""Sets the coefficient of the variable in the constraint."""
if self.is_always_false():
raise ValueError(
f"Constraint {self.index} is always false and cannot be modified"
)
self.assert_constraint_is_well_defined()
self.__helper.set_enforced_constraint_coefficient(
self.__index, var.index, coeff
)
def add_term(self, var: Variable, coeff: NumberT) -> None:
"""Adds var * coeff to the constraint."""
if self.is_always_false():
raise ValueError(
f"Constraint {self.index} is always false and cannot be modified"
)
self.assert_constraint_is_well_defined()
self.__helper.safe_add_term_to_enforced_constraint(
self.__index, var.index, coeff
)
def clear_terms(self) -> None:
"""Clear all terms of the constraint."""
self.assert_constraint_is_well_defined()
self.__helper.clear_enforced_constraint_terms(self.__index)
@@ -1321,12 +1338,16 @@ class Model:
Note that a special treatment is done when the argument does not contain any
variable, and thus evaluates to True or False.
model.add(True) will create a constraint 0 <= empty sum <= 0
model.add(True) will create a constraint 0 <= empty sum <= 0.
The constraint will be marked as under specified, and cannot be modified
further.
model.add(False) will create a constraint inf <= empty sum <= -inf
model.add(False) will create a constraint inf <= empty sum <= -inf. The
constraint will be marked as under specified, and cannot be modified
further.
you can check the if a constraint is always false (lb=inf, ub=-inf) by
calling LinearConstraint.is_always_false()
you can check the if a constraint is under specified by
checking LinearConstraint.is_under_specified.
"""
if isinstance(ct, _BoundedLinearExpr):
return ct._add_linear_constraint(self.__helper, name)

View File

@@ -391,7 +391,7 @@ ENDATA
x = model.new_num_var(0.0, math.inf, "x")
ct = model.add(False)
self.assertTrue(ct.is_always_false())
self.assertTrue(ct.is_under_specified)
self.assertRaises(ValueError, ct.add_term, x, 1)
model.maximize(x)
@@ -408,7 +408,8 @@ ENDATA
ct = model.add(True)
self.assertEqual(ct.lower_bound, 0.0)
self.assertEqual(ct.upper_bound, 0.0)
ct.add_term(var=x, coeff=1)
self.assertTrue(ct.is_under_specified)
self.assertRaises(ValueError, ct.add_term, x, 1)
model.maximize(x)
@@ -416,8 +417,6 @@ ENDATA
status = solver.solve(model)
self.assertEqual(status, mb.SolveStatus.OPTIMAL)
# Note that ct is binding.
self.assertEqual(0.0, solver.objective_value)
class InternalHelperTest(absltest.TestCase):