StaticLazy: Refactor

This commit is contained in:
2021-04-04 08:39:56 -05:00
parent 168f56c296
commit 6e614264b5
8 changed files with 340 additions and 429 deletions

View File

@@ -1,250 +1,32 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
from typing import Dict, cast, Hashable
from unittest.mock import Mock, call
import numpy as np
import pytest
from numpy.testing import assert_array_equal
from miplearn import LearningSolver, InternalSolver, Instance
from miplearn.classifiers import Classifier
from miplearn.components.lazy_static import StaticLazyConstraintsComponent
from miplearn.instance import Instance
from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.learning import LearningSolver
from miplearn.types import TrainingSample, Features
from miplearn.types import TrainingSample, Features, LearningSolveStats
def test_usage_with_solver():
solver = Mock(spec=LearningSolver)
solver.use_lazy_cb = False
solver.gap_tolerance = 1e-4
internal = solver.internal_solver = Mock(spec=InternalSolver)
internal.get_constraint_ids = Mock(return_value=["c1", "c2", "c3", "c4"])
internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid)
internal.is_constraint_satisfied = Mock(return_value=False)
instance = Mock(spec=Instance)
instance.has_static_lazy_constraints = Mock(return_value=True)
instance.is_constraint_lazy = Mock(
side_effect=lambda cid: {
"c1": False,
"c2": True,
"c3": True,
"c4": True,
}[cid]
)
instance.get_constraint_features = Mock(
side_effect=lambda cid: {
"c2": [1.0, 0.0],
"c3": [0.5, 0.5],
"c4": [1.0],
}[cid]
)
instance.get_constraint_category = Mock(
side_effect=lambda cid: {
"c2": "type-a",
"c3": "type-a",
"c4": "type-b",
}[cid]
)
component = StaticLazyConstraintsComponent(
threshold=0.90,
use_two_phase_gap=False,
violation_tolerance=1.0,
)
component.classifiers = {
"type-a": Mock(spec=Classifier),
"type-b": Mock(spec=Classifier),
}
component.classifiers["type-a"].predict_proba = Mock(
return_value=[
[0.20, 0.80],
[0.05, 0.95],
]
)
component.classifiers["type-b"].predict_proba = Mock(
return_value=[
[0.02, 0.98],
]
)
# LearningSolver calls before_solve
component.before_solve_mip(
solver=solver,
instance=instance,
model=None,
stats=None,
features=None,
training_data=None,
)
# Should ask if instance has static lazy constraints
instance.has_static_lazy_constraints.assert_called_once()
# Should ask internal solver for a list of constraints in the model
internal.get_constraint_ids.assert_called_once()
# Should ask if each constraint in the model is lazy
instance.is_constraint_lazy.assert_has_calls(
[
call("c1"),
call("c2"),
call("c3"),
call("c4"),
]
)
# For the lazy ones, should ask for features
instance.get_constraint_features.assert_has_calls(
[
call("c2"),
call("c3"),
call("c4"),
]
)
# Should also ask for categories
assert instance.get_constraint_category.call_count == 3
instance.get_constraint_category.assert_has_calls(
[
call("c2"),
call("c3"),
call("c4"),
]
)
# Should ask internal solver to remove constraints identified as lazy
assert internal.extract_constraint.call_count == 3
internal.extract_constraint.assert_has_calls(
[
call("c2"),
call("c3"),
call("c4"),
]
)
# Should ask ML to predict whether each lazy constraint should be enforced
component.classifiers["type-a"].predict_proba.assert_called_once_with(
[[1.0, 0.0], [0.5, 0.5]]
)
component.classifiers["type-b"].predict_proba.assert_called_once_with([[1.0]])
# For the ones that should be enforced, should ask solver to re-add them
# to the formulation. The remaining ones should remain in the pool.
assert internal.add_constraint.call_count == 2
internal.add_constraint.assert_has_calls(
[
call("<c3>"),
call("<c4>"),
]
)
internal.add_constraint.reset_mock()
# 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
internal.is_constraint_satisfied.assert_called_once_with("<c2>", tol=1.0)
internal.is_constraint_satisfied.reset_mock()
internal.add_constraint.assert_called_once_with("<c2>")
internal.add_constraint.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.is_constraint_satisfied.assert_not_called()
internal.add_constraint.assert_not_called()
# Should update instance object
assert instance.found_violated_lazy_constraints == ["c3", "c4", "c2"]
def test_fit():
instance_1 = Mock(spec=Instance)
instance_1.found_violated_lazy_constraints = ["c1", "c2", "c4", "c5"]
instance_1.get_constraint_category = Mock(
side_effect=lambda cid: {
"c1": "type-a",
"c2": "type-a",
"c3": "type-a",
"c4": "type-b",
"c5": "type-b",
}[cid]
)
instance_1.get_constraint_features = Mock(
side_effect=lambda cid: {
"c1": [1, 1],
"c2": [1, 2],
"c3": [1, 3],
"c4": [1, 4, 0],
"c5": [1, 5, 0],
}[cid]
)
instance_2 = Mock(spec=Instance)
instance_2.found_violated_lazy_constraints = ["c2", "c3", "c4"]
instance_2.get_constraint_category = Mock(
side_effect=lambda cid: {
"c1": "type-a",
"c2": "type-a",
"c3": "type-a",
"c4": "type-b",
"c5": "type-b",
}[cid]
)
instance_2.get_constraint_features = Mock(
side_effect=lambda cid: {
"c1": [2, 1],
"c2": [2, 2],
"c3": [2, 3],
"c4": [2, 4, 0],
"c5": [2, 5, 0],
}[cid]
)
instances = [instance_1, instance_2]
component = StaticLazyConstraintsComponent()
component.classifiers = {
"type-a": Mock(spec=Classifier),
"type-b": Mock(spec=Classifier),
}
expected_constraints = {
"type-a": ["c1", "c2", "c3"],
"type-b": ["c4", "c5"],
}
expected_x = {
"type-a": [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2], [2, 3]],
"type-b": [[1, 4, 0], [1, 5, 0], [2, 4, 0], [2, 5, 0]],
}
expected_y = {
"type-a": [[0, 1], [0, 1], [1, 0], [1, 0], [0, 1], [0, 1]],
"type-b": [[0, 1], [0, 1], [0, 1], [1, 0]],
}
assert component._collect_constraints(instances) == expected_constraints
assert component.x(instances) == expected_x
assert component.y(instances) == expected_y
component.fit(instances)
component.classifiers["type-a"].fit.assert_called_once_with(
expected_x["type-a"],
expected_y["type-a"],
)
component.classifiers["type-b"].fit.assert_called_once_with(
expected_x["type-b"],
expected_y["type-b"],
)
def test_xy_sample() -> None:
sample: TrainingSample = {
@pytest.fixture
def sample() -> TrainingSample:
return {
"LazyStatic: Enforced": {"c1", "c2", "c4"},
}
features: Features = {
@pytest.fixture
def features() -> Features:
return {
"Instance": {
"Lazy constraint count": 4,
},
"Constraints": {
"c1": {
"Category": "type-a",
@@ -271,27 +53,169 @@ def test_xy_sample() -> None:
"User features": [1.0, 5.0, 0.0],
"Lazy": False,
},
}
},
}
def test_usage_with_solver(features: Features) -> None:
solver = Mock(spec=LearningSolver)
solver.use_lazy_cb = False
solver.gap_tolerance = 1e-4
internal = solver.internal_solver = Mock(spec=InternalSolver)
internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid)
internal.is_constraint_satisfied = Mock(return_value=False)
instance = Mock(spec=Instance)
instance.has_static_lazy_constraints = Mock(return_value=True)
component = StaticLazyConstraintsComponent(
threshold=0.50,
violation_tolerance=1.0,
)
component.classifiers = {
"type-a": Mock(spec=Classifier),
"type-b": Mock(spec=Classifier),
}
component.classifiers["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["type-b"].predict_proba = Mock( # type: ignore
return_value=np.array(
[
[0.02, 0.98], # c4
]
)
)
sample: TrainingSample = {}
stats: LearningSolveStats = {}
# LearningSolver calls before_solve_mip
component.before_solve_mip(
solver=solver,
instance=instance,
model=None,
stats=stats,
features=features,
training_data=sample,
)
# Should ask ML to predict whether each lazy constraint should be enforced
component.classifiers["type-a"].predict_proba.assert_called_once()
component.classifiers["type-b"].predict_proba.assert_called_once()
# Should ask internal solver to remove some constraints
assert internal.extract_constraint.call_count == 1
internal.extract_constraint.assert_has_calls([call("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
internal.is_constraint_satisfied.assert_called_once_with("<c3>", tol=1.0)
internal.is_constraint_satisfied.reset_mock()
internal.add_constraint.assert_called_once_with("<c3>")
internal.add_constraint.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.is_constraint_satisfied.assert_not_called()
internal.add_constraint.assert_not_called()
# LearningSolver calls after_solve_mip
component.after_solve_mip(
solver=solver,
instance=instance,
model=None,
stats=stats,
features=features,
training_data=sample,
)
# Should update training sample
assert sample["LazyStatic: Enforced"] == {"c1", "c2", "c3", "c4"}
# 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(
features: Features,
sample: TrainingSample,
) -> None:
comp = StaticLazyConstraintsComponent(threshold=0.5)
comp.classifiers["type-a"] = Mock(spec=Classifier)
comp.classifiers["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["type-b"] = Mock(spec=Classifier)
comp.classifiers["type-b"].predict_proba = lambda _: np.array( # type:ignore
[
[0.0, 1.0], # c4
]
)
pred = comp.sample_predict(features, sample)
assert pred == ["c1", "c2", "c4"]
def test_fit_xy() -> None:
x = cast(
Dict[Hashable, np.ndarray],
{
"type-a": np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]]),
"type-b": np.array([[1.0, 4.0, 0.0]]),
},
)
y = cast(
Dict[Hashable, np.ndarray],
{
"type-a": np.array([[False, True], [False, True], [True, False]]),
"type-b": np.array([[False, True]]),
},
)
clf = Mock(spec=Classifier)
clf.clone = Mock(side_effect=lambda: Mock(spec=Classifier))
comp = StaticLazyConstraintsComponent(classifier=clf)
comp.fit_xy(x, y)
assert clf.clone.call_count == 2
clf_a = comp.classifiers["type-a"]
clf_b = comp.classifiers["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["type-a"]) # type: ignore
assert_array_equal(clf_b.fit.call_args[0][0], x["type-b"]) # type: ignore
def test_sample_xy(
features: Features,
sample: TrainingSample,
) -> None:
x_expected = {
"type-a": [
[1.0, 1.0],
[1.0, 2.0],
[1.0, 3.0],
],
"type-b": [
[1.0, 4.0, 0.0],
],
"type-a": [[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]],
"type-b": [[1.0, 4.0, 0.0]],
}
y_expected = {
"type-a": [
[False, True],
[False, True],
[True, False],
],
"type-b": [
[False, True],
],
"type-a": [[False, True], [False, True], [True, False]],
"type-b": [[False, True]],
}
xy = StaticLazyConstraintsComponent.sample_xy(features, sample)
assert xy is not None