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,
MIPSolveStats,
)
from miplearn.types import VarIndex
logger = logging.getLogger(__name__)
@ -230,7 +231,7 @@ class GurobiSolver(InternalSolver):
else:
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]
return self._get_value(var)
@ -244,26 +245,29 @@ class GurobiSolver(InternalSolver):
else:
return c.pi
def _get_value(self, var):
def _get_value(self, var: Any) -> Optional[float]:
if self.cb_where == self.GRB.Callback.MIPSOL:
return self.model.cbGetSolution(var)
elif self.cb_where == self.GRB.Callback.MIPNODE:
return self.model.cbGetNodeRel(var)
elif self.cb_where is None:
if self.is_infeasible():
return None
else:
return var.x
else:
raise Exception(
"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()
variables = {}
solution: Dict = {}
for (varname, vardict) in self._all_vars.items():
variables[varname] = []
solution[varname] = {}
for (idx, var) in vardict.items():
variables[varname] += [idx]
return variables
solution[varname][idx] = None
return solution
def add_constraint(self, constraint, name=""):
if type(constraint) is tuple:
@ -325,7 +329,7 @@ class GurobiSolver(InternalSolver):
else:
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 != "="]
return {c.ConstrName: c.Slack for c in ineqs}
@ -341,7 +345,7 @@ class GurobiSolver(InternalSolver):
c = self.model.getConstrByName(cid)
c.RHS = rhs
def relax(self):
def relax(self) -> None:
self.model = self.model.relax()
self._update_vars()

@ -12,6 +12,7 @@ from miplearn.types import (
IterationCallback,
LazyCallback,
MIPSolveStats,
VarIndex,
)
logger = logging.getLogger(__name__)
@ -196,21 +197,22 @@ class InternalSolver(ABC):
pass
@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
@abstractmethod
def relax(self):
def relax(self) -> None:
"""
Drops all integrality constraints from the model.
"""
pass
@abstractmethod
def get_inequality_slacks(self):
def get_inequality_slacks(self) -> Dict[str, float]:
"""
Returns a dictionary mapping constraint name to the constraint slack
in the current solution.
@ -218,7 +220,7 @@ class InternalSolver(ABC):
pass
@abstractmethod
def is_infeasible(self):
def is_infeasible(self) -> bool:
"""
Returns True if the model has been proved to be infeasible.
Must be called after solve.
@ -226,31 +228,30 @@ class InternalSolver(ABC):
pass
@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
value of the dual variable associated with this constraint. If the model is infeasible,
returns a portion of the infeasibility certificate corresponding to the given constraint.
If the model is feasible and has been solved to optimality, returns the
optimal value of the dual variable associated with this constraint. If the
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
@abstractmethod
def get_sense(self):
def get_sense(self) -> str:
"""
Returns the sense of the problem (either "min" or "max").
"""
pass
@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
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,
MIPSolveStats,
)
from miplearn.types import VarIndex
logger = logging.getLogger(__name__)
@ -53,20 +54,13 @@ class BasePyomoSolver(InternalSolver):
self,
tee: bool = False,
) -> LPSolveStats:
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)
self.relax()
streams: List[Any] = [StringIO()]
if tee:
streams += [sys.stdout]
with RedirectOutput(streams):
results = self._pyomo_solver.solve(tee=True)
for var in self._bin_vars:
var.domain = pyomo.core.base.set_types.Binary
self._pyomo_solver.update_var(var)
self._restore_integrality()
opt_value = None
if not self.is_infeasible():
opt_value = results["Problem"][0]["Lower bound"]
@ -75,6 +69,11 @@ class BasePyomoSolver(InternalSolver):
"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(
self,
tee: bool = False,
@ -166,19 +165,23 @@ class BasePyomoSolver(InternalSolver):
self._update_vars()
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]
return var[index].value
def get_variables(self):
variables = {}
def get_empty_solution(self) -> Dict:
solution: Dict = {}
for var in self.model.component_objects(Var):
variables[str(var)] = []
svar = str(var)
solution[svar] = {}
for index in var:
if var[index].fixed:
continue
variables[str(var)] += [index]
return variables
solution[svar][index] = None
return solution
def _clear_warm_start(self) -> None:
for var in self._all_vars:
@ -273,10 +276,15 @@ class BasePyomoSolver(InternalSolver):
def is_constraint_satisfied(self, cobj):
raise Exception("Not implemented")
def relax(self):
raise Exception("not implemented")
def relax(self) -> None:
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")
def set_constraint_sense(self, cid, sense):

@ -19,9 +19,9 @@ from miplearn.solvers.pyomo.xpress import XpressPyomoSolver
class InfeasiblePyomoInstance(PyomoInstance):
def to_model(self) -> pe.ConcreteModel:
model = pe.ConcreteModel()
model.x = pe.Var(domain=pe.Binary)
model.OBJ = pe.Objective(expr=model.x, sense=pe.maximize)
model.eq = pe.Constraint(expr=model.x >= 2)
model.x = pe.Var([0], domain=pe.Binary)
model.OBJ = pe.Objective(expr=model.x[0], sense=pe.maximize)
model.eq = pe.Constraint(expr=model.x[0] >= 2)
return model

@ -7,6 +7,7 @@ from io import StringIO
from warnings import warn
import pyomo.environ as pe
from pytest import raises
from miplearn.solvers import RedirectOutput
from miplearn.solvers.gurobi import GurobiSolver
@ -92,6 +93,7 @@ def test_internal_solver():
solver.set_instance(instance, model)
stats = solver.solve_lp()
assert not solver.is_infeasible()
assert round(stats["Optimal value"], 3) == 1287.923
assert len(stats["Log"]) > 100
@ -102,6 +104,7 @@ def test_internal_solver():
assert round(solution["x"][3], 3) == 0.000
stats = solver.solve(tee=True)
assert not solver.is_infeasible()
assert len(stats["Log"]) > 100
assert stats["Lower bound"] == 1183.0
assert stats["Upper bound"] == 1183.0
@ -131,6 +134,17 @@ def test_internal_solver():
assert stats["Lower bound"] == 1030.0
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
cobj = solver.extract_constraint("cut")
@ -160,6 +174,17 @@ def test_internal_solver():
solver.set_constraint_sense("cut", "=")
stats = solver.solve()
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():
@ -169,6 +194,7 @@ def test_infeasible_instance():
solver.set_instance(instance)
stats = solver.solve()
assert solver.is_infeasible()
assert solver.get_solution() is None
assert stats["Upper bound"] is None
assert stats["Lower bound"] is None
@ -176,6 +202,7 @@ def test_infeasible_instance():
stats = solver.solve_lp()
assert solver.get_solution() is None
assert stats["Optimal value"] is None
assert solver.get_value("x", 0) is None
def test_iteration_cb():

@ -2,7 +2,7 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# 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
@ -46,3 +46,5 @@ MIPSolveStats = TypedDict(
IterationCallback = Callable[[], bool]
LazyCallback = Callable[[Any, Any], None]
VarIndex = Union[str, int, List[Union[str, int]]]

Loading…
Cancel
Save