GurobiSolver: Implement relax/enforce constraint

master
Alinson S. Xavier 5 years ago
parent 4dd4ef52bd
commit 0ba8cc16fd
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -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")

@ -86,7 +86,7 @@ def test_usage_with_solver(instance: Instance) -> None:
solver.gap_tolerance = 1e-4
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.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
# satisfied and add the ones that are not
c3 = sample.after_load.constraints_old["c3"]
internal.is_constraint_satisfied.assert_called_once_with(c3, tol=1.0)
internal.is_constraint_satisfied.reset_mock()
internal.is_constraint_satisfied_old.assert_called_once_with(c3, tol=1.0)
internal.is_constraint_satisfied_old.reset_mock()
internal.add_constraint.assert_called_once_with(c3, name="c3")
internal.add_constraint.reset_mock()
@ -154,7 +154,7 @@ def test_usage_with_solver(instance: Instance) -> None:
assert not should_repeat
# 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()
# LearningSolver calls after_solve_mip

@ -35,3 +35,23 @@ def test_gurobi_pyomo_solver() -> None:
def test_gurobi_solver() -> None:
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

Loading…
Cancel
Save