# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. from .component import Component from ..extractors import Extractor from abc import ABC, abstractmethod from sklearn.neighbors import KNeighborsRegressor import numpy as np from tqdm.auto import tqdm from joblib import Parallel, delayed import multiprocessing def _default_branch_priority_predictor(): return KNeighborsRegressor(n_neighbors=1) class BranchPriorityComponent(Component): def __init__(self, node_limit=10_000, predictor=_default_branch_priority_predictor, ): self.pending_instances = [] self.x_train = {} self.y_train = {} self.predictors = {} self.node_limit = node_limit self.predictor_factory = predictor def before_solve(self, solver, instance, model): assert solver.internal_solver.name == "gurobi_persistent", "Only GurobiPersistent is currently supported" from gurobipy import GRB var_split = Extractor.split_variables(instance, model) for category in var_split.keys(): if category not in self.predictors.keys(): continue var_index_pairs = var_split[category] for (i, (var, index)) in enumerate(var_index_pairs): x = self._build_x(instance, var, index) y = self.predictors[category].predict([x])[0][0] gvar = solver.internal_solver._pyomo_var_to_solver_var_map[var[index]] gvar.setAttr(GRB.Attr.BranchPriority, int(round(y))) def after_solve(self, solver, instance, model): self.pending_instances += [instance] def fit(self, solver, n_jobs=1): def _process(instance): # Create LP file import subprocess, tempfile, os, sys lp_file = tempfile.NamedTemporaryFile(suffix=".lp") priority_file = tempfile.NamedTemporaryFile() model = instance.to_model() model.write(lp_file.name) # Run Julia script src_dirname = os.path.dirname(os.path.realpath(__file__)) priority_file = tempfile.NamedTemporaryFile(mode="r") subprocess.run(["julia", "%s/branching.jl" % src_dirname, lp_file.name, priority_file.name, str(self.node_limit), ], check=True, ) # Parse output tokens = [line.strip().split(",") for line in priority_file.readlines()] lp_varname_to_priority = {t[0]: int(t[1]) for t in tokens} # Map priorities back to Pyomo variables pyomo_var_to_priority = {} from pyomo.core import Var from pyomo.core.base.label import TextLabeler labeler = TextLabeler() symbol_map = list(model.solutions.symbol_map.values())[0] # Build x_train and y_train comp = BranchPriorityComponent() for var in model.component_objects(Var): for index in var: category = instance.get_variable_category(var, index) if category is None: continue lp_varname = symbol_map.getSymbol(var[index], labeler) var_priority = lp_varname_to_priority[lp_varname] x = self._build_x(instance, var, index) y = np.array([var_priority]) if category not in comp.x_train.keys(): comp.x_train[category] = np.array([x]) comp.y_train[category] = np.array([y]) else: comp.x_train[category] = np.vstack([comp.x_train[category], x]) comp.y_train[category] = np.vstack([comp.y_train[category], y]) return comp # Run strong branching on pending instances subcomponents = Parallel(n_jobs=n_jobs)( delayed(_process)(instance) for instance in tqdm(self.pending_instances, desc="Branch priority") ) self.merge(subcomponents) self.pending_instances.clear() # Retrain ML predictors for category in self.x_train.keys(): x_train = self.x_train[category] y_train = self.y_train[category] self.predictors[category] = self.predictor_factory() self.predictors[category].fit(x_train, y_train) def _build_x(self, instance, var, index): instance_features = instance.get_instance_features() var_features = instance.get_variable_features(var, index) return np.hstack([instance_features, var_features]) def merge(self, other_components): keys = set(self.x_train.keys()) for comp in other_components: self.pending_instances += comp.pending_instances keys = keys.union(set(comp.x_train.keys())) # Merge x_train and y_train for key in keys: x_train_submatrices = [comp.x_train[key] for comp in other_components if key in comp.x_train.keys()] y_train_submatrices = [comp.y_train[key] for comp in other_components if key in comp.y_train.keys()] if key in self.x_train.keys(): x_train_submatrices += [self.x_train[key]] y_train_submatrices += [self.y_train[key]] self.x_train[key] = np.vstack(x_train_submatrices) self.y_train[key] = np.vstack(y_train_submatrices) # Merge trained ML predictors for comp in other_components: for key in comp.predictors.keys(): if key not in self.predictors.keys(): self.predictors[key] = comp.predictors[key]