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.
186 lines
7.2 KiB
186 lines
7.2 KiB
# 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 TemporaryDirectory
|
|
from typing import Callable, Any
|
|
|
|
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: Callable, data: Any) -> None:
|
|
_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: AbstractModel) -> None:
|
|
with TemporaryDirectory() as tempdir:
|
|
with H5File(f"{tempdir}/data.h5", "w") as h5:
|
|
|
|
def test_scalar(key: str, expected_value: Any) -> None:
|
|
actual_value = h5.get_scalar(key)
|
|
assert actual_value is not None
|
|
assert actual_value == expected_value
|
|
|
|
def test_array(key: str, expected_value: Any) -> None:
|
|
actual_value = h5.get_array(key)
|
|
assert actual_value is not None
|
|
assert actual_value.tolist() == expected_value
|
|
|
|
def test_sparse(key: str, expected_value: Any) -> None:
|
|
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
|
|
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) -> None:
|
|
with TemporaryDirectory() as tempdir:
|
|
with H5File(f"{tempdir}/data.h5", "w") 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)
|
|
mip_var_values = h5.get_array("mip_var_values")
|
|
assert mip_var_values is not None
|
|
assert mip_var_values.tolist() == [1, 0, 0, 0, 1]
|
|
|
|
|
|
def _test_fix_vars(model: AbstractModel) -> None:
|
|
with TemporaryDirectory() as tempdir:
|
|
with H5File(f"{tempdir}/data.h5", "w") 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)
|
|
mip_var_values = h5.get_array("mip_var_values")
|
|
assert mip_var_values is not None
|
|
assert mip_var_values.tolist() == [1, 0, 0, 0, 1]
|
|
|
|
|
|
def _test_infeasible(model: AbstractModel) -> None:
|
|
with TemporaryDirectory() as tempdir:
|
|
with H5File(f"{tempdir}/data.h5", "w") 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
|