Implement MinWeightVertexCoverPerturber

This commit is contained in:
2025-12-08 15:37:16 -06:00
parent 146fb6b615
commit 485625e07f
3 changed files with 85 additions and 16 deletions

View File

@@ -1596,7 +1596,10 @@
"\n", "\n",
"The class [MinWeightVertexCoverGenerator][MinWeightVertexCoverGenerator] can be used to generate random instances of this problem. The class accepts the same parameters and behaves in the same way as [MaxWeightStableSetGenerator][MaxWeightStableSetGenerator]. See the [stable set section](#Stable-Set) for more details on the generation process.\n", "The class [MinWeightVertexCoverGenerator][MinWeightVertexCoverGenerator] can be used to generate random instances of this problem. The class accepts the same parameters and behaves in the same way as [MaxWeightStableSetGenerator][MaxWeightStableSetGenerator]. See the [stable set section](#Stable-Set) for more details on the generation process.\n",
"\n", "\n",
"To create multiple instances with the same graph structure but different vertex weights, you can use [MinWeightVertexCoverPerturber][MinWeightVertexCoverPerturber]. This class takes an existing MinWeightVertexCoverData instance and generates new instances by applying random scaling factors to the vertex weights while keeping the graph fixed.\n",
"\n",
"[MinWeightVertexCoverGenerator]: ../../api/problems/#module-miplearn.problems.vertexcover\n", "[MinWeightVertexCoverGenerator]: ../../api/problems/#module-miplearn.problems.vertexcover\n",
"[MinWeightVertexCoverPerturber]: ../../api/problems/#module-miplearn.problems.vertexcover\n",
"[MaxWeightStableSetGenerator]: ../../api/problems/#miplearn.problems.stab.MaxWeightStableSetGenerator\n", "[MaxWeightStableSetGenerator]: ../../api/problems/#miplearn.problems.stab.MaxWeightStableSetGenerator\n",
"\n", "\n",
"### Example" "### Example"
@@ -1604,7 +1607,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 9, "execution_count": 13,
"id": "5fff7afe-5b7a-4889-a502-66751ec979bf", "id": "5fff7afe-5b7a-4889-a502-66751ec979bf",
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
@@ -1618,8 +1621,8 @@
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"graph [(0, 2), (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), (8, 9)]\n", "graph [(0, 2), (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), (8, 9)]\n",
"weights[0] [37.45 95.07 73.2 59.87 15.6 15.6 5.81 86.62 60.11 70.81]\n", "weights[0] [33.78 94.78 71.97 55.15 14.32 14.33 5.41 82.5 56.7 65.79]\n",
"weights[1] [ 2.06 96.99 83.24 21.23 18.18 18.34 30.42 52.48 43.19 29.12]\n", "weights[1] [36. 86.89 68.02 56.08 14.75 15.26 5.35 82.41 57.66 64.06]\n",
"\n", "\n",
"Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (linux64 - \"Ubuntu 22.04.5 LTS\")\n", "Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (linux64 - \"Ubuntu 22.04.5 LTS\")\n",
"\n", "\n",
@@ -1627,34 +1630,34 @@
"Thread count: 16 physical cores, 16 logical processors, using up to 16 threads\n", "Thread count: 16 physical cores, 16 logical processors, using up to 16 threads\n",
"\n", "\n",
"Optimize a model with 15 rows, 10 columns and 30 nonzeros (Min)\n", "Optimize a model with 15 rows, 10 columns and 30 nonzeros (Min)\n",
"Model fingerprint: 0x2d2d1390\n", "Model fingerprint: 0xf99bd426\n",
"Model has 10 linear objective coefficients\n", "Model has 10 linear objective coefficients\n",
"Variable types: 0 continuous, 10 integer (10 binary)\n", "Variable types: 0 continuous, 10 integer (10 binary)\n",
"Coefficient statistics:\n", "Coefficient statistics:\n",
" Matrix range [1e+00, 1e+00]\n", " Matrix range [1e+00, 1e+00]\n",
" Objective range [6e+00, 1e+02]\n", " Objective range [5e+00, 9e+01]\n",
" Bounds range [1e+00, 1e+00]\n", " Bounds range [1e+00, 1e+00]\n",
" RHS range [1e+00, 1e+00]\n", " RHS range [1e+00, 1e+00]\n",
"Found heuristic solution: objective 301.0000000\n", "Found heuristic solution: objective 283.6700000\n",
"Presolve removed 7 rows and 2 columns\n", "Presolve removed 7 rows and 2 columns\n",
"Presolve time: 0.00s\n", "Presolve time: 0.00s\n",
"Presolved: 8 rows, 8 columns, 19 nonzeros\n", "Presolved: 8 rows, 8 columns, 19 nonzeros\n",
"Variable types: 0 continuous, 8 integer (8 binary)\n", "Variable types: 0 continuous, 8 integer (8 binary)\n",
"\n", "\n",
"Root relaxation: cutoff, 8 iterations, 0.00 seconds (0.00 work units)\n", "Root relaxation: cutoff, 6 iterations, 0.00 seconds (0.00 work units)\n",
"\n", "\n",
" Nodes | Current Node | Objective Bounds | Work\n", " Nodes | Current Node | Objective Bounds | Work\n",
" Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n",
"\n", "\n",
" 0 0 cutoff 0 301.00000 301.00000 0.00% - 0s\n", " 0 0 cutoff 0 283.67000 283.67000 0.00% - 0s\n",
"\n", "\n",
"Explored 1 nodes (8 simplex iterations) in 0.00 seconds (0.00 work units)\n", "Explored 1 nodes (6 simplex iterations) in 0.00 seconds (0.00 work units)\n",
"Thread count was 16 (of 16 available processors)\n", "Thread count was 16 (of 16 available processors)\n",
"\n", "\n",
"Solution count 1: 301 \n", "Solution count 1: 283.67 \n",
"\n", "\n",
"Optimal solution found (tolerance 1.00e-04)\n", "Optimal solution found (tolerance 1.00e-04)\n",
"Best objective 3.010000000000e+02, best bound 3.010000000000e+02, gap 0.0000%\n", "Best objective 2.836700000000e+02, best bound 2.836700000000e+02, gap 0.0000%\n",
"\n", "\n",
"User-callback calls 335, time in user-callback 0.00 sec\n" "User-callback calls 335, time in user-callback 0.00 sec\n"
] ]
@@ -1666,6 +1669,7 @@
"from scipy.stats import uniform, randint\n", "from scipy.stats import uniform, randint\n",
"from miplearn.problems.vertexcover import (\n", "from miplearn.problems.vertexcover import (\n",
" MinWeightVertexCoverGenerator,\n", " MinWeightVertexCoverGenerator,\n",
" MinWeightVertexCoverPerturber,\n",
" build_vertexcover_model_gurobipy,\n", " build_vertexcover_model_gurobipy,\n",
")\n", ")\n",
"\n", "\n",
@@ -1673,13 +1677,20 @@
"random.seed(42)\n", "random.seed(42)\n",
"np.random.seed(42)\n", "np.random.seed(42)\n",
"\n", "\n",
"# Generate random instances with a 10-node graph,\n", "# Generate a reference instance with a 10-node graph,\n",
"# 25% density and random weights in the [0, 100] interval.\n", "# 25% density and random weights in the [0, 100] interval.\n",
"data = MinWeightVertexCoverGenerator(\n", "generator = MinWeightVertexCoverGenerator(\n",
" w=uniform(loc=0.0, scale=100.0),\n", " w=uniform(loc=0.0, scale=100.0),\n",
" n=randint(low=10, high=11),\n", " n=randint(low=10, high=11),\n",
" p=uniform(loc=0.25, scale=0.0),\n", " p=uniform(loc=0.25, scale=0.0),\n",
").generate(10)\n", ")\n",
"reference_instance = generator.generate(1)[0]\n",
"\n",
"# Generate perturbed instances using the reference\n",
"perturber = MinWeightVertexCoverPerturber(\n",
" w_jitter=uniform(loc=0.9, scale=0.1),\n",
")\n",
"data = perturber.perturb(reference_instance, 10)\n",
"\n", "\n",
"# Print the graph and weights for two instances\n", "# Print the graph and weights for two instances\n",
"print(\"graph\", data[0].graph.edges)\n", "print(\"graph\", data[0].graph.edges)\n",

View File

@@ -12,7 +12,11 @@ from networkx import Graph
from scipy.stats import uniform, randint from scipy.stats import uniform, randint
from scipy.stats.distributions import rv_frozen from scipy.stats.distributions import rv_frozen
from .stab import MaxWeightStableSetGenerator from .stab import (
MaxWeightStableSetGenerator,
MaxWeightStableSetPerturber,
MaxWeightStableSetData,
)
from miplearn.solvers.gurobi import GurobiModel from miplearn.solvers.gurobi import GurobiModel
from ..io import read_pkl_gz from ..io import read_pkl_gz
@@ -24,12 +28,34 @@ class MinWeightVertexCoverData:
class MinWeightVertexCoverGenerator: class MinWeightVertexCoverGenerator:
"""Random instance generator for the Minimum-Weight Vertex Cover Problem.
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
`n` and `p`. For each instance, the generator independently samples each $w_v$ from
the user-provided probability distribution `w`.
"""
def __init__( def __init__(
self, self,
w: rv_frozen = uniform(loc=10.0, scale=1.0), w: rv_frozen = uniform(loc=10.0, scale=1.0),
n: rv_frozen = randint(low=250, high=251), n: rv_frozen = randint(low=250, high=251),
p: rv_frozen = uniform(loc=0.05, scale=0.0), p: rv_frozen = uniform(loc=0.05, scale=0.0),
): ):
"""Initialize the problem generator.
Parameters
----------
w: rv_continuous
Probability distribution for vertex weights.
n: rv_discrete
Probability distribution for parameter $n$ in Erdős-Rényi model.
p: rv_continuous
Probability distribution for parameter $p$ in Erdős-Rényi model.
"""
assert isinstance(w, rv_frozen), "w should be a SciPy probability distribution"
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._generator = MaxWeightStableSetGenerator(w, n, p) self._generator = MaxWeightStableSetGenerator(w, n, p)
def generate(self, n_samples: int) -> List[MinWeightVertexCoverData]: def generate(self, n_samples: int) -> List[MinWeightVertexCoverData]:
@@ -39,6 +65,38 @@ class MinWeightVertexCoverGenerator:
] ]
class MinWeightVertexCoverPerturber:
"""Perturbation generator for existing Minimum-Weight Vertex Cover instances.
Takes an existing MinWeightVertexCoverData instance and generates new instances
by applying randomization factors to the existing weights while keeping the graph fixed.
"""
def __init__(
self,
w_jitter: rv_frozen = uniform(loc=0.9, scale=0.2),
):
"""Initialize the perturbation generator.
Parameters
----------
w_jitter: rv_continuous
Probability distribution for randomization factors applied to vertex weights.
"""
self._perturber = MaxWeightStableSetPerturber(w_jitter)
def perturb(
self,
instance: MinWeightVertexCoverData,
n_samples: int,
) -> List[MinWeightVertexCoverData]:
stab_instance = MaxWeightStableSetData(instance.graph, instance.weights)
perturbed_instances = self._perturber.perturb(stab_instance, n_samples)
return [
MinWeightVertexCoverData(s.graph, s.weights) for s in perturbed_instances
]
def build_vertexcover_model_gurobipy( def build_vertexcover_model_gurobipy(
data: Union[str, MinWeightVertexCoverData] data: Union[str, MinWeightVertexCoverData]
) -> GurobiModel: ) -> GurobiModel:

View File

@@ -262,7 +262,7 @@ class PyomoModel(AbstractModel):
if len(obj_quad) > 0: if len(obj_quad) > 0:
nvars = len(names) nvars = len(names)
matrix = np.zeros((nvars, nvars)) matrix = np.zeros((nvars, nvars))
for ((left_varname, right_varname), coeff) in obj_quad.items(): for (left_varname, right_varname), coeff in obj_quad.items():
assert left_varname in varname_to_idx assert left_varname in varname_to_idx
assert right_varname in varname_to_idx assert right_varname in varname_to_idx
left_idx = varname_to_idx[left_varname] left_idx = varname_to_idx[left_varname]