mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Add types to remaining InternalSolver methods
This commit is contained in:
@@ -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:
|
||||
return var.x
|
||||
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):
|
||||
var = self._varname_to_var[var_name]
|
||||
return var[index].value
|
||||
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]]]
|
||||
|
||||
Reference in New Issue
Block a user