Skip to content

Commit

Permalink
added method to convert objective values to bin counts and other stuf…
Browse files Browse the repository at this point in the history
…f to 2d-bin packing
  • Loading branch information
thomasWeise committed Jan 4, 2024
1 parent 391a504 commit ef29906
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 69 deletions.
167 changes: 165 additions & 2 deletions moptipyapps/binpacking2d/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@

from importlib import resources # nosem
from os.path import basename
from typing import Final, cast
from statistics import quantiles
from typing import Final, Iterable, cast

import moptipy.utils.nputils as npu
import numpy as np
Expand Down Expand Up @@ -146,7 +147,7 @@
#: the index of the repetitions element in an item of an instance
IDX_REPETITION: Final[int] = 2

#: the the list of instance names of the 2DPackLib bin packing set downloaded
#: the list of instance names of the 2DPackLib bin packing set downloaded
#: from https://site.unibo.it/operations-research/en/research/2dpacklib
#: ('a*','beng*', 'cl*') as well as the four non-trivial 'Almost Squares in
#: Almost Squares' instances ('asqas*').
Expand Down Expand Up @@ -260,6 +261,109 @@
"cl10_100_09", "cl10_100_10")


def __divide_based_on_size(instances: Iterable[str],
divisions: int) -> list[list[str]]:
"""
Divide the instances based on their size.
:param instances: the instances
:param divisions: the number of divisions
:return: the divided instances
"""
insts: list[str] = list(instances)
if len(insts) <= 0:
return [insts]

try:
loaded: list[Instance] = [Instance.from_resource(n) for n in insts]
except (OSError, ValueError):
return [insts]

# get the quantiles = group divisions
qants: list[float | int] = quantiles((
inst.n_items for inst in loaded), n=divisions, method="inclusive")

# now put the instances into the groups
groups: list[list[str]] = []
for inst in loaded:
inst_size = inst.n_items
idx: int = 0
for q in qants:
if q > inst_size:
break
idx += 1
while idx >= len(groups):
groups.append([])
groups[idx].append(str(inst))

# remove useless groups
for idx in range(len(groups) - 1, -1, -1):
if len(groups[idx]) <= 0:
del groups[idx]
return groups


def _make_instance_groups(instances: Iterable[str]) \
-> tuple[tuple[str, str | None, tuple[str, ...]], ...]:
"""
Make the standard instance groups from an instance name list.
:return: the instance groups
"""
groups: list[tuple[str, str | None, tuple[str, ...]]] = []

a_divided: list[list[str]] = __divide_based_on_size(sorted(
b for b in instances if b.startswith("a")
and (len(b) == 3) and b[-1].isdigit()
and b[-2].isdigit()), 3)
if len(a_divided) > 0:
subnames: list[str | None] = [None] if (len(a_divided) <= 1) else (
["small", "large"] if (len(a_divided) <= 2) else
["small", "med", "large"])
for a_idx, a_group in enumerate(a_divided):
v: tuple[str, str | None, tuple[str, ...]] = (
"a", subnames[a_idx], tuple(a_group))
if len(v[2]) > 0:
groups.append(v)

v = ("beng", "1-8", tuple(sorted(
b for b in instances if b.startswith("beng")
and (int(b[-2:]) < 9))))
if len(v[2]) > 0:
groups.append(v)

v = ("beng", "9-10", tuple(sorted(
b for b in instances if b.startswith("beng")
and (int(b[-2:]) >= 9))))
if len(v[2]) > 0:
groups.append(v)

for i in range(1, 11):
name: str = f"class {i}"
preprefix: str = f"cl0{i}" if i < 10 else f"cl{i}"
for n in (20, 40, 60, 80, 100):
prefix: str = f"{preprefix}_0{n}_" \
if n < 100 else f"{preprefix}_{n}_"
v = (name, str(n), tuple(sorted(
b for b in _INSTANCES if b.startswith(prefix))))
if len(v[2]) > 0:
groups.append(v)

v = ("asqas", None, tuple(sorted(
b for b in _INSTANCES if b.startswith("asqas"))))
if len(v[2]) > 0:
groups.append(v)

all_set: set[str] = set()
for g in groups:
all_set.update(g[2])
inst_set: set[str] = set(instances)
if all_set != inst_set:
raise ValueError(f"group instances is {all_set!r} but "
f"instance set is {inst_set!r}!")
return tuple(groups)


def __cutsq(matrix: np.ndarray) -> list[int]:
"""
Cut all items into squares via the CUTSQ procedure.
Expand Down Expand Up @@ -714,6 +818,65 @@ def list_resources() -> tuple[str, ...]:
"""
return _INSTANCES

@staticmethod
def list_resources_groups() -> tuple[tuple[
str, str | None, tuple[str, ...]], ...]:
"""
List the instance groups in the resources.
One problem of the benchmark set for 2-dimensional bin packing is that
it has many instances:
>>> len(Instance.list_resources())
557
With this function, we can group several of these instances together
in a way that is compliant with literature in order to then compute
statistics over these groups. Presenting data gathered over...
>>> len(Instance.list_resources_groups())
56
...groups is much easier than dealing with over 500 instances.
:return: the instance groups, in a two level hierarchy. The result is
a sequence of tuples. Each tuple has the top-level group name and,
optionally, a second-level group name (or `None` if no second
level group exists). The third tuple element is a sequence of
instance names.
>>> [(v[0], v[1], len(v[2])) for v in
... Instance.list_resources_groups()]
[('a', 'small', 14), ('a', 'med', 14), ('a', 'large', 15), \
('beng', '1-8', 8), ('beng', '9-10', 2), ('class 1', '20', 10), \
('class 1', '40', 10), ('class 1', '60', 10), ('class 1', '80', 10), \
('class 1', '100', 10), ('class 2', '20', 10), ('class 2', '40', 10), \
('class 2', '60', 10), ('class 2', '80', 10), ('class 2', '100', 10), \
('class 3', '20', 10), ('class 3', '40', 10), ('class 3', '60', 10), \
('class 3', '80', 10), ('class 3', '100', 10), ('class 4', '20', 10), \
('class 4', '40', 10), ('class 4', '60', 10), ('class 4', '80', 10), \
('class 4', '100', 10), ('class 5', '20', 10), ('class 5', '40', 10), \
('class 5', '60', 10), ('class 5', '80', 10), ('class 5', '100', 10), \
('class 6', '20', 10), ('class 6', '40', 10), ('class 6', '60', 10), \
('class 6', '80', 10), ('class 6', '100', 10), ('class 7', '20', 10), \
('class 7', '40', 10), ('class 7', '60', 10), ('class 7', '80', 10), \
('class 7', '100', 10), ('class 8', '20', 10), ('class 8', '40', 10), \
('class 8', '60', 10), ('class 8', '80', 10), ('class 8', '100', 10), \
('class 9', '20', 10), ('class 9', '40', 10), ('class 9', '60', 10), \
('class 9', '80', 10), ('class 9', '100', 10), ('class 10', '20', 10), \
('class 10', '40', 10), ('class 10', '60', 10), ('class 10', '80', 10), \
('class 10', '100', 10), ('asqas', None, 4)]
"""
obj: object = Instance.list_resources_groups
attr: str = "__gs"
if not hasattr(obj, attr):
gs: tuple[tuple[str, str | None, tuple[str, ...]], ...] =\
_make_instance_groups(Instance.list_resources())
setattr(obj, attr, gs)
return gs
return cast(tuple[tuple[str, str | None, tuple[str, ...]], ...],
getattr(obj, attr))

@staticmethod
def from_resource(name: str) -> "Instance":
"""
Expand Down
57 changes: 54 additions & 3 deletions moptipyapps/binpacking2d/objectives/bin_count.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,48 @@
BIN_COUNT_NAME: Final[str] = "binCount"


def ceil_div(a: int, b: int) -> int:
"""
Compute a ceiling division.
This function is needed by sub-classes.
:param a: the number to be divided by `b`
:param b: the number dividing `a`
:return: the rounded-up result of the division
>>> ceil_div(1, 1)
1
>>> ceil_div(98, 98)
1
>>> ceil_div(98, 99)
1
>>> ceil_div(98, 97)
2
>>> ceil_div(3, 1)
3
>>> ceil_div(3, 2)
2
>>> ceil_div(3, 3)
1
>>> ceil_div(3, 4)
1
>>> ceil_div(4, 1)
4
>>> ceil_div(4, 2)
2
>>> ceil_div(4, 3)
2
>>> ceil_div(4, 4)
1
>>> ceil_div(4, 5)
1
>>> ceil_div(4, 23242398)
1
"""
return -((-a) // b)


class BinCount(Objective):
"""Compute the number of bins."""

Expand All @@ -28,7 +70,7 @@ def __init__(self, instance: Instance) -> None:
if not isinstance(instance, Instance):
raise type_error(instance, "instance", Instance)
#: the internal instance reference
self.__instance: Final[Instance] = instance
self._instance: Final[Instance] = instance

def evaluate(self, x) -> int:
"""
Expand Down Expand Up @@ -65,7 +107,7 @@ def lower_bound(self) -> int:
>>> BinCount(ins).lower_bound()
4
"""
return self.__instance.lower_bound_bins
return self._instance.lower_bound_bins

def is_always_integer(self) -> bool:
"""
Expand Down Expand Up @@ -100,7 +142,16 @@ def upper_bound(self) -> int:
>>> BinCount(ins).upper_bound()
31
"""
return self.__instance.n_items
return self._instance.n_items

def to_bin_count(self, z: int) -> int:
"""
Get the bin count corresponding to an objective value.
:param z:
:return: the value itself
"""
return z

def __str__(self) -> str:
"""
Expand Down
45 changes: 17 additions & 28 deletions moptipyapps/binpacking2d/objectives/bin_count_and_last_empty.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@

import numba # type: ignore
import numpy as np
from moptipy.api.objective import Objective
from moptipy.utils.types import type_error

from moptipyapps.binpacking2d.instance import Instance
from moptipyapps.binpacking2d.objectives.bin_count import BinCount, ceil_div
from moptipyapps.binpacking2d.packing import IDX_BIN


Expand Down Expand Up @@ -69,21 +67,9 @@ def bin_count_and_last_empty(y: np.ndarray) -> int:
return (n_items * (current_bin - 1)) + current_size # return objective


class BinCountAndLastEmpty(Objective):
class BinCountAndLastEmpty(BinCount):
"""Compute the number of bins and the emptiness of the last one."""

def __init__(self, instance: Instance) -> None:
"""
Initialize the objective function.
:param instance: the instance to load the bounds from
"""
super().__init__()
if not isinstance(instance, Instance):
raise type_error(instance, "instance", Instance)
#: the internal instance reference
self.__instance: Final[Instance] = instance

def evaluate(self, x) -> int:
"""
Evaluate the objective function.
Expand Down Expand Up @@ -114,6 +100,7 @@ def lower_bound(self) -> int:
:return: `max(n_items, (lb - 1) * n_items + 1)`
>>> from moptipyapps.binpacking2d.instance import Instance
>>> ins = Instance("a", 100, 50, [[10, 5, 1], [3, 3, 1], [5, 5, 1]])
>>> ins.n_items
3
Expand All @@ -138,24 +125,17 @@ def lower_bound(self) -> int:
>>> BinCountAndLastEmpty(ins).lower_bound()
94
"""
return max(self.__instance.n_items,
((self.__instance.lower_bound_bins - 1)
* self.__instance.n_items) + 1)

def is_always_integer(self) -> bool:
"""
Return `True` because there are only integer bins.
:retval True: always
"""
return True
return max(self._instance.n_items,
((self._instance.lower_bound_bins - 1)
* self._instance.n_items) + 1)

def upper_bound(self) -> int:
"""
Get the upper bound of the number of bins plus emptiness.
:return: the number of items in the instance to the square
>>> from moptipyapps.binpacking2d.instance import Instance
>>> ins = Instance("a", 100, 50, [[10, 5, 1], [3, 3, 1], [5, 5, 1]])
>>> ins.n_items
3
Expand All @@ -174,7 +154,16 @@ def upper_bound(self) -> int:
>>> BinCountAndLastEmpty(ins).upper_bound()
961
"""
return self.__instance.n_items * self.__instance.n_items
return self._instance.n_items * self._instance.n_items

def to_bin_count(self, z: int) -> int:
"""
Convert an objective value to a bin count.
:param z: the objective value
:return: the bin count
"""
return ceil_div(z, self._instance.n_items)

def __str__(self) -> str:
"""
Expand Down
Loading

0 comments on commit ef29906

Please sign in to comment.