OR-Tools  9.6
solution_serializer.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 
15 
16 #include <algorithm>
17 #include <optional>
18 #include <string>
19 #include <string_view>
20 #include <vector>
21 
22 #include "absl/strings/ascii.h"
23 #include "absl/time/clock.h"
24 #include "absl/time/time.h"
25 #include "ortools/base/logging.h"
26 
27 namespace operations_research {
28 
30  const std::string format_normalized =
31  absl::AsciiStrToLower(absl::StripAsciiWhitespace(format));
32  if (format_normalized == "tsplib") return RoutingOutputFormat::kTSPLIB;
33  if (format_normalized == "cvrplib") return RoutingOutputFormat::kCVRPLIB;
34  if (format_normalized == "carplib") return RoutingOutputFormat::kCARPLIB;
35  if (format_normalized == "nearplib") return RoutingOutputFormat::kNEARPLIB;
37 }
38 
39 // Helper for FromSplitRoutes.
40 namespace {
41 std::vector<RoutingSolution::Route> RoutesFromVector(
42  const std::vector<std::vector<int64_t>>& routes,
43  std::optional<int64_t> depot = std::nullopt);
44 } // namespace
45 
46 std::vector<std::vector<int64_t>> RoutingSolution::SplitRoutes(
47  const std::vector<int64_t>& solution, int64_t separator) {
48  // The solution vector separates routes by -1: split this vector into a vector
49  // per route, where the other helpers can make the rest of the way to a proper
50  // RoutingSolution object.
51  std::vector<std::vector<int64_t>> routes;
52  int64_t current_route = 0;
53  for (int64_t node : solution) {
54  if (routes.size() == current_route) {
55  routes.emplace_back(std::vector<int64_t>());
56  }
57 
58  if (node == separator) {
59  current_route += 1;
60  } else {
61  routes[current_route].emplace_back(node);
62  }
63  }
64  return routes;
65 }
66 
68  const std::vector<std::vector<int64_t>>& routes,
69  std::optional<int64_t> depot) {
70  std::vector<int64_t> total_demands(routes.size(), -1);
71  std::vector<int64_t> total_distances(routes.size(), -1);
72 
73  return {RoutesFromVector(routes, depot), total_demands, total_distances};
74 }
75 
76 int64_t RoutingSolution::NumberOfNonemptyRoutes() const {
77  int64_t num_nonempty_routes = 0;
78  for (const Route& route : routes_) {
79  if (!route.empty()) num_nonempty_routes++;
80  }
81  return num_nonempty_routes;
82 }
83 
85  const std::string& file_name) const {
86  File* file;
87  CHECK_OK(file::Open(file_name, "w", &file, file::Defaults()))
88  << "Could not open the solution file '" << file_name << "'";
90  file::Defaults()))
91  << "Could not write the solution file '" << file_name << "'";
92  CHECK_OK(file->Close(file::Defaults()));
93 }
94 
95 std::string RoutingSolution::SerializeToTSPLIBString() const {
96  std::string tour_out;
97  for (const Route& route : routes_) {
98  if (route.empty()) continue;
99 
100  for (const Event& event : route) {
101  if (event.type != RoutingSolution::Event::Type::kEnd) {
102  absl::StrAppendFormat(&tour_out, "%d\n", event.arc.head());
103  }
104  }
105  absl::StrAppendFormat(&tour_out, "-1\n");
106  }
107  return tour_out;
108 }
109 
110 std::string RoutingSolution::SerializeToTSPLIBSolutionFile() const {
111  // Determine the number of nodes as the maximum index of a node in the
112  // solution, plus one (due to TSPLIB being 1-based and C++ 0-based).
113  int64_t number_of_nodes = 0;
114  for (const Route& route : routes_) {
115  for (const Event& event : route) {
116  if (event.arc.tail() > number_of_nodes) {
117  number_of_nodes = event.arc.tail();
118  }
119  if (event.arc.head() > number_of_nodes) {
120  number_of_nodes = event.arc.head();
121  }
122  }
123  }
124  number_of_nodes += 1;
125 
126  std::string tour_out;
127  absl::StrAppendFormat(&tour_out, "NAME : %s\n", name_);
128  absl::StrAppendFormat(&tour_out, "COMMENT : Length = %d; Total time = %f s\n",
129  total_distance_, total_time_);
130  absl::StrAppendFormat(&tour_out, "TYPE : TOUR\n");
131  absl::StrAppendFormat(&tour_out, "DIMENSION : %d\n", number_of_nodes);
132  absl::StrAppendFormat(&tour_out, "TOUR_SECTION\n");
133  absl::StrAppendFormat(&tour_out, "%s", SerializeToTSPLIBString());
134  absl::StrAppendFormat(&tour_out, "EOF");
135  return tour_out;
136 }
137 
138 namespace {
139 std::string SerializeRouteToCVRPLIBString(const RoutingSolution::Route& route);
140 } // namespace
141 
142 std::string RoutingSolution::SerializeToCVRPLIBString() const {
143  std::string tour_out; // The complete solution.
144  int route_index = 1; // Index of the route being written.
145 
146  for (const Route& route : routes_) {
147  if (route.empty()) continue;
148  std::string current_route = SerializeRouteToCVRPLIBString(route);
149 
150  // Output the current route only if it is not empty.
151  if (!current_route.empty()) {
152  absl::StrAppendFormat(&tour_out, "Route #%d: %s\n", route_index++,
153  absl::StripAsciiWhitespace(current_route));
154  }
155  }
156  return tour_out;
157 }
158 
159 std::string RoutingSolution::SerializeToCVRPLIBSolutionFile() const {
160  std::string tour_out = SerializeToCVRPLIBString();
161  absl::StrAppendFormat(&tour_out, "Cost %d", total_cost_);
162  return tour_out;
163 }
164 
165 std::string RoutingSolution::SerializeToCARPLIBString() const {
166  std::string tour_out; // The complete solution.
167  int64_t num_out_route = 1; // Index of the route being written.
168  int64_t num_iteration_route = 0; // Index of the route being considered.
169  int64_t depot;
170 
171  for (const Route& route : routes_) {
172  std::string current_route;
173 
174  for (const RoutingSolution::Event& event : route) {
175  std::string type;
176  switch (event.type) {
178  ABSL_FALLTHROUGH_INTENDED;
180  CHECK_EQ(event.arc.tail(), event.arc.head());
181  depot = event.arc.tail();
182  type = "D";
183  break;
186  ABSL_FALLTHROUGH_INTENDED;
188  // The only difference is in the arc: when serving a node, both the
189  // head and the tail are the node being served.
190  type = "S";
191  break;
193  // Not present in CARPLIB output.
194  break;
195  }
196 
197  if (!type.empty()) {
198  absl::StrAppendFormat(&current_route, "(%s %d,%d,%d) ", type,
199  event.demand_id, event.arc.tail() + 1,
200  event.arc.head() + 1);
201  }
202  }
203 
204  // Output the current route only if it is not empty.
205  if (!route.empty()) {
206  const int64_t day = 1;
207  const int64_t num_events = std::count_if(
208  route.begin(), route.end(), [](const RoutingSolution::Event& event) {
209  // Bare transitions are not output in CARPLIB, don't count them.
210  return event.type != RoutingSolution::Event::Type::kTransit;
211  });
212 
213  absl::StrAppendFormat(
214  &tour_out, "%d %d %d %d %d %d %s\n",
215  depot, // Use a 0-based encoding for the depot here.
216  day, num_out_route, total_demands_[num_iteration_route],
217  total_distances_[num_iteration_route], num_events,
218  absl::StripAsciiWhitespace(current_route));
219 
220  num_out_route += 1;
221  }
222 
223  num_iteration_route += 1;
224  }
225  absl::StripTrailingAsciiWhitespace(&tour_out);
226  return tour_out;
227 }
228 
229 std::string RoutingSolution::SerializeToCARPLIBSolutionFile() const {
230  std::string solution;
231  absl::StrAppendFormat(&solution, "%d\n", total_cost_);
232  absl::StrAppendFormat(&solution, "%d\n", NumberOfNonemptyRoutes());
233  absl::StrAppendFormat(&solution, "%f\n", total_time_);
234  absl::StrAppend(&solution, SerializeToCARPLIBString());
235  return solution;
236 }
237 
238 std::string RoutingSolution::SerializeToNEARPLIBString() const {
239  std::string tour_out; // The complete solution.
240  int64_t route_index = 1; // Index of the route being written.
241 
242  for (const Route& route : routes_) {
243  std::string current_route;
244  int64_t current_node = -2; // Holds the last node that was output, i.e.
245  // where the vehicle is located at the beginning of each iteration. -1 is
246  // used for the depot, hence an even lower value.
247 
248  // Skip empty routes.
249  if (route.size() <= 1) continue;
250  if (route.size() == 2 &&
251  route[0].type == RoutingSolution::Event::Type::kStart &&
252  route[1].type == RoutingSolution::Event::Type::kEnd)
253  continue;
254 
255  // Print the nodes that are traversed only when they are a depot or some end
256  // of a serviced arc/edge, without repeating nodes when two consecutive
257  // serviced arcs/edges are incident to the same node in the middle.
258  // Hence, current_node is used to determine whether the sequence of
259  // arcs/edges is continued or should start over.
260  // Only set current_node when a sequence should be continued (e.g., not
261  // when only traversing an arc/edge).
262  for (const RoutingSolution::Event& event : route) {
263  switch (event.type) {
265  // Always start at the depot.
266  CHECK_EQ(event.arc.tail(), event.arc.head());
267  current_node = event.arc.tail();
268  absl::StrAppendFormat(&current_route, "%d", event.arc.tail() + 1);
269  break;
271  // Always print the end depot.
272  CHECK_EQ(event.arc.tail(), event.arc.head());
273  if (current_node != event.arc.tail()) {
274  absl::StrAppendFormat(&current_route, " %d", event.arc.tail() + 1);
275  }
276  break;
278  ABSL_FALLTHROUGH_INTENDED;
280  CHECK(!event.arc_name.empty())
281  << "Arc " << event.arc.tail() << "-" << event.arc.head()
282  << " does not have a name in the solution object.";
283 
284  // TODO(user): print the name of the node when it is served
285  // (i.e. there is a kServeNode event just after). For now, it's only
286  // done when the node happens before.
287  if (current_node == event.arc.tail()) {
288  // Direct continuation of the path: just add a hyphen and go on.
289  absl::StrAppendFormat(&current_route, "-%s-%d", event.arc_name,
290  event.arc.head() + 1);
291  } else {
292  // Some part of the path is not explicitly output before the
293  // previous node and the one after this edge is served.
294  absl::StrAppendFormat(&current_route, " %d-%s-%d",
295  event.arc.tail() + 1, event.arc_name,
296  event.arc.head() + 1);
297  }
298  current_node = event.arc.head();
299  break;
301  CHECK_EQ(event.arc.tail(), event.arc.head());
302  absl::StrAppendFormat(&current_route, " N%d", event.arc.head() + 1);
303  current_node = event.arc.head();
304  break;
306  current_node = -2;
307  break;
308  }
309  }
310 
311  // Output the current route only if it is not empty.
312  if (!current_route.empty()) {
313  absl::StrAppendFormat(&tour_out, "Route #%d : %s\n", route_index++,
314  absl::StripAsciiWhitespace(current_route));
315  }
316  }
317  absl::StripTrailingAsciiWhitespace(&tour_out);
318  return tour_out;
319 }
320 
321 std::string RoutingSolution::SerializeToNEARPLIBSolutionFile() const {
322  const std::string date =
323  absl::FormatTime("%B %d, %E4Y", absl::Now(), absl::LocalTimeZone());
324 
325  std::string solution;
326  absl::StrAppendFormat(&solution, "Instance name: %s\n", name_);
327  absl::StrAppendFormat(&solution, "Authors: %s\n", authors_);
328  absl::StrAppendFormat(&solution, "Date: %s\n", date);
329  absl::StrAppendFormat(&solution, "Reference: OR-Tools\n");
330  absl::StrAppendFormat(&solution, "Solution\n");
331  absl::StrAppendFormat(&solution, "%s\n", SerializeToNEARPLIBString());
332  absl::StrAppendFormat(&solution, "Total cost: %d", total_cost_);
333  // Official solutions for CBMix use "total cost", whereas the definition of
334  // the output format rather uses "cost":
335  // https://www.sintef.no/globalassets/project/top/nearp/cbmix-results/cbmix22.txt
336  // https://www.sintef.no/globalassets/project/top/nearp/solutionformat.txt
337  return solution;
338 }
339 
340 namespace {
341 RoutingSolution::Route RouteFromVector(
342  const std::vector<int64_t>& route_int,
343  std::optional<int64_t> depot = std::nullopt);
344 
345 std::vector<RoutingSolution::Route> RoutesFromVector(
346  const std::vector<std::vector<int64_t>>& routes,
347  std::optional<int64_t> depot) {
348  std::vector<RoutingSolution::Route> solution_routes;
349  solution_routes.reserve(routes.size());
350  for (const std::vector<int64_t>& route : routes) {
351  // TODO(user): explore merging RouteFromVector in this function.
352  solution_routes.emplace_back(RouteFromVector(route, depot));
353  }
354  return solution_routes;
355 }
356 
357 RoutingSolution::Route RouteFromVector(const std::vector<int64_t>& route_int,
358  std::optional<int64_t> forced_depot) {
359  // One route in input: from the node indices, create a Route object (not yet
360  // a RoutingSolution one).
362 
363  // If no depot is given, guess one.
364  int64_t depot =
365  (forced_depot.has_value()) ? forced_depot.value() : route_int[0];
366 
367  route.emplace_back(
368  RoutingSolution::Event{/*type=*/RoutingSolution::Event::Type::kStart,
369  /*demand_id=*/-1, /*arc=*/Arc{depot, depot}});
370  for (int64_t i = 0; i < route_int.size() - 1; ++i) {
371  int64_t tail = route_int[i];
372  int64_t head = route_int[i + 1];
373  route.emplace_back(RoutingSolution::Event{
375  }
376  route.emplace_back(RoutingSolution::Event{RoutingSolution::Event::Type::kEnd,
377  -1, Arc{depot, depot}});
378 
379  return route;
380 }
381 
382 std::string SerializeRouteToCVRPLIBString(const RoutingSolution::Route& route) {
383  // Before serializing the route, make some tests to check that the hypotheses
384  // are respected (otherwise, the output of the function is highly likely
385  // pure garbage).
386  RoutingSolution::Event first_event = route[0];
387  CHECK(first_event.type == RoutingSolution::Event::Type::kStart)
388  << "The route does not begin with a Start event to indicate "
389  "the depot.";
390  const int64_t depot = first_event.arc.tail();
391 
392  CHECK_GE(depot, 0) << "The given depot is negative: " << depot;
393  CHECK_LE(depot, 1) << "The given depot is greater than 1: " << depot;
394 
395  // Serialize this route, ignoring the depot (already dealt with).
396  std::string current_route;
397 
398  for (int64_t i = 1; i < route.size() - 1; ++i) {
399  RoutingSolution::Event event = route[i];
400 
401  // Ignore the depot, as CVRPLIB doesn't output the depot in the routes
402  // (all routes implicitly start and end at the depot).
403  int64_t node = event.arc.head();
404  if (node > depot) {
405  absl::StrAppendFormat(&current_route, "%d ", node - depot);
406  }
407  }
408 
409  // Last event: end at a depot. Due to the strange way CVRPLIB
410  // outputs nodes, the depot must be the same at the beginning and the
411  // end of the route.
412  RoutingSolution::Event last_event = route.back();
413  if (last_event.type == RoutingSolution::Event::Type::kEnd) {
414  CHECK_EQ(depot, last_event.arc.tail());
415  CHECK_EQ(last_event.arc.tail(), last_event.arc.head());
416  } else {
417  LOG(FATAL) << "The route does not finish with an End event to "
418  "indicate the depot.";
419  }
420 
421  return current_route;
422 }
423 } // namespace
424 } // namespace operations_research
Definition: base/file.h:33
static RoutingSolution FromSplitRoutes(const std::vector< std::vector< int64_t >> &routes, std::optional< int64_t > depot=std::nullopt)
static std::vector< std::vector< int64_t > > SplitRoutes(const std::vector< int64_t > &solution, int64_t separator)
void WriteToSolutionFile(RoutingOutputFormat format, const std::string &file_name) const
std::string SerializeToSolutionFile(RoutingOutputFormat format) const
absl::Status WriteString(File *file, const absl::string_view &contents, int flags)
Definition: base/file.cc:194
Options Defaults()
Definition: base/file.h:123
absl::Status Open(const absl::string_view &filename, const absl::string_view &mode, File **f, int flags)
Definition: base/file.cc:143
Collection of objects used to extend the Constraint Solver library.
RoutingOutputFormat RoutingOutputFormatFromString(std::string_view format)
int64_t tail
int64_t head