|
| 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