mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
GurobiSolver: Implement relax/enforce constraint
This commit is contained in:
@@ -163,7 +163,7 @@ class StaticLazyConstraintsComponent(Component):
|
|||||||
logger.info("Finding violated lazy constraints...")
|
logger.info("Finding violated lazy constraints...")
|
||||||
enforced: Dict[str, Constraint] = {}
|
enforced: Dict[str, Constraint] = {}
|
||||||
for (cid, c) in self.pool.items():
|
for (cid, c) in self.pool.items():
|
||||||
if not solver.internal_solver.is_constraint_satisfied(
|
if not solver.internal_solver.is_constraint_satisfied_old(
|
||||||
c,
|
c,
|
||||||
tol=self.violation_tolerance,
|
tol=self.violation_tolerance,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -72,12 +72,16 @@ class GurobiSolver(InternalSolver):
|
|||||||
self._has_mip_solution = False
|
self._has_mip_solution = False
|
||||||
|
|
||||||
self._varname_to_var: Dict[str, "gurobipy.Var"] = {}
|
self._varname_to_var: Dict[str, "gurobipy.Var"] = {}
|
||||||
|
self._cname_to_constr: Dict[str, "gurobipy.Constr"] = {}
|
||||||
self._gp_vars: Tuple["gurobipy.Var", ...] = tuple()
|
self._gp_vars: Tuple["gurobipy.Var", ...] = tuple()
|
||||||
|
self._gp_constrs: Tuple["gurobipy.Constr", ...] = tuple()
|
||||||
self._var_names: Tuple[str, ...] = tuple()
|
self._var_names: Tuple[str, ...] = tuple()
|
||||||
|
self._constr_names: Tuple[str, ...] = tuple()
|
||||||
self._var_types: Tuple[str, ...] = tuple()
|
self._var_types: Tuple[str, ...] = tuple()
|
||||||
self._var_lbs: Tuple[float, ...] = tuple()
|
self._var_lbs: Tuple[float, ...] = tuple()
|
||||||
self._var_ubs: Tuple[float, ...] = tuple()
|
self._var_ubs: Tuple[float, ...] = tuple()
|
||||||
self._var_obj_coeffs: Tuple[float, ...] = tuple()
|
self._var_obj_coeffs: Tuple[float, ...] = tuple()
|
||||||
|
self._relaxed_constrs: Dict[str, Tuple["gurobipy.LinExpr", str, float]] = {}
|
||||||
|
|
||||||
if self.lazy_cb_frequency == 1:
|
if self.lazy_cb_frequency == 1:
|
||||||
self.lazy_cb_where = [self.gp.GRB.Callback.MIPSOL]
|
self.lazy_cb_where = [self.gp.GRB.Callback.MIPSOL]
|
||||||
@@ -134,6 +138,16 @@ class GurobiSolver(InternalSolver):
|
|||||||
lazy_cb_frequency=self.lazy_cb_frequency,
|
lazy_cb_frequency=self.lazy_cb_frequency,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def enforce_constraints(self, names: List[str]) -> None:
|
||||||
|
constr = [self._relaxed_constrs[n] for n in names]
|
||||||
|
for (i, (lhs, sense, rhs)) in enumerate(constr):
|
||||||
|
if sense == "=":
|
||||||
|
self.model.addConstr(lhs == rhs, name=names[i])
|
||||||
|
elif sense == "<":
|
||||||
|
self.model.addConstr(lhs <= rhs, name=names[i])
|
||||||
|
else:
|
||||||
|
self.model.addConstr(lhs >= rhs, name=names[i])
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def fix(self, solution: Solution) -> None:
|
def fix(self, solution: Solution) -> None:
|
||||||
self._raise_if_callback()
|
self._raise_if_callback()
|
||||||
@@ -406,8 +420,30 @@ class GurobiSolver(InternalSolver):
|
|||||||
values=values,
|
values=values,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_constraint_satisfied(
|
||||||
|
self,
|
||||||
|
names: List[str],
|
||||||
|
tol: float = 1e-6,
|
||||||
|
) -> List[bool]:
|
||||||
|
def _check(c):
|
||||||
|
lhs, sense, rhs = c
|
||||||
|
lhs_value = lhs.getValue()
|
||||||
|
if sense == "=":
|
||||||
|
return abs(lhs_value - rhs) < tol
|
||||||
|
elif sense == ">":
|
||||||
|
return lhs_value > rhs - tol
|
||||||
|
else:
|
||||||
|
return lhs_value < rhs - tol
|
||||||
|
|
||||||
|
constrs = [self._relaxed_constrs[n] for n in names]
|
||||||
|
return list(map(_check, constrs))
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def is_constraint_satisfied(self, constr: Constraint, tol: float = 1e-6) -> bool:
|
def is_constraint_satisfied_old(
|
||||||
|
self,
|
||||||
|
constr: Constraint,
|
||||||
|
tol: float = 1e-6,
|
||||||
|
) -> bool:
|
||||||
assert constr.lhs is not None
|
assert constr.lhs is not None
|
||||||
lhs = 0.0
|
lhs = 0.0
|
||||||
for (varname, coeff) in constr.lhs.items():
|
for (varname, coeff) in constr.lhs.items():
|
||||||
@@ -425,6 +461,14 @@ class GurobiSolver(InternalSolver):
|
|||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
return self.model.status in [self.gp.GRB.INFEASIBLE, self.gp.GRB.INF_OR_UNBD]
|
return self.model.status in [self.gp.GRB.INFEASIBLE, self.gp.GRB.INF_OR_UNBD]
|
||||||
|
|
||||||
|
def relax_constraints(self, names: List[str]) -> None:
|
||||||
|
constrs = [self._cname_to_constr[n] for n in names]
|
||||||
|
for (i, name) in enumerate(names):
|
||||||
|
c = constrs[i]
|
||||||
|
self._relaxed_constrs[name] = self.model.getRow(c), c.sense, c.rhs
|
||||||
|
self.model.remove(constrs)
|
||||||
|
self.model.update()
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def remove_constraint(self, name: str) -> None:
|
def remove_constraint(self, name: str) -> None:
|
||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
@@ -444,7 +488,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.model = model
|
self.model = model
|
||||||
self.model.update()
|
self.model.update()
|
||||||
self._update_vars()
|
self._update()
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def set_warm_start(self, solution: Solution) -> None:
|
def set_warm_start(self, solution: Solution) -> None:
|
||||||
@@ -571,7 +615,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
self.model.update()
|
self.model.update()
|
||||||
self.model = self.model.relax()
|
self.model = self.model.relax()
|
||||||
self._update_vars()
|
self._update()
|
||||||
|
|
||||||
def _apply_params(self, streams: List[Any]) -> None:
|
def _apply_params(self, streams: List[Any]) -> None:
|
||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
@@ -620,19 +664,22 @@ class GurobiSolver(InternalSolver):
|
|||||||
if self.cb_where is not None:
|
if self.cb_where is not None:
|
||||||
raise Exception("method cannot be called from a callback")
|
raise Exception("method cannot be called from a callback")
|
||||||
|
|
||||||
def _update_vars(self) -> None:
|
def _update(self) -> None:
|
||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
gp_vars: List["gurobipy.Var"] = self.model.getVars()
|
gp_vars: List["gurobipy.Var"] = self.model.getVars()
|
||||||
|
gp_constrs: List["gurobipy.Constr"] = self.model.getConstrs()
|
||||||
var_names: List[str] = self.model.getAttr("varName", gp_vars)
|
var_names: List[str] = self.model.getAttr("varName", gp_vars)
|
||||||
var_types: List[str] = self.model.getAttr("vtype", gp_vars)
|
var_types: List[str] = self.model.getAttr("vtype", gp_vars)
|
||||||
var_ubs: List[float] = self.model.getAttr("ub", gp_vars)
|
var_ubs: List[float] = self.model.getAttr("ub", gp_vars)
|
||||||
var_lbs: List[float] = self.model.getAttr("lb", gp_vars)
|
var_lbs: List[float] = self.model.getAttr("lb", gp_vars)
|
||||||
var_obj_coeffs: List[float] = self.model.getAttr("obj", gp_vars)
|
var_obj_coeffs: List[float] = self.model.getAttr("obj", gp_vars)
|
||||||
|
constr_names: List[str] = self.model.getAttr("constrName", gp_constrs)
|
||||||
varname_to_var: Dict = {}
|
varname_to_var: Dict = {}
|
||||||
|
cname_to_constr: Dict = {}
|
||||||
for (i, gp_var) in enumerate(gp_vars):
|
for (i, gp_var) in enumerate(gp_vars):
|
||||||
assert var_names[i] not in varname_to_var, (
|
assert var_names[i] not in varname_to_var, (
|
||||||
f"Duplicated variable name detected: {var_names[i]}. "
|
f"Duplicated variable name detected: {var_names[i]}. "
|
||||||
f"Unique variable var_names are currently required."
|
f"Unique variable names are currently required."
|
||||||
)
|
)
|
||||||
if var_types[i] == "I":
|
if var_types[i] == "I":
|
||||||
assert var_ubs[i] == 1.0, (
|
assert var_ubs[i] == 1.0, (
|
||||||
@@ -649,9 +696,18 @@ class GurobiSolver(InternalSolver):
|
|||||||
"Variable {var.varName} has type {vtype}."
|
"Variable {var.varName} has type {vtype}."
|
||||||
)
|
)
|
||||||
varname_to_var[var_names[i]] = gp_var
|
varname_to_var[var_names[i]] = gp_var
|
||||||
|
for (i, gp_constr) in enumerate(gp_constrs):
|
||||||
|
assert constr_names[i] not in cname_to_constr, (
|
||||||
|
f"Duplicated constraint name detected: {constr_names[i]}. "
|
||||||
|
f"Unique constraint names are currently required."
|
||||||
|
)
|
||||||
|
cname_to_constr[constr_names[i]] = gp_constr
|
||||||
self._varname_to_var = varname_to_var
|
self._varname_to_var = varname_to_var
|
||||||
|
self._cname_to_constr = cname_to_constr
|
||||||
self._gp_vars = tuple(gp_vars)
|
self._gp_vars = tuple(gp_vars)
|
||||||
|
self._gp_constrs = tuple(gp_constrs)
|
||||||
self._var_names = tuple(var_names)
|
self._var_names = tuple(var_names)
|
||||||
|
self._constr_names = constr_names
|
||||||
self._var_types = tuple(var_types)
|
self._var_types = tuple(var_types)
|
||||||
self._var_lbs = tuple(var_lbs)
|
self._var_lbs = tuple(var_lbs)
|
||||||
self._var_ubs = tuple(var_ubs)
|
self._var_ubs = tuple(var_ubs)
|
||||||
@@ -692,8 +748,8 @@ class GurobiTestInstanceRedundancy(Instance):
|
|||||||
|
|
||||||
model = gp.Model()
|
model = gp.Model()
|
||||||
x = model.addVars(2, vtype=GRB.BINARY, name="x")
|
x = model.addVars(2, vtype=GRB.BINARY, name="x")
|
||||||
model.addConstr(x[0] + x[1] <= 1)
|
model.addConstr(x[0] + x[1] <= 1, name="c1")
|
||||||
model.addConstr(x[0] + x[1] <= 2)
|
model.addConstr(x[0] + x[1] <= 2, name="c2")
|
||||||
model.setObjective(x[0] + x[1], GRB.MAXIMIZE)
|
model.setObjective(x[0] + x[1], GRB.MAXIMIZE)
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,9 @@ class InternalSolver(ABC, EnforceOverrides):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_constraint_satisfied(self, constr: Constraint, tol: float = 1e-6) -> bool:
|
def is_constraint_satisfied_old(
|
||||||
|
self, constr: Constraint, tol: float = 1e-6
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if the current solution satisfies the given constraint.
|
Returns True if the current solution satisfies the given constraint.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -386,7 +386,9 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def is_constraint_satisfied(self, constr: Constraint, tol: float = 1e-6) -> bool:
|
def is_constraint_satisfied_old(
|
||||||
|
self, constr: Constraint, tol: float = 1e-6
|
||||||
|
) -> bool:
|
||||||
lhs = 0.0
|
lhs = 0.0
|
||||||
assert constr.lhs is not None
|
assert constr.lhs is not None
|
||||||
for (varname, coeff) in constr.lhs.items():
|
for (varname, coeff) in constr.lhs.items():
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
|||||||
|
|
||||||
# Build a new constraint
|
# Build a new constraint
|
||||||
cut = Constraint(lhs={"x[0]": 1.0}, sense="<", rhs=0.0)
|
cut = Constraint(lhs={"x[0]": 1.0}, sense="<", rhs=0.0)
|
||||||
assert not solver.is_constraint_satisfied(cut)
|
assert not solver.is_constraint_satisfied_old(cut)
|
||||||
|
|
||||||
# Add new constraint and verify that it is listed. Modifying the model should
|
# Add new constraint and verify that it is listed. Modifying the model should
|
||||||
# also clear the current solution.
|
# also clear the current solution.
|
||||||
@@ -215,7 +215,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
|||||||
# Re-solve MIP and verify that constraint affects the solution
|
# 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(cut)
|
assert solver.is_constraint_satisfied_old(cut)
|
||||||
|
|
||||||
# Remove the new constraint
|
# Remove the new constraint
|
||||||
solver.remove_constraint("cut")
|
solver.remove_constraint("cut")
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ def test_usage_with_solver(instance: Instance) -> None:
|
|||||||
solver.gap_tolerance = 1e-4
|
solver.gap_tolerance = 1e-4
|
||||||
|
|
||||||
internal = solver.internal_solver = Mock(spec=InternalSolver)
|
internal = solver.internal_solver = Mock(spec=InternalSolver)
|
||||||
internal.is_constraint_satisfied = Mock(return_value=False)
|
internal.is_constraint_satisfied_old = Mock(return_value=False)
|
||||||
|
|
||||||
component = StaticLazyConstraintsComponent(violation_tolerance=1.0)
|
component = StaticLazyConstraintsComponent(violation_tolerance=1.0)
|
||||||
component.thresholds["type-a"] = MinProbabilityThreshold([0.5, 0.5])
|
component.thresholds["type-a"] = MinProbabilityThreshold([0.5, 0.5])
|
||||||
@@ -144,8 +144,8 @@ def test_usage_with_solver(instance: Instance) -> None:
|
|||||||
# Should ask internal solver to verify if constraints in the pool are
|
# Should ask internal solver to verify if constraints in the pool are
|
||||||
# satisfied and add the ones that are not
|
# satisfied and add the ones that are not
|
||||||
c3 = sample.after_load.constraints_old["c3"]
|
c3 = sample.after_load.constraints_old["c3"]
|
||||||
internal.is_constraint_satisfied.assert_called_once_with(c3, tol=1.0)
|
internal.is_constraint_satisfied_old.assert_called_once_with(c3, tol=1.0)
|
||||||
internal.is_constraint_satisfied.reset_mock()
|
internal.is_constraint_satisfied_old.reset_mock()
|
||||||
internal.add_constraint.assert_called_once_with(c3, name="c3")
|
internal.add_constraint.assert_called_once_with(c3, name="c3")
|
||||||
internal.add_constraint.reset_mock()
|
internal.add_constraint.reset_mock()
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ def test_usage_with_solver(instance: Instance) -> None:
|
|||||||
assert not should_repeat
|
assert not should_repeat
|
||||||
|
|
||||||
# The lazy constraint pool should be empty by now, so no calls should be made
|
# The lazy constraint pool should be empty by now, so no calls should be made
|
||||||
internal.is_constraint_satisfied.assert_not_called()
|
internal.is_constraint_satisfied_old.assert_not_called()
|
||||||
internal.add_constraint.assert_not_called()
|
internal.add_constraint.assert_not_called()
|
||||||
|
|
||||||
# LearningSolver calls after_solve_mip
|
# LearningSolver calls after_solve_mip
|
||||||
|
|||||||
@@ -35,3 +35,23 @@ def test_gurobi_pyomo_solver() -> None:
|
|||||||
|
|
||||||
def test_gurobi_solver() -> None:
|
def test_gurobi_solver() -> None:
|
||||||
run_internal_solver_tests(GurobiSolver())
|
run_internal_solver_tests(GurobiSolver())
|
||||||
|
|
||||||
|
|
||||||
|
def test_redundancy() -> None:
|
||||||
|
solver = GurobiSolver()
|
||||||
|
instance = solver.build_test_instance_redundancy()
|
||||||
|
solver.set_instance(instance)
|
||||||
|
stats = solver.solve_lp()
|
||||||
|
assert stats.lp_value == 1.0
|
||||||
|
constraints = solver.get_constraints()
|
||||||
|
assert constraints.names[0] == "c1"
|
||||||
|
assert constraints.slacks[0] == 0.0
|
||||||
|
|
||||||
|
solver.relax_constraints(["c1"])
|
||||||
|
stats = solver.solve_lp()
|
||||||
|
assert stats.lp_value == 2.0
|
||||||
|
assert solver.is_constraint_satisfied(["c1"]) == [False]
|
||||||
|
|
||||||
|
solver.enforce_constraints(["c1"])
|
||||||
|
stats = solver.solve_lp()
|
||||||
|
assert stats.lp_value == 1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user