mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 01:18:52 -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...")
|
||||
enforced: Dict[str, Constraint] = {}
|
||||
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,
|
||||
tol=self.violation_tolerance,
|
||||
):
|
||||
|
||||
@@ -72,12 +72,16 @@ class GurobiSolver(InternalSolver):
|
||||
self._has_mip_solution = False
|
||||
|
||||
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_constrs: Tuple["gurobipy.Constr", ...] = tuple()
|
||||
self._var_names: Tuple[str, ...] = tuple()
|
||||
self._constr_names: Tuple[str, ...] = tuple()
|
||||
self._var_types: Tuple[str, ...] = tuple()
|
||||
self._var_lbs: Tuple[float, ...] = tuple()
|
||||
self._var_ubs: 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:
|
||||
self.lazy_cb_where = [self.gp.GRB.Callback.MIPSOL]
|
||||
@@ -134,6 +138,16 @@ class GurobiSolver(InternalSolver):
|
||||
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
|
||||
def fix(self, solution: Solution) -> None:
|
||||
self._raise_if_callback()
|
||||
@@ -406,8 +420,30 @@ class GurobiSolver(InternalSolver):
|
||||
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
|
||||
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
|
||||
lhs = 0.0
|
||||
for (varname, coeff) in constr.lhs.items():
|
||||
@@ -425,6 +461,14 @@ class GurobiSolver(InternalSolver):
|
||||
assert self.model is not None
|
||||
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
|
||||
def remove_constraint(self, name: str) -> None:
|
||||
assert self.model is not None
|
||||
@@ -444,7 +488,7 @@ class GurobiSolver(InternalSolver):
|
||||
self.instance = instance
|
||||
self.model = model
|
||||
self.model.update()
|
||||
self._update_vars()
|
||||
self._update()
|
||||
|
||||
@overrides
|
||||
def set_warm_start(self, solution: Solution) -> None:
|
||||
@@ -571,7 +615,7 @@ class GurobiSolver(InternalSolver):
|
||||
assert self.model is not None
|
||||
self.model.update()
|
||||
self.model = self.model.relax()
|
||||
self._update_vars()
|
||||
self._update()
|
||||
|
||||
def _apply_params(self, streams: List[Any]) -> None:
|
||||
assert self.model is not None
|
||||
@@ -620,19 +664,22 @@ class GurobiSolver(InternalSolver):
|
||||
if self.cb_where is not None:
|
||||
raise Exception("method cannot be called from a callback")
|
||||
|
||||
def _update_vars(self) -> None:
|
||||
def _update(self) -> None:
|
||||
assert self.model is not None
|
||||
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_types: List[str] = self.model.getAttr("vtype", gp_vars)
|
||||
var_ubs: List[float] = self.model.getAttr("ub", gp_vars)
|
||||
var_lbs: List[float] = self.model.getAttr("lb", 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 = {}
|
||||
cname_to_constr: Dict = {}
|
||||
for (i, gp_var) in enumerate(gp_vars):
|
||||
assert var_names[i] not in varname_to_var, (
|
||||
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":
|
||||
assert var_ubs[i] == 1.0, (
|
||||
@@ -649,9 +696,18 @@ class GurobiSolver(InternalSolver):
|
||||
"Variable {var.varName} has type {vtype}."
|
||||
)
|
||||
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._cname_to_constr = cname_to_constr
|
||||
self._gp_vars = tuple(gp_vars)
|
||||
self._gp_constrs = tuple(gp_constrs)
|
||||
self._var_names = tuple(var_names)
|
||||
self._constr_names = constr_names
|
||||
self._var_types = tuple(var_types)
|
||||
self._var_lbs = tuple(var_lbs)
|
||||
self._var_ubs = tuple(var_ubs)
|
||||
@@ -692,8 +748,8 @@ class GurobiTestInstanceRedundancy(Instance):
|
||||
|
||||
model = gp.Model()
|
||||
x = model.addVars(2, vtype=GRB.BINARY, name="x")
|
||||
model.addConstr(x[0] + x[1] <= 1)
|
||||
model.addConstr(x[0] + x[1] <= 2)
|
||||
model.addConstr(x[0] + x[1] <= 1, name="c1")
|
||||
model.addConstr(x[0] + x[1] <= 2, name="c2")
|
||||
model.setObjective(x[0] + x[1], GRB.MAXIMIZE)
|
||||
return model
|
||||
|
||||
|
||||
@@ -196,7 +196,9 @@ class InternalSolver(ABC, EnforceOverrides):
|
||||
pass
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
||||
@@ -386,7 +386,9 @@ class BasePyomoSolver(InternalSolver):
|
||||
]
|
||||
|
||||
@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
|
||||
assert constr.lhs is not None
|
||||
for (varname, coeff) in constr.lhs.items():
|
||||
|
||||
@@ -189,7 +189,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
||||
|
||||
# Build a new constraint
|
||||
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
|
||||
# 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
|
||||
stats = solver.solve()
|
||||
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
|
||||
solver.remove_constraint("cut")
|
||||
|
||||
Reference in New Issue
Block a user