Skip to content

Commit

Permalink
added secondary 2d binpacking objective function
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasWeise committed Jul 12, 2023
1 parent e251713 commit 62fe568
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 9 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,33 @@ We use the TSP instances from [TSPLib](http://comopt.ifi.uni-heidelberg.de/softw
Important work on this code has been contributed by Mr. Tianyu LIANG (梁天宇), <[email protected]> a Master's student at the Institute of Applied Optimization (应用优化研究所, http://iao.hfuu.edu.cn) of the School of Artificial Intelligence and Big Data (人工智能与大数据学院) at Hefei University (合肥学院) in Hefei, Anhui, China (中国安徽省合肥市) under the supervision of Prof. Dr. Thomas Weise (汤卫思教授).


### 3.3. Dynamic Controller Synthesis

Another interesting example for optimization is the synthesis of [active controllers for dynamic systems](https://thomasweise.github.io/moptipyapps/moptipyapps.dynamic_control.html).
Dynamic systems have a state that changes over time based on some laws.
These laws may be expressed as ordinary differential equations, for example.
The classical [Stuart-Landau system](https://thomasweise.github.io/moptipyapps/moptipyapps.dynamic_control.systems.html#module-moptipyapps.dynamic_control.systems.stuart_landau), for instance, represents an object whose coordinates on a two-dimensional plane change as follows:

```
sigma = 0.1 - x² - y²
dx/dt = sigma * x - y
dy/dt = sigma * y + x
```

Regardless on which `(x, y)` the object initially starts, it tends to move to a circular rotation path centered around the origin with radius `sqrt(0.1)`.
Now we try to create a controller `ctrl` for such a system that moves the object from this periodic circular path into a fixed and stable location.
The controller `ctrl` receives the current state, i.e., the object location, as input and can influence the system as follows:

```
sigma = 0.1 - x² - y²
c = ctrl(x, y)
dx/dt = sigma * x - y
dy/dt = sigma * y + x + c
```

What we try to find is the controller which can bring move object to the origin `(0, 0)` as quickly as possible while expending the least amount of force, i.e., having the smallest aggregated `c` values over time.


## 4. Unit Tests and Static Analysis

When developing and applying randomized algorithms, proper testing and checking of the source code is of utmost importance.
Expand Down
4 changes: 2 additions & 2 deletions moptipyapps/binpacking2d/bin_count_and_last_empty.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
:attr:`~moptipyapps.binpacking2d.instance.Instance.n_items`, as
well (because this is also the number of rows in the packing).
Now we return `(n_items * (n_bins - 1)) + number_of_items_in_last_bin`,
wher `number_of_items_in_last_bin` is, well, the number of items in the
where `number_of_items_in_last_bin` is, well, the number of items in the
very last bin.
The idea behind this is: If one of two packings has the smaller number of
Expand All @@ -27,7 +27,7 @@
from moptipyapps.binpacking2d.packing import IDX_BIN


@numba.njit(cache=True, inline="always")
@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
def bin_count_and_last_empty(y: np.ndarray) -> int:
"""
Compute the number of bins and the emptiness of the last bin.
Expand Down
164 changes: 164 additions & 0 deletions moptipyapps/binpacking2d/bin_count_and_last_small.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""
An objective function for minimizing the number of bins of packings.
This objective function first computes the number of bins used. Let's call it
`n_bins`. We know the area bin_area of a bin as well.
Now we return `(bin_area * (n_bins - 1)) + area_of_items_in_last_bin`,
where `area_of_items_in_last_bin` is, well, the area covered by items in the
very last bin.
The idea behind this is: If one of two packings has the smaller number of
bins, then this one will always have the smaller objective value. If two
packings have the same number of bins, but one requires less space in the very
last bin, then that one is better. With this mechanism, we drive the search
towards "emptying" the last bin. If the number of items in the last bin would
reach `0`, that last bin would disappear - and we have one bin less.
"""
from typing import Final

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.packing import (
IDX_BIN,
IDX_BOTTOM_Y,
IDX_LEFT_X,
IDX_RIGHT_X,
IDX_TOP_Y,
)


@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
def bin_count_and_last_small(y: np.ndarray, bin_area: int) -> int:
"""
Compute the number of bins and the occupied area in the last bin.
We compute the total number of bins minus 1 and multiply it with the
total area of items. We then add the area of items in the last bin.
:param y: the packing
:param bin_area: the area of a single bin
:return: the objective value
>>> bin_count_and_last_small(np.array([[1, 1, 10, 10, 20, 20],
... [1, 1, 30, 30, 40, 40],
... [1, 1, 20, 20, 30, 30]], int),
... 50*50)
300
>>> bin_count_and_last_small(np.array([[1, 1, 10, 10, 20, 20],
... [1, 2, 30, 30, 40, 40], # bin 2!
... [1, 1, 20, 20, 30, 30]], int),
... 50*50)
2600
>>> bin_count_and_last_small(np.array([[1, 2, 10, 10, 20, 20], # bin 2!
... [1, 2, 30, 30, 40, 40], # bin 2!
... [1, 1, 20, 20, 30, 30]], int),
... 50*50)
2700
>>> bin_count_and_last_small(np.array([[1, 3, 10, 10, 20, 20], # bin 3!
... [1, 2, 30, 30, 40, 40], # bin 2!
... [1, 1, 20, 20, 30, 30]], int),
... 50*50)
5100
"""
current_bin: int = -1 # the current idea of what the last bin is
current_area: int = 0 # the area of items already in that bin
n_items: Final[int] = len(y) # the number of rows in the matrix

for i in range(n_items): # iterate over all packed items
bin_idx: int = y[i, IDX_BIN] # get the bin index of the item
if bin_idx < current_bin:
continue
area: int = (y[i, IDX_RIGHT_X] - y[i, IDX_LEFT_X]) \
* (y[i, IDX_TOP_Y] - y[i, IDX_BOTTOM_Y])
if bin_idx > current_bin: # it's a new biggest bin = new last bin?
current_area = area # then the current area is this
current_bin = bin_idx # and we remember it
elif bin_idx == current_bin: # did item go into the current last bin?
current_area += area # then increase size
return (bin_area * (current_bin - 1)) + current_area # return objective


class BinCountAndLastSmall(Objective):
"""Compute the number of bins and the area in the last one."""

def __init__(self, instance: Instance) -> None: # +book
"""
Initialize the number of bins 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
self.evaluate = numba.njit( # type: ignore
lambda y, z=instance.bin_width * instance.bin_height:
bin_count_and_last_small(y, z),
cache=True, inline="always", fastmath=True, boundscheck=False)

def lower_bound(self) -> int:
"""
Get the lower bound of the number of bins and emptiness objective.
We know from the instance (:attr:`~moptipyapps.binpacking2d\
.instance.Instance.lower_bound_bins`) that we require at least as many bins
such that they can accommodate the total area of all items together.
Let's call this number `lb`. Now if `lb` is one, then all objects could
be in the first bin, in which case the objective value would equal to
the total area of all items (:attr:`~moptipyapps.binpacking2d\
.instance.Instance.total_item_area`).
If it is `lb=2`, then we know that we will need at least two bins. The
best case would be that almost all items are in the first bin and
only the smallest object is in the last bin. This means that we would
get `1 * bin_area + smallest_area` as objective value. If we have
`lb=3` bins, then we could again have all but the smallest items
distributed over the first two bins and only the smallest one in the
last bin, i.e., would get `(2 * bin_area) + smallest_area`. And so on.
:return: `total_item_area` if the lower bound `lb` of the number of
bins is `1`, else `(lb - 1) * bin_area + smallest_area`, where
`bin_area` is the area of a bin, `total_item_area` is the area of
all items added up, and `smallest_area` is the area of the
smallest item
"""
if self.__instance.lower_bound_bins == 1:
return self.__instance.total_item_area
smallest_area: int = -1
for row in self.__instance:
area: int = row[0] * row[1]
if (smallest_area <= 0) or (area > smallest_area):
smallest_area = area
return int(((self.__instance.lower_bound_bins - 1)
* (self.__instance.bin_height
* self.__instance.bin_height)) + smallest_area)

def is_always_integer(self) -> bool:
"""
Return `True` because there are only integer bins.
:retval True: always
"""
return True

def upper_bound(self) -> int:
"""
Get the upper bound of this objective function.
:return: a very coarse estimate of the upper bound
"""
return self.__instance.n_items * self.__instance.bin_height \
* self.__instance.bin_width

def __str__(self) -> str:
"""
Get the name of the bins objective function.
:return: `binCountAndLastSmall`
:retval "binCountAndLastSmall": always
"""
return "binCountAndLastSmall"
6 changes: 5 additions & 1 deletion moptipyapps/binpacking2d/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ class Instance(Component, np.ndarray):
n_items: int
#: the number of different items
n_different_items: int
#: the total area occupied by all items
total_item_area: int
#: the bin width
bin_width: int
#: the bin height
Expand Down Expand Up @@ -345,8 +347,10 @@ def __new__(cls, name: str,
obj.bin_height = bin_height
#: the width of the bins
obj.bin_width = bin_width
#: the total area occupied by all items
obj.total_item_area = item_area

# We need as least as many bins such that their area is big enough
# We need at least as many bins such that their area is big enough
# for the total area of the items.
bin_area: int = bin_height * bin_width
min_size: int = item_area // bin_area
Expand Down
2 changes: 0 additions & 2 deletions moptipyapps/dynamic_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from such more complicated systems, they are much faster and easier to
simulate, though. So we can play with them much more easily and quickly.
The initial starting point of the work here were conversations with
Prof. Dr. Bernd NOACK and Guy Yoslan CORNJO MACEDA of the Harbin Institute
of Technology in Shenzhen, China (哈尔滨工业大学(深圳)) as well as the
Expand Down
2 changes: 1 addition & 1 deletion moptipyapps/dynamic_control/controllers/predefined.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def predefined(system: System) -> tuple[Controller, ...]:
if system.state_dims == 2:
return (Controller("cornejo_maeda", 2, 1, 3, __cornejo_maeda), )
if system.state_dims == 3:
return (Controller("table_3_1_lgpc", 3, 1, 2, __table_3_1_ga),
return (Controller("table_3_1_ga", 3, 1, 2, __table_3_1_ga),
Controller("table_3_1_lgpc", 3, 1, 4, __table_3_1_lgpc))
raise ValueError("invalid state dimensions "
f"{system.state_dims} for {system!r}.")
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.6"
__version__: Final[str] = "0.8.7"
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

# `moptipy` provides the basic optimization infrastructure and the spaces and
# tools that we use for optimization.
moptipy == 0.9.83
moptipy == 0.9.84

# `numpy` is needed for its efficient data structures.
numpy == 1.24.3
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ project_urls =
include_package_data = True
install_requires =
certifi >= 2023.5.7
moptipy >= 0.9.83
moptipy >= 0.9.84
numpy >= 1.24.3
numba >= 0.57.1
matplotlib >= 3.7.1
Expand Down
63 changes: 63 additions & 0 deletions tests/binpacking2d/test_binpacking2d_bin_count_and_last_small.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Test the bin-count-and-last-small 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.bin_count_and_last_small import (
BinCountAndLastSmall,
)
from moptipyapps.binpacking2d.ibl_encoding_1 import (
ImprovedBottomLeftEncoding1,
)
from moptipyapps.binpacking2d.ibl_encoding_2 import (
ImprovedBottomLeftEncoding2,
)
from moptipyapps.binpacking2d.instance import Instance
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 = BinCountAndLastSmall(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_empty_objective() -> None:
"""Test the makespan objective function."""
random: rnd.Generator = rnd.default_rng()

checks: set[str] = {"a01", "a10", "a20", "beng03", "beng10",
"cl01_040_08", "cl04_100_10", "cl10_060_03"}
choices = list(Instance.list_resources())
while len(checks) < 10:
checks.add(choices.pop(random.integers(len(choices))))

for s in checks:
__check_for_instance(Instance.from_resource(s), random)

validate_objective_on_2dbinpacking(BinCountAndLastSmall, random)

0 comments on commit 62fe568

Please sign in to comment.