You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MIPLearn/tests/components/test_objective.py

259 lines
7.9 KiB

# 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 Hashable, Dict
from unittest.mock import Mock
import pytest
from numpy.testing import assert_array_equal
from miplearn import GurobiPyomoSolver, LearningSolver, Regressor, Instance
from miplearn.components.objective import ObjectiveValueComponent
from miplearn.features import TrainingSample, InstanceFeatures, Features
from tests.fixtures.knapsack import get_knapsack_instance
import numpy as np
@pytest.fixture
def instance(features: Features) -> Instance:
instance = Mock(spec=Instance)
instance.features = features
return instance
@pytest.fixture
def features() -> Features:
return Features(
instance=InstanceFeatures(
user_features=[1.0, 2.0],
)
)
@pytest.fixture
def sample() -> TrainingSample:
return TrainingSample(
lower_bound=1.0,
upper_bound=2.0,
lp_value=3.0,
)
@pytest.fixture
def sample_without_lp() -> TrainingSample:
return TrainingSample(
lower_bound=1.0,
upper_bound=2.0,
)
@pytest.fixture
def sample_without_ub() -> TrainingSample:
return TrainingSample(
lower_bound=1.0,
lp_value=3.0,
)
def test_sample_xy(
instance: Instance,
sample: TrainingSample,
) -> None:
x_expected = {
"Lower bound": [[1.0, 2.0, 3.0]],
"Upper bound": [[1.0, 2.0, 3.0]],
}
y_expected = {
"Lower bound": [[1.0]],
"Upper bound": [[2.0]],
}
xy = ObjectiveValueComponent.sample_xy(instance, sample)
assert xy is not None
x_actual, y_actual = xy
assert x_actual == x_expected
assert y_actual == y_expected
def test_sample_xy_without_lp(
instance: Instance,
sample_without_lp: TrainingSample,
) -> None:
x_expected = {
"Lower bound": [[1.0, 2.0]],
"Upper bound": [[1.0, 2.0]],
}
y_expected = {
"Lower bound": [[1.0]],
"Upper bound": [[2.0]],
}
xy = ObjectiveValueComponent.sample_xy(instance, sample_without_lp)
assert xy is not None
x_actual, y_actual = xy
assert x_actual == x_expected
assert y_actual == y_expected
def test_sample_xy_without_ub(
instance: Instance,
sample_without_ub: TrainingSample,
) -> None:
x_expected = {
"Lower bound": [[1.0, 2.0, 3.0]],
"Upper bound": [[1.0, 2.0, 3.0]],
}
y_expected = {"Lower bound": [[1.0]]}
xy = ObjectiveValueComponent.sample_xy(instance, sample_without_ub)
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:
x: Dict[Hashable, 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[Hashable, 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_fit_xy_without_ub() -> None:
x: Dict[Hashable, 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[Hashable, np.ndarray] = {
"Lower bound": np.array([[100.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 == 1
assert "Upper bound" not in comp.regressors
assert "Lower bound" in comp.regressors
assert comp.regressors["Lower bound"].fit.call_count == 1 # type: ignore
assert_array_equal(
comp.regressors["Lower bound"].fit.call_args[0][0], # type: ignore
x["Lower bound"],
)
assert_array_equal(
comp.regressors["Lower bound"].fit.call_args[0][1], # type: ignore
y["Lower bound"],
)
def test_sample_predict(
instance: Instance,
sample: TrainingSample,
) -> None:
x, y = ObjectiveValueComponent.sample_xy(instance, 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(instance, 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_predict_without_ub(
instance: Instance,
sample_without_ub: TrainingSample,
) -> None:
x, y = ObjectiveValueComponent.sample_xy(instance, sample_without_ub)
comp = ObjectiveValueComponent()
comp.regressors["Lower bound"] = Mock(spec=Regressor)
comp.regressors["Lower bound"].predict = Mock( # type: ignore
side_effect=lambda _: np.array([[50.0]])
)
pred = comp.sample_predict(instance, sample_without_ub)
assert pred == {
"Lower bound": 50.0,
}
assert_array_equal(
comp.regressors["Lower bound"].predict.call_args[0][0], # type: ignore
x["Lower bound"],
)
def test_sample_evaluate(instance: Instance, sample: TrainingSample) -> 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(instance, 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 = get_knapsack_instance(GurobiPyomoSolver())
solver.solve(instance)
solver.fit([instance])
stats = solver.solve(instance)
assert stats["Lower bound"] == stats["Objective: Predicted lower bound"]
assert stats["Upper bound"] == stats["Objective: Predicted upper bound"]