|
|
@ -25,8 +25,6 @@ from miplearn.types import (
|
|
|
|
SolverParams,
|
|
|
|
SolverParams,
|
|
|
|
UserCutCallback,
|
|
|
|
UserCutCallback,
|
|
|
|
Solution,
|
|
|
|
Solution,
|
|
|
|
VariableName,
|
|
|
|
|
|
|
|
Category,
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
@ -66,13 +64,18 @@ class GurobiSolver(InternalSolver):
|
|
|
|
self.params: SolverParams = params
|
|
|
|
self.params: SolverParams = params
|
|
|
|
self.cb_where: Optional[int] = None
|
|
|
|
self.cb_where: Optional[int] = None
|
|
|
|
self.lazy_cb_frequency = lazy_cb_frequency
|
|
|
|
self.lazy_cb_frequency = lazy_cb_frequency
|
|
|
|
self._bin_vars: List["gurobipy.Var"] = []
|
|
|
|
|
|
|
|
self._varname_to_var: Dict[str, "gurobipy.Var"] = {}
|
|
|
|
|
|
|
|
self._original_vtype: Dict["gurobipy.Var", str] = {}
|
|
|
|
|
|
|
|
self._dirty = True
|
|
|
|
self._dirty = True
|
|
|
|
self._has_lp_solution = False
|
|
|
|
self._has_lp_solution = False
|
|
|
|
self._has_mip_solution = False
|
|
|
|
self._has_mip_solution = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._varname_to_var: Dict[str, "gurobipy.Var"] = {}
|
|
|
|
|
|
|
|
self._gp_vars: List["gurobipy.Var"] = []
|
|
|
|
|
|
|
|
self._var_names: List[str] = []
|
|
|
|
|
|
|
|
self._var_types: List[str] = []
|
|
|
|
|
|
|
|
self._var_lbs: List[float] = []
|
|
|
|
|
|
|
|
self._var_ubs: List[float] = []
|
|
|
|
|
|
|
|
self._var_obj_coeffs: List[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]
|
|
|
|
else:
|
|
|
|
else:
|
|
|
@ -84,6 +87,8 @@ class GurobiSolver(InternalSolver):
|
|
|
|
@overrides
|
|
|
|
@overrides
|
|
|
|
def add_constraint(self, constr: Constraint, name: str) -> None:
|
|
|
|
def add_constraint(self, constr: Constraint, name: str) -> None:
|
|
|
|
assert self.model is not None
|
|
|
|
assert self.model is not None
|
|
|
|
|
|
|
|
assert self._varname_to_var is not None
|
|
|
|
|
|
|
|
|
|
|
|
assert constr.lhs is not None
|
|
|
|
assert constr.lhs is not None
|
|
|
|
lhs = self.gp.quicksum(
|
|
|
|
lhs = self.gp.quicksum(
|
|
|
|
self._varname_to_var[varname] * coeff
|
|
|
|
self._varname_to_var[varname] * coeff
|
|
|
@ -265,11 +270,18 @@ class GurobiSolver(InternalSolver):
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
@overrides
|
|
|
|
def get_variables(self, with_static: bool = True) -> Dict[str, Variable]:
|
|
|
|
def get_variables(
|
|
|
|
|
|
|
|
self,
|
|
|
|
|
|
|
|
with_static: bool = True,
|
|
|
|
|
|
|
|
with_sa: bool = True,
|
|
|
|
|
|
|
|
) -> Dict[str, Variable]:
|
|
|
|
assert self.model is not None
|
|
|
|
assert self.model is not None
|
|
|
|
variables = {}
|
|
|
|
|
|
|
|
gp_vars = self.model.getVars()
|
|
|
|
names = self._var_names
|
|
|
|
names = self.model.getAttr("varName", gp_vars)
|
|
|
|
ub = self._var_ubs
|
|
|
|
|
|
|
|
lb = self._var_lbs
|
|
|
|
|
|
|
|
obj_coeff = self._var_obj_coeffs
|
|
|
|
|
|
|
|
|
|
|
|
values = None
|
|
|
|
values = None
|
|
|
|
rc = None
|
|
|
|
rc = None
|
|
|
|
sa_obj_up = None
|
|
|
|
sa_obj_up = None
|
|
|
@ -279,26 +291,23 @@ class GurobiSolver(InternalSolver):
|
|
|
|
sa_lb_up = None
|
|
|
|
sa_lb_up = None
|
|
|
|
sa_lb_down = None
|
|
|
|
sa_lb_down = None
|
|
|
|
vbasis = None
|
|
|
|
vbasis = None
|
|
|
|
ub = None
|
|
|
|
|
|
|
|
lb = None
|
|
|
|
|
|
|
|
obj_coeff = None
|
|
|
|
|
|
|
|
if with_static:
|
|
|
|
|
|
|
|
lb = self.model.getAttr("lb", gp_vars)
|
|
|
|
|
|
|
|
ub = self.model.getAttr("ub", gp_vars)
|
|
|
|
|
|
|
|
obj_coeff = self.model.getAttr("obj", gp_vars)
|
|
|
|
|
|
|
|
if self.model.solCount > 0:
|
|
|
|
if self.model.solCount > 0:
|
|
|
|
values = self.model.getAttr("x", gp_vars)
|
|
|
|
values = self.model.getAttr("x", self._gp_vars)
|
|
|
|
|
|
|
|
|
|
|
|
if self._has_lp_solution:
|
|
|
|
if self._has_lp_solution:
|
|
|
|
rc = self.model.getAttr("rc", gp_vars)
|
|
|
|
rc = self.model.getAttr("rc", self._gp_vars)
|
|
|
|
sa_obj_up = self.model.getAttr("saobjUp", gp_vars)
|
|
|
|
vbasis = self.model.getAttr("vbasis", self._gp_vars)
|
|
|
|
sa_obj_down = self.model.getAttr("saobjLow", gp_vars)
|
|
|
|
if with_sa:
|
|
|
|
sa_ub_up = self.model.getAttr("saubUp", gp_vars)
|
|
|
|
sa_obj_up = self.model.getAttr("saobjUp", self._gp_vars)
|
|
|
|
sa_ub_down = self.model.getAttr("saubLow", gp_vars)
|
|
|
|
sa_obj_down = self.model.getAttr("saobjLow", self._gp_vars)
|
|
|
|
sa_lb_up = self.model.getAttr("salbUp", gp_vars)
|
|
|
|
sa_ub_up = self.model.getAttr("saubUp", self._gp_vars)
|
|
|
|
sa_lb_down = self.model.getAttr("salbLow", gp_vars)
|
|
|
|
sa_ub_down = self.model.getAttr("saubLow", self._gp_vars)
|
|
|
|
vbasis = self.model.getAttr("vbasis", gp_vars)
|
|
|
|
sa_lb_up = self.model.getAttr("salbUp", self._gp_vars)
|
|
|
|
|
|
|
|
sa_lb_down = self.model.getAttr("salbLow", self._gp_vars)
|
|
|
|
|
|
|
|
|
|
|
|
for (i, gp_var) in enumerate(gp_vars):
|
|
|
|
variables = {}
|
|
|
|
|
|
|
|
for (i, gp_var) in enumerate(self._gp_vars):
|
|
|
|
assert len(names[i]) > 0, "Empty variable name detected."
|
|
|
|
assert len(names[i]) > 0, "Empty variable name detected."
|
|
|
|
assert (
|
|
|
|
assert (
|
|
|
|
names[i] not in variables
|
|
|
|
names[i] not in variables
|
|
|
@ -311,24 +320,12 @@ class GurobiSolver(InternalSolver):
|
|
|
|
var.lower_bound = lb[i]
|
|
|
|
var.lower_bound = lb[i]
|
|
|
|
var.upper_bound = ub[i]
|
|
|
|
var.upper_bound = ub[i]
|
|
|
|
var.obj_coeff = obj_coeff[i]
|
|
|
|
var.obj_coeff = obj_coeff[i]
|
|
|
|
var.type = self._original_vtype[gp_var]
|
|
|
|
var.type = self._var_types[i]
|
|
|
|
if values is not None:
|
|
|
|
if values is not None:
|
|
|
|
var.value = values[i]
|
|
|
|
var.value = values[i]
|
|
|
|
if rc is not None:
|
|
|
|
if rc is not None:
|
|
|
|
assert sa_obj_up is not None
|
|
|
|
|
|
|
|
assert sa_obj_down is not None
|
|
|
|
|
|
|
|
assert sa_ub_up is not None
|
|
|
|
|
|
|
|
assert sa_ub_down is not None
|
|
|
|
|
|
|
|
assert sa_lb_up is not None
|
|
|
|
|
|
|
|
assert sa_lb_down is not None
|
|
|
|
|
|
|
|
assert vbasis is not None
|
|
|
|
assert vbasis is not None
|
|
|
|
var.reduced_cost = rc[i]
|
|
|
|
var.reduced_cost = rc[i]
|
|
|
|
var.sa_obj_up = sa_obj_up[i]
|
|
|
|
|
|
|
|
var.sa_obj_down = sa_obj_down[i]
|
|
|
|
|
|
|
|
var.sa_ub_up = sa_ub_up[i]
|
|
|
|
|
|
|
|
var.sa_ub_down = sa_ub_down[i]
|
|
|
|
|
|
|
|
var.sa_lb_up = sa_lb_up[i]
|
|
|
|
|
|
|
|
var.sa_lb_down = sa_lb_down[i]
|
|
|
|
|
|
|
|
if vbasis[i] == 0:
|
|
|
|
if vbasis[i] == 0:
|
|
|
|
var.basis_status = "B"
|
|
|
|
var.basis_status = "B"
|
|
|
|
elif vbasis[i] == -1:
|
|
|
|
elif vbasis[i] == -1:
|
|
|
@ -339,6 +336,19 @@ class GurobiSolver(InternalSolver):
|
|
|
|
var.basis_status = "S"
|
|
|
|
var.basis_status = "S"
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
raise Exception(f"unknown vbasis: {vbasis}")
|
|
|
|
raise Exception(f"unknown vbasis: {vbasis}")
|
|
|
|
|
|
|
|
if with_sa:
|
|
|
|
|
|
|
|
assert sa_obj_up is not None
|
|
|
|
|
|
|
|
assert sa_obj_down is not None
|
|
|
|
|
|
|
|
assert sa_ub_up is not None
|
|
|
|
|
|
|
|
assert sa_ub_down is not None
|
|
|
|
|
|
|
|
assert sa_lb_up is not None
|
|
|
|
|
|
|
|
assert sa_lb_down is not None
|
|
|
|
|
|
|
|
var.sa_obj_up = sa_obj_up[i]
|
|
|
|
|
|
|
|
var.sa_obj_down = sa_obj_down[i]
|
|
|
|
|
|
|
|
var.sa_ub_up = sa_ub_up[i]
|
|
|
|
|
|
|
|
var.sa_ub_down = sa_ub_down[i]
|
|
|
|
|
|
|
|
var.sa_lb_up = sa_lb_up[i]
|
|
|
|
|
|
|
|
var.sa_lb_down = sa_lb_down[i]
|
|
|
|
variables[names[i]] = var
|
|
|
|
variables[names[i]] = var
|
|
|
|
return variables
|
|
|
|
return variables
|
|
|
|
|
|
|
|
|
|
|
@ -479,15 +489,17 @@ class GurobiSolver(InternalSolver):
|
|
|
|
streams += [sys.stdout]
|
|
|
|
streams += [sys.stdout]
|
|
|
|
self._apply_params(streams)
|
|
|
|
self._apply_params(streams)
|
|
|
|
assert self.model is not None
|
|
|
|
assert self.model is not None
|
|
|
|
for var in self._bin_vars:
|
|
|
|
for (i, var) in enumerate(self._gp_vars):
|
|
|
|
var.vtype = self.gp.GRB.CONTINUOUS
|
|
|
|
if self._var_types[i] == "B":
|
|
|
|
var.lb = 0.0
|
|
|
|
var.vtype = self.gp.GRB.CONTINUOUS
|
|
|
|
var.ub = 1.0
|
|
|
|
var.lb = 0.0
|
|
|
|
|
|
|
|
var.ub = 1.0
|
|
|
|
with _RedirectOutput(streams):
|
|
|
|
with _RedirectOutput(streams):
|
|
|
|
self.model.optimize()
|
|
|
|
self.model.optimize()
|
|
|
|
self._dirty = False
|
|
|
|
self._dirty = False
|
|
|
|
for var in self._bin_vars:
|
|
|
|
for (i, var) in enumerate(self._gp_vars):
|
|
|
|
var.vtype = self.gp.GRB.BINARY
|
|
|
|
if self._var_types[i] == "B":
|
|
|
|
|
|
|
|
var.vtype = self.gp.GRB.BINARY
|
|
|
|
log = streams[0].getvalue()
|
|
|
|
log = streams[0].getvalue()
|
|
|
|
self._has_lp_solution = self.model.solCount > 0
|
|
|
|
self._has_lp_solution = self.model.solCount > 0
|
|
|
|
self._has_mip_solution = False
|
|
|
|
self._has_mip_solution = False
|
|
|
@ -577,33 +589,40 @@ class GurobiSolver(InternalSolver):
|
|
|
|
|
|
|
|
|
|
|
|
def _update_vars(self) -> None:
|
|
|
|
def _update_vars(self) -> None:
|
|
|
|
assert self.model is not None
|
|
|
|
assert self.model is not None
|
|
|
|
self._varname_to_var.clear()
|
|
|
|
gp_vars = self.model.getVars()
|
|
|
|
self._original_vtype = {}
|
|
|
|
var_names = self.model.getAttr("varName", gp_vars)
|
|
|
|
self._bin_vars.clear()
|
|
|
|
var_types = self.model.getAttr("vtype", gp_vars)
|
|
|
|
for var in self.model.getVars():
|
|
|
|
var_ubs = self.model.getAttr("ub", gp_vars)
|
|
|
|
assert var.varName not in self._varname_to_var, (
|
|
|
|
var_lbs = self.model.getAttr("lb", gp_vars)
|
|
|
|
f"Duplicated variable name detected: {var.varName}. "
|
|
|
|
var_obj_coeffs = self.model.getAttr("obj", gp_vars)
|
|
|
|
f"Unique variable names are currently required."
|
|
|
|
varname_to_var: 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."
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self._varname_to_var[var.varName] = var
|
|
|
|
if var_types[i] == "I":
|
|
|
|
vtype = var.vtype
|
|
|
|
assert var_ubs[i] == 1.0, (
|
|
|
|
if vtype == "I":
|
|
|
|
|
|
|
|
assert var.ub == 1.0, (
|
|
|
|
|
|
|
|
"Only binary and continuous variables are currently supported. "
|
|
|
|
"Only binary and continuous variables are currently supported. "
|
|
|
|
"Integer variable {var.varName} has upper bound {var.ub}."
|
|
|
|
"Integer variable {var.varName} has upper bound {var.ub}."
|
|
|
|
)
|
|
|
|
)
|
|
|
|
assert var.lb == 0.0, (
|
|
|
|
assert var_lbs[i] == 0.0, (
|
|
|
|
"Only binary and continuous variables are currently supported. "
|
|
|
|
"Only binary and continuous variables are currently supported. "
|
|
|
|
"Integer variable {var.varName} has lower bound {var.ub}."
|
|
|
|
"Integer variable {var.varName} has lower bound {var.ub}."
|
|
|
|
)
|
|
|
|
)
|
|
|
|
vtype = "B"
|
|
|
|
var_types[i] = "B"
|
|
|
|
assert vtype in ["B", "C"], (
|
|
|
|
assert var_types[i] in ["B", "C"], (
|
|
|
|
"Only binary and continuous variables are currently supported. "
|
|
|
|
"Only binary and continuous variables are currently supported. "
|
|
|
|
"Variable {var.varName} has type {vtype}."
|
|
|
|
"Variable {var.varName} has type {vtype}."
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self._original_vtype[var] = vtype
|
|
|
|
varname_to_var[var_names[i]] = gp_var
|
|
|
|
if vtype == "B":
|
|
|
|
self._varname_to_var = varname_to_var
|
|
|
|
self._bin_vars.append(var)
|
|
|
|
self._gp_vars = gp_vars
|
|
|
|
|
|
|
|
self._var_names = var_names
|
|
|
|
|
|
|
|
self._var_types = var_types
|
|
|
|
|
|
|
|
self._var_lbs = var_lbs
|
|
|
|
|
|
|
|
self._var_ubs = var_ubs
|
|
|
|
|
|
|
|
self._var_obj_coeffs = var_obj_coeffs
|
|
|
|
|
|
|
|
|
|
|
|
def __getstate__(self) -> Dict:
|
|
|
|
def __getstate__(self) -> Dict:
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|