OR-Tools  9.2
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/flags/parse.h"
90#include "absl/flags/usage.h"
91#include "absl/memory/memory.h"
92#include "absl/status/statusor.h"
93#include "absl/strings/str_format.h"
94#include "absl/strings/string_view.h"
99
100ABSL_FLAG(double, step_size, 0.95,
101 "Stepsize for gradient ascent, determined as step_size^t.");
102ABSL_FLAG(int, max_iterations, 1000,
103 "Max number of iterations for gradient ascent.");
104ABSL_FLAG(bool, dualize_resource_1, true,
105 "If true, the side constraint associated to resource 1 is dualized.");
106ABSL_FLAG(bool, dualize_resource_2, false,
107 "If true, the side constraint associated to resource 2 is dualized.");
108
109ABSL_FLAG(bool, lagrangian_output, false,
110 "If true, shows the iteration log of the subgradient ascent "
111 "procedure use to solve the Lagrangian problem");
112
113constexpr double kZeroTol = 1.0e-8;
114
115namespace {
116using ::operations_research::MathUtil;
117using ::operations_research::math_opt::LinearExpression;
118using ::operations_research::math_opt::Model;
119using ::operations_research::math_opt::SolveArguments;
120using ::operations_research::math_opt::SolveResult;
123using ::operations_research::math_opt::Variable;
125
126struct Arc {
127 int i;
128 int j;
129 double cost;
130 double resource_1;
131 double resource_2;
132};
133
134struct Graph {
135 int num_nodes;
136 std::vector<Arc> arcs;
137 int source;
138 int sink;
139};
140
141struct FlowModel {
142 FlowModel() : model(std::make_unique<Model>("LagrangianProblem")) {}
143 std::unique_ptr<Model> model;
144 LinearExpression cost;
145 LinearExpression resource_1;
146 LinearExpression resource_2;
147 std::vector<Variable> flow_vars;
148};
149
150// Populates `model` with variables and constraints of a shortest path problem.
151FlowModel CreateShortestPathModel(const Graph graph) {
152 FlowModel flow_model;
153 Model& model = *flow_model.model;
154 for (const Arc& arc : graph.arcs) {
155 Variable var = model.AddContinuousVariable(
156 /*lower_bound=*/0, /*upper_bound=*/1,
157 /*name=*/absl::StrFormat("x_%d_%d", arc.i, arc.j));
158 flow_model.cost += arc.cost * var;
159 flow_model.resource_1 += arc.resource_1 * var;
160 flow_model.resource_2 += arc.resource_2 * var;
161 flow_model.flow_vars.push_back(var);
162 }
163
164 // Flow balance constraints
165 std::vector<LinearExpression> out_flow(graph.num_nodes);
166 std::vector<LinearExpression> in_flow(graph.num_nodes);
167 for (int arc_index = 0; arc_index < graph.arcs.size(); ++arc_index) {
168 out_flow[graph.arcs[arc_index].i] += flow_model.flow_vars[arc_index];
169 in_flow[graph.arcs[arc_index].j] += flow_model.flow_vars[arc_index];
170 }
171 for (int node_index = 0; node_index < graph.num_nodes; ++node_index) {
172 int rhs = node_index == graph.source ? 1
173 : node_index == graph.sink ? -1
174 : 0;
175 model.AddLinearConstraint(out_flow[node_index] - in_flow[node_index] ==
176 rhs);
177 }
178
179 return flow_model;
180}
181
182Graph CreateSampleNetwork() {
183 std::vector<Arc> arcs;
184 arcs.push_back(
185 {.i = 0, .j = 1, .cost = 12, .resource_1 = 1, .resource_2 = 1});
186 arcs.push_back(
187 {.i = 0, .j = 2, .cost = 3, .resource_1 = 2.5, .resource_2 = 1});
188 arcs.push_back(
189 {.i = 1, .j = 3, .cost = 5, .resource_1 = 1, .resource_2 = 1.5});
190 arcs.push_back(
191 {.i = 1, .j = 4, .cost = 5, .resource_1 = 2.5, .resource_2 = 1});
192 arcs.push_back(
193 {.i = 2, .j = 1, .cost = 7, .resource_1 = 2.5, .resource_2 = 1});
194 arcs.push_back(
195 {.i = 2, .j = 3, .cost = 5, .resource_1 = 7, .resource_2 = 2.5});
196 arcs.push_back(
197 {.i = 2, .j = 4, .cost = 1, .resource_1 = 6.5, .resource_2 = 1});
198 arcs.push_back(
199 {.i = 3, .j = 5, .cost = 6, .resource_1 = 1, .resource_2 = 2.0});
200 arcs.push_back(
201 {.i = 4, .j = 3, .cost = 3, .resource_1 = 1, .resource_2 = 0.5});
202 arcs.push_back(
203 {.i = 4, .j = 5, .cost = 5, .resource_1 = 2.5, .resource_2 = 1});
204 const Graph graph = {.num_nodes = 6, .arcs = arcs, .source = 0, .sink = 5};
205
206 return graph;
207}
208
209// Solves the constrained shortest path as an MIP.
210FlowModel SolveMip(const Graph graph, const double max_resource_1,
211 const double max_resource_2) {
212 FlowModel flow_model;
213 Model& model = *flow_model.model;
214 for (const Arc& arc : graph.arcs) {
215 Variable var = model.AddBinaryVariable(
216 /*name=*/absl::StrFormat("x_%d_%d", arc.i, arc.j));
217 flow_model.cost += arc.cost * var;
218 flow_model.resource_1 += +arc.resource_1 * var;
219 flow_model.resource_2 += arc.resource_2 * var;
220 flow_model.flow_vars.push_back(var);
221 }
222
223 // Flow balance constraints
224 std::vector<LinearExpression> out_flow(graph.num_nodes);
225 std::vector<LinearExpression> in_flow(graph.num_nodes);
226 for (int arc_index = 0; arc_index < graph.arcs.size(); ++arc_index) {
227 out_flow[graph.arcs[arc_index].i] += flow_model.flow_vars[arc_index];
228 in_flow[graph.arcs[arc_index].j] += flow_model.flow_vars[arc_index];
229 }
230 for (int node_index = 0; node_index < graph.num_nodes; ++node_index) {
231 int rhs = node_index == graph.source ? 1
232 : node_index == graph.sink ? -1
233 : 0;
234 model.AddLinearConstraint(out_flow[node_index] - in_flow[node_index] ==
235 rhs);
236 }
237
238 model.AddLinearConstraint(flow_model.resource_1 <= max_resource_1,
239 "resource_ctr_1");
240 model.AddLinearConstraint(flow_model.resource_2 <= max_resource_2,
241 "resource_ctr_2");
242 model.Minimize(flow_model.cost);
243 const SolveResult result = Solve(model, SolverType::kGscip).value();
244 const VariableMap<double>& variable_values = result.variable_values();
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: " << flow_model.resource_1.Evaluate(variable_values)
250 << std::endl;
251 std::cout << "Resource 2: " << flow_model.resource_2.Evaluate(variable_values)
252 << std::endl;
253 std::cout << "========================================" << std::endl;
254 return flow_model;
255}
256
257// Solves the linear relaxation of a constrained shortest path problem
258// formulated as an MIP.
259void SolveLinearRelaxation(FlowModel& flow_model, const Graph& graph,
260 const double max_resource_1,
261 const double max_resource_2) {
262 Model& model = *flow_model.model;
263 const SolveResult result = Solve(model, SolverType::kGscip).value();
264 const VariableMap<double>& variable_values = result.variable_values();
265 std::cout << "LP relaxation with 2 side constraints" << std::endl;
266 std::cout << absl::StrFormat("LP objective value: %6.3f",
267 result.objective_value())
268 << std::endl;
269 std::cout << "Resource 1: " << flow_model.resource_1.Evaluate(variable_values)
270 << std::endl;
271 std::cout << "Resource 2: " << flow_model.resource_2.Evaluate(variable_values)
272 << std::endl;
273 std::cout << "========================================" << std::endl;
274}
275
276void SolveLagrangianRelaxation(const Graph graph, const double max_resource_1,
277 const double max_resource_2) {
278 // Model, variables, and linear expressions.
279 FlowModel flow_model = CreateShortestPathModel(graph);
280 Model& model = *flow_model.model;
281 LinearExpression& cost = flow_model.cost;
282 LinearExpression& resource_1 = flow_model.resource_1;
283 LinearExpression& resource_2 = flow_model.resource_2;
284
285 // Dualized constraints and dual variable iterates.
286 std::vector<double> mu;
287 std::vector<LinearExpression> grad_mu;
288 const bool dualized_resource_1 = absl::GetFlag(FLAGS_dualize_resource_1);
289 const bool dualized_resource_2 = absl::GetFlag(FLAGS_dualize_resource_2);
290 const bool lagrangian_output = absl::GetFlag(FLAGS_lagrangian_output);
291 CHECK(dualized_resource_1 || dualized_resource_2)
292 << "At least one of the side constraints should be dualized.";
293
294 // Modify the lagrangian problem according to the constraints that are
295 // dualized. We use a initial dual value different from zero to prioritize
296 // finding a feasible solution.
297 const double initial_dual_value = -10;
298 if (dualized_resource_1 && !dualized_resource_2) {
299 mu.push_back(initial_dual_value);
300 grad_mu.push_back(max_resource_1 - resource_1);
301 model.AddLinearConstraint(resource_2 <= max_resource_2);
302 for (Variable& var : flow_model.flow_vars) {
303 model.set_integer(var);
304 }
305 } else if (!dualized_resource_1 && dualized_resource_2) {
306 mu.push_back(initial_dual_value);
307 grad_mu.push_back(max_resource_2 - resource_2);
308 model.AddLinearConstraint(resource_1 <= max_resource_1);
309 for (Variable& var : flow_model.flow_vars) {
310 model.set_integer(var);
311 }
312 } else {
313 mu.push_back(initial_dual_value);
314 mu.push_back(initial_dual_value);
315 grad_mu.push_back(max_resource_1 - resource_1);
316 grad_mu.push_back(max_resource_2 - resource_2);
317 }
318
319 // Gradient ascent setup
320 bool termination = false;
321 int iterations = 1;
322 const double step_size = absl::GetFlag(FLAGS_step_size);
323 CHECK_GT(step_size, 0) << "step_size must be strictly positive";
324 CHECK_LT(step_size, 1) << "step_size must be strictly less than 1";
325 const int max_iterations = absl::GetFlag(FLAGS_max_iterations);
326 CHECK_GT(max_iterations, 0)
327 << "Number of iterations must be strictly positive.";
328
329 // Upper and lower bounds on the full problem.
330 double upper_bound = std::numeric_limits<double>::infinity();
331 double lower_bound = -std::numeric_limits<double>::infinity();
332 double best_solution_resource_1 = 0;
333 double best_solution_resource_2 = 0;
334
335 if (lagrangian_output) {
336 std::cout << "Starting gradient ascent..." << std::endl;
337 std::cout << absl::StrFormat("%4s %6s %6s %9s %10s %10s", "Iter", "LB",
338 "UB", "Step size", "mu_t", "grad_mu_t")
339 << std::endl;
340 }
341
342 while (!termination) {
343 LinearExpression lagrangian_function;
344 lagrangian_function += cost;
345 for (int k = 0; k < mu.size(); ++k) {
346 lagrangian_function += mu[k] * grad_mu[k];
347 }
348 model.Minimize(lagrangian_function);
349 SolveResult result = Solve(model, SolverType::kGscip).value();
350 CHECK_EQ(result.termination.reason, TerminationReason::kOptimal)
351 << result.termination;
352 const VariableMap<double>& vars_val = result.variable_values();
353 bool feasible = true;
354
355 // Iterate update. Takes a step in the direction of the gradient (since the
356 // Lagrangian dual is a max problem), and projects onto {mu: mu <=0} to
357 // satisfy the sign of the dual variable. In general, convergence to an
358 // optimal solution requires diminishing step sizes satisfying:
359 // * sum(step_size_t: t=1...) = infinity and,
360 // * sum((step_size_t)^2: t=1...) < infinity
361 // See details in Prop 3.2.6 Bertsekas 2015, Convex Optimization Algorithms.
362 // Here we use step_size_t = step_size^t which does NOT satisfy the
363 // first condition, but is good enough for the purpose of this example.
364 std::vector<double> grad_mu_vals;
365 const double step_size_t = MathUtil::IPow(step_size, iterations);
366 for (int k = 0; k < mu.size(); ++k) {
367 // Evaluate resource k and evaluate the gradient of z(mu).
368 const double grad_mu_val = grad_mu[k].Evaluate(vars_val);
369 grad_mu_vals.push_back(grad_mu_val);
370 mu[k] = std::min(0.0, mu[k] + step_size_t * grad_mu_val);
371 if (grad_mu_val < 0) {
372 feasible = false;
373 }
374 }
375 // Bounds update
376 const double path_cost = cost.Evaluate(vars_val);
377 if (feasible && path_cost < upper_bound) {
378 best_solution_resource_1 = resource_1.Evaluate(vars_val);
379 best_solution_resource_2 = resource_2.Evaluate(vars_val);
380 if (lagrangian_output) {
381 std::cout << "Feasible solution with"
382 << absl::StrFormat(
383 "cost=%4.2f, resource_1=%4.2f, and resource_2=%4.2f. ",
384 path_cost, best_solution_resource_1,
385 best_solution_resource_2)
386 << std::endl;
387 }
388 upper_bound = path_cost;
389 }
390 if (lower_bound < result.objective_value()) {
391 lower_bound = result.objective_value();
392 }
393
394 if (lagrangian_output) {
395 std::cout << absl::StrFormat("%4d %6.3f %6.3f %9.3f", iterations,
396 lower_bound, upper_bound, step_size_t)
397 << " " << gtl::LogContainer(mu) << " "
398 << gtl::LogContainer(grad_mu_vals) << std::endl;
399 }
400
401 // Termination criteria
402 double norm = 0;
403 for (double value : grad_mu_vals) {
404 norm += (value * value);
405 }
406 norm = sqrt(norm);
407 if (iterations == max_iterations || lower_bound == upper_bound ||
408 step_size_t * norm < kZeroTol) {
409 termination = true;
410 }
411 iterations++;
412 }
413
414 std::cout << "Lagrangian relaxation with 2 side constraints" << std::endl;
415 std::cout << "Constraint for resource 1 dualized: "
416 << (dualized_resource_1 ? "true" : "false") << std::endl;
417 std::cout << "Constraint for resource 2 dualized: "
418 << (dualized_resource_2 ? "true" : "false") << std::endl;
419 std::cout << absl::StrFormat("Lower bound: %6.3f", lower_bound) << std::endl;
420 std::cout << absl::StrFormat("Upper bound: %6.3f (Integer solution)",
422 << std::endl;
423 std::cout << "========================================" << std::endl;
424}
425
426void RelaxModel(FlowModel& flow_model) {
427 for (Variable& var : flow_model.flow_vars) {
428 flow_model.model->set_continuous(var);
429 flow_model.model->set_lower_bound(var, 0.0);
430 flow_model.model->set_upper_bound(var, 1.0);
431 }
432}
433
434void SolveFullModel(const Graph& graph, double max_resource_1,
435 double max_resource_2) {
436 FlowModel flow_model = SolveMip(graph, max_resource_1, max_resource_2);
437 RelaxModel(flow_model);
438 SolveLinearRelaxation(flow_model, graph, max_resource_1, max_resource_2);
439}
440
441} // namespace
442
443int main(int argc, char** argv) {
445 absl::ParseCommandLine(argc, argv);
446
447 // Problem data
448 const Graph graph = CreateSampleNetwork();
449 const double max_resource_1 = 10;
450 const double max_resource_2 = 4;
451
452 SolveFullModel(graph, max_resource_1, max_resource_2);
453
454 SolveLagrangianRelaxation(graph, max_resource_1, max_resource_2);
455 return 0;
456}
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:705
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:702
#define CHECK_GT(val1, val2)
Definition: base/logging.h:707
int64_t value
IntVar * var
Definition: expr_array.cc:1874
double upper_bound
double lower_bound
GRBmodel * model
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
void InitGoogleLogging(const char *argv0)
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:155
std::pair< int64_t, int64_t > Arc
Definition: search.cc:3434
ListGraph Graph
Definition: graph.h:2362
int64_t cost