From 23dd311d75a721a990eab533f01d056656e9a4ba Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 20 Jan 2021 12:02:25 -0600 Subject: [PATCH] Reorganize imports; start moving data to instance.training_data --- .mypy.ini | 1 + miplearn/__init__.py | 40 ++++----- miplearn/benchmark.py | 7 +- miplearn/classifiers/adaptive.py | 7 +- miplearn/classifiers/counting.py | 3 +- miplearn/classifiers/cv.py | 4 +- miplearn/classifiers/tests/test_counting.py | 3 +- miplearn/classifiers/tests/test_cv.py | 3 +- miplearn/classifiers/tests/test_evaluator.py | 3 +- miplearn/classifiers/tests/test_threshold.py | 1 + miplearn/components/composite.py | 2 +- miplearn/components/cuts.py | 9 +- miplearn/components/lazy_dynamic.py | 9 +- miplearn/components/lazy_static.py | 7 +- miplearn/components/objective.py | 13 +-- miplearn/components/relaxation.py | 2 +- miplearn/components/steps/convert_tight.py | 12 +-- miplearn/components/steps/drop_redundant.py | 2 +- .../components/steps/relax_integrality.py | 2 +- .../steps/tests/test_convert_tight.py | 10 ++- .../steps/tests/test_drop_redundant.py | 18 ++-- miplearn/components/tests/test_composite.py | 4 +- .../components/tests/test_lazy_dynamic.py | 7 +- miplearn/components/tests/test_lazy_static.py | 14 +-- miplearn/components/tests/test_objective.py | 3 +- miplearn/components/tests/test_primal.py | 3 +- miplearn/instance.py | 7 +- miplearn/log.py | 3 +- miplearn/problems/knapsack.py | 8 +- miplearn/problems/stab.py | 3 +- miplearn/problems/tests/test_knapsack.py | 6 +- miplearn/problems/tests/test_stab.py | 5 +- miplearn/problems/tests/test_tsp.py | 5 +- miplearn/problems/tsp.py | 8 +- miplearn/solvers/gurobi.py | 12 +-- miplearn/solvers/internal.py | 41 ++------- miplearn/solvers/learning.py | 88 +++++++++++-------- miplearn/solvers/pyomo/base.py | 17 ++-- miplearn/solvers/pyomo/cplex.py | 2 +- miplearn/solvers/pyomo/gurobi.py | 2 +- miplearn/solvers/pyomo/xpress.py | 2 +- miplearn/solvers/tests/__init__.py | 11 ++- .../solvers/tests/test_internal_solver.py | 9 +- miplearn/solvers/tests/test_lazy_cb.py | 4 +- .../solvers/tests/test_learning_solver.py | 10 +-- miplearn/tests/__init__.py | 3 +- miplearn/tests/test_benchmark.py | 4 +- miplearn/tests/test_extractors.py | 9 +- miplearn/types.py | 45 ++++++++++ 49 files changed, 275 insertions(+), 218 deletions(-) create mode 100644 miplearn/types.py diff --git a/.mypy.ini b/.mypy.ini index 60bfdf9..62d41bc 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -3,3 +3,4 @@ ignore_missing_imports = True #disallow_untyped_defs = True disallow_untyped_calls = True disallow_incomplete_defs = True +pretty = True diff --git a/miplearn/__init__.py b/miplearn/__init__.py index 54801fe..28003e7 100644 --- a/miplearn/__init__.py +++ b/miplearn/__init__.py @@ -2,37 +2,31 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from .extractors import ( - SolutionExtractor, - InstanceFeaturesExtractor, - ObjectiveValueExtractor, - VariableFeaturesExtractor, -) - +from .benchmark import BenchmarkRunner +from .classifiers import Classifier, Regressor +from .classifiers.adaptive import AdaptiveClassifier +from .classifiers.threshold import MinPrecisionThreshold from .components.component import Component -from .components.objective import ObjectiveValueComponent +from .components.cuts import UserCutsComponent from .components.lazy_dynamic import DynamicLazyConstraintsComponent from .components.lazy_static import StaticLazyConstraintsComponent -from .components.cuts import UserCutsComponent +from .components.objective import ObjectiveValueComponent from .components.primal import PrimalSolutionComponent from .components.relaxation import RelaxationComponent from .components.steps.convert_tight import ConvertTightIneqsIntoEqsStep -from .components.steps.relax_integrality import RelaxIntegralityStep from .components.steps.drop_redundant import DropRedundantInequalitiesStep - -from .classifiers import Classifier, Regressor -from .classifiers.adaptive import AdaptiveClassifier -from .classifiers.threshold import MinPrecisionThreshold - -from .benchmark import BenchmarkRunner - +from .components.steps.relax_integrality import RelaxIntegralityStep +from .extractors import ( + SolutionExtractor, + InstanceFeaturesExtractor, + ObjectiveValueExtractor, + VariableFeaturesExtractor, +) from .instance import Instance - -from .solvers.pyomo.base import BasePyomoSolver -from .solvers.pyomo.cplex import CplexPyomoSolver -from .solvers.pyomo.gurobi import GurobiPyomoSolver +from .log import setup_logger from .solvers.gurobi import GurobiSolver from .solvers.internal import InternalSolver from .solvers.learning import LearningSolver - -from .log import setup_logger +from .solvers.pyomo.base import BasePyomoSolver +from .solvers.pyomo.cplex import CplexPyomoSolver +from .solvers.pyomo.gurobi import GurobiPyomoSolver diff --git a/miplearn/benchmark.py b/miplearn/benchmark.py index ab9aea7..2f387d3 100644 --- a/miplearn/benchmark.py +++ b/miplearn/benchmark.py @@ -2,15 +2,14 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +import logging +import os from copy import deepcopy import pandas as pd -import numpy as np -import logging from tqdm.auto import tqdm -import os -from .solvers.learning import LearningSolver +from miplearn.solvers.learning import LearningSolver class BenchmarkRunner: diff --git a/miplearn/classifiers/adaptive.py b/miplearn/classifiers/adaptive.py index c7dbc33..f95922c 100644 --- a/miplearn/classifiers/adaptive.py +++ b/miplearn/classifiers/adaptive.py @@ -5,14 +5,15 @@ import logging 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.neighbors import KNeighborsClassifier from sklearn.pipeline import make_pipeline 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__) diff --git a/miplearn/classifiers/counting.py b/miplearn/classifiers/counting.py index 6b46bfd..274fbb7 100644 --- a/miplearn/classifiers/counting.py +++ b/miplearn/classifiers/counting.py @@ -2,9 +2,10 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from miplearn.classifiers import Classifier import numpy as np +from miplearn.classifiers import Classifier + class CountingClassifier(Classifier): """ diff --git a/miplearn/classifiers/cv.py b/miplearn/classifiers/cv.py index e8adfd9..3a4cc1f 100644 --- a/miplearn/classifiers/cv.py +++ b/miplearn/classifiers/cv.py @@ -2,15 +2,15 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +import logging from copy import deepcopy import numpy as np -from miplearn.classifiers import Classifier from sklearn.dummy import DummyClassifier from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score -import logging +from miplearn.classifiers import Classifier logger = logging.getLogger(__name__) diff --git a/miplearn/classifiers/tests/test_counting.py b/miplearn/classifiers/tests/test_counting.py index e05b09b..a8bbec8 100644 --- a/miplearn/classifiers/tests/test_counting.py +++ b/miplearn/classifiers/tests/test_counting.py @@ -1,11 +1,12 @@ # 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 miplearn.classifiers.counting import CountingClassifier import numpy as np from numpy.linalg import norm +from miplearn.classifiers.counting import CountingClassifier + E = 0.1 diff --git a/miplearn/classifiers/tests/test_cv.py b/miplearn/classifiers/tests/test_cv.py index ccb5df3..545ce41 100644 --- a/miplearn/classifiers/tests/test_cv.py +++ b/miplearn/classifiers/tests/test_cv.py @@ -3,11 +3,12 @@ # Released under the modified BSD license. See COPYING.md for more details. import numpy as np -from miplearn.classifiers.cv import CrossValidatedClassifier from numpy.linalg import norm from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC +from miplearn.classifiers.cv import CrossValidatedClassifier + E = 0.1 diff --git a/miplearn/classifiers/tests/test_evaluator.py b/miplearn/classifiers/tests/test_evaluator.py index 3ca3bbb..d0dd201 100644 --- a/miplearn/classifiers/tests/test_evaluator.py +++ b/miplearn/classifiers/tests/test_evaluator.py @@ -3,9 +3,10 @@ # Released under the modified BSD license. See COPYING.md for more details. import numpy as np -from miplearn.classifiers.evaluator import ClassifierEvaluator from sklearn.neighbors import KNeighborsClassifier +from miplearn.classifiers.evaluator import ClassifierEvaluator + def test_evaluator(): clf_a = KNeighborsClassifier(n_neighbors=1) diff --git a/miplearn/classifiers/tests/test_threshold.py b/miplearn/classifiers/tests/test_threshold.py index e5cb7b5..a96c326 100644 --- a/miplearn/classifiers/tests/test_threshold.py +++ b/miplearn/classifiers/tests/test_threshold.py @@ -5,6 +5,7 @@ from unittest.mock import Mock import numpy as np + from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import MinPrecisionThreshold diff --git a/miplearn/components/composite.py b/miplearn/components/composite.py index d9de089..26b7aa0 100644 --- a/miplearn/components/composite.py +++ b/miplearn/components/composite.py @@ -2,7 +2,7 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # 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): diff --git a/miplearn/components/cuts.py b/miplearn/components/cuts.py index 8b3ad85..4fb61e7 100644 --- a/miplearn/components/cuts.py +++ b/miplearn/components/cuts.py @@ -2,14 +2,17 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +import logging import sys from copy import deepcopy +import numpy as np +from tqdm.auto import tqdm + from miplearn.classifiers.counting import CountingClassifier from miplearn.components import classifier_evaluation_dict - -from .component import Component -from ..extractors import * +from miplearn.components.component import Component +from miplearn.extractors import InstanceFeaturesExtractor logger = logging.getLogger(__name__) diff --git a/miplearn/components/lazy_dynamic.py b/miplearn/components/lazy_dynamic.py index c7c0d2f..40e004c 100644 --- a/miplearn/components/lazy_dynamic.py +++ b/miplearn/components/lazy_dynamic.py @@ -2,14 +2,17 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +import logging import sys from copy import deepcopy +import numpy as np +from tqdm.auto import tqdm + from miplearn.classifiers.counting import CountingClassifier from miplearn.components import classifier_evaluation_dict - -from .component import Component -from ..extractors import * +from miplearn.components.component import Component +from miplearn.extractors import InstanceFeaturesExtractor, InstanceIterator logger = logging.getLogger(__name__) diff --git a/miplearn/components/lazy_static.py b/miplearn/components/lazy_static.py index 9770a6e..bd95aaa 100644 --- a/miplearn/components/lazy_static.py +++ b/miplearn/components/lazy_static.py @@ -2,12 +2,15 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +import logging import sys from copy import deepcopy +import numpy as np +from tqdm.auto import tqdm + from miplearn.classifiers.counting import CountingClassifier -from .component import Component -from ..extractors import * +from miplearn.components.component import Component logger = logging.getLogger(__name__) diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index a9c516a..da27b68 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -1,6 +1,12 @@ # 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. + +import logging +from copy import deepcopy + +import numpy as np +from sklearn.linear_model import LinearRegression from sklearn.metrics import ( mean_squared_error, explained_variance_score, @@ -9,11 +15,8 @@ from sklearn.metrics import ( r2_score, ) -from .. import Component, InstanceFeaturesExtractor, ObjectiveValueExtractor -from sklearn.linear_model import LinearRegression -from copy import deepcopy -import numpy as np -import logging +from miplearn.components.component import Component +from miplearn.extractors import InstanceFeaturesExtractor, ObjectiveValueExtractor logger = logging.getLogger(__name__) diff --git a/miplearn/components/relaxation.py b/miplearn/components/relaxation.py index d342358..3db8449 100644 --- a/miplearn/components/relaxation.py +++ b/miplearn/components/relaxation.py @@ -4,8 +4,8 @@ import logging -from miplearn import Component from miplearn.classifiers.counting import CountingClassifier +from miplearn.components.component import Component from miplearn.components.composite import CompositeComponent from miplearn.components.steps.convert_tight import ConvertTightIneqsIntoEqsStep from miplearn.components.steps.drop_redundant import DropRedundantInequalitiesStep diff --git a/miplearn/components/steps/convert_tight.py b/miplearn/components/steps/convert_tight.py index c36dd01..7b25b4a 100644 --- a/miplearn/components/steps/convert_tight.py +++ b/miplearn/components/steps/convert_tight.py @@ -3,17 +3,17 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging +import random from copy import deepcopy import numpy as np from tqdm import tqdm -import random -from ... import Component -from ...classifiers.counting import CountingClassifier -from ...components import classifier_evaluation_dict -from ...extractors import InstanceIterator -from .drop_redundant import DropRedundantInequalitiesStep +from miplearn.classifiers.counting import CountingClassifier +from miplearn.components import classifier_evaluation_dict +from miplearn.components.component import Component +from miplearn.components.steps.drop_redundant import DropRedundantInequalitiesStep +from miplearn.extractors import InstanceIterator logger = logging.getLogger(__name__) diff --git a/miplearn/components/steps/drop_redundant.py b/miplearn/components/steps/drop_redundant.py index 8de9e02..1eadfff 100644 --- a/miplearn/components/steps/drop_redundant.py +++ b/miplearn/components/steps/drop_redundant.py @@ -8,9 +8,9 @@ from copy import deepcopy import numpy as np from tqdm import tqdm -from miplearn import Component from miplearn.classifiers.counting import CountingClassifier from miplearn.components import classifier_evaluation_dict +from miplearn.components.component import Component from miplearn.components.lazy_static import LazyConstraint from miplearn.extractors import InstanceIterator diff --git a/miplearn/components/steps/relax_integrality.py b/miplearn/components/steps/relax_integrality.py index 7283524..77cb01e 100644 --- a/miplearn/components/steps/relax_integrality.py +++ b/miplearn/components/steps/relax_integrality.py @@ -4,7 +4,7 @@ import logging -from miplearn import Component +from miplearn.components.component import Component logger = logging.getLogger(__name__) diff --git a/miplearn/components/steps/tests/test_convert_tight.py b/miplearn/components/steps/tests/test_convert_tight.py index 35ca5bd..ae8e7dc 100644 --- a/miplearn/components/steps/tests/test_convert_tight.py +++ b/miplearn/components/steps/tests/test_convert_tight.py @@ -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.relax_integrality import RelaxIntegralityStep +from miplearn.instance import Instance from miplearn.problems.knapsack import GurobiKnapsackInstance - -from unittest.mock import Mock +from miplearn.solvers.gurobi import GurobiSolver +from miplearn.solvers.learning import LearningSolver def test_convert_tight_usage(): @@ -40,7 +43,6 @@ def test_convert_tight_usage(): class TestInstance(Instance): def to_model(self): import gurobipy as grb - from gurobipy import GRB m = grb.Model("model") x1 = m.addVar(name="x1") diff --git a/miplearn/components/steps/tests/test_drop_redundant.py b/miplearn/components/steps/tests/test_drop_redundant.py index a863536..fcc4cb3 100644 --- a/miplearn/components/steps/tests/test_drop_redundant.py +++ b/miplearn/components/steps/tests/test_drop_redundant.py @@ -2,21 +2,15 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -import numpy as np from unittest.mock import Mock, call -from miplearn import ( - LearningSolver, - Instance, - InternalSolver, - GurobiSolver, -) +import numpy as np + from miplearn.classifiers import Classifier -from miplearn.components.relaxation import ( - DropRedundantInequalitiesStep, - RelaxIntegralityStep, -) -from miplearn.problems.knapsack import GurobiKnapsackInstance +from miplearn.components.relaxation import DropRedundantInequalitiesStep +from miplearn.instance import Instance +from miplearn.solvers.internal import InternalSolver +from miplearn.solvers.learning import LearningSolver def _setup(): diff --git a/miplearn/components/tests/test_composite.py b/miplearn/components/tests/test_composite.py index cec70d0..ddcc427 100644 --- a/miplearn/components/tests/test_composite.py +++ b/miplearn/components/tests/test_composite.py @@ -4,8 +4,10 @@ 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.instance import Instance +from miplearn.solvers.learning import LearningSolver def test_composite(): diff --git a/miplearn/components/tests/test_lazy_dynamic.py b/miplearn/components/tests/test_lazy_dynamic.py index 7ed4707..6e6491b 100644 --- a/miplearn/components/tests/test_lazy_dynamic.py +++ b/miplearn/components/tests/test_lazy_dynamic.py @@ -5,10 +5,13 @@ from unittest.mock import Mock import numpy as np -from miplearn import DynamicLazyConstraintsComponent, LearningSolver, InternalSolver +from numpy.linalg import norm + 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 numpy.linalg import norm E = 0.1 diff --git a/miplearn/components/tests/test_lazy_static.py b/miplearn/components/tests/test_lazy_static.py index 9f9d320..663db45 100644 --- a/miplearn/components/tests/test_lazy_static.py +++ b/miplearn/components/tests/test_lazy_static.py @@ -4,13 +4,11 @@ from unittest.mock import Mock, call -from miplearn import ( - StaticLazyConstraintsComponent, - LearningSolver, - Instance, - InternalSolver, -) 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(): @@ -49,7 +47,9 @@ def test_usage_with_solver(): ) 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 = { "type-a": Mock(spec=Classifier), diff --git a/miplearn/components/tests/test_objective.py b/miplearn/components/tests/test_objective.py index 2889acf..02879ac 100644 --- a/miplearn/components/tests/test_objective.py +++ b/miplearn/components/tests/test_objective.py @@ -5,8 +5,9 @@ from unittest.mock import Mock import numpy as np -from miplearn import ObjectiveValueComponent + from miplearn.classifiers import Regressor +from miplearn.components.objective import ObjectiveValueComponent from miplearn.tests import get_test_pyomo_instances diff --git a/miplearn/components/tests/test_primal.py b/miplearn/components/tests/test_primal.py index f1eec68..9882e5b 100644 --- a/miplearn/components/tests/test_primal.py +++ b/miplearn/components/tests/test_primal.py @@ -5,8 +5,9 @@ from unittest.mock import Mock import numpy as np -from miplearn import PrimalSolutionComponent + from miplearn.classifiers import Classifier +from miplearn.components.primal import PrimalSolutionComponent from miplearn.tests import get_test_pyomo_instances diff --git a/miplearn/instance.py b/miplearn/instance.py index 6610314..b00c117 100644 --- a/miplearn/instance.py +++ b/miplearn/instance.py @@ -5,10 +5,12 @@ import gzip import json from abc import ABC, abstractmethod -from typing import Any +from typing import Any, List import numpy as np +from miplearn.types import TrainingSample + class Instance(ABC): """ @@ -20,6 +22,9 @@ class Instance(ABC): into arrays of features, which can be provided as inputs to machine learning models. """ + def __init__(self): + self.training_data: List[TrainingSample] = [] + @abstractmethod def to_model(self) -> Any: """ diff --git a/miplearn/log.py b/miplearn/log.py index 6f647d6..0345d01 100644 --- a/miplearn/log.py +++ b/miplearn/log.py @@ -2,10 +2,9 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from datetime import timedelta import logging -import time import sys +import time class TimeFormatter(logging.Formatter): diff --git a/miplearn/problems/knapsack.py b/miplearn/problems/knapsack.py index f00a300..02e3282 100644 --- a/miplearn/problems/knapsack.py +++ b/miplearn/problems/knapsack.py @@ -2,13 +2,13 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -import miplearn -from miplearn import Instance import numpy as np 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 miplearn.instance import Instance + class ChallengeA: """ @@ -56,6 +56,7 @@ class MultiKnapsackInstance(Instance): """ def __init__(self, prices, capacities, weights): + super().__init__() assert isinstance(prices, np.ndarray) assert isinstance(capacities, np.ndarray) assert isinstance(weights, np.ndarray) @@ -241,6 +242,7 @@ class KnapsackInstance(Instance): """ def __init__(self, weights, prices, capacity): + super().__init__() self.weights = weights self.prices = prices self.capacity = capacity diff --git a/miplearn/problems/stab.py b/miplearn/problems/stab.py index 03ea558..74ead9d 100644 --- a/miplearn/problems/stab.py +++ b/miplearn/problems/stab.py @@ -8,7 +8,7 @@ import pyomo.environ as pe from scipy.stats import uniform, randint from scipy.stats.distributions import rv_frozen -from miplearn import Instance +from miplearn.instance import Instance class ChallengeA: @@ -101,6 +101,7 @@ class MaxWeightStableSetInstance(Instance): """ def __init__(self, graph, weights): + super().__init__() self.graph = graph self.weights = weights diff --git a/miplearn/problems/tests/test_knapsack.py b/miplearn/problems/tests/test_knapsack.py index e28a3ec..61f0402 100644 --- a/miplearn/problems/tests/test_knapsack.py +++ b/miplearn/problems/tests/test_knapsack.py @@ -2,10 +2,10 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # 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 +from scipy.stats import uniform, randint + +from miplearn.problems.knapsack import MultiKnapsackGenerator def test_knapsack_generator(): diff --git a/miplearn/problems/tests/test_stab.py b/miplearn/problems/tests/test_stab.py index afd7c2a..dc44eb8 100644 --- a/miplearn/problems/tests/test_stab.py +++ b/miplearn/problems/tests/test_stab.py @@ -4,10 +4,11 @@ import networkx as nx import numpy as np -from miplearn import LearningSolver -from miplearn.problems.stab import MaxWeightStableSetInstance from scipy.stats import uniform, randint +from miplearn.problems.stab import MaxWeightStableSetInstance +from miplearn.solvers.learning import LearningSolver + def test_stab(): graph = nx.cycle_graph(5) diff --git a/miplearn/problems/tests/test_tsp.py b/miplearn/problems/tests/test_tsp.py index 089c488..e43bc75 100644 --- a/miplearn/problems/tests/test_tsp.py +++ b/miplearn/problems/tests/test_tsp.py @@ -2,13 +2,14 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # 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 from numpy.linalg import norm from scipy.spatial.distance import pdist, squareform from scipy.stats import uniform, randint +from miplearn.problems.tsp import TravelingSalesmanGenerator, TravelingSalesmanInstance +from miplearn.solvers.learning import LearningSolver + def test_generator(): instances = TravelingSalesmanGenerator( diff --git a/miplearn/problems/tsp.py b/miplearn/problems/tsp.py index f9dd407..ea0f40f 100644 --- a/miplearn/problems/tsp.py +++ b/miplearn/problems/tsp.py @@ -2,14 +2,14 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +import networkx as nx import numpy as np import pyomo.environ as pe -from miplearn import Instance -from scipy.stats import uniform, randint from scipy.spatial.distance import pdist, squareform +from scipy.stats import uniform, randint from scipy.stats.distributions import rv_frozen -import networkx as nx -import random + +from miplearn.instance import Instance class ChallengeA: diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index a888a71..ac17a04 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -8,15 +8,15 @@ from io import StringIO from random import randint from typing import List, Any, Dict, Union, Tuple, Optional -from . import RedirectOutput -from .internal import ( +from miplearn.instance import Instance +from miplearn.solvers import RedirectOutput +from miplearn.solvers.internal import ( InternalSolver, LPSolveStats, IterationCallback, LazyCallback, MIPSolveStats, ) -from .. import Instance logger = logging.getLogger(__name__) @@ -181,6 +181,7 @@ class GurobiSolver(InternalSolver): sense = "max" lb = self.model.objVal ub = self.model.objBound + ws_value = self._extract_warm_start_value(log) stats: MIPSolveStats = { "Lower bound": lb, "Upper bound": ub, @@ -188,10 +189,9 @@ class GurobiSolver(InternalSolver): "Nodes": total_nodes, "Sense": sense, "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 def get_solution(self) -> Dict: diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index d7aa3d3..785821d 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -4,11 +4,15 @@ import logging from abc import ABC, abstractmethod -from typing import Callable, Any, Dict, List - -from typing_extensions import TypedDict - -from ..instance import Instance +from typing import Any, Dict, List + +from miplearn.instance import Instance +from miplearn.types import ( + LPSolveStats, + IterationCallback, + LazyCallback, + MIPSolveStats, +) logger = logging.getLogger(__name__) @@ -21,33 +25,6 @@ class Constraint: 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): """ Abstract class representing the MIP solver used internally by LearningSolver. diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 156175c..4667f94 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -2,26 +2,24 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +import gzip import logging -import pickle import os +import pickle import tempfile -import gzip - 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 tempfile import NamedTemporaryFile - -from . import RedirectOutput -from .. import ( - ObjectiveValueComponent, - PrimalSolutionComponent, - DynamicLazyConstraintsComponent, - UserCutsComponent, -) -from ..solvers.internal import InternalSolver -from ..solvers.pyomo.gurobi import GurobiPyomoSolver + +from miplearn.components.cuts import UserCutsComponent +from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent +from miplearn.components.objective import ObjectiveValueComponent +from miplearn.components.primal import PrimalSolutionComponent +from miplearn.instance import Instance +from miplearn.solvers import RedirectOutput +from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver +from miplearn.types import MIPSolveStats, TrainingSample logger = logging.getLogger(__name__) @@ -192,46 +190,55 @@ class LearningSolver: def _solve( self, - instance, - model=None, - output="", - tee=False, - ): + instance: Instance, + model: Any = None, + output: str = "", + tee: bool = False, + ) -> MIPSolveStats: + + # Load instance from file, if necessary filename = None fileformat = None + file: Union[BinaryIO, gzip.GzipFile] if isinstance(instance, str): filename = instance logger.info("Reading: %s" % filename) if filename.endswith(".gz"): fileformat = "pickle-gz" with gzip.GzipFile(filename, "rb") as file: - instance = pickle.load(file) + instance = pickle.load(cast(IO[bytes], file)) else: fileformat = "pickle" with open(filename, "rb") as file: - instance = pickle.load(file) + instance = pickle.load(cast(IO[bytes], file)) + # Generate model if model is None: with RedirectOutput([]): model = instance.to_model() + # Initialize training data + training_sample: TrainingSample = {} + + # Initialize internal solver self.tee = tee self.internal_solver = self.solver_factory() self.internal_solver.set_instance(instance, model) + # Solve linear relaxation if self.solve_lp_first: logger.info("Solving LP relaxation...") - results = self.internal_solver.solve_lp(tee=tee) - instance.lp_solution = self.internal_solver.get_solution() - instance.lp_value = results["Optimal value"] - else: - instance.lp_solution = self.internal_solver.get_empty_solution() - instance.lp_value = 0.0 + stats = self.internal_solver.solve_lp(tee=tee) + training_sample["LP solution"] = self.internal_solver.get_solution() + training_sample["LP value"] = stats["Optimal value"] + training_sample["LP log"] = stats["Log"] + # Before-solve callbacks logger.debug("Running before_solve callbacks...") for component in self.components.values(): component.before_solve(self, instance, model) + # Define wrappers def iteration_cb(): should_repeat = False for comp in self.components.values(): @@ -247,29 +254,33 @@ class LearningSolver: if self.use_lazy_cb: lazy_cb = lazy_cb_wrapper + # Solve MILP logger.info("Solving MILP...") stats = self.internal_solver.solve( tee=tee, iteration_cb=iteration_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 - instance.lower_bound = stats["Lower bound"] - instance.upper_bound = stats["Upper bound"] - instance.solver_log = stats["Log"] - instance.solution = self.internal_solver.get_solution() + training_sample["Lower bound"] = stats["Lower bound"] + training_sample["Upper bound"] = stats["Upper bound"] + training_sample["MIP log"] = stats["Log"] + training_sample["Solution"] = self.internal_solver.get_solution() + # After-solve callbacks logger.debug("Calling after_solve callbacks...") - training_data = {} for component in self.components.values(): - component.after_solve(self, instance, model, stats, training_data) + component.after_solve(self, instance, model, stats, training_sample) + # Append training data if not hasattr(instance, "training_data"): instance.training_data = [] - instance.training_data += [training_data] + instance.training_data += [training_sample] + # Write to file, if necessary if filename is not None and output is not None: output_filename = output if len(output) == 0: @@ -277,11 +288,10 @@ class LearningSolver: logger.info("Writing: %s" % output_filename) if fileformat == "pickle": with open(output_filename, "wb") as file: - pickle.dump(instance, file) + pickle.dump(instance, cast(IO[bytes], file)) else: with gzip.GzipFile(output_filename, "wb") as file: - pickle.dump(instance, file) - + pickle.dump(instance, cast(IO[bytes], file)) return stats def parallel_solve( diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index a6b5b7a..de5bfe0 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -12,15 +12,15 @@ import pyomo from pyomo import environ as pe from pyomo.core import Var, Constraint -from .. import RedirectOutput -from ..internal import ( +from miplearn.instance import Instance +from miplearn.solvers import RedirectOutput +from miplearn.solvers.internal import ( InternalSolver, LPSolveStats, IterationCallback, LazyCallback, MIPSolveStats, ) -from ...instance import Instance logger = logging.getLogger(__name__) @@ -98,19 +98,18 @@ class BasePyomoSolver(InternalSolver): if not should_repeat: break log = streams[0].getvalue() + node_count = self._extract_node_count(log) + ws_value = self._extract_warm_start_value(log) stats: MIPSolveStats = { "Lower bound": results["Problem"][0]["Lower bound"], "Upper bound": results["Problem"][0]["Upper bound"], "Wallclock time": total_wallclock_time, "Sense": self._obj_sense, "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 def get_solution(self) -> Dict: diff --git a/miplearn/solvers/pyomo/cplex.py b/miplearn/solvers/pyomo/cplex.py index fd5cfa3..e0e7037 100644 --- a/miplearn/solvers/pyomo/cplex.py +++ b/miplearn/solvers/pyomo/cplex.py @@ -5,7 +5,7 @@ from pyomo import environ as pe from scipy.stats import randint -from .base import BasePyomoSolver +from miplearn.solvers.pyomo.base import BasePyomoSolver class CplexPyomoSolver(BasePyomoSolver): diff --git a/miplearn/solvers/pyomo/gurobi.py b/miplearn/solvers/pyomo/gurobi.py index 25dd861..426a48b 100644 --- a/miplearn/solvers/pyomo/gurobi.py +++ b/miplearn/solvers/pyomo/gurobi.py @@ -7,7 +7,7 @@ import logging from pyomo import environ as pe from scipy.stats import randint -from .base import BasePyomoSolver +from miplearn.solvers.pyomo.base import BasePyomoSolver logger = logging.getLogger(__name__) diff --git a/miplearn/solvers/pyomo/xpress.py b/miplearn/solvers/pyomo/xpress.py index d50d134..3efdd8d 100644 --- a/miplearn/solvers/pyomo/xpress.py +++ b/miplearn/solvers/pyomo/xpress.py @@ -7,7 +7,7 @@ import logging from pyomo import environ as pe from scipy.stats import randint -from .base import BasePyomoSolver +from miplearn.solvers.pyomo.base import BasePyomoSolver logger = logging.getLogger(__name__) diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index bfabe2e..5d65d2a 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -5,15 +5,18 @@ from inspect import isclass from typing import List, Callable -from miplearn import BasePyomoSolver, GurobiSolver, GurobiPyomoSolver, InternalSolver 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 def _get_instance(solver): - def _is_subclass_or_instance(solver, parentClass): - return isinstance(solver, parentClass) or ( - isclass(solver) and issubclass(solver, parentClass) + def _is_subclass_or_instance(obj, parent_class): + return isinstance(obj, parent_class) or ( + isclass(obj) and issubclass(obj, parent_class) ) if _is_subclass_or_instance(solver, BasePyomoSolver): diff --git a/miplearn/solvers/tests/test_internal_solver.py b/miplearn/solvers/tests/test_internal_solver.py index 5e25609..1b70fd8 100644 --- a/miplearn/solvers/tests/test_internal_solver.py +++ b/miplearn/solvers/tests/test_internal_solver.py @@ -8,9 +8,10 @@ from warnings import warn import pyomo.environ as pe -from miplearn import BasePyomoSolver, GurobiSolver 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__) @@ -44,7 +45,7 @@ def test_internal_solver_warm_starts(): } ) 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 else: 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) - assert "Warm start value" not in stats + assert stats["Warm start value"] is None solver.fix( { diff --git a/miplearn/solvers/tests/test_lazy_cb.py b/miplearn/solvers/tests/test_lazy_cb.py index 03e9861..0d7b3eb 100644 --- a/miplearn/solvers/tests/test_lazy_cb.py +++ b/miplearn/solvers/tests/test_lazy_cb.py @@ -4,8 +4,8 @@ import logging -from . import _get_instance -from ... import GurobiSolver +from miplearn.solvers.gurobi import GurobiSolver +from miplearn.solvers.tests import _get_instance logger = logging.getLogger(__name__) diff --git a/miplearn/solvers/tests/test_learning_solver.py b/miplearn/solvers/tests/test_learning_solver.py index 0d098ba..41b7317 100644 --- a/miplearn/solvers/tests/test_learning_solver.py +++ b/miplearn/solvers/tests/test_learning_solver.py @@ -7,13 +7,9 @@ import pickle import tempfile import os -from miplearn import ( - LearningSolver, - GurobiSolver, - DynamicLazyConstraintsComponent, -) - -from . import _get_instance, _get_internal_solvers +from miplearn.solvers.gurobi import GurobiSolver +from miplearn.solvers.learning import LearningSolver +from miplearn.solvers.tests import _get_instance, _get_internal_solvers logger = logging.getLogger(__name__) diff --git a/miplearn/tests/__init__.py b/miplearn/tests/__init__.py index e5ee0a1..62e7b5a 100644 --- a/miplearn/tests/__init__.py +++ b/miplearn/tests/__init__.py @@ -1,8 +1,9 @@ # 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 miplearn import LearningSolver + from miplearn.problems.knapsack import KnapsackInstance +from miplearn.solvers.learning import LearningSolver def get_test_pyomo_instances(): diff --git a/miplearn/tests/test_benchmark.py b/miplearn/tests/test_benchmark.py index ab838df..9e8011f 100644 --- a/miplearn/tests/test_benchmark.py +++ b/miplearn/tests/test_benchmark.py @@ -4,10 +4,12 @@ import os.path -from miplearn import LearningSolver, BenchmarkRunner +from miplearn.benchmark import BenchmarkRunner from miplearn.problems.stab import MaxWeightStableSetGenerator from scipy.stats import randint +from miplearn.solvers.learning import LearningSolver + def test_benchmark(): # Generate training and test instances diff --git a/miplearn/tests/test_extractors.py b/miplearn/tests/test_extractors.py index 8bee355..2d501db 100644 --- a/miplearn/tests/test_extractors.py +++ b/miplearn/tests/test_extractors.py @@ -1,16 +1,15 @@ # 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. +import numpy as np -from miplearn.problems.knapsack import KnapsackInstance -from miplearn import ( - LearningSolver, +from miplearn.extractors import ( SolutionExtractor, InstanceFeaturesExtractor, VariableFeaturesExtractor, ) -import numpy as np -import pyomo.environ as pe +from miplearn.problems.knapsack import KnapsackInstance +from miplearn.solvers.learning import LearningSolver def _get_instances(): diff --git a/miplearn/types.py b/miplearn/types.py new file mode 100644 index 0000000..c8aeb2b --- /dev/null +++ b/miplearn/types.py @@ -0,0 +1,45 @@ +# 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": Optional[str], + "LP solution": Optional[Dict], + "LP value": Optional[float], + "Lower bound": Optional[float], + "MIP log": Optional[str], + "Solution": Optional[Dict], + "Upper bound": Optional[float], + }, + 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]