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