diff --git a/miplearn/benchmark.py b/miplearn/benchmark.py index 8b5bcbd..940cbb0 100644 --- a/miplearn/benchmark.py +++ b/miplearn/benchmark.py @@ -18,15 +18,21 @@ class BenchmarkRunner: solver.load(filename) solver.fit() - def parallel_solve(self, instances, n_jobs=1): + def parallel_solve(self, instances, n_jobs=1, n_trials=1): if self.results is None: self.results = pd.DataFrame(columns=["Solver", "Instance", "Wallclock Time", - "Obj Value", + "Lower Bound", + "Upper Bound", + "Gap", + "Nodes", ]) + instances = instances * n_trials for (name, solver) in self.solvers.items(): - results = solver.parallel_solve(instances, n_jobs=n_jobs, label=name) + results = solver.parallel_solve(instances, + n_jobs=n_jobs, + label=name) for i in range(len(instances)): wallclock_time = None for key in ["Time", "Wall time", "Wallclock time"]: @@ -35,19 +41,35 @@ class BenchmarkRunner: if str(results[i]["Solver"][0][key]) == "": continue wallclock_time = float(results[i]["Solver"][0][key]) + nodes = results[i]["Solver"][0]["Nodes"] + lb = results[i]["Problem"][0]["Lower bound"] + ub = results[i]["Problem"][0]["Upper bound"] + gap = (ub - lb) / lb self.results = self.results.append({ "Solver": name, "Instance": i, "Wallclock Time": wallclock_time, - "Obj Value": results[i]["Problem"][0]["Lower bound"] + "Lower Bound": lb, + "Upper Bound": ub, + "Gap": gap, + "Nodes": nodes, }, ignore_index=True) groups = self.results.groupby("Instance") - best_obj_value = groups["Obj Value"].transform("max") + best_lower_bound = groups["Lower Bound"].transform("max") + best_upper_bound = groups["Upper Bound"].transform("min") + best_gap = groups["Gap"].transform("min") + best_nodes = groups["Nodes"].transform("min") best_wallclock_time = groups["Wallclock Time"].transform("min") - self.results["Relative Obj Value"] = \ - self.results["Obj Value"] / best_obj_value + self.results["Relative Lower Bound"] = \ + self.results["Lower Bound"] / best_lower_bound + self.results["Relative Upper Bound"] = \ + self.results["Upper Bound"] / best_upper_bound self.results["Relative Wallclock Time"] = \ self.results["Wallclock Time"] / best_wallclock_time + self.results["Relative Gap"] = \ + self.results["Gap"] / best_gap + self.results["Relative Nodes"] = \ + self.results["Nodes"] / best_nodes def raw_results(self): return self.results diff --git a/miplearn/solvers.py b/miplearn/solvers.py index 957fe70..465ccf6 100644 --- a/miplearn/solvers.py +++ b/miplearn/solvers.py @@ -3,19 +3,21 @@ # Written by Alinson S. Xavier from .transformers import PerVariableTransformer -from .warmstart import KnnWarmStartPredictor +from .warmstart import KnnWarmStartPredictor, LogisticWarmStartPredictor import pyomo.environ as pe import numpy as np from copy import copy, deepcopy import pickle from tqdm import tqdm from joblib import Parallel, delayed +from scipy.stats import randint import multiprocessing def _gurobi_factory(): solver = pe.SolverFactory('gurobi_persistent') solver.options["threads"] = 4 + solver.options["Seed"] = randint(low=0, high=1000).rvs() return solver class LearningSolver: @@ -27,7 +29,8 @@ class LearningSolver: def __init__(self, threads=4, internal_solver_factory=_gurobi_factory, - ws_predictor=KnnWarmStartPredictor(), + ws_predictor=LogisticWarmStartPredictor(), + branch_priority=None, mode="exact"): self.internal_solver_factory = internal_solver_factory self.internal_solver = self.internal_solver_factory() @@ -36,10 +39,14 @@ class LearningSolver: self.y_train = {} self.ws_predictors = {} self.ws_predictor_prototype = ws_predictor + self.branch_priority = branch_priority def solve(self, instance, tee=False): - # Convert instance into concrete model + # Load model into solver model = instance.to_model() + is_solver_persistent = hasattr(self.internal_solver, "set_instance") + if is_solver_persistent: + self.internal_solver.set_instance(model) # Split decision variables according to their category transformer = PerVariableTransformer() @@ -56,10 +63,11 @@ class LearningSolver: else: self.x_train[category] = np.vstack([self.x_train[category], x]) - # Predict warm start for category in var_split.keys(): + var_index_pairs = var_split[category] + + # Predict warm starts if category in self.ws_predictors.keys(): - var_index_pairs = var_split[category] ws = self.ws_predictors[category].predict(x_test[category]) assert ws.shape == (len(var_index_pairs), 2) for i in range(len(var_index_pairs)): @@ -75,9 +83,23 @@ class LearningSolver: elif ws[i,1] == 1: var[index].value = 1 - # Solve MILP - solve_results = self._solve(model, tee=tee) - + # Set custom branch priority + if self.branch_priority is not None: + assert is_solver_persistent + from gurobipy import GRB + for (i, (var, index)) in enumerate(var_index_pairs): + gvar = self.internal_solver._pyomo_var_to_solver_var_map[var[index]] + #priority = randint(low=0, high=1000).rvs() + gvar.setAttr(GRB.Attr.BranchPriority, self.branch_priority[index]) + + if is_solver_persistent: + solve_results = self.internal_solver.solve(tee=tee, warmstart=True) + else: + solve_results = self.internal_solver.solve(model, tee=tee, warmstart=True) + + solve_results["Solver"][0]["Nodes"] = self.internal_solver._solver_model.getAttr("NodeCount") + + # Update y_train for category in var_split.keys(): var_index_pairs = var_split[category] @@ -113,7 +135,7 @@ class LearningSolver: results = Parallel(n_jobs=n_jobs)( delayed(_process)(instance) - for instance in tqdm(instances, desc=label) + for instance in tqdm(instances, desc=label, ncols=80) ) x_train, y_train, results = _merge(results) @@ -148,10 +170,3 @@ class LearningSolver: self.x_train = data["x_train"] self.y_train = data["y_train"] self.ws_predictors = self.ws_predictors - - def _solve(self, model, tee=False): - if hasattr(self.internal_solver, "set_instance"): - self.internal_solver.set_instance(model) - return self.internal_solver.solve(tee=tee, warmstart=True) - else: - return self.internal_solver.solve(model, tee=tee, warmstart=True) diff --git a/miplearn/tests/test_benchmark.py b/miplearn/tests/test_benchmark.py index c4bf6b0..26850dd 100644 --- a/miplearn/tests/test_benchmark.py +++ b/miplearn/tests/test_benchmark.py @@ -28,12 +28,12 @@ def test_benchmark(): } benchmark = BenchmarkRunner(test_solvers) benchmark.load_fit("data.bin") - benchmark.parallel_solve(test_instances, n_jobs=2) - assert benchmark.raw_results().values.shape == (6,6) + benchmark.parallel_solve(test_instances, n_jobs=2, n_trials=2) + assert benchmark.raw_results().values.shape == (12,12) benchmark.save_results("/tmp/benchmark.csv") assert os.path.isfile("/tmp/benchmark.csv") benchmark = BenchmarkRunner(test_solvers) benchmark.load_results("/tmp/benchmark.csv") - assert benchmark.raw_results().values.shape == (6,6) + assert benchmark.raw_results().values.shape == (12,12) diff --git a/miplearn/tests/test_solver.py b/miplearn/tests/test_solver.py index 00cd230..9655d7d 100644 --- a/miplearn/tests/test_solver.py +++ b/miplearn/tests/test_solver.py @@ -40,4 +40,11 @@ def test_parallel_solve(): solver = LearningSolver() solver.parallel_solve(instances, n_jobs=3) assert len(solver.x_train[0]) == 10 - assert len(solver.y_train[0]) == 10 \ No newline at end of file + assert len(solver.y_train[0]) == 10 + +def test_solver_random_branch_priority(): + instance = KnapsackInstance2(weights=[23., 26., 20., 18.], + prices=[505., 352., 458., 220.], + capacity=67.) + solver = LearningSolver(branch_priority=[1, 2, 3, 4]) + solver.solve(instance, tee=True) \ No newline at end of file