Skip to content

Commit d546234

Browse files
authored
Merge pull request #99 from IBMDecisionOptimization/prepare_2.29.245
docplex 2.29.245
2 parents 2e90bae + dac3ef0 commit d546234

File tree

7 files changed

+263
-15
lines changed

7 files changed

+263
-15
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ These third-party dependencies are automatically installed with ``pip``
5151
- [requests](https://pypi.python.org/pypi/requests)
5252
- [six](https://pypi.python.org/pypi/six)
5353
- [certifi](https://pypi.python.org/pypi/certifi)
54-
- [chardet](https://pypi.python.org/pypi/chardet)
55-
- [idna](https://pypi.python.org/pypi/idna)
56-
- [urllib3](https://pypi.python.org/pypi/urllib3)
54+
5755

5856

5957
## License
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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+

examples/mp/jupyter/lagrangian_relaxation.ipynb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@
411411
"metadata": {},
412412
"outputs": [],
413413
"source": [
414+
"import builtins",
414415
"while loop_count <= max_iters:\n",
415416
" loop_count += 1\n",
416417
" # Rebuilt at each loop iteration\n",
@@ -441,7 +442,7 @@
441442
" else:\n",
442443
" # Update multipliers and start the loop again.\n",
443444
" scale_factor = 1.0 / float(loop_count)\n",
444-
" multipliers = [max(multipliers[i] - scale_factor * penalties[i], 0.) for i in c_range]\n",
445+
" multipliers = [builtins.max(multipliers[i] - scale_factor * penalties[i], 0.) for i in c_range]\n",
445446
" print('{0}> -- loop continues, m={1!s}, justifier={2:g}'.format(loop_count, multipliers, justifier))"
446447
]
447448
},

examples/mp/jupyter/marketing_campaign.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,9 +557,9 @@
557557
"\n",
558558
"from IPython.display import display\n",
559559
"try: \n",
560-
" display(offers.drop('customerid',1).sort_values(by='name')) #Pandas >= 0.17\n",
560+
" display(offers.drop('customerid',axis=1).sort_values(by='name')) #Pandas >= 0.17\n",
561561
"except:\n",
562-
" display(offers.drop('customerid',1).sort('name')) #Pandas < 0.17"
562+
" display(offers.drop('customerid',axis=1).sort('name')) #Pandas < 0.17"
563563
]
564564
},
565565
{
@@ -1279,7 +1279,7 @@
12791279
}
12801280
],
12811281
"source": [
1282-
"display(report_bd[report_bd['channel'] == \"seminar\"].drop('channel',1))"
1282+
"display(report_bd[report_bd['channel'] == \"seminar\"].drop('channel',axis=1))"
12831283
]
12841284
},
12851285
{

examples/mp/jupyter/nurses_scheduling.ipynb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,8 +1183,9 @@
11831183
}
11841184
],
11851185
"source": [
1186-
"min(shift_activities, key=lambda i: shift_activities[i].day_start_time)\n",
1187-
"min(s.day_start_time for s in shift_activities.values())"
1186+
"import builtins",
1187+
"builtins.min(shift_activities, key=lambda i: shift_activities[i].day_start_time)\n",
1188+
"builtins.min(s.day_start_time for s in shift_activities.values())"
11881189
]
11891190
},
11901191
{
@@ -1237,8 +1238,8 @@
12371238
" plt.xticks([shift_activities[s].day_start_time + w * 7 * 24 for w in [0,1] for (d, s) in shiftInfoByDay.items()],\n",
12381239
" [\"{}\".format(s.day) for w in [0,1] for (d, s) in shiftInfoByDay.items()])\n",
12391240
"\n",
1240-
" plt.xlim([min(s.day_start_time for s in shift_activities.values()) - 6,\n",
1241-
" max(s.day_start_time for s in shift_activities.values()) + 30])\n",
1241+
" plt.xlim([builtins.min(s.day_start_time for s in shift_activities.values()) - 6,\n",
1242+
" builtins.max(s.day_start_time for s in shift_activities.values()) + 30])\n",
12421243
" ax.set_xlabel(xlabel)\n",
12431244
" ax.set_ylabel(ylabel)\n",
12441245
" ax.grid()\n",
@@ -1267,7 +1268,7 @@
12671268
"# Plot number of assigned nurses for each shift, by department\n",
12681269
"def displayDepartmentsAssignments(ax):\n",
12691270
" ylabels, tickloc = [], []\n",
1270-
" maxNbAssignements = max(nbAssignmentsByShift.values())\n",
1271+
" maxNbAssignements = builtins.max(nbAssignmentsByShift.values())\n",
12711272
" for i, d in enumerate(departments):\n",
12721273
" for s in shiftsByDepartment[d]:\n",
12731274
" shift_activity = shift_activities[s]\n",

examples/mp/jupyter/oil_blending.ipynb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,7 @@
924924
}
925925
],
926926
"source": [
927+
"import builtins",
927928
"f, barplot = plt.subplots(1, figsize=(16,5))\n",
928929
"\n",
929930
"bar_width = 0.1\n",
@@ -933,7 +934,7 @@
933934
"# position of left-bar boundaries\n",
934935
"bar_l = [o for o in range_oil]\n",
935936
"\n",
936-
"mbar_w = 3*bar_width+2*max(0, offset-bar_width)\n",
937+
"mbar_w = 3*bar_width+2*builtins.max(0, offset-bar_width)\n",
937938
"\n",
938939
"tick_pos = [b*rho + mbar_w/2.0 for b in bar_l]\n",
939940
"\n",
@@ -951,7 +952,7 @@
951952
" \n",
952953
"\n",
953954
"# Set a buffer around the edge\n",
954-
"plt.xlim([0, max(tick_pos)+mbar_w +0.5])\n",
955+
"plt.xlim([0, builtins.max(tick_pos)+mbar_w +0.5])\n",
955956
"\n",
956957
"plt.show()"
957958
]

examples/mp/workflow/lagrangian_relaxation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# --------------------------------------------------------------------------
66

77
import json
8+
import builtins
89

910
from docplex.util.environment import get_environment
1011
from docplex.mp.model import Model
@@ -110,7 +111,7 @@ def run_GAP_model_with_Lagrangian_relaxation(As, Bs, Cs, max_iters=101, **kwargs
110111
else:
111112
# update multipliers and start loop again.
112113
scale_factor = 1.0 / float(loop_count)
113-
multipliers = [max(multipliers[i] - scale_factor * penalties[i], 0.) for i in c_range]
114+
multipliers = [builtins.max(multipliers[i] - scale_factor * penalties[i], 0.) for i in c_range]
114115
print('{0}> -- loop continues, m={1!s}, justifier={2:g}'.format(loop_count, multipliers, justifier))
115116

116117
return best

0 commit comments

Comments
 (0)