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