Skip to content

Commit

Permalink
Add MTP for bin packing problem (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmyrberg committed Aug 6, 2022
1 parent 5ed14c9 commit bdc3bc2
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 4 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Solving knapsack problems with Python using algorithms by [Martello and Toth](ht
* Change-making problem: MTC2
* Bounded change-making problem: MTCB
* Generalized assignment problem: MTG, MTHG
* Bin packing problem: MTP

Documentation is available [here](https://mknapsack.readthedocs.io).

Expand Down Expand Up @@ -156,6 +157,21 @@ capacities = [11, 22]
res = solve_generalized_assignment(profits, weights, capacities)
```

### Bin Packing Problem

```python
from mknapsack import solve_bin_packing

# Given six items with the following weights:
weights = [4, 1, 8, 1, 4, 2]

# ...and bins with the following capacity:
capacity = 10

# Assign items into bins while minimizing the number of bins required
res = solve_bin_packing(weights, capacity)
```


## References

Expand Down
2 changes: 2 additions & 0 deletions mknapsack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


__all__ = [
'solve_bin_packing',
'solve_bounded_knapsack',
'solve_bounded_change_making',
'solve_change_making',
Expand All @@ -28,6 +29,7 @@
from mknapsack._exceptions import FortranInputCheckError, NoSolutionError, \
ProblemSizeError # noqa: E402

from mknapsack._bin_packing import solve_bin_packing # noqa: E402
from mknapsack._bounded import solve_bounded_knapsack # noqa: E402
from mknapsack._bounded_change_making import solve_bounded_change_making # noqa: E402, E501
from mknapsack._change_making import solve_change_making # noqa: E402
Expand Down
4 changes: 4 additions & 0 deletions mknapsack/_algos.f
Original file line number Diff line number Diff line change
Expand Up @@ -8536,6 +8536,10 @@ subroutine mtp ( n, w, c, z, xstar, jdim, back, jck, lb, wr,
c parameters are unchanged except back , which gives the number of
c backtrackings performed.
c
cf2py intent(in) n, w, c, jdim, back, jck
cf2py intent(out) z, xstar, lb
cf2py intent(hide) wr, xstarr, dum, res, rel, x, r, wa, wb, kfix, fixit, xred, ls, lsb, xheu
cf2py depend(jdim) w, xstar, wr, xstarr, dum, res, rel, x, r, wa, wb, kfix, fixit, xred, ls, lsb, xheu
integer w(jdim)
integer xstar(jdim),c,z,back
integer wr(jdim),xstarr(jdim),dum(jdim),vstar
Expand Down
120 changes: 120 additions & 0 deletions mknapsack/_bin_packing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Module for solving bin packing problem."""


import logging

from typing import List, Optional

import numpy as np

from mknapsack._algos import mtp
from mknapsack._exceptions import FortranInputCheckError
from mknapsack._utils import preprocess_array, pad_array


logger = logging.getLogger(__name__)


def solve_bin_packing(
weights: List[int],
capacity: int,
method: str = 'mtp',
method_kwargs: Optional[dict] = None,
verbose: bool = False
) -> np.ndarray:
"""Solves the bin packing problem.
Given a set of items with weights, assign each item exactly to one bin
while minimizing the number of bins required.
Args:
weights: Weight of each item.
capacity: Capacity of the bins.
method:
Algorithm to use for solving, should be one of
- 'mtp' - provides a fast heuristical solution or an exact
solution if required
Defaults to 'mtp'.
method_kwargs:
Keyword arguments to pass to a given `method`.
- 'mtp'
* **require_exact** (int, optional) - Whether to require an
exact solution or not (0=no, 1=yes). Defaults to 0.
* **max_backtracks** (int, optional) - The maximum number
of backtracks to perform when ``require_exact=0``.
Defaults to 100000.
* **check_inputs** (int, optional) - Whether to check
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
verbose: Log details of the solution. Defaults to False.
Returns:
np.ndarray: Assigned bin for each item.
Raises:
FortranInputCheckError: Something is wrong with the inputs when
validated in the original Fortran source code side.
ValueError: Something is wrong with the given inputs.
Example:
.. code-block:: python
from mknapsack import solve_bin_packing
res = solve_bin_packing(
weights=[4, 1, 8, 1, 4, 2],
capacity=10
)
References:
* Silvano Martello, Paolo Toth, Knapsack Problems: Algorithms and
Computer Implementations, Wiley, 1990, ISBN: 0-471-92420-2,
LC: QA267.7.M37.
* `Original Fortran77 source code by Martello and Toth\
<https://people.sc.fsu.edu/~jburkardt/f77_src/knapsack/knapsack.f>`_
"""
weights = preprocess_array(weights)
n = len(weights)

# Sort items by weight in descending order
items_reorder = weights.argsort()[::-1]
items_reorder_reverse = items_reorder.argsort()
weights = weights[items_reorder]

method = method.lower()
method_kwargs = method_kwargs or {}
if method == 'mtp':
jdim = n
w = pad_array(weights, jdim)

if method_kwargs.get('require_exact', 0):
back = -1
else:
back = method_kwargs.get('max_backtracks', 100_000)

z, x, lb = mtp(
n=n,
w=w,
c=capacity,
jdim=jdim,
back=back,
jck=method_kwargs.get('check_inputs', 1)
)

if z < 0:
raise FortranInputCheckError(method=method, z=z)

if verbose:
logger.info(f'Method: "{method}"')
logger.info(f'Total profit: {z}')
logger.info(f'Solution vector: {x}')
logger.info(f'Lower bound: {lb}')
else:
raise ValueError(f'Given method "{method}" not known')

return np.array(x)[:n][items_reorder_reverse]
3 changes: 2 additions & 1 deletion mknapsack/_bounded.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def solve_bounded_knapsack(
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
verbose: Log details of the solution. Defaults to False.
Returns:
np.ndarray: Number of items assigned to the knapsack for each item
Expand Down Expand Up @@ -85,7 +86,7 @@ def solve_bounded_knapsack(
'Profits length must be equal to weights and n_items '
f'(not {len(profits) == len(weights) == len(n_items)}')

# Sort items by profit/ratio ratio in ascending order
# Sort items by profit/weights ratio in ascending order
items_reorder = (profits / weights).argsort()[::-1]
items_reorder_reverse = np.argsort(items_reorder)
profits = profits[items_reorder]
Expand Down
1 change: 1 addition & 0 deletions mknapsack/_bounded_change_making.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def solve_bounded_change_making(
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
verbose: Log details of the solution. Defaults to False.
Returns:
np.ndarray: Number of items for each item type.
Expand Down
1 change: 1 addition & 0 deletions mknapsack/_change_making.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def solve_change_making(
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
verbose: Log details of the solution. Defaults to False.
Returns:
np.ndarray: Number of items for each item type.
Expand Down
6 changes: 6 additions & 0 deletions mknapsack/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ class FortranInputCheckError(Exception):
-3: 'Profit, weight, or capacity is <= 0',
-4: 'One or more of weights is greater than knapsack capacity',
-5: 'One or more knapsacks cannot fit any items'
},
'mtp': {
-1: 'Number of items is less than 2',
-2: 'Item weight or bin capacity is <= 0',
-3: 'One or more of weights is greater than bin capacity',
-4: 'Weights should be in ascending order'
}
}

Expand Down
1 change: 1 addition & 0 deletions mknapsack/_generalized_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def solve_generalized_assignment(
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
verbose: Log details of the solution. Defaults to False.
Returns:
np.ndarray: Assigned knapsack for each item.
Expand Down
3 changes: 2 additions & 1 deletion mknapsack/_multiple.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def solve_multiple_knapsack(
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
verbose: Log details of the solution. Defaults to False.
Returns:
np.ndarray: The corresponding knapsack for each item, where 0 means
Expand Down Expand Up @@ -100,7 +101,7 @@ def solve_multiple_knapsack(
raise ValueError('Profits length must be equal to weights '
f'({len(profits) != len(weights)}')

# Sort items by profit/ratio ratio in ascending order
# Sort items by profit/weights ratio in ascending order
items_reorder = (profits / weights).argsort()[::-1]
items_reorder_reverse = items_reorder.argsort()
profits = profits[items_reorder]
Expand Down
3 changes: 2 additions & 1 deletion mknapsack/_single.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def solve_single_knapsack(
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
verbose: Log details of the solution. Defaults to False.
Returns:
np.ndarray: Indicator of knapsack assignment for each item, where 0
Expand Down Expand Up @@ -113,7 +114,7 @@ def solve_single_knapsack(
raise ValueError('Profits length must be equal to weights '
f'({len(profits) != len(weights)}')

# Sort items by profit/ratio ratio in ascending order
# Sort items by profit/weights ratio in ascending order
items_reorder = (profits / weights).argsort()[::-1]
items_reorder_reverse = items_reorder.argsort()
profits = profits[items_reorder]
Expand Down
3 changes: 2 additions & 1 deletion mknapsack/_unbounded.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def solve_unbounded_knapsack(
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
verbose: Log details of the solution. Defaults to False.
Returns:
np.ndarray: Number of items assigned to the knapsack for each item
Expand Down Expand Up @@ -93,7 +94,7 @@ def solve_unbounded_knapsack(
raise ValueError('Profits length must be equal to weights '
f'({len(profits) != len(weights)}')

# Sort items by profit/ratio ratio in ascending order
# Sort items by profit/weights ratio in ascending order
items_reorder = (profits / weights).argsort()[::-1]
items_reorder_reverse = np.argsort(items_reorder)
profits = profits[items_reorder]
Expand Down
Loading

0 comments on commit bdc3bc2

Please sign in to comment.