-
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 secondary 2d binpacking objective function
- Loading branch information
1 parent
e251713
commit 62fe568
Showing
10 changed files
with
265 additions
and
9 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
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 |
---|---|---|
@@ -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" |
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
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.6" | ||
__version__: Final[str] = "0.8.7" |
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
63 changes: 63 additions & 0 deletions
63
tests/binpacking2d/test_binpacking2d_bin_count_and_last_small.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,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) |