mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 01:18:52 -06:00
Add types to remaining InternalSolver methods
This commit is contained in:
@@ -17,6 +17,7 @@ from miplearn.solvers.internal import (
|
|||||||
LazyCallback,
|
LazyCallback,
|
||||||
MIPSolveStats,
|
MIPSolveStats,
|
||||||
)
|
)
|
||||||
|
from miplearn.types import VarIndex
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -230,7 +231,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
else:
|
else:
|
||||||
return "max"
|
return "max"
|
||||||
|
|
||||||
def get_value(self, var_name, index):
|
def get_value(self, var_name: str, index: VarIndex) -> Optional[float]:
|
||||||
var = self._all_vars[var_name][index]
|
var = self._all_vars[var_name][index]
|
||||||
return self._get_value(var)
|
return self._get_value(var)
|
||||||
|
|
||||||
@@ -244,26 +245,29 @@ class GurobiSolver(InternalSolver):
|
|||||||
else:
|
else:
|
||||||
return c.pi
|
return c.pi
|
||||||
|
|
||||||
def _get_value(self, var):
|
def _get_value(self, var: Any) -> Optional[float]:
|
||||||
if self.cb_where == self.GRB.Callback.MIPSOL:
|
if self.cb_where == self.GRB.Callback.MIPSOL:
|
||||||
return self.model.cbGetSolution(var)
|
return self.model.cbGetSolution(var)
|
||||||
elif self.cb_where == self.GRB.Callback.MIPNODE:
|
elif self.cb_where == self.GRB.Callback.MIPNODE:
|
||||||
return self.model.cbGetNodeRel(var)
|
return self.model.cbGetNodeRel(var)
|
||||||
elif self.cb_where is None:
|
elif self.cb_where is None:
|
||||||
return var.x
|
if self.is_infeasible():
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return var.x
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"get_value cannot be called from cb_where=%s" % self.cb_where
|
"get_value cannot be called from cb_where=%s" % self.cb_where
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_variables(self):
|
def get_empty_solution(self) -> Dict:
|
||||||
self._raise_if_callback()
|
self._raise_if_callback()
|
||||||
variables = {}
|
solution: Dict = {}
|
||||||
for (varname, vardict) in self._all_vars.items():
|
for (varname, vardict) in self._all_vars.items():
|
||||||
variables[varname] = []
|
solution[varname] = {}
|
||||||
for (idx, var) in vardict.items():
|
for (idx, var) in vardict.items():
|
||||||
variables[varname] += [idx]
|
solution[varname][idx] = None
|
||||||
return variables
|
return solution
|
||||||
|
|
||||||
def add_constraint(self, constraint, name=""):
|
def add_constraint(self, constraint, name=""):
|
||||||
if type(constraint) is tuple:
|
if type(constraint) is tuple:
|
||||||
@@ -325,7 +329,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
else:
|
else:
|
||||||
raise Exception("Unknown sense: %s" % sense)
|
raise Exception("Unknown sense: %s" % sense)
|
||||||
|
|
||||||
def get_inequality_slacks(self):
|
def get_inequality_slacks(self) -> Dict[str, float]:
|
||||||
ineqs = [c for c in self.model.getConstrs() if c.sense != "="]
|
ineqs = [c for c in self.model.getConstrs() if c.sense != "="]
|
||||||
return {c.ConstrName: c.Slack for c in ineqs}
|
return {c.ConstrName: c.Slack for c in ineqs}
|
||||||
|
|
||||||
@@ -341,7 +345,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
c = self.model.getConstrByName(cid)
|
c = self.model.getConstrByName(cid)
|
||||||
c.RHS = rhs
|
c.RHS = rhs
|
||||||
|
|
||||||
def relax(self):
|
def relax(self) -> None:
|
||||||
self.model = self.model.relax()
|
self.model = self.model.relax()
|
||||||
self._update_vars()
|
self._update_vars()
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from miplearn.types import (
|
|||||||
IterationCallback,
|
IterationCallback,
|
||||||
LazyCallback,
|
LazyCallback,
|
||||||
MIPSolveStats,
|
MIPSolveStats,
|
||||||
|
VarIndex,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -196,21 +197,22 @@ class InternalSolver(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_value(self, var_name, index):
|
def get_value(self, var_name: str, index: VarIndex) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Returns the current value of a decision variable.
|
Returns the value of a given variable in the current solution. If no
|
||||||
|
solution is available, returns None.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def relax(self):
|
def relax(self) -> None:
|
||||||
"""
|
"""
|
||||||
Drops all integrality constraints from the model.
|
Drops all integrality constraints from the model.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_inequality_slacks(self):
|
def get_inequality_slacks(self) -> Dict[str, float]:
|
||||||
"""
|
"""
|
||||||
Returns a dictionary mapping constraint name to the constraint slack
|
Returns a dictionary mapping constraint name to the constraint slack
|
||||||
in the current solution.
|
in the current solution.
|
||||||
@@ -218,7 +220,7 @@ class InternalSolver(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_infeasible(self):
|
def is_infeasible(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if the model has been proved to be infeasible.
|
Returns True if the model has been proved to be infeasible.
|
||||||
Must be called after solve.
|
Must be called after solve.
|
||||||
@@ -226,31 +228,30 @@ class InternalSolver(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_dual(self, cid):
|
def get_dual(self, cid: str) -> float:
|
||||||
"""
|
"""
|
||||||
If the model is feasible and has been solved to optimality, returns the optimal
|
If the model is feasible and has been solved to optimality, returns the
|
||||||
value of the dual variable associated with this constraint. If the model is infeasible,
|
optimal value of the dual variable associated with this constraint. If the
|
||||||
returns a portion of the infeasibility certificate corresponding to the given constraint.
|
model is infeasible, returns a portion of the infeasibility certificate
|
||||||
|
corresponding to the given constraint.
|
||||||
|
|
||||||
Must be called after solve.
|
Only available for relaxed problems. Must be called after solve.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_sense(self):
|
def get_sense(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the sense of the problem (either "min" or "max").
|
Returns the sense of the problem (either "min" or "max").
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_variables(self):
|
def get_empty_solution(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Returns a dictionary with the same shape as the one produced by
|
||||||
|
`get_solution`, but with all values set to None. This method is
|
||||||
|
used by the ML components to query what variables are there in
|
||||||
|
the model before a solution is available.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_empty_solution(self):
|
|
||||||
solution = {}
|
|
||||||
for (var, indices) in self.get_variables().items():
|
|
||||||
solution[var] = {}
|
|
||||||
for idx in indices:
|
|
||||||
solution[var][idx] = 0.0
|
|
||||||
return solution
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from miplearn.solvers.internal import (
|
|||||||
LazyCallback,
|
LazyCallback,
|
||||||
MIPSolveStats,
|
MIPSolveStats,
|
||||||
)
|
)
|
||||||
|
from miplearn.types import VarIndex
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -53,20 +54,13 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
self,
|
self,
|
||||||
tee: bool = False,
|
tee: bool = False,
|
||||||
) -> LPSolveStats:
|
) -> LPSolveStats:
|
||||||
for var in self._bin_vars:
|
self.relax()
|
||||||
lb, ub = var.bounds
|
|
||||||
var.setlb(lb)
|
|
||||||
var.setub(ub)
|
|
||||||
var.domain = pyomo.core.base.set_types.Reals
|
|
||||||
self._pyomo_solver.update_var(var)
|
|
||||||
streams: List[Any] = [StringIO()]
|
streams: List[Any] = [StringIO()]
|
||||||
if tee:
|
if tee:
|
||||||
streams += [sys.stdout]
|
streams += [sys.stdout]
|
||||||
with RedirectOutput(streams):
|
with RedirectOutput(streams):
|
||||||
results = self._pyomo_solver.solve(tee=True)
|
results = self._pyomo_solver.solve(tee=True)
|
||||||
for var in self._bin_vars:
|
self._restore_integrality()
|
||||||
var.domain = pyomo.core.base.set_types.Binary
|
|
||||||
self._pyomo_solver.update_var(var)
|
|
||||||
opt_value = None
|
opt_value = None
|
||||||
if not self.is_infeasible():
|
if not self.is_infeasible():
|
||||||
opt_value = results["Problem"][0]["Lower bound"]
|
opt_value = results["Problem"][0]["Lower bound"]
|
||||||
@@ -75,6 +69,11 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
"Log": streams[0].getvalue(),
|
"Log": streams[0].getvalue(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _restore_integrality(self) -> None:
|
||||||
|
for var in self._bin_vars:
|
||||||
|
var.domain = pyomo.core.base.set_types.Binary
|
||||||
|
self._pyomo_solver.update_var(var)
|
||||||
|
|
||||||
def solve(
|
def solve(
|
||||||
self,
|
self,
|
||||||
tee: bool = False,
|
tee: bool = False,
|
||||||
@@ -166,19 +165,23 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
self._update_vars()
|
self._update_vars()
|
||||||
self._update_constrs()
|
self._update_constrs()
|
||||||
|
|
||||||
def get_value(self, var_name, index):
|
def get_value(self, var_name: str, index: VarIndex) -> Optional[float]:
|
||||||
var = self._varname_to_var[var_name]
|
if self.is_infeasible():
|
||||||
return var[index].value
|
return None
|
||||||
|
else:
|
||||||
|
var = self._varname_to_var[var_name]
|
||||||
|
return var[index].value
|
||||||
|
|
||||||
def get_variables(self):
|
def get_empty_solution(self) -> Dict:
|
||||||
variables = {}
|
solution: Dict = {}
|
||||||
for var in self.model.component_objects(Var):
|
for var in self.model.component_objects(Var):
|
||||||
variables[str(var)] = []
|
svar = str(var)
|
||||||
|
solution[svar] = {}
|
||||||
for index in var:
|
for index in var:
|
||||||
if var[index].fixed:
|
if var[index].fixed:
|
||||||
continue
|
continue
|
||||||
variables[str(var)] += [index]
|
solution[svar][index] = None
|
||||||
return variables
|
return solution
|
||||||
|
|
||||||
def _clear_warm_start(self) -> None:
|
def _clear_warm_start(self) -> None:
|
||||||
for var in self._all_vars:
|
for var in self._all_vars:
|
||||||
@@ -273,10 +276,15 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
def is_constraint_satisfied(self, cobj):
|
def is_constraint_satisfied(self, cobj):
|
||||||
raise Exception("Not implemented")
|
raise Exception("Not implemented")
|
||||||
|
|
||||||
def relax(self):
|
def relax(self) -> None:
|
||||||
raise Exception("not implemented")
|
for var in self._bin_vars:
|
||||||
|
lb, ub = var.bounds
|
||||||
|
var.setlb(lb)
|
||||||
|
var.setub(ub)
|
||||||
|
var.domain = pyomo.core.base.set_types.Reals
|
||||||
|
self._pyomo_solver.update_var(var)
|
||||||
|
|
||||||
def get_inequality_slacks(self):
|
def get_inequality_slacks(self) -> Dict[str, float]:
|
||||||
raise Exception("not implemented")
|
raise Exception("not implemented")
|
||||||
|
|
||||||
def set_constraint_sense(self, cid, sense):
|
def set_constraint_sense(self, cid, sense):
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ from miplearn.solvers.pyomo.xpress import XpressPyomoSolver
|
|||||||
class InfeasiblePyomoInstance(PyomoInstance):
|
class InfeasiblePyomoInstance(PyomoInstance):
|
||||||
def to_model(self) -> pe.ConcreteModel:
|
def to_model(self) -> pe.ConcreteModel:
|
||||||
model = pe.ConcreteModel()
|
model = pe.ConcreteModel()
|
||||||
model.x = pe.Var(domain=pe.Binary)
|
model.x = pe.Var([0], domain=pe.Binary)
|
||||||
model.OBJ = pe.Objective(expr=model.x, sense=pe.maximize)
|
model.OBJ = pe.Objective(expr=model.x[0], sense=pe.maximize)
|
||||||
model.eq = pe.Constraint(expr=model.x >= 2)
|
model.eq = pe.Constraint(expr=model.x[0] >= 2)
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from io import StringIO
|
|||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
import pyomo.environ as pe
|
import pyomo.environ as pe
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
from miplearn.solvers import RedirectOutput
|
from miplearn.solvers import RedirectOutput
|
||||||
from miplearn.solvers.gurobi import GurobiSolver
|
from miplearn.solvers.gurobi import GurobiSolver
|
||||||
@@ -92,6 +93,7 @@ def test_internal_solver():
|
|||||||
solver.set_instance(instance, model)
|
solver.set_instance(instance, model)
|
||||||
|
|
||||||
stats = solver.solve_lp()
|
stats = solver.solve_lp()
|
||||||
|
assert not solver.is_infeasible()
|
||||||
assert round(stats["Optimal value"], 3) == 1287.923
|
assert round(stats["Optimal value"], 3) == 1287.923
|
||||||
assert len(stats["Log"]) > 100
|
assert len(stats["Log"]) > 100
|
||||||
|
|
||||||
@@ -102,6 +104,7 @@ def test_internal_solver():
|
|||||||
assert round(solution["x"][3], 3) == 0.000
|
assert round(solution["x"][3], 3) == 0.000
|
||||||
|
|
||||||
stats = solver.solve(tee=True)
|
stats = solver.solve(tee=True)
|
||||||
|
assert not solver.is_infeasible()
|
||||||
assert len(stats["Log"]) > 100
|
assert len(stats["Log"]) > 100
|
||||||
assert stats["Lower bound"] == 1183.0
|
assert stats["Lower bound"] == 1183.0
|
||||||
assert stats["Upper bound"] == 1183.0
|
assert stats["Upper bound"] == 1183.0
|
||||||
@@ -131,6 +134,17 @@ def test_internal_solver():
|
|||||||
assert stats["Lower bound"] == 1030.0
|
assert stats["Lower bound"] == 1030.0
|
||||||
|
|
||||||
if isinstance(solver, GurobiSolver):
|
if isinstance(solver, GurobiSolver):
|
||||||
|
|
||||||
|
assert solver.get_sense() == "max"
|
||||||
|
assert solver.get_constraint_sense("cut") == "<"
|
||||||
|
assert solver.get_constraint_sense("eq_capacity") == "<"
|
||||||
|
|
||||||
|
# Verify slacks
|
||||||
|
assert solver.get_inequality_slacks() == {
|
||||||
|
"cut": 0.0,
|
||||||
|
"eq_capacity": 3.0,
|
||||||
|
}
|
||||||
|
|
||||||
# Extract the new constraint
|
# Extract the new constraint
|
||||||
cobj = solver.extract_constraint("cut")
|
cobj = solver.extract_constraint("cut")
|
||||||
|
|
||||||
@@ -160,6 +174,17 @@ def test_internal_solver():
|
|||||||
solver.set_constraint_sense("cut", "=")
|
solver.set_constraint_sense("cut", "=")
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
assert round(stats["Lower bound"]) == 1179.0
|
assert round(stats["Lower bound"]) == 1179.0
|
||||||
|
assert round(solver.get_dual("eq_capacity")) == 12.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_relax():
|
||||||
|
for solver_class in _get_internal_solvers():
|
||||||
|
instance = _get_knapsack_instance(solver_class)
|
||||||
|
solver = solver_class()
|
||||||
|
solver.set_instance(instance)
|
||||||
|
solver.relax()
|
||||||
|
stats = solver.solve()
|
||||||
|
assert round(stats["Lower bound"]) == 1288.0
|
||||||
|
|
||||||
|
|
||||||
def test_infeasible_instance():
|
def test_infeasible_instance():
|
||||||
@@ -169,6 +194,7 @@ def test_infeasible_instance():
|
|||||||
solver.set_instance(instance)
|
solver.set_instance(instance)
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
|
|
||||||
|
assert solver.is_infeasible()
|
||||||
assert solver.get_solution() is None
|
assert solver.get_solution() is None
|
||||||
assert stats["Upper bound"] is None
|
assert stats["Upper bound"] is None
|
||||||
assert stats["Lower bound"] is None
|
assert stats["Lower bound"] is None
|
||||||
@@ -176,6 +202,7 @@ def test_infeasible_instance():
|
|||||||
stats = solver.solve_lp()
|
stats = solver.solve_lp()
|
||||||
assert solver.get_solution() is None
|
assert solver.get_solution() is None
|
||||||
assert stats["Optimal value"] is None
|
assert stats["Optimal value"] is None
|
||||||
|
assert solver.get_value("x", 0) is None
|
||||||
|
|
||||||
|
|
||||||
def test_iteration_cb():
|
def test_iteration_cb():
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
from typing import Optional, Dict, Callable, Any
|
from typing import Optional, Dict, Callable, Any, Union, List
|
||||||
|
|
||||||
from mypy_extensions import TypedDict
|
from mypy_extensions import TypedDict
|
||||||
|
|
||||||
@@ -46,3 +46,5 @@ MIPSolveStats = TypedDict(
|
|||||||
IterationCallback = Callable[[], bool]
|
IterationCallback = Callable[[], bool]
|
||||||
|
|
||||||
LazyCallback = Callable[[Any, Any], None]
|
LazyCallback = Callable[[Any, Any], None]
|
||||||
|
|
||||||
|
VarIndex = Union[str, int, List[Union[str, int]]]
|
||||||
|
|||||||
Reference in New Issue
Block a user