|
| 1 | +''' |
| 2 | +Problem description |
| 3 | +
|
| 4 | +The problem solved in this example is a two-dimensional (2D) Two-Stage Cutting Stock Problem with Guillotine Cuts |
| 5 | +and flexible length. This problem involves cutting large stock sheets into smaller, ordered items using a 2-stage |
| 6 | +process to optimize material utilization. The aim is to fulfill specific order requirements by dividing each stock |
| 7 | +into levels and allocating ordered items within each level, while minimizing unused material. All cuts are guillotine |
| 8 | +cuts, which span the entire width or length of the stock. |
| 9 | +The problem is described in the following paper : |
| 10 | +"Luo, Yiqing L., and J. Christopher Beck. "Packing by Scheduling: Using Constraint Programming to Solve a Complex 2D |
| 11 | +Cutting Stock Problem." |
| 12 | +
|
| 13 | +Problem Constraints: |
| 14 | + 1. **Level Width Constraint**: This constraint ensures that each level within a stock can accommodate the width |
| 15 | + of all items assigned to it without exceeding the stock’s total width. The constraint is only active for levels |
| 16 | + that have items allocated, ensuring that material usage is limited to what's needed for active levels and avoiding |
| 17 | + unnecessary use of stock width. |
| 18 | +
|
| 19 | + 2. **Stock Length Constraint**: This constraint guarantees that the sum of lengths of all levels within a stock |
| 20 | + remains within the stock’s overall length. It’s applied only to stocks that are in use, making sure that material |
| 21 | + is efficiently allocated and that unused stock remains unaffected, thus optimizing space. |
| 22 | +
|
| 23 | + 3. **Stock Area Constraint**: This constraint controls the total area of each order placed within a stock, ensuring |
| 24 | + that the area assigned meets each order’s area requirements range. It enforces efficient use of stock by balancing |
| 25 | + order needs with stock capacity, reducing waste while fully satisfying each order’s area specifications. |
| 26 | +
|
| 27 | + 4. **Level Length Constraint**: The length of each level within a stock must be appropriately sized to fit the |
| 28 | + items allocated to it. This is achieved by enforcing a range bounds on the level's length: |
| 29 | +
|
| 30 | + Each level in a stock must be at least as long as the largest or the minimum required length of each order. |
| 31 | + Similarly, each level in a stock must be at most as long as the smallest of the maximum required length of each |
| 32 | + order. |
| 33 | +
|
| 34 | + This ensures that all items fit within the level without exceeding its boundaries. |
| 35 | +
|
| 36 | +Objective: |
| 37 | + The objective is to minimize unused material by reducing the waste area within each stock after fulfilling all |
| 38 | + order requirements. This optimization ensures efficient utilization of resources and cost savings. |
| 39 | +
|
| 40 | +Secondary Considerations: |
| 41 | + - Guillotine cuts are mandatory, meaning each cut goes entirely from one stock edge to the opposite edge, either |
| 42 | + horizontally or vertically, to align with practical cutting machinery requirements. |
| 43 | + - The solution also seeks to minimize the number of stock pieces used, promoting economical use of material. |
| 44 | +
|
| 45 | +''' |
| 46 | + |
| 47 | + |
| 48 | +import docplex.cp.model as cp |
| 49 | +from sys import stdout |
| 50 | + |
| 51 | +import numpy as np |
| 52 | +import math |
| 53 | +import random |
| 54 | +import time |
| 55 | + |
| 56 | +# Parameters |
| 57 | +TIME_LIMIT = 60 |
| 58 | +DISPLAY_SOLUTION = 0 |
| 59 | +LOG_VERBOSITY = 'Terse' |
| 60 | + |
| 61 | +# Objective weights |
| 62 | +ALPHA = 1 |
| 63 | +BETA = 1 |
| 64 | + |
| 65 | +# Precision for integer approximation of continuous variables |
| 66 | +PRECISION = 100 |
| 67 | + |
| 68 | + |
| 69 | +# Randomly generation of an instance |
| 70 | +def generate_instance(random_seed, num_stock, num_orders): |
| 71 | + random_numbers = np.random.default_rng(seed=random_seed) |
| 72 | + # Length of each stock |
| 73 | + L = random_numbers.integers(170, 200) |
| 74 | + # Width of each stock |
| 75 | + W = random_numbers.integers(100, 160, size=num_stock) |
| 76 | + # Width of each item in orders respectively |
| 77 | + O_w = random_numbers.integers(20, 60, size=num_orders) |
| 78 | + # Length of each item in orders respectively |
| 79 | + O_l = [np.sort(random_numbers.integers(30, 70, size = 2)) for i in range (num_orders)] |
| 80 | + O_an = [np.sort(random_numbers.integers(1, num_orders // 2 + 2, size = 2)) for i in range (num_orders)] |
| 81 | + # Area of each order respectively |
| 82 | + O_a = [[O_an[i][0] * O_w[i] * O_l[i][0], O_an[i][1] * O_w[i] * O_l[i][1]] for i in range(num_orders)] |
| 83 | + # Number of cuts of the cutting machine |
| 84 | + Cutters = math.floor(L/(min(map(min, O_l)))) |
| 85 | + return Cutters, num_stock, num_orders, L, W, O_w, O_l, O_a |
| 86 | + |
| 87 | +# Define general and restricted domains |
| 88 | +def y_i_domain(dom_restriction, data): |
| 89 | + # Recover data |
| 90 | + (NUM_CUTS, NUM_STOCK, NUM_ORDERS, STOCK_LENGTH, STOCK_WIDTH, ORDER_WIDTH, ORDER_LENGTH, ORDER_AREA) = data |
| 91 | + if dom_restriction == "restricted": |
| 92 | + return ((0, (int(min(map(min, ORDER_LENGTH))) * PRECISION, int(max(map(max, ORDER_LENGTH))) * PRECISION))) |
| 93 | + else: |
| 94 | + return ((0, (1, int(max(map(max, ORDER_LENGTH))) * PRECISION))) |
| 95 | + |
| 96 | + |
| 97 | + |
| 98 | +def create_decision_variables(dom_restriction, mdl, data): |
| 99 | + # Recover data |
| 100 | + (NUM_CUTS, NUM_STOCK, NUM_ORDERS, STOCK_LENGTH, STOCK_WIDTH, ORDER_WIDTH, ORDER_LENGTH, ORDER_AREA) = data |
| 101 | + |
| 102 | + MAX_LEVEL = NUM_CUTS + 1 |
| 103 | + eta = math.ceil(max(STOCK_WIDTH) / min(ORDER_WIDTH)) # Maximum number of partition in each level |
| 104 | + |
| 105 | + # Maximum number of partition in a level |
| 106 | + maxPartition = math.ceil(max(STOCK_WIDTH) / min(ORDER_WIDTH)) |
| 107 | + |
| 108 | + # Order allocation variables |
| 109 | + x = [[[mdl.integer_var(0, maxPartition, "x[{}][{}][{}]".format(i, j, k)) for k in range(0, NUM_STOCK)] |
| 110 | + for j in range(0, MAX_LEVEL)] for i in range(0, NUM_ORDERS)] |
| 111 | + |
| 112 | + # Level length: Float variables |
| 113 | + y = [[mdl.float_var(0, max(map(max, ORDER_LENGTH)), "y[{}][{}]".format(j, k)) |
| 114 | + for k in range(0, NUM_STOCK)] |
| 115 | + for j in range(0, MAX_LEVEL)] |
| 116 | + |
| 117 | + # Scaled level length: Integer variables |
| 118 | + y_i_domain = (0, (int(min(map(min, ORDER_LENGTH))) * PRECISION, int(max(map(max, ORDER_LENGTH))) * PRECISION)) |
| 119 | + y_integer = [ |
| 120 | + [mdl.integer_var(domain=y_i_domain, name="y_integer[{}][{}]".format(j, k)) |
| 121 | + for k in range(0, NUM_STOCK)] |
| 122 | + for j in range(0, MAX_LEVEL)] |
| 123 | + |
| 124 | + # Connect level length(y) and scaled level length(y_integer) |
| 125 | + for k in range(NUM_STOCK): |
| 126 | + for j in range(MAX_LEVEL): |
| 127 | + mdl.add(y[j][k] == y_integer[j][k] / PRECISION) |
| 128 | + |
| 129 | + # Binary variable s[k][j] is true (1) if stock level j on stock k is used, false (0) otherwise |
| 130 | + s = [[mdl.binary_var("s[{}][{}]".format(j, k)) for k in range(0, NUM_STOCK)] for j in range(0, MAX_LEVEL)] |
| 131 | + # Binary variable c[k] is true (1) if stock k is used, false (0) otherwise |
| 132 | + c = [mdl.binary_var("c[{}]".format(k)) for k in range(0, NUM_STOCK)] |
| 133 | + |
| 134 | + return x, y, s, c |
| 135 | + |
| 136 | + |
| 137 | +def create_cutting_stock_model(dom_restriction, mdl, indicator, vars, data): |
| 138 | + # Recover variables |
| 139 | + (x, y, s, c) = vars |
| 140 | + |
| 141 | + # Recover data |
| 142 | + (NUM_CUTS, NUM_STOCK, NUM_ORDERS, STOCK_LENGTH, STOCK_WIDTH, ORDER_WIDTH, ORDER_LENGTH, ORDER_AREA) = data |
| 143 | + |
| 144 | + MAX_LEVEL = NUM_CUTS + 1 |
| 145 | + |
| 146 | + # Recover variables |
| 147 | + (x, y, s, c) = vars |
| 148 | + |
| 149 | + # s_jk (binary variable definition) |
| 150 | + for j in range(MAX_LEVEL): |
| 151 | + for k in range(NUM_STOCK): |
| 152 | + mdl.add(s[j][k] == mdl.any([x[i][j][k] > 0 for i in range(NUM_ORDERS)])) |
| 153 | + |
| 154 | + # c_k (binary variable definition) |
| 155 | + for k in range(NUM_STOCK): |
| 156 | + mdl.add(c[k] == mdl.any([s[j][k] == 1 for j in range(MAX_LEVEL)])) |
| 157 | + |
| 158 | + # 1-Total width of items in each level should be within the width of the stock |
| 159 | + for j in range(MAX_LEVEL): |
| 160 | + for k in range(NUM_STOCK): |
| 161 | + mdl.add(sum([x[i][j][k] * ORDER_WIDTH[i] for i in range(NUM_ORDERS)]) <= STOCK_WIDTH[k] * s[j][k]) |
| 162 | + |
| 163 | + # 2-Total length of levels in each stock should be within length of the stock |
| 164 | + for k in range(NUM_STOCK): |
| 165 | + mdl.add(sum([y[j][k] for j in range(MAX_LEVEL)]) <= STOCK_LENGTH * c[k]) |
| 166 | + |
| 167 | + # 3-Total area of orders in each stock |
| 168 | + for i in range(NUM_ORDERS): |
| 169 | + mdl.add(mdl.range(sum(x[i][j][k] * y[j][k] for j in range(MAX_LEVEL) for k in range(NUM_STOCK)), |
| 170 | + min(ORDER_AREA[i]) / ORDER_WIDTH[i], |
| 171 | + max(ORDER_AREA[i]) / ORDER_WIDTH[i])) |
| 172 | + |
| 173 | + # 4-Level bounds depend on orders assigned to that level |
| 174 | + for j in range(MAX_LEVEL): |
| 175 | + for k in range(NUM_STOCK): |
| 176 | + for i in range(NUM_ORDERS): |
| 177 | + mdl.add((x[i][j][k] >= 1) <= mdl.range(y[j][k], min(ORDER_LENGTH[i]), max(ORDER_LENGTH[i]))) |
| 178 | + |
| 179 | + # 5-Optional symmetry-breaking constraints. |
| 180 | + # Breaking symmetries can make solution finding more complex (because this removes solutions |
| 181 | + # to the model) but helps to have optimality proofs faster. |
| 182 | + # 5a - Ordering levels by length on each stock |
| 183 | + for k in range(NUM_STOCK): |
| 184 | + for j in range(NUM_CUTS): |
| 185 | + mdl.add(y[j][k] >= y[j + 1][k]) |
| 186 | + |
| 187 | + # 5b - Order used stock first. |
| 188 | + for k in range(NUM_STOCK): |
| 189 | + for k_ in range(k+1, NUM_STOCK): |
| 190 | + if STOCK_WIDTH[k] == STOCK_WIDTH[k_]: |
| 191 | + mdl.add(c[k] >= c[k_]) |
| 192 | + |
| 193 | + # Objective function setup |
| 194 | + term1 = STOCK_LENGTH * mdl.sum(STOCK_WIDTH[k] * c[k] for k in range(NUM_STOCK)) |
| 195 | + term2 = mdl.sum( |
| 196 | + ORDER_WIDTH[i] * x[i][j][k] * y[j][k] for i in range(NUM_ORDERS) for j in range(MAX_LEVEL) for k in |
| 197 | + range(NUM_STOCK)) |
| 198 | + |
| 199 | + objective = ALPHA * term1 - BETA * term2 |
| 200 | + # Minimize the total wastage |
| 201 | + mdl.minimize(objective) |
| 202 | + |
| 203 | + |
| 204 | +def solveDisplay_singleData(dom_restriction, indicator, data, time_lmt): |
| 205 | + # Recover data |
| 206 | + (NUM_CUTS, NUM_STOCK, NUM_ORDERS, STOCK_LENGTH, STOCK_WIDTH, ORDER_WIDTH, ORDER_LENGTH, ORDER_AREA) = data |
| 207 | + MAX_LEVEL = NUM_CUTS + 1 |
| 208 | + mdl = cp.CpoModel('2d_2s_cutting_stock_problem') |
| 209 | + vars = create_decision_variables(dom_restriction, mdl, data) |
| 210 | + create_cutting_stock_model(dom_restriction, mdl, indicator, vars, data) |
| 211 | + sol = mdl.solve(LogVerbosity=LOG_VERBOSITY, TimeLimit=time_lmt) |
| 212 | + # Recover variables |
| 213 | + (x, y, s, c) = vars |
| 214 | + cvalues = [sol[v] for v in c] |
| 215 | + svalues = [[sol[v] for v in d1] for d1 in s] |
| 216 | + yvalues = np.array([[sol[v] for v in d1] for d1 in y]) |
| 217 | + xvalues = [[[sol[v] for v in d2] for d2 in d1] for d1 in x] |
| 218 | + stdout.write('\n') |
| 219 | + obj = sol.get_objective_values() |
| 220 | + time_sl = sol.get_solve_time() |
| 221 | + if not obj: |
| 222 | + print("Search terminated by limit, no solution found.") |
| 223 | + if obj: |
| 224 | + print("Solution found") |
| 225 | + print("~~~~~~~~~~~~~~") |
| 226 | + print("Area of trim loss: {}".format(obj[0])) |
| 227 | + print("Solve time: ", sol.get_solve_time()) |
| 228 | + print() |
| 229 | + print("Cutting strategy") |
| 230 | + print("~~~~~~~~~~~~~~~~~~~~") |
| 231 | + for k in range(NUM_STOCK): |
| 232 | + if cvalues[k] != 0: |
| 233 | + print("Levels of stock {}:".format(k + 1)) |
| 234 | + for j in range(MAX_LEVEL): |
| 235 | + if svalues[j][k] != 0: |
| 236 | + print(" Level", j, "has length", yvalues[:, k][j], ", orders assigned :") |
| 237 | + for i in range(NUM_ORDERS): |
| 238 | + if xvalues[i][j][k] == 1: |
| 239 | + print(" . 1 order of type", i) |
| 240 | + elif xvalues[i][j][k] > 1: |
| 241 | + print(" .", xvalues[i][j][k], "orders of type", i) |
| 242 | + |
| 243 | + |
| 244 | +data = generate_instance(3, 3, 5) |
| 245 | +solveDisplay_singleData("restricted", "with_symmetry", data, 120) |
| 246 | + |
0 commit comments