Add types to remaining InternalSolver methods

master
Alinson S. Xavier 5 years ago
parent fb887d2444
commit 13e142432a

@ -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:
if self.is_infeasible():
return None
else:
return var.x 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]:
if self.is_infeasible():
return None
else:
var = self._varname_to_var[var_name] var = self._varname_to_var[var_name]
return var[index].value 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]]]

Loading…
Cancel
Save