google3: export routing/
This commit is contained in:
293
ortools/routing/BUILD.bazel
Normal file
293
ortools/routing/BUILD.bazel
Normal file
@@ -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",
|
||||
],
|
||||
)
|
||||
248
ortools/routing/carp_parser.cc
Normal file
248
ortools/routing/carp_parser.cc
Normal file
@@ -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 <array>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#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<const char*, 12>({
|
||||
"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<std::string> 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<int64_t> ParseNodeIndex(std::string_view text);
|
||||
} // namespace
|
||||
|
||||
bool CarpParser::ParseMetadataLine(const std::vector<std::string>& 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<int64_t> 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<std::string> words =
|
||||
absl::StrSplit(line, absl::ByAnyChar(" :\t(),"), absl::SkipEmpty());
|
||||
|
||||
// Parse the edge.
|
||||
std::optional<int64_t> 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<int64_t> 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<int64_t> 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
|
||||
185
ortools/routing/carp_parser.h
Normal file
185
ortools/routing/carp_parser.h
Normal file
@@ -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 : <INSTANCE-NAME>
|
||||
// COMENTARIO : <ARBITRARY-COMMENT>
|
||||
// VERTICES : <NUMBER-OF-NODES, int>
|
||||
// ARISTAS_REQ : <NUMBER-OF-EDGES-WITH-NONZERO-SERVICING, int>
|
||||
// ARISTAS_NOREQ : <NUMBER-OF-EDGES-WITH-ZERO-SERVICING, int>
|
||||
// VEHICULOS : <NUMBER-OF-VEHICLES, int>
|
||||
// CAPACIDAD : <CAPACITY-OF-EACH-VEHICLE, int>
|
||||
// TIPO_COSTES_ARISTAS : EXPLICITOS
|
||||
// COSTE_TOTAL_REQ : <TOTAL-SERVICING-COST>
|
||||
// LISTA_ARISTAS_REQ :
|
||||
// ( <HEAD-NODE-OF-EDGE, int>, <TAIL-NODE-OF-EDGE, int> )
|
||||
// coste <TRAVERSING-COST, int> demanda <SERVICING, int>
|
||||
// <repeated, one edge per line>
|
||||
// LISTA_ARISTAS_NOREQ :
|
||||
// ( <HEAD-NODE-OF-EDGE, int>, <TAIL-NODE-OF-EDGE, int> )
|
||||
// coste <TRAVERSING-COST, int>
|
||||
// <repeated, one edge per line>
|
||||
// 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 <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#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<Edge, int64_t>& servicing_demands() const {
|
||||
return servicing_demands_;
|
||||
}
|
||||
// Returns the traversing costs of the edges in the current routing problem.
|
||||
const gtl::linked_hash_map<Edge, int64_t>& 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<std::string>& 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<Edge, int64_t> traversing_costs_;
|
||||
gtl::linked_hash_map<Edge, int64_t> servicing_demands_;
|
||||
// - vehicles
|
||||
int64_t n_vehicles_;
|
||||
int64_t capacity_;
|
||||
};
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_ROUTING_CARP_PARSER_H_
|
||||
270
ortools/routing/carp_parser_test.cc
Normal file
270
ortools/routing/carp_parser_test.cc
Normal file
@@ -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 <string>
|
||||
|
||||
#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
|
||||
246
ortools/routing/cvrptw_lib.cc
Normal file
246
ortools/routing/cvrptw_lib.cc
Normal file
@@ -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 <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#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<int64_t>(absl::BitGen(), 0,
|
||||
std::numeric_limits<int64_t>::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<int64_t[]>(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<int> 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
|
||||
136
ortools/routing/cvrptw_lib.h
Normal file
136
ortools/routing/cvrptw_lib.h
Normal file
@@ -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 <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "ortools/base/strong_vector.h"
|
||||
#include "ortools/constraint_solver/routing.h"
|
||||
#include "ortools/util/random_engine.h"
|
||||
|
||||
namespace operations_research {
|
||||
|
||||
typedef std::function<int64_t(RoutingNodeIndex, RoutingNodeIndex)>
|
||||
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<RoutingIndexManager::NodeIndex, Location> 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<int64_t[]> 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_
|
||||
450
ortools/routing/nearp_parser.cc
Normal file
450
ortools/routing/nearp_parser.cc
Normal file
@@ -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 <array>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#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<const char*, 14>({
|
||||
"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<std::string> 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<int64_t> 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<ArcOrEdge> ParseArcOrEdge(std::string_view line,
|
||||
bool with_servicing);
|
||||
} // namespace
|
||||
|
||||
bool NearpParser::ParseMetadataLine(const std::vector<std::string>& 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<int64_t> 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<ArcOrEdge> 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<ArcOrEdge> 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<std::string> 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<int64_t> 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<ArcOrEdge> ParseArcOrEdge(std::string_view line,
|
||||
bool with_servicing) {
|
||||
const std::vector<std::string> 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<int64_t> 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<int64_t> 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
|
||||
257
ortools/routing/nearp_parser.h
Normal file
257
ortools/routing/nearp_parser.h
Normal file
@@ -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: <Instance name>
|
||||
// Optimal value: <Optimal value, -1 if unknown>
|
||||
// #Vehicles: <Max. number of vehicles, -1 if unconstrained>
|
||||
// Capacity: <Vehicle capacity Q>
|
||||
// Depot: <Index of depot node>
|
||||
// #Nodes: <number of nodes>
|
||||
// #Edges: <number of edges>
|
||||
// #Arcs: <number of arcs>
|
||||
// #Required N: <number of required nodes>
|
||||
// #Required E: <number of required edges>
|
||||
// #Required A: <number of required arcs>
|
||||
//
|
||||
// % 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 <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#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, int64_t>& 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, int64_t>& 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<int64_t, int64_t>& 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, int64_t>& 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, int64_t>& 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<int64_t, int64_t>& 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, int64_t>& 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, int64_t>& 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<std::string>& 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, int64_t> arc_traversing_costs_;
|
||||
gtl::linked_hash_map<Edge, int64_t> edge_traversing_costs_;
|
||||
|
||||
gtl::linked_hash_map<Arc, int64_t> arc_servicing_demands_;
|
||||
gtl::linked_hash_map<Edge, int64_t> edge_servicing_demands_;
|
||||
gtl::linked_hash_map<int64_t, int64_t> node_servicing_demands_;
|
||||
|
||||
gtl::linked_hash_map<Arc, int64_t> arc_servicing_costs_;
|
||||
gtl::linked_hash_map<Edge, int64_t> edge_servicing_costs_;
|
||||
gtl::linked_hash_map<int64_t, int64_t> node_servicing_costs_;
|
||||
|
||||
// - vehicles
|
||||
int num_vehicles_;
|
||||
int64_t capacity_;
|
||||
};
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_ROUTING_NEARP_PARSER_H_
|
||||
134
ortools/routing/nearp_parser_test.cc
Normal file
134
ortools/routing/nearp_parser_test.cc
Normal file
@@ -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 <string>
|
||||
|
||||
#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
|
||||
106
ortools/routing/pdtsp_parser.cc
Normal file
106
ortools/routing/pdtsp_parser.cc
Normal file
@@ -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 <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<int64_t(int, int)> PdTspParser::Distances() const {
|
||||
std::function<int64_t(int, int)> 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<std::string> 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
|
||||
59
ortools/routing/pdtsp_parser.h
Normal file
59
ortools/routing/pdtsp_parser.h
Normal file
@@ -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 <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<int64_t(int, int)> 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<double> x_;
|
||||
std::vector<double> y_;
|
||||
std::vector<int> deliveries_;
|
||||
};
|
||||
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_ROUTING_PDTSP_PARSER_H_
|
||||
50
ortools/routing/pdtsp_parser_test.cc
Normal file
50
ortools/routing/pdtsp_parser_test.cc
Normal file
@@ -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 <functional>
|
||||
#include <string>
|
||||
|
||||
#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<int64_t(int, int)> 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
|
||||
81
ortools/routing/samples/BUILD.bazel
Normal file
81
ortools/routing/samples/BUILD.bazel
Normal file
@@ -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",
|
||||
],
|
||||
)
|
||||
197
ortools/routing/samples/cvrp_disjoint_tw.cc
Normal file
197
ortools/routing/samples/cvrp_disjoint_tw.cc
Normal file
@@ -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 <algorithm>
|
||||
#include <cstdint>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#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<int64_t> forbid_points(2 * absl::GetFlag(FLAGS_vrp_windows), 0);
|
||||
for (int i = 0; i < forbid_points.size(); ++i) {
|
||||
forbid_points[i] = absl::Uniform<int32_t>(randomizer, 0, kHorizon);
|
||||
}
|
||||
std::sort(forbid_points.begin(), forbid_points.end());
|
||||
std::vector<int64_t> forbid_starts(1, 0);
|
||||
std::vector<int64_t> 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<int64_t> 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<int64_t> 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;
|
||||
}
|
||||
24
ortools/routing/samples/cvrp_disjoint_tw_test.sh
Executable file
24
ortools/routing/samples/cvrp_disjoint_tw_test.sh
Executable file
@@ -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 "$@"
|
||||
182
ortools/routing/samples/cvrptw.cc
Normal file
182
ortools/routing/samples/cvrptw.cc
Normal file
@@ -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 <cstdint>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#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<int32_t>(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<int64_t> 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<int64_t> 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;
|
||||
}
|
||||
206
ortools/routing/samples/cvrptw_soft_capacity.cc
Normal file
206
ortools/routing/samples/cvrptw_soft_capacity.cc
Normal file
@@ -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 <cstdint>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#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<int32_t>(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<int64_t> 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<int64_t> 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;
|
||||
}
|
||||
24
ortools/routing/samples/cvrptw_test.sh
Executable file
24
ortools/routing/samples/cvrptw_test.sh
Executable file
@@ -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 "$@"
|
||||
237
ortools/routing/samples/cvrptw_with_breaks.cc
Normal file
237
ortools/routing/samples/cvrptw_with_breaks.cc
Normal file
@@ -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 <cstdint>
|
||||
#include <random>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<int32_t>(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<int64_t> 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<std::vector<int>> 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<IntervalVar*> 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<int64_t> 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;
|
||||
}
|
||||
212
ortools/routing/samples/cvrptw_with_precedences.cc
Normal file
212
ortools/routing/samples/cvrptw_with_precedences.cc
Normal file
@@ -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 <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<int32_t>(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<int64_t> 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<int64_t> 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<std::pair<int, int>> 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<int, int>& 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;
|
||||
}
|
||||
193
ortools/routing/samples/cvrptw_with_refueling.cc
Normal file
193
ortools/routing/samples/cvrptw_with_refueling.cc
Normal file
@@ -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 <cstdint>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#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<int32_t>(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<int64_t> 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;
|
||||
}
|
||||
25
ortools/routing/samples/cvrptw_with_refueling_test.sh
Executable file
25
ortools/routing/samples/cvrptw_with_refueling_test.sh
Executable file
@@ -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 "$@"
|
||||
188
ortools/routing/samples/cvrptw_with_resources.cc
Normal file
188
ortools/routing/samples/cvrptw_with_resources.cc
Normal file
@@ -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 <cstdint>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#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<int32_t>(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<IntVar*> 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<IntervalVar*> 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<int64_t> 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<int64_t> 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;
|
||||
}
|
||||
25
ortools/routing/samples/cvrptw_with_resources_test.sh
Executable file
25
ortools/routing/samples/cvrptw_with_resources_test.sh
Executable file
@@ -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 "$@"
|
||||
224
ortools/routing/samples/cvrptw_with_stop_times_and_resources.cc
Normal file
224
ortools/routing/samples/cvrptw_with_stop_times_and_resources.cc
Normal file
@@ -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 <cstdint>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#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<int32_t>(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<IntervalVar*> intervals;
|
||||
for (int stop = 0; stop < absl::GetFlag(FLAGS_vrp_stops); ++stop) {
|
||||
std::vector<IntervalVar*> 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<int64_t> 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<int64_t> 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;
|
||||
}
|
||||
25
ortools/routing/samples/cvrptw_with_stop_times_and_resources_test.sh
Executable file
25
ortools/routing/samples/cvrptw_with_stop_times_and_resources_test.sh
Executable file
@@ -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 "$@"
|
||||
249
ortools/routing/samples/cvrptw_with_time_dependent_costs.cc
Normal file
249
ortools/routing/samples/cvrptw_with_time_dependent_costs.cc
Normal file
@@ -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 <cmath>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#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<double>(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<int64_t(int64_t)> 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<int32_t>(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<int64_t> 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;
|
||||
}
|
||||
24
ortools/routing/samples/cvrptw_with_time_dependent_costs.sh
Executable file
24
ortools/routing/samples/cvrptw_with_time_dependent_costs.sh
Executable file
@@ -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 "$@"
|
||||
21
ortools/routing/simple_graph.cc
Normal file
21
ortools/routing/simple_graph.cc
Normal file
@@ -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
|
||||
155
ortools/routing/simple_graph.h
Normal file
155
ortools/routing/simple_graph.h
Normal file
@@ -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 <algorithm>
|
||||
#include <functional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
#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 <typename H>
|
||||
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 <typename H>
|
||||
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<int64_t(int, int)> EdgeWeights;
|
||||
|
||||
// Real-world coordinates.
|
||||
template <typename T>
|
||||
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 <typename H>
|
||||
friend H AbslHashValue(H h, const Coordinates2& coordinates) {
|
||||
return H::combine(std::move(h), coordinates.x, coordinates.y);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
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 <typename H>
|
||||
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 <typename T>
|
||||
struct SimpleTimeWindow {
|
||||
T start;
|
||||
T end;
|
||||
};
|
||||
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_ROUTING_SIMPLE_GRAPH_H_
|
||||
103
ortools/routing/simple_graph_test.cc
Normal file
103
ortools/routing/simple_graph_test.cc
Normal file
@@ -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 <sstream>
|
||||
#include <string>
|
||||
|
||||
#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<int>(0, 0), Coordinates2<int>(0, 0));
|
||||
EXPECT_NE(Coordinates2<int>(0, 0), Coordinates2<int>(1, 2));
|
||||
|
||||
std::stringstream generated;
|
||||
generated << Coordinates2<int>(0, 1);
|
||||
EXPECT_EQ(generated.str(), "{x = 0, y = 1}");
|
||||
|
||||
EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly(
|
||||
{Coordinates2<int>(), Coordinates2<int>(0, 0), Coordinates2<int>(1, 2)}));
|
||||
}
|
||||
|
||||
TEST(Coordinates2Test, Double) {
|
||||
EXPECT_EQ(Coordinates2<double>(0, 0), Coordinates2<double>(0, 0));
|
||||
EXPECT_NE(Coordinates2<double>(0, 0), Coordinates2<double>(1, 2));
|
||||
|
||||
std::stringstream generated;
|
||||
generated << Coordinates2<double>(0.0, 1.0);
|
||||
EXPECT_EQ(generated.str(), "{x = 0, y = 1}");
|
||||
|
||||
EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly(
|
||||
{Coordinates2<double>(), Coordinates2<double>(0, 0),
|
||||
Coordinates2<double>(1, 2)}));
|
||||
}
|
||||
|
||||
TEST(Coordinates3Test, Int) {
|
||||
EXPECT_EQ(Coordinates3<int>(0, 0, 0), Coordinates3<int>(0, 0, 0));
|
||||
EXPECT_NE(Coordinates3<int>(0, 0, 0), Coordinates3<int>(1, 2, 3));
|
||||
|
||||
std::stringstream generated;
|
||||
generated << Coordinates3<int>(0, 1, 2);
|
||||
EXPECT_EQ(generated.str(), "{x = 0, y = 1, z = 2}");
|
||||
|
||||
EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly(
|
||||
{Coordinates3<int>(), Coordinates3<int>(0, 0, 0),
|
||||
Coordinates3<int>(1, 2, 3)}));
|
||||
}
|
||||
|
||||
TEST(Coordinates3Test, Double) {
|
||||
EXPECT_EQ(Coordinates3<double>(0, 0, 0), Coordinates3<double>(0, 0, 0));
|
||||
EXPECT_NE(Coordinates3<double>(0, 0, 0), Coordinates3<double>(1, 2, 3));
|
||||
|
||||
std::stringstream generated;
|
||||
generated << Coordinates3<double>(0.0, 1.0, 2.0);
|
||||
EXPECT_EQ(generated.str(), "{x = 0, y = 1, z = 2}");
|
||||
|
||||
EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly(
|
||||
{Coordinates3<double>(), Coordinates3<double>(0.0, 0.0, 0.0),
|
||||
Coordinates3<double>(1.0, 2.0, 3.0)}));
|
||||
}
|
||||
} // namespace
|
||||
} // namespace operations_research
|
||||
137
ortools/routing/solomon_parser.cc
Normal file
137
ortools/routing/solomon_parser.cc
Normal file
@@ -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 <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<zipfile::ZipArchive> 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<std::string> 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<int64_t> 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
|
||||
138
ortools/routing/solomon_parser.h
Normal file
138
ortools/routing/solomon_parser.h
Normal file
@@ -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:
|
||||
//
|
||||
// <instance_name>
|
||||
// VEHICLE
|
||||
// NUMBER CAPACITY
|
||||
// <number of nodes> <vehicle capacity>
|
||||
// CUSTOMER
|
||||
// CUST NO. XCOORD. YCOORD. DEMAND READY TIME DUE DATE SERVICE TIME
|
||||
// <node id> <x> <y> <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 <math.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<Coordinates2<int64_t>>& 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<int64_t>& demands() const { return demands_; }
|
||||
// Returns the time windows of the nodes in the current routing problem.
|
||||
const std::vector<SimpleTimeWindow<int64_t>>& time_windows() const {
|
||||
return time_windows_;
|
||||
}
|
||||
// Returns the service times of the nodes in the current routing problem.
|
||||
const std::vector<int64_t>& service_times() const { return service_times_; }
|
||||
// Returns the distance between two nodes.
|
||||
double GetDistance(int from, int to) const {
|
||||
const Coordinates2<int64_t>& from_coords = coordinates_[from];
|
||||
const Coordinates2<int64_t>& 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<std::string, Section> 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<Coordinates2<int64_t>> coordinates_;
|
||||
int64_t capacity_;
|
||||
std::vector<int64_t> demands_;
|
||||
std::vector<SimpleTimeWindow<int64_t>> time_windows_;
|
||||
std::vector<int64_t> service_times_;
|
||||
};
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_ROUTING_SOLOMON_PARSER_H_
|
||||
68
ortools/routing/solomon_parser_test.cc
Normal file
68
ortools/routing/solomon_parser_test.cc
Normal file
@@ -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 <memory>
|
||||
#include <string>
|
||||
|
||||
#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
|
||||
424
ortools/routing/solution_serializer.cc
Normal file
424
ortools/routing/solution_serializer.cc
Normal file
@@ -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 <algorithm>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#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<RoutingSolution::Route> RoutesFromVector(
|
||||
const std::vector<std::vector<int64_t>>& routes,
|
||||
std::optional<int64_t> depot = std::nullopt);
|
||||
} // namespace
|
||||
|
||||
std::vector<std::vector<int64_t>> RoutingSolution::SplitRoutes(
|
||||
const std::vector<int64_t>& 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<std::vector<int64_t>> routes;
|
||||
int64_t current_route = 0;
|
||||
for (int64_t node : solution) {
|
||||
if (routes.size() == current_route) {
|
||||
routes.emplace_back(std::vector<int64_t>());
|
||||
}
|
||||
|
||||
if (node == separator) {
|
||||
current_route += 1;
|
||||
} else {
|
||||
routes[current_route].emplace_back(node);
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
RoutingSolution RoutingSolution::FromSplitRoutes(
|
||||
const std::vector<std::vector<int64_t>>& routes,
|
||||
std::optional<int64_t> depot) {
|
||||
std::vector<int64_t> total_demands(routes.size(), -1);
|
||||
std::vector<int64_t> 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<int64_t>& route_int,
|
||||
std::optional<int64_t> depot = std::nullopt);
|
||||
|
||||
std::vector<RoutingSolution::Route> RoutesFromVector(
|
||||
const std::vector<std::vector<int64_t>>& routes,
|
||||
std::optional<int64_t> depot) {
|
||||
std::vector<RoutingSolution::Route> solution_routes;
|
||||
solution_routes.reserve(routes.size());
|
||||
for (const std::vector<int64_t>& 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<int64_t>& route_int,
|
||||
std::optional<int64_t> 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
|
||||
294
ortools/routing/solution_serializer.h
Normal file
294
ortools/routing/solution_serializer.h
Normal file
@@ -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 <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<Event>;
|
||||
|
||||
RoutingSolution(std::vector<Route> routes, std::vector<int64_t> total_demands,
|
||||
std::vector<int64_t> total_distances, int64_t total_cost = -1,
|
||||
int64_t total_distance = -1, double total_time = -1.0,
|
||||
std::string_view name = "")
|
||||
: 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<std::vector<int64_t>> SplitRoutes(
|
||||
const std::vector<int64_t>& 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<std::vector<int64_t>>& routes,
|
||||
std::optional<int64_t> 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<std::vector<Event>> routes_;
|
||||
std::vector<int64_t> total_demands_;
|
||||
std::vector<int64_t> 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 <typename T>
|
||||
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 <typename T>
|
||||
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_
|
||||
611
ortools/routing/solution_serializer_test.cc
Normal file
611
ortools/routing/solution_serializer_test.cc
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::vector<int64_t>> routes{{0, 1, 3, 0}};
|
||||
const RoutingSolution result = RoutingSolution::FromSplitRoutes(routes);
|
||||
|
||||
const RoutingSolution expected_output = RoutingSolution{
|
||||
std::vector<RoutingSolution::Route>{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<int64_t>{-1},
|
||||
std::vector<int64_t>{-1},
|
||||
};
|
||||
EXPECT_EQ(result, expected_output);
|
||||
}
|
||||
|
||||
TEST(RoutingSolutionSerializerTest, FromSplitRoutesWithTwoRoutes) {
|
||||
// Specifically test RoutesFromVector in the implementation.
|
||||
const std::vector<std::vector<int64_t>> routes{
|
||||
{0, 1, 3, 0},
|
||||
{0, 2, 0},
|
||||
};
|
||||
const RoutingSolution result = RoutingSolution::FromSplitRoutes(routes);
|
||||
|
||||
const RoutingSolution expected_output = {RoutingSolution{
|
||||
std::vector<RoutingSolution::Route>{
|
||||
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<int64_t>{-1, -1},
|
||||
std::vector<int64_t>{-1, -1},
|
||||
}};
|
||||
|
||||
EXPECT_EQ(result, expected_output);
|
||||
}
|
||||
|
||||
TEST(RoutingSolutionSerializerTest, SolutionToTsplib) {
|
||||
const std::vector<int64_t> 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<std::vector<int64_t>> 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<int64_t> 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<RoutingSolution::Route> 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<RoutingSolution::Route> 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<int64_t> 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<int64_t> 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<std::vector<int64_t>> 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>{
|
||||
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<int64_t>{5, 5, 5, 5, 2},
|
||||
std::vector<int64_t>{76, 60, 86, 53, 41},
|
||||
7,
|
||||
6,
|
||||
30.84};
|
||||
}
|
||||
|
||||
RoutingSolution MakeTestEdgeNodeArcRoutingInstance() {
|
||||
using Event = RoutingSolution::Event;
|
||||
using Type = Event::Type;
|
||||
return {std::vector<RoutingSolution::Route>{
|
||||
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<int64_t>{5, 5, 5, 5, 2},
|
||||
std::vector<int64_t>{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
|
||||
855
ortools/routing/tsplib_parser.cc
Normal file
855
ortools/routing/tsplib_parser.cc
Normal file
@@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<double>& from,
|
||||
const Coordinates3<double>& 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<double>& from,
|
||||
const Coordinates3<double>& 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<double>& from,
|
||||
const Coordinates3<double>& to) {
|
||||
return std::round(DoubleEuc2DDistance(from, to));
|
||||
}
|
||||
|
||||
static int64_t Euc3DDistance(const Coordinates3<double>& from,
|
||||
const Coordinates3<double>& 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<double>& from,
|
||||
const Coordinates3<double>& to) {
|
||||
return static_cast<int64_t>(ceil(DoubleEuc2DDistance(from, to)));
|
||||
}
|
||||
|
||||
static double ToRad(double x) {
|
||||
static const double kPi = 3.141592;
|
||||
const int64_t deg = static_cast<int64_t>(x);
|
||||
const double min = x - deg;
|
||||
return kPi * (deg + 5.0 * min / 3.0) / 180.0;
|
||||
}
|
||||
|
||||
static int64_t GeoDistance(const Coordinates3<double>& from,
|
||||
const Coordinates3<double>& 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<int64_t>(
|
||||
kRadius * acos(0.5 * ((1.0 + q1) * q2 - (1.0 - q1) * q3)) + 1.0);
|
||||
}
|
||||
|
||||
static int64_t GeoMDistance(const Coordinates3<double>& from,
|
||||
const Coordinates3<double>& 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<int64_t>(kRadius * atan2(sqrt(q1 * q1 + q2 * q2), q5) +
|
||||
1.0);
|
||||
}
|
||||
|
||||
static int64_t Man2DDistance(const Coordinates3<double>& from,
|
||||
const Coordinates3<double>& 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<double>& from,
|
||||
const Coordinates3<double>& 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<double>& from,
|
||||
const Coordinates3<double>& 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<double>& from,
|
||||
const Coordinates3<double>& 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<zipfile::ZipArchive> 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<zipfile::ZipArchive> 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<zipfile::ZipArchive> 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string> 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<std::vector<int>>& 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<std::string, TspLibParser::Sections>* const
|
||||
TspLibParser::kSections =
|
||||
new absl::flat_hash_map<std::string, TspLibParser::Sections>(
|
||||
{{"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<std::string, TspLibParser::Types>* const
|
||||
TspLibParser::kTypes =
|
||||
new absl::flat_hash_map<std::string, TspLibParser::Types>(
|
||||
{{"TSP", Types::TSP},
|
||||
{"ATSP", Types::ATSP},
|
||||
{"SOP", Types::SOP},
|
||||
{"HCP", Types::HCP},
|
||||
{"CVRP", Types::CVRP},
|
||||
{"TOUR", Types::TOUR}});
|
||||
|
||||
const absl::flat_hash_map<std::string, TspLibParser::EdgeDataFormat>* const
|
||||
TspLibParser::kEdgeDataFormats =
|
||||
new absl::flat_hash_map<std::string, TspLibParser::EdgeDataFormat>(
|
||||
{{"EDGE_LIST", EDGE_LIST}, {"ADJ_LIST", ADJ_LIST}});
|
||||
|
||||
const absl::flat_hash_map<std::string, TspLibParser::EdgeWeightTypes>* const
|
||||
TspLibParser::kEdgeWeightTypes =
|
||||
new absl::flat_hash_map<std::string, TspLibParser::EdgeWeightTypes>(
|
||||
{{"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<std::string, TspLibParser::EdgeWeightFormats>* const
|
||||
TspLibParser::kEdgeWeightFormats =
|
||||
new absl::flat_hash_map<std::string, TspLibParser::EdgeWeightFormats>(
|
||||
{{"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<zipfile::ZipArchive> 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<std::string> 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<std::string, TspLibTourParser::Sections>* const
|
||||
TspLibTourParser::kSections =
|
||||
new absl::flat_hash_map<std::string, TspLibTourParser::Sections>(
|
||||
{{"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<zipfile::ZipArchive> 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<std::string> 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
|
||||
253
ortools/routing/tsplib_parser.h
Normal file
253
ortools/routing/tsplib_parser.h
Normal file
@@ -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 <functional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<Coordinates3<double>>& 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<int64_t>& demands() const { return demands_; }
|
||||
// Returns the pairs of nodes corresponding to forced edges (second node is
|
||||
// directly after the first).
|
||||
const std::set<std::pair<int, int>> 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<std::vector<int>>& 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<std::vector<int>>& 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<std::string>& words);
|
||||
void ParseExplicitUpperRow(const std::vector<std::string>& words);
|
||||
void ParseExplicitLowerRow(const std::vector<std::string>& words);
|
||||
void ParseExplicitUpperDiagRow(const std::vector<std::string>& words);
|
||||
void ParseExplicitLowerDiagRow(const std::vector<std::string>& words);
|
||||
void ParseNodeCoord(const std::vector<std::string>& words);
|
||||
void SetUpEdgeWeightSection();
|
||||
void FinalizeEdgeWeights();
|
||||
void ParseSections(const std::vector<std::string>& 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<int64_t> demands_;
|
||||
EdgeWeights distance_function_;
|
||||
std::vector<int64_t> explicit_costs_;
|
||||
std::set<std::pair<int, int>> fixed_edges_;
|
||||
int depot_;
|
||||
std::vector<std::vector<int>> edges_;
|
||||
|
||||
// Parsing data
|
||||
static const absl::flat_hash_map<std::string, Sections>* const kSections;
|
||||
Sections section_;
|
||||
static const absl::flat_hash_map<std::string, Types>* const kTypes;
|
||||
Types type_;
|
||||
static const absl::flat_hash_map<std::string, EdgeDataFormat>* const
|
||||
kEdgeDataFormats;
|
||||
EdgeDataFormat edge_data_format_;
|
||||
static const absl::flat_hash_map<std::string, EdgeWeightTypes>* const
|
||||
kEdgeWeightTypes;
|
||||
EdgeWeightTypes edge_weight_type_;
|
||||
static const absl::flat_hash_map<std::string, EdgeWeightFormats>* const
|
||||
kEdgeWeightFormats;
|
||||
EdgeWeightFormats edge_weight_format_;
|
||||
int edge_row_;
|
||||
int edge_column_;
|
||||
std::vector<Coordinates3<double>> 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<int>& 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<std::string, Sections>* const kSections;
|
||||
Sections section_;
|
||||
std::string comments_;
|
||||
int64_t size_;
|
||||
std::vector<int> 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<std::vector<int>>& tours() const { return tours_; }
|
||||
int64_t cost() const { return cost_; }
|
||||
|
||||
private:
|
||||
void ProcessNewLine(const std::string& line);
|
||||
|
||||
std::vector<std::vector<int>> tours_;
|
||||
int64_t cost_;
|
||||
};
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_ROUTING_TSPLIB_PARSER_H_
|
||||
604
ortools/routing/tsplib_parser_test.cc
Normal file
604
ortools/routing/tsplib_parser_test.cc
Normal file
@@ -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 <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<absl::string_view> exceptions = {"xray.problems",
|
||||
"tspleap.c"};
|
||||
// Parsing from tar archive
|
||||
int file_index = 0;
|
||||
for (const char* const archive : kArchives) {
|
||||
std::vector<std::string> 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<zipfile::ZipArchive> zip_archive(zipfile::OpenZipArchive(
|
||||
file::JoinPath(absl::GetFlag(FLAGS_test_srcdir), archive)));
|
||||
ASSERT_NE(nullptr, zip_archive);
|
||||
std::vector<std::string> 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<std::string> 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<std::string> 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
|
||||
213
ortools/routing/tsptw_parser.cc
Normal file
213
ortools/routing/tsptw_parser.cc
Normal file
@@ -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 <cmath>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<double>& from,
|
||||
const Coordinates2<double>& 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<double>& from,
|
||||
const Coordinates2<double>& to) {
|
||||
return std::floor(DoubleEuc2DDistance(from, to));
|
||||
}
|
||||
|
||||
constexpr double kInfinity = std::numeric_limits<double>::infinity();
|
||||
|
||||
std::shared_ptr<zipfile::ZipArchive> 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<zipfile::ZipArchive> 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<std::string> 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<double> 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<std::string> 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<double> values;
|
||||
for (int i = 1; i < words.size(); ++i) {
|
||||
const double value = strings::ParseLeadingDoubleValue(
|
||||
words[i], std::numeric_limits<double>::infinity());
|
||||
if (value == std::numeric_limits<double>::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
|
||||
88
ortools/routing/tsptw_parser.h
Normal file
88
ortools/routing/tsptw_parser.h
Normal file
@@ -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 <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<double(int, int)>& 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<double(int, int)>& 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<Coordinates2<double>>& coordinates() const {
|
||||
return coords_;
|
||||
}
|
||||
// Returns the time windows of the nodes in the current routing problem.
|
||||
const std::vector<SimpleTimeWindow<double>>& time_windows() const {
|
||||
return time_windows_;
|
||||
}
|
||||
// Returns the service times of the nodes in the current routing problem.
|
||||
const std::vector<double>& 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<double(int, int)> distance_function_;
|
||||
std::function<double(int, int)> time_function_;
|
||||
std::vector<Coordinates2<double>> coords_;
|
||||
std::vector<SimpleTimeWindow<double>> time_windows_;
|
||||
std::vector<double> service_times_;
|
||||
std::vector<double> distance_matrix_;
|
||||
};
|
||||
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_ROUTING_TSPTW_PARSER_H_
|
||||
72
ortools/routing/tsptw_parser_test.cc
Normal file
72
ortools/routing/tsptw_parser_test.cc
Normal file
@@ -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 <string>
|
||||
|
||||
#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
|
||||
Reference in New Issue
Block a user