Require a callable as the internal solver

master
Alinson S. Xavier 5 years ago
parent 3ff773402d
commit ffc77075f5

@ -4,13 +4,16 @@
### Selecting the internal MIP solver ### Selecting the internal MIP solver
By default, `LearningSolver` uses [Gurobi](https://www.gurobi.com/) as its internal MIP solver. Another supported solver is [IBM ILOG CPLEX](https://www.ibm.com/products/ilog-cplex-optimization-studio). To switch between solvers, use the `solver` constructor argument, as shown below. It is also possible to specify a time limit (in seconds) and a relative MIP gap tolerance. By default, `LearningSolver` uses [Gurobi](https://www.gurobi.com/) as its internal MIP solver, and expects models to be provided using the Pyomo modeling language. Other supported solvers and modeling languages include:
* `CplexPyomoSolver`: [IBM ILOG CPLEX](https://www.ibm.com/products/ilog-cplex-optimization-studio) with Pyomo.
* `GurobiSolver`: Gurobi without any modeling language.
To switch between solvers, provide the desired class using the `solver` argument:
```python ```python
from miplearn import LearningSolver from miplearn import LearningSolver, CplexPyomoSolver
solver = LearningSolver(solver="cplex", solver = LearningSolver(solver=CplexPyomoSolver)
time_limit=300,
gap_tolerance=1e-3)
``` ```
## Customizing solver components ## Customizing solver components

@ -13,7 +13,7 @@ def test_convert_tight_usage():
capacity=16.0, capacity=16.0,
) )
solver = LearningSolver( solver = LearningSolver(
solver=GurobiSolver(), solver=GurobiSolver,
components=[ components=[
RelaxIntegralityStep(), RelaxIntegralityStep(),
ConvertTightIneqsIntoEqsStep(), ConvertTightIneqsIntoEqsStep(),
@ -64,7 +64,7 @@ def test_convert_tight_infeasibility():
comp.classifiers["c3"].predict_proba = Mock(return_value=[[1, 0]]) comp.classifiers["c3"].predict_proba = Mock(return_value=[[1, 0]])
solver = LearningSolver( solver = LearningSolver(
solver=GurobiSolver(params={}), solver=GurobiSolver,
components=[comp], components=[comp],
solve_lp_first=False, solve_lp_first=False,
) )
@ -87,7 +87,7 @@ def test_convert_tight_suboptimality():
comp.classifiers["c3"].predict_proba = Mock(return_value=[[0, 1]]) comp.classifiers["c3"].predict_proba = Mock(return_value=[[0, 1]])
solver = LearningSolver( solver = LearningSolver(
solver=GurobiSolver(params={}), solver=GurobiSolver,
components=[comp], components=[comp],
solve_lp_first=False, solve_lp_first=False,
) )
@ -110,7 +110,7 @@ def test_convert_tight_optimal():
comp.classifiers["c3"].predict_proba = Mock(return_value=[[0, 1]]) comp.classifiers["c3"].predict_proba = Mock(return_value=[[0, 1]])
solver = LearningSolver( solver = LearningSolver(
solver=GurobiSolver(params={}), solver=GurobiSolver,
components=[comp], components=[comp],
solve_lp_first=False, solve_lp_first=False,
) )

@ -36,18 +36,17 @@ def test_instance():
] ]
) )
instance = TravelingSalesmanInstance(n_cities, distances) instance = TravelingSalesmanInstance(n_cities, distances)
for solver_name in ["gurobi"]: solver = LearningSolver()
solver = LearningSolver(solver=solver_name) solver.solve(instance)
solver.solve(instance) x = instance.solution["x"]
x = instance.solution["x"] assert x[0, 1] == 1.0
assert x[0, 1] == 1.0 assert x[0, 2] == 0.0
assert x[0, 2] == 0.0 assert x[0, 3] == 1.0
assert x[0, 3] == 1.0 assert x[1, 2] == 1.0
assert x[1, 2] == 1.0 assert x[1, 3] == 0.0
assert x[1, 3] == 0.0 assert x[2, 3] == 1.0
assert x[2, 3] == 1.0 assert instance.lower_bound == 4.0
assert instance.lower_bound == 4.0 assert instance.upper_bound == 4.0
assert instance.upper_bound == 4.0
def test_subtour(): def test_subtour():
@ -64,17 +63,16 @@ def test_subtour():
) )
distances = squareform(pdist(cities)) distances = squareform(pdist(cities))
instance = TravelingSalesmanInstance(n_cities, distances) instance = TravelingSalesmanInstance(n_cities, distances)
for solver_name in ["gurobi"]: solver = LearningSolver()
solver = LearningSolver(solver=solver_name) solver.solve(instance)
solver.solve(instance) assert hasattr(instance, "found_violated_lazy_constraints")
assert hasattr(instance, "found_violated_lazy_constraints") assert hasattr(instance, "found_violated_user_cuts")
assert hasattr(instance, "found_violated_user_cuts") x = instance.solution["x"]
x = instance.solution["x"] assert x[0, 1] == 1.0
assert x[0, 1] == 1.0 assert x[0, 4] == 1.0
assert x[0, 4] == 1.0 assert x[1, 2] == 1.0
assert x[1, 2] == 1.0 assert x[2, 3] == 1.0
assert x[2, 3] == 1.0 assert x[3, 5] == 1.0
assert x[3, 5] == 1.0 assert x[4, 5] == 1.0
assert x[4, 5] == 1.0 solver.fit([instance])
solver.fit([instance]) solver.solve(instance)
solver.solve(instance)

@ -20,8 +20,8 @@ from .. import (
DynamicLazyConstraintsComponent, DynamicLazyConstraintsComponent,
UserCutsComponent, UserCutsComponent,
) )
from .pyomo.cplex import CplexPyomoSolver from ..solvers.internal import InternalSolver
from .pyomo.gurobi import GurobiPyomoSolver from ..solvers.pyomo.gurobi import GurobiPyomoSolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -52,33 +52,22 @@ class LearningSolver:
Parameters Parameters
---------- ----------
components components: [Component]
Set of components in the solver. By default, includes: Set of components in the solver. By default, includes:
- ObjectiveValueComponent - ObjectiveValueComponent
- PrimalSolutionComponent - PrimalSolutionComponent
- DynamicLazyConstraintsComponent - DynamicLazyConstraintsComponent
- UserCutsComponent - UserCutsComponent
gap_tolerance mode: str
Relative MIP gap tolerance. By default, 1e-4.
mode
If "exact", solves problem to optimality, keeping all optimality If "exact", solves problem to optimality, keeping all optimality
guarantees provided by the MIP solver. If "heuristic", uses machine guarantees provided by the MIP solver. If "heuristic", uses machine
learning more aggressively, and may return suboptimal solutions. learning more aggressively, and may return suboptimal solutions.
solver solver: Callable[[], InternalSolver]
The internal MIP solver to use. Can be either "cplex", "gurobi", a A callable that constructs the internal solver. If None is provided,
solver class such as GurobiSolver, or a solver instance such as use GurobiPyomoSolver.
GurobiSolver(). use_lazy_cb: bool
threads If true, use native solver callbacks for enforcing lazy constraints,
Maximum number of threads to use. If None, uses solver default. instead of a simple loop. May not be supported by all solvers.
time_limit
Maximum running time in seconds. If None, uses solver default.
node_limit
Maximum number of branch-and-bound nodes to explore. If None, uses
solver default.
use_lazy_cb
If True, uses lazy callbacks to enforce lazy constraints, instead of
a simple solver loop. This functionality may not supported by
all internal MIP solvers.
solve_lp_first: bool solve_lp_first: bool
If true, solve LP relaxation first, then solve original MILP. This If true, solve LP relaxation first, then solve original MILP. This
option should be activated if the LP relaxation is not very option should be activated if the LP relaxation is not very
@ -94,27 +83,23 @@ class LearningSolver:
def __init__( def __init__(
self, self,
components=None, components=None,
gap_tolerance=1e-4,
mode="exact", mode="exact",
solver="gurobi", solver=None,
threads=None,
time_limit=None,
node_limit=None,
solve_lp_first=True,
use_lazy_cb=False, use_lazy_cb=False,
solve_lp_first=True,
simulate_perfect=False, simulate_perfect=False,
): ):
if solver is None:
solver = GurobiPyomoSolver
assert callable(solver), f"Callable expected. Found {solver.__class__} instead."
self.components = {} self.components = {}
self.mode = mode self.mode = mode
self.internal_solver = None self.internal_solver = None
self.internal_solver_factory = solver self.solver_factory = solver
self.threads = threads self.use_lazy_cb = use_lazy_cb
self.time_limit = time_limit
self.gap_tolerance = gap_tolerance
self.tee = False self.tee = False
self.node_limit = node_limit
self.solve_lp_first = solve_lp_first self.solve_lp_first = solve_lp_first
self.use_lazy_cb = use_lazy_cb
self.simulate_perfect = simulate_perfect self.simulate_perfect = simulate_perfect
if components is not None: if components is not None:
@ -130,30 +115,6 @@ class LearningSolver:
for component in self.components.values(): for component in self.components.values():
component.mode = self.mode component.mode = self.mode
def _create_internal_solver(self):
logger.debug("Initializing %s" % self.internal_solver_factory)
if self.internal_solver_factory == "cplex":
solver = CplexPyomoSolver()
elif self.internal_solver_factory == "gurobi":
solver = GurobiPyomoSolver()
elif callable(self.internal_solver_factory):
solver = self.internal_solver_factory()
else:
solver = self.internal_solver_factory
if self.threads is not None:
logger.info("Setting threads to %d" % self.threads)
solver.set_threads(self.threads)
if self.time_limit is not None:
logger.info("Setting time limit to %f" % self.time_limit)
solver.set_time_limit(self.time_limit)
if self.gap_tolerance is not None:
logger.info("Setting gap tolerance to %f" % self.gap_tolerance)
solver.set_gap_tolerance(self.gap_tolerance)
if self.node_limit is not None:
logger.info("Setting node limit to %d" % self.node_limit)
solver.set_node_limit(self.node_limit)
return solver
def solve( def solve(
self, self,
instance, instance,
@ -255,7 +216,7 @@ class LearningSolver:
model = instance.to_model() model = instance.to_model()
self.tee = tee self.tee = tee
self.internal_solver = self._create_internal_solver() self.internal_solver = self.solver_factory()
self.internal_solver.set_instance(instance, model) self.internal_solver.set_instance(instance, model)
if self.solve_lp_first: if self.solve_lp_first:

@ -24,9 +24,6 @@ def test_learning_solver():
logger.info("Solver: %s" % internal_solver) logger.info("Solver: %s" % internal_solver)
instance = _get_instance(internal_solver) instance = _get_instance(internal_solver)
solver = LearningSolver( solver = LearningSolver(
time_limit=300,
gap_tolerance=1e-3,
threads=1,
solver=internal_solver, solver=internal_solver,
mode=mode, mode=mode,
) )
@ -115,7 +112,7 @@ def test_solve_fit_from_disk():
def test_simulate_perfect(): def test_simulate_perfect():
internal_solver = GurobiSolver() internal_solver = GurobiSolver
instance = _get_instance(internal_solver) instance = _get_instance(internal_solver)
with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as tmp: with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as tmp:
pickle.dump(instance, tmp) pickle.dump(instance, tmp)
@ -124,6 +121,5 @@ def test_simulate_perfect():
solver=internal_solver, solver=internal_solver,
simulate_perfect=True, simulate_perfect=True,
) )
stats = solver.solve(tmp.name) stats = solver.solve(tmp.name)
assert stats["Lower bound"] == stats["Predicted LB"] assert stats["Lower bound"] == stats["Predicted LB"]

Loading…
Cancel
Save