OR-Tools  9.6
cutting_stock_mo.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 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/status/status.h"
77 #include "absl/status/statusor.h"
79 #include "ortools/base/logging.h"
83 
84 namespace {
85 
87 constexpr double kInf = std::numeric_limits<double>::infinity();
88 
89 // piece_sizes and piece_demands must have equal length.
90 // every piece must have 0 < size <= board_length.
91 // every piece must have demand > 0.
92 struct CuttingStockInstance {
93  std::vector<int> piece_sizes;
94  std::vector<int> piece_demands;
95  int board_length;
96 };
97 
98 // pieces and quantity must have equal size.
99 // Defined for a related CuttingStockInstance, the total length all pieces
100 // weighted by their quantity must not exceed board_length.
101 struct Configuration {
102  std::vector<int> pieces;
103  std::vector<int> quantity;
104 };
105 
106 // configurations and quantity must have equal size.
107 // objective_value is the sum of the vales in quantity (how many total boards
108 // are used).
109 // To be feasible, the demand for each piece type must be met by the produced
110 // configurations.
111 struct CuttingStockSolution {
112  std::vector<Configuration> configurations;
113  std::vector<int> quantity;
114  int objective_value = 0;
115 };
116 
117 // Solves the worker problem.
118 //
119 // Solves the problem on finding the configuration (with its objective value) to
120 // add the to model that will give the greatest improvement in the LP
121 // relaxation. This is equivalent to a knapsack problem.
122 absl::StatusOr<std::pair<Configuration, double>> BestConfiguration(
123  const std::vector<double>& piece_prices,
124  const std::vector<int>& piece_sizes, const int board_size) {
125  int num_pieces = piece_prices.size();
126  CHECK_EQ(piece_sizes.size(), num_pieces);
127  math_opt::Model model("knapsack");
128  std::vector<math_opt::Variable> pieces;
129  for (int i = 0; i < num_pieces; ++i) {
130  pieces.push_back(
131  model.AddIntegerVariable(0, kInf, absl::StrCat("item_", i)));
132  }
133  model.Maximize(math_opt::InnerProduct(pieces, piece_prices));
134  model.AddLinearConstraint(math_opt::InnerProduct(pieces, piece_sizes) <=
135  board_size);
136  ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result,
137  math_opt::Solve(model, math_opt::SolverType::kCpSat));
138  if (solve_result.termination.reason !=
139  math_opt::TerminationReason::kOptimal) {
141  << "Failed to solve knapsack pricing problem to optimality: "
142  << solve_result.termination;
143  }
144  Configuration config;
145  for (int i = 0; i < num_pieces; ++i) {
146  const int use = static_cast<int>(
147  std::round(solve_result.variable_values().at(pieces[i])));
148  if (use > 0) {
149  config.pieces.push_back(i);
150  config.quantity.push_back(use);
151  }
152  }
153  return std::make_pair(config, solve_result.objective_value());
154 }
155 
156 // Solves the full cutting stock problem by decomposition.
157 absl::StatusOr<CuttingStockSolution> SolveCuttingStock(
158  const CuttingStockInstance& instance) {
159  math_opt::Model model("cutting_stock");
160  model.set_minimize();
161  const int n = instance.piece_sizes.size();
162  std::vector<math_opt::LinearConstraint> demand_met;
163  for (int i = 0; i < n; ++i) {
164  const int d = instance.piece_demands[i];
165  demand_met.push_back(model.AddLinearConstraint(d, d));
166  }
167  std::vector<std::pair<Configuration, math_opt::Variable>> configs;
168  auto add_config = [&](const Configuration& config) {
169  const math_opt::Variable v = model.AddContinuousVariable(0.0, kInf);
170  model.set_objective_coefficient(v, 1);
171  for (int i = 0; i < config.pieces.size(); ++i) {
172  const int item = config.pieces[i];
173  const int use = config.quantity[i];
174  if (use >= 1) {
175  model.set_coefficient(demand_met[item], v, use);
176  }
177  }
178  configs.push_back({config, v});
179  };
180 
181  // To ensure the leader problem is always feasible, begin a configuration for
182  // every item that has a single copy of the item.
183  for (int i = 0; i < n; ++i) {
184  add_config(Configuration{.pieces = {i}, .quantity = {1}});
185  }
186 
188  &model, math_opt::SolverType::kGlop));
189  int pricing_round = 0;
190  while (true) {
191  ASSIGN_OR_RETURN(math_opt::SolveResult solve_result, solver->Solve());
192  if (solve_result.termination.reason !=
193  math_opt::TerminationReason::kOptimal) {
195  << "Failed to solve leader LP problem to optimality at "
196  "iteration "
197  << pricing_round << " termination: " << solve_result.termination;
198  }
199  if (!solve_result.has_dual_feasible_solution()) {
200  // MathOpt does not require solvers to return a dual solution on optimal,
201  // but most LP solvers always will, see go/mathopt-solver-contracts for
202  // details.
204  << "no dual solution was returned with optimal solution at "
205  "iteration "
206  << pricing_round;
207  }
208  std::vector<double> prices;
209  for (const math_opt::LinearConstraint d : demand_met) {
210  prices.push_back(solve_result.dual_values().at(d));
211  }
213  (const auto [config, value]),
214  BestConfiguration(prices, instance.piece_sizes, instance.board_length));
215  if (value <= 1 + 1e-3) {
216  // The LP relaxation is solved, we can stop adding columns.
217  break;
218  }
219  add_config(config);
220  LOG(INFO) << "round: " << pricing_round
221  << " lp objective: " << solve_result.objective_value();
222  pricing_round++;
223  }
224  LOG(INFO) << "Done adding columns, switching to MIP";
225  for (const auto& [config, var] : configs) {
226  model.set_integer(var);
227  }
228  ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result,
229  math_opt::Solve(model, math_opt::SolverType::kCpSat));
230  switch (solve_result.termination.reason) {
231  case math_opt::TerminationReason::kOptimal:
232  case math_opt::TerminationReason::kFeasible:
233  break;
234  default:
236  << "Failed to solve final cutting stock MIP, termination: "
237  << solve_result.termination;
238  }
239  CuttingStockSolution solution;
240  for (const auto& [config, var] : configs) {
241  int use =
242  static_cast<int>(std::round(solve_result.variable_values().at(var)));
243  if (use > 0) {
244  solution.configurations.push_back(config);
245  solution.quantity.push_back(use);
246  solution.objective_value += use;
247  }
248  }
249  return solution;
250 }
251 
252 absl::Status RealMain() {
253  // Data from https://en.wikipedia.org/wiki/Cutting_stock_problem
254  CuttingStockInstance instance;
255  instance.board_length = 5600;
256  instance.piece_sizes = {1380, 1520, 1560, 1710, 1820, 1880, 1930,
257  2000, 2050, 2100, 2140, 2150, 2200};
258  instance.piece_demands = {22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20};
259  ASSIGN_OR_RETURN(CuttingStockSolution solution, SolveCuttingStock(instance));
260  std::cout << "Best known solution uses 73 rolls." << std::endl;
261  std::cout << "Total rolls used in actual solution found: "
262  << solution.objective_value << std::endl;
263  return absl::OkStatus();
264 }
265 
266 } // namespace
267 
268 int main(int argc, char** argv) {
269  InitGoogle(argv[0], &argc, &argv, true);
270  absl::Status result = RealMain();
271  if (!result.ok()) {
272  std::cout << result;
273  return 1;
274  }
275  return 0;
276 }
#define ASSIGN_OR_RETURN(lhs, rexpr)
static absl::StatusOr< std::unique_ptr< IncrementalSolver > > New(Model *model, SolverType solver_type, SolverInitArguments arguments={})
int main(int argc, char **argv)
int64_t value
IntVar * var
Definition: expr_array.cc:1874
GRBmodel * model
void InitGoogle(const char *usage, int *argc, char ***argv, bool deprecated)
Definition: init_google.h:34
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
LinearExpression InnerProduct(const LeftIterable &left, const RightIterable &right)
StatusBuilder InternalErrorBuilder()
const LinearConstraintMap< double > & dual_values() const
const VariableMap< double > & variable_values() const
double objective_value