mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 01:18:52 -06:00
Implement bulk constraint methods
This commit is contained in:
@@ -111,10 +111,59 @@ class GurobiSolver(InternalSolver):
|
|||||||
self._has_lp_solution = False
|
self._has_lp_solution = False
|
||||||
self._has_mip_solution = False
|
self._has_mip_solution = False
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
||||||
|
assert cf.names is not None
|
||||||
|
assert cf.senses is not None
|
||||||
|
assert cf.lhs is not None
|
||||||
|
assert cf.rhs is not None
|
||||||
|
assert self.model is not None
|
||||||
|
for i in range(len(cf.names)):
|
||||||
|
sense = cf.senses[i]
|
||||||
|
lhs = self.gp.quicksum(
|
||||||
|
self._varname_to_var[varname] * coeff for (varname, coeff) in cf.lhs[i]
|
||||||
|
)
|
||||||
|
if sense == "=":
|
||||||
|
self.model.addConstr(lhs == cf.rhs[i], name=cf.names[i])
|
||||||
|
elif sense == "<":
|
||||||
|
self.model.addConstr(lhs <= cf.rhs[i], name=cf.names[i])
|
||||||
|
else:
|
||||||
|
self.model.addConstr(lhs >= cf.rhs[i], name=cf.names[i])
|
||||||
|
self.model.update()
|
||||||
|
self._dirty = True
|
||||||
|
self._has_lp_solution = False
|
||||||
|
self._has_mip_solution = False
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def are_callbacks_supported(self) -> bool:
|
def are_callbacks_supported(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def are_constraints_satisfied(
|
||||||
|
self,
|
||||||
|
cf: ConstraintFeatures,
|
||||||
|
tol: float = 1e-5,
|
||||||
|
) -> List[bool]:
|
||||||
|
assert cf.names is not None
|
||||||
|
assert cf.senses is not None
|
||||||
|
assert cf.lhs is not None
|
||||||
|
assert cf.rhs is not None
|
||||||
|
assert self.model is not None
|
||||||
|
result = []
|
||||||
|
for i in range(len(cf.names)):
|
||||||
|
sense = cf.senses[i]
|
||||||
|
lhs = sum(
|
||||||
|
self._varname_to_var[varname].x * coeff
|
||||||
|
for (varname, coeff) in cf.lhs[i]
|
||||||
|
)
|
||||||
|
if sense == "<":
|
||||||
|
result.append(lhs <= cf.rhs[i] + tol)
|
||||||
|
elif sense == ">":
|
||||||
|
result.append(lhs >= cf.rhs[i] - tol)
|
||||||
|
else:
|
||||||
|
result.append(abs(cf.rhs[i] - lhs) <= tol)
|
||||||
|
return result
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def build_test_instance_infeasible(self) -> Instance:
|
def build_test_instance_infeasible(self) -> Instance:
|
||||||
return GurobiTestInstanceInfeasible()
|
return GurobiTestInstanceInfeasible()
|
||||||
@@ -477,6 +526,13 @@ class GurobiSolver(InternalSolver):
|
|||||||
constr = self.model.getConstrByName(name)
|
constr = self.model.getConstrByName(name)
|
||||||
self.model.remove(constr)
|
self.model.remove(constr)
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def remove_constraints(self, names: List[str]) -> None:
|
||||||
|
assert self.model is not None
|
||||||
|
constrs = [self.model.getConstrByName(n) for n in names]
|
||||||
|
self.model.remove(constrs)
|
||||||
|
self.model.update()
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def set_instance(
|
def set_instance(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -186,6 +186,22 @@ class InternalSolver(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
||||||
|
"""Adds the given constraints to the model."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def are_constraints_satisfied(
|
||||||
|
self,
|
||||||
|
cf: ConstraintFeatures,
|
||||||
|
tol: float = 1e-5,
|
||||||
|
) -> List[bool]:
|
||||||
|
"""
|
||||||
|
Checks whether the current solution satisfies the given constraints.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def remove_constraint(self, name: str) -> None:
|
def remove_constraint(self, name: str) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -193,6 +209,13 @@ class InternalSolver(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_constraints(self, names: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Removes the given constraints from the model.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_constraint_satisfied_old(
|
def is_constraint_satisfied_old(
|
||||||
self, constr: Constraint, tol: float = 1e-6
|
self, constr: Constraint, tol: float = 1e-6
|
||||||
@@ -235,6 +258,14 @@ class InternalSolver(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def build_test_instance_knapsack(self) -> Instance:
|
def build_test_instance_knapsack(self) -> Instance:
|
||||||
|
"""
|
||||||
|
Returns an instance corresponding to the following MIP, for testing purposes:
|
||||||
|
|
||||||
|
maximize 505 x0 + 352 x1 + 458 x2 + 220 x3
|
||||||
|
s.t. eq_capacity: z = 23 x0 + 26 x1 + 20 x2 + 18 x3
|
||||||
|
x0, x1, x2, x3 binary
|
||||||
|
0 <= z <= 67 continuous
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def are_callbacks_supported(self) -> bool:
|
def are_callbacks_supported(self) -> bool:
|
||||||
@@ -250,12 +281,28 @@ class InternalSolver(ABC):
|
|||||||
with_static: bool = True,
|
with_static: bool = True,
|
||||||
with_sa: bool = True,
|
with_sa: bool = True,
|
||||||
) -> VariableFeatures:
|
) -> VariableFeatures:
|
||||||
|
"""
|
||||||
|
Returns a description of the decision variables in the problem.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
with_static: bool
|
||||||
|
If True, include features that do not change during the solution process,
|
||||||
|
such as variable types and names. This parameter is used to reduce the
|
||||||
|
amount of duplicated data collected by LearningSolver. Features that do
|
||||||
|
not change are only collected once.
|
||||||
|
with_sa: bool
|
||||||
|
If True, collect sensitivity analysis information. For large models,
|
||||||
|
collecting this information may be expensive, so this parameter is useful
|
||||||
|
for reducing running times.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_constraint_attrs(self) -> List[str]:
|
def get_constraint_attrs(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Returns a list of constraint attributes supported by this solver.
|
Returns a list of constraint attributes supported by this solver. Used for
|
||||||
|
testing purposes only.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
@@ -263,6 +310,7 @@ class InternalSolver(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_variable_attrs(self) -> List[str]:
|
def get_variable_attrs(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Returns a list of variable attributes supported by this solver.
|
Returns a list of variable attributes supported by this solver. Used for
|
||||||
|
testing purposes only.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -98,10 +98,60 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
self._has_lp_solution = False
|
self._has_lp_solution = False
|
||||||
self._has_mip_solution = False
|
self._has_mip_solution = False
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
||||||
|
assert cf.names is not None
|
||||||
|
assert cf.senses is not None
|
||||||
|
assert cf.lhs is not None
|
||||||
|
assert cf.rhs is not None
|
||||||
|
assert self.model is not None
|
||||||
|
for (i, name) in enumerate(cf.names):
|
||||||
|
lhs = 0.0
|
||||||
|
for (varname, coeff) in cf.lhs[i]:
|
||||||
|
var = self._varname_to_var[varname]
|
||||||
|
lhs += var * coeff
|
||||||
|
if cf.senses[i] == "=":
|
||||||
|
expr = lhs == cf.rhs[i]
|
||||||
|
elif cf.senses[i] == "<":
|
||||||
|
expr = lhs <= cf.rhs[i]
|
||||||
|
else:
|
||||||
|
expr = lhs >= cf.rhs[i]
|
||||||
|
cl = pe.Constraint(expr=expr, name=name)
|
||||||
|
self.model.add_component(name, cl)
|
||||||
|
self._pyomo_solver.add_constraint(cl)
|
||||||
|
self._cname_to_constr[name] = cl
|
||||||
|
self._termination_condition = ""
|
||||||
|
self._has_lp_solution = False
|
||||||
|
self._has_mip_solution = False
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def are_callbacks_supported(self) -> bool:
|
def are_callbacks_supported(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def are_constraints_satisfied(
|
||||||
|
self,
|
||||||
|
cf: ConstraintFeatures,
|
||||||
|
tol: float = 1e-5,
|
||||||
|
) -> List[bool]:
|
||||||
|
assert cf.names is not None
|
||||||
|
assert cf.lhs is not None
|
||||||
|
assert cf.rhs is not None
|
||||||
|
assert cf.senses is not None
|
||||||
|
result = []
|
||||||
|
for (i, name) in enumerate(cf.names):
|
||||||
|
lhs = 0.0
|
||||||
|
for (varname, coeff) in cf.lhs[i]:
|
||||||
|
var = self._varname_to_var[varname]
|
||||||
|
lhs += var.value * coeff
|
||||||
|
if cf.senses[i] == "<":
|
||||||
|
result.append(lhs <= cf.rhs[i] + tol)
|
||||||
|
elif cf.senses[i] == ">":
|
||||||
|
result.append(lhs >= cf.rhs[i] - tol)
|
||||||
|
else:
|
||||||
|
result.append(abs(cf.rhs[i] - lhs) < tol)
|
||||||
|
return result
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def build_test_instance_infeasible(self) -> Instance:
|
def build_test_instance_infeasible(self) -> Instance:
|
||||||
return PyomoTestInstanceInfeasible()
|
return PyomoTestInstanceInfeasible()
|
||||||
@@ -413,6 +463,15 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
self.model.del_component(constr)
|
self.model.del_component(constr)
|
||||||
self._pyomo_solver.remove_constraint(constr)
|
self._pyomo_solver.remove_constraint(constr)
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def remove_constraints(self, names: List[str]) -> None:
|
||||||
|
assert self.model is not None
|
||||||
|
for name in names:
|
||||||
|
constr = self._cname_to_constr[name]
|
||||||
|
del self._cname_to_constr[name]
|
||||||
|
self.model.del_component(constr)
|
||||||
|
self._pyomo_solver.remove_constraint(constr)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def set_instance(
|
def set_instance(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -183,42 +183,54 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
|||||||
|
|
||||||
# Fetch constraints (after-mip)
|
# Fetch constraints (after-mip)
|
||||||
assert_equals(
|
assert_equals(
|
||||||
_round_constraints(solver.get_constraints_old(with_static=False)),
|
_round(solver.get_constraints(with_static=False)),
|
||||||
{"eq_capacity": Constraint(slack=0.0)},
|
_filter_attrs(
|
||||||
|
solver.get_constraint_attrs(),
|
||||||
|
ConstraintFeatures(
|
||||||
|
names=("eq_capacity",),
|
||||||
|
slacks=(0.0,),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build a new constraint
|
# Build new constraint and verify that it is violated
|
||||||
cut = Constraint(lhs={"x[0]": 1.0}, sense="<", rhs=0.0)
|
cf = ConstraintFeatures(
|
||||||
assert not solver.is_constraint_satisfied_old(cut)
|
names=("cut",),
|
||||||
|
lhs=((("x[0]", 1.0),),),
|
||||||
|
rhs=(0.0,),
|
||||||
|
senses=("<",),
|
||||||
|
)
|
||||||
|
assert_equals(solver.are_constraints_satisfied(cf), [False])
|
||||||
|
|
||||||
# Add new constraint and verify that it is listed. Modifying the model should
|
# Add constraint and verify it affects solution
|
||||||
# also clear the current solution.
|
solver.add_constraints(cf)
|
||||||
solver.add_constraint(cut, "cut")
|
|
||||||
assert_equals(
|
assert_equals(
|
||||||
_round_constraints(solver.get_constraints_old()),
|
_round(solver.get_constraints(with_static=True)),
|
||||||
{
|
_filter_attrs(
|
||||||
"eq_capacity": Constraint(
|
solver.get_constraint_attrs(),
|
||||||
lazy=False,
|
ConstraintFeatures(
|
||||||
lhs={"x[0]": 23.0, "x[1]": 26.0, "x[2]": 20.0, "x[3]": 18.0, "z": -1.0},
|
names=("eq_capacity", "cut"),
|
||||||
rhs=0.0,
|
rhs=(0.0, 0.0),
|
||||||
sense="=",
|
lhs=(
|
||||||
|
(
|
||||||
|
("x[0]", 23.0),
|
||||||
|
("x[1]", 26.0),
|
||||||
|
("x[2]", 20.0),
|
||||||
|
("x[3]", 18.0),
|
||||||
|
("z", -1.0),
|
||||||
|
),
|
||||||
|
(("x[0]", 1.0),),
|
||||||
|
),
|
||||||
|
senses=("=", "<"),
|
||||||
),
|
),
|
||||||
"cut": Constraint(
|
|
||||||
lazy=False,
|
|
||||||
lhs={"x[0]": 1.0},
|
|
||||||
rhs=0.0,
|
|
||||||
sense="<",
|
|
||||||
),
|
),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Re-solve MIP and verify that constraint affects the solution
|
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
assert_equals(stats.mip_lower_bound, 1030.0)
|
assert_equals(stats.mip_lower_bound, 1030.0)
|
||||||
assert solver.is_constraint_satisfied_old(cut)
|
assert_equals(solver.are_constraints_satisfied(cf), [True])
|
||||||
|
|
||||||
# Remove the new constraint
|
# Remove the new constraint
|
||||||
solver.remove_constraint("cut")
|
solver.remove_constraints(["cut"])
|
||||||
|
|
||||||
# New constraint should no longer affect solution
|
# New constraint should no longer affect solution
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
|
|||||||
Reference in New Issue
Block a user