-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added bin count and last skyline objective for 2d bin packing
- Loading branch information
1 parent
3ab6b82
commit 09b3efa
Showing
7 changed files
with
281 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
180 changes: 180 additions & 0 deletions
180
moptipyapps/binpacking2d/objectives/bin_count_and_last_skyline.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
""" | ||
An objective function indirectly minimizing the number of bins in packings. | ||
This objective function minimizes the number of bins and maximizes the | ||
"useable" space in the last bin. | ||
Which space is actually useful for our encodings? Let's say we have filled | ||
a bin to a certain degree and somewhere there is a "hole" in the filled area, | ||
but this hole is covered by another object. The area of the hole is not used, | ||
but it also cannot be used anymore. The area that we can definitely use is the | ||
area above the "skyline" of the objects in the bin. The skyline at any | ||
horizontal `x` coordinate be the highest border of any object that intersects | ||
with `x` horizontally. In other words, it is the `y` value at and above which | ||
no other object is located at this `x` coordinate. The area below the skyline | ||
cannot be used anymore. The area above the skyline can. | ||
If we minimize the area below the skyline in the very last bin, then this will | ||
a similar impact as minimizing the overall object area in the last bin (see | ||
:mod:`~moptipyapps.binpacking2d.objectives.bin_count_and_last_small`). We push | ||
the skyline lower and lower and, if we are lucky, the last bin eventually | ||
becomes empty. | ||
The objective | ||
:mod:`~moptipyapps.binpacking2d.objectives.bin_count_and_lowest_skyline` | ||
works quite similarly to this one, but minimizes the lowest skyline over any | ||
bin. | ||
""" | ||
from typing import Final | ||
|
||
import numba # type: ignore | ||
import numpy as np | ||
|
||
from moptipyapps.binpacking2d.instance import Instance | ||
from moptipyapps.binpacking2d.objectives.bin_count_and_last_small import ( | ||
BinCountAndLastSmall, | ||
) | ||
from moptipyapps.binpacking2d.packing import ( | ||
IDX_BIN, | ||
IDX_LEFT_X, | ||
IDX_RIGHT_X, | ||
IDX_TOP_Y, | ||
) | ||
|
||
|
||
@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False) | ||
def bin_count_and_last_skyline(y: np.ndarray, bin_width: int, | ||
bin_height: int) -> int: | ||
""" | ||
Compute the bin count-1 times the bin size + the space below the skyline. | ||
:param y: the packing | ||
:param bin_width: the bin width | ||
:param bin_height: the bin height | ||
:return: the objective value | ||
>>> 10*0 + 10*20 + 10*30 + 10*40 + 10*0 | ||
900 | ||
>>> bin_count_and_last_skyline(np.array([[1, 1, 10, 10, 20, 20], | ||
... [1, 1, 30, 30, 40, 40], | ||
... [1, 1, 20, 20, 30, 30]], int), | ||
... 50, 50) | ||
900 | ||
>>> 5 * 0 + 5 * 10 + 10 * 20 + 5 * 10 + 25 * 0 | ||
300 | ||
>>> bin_count_and_last_skyline(np.array([[1, 1, 5, 0, 15, 10], | ||
... [1, 1, 10, 10, 20, 20], | ||
... [1, 1, 15, 0, 25, 10]], int), | ||
... 50, 50) | ||
300 | ||
>>> 50*50 + 0*10 + 10*20 + 30*0 | ||
2700 | ||
>>> bin_count_and_last_skyline(np.array([[1, 1, 5, 0, 15, 10], | ||
... [1, 2, 10, 10, 20, 20], | ||
... [1, 1, 15, 0, 25, 10]], int), | ||
... 50, 50) | ||
2700 | ||
>>> 5 * 0 + 5 * 10 + 3 * 20 + (50 - 13) * 25 | ||
1035 | ||
>>> bin_count_and_last_skyline(np.array([[1, 1, 5, 0, 15, 10], | ||
... [1, 1, 10, 10, 20, 20], | ||
... [1, 1, 15, 0, 25, 10], | ||
... [2, 1, 13, 20, 50, 25]], int), | ||
... 50, 50) | ||
1035 | ||
>>> 50*50*3 + 25*50 | ||
8750 | ||
>>> bin_count_and_last_skyline(np.array([[1, 1, 0, 0, 10, 10], | ||
... [2, 2, 0, 0, 20, 20], | ||
... [3, 3, 0, 0, 25, 10], | ||
... [4, 4, 0, 0, 50, 25]], int), | ||
... 50, 50) | ||
8750 | ||
>>> 50*50*3 + 25*10 + 25*0 | ||
7750 | ||
>>> bin_count_and_last_skyline(np.array([[1, 1, 0, 0, 10, 10], | ||
... [2, 2, 0, 0, 20, 20], | ||
... [3, 4, 0, 0, 25, 10], | ||
... [4, 3, 0, 0, 50, 25]], int), | ||
... 50, 50) | ||
7750 | ||
>>> 50*50*3 + 20*20 + 30*0 | ||
7900 | ||
>>> bin_count_and_last_skyline(np.array([[1, 1, 0, 0, 10, 10], | ||
... [2, 4, 0, 0, 20, 20], | ||
... [3, 2, 0, 0, 25, 10], | ||
... [4, 3, 0, 0, 50, 25]], int), | ||
... 50, 50) | ||
7900 | ||
>>> 50*50*3 + 20*10 + 30*20 + 20*0 | ||
8300 | ||
>>> bin_count_and_last_skyline(np.array([[1, 1, 0, 0, 10, 10], | ||
... [2, 4, 0, 0, 20, 20], | ||
... [3, 2, 0, 0, 25, 10], | ||
... [2, 4, 10, 20, 30, 30], | ||
... [4, 3, 0, 0, 50, 25]], int), | ||
... 50, 50) | ||
8300 | ||
""" | ||
bins: Final[int] = int(y[:, IDX_BIN].max()) | ||
len_y: Final[int] = len(y) | ||
bin_size: Final[int] = bin_height * bin_width | ||
area_under_skyline: int = 0 | ||
|
||
cur_left: int = 0 | ||
use_bin: int = max(y[:, IDX_BIN]) # the bin to use | ||
while cur_left < bin_width: | ||
use_right = next_left = bin_width | ||
use_top: int = 0 | ||
for i in range(len_y): | ||
if y[i, IDX_BIN] != use_bin: | ||
continue | ||
left: int = int(y[i, IDX_LEFT_X]) | ||
right: int = int(y[i, IDX_RIGHT_X]) | ||
top: int = int(y[i, IDX_TOP_Y]) | ||
if left <= cur_left < right and top > use_top: | ||
use_top = top | ||
use_right = right | ||
if cur_left < left < next_left: | ||
next_left = left | ||
|
||
if next_left < use_right: | ||
use_right = next_left | ||
area_under_skyline += (use_right - cur_left) * use_top | ||
cur_left = use_right | ||
return ((bins - 1) * bin_size) + area_under_skyline | ||
|
||
|
||
class BinCountAndLastSkyline(BinCountAndLastSmall): | ||
"""Compute the number of bins and the useful area in the last bin.""" | ||
|
||
def __init__(self, instance: Instance) -> None: | ||
""" | ||
Initialize the objective function. | ||
:param instance: the instance to load the bounds from | ||
""" | ||
super().__init__(instance) | ||
#: the bin width | ||
self.__bin_width: Final[int] = instance.bin_width | ||
#: the bin height | ||
self.__bin_height: Final[int] = instance.bin_height | ||
|
||
def evaluate(self, x) -> int: | ||
""" | ||
Evaluate the objective function. | ||
:param x: the solution | ||
:return: the bin size and last-bin-small-area factor | ||
""" | ||
return bin_count_and_last_skyline( | ||
x, self.__bin_width, self.__bin_height) | ||
|
||
def __str__(self) -> str: | ||
""" | ||
Get the name of the bins objective function. | ||
:return: `binCountAndLowestSkyline` | ||
:retval "binCountAndLowestSkyline": always | ||
""" | ||
return "binCountAndLastSkyline" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
"""An internal file with the version of the `moptipyapps` package.""" | ||
from typing import Final | ||
|
||
__version__: Final[str] = "0.8.41" | ||
__version__: Final[str] = "0.8.42" |
85 changes: 85 additions & 0 deletions
85
tests/binpacking2d/objectives/test_binpacking2d_bin_count_and_last_skyline.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
"""Test the bin-count-and-last-skyline objective.""" | ||
import numpy.random as rnd | ||
from moptipy.operators.signed_permutations.op0_shuffle_and_flip import ( | ||
Op0ShuffleAndFlip, | ||
) | ||
from moptipy.spaces.signed_permutations import SignedPermutations | ||
from moptipy.tests.objective import validate_objective | ||
|
||
from moptipyapps.binpacking2d.encodings.ibl_encoding_1 import ( | ||
ImprovedBottomLeftEncoding1, | ||
) | ||
from moptipyapps.binpacking2d.encodings.ibl_encoding_2 import ( | ||
ImprovedBottomLeftEncoding2, | ||
) | ||
from moptipyapps.binpacking2d.instance import Instance | ||
from moptipyapps.binpacking2d.objectives.bin_count_and_last_skyline import ( | ||
BinCountAndLastSkyline, | ||
) | ||
from moptipyapps.binpacking2d.packing import Packing | ||
from moptipyapps.binpacking2d.packing_space import PackingSpace | ||
from moptipyapps.tests.on_binpacking2d import ( | ||
validate_objective_on_2dbinpacking, | ||
) | ||
|
||
|
||
def __check_for_instance(inst: Instance, random: rnd.Generator) -> None: | ||
""" | ||
Check the objective for one problem instance. | ||
:param inst: the instance | ||
""" | ||
search_space = SignedPermutations(inst.get_standard_item_sequence()) | ||
solution_space = PackingSpace(inst) | ||
encoding = (ImprovedBottomLeftEncoding1 if random.integers(2) == 0 | ||
else ImprovedBottomLeftEncoding2)(inst) | ||
objective = BinCountAndLastSkyline(inst) | ||
op0 = Op0ShuffleAndFlip(search_space) | ||
|
||
def __make_valid(ra: rnd.Generator, | ||
y: Packing, ss=search_space, | ||
en=encoding, o0=op0) -> Packing: | ||
x = ss.create() | ||
o0.op0(ra, x) | ||
en.decode(x, y) | ||
return y | ||
|
||
validate_objective(objective, solution_space, __make_valid) | ||
|
||
|
||
def test_bin_count_and_last_skyline_objective() -> None: | ||
"""Test the last-bin-skyline-area objective function.""" | ||
random: rnd.Generator = rnd.default_rng() | ||
|
||
choices = list(Instance.list_resources()) | ||
checks: set[str] = {c for c in choices if c.startswith(("a", "b"))} | ||
min_len: int = len(checks) + 10 | ||
while len(checks) < min_len: | ||
checks.add(choices.pop(random.integers(len(choices)))) | ||
|
||
for s in checks: | ||
__check_for_instance(Instance.from_resource(s), random) | ||
|
||
validate_objective_on_2dbinpacking(BinCountAndLastSkyline, random) | ||
|
||
|
||
def test_bin_count_and_last_skyline_objective_2() -> None: | ||
"""Test the last-bin-skyline-area objective function.""" | ||
random: rnd.Generator = rnd.default_rng() | ||
for inst in Instance.list_resources(): | ||
if not inst.startswith(("a", "b")): | ||
continue | ||
instance = Instance.from_resource(inst) | ||
search_space = SignedPermutations( | ||
instance.get_standard_item_sequence()) | ||
solution_space = PackingSpace(instance) | ||
encoding = (ImprovedBottomLeftEncoding1 if random.integers(2) == 0 | ||
else ImprovedBottomLeftEncoding2)(instance) | ||
objective = BinCountAndLastSkyline(instance) | ||
op0 = Op0ShuffleAndFlip(search_space) | ||
x = search_space.create() | ||
op0.op0(random, x) | ||
y = solution_space.create() | ||
encoding.decode(x, y) | ||
assert 0 <= objective.lower_bound() <= objective.evaluate(y) \ | ||
<= objective.upper_bound() <= 1_000_000_000_000_000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters