This repository was archived by the owner on Jun 5, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdyckhoff.py
More file actions
executable file
·177 lines (138 loc) · 7.22 KB
/
dyckhoff.py
File metadata and controls
executable file
·177 lines (138 loc) · 7.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/usr/bin/env python3
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU
# General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not,
# see <https://www.gnu.org/licenses/>.
from collections.abc import Iterable
from typing import Optional
CutVar = tuple[float, float]
""" A variable that represents a one-cut [l;k]. """
VarSum = dict[CutVar, float]
"""
A sum of variables is a dictionary where variables are associated to their linear coefficients.
"""
Containers = dict[float, Optional[int]]
""" Every container length associated to its available number (None for no upper bound). """
Items = dict[float, int]
""" Every item length associated to the number of that item that needs to be packed. """
def calculate_residuals(containers: Containers, items: Items) -> set[float]:
""" Calculates the set of residual lengths from the containers and items. """
min_items = min(items)
all_to_cut = list(containers)
residuals = set()
while len(all_to_cut) > 0:
to_cut = all_to_cut[0]
for item in items:
if to_cut > item:
cut = to_cut - item
residuals.add(item)
if cut >= min_items:
if cut not in residuals:
all_to_cut.append(cut)
residuals.add(cut)
all_to_cut = all_to_cut[1:]
return residuals
def merge_sums(sums: Iterable[VarSum]) -> VarSum:
""" Sums many variable sums together. """
final_sum: VarSum = {}
for summation in sums:
for variable, coefficient in summation.items():
final_coefficient = final_sum.get(variable)
final_sum[variable] = \
coefficient if final_coefficient is None else final_coefficient + coefficient
# Remove variables with coefficient zero and with redundant names (e.g.: y7_5 and y7_2)
ret = {variable: coefficient for variable, coefficient in final_sum.items() if variable != 0}
return {(l, k): coefficient for (l, k), coefficient in ret.items() if \
not (k < l - k and (l, l - k) in ret)}
def output_variable(variable: CutVar, all_variables: set[str], items: Items) -> str:
"""
Outputs LP code for a variable representing a cut. all_variables will be filled with all
variables ever outputted, so they can later be declared to be integers later on.
"""
l, k = variable
ret = f'y{l}_{k}' if k in items else f'y{l}_{l - k}'
all_variables.add(ret)
return ret
def output_sum(summation: VarSum, all_variables: set[str], items: Items) -> str:
"""
Outputs LP code with a sum of variables and their linear coefficients. all_variables will be
filled with allvariables ever outputted, so they can later be declared to be integers.
"""
ret = ''
for variable, coefficient in sorted(summation.items(), key=lambda item: item[0], reverse=True):
str_coefficient = '' if coefficient == 1 else f'{coefficient} '
ret += f'{str_coefficient}{output_variable(variable, all_variables, items)} + '
return ret[:-3]
def output_objective(containers: Containers, \
items: Items, \
residuals: set[float], \
all_variables: set[str]) -> str:
"""
Outputs LP code with the objective function of the problem. all_variables will be filled
with all variables ever outputted, so they can later be declared to be integers.
"""
ret = '/* Standardize optimizations of max{0, ...} */'
stock_residuals_union = set(containers.keys()).union(residuals)
for l in containers:
b_sum = {(k + l, k): -1 for k in items if k + l in stock_residuals_union}
c_sum = {(l, k): 1 for k in items if k < l}
ret += f'\nm{l} >= {output_sum(merge_sums([b_sum, c_sum]), all_variables, items)};'
objective = " + ".join([f'{l} m{l}' for l in containers])
return f'min: {objective};\n\n{ret}'
def output_balance_restrictions(containers: Containers, \
items: Items, \
residuals: set[float], \
all_variables: set[str]) -> str:
"""
Outputs LP code with all balance restrictions for the problem. all_variables will be filled
with all variables ever outputted, so they can later be declared to be integers.
"""
ret = '/* Balance restrictions */'
stock_residuals_union = set(containers.keys()).union(residuals)
l_values = sorted(set(items.keys()).union(residuals).difference(set(containers.keys())))
for l in l_values:
a_set = {k for k in stock_residuals_union if k > l} if l in items else set()
a_sum = {(k, l): 1 for k in a_set}
b_sum = {(k + l, k): 1 for k in items if k + l in stock_residuals_union}
c_sum = {(l, k): -1 for k in items if k < l}
summation = merge_sums([a_sum, b_sum, c_sum])
ret += f'\nl{l}: {output_sum(summation, all_variables, items)} >= {items.get(l, 0)};'
return ret
def output_container_count_bounds(containers: Containers, \
items: Items, \
residuals: set[float], \
all_variables: set[str]) -> str:
""" Outputs LP code limiting the number of containers of each type to their upper bound. """
ret = '/* Container upper bounds */'
stock_residuals_union = set(containers.keys()).union(residuals)
for l, upper_bound in containers.items():
if upper_bound is not None:
b_sum = {(k + l, k): -1 for k in items if k + l in stock_residuals_union}
c_sum = {(l, k): 1 for k in items if k < l}
summation = merge_sums([b_sum, c_sum])
ret += f'\nc{l}: {output_sum(summation, all_variables, items)} <= {upper_bound};'
return ret
def output_integer_restrictions(all_variables: set[str]) -> str:
""" Outputs LP code telling that all variables ever outputted are integers. """
return 'int ' + ', '.join(sorted(all_variables)) + ';'
def output_model(containers: Containers, items: Items) -> str:
""" Outputs LP code modelling a give bin-packing problem. """
ret = ''
all_variables: set[str] = set()
residuals = calculate_residuals(containers, items)
ret += f'{output_objective(containers, items, residuals, all_variables)}\n\n'
ret += f'{output_balance_restrictions(containers, items, residuals, all_variables)}\n\n'
ret += f'{output_container_count_bounds(containers, items, residuals, all_variables)}\n\n'
ret += f'{output_integer_restrictions(all_variables)}\n'
return ret
if __name__ == '__main__':
# Our problem's data
print(output_model({11: None, 10: 5, 7: 5}, {2: 13, 4: 9, 5: 5}), end='')
# Example from Dyckhoff's one-cut model:
# print(output_model({5: None, 6: None, 9: None}, {2: 20, 3: 10, 4: 20}), end='')