pykp 0.0.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pykp/__init__.py +4 -0
- pykp/arrangement.py +63 -0
- pykp/item.py +40 -0
- pykp/knapsack.py +742 -0
- pykp/sampler.py +81 -0
- pykp-0.0.22.dist-info/METADATA +108 -0
- pykp-0.0.22.dist-info/RECORD +9 -0
- pykp-0.0.22.dist-info/WHEEL +5 -0
- pykp-0.0.22.dist-info/top_level.txt +1 -0
pykp/__init__.py
ADDED
pykp/arrangement.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .item import Item
|
|
3
|
+
|
|
4
|
+
class Arrangement():
|
|
5
|
+
"""
|
|
6
|
+
Represents an arrangement of items for the knapsack problem.
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
* items (np.ndarray[Item]): An array of items for the knapsack problem.
|
|
10
|
+
* state (np.ndarray[int]): Binary array indicating the inclusion/exclusion of items in the arrangement.
|
|
11
|
+
* value (int): The total value of items in the arrangement.
|
|
12
|
+
* weight (int): The total weight of items in the arrangement.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
items: np.ndarray[Item],
|
|
17
|
+
state: np.ndarray[int],
|
|
18
|
+
):
|
|
19
|
+
"""Initialises an Arrangement instance.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
items (np.ndarray[Item]): An array of items for the knapsack problem.
|
|
23
|
+
state (np.ndarray[int]): Binary array indicating the inclusion/exclusion of items in the arrangement.
|
|
24
|
+
capacity (int): The maximum weight capacity constraint for the arrangement.
|
|
25
|
+
"""
|
|
26
|
+
if not np.all(np.isin(state, [0, 1])):
|
|
27
|
+
raise ValueError("Elements of `state` must be 0 or 1.")
|
|
28
|
+
|
|
29
|
+
self.items = items
|
|
30
|
+
self.state = state
|
|
31
|
+
self.value = self.__calculate_value()
|
|
32
|
+
self.weight = self.__calculate_weight()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def __calculate_value(self):
|
|
36
|
+
"""
|
|
37
|
+
Calculates the total value of items currently in the knapsack.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
float: The total value of items in the knapsack.
|
|
41
|
+
"""
|
|
42
|
+
return sum([self.items[i].value for i, inside in enumerate(self.state) if bool(inside)])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __calculate_weight(self):
|
|
46
|
+
"""
|
|
47
|
+
Calculates the total weight of items currently in the knapsack.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
float: The total weight of items in the knapsack.
|
|
51
|
+
"""
|
|
52
|
+
return sum([self.items[i].weight for i, inside in enumerate(self.state) if bool(inside)])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def __str__(self):
|
|
56
|
+
state = int("".join(self.state.astype(int).astype(str)), 2)
|
|
57
|
+
return f"(v: {self.value}, w: {self.weight}, s: {state})"
|
|
58
|
+
|
|
59
|
+
def __repr__(self):
|
|
60
|
+
state = int("".join(self.state.astype(int).astype(str)), 2)
|
|
61
|
+
return f"(v: {self.value}, w: {self.weight}, s: {state})"
|
|
62
|
+
|
|
63
|
+
|
pykp/item.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Item:
|
|
2
|
+
"""
|
|
3
|
+
Represents an item for the knapsack problem.
|
|
4
|
+
|
|
5
|
+
Attributes:
|
|
6
|
+
value (int): The value of the item.
|
|
7
|
+
weight (int): The weight of the item.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, value: int, weight: int):
|
|
10
|
+
"""
|
|
11
|
+
Initialises an Item instance.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
value (int): The value of the item.
|
|
15
|
+
weight (int): The weight of the item.
|
|
16
|
+
"""
|
|
17
|
+
self.weight = weight
|
|
18
|
+
self.value = value
|
|
19
|
+
|
|
20
|
+
def update_value(self, new_value: int):
|
|
21
|
+
"""Updates the value of the item.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
new_value (int): New value of the item.
|
|
25
|
+
"""
|
|
26
|
+
self.value = new_value
|
|
27
|
+
|
|
28
|
+
def update_weight(self, new_weight: int):
|
|
29
|
+
"""Updates the weight of the item.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
new_weight (int): New weight of the item.
|
|
33
|
+
"""
|
|
34
|
+
self.weight = new_weight
|
|
35
|
+
|
|
36
|
+
def __str__(self):
|
|
37
|
+
return f"weight: {self.weight}; value: {self.value}"
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
return str(self)
|
pykp/knapsack.py
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .item import Item
|
|
4
|
+
from .arrangement import Arrangement
|
|
5
|
+
import operator
|
|
6
|
+
import itertools
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
from anytree import Node, PreOrderIter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Knapsack:
|
|
13
|
+
"""
|
|
14
|
+
Represents a knapsack problem solver.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
* items (np.ndarray[Item]): An array of items available for the knapsack problem.
|
|
18
|
+
* capacity (int): The maximum weight capacity of the knapsack.
|
|
19
|
+
* state (np.ndarray): Binary array indicating the inclusion/exclusion of items in the knapsack.
|
|
20
|
+
* value (float): The total value of items currently in the knapsack.
|
|
21
|
+
* weight (float): The total weight of items currently in the knapsack.
|
|
22
|
+
* is_feasible (bool): Indicates if the knapsack is within its weight capacity.
|
|
23
|
+
* is_at_capacity (bool): Indicates if the knapsack is at full capacity.
|
|
24
|
+
* terminal_nodes (np.ndarray[Arrangement]): An array of all possible arrangements of items that are under the weight constraint, and at full capacity
|
|
25
|
+
* optimal_nodes (np.ndarray[Arrangement]): An array of optimal solutions to the knapsack problem.
|
|
26
|
+
|
|
27
|
+
Methods:
|
|
28
|
+
* add(item: Item) -> np.ndarray:
|
|
29
|
+
Adds the specified item to the knapsack and returns current state.
|
|
30
|
+
|
|
31
|
+
* remove(item: Item) -> np.ndarray:
|
|
32
|
+
Removes the specified item from the knapsack.
|
|
33
|
+
|
|
34
|
+
* set_state(state: np.ndarray) -> np.ndarray:
|
|
35
|
+
Sets the knapsack state using the provided binary array.
|
|
36
|
+
|
|
37
|
+
* empty() -> np.ndarray:
|
|
38
|
+
Empties the knapsack by setting all items to be excluded.
|
|
39
|
+
|
|
40
|
+
* solve_terminal_nodes():
|
|
41
|
+
Finds the terminal nodes of the knapsack.
|
|
42
|
+
|
|
43
|
+
* solve_feasible_nodes():
|
|
44
|
+
Finds the feasible nodes of the knapsack.
|
|
45
|
+
|
|
46
|
+
* solve_branch_and_bound():
|
|
47
|
+
Solves the optimal and second-best terminal nodes using best-first branch-and-bound.
|
|
48
|
+
|
|
49
|
+
* calculate_sahni_k(arrangement: Arrangement) -> int:
|
|
50
|
+
Calculates the Sahni-k value for a given arrangement.
|
|
51
|
+
|
|
52
|
+
* plot_terminal_nodes_histogram():
|
|
53
|
+
Plots a histogram of terminal node values.
|
|
54
|
+
|
|
55
|
+
* write_to_json():
|
|
56
|
+
Writes the knapsack configuration to a .json file.
|
|
57
|
+
|
|
58
|
+
* load_from_json():
|
|
59
|
+
Loads a knapsack configuration from a provided .json file.
|
|
60
|
+
|
|
61
|
+
* summary() -> pd.DataFrame:
|
|
62
|
+
Generates a summary DataFrame containing information about the knapsack state and solutions.
|
|
63
|
+
"""
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
items: np.ndarray[Item],
|
|
67
|
+
capacity: int,
|
|
68
|
+
load_from_json: bool = False,
|
|
69
|
+
path_to_spec: str = None
|
|
70
|
+
):
|
|
71
|
+
"""
|
|
72
|
+
Initialises a Knapsack instance.
|
|
73
|
+
|
|
74
|
+
Parameters:
|
|
75
|
+
items (np.ndarray[Item]): An array of items for the knapsack problem.
|
|
76
|
+
capacity (int): The maximum weight capacity of the knapsack.
|
|
77
|
+
load_from_json (bool, optional): Whether to load the instance from a .json spec. Default is False.
|
|
78
|
+
path_to_spec (str, optional): Path to json spec file. Default is None.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if load_from_json:
|
|
82
|
+
self.load_from_json(path_to_spec)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if len(items) == 0:
|
|
86
|
+
raise ValueError("`items` must have length greater than 0.")
|
|
87
|
+
if not np.all([isinstance(item, Item) for item in items]):
|
|
88
|
+
raise ValueError("All elements in `items` must be of type `Item`.")
|
|
89
|
+
if capacity < 0:
|
|
90
|
+
raise ValueError("`capacity` must be non-negative.")
|
|
91
|
+
if not isinstance(items, np.ndarray):
|
|
92
|
+
items = np.array(items)
|
|
93
|
+
|
|
94
|
+
self.items = items
|
|
95
|
+
self.capacity = capacity
|
|
96
|
+
self.state = np.zeros_like(items)
|
|
97
|
+
self.value = 0
|
|
98
|
+
self.weight = 0
|
|
99
|
+
self.is_feasible = True
|
|
100
|
+
self.is_at_capacity = False
|
|
101
|
+
|
|
102
|
+
self.nodes = np.array([])
|
|
103
|
+
self.feasible_nodes = np.array([])
|
|
104
|
+
self.terminal_nodes = np.array([])
|
|
105
|
+
self.optimal_nodes = np.array([])
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def solve(
|
|
109
|
+
self,
|
|
110
|
+
solve_terminal_nodes: bool = False,
|
|
111
|
+
solve_feasible_nodes: bool = False,
|
|
112
|
+
solve_second_best: bool = True
|
|
113
|
+
):
|
|
114
|
+
"""
|
|
115
|
+
Solves the knapsack problem and returns optimal arrangements.
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
solve_terminal_nodes (bool, optional): Whether to find all terminal nodes. Default is False.
|
|
119
|
+
solve_feasible_nodes (bool, optional): Whether to find all feasible nodes. Default is False.
|
|
120
|
+
solve_second_bnest (bool, optional): Whether to find the second best node. Default is False.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
np.ndarray: Optimal arrangements for the knapsack problem.
|
|
124
|
+
"""
|
|
125
|
+
if solve_terminal_nodes:
|
|
126
|
+
self.solve_terminal_nodes()
|
|
127
|
+
|
|
128
|
+
if solve_feasible_nodes:
|
|
129
|
+
self.solve_feasible_nodes()
|
|
130
|
+
|
|
131
|
+
self.solve_branch_and_bound(solve_second_best = solve_second_best)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def add(self, item: Item):
|
|
135
|
+
"""
|
|
136
|
+
Adds the specified item to the knapsack.
|
|
137
|
+
|
|
138
|
+
Parameters:
|
|
139
|
+
item (Item): The item to be added to the knapsack.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
np.ndarray: The updated knapsack state.
|
|
143
|
+
"""
|
|
144
|
+
if not isinstance(item, Item):
|
|
145
|
+
raise ValueError("`item` must be of type `Item`.")
|
|
146
|
+
if not item in self.items:
|
|
147
|
+
raise ValueError("`item` must be an existing `item` inside the `Knapsack` instance.")
|
|
148
|
+
self.state[np.where(self.items == item)[0][0]] = 1
|
|
149
|
+
self.__update_state()
|
|
150
|
+
return self.state
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def remove(self, item: Item):
|
|
154
|
+
"""
|
|
155
|
+
Removes the specified item from the knapsack.
|
|
156
|
+
|
|
157
|
+
Parameters:
|
|
158
|
+
item (Item): The item to be removed from the knapsack.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
np.ndarray: The updated knapsack state.
|
|
162
|
+
"""
|
|
163
|
+
if not isinstance(item, Item):
|
|
164
|
+
raise ValueError("`item` must be of type `Item`.")
|
|
165
|
+
if not item in self.items:
|
|
166
|
+
raise ValueError("`item` must be an existing `item` inside the `Knapsack` instance.")
|
|
167
|
+
|
|
168
|
+
self.state[np.where(self.items == item)] = 0
|
|
169
|
+
self.__update_state()
|
|
170
|
+
return self.state
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def set_state(self, state: np.ndarray):
|
|
174
|
+
"""
|
|
175
|
+
Sets the knapsack state using the provided binary array.
|
|
176
|
+
|
|
177
|
+
Parameters:
|
|
178
|
+
state (np.ndarray): Binary array indicating the inclusion/exclusion of items in the knapsack.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
np.ndarray: The updated knapsack state.
|
|
182
|
+
"""
|
|
183
|
+
self.state = state
|
|
184
|
+
self.__update_state()
|
|
185
|
+
return self.state
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def empty(self):
|
|
189
|
+
"""
|
|
190
|
+
Empties the knapsack by setting all items to be excluded.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
np.ndarray: The updated knapsack state.
|
|
194
|
+
"""
|
|
195
|
+
self.state = np.zeros_like(self.items)
|
|
196
|
+
self.__update_state()
|
|
197
|
+
return self.state
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def __update_state(self):
|
|
201
|
+
"""
|
|
202
|
+
Private method to update the knapsacks internal state.
|
|
203
|
+
"""
|
|
204
|
+
self.value = self.__calculate_value()
|
|
205
|
+
self.weight = self.__calculate_weight()
|
|
206
|
+
self.is_feasible = self.capacity >= self.weight
|
|
207
|
+
out_items = [self.items[i] for i, element in enumerate(self.state) if element == 0]
|
|
208
|
+
if sum(self.state) == len(self.state):
|
|
209
|
+
self.is_at_capacity = True
|
|
210
|
+
else:
|
|
211
|
+
self.is_at_capacity = min([
|
|
212
|
+
self.weight + item.weight
|
|
213
|
+
for item in out_items
|
|
214
|
+
]) > self.capacity
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def __calculate_value(self):
|
|
218
|
+
"""
|
|
219
|
+
Calculates the total value of items currently in the knapsack.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
float: The total value of items in the knapsack.
|
|
223
|
+
"""
|
|
224
|
+
mask = np.ma.make_mask(self.state, shrink=False)
|
|
225
|
+
return sum([item.value for item in self.items[mask]])
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def __calculate_weight(self):
|
|
229
|
+
"""
|
|
230
|
+
Calculates the total weight of items currently in the knapsack.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
float: The total weight of items in the knapsack.
|
|
234
|
+
"""
|
|
235
|
+
mask = np.ma.make_mask(self.state, shrink=False)
|
|
236
|
+
return sum([item.weight for item in self.items[mask]])
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def __calculate_upper_bound(
|
|
240
|
+
self,
|
|
241
|
+
included_items: np.ndarray[Item],
|
|
242
|
+
excluded_items: np.ndarray[Item]
|
|
243
|
+
) -> float:
|
|
244
|
+
"""
|
|
245
|
+
Calculates the upper bound of the supplied branch.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
included_items (np.ndarray[Item]): Items included by all nodes within the branch.
|
|
249
|
+
excluded_items (np.ndarray[Item]): Items excluded by all nodes within the branch.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
float: Upper bound of the branch.
|
|
253
|
+
"""
|
|
254
|
+
arrangement = Arrangement(
|
|
255
|
+
items = self.items,
|
|
256
|
+
state = np.array([int(item in included_items) for item in self.items])
|
|
257
|
+
)
|
|
258
|
+
candidate_items = np.array(sorted(
|
|
259
|
+
set(self.items) - set(included_items) - set(excluded_items),
|
|
260
|
+
key = lambda item: item.value/item.weight,
|
|
261
|
+
reverse = True
|
|
262
|
+
))
|
|
263
|
+
balance = self.capacity - arrangement.weight
|
|
264
|
+
|
|
265
|
+
if len(candidate_items) == 0:
|
|
266
|
+
upper_bound = arrangement.value
|
|
267
|
+
else:
|
|
268
|
+
i = 0
|
|
269
|
+
upper_bound = arrangement.value
|
|
270
|
+
while balance > 0 and i < len(candidate_items):
|
|
271
|
+
item = candidate_items[i]
|
|
272
|
+
added_weight = min(balance, item.weight)
|
|
273
|
+
upper_bound = upper_bound + added_weight * item.value / item.weight
|
|
274
|
+
balance = balance - added_weight
|
|
275
|
+
i += 1
|
|
276
|
+
return upper_bound
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def __explore_node(
|
|
280
|
+
self,
|
|
281
|
+
included_items: np.ndarray[Item],
|
|
282
|
+
excluded_items: np.ndarray[Item],
|
|
283
|
+
parent: Node,
|
|
284
|
+
upper_bound: float,
|
|
285
|
+
solve_second_best: bool,
|
|
286
|
+
):
|
|
287
|
+
"""
|
|
288
|
+
Determines weight/value of node and the upper bound of the branch containing the node. Prunes the branch if the upper bound is below the second-highest-valued terminal node discovered so far.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
included_items (np.ndarray[Item]): Items included in the node.
|
|
292
|
+
excluded_items (np.ndarray[Item]): Items excluded in the branch.
|
|
293
|
+
parent (Node): Parent of the node.
|
|
294
|
+
upper_bound (float): Upper bound of the node.
|
|
295
|
+
"""
|
|
296
|
+
arrangement = Arrangement(
|
|
297
|
+
items = self.items,
|
|
298
|
+
state = np.array([int(item in included_items) for item in self.items])
|
|
299
|
+
)
|
|
300
|
+
balance = self.capacity - arrangement.weight
|
|
301
|
+
if balance < 0:
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
if self.__is_subset_terminal(included_items):
|
|
305
|
+
self.__bb_minimum_values = sorted(
|
|
306
|
+
set([*self.__bb_minimum_values, arrangement.value])
|
|
307
|
+
)[-1 * (1 + int(solve_second_best)):]
|
|
308
|
+
|
|
309
|
+
node = Node(
|
|
310
|
+
name = {"state": arrangement.state, "value": arrangement.value},
|
|
311
|
+
items = arrangement.items,
|
|
312
|
+
state = arrangement.state,
|
|
313
|
+
value = arrangement.value,
|
|
314
|
+
weight = arrangement.weight,
|
|
315
|
+
upper_bound = upper_bound,
|
|
316
|
+
parent = parent,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if len(excluded_items) + len(included_items) < len(self.items):
|
|
320
|
+
next_item = self.items[len(excluded_items) + len(included_items)]
|
|
321
|
+
|
|
322
|
+
upper_bound = self.__calculate_upper_bound(
|
|
323
|
+
included_items = np.append(included_items, next_item),
|
|
324
|
+
excluded_items = excluded_items
|
|
325
|
+
)
|
|
326
|
+
if upper_bound > np.min(self.__bb_minimum_values):
|
|
327
|
+
self.__bb_queue = np.append(
|
|
328
|
+
self.__bb_queue,
|
|
329
|
+
{
|
|
330
|
+
"included_items": np.append(included_items, next_item),
|
|
331
|
+
"excluded_items": excluded_items,
|
|
332
|
+
"parent": node,
|
|
333
|
+
"upper_bound": upper_bound,
|
|
334
|
+
"solve_second_best": solve_second_best,
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
upper_bound = self.__calculate_upper_bound(
|
|
339
|
+
included_items = included_items,
|
|
340
|
+
excluded_items = np.append(excluded_items, next_item)
|
|
341
|
+
)
|
|
342
|
+
if upper_bound > np.min(self.__bb_minimum_values):
|
|
343
|
+
self.__bb_queue = np.append(
|
|
344
|
+
self.__bb_queue,
|
|
345
|
+
{
|
|
346
|
+
"included_items": included_items,
|
|
347
|
+
"excluded_items": np.append(excluded_items, next_item),
|
|
348
|
+
"parent": node,
|
|
349
|
+
"upper_bound": upper_bound,
|
|
350
|
+
"solve_second_best": solve_second_best,
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def solve_branch_and_bound(self, solve_second_best: bool):
|
|
356
|
+
"""
|
|
357
|
+
Solves the optimal and second-best terminal nodes using best-first branch-and-bound.
|
|
358
|
+
"""
|
|
359
|
+
self.items = np.array(sorted(
|
|
360
|
+
self.items,
|
|
361
|
+
key = lambda item: item.value/item.weight,
|
|
362
|
+
reverse = True
|
|
363
|
+
))
|
|
364
|
+
self.__bb_queue = np.array([])
|
|
365
|
+
self.__bb_minimum_values = np.array([-1])
|
|
366
|
+
initial_arrangement = Arrangement(
|
|
367
|
+
items = self.items,
|
|
368
|
+
state = np.zeros_like(self.items)
|
|
369
|
+
)
|
|
370
|
+
upper_bound = self.__calculate_upper_bound(
|
|
371
|
+
included_items = np.array([]),
|
|
372
|
+
excluded_items = np.array([])
|
|
373
|
+
)
|
|
374
|
+
root = Node(
|
|
375
|
+
name = {"state": initial_arrangement.state, "value": initial_arrangement.value},
|
|
376
|
+
items = self.items,
|
|
377
|
+
state = np.zeros_like(self.items),
|
|
378
|
+
value = 0,
|
|
379
|
+
weight = 0,
|
|
380
|
+
upper_bound = upper_bound,
|
|
381
|
+
parent = None,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
self.__bb_queue = np.append(
|
|
385
|
+
{
|
|
386
|
+
"included_items": np.array([self.items[0]]),
|
|
387
|
+
"excluded_items": np.array([]),
|
|
388
|
+
"parent": root,
|
|
389
|
+
"upper_bound": self.__calculate_upper_bound(
|
|
390
|
+
included_items = np.array([self.items[0]]),
|
|
391
|
+
excluded_items = np.array([]),
|
|
392
|
+
),
|
|
393
|
+
"solve_second_best": solve_second_best,
|
|
394
|
+
},
|
|
395
|
+
self.__bb_queue,
|
|
396
|
+
)
|
|
397
|
+
self.__bb_queue = np.append(
|
|
398
|
+
{
|
|
399
|
+
"included_items": np.array([]),
|
|
400
|
+
"excluded_items": np.array([self.items[0]]),
|
|
401
|
+
"parent": root,
|
|
402
|
+
"upper_bound": self.__calculate_upper_bound(
|
|
403
|
+
included_items = np.array([]),
|
|
404
|
+
excluded_items = np.array([self.items[0]]),
|
|
405
|
+
),
|
|
406
|
+
"solve_second_best": solve_second_best,
|
|
407
|
+
},
|
|
408
|
+
self.__bb_queue,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
while len(self.__bb_queue) > 0:
|
|
412
|
+
kwargs, self.__bb_queue = self.__bb_queue[0], self.__bb_queue[1:]
|
|
413
|
+
self.__explore_node(**kwargs)
|
|
414
|
+
|
|
415
|
+
self.__bb_queue = np.array(sorted(
|
|
416
|
+
[item for item in self.__bb_queue if item["upper_bound"] > np.min(self.__bb_minimum_values)],
|
|
417
|
+
key = lambda x: x["upper_bound"],
|
|
418
|
+
reverse = True
|
|
419
|
+
))
|
|
420
|
+
|
|
421
|
+
self.tree = root
|
|
422
|
+
nodes = sorted(
|
|
423
|
+
set([
|
|
424
|
+
(tuple(node.state), node.value)
|
|
425
|
+
for node in PreOrderIter(root)
|
|
426
|
+
if self.__is_subset_terminal(
|
|
427
|
+
[self.items[i] for i, inside in enumerate(node.state) if bool(inside)]
|
|
428
|
+
)
|
|
429
|
+
]),
|
|
430
|
+
key = lambda x: x[1],
|
|
431
|
+
reverse = True
|
|
432
|
+
)
|
|
433
|
+
self.optimal_nodes = np.array([
|
|
434
|
+
Arrangement(
|
|
435
|
+
items = self.items,
|
|
436
|
+
state = np.array(node[0])
|
|
437
|
+
) for node
|
|
438
|
+
in nodes
|
|
439
|
+
if node[1] == nodes[0][1]
|
|
440
|
+
])
|
|
441
|
+
self.sahni_k = self.calculate_sahni_k(self.optimal_nodes[0])
|
|
442
|
+
|
|
443
|
+
if solve_second_best:
|
|
444
|
+
self.terminal_nodes = np.append(
|
|
445
|
+
self.optimal_nodes,
|
|
446
|
+
np.array(Arrangement(
|
|
447
|
+
items = self.items,
|
|
448
|
+
state = np.array(nodes[len(self.optimal_nodes)][0])
|
|
449
|
+
) )
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def solve_terminal_nodes(self):
|
|
454
|
+
"""
|
|
455
|
+
Solves the knapsack problem and returns optimal arrangements.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
np.ndarray: Optimal arrangements for the knapsack problem.
|
|
459
|
+
"""
|
|
460
|
+
self.optimal_nodes = np.array([])
|
|
461
|
+
self.terminal_nodes = np.array([])
|
|
462
|
+
for i in range(1, len(self.items) + 1):
|
|
463
|
+
subsets = list(itertools.combinations(self.items, i))
|
|
464
|
+
for subset in subsets:
|
|
465
|
+
if self.__is_subset_terminal(subset):
|
|
466
|
+
self.terminal_nodes = np.append(
|
|
467
|
+
self.terminal_nodes,
|
|
468
|
+
Arrangement(
|
|
469
|
+
items = self.items,
|
|
470
|
+
state = np.array([int(item in subset) for item in self.items])
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
self.terminal_nodes = sorted(
|
|
475
|
+
self.terminal_nodes,
|
|
476
|
+
key = operator.attrgetter("value"),
|
|
477
|
+
reverse = True
|
|
478
|
+
)
|
|
479
|
+
self.optimal_nodes = np.array([
|
|
480
|
+
arrangement for arrangement
|
|
481
|
+
in self.terminal_nodes
|
|
482
|
+
if arrangement.value == self.terminal_nodes[0].value
|
|
483
|
+
])
|
|
484
|
+
self.sahni_k = self.calculate_sahni_k(self.optimal_nodes[0])
|
|
485
|
+
|
|
486
|
+
return self.optimal_nodes
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def solve_feasible_nodes(self) -> np.ndarray:
|
|
490
|
+
"""
|
|
491
|
+
Solves the knapsack problem and returns optimal arrangements.
|
|
492
|
+
|
|
493
|
+
Parameters:
|
|
494
|
+
verbose (bool, optional): If True, prints the string representation of the knapsack summary. Default is True.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
np.ndarray: Optimal arrangements for the knapsack problem.
|
|
498
|
+
"""
|
|
499
|
+
self.feasible_nodes = np.array([
|
|
500
|
+
Arrangement(
|
|
501
|
+
items = self.items,
|
|
502
|
+
state = np.zeros_like(self.items)
|
|
503
|
+
)
|
|
504
|
+
])
|
|
505
|
+
for i in range(1, len(self.items) + 1):
|
|
506
|
+
subsets = list(itertools.combinations(self.items, i))
|
|
507
|
+
for subset in subsets:
|
|
508
|
+
if self.__is_subset_feasible(subset):
|
|
509
|
+
self.feasible_nodes = np.append(
|
|
510
|
+
self.feasible_nodes,
|
|
511
|
+
Arrangement(
|
|
512
|
+
items = self.items,
|
|
513
|
+
state = np.array([int(item in subset) for item in self.items])
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
self.feasible_nodes = sorted(
|
|
518
|
+
self.feasible_nodes,
|
|
519
|
+
key = operator.attrgetter("value"),
|
|
520
|
+
)
|
|
521
|
+
return self.feasible_nodes
|
|
522
|
+
|
|
523
|
+
def solve_all_nodes(self) -> np.ndarray:
|
|
524
|
+
"""
|
|
525
|
+
Populates an array with all nodes in the knapsack problem.
|
|
526
|
+
"""
|
|
527
|
+
self.feasible_nodes = np.array([
|
|
528
|
+
Arrangement(
|
|
529
|
+
items = self.items,
|
|
530
|
+
state = np.zeros_like(self.items)
|
|
531
|
+
)
|
|
532
|
+
])
|
|
533
|
+
for i in range(1, len(self.items) + 1):
|
|
534
|
+
subsets = list(itertools.combinations(self.items, i))
|
|
535
|
+
for subset in subsets:
|
|
536
|
+
self.nodes = np.append(
|
|
537
|
+
self.nodes,
|
|
538
|
+
Arrangement(
|
|
539
|
+
items = self.items,
|
|
540
|
+
state = np.array([int(item in subset) for item in self.items])
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
self.feasible_nodes = sorted(
|
|
545
|
+
self.feasible_nodes,
|
|
546
|
+
key = operator.attrgetter("value"),
|
|
547
|
+
)
|
|
548
|
+
return self.feasible_nodes
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def __is_subset_feasible(self, subset: list[Item]) -> bool:
|
|
552
|
+
"""Private method to determine whether subset of items is feasible (below capacity limit).
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
subset (list[Item]): Subset of items.
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
bool: True if the node is terminal, otherwise False.
|
|
559
|
+
"""
|
|
560
|
+
weight = sum([i.weight for i in subset])
|
|
561
|
+
balance = self.capacity - weight
|
|
562
|
+
if balance < 0:
|
|
563
|
+
return False
|
|
564
|
+
return True
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def __is_subset_terminal(self, subset: list[Item]) -> bool:
|
|
568
|
+
"""Private method to determine whether subset of items is a terminal node.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
subset (list[Item]): Subset of items.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
bool: True if the node is terminal, otherwise False.
|
|
575
|
+
"""
|
|
576
|
+
weight = sum([i.weight for i in subset])
|
|
577
|
+
balance = self.capacity - weight
|
|
578
|
+
if balance < 0:
|
|
579
|
+
return False
|
|
580
|
+
remaining_items = set(self.items) - set(subset)
|
|
581
|
+
for i in remaining_items:
|
|
582
|
+
if i.weight <= balance:
|
|
583
|
+
return False
|
|
584
|
+
return True
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def calculate_sahni_k(self, arrangement: Arrangement):
|
|
588
|
+
"""
|
|
589
|
+
Calculates the Sahni-k value for a given arrangement.
|
|
590
|
+
|
|
591
|
+
Parameters:
|
|
592
|
+
arrangement (Arrangement): The arrangement for which to calculate Sahni-k.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
int: Sahni-k value.
|
|
596
|
+
"""
|
|
597
|
+
if not isinstance(arrangement, Arrangement):
|
|
598
|
+
raise ValueError("`arrangement` must be of type `Arrangement`.")
|
|
599
|
+
|
|
600
|
+
for subset_size in range(0, len(arrangement.state)+1):
|
|
601
|
+
in_indexes = [i for i, element in enumerate(arrangement.state) if element == 1]
|
|
602
|
+
for subset in itertools.combinations(in_indexes, subset_size):
|
|
603
|
+
initial_state = np.array([int(i in subset) for i in range(0, len(arrangement.state))])
|
|
604
|
+
self.set_state(initial_state.copy())
|
|
605
|
+
|
|
606
|
+
# Solve greedily
|
|
607
|
+
while not self.is_at_capacity:
|
|
608
|
+
out_items = [
|
|
609
|
+
self.items[i]
|
|
610
|
+
for i, element
|
|
611
|
+
in enumerate(self.state)
|
|
612
|
+
if element == 0 and self.items[i].weight + self.weight <= self.capacity
|
|
613
|
+
]
|
|
614
|
+
densities = [item.value/item.weight for item in out_items]
|
|
615
|
+
self.add(
|
|
616
|
+
out_items[densities.index(max(densities))]
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
if np.array_equal(arrangement.state, self.state):
|
|
620
|
+
return subset_size
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def plot_terminal_nodes_histogram(self):
|
|
624
|
+
"""
|
|
625
|
+
Plots a histogram of values for possible at-capacity arrangements.
|
|
626
|
+
"""
|
|
627
|
+
with plt.style.context(["nature"]):
|
|
628
|
+
fig, axes = plt.subplots(
|
|
629
|
+
figsize=(8, 3),
|
|
630
|
+
dpi=300,
|
|
631
|
+
nrows=1,
|
|
632
|
+
ncols=1,
|
|
633
|
+
constrained_layout=True
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
axes.hist(
|
|
637
|
+
[arrangement.value for arrangement in self.terminal_nodes],
|
|
638
|
+
bins=100,
|
|
639
|
+
color="#FF2C00",
|
|
640
|
+
alpha=0.7,
|
|
641
|
+
)
|
|
642
|
+
axes.set_ylabel("Number of solutions")
|
|
643
|
+
axes.set_xlabel("Solution value")
|
|
644
|
+
plt.show()
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def write_to_json(self, path: str):
|
|
648
|
+
"""
|
|
649
|
+
Writes the knapsack instance to .json.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
path (str): Filepath to output file.
|
|
653
|
+
"""
|
|
654
|
+
if self.optimal_nodes.size == 0:
|
|
655
|
+
self.solve(solve_second_best = False)
|
|
656
|
+
|
|
657
|
+
instance_spec = {
|
|
658
|
+
"capacity": self.capacity,
|
|
659
|
+
"sahni_k": self.calculate_sahni_k(self.optimal_nodes[0]),
|
|
660
|
+
"optimal_value": self.optimal_nodes[0].value,
|
|
661
|
+
"items": [
|
|
662
|
+
{
|
|
663
|
+
"id": i,
|
|
664
|
+
"value": item.value,
|
|
665
|
+
"weight": item.weight,
|
|
666
|
+
} for i, item in enumerate(self.items)
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
with open(path, "w") as f:
|
|
671
|
+
json.dump(instance_spec, f, indent = 4, default = int)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def load_from_json(self, path: str):
|
|
675
|
+
"""
|
|
676
|
+
Loads knapsack instance from a .json file.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
path (str): Filepath to instance .json file.
|
|
680
|
+
"""
|
|
681
|
+
with open(path) as f:
|
|
682
|
+
spec = json.load(f)
|
|
683
|
+
self.__init__(
|
|
684
|
+
items = np.array([Item(item["value"], item["weight"]) for item in spec["items"]]),
|
|
685
|
+
capacity = int(spec["capacity"])
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def summary(self):
|
|
690
|
+
"""
|
|
691
|
+
Generates a summary DataFrame containing information about the knapsack state and solutions.
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
pd.DataFrame: Summary DataFrame.
|
|
695
|
+
"""
|
|
696
|
+
best_inferior_solution = self.terminal_nodes[len(self.optimal_nodes)]
|
|
697
|
+
|
|
698
|
+
header = ", ".join([
|
|
699
|
+
f"C = {self.capacity}",
|
|
700
|
+
f"nC = {round(self.capacity / np.sum([item.weight for item in self.items]), 2)}",
|
|
701
|
+
f"nTerminal = {len(self.terminal_nodes)}",
|
|
702
|
+
f"nOptimal = {len(self.optimal_nodes)}",
|
|
703
|
+
f"Δ = {self.optimal_nodes[0].value - best_inferior_solution.value}",
|
|
704
|
+
f"Δ% = {(self.optimal_nodes[0].value - best_inferior_solution.value) / self.optimal_nodes[0].value:.3}"
|
|
705
|
+
])
|
|
706
|
+
columns = pd.MultiIndex.from_arrays([
|
|
707
|
+
[header] * len(self.items),
|
|
708
|
+
[i+1 for i, item in enumerate(self.items)]
|
|
709
|
+
])
|
|
710
|
+
|
|
711
|
+
rows = [
|
|
712
|
+
[item.value for item in self.items],
|
|
713
|
+
[item.weight for item in self.items],
|
|
714
|
+
[round(item.value / item.weight, 3) for item in self.items],
|
|
715
|
+
]
|
|
716
|
+
rows.extend([
|
|
717
|
+
np.where(arrangement.state == 1, "IN", "OUT")
|
|
718
|
+
for arrangement in self.optimal_nodes
|
|
719
|
+
])
|
|
720
|
+
rows.append(np.where(best_inferior_solution.state == 1, "IN", "OUT") )
|
|
721
|
+
|
|
722
|
+
index = ["v", "w", "density"]
|
|
723
|
+
index.extend([
|
|
724
|
+
", ".join([
|
|
725
|
+
f"solution (v = {arrangement.value}",
|
|
726
|
+
f"w = {arrangement.weight}",
|
|
727
|
+
f"k = {self.calculate_sahni_k(arrangement)})",
|
|
728
|
+
])
|
|
729
|
+
for arrangement in self.optimal_nodes
|
|
730
|
+
])
|
|
731
|
+
index.append(", ".join([
|
|
732
|
+
f"best inferior (v = {best_inferior_solution.value}",
|
|
733
|
+
f"w = {best_inferior_solution.weight}",
|
|
734
|
+
f"k = {self.calculate_sahni_k(best_inferior_solution)})",
|
|
735
|
+
]))
|
|
736
|
+
|
|
737
|
+
return pd.DataFrame(rows, columns=columns, index=index, dtype="object")
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def __str__(self):
|
|
741
|
+
return repr(self.summary())
|
|
742
|
+
|
pykp/sampler.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .knapsack import Knapsack
|
|
3
|
+
from .item import Item
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
class Arrangement():
|
|
7
|
+
def __init__(self, value):
|
|
8
|
+
self.value = value
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Sampler():
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
num_items: int,
|
|
15
|
+
normalised_capacity: float,
|
|
16
|
+
density_range: Tuple[float, float],
|
|
17
|
+
solution_value_range: Tuple[int, int],
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
A class for sampling knapsack instances.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
num_items (int): The number of items to sample.
|
|
24
|
+
normalised_capacity (float): The normalised capacity of the knapsack
|
|
25
|
+
(sum of weights / capacity constraint).
|
|
26
|
+
density_range (Tuple[float, float]): The range of item value densities
|
|
27
|
+
to sample from.
|
|
28
|
+
solution_value_range (Tuple[int, int]): The range of solution values
|
|
29
|
+
for the knapsack.
|
|
30
|
+
"""
|
|
31
|
+
self.num_items = num_items
|
|
32
|
+
self.normalised_capacity = normalised_capacity
|
|
33
|
+
self.density_range = density_range
|
|
34
|
+
self.solution_value_range = solution_value_range
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def sample(self) -> Knapsack:
|
|
38
|
+
"""
|
|
39
|
+
Samples a knapsack instance using the sampling criteria provided to
|
|
40
|
+
the sampler.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Knapsack: The sampled knapsack instance.
|
|
44
|
+
"""
|
|
45
|
+
weights = np.random.uniform(
|
|
46
|
+
low = 100,
|
|
47
|
+
high = 1000,
|
|
48
|
+
size = self.num_items,
|
|
49
|
+
)
|
|
50
|
+
densities = np.random.uniform(
|
|
51
|
+
low = self.density_range[0],
|
|
52
|
+
high = self.density_range[1],
|
|
53
|
+
size = self.num_items,
|
|
54
|
+
)
|
|
55
|
+
values = weights * densities
|
|
56
|
+
items = np.array([Item(int(values[i]), int(weights[i])) for i in range(self.num_items)])
|
|
57
|
+
|
|
58
|
+
sum_weights = np.sum([item.weight for item in items])
|
|
59
|
+
kp = Knapsack(
|
|
60
|
+
items = items,
|
|
61
|
+
capacity = int(self.normalised_capacity * sum_weights),
|
|
62
|
+
)
|
|
63
|
+
kp.solve()
|
|
64
|
+
solution_value = np.random.uniform(self.solution_value_range[0], self.solution_value_range[1])
|
|
65
|
+
scale_factor = solution_value / kp.optimal_nodes[0].value
|
|
66
|
+
scaled_items = np.array([
|
|
67
|
+
Item(
|
|
68
|
+
np.max([int(item.value * scale_factor), 1]),
|
|
69
|
+
np.max([int(item.weight * scale_factor), 1])
|
|
70
|
+
)
|
|
71
|
+
for item in items
|
|
72
|
+
])
|
|
73
|
+
scaled_items = np.array(
|
|
74
|
+
sorted(scaled_items, key=lambda item: item.value/item.weight, reverse=True)
|
|
75
|
+
)
|
|
76
|
+
scaled_kp = Knapsack(
|
|
77
|
+
items = scaled_items,
|
|
78
|
+
capacity = int(kp.capacity * scale_factor),
|
|
79
|
+
)
|
|
80
|
+
scaled_kp.solve()
|
|
81
|
+
return scaled_kp
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pykp
|
|
3
|
+
Version: 0.0.22
|
|
4
|
+
Summary: Tooling for sampling and solving instances of the 0-1 Knapsack Problem
|
|
5
|
+
Home-page: https://github.com/HRSAndrabi/pykp
|
|
6
|
+
Author: Hassan Andrabi
|
|
7
|
+
Author-email: hrs.andrabi@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: anytree >=2.12.1
|
|
15
|
+
Requires-Dist: pandas >=2.2.3
|
|
16
|
+
Requires-Dist: matplotlib ==3.9.2
|
|
17
|
+
Requires-Dist: numpy ==2.1.3
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest >=7.0 ; extra == 'dev'
|
|
20
|
+
Requires-Dist: twine >=4.0.2 ; extra == 'dev'
|
|
21
|
+
|
|
22
|
+
# PyKP: A Python Package for Knapsack Problem Solving
|
|
23
|
+
|
|
24
|
+
PyKP is a free and open-source library for sampling and solving instances of the knapsack problem. It provides tools to define knapsack instances, solve them efficiently, and analyse computational complexity metrics. You can also use `pykp` to sample knapsack problem instances based on specified distributions.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Define knapsack problem instances with custom items, weights, and values.
|
|
29
|
+
- Solve knapsack instances using branch-and-bound and other methods.
|
|
30
|
+
- Compute optimal and feasible solutions for different knapsack configurations.
|
|
31
|
+
- Analyse computational complexity metrics.
|
|
32
|
+
- Generate synthetic knapsack instances with custom weight, density, and solution value ranges.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
PyKP support Python version 3.12 and higher. To install PyKP, run
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
pip install pykp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Defining and Solving a Knapsack Problem
|
|
45
|
+
|
|
46
|
+
To start, define a knapsack problem with a set of items and solve it using the Knapsack class.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import numpy as np
|
|
50
|
+
from pykp import Knapsack
|
|
51
|
+
from pykp import Item
|
|
52
|
+
|
|
53
|
+
# Define items for the knapsack
|
|
54
|
+
items = np.array([
|
|
55
|
+
Item(value=10, weight=5),
|
|
56
|
+
Item(value=15, weight=10),
|
|
57
|
+
Item(value=7, weight=3)
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
# Initialise a Knapsack instance
|
|
61
|
+
capacity = 15
|
|
62
|
+
knapsack = Knapsack(items=items, capacity=capacity)
|
|
63
|
+
knapsack.solve()
|
|
64
|
+
|
|
65
|
+
# Display the optimal solution
|
|
66
|
+
print("Optimal Solution Value:", knapsack.optimal_nodes[0].value)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Generating Knapsack Instances with Sampler
|
|
70
|
+
|
|
71
|
+
The `Sampler` class allows you to generate knapsack instances based on specific ranges for item densities (value/weight ratio) and optimal solution values.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pykp import Sampler
|
|
75
|
+
|
|
76
|
+
# Initialise a Sampler instance with desired ranges
|
|
77
|
+
sampler = Sampler(
|
|
78
|
+
num_items=5,
|
|
79
|
+
normalised_capacity=0.6,
|
|
80
|
+
density_range=(0.5, 1.5),
|
|
81
|
+
solution_value_range=(100, 200)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Generate a sampled knapsack instance
|
|
85
|
+
sampled_knapsack = sampler.sample()
|
|
86
|
+
print("Sampled Knapsack Capacity:", sampled_knapsack.capacity)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Analysing Knapsack Solutions
|
|
90
|
+
|
|
91
|
+
The package provides methods to analyse the optimal solutions and other feasible arrangements.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# Display a summary of the knapsack solutions
|
|
95
|
+
print(sampled_knapsack.summary())
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
This project is licensed under the MIT License.
|
|
101
|
+
|
|
102
|
+
## Contributing
|
|
103
|
+
|
|
104
|
+
Contributions are welcome. Please fork the repository and submit a pull request.
|
|
105
|
+
|
|
106
|
+
## Contact
|
|
107
|
+
|
|
108
|
+
For questions or feedback, please reach out at hrs.andrabi@gmail.com.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pykp/__init__.py,sha256=QdffeMuDYTYwxcnVxj2YV3kVLNqotRFP21YTBRPftcM,119
|
|
2
|
+
pykp/arrangement.py,sha256=q_ZYKQ1u9FVTsJP9L76jtmNsoZDCMWZJadrZqaKL-P0,1999
|
|
3
|
+
pykp/item.py,sha256=5ZAHWFvQJjAwqwKevzstAhDWV89b3v07_lS2FFTtOgU,843
|
|
4
|
+
pykp/knapsack.py,sha256=FpuTEsIGZ_ncEVblCsxU9ydL8ZIeAdyxum2QLN62-8M,21334
|
|
5
|
+
pykp/sampler.py,sha256=-hp-lrV6Dl0sPEC2B5GOYlIQnUMnO9sPkLrqTkpbHdo,2202
|
|
6
|
+
pykp-0.0.22.dist-info/METADATA,sha256=X9Ax38eC5LKLgwYWtD5Poxn1H-L8kF_cPI-nlx5II_E,3183
|
|
7
|
+
pykp-0.0.22.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
8
|
+
pykp-0.0.22.dist-info/top_level.txt,sha256=kiQepwiX9Bp7ZxL_N3rQiI5mUV4d0kMfsSlyuimctnE,5
|
|
9
|
+
pykp-0.0.22.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pykp
|