diff --git a/ortools/routing/BUILD.bazel b/ortools/routing/BUILD.bazel index 05a94d41a25..7684c5bde33 100644 --- a/ortools/routing/BUILD.bazel +++ b/ortools/routing/BUILD.bazel @@ -192,6 +192,9 @@ cc_library( "//ortools/constraint_solver:cp", "//ortools/util:bitset", "//ortools/util:saturated_arithmetic", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/types:span", ], @@ -220,6 +223,7 @@ cc_library( deps = [ ":filter_committables", "//ortools/algorithms:binary_search", + "//ortools/base:types", "//ortools/util:saturated_arithmetic", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/types:span", @@ -269,6 +273,7 @@ cc_library( ":types", ":utils", "//ortools/base:adjustable_priority_queue", + "//ortools/base:base_export", "//ortools/base:dump_vars", "//ortools/base:log_severity", "//ortools/base:map_util", @@ -334,6 +339,7 @@ cc_library( srcs = ["filter_committables.cc"], hdrs = ["filter_committables.h"], deps = [ + "//ortools/base:types", "//ortools/util:bitset", "//ortools/util:saturated_arithmetic", "@abseil-cpp//absl/log:check", @@ -347,6 +353,7 @@ cc_library( hdrs = ["fourier_solver.h"], deps = [ "//ortools/base:strong_vector", + "//ortools/base:types", "//ortools/util:strong_integers", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/container:flat_hash_map", @@ -358,20 +365,21 @@ cc_library( ], ) -# cc_test( -# name = "fourier_solver_test", -# srcs = ["fourier_solver_test.cc"], -# deps = [ -# ":fourier_solver", -# "//ortools/base:gmock_main", -# "//ortools/util:optional_boolean_cc_proto", -# "@abseil-cpp//absl/algorithm:container", -# "@abseil-cpp//absl/container:flat_hash_map", -# "@abseil-cpp//absl/strings", -# "@abseil-cpp//absl/types:span", -# "@protobuf//:duration_cc_proto", -# ], -# ) +cc_test( + name = "fourier_solver_test", + srcs = ["fourier_solver_test.cc"], + deps = [ + ":fourier_solver", + "//ortools/base:gmock_main", + "//ortools/util:optional_boolean_cc_proto", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/types:span", + "@google_benchmark//:benchmark", + "@protobuf//:duration_cc_proto", + ], +) proto_library( name = "heuristic_parameters_proto", diff --git a/ortools/routing/CMakeLists.txt b/ortools/routing/CMakeLists.txt index 78a3b0fa835..80552f2a77e 100644 --- a/ortools/routing/CMakeLists.txt +++ b/ortools/routing/CMakeLists.txt @@ -12,6 +12,8 @@ # limitations under the License. file(GLOB _SRCS "*.h" "*.cc") +list(FILTER _SRCS EXCLUDE REGEX ".*/.*_benchmark.cc") +list(FILTER _SRCS EXCLUDE REGEX ".*/.*_test.cc") set(NAME ${PROJECT_NAME}_routing) # Will be merge in libortools.so diff --git a/ortools/routing/constraints.cc b/ortools/routing/constraints.cc index 2caab37b5d6..dbc3fe80093 100644 --- a/ortools/routing/constraints.cc +++ b/ortools/routing/constraints.cc @@ -28,6 +28,7 @@ #include "absl/log/check.h" #include "absl/types/span.h" #include "ortools/base/strong_vector.h" +#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraints.h" #include "ortools/constraint_solver/interval.h" @@ -264,7 +265,7 @@ class ResourceAssignmentConstraint : public Constraint { dim->CumulVar(model_.End(vehicle)) ->SetRange(attributes.end_domain().Min(), attributes.end_domain().Max()); - if (attributes.span_upper_bound() < std::numeric_limits::max()) { + if (attributes.span_upper_bound() < kint64max) { dim->vehicle_span_variables()[vehicle]->SetMax( attributes.span_upper_bound()); } @@ -359,22 +360,18 @@ class PathSpansAndTotalSlacks : public Constraint { // for span and total_slack or not. int64_t SpanMin(int vehicle, int64_t sum_fixed_transits) { DCHECK_GE(sum_fixed_transits, 0); - const int64_t span_min = spans_[vehicle] - ? spans_[vehicle]->Min() - : std::numeric_limits::max(); - const int64_t total_slack_min = total_slacks_[vehicle] - ? total_slacks_[vehicle]->Min() - : std::numeric_limits::max(); + const int64_t span_min = + spans_[vehicle] ? spans_[vehicle]->Min() : kint64max; + const int64_t total_slack_min = + total_slacks_[vehicle] ? total_slacks_[vehicle]->Min() : kint64max; return std::min(span_min, CapAdd(total_slack_min, sum_fixed_transits)); } int64_t SpanMax(int vehicle, int64_t sum_fixed_transits) { DCHECK_GE(sum_fixed_transits, 0); - const int64_t span_max = spans_[vehicle] - ? spans_[vehicle]->Max() - : std::numeric_limits::min(); - const int64_t total_slack_max = total_slacks_[vehicle] - ? total_slacks_[vehicle]->Max() - : std::numeric_limits::min(); + const int64_t span_max = + spans_[vehicle] ? spans_[vehicle]->Max() : kint64min; + const int64_t total_slack_max = + total_slacks_[vehicle] ? total_slacks_[vehicle]->Max() : kint64min; return std::max(span_max, CapAdd(total_slack_max, sum_fixed_transits)); } void SetSpanMin(int vehicle, int64_t min, int64_t sum_fixed_transits) { @@ -545,9 +542,7 @@ class PathSpansAndTotalSlacks : public Constraint { const int64_t span_max = SpanMax(vehicle, sum_fixed_transits); const int64_t slack_from_lb = CapSub(span_max, span_lb); const int64_t slack_from_ub = - span_ub < std::numeric_limits::max() - ? CapSub(span_ub, span_min) - : std::numeric_limits::max(); + span_ub < kint64max ? CapSub(span_ub, span_min) : kint64max; for (const int node : path_) { IntVar* transit_var = dimension_->TransitVar(node); const int64_t transit_i_min = transit_var->Min(); diff --git a/ortools/routing/csharp/routing.i b/ortools/routing/csharp/routing.i index 2707f45442d..34d006f2e17 100644 --- a/ortools/routing/csharp/routing.i +++ b/ortools/routing/csharp/routing.i @@ -224,10 +224,25 @@ using Domain = Google.OrTools.Util.Domain; %unignore Model::End; %unignore Model::GetArcCostForVehicle; %unignore Model::GetDimensionOrDie; +%unignore Model::GetDisjunctionMaxCardinality; +%unignore Model::GetDisjunctionMinCardinality; +%unignore Model::GetDisjunctionSoftMaxCardinality; +%unignore Model::GetDisjunctionSoftMaxPenalty; +%unignore Model::GetDisjunctionSoftMaxPenaltyCostBehavior; +%unignore Model::GetDisjunctionSoftMinCardinality; +%unignore Model::GetDisjunctionSoftMinPenalty; +%unignore Model::GetDisjunctionSoftMinPenaltyCostBehavior; +%unignore Model::GetDisjunctionPenalty; +%unignore Model::GetNumberOfDisjunctions; %unignore Model::GetMutableDimension; %unignore Model::IsEnd; %unignore Model::IsStart; %unignore Model::IsVehicleUsed; +%unignore Model::MakeDisjunction; +%unignore Model::SetDisjunctionHardMaximum; +%unignore Model::SetDisjunctionHardMinimum; +%unignore Model::SetDisjunctionSoftMaximum; +%unignore Model::SetDisjunctionSoftMinimum; %unignore Model::NextVar; %unignore Model::ReadAssignmentFromRoutes; %unignore Model::RegisterTransitCallback; diff --git a/ortools/routing/decision_builders.cc b/ortools/routing/decision_builders.cc index d4377e05310..abe8682744f 100644 --- a/ortools/routing/decision_builders.cc +++ b/ortools/routing/decision_builders.cc @@ -28,6 +28,7 @@ #include "absl/types/span.h" #include "ortools/base/map_util.h" #include "ortools/base/strong_vector.h" +#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/interval.h" #include "ortools/routing/lp_scheduling.h" @@ -172,7 +173,7 @@ void AppendRouteCumulAndBreakVarAndValues( int new_num_values = old_num_values; for (int j = old_num_values; j < vals.size(); ++j) { // Value kint64min signals an unoptimized variable, skip setting those. - if (vals[j] == std::numeric_limits::min()) continue; + if (vals[j] == kint64min) continue; // Skip variables that are not bound. if (vars[j]->Bound()) continue; vals[new_num_values] = vals[j]; @@ -667,7 +668,7 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { DCHECK_EQ(cp_variables_.size(), cp_values_.size()); // Value kint64min signals an unoptimized variable, set to min instead. for (int j = 0; j < cp_values_.size(); ++j) { - if (cp_values_[j] == std::numeric_limits::min()) { + if (cp_values_[j] == kint64min) { cp_values_[j] = cp_variables_[j]->Min(); } } diff --git a/ortools/routing/filter_committables.h b/ortools/routing/filter_committables.h index 3f591cb60d8..6975d934833 100644 --- a/ortools/routing/filter_committables.h +++ b/ortools/routing/filter_committables.h @@ -22,6 +22,7 @@ #include "absl/log/check.h" #include "absl/types/span.h" +#include "ortools/base/types.h" #include "ortools/util/bitset.h" #include "ortools/util/saturated_arithmetic.h" @@ -276,8 +277,7 @@ class DimensionValues { void Subtract(const Interval& other) { *this = *this - other; } // Returns an interval containing all integers: {kint64min, kint64max}. static Interval AllIntegers() { - return {.min = std::numeric_limits::min(), - .max = std::numeric_limits::max()}; + return {.min = kint64min, .max = kint64max}; } }; diff --git a/ortools/routing/filters.cc b/ortools/routing/filters.cc index 14318428b71..c8ae1d3405a 100644 --- a/ortools/routing/filters.cc +++ b/ortools/routing/filters.cc @@ -551,17 +551,17 @@ namespace { // Node disjunction filter class. class NodeDisjunctionFilter : public IntVarLocalSearchFilter { public: + using Disjunction = Model::Disjunction; + static constexpr auto kPenalizeOnce = + Model::PenaltyCostBehavior::PENALIZE_ONCE; explicit NodeDisjunctionFilter(const Model& routing_model, bool filter_cost) : IntVarLocalSearchFilter(routing_model.Nexts()), - routing_model_(routing_model), + model_(routing_model), count_per_disjunction_(routing_model.GetNumberOfDisjunctions(), {.active = 0, .inactive = 0}), synchronized_objective_value_(kint64min), accepted_objective_value_(kint64min), - filter_cost_(filter_cost), - has_mandatory_disjunctions_(routing_model.HasMandatoryDisjunctions()) {} - - using Disjunction = DisjunctionIndex; + filter_cost_(filter_cost) {} bool Accept(const Assignment* delta, const Assignment* /*deltadelta*/, int64_t /*objective_min*/, int64_t objective_max) override { @@ -588,8 +588,8 @@ class NodeDisjunctionFilter : public IntVarLocalSearchFilter { continue; } // Change counts of all disjunctions affected by this node. - for (const Disjunction disjunction : - routing_model_.GetDisjunctionIndices(node)) { + for (const DisjunctionIndex disjunction : + model_.GetDisjunctionIndices(node)) { ActivityCount new_count = count_per_disjunction_.Get(disjunction.value()); new_count.active += contribution_delta.active; @@ -597,45 +597,62 @@ class NodeDisjunctionFilter : public IntVarLocalSearchFilter { count_per_disjunction_.Set(disjunction.value(), new_count); } } - // Check if any disjunction has too many active nodes. - for (const int index : count_per_disjunction_.ChangedIndices()) { - if (count_per_disjunction_.Get(index).active > - routing_model_.GetDisjunctionMaxCardinality(Disjunction(index))) { + // Check if any disjunction is infeasible. + for (const int dindex : count_per_disjunction_.ChangedIndices()) { + const Disjunction disj = model_.GetDisjunction(DisjunctionIndex(dindex)); + if (count_per_disjunction_.Get(dindex).active > disj.max_cardinality) { + return false; + } + const int64_t max_inactives = disj.indices.size() - disj.min_cardinality; + if (max_inactives < count_per_disjunction_.Get(dindex).inactive) { return false; } } - if (lns_detected || (!filter_cost_ && !has_mandatory_disjunctions_)) { + if (lns_detected || !filter_cost_) { accepted_objective_value_ = 0; return true; } // Update penalty costs for disjunctions. accepted_objective_value_ = synchronized_objective_value_; for (const int index : count_per_disjunction_.ChangedIndices()) { - // If num inactives did not change, skip. Common shortcut. - const int old_inactives = - count_per_disjunction_.GetCommitted(index).inactive; - const int new_inactives = count_per_disjunction_.Get(index).inactive; - if (old_inactives == new_inactives) continue; - // If this disjunction has no penalty for inactive nodes, skip. - const Disjunction disjunction(index); - const int64_t penalty = routing_model_.GetDisjunctionPenalty(disjunction); - if (penalty == 0) continue; - - // Compute the new cost of activity bound violations. - const int max_inactives = - routing_model_.GetDisjunctionNodeIndices(disjunction).size() - - routing_model_.GetDisjunctionMaxCardinality(disjunction); - int new_violation = std::max(0, new_inactives - max_inactives); - int old_violation = std::max(0, old_inactives - max_inactives); - // If nodes are mandatory, there can be no violation. - if (penalty < 0 && new_violation > 0) return false; - if (routing_model_.GetDisjunctionPenaltyCostBehavior(disjunction) == - Model::PenaltyCostBehavior::PENALIZE_ONCE) { - new_violation = std::min(1, new_violation); - old_violation = std::min(1, old_violation); + const ActivityCount& new_counts = count_per_disjunction_.Get(index); + const ActivityCount& old_counts = + count_per_disjunction_.GetCommitted(index); + const Disjunction disj = model_.GetDisjunction(DisjunctionIndex(index)); + + // Compute the new cost of soft min activity bound violations. + if (disj.soft_min_penalty != 0 && + old_counts.inactive != new_counts.inactive) { + const int64_t max_inactives = + disj.indices.size() - disj.soft_min_cardinality; + int64_t new_min_violation = + std::max(0, new_counts.inactive - max_inactives); + int64_t old_min_violation = + std::max(0, old_counts.inactive - max_inactives); + if (disj.soft_min_penalty_type == kPenalizeOnce) { + new_min_violation = std::min(1, new_min_violation); + old_min_violation = std::min(1, old_min_violation); + } + CapAddTo(CapProd(disj.soft_min_penalty, + (new_min_violation - old_min_violation)), + &accepted_objective_value_); + } + + // Compute the new cost of soft max activity bound violations. + if (disj.soft_max_penalty != 0 && + old_counts.active != new_counts.active) { + int64_t new_max_violation = + std::max(new_counts.active - disj.soft_max_cardinality, 0); + int64_t old_max_violation = + std::max(old_counts.active - disj.soft_max_cardinality, 0); + if (disj.soft_max_penalty_type == kPenalizeOnce) { + new_max_violation = std::min(1, new_max_violation); + old_max_violation = std::min(1, old_max_violation); + } + CapAddTo(CapProd(disj.soft_max_penalty, + (new_max_violation - old_max_violation)), + &accepted_objective_value_); } - CapAddTo(CapProd(penalty, (new_violation - old_violation)), - &accepted_objective_value_); } // Only compare to max as a cost lower bound is computed. return accepted_objective_value_ <= objective_max; @@ -652,38 +669,46 @@ class NodeDisjunctionFilter : public IntVarLocalSearchFilter { void OnSynchronize(const Assignment* /*delta*/) override { synchronized_objective_value_ = 0; count_per_disjunction_.Revert(); - const int num_disjunctions = routing_model_.GetNumberOfDisjunctions(); - for (Disjunction disjunction(0); disjunction < num_disjunctions; - ++disjunction) { + const int num_disjunctions = model_.GetNumberOfDisjunctions(); + for (int dindex = 0; dindex < num_disjunctions; ++dindex) { + const Disjunction& disj = model_.GetDisjunction(DisjunctionIndex(dindex)); // Count number of active/inactive nodes of this disjunction. ActivityCount count = {.active = 0, .inactive = 0}; - const auto& nodes = routing_model_.GetDisjunctionNodeIndices(disjunction); - for (const int64_t node : nodes) { + for (const int64_t node : disj.indices) { if (!IsVarSynced(node)) continue; const int is_active = Value(node) != node; count.active += is_active; count.inactive += !is_active; } - count_per_disjunction_.Set(disjunction.value(), count); + count_per_disjunction_.Set(dindex, count); // Add penalty of this disjunction to total cost. if (!filter_cost_) continue; - const int64_t penalty = routing_model_.GetDisjunctionPenalty(disjunction); - const int max_actives = - routing_model_.GetDisjunctionMaxCardinality(disjunction); - int violation = count.inactive - (nodes.size() - max_actives); - if (violation > 0 && penalty > 0) { - if (routing_model_.GetDisjunctionPenaltyCostBehavior(disjunction) == - Model::PenaltyCostBehavior::PENALIZE_ONCE) { - violation = std::min(1, violation); + + if (disj.soft_min_penalty > 0) { + const int64_t max_inactives = + disj.indices.size() - disj.soft_min_cardinality; + int64_t violation = count.inactive - max_inactives; + if (violation > 0) { + if (disj.soft_min_penalty_type == kPenalizeOnce) violation = 1; + CapAddTo(CapProd(disj.soft_min_penalty, violation), + &synchronized_objective_value_); + } + } + + if (disj.soft_max_penalty > 0) { + int64_t violation = count.active - disj.soft_max_cardinality; + if (violation > 0) { + if (disj.soft_max_penalty_type == kPenalizeOnce) violation = 1; + CapAddTo(CapProd(disj.soft_max_penalty, violation), + &synchronized_objective_value_); } - CapAddTo(CapProd(penalty, violation), &synchronized_objective_value_); } } count_per_disjunction_.Commit(); accepted_objective_value_ = synchronized_objective_value_; } - const Model& routing_model_; + const Model& model_; struct ActivityCount { int active = 0; int inactive = 0; @@ -692,7 +717,6 @@ class NodeDisjunctionFilter : public IntVarLocalSearchFilter { int64_t synchronized_objective_value_; int64_t accepted_objective_value_; const bool filter_cost_; - const bool has_mandatory_disjunctions_; }; } // namespace diff --git a/ortools/routing/flow.cc b/ortools/routing/flow.cc index d8177bb77b4..c5fe68a9944 100644 --- a/ortools/routing/flow.cc +++ b/ortools/routing/flow.cc @@ -26,6 +26,7 @@ #include "absl/log/check.h" #include "absl/types/span.h" #include "ortools/base/map_util.h" +#include "ortools/base/types.h" #include "ortools/constraint_solver/assignment.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/graph/min_cost_flow.h" @@ -51,11 +52,15 @@ void AddDisjunctionsFromNodes(const Model& model, } // namespace bool Model::IsMatchingModel() const { + // Check that each node appears in at most one disjunction. // TODO(user): Support overlapping disjunctions and disjunctions with // a cardinality > 1. absl::flat_hash_set disjunction_nodes; for (DisjunctionIndex i(0); i < GetNumberOfDisjunctions(); ++i) { - if (GetDisjunctionMaxCardinality(i) > 1) return false; + const Disjunction disj = GetDisjunction(i); + if (disj.max_cardinality > 1) return false; + if (disj.soft_max_penalty != 0) return false; + if (disj.soft_min_cardinality > 1) return false; for (int64_t node : GetDisjunctionNodeIndices(i)) { if (!disjunction_nodes.insert(node).second) return false; } @@ -85,14 +90,13 @@ bool Model::IsMatchingModel() const { for (int64_t vehicle_capacity : dimension->vehicle_capacities()) { max_vehicle_capacity = std::max(max_vehicle_capacity, vehicle_capacity); } - std::vector transits(nexts_.size(), - std::numeric_limits::max()); + std::vector transits(nexts_.size(), kint64max); for (int i = 0; i < nexts_.size(); ++i) { if (!IsStart(i) && !IsEnd(i)) { transits[i] = std::min(transits[i], transit(i)); } } - int64_t min_transit = std::numeric_limits::max(); + int64_t min_transit = kint64max; // Find the minimal accumulated value resulting from a pickup and delivery // pair. for (const auto& [pickups, deliveries] : GetPickupAndDeliveryPairs()) { @@ -198,12 +202,14 @@ bool Model::SolveMatchingModel(Assignment* assignment, penalty = kNoPenalty; } else { for (DisjunctionIndex index : disjunctions) { - const int64_t d_penalty = GetDisjunctionPenalty(index); - if (d_penalty == kNoPenalty) { + const Disjunction& disj = GetDisjunction(index); + if (disj.min_cardinality == 1) { penalty = kNoPenalty; break; } - penalty = CapAdd(penalty, d_penalty); + if (disj.soft_min_cardinality == 1) { + penalty = CapAdd(penalty, disj.soft_min_penalty); + } } } disjunction_penalties.push_back(penalty); @@ -216,9 +222,13 @@ bool Model::SolveMatchingModel(Assignment* assignment, GetDisjunctionIndices(node); DCHECK_LE(disjunctions.size(), 1); disjunction_to_flow_nodes.push_back({}); - disjunction_penalties.push_back( - disjunctions.empty() ? kNoPenalty - : GetDisjunctionPenalty(disjunctions.back())); + int64_t penalty = kNoPenalty; + if (!disjunctions.empty()) { + const Disjunction& disj = GetDisjunction(disjunctions.back()); + if (disj.min_cardinality == 0) penalty = 0; + if (disj.soft_min_cardinality == 1) penalty = disj.soft_min_penalty; + } + disjunction_penalties.push_back(penalty); if (disjunctions.empty()) { in_disjunction[node] = true; flow_to_non_pd[num_flow_nodes] = node; @@ -372,7 +382,7 @@ bool Model::SolveMatchingModel(Assignment* assignment, const int actual_flow_num_nodes = num_flow_nodes + 3; if (log(static_cast(arc_with_max_cost.cost) + 1) + 2 * log(actual_flow_num_nodes) > - log(std::numeric_limits::max())) { + log(kint64max)) { scale_factor = CapProd(actual_flow_num_nodes, actual_flow_num_nodes); } diff --git a/ortools/routing/fourier_solver.cc b/ortools/routing/fourier_solver.cc index 7f8bcf44979..e9c2a9e8af0 100644 --- a/ortools/routing/fourier_solver.cc +++ b/ortools/routing/fourier_solver.cc @@ -35,6 +35,7 @@ #include "absl/strings/string_view.h" #include "absl/types/span.h" #include "ortools/base/strong_vector.h" +#include "ortools/base/types.h" namespace operations_research::routing { @@ -303,7 +304,7 @@ bool FourierSolver::Solve() { // TODO(b/492476073): compare other scores, could p * q - p - q be better? // TODO(b/492476073): count constraint-wise instead of variable-wise. ColIndex min_var{0}; - int64_t min_score = std::numeric_limits::max(); + int64_t min_score = kint64max; const int num_vars = num_variables_; for (ColIndex v{1}; v < num_vars; ++v) { if (variable_is_symbolic_[v]) continue; diff --git a/ortools/routing/fourier_solver_test.cc b/ortools/routing/fourier_solver_test.cc new file mode 100644 index 00000000000..e0c979274f7 --- /dev/null +++ b/ortools/routing/fourier_solver_test.cc @@ -0,0 +1,604 @@ +// 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/routing/fourier_solver.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" +#include "absl/strings/str_cat.h" +#include "absl/types/span.h" +#include "benchmark/benchmark.h" +#include "google/protobuf/duration.pb.h" +#include "gtest/gtest.h" +#include "ortools/util/optional_boolean.pb.h" + +namespace operations_research::routing { + +// Implement equality, comparison and hashability for local Arc classes. + +// All Arc local classes in tests below are `TupleConvertible`. +template +concept TupleConvertible = requires(const T& a) { + { a.AsTuple() }; +}; + +// Implements equality (and inequality) of `TupleConvertible` types. +template +bool operator==(const T& a, const T& b) + requires TupleConvertible +{ + return a.AsTuple() == b.AsTuple(); +} + +// Implements comparison of `TupleConvertible` types. +template +auto operator<=>(const T& a, const T& b) + requires TupleConvertible +{ + return a.AsTuple() <=> b.AsTuple(); +} + +// Implements hashing of `TupleConvertible` types. +template +H AbslHashValue(H h, const T& value) + requires TupleConvertible +{ + return H::combine(std::move(h), value.AsTuple()); +} + +TEST(MaxLinearExpressionEvaluatorTest, NoExpressions) { + MaxLinearExpressionEvaluator evaluator({}); + EXPECT_EQ(evaluator.Evaluate({}), -std::numeric_limits::infinity()); +} + +TEST(MaxLinearExpressionEvaluatorTest, NoVariables) { + MaxLinearExpressionEvaluator evaluator({{}, {}, {}}); + EXPECT_EQ(evaluator.Evaluate({}), 0.0); +} + +TEST(MaxLinearExpressionEvaluatorTest, OneVariable) { + MaxLinearExpressionEvaluator evaluator({{1.0}, {2.0}, {3.0}}); + EXPECT_EQ(evaluator.Evaluate({1.0}), 3.0); + EXPECT_EQ(evaluator.Evaluate({-1.0}), -1.0); +} + +TEST(MaxLinearExpressionEvaluatorTest, TwoVariables) { + MaxLinearExpressionEvaluator evaluator({{1.0, -1.0}, {5.0, 1.0}, {3.0, 3.0}}); + EXPECT_EQ(evaluator.Evaluate({0.0, -2.0}), 2.0); // Row 0 is the max. + EXPECT_EQ(evaluator.Evaluate({1.0, -1.0}), 4.0); // Row 1 is the max. + EXPECT_EQ(evaluator.Evaluate({1.0, 2.0}), 9.0); // Row 2 is the max. + + EXPECT_EQ(evaluator.Evaluate({1.0, -2.0}), 3.0); // Rows 0 and 1 are the max. + EXPECT_EQ(evaluator.Evaluate({1.0, 1.0}), 6.0); // Rows 1 and 2 are the max. +} + +// For many number of variables and expressions, set only one nonzero term, +// make sure that activating it with a particular value vector works. +TEST(MaxLinearExpressionEvaluatorTest, ManyVariablesManyExpressions) { + const int max_vars = 16; + const int max_rows = 32; + for (int num_vars = 1; num_vars <= max_vars; ++num_vars) { + for (int num_rows = 1; num_rows <= max_rows; ++num_rows) { + for (int argmax = 0; argmax < num_rows; ++argmax) { + std::vector> rows; + for (int r = 0; r < num_rows; ++r) { + std::vector row(num_vars, 0.0); + if (r == argmax) row[r % num_vars] = 5.0; + rows.push_back(std::move(row)); + } + MaxLinearExpressionEvaluator evaluator(rows); + std::vector values(num_vars, 0.0); + EXPECT_EQ(evaluator.Evaluate(values), 0.0); + values[argmax % num_vars] = 1.0; + EXPECT_EQ(evaluator.Evaluate(values), 5.0); + } + } + } +} + +void BM_MaxLinearExpressionEvaluator_Evaluate(benchmark::State& bench_state) { + const int num_variables = bench_state.range(0); + const int num_constraints = bench_state.range(1); + // Make constraints. + std::vector> rows; + for (int i = 0; i < num_constraints; ++i) { + std::vector row; + row.reserve(num_variables); + for (int j = 0; j < num_variables; ++j) { + row.push_back(i + 1); + } + rows.push_back(std::move(row)); + } + MaxLinearExpressionEvaluator evaluator(rows); + std::vector values(num_variables, 0.0); + absl::c_iota(values, 1); + int64_t num_items = 0; + for (auto _ : bench_state) { + benchmark::DoNotOptimize(evaluator.Evaluate(values)); + ++num_items; + } + bench_state.SetItemsProcessed(num_items); + bench_state.SetBytesProcessed(num_items * num_constraints * num_variables * + sizeof(double)); +} + +BENCHMARK(BM_MaxLinearExpressionEvaluator_Evaluate) + ->RangePair(1, 1 << 8, 1, 1 << 8); + +using FourierSolverTest = ::testing::TestWithParam; + +TEST_P(FourierSolverTest, NCubeTest) { + // With variables x_i in [0, 1], minimize objective -sum_i x_i. + const int num_variables = GetParam(); + FourierSolver solver; + for (int i = 0; i < num_variables; ++i) { + const FourierSolver::ColIndex xi = solver.AddVariable(0, 1); + solver.SetObjectiveCoefficient(xi, -1); + } + ASSERT_TRUE(solver.Solve()); + EXPECT_EQ(solver.EvaluateObjective(), -num_variables); +} + +INSTANTIATE_TEST_SUITE_P(FMESolverTest, FourierSolverTest, + ::testing::Values(1, 2, 3, 4, 5, 6, 7)); + +// FMESolver Tests. +TEST(FMESolverTest, ShortestPathFlowEncoding) { + // Shortest path on a graph. We set one {0, 1} variable per arc, there must be + // a flow of 1 from origin to destination, the cost is the sum of lengths of + // arcs set to 1. + struct Scenario { + int num_nodes; + struct Arc { + int tail; + int head; + double length; + + auto AsTuple() const { return std::make_tuple(tail, head, length); } + }; + std::vector arcs; + int source; + int destination; + bool feasible; + double objective; + }; + static_assert(TupleConvertible); + // Some scenarios are from "Network Flows", Ahuja, Magnanti, Orlin. + std::vector scenarios = { + {// Simple route 0 -> 1 -> 2 -> 3. + .num_nodes = 4, + .arcs = + { + {.tail = 0, .head = 1, .length = 4.0}, + {.tail = 1, .head = 2, .length = 3.0}, + {.tail = 2, .head = 3, .length = 5.0}, + }, + .source = 0, + .destination = 3, + .feasible = true, + .objective = 12.0}, + {// Infeasible route 0 -> 1 3 -> 2. + .num_nodes = 4, + .arcs = + { + {.tail = 0, .head = 1, .length = 4.0}, + {.tail = 3, .head = 2, .length = 5.0}, + }, + .source = 0, + .destination = 3, + .feasible = false, + .objective = 0.0}, + { + // Network Flows, figure 4.7. + .num_nodes = 7, // Node 0 is not used. + .arcs = + { + {.tail = 1, .head = 2, .length = 6.0}, + {.tail = 1, .head = 3, .length = 4.0}, + {.tail = 2, .head = 3, .length = 2.0}, + {.tail = 2, .head = 4, .length = 2.0}, + {.tail = 3, .head = 4, .length = 1.0}, + {.tail = 3, .head = 5, .length = 2.0}, + {.tail = 4, .head = 6, .length = 7.0}, + {.tail = 5, .head = 4, .length = 1.0}, + {.tail = 5, .head = 6, .length = 3.0}, + }, + .source = 1, + .destination = 6, + .feasible = true, + .objective = 9.0 // 1 -> 3 -> 5 -> 6. + }, + { + // Network Flows, figure 4.15 left. + .num_nodes = 7, // Node 0 is not used. + .arcs = + { + {.tail = 1, .head = 2, .length = 2.0}, + {.tail = 1, .head = 3, .length = 8.0}, + {.tail = 2, .head = 3, .length = 5.0}, + {.tail = 2, .head = 4, .length = 3.0}, + {.tail = 3, .head = 2, .length = 6.0}, + {.tail = 3, .head = 5, .length = 0.0}, + {.tail = 4, .head = 3, .length = 1.0}, + {.tail = 4, .head = 5, .length = 7.0}, + {.tail = 4, .head = 6, .length = 6.0}, + {.tail = 5, .head = 4, .length = 4.0}, + {.tail = 6, .head = 5, .length = 2.0}, + }, + .source = 1, + .destination = 6, + .feasible = true, + .objective = 11.0 // 1 -> 2 -> 4 -> 6. + }, + { + // Network Flows, figure 4.15 right. + .num_nodes = 13, // Node 0 is not used. + .arcs = + { + {.tail = 1, .head = 2, .length = 5.0}, + {.tail = 1, .head = 4, .length = 10.0}, + {.tail = 2, .head = 3, .length = 7.0}, + {.tail = 2, .head = 5, .length = 1.0}, + {.tail = 3, .head = 6, .length = 4.0}, + {.tail = 4, .head = 5, .length = 3.0}, + {.tail = 4, .head = 11, .length = 11.0}, + {.tail = 5, .head = 6, .length = 3.0}, + {.tail = 5, .head = 8, .length = 7.0}, + {.tail = 6, .head = 9, .length = 5.0}, + {.tail = 7, .head = 8, .length = 2.0}, + {.tail = 7, .head = 10, .length = 9.0}, + {.tail = 8, .head = 9, .length = 0.0}, + {.tail = 8, .head = 11, .length = 1.0}, + {.tail = 9, .head = 12, .length = 12.0}, + {.tail = 10, .head = 11, .length = 2.0}, + {.tail = 11, .head = 12, .length = 4.0}, + }, + .source = 1, + .destination = 12, + .feasible = true, + .objective = 18.0 // 1 -> 2 -> 5 -> 8 -> 11. + }, + }; + + for (int s = 0; s < 4; ++s) { + SCOPED_TRACE(absl::StrCat("Scenario ", s)); + const Scenario& scenario = scenarios[s]; + FourierSolver solver; + // Node linear expressions: at each node, outflow - inflow = supply. + // Foreach node, we keep outflow - inflow as a vector of CoefVars. + using CoefVar = FourierSolver::CoefficientVariable; + std::vector> linexpr_of_node(scenario.num_nodes); + using ColIndex = FourierSolver::ColIndex; + for (const Scenario::Arc& arc : scenario.arcs) { + const ColIndex var = solver.AddVariable(0, 1); + linexpr_of_node[arc.tail].push_back({1.0, var}); + linexpr_of_node[arc.head].push_back({-1.0, var}); + solver.SetObjectiveCoefficient(var, arc.length); + } + for (int n = 0; n < scenario.num_nodes; ++n) { + double supply = 0.0; + if (n == scenario.source) supply = 1.0; + if (n == scenario.destination) supply = -1.0; + solver.AddConstraint(supply, supply, linexpr_of_node[n]); + } + EXPECT_EQ(solver.Solve(), scenario.feasible); + if (scenario.feasible) { + EXPECT_EQ(solver.EvaluateObjective(), scenario.objective); + } + } +} + +TEST(FMESolverTest, ShortestPaths) { + // Shortest path on a graph. We set one variable per node, the distance from + // the origin, and look for the shortest path to destination. + struct Scenario { + int num_nodes; + struct Arc { + int tail; + int head; + double length; + + auto AsTuple() const { return std::make_tuple(tail, head, length); } + }; + std::vector arcs; + int source; + int destination; + bool feasible; + double objective; + }; + static_assert(TupleConvertible); + std::vector scenarios = { + {// Simple route 0 -> 1 -> 2 -> 3. + .num_nodes = 4, + .arcs = + { + {.tail = 0, .head = 1, .length = 4.0}, + {.tail = 1, .head = 2, .length = 3.0}, + {.tail = 2, .head = 3, .length = 5.0}, + }, + .source = 0, + .destination = 3, + .feasible = true, + .objective = 12.0}, + }; + + constexpr double kInfinity = std::numeric_limits::infinity(); + + for (int s = 0; s < scenarios.size(); ++s) { + SCOPED_TRACE(absl::StrCat("Scenario ", s)); + const Scenario& scenario = scenarios[s]; + FourierSolver solver; + // Node variables. + using ColIndex = FourierSolver::ColIndex; + std::vector distance; + for (int n = 0; n < scenario.num_nodes; ++n) { + distance.push_back(solver.AddVariable(0.0, 1e6)); + } + // Arc constraints: distance[head] <= distance[tail] + length(tail -> head). + for (const Scenario::Arc& arc : scenario.arcs) { + solver.AddConstraint( + -kInfinity, arc.length, + {{1.0, distance[arc.head]}, {-1.0, distance[arc.tail]}}); + } + // Maximize distance(destination) - distance(source), so minimize the + // opposite. + solver.SetObjectiveCoefficient(distance[scenario.destination], -1.0); + solver.SetObjectiveCoefficient(distance[scenario.source], 1.0); + EXPECT_EQ(solver.Solve(), scenario.feasible); + if (scenario.feasible) { + EXPECT_EQ(solver.EvaluateObjective(), -scenario.objective); + } + } +} + +// TODO(b/492476073): Add more tests. For instance Shortest path on DAG, +// Shortest path on general graph, Shortest path with negative loop, Min flow, +// Knapsack, min cost assignment, general LP. + +// Shortest span on route. +TEST(FMESolverTest, RoutingTest) { + FourierSolver solver; + using ColIndex = FourierSolver::ColIndex; + // Start + duration == end. + const ColIndex start = solver.AddVariable(0, 3); + const ColIndex duration = solver.AddVariable(3, 10); + const ColIndex end = solver.AddVariable(7, 10); + solver.AddConstraint(0, 0, {{1, start}, {1, duration}, {-1, end}}); + // Duration cost. + solver.SetObjectiveCoefficient(duration, 3); + + auto add_soft_max = [&](double coef, ColIndex var, double soft_max, + double cost) { + constexpr double kInfinity = std::numeric_limits::infinity(); + ColIndex violation = solver.AddVariable(0, kInfinity); + solver.AddConstraint(-kInfinity, soft_max, {{coef, var}, {-1, violation}}); + solver.SetObjectiveCoefficient(violation, cost); + }; + add_soft_max(1, duration, 5, 7); // duration soft max. + add_soft_max(-1, start, -1, 11); // start soft min is 1. + add_soft_max(1, start, 2, 13); // start soft max is 2. + add_soft_max(-1, end, -8, 17); // end soft min is 8. + add_soft_max(1, end, 9, 19); // end soft max is 9. + + EXPECT_TRUE(solver.Solve()); + EXPECT_EQ(solver.EvaluateObjective(), (8 - 2) * 3 + (8 - 2 - 5) * 7); +} + +// Shortest span on route. +TEST(FMESolverTest, RoutingTest2) { + FourierSolver solver; + using ColIndex = FourierSolver::ColIndex; + // Start + duration == end. + const ColIndex start = solver.AddVariable(0, 3); + const ColIndex end = solver.AddVariable(7, 10); + solver.AddConstraint(3, 10, {{1, end}, {-1, start}}); + // Duration cost. + solver.SetObjectiveCoefficient(end, 3); + solver.SetObjectiveCoefficient(start, -3); + + constexpr double kInfinity = std::numeric_limits::infinity(); + { + // Soft duration max. + ColIndex violation = solver.AddVariable(0, kInfinity); + solver.AddConstraint(-kInfinity, 5, + {{1, end}, {-1, start}, {-1, violation}}); + solver.SetObjectiveCoefficient(violation, 7); + } + + auto add_soft_max = [&](double coef, ColIndex var, double soft_max, + double cost) { + ColIndex violation = solver.AddVariable(0, kInfinity); + solver.AddConstraint(-kInfinity, soft_max, {{coef, var}, {-1, violation}}); + solver.SetObjectiveCoefficient(violation, cost); + }; + add_soft_max(-1, start, -1, 11); // start soft min is 1. + add_soft_max(1, start, 2, 13); // start soft max is 2. + add_soft_max(-1, end, -8, 17); // end soft min is 8. + add_soft_max(1, end, 9, 19); // end soft max is 9. + + EXPECT_TRUE(solver.Solve()); + EXPECT_EQ(solver.EvaluateObjective(), (8 - 2) * 3 + (8 - 2 - 5) * 7); +} + +TEST(FMESolverTest, RoutingTestSymbolic) { + FourierSolver solver; + using ColIndex = FourierSolver::ColIndex; + constexpr double kInfinity = FourierSolver::kInfinity; + // Start + duration == end. + const ColIndex start = solver.AddVariable(0, 3); + const ColIndex end = solver.AddVariable(7, 10); + const ColIndex duration = solver.AddVariable(0, 10); + solver.AddConstraint(0, 0, {{1, start}, {1, duration}, {-1, end}}); + + // Duration cost. + solver.SetObjectiveCoefficient(duration, 3); + + const ColIndex duration_lb = solver.AddVariable(0, 10, /*is_symbolic=*/true); + solver.AddConstraint(0, kInfinity, {{1, duration}, {-1, duration_lb}}); + { + ColIndex violation = solver.AddVariable(0, kInfinity); + solver.AddConstraint(-kInfinity, 5, {{1, duration}, {-1, violation}}); + solver.SetObjectiveCoefficient(violation, 7); + }; + + EXPECT_TRUE(solver.Solve()); + { + solver.SetSymbolicVariableValue(duration_lb, 3); + EXPECT_EQ(solver.EvaluateObjective(), 12); + } + { + solver.SetSymbolicVariableValue(duration_lb, 4); + EXPECT_EQ(solver.EvaluateObjective(), 12); + } + { + solver.SetSymbolicVariableValue(duration_lb, 5); + EXPECT_EQ(solver.EvaluateObjective(), 15); + } + { + solver.SetSymbolicVariableValue(duration_lb, 6); + EXPECT_EQ(solver.EvaluateObjective(), 18 + 7); + } +} + +TEST(FMESolverTest, RoutingTestSymbolic2) { + FourierSolver solver; + using ColIndex = FourierSolver::ColIndex; + constexpr double kInfinity = std::numeric_limits::infinity(); + // Start + duration == end. + // const ColIndex start = solver.AddVariable(0, 3); + // const ColIndex end = solver.AddVariable(7, 10); + // const ColIndex duration = solver.AddVariable(-kInfinity, 10); + const ColIndex start = solver.AddVariable(-kInfinity, kInfinity); + const ColIndex end = solver.AddVariable(-kInfinity, kInfinity); + const ColIndex duration = solver.AddVariable(-kInfinity, kInfinity); + solver.AddConstraint(0, 0, {{1, start}, {1, duration}, {-1, end}}); + // Duration cost. + solver.SetObjectiveCoefficient(duration, 3); + + auto add_soft_max = [&](double coef, ColIndex var, double soft_max, + double cost) { + ColIndex violation = solver.AddVariable(0, kInfinity); + solver.AddConstraint(-kInfinity, soft_max, {{coef, var}, {-1, violation}}); + solver.SetObjectiveCoefficient(violation, cost); + }; + add_soft_max(1, duration, 5, 7); // duration soft max. + add_soft_max(-1, start, -1, 9); // start soft min is 1. + add_soft_max(1, start, 2, 11); // start soft max is 2. + add_soft_max(-1, end, -8, 13); // end soft min is 8. + add_soft_max(1, end, 9, 17); // end soft max is 9. + + const ColIndex duration_min = solver.AddVariable(0, 10, true); + solver.AddConstraint(0, kInfinity, {{1, duration}, {-1, duration_min}}); + const ColIndex start_min = solver.AddVariable(0, 3, true); + solver.AddConstraint(0, kInfinity, {{1, start}, {-1, start_min}}); + const ColIndex end_min = solver.AddVariable(7, 10, true); + solver.AddConstraint(0, kInfinity, {{1, end}, {-1, end_min}}); + const ColIndex start_max = solver.AddVariable(0, 3, true); + solver.AddConstraint(0, kInfinity, {{1, start_max}, {-1, start}}); + const ColIndex end_max = solver.AddVariable(7, 10, true); + solver.AddConstraint(0, kInfinity, {{1, end_max}, {-1, end}}); + + EXPECT_TRUE(solver.Solve()); + { + solver.SetSymbolicVariableValue(duration_min, 3); + solver.SetSymbolicVariableValue(start_min, 0); + solver.SetSymbolicVariableValue(start_max, 3); + solver.SetSymbolicVariableValue(end_min, 7); + solver.SetSymbolicVariableValue(end_max, 10); + EXPECT_EQ(solver.EvaluateObjective(), (8 - 2) * 3 + (8 - 2 - 5) * 7); + } +} + +// Assignment +TEST(FMESolverTest, AssignmentTests) { + // Encode assignment problems: + // - sum_w x_jw = 1 for all j (all jobs must have one worker) + // - sum_j x_jw <= 1 for all w (a worker is assigned at most one job) + // - x_jw in {0, 1} + struct Scenario { + int num_jobs; + int num_workers; + struct Arc { + int job; + int worker; + double cost; + + auto AsTuple() const { return std::make_tuple(job, worker, cost); } + }; + std::vector arcs; + bool feasible; + double objective; + }; + static_assert(TupleConvertible); + std::vector scenarios = { + {.num_jobs = 3, + .num_workers = 4, + .arcs = {{.job = 0, .worker = 0, .cost = 1.0}, + {.job = 0, .worker = 1, .cost = 1.0}, + {.job = 0, .worker = 2, .cost = 1.0}, + {.job = 1, .worker = 1, .cost = 1.0}, + {.job = 1, .worker = 2, .cost = 1.0}, + {.job = 2, .worker = 1, .cost = 1.0}, + {.job = 2, .worker = 2, .cost = 1.0}, + {.job = 2, .worker = 3, .cost = 1.0}}, + .feasible = true, + .objective = 3.0}, + {.num_jobs = 3, + .num_workers = 4, + .arcs = {{.job = 0, .worker = 1, .cost = 1.0}, + {.job = 0, .worker = 2, .cost = 1.0}, + {.job = 1, .worker = 1, .cost = 1.0}, + {.job = 1, .worker = 2, .cost = 1.0}, + {.job = 2, .worker = 1, .cost = 1.0}, + {.job = 2, .worker = 2, .cost = 1.0}}, + .feasible = false, + .objective = -1.0}, + }; + + for (int s = 0; s < scenarios.size(); ++s) { + SCOPED_TRACE(absl::StrCat("Scenario ", s)); + const Scenario& scenario = scenarios[s]; + FourierSolver solver; + using ColIndex = FourierSolver::ColIndex; + absl::flat_hash_map variable_of_arc; + using CoefVar = FourierSolver::CoefficientVariable; + absl::flat_hash_map> linexpr_of_worker; + absl::flat_hash_map> linexpr_of_job; + for (const Scenario::Arc& arc : scenario.arcs) { + const ColIndex var = solver.AddVariable(0, 1); + solver.SetObjectiveCoefficient(var, arc.cost); + variable_of_arc[arc] = var; + linexpr_of_job[arc.job].push_back({1, var}); + linexpr_of_worker[arc.worker].push_back({1, var}); + } + for (int j = 0; j < scenario.num_jobs; ++j) { + solver.AddConstraint(1, 1, linexpr_of_job[j]); + } + for (int w = 0; w < scenario.num_workers; ++w) { + solver.AddConstraint(0, 1, linexpr_of_worker[w]); + } + EXPECT_EQ(solver.Solve(), scenario.feasible); + if (scenario.feasible) { + EXPECT_EQ(solver.EvaluateObjective(), scenario.objective); + } + } +} + +} // namespace operations_research::routing diff --git a/ortools/routing/ils.cc b/ortools/routing/ils.cc index fb5c404c87d..fdad27f18e6 100644 --- a/ortools/routing/ils.cc +++ b/ortools/routing/ils.cc @@ -31,6 +31,7 @@ #include "absl/time/time.h" #include "absl/types/span.h" #include "google/protobuf/repeated_ptr_field.h" +#include "ortools/base/types.h" #include "ortools/constraint_solver/assignment.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/routing/ils.pb.h" @@ -294,8 +295,7 @@ class GreedyDescentAcceptanceCriterion : public NeighborAcceptanceCriterion { explicit GreedyDescentAcceptanceCriterion(int late_acceptance_window) : late_acceptance_window_(late_acceptance_window), history_index_(0) { if (late_acceptance_window_ > 0) { - history_.resize(late_acceptance_window_, - std::numeric_limits::max()); + history_.resize(late_acceptance_window_, kint64max); } } @@ -457,13 +457,28 @@ class AllNodesPerformedAcceptanceCriterion [[maybe_unused]] const Assignment* reference) override { for (DisjunctionIndex d(0); d < model_.GetNumberOfDisjunctions(); ++d) { // This solution avoids counting non-fixed variables as inactive. - int num_possible_actives = model_.GetDisjunctionNodeIndices(d).size(); + int max_num_actives = model_.GetDisjunctionNodeIndices(d).size(); + int min_num_actives = 0; for (const int64_t node : model_.GetDisjunctionNodeIndices(d)) { if (candidate->Value(model_.NextVar(node)) == node) { - --num_possible_actives; + --max_num_actives; + } else { + ++min_num_actives; } } - if (num_possible_actives < model_.GetDisjunctionMaxCardinality(d)) { + // Candidate must be feasible: candidate cardinality interval must + // intersect disjunction cardinality interval. + if (min_num_actives > model_.GetDisjunctionMaxCardinality(d) || + max_num_actives < model_.GetDisjunctionMinCardinality(d)) { + return false; + } + // Candidate must have no disjunction penalty. + if (model_.GetDisjunctionSoftMinPenalty(d) > 0 && + max_num_actives < model_.GetDisjunctionSoftMinCardinality(d)) { + return false; + } + if (model_.GetDisjunctionSoftMaxPenalty(d) > 0 && + min_num_actives > model_.GetDisjunctionSoftMaxCardinality(d)) { return false; } } @@ -553,7 +568,7 @@ class AbsencesBasedAcceptanceCriterion : public NeighborAcceptanceCriterion { if (!remove_route_with_lowest_absences_) return; int candidate_route = -1; - int min_sum_absences = std::numeric_limits::max(); + int min_sum_absences = kint32max; for (int route = 0; route < model_.vehicles(); ++route) { if (model_.Next(*reference, model_.Start(route)) == model_.End(route)) diff --git a/ortools/routing/ils.proto b/ortools/routing/ils.proto index 6292c1bb446..5965d7a144d 100644 --- a/ortools/routing/ils.proto +++ b/ortools/routing/ils.proto @@ -19,10 +19,9 @@ // to an acceptance criterion. // The best found solution is eventually returned. -syntax = "proto3"; +edition = "2024"; option java_package = "com.google.ortools.routing"; -option java_multiple_files = true; option csharp_namespace = "Google.OrTools.Routing"; import "ortools/routing/enums.proto"; @@ -33,7 +32,7 @@ package operations_research.routing; // Ruin strategy that removes a number of spatially close routes. message SpatiallyCloseRoutesRuinStrategy { // Number of spatially close routes ruined at each ruin application. - optional uint32 num_ruined_routes = 3; + uint32 num_ruined_routes = 3 [default = 2]; } // Ruin strategy that removes a number of nodes by performing a random walk @@ -48,7 +47,7 @@ message RandomWalkRuinStrategy { // i.e., the removal of a pickup (respectively delivery) causes the removal of // the associated delivery (respectively pickup) and it counts as a single // removal. - optional uint32 num_removed_visits = 7; + uint32 num_removed_visits = 7 [default = 10]; } // Adaptive version of the RandomWalkRuinStrategy where the walk length is @@ -86,9 +85,9 @@ message RandomWalkRuinStrategy { // Daniele Vigo, Transportation Science 2021. message AdaptiveRandomWalkRuinStrategy { // Factor used to compute the strengthening threshold. - optional double strengthening_factor = 1; + double strengthening_factor = 1 [default = 0.375]; // Factor used to compute the weakening threshold. - optional double weakening_factor = 2; + double weakening_factor = 2 [default = 0.85]; } // Ruin strategy based on the "Slack Induction by String Removals for Vehicle @@ -164,16 +163,16 @@ message AdaptiveRandomWalkRuinStrategy { message SISRRuinStrategy { // Maximum number of removed visits per sequence. The parameter name in the // paper is L^{max} and the suggested value is 10. - optional uint32 max_removed_sequence_size = 1; + uint32 max_removed_sequence_size = 1 [default = 10]; // Number of visits that are removed on average. The parameter name in the // paper is \bar{c} and the suggested value is 10. - optional uint32 avg_num_removed_visits = 2; + uint32 avg_num_removed_visits = 2 [default = 10]; // Value in [0, 1] ruling the number of preserved nodes in the split // sequence removal. The parameter name in the paper is \alpha and the // suggested value is 0.01. - optional double bypass_factor = 3; + double bypass_factor = 3 [default = 0.01]; } // Ruin strategies, used in perturbation based on ruin and recreate approaches. @@ -197,10 +196,11 @@ message RecreateParameters { // Strategy defining how a solution is recreated. message RecreateStrategy { - optional FirstSolutionStrategy.Value heuristic = 1; + FirstSolutionStrategy.Value heuristic = 1 + [default = LOCAL_CHEAPEST_INSERTION]; // The selected parameters should match the chosen recreate heuristic. // If not set, the default parameters from the RoutingModel are used. - optional RecreateParameters parameters = 2; + RecreateParameters parameters = 2; } // The ruin composition strategies specifies how ruin are selected at every ILS @@ -229,7 +229,8 @@ message RuinRecreateParameters { // The composition strategy to use when combining the given 'ruin_strategies'. // Has no effect when ruin_strategies is composed of a single strategy. - RuinCompositionStrategy.Value ruin_composition_strategy = 2; + RuinCompositionStrategy.Value ruin_composition_strategy = 2 + [default = RUN_ALL_SEQUENTIALLY]; // Strategy defining how a reference solution is recreated. RecreateStrategy recreate_strategy = 3; @@ -250,9 +251,9 @@ message RuinRecreateParameters { // // Neighbors ratio, and minimum and maximum number of non start/end neighbor // nodes for the identification of spatially close routes. - optional double route_selection_neighbors_ratio = 4; - optional uint32 route_selection_min_neighbors = 5; - optional uint32 route_selection_max_neighbors = 6; + double route_selection_neighbors_ratio = 4 [default = 1.0]; + uint32 route_selection_min_neighbors = 5 [default = 10]; + uint32 route_selection_max_neighbors = 6 [default = 100]; } // Defines how a reference solution is perturbed. @@ -311,7 +312,7 @@ message CoolingScheduleStrategy { message GreedyDescentAcceptanceStrategy { // Specifies how many solutions in the past to consider for the late // acceptance. - optional uint32 late_acceptance_window = 1; + uint32 late_acceptance_window = 1 [default = 0]; } // Acceptance strategy in which solutions are accepted with a probability that @@ -319,13 +320,14 @@ message GreedyDescentAcceptanceStrategy { message SimulatedAnnealingAcceptanceStrategy { // Determines the speed at which the temperature changes from initial to // final. - CoolingScheduleStrategy.Value cooling_schedule_strategy = 1; + CoolingScheduleStrategy.Value cooling_schedule_strategy = 1 + [default = EXPONENTIAL]; // The initial temperature. See CoolingScheduleStrategy for its usage. - optional double initial_temperature = 2; + double initial_temperature = 2 [default = 100.0]; // The final temperature. See CoolingScheduleStrategy for its usage. - optional double final_temperature = 3; + double final_temperature = 3 [default = 0.01]; // Automatically define the value for the temperatures as follows. // First, a reference temperature t is defined as @@ -337,7 +339,7 @@ message SimulatedAnnealingAcceptanceStrategy { // The initial and final temperatures are then defined as // - initial_temperature: 0.1 * t // - final_temperature: 0.001 * t - optional bool automatic_temperatures = 4; + bool automatic_temperatures = 4 [default = false]; } // Acceptance strategy in which a solution is accepted only if all nodes @@ -370,7 +372,7 @@ message MoreNodesPerformedAcceptanceStrategy {} message AbsencesBasedAcceptanceStrategy { // If true, when a new best solution is found, the route with the lowest sum // of absences is removed from the reference solution. - optional bool remove_route_with_lowest_absences = 1; + bool remove_route_with_lowest_absences = 1 [default = false]; } // Determines when a candidate solution replaces another one. @@ -402,14 +404,15 @@ message IteratedLocalSearchParameters { // Determines how a reference solution S is perturbed to obtain a neighbor // solution S'. - PerturbationStrategy.Value perturbation_strategy = 1; + PerturbationStrategy.Value perturbation_strategy = 1 + [default = RUIN_AND_RECREATE]; // Parameters to customize a ruin and recreate perturbation. RuinRecreateParameters ruin_recreate_parameters = 2; // Determines whether solution S', obtained from the perturbation, should be // optimized with a local search application. - optional bool improve_perturbed_solution = 3; + bool improve_perturbed_solution = 3 [default = true]; // Determines when the neighbor solution S', possibly improved if // `improve_perturbed_solution` is true, replaces the reference solution S. diff --git a/ortools/routing/java/routing.swig b/ortools/routing/java/routing.swig index 22f6a13ad93..fd71836f6dd 100644 --- a/ortools/routing/java/routing.swig +++ b/ortools/routing/java/routing.swig @@ -292,6 +292,13 @@ import java.util.function.LongUnaryOperator; %rename (getDimensionOrDie) Model::GetDimensionOrDie; %rename (getDisjunctionIndices) Model::GetDisjunctionIndices; %rename (getDisjunctionMaxCardinality) Model::GetDisjunctionMaxCardinality; +%rename (getDisjunctionMinCardinality) Model::GetDisjunctionMinCardinality; +%rename (getDisjunctionSoftMaxCardinality) Model::GetDisjunctionSoftMaxCardinality; +%rename (getDisjunctionSoftMaxPenalty) Model::GetDisjunctionSoftMaxPenalty; +%rename (getDisjunctionSoftMaxPenaltyCostBehavior) Model::GetDisjunctionSoftMaxPenaltyCostBehavior; +%rename (getDisjunctionSoftMinCardinality) Model::GetDisjunctionSoftMinCardinality; +%rename (getDisjunctionSoftMinPenalty) Model::GetDisjunctionSoftMinPenalty; +%rename (getDisjunctionSoftMinPenaltyCostBehavior) Model::GetDisjunctionSoftMinPenaltyCostBehavior; %rename (getDisjunctionPenalty) Model::GetDisjunctionPenalty; %rename (getFixedCostOfVehicle) Model::GetFixedCostOfVehicle; %rename (getHomogeneousCost) Model::GetHomogeneousCost; @@ -321,6 +328,7 @@ import java.util.function.LongUnaryOperator; %rename (isStart) Model::IsStart; %rename (isVehicleAllowedForIndex) Model::IsVehicleAllowedForIndex; %rename (isVehicleUsed) Model::IsVehicleUsed; +%rename (makeDisjunction) Model::MakeDisjunction; %rename (makeGuidedSlackFinalizer) Model::MakeGuidedSlackFinalizer; %rename (makeSelfDependentDimensionFinalizer) Model::MakeSelfDependentDimensionFinalizer; %rename (mutablePreAssignment) Model::MutablePreAssignment; @@ -345,6 +353,10 @@ import java.util.function.LongUnaryOperator; %rename (setArcCostEvaluatorOfAllVehicles) Model::SetArcCostEvaluatorOfAllVehicles; %rename (setArcCostEvaluatorOfVehicle) Model::SetArcCostEvaluatorOfVehicle; %rename (setAssignmentFromOtherModelAssignment) Model::SetAssignmentFromOtherModelAssignment; +%rename (setDisjunctionHardMaximum) Model::SetDisjunctionHardMaximum; +%rename (setDisjunctionHardMinimum) Model::SetDisjunctionHardMinimum; +%rename (setDisjunctionSoftMaximum) Model::SetDisjunctionSoftMaximum; +%rename (setDisjunctionSoftMinimum) Model::SetDisjunctionSoftMinimum; %rename (setFirstSolutionEvaluator) Model::SetFirstSolutionEvaluator; %rename (setFixedCostOfAllVehicles) Model::SetFixedCostOfAllVehicles; %rename (setFixedCostOfVehicle) Model::SetFixedCostOfVehicle; diff --git a/ortools/routing/lp_scheduling.h b/ortools/routing/lp_scheduling.h index 7a562914e95..5fa9bfdff05 100644 --- a/ortools/routing/lp_scheduling.h +++ b/ortools/routing/lp_scheduling.h @@ -34,6 +34,7 @@ #include "absl/time/time.h" #include "absl/types/span.h" #include "ortools/base/mathutil.h" +#include "ortools/base/types.h" #include "ortools/glop/lp_solver.h" #include "ortools/glop/parameters.pb.h" #include "ortools/lp_data/lp_data.h" @@ -78,9 +79,7 @@ class CumulBoundsPropagator { int64_t CumulMax(int index) const { const int64_t negated_upper_bound = propagated_bounds_[NegativeNode(index)]; - return negated_upper_bound == std::numeric_limits::min() - ? std::numeric_limits::max() - : -negated_upper_bound; + return negated_upper_bound == kint64min ? kint64max : -negated_upper_bound; } const Dimension& dimension() const { return dimension_; } @@ -244,26 +243,24 @@ class LinearSolverWrapper { int64_t lower_bound, int64_t upper_bound, absl::Span> weighted_variables) { const int reification_ct = AddLinearConstraint(1, 1, {}); - if (std::numeric_limits::min() < lower_bound) { + if (kint64min < lower_bound) { const int under_lower_bound = AddVariable(0, 1); #ifndef NDEBUG SetVariableName(under_lower_bound, "under_lower_bound"); #endif SetCoefficient(reification_ct, under_lower_bound, 1); const int under_lower_bound_ct = - AddLinearConstraint(std::numeric_limits::min(), - lower_bound - 1, weighted_variables); + AddLinearConstraint(kint64min, lower_bound - 1, weighted_variables); SetEnforcementLiteral(under_lower_bound_ct, under_lower_bound); } - if (upper_bound < std::numeric_limits::max()) { + if (upper_bound < kint64max) { const int above_upper_bound = AddVariable(0, 1); #ifndef NDEBUG SetVariableName(above_upper_bound, "above_upper_bound"); #endif SetCoefficient(reification_ct, above_upper_bound, 1); - const int above_upper_bound_ct = AddLinearConstraint( - upper_bound + 1, std::numeric_limits::max(), - weighted_variables); + const int above_upper_bound_ct = + AddLinearConstraint(upper_bound + 1, kint64max, weighted_variables); SetEnforcementLiteral(above_upper_bound_ct, above_upper_bound); } const int within_bounds = AddVariable(0, 1); @@ -334,7 +331,7 @@ class GlopWrapper : public LinearSolverWrapper { const double upper_bound = linear_program_.variable_upper_bounds()[glop::ColIndex(index)]; DCHECK_GE(upper_bound, 0); - return upper_bound == glop::kInfinity ? std::numeric_limits::max() + return upper_bound == glop::kInfinity ? kint64max : static_cast(upper_bound); } void SetObjectiveCoefficient(int index, double coefficient) override { @@ -354,11 +351,8 @@ class GlopWrapper : public LinearSolverWrapper { int CreateNewConstraint(int64_t lower_bound, int64_t upper_bound) override { const glop::RowIndex ct = linear_program_.CreateNewConstraint(); linear_program_.SetConstraintBounds( - ct, - (lower_bound == std::numeric_limits::min()) ? -glop::kInfinity - : lower_bound, - (upper_bound == std::numeric_limits::max()) ? glop::kInfinity - : upper_bound); + ct, (lower_bound == kint64min) ? -glop::kInfinity : lower_bound, + (upper_bound == kint64max) ? glop::kInfinity : upper_bound); return ct.value(); } void SetCoefficient(int ct, int index, double coefficient) override { @@ -442,9 +436,8 @@ class GlopWrapper : public LinearSolverWrapper { } int64_t GetVariableValue(int index) const override { const double value_double = GetValueDouble(glop::ColIndex(index)); - return (value_double >= std::numeric_limits::max()) - ? std::numeric_limits::max() - : MathUtil::Round(value_double); + return (value_double >= kint64max) ? kint64max + : MathUtil::Round(value_double); } bool SolutionIsInteger() const override { return linear_program_.SolutionIsInteger(lp_solver_.variable_values(), @@ -588,8 +581,7 @@ class CPSatWrapper : public LinearSolverWrapper { for (int i = 0; i < objective.vars_size(); ++i) { activity += response_.solution(objective.vars(i)) * objective.coeffs(i); } - const int ct = - CreateNewConstraint(std::numeric_limits::min(), activity); + const int ct = CreateNewConstraint(kint64min, activity); for (int i = 0; i < objective.vars_size(); ++i) { SetCoefficient(ct, objective.vars(i), objective.coeffs(i)); } @@ -694,6 +686,9 @@ class CPSatWrapper : public LinearSolverWrapper { // Prints an understandable view of the model std::string PrintModel() const override; + // For testing. + sat::SatParameters* MutableParameters() { return ¶meters_; } + private: sat::CpModelProto model_; sat::CpSolverResponse response_; diff --git a/ortools/routing/neighborhoods.cc b/ortools/routing/neighborhoods.cc index 19bca40a881..13a9a54cb74 100644 --- a/ortools/routing/neighborhoods.cc +++ b/ortools/routing/neighborhoods.cc @@ -19,11 +19,14 @@ #include #include +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "absl/types/span.h" #include "ortools/base/types.h" #include "ortools/constraint_solver/assignment.h" #include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/constraint_solver/local_search.h" #include "ortools/routing/types.h" #include "ortools/routing/utils.h" #include "ortools/util/saturated_arithmetic.h" @@ -385,9 +388,11 @@ MakePairActiveOperator::MakePairActiveOperator( const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs) - : PathOperator(vars, secondary_vars, 2, false, true, - std::move(start_empty_path_class), nullptr, - nullptr), + : PathOperator( + vars, secondary_vars, /*number_of_base_nodes=*/2, + /*skip_locally_optimal_paths=*/false, + /*accept_path_end_base=*/true, std::move(start_empty_path_class), + nullptr, nullptr), inactive_pair_(0), inactive_pair_first_index_(0), inactive_pair_second_index_(0), @@ -406,9 +411,15 @@ bool MakePairActiveOperator::MakeOneNeighbor() { inactive_pair_first_index_ = 0; ++inactive_pair_second_index_; } else { + const int last_inactive_pair = inactive_pair_; inactive_pair_ = FindNextInactivePair(inactive_pair_ + 1); inactive_pair_first_index_ = 0; inactive_pair_second_index_ = 0; + if (inactive_pair_ >= pairs_.size() && last_start_node_ != -1) { + num_active_alternatives_of_pairs_of_start_node_[last_start_node_] + [last_inactive_pair]--; + } + last_start_node_ = -1; } } return false; @@ -417,6 +428,19 @@ bool MakePairActiveOperator::MakeOneNeighbor() { template bool MakePairActiveOperator::MakeNeighbor() { DCHECK_EQ(this->StartNode(0), this->StartNode(1)); + if (num_active_alternatives_of_pairs_of_start_node_[this->StartNode(0)] + [inactive_pair_] == 0) { + // Jump to the next destination path. + this->SetNextBaseToIncrement(-1); + return false; + } + if (last_start_node_ != this->StartNode(0)) { + if (last_start_node_ != -1) { + num_active_alternatives_of_pairs_of_start_node_[last_start_node_] + [inactive_pair_]--; + } + last_start_node_ = this->StartNode(0); + } // Inserting the second node of the pair before the first one which ensures // that the only solutions where both nodes are next to each other have the // first node before the second (the move is not symmetric and doing it this @@ -424,10 +448,17 @@ bool MakePairActiveOperator::MakeNeighbor() { // pair is not violated). const auto& [pickup_alternatives, delivery_alternatives] = pairs_[inactive_pair_]; - return this->MakeActive(delivery_alternatives[inactive_pair_second_index_], - this->BaseNode(1)) && - this->MakeActive(pickup_alternatives[inactive_pair_first_index_], - this->BaseNode(0)); + const int64_t pickup = pickup_alternatives[inactive_pair_first_index_]; + const int64_t delivery = delivery_alternatives[inactive_pair_second_index_]; + const int64_t destination_path = this->Path(this->BaseNode(0)); + if (!this->IsCompatibleWithPath(pickup, destination_path) || + !this->IsCompatibleWithPath(delivery, destination_path)) { + // Jump to the next destination path (both base nodes are on the same path). + this->SetNextBaseToIncrement(-1); + return false; + } + return this->MakeActive(delivery, this->BaseNode(1)) && + this->MakeActive(pickup, this->BaseNode(0)); } template @@ -447,6 +478,38 @@ void MakePairActiveOperator::OnNodeInitialization() { inactive_pair_ = FindNextInactivePair(0); inactive_pair_first_index_ = 0; inactive_pair_second_index_ = 0; + if (next_states_.empty()) { + next_states_.resize(this->number_of_nexts(), -1); + } + absl::flat_hash_set touched_path_starts; + for (int i = 0; i < this->number_of_nexts(); ++i) { + const int64_t next = this->Next(i); + if (next_states_[i] != next && next != i) { + int start = this->CurrentNodePathStart(i); + // Workaround for empty path start nodes. + // TODO(user): Maintain this properly in the parent class. + if (start == -1 && this->IsPathStart(i)) start = i; + touched_path_starts.insert(start); + } + next_states_[i] = next; + } + for (int start : touched_path_starts) { + ActivateAllPairsForPath(start); + } + last_start_node_ = -1; +} + +template +void MakePairActiveOperator::ActivateAllPairsForPath( + int start_node) { + auto& active_path_pair_count = + num_active_alternatives_of_pairs_of_start_node_[start_node]; + active_path_pair_count.assign(pairs_.size(), 0); + for (int i = 0; i < pairs_.size(); ++i) { + const auto& [pickup_alternatives, delivery_alternatives] = pairs_[i]; + active_path_pair_count[i] = + pickup_alternatives.size() * delivery_alternatives.size(); + } } template @@ -1461,7 +1524,8 @@ RelocateSubtrip::RelocateSubtrip( /*accept_path_end_base=*/false, std::move(start_empty_path_class), nullptr, // Incoming neighbors aren't supported as of 09/2024. std::move(get_outgoing_neighbors)), - pd_data_(this->number_of_nexts_, pairs) { + pd_data_(this->number_of_nexts_, pairs), + covered_nodes_(this->number_of_nexts_) { opened_pairs_set_.resize(pairs.size(), false); } @@ -1480,37 +1544,45 @@ bool RelocateSubtrip::RelocateSubTripFromPickup( if (this->Prev(chain_first_node) == insertion_node) return false; // Skip null move. - int num_opened_pairs = 0; - // Split chain into subtrip and rejected nodes. - rejected_nodes_ = {this->Prev(chain_first_node)}; - subtrip_nodes_ = {insertion_node}; - int current = chain_first_node; - do { - if (current == insertion_node) { - // opened_pairs_set_ must be all false when we leave this function. - opened_pairs_set_.assign(opened_pairs_set_.size(), false); - return false; - } - const int pair = pd_data_.GetPairOfNode(current); - if (pd_data_.IsDeliveryNode(current) && !opened_pairs_set_[pair]) { - rejected_nodes_.push_back(current); - } else { - subtrip_nodes_.push_back(current); - if (pd_data_.IsPickupNode(current)) { - ++num_opened_pairs; - opened_pairs_set_[pair] = true; - } else if (pd_data_.IsDeliveryNode(current)) { - --num_opened_pairs; - opened_pairs_set_[pair] = false; + if (chain_first_node == reference_node_) { + subtrip_nodes_.front() = insertion_node; + subtrip_nodes_.back() = this->Next(insertion_node); + } else { + reference_node_ = chain_first_node; + covered_nodes_.ResetAllToFalse(); + int num_opened_pairs = 0; + // Split chain into subtrip and rejected nodes. + rejected_nodes_ = {this->Prev(chain_first_node)}; + subtrip_nodes_ = {insertion_node}; + int current = chain_first_node; + do { + covered_nodes_.Set(current); + const int pair = pd_data_.GetPairOfNode(current); + if (pd_data_.IsDeliveryNode(current) && !opened_pairs_set_[pair]) { + rejected_nodes_.push_back(current); + } else { + subtrip_nodes_.push_back(current); + if (pd_data_.IsPickupNode(current)) { + ++num_opened_pairs; + opened_pairs_set_[pair] = true; + } else if (pd_data_.IsDeliveryNode(current)) { + --num_opened_pairs; + opened_pairs_set_[pair] = false; + } } - } - current = this->Next(current); - } while (num_opened_pairs != 0 && !this->IsPathEnd(current)); - DCHECK_EQ(num_opened_pairs, 0); - rejected_nodes_.push_back(current); - subtrip_nodes_.push_back(this->Next(insertion_node)); - + current = this->Next(current); + } while (num_opened_pairs != 0 && !this->IsPathEnd(current)); + DCHECK_EQ(num_opened_pairs, 0); + rejected_nodes_.push_back(current); + subtrip_nodes_.push_back(this->Next(insertion_node)); + } + if (covered_nodes_[insertion_node]) return false; // Set new paths. + if (!this->CheckPathCompatibility(subtrip_nodes_, + this->Path(insertion_node))) { + this->SetNextBaseToIncrement(0); + return false; + } SetPath(rejected_nodes_, this->Path(chain_first_node)); SetPath(subtrip_nodes_, this->Path(insertion_node)); return true; @@ -1524,42 +1596,52 @@ bool RelocateSubtrip::RelocateSubTripFromDelivery( // opened_pairs_set_ should be all false. DCHECK(std::none_of(opened_pairs_set_.begin(), opened_pairs_set_.end(), [](bool value) { return value; })); - int num_opened_pairs = 0; - // Split chain into subtrip and rejected nodes. Store nodes in reverse order. - rejected_nodes_ = {this->Next(chain_last_node)}; - subtrip_nodes_ = {this->Next(insertion_node)}; - int current = chain_last_node; - do { - if (current == insertion_node) { - opened_pairs_set_.assign(opened_pairs_set_.size(), false); - return false; - } - const int pair = pd_data_.GetPairOfNode(current); - if (pd_data_.IsPickupNode(current) && !opened_pairs_set_[pair]) { - rejected_nodes_.push_back(current); - } else { - subtrip_nodes_.push_back(current); - if (pd_data_.IsDeliveryNode(current)) { - ++num_opened_pairs; - opened_pairs_set_[pair] = true; - } else if (pd_data_.IsPickupNode(current)) { - --num_opened_pairs; - opened_pairs_set_[pair] = false; + if (chain_last_node == reference_node_) { + subtrip_nodes_.front() = insertion_node; + subtrip_nodes_.back() = this->Next(insertion_node); + } else { + reference_node_ = chain_last_node; + covered_nodes_.ResetAllToFalse(); + int num_opened_pairs = 0; + // Split chain into subtrip and rejected nodes. Store nodes in reverse + // order. + rejected_nodes_ = {this->Next(chain_last_node)}; + subtrip_nodes_ = {this->Next(insertion_node)}; + int current = chain_last_node; + do { + covered_nodes_.Set(current); + const int pair = pd_data_.GetPairOfNode(current); + if (pd_data_.IsPickupNode(current) && !opened_pairs_set_[pair]) { + rejected_nodes_.push_back(current); + } else { + subtrip_nodes_.push_back(current); + if (pd_data_.IsDeliveryNode(current)) { + ++num_opened_pairs; + opened_pairs_set_[pair] = true; + } else if (pd_data_.IsPickupNode(current)) { + --num_opened_pairs; + opened_pairs_set_[pair] = false; + } } - } - current = this->Prev(current); - } while (num_opened_pairs != 0 && !this->IsPathStart(current)); - DCHECK_EQ(num_opened_pairs, 0); - if (current == insertion_node) return false; // Skip null move. - rejected_nodes_.push_back(current); - subtrip_nodes_.push_back(insertion_node); - - // TODO(user): either remove those std::reverse() and adapt the loops - // below, or refactor the loops into a function that also DCHECKs the path. - std::reverse(rejected_nodes_.begin(), rejected_nodes_.end()); - std::reverse(subtrip_nodes_.begin(), subtrip_nodes_.end()); - + current = this->Prev(current); + } while (num_opened_pairs != 0 && !this->IsPathStart(current)); + DCHECK_EQ(num_opened_pairs, 0); + covered_nodes_.Set(current); // Skip null move. + rejected_nodes_.push_back(current); + subtrip_nodes_.push_back(insertion_node); + + // TODO(user): either remove those std::reverse() and adapt the loops + // below, or refactor the loops into a function that also DCHECKs the path. + std::reverse(rejected_nodes_.begin(), rejected_nodes_.end()); + std::reverse(subtrip_nodes_.begin(), subtrip_nodes_.end()); + } + if (covered_nodes_[insertion_node]) return false; // Set new paths. + if (!this->CheckPathCompatibility(subtrip_nodes_, + this->Path(insertion_node))) { + this->SetNextBaseToIncrement(0); + return false; + } SetPath(rejected_nodes_, this->Path(chain_last_node)); SetPath(subtrip_nodes_, this->Path(insertion_node)); return true; @@ -1584,6 +1666,8 @@ bool RelocateSubtrip::MakeNeighbor() { if (this->IsInactive(neighbor)) return false; return do_move(/*node=*/neighbor, /*insertion_node=*/this->BaseNode(0)); } + // TODO(user): Optimize the code to not recompute subtrip and rejected + // subpaths when BaseNode(0) has not changed. return do_move(/*node=*/this->BaseNode(0), /*insertion_node=*/this->BaseNode(1)); } @@ -1646,7 +1730,7 @@ void ExchangeSubtrip::SetPath(absl::Span path, namespace { bool VectorContains(absl::Span values, int64_t target) { - return std::find(values.begin(), values.end(), target) != values.end(); + return absl::c_find(values, target) != values.end(); } } // namespace @@ -1732,6 +1816,10 @@ bool ExchangeSubtrip::MakeNeighbor() { // record path_id0 and path_id11 before calling SetPath(); const int64_t path0_id = this->Path(node0); const int64_t path1_id = this->Path(node1); + if (!this->CheckPathCompatibility(path0_, path0_id) || + !this->CheckPathCompatibility(path1_, path1_id)) { + return false; + } SetPath(path0_, path0_id); SetPath(path1_, path1_id); return true; diff --git a/ortools/routing/neighborhoods.h b/ortools/routing/neighborhoods.h index b26754c947f..4d865c7630a 100644 --- a/ortools/routing/neighborhoods.h +++ b/ortools/routing/neighborhoods.h @@ -22,6 +22,7 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "absl/log/check.h" #include "absl/types/span.h" #include "ortools/constraint_solver/assignment.h" @@ -275,15 +276,34 @@ class MakePairActiveOperator : public PathOperator { /// compatible with GetBaseNodeRestartPosition. bool RestartAtPathStartOnSynchronize() override { return true; } + void ResetIncrementalism() override { + last_start_node_ = -1; + for (auto& [start_node, counts] : + num_active_alternatives_of_pairs_of_start_node_) { + ActivateAllPairsForPath(start_node); + } + } + private: void OnNodeInitialization() override; int FindNextInactivePair(int pair_index) const; bool ContainsActiveNodes(absl::Span nodes) const; + void ActivateAllPairsForPath(int start_node); int inactive_pair_; int inactive_pair_first_index_; int inactive_pair_second_index_; const std::vector pairs_; + std::vector next_states_; + /// num_active_alternatives_of_pairs_of_start_node_[start_node][pair_index] is + /// the number of alternatives of pair_index that remain to be generated for + /// the path of start_node. When moving to a new solution, the values in + /// num_active_alternatives_of_pairs_of_start_node_[start_node] of paths that + /// have changed are reset. Maintaining this count allows to skip pair-path + /// insertions that have already been tried and failed. + absl::flat_hash_map> + num_active_alternatives_of_pairs_of_start_node_; + int last_start_node_ = -1; }; LocalSearchOperator* MakePairActive( @@ -848,6 +868,12 @@ class RelocateSubtrip : public PathOperator { std::string DebugString() const override { return "RelocateSubtrip"; } bool MakeNeighbor() override; + protected: + void OnNodeInitialization() override { + reference_node_ = -1; + covered_nodes_.ResetAllToFalse(); + } + private: // Relocates the subtrip starting at chain_first_node. It must be a pickup. bool RelocateSubTripFromPickup(int64_t chain_first_node, @@ -865,6 +891,8 @@ class RelocateSubtrip : public PathOperator { std::vector rejected_nodes_; std::vector subtrip_nodes_; + SparseBitset<> covered_nodes_; + int64_t reference_node_ = -1; }; LocalSearchOperator* MakeRelocateSubtrip( diff --git a/ortools/routing/parameters.cc b/ortools/routing/parameters.cc index 3409329fee8..a9e57b236a8 100644 --- a/ortools/routing/parameters.cc +++ b/ortools/routing/parameters.cc @@ -59,65 +59,20 @@ RoutingModelParameters DefaultRoutingModelParameters() { } namespace { -// Creates IteratedLocalSearchParameters without setting any default values for -// the repeated fields. Repeated fields are problematic because they will only -// be appended to when merging with a proto containing this field. -IteratedLocalSearchParameters CreateMinimalIteratedLocalSearchParameters() { - IteratedLocalSearchParameters ils; - ils.set_perturbation_strategy(PerturbationStrategy::RUIN_AND_RECREATE); - RuinRecreateParameters* rr = ils.mutable_ruin_recreate_parameters(); - // NOTE: As of 07/2024, we no longer add any default ruin strategies to the - // default RuinRecreateParameters. Since ruin_strategies is a repeated - // field, it will only be appended to when merging with a proto containing - // this field. - // A ruin strategy can be added as follows. - // rr->add_ruin_strategies() - // ->mutable_spatially_close_routes() - // ->set_num_ruined_routes(2); - rr->set_ruin_composition_strategy(RuinCompositionStrategy::UNSET); - rr->mutable_recreate_strategy()->set_heuristic( - FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION); - rr->set_route_selection_neighbors_ratio(1.0); - rr->set_route_selection_min_neighbors(10); - rr->set_route_selection_max_neighbors(100); - ils.set_improve_perturbed_solution(true); - // NOTE: As of 12/2025, we no longer add any default acceptance policies to - // the default IteratedLocalSearchParameters. Since strategies is a - // repeated field, it will only be appended to when merging with a proto - // containing this field. - // Acceptance policies can be added as follows. - // ils.mutable_best_solution_acceptance_policy() - // ->add_strategies() - // ->mutable_greedy_descent(); - // SimulatedAnnealingAcceptanceStrategy* sa = - // ils.mutable_reference_solution_acceptance_policy() - // ->add_strategies() - // ->mutable_simulated_annealing(); - // sa->set_cooling_schedule_strategy(CoolingScheduleStrategy::EXPONENTIAL); - // sa->set_initial_temperature(100.0); - // sa->set_final_temperature(0.01); - // sa->set_automatic_temperatures(false); - return ils; -} -IteratedLocalSearchParameters CreateDefaultIteratedLocalSearchParameters() { - IteratedLocalSearchParameters ils = - CreateMinimalIteratedLocalSearchParameters(); +// Instantiates repeated message fields to satisfy validation requirements +// while delegating leaf config primitive behaviors to ils.proto defaults. +IteratedLocalSearchParameters CreateRecommendedIteratedLocalSearchParameters() { + IteratedLocalSearchParameters ils; ils.mutable_ruin_recreate_parameters() ->add_ruin_strategies() - ->mutable_spatially_close_routes() - ->set_num_ruined_routes(2); + ->mutable_spatially_close_routes(); ils.mutable_best_solution_acceptance_policy() ->add_strategies() ->mutable_greedy_descent(); - SimulatedAnnealingAcceptanceStrategy* sa = - ils.mutable_reference_solution_acceptance_policy() - ->add_strategies() - ->mutable_simulated_annealing(); - sa->set_cooling_schedule_strategy(CoolingScheduleStrategy::EXPONENTIAL); - sa->set_initial_temperature(100.0); - sa->set_final_temperature(0.01); - sa->set_automatic_temperatures(false); + ils.mutable_reference_solution_acceptance_policy() + ->add_strategies() + ->mutable_simulated_annealing(); return ils; } @@ -231,9 +186,6 @@ RoutingSearchParameters CreateDefaultRoutingSearchParameters() { p.set_log_search(false); p.set_log_cost_scaling_factor(1.0); p.set_log_cost_offset(0.0); - p.set_use_iterated_local_search(false); - *p.mutable_iterated_local_search_parameters() = - CreateMinimalIteratedLocalSearchParameters(); const std::string error = FindErrorInRoutingSearchParameters(p); LOG_IF(DFATAL, !error.empty()) @@ -244,9 +196,6 @@ RoutingSearchParameters CreateDefaultRoutingSearchParameters() { RoutingSearchParameters CreateDefaultSecondaryRoutingSearchParameters() { RoutingSearchParameters p = CreateDefaultRoutingSearchParameters(); p.set_local_search_metaheuristic(LocalSearchMetaheuristic::GREEDY_DESCENT); - p.set_use_iterated_local_search(false); - *p.mutable_iterated_local_search_parameters() = - CreateMinimalIteratedLocalSearchParameters(); RoutingSearchParameters::LocalSearchNeighborhoodOperators* o = p.mutable_local_search_operators(); o->set_use_relocate(BOOL_TRUE); @@ -304,10 +253,10 @@ RoutingSearchParameters DefaultSecondaryRoutingSearchParameters() { return *default_parameters; } -IteratedLocalSearchParameters DefaultIteratedLocalSearchParameters() { - static const auto* default_parameters = new IteratedLocalSearchParameters( - CreateDefaultIteratedLocalSearchParameters()); - return *default_parameters; +IteratedLocalSearchParameters RecommendedIteratedLocalSearchParameters() { + static const auto* recommended_parameters = new IteratedLocalSearchParameters( + CreateRecommendedIteratedLocalSearchParameters()); + return *recommended_parameters; } namespace { @@ -439,157 +388,136 @@ void FindErrorsInRecreateParameters( } } -// Searches for errors in ILS parameters and appends them to the given `errors` -// vector. -void FindErrorsInIteratedLocalSearchParameters( - const RoutingSearchParameters& search_parameters, - std::vector& errors) { - using absl::StrCat; - if (!search_parameters.use_iterated_local_search()) { - return; +void AppendStringsWithPrefix(const absl::string_view prefix, + const std::vector& strings, + std::vector& errors) { + for (const auto& str : strings) { + errors.emplace_back(absl::StrCat(prefix, str)); } +} - if (!search_parameters.has_iterated_local_search_parameters()) { - errors.emplace_back( - "use_iterated_local_search is true but " - "iterated_local_search_parameters are missing."); - return; - } +std::vector FindErrorsInRuinRecreateParameters( + const RuinRecreateParameters& rr) { + std::vector errors; + using absl::StrCat; - const IteratedLocalSearchParameters& ils = - search_parameters.iterated_local_search_parameters(); + if (rr.ruin_strategies().empty()) { + errors.emplace_back("`ruin_strategies` is empty"); + } - if (ils.perturbation_strategy() == PerturbationStrategy::UNSET) { + if (rr.ruin_strategies().size() > 1 && + rr.ruin_composition_strategy() == RuinCompositionStrategy::UNSET) { errors.emplace_back( - StrCat("Invalid value for " - "iterated_local_search_parameters.perturbation_strategy: ", - ils.perturbation_strategy())); + "`ruin_composition_strategy` cannot be unset when more than one ruin " + "strategy is defined"); } - if (ils.perturbation_strategy() == PerturbationStrategy::RUIN_AND_RECREATE) { - if (!ils.has_ruin_recreate_parameters()) { - errors.emplace_back(StrCat( - "iterated_local_search_parameters.perturbation_strategy is ", - PerturbationStrategy::RUIN_AND_RECREATE, - " but iterated_local_search_parameters.ruin_recreate_parameters are " - "missing.")); - return; - } - - const RuinRecreateParameters& rr = ils.ruin_recreate_parameters(); - - if (rr.ruin_strategies().empty()) { + for (const auto& ruin : rr.ruin_strategies()) { + if (ruin.strategy_case() == RuinStrategy::kSpatiallyCloseRoutes && + ruin.spatially_close_routes().num_ruined_routes() == 0) { errors.emplace_back( - StrCat("iterated_local_search_parameters.ruin_recreate_parameters." - "ruin_strategies is empty")); - } - - if (rr.ruin_strategies().size() > 1 && - rr.ruin_composition_strategy() == RuinCompositionStrategy::UNSET) { - errors.emplace_back(StrCat( - "iterated_local_search_parameters.ruin_recreate_parameters." - "ruin_composition_strategy cannot be unset when more than one ruin " - "strategy is defined")); - } - - for (const auto& ruin : rr.ruin_strategies()) { - if (ruin.strategy_case() == RuinStrategy::kSpatiallyCloseRoutes && - ruin.spatially_close_routes().num_ruined_routes() == 0) { - errors.emplace_back(StrCat( - "iterated_local_search_parameters.ruin_recreate_parameters." - "ruin_strategy is set to SpatiallyCloseRoutesRuinStrategy" - " but spatially_close_routes.num_ruined_routes is 0 (should be " - "strictly positive)")); - } else if (ruin.strategy_case() == RuinStrategy::kRandomWalk && - ruin.random_walk().num_removed_visits() == 0) { + "`spatially_close_routes.num_ruined_routes` is 0 (must be > 0)"); + } else if (ruin.strategy_case() == RuinStrategy::kRandomWalk && + ruin.random_walk().num_removed_visits() == 0) { + errors.emplace_back( + "`random_walk.num_removed_visits` is 0 (must be > 0)"); + } else if (ruin.strategy_case() == RuinStrategy::kSisr) { + if (ruin.sisr().avg_num_removed_visits() == 0) { + errors.emplace_back("`sisr.avg_num_removed_visits` is 0 (must be > 0)"); + } + if (ruin.sisr().max_removed_sequence_size() == 0) { errors.emplace_back( - StrCat("iterated_local_search_parameters.ruin_recreate_parameters." - "ruin_strategy is set to RandomWalkRuinStrategy" - " but random_walk.num_removed_visits is 0 (should be " - "strictly positive)")); - } else if (ruin.strategy_case() == RuinStrategy::kSisr) { - if (ruin.sisr().avg_num_removed_visits() == 0) { - errors.emplace_back( - "iterated_local_search_parameters.ruin_recreate_parameters." - "ruin is set to SISRRuinStrategy" - " but sisr.avg_num_removed_visits is 0 (should be strictly " - "positive)"); - } - if (ruin.sisr().max_removed_sequence_size() == 0) { - errors.emplace_back( - "iterated_local_search_parameters.ruin_recreate_parameters.ruin " - "is set to SISRRuinStrategy but " - "sisr.max_removed_sequence_size is 0 (should be strictly " - "positive)"); - } - if (ruin.sisr().bypass_factor() < 0 || - ruin.sisr().bypass_factor() > 1) { - errors.emplace_back(StrCat( - "iterated_local_search_parameters.ruin_recreate_parameters." - "ruin is set to SISRRuinStrategy" - " but sisr.bypass_factor is not in [0, 1]")); - } + "`sisr.max_removed_sequence_size` is 0 (must be > 0)"); + } + // Using !(...) to catch NaN. + if (!(ruin.sisr().bypass_factor() >= 0 && + ruin.sisr().bypass_factor() <= 1)) { + errors.emplace_back("`sisr.bypass_factor` is not in [0, 1]"); } } + } - if (const double ratio = rr.route_selection_neighbors_ratio(); - std::isnan(ratio) || ratio <= 0 || ratio > 1) { - errors.emplace_back( - StrCat("Invalid " - "iterated_local_search_parameters.ruin_recreate_parameters." - "route_selection_neighbors_ratio: ", - ratio)); - } - if (rr.route_selection_min_neighbors() == 0) { - errors.emplace_back( - StrCat("iterated_local_search_parameters.ruin_recreate_parameters." - "route_selection_min_neighbors must be positive")); - } - if (rr.route_selection_min_neighbors() > - rr.route_selection_max_neighbors()) { - errors.emplace_back( - StrCat("iterated_local_search_parameters.ruin_recreate_parameters." - "route_selection_min_neighbors cannot be greater than " - "iterated_local_search_parameters.ruin_recreate_parameters." - "route_selection_max_neighbors")); - } + if (const double ratio = rr.route_selection_neighbors_ratio(); + std::isnan(ratio) || ratio <= 0 || ratio > 1) { + errors.emplace_back( + StrCat("Invalid `route_selection_neighbors_ratio`: ", ratio)); + } + if (rr.route_selection_min_neighbors() == 0) { + errors.emplace_back(StrCat("`route_selection_min_neighbors` must be > 0")); + } + if (rr.route_selection_min_neighbors() > rr.route_selection_max_neighbors()) { + errors.emplace_back( + StrCat("`route_selection_min_neighbors` cannot be greater than " + "`route_selection_max_neighbors`")); + } - const FirstSolutionStrategy::Value recreate_heuristic = - rr.recreate_strategy().heuristic(); - if (recreate_heuristic == FirstSolutionStrategy::UNSET) { - errors.emplace_back( - StrCat("Invalid value for " - "iterated_local_search_parameters.ruin_recreate_parameters." - "recreate_strategy.heuristic: ", - FirstSolutionStrategy::Value_Name(recreate_heuristic))); - } + const FirstSolutionStrategy::Value recreate_heuristic = + rr.recreate_strategy().heuristic(); + if (recreate_heuristic == FirstSolutionStrategy::UNSET) { + errors.emplace_back( + StrCat("Invalid value for `recreate_strategy.heuristic`: ", + FirstSolutionStrategy::Value_Name(recreate_heuristic))); + } - if (rr.recreate_strategy().has_parameters()) { - const RecreateParameters& recreate_params = - rr.recreate_strategy().parameters(); - if (recreate_params.parameters_case() == - RecreateParameters::PARAMETERS_NOT_SET) { + if (rr.recreate_strategy().has_parameters()) { + const RecreateParameters& recreate_params = + rr.recreate_strategy().parameters(); + if (recreate_params.parameters_case() == + RecreateParameters::PARAMETERS_NOT_SET) { + errors.emplace_back( + StrCat("Invalid value for `recreate_strategy.parameters`: ", + GetRecreateParametersName(recreate_params.parameters_case()))); + } else { + if (const RecreateParameters::ParametersCase params = + GetParameterCaseForRecreateHeuristic(recreate_heuristic); + recreate_params.parameters_case() != params) { errors.emplace_back(StrCat( - "Invalid value for " - "iterated_local_search_parameters.ruin_recreate_parameters." - "recreate_strategy.parameters: ", + "`recreate_strategy.heuristic` is set to ", + FirstSolutionStrategy::Value_Name(recreate_heuristic), + " but `recreate_strategy.parameters` define ", GetRecreateParametersName(recreate_params.parameters_case()))); } else { - 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); - } + std::vector recreate_errors; + FindErrorsInRecreateParameters(recreate_heuristic, recreate_params, + errors); + AppendStringsWithPrefix("in `recreate_strategy`: ", recreate_errors, + errors); } } } + return errors; +} + +} // namespace + +// Searches for errors in ILS parameters and appends them to the given `errors` +// vector. +std::vector FindErrorsInIteratedLocalSearchParameters( + const routing::IteratedLocalSearchParameters& ils) { + std::vector errors; + using absl::StrCat; + + if (ils.perturbation_strategy() == PerturbationStrategy::UNSET) { + errors.emplace_back( + StrCat("Invalid `perturbation_strategy` = ", + PerturbationStrategy::Value_Name(PerturbationStrategy::UNSET))); + } + + // TODO(user): Make it invalid to have has_ruin_recreate_parameters and + // perturbation_strategy != RUIN_AND_RECREATE. + if (ils.perturbation_strategy() == PerturbationStrategy::RUIN_AND_RECREATE) { + if (!ils.has_ruin_recreate_parameters()) { + errors.emplace_back(StrCat("`perturbation_strategy`=", + PerturbationStrategy::Value_Name( + PerturbationStrategy::RUIN_AND_RECREATE), + " but `ruin_recreate_parameters` is missing")); + return errors; + } + std::vector rr_errors = + FindErrorsInRuinRecreateParameters(ils.ruin_recreate_parameters()); + AppendStringsWithPrefix("in `ruin_recreate_parameters`: ", rr_errors, + errors); + } struct NamedAcceptanceStrategy { std::string name; @@ -599,18 +527,14 @@ void FindErrorsInIteratedLocalSearchParameters( if (!ils.has_reference_solution_acceptance_policy()) { errors.emplace_back( - StrCat("Unset value for " - "iterated_local_search_parameters.reference_solution_acceptance_" - "policy.")); + StrCat("`reference_solution_acceptance_policy` is unset")); } else { named_acceptance_policies.push_back( {"reference_solution", ils.reference_solution_acceptance_policy()}); } if (!ils.has_best_solution_acceptance_policy()) { - errors.emplace_back(StrCat( - "Unset value for " - "iterated_local_search_parameters.best_solution_acceptance_policy.")); + errors.emplace_back(StrCat("`best_solution_acceptance_policy` is unset")); } else { named_acceptance_policies.push_back( {"best_solution", ils.best_solution_acceptance_policy()}); @@ -618,67 +542,60 @@ void FindErrorsInIteratedLocalSearchParameters( for (const auto& [name, acceptance_policy] : named_acceptance_policies) { if (acceptance_policy.strategies().empty()) { - errors.emplace_back(StrCat("iterated_local_search_parameters.", name, - "_acceptance_policy.strategies is empty")); + errors.emplace_back( + StrCat("`", name, "_acceptance_policy.strategies` is empty")); } if (acceptance_policy.strategies().size() > 1 && acceptance_policy.composition() == AcceptancePolicy::UNSET) { - errors.emplace_back( - StrCat("iterated_local_search_parameters.", name, - "_acceptance_policy.composition is unset but there are ", - acceptance_policy.strategies().size(), " strategies.")); + errors.emplace_back(StrCat( + "`", name, "_acceptance_policy.composition` is unset but there are ", + acceptance_policy.strategies().size(), " `strategies`.")); } for (const AcceptanceStrategy& acceptance_strategy : acceptance_policy.strategies()) { if (acceptance_strategy.has_simulated_annealing()) { + const std::string sa_prefix = StrCat( + "`", name, "_acceptance_policy.strategies.simulated_annealing."); const SimulatedAnnealingAcceptanceStrategy& sa_params = acceptance_strategy.simulated_annealing(); if (sa_params.cooling_schedule_strategy() == CoolingScheduleStrategy::UNSET) { errors.emplace_back(StrCat( - "Invalid value for " - "iterated_local_search_parameters.", - name, - "_acceptance_strategy.simulated_annealing.cooling_schedule_" - "strategy: ", - sa_params.cooling_schedule_strategy())); + "Invalid value for ", sa_prefix, "cooling_schedule_strategy`: ", + CoolingScheduleStrategy::Value_Name( + sa_params.cooling_schedule_strategy()))); } - if (!sa_params.automatic_temperatures()) { - if (sa_params.initial_temperature() < sa_params.final_temperature()) { - errors.emplace_back( - StrCat("iterated_local_search_parameters.", name, - "_acceptance_strategy.simulated_annealing." - "initial_temperature cannot be lower than " - "iterated_local_search_parameters.simulated_annealing_" - "parameters." - "final_temperature.")); + if (sa_params.automatic_temperatures()) { + if (sa_params.has_initial_temperature() || + sa_params.has_final_temperature()) { + errors.emplace_back(StrCat( + sa_prefix, + ".automatic_temperatures` is true so neither " + "`initial_temperature` nor `final_temperature` can be set")); } - - if (sa_params.initial_temperature() < 1e-9) { + } else { // !sa_params.automatic_temperatures(). + if (!(sa_params.initial_temperature() >= + sa_params.final_temperature())) { errors.emplace_back( - StrCat("iterated_local_search_parameters.", name, - "_acceptance_strategy.simulated_annealing." - "initial_temperature cannot be lower than 1e-9.")); + StrCat(sa_prefix, "initial_temperature` cannot be lower than ", + sa_prefix, "final_temperature`.")); } - if (sa_params.final_temperature() < 1e-9) { - errors.emplace_back( - StrCat("iterated_local_search_parameters.", name, - "_acceptance_strategy.simulated_annealing." - "final_temperature cannot be lower than 1e-9.")); + if (!(sa_params.final_temperature() >= 1e-9)) { + errors.emplace_back(StrCat( + sa_prefix, "final_temperature` cannot be lower than 1e-9.")); } } } } } + return errors; } -} // namespace - std::string FindErrorInRoutingSearchParameters( const RoutingSearchParameters& search_parameters) { const std::vector errors = @@ -937,7 +854,13 @@ std::vector FindErrorsInRoutingSearchParameters( "local_search_operators.use_swap_active_chain is BOOL_TRUE"); } - FindErrorsInIteratedLocalSearchParameters(search_parameters, errors); + if (search_parameters.has_iterated_local_search_parameters()) { + AppendStringsWithPrefix( + "in `iterated_local_search_parameters`: ", + FindErrorsInIteratedLocalSearchParameters( + search_parameters.iterated_local_search_parameters()), + errors); + } return errors; } diff --git a/ortools/routing/parameters.h b/ortools/routing/parameters.h index f139f7508fe..3893d5cd1b2 100644 --- a/ortools/routing/parameters.h +++ b/ortools/routing/parameters.h @@ -17,6 +17,7 @@ #include #include +#include "ortools/routing/ils.pb.h" #include "ortools/routing/parameters.pb.h" namespace operations_research::routing { @@ -24,7 +25,7 @@ namespace operations_research::routing { RoutingModelParameters DefaultRoutingModelParameters(); RoutingSearchParameters DefaultRoutingSearchParameters(); RoutingSearchParameters DefaultSecondaryRoutingSearchParameters(); -IteratedLocalSearchParameters DefaultIteratedLocalSearchParameters(); +IteratedLocalSearchParameters RecommendedIteratedLocalSearchParameters(); /// Returns an empty std::string if the routing search parameters are valid, and /// a non-empty, human readable error description if they're not. @@ -36,6 +37,11 @@ std::string FindErrorInRoutingSearchParameters( std::vector FindErrorsInRoutingSearchParameters( const RoutingSearchParameters& search_parameters); +/// Returns a list of std::string describing the errors in the iterated local +/// search parameters. Returns an empty vector if the parameters are valid. +std::vector FindErrorsInIteratedLocalSearchParameters( + const IteratedLocalSearchParameters& ils); + } // namespace operations_research::routing #endif // ORTOOLS_ROUTING_PARAMETERS_H_ diff --git a/ortools/routing/parameters.proto b/ortools/routing/parameters.proto index a12a15069dc..05d0027e9c9 100644 --- a/ortools/routing/parameters.proto +++ b/ortools/routing/parameters.proto @@ -41,7 +41,7 @@ package operations_research.routing; // To see those "default" parameters, call GetDefaultRoutingSearchParameters(). // Next ID: 73 message RoutingSearchParameters { - reserved 14, 15, 16, 18, 19, 21, 23, 31, 40, 44, 45, 46, 49, 55, 65, 67; + reserved 14, 15, 16, 18, 19, 21, 23, 31, 40, 44, 45, 46, 49, 55, 58, 65, 67; // First solution strategies, used as starting point of local search. FirstSolutionStrategy.Value first_solution_strategy = 1; @@ -570,11 +570,8 @@ message RoutingSearchParameters { // solution. Useful to sort out logs when several solves are run in parallel. string log_tag = 36; - // Whether the solver should use an Iterated Local Search approach to solve - // the problem. - bool use_iterated_local_search = 58; - - // Iterated Local Search parameters. + // Iterated Local Search parameters. When set, the solver will use an + // Iterated Local Search approach to solve the problem. IteratedLocalSearchParameters iterated_local_search_parameters = 60; } diff --git a/ortools/routing/python/doc.h b/ortools/routing/python/doc.h index 25b984a3531..1085b67a27a 100644 --- a/ortools/routing/python/doc.h +++ b/ortools/routing/python/doc.h @@ -2145,16 +2145,6 @@ static const char* __doc_operations_research_routing_Model_HasLocalCumulOptimizer = // NOLINT R"doc()doc"; -static const char* - __doc_operations_research_routing_Model_HasMandatoryDisjunctions = // NOLINT - R"doc(Returns true if the model contains mandatory disjunctions (ones with -kNoPenalty as penalty).)doc"; - -static const char* - __doc_operations_research_routing_Model_HasMaxCardinalityConstrainedDisjunctions = // NOLINT - R"doc(Returns true if the model contains at least one disjunction which -is constrained by its max_cardinality.)doc"; - static const char* __doc_operations_research_routing_Model_HasSameVehicleTypeRequirements = R"doc(Returns true iff any same-route (resp. temporal) type requirements diff --git a/ortools/routing/python/routing.cc b/ortools/routing/python/routing.cc index dfb4b1ca4d5..58aa39c2a65 100644 --- a/ortools/routing/python/routing.cc +++ b/ortools/routing/python/routing.cc @@ -608,6 +608,48 @@ PYBIND11_MODULE(routing, m) { py::arg("penalty_cost_behavior") = Model::PenaltyCostBehavior::PENALIZE_ONCE, DOC(operations_research, routing, Model, AddDisjunction)); + rm.def( + "make_disjunction", + [](Model* routing_model, const std::vector& indices) -> int { + return routing_model->MakeDisjunction(indices).value(); + }, + py::arg("indices")); + rm.def( + "set_disjunction_hard_maximum", + [](Model* model, int disjunction, int max_cardinality) { + model->SetDisjunctionHardMaximum( + operations_research::routing::DisjunctionIndex(disjunction), + max_cardinality); + }, + py::arg("disjunction"), py::arg("max_cardinality")); + rm.def( + "set_disjunction_hard_minimum", + [](Model* model, int disjunction, int min_cardinality) { + model->SetDisjunctionHardMinimum( + operations_research::routing::DisjunctionIndex(disjunction), + min_cardinality); + }, + py::arg("disjunction"), py::arg("min_cardinality")); + rm.def( + "set_disjunction_soft_maximum", + [](Model* model, int disjunction, int soft_max_cardinality, + int64_t penalty, Model::PenaltyCostBehavior penalty_cost_behavior) { + model->SetDisjunctionSoftMaximum( + operations_research::routing::DisjunctionIndex(disjunction), + soft_max_cardinality, penalty, penalty_cost_behavior); + }, + py::arg("disjunction"), py::arg("soft_max_cardinality"), + py::arg("penalty"), py::arg("penalty_cost_behavior")); + rm.def( + "set_disjunction_soft_minimum", + [](Model* model, int disjunction, int soft_min_cardinality, + int64_t penalty, Model::PenaltyCostBehavior penalty_cost_behavior) { + model->SetDisjunctionSoftMinimum( + operations_research::routing::DisjunctionIndex(disjunction), + soft_min_cardinality, penalty, penalty_cost_behavior); + }, + py::arg("disjunction"), py::arg("soft_min_cardinality"), + py::arg("penalty"), py::arg("penalty_cost_behavior")); rm.def("add_pickup_and_delivery", &Model::AddPickupAndDelivery, py::arg("pickup"), py::arg("delivery"), DOC(operations_research, routing, Model, AddPickupAndDelivery)); @@ -621,6 +663,71 @@ PYBIND11_MODULE(routing, m) { }, py::arg("pickup_disjunction"), py::arg("delivery_disjunction"), DOC(operations_research, routing, Model, AddPickupAndDeliverySets)); + rm.def( + "get_disjunction_max_cardinality", + [](const Model* model, int index) { + return model->GetDisjunctionMaxCardinality( + operations_research::routing::DisjunctionIndex(index)); + }, + py::arg("index")); + rm.def( + "get_disjunction_min_cardinality", + [](const Model* model, int index) { + return model->GetDisjunctionMinCardinality( + operations_research::routing::DisjunctionIndex(index)); + }, + py::arg("index")); + rm.def( + "get_disjunction_soft_max_cardinality", + [](const Model* model, int index) { + return model->GetDisjunctionSoftMaxCardinality( + operations_research::routing::DisjunctionIndex(index)); + }, + py::arg("index")); + rm.def( + "get_disjunction_soft_min_cardinality", + [](const Model* model, int index) { + return model->GetDisjunctionSoftMinCardinality( + operations_research::routing::DisjunctionIndex(index)); + }, + py::arg("index")); + rm.def( + "get_disjunction_soft_max_penalty", + [](const Model* model, int index) { + return model->GetDisjunctionSoftMaxPenalty( + operations_research::routing::DisjunctionIndex(index)); + }, + py::arg("index")); + rm.def( + "get_disjunction_soft_min_penalty", + [](const Model* model, int index) { + return model->GetDisjunctionSoftMinPenalty( + operations_research::routing::DisjunctionIndex(index)); + }, + py::arg("index")); + rm.def( + "get_disjunction_penalty", + [](const Model* model, int index) { + return model->GetDisjunctionPenalty( + operations_research::routing::DisjunctionIndex(index)); + }, + py::arg("index")); + rm.def( + "get_disjunction_soft_min_penalty_cost_behavior", + [](const Model* model, int index) { + return model->GetDisjunctionSoftMinPenaltyCostBehavior( + operations_research::routing::DisjunctionIndex(index)); + }, + py::arg("index")); + rm.def( + "get_disjunction_soft_max_penalty_cost_behavior", + [](const Model* model, int index) { + return model->GetDisjunctionSoftMaxPenaltyCostBehavior( + operations_research::routing::DisjunctionIndex(index)); + }, + py::arg("index")); + rm.def("get_number_of_disjunctions", &Model::GetNumberOfDisjunctions); + rm.def("has_hard_disjunctions", &Model::HasHardDisjunctions); rm.def("get_pickup_position", &Model::GetPickupPosition, py::arg("node_index")); // Missing doc. rm.def("get_delivery_position", &Model::GetDeliveryPosition, diff --git a/ortools/routing/python/routing.swig b/ortools/routing/python/routing.swig index 6ccd20f693b..6947b18956e 100644 --- a/ortools/routing/python/routing.swig +++ b/ortools/routing/python/routing.swig @@ -210,12 +210,26 @@ namespace operations_research::routing { %unignore Model::GetAllDimensionNames; %unignore Model::GetDimensions; %unignore Model::GetDisjunctionIndices; +%unignore Model::GetDisjunctionMaxCardinality; +%unignore Model::GetDisjunctionMinCardinality; +%unignore Model::GetDisjunctionSoftMaxCardinality; +%unignore Model::GetDisjunctionSoftMaxPenalty; +%unignore Model::GetDisjunctionSoftMaxPenaltyCostBehavior; +%unignore Model::GetDisjunctionSoftMinCardinality; +%unignore Model::GetDisjunctionSoftMinPenalty; +%unignore Model::GetDisjunctionSoftMinPenaltyCostBehavior; %unignore Model::GetDisjunctionPenalty; +%unignore Model::GetNumberOfDisjunctions; +%unignore Model::MakeDisjunction; %unignore Model::ReadAssignment; %unignore Model::RegisterCumulDependentTransitCallback; %unignore Model::ResourceVar; %unignore Model::RestoreAssignment; %unignore Model::SetArcCostEvaluatorOfVehicle; +%unignore Model::SetDisjunctionHardMaximum; +%unignore Model::SetDisjunctionHardMinimum; +%unignore Model::SetDisjunctionSoftMaximum; +%unignore Model::SetDisjunctionSoftMinimum; %unignore Model::SetPathEnergyCostOfVehicle; %unignore Model::TransitCallback; %unignore Model::UnaryTransitCallbackOrNull; diff --git a/ortools/routing/routing.cc b/ortools/routing/routing.cc index 78ecac0e840..8bde94312d8 100644 --- a/ortools/routing/routing.cc +++ b/ortools/routing/routing.cc @@ -148,14 +148,11 @@ const Assignment* Model::PackCumulsOfOptimizerDimensionsFromAssignment( return original_assignment; } RegularLimit* const limit = GetOrCreateLimit(); - limit->UpdateLimits(duration_limit, std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()); + limit->UpdateLimits(duration_limit, kint64max, kint64max, kint64max); RegularLimit* const cumulative_limit = GetOrCreateCumulativeLimit(); - cumulative_limit->UpdateLimits( - duration_limit, std::numeric_limits::max(), - std::numeric_limits::max(), std::numeric_limits::max()); + cumulative_limit->UpdateLimits(duration_limit, kint64max, kint64max, + kint64max); // Initialize the packed_assignment with the Next values in the // original_assignment. @@ -589,7 +586,7 @@ Model::Model(const IndexManager& index_manager, for (int i = 0; i < index_to_node.size(); ++i) { index_to_equivalence_class_[i] = index_to_node[i].value(); } - allowed_vehicles_.resize(Size() + vehicles_); + allowed_vehicles_.resize(Size() + vehicles_, LazyMonotonicSet(vehicles_)); } void Model::Initialize() { @@ -756,7 +753,7 @@ void Model::AddNoCycleConstraintInternal() { bool Model::AddDimension(int evaluator_index, int64_t slack_max, int64_t capacity, bool fix_start_cumul_to_zero, - const std::string& name) { + absl::string_view name) { const std::vector evaluator_indices(vehicles_, evaluator_index); std::vector capacities(vehicles_, capacity); return AddDimensionWithCapacityInternal(evaluator_indices, {}, slack_max, @@ -766,7 +763,7 @@ bool Model::AddDimension(int evaluator_index, int64_t slack_max, bool Model::AddDimensionWithVehicleTransits( const std::vector& evaluator_indices, int64_t slack_max, - int64_t capacity, bool fix_start_cumul_to_zero, const std::string& name) { + int64_t capacity, bool fix_start_cumul_to_zero, absl::string_view name) { std::vector capacities(vehicles_, capacity); return AddDimensionWithCapacityInternal(evaluator_indices, {}, slack_max, std::move(capacities), @@ -776,7 +773,7 @@ bool Model::AddDimensionWithVehicleTransits( bool Model::AddDimensionWithVehicleCapacity( int evaluator_index, int64_t slack_max, std::vector vehicle_capacities, bool fix_start_cumul_to_zero, - const std::string& name) { + absl::string_view name) { const std::vector evaluator_indices(vehicles_, evaluator_index); return AddDimensionWithCapacityInternal(evaluator_indices, {}, slack_max, std::move(vehicle_capacities), @@ -786,7 +783,7 @@ bool Model::AddDimensionWithVehicleCapacity( bool Model::AddDimensionWithVehicleTransitAndCapacity( const std::vector& evaluator_indices, int64_t slack_max, std::vector vehicle_capacities, bool fix_start_cumul_to_zero, - const std::string& name) { + absl::string_view name) { return AddDimensionWithCapacityInternal(evaluator_indices, {}, slack_max, std::move(vehicle_capacities), fix_start_cumul_to_zero, name); @@ -796,7 +793,7 @@ bool Model::AddDimensionWithCumulDependentVehicleTransitAndCapacity( const std::vector& fixed_evaluator_indices, const std::vector& cumul_dependent_evaluator_indices, int64_t slack_max, std::vector vehicle_capacities, - bool fix_start_cumul_to_zero, const std::string& name) { + bool fix_start_cumul_to_zero, absl::string_view name) { return AddDimensionWithCapacityInternal( fixed_evaluator_indices, cumul_dependent_evaluator_indices, slack_max, std::move(vehicle_capacities), fix_start_cumul_to_zero, name); @@ -806,7 +803,7 @@ bool Model::AddDimensionWithCapacityInternal( const std::vector& evaluator_indices, const std::vector& cumul_dependent_evaluator_indices, int64_t slack_max, std::vector vehicle_capacities, - bool fix_start_cumul_to_zero, const std::string& name) { + bool fix_start_cumul_to_zero, absl::string_view name) { CHECK_EQ(vehicles_, vehicle_capacities.size()); return InitializeDimensionInternal( evaluator_indices, cumul_dependent_evaluator_indices, @@ -848,7 +845,7 @@ bool Model::InitializeDimensionInternal( std::pair Model::AddConstantDimensionWithSlack( int64_t value, int64_t capacity, int64_t slack_max, - bool fix_start_cumul_to_zero, const std::string& dimension_name) { + bool fix_start_cumul_to_zero, absl::string_view dimension_name) { const TransitEvaluatorSign sign = value < 0 ? kTransitEvaluatorSignNegativeOrZero : kTransitEvaluatorSignPositiveOrZero; @@ -861,7 +858,7 @@ std::pair Model::AddConstantDimensionWithSlack( std::pair Model::AddVectorDimension( std::vector values, int64_t capacity, bool fix_start_cumul_to_zero, - const std::string& dimension_name) { + absl::string_view dimension_name) { const int evaluator_index = RegisterUnaryTransitVector(std::move(values)); return std::make_pair(evaluator_index, AddDimension(evaluator_index, 0, capacity, @@ -870,7 +867,7 @@ std::pair Model::AddVectorDimension( std::pair Model::AddMatrixDimension( std::vector> values, int64_t capacity, - bool fix_start_cumul_to_zero, const std::string& dimension_name) { + bool fix_start_cumul_to_zero, absl::string_view dimension_name) { const int evaluator_index = RegisterTransitMatrix(std::move(values)); return std::make_pair(evaluator_index, AddDimension(evaluator_index, 0, capacity, @@ -895,7 +892,7 @@ class RangeMakeElementExpr : public BaseIntExpr { const int idx_min = index_->Min(); const int idx_max = index_->Max() + 1; return (idx_min < idx_max) ? callback_->RangeMin(idx_min, idx_max) - : std::numeric_limits::max(); + : kint64max; } void SetMin(int64_t new_min) override { const int64_t old_min = Min(); @@ -920,7 +917,7 @@ class RangeMakeElementExpr : public BaseIntExpr { const int idx_min = index_->Min(); const int idx_max = index_->Max() + 1; return (idx_min < idx_max) ? callback_->RangeMax(idx_min, idx_max) - : std::numeric_limits::min(); + : kint64min; } void SetMax(int64_t new_max) override { const int64_t old_min = Min(); @@ -957,7 +954,7 @@ IntExpr* MakeRangeMakeElementExpr(const RangeIntToIntFunction* callback, bool Model::AddDimensionDependentDimensionWithVehicleCapacity( const std::vector& dependent_transits, const Dimension* base_dimension, int64_t slack_max, std::vector vehicle_capacities, - bool fix_start_cumul_to_zero, const std::string& name) { + bool fix_start_cumul_to_zero, absl::string_view name) { const std::vector pure_transits(vehicles_, /*zero_evaluator*/ 0); return AddDimensionDependentDimensionWithVehicleCapacity( pure_transits, dependent_transits, base_dimension, slack_max, @@ -967,7 +964,7 @@ bool Model::AddDimensionDependentDimensionWithVehicleCapacity( bool Model::AddDimensionDependentDimensionWithVehicleCapacity( int transit, const Dimension* dimension, int64_t slack_max, int64_t vehicle_capacity, bool fix_start_cumul_to_zero, - const std::string& name) { + absl::string_view name) { return AddDimensionDependentDimensionWithVehicleCapacity( /*zero_evaluator*/ 0, transit, dimension, slack_max, vehicle_capacity, fix_start_cumul_to_zero, name); @@ -977,7 +974,7 @@ bool Model::AddDimensionDependentDimensionWithVehicleCapacityInternal( const std::vector& pure_transits, const std::vector& dependent_transits, const Dimension* base_dimension, int64_t slack_max, std::vector vehicle_capacities, - bool fix_start_cumul_to_zero, const std::string& name) { + bool fix_start_cumul_to_zero, absl::string_view name) { CHECK_EQ(vehicles_, vehicle_capacities.size()); Dimension* new_dimension = nullptr; if (base_dimension == nullptr) { @@ -996,7 +993,7 @@ bool Model::AddDimensionDependentDimensionWithVehicleCapacityInternal( bool Model::AddDimensionDependentDimensionWithVehicleCapacity( int pure_transit, int dependent_transit, const Dimension* base_dimension, int64_t slack_max, int64_t vehicle_capacity, bool fix_start_cumul_to_zero, - const std::string& name) { + absl::string_view name) { std::vector pure_transits(vehicles_, pure_transit); std::vector dependent_transits(vehicles_, dependent_transit); std::vector vehicle_capacities(vehicles_, vehicle_capacity); @@ -1094,11 +1091,10 @@ DimensionIndex Model::GetDimensionIndex( const Dimension& Model::GetDimensionOrDie( absl::string_view dimension_name) const { - return *dimensions_[gtl::FindOrDie(dimension_name_to_index_, - std::string(dimension_name))]; + return *dimensions_[dimension_name_to_index_.at(dimension_name)]; } -Dimension* Model::GetMutableDimension(const std::string& dimension_name) const { +Dimension* Model::GetMutableDimension(absl::string_view dimension_name) const { const DimensionIndex index = GetDimensionIndex(dimension_name); if (index != kNoDimension) { return dimensions_[index]; @@ -1155,7 +1151,7 @@ ResourceGroup* Model::AddResourceGroup() { // Create and add the resource vars (the proper variable bounds and // constraints are set up when closing the model). resource_vars_.push_back({}); - solver_->MakeIntVarArray(vehicles(), -1, std::numeric_limits::max(), + solver_->MakeIntVarArray(vehicles(), -1, kint64max, absl::StrCat("Resources[", rg_index, "]"), &resource_vars_.back()); @@ -1397,8 +1393,8 @@ void Model::FinalizeAllowedVehicles() { // For each dimension, find the range of possible total transits. // This is precomputed to heuristically avoid a linear test on all vehicles. struct TransitBounds { - int64_t min = std::numeric_limits::max(); - int64_t max = std::numeric_limits::min(); + int64_t min = kint64max; + int64_t max = kint64min; }; std::vector dimension_bounds(unary_dimensions.size()); for (int d = 0; d < unary_dimensions.size(); ++d) { @@ -1440,13 +1436,9 @@ void Model::FinalizeAllowedVehicles() { } for (int node = 0; node < Size(); ++node) { if (IsStart(node)) continue; - absl::flat_hash_set& allowed_vehicles = allowed_vehicles_[node]; - // NOTE: An empty set of "allowed_vehicles" actually means all - // vehicles are allowed for this node, so we lazily fill - // "allowed_vehicles" to [-1, num_vehicles) when removing a vehicle. - + LazyMonotonicSet& allowed_vehicles = allowed_vehicles_[node]; // The vehicle is already forbidden for this node. - if (!allowed_vehicles.empty() && !allowed_vehicles.contains(vehicle)) { + if (!allowed_vehicles.Contains(vehicle)) { continue; } // If the transit is within the allowed range, we can keep the vehicle. @@ -1456,22 +1448,61 @@ void Model::FinalizeAllowedVehicles() { CapAdd(transit, slack_var->Min()) <= allowed_transits.max) { continue; } - // We will remove the vehicle, lazy fill. - if (allowed_vehicles.empty()) { - allowed_vehicles.reserve(vehicles_); - for (int v = 0; v < vehicles_; ++v) allowed_vehicles.insert(v); - } - allowed_vehicles.erase(vehicle); - if (allowed_vehicles.empty()) { - // If after erasing 'vehicle', allowed_vehicles becomes empty, it - // means no vehicle is allowed for this node, so we insert the value - // -1 in allowed_vehicles to distinguish with an empty - // allowed_vehicles which actually means all vehicles allowed. - allowed_vehicles.insert(-1); - } + allowed_vehicles.Erase(vehicle); } } } + // For each vehicle-node pair, find whether dimension cumuls are outside + // vehicle dimension cumul bounds. + auto prune_vehicle_domains = + [this](std::vector> vehicle_values, + std::vector> node_values) { + absl::c_sort(vehicle_values); + absl::c_sort(node_values); + int vehicle_idx = 0; + int node_idx = 0; + SparseBitset<> forbidden_vehicles(vehicles_); + while (node_idx < node_values.size()) { + if (vehicle_idx < vehicle_values.size() && + vehicle_values[vehicle_idx].first < node_values[node_idx].first) { + forbidden_vehicles.Set(vehicle_values[vehicle_idx].second); + vehicle_idx++; + } else { + LazyMonotonicSet& allowed_vehicles = + allowed_vehicles_[node_values[node_idx].second]; + for (int vehicle : forbidden_vehicles.PositionsSetAtLeastOnce()) { + allowed_vehicles.Erase(vehicle); + } + node_idx++; + } + } + }; + for (Dimension* dimension : dimensions_) { + const size_t num_cumuls = dimension->cumuls_.size(); + std::vector> vehicle_end_max; + for (int i = 0; i < vehicles_; ++i) { + if (!dimension->AreVehicleTransitsPositive(i)) continue; + vehicle_end_max.emplace_back(dimension->CumulVar(End(i))->Max(), i); + } + std::vector> node_start_min(num_cumuls); + for (int i = 0; i < num_cumuls; ++i) { + node_start_min[i] = {dimension->cumuls_[i]->Min(), i}; + } + prune_vehicle_domains(std::move(vehicle_end_max), + std::move(node_start_min)); + std::vector> vehicle_neg_start_min; + for (int i = 0; i < vehicles_; ++i) { + if (!dimension->AreVehicleTransitsPositive(i)) continue; + vehicle_neg_start_min.emplace_back(-dimension->CumulVar(Start(i))->Min(), + i); + } + std::vector> node_neg_start_max(num_cumuls); + for (int i = 0; i < num_cumuls; ++i) { + node_neg_start_max[i] = {-dimension->cumuls_[i]->Max(), i}; + } + prune_vehicle_domains(std::move(vehicle_neg_start_min), + std::move(node_neg_start_max)); + } } // static @@ -1904,8 +1935,7 @@ void Model::FinalizePrecedences() { std::vector in_degree(Size(), 0); SparseBitset<> nodes_in_precedences(Size()); std::vector> successors(Size()); - std::vector node_max_offset(Size(), - std::numeric_limits::min()); + std::vector node_max_offset(Size(), kint64min); // Note: A precedence constraint between first_node and second_node with an // offset enforces cumuls(second_node) >= cumuls(first_node) + offset. for (const auto [first_node, second_node, offset, unused] : @@ -1942,25 +1972,116 @@ DisjunctionIndex Model::AddDisjunction( CHECK_NE(kUnassigned, indices[i]); } - const DisjunctionIndex disjunction_index(disjunctions_.size()); - disjunctions_.push_back( - {indices, {penalty, max_cardinality, penalty_cost_behavior}}); - for (const int64_t index : indices) { - index_to_disjunctions_[index].push_back(disjunction_index); + DisjunctionIndex disj_index = MakeDisjunction(indices); + SetDisjunctionHardMaximum(disj_index, max_cardinality); + // Set min == max if penalty is < 0 or int max, otherwise soft_min = max. + if (penalty < 0 || penalty == kint64max) { + SetDisjunctionHardMinimum(disj_index, max_cardinality); + } else { + SetDisjunctionSoftMinimum(disj_index, max_cardinality, penalty, + penalty_cost_behavior); } - return disjunction_index; + return disj_index; } -bool Model::HasMandatoryDisjunctions() const { - for (const auto& [indices, value] : disjunctions_) { - if (value.penalty == kNoPenalty) return true; +DisjunctionIndex Model::MakeDisjunction(const std::vector& indices) { + const DisjunctionIndex disj_index(disjunctions_.size()); + disjunctions_.push_back({}); + disjunctions_.back().indices = indices; + disjunctions_.back().max_cardinality = indices.size(); + for (const int64_t index : indices) { + index_to_disjunctions_[index].push_back(disj_index); } - return false; + return disj_index; +} + +void Model::SetDisjunctionHardMinimum(DisjunctionIndex disj_index, + int64_t min_cardinality) { + Disjunction& disj = disjunctions_[disj_index]; + DCHECK_GE(min_cardinality, 0); + DCHECK_LE(min_cardinality, disj.indices.size()); + disj.min_cardinality = min_cardinality; +} + +void Model::SetDisjunctionSoftMinimum( + DisjunctionIndex disj_index, int64_t soft_min_cardinality, int64_t penalty, + PenaltyCostBehavior penalty_cost_behavior) { + CHECK_GE(penalty, 0); + Disjunction& disj = disjunctions_[disj_index]; + DCHECK_GE(soft_min_cardinality, 0); + DCHECK_LE(soft_min_cardinality, disj.indices.size()); + disj.soft_min_cardinality = soft_min_cardinality; + disj.soft_min_penalty = penalty; + disj.soft_min_penalty_type = penalty_cost_behavior; +} + +void Model::SetDisjunctionHardMaximum(DisjunctionIndex disj_index, + int64_t max_cardinality) { + Disjunction& disj = disjunctions_[disj_index]; + DCHECK_GE(max_cardinality, 0); + DCHECK_LE(max_cardinality, disj.indices.size()); + disj.max_cardinality = max_cardinality; +} + +void Model::SetDisjunctionSoftMaximum( + DisjunctionIndex disj_index, int64_t soft_max_cardinality, int64_t penalty, + PenaltyCostBehavior penalty_cost_behavior) { + CHECK_GE(penalty, 0); + Disjunction& disj = disjunctions_[disj_index]; + DCHECK_GE(soft_max_cardinality, 0); + DCHECK_LE(soft_max_cardinality, disj.indices.size()); + disj.soft_max_cardinality = soft_max_cardinality; + disj.soft_max_penalty = penalty; + disj.soft_max_penalty_type = penalty_cost_behavior; +} + +int64_t Model::GetDisjunctionMaxCardinality(DisjunctionIndex index) const { + return disjunctions_[index].max_cardinality; +} + +int64_t Model::GetDisjunctionMinCardinality(DisjunctionIndex index) const { + return disjunctions_[index].min_cardinality; +} + +int64_t Model::GetDisjunctionSoftMaxCardinality(DisjunctionIndex index) const { + return disjunctions_[index].soft_max_cardinality; +} + +int64_t Model::GetDisjunctionSoftMinCardinality(DisjunctionIndex index) const { + return disjunctions_[index].soft_min_cardinality; } -bool Model::HasMaxCardinalityConstrainedDisjunctions() const { - for (const auto& [indices, value] : disjunctions_) { - if (indices.size() > value.max_cardinality) return true; +int64_t Model::GetDisjunctionSoftMaxPenalty(DisjunctionIndex index) const { + return disjunctions_[index].soft_max_penalty; +} + +int64_t Model::GetDisjunctionSoftMinPenalty(DisjunctionIndex index) const { + return disjunctions_[index].soft_min_penalty; +} + +int64_t Model::GetDisjunctionPenalty(DisjunctionIndex index) const { + const Disjunction& disj = disjunctions_[index]; + return disj.min_cardinality > 0 ? kNoPenalty : disj.soft_min_penalty; +} + +Model::PenaltyCostBehavior Model::GetDisjunctionSoftMinPenaltyCostBehavior( + DisjunctionIndex index) const { + return disjunctions_[index].soft_min_penalty_type; +} + +Model::PenaltyCostBehavior Model::GetDisjunctionSoftMaxPenaltyCostBehavior( + DisjunctionIndex index) const { + return disjunctions_[index].soft_max_penalty_type; +} + +int Model::GetNumberOfDisjunctions() const { return disjunctions_.size(); } + +bool Model::HasHardDisjunctions() const { + for (const Disjunction& disj : disjunctions_) { + if (disj.min_cardinality > 0 || + disj.max_cardinality < disj.indices.size()) { + return true; + } } return false; } @@ -1968,11 +2089,10 @@ bool Model::HasMaxCardinalityConstrainedDisjunctions() const { std::vector> Model::GetPerfectBinaryDisjunctions() const { std::vector> var_index_pairs; - for (const Disjunction& disjunction : disjunctions_) { - const std::vector& var_indices = disjunction.indices; - if (var_indices.size() != 2) continue; - const int64_t v0 = var_indices[0]; - const int64_t v1 = var_indices[1]; + for (const Disjunction& disj : disjunctions_) { + if (disj.indices.size() != 2) continue; + const int64_t v0 = disj.indices[0]; + const int64_t v1 = disj.indices[1]; if (index_to_disjunctions_[v0].size() == 1 && index_to_disjunctions_[v1].size() == 1) { // We output sorted pairs. @@ -1994,50 +2114,64 @@ void Model::IgnoreDisjunctionsAlreadyForcedToZero() { } } if (!has_one_potentially_active_var) { - disjunction.value.max_cardinality = 0; + disjunction.max_cardinality = 0; + disjunction.min_cardinality = 0; + disjunction.soft_max_cardinality = 0; + disjunction.soft_min_cardinality = 0; } } } IntVar* Model::CreateDisjunction(DisjunctionIndex disjunction) { - const std::vector& indices = disjunctions_[disjunction].indices; - const int indices_size = indices.size(); - std::vector disjunction_vars(indices_size); - for (int i = 0; i < indices_size; ++i) { - const int64_t index = indices[i]; - CHECK_LT(index, Size()); - disjunction_vars[i] = ActiveVar(index); - } - const int64_t max_cardinality = - disjunctions_[disjunction].value.max_cardinality; - - IntVar* number_active_vars = solver_->MakeIntVar(0, max_cardinality); - solver_->AddConstraint( - solver_->MakeSumEquality(disjunction_vars, number_active_vars)); - - const int64_t penalty = disjunctions_[disjunction].value.penalty; - // If penalty is negative, then disjunction is mandatory - // i.e. number of active vars must be equal to max cardinality. - if (penalty < 0) { - solver_->AddConstraint( - solver_->MakeEquality(number_active_vars, max_cardinality)); - return nullptr; + const Disjunction& disj = disjunctions_[disjunction]; + std::vector active_vars; + active_vars.reserve(disj.indices.size()); + for (const int64_t node : disj.indices) { + CHECK_LT(node, Size()); + active_vars.push_back(ActiveVar(node)); + } + IntVar* num_actives_var = solver_->MakeSum(active_vars)->Var(); + num_actives_var->SetRange(disj.min_cardinality, disj.max_cardinality); + + std::vector penalty_vars; + std::vector penalty_weights; + + if (disj.soft_min_penalty > 0 && + disj.soft_min_cardinality > disj.min_cardinality) { + IntVar* num_violations = nullptr; + if (disj.soft_min_penalty_type == PenaltyCostBehavior::PENALIZE_ONCE || + disj.min_cardinality + 1 == disj.soft_min_cardinality) { + num_violations = + solver_->MakeIsLessCstVar(num_actives_var, disj.soft_min_cardinality); + } else { + IntExpr* diff = + solver_->MakeDifference(disj.soft_min_cardinality, num_actives_var); + num_violations = solver_->MakeMax(diff, 0)->Var(); + } + penalty_vars.push_back(num_violations); + penalty_weights.push_back(disj.soft_min_penalty); + } + + if (disj.soft_max_penalty > 0 && + disj.soft_max_cardinality < disj.max_cardinality) { + IntVar* num_violations = nullptr; + if (disj.soft_max_penalty_type == PenaltyCostBehavior::PENALIZE_ONCE || + disj.soft_max_cardinality + 1 == disj.max_cardinality) { + num_violations = solver_->MakeIsGreaterCstVar(num_actives_var, + disj.soft_max_cardinality); + } else { + IntExpr* diff = + solver_->MakeSum(num_actives_var, -disj.soft_max_cardinality); + num_violations = solver_->MakeMax(diff, 0)->Var(); + } + penalty_vars.push_back(num_violations); + penalty_weights.push_back(disj.soft_max_penalty); } - const PenaltyCostBehavior penalty_cost_behavior = - disjunctions_[disjunction].value.penalty_cost_behavior; - if (max_cardinality == 1 || - penalty_cost_behavior == PenaltyCostBehavior::PENALIZE_ONCE) { - IntVar* penalize_var = solver_->MakeBoolVar(); - solver_->AddConstraint(solver_->MakeIsDifferentCstCt( - number_active_vars, max_cardinality, penalize_var)); - return solver_->MakeProd(penalize_var, penalty)->Var(); + if (penalty_vars.empty()) { + return nullptr; } else { - IntVar* number_no_active_vars = solver_->MakeIntVar(0, max_cardinality); - solver_->AddConstraint(solver_->MakeEquality( - number_no_active_vars, - solver_->MakeDifference(max_cardinality, number_active_vars))); - return solver_->MakeProd(number_no_active_vars, penalty)->Var(); + return solver_->MakeScalProd(penalty_vars, penalty_weights)->Var(); } } @@ -2052,11 +2186,7 @@ void Model::AddSoftSameVehicleConstraint(std::vector indices, void Model::SetAllowedVehiclesForIndex(absl::Span vehicles, int64_t index) { DCHECK(!closed_); - auto& allowed_vehicles = allowed_vehicles_[index]; - allowed_vehicles.clear(); - for (int vehicle : vehicles) { - allowed_vehicles.insert(vehicle); - } + allowed_vehicles_[index].Reset(vehicles); } void Model::AddPickupAndDelivery(int64_t pickup, int64_t delivery) { @@ -2231,8 +2361,7 @@ void Model::AppendHomogeneousArcCosts(const RoutingSearchParameters& parameters, // Only supporting positive costs. // TODO(user): Detect why changing lower bound to kint64min stalls // the search in GLS in some cases (Solomon instances for instance). - IntVar* const base_cost_var = - solver_->MakeIntVar(0, std::numeric_limits::max()); + IntVar* const base_cost_var = solver_->MakeIntVar(0, kint64max); solver_->AddConstraint(solver_->MakeLightElement( arc_cost_evaluator, base_cost_var, nexts_[node_index], [this]() { return enable_deep_serialization_; })); @@ -2256,8 +2385,7 @@ void Model::AppendArcCosts(const RoutingSearchParameters& parameters, // Only supporting positive costs. // TODO(user): Detect why changing lower bound to kint64min stalls // the search in GLS in some cases (Solomon instances for instance). - IntVar* const base_cost_var = - solver_->MakeIntVar(0, std::numeric_limits::max()); + IntVar* const base_cost_var = solver_->MakeIntVar(0, kint64max); solver_->AddConstraint(solver_->MakeLightElement( [this, node_index](int64_t to, int64_t vehicle) { return GetArcCostForVehicle(node_index, to, vehicle); @@ -2567,9 +2695,8 @@ void Model::CloseModelWithParameters( closed_ = true; // Setup the time limit to be able to check it while closing the model. - GetOrCreateLimit()->UpdateLimits( - GetTimeLimit(parameters), std::numeric_limits::max(), - std::numeric_limits::max(), parameters.solution_limit()); + GetOrCreateLimit()->UpdateLimits(GetTimeLimit(parameters), kint64max, + kint64max, parameters.solution_limit()); for (Dimension* const dimension : dimensions_) { dimension->CloseModel(UsesLightPropagation(parameters)); @@ -2622,11 +2749,11 @@ void Model::CloseModelWithParameters( // Reduce domains of vehicle variables. for (int i = 0; i < allowed_vehicles_.size(); ++i) { const auto& allowed_vehicles = allowed_vehicles_[i]; - if (!allowed_vehicles.empty()) { + if (allowed_vehicles.Touched()) { std::vector vehicles; - vehicles.reserve(allowed_vehicles.size() + 1); + vehicles.reserve(allowed_vehicles.Values().size() + 1); vehicles.push_back(-1); - for (int vehicle : allowed_vehicles) { + for (int vehicle : allowed_vehicles.Values()) { vehicles.push_back(vehicle); } solver_->AddConstraint(solver_->MakeMemberCt(VehicleVar(i), vehicles)); @@ -2661,9 +2788,9 @@ void Model::CloseModelWithParameters( const std::vector& disjunctions = GetDisjunctionIndices(i); bool is_mandatory = disjunctions.empty(); - for (const DisjunctionIndex& disjunction : disjunctions) { - if (GetDisjunctionNodeIndices(disjunction).size() == 1 && - GetDisjunctionPenalty(disjunction) == kNoPenalty) { + for (const DisjunctionIndex& index : disjunctions) { + const Disjunction& disj = disjunctions_[index]; + if (disj.indices.size() == disj.min_cardinality) { is_mandatory = true; break; } @@ -2783,9 +2910,7 @@ void Model::CloseModelWithParameters( std::any_of(slack_costs.begin(), slack_costs.end(), [](int64_t coeff) { return coeff != 0; }) || std::any_of(span_ubs.begin(), span_ubs.end(), - [](int64_t value) { - return value < std::numeric_limits::max(); - }) || + [](int64_t value) { return value < kint64max; }) || std::any_of( rg_indices.begin(), rg_indices.end(), [this, dimension](int64_t rg_index) { @@ -2796,8 +2921,7 @@ void Model::CloseModelWithParameters( for (const ResourceGroup::Resource& resource : resource_group.GetResources()) { if (resource.GetDimensionAttributes(dimension->index()) - .span_upper_bound() < - std::numeric_limits::max()) { + .span_upper_bound() < kint64max) { return true; } } @@ -2811,7 +2935,7 @@ void Model::CloseModelWithParameters( std::vector total_slacks(vehicles(), nullptr); // Generate variables only where needed. for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { - if (span_ubs[vehicle] < std::numeric_limits::max()) { + if (span_ubs[vehicle] < kint64max) { spans[vehicle] = solver_->MakeIntVar(0, span_ubs[vehicle], ""); } if (span_costs[vehicle] != 0 || slack_costs[vehicle] != 0) { @@ -2851,13 +2975,12 @@ void Model::CloseModelWithParameters( for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { if (!spans[vehicle] && !total_slacks[vehicle]) continue; if (spans[vehicle]) { - AddVariableTargetToFinalizer(spans[vehicle], - std::numeric_limits::min()); + AddVariableTargetToFinalizer(spans[vehicle], kint64min); } AddVariableTargetToFinalizer(dimension->CumulVar(End(vehicle)), - std::numeric_limits::min()); + kint64min); AddVariableTargetToFinalizer(dimension->CumulVar(Start(vehicle)), - std::numeric_limits::max()); + kint64max); } // Add costs of variables. for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { @@ -2880,9 +3003,7 @@ void Model::CloseModelWithParameters( for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { const auto bound_cost = dimension->GetSoftSpanUpperBoundForVehicle(vehicle); - if (bound_cost.cost == 0 || - bound_cost.bound == std::numeric_limits::max()) - continue; + if (bound_cost.cost == 0 || bound_cost.bound == kint64max) continue; DCHECK(spans[vehicle] != nullptr); // Additional cost is vehicle_cost_considered_[vehicle] * // max(0, spans[vehicle] - bound_cost.bound) * bound_cost.cost. @@ -2905,9 +3026,7 @@ void Model::CloseModelWithParameters( for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { const auto bound_cost = dimension->GetQuadraticCostSoftSpanUpperBoundForVehicle(vehicle); - if (bound_cost.cost == 0 || - bound_cost.bound == std::numeric_limits::max()) - continue; + if (bound_cost.cost == 0 || bound_cost.bound == kint64max) continue; DCHECK(spans[vehicle] != nullptr); // Additional cost is vehicle_cost_considered_[vehicle] * // max(0, spans[vehicle] - bound_cost.bound)^2 * bound_cost.cost. @@ -3048,7 +3167,7 @@ void Model::CloseModelWithParameters( for (const Dimension* const dimension : dimensions_) { // Dimension path precedences, discovered by model inspection (which must be // performed before adding path transit precedences). - const ReverseArcListGraph& graph = + const util::ReverseArcListGraph& graph = dimension->GetPathPrecedenceGraph(); std::vector> path_precedences; for (const auto tail : graph.AllNodes()) { @@ -3289,12 +3408,10 @@ bool Model::UpdateLimits(const RoutingSearchParameters& parameters, ? absl::ZeroDuration() : parameters.secondary_ls_time_limit_ratio() * time_left; const absl::Duration time_limit = time_left - secondary_solve_buffer; - limit_->UpdateLimits(time_limit, std::numeric_limits::max(), - std::numeric_limits::max(), + limit_->UpdateLimits(time_limit, kint64max, kint64max, parameters.solution_limit()); DCHECK_NE(ls_limit_, nullptr); - ls_limit_->UpdateLimits(time_limit, std::numeric_limits::max(), - std::numeric_limits::max(), 1); + ls_limit_->UpdateLimits(time_limit, kint64max, kint64max, 1); // TODO(user): Come up with a better formula. Ideally this should be // calibrated in the first solution strategies. time_buffer_ = std::min(absl::Seconds(1), time_limit * 0.05); @@ -3345,9 +3462,8 @@ const Assignment* Model::FastSolveFromAssignmentWithParameters( InitializeSearch(search_parameters); if (!start_time_ms.has_value()) return nullptr; if (assignment == nullptr) return nullptr; - limit_->UpdateLimits( - GetTimeLimit(search_parameters), std::numeric_limits::max(), - std::numeric_limits::max(), search_parameters.solution_limit()); + limit_->UpdateLimits(GetTimeLimit(search_parameters), kint64max, kint64max, + search_parameters.solution_limit()); std::vector monitors = {metaheuristic_}; if (search_log_ != nullptr) monitors.push_back(search_log_); Assignment* solution = solver()->RunUncheckedLocalSearch( @@ -3471,9 +3587,8 @@ const Assignment* Model::SolveFromAssignmentsWithParameters( status_ = RoutingSearchStatus::ROUTING_FAIL_TIMEOUT; return nullptr; } - lns_limit_->UpdateLimits( - GetLnsTimeLimit(parameters), std::numeric_limits::max(), - std::numeric_limits::max(), std::numeric_limits::max()); + lns_limit_->UpdateLimits(GetLnsTimeLimit(parameters), kint64max, kint64max, + kint64max); // NOTE: Allow more time for the first solution's scheduling, since if it // fails, we won't have anything to build upon. // We set this time limit based on whether local/global dimension optimizers @@ -3484,9 +3599,8 @@ const Assignment* Model::SolveFromAssignmentsWithParameters( const absl::Duration first_solution_lns_time_limit = std::max(GetTimeLimit(parameters) / time_limit_shares, GetLnsTimeLimit(parameters)); - first_solution_lns_limit_->UpdateLimits( - first_solution_lns_time_limit, std::numeric_limits::max(), - std::numeric_limits::max(), std::numeric_limits::max()); + first_solution_lns_limit_->UpdateLimits(first_solution_lns_time_limit, + kint64max, kint64max, kint64max); std::vector> solution_pool; std::vector first_solution_assignments; @@ -3662,7 +3776,7 @@ const Assignment* Model::SolveFromAssignmentsWithParameters( const Assignment* Model::SolveWithIteratedLocalSearch( const RoutingSearchParameters& parameters) { - DCHECK(parameters.use_iterated_local_search()); + DCHECK(parameters.has_iterated_local_search_parameters()); if (nodes() == 0) { return nullptr; @@ -3709,12 +3823,10 @@ const Assignment* Model::SolveWithIteratedLocalSearch( if (time_left < absl::ZeroDuration()) { return false; } - limit_->UpdateLimits(time_left, std::numeric_limits::max(), - std::numeric_limits::max(), + limit_->UpdateLimits(time_left, kint64max, kint64max, parameters.solution_limit()); DCHECK_NE(ls_limit_, nullptr); - ls_limit_->UpdateLimits(time_left, std::numeric_limits::max(), - std::numeric_limits::max(), 1); + ls_limit_->UpdateLimits(time_left, kint64max, kint64max, 1); // TODO(user): Come up with a better formula. Ideally this should be // calibrated in the first solution strategies. time_buffer_ = std::min(absl::Seconds(1), time_left * 0.05); @@ -4450,8 +4562,7 @@ int64_t Model::GetArcCostForFirstSolution(int64_t from_index, nexts_, active_, is_bound_to_end_, zero_transit)); is_bound_to_end_ct_added_.Switch(solver_.get()); } - if (is_bound_to_end_[to_index]->Min() == 1) - return std::numeric_limits::max(); + if (is_bound_to_end_[to_index]->Min() == 1) return kint64max; // TODO(user): Take vehicle into account. return GetHomogeneousCost(from_index, to_index); } @@ -4746,16 +4857,16 @@ int64_t Model::UnperformedPenalty(int64_t var_index) const { int64_t Model::UnperformedPenaltyOrValue(int64_t default_value, int64_t var_index) const { - if (active_[var_index]->Min() == 1) - return std::numeric_limits::max(); // Forced active. + if (active_[var_index]->Min() == 1) return kint64max; // Forced active. const std::vector& disjunction_indices = GetDisjunctionIndices(var_index); if (disjunction_indices.size() != 1) return default_value; - const DisjunctionIndex disjunction_index = disjunction_indices[0]; + const Disjunction& disj = disjunctions_[disjunction_indices[0]]; // The disjunction penalty can be kNoPenalty iff there is more than one node // in the disjunction; otherwise we would have caught it earlier (the node // would be forced active). - return std::max(int64_t{0}, disjunctions_[disjunction_index].value.penalty); + return std::max(int64_t{0}, + disj.soft_min_cardinality > 0 ? disj.soft_min_penalty : 0); } std::string Model::DebugOutputAssignment( @@ -4852,51 +4963,45 @@ Assignment* Model::GetOrCreateTmpAssignment() { RegularLimit* Model::GetOrCreateLimit() { if (limit_ == nullptr) { - limit_ = solver_->MakeLimit( - absl::InfiniteDuration(), std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), /*smart_time_check=*/true); + limit_ = solver_->MakeLimit(absl::InfiniteDuration(), kint64max, kint64max, + kint64max, /*smart_time_check=*/true); } return limit_; } RegularLimit* Model::GetOrCreateCumulativeLimit() { if (cumulative_limit_ == nullptr) { - cumulative_limit_ = solver_->MakeLimit( - absl::InfiniteDuration(), std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), /*smart_time_check=*/true, - /*cumulative=*/true); + cumulative_limit_ = + solver_->MakeLimit(absl::InfiniteDuration(), kint64max, kint64max, + kint64max, /*smart_time_check=*/true, + /*cumulative=*/true); } return cumulative_limit_; } RegularLimit* Model::GetOrCreateLocalSearchLimit() { if (ls_limit_ == nullptr) { - ls_limit_ = solver_->MakeLimit(absl::InfiniteDuration(), - std::numeric_limits::max(), - std::numeric_limits::max(), - /*solutions=*/1, /*smart_time_check=*/true); + ls_limit_ = + solver_->MakeLimit(absl::InfiniteDuration(), kint64max, kint64max, + /*solutions=*/1, /*smart_time_check=*/true); } return ls_limit_; } RegularLimit* Model::GetOrCreateLargeNeighborhoodSearchLimit() { if (lns_limit_ == nullptr) { - lns_limit_ = solver_->MakeLimit( - absl::InfiniteDuration(), std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), /*smart_time_check=*/false); + lns_limit_ = + solver_->MakeLimit(absl::InfiniteDuration(), kint64max, kint64max, + kint64max, /*smart_time_check=*/false); } return lns_limit_; } RegularLimit* Model::GetOrCreateFirstSolutionLargeNeighborhoodSearchLimit() { if (first_solution_lns_limit_ == nullptr) { - first_solution_lns_limit_ = solver_->MakeLimit( - absl::InfiniteDuration(), std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), /*smart_time_check=*/false); + first_solution_lns_limit_ = + solver_->MakeLimit(absl::InfiniteDuration(), kint64max, kint64max, + kint64max, /*smart_time_check=*/false); } return first_solution_lns_limit_; } @@ -5051,8 +5156,7 @@ void Model::CreateNeighborhoodOperators( // Only add disjunctions of cardinality 1 and of size > 1, as // SwapActiveToShortestPathOperator and TwoOptWithShortestPathOperator only // support DAGs, and don't care about chain-DAGS. - if (disjunction.value.max_cardinality == 1 && - disjunction.indices.size() > 1) { + if (disjunction.max_cardinality == 1 && disjunction.indices.size() > 1) { alternative_sets.push_back(disjunction.indices); } } @@ -5252,9 +5356,8 @@ LocalSearchOperator* Model::GetNeighborhoodOperators( CP_ROUTING_PUSH_OPERATOR(EXCHANGE, exchange); CP_ROUTING_PUSH_OPERATOR(CROSS, cross); } - if (!pickup_delivery_pairs_.empty() || - search_parameters.local_search_operators().use_relocate_neighbors() == - BOOL_TRUE) { + if (search_parameters.local_search_operators().use_relocate_neighbors() == + BOOL_TRUE) { operators.push_back(local_search_operators_[RELOCATE_NEIGHBORS]); } const LocalSearchMetaheuristic::Value local_search_metaheuristic = @@ -5485,8 +5588,7 @@ Model::CreateLocalSearchFilters(const RoutingSearchParameters& parameters, } if (!disjunctions_.empty()) { - if (options.filter_objective || HasMandatoryDisjunctions() || - HasMaxCardinalityConstrainedDisjunctions()) { + if (options.filter_objective || HasHardDisjunctions()) { filter_events.push_back( {MakeNodeDisjunctionFilter(*this, options.filter_objective), kAccept, priority}); @@ -5746,8 +5848,7 @@ void Model::StoreDimensionCumulOptimizers( if (!AllTransitsPositive(*dimension)) { dimension->SetOffsetForGlobalOptimizer(0); } else { - int64_t offset = - vehicles() == 0 ? 0 : std::numeric_limits::max(); + int64_t offset = vehicles() == 0 ? 0 : kint64max; for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { DCHECK_GE(dimension->CumulVar(Start(vehicle))->Min(), 0); offset = @@ -5773,8 +5874,7 @@ void Model::StoreDimensionCumulOptimizers( if (dimension->GetSpanCostCoefficientForVehicle(vehicle) > 0) { has_span_cost = true; } - if (dimension->GetSpanUpperBoundForVehicle(vehicle) < - std::numeric_limits::max()) { + if (dimension->GetSpanUpperBoundForVehicle(vehicle) < kint64max) { has_span_limit = true; } DCHECK_GE(dimension->CumulVar(Start(vehicle))->Min(), 0); @@ -5964,6 +6064,23 @@ DecisionBuilder* Model::CreateSolutionFinalizer( return solver_->Compose(decision_builders); } +namespace { +class ResetLSOperators : public DecisionBuilder { + public: + explicit ResetLSOperators(LocalSearchOperator* ls_operators) + : ls_operators_(ls_operators) {} + ~ResetLSOperators() override = default; + Decision* Next(Solver* /*solver*/) override { + ls_operators_->Reset(); + return nullptr; + } + std::string DebugString() const override { return "ResetLSOperators"; } + + private: + LocalSearchOperator* const ls_operators_; +}; +} // namespace + void Model::CreateFirstSolutionDecisionBuilders( const RoutingSearchParameters& search_parameters) { first_solution_decision_builders_.resize( @@ -6047,14 +6164,20 @@ void Model::CreateFirstSolutionDecisionBuilders( MakeAllUnperformed(this); // Best insertion heuristic. RegularLimit* const ls_limit = solver_->MakeLimit( - GetTimeLimit(search_parameters), std::numeric_limits::max(), - std::numeric_limits::max(), std::numeric_limits::max(), + GetTimeLimit(search_parameters), kint64max, kint64max, kint64max, /*smart_time_check=*/true); + LocalSearchOperator* const insertion_operator = CreateInsertionOperator(); + // BEST_INSERTION will perform best accept local search using insertion + // operators. As of 04/2026, some of these operators keep track of optimal + // sub-spaces which need to be cleared for best accept. + DecisionBuilder* const reset_ls_operators = + solver_->RevAlloc(new ResetLSOperators(insertion_operator)); DecisionBuilder* const finalize = solver_->MakeSolveOnce( - finalize_solution, GetOrCreateLargeNeighborhoodSearchLimit()); + solver_->Compose(finalize_solution, reset_ls_operators), + GetOrCreateLargeNeighborhoodSearchLimit()); LocalSearchPhaseParameters* const insertion_parameters = solver_->MakeLocalSearchPhaseParameters( - nullptr, CreateInsertionOperator(), finalize, ls_limit, + nullptr, insertion_operator, finalize, ls_limit, GetOrCreateLocalSearchFilterManager( search_parameters, {/*filter_objective=*/true, /*filter_with_cp_solver=*/false})); @@ -6255,7 +6378,8 @@ void Model::CreateFirstSolutionDecisionBuilders( [](Dimension* dim) { return !dim->GetNodePrecedences().empty(); }); bool has_single_vehicle_node = false; for (int node = 0; node < Size(); node++) { - if (!IsStart(node) && !IsEnd(node) && allowed_vehicles_[node].size() == 1) { + if (!IsStart(node) && !IsEnd(node) && + allowed_vehicles_[node].Values().size() == 1) { has_single_vehicle_node = true; break; } @@ -6443,8 +6567,7 @@ void Model::SetupMetaheuristics( // Some metaheuristics will effectively never terminate; warn // user if they fail to set a time limit. bool limit_too_long = !search_parameters.has_time_limit() && - search_parameters.solution_limit() == - std::numeric_limits::max(); + search_parameters.solution_limit() == kint64max; const int64_t optimization_step = std::max( MathUtil::SafeRound(search_parameters.optimization_step()), One()); @@ -6752,7 +6875,7 @@ const char ModelVisitor::kLightElement2[] = "LightElement2"; const char ModelVisitor::kRemoveValues[] = "RemoveValues"; Dimension::Dimension(Model* model, std::vector vehicle_capacities, - const std::string& name, const Dimension* base_dimension) + absl::string_view name, const Dimension* base_dimension) : vehicle_capacities_(std::move(vehicle_capacities)), base_dimension_(base_dimension), global_span_cost_coefficient_(0), @@ -6761,15 +6884,14 @@ Dimension::Dimension(Model* model, std::vector vehicle_capacities, name_(name), global_optimizer_offset_(0) { CHECK(model != nullptr); - vehicle_span_upper_bounds_.assign(model->vehicles(), - std::numeric_limits::max()); + vehicle_span_upper_bounds_.assign(model->vehicles(), kint64max); vehicle_span_cost_coefficients_.assign(model->vehicles(), 0); vehicle_slack_cost_coefficients_.assign(model->vehicles(), 0); vehicle_span_vars_.resize(model->vehicles(), nullptr); } Dimension::Dimension(Model* model, std::vector vehicle_capacities, - const std::string& name, SelfBased) + absl::string_view name, SelfBased) : Dimension(model, std::move(vehicle_capacities), name, this) {} Dimension::~Dimension() { cumul_var_piecewise_linear_cost_.clear(); } @@ -6803,8 +6925,7 @@ void Dimension::InitializeCumuls() { forbidden_intervals_.resize(size); capacity_vars_.clear(); if (min_capacity != max_capacity) { - solver->MakeIntVarArray(size, 0, std::numeric_limits::max(), - &capacity_vars_); + solver->MakeIntVarArray(size, 0, kint64max, &capacity_vars_); for (int i = 0; i < size; ++i) { IntVar* const capacity_var = capacity_vars_[i]; if (i < model_->Size()) { @@ -6869,7 +6990,7 @@ void Dimension::InitializeTransitVariables(int64_t slack_max) { } const bool is_unary = IsUnary(); for (int64_t i = 0; i < size; ++i) { - int64_t min_fixed_transit = std::numeric_limits::max(); + int64_t min_fixed_transit = kint64max; if (is_unary) { for (int evaluator_index : class_evaluators_) { const auto& unary_transit_callback = @@ -6879,11 +7000,11 @@ void Dimension::InitializeTransitVariables(int64_t slack_max) { std::min(min_fixed_transit, unary_transit_callback(i)); } } - fixed_transits_[i] = solver->MakeIntVar( - is_unary ? min_fixed_transit - : are_all_evaluators_positive ? int64_t{0} - : std::numeric_limits::min(), - std::numeric_limits::max(), absl::StrCat(transit_name, i)); + fixed_transits_[i] = + solver->MakeIntVar(is_unary ? min_fixed_transit + : are_all_evaluators_positive ? int64_t{0} + : kint64min, + kint64max, absl::StrCat(transit_name, i)); // Setting dependent_transits_[i]. if (base_dimension_ != nullptr) { if (state_dependent_class_evaluators_.size() == 1) { @@ -7237,8 +7358,7 @@ void TypeRegulationsConstraint::InitialPropagate() { void Dimension::CloseModel(bool use_light_propagation) { Solver* const solver = model_->solver(); const auto capacity_lambda = [this](int64_t vehicle) { - return vehicle >= 0 ? vehicle_capacities_[vehicle] - : std::numeric_limits::max(); + return vehicle >= 0 ? vehicle_capacities_[vehicle] : kint64max; }; for (int i = 0; i < capacity_vars_.size(); ++i) { IntVar* const vehicle_var = model_->VehicleVar(i); @@ -7580,8 +7700,7 @@ void Dimension::SetupGlobalSpanCost(std::vector* cost_elements) const { max_end_cumul, global_span_cost_coefficient_); std::vector start_cumuls; for (int i = 0; i < model_->vehicles(); ++i) { - IntVar* global_span_cost_start_cumul = - solver->MakeIntVar(0, std::numeric_limits::max()); + IntVar* global_span_cost_start_cumul = solver->MakeIntVar(0, kint64max); solver->AddConstraint(solver->MakeIfThenElseCt( model_->vehicle_route_considered_[i], cumuls_[model_->Start(i)], max_end_cumul, global_span_cost_start_cumul)); @@ -7663,16 +7782,16 @@ void Dimension::SetBreakIntervalsOfVehicle(std::vector breaks, model_->AddVariableTargetToFinalizer(interval->PerformedExpr()->Var(), 0); } model_->AddVariableTargetToFinalizer(interval->SafeStartExpr(0)->Var(), - std::numeric_limits::min()); + kint64min); model_->AddVariableTargetToFinalizer(interval->SafeDurationExpr(0)->Var(), - std::numeric_limits::min()); + kint64min); } // When a vehicle has breaks, if its start and end are fixed, // then propagation keeps the cumuls min and max on its path feasible. model_->AddVariableTargetToFinalizer(CumulVar(model_->End(vehicle)), - std::numeric_limits::min()); + kint64min); model_->AddVariableTargetToFinalizer(CumulVar(model_->Start(vehicle)), - std::numeric_limits::max()); + kint64max); } void Dimension::InitializeBreaks() { @@ -7718,9 +7837,9 @@ void Dimension::SetBreakDistanceDurationOfVehicle(int64_t distance, // When a vehicle has breaks, if its start and end are fixed, // then propagation keeps the cumuls min and max on its path feasible. model_->AddVariableTargetToFinalizer(CumulVar(model_->End(vehicle)), - std::numeric_limits::min()); + kint64min); model_->AddVariableTargetToFinalizer(CumulVar(model_->Start(vehicle)), - std::numeric_limits::max()); + kint64max); } const std::vector>& @@ -7750,13 +7869,13 @@ int64_t Dimension::GetPickupToDeliveryLimitForPair( DCHECK_GE(pair_index, 0); if (pair_index >= pickup_to_delivery_limits_per_pair_index_.size()) { - return std::numeric_limits::max(); + return kint64max; } const PickupToDeliveryLimitFunction& pickup_to_delivery_limit_function = pickup_to_delivery_limits_per_pair_index_[pair_index]; if (!pickup_to_delivery_limit_function) { // No limit function set for this pair. - return std::numeric_limits::max(); + return kint64max; } DCHECK_GE(pickup_alternative_index, 0); DCHECK_GE(delivery_alternative_index, 0); @@ -7796,13 +7915,12 @@ void Dimension::SetupSlackAndDependentTransitCosts() const { it != dimensions_with_relevant_slacks.rend(); ++it) { for (int i = 0; i < model_->vehicles(); ++i) { model_->AddVariableTargetToFinalizer((*it)->cumuls_[model_->End(i)], - std::numeric_limits::min()); + kint64min); model_->AddVariableTargetToFinalizer((*it)->cumuls_[model_->Start(i)], - std::numeric_limits::max()); + kint64max); } for (IntVar* const slack : (*it)->slacks_) { - model_->AddVariableTargetToFinalizer(slack, - std::numeric_limits::min()); + model_->AddVariableTargetToFinalizer(slack, kint64min); } } } diff --git a/ortools/routing/routing.h b/ortools/routing/routing.h index 49137d94592..00186b37d88 100644 --- a/ortools/routing/routing.h +++ b/ortools/routing/routing.h @@ -175,7 +175,9 @@ Keywords: Vehicle Routing, Traveling Salesman Problem, TSP, VRP, CVRPTW, PDP. #include "absl/strings/string_view.h" #include "absl/time/time.h" #include "absl/types/span.h" +#include "ortools/base/base_export.h" #include "ortools/base/strong_vector.h" +#include "ortools/base/types.h" #include "ortools/constraint_solver/assignment.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/local_search.h" @@ -198,12 +200,47 @@ class Dimension; class FinalizerVariables; class GlobalDimensionCumulOptimizer; class LocalDimensionCumulOptimizer; -#if !defined(SWIG) class IntVarFilteredDecisionBuilder; class RoutingFilteredDecisionBuilder; -using util::ReverseArcListGraph; class SweepArranger; -#endif // !defined(SWIG) + +// A set of integers that by default contains all the integers in [0, size) and +// only supports erasing values. The implementation delays the materialization +// of the set until the first call to `Erase()`. +class LazyMonotonicSet { + public: + explicit LazyMonotonicSet(int size) : size_(size), touched_(false) {} + LazyMonotonicSet(const LazyMonotonicSet& other) = default; + LazyMonotonicSet& operator=(const LazyMonotonicSet& other) = default; + void Reset(absl::Span values) { + set_.clear(); + touched_ = false; + set_.reserve(values.size()); + for (int value : values) { + set_.insert(value); + touched_ = true; + } + } + void Erase(int value) { + if (!touched_) { + set_.reserve(size_); + for (int i = 0; i < size_; ++i) set_.insert(i); + touched_ = true; + } + set_.erase(value); + } + bool Contains(int value) const { + if (!Touched()) return true; + return set_.contains(value); + } + bool Touched() const { return touched_; } + absl::flat_hash_set Values() const { return set_; } + + private: + int size_; + absl::flat_hash_set set_; + bool touched_; +}; class PathsMetadata { public: @@ -419,9 +456,8 @@ class OR_DLL Model { class Attributes { public: Attributes(); - Attributes( - Domain start_domain, Domain end_domain, - int64_t span_upper_bound = std::numeric_limits::max()); + Attributes(Domain start_domain, Domain end_domain, + int64_t span_upper_bound = kint64max); const Domain& start_domain() const { return start_domain_; } const Domain& end_domain() const { return end_domain_; } @@ -448,7 +484,7 @@ class OR_DLL Model { /// The span upper bound constrains the dimension span of the vehicle /// assigned to this resource: cumul[End(v)] - cumul[Start(v)] <= /// span_upper_bound - int64_t span_upper_bound_ = std::numeric_limits::max(); + int64_t span_upper_bound_ = kint64max; }; /// A Resource sets attributes (costs/constraints) for a set of dimensions. @@ -707,18 +743,18 @@ class OR_DLL Model { /// (and doesn't create the new dimension). /// Takes ownership of the callback 'evaluator'. bool AddDimension(int evaluator_index, int64_t slack_max, int64_t capacity, - bool fix_start_cumul_to_zero, const std::string& name); + bool fix_start_cumul_to_zero, absl::string_view name); bool AddDimensionWithVehicleTransits( const std::vector& evaluator_indices, int64_t slack_max, - int64_t capacity, bool fix_start_cumul_to_zero, const std::string& name); + int64_t capacity, bool fix_start_cumul_to_zero, absl::string_view name); bool AddDimensionWithVehicleCapacity(int evaluator_index, int64_t slack_max, std::vector vehicle_capacities, bool fix_start_cumul_to_zero, - const std::string& name); + absl::string_view name); bool AddDimensionWithVehicleTransitAndCapacity( const std::vector& evaluator_indices, int64_t slack_max, std::vector vehicle_capacities, bool fix_start_cumul_to_zero, - const std::string& name); + absl::string_view name); /// Creates a dimension where the transit variable on arc i->j is the sum of: /// - A "fixed" transit value, obtained from the fixed_evaluator_index for /// this vehicle, referencing evaluators in transit_evaluators_, and @@ -729,7 +765,7 @@ class OR_DLL Model { const std::vector& fixed_evaluator_indices, const std::vector& cumul_dependent_evaluator_indices, int64_t slack_max, std::vector vehicle_capacities, - bool fix_start_cumul_to_zero, const std::string& name); + bool fix_start_cumul_to_zero, absl::string_view name); /// Creates a dimension where the transit variable is constrained to be /// equal to 'value'; 'capacity' is the upper bound of the cumul variables. @@ -741,10 +777,10 @@ class OR_DLL Model { /// (and doesn't create the new dimension but still register a new callback). std::pair AddConstantDimensionWithSlack( int64_t value, int64_t capacity, int64_t slack_max, - bool fix_start_cumul_to_zero, const std::string& name); + bool fix_start_cumul_to_zero, absl::string_view name); std::pair AddConstantDimension(int64_t value, int64_t capacity, bool fix_start_cumul_to_zero, - const std::string& name) { + absl::string_view name) { return AddConstantDimensionWithSlack(value, capacity, 0, fix_start_cumul_to_zero, name); } @@ -760,7 +796,7 @@ class OR_DLL Model { std::pair AddVectorDimension(std::vector values, int64_t capacity, bool fix_start_cumul_to_zero, - const std::string& name); + absl::string_view name); /// Creates a dimension where the transit variable is constrained to be /// equal to 'values[i][next(i)]' for node i; 'capacity' is the upper bound of /// the cumul variables. 'name' is the name used to reference the dimension; @@ -772,7 +808,7 @@ class OR_DLL Model { /// (and doesn't create the new dimension but still register a new callback). std::pair AddMatrixDimension( std::vector /*needed_for_swig*/> values, - int64_t capacity, bool fix_start_cumul_to_zero, const std::string& name); + int64_t capacity, bool fix_start_cumul_to_zero, absl::string_view name); /// Creates a dimension with transits depending on the cumuls of another /// dimension. 'pure_transits' are the per-vehicle fixed transits as above. /// 'dependent_transits' is a vector containing for each vehicle an index to a @@ -784,7 +820,7 @@ class OR_DLL Model { const std::vector& dependent_transits, const Dimension* base_dimension, int64_t slack_max, std::vector vehicle_capacities, bool fix_start_cumul_to_zero, - const std::string& name) { + absl::string_view name) { return AddDimensionDependentDimensionWithVehicleCapacityInternal( pure_transits, dependent_transits, base_dimension, slack_max, std::move(vehicle_capacities), fix_start_cumul_to_zero, name); @@ -794,16 +830,16 @@ class OR_DLL Model { bool AddDimensionDependentDimensionWithVehicleCapacity( const std::vector& transits, const Dimension* base_dimension, int64_t slack_max, std::vector vehicle_capacities, - bool fix_start_cumul_to_zero, const std::string& name); + bool fix_start_cumul_to_zero, absl::string_view name); /// Homogeneous versions of the functions above. bool AddDimensionDependentDimensionWithVehicleCapacity( int transit, const Dimension* base_dimension, int64_t slack_max, int64_t vehicle_capacity, bool fix_start_cumul_to_zero, - const std::string& name); + absl::string_view name); bool AddDimensionDependentDimensionWithVehicleCapacity( int pure_transit, int dependent_transit, const Dimension* base_dimension, int64_t slack_max, int64_t vehicle_capacity, bool fix_start_cumul_to_zero, - const std::string& name); + absl::string_view name); /// Creates a cached StateDependentTransit from an std::function. static Model::StateDependentTransit MakeStateDependentTransit( @@ -850,7 +886,7 @@ class OR_DLL Model { const Dimension& GetDimensionOrDie(absl::string_view dimension_name) const; /// Returns a dimension from its name. Returns nullptr if the dimension does /// not exist. - Dimension* GetMutableDimension(const std::string& dimension_name) const; + Dimension* GetMutableDimension(absl::string_view dimension_name) const; /// Set the given dimension as "primary constrained". As of August 2013, this /// is only used by ArcIsMoreConstrainedThanArc(). /// "dimension" must be the name of an existing dimension, or be empty, in @@ -917,25 +953,59 @@ class OR_DLL Model { int64_t max_cardinality = 1, PenaltyCostBehavior penalty_cost_behavior = PenaltyCostBehavior::PENALIZE_ONCE); + /// Creates a disjunction without setting its maximum or minimum cardinality. + DisjunctionIndex MakeDisjunction(const std::vector& indices); + /// Sets the maximum number of active indices in the disjunction. It must be + /// in [0, num_indices]. + void SetDisjunctionHardMaximum(DisjunctionIndex disj_index, + int64_t max_cardinality); + /// Sets the minimum number of active indices in the disjunction. It must be + /// in [0, num_indices]. + void SetDisjunctionHardMinimum(DisjunctionIndex disj_index, + int64_t min_cardinality); + /// Sets the penalty and the soft maximum number of active indices in the + /// disjunction. Cardinality must be in [0, num_indices]. If n is the number + /// of active indices in the disjunction, the following penalty is added to + /// the cost function: + /// - if `penalty_cost_behavior` is `PENALIZE_PER_INACTIVE`: + /// `penalty * max(0, n - soft_max_cardinality)` + /// - if `penalty_cost_behavior` is `PENALIZE_ONCE`: + /// `penalty * (n > soft_max_cardinality ? 1 : 0)`. + void SetDisjunctionSoftMaximum(DisjunctionIndex disj_index, + int64_t soft_max_cardinality, int64_t penalty, + PenaltyCostBehavior penalty_cost_behavior); + /// Sets the penalty and the soft minimum number of active indices in the + /// disjunction. Cardinality must be in [0, num_indices]. If n is the number + /// of active indices in the disjunction, the following penalty is added to + /// the cost function: + /// - if `penalty_cost_behavior` is `PENALIZE_PER_INACTIVE`: + /// `penalty * max(0, soft_min_cardinality - n)` + /// - if `penalty_cost_behavior` is `PENALIZE_ONCE`: + /// `penalty * (n < soft_min_cardinality ? 1 : 0)`. + void SetDisjunctionSoftMinimum(DisjunctionIndex disj_index, + int64_t soft_min_cardinality, int64_t penalty, + PenaltyCostBehavior penalty_cost_behavior); /// Returns the indices of the disjunctions to which an index belongs. const std::vector& GetDisjunctionIndices( int64_t index) const { return index_to_disjunctions_[index]; } - /// Calls f for each variable index of indices in the same disjunctions as the - /// node corresponding to the variable index 'index'; only disjunctions of - /// cardinality 'cardinality' are considered. - template - void ForEachNodeInDisjunctionWithMaxCardinalityFromIndex( - int64_t index, int64_t max_cardinality, F f) const { - for (const DisjunctionIndex disjunction : GetDisjunctionIndices(index)) { - if (disjunctions_[disjunction].value.max_cardinality == max_cardinality) { - for (const int64_t d_index : disjunctions_[disjunction].indices) { - f(d_index); - } - } - } - } + /// Structure storing a value for a set of variable indices. Is used to store + /// data for index disjunctions (variable indices, max_cardinality and penalty + /// when unperformed). + struct Disjunction { + std::vector indices; + int64_t min_cardinality = 0; + int64_t soft_min_cardinality = 0; + int64_t soft_min_penalty = 0; + PenaltyCostBehavior soft_min_penalty_type = + PenaltyCostBehavior::PENALIZE_ONCE; + int64_t max_cardinality = kint64max; + int64_t soft_max_cardinality = kint64max; + int64_t soft_max_penalty = 0; + PenaltyCostBehavior soft_max_penalty_type = + PenaltyCostBehavior::PENALIZE_ONCE; + }; #if !defined(SWIGPYTHON) /// Returns the variable indices of the nodes in the disjunction of index /// 'index'. @@ -943,30 +1013,41 @@ class OR_DLL Model { DisjunctionIndex index) const { return disjunctions_[index].indices; } -#endif // !defined(SWIGPYTHON) - /// Returns the penalty of the node disjunction of index 'index'. - int64_t GetDisjunctionPenalty(DisjunctionIndex index) const { - return disjunctions_[index].value.penalty; - } - /// Returns the maximum number of possible active nodes of the node - /// disjunction of index 'index'. - int64_t GetDisjunctionMaxCardinality(DisjunctionIndex index) const { - return disjunctions_[index].value.max_cardinality; - } - /// Returns the @ref PenaltyCostBehavior used by the disjunction of index - /// 'index'. - PenaltyCostBehavior GetDisjunctionPenaltyCostBehavior( - DisjunctionIndex index) const { - return disjunctions_[index].value.penalty_cost_behavior; + const Disjunction& GetDisjunction(DisjunctionIndex index) const { + return disjunctions_[index]; } +#endif // !defined(SWIGPYTHON) + /// Returns the maximum number of possible active nodes of the given + /// disjunction. + int64_t GetDisjunctionMaxCardinality(DisjunctionIndex index) const; + /// Returns the minimum number of possible active nodes of the given + /// disjunction. + int64_t GetDisjunctionMinCardinality(DisjunctionIndex index) const; + /// Returns the soft maximum number of possible active nodes of the given + /// disjunction. + int64_t GetDisjunctionSoftMaxCardinality(DisjunctionIndex index) const; + /// Returns the soft minimum number of possible active nodes of the given + /// disjunction. + int64_t GetDisjunctionSoftMinCardinality(DisjunctionIndex index) const; + /// Returns the soft max penalty of the given disjunction. + int64_t GetDisjunctionSoftMaxPenalty(DisjunctionIndex index) const; + /// Returns the soft min penalty of the given disjunction. + int64_t GetDisjunctionSoftMinPenalty(DisjunctionIndex index) const; + int64_t GetDisjunctionPenalty(DisjunctionIndex index) const; + /// Returns the @ref PenaltyCostBehavior used by the given disjunction for + /// for soft minimums. + PenaltyCostBehavior GetDisjunctionSoftMinPenaltyCostBehavior( + DisjunctionIndex index) const; + /// Returns the @ref PenaltyCostBehavior used by the given disjunction for + /// soft maximums. + PenaltyCostBehavior GetDisjunctionSoftMaxPenaltyCostBehavior( + DisjunctionIndex index) const; /// Returns the number of node disjunctions in the model. - int GetNumberOfDisjunctions() const { return disjunctions_.size(); } - /// Returns true if the model contains mandatory disjunctions (ones with - /// kNoPenalty as penalty). - bool HasMandatoryDisjunctions() const; - /// Returns true if the model contains at least one disjunction which is - /// constrained by its max_cardinality. - bool HasMaxCardinalityConstrainedDisjunctions() const; + int GetNumberOfDisjunctions() const; + /// Returns true if the model contains hard-constrained disjunctions, + /// i.e. disjunctions with a penalty cost set to kNoPenalty, + /// with a min_cardinality > 0 or a max_cardinality < indices.size(). + bool HasHardDisjunctions() const; /// Returns the list of all perfect binary disjunctions, as pairs of variable /// indices: a disjunction is "perfect" when its variables do not appear in /// any other disjunction. Each pair is sorted (lowest variable index first), @@ -1008,8 +1089,7 @@ class OR_DLL Model { /// Returns true if a vehicle is allowed to visit a given node. bool IsVehicleAllowedForIndex(int vehicle, int64_t index) const { - return allowed_vehicles_[index].empty() || - allowed_vehicles_[index].contains(vehicle); + return allowed_vehicles_[index].Contains(vehicle); } /// Notifies that index1 and index2 form a pair of nodes which should belong @@ -2059,9 +2139,7 @@ class OR_DLL Model { /// Updates the time limit of the search limit. void UpdateTimeLimit(absl::Duration time_limit) { RegularLimit* limit = GetOrCreateLimit(); - limit->UpdateLimits(time_limit, std::numeric_limits::max(), - std::numeric_limits::max(), - limit->solutions()); + limit->UpdateLimits(time_limit, kint64max, kint64max, limit->solutions()); } /// Helper method to update time limits. @@ -2246,21 +2324,6 @@ class OR_DLL Model { LOCAL_SEARCH_OPERATOR_COUNTER }; - /// Structure storing a value for a set of variable indices. Is used to store - /// data for index disjunctions (variable indices, max_cardinality and penalty - /// when unperformed). - template - struct ValuedNodes { - std::vector indices; - T value; - }; - struct DisjunctionValues { - int64_t penalty; - int64_t max_cardinality; - PenaltyCostBehavior penalty_cost_behavior; - }; - typedef ValuedNodes Disjunction; - /// Storage of a cost cache element corresponding to a cost arc ending at /// node 'index' and on the cost class 'cost_class'. struct CostCacheElement { @@ -2289,13 +2352,13 @@ class OR_DLL Model { const std::vector& evaluator_indices, const std::vector& cumul_dependent_evaluator_indices, int64_t slack_max, std::vector vehicle_capacities, - bool fix_start_cumul_to_zero, const std::string& name); + bool fix_start_cumul_to_zero, absl::string_view name); bool AddDimensionDependentDimensionWithVehicleCapacityInternal( const std::vector& pure_transits, const std::vector& dependent_transits, const Dimension* base_dimension, int64_t slack_max, std::vector vehicle_capacities, bool fix_start_cumul_to_zero, - const std::string& name); + absl::string_view name); bool InitializeDimensionInternal( const std::vector& evaluator_indices, const std::vector& cumul_dependent_evaluator_indices, @@ -2640,14 +2703,12 @@ class OR_DLL Model { /// i.e. by default empty routes will not contribute to the cost nor be /// considered for constraints. std::vector vehicle_used_when_empty_; -#if !defined(SWIG) absl::flat_hash_map< std::pair, std::vector, absl::Hash>> force_distance_to_energy_costs_; util_intops::StrongVector cost_classes_; -#endif // !defined(SWIG) bool costs_are_homogeneous_across_vehicles_; bool cache_callbacks_; mutable std::vector cost_cache_; /// Index by source index. @@ -2660,11 +2721,14 @@ class OR_DLL Model { util_intops::StrongVector disjunctions_; std::vector> index_to_disjunctions_; /// Same vehicle costs + template + struct ValuedNodes { + std::vector indices; + T value; + }; std::vector> same_vehicle_costs_; /// Allowed vehicles -#if !defined(SWIG) - std::vector> allowed_vehicles_; -#endif // !defined(SWIG) + std::vector allowed_vehicles_; /// Pickup and delivery std::vector pickup_delivery_pairs_; std::vector @@ -2797,9 +2861,7 @@ class OR_DLL Model { std::unique_ptr> node_neighbors_by_cost_class_per_size_; std::unique_ptr finalizer_variables_; -#if !defined(SWIG) std::unique_ptr sweep_arranger_; -#endif // !defined(SWIG) RegularLimit* limit_ = nullptr; RegularLimit* cumulative_limit_ = nullptr; @@ -3408,7 +3470,7 @@ class Dimension { /// Accessors. #if !defined(SWIG) - const ReverseArcListGraph& GetPathPrecedenceGraph() const { + const util::ReverseArcListGraph& GetPathPrecedenceGraph() const { return path_precedence_graph_; } #endif // !defined(SWIG) @@ -3612,9 +3674,9 @@ class Dimension { class SelfBased {}; Dimension(Model* model, std::vector vehicle_capacities, - const std::string& name, const Dimension* base_dimension); + absl::string_view name, const Dimension* base_dimension); Dimension(Model* model, std::vector vehicle_capacities, - const std::string& name, SelfBased); + absl::string_view name, SelfBased); void Initialize(absl::Span transit_evaluators, absl::Span cumul_dependent_transit_evaluators, absl::Span state_dependent_transit_evaluators, @@ -3669,9 +3731,7 @@ class Dimension { /// class. std::vector cumul_dependent_class_evaluators_; std::vector vehicle_to_cumul_dependent_class_; -#if !defined(SWIG) - ReverseArcListGraph path_precedence_graph_; -#endif // !defined(SWIG) + util::ReverseArcListGraph path_precedence_graph_; // For every {first_node, second_node, offset} element in node_precedences_, // if both first_node and second_node are performed, then // cumuls_[second_node] must be greater than (or equal to) diff --git a/ortools/routing/samples/VrpSolutionCallback.java b/ortools/routing/samples/VrpSolutionCallback.java index 6dd2b5288c3..2411f268491 100644 --- a/ortools/routing/samples/VrpSolutionCallback.java +++ b/ortools/routing/samples/VrpSolutionCallback.java @@ -117,7 +117,7 @@ public void run() { routingModel.solver().finishCurrentSearch(); } } - }; + } // [END solution_callback] diff --git a/ortools/routing/sat.cc b/ortools/routing/sat.cc index 5c9b7f4bfdc..f37459ccdab 100644 --- a/ortools/routing/sat.cc +++ b/ortools/routing/sat.cc @@ -16,15 +16,18 @@ #include #include #include +#include #include #include #include #include "absl/container/btree_map.h" +#include "absl/container/flat_hash_map.h" #include "absl/log/check.h" #include "absl/time/time.h" #include "absl/types/span.h" #include "ortools/base/map_util.h" +#include "ortools/base/types.h" #include "ortools/constraint_solver/assignment.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/routing/parameters.pb.h" @@ -142,8 +145,8 @@ void AddSoftCumulBounds(const Dimension* dimension, int index, int cumul, const int soft_ub_var = AddVariable(cp_model, 0, CapSub(cumul_max, soft_ub)); // soft_ub_var >= cumul - soft_ub - AddLinearConstraint(cp_model, std::numeric_limits::min(), - soft_ub, {{cumul, 1}, {soft_ub_var, -1}}); + AddLinearConstraint(cp_model, kint64min, soft_ub, + {{cumul, 1}, {soft_ub_var, -1}}); cp_model->mutable_objective()->add_vars(soft_ub_var); cp_model->mutable_objective()->add_coeffs(soft_ub_coef); } @@ -156,8 +159,7 @@ void AddSoftCumulBounds(const Dimension* dimension, int index, int cumul, const int soft_lb_var = AddVariable(cp_model, 0, CapSub(soft_lb, cumul_min)); // soft_lb_var >= soft_lb - cumul - AddLinearConstraint(cp_model, soft_lb, - std::numeric_limits::max(), + AddLinearConstraint(cp_model, soft_lb, kint64max, {{cumul, 1}, {soft_lb_var, 1}}); cp_model->mutable_objective()->add_vars(soft_lb_var); cp_model->mutable_objective()->add_coeffs(soft_lb_coef); @@ -181,11 +183,11 @@ void AddDimensions(const routing::Model& model, const ArcVarMap& arc_vars, if (model.IsStart(i) || model.IsEnd(i)) continue; // Reducing bounds supposing the triangular inequality. const int64_t cumul_min = std::max( - std::numeric_limits::min() / num_cumuls, + kint64min / num_cumuls, std::max(dimension->cumuls()[i]->Min(), CapAdd(transit(model.Start(0), i), min_start))); const int64_t cumul_max = std::min( - std::numeric_limits::max() / num_cumuls, + kint64max / num_cumuls, std::min(dimension->cumuls()[i]->Max(), CapSub(max_end, transit(i, model.End(0))))); cumuls[i] = AddVariable(cp_model, cumul_min, cumul_max); @@ -198,9 +200,9 @@ void AddDimensions(const routing::Model& model, const ArcVarMap& arc_vars, if (tail == head || model.IsStart(tail) || model.IsStart(head)) continue; // arc[tail][head] -> cumuls[head] >= cumuls[tail] + transit. // This is a relaxation of the model as it does not consider slack max. - AddLinearConstraint( - cp_model, transit(tail, head), std::numeric_limits::max(), - {{cumuls[head], 1}, {cumuls[tail], -1}}, {arc_var.second}); + AddLinearConstraint(cp_model, transit(tail, head), kint64max, + {{cumuls[head], 1}, {cumuls[tail], -1}}, + {arc_var.second}); } } } @@ -271,7 +273,7 @@ void AddPickupDeliveryConstraints(const routing::Model& model, const int64_t pickup = pickups[0]; const int64_t delivery = deliveries[0]; // ranks[pickup] + 1 <= ranks[delivery]. - AddLinearConstraint(cp_model, 1, std::numeric_limits::max(), + AddLinearConstraint(cp_model, 1, kint64max, {{ranks[delivery], 1}, {ranks[pickup], -1}}); // vehicles[pickup] == vehicles[delivery] AddLinearConstraint(cp_model, 0, 0, @@ -306,7 +308,7 @@ ArcVarMap PopulateMultiRouteModelFromRoutingModel(const routing::Model& model, if (head_index == tail_index && head_index == depot) continue; const int64_t cost = tail != head ? model.GetHomogeneousCost(tail, head) : model.UnperformedPenalty(tail); - if (cost == std::numeric_limits::max()) continue; + if (cost == kint64max) continue; const Arc arc = {tail_index, head_index}; if (arc_vars.contains(arc)) continue; const int index = AddVariable(cp_model, 0, 1); @@ -372,7 +374,7 @@ ArcVarMap PopulateSingleRouteModelFromRoutingModel(const routing::Model& model, if (model.IsEnd(head)) head = model.Start(0); const int64_t cost = tail != head ? model.GetHomogeneousCost(tail, head) : model.UnperformedPenalty(tail); - if (cost == std::numeric_limits::max()) continue; + if (cost == kint64max) continue; const int index = AddVariable(cp_model, 0, 1); circuit->add_literals(index); circuit->add_tails(tail); @@ -477,9 +479,8 @@ void AddGeneralizedDimensions( for (int cp_node = 1; cp_node < num_cp_nodes; ++cp_node) { const int node = cp_node - 1; int64_t cumul_min = dimension->cumuls()[node]->Min(); - int64_t cumul_max = - std::min(dimension->cumuls()[node]->Max(), - std::numeric_limits::max() / (2 * num_cp_nodes)); + int64_t cumul_max = std::min(dimension->cumuls()[node]->Max(), + kint64max / (2 * num_cp_nodes)); if (model.IsStart(node) || model.IsEnd(node)) { const int vehicle = model.VehicleIndex(node); cumul_max = @@ -496,8 +497,8 @@ void AddGeneralizedDimensions( if (!vehicle_performs_node[vehicle].contains(cp_node)) continue; const int64_t vehicle_capacity = dimension->vehicle_capacities()[vehicle]; - AddLinearConstraint(cp_model, std::numeric_limits::min(), - vehicle_capacity, {{cumuls[cp_node], 1}}, + AddLinearConstraint(cp_model, kint64min, vehicle_capacity, + {{cumuls[cp_node], 1}}, {vehicle_performs_node[vehicle].at(cp_node)}); } } @@ -522,9 +523,7 @@ void AddGeneralizedDimensions( ? dimension->slacks()[cp_tail - 1]->Max() : 0; slack[cp_tail] = AddVariable( - cp_model, 0, - std::min(slack_max, std::numeric_limits::max() / - (2 * num_cp_nodes))); + cp_model, 0, std::min(slack_max, kint64max / (2 * num_cp_nodes))); if (slack_max > 0 && slack_cost > 0) { cp_model->mutable_objective()->add_vars(slack[cp_tail]); cp_model->mutable_objective()->add_coeffs(slack_cost); @@ -545,11 +544,10 @@ void AddGeneralizedDimensions( for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) { const int64_t span_limit = dimension->vehicle_span_upper_bounds()[vehicle]; - if (span_limit == std::numeric_limits::max()) continue; + if (span_limit == kint64max) continue; int cp_start = model.Start(vehicle) + 1; int cp_end = model.End(vehicle) + 1; - AddLinearConstraint(cp_model, std::numeric_limits::min(), - span_limit, + AddLinearConstraint(cp_model, kint64min, span_limit, {{cumuls[cp_end], 1}, {cumuls[cp_start], -1}}); } @@ -566,7 +564,7 @@ void AddGeneralizedDimensions( dimension->vehicle_capacities()[vehicle])); // -inf <= cumuls[cp_end] - cumuls[cp_start] - extra <= bound AddLinearConstraint( - cp_model, std::numeric_limits::min(), bound, + cp_model, kint64min, bound, {{cumuls[cp_end], 1}, {cumuls[cp_start], -1}, {extra, -1}}); // Add extra * cost to objective. cp_model->mutable_objective()->add_vars(extra); @@ -665,8 +663,7 @@ void AddGeneralizedPickupDeliveryConstraints( ranks_difference.push_back({ranks[cp_delivery], 1}); } // SUM(delivery)ranks[delivery] - SUM(pickup)ranks[pickup] >= 1 - AddLinearConstraint(cp_model, 1, std::numeric_limits::max(), - ranks_difference); + AddLinearConstraint(cp_model, 1, kint64max, ranks_difference); } } @@ -704,10 +701,8 @@ ArcVarMap PopulateGeneralizedRouteModelFromRoutingModel( vehicle_performs_node[vehicle][cp_end] = end_arc_var; } - // is_unperformed[node] variable equals to 1 if visit is unperformed, and 0 - // otherwise. + // is_unperformed[node] variable is 1 iff visit is unperformed. std::vector is_unperformed(num_cp_nodes, -1); - // Initialize is_unperformed variables for nodes that must be performed. for (int node = 0; node < num_nodes; node++) { const int cp_node = node + 1; // Forced active and nodes that are not involved in any disjunctions are @@ -716,80 +711,71 @@ ArcVarMap PopulateGeneralizedRouteModelFromRoutingModel( model.GetDisjunctionIndices(node); if (disjunction_indices.empty() || model.ActiveVar(node)->Min() == 1) { is_unperformed[cp_node] = AddVariable(cp_model, 0, 0); - continue; - } - // Check if the node is in a forced active disjunction. - for (DisjunctionIndex disjunction_index : disjunction_indices) { - const int num_nodes = - model.GetDisjunctionNodeIndices(disjunction_index).size(); - const int64_t penalty = model.GetDisjunctionPenalty(disjunction_index); - const int64_t max_cardinality = - model.GetDisjunctionMaxCardinality(disjunction_index); - if (num_nodes == max_cardinality && - (penalty < 0 || penalty == std::numeric_limits::max())) { - // Nodes in this disjunction are forced active. - is_unperformed[cp_node] = AddVariable(cp_model, 0, 0); - break; - } - } - } - // Add alternative visits. Create self-looped arc variables. Set penalty for - // not performing disjunctions. - for (DisjunctionIndex disjunction_index(0); - disjunction_index < model.GetNumberOfDisjunctions(); - disjunction_index++) { - const std::vector& disjunction_indices = - model.GetDisjunctionNodeIndices(disjunction_index); - const int disjunction_size = disjunction_indices.size(); - const int64_t penalty = model.GetDisjunctionPenalty(disjunction_index); - const int64_t max_cardinality = - model.GetDisjunctionMaxCardinality(disjunction_index); - // Case when disjunction involves only 1 node, the node is only present in - // this disjunction, and the node can be unperformed. - if (disjunction_size == 1 && - model.GetDisjunctionIndices(disjunction_indices[0]).size() == 1 && - is_unperformed[disjunction_indices[0] + 1] == -1) { - const int cp_node = disjunction_indices[0] + 1; + } else { const Arc arc = {cp_node, cp_node}; DCHECK(!arc_vars.contains(arc)); is_unperformed[cp_node] = AddVariable(cp_model, 0, 1); arc_vars.insert({arc, is_unperformed[cp_node]}); - cp_model->mutable_objective()->add_vars(is_unperformed[cp_node]); - cp_model->mutable_objective()->add_coeffs(penalty); - continue; } + } + // Set disjunction constraints and penalties. + for (DisjunctionIndex index(0); index < model.GetNumberOfDisjunctions(); + index++) { + const routing::Model::Disjunction& disj = model.GetDisjunction(index); // num_performed + SUM(node)is_unperformed[node] = disjunction_size - const int num_performed = AddVariable(cp_model, 0, max_cardinality); + const int num_performed = + AddVariable(cp_model, disj.min_cardinality, disj.max_cardinality); std::vector> var_coeffs; var_coeffs.push_back({num_performed, 1}); - for (const int node : disjunction_indices) { + for (const int node : disj.indices) { const int cp_node = node + 1; - // Node can be unperformed. - if (is_unperformed[cp_node] == -1) { - const Arc arc = {cp_node, cp_node}; - DCHECK(!arc_vars.contains(arc)); - is_unperformed[cp_node] = AddVariable(cp_model, 0, 1); - arc_vars.insert({arc, is_unperformed[cp_node]}); - } var_coeffs.push_back({is_unperformed[cp_node], 1}); } - AddLinearConstraint(cp_model, disjunction_size, disjunction_size, + AddLinearConstraint(cp_model, disj.indices.size(), disj.indices.size(), var_coeffs); - // When penalty is negative or max int64_t (forced active), num_violated is - // 0. - if (penalty < 0 || penalty == std::numeric_limits::max()) { - AddLinearConstraint(cp_model, max_cardinality, max_cardinality, - {{num_performed, 1}}); - continue; + // Soft min violation. + if (disj.soft_min_penalty > 0 && disj.soft_min_cardinality > 0) { + int violation_var = -1; + if (disj.soft_min_penalty_type == routing::Model::PENALIZE_ONCE) { + // Big-M formulation for !has_violation -> (soft_min <= num_performed): + // with M = max(soft_min - num_performed) = soft_min, + // soft_min <= num_performed + has_violation * M. + violation_var = AddVariable(cp_model, 0, 1); + AddLinearConstraint( + cp_model, disj.soft_min_cardinality, kint64max, + {{num_performed, 1}, {violation_var, disj.soft_min_cardinality}}); + } else { // PENALIZE_PER_INACTIVE + // num_performed + violation >= soft_min. + violation_var = AddVariable(cp_model, 0, disj.soft_min_cardinality); + AddLinearConstraint(cp_model, disj.soft_min_cardinality, kint64max, + {{num_performed, 1}, {violation_var, 1}}); + } + cp_model->mutable_objective()->add_vars(violation_var); + cp_model->mutable_objective()->add_coeffs(disj.soft_min_penalty); + } + // Soft max violation. + if (disj.soft_max_penalty > 0 && + disj.soft_max_cardinality < disj.indices.size()) { + int violation_var = -1; + const int64_t soft_max_slack = + disj.indices.size() - disj.soft_max_cardinality; + if (disj.soft_max_penalty_type == routing::Model::PENALIZE_ONCE) { + // Big-M formulation for !has_violation -> (num_performed <= soft_max): + // with M = max(num_performed - soft_max) = #indices - soft_max, + // num_performed <= soft_max + has_violation * M. + violation_var = AddVariable(cp_model, 0, 1); + AddLinearConstraint( + cp_model, kint64min, disj.soft_max_cardinality, + {{num_performed, 1}, {violation_var, -soft_max_slack}}); + } else { // PENALIZE_PER_INACTIVE + // num_performed - violation <= soft_max. + violation_var = AddVariable(cp_model, 0, soft_max_slack); + AddLinearConstraint(cp_model, kint64min, disj.soft_max_cardinality, + {{num_performed, 1}, {violation_var, 1}}); + } + cp_model->mutable_objective()->add_vars(violation_var); + cp_model->mutable_objective()->add_coeffs(disj.soft_max_penalty); } - // If number of active indices is less than max_cardinality, then for each - // violated index 'penalty' is paid. - const int num_violated = AddVariable(cp_model, 0, max_cardinality); - cp_model->mutable_objective()->add_vars(num_violated); - cp_model->mutable_objective()->add_coeffs(penalty); - // num_performed + num_violated = max_cardinality - AddLinearConstraint(cp_model, max_cardinality, max_cardinality, - {{num_performed, 1}, {num_violated, 1}}); } // Create "arc" variables. std::vector> first_to_end_arcs; @@ -811,8 +797,7 @@ ArcVarMap PopulateGeneralizedRouteModelFromRoutingModel( bool feasible = false; for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) { - if (model.GetArcCostForVehicle(tail, head, vehicle) != - std::numeric_limits::max()) { + if (model.GetArcCostForVehicle(tail, head, vehicle) != kint64max) { feasible = true; break; } @@ -915,7 +900,7 @@ ArcVarMap PopulateGeneralizedRouteModelFromRoutingModel( } int64_t cost = model.GetArcCostForVehicle(tail, head, vehicle); // Arcs with int64_t's max cost are infeasible. - if (cost == std::numeric_limits::max()) continue; + if (cost == kint64max) continue; const int vehicle_class = model.GetVehicleClassIndexOfVehicle(vehicle).value(); if (!vehicle_class_performs_arc[vehicle_class].contains(arc_var)) { @@ -1001,7 +986,7 @@ ArcVarMap PopulateGeneralizedRouteModelFromRoutingModel( } if (primary_dimension != nullptr) { for (int cp_node = 0; cp_node < num_cp_nodes; ++cp_node) { - int64_t min_transit = std::numeric_limits::max(); + int64_t min_transit = kint64max; if (cp_node != 0 && !model.IsEnd(cp_node - 1)) { for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) { const TransitCallback1& transit = @@ -1014,7 +999,7 @@ ArcVarMap PopulateGeneralizedRouteModelFromRoutingModel( routes_ct->add_demands(min_transit); } DCHECK_EQ(routes_ct->demands_size(), num_cp_nodes); - int64_t max_capacity = std::numeric_limits::min(); + int64_t max_capacity = kint64min; for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) { max_capacity = std::max(max_capacity, primary_dimension->vehicle_capacities()[vehicle]); diff --git a/ortools/routing/search.cc b/ortools/routing/search.cc index 61f179e1797..8384720f572 100644 --- a/ortools/routing/search.cc +++ b/ortools/routing/search.cc @@ -505,7 +505,7 @@ IntVarFilteredHeuristic::IntVarFilteredHeuristic( delta_(solver->MakeAssignment()), empty_(solver->MakeAssignment()), filter_manager_(filter_manager), - objective_upper_bound_(std::numeric_limits::max()), + objective_upper_bound_(kint64max), number_of_decisions_(0), number_of_rejects_(0) { if (!secondary_vars.empty()) { @@ -623,16 +623,15 @@ std::optional IntVarFilteredHeuristic::Evaluate( void IntVarFilteredHeuristic::SynchronizeFilters() { if (filter_manager_) filter_manager_->Synchronize(assignment_, delta_); // Resetting the upper bound to allow cost-increasing insertions. - objective_upper_bound_ = std::numeric_limits::max(); + objective_upper_bound_ = kint64max; } bool IntVarFilteredHeuristic::FilterAccept(bool ignore_upper_bound) { if (!filter_manager_) return true; LocalSearchMonitor* const monitor = solver_->GetLocalSearchMonitor(); return filter_manager_->Accept( - monitor, delta_, empty_, std::numeric_limits::min(), - ignore_upper_bound ? std::numeric_limits::max() - : objective_upper_bound_); + monitor, delta_, empty_, kint64min, + ignore_upper_bound ? kint64max : objective_upper_bound_); } // RoutingFilteredHeuristic @@ -693,12 +692,17 @@ bool RoutingFilteredHeuristic::InitializeSolution() { } void RoutingFilteredHeuristic::MakeDisjunctionNodesUnperformed(int64_t node) { - model()->ForEachNodeInDisjunctionWithMaxCardinalityFromIndex( - node, 1, [this, node](int alternate) { + for (const RoutingModel::DisjunctionIndex disjunction : + model()->GetDisjunctionIndices(node)) { + if (model()->GetDisjunctionMaxCardinality(disjunction) == 1) { + for (const int64_t alternate : + model()->GetDisjunctionNodeIndices(disjunction)) { if (node != alternate && !Contains(alternate)) { SetNext(alternate, alternate, -1); } - }); + } + } + } } void RoutingFilteredHeuristic::AddUnassignedNodesToEmptyVehicles() { @@ -970,7 +974,7 @@ int64_t CheapestInsertionFilteredHeuristic::GetUnperformedValue( if (penalty_evaluator_ != nullptr) { return penalty_evaluator_(node_to_insert); } - return std::numeric_limits::max(); + return kint64max; } // GlobalCheapestInsertionFilteredHeuristic @@ -1886,10 +1890,8 @@ bool GlobalCheapestInsertionFilteredHeuristic::InitializePairPositions( // TODO(user): Adapt the code to make pair disjunctions unperformed. if (gci_params_.add_unperformed_entries() && pickups.size() == 1 && deliveries.size() == 1 && - GetUnperformedValue(pickup) != - std::numeric_limits::max() && - GetUnperformedValue(delivery) != - std::numeric_limits::max()) { + GetUnperformedValue(pickup) != kint64max && + GetUnperformedValue(delivery) != kint64max) { AddPairEntry(pickup, -1, delivery, -1, -1, priority_queue, nullptr, nullptr); } @@ -2314,7 +2316,7 @@ bool GlobalCheapestInsertionFilteredHeuristic::InitializePositions( if (StopSearch()) return false; // Add insertion entry making node unperformed. if (gci_params_.add_unperformed_entries() && - GetUnperformedValue(node) != std::numeric_limits::max()) { + GetUnperformedValue(node) != kint64max) { AddNodeEntry(node, node, -1, all_vehicles, queue); } // Add all insertion entries making node performed. @@ -2731,7 +2733,7 @@ int64_t GetNegMaxDistanceFromVehicles(const Model& model, int pair_index) { const std::vector& cost_from_start = pickup_costs[pickup]; for (int64_t delivery : deliveries) { const std::vector& cost_to_end = delivery_costs[delivery]; - int64_t closest_vehicle_distance = std::numeric_limits::max(); + int64_t closest_vehicle_distance = kint64max; for (int v = 0; v < model.vehicles(); v++) { if (cost_from_start[v] < 0 || cost_to_end[v] < 0) { // Vehicle not in the pickup and/or delivery's vehicle var domain. @@ -3053,7 +3055,7 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { for (int pair_index = 0; pair_index < pairs.size(); ++pair_index) { const auto& [pickups, deliveries] = pairs[pair_index]; - int64_t num_allowed_vehicles = std::numeric_limits::max(); + int64_t num_allowed_vehicles = kint64max; int64_t pickup_penalty = 0; int hint_weight = 0; int reversed_hint_weight = 0; @@ -3110,7 +3112,7 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { for (int node = 0; node < model.Size(); ++node) { if (model.IsStart(node) || model.IsEnd(node)) continue; - int64_t min_distance = std::numeric_limits::max(); + int64_t min_distance = kint64max; int64_t sum_distance = 0; ProcessVehicleStartEndCosts( model, node, @@ -3176,6 +3178,8 @@ void LocalCheapestInsertionFilteredHeuristic::InsertBestPickupThenDelivery( vehicle)) { continue; } + // Do not propagate pickup insertion cost to the delivery insertions. + ResetUpperBound(); for (const NodeInsertion& delivery_insertion : ComputeEvaluatorSortedPositionsOnRouteAfter( delivery, pickup, Value(pickup_insertion.insert_after), @@ -3753,12 +3757,11 @@ EvaluatorCheapestAdditionFilteredHeuristic:: int64_t EvaluatorCheapestAdditionFilteredHeuristic::FindTopSuccessor( int64_t node, const std::vector& successors) { - int64_t best_evaluation = std::numeric_limits::max(); + int64_t best_evaluation = kint64max; int64_t best_successor = -1; for (int64_t successor : successors) { - const int64_t evaluation = (successor >= 0) - ? evaluator_(node, successor) - : std::numeric_limits::max(); + const int64_t evaluation = + (successor >= 0) ? evaluator_(node, successor) : kint64max; if (evaluation < best_evaluation || (evaluation == best_evaluation && successor > best_successor)) { best_evaluation = evaluation; @@ -4418,9 +4421,9 @@ bool SavingsFilteredHeuristic::ComputeSavings() { const double weighted_arc_cost_fp = savings_params_.arc_coefficient() * arc_cost; const int64_t weighted_arc_cost = - weighted_arc_cost_fp < std::numeric_limits::max() + weighted_arc_cost_fp < kint64max ? static_cast(weighted_arc_cost_fp) - : std::numeric_limits::max(); + : kint64max; const int64_t saving_value = CapSub( CapAdd(before_to_end_cost, start_to_after_cost), weighted_arc_cost); @@ -4849,7 +4852,7 @@ bool ChristofidesFilteredHeuristic::BuildSolutionInternal() { // value supported by MinCostPerfectMatching. // TODO(user): Investigate if ChristofidesPathSolver should not // return a status to bail out fast in case of problem. - return std::min(cost, std::numeric_limits::max() / 2); + return std::min(cost, kint64max / 2); }; using Cost = decltype(cost); ChristofidesPathSolver christofides_solver( @@ -5576,8 +5579,7 @@ GuidedSlackFinalizer::GuidedSlackFinalizer( model_(ABSL_DIE_IF_NULL(model)), initializer_(std::move(initializer)), is_initialized_(dimension->slacks().size(), false), - initial_values_(dimension->slacks().size(), - std::numeric_limits::min()), + initial_values_(dimension->slacks().size(), kint64min), current_index_(model_->Start(0)), current_route_(0), last_delta_used_(dimension->slacks().size(), 0) {} @@ -5770,7 +5772,7 @@ void GreedyDescentLSOperator::Start(const Assignment* assignment) { int64_t GreedyDescentLSOperator::FindMaxDistanceToDomain( const Assignment* assignment) { - int64_t result = std::numeric_limits::min(); + int64_t result = kint64min; for (const IntVar* const var : variables_) { result = std::max(result, std::abs(var->Max() - assignment->Value(var))); result = std::max(result, std::abs(var->Min() - assignment->Value(var))); diff --git a/ortools/routing/search.h b/ortools/routing/search.h index c7558bc363c..d988d8a24ac 100644 --- a/ortools/routing/search.h +++ b/ortools/routing/search.h @@ -38,6 +38,7 @@ #include "absl/log/check.h" #include "absl/types/span.h" #include "ortools/base/adjustable_priority_queue.h" +#include "ortools/base/types.h" #include "ortools/constraint_solver/assignment.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/routing/enums.pb.h" @@ -271,6 +272,10 @@ class IntVarFilteredHeuristic { /// returning. std::optional Evaluate(bool commit, bool ignore_upper_bound = false, bool update_upper_bound = true); + // Reset current cost upper bound. + void ResetUpperBound() { + objective_upper_bound_ = std::numeric_limits::max(); + } /// Returns true if the search must be stopped. virtual bool StopSearch() { return false; } /// Modifies the current solution by setting the variable of index 'index' to @@ -543,7 +548,7 @@ class GlobalCheapestInsertionFilteredHeuristic PairEntry(int pickup_to_insert, int pickup_insert_after, int delivery_to_insert, int delivery_insert_after, int vehicle, int64_t bucket) - : value_(std::numeric_limits::max()), + : value_(kint64max), heap_index_(-1), pickup_to_insert_(pickup_to_insert), pickup_insert_after_(pickup_insert_after),