Coverage for moptipyapps / binpacking2d / instance.py: 94%
334 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 04:40 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 04:40 +0000
1"""
2A Two-Dimensional Bin Packing instance.
4This module provides an instance of the two-dimensional bin packing problem
5as defined in 2DPackLib [1, 2] as well as the four non-trivial 'Almost Squares
6in Almost Squares' instances [6, 7].
8All instances of :class:`~moptipyapps.binpacking2d.instance.Instance`
9are two-dimensional numpy `ndarrays` with additional attributes.
10Each instance has a :attr:`~Instance.name`. Instances also specify a
11:attr:`~Instance.bin_width` and :attr:`~Instance.bin_height`.
12They define the number :attr:`~Instance.n_different_items` of items with
13*different* IDs. Notice that in the 2DPackLib dataset, a benchmark instance
14may contain the same item multiple times. Obviously, all items of the same
15ID have the exact same width and height, meaning that we only need to store
16them once and remember how often they occur. (Notice that the opposite is not
17true, i.e., not all items with the same width and height do have the same ID.)
18Anyway, the total number :attr:`~Instance.n_items` of items, i.e., the sum of
19all the repetitions of all items, is also stored.
21The matrix data of the instance class is laid out as follows: There is one row
22for each item. The row contains the width of the item, the height of the item,
23and the number of times the item will occur. The row at index `i` stands for
24the item with ID `i+1`.
26Instances can be loaded directly from a 2DPackLib file via
27:meth:`Instance.from_2dpacklib`. They can also be loaded from a compact string
28representation (via :meth:`Instance.from_compact_str`) and can also be
29converted to such a compact representation
30(via :meth:`Instance.to_compact_str`). This library ships with a set of
31pre-defined instances as resource which can be obtained via
32:meth:`Instance.from_resource` and listed via
33:meth:`Instance.list_resources`.
35We provide the instances of the sets `A` [3], `BENG` [4], and `CLASS` [5]
36from 2DPackLib. Additionally, we include the four non-trivial 'Almost Squares
37in Almost Squares' instances [6,7].
39Initial work on this code has been contributed by Mr. Rui ZHAO (赵睿),
40<zr1329142665@163.com> a Master's student at the Institute of Applied
41Optimization (应用优化研究所) of the School of
42Artificial Intelligence and Big Data (人工智能与大数据学院) at Hefei University
43(合肥大学) in Hefei, Anhui, China (中国安徽省合肥市) under the supervision of
44Prof. Dr. Thomas Weise (汤卫思教授).
461. Manuel Iori, Vinícius Loti de Lima, Silvano Martello, and Michele Monaci.
47 *2DPackLib*.
48 https://site.unibo.it/operations-research/en/research/2dpacklib
492. Manuel Iori, Vinícius Loti de Lima, Silvano Martello, and Michele
50 Monaci. 2DPackLib: A Two-Dimensional Cutting and Packing Library.
51 *Optimization Letters* 16(2):471-480. March 2022.
52 https://doi.org/10.1007/s11590-021-01808-y
533. Rita Macedo, Cláudio Alves, and José M. Valério de Carvalho. Arc-Flow
54 Model for the Two-Dimensional Guillotine Cutting Stock Problem.
55 *Computers & Operations Research* 37(6):991-1001. June 2010.
56 https://doi.org/10.1016/j.cor.2009.08.005.
574. Bengt-Erik Bengtsson. Packing Rectangular Pieces - A Heuristic Approach.
58 *The Computer Journal* 25(3):353-357, August 1982.
59 https://doi.org/10.1093/comjnl/25.3.353
605. J.O. Berkey and P.Y. Wang. Two Dimensional Finite Bin Packing Algorithms.
61 *Journal of the Operational Research Society* 38(5):423-429. May 1987.
62 https://doi.org/10.1057/jors.1987.70
636. Daan van den Berg, Florian Braam, Mark Moes, Emiel Suilen, and
64 Sandjai Bhulai. Almost Squares in Almost Squares: Solving the Final
65 Instance. In *The Fifth International Conference on Data Analytics
66 (DATA ANALYTICS'16)* October 9-13, 2016, Venice, Italy, pages 69-74.
67 IARIA. ISBN: 9781612085104. https://hdl.handle.net/11245/1.545914.
68 https://math.vu.nl/~sbhulai/publications/data_analytics2016b.pdf.
697. H. Simonis and B. O'Sullivan. Almost Square Packing in *8th International
70 Conference on Integration of AI and OR Techniques in Constraint Programming
71 for Combinatorial Optimization Problems* (CPAIOR'11), May 23-27, 2011,
72 Berlin, Germany, pages 196-209. Berlin, Germany: Springer-Verlag.
73 doi:10.1007/978-3-642-21311-3_19.
75>>> ins = Instance("a", 100, 50, [[10, 5, 1], [3, 3, 1], [5, 5, 1]])
76>>> ins.name
77'a'
78>>> ins.bin_width
79100
80>>> ins.bin_height
8150
82>>> ins.dtype
83dtype('int8')
84>>> ins.n_different_items
853
86>>> ins.to_compact_str()
87'a;3;100;50;10,5;3,3;5,5'
88>>> ins = Instance("b", 100, 50, np.array([[10, 5, 1], [3, 3, 1], [3, 3, 1]]))
89>>> ins.name
90'b'
91>>> ins.bin_width
92100
93>>> ins.bin_height
9450
95>>> ins.dtype
96dtype('int8')
97>>> ins.to_compact_str()
98'b;3;100;50;10,5;3,3;3,3'
99>>> ins = Instance.from_resource("cl02_020_06")
100>>> ins.dtype
101dtype('int8')
102>>> ins.n_different_items
10320
104>>> ins.n_items
10520
106>>> ins = Instance.from_resource("a25")
107>>> ins.dtype
108dtype('int16')
109>>> ins.n_different_items
11075
111>>> ins.n_items
112156
113"""
115from importlib import resources # nosem
116from os.path import basename
117from statistics import quantiles
118from typing import Final, Iterable, cast
120import moptipy.utils.nputils as npu
121import numpy as np
122from moptipy.api.component import Component
123from moptipy.utils.logger import CSV_SEPARATOR, KeyValueLogSection
124from moptipy.utils.nputils import int_range_to_dtype
125from moptipy.utils.strings import sanitize_name
126from pycommons.io.path import UTF8, Path, file_path
127from pycommons.types import check_int_range, check_to_int_range, type_error
129#: the instances resource name
130INSTANCES_RESOURCE: Final[str] = "instances.txt"
132#: the total number of items
133N_ITEMS: Final[str] = "nItems"
134#: the number of different items.
135N_DIFFERENT_ITEMS: Final[str] = "nDifferentItems"
136#: the bin width
137BIN_WIDTH: Final[str] = "binWidth"
138#: the bin height
139BIN_HEIGHT: Final[str] = "binHeight"
140#: The internal item separator
141INTERNAL_SEP: Final[str] = "," if CSV_SEPARATOR == ";" else ";"
143#: the index of the width element in an item of an instance
144IDX_WIDTH: Final[int] = 0
145#: the index of the height element in an item of an instance
146IDX_HEIGHT: Final[int] = 1
147#: the index of the repetitions element in an item of an instance
148IDX_REPETITION: Final[int] = 2
150#: the list of instance names of the 2DPackLib bin packing set downloaded
151#: from https://site.unibo.it/operations-research/en/research/2dpacklib
152#: ('a*','beng*', 'cl*') as well as the four non-trivial 'Almost Squares in
153#: Almost Squares' instances ('asqas*').
154_INSTANCES: Final[tuple[str, ...]] = (
155 "a01", "a02", "a03", "a04", "a05", "a06", "a07", "a08", "a09", "a10",
156 "a11", "a12", "a13", "a14", "a15", "a16", "a17", "a18", "a19", "a20",
157 "a21", "a22", "a23", "a24", "a25", "a26", "a27", "a28", "a29", "a30",
158 "a31", "a32", "a33", "a34", "a35", "a36", "a37", "a38", "a39", "a40",
159 "a41", "a42", "a43", "asqas03", "asqas08", "asqas20", "asqas34", "beng01",
160 "beng02", "beng03", "beng04", "beng05", "beng06", "beng07", "beng08",
161 "beng09", "beng10", "cl01_020_01", "cl01_020_02", "cl01_020_03",
162 "cl01_020_04", "cl01_020_05", "cl01_020_06", "cl01_020_07", "cl01_020_08",
163 "cl01_020_09", "cl01_020_10", "cl01_040_01", "cl01_040_02", "cl01_040_03",
164 "cl01_040_04", "cl01_040_05", "cl01_040_06", "cl01_040_07", "cl01_040_08",
165 "cl01_040_09", "cl01_040_10", "cl01_060_01", "cl01_060_02", "cl01_060_03",
166 "cl01_060_04", "cl01_060_05", "cl01_060_06", "cl01_060_07", "cl01_060_08",
167 "cl01_060_09", "cl01_060_10", "cl01_080_01", "cl01_080_02", "cl01_080_03",
168 "cl01_080_04", "cl01_080_05", "cl01_080_06", "cl01_080_07", "cl01_080_08",
169 "cl01_080_09", "cl01_080_10", "cl01_100_01", "cl01_100_02", "cl01_100_03",
170 "cl01_100_04", "cl01_100_05", "cl01_100_06", "cl01_100_07", "cl01_100_08",
171 "cl01_100_09", "cl01_100_10", "cl02_020_01", "cl02_020_02", "cl02_020_03",
172 "cl02_020_04", "cl02_020_05", "cl02_020_06", "cl02_020_07", "cl02_020_08",
173 "cl02_020_09", "cl02_020_10", "cl02_040_01", "cl02_040_02", "cl02_040_03",
174 "cl02_040_04", "cl02_040_05", "cl02_040_06", "cl02_040_07", "cl02_040_08",
175 "cl02_040_09", "cl02_040_10", "cl02_060_01", "cl02_060_02", "cl02_060_03",
176 "cl02_060_04", "cl02_060_05", "cl02_060_06", "cl02_060_07", "cl02_060_08",
177 "cl02_060_09", "cl02_060_10", "cl02_080_01", "cl02_080_02", "cl02_080_03",
178 "cl02_080_04", "cl02_080_05", "cl02_080_06", "cl02_080_07", "cl02_080_08",
179 "cl02_080_09", "cl02_080_10", "cl02_100_01", "cl02_100_02", "cl02_100_03",
180 "cl02_100_04", "cl02_100_05", "cl02_100_06", "cl02_100_07", "cl02_100_08",
181 "cl02_100_09", "cl02_100_10", "cl03_020_01", "cl03_020_02", "cl03_020_03",
182 "cl03_020_04", "cl03_020_05", "cl03_020_06", "cl03_020_07", "cl03_020_08",
183 "cl03_020_09", "cl03_020_10", "cl03_040_01", "cl03_040_02", "cl03_040_03",
184 "cl03_040_04", "cl03_040_05", "cl03_040_06", "cl03_040_07", "cl03_040_08",
185 "cl03_040_09", "cl03_040_10", "cl03_060_01", "cl03_060_02", "cl03_060_03",
186 "cl03_060_04", "cl03_060_05", "cl03_060_06", "cl03_060_07", "cl03_060_08",
187 "cl03_060_09", "cl03_060_10", "cl03_080_01", "cl03_080_02", "cl03_080_03",
188 "cl03_080_04", "cl03_080_05", "cl03_080_06", "cl03_080_07", "cl03_080_08",
189 "cl03_080_09", "cl03_080_10", "cl03_100_01", "cl03_100_02", "cl03_100_03",
190 "cl03_100_04", "cl03_100_05", "cl03_100_06", "cl03_100_07", "cl03_100_08",
191 "cl03_100_09", "cl03_100_10", "cl04_020_01", "cl04_020_02", "cl04_020_03",
192 "cl04_020_04", "cl04_020_05", "cl04_020_06", "cl04_020_07", "cl04_020_08",
193 "cl04_020_09", "cl04_020_10", "cl04_040_01", "cl04_040_02", "cl04_040_03",
194 "cl04_040_04", "cl04_040_05", "cl04_040_06", "cl04_040_07", "cl04_040_08",
195 "cl04_040_09", "cl04_040_10", "cl04_060_01", "cl04_060_02", "cl04_060_03",
196 "cl04_060_04", "cl04_060_05", "cl04_060_06", "cl04_060_07", "cl04_060_08",
197 "cl04_060_09", "cl04_060_10", "cl04_080_01", "cl04_080_02", "cl04_080_03",
198 "cl04_080_04", "cl04_080_05", "cl04_080_06", "cl04_080_07", "cl04_080_08",
199 "cl04_080_09", "cl04_080_10", "cl04_100_01", "cl04_100_02", "cl04_100_03",
200 "cl04_100_04", "cl04_100_05", "cl04_100_06", "cl04_100_07", "cl04_100_08",
201 "cl04_100_09", "cl04_100_10", "cl05_020_01", "cl05_020_02", "cl05_020_03",
202 "cl05_020_04", "cl05_020_05", "cl05_020_06", "cl05_020_07", "cl05_020_08",
203 "cl05_020_09", "cl05_020_10", "cl05_040_01", "cl05_040_02", "cl05_040_03",
204 "cl05_040_04", "cl05_040_05", "cl05_040_06", "cl05_040_07", "cl05_040_08",
205 "cl05_040_09", "cl05_040_10", "cl05_060_01", "cl05_060_02", "cl05_060_03",
206 "cl05_060_04", "cl05_060_05", "cl05_060_06", "cl05_060_07", "cl05_060_08",
207 "cl05_060_09", "cl05_060_10", "cl05_080_01", "cl05_080_02", "cl05_080_03",
208 "cl05_080_04", "cl05_080_05", "cl05_080_06", "cl05_080_07", "cl05_080_08",
209 "cl05_080_09", "cl05_080_10", "cl05_100_01", "cl05_100_02", "cl05_100_03",
210 "cl05_100_04", "cl05_100_05", "cl05_100_06", "cl05_100_07", "cl05_100_08",
211 "cl05_100_09", "cl05_100_10", "cl06_020_01", "cl06_020_02", "cl06_020_03",
212 "cl06_020_04", "cl06_020_05", "cl06_020_06", "cl06_020_07", "cl06_020_08",
213 "cl06_020_09", "cl06_020_10", "cl06_040_01", "cl06_040_02", "cl06_040_03",
214 "cl06_040_04", "cl06_040_05", "cl06_040_06", "cl06_040_07", "cl06_040_08",
215 "cl06_040_09", "cl06_040_10", "cl06_060_01", "cl06_060_02", "cl06_060_03",
216 "cl06_060_04", "cl06_060_05", "cl06_060_06", "cl06_060_07", "cl06_060_08",
217 "cl06_060_09", "cl06_060_10", "cl06_080_01", "cl06_080_02", "cl06_080_03",
218 "cl06_080_04", "cl06_080_05", "cl06_080_06", "cl06_080_07", "cl06_080_08",
219 "cl06_080_09", "cl06_080_10", "cl06_100_01", "cl06_100_02", "cl06_100_03",
220 "cl06_100_04", "cl06_100_05", "cl06_100_06", "cl06_100_07", "cl06_100_08",
221 "cl06_100_09", "cl06_100_10", "cl07_020_01", "cl07_020_02", "cl07_020_03",
222 "cl07_020_04", "cl07_020_05", "cl07_020_06", "cl07_020_07", "cl07_020_08",
223 "cl07_020_09", "cl07_020_10", "cl07_040_01", "cl07_040_02", "cl07_040_03",
224 "cl07_040_04", "cl07_040_05", "cl07_040_06", "cl07_040_07", "cl07_040_08",
225 "cl07_040_09", "cl07_040_10", "cl07_060_01", "cl07_060_02", "cl07_060_03",
226 "cl07_060_04", "cl07_060_05", "cl07_060_06", "cl07_060_07", "cl07_060_08",
227 "cl07_060_09", "cl07_060_10", "cl07_080_01", "cl07_080_02", "cl07_080_03",
228 "cl07_080_04", "cl07_080_05", "cl07_080_06", "cl07_080_07", "cl07_080_08",
229 "cl07_080_09", "cl07_080_10", "cl07_100_01", "cl07_100_02", "cl07_100_03",
230 "cl07_100_04", "cl07_100_05", "cl07_100_06", "cl07_100_07", "cl07_100_08",
231 "cl07_100_09", "cl07_100_10", "cl08_020_01", "cl08_020_02", "cl08_020_03",
232 "cl08_020_04", "cl08_020_05", "cl08_020_06", "cl08_020_07", "cl08_020_08",
233 "cl08_020_09", "cl08_020_10", "cl08_040_01", "cl08_040_02", "cl08_040_03",
234 "cl08_040_04", "cl08_040_05", "cl08_040_06", "cl08_040_07", "cl08_040_08",
235 "cl08_040_09", "cl08_040_10", "cl08_060_01", "cl08_060_02", "cl08_060_03",
236 "cl08_060_04", "cl08_060_05", "cl08_060_06", "cl08_060_07", "cl08_060_08",
237 "cl08_060_09", "cl08_060_10", "cl08_080_01", "cl08_080_02", "cl08_080_03",
238 "cl08_080_04", "cl08_080_05", "cl08_080_06", "cl08_080_07", "cl08_080_08",
239 "cl08_080_09", "cl08_080_10", "cl08_100_01", "cl08_100_02", "cl08_100_03",
240 "cl08_100_04", "cl08_100_05", "cl08_100_06", "cl08_100_07", "cl08_100_08",
241 "cl08_100_09", "cl08_100_10", "cl09_020_01", "cl09_020_02", "cl09_020_03",
242 "cl09_020_04", "cl09_020_05", "cl09_020_06", "cl09_020_07", "cl09_020_08",
243 "cl09_020_09", "cl09_020_10", "cl09_040_01", "cl09_040_02", "cl09_040_03",
244 "cl09_040_04", "cl09_040_05", "cl09_040_06", "cl09_040_07", "cl09_040_08",
245 "cl09_040_09", "cl09_040_10", "cl09_060_01", "cl09_060_02", "cl09_060_03",
246 "cl09_060_04", "cl09_060_05", "cl09_060_06", "cl09_060_07", "cl09_060_08",
247 "cl09_060_09", "cl09_060_10", "cl09_080_01", "cl09_080_02", "cl09_080_03",
248 "cl09_080_04", "cl09_080_05", "cl09_080_06", "cl09_080_07", "cl09_080_08",
249 "cl09_080_09", "cl09_080_10", "cl09_100_01", "cl09_100_02", "cl09_100_03",
250 "cl09_100_04", "cl09_100_05", "cl09_100_06", "cl09_100_07", "cl09_100_08",
251 "cl09_100_09", "cl09_100_10", "cl10_020_01", "cl10_020_02", "cl10_020_03",
252 "cl10_020_04", "cl10_020_05", "cl10_020_06", "cl10_020_07", "cl10_020_08",
253 "cl10_020_09", "cl10_020_10", "cl10_040_01", "cl10_040_02", "cl10_040_03",
254 "cl10_040_04", "cl10_040_05", "cl10_040_06", "cl10_040_07", "cl10_040_08",
255 "cl10_040_09", "cl10_040_10", "cl10_060_01", "cl10_060_02", "cl10_060_03",
256 "cl10_060_04", "cl10_060_05", "cl10_060_06", "cl10_060_07", "cl10_060_08",
257 "cl10_060_09", "cl10_060_10", "cl10_080_01", "cl10_080_02", "cl10_080_03",
258 "cl10_080_04", "cl10_080_05", "cl10_080_06", "cl10_080_07", "cl10_080_08",
259 "cl10_080_09", "cl10_080_10", "cl10_100_01", "cl10_100_02", "cl10_100_03",
260 "cl10_100_04", "cl10_100_05", "cl10_100_06", "cl10_100_07", "cl10_100_08",
261 "cl10_100_09", "cl10_100_10")
264def __divide_based_on_size(instances: Iterable[str],
265 divisions: int) -> list[list[str]]:
266 """
267 Divide the instances based on their size.
269 :param instances: the instances
270 :param divisions: the number of divisions
271 :return: the divided instances
272 """
273 insts: list[str] = list(instances)
274 if len(insts) <= 0:
275 return [insts]
277 try:
278 loaded: list[Instance] = [Instance.from_resource(n) for n in insts]
279 except (OSError, ValueError):
280 return [insts]
282 # get the quantiles = group divisions
283 qants: list[float | int] = quantiles((
284 inst.n_items for inst in loaded), n=divisions, method="inclusive")
286 # now put the instances into the groups
287 groups: list[list[str]] = []
288 for inst in loaded:
289 inst_size = inst.n_items
290 idx: int = 0
291 for q in qants:
292 if q > inst_size:
293 break
294 idx += 1
295 while idx >= len(groups):
296 groups.append([])
297 groups[idx].append(str(inst))
299 # remove useless groups
300 for idx in range(len(groups) - 1, -1, -1):
301 if len(groups[idx]) <= 0:
302 del groups[idx]
303 return groups
306def _make_instance_groups(instances: Iterable[str]) \
307 -> "tuple[tuple[str, str | None, tuple[str, ...]], ...]":
308 """
309 Make the standard instance groups from an instance name list.
311 :return: the instance groups
312 """
313 groups: list[tuple[str, str | None, tuple[str, ...]]] = []
315 a_divided: list[list[str]] = __divide_based_on_size(sorted(
316 b for b in instances if b.startswith("a")
317 and (len(b) == 3) and b[-1].isdigit()
318 and b[-2].isdigit()), 3)
319 if len(a_divided) > 0:
320 subnames: list[str | None] = [None] if (len(a_divided) <= 1) else (
321 ["small", "large"] if (len(a_divided) <= 2) else
322 ["small", "med", "large"])
323 for a_idx, a_group in enumerate(a_divided):
324 v: tuple[str, str | None, tuple[str, ...]] = (
325 "a", subnames[a_idx], tuple(a_group))
326 if len(v[2]) > 0:
327 groups.append(v)
329 v = ("beng", "1-8", tuple(sorted(
330 b for b in instances if b.startswith("beng")
331 and (int(b[-2:]) < 9))))
332 if len(v[2]) > 0:
333 groups.append(v)
335 v = ("beng", "9-10", tuple(sorted(
336 b for b in instances if b.startswith("beng")
337 and (int(b[-2:]) >= 9))))
338 if len(v[2]) > 0:
339 groups.append(v)
341 for i in range(1, 11):
342 name: str = f"class {i}"
343 preprefix: str = f"cl0{i}" if i < 10 else f"cl{i}"
344 for n in (20, 40, 60, 80, 100):
345 prefix: str = f"{preprefix}_0{n}_" \
346 if n < 100 else f"{preprefix}_{n}_"
347 v = (name, str(n), tuple(sorted(
348 b for b in _INSTANCES if b.startswith(prefix))))
349 if len(v[2]) > 0:
350 groups.append(v)
352 v = ("asqas", None, tuple(sorted(
353 b for b in _INSTANCES if b.startswith("asqas"))))
354 if len(v[2]) > 0:
355 groups.append(v)
357 all_set: set[str] = set()
358 for g in groups:
359 all_set.update(g[2])
360 inst_set: set[str] = set(instances)
361 if all_set != inst_set:
362 raise ValueError(f"group instances is {all_set!r} but "
363 f"instance set is {inst_set!r}!")
364 return tuple(groups)
367def __cutsq(matrix: np.ndarray) -> list[int]:
368 """
369 Cut all items into squares via the CUTSQ procedure.
371 :param matrix: the item matrix
372 :return: the list of squares
374 >>> __cutsq(np.array([[14, 12, 1]], int))
375 [12, 2, 2, 2, 2, 2, 2]
377 >>> __cutsq(np.array([[14, 12, 2]], int))
378 [12, 12, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
379 """
380 # create list of items in horizontal orientation
381 j_sq: Final[list[int]] = [] # the list of squares (width = height)
382 s: Final[list[int]] = [] # a temporary list
383 for row in matrix:
384 w: int = int(row[0])
385 h: int = int(row[1])
386 if h > w:
387 w, h = h, w
388 while h > 1:
389 k: int = w // h
390 for _ in range(k):
391 s.append(h)
392 w, h = h, w - (k * h)
393 times: int = int(row[2])
394 j_sq.extend(s * times if times > 1 else s)
395 s.clear()
397 j_sq.sort(reverse=True) # sort the squares in decreasing size
398 return j_sq
401def __lb_q(bin_width: int, bin_height: int, q: int, j_js: list[int]) -> int:
402 """
403 Compute the lower bound for a given q.
405 :param bin_width: the bin width
406 :param bin_height: the bin height
407 :param q: the parameter q
408 :param j_js: the sorted square list
409 :return: the lower bound
411 >>> jj = [18, 18, 12, 12, 11, 11, 11, 11, 11, 7, 7, 7, 7, 7, 7]
412 >>> len(jj)
413 15
414 >>> __lb_q(23, 20, 6, jj)
415 6
416 """
417 m: Final[int] = len(j_js)
418 half_width: Final[float] = bin_width / 2
419 half_height: Final[float] = bin_height / 2
420 width_m_q: Final[int] = bin_width - q
422 # First we compute sets S1 to S4.
423 s1: list[int] = [] # S1 from Equation 2
424 s2: list[int] = [] # S2 from Equation 3
425 s3: list[int] = [] # S2 from Equation 4
426 s4: list[int] = [] # S2 from Equation 5
428 for i in range(m):
429 l_i: int = j_js[i]
430 if l_i > width_m_q:
431 s1.append(i) # Equation 2
432 elif l_i > half_width:
433 s2.append(i) # Equation 3
434 elif l_i > half_height:
435 s3.append(i) # Equation 4
436 elif l_i >= q:
437 s4.append(i) # Equation 5
438 else:
439 break
441 # compute set S23 as in Theorem 3 under Equation 7
442 height_m_q: Final[int] = bin_height - q
443 s23: Final[list[int]] = [j for j in (s2 + s3) if j_js[j] > height_m_q]
445 # Now we sort S2 by non-increasing value of residual space.
446 s2.reverse() # = .sort(key=lambda i: bin_width - j_js[i], reverse=True)
448 # Now we compute S3 - ^S3^
449 s3_minus_s3d: list[int] = s3.copy()
450 for i in s2:
451 residual: int = bin_width - j_js[i]
452 not_found: bool = True
453 for j, idx in enumerate(s3_minus_s3d):
454 needs: int = j_js[idx]
455 if needs <= residual:
456 del s3_minus_s3d[j]
457 not_found = False
458 break
459 if not_found:
460 break
462 sum_s3_l: int = sum(j_js[i] for i in s3_minus_s3d)
463 b1 = sum_s3_l // bin_width
464 if (b1 * bin_width) < sum_s3_l:
465 b1 += 1
467 len_s3: int = len(s3_minus_s3d)
468 div: int = bin_width // ((bin_height // 2) + 1)
469 b2 = len_s3 // div
470 if (b2 * div) < len_s3:
471 b2 += 1
473 l_tilde: Final[int] = len(s2) + max(b1, b2) # Equation 6.
474 bound: int = len(s1) + l_tilde
476 # Now compute the final bound based on Theorem 3 / Equation 7.
477 bin_size: Final[int] = bin_width * bin_height
478 denom: int = sum(j_js[i] ** 2 for i in (s2 + s3 + s4)) \
479 - ((bin_size * l_tilde) - sum(j_js[i] * (
480 bin_height - j_js[i]) for i in s23))
481 if denom > 0:
482 b = denom // bin_size
483 if (b * bin_size) < denom:
484 b += 1
485 bound += b
487 return bound
490def _lower_bound_damv(bin_width: int, bin_height: int,
491 matrix: np.ndarray) -> int:
492 """
493 Compute the lower bound as defined by Dell'Amico et al.
495 :param bin_width: the bin width
496 :param bin_height: the bin height
497 :param matrix: the item matrix
498 :return: the lower bound
500 >>> mat = np.array([[10, 5, 1], [3, 3, 1], [3, 3, 1]])
501 >>> _lower_bound_damv(23, 20, mat)
502 1
504 >>> mat = np.array([[20, 5, 3], [13, 23, 1], [13, 9, 3]])
505 >>> _lower_bound_damv(23, 20, mat)
506 3
507 """
508 # ensure horizontal orientation (width >= height)
509 if bin_height > bin_width:
510 bin_width, bin_height = bin_height, bin_width
511 j_sq: Final[list[int]] = __cutsq(matrix)
512 res: Final[int] = max(__lb_q(bin_width, bin_height, q, j_sq)
513 for q in range((bin_height // 2) + 1))
514 return max(1, res)
517class Instance(Component, np.ndarray):
518 """
519 An instance of the 2D Bin Packing Problem.
521 Each row of the matrix contains three values: 1. the item's width, 2. the
522 item's height, 3. how often the item occurs.
523 """
525 #: the name of the instance
526 name: str
527 #: the total number of items (including repetitions)
528 n_items: int
529 #: the number of different items
530 n_different_items: int
531 #: the total area occupied by all items
532 total_item_area: int
533 #: the bin width
534 bin_width: int
535 #: the bin height
536 bin_height: int
537 #: the minimum number of bins that this instance requires
538 lower_bound_bins: int
540 def __new__(cls, name: str,
541 bin_width: int, bin_height: int,
542 matrix: np.ndarray | list[list[int]]) -> "Instance":
543 """
544 Create an instance of the 2D bin packing problem.
546 :param cls: the class
547 :param name: the name of the instance
548 :param bin_width: the bin width
549 :param bin_height: the bin height
550 :param matrix: the matrix with the data (will be copied); rows have
551 the form `(width, height, repetitions)`
552 """
553 use_name: Final[str] = sanitize_name(name)
554 if name != use_name:
555 raise ValueError(f"Name {name!r} is not a valid name.")
557 check_int_range(bin_width, "bin_width", 1, 1_000_000_000_000)
558 check_int_range(bin_height, "bin_height", 1, 1_000_000_000_000)
559 max_dim: Final[int] = max(bin_width, bin_height)
560 min_dim: Final[int] = min(bin_width, bin_height)
562 n_different_items: Final[int] = check_int_range(
563 len(matrix), "n_different_items", 1, 100_000_000)
564 use_shape: Final[tuple[int, int]] = (n_different_items, 3)
566 if isinstance(matrix, np.ndarray):
567 if not npu.is_np_int(matrix.dtype):
568 raise ValueError(
569 "Matrix must have an integer type, but is of type "
570 f"{str(matrix.dtype)!r} in instance {name!r}.")
571 if matrix.shape != use_shape:
572 raise ValueError(
573 f"Invalid shape {str(matrix.shape)!r} of matrix: "
574 "must have three columns and two dimensions, must be "
575 f"equal to {use_shape} in instance {name!r}.")
576 elif not isinstance(matrix, list):
577 raise type_error(matrix, "matrix", np.ndarray)
579 n_items: int = 0
580 max_size: int = -1
581 item_area: int = 0
582 for i in range(n_different_items):
583 row = matrix[i]
584 if not isinstance(row, list | np.ndarray):
585 raise type_error(
586 row, f"{row} at index {i} in {use_name!r}",
587 (list, np.ndarray))
588 if len(row) != 3:
589 raise ValueError(
590 f"invalid row {row} at index {i} in {use_name!r}.")
591 width, height, repetitions = row
592 width = check_int_range(int(width), "width", 1, max_dim)
593 height = check_int_range(int(height), "height", 1, max_dim)
594 repetitions = check_int_range(int(repetitions), "repetitions",
595 1, 100_000_000)
596 item_area += (width * height * repetitions)
597 max_size = max(max_size, width, height)
598 if (width > min_dim) and (height > min_dim):
599 raise ValueError(
600 f"object with width={width} and height={height} does "
601 f"not fit into bin with width={width} and "
602 f"height={height}.")
603 n_items += repetitions
605 obj: Final[Instance] = super().__new__(
606 cls, use_shape, int_range_to_dtype(
607 min_value=0, max_value=max(
608 max_dim + max_size + 1, n_items + 1), force_signed=True))
609 for i in range(n_different_items):
610 obj[i, :] = matrix[i]
612 #: the name of the instance
613 obj.name = use_name
614 #: the number of different items
615 obj.n_different_items = n_different_items
616 #: the total number of items, i.e., the number of different items
617 #: multiplied with their repetition counts
618 obj.n_items = check_int_range(
619 n_items, "n_items", n_different_items, 1_000_000_000_000)
620 #: the height of the bins
621 obj.bin_height = bin_height
622 #: the width of the bins
623 obj.bin_width = bin_width
624 #: the total area occupied by all items
625 obj.total_item_area = item_area
627# We need at least as many bins such that their area is big enough
628# for the total area of the items.
629 bin_area: int = bin_height * bin_width
630 lower_bound_geo: int = item_area // bin_area
631 if (lower_bound_geo * bin_area) < item_area:
632 lower_bound_geo += 1
633 lower_bound_geo = check_int_range(
634 lower_bound_geo, "lower_bound_bins_geometric",
635 1, 1_000_000_000_000)
637# We now compute the lower bound by Dell'Amico et al.
638 lower_bound_damv = check_int_range(_lower_bound_damv(
639 bin_width, bin_height, obj), "lower_bound_bins_damv",
640 1, 1_000_000_000_000)
642# The overall computed lower bound is the maximum of the geometric and the
643# Dell'Amico lower bound.
644 obj.lower_bound_bins = max(lower_bound_damv, lower_bound_geo)
645 return obj
647 def __str__(self):
648 """
649 Get the name of this instance.
651 :return: the name of this instance
652 """
653 return self.name
655 def get_standard_item_sequence(self) -> list[int]:
656 """
657 Get the standardized item sequence.
659 :return: the standardized item sequence
661 >>> ins = Instance("a", 100, 50, [[10, 5, 1], [3, 3, 1], [5, 5, 1]])
662 >>> ins.get_standard_item_sequence()
663 [1, 2, 3]
664 >>> ins = Instance("a", 100, 50, [[10, 5, 1], [3, 3, 1], [5, 5, 2]])
665 >>> ins.get_standard_item_sequence()
666 [1, 2, 3, 3]
667 >>> ins = Instance("a", 100, 50, [[10, 5, 2], [3, 3, 3], [5, 5, 4]])
668 >>> ins.get_standard_item_sequence()
669 [1, 1, 2, 2, 2, 3, 3, 3, 3]
670 """
671 base_string: Final[list[int]] = []
672 for i in range(1, self.n_different_items + 1):
673 for _ in range(self[i - 1, IDX_REPETITION]):
674 base_string.append(int(i))
675 return base_string
677 @staticmethod
678 def from_2dpacklib(file: str) -> "Instance":
679 """
680 Load a problem instance from the 2dpacklib format.
682 :param file: the file path
683 :return: the instance
684 """
685 path: Final[Path] = file_path(file)
686 name: str = basename(path).lower()
687 if name.endswith(".ins2d"):
688 name = sanitize_name(name[:-6])
690 lines: Final[list[str]] = str.splitlines(path.read_all_str())
691 n_different_items: Final[int] = check_to_int_range(
692 lines[0], "n_different_items", 1, 100_000_000)
694 wh: Final[str] = lines[1]
695 spaceidx: Final[int] = wh.index(" ")
696 if spaceidx <= 0:
697 raise ValueError("Did not find space in second line.")
698 bin_width: Final[int] = check_to_int_range(
699 wh[:spaceidx], "bin_width", 1, 1_000_000_000_000)
700 bin_height: Final[int] = check_to_int_range(
701 wh[spaceidx + 1:], "bin_height", 1, 1_000_000_000_000)
702 del lines[0:2]
704 max_dim: Final[int] = max(bin_width, bin_height)
705 data: Final[list[list[int]]] = []
706 old_item_id: int = 0
707 for line in lines:
708 text: list[str] = line.split(" ")
709 itemid: int = check_to_int_range(
710 text[0], "item-id", 1, n_different_items)
711 if itemid != (old_item_id + 1):
712 raise ValueError(
713 f"non-sequential item id {itemid} after {old_item_id}!")
714 old_item_id = itemid
715 width: int = check_to_int_range(text[1], "width", 1, max_dim)
716 height: int = check_to_int_range(text[2], "height", 1, max_dim)
717 count: int = 1 if len(text) <= 3 else \
718 check_to_int_range(text[3], "count", 1, 1_000_000)
719 data.append([width, height, count])
720 data.sort()
721 return Instance(name, bin_width, bin_height, data)
723 def to_compact_str(self) -> str:
724 """
725 Convert the instance to a compact string.
727 The format of the string is a single line of semi-colon separated
728 values. The values are: `name;n_items;bin_width;bin_height`,
729 followed by the sequence of items, each in the form of
730 `;width,heigh[,times]`, where `times` is optional and only added
731 if the item occurs more than once.
733 :return: a single line string with all the instance data
735 >>> ins = Instance("x", 500, 50, [[3, 5, 1], [2, 5, 2]])
736 >>> ins.to_compact_str()
737 'x;2;500;50;3,5;2,5,2'
738 >>> ins.n_different_items
739 2
740 """
741 lst: Final[list[str]] = [self.name, str(self.n_different_items),
742 str(self.bin_width), str(self.bin_height)]
743 for i in range(self.n_different_items):
744 width: int = self[i, IDX_WIDTH]
745 height: int = self[i, IDX_HEIGHT]
746 repetitions: int = self[i, IDX_REPETITION]
747 lst.append(
748 f"{width}{INTERNAL_SEP}{height}" if repetitions == 1 else
749 f"{width}{INTERNAL_SEP}{height}{INTERNAL_SEP}{repetitions}")
750 return CSV_SEPARATOR.join(lst)
752 @staticmethod
753 def from_compact_str(data: str) -> "Instance":
754 """
755 Transform a compact string back to an instance.
757 :param data: the string data
758 :return: the instance
760 >>> ins = Instance("x", 500, 50, [[3, 5, 1], [2, 5, 2]])
761 >>> Instance.from_compact_str(ins.to_compact_str()).to_compact_str()
762 'x;2;500;50;3,5;2,5,2'
763 """
764 if not isinstance(data, str):
765 raise type_error(data, "data", str)
766 text: Final[list[str]] = data.split(CSV_SEPARATOR)
767 name: Final[str] = text[0]
768 n_different_items: Final[int] = check_to_int_range(
769 text[1], "n_different_items", 1, 100_000_000)
770 bin_width: Final[int] = check_to_int_range(
771 text[2], "bin_width", 1, 1_000_000_000_000)
772 bin_height: Final[int] = check_to_int_range(
773 text[3], "bin_height", 1, 1_000_000_000_000)
774 max_dim: Final[int] = max(bin_width, bin_height)
775 items: list[list[int]] = []
776 for i in range(4, n_different_items + 4):
777 s: list[str] = text[i].split(INTERNAL_SEP)
778 row: list[int] = [
779 check_to_int_range(s[IDX_WIDTH], "width", 1, max_dim),
780 check_to_int_range(s[IDX_HEIGHT], "height", 1, max_dim),
781 1 if len(s) <= IDX_REPETITION else
782 check_to_int_range(
783 s[IDX_REPETITION], "times", 1, 100_000_000)]
784 items.append(row)
785 return Instance(name, bin_width, bin_height, items)
787 def log_parameters_to(self, logger: KeyValueLogSection) -> None:
788 """
789 Log the parameters describing this bin packing instance to the logger.
791 :param logger: the logger for the parameters
793 >>> from moptipy.utils.logger import InMemoryLogger
794 >>> with InMemoryLogger() as l:
795 ... with l.key_values("I") as kv:
796 ... Instance.from_resource("beng05").log_parameters_to(kv)
797 ... print(repr('@'.join(l.get_log())))
798 'BEGIN_I@name: beng05@class: moptipyapps.binpacking2d\
799.instance.Instance@nItems: 100@nDifferentItems: 100@binWidth: 25\
800@binHeight: 10@dtype: b@END_I'
801 """
802 super().log_parameters_to(logger)
803 logger.key_value(N_ITEMS, self.n_items)
804 logger.key_value(N_DIFFERENT_ITEMS, self.n_different_items)
805 logger.key_value(BIN_WIDTH, self.bin_width)
806 logger.key_value(BIN_HEIGHT, self.bin_height)
807 logger.key_value(npu.KEY_NUMPY_TYPE, self.dtype.char)
809 @staticmethod
810 def list_resources() -> tuple[str, ...]:
811 """
812 Get a tuple of all the instances available as resource.
814 :return: the tuple with the instance names
816 >>> len(list(Instance.list_resources()))
817 557
818 """
819 return _INSTANCES
821 @staticmethod
822 def list_resources_groups() -> tuple[tuple[
823 str, str | None, tuple[str, ...]], ...]:
824 """
825 List the instance groups in the resources.
827 One problem of the benchmark set for 2-dimensional bin packing is that
828 it has many instances:
830 >>> len(Instance.list_resources())
831 557
833 With this function, we can group several of these instances together
834 in a way that is compliant with literature in order to then compute
835 statistics over these groups. Presenting data gathered over...
837 >>> len(Instance.list_resources_groups())
838 56
840 ...groups is much easier than dealing with over 500 instances.
842 :return: the instance groups, in a two level hierarchy. The result is
843 a sequence of tuples. Each tuple has the top-level group name and,
844 optionally, a second-level group name (or `None` if no second
845 level group exists). The third tuple element is a sequence of
846 instance names.
848 >>> [(v[0], v[1], len(v[2])) for v in
849 ... Instance.list_resources_groups()]
850 [('a', 'small', 14), ('a', 'med', 14), ('a', 'large', 15), \
851('beng', '1-8', 8), ('beng', '9-10', 2), ('class 1', '20', 10), \
852('class 1', '40', 10), ('class 1', '60', 10), ('class 1', '80', 10), \
853('class 1', '100', 10), ('class 2', '20', 10), ('class 2', '40', 10), \
854('class 2', '60', 10), ('class 2', '80', 10), ('class 2', '100', 10), \
855('class 3', '20', 10), ('class 3', '40', 10), ('class 3', '60', 10), \
856('class 3', '80', 10), ('class 3', '100', 10), ('class 4', '20', 10), \
857('class 4', '40', 10), ('class 4', '60', 10), ('class 4', '80', 10), \
858('class 4', '100', 10), ('class 5', '20', 10), ('class 5', '40', 10), \
859('class 5', '60', 10), ('class 5', '80', 10), ('class 5', '100', 10), \
860('class 6', '20', 10), ('class 6', '40', 10), ('class 6', '60', 10), \
861('class 6', '80', 10), ('class 6', '100', 10), ('class 7', '20', 10), \
862('class 7', '40', 10), ('class 7', '60', 10), ('class 7', '80', 10), \
863('class 7', '100', 10), ('class 8', '20', 10), ('class 8', '40', 10), \
864('class 8', '60', 10), ('class 8', '80', 10), ('class 8', '100', 10), \
865('class 9', '20', 10), ('class 9', '40', 10), ('class 9', '60', 10), \
866('class 9', '80', 10), ('class 9', '100', 10), ('class 10', '20', 10), \
867('class 10', '40', 10), ('class 10', '60', 10), ('class 10', '80', 10), \
868('class 10', '100', 10), ('asqas', None, 4)]
869 """
870 obj: object = Instance.list_resources_groups
871 attr: str = "__gs"
872 if not hasattr(obj, attr):
873 gs: tuple[tuple[str, str | None, tuple[str, ...]], ...] =\
874 _make_instance_groups(Instance.list_resources())
875 setattr(obj, attr, gs)
876 return gs
877 return cast("tuple[tuple[str, str | None, tuple[str, ...]], ...]",
878 getattr(obj, attr))
880 @staticmethod
881 def from_resource(name: str) -> "Instance":
882 """
883 Load an instance from a resource.
885 :param name: the name string
886 :return: the instance
888 >>> Instance.from_resource("a01").to_compact_str()
889 'a01;2;2750;1220;463,386,18;1680,420,6'
890 >>> Instance.from_resource("a07").to_compact_str()
891 'a07;5;2750;1220;706,286,8;706,516,16;986,433,10;1120,486,\
89212;1120,986,12'
893 >>> Instance.from_resource("a08") is Instance.from_resource("a08")
894 True
895 >>> Instance.from_resource("a08") is Instance.from_resource("a09")
896 False
897 """
898 if not isinstance(name, str):
899 raise type_error(name, "name", str)
900 container: Final = Instance.from_resource
901 inst_attr: Final[str] = f"__inst_{name}"
902 if hasattr(container, inst_attr): # instance loaded?
903 return cast("Instance", getattr(container, inst_attr))
904 text_attr: Final[str] = f"__text_{INSTANCES_RESOURCE}"
905 text: list[str]
906 total_attr: Final[str] = "__total_insts"
907 if hasattr(container, text_attr): # ok, we got the text already
908 text = cast("list[str]", getattr(container, text_attr))
909 else: # the first time we load the text
910 with resources.files(__package__).joinpath(
911 INSTANCES_RESOURCE).open("r", encoding=UTF8) as stream:
912 text = [line for line in (line_raw.strip() for line_raw
913 in stream.readlines())
914 if len(line) > 0]
915 setattr(container, text_attr, text)
916 setattr(container, total_attr, 0) # so far, no instance
918 imax: int = len(text)
919 imin: int = 0
920 while imin <= imax: # binary search for instance
921 imid: int = (imax + imin) // 2
922 line: str = text[imid]
923 idx: int = line.index(CSV_SEPARATOR)
924 prefix: str = line[0:idx]
925 if prefix == name:
926 instance = Instance.from_compact_str(line)
927 setattr(container, inst_attr, instance)
928 got: int = getattr(container, total_attr)
929 got += 1
930 if got >= len(_INSTANCES): # got all instances, can del text
931 delattr(container, total_attr)
932 delattr(container, text_attr)
933 return instance
934 if prefix < name:
935 imin = imid + 1
936 else:
937 imax = imid - 1
938 raise ValueError(f"instance {name!r} not found.")