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:
@@ -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.
|
||||
|
||||
0
tests/components/primal/__init__.py
Normal file
0
tests/components/primal/__init__.py
Normal file
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
|
||||
Reference in New Issue
Block a user