|
|
@ -81,6 +81,191 @@ class GurobiSolver(InternalSolver):
|
|
|
|
self.gp.GRB.Callback.MIPNODE,
|
|
|
|
self.gp.GRB.Callback.MIPNODE,
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def add_constraint(self, constr: Constraint, name: str) -> None:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
lhs = self.gp.quicksum(
|
|
|
|
|
|
|
|
self._varname_to_var[varname] * coeff
|
|
|
|
|
|
|
|
for (varname, coeff) in constr.lhs.items()
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if constr.sense == "=":
|
|
|
|
|
|
|
|
self.model.addConstr(lhs == constr.rhs, name=name)
|
|
|
|
|
|
|
|
elif constr.sense == "<":
|
|
|
|
|
|
|
|
self.model.addConstr(lhs <= constr.rhs, name=name)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self.model.addConstr(lhs >= constr.rhs, name=name)
|
|
|
|
|
|
|
|
self._dirty = True
|
|
|
|
|
|
|
|
self._has_lp_solution = False
|
|
|
|
|
|
|
|
self._has_mip_solution = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def are_callbacks_supported(self) -> bool:
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def build_test_instance_infeasible(self) -> Instance:
|
|
|
|
|
|
|
|
return GurobiTestInstanceInfeasible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def build_test_instance_knapsack(self) -> Instance:
|
|
|
|
|
|
|
|
return GurobiTestInstanceKnapsack(
|
|
|
|
|
|
|
|
weights=[23.0, 26.0, 20.0, 18.0],
|
|
|
|
|
|
|
|
prices=[505.0, 352.0, 458.0, 220.0],
|
|
|
|
|
|
|
|
capacity=67.0,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def build_test_instance_redundancy(self) -> Instance:
|
|
|
|
|
|
|
|
return GurobiTestInstanceRedundancy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def clone(self) -> "GurobiSolver":
|
|
|
|
|
|
|
|
return GurobiSolver(
|
|
|
|
|
|
|
|
params=self.params,
|
|
|
|
|
|
|
|
lazy_cb_frequency=self.lazy_cb_frequency,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def fix(self, solution: Solution) -> None:
|
|
|
|
|
|
|
|
self._raise_if_callback()
|
|
|
|
|
|
|
|
for (varname, value) in solution.items():
|
|
|
|
|
|
|
|
if value is None:
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
var = self._varname_to_var[varname]
|
|
|
|
|
|
|
|
var.vtype = self.gp.GRB.CONTINUOUS
|
|
|
|
|
|
|
|
var.lb = value
|
|
|
|
|
|
|
|
var.ub = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_dual(self, cid: str) -> float:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
c = self.model.getConstrByName(cid)
|
|
|
|
|
|
|
|
if self.is_infeasible():
|
|
|
|
|
|
|
|
return c.farkasDual
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
return c.pi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_constraint_attrs(self) -> List[str]:
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
|
|
"basis_status",
|
|
|
|
|
|
|
|
"category",
|
|
|
|
|
|
|
|
"dual_value",
|
|
|
|
|
|
|
|
"lazy",
|
|
|
|
|
|
|
|
"lhs",
|
|
|
|
|
|
|
|
"rhs",
|
|
|
|
|
|
|
|
"sa_rhs_down",
|
|
|
|
|
|
|
|
"sa_rhs_up",
|
|
|
|
|
|
|
|
"sense",
|
|
|
|
|
|
|
|
"slack",
|
|
|
|
|
|
|
|
"user_features",
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_constraints(self) -> Dict[str, Constraint]:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
self._raise_if_callback()
|
|
|
|
|
|
|
|
if self._dirty:
|
|
|
|
|
|
|
|
self.model.update()
|
|
|
|
|
|
|
|
self._dirty = False
|
|
|
|
|
|
|
|
constraints: Dict[str, Constraint] = {}
|
|
|
|
|
|
|
|
for c in self.model.getConstrs():
|
|
|
|
|
|
|
|
constr = self._parse_gurobi_constraint(c)
|
|
|
|
|
|
|
|
assert c.constrName not in constraints
|
|
|
|
|
|
|
|
constraints[c.constrName] = constr
|
|
|
|
|
|
|
|
return constraints
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_sense(self) -> str:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
if self.model.modelSense == 1:
|
|
|
|
|
|
|
|
return "min"
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
return "max"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_solution(self) -> Optional[Solution]:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
if self.cb_where is not None:
|
|
|
|
|
|
|
|
if self.cb_where == self.gp.GRB.Callback.MIPNODE:
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
v.varName: self.model.cbGetNodeRel(v) for v in self.model.getVars()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
elif self.cb_where == self.gp.GRB.Callback.MIPSOL:
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
v.varName: self.model.cbGetSolution(v) for v in self.model.getVars()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
raise Exception(
|
|
|
|
|
|
|
|
f"get_solution can only be called from a callback "
|
|
|
|
|
|
|
|
f"when cb_where is either MIPNODE or MIPSOL"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if self.model.solCount == 0:
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
return {v.varName: v.x for v in self.model.getVars()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_variable_attrs(self) -> List[str]:
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
|
|
"basis_status",
|
|
|
|
|
|
|
|
"category",
|
|
|
|
|
|
|
|
"lower_bound",
|
|
|
|
|
|
|
|
"obj_coeff",
|
|
|
|
|
|
|
|
"reduced_cost",
|
|
|
|
|
|
|
|
"sa_lb_down",
|
|
|
|
|
|
|
|
"sa_lb_up",
|
|
|
|
|
|
|
|
"sa_obj_down",
|
|
|
|
|
|
|
|
"sa_obj_up",
|
|
|
|
|
|
|
|
"sa_ub_down",
|
|
|
|
|
|
|
|
"sa_ub_up",
|
|
|
|
|
|
|
|
"type",
|
|
|
|
|
|
|
|
"upper_bound",
|
|
|
|
|
|
|
|
"user_features",
|
|
|
|
|
|
|
|
"value",
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_variable_names(self) -> List[VariableName]:
|
|
|
|
|
|
|
|
self._raise_if_callback()
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
return [v.varName for v in self.model.getVars()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_variables(self) -> Dict[str, Variable]:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
variables = {}
|
|
|
|
|
|
|
|
for gp_var in self.model.getVars():
|
|
|
|
|
|
|
|
name = gp_var.varName
|
|
|
|
|
|
|
|
assert len(name) > 0, f"empty variable name detected"
|
|
|
|
|
|
|
|
assert name not in variables, f"duplicated variable name detected: {name}"
|
|
|
|
|
|
|
|
var = self._parse_gurobi_var(gp_var)
|
|
|
|
|
|
|
|
variables[name] = var
|
|
|
|
|
|
|
|
return variables
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def is_constraint_satisfied(self, constr: Constraint, tol: float = 1e-6) -> bool:
|
|
|
|
|
|
|
|
lhs = 0.0
|
|
|
|
|
|
|
|
for (varname, coeff) in constr.lhs.items():
|
|
|
|
|
|
|
|
var = self._varname_to_var[varname]
|
|
|
|
|
|
|
|
lhs += self._get_value(var) * coeff
|
|
|
|
|
|
|
|
if constr.sense == "<":
|
|
|
|
|
|
|
|
return lhs <= constr.rhs + tol
|
|
|
|
|
|
|
|
elif constr.sense == ">":
|
|
|
|
|
|
|
|
return lhs >= constr.rhs - tol
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
return abs(constr.rhs - lhs) < abs(tol)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def is_infeasible(self) -> bool:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
return self.model.status in [self.gp.GRB.INFEASIBLE, self.gp.GRB.INF_OR_UNBD]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def remove_constraint(self, name: str) -> None:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
constr = self.model.getConstrByName(name)
|
|
|
|
|
|
|
|
self.model.remove(constr)
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
@overrides
|
|
|
|
def set_instance(
|
|
|
|
def set_instance(
|
|
|
|
self,
|
|
|
|
self,
|
|
|
@ -96,66 +281,14 @@ class GurobiSolver(InternalSolver):
|
|
|
|
self.model.update()
|
|
|
|
self.model.update()
|
|
|
|
self._update_vars()
|
|
|
|
self._update_vars()
|
|
|
|
|
|
|
|
|
|
|
|
def _raise_if_callback(self) -> None:
|
|
|
|
|
|
|
|
if self.cb_where is not None:
|
|
|
|
|
|
|
|
raise Exception("method cannot be called from a callback")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _update_vars(self) -> None:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
self._varname_to_var.clear()
|
|
|
|
|
|
|
|
self._original_vtype = {}
|
|
|
|
|
|
|
|
self._bin_vars.clear()
|
|
|
|
|
|
|
|
for var in self.model.getVars():
|
|
|
|
|
|
|
|
assert var.varName not in self._varname_to_var, (
|
|
|
|
|
|
|
|
f"Duplicated variable name detected: {var.varName}. "
|
|
|
|
|
|
|
|
f"Unique variable names are currently required."
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self._varname_to_var[var.varName] = var
|
|
|
|
|
|
|
|
assert var.vtype in ["B", "C"], (
|
|
|
|
|
|
|
|
"Only binary and continuous variables are currently supported. "
|
|
|
|
|
|
|
|
"Variable {var.varName} has type {var.vtype}."
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self._original_vtype[var] = var.vtype
|
|
|
|
|
|
|
|
if var.vtype == "B":
|
|
|
|
|
|
|
|
self._bin_vars.append(var)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_params(self, streams: List[Any]) -> None:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
with _RedirectOutput(streams):
|
|
|
|
|
|
|
|
for (name, value) in self.params.items():
|
|
|
|
|
|
|
|
self.model.setParam(name, value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
@overrides
|
|
|
|
def solve_lp(
|
|
|
|
def set_warm_start(self, solution: Solution) -> None:
|
|
|
|
self,
|
|
|
|
|
|
|
|
tee: bool = False,
|
|
|
|
|
|
|
|
) -> LPSolveStats:
|
|
|
|
|
|
|
|
self._raise_if_callback()
|
|
|
|
self._raise_if_callback()
|
|
|
|
streams: List[Any] = [StringIO()]
|
|
|
|
self._clear_warm_start()
|
|
|
|
if tee:
|
|
|
|
for (var_name, value) in solution.items():
|
|
|
|
streams += [sys.stdout]
|
|
|
|
var = self._varname_to_var[var_name]
|
|
|
|
self._apply_params(streams)
|
|
|
|
if value is not None:
|
|
|
|
assert self.model is not None
|
|
|
|
var.start = value
|
|
|
|
for var in self._bin_vars:
|
|
|
|
|
|
|
|
var.vtype = self.gp.GRB.CONTINUOUS
|
|
|
|
|
|
|
|
var.lb = 0.0
|
|
|
|
|
|
|
|
var.ub = 1.0
|
|
|
|
|
|
|
|
with _RedirectOutput(streams):
|
|
|
|
|
|
|
|
self.model.optimize()
|
|
|
|
|
|
|
|
self._dirty = False
|
|
|
|
|
|
|
|
for var in self._bin_vars:
|
|
|
|
|
|
|
|
var.vtype = self.gp.GRB.BINARY
|
|
|
|
|
|
|
|
log = streams[0].getvalue()
|
|
|
|
|
|
|
|
self._has_lp_solution = self.model.solCount > 0
|
|
|
|
|
|
|
|
self._has_mip_solution = False
|
|
|
|
|
|
|
|
opt_value = None
|
|
|
|
|
|
|
|
if not self.is_infeasible():
|
|
|
|
|
|
|
|
opt_value = self.model.objVal
|
|
|
|
|
|
|
|
return LPSolveStats(
|
|
|
|
|
|
|
|
lp_value=opt_value,
|
|
|
|
|
|
|
|
lp_log=log,
|
|
|
|
|
|
|
|
lp_wallclock_time=self.model.runtime,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
@overrides
|
|
|
|
def solve(
|
|
|
|
def solve(
|
|
|
@ -235,126 +368,36 @@ class GurobiSolver(InternalSolver):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
@overrides
|
|
|
|
def get_solution(self) -> Optional[Solution]:
|
|
|
|
def solve_lp(
|
|
|
|
assert self.model is not None
|
|
|
|
self,
|
|
|
|
if self.cb_where is not None:
|
|
|
|
tee: bool = False,
|
|
|
|
if self.cb_where == self.gp.GRB.Callback.MIPNODE:
|
|
|
|
) -> LPSolveStats:
|
|
|
|
return {
|
|
|
|
|
|
|
|
v.varName: self.model.cbGetNodeRel(v) for v in self.model.getVars()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
elif self.cb_where == self.gp.GRB.Callback.MIPSOL:
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
v.varName: self.model.cbGetSolution(v) for v in self.model.getVars()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
raise Exception(
|
|
|
|
|
|
|
|
f"get_solution can only be called from a callback "
|
|
|
|
|
|
|
|
f"when cb_where is either MIPNODE or MIPSOL"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if self.model.solCount == 0:
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
return {v.varName: v.x for v in self.model.getVars()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_variable_names(self) -> List[VariableName]:
|
|
|
|
|
|
|
|
self._raise_if_callback()
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
return [v.varName for v in self.model.getVars()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def set_warm_start(self, solution: Solution) -> None:
|
|
|
|
|
|
|
|
self._raise_if_callback()
|
|
|
|
self._raise_if_callback()
|
|
|
|
self._clear_warm_start()
|
|
|
|
streams: List[Any] = [StringIO()]
|
|
|
|
for (var_name, value) in solution.items():
|
|
|
|
if tee:
|
|
|
|
var = self._varname_to_var[var_name]
|
|
|
|
streams += [sys.stdout]
|
|
|
|
if value is not None:
|
|
|
|
self._apply_params(streams)
|
|
|
|
var.start = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_sense(self) -> str:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
if self.model.modelSense == 1:
|
|
|
|
|
|
|
|
return "min"
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
return "max"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def is_infeasible(self) -> bool:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
return self.model.status in [self.gp.GRB.INFEASIBLE, self.gp.GRB.INF_OR_UNBD]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_dual(self, cid: str) -> float:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
c = self.model.getConstrByName(cid)
|
|
|
|
|
|
|
|
if self.is_infeasible():
|
|
|
|
|
|
|
|
return c.farkasDual
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
return c.pi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_value(self, var: Any) -> float:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
if self.cb_where == self.gp.GRB.Callback.MIPSOL:
|
|
|
|
|
|
|
|
return self.model.cbGetSolution(var)
|
|
|
|
|
|
|
|
elif self.cb_where == self.gp.GRB.Callback.MIPNODE:
|
|
|
|
|
|
|
|
return self.model.cbGetNodeRel(var)
|
|
|
|
|
|
|
|
elif self.cb_where is None:
|
|
|
|
|
|
|
|
return var.x
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
raise Exception(
|
|
|
|
|
|
|
|
"get_value cannot be called from cb_where=%s" % self.cb_where
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def add_constraint(self, constr: Constraint, name: str) -> None:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
lhs = self.gp.quicksum(
|
|
|
|
|
|
|
|
self._varname_to_var[varname] * coeff
|
|
|
|
|
|
|
|
for (varname, coeff) in constr.lhs.items()
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if constr.sense == "=":
|
|
|
|
|
|
|
|
self.model.addConstr(lhs == constr.rhs, name=name)
|
|
|
|
|
|
|
|
elif constr.sense == "<":
|
|
|
|
|
|
|
|
self.model.addConstr(lhs <= constr.rhs, name=name)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self.model.addConstr(lhs >= constr.rhs, name=name)
|
|
|
|
|
|
|
|
self._dirty = True
|
|
|
|
|
|
|
|
self._has_lp_solution = False
|
|
|
|
|
|
|
|
self._has_mip_solution = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def remove_constraint(self, name: str) -> None:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
assert self.model is not None
|
|
|
|
constr = self.model.getConstrByName(name)
|
|
|
|
for var in self._bin_vars:
|
|
|
|
self.model.remove(constr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def is_constraint_satisfied(self, constr: Constraint, tol: float = 1e-6) -> bool:
|
|
|
|
|
|
|
|
lhs = 0.0
|
|
|
|
|
|
|
|
for (varname, coeff) in constr.lhs.items():
|
|
|
|
|
|
|
|
var = self._varname_to_var[varname]
|
|
|
|
|
|
|
|
lhs += self._get_value(var) * coeff
|
|
|
|
|
|
|
|
if constr.sense == "<":
|
|
|
|
|
|
|
|
return lhs <= constr.rhs + tol
|
|
|
|
|
|
|
|
elif constr.sense == ">":
|
|
|
|
|
|
|
|
return lhs >= constr.rhs - tol
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
return abs(constr.rhs - lhs) < abs(tol)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _clear_warm_start(self) -> None:
|
|
|
|
|
|
|
|
for var in self._varname_to_var.values():
|
|
|
|
|
|
|
|
var.start = self.gp.GRB.UNDEFINED
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def fix(self, solution: Solution) -> None:
|
|
|
|
|
|
|
|
self._raise_if_callback()
|
|
|
|
|
|
|
|
for (varname, value) in solution.items():
|
|
|
|
|
|
|
|
if value is None:
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
var = self._varname_to_var[varname]
|
|
|
|
|
|
|
|
var.vtype = self.gp.GRB.CONTINUOUS
|
|
|
|
var.vtype = self.gp.GRB.CONTINUOUS
|
|
|
|
var.lb = value
|
|
|
|
var.lb = 0.0
|
|
|
|
var.ub = value
|
|
|
|
var.ub = 1.0
|
|
|
|
|
|
|
|
with _RedirectOutput(streams):
|
|
|
|
|
|
|
|
self.model.optimize()
|
|
|
|
|
|
|
|
self._dirty = False
|
|
|
|
|
|
|
|
for var in self._bin_vars:
|
|
|
|
|
|
|
|
var.vtype = self.gp.GRB.BINARY
|
|
|
|
|
|
|
|
log = streams[0].getvalue()
|
|
|
|
|
|
|
|
self._has_lp_solution = self.model.solCount > 0
|
|
|
|
|
|
|
|
self._has_mip_solution = False
|
|
|
|
|
|
|
|
opt_value = None
|
|
|
|
|
|
|
|
if not self.is_infeasible():
|
|
|
|
|
|
|
|
opt_value = self.model.objVal
|
|
|
|
|
|
|
|
return LPSolveStats(
|
|
|
|
|
|
|
|
lp_value=opt_value,
|
|
|
|
|
|
|
|
lp_log=log,
|
|
|
|
|
|
|
|
lp_wallclock_time=self.model.runtime,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
@overrides
|
|
|
|
def relax(self) -> None:
|
|
|
|
def relax(self) -> None:
|
|
|
@ -363,14 +406,18 @@ class GurobiSolver(InternalSolver):
|
|
|
|
self.model = self.model.relax()
|
|
|
|
self.model = self.model.relax()
|
|
|
|
self._update_vars()
|
|
|
|
self._update_vars()
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_warm_start_value(self, log: str) -> Optional[float]:
|
|
|
|
def _apply_params(self, streams: List[Any]) -> None:
|
|
|
|
ws = self.__extract(log, "MIP start with objective ([0-9.e+-]*)")
|
|
|
|
assert self.model is not None
|
|
|
|
if ws is None:
|
|
|
|
with _RedirectOutput(streams):
|
|
|
|
return None
|
|
|
|
for (name, value) in self.params.items():
|
|
|
|
return float(ws)
|
|
|
|
self.model.setParam(name, value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _clear_warm_start(self) -> None:
|
|
|
|
|
|
|
|
for var in self._varname_to_var.values():
|
|
|
|
|
|
|
|
var.start = self.gp.GRB.UNDEFINED
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
@staticmethod
|
|
|
|
def __extract(
|
|
|
|
def _extract(
|
|
|
|
log: str,
|
|
|
|
log: str,
|
|
|
|
regexp: str,
|
|
|
|
regexp: str,
|
|
|
|
default: Optional[str] = None,
|
|
|
|
default: Optional[str] = None,
|
|
|
@ -383,53 +430,24 @@ class GurobiSolver(InternalSolver):
|
|
|
|
value = matches[0]
|
|
|
|
value = matches[0]
|
|
|
|
return value
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def __getstate__(self) -> Dict:
|
|
|
|
def _extract_warm_start_value(self, log: str) -> Optional[float]:
|
|
|
|
return {
|
|
|
|
ws = self._extract(log, "MIP start with objective ([0-9.e+-]*)")
|
|
|
|
"params": self.params,
|
|
|
|
if ws is None:
|
|
|
|
"lazy_cb_where": self.lazy_cb_where,
|
|
|
|
return None
|
|
|
|
}
|
|
|
|
return float(ws)
|
|
|
|
|
|
|
|
|
|
|
|
def __setstate__(self, state: Dict) -> None:
|
|
|
|
|
|
|
|
self.params = state["params"]
|
|
|
|
|
|
|
|
self.lazy_cb_where = state["lazy_cb_where"]
|
|
|
|
|
|
|
|
self.instance = None
|
|
|
|
|
|
|
|
self.model = None
|
|
|
|
|
|
|
|
self.cb_where = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def clone(self) -> "GurobiSolver":
|
|
|
|
|
|
|
|
return GurobiSolver(
|
|
|
|
|
|
|
|
params=self.params,
|
|
|
|
|
|
|
|
lazy_cb_frequency=self.lazy_cb_frequency,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def build_test_instance_infeasible(self) -> Instance:
|
|
|
|
|
|
|
|
return GurobiTestInstanceInfeasible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def build_test_instance_redundancy(self) -> Instance:
|
|
|
|
|
|
|
|
return GurobiTestInstanceRedundancy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def build_test_instance_knapsack(self) -> Instance:
|
|
|
|
|
|
|
|
return GurobiTestInstanceKnapsack(
|
|
|
|
|
|
|
|
weights=[23.0, 26.0, 20.0, 18.0],
|
|
|
|
|
|
|
|
prices=[505.0, 352.0, 458.0, 220.0],
|
|
|
|
|
|
|
|
capacity=67.0,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
def _get_value(self, var: Any) -> float:
|
|
|
|
def get_variables(self) -> Dict[str, Variable]:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
assert self.model is not None
|
|
|
|
variables = {}
|
|
|
|
if self.cb_where == self.gp.GRB.Callback.MIPSOL:
|
|
|
|
for gp_var in self.model.getVars():
|
|
|
|
return self.model.cbGetSolution(var)
|
|
|
|
name = gp_var.varName
|
|
|
|
elif self.cb_where == self.gp.GRB.Callback.MIPNODE:
|
|
|
|
assert len(name) > 0, f"empty variable name detected"
|
|
|
|
return self.model.cbGetNodeRel(var)
|
|
|
|
assert name not in variables, f"duplicated variable name detected: {name}"
|
|
|
|
elif self.cb_where is None:
|
|
|
|
var = self._parse_gurobi_var(gp_var)
|
|
|
|
return var.x
|
|
|
|
variables[name] = var
|
|
|
|
else:
|
|
|
|
return variables
|
|
|
|
raise Exception(
|
|
|
|
|
|
|
|
"get_value cannot be called from cb_where=%s" % self.cb_where
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_gurobi_var(self, gp_var: Any) -> Variable:
|
|
|
|
def _parse_gurobi_var(self, gp_var: Any) -> Variable:
|
|
|
|
assert self.model is not None
|
|
|
|
assert self.model is not None
|
|
|
@ -462,20 +480,6 @@ class GurobiSolver(InternalSolver):
|
|
|
|
var.value = gp_var.x
|
|
|
|
var.value = gp_var.x
|
|
|
|
return var
|
|
|
|
return var
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_constraints(self) -> Dict[str, Constraint]:
|
|
|
|
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
self._raise_if_callback()
|
|
|
|
|
|
|
|
if self._dirty:
|
|
|
|
|
|
|
|
self.model.update()
|
|
|
|
|
|
|
|
self._dirty = False
|
|
|
|
|
|
|
|
constraints: Dict[str, Constraint] = {}
|
|
|
|
|
|
|
|
for c in self.model.getConstrs():
|
|
|
|
|
|
|
|
constr = self._parse_gurobi_constraint(c)
|
|
|
|
|
|
|
|
assert c.constrName not in constraints
|
|
|
|
|
|
|
|
constraints[c.constrName] = constr
|
|
|
|
|
|
|
|
return constraints
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_gurobi_constraint(self, gp_constr: Any) -> Constraint:
|
|
|
|
def _parse_gurobi_constraint(self, gp_constr: Any) -> Constraint:
|
|
|
|
assert self.model is not None
|
|
|
|
assert self.model is not None
|
|
|
|
expr = self.model.getRow(gp_constr)
|
|
|
|
expr = self.model.getRow(gp_constr)
|
|
|
@ -501,45 +505,41 @@ class GurobiSolver(InternalSolver):
|
|
|
|
constr.slack = gp_constr.slack
|
|
|
|
constr.slack = gp_constr.slack
|
|
|
|
return constr
|
|
|
|
return constr
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
def _raise_if_callback(self) -> None:
|
|
|
|
def are_callbacks_supported(self) -> bool:
|
|
|
|
if self.cb_where is not None:
|
|
|
|
return True
|
|
|
|
raise Exception("method cannot be called from a callback")
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
def _update_vars(self) -> None:
|
|
|
|
def get_constraint_attrs(self) -> List[str]:
|
|
|
|
assert self.model is not None
|
|
|
|
return [
|
|
|
|
self._varname_to_var.clear()
|
|
|
|
"basis_status",
|
|
|
|
self._original_vtype = {}
|
|
|
|
"category",
|
|
|
|
self._bin_vars.clear()
|
|
|
|
"dual_value",
|
|
|
|
for var in self.model.getVars():
|
|
|
|
"lazy",
|
|
|
|
assert var.varName not in self._varname_to_var, (
|
|
|
|
"lhs",
|
|
|
|
f"Duplicated variable name detected: {var.varName}. "
|
|
|
|
"rhs",
|
|
|
|
f"Unique variable names are currently required."
|
|
|
|
"sa_rhs_down",
|
|
|
|
)
|
|
|
|
"sa_rhs_up",
|
|
|
|
self._varname_to_var[var.varName] = var
|
|
|
|
"sense",
|
|
|
|
assert var.vtype in ["B", "C"], (
|
|
|
|
"slack",
|
|
|
|
"Only binary and continuous variables are currently supported. "
|
|
|
|
"user_features",
|
|
|
|
"Variable {var.varName} has type {var.vtype}."
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
self._original_vtype[var] = var.vtype
|
|
|
|
|
|
|
|
if var.vtype == "B":
|
|
|
|
|
|
|
|
self._bin_vars.append(var)
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
def __getstate__(self) -> Dict:
|
|
|
|
def get_variable_attrs(self) -> List[str]:
|
|
|
|
return {
|
|
|
|
return [
|
|
|
|
"params": self.params,
|
|
|
|
"basis_status",
|
|
|
|
"lazy_cb_where": self.lazy_cb_where,
|
|
|
|
"category",
|
|
|
|
}
|
|
|
|
"lower_bound",
|
|
|
|
|
|
|
|
"obj_coeff",
|
|
|
|
def __setstate__(self, state: Dict) -> None:
|
|
|
|
"reduced_cost",
|
|
|
|
self.params = state["params"]
|
|
|
|
"sa_lb_down",
|
|
|
|
self.lazy_cb_where = state["lazy_cb_where"]
|
|
|
|
"sa_lb_up",
|
|
|
|
self.instance = None
|
|
|
|
"sa_obj_down",
|
|
|
|
self.model = None
|
|
|
|
"sa_obj_up",
|
|
|
|
self.cb_where = None
|
|
|
|
"sa_ub_down",
|
|
|
|
|
|
|
|
"sa_ub_up",
|
|
|
|
|
|
|
|
"type",
|
|
|
|
|
|
|
|
"upper_bound",
|
|
|
|
|
|
|
|
"user_features",
|
|
|
|
|
|
|
|
"value",
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GurobiTestInstanceInfeasible(Instance):
|
|
|
|
class GurobiTestInstanceInfeasible(Instance):
|
|
|
|