OR-Tools  9.3
lagrangian_relaxation.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// 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"
101
102ABSL_FLAG(double, step_size, 0.95,
103 "Stepsize for gradient ascent, determined as step_size^t.");
104ABSL_FLAG(int, max_iterations, 1000,
105 "Max number of iterations for gradient ascent.");
106ABSL_FLAG(bool, dualize_resource_1, true,
107 "If true, the side constraint associated to resource 1 is dualized.");
108ABSL_FLAG(bool, dualize_resource_2, false,
109 "If true, the side constraint associated to resource 2 is dualized.");
110
111ABSL_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
115constexpr double kZeroTol = 1.0e-8;
116
117namespace {
118using ::operations_research::MathUtil;
119
120namespace math_opt = ::operations_research::math_opt;
121
122struct Arc {
123 int i;
124 int j;
125 double cost;
126 double resource_1;
127 double resource_2;
128};
129
130struct Graph {
131 int num_nodes;
132 std::vector<Arc> arcs;
133 int source;
134 int sink;
135};
136
137struct FlowModel {
138 FlowModel() : model(std::make_unique<math_opt::Model>("LagrangianProblem")) {}
139 std::unique_ptr<math_opt::Model> model;
143 std::vector<math_opt::Variable> flow_vars;
144};
145
146// Populates `model` with variables and constraints of a shortest path problem.
147FlowModel 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
178Graph 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.
206absl::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.
265absl::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
292absl::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)",
448 << std::endl;
449 std::cout << "========================================" << std::endl;
450 return absl::OkStatus();
451}
452
453void 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
461absl::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
470absl::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
485int 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 CHECK(condition)
Definition: base/logging.h:495
#define CHECK_LT(val1, val2)
Definition: base/logging.h:706
#define CHECK_GT(val1, val2)
Definition: base/logging.h:708
#define LOG(severity)
Definition: base/logging.h:420
#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:35
double upper_bound
double lower_bound
GRBmodel * model
void InitGoogle(const char *usage, int *argc, char ***argv, bool deprecated)
Definition: init_google.h:32
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)
Definition: solve.cc:94
std::pair< int64_t, int64_t > Arc
Definition: search.cc:3434
ListGraph Graph
Definition: graph.h:2362
StatusBuilder InternalErrorBuilder()
int64_t cost
const VariableMap< double > & variable_values() const