OR-Tools  9.2
cutting_stock.cc
Go to the documentation of this file.
1// Copyright 2010-2021 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14// The Cutting Stock problem is as follows. You begin with unlimited boards, all
15// of the same length. You are also given a list of smaller pieces to cut out,
16// each with a length and a demanded quantity. You want to cut out all these
17// pieces using as few of your starting boards as possible.
18//
19// E.g. you begin with boards that are 20 feet long, and you must cut out 3
20// pieces that are 6 feet long and 5 pieces that are 8 feet long. An optimal
21// solution is:
22// [(6,), (8, 8) (8, 8), (6, 6, 8)]
23// (We cut a 6 foot piece from the first board, two 8 foot pieces from
24// the second board, and so on.)
25//
26// This example approximately solves the problem with a column generation
27// heuristic. The leader problem is a set cover problem, and the worker is a
28// knapsack problem. We alternate between solving the LP relaxation of the
29// leader incrementally, and solving the worker to generate new a configuration
30// (a column) for the leader. When the worker can no longer find a column
31// improving the LP cost, we convert the leader problem to a MIP and solve
32// again. We now give precise statements of the leader and worker.
33//
34// Problem data:
35// * l_i: the length of each piece we need to cut out.
36// * d_i: how many copies each piece we need.
37// * L: the length of our initial boards.
38// * q_ci: for configuration c, the quantity of piece i produced.
39//
40// Leader problem variables:
41// * x_c: how many copies of configuration c to produce.
42//
43// Leader problem formulation:
44// min sum_c x_c
45// s.t. sum_c q_ci * x_c = d_i for all i
46// x_c >= 0, integer for all c.
47//
48// The worker problem is to generate new configurations for the leader problem
49// based on the dual variables of the demand constraints in the LP relaxation.
50// Worker problem data:
51// * p_i: The "price" of piece i (dual value from leader's demand constraint)
52//
53// Worker decision variables:
54// * y_i: How many copies of piece i should be in the configuration.
55//
56// Worker formulation
57// max sum_i p_i * y_i
58// s.t. sum_i l_i * y_i <= L
59// y_i >= 0, integer for all i
60//
61// An optimal solution y* defines a new configuration c with q_ci = y_i* for all
62// i. If the solution has objective value <= 1, no further improvement on the LP
63// is possible. For additional background and proofs see:
64// https://people.orie.cornell.edu/shmoys/or630/notes-06/lec16.pdf
65// or any other reference on the "Cutting Stock Problem".
66//
67// Note: this problem is equivalent to symmetric bin packing:
68// https://en.wikipedia.org/wiki/Bin_packing_problem#Formal_statement
69// but typically in bin packing it is not assumed that you should exploit having
70// multiple items of the same size.
71#include <iostream>
72#include <limits>
73#include <utility>
74#include <vector>
75
76#include "absl/flags/parse.h"
77#include "absl/flags/usage.h"
78#include "absl/status/status.h"
79#include "absl/status/statusor.h"
84
85namespace {
86
88constexpr double kInf = std::numeric_limits<double>::infinity();
89
90// piece_sizes and piece_demands must have equal length.
91// every piece must have 0 < size <= board_length.
92// every piece must have demand > 0.
93struct CuttingStockInstance {
94 std::vector<int> piece_sizes;
95 std::vector<int> piece_demands;
96 int board_length;
97};
98
99// pieces and quantity must have equal size.
100// Defined for a related CuttingStockInstance, the total length all pieces
101// weighted by their quantity must not exceed board_length.
102struct Configuration {
103 std::vector<int> pieces;
104 std::vector<int> quantity;
105};
106
107// configurations and quantity must have equal size.
108// objective_value is the sum of the vales in quantity (how many total boards
109// are used).
110// To be feasible, the demand for each piece type must be met by the produced
111// configurations.
112struct CuttingStockSolution {
113 std::vector<Configuration> configurations;
114 std::vector<int> quantity;
115 int objective_value = 0;
116};
117
118// Solves the worker problem.
119//
120// Solves the problem on finding the configuration (with its objective value) to
121// add the to model that will give the greatest improvement in the LP
122// relaxation. This is equivalent to a knapsack problem.
123absl::StatusOr<std::pair<Configuration, double>> BestConfiguration(
124 const std::vector<double>& piece_prices,
125 const std::vector<int>& piece_sizes, const int board_size) {
126 int num_pieces = piece_prices.size();
127 CHECK_EQ(piece_sizes.size(), num_pieces);
128 math_opt::Model model("knapsack");
129 std::vector<math_opt::Variable> pieces;
130 for (int i = 0; i < num_pieces; ++i) {
131 pieces.push_back(
132 model.AddIntegerVariable(0, kInf, absl::StrCat("item_", i)));
133 }
134 model.Maximize(math_opt::InnerProduct(pieces, piece_prices));
135 model.AddLinearConstraint(math_opt::InnerProduct(pieces, piece_sizes) <=
136 board_size);
137 ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result,
138 math_opt::Solve(model, math_opt::SolverType::kCpSat));
139 if (solve_result.termination.reason !=
140 math_opt::TerminationReason::kOptimal) {
142 << "Failed to solve knapsack pricing problem: "
143 << solve_result.termination;
144 }
145 Configuration config;
146 for (int i = 0; i < num_pieces; ++i) {
147 const int use = static_cast<int>(
148 std::round(solve_result.variable_values().at(pieces[i])));
149 if (use > 0) {
150 config.pieces.push_back(i);
151 config.quantity.push_back(use);
152 }
153 }
154 return std::make_pair(config, solve_result.objective_value());
155}
156
157// Solves the full cutting stock problem by decomposition.
158absl::StatusOr<CuttingStockSolution> SolveCuttingStock(
159 const CuttingStockInstance& instance) {
160 math_opt::Model model("cutting_stock");
161 model.set_minimize();
162 const int n = instance.piece_sizes.size();
163 std::vector<math_opt::LinearConstraint> demand_met;
164 for (int i = 0; i < n; ++i) {
165 const int d = instance.piece_demands[i];
166 demand_met.push_back(model.AddLinearConstraint(d, d));
167 }
168 std::vector<std::pair<Configuration, math_opt::Variable>> configs;
169 auto add_config = [&](const Configuration& config) {
170 const math_opt::Variable v = model.AddContinuousVariable(0.0, kInf);
171 model.set_objective_coefficient(v, 1);
172 for (int i = 0; i < config.pieces.size(); ++i) {
173 const int item = config.pieces[i];
174 const int use = config.quantity[i];
175 if (use >= 1) {
176 model.set_coefficient(demand_met[item], v, use);
177 }
178 }
179 configs.push_back({config, v});
180 };
181
182 // To ensure the leader problem is always feasible, begin a configuration for
183 // every item that has a single copy of the item.
184 for (int i = 0; i < n; ++i) {
185 add_config(Configuration{.pieces = {i}, .quantity = {1}});
186 }
187
189 model, math_opt::SolverType::kGlop));
190 int pricing_round = 0;
191 while (true) {
192 ASSIGN_OR_RETURN(math_opt::SolveResult solve_result, solver->Solve());
193 if (solve_result.termination.reason !=
194 math_opt::TerminationReason::kOptimal) {
196 << "Failed to solve leader LP problem at iteration "
197 << pricing_round << " termination: " << solve_result.termination;
198 }
199 // GLOP always returns a dual solution on optimal
200 CHECK(solve_result.has_dual_feasible_solution());
201 std::vector<double> prices;
202 for (const math_opt::LinearConstraint d : demand_met) {
203 prices.push_back(solve_result.dual_values().at(d));
204 }
206 (const auto [config, value]),
207 BestConfiguration(prices, instance.piece_sizes, instance.board_length));
208 if (value <= 1 + 1e-3) {
209 // The LP relaxation is solved, we can stop adding columns.
210 break;
211 }
212 add_config(config);
213 LOG(INFO) << "round: " << pricing_round
214 << " lp objective: " << solve_result.objective_value();
215 pricing_round++;
216 }
217 LOG(INFO) << "Done adding columns, switching to MIP";
218 for (const auto& [config, var] : configs) {
219 model.set_integer(var);
220 }
221 ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result,
222 math_opt::Solve(model, math_opt::SolverType::kCpSat));
223 if (solve_result.termination.reason !=
224 math_opt::TerminationReason::kOptimal) {
226 << "Failed to solve final cutting stock MIP, termination: "
227 << solve_result.termination;
228 }
229 CuttingStockSolution solution;
230 for (const auto& [config, var] : configs) {
231 int use =
232 static_cast<int>(std::round(solve_result.variable_values().at(var)));
233 if (use > 0) {
234 solution.configurations.push_back(config);
235 solution.quantity.push_back(use);
236 solution.objective_value += use;
237 }
238 }
239 return solution;
240}
241
242absl::Status RealMain() {
243 // Data from https://en.wikipedia.org/wiki/Cutting_stock_problem
244 CuttingStockInstance instance;
245 instance.board_length = 5600;
246 instance.piece_sizes = {1380, 1520, 1560, 1710, 1820, 1880, 1930,
247 2000, 2050, 2100, 2140, 2150, 2200};
248 instance.piece_demands = {22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20};
249 ASSIGN_OR_RETURN(CuttingStockSolution solution, SolveCuttingStock(instance));
250 std::cout << "Best known solution uses 73 rolls." << std::endl;
251 std::cout << "Total rolls used in actual solution found: "
252 << solution.objective_value << std::endl;
253 return absl::OkStatus();
254}
255
256} // namespace
257
258int main(int argc, char** argv) {
260 absl::ParseCommandLine(argc, argv);
261 absl::Status result = RealMain();
262 if (!result.ok()) {
263 std::cout << result;
264 return 1;
265 }
266 return 0;
267}
#define CHECK(condition)
Definition: base/logging.h:495
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:702
#define LOG(severity)
Definition: base/logging.h:420
static absl::StatusOr< std::unique_ptr< IncrementalSolver > > New(Model &model, SolverType solver_type, SolverInitArguments arguments={})
Definition: solve.cc:165
int main(int argc, char **argv)
int64_t value
IntVar * var
Definition: expr_array.cc:1874
GRBmodel * model
const int INFO
Definition: log_severity.h:31
void InitGoogleLogging(const char *argv0)
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
Definition: solve.cc:155
LinearExpression InnerProduct(const LeftIterable &left, const RightIterable &right)
StatusBuilder InternalErrorBuilder()
StatusBuilder InvalidArgumentErrorBuilder()
#define ASSIGN_OR_RETURN(lhs, rexpr)
Definition: status_macros.h:48
const LinearConstraintMap< double > & dual_values() const
const VariableMap< double > & variable_values() const