From ffc77075f5294d4c62fb594ae2ca6811b37a9eff Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Tue, 19 Jan 2021 21:21:39 -0600 Subject: [PATCH] Require a callable as the internal solver --- docs/customization.md | 13 ++-- .../steps/tests/test_convert_tight.py | 8 +- miplearn/problems/tests/test_tsp.py | 50 ++++++------ miplearn/solvers/learning.py | 77 +++++-------------- .../solvers/tests/test_learning_solver.py | 6 +- 5 files changed, 56 insertions(+), 98 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 93126d4..657d4cc 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -4,13 +4,16 @@ ### 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 -from miplearn import LearningSolver -solver = LearningSolver(solver="cplex", - time_limit=300, - gap_tolerance=1e-3) +from miplearn import LearningSolver, CplexPyomoSolver +solver = LearningSolver(solver=CplexPyomoSolver) ``` ## Customizing solver components diff --git a/miplearn/components/steps/tests/test_convert_tight.py b/miplearn/components/steps/tests/test_convert_tight.py index 43d62f4..35ca5bd 100644 --- a/miplearn/components/steps/tests/test_convert_tight.py +++ b/miplearn/components/steps/tests/test_convert_tight.py @@ -13,7 +13,7 @@ def test_convert_tight_usage(): capacity=16.0, ) solver = LearningSolver( - solver=GurobiSolver(), + solver=GurobiSolver, components=[ RelaxIntegralityStep(), ConvertTightIneqsIntoEqsStep(), @@ -64,7 +64,7 @@ def test_convert_tight_infeasibility(): comp.classifiers["c3"].predict_proba = Mock(return_value=[[1, 0]]) solver = LearningSolver( - solver=GurobiSolver(params={}), + solver=GurobiSolver, components=[comp], solve_lp_first=False, ) @@ -87,7 +87,7 @@ def test_convert_tight_suboptimality(): comp.classifiers["c3"].predict_proba = Mock(return_value=[[0, 1]]) solver = LearningSolver( - solver=GurobiSolver(params={}), + solver=GurobiSolver, components=[comp], solve_lp_first=False, ) @@ -110,7 +110,7 @@ def test_convert_tight_optimal(): comp.classifiers["c3"].predict_proba = Mock(return_value=[[0, 1]]) solver = LearningSolver( - solver=GurobiSolver(params={}), + solver=GurobiSolver, components=[comp], solve_lp_first=False, ) diff --git a/miplearn/problems/tests/test_tsp.py b/miplearn/problems/tests/test_tsp.py index 80eb54d..089c488 100644 --- a/miplearn/problems/tests/test_tsp.py +++ b/miplearn/problems/tests/test_tsp.py @@ -36,18 +36,17 @@ def test_instance(): ] ) instance = TravelingSalesmanInstance(n_cities, distances) - for solver_name in ["gurobi"]: - solver = LearningSolver(solver=solver_name) - solver.solve(instance) - x = instance.solution["x"] - assert x[0, 1] == 1.0 - assert x[0, 2] == 0.0 - assert x[0, 3] == 1.0 - assert x[1, 2] == 1.0 - assert x[1, 3] == 0.0 - assert x[2, 3] == 1.0 - assert instance.lower_bound == 4.0 - assert instance.upper_bound == 4.0 + solver = LearningSolver() + solver.solve(instance) + x = instance.solution["x"] + assert x[0, 1] == 1.0 + assert x[0, 2] == 0.0 + assert x[0, 3] == 1.0 + assert x[1, 2] == 1.0 + assert x[1, 3] == 0.0 + assert x[2, 3] == 1.0 + assert instance.lower_bound == 4.0 + assert instance.upper_bound == 4.0 def test_subtour(): @@ -64,17 +63,16 @@ def test_subtour(): ) distances = squareform(pdist(cities)) instance = TravelingSalesmanInstance(n_cities, distances) - for solver_name in ["gurobi"]: - solver = LearningSolver(solver=solver_name) - solver.solve(instance) - assert hasattr(instance, "found_violated_lazy_constraints") - assert hasattr(instance, "found_violated_user_cuts") - x = instance.solution["x"] - assert x[0, 1] == 1.0 - assert x[0, 4] == 1.0 - assert x[1, 2] == 1.0 - assert x[2, 3] == 1.0 - assert x[3, 5] == 1.0 - assert x[4, 5] == 1.0 - solver.fit([instance]) - solver.solve(instance) + solver = LearningSolver() + solver.solve(instance) + assert hasattr(instance, "found_violated_lazy_constraints") + assert hasattr(instance, "found_violated_user_cuts") + x = instance.solution["x"] + assert x[0, 1] == 1.0 + assert x[0, 4] == 1.0 + assert x[1, 2] == 1.0 + assert x[2, 3] == 1.0 + assert x[3, 5] == 1.0 + assert x[4, 5] == 1.0 + solver.fit([instance]) + solver.solve(instance) diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index cc56f64..e70e90a 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -20,8 +20,8 @@ from .. import ( DynamicLazyConstraintsComponent, UserCutsComponent, ) -from .pyomo.cplex import CplexPyomoSolver -from .pyomo.gurobi import GurobiPyomoSolver +from ..solvers.internal import InternalSolver +from ..solvers.pyomo.gurobi import GurobiPyomoSolver logger = logging.getLogger(__name__) @@ -52,33 +52,22 @@ class LearningSolver: Parameters ---------- - components + components: [Component] Set of components in the solver. By default, includes: - ObjectiveValueComponent - PrimalSolutionComponent - DynamicLazyConstraintsComponent - UserCutsComponent - gap_tolerance - Relative MIP gap tolerance. By default, 1e-4. - mode + mode: str If "exact", solves problem to optimality, keeping all optimality guarantees provided by the MIP solver. If "heuristic", uses machine learning more aggressively, and may return suboptimal solutions. - solver - The internal MIP solver to use. Can be either "cplex", "gurobi", a - solver class such as GurobiSolver, or a solver instance such as - GurobiSolver(). - threads - Maximum number of threads to use. If None, uses solver default. - 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. + solver: Callable[[], InternalSolver] + A callable that constructs the internal solver. If None is provided, + use GurobiPyomoSolver. + use_lazy_cb: bool + If true, use native solver callbacks for enforcing lazy constraints, + instead of a simple loop. May not be supported by all solvers. solve_lp_first: bool If true, solve LP relaxation first, then solve original MILP. This option should be activated if the LP relaxation is not very @@ -94,27 +83,23 @@ class LearningSolver: def __init__( self, components=None, - gap_tolerance=1e-4, mode="exact", - solver="gurobi", - threads=None, - time_limit=None, - node_limit=None, - solve_lp_first=True, + solver=None, use_lazy_cb=False, + solve_lp_first=True, simulate_perfect=False, ): + if solver is None: + solver = GurobiPyomoSolver + assert callable(solver), f"Callable expected. Found {solver.__class__} instead." + self.components = {} self.mode = mode self.internal_solver = None - self.internal_solver_factory = solver - self.threads = threads - self.time_limit = time_limit - self.gap_tolerance = gap_tolerance + self.solver_factory = solver + self.use_lazy_cb = use_lazy_cb self.tee = False - self.node_limit = node_limit self.solve_lp_first = solve_lp_first - self.use_lazy_cb = use_lazy_cb self.simulate_perfect = simulate_perfect if components is not None: @@ -130,30 +115,6 @@ class LearningSolver: for component in self.components.values(): 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( self, instance, @@ -255,7 +216,7 @@ class LearningSolver: model = instance.to_model() self.tee = tee - self.internal_solver = self._create_internal_solver() + self.internal_solver = self.solver_factory() self.internal_solver.set_instance(instance, model) if self.solve_lp_first: diff --git a/miplearn/solvers/tests/test_learning_solver.py b/miplearn/solvers/tests/test_learning_solver.py index 6bd6841..faa9ae8 100644 --- a/miplearn/solvers/tests/test_learning_solver.py +++ b/miplearn/solvers/tests/test_learning_solver.py @@ -24,9 +24,6 @@ def test_learning_solver(): logger.info("Solver: %s" % internal_solver) instance = _get_instance(internal_solver) solver = LearningSolver( - time_limit=300, - gap_tolerance=1e-3, - threads=1, solver=internal_solver, mode=mode, ) @@ -115,7 +112,7 @@ def test_solve_fit_from_disk(): def test_simulate_perfect(): - internal_solver = GurobiSolver() + internal_solver = GurobiSolver instance = _get_instance(internal_solver) with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as tmp: pickle.dump(instance, tmp) @@ -124,6 +121,5 @@ def test_simulate_perfect(): solver=internal_solver, simulate_perfect=True, ) - stats = solver.solve(tmp.name) assert stats["Lower bound"] == stats["Predicted LB"]