-
Notifications
You must be signed in to change notification settings - Fork 3
/
pattern_generation.py
163 lines (136 loc) · 5.82 KB
/
pattern_generation.py
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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from math import floor
from amplpy import AMPL
import amplpy_gurobi as ampls
SOLVER = "gurobi"
# Example description
# Solves a cutting stock problem using two AMPL problems for pattern generation
# and shows how to terminate the solution of the final MIP with a callback by
# increasing the allowed MIP gap depending on how many solutions have been found
def create_models() -> tuple:
master="""param nPatterns integer > 0;
set PATTERNS = 1..nPatterns; # patterns
set WIDTHS; # finished widths
param order {WIDTHS} >= 0; # rolls of width j ordered
param overrun; # permitted overrun on any width
param rawWidth; # width of raw rolls to be cut
param rolls {WIDTHS,PATTERNS} >= 0, default 0;
# rolls of width i in pattern j
var Cut {PATTERNS} integer >= 0; # raw rolls to cut in each pattern
minimize TotalRawRolls: sum {p in PATTERNS} Cut[p];
subject to OrderLimits {w in WIDTHS}:
order[w] <= sum {p in PATTERNS} rolls[w,p] * Cut[p] <= order[w] + overrun;"""
subproblem = """set SIZES; # ordered widths
param cap >= 0; # width of raw rolls
param val {SIZES}; # shadow prices of ordered widths (from Master)
var Qty {SIZES} integer >= 0; # number of each width in generated pattern
maximize TotVal: sum {s in SIZES} val[s] * Qty[s];
subject to Cap: sum {s in SIZES} s * Qty[s] <= cap;"""
# Data
roll_width = 6000
overrun = 30
orders = {
1630: 172, 1625: 714, 1620: 110, 1617: 262, 1540: 32, 1529: 100, 1528: 76,
1505: 110, 1504: 20, 1484: 58, 1466: 15, 1450: 10, 1283: 40, 1017: 50,
970: 70, 930: 8, 916: 210, 898: 395, 894: 49, 881: 17, 855: 20, 844: 10,
805: 718, 787: 17, 786: 710, 780: 150, 754: 34, 746: 15, 707: 122, 698: 7,
651: 10, 644: 15, 638: 10, 605: 10, 477: 4, 473: 34, 471: 25, 468: 10,
460: 908, 458: 161, 453: 765, 447: 21, 441: 20, 422: 318, 421: 22,
419: 382, 396: 22, 309: 123,266: 35
}
widths = list(sorted(orders.keys(), reverse=True))
# Create master
Master = AMPL()
Master.eval(master)
# Send scalar values
Master.param["nPatterns"] = len(widths)
Master.param["overrun"] = overrun
Master.param["rawWidth"] = roll_width
# Send order vector
Master.set["WIDTHS"] = widths
Master.param["order"] = orders
# Generate and send initial pattern matrix
Master.param["rolls"] = {
(widths[i], 1+i): int(floor(roll_width/widths[i]))
for i in range(len(widths))
}
# Define a param for sending new patterns to master problem
Master.eval("param newPat {WIDTHS} integer >= 0;")
# Set solve options
Master.option["solver"] = SOLVER
Master.option["relax_integrality"] = 1
# Create subproblem
Sub = AMPL()
Sub.eval(subproblem)
Sub.set["SIZES"] = widths
Sub.param["cap"] = roll_width
Sub.option["solver"] = SOLVER
return Master,Sub
Master,Sub = create_models()
# Main cycle, solves the relaxed master and use the duals
# to populate the subproblem.
# Solve the subproblem to generate new patterns and add them
# to the master
while True:
Master.solve()
Sub.param["val"].setValues(Master.con["OrderLimits"].getValues())
Sub.solve()
if Sub.obj["TotVal"].value() <= 1.00001:
break
Master.param["newPat"].setValues(Sub.var["Qty"].getValues())
Master.eval("let nPatterns := nPatterns + 1;")
Master.eval("let {w in WIDTHS} rolls[w, nPatterns] := newPat[w];")
# At this point, we export the non-relaxed master problem
# and solve with ampls
Master.option["relax_integrality"] = 0
# Export model to ampls
# If we plan to import the results back to AMPL, we have to explicitly set what additional
# suffixes we want returned at this stage
# In this case, we want to return the mip gap as a suffix
ampls_model = Master.to_ampls(SOLVER, ["return_mipgap=5"])
class MyCallback(ampls.GenericCallback):
"""Callback implementing the stopping rule"""
def __init__(self, stoprule):
super(MyCallback, self).__init__()
self._stoprule = stoprule
self._current = 0
self._continueOpt = True
def setCurrentGap(self):
print("Increasing gap tolerance to %.2f%%" % \
(100*self._stoprule["gaptol"][self._current]))
ampls_model.setAMPLParameter(ampls.SolverParams.DBL_MIPGap,
self._stoprule["gaptol"][self._current])
self._current += 1
def run(self):
where = self.getAMPLWhere()
if where == ampls.Where.MIPNODE:
runtime = self.getValue(ampls.Value.RUNTIME)
if runtime >= self._stoprule["time"][self._current]:
print(f"Current is: {self._stoprule['time'][self._current]}")
print(f"Stopping optimization at {runtime} seconds")
self._continueOpt = True
return -1
return 0
# Callback"s stopping rule is created here...
stopdict = { "time" : ( 1, 2, 3, 60 ),
"gaptol" : ( .0002, .002, .02, .1 )
}
# ...and initialized in the constructor
callback = MyCallback(stopdict)
ampls_model.setCallback(callback)
# Invoke solver
# Most solvers (e.g. Gurobi https://support.gurobi.com/hc/en-us/articles/360039233191-How-do-I-change-parameters-in-a-callback-)
# do not support changing optimization parameters from a callback
# Instead we have to stop the optimization, change the desired parameters
# and start it again
callback._continueOpt = True
while callback._continueOpt:
callback._continueOpt = False
ampls_model.optimize()
if callback._continueOpt:
callback.setCurrentGap()
# Import solution from the solver
Master.importSolution(ampls_model)
print(f"From AMPL MIPGap={100*Master.getValue('TotalRawRolls.relmipgap'):.3f}%")
print(f"Objective value: {Master.getValue('TotalRawRolls')}")