mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 01:18:52 -06:00
MIPLearn v0.3
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
|
||||
def _build_circle_training_data() -> Tuple[np.ndarray, np.ndarray]:
|
||||
x_train = StandardScaler().fit_transform(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
x1,
|
||||
x2,
|
||||
]
|
||||
for x1 in range(-10, 11)
|
||||
for x2 in range(-10, 11)
|
||||
]
|
||||
)
|
||||
)
|
||||
y_train = np.array(
|
||||
[
|
||||
[
|
||||
False,
|
||||
True,
|
||||
]
|
||||
if x1 * x1 + x2 * x2 <= 100
|
||||
else [
|
||||
True,
|
||||
False,
|
||||
]
|
||||
for x1 in range(-10, 11)
|
||||
for x2 in range(-10, 11)
|
||||
]
|
||||
)
|
||||
return x_train, y_train
|
||||
@@ -1,40 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
from numpy.linalg import norm
|
||||
from sklearn.svm import SVC
|
||||
|
||||
from miplearn.classifiers.adaptive import CandidateClassifierSpecs, AdaptiveClassifier
|
||||
from miplearn.classifiers.sklearn import ScikitLearnClassifier
|
||||
from tests.classifiers import _build_circle_training_data
|
||||
|
||||
|
||||
def test_adaptive() -> None:
|
||||
clf = AdaptiveClassifier(
|
||||
candidates={
|
||||
"linear": CandidateClassifierSpecs(
|
||||
classifier=ScikitLearnClassifier(
|
||||
SVC(
|
||||
probability=True,
|
||||
random_state=42,
|
||||
)
|
||||
)
|
||||
),
|
||||
"poly": CandidateClassifierSpecs(
|
||||
classifier=ScikitLearnClassifier(
|
||||
SVC(
|
||||
probability=True,
|
||||
kernel="poly",
|
||||
degree=2,
|
||||
random_state=42,
|
||||
)
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
x_train, y_train = _build_circle_training_data()
|
||||
clf.fit(x_train, y_train)
|
||||
proba = clf.predict_proba(x_train)
|
||||
y_pred = (proba[:, 1] > 0.5).astype(float)
|
||||
assert norm(y_train[:, 1] - y_pred) < 0.1
|
||||
@@ -1,38 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
|
||||
from miplearn.classifiers.counting import CountingClassifier
|
||||
|
||||
E = 0.1
|
||||
|
||||
|
||||
def test_counting() -> None:
|
||||
clf = CountingClassifier()
|
||||
n_features = 25
|
||||
x_train = np.zeros((8, n_features))
|
||||
y_train = np.array(
|
||||
[
|
||||
[True, False, False],
|
||||
[True, False, False],
|
||||
[False, True, False],
|
||||
[True, False, False],
|
||||
[False, True, False],
|
||||
[False, True, False],
|
||||
[False, True, False],
|
||||
[False, False, True],
|
||||
]
|
||||
)
|
||||
x_test = np.zeros((2, n_features))
|
||||
y_expected = np.array(
|
||||
[
|
||||
[3 / 8.0, 4 / 8.0, 1 / 8.0],
|
||||
[3 / 8.0, 4 / 8.0, 1 / 8.0],
|
||||
]
|
||||
)
|
||||
clf.fit(x_train, y_train)
|
||||
y_actual = clf.predict_proba(x_test)
|
||||
assert norm(y_actual - y_expected) < E
|
||||
@@ -1,58 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
from sklearn.svm import SVC
|
||||
|
||||
from miplearn.classifiers.cv import CrossValidatedClassifier
|
||||
from miplearn.classifiers.sklearn import ScikitLearnClassifier
|
||||
from tests.classifiers import _build_circle_training_data
|
||||
|
||||
E = 0.1
|
||||
|
||||
|
||||
def test_cv() -> None:
|
||||
x_train, y_train = _build_circle_training_data()
|
||||
n_samples = x_train.shape[0]
|
||||
|
||||
# Support vector machines with linear kernels do not perform well on this
|
||||
# data set, so predictor should return the given constant.
|
||||
clf = CrossValidatedClassifier(
|
||||
classifier=ScikitLearnClassifier(
|
||||
SVC(
|
||||
probability=True,
|
||||
random_state=42,
|
||||
)
|
||||
),
|
||||
threshold=0.90,
|
||||
constant=[True, False],
|
||||
cv=30,
|
||||
)
|
||||
clf.fit(x_train, y_train)
|
||||
proba = clf.predict_proba(x_train)
|
||||
assert isinstance(proba, np.ndarray)
|
||||
assert proba.shape == (n_samples, 2)
|
||||
|
||||
y_pred = (proba[:, 1] > 0.5).astype(float)
|
||||
assert norm(np.zeros(n_samples) - y_pred) < E
|
||||
|
||||
# Support vector machines with quadratic kernels perform almost perfectly
|
||||
# on this data set, so predictor should return their prediction.
|
||||
clf = CrossValidatedClassifier(
|
||||
classifier=ScikitLearnClassifier(
|
||||
SVC(
|
||||
probability=True,
|
||||
kernel="poly",
|
||||
degree=2,
|
||||
random_state=42,
|
||||
)
|
||||
),
|
||||
threshold=0.90,
|
||||
cv=30,
|
||||
)
|
||||
clf.fit(x_train, y_train)
|
||||
proba = clf.predict_proba(x_train)
|
||||
y_pred = (proba[:, 1] > 0.5).astype(float)
|
||||
assert norm(y_train[:, 1] - y_pred) < E
|
||||
@@ -1,33 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_array_equal
|
||||
from sklearn.linear_model import LinearRegression
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
|
||||
from miplearn.classifiers.sklearn import ScikitLearnClassifier, ScikitLearnRegressor
|
||||
|
||||
|
||||
def test_constant_prediction() -> None:
|
||||
x_train = np.array([[0.0, 1.0], [1.0, 0.0]])
|
||||
y_train = np.array([[True, False], [True, False]])
|
||||
clf = ScikitLearnClassifier(KNeighborsClassifier(n_neighbors=1))
|
||||
clf.fit(x_train, y_train)
|
||||
proba = clf.predict_proba(x_train)
|
||||
assert_array_equal(
|
||||
proba,
|
||||
np.array([[1.0, 0.0], [1.0, 0.0]]),
|
||||
)
|
||||
|
||||
|
||||
def test_regressor() -> None:
|
||||
x_train = np.array([[0.0, 1.0], [1.0, 4.0], [2.0, 2.0]])
|
||||
y_train = np.array([[1.0], [5.0], [4.0]])
|
||||
x_test = np.array([[4.0, 4.0], [0.0, 0.0]])
|
||||
clf = ScikitLearnRegressor(LinearRegression())
|
||||
clf.fit(x_train, y_train)
|
||||
y_test_actual = clf.predict(x_test)
|
||||
y_test_expected = np.array([[8.0], [0.0]])
|
||||
assert_array_equal(np.round(y_test_actual, 2), y_test_expected)
|
||||
@@ -1,56 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
from miplearn.classifiers import Classifier
|
||||
from miplearn.classifiers.threshold import MinPrecisionThreshold
|
||||
|
||||
|
||||
def test_threshold_dynamic() -> None:
|
||||
clf = Mock(spec=Classifier)
|
||||
clf.predict_proba = Mock(
|
||||
return_value=np.array(
|
||||
[
|
||||
[0.10, 0.90],
|
||||
[0.25, 0.75],
|
||||
[0.40, 0.60],
|
||||
[0.90, 0.10],
|
||||
]
|
||||
)
|
||||
)
|
||||
x_train = np.array(
|
||||
[
|
||||
[0],
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
]
|
||||
)
|
||||
y_train = np.array(
|
||||
[
|
||||
[False, True],
|
||||
[False, True],
|
||||
[True, False],
|
||||
[True, False],
|
||||
]
|
||||
)
|
||||
|
||||
threshold = MinPrecisionThreshold(min_precision=[1.0, 1.0])
|
||||
threshold.fit(clf, x_train, y_train)
|
||||
assert threshold.predict(x_train) == [0.40, 0.75]
|
||||
|
||||
# threshold = MinPrecisionThreshold(min_precision=0.65)
|
||||
# threshold.fit(clf, x_train, y_train)
|
||||
# assert threshold.predict(x_train) == [0.0, 0.80]
|
||||
|
||||
# threshold = MinPrecisionThreshold(min_precision=0.50)
|
||||
# threshold.fit(clf, x_train, y_train)
|
||||
# assert threshold.predict(x_train) == [0.0, 0.70]
|
||||
#
|
||||
# threshold = MinPrecisionThreshold(min_precision=0.00)
|
||||
# threshold.fit(clf, x_train, y_train)
|
||||
# assert threshold.predict(x_train) == [0.0, 0.70]
|
||||
@@ -1,3 +1,3 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
26
tests/components/primal/test_expert.py
Normal file
26
tests/components/primal/test_expert.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from typing import List, Dict, Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
from miplearn.components.primal.actions import SetWarmStart, FixVariables
|
||||
from miplearn.components.primal.expert import ExpertPrimalComponent
|
||||
|
||||
|
||||
def test_expert(multiknapsack_h5: List[str]) -> None:
|
||||
model = Mock()
|
||||
stats: Dict[str, Any] = {}
|
||||
comp = ExpertPrimalComponent(action=SetWarmStart())
|
||||
comp.before_mip(multiknapsack_h5[0], model, stats)
|
||||
model.set_warm_starts.assert_called()
|
||||
names, starts, _ = model.set_warm_starts.call_args.args
|
||||
assert names.shape == (100,)
|
||||
assert starts.shape == (1, 100)
|
||||
|
||||
comp = ExpertPrimalComponent(action=FixVariables())
|
||||
comp.before_mip(multiknapsack_h5[0], model, stats)
|
||||
model.fix_variables.assert_called()
|
||||
names, v, _ = model.fix_variables.call_args.args
|
||||
assert names.shape == (100,)
|
||||
assert v.shape == (100,)
|
||||
51
tests/components/primal/test_indep.py
Normal file
51
tests/components/primal/test_indep.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from typing import List, Dict, Any
|
||||
from unittest.mock import Mock, call
|
||||
|
||||
from sklearn.dummy import DummyClassifier
|
||||
|
||||
from miplearn.components.primal.actions import SetWarmStart
|
||||
from miplearn.components.primal.indep import IndependentVarsPrimalComponent
|
||||
from miplearn.extractors.fields import H5FieldsExtractor
|
||||
|
||||
|
||||
def test_indep(multiknapsack_h5: List[str]) -> None:
|
||||
# Create and fit component
|
||||
clone_fn = Mock(return_value=Mock(wraps=DummyClassifier()))
|
||||
comp = IndependentVarsPrimalComponent(
|
||||
base_clf="dummy",
|
||||
extractor=H5FieldsExtractor(var_fields=["lp_var_values"]),
|
||||
clone_fn=clone_fn,
|
||||
action=SetWarmStart(),
|
||||
)
|
||||
comp.fit(multiknapsack_h5)
|
||||
|
||||
# Should call clone 100 times and store the 100 classifiers
|
||||
clone_fn.assert_has_calls([call("dummy") for _ in range(100)])
|
||||
assert len(comp.clf_) == 100
|
||||
|
||||
for v in [b"x[0]", b"x[1]"]:
|
||||
# Should pass correct data to fit
|
||||
comp.clf_[v].fit.assert_called()
|
||||
x, y = comp.clf_[v].fit.call_args.args
|
||||
assert x.shape == (3, 1)
|
||||
assert y.shape == (3,)
|
||||
|
||||
# Call before-mip
|
||||
stats: Dict[str, Any] = {}
|
||||
model = Mock()
|
||||
comp.before_mip(multiknapsack_h5[0], model, stats)
|
||||
|
||||
# Should call predict with correct args
|
||||
for v in [b"x[0]", b"x[1]"]:
|
||||
comp.clf_[v].predict.assert_called()
|
||||
(x_test,) = comp.clf_[v].predict.call_args.args
|
||||
assert x_test.shape == (1, 1)
|
||||
|
||||
# Should set warm starts
|
||||
model.set_warm_starts.assert_called()
|
||||
names, starts, _ = model.set_warm_starts.call_args.args
|
||||
assert len(names) == 100
|
||||
assert starts.shape == (1, 100)
|
||||
46
tests/components/primal/test_joint.py
Normal file
46
tests/components/primal/test_joint.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from typing import List, Dict, Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
from sklearn.dummy import DummyClassifier
|
||||
|
||||
from miplearn.components.primal.actions import SetWarmStart
|
||||
from miplearn.components.primal.joint import JointVarsPrimalComponent
|
||||
from miplearn.extractors.fields import H5FieldsExtractor
|
||||
|
||||
|
||||
def test_joint(multiknapsack_h5: List[str]) -> None:
|
||||
# Create mock classifier
|
||||
clf = Mock(wraps=DummyClassifier())
|
||||
|
||||
# Create and fit component
|
||||
comp = JointVarsPrimalComponent(
|
||||
clf=clf,
|
||||
extractor=H5FieldsExtractor(instance_fields=["static_var_obj_coeffs"]),
|
||||
action=SetWarmStart(),
|
||||
)
|
||||
comp.fit(multiknapsack_h5)
|
||||
|
||||
# Should call fit method with correct arguments
|
||||
clf.fit.assert_called()
|
||||
x, y = clf.fit.call_args.args
|
||||
assert x.shape == (3, 100)
|
||||
assert y.shape == (3, 100)
|
||||
|
||||
# Call before-mip
|
||||
stats: Dict[str, Any] = {}
|
||||
model = Mock()
|
||||
comp.before_mip(multiknapsack_h5[0], model, stats)
|
||||
|
||||
# Should call predict with correct args
|
||||
clf.predict.assert_called()
|
||||
(x_test,) = clf.predict.call_args.args
|
||||
assert x_test.shape == (1, 100)
|
||||
|
||||
# Should set warm starts
|
||||
model.set_warm_starts.assert_called()
|
||||
names, starts, _ = model.set_warm_starts.call_args.args
|
||||
assert len(names) == 100
|
||||
assert starts.shape == (1, 100)
|
||||
86
tests/components/primal/test_mem.py
Normal file
86
tests/components/primal/test_mem.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
from sklearn.dummy import DummyClassifier
|
||||
|
||||
from miplearn.components.primal.actions import SetWarmStart
|
||||
from miplearn.components.primal.mem import (
|
||||
MemorizingPrimalComponent,
|
||||
SelectTopSolutions,
|
||||
MergeTopSolutions,
|
||||
)
|
||||
from miplearn.extractors.abstract import FeaturesExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def test_mem_component(
|
||||
multiknapsack_h5: List[str], default_extractor: FeaturesExtractor
|
||||
) -> None:
|
||||
# Create mock classifier
|
||||
clf = Mock(wraps=DummyClassifier())
|
||||
|
||||
# Create and fit component
|
||||
comp = MemorizingPrimalComponent(
|
||||
clf,
|
||||
extractor=default_extractor,
|
||||
constructor=SelectTopSolutions(2),
|
||||
action=SetWarmStart(),
|
||||
)
|
||||
comp.fit(multiknapsack_h5)
|
||||
|
||||
# Should call fit method with correct arguments
|
||||
clf.fit.assert_called()
|
||||
x, y = clf.fit.call_args.args
|
||||
assert x.shape == (3, 100)
|
||||
assert y.tolist() == [0, 1, 2]
|
||||
|
||||
# Should store solutions
|
||||
assert comp.solutions_ is not None
|
||||
assert comp.solutions_.shape == (3, 100)
|
||||
assert comp.bin_var_names_ is not None
|
||||
assert len(comp.bin_var_names_) == 100
|
||||
|
||||
# Call before-mip
|
||||
stats: Dict[str, Any] = {}
|
||||
model = Mock()
|
||||
comp.before_mip(multiknapsack_h5[0], model, stats)
|
||||
|
||||
# Should call predict_proba with correct args
|
||||
clf.predict_proba.assert_called()
|
||||
(x_test,) = clf.predict_proba.call_args.args
|
||||
assert x_test.shape == (1, 100)
|
||||
|
||||
# Should set warm starts
|
||||
model.set_warm_starts.assert_called()
|
||||
names, starts, _ = model.set_warm_starts.call_args.args
|
||||
assert len(names) == 100
|
||||
assert starts.shape == (2, 100)
|
||||
assert np.all(starts[0, :] == comp.solutions_[0, :])
|
||||
assert np.all(starts[1, :] == comp.solutions_[1, :])
|
||||
|
||||
|
||||
def test_merge_top_solutions() -> None:
|
||||
solutions = np.array(
|
||||
[
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 1],
|
||||
[0, 1, 1, 1],
|
||||
[0, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
]
|
||||
)
|
||||
y_proba = np.array([0.25, 0.25, 0.25, 0.25, 0])
|
||||
starts = MergeTopSolutions(k=4, thresholds=[0.25, 0.75]).construct(
|
||||
y_proba, solutions
|
||||
)
|
||||
assert starts.shape == (1, 4)
|
||||
assert starts[0, 0] == 0
|
||||
assert starts[0, 1] == 1
|
||||
assert np.isnan(starts[0, 2])
|
||||
assert starts[0, 3] == 1
|
||||
@@ -1,147 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from typing import List, cast
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from miplearn.classifiers import Classifier
|
||||
from miplearn.classifiers.threshold import MinProbabilityThreshold
|
||||
from miplearn.components import classifier_evaluation_dict
|
||||
from miplearn.components.dynamic_common import DynamicConstraintsComponent
|
||||
from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent
|
||||
from miplearn.features.sample import MemorySample
|
||||
from miplearn.instance.base import Instance
|
||||
from miplearn.solvers.tests import assert_equals
|
||||
|
||||
E = 0.1
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def training_instances() -> List[Instance]:
|
||||
instances = [cast(Instance, Mock(spec=Instance)) for _ in range(2)]
|
||||
samples_0 = [
|
||||
MemorySample(
|
||||
{
|
||||
"mip_constr_lazy": DynamicConstraintsComponent.encode(
|
||||
{
|
||||
b"c1": 0,
|
||||
b"c2": 0,
|
||||
}
|
||||
),
|
||||
"static_instance_features": np.array([5.0]),
|
||||
},
|
||||
),
|
||||
MemorySample(
|
||||
{
|
||||
"mip_constr_lazy": DynamicConstraintsComponent.encode(
|
||||
{
|
||||
b"c2": 0,
|
||||
b"c3": 0,
|
||||
}
|
||||
),
|
||||
"static_instance_features": np.array([5.0]),
|
||||
},
|
||||
),
|
||||
]
|
||||
instances[0].get_samples = Mock(return_value=samples_0) # type: ignore
|
||||
instances[0].get_constraint_categories = Mock( # type: ignore
|
||||
return_value=np.array(["type-a", "type-a", "type-b", "type-b"], dtype="S")
|
||||
)
|
||||
instances[0].get_constraint_features = Mock( # type: ignore
|
||||
return_value=np.array(
|
||||
[
|
||||
[1.0, 2.0, 3.0],
|
||||
[4.0, 5.0, 6.0],
|
||||
[1.0, 2.0, 0.0],
|
||||
[3.0, 4.0, 0.0],
|
||||
]
|
||||
)
|
||||
)
|
||||
instances[0].are_constraints_lazy = Mock( # type: ignore
|
||||
return_value=np.zeros(4, dtype=bool)
|
||||
)
|
||||
samples_1 = [
|
||||
MemorySample(
|
||||
{
|
||||
"mip_constr_lazy": DynamicConstraintsComponent.encode(
|
||||
{
|
||||
b"c3": 0,
|
||||
b"c4": 0,
|
||||
}
|
||||
),
|
||||
"static_instance_features": np.array([8.0]),
|
||||
},
|
||||
)
|
||||
]
|
||||
instances[1].get_samples = Mock(return_value=samples_1) # type: ignore
|
||||
instances[1].get_constraint_categories = Mock( # type: ignore
|
||||
return_value=np.array(["", "type-a", "type-b", "type-b"], dtype="S")
|
||||
)
|
||||
instances[1].get_constraint_features = Mock( # type: ignore
|
||||
return_value=np.array(
|
||||
[
|
||||
[7.0, 8.0, 9.0],
|
||||
[5.0, 6.0, 0.0],
|
||||
[7.0, 8.0, 0.0],
|
||||
]
|
||||
)
|
||||
)
|
||||
instances[1].are_constraints_lazy = Mock( # type: ignore
|
||||
return_value=np.zeros(4, dtype=bool)
|
||||
)
|
||||
return instances
|
||||
|
||||
|
||||
def test_sample_xy(training_instances: List[Instance]) -> None:
|
||||
comp = DynamicLazyConstraintsComponent()
|
||||
comp.pre_fit(
|
||||
[
|
||||
{b"c1": 0, b"c3": 0, b"c4": 0},
|
||||
{b"c1": 0, b"c2": 0, b"c4": 0},
|
||||
]
|
||||
)
|
||||
x_expected = {
|
||||
b"type-a": np.array([[5.0, 1.0, 2.0, 3.0], [5.0, 4.0, 5.0, 6.0]]),
|
||||
b"type-b": np.array([[5.0, 1.0, 2.0, 0.0], [5.0, 3.0, 4.0, 0.0]]),
|
||||
}
|
||||
y_expected = {
|
||||
b"type-a": np.array([[False, True], [False, True]]),
|
||||
b"type-b": np.array([[True, False], [True, False]]),
|
||||
}
|
||||
x_actual, y_actual = comp.sample_xy(
|
||||
training_instances[0],
|
||||
training_instances[0].get_samples()[0],
|
||||
)
|
||||
assert_equals(x_actual, x_expected)
|
||||
assert_equals(y_actual, y_expected)
|
||||
|
||||
|
||||
def test_sample_predict_evaluate(training_instances: List[Instance]) -> None:
|
||||
comp = DynamicLazyConstraintsComponent()
|
||||
comp.known_violations[b"c1"] = 0
|
||||
comp.known_violations[b"c2"] = 0
|
||||
comp.known_violations[b"c3"] = 0
|
||||
comp.known_violations[b"c4"] = 0
|
||||
comp.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5])
|
||||
comp.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5])
|
||||
comp.classifiers[b"type-a"] = Mock(spec=Classifier)
|
||||
comp.classifiers[b"type-b"] = Mock(spec=Classifier)
|
||||
comp.classifiers[b"type-a"].predict_proba = Mock( # type: ignore
|
||||
side_effect=lambda _: np.array([[0.1, 0.9], [0.8, 0.2]])
|
||||
)
|
||||
comp.classifiers[b"type-b"].predict_proba = Mock( # type: ignore
|
||||
side_effect=lambda _: np.array([[0.9, 0.1], [0.1, 0.9]])
|
||||
)
|
||||
pred = comp.sample_predict(
|
||||
training_instances[0],
|
||||
training_instances[0].get_samples()[0],
|
||||
)
|
||||
assert pred == [b"c1", b"c4"]
|
||||
ev = comp.sample_evaluate(
|
||||
training_instances[0],
|
||||
training_instances[0].get_samples()[0],
|
||||
)
|
||||
assert ev == classifier_evaluation_dict(tp=1, fp=1, tn=1, fn=1)
|
||||
@@ -1,105 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, List, Dict
|
||||
|
||||
import gurobipy
|
||||
import gurobipy as gp
|
||||
import networkx as nx
|
||||
import pytest
|
||||
from gurobipy import GRB
|
||||
from networkx import Graph
|
||||
from overrides import overrides
|
||||
|
||||
from miplearn.components.dynamic_user_cuts import UserCutsComponent
|
||||
from miplearn.instance.base import Instance
|
||||
from miplearn.solvers.gurobi import GurobiSolver
|
||||
from miplearn.solvers.learning import LearningSolver
|
||||
from miplearn.types import ConstraintName
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GurobiStableSetProblem(Instance):
|
||||
def __init__(self, graph: Graph) -> None:
|
||||
super().__init__()
|
||||
self.graph: Graph = graph
|
||||
|
||||
@overrides
|
||||
def to_model(self) -> Any:
|
||||
model = gp.Model()
|
||||
x = [model.addVar(vtype=GRB.BINARY) for _ in range(len(self.graph.nodes))]
|
||||
model.setObjective(gp.quicksum(x), GRB.MAXIMIZE)
|
||||
for e in list(self.graph.edges):
|
||||
model.addConstr(x[e[0]] + x[e[1]] <= 1)
|
||||
return model
|
||||
|
||||
@overrides
|
||||
def has_user_cuts(self) -> bool:
|
||||
return True
|
||||
|
||||
@overrides
|
||||
def find_violated_user_cuts(self, model: Any) -> Dict[ConstraintName, Any]:
|
||||
assert isinstance(model, gp.Model)
|
||||
try:
|
||||
vals = model.cbGetNodeRel(model.getVars())
|
||||
except gurobipy.GurobiError:
|
||||
return {}
|
||||
violations = {}
|
||||
for clique in nx.find_cliques(self.graph):
|
||||
if sum(vals[i] for i in clique) > 1:
|
||||
vname = (",".join([str(i) for i in clique])).encode()
|
||||
violations[vname] = list(clique)
|
||||
return violations
|
||||
|
||||
@overrides
|
||||
def enforce_user_cut(
|
||||
self,
|
||||
solver: GurobiSolver,
|
||||
model: Any,
|
||||
clique: List[int],
|
||||
) -> Any:
|
||||
x = model.getVars()
|
||||
constr = gp.quicksum([x[i] for i in clique]) <= 1
|
||||
if solver.cb_where:
|
||||
model.cbCut(constr)
|
||||
else:
|
||||
model.addConstr(constr)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stab_instance() -> Instance:
|
||||
graph = nx.generators.random_graphs.binomial_graph(50, 0.50, seed=42)
|
||||
return GurobiStableSetProblem(graph)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def solver() -> LearningSolver:
|
||||
return LearningSolver(
|
||||
solver=GurobiSolver(params={"Threads": 1}),
|
||||
components=[UserCutsComponent()],
|
||||
)
|
||||
|
||||
|
||||
def test_usage(
|
||||
stab_instance: Instance,
|
||||
solver: LearningSolver,
|
||||
) -> None:
|
||||
stats_before = solver._solve(stab_instance)
|
||||
sample = stab_instance.get_samples()[0]
|
||||
user_cuts_encoded = sample.get_scalar("mip_user_cuts")
|
||||
assert user_cuts_encoded is not None
|
||||
user_cuts = json.loads(user_cuts_encoded)
|
||||
assert user_cuts is not None
|
||||
assert len(user_cuts) > 0
|
||||
assert stats_before["UserCuts: Added ahead-of-time"] == 0
|
||||
assert stats_before["UserCuts: Added in callback"] > 0
|
||||
|
||||
solver._fit([stab_instance])
|
||||
stats_after = solver._solve(stab_instance)
|
||||
assert (
|
||||
stats_after["UserCuts: Added ahead-of-time"]
|
||||
== stats_before["UserCuts: Added in callback"]
|
||||
)
|
||||
@@ -1,141 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from typing import Dict
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_array_equal
|
||||
|
||||
from miplearn.classifiers import Regressor
|
||||
from miplearn.components.objective import ObjectiveValueComponent
|
||||
from miplearn.features.sample import Sample, MemorySample
|
||||
from miplearn.solvers.learning import LearningSolver
|
||||
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
|
||||
from miplearn.solvers.tests import assert_equals
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample() -> Sample:
|
||||
sample = MemorySample(
|
||||
{
|
||||
"mip_lower_bound": 1.0,
|
||||
"mip_upper_bound": 2.0,
|
||||
"lp_instance_features": np.array([1.0, 2.0, 3.0]),
|
||||
},
|
||||
)
|
||||
return sample
|
||||
|
||||
|
||||
def test_sample_xy(sample: Sample) -> None:
|
||||
x_expected = {
|
||||
"Lower bound": np.array([[1.0, 2.0, 3.0]]),
|
||||
"Upper bound": np.array([[1.0, 2.0, 3.0]]),
|
||||
}
|
||||
y_expected = {
|
||||
"Lower bound": np.array([[1.0]]),
|
||||
"Upper bound": np.array([[2.0]]),
|
||||
}
|
||||
xy = ObjectiveValueComponent().sample_xy(None, sample)
|
||||
assert xy is not None
|
||||
x_actual, y_actual = xy
|
||||
assert_equals(x_actual, x_expected)
|
||||
assert_equals(y_actual, y_expected)
|
||||
|
||||
|
||||
def test_fit_xy() -> None:
|
||||
x: Dict[str, np.ndarray] = {
|
||||
"Lower bound": np.array([[0.0, 0.0], [1.0, 2.0]]),
|
||||
"Upper bound": np.array([[0.0, 0.0], [1.0, 2.0]]),
|
||||
}
|
||||
y: Dict[str, np.ndarray] = {
|
||||
"Lower bound": np.array([[100.0]]),
|
||||
"Upper bound": np.array([[200.0]]),
|
||||
}
|
||||
reg = Mock(spec=Regressor)
|
||||
reg.clone = Mock(side_effect=lambda: Mock(spec=Regressor))
|
||||
comp = ObjectiveValueComponent(regressor=reg)
|
||||
assert "Upper bound" not in comp.regressors
|
||||
assert "Lower bound" not in comp.regressors
|
||||
comp.fit_xy(x, y)
|
||||
assert reg.clone.call_count == 2
|
||||
assert "Upper bound" in comp.regressors
|
||||
assert "Lower bound" in comp.regressors
|
||||
assert comp.regressors["Upper bound"].fit.call_count == 1 # type: ignore
|
||||
assert comp.regressors["Lower bound"].fit.call_count == 1 # type: ignore
|
||||
assert_array_equal(
|
||||
comp.regressors["Upper bound"].fit.call_args[0][0], # type: ignore
|
||||
x["Upper bound"],
|
||||
)
|
||||
assert_array_equal(
|
||||
comp.regressors["Lower bound"].fit.call_args[0][0], # type: ignore
|
||||
x["Lower bound"],
|
||||
)
|
||||
assert_array_equal(
|
||||
comp.regressors["Upper bound"].fit.call_args[0][1], # type: ignore
|
||||
y["Upper bound"],
|
||||
)
|
||||
assert_array_equal(
|
||||
comp.regressors["Lower bound"].fit.call_args[0][1], # type: ignore
|
||||
y["Lower bound"],
|
||||
)
|
||||
|
||||
|
||||
def test_sample_predict(sample: Sample) -> None:
|
||||
x, y = ObjectiveValueComponent().sample_xy(None, sample)
|
||||
comp = ObjectiveValueComponent()
|
||||
comp.regressors["Lower bound"] = Mock(spec=Regressor)
|
||||
comp.regressors["Upper bound"] = Mock(spec=Regressor)
|
||||
comp.regressors["Lower bound"].predict = Mock( # type: ignore
|
||||
side_effect=lambda _: np.array([[50.0]])
|
||||
)
|
||||
comp.regressors["Upper bound"].predict = Mock( # type: ignore
|
||||
side_effect=lambda _: np.array([[60.0]])
|
||||
)
|
||||
pred = comp.sample_predict(sample)
|
||||
assert pred == {
|
||||
"Lower bound": 50.0,
|
||||
"Upper bound": 60.0,
|
||||
}
|
||||
assert_array_equal(
|
||||
comp.regressors["Upper bound"].predict.call_args[0][0], # type: ignore
|
||||
x["Upper bound"],
|
||||
)
|
||||
assert_array_equal(
|
||||
comp.regressors["Lower bound"].predict.call_args[0][0], # type: ignore
|
||||
x["Lower bound"],
|
||||
)
|
||||
|
||||
|
||||
def test_sample_evaluate(sample: Sample) -> None:
|
||||
comp = ObjectiveValueComponent()
|
||||
comp.regressors["Lower bound"] = Mock(spec=Regressor)
|
||||
comp.regressors["Lower bound"].predict = lambda _: np.array([[1.05]]) # type: ignore
|
||||
comp.regressors["Upper bound"] = Mock(spec=Regressor)
|
||||
comp.regressors["Upper bound"].predict = lambda _: np.array([[2.50]]) # type: ignore
|
||||
ev = comp.sample_evaluate(None, sample)
|
||||
assert ev == {
|
||||
"Lower bound": {
|
||||
"Actual value": 1.0,
|
||||
"Predicted value": 1.05,
|
||||
"Absolute error": 0.05,
|
||||
"Relative error": 0.05,
|
||||
},
|
||||
"Upper bound": {
|
||||
"Actual value": 2.0,
|
||||
"Predicted value": 2.50,
|
||||
"Absolute error": 0.5,
|
||||
"Relative error": 0.25,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_usage() -> None:
|
||||
solver = LearningSolver(components=[ObjectiveValueComponent()])
|
||||
instance = GurobiPyomoSolver().build_test_instance_knapsack()
|
||||
solver._solve(instance)
|
||||
solver._fit([instance])
|
||||
stats = solver._solve(instance)
|
||||
assert stats["mip_lower_bound"] == stats["Objective: Predicted lower bound"]
|
||||
assert stats["mip_upper_bound"] == stats["Objective: Predicted upper bound"]
|
||||
@@ -1,166 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_array_equal
|
||||
from scipy.stats import randint
|
||||
|
||||
from miplearn.classifiers import Classifier
|
||||
from miplearn.classifiers.threshold import Threshold
|
||||
from miplearn.components import classifier_evaluation_dict
|
||||
from miplearn.components.primal import PrimalSolutionComponent
|
||||
from miplearn.features.sample import Sample, MemorySample
|
||||
from miplearn.problems.tsp import TravelingSalesmanGenerator, TravelingSalesmanInstance
|
||||
from miplearn.solvers.learning import LearningSolver
|
||||
from miplearn.solvers.tests import assert_equals
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample() -> Sample:
|
||||
sample = MemorySample(
|
||||
{
|
||||
"static_var_names": np.array(["x[0]", "x[1]", "x[2]", "x[3]"], dtype="S"),
|
||||
"static_var_types": np.array(["B", "B", "B", "B"], dtype="S"),
|
||||
"static_var_categories": np.array(
|
||||
["default", "", "default", "default"],
|
||||
dtype="S",
|
||||
),
|
||||
"mip_var_values": np.array([0.0, 1.0, 1.0, 0.0]),
|
||||
"static_instance_features": np.array([5.0]),
|
||||
"static_var_features": np.array(
|
||||
[
|
||||
[0.0, 0.0],
|
||||
[0.0, 0.0],
|
||||
[1.0, 0.0],
|
||||
[1.0, 1.0],
|
||||
]
|
||||
),
|
||||
"lp_var_features": np.array(
|
||||
[
|
||||
[0.0, 0.0, 2.0, 2.0],
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
[1.0, 0.0, 3.0, 2.0],
|
||||
[1.0, 1.0, 3.0, 3.0],
|
||||
]
|
||||
),
|
||||
},
|
||||
)
|
||||
return sample
|
||||
|
||||
|
||||
def test_xy(sample: Sample) -> None:
|
||||
x_expected = {
|
||||
b"default": [
|
||||
[5.0, 0.0, 0.0, 2.0, 2.0],
|
||||
[5.0, 1.0, 0.0, 3.0, 2.0],
|
||||
[5.0, 1.0, 1.0, 3.0, 3.0],
|
||||
]
|
||||
}
|
||||
y_expected = {
|
||||
b"default": [
|
||||
[True, False],
|
||||
[False, True],
|
||||
[True, False],
|
||||
]
|
||||
}
|
||||
xy = PrimalSolutionComponent().sample_xy(None, sample)
|
||||
assert xy is not None
|
||||
x_actual, y_actual = xy
|
||||
assert x_actual == x_expected
|
||||
assert y_actual == y_expected
|
||||
|
||||
|
||||
def test_fit_xy() -> None:
|
||||
clf = Mock(spec=Classifier)
|
||||
clf.clone = lambda: Mock(spec=Classifier) # type: ignore
|
||||
thr = Mock(spec=Threshold)
|
||||
thr.clone = lambda: Mock(spec=Threshold)
|
||||
comp = PrimalSolutionComponent(classifier=clf, threshold=thr)
|
||||
x = {
|
||||
b"type-a": np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]),
|
||||
b"type-b": np.array([[7.0, 8.0, 9.0]]),
|
||||
}
|
||||
y = {
|
||||
b"type-a": np.array([[True, False], [False, True]]),
|
||||
b"type-b": np.array([[True, False]]),
|
||||
}
|
||||
comp.fit_xy(x, y)
|
||||
for category in [b"type-a", b"type-b"]:
|
||||
assert category in comp.classifiers
|
||||
assert category in comp.thresholds
|
||||
clf = comp.classifiers[category] # type: ignore
|
||||
clf.fit.assert_called_once()
|
||||
assert_array_equal(x[category], clf.fit.call_args[0][0])
|
||||
assert_array_equal(y[category], clf.fit.call_args[0][1])
|
||||
thr = comp.thresholds[category] # type: ignore
|
||||
thr.fit.assert_called_once()
|
||||
assert_array_equal(x[category], thr.fit.call_args[0][1])
|
||||
assert_array_equal(y[category], thr.fit.call_args[0][2])
|
||||
|
||||
|
||||
def test_usage() -> None:
|
||||
solver = LearningSolver(
|
||||
components=[
|
||||
PrimalSolutionComponent(),
|
||||
]
|
||||
)
|
||||
gen = TravelingSalesmanGenerator(n=randint(low=5, high=6))
|
||||
data = gen.generate(1)
|
||||
instance = TravelingSalesmanInstance(data[0].n_cities, data[0].distances)
|
||||
solver._solve(instance)
|
||||
solver._fit([instance])
|
||||
stats = solver._solve(instance)
|
||||
assert stats["Primal: Free"] == 0
|
||||
assert stats["Primal: One"] + stats["Primal: Zero"] == 10
|
||||
assert stats["mip_lower_bound"] == stats["mip_warm_start_value"]
|
||||
|
||||
|
||||
def test_evaluate(sample: Sample) -> None:
|
||||
comp = PrimalSolutionComponent()
|
||||
comp.sample_predict = lambda _: { # type: ignore
|
||||
b"x[0]": 1.0,
|
||||
b"x[1]": 1.0,
|
||||
b"x[2]": 0.0,
|
||||
b"x[3]": None,
|
||||
}
|
||||
ev = comp.sample_evaluate(None, sample)
|
||||
assert_equals(
|
||||
ev,
|
||||
{
|
||||
"0": classifier_evaluation_dict(tp=0, fp=1, tn=1, fn=2),
|
||||
"1": classifier_evaluation_dict(tp=1, fp=1, tn=1, fn=1),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_predict(sample: Sample) -> None:
|
||||
clf = Mock(spec=Classifier)
|
||||
clf.predict_proba = Mock(
|
||||
return_value=np.array(
|
||||
[
|
||||
[0.9, 0.1],
|
||||
[0.5, 0.5],
|
||||
[0.1, 0.9],
|
||||
]
|
||||
)
|
||||
)
|
||||
thr = Mock(spec=Threshold)
|
||||
thr.predict = Mock(return_value=[0.75, 0.75])
|
||||
comp = PrimalSolutionComponent()
|
||||
x, _ = comp.sample_xy(None, sample)
|
||||
comp.classifiers = {b"default": clf}
|
||||
comp.thresholds = {b"default": thr}
|
||||
pred = comp.sample_predict(sample)
|
||||
clf.predict_proba.assert_called_once()
|
||||
thr.predict.assert_called_once()
|
||||
assert_array_equal(x[b"default"], clf.predict_proba.call_args[0][0])
|
||||
assert_array_equal(x[b"default"], thr.predict.call_args[0][0])
|
||||
assert pred == {
|
||||
b"x[0]": 0.0,
|
||||
b"x[1]": None,
|
||||
b"x[2]": None,
|
||||
b"x[3]": 1.0,
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from typing import Dict, cast
|
||||
from unittest.mock import Mock, call
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_array_equal
|
||||
|
||||
from miplearn.classifiers import Classifier
|
||||
from miplearn.classifiers.threshold import Threshold, MinProbabilityThreshold
|
||||
from miplearn.components.static_lazy import StaticLazyConstraintsComponent
|
||||
from miplearn.features.sample import Sample, MemorySample
|
||||
from miplearn.instance.base import Instance
|
||||
from miplearn.solvers.internal import InternalSolver, Constraints
|
||||
from miplearn.solvers.learning import LearningSolver
|
||||
from miplearn.types import (
|
||||
LearningSolveStats,
|
||||
ConstraintCategory,
|
||||
)
|
||||
from miplearn.solvers.tests import assert_equals
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample() -> Sample:
|
||||
sample = MemorySample(
|
||||
{
|
||||
"static_constr_categories": [
|
||||
b"type-a",
|
||||
b"type-a",
|
||||
b"type-a",
|
||||
b"type-b",
|
||||
b"type-b",
|
||||
],
|
||||
"static_constr_lazy": np.array([True, True, True, True, False]),
|
||||
"static_constr_names": np.array(["c1", "c2", "c3", "c4", "c5"], dtype="S"),
|
||||
"static_instance_features": [5.0],
|
||||
"mip_constr_lazy_enforced": np.array(["c1", "c2", "c4"], dtype="S"),
|
||||
"lp_constr_features": np.array(
|
||||
[
|
||||
[1.0, 1.0, 0.0],
|
||||
[1.0, 2.0, 0.0],
|
||||
[1.0, 3.0, 0.0],
|
||||
[1.0, 4.0, 0.0],
|
||||
[0.0, 0.0, 0.0],
|
||||
]
|
||||
),
|
||||
"static_constr_lazy_count": 4,
|
||||
},
|
||||
)
|
||||
return sample
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def instance(sample: Sample) -> Instance:
|
||||
instance = Mock(spec=Instance)
|
||||
instance.get_samples = Mock(return_value=[sample]) # type: ignore
|
||||
instance.has_static_lazy_constraints = Mock(return_value=True)
|
||||
return instance
|
||||
|
||||
|
||||
def test_usage_with_solver(instance: Instance) -> None:
|
||||
solver = Mock(spec=LearningSolver)
|
||||
solver.use_lazy_cb = False
|
||||
solver.gap_tolerance = 1e-4
|
||||
|
||||
internal = solver.internal_solver = Mock(spec=InternalSolver)
|
||||
internal.is_constraint_satisfied_old = Mock(return_value=False)
|
||||
internal.are_constraints_satisfied = Mock(
|
||||
side_effect=lambda cf, tol=1.0: [False for i in range(len(cf.names))]
|
||||
)
|
||||
|
||||
component = StaticLazyConstraintsComponent(violation_tolerance=1.0)
|
||||
component.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5])
|
||||
component.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5])
|
||||
component.classifiers = {
|
||||
b"type-a": Mock(spec=Classifier),
|
||||
b"type-b": Mock(spec=Classifier),
|
||||
}
|
||||
component.classifiers[b"type-a"].predict_proba = Mock( # type: ignore
|
||||
return_value=np.array(
|
||||
[
|
||||
[0.00, 1.00], # c1
|
||||
[0.20, 0.80], # c2
|
||||
[0.99, 0.01], # c3
|
||||
]
|
||||
)
|
||||
)
|
||||
component.classifiers[b"type-b"].predict_proba = Mock( # type: ignore
|
||||
return_value=np.array(
|
||||
[
|
||||
[0.02, 0.98], # c4
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
stats: LearningSolveStats = {}
|
||||
sample = instance.get_samples()[0]
|
||||
assert sample.get_array("mip_constr_lazy_enforced") is not None
|
||||
|
||||
# LearningSolver calls before_solve_mip
|
||||
component.before_solve_mip(
|
||||
solver=solver,
|
||||
instance=instance,
|
||||
model=None,
|
||||
stats=stats,
|
||||
sample=sample,
|
||||
)
|
||||
|
||||
# Should ask ML to predict whether each lazy constraint should be enforced
|
||||
component.classifiers[b"type-a"].predict_proba.assert_called_once()
|
||||
component.classifiers[b"type-b"].predict_proba.assert_called_once()
|
||||
|
||||
# Should ask internal solver to remove some constraints
|
||||
assert internal.remove_constraints.call_count == 1
|
||||
internal.remove_constraints.assert_has_calls([call([b"c3"])])
|
||||
|
||||
# LearningSolver calls after_iteration (first time)
|
||||
should_repeat = component.iteration_cb(solver, instance, None)
|
||||
assert should_repeat
|
||||
|
||||
# Should ask internal solver to verify if constraints in the pool are
|
||||
# satisfied and add the ones that are not
|
||||
c = Constraints.from_sample(sample)[[False, False, True, False, False]]
|
||||
internal.are_constraints_satisfied.assert_called_once_with(c, tol=1.0)
|
||||
internal.are_constraints_satisfied.reset_mock()
|
||||
internal.add_constraints.assert_called_once_with(c)
|
||||
internal.add_constraints.reset_mock()
|
||||
|
||||
# LearningSolver calls after_iteration (second time)
|
||||
should_repeat = component.iteration_cb(solver, instance, None)
|
||||
assert not should_repeat
|
||||
|
||||
# The lazy constraint pool should be empty by now, so no calls should be made
|
||||
internal.are_constraints_satisfied.assert_not_called()
|
||||
internal.add_constraints.assert_not_called()
|
||||
|
||||
# LearningSolver calls after_solve_mip
|
||||
component.after_solve_mip(
|
||||
solver=solver,
|
||||
instance=instance,
|
||||
model=None,
|
||||
stats=stats,
|
||||
sample=sample,
|
||||
)
|
||||
|
||||
# Should update training sample
|
||||
mip_constr_lazy_enforced = sample.get_array("mip_constr_lazy_enforced")
|
||||
assert mip_constr_lazy_enforced is not None
|
||||
assert_equals(
|
||||
sorted(mip_constr_lazy_enforced),
|
||||
np.array(["c1", "c2", "c3", "c4"], dtype="S"),
|
||||
)
|
||||
|
||||
# Should update stats
|
||||
assert stats["LazyStatic: Removed"] == 1
|
||||
assert stats["LazyStatic: Kept"] == 3
|
||||
assert stats["LazyStatic: Restored"] == 1
|
||||
assert stats["LazyStatic: Iterations"] == 1
|
||||
|
||||
|
||||
def test_sample_predict(sample: Sample) -> None:
|
||||
comp = StaticLazyConstraintsComponent()
|
||||
comp.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5])
|
||||
comp.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5])
|
||||
comp.classifiers[b"type-a"] = Mock(spec=Classifier)
|
||||
comp.classifiers[b"type-a"].predict_proba = lambda _: np.array( # type:ignore
|
||||
[
|
||||
[0.0, 1.0], # c1
|
||||
[0.0, 0.9], # c2
|
||||
[0.9, 0.1], # c3
|
||||
]
|
||||
)
|
||||
comp.classifiers[b"type-b"] = Mock(spec=Classifier)
|
||||
comp.classifiers[b"type-b"].predict_proba = lambda _: np.array( # type:ignore
|
||||
[
|
||||
[0.0, 1.0], # c4
|
||||
]
|
||||
)
|
||||
pred = comp.sample_predict(sample)
|
||||
assert pred == [b"c1", b"c2", b"c4"]
|
||||
|
||||
|
||||
def test_fit_xy() -> None:
|
||||
x = cast(
|
||||
Dict[ConstraintCategory, np.ndarray],
|
||||
{
|
||||
b"type-a": np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]]),
|
||||
b"type-b": np.array([[1.0, 4.0, 0.0]]),
|
||||
},
|
||||
)
|
||||
y = cast(
|
||||
Dict[ConstraintCategory, np.ndarray],
|
||||
{
|
||||
b"type-a": np.array([[False, True], [False, True], [True, False]]),
|
||||
b"type-b": np.array([[False, True]]),
|
||||
},
|
||||
)
|
||||
clf: Classifier = Mock(spec=Classifier)
|
||||
thr: Threshold = Mock(spec=Threshold)
|
||||
clf.clone = Mock(side_effect=lambda: Mock(spec=Classifier)) # type: ignore
|
||||
thr.clone = Mock(side_effect=lambda: Mock(spec=Threshold)) # type: ignore
|
||||
comp = StaticLazyConstraintsComponent(
|
||||
classifier=clf,
|
||||
threshold=thr,
|
||||
)
|
||||
comp.fit_xy(x, y)
|
||||
assert clf.clone.call_count == 2
|
||||
clf_a = comp.classifiers[b"type-a"]
|
||||
clf_b = comp.classifiers[b"type-b"]
|
||||
assert clf_a.fit.call_count == 1 # type: ignore
|
||||
assert clf_b.fit.call_count == 1 # type: ignore
|
||||
assert_array_equal(clf_a.fit.call_args[0][0], x[b"type-a"]) # type: ignore
|
||||
assert_array_equal(clf_b.fit.call_args[0][0], x[b"type-b"]) # type: ignore
|
||||
assert thr.clone.call_count == 2
|
||||
thr_a = comp.thresholds[b"type-a"]
|
||||
thr_b = comp.thresholds[b"type-b"]
|
||||
assert thr_a.fit.call_count == 1 # type: ignore
|
||||
assert thr_b.fit.call_count == 1 # type: ignore
|
||||
assert thr_a.fit.call_args[0][0] == clf_a # type: ignore
|
||||
assert thr_b.fit.call_args[0][0] == clf_b # type: ignore
|
||||
|
||||
|
||||
def test_sample_xy(sample: Sample) -> None:
|
||||
x_expected = {
|
||||
b"type-a": [[5.0, 1.0, 1.0, 0.0], [5.0, 1.0, 2.0, 0.0], [5.0, 1.0, 3.0, 0.0]],
|
||||
b"type-b": [[5.0, 1.0, 4.0, 0.0]],
|
||||
}
|
||||
y_expected = {
|
||||
b"type-a": [[False, True], [False, True], [True, False]],
|
||||
b"type-b": [[False, True]],
|
||||
}
|
||||
xy = StaticLazyConstraintsComponent().sample_xy(None, sample)
|
||||
assert xy is not None
|
||||
x_actual, y_actual = xy
|
||||
assert x_actual == x_expected
|
||||
assert y_actual == y_expected
|
||||
25
tests/conftest.py
Normal file
25
tests/conftest.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
from glob import glob
|
||||
from os.path import dirname
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from miplearn.extractors.fields import H5FieldsExtractor
|
||||
from miplearn.extractors.abstract import FeaturesExtractor
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def multiknapsack_h5() -> List[str]:
|
||||
return sorted(glob(f"{dirname(__file__)}/fixtures/multiknapsack*.h5"))
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def default_extractor() -> FeaturesExtractor:
|
||||
return H5FieldsExtractor(
|
||||
instance_fields=["static_var_obj_coeffs"],
|
||||
var_fields=["lp_var_features"],
|
||||
)
|
||||
@@ -1,3 +1,3 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
19
tests/extractors/test_dummy.py
Normal file
19
tests/extractors/test_dummy.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
from typing import List
|
||||
|
||||
from miplearn.extractors.dummy import DummyExtractor
|
||||
from miplearn.h5 import H5File
|
||||
|
||||
|
||||
def test_dummy(multiknapsack_h5: List[str]) -> None:
|
||||
ext = DummyExtractor()
|
||||
with H5File(multiknapsack_h5[0], "r") as h5:
|
||||
x = ext.get_instance_features(h5)
|
||||
assert x.shape == (1,)
|
||||
x = ext.get_var_features(h5)
|
||||
assert x.shape == (100, 1)
|
||||
x = ext.get_constr_features(h5)
|
||||
assert x.shape == (4, 1)
|
||||
33
tests/extractors/test_fields.py
Normal file
33
tests/extractors/test_fields.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from miplearn.extractors.fields import H5FieldsExtractor
|
||||
from miplearn.h5 import H5File
|
||||
|
||||
|
||||
def test_fields_instance(multiknapsack_h5: List[str]) -> None:
|
||||
ext = H5FieldsExtractor(
|
||||
instance_fields=[
|
||||
"lp_obj_value",
|
||||
"lp_var_values",
|
||||
"static_var_obj_coeffs",
|
||||
],
|
||||
var_fields=["lp_var_values"],
|
||||
)
|
||||
with H5File(multiknapsack_h5[0], "r") as h5:
|
||||
x = ext.get_instance_features(h5)
|
||||
assert x.shape == (201,)
|
||||
|
||||
x = ext.get_var_features(h5)
|
||||
assert x.shape == (100, 1)
|
||||
|
||||
|
||||
def test_fields_instance_none(multiknapsack_h5: List[str]) -> None:
|
||||
ext = H5FieldsExtractor(instance_fields=None)
|
||||
with H5File(multiknapsack_h5[0], "r") as h5:
|
||||
with pytest.raises(Exception):
|
||||
ext.get_instance_features(h5)
|
||||
@@ -1,709 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import cProfile
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import gurobipy as gp
|
||||
import numpy as np
|
||||
from scipy.sparse import coo_matrix
|
||||
|
||||
from miplearn.features.extractor import FeaturesExtractor
|
||||
from miplearn.features.sample import Hdf5Sample, MemorySample
|
||||
from miplearn.instance.base import Instance
|
||||
from miplearn.solvers.gurobi import GurobiSolver
|
||||
from miplearn.solvers.internal import Variables, Constraints
|
||||
from miplearn.solvers.tests import assert_equals
|
||||
|
||||
inf = float("inf")
|
||||
|
||||
|
||||
def test_knapsack() -> None:
|
||||
solver = GurobiSolver()
|
||||
instance = solver.build_test_instance_knapsack()
|
||||
model = instance.to_model()
|
||||
solver.set_instance(instance, model)
|
||||
extractor = FeaturesExtractor()
|
||||
sample = MemorySample()
|
||||
|
||||
# after-load
|
||||
# -------------------------------------------------------
|
||||
extractor.extract_after_load_features(instance, solver, sample)
|
||||
assert_equals(
|
||||
sample.get_array("static_instance_features"),
|
||||
np.array([67.0, 21.75]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_var_names"),
|
||||
np.array(["x[0]", "x[1]", "x[2]", "x[3]", "z"], dtype="S"),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_var_lower_bounds"),
|
||||
np.array([0.0, 0.0, 0.0, 0.0, 0.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_var_obj_coeffs"),
|
||||
np.array([505.0, 352.0, 458.0, 220.0, 0.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_var_types"),
|
||||
np.array(["B", "B", "B", "B", "C"], dtype="S"),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_var_upper_bounds"),
|
||||
np.array([1.0, 1.0, 1.0, 1.0, 67.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_var_categories"),
|
||||
np.array(["default", "default", "default", "default", ""], dtype="S"),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_var_features"),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
23.0,
|
||||
505.0,
|
||||
1.0,
|
||||
0.32899,
|
||||
1e20,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
21.956522,
|
||||
1.0,
|
||||
21.956522,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.264368,
|
||||
1.0,
|
||||
0.264368,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
23.0,
|
||||
1.0,
|
||||
23.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
],
|
||||
[
|
||||
26.0,
|
||||
352.0,
|
||||
1.0,
|
||||
0.229316,
|
||||
1e20,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
13.538462,
|
||||
1.0,
|
||||
13.538462,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.298851,
|
||||
1.0,
|
||||
0.298851,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
26.0,
|
||||
1.0,
|
||||
26.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
],
|
||||
[
|
||||
20.0,
|
||||
458.0,
|
||||
1.0,
|
||||
0.298371,
|
||||
1e20,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
22.9,
|
||||
1.0,
|
||||
22.9,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.229885,
|
||||
1.0,
|
||||
0.229885,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
20.0,
|
||||
1.0,
|
||||
20.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
],
|
||||
[
|
||||
18.0,
|
||||
220.0,
|
||||
1.0,
|
||||
0.143322,
|
||||
1e20,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
12.222222,
|
||||
1.0,
|
||||
12.222222,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.206897,
|
||||
1.0,
|
||||
0.206897,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
18.0,
|
||||
1.0,
|
||||
18.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.011494,
|
||||
1.0,
|
||||
0.011494,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
],
|
||||
]
|
||||
),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_constr_names"),
|
||||
np.array(["eq_capacity"], dtype="S"),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_sparse("static_constr_lhs"),
|
||||
[[23.0, 26.0, 20.0, 18.0, -1.0]],
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_constr_rhs"),
|
||||
np.array([0.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_constr_senses"),
|
||||
np.array(["="], dtype="S"),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_constr_features"),
|
||||
np.array([[0.0]]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_constr_categories"),
|
||||
np.array(["eq_capacity"], dtype="S"),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_constr_lazy"),
|
||||
np.array([False]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("static_instance_features"),
|
||||
np.array([67.0, 21.75]),
|
||||
)
|
||||
assert_equals(sample.get_scalar("static_constr_lazy_count"), 0)
|
||||
|
||||
# after-lp
|
||||
# -------------------------------------------------------
|
||||
lp_stats = solver.solve_lp()
|
||||
extractor.extract_after_lp_features(solver, sample, lp_stats)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_basis_status"),
|
||||
np.array(["U", "B", "U", "L", "U"], dtype="S"),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_reduced_costs"),
|
||||
[193.615385, 0.0, 187.230769, -23.692308, 13.538462],
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_sa_lb_down"),
|
||||
[-inf, -inf, -inf, -0.111111, -inf],
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_sa_lb_up"),
|
||||
[1.0, 0.923077, 1.0, 1.0, 67.0],
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_sa_obj_down"),
|
||||
[311.384615, 317.777778, 270.769231, -inf, -13.538462],
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_sa_obj_up"),
|
||||
[inf, 570.869565, inf, 243.692308, inf],
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_sa_ub_down"),
|
||||
np.array([0.913043, 0.923077, 0.9, 0.0, 43.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_sa_ub_up"),
|
||||
np.array([2.043478, inf, 2.2, inf, 69.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_values"),
|
||||
np.array([1.0, 0.923077, 1.0, 0.0, 67.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_features"),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
23.0,
|
||||
505.0,
|
||||
1.0,
|
||||
0.32899,
|
||||
1e20,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
21.956522,
|
||||
1.0,
|
||||
21.956522,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.264368,
|
||||
1.0,
|
||||
0.264368,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
23.0,
|
||||
1.0,
|
||||
23.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
5.265874,
|
||||
0.0,
|
||||
193.615385,
|
||||
-0.111111,
|
||||
1.0,
|
||||
311.384615,
|
||||
570.869565,
|
||||
0.913043,
|
||||
2.043478,
|
||||
1.0,
|
||||
],
|
||||
[
|
||||
26.0,
|
||||
352.0,
|
||||
1.0,
|
||||
0.229316,
|
||||
1e20,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
13.538462,
|
||||
1.0,
|
||||
13.538462,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.298851,
|
||||
1.0,
|
||||
0.298851,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
26.0,
|
||||
1.0,
|
||||
26.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.076923,
|
||||
1.0,
|
||||
1.0,
|
||||
3.532875,
|
||||
0.0,
|
||||
0.0,
|
||||
-0.111111,
|
||||
0.923077,
|
||||
317.777778,
|
||||
570.869565,
|
||||
0.923077,
|
||||
69.0,
|
||||
0.923077,
|
||||
],
|
||||
[
|
||||
20.0,
|
||||
458.0,
|
||||
1.0,
|
||||
0.298371,
|
||||
1e20,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
22.9,
|
||||
1.0,
|
||||
22.9,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.229885,
|
||||
1.0,
|
||||
0.229885,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
20.0,
|
||||
1.0,
|
||||
20.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
5.232342,
|
||||
0.0,
|
||||
187.230769,
|
||||
-0.111111,
|
||||
1.0,
|
||||
270.769231,
|
||||
570.869565,
|
||||
0.9,
|
||||
2.2,
|
||||
1.0,
|
||||
],
|
||||
[
|
||||
18.0,
|
||||
220.0,
|
||||
1.0,
|
||||
0.143322,
|
||||
1e20,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
12.222222,
|
||||
1.0,
|
||||
12.222222,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.206897,
|
||||
1.0,
|
||||
0.206897,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
18.0,
|
||||
1.0,
|
||||
18.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
-1.0,
|
||||
5.265874,
|
||||
0.0,
|
||||
-23.692308,
|
||||
-0.111111,
|
||||
1.0,
|
||||
-13.538462,
|
||||
243.692308,
|
||||
0.0,
|
||||
69.0,
|
||||
0.0,
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.011494,
|
||||
1.0,
|
||||
0.011494,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
-1.0,
|
||||
5.265874,
|
||||
0.0,
|
||||
13.538462,
|
||||
-0.111111,
|
||||
67.0,
|
||||
-13.538462,
|
||||
570.869565,
|
||||
43.0,
|
||||
69.0,
|
||||
67.0,
|
||||
],
|
||||
]
|
||||
),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_constr_basis_status"),
|
||||
np.array(["N"], dtype="S"),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_constr_dual_values"),
|
||||
np.array([13.538462]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_constr_sa_rhs_down"),
|
||||
np.array([-24.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_constr_sa_rhs_up"),
|
||||
np.array([2.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_constr_slacks"),
|
||||
np.array([0.0]),
|
||||
)
|
||||
assert_equals(
|
||||
sample.get_array("lp_constr_features"),
|
||||
np.array([[0.0, 13.538462, -24.0, 2.0, 0.0]]),
|
||||
)
|
||||
|
||||
# after-mip
|
||||
# -------------------------------------------------------
|
||||
solver.solve()
|
||||
extractor.extract_after_mip_features(solver, sample)
|
||||
assert_equals(
|
||||
sample.get_array("mip_var_values"), np.array([1.0, 0.0, 1.0, 1.0, 61.0])
|
||||
)
|
||||
assert_equals(sample.get_array("mip_constr_slacks"), np.array([0.0]))
|
||||
|
||||
|
||||
def test_constraint_getindex() -> None:
|
||||
cf = Constraints(
|
||||
names=np.array(["c1", "c2", "c3"], dtype="S"),
|
||||
rhs=np.array([1.0, 2.0, 3.0]),
|
||||
senses=np.array(["=", "<", ">"], dtype="S"),
|
||||
lhs=coo_matrix(
|
||||
[
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
[7, 8, 9],
|
||||
]
|
||||
),
|
||||
)
|
||||
assert_equals(
|
||||
cf[[True, False, True]],
|
||||
Constraints(
|
||||
names=np.array(["c1", "c3"], dtype="S"),
|
||||
rhs=np.array([1.0, 3.0]),
|
||||
senses=np.array(["=", ">"], dtype="S"),
|
||||
lhs=coo_matrix(
|
||||
[
|
||||
[1, 2, 3],
|
||||
[7, 8, 9],
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_assert_equals() -> None:
|
||||
assert_equals("hello", "hello")
|
||||
assert_equals([1.0, 2.0], [1.0, 2.0])
|
||||
assert_equals(np.array([1.0, 2.0]), np.array([1.0, 2.0]))
|
||||
assert_equals(
|
||||
np.array([[1.0, 2.0], [3.0, 4.0]]),
|
||||
np.array([[1.0, 2.0], [3.0, 4.0]]),
|
||||
)
|
||||
assert_equals(
|
||||
Variables(values=np.array([1.0, 2.0])), # type: ignore
|
||||
Variables(values=np.array([1.0, 2.0])), # type: ignore
|
||||
)
|
||||
assert_equals(np.array([True, True]), [True, True])
|
||||
assert_equals((1.0,), (1.0,))
|
||||
assert_equals({"x": 10}, {"x": 10})
|
||||
|
||||
|
||||
class MpsInstance(Instance):
|
||||
def __init__(self, filename: str) -> None:
|
||||
super().__init__()
|
||||
self.filename = filename
|
||||
|
||||
def to_model(self) -> Any:
|
||||
return gp.read(self.filename)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
solver = GurobiSolver()
|
||||
instance = MpsInstance(sys.argv[1])
|
||||
solver.set_instance(instance)
|
||||
extractor = FeaturesExtractor(with_lhs=False)
|
||||
sample = Hdf5Sample("tmp/prof.h5", mode="w")
|
||||
extractor.extract_after_load_features(instance, solver, sample)
|
||||
lp_stats = solver.solve_lp(tee=True)
|
||||
extractor.extract_after_lp_features(solver, sample, lp_stats)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cProfile.run("main()", filename="tmp/prof")
|
||||
os.system("flameprof tmp/prof > tmp/prof.svg")
|
||||
@@ -1,71 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse import coo_matrix
|
||||
|
||||
from miplearn.features.sample import MemorySample, Sample, Hdf5Sample
|
||||
|
||||
|
||||
def test_memory_sample() -> None:
|
||||
_test_sample(MemorySample())
|
||||
|
||||
|
||||
def test_hdf5_sample() -> None:
|
||||
file = NamedTemporaryFile()
|
||||
_test_sample(Hdf5Sample(file.name))
|
||||
|
||||
|
||||
def _test_sample(sample: Sample) -> None:
|
||||
_assert_roundtrip_scalar(sample, "A")
|
||||
_assert_roundtrip_scalar(sample, True)
|
||||
_assert_roundtrip_scalar(sample, 1)
|
||||
_assert_roundtrip_scalar(sample, 1.0)
|
||||
assert sample.get_scalar("unknown-key") is None
|
||||
|
||||
_assert_roundtrip_array(sample, np.array([True, False]))
|
||||
_assert_roundtrip_array(sample, np.array([1, 2, 3]))
|
||||
_assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0]))
|
||||
_assert_roundtrip_array(sample, np.array(["A", "BB", "CCC"], dtype="S"))
|
||||
assert sample.get_array("unknown-key") is None
|
||||
|
||||
_assert_roundtrip_sparse(
|
||||
sample,
|
||||
coo_matrix(
|
||||
[
|
||||
[1.0, 0.0, 0.0],
|
||||
[0.0, 2.0, 3.0],
|
||||
[0.0, 0.0, 4.0],
|
||||
],
|
||||
),
|
||||
)
|
||||
assert sample.get_sparse("unknown-key") is None
|
||||
|
||||
|
||||
def _assert_roundtrip_array(sample: Sample, original: np.ndarray) -> None:
|
||||
sample.put_array("key", original)
|
||||
recovered = sample.get_array("key")
|
||||
assert recovered is not None
|
||||
assert isinstance(recovered, np.ndarray)
|
||||
assert (recovered == original).all()
|
||||
|
||||
|
||||
def _assert_roundtrip_scalar(sample: Sample, original: Any) -> None:
|
||||
sample.put_scalar("key", original)
|
||||
recovered = sample.get_scalar("key")
|
||||
assert recovered == original
|
||||
assert recovered is not None
|
||||
assert isinstance(
|
||||
recovered, original.__class__
|
||||
), f"Expected {original.__class__}, found {recovered.__class__} instead"
|
||||
|
||||
|
||||
def _assert_roundtrip_sparse(sample: Sample, original: coo_matrix) -> None:
|
||||
sample.put_sparse("key", original)
|
||||
recovered = sample.get_sparse("key")
|
||||
assert recovered is not None
|
||||
assert isinstance(recovered, coo_matrix)
|
||||
assert (original != recovered).sum() == 0
|
||||
@@ -1,32 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import tempfile
|
||||
|
||||
from miplearn.solvers.learning import LearningSolver
|
||||
from miplearn.solvers.gurobi import GurobiSolver
|
||||
from miplearn.features.sample import Hdf5Sample
|
||||
from miplearn.instance.file import FileInstance
|
||||
|
||||
|
||||
def test_usage() -> None:
|
||||
# Create original instance
|
||||
original = GurobiSolver().build_test_instance_knapsack()
|
||||
|
||||
# Save instance to disk
|
||||
filename = tempfile.mktemp()
|
||||
FileInstance.save(original, filename)
|
||||
sample = Hdf5Sample(filename)
|
||||
assert len(sample.get_array("pickled")) > 0
|
||||
|
||||
# Solve instance from disk
|
||||
solver = LearningSolver(solver=GurobiSolver())
|
||||
solver._solve(FileInstance(filename))
|
||||
|
||||
# Assert HDF5 contains training data
|
||||
sample = FileInstance(filename).get_samples()[0]
|
||||
assert sample.get_scalar("mip_lower_bound") == 1183.0
|
||||
assert sample.get_scalar("mip_upper_bound") == 1183.0
|
||||
assert len(sample.get_array("lp_var_values")) == 5
|
||||
assert len(sample.get_array("mip_var_values")) == 5
|
||||
@@ -1,33 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import tempfile
|
||||
from typing import cast, IO
|
||||
|
||||
from miplearn.instance.picklegz import write_pickle_gz, PickleGzInstance
|
||||
from miplearn.solvers.gurobi import GurobiSolver
|
||||
from miplearn import save
|
||||
from os.path import exists
|
||||
import gzip
|
||||
import pickle
|
||||
|
||||
|
||||
def test_usage() -> None:
|
||||
original = GurobiSolver().build_test_instance_knapsack()
|
||||
file = tempfile.NamedTemporaryFile()
|
||||
write_pickle_gz(original, file.name)
|
||||
pickled = PickleGzInstance(file.name)
|
||||
pickled.load()
|
||||
assert pickled.to_model() is not None
|
||||
|
||||
|
||||
def test_save() -> None:
|
||||
objs = [1, "ABC", True]
|
||||
with tempfile.TemporaryDirectory() as dirname:
|
||||
filenames = save(objs, dirname)
|
||||
assert len(filenames) == 3
|
||||
for (idx, f) in enumerate(filenames):
|
||||
assert exists(f)
|
||||
with gzip.GzipFile(f, "rb") as file:
|
||||
assert pickle.load(cast(IO[bytes], file)) == objs[idx]
|
||||
@@ -1,3 +1,3 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
58
tests/problems/test_binpack.py
Normal file
58
tests/problems/test_binpack.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import uniform, randint
|
||||
|
||||
from miplearn.problems.binpack import build_binpack_model, BinPackData, BinPackGenerator
|
||||
|
||||
|
||||
def test_binpack_generator() -> None:
|
||||
np.random.seed(42)
|
||||
gen = BinPackGenerator(
|
||||
n=randint(low=10, high=11),
|
||||
sizes=uniform(loc=0, scale=10),
|
||||
capacity=uniform(loc=100, scale=0),
|
||||
sizes_jitter=uniform(loc=0.9, scale=0.2),
|
||||
capacity_jitter=uniform(loc=0.9, scale=0.2),
|
||||
fix_items=True,
|
||||
)
|
||||
data = gen.generate(2)
|
||||
assert data[0].sizes.tolist() == [
|
||||
3.39,
|
||||
10.4,
|
||||
7.81,
|
||||
5.64,
|
||||
1.46,
|
||||
1.46,
|
||||
0.56,
|
||||
8.7,
|
||||
5.93,
|
||||
6.79,
|
||||
]
|
||||
assert data[0].capacity == 102.24
|
||||
assert data[1].sizes.tolist() == [
|
||||
3.48,
|
||||
9.11,
|
||||
7.12,
|
||||
5.93,
|
||||
1.65,
|
||||
1.47,
|
||||
0.58,
|
||||
8.82,
|
||||
5.47,
|
||||
7.23,
|
||||
]
|
||||
assert data[1].capacity == 93.41
|
||||
|
||||
|
||||
def test_binpack() -> None:
|
||||
model = build_binpack_model(
|
||||
BinPackData(
|
||||
sizes=np.array([4, 8, 1, 4, 2, 1]),
|
||||
capacity=10,
|
||||
)
|
||||
)
|
||||
model.optimize()
|
||||
assert model.inner.objVal == 2.0
|
||||
@@ -1,39 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import uniform, randint
|
||||
|
||||
from miplearn import LearningSolver
|
||||
from miplearn.problems.knapsack import MultiKnapsackGenerator, MultiKnapsackInstance
|
||||
|
||||
|
||||
def test_knapsack_generator() -> None:
|
||||
gen = MultiKnapsackGenerator(
|
||||
n=randint(low=100, high=101),
|
||||
m=randint(low=30, high=31),
|
||||
w=randint(low=0, high=1000),
|
||||
K=randint(low=500, high=501),
|
||||
u=uniform(loc=1.0, scale=1.0),
|
||||
alpha=uniform(loc=0.50, scale=0.0),
|
||||
)
|
||||
data = gen.generate(100)
|
||||
w_sum = sum(d.weights for d in data) / len(data)
|
||||
b_sum = sum(d.capacities for d in data) / len(data)
|
||||
assert round(float(np.mean(w_sum)), -1) == 500.0
|
||||
assert round(float(np.mean(b_sum)), -3) == 25000.0
|
||||
|
||||
|
||||
def test_knapsack() -> None:
|
||||
data = MultiKnapsackGenerator(
|
||||
n=randint(low=5, high=6),
|
||||
m=randint(low=5, high=6),
|
||||
).generate(1)
|
||||
instance = MultiKnapsackInstance(
|
||||
prices=data[0].prices,
|
||||
capacities=data[0].capacities,
|
||||
weights=data[0].weights,
|
||||
)
|
||||
solver = LearningSolver()
|
||||
solver._solve(instance)
|
||||
61
tests/problems/test_multiknapsack.py
Normal file
61
tests/problems/test_multiknapsack.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import uniform, randint
|
||||
|
||||
from miplearn.problems.multiknapsack import (
|
||||
MultiKnapsackGenerator,
|
||||
MultiKnapsackData,
|
||||
build_multiknapsack_model,
|
||||
)
|
||||
|
||||
|
||||
def test_knapsack_generator() -> None:
|
||||
np.random.seed(42)
|
||||
gen = MultiKnapsackGenerator(
|
||||
n=randint(low=5, high=6),
|
||||
m=randint(low=3, high=4),
|
||||
w=randint(low=0, high=1000),
|
||||
K=randint(low=500, high=501),
|
||||
u=uniform(loc=0.0, scale=1.0),
|
||||
alpha=uniform(loc=0.25, scale=0.0),
|
||||
fix_w=True,
|
||||
w_jitter=uniform(loc=0.9, scale=0.2),
|
||||
p_jitter=uniform(loc=0.9, scale=0.2),
|
||||
round=True,
|
||||
)
|
||||
data = gen.generate(2)
|
||||
assert data[0].prices.tolist() == [433.0, 477.0, 802.0, 494.0, 458.0]
|
||||
assert data[0].capacities.tolist() == [458.0, 357.0, 392.0]
|
||||
assert data[0].weights.tolist() == [
|
||||
[111.0, 392.0, 945.0, 276.0, 108.0],
|
||||
[64.0, 633.0, 20.0, 602.0, 110.0],
|
||||
[510.0, 203.0, 303.0, 469.0, 85.0],
|
||||
]
|
||||
|
||||
assert data[1].prices.tolist() == [344.0, 527.0, 658.0, 519.0, 460.0]
|
||||
assert data[1].capacities.tolist() == [449.0, 377.0, 380.0]
|
||||
assert data[1].weights.tolist() == [
|
||||
[92.0, 473.0, 871.0, 264.0, 96.0],
|
||||
[67.0, 664.0, 21.0, 628.0, 129.0],
|
||||
[436.0, 209.0, 309.0, 481.0, 86.0],
|
||||
]
|
||||
|
||||
|
||||
def test_knapsack_model() -> None:
|
||||
data = MultiKnapsackData(
|
||||
prices=np.array([344.0, 527.0, 658.0, 519.0, 460.0]),
|
||||
capacities=np.array([449.0, 377.0, 380.0]),
|
||||
weights=np.array(
|
||||
[
|
||||
[92.0, 473.0, 871.0, 264.0, 96.0],
|
||||
[67.0, 664.0, 21.0, 628.0, 129.0],
|
||||
[436.0, 209.0, 309.0, 481.0, 86.0],
|
||||
]
|
||||
),
|
||||
)
|
||||
model = build_multiknapsack_model(data)
|
||||
model.optimize()
|
||||
assert model.inner.objVal == -460.0
|
||||
53
tests/problems/test_pmedian.py
Normal file
53
tests/problems/test_pmedian.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import uniform, randint
|
||||
|
||||
from miplearn.problems.pmedian import PMedianGenerator, build_pmedian_model
|
||||
|
||||
|
||||
def test_pmedian() -> None:
|
||||
np.random.seed(42)
|
||||
gen = PMedianGenerator(
|
||||
x=uniform(loc=0.0, scale=100.0),
|
||||
y=uniform(loc=0.0, scale=100.0),
|
||||
n=randint(low=5, high=6),
|
||||
p=randint(low=2, high=3),
|
||||
demands=uniform(loc=0, scale=20),
|
||||
capacities=uniform(loc=0, scale=100),
|
||||
distances_jitter=uniform(loc=0.95, scale=0.1),
|
||||
demands_jitter=uniform(loc=0.95, scale=0.1),
|
||||
capacities_jitter=uniform(loc=0.95, scale=0.1),
|
||||
fixed=True,
|
||||
)
|
||||
data = gen.generate(2)
|
||||
|
||||
assert data[0].p == 2
|
||||
assert data[0].demands.tolist() == [0.41, 19.4, 16.65, 4.25, 3.64]
|
||||
assert data[0].capacities.tolist() == [18.34, 30.42, 52.48, 43.19, 29.12]
|
||||
assert data[0].distances.tolist() == [
|
||||
[0.0, 50.17, 82.42, 32.76, 33.2],
|
||||
[50.17, 0.0, 72.64, 72.51, 17.06],
|
||||
[82.42, 72.64, 0.0, 71.69, 70.92],
|
||||
[32.76, 72.51, 71.69, 0.0, 56.56],
|
||||
[33.2, 17.06, 70.92, 56.56, 0.0],
|
||||
]
|
||||
|
||||
assert data[1].p == 2
|
||||
assert data[1].demands.tolist() == [0.42, 19.03, 16.68, 4.27, 3.53]
|
||||
assert data[1].capacities.tolist() == [19.2, 31.26, 54.79, 44.9, 29.41]
|
||||
assert data[1].distances.tolist() == [
|
||||
[0.0, 51.6, 83.31, 33.77, 31.95],
|
||||
[51.6, 0.0, 70.25, 71.09, 17.05],
|
||||
[83.31, 70.25, 0.0, 68.81, 67.62],
|
||||
[33.77, 71.09, 68.81, 0.0, 58.88],
|
||||
[31.95, 17.05, 67.62, 58.88, 0.0],
|
||||
]
|
||||
|
||||
model = build_pmedian_model(data[0])
|
||||
assert model.inner.numVars == 30
|
||||
assert model.inner.numConstrs == 11
|
||||
model.optimize()
|
||||
assert round(model.inner.objVal) == 107
|
||||
91
tests/problems/test_setcover.py
Normal file
91
tests/problems/test_setcover.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import randint, uniform
|
||||
|
||||
from miplearn.h5 import H5File
|
||||
from miplearn.problems.setcover import (
|
||||
SetCoverData,
|
||||
build_setcover_model_gurobipy,
|
||||
SetCoverGenerator,
|
||||
build_setcover_model_pyomo,
|
||||
)
|
||||
|
||||
|
||||
def test_set_cover_generator() -> None:
|
||||
np.random.seed(42)
|
||||
gen = SetCoverGenerator(
|
||||
n_elements=randint(low=3, high=4),
|
||||
n_sets=randint(low=5, high=6),
|
||||
costs=uniform(loc=0.0, scale=100.0),
|
||||
costs_jitter=uniform(loc=0.95, scale=0.10),
|
||||
density=uniform(loc=0.5, scale=0),
|
||||
K=uniform(loc=25, scale=0),
|
||||
fix_sets=False,
|
||||
)
|
||||
data = gen.generate(2)
|
||||
|
||||
assert data[0].costs.round(1).tolist() == [136.8, 86.2, 25.7, 27.3, 102.5]
|
||||
assert data[0].incidence_matrix.tolist() == [
|
||||
[1, 0, 1, 0, 1],
|
||||
[1, 1, 0, 0, 0],
|
||||
[1, 0, 0, 1, 1],
|
||||
]
|
||||
assert data[1].costs.round(1).tolist() == [63.5, 76.6, 48.1, 74.1, 93.3]
|
||||
assert data[1].incidence_matrix.tolist() == [
|
||||
[1, 1, 0, 1, 1],
|
||||
[0, 1, 0, 1, 0],
|
||||
[0, 1, 1, 0, 0],
|
||||
]
|
||||
|
||||
|
||||
def test_set_cover_generator_with_fixed_sets() -> None:
|
||||
np.random.seed(42)
|
||||
gen = SetCoverGenerator(
|
||||
n_elements=randint(low=3, high=4),
|
||||
n_sets=randint(low=5, high=6),
|
||||
costs=uniform(loc=0.0, scale=100.0),
|
||||
costs_jitter=uniform(loc=0.95, scale=0.10),
|
||||
density=uniform(loc=0.5, scale=0.00),
|
||||
fix_sets=True,
|
||||
)
|
||||
data = gen.generate(3)
|
||||
|
||||
assert data[0].costs.tolist() == [136.75, 86.17, 25.71, 27.31, 102.48]
|
||||
assert data[1].costs.tolist() == [135.38, 82.26, 26.92, 26.58, 98.28]
|
||||
assert data[2].costs.tolist() == [138.37, 85.15, 26.95, 27.22, 106.17]
|
||||
|
||||
print(data[0].incidence_matrix)
|
||||
|
||||
for i in range(3):
|
||||
assert data[i].incidence_matrix.tolist() == [
|
||||
[1, 0, 1, 0, 1],
|
||||
[1, 1, 0, 0, 0],
|
||||
[1, 0, 0, 1, 1],
|
||||
]
|
||||
|
||||
|
||||
def test_set_cover() -> None:
|
||||
data = SetCoverData(
|
||||
costs=np.array([5, 10, 12, 6, 8]),
|
||||
incidence_matrix=np.array(
|
||||
[
|
||||
[1, 0, 0, 1, 0],
|
||||
[1, 1, 0, 0, 0],
|
||||
[0, 0, 1, 1, 1],
|
||||
],
|
||||
),
|
||||
)
|
||||
for model in [
|
||||
build_setcover_model_pyomo(data),
|
||||
build_setcover_model_gurobipy(data),
|
||||
]:
|
||||
with NamedTemporaryFile() as tempfile:
|
||||
with H5File(tempfile.name) as h5:
|
||||
model.optimize()
|
||||
model.extract_after_mip(h5)
|
||||
assert h5.get_scalar("mip_obj_value") == 11.0
|
||||
26
tests/problems/test_setpack.py
Normal file
26
tests/problems/test_setpack.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, 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.setpack import (
|
||||
SetPackData,
|
||||
build_setpack_model,
|
||||
)
|
||||
|
||||
|
||||
def test_setpack() -> None:
|
||||
data = SetPackData(
|
||||
costs=np.array([5, 10, 12, 6, 8]),
|
||||
incidence_matrix=np.array(
|
||||
[
|
||||
[1, 0, 0, 1, 0],
|
||||
[1, 1, 0, 0, 0],
|
||||
[0, 0, 1, 1, 1],
|
||||
],
|
||||
),
|
||||
)
|
||||
model = build_setpack_model(data)
|
||||
model.optimize()
|
||||
assert model.inner.objval == -22.0
|
||||
@@ -1,53 +1,30 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
from scipy.stats import uniform, randint
|
||||
|
||||
from miplearn.problems.stab import MaxWeightStableSetInstance
|
||||
from miplearn.solvers.learning import LearningSolver
|
||||
from miplearn.h5 import H5File
|
||||
from miplearn.problems.stab import (
|
||||
MaxWeightStableSetData,
|
||||
build_stab_model_pyomo,
|
||||
build_stab_model_gurobipy,
|
||||
)
|
||||
|
||||
|
||||
def test_stab() -> None:
|
||||
graph = nx.cycle_graph(5)
|
||||
weights = np.array([1.0, 1.0, 1.0, 1.0, 1.0])
|
||||
instance = MaxWeightStableSetInstance(graph, weights)
|
||||
solver = LearningSolver()
|
||||
stats = solver._solve(instance)
|
||||
assert stats["mip_lower_bound"] == 2.0
|
||||
|
||||
|
||||
def test_stab_generator_fixed_graph() -> None:
|
||||
np.random.seed(42)
|
||||
from miplearn.problems.stab import MaxWeightStableSetGenerator
|
||||
|
||||
gen = MaxWeightStableSetGenerator(
|
||||
w=uniform(loc=50.0, scale=10.0),
|
||||
n=randint(low=10, high=11),
|
||||
p=uniform(loc=0.05, scale=0.0),
|
||||
fix_graph=True,
|
||||
data = MaxWeightStableSetData(
|
||||
graph=nx.cycle_graph(5),
|
||||
weights=np.array([1.0, 1.0, 1.0, 1.0, 1.0]),
|
||||
)
|
||||
data = gen.generate(1_000)
|
||||
weights = np.array([d.weights for d in data])
|
||||
weights_avg_actual = np.round(np.average(weights, axis=0))
|
||||
weights_avg_expected = [55.0] * 10
|
||||
assert list(weights_avg_actual) == weights_avg_expected
|
||||
|
||||
|
||||
def test_stab_generator_random_graph() -> None:
|
||||
np.random.seed(42)
|
||||
from miplearn.problems.stab import MaxWeightStableSetGenerator
|
||||
|
||||
gen = MaxWeightStableSetGenerator(
|
||||
w=uniform(loc=50.0, scale=10.0),
|
||||
n=randint(low=30, high=41),
|
||||
p=uniform(loc=0.5, scale=0.0),
|
||||
fix_graph=False,
|
||||
)
|
||||
data = gen.generate(1_000)
|
||||
n_nodes = [d.graph.number_of_nodes() for d in data]
|
||||
n_edges = [d.graph.number_of_edges() for d in data]
|
||||
assert np.round(np.mean(n_nodes)) == 35.0
|
||||
assert np.round(np.mean(n_edges), -1) == 300.0
|
||||
for model in [
|
||||
build_stab_model_pyomo(data),
|
||||
build_stab_model_gurobipy(data),
|
||||
]:
|
||||
with NamedTemporaryFile() as tempfile:
|
||||
with H5File(tempfile.name) as h5:
|
||||
model.optimize()
|
||||
model.extract_after_mip(h5)
|
||||
assert h5.get_scalar("mip_obj_value") == -2.0
|
||||
|
||||
@@ -1,100 +1,72 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
from miplearn.problems.tsp import (
|
||||
TravelingSalesmanData,
|
||||
TravelingSalesmanGenerator,
|
||||
build_tsp_model,
|
||||
)
|
||||
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
|
||||
from miplearn.solvers.tests import assert_equals
|
||||
from scipy.stats import randint, uniform
|
||||
|
||||
|
||||
def test_generator() -> None:
|
||||
data = TravelingSalesmanGenerator(
|
||||
def test_tsp_generator() -> None:
|
||||
np.random.seed(42)
|
||||
gen = TravelingSalesmanGenerator(
|
||||
x=uniform(loc=0.0, scale=1000.0),
|
||||
y=uniform(loc=0.0, scale=1000.0),
|
||||
n=randint(low=100, high=101),
|
||||
gamma=uniform(loc=0.95, scale=0.1),
|
||||
n=randint(low=3, high=4),
|
||||
gamma=uniform(loc=1.0, scale=0.25),
|
||||
fix_cities=True,
|
||||
).generate(100)
|
||||
assert len(data) == 100
|
||||
assert data[0].n_cities == 100
|
||||
assert norm(data[0].distances - data[0].distances.T) < 1e-6
|
||||
d = [d.distances[0, 1] for d in data]
|
||||
assert np.std(d) > 0
|
||||
|
||||
|
||||
def test_instance() -> None:
|
||||
n_cities = 4
|
||||
distances = np.array(
|
||||
[
|
||||
[0.0, 1.0, 2.0, 1.0],
|
||||
[1.0, 0.0, 1.0, 2.0],
|
||||
[2.0, 1.0, 0.0, 1.0],
|
||||
[1.0, 2.0, 1.0, 0.0],
|
||||
]
|
||||
round=True,
|
||||
)
|
||||
instance = TravelingSalesmanInstance(n_cities, distances)
|
||||
solver = LearningSolver()
|
||||
solver._solve(instance)
|
||||
assert len(instance.get_samples()) == 1
|
||||
sample = instance.get_samples()[0]
|
||||
assert_equals(sample.get_array("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 0.0, 1.0])
|
||||
assert sample.get_scalar("mip_lower_bound") == 4.0
|
||||
assert sample.get_scalar("mip_upper_bound") == 4.0
|
||||
data = gen.generate(2)
|
||||
assert data[0].distances.tolist() == [
|
||||
[0.0, 591.0, 996.0],
|
||||
[591.0, 0.0, 765.0],
|
||||
[996.0, 765.0, 0.0],
|
||||
]
|
||||
assert data[1].distances.tolist() == [
|
||||
[0.0, 556.0, 853.0],
|
||||
[556.0, 0.0, 779.0],
|
||||
[853.0, 779.0, 0.0],
|
||||
]
|
||||
|
||||
|
||||
def test_subtour() -> None:
|
||||
n_cities = 6
|
||||
cities = np.array(
|
||||
[
|
||||
[0.0, 0.0],
|
||||
[1.0, 0.0],
|
||||
[2.0, 0.0],
|
||||
[3.0, 0.0],
|
||||
[0.0, 1.0],
|
||||
[3.0, 1.0],
|
||||
]
|
||||
def test_tsp() -> None:
|
||||
data = TravelingSalesmanData(
|
||||
n_cities=6,
|
||||
distances=squareform(
|
||||
pdist(
|
||||
[
|
||||
[0.0, 0.0],
|
||||
[1.0, 0.0],
|
||||
[2.0, 0.0],
|
||||
[3.0, 0.0],
|
||||
[0.0, 1.0],
|
||||
[3.0, 1.0],
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
distances = squareform(pdist(cities))
|
||||
instance = TravelingSalesmanInstance(n_cities, distances)
|
||||
solver = LearningSolver()
|
||||
solver._solve(instance)
|
||||
samples = instance.get_samples()
|
||||
assert len(samples) == 1
|
||||
sample = samples[0]
|
||||
|
||||
lazy_encoded = sample.get_scalar("mip_constr_lazy")
|
||||
assert lazy_encoded is not None
|
||||
lazy = json.loads(lazy_encoded)
|
||||
assert lazy == {
|
||||
"st[0,1,4]": [0, 1, 4],
|
||||
"st[2,3,5]": [2, 3, 5],
|
||||
}
|
||||
|
||||
assert_equals(
|
||||
sample.get_array("mip_var_values"),
|
||||
[
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
],
|
||||
)
|
||||
solver._fit([instance])
|
||||
solver._solve(instance)
|
||||
model = build_tsp_model(data)
|
||||
model.optimize()
|
||||
assert model.inner.getAttr("x", model.inner.getVars()) == [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
]
|
||||
|
||||
71
tests/problems/test_uc.py
Normal file
71
tests/problems/test_uc.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import uniform, randint
|
||||
|
||||
from miplearn.problems.uc import (
|
||||
UnitCommitmentData,
|
||||
build_uc_model,
|
||||
UnitCommitmentGenerator,
|
||||
)
|
||||
|
||||
|
||||
def test_generator() -> None:
|
||||
np.random.seed(42)
|
||||
gen = UnitCommitmentGenerator(
|
||||
n_units=randint(low=3, high=4),
|
||||
n_periods=randint(low=4, high=5),
|
||||
max_power=uniform(loc=50, scale=450),
|
||||
min_power=uniform(loc=0.25, scale=0.5),
|
||||
cost_startup=uniform(loc=1, scale=1),
|
||||
cost_prod=uniform(loc=1, scale=1),
|
||||
cost_fixed=uniform(loc=1, scale=1),
|
||||
min_uptime=randint(low=1, high=8),
|
||||
min_downtime=randint(low=1, high=8),
|
||||
cost_jitter=uniform(loc=0.75, scale=0.5),
|
||||
demand_jitter=uniform(loc=0.9, scale=0.2),
|
||||
fix_units=True,
|
||||
)
|
||||
data = gen.generate(2)
|
||||
|
||||
assert data[0].demand.tolist() == [430.3, 518.65, 448.16, 860.61]
|
||||
assert data[0].min_power.tolist() == [120.05, 156.73, 124.44]
|
||||
assert data[0].max_power.tolist() == [218.54, 477.82, 379.4]
|
||||
assert data[0].min_uptime.tolist() == [3, 3, 5]
|
||||
assert data[0].min_downtime.tolist() == [4, 3, 6]
|
||||
assert data[0].cost_startup.tolist() == [1.06, 1.72, 1.94]
|
||||
assert data[0].cost_prod.tolist() == [1.0, 1.99, 1.62]
|
||||
assert data[0].cost_fixed.tolist() == [1.61, 1.01, 1.02]
|
||||
|
||||
assert data[1].demand.tolist() == [407.3, 476.18, 458.77, 840.38]
|
||||
assert data[1].min_power.tolist() == [120.05, 156.73, 124.44]
|
||||
assert data[1].max_power.tolist() == [218.54, 477.82, 379.4]
|
||||
assert data[1].min_uptime.tolist() == [3, 3, 5]
|
||||
assert data[1].min_downtime.tolist() == [4, 3, 6]
|
||||
assert data[1].cost_startup.tolist() == [1.32, 1.69, 2.29]
|
||||
assert data[1].cost_prod.tolist() == [1.09, 1.94, 1.23]
|
||||
assert data[1].cost_fixed.tolist() == [1.97, 1.04, 0.96]
|
||||
|
||||
|
||||
def test_uc() -> None:
|
||||
data = UnitCommitmentData(
|
||||
demand=np.array([10, 12, 15, 10, 8, 5]),
|
||||
min_power=np.array([5, 5, 10]),
|
||||
max_power=np.array([10, 8, 20]),
|
||||
min_uptime=np.array([4, 3, 2]),
|
||||
min_downtime=np.array([4, 3, 2]),
|
||||
cost_startup=np.array([100, 120, 200]),
|
||||
cost_prod=np.array([1.0, 1.25, 1.5]),
|
||||
cost_fixed=np.array([10, 12, 9]),
|
||||
)
|
||||
model = build_uc_model(data)
|
||||
model.optimize()
|
||||
assert model.inner.objVal == 154.5
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = UnitCommitmentGenerator().generate(1)[0]
|
||||
model = build_uc_model(data)
|
||||
model.optimize()
|
||||
21
tests/problems/test_vertexcover.py
Normal file
21
tests/problems/test_vertexcover.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, 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
|
||||
|
||||
from miplearn.problems.vertexcover import (
|
||||
MinWeightVertexCoverData,
|
||||
build_vertexcover_model,
|
||||
)
|
||||
|
||||
|
||||
def test_stab() -> None:
|
||||
data = MinWeightVertexCoverData(
|
||||
graph=nx.cycle_graph(5),
|
||||
weights=np.array([1.0, 1.0, 1.0, 1.0, 1.0]),
|
||||
)
|
||||
model = build_vertexcover_model(data)
|
||||
model.optimize()
|
||||
assert model.inner.objVal == 3.0
|
||||
@@ -1,17 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from io import StringIO
|
||||
|
||||
from miplearn.solvers import _RedirectOutput
|
||||
|
||||
|
||||
def test_redirect_output() -> None:
|
||||
import sys
|
||||
|
||||
original_stdout = sys.stdout
|
||||
io = StringIO()
|
||||
with _RedirectOutput([io]):
|
||||
print("Hello world")
|
||||
assert sys.stdout == original_stdout
|
||||
assert io.getvalue() == "Hello world\n"
|
||||
@@ -1,37 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from miplearn.solvers.gurobi import GurobiSolver
|
||||
from miplearn.solvers.internal import InternalSolver
|
||||
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
|
||||
from miplearn.solvers.pyomo.xpress import XpressPyomoSolver
|
||||
from miplearn.solvers.tests import run_internal_solver_tests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def internal_solvers() -> List[InternalSolver]:
|
||||
return [
|
||||
XpressPyomoSolver(),
|
||||
GurobiSolver(),
|
||||
GurobiPyomoSolver(),
|
||||
]
|
||||
|
||||
|
||||
def test_xpress_pyomo_solver() -> None:
|
||||
run_internal_solver_tests(XpressPyomoSolver())
|
||||
|
||||
|
||||
def test_gurobi_pyomo_solver() -> None:
|
||||
run_internal_solver_tests(GurobiPyomoSolver())
|
||||
|
||||
|
||||
def test_gurobi_solver() -> None:
|
||||
run_internal_solver_tests(GurobiSolver())
|
||||
@@ -1,163 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from os.path import exists
|
||||
from typing import List, cast
|
||||
|
||||
import dill
|
||||
from scipy.stats import randint
|
||||
|
||||
from miplearn.features.sample import Hdf5Sample
|
||||
from miplearn.instance.base import Instance
|
||||
from miplearn.instance.picklegz import (
|
||||
PickleGzInstance,
|
||||
write_pickle_gz,
|
||||
read_pickle_gz,
|
||||
save,
|
||||
)
|
||||
from miplearn.problems.stab import MaxWeightStableSetGenerator, build_stab_model
|
||||
from miplearn.solvers.internal import InternalSolver
|
||||
from miplearn.solvers.learning import LearningSolver
|
||||
from miplearn.solvers.tests import assert_equals
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from tests.solvers.test_internal_solver import internal_solvers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def test_learning_solver(
|
||||
internal_solvers: List[InternalSolver],
|
||||
) -> None:
|
||||
for mode in ["exact", "heuristic"]:
|
||||
for internal_solver in internal_solvers:
|
||||
logger.info("Solver: %s" % internal_solver)
|
||||
instance = internal_solver.build_test_instance_knapsack()
|
||||
solver = LearningSolver(
|
||||
solver=internal_solver,
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
solver._solve(instance)
|
||||
assert len(instance.get_samples()) > 0
|
||||
sample = instance.get_samples()[0]
|
||||
|
||||
assert_equals(
|
||||
sample.get_array("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0]
|
||||
)
|
||||
assert sample.get_scalar("mip_lower_bound") == 1183.0
|
||||
assert sample.get_scalar("mip_upper_bound") == 1183.0
|
||||
mip_log = sample.get_scalar("mip_log")
|
||||
assert mip_log is not None
|
||||
assert len(mip_log) > 100
|
||||
|
||||
assert_equals(
|
||||
sample.get_array("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]
|
||||
)
|
||||
assert_equals(sample.get_scalar("lp_value"), 1287.923077)
|
||||
lp_log = sample.get_scalar("lp_log")
|
||||
assert lp_log is not None
|
||||
assert len(lp_log) > 100
|
||||
|
||||
solver._fit([instance], n_jobs=4)
|
||||
solver._solve(instance)
|
||||
|
||||
# Assert solver is picklable
|
||||
with tempfile.TemporaryFile() as file:
|
||||
dill.dump(solver, file)
|
||||
|
||||
|
||||
def test_solve_without_lp(
|
||||
internal_solvers: List[InternalSolver],
|
||||
) -> None:
|
||||
for internal_solver in internal_solvers:
|
||||
logger.info("Solver: %s" % internal_solver)
|
||||
instance = internal_solver.build_test_instance_knapsack()
|
||||
solver = LearningSolver(
|
||||
solver=internal_solver,
|
||||
solve_lp=False,
|
||||
)
|
||||
solver._solve(instance)
|
||||
solver._fit([instance])
|
||||
solver._solve(instance)
|
||||
|
||||
|
||||
def test_parallel_solve(
|
||||
internal_solvers: List[InternalSolver],
|
||||
) -> None:
|
||||
for internal_solver in internal_solvers:
|
||||
instances = [internal_solver.build_test_instance_knapsack() for _ in range(10)]
|
||||
solver = LearningSolver(solver=internal_solver)
|
||||
results = solver.parallel_solve(instances, n_jobs=3)
|
||||
assert len(results) == 10
|
||||
for instance in instances:
|
||||
assert len(instance.get_samples()) == 1
|
||||
|
||||
|
||||
def test_solve_fit_from_disk(
|
||||
internal_solvers: List[InternalSolver],
|
||||
) -> None:
|
||||
for internal_solver in internal_solvers:
|
||||
# Create instances and pickle them
|
||||
instances: List[Instance] = []
|
||||
for k in range(3):
|
||||
instance = internal_solver.build_test_instance_knapsack()
|
||||
with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as file:
|
||||
instances += [PickleGzInstance(file.name)]
|
||||
write_pickle_gz(instance, file.name)
|
||||
|
||||
# Test: solve
|
||||
solver = LearningSolver(solver=internal_solver)
|
||||
solver._solve(instances[0])
|
||||
instance_loaded = read_pickle_gz(cast(PickleGzInstance, instances[0]).filename)
|
||||
assert len(instance_loaded.get_samples()) > 0
|
||||
|
||||
# Test: parallel_solve
|
||||
solver.parallel_solve(instances)
|
||||
for instance in instances:
|
||||
instance_loaded = read_pickle_gz(cast(PickleGzInstance, instance).filename)
|
||||
assert len(instance_loaded.get_samples()) > 0
|
||||
|
||||
# Delete temporary files
|
||||
for instance in instances:
|
||||
os.remove(cast(PickleGzInstance, instance).filename)
|
||||
|
||||
|
||||
def test_basic_usage() -> None:
|
||||
with tempfile.TemporaryDirectory() as dirname:
|
||||
# Generate instances
|
||||
data = MaxWeightStableSetGenerator(n=randint(low=20, high=21)).generate(4)
|
||||
train_files = save(data[0:3], f"{dirname}/train")
|
||||
test_files = save(data[3:4], f"{dirname}/test")
|
||||
|
||||
# Solve training instances
|
||||
solver = LearningSolver()
|
||||
stats = solver.solve(train_files, build_stab_model)
|
||||
assert len(stats) == 3
|
||||
for f in train_files:
|
||||
sample_filename = f.replace(".pkl.gz", ".h5")
|
||||
assert exists(sample_filename)
|
||||
sample = Hdf5Sample(sample_filename)
|
||||
assert sample.get_scalar("mip_lower_bound") > 0
|
||||
|
||||
# Fit
|
||||
solver.fit(train_files, build_stab_model)
|
||||
|
||||
# Solve test instances
|
||||
stats = solver.solve(test_files, build_stab_model)
|
||||
assert isinstance(stats, list)
|
||||
assert "Objective: Predicted lower bound" in stats[0].keys()
|
||||
|
||||
|
||||
def test_gap() -> None:
|
||||
assert LearningSolver._compute_gap(ub=0.0, lb=0.0) == 0.0
|
||||
assert LearningSolver._compute_gap(ub=1.0, lb=0.5) == 0.5
|
||||
assert LearningSolver._compute_gap(ub=1.0, lb=1.0) == 0.0
|
||||
assert LearningSolver._compute_gap(ub=1.0, lb=-1.0) is None
|
||||
assert LearningSolver._compute_gap(ub=1.0, lb=None) is None
|
||||
assert LearningSolver._compute_gap(ub=None, lb=1.0) is None
|
||||
assert LearningSolver._compute_gap(ub=None, lb=None) is None
|
||||
@@ -1,48 +0,0 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import os.path
|
||||
|
||||
from scipy.stats import randint
|
||||
|
||||
from miplearn.benchmark import BenchmarkRunner
|
||||
from miplearn.problems.stab import (
|
||||
MaxWeightStableSetInstance,
|
||||
MaxWeightStableSetGenerator,
|
||||
)
|
||||
from miplearn.solvers.learning import LearningSolver
|
||||
|
||||
|
||||
def test_benchmark() -> None:
|
||||
for n_jobs in [1, 4]:
|
||||
# Generate training and test instances
|
||||
generator = MaxWeightStableSetGenerator(n=randint(low=25, high=26))
|
||||
train_instances = [
|
||||
MaxWeightStableSetInstance(data.graph, data.weights)
|
||||
for data in generator.generate(5)
|
||||
]
|
||||
test_instances = [
|
||||
MaxWeightStableSetInstance(data.graph, data.weights)
|
||||
for data in generator.generate(3)
|
||||
]
|
||||
|
||||
# Solve training instances
|
||||
training_solver = LearningSolver()
|
||||
training_solver.parallel_solve(train_instances, n_jobs=n_jobs) # type: ignore
|
||||
|
||||
# Benchmark
|
||||
test_solvers = {
|
||||
"Strategy A": LearningSolver(),
|
||||
"Strategy B": LearningSolver(),
|
||||
}
|
||||
benchmark = BenchmarkRunner(test_solvers)
|
||||
benchmark.fit(train_instances, n_jobs=n_jobs) # type: ignore
|
||||
benchmark.parallel_solve(
|
||||
test_instances, # type: ignore
|
||||
n_jobs=n_jobs,
|
||||
n_trials=2,
|
||||
)
|
||||
benchmark.write_csv("/tmp/benchmark.csv")
|
||||
assert os.path.isfile("/tmp/benchmark.csv")
|
||||
assert benchmark.results.values.shape == (12, 21)
|
||||
64
tests/test_h5.py
Normal file
64
tests/test_h5.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse import coo_matrix
|
||||
|
||||
from miplearn.h5 import H5File
|
||||
|
||||
|
||||
def test_h5() -> None:
|
||||
file = NamedTemporaryFile()
|
||||
h5 = H5File(file.name)
|
||||
_assert_roundtrip_scalar(h5, "A")
|
||||
_assert_roundtrip_scalar(h5, True)
|
||||
_assert_roundtrip_scalar(h5, 1)
|
||||
_assert_roundtrip_scalar(h5, 1.0)
|
||||
assert h5.get_scalar("unknown-key") is None
|
||||
|
||||
_assert_roundtrip_array(h5, np.array([True, False]))
|
||||
_assert_roundtrip_array(h5, np.array([1, 2, 3]))
|
||||
_assert_roundtrip_array(h5, np.array([1.0, 2.0, 3.0]))
|
||||
_assert_roundtrip_array(h5, np.array(["A", "BB", "CCC"], dtype="S"))
|
||||
assert h5.get_array("unknown-key") is None
|
||||
|
||||
_assert_roundtrip_sparse(
|
||||
h5,
|
||||
coo_matrix(
|
||||
[
|
||||
[1.0, 0.0, 0.0],
|
||||
[0.0, 2.0, 3.0],
|
||||
[0.0, 0.0, 4.0],
|
||||
],
|
||||
),
|
||||
)
|
||||
assert h5.get_sparse("unknown-key") is None
|
||||
|
||||
|
||||
def _assert_roundtrip_array(h5: H5File, original: np.ndarray) -> None:
|
||||
h5.put_array("key", original)
|
||||
recovered = h5.get_array("key")
|
||||
assert recovered is not None
|
||||
assert isinstance(recovered, np.ndarray)
|
||||
assert (recovered == original).all()
|
||||
|
||||
|
||||
def _assert_roundtrip_scalar(h5: H5File, original: Any) -> None:
|
||||
h5.put_scalar("key", original)
|
||||
recovered = h5.get_scalar("key")
|
||||
assert recovered == original
|
||||
assert recovered is not None
|
||||
assert isinstance(
|
||||
recovered, original.__class__
|
||||
), f"Expected {original.__class__}, found {recovered.__class__} instead"
|
||||
|
||||
|
||||
def _assert_roundtrip_sparse(h5: H5File, original: coo_matrix) -> None:
|
||||
h5.put_sparse("key", original)
|
||||
recovered = h5.get_sparse("key")
|
||||
assert recovered is not None
|
||||
assert isinstance(recovered, coo_matrix)
|
||||
assert (original != recovered).sum() == 0
|
||||
181
tests/test_solvers.py
Normal file
181
tests/test_solvers.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from miplearn.h5 import H5File
|
||||
from miplearn.problems.setcover import (
|
||||
SetCoverData,
|
||||
build_setcover_model_gurobipy,
|
||||
build_setcover_model_pyomo,
|
||||
)
|
||||
from miplearn.solvers.abstract import AbstractModel
|
||||
|
||||
inf = float("inf")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data() -> SetCoverData:
|
||||
return SetCoverData(
|
||||
costs=np.array([5, 10, 12, 6, 8]),
|
||||
incidence_matrix=np.array(
|
||||
[
|
||||
[1, 0, 0, 1, 0],
|
||||
[1, 1, 0, 0, 0],
|
||||
[0, 0, 1, 1, 1],
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_gurobi(data: SetCoverData) -> None:
|
||||
_test_solver(build_setcover_model_gurobipy, data)
|
||||
|
||||
|
||||
def test_pyomo_persistent(data: SetCoverData) -> None:
|
||||
_test_solver(lambda d: build_setcover_model_pyomo(d, "gurobi_persistent"), data)
|
||||
|
||||
|
||||
def _test_solver(build_model, data):
|
||||
_test_extract(build_model(data))
|
||||
_test_add_constr(build_model(data))
|
||||
_test_fix_vars(build_model(data))
|
||||
_test_infeasible(build_model(data))
|
||||
|
||||
|
||||
def _test_extract(model):
|
||||
with NamedTemporaryFile() as tempfile:
|
||||
with H5File(tempfile.name) as h5:
|
||||
|
||||
def test_scalar(key, expected_value):
|
||||
actual_value = h5.get_scalar(key)
|
||||
assert actual_value is not None
|
||||
assert actual_value == expected_value
|
||||
|
||||
def test_array(key, expected_value):
|
||||
actual_value = h5.get_array(key)
|
||||
assert actual_value is not None
|
||||
assert actual_value.tolist() == expected_value
|
||||
|
||||
def test_sparse(key, expected_value):
|
||||
actual_value = h5.get_sparse(key)
|
||||
assert actual_value is not None
|
||||
assert actual_value.todense().tolist() == expected_value
|
||||
|
||||
model.extract_after_load(h5)
|
||||
test_sparse(
|
||||
"static_constr_lhs",
|
||||
[
|
||||
[1.0, 0.0, 0.0, 1.0, 0.0],
|
||||
[1.0, 1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
],
|
||||
)
|
||||
test_array("static_constr_names", [b"eqs[0]", b"eqs[1]", b"eqs[2]"])
|
||||
test_array("static_constr_rhs", [1, 1, 1])
|
||||
test_array("static_constr_sense", [b">", b">", b">"])
|
||||
test_scalar("static_obj_offset", 0.0)
|
||||
test_scalar("static_sense", "min")
|
||||
test_array("static_var_lower_bounds", [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
test_array(
|
||||
"static_var_names",
|
||||
[
|
||||
b"x[0]",
|
||||
b"x[1]",
|
||||
b"x[2]",
|
||||
b"x[3]",
|
||||
b"x[4]",
|
||||
],
|
||||
)
|
||||
test_array("static_var_obj_coeffs", [5.0, 10.0, 12.0, 6.0, 8.0])
|
||||
test_array("static_var_types", [b"B", b"B", b"B", b"B", b"B"])
|
||||
test_array("static_var_upper_bounds", [1.0, 1.0, 1.0, 1.0, 1.0])
|
||||
|
||||
relaxed = model.relax()
|
||||
relaxed.optimize()
|
||||
relaxed.extract_after_lp(h5)
|
||||
test_array("lp_constr_dual_values", [0, 5, 6])
|
||||
test_array("lp_constr_slacks", [1, 0, 0])
|
||||
test_scalar("lp_obj_value", 11.0)
|
||||
test_array("lp_var_reduced_costs", [0.0, 5.0, 6.0, 0.0, 2.0])
|
||||
test_array("lp_var_values", [1.0, 0.0, 0.0, 1.0, 0.0])
|
||||
if model._supports_basis_status:
|
||||
test_array("lp_var_basis_status", [b"B", b"L", b"L", b"B", b"L"])
|
||||
test_array("lp_constr_basis_status", [b"B", b"N", b"N"])
|
||||
if model._supports_sensitivity_analysis:
|
||||
test_array("lp_constr_sa_rhs_up", [2, 1, 1])
|
||||
test_array("lp_constr_sa_rhs_down", [-inf, 0, 0])
|
||||
test_array("lp_var_sa_obj_up", [10.0, inf, inf, 8.0, inf])
|
||||
test_array("lp_var_sa_obj_down", [0.0, 5.0, 6.0, 0.0, 6.0])
|
||||
test_array("lp_var_sa_ub_up", [inf, inf, inf, inf, inf])
|
||||
test_array("lp_var_sa_ub_down", [1.0, 0.0, 0.0, 1.0, 0.0])
|
||||
test_array("lp_var_sa_lb_up", [1.0, 1.0, 1.0, 1.0, 1.0])
|
||||
test_array("lp_var_sa_lb_down", [-inf, 0.0, 0.0, -inf, 0.0])
|
||||
lp_wallclock_time = h5.get_scalar("lp_wallclock_time")
|
||||
assert lp_wallclock_time is not None
|
||||
assert lp_wallclock_time >= 0
|
||||
|
||||
model.optimize()
|
||||
model.extract_after_mip(h5)
|
||||
test_array("mip_constr_slacks", [1, 0, 0])
|
||||
test_array("mip_var_values", [1.0, 0.0, 0.0, 1.0, 0.0])
|
||||
test_scalar("mip_gap", 0)
|
||||
test_scalar("mip_obj_bound", 11.0)
|
||||
test_scalar("mip_obj_value", 11.0)
|
||||
mip_wallclock_time = h5.get_scalar("mip_wallclock_time")
|
||||
assert mip_wallclock_time is not None
|
||||
assert mip_wallclock_time > 0
|
||||
if model._supports_node_count:
|
||||
count = h5.get_scalar("mip_node_count")
|
||||
assert count is not None
|
||||
assert count >= 0
|
||||
if model._supports_solution_pool:
|
||||
pool_var_values = h5.get_array("pool_var_values")
|
||||
pool_obj_values = h5.get_array("pool_obj_values")
|
||||
assert pool_var_values is not None
|
||||
assert pool_obj_values is not None
|
||||
assert len(pool_obj_values.shape) == 1
|
||||
n_sols = len(pool_obj_values)
|
||||
assert pool_var_values.shape == (n_sols, 5)
|
||||
|
||||
|
||||
def _test_add_constr(model: AbstractModel):
|
||||
with NamedTemporaryFile() as tempfile:
|
||||
with H5File(tempfile.name) as h5:
|
||||
model.add_constrs(
|
||||
np.array([b"x[2]", b"x[3]"], dtype="S"),
|
||||
np.array([[0, 1], [1, 0]]),
|
||||
np.array(["=", "="], dtype="S"),
|
||||
np.array([0, 0]),
|
||||
)
|
||||
model.optimize()
|
||||
model.extract_after_mip(h5)
|
||||
assert h5.get_array("mip_var_values").tolist() == [1, 0, 0, 0, 1]
|
||||
|
||||
|
||||
def _test_fix_vars(model: AbstractModel):
|
||||
with NamedTemporaryFile() as tempfile:
|
||||
with H5File(tempfile.name) as h5:
|
||||
model.fix_variables(
|
||||
var_names=np.array([b"x[2]", b"x[3]"], dtype="S"),
|
||||
var_values=np.array([0, 0]),
|
||||
)
|
||||
model.optimize()
|
||||
model.extract_after_mip(h5)
|
||||
assert h5.get_array("mip_var_values").tolist() == [1, 0, 0, 0, 1]
|
||||
|
||||
|
||||
def _test_infeasible(model: AbstractModel):
|
||||
with NamedTemporaryFile() as tempfile:
|
||||
with H5File(tempfile.name) as h5:
|
||||
model.fix_variables(
|
||||
var_names=np.array([b"x[0]", b"x[3]"], dtype="S"),
|
||||
var_values=np.array([0, 0]),
|
||||
)
|
||||
model.optimize()
|
||||
model.extract_after_mip(h5)
|
||||
assert h5.get_array("mip_var_values") is None
|
||||
Reference in New Issue
Block a user