mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-08 18:38:51 -06:00
Implement MaxCutPerturber
This commit is contained in:
@@ -1666,17 +1666,7 @@
|
||||
"cell_type": "markdown",
|
||||
"id": "j49upfw2o8k",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Random instance generator\n",
|
||||
"\n",
|
||||
"The class [MaxCutGenerator][MaxCutGenerator] can be used to generate random instances of this problem. The generator operates in two modes:\n",
|
||||
"\n",
|
||||
"When `fix_graph=False`, a new random Erdős-Rényi graph $G_{n,p}$ is generated for each instance, where $n$ (number of vertices) and $p$ (edge probability) are sampled from the provided probability distributions. Each edge is assigned a random weight drawn from the set $\\{-1, +1\\}$ with equal probability.\n",
|
||||
"\n",
|
||||
"When `fix_graph=True`, a single random graph is generated during initialization and reused across all instances. To create variations, the generator randomly flips the sign of each edge weight with probability `w_jitter`, allowing for instances with the same graph structure but different edge weight patterns.\n",
|
||||
"\n",
|
||||
"[MaxCutGenerator]: ../../api/problems/#miplearn.problems.maxcut.MaxCutGenerator"
|
||||
]
|
||||
"source": "### Random instance generator\n\nThe class [MaxCutGenerator][MaxCutGenerator] can be used to generate random instances of this problem. For each instance, the generator creates a new random Erdős-Rényi graph $G_{n,p}$, where $n$ (number of vertices) and $p$ (edge probability) are sampled from user-provided probability distributions. Each edge is assigned a random weight drawn from the set $\\{-1, +1\\}$ with equal probability.\n\nTo create multiple instances with the same graph structure but different edge weight patterns, you can use [MaxCutPerturber][MaxCutPerturber]. This class takes an existing MaxCutData instance and generates new instances by randomly flipping the sign of each edge weight with a given probability while keeping the graph structure fixed.\n\n[MaxCutGenerator]: ../../api/problems/#miplearn.problems.maxcut.MaxCutGenerator\n[MaxCutPerturber]: ../../api/problems/#miplearn.problems.maxcut.MaxCutPerturber"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -1688,85 +1678,11 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"execution_count": null,
|
||||
"id": "uge28hmv3a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"graph edges: [(0, 2), (0, 3), (0, 4), (0, 8), (1, 2), (1, 3), (1, 5), (1, 6), (1, 9), (2, 5), (2, 9), (3, 6), (3, 7), (6, 9), (7, 8), (7, 9), (8, 9)]\n",
|
||||
"weights[0]: [ 1 1 1 -1 -1 -1 -1 -1 -1 -1 1 -1 -1 1 1 -1 -1]\n",
|
||||
"weights[1]: [-1 1 -1 -1 -1 1 -1 1 -1 1 -1 1 -1 -1 1 -1 1]\n",
|
||||
"\n",
|
||||
"Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (linux64 - \"Ubuntu 22.04.5 LTS\")\n",
|
||||
"\n",
|
||||
"CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]\n",
|
||||
"Thread count: 16 physical cores, 16 logical processors, using up to 16 threads\n",
|
||||
"\n",
|
||||
"Optimize a model with 0 rows, 10 columns and 0 nonzeros (Min)\n",
|
||||
"Model fingerprint: 0x005f9eac\n",
|
||||
"Model has 5 linear objective coefficients\n",
|
||||
"Model has 17 quadratic objective terms\n",
|
||||
"Variable types: 0 continuous, 10 integer (10 binary)\n",
|
||||
"Coefficient statistics:\n",
|
||||
" Matrix range [0e+00, 0e+00]\n",
|
||||
" Objective range [1e+00, 5e+00]\n",
|
||||
" QObjective range [2e+00, 2e+00]\n",
|
||||
" Bounds range [1e+00, 1e+00]\n",
|
||||
" RHS range [0e+00, 0e+00]\n",
|
||||
"Found heuristic solution: objective 0.0000000\n",
|
||||
"Found heuristic solution: objective -3.0000000\n",
|
||||
"Presolve removed 0 rows and 10 columns\n",
|
||||
"Presolve time: 0.00s\n",
|
||||
"Presolve: All rows and columns removed\n",
|
||||
"\n",
|
||||
"Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)\n",
|
||||
"Thread count was 1 (of 16 available processors)\n",
|
||||
"\n",
|
||||
"Solution count 2: -3 0 \n",
|
||||
"No other solutions better than -3\n",
|
||||
"\n",
|
||||
"Optimal solution found (tolerance 1.00e-04)\n",
|
||||
"Best objective -3.000000000000e+00, best bound -3.000000000000e+00, gap 0.0000%\n",
|
||||
"\n",
|
||||
"User-callback calls 100, time in user-callback 0.00 sec\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import random\n",
|
||||
"import numpy as np\n",
|
||||
"from scipy.stats import uniform, randint\n",
|
||||
"from miplearn.problems.maxcut import (\n",
|
||||
" MaxCutGenerator,\n",
|
||||
" build_maxcut_model_gurobipy,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Set random seed to make example reproducible\n",
|
||||
"random.seed(42)\n",
|
||||
"np.random.seed(42)\n",
|
||||
"\n",
|
||||
"# Generate random instances with a fixed 10-node graph,\n",
|
||||
"# 30% edge probability, and random weight jittering\n",
|
||||
"data = MaxCutGenerator(\n",
|
||||
" n=randint(low=10, high=11),\n",
|
||||
" p=uniform(loc=0.3, scale=0.0),\n",
|
||||
" w_jitter=0.2,\n",
|
||||
" fix_graph=True,\n",
|
||||
").generate(10)\n",
|
||||
"\n",
|
||||
"# Print the graph and weights for two instances\n",
|
||||
"print(\"graph edges:\", list(data[0].graph.edges()))\n",
|
||||
"print(\"weights[0]:\", data[0].weights)\n",
|
||||
"print(\"weights[1]:\", data[1].weights)\n",
|
||||
"print()\n",
|
||||
"\n",
|
||||
"# Build and optimize the first instance\n",
|
||||
"model = build_maxcut_model_gurobipy(data[0])\n",
|
||||
"model.optimize()"
|
||||
]
|
||||
"outputs": [],
|
||||
"source": "import random\nimport numpy as np\nfrom scipy.stats import uniform, randint\nfrom miplearn.problems.maxcut import (\n MaxCutGenerator,\n MaxCutPerturber,\n build_maxcut_model_gurobipy,\n)\n\n# Set random seed to make example reproducible\nrandom.seed(42)\nnp.random.seed(42)\n\n# Generate a reference instance with a 10-node graph and 30% edge probability\ngenerator = MaxCutGenerator(\n n=randint(low=10, high=11),\n p=uniform(loc=0.3, scale=0.0),\n)\nreference_instance = generator.generate(1)[0]\n\n# Generate perturbed instances using the reference\nperturber = MaxCutPerturber(w_jitter=0.2)\ndata = perturber.perturb(reference_instance, 10)\n\n# Print the graph and weights for two instances\nprint(\"graph edges:\", list(data[0].graph.edges()))\nprint(\"weights[0]:\", data[0].weights)\nprint(\"weights[1]:\", data[1].weights)\nprint()\n\n# Build and optimize the first instance\nmodel = build_maxcut_model_gurobipy(data[0])\nmodel.optimize()"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -1790,4 +1706,4 @@
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
}
|
||||
@@ -25,24 +25,18 @@ class MaxCutData:
|
||||
|
||||
|
||||
class MaxCutGenerator:
|
||||
"""
|
||||
Random instance generator for the Maximum Cut Problem.
|
||||
"""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, and each edge is assigned a random weight drawn from the set {-1, 1}, with equal probability. To
|
||||
generate each instance variation, the generator randomly flips the sign of each edge weight with probability
|
||||
`w_jitter`. The graph remains the same across all variations.
|
||||
|
||||
When `fix_graph=False`, a new random graph is generated for each instance, with random {-1,1} edge weights.
|
||||
Generates instances by creating a new random Erdős-Rényi graph $G_{n,p}$ for each
|
||||
instance, where $n$ and $p$ are sampled from user-provided probability distributions.
|
||||
For each instance, the generator assigns random edge weights drawn from the set {-1, 1}
|
||||
with equal probability.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
n: rv_frozen,
|
||||
p: rv_frozen,
|
||||
w_jitter: float = 0.0,
|
||||
fix_graph: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize the problem generator.
|
||||
@@ -53,35 +47,16 @@ class MaxCutGenerator:
|
||||
Probability distribution for the number of nodes.
|
||||
p: rv_continuous
|
||||
Probability distribution for the graph density.
|
||||
w_jitter: float
|
||||
Probability that each edge weight flips from -1 to 1. Only applicable if fix_graph is True.
|
||||
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.w_jitter = w_jitter
|
||||
self.fix_graph = fix_graph
|
||||
self.graph = None
|
||||
self.weights = None
|
||||
if fix_graph:
|
||||
self.graph = self._generate_graph()
|
||||
self.weights = self._generate_weights(self.graph)
|
||||
|
||||
def generate(self, n_samples: int) -> List[MaxCutData]:
|
||||
def _sample() -> MaxCutData:
|
||||
if self.graph is not None:
|
||||
graph = self.graph
|
||||
weights = self.weights
|
||||
jitter = self._generate_jitter(graph)
|
||||
weights = weights * jitter
|
||||
else:
|
||||
graph = self._generate_graph()
|
||||
weights = self._generate_weights(graph)
|
||||
assert weights is not None
|
||||
graph = self._generate_graph()
|
||||
weights = self._generate_weights(graph)
|
||||
return MaxCutData(graph, weights)
|
||||
|
||||
return [_sample() for _ in range(n_samples)]
|
||||
@@ -94,6 +69,41 @@ class MaxCutGenerator:
|
||||
m = graph.number_of_edges()
|
||||
return np.random.randint(2, size=(m,)) * 2 - 1
|
||||
|
||||
|
||||
class MaxCutPerturber:
|
||||
"""Perturbation generator for existing Maximum Cut instances.
|
||||
|
||||
Takes an existing MaxCutData instance and generates new instances by randomly
|
||||
flipping the sign of each edge weight with a given probability while keeping
|
||||
the graph structure fixed.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
w_jitter: float = 0.05,
|
||||
):
|
||||
"""Initialize the perturbation generator.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
w_jitter: float
|
||||
Probability that each edge weight flips sign (from -1 to 1 or vice versa).
|
||||
"""
|
||||
assert 0.0 <= w_jitter <= 1.0, "w_jitter should be between 0.0 and 1.0"
|
||||
self.w_jitter = w_jitter
|
||||
|
||||
def perturb(
|
||||
self,
|
||||
instance: MaxCutData,
|
||||
n_samples: int,
|
||||
) -> List[MaxCutData]:
|
||||
def _sample() -> MaxCutData:
|
||||
jitter = self._generate_jitter(instance.graph)
|
||||
weights = instance.weights * jitter
|
||||
return MaxCutData(instance.graph, weights)
|
||||
|
||||
return [_sample() for _ in range(n_samples)]
|
||||
|
||||
def _generate_jitter(self, graph: Graph) -> np.ndarray:
|
||||
m = graph.number_of_edges()
|
||||
return (np.random.rand(m) >= self.w_jitter).astype(int) * 2 - 1
|
||||
|
||||
@@ -22,12 +22,11 @@ def _set_seed() -> None:
|
||||
np.random.seed(42)
|
||||
|
||||
|
||||
def test_maxcut_generator_not_fixed() -> None:
|
||||
def test_maxcut_generator() -> 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
|
||||
@@ -41,35 +40,6 @@ def test_maxcut_generator_not_fixed() -> None:
|
||||
(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,
|
||||
w_jitter=0.25,
|
||||
)
|
||||
data = gen.generate(3)
|
||||
assert len(data) == 3
|
||||
for i in range(3):
|
||||
assert list(data[i].graph.nodes()) == [0, 1, 2, 3, 4]
|
||||
assert list(data[i].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 data[1].weights.tolist() == [-1, -1, -1, -1, 1, -1]
|
||||
assert data[2].weights.tolist() == [1, 1, -1, -1, -1, 1]
|
||||
|
||||
|
||||
def test_maxcut_model() -> None:
|
||||
@@ -77,7 +47,6 @@ def test_maxcut_model() -> None:
|
||||
data = MaxCutGenerator(
|
||||
n=randint(low=10, high=11),
|
||||
p=uniform(loc=0.5, scale=0.0),
|
||||
fix_graph=True,
|
||||
).generate(1)[0]
|
||||
for model in [
|
||||
build_maxcut_model_gurobipy(data),
|
||||
|
||||
Reference in New Issue
Block a user