mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 01:18:52 -06:00
Require a callable as the internal solver
This commit is contained in:
@@ -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.time_limit = time_limit
|
|
||||||
self.gap_tolerance = gap_tolerance
|
|
||||||
self.tee = False
|
|
||||||
self.node_limit = node_limit
|
|
||||||
self.solve_lp_first = solve_lp_first
|
|
||||||
self.use_lazy_cb = use_lazy_cb
|
self.use_lazy_cb = use_lazy_cb
|
||||||
|
self.tee = False
|
||||||
|
self.solve_lp_first = solve_lp_first
|
||||||
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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user