Implement BinPackPerturber

This commit is contained in:
2025-12-08 13:16:23 -06:00
parent a4cb46f73e
commit 9192bb02eb
3 changed files with 111 additions and 100 deletions

View File

@@ -87,11 +87,12 @@
"\n",
"Random instances of the bin packing problem can be generated using the class [BinPackGenerator][BinPackGenerator].\n",
"\n",
"If `fix_items=False`, the class samples the user-provided probability distributions `n`, `sizes` and `capacity` to decide, respectively, the number of items, the sizes of the items and capacity of the bin. All values are sampled independently.\n",
"The class samples the user-provided probability distributions `n`, `sizes` and `capacity` to decide, respectively, the number of items, the sizes of the items and capacity of the bin. All values are sampled independently.\n",
"\n",
"If `fix_items=True`, the class creates a reference instance, using the method previously described, then generates additional instances by perturbing its item sizes and bin capacity. More specifically, the sizes of the items are set to $s_i \\gamma_i$, where $s_i$ is the size of the $i$-th item in the reference instance and $\\gamma_i$ is sampled from `sizes_jitter`. Similarly, the bin size is set to $B \\beta$, where $B$ is the reference bin size and $\\beta$ is sampled from `capacity_jitter`. The number of items remains the same across all generated instances.\n",
"To create multiple instances with the same items but different sizes and capacities, you can use [BinPackPerturber][BinPackPerturber]. This class takes an existing BinPackData instance and generates new instances by perturbing its item sizes and bin capacity. More specifically, the sizes of the items are set to $s_i \\gamma_i$, where $s_i$ is the size of the $i$-th item in the reference instance and $\\gamma_i$ is sampled from `sizes_jitter`. Similarly, the bin size is set to $B \\beta$, where $B$ is the reference bin size and $\\beta$ is sampled from `capacity_jitter`. The number of items remains the same across all generated instances.\n",
"\n",
"[BinPackGenerator]: ../../api/problems/#miplearn.problems.binpack.BinPackGenerator"
"[BinPackGenerator]: ../../api/problems/#miplearn.problems.binpack.BinPackGenerator\n",
"[BinPackPerturber]: ../../api/problems/#miplearn.problems.binpack.BinPackPerturber"
]
},
{
@@ -104,7 +105,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 2,
"id": "f14e560c-ef9f-4c48-8467-72d6acce5f9f",
"metadata": {
"ExecuteTime": {
@@ -118,25 +119,24 @@
"name": "stdout",
"output_type": "stream",
"text": [
"0 [ 8.47 26. 19.52 14.11 3.65 3.65 1.4 21.76 14.82 16.96] 102.24\n",
"1 [ 8.69 22.78 17.81 14.83 4.12 3.67 1.46 22.05 13.66 18.08] 93.41\n",
"2 [ 8.55 25.9 20. 15.89 3.75 3.59 1.51 21.4 13.89 17.68] 90.69\n",
"3 [10.13 22.62 18.89 14.4 3.92 3.94 1.36 23.69 15.85 19.26] 107.9\n",
"4 [ 9.55 25.77 16.79 14.06 3.55 3.76 1.42 20.66 16.02 17.19] 95.62\n",
"5 [ 9.44 22.06 19.41 13.69 4.28 4.11 1.36 19.51 15.98 18.43] 104.58\n",
"6 [ 9.87 21.74 17.78 13.82 4.18 4. 1.4 19.76 14.46 17.08] 104.59\n",
"7 [ 9.62 25.61 18.2 13.83 4.07 4.1 1.47 22.83 15.01 17.78] 98.55\n",
"8 [ 8.47 21.9 16.58 15.37 3.76 3.91 1.57 20.57 14.76 18.61] 94.58\n",
"9 [ 8.57 22.77 17.06 16.25 4.14 4. 1.56 22.97 14.09 19.09] 100.79\n",
"0 [ 8.46 26. 19.52 14.11 3.65 3.65 1.39 21.76 14.83 16.96] 102.24\n",
"1 [ 8.69 22.78 17.81 14.84 4.12 3.67 1.45 22.05 13.67 18.08] 93.41\n",
"2 [ 8.55 25.9 20. 15.89 3.75 3.59 1.5 21.39 13.89 17.68] 90.69\n",
"3 [10.13 22.62 18.89 14.41 3.92 3.94 1.36 23.68 15.86 19.26] 107.9\n",
"4 [ 9.54 25.78 16.79 14.06 3.55 3.76 1.42 20.66 16.02 17.19] 95.62\n",
"5 [ 9.44 22.06 19.41 13.7 4.28 4.11 1.36 19.51 15.98 18.43] 104.58\n",
"6 [ 9.87 21.75 17.78 13.82 4.18 4. 1.4 19.76 14.46 17.08] 104.59\n",
"7 [ 9.62 25.61 18.2 13.83 4.07 4.1 1.47 22.82 15.01 17.78] 98.55\n",
"8 [ 8.47 21.91 16.59 15.38 3.76 3.91 1.57 20.56 14.76 18.6 ] 94.58\n",
"9 [ 8.57 22.77 17.06 16.26 4.14 4. 1.56 22.96 14.09 19.09] 100.79\n",
"\n",
"Set parameter LicenseID to value 389975\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 20 rows, 110 columns and 210 nonzeros (Min)\n",
"Model fingerprint: 0x1ff9913f\n",
"Model fingerprint: 0xde862da6\n",
"Model has 10 linear objective coefficients\n",
"Variable types: 0 continuous, 110 integer (110 binary)\n",
"Coefficient statistics:\n",
@@ -149,18 +149,18 @@
"Presolved: 20 rows, 110 columns, 210 nonzeros\n",
"Variable types: 0 continuous, 110 integer (110 binary)\n",
"\n",
"Root relaxation: objective 1.274844e+00, 38 iterations, 0.00 seconds (0.00 work units)\n",
"Root relaxation: objective 1.274746e+00, 39 iterations, 0.00 seconds (0.00 work units)\n",
"\n",
" Nodes | Current Node | Objective Bounds | Work\n",
" Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n",
"\n",
" 0 0 1.27484 0 4 5.00000 1.27484 74.5% - 0s\n",
"H 0 0 4.0000000 1.27484 68.1% - 0s\n",
"H 0 0 3.0000000 1.27484 57.5% - 0s\n",
"H 0 0 2.0000000 1.27484 36.3% - 0s\n",
" 0 0 1.27484 0 4 2.00000 1.27484 36.3% - 0s\n",
" 0 0 1.27475 0 4 5.00000 1.27475 74.5% - 0s\n",
"H 0 0 4.0000000 1.27475 68.1% - 0s\n",
"H 0 0 3.0000000 1.27475 57.5% - 0s\n",
"H 0 0 2.0000000 1.27475 36.3% - 0s\n",
" 0 0 1.27475 0 4 2.00000 1.27475 36.3% - 0s\n",
"\n",
"Explored 1 nodes (38 simplex iterations) in 0.00 seconds (0.00 work units)\n",
"Explored 1 nodes (39 simplex iterations) in 0.01 seconds (0.00 work units)\n",
"Thread count was 16 (of 16 available processors)\n",
"\n",
"Solution count 4: 2 3 4 5 \n",
@@ -170,33 +170,30 @@
"\n",
"User-callback calls 149, time in user-callback 0.00 sec\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/axavier/Notebooks/[2025-12] Parallel BB Datasets/venv/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
" from .autonotebook import tqdm as notebook_tqdm\n"
]
}
],
"source": [
"import numpy as np\n",
"from scipy.stats import uniform, randint\n",
"from miplearn.problems.binpack import BinPackGenerator, build_binpack_model_gurobipy\n",
"from miplearn.problems.binpack import BinPackGenerator, BinPackPerturber, build_binpack_model_gurobipy\n",
"\n",
"# Set random seed, to make example reproducible\n",
"np.random.seed(42)\n",
"\n",
"# Generate random instances of the binpack problem with ten items\n",
"data = BinPackGenerator(\n",
"# Generate a reference instance with ten items\n",
"generator = BinPackGenerator(\n",
" n=randint(low=10, high=11),\n",
" sizes=uniform(loc=0, scale=25),\n",
" capacity=uniform(loc=100, scale=0),\n",
")\n",
"reference_instance = generator.generate(1)[0]\n",
"\n",
"# Generate perturbed instances using the reference\n",
"perturber = BinPackPerturber(\n",
" sizes_jitter=uniform(loc=0.9, scale=0.2),\n",
" capacity_jitter=uniform(loc=0.9, scale=0.2),\n",
" fix_items=True,\n",
").generate(10)\n",
")\n",
"data = perturber.perturb(reference_instance, 10)\n",
"\n",
"# Print sizes and capacities\n",
"for i in range(10):\n",

View File

@@ -34,19 +34,10 @@ class BinPackData:
class BinPackGenerator:
"""Random instance generator for the bin packing problem.
If `fix_items=False`, the class samples the user-provided probability distributions
Generates instances by sampling the user-provided probability distributions
n, sizes and capacity to decide, respectively, the number of items, the sizes of
the items and capacity of the bin. All values are sampled independently.
If `fix_items=True`, the class creates a reference instance, using the method
previously described, then generates additional instances by perturbing its item
sizes and bin capacity. More specifically, the sizes of the items are set to `s_i
* gamma_i` where `s_i` is the size of the i-th item in the reference instance and
`gamma_i` is sampled from `sizes_jitter`. Similarly, the bin capacity is set to `B *
beta`, where `B` is the reference bin capacity and `beta` is sampled from
`capacity_jitter`. The number of items remains the same across all generated
instances.
Args
----
n
@@ -55,13 +46,6 @@ class BinPackGenerator:
Probability distribution for the item sizes.
capacity
Probability distribution for the bin capacity.
sizes_jitter
Probability distribution for the item size randomization.
capacity_jitter
Probability distribution for the bin capacity.
fix_items
If `True`, generates a reference instance, then applies some perturbation to it.
If `False`, generates completely different instances.
"""
def __init__(
@@ -69,17 +53,10 @@ class BinPackGenerator:
n: rv_frozen,
sizes: rv_frozen,
capacity: rv_frozen,
sizes_jitter: rv_frozen,
capacity_jitter: rv_frozen,
fix_items: bool,
) -> None:
self.n = n
self.sizes = sizes
self.capacity = capacity
self.sizes_jitter = sizes_jitter
self.capacity_jitter = capacity_jitter
self.fix_items = fix_items
self.ref_data: Optional[BinPackData] = None
def generate(self, n_samples: int) -> List[BinPackData]:
"""Generates random instances.
@@ -91,22 +68,62 @@ class BinPackGenerator:
"""
def _sample() -> BinPackData:
if self.ref_data is None:
n = self.n.rvs()
sizes = self.sizes.rvs(n)
capacity = self.capacity.rvs()
if self.fix_items:
self.ref_data = BinPackData(sizes, capacity)
else:
n = self.ref_data.sizes.shape[0]
sizes = self.ref_data.sizes
capacity = self.ref_data.capacity
sizes = sizes * self.sizes_jitter.rvs(n)
capacity = capacity * self.capacity_jitter.rvs()
n = self.n.rvs()
sizes = self.sizes.rvs(n)
capacity = self.capacity.rvs()
return BinPackData(sizes.round(2), capacity.round(2))
return [_sample() for n in range(n_samples)]
return [_sample() for _ in range(n_samples)]
class BinPackPerturber:
"""Perturbation generator for existing bin packing instances.
Takes an existing BinPackData instance and generates new instances by perturbing
its item sizes and bin capacity. The sizes of the items are set to `s_i * gamma_i`
where `s_i` is the size of the i-th item in the reference instance and `gamma_i`
is sampled from `sizes_jitter`. Similarly, the bin capacity is set to `B * beta`,
where `B` is the reference bin capacity and `beta` is sampled from `capacity_jitter`.
The number of items remains the same across all generated instances.
Args
----
sizes_jitter
Probability distribution for the item size randomization.
capacity_jitter
Probability distribution for the bin capacity randomization.
"""
def __init__(
self,
sizes_jitter: rv_frozen,
capacity_jitter: rv_frozen,
) -> None:
self.sizes_jitter = sizes_jitter
self.capacity_jitter = capacity_jitter
def perturb(
self,
instance: BinPackData,
n_samples: int,
) -> List[BinPackData]:
"""Generates perturbed instances.
Parameters
----------
instance
The reference instance to perturb.
n_samples
Number of samples to generate.
"""
def _sample() -> BinPackData:
n = instance.sizes.shape[0]
sizes = instance.sizes * self.sizes_jitter.rvs(n)
capacity = instance.capacity * self.capacity_jitter.rvs()
return BinPackData(sizes.round(2), capacity.round(2))
return [_sample() for _ in range(n_samples)]
def build_binpack_model_gurobipy(data: Union[str, BinPackData]) -> GurobiModel:

View File

@@ -18,37 +18,34 @@ def test_binpack_generator() -> None:
n=randint(low=10, high=11),
sizes=uniform(loc=0, scale=10),
capacity=uniform(loc=100, scale=0),
sizes_jitter=uniform(loc=0.9, scale=0.2),
capacity_jitter=uniform(loc=0.9, scale=0.2),
fix_items=True,
)
data = gen.generate(2)
assert data[0].sizes.tolist() == [
3.39,
10.4,
7.81,
5.64,
1.46,
1.46,
0.56,
8.7,
5.93,
6.79,
]
assert data[0].capacity == 102.24
assert data[1].sizes.tolist() == [
3.48,
9.11,
7.12,
5.93,
1.65,
1.47,
3.75,
9.51,
7.32,
5.99,
1.56,
1.56,
0.58,
8.82,
5.47,
7.23,
8.66,
6.01,
7.08,
]
assert data[1].capacity == 93.41
assert data[0].capacity == 100.0
assert data[1].sizes.tolist() == [
0.21,
9.7,
8.32,
2.12,
1.82,
1.83,
3.04,
5.25,
4.32,
2.91,
]
assert data[1].capacity == 100.0
def test_binpack() -> None: