Compare commits

...

2 Commits

@ -0,0 +1,111 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
from dataclasses import dataclass
from typing import List, Union, Optional, Any
import gurobipy as gp
import networkx as nx
import numpy as np
from gurobipy import quicksum
from networkx import Graph
from scipy.stats.distributions import rv_frozen
from miplearn.io import read_pkl_gz
from miplearn.problems import _gurobipy_set_params
from miplearn.solvers.gurobi import GurobiModel
@dataclass
class MaxCutData:
graph: Graph
weights: np.ndarray
class MaxCutGenerator:
"""
Random instance generator for the Maximum Cut Problem.
The generator operates in two modes. When `fix_graph=True`, a single random
Erdős-Rényi graph $G_{n,p}$ is generated during initialization, with parameters $n$
and $p$ drawn from their respective probability distributions. For each instance,
only edge weights are randomly sampled from the set {1, -1}, while the graph
structure remains fixed.
When `fix_graph=False`, both the graph structure and edge weights are randomly
generated for each instance.
"""
def __init__(
self,
n: rv_frozen,
p: rv_frozen,
fix_graph: bool,
):
"""
Initialize the problem generator.
Parameters
----------
n: rv_discrete
Probability distribution for the number of nodes.
p: rv_continuous
Probability distribution for the graph density.
fix_graph: bool
Controls graph generation for instances. If false, a new random graph is
generated for each instance. If true, the same graph is reused across instances.
"""
assert isinstance(n, rv_frozen), "n should be a SciPy probability distribution"
assert isinstance(p, rv_frozen), "p should be a SciPy probability distribution"
self.n = n
self.p = p
self.fix_graph = fix_graph
self.graph = None
if fix_graph:
self.graph = self._generate_graph()
def generate(self, n_samples: int) -> List[MaxCutData]:
def _sample() -> MaxCutData:
if self.graph is not None:
graph = self.graph
else:
graph = self._generate_graph()
m = graph.number_of_edges()
weights = np.random.randint(2, size=(m,)) * 2 - 1
return MaxCutData(graph, weights)
return [_sample() for _ in range(n_samples)]
def _generate_graph(self) -> Graph:
return nx.generators.random_graphs.binomial_graph(self.n.rvs(), self.p.rvs())
def build_maxcut_model_gurobipy(
data: Union[str, MaxCutData],
params: Optional[dict[str, Any]] = None,
) -> GurobiModel:
# Initialize model
model = gp.Model()
_gurobipy_set_params(model, params)
# Read data
data = _maxcut_read(data)
nodes = list(data.graph.nodes())
edges = list(data.graph.edges())
# Add decision variables
x = model.addVars(nodes, vtype=gp.GRB.BINARY, name="x")
# Add the objective function
model.setObjective(quicksum(
- data.weights[i] * x[e[0]] * (1 - x[e[1]]) for (i, e) in enumerate(edges)
))
model.update()
return GurobiModel(model)
def _maxcut_read(data: Union[str, MaxCutData]) -> MaxCutData:
if isinstance(data, str):
data = read_pkl_gz(data)
assert isinstance(data, MaxCutData)
return data

@ -0,0 +1,57 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
import random
import numpy as np
from miplearn.problems.maxcut import MaxCutGenerator, build_maxcut_model_gurobipy
from scipy.stats import randint, uniform
def _set_seed():
random.seed(42)
np.random.seed(42)
def test_maxcut_generator_not_fixed() -> None:
_set_seed()
gen = MaxCutGenerator(
n=randint(low=5, high=6),
p=uniform(loc=0.5, scale=0.0),
fix_graph=False,
)
data = gen.generate(3)
assert len(data) == 3
assert list(data[0].graph.nodes()) == [0, 1, 2, 3, 4]
assert list(data[0].graph.edges()) == [(0, 2), (0, 3), (0, 4), (2, 3), (2, 4), (3, 4)]
assert data[0].weights.tolist() == [-1, 1, -1, -1, -1, 1]
assert list(data[1].graph.nodes()) == [0, 1, 2, 3, 4]
assert list(data[1].graph.edges()) == [(0, 1), (0, 3), (0, 4), (1, 4), (3, 4)]
assert data[1].weights.tolist() == [-1, -1, -1, 1, -1]
def test_maxcut_generator_fixed() -> None:
random.seed(42)
np.random.seed(42)
gen = MaxCutGenerator(
n=randint(low=5, high=6),
p=uniform(loc=0.5, scale=0.0),
fix_graph=True,
)
data = gen.generate(3)
assert len(data) == 3
assert list(data[0].graph.nodes()) == [0, 1, 2, 3, 4]
assert list(data[0].graph.edges()) == [(0, 2), (0, 3), (0, 4), (2, 3), (2, 4), (3, 4)]
assert data[0].weights.tolist() == [-1, 1, -1, -1, -1, 1]
assert list(data[1].graph.nodes()) == [0, 1, 2, 3, 4]
assert list(data[1].graph.edges()) == [(0, 2), (0, 3), (0, 4), (2, 3), (2, 4), (3, 4)]
assert data[1].weights.tolist() == [-1, -1, -1, 1, -1, -1]
def test_maxcut_model():
_set_seed()
data = MaxCutGenerator(
n=randint(low=20, high=21),
p=uniform(loc=0.5, scale=0.0),
fix_graph=True,
).generate(1)[0]
model = build_maxcut_model_gurobipy(data)
model.optimize()
assert model.inner.ObjVal == -26

@ -2,7 +2,7 @@
# 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 tempfile import TemporaryDirectory
import numpy as np
from scipy.stats import randint, uniform
@ -86,8 +86,8 @@ def test_set_cover() -> None:
build_setcover_model_gurobipy(data),
]:
assert isinstance(model, AbstractModel)
with NamedTemporaryFile() as tempfile:
with H5File(tempfile.name) as h5:
with TemporaryDirectory() as tempdir:
with H5File(f"{tempdir}/data.h5", "w") as h5:
model.optimize()
model.extract_after_mip(h5)
assert h5.get_scalar("mip_obj_value") == 11.0

@ -1,7 +1,7 @@
# 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 tempfile import TemporaryDirectory
import networkx as nx
import numpy as np
@ -25,8 +25,8 @@ def test_stab() -> None:
build_stab_model_pyomo(data),
]:
assert isinstance(model, AbstractModel)
with NamedTemporaryFile() as tempfile:
with H5File(tempfile.name) as h5:
with TemporaryDirectory() as tempdir:
with H5File(f"{tempdir}/data.h5", "w") as h5:
model.optimize()
model.extract_after_mip(h5)
assert h5.get_scalar("mip_obj_value") == -2.0

@ -1,7 +1,7 @@
# 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 tempfile import TemporaryDirectory
from typing import Any
import numpy as np
@ -11,31 +11,31 @@ 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
with TemporaryDirectory() as tempdir:
with H5File(f"{tempdir}/data.h5", "w") as h5:
_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_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
_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:

@ -2,7 +2,7 @@
# 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 tempfile import TemporaryDirectory
from typing import Callable, Any
import numpy as np
@ -49,8 +49,8 @@ def _test_solver(build_model: Callable, data: Any) -> None:
def _test_extract(model: AbstractModel) -> None:
with NamedTemporaryFile() as tempfile:
with H5File(tempfile.name) as h5:
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)
@ -129,7 +129,6 @@ def _test_extract(model: AbstractModel) -> None:
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
@ -145,8 +144,8 @@ def _test_extract(model: AbstractModel) -> None:
def _test_add_constr(model: AbstractModel) -> None:
with NamedTemporaryFile() as tempfile:
with H5File(tempfile.name) as h5:
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]]),
@ -161,8 +160,8 @@ def _test_add_constr(model: AbstractModel) -> None:
def _test_fix_vars(model: AbstractModel) -> None:
with NamedTemporaryFile() as tempfile:
with H5File(tempfile.name) as h5:
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]),
@ -175,8 +174,8 @@ def _test_fix_vars(model: AbstractModel) -> None:
def _test_infeasible(model: AbstractModel) -> None:
with NamedTemporaryFile() as tempfile:
with H5File(tempfile.name) as h5:
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]),

Loading…
Cancel
Save