Skip to content

Commit

Permalink
Add mtb2, change output dtype and remove pandas (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmyrberg authored Jul 24, 2022
1 parent a48cb4a commit d6fcb6a
Show file tree
Hide file tree
Showing 13 changed files with 496 additions and 247 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Solving knapsack problems with Python using algorithms by [Martello and Toth](https://dl.acm.org/doi/book/10.5555/98124):

* Single 0-1 knapsack problem: MT1, MT2, MT1R (real numbers)
* Bounded knapsack problem: MTB2
* Multiple 0-1 knapsack problem: MTM, MTHM

Documentation is available [here](https://mknapsack.readthedocs.io).
Expand Down Expand Up @@ -41,12 +42,31 @@ weights = [18, 9, 23, 20, 59, 61, 70, 75, 76, 30]
# ...and a knapsack with the following capacity:
capacity = 190

# Assign items into the knapsack while maximizing profit
# Assign items into the knapsack while maximizing profits
res = solve_single_knapsack(profits, weights, capacity)
```

If your inputs are real numbers, you may set parameter `method='mt1r'`.

### Bounded Knapsack Problem

```python
from mknapsack import solve_bounded_knapsack

# Given ten item types with the following profits and weights:
profits = [78, 35, 89, 36, 94, 75, 74, 79, 80, 16]
weights = [18, 9, 23, 20, 59, 61, 70, 75, 76, 30]

# ..and the number of items for each item type:
n_items = [1, 2, 3, 2, 2, 1, 2, 2, 1, 4]

# ...and a knapsack with the following capacity:
capacity = 190

# Assign items into the knapsack while maximizing profits
res = solve_bounded_knapsack(profits, weights, capacity, n_items)
```

### Multiple 0-1 Knapsack Problem

```python
Expand All @@ -59,7 +79,7 @@ weights = [18, 9, 23, 20, 59, 61, 70, 75, 76, 30]
# ...and two knapsacks with the following capacities:
capacities = [90, 100]

# Assign items into knapsacks while maximizing profit
# Assign items into knapsacks while maximizing profits
res = solve_multiple_knapsack(profits, weights, capacities)
```

Expand Down
7 changes: 6 additions & 1 deletion mknapsack/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"""Solving knapsack problems with Python."""

__all__ = ['solve_single_knapsack', 'solve_multiple_knapsack']
__all__ = [
'solve_bounded_knapsack',
'solve_single_knapsack',
'solve_multiple_knapsack'
]

import os
import sys
Expand All @@ -13,5 +17,6 @@
os.add_dll_directory(extra_dll_dir)


from mknapsack._bounded import solve_bounded_knapsack # noqa: E402
from mknapsack._single import solve_single_knapsack # noqa: E402
from mknapsack._multiple import solve_multiple_knapsack # noqa: E402
5 changes: 5 additions & 0 deletions mknapsack/_algos.f
Original file line number Diff line number Diff line change
Expand Up @@ -6499,6 +6499,11 @@ subroutine mtb2(n,p,w,b,c,z,x,jdim1,jdim2,jfo,jfs,jck,jub,
c all the parameters but rd8 are integer. on return of mtb2 all the
c input parameters are unchanged.
c
cf2py intent(in) n, p, w, b, c, jdim1, jdim2, jfo, jfs, jck
cf2py intent(hide) id1, id2, id3, id4, id5, id6, id7, rd8
cf2py intent(out) z, x, jub
cf2py depend(jdim1) p, w, b, x
cf2py depend(jdim2) id1, id2, id3, id4, id5, id6, id7, rd8
integer p(jdim1),w(jdim1),b(jdim1),x(jdim1),c,z
integer id1(jdim2),id2(jdim2),id3(jdim2),id4(jdim2),id5(jdim2),
& id6(jdim2),id7(jdim2)
Expand Down
135 changes: 135 additions & 0 deletions mknapsack/_bounded.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Module for solving bounded knapsack problems.
TODO:
mtu1: Unbounded single knapsack problem
mtu2: Unbounded single knapsack problem
"""


import logging

from typing import List, Optional

import numpy as np

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


logger = logging.getLogger(__name__)


def solve_bounded_knapsack(
profits: List[float],
weights: List[float],
n_items: List[float],
capacity: float,
method: str = 'mtb2',
method_kwargs: Optional[dict] = None,
verbose: bool = False
) -> np.ndarray:
"""Solves the bounded knapsack problem.
Given a certain number of item types with profits and weights, and a
knapsack with given capacity, how many of each item type should be picked
to maximize profits?
Args:
profits: Profit of each item type.
weights: Weight of each item type.
n_items: Number of items available for each item type.
capacity: Capacity of knapsack.
method: Algorithm to use for solving, currently only 'mtb2' is
supported. Defaults to 'mtb2'.
method_kwargs:
Keyword arguments to pass to 'mtb2' algorithm:
* **require_exact** (int, optional) - Whether to require an
exact solution or not (0=no, 1=yes). Defaults to 0.
* **check_inputs** (int, optional) - Whether to check
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
Returns:
np.ndarray: Number of items assigned to the knapsack for each item
type.
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_bounded_knapsack
res = solve_bounded_knapsack(
profits=[78, 35, 89, 36, 94, 75, 74, 100, 80, 16],
weights=[18, 9, 23, 20, 59, 61, 70, 75, 76, 30],
n_items=[1, 2, 3, 2, 2, 1, 2, 2, 1, 4],
capacity=190
)
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>`_
"""
profits = preprocess_array(profits)
weights = preprocess_array(weights)
n_items = preprocess_array(n_items)

if len(profits) != len(weights) or len(profits) != len(n_items):
raise ValueError(
'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
items_reorder = (profits / weights).argsort()[::-1]
items_reorder_reverse = np.argsort(items_reorder)
profits = profits[items_reorder]
weights = weights[items_reorder]
n_items = n_items[items_reorder]

n = len(profits)

method = method.lower()
method_kwargs = method_kwargs or {}
if method == 'mtb2':
jdim1 = n + 1
jdim2 = n + int(np.ceil(np.log2(n_items).sum())) + 3
p = pad_array(profits, jdim1)
w = pad_array(weights, jdim1)
b = pad_array(n_items, jdim1)
z, x, jub = mtb2(
n=n,
p=p,
w=w,
b=b,
c=capacity,
jdim1=jdim1,
jdim2=jdim2,
jfo=method_kwargs.pop('require_exact', False),
jfs=1,
jck=method_kwargs.pop('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 (non-original order): {x}')
logger.info(f'Solution upper bound: {jub}')
else:
raise ValueError(f'Given method "{method}" not known')

# Inverse items and knapsacks to original order
return np.array(x)[:n][items_reorder_reverse]
9 changes: 9 additions & 0 deletions mknapsack/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ class FortranInputCheckError(Exception):
-3.0: 'One or more of weights is greater than knapsack capacity',
-4.0: 'Total weight is smaller than knapsack capacity',
-5.0: 'Items should be ordered in descending profit/weight order'
},
'mtb2': {
-1: 'Number of items is less than 2',
-2: 'Profit, weight, capacity or n_items is <= 0',
-3: 'Total weight (weight * total_n_items) of one or more item '
'types is greater than the knapsack capacity',
-4: 'Total weight of all items is smaller than knapsack capacity',
-5: 'Problem with preprocessing before Fortran code',
-6: 'Items should be ordered in descending profit/weight order'
}
}

Expand Down
75 changes: 18 additions & 57 deletions mknapsack/_multiple.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from typing import List, Optional

import numpy as np
import pandas as pd

from mknapsack._algos import mtm, mthm
from mknapsack._exceptions import FortranInputCheckError
Expand All @@ -16,43 +15,22 @@
logger = logging.getLogger(__name__)


def process_results(profits, weights, capacities, x):
"""Preprocess multiple 0-1 knapsack results."""
given_knapsacks = pd.DataFrame({
'knapsack_id': np.arange(len(capacities)) + 1,
'knapsack_capacity': capacities
})
no_knapsack = pd.DataFrame([{'knapsack_id': 0, 'knapsack_capacity': 0}])
knapsacks = pd.concat([no_knapsack, given_knapsacks], axis=0)
items = (
pd.DataFrame({
'item_id': np.arange(len(profits)) + 1,
'profit': profits,
'weight': weights,
'knapsack_id': x[:len(profits)]
})
.merge(knapsacks, on='knapsack_id', how='left')
.assign(assigned=lambda x: x['knapsack_id'] > 0)
)
return items


def solve_multiple_knapsack(
profits: List[int],
weights: List[int],
capacities: List[int],
method: str = 'mthm',
method_kwargs: Optional[dict] = None,
verbose: bool = False
) -> pd.DataFrame:
) -> np.ndarray:
"""Solves the multiple 0-1 knapsack problem.
Given a set of items with profits and weights and knapsacks with given
capacities, how should assign the items into knapsacks in order to maximize
profits?
Args:
profits: Profits of each item.
profits: Profit of each item.
weights: Weight of each item.
capacities: Capacity of each knapsack.
method:
Expand Down Expand Up @@ -87,8 +65,8 @@ def solve_multiple_knapsack(
Defaults to None.
Returns:
pd.DataFrame: The corresponding knapsack for each item, where
``knapsack_id=0`` means that the item is not assigned to a knapsack.
np.ndarray: The corresponding knapsack for each item, where 0 means
that the item was not assigned to a knapsack.
Raises:
FortranInputCheckError: Something is wrong with the inputs when
Expand Down Expand Up @@ -123,15 +101,18 @@ def solve_multiple_knapsack(
f'({len(profits) != len(weights)}')

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

# Sort knapsacks by their capacity in ascending order
knapsacks_order_idx = capacities.argsort()
knapsacks_reverse_idx = knapsacks_order_idx.argsort()
capacities = capacities[knapsacks_order_idx]
knapsacks_reorder = capacities.argsort()
capacities = capacities[knapsacks_reorder]
knapsack_reorder_reverse_map = {
idx + 1: i + 1 for i, idx in enumerate(knapsacks_reorder.argsort())
}
knapsack_reorder_reverse_map[0] = 0

n = len(profits)
m = len(capacities)
Expand Down Expand Up @@ -170,10 +151,8 @@ def solve_multiple_knapsack(
if verbose:
logger.info(f'Method: "{method}"')
logger.info(f'Total profit: {z}')
logger.info('Solution vector: '
f'{x[:n][items_reverse_idx].tolist()}')
logger.info(f'Solution vector (non-original order): {x}')
logger.info(f'Number of backtracks: {back}')

elif method == 'mthm':
p = pad_array(profits, n + 1)
w = pad_array(weights, n + 1)
Expand All @@ -196,30 +175,12 @@ def solve_multiple_knapsack(
if verbose:
logger.info(f'Method: "{method}"')
logger.info(f'Total profit: {z}')
logger.info('Solution vector: '
f'{x[:n][items_reverse_idx].tolist()}')
logger.info(f'Solution vector (non-original order): {x}')
else:
raise ValueError(f'Given method "{method}" not known')

# Inverse items and knapsacks to original order
profits = profits[items_reverse_idx]
weights = weights[items_reverse_idx]
x = np.array(x)[items_reverse_idx]
capacities = capacities[knapsacks_reverse_idx]

res = process_results(profits, weights, capacities, x)

if verbose:
knapsack_results = (
res
.groupby('knapsack_id')
.agg(
capacity_used=('weight', 'sum'),
capacity_available=('knapsack_capacity', 'first'),
profit=('profit', 'sum'),
items=('item_id', 'unique')
)
)
logger.info(f'Results by knapsack_id:\n{knapsack_results.to_string()}')
res = np.array([knapsack_reorder_reverse_map[i]
for i in x[:n][items_reorder_reverse]])

return res
Loading

0 comments on commit d6fcb6a

Please sign in to comment.