Merge branch 'feature/training_sample' into dev

master
Alinson S. Xavier 5 years ago
commit 05497cab07

@ -3,3 +3,4 @@ ignore_missing_imports = True
#disallow_untyped_defs = True #disallow_untyped_defs = True
disallow_untyped_calls = True disallow_untyped_calls = True
disallow_incomplete_defs = True disallow_incomplete_defs = True
pretty = True

@ -2,37 +2,31 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from .extractors import ( from .benchmark import BenchmarkRunner
SolutionExtractor, from .classifiers import Classifier, Regressor
InstanceFeaturesExtractor, from .classifiers.adaptive import AdaptiveClassifier
ObjectiveValueExtractor, from .classifiers.threshold import MinPrecisionThreshold
VariableFeaturesExtractor,
)
from .components.component import Component from .components.component import Component
from .components.objective import ObjectiveValueComponent from .components.cuts import UserCutsComponent
from .components.lazy_dynamic import DynamicLazyConstraintsComponent from .components.lazy_dynamic import DynamicLazyConstraintsComponent
from .components.lazy_static import StaticLazyConstraintsComponent from .components.lazy_static import StaticLazyConstraintsComponent
from .components.cuts import UserCutsComponent from .components.objective import ObjectiveValueComponent
from .components.primal import PrimalSolutionComponent from .components.primal import PrimalSolutionComponent
from .components.relaxation import RelaxationComponent from .components.relaxation import RelaxationComponent
from .components.steps.convert_tight import ConvertTightIneqsIntoEqsStep from .components.steps.convert_tight import ConvertTightIneqsIntoEqsStep
from .components.steps.relax_integrality import RelaxIntegralityStep
from .components.steps.drop_redundant import DropRedundantInequalitiesStep from .components.steps.drop_redundant import DropRedundantInequalitiesStep
from .components.steps.relax_integrality import RelaxIntegralityStep
from .classifiers import Classifier, Regressor from .extractors import (
from .classifiers.adaptive import AdaptiveClassifier SolutionExtractor,
from .classifiers.threshold import MinPrecisionThreshold InstanceFeaturesExtractor,
ObjectiveValueExtractor,
from .benchmark import BenchmarkRunner VariableFeaturesExtractor,
)
from .instance import Instance from .instance import Instance
from .log import setup_logger
from .solvers.pyomo.base import BasePyomoSolver
from .solvers.pyomo.cplex import CplexPyomoSolver
from .solvers.pyomo.gurobi import GurobiPyomoSolver
from .solvers.gurobi import GurobiSolver from .solvers.gurobi import GurobiSolver
from .solvers.internal import InternalSolver from .solvers.internal import InternalSolver
from .solvers.learning import LearningSolver from .solvers.learning import LearningSolver
from .solvers.pyomo.base import BasePyomoSolver
from .log import setup_logger from .solvers.pyomo.cplex import CplexPyomoSolver
from .solvers.pyomo.gurobi import GurobiPyomoSolver

@ -2,15 +2,14 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import logging
import os
from copy import deepcopy from copy import deepcopy
import pandas as pd import pandas as pd
import numpy as np
import logging
from tqdm.auto import tqdm from tqdm.auto import tqdm
import os
from .solvers.learning import LearningSolver from miplearn.solvers.learning import LearningSolver
class BenchmarkRunner: class BenchmarkRunner:

@ -5,14 +5,15 @@
import logging import logging
from copy import deepcopy from copy import deepcopy
from miplearn.classifiers import Classifier
from miplearn.classifiers.counting import CountingClassifier
from miplearn.classifiers.evaluator import ClassifierEvaluator
from sklearn.linear_model import LogisticRegression from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import StandardScaler
from miplearn.classifiers import Classifier
from miplearn.classifiers.counting import CountingClassifier
from miplearn.classifiers.evaluator import ClassifierEvaluator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -2,9 +2,10 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from miplearn.classifiers import Classifier
import numpy as np import numpy as np
from miplearn.classifiers import Classifier
class CountingClassifier(Classifier): class CountingClassifier(Classifier):
""" """

@ -2,15 +2,15 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import logging
from copy import deepcopy from copy import deepcopy
import numpy as np import numpy as np
from miplearn.classifiers import Classifier
from sklearn.dummy import DummyClassifier from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score from sklearn.model_selection import cross_val_score
import logging from miplearn.classifiers import Classifier
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -1,11 +1,12 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from miplearn.classifiers.counting import CountingClassifier
import numpy as np import numpy as np
from numpy.linalg import norm from numpy.linalg import norm
from miplearn.classifiers.counting import CountingClassifier
E = 0.1 E = 0.1

@ -3,11 +3,12 @@
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import numpy as np import numpy as np
from miplearn.classifiers.cv import CrossValidatedClassifier
from numpy.linalg import norm from numpy.linalg import norm
from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC from sklearn.svm import SVC
from miplearn.classifiers.cv import CrossValidatedClassifier
E = 0.1 E = 0.1

@ -3,9 +3,10 @@
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import numpy as np import numpy as np
from miplearn.classifiers.evaluator import ClassifierEvaluator
from sklearn.neighbors import KNeighborsClassifier from sklearn.neighbors import KNeighborsClassifier
from miplearn.classifiers.evaluator import ClassifierEvaluator
def test_evaluator(): def test_evaluator():
clf_a = KNeighborsClassifier(n_neighbors=1) clf_a = KNeighborsClassifier(n_neighbors=1)

@ -5,6 +5,7 @@
from unittest.mock import Mock from unittest.mock import Mock
import numpy as np import numpy as np
from miplearn.classifiers import Classifier from miplearn.classifiers import Classifier
from miplearn.classifiers.threshold import MinPrecisionThreshold from miplearn.classifiers.threshold import MinPrecisionThreshold

@ -2,7 +2,7 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from miplearn import Component from miplearn.components.component import Component
class CompositeComponent(Component): class CompositeComponent(Component):

@ -2,14 +2,17 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import logging
import sys import sys
from copy import deepcopy from copy import deepcopy
import numpy as np
from tqdm.auto import tqdm
from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.counting import CountingClassifier
from miplearn.components import classifier_evaluation_dict from miplearn.components import classifier_evaluation_dict
from miplearn.components.component import Component
from .component import Component from miplearn.extractors import InstanceFeaturesExtractor
from ..extractors import *
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -2,14 +2,17 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import logging
import sys import sys
from copy import deepcopy from copy import deepcopy
import numpy as np
from tqdm.auto import tqdm
from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.counting import CountingClassifier
from miplearn.components import classifier_evaluation_dict from miplearn.components import classifier_evaluation_dict
from miplearn.components.component import Component
from .component import Component from miplearn.extractors import InstanceFeaturesExtractor, InstanceIterator
from ..extractors import *
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -2,12 +2,15 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import logging
import sys import sys
from copy import deepcopy from copy import deepcopy
import numpy as np
from tqdm.auto import tqdm
from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.counting import CountingClassifier
from .component import Component from miplearn.components.component import Component
from ..extractors import *
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -1,6 +1,12 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import logging
from copy import deepcopy
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import ( from sklearn.metrics import (
mean_squared_error, mean_squared_error,
explained_variance_score, explained_variance_score,
@ -9,11 +15,8 @@ from sklearn.metrics import (
r2_score, r2_score,
) )
from .. import Component, InstanceFeaturesExtractor, ObjectiveValueExtractor from miplearn.components.component import Component
from sklearn.linear_model import LinearRegression from miplearn.extractors import InstanceFeaturesExtractor, ObjectiveValueExtractor
from copy import deepcopy
import numpy as np
import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -75,7 +78,15 @@ class ObjectiveValueComponent(Component):
def evaluate(self, instances): def evaluate(self, instances):
y_pred = self.predict(instances) y_pred = self.predict(instances)
y_true = np.array([[inst.lower_bound, inst.upper_bound] for inst in instances]) y_true = np.array(
[
[
inst.training_data[0]["Lower bound"],
inst.training_data[0]["Upper bound"],
]
for inst in instances
]
)
y_true_lb, y_true_ub = y_true[:, 0], y_true[:, 1] y_true_lb, y_true_ub = y_true[:, 0], y_true[:, 1]
y_pred_lb, y_pred_ub = y_pred[:, 1], y_pred[:, 1] y_pred_lb, y_pred_ub = y_pred[:, 1], y_pred[:, 1]
ev = { ev = {

@ -68,7 +68,8 @@ class PrimalSolutionComponent(Component):
for label in [0, 1]: for label in [0, 1]:
y_train = solutions[category][:, label].astype(int) y_train = solutions[category][:, label].astype(int)
# If all samples are either positive or negative, make constant predictions # If all samples are either positive or negative, make constant
# predictions
y_avg = np.average(y_train) y_avg = np.average(y_train)
if y_avg < 0.001 or y_avg >= 0.999: if y_avg < 0.001 or y_avg >= 0.999:
self.classifiers[category, label] = round(y_avg) self.classifiers[category, label] = round(y_avg)
@ -130,7 +131,7 @@ class PrimalSolutionComponent(Component):
desc="Evaluate (primal)", desc="Evaluate (primal)",
): ):
instance = instances[instance_idx] instance = instances[instance_idx]
solution_actual = instance.solution solution_actual = instance.training_data[0]["Solution"]
solution_pred = self.predict(instance) solution_pred = self.predict(instance)
vars_all, vars_one, vars_zero = set(), set(), set() vars_all, vars_one, vars_zero = set(), set(), set()

@ -4,8 +4,8 @@
import logging import logging
from miplearn import Component
from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.counting import CountingClassifier
from miplearn.components.component import Component
from miplearn.components.composite import CompositeComponent from miplearn.components.composite import CompositeComponent
from miplearn.components.steps.convert_tight import ConvertTightIneqsIntoEqsStep from miplearn.components.steps.convert_tight import ConvertTightIneqsIntoEqsStep
from miplearn.components.steps.drop_redundant import DropRedundantInequalitiesStep from miplearn.components.steps.drop_redundant import DropRedundantInequalitiesStep

@ -3,17 +3,17 @@
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import logging import logging
import random
from copy import deepcopy from copy import deepcopy
import numpy as np import numpy as np
from tqdm import tqdm from tqdm import tqdm
import random
from ... import Component from miplearn.classifiers.counting import CountingClassifier
from ...classifiers.counting import CountingClassifier from miplearn.components import classifier_evaluation_dict
from ...components import classifier_evaluation_dict from miplearn.components.component import Component
from ...extractors import InstanceIterator from miplearn.components.steps.drop_redundant import DropRedundantInequalitiesStep
from .drop_redundant import DropRedundantInequalitiesStep from miplearn.extractors import InstanceIterator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -8,9 +8,9 @@ from copy import deepcopy
import numpy as np import numpy as np
from tqdm import tqdm from tqdm import tqdm
from miplearn import Component
from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.counting import CountingClassifier
from miplearn.components import classifier_evaluation_dict from miplearn.components import classifier_evaluation_dict
from miplearn.components.component import Component
from miplearn.components.lazy_static import LazyConstraint from miplearn.components.lazy_static import LazyConstraint
from miplearn.extractors import InstanceIterator from miplearn.extractors import InstanceIterator

@ -4,7 +4,7 @@
import logging import logging
from miplearn import Component from miplearn.components.component import Component
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -1,9 +1,12 @@
from miplearn import LearningSolver, GurobiSolver, Instance, Classifier from unittest.mock import Mock
from miplearn.classifiers import Classifier
from miplearn.components.steps.convert_tight import ConvertTightIneqsIntoEqsStep from miplearn.components.steps.convert_tight import ConvertTightIneqsIntoEqsStep
from miplearn.components.steps.relax_integrality import RelaxIntegralityStep from miplearn.components.steps.relax_integrality import RelaxIntegralityStep
from miplearn.instance import Instance
from miplearn.problems.knapsack import GurobiKnapsackInstance from miplearn.problems.knapsack import GurobiKnapsackInstance
from miplearn.solvers.gurobi import GurobiSolver
from unittest.mock import Mock from miplearn.solvers.learning import LearningSolver
def test_convert_tight_usage(): def test_convert_tight_usage():
@ -21,8 +24,8 @@ def test_convert_tight_usage():
) )
# Solve original problem # Solve original problem
solver.solve(instance) stats = solver.solve(instance)
original_upper_bound = instance.upper_bound original_upper_bound = stats["Upper bound"]
# Should collect training data # Should collect training data
assert instance.training_data[0]["slacks"]["eq_capacity"] == 0.0 assert instance.training_data[0]["slacks"]["eq_capacity"] == 0.0
@ -32,15 +35,14 @@ def test_convert_tight_usage():
stats = solver.solve(instance) stats = solver.solve(instance)
# Objective value should be the same # Objective value should be the same
assert instance.upper_bound == original_upper_bound assert stats["Upper bound"] == original_upper_bound
assert stats["ConvertTight: Inf iterations"] == 0 assert stats["ConvertTight: Inf iterations"] == 0
assert stats["ConvertTight: Subopt iterations"] == 0 assert stats["ConvertTight: Subopt iterations"] == 0
class TestInstance(Instance): class SampleInstance(Instance):
def to_model(self): def to_model(self):
import gurobipy as grb import gurobipy as grb
from gurobipy import GRB
m = grb.Model("model") m = grb.Model("model")
x1 = m.addVar(name="x1") x1 = m.addVar(name="x1")
@ -68,9 +70,9 @@ def test_convert_tight_infeasibility():
components=[comp], components=[comp],
solve_lp_first=False, solve_lp_first=False,
) )
instance = TestInstance() instance = SampleInstance()
stats = solver.solve(instance) stats = solver.solve(instance)
assert instance.lower_bound == 5.0 assert stats["Upper bound"] == 5.0
assert stats["ConvertTight: Inf iterations"] == 1 assert stats["ConvertTight: Inf iterations"] == 1
assert stats["ConvertTight: Subopt iterations"] == 0 assert stats["ConvertTight: Subopt iterations"] == 0
@ -91,9 +93,9 @@ def test_convert_tight_suboptimality():
components=[comp], components=[comp],
solve_lp_first=False, solve_lp_first=False,
) )
instance = TestInstance() instance = SampleInstance()
stats = solver.solve(instance) stats = solver.solve(instance)
assert instance.lower_bound == 5.0 assert stats["Upper bound"] == 5.0
assert stats["ConvertTight: Inf iterations"] == 0 assert stats["ConvertTight: Inf iterations"] == 0
assert stats["ConvertTight: Subopt iterations"] == 1 assert stats["ConvertTight: Subopt iterations"] == 1
@ -114,8 +116,8 @@ def test_convert_tight_optimal():
components=[comp], components=[comp],
solve_lp_first=False, solve_lp_first=False,
) )
instance = TestInstance() instance = SampleInstance()
stats = solver.solve(instance) stats = solver.solve(instance)
assert instance.lower_bound == 5.0 assert stats["Upper bound"] == 5.0
assert stats["ConvertTight: Inf iterations"] == 0 assert stats["ConvertTight: Inf iterations"] == 0
assert stats["ConvertTight: Subopt iterations"] == 0 assert stats["ConvertTight: Subopt iterations"] == 0

@ -2,21 +2,15 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import numpy as np
from unittest.mock import Mock, call from unittest.mock import Mock, call
from miplearn import ( import numpy as np
LearningSolver,
Instance,
InternalSolver,
GurobiSolver,
)
from miplearn.classifiers import Classifier from miplearn.classifiers import Classifier
from miplearn.components.relaxation import ( from miplearn.components.relaxation import DropRedundantInequalitiesStep
DropRedundantInequalitiesStep, from miplearn.instance import Instance
RelaxIntegralityStep, from miplearn.solvers.internal import InternalSolver
) from miplearn.solvers.learning import LearningSolver
from miplearn.problems.knapsack import GurobiKnapsackInstance
def _setup(): def _setup():

@ -4,8 +4,10 @@
from unittest.mock import Mock, call from unittest.mock import Mock, call
from miplearn import Component, LearningSolver, Instance from miplearn.components.component import Component
from miplearn.components.composite import CompositeComponent from miplearn.components.composite import CompositeComponent
from miplearn.instance import Instance
from miplearn.solvers.learning import LearningSolver
def test_composite(): def test_composite():

@ -5,10 +5,13 @@
from unittest.mock import Mock from unittest.mock import Mock
import numpy as np import numpy as np
from miplearn import DynamicLazyConstraintsComponent, LearningSolver, InternalSolver from numpy.linalg import norm
from miplearn.classifiers import Classifier from miplearn.classifiers import Classifier
from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent
from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.learning import LearningSolver
from miplearn.tests import get_test_pyomo_instances from miplearn.tests import get_test_pyomo_instances
from numpy.linalg import norm
E = 0.1 E = 0.1

@ -4,13 +4,11 @@
from unittest.mock import Mock, call from unittest.mock import Mock, call
from miplearn import (
StaticLazyConstraintsComponent,
LearningSolver,
Instance,
InternalSolver,
)
from miplearn.classifiers import Classifier from miplearn.classifiers import Classifier
from miplearn.components.lazy_static import StaticLazyConstraintsComponent
from miplearn.instance import Instance
from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.learning import LearningSolver
def test_usage_with_solver(): def test_usage_with_solver():
@ -49,7 +47,9 @@ def test_usage_with_solver():
) )
component = StaticLazyConstraintsComponent( component = StaticLazyConstraintsComponent(
threshold=0.90, use_two_phase_gap=False, violation_tolerance=1.0 threshold=0.90,
use_two_phase_gap=False,
violation_tolerance=1.0,
) )
component.classifiers = { component.classifiers = {
"type-a": Mock(spec=Classifier), "type-a": Mock(spec=Classifier),

@ -5,8 +5,9 @@
from unittest.mock import Mock from unittest.mock import Mock
import numpy as np import numpy as np
from miplearn import ObjectiveValueComponent
from miplearn.classifiers import Regressor from miplearn.classifiers import Regressor
from miplearn.components.objective import ObjectiveValueComponent
from miplearn.tests import get_test_pyomo_instances from miplearn.tests import get_test_pyomo_instances
@ -14,8 +15,8 @@ def test_usage():
instances, models = get_test_pyomo_instances() instances, models = get_test_pyomo_instances()
comp = ObjectiveValueComponent() comp = ObjectiveValueComponent()
comp.fit(instances) comp.fit(instances)
assert instances[0].lower_bound == 1183.0 assert instances[0].training_data[0]["Lower bound"] == 1183.0
assert instances[0].upper_bound == 1183.0 assert instances[0].training_data[0]["Upper bound"] == 1183.0
assert np.round(comp.predict(instances), 2).tolist() == [ assert np.round(comp.predict(instances), 2).tolist() == [
[1183.0, 1183.0], [1183.0, 1183.0],
[1070.0, 1070.0], [1070.0, 1070.0],

@ -5,8 +5,9 @@
from unittest.mock import Mock from unittest.mock import Mock
import numpy as np import numpy as np
from miplearn import PrimalSolutionComponent
from miplearn.classifiers import Classifier from miplearn.classifiers import Classifier
from miplearn.components.primal import PrimalSolutionComponent
from miplearn.tests import get_test_pyomo_instances from miplearn.tests import get_test_pyomo_instances
@ -49,7 +50,7 @@ def test_evaluate():
comp = PrimalSolutionComponent(classifier=[clf_zero, clf_one], threshold=0.50) comp = PrimalSolutionComponent(classifier=[clf_zero, clf_one], threshold=0.50)
comp.fit(instances[:1]) comp.fit(instances[:1])
assert comp.predict(instances[0]) == {"x": {0: 0, 1: 0, 2: 1, 3: None}} assert comp.predict(instances[0]) == {"x": {0: 0, 1: 0, 2: 1, 3: None}}
assert instances[0].solution == {"x": {0: 1, 1: 0, 2: 1, 3: 1}} assert instances[0].training_data[0]["Solution"] == {"x": {0: 1, 1: 0, 2: 1, 3: 1}}
ev = comp.evaluate(instances[:1]) ev = comp.evaluate(instances[:1])
assert ev == { assert ev == {
"Fix one": { "Fix one": {

@ -2,14 +2,13 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import gzip
import logging import logging
import pickle import pickle
import gzip from abc import ABC, abstractmethod
import numpy as np import numpy as np
from tqdm.auto import tqdm from tqdm.auto import tqdm
from abc import ABC, abstractmethod
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -48,10 +47,10 @@ class Extractor(ABC):
@staticmethod @staticmethod
def split_variables(instance): def split_variables(instance):
assert hasattr(instance, "lp_solution")
result = {} result = {}
for var_name in instance.lp_solution: lp_solution = instance.training_data[0]["LP solution"]
for index in instance.lp_solution[var_name]: for var_name in lp_solution:
for index in lp_solution[var_name]:
category = instance.get_variable_category(var_name, index) category = instance.get_variable_category(var_name, index)
if category is None: if category is None:
continue continue
@ -71,6 +70,7 @@ class VariableFeaturesExtractor(Extractor):
): ):
instance_features = instance.get_instance_features() instance_features = instance.get_instance_features()
var_split = self.split_variables(instance) var_split = self.split_variables(instance)
lp_solution = instance.training_data[0]["LP solution"]
for (category, var_index_pairs) in var_split.items(): for (category, var_index_pairs) in var_split.items():
if category not in result: if category not in result:
result[category] = [] result[category] = []
@ -78,7 +78,7 @@ class VariableFeaturesExtractor(Extractor):
result[category] += [ result[category] += [
instance_features.tolist() instance_features.tolist()
+ instance.get_variable_features(var_name, index).tolist() + instance.get_variable_features(var_name, index).tolist()
+ [instance.lp_solution[var_name][index]] + [lp_solution[var_name][index]]
] ]
for category in result: for category in result:
result[category] = np.array(result[category]) result[category] = np.array(result[category])
@ -97,14 +97,15 @@ class SolutionExtractor(Extractor):
disable=len(instances) < 5, disable=len(instances) < 5,
): ):
var_split = self.split_variables(instance) var_split = self.split_variables(instance)
if self.relaxation:
solution = instance.training_data[0]["LP solution"]
else:
solution = instance.training_data[0]["Solution"]
for (category, var_index_pairs) in var_split.items(): for (category, var_index_pairs) in var_split.items():
if category not in result: if category not in result:
result[category] = [] result[category] = []
for (var_name, index) in var_index_pairs: for (var_name, index) in var_index_pairs:
if self.relaxation: v = solution[var_name][index]
v = instance.lp_solution[var_name][index]
else:
v = instance.solution[var_name][index]
if v is None: if v is None:
result[category] += [[0, 0]] result[category] += [[0, 0]]
else: else:
@ -121,7 +122,7 @@ class InstanceFeaturesExtractor(Extractor):
np.hstack( np.hstack(
[ [
instance.get_instance_features(), instance.get_instance_features(),
instance.lp_value, instance.training_data[0]["LP value"],
] ]
) )
for instance in InstanceIterator(instances) for instance in InstanceIterator(instances)
@ -137,13 +138,22 @@ class ObjectiveValueExtractor(Extractor):
def extract(self, instances): def extract(self, instances):
if self.kind == "lower bound": if self.kind == "lower bound":
return np.array( return np.array(
[[instance.lower_bound] for instance in InstanceIterator(instances)] [
[instance.training_data[0]["Lower bound"]]
for instance in InstanceIterator(instances)
]
) )
if self.kind == "upper bound": if self.kind == "upper bound":
return np.array( return np.array(
[[instance.upper_bound] for instance in InstanceIterator(instances)] [
[instance.training_data[0]["Upper bound"]]
for instance in InstanceIterator(instances)
]
) )
if self.kind == "lp": if self.kind == "lp":
return np.array( return np.array(
[[instance.lp_value] for instance in InstanceIterator(instances)] [
[instance.training_data[0]["LP value"]]
for instance in InstanceIterator(instances)
]
) )

@ -5,21 +5,28 @@
import gzip import gzip
import json import json
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any from typing import Any, List
import numpy as np import numpy as np
from miplearn.types import TrainingSample
class Instance(ABC): class Instance(ABC):
""" """
Abstract class holding all the data necessary to generate a concrete model of the problem. Abstract class holding all the data necessary to generate a concrete model of the
problem.
In the knapsack problem, for example, this class could hold the number of items, their weights
and costs, as well as the size of the knapsack. Objects implementing this class are able to In the knapsack problem, for example, this class could hold the number of items,
convert themselves into a concrete optimization model, which can be optimized by a solver, or their weights and costs, as well as the size of the knapsack. Objects
into arrays of features, which can be provided as inputs to machine learning models. implementing this class are able to convert themselves into a concrete
optimization model, which can be optimized by a solver, or into arrays of
features, which can be provided as inputs to machine learning models.
""" """
def __init__(self):
self.training_data: List[TrainingSample] = []
@abstractmethod @abstractmethod
def to_model(self) -> Any: def to_model(self) -> Any:
""" """
@ -29,21 +36,23 @@ class Instance(ABC):
def get_instance_features(self): def get_instance_features(self):
""" """
Returns a 1-dimensional Numpy array of (numerical) features describing the entire instance. Returns a 1-dimensional Numpy array of (numerical) features describing the
entire instance.
The array is used by LearningSolver to determine how similar two instances are. It may also The array is used by LearningSolver to determine how similar two instances
be used to predict, in combination with variable-specific features, the values of binary are. It may also be used to predict, in combination with variable-specific
decision variables in the problem. features, the values of binary decision variables in the problem.
There is not necessarily a one-to-one correspondence between models and instance features: There is not necessarily a one-to-one correspondence between models and
the features may encode only part of the data necessary to generate the complete model. instance features: the features may encode only part of the data necessary to
Features may also be statistics computed from the original data. For example, in the generate the complete model. Features may also be statistics computed from
knapsack problem, an implementation may decide to provide as instance features only the original data. For example, in the knapsack problem, an implementation
the average weights, average prices, number of items and the size of the knapsack. may decide to provide as instance features only the average weights, average
prices, number of items and the size of the knapsack.
The returned array MUST have the same length for all relevant instances of the problem. If The returned array MUST have the same length for all relevant instances of
two instances map into arrays of different lengths, they cannot be solved by the same the problem. If two instances map into arrays of different lengths,
LearningSolver object. they cannot be solved by the same LearningSolver object.
By default, returns [0]. By default, returns [0].
""" """
@ -51,20 +60,22 @@ class Instance(ABC):
def get_variable_features(self, var, index): def get_variable_features(self, var, index):
""" """
Returns a 1-dimensional array of (numerical) features describing a particular decision Returns a 1-dimensional array of (numerical) features describing a particular
variable. decision variable.
The argument `var` is a pyomo.core.Var object, which represents a collection of decision The argument `var` is a pyomo.core.Var object, which represents a collection
variables. The argument `index` specifies which variable in the collection is the relevant of decision variables. The argument `index` specifies which variable in the
one. collection is the relevant one.
In combination with instance features, variable features are used by LearningSolver to In combination with instance features, variable features are used by
predict, among other things, the optimal value of each decision variable before the LearningSolver to predict, among other things, the optimal value of each
optimization takes place. In the knapsack problem, for example, an implementation could decision variable before the optimization takes place. In the knapsack
provide as variable features the weight and the price of a specific item. problem, for example, an implementation could provide as variable features
the weight and the price of a specific item.
Like instance features, the arrays returned by this method MUST have the same length for Like instance features, the arrays returned by this method MUST have the same
all variables within the same category, for all relevant instances of the problem. length for all variables within the same category, for all relevant instances
of the problem.
By default, returns [0]. By default, returns [0].
""" """
@ -72,12 +83,12 @@ class Instance(ABC):
def get_variable_category(self, var, index): def get_variable_category(self, var, index):
""" """
Returns the category (a string, an integer or any hashable type) for each decision Returns the category (a string, an integer or any hashable type) for each
variable. decision variable.
If two variables have the same category, LearningSolver will use the same internal ML If two variables have the same category, LearningSolver will use the same
model to predict the values of both variables. If the returned category is None, ML internal ML model to predict the values of both variables. If the returned
models will ignore the variable. category is None, ML models will ignore the variable.
By default, returns "default". By default, returns "default".
""" """
@ -102,16 +113,16 @@ class Instance(ABC):
""" """
Returns lazy constraint violations found for the current solution. Returns lazy constraint violations found for the current solution.
After solving a model, LearningSolver will ask the instance to identify which lazy After solving a model, LearningSolver will ask the instance to identify which
constraints are violated by the current solution. For each identified violation, lazy constraints are violated by the current solution. For each identified
LearningSolver will then call the build_lazy_constraint, add the generated Pyomo violation, LearningSolver will then call the build_lazy_constraint, add the
constraint to the model, then resolve the problem. The process repeats until no further generated Pyomo constraint to the model, then resolve the problem. The
lazy constraint violations are found. process repeats until no further lazy constraint violations are found.
Each "violation" is simply a string, a tuple or any other hashable type which allows the Each "violation" is simply a string, a tuple or any other hashable type which
instance to identify unambiguously which lazy constraint should be generated. In the allows the instance to identify unambiguously which lazy constraint should be
Traveling Salesman Problem, for example, a subtour violation could be a frozen set generated. In the Traveling Salesman Problem, for example, a subtour
containing the cities in the subtour. violation could be a frozen set containing the cities in the subtour.
For a concrete example, see TravelingSalesmanInstance. For a concrete example, see TravelingSalesmanInstance.
""" """
@ -121,15 +132,17 @@ class Instance(ABC):
""" """
Returns a Pyomo constraint which fixes a given violation. Returns a Pyomo constraint which fixes a given violation.
This method is typically called immediately after find_violated_lazy_constraints. The violation object This method is typically called immediately after
provided to this method is exactly the same object returned earlier by find_violated_lazy_constraints. find_violated_lazy_constraints. The violation object provided to this method
After some training, LearningSolver may decide to proactively build some lazy constraints is exactly the same object returned earlier by
at the beginning of the optimization process, before a solution is even available. In this find_violated_lazy_constraints. After some training, LearningSolver may
case, build_lazy_constraints will be called without a corresponding call to decide to proactively build some lazy constraints at the beginning of the
optimization process, before a solution is even available. In this case,
build_lazy_constraints will be called without a corresponding call to
find_violated_lazy_constraints. find_violated_lazy_constraints.
The implementation should not directly add the constraint to the model. The constraint The implementation should not directly add the constraint to the model. The
will be added by LearningSolver after the method returns. constraint will be added by LearningSolver after the method returns.
For a concrete example, see TravelingSalesmanInstance. For a concrete example, see TravelingSalesmanInstance.
""" """

@ -2,10 +2,9 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from datetime import timedelta
import logging import logging
import time
import sys import sys
import time
class TimeFormatter(logging.Formatter): class TimeFormatter(logging.Formatter):

@ -2,13 +2,13 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import miplearn
from miplearn import Instance
import numpy as np import numpy as np
import pyomo.environ as pe import pyomo.environ as pe
from scipy.stats import uniform, randint, bernoulli from scipy.stats import uniform, randint
from scipy.stats.distributions import rv_frozen from scipy.stats.distributions import rv_frozen
from miplearn.instance import Instance
class ChallengeA: class ChallengeA:
""" """
@ -56,6 +56,7 @@ class MultiKnapsackInstance(Instance):
""" """
def __init__(self, prices, capacities, weights): def __init__(self, prices, capacities, weights):
super().__init__()
assert isinstance(prices, np.ndarray) assert isinstance(prices, np.ndarray)
assert isinstance(capacities, np.ndarray) assert isinstance(capacities, np.ndarray)
assert isinstance(weights, np.ndarray) assert isinstance(weights, np.ndarray)
@ -241,6 +242,7 @@ class KnapsackInstance(Instance):
""" """
def __init__(self, weights, prices, capacity): def __init__(self, weights, prices, capacity):
super().__init__()
self.weights = weights self.weights = weights
self.prices = prices self.prices = prices
self.capacity = capacity self.capacity = capacity

@ -8,7 +8,7 @@ import pyomo.environ as pe
from scipy.stats import uniform, randint from scipy.stats import uniform, randint
from scipy.stats.distributions import rv_frozen from scipy.stats.distributions import rv_frozen
from miplearn import Instance from miplearn.instance import Instance
class ChallengeA: class ChallengeA:
@ -101,6 +101,7 @@ class MaxWeightStableSetInstance(Instance):
""" """
def __init__(self, graph, weights): def __init__(self, graph, weights):
super().__init__()
self.graph = graph self.graph = graph
self.weights = weights self.weights = weights

@ -2,10 +2,10 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from miplearn import LearningSolver
from miplearn.problems.knapsack import MultiKnapsackGenerator, MultiKnapsackInstance
from scipy.stats import uniform, randint
import numpy as np import numpy as np
from scipy.stats import uniform, randint
from miplearn.problems.knapsack import MultiKnapsackGenerator
def test_knapsack_generator(): def test_knapsack_generator():

@ -4,18 +4,19 @@
import networkx as nx import networkx as nx
import numpy as np import numpy as np
from miplearn import LearningSolver
from miplearn.problems.stab import MaxWeightStableSetInstance
from scipy.stats import uniform, randint from scipy.stats import uniform, randint
from miplearn.problems.stab import MaxWeightStableSetInstance
from miplearn.solvers.learning import LearningSolver
def test_stab(): def test_stab():
graph = nx.cycle_graph(5) graph = nx.cycle_graph(5)
weights = [1.0, 1.0, 1.0, 1.0, 1.0] weights = [1.0, 1.0, 1.0, 1.0, 1.0]
instance = MaxWeightStableSetInstance(graph, weights) instance = MaxWeightStableSetInstance(graph, weights)
solver = LearningSolver() solver = LearningSolver()
solver.solve(instance) stats = solver.solve(instance)
assert instance.lower_bound == 2.0 assert stats["Lower bound"] == 2.0
def test_stab_generator_fixed_graph(): def test_stab_generator_fixed_graph():

@ -2,13 +2,14 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from miplearn import LearningSolver
from miplearn.problems.tsp import TravelingSalesmanGenerator, TravelingSalesmanInstance
import numpy as np import numpy as np
from numpy.linalg import norm from numpy.linalg import norm
from scipy.spatial.distance import pdist, squareform from scipy.spatial.distance import pdist, squareform
from scipy.stats import uniform, randint from scipy.stats import uniform, randint
from miplearn.problems.tsp import TravelingSalesmanGenerator, TravelingSalesmanInstance
from miplearn.solvers.learning import LearningSolver
def test_generator(): def test_generator():
instances = TravelingSalesmanGenerator( instances = TravelingSalesmanGenerator(
@ -37,16 +38,16 @@ def test_instance():
) )
instance = TravelingSalesmanInstance(n_cities, distances) instance = TravelingSalesmanInstance(n_cities, distances)
solver = LearningSolver() solver = LearningSolver()
solver.solve(instance) stats = solver.solve(instance)
x = instance.solution["x"] x = instance.training_data[0]["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 stats["Lower bound"] == 4.0
assert instance.upper_bound == 4.0 assert stats["Upper bound"] == 4.0
def test_subtour(): def test_subtour():
@ -67,7 +68,7 @@ def test_subtour():
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.training_data[0]["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

@ -2,14 +2,14 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import networkx as nx
import numpy as np import numpy as np
import pyomo.environ as pe import pyomo.environ as pe
from miplearn import Instance
from scipy.stats import uniform, randint
from scipy.spatial.distance import pdist, squareform from scipy.spatial.distance import pdist, squareform
from scipy.stats import uniform, randint
from scipy.stats.distributions import rv_frozen from scipy.stats.distributions import rv_frozen
import networkx as nx
import random from miplearn.instance import Instance
class ChallengeA: class ChallengeA:

@ -8,15 +8,15 @@ from io import StringIO
from random import randint from random import randint
from typing import List, Any, Dict, Union, Tuple, Optional from typing import List, Any, Dict, Union, Tuple, Optional
from . import RedirectOutput from miplearn.instance import Instance
from .internal import ( from miplearn.solvers import RedirectOutput
from miplearn.solvers.internal import (
InternalSolver, InternalSolver,
LPSolveStats, LPSolveStats,
IterationCallback, IterationCallback,
LazyCallback, LazyCallback,
MIPSolveStats, MIPSolveStats,
) )
from .. import Instance
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -181,6 +181,7 @@ class GurobiSolver(InternalSolver):
sense = "max" sense = "max"
lb = self.model.objVal lb = self.model.objVal
ub = self.model.objBound ub = self.model.objBound
ws_value = self._extract_warm_start_value(log)
stats: MIPSolveStats = { stats: MIPSolveStats = {
"Lower bound": lb, "Lower bound": lb,
"Upper bound": ub, "Upper bound": ub,
@ -188,10 +189,9 @@ class GurobiSolver(InternalSolver):
"Nodes": total_nodes, "Nodes": total_nodes,
"Sense": sense, "Sense": sense,
"Log": log, "Log": log,
"Warm start value": ws_value,
"LP value": None,
} }
ws_value = self._extract_warm_start_value(log)
if ws_value is not None:
stats["Warm start value"] = ws_value
return stats return stats
def get_solution(self) -> Dict: def get_solution(self) -> Dict:

@ -4,11 +4,15 @@
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Callable, Any, Dict, List from typing import Any, Dict, List
from typing_extensions import TypedDict from miplearn.instance import Instance
from miplearn.types import (
from ..instance import Instance LPSolveStats,
IterationCallback,
LazyCallback,
MIPSolveStats,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,33 +25,6 @@ class Constraint:
pass pass
LPSolveStats = TypedDict(
"LPSolveStats",
{
"Optimal value": float,
"Log": str,
},
)
MIPSolveStats = TypedDict(
"MIPSolveStats",
{
"Lower bound": float,
"Upper bound": float,
"Wallclock time": float,
"Nodes": float,
"Sense": str,
"Log": str,
"Warm start value": float,
},
total=False,
)
IterationCallback = Callable[[], bool]
LazyCallback = Callable[[Any, Any], None]
class InternalSolver(ABC): class InternalSolver(ABC):
""" """
Abstract class representing the MIP solver used internally by LearningSolver. Abstract class representing the MIP solver used internally by LearningSolver.

@ -2,26 +2,24 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import gzip
import logging import logging
import pickle
import os import os
import pickle
import tempfile import tempfile
import gzip
from copy import deepcopy from copy import deepcopy
from typing import Optional, List from typing import Optional, List, Any, IO, cast, BinaryIO, Union
from p_tqdm import p_map from p_tqdm import p_map
from tempfile import NamedTemporaryFile
from miplearn.components.cuts import UserCutsComponent
from . import RedirectOutput from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent
from .. import ( from miplearn.components.objective import ObjectiveValueComponent
ObjectiveValueComponent, from miplearn.components.primal import PrimalSolutionComponent
PrimalSolutionComponent, from miplearn.instance import Instance
DynamicLazyConstraintsComponent, from miplearn.solvers import RedirectOutput
UserCutsComponent, from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
) from miplearn.types import MIPSolveStats, TrainingSample
from ..solvers.internal import InternalSolver
from ..solvers.pyomo.gurobi import GurobiPyomoSolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -117,11 +115,11 @@ class LearningSolver:
def solve( def solve(
self, self,
instance, instance: Union[Instance, str],
model=None, model: Any = None,
output="", output: str = "",
tee=False, tee: bool = False,
): ) -> MIPSolveStats:
""" """
Solves the given instance. If trained machine-learning models are Solves the given instance. If trained machine-learning models are
available, they will be used to accelerate the solution process. available, they will be used to accelerate the solution process.
@ -129,20 +127,9 @@ class LearningSolver:
The argument `instance` may be either an Instance object or a The argument `instance` may be either an Instance object or a
filename pointing to a pickled Instance object. filename pointing to a pickled Instance object.
This method modifies the instance object. Specifically, the following This method adds a new training sample to `instance.training_sample`.
properties are set: If a filename is provided, then the file is modified in-place. That is,
the original file is overwritten.
- instance.lp_solution
- instance.lp_value
- instance.lower_bound
- instance.upper_bound
- instance.solution
- instance.solver_log
Additional solver components may set additional properties. Please
see their documentation for more details. If a filename is provided,
then the file is modified in-place. That is, the original file is
overwritten.
If `solver.solve_lp_first` is False, the properties lp_solution and If `solver.solve_lp_first` is False, the properties lp_solution and
lp_value will be set to dummy values. lp_value will be set to dummy values.
@ -192,46 +179,62 @@ class LearningSolver:
def _solve( def _solve(
self, self,
instance, instance: Union[Instance, str],
model=None, model: Any = None,
output="", output: str = "",
tee=False, tee: bool = False,
): ) -> MIPSolveStats:
# Load instance from file, if necessary
filename = None filename = None
fileformat = None fileformat = None
file: Union[BinaryIO, gzip.GzipFile]
if isinstance(instance, str): if isinstance(instance, str):
filename = instance filename = instance
logger.info("Reading: %s" % filename) logger.info("Reading: %s" % filename)
if filename.endswith(".gz"): if filename.endswith(".gz"):
fileformat = "pickle-gz" fileformat = "pickle-gz"
with gzip.GzipFile(filename, "rb") as file: with gzip.GzipFile(filename, "rb") as file:
instance = pickle.load(file) instance = pickle.load(cast(IO[bytes], file))
else: else:
fileformat = "pickle" fileformat = "pickle"
with open(filename, "rb") as file: with open(filename, "rb") as file:
instance = pickle.load(file) instance = pickle.load(cast(IO[bytes], file))
assert isinstance(instance, Instance)
# Generate model
if model is None: if model is None:
with RedirectOutput([]): with RedirectOutput([]):
model = instance.to_model() model = instance.to_model()
# Initialize training sample
training_sample: TrainingSample = {}
if not hasattr(instance, "training_data"):
instance.training_data = []
instance.training_data += [training_sample]
# Initialize internal solver
self.tee = tee self.tee = tee
self.internal_solver = self.solver_factory() self.internal_solver = self.solver_factory()
self.internal_solver.set_instance(instance, model) self.internal_solver.set_instance(instance, model)
# Solve linear relaxation
if self.solve_lp_first: if self.solve_lp_first:
logger.info("Solving LP relaxation...") logger.info("Solving LP relaxation...")
results = self.internal_solver.solve_lp(tee=tee) stats = self.internal_solver.solve_lp(tee=tee)
instance.lp_solution = self.internal_solver.get_solution() training_sample["LP solution"] = self.internal_solver.get_solution()
instance.lp_value = results["Optimal value"] training_sample["LP value"] = stats["Optimal value"]
training_sample["LP log"] = stats["Log"]
else: else:
instance.lp_solution = self.internal_solver.get_empty_solution() training_sample["LP solution"] = self.internal_solver.get_empty_solution()
instance.lp_value = 0.0 training_sample["LP value"] = 0
# Before-solve callbacks
logger.debug("Running before_solve callbacks...") logger.debug("Running before_solve callbacks...")
for component in self.components.values(): for component in self.components.values():
component.before_solve(self, instance, model) component.before_solve(self, instance, model)
# Define wrappers
def iteration_cb(): def iteration_cb():
should_repeat = False should_repeat = False
for comp in self.components.values(): for comp in self.components.values():
@ -247,29 +250,28 @@ class LearningSolver:
if self.use_lazy_cb: if self.use_lazy_cb:
lazy_cb = lazy_cb_wrapper lazy_cb = lazy_cb_wrapper
# Solve MILP
logger.info("Solving MILP...") logger.info("Solving MILP...")
stats = self.internal_solver.solve( stats = self.internal_solver.solve(
tee=tee, tee=tee,
iteration_cb=iteration_cb, iteration_cb=iteration_cb,
lazy_cb=lazy_cb, lazy_cb=lazy_cb,
) )
stats["LP value"] = instance.lp_value if "LP value" in training_sample.keys():
stats["LP value"] = training_sample["LP value"]
# Read MIP solution and bounds # Read MIP solution and bounds
instance.lower_bound = stats["Lower bound"] training_sample["Lower bound"] = stats["Lower bound"]
instance.upper_bound = stats["Upper bound"] training_sample["Upper bound"] = stats["Upper bound"]
instance.solver_log = stats["Log"] training_sample["MIP log"] = stats["Log"]
instance.solution = self.internal_solver.get_solution() training_sample["Solution"] = self.internal_solver.get_solution()
# After-solve callbacks
logger.debug("Calling after_solve callbacks...") logger.debug("Calling after_solve callbacks...")
training_data = {}
for component in self.components.values(): for component in self.components.values():
component.after_solve(self, instance, model, stats, training_data) component.after_solve(self, instance, model, stats, training_sample)
if not hasattr(instance, "training_data"):
instance.training_data = []
instance.training_data += [training_data]
# Write to file, if necessary
if filename is not None and output is not None: if filename is not None and output is not None:
output_filename = output output_filename = output
if len(output) == 0: if len(output) == 0:
@ -277,11 +279,10 @@ class LearningSolver:
logger.info("Writing: %s" % output_filename) logger.info("Writing: %s" % output_filename)
if fileformat == "pickle": if fileformat == "pickle":
with open(output_filename, "wb") as file: with open(output_filename, "wb") as file:
pickle.dump(instance, file) pickle.dump(instance, cast(IO[bytes], file))
else: else:
with gzip.GzipFile(output_filename, "wb") as file: with gzip.GzipFile(output_filename, "wb") as file:
pickle.dump(instance, file) pickle.dump(instance, cast(IO[bytes], file))
return stats return stats
def parallel_solve( def parallel_solve(
@ -340,7 +341,7 @@ class LearningSolver:
self._restore_miplearn_logger() self._restore_miplearn_logger()
return stats return stats
def fit(self, training_instances): def fit(self, training_instances: Union[List[str], List[Instance]]) -> None:
if len(training_instances) == 0: if len(training_instances) == 0:
return return
for component in self.components.values(): for component in self.components.values():

@ -12,15 +12,15 @@ import pyomo
from pyomo import environ as pe from pyomo import environ as pe
from pyomo.core import Var, Constraint from pyomo.core import Var, Constraint
from .. import RedirectOutput from miplearn.instance import Instance
from ..internal import ( from miplearn.solvers import RedirectOutput
from miplearn.solvers.internal import (
InternalSolver, InternalSolver,
LPSolveStats, LPSolveStats,
IterationCallback, IterationCallback,
LazyCallback, LazyCallback,
MIPSolveStats, MIPSolveStats,
) )
from ...instance import Instance
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -98,19 +98,18 @@ class BasePyomoSolver(InternalSolver):
if not should_repeat: if not should_repeat:
break break
log = streams[0].getvalue() log = streams[0].getvalue()
node_count = self._extract_node_count(log)
ws_value = self._extract_warm_start_value(log)
stats: MIPSolveStats = { stats: MIPSolveStats = {
"Lower bound": results["Problem"][0]["Lower bound"], "Lower bound": results["Problem"][0]["Lower bound"],
"Upper bound": results["Problem"][0]["Upper bound"], "Upper bound": results["Problem"][0]["Upper bound"],
"Wallclock time": total_wallclock_time, "Wallclock time": total_wallclock_time,
"Sense": self._obj_sense, "Sense": self._obj_sense,
"Log": log, "Log": log,
"Nodes": node_count,
"Warm start value": ws_value,
"LP value": None,
} }
node_count = self._extract_node_count(log)
ws_value = self._extract_warm_start_value(log)
if node_count is not None:
stats["Nodes"] = node_count
if ws_value is not None:
stats["Warm start value"] = ws_value
return stats return stats
def get_solution(self) -> Dict: def get_solution(self) -> Dict:

@ -5,7 +5,7 @@
from pyomo import environ as pe from pyomo import environ as pe
from scipy.stats import randint from scipy.stats import randint
from .base import BasePyomoSolver from miplearn.solvers.pyomo.base import BasePyomoSolver
class CplexPyomoSolver(BasePyomoSolver): class CplexPyomoSolver(BasePyomoSolver):

@ -7,7 +7,7 @@ import logging
from pyomo import environ as pe from pyomo import environ as pe
from scipy.stats import randint from scipy.stats import randint
from .base import BasePyomoSolver from miplearn.solvers.pyomo.base import BasePyomoSolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -7,7 +7,7 @@ import logging
from pyomo import environ as pe from pyomo import environ as pe
from scipy.stats import randint from scipy.stats import randint
from .base import BasePyomoSolver from miplearn.solvers.pyomo.base import BasePyomoSolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -5,15 +5,18 @@
from inspect import isclass from inspect import isclass
from typing import List, Callable from typing import List, Callable
from miplearn import BasePyomoSolver, GurobiSolver, GurobiPyomoSolver, InternalSolver
from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance
from miplearn.solvers.gurobi import GurobiSolver
from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.pyomo.base import BasePyomoSolver
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
from miplearn.solvers.pyomo.xpress import XpressPyomoSolver from miplearn.solvers.pyomo.xpress import XpressPyomoSolver
def _get_instance(solver): def _get_instance(solver):
def _is_subclass_or_instance(solver, parentClass): def _is_subclass_or_instance(obj, parent_class):
return isinstance(solver, parentClass) or ( return isinstance(obj, parent_class) or (
isclass(solver) and issubclass(solver, parentClass) isclass(obj) and issubclass(obj, parent_class)
) )
if _is_subclass_or_instance(solver, BasePyomoSolver): if _is_subclass_or_instance(solver, BasePyomoSolver):

@ -8,9 +8,10 @@ from warnings import warn
import pyomo.environ as pe import pyomo.environ as pe
from miplearn import BasePyomoSolver, GurobiSolver
from miplearn.solvers import RedirectOutput from miplearn.solvers import RedirectOutput
from . import _get_instance, _get_internal_solvers from miplearn.solvers.gurobi import GurobiSolver
from miplearn.solvers.pyomo.base import BasePyomoSolver
from miplearn.solvers.tests import _get_instance, _get_internal_solvers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -44,7 +45,7 @@ def test_internal_solver_warm_starts():
} }
) )
stats = solver.solve(tee=True) stats = solver.solve(tee=True)
if "Warm start value" in stats: if stats["Warm start value"] is not None:
assert stats["Warm start value"] == 725.0 assert stats["Warm start value"] == 725.0
else: else:
warn(f"{solver_class.__name__} should set warm start value") warn(f"{solver_class.__name__} should set warm start value")
@ -60,7 +61,7 @@ def test_internal_solver_warm_starts():
} }
) )
stats = solver.solve(tee=True) stats = solver.solve(tee=True)
assert "Warm start value" not in stats assert stats["Warm start value"] is None
solver.fix( solver.fix(
{ {

@ -4,8 +4,8 @@
import logging import logging
from . import _get_instance from miplearn.solvers.gurobi import GurobiSolver
from ... import GurobiSolver from miplearn.solvers.tests import _get_instance
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -7,13 +7,9 @@ import pickle
import tempfile import tempfile
import os import os
from miplearn import ( from miplearn.solvers.gurobi import GurobiSolver
LearningSolver, from miplearn.solvers.learning import LearningSolver
GurobiSolver, from miplearn.solvers.tests import _get_instance, _get_internal_solvers
DynamicLazyConstraintsComponent,
)
from . import _get_instance, _get_internal_solvers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,20 +25,19 @@ def test_learning_solver():
) )
solver.solve(instance) solver.solve(instance)
assert instance.solution["x"][0] == 1.0 data = instance.training_data[0]
assert instance.solution["x"][1] == 0.0 assert data["Solution"]["x"][0] == 1.0
assert instance.solution["x"][2] == 1.0 assert data["Solution"]["x"][1] == 0.0
assert instance.solution["x"][3] == 1.0 assert data["Solution"]["x"][2] == 1.0
assert instance.lower_bound == 1183.0 assert data["Solution"]["x"][3] == 1.0
assert instance.upper_bound == 1183.0 assert data["Lower bound"] == 1183.0
assert round(instance.lp_solution["x"][0], 3) == 1.000 assert data["Upper bound"] == 1183.0
assert round(instance.lp_solution["x"][1], 3) == 0.923 assert round(data["LP solution"]["x"][0], 3) == 1.000
assert round(instance.lp_solution["x"][2], 3) == 1.000 assert round(data["LP solution"]["x"][1], 3) == 0.923
assert round(instance.lp_solution["x"][3], 3) == 0.000 assert round(data["LP solution"]["x"][2], 3) == 1.000
assert round(instance.lp_value, 3) == 1287.923 assert round(data["LP solution"]["x"][3], 3) == 0.000
assert instance.found_violated_lazy_constraints == [] assert round(data["LP value"], 3) == 1287.923
assert instance.found_violated_user_cuts == [] assert len(data["MIP log"]) > 100
assert len(instance.solver_log) > 100
solver.fit([instance]) solver.fit([instance])
solver.solve(instance) solver.solve(instance)
@ -52,6 +47,19 @@ def test_learning_solver():
pickle.dump(solver, file) pickle.dump(solver, file)
def test_solve_without_lp():
for internal_solver in _get_internal_solvers():
logger.info("Solver: %s" % internal_solver)
instance = _get_instance(internal_solver)
solver = LearningSolver(
solver=internal_solver,
solve_lp_first=False,
)
solver.solve(instance)
solver.fit([instance])
solver.solve(instance)
def test_parallel_solve(): def test_parallel_solve():
for internal_solver in _get_internal_solvers(): for internal_solver in _get_internal_solvers():
instances = [_get_instance(internal_solver) for _ in range(10)] instances = [_get_instance(internal_solver) for _ in range(10)]
@ -59,7 +67,8 @@ def test_parallel_solve():
results = solver.parallel_solve(instances, n_jobs=3) results = solver.parallel_solve(instances, n_jobs=3)
assert len(results) == 10 assert len(results) == 10
for instance in instances: for instance in instances:
assert len(instance.solution["x"].keys()) == 4 data = instance.training_data[0]
assert len(data["Solution"]["x"].keys()) == 4
def test_solve_fit_from_disk(): def test_solve_fit_from_disk():
@ -77,14 +86,14 @@ def test_solve_fit_from_disk():
solver.solve(filenames[0]) solver.solve(filenames[0])
with open(filenames[0], "rb") as file: with open(filenames[0], "rb") as file:
instance = pickle.load(file) instance = pickle.load(file)
assert hasattr(instance, "solution") assert len(instance.training_data) > 0
# Test: parallel_solve # Test: parallel_solve
solver.parallel_solve(filenames) solver.parallel_solve(filenames)
for filename in filenames: for filename in filenames:
with open(filename, "rb") as file: with open(filename, "rb") as file:
instance = pickle.load(file) instance = pickle.load(file)
assert hasattr(instance, "solution") assert len(instance.training_data) > 0
# Test: solve (with specified output) # Test: solve (with specified output)
output = [f + ".out" for f in filenames] output = [f + ".out" for f in filenames]

@ -1,8 +1,9 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from miplearn import LearningSolver
from miplearn.problems.knapsack import KnapsackInstance from miplearn.problems.knapsack import KnapsackInstance
from miplearn.solvers.learning import LearningSolver
def get_test_pyomo_instances(): def get_test_pyomo_instances():

@ -4,10 +4,12 @@
import os.path import os.path
from miplearn import LearningSolver, BenchmarkRunner from miplearn.benchmark import BenchmarkRunner
from miplearn.problems.stab import MaxWeightStableSetGenerator from miplearn.problems.stab import MaxWeightStableSetGenerator
from scipy.stats import randint from scipy.stats import randint
from miplearn.solvers.learning import LearningSolver
def test_benchmark(): def test_benchmark():
# Generate training and test instances # Generate training and test instances

@ -1,16 +1,15 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import numpy as np
from miplearn.problems.knapsack import KnapsackInstance from miplearn.extractors import (
from miplearn import (
LearningSolver,
SolutionExtractor, SolutionExtractor,
InstanceFeaturesExtractor, InstanceFeaturesExtractor,
VariableFeaturesExtractor, VariableFeaturesExtractor,
) )
import numpy as np from miplearn.problems.knapsack import KnapsackInstance
import pyomo.environ as pe from miplearn.solvers.learning import LearningSolver
def _get_instances(): def _get_instances():

@ -0,0 +1,46 @@
# 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 typing import TypedDict, Optional, Dict, Callable, Any
TrainingSample = TypedDict(
"TrainingSample",
{
"LP log": str,
"LP solution": Dict,
"LP value": float,
"Lower bound": float,
"MIP log": str,
"Solution": Dict,
"Upper bound": float,
"slacks": Dict,
},
total=False,
)
LPSolveStats = TypedDict(
"LPSolveStats",
{
"Optimal value": float,
"Log": str,
},
)
MIPSolveStats = TypedDict(
"MIPSolveStats",
{
"Lower bound": Optional[float],
"Upper bound": Optional[float],
"Wallclock time": float,
"Nodes": Optional[int],
"Sense": str,
"Log": str,
"Warm start value": Optional[float],
"LP value": Optional[float],
},
)
IterationCallback = Callable[[], bool]
LazyCallback = Callable[[Any, Any], None]
Loading…
Cancel
Save