diff --git a/ortools/routing/BUILD.bazel b/ortools/routing/BUILD.bazel new file mode 100644 index 0000000000..ce408f3a54 --- /dev/null +++ b/ortools/routing/BUILD.bazel @@ -0,0 +1,293 @@ +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "simple_graph", + srcs = ["simple_graph.cc"], + hdrs = ["simple_graph.h"], + deps = [ + "@com_google_absl//absl/hash", + ], +) + +cc_test( + name = "simple_graph_test", + size = "small", + srcs = ["simple_graph_test.cc"], + deps = [ + ":simple_graph", + "@com_google_absl//absl/hash:hash_testing", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "solomon_parser", + srcs = ["solomon_parser.cc"], + hdrs = ["solomon_parser.h"], + deps = [ + ":simple_graph", + "//ortools/base", + "//ortools/base:map_util", + "//ortools/base:numbers", + "//ortools/base:path", + "//ortools/base:zipfile", + "//ortools/util:filelineiter", + "@com_google_absl//absl/strings", + ], +) + +cc_test( + name = "solomon_parser_test", + size = "small", + srcs = ["solomon_parser_test.cc"], + data = ["//ortools/routing/testdata:solomon.zip"], + deps = [ + ":solomon_parser", + "//ortools/base", + "//ortools/base:file", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "carp_parser", + srcs = ["carp_parser.cc"], + hdrs = ["carp_parser.h"], + deps = [ + ":simple_graph", + "//ortools/base", + "//ortools/base:linked_hash_map", + "//ortools/base:numbers", + "//ortools/util:filelineiter", + ], +) + +cc_test( + name = "carp_parser_test", + size = "small", + srcs = ["carp_parser_test.cc"], + data = [ + "//ortools/routing/testdata:carp_gdb19.dat", + "//ortools/routing/testdata:carp_gdb19_diferente_deposito.dat", + "//ortools/routing/testdata:carp_gdb19_incorrecta_lista_aristas_req.dat", + "//ortools/routing/testdata:carp_gdb19_incorrecto_arinoreq.dat", + "//ortools/routing/testdata:carp_gdb19_incorrecto_arireq.dat", + "//ortools/routing/testdata:carp_gdb19_incorrecto_capacidad.dat", + "//ortools/routing/testdata:carp_gdb19_incorrecto_coste.dat", + "//ortools/routing/testdata:carp_gdb19_incorrecto_deposito.dat", + "//ortools/routing/testdata:carp_gdb19_incorrecto_tipo.dat", + "//ortools/routing/testdata:carp_gdb19_incorrecto_vehiculos.dat", + "//ortools/routing/testdata:carp_gdb19_incorrecto_vertices.dat", + "//ortools/routing/testdata:carp_gdb19_mixed_arcs.dat", + "//ortools/routing/testdata:carp_gdb19_no_arista_req.dat", + ], + deps = [ + ":carp_parser", + "//ortools/base:mock_log", + "//ortools/base:path", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/hash:hash_testing", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "nearp_parser", + srcs = ["nearp_parser.cc"], + hdrs = ["nearp_parser.h"], + deps = [ + ":simple_graph", + "//ortools/base", + "//ortools/base:linked_hash_map", + "//ortools/base:numbers", + "//ortools/util:filelineiter", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/hash", + ], +) + +cc_test( + name = "nearp_parser_test", + size = "small", + srcs = ["nearp_parser_test.cc"], + data = [ + "//ortools/routing/testdata:nearp_BHW1.dat", + "//ortools/routing/testdata:nearp_toy.dat", + ], + deps = [ + ":nearp_parser", + "//ortools/base:file", + "//ortools/base:mock_log", + "//ortools/base:path", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/hash:hash_testing", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "pdtsp_parser", + srcs = ["pdtsp_parser.cc"], + hdrs = ["pdtsp_parser.h"], + visibility = ["//visibility:public"], + deps = [ + "//ortools/base", + "//ortools/base:file", + "//ortools/base:gzipfile", + "//ortools/base:mathutil", + "//ortools/base:numbers", + "//ortools/base:path", + "//ortools/base:strtoint", + "//ortools/util:filelineiter", + "@com_google_absl//absl/strings", + ], +) + +cc_test( + name = "pdtsp_parser_test", + size = "small", + srcs = ["pdtsp_parser_test.cc"], + data = [ + "//ortools/routing/testdata:pdtsp_prob10b.txt", + "//ortools/routing/testdata:pdtsp_prob10b.txt.gz", + ], + deps = [ + ":pdtsp_parser", + "//ortools/base", + "//ortools/base:path", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "tsplib_parser", + srcs = ["tsplib_parser.cc"], + hdrs = ["tsplib_parser.h"], + visibility = ["//visibility:public"], + deps = [ + ":simple_graph", + "//ortools/base", + "//ortools/base:map_util", + "//ortools/base:mathutil", + "//ortools/base:numbers", + "//ortools/base:path", + "//ortools/base:strtoint", + "//ortools/base:zipfile", + "//ortools/util:filelineiter", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/strings", + "@com_google_re2//:re2", + ], +) + +cc_test( + name = "tsplib_parser_test", + size = "small", + timeout = "moderate", + srcs = ["tsplib_parser_test.cc"], + data = [ + #"@com_google_ortools_data//TSPLIB95:ALL_atsp.tar", + #"@com_google_ortools_data//TSPLIB95:ALL_hcp.tar", + #"@com_google_ortools_data//TSPLIB95:ALL_sop.tar", + #"@com_google_ortools_data//TSPLIB95:ALL_tsp.tar.gz", + #"@com_google_ortools_data//TSPLIB95:ALL_tsp.zip", + #"@com_google_ortools_data//TSPLIB95:ALL_vrp.tar", + #"@com_google_ortools_data//TSPLIB95:ALL_vrp.zip", + "//ortools/routing/testdata:tsplib_Kytojoki_33.vrp", + ], + deps = [ + ":tsplib_parser", + "//ortools/base", + "//ortools/base:filesystem", + "//ortools/base:memfile", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "tsptw_parser", + srcs = ["tsptw_parser.cc"], + hdrs = ["tsptw_parser.h"], + visibility = ["//visibility:public"], + deps = [ + ":simple_graph", + "//ortools/base", + "//ortools/base:mathutil", + "//ortools/base:numbers", + "//ortools/base:path", + "//ortools/base:strtoint", + "//ortools/base:zipfile", + "//ortools/util:filelineiter", + "@com_google_absl//absl/strings", + ], +) + +cc_test( + name = "tsptw_parser_test", + size = "small", + srcs = ["tsptw_parser_test.cc"], + data = [ + #"//ortools/util/testdata:n20w20.001.txt", + #"//ortools/util/testdata:n20w20.002.txt", + #"//ortools/util/testdata:rc201.0", + ], + deps = [ + ":tsptw_parser", + "//ortools/base", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "solution_serializer", + srcs = ["solution_serializer.cc"], + hdrs = ["solution_serializer.h"], + deps = [ + ":simple_graph", + "//ortools/base", + "//ortools/base:file", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/time", + ], +) + +cc_test( + name = "solution_serializer_test", + size = "small", + srcs = ["solution_serializer_test.cc"], + deps = [ + ":solution_serializer", + "//ortools/base", + "//ortools/base:mutable_memfile", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "cvrptw_lib", + srcs = ["cvrptw_lib.cc"], + hdrs = ["cvrptw_lib.h"], + deps = [ + "//ortools/base", + "//ortools/constraint_solver:routing", + "//ortools/constraint_solver:routing_flags", + "//ortools/util:random_engine", + ], +) diff --git a/ortools/routing/carp_parser.cc b/ortools/routing/carp_parser.cc new file mode 100644 index 0000000000..e5b735f687 --- /dev/null +++ b/ortools/routing/carp_parser.cc @@ -0,0 +1,248 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/carp_parser.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "ortools/base/numbers.h" +#include "ortools/util/filelineiter.h" + +namespace operations_research { + +CarpParser::CarpParser() { Initialize(); } + +void CarpParser::Initialize() { + name_.clear(); + comment_.clear(); + number_of_nodes_ = 0; + number_of_edges_with_servicing_ = 0; + number_of_edges_without_servicing_ = 0; + total_servicing_cost_ = 0; + depot_ = 0; + traversing_costs_.clear(); + servicing_demands_.clear(); + n_vehicles_ = 0; + capacity_ = 0; + section_ = METADATA; +} + +bool CarpParser::LoadFile(const std::string& file_name) { + Initialize(); + return ParseFile(file_name); +} + +bool CarpParser::ParseFile(const std::string& file_name) { + static auto section_headers = std::array({ + "NOMBRE", + "COMENTARIO", + "VERTICES", + "ARISTAS_REQ", + "ARISTAS_NOREQ", + "VEHICULOS", + "CAPACIDAD", + "TIPO_COSTES_ARISTAS", + "COSTE_TOTAL_REQ", + "LISTA_ARISTAS_REQ", + "LISTA_ARISTAS_NOREQ", + "DEPOSITO", + }); + + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + const std::vector words = + absl::StrSplit(line, absl::ByAnyChar(" :\t"), absl::SkipEmpty()); + + if (absl::c_linear_search(section_headers, words[0])) { + // First, check if a new section has been met. + if (words[0] == "LISTA_ARISTAS_REQ") { + traversing_costs_.reserve(NumberOfEdges()); + servicing_demands_.reserve(NumberOfEdgesWithServicing()); + section_ = ARCS_WITH_SERVICING; + } else if (words[0] == "LISTA_ARISTAS_NOREQ") { + traversing_costs_.reserve(NumberOfEdges()); + section_ = ARCS_WITHOUT_SERVICING; + } else { + if (!ParseMetadataLine(words)) { + LOG(ERROR) << "Error when parsing the following metadata line: " + << line; + return false; + } + } + } else { + // If no new section is detected, process according to the current state. + switch (section_) { + case ARCS_WITH_SERVICING: + if (!ParseEdge(line, true)) { + LOG(ERROR) << "Could not parse line in LISTA_ARISTAS_REQ: " << line; + return false; + } + break; + case ARCS_WITHOUT_SERVICING: + if (!ParseEdge(line, false)) { + LOG(ERROR) << "Could not parse line in LISTA_ARISTAS_NOREQ: " + << line; + return false; + } + break; + default: + LOG(ERROR) << "Could not parse line outside edge lists: " << line; + return false; + } + } + } + + return !servicing_demands_.empty(); +} + +namespace { +std::optional ParseNodeIndex(std::string_view text); +} // namespace + +bool CarpParser::ParseMetadataLine(const std::vector& words) { + if (words[0] == "NOMBRE") { + name_ = absl::StrJoin(words.begin() + 1, words.end(), " "); + } else if (words[0] == "COMENTARIO") { + comment_ = absl::StrJoin(words.begin() + 1, words.end(), " "); + } else if (words[0] == "VERTICES") { + number_of_nodes_ = strings::ParseLeadingInt64Value(words[1], -1); + if (number_of_nodes_ <= 0) { + LOG(ERROR) << "Error when parsing the number of nodes: " << words[1]; + return false; + } + } else if (words[0] == "ARISTAS_REQ") { + number_of_edges_with_servicing_ = + strings::ParseLeadingInt64Value(words[1], -1); + if (number_of_edges_with_servicing_ <= 0) { + LOG(ERROR) << "Error when parsing the number of edges with servicing: " + << words[1]; + return false; + } + } else if (words[0] == "ARISTAS_NOREQ") { + number_of_edges_without_servicing_ = + strings::ParseLeadingInt64Value(words[1], -1); + if (number_of_edges_without_servicing_ < 0) { + // It is possible to have a valid instance with zero edges that have no + // servicing (i.e. number_of_edges_without_servicing_ == 0). + LOG(ERROR) << "Error when parsing the number of edges without servicing: " + << words[1]; + return false; + } + } else if (words[0] == "VEHICULOS") { + n_vehicles_ = strings::ParseLeadingInt64Value(words[1], -1); + if (n_vehicles_ <= 0) { + LOG(ERROR) << "Error when parsing the number of vehicles: " << words[1]; + return false; + } + } else if (words[0] == "CAPACIDAD") { + capacity_ = strings::ParseLeadingInt64Value(words[1], -1); + if (capacity_ <= 0) { + LOG(ERROR) << "Error when parsing the capacity: " << words[1]; + return false; + } + } else if (words[0] == "TIPO_COSTES_ARISTAS") { + if (words[1] != "EXPLICITOS") { + // Actually, this is the only defined value for this file format. + LOG(ERROR) << "Value of TIPO_COSTES_ARISTAS is unexpected, only " + "EXPLICITOS is supported, but " + << words[1] << " was found"; + return false; + } + } else if (words[0] == "COSTE_TOTAL_REQ") { + total_servicing_cost_ = strings::ParseLeadingInt64Value(words[1], -1); + if (total_servicing_cost_ == -1) { + LOG(ERROR) << "Error when parsing the total servicing cost: " << words[1]; + return false; + } + } else if (words[0] == "DEPOSITO") { + // Supposed to be the last value of the file. + const std::optional depot = ParseNodeIndex(words[1]); + if (!depot.has_value()) { + LOG(ERROR) << "Error when parsing the depot: " << words[1]; + return false; + } + depot_ = depot.value(); + } + return true; +} + +bool CarpParser::ParseEdge(std::string_view line, bool with_servicing) { + const std::vector words = + absl::StrSplit(line, absl::ByAnyChar(" :\t(),"), absl::SkipEmpty()); + + // Parse the edge. + std::optional opt_head = ParseNodeIndex(words[0]); + if (!opt_head.has_value()) { + LOG(ERROR) << "Error when parsing the head node: " << words[0]; + return false; + } + const int64_t head = opt_head.value(); + + std::optional opt_tail = ParseNodeIndex(words[1]); + if (!opt_tail.has_value()) { + LOG(ERROR) << "Error when parsing the tail node: " << words[1]; + return false; + } + const int64_t tail = opt_tail.value(); + + if (head == tail) { + LOG(ERROR) << "The head and tail nodes are identical: " << line; + return false; + } + + // Parse the cost. + if (words[2] != "coste") { + LOG(ERROR) << "Unexpected keyword: " << words[2]; + return false; + } + const int64_t cost = strings::ParseLeadingInt64Value(words[3], -1); + traversing_costs_[{tail, head}] = cost; + + // Parse the servicing if needed. + if (with_servicing) { + if (words[4] != "demanda") { + LOG(ERROR) << "Unexpected keyword: " << words[2]; + return false; + } + const int64_t servicing = strings::ParseLeadingInt64Value(words[5], -1); + servicing_demands_[{tail, head}] = servicing; + } + + // Ensure there are no extraneous elements. + const int64_t next_id = (with_servicing) ? 6 : 4; + if (words.size() > next_id) { + LOG(ERROR) << "Extraneous elements in line, starting with: " + << words[next_id]; + return false; + } + + return true; +} + +namespace { +std::optional ParseNodeIndex(std::string_view text) { + const int64_t node = strings::ParseLeadingInt64Value(text, -1); + if (node < 0) { + LOG(ERROR) << "Could not parse node index: " << text; + return std::nullopt; + } + return {node - 1}; +} +} // namespace +} // namespace operations_research diff --git a/ortools/routing/carp_parser.h b/ortools/routing/carp_parser.h new file mode 100644 index 0000000000..4bbe67a204 --- /dev/null +++ b/ortools/routing/carp_parser.h @@ -0,0 +1,185 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A parser for CARPLIB instances. The base files are available online, as well +// as a description of the (Spanish-based) format: +// https://www.uv.es/belengue/carp.html ("CARPLIB") +// https://www.uv.es/~belengue/carp/READ_ME +// +// The goal is to find routes starting and ending at a depot which visit a +// set of arcs (whereas a VRP visits nodes). The objective is to minimize the +// total cost, which is due to either servicing an edge (i.e. performing the +// required action) or traversing an edge (to get to another point in space). +// Not all arcs/edges in the graph must be serviced. +// +// By this formulation, the total cost of servicing is known in advance. +// All vehicles start at the same node, the depot, having index 1. +// Servicing an edge requires resources, vehicles have a limited capacity. All +// vehicles have the same capacity. +// +// The format of the data is the following: +// +// NOMBRE : +// COMENTARIO : +// VERTICES : +// ARISTAS_REQ : +// ARISTAS_NOREQ : +// VEHICULOS : +// CAPACIDAD : +// TIPO_COSTES_ARISTAS : EXPLICITOS +// COSTE_TOTAL_REQ : +// LISTA_ARISTAS_REQ : +// ( , ) +// coste demanda +// +// LISTA_ARISTAS_NOREQ : +// ( , ) +// coste +// +// DEPOSITO : 1 +// +// While the file format is defined with 1-based indexing, the output of the +// parser is always 0-based. Users of this parser should never see any 1-based +// index; only 0-based index should be used to query values. + +#ifndef OR_TOOLS_ROUTING_CARP_PARSER_H_ +#define OR_TOOLS_ROUTING_CARP_PARSER_H_ + +#include +#include +#include +#include + +#include "ortools/base/linked_hash_map.h" +#include "ortools/base/logging.h" +#include "ortools/routing/simple_graph.h" + +namespace operations_research { +class CarpParser { + public: + CarpParser(); + +#ifndef SWIG + CarpParser(const CarpParser&) = delete; + const CarpParser& operator=(const CarpParser&) = delete; +#endif + + // Loads instance from a file into this parser object. + bool LoadFile(const std::string& file_name); + + // Returns the name of the instance being solved. + const std::string& name() const { return name_; } + // Returns the comment of the instance being solved, typically an upper bound. + const std::string& comment() const { return comment_; } + // Returns the index of the depot. + int64_t depot() const { return depot_; } + // Returns the number of nodes in the current routing problem. + int64_t NumberOfNodes() const { return number_of_nodes_; } + // Returns the number of edges in the current routing problem, with or + // without servicing required. + int64_t NumberOfEdges() const { + return NumberOfEdgesWithServicing() + NumberOfEdgesWithoutServicing(); + } + // Returns the number of edges in the current routing problem that require + // servicing. + int64_t NumberOfEdgesWithServicing() const { + return number_of_edges_with_servicing_; + } + // Returns the number of edges in the current routing problem that do not + // require servicing. + int64_t NumberOfEdgesWithoutServicing() const { + return number_of_edges_without_servicing_; + } + // Returns the total servicing cost for all arcs. + int64_t TotalServicingCost() const { return total_servicing_cost_; } + // Returns the servicing of the edges in the current routing problem. + const gtl::linked_hash_map& servicing_demands() const { + return servicing_demands_; + } + // Returns the traversing costs of the edges in the current routing problem. + const gtl::linked_hash_map& traversing_costs() const { + return traversing_costs_; + } + // Returns the maximum number of vehicles to use. + int64_t NumberOfVehicles() const { return n_vehicles_; } + // Returns the capacity of the vehicles. + int64_t capacity() const { return capacity_; } + + // Returns the traversing cost for an edge. All edges are supposed to have + // a traversing cost. + int64_t GetTraversingCost(Edge edge) const { + CHECK(traversing_costs_.contains(edge)) + << "Unknown edge: " << edge.tail() << " - " << edge.head(); + return traversing_costs_.at(edge); + } + int64_t GetTraversingCost(int64_t tail, int64_t head) const { + return GetTraversingCost({tail, head}); + } + + // Checks whether this edge requires servicing. + int64_t HasServicingNeed(Edge edge) const { + return servicing_demands_.contains(edge); + } + int64_t HasServicingNeed(int64_t tail, int64_t head) const { + return HasServicingNeed({tail, head}); + } + + // Returns the servicing for an edge. Only a subset of edges have a servicing + // need. + int64_t GetServicing(Edge edge) const { + CHECK(HasServicingNeed(edge)) + << "Unknown edge: " << edge.tail() << " - " << edge.head(); + return servicing_demands_.at(edge); + } + int64_t GetServicing(int64_t tail, int64_t head) const { + return GetServicing({tail, head}); + } + + private: + // Parsing. + enum Section { + METADATA, + ARCS_WITH_SERVICING, + ARCS_WITHOUT_SERVICING, + UNDEFINED_SECTION + }; + + void Initialize(); + bool ParseFile(const std::string& file_name); + bool ParseMetadataLine(const std::vector& words); + bool ParseEdge(std::string_view line, bool with_servicing); + + // Parsing data. + Section section_; + + // Instance data: + // - metadata + std::string name_; + std::string comment_; + int64_t number_of_nodes_; + int64_t number_of_edges_with_servicing_; + int64_t number_of_edges_without_servicing_; + int64_t total_servicing_cost_; + int64_t depot_; + // - graph costs and servicing demands. Keep track of the order of the + // demands: the output format requires to use the servicing-demands IDs, + // which are indices when iterating over this map. + gtl::linked_hash_map traversing_costs_; + gtl::linked_hash_map servicing_demands_; + // - vehicles + int64_t n_vehicles_; + int64_t capacity_; +}; +} // namespace operations_research + +#endif // OR_TOOLS_ROUTING_CARP_PARSER_H_ diff --git a/ortools/routing/carp_parser_test.cc b/ortools/routing/carp_parser_test.cc new file mode 100644 index 0000000000..e9556c9b8e --- /dev/null +++ b/ortools/routing/carp_parser_test.cc @@ -0,0 +1,270 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/carp_parser.h" + +#include + +#include "absl/flags/flag.h" +#include "gtest/gtest.h" +#include "ortools/base/mock-log.h" +#include "ortools/base/path.h" + +ABSL_FLAG(std::string, test_srcdir, "", "REQUIRED: src dir"); + +namespace operations_research { +namespace { +TEST(CarpParserTest, Constructor) { + CarpParser parser; + EXPECT_EQ(parser.name(), ""); + EXPECT_EQ(parser.comment(), ""); + EXPECT_EQ(parser.NumberOfNodes(), 0); + EXPECT_EQ(parser.NumberOfEdgesWithServicing(), 0); + EXPECT_EQ(parser.NumberOfEdgesWithoutServicing(), 0); + EXPECT_EQ(parser.NumberOfEdges(), 0); + EXPECT_EQ(parser.NumberOfVehicles(), 0); + EXPECT_EQ(parser.capacity(), 0); + EXPECT_EQ(parser.TotalServicingCost(), 0); + EXPECT_EQ(parser.depot(), 0); +} + +TEST(CarpParserTest, LoadEmptyFileName) { + std::string empty_file_name; + CarpParser parser; + EXPECT_FALSE(parser.LoadFile(empty_file_name)); +} + +TEST(CarpParserTest, LoadNonExistingFile) { + CarpParser parser; + EXPECT_FALSE(parser.LoadFile("")); +} + +TEST(CarpParserTest, LoadInvalidFileIncorrectNumberOfNodes) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Error when parsing the number of nodes: -4")); + EXPECT_CALL( + log, + Log(ERROR, testing::_, + "Error when parsing the following metadata line: VERTICES : -4")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_incorrecto_vertices.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileIncorrectNumberOfArcsWithServicings) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL( + log, Log(ERROR, testing::_, + "Error when parsing the number of edges with servicing: -11")); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Error when parsing the following metadata line: " + "ARISTAS_REQ : -11")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_incorrecto_arireq.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileIncorrectNumberOfArcsWithoutServicings) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL( + log, Log(ERROR, testing::_, + "Error when parsing the number of edges without servicing: a")); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Error when parsing the following metadata line: " + "ARISTAS_NOREQ : a")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_incorrecto_arinoreq.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileIncorrectNumberOfVehicles) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Error when parsing the number of vehicles: 0")); + EXPECT_CALL( + log, + Log(ERROR, testing::_, + "Error when parsing the following metadata line: VEHICULOS : 0")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_incorrecto_vehiculos.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileIncorrectCapacity) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL(log, + Log(ERROR, testing::_, "Error when parsing the capacity: 0")); + EXPECT_CALL( + log, + Log(ERROR, testing::_, + "Error when parsing the following metadata line: CAPACIDAD : 0")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_incorrecto_capacidad.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileIncorrectTypeOfArcCost) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Value of TIPO_COSTES_ARISTAS is unexpected, only " + "EXPLICITOS is supported, but IMPLICITOS was found")); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Error when parsing the following metadata line: " + "TIPO_COSTES_ARISTAS : IMPLICITOS")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_incorrecto_tipo.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileIncorrectTotalServicingCost) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Error when parsing the total servicing cost: qwertz")); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Error when parsing the following metadata line: " + "COSTE_TOTAL_REQ : qwertz")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_incorrecto_coste.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileIncorrectDepot) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL(log, Log(ERROR, testing::_, "Could not parse node index: -1")); + EXPECT_CALL(log, Log(ERROR, testing::_, "Error when parsing the depot: -1")); + EXPECT_CALL( + log, + Log(ERROR, testing::_, + "Error when parsing the following metadata line: DEPOSITO : -1")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_incorrecto_deposito.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileNoEdgeWithServicing) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL(log, + Log(ERROR, testing::_, + "Error when parsing the number of edges with servicing: 0")); + EXPECT_CALL( + log, + Log(ERROR, testing::_, + "Error when parsing the following metadata line: ARISTAS_REQ : 0")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/" + "testdata/carp_gdb19_no_arista_req.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileServicingForArcsWithoutServicing) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Extraneous elements in line, starting with: demanda")); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Could not parse line in LISTA_ARISTAS_NOREQ: ( 1, 4) " + "coste 3 demanda 3")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE( + parser.LoadFile(file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/" + "testdata/carp_gdb19_mixed_arcs.dat"))); +} + +TEST(CarpParserTest, LoadInvalidFileServicingForArcsInWrongOrder) { + testing::ScopedMockLog log(testing::kDoNotCaptureLogsYet); + EXPECT_CALL(log, Log(ERROR, testing::_, "Unexpected keyword: demanda")); + EXPECT_CALL(log, Log(ERROR, testing::_, + "Could not parse line in LISTA_ARISTAS_REQ: ( 1, 4) " + "demanda 3 coste 3")); + log.StartCapturingLogs(); + + CarpParser parser; + EXPECT_FALSE(parser.LoadFile( + file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_incorrecta_lista_aristas_req.dat"))); +} + +TEST(CarpParserTest, LoadInstanceFile) { + std::string file_name = + file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/carp_gdb19.dat"); + CarpParser parser; + EXPECT_TRUE(parser.LoadFile(file_name)); + EXPECT_EQ(parser.name(), "gdb19"); + EXPECT_EQ(parser.comment(), "10000 (cota superior)"); + EXPECT_EQ(parser.NumberOfNodes(), 8); + EXPECT_EQ(parser.NumberOfEdgesWithServicing(), 11); + EXPECT_EQ(parser.NumberOfEdgesWithoutServicing(), 0); + EXPECT_EQ(parser.NumberOfEdges(), 11); + EXPECT_EQ(parser.NumberOfVehicles(), 3); + EXPECT_EQ(parser.capacity(), 27); + EXPECT_EQ(parser.TotalServicingCost(), 45); + EXPECT_EQ(parser.depot(), 0); + + EXPECT_EQ(parser.traversing_costs().size(), 11); + EXPECT_EQ(parser.GetTraversingCost(0, 1), 4); + EXPECT_EQ(parser.GetTraversingCost(1, 0), 4); + EXPECT_EQ(parser.servicing_demands().size(), 11); + EXPECT_EQ(parser.GetServicing(0, 1), 8); + EXPECT_EQ(parser.GetServicing(1, 0), 8); +} + +TEST(CarpParserTest, LoadInstanceFileWithDifferentDepot) { + std::string file_name = file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/" + "carp_gdb19_diferente_deposito.dat"); + CarpParser parser; + EXPECT_TRUE(parser.LoadFile(file_name)); + EXPECT_EQ(parser.depot(), 4); +} +} // namespace +} // namespace operations_research diff --git a/ortools/routing/cvrptw_lib.cc b/ortools/routing/cvrptw_lib.cc new file mode 100644 index 0000000000..1ac4427aa3 --- /dev/null +++ b/ortools/routing/cvrptw_lib.cc @@ -0,0 +1,246 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/cvrptw_lib.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/btree_set.h" +#include "absl/memory/memory.h" +#include "absl/strings/str_format.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/util/random_engine.h" + +namespace operations_research { + +using NodeIndex = RoutingIndexManager::NodeIndex; + +int32_t GetSeed(bool deterministic) { + if (deterministic) { + return 7777777; + } else { + return absl::Uniform(absl::BitGen(), 0, + std::numeric_limits::max()); + } +} + +LocationContainer::LocationContainer(int64_t speed, bool use_deterministic_seed) + : randomizer_(GetSeed(use_deterministic_seed)), speed_(speed) { + CHECK_LT(0, speed_); +} + +void LocationContainer::AddRandomLocation(int64_t x_max, int64_t y_max) { + AddRandomLocation(x_max, y_max, 1); +} + +void LocationContainer::AddRandomLocation(int64_t x_max, int64_t y_max, + int duplicates) { + const int64_t x = randomizer_.Uniform(x_max + 1); + const int64_t y = randomizer_.Uniform(y_max + 1); + for (int i = 0; i < duplicates; ++i) { + AddLocation(x, y); + } +} + +int64_t LocationContainer::ManhattanDistance(NodeIndex from, + NodeIndex to) const { + return locations_[from].DistanceTo(locations_[to]); +} + +int64_t LocationContainer::NegManhattanDistance(NodeIndex from, + NodeIndex to) const { + return -ManhattanDistance(from, to); +} + +int64_t LocationContainer::ManhattanTime(NodeIndex from, NodeIndex to) const { + return ManhattanDistance(from, to) / speed_; +} + +bool LocationContainer::SameLocation(NodeIndex node1, NodeIndex node2) const { + if (node1 < locations_.size() && node2 < locations_.size()) { + return locations_[node1].IsAtSameLocation(locations_[node2]); + } + return false; +} +int64_t LocationContainer::SameLocationFromIndex(int64_t node1, + int64_t node2) const { + // The direct conversion from constraint model indices to routing model + // nodes is correct because the depot is node 0. + // TODO(user): Fetch proper indices from routing model. + return SameLocation(NodeIndex(node1), NodeIndex(node2)); +} + +LocationContainer::Location::Location() : x_(0), y_(0) {} + +LocationContainer::Location::Location(int64_t x, int64_t y) : x_(x), y_(y) {} + +int64_t LocationContainer::Location::DistanceTo( + const Location& location) const { + return Abs(x_ - location.x_) + Abs(y_ - location.y_); +} + +bool LocationContainer::Location::IsAtSameLocation( + const Location& location) const { + return x_ == location.x_ && y_ == location.y_; +} + +int64_t LocationContainer::Location::Abs(int64_t value) { + return std::max(value, -value); +} + +RandomDemand::RandomDemand(int size, NodeIndex depot, + bool use_deterministic_seed) + : size_(size), + depot_(depot), + use_deterministic_seed_(use_deterministic_seed) { + CHECK_LT(0, size_); +} + +void RandomDemand::Initialize() { + const int64_t kDemandMax = 5; + const int64_t kDemandMin = 1; + demand_ = std::make_unique(size_); + random_engine_t randomizer(GetSeed(use_deterministic_seed_)); + for (int order = 0; order < size_; ++order) { + if (order == depot_) { + demand_[order] = 0; + } else { + demand_[order] = + kDemandMin + randomizer.Uniform(kDemandMax - kDemandMin + 1); + } + } +} + +int64_t RandomDemand::Demand(NodeIndex from, NodeIndex /*to*/) const { + return demand_[from.value()]; +} + +ServiceTimePlusTransition::ServiceTimePlusTransition( + int64_t time_per_demand_unit, RoutingNodeEvaluator2 demand, + RoutingNodeEvaluator2 transition_time) + : time_per_demand_unit_(time_per_demand_unit), + demand_(std::move(demand)), + transition_time_(std::move(transition_time)) {} + +int64_t ServiceTimePlusTransition::Compute(NodeIndex from, NodeIndex to) const { + return time_per_demand_unit_ * demand_(from, to) + transition_time_(from, to); +} + +StopServiceTimePlusTransition::StopServiceTimePlusTransition( + int64_t stop_time, const LocationContainer& location_container, + RoutingNodeEvaluator2 transition_time) + : stop_time_(stop_time), + location_container_(location_container), + transition_time_(std::move(transition_time)) {} + +int64_t StopServiceTimePlusTransition::Compute(NodeIndex from, + NodeIndex to) const { + return location_container_.SameLocation(from, to) + ? 0 + : stop_time_ + transition_time_(from, to); +} + +void DisplayPlan( + const RoutingIndexManager& manager, const RoutingModel& routing, + const operations_research::Assignment& plan, bool use_same_vehicle_costs, + int64_t max_nodes_per_group, int64_t same_vehicle_cost, + const operations_research::RoutingDimension& capacity_dimension, + const operations_research::RoutingDimension& time_dimension) { + // Display plan cost. + std::string plan_output = absl::StrFormat("Cost %d\n", plan.ObjectiveValue()); + + // Display dropped orders. + std::string dropped; + for (int64_t order = 0; order < routing.Size(); ++order) { + if (routing.IsStart(order) || routing.IsEnd(order)) continue; + if (plan.Value(routing.NextVar(order)) == order) { + if (dropped.empty()) { + absl::StrAppendFormat(&dropped, " %d", + manager.IndexToNode(order).value()); + } else { + absl::StrAppendFormat(&dropped, ", %d", + manager.IndexToNode(order).value()); + } + } + } + if (!dropped.empty()) { + plan_output += "Dropped orders:" + dropped + "\n"; + } + + if (use_same_vehicle_costs) { + int group_size = 0; + int64_t group_same_vehicle_cost = 0; + absl::btree_set visited; + for (int64_t order = 0; order < routing.Size(); ++order) { + if (routing.IsStart(order) || routing.IsEnd(order)) continue; + ++group_size; + visited.insert(plan.Value(routing.VehicleVar(order))); + if (group_size == max_nodes_per_group) { + if (visited.size() > 1) { + group_same_vehicle_cost += (visited.size() - 1) * same_vehicle_cost; + } + group_size = 0; + visited.clear(); + } + } + if (visited.size() > 1) { + group_same_vehicle_cost += (visited.size() - 1) * same_vehicle_cost; + } + LOG(INFO) << "Same vehicle costs: " << group_same_vehicle_cost; + } + + // Display actual output for each vehicle. + for (int route_number = 0; route_number < routing.vehicles(); + ++route_number) { + int64_t order = routing.Start(route_number); + absl::StrAppendFormat(&plan_output, "Route %d: ", route_number); + if (routing.IsEnd(plan.Value(routing.NextVar(order)))) { + plan_output += "Empty\n"; + } else { + while (true) { + operations_research::IntVar* const load_var = + capacity_dimension.CumulVar(order); + operations_research::IntVar* const time_var = + time_dimension.CumulVar(order); + operations_research::IntVar* const slack_var = + routing.IsEnd(order) ? nullptr : time_dimension.SlackVar(order); + if (slack_var != nullptr && plan.Contains(slack_var)) { + absl::StrAppendFormat( + &plan_output, "%d Load(%d) Time(%d, %d) Slack(%d, %d)", + manager.IndexToNode(order).value(), plan.Value(load_var), + plan.Min(time_var), plan.Max(time_var), plan.Min(slack_var), + plan.Max(slack_var)); + } else { + absl::StrAppendFormat(&plan_output, "%d Load(%d) Time(%d, %d)", + manager.IndexToNode(order).value(), + plan.Value(load_var), plan.Min(time_var), + plan.Max(time_var)); + } + if (routing.IsEnd(order)) break; + plan_output += " -> "; + order = plan.Value(routing.NextVar(order)); + } + plan_output += "\n"; + } + } + LOG(INFO) << plan_output; +} +} // namespace operations_research diff --git a/ortools/routing/cvrptw_lib.h b/ortools/routing/cvrptw_lib.h new file mode 100644 index 0000000000..6af2313d69 --- /dev/null +++ b/ortools/routing/cvrptw_lib.h @@ -0,0 +1,136 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This header provides functions to help create random instances of the +// vehicle routing problem; random capacities and random time windows. +#ifndef OR_TOOLS_ROUTING_CVRPTW_LIB_H_ +#define OR_TOOLS_ROUTING_CVRPTW_LIB_H_ + +#include +#include +#include + +#include "ortools/base/strong_vector.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/util/random_engine.h" + +namespace operations_research { + +typedef std::function + RoutingNodeEvaluator2; + +// Random seed generator. +int32_t GetSeed(bool deterministic); + +// Location container, contains positions of orders and can be used to obtain +// Manhattan distances/times between locations. +class LocationContainer { + public: + LocationContainer(int64_t speed, bool use_deterministic_seed); + void AddLocation(int64_t x, int64_t y) { + locations_.push_back(Location(x, y)); + } + void AddRandomLocation(int64_t x_max, int64_t y_max); + void AddRandomLocation(int64_t x_max, int64_t y_max, int duplicates); + int64_t ManhattanDistance(RoutingIndexManager::NodeIndex from, + RoutingIndexManager::NodeIndex to) const; + int64_t NegManhattanDistance(RoutingIndexManager::NodeIndex from, + RoutingIndexManager::NodeIndex to) const; + int64_t ManhattanTime(RoutingIndexManager::NodeIndex from, + RoutingIndexManager::NodeIndex to) const; + + bool SameLocation(RoutingIndexManager::NodeIndex node1, + RoutingIndexManager::NodeIndex node2) const; + int64_t SameLocationFromIndex(int64_t node1, int64_t node2) const; + + private: + class Location { + public: + Location(); + Location(int64_t x, int64_t y); + int64_t DistanceTo(const Location& location) const; + bool IsAtSameLocation(const Location& location) const; + + private: + static int64_t Abs(int64_t value); + + int64_t x_; + int64_t y_; + }; + + random_engine_t randomizer_; + const int64_t speed_; + absl::StrongVector locations_; +}; + +// Random demand. +class RandomDemand { + public: + RandomDemand(int size, RoutingIndexManager::NodeIndex depot, + bool use_deterministic_seed); + void Initialize(); + int64_t Demand(RoutingIndexManager::NodeIndex from, + RoutingIndexManager::NodeIndex to) const; + + private: + std::unique_ptr demand_; + const int size_; + const RoutingIndexManager::NodeIndex depot_; + const bool use_deterministic_seed_; +}; + +// Service time (proportional to demand) + transition time callback. +class ServiceTimePlusTransition { + public: + ServiceTimePlusTransition( + int64_t time_per_demand_unit, + operations_research::RoutingNodeEvaluator2 demand, + operations_research::RoutingNodeEvaluator2 transition_time); + int64_t Compute(RoutingIndexManager::NodeIndex from, + RoutingIndexManager::NodeIndex to) const; + + private: + const int64_t time_per_demand_unit_; + operations_research::RoutingNodeEvaluator2 demand_; + operations_research::RoutingNodeEvaluator2 transition_time_; +}; + +// Stop service time + transition time callback. +class StopServiceTimePlusTransition { + public: + StopServiceTimePlusTransition( + int64_t stop_time, const LocationContainer& location_container, + operations_research::RoutingNodeEvaluator2 transition_time); + int64_t Compute(RoutingIndexManager::NodeIndex from, + RoutingIndexManager::NodeIndex to) const; + + private: + const int64_t stop_time_; + const LocationContainer& location_container_; + operations_research::RoutingNodeEvaluator2 demand_; + operations_research::RoutingNodeEvaluator2 transition_time_; +}; + +// Route plan displayer. +// TODO(user): Move the display code to the routing library. +void DisplayPlan( + const operations_research::RoutingIndexManager& manager, + const operations_research::RoutingModel& routing, + const operations_research::Assignment& plan, bool use_same_vehicle_costs, + int64_t max_nodes_per_group, int64_t same_vehicle_cost, + const operations_research::RoutingDimension& capacity_dimension, + const operations_research::RoutingDimension& time_dimension); + +} // namespace operations_research + +#endif // OR_TOOLS_ROUTING_CVRPTW_LIB_H_ diff --git a/ortools/routing/nearp_parser.cc b/ortools/routing/nearp_parser.cc new file mode 100644 index 0000000000..42c3c6a3ef --- /dev/null +++ b/ortools/routing/nearp_parser.cc @@ -0,0 +1,450 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/nearp_parser.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "ortools/base/numbers.h" +#include "ortools/util/filelineiter.h" + +namespace operations_research { + +NearpParser::NearpParser() { Initialize(); } + +void NearpParser::Initialize() { + name_.clear(); + comment_.clear(); + num_arcs_ = 0; + num_edges_ = 0; + num_nodes_ = 0; + num_arcs_with_servicing_ = 0; + num_edges_with_servicing_ = 0; + num_nodes_with_servicing_ = 0; + depot_ = 0; + arc_traversing_costs_.clear(); + edge_traversing_costs_.clear(); + arc_servicing_demands_.clear(); + edge_servicing_demands_.clear(); + node_servicing_demands_.clear(); + num_vehicles_ = 0; + capacity_ = 0; + section_ = METADATA; +} + +bool NearpParser::LoadFile(const std::string& file_name) { + Initialize(); + return ParseFile(file_name); +} + +bool NearpParser::ParseFile(const std::string& file_name) { + // Only put the first word as header, as the main check is just done on this + // first word (no ambiguity is possible for well-formed files; a more precise + // check is done for metadata). + static auto section_headers = std::array({ + "Name", + "Optimal", // "value" + "#Vehicles", + "Capacity", + "Depot", // "Node" + "#Nodes", + "#Edges", + "#Arcs", + "#Required", // "N", "E", or "A" + "ReN.", + "ReE.", + "EDGE", + "ReA.", + "ARC", + }); + + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + const std::vector words = + absl::StrSplit(line, absl::ByAnyChar(" :\t"), absl::SkipEmpty()); + + if (words.empty()) continue; + + if (absl::c_linear_search(section_headers, words[0])) { + // First, check if a new section has been met. + if (words[0] == "ReN.") { + node_servicing_demands_.reserve(NumberOfNodesWithServicing()); + section_ = NODES_WITH_SERVICING; + } else if (words[0] == "ReE.") { + edge_traversing_costs_.reserve(NumberOfEdges()); + edge_servicing_demands_.reserve(NumberOfEdgesWithServicing()); + section_ = EDGES_WITH_SERVICING; + } else if (words[0] == "EDGE") { + edge_traversing_costs_.reserve(NumberOfEdges()); + section_ = EDGES_WITHOUT_SERVICING; + } else if (words[0] == "ReA.") { + arc_traversing_costs_.reserve(NumberOfArcs()); + arc_servicing_demands_.reserve(NumberOfArcsWithServicing()); + section_ = ARCS_WITH_SERVICING; + } else if (words[0] == "ARC") { + arc_traversing_costs_.reserve(NumberOfArcs()); + section_ = ARCS_WITHOUT_SERVICING; + } else { + if (!ParseMetadataLine(words)) { + LOG(ERROR) << "Error when parsing a metadata line: " << line; + return false; + } + } + } else { + // If no new section is detected, process according to the current state. + + // Is there still data expected? Don't process the line if every element + // it should contain have been read: there might be some garbage at the + // end (like comments without delimiter). + switch (section_) { + case NODES_WITH_SERVICING: + if (node_servicing_demands_.size() == NumberOfNodesWithServicing()) + continue; + break; + case EDGES_WITH_SERVICING: + if (edge_servicing_demands_.size() == NumberOfEdgesWithServicing()) + continue; + break; + case EDGES_WITHOUT_SERVICING: + // edge_traversing_costs_ is filled with all edges, whether they need + // servicing or not. The section EDGES_WITHOUT_SERVICING must be after + // EDGES_WITH_SERVICING: once edge_traversing_costs_ has one value + // per edge (coming first from EDGES_WITH_SERVICING, then from + // EDGES_WITHOUT_SERVICING), no more value should come. + if (edge_traversing_costs_.size() == NumberOfEdges()) continue; + break; + case ARCS_WITH_SERVICING: + if (arc_servicing_demands_.size() == NumberOfArcsWithServicing()) + continue; + break; + case ARCS_WITHOUT_SERVICING: + // arc_traversing_costs_ is filled with all arcs, do they need + // servicing or not. The section ARCS_WITHOUT_SERVICING must be after + // ARCS_WITH_SERVICING: once edge_traversing_costs_ has one value + // per arc (coming first from ARCS_WITH_SERVICING, then from + // ARCS_WITHOUT_SERVICING), no more value should come. + if (arc_traversing_costs_.size() == NumberOfArcs()) continue; + break; + default: + break; + } + + // Data is still expected: parse the current line according to the state. + switch (section_) { + case NODES_WITH_SERVICING: + if (!ParseNode(line)) { + LOG(ERROR) << "Could not parse line in required nodes: " << line; + return false; + } + break; + case EDGES_WITH_SERVICING: + if (!ParseEdge(line, true)) { + LOG(ERROR) << "Could not parse line in required edges: " << line; + return false; + } + break; + case EDGES_WITHOUT_SERVICING: + if (!ParseEdge(line, false)) { + LOG(ERROR) << "Could not parse line in edges: " << line; + return false; + } + break; + case ARCS_WITH_SERVICING: + if (!ParseArc(line, true)) { + LOG(ERROR) << "Could not parse line in required arcs: " << line; + return false; + } + break; + case ARCS_WITHOUT_SERVICING: + if (!ParseArc(line, false)) { + LOG(ERROR) << "Could not parse line in arcs: " << line; + return false; + } + break; + default: + LOG(ERROR) << "Could not parse line outside node-edge-arc lists: " + << line; + return false; + } + } + } + + return section_ != METADATA; +} + +namespace { +std::optional ParseNodeIndex(std::string_view text); + +struct ArcOrEdge { + const int64_t tail; + const int64_t head; + const int64_t traversing_cost; + const int64_t servicing_demand; + const int64_t servicing_cost; +}; + +std::optional ParseArcOrEdge(std::string_view line, + bool with_servicing); +} // namespace + +bool NearpParser::ParseMetadataLine(const std::vector& words) { + if (words[0] == "Name") { + name_ = absl::StrJoin(words.begin() + 1, words.end(), " "); + } else if (words[0] == "Optimal" && words[1] == "value") { + comment_ = absl::StrJoin(words.begin() + 2, words.end(), " "); + } else if (words[0] == "#Vehicles") { + num_vehicles_ = strings::ParseLeadingInt32Value(words[1], -1); + // -1 indicates that the number of vehicles is unknown. + if (num_vehicles_ < -1) { + LOG(ERROR) << "Error when parsing the number of vehicles: " << words[1]; + return false; + } + } else if (words[0] == "Capacity") { + capacity_ = strings::ParseLeadingInt64Value(words[1], -1); + if (capacity_ <= 0) { + LOG(ERROR) << "Error when parsing the capacity: " << words[1]; + return false; + } + } else if (words[0] == "Depot" && words[1] == "Node") { + const std::optional depot = ParseNodeIndex(words[2]); + if (!depot.has_value()) { + LOG(ERROR) << "Error when parsing the depot: " << words[1]; + return false; + } + depot_ = depot.value(); + } else if (words[0] == "#Nodes") { + num_nodes_ = strings::ParseLeadingInt32Value(words[1], -1); + if (num_nodes_ <= 0) { + LOG(ERROR) << "Error when parsing the number of nodes: " << words[1]; + return false; + } + } else if (words[0] == "#Edges") { + num_edges_ = strings::ParseLeadingInt32Value(words[1], -1); + if (num_edges_ <= 0) { + LOG(ERROR) << "Error when parsing the number of edges: " << words[1]; + return false; + } + } else if (words[0] == "#Arcs") { + num_arcs_ = strings::ParseLeadingInt32Value(words[1], -1); + if (num_arcs_ <= 0) { + LOG(ERROR) << "Error when parsing the number of arcs: " << words[1]; + return false; + } + } else if (words[0] == "#Required" && words[1] == "N") { + num_nodes_with_servicing_ = strings::ParseLeadingInt32Value(words[2], -1); + if (num_nodes_with_servicing_ <= 0) { + LOG(ERROR) << "Error when parsing the number of nodes with servicing: " + << words[1]; + return false; + } + } else if (words[0] == "#Required" && words[1] == "E") { + num_edges_with_servicing_ = strings::ParseLeadingInt32Value(words[2], -1); + if (num_edges_with_servicing_ <= 0) { + LOG(ERROR) << "Error when parsing the number of edges with servicing: " + << words[1]; + return false; + } + } else if (words[0] == "#Required" && words[1] == "A") { + num_arcs_with_servicing_ = strings::ParseLeadingInt32Value(words[2], -1); + if (num_arcs_with_servicing_ <= 0) { + LOG(ERROR) << "Error when parsing the number of arcs with servicing: " + << words[1]; + return false; + } + } else { + LOG(ERROR) << "Unrecognized metadata line: " << absl::StrJoin(words, " "); + return false; + } + return true; +} + +bool NearpParser::ParseArc(std::string_view line, bool with_servicing) { + std::optional parsed_arc = ParseArcOrEdge(line, with_servicing); + if (!parsed_arc.has_value()) { + return false; + } + + Arc arc{parsed_arc->tail, parsed_arc->head}; + arc_traversing_costs_[arc] = parsed_arc->traversing_cost; + + if (with_servicing) { + arc_servicing_demands_[arc] = parsed_arc->servicing_demand; + arc_servicing_costs_[arc] = parsed_arc->servicing_cost; + } + + return true; +} + +bool NearpParser::ParseEdge(std::string_view line, bool with_servicing) { + std::optional parsed_edge = ParseArcOrEdge(line, with_servicing); + if (!parsed_edge.has_value()) { + return false; + } + + Edge edge{parsed_edge->tail, parsed_edge->head}; + edge_traversing_costs_[edge] = parsed_edge->traversing_cost; + + if (with_servicing) { + edge_servicing_demands_[edge] = parsed_edge->servicing_demand; + edge_servicing_costs_[edge] = parsed_edge->servicing_cost; + } + + return true; +} + +bool NearpParser::ParseNode(std::string_view line) { + const std::vector words = + absl::StrSplit(line, absl::ByAnyChar(" :\t(),"), absl::SkipEmpty()); + + // Parse the name and ID. + const int64_t id = strings::ParseLeadingInt64Value(words[0].substr(1), 0) - 1; + if (id < 0) { + LOG(ERROR) << "Error when parsing the node name: " << words[0]; + return false; + } + + // Parse the servicing details if needed. + const int64_t servicing_demand = + strings::ParseLeadingInt64Value(words[1], -1); + if (servicing_demand < 0) { + LOG(ERROR) << "Error when parsing the node servicing demand: " << words[1]; + return false; + } + + const int64_t servicing_cost = strings::ParseLeadingInt64Value(words[2], -1); + if (servicing_cost < 0) { + LOG(ERROR) << "Error when parsing the node servicing cost: " << words[1]; + return false; + } + + // Once the values have been parsed successfully, save them. + node_servicing_demands_.insert({id, servicing_demand}); + node_servicing_costs_.insert({id, servicing_cost}); + + return true; +} + +namespace { +std::optional ParseNodeIndex(std::string_view text) { + const int64_t node = strings::ParseLeadingInt64Value(text, -1); + if (node < 0) { + LOG(ERROR) << "Could not parse node index: " << text; + return std::nullopt; + } + return {node - 1}; +} + +std::optional ParseArcOrEdge(std::string_view line, + bool with_servicing) { + const std::vector words = + absl::StrSplit(line, absl::ByAnyChar(" :\t(),"), absl::SkipEmpty()); + + // Parse the name. + const std::string name = words[0]; + + // Parse the tail and the head of the arc/edge. + std::optional opt_tail = ParseNodeIndex(words[1]); + if (!opt_tail.has_value()) { + LOG(ERROR) << "Error when parsing the tail node: " << words[1]; + return {}; + } + const int64_t tail = opt_tail.value(); + + std::optional opt_head = ParseNodeIndex(words[2]); + if (!opt_head.has_value()) { + LOG(ERROR) << "Error when parsing the head node: " << words[2]; + return {}; + } + const int64_t head = opt_head.value(); + + if (tail == head) { + LOG(ERROR) << "The head and tail nodes are identical: " << line; + return {}; + } + + // Parse the traversing cost. + const int64_t traversing_cost = strings::ParseLeadingInt64Value(words[3], -1); + if (traversing_cost < 0) { + LOG(ERROR) << "Error when parsing the traversing cost: " << words[3]; + return {}; + } + + // Ensure there are no extraneous elements. + const int64_t next_id = (with_servicing) ? 6 : 4; + if (words.size() > next_id) { + LOG(ERROR) << "Extraneous elements in line, starting with: " + << words[next_id]; + return {}; + } + + // Parse the servicing details if needed and return the elements. + if (!with_servicing) { + return ArcOrEdge{tail, head, traversing_cost, + /*servicing_demand=*/-1, + /*servicing_cost=*/-1}; + } else { + const int64_t servicing_demand = + strings::ParseLeadingInt64Value(words[4], -1); + if (servicing_demand < 0) { + LOG(ERROR) << "Error when parsing the servicing demand: " << words[4]; + return {}; + } + + const int64_t servicing_cost = + strings::ParseLeadingInt64Value(words[5], -1); + if (servicing_cost < 0) { + LOG(ERROR) << "Error when parsing the servicing cost: " << words[5]; + return {}; + } + + return ArcOrEdge{tail, head, traversing_cost, servicing_demand, + servicing_cost}; + } +} +} // namespace + +std::string NearpParser::GetArcName(Arc arc) const { + if (arc_servicing_costs_.contains(arc)) { + int64_t arc_position = std::distance(arc_servicing_demands_.begin(), + arc_servicing_demands_.find(arc)); + CHECK_LT(arc_position, arc_servicing_demands_.size()); + return absl::StrCat("A", arc_position + 1); + } else { + int64_t arc_position = std::distance(arc_traversing_costs_.begin(), + arc_traversing_costs_.find(arc)); + CHECK_LT(arc_position, arc_traversing_costs_.size()); + return absl::StrCat("NrA", arc_position - num_arcs_with_servicing_ + 1); + } +} + +std::string NearpParser::GetEdgeName(Edge edge) const { + if (edge_servicing_costs_.contains(edge)) { + int64_t edge_position = std::distance(edge_servicing_demands_.begin(), + edge_servicing_demands_.find(edge)); + CHECK_LT(edge_position, edge_servicing_demands_.size()); + return absl::StrCat("E", edge_position + 1); + } else { + int64_t edge_position = std::distance(edge_traversing_costs_.begin(), + edge_traversing_costs_.find(edge)); + CHECK_LT(edge_position, edge_traversing_costs_.size()); + return absl::StrCat("NrE", edge_position - num_edges_with_servicing_ + 1); + } +} +} // namespace operations_research diff --git a/ortools/routing/nearp_parser.h b/ortools/routing/nearp_parser.h new file mode 100644 index 0000000000..54a9c25d44 --- /dev/null +++ b/ortools/routing/nearp_parser.h @@ -0,0 +1,257 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A parser for NEARPLIB instances. The base files are available online, as well +// as a description of the format: +// https://www.sintef.no/projectweb/top/nearp/documentation/ +// +// The goal is to find routes starting and ending at a depot which visit a +// set of arcs (directed), edges (undirected), and nodes, whereas a VRP only +// visits nodes. The objective is to minimize the total cost, which is due to +// either servicing a part of the graph (i.e. performing the required action) +// or traversing an edge (to get to another point in space). Not all arcs/edges +// in the graph must be serviced. These components are summarized in NEARP: +// node-edge-arc routing problem. The problem is sometimes also called MCGRP: +// mixed capacitated generalized routing problem. +// +// All vehicles start at the same node, the depot. Its index is often 1, but +// many instances have another depot. +// Servicing a part of the graph requires resources, vehicles have a limited +// capacity. All vehicles have the same capacity. +// +// The format of the data is the following (from +// https://www.sintef.no/projectweb/top/nearp/documentation/): +// +// Name: +// Optimal value: +// #Vehicles: +// Capacity: +// Depot: +// #Nodes: +// #Edges: +// #Arcs: +// #Required N: +// #Required E: +// #Required A: +// +// % Required nodes: Ni q_i s_i +// NODE INDEX, DEMAND, SERVICE COST +// +// % Required edges: Ek i j q_ij c_ij s_ij +// EDGE INDEX, FROM NODE, TO NODE, TRAVERSAL COST, DEMAND, SERVICE COST +// +// % Non-required edges: NrEl i j c_ij +// EDGE INDEX, FROM NODE, TO NODE, TRAVERSAL COST +// +// % Required arcs: Ar i j q_ij c_ij +// ARC INDEX, FROM NODE, TO NODE, TRAVERSAL COST, DEMAND, SERVICE COST +// +// % Non-required arcs: NrAs i j c_ij +// ARC INDEX, FROM NODE, TO NODE, TRAVERSAL COST +// +// For nodes, the index is of the form NX, where X is the node index (for +// instance, N1 is the first node that requires servicing). The elements of +// each section are not necessarily sorted. Nodes are indexed together, with no +// separation between those that require servicing and those that do not, +// from 1 to the number of nodes. Conversely, arcs and edges have separate +// indexing depending on whether they require indexing: E1 to EM all require +// servicing, NrE1 to NrEN do not, for a total of M + N edges (respectively, +// for arcs, A1 to AK and NrA1 to NrAL for K + L arcs). +// +// While the file format is defined with 1-based indexing, the output of the +// parser is always 0-based. Users of this parser should never see any 1-based +// index; only 0-based index should be used to query values. + +#ifndef OR_TOOLS_ROUTING_NEARP_PARSER_H_ +#define OR_TOOLS_ROUTING_NEARP_PARSER_H_ + +#include +#include +#include +#include + +#include "ortools/base/linked_hash_map.h" +#include "ortools/base/logging.h" +#include "ortools/routing/simple_graph.h" + +namespace operations_research { +class NearpParser { + public: + NearpParser(); + +#ifndef SWIG + NearpParser(const NearpParser&) = delete; + const NearpParser& operator=(const NearpParser&) = delete; +#endif + + // Loads instance from a file into this parser object. + bool LoadFile(const std::string& file_name); + + // Returns the name of the instance being solved. + const std::string& name() const { return name_; } + // Returns the comment of the instance being solved, typically an upper bound. + const std::string& comment() const { return comment_; } + // Returns the index of the depot. + int64_t depot() const { return depot_; } + + // Returns the maximum number of vehicles to use. + int NumberOfVehicles() const { return num_vehicles_; } + // Returns the capacity of the vehicles. + int64_t capacity() const { return capacity_; } + + // Returns the number of nodes in the current routing problem. + int NumberOfNodes() const { return num_nodes_; } + // Returns the number of arc in the current routing problem, with or + // without servicing required. + int NumberOfArcs() const { return num_arcs_; } + // Returns the number of edges in the current routing problem, with or + // without servicing required. + int NumberOfEdges() const { return num_edges_; } + + // Returns the number of arcs in the current routing problem that require + // servicing. + int NumberOfArcsWithServicing() const { return num_arcs_with_servicing_; } + // Returns the number of edges in the current routing problem that require + // servicing. + int NumberOfEdgesWithServicing() const { return num_edges_with_servicing_; } + // Returns the number of nodes in the current routing problem that require + // servicing. + int NumberOfNodesWithServicing() const { return num_nodes_with_servicing_; } + + // Returns the number of arcs in the current routing problem that do not + // require servicing. + int NumberOfArcsWithoutServicing() const { + return NumberOfArcs() - NumberOfArcsWithServicing(); + } + // Returns the number of edges in the current routing problem that do not + // require servicing. + int NumberOfEdgesWithoutServicing() const { + return NumberOfEdges() - NumberOfEdgesWithServicing(); + } + // Returns the number of nodes in the current routing problem that do not + // require servicing. + int64_t NumberOfNodesWithoutServicing() const { + return NumberOfNodes() - NumberOfNodesWithServicing(); + } + + // Returns the servicing demands of the arcs in the current routing problem. + const gtl::linked_hash_map& arc_servicing_demands() const { + return arc_servicing_demands_; + } + // Returns the servicing demands of the edges in the current routing problem. + const gtl::linked_hash_map& edge_servicing_demands() const { + return edge_servicing_demands_; + } + // Returns the servicing demands of the nodes in the current routing problem. + const gtl::linked_hash_map& node_servicing_demands() const { + return node_servicing_demands_; + } + + // Returns the servicing costs of the arcs in the current routing problem. + const gtl::linked_hash_map& arc_servicing_costs() const { + return arc_servicing_costs_; + } + // Returns the servicing costs of the edges in the current routing problem. + const gtl::linked_hash_map& edge_servicing_costs() const { + return edge_servicing_costs_; + } + // Returns the servicing costs of the nodes in the current routing problem. + const gtl::linked_hash_map& node_servicing_costs() const { + return node_servicing_costs_; + } + + // Returns the traversing costs of the arcs in the current routing problem. + const gtl::linked_hash_map& arc_traversing_costs() const { + return arc_traversing_costs_; + } + // Returns the traversing costs of the edges in the current routing problem. + const gtl::linked_hash_map& edge_traversing_costs() const { + return edge_traversing_costs_; + } + + // Returns the name of graph objects. The implementations should fit all + // instances of NEARP files, + std::string GetArcName(Arc arc) const; + std::string GetArcName(int64_t tail, int64_t head) const { + return GetArcName({tail, head}); + } + std::string GetEdgeName(Edge edge) const; + std::string GetEdgeName(int64_t tail, int64_t head) const { + return GetEdgeName({tail, head}); + } + std::string GetNodeName(int64_t node) const { + CHECK_GE(node, 0); + CHECK_LT(node, NumberOfNodes()); + return absl::StrCat("N", node + 1); + } + + private: + // Parsing. + enum Section { + METADATA, + ARCS_WITH_SERVICING, + ARCS_WITHOUT_SERVICING, + EDGES_WITH_SERVICING, + EDGES_WITHOUT_SERVICING, + NODES_WITH_SERVICING, + // No need for a state to parse nodes without servicing demands: they do + // not have any data associated with them (their number is known in the + // header of the data file). + UNDEFINED_SECTION + }; + + void Initialize(); + bool ParseFile(const std::string& file_name); + bool ParseMetadataLine(const std::vector& words); + bool ParseArc(std::string_view line, bool with_servicing); + bool ParseEdge(std::string_view line, bool with_servicing); + bool ParseNode(std::string_view line); + + // Parsing data. + Section section_; + + // Instance data: + // - metadata + std::string name_; + std::string comment_; + int num_arcs_; + int num_edges_; + int num_nodes_; + int num_arcs_with_servicing_; + int num_edges_with_servicing_; + int num_nodes_with_servicing_; + int64_t depot_; + + // - graph costs and servicing demands. Keep track of the order of the + // demands: the output format requires to use the servicing-demands IDs, + // which are indices when iterating through these maps. + // Specifically, for nodes, a vector is not suitable, as indices are not + // necessarily contiguous. + gtl::linked_hash_map arc_traversing_costs_; + gtl::linked_hash_map edge_traversing_costs_; + + gtl::linked_hash_map arc_servicing_demands_; + gtl::linked_hash_map edge_servicing_demands_; + gtl::linked_hash_map node_servicing_demands_; + + gtl::linked_hash_map arc_servicing_costs_; + gtl::linked_hash_map edge_servicing_costs_; + gtl::linked_hash_map node_servicing_costs_; + + // - vehicles + int num_vehicles_; + int64_t capacity_; +}; +} // namespace operations_research + +#endif // OR_TOOLS_ROUTING_NEARP_PARSER_H_ diff --git a/ortools/routing/nearp_parser_test.cc b/ortools/routing/nearp_parser_test.cc new file mode 100644 index 0000000000..2f9def3f01 --- /dev/null +++ b/ortools/routing/nearp_parser_test.cc @@ -0,0 +1,134 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/nearp_parser.h" + +#include + +#include "absl/flags/flag.h" +#include "gtest/gtest.h" +#include "ortools/base/mock-log.h" +#include "ortools/base/path.h" + +ABSL_FLAG(std::string, test_srcdir, "", "REQUIRED: src dir"); + +namespace operations_research { +namespace { +TEST(NearpParserTest, Constructor) { + NearpParser parser; + EXPECT_EQ(parser.name(), ""); + EXPECT_EQ(parser.comment(), ""); + EXPECT_EQ(parser.NumberOfNodes(), 0); + EXPECT_EQ(parser.NumberOfEdgesWithServicing(), 0); + EXPECT_EQ(parser.NumberOfEdgesWithoutServicing(), 0); + EXPECT_EQ(parser.NumberOfEdges(), 0); + EXPECT_EQ(parser.NumberOfVehicles(), 0); + EXPECT_EQ(parser.capacity(), 0); + EXPECT_EQ(parser.depot(), 0); +} + +TEST(NearpParserTest, LoadEmptyFileName) { + std::string empty_file_name; + NearpParser parser; + EXPECT_FALSE(parser.LoadFile(empty_file_name)); +} + +TEST(NearpParserTest, LoadNonExistingFile) { + NearpParser parser; + EXPECT_FALSE(parser.LoadFile("google2/nonexistent.dat")); +} + +TEST(NearpParserTest, LoadBHW1) { + std::string file_name = + file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/nearp_BHW1.dat"); + NearpParser parser; + EXPECT_TRUE(parser.LoadFile(file_name)); + EXPECT_EQ(parser.name(), "BHW1"); + EXPECT_EQ(parser.comment(), "-1"); + EXPECT_EQ(parser.NumberOfNodes(), 12); + EXPECT_EQ(parser.NumberOfNodesWithServicing(), 7); + EXPECT_EQ(parser.NumberOfNodesWithoutServicing(), 5); + EXPECT_EQ(parser.NumberOfEdges(), 11); + EXPECT_EQ(parser.NumberOfEdgesWithServicing(), 11); + EXPECT_EQ(parser.NumberOfEdgesWithoutServicing(), 0); + EXPECT_EQ(parser.NumberOfArcs(), 22); + EXPECT_EQ(parser.NumberOfArcsWithServicing(), 11); + EXPECT_EQ(parser.NumberOfArcsWithoutServicing(), 11); + EXPECT_EQ(parser.NumberOfVehicles(), -1); + EXPECT_EQ(parser.capacity(), 5); + EXPECT_EQ(parser.depot(), 0); + + EXPECT_EQ(parser.arc_traversing_costs().size(), 22); + EXPECT_EQ(parser.arc_servicing_costs().size(), 11); + EXPECT_EQ(parser.arc_servicing_demands().size(), 11); + EXPECT_EQ(parser.edge_traversing_costs().size(), 11); + EXPECT_EQ(parser.edge_servicing_demands().size(), 11); + EXPECT_EQ(parser.edge_servicing_costs().size(), 11); + EXPECT_EQ(parser.node_servicing_demands().size(), 7); + EXPECT_EQ(parser.node_servicing_costs().size(), 7); + + EXPECT_EQ(parser.GetArcName(0, 1), "A1"); + EXPECT_EQ(parser.GetArcName(Arc(0, 1)), "A1"); + EXPECT_EQ(parser.GetArcName(3, 0), "NrA2"); + EXPECT_EQ(parser.GetArcName(Arc(3, 0)), "NrA2"); + EXPECT_EQ(parser.GetEdgeName(2, 1), "E1"); + EXPECT_EQ(parser.GetEdgeName(Edge(2, 1)), "E1"); + EXPECT_EQ(parser.GetEdgeName(1, 2), "E1"); + EXPECT_EQ(parser.GetEdgeName(Edge(1, 2)), "E1"); + EXPECT_EQ(parser.GetNodeName(3), "N4"); +} + +TEST(NearpParserTest, LoadToy) { + std::string file_name = + file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/nearp_toy.dat"); + NearpParser parser; + EXPECT_TRUE(parser.LoadFile(file_name)); + EXPECT_EQ(parser.name(), "Toy"); + EXPECT_EQ(parser.comment(), "-1"); + EXPECT_EQ(parser.NumberOfNodes(), 4); + EXPECT_EQ(parser.NumberOfNodesWithServicing(), 1); + EXPECT_EQ(parser.NumberOfNodesWithoutServicing(), 3); + EXPECT_EQ(parser.NumberOfEdges(), 3); + EXPECT_EQ(parser.NumberOfEdgesWithServicing(), 2); + EXPECT_EQ(parser.NumberOfEdgesWithoutServicing(), 1); + EXPECT_EQ(parser.NumberOfArcs(), 3); + EXPECT_EQ(parser.NumberOfArcsWithServicing(), 2); + EXPECT_EQ(parser.NumberOfArcsWithoutServicing(), 1); + EXPECT_EQ(parser.NumberOfVehicles(), -1); + EXPECT_EQ(parser.capacity(), 5); + EXPECT_EQ(parser.depot(), 0); + + EXPECT_EQ(parser.arc_traversing_costs().size(), 3); + EXPECT_EQ(parser.arc_servicing_costs().size(), 2); + EXPECT_EQ(parser.arc_servicing_demands().size(), 2); + EXPECT_EQ(parser.edge_traversing_costs().size(), 3); + EXPECT_EQ(parser.edge_servicing_demands().size(), 2); + EXPECT_EQ(parser.edge_servicing_costs().size(), 2); + EXPECT_EQ(parser.node_servicing_demands().size(), 1); + EXPECT_EQ(parser.node_servicing_costs().size(), 1); + + EXPECT_DEATH(parser.GetArcName(0, 1), ""); + EXPECT_DEATH(parser.GetArcName(3, 0), ""); + EXPECT_DEATH(parser.GetEdgeName(3, 1), ""); + EXPECT_DEATH(parser.GetEdgeName(1, 3), ""); + + EXPECT_EQ(parser.GetArcName(1, 3), "A1"); + EXPECT_EQ(parser.GetArcName(3, 1), "NrA1"); + EXPECT_EQ(parser.GetEdgeName(2, 1), "E2"); + EXPECT_EQ(parser.GetEdgeName(1, 2), "E2"); + EXPECT_EQ(parser.GetNodeName(3), "N4"); +} +} // namespace +} // namespace operations_research diff --git a/ortools/routing/pdtsp_parser.cc b/ortools/routing/pdtsp_parser.cc new file mode 100644 index 0000000000..8f48a605d4 --- /dev/null +++ b/ortools/routing/pdtsp_parser.cc @@ -0,0 +1,106 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/pdtsp_parser.h" + +#include +#include +#include + +#include "absl/strings/str_split.h" +#include "ortools/base/gzipfile.h" +#include "ortools/base/mathutil.h" +#include "ortools/base/numbers.h" +#include "ortools/base/path.h" +#include "ortools/base/strtoint.h" +#include "ortools/util/filelineiter.h" + +namespace operations_research { +namespace { + +using absl::ByAnyChar; + +File* OpenReadOnly(const std::string& file_name) { + File* file = nullptr; + if (file::Open(file_name, "r", &file, file::Defaults()).ok() && + file::Extension(file_name) == "gz") { + file = GZipFileReader(file_name, file, TAKE_OWNERSHIP); + } + return file; +} +} // namespace + +PdTspParser::PdTspParser() : section_(SIZE_SECTION) {} + +bool PdTspParser::LoadFile(const std::string& file_name) { + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + ProcessNewLine(line); + } + return true; +} + +std::function PdTspParser::Distances() const { + std::function distances = [this](int from, int to) { + const double xd = x_[from] - x_[to]; + const double yd = y_[from] - y_[to]; + const double d = sqrt(xd * xd + yd * yd); + return MathUtil::FastInt64Round(d); + }; + return distances; +} + +void PdTspParser::ProcessNewLine(const std::string& line) { + const std::vector words = + absl::StrSplit(line, ByAnyChar(" :\t"), absl::SkipEmpty()); + if (!words.empty()) { + switch (section_) { + case SIZE_SECTION: { + const int size = atoi64(words[0]); + x_.resize(size, 0); + y_.resize(size, 0); + deliveries_.resize(size, -1); + section_ = DEPOT_SECTION; + break; + } + case DEPOT_SECTION: + depot_ = atoi64(words[0]) - 1; + x_[depot_] = atoi64(words[1]); + y_[depot_] = atoi64(words[2]); + deliveries_[depot_] = -1; + section_ = NODE_SECTION; + break; + case NODE_SECTION: { + const int kEof = -999; + const int id = atoi64(words[0]) - 1; + if (id + 1 == kEof) { + section_ = EOF_SECTION; + } else { + x_[id] = atoi64(words[1]); + y_[id] = atoi64(words[2]); + const bool is_pickup = atoi64(words[3]) == 0; + if (is_pickup) { + deliveries_[id] = atoi64(words[4]) - 1; + } + } + break; + } + case EOF_SECTION: + break; + default: + break; + } + } +} + +} // namespace operations_research diff --git a/ortools/routing/pdtsp_parser.h b/ortools/routing/pdtsp_parser.h new file mode 100644 index 0000000000..746f95af9a --- /dev/null +++ b/ortools/routing/pdtsp_parser.h @@ -0,0 +1,59 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A TSPPD parser used to parse instances of Traveling Salesman Problems with +// pickup and delivery constraints. This format was created by Stefan Ropke. +// https://link.springer.com/article/10.1007%2Fs10107-008-0234-9 + +#ifndef OR_TOOLS_ROUTING_PDTSP_PARSER_H_ +#define OR_TOOLS_ROUTING_PDTSP_PARSER_H_ + +#include +#include +#include + +#include "ortools/base/integral_types.h" + +namespace operations_research { + +class PdTspParser { + public: + PdTspParser(); + ~PdTspParser() = default; + // Loads and parse a PDTSP from a given file. + bool LoadFile(const std::string& file_name); + // Returns the index of the depot. + int depot() const { return depot_; } + // Returns the number of nodes in the PDTSP. + int Size() const { return x_.size(); } + // Returns true if the index corresponding to a node is a pickup. + bool IsPickup(int index) const { return deliveries_[index] >= 0; } + // Returns the delivery corresponding to a pickup. + int DeliveryFromPickup(int index) const { return deliveries_[index]; } + // Returns a function returning distances between nodes. + std::function Distances() const; + + private: + enum Sections { SIZE_SECTION, DEPOT_SECTION, NODE_SECTION, EOF_SECTION }; + void ProcessNewLine(const std::string& line); + + int depot_; + Sections section_; + std::vector x_; + std::vector y_; + std::vector deliveries_; +}; + +} // namespace operations_research + +#endif // OR_TOOLS_ROUTING_PDTSP_PARSER_H_ diff --git a/ortools/routing/pdtsp_parser_test.cc b/ortools/routing/pdtsp_parser_test.cc new file mode 100644 index 0000000000..9e634c5087 --- /dev/null +++ b/ortools/routing/pdtsp_parser_test.cc @@ -0,0 +1,50 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/pdtsp_parser.h" + +#include +#include + +#include "absl/flags/flag.h" +#include "gtest/gtest.h" +#include "ortools/base/helpers.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/path.h" + +ABSL_FLAG(std::string, test_srcdir, "", "REQUIRED: src dir"); + +namespace operations_research { +namespace { +TEST(PdTspParserTest, LoadDataSet) { + for (const std::string& data : {"ortools/routing/testdata/pdtsp_prob10b.txt", + "ortools/routing/testdata/" + "pdtsp_prob10b.txt.gz"}) { + PdTspParser parser; + EXPECT_TRUE(parser.LoadFile( + file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), data))); + EXPECT_EQ(0, parser.depot()); + EXPECT_EQ(21, parser.Size()); + EXPECT_FALSE(parser.IsPickup(0)); // depot + EXPECT_FALSE(parser.IsPickup(11)); // delivery + EXPECT_TRUE(parser.IsPickup(2)); // pickup + EXPECT_EQ(12, parser.DeliveryFromPickup(2)); + std::function distances = parser.Distances(); + for (int i = 0; i < parser.Size(); ++i) { + EXPECT_EQ(0, distances(i, i)); + } + EXPECT_EQ(557, distances(1, 20)); + } +} +} // namespace +} // namespace operations_research diff --git a/ortools/routing/samples/BUILD.bazel b/ortools/routing/samples/BUILD.bazel new file mode 100644 index 0000000000..fd012912da --- /dev/null +++ b/ortools/routing/samples/BUILD.bazel @@ -0,0 +1,81 @@ +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cc_binary( + name = "cvrptw", + srcs = ["cvrptw.cc"], + deps = [ + "//ortools/base", + "//ortools/constraint_solver:routing", + "//ortools/constraint_solver:routing_flags", + "//ortools/routing:cvrptw_lib", + ], +) + +cc_binary( + name = "cvrp_disjoint_tw", + srcs = ["cvrp_disjoint_tw.cc"], + deps = [ + "//ortools/base", + "//ortools/constraint_solver:routing", + "//ortools/constraint_solver:routing_flags", + "//ortools/routing:cvrptw_lib", + ], +) + +cc_binary( + name = "cvrptw_with_breaks", + srcs = ["cvrptw_with_breaks.cc"], + deps = [ + "//ortools/base", + "//ortools/constraint_solver:routing", + "//ortools/constraint_solver:routing_enums_cc_proto", + "//ortools/constraint_solver:routing_flags", + "//ortools/routing:cvrptw_lib", + "@com_google_absl//absl/strings", + ], +) + +cc_binary( + name = "cvrptw_with_resources", + srcs = ["cvrptw_with_resources.cc"], + deps = [ + "//ortools/base", + "//ortools/constraint_solver:routing", + "//ortools/constraint_solver:routing_flags", + "//ortools/routing:cvrptw_lib", + ], +) + +cc_binary( + name = "cvrptw_with_stop_times_and_resources", + srcs = ["cvrptw_with_stop_times_and_resources.cc"], + deps = [ + "//ortools/base", + "//ortools/constraint_solver:routing", + "//ortools/constraint_solver:routing_flags", + "//ortools/routing:cvrptw_lib", + "@com_google_absl//absl/strings", + ], +) + +cc_binary( + name = "cvrptw_with_refueling", + srcs = ["cvrptw_with_refueling.cc"], + deps = [ + "//ortools/base", + "//ortools/constraint_solver:routing", + "//ortools/constraint_solver:routing_flags", + "//ortools/routing:cvrptw_lib", + ], +) diff --git a/ortools/routing/samples/cvrp_disjoint_tw.cc b/ortools/routing/samples/cvrp_disjoint_tw.cc new file mode 100644 index 0000000000..fc329d270f --- /dev/null +++ b/ortools/routing/samples/cvrp_disjoint_tw.cc @@ -0,0 +1,197 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// Capacitated Vehicle Routing Problem with Disjoint Time Windows (and optional +// orders). +// A description of the problem can be found here: +// http://en.wikipedia.org/wiki/Vehicle_routing_problem. +// The variant which is tackled by this model includes a capacity dimension, +// disjoint time windows and optional orders, with a penalty cost if orders are +// not performed. For the sake of simplicity, orders are randomly located and +// distances are computed using the Manhattan distance. Distances are assumed +// to be in meters and times in seconds. + +#include +#include +#include +#include + +#include "absl/random/random.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/cvrptw_lib.h" + +using operations_research::Assignment; +using operations_research::DefaultRoutingSearchParameters; +using operations_research::GetSeed; +using operations_research::LocationContainer; +using operations_research::RandomDemand; +using operations_research::RoutingDimension; +using operations_research::RoutingIndexManager; +using operations_research::RoutingModel; +using operations_research::RoutingNodeIndex; +using operations_research::RoutingSearchParameters; +using operations_research::ServiceTimePlusTransition; +using operations_research::Solver; + +ABSL_FLAG(int, vrp_orders, 100, "Number of nodes in the problem."); +ABSL_FLAG(int, vrp_vehicles, 20, "Number of vehicles in the problem."); +ABSL_FLAG(int, vrp_windows, 5, "Number of disjoint windows per node."); +ABSL_FLAG(bool, vrp_use_deterministic_random_seed, false, + "Use deterministic random seeds."); +ABSL_FLAG(bool, vrp_use_same_vehicle_costs, false, + "Use same vehicle costs in the routing model"); +ABSL_FLAG(std::string, routing_search_parameters, "", + "Text proto RoutingSearchParameters (possibly partial) that will " + "override the DefaultRoutingSearchParameters()"); + +const char* kTime = "Time"; +const char* kCapacity = "Capacity"; +const int64_t kMaxNodesPerGroup = 10; +const int64_t kSameVehicleCost = 1000; + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) + << "Specify a non-null vehicle fleet size."; + // VRP of size absl::GetFlag(FLAGS_vrp_size). + // Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and + // ends of the routes are at node 0. + const RoutingIndexManager::NodeIndex kDepot(0); + RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1, + absl::GetFlag(FLAGS_vrp_vehicles), kDepot); + RoutingModel routing(manager); + + // Setting up locations. + const int64_t kXMax = 100000; + const int64_t kYMax = 100000; + const int64_t kSpeed = 10; + LocationContainer locations( + kSpeed, absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + for (int location = 0; location <= absl::GetFlag(FLAGS_vrp_orders); + ++location) { + locations.AddRandomLocation(kXMax, kYMax); + } + + // Setting the cost function. + const int vehicle_cost = routing.RegisterTransitCallback( + [&locations, &manager](int64_t i, int64_t j) { + return locations.ManhattanDistance(manager.IndexToNode(i), + manager.IndexToNode(j)); + }); + routing.SetArcCostEvaluatorOfAllVehicles(vehicle_cost); + + // Adding capacity dimension constraints. + const int64_t kVehicleCapacity = 40; + const int64_t kNullCapacitySlack = 0; + RandomDemand demand(manager.num_nodes(), kDepot, + absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + demand.Initialize(); + routing.AddDimension(routing.RegisterTransitCallback( + [&demand, &manager](int64_t i, int64_t j) { + return demand.Demand(manager.IndexToNode(i), + manager.IndexToNode(j)); + }), + kNullCapacitySlack, kVehicleCapacity, + /*fix_start_cumul_to_zero=*/true, kCapacity); + + // Adding time dimension constraints. + const int64_t kTimePerDemandUnit = 300; + const int64_t kHorizon = 24 * 3600; + ServiceTimePlusTransition time( + kTimePerDemandUnit, + [&demand](RoutingNodeIndex i, RoutingNodeIndex j) { + return demand.Demand(i, j); + }, + [&locations](RoutingNodeIndex i, RoutingNodeIndex j) { + return locations.ManhattanTime(i, j); + }); + routing.AddDimension( + routing.RegisterTransitCallback([&time, &manager](int64_t i, int64_t j) { + return time.Compute(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kHorizon, kHorizon, /*fix_start_cumul_to_zero=*/false, kTime); + const RoutingDimension& time_dimension = routing.GetDimensionOrDie(kTime); + + // Adding disjoint time windows. + Solver* solver = routing.solver(); + std::mt19937 randomizer( + GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed))); + for (int order = 1; order < manager.num_nodes(); ++order) { + std::vector forbid_points(2 * absl::GetFlag(FLAGS_vrp_windows), 0); + for (int i = 0; i < forbid_points.size(); ++i) { + forbid_points[i] = absl::Uniform(randomizer, 0, kHorizon); + } + std::sort(forbid_points.begin(), forbid_points.end()); + std::vector forbid_starts(1, 0); + std::vector forbid_ends; + for (int i = 0; i < forbid_points.size(); i += 2) { + forbid_ends.push_back(forbid_points[i]); + forbid_starts.push_back(forbid_points[i + 1]); + } + forbid_ends.push_back(kHorizon); + solver->AddConstraint(solver->MakeNotMemberCt( + time_dimension.CumulVar(order), forbid_starts, forbid_ends)); + } + + // Adding penalty costs to allow skipping orders. + const int64_t kPenalty = 10000000; + const RoutingIndexManager::NodeIndex kFirstNodeAfterDepot(1); + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < manager.num_nodes(); ++order) { + std::vector orders(1, manager.NodeToIndex(order)); + routing.AddDisjunction(orders, kPenalty); + } + + // Adding same vehicle constraint costs for consecutive nodes. + if (absl::GetFlag(FLAGS_vrp_use_same_vehicle_costs)) { + std::vector group; + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < manager.num_nodes(); ++order) { + group.push_back(manager.NodeToIndex(order)); + if (group.size() == kMaxNodesPerGroup) { + routing.AddSoftSameVehicleConstraint(group, kSameVehicleCost); + group.clear(); + } + } + if (!group.empty()) { + routing.AddSoftSameVehicleConstraint(group, kSameVehicleCost); + } + } + + // Solve, returns a solution if any (owned by RoutingModel). + RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); + CHECK(google::protobuf::TextFormat::MergeFromString( + absl::GetFlag(FLAGS_routing_search_parameters), ¶meters)); + const Assignment* solution = routing.SolveWithParameters(parameters); + if (solution != nullptr) { + DisplayPlan(manager, routing, *solution, + absl::GetFlag(FLAGS_vrp_use_same_vehicle_costs), + kMaxNodesPerGroup, kSameVehicleCost, + routing.GetDimensionOrDie(kCapacity), + routing.GetDimensionOrDie(kTime)); + } else { + LOG(INFO) << "No solution found."; + } + return EXIT_SUCCESS; +} diff --git a/ortools/routing/samples/cvrp_disjoint_tw_test.sh b/ortools/routing/samples/cvrp_disjoint_tw_test.sh new file mode 100755 index 0000000000..3fe364646e --- /dev/null +++ b/ortools/routing/samples/cvrp_disjoint_tw_test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +source gbash.sh || exit +source module gbash_unit.sh + +function test::operations_research_examples::cvrptw() { + declare -r DIR="${TEST_SRCDIR}/ortools/routing/samples" + EXPECT_SUCCEED '${DIR}/cvrp_disjoint_tw --vrp_use_deterministic_random_seed' +} + +gbash::unit::main "$@" diff --git a/ortools/routing/samples/cvrptw.cc b/ortools/routing/samples/cvrptw.cc new file mode 100644 index 0000000000..fca4ba8ba9 --- /dev/null +++ b/ortools/routing/samples/cvrptw.cc @@ -0,0 +1,182 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// Capacitated Vehicle Routing Problem with Time Windows (and optional orders). +// A description of the problem can be found here: +// http://en.wikipedia.org/wiki/Vehicle_routing_problem. +// The variant which is tackled by this model includes a capacity dimension, +// time windows and optional orders, with a penalty cost if orders are not +// performed. For the sake of simplicity, orders are randomly located and +// distances are computed using the Manhattan distance. Distances are assumed +// to be in meters and times in seconds. + +#include +#include +#include + +#include "absl/random/random.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/cvrptw_lib.h" + +using operations_research::Assignment; +using operations_research::DefaultRoutingSearchParameters; +using operations_research::GetSeed; +using operations_research::LocationContainer; +using operations_research::RandomDemand; +using operations_research::RoutingDimension; +using operations_research::RoutingIndexManager; +using operations_research::RoutingModel; +using operations_research::RoutingNodeIndex; +using operations_research::RoutingSearchParameters; +using operations_research::ServiceTimePlusTransition; + +ABSL_FLAG(int, vrp_orders, 100, "Number of nodes in the problem"); +ABSL_FLAG(int, vrp_vehicles, 20, "Number of vehicles in the problem"); +ABSL_FLAG(bool, vrp_use_deterministic_random_seed, false, + "Use deterministic random seeds"); +ABSL_FLAG(bool, vrp_use_same_vehicle_costs, false, + "Use same vehicle costs in the routing model"); +ABSL_FLAG(std::string, routing_search_parameters, "", + "Text proto RoutingSearchParameters (possibly partial) that will " + "override the DefaultRoutingSearchParameters()"); + +const char* kTime = "Time"; +const char* kCapacity = "Capacity"; +const int64_t kMaxNodesPerGroup = 10; +const int64_t kSameVehicleCost = 1000; + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) + << "Specify a non-null vehicle fleet size."; + // VRP of size absl::GetFlag(FLAGS_vrp_size). + // Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and + // ends of the routes are at node 0. + const RoutingIndexManager::NodeIndex kDepot(0); + RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1, + absl::GetFlag(FLAGS_vrp_vehicles), kDepot); + RoutingModel routing(manager); + + // Setting up locations. + const int64_t kXMax = 100000; + const int64_t kYMax = 100000; + const int64_t kSpeed = 10; + LocationContainer locations( + kSpeed, absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + for (int location = 0; location <= absl::GetFlag(FLAGS_vrp_orders); + ++location) { + locations.AddRandomLocation(kXMax, kYMax); + } + + // Setting the cost function. + const int vehicle_cost = routing.RegisterTransitCallback( + [&locations, &manager](int64_t i, int64_t j) { + return locations.ManhattanDistance(manager.IndexToNode(i), + manager.IndexToNode(j)); + }); + routing.SetArcCostEvaluatorOfAllVehicles(vehicle_cost); + + // Adding capacity dimension constraints. + const int64_t kVehicleCapacity = 40; + const int64_t kNullCapacitySlack = 0; + RandomDemand demand(manager.num_nodes(), kDepot, + absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + demand.Initialize(); + routing.AddDimension(routing.RegisterTransitCallback( + [&demand, &manager](int64_t i, int64_t j) { + return demand.Demand(manager.IndexToNode(i), + manager.IndexToNode(j)); + }), + kNullCapacitySlack, kVehicleCapacity, + /*fix_start_cumul_to_zero=*/true, kCapacity); + + // Adding time dimension constraints. + const int64_t kTimePerDemandUnit = 300; + const int64_t kHorizon = 24 * 3600; + ServiceTimePlusTransition time( + kTimePerDemandUnit, + [&demand](RoutingNodeIndex i, RoutingNodeIndex j) { + return demand.Demand(i, j); + }, + [&locations](RoutingNodeIndex i, RoutingNodeIndex j) { + return locations.ManhattanTime(i, j); + }); + routing.AddDimension( + routing.RegisterTransitCallback([&time, &manager](int64_t i, int64_t j) { + return time.Compute(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kHorizon, kHorizon, /*fix_start_cumul_to_zero=*/true, kTime); + const RoutingDimension& time_dimension = routing.GetDimensionOrDie(kTime); + + // Adding time windows. + std::mt19937 randomizer( + GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed))); + const int64_t kTWDuration = 5 * 3600; + for (int order = 1; order < manager.num_nodes(); ++order) { + const int64_t start = + absl::Uniform(randomizer, 0, kHorizon - kTWDuration); + time_dimension.CumulVar(order)->SetRange(start, start + kTWDuration); + } + + // Adding penalty costs to allow skipping orders. + const int64_t kPenalty = 10000000; + const RoutingIndexManager::NodeIndex kFirstNodeAfterDepot(1); + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < manager.num_nodes(); ++order) { + std::vector orders(1, manager.NodeToIndex(order)); + routing.AddDisjunction(orders, kPenalty); + } + + // Adding same vehicle constraint costs for consecutive nodes. + if (absl::GetFlag(FLAGS_vrp_use_same_vehicle_costs)) { + std::vector group; + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < manager.num_nodes(); ++order) { + group.push_back(manager.NodeToIndex(order)); + if (group.size() == kMaxNodesPerGroup) { + routing.AddSoftSameVehicleConstraint(group, kSameVehicleCost); + group.clear(); + } + } + if (!group.empty()) { + routing.AddSoftSameVehicleConstraint(group, kSameVehicleCost); + } + } + + // Solve, returns a solution if any (owned by RoutingModel). + RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); + CHECK(google::protobuf::TextFormat::MergeFromString( + absl::GetFlag(FLAGS_routing_search_parameters), ¶meters)); + const Assignment* solution = routing.SolveWithParameters(parameters); + if (solution != nullptr) { + DisplayPlan(manager, routing, *solution, + absl::GetFlag(FLAGS_vrp_use_same_vehicle_costs), + kMaxNodesPerGroup, kSameVehicleCost, + routing.GetDimensionOrDie(kCapacity), + routing.GetDimensionOrDie(kTime)); + } else { + LOG(INFO) << "No solution found."; + } + return EXIT_SUCCESS; +} diff --git a/ortools/routing/samples/cvrptw_soft_capacity.cc b/ortools/routing/samples/cvrptw_soft_capacity.cc new file mode 100644 index 0000000000..df1e125985 --- /dev/null +++ b/ortools/routing/samples/cvrptw_soft_capacity.cc @@ -0,0 +1,206 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Soft-Capacitated Vehicle Routing Problem. +// A description of the problem can be found here: +// http://en.wikipedia.org/wiki/Vehicle_routing_problem. +// The variant which is tackled by this model includes a capacity dimension, +// implemented as a soft constraint: using more than the available capacity is +// penalized (i.e. "costs" more) but not forbidden. For the sake of simplicity, +// orders are randomly located and distances are computed using the Manhattan +// distance. Distances are assumed to be in meters and times in seconds. + +#include +#include +#include + +#include "absl/random/random.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/cvrptw_lib.h" + +using operations_research::Assignment; +using operations_research::DefaultRoutingSearchParameters; +using operations_research::GetSeed; +using operations_research::LocationContainer; +using operations_research::RandomDemand; +using operations_research::RoutingDimension; +using operations_research::RoutingIndexManager; +using operations_research::RoutingModel; +using operations_research::RoutingNodeIndex; +using operations_research::RoutingSearchParameters; +using operations_research::ServiceTimePlusTransition; + +ABSL_FLAG(int, vrp_orders, 100, "Number of nodes in the problem."); +ABSL_FLAG(int, vrp_vehicles, 20, "Number of vehicles in the problem."); +ABSL_FLAG(int, vrp_vehicle_hard_capacity, 80, + "Hard capacity for a vehicle; set to 0 to disable the hard capacity " + "constraint"); +ABSL_FLAG(int, vrp_vehicle_soft_capacity, 40, + "Soft capacity for a vehicle; set to 0 to disable the soft capacity " + "constraint"); +ABSL_FLAG(int, vrp_vehicle_soft_capacity_cost, 5000, + "Cost of using a vehicle beyond its soft capacity (per unit " + "of storage over the soft capacity)"); +ABSL_FLAG(bool, vrp_use_deterministic_random_seed, false, + "Use deterministic random seeds."); +ABSL_FLAG(bool, vrp_use_same_vehicle_costs, false, + "Use same vehicle costs in the routing model"); +ABSL_FLAG(std::string, routing_search_parameters, "", + "Text proto RoutingSearchParameters (possibly partial) that will " + "override the DefaultRoutingSearchParameters()"); + +const char* kTime = "Time"; +const char* kCapacity = "Capacity"; +const int64_t kMaxNodesPerGroup = 10; +const int64_t kSameVehicleCost = 1000; + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) + << "Specify a non-null vehicle fleet size."; + if (absl::GetFlag(FLAGS_vrp_vehicle_hard_capacity) > 0 && + absl::GetFlag(FLAGS_vrp_vehicle_soft_capacity) > 0) { + CHECK_LT(absl::GetFlag(FLAGS_vrp_vehicle_soft_capacity), + absl::GetFlag(FLAGS_vrp_vehicle_hard_capacity)) + << "The hard capacity must be higher than the soft capacity."; + } + + // VRP of size absl::GetFlag(FLAGS_vrp_size). + // Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and + // ends of the routes are at node 0. + const RoutingIndexManager::NodeIndex kDepot(0); + RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1, + absl::GetFlag(FLAGS_vrp_vehicles), kDepot); + RoutingModel routing(manager); + + // Setting up locations. + const int64_t kXMax = 100'000; + const int64_t kYMax = 100'000; + const int64_t kSpeed = 10; + LocationContainer locations( + kSpeed, absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + for (int location = 0; location <= absl::GetFlag(FLAGS_vrp_orders); + ++location) { + locations.AddRandomLocation(kXMax, kYMax); + } + + // Setting the cost function. + const int vehicle_cost = routing.RegisterTransitCallback( + [&locations, &manager](int64_t i, int64_t j) { + return locations.ManhattanDistance(manager.IndexToNode(i), + manager.IndexToNode(j)); + }); + routing.SetArcCostEvaluatorOfAllVehicles(vehicle_cost); + + // Adding capacity dimension constraints with slacks. + const int64_t kNullCapacitySlack = 0; + RandomDemand demand(manager.num_nodes(), kDepot, + absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + demand.Initialize(); + routing.AddDimension( + routing.RegisterTransitCallback([&demand, &manager](int64_t i, + int64_t j) { + return demand.Demand(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kNullCapacitySlack, absl::GetFlag(FLAGS_vrp_vehicle_hard_capacity), + /*fix_start_cumul_to_zero=*/true, kCapacity); + RoutingDimension* capacity_dimension = routing.GetMutableDimension(kCapacity); + + // Penalise the capacity slacks to implement the soft constraint (a hard + // constraint has a zero slack). + const int num_vehicles = absl::GetFlag(FLAGS_vrp_vehicles); + for (int vehicle = 0; vehicle < num_vehicles; ++vehicle) { + capacity_dimension->SetCumulVarSoftUpperBound( + routing.End(vehicle), absl::GetFlag(FLAGS_vrp_vehicle_soft_capacity), + absl::GetFlag(FLAGS_vrp_vehicle_soft_capacity_cost)); + } + + // Adding time dimension constraints. + const int64_t kTimePerDemandUnit = 300; + const int64_t kHorizon = 24 * 3600; + ServiceTimePlusTransition time( + kTimePerDemandUnit, + [&demand](RoutingNodeIndex i, RoutingNodeIndex j) { + return demand.Demand(i, j); + }, + [&locations](RoutingNodeIndex i, RoutingNodeIndex j) { + return locations.ManhattanTime(i, j); + }); + routing.AddDimension( + routing.RegisterTransitCallback([&time, &manager](int64_t i, int64_t j) { + return time.Compute(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kHorizon, kHorizon, /*fix_start_cumul_to_zero=*/true, kTime); + const RoutingDimension& time_dimension = routing.GetDimensionOrDie(kTime); + + // Adding time windows. + std::mt19937 randomizer( + GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed))); + const int64_t kTWDuration = 5 * 3600; + for (int order = 1; order < manager.num_nodes(); ++order) { + const int64_t start = + absl::Uniform(randomizer, 0, kHorizon - kTWDuration); + time_dimension.CumulVar(order)->SetRange(start, start + kTWDuration); + } + + // Adding penalty costs to allow skipping orders. + const int64_t kPenalty = 10'000'000; + const RoutingIndexManager::NodeIndex kFirstNodeAfterDepot(1); + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < manager.num_nodes(); ++order) { + std::vector orders(1, manager.NodeToIndex(order)); + routing.AddDisjunction(orders, kPenalty); + } + + // Adding same vehicle constraint costs for consecutive nodes. + if (absl::GetFlag(FLAGS_vrp_use_same_vehicle_costs)) { + std::vector group; + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < manager.num_nodes(); ++order) { + group.push_back(manager.NodeToIndex(order)); + if (group.size() == kMaxNodesPerGroup) { + routing.AddSoftSameVehicleConstraint(group, kSameVehicleCost); + group.clear(); + } + } + if (!group.empty()) { + routing.AddSoftSameVehicleConstraint(group, kSameVehicleCost); + } + } + + // Solve, returns a solution if any (owned by RoutingModel). + RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); + CHECK(google::protobuf::TextFormat::MergeFromString( + absl::GetFlag(FLAGS_routing_search_parameters), ¶meters)); + const Assignment* solution = routing.SolveWithParameters(parameters); + if (solution != nullptr) { + DisplayPlan(manager, routing, *solution, + absl::GetFlag(FLAGS_vrp_use_same_vehicle_costs), + kMaxNodesPerGroup, kSameVehicleCost, + routing.GetDimensionOrDie(kCapacity), + routing.GetDimensionOrDie(kTime)); + } else { + LOG(INFO) << "No solution found."; + } + return EXIT_SUCCESS; +} diff --git a/ortools/routing/samples/cvrptw_test.sh b/ortools/routing/samples/cvrptw_test.sh new file mode 100755 index 0000000000..0124df1ce7 --- /dev/null +++ b/ortools/routing/samples/cvrptw_test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +source gbash.sh || exit +source module gbash_unit.sh + +function test::operations_research_examples::cvrptw() { + declare -r DIR="${TEST_SRCDIR}/ortools/routing/samples" + EXPECT_SUCCEED '${DIR}/cvrptw --vrp_use_deterministic_random_seed' +} + +gbash::unit::main "$@" diff --git a/ortools/routing/samples/cvrptw_with_breaks.cc b/ortools/routing/samples/cvrptw_with_breaks.cc new file mode 100644 index 0000000000..bf3477505d --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_breaks.cc @@ -0,0 +1,237 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// Capacitated Vehicle Routing Problem with Time Windows and Breaks. +// A description of the Capacitated Vehicle Routing Problem with Time Windows +// can be found here: +// http://en.wikipedia.org/wiki/Vehicle_routing_problem. +// The variant which is tackled by this model includes a capacity dimension, +// time windows and optional orders, with a penalty cost if orders are not +// performed. For the sake of simplicty, orders are randomly located and +// distances are computed using the Manhattan distance. Distances are assumed +// to be in meters and times in seconds. +// This variant also includes vehicle breaks which must happen during the day +// with two alternate breaks schemes: either a long break in the middle of the +// day or two smaller ones which can be taken during a longer period of the day. + +#include +#include +#include +#include + +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_enums.pb.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/cvrptw_lib.h" + +using operations_research::Assignment; +using operations_research::DefaultRoutingSearchParameters; +using operations_research::FirstSolutionStrategy; +using operations_research::GetSeed; +using operations_research::IntervalVar; +using operations_research::LocationContainer; +using operations_research::RandomDemand; +using operations_research::RoutingDimension; +using operations_research::RoutingIndexManager; +using operations_research::RoutingModel; +using operations_research::RoutingNodeIndex; +using operations_research::RoutingSearchParameters; +using operations_research::ServiceTimePlusTransition; +using operations_research::Solver; + +ABSL_FLAG(int, vrp_orders, 100, "Nodes in the problem."); +ABSL_FLAG(int, vrp_vehicles, 20, + "Size of Traveling Salesman Problem instance."); +ABSL_FLAG(bool, vrp_use_deterministic_random_seed, false, + "Use deterministic random seeds."); +ABSL_FLAG(std::string, routing_search_parameters, "", + "Text proto RoutingSearchParameters (possibly partial) that will " + "override the DefaultRoutingSearchParameters()"); + +const char* kTime = "Time"; +const char* kCapacity = "Capacity"; + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) + << "Specify a non-null vehicle fleet size."; + // VRP of size absl::GetFlag(FLAGS_vrp_size). + // Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and + // ends of the routes are at node 0. + const RoutingIndexManager::NodeIndex kDepot(0); + RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1, + absl::GetFlag(FLAGS_vrp_vehicles), kDepot); + RoutingModel routing(manager); + RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); + CHECK(google::protobuf::TextFormat::MergeFromString( + absl::GetFlag(FLAGS_routing_search_parameters), ¶meters)); + parameters.set_first_solution_strategy( + FirstSolutionStrategy::PARALLEL_CHEAPEST_INSERTION); + + // Setting up locations. + const int64_t kXMax = 100000; + const int64_t kYMax = 100000; + const int64_t kSpeed = 10; + LocationContainer locations( + kSpeed, absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + for (int location = 0; location <= absl::GetFlag(FLAGS_vrp_orders); + ++location) { + locations.AddRandomLocation(kXMax, kYMax); + } + + // Setting the cost function. + const int vehicle_cost = routing.RegisterTransitCallback( + [&locations, &manager](int64_t i, int64_t j) { + return locations.ManhattanDistance(manager.IndexToNode(i), + manager.IndexToNode(j)); + }); + routing.SetArcCostEvaluatorOfAllVehicles(vehicle_cost); + + // Adding capacity dimension constraints. + const int64_t kVehicleCapacity = 40; + const int64_t kNullCapacitySlack = 0; + RandomDemand demand(manager.num_nodes(), kDepot, + absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + demand.Initialize(); + routing.AddDimension(routing.RegisterTransitCallback( + [&demand, &manager](int64_t i, int64_t j) { + return demand.Demand(manager.IndexToNode(i), + manager.IndexToNode(j)); + }), + kNullCapacitySlack, kVehicleCapacity, + /*fix_start_cumul_to_zero=*/true, kCapacity); + + // Adding time dimension constraints. + const int64_t kTimePerDemandUnit = 300; + const int64_t kHorizon = 24 * 3600; + ServiceTimePlusTransition time( + kTimePerDemandUnit, + [&demand](RoutingNodeIndex i, RoutingNodeIndex j) { + return demand.Demand(i, j); + }, + [&locations](RoutingNodeIndex i, RoutingNodeIndex j) { + return locations.ManhattanTime(i, j); + }); + routing.AddDimension( + routing.RegisterTransitCallback([&time, &manager](int64_t i, int64_t j) { + return time.Compute(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kHorizon, kHorizon, /*fix_start_cumul_to_zero=*/false, kTime); + RoutingDimension* const time_dimension = routing.GetMutableDimension(kTime); + + // Adding time windows. + std::mt19937 randomizer( + GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed))); + const int64_t kTWDuration = 5 * 3600; + for (int order = 1; order < manager.num_nodes(); ++order) { + const int64_t start = + absl::Uniform(randomizer, 0, kHorizon - kTWDuration); + time_dimension->CumulVar(order)->SetRange(start, start + kTWDuration); + routing.AddToAssignment(time_dimension->SlackVar(order)); + } + + // Minimize time variables. + for (int i = 0; i < routing.Size(); ++i) { + routing.AddVariableMinimizedByFinalizer(time_dimension->CumulVar(i)); + } + for (int j = 0; j < absl::GetFlag(FLAGS_vrp_vehicles); ++j) { + routing.AddVariableMinimizedByFinalizer( + time_dimension->CumulVar(routing.Start(j))); + routing.AddVariableMinimizedByFinalizer( + time_dimension->CumulVar(routing.End(j))); + } + + // Adding vehicle breaks: + // - 40min breaks between 11:00am and 1:00pm + // or + // - 2 x 30min breaks between 10:00am and 3:00pm, at least 1h apart + // First, fill service time vector. + std::vector service_times(routing.Size()); + for (int node = 0; node < routing.Size(); node++) { + if (node >= routing.nodes()) { + service_times[node] = 0; + } else { + const RoutingIndexManager::NodeIndex index(node); + service_times[node] = kTimePerDemandUnit * demand.Demand(index, index); + } + } + const std::vector> break_data = { + {/*start_min*/ 11, /*start_max*/ 13, /*duration*/ 2400}, + {/*start_min*/ 10, /*start_max*/ 15, /*duration*/ 1800}, + {/*start_min*/ 10, /*start_max*/ 15, /*duration*/ 1800}}; + Solver* const solver = routing.solver(); + for (int vehicle = 0; vehicle < absl::GetFlag(FLAGS_vrp_vehicles); + ++vehicle) { + std::vector breaks; + for (int i = 0; i < break_data.size(); ++i) { + IntervalVar* const break_interval = solver->MakeFixedDurationIntervalVar( + break_data[i][0] * 3600, break_data[i][1] * 3600, break_data[i][2], + true, absl::StrCat("Break ", i, " on vehicle ", vehicle)); + breaks.push_back(break_interval); + } + // break1 performed iff break2 performed + solver->AddConstraint(solver->MakeEquality(breaks[1]->PerformedExpr(), + breaks[2]->PerformedExpr())); + // break2 start 1h after break1. + solver->AddConstraint(solver->MakeIntervalVarRelationWithDelay( + breaks[2], Solver::STARTS_AFTER_END, breaks[1], 3600)); + // break0 performed iff break2 unperformed + solver->AddConstraint(solver->MakeNonEquality(breaks[0]->PerformedExpr(), + breaks[2]->PerformedExpr())); + + time_dimension->SetBreakIntervalsOfVehicle(std::move(breaks), vehicle, + service_times); + } + + // Adding penalty costs to allow skipping orders. + const int64_t kPenalty = 10000000; + const RoutingIndexManager::NodeIndex kFirstNodeAfterDepot(1); + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < routing.nodes(); ++order) { + std::vector orders(1, manager.NodeToIndex(order)); + routing.AddDisjunction(orders, kPenalty); + } + + // Solve, returns a solution if any (owned by RoutingModel). + const Assignment* solution = routing.SolveWithParameters(parameters); + if (solution != nullptr) { + LOG(INFO) << "Breaks: "; + for (const auto& break_interval : + solution->IntervalVarContainer().elements()) { + if (break_interval.PerformedValue() == 1) { + LOG(INFO) << break_interval.Var()->name() << " " + << break_interval.DebugString(); + } else { + LOG(INFO) << break_interval.Var()->name() << " unperformed"; + } + } + DisplayPlan(manager, routing, *solution, false, 0, 0, + routing.GetDimensionOrDie(kCapacity), + routing.GetDimensionOrDie(kTime)); + } else { + LOG(INFO) << "No solution found."; + } + return EXIT_SUCCESS; +} diff --git a/ortools/routing/samples/cvrptw_with_precedences.cc b/ortools/routing/samples/cvrptw_with_precedences.cc new file mode 100644 index 0000000000..d5c8b8e272 --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_precedences.cc @@ -0,0 +1,212 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// Capacitated Vehicle Routing Problem with Time Windows (and optional orders). +// A description of the problem can be found here: +// http://en.wikipedia.org/wiki/Vehicle_routing_problem. +// The variant which is tackled by this model includes a capacity dimension, +// time windows and optional orders, with a penalty cost if orders are not +// performed. For the sake of simplicty, orders are randomly located and +// distances are computed using the Manhattan distance. Distances are assumed +// to be in meters and times in seconds. + +#include +#include +#include + +#include "absl/random/random.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/graph/graph_builder.h" +#include "ortools/routing/cvrptw_lib.h" +#include "ortools/util/random_engine.h" + +using operations_research::Assignment; +using operations_research::DefaultRoutingSearchParameters; +using operations_research::GetSeed; +using operations_research::LocationContainer; +using operations_research::RandomDemand; +using operations_research::RoutingDimension; +using operations_research::RoutingIndexManager; +using operations_research::RoutingModel; +using operations_research::RoutingNodeIndex; +using operations_research::RoutingSearchParameters; +using operations_research::ServiceTimePlusTransition; + +ABSL_FLAG(int, vrp_orders, 100, "Nodes in the problem."); +ABSL_FLAG(int, vrp_vehicles, 20, + "Size of Traveling Salesman Problem instance."); +ABSL_FLAG(bool, vrp_use_deterministic_random_seed, false, + "Use deterministic random seeds."); +ABSL_FLAG(bool, vrp_use_same_vehicle_costs, false, + "Use same vehicle costs in the routing model"); +ABSL_FLAG(std::string, routing_search_parameters, "", + "Text proto RoutingSearchParameters (possibly partial) that will " + "override the DefaultRoutingSearchParameters()"); +ABSL_FLAG(int, vrp_precedences, 5, + "Number of precedence indices. Precedences will be chosen " + "randomly with the constraint that they don't form cycles."); +ABSL_FLAG(int64_t, vrp_precedence_offset, 100, + "The offset that applies to the precedences. For each pair linked " + "by a precedence constraint, pair.second can only start after the " + "start of pair.first + offset."); + +const char* kTime = "Time"; +const char* kCapacity = "Capacity"; +const int64_t kMaxNodesPerGroup = 10; +const int64_t kSameVehicleCost = 1000; + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) + << "Specify a non-null vehicle fleet size."; + // VRP of size absl::GetFlag(FLAGS_vrp_size). + // Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and + // ends of the routes are at node 0. + const RoutingIndexManager::NodeIndex kDepot(0); + RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1, + absl::GetFlag(FLAGS_vrp_vehicles), kDepot); + RoutingModel routing(manager); + + // Setting up locations. + const int64_t kXMax = 100000; + const int64_t kYMax = 100000; + const int64_t kSpeed = 10; + LocationContainer locations( + kSpeed, absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + for (int location = 0; location <= absl::GetFlag(FLAGS_vrp_orders); + ++location) { + locations.AddRandomLocation(kXMax, kYMax); + } + + // Setting the cost function. + const int vehicle_cost = routing.RegisterTransitCallback( + [&locations, &manager](int64_t i, int64_t j) { + return locations.ManhattanDistance(manager.IndexToNode(i), + manager.IndexToNode(j)); + }); + routing.SetArcCostEvaluatorOfAllVehicles(vehicle_cost); + + // Adding capacity dimension constraints. + const int64_t kVehicleCapacity = 40; + const int64_t kNullCapacitySlack = 0; + RandomDemand demand(manager.num_nodes(), kDepot, + absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + demand.Initialize(); + routing.AddDimension(routing.RegisterTransitCallback( + [&demand, &manager](int64_t i, int64_t j) { + return demand.Demand(manager.IndexToNode(i), + manager.IndexToNode(j)); + }), + kNullCapacitySlack, kVehicleCapacity, + /*fix_start_cumul_to_zero=*/true, kCapacity); + + // Adding time dimension constraints. + const int64_t kTimePerDemandUnit = 300; + const int64_t kHorizon = 24 * 3600; + ServiceTimePlusTransition time( + kTimePerDemandUnit, + [&demand](RoutingNodeIndex i, RoutingNodeIndex j) { + return demand.Demand(i, j); + }, + [&locations](RoutingNodeIndex i, RoutingNodeIndex j) { + return locations.ManhattanTime(i, j); + }); + routing.AddDimension( + routing.RegisterTransitCallback([&time, &manager](int64_t i, int64_t j) { + return time.Compute(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kHorizon, kHorizon, /*fix_start_cumul_to_zero=*/true, kTime); + RoutingDimension* time_dimension = routing.GetMutableDimension(kTime); + + // Adding time windows. + random_engine_t randomizer( + GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed))); + const int64_t kTWDuration = 5 * 3600; + for (int order = 1; order < manager.num_nodes(); ++order) { + const int64_t start = + absl::Uniform(randomizer, 0, kHorizon - kTWDuration); + time_dimension->CumulVar(order)->SetRange(start, start + kTWDuration); + } + + // Adding penalty costs to allow skipping orders. + const int64_t kPenalty = 10000000; + const RoutingIndexManager::NodeIndex kFirstNodeAfterDepot(1); + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < manager.num_nodes(); ++order) { + std::vector orders(1, manager.NodeToIndex(order)); + routing.AddDisjunction(orders, kPenalty); + } + + // Adding same vehicle constraint costs for consecutive nodes. + if (absl::GetFlag(FLAGS_vrp_use_same_vehicle_costs)) { + std::vector group; + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < manager.num_nodes(); ++order) { + group.push_back(manager.NodeToIndex(order)); + if (group.size() == kMaxNodesPerGroup) { + routing.AddSoftSameVehicleConstraint(group, kSameVehicleCost); + group.clear(); + } + } + if (!group.empty()) { + routing.AddSoftSameVehicleConstraint(group, kSameVehicleCost); + } + } + + // If the flag is > 0, we create a DAG with random edges representing + // precedences. If it is not possible to meet the precedence constraints, for + // instance if the generated time window are incompatible, we expect one of + // the underlying orders to be skipped. + if (absl::GetFlag(FLAGS_vrp_precedences) > 0) { + // Randomly select edges in a graph that will act as precedences. + std::vector> precedences; + GraphBuilder::RandomEdges( + GraphBuilder::DISALLOW_ALL_CYCLES, absl::GetFlag(FLAGS_vrp_orders), + absl::GetFlag(FLAGS_vrp_precedences), &randomizer, &precedences); + + LOG(INFO) << "Adding precedences: "; + for (const std::pair& precedence : precedences) { + LOG(INFO) << precedence.first << " -> " << precedence.second; + time_dimension->AddNodePrecedence( + {precedence.first, precedence.second, + absl::GetFlag(FLAGS_vrp_precedence_offset)}); + } + } + // Solve, returns a solution if any (owned by RoutingModel). + RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); + CHECK(google::protobuf::TextFormat::MergeFromString( + absl::GetFlag(FLAGS_routing_search_parameters), ¶meters)); + const Assignment* solution = routing.SolveWithParameters(parameters); + if (solution != nullptr) { + DisplayPlan(manager, routing, *solution, + absl::GetFlag(FLAGS_vrp_use_same_vehicle_costs), + kMaxNodesPerGroup, kSameVehicleCost, + routing.GetDimensionOrDie(kCapacity), + routing.GetDimensionOrDie(kTime)); + } else { + LOG(INFO) << "No solution found."; + } + + return 0; +} diff --git a/ortools/routing/samples/cvrptw_with_refueling.cc b/ortools/routing/samples/cvrptw_with_refueling.cc new file mode 100644 index 0000000000..05a7a62c62 --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_refueling.cc @@ -0,0 +1,193 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Capacitated Vehicle Routing Problem with Time Windows and refueling +// constraints. +// This is an extension to the model in cvrptw.cc so refer to that file for +// more information on the common part of the model. The model implemented here +// takes into account refueling constraints using a specific dimension: vehicles +// must visit certain nodes (refueling nodes) before the quantity of fuel +// reaches zero. Fuel consumption is proportional to the distance traveled. + +#include +#include +#include + +#include "absl/random/random.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/cvrptw_lib.h" + +using operations_research::Assignment; +using operations_research::DefaultRoutingSearchParameters; +using operations_research::GetSeed; +using operations_research::LocationContainer; +using operations_research::RandomDemand; +using operations_research::RoutingDimension; +using operations_research::RoutingIndexManager; +using operations_research::RoutingModel; +using operations_research::RoutingNodeIndex; +using operations_research::RoutingSearchParameters; +using operations_research::ServiceTimePlusTransition; + +ABSL_FLAG(int, vrp_orders, 100, "Nodes in the problem."); +ABSL_FLAG(int, vrp_vehicles, 20, + "Size of Traveling Salesman Problem instance."); +ABSL_FLAG(bool, vrp_use_deterministic_random_seed, false, + "Use deterministic random seeds."); +ABSL_FLAG(std::string, routing_search_parameters, "", + "Text proto RoutingSearchParameters (possibly partial) that will " + "override the DefaultRoutingSearchParameters()"); + +const char* kTime = "Time"; +const char* kCapacity = "Capacity"; +const char* kFuel = "Fuel"; + +// Returns true if node is a refueling node (based on node / refuel node ratio). +bool IsRefuelNode(int64_t node) { + const int64_t kRefuelNodeRatio = 10; + return (node % kRefuelNodeRatio == 0); +} + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) + << "Specify a non-null vehicle fleet size."; + // VRP of size absl::GetFlag(FLAGS_vrp_size). + // Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and + // ends of the routes are at node 0. + const RoutingIndexManager::NodeIndex kDepot(0); + RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1, + absl::GetFlag(FLAGS_vrp_vehicles), kDepot); + RoutingModel routing(manager); + + // Setting up locations. + const int64_t kXMax = 100000; + const int64_t kYMax = 100000; + const int64_t kSpeed = 10; + LocationContainer locations( + kSpeed, absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + for (int location = 0; location <= absl::GetFlag(FLAGS_vrp_orders); + ++location) { + locations.AddRandomLocation(kXMax, kYMax); + } + + // Setting the cost function. + const int vehicle_cost = routing.RegisterTransitCallback( + [&locations, &manager](int64_t i, int64_t j) { + return locations.ManhattanDistance(manager.IndexToNode(i), + manager.IndexToNode(j)); + }); + routing.SetArcCostEvaluatorOfAllVehicles(vehicle_cost); + + // Adding capacity dimension constraints. + const int64_t kVehicleCapacity = 40; + const int64_t kNullCapacitySlack = 0; + RandomDemand demand(manager.num_nodes(), kDepot, + absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + demand.Initialize(); + routing.AddDimension(routing.RegisterTransitCallback( + [&demand, &manager](int64_t i, int64_t j) { + return demand.Demand(manager.IndexToNode(i), + manager.IndexToNode(j)); + }), + kNullCapacitySlack, kVehicleCapacity, + /*fix_start_cumul_to_zero=*/true, kCapacity); + + // Adding time dimension constraints. + const int64_t kTimePerDemandUnit = 300; + const int64_t kHorizon = 24 * 3600; + ServiceTimePlusTransition time( + kTimePerDemandUnit, + [&demand](RoutingNodeIndex i, RoutingNodeIndex j) { + return demand.Demand(i, j); + }, + [&locations](RoutingNodeIndex i, RoutingNodeIndex j) { + return locations.ManhattanTime(i, j); + }); + routing.AddDimension( + routing.RegisterTransitCallback([&time, &manager](int64_t i, int64_t j) { + return time.Compute(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kHorizon, kHorizon, /*fix_start_cumul_to_zero=*/true, kTime); + const RoutingDimension& time_dimension = routing.GetDimensionOrDie(kTime); + // Adding time windows. + // NOTE(user): This randomized test case is quite sensible to the seed: + // the generated model can be much easier or harder to solve, depending on + // the seed. It turns out that most seeds yield pretty slow/bad solver + // performance: I got good performance for about 10% of the seeds. + std::mt19937 randomizer( + 144 + GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed))); + const int64_t kTWDuration = 5 * 3600; + for (int order = 1; order < manager.num_nodes(); ++order) { + if (!IsRefuelNode(order)) { + const int64_t start = + absl::Uniform(randomizer, 0, kHorizon - kTWDuration); + time_dimension.CumulVar(order)->SetRange(start, start + kTWDuration); + } + } + + // Adding fuel dimension. This dimension consumes a quantity equal to the + // distance traveled. Only refuel nodes can make the quantity of dimension + // increase by letting slack variable replenish the fuel. + const int64_t kFuelCapacity = kXMax + kYMax; + routing.AddDimension( + routing.RegisterTransitCallback( + [&locations, &manager](int64_t i, int64_t j) { + return locations.NegManhattanDistance(manager.IndexToNode(i), + manager.IndexToNode(j)); + }), + kFuelCapacity, kFuelCapacity, /*fix_start_cumul_to_zero=*/false, kFuel); + const RoutingDimension& fuel_dimension = routing.GetDimensionOrDie(kFuel); + for (int order = 0; order < routing.Size(); ++order) { + // Only let slack free for refueling nodes. + if (!IsRefuelNode(order) || routing.IsStart(order)) { + fuel_dimension.SlackVar(order)->SetValue(0); + } + // Needed to instantiate fuel quantity at each node. + routing.AddVariableMinimizedByFinalizer(fuel_dimension.CumulVar(order)); + } + + // Adding penalty costs to allow skipping orders. + const int64_t kPenalty = 100000; + const RoutingIndexManager::NodeIndex kFirstNodeAfterDepot(1); + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < routing.nodes(); ++order) { + std::vector orders(1, manager.NodeToIndex(order)); + routing.AddDisjunction(orders, kPenalty); + } + + // Solve, returns a solution if any (owned by RoutingModel). + RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); + CHECK(google::protobuf::TextFormat::MergeFromString( + absl::GetFlag(FLAGS_routing_search_parameters), ¶meters)); + const Assignment* solution = routing.SolveWithParameters(parameters); + if (solution != nullptr) { + DisplayPlan(manager, routing, *solution, /*use_same_vehicle_costs=*/false, + /*max_nodes_per_group=*/0, /*same_vehicle_cost=*/0, + routing.GetDimensionOrDie(kCapacity), + routing.GetDimensionOrDie(kTime)); + } else { + LOG(INFO) << "No solution found."; + } + return EXIT_SUCCESS; +} diff --git a/ortools/routing/samples/cvrptw_with_refueling_test.sh b/ortools/routing/samples/cvrptw_with_refueling_test.sh new file mode 100755 index 0000000000..b7bc0634db --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_refueling_test.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +source gbash.sh || exit +source module gbash_unit.sh + +function test::operations_research_examples::cvrptw_with_refueling() { + declare -r DIR="${TEST_SRCDIR}/ortools/routing/samples" + EXPECT_SUCCEED "${DIR}/cvrptw_with_refueling\ + --vrp_use_deterministic_random_seed --cp_random_seed=144" +} + +gbash::unit::main "$@" diff --git a/ortools/routing/samples/cvrptw_with_resources.cc b/ortools/routing/samples/cvrptw_with_resources.cc new file mode 100644 index 0000000000..61fec9704a --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_resources.cc @@ -0,0 +1,188 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Capacitated Vehicle Routing Problem with Time Windows and capacitated +// resources. +// This is an extension to the model in cvrptw.cc so refer to that file for +// more information on the common part of the model. The model implemented here +// limits the number of vehicles which can simultaneously leave or enter the +// depot due to limited resources (or capacity) available. +// TODO(user): The current model consumes resources even for vehicles with +// empty routes; fix this when we have an API on the cumulative constraints +// with variable demands. + +#include +#include +#include + +#include "absl/random/random.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/cvrptw_lib.h" + +using operations_research::Assignment; +using operations_research::DefaultRoutingSearchParameters; +using operations_research::GetSeed; +using operations_research::IntervalVar; +using operations_research::IntVar; +using operations_research::LocationContainer; +using operations_research::RandomDemand; +using operations_research::RoutingDimension; +using operations_research::RoutingIndexManager; +using operations_research::RoutingModel; +using operations_research::RoutingNodeIndex; +using operations_research::RoutingSearchParameters; +using operations_research::ServiceTimePlusTransition; +using operations_research::Solver; + +ABSL_FLAG(int, vrp_orders, 100, "Nodes in the problem."); +ABSL_FLAG(int, vrp_vehicles, 20, + "Size of Traveling Salesman Problem instance."); +ABSL_FLAG(bool, vrp_use_deterministic_random_seed, false, + "Use deterministic random seeds."); +ABSL_FLAG(std::string, routing_search_parameters, "", + "Text proto RoutingSearchParameters (possibly partial) that will " + "override the DefaultRoutingSearchParameters()"); + +const char* kTime = "Time"; +const char* kCapacity = "Capacity"; + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) + << "Specify a non-null vehicle fleet size."; + // VRP of size absl::GetFlag(FLAGS_vrp_size). + // Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and + // ends of the routes are at node 0. + const RoutingIndexManager::NodeIndex kDepot(0); + RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1, + absl::GetFlag(FLAGS_vrp_vehicles), kDepot); + RoutingModel routing(manager); + + // Setting up locations. + const int64_t kXMax = 100000; + const int64_t kYMax = 100000; + const int64_t kSpeed = 10; + LocationContainer locations( + kSpeed, absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + for (int location = 0; location <= absl::GetFlag(FLAGS_vrp_orders); + ++location) { + locations.AddRandomLocation(kXMax, kYMax); + } + + // Setting the cost function. + const int vehicle_cost = routing.RegisterTransitCallback( + [&locations, &manager](int64_t i, int64_t j) { + return locations.ManhattanDistance(manager.IndexToNode(i), + manager.IndexToNode(j)); + }); + routing.SetArcCostEvaluatorOfAllVehicles(vehicle_cost); + + // Adding capacity dimension constraints. + const int64_t kVehicleCapacity = 40; + const int64_t kNullCapacitySlack = 0; + RandomDemand demand(manager.num_nodes(), kDepot, + absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + demand.Initialize(); + routing.AddDimension(routing.RegisterTransitCallback( + [&demand, &manager](int64_t i, int64_t j) { + return demand.Demand(manager.IndexToNode(i), + manager.IndexToNode(j)); + }), + kNullCapacitySlack, kVehicleCapacity, + /*fix_start_cumul_to_zero=*/true, kCapacity); + + // Adding time dimension constraints. + const int64_t kTimePerDemandUnit = 300; + const int64_t kHorizon = 24 * 3600; + ServiceTimePlusTransition time( + kTimePerDemandUnit, + [&demand](RoutingNodeIndex i, RoutingNodeIndex j) { + return demand.Demand(i, j); + }, + [&locations](RoutingNodeIndex i, RoutingNodeIndex j) { + return locations.ManhattanTime(i, j); + }); + routing.AddDimension( + routing.RegisterTransitCallback([&time, &manager](int64_t i, int64_t j) { + return time.Compute(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kHorizon, kHorizon, /*fix_start_cumul_to_zero=*/false, kTime); + const RoutingDimension& time_dimension = routing.GetDimensionOrDie(kTime); + + // Adding time windows. + std::mt19937 randomizer( + GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed))); + const int64_t kTWDuration = 5 * 3600; + for (int order = 1; order < manager.num_nodes(); ++order) { + const int64_t start = + absl::Uniform(randomizer, 0, kHorizon - kTWDuration); + time_dimension.CumulVar(order)->SetRange(start, start + kTWDuration); + } + + // Adding resource constraints at the depot (start and end location of + // routes). + std::vector start_end_times; + for (int i = 0; i < absl::GetFlag(FLAGS_vrp_vehicles); ++i) { + start_end_times.push_back(time_dimension.CumulVar(routing.End(i))); + start_end_times.push_back(time_dimension.CumulVar(routing.Start(i))); + } + // Build corresponding time intervals. + const int64_t kVehicleSetup = 180; + Solver* const solver = routing.solver(); + std::vector intervals; + solver->MakeFixedDurationIntervalVarArray(start_end_times, kVehicleSetup, + "depot_interval", &intervals); + // Constrain the number of maximum simultaneous intervals at depot. + const int64_t kDepotCapacity = 5; + std::vector depot_usage(start_end_times.size(), 1); + solver->AddConstraint( + solver->MakeCumulative(intervals, depot_usage, kDepotCapacity, "depot")); + // Instantiate route start and end times to produce feasible times. + for (int i = 0; i < start_end_times.size(); ++i) { + routing.AddVariableMinimizedByFinalizer(start_end_times[i]); + } + + // Adding penalty costs to allow skipping orders. + const int64_t kPenalty = 100000; + const RoutingIndexManager::NodeIndex kFirstNodeAfterDepot(1); + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < manager.num_nodes(); ++order) { + std::vector orders(1, manager.NodeToIndex(order)); + routing.AddDisjunction(orders, kPenalty); + } + + // Solve, returns a solution if any (owned by RoutingModel). + RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); + CHECK(google::protobuf::TextFormat::MergeFromString( + absl::GetFlag(FLAGS_routing_search_parameters), ¶meters)); + const Assignment* solution = routing.SolveWithParameters(parameters); + if (solution != nullptr) { + DisplayPlan(manager, routing, *solution, /*use_same_vehicle_costs=*/false, + /*max_nodes_per_group=*/0, /*same_vehicle_cost=*/0, + routing.GetDimensionOrDie(kCapacity), + routing.GetDimensionOrDie(kTime)); + } else { + LOG(INFO) << "No solution found."; + } + return EXIT_SUCCESS; +} diff --git a/ortools/routing/samples/cvrptw_with_resources_test.sh b/ortools/routing/samples/cvrptw_with_resources_test.sh new file mode 100755 index 0000000000..651456641f --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_resources_test.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +source gbash.sh || exit +source module gbash_unit.sh + +function test::operations_research_examples::cvrptw_with_resources() { + declare -r DIR="${TEST_SRCDIR}/ortools/routing/samples" + EXPECT_SUCCEED "${DIR}/cvrptw_with_resources\ + --vrp_use_deterministic_random_seed" +} + +gbash::unit::main "$@" diff --git a/ortools/routing/samples/cvrptw_with_stop_times_and_resources.cc b/ortools/routing/samples/cvrptw_with_stop_times_and_resources.cc new file mode 100644 index 0000000000..d6ae1ad0a4 --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_stop_times_and_resources.cc @@ -0,0 +1,224 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Capacitated Vehicle Routing Problem with Time Windows, fixed stop times and +// capacitated resources. A stop is defined as consecutive nodes at the same +// location. +// This is an extension to the model in cvrptw.cc so refer to that file for +// more information on the common part of the model. The model implemented here +// limits the number of vehicles which can simultaneously leave or enter a node +// to one. + +#include +#include +#include + +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/cvrptw_lib.h" + +using operations_research::Assignment; +using operations_research::DefaultRoutingSearchParameters; +using operations_research::GetSeed; +using operations_research::IntervalVar; +using operations_research::IntVar; +using operations_research::LocationContainer; +using operations_research::RandomDemand; +using operations_research::RoutingDimension; +using operations_research::RoutingIndexManager; +using operations_research::RoutingModel; +using operations_research::RoutingNodeIndex; +using operations_research::RoutingSearchParameters; +using operations_research::Solver; +using operations_research::StopServiceTimePlusTransition; + +ABSL_FLAG(int, vrp_stops, 25, "Stop locations in the problem."); +ABSL_FLAG(int, vrp_orders_per_stop, 5, "Nodes for each stop."); +ABSL_FLAG(int, vrp_vehicles, 20, + "Size of Traveling Salesman Problem instance."); +ABSL_FLAG(bool, vrp_use_deterministic_random_seed, false, + "Use deterministic random seeds."); +ABSL_FLAG(std::string, routing_search_parameters, "", + "Text proto RoutingSearchParameters (possibly partial) that will " + "override the DefaultRoutingSearchParameters()"); + +const char* kTime = "Time"; +const char* kCapacity = "Capacity"; + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_stops)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders_per_stop)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) + << "Specify a non-null vehicle fleet size."; + const int vrp_orders = + absl::GetFlag(FLAGS_vrp_stops) * absl::GetFlag(FLAGS_vrp_orders_per_stop); + // Nodes are indexed from 0 to vrp_orders, the starts and ends of the routes + // are at node 0. + const RoutingIndexManager::NodeIndex kDepot(0); + RoutingIndexManager manager(vrp_orders + 1, absl::GetFlag(FLAGS_vrp_vehicles), + kDepot); + RoutingModel routing(manager); + + // Setting up locations. + const int64_t kXMax = 100000; + const int64_t kYMax = 100000; + const int64_t kSpeed = 10; + LocationContainer locations( + kSpeed, absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + for (int stop = 0; stop <= absl::GetFlag(FLAGS_vrp_stops); ++stop) { + const int num_orders = + stop == 0 ? 1 : absl::GetFlag(FLAGS_vrp_orders_per_stop); + locations.AddRandomLocation(kXMax, kYMax, num_orders); + } + + // Setting the cost function. + const int vehicle_cost = routing.RegisterTransitCallback( + [&locations, &manager](int64_t i, int64_t j) { + return locations.ManhattanDistance(manager.IndexToNode(i), + manager.IndexToNode(j)); + }); + routing.SetArcCostEvaluatorOfAllVehicles(vehicle_cost); + + // Adding capacity dimension constraints. + const int64_t kVehicleCapacity = 40; + const int64_t kNullCapacitySlack = 0; + RandomDemand demand(manager.num_nodes(), kDepot, + absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + demand.Initialize(); + routing.AddDimension(routing.RegisterTransitCallback( + [&demand, &manager](int64_t i, int64_t j) { + return demand.Demand(manager.IndexToNode(i), + manager.IndexToNode(j)); + }), + kNullCapacitySlack, kVehicleCapacity, + /*fix_start_cumul_to_zero=*/true, kCapacity); + + // Adding time dimension constraints. + const int64_t kStopTime = 300; + const int64_t kHorizon = 24 * 3600; + StopServiceTimePlusTransition time( + kStopTime, locations, + [&locations](RoutingNodeIndex i, RoutingNodeIndex j) { + return locations.ManhattanTime(i, j); + }); + routing.AddDimension( + routing.RegisterTransitCallback([&time, &manager](int64_t i, int64_t j) { + return time.Compute(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kHorizon, kHorizon, /*fix_start_cumul_to_zero=*/false, kTime); + const RoutingDimension& time_dimension = routing.GetDimensionOrDie(kTime); + + // Adding time windows, for the sake of simplicty same for each stop. + std::mt19937 randomizer( + GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed))); + const int64_t kTWDuration = 5 * 3600; + for (int stop = 0; stop < absl::GetFlag(FLAGS_vrp_stops); ++stop) { + const int64_t start = + absl::Uniform(randomizer, 0, kHorizon - kTWDuration); + for (int stop_order = 0; + stop_order < absl::GetFlag(FLAGS_vrp_orders_per_stop); ++stop_order) { + const int order = + stop * absl::GetFlag(FLAGS_vrp_orders_per_stop) + stop_order + 1; + time_dimension.CumulVar(order)->SetRange(start, start + kTWDuration); + } + } + + // Adding resource constraints at order locations. + Solver* const solver = routing.solver(); + std::vector intervals; + for (int stop = 0; stop < absl::GetFlag(FLAGS_vrp_stops); ++stop) { + std::vector stop_intervals; + for (int stop_order = 0; + stop_order < absl::GetFlag(FLAGS_vrp_orders_per_stop); ++stop_order) { + const int order = + stop * absl::GetFlag(FLAGS_vrp_orders_per_stop) + stop_order + 1; + IntervalVar* const interval = solver->MakeFixedDurationIntervalVar( + 0, kHorizon, kStopTime, true, absl::StrCat("Order", order)); + intervals.push_back(interval); + stop_intervals.push_back(interval); + // Link order and interval. + IntVar* const order_start = time_dimension.CumulVar(order); + solver->AddConstraint( + solver->MakeIsEqualCt(interval->SafeStartExpr(0), order_start, + interval->PerformedExpr()->Var())); + // Make interval performed iff corresponding order has service time. + // An order has no service time iff it is at the same location as the + // next order on the route. + IntVar* const is_null_duration = + solver + ->MakeElement( + [&locations, order](int64_t index) { + return locations.SameLocationFromIndex(order, index); + }, + routing.NextVar(order)) + ->Var(); + solver->AddConstraint( + solver->MakeNonEquality(interval->PerformedExpr(), is_null_duration)); + routing.AddIntervalToAssignment(interval); + // We are minimizing route durations by minimizing route ends; so we can + // maximize order starts to pack them together. + routing.AddVariableMaximizedByFinalizer(order_start); + } + // Only one order can happen at the same time at a given location. + std::vector location_usage(stop_intervals.size(), 1); + solver->AddConstraint(solver->MakeCumulative( + stop_intervals, location_usage, 1, absl::StrCat("Client", stop))); + } + // Minimizing route duration. + for (int vehicle = 0; vehicle < manager.num_vehicles(); ++vehicle) { + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.End(vehicle))); + } + + // Adding penalty costs to allow skipping orders. + const int64_t kPenalty = 100000; + const RoutingIndexManager::NodeIndex kFirstNodeAfterDepot(1); + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < routing.nodes(); ++order) { + std::vector orders(1, manager.NodeToIndex(order)); + routing.AddDisjunction(orders, kPenalty); + } + + // Solve, returns a solution if any (owned by RoutingModel). + RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); + CHECK(google::protobuf::TextFormat::MergeFromString( + absl::GetFlag(FLAGS_routing_search_parameters), ¶meters)); + const Assignment* solution = routing.SolveWithParameters(parameters); + if (solution != nullptr) { + DisplayPlan(manager, routing, *solution, /*use_same_vehicle_costs=*/false, + /*max_nodes_per_group=*/0, /*same_vehicle_cost=*/0, + routing.GetDimensionOrDie(kCapacity), + routing.GetDimensionOrDie(kTime)); + LOG(INFO) << "Stop intervals:"; + for (IntervalVar* const interval : intervals) { + if (solution->PerformedValue(interval)) { + LOG(INFO) << interval->name() << ": " << solution->StartValue(interval); + } + } + } else { + LOG(INFO) << "No solution found."; + } + return EXIT_SUCCESS; +} diff --git a/ortools/routing/samples/cvrptw_with_stop_times_and_resources_test.sh b/ortools/routing/samples/cvrptw_with_stop_times_and_resources_test.sh new file mode 100755 index 0000000000..cf6296320d --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_stop_times_and_resources_test.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +source gbash.sh || exit +source module gbash_unit.sh + +function test::operations_research_examples::cvrptw_with_stop_times_and_resources() { + declare -r DIR="${TEST_SRCDIR}/ortools/routing/samples" + EXPECT_SUCCEED "${DIR}/cvrptw_with_stop_times_and_resources\ + --vrp_use_deterministic_random_seed" +} + +gbash::unit::main "$@" diff --git a/ortools/routing/samples/cvrptw_with_time_dependent_costs.cc b/ortools/routing/samples/cvrptw_with_time_dependent_costs.cc new file mode 100644 index 0000000000..54b1a94be2 --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_time_dependent_costs.cc @@ -0,0 +1,249 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This example is very similar to cvrptw.cc, but distances are time dependent. +// The function RandomStepFunction is used to add random noise to each transit. + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/functional/bind_front.h" +#include "absl/random/random.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/cvrptw_lib.h" +#include "ortools/util/random_engine.h" +#include "ortools/util/range_query_function.h" +#include "ortools/util/step_function.h" + +using operations_research::Assignment; +using operations_research::DefaultRoutingSearchParameters; +using operations_research::GetSeed; +using operations_research::LocationContainer; +using operations_research::RandomDemand; +using operations_research::RoutingDimension; +using operations_research::RoutingIndexManager; +using operations_research::RoutingModel; +using operations_research::RoutingNodeIndex; +using operations_research::RoutingSearchParameters; +using operations_research::ServiceTimePlusTransition; +using operations_research::StepFunction; + +ABSL_FLAG(int, vrp_orders, 25, "Nodes in the problem."); +ABSL_FLAG(int, vrp_vehicles, 10, + "Size of Traveling Salesman Problem instance."); +ABSL_FLAG(bool, vrp_use_deterministic_random_seed, false, + "Use deterministic random seeds."); +ABSL_FLAG(std::string, routing_search_parameters, "", + "Text proto RoutingSearchParameters (possibly partial) that will " + "override the DefaultRoutingSearchParameters()"); + +static const char kTime[] = "Time"; +static const char kCapacity[] = "Capacity"; +static const char kTimeDepedentCost[] = "TimeDependentCost"; + +// This class implements the Pólya urn stochastic process, for more information: +// https://en.wikipedia.org/wiki/P%C3%B3lya_urn_model +// Basically, the polya urn is a martingale that converges almost surely to a +// uniform random variable over [0, 1]. It is questionable if it's realistic to +// model traffic deviations with this process, but traffic is hard to model in +// general. +class PolyaUrn { + public: + PolyaUrn(int red_balls, int blue_balls, int seed) + : red_balls_(red_balls), + all_balls_(red_balls + blue_balls), + generator_(seed) { + CHECK_LT(0, red_balls_); + CHECK_LT(red_balls_, all_balls_); + } + // Every call to Next moves the process one step forward and returns the + // current value. + double Next() { + CHECK_LT(0, red_balls_); + CHECK_LT(red_balls_, all_balls_); + + const double return_value = static_cast(red_balls_) / all_balls_; + red_balls_ += (generator_.Uniform(all_balls_) < red_balls_); + all_balls_ += 1; + + CHECK_LT(0, return_value); + CHECK_LT(return_value, 1); + return return_value - 0.5; + } + + private: + int red_balls_; + int all_balls_; + random_engine_t generator_; +}; + +// Creates a random histogram over the interval [0, interval_end) using the urn. +StepFunction RandomStepFunction(int64_t mean, int64_t step_size, + int64_t interval_end, int seed) { + PolyaUrn random_generator(1, 1, seed); + StepFunction result; + for (int64_t step = 0; step < interval_end; step += step_size) { + result.AddStepToEnd(step, 2 * mean * random_generator.Next() - mean); + } + result.AddStepToEnd(interval_end, 0); + return result; +} + +class TrafficTransitionEvaluator { + public: + TrafficTransitionEvaluator(const LocationContainer& distance_evaluator, + int64_t max_time) + : distance_evaluator_(distance_evaluator), max_time_(max_time) {} + + RoutingModel::StateDependentTransit Run(const RoutingIndexManager& manager, + int64_t from_index, + int64_t to_index) { + const RoutingIndexManager::NodeIndex from = manager.IndexToNode(from_index); + const RoutingIndexManager::NodeIndex to = manager.IndexToNode(to_index); + static const int magic_number = 0xfe3498aa; + const int64_t seed = + (from.value() ^ magic_number) * (to.value() ^ (~magic_number)); + const int64_t distance = distance_evaluator_.ManhattanDistance(from, to); + const int64_t mean_deviation = sqrt(distance); + const StepFunction deviation = + RandomStepFunction(mean_deviation, sqrt(max_time_), max_time_, seed); + const std::function travel_time = + [distance, &deviation](int64_t time) -> int64_t { + return distance + deviation.GetValue(time); + }; + return RoutingModel::MakeStateDependentTransit(travel_time, 0, max_time_); + // Now the local variables deviation and travel_time are going to be + // destroyed, but MakeStateDependentTransit does not store either and it + // uses its own caches. + } + + private: + const LocationContainer& distance_evaluator_; + const int64_t max_time_; +}; + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) + << "Specify an instance size greater than 0."; + CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) + << "Specify a non-null vehicle fleet size."; + // VRP of size absl::GetFlag(FLAGS_vrp_size). + // Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and + // ends of the routes are at node 0. + static const RoutingIndexManager::NodeIndex kDepot(0); + static const RoutingIndexManager::NodeIndex kFirstNodeAfterDepot(1); + RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1, + absl::GetFlag(FLAGS_vrp_vehicles), kDepot); + RoutingModel routing(manager); + + // Setting up locations. + const int64_t kXMax = 1000; + const int64_t kYMax = 1000; + const int64_t kSpeed = 10; + LocationContainer locations( + kSpeed, absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + for (int location = 0; location <= absl::GetFlag(FLAGS_vrp_orders); + ++location) { + locations.AddRandomLocation(kXMax, kYMax); + } + + // Adding capacity dimension constraints. + const int64_t kVehicleCapacity = 40; + const int64_t kNullCapacitySlack = 0; + RandomDemand demand(manager.num_nodes(), kDepot, + absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)); + demand.Initialize(); + routing.AddDimension(routing.RegisterTransitCallback( + [&demand, &manager](int64_t i, int64_t j) { + return demand.Demand(manager.IndexToNode(i), + manager.IndexToNode(j)); + }), + kNullCapacitySlack, kVehicleCapacity, + /*fix_start_cumul_to_zero=*/true, kCapacity); + + // Adding time dimension constraints. + const int64_t kTimePerDemandUnit = 3; + const int64_t kHorizon = 24 * 36; + ServiceTimePlusTransition time( + kTimePerDemandUnit, + [&demand](RoutingNodeIndex i, RoutingNodeIndex j) { + return demand.Demand(i, j); + }, + [&locations](RoutingNodeIndex i, RoutingNodeIndex j) { + return locations.ManhattanTime(i, j); + }); + routing.AddDimension( + routing.RegisterTransitCallback([&time, &manager](int64_t i, int64_t j) { + return time.Compute(manager.IndexToNode(i), manager.IndexToNode(j)); + }), + kHorizon, kHorizon, /*fix_start_cumul_to_zero=*/true, kTime); + + // Setting the cost function. In fact, we create a time dependent dimension. + const int64_t max_time = manager.num_nodes() * (kXMax + kYMax) / kSpeed; + TrafficTransitionEvaluator traffic_evaluator(locations, max_time); + routing.AddDimensionDependentDimensionWithVehicleCapacity( + routing.RegisterStateDependentTransitCallback(::absl::bind_front( + &TrafficTransitionEvaluator::Run, &traffic_evaluator, manager)), + &routing.GetDimensionOrDie(kTime), kHorizon, kHorizon, + /*fix_start_cumul_to_zero=*/true, kTimeDepedentCost); + routing.GetMutableDimension(kTimeDepedentCost) + ->SetSpanCostCoefficientForAllVehicles(1); + + // Adding time windows. + std::mt19937 randomizer( + GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed))); + const RoutingDimension& time_dimension = routing.GetDimensionOrDie(kTime); + const int64_t kTWDuration = 5 * 36; + for (int order = 1; order < manager.num_nodes(); ++order) { + const int64_t start = + absl::Uniform(randomizer, 0, kHorizon - kTWDuration); + time_dimension.CumulVar(order)->SetRange(start, start + kTWDuration); + } + + // Adding penalty costs to allow skipping orders. + const int64_t kPenalty = 10000000; + for (RoutingIndexManager::NodeIndex order = kFirstNodeAfterDepot; + order < routing.nodes(); ++order) { + std::vector orders(1, manager.NodeToIndex(order)); + routing.AddDisjunction(orders, kPenalty); + } + + // Solve, returns a solution if any (owned by RoutingModel). + RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); + CHECK(google::protobuf::TextFormat::MergeFromString( + absl::GetFlag(FLAGS_routing_search_parameters), ¶meters)); + const Assignment* solution = routing.SolveWithParameters(parameters); + if (solution != nullptr) { + DisplayPlan(manager, routing, *solution, /*use_same_vehicle_costs=*/false, + /*max_nodes_per_group=*/0, /*same_vehicle_cost=*/0, + routing.GetDimensionOrDie(kCapacity), + routing.GetDimensionOrDie(kTime)); + } else { + LOG(INFO) << "No solution found."; + } + return 0; +} diff --git a/ortools/routing/samples/cvrptw_with_time_dependent_costs.sh b/ortools/routing/samples/cvrptw_with_time_dependent_costs.sh new file mode 100755 index 0000000000..c9bb1c9268 --- /dev/null +++ b/ortools/routing/samples/cvrptw_with_time_dependent_costs.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +source gbash.sh || exit +source module gbash_unit.sh + +function test::operations_research_examples::cvrptw_with_time_dependent_costs() { + declare -r DIR="${TEST_SRCDIR}/ortools/routing/samples" + EXPECT_SUCCEED '${DIR}/cvrptw_with_time_dependent_costs --vrp_use_deterministic_random_seed' +} + +gbash::unit::main "$@" diff --git a/ortools/routing/simple_graph.cc b/ortools/routing/simple_graph.cc new file mode 100644 index 0000000000..523eb23538 --- /dev/null +++ b/ortools/routing/simple_graph.cc @@ -0,0 +1,21 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/simple_graph.h" + +namespace operations_research { + +Edge::Edge(const Arc& arc) : tail_(arc.tail()), head_(arc.head()) {} +Arc::Arc(const Edge& edge) : tail_(edge.tail()), head_(edge.head()) {} + +} // namespace operations_research diff --git a/ortools/routing/simple_graph.h b/ortools/routing/simple_graph.h new file mode 100644 index 0000000000..45db752779 --- /dev/null +++ b/ortools/routing/simple_graph.h @@ -0,0 +1,155 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Common utilities for parsing routing instances. + +#ifndef OR_TOOLS_ROUTING_SIMPLE_GRAPH_H_ +#define OR_TOOLS_ROUTING_SIMPLE_GRAPH_H_ + +#include +#include +#include +#include + +#include "absl/hash/hash.h" + +namespace operations_research { + +class Arc; + +// Edge, undirected, between the head to the tail. +// With a few bells and whistles to allow its use within hash tables. +class Edge { + public: + Edge(int64_t tail, int64_t head) : tail_(tail), head_(head) {} + explicit Edge(const Arc& arc); + + int64_t tail() const { return tail_; } + int64_t head() const { return head_; } + + bool operator==(const Edge& other) const { + return (head_ == other.head_ && tail_ == other.tail_) || + (head_ == other.tail_ && tail_ == other.head_); + } + + bool operator!=(const Edge& other) const { return !this->operator==(other); } + + template + friend H AbslHashValue(H h, const Edge& a) { + // This hash value should not depend on the direction of the edge, hence + // the use of min and max. + return H::combine(std::move(h), std::min(a.head_, a.tail_), + std::max(a.head_, a.tail_)); + } + + private: + const int64_t tail_; + const int64_t head_; +}; + +// Arc, directed, from the tail to the head. +// With a few bells and whistles to allow its use within hash tables. +class Arc { + public: + Arc(int64_t tail, int64_t head) : tail_(tail), head_(head) {} + explicit Arc(const Edge& edge); + + int64_t tail() const { return tail_; } + int64_t head() const { return head_; } + Arc reversed() const { return {head_, tail_}; } + + bool operator==(const Arc& other) const { + return head_ == other.head_ && tail_ == other.tail_; + } + + bool operator!=(const Arc& other) const { return !this->operator==(other); } + + template + friend H AbslHashValue(H h, const Arc& a) { + // Unlike the edge, this value *must* depend on the direction of the arc. + return H::combine(std::move(h), a.tail_, a.head_); + } + + private: + const int64_t tail_; + const int64_t head_; +}; + +// Mapping between an edge (given by its tail and its head) and its weight. +typedef std::function EdgeWeights; + +// Real-world coordinates. +template +struct Coordinates2 { + T x = {}; + T y = {}; + + Coordinates2() = default; + Coordinates2(T x, T y) : x(x), y(y) {} + + friend bool operator==(const Coordinates2& a, const Coordinates2& b) { + return a.x == b.x && a.y == b.y; + } + friend bool operator!=(const Coordinates2& a, const Coordinates2& b) { + return !(a == b); + } + friend std::ostream& operator<<(std::ostream& stream, + const Coordinates2& coordinates) { + return stream << "{x = " << coordinates.x << ", y = " << coordinates.y + << "}"; + } + template + friend H AbslHashValue(H h, const Coordinates2& coordinates) { + return H::combine(std::move(h), coordinates.x, coordinates.y); + } +}; + +template +struct Coordinates3 { + T x = {}; + T y = {}; + T z = {}; + + Coordinates3() = default; + Coordinates3(T x, T y, T z) : x(x), y(y), z(z) {} + + friend bool operator==(const Coordinates3& a, const Coordinates3& b) { + return a.x == b.x && a.y == b.y && a.z == b.z; + } + friend bool operator!=(const Coordinates3& a, const Coordinates3& b) { + return !(a == b); + } + friend std::ostream& operator<<(std::ostream& stream, + const Coordinates3& coordinates) { + return stream << "{x = " << coordinates.x << ", y = " << coordinates.y + << ", z = " << coordinates.z << "}"; + } + template + friend H AbslHashValue(H h, const Coordinates3& coordinates) { + return H::combine(std::move(h), coordinates.x, coordinates.y, + coordinates.z); + } +}; + +// Time window, typically used for a node. +// Name chosen to avoid clash with tour_optimization.proto, defining a +// TimeWindow message with more fields. +template +struct SimpleTimeWindow { + T start; + T end; +}; + +} // namespace operations_research + +#endif // OR_TOOLS_ROUTING_SIMPLE_GRAPH_H_ diff --git a/ortools/routing/simple_graph_test.cc b/ortools/routing/simple_graph_test.cc new file mode 100644 index 0000000000..197439f5ad --- /dev/null +++ b/ortools/routing/simple_graph_test.cc @@ -0,0 +1,103 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/simple_graph.h" + +#include +#include + +#include "absl/hash/hash_testing.h" +#include "gtest/gtest.h" + +namespace operations_research { +namespace { +TEST(SimpleGraphTest, EdgeHashing) { + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly({ + Edge(0, 0), + Edge(1, 2), + Edge(2, 1), + })); +} + +TEST(SimpleGraphTest, EdgeEquality) { + EXPECT_EQ(Edge(0, 0), Edge(0, 0)); + EXPECT_NE(Edge(0, 0), Edge(1, 2)); + EXPECT_EQ(Edge(2, 1), Edge(1, 2)); // Undirected edge. +} + +TEST(SimpleGraphTest, ArcHashing) { + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly({ + Arc(0, 0), + Arc(1, 2), + Arc(2, 1), + })); +} + +TEST(SimpleGraphTest, ArcEquality) { + EXPECT_EQ(Arc(0, 0), Arc(0, 0)); + EXPECT_NE(Arc(0, 0), Arc(1, 2)); + EXPECT_NE(Arc(2, 1), Arc(1, 2)); // Directed edge. +} + +TEST(Coordinates2Test, Int) { + EXPECT_EQ(Coordinates2(0, 0), Coordinates2(0, 0)); + EXPECT_NE(Coordinates2(0, 0), Coordinates2(1, 2)); + + std::stringstream generated; + generated << Coordinates2(0, 1); + EXPECT_EQ(generated.str(), "{x = 0, y = 1}"); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {Coordinates2(), Coordinates2(0, 0), Coordinates2(1, 2)})); +} + +TEST(Coordinates2Test, Double) { + EXPECT_EQ(Coordinates2(0, 0), Coordinates2(0, 0)); + EXPECT_NE(Coordinates2(0, 0), Coordinates2(1, 2)); + + std::stringstream generated; + generated << Coordinates2(0.0, 1.0); + EXPECT_EQ(generated.str(), "{x = 0, y = 1}"); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {Coordinates2(), Coordinates2(0, 0), + Coordinates2(1, 2)})); +} + +TEST(Coordinates3Test, Int) { + EXPECT_EQ(Coordinates3(0, 0, 0), Coordinates3(0, 0, 0)); + EXPECT_NE(Coordinates3(0, 0, 0), Coordinates3(1, 2, 3)); + + std::stringstream generated; + generated << Coordinates3(0, 1, 2); + EXPECT_EQ(generated.str(), "{x = 0, y = 1, z = 2}"); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {Coordinates3(), Coordinates3(0, 0, 0), + Coordinates3(1, 2, 3)})); +} + +TEST(Coordinates3Test, Double) { + EXPECT_EQ(Coordinates3(0, 0, 0), Coordinates3(0, 0, 0)); + EXPECT_NE(Coordinates3(0, 0, 0), Coordinates3(1, 2, 3)); + + std::stringstream generated; + generated << Coordinates3(0.0, 1.0, 2.0); + EXPECT_EQ(generated.str(), "{x = 0, y = 1, z = 2}"); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {Coordinates3(), Coordinates3(0.0, 0.0, 0.0), + Coordinates3(1.0, 2.0, 3.0)})); +} +} // namespace +} // namespace operations_research diff --git a/ortools/routing/solomon_parser.cc b/ortools/routing/solomon_parser.cc new file mode 100644 index 0000000000..f894ff0df7 --- /dev/null +++ b/ortools/routing/solomon_parser.cc @@ -0,0 +1,137 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/solomon_parser.h" + +#include +#include +#include +#include + +#include "absl/strings/match.h" +#include "absl/strings/str_split.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/logging.h" +#include "ortools/base/map_util.h" +#include "ortools/base/numbers.h" +#include "ortools/base/path.h" +#include "ortools/base/zipfile.h" +#include "ortools/util/filelineiter.h" + +namespace operations_research { + +SolomonParser::SolomonParser() + : sections_({{"VEHICLE", VEHICLE}, {"CUSTOMER", CUSTOMER}}) { + Initialize(); +} + +bool SolomonParser::LoadFile(const std::string& file_name) { + Initialize(); + return ParseFile(file_name); +} + +bool SolomonParser::LoadFile(const std::string& file_name, + const std::string& archive_name) { + Initialize(); + if (!absl::StartsWith(archive_name, "/")) { + return false; + } + const std::string fake_zip_path = "/zip" + archive_name; + std::shared_ptr fake_zip_closer( + zipfile::OpenZipArchive(archive_name)); + if (nullptr == fake_zip_closer) return false; + const std::string zip_filename = file::JoinPath(fake_zip_path, file_name); + return ParseFile(zip_filename); +} + +void SolomonParser::Initialize() { + name_.clear(); + vehicles_ = 0; + coordinates_.clear(); + capacity_ = 0; + demands_.clear(); + time_windows_.clear(); + service_times_.clear(); + section_ = NAME; + to_read_ = 1; +} + +bool SolomonParser::ParseFile(const std::string& file_name) { + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + const std::vector words = + absl::StrSplit(line, absl::ByAnyChar(" :\t"), absl::SkipEmpty()); + // Skip blank lines + if (words.empty()) continue; + if (to_read_ > 0) { + switch (section_) { + case NAME: { + name_ = words[0]; + break; + } + case VEHICLE: { + if (to_read_ == 1) { + if (words.size() != 2) return false; + vehicles_ = strings::ParseLeadingInt32Value(words[0], -1); + if (vehicles_ < 0) return false; + capacity_ = strings::ParseLeadingInt32Value(words[1], -1); + if (capacity_ < 0) return false; + } + break; + } + case CUSTOMER: { + if (to_read_ < 2) { + std::vector values; + for (int i = 1; i < words.size(); ++i) { + const int64_t value = + strings::ParseLeadingInt64Value(words[i], -1); + if (value < 0) return false; + values.push_back(value); + } + coordinates_.push_back({values[0], values[1]}); + demands_.push_back(values[2]); + time_windows_.push_back({values[3], values[4]}); + service_times_.push_back(values[5]); + ++to_read_; + } + break; + } + default: { + LOG(ERROR) << "Reading data outside section"; + return false; + } + } + --to_read_; + } else { // New section + section_ = gtl::FindWithDefault(sections_, words[0], UNKNOWN); + switch (section_) { + case VEHICLE: { + // Two rows: header and data. + to_read_ = 2; + break; + } + case CUSTOMER: { + to_read_ = 2; + break; + } + default: { + LOG(ERROR) << "Unknown section: " << section_; + return false; + } + } + } + } + return section_ == CUSTOMER; +} + +} // namespace operations_research diff --git a/ortools/routing/solomon_parser.h b/ortools/routing/solomon_parser.h new file mode 100644 index 0000000000..ccd744d255 --- /dev/null +++ b/ortools/routing/solomon_parser.h @@ -0,0 +1,138 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A parser for "Solomon" instances. The Solomon file library is a set of +// Capacitated Vehicle Routing Problems with Time Windows created by +// Pr. Marius Solomon. +// +// The goal is to find routes starting and ending at a depot which visit a +// set of nodes. The objective is first to minimize the number of routes and +// then to minimize the total distance traveled, distances being measured with +// the Euclidean distance. There are two types of constraints limiting the +// route lengths: +// - time windows restricting the time during which a node can be visited +// - vehicle capacity which limits the load of the vehicles performing the +// routes (each node has a corresponding demand which must be picked up +// by the vehicle). +// +// The format of the data is the following: +// +// +// VEHICLE +// NUMBER CAPACITY +// +// CUSTOMER +// CUST NO. XCOORD. YCOORD. DEMAND READY TIME DUE DATE SERVICE TIME +// +// +// The parser supports both standard instance files and zipped archives +// containing multiple instances. +// + +#ifndef OR_TOOLS_ROUTING_SOLOMON_PARSER_H_ +#define OR_TOOLS_ROUTING_SOLOMON_PARSER_H_ + +#include + +#include +#include +#include +#include + +#include "ortools/base/integral_types.h" +#include "ortools/base/macros.h" +#include "ortools/routing/simple_graph.h" + +namespace operations_research { + +// Solomon parser class. +class SolomonParser { + public: + SolomonParser(); + +#ifndef SWIG + SolomonParser(const SolomonParser&) = delete; + const SolomonParser& operator=(const SolomonParser&) = delete; +#endif + + // Loading an instance. Both methods return false in case of failure to read + // the instance. Loading a new instance clears the previously loaded instance. + + // Loads instance from a file. + bool LoadFile(const std::string& file_name); + // Loads instance from a file contained in a zipped archive; the archive can + // contain multiple files. + bool LoadFile(const std::string& file_name, const std::string& archive_name); + + // Returns the name of the instance being solved. + const std::string& name() const { return name_; } + // Returns the index of the depot. + int Depot() const { return 0; } + // Returns the number of nodes in the current routing problem. + int NumberOfNodes() const { return coordinates_.size(); } + // Returns the maximum number of vehicles to use. + int NumberOfVehicles() const { return vehicles_; } + // Returns the coordinates of the nodes in the current routing problem. + const std::vector>& coordinates() const { + return coordinates_; + } + // Returns the capacity of the vehicles. + int64_t capacity() const { return capacity_; } + // Returns the demand of the nodes in the current routing problem. + const std::vector& demands() const { return demands_; } + // Returns the time windows of the nodes in the current routing problem. + const std::vector>& time_windows() const { + return time_windows_; + } + // Returns the service times of the nodes in the current routing problem. + const std::vector& service_times() const { return service_times_; } + // Returns the distance between two nodes. + double GetDistance(int from, int to) const { + const Coordinates2& from_coords = coordinates_[from]; + const Coordinates2& to_coords = coordinates_[to]; + const double xd = from_coords.x - to_coords.x; + const double yd = from_coords.y - to_coords.y; + return sqrt(xd * xd + yd * yd); + } + // Returns the travel time between two nodes. + double GetTravelTime(int from, int to) const { + return service_times_[from] + GetDistance(from, to); + } + + private: + enum Section { UNKNOWN, NAME, VEHICLE, CUSTOMER }; + + // Parsing + void Initialize(); + bool ParseFile(const std::string& file_name); + + // Parsing data + const std::map sections_; + Section section_; + int to_read_; + // Input data + // Using int64_t to represent different dimension values of the problem: + // - demand and vehicle capacity, + // - distances and node coordinates, + // - time, including time window values and service times. + std::string name_; + int vehicles_; + std::vector> coordinates_; + int64_t capacity_; + std::vector demands_; + std::vector> time_windows_; + std::vector service_times_; +}; +} // namespace operations_research + +#endif // OR_TOOLS_ROUTING_SOLOMON_PARSER_H_ diff --git a/ortools/routing/solomon_parser_test.cc b/ortools/routing/solomon_parser_test.cc new file mode 100644 index 0000000000..0b71f8878f --- /dev/null +++ b/ortools/routing/solomon_parser_test.cc @@ -0,0 +1,68 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/solomon_parser.h" + +#include +#include + +#include "absl/flags/flag.h" +#include "gtest/gtest.h" +#include "ortools/base/commandlineflags.h" +#include "ortools/base/file.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/path.h" + +ABSL_FLAG(std::string, test_srcdir, "", "REQUIRED: src dir"); + +ABSL_FLAG(std::string, solomon_test_archive, + "ortools/bench/solomon/" + "testdata/solomon.zip", + "Solomon: testing archive"); +ABSL_FLAG(std::string, solomon_test_instance, "google2.txt", + "Solomon: testing instance"); + +namespace operations_research { +namespace { +TEST(SolomonParserTest, LoadEmptyFileName) { + std::string empty_file_name; + SolomonParser parser; + EXPECT_FALSE(parser.LoadFile(empty_file_name)); +} + +TEST(SolomonParserTest, LoadNonExistingFile) { + SolomonParser parser; + EXPECT_FALSE(parser.LoadFile("")); +} + +TEST(SolomonParserTest, LoadNonEmptyArchive) { + std::string empty_archive_name; + SolomonParser parser; + EXPECT_FALSE(parser.LoadFile(absl::GetFlag(FLAGS_solomon_test_instance), + empty_archive_name)); +} + +TEST(SolomonParserTest, LoadNonExistingArchive) { + SolomonParser parser; + EXPECT_FALSE(parser.LoadFile(absl::GetFlag(FLAGS_solomon_test_instance), "")); +} + +TEST(SolomonParserTest, LoadNonExistingInstance) { + SolomonParser parser; + EXPECT_FALSE(parser.LoadFile( + "doesnotexist.txt", + file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + absl::GetFlag(FLAGS_solomon_test_archive)))); +} +} // namespace +} // namespace operations_research diff --git a/ortools/routing/solution_serializer.cc b/ortools/routing/solution_serializer.cc new file mode 100644 index 0000000000..2485657acc --- /dev/null +++ b/ortools/routing/solution_serializer.cc @@ -0,0 +1,424 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools//routing/solution_serializer.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/ascii.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "ortools/base/logging.h" + +namespace operations_research { + +RoutingOutputFormat RoutingOutputFormatFromString(std::string_view format) { + const std::string format_normalized = + absl::AsciiStrToLower(absl::StripAsciiWhitespace(format)); + if (format_normalized == "tsplib") return RoutingOutputFormat::kTSPLIB; + if (format_normalized == "cvrplib") return RoutingOutputFormat::kCVRPLIB; + if (format_normalized == "carplib") return RoutingOutputFormat::kCARPLIB; + if (format_normalized == "nearplib") return RoutingOutputFormat::kNEARPLIB; + return RoutingOutputFormat::kNone; +} + +// Helper for FromSplitRoutes. +namespace { +std::vector RoutesFromVector( + const std::vector>& routes, + std::optional depot = std::nullopt); +} // namespace + +std::vector> RoutingSolution::SplitRoutes( + const std::vector& solution, int64_t separator) { + // The solution vector separates routes by -1: split this vector into a vector + // per route, where the other helpers can make the rest of the way to a proper + // RoutingSolution object. + std::vector> routes; + int64_t current_route = 0; + for (int64_t node : solution) { + if (routes.size() == current_route) { + routes.emplace_back(std::vector()); + } + + if (node == separator) { + current_route += 1; + } else { + routes[current_route].emplace_back(node); + } + } + return routes; +} + +RoutingSolution RoutingSolution::FromSplitRoutes( + const std::vector>& routes, + std::optional depot) { + std::vector total_demands(routes.size(), -1); + std::vector total_distances(routes.size(), -1); + + return {RoutesFromVector(routes, depot), total_demands, total_distances}; +} + +int64_t RoutingSolution::NumberOfNonemptyRoutes() const { + int64_t num_nonempty_routes = 0; + for (const Route& route : routes_) { + if (!route.empty()) num_nonempty_routes++; + } + return num_nonempty_routes; +} + +void RoutingSolution::WriteToSolutionFile(RoutingOutputFormat format, + const std::string& file_name) const { + File* file; + CHECK_OK(file::Open(file_name, "w", &file, file::Defaults())) + << "Could not open the solution file '" << file_name << "'"; + CHECK_OK(file::WriteString(file, SerializeToSolutionFile(format), + file::Defaults())) + << "Could not write the solution file '" << file_name << "'"; + CHECK_OK(file->Close(file::Defaults())); +} + +std::string RoutingSolution::SerializeToTSPLIBString() const { + std::string tour_out; + for (const Route& route : routes_) { + if (route.empty()) continue; + + for (const Event& event : route) { + if (event.type != RoutingSolution::Event::Type::kEnd) { + absl::StrAppendFormat(&tour_out, "%d\n", event.arc.head()); + } + } + absl::StrAppendFormat(&tour_out, "-1\n"); + } + return tour_out; +} + +std::string RoutingSolution::SerializeToTSPLIBSolutionFile() const { + // Determine the number of nodes as the maximum index of a node in the + // solution, plus one (due to TSPLIB being 1-based and C++ 0-based). + int64_t number_of_nodes = 0; + for (const Route& route : routes_) { + for (const Event& event : route) { + if (event.arc.tail() > number_of_nodes) { + number_of_nodes = event.arc.tail(); + } + if (event.arc.head() > number_of_nodes) { + number_of_nodes = event.arc.head(); + } + } + } + number_of_nodes += 1; + + std::string tour_out; + absl::StrAppendFormat(&tour_out, "NAME : %s\n", name_); + absl::StrAppendFormat(&tour_out, "COMMENT : Length = %d; Total time = %f s\n", + total_distance_, total_time_); + absl::StrAppendFormat(&tour_out, "TYPE : TOUR\n"); + absl::StrAppendFormat(&tour_out, "DIMENSION : %d\n", number_of_nodes); + absl::StrAppendFormat(&tour_out, "TOUR_SECTION\n"); + absl::StrAppendFormat(&tour_out, "%s", SerializeToTSPLIBString()); + absl::StrAppendFormat(&tour_out, "EOF"); + return tour_out; +} + +namespace { +std::string SerializeRouteToCVRPLIBString(const RoutingSolution::Route& route); +} // namespace + +std::string RoutingSolution::SerializeToCVRPLIBString() const { + std::string tour_out; // The complete solution. + int route_index = 1; // Index of the route being written. + + for (const Route& route : routes_) { + if (route.empty()) continue; + std::string current_route = SerializeRouteToCVRPLIBString(route); + + // Output the current route only if it is not empty. + if (!current_route.empty()) { + absl::StrAppendFormat(&tour_out, "Route #%d: %s\n", route_index++, + absl::StripAsciiWhitespace(current_route)); + } + } + return tour_out; +} + +std::string RoutingSolution::SerializeToCVRPLIBSolutionFile() const { + std::string tour_out = SerializeToCVRPLIBString(); + absl::StrAppendFormat(&tour_out, "Cost %d", total_cost_); + return tour_out; +} + +std::string RoutingSolution::SerializeToCARPLIBString() const { + std::string tour_out; // The complete solution. + int64_t num_out_route = 1; // Index of the route being written. + int64_t num_iteration_route = 0; // Index of the route being considered. + int64_t depot; + + for (const Route& route : routes_) { + std::string current_route; + + for (const RoutingSolution::Event& event : route) { + std::string type; + switch (event.type) { + case RoutingSolution::Event::Type::kStart: + ABSL_FALLTHROUGH_INTENDED; + case RoutingSolution::Event::Type::kEnd: + CHECK_EQ(event.arc.tail(), event.arc.head()); + depot = event.arc.tail(); + type = "D"; + break; + case RoutingSolution::Event::Type::kServeArc: + case RoutingSolution::Event::Type::kServeEdge: + ABSL_FALLTHROUGH_INTENDED; + case RoutingSolution::Event::Type::kServeNode: + // The only difference is in the arc: when serving a node, both the + // head and the tail are the node being served. + type = "S"; + break; + case RoutingSolution::Event::Type::kTransit: + // Not present in CARPLIB output. + break; + } + + if (!type.empty()) { + absl::StrAppendFormat(¤t_route, "(%s %d,%d,%d) ", type, + event.demand_id, event.arc.tail() + 1, + event.arc.head() + 1); + } + } + + // Output the current route only if it is not empty. + if (!route.empty()) { + const int64_t day = 1; + const int64_t num_events = std::count_if( + route.begin(), route.end(), [](const RoutingSolution::Event& event) { + // Bare transitions are not output in CARPLIB, don't count them. + return event.type != RoutingSolution::Event::Type::kTransit; + }); + + absl::StrAppendFormat( + &tour_out, "%d %d %d %d %d %d %s\n", + depot, // Use a 0-based encoding for the depot here. + day, num_out_route, total_demands_[num_iteration_route], + total_distances_[num_iteration_route], num_events, + absl::StripAsciiWhitespace(current_route)); + + num_out_route += 1; + } + + num_iteration_route += 1; + } + absl::StripTrailingAsciiWhitespace(&tour_out); + return tour_out; +} + +std::string RoutingSolution::SerializeToCARPLIBSolutionFile() const { + std::string solution; + absl::StrAppendFormat(&solution, "%d\n", total_cost_); + absl::StrAppendFormat(&solution, "%d\n", NumberOfNonemptyRoutes()); + absl::StrAppendFormat(&solution, "%f\n", total_time_); + absl::StrAppend(&solution, SerializeToCARPLIBString()); + return solution; +} + +std::string RoutingSolution::SerializeToNEARPLIBString() const { + std::string tour_out; // The complete solution. + int64_t route_index = 1; // Index of the route being written. + + for (const Route& route : routes_) { + std::string current_route; + int64_t current_node = -2; // Holds the last node that was output, i.e. + // where the vehicle is located at the beginning of each iteration. -1 is + // used for the depot, hence an even lower value. + + // Skip empty routes. + if (route.size() <= 1) continue; + if (route.size() == 2 && + route[0].type == RoutingSolution::Event::Type::kStart && + route[1].type == RoutingSolution::Event::Type::kEnd) + continue; + + // Print the nodes that are traversed only when they are a depot or some end + // of a serviced arc/edge, without repeating nodes when two consecutive + // serviced arcs/edges are incident to the same node in the middle. + // Hence, current_node is used to determine whether the sequence of + // arcs/edges is continued or should start over. + // Only set current_node when a sequence should be continued (e.g., not + // when only traversing an arc/edge). + for (const RoutingSolution::Event& event : route) { + switch (event.type) { + case RoutingSolution::Event::Type::kStart: + // Always start at the depot. + CHECK_EQ(event.arc.tail(), event.arc.head()); + current_node = event.arc.tail(); + absl::StrAppendFormat(¤t_route, "%d", event.arc.tail() + 1); + break; + case RoutingSolution::Event::Type::kEnd: + // Always print the end depot. + CHECK_EQ(event.arc.tail(), event.arc.head()); + if (current_node != event.arc.tail()) { + absl::StrAppendFormat(¤t_route, " %d", event.arc.tail() + 1); + } + break; + case RoutingSolution::Event::Type::kServeArc: + ABSL_FALLTHROUGH_INTENDED; + case RoutingSolution::Event::Type::kServeEdge: + CHECK(!event.arc_name.empty()) + << "Arc " << event.arc.tail() << "-" << event.arc.head() + << " does not have a name in the solution object."; + + // TODO(user): print the name of the node when it is served + // (i.e. there is a kServeNode event just after). For now, it's only + // done when the node happens before. + if (current_node == event.arc.tail()) { + // Direct continuation of the path: just add a hyphen and go on. + absl::StrAppendFormat(¤t_route, "-%s-%d", event.arc_name, + event.arc.head() + 1); + } else { + // Some part of the path is not explicitly output before the + // previous node and the one after this edge is served. + absl::StrAppendFormat(¤t_route, " %d-%s-%d", + event.arc.tail() + 1, event.arc_name, + event.arc.head() + 1); + } + current_node = event.arc.head(); + break; + case RoutingSolution::Event::Type::kServeNode: + CHECK_EQ(event.arc.tail(), event.arc.head()); + absl::StrAppendFormat(¤t_route, " N%d", event.arc.head() + 1); + current_node = event.arc.head(); + break; + case RoutingSolution::Event::Type::kTransit: + current_node = -2; + break; + } + } + + // Output the current route only if it is not empty. + if (!current_route.empty()) { + absl::StrAppendFormat(&tour_out, "Route #%d : %s\n", route_index++, + absl::StripAsciiWhitespace(current_route)); + } + } + absl::StripTrailingAsciiWhitespace(&tour_out); + return tour_out; +} + +std::string RoutingSolution::SerializeToNEARPLIBSolutionFile() const { + const std::string date = + absl::FormatTime("%B %d, %E4Y", absl::Now(), absl::LocalTimeZone()); + + std::string solution; + absl::StrAppendFormat(&solution, "Instance name: %s\n", name_); + absl::StrAppendFormat(&solution, "Authors: %s\n", authors_); + absl::StrAppendFormat(&solution, "Date: %s\n", date); + absl::StrAppendFormat(&solution, "Reference: OR-Tools\n"); + absl::StrAppendFormat(&solution, "Solution\n"); + absl::StrAppendFormat(&solution, "%s\n", SerializeToNEARPLIBString()); + absl::StrAppendFormat(&solution, "Total cost: %d", total_cost_); + // Official solutions for CBMix use "total cost", whereas the definition of + // the output format rather uses "cost": + // https://www.sintef.no/globalassets/project/top/nearp/cbmix-results/cbmix22.txt + // https://www.sintef.no/globalassets/project/top/nearp/solutionformat.txt + return solution; +} + +namespace { +RoutingSolution::Route RouteFromVector( + const std::vector& route_int, + std::optional depot = std::nullopt); + +std::vector RoutesFromVector( + const std::vector>& routes, + std::optional depot) { + std::vector solution_routes; + solution_routes.reserve(routes.size()); + for (const std::vector& route : routes) { + // TODO(user): explore merging RouteFromVector in this function. + solution_routes.emplace_back(RouteFromVector(route, depot)); + } + return solution_routes; +} + +RoutingSolution::Route RouteFromVector(const std::vector& route_int, + std::optional forced_depot) { + // One route in input: from the node indices, create a Route object (not yet + // a RoutingSolution one). + RoutingSolution::Route route; + + // If no depot is given, guess one. + int64_t depot = + (forced_depot.has_value()) ? forced_depot.value() : route_int[0]; + + route.emplace_back( + RoutingSolution::Event{/*type=*/RoutingSolution::Event::Type::kStart, + /*demand_id=*/-1, /*arc=*/Arc{depot, depot}}); + for (int64_t i = 0; i < route_int.size() - 1; ++i) { + int64_t tail = route_int[i]; + int64_t head = route_int[i + 1]; + route.emplace_back(RoutingSolution::Event{ + RoutingSolution::Event::Type::kTransit, -1, Arc{tail, head}}); + } + route.emplace_back(RoutingSolution::Event{RoutingSolution::Event::Type::kEnd, + -1, Arc{depot, depot}}); + + return route; +} + +std::string SerializeRouteToCVRPLIBString(const RoutingSolution::Route& route) { + // Before serializing the route, make some tests to check that the hypotheses + // are respected (otherwise, the output of the function is highly likely + // pure garbage). + RoutingSolution::Event first_event = route[0]; + CHECK(first_event.type == RoutingSolution::Event::Type::kStart) + << "The route does not begin with a Start event to indicate " + "the depot."; + const int64_t depot = first_event.arc.tail(); + + CHECK_GE(depot, 0) << "The given depot is negative: " << depot; + CHECK_LE(depot, 1) << "The given depot is greater than 1: " << depot; + + // Serialize this route, ignoring the depot (already dealt with). + std::string current_route; + + for (int64_t i = 1; i < route.size() - 1; ++i) { + RoutingSolution::Event event = route[i]; + + // Ignore the depot, as CVRPLIB doesn't output the depot in the routes + // (all routes implicitly start and end at the depot). + int64_t node = event.arc.head(); + if (node > depot) { + absl::StrAppendFormat(¤t_route, "%d ", node - depot); + } + } + + // Last event: end at a depot. Due to the strange way CVRPLIB + // outputs nodes, the depot must be the same at the beginning and the + // end of the route. + RoutingSolution::Event last_event = route.back(); + if (last_event.type == RoutingSolution::Event::Type::kEnd) { + CHECK_EQ(depot, last_event.arc.tail()); + CHECK_EQ(last_event.arc.tail(), last_event.arc.head()); + } else { + LOG(FATAL) << "The route does not finish with an End event to " + "indicate the depot."; + } + + return current_route; +} +} // namespace +} // namespace operations_research diff --git a/ortools/routing/solution_serializer.h b/ortools/routing/solution_serializer.h new file mode 100644 index 0000000000..bee31ac056 --- /dev/null +++ b/ortools/routing/solution_serializer.h @@ -0,0 +1,294 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Utilities to serialize VRP-like solutions in standardised formats: either +// TSPLIB or CVRPLIB. + +#ifndef OR_TOOLS_ROUTING_SOLUTION_SERIALIZER_H_ +#define OR_TOOLS_ROUTING_SOLUTION_SERIALIZER_H_ + +#include +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "ortools/base/file.h" +#include "ortools/base/helpers.h" +#include "ortools/base/logging.h" +#include "ortools/routing/simple_graph.h" + +namespace operations_research { + +// Indicates the format in which the output should be done. This enumeration is +// used for solutions and solver statistics. +enum class RoutingOutputFormat { + kNone, + kTSPLIB, + kCVRPLIB, + kCARPLIB, + kNEARPLIB +}; + +// Parses a user-provided description of the output format. Expected inputs look +// like (without quotes): "tsplib", "cvrplib", "carplib". Unrecognized strings +// are parsed as kNone. +RoutingOutputFormat RoutingOutputFormatFromString(std::string_view format); + +// Describes completely a solution to a routing problem in preparation of its +// serialization as a string. +class RoutingSolution { + public: + // Describes a state transition performed by a vehicle: starting from/ending + // at a given depot, serving a given customer, etc. + // When need be, each event can have a specific demand ID (this is mostly + // useful when servicing arcs and edges). An event always stores an arc: + // this is simply the edge when servicing the edge (it should correspond to + // the direction in which the edge is traversed); when the event is about + // a node (either a depot or a demand), both ends of the arc should be the + // node the event is about. + struct Event { + // Describes the type of events that occur along a route. + enum class Type { + // The vehicle starts its route at a depot. + kStart, + // The vehicle ends its route at a depot (not necessarily the same as the + // starting one). + kEnd, + // The vehicle traverses the arc while servicing it. + kServeArc, + // The vehicle traverses the edge while servicing it. + kServeEdge, + // The vehicle serves the demand of the node. + kServeNode, + // The vehicle simply goes through an edge or an arc without servicing. + kTransit + }; + + Type type; + int64_t demand_id; + Arc arc; + std::string arc_name; + + Event(Type type, int64_t demand_id, Arc arc) + : type(type), demand_id(demand_id), arc(arc) {} + Event(Type type, int64_t demand_id, Arc arc, std::string_view arc_name) + : type(type), demand_id(demand_id), arc(arc), arc_name(arc_name) {} + + bool operator==(const Event& other) const { + return type == other.type && demand_id == other.demand_id && + arc == other.arc && arc_name == other.arc_name; + } + bool operator!=(const Event& other) const { return !(*this == other); } + }; + + using Route = std::vector; + + RoutingSolution(std::vector routes, std::vector total_demands, + std::vector total_distances, int64_t total_cost = -1, + int64_t total_distance = -1, double total_time = -1.0, + std::string_view name = "") + : routes_(std::move(routes)), + total_demands_(std::move(total_demands)), + total_distances_(std::move(total_distances)), + total_cost_(total_cost), + total_distance_(total_distance), + total_time_(total_time), + name_(name) { + CHECK_EQ(routes_.size(), total_demands_.size()); + CHECK_EQ(routes_.size(), total_distances_.size()); + } + + bool operator==(const RoutingSolution& other) const { + return routes_ == other.routes_ && total_demands_ == other.total_demands_ && + total_distances_ == other.total_distances_ && + total_cost_ == other.total_cost_ && total_time_ == other.total_time_; + } + bool operator!=(const RoutingSolution& other) const { + return !(*this == other); + } + + // Setters for solution metadata. + void SetTotalTime(double total_time) { total_time_ = total_time; } + void SetTotalCost(int64_t total_cost) { total_cost_ = total_cost; } + void SetTotalDistance(int64_t total_distance) { + total_distance_ = total_distance; + } + void SetName(std::string_view name) { name_ = name; } + void SetAuthors(std::string_view authors) { authors_ = authors; } + + // Public-facing builders. + + // Splits a list of nodes whose routes are separated by the given separator + // (TSPLIB uses -1; it is crucial that the separator cannot be a node) into + // a vector per route, for use in FromSplit* functions. + static std::vector> SplitRoutes( + const std::vector& solution, int64_t separator); + + // Builds a RoutingSolution object from a vector of routes, each represented + // as a vector of nodes being traversed. All the routes are supposed to start + // and end at the depot if specified. + static RoutingSolution FromSplitRoutes( + const std::vector>& routes, + std::optional depot = std::nullopt); + + // Serializes the bare solution to a string, i.e. only the routes for the + // vehicles, without other metadata that is typically present in solution + // files. + std::string SerializeToString(RoutingOutputFormat format) const { + switch (format) { + case RoutingOutputFormat::kNone: + return ""; + case RoutingOutputFormat::kTSPLIB: + return SerializeToTSPLIBString(); + case RoutingOutputFormat::kCVRPLIB: + return SerializeToCVRPLIBString(); + case RoutingOutputFormat::kCARPLIB: + return SerializeToCARPLIBString(); + case RoutingOutputFormat::kNEARPLIB: + return SerializeToNEARPLIBString(); + } + } + + // Serializes the full solution to the given file, including metadata like + // instance name or total cost, depending on the format. + // For TSPLIB, solution files are typically called "tours". + std::string SerializeToSolutionFile(RoutingOutputFormat format) const { + switch (format) { + case RoutingOutputFormat::kNone: + return ""; + case RoutingOutputFormat::kTSPLIB: + return SerializeToTSPLIBSolutionFile(); + case RoutingOutputFormat::kCVRPLIB: + return SerializeToCVRPLIBSolutionFile(); + case RoutingOutputFormat::kCARPLIB: + return SerializeToCARPLIBSolutionFile(); + case RoutingOutputFormat::kNEARPLIB: + return SerializeToNEARPLIBSolutionFile(); + } + } + + // Serializes the full solution to the given file, including metadata like + // instance name or total cost, depending on the format. + void WriteToSolutionFile(RoutingOutputFormat format, + const std::string& file_name) const; + + private: + // Description of the solution. Typically, one element per route (e.g., one + // vector of visited nodes per route). These elements are supposed to be + // returned by a solver. + // Depots are not explicitly stored as a route-level attribute, but rather by + // specific transitions (starting or ending at a depot). + std::vector> routes_; + std::vector total_demands_; + std::vector total_distances_; + + // Solution metadata. These elements could be set either by the solver or by + // the caller. + int64_t total_cost_; + int64_t total_distance_; + double total_time_; + std::string name_; + std::string authors_; + + int64_t NumberOfNonemptyRoutes() const; + + // The various implementations of SerializeToString depending on the format. + + // Generates a string representation of a solution in the TSPLIB format. + // TSPLIB explicitly outputs the depot in its tours. + // It has been defined in + // http://comopt.ifi.uni-heidelberg.de/software/TSPLIB95/ where solutions are + // referred to as "tours". + std::string SerializeToTSPLIBString() const; + // Generates a string representation of a solution in the CVRPLIB format. + // CVRPLIB doesn't explicitly output the depot in its tours. + // Format used in http://vrp.atd-lab.inf.puc-rio.br/ + // Better description of the format: + // http://dimacs.rutgers.edu/programs/challenge/vrp/cvrp/ + std::string SerializeToCVRPLIBString() const; + // Generates a string representation of a solution in the CARPLIB format. + // Format used in https://www.uv.es/belengue/carp.html + // Formal description of the format: https://www.uv.es/~belengue/carp/READ_ME + // Another description of the format: + // http://dimacs.rutgers.edu/programs/challenge/vrp/carp/ + std::string SerializeToCARPLIBString() const; + // Generates a string representation of a solution in the NEARPLIB format. + // Format used in https://www.sintef.no/projectweb/top/nearp/ + // Formal description of the format: + // https://www.sintef.no/projectweb/top/nearp/documentation/ + // Example: + // https://www.sintef.no/globalassets/project/top/nearp/solutionformat.txt + std::string SerializeToNEARPLIBString() const; + + // The various implementations of SerializeToSolutionFile depending on the + // format. These methods are highly similar to the previous ones. + std::string SerializeToTSPLIBSolutionFile() const; + std::string SerializeToCVRPLIBSolutionFile() const; + std::string SerializeToCARPLIBSolutionFile() const; + std::string SerializeToNEARPLIBSolutionFile() const; +}; + +// Formats a solution or solver statistic according to the given format. +template +std::string FormatStatistic(const std::string& name, T value, + RoutingOutputFormat format) { + // TODO(user): think about using an enum instead of names (or even a + // full-fledged struct/class) for the various types of fields. + switch (format) { + case RoutingOutputFormat::kNone: + ABSL_FALLTHROUGH_INTENDED; + case RoutingOutputFormat::kTSPLIB: + return absl::StrCat(name, " = ", value); + case RoutingOutputFormat::kCVRPLIB: + return absl::StrCat(name, " ", value); + case RoutingOutputFormat::kCARPLIB: + // For CARPLIB, the statistics do not have names, it's up to the user to + // memorize their order. + return absl::StrCat(value); + case RoutingOutputFormat::kNEARPLIB: + return absl::StrCat(name, " : ", value); + } +} + +// Specialization for doubles to show a higher precision: without this +// specialization, 591.556557 is displayed as 591.557. +template <> +inline std::string FormatStatistic(const std::string& name, double value, + RoutingOutputFormat format) { + switch (format) { + case RoutingOutputFormat::kNone: + ABSL_FALLTHROUGH_INTENDED; + case RoutingOutputFormat::kTSPLIB: + return absl::StrFormat("%s = %f", name, value); + case RoutingOutputFormat::kCVRPLIB: + return absl::StrFormat("%s %f", name, value); + case RoutingOutputFormat::kCARPLIB: + return absl::StrFormat("%f", value); + case RoutingOutputFormat::kNEARPLIB: + return absl::StrFormat("%s : %f", name, value); + } +} + +// Prints a formatted solution or solver statistic according to the given +// format. +template +void PrintStatistic(const std::string& name, T value, + RoutingOutputFormat format) { + absl::PrintF("%s\n", FormatStatistic(name, value, format)); +} +} // namespace operations_research + +#endif // OR_TOOLS_ROUTING_SOLUTION_SERIALIZER_H_ diff --git a/ortools/routing/solution_serializer_test.cc b/ortools/routing/solution_serializer_test.cc new file mode 100644 index 0000000000..097c61a60b --- /dev/null +++ b/ortools/routing/solution_serializer_test.cc @@ -0,0 +1,611 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/solution_serializer.h" + +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/mutable_memfile.h" + +namespace operations_research { +namespace { +TEST(RoutingSolutionSerializerTest, RoutingSolutionEventComparison) { + RoutingSolution::Event t1 = {RoutingSolution::Event::Type::kStart, 0, + Arc{0, 0}}; + RoutingSolution::Event t2 = {RoutingSolution::Event::Type::kStart, 0, + Arc{0, 0}}; // Same as t1. + RoutingSolution::Event t3 = {RoutingSolution::Event::Type::kEnd, 0, + Arc{0, 0}}; + RoutingSolution::Event t4 = {RoutingSolution::Event::Type::kStart, 1, + Arc{0, 0}}; + RoutingSolution::Event t5 = {RoutingSolution::Event::Type::kStart, 0, + Arc{1, 0}}; + RoutingSolution::Event t6 = {RoutingSolution::Event::Type::kStart, 0, + Arc{0, 1}}; + EXPECT_EQ(t1, t1); + EXPECT_EQ(t1, t2); + EXPECT_NE(t1, t3); + EXPECT_NE(t1, t4); + EXPECT_NE(t1, t5); + EXPECT_NE(t1, t6); +} + +TEST(RoutingSolutionSerializerTest, ParseEmptyString) { + EXPECT_EQ(RoutingOutputFormatFromString(""), RoutingOutputFormat::kNone); +} + +TEST(RoutingSolutionSerializerTest, ParseUnrecognizedString) { + EXPECT_EQ(RoutingOutputFormatFromString("ThisIsPureGarbage"), + RoutingOutputFormat::kNone); +} + +TEST(RoutingSolutionSerializerTest, ParseNoneString) { + EXPECT_EQ(RoutingOutputFormatFromString("NONE"), RoutingOutputFormat::kNone); +} + +TEST(RoutingSolutionSerializerTest, ParseTsplibString) { + EXPECT_EQ(RoutingOutputFormatFromString("tsplib"), + RoutingOutputFormat::kTSPLIB); + EXPECT_EQ(RoutingOutputFormatFromString("TSPLIB"), + RoutingOutputFormat::kTSPLIB); +} + +TEST(RoutingSolutionSerializerTest, ParseCvrplibString) { + EXPECT_EQ(RoutingOutputFormatFromString("cvrplib"), + RoutingOutputFormat::kCVRPLIB); + EXPECT_EQ(RoutingOutputFormatFromString("CVRPLIB"), + RoutingOutputFormat::kCVRPLIB); +} + +TEST(RoutingSolutionSerializerTest, ParseCarplibString) { + EXPECT_EQ(RoutingOutputFormatFromString("carplib"), + RoutingOutputFormat::kCARPLIB); + EXPECT_EQ(RoutingOutputFormatFromString("CARPLIB"), + RoutingOutputFormat::kCARPLIB); +} + +TEST(RoutingSolutionSerializerTest, ParseNearplibString) { + EXPECT_EQ(RoutingOutputFormatFromString("nearplib"), + RoutingOutputFormat::kNEARPLIB); + EXPECT_EQ(RoutingOutputFormatFromString("NEARPLIB"), + RoutingOutputFormat::kNEARPLIB); +} + +TEST(RoutingSolutionSerializerTest, FromSplitRoutesWithOneRoute) { + // Specifically test RouteFromVector in the implementation. + const std::vector> routes{{0, 1, 3, 0}}; + const RoutingSolution result = RoutingSolution::FromSplitRoutes(routes); + + const RoutingSolution expected_output = RoutingSolution{ + std::vector{RoutingSolution::Route{ + RoutingSolution::Event{RoutingSolution::Event::Type::kStart, -1, + Arc{0, 0}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{0, 1}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{1, 3}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{3, 0}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kEnd, -1, + Arc{0, 0}}, + }}, + std::vector{-1}, + std::vector{-1}, + }; + EXPECT_EQ(result, expected_output); +} + +TEST(RoutingSolutionSerializerTest, FromSplitRoutesWithTwoRoutes) { + // Specifically test RoutesFromVector in the implementation. + const std::vector> routes{ + {0, 1, 3, 0}, + {0, 2, 0}, + }; + const RoutingSolution result = RoutingSolution::FromSplitRoutes(routes); + + const RoutingSolution expected_output = {RoutingSolution{ + std::vector{ + RoutingSolution::Route{ + RoutingSolution::Event{RoutingSolution::Event::Type::kStart, -1, + Arc{0, 0}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{0, 1}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{1, 3}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{3, 0}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kEnd, -1, + Arc{0, 0}}, + }, + RoutingSolution::Route{ + RoutingSolution::Event{RoutingSolution::Event::Type::kStart, -1, + Arc{0, 0}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{0, 2}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{2, 0}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kEnd, -1, + Arc{0, 0}}, + }, + }, + std::vector{-1, -1}, + std::vector{-1, -1}, + }}; + + EXPECT_EQ(result, expected_output); +} + +TEST(RoutingSolutionSerializerTest, SolutionToTsplib) { + const std::vector solution{0, 1, 2, 3, 0, -1, 0, 4, 5, 6, 0, -1}; + const std::string expected_output = "0\n1\n2\n3\n0\n-1\n0\n4\n5\n6\n0\n-1\n"; + EXPECT_EQ(RoutingSolution::FromSplitRoutes( + RoutingSolution::SplitRoutes(solution, -1), 0) + .SerializeToString(RoutingOutputFormat::kTSPLIB), + expected_output); +} + +TEST(RoutingSolutionSerializerTest, SolutionToTsplibFile) { + std::string file_name = "/mmemfile/sol_tsplib"; + RegisteredMutableMemFile registered(file_name); + + const std::vector> solution_vector{{0, 1, 2, 3, 0}, + {0, 4, 5, 6, 0}}; + const std::string expected_output = + "NAME : Test name\n" + "COMMENT : Length = -1; Total time = -1.000000 s\n" + "TYPE : TOUR\n" + "DIMENSION : 7\n" + "TOUR_SECTION\n" + "0\n1\n2\n3\n0\n-1\n0\n4\n5\n6\n0\n-1\n" + "EOF"; + + RoutingSolution solution = + RoutingSolution::FromSplitRoutes(solution_vector, 0); + solution.SetName("Test name"); + solution.WriteToSolutionFile(RoutingOutputFormat::kTSPLIB, file_name); + std::string written_solution; + CHECK_OK(file::GetContents(file_name, &written_solution, file::Defaults())); + EXPECT_EQ(written_solution, expected_output); +} + +TEST(RoutingSolutionSerializerTest, SolutionToCvrplib) { + // Depot: 1. + const std::vector solution{1, 2, 3, 1, -1, 1, 4, 5, 6, 1, -1}; + const std::string expected_output = "Route #1: 1 2\nRoute #2: 3 4 5\n"; + + EXPECT_EQ(RoutingSolution::FromSplitRoutes( + RoutingSolution::SplitRoutes(solution, -1), 1) + .SerializeToString(RoutingOutputFormat::kCVRPLIB), + expected_output); +} + +TEST(RoutingSolutionSerializerTest, SolutionToCvrplibInvalidNoStart) { + const std::vector routes = { + RoutingSolution::Route{ + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{0, 1}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kEnd, -1, + Arc{0, 0}}, + }, + }; + const RoutingSolution solution{routes, {4}, {4}}; + std::string solution_str; + + EXPECT_DEATH( + solution_str = solution.SerializeToString(RoutingOutputFormat::kCVRPLIB), + ""); +} + +TEST(RoutingSolutionSerializerTest, SolutionToCvrplibInvalidNoEnd) { + const std::vector routes = { + RoutingSolution::Route{ + RoutingSolution::Event{RoutingSolution::Event::Type::kStart, -1, + Arc{0, 0}}, + RoutingSolution::Event{RoutingSolution::Event::Type::kTransit, -1, + Arc{0, 1}}, + }, + }; + const RoutingSolution solution{routes, {4}, {4}}; + std::string solution_str; + + EXPECT_DEATH( + solution_str = solution.SerializeToString(RoutingOutputFormat::kCVRPLIB), + ""); +} + +TEST(RoutingSolutionSerializerTest, SolutionToCvrplibDepot0Dimacs) { + // Section 7 from + // http://dimacs.rutgers.edu/files/6916/3848/0327/CVRP_Competition_Rules.pdf + const std::vector solution{0, 1, 4, 0, -1, 0, 3, 2, 5, 0, -1}; + const std::string expected_output = "Route #1: 1 4\nRoute #2: 3 2 5\n"; + + EXPECT_EQ(RoutingSolution::FromSplitRoutes( + RoutingSolution::SplitRoutes(solution, -1), 0) + .SerializeToString(RoutingOutputFormat::kCVRPLIB), + expected_output); +} + +TEST(RoutingSolutionSerializerTest, SolutionToCvrplibDepot1Dimacs) { + // Section 7 from + // http://dimacs.rutgers.edu/files/6916/3848/0327/CVRP_Competition_Rules.pdf + const std::vector solution{1, 2, 5, 1, -1, 1, 4, 3, 6, 1, -1}; + const std::string expected_output = "Route #1: 1 4\nRoute #2: 3 2 5\n"; + + EXPECT_EQ(RoutingSolution::FromSplitRoutes( + RoutingSolution::SplitRoutes(solution, -1), 1) + .SerializeToString(RoutingOutputFormat::kCVRPLIB), + expected_output); +} + +TEST(RoutingSolutionSerializerTest, SolutionToCvrplibFile) { + std::string file_name = "/mmemfile/sol_cvrplib"; + RegisteredMutableMemFile registered(file_name); + + const std::vector> solution_vector{{0, 1, 2, 3, 0}, + {0, 4, 5, 6, 0}}; + const std::string expected_output = + "Route #1: 1 2 3\n" + "Route #2: 4 5 6\n" + "Cost 4857"; + + RoutingSolution solution = + RoutingSolution::FromSplitRoutes(solution_vector, 0); + solution.SetTotalCost(4857); + solution.WriteToSolutionFile(RoutingOutputFormat::kCVRPLIB, file_name); + std::string written_solution; + CHECK_OK(file::GetContents(file_name, &written_solution, file::Defaults())); + EXPECT_EQ(written_solution, expected_output); +} + +RoutingSolution MakeTestArcRoutingInstance() { + using Event = RoutingSolution::Event; + using Type = Event::Type; + return {std::vector{ + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kServeArc, 12, Arc{4, 10}, "A1"}, + Event{Type::kServeArc, 21, Arc{10, 8}, "A2"}, + Event{Type::kServeArc, 8, Arc{8, 1}, "A3"}, + Event{Type::kServeArc, 7, Arc{1, 3}, "A4"}, + Event{Type::kServeArc, 2, Arc{3, 0}, "A5"}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kServeArc, 5, Arc{0, 11}, "A6"}, + Event{Type::kServeArc, 14, Arc{5, 6}, "A7"}, + Event{Type::kServeArc, 19, Arc{7, 10}, "A8"}, + Event{Type::kServeArc, 22, Arc{10, 9}, "A9"}, + Event{Type::kServeArc, 4, Arc{9, 0}, "A10"}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kServeArc, 13, Arc{11, 4}, "A11"}, + Event{Type::kServeArc, 9, Arc{2, 3}, "A12"}, + Event{Type::kServeArc, 6, Arc{1, 2}, "A13"}, + Event{Type::kServeArc, 10, Arc{2, 4}, "A14"}, + Event{Type::kServeArc, 11, Arc{4, 5}, "A15"}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kServeArc, 15, Arc{11, 5}, "A16"}, + Event{Type::kServeArc, 16, Arc{6, 7}, "A17"}, + Event{Type::kServeArc, 18, Arc{7, 9}, "A18"}, + Event{Type::kServeArc, 20, Arc{9, 8}, "A19"}, + Event{Type::kServeArc, 1, Arc{1, 0}, "A20"}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kServeArc, 17, Arc{11, 6}, "A21"}, + Event{Type::kServeArc, 3, Arc{6, 0}, "A22"}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + }, + std::vector{5, 5, 5, 5, 2}, + std::vector{76, 60, 86, 53, 41}, + 7, + 6, + 30.84}; +} + +RoutingSolution MakeTestEdgeNodeArcRoutingInstance() { + using Event = RoutingSolution::Event; + using Type = Event::Type; + return {std::vector{ + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kTransit, -1, Arc{0, 4}}, + Event{Type::kServeEdge, 12, Arc{4, 10}, "E1"}, + Event{Type::kServeArc, 21, Arc{10, 8}, "A2"}, + Event{Type::kServeNode, 8, Arc{8, 8}}, + Event{Type::kTransit, -1, Arc{8, 1}}, + Event{Type::kServeEdge, 7, Arc{1, 3}, "E3"}, + Event{Type::kServeArc, 2, Arc{3, 0}, "A4"}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kServeEdge, 5, Arc{0, 11}, "E5"}, + Event{Type::kTransit, -1, Arc{11, 5}}, + Event{Type::kServeEdge, 14, Arc{5, 6}, "E6"}, + Event{Type::kTransit, -1, Arc{6, 7}}, + Event{Type::kServeEdge, 19, Arc{7, 10}, "E7"}, + Event{Type::kServeEdge, 22, Arc{10, 9}, "E8"}, + Event{Type::kServeEdge, 4, Arc{9, 0}, "E9"}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kTransit, -1, Arc{0, 11}}, + Event{Type::kServeArc, 13, Arc{11, 4}, "A10"}, + Event{Type::kTransit, -1, Arc{4, 2}}, + Event{Type::kServeEdge, 9, Arc{2, 3}, "E11"}, + Event{Type::kTransit, -1, Arc{3, 1}}, + Event{Type::kServeArc, 6, Arc{1, 2}, "A12"}, + Event{Type::kServeNode, 10, Arc{2, 2}}, + Event{Type::kTransit, -1, Arc{2, 4}}, + Event{Type::kServeEdge, 11, Arc{4, 5}, "E13"}, + Event{Type::kTransit, -1, Arc{5, 0}}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kTransit, -1, Arc{0, 11}}, + Event{Type::kServeNode, 15, Arc{11, 11}}, + Event{Type::kServeEdge, 16, Arc{11, 7}, "E14"}, + Event{Type::kServeEdge, 18, Arc{7, 9}, "E15"}, + Event{Type::kServeEdge, 20, Arc{9, 8}, "E16"}, + Event{Type::kTransit, -1, Arc{8, 1}}, + Event{Type::kServeEdge, 1, Arc{1, 0}, "E17"}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + RoutingSolution::Route{ + Event{Type::kStart, 0, Arc{0, 0}}, + Event{Type::kTransit, -1, Arc{0, 11}}, + Event{Type::kServeNode, 17, Arc{11, 11}}, + Event{Type::kTransit, -1, Arc{11, 6}}, + Event{Type::kServeNode, 3, Arc{6, 6}}, + Event{Type::kTransit, -1, Arc{6, 0}}, + Event{Type::kEnd, 0, Arc{0, 0}}, + }, + }, + std::vector{5, 5, 5, 5, 2}, + std::vector{76, 60, 86, 53, 41}, + 7, + 6, + 30.84}; +} + +TEST(RoutingSolutionSerializerTest, CarpSolutionToCarplib) { + // http://dimacs.rutgers.edu/programs/challenge/vrp/carp/ + const std::string expected_solution_output = + "0 1 1 5 76 7 (D 0,1,1) (S 12,5,11) (S 21,11,9) (S 8,9,2) (S 7,2,4) " + "(S 2,4,1) (D 0,1,1)\n" + "0 1 2 5 60 7 (D 0,1,1) (S 5,1,12) (S 14,6,7) (S 19,8,11) (S 22,11,10) " + "(S 4,10,1) (D 0,1,1)\n" + "0 1 3 5 86 7 (D 0,1,1) (S 13,12,5) (S 9,3,4) (S 6,2,3) (S 10,3,5) " + "(S 11,5,6) (D 0,1,1)\n" + "0 1 4 5 53 7 (D 0,1,1) (S 15,12,6) (S 16,7,8) (S 18,8,10) (S 20,10,9) " + "(S 1,2,1) (D 0,1,1)\n" + "0 1 5 2 41 4 (D 0,1,1) (S 17,12,7) (S 3,7,1) (D 0,1,1)"; + + const RoutingSolution solution = MakeTestArcRoutingInstance(); + EXPECT_EQ(solution.SerializeToString(RoutingOutputFormat::kCARPLIB), + expected_solution_output); +} + +TEST(RoutingSolutionSerializerTest, CarpSolutionToCarplibFile) { + std::string file_name = "/mmemfile/sol_carp_carplib"; + RegisteredMutableMemFile registered(file_name); + + RoutingSolution solution = MakeTestArcRoutingInstance(); + const std::string expected_output = + "7\n" + "5\n" + "30.840000\n" + "0 1 1 5 76 7 (D 0,1,1) (S 12,5,11) (S 21,11,9) (S 8,9,2) (S 7,2,4) " + "(S 2,4,1) (D 0,1,1)\n" + "0 1 2 5 60 7 (D 0,1,1) (S 5,1,12) (S 14,6,7) (S 19,8,11) (S 22,11,10) " + "(S 4,10,1) (D 0,1,1)\n" + "0 1 3 5 86 7 (D 0,1,1) (S 13,12,5) (S 9,3,4) (S 6,2,3) (S 10,3,5) " + "(S 11,5,6) (D 0,1,1)\n" + "0 1 4 5 53 7 (D 0,1,1) (S 15,12,6) (S 16,7,8) (S 18,8,10) (S 20,10,9) " + "(S 1,2,1) (D 0,1,1)\n" + "0 1 5 2 41 4 (D 0,1,1) (S 17,12,7) (S 3,7,1) (D 0,1,1)"; + solution.SetName("Test name"); + solution.WriteToSolutionFile(RoutingOutputFormat::kCARPLIB, file_name); + + std::string written_solution; + CHECK_OK(file::GetContents(file_name, &written_solution, file::Defaults())); + EXPECT_EQ(written_solution, expected_output); +} + +TEST(RoutingSolutionSerializerTest, NearpSolutionToCarplib) { + const std::string expected_solution_output = + "0 1 1 5 76 7 (D 0,1,1) (S 12,5,11) (S 21,11,9) (S 8,9,9) (S 7,2,4) " + "(S 2,4,1) (D 0,1,1)\n" + "0 1 2 5 60 7 (D 0,1,1) (S 5,1,12) (S 14,6,7) (S 19,8,11) (S 22,11,10) " + "(S 4,10,1) (D 0,1,1)\n" + "0 1 3 5 86 7 (D 0,1,1) (S 13,12,5) (S 9,3,4) (S 6,2,3) (S 10,3,3) " + "(S 11,5,6) (D 0,1,1)\n" + "0 1 4 5 53 7 (D 0,1,1) (S 15,12,12) (S 16,12,8) (S 18,8,10) (S 20,10,9) " + "(S 1,2,1) (D 0,1,1)\n" + "0 1 5 2 41 4 (D 0,1,1) (S 17,12,12) (S 3,7,7) (D 0,1,1)"; + + const RoutingSolution solution = MakeTestEdgeNodeArcRoutingInstance(); + EXPECT_EQ(solution.SerializeToString(RoutingOutputFormat::kCARPLIB), + expected_solution_output); +} + +TEST(RoutingSolutionSerializerTest, NearpSolutionToCarplibFile) { + std::string file_name = "/mmemfile/sol_nearp_carplib"; + RegisteredMutableMemFile registered(file_name); + + RoutingSolution solution = MakeTestEdgeNodeArcRoutingInstance(); + const std::string expected_output = + "7\n" + "5\n" + "30.840000\n" + "0 1 1 5 76 7 (D 0,1,1) (S 12,5,11) (S 21,11,9) (S 8,9,9) (S 7,2,4) " + "(S 2,4,1) (D 0,1,1)\n" + "0 1 2 5 60 7 (D 0,1,1) (S 5,1,12) (S 14,6,7) (S 19,8,11) (S 22,11,10) " + "(S 4,10,1) (D 0,1,1)\n" + "0 1 3 5 86 7 (D 0,1,1) (S 13,12,5) (S 9,3,4) (S 6,2,3) (S 10,3,3) " + "(S 11,5,6) (D 0,1,1)\n" + "0 1 4 5 53 7 (D 0,1,1) (S 15,12,12) (S 16,12,8) (S 18,8,10) (S 20,10,9) " + "(S 1,2,1) (D 0,1,1)\n" + "0 1 5 2 41 4 (D 0,1,1) (S 17,12,12) (S 3,7,7) (D 0,1,1)"; + solution.SetName("Test name"); + solution.WriteToSolutionFile(RoutingOutputFormat::kCARPLIB, file_name); + + std::string written_solution; + CHECK_OK(file::GetContents(file_name, &written_solution, file::Defaults())); + EXPECT_EQ(written_solution, expected_output); +} + +TEST(RoutingSolutionSerializerTest, CarpSolutionToNearplib) { + const std::string expected_solution_output = + "Route #1 : 1 5-A1-11-A2-9-A3-2-A4-4-A5-1\n" + "Route #2 : 1-A6-12 6-A7-7 8-A8-11-A9-10-A10-1\n" + "Route #3 : 1 12-A11-5 3-A12-4 2-A13-3-A14-5-A15-6 1\n" + "Route #4 : 1 12-A16-6 7-A17-8-A18-10-A19-9 2-A20-1\n" + "Route #5 : 1 12-A21-7-A22-1"; + + const RoutingSolution solution = MakeTestArcRoutingInstance(); + EXPECT_EQ(solution.SerializeToString(RoutingOutputFormat::kNEARPLIB), + expected_solution_output); +} + +TEST(RoutingSolutionSerializerTest, CarpSolutionToNearplibFile) { + std::string file_name = "/mmemfile/sol_carp_nearplib"; + RegisteredMutableMemFile registered(file_name); + + RoutingSolution solution = MakeTestArcRoutingInstance(); + const std::string date = + absl::FormatTime("%B %d, %E4Y", absl::Now(), absl::LocalTimeZone()); + const std::string expected_output = + "Instance name: Test name\n" + "Authors: DIMACS CARP\n" + "Date: " + + date + + "\n" + "Reference: OR-Tools\n" + "Solution\n" + "Route #1 : 1 5-A1-11-A2-9-A3-2-A4-4-A5-1\n" + "Route #2 : 1-A6-12 6-A7-7 8-A8-11-A9-10-A10-1\n" + "Route #3 : 1 12-A11-5 3-A12-4 2-A13-3-A14-5-A15-6 1\n" + "Route #4 : 1 12-A16-6 7-A17-8-A18-10-A19-9 2-A20-1\n" + "Route #5 : 1 12-A21-7-A22-1\n" + "Total cost: 7"; + solution.SetName("Test name"); + solution.SetAuthors("DIMACS CARP"); + solution.WriteToSolutionFile(RoutingOutputFormat::kNEARPLIB, file_name); + + std::string written_solution; + CHECK_OK(file::GetContents(file_name, &written_solution, file::Defaults())); + EXPECT_EQ(written_solution, expected_output); +} + +TEST(RoutingSolutionSerializerTest, NearpSolutionToNearplib) { + const std::string expected_solution_output = + "Route #1 : 1 5-E1-11-A2-9 N9 2-E3-4-A4-1\n" + "Route #2 : 1-E5-12 6-E6-7 8-E7-11-E8-10-E9-1\n" + "Route #3 : 1 12-A10-5 3-E11-4 2-A12-3 N3 5-E13-6 1\n" + "Route #4 : 1 N12-E14-8-E15-10-E16-9 2-E17-1\n" + "Route #5 : 1 N12 N7 1"; + // TODO(user): the following output would be ideal (because shorter). It + // would be achieved by implementing the relevant TODO in + // SerializeToNEARPLIBString. + // Route #1 : 1 5-E1-11-A2-N9 2-E3-4-A4-1 + // Route #3 : 1 12-A10-5 3-E11-4 2-A12-N3 5-E13-6 1 + + const RoutingSolution solution = MakeTestEdgeNodeArcRoutingInstance(); + EXPECT_EQ(solution.SerializeToString(RoutingOutputFormat::kNEARPLIB), + expected_solution_output); +} + +TEST(RoutingSolutionSerializerTest, NearpSolutionToNearplibFile) { + std::string file_name = "/mmemfile/sol_nearp_nearplib"; + RegisteredMutableMemFile registered(file_name); + + RoutingSolution solution = MakeTestEdgeNodeArcRoutingInstance(); + const std::string date = + absl::FormatTime("%B %d, %E4Y", absl::Now(), absl::LocalTimeZone()); + const std::string expected_output = + "Instance name: Test name\n" + "Authors: Based on DIMACS CARP\n" + "Date: " + + date + + "\n" + "Reference: OR-Tools\n" + "Solution\n" + "Route #1 : 1 5-E1-11-A2-9 N9 2-E3-4-A4-1\n" + "Route #2 : 1-E5-12 6-E6-7 8-E7-11-E8-10-E9-1\n" + "Route #3 : 1 12-A10-5 3-E11-4 2-A12-3 N3 5-E13-6 1\n" + "Route #4 : 1 N12-E14-8-E15-10-E16-9 2-E17-1\n" + "Route #5 : 1 N12 N7 1\n" + "Total cost: 7"; + solution.SetName("Test name"); + solution.SetAuthors("Based on DIMACS CARP"); + solution.WriteToSolutionFile(RoutingOutputFormat::kNEARPLIB, file_name); + + std::string written_solution; + CHECK_OK(file::GetContents(file_name, &written_solution, file::Defaults())); + EXPECT_EQ(written_solution, expected_output); +} + +TEST(RoutingSolutionSerializerTest, FormatStatisticAsTsplib) { + EXPECT_EQ(FormatStatistic("STAT", 4, RoutingOutputFormat::kTSPLIB), + "STAT = 4"); +} + +TEST(RoutingSolutionSerializerTest, FormatStatisticAsCvrplib) { + EXPECT_EQ(FormatStatistic("STAT", 4, RoutingOutputFormat::kCVRPLIB), + "STAT 4"); +} + +TEST(RoutingSolutionSerializerTest, FormatStatisticAsCarplib) { + EXPECT_EQ(FormatStatistic("STAT", 4, RoutingOutputFormat::kCARPLIB), "4"); +} + +TEST(RoutingSolutionSerializerTest, FormatStatisticAsNearplib) { + EXPECT_EQ(FormatStatistic("STAT", 4, RoutingOutputFormat::kNEARPLIB), + "STAT : 4"); +} + +TEST(RoutingSolutionSerializerTest, FormatStatisticAsTsplibLongPrecision) { + EXPECT_EQ(FormatStatistic("STAT", 591.556557, RoutingOutputFormat::kTSPLIB), + "STAT = 591.556557"); +} + +TEST(RoutingSolutionSerializerTest, FormatStatisticAsCvrplibLongPrecision) { + EXPECT_EQ(FormatStatistic("STAT", 591.556557, RoutingOutputFormat::kCVRPLIB), + "STAT 591.556557"); +} + +TEST(RoutingSolutionSerializerTest, FormatStatisticAsCarplibLongPrecision) { + EXPECT_EQ(FormatStatistic("STAT", 591.556557, RoutingOutputFormat::kCARPLIB), + "591.556557"); +} + +TEST(RoutingSolutionSerializerTest, FormatStatisticAsNearplibLongPrecision) { + EXPECT_EQ(FormatStatistic("STAT", 591.556557, RoutingOutputFormat::kNEARPLIB), + "STAT : 591.556557"); +} +} // namespace +} // namespace operations_research diff --git a/ortools/routing/tsplib_parser.cc b/ortools/routing/tsplib_parser.cc new file mode 100644 index 0000000000..664fe245bc --- /dev/null +++ b/ortools/routing/tsplib_parser.cc @@ -0,0 +1,855 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/tsplib_parser.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "ortools/base/check.h" +#include "ortools/base/map_util.h" +#include "ortools/base/numbers.h" +#include "ortools/base/path.h" +#include "ortools/base/strtoint.h" +#include "ortools/base/zipfile.h" +#include "ortools/util/filelineiter.h" +#include "re2/re2.h" + +namespace operations_research { +namespace { + +// ----- Distances ----- +// As defined by the TSPLIB95 doc + +static int64_t ATTDistance(const Coordinates3& from, + const Coordinates3& to) { + const double xd = from.x - to.x; + const double yd = from.y - to.y; + const double euc = sqrt((xd * xd + yd * yd) / 10.0); + int64_t distance = std::round(euc); + if (distance < euc) ++distance; + return distance; +} + +static double DoubleEuc2DDistance(const Coordinates3& from, + const Coordinates3& to) { + const double xd = from.x - to.x; + const double yd = from.y - to.y; + return sqrt(xd * xd + yd * yd); +} + +static int64_t Euc2DDistance(const Coordinates3& from, + const Coordinates3& to) { + return std::round(DoubleEuc2DDistance(from, to)); +} + +static int64_t Euc3DDistance(const Coordinates3& from, + const Coordinates3& to) { + const double xd = from.x - to.x; + const double yd = from.y - to.y; + const double zd = from.z - to.z; + const double euc = sqrt(xd * xd + yd * yd + zd * zd); + return std::round(euc); +} + +static int64_t Ceil2DDistance(const Coordinates3& from, + const Coordinates3& to) { + return static_cast(ceil(DoubleEuc2DDistance(from, to))); +} + +static double ToRad(double x) { + static const double kPi = 3.141592; + const int64_t deg = static_cast(x); + const double min = x - deg; + return kPi * (deg + 5.0 * min / 3.0) / 180.0; +} + +static int64_t GeoDistance(const Coordinates3& from, + const Coordinates3& to) { + static const double kRadius = 6378.388; + const double q1 = cos(ToRad(from.y) - ToRad(to.y)); + const double q2 = cos(ToRad(from.x) - ToRad(to.x)); + const double q3 = cos(ToRad(from.x) + ToRad(to.x)); + return static_cast( + kRadius * acos(0.5 * ((1.0 + q1) * q2 - (1.0 - q1) * q3)) + 1.0); +} + +static int64_t GeoMDistance(const Coordinates3& from, + const Coordinates3& to) { + static const double kPi = 3.14159265358979323846264; + static const double kRadius = 6378388.0; + const double from_lat = kPi * from.x / 180.0; + const double to_lat = kPi * to.x / 180.0; + const double from_lng = kPi * from.y / 180.0; + const double to_lng = kPi * to.y / 180.0; + const double q1 = cos(to_lat) * sin(from_lng - to_lng); + const double q3 = sin((from_lng - to_lng) / 2.0); + const double q4 = cos((from_lng - to_lng) / 2.0); + const double q2 = + sin(from_lat + to_lat) * q3 * q3 - sin(from_lat - to_lat) * q4 * q4; + const double q5 = + cos(from_lat - to_lat) * q4 * q4 - cos(from_lat + to_lat) * q3 * q3; + return static_cast(kRadius * atan2(sqrt(q1 * q1 + q2 * q2), q5) + + 1.0); +} + +static int64_t Man2DDistance(const Coordinates3& from, + const Coordinates3& to) { + const double xd = fabs(from.x - to.x); + const double yd = fabs(from.y - to.y); + return std::round(xd + yd); +} + +static int64_t Man3DDistance(const Coordinates3& from, + const Coordinates3& to) { + const double xd = fabs(from.x - to.x); + const double yd = fabs(from.y - to.y); + const double zd = fabs(from.z - to.z); + return std::round(xd + yd + zd); +} + +static int64_t Max2DDistance(const Coordinates3& from, + const Coordinates3& to) { + const double xd = fabs(from.x - to.x); + const double yd = fabs(from.y - to.y); + return std::round(std::max(xd, yd)); +} + +static int64_t Max3DDistance(const Coordinates3& from, + const Coordinates3& to) { + const double xd = fabs(from.x - to.x); + const double yd = fabs(from.y - to.y); + const double zd = fabs(from.z - to.z); + return std::round(std::max(xd, std::max(yd, zd))); +} + +std::shared_ptr OpenZipArchiveIfItExists( + const std::string& file_name) { + const absl::string_view archive_name = file::Dirname(file_name); + if (file::Extension(archive_name) == "zip") { + return zipfile::OpenZipArchive(archive_name); + } else { + return nullptr; + } +} + +} // namespace + +TspLibParser::TspLibParser() + : size_(0), + capacity_(kint64max), + max_distance_(kint64max), + distance_function_(nullptr), + explicit_costs_(), + depot_(0), + section_(UNDEFINED_SECTION), + type_(Types::UNDEFINED_TYPE), + edge_weight_type_(UNDEFINED_EDGE_WEIGHT_TYPE), + edge_weight_format_(UNDEFINED_EDGE_WEIGHT_FORMAT), + edge_row_(0), + edge_column_(0), + to_read_(0) {} + +bool TspLibParser::LoadFile(const std::string& file_name) { + std::shared_ptr zip_archive( + OpenZipArchiveIfItExists(file_name)); + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + ProcessNewLine(line); + } + FinalizeEdgeWeights(); + return true; +} + +int TspLibParser::SizeFromFile(const std::string& file_name) const { + std::shared_ptr zip_archive( + OpenZipArchiveIfItExists(file_name)); + int size = 0; + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + if (RE2::PartialMatch(line, "DIMENSION\\s*:\\s*(\\d+)", &size)) { + break; + } + } + return size; +} + +void TspLibParser::ParseExplicitFullMatrix( + const std::vector& words) { + CHECK_LT(edge_row_, size_); + if (type_ == Types::SOP && to_read_ == size_ * size_) { + // Matrix size is present in SOP which is redundant with dimension and must + // not be confused with the first cell of the matrix. + return; + } + for (const std::string& word : words) { + SetExplicitCost(edge_row_, edge_column_, atoi64(word)); + ++edge_column_; + if (edge_column_ >= size_) { + edge_column_ = 0; + ++edge_row_; + } + --to_read_; + } +} + +void TspLibParser::ParseExplicitUpperRow( + const std::vector& words) { + CHECK_LT(edge_row_, size_); + for (const std::string& word : words) { + SetExplicitCost(edge_row_, edge_column_, atoi64(word)); + SetExplicitCost(edge_column_, edge_row_, atoi64(word)); + ++edge_column_; + if (edge_column_ >= size_) { + ++edge_row_; + SetExplicitCost(edge_row_, edge_row_, 0); + edge_column_ = edge_row_ + 1; + } + --to_read_; + } +} + +void TspLibParser::ParseExplicitLowerRow( + const std::vector& words) { + CHECK_LT(edge_row_, size_); + for (const std::string& word : words) { + SetExplicitCost(edge_row_, edge_column_, atoi64(word)); + SetExplicitCost(edge_column_, edge_row_, atoi64(word)); + ++edge_column_; + if (edge_column_ >= edge_row_) { + SetExplicitCost(edge_column_, edge_column_, 0); + edge_column_ = 0; + ++edge_row_; + } + --to_read_; + } +} + +void TspLibParser::ParseExplicitUpperDiagRow( + const std::vector& words) { + CHECK_LT(edge_row_, size_); + for (const std::string& word : words) { + SetExplicitCost(edge_row_, edge_column_, atoi64(word)); + SetExplicitCost(edge_column_, edge_row_, atoi64(word)); + ++edge_column_; + if (edge_column_ >= size_) { + ++edge_row_; + edge_column_ = edge_row_; + } + --to_read_; + } +} + +void TspLibParser::ParseExplicitLowerDiagRow( + const std::vector& words) { + CHECK_LT(edge_row_, size_); + for (const std::string& word : words) { + SetExplicitCost(edge_row_, edge_column_, atoi64(word)); + SetExplicitCost(edge_column_, edge_row_, atoi64(word)); + ++edge_column_; + if (edge_column_ > edge_row_) { + edge_column_ = 0; + ++edge_row_; + } + --to_read_; + } +} + +void TspLibParser::ParseNodeCoord(const std::vector& words) { + CHECK_LE(3, words.size()) << words[0]; + CHECK_GE(4, words.size()) << words[4]; + const int node(atoi32(words[0]) - 1); + coords_[node].x = strings::ParseLeadingDoubleValue(words[1].c_str(), 0); + coords_[node].y = strings::ParseLeadingDoubleValue(words[2].c_str(), 0); + if (4 == words.size()) { + coords_[node].z = strings::ParseLeadingDoubleValue(words[3].c_str(), 0); + } else { + coords_[node].z = 0; + } + --to_read_; +} + +void TspLibParser::SetUpEdgeWeightSection() { + edge_row_ = 0; + edge_column_ = 0; + switch (edge_weight_format_) { + case FULL_MATRIX: + to_read_ = size_ * size_; + break; + case LOWER_COL: + case UPPER_ROW: + SetExplicitCost(0, 0, 0); + ++edge_column_; + to_read_ = ((size_ - 1) * size_) / 2; + break; + case UPPER_COL: + case LOWER_ROW: + SetExplicitCost(0, 0, 0); + ++edge_row_; + to_read_ = ((size_ - 1) * size_) / 2; + break; + case LOWER_DIAG_COL: + case UPPER_DIAG_ROW: + to_read_ = ((size_ + 1) * size_) / 2; + break; + case UPPER_DIAG_COL: + case LOWER_DIAG_ROW: + to_read_ = ((size_ + 1) * size_) / 2; + break; + default: + LOG(WARNING) << "Unknown EDGE_WEIGHT_FORMAT: " << edge_weight_format_; + } +} + +// Fill in the edge weight matrix. +void TspLibParser::FinalizeEdgeWeights() { + distance_function_ = nullptr; + if (type_ == Types::HCP) { + VLOG(2) << "No edge weights"; + return; + } + VLOG(2) << "Edge weight type: " << edge_weight_type_; + switch (edge_weight_type_) { + case EXPLICIT: + distance_function_ = [this](int from, int to) { + return explicit_costs_[from * size_ + to]; + }; + break; + case EUC_2D: + distance_function_ = [this](int from, int to) { + return Euc2DDistance(coords_[from], coords_[to]); + }; + break; + case EUC_3D: + distance_function_ = [this](int from, int to) { + return Euc3DDistance(coords_[from], coords_[to]); + }; + break; + case MAX_2D: + distance_function_ = [this](int from, int to) { + return Max2DDistance(coords_[from], coords_[to]); + }; + break; + case MAX_3D: + distance_function_ = [this](int from, int to) { + return Max3DDistance(coords_[from], coords_[to]); + }; + break; + case MAN_2D: + distance_function_ = [this](int from, int to) { + return Man2DDistance(coords_[from], coords_[to]); + }; + break; + case MAN_3D: + distance_function_ = [this](int from, int to) { + return Man3DDistance(coords_[from], coords_[to]); + }; + break; + case CEIL_2D: + distance_function_ = [this](int from, int to) { + return Ceil2DDistance(coords_[from], coords_[to]); + }; + break; + case GEO: + distance_function_ = [this](int from, int to) { + return GeoDistance(coords_[from], coords_[to]); + }; + break; + case GEOM: + distance_function_ = [this](int from, int to) { + return GeoMDistance(coords_[from], coords_[to]); + }; + break; + case ATT: + distance_function_ = [this](int from, int to) { + return ATTDistance(coords_[from], coords_[to]); + }; + break; + case XRAY1: + LOG(WARNING) << "XRAY1 not supported for EDGE_WEIGHT_TYPE"; + break; + case XRAY2: + LOG(WARNING) << "XRAY2 not supported for EDGE_WEIGHT_TYPE"; + break; + case SPECIAL: + LOG(WARNING) << "SPECIAL not supported for EDGE_WEIGHT_TYPE"; + break; + default: + LOG(WARNING) << "Unknown EDGE_WEIGHT_TYPE: " << edge_weight_type_; + } +} + +void TspLibParser::ParseSections(const std::vector& words) { + const int words_size = words.size(); + CHECK_GT(words_size, 0); + if (!gtl::FindCopy(*kSections, words[0], §ion_)) { + LOG(WARNING) << "Unknown section: " << words[0]; + return; + } + const std::string& last_word = words[words_size - 1]; + switch (section_) { + case NAME: { + name_ = absl::StrJoin(words.begin() + 1, words.end(), " "); + break; + } + case TYPE: { + CHECK_LE(2, words.size()); + const std::string& type = words[1]; + if (!gtl::FindCopy(*kTypes, type, &type_)) { + LOG(WARNING) << "Unknown TYPE: " << type; + } + break; + } + case COMMENT: { + if (!comments_.empty()) { + comments_ += "\n"; + } + comments_ += absl::StrJoin(words.begin() + 1, words.end(), " "); + break; + } + case DIMENSION: { + size_ = atoi32(last_word); + coords_.resize(size_); + break; + } + case DISTANCE: { + // This is non-standard but is supported as an upper bound on the length + // of each route. + max_distance_ = atoi64(last_word); + break; + } + case CAPACITY: { + capacity_ = atoi64(last_word); + break; + } + case EDGE_DATA_FORMAT: { + CHECK(Types::HCP == type_); + if (!gtl::FindCopy(*kEdgeDataFormats, last_word, &edge_data_format_)) { + LOG(WARNING) << "Unknown EDGE_DATA_FORMAT: " << last_word; + } + break; + } + case EDGE_DATA_SECTION: { + CHECK(Types::HCP == type_); + edges_.resize(size_); + to_read_ = 1; + break; + } + case EDGE_WEIGHT_TYPE: { + if (!gtl::FindCopy(*kEdgeWeightTypes, last_word, &edge_weight_type_)) { + // Some data sets invert EDGE_WEIGHT_TYPE and EDGE_WEIGHT_FORMAT. + LOG(WARNING) << "Unknown EDGE_WEIGHT_TYPE: " << last_word; + LOG(WARNING) << "Trying in EDGE_WEIGHT_FORMAT formats"; + if (!gtl::FindCopy(*kEdgeWeightFormats, last_word, + &edge_weight_format_)) { + LOG(WARNING) << "Unknown EDGE_WEIGHT_FORMAT: " << last_word; + } + } + break; + } + case EDGE_WEIGHT_FORMAT: { + if (!gtl::FindCopy(*kEdgeWeightFormats, last_word, + &edge_weight_format_)) { + // Some data sets invert EDGE_WEIGHT_TYPE and EDGE_WEIGHT_FORMAT. + LOG(WARNING) << "Unknown EDGE_WEIGHT_FORMAT: " << last_word; + LOG(WARNING) << "Trying in EDGE_WEIGHT_TYPE types"; + if (!gtl::FindCopy(*kEdgeWeightTypes, last_word, &edge_weight_type_)) { + LOG(WARNING) << "Unknown EDGE_WEIGHT_TYPE: " << last_word; + } + } + break; + } + case EDGE_WEIGHT_SECTION: { + SetUpEdgeWeightSection(); + break; + } + case FIXED_EDGES_SECTION: { + to_read_ = kint64max; + break; + } + case NODE_COORD_TYPE: { + break; + } + case DISPLAY_DATA_TYPE: { + break; + } + case DISPLAY_DATA_SECTION: { + to_read_ = size_; + break; + } + case NODE_COORD_SECTION: { + to_read_ = size_; + break; + } + case DEPOT_SECTION: { + to_read_ = kint64max; + break; + } + case DEMAND_SECTION: { + demands_.resize(size_, 0); + to_read_ = size_; + break; + } + case END_OF_FILE: { + break; + } + default: { + LOG(WARNING) << "Unknown section: " << words[0]; + } + } +} + +void TspLibParser::ProcessNewLine(const std::string& line) { + const std::vector words = + absl::StrSplit(line, absl::ByAnyChar(" :\t"), absl::SkipEmpty()); + if (!words.empty()) { + // New section detected. + if (kSections->contains(words[0])) { + to_read_ = 0; + } + if (to_read_ > 0) { + switch (section_) { + case EDGE_DATA_SECTION: { + CHECK(!words.empty()); + switch (edge_data_format_) { + case EDGE_LIST: { + if (words[0] == "-1") { + CHECK_EQ(words.size(), 1); + // Remove duplicate edges + for (auto& edges : edges_) { + std::sort(edges.begin(), edges.end()); + const auto it = std::unique(edges.begin(), edges.end()); + edges.resize(std::distance(edges.begin(), it)); + } + to_read_ = 0; + } else { + CHECK_EQ(words.size(), 2); + const int from = atoi64(words[0]) - 1; + const int to = atoi64(words[1]) - 1; + edges_[std::min(from, to)].push_back(std::max(from, to)); + } + break; + } + case ADJ_LIST: { + const int from = atoi64(words[0]) - 1; + for (int i = 1; i < words.size(); ++i) { + const int to = atoi64(words[i]) - 1; + if (to != -2) { + edges_[std::min(from, to)].push_back(std::max(from, to)); + } else { + CHECK_EQ(i, words.size() - 1); + } + } + if (atoi64(words.back()) != -1) { + LOG(WARNING) << "Missing -1 at the end of ADJ_LIST"; + } + break; + } + default: + LOG(WARNING) << "Unknown EDGE_DATA_FORMAT: " << edge_data_format_; + } + break; + } + case EDGE_WEIGHT_SECTION: { + switch (edge_weight_format_) { + case FULL_MATRIX: + ParseExplicitFullMatrix(words); + break; + case UPPER_ROW: + case LOWER_COL: + ParseExplicitUpperRow(words); + break; + case LOWER_ROW: + case UPPER_COL: + ParseExplicitLowerRow(words); + break; + case UPPER_DIAG_ROW: + case LOWER_DIAG_COL: + ParseExplicitUpperDiagRow(words); + break; + case LOWER_DIAG_ROW: + case UPPER_DIAG_COL: + ParseExplicitLowerDiagRow(words); + break; + default: + LOG(WARNING) << "Unknown EDGE_WEIGHT_FORMAT: " + << edge_weight_format_; + } + break; + } + case FIXED_EDGES_SECTION: { + if (words.size() == 1) { + CHECK_EQ(-1, atoi64(words[0])); + to_read_ = 0; + } else { + CHECK_EQ(2, words.size()); + fixed_edges_.insert( + std::make_pair(atoi64(words[0]) - 1, atoi64(words[1]) - 1)); + } + break; + } + case NODE_COORD_SECTION: { + ParseNodeCoord(words); + break; + } + case DEPOT_SECTION: { + if (words.size() == 1) { + const int depot(atoi64(words[0]) - 1); + if (depot >= 0) { + VLOG(2) << "Depot: " << depot; + depot_ = depot; + } else { + to_read_ = 0; + } + } else if (words.size() >= 2) { + CHECK_GE(3, words.size()) << words[3]; + const int depot(size_ - 1); + VLOG(2) << "Depot: " << depot; + depot_ = depot; + coords_[depot].x = + strings::ParseLeadingDoubleValue(words[0].c_str(), 0); + coords_[depot].y = + strings::ParseLeadingDoubleValue(words[1].c_str(), 0); + if (3 == words.size()) { + coords_[depot].z = + strings::ParseLeadingDoubleValue(words[2].c_str(), 0); + } else { + coords_[depot].z = 0; + } + } + break; + } + case DEMAND_SECTION: { + const int64_t node = atoi64(words[0]) - 1; + demands_[node] = atoi64(words[1]); + --to_read_; + break; + } + case DISPLAY_DATA_SECTION: { + ParseNodeCoord(words); + break; + } + default: { + LOG(ERROR) << "Reading data outside section"; + } + } + } else { + ParseSections(words); + } + } +} + +std::string TspLibParser::BuildTourFromRoutes( + const std::vector>& routes) const { + std::string tours = absl::StrCat( + "NAME : ", name_, "\nCOMMENT :\nTYPE : TOUR\nDIMENSION : ", size(), + "\nTOUR_SECTION\n"); + for (const auto& route : routes) { + for (const int node : route) { + absl::StrAppend(&tours, node + 1, "\n"); + } + absl::StrAppend(&tours, "-1\n"); + } + return absl::StrCat(tours, "EOF"); +} + +const absl::flat_hash_map* const + TspLibParser::kSections = + new absl::flat_hash_map( + {{"NAME", NAME}, + {"TYPE", TYPE}, + {"COMMENT", COMMENT}, + {"DIMENSION", DIMENSION}, + {"DISTANCE", DISTANCE}, + {"CAPACITY", CAPACITY}, + {"EDGE_DATA_FORMAT", EDGE_DATA_FORMAT}, + {"EDGE_DATA_SECTION", EDGE_DATA_SECTION}, + {"EDGE_WEIGHT_TYPE", EDGE_WEIGHT_TYPE}, + {"EDGE_WEIGHT_FORMAT", EDGE_WEIGHT_FORMAT}, + {"EDGE_WEIGHT_SECTION", EDGE_WEIGHT_SECTION}, + {"FIXED_EDGES_SECTION", FIXED_EDGES_SECTION}, + {"FIXED_EDGES", FIXED_EDGES_SECTION}, + {"DISPLAY_DATA_SECTION", DISPLAY_DATA_SECTION}, + {"NODE_COORD_TYPE", NODE_COORD_TYPE}, + {"DISPLAY_DATA_TYPE", DISPLAY_DATA_TYPE}, + {"NODE_COORD_SECTION", NODE_COORD_SECTION}, + {"DEPOT_SECTION", DEPOT_SECTION}, + {"DEMAND_SECTION", DEMAND_SECTION}, + {"EOF", END_OF_FILE}}); + +const absl::flat_hash_map* const + TspLibParser::kTypes = + new absl::flat_hash_map( + {{"TSP", Types::TSP}, + {"ATSP", Types::ATSP}, + {"SOP", Types::SOP}, + {"HCP", Types::HCP}, + {"CVRP", Types::CVRP}, + {"TOUR", Types::TOUR}}); + +const absl::flat_hash_map* const + TspLibParser::kEdgeDataFormats = + new absl::flat_hash_map( + {{"EDGE_LIST", EDGE_LIST}, {"ADJ_LIST", ADJ_LIST}}); + +const absl::flat_hash_map* const + TspLibParser::kEdgeWeightTypes = + new absl::flat_hash_map( + {{"EXPLICIT", EXPLICIT}, + {"EUC_2D", EUC_2D}, + {"EUC_3D", EUC_3D}, + {"MAX_2D", MAX_2D}, + {"MAX_3D", MAX_3D}, + {"MAN_2D", MAN_2D}, + {"MAN_3D", MAN_3D}, + {"CEIL_2D", CEIL_2D}, + {"GEO", GEO}, + {"GEOM", GEOM}, + {"ATT", ATT}, + {"XRAY1", XRAY1}, + {"XRAY2", XRAY2}, + {"SPECIAL", SPECIAL}}); + +const absl::flat_hash_map* const + TspLibParser::kEdgeWeightFormats = + new absl::flat_hash_map( + {{"FUNCTION", FUNCTION}, + {"FULL_MATRIX", FULL_MATRIX}, + {"UPPER_ROW", UPPER_ROW}, + {"LOWER_ROW", LOWER_ROW}, + {"UPPER_DIAG_ROW", UPPER_DIAG_ROW}, + {"LOWER_DIAG_ROW", LOWER_DIAG_ROW}, + {"UPPER_COL", UPPER_COL}, + {"LOWER_COL", LOWER_COL}, + {"UPPER_DIAG_COL", UPPER_DIAG_COL}, + {"LOWER_DIAG_COL", LOWER_DIAG_COL}}); + +TspLibTourParser::TspLibTourParser() : section_(UNDEFINED_SECTION), size_(0) {} + +// TODO(user): Return false when issues were encountered while parsing the +// file. +bool TspLibTourParser::LoadFile(const std::string& file_name) { + section_ = UNDEFINED_SECTION; + comments_.clear(); + tour_.clear(); + std::shared_ptr zip_archive( + OpenZipArchiveIfItExists(file_name)); + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + ProcessNewLine(line); + } + return true; +} + +void TspLibTourParser::ProcessNewLine(const std::string& line) { + const std::vector words = + absl::StrSplit(line, ' ', absl::SkipEmpty()); + const int word_size = words.size(); + if (word_size > 0) { + if (section_ == TOUR_SECTION) { + for (const std::string& word : words) { + const int node = atoi32(word); + if (node >= 0) { + tour_.push_back(atoi32(word) - 1); + } else { + section_ = UNDEFINED_SECTION; + } + } + } else { + const std::string& last_word = words[word_size - 1]; + if (!gtl::FindCopy(*kSections, words[0], §ion_)) { + LOG(WARNING) << "Unknown section: " << words[0]; + return; + } + switch (section_) { + case NAME: + break; + case TYPE: + CHECK_EQ("TOUR", last_word); + break; + case COMMENT: { + comments_ = absl::StrJoin(words.begin() + 1, words.end(), " "); + break; + } + case DIMENSION: + size_ = atoi32(last_word); + break; + case TOUR_SECTION: + break; + case END_OF_FILE: + break; + default: + LOG(WARNING) << "Unknown key word: " << words[0]; + } + } + } +} + +const absl::flat_hash_map* const + TspLibTourParser::kSections = + new absl::flat_hash_map( + {{"NAME", NAME}, + {"TYPE", TYPE}, + {"COMMENT", COMMENT}, + {"DIMENSION", DIMENSION}, + {"TOUR_SECTION", TOUR_SECTION}, + {"EOF", END_OF_FILE}}); + +CVRPToursParser::CVRPToursParser() : cost_(0) {} + +// TODO(user): Return false when issues were encountered while parsing the +// file. +bool CVRPToursParser::LoadFile(const std::string& file_name) { + tours_.clear(); + cost_ = 0; + std::shared_ptr zip_archive( + OpenZipArchiveIfItExists(file_name)); + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + ProcessNewLine(line); + } + return true; +} + +void CVRPToursParser::ProcessNewLine(const std::string& line) { + const std::vector words = + absl::StrSplit(line, ' ', absl::SkipEmpty()); + const int word_size = words.size(); + if (word_size > 0) { + if (absl::AsciiStrToUpper(words[0]) == "COST") { + CHECK_EQ(word_size, 2); + cost_ = atoi32(words[1]); + return; + } + if (absl::AsciiStrToUpper(words[0]) == "ROUTE") { + CHECK_GT(word_size, 2); + tours_.resize(tours_.size() + 1); + for (int i = 2; i < word_size; ++i) { + tours_.back().push_back(atoi32(words[i])); + } + return; + } + LOG(WARNING) << "Unknown key word: " << words[0]; + } +} + +} // namespace operations_research diff --git a/ortools/routing/tsplib_parser.h b/ortools/routing/tsplib_parser.h new file mode 100644 index 0000000000..51b04e2940 --- /dev/null +++ b/ortools/routing/tsplib_parser.h @@ -0,0 +1,253 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A TSPLIB parser. The TSPLIB is a library containing Traveling +// Salesman Problems and other vehicle routing problems. +// Limitations: +// - only TSP and CVRP files are currently supported. +// - XRAY1, XRAY2 and SPECIAL edge weight types are not supported. +// +// Takes as input a data file, potentially gzipped. The data must +// follow the TSPLIB95 format (described at +// http://www.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95/DOC.PS). + +#ifndef OR_TOOLS_ROUTING_TSPLIB_PARSER_H_ +#define OR_TOOLS_ROUTING_TSPLIB_PARSER_H_ + +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "ortools/base/integral_types.h" +#include "ortools/routing/simple_graph.h" + +namespace operations_research { + +class TspLibParser final { + public: + // Routing model types (cf. the link above for a description). + enum Types { TSP, ATSP, SOP, HCP, CVRP, TOUR, UNDEFINED_TYPE }; + + TspLibParser(); + // Loads and parses a routing problem from a given file. + bool LoadFile(const std::string& file_name); + // Returns the number of nodes in the routing problem stored in a given file. + int SizeFromFile(const std::string& file_name) const; + // Returns a function returning edge weights between nodes. + EdgeWeights GetEdgeWeights() const { return distance_function_; } + // Returns the index of the depot. + int depot() const { return depot_; } + // Returns the number of nodes in the current routing problem. + int size() const { return size_; } + // Returns the type of the current routing problem. + Types type() const { return type_; } + // Returns the coordinates of the nodes in the current routing problem (if + // they exist). + const std::vector>& coordinates() const { + return coords_; + } + // Returns the capacity of the vehicles in the current routing problem. + int64_t capacity() const { return capacity_; } + // Returns the maximal distance vehicles can travel. + int64_t max_distance() const { return max_distance_; } + // Returns the demands (or quantities picked up) at each node. + const std::vector& demands() const { return demands_; } + // Returns the pairs of nodes corresponding to forced edges (second node is + // directly after the first). + const std::set> fixed_edges() const { + return fixed_edges_; + } + // Returns edges of the graph on which Hamiltonian cycles need to be built. + // Edges are represented as adjacency lists for each node. + const std::vector>& edges() const { return edges_; } + // Returns the name of the current routing model. + const std::string& name() const { return name_; } + // Returns the comments attached to the data. + const std::string& comments() const { return comments_; } + // Build a tour output in TSPLIB95 format from a vector of routes, a route + // being a sequence of node indices. + std::string BuildTourFromRoutes( + const std::vector>& routes) const; + + private: + enum Sections { + NAME, + TYPE, + COMMENT, + DIMENSION, + DISTANCE, + CAPACITY, + EDGE_DATA_FORMAT, + EDGE_DATA_SECTION, + EDGE_WEIGHT_TYPE, + EDGE_WEIGHT_FORMAT, + EDGE_WEIGHT_SECTION, + FIXED_EDGES_SECTION, + NODE_COORD_TYPE, + DISPLAY_DATA_TYPE, + DISPLAY_DATA_SECTION, + NODE_COORD_SECTION, + DEPOT_SECTION, + DEMAND_SECTION, + END_OF_FILE, + UNDEFINED_SECTION + }; + enum EdgeDataFormat { EDGE_LIST, ADJ_LIST }; + enum EdgeWeightTypes { + EXPLICIT, + EUC_2D, + EUC_3D, + MAX_2D, + MAX_3D, + MAN_2D, + MAN_3D, + CEIL_2D, + GEO, + GEOM, + ATT, + XRAY1, + XRAY2, + SPECIAL, + UNDEFINED_EDGE_WEIGHT_TYPE + }; + enum EdgeWeightFormats { + FUNCTION, + FULL_MATRIX, + UPPER_ROW, + LOWER_ROW, + UPPER_DIAG_ROW, + LOWER_DIAG_ROW, + UPPER_COL, + LOWER_COL, + UPPER_DIAG_COL, + LOWER_DIAG_COL, + UNDEFINED_EDGE_WEIGHT_FORMAT + }; + +#ifndef SWIG + TspLibParser(const TspLibParser&) = delete; + void operator=(const TspLibParser&) = delete; +#endif + + void ParseExplicitFullMatrix(const std::vector& words); + void ParseExplicitUpperRow(const std::vector& words); + void ParseExplicitLowerRow(const std::vector& words); + void ParseExplicitUpperDiagRow(const std::vector& words); + void ParseExplicitLowerDiagRow(const std::vector& words); + void ParseNodeCoord(const std::vector& words); + void SetUpEdgeWeightSection(); + void FinalizeEdgeWeights(); + void ParseSections(const std::vector& words); + void ProcessNewLine(const std::string& line); + void SetExplicitCost(int from, int to, int64_t cost) { + if (explicit_costs_.size() != size_ * size_) { + explicit_costs_.resize(size_ * size_, 0); + } + explicit_costs_[from * size_ + to] = cost; + } + + // Model data + int64_t size_; + int64_t capacity_; + int64_t max_distance_; + std::vector demands_; + EdgeWeights distance_function_; + std::vector explicit_costs_; + std::set> fixed_edges_; + int depot_; + std::vector> edges_; + + // Parsing data + static const absl::flat_hash_map* const kSections; + Sections section_; + static const absl::flat_hash_map* const kTypes; + Types type_; + static const absl::flat_hash_map* const + kEdgeDataFormats; + EdgeDataFormat edge_data_format_; + static const absl::flat_hash_map* const + kEdgeWeightTypes; + EdgeWeightTypes edge_weight_type_; + static const absl::flat_hash_map* const + kEdgeWeightFormats; + EdgeWeightFormats edge_weight_format_; + int edge_row_; + int edge_column_; + std::vector> coords_; + std::string name_; + std::string comments_; + int64_t to_read_; +}; + +// Class parsing tour (solution) data in TSLIB95 format. + +class TspLibTourParser final { + public: + TspLibTourParser(); + // Loads and parses a given tour file. + bool LoadFile(const std::string& file_name); + // Returns a vector corresponding to the sequence of nodes of the tour. + const std::vector& tour() const { return tour_; } + // Returns the size of the tour. + int size() const { return size_; } + // Returns the comments attached to the data. + const std::string& comments() const { return comments_; } + + private: + enum Sections { + NAME, + TYPE, + COMMENT, + DIMENSION, + TOUR_SECTION, + END_OF_FILE, + UNDEFINED_SECTION + }; + +#ifndef SWIG + TspLibTourParser(const TspLibTourParser&) = delete; + void operator=(const TspLibTourParser&) = delete; +#endif + + void ProcessNewLine(const std::string& line); + + static const absl::flat_hash_map* const kSections; + Sections section_; + std::string comments_; + int64_t size_; + std::vector tour_; +}; + +// Class parsing tours (solution) data in CVRPlib format. + +class CVRPToursParser final { + public: + CVRPToursParser(); + // Loads and parses a given tours file. + bool LoadFile(const std::string& file_name); + // Returns a vector corresponding to the sequence of nodes of tours. + const std::vector>& tours() const { return tours_; } + int64_t cost() const { return cost_; } + + private: + void ProcessNewLine(const std::string& line); + + std::vector> tours_; + int64_t cost_; +}; +} // namespace operations_research + +#endif // OR_TOOLS_ROUTING_TSPLIB_PARSER_H_ diff --git a/ortools/routing/tsplib_parser_test.cc b/ortools/routing/tsplib_parser_test.cc new file mode 100644 index 0000000000..f32cb3400a --- /dev/null +++ b/ortools/routing/tsplib_parser_test.cc @@ -0,0 +1,604 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/tsplib_parser.h" + +#include +#include +#include + +#include "absl/container/btree_set.h" +#include "absl/flags/flag.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" +#include "ortools/base/filesystem.h" +#include "ortools/base/helpers.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/map_util.h" +#include "ortools/base/memfile.h" +#include "ortools/base/path.h" +#include "ortools/base/zipfile.h" + +ABSL_FLAG(std::string, test_srcdir, "", "REQUIRED: src dir"); + +namespace operations_research { +namespace { + +TEST(TspLibParserTest, LoadAllDataSets) { + static const char* kArchives[] = { + "operations_research_data/TSPLIB95/ALL_tsp.tar.gz", + "operations_research_data/TSPLIB95/ALL_vrp.tar", + "operations_research_data/TSPLIB95/ALL_atsp.tar", + "operations_research_data/TSPLIB95/ALL_sop.tar", + "operations_research_data/TSPLIB95/ALL_hcp.tar"}; + static const char* kZipArchives[] = { + "operations_research_data/TSPLIB95/ALL_tsp.zip", + "operations_research_data/TSPLIB95/ALL_vrp.zip"}; + static const char* kExpectedComments[] = { + "drilling problem (Ludwig)", + "535 Airports around the globe (Padberg/Rinaldi)", + "48 capitals of the US (Padberg/Rinaldi)", + "532-city problem (Padberg/Rinaldi)", + "29 Cities in Bavaria, " // NOLINT(bugprone-suspicious-missing-comma) + "geographical distances (Groetschel,Juenger,Reinelt)", + "29 cities in Bavaria, street distances (Groetschel,Juenger,Reinelt)", + "52 locations in Berlin (Groetschel)", + "127 Biergaerten in Augsburg (Juenger/Reinelt)", + "58 cities in Brazil (Ferreira)", + "BR Deutschland in den Grenzen von 1989 (Bachem/Wottawa)", + "Bridge tournament problem (Rinaldi)", + "14-Staedte in Burma (Zaw Win)", + "130 city problem (Churritz)", + "150 city Problem (churritz)", + "Drilling problem (Reinelt)", + "Deutschland-Problem (A.Rohe)", + "Drilling problem (Reinelt)", + "Bundesrepublik Deutschland (mit Ex-DDR) (Bachem/Wottawa)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "42 cities (Dantzig)", + "Clustered random problem (Johnson)", + "101-city problem (Christofides/Eilon)", + "51-city problem (Christofides/Eilon)", + "76-city problem (Christofides/Eilon)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Die 5 neuen Laender Deutschlands (Ex-DDR) (Bachem/Wottawa)", + "26 Staedte (Fricker)", + "262-city problem (Gillet/Johnson)", + "120 cities in Germany (Groetschel)", + "America-Subproblem of 666-city TSP (Groetschel)", + "17-city problem (Groetschel)", + "Europe-Subproblem of 666-city TSP (Groetschel)", + "21-city problem (Groetschel)", + "Asia/Australia-Subproblem of 666-city TSP (Groetschel)", + "24-city problem (Groetschel)", + "Europe/Asia/Australia-Subproblem of 666-city TSP (Groetschel)", + "48-city problem (Groetschel)", + "666 cities around the world (Groetschel)", + "Africa-Subproblem of 666-city TSP (Groetschel)", + "48-city problem (Held/Karp)", + "100-city problem A (Krolak/Felts/Nelson)", + "150-city problem A (Krolak/Felts/Nelson)", + "200-city problem A (Krolak/Felts/Nelson)", + "100-city problem B (Krolak/Felts/Nelson)", + "150-city problem B (Krolak/Felts/Nelson)", + "200-city problem B (Krolak/Felts/Nelson)", + "100-city problem C (Krolak/Felts/Nelson)", + "100-city problem D (Krolak/Felts/Nelson)", + "100-city problem E (Krolak/Felts/Nelson)", + "105-city problem (Subproblem of lin318)", + "318-city problem (Lin/Kernighan)", + "Original 318-city problem (Lin/Kernighan)", + "1379 Orte in Nordrhein-Westfalen (Bachem/Wottawa)", + "Drilling problem (Reinelt)", + "561-city problem (Kleinschmidt)", + "Drilling problem (Juenger/Reinelt)", + "Drilling problem (Junger/Reinelt)", + "Drilling problem (Groetschel/Juenger/Reinelt)", + "Programmed logic array (Johnson)", + "Programmed logic array (Johnson)", + "Programmed logic array (Johnson)", + "1002-city problem (Padberg/Rinaldi)", + "107-city problem (Padberg/Rinaldi)", + "124-city problem (Padberg/Rinaldi)", + "136-city problem (Padberg/Rinaldi)", + "144-city problem (Padberg/Rinaldi)", + "152-city problem (Padberg/Rinaldi)", + "226-city problem (Padberg/Rinaldi)", + "2392-city problem (Padberg/Rinaldi)", + "264-city problem (Padberg/Rinaldi)", + "299-city problem (Padberg/Rinaldi)", + "439-city problem (Padberg/Rinaldi)", + "76-city problem (Padberg/Rinaldi)", + "Rattled grid (Pulleyblank)", + "Rattled grid (Pulleyblank)", + "Rattled grid (Pulleyblank)", + "Rattled grid (Pulleyblank)", + "100-city random TSP (Reinelt)", + "400-city random TSP (Reinelt)", + "11849-city TSP (Reinelt)", + "1304-city TSP (Reinelt)", + "1323-city TSP (Reinelt)", + "1889-city TSP (Reinelt)", + "5915-city TSP (Reinelt)", + "5934-city TSP (Reinelt)", + "", + "", + "", + "70-city problem (Smith/Thompson)", + "42 Staedte Schweiz (Fricker)", + "225-city problem (Juenger,Raecke,Tschoecke)", + "A TSP problem (Reinelt)", + "Drilling problem problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Drilling problem (Reinelt)", + "Odyssey of Ulysses (Groetschel/Padberg)", + "Odyssey of Ulysses (Groetschel/Padberg)", + "Cities with population at least 500 in the continental US.\nContributed" + " by David Applegate and Andre Rohe, based on the\ndata set" + " \"US.lat-long\" from the ftp site ftp.cs.toronto.edu.\nThe file" + " US.lat-long.Z can be found in the directory /doc/geography.", + "1084-city problem (Reinelt)", + "1784-city problem (Reinelt)", + "(Rinaldi,Yarrow/Araque)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Eilon et al.)", + "(Gillet and Johnson)", + "17 city problem (Repetto)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Fischetti)", + "Asymmetric TSP (Repetto,Pekny)", + "Stacker crane application (Ascheuer)", + "Stacker crane application (Ascheuer)", + "Stacker crane application (Ascheuer)", + "Stacker crane application (Ascheuer)", + "Asymmetric TSP (Fischetti)", + "Received by Norbert Ascheuer / Laureano Escudero", + "Received by Norbert Ascheuer / Laureano Escudero", + "Received by Norbert Ascheuer / Laureano Escudero", + "Received by Norbert Ascheuer / Laureano Escudero", + "Received by Norbert Ascheuer / Laureano Escudero", + "Received by Norbert Ascheuer / Laureano Escudero", + "Received by Norbert Ascheuer / Laureano Escudero", + "br17.atsp plus random precedences (Norbert Ascheuer)", + "br17.atsp plus random precedences (Norbert Ascheuer)", + "Received by Norbert Ascheuer (based on ft53.atsp)", + "Received by Norbert Ascheuer (based on ft53.atsp)", + "Received by Norbert Ascheuer (based on ft53.atsp)", + "Received by Norbert Ascheuer (based on ft53.atsp)", + "Received by Norbert Ascheuer (based on ft70.atsp)", + "Received by Norbert Ascheuer (based on ft70.atsp)", + "Received by Norbert Ascheuer (based on ft70.atsp)", + "Received by Norbert Ascheuer (based on ft70.atsp)", + "Received by Norbert Ascheuer (based on kro124p.atsp)", + "Received by Norbert Ascheuer (based on kro124p.atsp)", + "Received by Norbert Ascheuer (based on kro124p.atsp)", + "Received by Norbert Ascheuer (based on kro124p.atsp)", + "Received by Norbert Ascheuer (based on p43.atsp)", + "Received by Norbert Ascheuer (based on p43.atsp)", + "Received by Norbert Ascheuer (based on p43.atsp)", + "Received by Norbert Ascheuer (based on p43.atsp)", + "Received by N. Ascheuer / M. Juenger / G. Reinelt", + "Received by N. Ascheuer / M. Juenger / G. Reinelt", + "Stacker crane application (Norbert Ascheuer)", + "Stacker crane application (Norbert Ascheuer)", + "Stacker crane application (Norbert Ascheuer)", + "Stacker crane application (Norbert Ascheuer)", + "Stacker crane application (Norbert Ascheuer)", + "Stacker crane application (Norbert Ascheuer)", + "Stacker crane application (Norbert Ascheuer)", + "Stacker crane application (Norbert Ascheuer)", + "Stacker crane application (Norbert Ascheuer)", + "Stacker crane application (Norbert Ascheuer)", + "Received by Norbert Ascheuer (based on ry48p.atsp)", + "Received by Norbert Ascheuer (based on ry48p.atsp)", + "Received by Norbert Ascheuer (based on ry48p.atsp)", + "Received by Norbert Ascheuer (based on ry48p.atsp)", + "Hamiltonian cycle problem (Erbacci)", + "Hamiltonian cycle problem (Erbacci)", + "Hamiltonian cycle problem (Erbacci)", + "Hamiltonian cycle problem (Erbacci)", + "Hamiltonian cycle problem (Erbacci)", + "Hamiltonian cycle problem (Erbacci)", + "Hamiltonian cycle problem (Erbacci)", + "Hamiltonian cycle problem (Erbacci)", + "Hamiltonian cycle problem (Erbacci)"}; + // Skipping unsupported instances. + const absl::btree_set exceptions = {"xray.problems", + "tspleap.c"}; + // Parsing from tar archive + int file_index = 0; + for (const char* const archive : kArchives) { + std::vector matches; + if (file::Match(file::JoinPath("/tarfs", absl::GetFlag(FLAGS_test_srcdir), + archive, "*"), + &matches, file::Defaults()) + .ok()) { + for (const std::string& match : matches) { + const absl::string_view stem = file::Stem(match); + if (!exceptions.contains(stem) && file::Extension(stem) != "tour") { + TspLibParser parser; + EXPECT_TRUE(parser.LoadFile(match)); + EXPECT_EQ(kExpectedComments[file_index], parser.comments()); + EXPECT_LT(0, parser.SizeFromFile(match)); + file_index++; + } + } + } + } + // Parsing from zip archive. + file_index = 0; + for (const char* const archive : kZipArchives) { + std::shared_ptr zip_archive(zipfile::OpenZipArchive( + file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), archive))); + ASSERT_NE(nullptr, zip_archive); + std::vector matches; + ASSERT_TRUE( + file::Match(file::JoinPath("/zip", absl::GetFlag(FLAGS_test_srcdir), + archive, "*"), + &matches, file::Defaults()) + .ok()); + for (const std::string& match : matches) { + const absl::string_view stem = file::Stem(match); + if (!exceptions.contains(stem) && file::Extension(stem) != "tour") { + TspLibParser parser; + EXPECT_TRUE(parser.LoadFile(match)); + EXPECT_EQ(kExpectedComments[file_index], parser.comments()); + EXPECT_LT(0, parser.SizeFromFile(match)); + file_index++; + } + } + } +} + +TEST(TspLibParserTest, GeneratedDataSets) { + static const char kName[] = "GoogleTest"; + static const char* const kTypes[] = {"TSP", "CVRP"}; + static const char kComment[] = "This is a test"; + static const int kDimension = 4; + static const int kCoordSize = 2; + static const int kCapacity = 2; + static const char* const kEdgeWeightTypes[] = { + "EXPLICIT", "EUC_2D", "EUC_3D", "MAX_2D", "MAX_3D", + "MAN_2D", "MAN_3D", "CEIL_2D", "GEO", "ATT"}; + static const char* const kEdgeWeightFormats[] = { + "FULL_MATRIX", "UPPER_ROW", "LOWER_ROW", + "UPPER_DIAG_ROW", "LOWER_DIAG_ROW", "UPPER_COL", + "LOWER_COL", "UPPER_DIAG_COL", "LOWER_DIAG_COL"}; + static const char* const kNodeCoordTypes[] = {"TWOD_COORDS", "THREED_COORDS", + "NO_COORDS"}; + static const char* const kDisplayDataTypes[] = {"COORD_DISPLAY", + "TWOD_DISPLAY", "NO_DISPLAY"}; + for (int type = 0; type < ABSL_ARRAYSIZE(kTypes); ++type) { + for (int edge_type = 0; edge_type < ABSL_ARRAYSIZE(kEdgeWeightTypes); + ++edge_type) { + for (int edge_format = 0; + edge_format < ABSL_ARRAYSIZE(kEdgeWeightFormats); ++edge_format) { + for (int node_type = 0; node_type < ABSL_ARRAYSIZE(kNodeCoordTypes); + ++node_type) { + if (node_type == 2 && edge_type != 0) break; + if (node_type == 1 && edge_type != 2 && edge_type != 4 && + edge_type != 6) + break; + if (node_type == 0 && edge_type != 1 && edge_type != 3 && + edge_type != 5 && edge_type < 7) + break; + for (int display_type = 0; + display_type < ABSL_ARRAYSIZE(kDisplayDataTypes); + ++display_type) { + if (display_type == 0 && node_type == 2) break; + std::string data = absl::StrFormat("NAME: %s\n", kName); + absl::StrAppendFormat(&data, "TYPE: %s\n", kTypes[type]); + absl::StrAppendFormat(&data, "COMMENT: %s\n", kComment); + absl::StrAppendFormat(&data, "DIMENSION: %d\n", kDimension); + if (type == 1) { + absl::StrAppendFormat(&data, "CAPACITY: %d\n", kCapacity); + } + absl::StrAppendFormat(&data, "EDGE_WEIGHT_TYPE: %s\n", + kEdgeWeightTypes[edge_type]); + if (edge_type == 0) { + absl::StrAppendFormat(&data, "EDGE_WEIGHT_FORMAT: %s\n", + kEdgeWeightFormats[edge_format]); + } + absl::StrAppendFormat(&data, "NODE_COORD_TYPE: %s\n", + kNodeCoordTypes[node_type]); + absl::StrAppendFormat(&data, "DISPLAY_DATA_TYPE: %s\n", + kDisplayDataTypes[display_type]); + if (node_type != 2) { + data += "NODE_COORD_SECTION\n"; + for (int i = 0; i < kDimension; ++i) { + absl::StrAppendFormat(&data, "%d %d %d", i + 1, i % kCoordSize, + i / kCoordSize); + if (node_type == 1) { + data += " 0"; + } + data += "\n"; + } + } + if (type == 1) { + data += "DEPOT_SECTION\n1\n-1\n"; + data += "DEMAND_SECTION\n"; + for (int i = 0; i < kDimension; ++i) { + absl::StrAppendFormat(&data, "%d %d\n", i + 1, 1); + } + } + if (display_type == 1) { + data += "DISPLAY_DATA_SECTION\n"; + for (int i = 0; i < kDimension; ++i) { + absl::StrAppendFormat(&data, "%d %d %d\n", i + 1, + i % kCoordSize, i / kCoordSize); + } + } + if (edge_type == 0) { + data += "EDGE_WEIGHT_SECTION\n"; + // Manhattan distances + switch (edge_format) { + case 0: + for (int i = 0; i < kDimension; ++i) { + const int x = i % kCoordSize; + const int y = i / kCoordSize; + for (int j = 0; j < kDimension; ++j) { + const int distance = + abs(x - (j % kCoordSize)) + abs(y - (j / kCoordSize)); + absl::StrAppendFormat(&data, "%d ", distance); + } + data += "\n"; + } + break; + case 1: + case 6: + for (int i = 0; i < kDimension; ++i) { + const int x = i % kCoordSize; + const int y = i / kCoordSize; + for (int j = i + 1; j < kDimension; ++j) { + const int distance = + abs(x - (j % kCoordSize)) + abs(y - (j / kCoordSize)); + absl::StrAppendFormat(&data, "%d ", distance); + } + data += "\n"; + } + break; + case 2: + case 5: + for (int i = 0; i < kDimension; ++i) { + const int x = i % kCoordSize; + const int y = i / kCoordSize; + for (int j = 0; j < i; ++j) { + const int distance = + abs(x - (j % kCoordSize)) + abs(y - (j / kCoordSize)); + absl::StrAppendFormat(&data, "%d ", distance); + } + data += "\n"; + } + break; + case 3: + case 8: + for (int i = 0; i < kDimension; ++i) { + const int x = i % kCoordSize; + const int y = i / kCoordSize; + for (int j = i; j < kDimension; ++j) { + const int distance = + abs(x - (j % kCoordSize)) + abs(y - (j / kCoordSize)); + absl::StrAppendFormat(&data, "%d ", distance); + } + data += "\n"; + } + break; + case 4: + case 7: + for (int i = 0; i < kDimension; ++i) { + const int x = i % kCoordSize; + const int y = i / kCoordSize; + for (int j = 0; j <= i; ++j) { + const int distance = + abs(x - (j % kCoordSize)) + abs(y - (j / kCoordSize)); + absl::StrAppendFormat(&data, "%d ", distance); + } + data += "\n"; + } + break; + } + } + data += "EOF"; + static const char kMMFileName[] = "/memfile/dummy.tsp"; + RegisteredMemFile registered(kMMFileName, data); + TspLibParser parser; + EXPECT_TRUE(parser.LoadFile(kMMFileName)); + EXPECT_EQ(kDimension, parser.SizeFromFile(kMMFileName)); + } + } + } + } + } +} + +TEST(TspLibParserTest, ParseHCPEdgeList) { + static const char* kData = + "NAME : test\n" + "COMMENT : Test\n" + "TYPE : HCP\n" + "DIMENSION : 3\n" + "EDGE_DATA_FORMAT : EDGE_LIST\n" + "EDGE_DATA_SECTION\n" + " 3 1\n" + " 2 1\n" + "-1\nEOF"; + static const char kMMFileName[] = "/memfile/dummy.hcp"; + RegisteredMemFile registered(kMMFileName, kData); + TspLibParser parser; + EXPECT_TRUE(parser.LoadFile(kMMFileName)); + EXPECT_EQ(3, parser.SizeFromFile(kMMFileName)); + EXPECT_EQ(2, parser.edges()[0].size()); + EXPECT_EQ(1, parser.edges()[0][0]); + EXPECT_EQ(2, parser.edges()[0][1]); + EXPECT_EQ(0, parser.edges()[1].size()); + EXPECT_EQ(0, parser.edges()[2].size()); +} + +TEST(TspLibParserTest, ParseHCPAdjList) { + static const char* kData = + "NAME : test\n" + "COMMENT : Test\n" + "TYPE : HCP\n" + "DIMENSION : 3\n" + "EDGE_DATA_FORMAT : ADJ_LIST\n" + "EDGE_DATA_SECTION\n" + " 3 1 2 -1\n" + "-1\nEOF"; + static const char kMMFileName[] = "/memfile/dummy.hcp"; + RegisteredMemFile registered(kMMFileName, kData); + TspLibParser parser; + EXPECT_TRUE(parser.LoadFile(kMMFileName)); + EXPECT_EQ(3, parser.SizeFromFile(kMMFileName)); + EXPECT_EQ(1, parser.edges()[0].size()); + EXPECT_EQ(2, parser.edges()[0][0]); + EXPECT_EQ(1, parser.edges()[1].size()); + EXPECT_EQ(2, parser.edges()[1][0]); + EXPECT_EQ(0, parser.edges()[2].size()); +} + +TEST(TspLibParserTest, ParseKytojoki33Depot) { + // This file inverts EDGE_WEIGHT_TYPE and EDGE_WEIGHT_FORMAT. + std::string file_name = + file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), + "ortools/routing/testdata/", "tsplib_Kytojoki_33.vrp"); + TspLibParser parser; + EXPECT_TRUE(parser.LoadFile(file_name)); + // The depot is a new node, given by its coordinates, instead of an existing + // node in the graph. + EXPECT_EQ(2400, parser.depot()); + EXPECT_EQ(0, parser.edges().size()); + EXPECT_EQ(0.0, parser.coordinates()[parser.depot()].x); + EXPECT_EQ(0.0, parser.coordinates()[parser.depot()].y); +} + +TEST(TspLibTourParserTest, LoadAllDataSets) { + static const char kArchive[] = + "operations_research_data/TSPLIB95/ALL_tsp.tar.gz"; + static const char* kExpectedComments[] = { + "", + ": Optimum solution for att48", + ": Optimum solution of bayg29", + ": Optimum solution of bays29", + "", + "", + ": Length 6110", + ": Length 6528", + ": Optimum tour for eil101.tsp (Length 629)", + ": Optimal tour for eil51.tsp (426)", + ": Optimum tour for eil76.tsp (538)", + ": optimal tour for fri26 (937)", + ": Optimal tour for gr120 (6942)", + ": Optimal solution for gr202 (40160)", + ": Optimal solution for gr24 (1272)", + ": Optimal solution for gr48 (5046)", + ": Optimal solution of gr666 (294358)", + ": Optimal tour for gr96 (55209)", + ": Optimum tour for kroA100 (21282)", + ": Optimal tour for kroC100 (20749)", + ": Optimal tour for kroD100 (21294)", + ": Optimal tour for lin105 (14379)", + ": Optimal tour for pa561 (2763)", + ": Optimal solution for pcb442 (50778)", + ": optimal tour for pr1002 (259045)", + ": Optimal solution for pr2392 (378032)", + ": Optimal tour for pr76 (108159)", + ": Optimal solution for rd100 (7910)", + ": Optimal tour for st70 (675)", + ": Optimal solution for tsp225 (3919)", + ": Optimal solution for ulysses16 (6859)", + ": Optimal solution of ulysses22 (7013)"}; + int file_index = 0; + std::vector matches; + if (file::Match(file::JoinPath("/tarfs", absl::GetFlag(FLAGS_test_srcdir), + kArchive, "*\\.opt\\.tour\\.gz"), + &matches, file::Defaults()) + .ok()) { + for (const std::string& match : matches) { + TspLibTourParser parser; + EXPECT_TRUE(parser.LoadFile(match)); + EXPECT_EQ(kExpectedComments[file_index], parser.comments()); + file_index++; + } + } +} + +TEST(CVRPToursParserTest, LoadAllDataSets) { + static const char kArchive[] = + "operations_research_data/CVRP/Augerat/A-VRP-sol.zip"; + static const int kExpectedCosts[] = {/*opt-A-n32-k5*/ 784, + /*opt-A-n33-k5*/ 661, + /*opt-A-n33-k6*/ 742, + /*opt-A-n34-k5*/ 778, + /*opt-A-n36-k5*/ 799, + /*opt-A-n37-k5*/ 669, + /*opt-A-n37-k6*/ 949, + /*opt-A-n38-k5*/ 730, + /*opt-A-n39-k5*/ 822, + /*opt-A-n39-k6*/ 831, + /*opt-A-n44-k6*/ 937, + /*opt-A-n45-k6*/ 944, + /*opt-A-n45-k7*/ 1146, + /*opt-A-n46-k7*/ 914, + /*opt-A-n48-k7*/ 1073, + /*opt-A-n53-k7*/ 1010, + /*opt-A-n55-k9*/ 1073}; + int file_index = 0; + std::vector matches; + if (file::Match(file::JoinPath("/zip", absl::GetFlag(FLAGS_test_srcdir), + kArchive, "opt-A-\\.*"), + &matches, file::Defaults()) + .ok()) { + for (const std::string& match : matches) { + CVRPToursParser parser; + EXPECT_TRUE(parser.LoadFile(match)); + EXPECT_EQ(kExpectedCosts[file_index], parser.cost()); + file_index++; + } + } +} +} // namespace +} // namespace operations_research diff --git a/ortools/routing/tsptw_parser.cc b/ortools/routing/tsptw_parser.cc new file mode 100644 index 0000000000..b2de28fd70 --- /dev/null +++ b/ortools/routing/tsptw_parser.cc @@ -0,0 +1,213 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/tsptw_parser.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/match.h" +#include "absl/strings/str_split.h" +#include "ortools/base/mathutil.h" +#include "ortools/base/numbers.h" +#include "ortools/base/path.h" +#include "ortools/base/strtoint.h" +#include "ortools/base/zipfile.h" +#include "ortools/util/filelineiter.h" + +namespace operations_research { + +namespace { + +double DoubleEuc2DDistance(const Coordinates2& from, + const Coordinates2& to) { + const double xd = from.x - to.x; + const double yd = from.y - to.y; + return sqrt(xd * xd + yd * yd); +} + +double Euc2DDistance(const Coordinates2& from, + const Coordinates2& to) { + return std::floor(DoubleEuc2DDistance(from, to)); +} + +constexpr double kInfinity = std::numeric_limits::infinity(); + +std::shared_ptr OpenZipArchiveIfItExists( + const std::string& file_name) { + const absl::string_view archive_name = file::Dirname(file_name); + if (file::Extension(archive_name) == "zip") { + return zipfile::OpenZipArchive(archive_name); + } else { + return nullptr; + } +} + +} // namespace + +TspTWParser::TspTWParser() + : size_(0), + depot_(0), + total_service_time_(0), + distance_function_(nullptr), + time_function_(nullptr) {} + +bool TspTWParser::LoadFile(const std::string& file_name) { + std::shared_ptr zip_archive( + OpenZipArchiveIfItExists(file_name)); + coords_.clear(); + time_windows_.clear(); + service_times_.clear(); + distance_matrix_.clear(); + size_ = 0; + depot_ = 0; + total_service_time_ = 0; + distance_function_ = nullptr; + time_function_ = nullptr; + return ParseLopezIbanezBlum(file_name) || ParseDaSilvaUrrutia(file_name); +} + +bool TspTWParser::ParseLopezIbanezBlum(const std::string& file_name) { + int section = 0; + int entry_count = 0; + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + const std::vector words = + absl::StrSplit(line, absl::ByAnyChar(" :\t"), absl::SkipEmpty()); + if (words.empty()) continue; + // Parsing comments. + if (words[0] == "#") { + if (absl::StrContains(line, "service times")) { + const double total_service_time = + strings::ParseLeadingDoubleValue(words.back(), kInfinity); + if (total_service_time != kInfinity) { + total_service_time_ = MathUtil::FastInt64Round(total_service_time); + } + } + continue; + } + switch (section) { + case 0: { // Parsing size. + if (words.size() != 1) return false; + size_ = strings::ParseLeadingInt32Value(words[0], -1); + if (size_ < 0) return false; + distance_matrix_.reserve(size_ * size_); + ++section; + entry_count = 0; + break; + } + case 1: { // Parsing distances. + if (words.size() != size_) return false; + for (const std::string& word : words) { + const double distance = + strings::ParseLeadingDoubleValue(word, kInfinity); + if (distance == kInfinity) return false; + distance_matrix_.push_back(distance); + } + ++entry_count; + if (entry_count == size_) { + ++section; + entry_count = 0; + } + break; + } + case 2: { // Parsing time windows. + if (words.size() != 2) return false; + std::vector values; + for (const std::string& word : words) { + const double value = + strings::ParseLeadingDoubleValue(word, kInfinity); + if (value == kInfinity) return false; + values.push_back(value); + } + time_windows_.push_back({values[0], values[1]}); + service_times_.push_back(0); + ++entry_count; + if (entry_count == size_) { + ++section; + } + break; + } + default: { + return false; + } + } + } + distance_function_ = [this](int from, int to) { + return distance_matrix_[from * size_ + to]; + }; + time_function_ = distance_function_; + return entry_count == size_; +} + +bool TspTWParser::ParseDaSilvaUrrutia(const std::string& file_name) { + for (const std::string& line : + FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + // Skip header. + if (absl::StartsWith(line, "CUST NO.")) continue; + const std::vector words = + absl::StrSplit(line, absl::ByAnyChar(" :\t"), absl::SkipEmpty()); + // Skip comments and empty lines. + if (words.empty() || words[0] == "!!" || words[0][0] == '#') continue; + if (words.size() != 7) return false; + // Check that all field values are doubles, except first which must be a + // positive integer. + const int value = strings::ParseLeadingInt32Value(words[0], -1); + if (value < 0) return false; + // 999 represents the eof. + if (value == 999) continue; + std::vector values; + for (int i = 1; i < words.size(); ++i) { + const double value = strings::ParseLeadingDoubleValue( + words[i], std::numeric_limits::infinity()); + if (value == std::numeric_limits::infinity()) return false; + values.push_back(value); + } + coords_.push_back({values[0], values[1]}); + time_windows_.push_back({values[3], values[4]}); + service_times_.push_back(values[5]); + } + size_ = coords_.size(); + + // Enforce the triangular inequality (needed due to rounding). + distance_matrix_.reserve(size_ * size_); + for (int i = 0; i < size_; ++i) { + for (int j = 0; j < size_; ++j) { + distance_matrix_.push_back(Euc2DDistance(coords_[i], coords_[j])); + } + } + for (int i = 0; i < size_; i++) { + for (int j = 0; j < size_; j++) { + for (int k = 0; k < size_; k++) { + if (distance_matrix_[i * size_ + j] > + distance_matrix_[i * size_ + k] + distance_matrix_[k * size_ + j]) { + distance_matrix_[i * size_ + j] = + distance_matrix_[i * size_ + k] + distance_matrix_[k * size_ + j]; + } + } + } + } + + distance_function_ = [this](int from, int to) { + return distance_matrix_[from * size_ + to]; + }; + time_function_ = [this](int from, int to) { + return distance_matrix_[from * size_ + to] + service_times_[from]; + }; + return true; +} + +} // namespace operations_research diff --git a/ortools/routing/tsptw_parser.h b/ortools/routing/tsptw_parser.h new file mode 100644 index 0000000000..249b3d1174 --- /dev/null +++ b/ortools/routing/tsptw_parser.h @@ -0,0 +1,88 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A TSPTW parser. +// +// Takes as input a data file, potentially gzipped. The data must +// follow the format described at +// http://lopez-ibanez.eu/tsptw-instances and +// https://homepages.dcc.ufmg.br/~rfsilva/tsptw. + +#ifndef OR_TOOLS_ROUTING_TSPTW_PARSER_H_ +#define OR_TOOLS_ROUTING_TSPTW_PARSER_H_ + +#include +#include +#include + +#include "ortools/base/integral_types.h" +#include "ortools/routing/simple_graph.h" + +namespace operations_research { + +class TspTWParser final { + public: + TspTWParser(); + // Loads and parses a routing problem from a given file. + bool LoadFile(const std::string& file_name); + // Returns a function returning the distance between nodes. On some instances + // service times are already included in values returned by this function. + // The actual distance of a route can be obtained by removing + // total_service_time() from the sum of distances in that case. + const std::function& distance_function() const { + return distance_function_; + } + // Returns a function returning the time between nodes (equivalent to + // distance_function(i, j) + service_time(j)). + const std::function& time_function() const { + return time_function_; + } + // Returns the index of the depot. + int depot() const { return depot_; } + // Returns the number of nodes in the current routing problem. + int size() const { return size_; } + // Returns the total service time already included in distance_function. + double total_service_time() const { return total_service_time_; } + // Returns the coordinates of the nodes in the current routing problem. + const std::vector>& coordinates() const { + return coords_; + } + // Returns the time windows of the nodes in the current routing problem. + const std::vector>& time_windows() const { + return time_windows_; + } + // Returns the service times of the nodes in the current routing problem. + const std::vector& service_times() const { return service_times_; } + + private: +#ifndef SWIG + TspTWParser(const TspTWParser&) = delete; + void operator=(const TspTWParser&) = delete; +#endif + bool ParseLopezIbanezBlum(const std::string& file_name); + bool ParseDaSilvaUrrutia(const std::string& file_name); + + int64_t size_; + int depot_; + double total_service_time_; + std::function distance_function_; + std::function time_function_; + std::vector> coords_; + std::vector> time_windows_; + std::vector service_times_; + std::vector distance_matrix_; +}; + +} // namespace operations_research + +#endif // OR_TOOLS_ROUTING_TSPTW_PARSER_H_ diff --git a/ortools/routing/tsptw_parser_test.cc b/ortools/routing/tsptw_parser_test.cc new file mode 100644 index 0000000000..5772dfda49 --- /dev/null +++ b/ortools/routing/tsptw_parser_test.cc @@ -0,0 +1,72 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/tsptw_parser.h" + +#include + +#include "absl/flags/flag.h" +#include "gtest/gtest.h" +#include "ortools/base/helpers.h" +#include "ortools/base/integral_types.h" +#include "ortools/base/path.h" + +ABSL_FLAG(std::string, test_srcdir, "", "REQUIRED: src dir"); + +namespace operations_research { +namespace { + +TEST(TspTWParserTest, LoadDataSet) { + const int sizes[] = {26, 21, 21}; + const double distances[] = {25166.316, 9538, 9006}; + const double times[] = {25166.316, 9538, 9006}; + const double starts[] = {9362, 2388, 2392}; + const double ends[] = {13322, 3131, 3146}; + const double service_times[] = {250, 0, 0}; + const bool has_coordinates[] = {false, false, true}; + int count = 0; + for (const std::string& data : + {"ortools/util/testdata/rc201.0", "ortools/util/testdata/n20w20.001.txt", + "ortools/util/testdata/n20w20.002.txt"}) { + TspTWParser parser; + EXPECT_TRUE(parser.LoadFile( + file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), data))); + EXPECT_EQ(0, parser.depot()); + const int size = sizes[count]; + EXPECT_EQ(size, parser.size()); + double total_distances = 0; + double total_times = 0; + for (int i = 0; i < size; ++i) { + for (int j = 0; j < size; ++j) { + total_distances += parser.distance_function()(i, j); + total_times += parser.time_function()(i, j); + } + } + EXPECT_NEAR(distances[count], total_distances, 1e-6); + EXPECT_NEAR(times[count], total_times, 1e-6); + EXPECT_EQ(service_times[count], parser.total_service_time()); + EXPECT_EQ(has_coordinates[count], !parser.coordinates().empty()); + double total_starts = 0; + double total_ends = 0; + for (int i = 0; i < size; ++i) { + EXPECT_EQ(0, parser.service_times()[i]); + total_starts += parser.time_windows()[i].start; + total_ends += parser.time_windows()[i].end; + } + EXPECT_EQ(starts[count], total_starts); + EXPECT_EQ(ends[count], total_ends); + ++count; + } +} +} // namespace +} // namespace operations_research