OR-Tools  9.6
lagrangian_relaxation_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 // Solves a constrained shortest path problem via Lagrangian Relaxation. The
15 // Lagrangian dual is solved with subgradient ascent.
16 //
17 // Problem data:
18 // * N: set of nodes.
19 // * A: set of arcs.
20 // * R: set of resources.
21 // * c_(i,j): cost of traversing arc (i,j) in A.
22 // * r_(i,j,k): resource k spent by traversing arc (i,j) in A, for all k in R.
23 // * b_i: flow balance at node i in N (+1 at the source, -1 at the sink, and 0
24 // otherwise).
25 // * r_max_k: availability of resource k for a path, for all k in R.
26 //
27 // Decision variables:
28 // * x_(i,j): flow through arc (i,j) in A.
29 //
30 // Formulation:
31 // Z = min sum(c_(i,j) * x_(i,j): (i,j) in A)
32 // s.t.
33 // sum(x_(i,j): (i,j) in A) - sum(x_(j,i): (j,i) in A) = b_i for all i in N,
34 // sum(r_(i,j,k) * x_(i,j): (i,j) in A) <= r_max_k for all k in R,
35 // x_(i,j) in {0,1} for all (i,j) in A.
36 //
37 // Upon dualizing a subset of the constraints (here we chose to relax some or
38 // all of the knapsack constraints), we obtaing a subproblem parameterized by
39 // dual variables mu (one per dualized constraint). We refer to this as the
40 // Lagrangian subproblem. Let R+ be the set of knapsack constraints that we
41 // keep, and R- the set of knapsack constraints that get dualized. The
42 // Lagrangian subproblem follows:
43 //
44 // z(mu) = min sum(
45 // (c_(i,j) - sum(mu_k * r_(i,j,k): k in R)) * x_(i,j): (i,j) in A)
46 // + sum(mu_k * r_max_k: k in R-)
47 // s.t.
48 // sum(x_(i,j): (i,j) in A) - sum(x_(j,i): (j,i) in A) = b_i for all i in N,
49 // sum(r_(i,j,k) * x_(i,j): (i,j) in A) <= r_max_k for all k in R+,
50 // x_(i,j) in {0,1} for all (i,j) in A.
51 //
52 // We seek to solve the Lagrangian dual, which is of the form:
53 // Z_D = max{ z(mu) : mu <=0 }. Concavity of z(mu) allows us to solve the
54 // Lagrangian dual with the iterates:
55 // mu_(t+1) = mu_t + step_size_t * grad_mu_t, where
56 // grad_mu_t = r_max - sum(t_(i,j) * x_(i,j)^t: (i,j) in A) is a subgradient of
57 // z(mu_t) and x^t is an optimal solution to the problem induced by z(mu_t).
58 //
59 // In general we have that Z_D <= Z. For convex problems, Z_D = Z. For MIPs,
60 // Z_LP <= Z_D <= Z, where Z_LP is the linear relaxation of the original
61 // problem.
62 //
63 // In this particular example, we use two resource constraints. Either
64 // constraint or both can be dualized via the flags `dualize_resource_1` and
65 // `dualize_resource_2`. If both constraints are dualized, we have that Z_LP =
66 // Z_D because the resulting Lagrangian subproblem can be solved as a linear
67 // program (i.e., the problem becomes a pure shortest path problem upon
68 // dualizing all the side constraints). When only one of the side constraints is
69 // dualized, we can have Z_LP <= Z_D because the resulting Lagrangian subproblem
70 // needs to be solved as an MIP. For the particular data used in this example,
71 // dualizing only the first resource constraint leads to Z_LP < Z_D, while
72 // dualizing only the second resource constraint leads to Z_LP = Z_D. In either
73 // case, solving the Lagrandual dual also provides an upper bound to Z.
74 //
75 // Usage: blaze build -c opt
76 // ortools/math_opt/examples:lagrangian_relaxation
77 // blaze-bin/ortools/math_opt/examples/lagrangian_relaxation
78 
79 #include <math.h>
80 
81 #include <algorithm>
82 #include <iostream>
83 #include <limits>
84 #include <memory>
85 #include <string>
86 #include <vector>
87 
88 #include "absl/flags/flag.h"
89 #include "absl/memory/memory.h"
90 #include "absl/status/status.h"
91 #include "absl/status/statusor.h"
92 #include "absl/strings/str_format.h"
93 #include "absl/strings/string_view.h"
96 #include "ortools/base/logging.h"
97 #include "ortools/base/mathutil.h"
101 
102 ABSL_FLAG(double, step_size, 0.95,
103  "Stepsize for gradient ascent, determined as step_size^t.");
104 ABSL_FLAG(int, max_iterations, 1000,
105  "Max number of iterations for gradient ascent.");
106 ABSL_FLAG(bool, dualize_resource_1, true,
107  "If true, the side constraint associated to resource 1 is dualized.");
108 ABSL_FLAG(bool, dualize_resource_2, false,
109  "If true, the side constraint associated to resource 2 is dualized.");
110 
111 ABSL_FLAG(bool, lagrangian_output, false,
112  "If true, shows the iteration log of the subgradient ascent "
113  "procedure use to solve the Lagrangian problem");
114 
115 constexpr double kZeroTol = 1.0e-8;
116 
117 namespace {
118 using ::operations_research::MathUtil;
119 
120 namespace math_opt = ::operations_research::math_opt;
121 
122 struct Arc {
123  int i;
124  int j;
125  double cost;
126  double resource_1;
127  double resource_2;
128 };
129 
130 struct Graph {
131  int num_nodes;
132  std::vector<Arc> arcs;
133  int source;
134  int sink;
135 };
136 
137 struct FlowModel {
138  FlowModel() : model(std::make_unique<math_opt::Model>("LagrangianProblem")) {}
139  std::unique_ptr<math_opt::Model> model;
141  math_opt::LinearExpression resource_1;
142  math_opt::LinearExpression resource_2;
143  std::vector<math_opt::Variable> flow_vars;
144 };
145 
146 // Populates `model` with variables and constraints of a shortest path problem.
147 FlowModel CreateShortestPathModel(const Graph graph) {
148  FlowModel flow_model;
149  math_opt::Model& model = *flow_model.model;
150  for (const Arc& arc : graph.arcs) {
151  math_opt::Variable var = model.AddContinuousVariable(
152  /*lower_bound=*/0, /*upper_bound=*/1,
153  /*name=*/absl::StrFormat("x_%d_%d", arc.i, arc.j));
154  flow_model.cost += arc.cost * var;
155  flow_model.resource_1 += arc.resource_1 * var;
156  flow_model.resource_2 += arc.resource_2 * var;
157  flow_model.flow_vars.push_back(var);
158  }
159 
160  // Flow balance constraints
161  std::vector<math_opt::LinearExpression> out_flow(graph.num_nodes);
162  std::vector<math_opt::LinearExpression> in_flow(graph.num_nodes);
163  for (int arc_index = 0; arc_index < graph.arcs.size(); ++arc_index) {
164  out_flow[graph.arcs[arc_index].i] += flow_model.flow_vars[arc_index];
165  in_flow[graph.arcs[arc_index].j] += flow_model.flow_vars[arc_index];
166  }
167  for (int node_index = 0; node_index < graph.num_nodes; ++node_index) {
168  int rhs = node_index == graph.source ? 1
169  : node_index == graph.sink ? -1
170  : 0;
171  model.AddLinearConstraint(out_flow[node_index] - in_flow[node_index] ==
172  rhs);
173  }
174 
175  return flow_model;
176 }
177 
178 Graph CreateSampleNetwork() {
179  std::vector<Arc> arcs;
180  arcs.push_back(
181  {.i = 0, .j = 1, .cost = 12, .resource_1 = 1, .resource_2 = 1});
182  arcs.push_back(
183  {.i = 0, .j = 2, .cost = 3, .resource_1 = 2.5, .resource_2 = 1});
184  arcs.push_back(
185  {.i = 1, .j = 3, .cost = 5, .resource_1 = 1, .resource_2 = 1.5});
186  arcs.push_back(
187  {.i = 1, .j = 4, .cost = 5, .resource_1 = 2.5, .resource_2 = 1});
188  arcs.push_back(
189  {.i = 2, .j = 1, .cost = 7, .resource_1 = 2.5, .resource_2 = 1});
190  arcs.push_back(
191  {.i = 2, .j = 3, .cost = 5, .resource_1 = 7, .resource_2 = 2.5});
192  arcs.push_back(
193  {.i = 2, .j = 4, .cost = 1, .resource_1 = 6.5, .resource_2 = 1});
194  arcs.push_back(
195  {.i = 3, .j = 5, .cost = 6, .resource_1 = 1, .resource_2 = 2.0});
196  arcs.push_back(
197  {.i = 4, .j = 3, .cost = 3, .resource_1 = 1, .resource_2 = 0.5});
198  arcs.push_back(
199  {.i = 4, .j = 5, .cost = 5, .resource_1 = 2.5, .resource_2 = 1});
200  const Graph graph = {.num_nodes = 6, .arcs = arcs, .source = 0, .sink = 5};
201 
202  return graph;
203 }
204 
205 // Solves the constrained shortest path as an MIP.
206 absl::StatusOr<FlowModel> SolveMip(const Graph graph,
207  const double max_resource_1,
208  const double max_resource_2) {
209  FlowModel flow_model;
210  math_opt::Model& model = *flow_model.model;
211  for (const Arc& arc : graph.arcs) {
212  math_opt::Variable var = model.AddBinaryVariable(
213  /*name=*/absl::StrFormat("x_%d_%d", arc.i, arc.j));
214  flow_model.cost += arc.cost * var;
215  flow_model.resource_1 += +arc.resource_1 * var;
216  flow_model.resource_2 += arc.resource_2 * var;
217  flow_model.flow_vars.push_back(var);
218  }
219 
220  // Flow balance constraints
221  std::vector<math_opt::LinearExpression> out_flow(graph.num_nodes);
222  std::vector<math_opt::LinearExpression> in_flow(graph.num_nodes);
223  for (int arc_index = 0; arc_index < graph.arcs.size(); ++arc_index) {
224  out_flow[graph.arcs[arc_index].i] += flow_model.flow_vars[arc_index];
225  in_flow[graph.arcs[arc_index].j] += flow_model.flow_vars[arc_index];
226  }
227  for (int node_index = 0; node_index < graph.num_nodes; ++node_index) {
228  int rhs = node_index == graph.source ? 1
229  : node_index == graph.sink ? -1
230  : 0;
231  model.AddLinearConstraint(out_flow[node_index] - in_flow[node_index] ==
232  rhs);
233  }
234 
235  model.AddLinearConstraint(flow_model.resource_1 <= max_resource_1,
236  "resource_ctr_1");
237  model.AddLinearConstraint(flow_model.resource_2 <= max_resource_2,
238  "resource_ctr_2");
239  model.Minimize(flow_model.cost);
241  Solve(model, math_opt::SolverType::kGscip));
242  switch (result.termination.reason) {
243  case math_opt::TerminationReason::kOptimal:
244  case math_opt::TerminationReason::kFeasible:
245  std::cout << "MIP Solution with 2 side constraints" << std::endl;
246  std::cout << absl::StrFormat("MIP objective value: %6.3f",
247  result.objective_value())
248  << std::endl;
249  std::cout << "Resource 1: "
250  << flow_model.resource_1.Evaluate(result.variable_values())
251  << std::endl;
252  std::cout << "Resource 2: "
253  << flow_model.resource_2.Evaluate(result.variable_values())
254  << std::endl;
255  std::cout << "========================================" << std::endl;
256  return flow_model;
257  default:
259  << "model failed to solve: " << result.termination;
260  }
261 }
262 
263 // Solves the linear relaxation of a constrained shortest path problem
264 // formulated as an MIP.
265 absl::Status SolveLinearRelaxation(FlowModel& flow_model, const Graph& graph,
266  const double max_resource_1,
267  const double max_resource_2) {
268  math_opt::Model& model = *flow_model.model;
270  Solve(model, math_opt::SolverType::kGscip));
271  switch (result.termination.reason) {
272  case math_opt::TerminationReason::kOptimal:
273  case math_opt::TerminationReason::kFeasible:
274  std::cout << "LP relaxation with 2 side constraints" << std::endl;
275  std::cout << absl::StrFormat("LP objective value: %6.3f",
276  result.objective_value())
277  << std::endl;
278  std::cout << "Resource 1: "
279  << flow_model.resource_1.Evaluate(result.variable_values())
280  << std::endl;
281  std::cout << "Resource 2: "
282  << flow_model.resource_2.Evaluate(result.variable_values())
283  << std::endl;
284  std::cout << "========================================" << std::endl;
285  return absl::OkStatus();
286  default:
288  << "model failed to solve: " << result.termination;
289  }
290 }
291 
292 absl::Status SolveLagrangianRelaxation(const Graph graph,
293  const double max_resource_1,
294  const double max_resource_2) {
295  // Model, variables, and linear expressions.
296  FlowModel flow_model = CreateShortestPathModel(graph);
297  math_opt::Model& model = *flow_model.model;
298  math_opt::LinearExpression& cost = flow_model.cost;
299  math_opt::LinearExpression& resource_1 = flow_model.resource_1;
300  math_opt::LinearExpression& resource_2 = flow_model.resource_2;
301 
302  // Dualized constraints and dual variable iterates.
303  std::vector<double> mu;
304  std::vector<math_opt::LinearExpression> grad_mu;
305  const bool dualized_resource_1 = absl::GetFlag(FLAGS_dualize_resource_1);
306  const bool dualized_resource_2 = absl::GetFlag(FLAGS_dualize_resource_2);
307  const bool lagrangian_output = absl::GetFlag(FLAGS_lagrangian_output);
308  CHECK(dualized_resource_1 || dualized_resource_2)
309  << "At least one of the side constraints should be dualized.";
310 
311  // Modify the lagrangian problem according to the constraints that are
312  // dualized. We use a initial dual value different from zero to prioritize
313  // finding a feasible solution.
314  const double initial_dual_value = -10;
315  if (dualized_resource_1 && !dualized_resource_2) {
316  mu.push_back(initial_dual_value);
317  grad_mu.push_back(max_resource_1 - resource_1);
318  model.AddLinearConstraint(resource_2 <= max_resource_2);
319  for (math_opt::Variable& var : flow_model.flow_vars) {
320  model.set_integer(var);
321  }
322  } else if (!dualized_resource_1 && dualized_resource_2) {
323  mu.push_back(initial_dual_value);
324  grad_mu.push_back(max_resource_2 - resource_2);
325  model.AddLinearConstraint(resource_1 <= max_resource_1);
326  for (math_opt::Variable& var : flow_model.flow_vars) {
327  model.set_integer(var);
328  }
329  } else {
330  mu.push_back(initial_dual_value);
331  mu.push_back(initial_dual_value);
332  grad_mu.push_back(max_resource_1 - resource_1);
333  grad_mu.push_back(max_resource_2 - resource_2);
334  }
335 
336  // Gradient ascent setup
337  bool termination = false;
338  int iterations = 1;
339  const double step_size = absl::GetFlag(FLAGS_step_size);
340  CHECK_GT(step_size, 0) << "step_size must be strictly positive";
341  CHECK_LT(step_size, 1) << "step_size must be strictly less than 1";
342  const int max_iterations = absl::GetFlag(FLAGS_max_iterations);
343  CHECK_GT(max_iterations, 0)
344  << "Number of iterations must be strictly positive.";
345 
346  // Upper and lower bounds on the full problem.
347  double upper_bound = std::numeric_limits<double>::infinity();
348  double lower_bound = -std::numeric_limits<double>::infinity();
349  double best_solution_resource_1 = 0;
350  double best_solution_resource_2 = 0;
351 
352  if (lagrangian_output) {
353  std::cout << "Starting gradient ascent..." << std::endl;
354  std::cout << absl::StrFormat("%4s %6s %6s %9s %10s %10s", "Iter", "LB",
355  "UB", "Step size", "mu_t", "grad_mu_t")
356  << std::endl;
357  }
358 
359  while (!termination) {
360  math_opt::LinearExpression lagrangian_function;
361  lagrangian_function += cost;
362  for (int k = 0; k < mu.size(); ++k) {
363  lagrangian_function += mu[k] * grad_mu[k];
364  }
365  model.Minimize(lagrangian_function);
367  Solve(model, math_opt::SolverType::kGscip));
368  switch (result.termination.reason) {
369  case math_opt::TerminationReason::kOptimal:
370  case math_opt::TerminationReason::kFeasible:
371  break;
372  default:
374  << "failed to minimize lagrangian function: "
375  << result.termination;
376  }
377 
378  const math_opt::VariableMap<double>& vars_val = result.variable_values();
379  bool feasible = true;
380 
381  // Iterate update. Takes a step in the direction of the gradient (since the
382  // Lagrangian dual is a max problem), and projects onto {mu: mu <=0} to
383  // satisfy the sign of the dual variable. In general, convergence to an
384  // optimal solution requires diminishing step sizes satisfying:
385  // * sum(step_size_t: t=1...) = infinity and,
386  // * sum((step_size_t)^2: t=1...) < infinity
387  // See details in Prop 3.2.6 Bertsekas 2015, Convex Optimization Algorithms.
388  // Here we use step_size_t = step_size^t which does NOT satisfy the
389  // first condition, but is good enough for the purpose of this example.
390  std::vector<double> grad_mu_vals;
391  const double step_size_t = MathUtil::IPow(step_size, iterations);
392  for (int k = 0; k < mu.size(); ++k) {
393  // Evaluate resource k and evaluate the gradient of z(mu).
394  const double grad_mu_val = grad_mu[k].Evaluate(vars_val);
395  grad_mu_vals.push_back(grad_mu_val);
396  mu[k] = std::min(0.0, mu[k] + step_size_t * grad_mu_val);
397  if (grad_mu_val < 0) {
398  feasible = false;
399  }
400  }
401  // Bounds update
402  const double path_cost = cost.Evaluate(vars_val);
403  if (feasible && path_cost < upper_bound) {
404  best_solution_resource_1 = resource_1.Evaluate(vars_val);
405  best_solution_resource_2 = resource_2.Evaluate(vars_val);
406  if (lagrangian_output) {
407  std::cout << "Feasible solution with"
408  << absl::StrFormat(
409  "cost=%4.2f, resource_1=%4.2f, and resource_2=%4.2f. ",
410  path_cost, best_solution_resource_1,
411  best_solution_resource_2)
412  << std::endl;
413  }
414  upper_bound = path_cost;
415  }
416  if (lower_bound < result.objective_value()) {
417  lower_bound = result.objective_value();
418  }
419 
420  if (lagrangian_output) {
421  std::cout << absl::StrFormat("%4d %6.3f %6.3f %9.3f", iterations,
422  lower_bound, upper_bound, step_size_t)
423  << " " << gtl::LogContainer(mu) << " "
424  << gtl::LogContainer(grad_mu_vals) << std::endl;
425  }
426 
427  // Termination criteria
428  double norm = 0;
429  for (double value : grad_mu_vals) {
430  norm += (value * value);
431  }
432  norm = sqrt(norm);
433  if (iterations == max_iterations || lower_bound == upper_bound ||
434  step_size_t * norm < kZeroTol) {
435  termination = true;
436  }
437  iterations++;
438  }
439 
440  std::cout << "Lagrangian relaxation with 2 side constraints" << std::endl;
441  std::cout << "Constraint for resource 1 dualized: "
442  << (dualized_resource_1 ? "true" : "false") << std::endl;
443  std::cout << "Constraint for resource 2 dualized: "
444  << (dualized_resource_2 ? "true" : "false") << std::endl;
445  std::cout << absl::StrFormat("Lower bound: %6.3f", lower_bound) << std::endl;
446  std::cout << absl::StrFormat("Upper bound: %6.3f (Integer solution)",
447  upper_bound)
448  << std::endl;
449  std::cout << "========================================" << std::endl;
450  return absl::OkStatus();
451 }
452 
453 void RelaxModel(FlowModel& flow_model) {
454  for (math_opt::Variable& var : flow_model.flow_vars) {
455  flow_model.model->set_continuous(var);
456  flow_model.model->set_lower_bound(var, 0.0);
457  flow_model.model->set_upper_bound(var, 1.0);
458  }
459 }
460 
461 absl::Status SolveFullModel(const Graph& graph, double max_resource_1,
462  double max_resource_2) {
463  ASSIGN_OR_RETURN(FlowModel flow_model,
464  SolveMip(graph, max_resource_1, max_resource_2));
465  RelaxModel(flow_model);
466  return SolveLinearRelaxation(flow_model, graph, max_resource_1,
467  max_resource_2);
468 }
469 
470 absl::Status Main() {
471  // Problem data
472  const Graph graph = CreateSampleNetwork();
473  const double max_resource_1 = 10;
474  const double max_resource_2 = 4;
475 
476  RETURN_IF_ERROR(SolveFullModel(graph, max_resource_1, max_resource_2))
477  << "full solve failed";
479  SolveLagrangianRelaxation(graph, max_resource_1, max_resource_2))
480  << "lagrangian solve failed";
481  return absl::OkStatus();
482 }
483 } // namespace
484 
485 int main(int argc, char** argv) {
486  InitGoogle(argv[0], &argc, &argv, true);
487  const absl::Status status = Main();
488  if (!status.ok()) {
489  LOG(QFATAL) << status;
490  }
491  return 0;
492 }
int64_t min
Definition: alldiff_cst.cc:139
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
double Evaluate(const VariableMap< double > &variable_values) const
int64_t value
IntVar * var
Definition: expr_array.cc:1874
absl::Status status
Definition: g_gurobi.cc:41
GRBmodel * model
void InitGoogle(const char *usage, int *argc, char ***argv, bool deprecated)
Definition: init_google.h:34
int main(int argc, char **argv)
ABSL_FLAG(double, step_size, 0.95, "Stepsize for gradient ascent, determined as step_size^t.")
constexpr double kZeroTol
int arc
auto LogContainer(const ContainerT &container, const PolicyT &policy) -> decltype(gtl::LogRange(container.begin(), container.end(), policy))
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
ListGraph Graph
Definition: graph.h:2398
StatusBuilder InternalErrorBuilder()
IntVar * upper_bound
Definition: routing.cc:1087
IntVar * lower_bound
Definition: routing.cc:1086
int64_t cost
const VariableMap< double > & variable_values() const