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

Loading…
Cancel
Save