diff --git a/ortools/constraint_solver/BUILD.bazel b/ortools/constraint_solver/BUILD.bazel index 58243c1dcb..f03fd3a6be 100644 --- a/ortools/constraint_solver/BUILD.bazel +++ b/ortools/constraint_solver/BUILD.bazel @@ -314,6 +314,7 @@ cc_library( ":routing_enums_cc_proto", ":routing_heuristic_parameters_cc_proto", ":routing_ils_cc_proto", + ":routing_ils_parameters_utils", ":routing_parameters_cc_proto", ":routing_parameters_utils", ":solver_parameters_cc_proto", @@ -326,6 +327,7 @@ cc_library( "//ortools/util:optional_boolean_cc_proto", "//ortools/util:testing_utils", "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/log", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:str_format", "@abseil-cpp//absl/time", @@ -345,6 +347,17 @@ cc_library( ], ) +cc_library( + name = "routing_ils_parameters_utils", + srcs = ["routing_ils_parameters_utils.cc"], + hdrs = ["routing_ils_parameters_utils.h"], + visibility = ["//visibility:public"], + deps = [ + ":routing_enums_cc_proto", + ":routing_ils_cc_proto", + ], +) + cc_library( name = "routing_types", hdrs = ["routing_types.h"], diff --git a/ortools/constraint_solver/routing.cc b/ortools/constraint_solver/routing.cc index 0621e3d5fc..3e91a0d867 100644 --- a/ortools/constraint_solver/routing.cc +++ b/ortools/constraint_solver/routing.cc @@ -2981,6 +2981,15 @@ void RoutingModel::CloseModelWithParameters( solver_->AddConstraint(solver_->MakePathPrecedenceConstraint( nexts_, pickup_delivery_precedences, lifo_vehicles, fifo_vehicles)); + // Add ordered activity group constraints. + for (const auto& nodes : ordered_activity_groups_) { + if (nodes.size() <= 1) continue; + for (int i = 1; i < nodes.size(); ++i) { + solver_->AddConstraint(solver_->MakeLessOrEqual(ActiveVar(nodes[i]), + ActiveVar(nodes[i - 1]))); + } + } + // Detect constraints enable_deep_serialization_ = false; std::unique_ptr inspector( @@ -5354,6 +5363,10 @@ RoutingModel::CreateLocalSearchFilters( filter_events.push_back( {MakeActiveNodeGroupFilter(*this), kAccept, priority}); } + if (!GetOrderedActivityGroups().empty()) { + filter_events.push_back( + {MakeOrderedActivityGroupFilter(*this), kAccept, priority}); + } if (!disjunctions_.empty()) { if (options.filter_objective || HasMandatoryDisjunctions() || @@ -6858,7 +6871,7 @@ void RoutingDimension::InitializeTransits( InitializeTransitVariables(slack_max); } -// TODO(user): Apply -pointer-following. +// TODO(user): Apply http://go/minimize-pointer-following. void FillPathEvaluation(absl::Span path, const RoutingModel::TransitCallback2& evaluator, std::vector* values) { diff --git a/ortools/constraint_solver/routing.h b/ortools/constraint_solver/routing.h index 2987698976..43ad67b484 100644 --- a/ortools/constraint_solver/routing.h +++ b/ortools/constraint_solver/routing.h @@ -1923,6 +1923,14 @@ class OR_DLL RoutingModel { DCHECK(closed_); return same_vehicle_groups_[same_vehicle_group_[node]]; } + void AddSameActivityGroup(const std::vector& nodes) { + DCHECK(!closed_); + if (nodes.size() <= 1) return; + for (auto it = nodes.begin() + 1; it != nodes.end(); ++it) { + solver_->AddConstraint( + solver_->MakeEquality(ActiveVar(*it), ActiveVar(*(it - 1)))); + } + } /// Returns variable indices of nodes constrained to have the same activity. const std::vector& GetSameActivityIndicesOfIndex(int node) const { DCHECK(closed_); @@ -1933,6 +1941,11 @@ class OR_DLL RoutingModel { DCHECK(closed_); return same_active_var_group_[node]; } + /// Returns same activity groups of all nodes. + const std::vector& GetSameActivityGroups() const { + DCHECK(closed_); + return same_active_var_group_; + } /// Returns the number of same activity groups. int GetSameActivityGroupsCount() const { DCHECK(closed_); @@ -1943,7 +1956,19 @@ class OR_DLL RoutingModel { DCHECK(closed_); return same_active_var_groups_[group]; } - + /// Adds an ordered activity group. This enforces that if nodes[i] is active, + /// then nodes[i-1] must be active. + void AddOrderedActivityGroup(std::vector nodes) { + DCHECK(!closed_); + if (nodes.size() <= 1) return; + ordered_activity_groups_.push_back(std::move(nodes)); + } +#ifndef SWIG + /// Returns all ordered activity groups. + const std::vector>& GetOrderedActivityGroups() const { + return ordered_activity_groups_; + } +#endif // SWIG const VehicleTypeContainer& GetVehicleTypeContainer() const { DCHECK(closed_); return vehicle_type_container_; @@ -2620,6 +2645,8 @@ class OR_DLL RoutingModel { std::vector same_active_var_group_; // Same active var groups. std::vector> same_active_var_groups_; + // Ordered activity groups. + std::vector> ordered_activity_groups_; // Node visit types // Variable index to visit type index. std::vector index_to_visit_type_; diff --git a/ortools/constraint_solver/routing_filters.cc b/ortools/constraint_solver/routing_filters.cc index f35344e3ab..9e194bb6d0 100644 --- a/ortools/constraint_solver/routing_filters.cc +++ b/ortools/constraint_solver/routing_filters.cc @@ -227,12 +227,151 @@ IntVarLocalSearchFilter* MakeMaxActiveVehiclesFilter( namespace { +class SameActivityGroupManager { + public: + explicit SameActivityGroupManager(const RoutingModel& routing_model) + : routing_model_(routing_model) {} + int NumberOfGroups() const { + return routing_model_.GetSameActivityGroupsCount(); + } + absl::Span GetGroupsFromNode(int node) const { + return absl::MakeConstSpan(routing_model_.GetSameActivityGroups()) + .subspan(node, 1); + } + const std::vector& GetGroupNodes(int group) const { + return routing_model_.GetSameActivityIndicesOfGroup(group); + } + void Revert() {} + bool CheckGroup(int group, int active, int unknown, + const CommittableArray& /*node_is_active*/, + const CommittableArray& /*node_is_unknown*/) const { + const int group_size = GetGroupNodes(group).size(); + // The group constraint is respected iff either 0 or group size is inside + // interval [num_active, num_active + num_unknown], + if (active == 0) return true; + if (active <= group_size && group_size <= active + unknown) { + return true; + } + return false; + } + + private: + const RoutingModel& routing_model_; +}; + +class OrderedActivityGroupManager { + public: + explicit OrderedActivityGroupManager(const RoutingModel& routing_model) + : groups_(routing_model.GetOrderedActivityGroups()), + group_bounds_(routing_model.GetOrderedActivityGroups().size(), {0, 0}) { + node_groups_.resize(routing_model.Size()); + for (int group = 0; group < groups_.size(); ++group) { + for (int node : groups_[group]) { + node_groups_[node].push_back(group); + } + group_bounds_.Set(group, std::make_pair(0, groups_[group].size() - 1)); + } + group_bounds_.Commit(); + } + int NumberOfGroups() const { return groups_.size(); } + absl::Span GetGroupsFromNode(int node) const { + return node_groups_[node]; + } + const std::vector& GetGroupNodes(int group) const { + return groups_[group]; + } + void Revert() { + group_bounds_.Revert(); + touched_nodes_.clear(); + } + bool CheckGroup(int group, int active, int unknown, + CommittableArray& node_is_active, + CommittableArray& node_is_unknown) { + if (active == 0) return true; + auto& [min_rank, max_rank] = group_bounds_.GetMutable(group); + for (int rank = min_rank; rank <= max_rank; ++rank) { + const int node = groups_[group][rank]; + if (node_is_unknown.Get(node)) continue; + if (!node_is_active.Get(node)) { + touched_nodes_.push_back(node); + break; + } + } + for (int rank = max_rank; rank >= min_rank; --rank) { + const int node = groups_[group][rank]; + if (node_is_unknown.Get(node)) continue; + if (node_is_active.Get(node)) { + touched_nodes_.push_back(node); + break; + } + } + while (!touched_nodes_.empty()) { + const int node = touched_nodes_.back(); + touched_nodes_.pop_back(); + if (!Propagate(node, node_is_active, node_is_unknown)) { + return false; + } +#ifndef NDEBUG + for (int n : touched_nodes_) DCHECK_NE(n, node); +#endif // NDEBUG + } + return true; + } + bool Propagate(int node, CommittableArray& node_is_active, + CommittableArray& node_is_unknown) { + for (int group_index : node_groups_[node]) { + const std::vector& group = groups_[group_index]; + auto& [min_rank, max_rank] = group_bounds_.GetMutable(group_index); + if (max_rank < min_rank) continue; + if (node_is_active.Get(node)) { + // Make all active between min_rank and node. + int rank = min_rank; + while (group[rank] != node) { + const int current_node = group[rank]; + if (node_is_unknown.Get(current_node)) { + node_is_active.Set(current_node, true); + node_is_unknown.Set(current_node, false); + touched_nodes_.push_back(current_node); + } else if (!node_is_active.Get(current_node)) { + return false; + } + rank++; + } + min_rank = rank + 1; + } else { + // Make all inactive between node and max_rank. + int rank = max_rank; + while (group[rank] != node) { + const int current_node = group[rank]; + if (node_is_unknown.Get(current_node)) { + node_is_active.Set(current_node, false); + node_is_unknown.Set(current_node, false); + touched_nodes_.push_back(current_node); + } else if (node_is_active.Get(current_node)) { + return false; + } + rank--; + } + max_rank = rank - 1; + } + } + return true; + } + + private: + const std::vector>& groups_; + std::vector> node_groups_; + CommittableArray> group_bounds_; + std::vector touched_nodes_; +}; + +template class ActiveNodeGroupFilter : public IntVarLocalSearchFilter { public: explicit ActiveNodeGroupFilter(const RoutingModel& routing_model) : IntVarLocalSearchFilter(routing_model.Nexts()), - routing_model_(routing_model), - active_count_per_group_(routing_model.GetSameActivityGroupsCount(), + group_accessor_(routing_model), + active_count_per_group_(group_accessor_.NumberOfGroups(), {.active = 0, .unknown = 0}), node_is_active_(routing_model.Nexts().size(), false), node_is_unknown_(routing_model.Nexts().size(), false) {} @@ -240,35 +379,43 @@ class ActiveNodeGroupFilter : public IntVarLocalSearchFilter { bool Accept(const Assignment* delta, const Assignment* /*deltadelta*/, int64_t /*objective_min*/, int64_t /*objective_max*/) override { active_count_per_group_.Revert(); + node_is_active_.Revert(); + node_is_unknown_.Revert(); + group_accessor_.Revert(); const Assignment::IntContainer& container = delta->IntVarContainer(); + // Updating group counters. for (const IntVarElement& new_element : container.elements()) { IntVar* const var = new_element.Var(); int64_t index = -1; if (!FindIndex(var, &index)) continue; - const int group = routing_model_.GetSameActivityGroupOfIndex(index); - ActivityCounts counts = active_count_per_group_.Get(group); - // Change contribution to counts: remove old state, add new state. - if (node_is_unknown_[index]) --counts.unknown; - if (node_is_active_[index]) --counts.active; - if (new_element.Min() != new_element.Max()) { - ++counts.unknown; - } else if (new_element.Min() != index) { - ++counts.active; + for (const int group : group_accessor_.GetGroupsFromNode(index)) { + ActivityCounts counts = active_count_per_group_.Get(group); + // Change contribution to counts: remove old state, add new state. + if (node_is_unknown_.Get(index)) --counts.unknown; + if (node_is_active_.Get(index)) --counts.active; + if (new_element.Min() != new_element.Max()) { + ++counts.unknown; + } else if (new_element.Min() != index) { + ++counts.active; + } + active_count_per_group_.Set(group, counts); } - active_count_per_group_.Set(group, counts); + } + // Updating node states. + for (const IntVarElement& new_element : container.elements()) { + IntVar* const var = new_element.Var(); + int64_t index = -1; + if (!FindIndex(var, &index)) continue; + node_is_unknown_.Set(index, new_element.Min() != new_element.Max()); + node_is_active_.Set(index, new_element.Min() == new_element.Max() && + new_element.Min() != index); } for (const int group : active_count_per_group_.ChangedIndices()) { const ActivityCounts counts = active_count_per_group_.Get(group); - const int group_size = - routing_model_.GetSameActivityIndicesOfGroup(group).size(); - // The group constraint is respected iff either 0 or group size is inside - // interval [num_active, num_active + num_unknown], - if (counts.active == 0) continue; - if (counts.active <= group_size && - group_size <= counts.active + counts.unknown) { - continue; + if (!group_accessor_.CheckGroup(group, counts.active, counts.unknown, + node_is_active_, node_is_unknown_)) { + return false; } - return false; } return true; } @@ -276,27 +423,29 @@ class ActiveNodeGroupFilter : public IntVarLocalSearchFilter { private: void OnSynchronize(const Assignment* /*delta*/) override { - const int num_groups = routing_model_.GetSameActivityGroupsCount(); + const int num_groups = group_accessor_.NumberOfGroups(); for (int group = 0; group < num_groups; ++group) { ActivityCounts counts = {.active = 0, .unknown = 0}; - for (int node : routing_model_.GetSameActivityIndicesOfGroup(group)) { + for (int node : group_accessor_.GetGroupNodes(group)) { if (IsVarSynced(node)) { const bool is_active = (Value(node) != node); - node_is_active_[node] = is_active; - node_is_unknown_[node] = false; + node_is_active_.Set(node, is_active); + node_is_unknown_.Set(node, false); counts.active += is_active ? 1 : 0; } else { ++counts.unknown; - node_is_unknown_[node] = true; - node_is_active_[node] = false; + node_is_unknown_.Set(node, true); + node_is_active_.Set(node, false); } } active_count_per_group_.Set(group, counts); } active_count_per_group_.Commit(); + node_is_active_.Commit(); + node_is_unknown_.Commit(); } - const RoutingModel& routing_model_; + GroupAccessor group_accessor_; struct ActivityCounts { int active; int unknown; @@ -304,10 +453,10 @@ class ActiveNodeGroupFilter : public IntVarLocalSearchFilter { CommittableArray active_count_per_group_; // node_is_active_[node] is true iff node was synced and active at last // Synchronize(). - std::vector node_is_active_; + CommittableArray node_is_active_; // node_is_unknown_[node] is true iff node was not synced at last // Synchronize(). - std::vector node_is_unknown_; + CommittableArray node_is_unknown_; }; } // namespace @@ -315,7 +464,13 @@ class ActiveNodeGroupFilter : public IntVarLocalSearchFilter { IntVarLocalSearchFilter* MakeActiveNodeGroupFilter( const RoutingModel& routing_model) { return routing_model.solver()->RevAlloc( - new ActiveNodeGroupFilter(routing_model)); + new ActiveNodeGroupFilter(routing_model)); +} + +IntVarLocalSearchFilter* MakeOrderedActivityGroupFilter( + const RoutingModel& routing_model) { + return routing_model.solver()->RevAlloc( + new ActiveNodeGroupFilter(routing_model)); } namespace { diff --git a/ortools/constraint_solver/routing_filters.h b/ortools/constraint_solver/routing_filters.h index e3c3cebc49..410041c7c8 100644 --- a/ortools/constraint_solver/routing_filters.h +++ b/ortools/constraint_solver/routing_filters.h @@ -81,6 +81,11 @@ IntVarLocalSearchFilter* MakeMaxActiveVehiclesFilter( IntVarLocalSearchFilter* MakeActiveNodeGroupFilter( const RoutingModel& routing_model); +/// Returns a filter ensuring that for each ordered activity group, +/// if nodes[i] is active then nodes[i-1] is active. +IntVarLocalSearchFilter* MakeOrderedActivityGroupFilter( + const RoutingModel& routing_model); + /// Returns a filter ensuring that node disjunction constraints are enforced. IntVarLocalSearchFilter* MakeNodeDisjunctionFilter( const RoutingModel& routing_model, bool filter_cost); diff --git a/ortools/constraint_solver/routing_ils.cc b/ortools/constraint_solver/routing_ils.cc index 4bd085b9fc..a8cc1801fe 100644 --- a/ortools/constraint_solver/routing_ils.cc +++ b/ortools/constraint_solver/routing_ils.cc @@ -66,7 +66,8 @@ MakeGlobalCheapestInsertionParameters( // Returns local cheapest insertion parameters based on the given recreate // strategy if available. Returns default parameters otherwise. -LocalCheapestInsertionParameters GetLocalCheapestInsertionParameters( +LocalCheapestInsertionParameters +GetLocalCheapestInsertionParametersForRecreateStrategy( const RecreateStrategy& recreate_strategy, const LocalCheapestInsertionParameters& default_parameters) { return recreate_strategy.has_parameters() && @@ -75,6 +76,15 @@ LocalCheapestInsertionParameters GetLocalCheapestInsertionParameters( : default_parameters; } +SavingsParameters GetSavingsParametersForRecreateStrategy( + const RecreateStrategy& recreate_strategy, + const SavingsParameters& default_parameters) { + return recreate_strategy.has_parameters() && + recreate_strategy.parameters().has_savings() + ? recreate_strategy.parameters().savings() + : default_parameters; +} + // Returns a ruin procedure based on the given ruin strategy. std::unique_ptr MakeRuinProcedure( RoutingModel* model, std::mt19937* rnd, RuinStrategy ruin, @@ -229,7 +239,7 @@ std::unique_ptr MakeRecreateProcedure( return std::make_unique( model, std::move(stop_search), absl::bind_front(&RoutingModel::GetArcCostForVehicle, model), - GetLocalCheapestInsertionParameters( + GetLocalCheapestInsertionParametersForRecreateStrategy( recreate_strategy, parameters.local_cheapest_insertion_parameters()), filter_manager, model->GetBinCapacities()); @@ -238,7 +248,7 @@ std::unique_ptr MakeRecreateProcedure( return std::make_unique( model, std::move(stop_search), /*evaluator=*/nullptr, - GetLocalCheapestInsertionParameters( + GetLocalCheapestInsertionParametersForRecreateStrategy( recreate_strategy, parameters.local_cheapest_cost_insertion_parameters()), filter_manager, model->GetBinCapacities()); @@ -266,15 +276,17 @@ std::unique_ptr MakeRecreateProcedure( filter_manager, gci_parameters); } case FirstSolutionStrategy::SAVINGS: { - // TODO(user): support ILS-specific savings parameters. return std::make_unique( - model, std::move(stop_search), parameters.savings_parameters(), + model, std::move(stop_search), + GetSavingsParametersForRecreateStrategy( + recreate_strategy, parameters.savings_parameters()), filter_manager); } case FirstSolutionStrategy::PARALLEL_SAVINGS: { - // TODO(user): support ILS-specific savings parameters. return std::make_unique( - model, std::move(stop_search), parameters.savings_parameters(), + model, std::move(stop_search), + GetSavingsParametersForRecreateStrategy( + recreate_strategy, parameters.savings_parameters()), filter_manager); } default: diff --git a/ortools/constraint_solver/routing_ils.proto b/ortools/constraint_solver/routing_ils.proto index fecd7a8631..468c52fb65 100644 --- a/ortools/constraint_solver/routing_ils.proto +++ b/ortools/constraint_solver/routing_ils.proto @@ -151,6 +151,7 @@ message RuinStrategy { message RecreateParameters { oneof parameters { LocalCheapestInsertionParameters local_cheapest_insertion = 1; + SavingsParameters savings = 2; } } diff --git a/ortools/constraint_solver/routing_ils_parameters_utils.cc b/ortools/constraint_solver/routing_ils_parameters_utils.cc new file mode 100644 index 0000000000..6acff4f7ee --- /dev/null +++ b/ortools/constraint_solver/routing_ils_parameters_utils.cc @@ -0,0 +1,58 @@ +// Copyright 2010-2025 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/constraint_solver/routing_ils_parameters_utils.h" + +#include +#include + +#include "ortools/constraint_solver/routing_enums.pb.h" +#include "ortools/constraint_solver/routing_ils.pb.h" + +namespace operations_research { + +RecreateParameters::ParametersCase GetParameterCaseForRecreateHeuristic( + FirstSolutionStrategy::Value recreate_heuristic) { + switch (recreate_heuristic) { + case FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION: + return RecreateParameters::kLocalCheapestInsertion; + case FirstSolutionStrategy::LOCAL_CHEAPEST_COST_INSERTION: + return RecreateParameters::kLocalCheapestInsertion; + case FirstSolutionStrategy::SAVINGS: + return RecreateParameters::kSavings; + case FirstSolutionStrategy::PARALLEL_SAVINGS: + return RecreateParameters::kSavings; + default: + return RecreateParameters::PARAMETERS_NOT_SET; + } +} + +std::vector +GetSupportedRecreateParametersCases() { + return {RecreateParameters::kLocalCheapestInsertion, + RecreateParameters::kSavings}; +} + +std::string GetRecreateParametersName( + RecreateParameters::ParametersCase parameters_case) { + switch (parameters_case) { + case RecreateParameters::kLocalCheapestInsertion: + return "local_cheapest_insertion"; + case RecreateParameters::kSavings: + return "savings"; + case RecreateParameters::PARAMETERS_NOT_SET: + return "PARAMETERS_NOT_SET"; + } +} + +} // namespace operations_research diff --git a/ortools/constraint_solver/routing_ils_parameters_utils.h b/ortools/constraint_solver/routing_ils_parameters_utils.h new file mode 100644 index 0000000000..0dc3b58e02 --- /dev/null +++ b/ortools/constraint_solver/routing_ils_parameters_utils.h @@ -0,0 +1,40 @@ +// Copyright 2010-2025 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_CONSTRAINT_SOLVER_ROUTING_ILS_PARAMETERS_UTILS_H_ +#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_ILS_PARAMETERS_UTILS_H_ + +#include +#include + +#include "ortools/constraint_solver/routing_enums.pb.h" +#include "ortools/constraint_solver/routing_ils.pb.h" + +namespace operations_research { + +// Returns the appropriate parameters case for the given recreate heuristic. +// Returns PARAMETERS_NOT_SET if the heuristic is not supported. +RecreateParameters::ParametersCase GetParameterCaseForRecreateHeuristic( + FirstSolutionStrategy::Value recreate_heuristic); + +// Returns the list of supported recreate parameters cases. +std::vector +GetSupportedRecreateParametersCases(); + +// Returns the name of the given recreate parameter. +std::string GetRecreateParametersName( + RecreateParameters::ParametersCase parameters_case); + +} // namespace operations_research + +#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_ILS_PARAMETERS_UTILS_H_ diff --git a/ortools/constraint_solver/routing_index_manager.h b/ortools/constraint_solver/routing_index_manager.h index 626d94ef65..2549a7af1c 100644 --- a/ortools/constraint_solver/routing_index_manager.h +++ b/ortools/constraint_solver/routing_index_manager.h @@ -23,7 +23,6 @@ #include "ortools/base/base_export.h" #include "ortools/base/strong_vector.h" #include "ortools/constraint_solver/routing_types.h" - namespace operations_research { /// Manager for any NodeIndex <-> variable index conversion. The routing solver diff --git a/ortools/constraint_solver/routing_parameters.cc b/ortools/constraint_solver/routing_parameters.cc index 5b6e4ecdf0..92a1f90d52 100644 --- a/ortools/constraint_solver/routing_parameters.cc +++ b/ortools/constraint_solver/routing_parameters.cc @@ -36,6 +36,7 @@ #include "ortools/constraint_solver/routing_enums.pb.h" #include "ortools/constraint_solver/routing_heuristic_parameters.pb.h" #include "ortools/constraint_solver/routing_ils.pb.h" +#include "ortools/constraint_solver/routing_ils_parameters_utils.h" #include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/constraint_solver/solver_parameters.pb.h" #include "ortools/port/proto_utils.h" @@ -304,6 +305,32 @@ void FindErrorsInLocalCheapestInsertionParameters( } } +// Searches for errors in SavingsParameters and appends them to the given +// `errors` vector. +void FindErrorsInSavingsParameters(const absl::string_view prefix, + const SavingsParameters& savings_parameters, + std::vector& errors) { + using absl::StrCat; + + if (const double ratio = savings_parameters.neighbors_ratio(); + std::isnan(ratio) || ratio <= 0 || ratio > 1) { + errors.emplace_back(StrCat( + prefix, " - Invalid savings_parameters.neighbors_ratio: ", ratio)); + } + if (const double max_memory = savings_parameters.max_memory_usage_bytes(); + std::isnan(max_memory) || max_memory <= 0 || max_memory > 1e10) { + errors.emplace_back(StrCat( + prefix, + " - Invalid savings_parameters.max_memory_usage_bytes: ", max_memory)); + } + if (const double coefficient = savings_parameters.arc_coefficient(); + std::isnan(coefficient) || coefficient <= 0 || std::isinf(coefficient)) { + errors.emplace_back( + StrCat(prefix, + " - Invalid savings_parameters.arc_coefficient: ", coefficient)); + } +} + void FindErrorsInRecreateParameters( const FirstSolutionStrategy::Value heuristic, const RecreateParameters& parameters, std::vector& errors) { @@ -317,21 +344,16 @@ void FindErrorsInRecreateParameters( prefix, parameters.local_cheapest_insertion(), errors); break; } + case RecreateParameters::kSavings: + FindErrorsInSavingsParameters("Savings (recreate heuristic)", + parameters.savings(), errors); + break; default: LOG(DFATAL) << "Unsupported unset recreate parameters."; break; } } -std::string GetRecreateParametersName(const RecreateParameters& parameters) { - switch (parameters.parameters_case()) { - case RecreateParameters::kLocalCheapestInsertion: - return "local_cheapest_insertion"; - case RecreateParameters::PARAMETERS_NOT_SET: - return "PARAMETERS_NOT_SET"; - } -} - // Searches for errors in ILS parameters and appends them to the given `errors` // vector. void FindErrorsInIteratedLocalSearchParameters( @@ -462,32 +484,20 @@ void FindErrorsInIteratedLocalSearchParameters( rr.recreate_strategy().parameters(); if (recreate_params.parameters_case() == RecreateParameters::PARAMETERS_NOT_SET) { - errors.emplace_back( - StrCat("Invalid value for " - "iterated_local_search_parameters.ruin_recreate_parameters." - "recreate_strategy.parameters: ", - GetRecreateParametersName(recreate_params))); + errors.emplace_back(StrCat( + "Invalid value for " + "iterated_local_search_parameters.ruin_recreate_parameters." + "recreate_strategy.parameters: ", + GetRecreateParametersName(recreate_params.parameters_case()))); } else { - const absl::flat_hash_map - strategy_to_parameters_case_map = { - {FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION, - RecreateParameters::kLocalCheapestInsertion}, - {FirstSolutionStrategy::LOCAL_CHEAPEST_COST_INSERTION, - RecreateParameters::kLocalCheapestInsertion}}; - - const RecreateParameters& recreate_params = - rr.recreate_strategy().parameters(); - - if (const auto params = - strategy_to_parameters_case_map.find(recreate_heuristic); - params == strategy_to_parameters_case_map.end() || - recreate_params.parameters_case() != params->second) { - errors.emplace_back( - StrCat("recreate_strategy.heuristic is set to ", - FirstSolutionStrategy::Value_Name(recreate_heuristic), - " but recreate_strategy.parameters define ", - GetRecreateParametersName(recreate_params))); + if (const RecreateParameters::ParametersCase params = + GetParameterCaseForRecreateHeuristic(recreate_heuristic); + recreate_params.parameters_case() != params) { + errors.emplace_back(StrCat( + "recreate_strategy.heuristic is set to ", + FirstSolutionStrategy::Value_Name(recreate_heuristic), + " but recreate_strategy.parameters define ", + GetRecreateParametersName(recreate_params.parameters_case()))); } else { FindErrorsInRecreateParameters(recreate_heuristic, recreate_params, errors); @@ -598,24 +608,8 @@ std::vector FindErrorsInRoutingSearchParameters( } } #endif // !__ANDROID__ && !__wasm__ - if (const double ratio = - search_parameters.savings_parameters().neighbors_ratio(); - std::isnan(ratio) || ratio <= 0 || ratio > 1) { - errors.emplace_back( - StrCat("Invalid savings_parameters.neighbors_ratio: ", ratio)); - } - if (const double max_memory = - search_parameters.savings_parameters().max_memory_usage_bytes(); - std::isnan(max_memory) || max_memory <= 0 || max_memory > 1e10) { - errors.emplace_back(StrCat( - "Invalid savings_parameters.max_memory_usage_bytes: ", max_memory)); - } - if (const double coefficient = - search_parameters.savings_parameters().arc_coefficient(); - std::isnan(coefficient) || coefficient <= 0 || std::isinf(coefficient)) { - errors.emplace_back( - StrCat("Invalid savings_parameters.arc_coefficient: ", coefficient)); - } + FindErrorsInSavingsParameters("Savings (first solution heuristic)", + search_parameters.savings_parameters(), errors); if (const double ratio = search_parameters.cheapest_insertion_farthest_seeds_ratio(); std::isnan(ratio) || ratio < 0 || ratio > 1) { diff --git a/ortools/constraint_solver/routing_search.h b/ortools/constraint_solver/routing_search.h index 20477c80dd..85f51b274a 100644 --- a/ortools/constraint_solver/routing_search.h +++ b/ortools/constraint_solver/routing_search.h @@ -14,8 +14,6 @@ #ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_SEARCH_H_ #define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_SEARCH_H_ -#include - #include #include #include diff --git a/ortools/routing/parsers/BUILD.bazel b/ortools/routing/parsers/BUILD.bazel index 8aab7f14e1..17a1f33fb5 100644 --- a/ortools/routing/parsers/BUILD.bazel +++ b/ortools/routing/parsers/BUILD.bazel @@ -14,6 +14,7 @@ load("@protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") load("@protobuf//bazel:proto_library.bzl", "proto_library") load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") package(default_visibility = ["//visibility:public"]) @@ -262,6 +263,7 @@ cc_test( ":tsplib_parser", "//ortools/base", "//ortools/base:file", + "//ortools/base:gmock", "//ortools/base:memfile", "@abseil-cpp//absl/container:btree", "@abseil-cpp//absl/strings", diff --git a/ortools/routing/parsers/CMakeLists.txt b/ortools/routing/parsers/CMakeLists.txt index c2eb296602..823bda2205 100644 --- a/ortools/routing/parsers/CMakeLists.txt +++ b/ortools/routing/parsers/CMakeLists.txt @@ -26,11 +26,13 @@ target_include_directories(${NAME} PUBLIC $ $) target_link_libraries(${NAME} PRIVATE + ZLIB::ZLIB absl::memory absl::strings absl::status absl::str_format protobuf::libprotobuf ${RE2_DEPS} - ${PROJECT_NAMESPACE}::ortools_proto) + ${PROJECT_NAMESPACE}::ortools_proto +) #add_library(${PROJECT_NAMESPACE}::routing_parsers ALIAS ${NAME}) diff --git a/ortools/routing/parsers/README.md b/ortools/routing/parsers/README.md index 7019481387..1e5e11d71d 100644 --- a/ortools/routing/parsers/README.md +++ b/ortools/routing/parsers/README.md @@ -6,6 +6,7 @@ and utilities directly related to these file formats. `solution_serializer.h` contains a generic serializer for routing solutions for many formats. + | Problem type | File format | Corresponding parser | Data storage | Data sets | | ------------ | ----------- | -------------------- | ------------ | --------- | | TSP | TSPLIB | `tsplib_parser.h` | | [TSPLIB95][tsplib95] | @@ -17,6 +18,7 @@ many formats. | NEARP | NEARPLIB | `nearplib_parser.h` | | [NEARPLIB][nearplib] | | PDPTW | LiLim | `lilim_parser.h` | | [LiLim][lilim] | | MCND | Dow | `dow_parser.h` | `capacity_planning.proto` | [Canad][canad] | + In the future, this folder will contain the whole routing solver. diff --git a/ortools/routing/parsers/carp_parser.cc b/ortools/routing/parsers/carp_parser.cc index 64dc45433f..5c0801761e 100644 --- a/ortools/routing/parsers/carp_parser.cc +++ b/ortools/routing/parsers/carp_parser.cc @@ -14,11 +14,14 @@ #include "ortools/routing/parsers/carp_parser.h" #include +#include #include #include #include #include +#include "absl/algorithm/container.h" +#include "absl/log/log.h" #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" diff --git a/ortools/routing/parsers/cvrptw_lib.cc b/ortools/routing/parsers/cvrptw_lib.cc index 7997807b60..6776619201 100644 --- a/ortools/routing/parsers/cvrptw_lib.cc +++ b/ortools/routing/parsers/cvrptw_lib.cc @@ -168,7 +168,7 @@ void DisplayPlan(const RoutingIndexManager& manager, bool use_same_vehicle_costs, int64_t max_nodes_per_group, int64_t same_vehicle_cost, absl::Span dimension_names) { - std::vector dimensions; + std::vector dimensions; for (const std::string& dimension_name : dimension_names) { dimensions.push_back(&routing.GetDimensionOrDie(dimension_name)); } @@ -239,7 +239,7 @@ void DisplayPlan(const RoutingIndexManager& manager, while (true) { absl::StrAppendFormat(&plan_output, "%d ", manager.IndexToNode(order).value()); - for (const operations_research::RoutingDimension* dimension : dimensions) { + for (const RoutingDimension* dimension : dimensions) { str_append_variable(dimension->CumulVar(order), dimension->name()); operations_research::IntVar* const slack_var = routing.IsEnd(order) ? nullptr : dimension->SlackVar(order); diff --git a/ortools/routing/parsers/cvrptw_lib.h b/ortools/routing/parsers/cvrptw_lib.h index ca75e9adbb..c9888f15c5 100644 --- a/ortools/routing/parsers/cvrptw_lib.h +++ b/ortools/routing/parsers/cvrptw_lib.h @@ -95,43 +95,42 @@ class RandomDemand { // Service time (proportional to demand) + transition time callback. class ServiceTimePlusTransition { public: - ServiceTimePlusTransition( - int64_t time_per_demand_unit, - operations_research::RoutingNodeEvaluator2 demand, - operations_research::RoutingNodeEvaluator2 transition_time); + ServiceTimePlusTransition(int64_t time_per_demand_unit, + RoutingNodeEvaluator2 demand, + RoutingNodeEvaluator2 transition_time); int64_t Compute(RoutingIndexManager::NodeIndex from, RoutingIndexManager::NodeIndex to) const; private: const int64_t time_per_demand_unit_; - operations_research::RoutingNodeEvaluator2 demand_; - operations_research::RoutingNodeEvaluator2 transition_time_; + RoutingNodeEvaluator2 demand_; + RoutingNodeEvaluator2 transition_time_; }; // Stop service time + transition time callback. class StopServiceTimePlusTransition { public: - StopServiceTimePlusTransition( - int64_t stop_time, const LocationContainer& location_container, - operations_research::RoutingNodeEvaluator2 transition_time); + StopServiceTimePlusTransition(int64_t stop_time, + const LocationContainer& location_container, + RoutingNodeEvaluator2 transition_time); int64_t Compute(RoutingIndexManager::NodeIndex from, RoutingIndexManager::NodeIndex to) const; private: const int64_t stop_time_; const LocationContainer& location_container_; - operations_research::RoutingNodeEvaluator2 demand_; - operations_research::RoutingNodeEvaluator2 transition_time_; + RoutingNodeEvaluator2 demand_; + RoutingNodeEvaluator2 transition_time_; }; // Route plan displayer. // TODO(user): Move the display code to the routing library. -void DisplayPlan( - const operations_research::RoutingIndexManager& manager, - const operations_research::RoutingModel& routing, - const operations_research::Assignment& plan, bool use_same_vehicle_costs, - int64_t max_nodes_per_group, int64_t same_vehicle_cost, - absl::Span dimension_names); +void DisplayPlan(const RoutingIndexManager& manager, + const RoutingModel& routing, + const operations_research::Assignment& plan, + bool use_same_vehicle_costs, int64_t max_nodes_per_group, + int64_t same_vehicle_cost, + absl::Span dimension_names); } // namespace operations_research diff --git a/ortools/routing/parsers/dow_parser_test.cc b/ortools/routing/parsers/dow_parser_test.cc index 0f730f01b8..77ade57218 100644 --- a/ortools/routing/parsers/dow_parser_test.cc +++ b/ortools/routing/parsers/dow_parser_test.cc @@ -13,8 +13,6 @@ #include "ortools/routing/parsers/dow_parser.h" -#include - #include "absl/status/status.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" diff --git a/ortools/routing/parsers/pdtsp_parser.cc b/ortools/routing/parsers/pdtsp_parser.cc index ba1406a043..53402b6cdc 100644 --- a/ortools/routing/parsers/pdtsp_parser.cc +++ b/ortools/routing/parsers/pdtsp_parser.cc @@ -13,33 +13,19 @@ #include "ortools/routing/parsers/pdtsp_parser.h" +#include +#include #include #include #include #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" -#include "ortools/base/gzipfile.h" #include "ortools/base/mathutil.h" -#include "ortools/base/numbers.h" -#include "ortools/base/path.h" #include "ortools/base/strtoint.h" #include "ortools/util/filelineiter.h" namespace operations_research::routing { -namespace { - -using absl::ByAnyChar; - -File* OpenReadOnly(absl::string_view file_name) { - File* file = nullptr; - if (file::Open(file_name, "r", &file, file::Defaults()).ok() && - file::Extension(file_name) == "gz") { - file = GZipFileReader(file_name, file, TAKE_OWNERSHIP); - } - return file; -} -} // namespace PdTspParser::PdTspParser() : section_(SIZE_SECTION) {} @@ -63,7 +49,7 @@ std::function PdTspParser::Distances() const { void PdTspParser::ProcessNewLine(const std::string& line) { const std::vector words = - absl::StrSplit(line, ByAnyChar(" :\t"), absl::SkipEmpty()); + absl::StrSplit(line, absl::ByAnyChar(" :\t"), absl::SkipEmpty()); if (!words.empty()) { switch (section_) { case SIZE_SECTION: { diff --git a/ortools/routing/parsers/solomon_parser_test.cc b/ortools/routing/parsers/solomon_parser_test.cc index 0791d364e1..d976f929e9 100644 --- a/ortools/routing/parsers/solomon_parser_test.cc +++ b/ortools/routing/parsers/solomon_parser_test.cc @@ -17,14 +17,13 @@ #include "absl/flags/flag.h" #include "gtest/gtest.h" -#include "ortools/base/commandlineflags.h" #include "ortools/base/gmock.h" #include "ortools/base/path.h" #define ROOT_DIR "_main/" ABSL_FLAG(std::string, solomon_test_archive, - "ortools/bench/solomon/" + "ortools/routing/benchmarks/solomon/" "testdata/solomon.zip", "Solomon: testing archive"); ABSL_FLAG(std::string, solomon_test_instance, "google2.txt", diff --git a/ortools/routing/parsers/tsplib_parser.cc b/ortools/routing/parsers/tsplib_parser.cc index 6af8145729..0ef4b2eb26 100644 --- a/ortools/routing/parsers/tsplib_parser.cc +++ b/ortools/routing/parsers/tsplib_parser.cc @@ -15,7 +15,9 @@ #include #include +#include #include +#include #include #include #include @@ -23,16 +25,25 @@ #include "absl/container/flat_hash_map.h" #include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" +#include "ortools/base/file.h" #include "ortools/base/map_util.h" #include "ortools/base/numbers.h" +#include "ortools/base/options.h" #include "ortools/base/path.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" #include "ortools/base/strtoint.h" #include "ortools/base/zipfile.h" +#include "ortools/routing/parsers/simple_graph.h" #include "ortools/util/filelineiter.h" #include "re2/re2.h" @@ -158,8 +169,8 @@ std::shared_ptr OpenZipArchiveIfItExists( TspLibParser::TspLibParser() : size_(0), - capacity_(kint64max), - max_distance_(kint64max), + capacity_(std::numeric_limits::max()), + max_distance_(std::numeric_limits::max()), distance_function_(nullptr), explicit_costs_(), depot_(0), @@ -171,28 +182,45 @@ TspLibParser::TspLibParser() edge_column_(0), to_read_(0) {} -bool TspLibParser::LoadFile(absl::string_view file_name) { +namespace { +absl::StatusOr OpenFile(absl::string_view file_name) { + File* file = nullptr; + RETURN_IF_ERROR(file::Open(file_name, "r", &file, file::Defaults())); + return file; +} +} // namespace + +absl::Status TspLibParser::LoadFile(absl::string_view file_name) { std::shared_ptr zip_archive( OpenZipArchiveIfItExists(file_name)); + valid_section_found_ = false; + ASSIGN_OR_RETURN(File* const file, OpenFile(file_name)); for (const std::string& line : - FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + FileLines(file_name, file, FileLineIterator::REMOVE_INLINE_CR)) { ProcessNewLine(line); } FinalizeEdgeWeights(); - return true; + if (!valid_section_found_) { + return util::InvalidArgumentErrorBuilder() + << "Could not find any valid sections in " << file_name; + } + return absl::OkStatus(); } -int TspLibParser::SizeFromFile(absl::string_view file_name) const { +absl::StatusOr TspLibParser::SizeFromFile( + absl::string_view file_name) const { std::shared_ptr zip_archive( OpenZipArchiveIfItExists(file_name)); + ASSIGN_OR_RETURN(File* const file, OpenFile(file_name)); int size = 0; for (const std::string& line : - FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + FileLines(file_name, file, FileLineIterator::REMOVE_INLINE_CR)) { if (RE2::PartialMatch(line, "DIMENSION\\s*:\\s*(\\d+)", &size)) { - break; + return size; } } - return size; + return util::InvalidArgumentErrorBuilder() + << "Could not determine problem size from " << file_name; } void TspLibParser::ParseExplicitFullMatrix( @@ -398,12 +426,12 @@ void TspLibParser::FinalizeEdgeWeights() { } } -void TspLibParser::ParseSections(absl::Span words) { +bool TspLibParser::ParseSections(absl::Span words) { const int words_size = words.size(); CHECK_GT(words_size, 0); if (!gtl::FindCopy(*kSections, words[0], §ion_)) { LOG(WARNING) << "Unknown section: " << words[0]; - return; + return false; } const std::string& last_word = words[words_size - 1]; switch (section_) { @@ -483,7 +511,7 @@ void TspLibParser::ParseSections(absl::Span words) { break; } case FIXED_EDGES_SECTION: { - to_read_ = kint64max; + to_read_ = std::numeric_limits::max(); break; } case NODE_COORD_TYPE: { @@ -501,7 +529,7 @@ void TspLibParser::ParseSections(absl::Span words) { break; } case DEPOT_SECTION: { - to_read_ = kint64max; + to_read_ = std::numeric_limits::max(); break; } case DEMAND_SECTION: { @@ -516,6 +544,7 @@ void TspLibParser::ParseSections(absl::Span words) { LOG(WARNING) << "Unknown section: " << words[0]; } } + return true; } void TspLibParser::ProcessNewLine(const std::string& line) { @@ -653,7 +682,9 @@ void TspLibParser::ProcessNewLine(const std::string& line) { } } } else { - ParseSections(words); + // TODO(user): Check that proper sections were read (necessary and + // non-overlapping ones). + valid_section_found_ |= ParseSections(words); } } } @@ -745,19 +776,18 @@ const absl::flat_hash_map* const TspLibTourParser::TspLibTourParser() : section_(UNDEFINED_SECTION), size_(0) {} -// TODO(user): Return false when issues were encountered while parsing the -// file. -bool TspLibTourParser::LoadFile(absl::string_view file_name) { +absl::Status TspLibTourParser::LoadFile(absl::string_view file_name) { section_ = UNDEFINED_SECTION; comments_.clear(); tour_.clear(); std::shared_ptr zip_archive( OpenZipArchiveIfItExists(file_name)); + ASSIGN_OR_RETURN(File* const file, OpenFile(file_name)); for (const std::string& line : - FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + FileLines(file_name, file, FileLineIterator::REMOVE_INLINE_CR)) { ProcessNewLine(line); } - return true; + return absl::OkStatus(); } void TspLibTourParser::ProcessNewLine(const std::string& line) { @@ -816,18 +846,17 @@ const absl::flat_hash_map* const CVRPToursParser::CVRPToursParser() : cost_(0) {} -// TODO(user): Return false when issues were encountered while parsing the -// file. -bool CVRPToursParser::LoadFile(absl::string_view file_name) { +absl::Status CVRPToursParser::LoadFile(absl::string_view file_name) { tours_.clear(); cost_ = 0; std::shared_ptr zip_archive( OpenZipArchiveIfItExists(file_name)); + ASSIGN_OR_RETURN(File* const file, OpenFile(file_name)); for (const std::string& line : - FileLines(file_name, FileLineIterator::REMOVE_INLINE_CR)) { + FileLines(file_name, file, FileLineIterator::REMOVE_INLINE_CR)) { ProcessNewLine(line); } - return true; + return absl::OkStatus(); } void CVRPToursParser::ProcessNewLine(const std::string& line) { diff --git a/ortools/routing/parsers/tsplib_parser.h b/ortools/routing/parsers/tsplib_parser.h index 679ee19569..adaea67c0b 100644 --- a/ortools/routing/parsers/tsplib_parser.h +++ b/ortools/routing/parsers/tsplib_parser.h @@ -24,16 +24,17 @@ #ifndef OR_TOOLS_ROUTING_PARSERS_TSPLIB_PARSER_H_ #define OR_TOOLS_ROUTING_PARSERS_TSPLIB_PARSER_H_ -#include +#include #include #include #include #include #include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" -#include "ortools/base/types.h" #include "ortools/routing/parsers/simple_graph.h" namespace operations_research::routing { @@ -45,9 +46,9 @@ class TspLibParser final { TspLibParser(); // Loads and parses a routing problem from a given file. - bool LoadFile(absl::string_view file_name); + absl::Status LoadFile(absl::string_view file_name); // Returns the number of nodes in the routing problem stored in a given file. - int SizeFromFile(absl::string_view file_name) const; + absl::StatusOr SizeFromFile(absl::string_view file_name) const; // Returns a function returning edge weights between nodes. EdgeWeights GetEdgeWeights() const { return distance_function_; } // Returns the index of the depot. @@ -150,7 +151,7 @@ class TspLibParser final { void ParseNodeCoord(absl::Span words); void SetUpEdgeWeightSection(); void FinalizeEdgeWeights(); - void ParseSections(absl::Span words); + bool ParseSections(absl::Span words); void ProcessNewLine(const std::string& line); void SetExplicitCost(int from, int to, int64_t cost) { if (explicit_costs_.size() != size_ * size_) { @@ -190,6 +191,7 @@ class TspLibParser final { std::string name_; std::string comments_; int64_t to_read_; + bool valid_section_found_ = false; }; // Class parsing tour (solution) data in TSLIB95 format. @@ -198,7 +200,7 @@ class TspLibTourParser final { public: TspLibTourParser(); // Loads and parses a given tour file. - bool LoadFile(absl::string_view file_name); + absl::Status LoadFile(absl::string_view file_name); // Returns a vector corresponding to the sequence of nodes of the tour. const std::vector& tour() const { return tour_; } // Returns the size of the tour. @@ -237,7 +239,7 @@ class CVRPToursParser final { public: CVRPToursParser(); // Loads and parses a given tours file. - bool LoadFile(absl::string_view file_name); + absl::Status LoadFile(absl::string_view file_name); // Returns a vector corresponding to the sequence of nodes of tours. const std::vector>& tours() const { return tours_; } int64_t cost() const { return cost_; } diff --git a/ortools/routing/parsers/tsplib_parser_test.cc b/ortools/routing/parsers/tsplib_parser_test.cc index 4854d35890..357014bfdc 100644 --- a/ortools/routing/parsers/tsplib_parser_test.cc +++ b/ortools/routing/parsers/tsplib_parser_test.cc @@ -21,10 +21,12 @@ #include "absl/base/macros.h" #include "absl/container/btree_set.h" +#include "absl/status/status.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" #include "gtest/gtest.h" #include "ortools/base/filesystem.h" +#include "ortools/base/gmock.h" #include "ortools/base/memfile.h" #include "ortools/base/options.h" #include "ortools/base/path.h" @@ -35,6 +37,9 @@ namespace operations_research::routing { namespace { +using ::testing::Gt; +using ::testing::status::IsOkAndHolds; + TEST(TspLibParserTest, GeneratedDataSets) { static const char kName[] = "GoogleTest"; static const char* const kTypes[] = {"TSP", "CVRP"}; @@ -187,8 +192,9 @@ TEST(TspLibParserTest, GeneratedDataSets) { const std::string kMMFileName{std::tmpnam(nullptr)}; RegisteredMemFile registered(kMMFileName, data); TspLibParser parser; - EXPECT_TRUE(parser.LoadFile(kMMFileName)); - EXPECT_EQ(kDimension, parser.SizeFromFile(kMMFileName)); + EXPECT_OK(parser.LoadFile(kMMFileName)); + EXPECT_THAT(parser.SizeFromFile(kMMFileName), + IsOkAndHolds(kDimension)); } } } @@ -210,8 +216,8 @@ TEST(TspLibParserTest, ParseHCPEdgeList) { const std::string kMMFileName{std::tmpnam(nullptr)}; RegisteredMemFile registered(kMMFileName, kData); TspLibParser parser; - EXPECT_TRUE(parser.LoadFile(kMMFileName)); - EXPECT_EQ(3, parser.SizeFromFile(kMMFileName)); + EXPECT_OK(parser.LoadFile(kMMFileName)); + EXPECT_THAT(parser.SizeFromFile(kMMFileName), IsOkAndHolds(3)); EXPECT_EQ(2, parser.edges()[0].size()); EXPECT_EQ(1, parser.edges()[0][0]); EXPECT_EQ(2, parser.edges()[0][1]); @@ -232,8 +238,8 @@ TEST(TspLibParserTest, ParseHCPAdjList) { const std::string kMMFileName{std::tmpnam(nullptr)}; RegisteredMemFile registered(kMMFileName, kData); TspLibParser parser; - EXPECT_TRUE(parser.LoadFile(kMMFileName)); - EXPECT_EQ(3, parser.SizeFromFile(kMMFileName)); + EXPECT_OK(parser.LoadFile(kMMFileName)); + EXPECT_THAT(parser.SizeFromFile(kMMFileName), IsOkAndHolds(3)); EXPECT_EQ(1, parser.edges()[0].size()); EXPECT_EQ(2, parser.edges()[0][0]); EXPECT_EQ(1, parser.edges()[1].size()); @@ -247,7 +253,7 @@ TEST(TspLibParserTest, ParseKytojoki33Depot) { ::testing::SrcDir(), ROOT_DIR "ortools/routing/parsers/testdata/", "tsplib_Kytojoki_33.vrp"); TspLibParser parser; - EXPECT_TRUE(parser.LoadFile(file_name)); + EXPECT_OK(parser.LoadFile(file_name)); // The depot is a new node, given by its coordinates, instead of an existing // node in the graph. EXPECT_EQ(2400, parser.depot()); @@ -256,6 +262,18 @@ TEST(TspLibParserTest, ParseKytojoki33Depot) { EXPECT_EQ(0.0, parser.coordinates()[parser.depot()].y); } +// Make sure we properly fail when reading an invalid file. To test this, +// reading from a raw tar file instead of the included subfiles. +TEST(TspLibParserTest, ReadFailOnInvalidFile) { + std::string file_name = + file::JoinPath(::testing::SrcDir(), ROOT_DIR + "operations_research_data/operations_research_data/" + "TSPLIB95/ALL_tsp.tar.gz"); + TspLibParser parser; + EXPECT_THAT(parser.LoadFile(file_name), + testing::status::StatusIs(absl::StatusCode::kInvalidArgument)); +} + TEST(TspLibTourParserTest, LoadAllDataSets) { static const char kArchive[] = ROOT_DIR "operations_research_data/TSPLIB95/ALL_tsp.tar.gz"; @@ -300,7 +318,7 @@ TEST(TspLibTourParserTest, LoadAllDataSets) { .ok()) { for (const std::string& match : matches) { TspLibTourParser parser; - EXPECT_TRUE(parser.LoadFile(match)); + EXPECT_OK(parser.LoadFile(match)); EXPECT_EQ(kExpectedComments[file_index], parser.comments()); file_index++; } @@ -335,7 +353,7 @@ TEST(CVRPToursParserTest, LoadAllDataSets) { .ok()) { for (const std::string& match : matches) { CVRPToursParser parser; - EXPECT_TRUE(parser.LoadFile(match)); + EXPECT_OK(parser.LoadFile(match)); EXPECT_EQ(kExpectedCosts[file_index], parser.cost()); file_index++; } diff --git a/ortools/routing/parsers/tsptw_parser.cc b/ortools/routing/parsers/tsptw_parser.cc index ac978a4892..d6ed7af952 100644 --- a/ortools/routing/parsers/tsptw_parser.cc +++ b/ortools/routing/parsers/tsptw_parser.cc @@ -25,7 +25,6 @@ #include "ortools/base/mathutil.h" #include "ortools/base/numbers.h" #include "ortools/base/path.h" -#include "ortools/base/strtoint.h" #include "ortools/base/zipfile.h" #include "ortools/util/filelineiter.h"