From 781a98a402b7c7cce47bd29566a41523c175b310 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Thu, 3 Oct 2024 11:00:53 +0200 Subject: [PATCH] routing: export from google3 --- ortools/base/BUILD.bazel | 9 + ortools/base/proto_enum_utils.h | 209 ++++++++++++++++++ .../constraint_solver/constraint_solver.cc | 1 + ortools/constraint_solver/local_search.cc | 129 +++++++---- ortools/constraint_solver/search_stats.proto | 11 +- ortools/routing/BUILD.bazel | 39 ++-- ortools/routing/filters.cc | 77 +++++-- ortools/routing/ils.cc | 11 +- ortools/routing/insertion_lns.cc | 2 + ortools/routing/parameters.cc | 22 ++ ortools/routing/parameters.proto | 40 ++-- ortools/routing/parameters_utils.cc | 46 ++++ ortools/routing/parameters_utils.h | 33 +++ ortools/routing/routing.cc | 45 ++-- ortools/routing/search.cc | 56 +++-- ortools/routing/search.h | 18 +- 16 files changed, 599 insertions(+), 149 deletions(-) create mode 100644 ortools/base/proto_enum_utils.h create mode 100644 ortools/routing/parameters_utils.cc create mode 100644 ortools/routing/parameters_utils.h diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index a74cdf1575f..e4bea633898 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -460,6 +460,15 @@ cc_library( ], ) +cc_library( + name = "proto_enum_utils", + hdrs = ["proto_enum_utils.h"], + deps = [ + "@com_google_absl//absl/types:span", + "@com_google_protobuf//:protobuf", + ], +) + cc_library( name = "ptr_util", hdrs = ["ptr_util.h"], diff --git a/ortools/base/proto_enum_utils.h b/ortools/base/proto_enum_utils.h new file mode 100644 index 00000000000..301e9988690 --- /dev/null +++ b/ortools/base/proto_enum_utils.h @@ -0,0 +1,209 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_BASE_PROTO_ENUM_UTILS_H_ +#define OR_TOOLS_BASE_PROTO_ENUM_UTILS_H_ + +// Provides utility functions that help with handling Protocol Buffer enums. +// +// Examples: +// +// A function to easily iterate over all defined values of an enum known at +// compile-time: +// +// for (Proto::Enum e : EnumerateEnumValues()) { +// ... +// } +// + +#include +#include + +#include "absl/types/span.h" +#include "google/protobuf/descriptor.pb.h" + +namespace google::protobuf::contrib::utils { + +using google::protobuf::GetEnumDescriptor; +using google::protobuf::RepeatedField; + +template +class ProtoEnumIterator; + +template +class EnumeratedProtoEnumView; + +template +bool operator==(const ProtoEnumIterator& a, const ProtoEnumIterator& b); + +template +bool operator!=(const ProtoEnumIterator& a, const ProtoEnumIterator& b); + +// Generic Proto enum iterator. +template +class ProtoEnumIterator { + public: + typedef E value_type; + typedef std::forward_iterator_tag iterator_category; + typedef int difference_type; + typedef E* pointer; + typedef E& reference; + + ProtoEnumIterator() : current_(0) {} + + ProtoEnumIterator(const ProtoEnumIterator& other) + : current_(other.current_) {} + + ProtoEnumIterator& operator=(const ProtoEnumIterator& other) { + current_ = other.current_; + return *this; + } + + ProtoEnumIterator operator++(int) { + ProtoEnumIterator other(*this); + ++(*this); + return other; + } + + ProtoEnumIterator& operator++() { + ++current_; + return *this; + } + + E operator*() const { + return static_cast(GetEnumDescriptor()->value(current_)->number()); + } + + private: + explicit ProtoEnumIterator(int current) : current_(current) {} + + int current_; + + // Only EnumeratedProtoEnumView can instantiate ProtoEnumIterator. + friend class EnumeratedProtoEnumView; + friend bool operator== + <>(const ProtoEnumIterator& a, const ProtoEnumIterator& b); + friend bool operator!= + <>(const ProtoEnumIterator& a, const ProtoEnumIterator& b); +}; + +template +bool operator==(const ProtoEnumIterator& a, const ProtoEnumIterator& b) { + return a.current_ == b.current_; +} + +template +bool operator!=(const ProtoEnumIterator& a, const ProtoEnumIterator& b) { + return a.current_ != b.current_; +} + +template +class EnumeratedProtoEnumView { + public: + typedef E value_type; + typedef ProtoEnumIterator iterator; + iterator begin() const { return iterator(0); } + iterator end() const { + return iterator(GetEnumDescriptor()->value_count()); + } +}; + +// Returns an EnumeratedProtoEnumView that can be iterated over: +// for (Proto::Enum e : EnumerateEnumValues()) { +// ... +// } +template +EnumeratedProtoEnumView EnumerateEnumValues() { + return EnumeratedProtoEnumView(); +} + +// Returns a view that allows to iterate directly over the enum values +// in an enum repeated field, wrapping the repeated field with a type-safe +// iterator that provides access to the enum values. +// +// for (Enum enum : +// REPEATED_ENUM_ADAPTER(message, repeated_enum_field)) { +// ... +// } +// +// It provides greater safety than iterating over the enum directly, as the +// following will fail to type-check: +// +// .proto +// RightEnum enum = 5; +// +// client .cc +// for (WrongEnum e : REPEATED_ENUM_ADAPTER(proto, enum)) { <- Error: Cannot +// cast from +// RightEnum to +// WrongEnum +// } +// +// NOTE: As per http://shortn/_CYfjpruK6N, unrecognized enum values are treated +// differently between proto2 and proto3. +// +// For proto2, they are stripped out from the message when read, so all +// unrecognized enum values from the wire format will be skipped when iterating +// over the wrapper (this is the same behavior as iterating over the +// RepeatedField directly). +// +// For proto3, they are left as-is, so unrecognized enum values from the wire +// format will still be returned when iterating over the wrapper (this is the +// same behavior as iterating over the RepeatedField directly). +// +#define REPEATED_ENUM_ADAPTER(var, field) \ + google::protobuf::contrib::utils::internal::RepeatedEnumView< \ + decltype(var.field(0))>(var.field()) + +// ==== WARNING TO USERS ==== +// Below are internal implementations, not public API, and may change without +// notice. Do NOT use directly. + +namespace internal { + +// Implementation for REPEATED_ENUM_ADAPTER. This does not provide type safety +// thus should be used through REPEATED_ENUM_ADAPTER only. See cr/246914845 for +// context. +template +class RepeatedEnumView { + public: + class Iterator : public std::iterator { + public: + explicit Iterator(RepeatedField::const_iterator ptr) : ptr_(ptr) {} + bool operator==(const Iterator& it) const { return ptr_ == it.ptr_; } + bool operator!=(const Iterator& it) const { return ptr_ != it.ptr_; } + Iterator& operator++() { + ++ptr_; + return *this; + } + E operator*() const { return static_cast(*ptr_); } + + private: + RepeatedField::const_iterator ptr_; + }; + + explicit RepeatedEnumView(const RepeatedField& repeated_field) + : repeated_field_(repeated_field) {} + + Iterator begin() const { return Iterator(repeated_field_.begin()); } + Iterator end() const { return Iterator(repeated_field_.end()); } + + private: + const RepeatedField& repeated_field_; +}; + +} // namespace internal + +} // namespace google::protobuf::contrib::utils + +#endif // OR_TOOLS_BASE_PROTO_ENUM_UTILS_H_ diff --git a/ortools/constraint_solver/constraint_solver.cc b/ortools/constraint_solver/constraint_solver.cc index 09f5812b95b..3a1cee76fdb 100644 --- a/ortools/constraint_solver/constraint_solver.cc +++ b/ortools/constraint_solver/constraint_solver.cc @@ -3280,6 +3280,7 @@ Decision* ProfiledDecisionBuilder::Next(Solver* const solver) { Decision* const decision = db_->Next(solver); timer_.Stop(); seconds_ += timer_.Get(); + solver->set_context(""); return decision; } diff --git a/ortools/constraint_solver/local_search.cc b/ortools/constraint_solver/local_search.cc index ec33c281278..3bcd9f993b9 100644 --- a/ortools/constraint_solver/local_search.cc +++ b/ortools/constraint_solver/local_search.cc @@ -1418,11 +1418,21 @@ class MakeChainInactiveOperator : public PathOperator { MakeChainInactiveOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class) - : PathOperator(vars, secondary_vars, 2, true, false, + : PathOperator(vars, secondary_vars, 2, + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/false, std::move(start_empty_path_class), nullptr) {} ~MakeChainInactiveOperator() override {} bool MakeNeighbor() override { - return MakeChainInactive(BaseNode(0), BaseNode(1)); + const int64_t chain_end = BaseNode(1); + if (!IsPathEnd(chain_end) && chain_end != BaseNode(0) && + !Var(chain_end)->Contains(chain_end)) { + // Move to the next before_chain since an unskippable node has been + // encountered. + SetNextBaseToIncrement(0); + return false; + } + return MakeChainInactive(BaseNode(0), chain_end); } std::string DebugString() const override { @@ -3683,13 +3693,13 @@ class LocalSearchProfiler : public LocalSearchMonitor { std::string DebugString() const override { return "LocalSearchProfiler"; } void RestartSearch() override { operator_stats_.clear(); - filter_stats_.clear(); + filter_stats_per_context_.clear(); + last_operator_ = nullptr; } void ExitSearch() override { // Update times for current operator when the search ends. - if (solver()->TopLevelSearch() == solver()->ActiveSearch()) { - UpdateTime(); - } + UpdateTime(); + last_operator_ = nullptr; } template void ParseFirstSolutionStatistics(const Callback& callback) const { @@ -3714,26 +3724,26 @@ class LocalSearchProfiler : public LocalSearchMonitor { for (const LocalSearchOperator* const op : operators) { const OperatorStats& stats = gtl::FindOrDie(operator_stats_, op); callback(op->DebugString(), stats.neighbors, stats.filtered_neighbors, - stats.accepted_neighbors, stats.seconds); + stats.accepted_neighbors, stats.seconds, + stats.make_next_neighbor_seconds, stats.accept_neighbor_seconds); } } template void ParseLocalSearchFilterStatistics(const Callback& callback) const { - absl::flat_hash_map> - filters_per_context; - for (const auto& stat : filter_stats_) { - filters_per_context[stat.second.context].push_back(stat.first); - } - for (auto& [context, filters] : filters_per_context) { + for (const auto& [context, filter_stats] : filter_stats_per_context_) { + std::vector filters; + for (const auto& [filter, stats] : filter_stats) { + filters.push_back(filter); + } std::sort(filters.begin(), filters.end(), - [this](const LocalSearchFilter* filter1, - const LocalSearchFilter* filter2) { - return gtl::FindOrDie(filter_stats_, filter1).calls > - gtl::FindOrDie(filter_stats_, filter2).calls; + [&filter_stats](const LocalSearchFilter* filter1, + const LocalSearchFilter* filter2) { + return gtl::FindOrDie(filter_stats, filter1).calls > + gtl::FindOrDie(filter_stats, filter2).calls; }); for (const LocalSearchFilter* const filter : filters) { - const FilterStats& stats = gtl::FindOrDie(filter_stats_, filter); + const FilterStats& stats = gtl::FindOrDie(filter_stats, filter); callback(context, filter->DebugString(), stats.calls, stats.rejects, stats.seconds); } @@ -3749,23 +3759,30 @@ class LocalSearchProfiler : public LocalSearchMonitor { first_solution_statistics->set_strategy(name); first_solution_statistics->set_duration_seconds(duration_seconds); }); - ParseLocalSearchOperatorStatistics([&statistics_proto]( - absl::string_view name, - int64_t num_neighbors, - int64_t num_filtered_neighbors, - int64_t num_accepted_neighbors, - double duration_seconds) { - LocalSearchStatistics::LocalSearchOperatorStatistics* const - local_search_operator_statistics = - statistics_proto.add_local_search_operator_statistics(); - local_search_operator_statistics->set_local_search_operator(name); - local_search_operator_statistics->set_num_neighbors(num_neighbors); - local_search_operator_statistics->set_num_filtered_neighbors( - num_filtered_neighbors); - local_search_operator_statistics->set_num_accepted_neighbors( - num_accepted_neighbors); - local_search_operator_statistics->set_duration_seconds(duration_seconds); - }); + ParseLocalSearchOperatorStatistics( + [&statistics_proto]( + absl::string_view name, int64_t num_neighbors, + int64_t num_filtered_neighbors, int64_t num_accepted_neighbors, + double duration_seconds, double make_next_neighbor_duration_seconds, + double accept_neighbor_duration_seconds) { + LocalSearchStatistics::LocalSearchOperatorStatistics* const + local_search_operator_statistics = + statistics_proto.add_local_search_operator_statistics(); + local_search_operator_statistics->set_local_search_operator(name); + local_search_operator_statistics->set_num_neighbors(num_neighbors); + local_search_operator_statistics->set_num_filtered_neighbors( + num_filtered_neighbors); + local_search_operator_statistics->set_num_accepted_neighbors( + num_accepted_neighbors); + local_search_operator_statistics->set_duration_seconds( + duration_seconds); + local_search_operator_statistics + ->set_make_next_neighbor_duration_seconds( + make_next_neighbor_duration_seconds); + local_search_operator_statistics + ->set_accept_neighbor_duration_seconds( + accept_neighbor_duration_seconds); + }); ParseLocalSearchFilterStatistics([&statistics_proto]( absl::string_view context, absl::string_view name, @@ -3808,11 +3825,11 @@ class LocalSearchProfiler : public LocalSearchMonitor { }); } max_name_size = 0; - ParseLocalSearchOperatorStatistics([&max_name_size](absl::string_view name, - int64_t, int64_t, - int64_t, double) { - max_name_size = std::max(max_name_size, name.length()); - }); + ParseLocalSearchOperatorStatistics( + [&max_name_size](absl::string_view name, int64_t, int64_t, int64_t, + double, double, double) { + max_name_size = std::max(max_name_size, name.length()); + }); if (max_name_size > 0) { absl::StrAppendFormat( &overview, @@ -3824,7 +3841,11 @@ class LocalSearchProfiler : public LocalSearchMonitor { [&overview, &total_stats, max_name_size]( absl::string_view name, int64_t num_neighbors, int64_t num_filtered_neighbors, int64_t num_accepted_neighbors, - double duration_seconds) { + double duration_seconds, + double make_next_neighbor_duration_seconds, + double accept_neighbor_duration_seconds) { + // TODO(user): Add make_next_neighbor_duration_seconds and + // accept_neighbor_duration_seconds to stats. absl::StrAppendFormat( &overview, "%*s | %9ld | %8ld | %8ld | %7.2g\n", max_name_size, name, num_neighbors, num_filtered_neighbors, @@ -3893,9 +3914,13 @@ class LocalSearchProfiler : public LocalSearchMonitor { UpdateTime(); last_operator_ = op->Self(); } + make_next_neighbor_timer_.Start(); } void EndMakeNextNeighbor(const LocalSearchOperator* op, bool neighbor_found, const Assignment*, const Assignment*) override { + make_next_neighbor_timer_.Stop(); + operator_stats_[op->Self()].make_next_neighbor_seconds += + make_next_neighbor_timer_.Get(); if (neighbor_found) { operator_stats_[op->Self()].neighbors++; } @@ -3907,22 +3932,27 @@ class LocalSearchProfiler : public LocalSearchMonitor { operator_stats_[op->Self()].filtered_neighbors++; } } - void BeginAcceptNeighbor(const LocalSearchOperator*) override {} + void BeginAcceptNeighbor(const LocalSearchOperator*) override { + accept_neighbor_timer_.Start(); + } void EndAcceptNeighbor(const LocalSearchOperator* op, bool neighbor_found) override { + accept_neighbor_timer_.Stop(); + operator_stats_[op->Self()].accept_neighbor_seconds += + accept_neighbor_timer_.Get(); if (neighbor_found) { operator_stats_[op->Self()].accepted_neighbors++; } } void BeginFiltering(const LocalSearchFilter* filter) override { - FilterStats& filter_stats = filter_stats_[filter]; + FilterStats& filter_stats = + filter_stats_per_context_[solver()->context()][filter]; filter_stats.calls++; - filter_stats.context = solver()->context(); filter_timer_.Start(); } void EndFiltering(const LocalSearchFilter* filter, bool reject) override { filter_timer_.Stop(); - auto& stats = filter_stats_[filter]; + auto& stats = filter_stats_per_context_[solver()->context()][filter]; stats.seconds += filter_timer_.Get(); if (reject) { stats.rejects++; @@ -3949,20 +3979,25 @@ class LocalSearchProfiler : public LocalSearchMonitor { int64_t filtered_neighbors = 0; int64_t accepted_neighbors = 0; double seconds = 0; + double make_next_neighbor_seconds = 0; + double accept_neighbor_seconds = 0; }; struct FilterStats { int64_t calls = 0; int64_t rejects = 0; double seconds = 0; - std::string context; }; WallTimer timer_; + WallTimer make_next_neighbor_timer_; + WallTimer accept_neighbor_timer_; WallTimer filter_timer_; const LocalSearchOperator* last_operator_ = nullptr; absl::flat_hash_map operator_stats_; - absl::flat_hash_map filter_stats_; + absl::flat_hash_map< + std::string, absl::flat_hash_map> + filter_stats_per_context_; // Profiled decision builders. std::vector profiled_decision_builders_; }; diff --git a/ortools/constraint_solver/search_stats.proto b/ortools/constraint_solver/search_stats.proto index 9e300d97960..e8ae1a167e3 100644 --- a/ortools/constraint_solver/search_stats.proto +++ b/ortools/constraint_solver/search_stats.proto @@ -44,6 +44,11 @@ message LocalSearchStatistics { int64 num_accepted_neighbors = 4; // Time spent in the operator. double duration_seconds = 5; + // Time spent in creating neighbors (calling MakeNextNeighbor). + double make_next_neighbor_duration_seconds = 6; + // Time spent in accepting a neighbor (restoration and storage, not + // including filtering). + double accept_neighbor_duration_seconds = 7; } // Statistics for each operator called during the search. repeated LocalSearchOperatorStatistics local_search_operator_statistics = 1; @@ -86,8 +91,8 @@ message ConstraintSolverStatistics { // Search statistics. message SearchStatistics { - // Local search statistics. - LocalSearchStatistics local_search_statistics = 1; + // Local search statistics for each solver context. + repeated LocalSearchStatistics local_search_statistics = 1; // Constraint solver statistics. - ConstraintSolverStatistics constraint_solver_statistics = 2; + repeated ConstraintSolverStatistics constraint_solver_statistics = 2; } diff --git a/ortools/routing/BUILD.bazel b/ortools/routing/BUILD.bazel index 02f2e514b83..a143d923131 100644 --- a/ortools/routing/BUILD.bazel +++ b/ortools/routing/BUILD.bazel @@ -31,17 +31,6 @@ config_setting( constraint_values = ["@platforms//os:windows"], ) -proto_library( - name = "ils_proto", - srcs = ["ils.proto"], - deps = [":enums_proto"], -) - -cc_proto_library( - name = "ils_cc_proto", - deps = ["ils_proto"], -) - proto_library( name = "enums_proto", srcs = ["enums.proto"], @@ -57,6 +46,17 @@ cc_proto_library( # deps = [":enums_proto"], # ) +proto_library( + name = "ils_proto", + srcs = ["ils.proto"], + deps = [":enums_proto"], +) + +cc_proto_library( + name = "ils_cc_proto", + deps = ["ils_proto"], +) + proto_library( name = "parameters_proto", srcs = ["parameters.proto"], @@ -91,6 +91,7 @@ cc_library( hdrs = ["parameters.h"], deps = [ "//ortools/base", + "//ortools/base:proto_enum_utils", "//ortools/base:protoutil", "//ortools/constraint_solver:cp", "//ortools/constraint_solver:solver_parameters_cc_proto", @@ -115,6 +116,16 @@ cc_library( ], ) +cc_library( + name = "parameters_utils", + srcs = ["parameters_utils.cc"], + hdrs = ["parameters_utils.h"], + deps = [ + ":parameters_cc_proto", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "utils", srcs = ["utils.cc"], @@ -184,9 +195,13 @@ cc_library( "//conditions:default": [], }), deps = [ + ":enums_cc_proto", + ":ils_cc_proto", ":index_manager", ":neighborhoods", ":parameters", + ":parameters_cc_proto", + ":parameters_utils", ":types", ":utils", "//ortools/base", @@ -209,8 +224,6 @@ cc_library( "//ortools/graph:topologicalsorter", "//ortools/lp_data", "//ortools/lp_data:base", - "//ortools/routing:enums_cc_proto", - "//ortools/routing:parameters_cc_proto", "//ortools/sat:boolean_problem", "//ortools/sat:cp_constraints", "//ortools/sat:cp_model", diff --git a/ortools/routing/filters.cc b/ortools/routing/filters.cc index 29778789e84..63829379027 100644 --- a/ortools/routing/filters.cc +++ b/ortools/routing/filters.cc @@ -4266,41 +4266,78 @@ void LightVehicleBreaksChecker::Relax() const { bool LightVehicleBreaksChecker::Check() const { for (const int path : path_state_->ChangedPaths()) { if (!path_data_[path].span.Exists()) continue; - const int64_t total_transit = path_data_[path].total_transit.Min(); - // Compute lower bound of path span from break and path time windows. const PathData& data = path_data_[path]; + const int64_t total_transit = data.total_transit.Min(); + int64_t lb_span = data.span.Min(); + // Improve bounds on span/start max/end min using time windows: breaks that + // must occur inside the path have their duration accumulated into + // lb_span_tw, they also widen [start_max, end_min). int64_t lb_span_tw = total_transit; - const int64_t start_max = data.start_cumul.Max(); - const int64_t end_min = data.end_cumul.Min(); + int64_t start_max = data.start_cumul.Max(); + int64_t end_min = data.end_cumul.Min(); for (const auto& br : data.vehicle_breaks) { if (!br.is_performed_min) continue; if (br.start_max < end_min && start_max < br.end_min) { CapAddTo(br.duration_min, &lb_span_tw); + start_max = std::min(start_max, br.start_max); + end_min = std::max(end_min, br.end_min); } } - int64_t lb_span_interbreak = 0; + lb_span = std::max({lb_span, lb_span_tw, CapSub(end_min, start_max)}); + // Compute num_feasible_breaks = number of breaks that may fit into route, + // and [breaks_start_min, breaks_end_max) = max coverage of breaks. + int64_t break_start_min = kint64max; + int64_t break_end_max = kint64min; + int64_t start_min = data.start_cumul.Min(); + start_min = std::max(start_min, CapSub(end_min, data.span.Max())); + int64_t end_max = data.end_cumul.Max(); + end_max = std::min(end_max, CapAdd(start_max, data.span.Max())); + int num_feasible_breaks = 0; + for (const auto& br : data.vehicle_breaks) { + if (start_min <= br.start_max && br.end_min <= end_max) { + break_start_min = std::min(break_start_min, br.start_min); + break_end_max = std::max(break_end_max, br.end_max); + ++num_feasible_breaks; + } + } + // Improve span/start min/end max using interbreak limits: there must be + // enough breaks inside the path, so that for each limit, the union of + // [br.start - max_interbreak, br.end + max_interbreak) covers [start, end), + // or [start, end) is shorter than max_interbreak. for (const auto& [max_interbreak, min_break_duration] : data.interbreak_limits) { // Minimal number of breaks depends on total transit: // 0 breaks for 0 <= total transit <= limit, // 1 break for limit + 1 <= total transit <= 2 * limit, // i breaks for i * limit + 1 <= total transit <= (i+1) * limit, ... - if (total_transit == 0) continue; - if (max_interbreak == 0) return false; - const int min_num_breaks = (total_transit - 1) / max_interbreak; - if (min_num_breaks > data.vehicle_breaks.size()) return false; - lb_span_interbreak = std::max( - lb_span_interbreak, CapProd(min_num_breaks, min_break_duration)); - } - lb_span_interbreak = CapAdd(lb_span_interbreak, total_transit); - const int64_t lb_span = std::max(lb_span_tw, lb_span_interbreak); - if (!data.span.SetMin(lb_span)) return false; - if (!data.start_cumul.SetMax(CapSub(data.end_cumul.Max(), lb_span))) { - return false; - } - if (!data.end_cumul.SetMin(CapAdd(data.start_cumul.Min(), lb_span))) { - return false; + if (max_interbreak == 0) { + if (total_transit > 0) return false; + continue; + } + int64_t min_num_breaks = + std::max(0, (total_transit - 1) / max_interbreak); + if (lb_span > max_interbreak) { + min_num_breaks = std::max(min_num_breaks, 1); + } + if (min_num_breaks > num_feasible_breaks) return false; + lb_span = std::max( + lb_span, + CapAdd(total_transit, CapProd(min_num_breaks, min_break_duration))); + if (min_num_breaks > 0) { + if (!data.start_cumul.SetMin(CapSub(break_start_min, max_interbreak))) { + return false; + } + if (!data.end_cumul.SetMax(CapAdd(break_end_max, max_interbreak))) { + return false; + } + } } + if (!data.span.SetMin(lb_span)) return false; + // Merge span lb information directly in start/end variables. + start_max = std::min(start_max, CapSub(end_max, lb_span)); + if (!data.start_cumul.SetMax(start_max)) return false; + end_min = std::max(end_min, CapAdd(start_min, lb_span)); + if (!data.end_cumul.SetMin(end_min)) return false; } return true; } diff --git a/ortools/routing/ils.cc b/ortools/routing/ils.cc index 7727b231614..47c5e237e65 100644 --- a/ortools/routing/ils.cc +++ b/ortools/routing/ils.cc @@ -34,6 +34,7 @@ #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/routing/ils.pb.h" #include "ortools/routing/parameters.pb.h" +#include "ortools/routing/parameters_utils.h" #include "ortools/routing/routing.h" #include "ortools/routing/search.h" #include "ortools/routing/types.h" @@ -233,15 +234,17 @@ std::unique_ptr MakeRecreateProcedure( model, std::move(stop_search), absl::bind_front(&RoutingModel::GetArcCostForVehicle, model), parameters.local_cheapest_cost_insertion_pickup_delivery_strategy(), - parameters.local_cheapest_insertion_sorting_mode(), filter_manager, - model->GetBinCapacities()); + GetLocalCheapestInsertionSortingProperties( + parameters.local_cheapest_insertion_sorting_properties()), + filter_manager, model->GetBinCapacities()); case FirstSolutionStrategy::LOCAL_CHEAPEST_COST_INSERTION: return std::make_unique( model, std::move(stop_search), /*evaluator=*/nullptr, parameters.local_cheapest_cost_insertion_pickup_delivery_strategy(), - parameters.local_cheapest_insertion_sorting_mode(), filter_manager, - model->GetBinCapacities()); + GetLocalCheapestInsertionSortingProperties( + parameters.local_cheapest_insertion_sorting_properties()), + filter_manager, model->GetBinCapacities()); case FirstSolutionStrategy::SEQUENTIAL_CHEAPEST_INSERTION: { GlobalCheapestInsertionFilteredHeuristic:: GlobalCheapestInsertionParameters gci_parameters = diff --git a/ortools/routing/insertion_lns.cc b/ortools/routing/insertion_lns.cc index bbb39dced6e..5399083a6d7 100644 --- a/ortools/routing/insertion_lns.cc +++ b/ortools/routing/insertion_lns.cc @@ -74,8 +74,10 @@ bool FilteredHeuristicLocalSearchOperator::MakeChangesAndInsertNodes() { if (next_accessor == nullptr) { return false; } + model_->solver()->set_context(DebugString()); const Assignment* const result_assignment = heuristic_->BuildSolutionFromRoutes(next_accessor); + model_->solver()->set_context(""); if (result_assignment == nullptr) { return false; diff --git a/ortools/routing/parameters.cc b/ortools/routing/parameters.cc index 57b93f3651f..264d6464a42 100644 --- a/ortools/routing/parameters.cc +++ b/ortools/routing/parameters.cc @@ -18,6 +18,7 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/time/time.h" @@ -25,6 +26,7 @@ #include "google/protobuf/duration.pb.h" #include "google/protobuf/message.h" #include "ortools/base/logging.h" +#include "ortools/base/proto_enum_utils.h" #include "ortools/base/protoutil.h" #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" @@ -523,6 +525,26 @@ std::vector FindErrorsInRoutingSearchParameters( "Invalid cheapest_insertion_ls_operator_min_neighbors: ", min_neighbors, ". Must be greater or equal to 1.")); } + { + absl::flat_hash_map + sorting_properties_map; + for (const RoutingSearchParameters::InsertionSortingProperty property : + REPEATED_ENUM_ADAPTER(search_parameters, + local_cheapest_insertion_sorting_properties)) { + if (property == RoutingSearchParameters::SORTING_PROPERTY_UNSPECIFIED) { + errors.emplace_back( + StrCat("Invalid local cheapest insertion sorting property: ", + RoutingSearchParameters::InsertionSortingProperty_Name( + RoutingSearchParameters::SORTING_PROPERTY_UNSPECIFIED))); + } + const int occurrences = sorting_properties_map[property]++; + if (occurrences == 2) { + errors.emplace_back(StrCat( + "Duplicate local cheapest insertion sorting property: ", + RoutingSearchParameters::InsertionSortingProperty_Name(property))); + } + } + } if (const double ratio = search_parameters.ls_operator_neighbors_ratio(); std::isnan(ratio) || ratio <= 0 || ratio > 1) { errors.emplace_back(StrCat("Invalid ls_operator_neighbors_ratio: ", ratio)); diff --git a/ortools/routing/parameters.proto b/ortools/routing/parameters.proto index 96a4fc9b178..e414802b093 100644 --- a/ortools/routing/parameters.proto +++ b/ortools/routing/parameters.proto @@ -36,9 +36,9 @@ package operations_research.routing; // then the routing library will pick its preferred value for that parameter // automatically: this should be the case for most parameters. // To see those "default" parameters, call GetDefaultRoutingSearchParameters(). -// Next ID: 67 +// Next ID: 68 message RoutingSearchParameters { - reserved 19; + reserved 19, 65; // First solution strategies, used as starting point of local search. FirstSolutionStrategy.Value first_solution_strategy = 1; @@ -132,24 +132,28 @@ message RoutingSearchParameters { PairInsertionStrategy local_cheapest_cost_insertion_pickup_delivery_strategy = 55; - // A mode to select in which order nodes or node pairs are considered in - // insertion heuristics. - enum InsertionSortingMode { - // Default mode, equivalent to SORT_BY_ALLOWED_VEHICLES_THEN_PENALTY. - SORTING_MODE_UNSET = 0; - // Selects nodes with the least number of allowed vehicles first, then the - // ones with the highest penalty. - SORT_BY_ALLOWED_VEHICLES_THEN_PENALTY = 1; - // Selects nodes with the highest penalty first, then the ones with the - // least number of allowed vehicles. - SORT_BY_PENALTY_THEN_ALLOWED_VEHICLES = 2; + // Properties used to select in which order nodes or node pairs are considered + // in insertion heuristics. + enum InsertionSortingProperty { + // Invalid property. + SORTING_PROPERTY_UNSPECIFIED = 0; + // Selects nodes with the least number of allowed vehicles. + SORTING_PROPERTY_ALLOWED_VEHICLES = 1; + // Selects nodes with the highest penalty. + SORTING_PROPERTY_PENALTY = 2; // Selects nodes with the highest penalty / number of allowed vehicles - // ratio first, then the ones with the highest penalty. - SORT_BY_PENALTY_ALLOWED_VEHICLES_RATIO_THEN_PENALTY = 3; + // ratio. + SORTING_PROPERTY_PENALTY_OVER_ALLOWED_VEHICLES_RATIO = 3; } - // The node insertion sorting mode used in local cheapest insertion - // heuristics. - InsertionSortingMode local_cheapest_insertion_sorting_mode = 65; + + // The properties used to sort insertion entries in the local cheapest + // insertion heuristic, in *decreasing* order of priority. The properties + // listed here are applied hierarchically, from highest to lowest priority. + // When no properties are provided + // (SORTING_PROPERTY_ALLOWED_VEHICLES, SORTING_PROPERTY_PENALTY) + // is used by default. + repeated InsertionSortingProperty + local_cheapest_insertion_sorting_properties = 67; // If true use minimum matching instead of minimal matching in the // Christofides algorithm. diff --git a/ortools/routing/parameters_utils.cc b/ortools/routing/parameters_utils.cc new file mode 100644 index 00000000000..f4ac03f1231 --- /dev/null +++ b/ortools/routing/parameters_utils.cc @@ -0,0 +1,46 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/parameters_utils.h" + +#include + +#include "absl/types/span.h" + +namespace operations_research::routing { + +std::vector +GetLocalCheapestInsertionSortingProperties( + absl::Span lci_insertion_sorting_properties) { + std::vector + sorting_properties; + + for (const int property : lci_insertion_sorting_properties) { + sorting_properties.push_back( + static_cast( + property)); + } + + // For historical reasons if no insertion order is specified, we fallback to + // selecting nodes with the least number of allowed vehicles first, then the + // ones with the highest penalty. + if (sorting_properties.empty()) { + sorting_properties.push_back( + RoutingSearchParameters::SORTING_PROPERTY_ALLOWED_VEHICLES); + sorting_properties.push_back( + RoutingSearchParameters::SORTING_PROPERTY_PENALTY); + } + return sorting_properties; +} + +} // namespace operations_research::routing diff --git a/ortools/routing/parameters_utils.h b/ortools/routing/parameters_utils.h new file mode 100644 index 00000000000..c0e8ea276f4 --- /dev/null +++ b/ortools/routing/parameters_utils.h @@ -0,0 +1,33 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_ROUTING_PARAMETERS_UTILS_H_ +#define OR_TOOLS_ROUTING_PARAMETERS_UTILS_H_ + +#include + +#include "absl/types/span.h" +#include "ortools/routing/parameters.pb.h" + +namespace operations_research::routing { + +// Takes RoutingSearchParameters::local_cheapest_insertion_sorting_properties in +// input and returns the ordered list of properties that is used to sort nodes +// when performing a local cheapest insertion first heuristic. +std::vector +GetLocalCheapestInsertionSortingProperties( + absl::Span lci_insertion_sorting_properties); + +} // namespace operations_research::routing + +#endif // OR_TOOLS_ROUTING_PARAMETERS_UTILS_H_ diff --git a/ortools/routing/routing.cc b/ortools/routing/routing.cc index bbe5d390885..dcef548e1c7 100644 --- a/ortools/routing/routing.cc +++ b/ortools/routing/routing.cc @@ -76,6 +76,7 @@ #include "ortools/routing/neighborhoods.h" #include "ortools/routing/parameters.h" #include "ortools/routing/parameters.pb.h" +#include "ortools/routing/parameters_utils.h" #include "ortools/routing/search.h" #include "ortools/routing/types.h" #include "ortools/routing/utils.h" @@ -4802,7 +4803,8 @@ void RoutingModel::CreateNeighborhoodOperators( this, [this]() { return CheckLimit(time_buffer_); }, GetLocalSearchArcCostCallback(parameters), parameters.local_cheapest_insertion_pickup_delivery_strategy(), - parameters.local_cheapest_insertion_sorting_mode(), + GetLocalCheapestInsertionSortingProperties( + parameters.local_cheapest_insertion_sorting_properties()), GetOrCreateLocalSearchFilterManager( parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/false}), @@ -5756,27 +5758,29 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( } const RoutingSearchParameters::PairInsertionStrategy lci_pair_strategy = search_parameters.local_cheapest_insertion_pickup_delivery_strategy(); - const RoutingSearchParameters::InsertionSortingMode sorting_mode = - search_parameters.local_cheapest_insertion_sorting_mode(); - first_solution_filtered_decision_builders_ - [FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION] = - CreateIntVarFilteredDecisionBuilder< - LocalCheapestInsertionFilteredHeuristic>( - [this](int64_t i, int64_t j, int64_t vehicle) { - return GetArcCostForVehicle(i, j, vehicle); - }, - lci_pair_strategy, sorting_mode, - GetOrCreateLocalSearchFilterManager( - search_parameters, {/*filter_objective=*/false, - /*filter_with_cp_solver=*/false}), - bin_capacities_.get(), optimize_on_insertion); + first_solution_filtered_decision_builders_[FirstSolutionStrategy:: + LOCAL_CHEAPEST_INSERTION] = + CreateIntVarFilteredDecisionBuilder< + LocalCheapestInsertionFilteredHeuristic>( + [this](int64_t i, int64_t j, int64_t vehicle) { + return GetArcCostForVehicle(i, j, vehicle); + }, + lci_pair_strategy, + GetLocalCheapestInsertionSortingProperties( + search_parameters.local_cheapest_insertion_sorting_properties()), + GetOrCreateLocalSearchFilterManager( + search_parameters, {/*filter_objective=*/false, + /*filter_with_cp_solver=*/false}), + bin_capacities_.get(), optimize_on_insertion); IntVarFilteredDecisionBuilder* const strong_lci = CreateIntVarFilteredDecisionBuilder< LocalCheapestInsertionFilteredHeuristic>( [this](int64_t i, int64_t j, int64_t vehicle) { return GetArcCostForVehicle(i, j, vehicle); }, - lci_pair_strategy, sorting_mode, + lci_pair_strategy, + GetLocalCheapestInsertionSortingProperties( + search_parameters.local_cheapest_insertion_sorting_properties()), GetOrCreateLocalSearchFilterManager(search_parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/true}), @@ -5797,7 +5801,10 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( [FirstSolutionStrategy::LOCAL_CHEAPEST_COST_INSERTION] = CreateIntVarFilteredDecisionBuilder< LocalCheapestInsertionFilteredHeuristic>( - /*evaluator=*/nullptr, lcci_pair_strategy, sorting_mode, + /*evaluator=*/nullptr, lcci_pair_strategy, + GetLocalCheapestInsertionSortingProperties( + search_parameters + .local_cheapest_insertion_sorting_properties()), GetOrCreateLocalSearchFilterManager( search_parameters, {/*filter_objective=*/true, /*filter_with_cp_solver=*/false}), @@ -5805,7 +5812,9 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( IntVarFilteredDecisionBuilder* const strong_lcci = CreateIntVarFilteredDecisionBuilder< LocalCheapestInsertionFilteredHeuristic>( - /*evaluator=*/nullptr, lcci_pair_strategy, sorting_mode, + /*evaluator=*/nullptr, lcci_pair_strategy, + GetLocalCheapestInsertionSortingProperties( + search_parameters.local_cheapest_insertion_sorting_properties()), GetOrCreateLocalSearchFilterManager(search_parameters, {/*filter_objective=*/true, /*filter_with_cp_solver=*/true}), diff --git a/ortools/routing/search.cc b/ortools/routing/search.cc index 76e9298caf0..078b636dd3c 100644 --- a/ortools/routing/search.cc +++ b/ortools/routing/search.cc @@ -38,6 +38,7 @@ #include "absl/base/attributes.h" #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/container/inlined_vector.h" #include "absl/flags/flag.h" #include "absl/log/check.h" #include "absl/log/die_if_null.h" @@ -620,7 +621,7 @@ void CheapestInsertionFilteredHeuristic::AddSeedNodeToQueue( } const int64_t num_allowed_vehicles = model()->VehicleVar(node)->Size(); const int64_t neg_penalty = CapOpp(model()->UnperformedPenalty(node)); - sq->priority_queue.push({.key = {num_allowed_vehicles, neg_penalty}, + sq->priority_queue.push({.properties = {num_allowed_vehicles, neg_penalty}, .start_end_value = start_end_value, .is_node_index = true, .index = node}); @@ -2277,7 +2278,8 @@ LocalCheapestInsertionFilteredHeuristic:: RoutingModel* model, std::function stop_search, std::function evaluator, RoutingSearchParameters::PairInsertionStrategy pair_insertion_strategy, - RoutingSearchParameters::InsertionSortingMode insertion_sorting_mode, + std::vector + insertion_sorting_properties, LocalSearchFilterManager* filter_manager, BinCapacities* bin_capacities, std::function&, std::vector*)> @@ -2286,9 +2288,11 @@ LocalCheapestInsertionFilteredHeuristic:: std::move(evaluator), nullptr, filter_manager), pair_insertion_strategy_(pair_insertion_strategy), - insertion_sorting_mode_(insertion_sorting_mode), + insertion_sorting_properties_(std::move(insertion_sorting_properties)), bin_capacities_(bin_capacities), - optimize_on_insertion_(std::move(optimize_on_insertion)) {} + optimize_on_insertion_(std::move(optimize_on_insertion)) { + DCHECK(!insertion_sorting_properties_.empty()); +} void LocalCheapestInsertionFilteredHeuristic::Initialize() { // NOTE(user): Keeping the code in a separate function as opposed to @@ -2396,22 +2400,34 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { insertion_order_.reserve(model.Size() + model.GetPickupAndDeliveryPairs().size()); - auto get_insertion_key = - [this](int64_t penalty, - int64_t num_allowed_vehicles) -> std::tuple { + auto get_insertion_properties = [this](int64_t penalty, + int64_t num_allowed_vehicles) { DCHECK_NE(0, num_allowed_vehicles); - switch (insertion_sorting_mode_) { - case RoutingSearchParameters::SORT_BY_PENALTY_THEN_ALLOWED_VEHICLES: - return {CapOpp(penalty), num_allowed_vehicles}; - case RoutingSearchParameters:: - SORT_BY_PENALTY_ALLOWED_VEHICLES_RATIO_THEN_PENALTY: - return {CapOpp(penalty / num_allowed_vehicles), CapOpp(penalty)}; - default: - return {num_allowed_vehicles, CapOpp(penalty)}; + absl::InlinedVector properties; + properties.reserve(insertion_sorting_properties_.size()); + for (const int property : insertion_sorting_properties_) { + switch (property) { + case RoutingSearchParameters::SORTING_PROPERTY_ALLOWED_VEHICLES: + properties.push_back(num_allowed_vehicles); + break; + case RoutingSearchParameters::SORTING_PROPERTY_PENALTY: + properties.push_back(CapOpp(penalty)); + break; + case RoutingSearchParameters:: + SORTING_PROPERTY_PENALTY_OVER_ALLOWED_VEHICLES_RATIO: + properties.push_back(CapOpp(penalty / num_allowed_vehicles)); + break; + default: + LOG(DFATAL) + << "Unknown RoutingSearchParameter::InsertionSortingProperty " + "used!"; + break; + } } + return properties; }; - // Iterating on pickup and delivery pairs + // Iterating on pickup and delivery pairs. const std::vector& pairs = model.GetPickupAndDeliveryPairs(); @@ -2435,8 +2451,8 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { std::max(delivery_penalty, model.UnperformedPenalty(delivery)); } insertion_order_.push_back( - {.key = get_insertion_key(CapAdd(pickup_penalty, delivery_penalty), - num_allowed_vehicles), + {.properties = get_insertion_properties( + CapAdd(pickup_penalty, delivery_penalty), num_allowed_vehicles), .start_end_value = {GetNegMaxDistanceFromVehicles(model, pair_index), 0}, .is_node_index = false, @@ -2457,8 +2473,8 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { }, vehicle_set); insertion_order_.push_back( - {.key = get_insertion_key(model.UnperformedPenalty(node), - model.VehicleVar(node)->Size()), + {.properties = get_insertion_properties(model.UnperformedPenalty(node), + model.VehicleVar(node)->Size()), .start_end_value = {CapOpp(min_distance), 0}, .is_node_index = true, .index = node}); diff --git a/ortools/routing/search.h b/ortools/routing/search.h index a38dfb5539b..2057c5b0646 100644 --- a/ortools/routing/search.h +++ b/ortools/routing/search.h @@ -33,6 +33,7 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/container/inlined_vector.h" #include "absl/log/check.h" #include "absl/types/span.h" #include "ortools/base/adjustable_priority_queue.h" @@ -345,7 +346,7 @@ class CheapestInsertionFilteredHeuristic : public RoutingFilteredHeuristic { } }; struct Seed { - std::tuple key; + absl::InlinedVector properties; StartEndValue start_end_value; /// Indicates whether this Seed corresponds to a pair or a single node. /// If false, the 'index' is the pair_index, otherwise it's the node index. @@ -353,9 +354,12 @@ class CheapestInsertionFilteredHeuristic : public RoutingFilteredHeuristic { int index; bool operator>(const Seed& other) const { - return std::tie(key, start_end_value, is_node_index, index) > - std::tie(other.key, other.start_end_value, other.is_node_index, - other.index); + for (size_t i = 0; i < properties.size(); ++i) { + if (properties[i] == other.properties[i]) continue; + return properties[i] > other.properties[i]; + } + return std::tie(start_end_value, is_node_index, index) > + std::tie(other.start_end_value, other.is_node_index, other.index); } }; // clang-format off @@ -1067,7 +1071,8 @@ class LocalCheapestInsertionFilteredHeuristic RoutingModel* model, std::function stop_search, std::function evaluator, RoutingSearchParameters::PairInsertionStrategy pair_insertion_strategy, - RoutingSearchParameters::InsertionSortingMode insertion_sorting_mode, + std::vector + insertion_sorting_properties, LocalSearchFilterManager* filter_manager, BinCapacities* bin_capacities = nullptr, std::function&, @@ -1130,7 +1135,8 @@ class LocalCheapestInsertionFilteredHeuristic std::vector insertion_order_; const RoutingSearchParameters::PairInsertionStrategy pair_insertion_strategy_; - const RoutingSearchParameters::InsertionSortingMode insertion_sorting_mode_; + std::vector + insertion_sorting_properties_; InsertionSequenceContainer insertion_container_; InsertionSequenceGenerator insertion_generator_;