Skip to content

Commit 3caea82

Browse files
dourouc05mlubin
authored andcommitted
Add an example of column generation (#2010)
* Restore the old PR. #2006 * Use the new function set_objective_coefficient. * Add the standard license header. * Remove the workaround for current GLPK.jl. * Update to JuMP 0.20.
1 parent 6968fbe commit 3caea82

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Copyright 2019, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
#############################################################################
6+
# JuMP
7+
# An algebraic modeling language for Julia
8+
# See http://github.com/JuliaOpt/JuMP.jl
9+
#############################################################################
10+
11+
# Based on http://doi.org/10.5281/zenodo.3329388
12+
13+
using JuMP, GLPK, SparseArrays, Test
14+
15+
"""
16+
solve_pricing(dual_demand_satisfaction, maxwidth, widths, rollcost, demand, prices)
17+
This function specifically implements the pricing problem for
18+
`example_cutting_stock`. It takes, as input, the dual solution from the master
19+
problem and the cutting stock instance. It outputs either a new cutting pattern,
20+
or `nothing` if no pattern could improve the current cost.
21+
"""
22+
function solve_pricing(dual_demand_satisfaction, maxwidth, widths, rollcost, demand, prices)
23+
reduced_costs = dual_demand_satisfaction + prices
24+
n = length(reduced_costs)
25+
26+
# The actual pricing model.
27+
submodel = Model(with_optimizer(GLPK.Optimizer))
28+
@variable(submodel, xs[1:n] >= 0, Int)
29+
@constraint(submodel, sum(xs .* widths) <= maxwidth)
30+
@objective(submodel, Max, sum(xs .* reduced_costs))
31+
32+
optimize!(submodel)
33+
34+
# If the net cost of this new pattern is nonnegative, no more patterns to add.
35+
new_pattern = round.(Int, value.(xs))
36+
net_cost = rollcost - sum(new_pattern .* (dual_demand_satisfaction .+ prices))
37+
38+
if net_cost >= 0 # No new pattern to add.
39+
return nothing
40+
else
41+
return new_pattern
42+
end
43+
end
44+
45+
"""
46+
example_cutting_stock(maxwidth, widths, rollcost, demand, prices)
47+
Solves the cutting stock problem (sometimes also called the cutting rod
48+
problem) using a column-generation technique.
49+
Intuitively, this problem is about cutting large rolls of paper into smaller
50+
pieces. There is an exact demand of pieces to meet, and all rolls have the
51+
same size. The goal is to meet the demand while maximising the profits (each
52+
paper roll has a fixed cost, each sold piece allows earning some money),
53+
which is roughly equivalent to using the smallest amount of rolls
54+
to cut (or, equivalently, to minimise the amount of paper waste).
55+
This function takes five parameters:
56+
* `maxwidth`: the maximum width of a roll (or length of a rod)
57+
* `widths`: an array of the requested widths
58+
* `rollcost`: the cost of a complete roll
59+
* `demand`: the demand, in number of pieces, for each width
60+
* `prices`: the selling price for each width
61+
Mathematically, this problem might be formulated with two variables:
62+
* `x[i, j] ∈ ℕ`: the number of times the width `i` is cut out of the roll `j`
63+
* `y[j] ∈ 𝔹`: whether the roll `j` is used
64+
Several constraints are needed:
65+
* the demand must be satisfied, for each width `i`:
66+
∑j x[i, j] = demand[i]
67+
* the roll size cannot be exceed, for each roll `j` that is used:
68+
∑i x[i, j] width[i] ≤ maxwidth y[j]
69+
If you want to implement this naïve model, you will need an upper bound on the
70+
number of rolls to use: the simplest one is to consider that each required
71+
width is cut from its own roll, i.e. `j` varies from 1 to ∑i demand[i].
72+
This example prefers a more advanced technique to solve this problem:
73+
column generation. It considers a different set of variables: patterns of
74+
width to cut a roll. The decisions then become the number of times each pattern
75+
is used (i.e. the number of rolls that are cut following this pattern).
76+
The intelligence comes from the way these patterns are chosen: not all of them
77+
are considered, but only the "interesting" ones, within the master problem.
78+
A "pricing" problem is used to decide whether a new pattern should be generated
79+
or not (it is implemented in the function `solve_pricing`). "Interesting" means,
80+
for a pattern, that the optimal solution may use this cutting pattern.
81+
In more details, the solving process is the following. First, a series of
82+
dumb patterns is generated (just one width per roll, repeated until the roll is
83+
completely cut). Then, the master problem is solved with these first patterns
84+
and its dual solution is passed on to the pricing problem. The latter decides
85+
if there is a new pattern to include in the formulation or not; if so,
86+
it returns it to the master problem. The master is solved again, the new dual
87+
variables are given to the pricing problem, until there is no more pattern to
88+
generate from the pricing problem: all "interesting" patterns have been
89+
generated, and the master can take its optimal decision. In the implementation,
90+
the variables deciding how many times a pattern is chosen are called `θ`.
91+
For more information on column-generation techniques applied on the cutting
92+
stock problem, you can see:
93+
* [Integer programming column generation strategies forthe cutting stock
94+
problem and its variants](https://tel.archives-ouvertes.fr/tel-00011657/document)
95+
* [Tackling the cutting stock problem](http://doi.org/10.5281/zenodo.3329388)
96+
"""
97+
function example_cutting_stock(; max_gen_cols::Int=5000)
98+
maxwidth = 100.0
99+
rollcost = 500.0
100+
prices = Float64[167.0, 197.0, 281.0, 212.0, 225.0, 111.0, 93.0, 129.0, 108.0, 106.0, 55.0, 85.0, 66.0, 44.0, 47.0, 15.0, 24.0, 13.0, 16.0, 14.0]
101+
widths = Float64[75.0, 75.0, 75.0, 75.0, 75.0, 53.8, 53.0, 51.0, 50.2, 32.2, 30.8, 29.8, 20.1, 16.2, 14.5, 11.0, 8.6, 8.2, 6.6, 5.1]
102+
demand = Int[38, 44, 30, 41, 36, 33, 36, 41, 35, 37, 44, 49, 37, 36, 42, 33, 47, 35, 49, 42]
103+
nwidths = length(prices)
104+
105+
n = length(widths)
106+
ncols = length(widths)
107+
108+
# Initial set of patterns (stored in a sparse matrix: a pattern won't include many different cuts).
109+
patterns = spzeros(UInt16, n, ncols)
110+
for i in 1:n
111+
patterns[i, i] = min(floor(Int, maxwidth / widths[i]), round(Int, demand[i]))
112+
end
113+
114+
# Write the master problem with this "reduced" set of patterns.
115+
# Not yet integer variables: otherwise, the dual values may make no sense
116+
# (actually, GLPK will yell at you if you're trying to get duals for integer problems)
117+
m = Model(with_optimizer(GLPK.Optimizer))
118+
@variable(m, θ[1:ncols] >= 0)
119+
@objective(m, Min,
120+
sum(θ[p] * (rollcost - sum(patterns[j, p] * prices[j] for j in 1:n)) for p in 1:ncols)
121+
)
122+
@constraint(m, demand_satisfaction[j=1:n], sum(patterns[j, p] * θ[p] for p in 1:ncols) >= demand[j])
123+
124+
# First solve of the master problem.
125+
optimize!(m)
126+
if termination_status(m) != MOI.OPTIMAL
127+
warn("Master not optimal ($ncols patterns so far)")
128+
end
129+
130+
# Then, generate new patterns, based on the dual information.
131+
while ncols - n <= max_gen_cols # Generate at most max_gen_cols columns.
132+
if ! has_duals(m)
133+
break
134+
end
135+
136+
new_pattern = solve_pricing(dual.(demand_satisfaction), maxwidth, widths, rollcost, demand, prices)
137+
138+
# No new pattern to add to the formulation: done!
139+
if new_pattern === nothing
140+
break
141+
end
142+
143+
# Otherwise, add the new pattern to the master problem, recompute the duals,
144+
# and go on waltzing one more time with the pricing problem.
145+
ncols += 1
146+
patterns = hcat(patterns, new_pattern)
147+
148+
# One new variable.
149+
new_var = @variable(m, [ncols], base_name="θ", lower_bound=0)
150+
push!(θ, new_var[ncols])
151+
152+
# Update the objective function.
153+
set_objective_coefficient(m, θ[ncols], rollcost - sum(patterns[j, ncols] * prices[j] for j=1:n))
154+
155+
# Update the constraint number j if the new pattern impacts this production.
156+
for j in 1:n
157+
if new_pattern[j] > 0
158+
set_normalized_coefficient(demand_satisfaction[j], new_var[ncols], new_pattern[j])
159+
end
160+
end
161+
162+
# Solve the new master problem to update the dual variables.
163+
optimize!(m)
164+
if termination_status(m) != MOI.OPTIMAL
165+
warn("Master not optimal ($ncols patterns so far)")
166+
end
167+
end
168+
169+
# Finally, impose the master variables to be integer and resolve.
170+
# To be exact, at each node in the branch-and-bound tree, we would need to restart
171+
# the column generation process (just in case a new column would be interesting
172+
# to add). This way, we only get an upper bound (a feasible solution).
173+
set_integer.(θ)
174+
optimize!(m)
175+
176+
if termination_status(m) != MOI.OPTIMAL
177+
warn("Final master not optimal ($ncols patterns)")
178+
end
179+
180+
@test JuMP.objective_value(m) 78374.0 atol = 1e-3
181+
end
182+
183+
example_cutting_stock()

0 commit comments

Comments
 (0)