Skip to content

Commit

Permalink
added bin count and last skyline objective for 2d bin packing
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasWeise committed Dec 21, 2023
1 parent 3ab6b82 commit 09b3efa
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 7 deletions.
4 changes: 4 additions & 0 deletions moptipyapps/binpacking2d/objectives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@
returns a combination of the number of bins occupied by a given packing and
the smallest area under the skyline in any bin, where the "skyline" is the
upper border of the space occupied by objects.
- :mod:`~moptipyapps.binpacking2d.objectives.bin_count_and_last_skyline`
returns a combination of the number of bins occupied by a given packing and
the smallest area under the skyline in the last bin, where the "skyline" is
the upper border of the space occupied by objects.
"""
180 changes: 180 additions & 0 deletions moptipyapps/binpacking2d/objectives/bin_count_and_last_skyline.py
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"
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
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.
becomes empty. This is done with the objective
:mod:`~moptipyapps.binpacking2d.objectives.bin_count_and_last_skyline`.
But we could also minimize the area under the skylines in the other bins.
Because a) if we can get any skyline in any bin to become 0, then this bin
Expand Down
6 changes: 5 additions & 1 deletion moptipyapps/binpacking2d/packing_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
from moptipyapps.binpacking2d.objectives.bin_count_and_last_empty import (
BinCountAndLastEmpty,
)
from moptipyapps.binpacking2d.objectives.bin_count_and_last_skyline import (
BinCountAndLastSkyline,
)
from moptipyapps.binpacking2d.objectives.bin_count_and_last_small import (
BinCountAndLastSmall,
)
Expand Down Expand Up @@ -102,7 +105,8 @@
#: the default objective functions
DEFAULT_OBJECTIVES: Final[tuple[Callable[[Instance], Objective], ...]] = (
BinCount, BinCountAndLastEmpty, BinCountAndLastSmall,
BinCountAndLowestSkyline, BinCountAndEmpty, BinCountAndSmall,
BinCountAndLastSkyline, BinCountAndEmpty, BinCountAndSmall,
BinCountAndLowestSkyline,
)


Expand Down
2 changes: 1 addition & 1 deletion moptipyapps/version.py
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"
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def __make_valid(ra: rnd.Generator,
validate_objective(objective, solution_space, __make_valid)


def test_bin_count_and_last_small_objective() -> None:
"""Test the last-bin-small-area objective function."""
def test_bin_count_and_lowest_skyline_objective() -> None:
"""Test the lowest-skyline objective function."""
random: rnd.Generator = rnd.default_rng()

choices = list(Instance.list_resources())
Expand All @@ -63,8 +63,8 @@ def test_bin_count_and_last_small_objective() -> None:
validate_objective_on_2dbinpacking(BinCountAndLowestSkyline, random)


def test_bin_count_and_last_small_objective_2() -> None:
"""Test the last-bin-small-area objective function."""
def test_bin_count_and_lowest_skyline_objective_2() -> None:
"""Test the lowest-skyline objective function."""
random: rnd.Generator = rnd.default_rng()
for inst in Instance.list_resources():
if not inst.startswith(("a", "b")):
Expand Down

0 comments on commit 09b3efa

Please sign in to comment.