From 2ee551d6357fc2f7f7d5588d102d947e930aea5f Mon Sep 17 00:00:00 2001 From: halilsen Date: Thu, 3 Feb 2022 14:25:28 +0100 Subject: [PATCH 01/30] Fix compiler warnings and uninitialized variables Remaining 4 warnings are because of protobuf auto-generated files due to a protobuf bug which is fixed at v3.14 and the warnings will be disappear once the or-tools version is increased to v9+ which upgrades the protobuf dependency. https://github.com/protocolbuffers/protobuf/issues/7140#issuecomment-701014222 --- Dockerfile | 2 +- Makefile | 11 +++-- limits.h | 8 ++-- tsp_simple.cc | 4 +- tsptw_data_dt.h | 104 +++++++++++++++++++++++++----------------------- 5 files changed, 68 insertions(+), 61 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6a793a0d..7a00957a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,4 +30,4 @@ RUN wget -qO- $ORTOOLS_URL | tar xz --strip-components=1 -C /srv/or-tools ADD . /srv/optimizer-ortools WORKDIR /srv/optimizer-ortools -RUN make -j3 tsp_simple +RUN make tsp_simple diff --git a/Makefile b/Makefile index 7957b319..d9c37d21 100644 --- a/Makefile +++ b/Makefile @@ -2,18 +2,21 @@ OR_TOOLS_TOP=../or-tools TUTORIAL=resources -CFLAGS := -std=c++14 -I $(OR_TOOLS_TOP)/include +# -isystem prevents most of the warnings rooted in or-tools library appearing in our compilation +CFLAGS := -std=c++14 -isystem$(OR_TOOLS_TOP)/include # During development uncomment the next line to have debug checks and other verifications # DEVELOPMENT = true ifeq ($(DEVELOPMENT), true) - # -isystem prevents warnings rooted in or-tools library appearing in our compilation - CFLAGS := $(CFLAGS) -O0 -DDEBUG -ggdb3 -fsanitize=address -fkeep-inline-functions -fno-inline-small-functions -Wall -Wextra -Wshadow -Wunreachable-code -Winit-self -Wmissing-include-dirs -Wswitch-enum -Wfloat-equal -Wundef -isystem$(OR_TOOLS_TOP)/. -isystem$(OR_TOOLS_TOP)/include - CXX := LSAN_OPTION=verbosity=1:log_threads=1 $(CXX) + CFLAGS := $(CFLAGS) -O0 -DDEBUG -ggdb3 -fsanitize=address -fkeep-inline-functions -fno-inline-small-functions + CXX := LSAN_OPTION=verbosity=1:log_threads=1 $(CXX) # adress sanitizer works only if the executable launched without gdb else CFLAGS := $(CFLAGS) -O3 -DNDEBUG endif +# Activate warnings +CFLAGS := $(CFLAGS) -Wall -Wextra -Wshadow -Wmissing-include-dirs -Wswitch-enum -Wfloat-equal -Wundef + .PHONY: all local_clean all: $(EXE) diff --git a/limits.h b/limits.h index fa1fe66d..31a56487 100644 --- a/limits.h +++ b/limits.h @@ -90,8 +90,7 @@ class NoImprovementLimit : public SearchLimit { best_result_ = kint64min; } - DCHECK_NOTNULL(objective_var); - prototype_->AddObjective(objective_var); + prototype_->AddObjective(DCHECK_NOTNULL(objective_var)); } virtual void Init() { @@ -145,7 +144,7 @@ class NoImprovementLimit : public SearchLimit { virtual void Copy(const SearchLimit* const limit) { const NoImprovementLimit* const copy_limit = - reinterpret_cast(limit); + reinterpret_cast(limit); best_result_ = copy_limit->best_result_; solution_nbr_tolerance_ = copy_limit->solution_nbr_tolerance_; @@ -597,8 +596,7 @@ class LoggerMonitor : public SearchMonitor { } virtual void Copy(const SearchLimit* const limit) { - const LoggerMonitor* const copy_limit = - reinterpret_cast(limit); + const LoggerMonitor* const copy_limit = reinterpret_cast(limit); best_result_ = copy_limit->best_result_; cleaned_cost_ = copy_limit->cleaned_cost_; diff --git a/tsp_simple.cc b/tsp_simple.cc index 2316d54f..c5816150 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -314,7 +314,7 @@ RestBuilder(const TSPTWDataDT& data, RoutingModel& routing, const int64 horizon) } void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, - RoutingIndexManager& manager, bool& has_overall_duration) { + bool& has_overall_duration) { Solver* solver = routing.solver(); // const int size_vehicles = data.Vehicles().size(); @@ -1520,7 +1520,7 @@ const ortools_result::Result* TSPTWSolver(const TSPTWDataDT& data, min_start); std::vector> stored_rests = RestBuilder(data, routing, horizon); - RelationBuilder(data, routing, manager, has_overall_duration); + RelationBuilder(data, routing, has_overall_duration); RoutingSearchParameters parameters = DefaultRoutingSearchParameters(); CHECK(google::protobuf::TextFormat::MergeFromString( diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 6cc1ebaa..213fe21e 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -45,7 +45,29 @@ namespace operations_research { class TSPTWDataDT { public: - explicit TSPTWDataDT(const std::string& filename) { LoadInstance(filename); } + explicit TSPTWDataDT(const std::string& filename) + : size_problem_(0) + , size_(0) + , size_matrix_(0) + , size_missions_(0) + , size_rest_(0) + , deliveries_counter_(0) + , horizon_(0) + , max_distance_(0) + , max_distance_cost_(0) + , max_rest_(0) + , max_service_(0) + , max_time_(0) + , max_time_cost_(0) + , max_value_(0) + , max_value_cost_(0) + , multiple_tws_counter_(0) + , sum_max_time_(0) + , tws_counter_(0) + , max_coef_service_(0) + , max_coef_setup_(0) { + LoadInstance(filename); + } void LoadInstance(const std::string& filename); @@ -185,7 +207,7 @@ class TSPTWDataDT { } bool AllServicesHaveEnd() const { - for (int i = 0; i < tsptw_clients_.size(); i++) { + for (std::size_t i = 0; i < tsptw_clients_.size(); i++) { if (tsptw_clients_[i].due_time.size() == 0) return false; } @@ -274,7 +296,7 @@ class TSPTWDataDT { std::vector MaxTimes(const ortools_vrp::Matrix& matrix) const { int64 max_row; - int32 size_matrix = sqrt(matrix.time_size()); + int32 size_matrix = sqrt(matrix.time_size()); std::vector max_times; for (int32 i = 0; i < size_matrix; i++) { max_row = 0; @@ -571,7 +593,7 @@ class TSPTWDataDT { } bool AllVehiclesHaveEnd() { - for (int v = 0; v < tsptw_vehicles_.size(); v++) { + for (std::size_t v = 0; v < tsptw_vehicles_.size(); v++) { if (!VehicleHasEnd(v)) return false; } @@ -702,11 +724,26 @@ class TSPTWDataDT { bool is_break; }; + uint32 size_problem_; int32 size_; - int32 size_missions_; int32 size_matrix_; + int32 size_missions_; int32 size_rest_; - uint32 size_problem_; + int64 deliveries_counter_; + int64 horizon_; + int64 max_distance_; + int64 max_distance_cost_; + int64 max_rest_; + int64 max_service_; + int64 max_time_; + int64 max_time_cost_; + int64 max_value_; + int64 max_value_cost_; + int64 multiple_tws_counter_; + int64 sum_max_time_; + int64 tws_counter_; + float max_coef_service_; + float max_coef_setup_; std::vector tws_size_; std::vector tsptw_vehicles_; std::vector tsptw_relations_; @@ -719,21 +756,6 @@ class TSPTWDataDT { std::vector vehicles_day_; std::vector service_times_; std::string details_; - int64 horizon_; - int64 max_time_; - int64 sum_max_time_; - int64 max_distance_; - int64 max_value_; - int64 max_time_cost_; - int64 max_distance_cost_; - int64 max_value_cost_; - float max_coef_service_; - float max_coef_setup_; - int64 max_service_; - int64 max_rest_; - int64 tws_counter_; - int64 deliveries_counter_; - int64 multiple_tws_counter_; std::map ids_map_; std::map vehicle_ids_map_; std::map day_index_to_vehicle_index_; @@ -751,12 +773,8 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { } } - int32 node_index = 0; - tws_counter_ = 0; - multiple_tws_counter_ = 0; - deliveries_counter_ = 0; - int32 matrix_index = 0; - size_problem_ = 0; + int32 node_index = 0; + int32 matrix_index = 0; std::vector matrix_indices; for (const ortools_vrp::Service& service : problem.services()) { if (!alternative_size_map_.count(service.problem_index())) @@ -877,7 +895,6 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { ++matrix_index; } - size_rest_ = 0; for (const ortools_vrp::Vehicle& vehicle : problem.vehicles()) { service_times_.push_back(0); service_times_.push_back(0); @@ -888,14 +905,6 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { size_missions_ = node_index; size_ = node_index + 2; - max_time_ = 0; - max_distance_ = 0; - max_value_ = 0; - max_time_cost_ = 0; - max_distance_cost_ = 0; - max_value_cost_ = 0; - sum_max_time_ = 0; - for (const ortools_vrp::Matrix& matrix : problem.matrices()) { // + 2 In case vehicles have no depots int32 problem_size = @@ -922,9 +931,9 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { // Estimate necessary horizon due to time matrix std::vector max_times(MaxTimes(matrix)); - int64 matrix_sum_time = 0; + int64 matrix_sum_time = 0; if (sqrt(matrix.time_size()) > 0) { - for (int64 i = 0; i < matrix_indices.size(); i++) { + for (std::size_t i = 0; i < matrix_indices.size(); i++) { matrix_sum_time += max_times.at(matrix_indices[i]); } } @@ -1112,29 +1121,26 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { } // Compute horizon - horizon_ = 0; - max_service_ = 0; - max_rest_ = 0; int64 rest_duration; - for (int32 v = 0; v < tsptw_vehicles_.size(); v++) { + for (std::size_t v = 0; v < tsptw_vehicles_.size(); v++) { rest_duration = 0; - for (int32 r = 0; r < tsptw_vehicles_[v].Rests().size(); r++) { + for (std::size_t r = 0; r < tsptw_vehicles_[v].Rests().size(); r++) { rest_duration += tsptw_vehicles_[v].Rests()[r].duration; } max_rest_ = std::max(max_rest_, rest_duration); } if (AllVehiclesHaveEnd()) { - for (int32 v = 0; v < tsptw_vehicles_.size(); v++) { + for (std::size_t v = 0; v < tsptw_vehicles_.size(); v++) { horizon_ = std::max(horizon_, tsptw_vehicles_[v].time_end + tsptw_vehicles_[v].time_maximum_lateness); } } else if (AllServicesHaveEnd()) { - for (int32 i = 0; i < tsptw_clients_.size(); i++) { + for (std::size_t i = 0; i < tsptw_clients_.size(); i++) { horizon_ = std::max(horizon_, tsptw_clients_[i].due_time.back() + tsptw_clients_[i].maximum_lateness.back()); } - for (int32 v = 0; v < tsptw_vehicles_.size(); v++) { - for (int32 r = 0; r < tsptw_vehicles_[v].Rests().size(); r++) { + for (std::size_t v = 0; v < tsptw_vehicles_.size(); v++) { + for (std::size_t r = 0; r < tsptw_vehicles_[v].Rests().size(); r++) { horizon_ = std::max(horizon_, tsptw_vehicles_[v].Rests()[r].due_time); } } @@ -1150,10 +1156,10 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { latest_start = std::max(latest_start, tsptw_clients_[i].ready_time.back()); } } - for (int32 v = 0; v < tsptw_vehicles_.size(); v++) { + for (std::size_t v = 0; v < tsptw_vehicles_.size(); v++) { latest_start = std::max(latest_start, tsptw_vehicles_[v].time_start); - for (int32 r = 0; r < tsptw_vehicles_[v].Rests().size(); r++) { + for (std::size_t r = 0; r < tsptw_vehicles_[v].Rests().size(); r++) { latest_rest_end = std::max(latest_rest_end, tsptw_vehicles_[v].Rests()[r].due_time); } From a71c37dff86b337bbd139fe0086fca6694a00a3d Mon Sep 17 00:00:00 2001 From: halilsen Date: Thu, 3 Feb 2022 19:22:46 +0100 Subject: [PATCH 02/30] Deactivate Warray-bounds until protobuf is updated --- Makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Makefile b/Makefile index d9c37d21..658f31b8 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,13 @@ endif # Activate warnings CFLAGS := $(CFLAGS) -Wall -Wextra -Wshadow -Wmissing-include-dirs -Wswitch-enum -Wfloat-equal -Wundef +# The following is to supress a warning due to a protobuf that is fixed at v3.14. +# It can be removed when or-tools is upgraded to v8.1+ (where the protobuf dependency is upgraded to v3.14). +PROTOBUF_VERSION := $(shell $(OR_TOOLS_TOP)/bin/protoc --version | cut -d" " -f2) +ifeq ($(shell dpkg --compare-versions $(PROTOBUF_VERSION) 'lt' '3.14' && echo true), true) + CFLAGS := $(CFLAGS) -Wno-array-bounds +endif + .PHONY: all local_clean all: $(EXE) From 647bd6486360a68a926516fda423e74fa2ebaee2 Mon Sep 17 00:00:00 2001 From: halilsen Date: Thu, 2 Dec 2021 11:55:19 +0100 Subject: [PATCH 03/30] Fix: respect compatible vehicle list --- tsp_simple.cc | 35 ++++++++++++++--------------------- tsptw_data_dt.h | 4 ---- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/tsp_simple.cc b/tsp_simple.cc index c5816150..7eef91fd 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -101,6 +101,10 @@ void MissionsBuilder(const TSPTWDataDT& data, RoutingModel& routing, int64 disjunction_cost = !overflow_danger && !CheckOverflow(data_verif, size) ? data_verif : std::pow(2, 52); + std::vector all_vehicle_indices; + for (int v = 0; v < size_vehicles; ++v) + all_vehicle_indices.push_back(v); + for (int activity = 0; activity <= size_problem; ++activity) { std::vector* vect = new std::vector(); @@ -118,8 +122,7 @@ void MissionsBuilder(const TSPTWDataDT& data, RoutingModel& routing, IntVar* cumul_var = routing.GetMutableDimension(kTime)->CumulVar(index); const int64 late_multiplier = data.LateMultiplier(i); - std::vector sticky_vehicle = data.VehicleIndices(i); - std::string service_id = data.ServiceId(i); + std::string service_id = data.ServiceId(i); if (ready.size() > 0 && (ready[0] > -CUSTOM_MAX_INT || due.back() < CUSTOM_MAX_INT)) { if (absl::GetFlag(FLAGS_debug)) { @@ -163,25 +166,15 @@ void MissionsBuilder(const TSPTWDataDT& data, RoutingModel& routing, } } - if (sticky_vehicle.size() > 0) { - std::vector vehicle_indices; - std::vector vehicle_intersection; - std::vector vehicle_difference; - - for (int v = 0; v < size_vehicles; ++v) - vehicle_indices.push_back(v); - - std::set_intersection(vehicle_indices.begin(), vehicle_indices.end(), - sticky_vehicle.begin(), sticky_vehicle.end(), - std::back_inserter(vehicle_intersection)); - std::set_difference(vehicle_indices.begin(), vehicle_indices.end(), - vehicle_intersection.begin(), vehicle_intersection.end(), - std::back_inserter(vehicle_difference)); - if (vehicle_difference.size() > 0) { - for (int64 remove : vehicle_difference) - routing.VehicleVar(index)->RemoveValue(remove); - } - } + std::vector incompatible_vehicle_indices; + std::vector compatible_vehicles = data.VehicleIndices(i); + + std::set_difference(all_vehicle_indices.begin(), all_vehicle_indices.end(), + compatible_vehicles.begin(), compatible_vehicles.end(), + std::back_inserter(incompatible_vehicle_indices)); + + for (int64 remove : incompatible_vehicle_indices) + routing.VehicleVar(index)->RemoveValue(remove); const std::vector& refill_quantities = data.RefillQuantities(i); for (std::size_t q = 0; q < data.Quantities(i).size(); ++q) { diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 213fe21e..47090237 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -249,10 +249,6 @@ class TSPTWDataDT { return tsptw_clients_[i.value()].vehicle_indices; } - void SetVehicleIndices(const int64 i, std::vector&& vehicle_indices) { - tsptw_clients_[i].vehicle_indices = std::move(vehicle_indices); - } - int32 TimeWindowsSize(const int i) const { return tws_size_[i]; } int32 Size() const { return size_; } From ecc6c95e3135165d67dd8c89dbef7806a2196eaa Mon Sep 17 00:00:00 2001 From: Braktar Date: Mon, 28 Feb 2022 13:29:44 +0100 Subject: [PATCH 04/30] Setup_duration related to matrix index --- tsptw_data_dt.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 47090237..c5601ce4 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -466,9 +466,10 @@ class TSPTWDataDT { const RoutingIndexManager::NodeIndex to) const { return Time(from, to) + coef_service * data->ServiceTime(from) + additional_service + - (Time(from, to) > 0 ? coef_setup * data->SetupTime(to) + - (data->SetupTime(to) > 0 ? additional_setup : 0) - : 0); + (vehicle_indices[from.value()] != vehicle_indices[to.value()] + ? coef_setup * data->SetupTime(to) + + (data->SetupTime(to) > 0 ? additional_setup : 0) + : 0); // FIXME: // (Time(from, to) == 0 ? 0 // and @@ -481,7 +482,7 @@ class TSPTWDataDT { const RoutingIndexManager::NodeIndex to) const { return FakeTime(from, to) + coef_service * data->ServiceTime(from) + additional_service + - (FakeTime(from, to) > 0 + (vehicle_indices[from.value()] != vehicle_indices[to.value()] ? coef_setup * data->SetupTime(to) + (data->SetupTime(to) > 0 ? additional_setup : 0) : 0); From 92a4de661e76818191f91fe4ee064eab78517891 Mon Sep 17 00:00:00 2001 From: Braktar Date: Wed, 9 Mar 2022 16:39:32 +0100 Subject: [PATCH 05/30] Fix IdIndex map --- tsptw_data_dt.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index c5601ce4..9301c4f1 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -852,7 +852,8 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { service_times_.push_back(service.duration()); alternative_size_map_[service.problem_index()] += 1; - ids_map_[(std::string)service.id()] = node_index; + if (ids_map_.find((std::string)service.id()) == ids_map_.end()) + ids_map_[(std::string)service.id()] = node_index; node_index++; ++timewindow_index; } while (timewindow_index < service.time_windows_size()); From da1b9cdd3c8f24849e5d77a9ba9ffa7653642994 Mon Sep 17 00:00:00 2001 From: Braktar Date: Fri, 17 Dec 2021 09:45:19 +0100 Subject: [PATCH 06/30] Extand alternative to route position relations --- tsp_simple.cc | 54 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/tsp_simple.cc b/tsp_simple.cc index 7eef91fd..d133905c 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -607,12 +607,18 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, case NeverFirst: for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { current_index = data.IdIndex(relation.linked_ids[link_index]); + int32 service_index = + data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); + alternative_size = data.AlternativeSize(service_index); - for (std::size_t v = 0; v < data.Vehicles().size(); ++v) { - int64 start_index = routing.Start(v); - // int64 end_index = routing.End(v); - IntVar* const next_var = routing.NextVar(start_index); - next_var->RemoveValue(current_index); + for (int64 alternative_index = current_index; + alternative_index < current_index + alternative_size; ++alternative_index) { + for (std::size_t v = 0; v < data.Vehicles().size(); ++v) { + int64 start_index = routing.Start(v); + // int64 end_index = routing.End(v); + IntVar* const next_var = routing.NextVar(start_index); + next_var->RemoveValue(alternative_index); + } } } break; @@ -623,12 +629,18 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { current_index = data.IdIndex(relation.linked_ids[link_index]); + int32 service_index = + data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); + alternative_size = data.AlternativeSize(service_index); - intermediate_values.push_back(current_index); + for (int64 alternative_index = current_index; + alternative_index < current_index + alternative_size; ++alternative_index) { + intermediate_values.push_back(alternative_index); - std::vector::iterator it = - std::find(values.begin(), values.end(), current_index); - values.erase(it); + std::vector::iterator it = + std::find(values.begin(), values.end(), alternative_index); + values.erase(it); + } } for (int index : values) { @@ -639,19 +651,31 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, case NeverLast: for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { current_index = data.IdIndex(relation.linked_ids[link_index]); + int32 service_index = + data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); + alternative_size = data.AlternativeSize(service_index); - IntVar* const next_var = routing.NextVar(current_index); - for (std::size_t v = 0; v < data.Vehicles().size(); ++v) { - int64 end_index = routing.End(v); - next_var->RemoveValue(end_index); + for (int64 alternative_index = current_index; + alternative_index < current_index + alternative_size; ++alternative_index) { + IntVar* const next_var = routing.NextVar(alternative_index); + for (std::size_t v = 0; v < data.Vehicles().size(); ++v) { + int64 end_index = routing.End(v); + next_var->RemoveValue(end_index); + } } } break; case ForceLast: for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { current_index = data.IdIndex(relation.linked_ids[link_index]); + int32 service_index = + data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); + alternative_size = data.AlternativeSize(service_index); - values.push_back(current_index); + for (int64 alternative_index = current_index; + alternative_index < current_index + alternative_size; ++alternative_index) { + values.push_back(alternative_index); + } } intermediate_values.insert(intermediate_values.end(), values.begin(), values.end()); for (std::size_t v = 0; v < data.Vehicles().size(); ++v) { @@ -732,7 +756,7 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, default: std::cout << "ERROR: Relation type (" << relation.type << ") is not implemented" << std::endl; - throw - 1; + throw -1; } } } From b7d344d032848be58e9d60fced47f97eed4e2d54 Mon Sep 17 00:00:00 2001 From: Braktar Date: Wed, 6 Apr 2022 12:12:42 +0200 Subject: [PATCH 07/30] Alternative for order/sequence/same_route relations --- tsp_simple.cc | 218 +++++++++++++++++++++++++++++++++++------------- tsptw_data_dt.h | 3 +- 2 files changed, 164 insertions(+), 57 deletions(-) diff --git a/tsp_simple.cc b/tsp_simple.cc index d133905c..99c8bec1 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -341,6 +341,11 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, RoutingModel::DisjunctionIndex current_disjunction_index; std::vector previous_vehicle_index_set; std::vector current_vehicle_index_set; + std::vector previous_active_var_set; + std::vector current_active_var_set; + std::vector current_active_index_set; + std::vector previous_active_next_var_set; + std::vector current_active_next_var_set; std::vector previous_vehicle_var_set; std::vector current_vehicle_var_set; std::vector previous_active_cumul_var_set; @@ -348,81 +353,182 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, switch (relation.type) { case Sequence: - // int64 new_current_index; - previous_index = data.IdIndex(relation.linked_ids[0]); - for (int link_index = 1; link_index < relation.linked_ids.size(); ++link_index) { - previous_indices.push_back(previous_index); + for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { current_index = data.IdIndex(relation.linked_ids[link_index]); + int32 service_index = + data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); + alternative_size = data.AlternativeSize(service_index); - pairs.push_back(std::make_pair(previous_index, current_index)); + current_active_var_set.clear(); + current_active_cumul_var_set.clear(); + current_vehicle_var_set.clear(); + current_active_next_var_set.clear(); + current_active_index_set.clear(); + for (int64 alternative_index = current_index; + alternative_index < current_index + alternative_size; ++alternative_index) { + IntVar* const active_node = routing.ActiveVar(alternative_index); + current_vehicle_var_set.push_back( + solver->MakeProd(active_node, routing.VehicleVar(alternative_index)) + ->Var()); - IntVar* const previous_active_var = routing.ActiveVar(previous_index); - IntVar* const active_var = routing.ActiveVar(current_index); - solver->AddConstraint(solver->MakeLessOrEqual(active_var, previous_active_var)); - // routing.AddPickupAndDelivery(previous_index, current_index); + // The next var of the current active alternative + current_active_next_var_set.push_back( + solver->MakeProd(active_node, routing.NextVar(alternative_index))->Var()); - IntVar* const previous_vehicle_var = routing.VehicleVar(previous_index); - IntVar* const vehicle_var = routing.VehicleVar(current_index); - IntExpr* const isConstraintActive = - solver->MakeProd(previous_active_var, active_var)->Var(); - routing.NextVar(current_index)->RemoveValues(previous_indices); + // The index of the current alternative + current_active_index_set.push_back( + solver->MakeProd(active_node, alternative_index)->Var()); - solver->AddConstraint(solver->MakeEquality( - solver->MakeProd(isConstraintActive, previous_vehicle_var), - solver->MakeProd(isConstraintActive, vehicle_var))); - solver->AddConstraint(solver->MakeEquality( - solver->MakeProd(isConstraintActive, routing.NextVar(previous_index)), - solver->MakeProd(isConstraintActive, current_index))); - previous_index = current_index; + current_active_var_set.push_back(active_node); + current_active_cumul_var_set.push_back( + solver + ->MakeProd(active_node, routing.GetMutableDimension(kTime)->CumulVar( + alternative_index)) + ->Var()); + } + + if (link_index >= 1) { + // The constraint is only active if both of the services have one active + // alternative + IntVar* active_constraint = + solver + ->MakeProd(solver->MakeMax(previous_active_var_set), + solver->MakeMax(current_active_var_set)) + ->Var(); + // The current set may be active only if the predecessor set is active + solver->AddConstraint( + solver->MakeLessOrEqual(solver->MakeMax(current_active_var_set), + solver->MakeMax(previous_active_var_set))); + + // The current active alternative node should belong to the same route than the + // active predecessor + solver->AddConstraint(solver->MakeEquality( + solver->MakeProd(active_constraint, + solver->MakeMax(previous_vehicle_var_set)), + solver->MakeProd(active_constraint, + solver->MakeMax(current_vehicle_var_set)))); + + // The successor of the predecessor should be the current active alternative + // node + solver->AddConstraint(solver->MakeEquality( + solver->MakeProd(active_constraint, + solver->MakeMax(previous_active_next_var_set)), + solver->MakeProd(active_constraint, + solver->MakeMax(current_active_index_set)))); + + // The current active alternative should start after the active predecessor + solver->AddConstraint(solver->MakeLessOrEqual( + solver->MakeProd(active_constraint, + solver->MakeMax(previous_active_cumul_var_set)), + solver->MakeProd(active_constraint, + solver->MakeMax(current_active_cumul_var_set)))); + } + previous_active_var_set = current_active_var_set; + previous_active_next_var_set = current_active_next_var_set; + previous_vehicle_var_set = current_vehicle_var_set; + previous_active_cumul_var_set = current_active_cumul_var_set; } - if (relation.linked_ids.size() > 1) - solver->AddConstraint(solver->MakePathPrecedenceConstraint(next_vars, pairs)); break; case Order: - previous_index = data.IdIndex(relation.linked_ids[0]); - previous_indices.push_back(previous_index); - for (int link_index = 1; link_index < relation.linked_ids.size(); ++link_index) { + for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { current_index = data.IdIndex(relation.linked_ids[link_index]); + int32 service_index = + data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); + alternative_size = data.AlternativeSize(service_index); - pairs.push_back(std::make_pair(previous_index, current_index)); - // routing.AddPickupAndDelivery(previous_index, current_index); - IntVar* const previous_active_var = routing.ActiveVar(previous_index); - IntVar* const active_var = routing.ActiveVar(current_index); + current_active_var_set.clear(); + current_vehicle_var_set.clear(); + current_active_cumul_var_set.clear(); - IntVar* const previous_vehicle_var = routing.VehicleVar(previous_index); - IntVar* const vehicle_var = routing.VehicleVar(current_index); - routing.NextVar(current_index)->RemoveValues(previous_indices); + for (int64 alternative_index = current_index; + alternative_index < current_index + alternative_size; ++alternative_index) { + IntVar* active_node = routing.ActiveVar(alternative_index); + current_active_var_set.push_back(active_node); + current_vehicle_var_set.push_back(routing.VehicleVar(alternative_index)); + current_active_cumul_var_set.push_back( + solver + ->MakeProd(active_node, routing.GetMutableDimension(kTime)->CumulVar( + alternative_index)) + ->Var()); + } - solver->AddConstraint(solver->MakeLessOrEqual(active_var, previous_active_var)); - IntExpr* const isConstraintActive = - solver->MakeProd(previous_active_var, active_var); - solver->AddConstraint(solver->MakeEquality( - solver->MakeProd(isConstraintActive, previous_vehicle_var), - solver->MakeProd(isConstraintActive, vehicle_var))); - previous_indices.push_back(current_index); - previous_index = current_index; + if (link_index >= 1) { + // The constraint is only active if both of the services have one active + // alternative + IntVar* active_constraint = + solver + ->MakeProd(solver->MakeMax(previous_active_var_set), + solver->MakeMax(current_active_var_set)) + ->Var(); + + // The current set may be active only if the predessor set is active + solver->AddConstraint( + solver->MakeLessOrEqual(solver->MakeMax(current_active_var_set), + solver->MakeMax(previous_active_var_set))); + // The active alternatives should belong to the same route + solver->AddConstraint(solver->MakeEquality( + solver->MakeProd(active_constraint, + solver->MakeMax(previous_vehicle_var_set)), + solver->MakeProd(active_constraint, + solver->MakeMax(current_vehicle_var_set)))); + + // The current active alternative should start after the active predecessor + solver->AddConstraint(solver->MakeLessOrEqual( + solver->MakeProd(active_constraint, + solver->MakeMax(previous_active_cumul_var_set)), + solver->MakeProd(active_constraint, + solver->MakeMax(current_active_cumul_var_set)))); + } + previous_active_var_set = current_active_var_set; + previous_vehicle_var_set = current_vehicle_var_set; + previous_active_cumul_var_set = current_active_cumul_var_set; } - if (relation.linked_ids.size() > 1) - solver->AddConstraint(solver->MakePathPrecedenceConstraint(next_vars, pairs)); break; case SameRoute: - previous_index = data.IdIndex(relation.linked_ids[0]); - for (int link_index = 1; link_index < relation.linked_ids.size(); ++link_index) { + for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { current_index = data.IdIndex(relation.linked_ids[link_index]); + int32 service_index = + data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); + alternative_size = data.AlternativeSize(service_index); - IntVar* const previous_active_var = routing.ActiveVar(previous_index); - IntVar* const active_var = routing.ActiveVar(current_index); + current_active_var_set.clear(); + current_vehicle_var_set.clear(); + for (int64 alternative_index = current_index; + alternative_index < current_index + alternative_size; ++alternative_index) { + IntVar* const active_node = routing.ActiveVar(alternative_index); + current_active_var_set.push_back(active_node); + // Current vehicle var is active only if one of the previous active var is + // active and if the current alternative is active + current_vehicle_var_set.push_back( + solver + ->MakeProd(solver->MakeProd(solver->MakeMax(previous_active_var_set), + active_node), + routing.VehicleVar(alternative_index)) + ->Var()); + } - IntVar* const previous_vehicle_var = routing.VehicleVar(previous_index); - IntVar* const vehicle_var = routing.VehicleVar(current_index); + if (link_index >= 1) { + // The constraint is only active if both of the services have one active + // alternative + IntVar* active_constraint = + solver + ->MakeProd(solver->MakeMax(previous_active_var_set), + solver->MakeMax(current_active_var_set)) + ->Var(); - solver->AddConstraint(solver->MakeLessOrEqual(active_var, previous_active_var)); - IntVar* const isConstraintActive = - solver->MakeProd(previous_active_var, active_var)->Var(); - solver->AddConstraint(solver->MakeEquality( - solver->MakeProd(previous_vehicle_var, isConstraintActive), - solver->MakeProd(vehicle_var, isConstraintActive))); - previous_index = current_index; + // The current set may be active only if the predessor set is active + solver->AddConstraint( + solver->MakeLessOrEqual(solver->MakeMax(current_active_var_set), + solver->MakeMax(previous_active_var_set))); + // The active alternatives should belong to the same route + solver->AddConstraint(solver->MakeEquality( + solver->MakeProd(active_constraint, + solver->MakeMax(previous_vehicle_var_set)), + solver->MakeProd(active_constraint, + solver->MakeMax(current_vehicle_var_set)))); + } + previous_active_var_set = current_active_var_set; + previous_vehicle_var_set = current_vehicle_var_set; } break; case MinimumDayLapse: diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 9301c4f1..ed33bb55 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -887,7 +887,8 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { r_q)); service_times_.push_back(service.duration()); alternative_size_map_[service.problem_index()] += 1; - ids_map_[(std::string)service.id()] = node_index; + if (ids_map_.find((std::string)service.id()) == ids_map_.end()) + ids_map_[(std::string)service.id()] = node_index; node_index++; } ++matrix_index; From 59e18d569a3429bd011991690d2b6d67cefe7fbd Mon Sep 17 00:00:00 2001 From: Braktar Date: Thu, 7 Apr 2022 16:08:44 +0200 Subject: [PATCH 08/30] fix shipment relation --- tsp_simple.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tsp_simple.cc b/tsp_simple.cc index 99c8bec1..2924995a 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -608,18 +608,20 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, IntVar* const active_node = routing.ActiveVar(alternative_index); current_active_cumul_var_set.push_back( solver - ->MakeProd(active_node, - routing.GetMutableDimension(kTime)->CumulVar(current_index)) + ->MakeProd(active_node, routing.GetMutableDimension(kTime)->CumulVar( + alternative_index)) ->Var()); } if (link_index >= 1) { routing.AddPickupAndDeliverySets(previous_disjunction_index, current_disjunction_index); + // The active alternatives should belong to the same route solver->AddConstraint( solver->MakeEquality(solver->MakeMax(previous_vehicle_var_set), solver->MakeMax(current_vehicle_var_set))); + // The current active alternative should start after the active predecessor solver->AddConstraint( solver->MakeLessOrEqual(solver->MakeMax(previous_active_cumul_var_set), solver->MakeMax(current_active_cumul_var_set))); From c337b15e1973c3c7216d7eac1a164eb72c8969b8 Mon Sep 17 00:00:00 2001 From: Braktar Date: Fri, 8 Apr 2022 11:53:11 +0200 Subject: [PATCH 09/30] Increase time+service time in case of relations --- tsptw_data_dt.h | 59 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index ed33bb55..19bb0e3a 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -51,6 +51,7 @@ class TSPTWDataDT { , size_matrix_(0) , size_missions_(0) , size_rest_(0) + , size_alternative_relations_(0) , deliveries_counter_(0) , horizon_(0) , max_distance_(0) @@ -261,6 +262,8 @@ class TSPTWDataDT { int32 SizeRest() const { return size_rest_; } + int32 SizeAlternativeRelations() const { return size_alternative_relations_; } + const std::vector& RefillQuantities(const RoutingIndexManager::NodeIndex i) const { return tsptw_clients_[i.value()].refill_quantities; @@ -464,12 +467,22 @@ class TSPTWDataDT { // This is the quantity added after visiting node "from" int64 TimePlusServiceTime(const RoutingIndexManager::NodeIndex from, const RoutingIndexManager::NodeIndex to) const { - return Time(from, to) + coef_service * data->ServiceTime(from) + - additional_service + - (vehicle_indices[from.value()] != vehicle_indices[to.value()] - ? coef_setup * data->SetupTime(to) + - (data->SetupTime(to) > 0 ? additional_setup : 0) - : 0); + int64 current_time = Time(from, to) + coef_service * data->ServiceTime(from) + + additional_service + + (vehicle_indices[from.value()] != vehicle_indices[to.value()] + ? coef_setup * data->SetupTime(to) + + (data->SetupTime(to) > 0 ? additional_setup : 0) + : 0); + + // In case of order or sequence relations having no duration + // will violate relations as the cumul_var will be the same. + // Moreover with sequence+shipment lead or-tools to try only + // invalid order of nodes + if (current_time == 0 && data->SizeAlternativeRelations() > 0) { + ++current_time; + } + + return current_time; // FIXME: // (Time(from, to) == 0 ? 0 // and @@ -726,6 +739,7 @@ class TSPTWDataDT { int32 size_matrix_; int32 size_missions_; int32 size_rest_; + int32 size_alternative_relations_; int64 deliveries_counter_; int64 horizon_; int64 max_distance_; @@ -1077,31 +1091,39 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { for (const ortools_vrp::Relation& relation : problem.relations()) { RelationType relType; - if (relation.type() == "sequence") + if (relation.type() == "sequence") { + ++size_alternative_relations_; relType = Sequence; - else if (relation.type() == "order") { + } else if (relation.type() == "order") { + ++size_alternative_relations_; relType = Order; - } else if (relation.type() == "same_route") + } else if (relation.type() == "same_route") { + ++size_alternative_relations_; relType = SameRoute; - else if (relation.type() == "minimum_day_lapse") + } else if (relation.type() == "minimum_day_lapse") relType = MinimumDayLapse; else if (relation.type() == "maximum_day_lapse") relType = MaximumDayLapse; - else if (relation.type() == "shipment") + else if (relation.type() == "shipment") { + ++size_alternative_relations_; relType = Shipment; - else if (relation.type() == "meetup") + } else if (relation.type() == "meetup") relType = MeetUp; else if (relation.type() == "maximum_duration_lapse") relType = MaximumDurationLapse; - else if (relation.type() == "force_first") + else if (relation.type() == "force_first") { + ++size_alternative_relations_; relType = ForceFirst; - else if (relation.type() == "never_first") + } else if (relation.type() == "never_first") { + ++size_alternative_relations_; relType = NeverFirst; - else if (relation.type() == "never_last") + } else if (relation.type() == "never_last") { + ++size_alternative_relations_; relType = NeverLast; - else if (relation.type() == "force_end") + } else if (relation.type() == "force_end") { + ++size_alternative_relations_; relType = ForceLast; - else if (relation.type() == "vehicle_group_duration") + } else if (relation.type() == "vehicle_group_duration") relType = VehicleGroupDuration; else if (relation.type() == "vehicle_trips") { relType = VehicleTrips; @@ -1167,6 +1189,9 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { sum_setup * max_coef_setup_ + sum_max_time_ + sum_lapse + max_rest_; } + if (size_alternative_relations_ > 0) + horizon_ += size_missions_; + for (int32 i = 0; i < size_missions_; ++i) { max_service_ = std::max(max_service_, tsptw_clients_[i].service_time); } From 25e9feb262f18fd3b30350f1b3260d67f9122300 Mon Sep 17 00:00:00 2001 From: Braktar Date: Fri, 22 Apr 2022 15:04:05 +0200 Subject: [PATCH 10/30] remove previous indices and alternatives from nextVar --- tsp_simple.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tsp_simple.cc b/tsp_simple.cc index 2924995a..34862e91 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -385,6 +385,8 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, ->MakeProd(active_node, routing.GetMutableDimension(kTime)->CumulVar( alternative_index)) ->Var()); + routing.NextVar(alternative_index)->RemoveValues(previous_indices); + previous_indices.push_back(alternative_index); } if (link_index >= 1) { @@ -450,6 +452,8 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, ->MakeProd(active_node, routing.GetMutableDimension(kTime)->CumulVar( alternative_index)) ->Var()); + routing.NextVar(alternative_index)->RemoveValues(previous_indices); + previous_indices.push_back(alternative_index); } if (link_index >= 1) { From b6ad38707b0b1b0b84fd7579ab3fd57009042bc4 Mon Sep 17 00:00:00 2001 From: halilsen Date: Fri, 20 May 2022 10:28:55 +0200 Subject: [PATCH 11/30] Remove dead code --- tsp_simple.cc | 8 -------- tsptw_data_dt.h | 32 -------------------------------- 2 files changed, 40 deletions(-) diff --git a/tsp_simple.cc b/tsp_simple.cc index 34862e91..90e893c7 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -293,14 +293,6 @@ RestBuilder(const TSPTWDataDT& data, RoutingModel& routing, const int64 horizon) routing.GetMutableDimension(kTime)->SetBreakIntervalsOfVehicle( rest_array, vehicle_index, data.ServiceTimes()); - if (vehicle.max_interval_between_breaks > 0) { - DLOG(INFO) << "\n\nSetting max break distance to " - << vehicle.max_interval_between_breaks << std::endl; - // Put an upperbound on the intervals between breaks that are longer than "dur" - routing.GetMutableDimension(kTime)->SetBreakDistanceDurationOfVehicle( - /*upperbound*/ vehicle.max_interval_between_breaks, /*dur*/ 0, vehicle_index); - } - stored_rests.push_back(rest_array); } return stored_rests; diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 19bb0e3a..9a5c5dfd 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -333,9 +333,6 @@ class TSPTWDataDT { , capacity(0) , overload_multiplier(0) , break_size(0) - , max_interval_between_breaks(0) - , max_interval_between_breaks_UB(0) - , max_interval_between_breaks_LB(0) , time_start(0) , time_end(0) , time_maximum_lateness(CUSTOM_MAX_INT) @@ -542,9 +539,6 @@ class TSPTWDataDT { std::vector overload_multiplier; std::vector rests; int32 break_size; - int64 max_interval_between_breaks; - int64 max_interval_between_breaks_UB; - int64 max_interval_between_breaks_LB; int64 time_start; int64 time_end; int64 time_maximum_lateness; @@ -572,32 +566,6 @@ class TSPTWDataDT { const Vehicle& Vehicles(const int64 index) const { return tsptw_vehicles_[index]; } - int64 MaxBreakDistOfVehicle(const int64 index) const { - return tsptw_vehicles_[index].max_interval_between_breaks; - } - - int64 MaxBreakDistUBOfVehicle(const int64 index) const { - return tsptw_vehicles_[index].max_interval_between_breaks_UB; - } - - int64 MaxBreakDistLBOfVehicle(const int64 index) const { - return tsptw_vehicles_[index].max_interval_between_breaks_LB; - } - - void SetMaxBreakDistOfVehicle(const int64 index, const int64 max_interval) { - tsptw_vehicles_[index].max_interval_between_breaks = max_interval; - } - - void SetMaxBreakDistUBOfVehicle(const int64 index, - const int64 max_interval_upperbound) { - tsptw_vehicles_[index].max_interval_between_breaks_UB = max_interval_upperbound; - } - - void SetMaxBreakDistLBOfVehicle(const int64 index, - const int64 max_interval_lowerbound) { - tsptw_vehicles_[index].max_interval_between_breaks_LB = max_interval_lowerbound; - } - bool VehicleHasEnd(const int64 index) const { return tsptw_vehicles_[index].time_end < CUSTOM_MAX_INT; } From 44ed54a364f8a70976c6803f8eb332fd2a50fd2b Mon Sep 17 00:00:00 2001 From: Braktar Date: Mon, 23 May 2022 11:17:51 +0200 Subject: [PATCH 12/30] Fix Cost computation when kFakeTime is used --- tsp_simple.cc | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tsp_simple.cc b/tsp_simple.cc index 90e893c7..729e993c 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -80,7 +80,9 @@ double GetUpperBoundCostForDimension(const RoutingModel& routing, void MissionsBuilder(const TSPTWDataDT& data, RoutingModel& routing, RoutingValues& routing_values, RoutingIndexManager& manager, - Assignment* assignment, const int64 size, const int64 min_start) { + Assignment* assignment, const int64 size, const int64 min_start, + const bool free_approach_return) { + Solver* solver = routing.solver(); const int size_vehicles = data.Vehicles().size(); // const int size_matrix = data.SizeMatrix(); const int size_problem = data.SizeProblem(); @@ -120,7 +122,18 @@ void MissionsBuilder(const TSPTWDataDT& data, RoutingModel& routing, const std::vector& maximum_lateness = data.MaximumLateness(i); const int64 initial_value = routing_values.NodeValues(i).initial_time_value; - IntVar* cumul_var = routing.GetMutableDimension(kTime)->CumulVar(index); + // Timewindows should apply both on kTime and kFakeTime Dimensions.kTime both + // compute the true timings of activities and the time cost kFakeTime allows to + // compute the time cost of the route when there is an free_approach or free_return + // dimension. But it requires true timings to consider correctly waiting times. + // Lateness could only apply on true timings, which means these contraints should + // only be applied on kTime dimension. Dimensions kNoWait and kFakeNoWait doesn't + // need time windows because it expects no waiting times + IntVar* cumul_var = routing.GetMutableDimension(kTime)->CumulVar(index); + if (free_approach_return) + solver->AddConstraint(solver->MakeEquality( + routing.GetMutableDimension(kFakeTime)->CumulVar(index), cumul_var)); + const int64 late_multiplier = data.LateMultiplier(i); std::string service_id = data.ServiceId(i); if (ready.size() > 0 && @@ -1487,7 +1500,10 @@ void ParseSolutionIntoResult(const Assignment* const solution, const double time_without_wait_cost = GetSpanCostForVehicleForDimension(routing, solution, route_nbr, kTimeNoWait); - route_costs->set_time_without_wait(time_without_wait_cost); + const double fake_time_without_wait_cost = GetSpanCostForVehicleForDimension( + routing, solution, route_nbr, kFakeTimeNoWait); + route_costs->set_time_without_wait(time_without_wait_cost + + fake_time_without_wait_cost); const double value_cost = GetSpanCostForVehicleForDimension(routing, solution, route_nbr, kValue); @@ -1637,8 +1653,8 @@ const ortools_result::Result* TSPTWSolver(const TSPTWDataDT& data, } // Setting visit time windows - MissionsBuilder(data, routing, routing_values, manager, assignment, size - 2, - min_start); + MissionsBuilder(data, routing, routing_values, manager, assignment, size - 2, min_start, + free_approach_return); std::vector> stored_rests = RestBuilder(data, routing, horizon); RelationBuilder(data, routing, has_overall_duration); From 24486589652410f35e6e128157c6d171f559f713 Mon Sep 17 00:00:00 2001 From: Braktar Date: Fri, 20 May 2022 09:49:39 +0200 Subject: [PATCH 13/30] Shift internal timewindows to earliest possible --- limits.h | 7 ++++--- tsp_simple.cc | 16 ++++++++++------ tsptw_data_dt.h | 44 ++++++++++++++++++++++++++++---------------- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/limits.h b/limits.h index 31a56487..27b488de 100644 --- a/limits.h +++ b/limits.h @@ -319,6 +319,7 @@ class LoggerMonitor : public SearchMonitor { int64 lateness_cost = 0; int64 overload_cost = 0; bool vehicle_used = false; + const int64 earliest_start = data_.EarliestStart(); for (int64 index = routing_->Start(route_nbr); !routing_->IsEnd(index); index = routing_->NextVar(index)->Value()) { for (std::vector::iterator it = rests.begin(); @@ -338,7 +339,7 @@ class LoggerMonitor : public SearchMonitor { ortools_result::Activity* rest = route->add_activities(); rest->set_type("break"); rest->set_id(parsed_name[1]); - rest->set_start_time(rest_start_time); + rest->set_start_time(rest_start_time + earliest_start); it = rests.erase(it); } else { ++it; @@ -350,7 +351,7 @@ class LoggerMonitor : public SearchMonitor { activity->set_index(data_.ProblemIndex(nodeIndex)); const int64 start_time = routing_->GetMutableDimension(kTime)->CumulVar(index)->Min(); - activity->set_start_time(start_time); + activity->set_start_time(start_time + earliest_start); const int64 upper_bound = routing_->GetMutableDimension(kTime)->GetCumulVarSoftUpperBound(index); const int64 lateness = std::max(start_time - upper_bound, 0); @@ -407,7 +408,7 @@ class LoggerMonitor : public SearchMonitor { const int64 start_time = routing_->GetMutableDimension(kTime)->CumulVar(end_index)->Min(); - end_activity->set_start_time(start_time); + end_activity->set_start_time(start_time + earliest_start); const int64 upper_bound = routing_->GetMutableDimension(kTime)->GetCumulVarSoftUpperBound(end_index); const int64 lateness = std::max(start_time - upper_bound, 0); diff --git a/tsp_simple.cc b/tsp_simple.cc index 90e893c7..93236bd6 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -1320,6 +1320,8 @@ void ParseSolutionIntoResult(const Assignment* const solution, std::vector>& stored_rests) { result->clear_routes(); + const int64_t earliest_start = data.EarliestStart(); + double total_time_order_cost(0.0), total_distance_order_cost(0.0), total_rest_position_cost(0.0); @@ -1349,7 +1351,7 @@ void ParseSolutionIntoResult(const Assignment* const solution, ortools_result::Activity* rest = route->add_activities(); rest->set_type("break"); rest->set_id(parsed_name[1]); - rest->set_start_time(rest_start_time); + rest->set_start_time(rest_start_time + earliest_start); it = rests.erase(it); } else { ++it; @@ -1359,7 +1361,7 @@ void ParseSolutionIntoResult(const Assignment* const solution, ortools_result::Activity* activity = route->add_activities(); RoutingIndexManager::NodeIndex nodeIndex = manager.IndexToNode(index); activity->set_index(data.ProblemIndex(nodeIndex)); - activity->set_start_time(start_time); + activity->set_start_time(start_time + earliest_start); const int64 upper_bound = routing.GetMutableDimension(kTime)->GetCumulVarSoftUpperBound(index); const int64 lateness = std::max(start_time - upper_bound, 0); @@ -1371,7 +1373,8 @@ void ParseSolutionIntoResult(const Assignment* const solution, activity->set_type("start"); DLOG(INFO) << "RouteStartValues:" << route_nbr << "\t start_time: " << start_time << std::endl; - routing_values.RouteStartValues(route_nbr).initial_time_value = start_time; + routing_values.RouteStartValues(route_nbr).initial_time_value = + start_time + earliest_start; } else { vehicle_used = true; activity->set_type("service"); @@ -1379,7 +1382,8 @@ void ParseSolutionIntoResult(const Assignment* const solution, activity->set_alternative(data.AlternativeIndex(nodeIndex)); DLOG(INFO) << "nodeIndex:" << nodeIndex << "\t start_time: " << start_time << std::endl; - routing_values.NodeValues(nodeIndex).initial_time_value = start_time; + routing_values.NodeValues(nodeIndex).initial_time_value = + start_time + earliest_start; } for (std::size_t q = 0; q < data.Quantities(RoutingIndexManager::NodeIndex(0)).size(); ++q) { @@ -1409,7 +1413,7 @@ void ParseSolutionIntoResult(const Assignment* const solution, } rest->set_type("break"); rest->set_id(parsed_name[1]); - rest->set_start_time(rest_start_time); + rest->set_start_time(rest_start_time + earliest_start); } } @@ -1421,7 +1425,7 @@ void ParseSolutionIntoResult(const Assignment* const solution, const int64 start_time = solution->Min(routing.GetMutableDimension(kTime)->CumulVar(end_index)); - end_activity->set_start_time(start_time); + end_activity->set_start_time(start_time + earliest_start); const int64 upper_bound = routing.GetMutableDimension(kTime)->GetCumulVarSoftUpperBound(end_index); const int64 lateness = std::max(start_time - upper_bound, 0); diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 9a5c5dfd..75ccdb89 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -54,6 +54,7 @@ class TSPTWDataDT { , size_alternative_relations_(0) , deliveries_counter_(0) , horizon_(0) + , earliest_start_(CUSTOM_MAX_INT) , max_distance_(0) , max_distance_cost_(0) , max_rest_(0) @@ -138,6 +139,8 @@ class TSPTWDataDT { return tsptw_clients_[i.value()].matrix_index; } + int64 EarliestStart() const { return earliest_start_; } + int64 MaxTime() const { return max_time_; } int64 MaxDistance() const { return max_distance_; } @@ -710,6 +713,7 @@ class TSPTWDataDT { int32 size_alternative_relations_; int64 deliveries_counter_; int64 horizon_; + int64 earliest_start_; int64 max_distance_; int64 max_distance_cost_; int64 max_rest_; @@ -752,6 +756,11 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { } } + // compute earliest start first + for (const ortools_vrp::Vehicle& vehicle : problem.vehicles()) { + earliest_start_ = std::min((int64)vehicle.time_window().start(), earliest_start_); + } + int32 node_index = 0; int32 matrix_index = 0; std::vector matrix_indices; @@ -799,15 +808,15 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { matrix_indices.push_back(service.matrix_index()); std::vector start; if (timewindows.size() > 0 && - timewindows[timewindow_index]->start() > -CUSTOM_MAX_INT) - start.push_back(timewindows[timewindow_index]->start()); + (timewindows[timewindow_index]->start() - earliest_start_) > 0) + start.push_back(timewindows[timewindow_index]->start() - earliest_start_); else - start.push_back(-CUSTOM_MAX_INT); + start.push_back(0); std::vector end; if (timewindows.size() > 0 && - timewindows[timewindow_index]->end() < CUSTOM_MAX_INT) - end.push_back(timewindows[timewindow_index]->end()); + (timewindows[timewindow_index]->end() - earliest_start_) < CUSTOM_MAX_INT) + end.push_back(timewindows[timewindow_index]->end() - earliest_start_); else end.push_back(CUSTOM_MAX_INT); @@ -845,10 +854,12 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { std::vector max_lateness; for (const ortools_vrp::TimeWindow* timewindow : timewindows) { - timewindow->start() > -CUSTOM_MAX_INT ? ready_time.push_back(timewindow->start()) - : ready_time.push_back(-CUSTOM_MAX_INT); - timewindow->end() < CUSTOM_MAX_INT ? due_time.push_back(timewindow->end()) - : due_time.push_back(CUSTOM_MAX_INT); + (timewindow->start() - earliest_start_) > 0 + ? ready_time.push_back(timewindow->start() - earliest_start_) + : ready_time.push_back(0); + (timewindow->end() - earliest_start_) < CUSTOM_MAX_INT + ? due_time.push_back(timewindow->end() - earliest_start_) + : due_time.push_back(CUSTOM_MAX_INT); timewindow->maximum_lateness() < CUSTOM_MAX_INT ? max_lateness.push_back(timewindow->maximum_lateness()) : max_lateness.push_back(CUSTOM_MAX_INT); @@ -964,11 +975,11 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { v->problem_matrix_index = vehicle.matrix_index(); v->value_matrix_index = vehicle.value_matrix_index(); v->vehicle_indices = vehicle_indices; - v->time_start = vehicle.time_window().start() > -CUSTOM_MAX_INT - ? vehicle.time_window().start() - : -CUSTOM_MAX_INT; - v->time_end = vehicle.time_window().end() < CUSTOM_MAX_INT - ? vehicle.time_window().end() + v->time_start = (vehicle.time_window().start() - earliest_start_) > 0 + ? vehicle.time_window().start() - earliest_start_ + : 0; + v->time_end = (vehicle.time_window().end() - earliest_start_) < CUSTOM_MAX_INT + ? vehicle.time_window().end() - earliest_start_ : CUSTOM_MAX_INT; v->time_maximum_lateness = vehicle.time_window().maximum_lateness() < CUSTOM_MAX_INT ? vehicle.time_window().maximum_lateness() @@ -1020,8 +1031,9 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { // Add vehicle rests for (const ortools_vrp::Rest& rest : vehicle.rests()) { - v->rests.emplace_back((std::string)rest.id(), rest.time_window().start(), - rest.time_window().end(), rest.duration()); + v->rests.emplace_back((std::string)rest.id(), + rest.time_window().start() - earliest_start_, + rest.time_window().end() - earliest_start_, rest.duration()); } v_idx++; From 5e9f253cd20d0b00a2b435d27a5fa2eb048225e6 Mon Sep 17 00:00:00 2001 From: Braktar Date: Tue, 24 May 2022 11:21:44 +0200 Subject: [PATCH 14/30] Throw an error if vehicle has no timewindow --- tsptw_data_dt.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 75ccdb89..cb9cb4fc 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -947,6 +947,11 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { int v_idx = 0; day_index_to_vehicle_index_[0] = v_idx; for (const ortools_vrp::Vehicle& vehicle : problem.vehicles()) { + if (!vehicle.has_time_window()) { + throw std::invalid_argument( + "A vehicle should always have an initialized timewindow"); + } + tsptw_vehicles_.emplace_back(this, size_); auto v = tsptw_vehicles_.rbegin(); @@ -1113,7 +1118,7 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { else if (relation.type() == "minimum_duration_lapse") relType = MinimumDurationLapse; else - throw "Unknown relation type"; + throw std::invalid_argument("Unknown relation type"); sum_lapse += relation.lapse(); tsptw_relations_.emplace_back(re_index, relType, relation.linked_ids(), From f2c31357c3ee0a3c1a71b8a02600454df3b6b94f Mon Sep 17 00:00:00 2001 From: Braktar Date: Tue, 31 May 2022 12:26:22 +0200 Subject: [PATCH 15/30] Return depot doesn't need time shift --- tsptw_data_dt.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index cb9cb4fc..fa26c8ba 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -478,7 +478,7 @@ class TSPTWDataDT { // will violate relations as the cumul_var will be the same. // Moreover with sequence+shipment lead or-tools to try only // invalid order of nodes - if (current_time == 0 && data->SizeAlternativeRelations() > 0) { + if (current_time == 0 && data->SizeAlternativeRelations() > 0 && to.value() < data->SizeMissions()) { ++current_time; } From c0aed8955dfa55017a638325ccc8d1f60209726f Mon Sep 17 00:00:00 2001 From: Braktar Date: Tue, 31 May 2022 11:57:11 +0200 Subject: [PATCH 16/30] An infeasible timewindow doesn't have to generate its own node --- limits.h | 25 +++++----- tsp_simple.cc | 15 +++--- tsptw_data_dt.h | 119 ++++++++++++++++++++++++++---------------------- 3 files changed, 82 insertions(+), 77 deletions(-) diff --git a/limits.h b/limits.h index 27b488de..9bdf940b 100644 --- a/limits.h +++ b/limits.h @@ -205,8 +205,8 @@ namespace { class LoggerMonitor : public SearchMonitor { public: LoggerMonitor(const TSPTWDataDT& data, RoutingModel* routing, - RoutingIndexManager* manager, int64 min_start, int64 size_matrix, - bool debug, bool intermediate, ortools_result::Result* result, + RoutingIndexManager* manager, int64 size_matrix, bool debug, + bool intermediate, ortools_result::Result* result, std::vector> stored_rests, std::string filename, const bool minimize = true) : SearchMonitor(routing->solver()) @@ -215,7 +215,6 @@ class LoggerMonitor : public SearchMonitor { , manager_(manager) , solver_(routing->solver()) , start_time_(absl::GetCurrentTimeNanos()) - , min_start_(min_start) , size_matrix_(size_matrix) , minimize_(minimize) , limit_reached_(false) @@ -560,7 +559,6 @@ class LoggerMonitor : public SearchMonitor { } if (debug_ && new_best) { - std::cout << "min start : " << min_start_ << std::endl; for (RoutingIndexManager::NodeIndex i(0); i < data_.SizeMatrix() - 1; ++i) { const int64 index = manager_->NodeToIndex(i); const IntVar* cumul_var = routing_->GetMutableDimension(kTime)->CumulVar(index); @@ -572,8 +570,8 @@ class LoggerMonitor : public SearchMonitor { slack_var->Bound()) { std::cout << "Node " << i << " index " << index << " [" << vehicle_var->Value() << "] |"; - std::cout << (cumul_var->Value() - min_start_) << " + " << transit_var->Value() - << " -> " << slack_var->Value() << std::endl; + std::cout << (cumul_var->Value()) << " + " << transit_var->Value() << " -> " + << slack_var->Value() << std::endl; } } std::cout << "-----------" << std::endl; @@ -614,9 +612,9 @@ class LoggerMonitor : public SearchMonitor { // Allocates a clone of the limit virtual SearchMonitor* MakeClone() const { // we don't to copy the variables - return solver_->RevAlloc( - new LoggerMonitor(data_, routing_, manager_, min_start_, size_matrix_, debug_, - intermediate_, result_, stored_rests_, filename_, minimize_)); + return solver_->RevAlloc(new LoggerMonitor(data_, routing_, manager_, size_matrix_, + debug_, intermediate_, result_, + stored_rests_, filename_, minimize_)); } virtual std::string DebugString() const { @@ -643,7 +641,6 @@ class LoggerMonitor : public SearchMonitor { int64 best_result_; double cleaned_cost_; double start_time_; - int64 min_start_; int64 size_matrix_; bool minimize_; bool limit_reached_; @@ -660,14 +657,14 @@ class LoggerMonitor : public SearchMonitor { } // namespace LoggerMonitor* MakeLoggerMonitor(const TSPTWDataDT& data, RoutingModel* routing, - RoutingIndexManager* manager, int64 min_start, - int64 size_matrix, bool debug, bool intermediate, + RoutingIndexManager* manager, int64 size_matrix, + bool debug, bool intermediate, ortools_result::Result* result, std::vector> stored_rests, std::string filename, const bool minimize = true) { return routing->solver()->RevAlloc( - new LoggerMonitor(data, routing, manager, min_start, size_matrix, debug, - intermediate, result, stored_rests, filename, minimize)); + new LoggerMonitor(data, routing, manager, size_matrix, debug, intermediate, result, + stored_rests, filename, minimize)); } } // namespace operations_research diff --git a/tsp_simple.cc b/tsp_simple.cc index 46d08b28..b8a5cd78 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -80,7 +80,7 @@ double GetUpperBoundCostForDimension(const RoutingModel& routing, void MissionsBuilder(const TSPTWDataDT& data, RoutingModel& routing, RoutingValues& routing_values, RoutingIndexManager& manager, - Assignment* assignment, const int64 size, const int64 min_start, + Assignment* assignment, const int64 size, const bool free_approach_return) { Solver* solver = routing.solver(); const int size_vehicles = data.Vehicles().size(); @@ -139,9 +139,8 @@ void MissionsBuilder(const TSPTWDataDT& data, RoutingModel& routing, if (ready.size() > 0 && (ready[0] > -CUSTOM_MAX_INT || due.back() < CUSTOM_MAX_INT)) { if (absl::GetFlag(FLAGS_debug)) { - std::cout << "Node " << i << " index " << index << " [" - << (ready[0] - min_start) << " : " << (due.back() - min_start) - << "]:" << data.ServiceTime(i) << std::endl; + std::cout << "Node " << i << " index " << index << " [" << (ready[0]) << " : " + << (due.back()) << "]:" << data.ServiceTime(i) << std::endl; } if (ready[0] > -CUSTOM_MAX_INT) { cumul_var->SetMin(ready[0]); @@ -1588,12 +1587,10 @@ const ortools_result::Result* TSPTWSolver(const TSPTWDataDT& data, AddVehicleDistanceConstraints(data, routing); AddVehicleCapacityConstraints(data, routing); - v = 0; - int64 min_start = CUSTOM_MAX_INT; + v = 0; for (const TSPTWDataDT::Vehicle& vehicle : data.Vehicles()) { routing.SetFixedCostOfVehicle(vehicle.cost_fixed, v); - min_start = std::min(min_start, vehicle.time_start); ++v; } @@ -1657,7 +1654,7 @@ const ortools_result::Result* TSPTWSolver(const TSPTWDataDT& data, } // Setting visit time windows - MissionsBuilder(data, routing, routing_values, manager, assignment, size - 2, min_start, + MissionsBuilder(data, routing, routing_values, manager, assignment, size - 2, free_approach_return); std::vector> stored_rests = RestBuilder(data, routing, horizon); @@ -1701,7 +1698,7 @@ const ortools_result::Result* TSPTWSolver(const TSPTWDataDT& data, const bool build_route = RouteBuilder(data, routing, manager, assignment); LoggerMonitor* const logger = MakeLoggerMonitor( - data, &routing, &manager, min_start, size_matrix, absl::GetFlag(FLAGS_debug), + data, &routing, &manager, size_matrix, absl::GetFlag(FLAGS_debug), absl::GetFlag(FLAGS_intermediate_solutions), result, stored_rests, filename, true); routing.AddSearchMonitor(logger); diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index cb9cb4fc..80e0b35c 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -761,8 +761,9 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { earliest_start_ = std::min((int64)vehicle.time_window().start(), earliest_start_); } - int32 node_index = 0; - int32 matrix_index = 0; + int32 node_index = 0; + int32 matrix_index = 0; + int32 previous_matrix_size = 0; std::vector matrix_indices; for (const ortools_vrp::Service& service : problem.services()) { if (!alternative_size_map_.count(service.problem_index())) @@ -805,47 +806,50 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { if (service.late_multiplier() > 0) { do { - matrix_indices.push_back(service.matrix_index()); - std::vector start; - if (timewindows.size() > 0 && - (timewindows[timewindow_index]->start() - earliest_start_) > 0) - start.push_back(timewindows[timewindow_index]->start() - earliest_start_); - else - start.push_back(0); - - std::vector end; - if (timewindows.size() > 0 && - (timewindows[timewindow_index]->end() - earliest_start_) < CUSTOM_MAX_INT) - end.push_back(timewindows[timewindow_index]->end() - earliest_start_); - else - end.push_back(CUSTOM_MAX_INT); - - std::vector max_lateness; - if (timewindows.size() > 0 && - timewindows[timewindow_index]->maximum_lateness() < CUSTOM_MAX_INT) - max_lateness.push_back(timewindows[timewindow_index]->maximum_lateness()); - else - max_lateness.push_back(CUSTOM_MAX_INT); - - size_problem_ = std::max(size_problem_, service.problem_index()); - tsptw_clients_.push_back(TSPTWClient( - (std::string)service.id(), matrix_index, service.problem_index(), - alternative_size_map_[service.problem_index()], start, end, max_lateness, - service.duration(), service.additional_value(), service.setup_duration(), - service.priority(), - timewindows.size() > 0 - ? (int64)(service.late_multiplier() * CUSTOM_BIGNUM_COST) - : 0, - v_i, q, s_q, - service.exclusion_cost() > 0 ? service.exclusion_cost() * CUSTOM_BIGNUM_COST - : -1, - r_q)); - - service_times_.push_back(service.duration()); - alternative_size_map_[service.problem_index()] += 1; - if (ids_map_.find((std::string)service.id()) == ids_map_.end()) - ids_map_[(std::string)service.id()] = node_index; - node_index++; + if (timewindows.size() == 0 || + (earliest_start_ < timewindows[timewindow_index]->end() + + timewindows[timewindow_index]->maximum_lateness())) { + matrix_indices.push_back(service.matrix_index()); + std::vector start; + if (timewindows.size() > 0 && + (timewindows[timewindow_index]->start() - earliest_start_) > 0) + start.push_back(timewindows[timewindow_index]->start() - earliest_start_); + else + start.push_back(0); + + std::vector end; + if (timewindows.size() > 0 && + (timewindows[timewindow_index]->end() - earliest_start_) < CUSTOM_MAX_INT) + end.push_back(timewindows[timewindow_index]->end() - earliest_start_); + else + end.push_back(CUSTOM_MAX_INT); + + std::vector max_lateness; + if (timewindows.size() > 0 && + timewindows[timewindow_index]->maximum_lateness() < CUSTOM_MAX_INT) + max_lateness.push_back(timewindows[timewindow_index]->maximum_lateness()); + else + max_lateness.push_back(CUSTOM_MAX_INT); + + size_problem_ = std::max(size_problem_, service.problem_index()); + tsptw_clients_.push_back(TSPTWClient( + (std::string)service.id(), matrix_index, service.problem_index(), + alternative_size_map_[service.problem_index()], start, end, max_lateness, + service.duration(), service.additional_value(), service.setup_duration(), + service.priority(), + start.size() > 0 ? (int64)(service.late_multiplier() * CUSTOM_BIGNUM_COST) + : 0, + v_i, q, s_q, + service.exclusion_cost() > 0 ? service.exclusion_cost() * CUSTOM_BIGNUM_COST + : -1, + r_q)); + + service_times_.push_back(service.duration()); + alternative_size_map_[service.problem_index()] += 1; + if (ids_map_.find((std::string)service.id()) == ids_map_.end()) + ids_map_[(std::string)service.id()] = node_index; + node_index++; + } ++timewindow_index; } while (timewindow_index < service.time_windows_size()); } else { @@ -854,15 +858,17 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { std::vector max_lateness; for (const ortools_vrp::TimeWindow* timewindow : timewindows) { - (timewindow->start() - earliest_start_) > 0 - ? ready_time.push_back(timewindow->start() - earliest_start_) - : ready_time.push_back(0); - (timewindow->end() - earliest_start_) < CUSTOM_MAX_INT - ? due_time.push_back(timewindow->end() - earliest_start_) - : due_time.push_back(CUSTOM_MAX_INT); - timewindow->maximum_lateness() < CUSTOM_MAX_INT - ? max_lateness.push_back(timewindow->maximum_lateness()) - : max_lateness.push_back(CUSTOM_MAX_INT); + if (earliest_start_ < timewindow->end()) { + (timewindow->start() - earliest_start_) > 0 + ? ready_time.push_back(timewindow->start() - earliest_start_) + : ready_time.push_back(0); + (timewindow->end() - earliest_start_) < CUSTOM_MAX_INT + ? due_time.push_back(timewindow->end() - earliest_start_) + : due_time.push_back(CUSTOM_MAX_INT); + timewindow->maximum_lateness() < CUSTOM_MAX_INT + ? max_lateness.push_back(timewindow->maximum_lateness()) + : max_lateness.push_back(CUSTOM_MAX_INT); + } } matrix_indices.push_back(service.matrix_index()); @@ -872,8 +878,8 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { alternative_size_map_[service.problem_index()], ready_time, due_time, max_lateness, service.duration(), service.additional_value(), service.setup_duration(), service.priority(), - timewindows.size() > 0 ? (int64)(service.late_multiplier() * CUSTOM_BIGNUM_COST) - : 0, + ready_time.size() > 0 ? (int64)(service.late_multiplier() * CUSTOM_BIGNUM_COST) + : 0, v_i, q, s_q, service.exclusion_cost() > 0 ? service.exclusion_cost() * CUSTOM_BIGNUM_COST : -1, @@ -884,6 +890,11 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { ids_map_[(std::string)service.id()] = node_index; node_index++; } + if (previous_matrix_size == (int32)matrix_indices.size()) { + throw std::invalid_argument( + "A Service transmitted should always lead to at least one Node"); + } + previous_matrix_size = matrix_indices.size(); ++matrix_index; } From 9baea946aef9ba531223fa674657b31a181f6af1 Mon Sep 17 00:00:00 2001 From: Pierre Graber Date: Wed, 8 Jun 2022 10:40:29 +0200 Subject: [PATCH 17/30] fix initial route generation --- tsp_simple.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tsp_simple.cc b/tsp_simple.cc index b8a5cd78..5309884c 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -252,7 +252,10 @@ bool RouteBuilder(const TSPTWDataDT& data, RoutingModel& routing, route_variable_indicies.end()); } } - return routing.RoutesToAssignment(routes, true, false, assignment); + + assignment = routing.ReadAssignmentFromRoutes(routes, true); + + return assignment == nullptr ? false : true; } std::vector> From e19d60d196c7da9731a09e6e2090d80c3d589095 Mon Sep 17 00:00:00 2001 From: Pierre Graber Date: Fri, 8 Jul 2022 15:41:25 +0200 Subject: [PATCH 18/30] Base setup_duration on point_id --- ortools_vrp.proto | 2 ++ ortools_vrp_pb.rb | 2 ++ tsptw_data_dt.h | 44 ++++++++++++++++++++++++++++++-------------- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/ortools_vrp.proto b/ortools_vrp.proto index 1432f53c..4b8d3091 100644 --- a/ortools_vrp.proto +++ b/ortools_vrp.proto @@ -30,6 +30,7 @@ message Service { float exclusion_cost = 13; repeated bool refill_quantities = 14; uint32 problem_index = 15; + string point_id = 16; } message Rest { @@ -75,6 +76,7 @@ message Vehicle { uint32 additional_setup = 26; bool free_approach = 27; bool free_return = 28; + string start_point_id = 29; } message Relation { diff --git a/ortools_vrp_pb.rb b/ortools_vrp_pb.rb index ad69e8e8..5d9c01ad 100644 --- a/ortools_vrp_pb.rb +++ b/ortools_vrp_pb.rb @@ -30,6 +30,7 @@ optional :exclusion_cost, :float, 13 repeated :refill_quantities, :bool, 14 optional :problem_index, :uint32, 15 + optional :point_id, :string, 16 end add_message "ortools_vrp.Rest" do optional :time_window, :message, 1, "ortools_vrp.TimeWindow" @@ -72,6 +73,7 @@ optional :additional_setup, :uint32, 26 optional :free_approach, :bool, 27 optional :free_return, :bool, 28 + optional :start_point_id, :string, 29 end add_message "ortools_vrp.Relation" do optional :type, :string, 1 diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 6f96f280..218542e1 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -194,6 +194,10 @@ class TSPTWDataDT { return tsptw_clients_[i.value()].customer_id; } + std::string PointId(const RoutingIndexManager::NodeIndex i) const { + return tsptw_clients_[i.value()].point_id; + } + int32 ProblemIndex(const RoutingIndexManager::NodeIndex i) const { return tsptw_clients_[i.value()].problem_index; } @@ -330,6 +334,7 @@ class TSPTWDataDT { , size(size_) , problem_matrix_index(0) , value_matrix_index(0) + , start_point_id("") , vehicle_indices(0) , initial_capacity(0) , initial_load(0) @@ -467,9 +472,11 @@ class TSPTWDataDT { // This is the quantity added after visiting node "from" int64 TimePlusServiceTime(const RoutingIndexManager::NodeIndex from, const RoutingIndexManager::NodeIndex to) const { + std::string from_id = + from.value() > data->SizeMissions() ? start_point_id : data->PointId(from); int64 current_time = Time(from, to) + coef_service * data->ServiceTime(from) + additional_service + - (vehicle_indices[from.value()] != vehicle_indices[to.value()] + (from_id != data->PointId(to) ? coef_setup * data->SetupTime(to) + (data->SetupTime(to) > 0 ? additional_setup : 0) : 0); @@ -493,9 +500,11 @@ class TSPTWDataDT { int64 FakeTimePlusServiceTime(const RoutingIndexManager::NodeIndex from, const RoutingIndexManager::NodeIndex to) const { + std::string from_id = + from.value() > data->SizeMissions() ? start_point_id : data->PointId(from); return FakeTime(from, to) + coef_service * data->ServiceTime(from) + additional_service + - (vehicle_indices[from.value()] != vehicle_indices[to.value()] + (from_id != data->PointId(to) ? coef_setup * data->SetupTime(to) + (data->SetupTime(to) > 0 ? additional_setup : 0) : 0); @@ -534,6 +543,7 @@ class TSPTWDataDT { RoutingIndexManager::NodeIndex stop; int64 problem_matrix_index; int64 value_matrix_index; + std::string start_point_id; std::vector vehicle_indices; std::vector initial_capacity; std::vector initial_load; @@ -647,8 +657,9 @@ class TSPTWDataDT { struct TSPTWClient { // Depot definition - TSPTWClient(std::string cust_id, int32 m_i, int32 p_i) + TSPTWClient(std::string cust_id, std::string p_id, int32 m_i, int32 p_i) : customer_id(cust_id) + , point_id(p_id) , matrix_index(m_i) , problem_index(p_i) , alternative_index(0) @@ -662,12 +673,13 @@ class TSPTWDataDT { , late_multiplier(0) , is_break(false) {} // Mission definition - TSPTWClient(std::string cust_id, int32 m_i, int32 p_i, int32 a_i, + TSPTWClient(std::string cust_id, std::string p_id, int32 m_i, int32 p_i, int32 a_i, std::vector r_t, std::vector d_t, std::vector& max_lateness, double s_t, double s_v, double st_t, int32 p_t, double l_m, std::vector& v_i, std::vector& q, std::vector& s_q, int64 e_c, std::vector& r_q) : customer_id(cust_id) + , point_id(p_id) , matrix_index(m_i) , problem_index(p_i) , alternative_index(a_i) @@ -686,6 +698,7 @@ class TSPTWDataDT { , refill_quantities(r_q) , is_break(false) {} std::string customer_id; + std::string point_id; int32 matrix_index; int32 problem_index; int32 alternative_index; @@ -833,10 +846,10 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { size_problem_ = std::max(size_problem_, service.problem_index()); tsptw_clients_.push_back(TSPTWClient( - (std::string)service.id(), matrix_index, service.problem_index(), - alternative_size_map_[service.problem_index()], start, end, max_lateness, - service.duration(), service.additional_value(), service.setup_duration(), - service.priority(), + (std::string)service.id(), (std::string)service.point_id(), matrix_index, + service.problem_index(), alternative_size_map_[service.problem_index()], + start, end, max_lateness, service.duration(), service.additional_value(), + service.setup_duration(), service.priority(), start.size() > 0 ? (int64)(service.late_multiplier() * CUSTOM_BIGNUM_COST) : 0, v_i, q, s_q, @@ -874,10 +887,10 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { matrix_indices.push_back(service.matrix_index()); size_problem_ = std::max(size_problem_, service.problem_index()); tsptw_clients_.push_back(TSPTWClient( - (std::string)service.id(), matrix_index, service.problem_index(), - alternative_size_map_[service.problem_index()], ready_time, due_time, - max_lateness, service.duration(), service.additional_value(), - service.setup_duration(), service.priority(), + (std::string)service.id(), (std::string)service.point_id(), matrix_index, + service.problem_index(), alternative_size_map_[service.problem_index()], + ready_time, due_time, max_lateness, service.duration(), + service.additional_value(), service.setup_duration(), service.priority(), ready_time.size() > 0 ? (int64)(service.late_multiplier() * CUSTOM_BIGNUM_COST) : 0, v_i, q, s_q, @@ -990,6 +1003,7 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { v->break_size = vehicle.rests().size(); v->problem_matrix_index = vehicle.matrix_index(); v->value_matrix_index = vehicle.value_matrix_index(); + v->start_point_id = vehicle.start_point_id(); v->vehicle_indices = vehicle_indices; v->time_start = (vehicle.time_window().start() - earliest_start_) > 0 ? vehicle.time_window().start() - earliest_start_ @@ -1073,7 +1087,8 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { for (Vehicle& v : tsptw_vehicles_) { v.start = RoutingIndexManager::NodeIndex(node_index); } - tsptw_clients_.push_back(TSPTWClient("vehicles_start", matrix_index, node_index)); + tsptw_clients_.push_back( + TSPTWClient("vehicles_start", "vehicles_start", matrix_index, node_index)); service_times_.push_back(0); node_index++; @@ -1082,7 +1097,8 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { v.stop = RoutingIndexManager::NodeIndex(node_index); } // node_index++; - tsptw_clients_.push_back(TSPTWClient("vehicles_end", ++matrix_index, node_index)); + tsptw_clients_.push_back( + TSPTWClient("vehicles_end", "vehicles_end", ++matrix_index, node_index)); service_times_.push_back(0); for (const ortools_vrp::Relation& relation : problem.relations()) { From 2d0c9bf63316386389b31dbbffb99aa47c3c482d Mon Sep 17 00:00:00 2001 From: halilsen Date: Tue, 14 Dec 2021 18:02:46 +0100 Subject: [PATCH 19/30] Remove dead code and fix small warnings --- Makefile | 5 +++-- tsp_simple.cc | 4 ---- tsptw_data_dt.h | 30 ++---------------------------- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/Makefile b/Makefile index 658f31b8..135f5b7d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ OR_TOOLS_TOP=../or-tools -TUTORIAL=resources +TUTORIAL=./resources # -isystem prevents most of the warnings rooted in or-tools library appearing in our compilation CFLAGS := -std=c++14 -isystem$(OR_TOOLS_TOP)/include @@ -9,6 +9,7 @@ CFLAGS := -std=c++14 -isystem$(OR_TOOLS_TOP)/include # DEVELOPMENT = true ifeq ($(DEVELOPMENT), true) CFLAGS := $(CFLAGS) -O0 -DDEBUG -ggdb3 -fsanitize=address -fkeep-inline-functions -fno-inline-small-functions + # CXX := ../callcatcher/build/scripts-3.6/callcatcher $(CXX) # comment out to check uncalled functions CXX := LSAN_OPTION=verbosity=1:log_threads=1 $(CXX) # adress sanitizer works only if the executable launched without gdb else CFLAGS := $(CFLAGS) -O3 -DNDEBUG @@ -44,7 +45,7 @@ tsp_simple.o: tsp_simple.cc ortools_vrp.pb.h \ tsptw_data_dt.h \ limits.h \ values.h - $(CXX) $(CFLAGS) -I $(TUTORIAL) -c ./tsp_simple.cc -o tsp_simple.o + $(CXX) $(CFLAGS) -isystem$(TUTORIAL) -c ./tsp_simple.cc -o tsp_simple.o tsp_simple: $(ROUTING_DEPS) tsp_simple.o ortools_vrp.pb.o ortools_result.pb.o $(OR_TOOLS_TOP)/lib/libortools.so $(CXX) $(CFLAGS) -fwhole-program tsp_simple.o ortools_vrp.pb.o ortools_result.pb.o $(OR_TOOLS_LD_FLAGS) \ diff --git a/tsp_simple.cc b/tsp_simple.cc index 5309884c..2a303ea4 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -326,10 +326,6 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, return data.DayIndexToVehicleIndex(index); }; - // Solver::IndexEvaluator1 alternative_vehicle_evaluator = [&data](int64 index) { - // return data.VehicleDayAlt(index); - // }; - std::vector next_vars; for (int i = 0; i < data.SizeMissions(); ++i) { next_vars.push_back(routing.NextVar(i)); diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 218542e1..3e52001a 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -135,10 +135,6 @@ class TSPTWDataDT { int64 Horizon() const { return horizon_; } - int64 MatrixIndex(const RoutingIndexManager::NodeIndex i) const { - return tsptw_clients_[i.value()].matrix_index; - } - int64 EarliestStart() const { return earliest_start_; } int64 MaxTime() const { return max_time_; } @@ -155,8 +151,6 @@ class TSPTWDataDT { int64 MaxValueCost() const { return max_value_cost_; } - int64 TWsCounter() const { return tws_counter_; } - int64 TwiceTWsCounter() const { return multiple_tws_counter_; } int64 DeliveriesCounter() const { return deliveries_counter_; } @@ -257,8 +251,6 @@ class TSPTWDataDT { return tsptw_clients_[i.value()].vehicle_indices; } - int32 TimeWindowsSize(const int i) const { return tws_size_[i]; } - int32 Size() const { return size_; } int32 SizeMissions() const { return size_missions_; } @@ -346,25 +338,16 @@ class TSPTWDataDT { , time_maximum_lateness(CUSTOM_MAX_INT) , late_multiplier(0) {} - int32 SizeMatrix() const { return size_matrix; } - - int32 SizeRest() const { return size_rest; } - void SetStart(const RoutingIndexManager::NodeIndex s) { - CHECK_LT(s, size); + DCHECK_LT(s, size); start = s; } void SetStop(const RoutingIndexManager::NodeIndex s) { - CHECK_LT(s, size); + DCHECK_LT(s, size); stop = s; } - int64 ReturnZero(const RoutingIndexManager::NodeIndex, - const RoutingIndexManager::NodeIndex) const { - return 0; - } - int64 Distance(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { CheckNodeIsValid(i); @@ -636,8 +619,6 @@ class TSPTWDataDT { const std::vector& Relations() const { return tsptw_relations_; } - const std::vector& VehiclesDay() const { return vehicles_day_; } - int VehicleDay(const int64 index) const { if (index < 0) { return -1; @@ -645,13 +626,6 @@ class TSPTWDataDT { return vehicles_day_[index]; } - int VehicleDayAlt(const int64 index) const { - if (index < 0) { - return CUSTOM_MAX_INT; - } - return vehicles_day_[index]; - } - private: void ProcessNewLine(char* const line); From 9526bf69b96de5c69c336a30bd3bc93b2246d034 Mon Sep 17 00:00:00 2001 From: halilsen Date: Thu, 9 Dec 2021 10:40:21 +0100 Subject: [PATCH 20/30] Fix variable naming issues --- tsptw_data_dt.h | 96 ++++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 3e52001a..4d9790f0 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -327,7 +327,7 @@ class TSPTWDataDT { , problem_matrix_index(0) , value_matrix_index(0) , start_point_id("") - , vehicle_indices(0) + , matrix_indices(0) , initial_capacity(0) , initial_load(0) , capacity(0) @@ -352,103 +352,101 @@ class TSPTWDataDT { const RoutingIndexManager::NodeIndex j) const { CheckNodeIsValid(i); CheckNodeIsValid(j); - if (vehicle_indices[i.value()] == -1 || vehicle_indices[j.value()] == -1) + if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) return 0; if (i != Start() && j != Stop() && max_ride_distance_ > 0 && data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()])) > + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()])) > max_ride_distance_) return CUSTOM_MAX_INT; return data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()])); + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); } int64 FakeDistance(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { CheckNodeIsValid(i); CheckNodeIsValid(j); - if (vehicle_indices[i.value()] == -1 || vehicle_indices[j.value()] == -1 || + if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1 || (i == Start() && free_approach) || (j == Stop() && free_return)) return 0; if (i != Start() && j != Stop() && max_ride_distance_ > 0 && data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()])) > + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()])) > max_ride_distance_) return CUSTOM_MAX_INT; return data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()])); + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); } int64 Time(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { CheckNodeIsValid(i); CheckNodeIsValid(j); - if (vehicle_indices[i.value()] == -1 || vehicle_indices[j.value()] == -1) + if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) return 0; if (i != Start() && j != Stop() && max_ride_time_ > 0 && data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()])) > - max_ride_time_) + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()])) > max_ride_time_) return CUSTOM_MAX_INT; return data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()])); + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); } int64 FakeTime(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { CheckNodeIsValid(i); CheckNodeIsValid(j); - if (vehicle_indices[i.value()] == -1 || vehicle_indices[j.value()] == -1 || + if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1 || (i == Start() && free_approach) || (j == Stop() && free_return)) return 0; if (i != Start() && j != Stop() && max_ride_time_ > 0 && data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()])) > - max_ride_time_) + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()])) > max_ride_time_) return CUSTOM_MAX_INT; return data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()])); + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); } int64 Value(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { CheckNodeIsValid(i); CheckNodeIsValid(j); - if (vehicle_indices[i.value()] == -1 || vehicle_indices[j.value()] == -1) + if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) return 0; return data->values_matrices_[value_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()])); + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); } int64 TimeOrder(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { CheckNodeIsValid(i); CheckNodeIsValid(j); - if (vehicle_indices[i.value()] == -1 || vehicle_indices[j.value()] == -1) + if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) return 0; return 10 * std::sqrt(data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()]))); + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()]))); } int64 DistanceOrder(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { CheckNodeIsValid(i); CheckNodeIsValid(j); - if (vehicle_indices[i.value()] == -1 || vehicle_indices[j.value()] == -1) + if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) return 0; return 100 * std::sqrt(data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(vehicle_indices[i.value()]), - RoutingIndexManager::NodeIndex(vehicle_indices[j.value()]))); + RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), + RoutingIndexManager::NodeIndex(matrix_indices[j.value()]))); } // Transit quantity at a node "from" @@ -468,7 +466,8 @@ class TSPTWDataDT { // will violate relations as the cumul_var will be the same. // Moreover with sequence+shipment lead or-tools to try only // invalid order of nodes - if (current_time == 0 && data->SizeAlternativeRelations() > 0 && to.value() < data->SizeMissions()) { + if (current_time == 0 && data->SizeAlternativeRelations() > 0 && + to.value() < data->SizeMissions()) { ++current_time; } @@ -527,7 +526,7 @@ class TSPTWDataDT { int64 problem_matrix_index; int64 value_matrix_index; std::string start_point_id; - std::vector vehicle_indices; + std::vector matrix_indices; std::vector initial_capacity; std::vector initial_load; std::vector capacity; @@ -751,7 +750,7 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { int32 node_index = 0; int32 matrix_index = 0; int32 previous_matrix_size = 0; - std::vector matrix_indices; + std::vector service_matrix_indices; for (const ortools_vrp::Service& service : problem.services()) { if (!alternative_size_map_.count(service.problem_index())) alternative_size_map_[service.problem_index()] = 0; @@ -796,7 +795,7 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { if (timewindows.size() == 0 || (earliest_start_ < timewindows[timewindow_index]->end() + timewindows[timewindow_index]->maximum_lateness())) { - matrix_indices.push_back(service.matrix_index()); + service_matrix_indices.push_back(service.matrix_index()); std::vector start; if (timewindows.size() > 0 && (timewindows[timewindow_index]->start() - earliest_start_) > 0) @@ -824,8 +823,9 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { service.problem_index(), alternative_size_map_[service.problem_index()], start, end, max_lateness, service.duration(), service.additional_value(), service.setup_duration(), service.priority(), - start.size() > 0 ? (int64)(service.late_multiplier() * CUSTOM_BIGNUM_COST) - : 0, + timewindows.size() > 0 + ? (int64)(service.late_multiplier() * CUSTOM_BIGNUM_COST) + : 0, v_i, q, s_q, service.exclusion_cost() > 0 ? service.exclusion_cost() * CUSTOM_BIGNUM_COST : -1, @@ -858,7 +858,7 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { } } - matrix_indices.push_back(service.matrix_index()); + service_matrix_indices.push_back(service.matrix_index()); size_problem_ = std::max(size_problem_, service.problem_index()); tsptw_clients_.push_back(TSPTWClient( (std::string)service.id(), (std::string)service.point_id(), matrix_index, @@ -877,11 +877,11 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { ids_map_[(std::string)service.id()] = node_index; node_index++; } - if (previous_matrix_size == (int32)matrix_indices.size()) { + if (previous_matrix_size == (int32)service_matrix_indices.size()) { throw std::invalid_argument( "A Service transmitted should always lead to at least one Node"); } - previous_matrix_size = matrix_indices.size(); + previous_matrix_size = service_matrix_indices.size(); ++matrix_index; } @@ -923,8 +923,8 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { std::vector max_times(MaxTimes(matrix)); int64 matrix_sum_time = 0; if (sqrt(matrix.time_size()) > 0) { - for (std::size_t i = 0; i < matrix_indices.size(); i++) { - matrix_sum_time += max_times.at(matrix_indices[i]); + for (std::size_t i = 0; i < service_matrix_indices.size(); i++) { + matrix_sum_time += max_times.at(service_matrix_indices[i]); } } sum_max_time_ = std::max(sum_max_time_, matrix_sum_time); @@ -954,9 +954,9 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { auto v = tsptw_vehicles_.rbegin(); // Every vehicle has its own matrix definition - std::vector vehicle_indices(matrix_indices); - vehicle_indices.push_back(vehicle.start_index()); - vehicle_indices.push_back(vehicle.end_index()); + std::vector matrix_indices(service_matrix_indices); + matrix_indices.push_back(vehicle.start_index()); + matrix_indices.push_back(vehicle.end_index()); for (const ortools_vrp::Capacity& capacity : vehicle.capacities()) { v->capacity.push_back(std::round(capacity.limit() * CUSTOM_BIGNUM_QUANTITY)); @@ -977,8 +977,8 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { v->break_size = vehicle.rests().size(); v->problem_matrix_index = vehicle.matrix_index(); v->value_matrix_index = vehicle.value_matrix_index(); - v->start_point_id = vehicle.start_point_id(); - v->vehicle_indices = vehicle_indices; + v->start_point_id = vehicle.start_point_id(); + v->matrix_indices = matrix_indices; v->time_start = (vehicle.time_window().start() - earliest_start_) > 0 ? vehicle.time_window().start() - earliest_start_ : 0; From 756f886045b971000269bf250271ee5b773fb70e Mon Sep 17 00:00:00 2001 From: halilsen Date: Mon, 13 Dec 2021 12:07:17 +0100 Subject: [PATCH 21/30] Simplify Time & Distance functions --- tsptw_data_dt.h | 52 ++++++++----------------------------------------- 1 file changed, 8 insertions(+), 44 deletions(-) diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 4d9790f0..d14753a6 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -367,20 +367,10 @@ class TSPTWDataDT { int64 FakeDistance(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { - CheckNodeIsValid(i); - CheckNodeIsValid(j); - if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1 || - (i == Start() && free_approach) || (j == Stop() && free_return)) + if ((i == Start() && free_approach) || (j == Stop() && free_return)) return 0; - if (i != Start() && j != Stop() && max_ride_distance_ > 0 && - data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()])) > - max_ride_distance_) - return CUSTOM_MAX_INT; - return data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); + + return Distance(i, j); } int64 Time(const RoutingIndexManager::NodeIndex i, @@ -401,19 +391,10 @@ class TSPTWDataDT { int64 FakeTime(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { - CheckNodeIsValid(i); - CheckNodeIsValid(j); - if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1 || - (i == Start() && free_approach) || (j == Stop() && free_return)) + if ((i == Start() && free_approach) || (j == Stop() && free_return)) return 0; - if (i != Start() && j != Stop() && max_ride_time_ > 0 && - data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()])) > max_ride_time_) - return CUSTOM_MAX_INT; - return data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); + + return Time(i, j); } int64 Value(const RoutingIndexManager::NodeIndex i, @@ -429,24 +410,12 @@ class TSPTWDataDT { int64 TimeOrder(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { - CheckNodeIsValid(i); - CheckNodeIsValid(j); - if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) - return 0; - return 10 * std::sqrt(data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()]))); + return 10 * std::sqrt(Time(i, j)); } int64 DistanceOrder(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { - CheckNodeIsValid(i); - CheckNodeIsValid(j); - if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) - return 0; - return 100 * std::sqrt(data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()]))); + return 100 * std::sqrt(Distance(i, j)); } // Transit quantity at a node "from" @@ -497,11 +466,6 @@ class TSPTWDataDT { return Time(from, to) + data->ServiceValue(from); } - int64 TimePlus(const RoutingIndexManager::NodeIndex from, - const RoutingIndexManager::NodeIndex to) const { - return Time(from, to); - } - RoutingIndexManager::NodeIndex Start() const { return start; } RoutingIndexManager::NodeIndex Stop() const { return stop; } From 84dea87b70ff24e771f9a5ae652da41758eac32a Mon Sep 17 00:00:00 2001 From: halilsen Date: Tue, 14 Dec 2021 14:31:56 +0100 Subject: [PATCH 22/30] Fix: value evaluator should not call time function --- tsptw_data_dt.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index d14753a6..b7dd1dc2 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -463,7 +463,7 @@ class TSPTWDataDT { int64 ValuePlusServiceValue(const RoutingIndexManager::NodeIndex from, const RoutingIndexManager::NodeIndex to) const { - return Time(from, to) + data->ServiceValue(from); + return Value(from, to) + data->ServiceValue(from); } RoutingIndexManager::NodeIndex Start() const { return start; } From f35e8efa9ac30c2ba95d89634ba1f0e9cf4f7a53 Mon Sep 17 00:00:00 2001 From: halilsen Date: Mon, 7 Feb 2022 16:01:37 +0100 Subject: [PATCH 23/30] Respect variables type defined in protobuf TODO: There are other class and interim variables in violation of this. --- tsp_simple.cc | 10 ++++------ tsptw_data_dt.h | 16 ++++++++-------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/tsp_simple.cc b/tsp_simple.cc index 2a303ea4..44010d53 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -287,8 +287,8 @@ RestBuilder(const TSPTWDataDT& data, RoutingModel& routing, const int64 horizon) const TSPTWDataDT::Vehicle& vehicle = data.Vehicles(vehicle_index); for (const TSPTWDataDT::Rest& rest : vehicle.Rests()) { IntervalVar* const rest_interval = solver->MakeFixedDurationIntervalVar( - std::max(rest.ready_time, vehicle.time_start), - std::min(rest.due_time, vehicle.time_end - rest.duration), + std::max(rest.ready_time, vehicle.time_start), + std::min(rest.due_time, vehicle.time_end - rest.duration), rest.duration, // Currently only one timewindow false, absl::StrCat("Rest/", rest.rest_id, "/", vehicle_index)); rest_array.push_back(rest_interval); @@ -1230,11 +1230,9 @@ void AddVehicleDistanceConstraints(const TSPTWDataDT& data, RoutingModel& routin routing.GetDimensionOrDie(kDistance); for (const TSPTWDataDT::Vehicle& vehicle : data.Vehicles()) { if (vehicle.distance > 0) { - const int64 end_index = routing.End(v); // Vehicle maximum distance - IntVar* const dist_end_cumul_var = distance_dimension.CumulVar(end_index); - solver->AddConstraint( - solver->MakeLessOrEqual(dist_end_cumul_var, vehicle.distance)); + solver->AddConstraint(solver->MakeLessOrEqual( + distance_dimension.CumulVar(routing.End(v)), vehicle.distance)); } ++v; } diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index b7dd1dc2..33889160 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -315,9 +315,9 @@ class TSPTWDataDT { , due_time(std::min(CUSTOM_MAX_INT, due_t)) , duration(dur) {} std::string rest_id; - int64 ready_time; - int64 due_time; - int64 duration; + uint32 ready_time; + uint32 due_time; + uint32 duration; }; struct Vehicle { @@ -515,8 +515,8 @@ class TSPTWDataDT { int64 distance; ShiftPref shift_preference; int32 day_index; - int64 max_ride_time_; - int64 max_ride_distance_; + uint32 max_ride_time_; + uint32 max_ride_distance_; bool free_approach; bool free_return; }; @@ -970,7 +970,7 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { max_coef_setup_ = std::max(max_coef_setup_, v->coef_setup); v->additional_setup = vehicle.additional_setup(); - v->duration = (int64)(vehicle.duration()); + v->duration = vehicle.duration(); v->distance = vehicle.distance(); v->free_approach = vehicle.free_approach(); v->free_return = vehicle.free_return(); @@ -1112,7 +1112,7 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { } for (std::size_t v = 0; v < tsptw_vehicles_.size(); v++) { for (std::size_t r = 0; r < tsptw_vehicles_[v].Rests().size(); r++) { - horizon_ = std::max(horizon_, tsptw_vehicles_[v].Rests()[r].due_time); + horizon_ = std::max(horizon_, tsptw_vehicles_[v].Rests()[r].due_time); } } } else { @@ -1132,7 +1132,7 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { for (std::size_t r = 0; r < tsptw_vehicles_[v].Rests().size(); r++) { latest_rest_end = - std::max(latest_rest_end, tsptw_vehicles_[v].Rests()[r].due_time); + std::max(latest_rest_end, tsptw_vehicles_[v].Rests()[r].due_time); } } horizon_ = std::max(latest_start, latest_rest_end) + sum_service * max_coef_service_ + From 5fc2311904d27f755731411da30f744feb1b7141 Mon Sep 17 00:00:00 2001 From: halilsen Date: Mon, 7 Feb 2022 17:30:42 +0100 Subject: [PATCH 24/30] Matrix refacto: Use existing proto matrix --- ...test_ortools_single_route_with_route_order | Bin 88313 -> 71485 bytes data/test_with_cluster | Bin 0 -> 329 bytes ortools_vrp.proto | 1 + ortools_vrp_pb.rb | 1 + tsp_simple.cc | 17 +- tsptw_data_dt.h | 242 +++++++----------- 6 files changed, 105 insertions(+), 156 deletions(-) create mode 100644 data/test_with_cluster diff --git a/data/test_ortools_single_route_with_route_order b/data/test_ortools_single_route_with_route_order index 2d7596c0ddf71c503115c728b2bc55aa9f3f9566..6a027c90788479064d9ef4204178ed480e5d8ecb 100644 GIT binary patch literal 71485 zcmeF4dz_7B+xM?QQ%X`KBu!|F#IUkWTFhERDrHmdRNAdOnMx^9stYBXNz+bdrHoCs zLqa0E6_U*?*(D^6O=Fvglv2Il-?6T1tf%*W?)QG4_w&4eyq|l0KEL}och`BH=kGYq z<2=su8r@Y|pIxd?gN*vs&pFH8nNlvczC@**stqstczu(w)uD*l$1p&s{PFVw|b?tQngc@c4zi(&?KW#qo$psxw$PKp?*pA zs#cSc;?*_II9=B_#jh>l*Va#}E#cRe^lKX=)t2;YOZl~dq}oz`?HPV;!=&0X{MuB% zwoy`Ts$YAiU)wmT_DsL_EWfr%QterOZE3$YiHp9a{o1p=+9t_tobA`1|{MvK9+WN_(I@hl)>(?fas;pmoo>$u-x##oz+H!tvGSB7w+VlO| zYcKF>1IayK;MbP-Ym<2{@7GrFYm;YG!LPm0tGy<<=L`MXihgafT~+jJFY;@X?dl@G zwvu0)Jl{%wZDqgqnk4O4_G_#7waFT+;@4j6*Ct!t#eQv7zczWkRsGsayxK;|GrGjD zt>)Jz�PTd#PWWJflnf+BCm5c}8h|?PY##@{BI?YpeUU$up|%*VgcBll5D}udV6V zCeOE~UwgSi{MuwTuJCKG^lOvZxYDn!<<};&QOmEb?bjx= zQQNP*%CAjk<0`+lj$fP1MjgNQYQHv_jjR3Ix_)gk8+HBKdS2~+@GABE+6=!onX3%H zw!UAR%vF8Awt-)p%vA%wHsIGLa~1GwukmY>xw^)$ZRpn~v(eD6ZRFP`v(d<}ZS2=3 zv(ebEZQ|87O`dNPzqYAgn>^p9er+?qHhI3y{Mu{%+T{6O>(@5-Ym>F#+^@aPuT7rs zb$)FNzczWkE&ST+{o3UDUhmfi6SYnL!@dyoYg_uY$@6XL*S7L&ljqyYug&ypljoc1 z*M|JsWc`Nx+OS`n%tqL+ZSB`4v(ehGZR6J_v(d({y}_?dX5$9GwyjrNKY3qh>({pP zYm;rGonL#SUz^Ovjec!=zc!hT_I~Y6er+-vH~Fqv*x7IMYdiY2$!v7=YdiV1$(GZ}ukGyDCUe!*v}0*PPvvOTRQI0OyW_} zIiOT4$B7~LfxINlB)x;WEZWA@CINQ=`6Zwv_%PFP2O}3DDYv>PdEmZ}90Q&Qd0;k} z1=fQi>UV&JU@G_+{7kv}rcdkltdQd@B^?91s4q#rRLF5xk^Y{tP2d>muaUojuSh%8 zZ%EX|&ZNEi_?7ZM6L1bvSDLaz$P-{E<@6ORLD^o?sXVXpGeS=6Y|_6_wgu=J@#IV$GvDf59vePii0x& z9J)i0@b9V|IHQs8ASZ#bU;r3L*&HM=KK7CR2Eea78GHmzg8ATs1dK1<<1VJ%9*+}o zzLNY8l*6lg6l?{0_Djm))$~AJOniuMIE$eN!G-6TxH>?)<2>^%Qe`QWeTRf+=ci03 zD7~ITS~eg(!nH#_h>VGE=|cU$buiVFH7UQA_JTN01NynwJMIX|3WEub)8O(K>dGLu zAbrT*k5l$EZ3~bdjO*o;m1P`rk=aPu;in0>W01h?b^`K6AX}e@{1~|zDLcbvWxMWl z@CoVPk#H-$2%4Mo8R=yJ+Y7>lD?Z}ne+1$ITeNlZ2XY&IV5chgIWnHqugZaQgfzUl z=*y9g(4&LSj9+vR^wxvy);*W@r>Ngfc^P;H4sgqn7WV?2OQ(`fI%q|EL0p#uaBY

l*6~HwF2MLgXtoPZwI~|_;x(L9glCVYgcP| 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 z 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≦dwUkz%us7%A8qC6O#>HfQ`&&-{g*l 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 WxyOv*LpyK&|J_aINBzIs-Twz5h&n?6 literal 88313 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 literal 0 HcmV?d00001 diff --git a/ortools_vrp.proto b/ortools_vrp.proto index 4b8d3091..5c78bbad 100644 --- a/ortools_vrp.proto +++ b/ortools_vrp.proto @@ -4,6 +4,7 @@ package ortools_vrp; option optimize_for = SPEED; message Matrix { + uint32 size = 1; repeated float time = 2 [ packed = true ]; repeated float distance = 3 [ packed = true ]; repeated float value = 4 [ packed = true ]; diff --git a/ortools_vrp_pb.rb b/ortools_vrp_pb.rb index 5d9c01ad..cfa5dcdc 100644 --- a/ortools_vrp_pb.rb +++ b/ortools_vrp_pb.rb @@ -6,6 +6,7 @@ Google::Protobuf::DescriptorPool.generated_pool.build do add_file("ortools_vrp.proto", :syntax => :proto3) do add_message "ortools_vrp.Matrix" do + optional :size, :uint32, 1 repeated :time, :float, 2 repeated :distance, :float, 3 repeated :value, :float, 4 diff --git a/tsp_simple.cc b/tsp_simple.cc index 44010d53..8b787414 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -1117,11 +1117,18 @@ void AddValueDimensions(const TSPTWDataDT& data, RoutingModel& routing, RoutingIndexManager& manager) { std::vector value_evaluators; for (const TSPTWDataDT::Vehicle& vehicle : data.Vehicles()) { - value_evaluators.push_back(routing.RegisterTransitCallback( - [&vehicle, &manager](const int64 i, const int64 j) { - return vehicle.ValuePlusServiceValue(manager.IndexToNode(i), - manager.IndexToNode(j)); - })); + if (vehicle.value_matrix->value().empty()) { + value_evaluators.push_back( + routing.RegisterTransitCallback([&data, &manager](const int64 i, const int64) { + return data.ServiceValue(manager.IndexToNode(i)); + })); + } else { + value_evaluators.push_back(routing.RegisterTransitCallback( + [&vehicle, &data, &manager](const int64 i, const int64 j) { + return vehicle.Value(manager.IndexToNode(i), manager.IndexToNode(j)) + + data.ServiceValue(manager.IndexToNode(i)); + })); + } } routing.AddDimensionWithVehicleTransits(value_evaluators, 0, LLONG_MAX, true, kValue); int v = 0; diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 33889160..8fc4cb7c 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -21,6 +21,13 @@ #define CUSTOM_BIGNUM_QUANTITY 1e3 // Needs to stay smaller than CUSTOM_BIGNUM_COST +static bool compare_less_than_custom_max_int(const float& value1, const float& value2) { + if (value2 < CUSTOM_MAX_INT && value1 < value2) + return true; + else + return false; +} + enum RelationType { MinimumDurationLapse = 15, VehicleGroupNumber = 14, @@ -73,66 +80,6 @@ class TSPTWDataDT { void LoadInstance(const std::string& filename); - // Helper function - int64& SetDistMatrix(const int i, const int j) { - return distances_matrices_.back().Cost(RoutingIndexManager::NodeIndex(i), - RoutingIndexManager::NodeIndex(j)); - } - - int64& SetTimeMatrix(const int i, const int j) { - return times_matrices_.back().Cost(RoutingIndexManager::NodeIndex(i), - RoutingIndexManager::NodeIndex(j)); - } - - int64& SetValueMatrix(const int i, const int j) { - return values_matrices_.back().Cost(RoutingIndexManager::NodeIndex(i), - RoutingIndexManager::NodeIndex(j)); - } - - int64 BuildTimeMatrix(const ortools_vrp::Matrix& matrix) { - int64 max_time = 0; - const int32 size_matrix = sqrt(matrix.time_size()); - for (int64 i = 0; i < size_matrix; ++i) { - for (int64 j = 0; j < size_matrix; ++j) { - const int64 time = matrix.time(i * size_matrix + j) + 0.5; - if (time < CUSTOM_MAX_INT) - max_time = std::max(max_time, time); - SetTimeMatrix(i, j) = time; - } - // std::cout << std::endl; - } - - return max_time; - } - - int64 BuildDistanceMatrix(const ortools_vrp::Matrix& matrix) { - int64 max_distance = 0; - const int32 size_matrix = sqrt(matrix.distance_size()); - for (int64 i = 0; i < size_matrix; ++i) { - for (int64 j = 0; j < size_matrix; ++j) { - const int64 distance = matrix.distance(i * size_matrix + j); - if (distance < CUSTOM_MAX_INT) - max_distance = std::max(max_distance, distance); - SetDistMatrix(i, j) = distance; - } - } - return max_distance; - } - - int64 BuildValueMatrix(const ortools_vrp::Matrix& matrix) { - int64 max_value = 0; - const int32 size_matrix = sqrt(matrix.value_size()); - for (int64 i = 0; i < size_matrix; ++i) { - for (int64 j = 0; j < size_matrix; ++j) { - const int64 value = matrix.value(i * size_matrix + j); - if (value < CUSTOM_MAX_INT) - max_value = std::max(max_value, value); - SetValueMatrix(i, j) = value; - } - } - return max_value; - } - int64 Horizon() const { return horizon_; } int64 EarliestStart() const { return earliest_start_; } @@ -293,13 +240,12 @@ class TSPTWDataDT { } std::vector MaxTimes(const ortools_vrp::Matrix& matrix) const { - int64 max_row; - int32 size_matrix = sqrt(matrix.time_size()); + const uint32 matrix_size = matrix.size(); std::vector max_times; - for (int32 i = 0; i < size_matrix; i++) { - max_row = 0; - for (int32 j = 0; j < size_matrix; j++) { - int64 cell = matrix.time(i * size_matrix + j); + for (uint32 i = 0; i < matrix_size; i++) { + int64 max_row = 0; + for (uint32 j = 0; j < matrix_size; j++) { + int64 cell = matrix.time(i * matrix_size + j); if (cell + 0.5 < CUSTOM_MAX_INT) max_row = std::max(max_row, (int64)(cell + 0.5)); } @@ -321,11 +267,12 @@ class TSPTWDataDT { }; struct Vehicle { - Vehicle(TSPTWDataDT* data_, int32 size_) + Vehicle(TSPTWDataDT* data_, int32 size_, const ortools_vrp::Matrix& matrix_, + const ortools_vrp::Matrix& value_matrix_) : data(data_) , size(size_) - , problem_matrix_index(0) - , value_matrix_index(0) + , matrix(&matrix_) + , value_matrix(&value_matrix_) , start_point_id("") , matrix_indices(0) , initial_capacity(0) @@ -348,45 +295,59 @@ class TSPTWDataDT { stop = s; } - int64 Distance(const RoutingIndexManager::NodeIndex i, - const RoutingIndexManager::NodeIndex j) const { + inline int MatrixIndex(const RoutingIndexManager::NodeIndex i, + const RoutingIndexManager::NodeIndex j, + const size_t matrix_size) const { CheckNodeIsValid(i); CheckNodeIsValid(j); - if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) - return 0; - if (i != Start() && j != Stop() && max_ride_distance_ > 0 && - data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()])) > - max_ride_distance_) - return CUSTOM_MAX_INT; - return data->distances_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); + const auto i_matrix_index = matrix_indices[i.value()]; + const auto j_matrix_index = matrix_indices[j.value()]; + + if (i_matrix_index == -1 || j_matrix_index == -1) + return -1; + + DCHECK_LT(i_matrix_index, matrix_size); + DCHECK_LT(j_matrix_index, matrix_size); + + return i_matrix_index * matrix_size + j_matrix_index; } - int64 FakeDistance(const RoutingIndexManager::NodeIndex i, - const RoutingIndexManager::NodeIndex j) const { - if ((i == Start() && free_approach) || (j == Stop() && free_return)) + int64 Distance(const RoutingIndexManager::NodeIndex i, + const RoutingIndexManager::NodeIndex j) const { + const auto index = MatrixIndex(i, j, matrix->size()); + if (index == -1) return 0; - return Distance(i, j); + const auto dist = matrix->distance(index); + + if (max_ride_distance_ > 0 && i != Start() && j != Stop() && + dist > max_ride_distance_) + return CUSTOM_MAX_INT; + + return dist; } int64 Time(const RoutingIndexManager::NodeIndex i, const RoutingIndexManager::NodeIndex j) const { - CheckNodeIsValid(i); - CheckNodeIsValid(j); - if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) + const auto index = MatrixIndex(i, j, matrix->size()); + if (index == -1) return 0; - if (i != Start() && j != Stop() && max_ride_time_ > 0 && - data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()])) > max_ride_time_) + + const auto time = matrix->time(index); + + if (max_ride_time_ > 0 && i != Start() && j != Stop() && time > max_ride_time_) return CUSTOM_MAX_INT; - return data->times_matrices_[problem_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); + + return time; + } + + int64 Value(const RoutingIndexManager::NodeIndex i, + const RoutingIndexManager::NodeIndex j) const { + const auto index = MatrixIndex(i, j, value_matrix->size()); + if (index == -1) + return 0; + + return value_matrix->value(index); } int64 FakeTime(const RoutingIndexManager::NodeIndex i, @@ -397,15 +358,12 @@ class TSPTWDataDT { return Time(i, j); } - int64 Value(const RoutingIndexManager::NodeIndex i, - const RoutingIndexManager::NodeIndex j) const { - CheckNodeIsValid(i); - CheckNodeIsValid(j); - if (matrix_indices[i.value()] == -1 || matrix_indices[j.value()] == -1) + int64 FakeDistance(const RoutingIndexManager::NodeIndex i, + const RoutingIndexManager::NodeIndex j) const { + if ((i == Start() && free_approach) || (j == Stop() && free_return)) return 0; - return data->values_matrices_[value_matrix_index].Cost( - RoutingIndexManager::NodeIndex(matrix_indices[i.value()]), - RoutingIndexManager::NodeIndex(matrix_indices[j.value()])); + + return Distance(i, j); } int64 TimeOrder(const RoutingIndexManager::NodeIndex i, @@ -461,11 +419,6 @@ class TSPTWDataDT { : 0); } - int64 ValuePlusServiceValue(const RoutingIndexManager::NodeIndex from, - const RoutingIndexManager::NodeIndex to) const { - return Value(from, to) + data->ServiceValue(from); - } - RoutingIndexManager::NodeIndex Start() const { return start; } RoutingIndexManager::NodeIndex Stop() const { return stop; } @@ -483,12 +436,12 @@ class TSPTWDataDT { std::string id; int64 vehicle_index; int32 size; + const ortools_vrp::Matrix* const matrix; + const ortools_vrp::Matrix* const value_matrix; int32 size_matrix; int32 size_rest; RoutingIndexManager::NodeIndex start; RoutingIndexManager::NodeIndex stop; - int64 problem_matrix_index; - int64 value_matrix_index; std::string start_point_id; std::vector matrix_indices; std::vector initial_capacity; @@ -590,6 +543,8 @@ class TSPTWDataDT { } private: + ortools_vrp::Problem problem; + void ProcessNewLine(char* const line); struct TSPTWClient { @@ -683,9 +638,6 @@ class TSPTWDataDT { std::vector tsptw_clients_; std::map alternative_size_map_; std::vector tsptw_routes_; - std::vector distances_matrices_; - std::vector times_matrices_; - std::vector values_matrices_; std::vector vehicles_day_; std::vector service_times_; std::string details_; @@ -697,8 +649,6 @@ class TSPTWDataDT { void TSPTWDataDT::LoadInstance(const std::string& filename) { GOOGLE_PROTOBUF_VERIFY_VERSION; - ortools_vrp::Problem problem; - { std::fstream input(filename, std::ios::in | std::ios::binary); if (!problem.ParseFromIstream(&input)) { @@ -860,33 +810,16 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { size_ = node_index + 2; for (const ortools_vrp::Matrix& matrix : problem.matrices()) { - // + 2 In case vehicles have no depots - int32 problem_size = - std::max(std::max(sqrt(matrix.distance_size()), sqrt(matrix.time_size())), - sqrt(matrix.value_size())) + - 2 + (size_rest_ > 0 ? 1 : 0); - - distances_matrices_.emplace_back(std::max(problem_size, 3)); - times_matrices_.emplace_back(std::max(problem_size, 3)); - values_matrices_.emplace_back(std::max(problem_size, 3)); - - // Matrix default values - for (int64 i = 0; i < std::max(problem_size, 3); ++i) { - for (int64 j = 0; j < std::max(problem_size, 3); ++j) { - SetTimeMatrix(i, j) = 0; - SetDistMatrix(i, j) = 0; - SetValueMatrix(i, j) = 0; - } - } - - if (matrix.time_size() > 0) { - max_time_ = std::max(max_time_, BuildTimeMatrix(matrix)); - } - // Estimate necessary horizon due to time matrix std::vector max_times(MaxTimes(matrix)); int64 matrix_sum_time = 0; - if (sqrt(matrix.time_size()) > 0) { + if (matrix.size() > 0) { + const int64 max_time = std::round(*std::max_element( + max_times.begin(), max_times.end(), compare_less_than_custom_max_int)); + + if (max_time < CUSTOM_MAX_INT) + max_time_ = std::max(max_time_, max_time); + for (std::size_t i = 0; i < service_matrix_indices.size(); i++) { matrix_sum_time += max_times.at(service_matrix_indices[i]); } @@ -894,11 +827,19 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { sum_max_time_ = std::max(sum_max_time_, matrix_sum_time); if (matrix.distance_size() > 0) { - max_distance_ = std::max(max_distance_, BuildDistanceMatrix(matrix)); + const int64 max_distance = + std::round(*std::max_element(matrix.distance().begin(), matrix.distance().end(), + compare_less_than_custom_max_int)); + if (max_distance < CUSTOM_MAX_INT) + max_distance_ = std::max(max_distance_, max_distance); } if (matrix.value_size() > 0) { - max_value_ = std::max(max_value_, BuildValueMatrix(matrix)); + const int64 max_value = + std::round(*std::max_element(matrix.value().begin(), matrix.value().end(), + compare_less_than_custom_max_int)); + if (max_value < CUSTOM_MAX_INT) + max_value_ = std::max(max_value_, max_value); } } @@ -914,7 +855,8 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { "A vehicle should always have an initialized timewindow"); } - tsptw_vehicles_.emplace_back(this, size_); + tsptw_vehicles_.emplace_back(this, size_, problem.matrices(vehicle.matrix_index()), + problem.matrices(vehicle.value_matrix_index())); auto v = tsptw_vehicles_.rbegin(); // Every vehicle has its own matrix definition @@ -936,14 +878,12 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { v->counting.push_back(capacity.counting()); } - v->id = vehicle.id(); - v->vehicle_index = v_idx; - v->break_size = vehicle.rests().size(); - v->problem_matrix_index = vehicle.matrix_index(); - v->value_matrix_index = vehicle.value_matrix_index(); - v->start_point_id = vehicle.start_point_id(); - v->matrix_indices = matrix_indices; - v->time_start = (vehicle.time_window().start() - earliest_start_) > 0 + v->id = vehicle.id(); + v->vehicle_index = v_idx; + v->break_size = vehicle.rests().size(); + v->start_point_id = vehicle.start_point_id(); + v->matrix_indices = matrix_indices; + v->time_start = (vehicle.time_window().start() - earliest_start_) > 0 ? vehicle.time_window().start() - earliest_start_ : 0; v->time_end = (vehicle.time_window().end() - earliest_start_) < CUSTOM_MAX_INT From d920d248ded368377fb2b4c3a57668fd604fd9ba Mon Sep 17 00:00:00 2001 From: Pierre Graber Date: Mon, 1 Aug 2022 16:57:43 +0200 Subject: [PATCH 25/30] Apply positions only for concerned activities --- ortools_vrp.proto | 1 + ortools_vrp_pb.rb | 1 + tsp_simple.cc | 51 +++++++++++++++++++++++++++++++---------------- tsptw_data_dt.h | 48 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 17 deletions(-) diff --git a/ortools_vrp.proto b/ortools_vrp.proto index 4b8d3091..96f26ce4 100644 --- a/ortools_vrp.proto +++ b/ortools_vrp.proto @@ -31,6 +31,7 @@ message Service { repeated bool refill_quantities = 14; uint32 problem_index = 15; string point_id = 16; + uint32 alternative_index = 17; } message Rest { diff --git a/ortools_vrp_pb.rb b/ortools_vrp_pb.rb index 5d9c01ad..1f616e32 100644 --- a/ortools_vrp_pb.rb +++ b/ortools_vrp_pb.rb @@ -31,6 +31,7 @@ repeated :refill_quantities, :bool, 14 optional :problem_index, :uint32, 15 optional :point_id, :string, 16 + optional :alternative_index, :uint32, 17 end add_message "ortools_vrp.Rest" do optional :time_window, :message, 1, "ortools_vrp.TimeWindow" diff --git a/tsp_simple.cc b/tsp_simple.cc index 5309884c..dc64d37f 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -725,11 +725,16 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, break; case NeverFirst: for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { - current_index = data.IdIndex(relation.linked_ids[link_index]); - int32 service_index = - data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); - alternative_size = data.AlternativeSize(service_index); - + current_index = data.AlternativeActivityIndex(relation.linked_ids[link_index]); + if (current_index >= 0) { + alternative_size = + data.AlternativeActivitySize(relation.linked_ids[link_index]); + } else { + current_index = data.IdIndex(relation.linked_ids[link_index]); + alternative_size = data.AlternativeSize(current_index); + } + std::cout << "alternative_size :" << alternative_size + << " current_index : " << current_index << std::endl; for (int64 alternative_index = current_index; alternative_index < current_index + alternative_size; ++alternative_index) { for (std::size_t v = 0; v < data.Vehicles().size(); ++v) { @@ -747,10 +752,14 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, } for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { - current_index = data.IdIndex(relation.linked_ids[link_index]); - int32 service_index = - data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); - alternative_size = data.AlternativeSize(service_index); + current_index = data.AlternativeActivityIndex(relation.linked_ids[link_index]); + if (current_index >= 0) { + alternative_size = + data.AlternativeActivitySize(relation.linked_ids[link_index]); + } else { + current_index = data.IdIndex(relation.linked_ids[link_index]); + alternative_size = data.AlternativeSize(current_index); + } for (int64 alternative_index = current_index; alternative_index < current_index + alternative_size; ++alternative_index) { @@ -769,10 +778,14 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, break; case NeverLast: for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { - current_index = data.IdIndex(relation.linked_ids[link_index]); - int32 service_index = - data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); - alternative_size = data.AlternativeSize(service_index); + current_index = data.AlternativeActivityIndex(relation.linked_ids[link_index]); + if (current_index >= 0) { + alternative_size = + data.AlternativeActivitySize(relation.linked_ids[link_index]); + } else { + current_index = data.IdIndex(relation.linked_ids[link_index]); + alternative_size = data.AlternativeSize(current_index); + } for (int64 alternative_index = current_index; alternative_index < current_index + alternative_size; ++alternative_index) { @@ -786,10 +799,14 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, break; case ForceLast: for (int link_index = 0; link_index < relation.linked_ids.size(); ++link_index) { - current_index = data.IdIndex(relation.linked_ids[link_index]); - int32 service_index = - data.ProblemIndex(RoutingIndexManager::NodeIndex(current_index)); - alternative_size = data.AlternativeSize(service_index); + current_index = data.AlternativeActivityIndex(relation.linked_ids[link_index]); + if (current_index >= 0) { + alternative_size = + data.AlternativeActivitySize(relation.linked_ids[link_index]); + } else { + current_index = data.IdIndex(relation.linked_ids[link_index]); + alternative_size = data.AlternativeSize(current_index); + } for (int64 alternative_index = current_index; alternative_index < current_index + alternative_size; ++alternative_index) { diff --git a/tsptw_data_dt.h b/tsptw_data_dt.h index 218542e1..cedc7f8a 100644 --- a/tsptw_data_dt.h +++ b/tsptw_data_dt.h @@ -206,6 +206,20 @@ class TSPTWDataDT { return tsptw_clients_[i.value()].alternative_index; } + int32 AlternativeActivityIndex(const std::string id) const { + if (alternative_activity_ids_map_.find(id) != alternative_activity_ids_map_.end()) { + return alternative_activity_ids_map_.at(id); + } + return -1; + } + + int32 AlternativeActivitySize(const std::string id) const { + if (alternative_activity_size_map_.find(id) != alternative_activity_size_map_.end()) { + return alternative_activity_size_map_.at(id); + } + return 0; + } + const std::vector& ReadyTime(const RoutingIndexManager::NodeIndex i) const { return tsptw_clients_[i.value()].ready_time; } @@ -702,6 +716,7 @@ class TSPTWDataDT { int32 matrix_index; int32 problem_index; int32 alternative_index; + int32 alternative_activity_index; std::vector ready_time; std::vector due_time; std::vector maximum_lateness; @@ -745,6 +760,8 @@ class TSPTWDataDT { std::vector tsptw_relations_; std::vector tsptw_clients_; std::map alternative_size_map_; + std::map alternative_activity_size_map_; + std::map alternative_activity_ids_map_; std::vector tsptw_routes_; std::vector distances_matrices_; std::vector times_matrices_; @@ -861,6 +878,23 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { alternative_size_map_[service.problem_index()] += 1; if (ids_map_.find((std::string)service.id()) == ids_map_.end()) ids_map_[(std::string)service.id()] = node_index; + + if (alternative_activity_ids_map_.find( + (std::string)service.id() + "#" + + std::to_string(service.alternative_index())) == + alternative_activity_ids_map_.end()) { + alternative_activity_ids_map_[service.id() + "#" + + std::to_string(service.alternative_index())] = + node_index; + alternative_activity_size_map_[service.id() + "#" + + std::to_string(service.alternative_index())] = + 1; + } else { + alternative_activity_size_map_[service.id() + "#" + + std::to_string(service.alternative_index())] += + 1; + } + node_index++; } ++timewindow_index; @@ -901,6 +935,20 @@ void TSPTWDataDT::LoadInstance(const std::string& filename) { alternative_size_map_[service.problem_index()] += 1; if (ids_map_.find((std::string)service.id()) == ids_map_.end()) ids_map_[(std::string)service.id()] = node_index; + + if (alternative_activity_ids_map_.find( + (std::string)service.id() + "#" + + std::to_string(service.alternative_index())) == + alternative_activity_ids_map_.end()) { + alternative_activity_ids_map_[service.id() + "#" + + std::to_string(service.alternative_index())] = + node_index; + alternative_activity_size_map_[service.id() + "#" + + std::to_string(service.alternative_index())] = 1; + } else { + alternative_activity_size_map_[service.id() + "#" + + std::to_string(service.alternative_index())] += 1; + } node_index++; } if (previous_matrix_size == (int32)matrix_indices.size()) { From dfb796f9f9336a152087ee07d2cb0d8a599ab88c Mon Sep 17 00:00:00 2001 From: Pierre Graber Date: Mon, 1 Aug 2022 16:57:43 +0200 Subject: [PATCH 26/30] Remove debug output --- tsp_simple.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/tsp_simple.cc b/tsp_simple.cc index 4fd16983..89b4a701 100644 --- a/tsp_simple.cc +++ b/tsp_simple.cc @@ -729,8 +729,6 @@ void RelationBuilder(const TSPTWDataDT& data, RoutingModel& routing, current_index = data.IdIndex(relation.linked_ids[link_index]); alternative_size = data.AlternativeSize(current_index); } - std::cout << "alternative_size :" << alternative_size - << " current_index : " << current_index << std::endl; for (int64 alternative_index = current_index; alternative_index < current_index + alternative_size; ++alternative_index) { for (std::size_t v = 0; v < data.Vehicles().size(); ++v) { From c005004081bd891e95d08bc1f9b2aa55b751061d Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Fri, 30 Dec 2022 15:15:37 +0100 Subject: [PATCH 27/30] Use ruby as base instead of the monolithic passenger --- .env | 3 +++ .github/actions/build.sh | 2 +- Dockerfile | 19 ++++--------------- docker-compose.yml | 23 +++++++++++++++++++++++ 4 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 .env create mode 100644 docker-compose.yml diff --git a/.env b/.env new file mode 100644 index 00000000..6003c50a --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +ORTOOLS_URL="https://github.com/google/or-tools/releases/download/v7.8/or-tools_debian-10_v7.8.7959.tar.gz" +ORTOOLS_VERSION=v7.8 # select from https://github.com/google/or-tools/releases +RUBY_VERSION=2.5 diff --git a/.github/actions/build.sh b/.github/actions/build.sh index a51a4c67..be75add7 100755 --- a/.github/actions/build.sh +++ b/.github/actions/build.sh @@ -31,7 +31,7 @@ case $ORTOOLS_VERSION in esac echo "Download asset at ${ORTOOLS_URL}" -docker build --build-arg ORTOOLS_URL=${ORTOOLS_URL} -f ./Dockerfile -t "${IMAGE_NAME}" . +docker build --build-arg RUBY_VERSION="2.5" --build-arg ORTOOLS_URL=${ORTOOLS_URL} -f ./Dockerfile -t "${IMAGE_NAME}" . docker run -d --name optimizer -t "${IMAGE_NAME}" docker exec -i optimizer bash -c "LD_LIBRARY_PATH=/srv/or-tools/lib/ /srv/optimizer-ortools/tsp_simple -time_limit_in_ms 500 -intermediate_solutions -instance_file '/srv/optimizer-ortools/data/test_ortools_single_route_with_route_order' -solution_file '/tmp/optimize-or-tools-output'" diff --git a/Dockerfile b/Dockerfile index 7a00957a..7d74e2a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ -# 1.0.13 is the latest version containing bundler 2 required for optimizer-api -FROM phusion/passenger-ruby25:1.0.13 +ARG RUBY_VERSION +ARG ARCHITECTURE + +FROM ${ARCHITECTURE}ruby:${RUBY_VERSION} ARG ORTOOLS_URL=${ORTOOLS_URL} @@ -7,19 +9,6 @@ LABEL maintainer="Mapotempo " WORKDIR /srv/ -# Trick to install passenger-docker on Ruby 2.5. Othwerwise `apt-get update` fails with a -# certificate error. See following links for explanantion: -# https://issueexplorer.com/issue/phusion/passenger-docker/325 -# and -# https://issueexplorer.com/issue/phusion/passenger-docker/322 -# Basically, DST Root CA X3 certificates are expired on Setember 2021 and apt-get cannot validate -# with the old certificates and the certification correction is only done for Ruby 2.6+ on the -# passenger-docker repo because Ruby 2.5 is EOL. -RUN mv /etc/apt/sources.list.d /etc/apt/sources.list.d.bak -RUN apt update && apt install -y ca-certificates -RUN mv /etc/apt/sources.list.d.bak /etc/apt/sources.list.d -# The above trick can be removed after Ruby version is increased. - RUN apt-get update > /dev/null && \ apt-get -y install git wget pkg-config build-essential cmake autoconf libtool zlib1g-dev lsb-release > /dev/null diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..3097325b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.7' +x-app-args: &app-args + ARCHITECTURE: ${ARCHITECTURE} + ORTOOLS_URL: ${ORTOOLS_URL} + ORTOOLS_VERSION: ${ORTOOLS_VERSION} + RUBY_VERSION: ${RUBY_VERSION} + +x-app: &default-app + volumes: + - ./:/srv/app/ + env_file: + - ./.env + +services: + main: + <<: *default-app + build: + args: + <<: *app-args + context: . + dockerfile: Dockerfile + image: dev.example.com/mapotempo/optimizer-ortools + tty: true From 8f8a0bcd9cbf8d2f9ffcb5dfbfdb92513f19e595 Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Fri, 20 Jan 2023 08:39:32 +0000 Subject: [PATCH 28/30] Fix github actions deprecation warning --- .github/actions/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build.sh b/.github/actions/build.sh index be75add7..3384bbc7 100755 --- a/.github/actions/build.sh +++ b/.github/actions/build.sh @@ -35,4 +35,4 @@ docker build --build-arg RUBY_VERSION="2.5" --build-arg ORTOOLS_URL=${ORTOOLS_UR docker run -d --name optimizer -t "${IMAGE_NAME}" docker exec -i optimizer bash -c "LD_LIBRARY_PATH=/srv/or-tools/lib/ /srv/optimizer-ortools/tsp_simple -time_limit_in_ms 500 -intermediate_solutions -instance_file '/srv/optimizer-ortools/data/test_ortools_single_route_with_route_order' -solution_file '/tmp/optimize-or-tools-output'" -echo "::set-output name=image_name::${IMAGE_NAME}" +echo "name=image_name::${IMAGE_NAME}" >> $GITHUB_OUTPUT From c3e1aaa32f74e78cb0a0c2f05e632ed405b36f3d Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Fri, 20 Jan 2023 08:52:17 +0000 Subject: [PATCH 29/30] Set image name standard --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3097325b..d137c252 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,5 +19,5 @@ services: <<: *app-args context: . dockerfile: Dockerfile - image: dev.example.com/mapotempo/optimizer-ortools + image: dev.example.com/mapotempo-ce/optimizer-ortools tty: true From a63bc671012fcaf60f37d7af2fa552c24fba4929 Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Tue, 18 Apr 2023 12:28:46 +0000 Subject: [PATCH 30/30] Update Ruby version --- .env | 2 +- .github/actions/build.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 6003c50a..589ad321 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ ORTOOLS_URL="https://github.com/google/or-tools/releases/download/v7.8/or-tools_debian-10_v7.8.7959.tar.gz" ORTOOLS_VERSION=v7.8 # select from https://github.com/google/or-tools/releases -RUBY_VERSION=2.5 +RUBY_VERSION=2.7 diff --git a/.github/actions/build.sh b/.github/actions/build.sh index 3384bbc7..329d2e04 100755 --- a/.github/actions/build.sh +++ b/.github/actions/build.sh @@ -31,7 +31,7 @@ case $ORTOOLS_VERSION in esac echo "Download asset at ${ORTOOLS_URL}" -docker build --build-arg RUBY_VERSION="2.5" --build-arg ORTOOLS_URL=${ORTOOLS_URL} -f ./Dockerfile -t "${IMAGE_NAME}" . +docker build --build-arg RUBY_VERSION="2.7" --build-arg ORTOOLS_URL=${ORTOOLS_URL} -f ./Dockerfile -t "${IMAGE_NAME}" . docker run -d --name optimizer -t "${IMAGE_NAME}" docker exec -i optimizer bash -c "LD_LIBRARY_PATH=/srv/or-tools/lib/ /srv/optimizer-ortools/tsp_simple -time_limit_in_ms 500 -intermediate_solutions -instance_file '/srv/optimizer-ortools/data/test_ortools_single_route_with_route_order' -solution_file '/tmp/optimize-or-tools-output'"