diff --git a/ortools/constraint_solver/BUILD.bazel b/ortools/constraint_solver/BUILD.bazel index 6d7566786d4..ee162296853 100644 --- a/ortools/constraint_solver/BUILD.bazel +++ b/ortools/constraint_solver/BUILD.bazel @@ -15,7 +15,6 @@ load("@rules_cc//cc:defs.bzl", "cc_library", "cc_proto_library") load("@rules_proto//proto:defs.bzl", "proto_library") -load("@rules_python//python:proto.bzl", "py_proto_library") package(default_visibility = ["//visibility:public"]) @@ -301,6 +300,30 @@ cc_library( ], ) +cc_library( + name = "routing_utils", + srcs = ["routing_utils.cc"], + hdrs = ["routing_utils.h"], + visibility = ["//visibility:public"], + deps = [ + "//ortools/base", + "//ortools/util:saturated_arithmetic", + ], +) + +cc_library( + name = "routing_neighborhoods", + srcs = ["routing_neighborhoods.cc"], + hdrs = ["routing_neighborhoods.h"], + visibility = ["//visibility:public"], + deps = [ + ":cp", + ":routing_types", + ":routing_utils", + "//ortools/base", + ], +) + cc_library( name = "routing_index_manager", srcs = ["routing_index_manager.cc"], @@ -319,18 +342,22 @@ cc_library( srcs = [ "routing.cc", "routing_breaks.cc", + "routing_constraints.cc", + "routing_decision_builders.cc", "routing_filters.cc", "routing_flow.cc", + "routing_insertion_lns.cc", "routing_lp_scheduling.cc", - "routing_neighborhoods.cc", "routing_sat.cc", "routing_search.cc", ], hdrs = [ "routing.h", + "routing_constraints.h", + "routing_decision_builders.h", "routing_filters.h", + "routing_insertion_lns.h", "routing_lp_scheduling.h", - "routing_neighborhoods.h", "routing_search.h", ], copts = select({ @@ -343,9 +370,11 @@ cc_library( ":cp", ":routing_enums_cc_proto", ":routing_index_manager", + ":routing_neighborhoods", ":routing_parameters", ":routing_parameters_cc_proto", ":routing_types", + ":routing_utils", "//ortools/base", "//ortools/base:adjustable_priority_queue", "//ortools/base:dump_vars", diff --git a/ortools/constraint_solver/constraint_solver.h b/ortools/constraint_solver/constraint_solver.h index 43c88aaf8e6..f16a8b9cc64 100644 --- a/ortools/constraint_solver/constraint_solver.h +++ b/ortools/constraint_solver/constraint_solver.h @@ -2422,6 +2422,11 @@ class Solver { SearchMonitor* MakeSearchLog(int branch_period, IntVar* var, std::function display_callback); + /// At each solution, this monitor will display the 'vars' values and the + /// result of @p display_callback. + SearchMonitor* MakeSearchLog(int branch_period, std::vector vars, + std::function display_callback); + /// OptimizeVar Search Logs /// At each solution, this monitor will also display the 'opt_var' value. SearchMonitor* MakeSearchLog(int branch_period, OptimizeVar* opt_var); @@ -2436,15 +2441,15 @@ class Solver { /// SearchMonitors will display a periodic search log every branch_period /// branches explored. int branch_period = 1; - /// SearchMonitors will display values of objective or variable (both cannot - /// be used together). + /// SearchMonitors will display values of objective or variables (both + /// cannot be used together). OptimizeVar* objective = nullptr; - IntVar* variable = nullptr; + std::vector variables; /// When displayed, objective or var values will be scaled and offset by /// the given values in the following way: /// scaling_factor * (value + offset). - double scaling_factor = 1.0; - double offset = 0; + std::vector scaling_factors; + std::vector offsets; /// SearchMonitors will display the result of display_callback at each new /// solution found and when the search finishes if /// display_on_new_solutions_only is false. @@ -4425,6 +4430,7 @@ class ObjectiveMonitor : public SearchMonitor { bool found_initial_solution_; private: + friend class Solver; const std::vector objective_vars_; std::vector minimization_vars_; std::vector upper_bounds_; @@ -4459,7 +4465,7 @@ class OptimizeVar : public ObjectiveMonitor { void RefuteDecision(Decision* d) override; bool AtSolution() override; bool AcceptSolution() override; - virtual std::string Print() const; + virtual std::string Name() const; std::string DebugString() const override; void ApplyBound(); diff --git a/ortools/constraint_solver/constraint_solveri.h b/ortools/constraint_solver/constraint_solveri.h index 539a3cfe47d..138b1209bf8 100644 --- a/ortools/constraint_solver/constraint_solveri.h +++ b/ortools/constraint_solver/constraint_solveri.h @@ -1406,10 +1406,11 @@ class PathOperator : public IntVarLocalSearchOperator { /// Returns the vector of path start nodes. const std::vector& path_starts() const { return path_starts_; } /// Returns the class of the path of the ith base node. - int PathClass(int i) const { + int PathClass(int i) const { return PathClassFromStartNode(StartNode(i)); } + int PathClassFromStartNode(int64_t start_node) const { return iteration_parameters_.start_empty_path_class != nullptr - ? iteration_parameters_.start_empty_path_class(StartNode(i)) - : StartNode(i); + ? iteration_parameters_.start_empty_path_class(start_node) + : start_node; } /// When the operator is being synchronized with a new solution (when Start() @@ -1463,6 +1464,12 @@ class PathOperator : public IntVarLocalSearchOperator { return ignore_path_vars_ ? 0LL : OldValue(node + number_of_nexts_); } + int CurrentNodePathStart(int64_t node) const { + return node_path_starts_[node]; + } + + int CurrentNodePathEnd(int64_t node) const { return node_path_ends_[node]; } + /// Moves the chain starting after the node before_chain and ending at the /// node chain_end after the node destination bool MoveChain(int64_t before_chain, int64_t chain_end, int64_t destination); @@ -1609,6 +1616,8 @@ class PathOperator : public IntVarLocalSearchOperator { std::vector base_sibling_alternatives_; std::vector end_nodes_; std::vector base_paths_; + std::vector node_path_starts_; + std::vector node_path_ends_; std::vector calls_per_base_node_; std::vector path_starts_; std::vector path_ends_; @@ -2049,8 +2058,9 @@ class SymmetryBreaker : public DecisionVisitor { /// the search is running. class SearchLog : public SearchMonitor { public: - SearchLog(Solver* s, OptimizeVar* obj, IntVar* var, double scaling_factor, - double offset, std::function display_callback, + SearchLog(Solver* solver, std::vector vars, std::string vars_name, + std::vector scaling_factors, std::vector offsets, + std::function display_callback, bool display_on_new_solutions_only, int period); ~SearchLog() override; void EnterSearch() override; @@ -2076,16 +2086,16 @@ class SearchLog : public SearchMonitor { const int period_; std::unique_ptr timer_; - IntVar* const var_; - OptimizeVar* const obj_; - const double scaling_factor_; - const double offset_; + const std::vector vars_; + const std::string vars_name_; + const std::vector scaling_factors_; + const std::vector offsets_; std::function display_callback_; const bool display_on_new_solutions_only_; int nsol_; int64_t tick_; - int64_t objective_min_; - int64_t objective_max_; + std::vector objective_min_; + std::vector objective_max_; int min_right_depth_; int max_depth_; int sliding_min_depth_; diff --git a/ortools/constraint_solver/java/routing.i b/ortools/constraint_solver/java/routing.i index 34ccaff5f85..8de67c15ea9 100644 --- a/ortools/constraint_solver/java/routing.i +++ b/ortools/constraint_solver/java/routing.i @@ -200,11 +200,9 @@ import java.util.function.LongUnaryOperator; %rename (registerUnaryTransitVector) RoutingModel::RegisterUnaryTransitVector; %rename (registerUnaryTransitCallback) RoutingModel::RegisterUnaryTransitCallback; -%rename (registerPositiveUnaryTransitCallback) RoutingModel::RegisterPositiveUnaryTransitCallback; // not tested %rename (registerTransitMatrix) RoutingModel::RegisterTransitMatrix; %rename (registerTransitCallback) RoutingModel::RegisterTransitCallback; -%rename (registerPositiveTransitCallback) RoutingModel::RegisterPositiveTransitCallback; // not tested %rename (restoreAssignment) RoutingModel::RestoreAssignment; %rename (routesToAssignment) RoutingModel::RoutesToAssignment; diff --git a/ortools/constraint_solver/local_search.cc b/ortools/constraint_solver/local_search.cc index c748129a139..83f7b865215 100644 --- a/ortools/constraint_solver/local_search.cc +++ b/ortools/constraint_solver/local_search.cc @@ -356,6 +356,8 @@ PathOperator::PathOperator(const std::vector& next_vars, base_sibling_alternatives_(iteration_parameters.number_of_base_nodes), end_nodes_(iteration_parameters.number_of_base_nodes), base_paths_(iteration_parameters.number_of_base_nodes), + node_path_starts_(number_of_nexts_, -1), + node_path_ends_(number_of_nexts_, -1), calls_per_base_node_(iteration_parameters.number_of_base_nodes, 0), just_started_(false), first_start_(true), @@ -770,10 +772,26 @@ void PathOperator::InitializePathStarts() { // TODO(user): make this faster, maybe by pairing starts with ends. path_ends_.clear(); path_ends_.reserve(path_starts_.size()); + int64_t max_node_index = number_of_nexts_ - 1; for (const int start_node : path_starts_) { int64_t node = start_node; while (!IsPathEnd(node)) node = OldNext(node); path_ends_.push_back(node); + max_node_index = std::max(max_node_index, node); + } + node_path_starts_.assign(max_node_index + 1, -1); + node_path_ends_.assign(max_node_index + 1, -1); + for (int i = 0; i < path_starts_.size(); ++i) { + const int64_t start_node = path_starts_[i]; + const int64_t end_node = path_ends_[i]; + int64_t node = start_node; + while (!IsPathEnd(node)) { + node_path_starts_[node] = start_node; + node_path_ends_[node] = end_node; + node = OldNext(node); + } + node_path_starts_[node] = start_node; + node_path_ends_[node] = end_node; } } @@ -899,9 +917,10 @@ class TwoOpt : public PathOperator { const std::vector& secondary_vars, std::function start_empty_path_class, std::function&(int, int)> get_neighbors = nullptr) - : PathOperator(vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, - true, true, std::move(start_empty_path_class), - std::move(get_neighbors)), + : PathOperator( + vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, + /*skip_locally_optimal_paths=*/true, /*accept_path_end_base=*/true, + std::move(start_empty_path_class), std::move(get_neighbors)), last_base_(-1), last_(-1) {} ~TwoOpt() override {} @@ -911,7 +930,7 @@ class TwoOpt : public PathOperator { std::string DebugString() const override { return "TwoOpt"; } protected: - bool OnSamePathAsPreviousBase(int64_t base_index) override { + bool OnSamePathAsPreviousBase(int64_t /*base_index*/) override { // Both base nodes have to be on the same path. return true; } @@ -927,21 +946,30 @@ class TwoOpt : public PathOperator { }; bool TwoOpt::MakeNeighbor() { - // TODO(user): Support version taking into account neighbors of - // BaseNode(0). This requires filtering neighbors which are on the same path - // as BaseNode(0), which itself requires a method to obtain the current path - // of a non-base node. - DCHECK_EQ(StartNode(0), StartNode(1)); - if (last_base_ != BaseNode(0) || last_ == -1) { + const int64_t node0 = BaseNode(0); + int64_t node1 = -1; + if (HasNeighbors()) { + const int64_t neighbor = GetNeighborForBaseNode(0); + if (IsInactive(neighbor)) return false; + if (CurrentNodePathStart(node0) != CurrentNodePathStart(neighbor)) { + return false; + } + node1 = Next(neighbor); + } else { + DCHECK_EQ(StartNode(0), StartNode(1)); + node1 = BaseNode(1); + } + // Incrementality is disabled with neighbors. + if (last_base_ != node0 || last_ == -1 || HasNeighbors()) { RevertChanges(false); - if (IsPathEnd(BaseNode(0))) { + if (IsPathEnd(node0)) { last_ = -1; return false; } - last_base_ = BaseNode(0); - last_ = Next(BaseNode(0)); + last_base_ = node0; + last_ = Next(node0); int64_t chain_last; - if (ReverseChain(BaseNode(0), BaseNode(1), &chain_last) + if (ReverseChain(node0, node1, &chain_last) // Check there are more than one node in the chain (reversing a // single node is a NOP). && last_ != chain_last) { @@ -951,8 +979,8 @@ bool TwoOpt::MakeNeighbor() { return false; } const int64_t to_move = Next(last_); - DCHECK_EQ(Next(to_move), BaseNode(1)); - return MoveChain(last_, to_move, BaseNode(0)); + DCHECK_EQ(Next(to_move), node1); + return MoveChain(last_, to_move, node0); } // ----- Relocate ----- @@ -977,9 +1005,10 @@ class Relocate : public PathOperator { std::function start_empty_path_class, std::function&(int, int)> get_neighbors, int64_t chain_length = 1LL, bool single_path = false) - : PathOperator(vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, - true, false, std::move(start_empty_path_class), - std::move(get_neighbors)), + : PathOperator( + vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, + /*skip_locally_optimal_paths=*/true, /*accept_path_end_base=*/false, + std::move(start_empty_path_class), std::move(get_neighbors)), chain_length_(chain_length), single_path_(single_path), name_(name) { @@ -1059,8 +1088,9 @@ class Exchange : public PathOperator { std::function start_empty_path_class, std::function&(int, int)> get_neighbors = nullptr) : PathOperator(vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, - true, false, std::move(start_empty_path_class), - get_neighbors) {} + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/false, + std::move(start_empty_path_class), get_neighbors) {} ~Exchange() override {} bool MakeNeighbor() override; @@ -1103,9 +1133,10 @@ class Cross : public PathOperator { const std::vector& secondary_vars, std::function start_empty_path_class, std::function&(int, int)> get_neighbors = nullptr) - : PathOperator(vars, secondary_vars, 2, true, true, - std::move(start_empty_path_class), - std::move(get_neighbors)) {} + : PathOperator( + vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, + /*skip_locally_optimal_paths=*/true, /*accept_path_end_base=*/true, + std::move(start_empty_path_class), std::move(get_neighbors)) {} ~Cross() override {} bool MakeNeighbor() override; @@ -1113,23 +1144,42 @@ class Cross : public PathOperator { }; bool Cross::MakeNeighbor() { - // TODO(user): Support version taking into account neighbors of - // BaseNode(0). This requires a method to obtain the current path start/end of - // a non-base node in order to cross the two paths. const int64_t start0 = StartNode(0); - const int64_t start1 = StartNode(1); - if (start1 == start0) return false; + int64_t start1 = -1; const int64_t node0 = BaseNode(0); + int64_t node1 = -1; if (node0 == start0) return false; - const int64_t node1 = BaseNode(1); - if (node1 == start1) return false; + if (HasNeighbors()) { + const int64_t neighbor = GetNeighborForBaseNode(0); + DCHECK(!IsPathStart(neighbor)); + if (IsInactive(neighbor)) return false; + start1 = CurrentNodePathStart(neighbor); + // Tricky: In all cases we want to connect node0 to neighbor. If we are + // crossing path starts, node0 is the end of a chain and neighbor is the + // the first node after the other chain ending at node1, therefore + // node1 = prev(neighbor). + // If we are crossing path ends, node0 is the start of a chain and neighbor + // is the last node before the other chain starting at node1, therefore + // node1 = next(neighbor). + // TODO(user): When neighbors are considered, explore if having two + // versions of Cross makes sense, one exchanging path starts, the other + // path ends. Rationale: neighborhoods might not be symmetric. In practice, + // in particular when used through RoutingModel, neighborhoods are + // actually symmetric. + node1 = (start0 < start1) ? Prev(neighbor) : Next(neighbor); + } else { + start1 = StartNode(1); + node1 = BaseNode(1); + } + if (start1 == start0 || node1 == start1) return false; bool moved = false; if (start0 < start1) { // Cross path starts. // If two paths are equivalent don't exchange the full paths. - if (PathClass(0) == PathClass(1) && !IsPathEnd(node0) && - IsPathEnd(Next(node0)) && !IsPathEnd(node1) && IsPathEnd(Next(node1))) { + if (PathClassFromStartNode(start0) == PathClassFromStartNode(start1) && + !IsPathEnd(node0) && IsPathEnd(Next(node0)) && !IsPathEnd(node1) && + IsPathEnd(Next(node1))) { return false; } @@ -1140,14 +1190,18 @@ bool Cross::MakeNeighbor() { // Cross path ends. // If paths are equivalent, every end crossing has a corresponding start // crossing, we don't generate those symmetric neighbors. - if (PathClass(0) == PathClass(1)) return false; + if (PathClassFromStartNode(start0) == PathClassFromStartNode(start1) && + !HasNeighbors()) { + return false; + } // Never exchange full paths, equivalent or not. // Full path exchange is only performed when start0 < start1. - if (IsPathStart(Prev(node0)) && IsPathStart(Prev(node1))) { + if (IsPathStart(Prev(node0)) && IsPathStart(Prev(node1)) && + !HasNeighbors()) { return false; } - const int prev_end_node1 = Prev(EndNode(1)); + const int prev_end_node1 = Prev(CurrentNodePathEnd(node1)); if (!IsPathEnd(node0)) { moved |= MoveChain(Prev(node0), Prev(EndNode(0)), prev_end_node1); } @@ -2417,11 +2471,13 @@ LocalSearchOperator* MakeLocalSearchOperatorWithNeighbors( } MAKE_LOCAL_SEARCH_OPERATOR(TwoOpt) +MAKE_LOCAL_SEARCH_OPERATOR_WITH_NEIGHBORS(TwoOpt) MAKE_LOCAL_SEARCH_OPERATOR(Relocate) MAKE_LOCAL_SEARCH_OPERATOR_WITH_NEIGHBORS(Relocate) MAKE_LOCAL_SEARCH_OPERATOR(Exchange) MAKE_LOCAL_SEARCH_OPERATOR_WITH_NEIGHBORS(Exchange) MAKE_LOCAL_SEARCH_OPERATOR(Cross) +MAKE_LOCAL_SEARCH_OPERATOR_WITH_NEIGHBORS(Cross) MAKE_LOCAL_SEARCH_OPERATOR(MakeActiveOperator) MAKE_LOCAL_SEARCH_OPERATOR(MakeInactiveOperator) MAKE_LOCAL_SEARCH_OPERATOR(MakeChainInactiveOperator) @@ -2433,6 +2489,9 @@ MAKE_LOCAL_SEARCH_OPERATOR(RelocateAndMakeInactiveOperator) #undef MAKE_LOCAL_SEARCH_OPERATOR +// TODO(user): Remove (parts of) the following methods as they are mostly +// redundant with the MakeLocalSearchOperatorWithNeighbors and +// MakeLocalSearchOperator functions. LocalSearchOperator* Solver::MakeOperator( const std::vector& vars, Solver::LocalSearchOperators op, std::function&(int, int)> get_neighbors) { @@ -2443,11 +2502,10 @@ LocalSearchOperator* Solver::MakeOperator( const std::vector& vars, const std::vector& secondary_vars, Solver::LocalSearchOperators op, std::function&(int, int)> get_neighbors) { - LocalSearchOperator* result = nullptr; switch (op) { case Solver::TWOOPT: { - result = RevAlloc(new TwoOpt(vars, secondary_vars, nullptr)); - break; + return MakeLocalSearchOperatorWithNeighbors( + this, vars, secondary_vars, nullptr, std::move(get_neighbors)); } case Solver::OROPT: { std::vector operators; @@ -2459,95 +2517,77 @@ LocalSearchOperator* Solver::MakeOperator( /*get_neighbors=*/nullptr, /*chain_length=*/i, /*single_path=*/true))); } - result = ConcatenateOperators(operators); - break; + return ConcatenateOperators(operators); } case Solver::RELOCATE: { - result = MakeLocalSearchOperatorWithNeighbors( + return MakeLocalSearchOperatorWithNeighbors( this, vars, secondary_vars, nullptr, std::move(get_neighbors)); - break; } case Solver::EXCHANGE: { - result = MakeLocalSearchOperatorWithNeighbors( + return MakeLocalSearchOperatorWithNeighbors( this, vars, secondary_vars, nullptr, std::move(get_neighbors)); - break; } case Solver::CROSS: { - result = - MakeLocalSearchOperator(this, vars, secondary_vars, nullptr); - break; + return MakeLocalSearchOperatorWithNeighbors( + this, vars, secondary_vars, nullptr, std::move(get_neighbors)); } case Solver::MAKEACTIVE: { - result = MakeLocalSearchOperator( + return MakeLocalSearchOperator( this, vars, secondary_vars, nullptr); - break; } case Solver::MAKEINACTIVE: { - result = MakeLocalSearchOperator( + return MakeLocalSearchOperator( this, vars, secondary_vars, nullptr); - break; } case Solver::MAKECHAININACTIVE: { - result = MakeLocalSearchOperator( + return MakeLocalSearchOperator( this, vars, secondary_vars, nullptr); - break; } case Solver::SWAPACTIVE: { - result = MakeLocalSearchOperator( + return MakeLocalSearchOperator( this, vars, secondary_vars, nullptr); - break; } case Solver::EXTENDEDSWAPACTIVE: { - result = MakeLocalSearchOperator( + return MakeLocalSearchOperator( this, vars, secondary_vars, nullptr); - break; } case Solver::PATHLNS: { - result = RevAlloc(new PathLns(vars, secondary_vars, 2, 3, false)); - break; + return RevAlloc(new PathLns(vars, secondary_vars, 2, 3, false)); } case Solver::FULLPATHLNS: { - result = RevAlloc(new PathLns(vars, secondary_vars, - /*number_of_chunks=*/1, - /*chunk_size=*/0, - /*unactive_fragments=*/true)); - break; + return RevAlloc(new PathLns(vars, secondary_vars, + /*number_of_chunks=*/1, + /*chunk_size=*/0, + /*unactive_fragments=*/true)); } case Solver::UNACTIVELNS: { - result = RevAlloc(new PathLns(vars, secondary_vars, 1, 6, true)); - break; + return RevAlloc(new PathLns(vars, secondary_vars, 1, 6, true)); } case Solver::INCREMENT: { - if (secondary_vars.empty()) { - result = RevAlloc(new IncrementValue(vars)); - } else { + if (!secondary_vars.empty()) { LOG(FATAL) << "Operator " << op << " does not support secondary variables"; } - break; + return RevAlloc(new IncrementValue(vars)); } case Solver::DECREMENT: { - if (secondary_vars.empty()) { - result = RevAlloc(new DecrementValue(vars)); - } else { + if (!secondary_vars.empty()) { LOG(FATAL) << "Operator " << op << " does not support secondary variables"; } - break; + return RevAlloc(new DecrementValue(vars)); } case Solver::SIMPLELNS: { - if (secondary_vars.empty()) { - result = RevAlloc(new SimpleLns(vars, 1)); - } else { + if (!secondary_vars.empty()) { LOG(FATAL) << "Operator " << op << " does not support secondary variables"; } - break; + return RevAlloc(new SimpleLns(vars, 1)); } default: LOG(FATAL) << "Unknown operator " << op; } - return result; + return nullptr; } LocalSearchOperator* Solver::MakeOperator( @@ -2561,7 +2601,6 @@ LocalSearchOperator* Solver::MakeOperator( const std::vector& secondary_vars, Solver::IndexEvaluator3 evaluator, Solver::EvaluatorLocalSearchOperators op) { - LocalSearchOperator* result = nullptr; switch (op) { case Solver::LK: { std::vector operators; @@ -2569,25 +2608,22 @@ LocalSearchOperator* Solver::MakeOperator( new LinKernighan(vars, secondary_vars, evaluator, /*topt=*/false))); operators.push_back(RevAlloc( new LinKernighan(vars, secondary_vars, evaluator, /*topt=*/true))); - result = ConcatenateOperators(operators); - break; + return ConcatenateOperators(operators); } case Solver::TSPOPT: { - result = RevAlloc( - new TSPOpt(vars, secondary_vars, evaluator, + return RevAlloc( + new TSPOpt(vars, secondary_vars, std::move(evaluator), absl::GetFlag(FLAGS_cp_local_search_tsp_opt_size))); - break; } case Solver::TSPLNS: { - result = RevAlloc( - new TSPLns(vars, secondary_vars, evaluator, + return RevAlloc( + new TSPLns(vars, secondary_vars, std::move(evaluator), absl::GetFlag(FLAGS_cp_local_search_tsp_lns_size))); - break; } default: LOG(FATAL) << "Unknown operator " << op; } - return result; + return nullptr; } namespace { diff --git a/ortools/constraint_solver/routing.cc b/ortools/constraint_solver/routing.cc index 0d5411c4c4f..03dffc13be2 100644 --- a/ortools/constraint_solver/routing.cc +++ b/ortools/constraint_solver/routing.cc @@ -16,8 +16,6 @@ #include #include -#include -#include #include #include #include @@ -30,7 +28,6 @@ #include #include #include -#include #include #include @@ -56,9 +53,12 @@ #include "ortools/base/strong_vector.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" +#include "ortools/constraint_solver/routing_constraints.h" +#include "ortools/constraint_solver/routing_decision_builders.h" #include "ortools/constraint_solver/routing_enums.pb.h" #include "ortools/constraint_solver/routing_filters.h" #include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_insertion_lns.h" #include "ortools/constraint_solver/routing_lp_scheduling.h" #include "ortools/constraint_solver/routing_neighborhoods.h" #include "ortools/constraint_solver/routing_parameters.h" @@ -68,7 +68,6 @@ #include "ortools/constraint_solver/solver_parameters.pb.h" #include "ortools/graph/connected_components.h" #include "ortools/graph/ebert_graph.h" -#include "ortools/graph/graph.h" #include "ortools/graph/linear_assignment.h" #include "ortools/util/bitset.h" #include "ortools/util/optional_boolean.pb.h" @@ -95,9 +94,6 @@ class TwoOpt; // Trace settings -// TODO(user): Move most of the following settings to a model parameter -// proto. - namespace operations_research { std::string RoutingModel::RouteDimensionTravelInfo::DebugString( @@ -140,523 +136,6 @@ std::string RoutingModel::RouteDimensionTravelInfo::TransitionInfo:: DUMP_VARS(y_anchors).str()); } -namespace { -using ResourceGroup = RoutingModel::ResourceGroup; - -// A decision builder which tries to assign values to variables as close as -// possible to target values first. -// TODO(user): Move to CP solver. -class SetValuesFromTargets : public DecisionBuilder { - public: - SetValuesFromTargets(std::vector variables, - std::vector targets) - : variables_(std::move(variables)), - targets_(std::move(targets)), - index_(0), - steps_(variables_.size(), 0) { - DCHECK_EQ(variables_.size(), targets_.size()); - } - Decision* Next(Solver* const solver) override { - int index = index_.Value(); - while (index < variables_.size() && variables_[index]->Bound()) { - ++index; - } - index_.SetValue(solver, index); - if (index >= variables_.size()) return nullptr; - const int64_t variable_min = variables_[index]->Min(); - const int64_t variable_max = variables_[index]->Max(); - // Target can be before, inside, or after the variable range. - // We do a trichotomy on this for clarity. - if (targets_[index] <= variable_min) { - return solver->MakeAssignVariableValue(variables_[index], variable_min); - } else if (targets_[index] >= variable_max) { - return solver->MakeAssignVariableValue(variables_[index], variable_max); - } else { - int64_t step = steps_[index]; - int64_t value = CapAdd(targets_[index], step); - // If value is out of variable's range, we can remove the interval of - // values already explored (which can make the solver fail) and - // recall Next() to get back into the trichotomy above. - if (value < variable_min || variable_max < value) { - step = GetNextStep(step); - value = CapAdd(targets_[index], step); - if (step > 0) { - // Values in [variable_min, value) were already explored. - variables_[index]->SetMin(value); - } else { - // Values in (value, variable_max] were already explored. - variables_[index]->SetMax(value); - } - return Next(solver); - } - steps_.SetValue(solver, index, GetNextStep(step)); - return solver->MakeAssignVariableValueOrDoNothing(variables_[index], - value); - } - } - - private: - int64_t GetNextStep(int64_t step) const { - return (step > 0) ? -step : CapSub(1, step); - } - const std::vector variables_; - const std::vector targets_; - Rev index_; - RevArray steps_; -}; - -} // namespace - -DecisionBuilder* MakeSetValuesFromTargets(Solver* solver, - std::vector variables, - std::vector targets) { - return solver->RevAlloc( - new SetValuesFromTargets(std::move(variables), std::move(targets))); -} - -namespace { - -bool DimensionFixedTransitsEqualTransitEvaluatorForVehicle( - const RoutingDimension& dimension, int vehicle) { - const RoutingModel* const model = dimension.model(); - int node = model->Start(vehicle); - while (!model->IsEnd(node)) { - if (!model->NextVar(node)->Bound()) { - return false; - } - const int next = model->NextVar(node)->Value(); - if (dimension.transit_evaluator(vehicle)(node, next) != - dimension.FixedTransitVar(node)->Value()) { - return false; - } - node = next; - } - return true; -} - -bool DimensionFixedTransitsEqualTransitEvaluators( - const RoutingDimension& dimension) { - for (int vehicle = 0; vehicle < dimension.model()->vehicles(); vehicle++) { - if (!DimensionFixedTransitsEqualTransitEvaluatorForVehicle(dimension, - vehicle)) { - return false; - } - } - return true; -} - -// Concatenates cumul_values and break_values into 'values', and generates the -// corresponding 'variables' vector. -void ConcatenateRouteCumulAndBreakVarAndValues( - const RoutingDimension& dimension, int vehicle, - const std::vector& cumul_values, - const std::vector& break_values, std::vector* variables, - std::vector* values) { - *values = cumul_values; - variables->clear(); - const RoutingModel& model = *dimension.model(); - { - int current = model.Start(vehicle); - while (true) { - variables->push_back(dimension.CumulVar(current)); - if (!model.IsEnd(current)) { - current = model.NextVar(current)->Value(); - } else { - break; - } - } - } - // Setting the cumuls of path start/end first is more efficient than - // setting the cumuls in order of path appearance, because setting start - // and end cumuls gives an opportunity to fix all cumuls with two - // decisions instead of |path| decisions. - // To this effect, we put end cumul just after the start cumul. - std::swap(variables->at(1), variables->back()); - std::swap(values->at(1), values->back()); - if (dimension.HasBreakConstraints()) { - for (IntervalVar* interval : - dimension.GetBreakIntervalsOfVehicle(vehicle)) { - variables->push_back(interval->SafeStartExpr(0)->Var()); - variables->push_back(interval->SafeEndExpr(0)->Var()); - } - values->insert(values->end(), break_values.begin(), break_values.end()); - } - // Value kint64min signals an unoptimized variable, set to min instead. - for (int j = 0; j < values->size(); ++j) { - if (values->at(j) == std::numeric_limits::min()) { - values->at(j) = variables->at(j)->Min(); - } - } - DCHECK_EQ(variables->size(), values->size()); -} - -class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { - public: - SetCumulsFromLocalDimensionCosts( - LocalDimensionCumulOptimizer* local_optimizer, - LocalDimensionCumulOptimizer* local_mp_optimizer, SearchMonitor* monitor, - bool optimize_and_pack = false, - std::vector - dimension_travel_info_per_route = {}) - : local_optimizer_(local_optimizer), - local_mp_optimizer_(local_mp_optimizer), - monitor_(monitor), - optimize_and_pack_(optimize_and_pack), - dimension_travel_info_per_route_( - std::move(dimension_travel_info_per_route)) { - DCHECK(dimension_travel_info_per_route_.empty() || - dimension_travel_info_per_route_.size() == - local_optimizer_->dimension()->model()->vehicles()); - const RoutingDimension* const dimension = local_optimizer->dimension(); - const std::vector& resource_groups = - dimension->model()->GetDimensionResourceGroupIndices(dimension); - DCHECK_LE(resource_groups.size(), optimize_and_pack ? 1 : 0); - resource_group_index_ = resource_groups.empty() ? -1 : resource_groups[0]; - } - - Decision* Next(Solver* const solver) override { - const RoutingDimension& dimension = *local_optimizer_->dimension(); - RoutingModel* const model = dimension.model(); - // The following boolean variable indicates if the solver should fail, in - // order to postpone the Fail() call until after the for loop, so there are - // no memory leaks related to the cumul_values vector. - bool should_fail = false; - for (int vehicle = 0; vehicle < model->vehicles(); ++vehicle) { - solver->TopPeriodicCheck(); - // TODO(user): Investigate if we should skip unused vehicles. - DCHECK(DimensionFixedTransitsEqualTransitEvaluatorForVehicle(dimension, - vehicle)); - const bool vehicle_has_break_constraint = - dimension.HasBreakConstraints() && - !dimension.GetBreakIntervalsOfVehicle(vehicle).empty(); - LocalDimensionCumulOptimizer* const optimizer = - vehicle_has_break_constraint ? local_mp_optimizer_ : local_optimizer_; - DCHECK(optimizer != nullptr); - std::vector cumul_values; - std::vector break_start_end_values; - const DimensionSchedulingStatus status = - ComputeCumulAndBreakValuesForVehicle( - optimizer, vehicle, &cumul_values, &break_start_end_values); - if (status == DimensionSchedulingStatus::INFEASIBLE) { - should_fail = true; - break; - } - // If relaxation is not feasible, try the MILP optimizer. - if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { - DCHECK(local_mp_optimizer_ != nullptr); - if (ComputeCumulAndBreakValuesForVehicle(local_mp_optimizer_, vehicle, - &cumul_values, - &break_start_end_values) == - DimensionSchedulingStatus::INFEASIBLE) { - should_fail = true; - break; - } - } else { - DCHECK(status == DimensionSchedulingStatus::OPTIMAL); - } - // Concatenate cumul_values and break_start_end_values into cp_values, - // generate corresponding cp_variables vector. - std::vector cp_variables; - std::vector cp_values; - ConcatenateRouteCumulAndBreakVarAndValues( - dimension, vehicle, cumul_values, break_start_end_values, - &cp_variables, &cp_values); - if (!solver->SolveAndCommit( - MakeSetValuesFromTargets(solver, std::move(cp_variables), - std::move(cp_values)), - monitor_)) { - should_fail = true; - break; - } - } - if (should_fail) { - solver->Fail(); - } - return nullptr; - } - - private: - using Resource = RoutingModel::ResourceGroup::Resource; - using RouteDimensionTravelInfo = RoutingModel::RouteDimensionTravelInfo; - - DimensionSchedulingStatus ComputeCumulAndBreakValuesForVehicle( - LocalDimensionCumulOptimizer* optimizer, int vehicle, - std::vector* cumul_values, - std::vector* break_start_end_values) { - cumul_values->clear(); - break_start_end_values->clear(); - RoutingModel* const model = optimizer->dimension()->model(); - const auto next = [model](int64_t n) { return model->NextVar(n)->Value(); }; - const RouteDimensionTravelInfo& dimension_travel_info = - dimension_travel_info_per_route_.empty() - ? RouteDimensionTravelInfo() - : dimension_travel_info_per_route_[vehicle]; - if (optimize_and_pack_) { - const int resource_index = - resource_group_index_ < 0 - ? -1 - : model->ResourceVar(vehicle, resource_group_index_)->Value(); - const Resource* const resource = - resource_index < 0 ? nullptr - : &model->GetResourceGroup(resource_group_index_) - ->GetResource(resource_index); - return optimizer->ComputePackedRouteCumuls( - vehicle, next, dimension_travel_info, resource, cumul_values, - break_start_end_values); - } else { - // TODO(user): Add the resource to the call in this case too! - return optimizer->ComputeRouteCumuls(vehicle, next, dimension_travel_info, - cumul_values, - break_start_end_values); - } - } - - LocalDimensionCumulOptimizer* const local_optimizer_; - LocalDimensionCumulOptimizer* const local_mp_optimizer_; - // Stores the resource group index of the local_[mp_]optimizer_'s dimension. - int resource_group_index_; - SearchMonitor* const monitor_; - const bool optimize_and_pack_; - const std::vector dimension_travel_info_per_route_; -}; - -class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { - public: - SetCumulsFromGlobalDimensionCosts( - GlobalDimensionCumulOptimizer* global_optimizer, - GlobalDimensionCumulOptimizer* global_mp_optimizer, - SearchMonitor* monitor, bool optimize_and_pack = false, - std::vector - dimension_travel_info_per_route = {}) - : global_optimizer_(global_optimizer), - global_mp_optimizer_(global_mp_optimizer), - monitor_(monitor), - optimize_and_pack_(optimize_and_pack), - dimension_travel_info_per_route_( - std::move(dimension_travel_info_per_route)) { - DCHECK(dimension_travel_info_per_route_.empty() || - dimension_travel_info_per_route_.size() == - global_optimizer_->dimension()->model()->vehicles()); - } - - Decision* Next(Solver* const solver) override { - // The following boolean variable indicates if the solver should fail, in - // order to postpone the Fail() call until after the scope, so there are - // no memory leaks related to the cumul_values vector. - bool should_fail = false; - { - const RoutingDimension* dimension = global_optimizer_->dimension(); - DCHECK(DimensionFixedTransitsEqualTransitEvaluators(*dimension)); - RoutingModel* const model = dimension->model(); - - GlobalDimensionCumulOptimizer* const optimizer = - model->GetDimensionResourceGroupIndices(dimension).empty() - ? global_optimizer_ - : global_mp_optimizer_; - std::vector cumul_values; - std::vector break_start_end_values; - std::vector> resource_indices_per_group; - const DimensionSchedulingStatus status = - ComputeCumulBreakAndResourceValues(optimizer, &cumul_values, - &break_start_end_values, - &resource_indices_per_group); - - if (status == DimensionSchedulingStatus::INFEASIBLE) { - should_fail = true; - } else if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { - // If relaxation is not feasible, try the MILP optimizer. - const DimensionSchedulingStatus mp_status = - ComputeCumulBreakAndResourceValues( - global_mp_optimizer_, &cumul_values, &break_start_end_values, - &resource_indices_per_group); - if (mp_status != DimensionSchedulingStatus::OPTIMAL) { - should_fail = true; - } - } else { - DCHECK(status == DimensionSchedulingStatus::OPTIMAL); - } - if (!should_fail) { - // Concatenate cumul_values and break_start_end_values into cp_values, - // generate corresponding cp_variables vector. - std::vector cp_variables = dimension->cumuls(); - std::vector cp_values; - std::swap(cp_values, cumul_values); - if (dimension->HasBreakConstraints()) { - const int num_vehicles = model->vehicles(); - for (int vehicle = 0; vehicle < num_vehicles; ++vehicle) { - for (IntervalVar* interval : - dimension->GetBreakIntervalsOfVehicle(vehicle)) { - cp_variables.push_back(interval->SafeStartExpr(0)->Var()); - cp_variables.push_back(interval->SafeEndExpr(0)->Var()); - } - } - cp_values.insert(cp_values.end(), break_start_end_values.begin(), - break_start_end_values.end()); - } - for (int rg_index : - model->GetDimensionResourceGroupIndices(dimension)) { - const std::vector& resource_values = - resource_indices_per_group[rg_index]; - DCHECK(!resource_values.empty()); - cp_values.insert(cp_values.end(), resource_values.begin(), - resource_values.end()); - const std::vector& resource_vars = - model->ResourceVars(rg_index); - DCHECK_EQ(resource_vars.size(), resource_values.size()); - cp_variables.insert(cp_variables.end(), resource_vars.begin(), - resource_vars.end()); - } - // 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()) { - cp_values[j] = cp_variables[j]->Min(); - } - } - if (!solver->SolveAndCommit( - MakeSetValuesFromTargets(solver, std::move(cp_variables), - std::move(cp_values)), - monitor_)) { - should_fail = true; - } - } - } - if (should_fail) { - solver->Fail(); - } - return nullptr; - } - - private: - DimensionSchedulingStatus ComputeCumulBreakAndResourceValues( - GlobalDimensionCumulOptimizer* optimizer, - std::vector* cumul_values, - std::vector* break_start_end_values, - std::vector>* resource_indices_per_group) { - DCHECK_NE(optimizer, nullptr); - cumul_values->clear(); - break_start_end_values->clear(); - resource_indices_per_group->clear(); - RoutingModel* const model = optimizer->dimension()->model(); - const auto next = [model](int64_t n) { return model->NextVar(n)->Value(); }; - return optimize_and_pack_ - ? optimizer->ComputePackedCumuls( - next, dimension_travel_info_per_route_, cumul_values, - break_start_end_values, resource_indices_per_group) - : optimizer->ComputeCumuls( - next, dimension_travel_info_per_route_, cumul_values, - break_start_end_values, resource_indices_per_group); - } - - GlobalDimensionCumulOptimizer* const global_optimizer_; - GlobalDimensionCumulOptimizer* const global_mp_optimizer_; - SearchMonitor* const monitor_; - const bool optimize_and_pack_; - const std::vector - dimension_travel_info_per_route_; -}; - -class SetCumulsFromResourceAssignmentCosts : public DecisionBuilder { - public: - SetCumulsFromResourceAssignmentCosts( - LocalDimensionCumulOptimizer* lp_optimizer, - LocalDimensionCumulOptimizer* mp_optimizer, SearchMonitor* monitor) - : model_(*lp_optimizer->dimension()->model()), - dimension_(*lp_optimizer->dimension()), - lp_optimizer_(lp_optimizer), - mp_optimizer_(mp_optimizer), - rg_index_(model_.GetDimensionResourceGroupIndex(&dimension_)), - resource_group_(*model_.GetResourceGroup(rg_index_)), - monitor_(monitor) {} - - Decision* Next(Solver* const solver) override { - bool should_fail = false; - { - const int num_vehicles = model_.vehicles(); - std::vector> assignment_costs(num_vehicles); - std::vector>> cumul_values(num_vehicles); - std::vector>> break_values(num_vehicles); - - const auto next = [&model = model_](int64_t n) { - return model.NextVar(n)->Value(); - }; - DCHECK(DimensionFixedTransitsEqualTransitEvaluators(dimension_)); - - for (int v : resource_group_.GetVehiclesRequiringAResource()) { - if (!ComputeVehicleToResourcesAssignmentCosts( - v, resource_group_, next, dimension_.transit_evaluator(v), - /*optimize_vehicle_costs*/ true, lp_optimizer_, mp_optimizer_, - &assignment_costs[v], &cumul_values[v], &break_values[v])) { - should_fail = true; - break; - } - } - - std::vector resource_indices(num_vehicles); - should_fail = - should_fail || - ComputeBestVehicleToResourceAssignment( - resource_group_.GetVehiclesRequiringAResource(), - resource_group_.Size(), - [&assignment_costs](int v) { return &assignment_costs[v]; }, - &resource_indices) < 0; - - if (!should_fail) { - DCHECK_EQ(resource_indices.size(), num_vehicles); - const int num_resources = resource_group_.Size(); - for (int v : resource_group_.GetVehiclesRequiringAResource()) { - if (next(model_.Start(v)) == model_.End(v) && - !model_.IsVehicleUsedWhenEmpty(v)) { - continue; - } - const int resource_index = resource_indices[v]; - DCHECK_GE(resource_index, 0); - DCHECK_EQ(cumul_values[v].size(), num_resources); - DCHECK_EQ(break_values[v].size(), num_resources); - const std::vector& optimal_cumul_values = - cumul_values[v][resource_index]; - const std::vector& optimal_break_values = - break_values[v][resource_index]; - std::vector cp_variables; - std::vector cp_values; - ConcatenateRouteCumulAndBreakVarAndValues( - dimension_, v, optimal_cumul_values, optimal_break_values, - &cp_variables, &cp_values); - - const std::vector& resource_vars = - model_.ResourceVars(rg_index_); - DCHECK_EQ(resource_vars.size(), resource_indices.size()); - cp_variables.insert(cp_variables.end(), resource_vars.begin(), - resource_vars.end()); - cp_values.insert(cp_values.end(), resource_indices.begin(), - resource_indices.end()); - if (!solver->SolveAndCommit( - MakeSetValuesFromTargets(solver, std::move(cp_variables), - std::move(cp_values)), - monitor_)) { - should_fail = true; - break; - } - } - } - } - if (should_fail) { - solver->Fail(); - } - return nullptr; - } - - private: - const RoutingModel& model_; - const RoutingDimension& dimension_; - LocalDimensionCumulOptimizer* lp_optimizer_; - LocalDimensionCumulOptimizer* mp_optimizer_; - const int rg_index_; - const ResourceGroup& resource_group_; - SearchMonitor* const monitor_; -}; - -} // namespace - const Assignment* RoutingModel::PackCumulsOfOptimizerDimensionsFromAssignment( const Assignment* original_assignment, absl::Duration duration_limit, bool* time_limit_was_reached) { @@ -675,6 +154,11 @@ const Assignment* RoutingModel::PackCumulsOfOptimizerDimensionsFromAssignment( std::numeric_limits::max(), std::numeric_limits::max()); + RegularLimit* const cumulative_limit = GetOrCreateCumulativeLimit(); + cumulative_limit->UpdateLimits( + duration_limit, std::numeric_limits::max(), + std::numeric_limits::max(), std::numeric_limits::max()); + // Initialize the packed_assignment with the Next values in the // original_assignment. Assignment* packed_assignment = solver_->MakeAssignment(); @@ -699,21 +183,16 @@ const Assignment* RoutingModel::PackCumulsOfOptimizerDimensionsFromAssignment( // Don't set cumuls of dimensions with a global optimizer. continue; } - decision_builders.push_back( - solver_->RevAlloc(new SetCumulsFromLocalDimensionCosts( - lp_optimizer.get(), mp_optimizer.get(), - GetOrCreateLargeNeighborhoodSearchLimit(), - /*optimize_and_pack=*/true))); + decision_builders.push_back(MakeSetCumulsFromLocalDimensionCosts( + solver_.get(), lp_optimizer.get(), mp_optimizer.get(), cumulative_limit, + /*optimize_and_pack=*/true)); } for (auto& [lp_optimizer, mp_optimizer] : global_dimension_optimizers_) { - decision_builders.push_back( - solver_->RevAlloc(new SetCumulsFromGlobalDimensionCosts( - lp_optimizer.get(), mp_optimizer.get(), - GetOrCreateLargeNeighborhoodSearchLimit(), - /*optimize_and_pack=*/true))); + decision_builders.push_back(MakeSetCumulsFromGlobalDimensionCosts( + solver_.get(), lp_optimizer.get(), mp_optimizer.get(), cumulative_limit, + /*optimize_and_pack=*/true)); } - decision_builders.push_back( - CreateFinalizerForMinimizedAndMaximizedVariables()); + decision_builders.push_back(finalizer_variables_->CreateFinalizer()); DecisionBuilder* restore_pack_and_finalize = solver_->Compose(decision_builders); @@ -753,16 +232,17 @@ void RoutingModel::NodeNeighborsByCostClass::ComputeNeighbors( bool add_vehicle_starts_to_neighbors) { // TODO(user): consider checking search limits. const int size = routing_model.Size(); + const int size_with_vehicle_nodes = size + routing_model.vehicles(); node_index_to_neighbors_by_cost_class_.clear(); if (num_neighbors >= size) { - all_nodes_.resize(routing_model.Size()); + all_nodes_.resize(size); std::iota(all_nodes_.begin(), all_nodes_.end(), 0); return; } - node_index_to_neighbors_by_cost_class_.resize(size); + node_index_to_neighbors_by_cost_class_.resize(size_with_vehicle_nodes); const int num_cost_classes = routing_model.GetCostClassesCount(); - for (int node_index = 0; node_index < size; node_index++) { + for (int node_index = 0; node_index < size_with_vehicle_nodes; node_index++) { node_index_to_neighbors_by_cost_class_[node_index].resize(num_cost_classes); for (int cc = 0; cc < num_cost_classes; cc++) { node_index_to_neighbors_by_cost_class_[node_index][cc] = @@ -772,10 +252,9 @@ void RoutingModel::NodeNeighborsByCostClass::ComputeNeighbors( std::vector> cost_nodes; cost_nodes.reserve(size); - for (int node_index = 0; node_index < size; ++node_index) { - DCHECK(!routing_model.IsEnd(node_index)); - if (routing_model.IsStart(node_index)) { - // For vehicle starts, we consider all nodes. + for (int node_index = 0; node_index < size_with_vehicle_nodes; ++node_index) { + if (routing_model.IsStart(node_index) || routing_model.IsEnd(node_index)) { + // For vehicle starts/ends, we consider all nodes (see below) continue; } @@ -822,6 +301,9 @@ void RoutingModel::NodeNeighborsByCostClass::ComputeNeighbors( if (add_vehicle_starts_to_neighbors) node_neighbors->Set(vehicle_start); node_index_to_neighbors_by_cost_class_[vehicle_start][cost_class]->Set( node_index); + node_index_to_neighbors_by_cost_class_[routing_model.End(vehicle)] + [cost_class] + ->Set(node_index); } } } @@ -865,278 +347,6 @@ RoutingModel::GetOrCreateNodeNeighborsByCostClass( } namespace { -// Constraint which ensures that var != values. -class DifferentFromValues : public Constraint { - public: - DifferentFromValues(Solver* solver, IntVar* var, std::vector values) - : Constraint(solver), var_(var), values_(std::move(values)) {} - void Post() override {} - void InitialPropagate() override { var_->RemoveValues(values_); } - std::string DebugString() const override { return "DifferentFromValues"; } - void Accept(ModelVisitor* const visitor) const override { - visitor->BeginVisitConstraint(RoutingModelVisitor::kRemoveValues, this); - visitor->VisitIntegerVariableArrayArgument(ModelVisitor::kVarsArgument, - {var_}); - visitor->VisitIntegerArrayArgument(ModelVisitor::kValuesArgument, values_); - visitor->EndVisitConstraint(RoutingModelVisitor::kRemoveValues, this); - } - - private: - IntVar* const var_; - const std::vector values_; -}; - -// For each vehicle, computes information on the partially fixed start/end -// chains (based on bound NextVar values): -// - For every 'end_node', the last node of a start chain of a vehicle, -// vehicle_index_of_start_chain_end[end_node] contains the corresponding -// vehicle index. Contains -1 for other nodes. -// - For every vehicle 'v', end_chain_starts[v] contains the first node of the -// end chain of that vehicle. -void ComputeVehicleChainStartEndInfo( - const RoutingModel& model, std::vector* end_chain_starts, - std::vector* vehicle_index_of_start_chain_end) { - vehicle_index_of_start_chain_end->resize(model.Size() + model.vehicles(), -1); - - for (int vehicle = 0; vehicle < model.vehicles(); ++vehicle) { - int64_t node = model.Start(vehicle); - while (!model.IsEnd(node) && model.NextVar(node)->Bound()) { - node = model.NextVar(node)->Value(); - } - vehicle_index_of_start_chain_end->at(node) = vehicle; - } - - *end_chain_starts = ComputeVehicleEndChainStarts(model); -} - -class ResourceAssignmentConstraint : public Constraint { - public: - ResourceAssignmentConstraint( - const ResourceGroup* resource_group, - const std::vector* vehicle_resource_vars, RoutingModel* model) - : Constraint(model->solver()), - model_(*model), - resource_group_(*resource_group), - vehicle_resource_vars_(*vehicle_resource_vars), - vehicle_to_start_bound_vars_per_dimension_(model->vehicles()), - vehicle_to_end_bound_vars_per_dimension_(model->vehicles()) { - DCHECK_EQ(vehicle_resource_vars_.size(), model_.vehicles()); - - const std::vector& dimensions = model_.GetDimensions(); - for (int v = 0; v < model_.vehicles(); v++) { - IntVar* const resource_var = vehicle_resource_vars_[v]; - model->AddToAssignment(resource_var); - // The resource variable must be fixed by the search. - model->AddVariableTargetToFinalizer(resource_var, -1); - - if (!resource_group_.VehicleRequiresAResource(v)) { - continue; - } - - vehicle_to_start_bound_vars_per_dimension_[v].resize(dimensions.size()); - vehicle_to_end_bound_vars_per_dimension_[v].resize(dimensions.size()); - - for (const RoutingModel::DimensionIndex d : - resource_group_.GetAffectedDimensionIndices()) { - const RoutingDimension* const dim = dimensions[d.value()]; - // The vehicle's start/end cumuls must be fixed by the search. - model->AddVariableMinimizedByFinalizer(dim->CumulVar(model_.End(v))); - model->AddVariableMaximizedByFinalizer(dim->CumulVar(model_.Start(v))); - for (ResourceBoundVars* bound_vars : - {&vehicle_to_start_bound_vars_per_dimension_[v][d.value()], - &vehicle_to_end_bound_vars_per_dimension_[v][d.value()]}) { - bound_vars->lower_bound = - solver()->MakeIntVar(std::numeric_limits::min(), - std::numeric_limits::max()); - bound_vars->upper_bound = - solver()->MakeIntVar(std::numeric_limits::min(), - std::numeric_limits::max()); - } - } - } - } - - void Post() override {} - - void InitialPropagate() override { - if (!AllResourceAssignmentsFeasible()) { - solver()->Fail(); - } - SetupResourceConstraints(); - } - - private: - bool AllResourceAssignmentsFeasible() { - DCHECK(!model_.GetResourceGroups().empty()); - - std::vector end_chain_starts; - std::vector vehicle_index_of_start_chain_end; - ComputeVehicleChainStartEndInfo(model_, &end_chain_starts, - &vehicle_index_of_start_chain_end); - const auto next = [&model = model_, &end_chain_starts, - &vehicle_index_of_start_chain_end](int64_t node) { - if (model.NextVar(node)->Bound()) return model.NextVar(node)->Value(); - const int vehicle = vehicle_index_of_start_chain_end[node]; - if (vehicle < 0) { - // The node isn't the last node of a route start chain and is considered - // as unperformed and ignored when evaluating the feasibility of the - // resource assignment. - return node; - } - return end_chain_starts[vehicle]; - }; - - const std::vector& dimensions = model_.GetDimensions(); - for (RoutingModel::DimensionIndex d : - resource_group_.GetAffectedDimensionIndices()) { - if (!ResourceAssignmentFeasibleForDimension(*dimensions[d.value()], - next)) { - return false; - } - } - return true; - } - - bool ResourceAssignmentFeasibleForDimension( - const RoutingDimension& dimension, - const std::function& next) { - LocalDimensionCumulOptimizer* const optimizer = - model_.GetMutableLocalCumulLPOptimizer(dimension); - - if (optimizer == nullptr) return true; - - LocalDimensionCumulOptimizer* const mp_optimizer = - model_.GetMutableLocalCumulMPOptimizer(dimension); - DCHECK_NE(mp_optimizer, nullptr); - const auto transit = [&dimension](int64_t node, int64_t /*next*/) { - // TODO(user): Get rid of this max() by only allowing resources on - // dimensions with positive transits (model.AreVehicleTransitsPositive()). - // TODO(user): The transit lower bounds have not necessarily been - // propagated at this point. Add demons to check the resource assignment - // feasibility after the transit ranges have been propagated. - return std::max(dimension.FixedTransitVar(node)->Min(), 0); - }; - - std::vector> assignment_costs(model_.vehicles()); - for (int v : resource_group_.GetVehiclesRequiringAResource()) { - if (!ComputeVehicleToResourcesAssignmentCosts( - v, resource_group_, next, transit, - /*optimize_vehicle_costs*/ false, - model_.GetMutableLocalCumulLPOptimizer(dimension), - model_.GetMutableLocalCumulMPOptimizer(dimension), - &assignment_costs[v], nullptr, nullptr)) { - return false; - } - } - // TODO(user): Replace this call with a more efficient max-flow, instead - // of running the full min-cost flow. - return ComputeBestVehicleToResourceAssignment( - resource_group_.GetVehiclesRequiringAResource(), - resource_group_.Size(), - [&assignment_costs](int v) { return &assignment_costs[v]; }, - nullptr) >= 0; - } - - void SetupResourceConstraints() { - Solver* const s = solver(); - // Resources cannot be shared, so assigned resources must all be different - // (note that resource_var == -1 means no resource assigned). - s->AddConstraint(s->MakeAllDifferentExcept(vehicle_resource_vars_, -1)); - const std::vector& dimensions = model_.GetDimensions(); - for (int v = 0; v < model_.vehicles(); v++) { - IntVar* const resource_var = vehicle_resource_vars_[v]; - if (!resource_group_.VehicleRequiresAResource(v)) { - resource_var->SetValue(-1); - continue; - } - // vehicle_route_considered_[v] <--> vehicle_res_vars[v] != -1. - s->AddConstraint( - s->MakeEquality(model_.VehicleRouteConsideredVar(v), - s->MakeIsDifferentCstVar(resource_var, -1))); - - // Add dimension cumul constraints. - for (const RoutingModel::DimensionIndex dim_index : - resource_group_.GetAffectedDimensionIndices()) { - const int d = dim_index.value(); - const RoutingDimension* const dim = dimensions[d]; - - // resource_start_lb_var <= cumul[start(v)] <= resource_start_ub_var, - // resource_end_lb_var <= cumul[end(v)] <= resource_end_ub_var - for (bool start_cumul : {true, false}) { - IntVar* const cumul_var = start_cumul ? dim->CumulVar(model_.Start(v)) - : dim->CumulVar(model_.End(v)); - - IntVar* const resource_lb_var = - start_cumul - ? vehicle_to_start_bound_vars_per_dimension_[v][d].lower_bound - : vehicle_to_end_bound_vars_per_dimension_[v][d].lower_bound; - s->AddConstraint(s->MakeLightElement( - [dim, start_cumul, &resource_group = resource_group_](int r) { - if (r < 0) return std::numeric_limits::min(); - return start_cumul ? resource_group.GetResources()[r] - .GetDimensionAttributes(dim) - .start_domain() - .Min() - : resource_group.GetResources()[r] - .GetDimensionAttributes(dim) - .end_domain() - .Min(); - }, - resource_lb_var, resource_var, - [&model = model_]() { - return model.enable_deep_serialization(); - })); - s->AddConstraint(s->MakeGreaterOrEqual(cumul_var, resource_lb_var)); - - IntVar* const resource_ub_var = - start_cumul - ? vehicle_to_start_bound_vars_per_dimension_[v][d].upper_bound - : vehicle_to_end_bound_vars_per_dimension_[v][d].upper_bound; - s->AddConstraint(s->MakeLightElement( - [dim, start_cumul, &resource_group = resource_group_](int r) { - if (r < 0) return std::numeric_limits::max(); - return start_cumul ? resource_group.GetResources()[r] - .GetDimensionAttributes(dim) - .start_domain() - .Max() - : resource_group.GetResources()[r] - .GetDimensionAttributes(dim) - .end_domain() - .Max(); - }, - resource_ub_var, resource_var, - [&model = model_]() { - return model.enable_deep_serialization(); - })); - s->AddConstraint(s->MakeLessOrEqual(cumul_var, resource_ub_var)); - } - } - } - } - - struct ResourceBoundVars { - IntVar* lower_bound; - IntVar* upper_bound; - }; - - const RoutingModel& model_; - const ResourceGroup& resource_group_; - const std::vector& vehicle_resource_vars_; - // The following vectors store the IntVars keeping track of the lower and - // upper bound on the cumul start/end of every vehicle (requiring a resource) - // based on its assigned resource (determined by vehicle_resource_vars_). - std::vector> - vehicle_to_start_bound_vars_per_dimension_; - std::vector> - vehicle_to_end_bound_vars_per_dimension_; -}; - -Constraint* MakeResourceConstraint( - const ResourceGroup* resource_group, - const std::vector* vehicle_resource_vars, RoutingModel* model) { - return model->solver()->RevAlloc(new ResourceAssignmentConstraint( - resource_group, vehicle_resource_vars, model)); -} // Evaluators template @@ -1144,18 +354,6 @@ static int64_t ReturnZero(A, B) { return 0; } -bool TransitCallbackPositive(const RoutingTransitCallback2& callback, int size1, - int size2) { - for (int i = 0; i < size1; i++) { - for (int j = 0; j < size2; j++) { - if (callback(i, j) < 0) { - return false; - } - } - } - return true; -} - } // namespace // ----- Routing model ----- @@ -1170,9 +368,21 @@ const RoutingModel::DimensionIndex RoutingModel::kNoDimension(-1); RoutingModel::RoutingModel(const RoutingIndexManager& index_manager) : RoutingModel(index_manager, DefaultRoutingModelParameters()) {} +namespace { +std::unique_ptr CreateSolverFromParameters( + const RoutingModelParameters& parameters) { + VLOG(1) << "Model parameters:\n" << parameters; + ConstraintSolverParameters solver_parameters = + parameters.has_solver_parameters() ? parameters.solver_parameters() + : Solver::DefaultSolverParameters(); + return std::make_unique("Routing", solver_parameters); +} +} // namespace + RoutingModel::RoutingModel(const RoutingIndexManager& index_manager, const RoutingModelParameters& parameters) - : nodes_(index_manager.num_nodes()), + : solver_(CreateSolverFromParameters(parameters)), + nodes_(index_manager.num_nodes()), vehicles_(index_manager.num_vehicles()), max_active_vehicles_(vehicles_), fixed_cost_of_vehicle_(vehicles_, 0), @@ -1193,19 +403,18 @@ RoutingModel::RoutingModel(const RoutingIndexManager& index_manager, has_temporal_type_requirements_(false), num_visit_types_(0), paths_metadata_(index_manager), - manager_(index_manager) { + manager_(index_manager), + finalizer_variables_( + std::make_unique(solver_.get())) { // Initialize vehicle costs to the zero evaluator. vehicle_to_transit_cost_.assign( - vehicles_, RegisterTransitCallback(ReturnZero)); + vehicles_, RegisterTransitCallback( + ReturnZero, + RoutingModel::kTransitEvaluatorSignPositiveOrZero)); // Active caching after initializing vehicle_to_transit_cost_ to avoid // uselessly caching ReturnZero. cache_callbacks_ = (nodes_ <= parameters.max_callback_cache_size()); - VLOG(1) << "Model parameters:\n" << parameters; - ConstraintSolverParameters solver_parameters = - parameters.has_solver_parameters() ? parameters.solver_parameters() - : Solver::DefaultSolverParameters(); - solver_ = std::make_unique("Routing", solver_parameters); // TODO(user): Remove when removal of NodeIndex is complete. start_end_count_ = index_manager.num_unique_depots(); Initialize(); @@ -1267,80 +476,85 @@ RoutingModel::~RoutingModel() { gtl::STLDeleteElements(&index_functions_delete); } -namespace { -int RegisterCallback(RoutingTransitCallback2 callback, bool is_positive, - RoutingModel* model) { - if (is_positive) { - return model->RegisterPositiveTransitCallback(std::move(callback)); - } - return model->RegisterTransitCallback(std::move(callback)); -} - -int RegisterUnaryCallback(RoutingTransitCallback1 callback, bool is_positive, - RoutingModel* model) { - if (is_positive) { - return model->RegisterPositiveUnaryTransitCallback(std::move(callback)); - } - return model->RegisterUnaryTransitCallback(std::move(callback)); -} -} // namespace - int RoutingModel::RegisterUnaryTransitVector(std::vector values) { - bool is_positive = std::all_of(std::cbegin(values), std::cend(values), - [](int64_t transit) { return transit >= 0; }); - return RegisterUnaryCallback( + TransitEvaluatorSign sign = kTransitEvaluatorSignUnknown; + if (std::all_of(std::cbegin(values), std::cend(values), + [](int64_t transit) { return transit >= 0; })) { + sign = kTransitEvaluatorSignPositiveOrZero; + } else if (std::all_of(std::cbegin(values), std::cend(values), + [](int64_t transit) { return transit <= 0; })) { + sign = kTransitEvaluatorSignNegativeOrZero; + } + return RegisterUnaryTransitCallback( [this, values = std::move(values)](int64_t i) { return values[manager_.IndexToNode(i).value()]; }, - is_positive, this); + sign); } -int RoutingModel::RegisterUnaryTransitCallback(TransitCallback1 callback) { +int RoutingModel::RegisterUnaryTransitCallback(TransitCallback1 callback, + TransitEvaluatorSign sign) { const int index = unary_transit_evaluators_.size(); unary_transit_evaluators_.push_back(std::move(callback)); - return RegisterTransitCallback([this, index](int i, int /*j*/) { - return unary_transit_evaluators_[index](i); - }); + return RegisterTransitCallback( + [this, index](int i, int /*j*/) { + return unary_transit_evaluators_[index](i); + }, + sign); } int RoutingModel::RegisterTransitMatrix( std::vector /*needed_for_swig*/> values) { - bool all_transits_positive = true; + // TODO(user): when we move away from std::functions, use a (potentially + // vectorized) helper to compute the sign of a range. + bool all_transits_geq_zero = true; + bool all_transits_leq_zero = true; for (const std::vector& transit_values : values) { - all_transits_positive = - std::all_of(std::cbegin(transit_values), std::cend(transit_values), - [](int64_t transit) { return transit >= 0; }); - if (!all_transits_positive) { - break; - } - } - return RegisterCallback( + for (const int64_t value : transit_values) { + all_transits_leq_zero &= value <= 0; + all_transits_geq_zero &= value >= 0; + } + if (!all_transits_geq_zero && !all_transits_leq_zero) break; + } + const TransitEvaluatorSign sign = + all_transits_geq_zero + ? kTransitEvaluatorSignPositiveOrZero + : (all_transits_leq_zero ? kTransitEvaluatorSignNegativeOrZero + : kTransitEvaluatorSignUnknown); + return RegisterTransitCallback( [this, values = std::move(values)](int64_t i, int64_t j) { return values[manager_.IndexToNode(i).value()] [manager_.IndexToNode(j).value()]; }, - all_transits_positive, this); + sign); } -int RoutingModel::RegisterPositiveUnaryTransitCallback( - TransitCallback1 callback) { - is_transit_evaluator_positive_.push_back(true); - DCHECK(TransitCallbackPositive( - [&callback](int i, int) { return callback(i); }, Size() + vehicles(), 1)); - return RegisterUnaryTransitCallback(std::move(callback)); -} - -int RoutingModel::RegisterTransitCallback(TransitCallback2 callback) { +int RoutingModel::RegisterTransitCallback(TransitCallback2 callback, + TransitEvaluatorSign sign) { if (cache_callbacks_) { + TransitEvaluatorSign actual_sign = sign; const int size = Size() + vehicles(); std::vector cache(size * size, 0); + bool all_transits_geq_zero = true; + bool all_transits_leq_zero = true; for (int i = 0; i < size; ++i) { for (int j = 0; j < size; ++j) { - cache[i * size + j] = callback(i, j); + const int64_t value = callback(i, j); + cache[i * size + j] = value; + all_transits_geq_zero &= value >= 0; + all_transits_leq_zero &= value <= 0; } } + actual_sign = + all_transits_geq_zero + ? kTransitEvaluatorSignPositiveOrZero + : (all_transits_leq_zero ? kTransitEvaluatorSignNegativeOrZero + : kTransitEvaluatorSignUnknown); transit_evaluators_.push_back( - [cache, size](int64_t i, int64_t j) { return cache[i * size + j]; }); + [cache = std::move(cache), size](int64_t i, int64_t j) { + return cache[i * size + j]; + }); + DCHECK(sign == kTransitEvaluatorSignUnknown || actual_sign == sign); } else { transit_evaluators_.push_back(std::move(callback)); } @@ -1348,21 +562,10 @@ int RoutingModel::RegisterTransitCallback(TransitCallback2 callback) { DCHECK_EQ(transit_evaluators_.size(), unary_transit_evaluators_.size() + 1); unary_transit_evaluators_.push_back(nullptr); } - if (transit_evaluators_.size() != is_transit_evaluator_positive_.size()) { - DCHECK_EQ(transit_evaluators_.size(), - is_transit_evaluator_positive_.size() + 1); - is_transit_evaluator_positive_.push_back(false); - } + transit_evaluator_sign_.push_back(sign); return transit_evaluators_.size() - 1; } -int RoutingModel::RegisterPositiveTransitCallback(TransitCallback2 callback) { - is_transit_evaluator_positive_.push_back(true); - DCHECK(TransitCallbackPositive(callback, Size() + vehicles(), - Size() + vehicles())); - return RegisterTransitCallback(std::move(callback)); -} - int RoutingModel::RegisterStateDependentTransitCallback( VariableIndexEvaluator2 callback) { state_dependent_transit_evaluators_cache_.push_back( @@ -1469,9 +672,11 @@ bool RoutingModel::InitializeDimensionInternal( std::pair RoutingModel::AddConstantDimensionWithSlack( int64_t value, int64_t capacity, int64_t slack_max, bool fix_start_cumul_to_zero, const std::string& dimension_name) { + const TransitEvaluatorSign sign = value < 0 + ? kTransitEvaluatorSignNegativeOrZero + : kTransitEvaluatorSignPositiveOrZero; const int evaluator_index = - RegisterUnaryCallback([value](int64_t) { return value; }, - /*is_positive=*/value >= 0, this); + RegisterUnaryTransitCallback([value](int64_t) { return value; }, sign); return std::make_pair(evaluator_index, AddDimension(evaluator_index, slack_max, capacity, fix_start_cumul_to_zero, dimension_name)); @@ -1729,13 +934,16 @@ RoutingDimension* RoutingModel::GetMutableDimension( } // ResourceGroup +namespace { +using ResourceGroup = RoutingModel::ResourceGroup; +} // namespace + ResourceGroup::Attributes::Attributes() : start_domain_(Domain::AllValues()), end_domain_(Domain::AllValues()) { /// The default attributes have unconstrained start/end domains. } -RoutingModel::ResourceGroup::Attributes::Attributes(Domain start_domain, - Domain end_domain) +ResourceGroup::Attributes::Attributes(Domain start_domain, Domain end_domain) : start_domain_(std::move(start_domain)), end_domain_(std::move(end_domain)) {} @@ -1780,8 +988,8 @@ int RoutingModel::AddResourceGroup() { return rg_index; } -int RoutingModel::ResourceGroup::AddResource( - Attributes attributes, const RoutingDimension* dimension) { +int ResourceGroup::AddResource(Attributes attributes, + const RoutingDimension* dimension) { resources_.push_back(Resource(model_)); resources_.back().SetDimensionAttributes(std::move(attributes), dimension); @@ -1797,7 +1005,7 @@ int RoutingModel::ResourceGroup::AddResource( return resources_.size() - 1; } -void RoutingModel::ResourceGroup::NotifyVehicleRequiresAResource(int vehicle) { +void ResourceGroup::NotifyVehicleRequiresAResource(int vehicle) { DCHECK_LT(vehicle, vehicle_requires_resource_.size()); if (vehicle_requires_resource_[vehicle]) return; vehicle_requires_resource_[vehicle] = true; @@ -2842,8 +2050,8 @@ void RoutingModel::CloseModelWithParameters( // Reduce domain of next variables. for (int i = 0; i < size; ++i) { // No variable can point back to a start. - solver_->AddConstraint(solver_->RevAlloc(new DifferentFromValues( - solver_.get(), nexts_[i], paths_metadata_.Starts()))); + solver_->AddConstraint(MakeDifferentFromValues(solver_.get(), nexts_[i], + paths_metadata_.Starts())); // Extra constraint to state an active node can't point to itself. solver_->AddConstraint( solver_->MakeIsDifferentCstCt(nexts_[i], i, active_[i])); @@ -2870,8 +2078,8 @@ void RoutingModel::CloseModelWithParameters( forbidden_ends.push_back(End(j)); } } - solver_->AddConstraint(solver_->RevAlloc(new DifferentFromValues( - solver_.get(), nexts_[Start(i)], std::move(forbidden_ends)))); + solver_->AddConstraint(MakeDifferentFromValues( + solver_.get(), nexts_[Start(i)], std::move(forbidden_ends))); } // Constraining is_bound_to_end_ variables. @@ -3204,169 +2412,6 @@ void RoutingModel::CloseModelWithParameters( SetupSearch(parameters); } -namespace { -// A decision builder that tries to set variables to their value in the last -// solution, if their corresponding vehicle path has not changed. -// This tries to constrain all such variables in one shot in order to speed up -// instantiation. -// TODO(user): try to use Assignment instead of MakeAssignment(), -// try to record and restore the min/max instead of a single value. -class RestoreDimensionValuesForUnchangedRoutes : public DecisionBuilder { - public: - explicit RestoreDimensionValuesForUnchangedRoutes(RoutingModel* model) - : model_(model) { - model_->AddAtSolutionCallback([this]() { AtSolution(); }); - next_last_value_.resize(model_->Nexts().size(), -1); - } - - // In a given branch of a search tree, this decision builder only returns - // a Decision once, the first time it is called in that branch. - Decision* Next(Solver* const s) override { - if (!must_return_decision_) return nullptr; - s->SaveAndSetValue(&must_return_decision_, false); - return MakeDecision(s); - } - - private: - // Initialize() is lazy to make sure all dimensions have been instantiated - // when initialization is done. - void Initialize() { - is_initialized_ = true; - const int num_nodes = model_->VehicleVars().size(); - node_to_integer_variable_indices_.resize(num_nodes); - node_to_interval_variable_indices_.resize(num_nodes); - // Search for dimension variables that correspond to input variables. - for (const std::string& dimension_name : model_->GetAllDimensionNames()) { - const RoutingDimension& dimension = - model_->GetDimensionOrDie(dimension_name); - // Search among cumuls and slacks, and attach them to corresponding nodes. - for (const std::vector& dimension_variables : - {dimension.cumuls(), dimension.slacks()}) { - const int num_dimension_variables = dimension_variables.size(); - DCHECK_LE(num_dimension_variables, num_nodes); - for (int node = 0; node < num_dimension_variables; ++node) { - node_to_integer_variable_indices_[node].push_back( - integer_variables_.size()); - integer_variables_.push_back(dimension_variables[node]); - } - } - // Search for break start/end variables, attach them to vehicle starts. - for (int vehicle = 0; vehicle < model_->vehicles(); ++vehicle) { - if (!dimension.HasBreakConstraints()) continue; - const int vehicle_start = model_->Start(vehicle); - for (IntervalVar* interval : - dimension.GetBreakIntervalsOfVehicle(vehicle)) { - node_to_interval_variable_indices_[vehicle_start].push_back( - interval_variables_.size()); - interval_variables_.push_back(interval); - } - } - } - integer_variables_last_min_.resize(integer_variables_.size()); - interval_variables_last_start_min_.resize(interval_variables_.size()); - interval_variables_last_end_max_.resize(interval_variables_.size()); - } - - Decision* MakeDecision(Solver* const s) { - if (!is_initialized_) return nullptr; - // Collect vehicles that have not changed. - std::vector unchanged_vehicles; - const int num_vehicles = model_->vehicles(); - for (int v = 0; v < num_vehicles; ++v) { - bool unchanged = true; - for (int current = model_->Start(v); !model_->IsEnd(current); - current = next_last_value_[current]) { - if (!model_->NextVar(current)->Bound() || - next_last_value_[current] != model_->NextVar(current)->Value()) { - unchanged = false; - break; - } - } - if (unchanged) unchanged_vehicles.push_back(v); - } - // If all routes are unchanged, the solver might be trying to do a full - // reschedule. Do nothing. - if (unchanged_vehicles.size() == num_vehicles) return nullptr; - - // Collect cumuls and slacks of unchanged routes to be assigned a value. - std::vector vars; - std::vector values; - for (const int vehicle : unchanged_vehicles) { - for (int current = model_->Start(vehicle); true; - current = next_last_value_[current]) { - for (const int index : node_to_integer_variable_indices_[current]) { - vars.push_back(integer_variables_[index]); - values.push_back(integer_variables_last_min_[index]); - } - for (const int index : node_to_interval_variable_indices_[current]) { - const int64_t start_min = interval_variables_last_start_min_[index]; - const int64_t end_max = interval_variables_last_end_max_[index]; - if (start_min < end_max) { - vars.push_back(interval_variables_[index]->SafeStartExpr(0)->Var()); - values.push_back(interval_variables_last_start_min_[index]); - vars.push_back(interval_variables_[index]->SafeEndExpr(0)->Var()); - values.push_back(interval_variables_last_end_max_[index]); - } else { - vars.push_back(interval_variables_[index]->PerformedExpr()->Var()); - values.push_back(0); - } - } - if (model_->IsEnd(current)) break; - } - } - return s->MakeAssignVariablesValuesOrDoNothing(vars, values); - } - - void AtSolution() { - if (!is_initialized_) Initialize(); - const int num_integers = integer_variables_.size(); - // Variables may not be fixed at solution time, - // the decision builder is fine with the Min() of the unfixed variables. - for (int i = 0; i < num_integers; ++i) { - integer_variables_last_min_[i] = integer_variables_[i]->Min(); - } - const int num_intervals = interval_variables_.size(); - for (int i = 0; i < num_intervals; ++i) { - const bool is_performed = interval_variables_[i]->MustBePerformed(); - interval_variables_last_start_min_[i] = - is_performed ? interval_variables_[i]->StartMin() : 0; - interval_variables_last_end_max_[i] = - is_performed ? interval_variables_[i]->EndMax() : -1; - } - const int num_nodes = next_last_value_.size(); - for (int node = 0; node < num_nodes; ++node) { - if (model_->IsEnd(node)) continue; - next_last_value_[node] = model_->NextVar(node)->Value(); - } - } - - // Input data. - RoutingModel* const model_; - - // The valuation of the last solution. - std::vector next_last_value_; - // For every node, the indices of integer_variables_ and interval_variables_ - // that correspond to that node. - std::vector> node_to_integer_variable_indices_; - std::vector> node_to_interval_variable_indices_; - // Variables and the value they had in the previous solution. - std::vector integer_variables_; - std::vector integer_variables_last_min_; - std::vector interval_variables_; - std::vector interval_variables_last_start_min_; - std::vector interval_variables_last_end_max_; - - bool is_initialized_ = false; - bool must_return_decision_ = true; -}; -} // namespace - -DecisionBuilder* MakeRestoreDimensionValuesForUnchangedRoutes( - RoutingModel* model) { - return model->solver()->RevAlloc( - new RestoreDimensionValuesForUnchangedRoutes(model)); -} - void RoutingModel::AddSearchMonitor(SearchMonitor* const monitor) { monitors_.push_back(monitor); } @@ -3374,22 +2419,34 @@ void RoutingModel::AddSearchMonitor(SearchMonitor* const monitor) { namespace { class AtSolutionCallbackMonitor : public SearchMonitor { public: - AtSolutionCallbackMonitor(Solver* solver, std::function callback) - : SearchMonitor(solver), callback_(std::move(callback)) {} + AtSolutionCallbackMonitor(Solver* solver, std::function callback, + bool track_unchecked_neighbors) + : SearchMonitor(solver), + callback_(std::move(callback)), + track_unchecked_neighbors_(track_unchecked_neighbors) {} bool AtSolution() override { callback_(); return false; } - void Install() override { ListenToEvent(Solver::MonitorEvent::kAtSolution); } + void AcceptUncheckedNeighbor() override { AtSolution(); } + void Install() override { + ListenToEvent(Solver::MonitorEvent::kAtSolution); + if (track_unchecked_neighbors_) { + ListenToEvent(Solver::MonitorEvent::kAcceptUncheckedNeighbor); + } + } private: std::function callback_; + const bool track_unchecked_neighbors_; }; } // namespace -void RoutingModel::AddAtSolutionCallback(std::function callback) { - AtSolutionCallbackMonitor* const monitor = solver_->RevAlloc( - new AtSolutionCallbackMonitor(solver_.get(), std::move(callback))); +void RoutingModel::AddAtSolutionCallback(std::function callback, + bool track_unchecked_neighbors) { + AtSolutionCallbackMonitor* const monitor = + solver_->RevAlloc(new AtSolutionCallbackMonitor( + solver_.get(), std::move(callback), track_unchecked_neighbors)); at_solution_monitors_.push_back(monitor); AddSearchMonitor(monitor); } @@ -4704,6 +3761,17 @@ RegularLimit* RoutingModel::GetOrCreateLimit() { return limit_; } +RegularLimit* RoutingModel::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); + } + return cumulative_limit_; +} + RegularLimit* RoutingModel::GetOrCreateLocalSearchLimit() { if (ls_limit_ == nullptr) { ls_limit_ = solver_->MakeLimit(absl::InfiniteDuration(), @@ -4819,8 +3887,10 @@ void RoutingModel::CreateNeighborhoodOperators( local_search_operators_[EXCHANGE] = CreateOperatorWithNeighborsRatio(neighbors_ratio_used, get_neighbors); - local_search_operators_[CROSS] = CreateCPOperator(); - local_search_operators_[TWO_OPT] = CreateCPOperator(); + local_search_operators_[CROSS] = CreateOperatorWithNeighborsRatio( + neighbors_ratio_used, get_neighbors); + local_search_operators_[TWO_OPT] = CreateOperatorWithNeighborsRatio( + neighbors_ratio_used, get_neighbors); local_search_operators_[RELOCATE_AND_MAKE_ACTIVE] = CreateCPOperator(); local_search_operators_[MAKE_ACTIVE_AND_RELOCATE] = @@ -4858,8 +3928,13 @@ void RoutingModel::CreateNeighborhoodOperators( local_search_operators_[LIGHT_RELOCATE_PAIR] = solver_->ConcatenateOperators(light_relocate_pair_operators); local_search_operators_[EXCHANGE_PAIR] = solver_->ConcatenateOperators( - {CreatePairOperator(), - CreatePairOperator()}); + {CreatePairOperator(neighbors_ratio_used, + get_neighbors), + solver_->RevAlloc(new SwapIndexPairOperator( + nexts_, + CostsAreHomogeneousAcrossVehicles() ? std::vector() + : vehicle_vars_, + pickup_delivery_pairs_))}); local_search_operators_[EXCHANGE_RELOCATE_PAIR] = CreatePairOperator(); local_search_operators_[RELOCATE_NEIGHBORS] = @@ -5382,6 +4457,15 @@ void RoutingModel::StoreDimensionCumulOptimizers( offset = std::min(offset, dimension->CumulVar(Start(vehicle))->Min() - 1); } + if (dimension->HasBreakConstraints()) { + for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { + for (const IntervalVar* br : + dimension->GetBreakIntervalsOfVehicle(vehicle)) { + offset = std::min(offset, CapSub(br->StartMin(), 1)); + } + } + } + dimension->SetOffsetForGlobalOptimizer(std::max(Zero(), offset)); } } @@ -5398,10 +4482,17 @@ void RoutingModel::StoreDimensionCumulOptimizers( has_span_limit = true; } DCHECK_GE(dimension->CumulVar(Start(vehicle))->Min(), 0); - vehicle_offsets[vehicle] = - dimension->AreVehicleTransitsPositive(vehicle) - ? std::max(Zero(), dimension->CumulVar(Start(vehicle))->Min() - 1) - : 0; + int64_t offset = 0; + if (dimension->AreVehicleTransitsPositive(vehicle)) { + offset = CapSub(dimension->CumulVar(Start(vehicle))->Min(), 1); + if (dimension->HasBreakConstraints()) { + for (const IntervalVar* br : + dimension->GetBreakIntervalsOfVehicle(vehicle)) { + offset = std::min(offset, CapSub(br->StartMin(), 1)); + } + } + } + vehicle_offsets[vehicle] = std::max(0, offset); } bool has_soft_lower_bound = false; bool has_soft_upper_bound = false; @@ -5500,32 +4591,6 @@ RoutingModel::GetDimensionsWithLocalCumulOptimizers() const { return local_optimizer_dimensions; } -DecisionBuilder* -RoutingModel::CreateFinalizerForMinimizedAndMaximizedVariables() { - std::stable_sort(weighted_finalizer_variable_targets_.begin(), - weighted_finalizer_variable_targets_.end(), - [](const std::pair& var_cost1, - const std::pair& var_cost2) { - return var_cost1.second > var_cost2.second; - }); - const int num_variables = weighted_finalizer_variable_targets_.size() + - finalizer_variable_targets_.size(); - std::vector variables; - std::vector targets; - variables.reserve(num_variables); - targets.reserve(num_variables); - for (const auto& [var_target, cost] : weighted_finalizer_variable_targets_) { - variables.push_back(var_target.var); - targets.push_back(var_target.target); - } - for (const auto& [var, target] : finalizer_variable_targets_) { - variables.push_back(var); - targets.push_back(target); - } - return MakeSetValuesFromTargets(solver(), std::move(variables), - std::move(targets)); -} - bool RoutingModel::AreRoutesInterdependent( const RoutingSearchParameters& parameters) const { // By default, GENERIC_TABU_SEARCH applies tabu search on the cost variable. @@ -5573,9 +4638,8 @@ DecisionBuilder* RoutingModel::CreateSolutionFinalizer( // optimizer. continue; } - decision_builders.push_back( - solver_->RevAlloc(new SetCumulsFromLocalDimensionCosts( - lp_optimizer.get(), mp_optimizer.get(), lns_limit))); + decision_builders.push_back(MakeSetCumulsFromLocalDimensionCosts( + solver_.get(), lp_optimizer.get(), mp_optimizer.get(), lns_limit)); } // Add a specific DB for setting cumuls of dimensions with a single resource // and no global optimizer. @@ -5591,21 +4655,18 @@ DecisionBuilder* RoutingModel::CreateSolutionFinalizer( LocalDimensionCumulOptimizer* const mp_optimizer = GetMutableLocalCumulMPOptimizer(*dim); DCHECK_NE(mp_optimizer, nullptr); - decision_builders.push_back( - solver_->RevAlloc(new SetCumulsFromResourceAssignmentCosts( - optimizer, mp_optimizer, lns_limit))); + decision_builders.push_back(MakeSetCumulsFromResourceAssignmentCosts( + solver_.get(), optimizer, mp_optimizer, lns_limit)); } } DCHECK(global_dimension_optimizers_.empty() || can_use_dimension_cumul_optimizers); for (auto& [lp_optimizer, mp_optimizer] : global_dimension_optimizers_) { - decision_builders.push_back( - solver_->RevAlloc(new SetCumulsFromGlobalDimensionCosts( - lp_optimizer.get(), mp_optimizer.get(), lns_limit))); + decision_builders.push_back(MakeSetCumulsFromGlobalDimensionCosts( + solver_.get(), lp_optimizer.get(), mp_optimizer.get(), lns_limit)); } - decision_builders.push_back( - CreateFinalizerForMinimizedAndMaximizedVariables()); + decision_builders.push_back(finalizer_variables_->CreateFinalizer()); return solver_->Compose(decision_builders); } @@ -6141,10 +5202,10 @@ void RoutingModel::SetupTrace( Solver::SearchLogParameters search_log_parameters; search_log_parameters.branch_period = 10000; search_log_parameters.objective = nullptr; - search_log_parameters.variable = cost_; - search_log_parameters.scaling_factor = - search_parameters.log_cost_scaling_factor(); - search_log_parameters.offset = search_parameters.log_cost_offset(); + search_log_parameters.variables = {cost_}; + search_log_parameters.scaling_factors = { + search_parameters.log_cost_scaling_factor()}; + search_log_parameters.offsets = {search_parameters.log_cost_offset()}; if (!search_parameters.log_tag().empty()) { const std::string& tag = search_parameters.log_tag(); search_log_parameters.display_callback = [tag]() { return tag; }; @@ -6237,48 +5298,29 @@ bool RoutingModel::UsesLightPropagation( void RoutingModel::AddWeightedVariableTargetToFinalizer(IntVar* var, int64_t target, int64_t cost) { - CHECK(var != nullptr); - const int index = - gtl::LookupOrInsert(&weighted_finalizer_variable_index_, var, - weighted_finalizer_variable_targets_.size()); - if (index < weighted_finalizer_variable_targets_.size()) { - auto& [var_target, total_cost] = - weighted_finalizer_variable_targets_[index]; - DCHECK_EQ(var_target.var, var); - DCHECK_EQ(var_target.target, target); - total_cost = CapAdd(total_cost, cost); - } else { - DCHECK_EQ(index, weighted_finalizer_variable_targets_.size()); - weighted_finalizer_variable_targets_.emplace_back(VarTarget(var, target), - cost); - } + finalizer_variables_->AddWeightedVariableTarget(var, target, cost); } void RoutingModel::AddWeightedVariableMinimizedByFinalizer(IntVar* var, int64_t cost) { - AddWeightedVariableTargetToFinalizer(var, std::numeric_limits::min(), - cost); + finalizer_variables_->AddWeightedVariableToMinimize(var, cost); } void RoutingModel::AddWeightedVariableMaximizedByFinalizer(IntVar* var, int64_t cost) { - AddWeightedVariableTargetToFinalizer(var, std::numeric_limits::max(), - cost); + finalizer_variables_->AddWeightedVariableToMaximize(var, cost); } void RoutingModel::AddVariableTargetToFinalizer(IntVar* var, int64_t target) { - CHECK(var != nullptr); - if (finalizer_variable_target_set_.contains(var)) return; - finalizer_variable_target_set_.insert(var); - finalizer_variable_targets_.emplace_back(var, target); + finalizer_variables_->AddVariableTarget(var, target); } void RoutingModel::AddVariableMaximizedByFinalizer(IntVar* var) { - AddVariableTargetToFinalizer(var, std::numeric_limits::max()); + finalizer_variables_->AddVariableToMaximize(var); } void RoutingModel::AddVariableMinimizedByFinalizer(IntVar* var) { - AddVariableTargetToFinalizer(var, std::numeric_limits::min()); + finalizer_variables_->AddVariableToMinimize(var); } void RoutingModel::SetupSearch( @@ -6295,362 +5337,6 @@ void RoutingModel::AddIntervalToAssignment(IntervalVar* const interval) { extra_intervals_.push_back(interval); } -namespace { - -class PathSpansAndTotalSlacks : public Constraint { - public: - PathSpansAndTotalSlacks(const RoutingModel* model, - const RoutingDimension* dimension, - std::vector spans, - std::vector total_slacks) - : Constraint(model->solver()), - model_(model), - dimension_(dimension), - spans_(std::move(spans)), - total_slacks_(std::move(total_slacks)) { - CHECK_EQ(spans_.size(), model_->vehicles()); - CHECK_EQ(total_slacks_.size(), model_->vehicles()); - vehicle_demons_.resize(model_->vehicles()); - } - - std::string DebugString() const override { return "PathSpansAndTotalSlacks"; } - - void Post() override { - const int num_nodes = model_->VehicleVars().size(); - const int num_transits = model_->Nexts().size(); - for (int node = 0; node < num_nodes; ++node) { - auto* demon = MakeConstraintDemon1( - model_->solver(), this, &PathSpansAndTotalSlacks::PropagateNode, - "PathSpansAndTotalSlacks::PropagateNode", node); - dimension_->CumulVar(node)->WhenRange(demon); - model_->VehicleVar(node)->WhenBound(demon); - if (node < num_transits) { - dimension_->TransitVar(node)->WhenRange(demon); - dimension_->FixedTransitVar(node)->WhenBound(demon); - model_->NextVar(node)->WhenBound(demon); - } - } - for (int vehicle = 0; vehicle < spans_.size(); ++vehicle) { - if (!spans_[vehicle] && !total_slacks_[vehicle]) continue; - auto* demon = MakeDelayedConstraintDemon1( - solver(), this, &PathSpansAndTotalSlacks::PropagateVehicle, - "PathSpansAndTotalSlacks::PropagateVehicle", vehicle); - vehicle_demons_[vehicle] = demon; - if (spans_[vehicle]) spans_[vehicle]->WhenRange(demon); - if (total_slacks_[vehicle]) total_slacks_[vehicle]->WhenRange(demon); - if (dimension_->HasBreakConstraints()) { - for (IntervalVar* b : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - b->WhenAnything(demon); - } - } - } - } - - // Call propagator on all vehicles. - void InitialPropagate() override { - for (int vehicle = 0; vehicle < spans_.size(); ++vehicle) { - if (!spans_[vehicle] && !total_slacks_[vehicle]) continue; - PropagateVehicle(vehicle); - } - } - - private: - // Called when a path/dimension variables of the node changes, - // this delays propagator calls until path variables (Next and VehicleVar) - // are instantiated, which saves fruitless and multiple identical calls. - void PropagateNode(int node) { - if (!model_->VehicleVar(node)->Bound()) return; - const int vehicle = model_->VehicleVar(node)->Min(); - if (vehicle < 0 || vehicle_demons_[vehicle] == nullptr) return; - EnqueueDelayedDemon(vehicle_demons_[vehicle]); - } - - // In order to make reasoning on span and total_slack of a vehicle uniform, - // we rely on the fact that span == sum_fixed_transits + total_slack - // to present both span and total_slack in terms of span and fixed transit. - // This allows to use the same code whether there actually are variables - // 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(); - 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(); - return std::max(span_max, CapAdd(total_slack_max, sum_fixed_transits)); - } - void SetSpanMin(int vehicle, int64_t min, int64_t sum_fixed_transits) { - DCHECK_GE(sum_fixed_transits, 0); - if (spans_[vehicle]) { - spans_[vehicle]->SetMin(min); - } - if (total_slacks_[vehicle]) { - total_slacks_[vehicle]->SetMin(CapSub(min, sum_fixed_transits)); - } - } - void SetSpanMax(int vehicle, int64_t max, int64_t sum_fixed_transits) { - DCHECK_GE(sum_fixed_transits, 0); - if (spans_[vehicle]) { - spans_[vehicle]->SetMax(max); - } - if (total_slacks_[vehicle]) { - total_slacks_[vehicle]->SetMax(CapSub(max, sum_fixed_transits)); - } - } - // Propagates span == sum_fixed_transits + total_slack. - // This should be called at least once during PropagateVehicle(). - void SynchronizeSpanAndTotalSlack(int vehicle, int64_t sum_fixed_transits) { - DCHECK_GE(sum_fixed_transits, 0); - IntVar* span = spans_[vehicle]; - IntVar* total_slack = total_slacks_[vehicle]; - if (!span || !total_slack) return; - span->SetMin(CapAdd(total_slack->Min(), sum_fixed_transits)); - span->SetMax(CapAdd(total_slack->Max(), sum_fixed_transits)); - total_slack->SetMin(CapSub(span->Min(), sum_fixed_transits)); - total_slack->SetMax(CapSub(span->Max(), sum_fixed_transits)); - } - - void PropagateVehicle(int vehicle) { - DCHECK(spans_[vehicle] || total_slacks_[vehicle]); - const int start = model_->Start(vehicle); - const int end = model_->End(vehicle); - // If transits are positive, the domain of the span variable can be reduced - // to cumul(end) - cumul(start). - if (spans_[vehicle] != nullptr && - dimension_->AreVehicleTransitsPositive(vehicle)) { - spans_[vehicle]->SetRange(CapSub(dimension_->CumulVar(end)->Min(), - dimension_->CumulVar(start)->Max()), - CapSub(dimension_->CumulVar(end)->Max(), - dimension_->CumulVar(start)->Min())); - } - // Record path, if it is not fixed from start to end, stop here. - // TRICKY: do not put end node yet, we look only at transits in the next - // reasonings, we will append the end when we look at cumuls. - { - path_.clear(); - int curr_node = start; - while (!model_->IsEnd(curr_node)) { - const IntVar* next_var = model_->NextVar(curr_node); - if (!next_var->Bound()) return; - path_.push_back(curr_node); - curr_node = next_var->Value(); - } - } - // Compute the sum of fixed transits. Fixed transit variables should all be - // fixed, otherwise we wait to get called later when propagation does it. - int64_t sum_fixed_transits = 0; - for (const int node : path_) { - const IntVar* fixed_transit_var = dimension_->FixedTransitVar(node); - if (!fixed_transit_var->Bound()) return; - sum_fixed_transits = - CapAdd(sum_fixed_transits, fixed_transit_var->Value()); - } - - SynchronizeSpanAndTotalSlack(vehicle, sum_fixed_transits); - - // The amount of break time that must occur during the route must be smaller - // than span max - sum_fixed_transits. A break must occur on the route if it - // must be after the route's start and before the route's end. - // Propagate lower bound on span, then filter out values - // that would force more breaks in route than possible. - if (dimension_->HasBreakConstraints() && - !dimension_->GetBreakIntervalsOfVehicle(vehicle).empty()) { - const int64_t vehicle_start_max = dimension_->CumulVar(start)->Max(); - const int64_t vehicle_end_min = dimension_->CumulVar(end)->Min(); - // Compute and propagate lower bound. - int64_t min_break_duration = 0; - for (IntervalVar* br : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - if (!br->MustBePerformed()) continue; - if (vehicle_start_max < br->EndMin() && - br->StartMax() < vehicle_end_min) { - min_break_duration = CapAdd(min_break_duration, br->DurationMin()); - } - } - SetSpanMin(vehicle, CapAdd(min_break_duration, sum_fixed_transits), - sum_fixed_transits); - // If a break that is not inside the route may violate slack_max, - // we can propagate in some cases: when the break must be before or - // must be after the route. - // In the other cases, we cannot deduce a better bound on a CumulVar or - // on a break, so we do nothing. - const int64_t slack_max = - CapSub(SpanMax(vehicle, sum_fixed_transits), sum_fixed_transits); - const int64_t max_additional_slack = - CapSub(slack_max, min_break_duration); - for (IntervalVar* br : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - if (!br->MustBePerformed()) continue; - // Break must be before end, detect whether it must be before start. - if (vehicle_start_max >= br->EndMin() && - br->StartMax() < vehicle_end_min) { - if (br->DurationMin() > max_additional_slack) { - // Having the break inside would violate max_additional_slack.. - // Thus, it must be outside the route, in this case, before. - br->SetEndMax(vehicle_start_max); - dimension_->CumulVar(start)->SetMin(br->EndMin()); - } - } - // Break must be after start, detect whether it must be after end. - // Same reasoning, in the case where the break is after. - if (vehicle_start_max < br->EndMin() && - br->StartMax() >= vehicle_end_min) { - if (br->DurationMin() > max_additional_slack) { - br->SetStartMin(vehicle_end_min); - dimension_->CumulVar(end)->SetMax(br->StartMax()); - } - } - } - } - - // Propagate span == cumul(end) - cumul(start). - { - IntVar* start_cumul = dimension_->CumulVar(start); - IntVar* end_cumul = dimension_->CumulVar(end); - const int64_t start_min = start_cumul->Min(); - const int64_t start_max = start_cumul->Max(); - const int64_t end_min = end_cumul->Min(); - const int64_t end_max = end_cumul->Max(); - // Propagate from cumuls to span. - const int64_t span_lb = CapSub(end_min, start_max); - SetSpanMin(vehicle, span_lb, sum_fixed_transits); - const int64_t span_ub = CapSub(end_max, start_min); - SetSpanMax(vehicle, span_ub, sum_fixed_transits); - // Propagate from span to cumuls. - const int64_t span_min = SpanMin(vehicle, sum_fixed_transits); - 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 = CapSub(span_ub, span_min); - // start >= start_max - (span_max - span_lb). - start_cumul->SetMin(CapSub(start_max, slack_from_lb)); - // end <= end_min + (span_max - span_lb). - end_cumul->SetMax(CapAdd(end_min, slack_from_lb)); - // // start <= start_min + (span_ub - span_min) - start_cumul->SetMax(CapAdd(start_min, slack_from_ub)); - // // end >= end_max - (span_ub - span_min) - end_cumul->SetMin(CapSub(end_max, slack_from_ub)); - } - - // Propagate sum transits == span. - { - // Propagate from transits to span. - int64_t span_lb = 0; - int64_t span_ub = 0; - for (const int node : path_) { - span_lb = CapAdd(span_lb, dimension_->TransitVar(node)->Min()); - span_ub = CapAdd(span_ub, dimension_->TransitVar(node)->Max()); - } - SetSpanMin(vehicle, span_lb, sum_fixed_transits); - SetSpanMax(vehicle, span_ub, sum_fixed_transits); - // Propagate from span to transits. - // transit[i] <= transit_i_min + (span_max - span_lb) - // transit[i] >= transit_i_max - (span_ub - span_min) - const int64_t span_min = SpanMin(vehicle, sum_fixed_transits); - 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(); - for (const int node : path_) { - IntVar* transit_var = dimension_->TransitVar(node); - const int64_t transit_i_min = transit_var->Min(); - const int64_t transit_i_max = transit_var->Max(); - // TRICKY: the first propagation might change transit_var->Max(), - // but we must use the same value of transit_i_max in the computation - // of transit[i]'s lower bound that was used for span_ub. - transit_var->SetMax(CapAdd(transit_i_min, slack_from_lb)); - transit_var->SetMin(CapSub(transit_i_max, slack_from_ub)); - } - } - - // TRICKY: add end node now, we will look at cumuls. - path_.push_back(end); - - // A stronger bound: from start min of the route, go to node i+1 with time - // max(cumul[i] + fixed_transit, cumul[i+1].Min()). - // Record arrival time (should be the same as end cumul min). - // Then do the reverse route, going to time - // min(cumul[i+1] - fixed_transit, cumul[i].Max()) - // Record final time as departure time. - // Then arrival time - departure time is a valid lower bound of span. - // First reasoning: start - end - start - { - int64_t arrival_time = dimension_->CumulVar(start)->Min(); - for (int i = 1; i < path_.size(); ++i) { - arrival_time = - std::max(CapAdd(arrival_time, - dimension_->FixedTransitVar(path_[i - 1])->Min()), - dimension_->CumulVar(path_[i])->Min()); - } - int64_t departure_time = arrival_time; - for (int i = path_.size() - 2; i >= 0; --i) { - departure_time = - std::min(CapSub(departure_time, - dimension_->FixedTransitVar(path_[i])->Min()), - dimension_->CumulVar(path_[i])->Max()); - } - const int64_t span_lb = CapSub(arrival_time, departure_time); - SetSpanMin(vehicle, span_lb, sum_fixed_transits); - const int64_t maximum_deviation = - CapSub(SpanMax(vehicle, sum_fixed_transits), span_lb); - const int64_t start_lb = CapSub(departure_time, maximum_deviation); - dimension_->CumulVar(start)->SetMin(start_lb); - } - // Second reasoning: end - start - end - { - int64_t departure_time = dimension_->CumulVar(end)->Max(); - for (int i = path_.size() - 2; i >= 0; --i) { - const int curr_node = path_[i]; - departure_time = - std::min(CapSub(departure_time, - dimension_->FixedTransitVar(curr_node)->Min()), - dimension_->CumulVar(curr_node)->Max()); - } - int arrival_time = departure_time; - for (int i = 1; i < path_.size(); ++i) { - arrival_time = - std::max(CapAdd(arrival_time, - dimension_->FixedTransitVar(path_[i - 1])->Min()), - dimension_->CumulVar(path_[i])->Min()); - } - const int64_t span_lb = CapSub(arrival_time, departure_time); - SetSpanMin(vehicle, span_lb, sum_fixed_transits); - const int64_t maximum_deviation = - CapSub(SpanMax(vehicle, sum_fixed_transits), span_lb); - dimension_->CumulVar(end)->SetMax( - CapAdd(arrival_time, maximum_deviation)); - } - } - - const RoutingModel* const model_; - const RoutingDimension* const dimension_; - std::vector spans_; - std::vector total_slacks_; - std::vector path_; - std::vector vehicle_demons_; -}; - -} // namespace - -Constraint* RoutingModel::MakePathSpansAndTotalSlacks( - const RoutingDimension* dimension, std::vector spans, - std::vector total_slacks) { - CHECK_EQ(vehicles_, spans.size()); - CHECK_EQ(vehicles_, total_slacks.size()); - return solver()->RevAlloc( - new PathSpansAndTotalSlacks(this, dimension, spans, total_slacks)); -} - const char RoutingModelVisitor::kLightElement[] = "LightElement"; const char RoutingModelVisitor::kLightElement2[] = "LightElement2"; const char RoutingModelVisitor::kRemoveValues[] = "RemoveValues"; @@ -6689,77 +5375,6 @@ void RoutingDimension::Initialize( slack_max); } -namespace { -// Very light version of the RangeLessOrEqual constraint (see ./range_cst.cc). -// Only performs initial propagation and then checks the compatibility of the -// variable domains without domain pruning. -// This is useful when to avoid ping-pong effects with costly constraints -// such as the PathCumul constraint. -// This constraint has not been added to the cp library (in range_cst.cc) given -// it only does checking and no propagation (except the initial propagation) -// and is only fit for local search, in particular in the context of vehicle -// routing. -class LightRangeLessOrEqual : public Constraint { - public: - LightRangeLessOrEqual(Solver* s, IntExpr* l, IntExpr* r); - ~LightRangeLessOrEqual() override {} - void Post() override; - void InitialPropagate() override; - std::string DebugString() const override; - IntVar* Var() override { - return solver()->MakeIsLessOrEqualVar(left_, right_); - } - // TODO(user): introduce a kLightLessOrEqual tag. - void Accept(ModelVisitor* const visitor) const override { - visitor->BeginVisitConstraint(ModelVisitor::kLessOrEqual, this); - visitor->VisitIntegerExpressionArgument(ModelVisitor::kLeftArgument, left_); - visitor->VisitIntegerExpressionArgument(ModelVisitor::kRightArgument, - right_); - visitor->EndVisitConstraint(ModelVisitor::kLessOrEqual, this); - } - - private: - void CheckRange(); - - IntExpr* const left_; - IntExpr* const right_; - Demon* demon_; -}; - -LightRangeLessOrEqual::LightRangeLessOrEqual(Solver* const s, IntExpr* const l, - IntExpr* const r) - : Constraint(s), left_(l), right_(r), demon_(nullptr) {} - -void LightRangeLessOrEqual::Post() { - demon_ = MakeConstraintDemon0( - solver(), this, &LightRangeLessOrEqual::CheckRange, "CheckRange"); - left_->WhenRange(demon_); - right_->WhenRange(demon_); -} - -void LightRangeLessOrEqual::InitialPropagate() { - left_->SetMax(right_->Max()); - right_->SetMin(left_->Min()); - if (left_->Max() <= right_->Min()) { - demon_->inhibit(solver()); - } -} - -void LightRangeLessOrEqual::CheckRange() { - if (left_->Min() > right_->Max()) { - solver()->Fail(); - } - if (left_->Max() <= right_->Min()) { - demon_->inhibit(solver()); - } -} - -std::string LightRangeLessOrEqual::DebugString() const { - return left_->DebugString() + " < " + right_->DebugString(); -} - -} // namespace - void RoutingDimension::InitializeCumuls() { Solver* const solver = model_->solver(); const int size = model_->Size() + model_->vehicles(); @@ -6837,7 +5452,8 @@ void RoutingDimension::InitializeTransitVariables(int64_t slack_max) { bool are_all_evaluators_positive = true; for (int class_evaluator : class_evaluators_) { - if (!model()->is_transit_evaluator_positive_[class_evaluator]) { + if (model()->transit_evaluator_sign_[class_evaluator] != + RoutingModel::kTransitEvaluatorSignPositiveOrZero) { are_all_evaluators_positive = false; break; } @@ -7563,7 +6179,8 @@ void RoutingDimension::SetBreakIntervalsOfVehicle( const int visit_evaluator = model()->RegisterTransitCallback( [node_visit_transits](int64_t from, int64_t /*to*/) { return node_visit_transits[from]; - }); + }, + RoutingModel::kTransitEvaluatorSignPositiveOrZero); SetBreakIntervalsOfVehicle(std::move(breaks), vehicle, visit_evaluator, -1); } @@ -7575,9 +6192,10 @@ void RoutingDimension::SetBreakIntervalsOfVehicle( const int visit_evaluator = model()->RegisterTransitCallback( [node_visit_transits](int64_t from, int64_t /*to*/) { return node_visit_transits[from]; - }); - const int delay_evaluator = - model()->RegisterTransitCallback(std::move(delays)); + }, + RoutingModel::kTransitEvaluatorSignPositiveOrZero); + const int delay_evaluator = model()->RegisterTransitCallback( + std::move(delays), RoutingModel::kTransitEvaluatorSignPositiveOrZero); SetBreakIntervalsOfVehicle(std::move(breaks), vehicle, visit_evaluator, delay_evaluator); } diff --git a/ortools/constraint_solver/routing.h b/ortools/constraint_solver/routing.h index 79393b6b6b0..5df25c15845 100644 --- a/ortools/constraint_solver/routing.h +++ b/ortools/constraint_solver/routing.h @@ -171,6 +171,8 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/container/inlined_vector.h" +#include "absl/hash/hash.h" +#include "absl/log/check.h" #include "absl/time/time.h" #include "ortools/base/int_type.h" #include "ortools/base/integral_types.h" @@ -185,6 +187,7 @@ #include "ortools/constraint_solver/routing_types.h" #include "ortools/graph/graph.h" #include "ortools/sat/theta_tree.h" +#include "ortools/util/bitset.h" #include "ortools/util/piecewise_linear_function.h" #include "ortools/util/range_query_function.h" #include "ortools/util/saturated_arithmetic.h" @@ -192,6 +195,7 @@ namespace operations_research { +class FinalizerVariables; class GlobalDimensionCumulOptimizer; class LocalDimensionCumulOptimizer; class LocalSearchPhaseParameters; @@ -533,15 +537,27 @@ class RoutingModel { const RoutingModelParameters& parameters); ~RoutingModel(); + /// Represents the sign of values returned by a transit evaluator. + enum TransitEvaluatorSign { + kTransitEvaluatorSignUnknown = 0, + kTransitEvaluatorSignPositiveOrZero = 1, + kTransitEvaluatorSignNegativeOrZero = 2, + }; /// Registers 'callback' and returns its index. + /// The sign parameter allows to notify the solver that the callback only + /// return values of the given sign. This can help the solver, but passing + /// an incorrect sign may crash in non-opt compilation mode, and yield + /// incorrect results in opt. int RegisterUnaryTransitVector(std::vector values); - int RegisterUnaryTransitCallback(TransitCallback1 callback); - int RegisterPositiveUnaryTransitCallback(TransitCallback1 callback); + int RegisterUnaryTransitCallback( + TransitCallback1 callback, + TransitEvaluatorSign sign = kTransitEvaluatorSignUnknown); int RegisterTransitMatrix( std::vector /*needed_for_swig*/> values); - int RegisterTransitCallback(TransitCallback2 callback); - int RegisterPositiveTransitCallback(TransitCallback2 callback); + int RegisterTransitCallback( + TransitCallback2 callback, + TransitEvaluatorSign sign = kTransitEvaluatorSignUnknown); int RegisterStateDependentTransitCallback(VariableIndexEvaluator2 callback); const TransitCallback2& TransitCallback(int callback_index) const { @@ -673,19 +689,6 @@ class RoutingModel { const std::function& f, int64_t domain_start, int64_t domain_end); - /// For every vehicle of the routing model: - /// - if total_slacks[vehicle] is not nullptr, constrains it to be the sum of - /// slacks on that vehicle, that is, - /// dimension->CumulVar(end) - dimension->CumulVar(start) - - /// sum_{node in path of vehicle} dimension->FixedTransitVar(node). - /// - if spans[vehicle] is not nullptr, constrains it to be - /// dimension->CumulVar(end) - dimension->CumulVar(start) - /// This does stronger propagation than a decomposition, and takes breaks into - /// account. - Constraint* MakePathSpansAndTotalSlacks(const RoutingDimension* dimension, - std::vector spans, - std::vector total_slacks); - /// Outputs the names of all dimensions added to the routing engine. // TODO(user): rename. std::vector GetAllDimensionNames() const; @@ -1153,7 +1156,11 @@ class RoutingModel { /// Adds a callback called each time a solution is found during the search. /// This is a shortcut to creating a monitor to call the callback on /// AtSolution() and adding it with AddSearchMonitor. - void AddAtSolutionCallback(std::function callback); + /// If track_unchecked_neighbors is true, the callback will also be called on + /// AcceptUncheckedNeighbor() events, which is useful to grab solutions + /// obtained when solver_parameters.check_solution_period > 1 (aka fastLS). + void AddAtSolutionCallback(std::function callback, + bool track_unchecked_neighbors = false); /// Adds a variable to minimize in the solution finalizer. The solution /// finalizer is called each time a solution is found during the search and /// allows to instantiate secondary variables (such as dimension cumul @@ -1973,6 +1980,7 @@ class RoutingModel { Assignment* GetOrCreateAssignment(); Assignment* GetOrCreateTmpAssignment(); RegularLimit* GetOrCreateLimit(); + RegularLimit* GetOrCreateCumulativeLimit(); RegularLimit* GetOrCreateLocalSearchLimit(); RegularLimit* GetOrCreateLargeNeighborhoodSearchLimit(); RegularLimit* GetOrCreateFirstSolutionLargeNeighborhoodSearchLimit(); @@ -2101,7 +2109,6 @@ class RoutingModel { const RoutingSearchParameters& parameters, const FilterOptions& options); DecisionBuilder* CreateSolutionFinalizer( const RoutingSearchParameters& parameters, SearchLimit* lns_limit); - DecisionBuilder* CreateFinalizerForMinimizedAndMaximizedVariables(); void CreateFirstSolutionDecisionBuilders( const RoutingSearchParameters& search_parameters); DecisionBuilder* GetFirstSolutionDecisionBuilder( @@ -2370,22 +2377,13 @@ class RoutingModel { absl::flat_hash_map> node_neighbors_by_cost_class_per_size_; + std::unique_ptr finalizer_variables_; #ifndef SWIG - struct VarTarget { - VarTarget(IntVar* v, int64_t t) : var(v), target(t) {} - - IntVar* var; - int64_t target; - }; - std::vector> - weighted_finalizer_variable_targets_; - std::vector finalizer_variable_targets_; - absl::flat_hash_map weighted_finalizer_variable_index_; - absl::flat_hash_set finalizer_variable_target_set_; std::unique_ptr sweep_arranger_; #endif RegularLimit* limit_ = nullptr; + RegularLimit* cumulative_limit_ = nullptr; RegularLimit* ls_limit_ = nullptr; RegularLimit* lns_limit_ = nullptr; RegularLimit* first_solution_lns_limit_ = nullptr; @@ -2396,19 +2394,18 @@ class RoutingModel { typedef absl::flat_hash_map StateDependentTransitCallbackCache; + // All transit callbacks are stored in transit_evaluators_, + // we refer to callbacks by the index in this vector. + // We maintain unary_transit_evaluators_[] (with the same size) to store + // callbacks that are unary: + // - if a callback is unary, it is in unary_transit_evaluators_[i], + // and a binary version is stored at transit_evaluators_[i]. + // - if a callback is binary, it is stored at transit_evaluators_[i], + // and unary_transit_evaluators_[i] is nullptr. std::vector unary_transit_evaluators_; std::vector transit_evaluators_; - // The following vector stores a boolean per transit_evaluator_, indicating - // whether the transits are all positive. - // is_transit_evaluator_positive_ will be set to true only when registering a - // callback via RegisterPositiveTransitCallback(), and to false otherwise. - // The actual positivity of the transit values will only be checked in debug - // mode, when calling RegisterPositiveTransitCallback(). - // Therefore, RegisterPositiveTransitCallback() should only be called when the - // transits are known to be positive, as the positivity of a callback will - // allow some improvements in the solver, but will entail in errors if the - // transits are falsely assumed positive. - std::vector is_transit_evaluator_positive_; + std::vector transit_evaluator_sign_; + std::vector state_dependent_transit_evaluators_; std::vector> state_dependent_transit_evaluators_cache_; @@ -2962,8 +2959,14 @@ class RoutingDimension { /// Returns true iff the transit evaluator of 'vehicle' is positive for all /// arcs. bool AreVehicleTransitsPositive(int vehicle) const { - return model()->is_transit_evaluator_positive_ - [class_evaluators_[vehicle_to_class_[vehicle]]]; + const int evaluator_index = class_evaluators_[vehicle_to_class_[vehicle]]; + return model()->transit_evaluator_sign_[evaluator_index] == + RoutingModel::kTransitEvaluatorSignPositiveOrZero; + } + RoutingModel::TransitEvaluatorSign GetTransitEvaluatorSign( + int vehicle) const { + const int evaluator_index = class_evaluators_[vehicle_to_class_[vehicle]]; + return model()->transit_evaluator_sign_[evaluator_index]; } int vehicle_to_class(int vehicle) const { return vehicle_to_class_[vehicle]; } #endif /// !defined(SWIGCSHARP) && !defined(SWIGJAVA) @@ -3366,12 +3369,6 @@ class RoutingDimension { DISALLOW_COPY_AND_ASSIGN(RoutingDimension); }; -/// A decision builder which tries to assign values to variables as close as -/// possible to target values first. -DecisionBuilder* MakeSetValuesFromTargets(Solver* solver, - std::vector variables, - std::vector targets); - /// Attempts to solve the model using the cp-sat solver. As of 5/2019, will /// solve the TSP corresponding to the model if it has a single vehicle. /// Therefore the resulting solution might not actually be feasible. Will return @@ -3384,19 +3381,6 @@ bool SolveModelWithSat(const RoutingModel& model, #if !defined(SWIG) IntVarLocalSearchFilter* MakeVehicleBreaksFilter( const RoutingModel& routing_model, const RoutingDimension& dimension); - -// A decision builder that monitors solutions, and tries to fix dimension -// variables whose route did not change in the candidate solution. -// Dimension variables are Cumul, Slack and break variables of all dimensions. -// The user must make sure that those variables will be always be fixed at -// solution, typically by composing another DecisionBuilder after this one. -// If this DecisionBuilder returns a non-nullptr value at some node of the -// search tree, it will always return nullptr in the subtree of that node. -// Moreover, the decision will be a simultaneous assignment of the dimension -// variables of unchanged routes on the left branch, and an empty decision on -// the right branch. -DecisionBuilder* MakeRestoreDimensionValuesForUnchangedRoutes( - RoutingModel* model); #endif } // namespace operations_research diff --git a/ortools/constraint_solver/routing_constraints.cc b/ortools/constraint_solver/routing_constraints.cc new file mode 100644 index 00000000000..a804421bb47 --- /dev/null +++ b/ortools/constraint_solver/routing_constraints.cc @@ -0,0 +1,742 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/constraint_solver/routing_constraints.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/constraint_solver/constraint_solveri.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_lp_scheduling.h" +#include "ortools/constraint_solver/routing_search.h" +#include "ortools/util/saturated_arithmetic.h" + +namespace operations_research { + +namespace { +// Constraint which ensures that var != values. +class DifferentFromValues : public Constraint { + public: + DifferentFromValues(Solver* solver, IntVar* var, std::vector values) + : Constraint(solver), var_(var), values_(std::move(values)) {} + void Post() override {} + void InitialPropagate() override { var_->RemoveValues(values_); } + std::string DebugString() const override { return "DifferentFromValues"; } + void Accept(ModelVisitor* const visitor) const override { + visitor->BeginVisitConstraint(RoutingModelVisitor::kRemoveValues, this); + visitor->VisitIntegerVariableArrayArgument(ModelVisitor::kVarsArgument, + {var_}); + visitor->VisitIntegerArrayArgument(ModelVisitor::kValuesArgument, values_); + visitor->EndVisitConstraint(RoutingModelVisitor::kRemoveValues, this); + } + + private: + IntVar* const var_; + const std::vector values_; +}; +} // namespace + +Constraint* MakeDifferentFromValues(Solver* solver, IntVar* var, + std::vector values) { + return solver->RevAlloc( + new DifferentFromValues(solver, var, std::move(values))); +} + +namespace { +// For each vehicle, computes information on the partially fixed start/end +// chains (based on bound NextVar values): +// - For every 'end_node', the last node of a start chain of a vehicle, +// vehicle_index_of_start_chain_end[end_node] contains the corresponding +// vehicle index. Contains -1 for other nodes. +// - For every vehicle 'v', end_chain_starts[v] contains the first node of the +// end chain of that vehicle. +void ComputeVehicleChainStartEndInfo( + const RoutingModel& model, std::vector* end_chain_starts, + std::vector* vehicle_index_of_start_chain_end) { + vehicle_index_of_start_chain_end->resize(model.Size() + model.vehicles(), -1); + + for (int vehicle = 0; vehicle < model.vehicles(); ++vehicle) { + int64_t node = model.Start(vehicle); + while (!model.IsEnd(node) && model.NextVar(node)->Bound()) { + node = model.NextVar(node)->Value(); + } + vehicle_index_of_start_chain_end->at(node) = vehicle; + } + + *end_chain_starts = ComputeVehicleEndChainStarts(model); +} + +class ResourceAssignmentConstraint : public Constraint { + public: + ResourceAssignmentConstraint( + const RoutingModel::ResourceGroup* resource_group, + const std::vector* vehicle_resource_vars, RoutingModel* model) + : Constraint(model->solver()), + model_(*model), + resource_group_(*resource_group), + vehicle_resource_vars_(*vehicle_resource_vars), + vehicle_to_start_bound_vars_per_dimension_(model->vehicles()), + vehicle_to_end_bound_vars_per_dimension_(model->vehicles()) { + DCHECK_EQ(vehicle_resource_vars_.size(), model_.vehicles()); + + const std::vector& dimensions = model_.GetDimensions(); + for (int v = 0; v < model_.vehicles(); v++) { + IntVar* const resource_var = vehicle_resource_vars_[v]; + model->AddToAssignment(resource_var); + // The resource variable must be fixed by the search. + model->AddVariableTargetToFinalizer(resource_var, -1); + + if (!resource_group_.VehicleRequiresAResource(v)) { + continue; + } + + vehicle_to_start_bound_vars_per_dimension_[v].resize(dimensions.size()); + vehicle_to_end_bound_vars_per_dimension_[v].resize(dimensions.size()); + + for (const RoutingModel::DimensionIndex d : + resource_group_.GetAffectedDimensionIndices()) { + const RoutingDimension* const dim = dimensions[d.value()]; + // The vehicle's start/end cumuls must be fixed by the search. + model->AddVariableMinimizedByFinalizer(dim->CumulVar(model_.End(v))); + model->AddVariableMaximizedByFinalizer(dim->CumulVar(model_.Start(v))); + for (ResourceBoundVars* bound_vars : + {&vehicle_to_start_bound_vars_per_dimension_[v][d.value()], + &vehicle_to_end_bound_vars_per_dimension_[v][d.value()]}) { + bound_vars->lower_bound = + solver()->MakeIntVar(std::numeric_limits::min(), + std::numeric_limits::max()); + bound_vars->upper_bound = + solver()->MakeIntVar(std::numeric_limits::min(), + std::numeric_limits::max()); + } + } + } + } + + void Post() override {} + + void InitialPropagate() override { + if (!AllResourceAssignmentsFeasible()) { + solver()->Fail(); + } + SetupResourceConstraints(); + } + + private: + bool AllResourceAssignmentsFeasible() { + DCHECK(!model_.GetResourceGroups().empty()); + + std::vector end_chain_starts; + std::vector vehicle_index_of_start_chain_end; + ComputeVehicleChainStartEndInfo(model_, &end_chain_starts, + &vehicle_index_of_start_chain_end); + const auto next = [&model = model_, &end_chain_starts, + &vehicle_index_of_start_chain_end](int64_t node) { + if (model.NextVar(node)->Bound()) return model.NextVar(node)->Value(); + const int vehicle = vehicle_index_of_start_chain_end[node]; + if (vehicle < 0) { + // The node isn't the last node of a route start chain and is considered + // as unperformed and ignored when evaluating the feasibility of the + // resource assignment. + return node; + } + return end_chain_starts[vehicle]; + }; + + const std::vector& dimensions = model_.GetDimensions(); + for (RoutingModel::DimensionIndex d : + resource_group_.GetAffectedDimensionIndices()) { + if (!ResourceAssignmentFeasibleForDimension(*dimensions[d.value()], + next)) { + return false; + } + } + return true; + } + + bool ResourceAssignmentFeasibleForDimension( + const RoutingDimension& dimension, + const std::function& next) { + LocalDimensionCumulOptimizer* const optimizer = + model_.GetMutableLocalCumulLPOptimizer(dimension); + + if (optimizer == nullptr) return true; + + LocalDimensionCumulOptimizer* const mp_optimizer = + model_.GetMutableLocalCumulMPOptimizer(dimension); + DCHECK_NE(mp_optimizer, nullptr); + const auto transit = [&dimension](int64_t node, int64_t /*next*/) { + // TODO(user): Get rid of this max() by only allowing resources on + // dimensions with positive transits (model.AreVehicleTransitsPositive()). + // TODO(user): The transit lower bounds have not necessarily been + // propagated at this point. Add demons to check the resource assignment + // feasibility after the transit ranges have been propagated. + return std::max(dimension.FixedTransitVar(node)->Min(), 0); + }; + + std::vector> assignment_costs(model_.vehicles()); + for (int v : resource_group_.GetVehiclesRequiringAResource()) { + if (!ComputeVehicleToResourcesAssignmentCosts( + v, resource_group_, next, transit, + /*optimize_vehicle_costs*/ false, + model_.GetMutableLocalCumulLPOptimizer(dimension), + model_.GetMutableLocalCumulMPOptimizer(dimension), + &assignment_costs[v], nullptr, nullptr)) { + return false; + } + } + // TODO(user): Replace this call with a more efficient max-flow, instead + // of running the full min-cost flow. + return ComputeBestVehicleToResourceAssignment( + resource_group_.GetVehiclesRequiringAResource(), + resource_group_.Size(), + [&assignment_costs](int v) { return &assignment_costs[v]; }, + nullptr) >= 0; + } + + void SetupResourceConstraints() { + Solver* const s = solver(); + // Resources cannot be shared, so assigned resources must all be different + // (note that resource_var == -1 means no resource assigned). + s->AddConstraint(s->MakeAllDifferentExcept(vehicle_resource_vars_, -1)); + const std::vector& dimensions = model_.GetDimensions(); + for (int v = 0; v < model_.vehicles(); v++) { + IntVar* const resource_var = vehicle_resource_vars_[v]; + if (!resource_group_.VehicleRequiresAResource(v)) { + resource_var->SetValue(-1); + continue; + } + // vehicle_route_considered_[v] <--> vehicle_res_vars[v] != -1. + s->AddConstraint( + s->MakeEquality(model_.VehicleRouteConsideredVar(v), + s->MakeIsDifferentCstVar(resource_var, -1))); + + // Add dimension cumul constraints. + for (const RoutingModel::DimensionIndex dim_index : + resource_group_.GetAffectedDimensionIndices()) { + const int d = dim_index.value(); + const RoutingDimension* const dim = dimensions[d]; + + // resource_start_lb_var <= cumul[start(v)] <= resource_start_ub_var, + // resource_end_lb_var <= cumul[end(v)] <= resource_end_ub_var + for (bool start_cumul : {true, false}) { + IntVar* const cumul_var = start_cumul ? dim->CumulVar(model_.Start(v)) + : dim->CumulVar(model_.End(v)); + + IntVar* const resource_lb_var = + start_cumul + ? vehicle_to_start_bound_vars_per_dimension_[v][d].lower_bound + : vehicle_to_end_bound_vars_per_dimension_[v][d].lower_bound; + s->AddConstraint(s->MakeLightElement( + [dim, start_cumul, &resource_group = resource_group_](int r) { + if (r < 0) return std::numeric_limits::min(); + return start_cumul ? resource_group.GetResources()[r] + .GetDimensionAttributes(dim) + .start_domain() + .Min() + : resource_group.GetResources()[r] + .GetDimensionAttributes(dim) + .end_domain() + .Min(); + }, + resource_lb_var, resource_var, + [&model = model_]() { + return model.enable_deep_serialization(); + })); + s->AddConstraint(s->MakeGreaterOrEqual(cumul_var, resource_lb_var)); + + IntVar* const resource_ub_var = + start_cumul + ? vehicle_to_start_bound_vars_per_dimension_[v][d].upper_bound + : vehicle_to_end_bound_vars_per_dimension_[v][d].upper_bound; + s->AddConstraint(s->MakeLightElement( + [dim, start_cumul, &resource_group = resource_group_](int r) { + if (r < 0) return std::numeric_limits::max(); + return start_cumul ? resource_group.GetResources()[r] + .GetDimensionAttributes(dim) + .start_domain() + .Max() + : resource_group.GetResources()[r] + .GetDimensionAttributes(dim) + .end_domain() + .Max(); + }, + resource_ub_var, resource_var, + [&model = model_]() { + return model.enable_deep_serialization(); + })); + s->AddConstraint(s->MakeLessOrEqual(cumul_var, resource_ub_var)); + } + } + } + } + + struct ResourceBoundVars { + IntVar* lower_bound; + IntVar* upper_bound; + }; + + const RoutingModel& model_; + const RoutingModel::ResourceGroup& resource_group_; + const std::vector& vehicle_resource_vars_; + // The following vectors store the IntVars keeping track of the lower and + // upper bound on the cumul start/end of every vehicle (requiring a resource) + // based on its assigned resource (determined by vehicle_resource_vars_). + std::vector> + vehicle_to_start_bound_vars_per_dimension_; + std::vector> + vehicle_to_end_bound_vars_per_dimension_; +}; +} // namespace + +Constraint* MakeResourceConstraint( + const RoutingModel::ResourceGroup* resource_group, + const std::vector* vehicle_resource_vars, RoutingModel* model) { + return model->solver()->RevAlloc(new ResourceAssignmentConstraint( + resource_group, vehicle_resource_vars, model)); +} + +namespace { +class PathSpansAndTotalSlacks : public Constraint { + public: + PathSpansAndTotalSlacks(const RoutingModel* model, + const RoutingDimension* dimension, + std::vector spans, + std::vector total_slacks) + : Constraint(model->solver()), + model_(model), + dimension_(dimension), + spans_(std::move(spans)), + total_slacks_(std::move(total_slacks)) { + CHECK_EQ(spans_.size(), model_->vehicles()); + CHECK_EQ(total_slacks_.size(), model_->vehicles()); + vehicle_demons_.resize(model_->vehicles()); + } + + std::string DebugString() const override { return "PathSpansAndTotalSlacks"; } + + void Post() override { + const int num_nodes = model_->VehicleVars().size(); + const int num_transits = model_->Nexts().size(); + for (int node = 0; node < num_nodes; ++node) { + auto* demon = MakeConstraintDemon1( + model_->solver(), this, &PathSpansAndTotalSlacks::PropagateNode, + "PathSpansAndTotalSlacks::PropagateNode", node); + dimension_->CumulVar(node)->WhenRange(demon); + model_->VehicleVar(node)->WhenBound(demon); + if (node < num_transits) { + dimension_->TransitVar(node)->WhenRange(demon); + dimension_->FixedTransitVar(node)->WhenBound(demon); + model_->NextVar(node)->WhenBound(demon); + } + } + for (int vehicle = 0; vehicle < spans_.size(); ++vehicle) { + if (!spans_[vehicle] && !total_slacks_[vehicle]) continue; + auto* demon = MakeDelayedConstraintDemon1( + solver(), this, &PathSpansAndTotalSlacks::PropagateVehicle, + "PathSpansAndTotalSlacks::PropagateVehicle", vehicle); + vehicle_demons_[vehicle] = demon; + if (spans_[vehicle]) spans_[vehicle]->WhenRange(demon); + if (total_slacks_[vehicle]) total_slacks_[vehicle]->WhenRange(demon); + if (dimension_->HasBreakConstraints()) { + for (IntervalVar* b : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { + b->WhenAnything(demon); + } + } + } + } + + // Call propagator on all vehicles. + void InitialPropagate() override { + for (int vehicle = 0; vehicle < spans_.size(); ++vehicle) { + if (!spans_[vehicle] && !total_slacks_[vehicle]) continue; + PropagateVehicle(vehicle); + } + } + + private: + // Called when a path/dimension variables of the node changes, + // this delays propagator calls until path variables (Next and VehicleVar) + // are instantiated, which saves fruitless and multiple identical calls. + void PropagateNode(int node) { + if (!model_->VehicleVar(node)->Bound()) return; + const int vehicle = model_->VehicleVar(node)->Min(); + if (vehicle < 0 || vehicle_demons_[vehicle] == nullptr) return; + EnqueueDelayedDemon(vehicle_demons_[vehicle]); + } + + // In order to make reasoning on span and total_slack of a vehicle uniform, + // we rely on the fact that span == sum_fixed_transits + total_slack + // to present both span and total_slack in terms of span and fixed transit. + // This allows to use the same code whether there actually are variables + // 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(); + 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(); + return std::max(span_max, CapAdd(total_slack_max, sum_fixed_transits)); + } + void SetSpanMin(int vehicle, int64_t min, int64_t sum_fixed_transits) { + DCHECK_GE(sum_fixed_transits, 0); + if (spans_[vehicle]) { + spans_[vehicle]->SetMin(min); + } + if (total_slacks_[vehicle]) { + total_slacks_[vehicle]->SetMin(CapSub(min, sum_fixed_transits)); + } + } + void SetSpanMax(int vehicle, int64_t max, int64_t sum_fixed_transits) { + DCHECK_GE(sum_fixed_transits, 0); + if (spans_[vehicle]) { + spans_[vehicle]->SetMax(max); + } + if (total_slacks_[vehicle]) { + total_slacks_[vehicle]->SetMax(CapSub(max, sum_fixed_transits)); + } + } + // Propagates span == sum_fixed_transits + total_slack. + // This should be called at least once during PropagateVehicle(). + void SynchronizeSpanAndTotalSlack(int vehicle, int64_t sum_fixed_transits) { + DCHECK_GE(sum_fixed_transits, 0); + IntVar* span = spans_[vehicle]; + IntVar* total_slack = total_slacks_[vehicle]; + if (!span || !total_slack) return; + span->SetMin(CapAdd(total_slack->Min(), sum_fixed_transits)); + span->SetMax(CapAdd(total_slack->Max(), sum_fixed_transits)); + total_slack->SetMin(CapSub(span->Min(), sum_fixed_transits)); + total_slack->SetMax(CapSub(span->Max(), sum_fixed_transits)); + } + + void PropagateVehicle(int vehicle) { + DCHECK(spans_[vehicle] || total_slacks_[vehicle]); + const int start = model_->Start(vehicle); + const int end = model_->End(vehicle); + // If transits are positive, the domain of the span variable can be reduced + // to cumul(end) - cumul(start). + if (spans_[vehicle] != nullptr && + dimension_->AreVehicleTransitsPositive(vehicle)) { + spans_[vehicle]->SetRange(CapSub(dimension_->CumulVar(end)->Min(), + dimension_->CumulVar(start)->Max()), + CapSub(dimension_->CumulVar(end)->Max(), + dimension_->CumulVar(start)->Min())); + } + // Record path, if it is not fixed from start to end, stop here. + // TRICKY: do not put end node yet, we look only at transits in the next + // reasonings, we will append the end when we look at cumuls. + { + path_.clear(); + int curr_node = start; + while (!model_->IsEnd(curr_node)) { + const IntVar* next_var = model_->NextVar(curr_node); + if (!next_var->Bound()) return; + path_.push_back(curr_node); + curr_node = next_var->Value(); + } + } + // Compute the sum of fixed transits. Fixed transit variables should all be + // fixed, otherwise we wait to get called later when propagation does it. + int64_t sum_fixed_transits = 0; + for (const int node : path_) { + const IntVar* fixed_transit_var = dimension_->FixedTransitVar(node); + if (!fixed_transit_var->Bound()) return; + sum_fixed_transits = + CapAdd(sum_fixed_transits, fixed_transit_var->Value()); + } + + SynchronizeSpanAndTotalSlack(vehicle, sum_fixed_transits); + + // The amount of break time that must occur during the route must be smaller + // than span max - sum_fixed_transits. A break must occur on the route if it + // must be after the route's start and before the route's end. + // Propagate lower bound on span, then filter out values + // that would force more breaks in route than possible. + if (dimension_->HasBreakConstraints() && + !dimension_->GetBreakIntervalsOfVehicle(vehicle).empty()) { + const int64_t vehicle_start_max = dimension_->CumulVar(start)->Max(); + const int64_t vehicle_end_min = dimension_->CumulVar(end)->Min(); + // Compute and propagate lower bound. + int64_t min_break_duration = 0; + for (IntervalVar* br : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { + if (!br->MustBePerformed()) continue; + if (vehicle_start_max < br->EndMin() && + br->StartMax() < vehicle_end_min) { + min_break_duration = CapAdd(min_break_duration, br->DurationMin()); + } + } + SetSpanMin(vehicle, CapAdd(min_break_duration, sum_fixed_transits), + sum_fixed_transits); + // If a break that is not inside the route may violate slack_max, + // we can propagate in some cases: when the break must be before or + // must be after the route. + // In the other cases, we cannot deduce a better bound on a CumulVar or + // on a break, so we do nothing. + const int64_t slack_max = + CapSub(SpanMax(vehicle, sum_fixed_transits), sum_fixed_transits); + const int64_t max_additional_slack = + CapSub(slack_max, min_break_duration); + for (IntervalVar* br : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { + if (!br->MustBePerformed()) continue; + // Break must be before end, detect whether it must be before start. + if (vehicle_start_max >= br->EndMin() && + br->StartMax() < vehicle_end_min) { + if (br->DurationMin() > max_additional_slack) { + // Having the break inside would violate max_additional_slack.. + // Thus, it must be outside the route, in this case, before. + br->SetEndMax(vehicle_start_max); + dimension_->CumulVar(start)->SetMin(br->EndMin()); + } + } + // Break must be after start, detect whether it must be after end. + // Same reasoning, in the case where the break is after. + if (vehicle_start_max < br->EndMin() && + br->StartMax() >= vehicle_end_min) { + if (br->DurationMin() > max_additional_slack) { + br->SetStartMin(vehicle_end_min); + dimension_->CumulVar(end)->SetMax(br->StartMax()); + } + } + } + } + + // Propagate span == cumul(end) - cumul(start). + { + IntVar* start_cumul = dimension_->CumulVar(start); + IntVar* end_cumul = dimension_->CumulVar(end); + const int64_t start_min = start_cumul->Min(); + const int64_t start_max = start_cumul->Max(); + const int64_t end_min = end_cumul->Min(); + const int64_t end_max = end_cumul->Max(); + // Propagate from cumuls to span. + const int64_t span_lb = CapSub(end_min, start_max); + SetSpanMin(vehicle, span_lb, sum_fixed_transits); + const int64_t span_ub = CapSub(end_max, start_min); + SetSpanMax(vehicle, span_ub, sum_fixed_transits); + // Propagate from span to cumuls. + const int64_t span_min = SpanMin(vehicle, sum_fixed_transits); + 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 = CapSub(span_ub, span_min); + // start >= start_max - (span_max - span_lb). + start_cumul->SetMin(CapSub(start_max, slack_from_lb)); + // end <= end_min + (span_max - span_lb). + end_cumul->SetMax(CapAdd(end_min, slack_from_lb)); + // // start <= start_min + (span_ub - span_min) + start_cumul->SetMax(CapAdd(start_min, slack_from_ub)); + // // end >= end_max - (span_ub - span_min) + end_cumul->SetMin(CapSub(end_max, slack_from_ub)); + } + + // Propagate sum transits == span. + { + // Propagate from transits to span. + int64_t span_lb = 0; + int64_t span_ub = 0; + for (const int node : path_) { + span_lb = CapAdd(span_lb, dimension_->TransitVar(node)->Min()); + span_ub = CapAdd(span_ub, dimension_->TransitVar(node)->Max()); + } + SetSpanMin(vehicle, span_lb, sum_fixed_transits); + SetSpanMax(vehicle, span_ub, sum_fixed_transits); + // Propagate from span to transits. + // transit[i] <= transit_i_min + (span_max - span_lb) + // transit[i] >= transit_i_max - (span_ub - span_min) + const int64_t span_min = SpanMin(vehicle, sum_fixed_transits); + 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(); + for (const int node : path_) { + IntVar* transit_var = dimension_->TransitVar(node); + const int64_t transit_i_min = transit_var->Min(); + const int64_t transit_i_max = transit_var->Max(); + // TRICKY: the first propagation might change transit_var->Max(), + // but we must use the same value of transit_i_max in the computation + // of transit[i]'s lower bound that was used for span_ub. + transit_var->SetMax(CapAdd(transit_i_min, slack_from_lb)); + transit_var->SetMin(CapSub(transit_i_max, slack_from_ub)); + } + } + + // TRICKY: add end node now, we will look at cumuls. + path_.push_back(end); + + // A stronger bound: from start min of the route, go to node i+1 with time + // max(cumul[i] + fixed_transit, cumul[i+1].Min()). + // Record arrival time (should be the same as end cumul min). + // Then do the reverse route, going to time + // min(cumul[i+1] - fixed_transit, cumul[i].Max()) + // Record final time as departure time. + // Then arrival time - departure time is a valid lower bound of span. + // First reasoning: start - end - start + { + int64_t arrival_time = dimension_->CumulVar(start)->Min(); + for (int i = 1; i < path_.size(); ++i) { + arrival_time = + std::max(CapAdd(arrival_time, + dimension_->FixedTransitVar(path_[i - 1])->Min()), + dimension_->CumulVar(path_[i])->Min()); + } + int64_t departure_time = arrival_time; + for (int i = path_.size() - 2; i >= 0; --i) { + departure_time = + std::min(CapSub(departure_time, + dimension_->FixedTransitVar(path_[i])->Min()), + dimension_->CumulVar(path_[i])->Max()); + } + const int64_t span_lb = CapSub(arrival_time, departure_time); + SetSpanMin(vehicle, span_lb, sum_fixed_transits); + const int64_t maximum_deviation = + CapSub(SpanMax(vehicle, sum_fixed_transits), span_lb); + const int64_t start_lb = CapSub(departure_time, maximum_deviation); + dimension_->CumulVar(start)->SetMin(start_lb); + } + // Second reasoning: end - start - end + { + int64_t departure_time = dimension_->CumulVar(end)->Max(); + for (int i = path_.size() - 2; i >= 0; --i) { + const int curr_node = path_[i]; + departure_time = + std::min(CapSub(departure_time, + dimension_->FixedTransitVar(curr_node)->Min()), + dimension_->CumulVar(curr_node)->Max()); + } + int arrival_time = departure_time; + for (int i = 1; i < path_.size(); ++i) { + arrival_time = + std::max(CapAdd(arrival_time, + dimension_->FixedTransitVar(path_[i - 1])->Min()), + dimension_->CumulVar(path_[i])->Min()); + } + const int64_t span_lb = CapSub(arrival_time, departure_time); + SetSpanMin(vehicle, span_lb, sum_fixed_transits); + const int64_t maximum_deviation = + CapSub(SpanMax(vehicle, sum_fixed_transits), span_lb); + dimension_->CumulVar(end)->SetMax( + CapAdd(arrival_time, maximum_deviation)); + } + } + + const RoutingModel* const model_; + const RoutingDimension* const dimension_; + std::vector spans_; + std::vector total_slacks_; + std::vector path_; + std::vector vehicle_demons_; +}; +} // namespace + +Constraint* MakePathSpansAndTotalSlacks(const RoutingDimension* dimension, + std::vector spans, + std::vector total_slacks) { + RoutingModel* const model = dimension->model(); + CHECK_EQ(model->vehicles(), spans.size()); + CHECK_EQ(model->vehicles(), total_slacks.size()); + return model->solver()->RevAlloc( + new PathSpansAndTotalSlacks(model, dimension, spans, total_slacks)); +} + +namespace { +// Very light version of the RangeLessOrEqual constraint (see ./range_cst.cc). +// Only performs initial propagation and then checks the compatibility of the +// variable domains without domain pruning. +// This is useful when to avoid ping-pong effects with costly constraints +// such as the PathCumul constraint. +// This constraint has not been added to the cp library (in range_cst.cc) given +// it only does checking and no propagation (except the initial propagation) +// and is only fit for local search, in particular in the context of vehicle +// routing. +class LightRangeLessOrEqual : public Constraint { + public: + LightRangeLessOrEqual(Solver* s, IntExpr* l, IntExpr* r); + ~LightRangeLessOrEqual() override {} + void Post() override; + void InitialPropagate() override; + std::string DebugString() const override; + IntVar* Var() override { + return solver()->MakeIsLessOrEqualVar(left_, right_); + } + // TODO(user): introduce a kLightLessOrEqual tag. + void Accept(ModelVisitor* const visitor) const override { + visitor->BeginVisitConstraint(ModelVisitor::kLessOrEqual, this); + visitor->VisitIntegerExpressionArgument(ModelVisitor::kLeftArgument, left_); + visitor->VisitIntegerExpressionArgument(ModelVisitor::kRightArgument, + right_); + visitor->EndVisitConstraint(ModelVisitor::kLessOrEqual, this); + } + + private: + void CheckRange(); + + IntExpr* const left_; + IntExpr* const right_; + Demon* demon_; +}; + +LightRangeLessOrEqual::LightRangeLessOrEqual(Solver* const s, IntExpr* const l, + IntExpr* const r) + : Constraint(s), left_(l), right_(r), demon_(nullptr) {} + +void LightRangeLessOrEqual::Post() { + demon_ = MakeConstraintDemon0( + solver(), this, &LightRangeLessOrEqual::CheckRange, "CheckRange"); + left_->WhenRange(demon_); + right_->WhenRange(demon_); +} + +void LightRangeLessOrEqual::InitialPropagate() { + left_->SetMax(right_->Max()); + right_->SetMin(left_->Min()); + if (left_->Max() <= right_->Min()) { + demon_->inhibit(solver()); + } +} + +void LightRangeLessOrEqual::CheckRange() { + if (left_->Min() > right_->Max()) { + solver()->Fail(); + } + if (left_->Max() <= right_->Min()) { + demon_->inhibit(solver()); + } +} + +std::string LightRangeLessOrEqual::DebugString() const { + return left_->DebugString() + " < " + right_->DebugString(); +} +} // namespace + +} // namespace operations_research diff --git a/ortools/constraint_solver/routing_constraints.h b/ortools/constraint_solver/routing_constraints.h new file mode 100644 index 00000000000..effff21ab98 --- /dev/null +++ b/ortools/constraint_solver/routing_constraints.h @@ -0,0 +1,47 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_CONSTRAINTS_H_ +#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_CONSTRAINTS_H_ + +#include +#include + +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/constraint_solver/routing.h" + +namespace operations_research { + +Constraint* MakeDifferentFromValues(Solver* solver, IntVar* var, + std::vector values); + +Constraint* MakeResourceConstraint( + const RoutingModel::ResourceGroup* resource_group, + const std::vector* vehicle_resource_vars, RoutingModel* model); + +/// For every vehicle of the routing model: +/// - if total_slacks[vehicle] is not nullptr, constrains it to be the sum of +/// slacks on that vehicle, that is, +/// dimension->CumulVar(end) - dimension->CumulVar(start) - +/// sum_{node in path of vehicle} dimension->FixedTransitVar(node). +/// - if spans[vehicle] is not nullptr, constrains it to be +/// dimension->CumulVar(end) - dimension->CumulVar(start) +/// This does stronger propagation than a decomposition, and takes breaks into +/// account. +Constraint* MakePathSpansAndTotalSlacks(const RoutingDimension* dimension, + std::vector spans, + std::vector total_slacks); + +} // namespace operations_research + +#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_CONSTRAINTS_H_ diff --git a/ortools/constraint_solver/routing_decision_builders.cc b/ortools/constraint_solver/routing_decision_builders.cc new file mode 100644 index 00000000000..4dd72180e30 --- /dev/null +++ b/ortools/constraint_solver/routing_decision_builders.cc @@ -0,0 +1,817 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/constraint_solver/routing_decision_builders.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "ortools/base/map_util.h" +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_lp_scheduling.h" +#include "ortools/util/saturated_arithmetic.h" + +namespace operations_research { + +namespace { + +// A decision builder which tries to assign values to variables as close as +// possible to target values first. +class SetValuesFromTargets : public DecisionBuilder { + public: + SetValuesFromTargets(std::vector variables, + std::vector targets) + : variables_(std::move(variables)), + targets_(std::move(targets)), + index_(0), + steps_(variables_.size(), 0) { + DCHECK_EQ(variables_.size(), targets_.size()); + } + Decision* Next(Solver* solver) override { + int index = index_.Value(); + while (index < variables_.size() && variables_[index]->Bound()) { + ++index; + } + index_.SetValue(solver, index); + if (index >= variables_.size()) return nullptr; + const int64_t variable_min = variables_[index]->Min(); + const int64_t variable_max = variables_[index]->Max(); + // Target can be before, inside, or after the variable range. + // We do a trichotomy on this for clarity. + if (targets_[index] <= variable_min) { + return solver->MakeAssignVariableValue(variables_[index], variable_min); + } else if (targets_[index] >= variable_max) { + return solver->MakeAssignVariableValue(variables_[index], variable_max); + } else { + int64_t step = steps_[index]; + int64_t value = CapAdd(targets_[index], step); + // If value is out of variable's range, we can remove the interval of + // values already explored (which can make the solver fail) and + // recall Next() to get back into the trichotomy above. + if (value < variable_min || variable_max < value) { + step = GetNextStep(step); + value = CapAdd(targets_[index], step); + if (step > 0) { + // Values in [variable_min, value) were already explored. + variables_[index]->SetMin(value); + } else { + // Values in (value, variable_max] were already explored. + variables_[index]->SetMax(value); + } + return Next(solver); + } + steps_.SetValue(solver, index, GetNextStep(step)); + return solver->MakeAssignVariableValueOrDoNothing(variables_[index], + value); + } + } + + private: + int64_t GetNextStep(int64_t step) const { + return (step > 0) ? -step : CapSub(1, step); + } + const std::vector variables_; + const std::vector targets_; + Rev index_; + RevArray steps_; +}; + +} // namespace + +DecisionBuilder* MakeSetValuesFromTargets(Solver* solver, + std::vector variables, + std::vector targets) { + return solver->RevAlloc( + new SetValuesFromTargets(std::move(variables), std::move(targets))); +} + +namespace { + +bool DimensionFixedTransitsEqualTransitEvaluatorForVehicle( + const RoutingDimension& dimension, int vehicle) { + const RoutingModel* const model = dimension.model(); + int node = model->Start(vehicle); + while (!model->IsEnd(node)) { + if (!model->NextVar(node)->Bound()) { + return false; + } + const int next = model->NextVar(node)->Value(); + if (dimension.transit_evaluator(vehicle)(node, next) != + dimension.FixedTransitVar(node)->Value()) { + return false; + } + node = next; + } + return true; +} + +bool DimensionFixedTransitsEqualTransitEvaluators( + const RoutingDimension& dimension) { + for (int vehicle = 0; vehicle < dimension.model()->vehicles(); vehicle++) { + if (!DimensionFixedTransitsEqualTransitEvaluatorForVehicle(dimension, + vehicle)) { + return false; + } + } + return true; +} + +// Concatenates cumul_values and break_values into 'values', and generates the +// corresponding 'variables' vector. +void ConcatenateRouteCumulAndBreakVarAndValues( + const RoutingDimension& dimension, int vehicle, + const std::vector& cumul_values, + const std::vector& break_values, std::vector* variables, + std::vector* values) { + *values = cumul_values; + variables->clear(); + const RoutingModel& model = *dimension.model(); + { + int current = model.Start(vehicle); + while (true) { + variables->push_back(dimension.CumulVar(current)); + if (!model.IsEnd(current)) { + current = model.NextVar(current)->Value(); + } else { + break; + } + } + } + // Setting the cumuls of path start/end first is more efficient than + // setting the cumuls in order of path appearance, because setting start + // and end cumuls gives an opportunity to fix all cumuls with two + // decisions instead of |path| decisions. + // To this effect, we put end cumul just after the start cumul. + std::swap(variables->at(1), variables->back()); + std::swap(values->at(1), values->back()); + if (dimension.HasBreakConstraints()) { + for (IntervalVar* interval : + dimension.GetBreakIntervalsOfVehicle(vehicle)) { + variables->push_back(interval->SafeStartExpr(0)->Var()); + variables->push_back(interval->SafeEndExpr(0)->Var()); + } + values->insert(values->end(), break_values.begin(), break_values.end()); + } + // Value kint64min signals an unoptimized variable, set to min instead. + for (int j = 0; j < values->size(); ++j) { + if (values->at(j) == std::numeric_limits::min()) { + values->at(j) = variables->at(j)->Min(); + } + } + DCHECK_EQ(variables->size(), values->size()); +} + +class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { + public: + SetCumulsFromLocalDimensionCosts( + LocalDimensionCumulOptimizer* local_optimizer, + LocalDimensionCumulOptimizer* local_mp_optimizer, SearchMonitor* monitor, + bool optimize_and_pack, + std::vector + dimension_travel_info_per_route) + : local_optimizer_(local_optimizer), + local_mp_optimizer_(local_mp_optimizer), + monitor_(monitor), + optimize_and_pack_(optimize_and_pack), + dimension_travel_info_per_route_( + std::move(dimension_travel_info_per_route)) { + DCHECK(dimension_travel_info_per_route_.empty() || + dimension_travel_info_per_route_.size() == + local_optimizer_->dimension()->model()->vehicles()); + const RoutingDimension* const dimension = local_optimizer->dimension(); + const std::vector& resource_groups = + dimension->model()->GetDimensionResourceGroupIndices(dimension); + DCHECK_LE(resource_groups.size(), optimize_and_pack ? 1 : 0); + resource_group_index_ = resource_groups.empty() ? -1 : resource_groups[0]; + } + + Decision* Next(Solver* solver) override { + const RoutingDimension& dimension = *local_optimizer_->dimension(); + RoutingModel* const model = dimension.model(); + // The following boolean variable indicates if the solver should fail, in + // order to postpone the Fail() call until after the for loop, so there are + // no memory leaks related to the cumul_values vector. + bool should_fail = false; + for (int vehicle = 0; vehicle < model->vehicles(); ++vehicle) { + solver->TopPeriodicCheck(); + // TODO(user): Investigate if we should skip unused vehicles. + DCHECK(DimensionFixedTransitsEqualTransitEvaluatorForVehicle(dimension, + vehicle)); + const bool vehicle_has_break_constraint = + dimension.HasBreakConstraints() && + !dimension.GetBreakIntervalsOfVehicle(vehicle).empty(); + LocalDimensionCumulOptimizer* const optimizer = + vehicle_has_break_constraint ? local_mp_optimizer_ : local_optimizer_; + DCHECK(optimizer != nullptr); + std::vector cumul_values; + std::vector break_start_end_values; + const DimensionSchedulingStatus status = + ComputeCumulAndBreakValuesForVehicle( + optimizer, vehicle, &cumul_values, &break_start_end_values); + if (status == DimensionSchedulingStatus::INFEASIBLE) { + should_fail = true; + break; + } + // If relaxation is not feasible, try the MILP optimizer. + if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { + DCHECK(local_mp_optimizer_ != nullptr); + if (ComputeCumulAndBreakValuesForVehicle(local_mp_optimizer_, vehicle, + &cumul_values, + &break_start_end_values) == + DimensionSchedulingStatus::INFEASIBLE) { + should_fail = true; + break; + } + } else { + DCHECK(status == DimensionSchedulingStatus::OPTIMAL); + } + // Concatenate cumul_values and break_start_end_values into cp_values, + // generate corresponding cp_variables vector. + std::vector cp_variables; + std::vector cp_values; + ConcatenateRouteCumulAndBreakVarAndValues( + dimension, vehicle, cumul_values, break_start_end_values, + &cp_variables, &cp_values); + if (!solver->SolveAndCommit( + MakeSetValuesFromTargets(solver, std::move(cp_variables), + std::move(cp_values)), + monitor_)) { + should_fail = true; + break; + } + } + if (should_fail) { + solver->Fail(); + } + return nullptr; + } + + private: + using ResourceGroup = RoutingModel::ResourceGroup; + using Resource = RoutingModel::ResourceGroup::Resource; + using RouteDimensionTravelInfo = RoutingModel::RouteDimensionTravelInfo; + + DimensionSchedulingStatus ComputeCumulAndBreakValuesForVehicle( + LocalDimensionCumulOptimizer* optimizer, int vehicle, + std::vector* cumul_values, + std::vector* break_start_end_values) { + cumul_values->clear(); + break_start_end_values->clear(); + RoutingModel* const model = optimizer->dimension()->model(); + const auto next = [model](int64_t n) { return model->NextVar(n)->Value(); }; + const RouteDimensionTravelInfo& dimension_travel_info = + dimension_travel_info_per_route_.empty() + ? RouteDimensionTravelInfo() + : dimension_travel_info_per_route_[vehicle]; + if (optimize_and_pack_) { + const int resource_index = + resource_group_index_ < 0 + ? -1 + : model->ResourceVar(vehicle, resource_group_index_)->Value(); + const Resource* const resource = + resource_index < 0 ? nullptr + : &model->GetResourceGroup(resource_group_index_) + ->GetResource(resource_index); + return optimizer->ComputePackedRouteCumuls( + vehicle, next, dimension_travel_info, resource, cumul_values, + break_start_end_values); + } else { + // TODO(user): Add the resource to the call in this case too! + return optimizer->ComputeRouteCumuls(vehicle, next, dimension_travel_info, + cumul_values, + break_start_end_values); + } + } + + LocalDimensionCumulOptimizer* const local_optimizer_; + LocalDimensionCumulOptimizer* const local_mp_optimizer_; + // Stores the resource group index of the local_[mp_]optimizer_'s dimension. + int resource_group_index_; + SearchMonitor* const monitor_; + const bool optimize_and_pack_; + const std::vector dimension_travel_info_per_route_; +}; + +} // namespace + +DecisionBuilder* MakeSetCumulsFromLocalDimensionCosts( + Solver* solver, LocalDimensionCumulOptimizer* local_optimizer, + LocalDimensionCumulOptimizer* local_mp_optimizer, SearchMonitor* monitor, + bool optimize_and_pack, + std::vector + dimension_travel_info_per_route) { + return solver->RevAlloc(new SetCumulsFromLocalDimensionCosts( + local_optimizer, local_mp_optimizer, monitor, optimize_and_pack, + std::move(dimension_travel_info_per_route))); +} + +namespace { + +class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { + public: + SetCumulsFromGlobalDimensionCosts( + GlobalDimensionCumulOptimizer* global_optimizer, + GlobalDimensionCumulOptimizer* global_mp_optimizer, + SearchMonitor* monitor, bool optimize_and_pack, + std::vector + dimension_travel_info_per_route) + : global_optimizer_(global_optimizer), + global_mp_optimizer_(global_mp_optimizer), + monitor_(monitor), + optimize_and_pack_(optimize_and_pack), + dimension_travel_info_per_route_( + std::move(dimension_travel_info_per_route)) { + DCHECK(dimension_travel_info_per_route_.empty() || + dimension_travel_info_per_route_.size() == + global_optimizer_->dimension()->model()->vehicles()); + } + + Decision* Next(Solver* solver) override { + // The following boolean variable indicates if the solver should fail, in + // order to postpone the Fail() call until after the scope, so there are + // no memory leaks related to the cumul_values vector. + bool should_fail = false; + { + const RoutingDimension* dimension = global_optimizer_->dimension(); + DCHECK(DimensionFixedTransitsEqualTransitEvaluators(*dimension)); + RoutingModel* const model = dimension->model(); + + GlobalDimensionCumulOptimizer* const optimizer = + model->GetDimensionResourceGroupIndices(dimension).empty() + ? global_optimizer_ + : global_mp_optimizer_; + std::vector cumul_values; + std::vector break_start_end_values; + std::vector> resource_indices_per_group; + const DimensionSchedulingStatus status = + ComputeCumulBreakAndResourceValues(optimizer, &cumul_values, + &break_start_end_values, + &resource_indices_per_group); + + if (status == DimensionSchedulingStatus::INFEASIBLE) { + should_fail = true; + } else if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { + // If relaxation is not feasible, try the MILP optimizer. + const DimensionSchedulingStatus mp_status = + ComputeCumulBreakAndResourceValues( + global_mp_optimizer_, &cumul_values, &break_start_end_values, + &resource_indices_per_group); + if (mp_status != DimensionSchedulingStatus::OPTIMAL) { + should_fail = true; + } + } else { + DCHECK(status == DimensionSchedulingStatus::OPTIMAL); + } + if (!should_fail) { + // Concatenate cumul_values and break_start_end_values into cp_values, + // generate corresponding cp_variables vector. + std::vector cp_variables = dimension->cumuls(); + std::vector cp_values; + std::swap(cp_values, cumul_values); + if (dimension->HasBreakConstraints()) { + const int num_vehicles = model->vehicles(); + for (int vehicle = 0; vehicle < num_vehicles; ++vehicle) { + for (IntervalVar* interval : + dimension->GetBreakIntervalsOfVehicle(vehicle)) { + cp_variables.push_back(interval->SafeStartExpr(0)->Var()); + cp_variables.push_back(interval->SafeEndExpr(0)->Var()); + } + } + cp_values.insert(cp_values.end(), break_start_end_values.begin(), + break_start_end_values.end()); + } + for (int rg_index : + model->GetDimensionResourceGroupIndices(dimension)) { + const std::vector& resource_values = + resource_indices_per_group[rg_index]; + DCHECK(!resource_values.empty()); + cp_values.insert(cp_values.end(), resource_values.begin(), + resource_values.end()); + const std::vector& resource_vars = + model->ResourceVars(rg_index); + DCHECK_EQ(resource_vars.size(), resource_values.size()); + cp_variables.insert(cp_variables.end(), resource_vars.begin(), + resource_vars.end()); + } + // 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()) { + cp_values[j] = cp_variables[j]->Min(); + } + } + if (!solver->SolveAndCommit( + MakeSetValuesFromTargets(solver, std::move(cp_variables), + std::move(cp_values)), + monitor_)) { + should_fail = true; + } + } + } + if (should_fail) { + solver->Fail(); + } + return nullptr; + } + + private: + DimensionSchedulingStatus ComputeCumulBreakAndResourceValues( + GlobalDimensionCumulOptimizer* optimizer, + std::vector* cumul_values, + std::vector* break_start_end_values, + std::vector>* resource_indices_per_group) { + DCHECK_NE(optimizer, nullptr); + cumul_values->clear(); + break_start_end_values->clear(); + resource_indices_per_group->clear(); + RoutingModel* const model = optimizer->dimension()->model(); + const auto next = [model](int64_t n) { return model->NextVar(n)->Value(); }; + return optimize_and_pack_ + ? optimizer->ComputePackedCumuls( + next, dimension_travel_info_per_route_, cumul_values, + break_start_end_values, resource_indices_per_group) + : optimizer->ComputeCumuls( + next, dimension_travel_info_per_route_, cumul_values, + break_start_end_values, resource_indices_per_group); + } + + GlobalDimensionCumulOptimizer* const global_optimizer_; + GlobalDimensionCumulOptimizer* const global_mp_optimizer_; + SearchMonitor* const monitor_; + const bool optimize_and_pack_; + const std::vector + dimension_travel_info_per_route_; +}; + +} // namespace + +DecisionBuilder* MakeSetCumulsFromGlobalDimensionCosts( + Solver* solver, GlobalDimensionCumulOptimizer* global_optimizer, + GlobalDimensionCumulOptimizer* global_mp_optimizer, SearchMonitor* monitor, + bool optimize_and_pack, + std::vector + dimension_travel_info_per_route) { + return solver->RevAlloc(new SetCumulsFromGlobalDimensionCosts( + global_optimizer, global_mp_optimizer, monitor, optimize_and_pack, + std::move(dimension_travel_info_per_route))); +} + +namespace { + +class SetCumulsFromResourceAssignmentCosts : public DecisionBuilder { + public: + SetCumulsFromResourceAssignmentCosts( + LocalDimensionCumulOptimizer* lp_optimizer, + LocalDimensionCumulOptimizer* mp_optimizer, SearchMonitor* monitor) + : model_(*lp_optimizer->dimension()->model()), + dimension_(*lp_optimizer->dimension()), + lp_optimizer_(lp_optimizer), + mp_optimizer_(mp_optimizer), + rg_index_(model_.GetDimensionResourceGroupIndex(&dimension_)), + resource_group_(*model_.GetResourceGroup(rg_index_)), + monitor_(monitor) {} + + Decision* Next(Solver* const solver) override { + bool should_fail = false; + { + const int num_vehicles = model_.vehicles(); + std::vector> assignment_costs(num_vehicles); + std::vector>> cumul_values(num_vehicles); + std::vector>> break_values(num_vehicles); + + const auto next = [&model = model_](int64_t n) { + return model.NextVar(n)->Value(); + }; + DCHECK(DimensionFixedTransitsEqualTransitEvaluators(dimension_)); + + for (int v : resource_group_.GetVehiclesRequiringAResource()) { + if (!ComputeVehicleToResourcesAssignmentCosts( + v, resource_group_, next, dimension_.transit_evaluator(v), + /*optimize_vehicle_costs*/ true, lp_optimizer_, mp_optimizer_, + &assignment_costs[v], &cumul_values[v], &break_values[v])) { + should_fail = true; + break; + } + } + + std::vector resource_indices(num_vehicles); + should_fail = + should_fail || + ComputeBestVehicleToResourceAssignment( + resource_group_.GetVehiclesRequiringAResource(), + resource_group_.Size(), + [&assignment_costs](int v) { return &assignment_costs[v]; }, + &resource_indices) < 0; + + if (!should_fail) { + DCHECK_EQ(resource_indices.size(), num_vehicles); + const int num_resources = resource_group_.Size(); + for (int v : resource_group_.GetVehiclesRequiringAResource()) { + if (next(model_.Start(v)) == model_.End(v) && + !model_.IsVehicleUsedWhenEmpty(v)) { + continue; + } + const int resource_index = resource_indices[v]; + DCHECK_GE(resource_index, 0); + DCHECK_EQ(cumul_values[v].size(), num_resources); + DCHECK_EQ(break_values[v].size(), num_resources); + const std::vector& optimal_cumul_values = + cumul_values[v][resource_index]; + const std::vector& optimal_break_values = + break_values[v][resource_index]; + std::vector cp_variables; + std::vector cp_values; + ConcatenateRouteCumulAndBreakVarAndValues( + dimension_, v, optimal_cumul_values, optimal_break_values, + &cp_variables, &cp_values); + + const std::vector& resource_vars = + model_.ResourceVars(rg_index_); + DCHECK_EQ(resource_vars.size(), resource_indices.size()); + cp_variables.insert(cp_variables.end(), resource_vars.begin(), + resource_vars.end()); + cp_values.insert(cp_values.end(), resource_indices.begin(), + resource_indices.end()); + if (!solver->SolveAndCommit( + MakeSetValuesFromTargets(solver, std::move(cp_variables), + std::move(cp_values)), + monitor_)) { + should_fail = true; + break; + } + } + } + } + if (should_fail) { + solver->Fail(); + } + return nullptr; + } + + private: + const RoutingModel& model_; + const RoutingDimension& dimension_; + LocalDimensionCumulOptimizer* lp_optimizer_; + LocalDimensionCumulOptimizer* mp_optimizer_; + const int rg_index_; + const RoutingModel::ResourceGroup& resource_group_; + SearchMonitor* const monitor_; +}; + +} // namespace + +DecisionBuilder* MakeSetCumulsFromResourceAssignmentCosts( + Solver* solver, LocalDimensionCumulOptimizer* lp_optimizer, + LocalDimensionCumulOptimizer* mp_optimizer, SearchMonitor* monitor) { + return solver->RevAlloc(new SetCumulsFromResourceAssignmentCosts( + lp_optimizer, mp_optimizer, monitor)); +} +namespace { +// A decision builder that tries to set variables to their value in the last +// solution, if their corresponding vehicle path has not changed. +// This tries to constrain all such variables in one shot in order to speed up +// instantiation. +// TODO(user): try to use Assignment instead of MakeAssignment(), +// try to record and restore the min/max instead of a single value. +class RestoreDimensionValuesForUnchangedRoutes : public DecisionBuilder { + public: + explicit RestoreDimensionValuesForUnchangedRoutes(RoutingModel* model) + : model_(model) { + model_->AddAtSolutionCallback([this]() { AtSolution(); }); + next_last_value_.resize(model_->Nexts().size(), -1); + } + + // In a given branch of a search tree, this decision builder only returns + // a Decision once, the first time it is called in that branch. + Decision* Next(Solver* const s) override { + if (!must_return_decision_) return nullptr; + s->SaveAndSetValue(&must_return_decision_, false); + return MakeDecision(s); + } + + private: + // Initialize() is lazy to make sure all dimensions have been instantiated + // when initialization is done. + void Initialize() { + is_initialized_ = true; + const int num_nodes = model_->VehicleVars().size(); + node_to_integer_variable_indices_.resize(num_nodes); + node_to_interval_variable_indices_.resize(num_nodes); + // Search for dimension variables that correspond to input variables. + for (const std::string& dimension_name : model_->GetAllDimensionNames()) { + const RoutingDimension& dimension = + model_->GetDimensionOrDie(dimension_name); + // Search among cumuls and slacks, and attach them to corresponding nodes. + for (const std::vector& dimension_variables : + {dimension.cumuls(), dimension.slacks()}) { + const int num_dimension_variables = dimension_variables.size(); + DCHECK_LE(num_dimension_variables, num_nodes); + for (int node = 0; node < num_dimension_variables; ++node) { + node_to_integer_variable_indices_[node].push_back( + integer_variables_.size()); + integer_variables_.push_back(dimension_variables[node]); + } + } + // Search for break start/end variables, attach them to vehicle starts. + for (int vehicle = 0; vehicle < model_->vehicles(); ++vehicle) { + if (!dimension.HasBreakConstraints()) continue; + const int vehicle_start = model_->Start(vehicle); + for (IntervalVar* interval : + dimension.GetBreakIntervalsOfVehicle(vehicle)) { + node_to_interval_variable_indices_[vehicle_start].push_back( + interval_variables_.size()); + interval_variables_.push_back(interval); + } + } + } + integer_variables_last_min_.resize(integer_variables_.size()); + interval_variables_last_start_min_.resize(interval_variables_.size()); + interval_variables_last_end_max_.resize(interval_variables_.size()); + } + + Decision* MakeDecision(Solver* const s) { + if (!is_initialized_) return nullptr; + // Collect vehicles that have not changed. + std::vector unchanged_vehicles; + const int num_vehicles = model_->vehicles(); + for (int v = 0; v < num_vehicles; ++v) { + bool unchanged = true; + for (int current = model_->Start(v); !model_->IsEnd(current); + current = next_last_value_[current]) { + if (!model_->NextVar(current)->Bound() || + next_last_value_[current] != model_->NextVar(current)->Value()) { + unchanged = false; + break; + } + } + if (unchanged) unchanged_vehicles.push_back(v); + } + // If all routes are unchanged, the solver might be trying to do a full + // reschedule. Do nothing. + if (unchanged_vehicles.size() == num_vehicles) return nullptr; + + // Collect cumuls and slacks of unchanged routes to be assigned a value. + std::vector vars; + std::vector values; + for (const int vehicle : unchanged_vehicles) { + for (int current = model_->Start(vehicle); true; + current = next_last_value_[current]) { + for (const int index : node_to_integer_variable_indices_[current]) { + vars.push_back(integer_variables_[index]); + values.push_back(integer_variables_last_min_[index]); + } + for (const int index : node_to_interval_variable_indices_[current]) { + const int64_t start_min = interval_variables_last_start_min_[index]; + const int64_t end_max = interval_variables_last_end_max_[index]; + if (start_min < end_max) { + vars.push_back(interval_variables_[index]->SafeStartExpr(0)->Var()); + values.push_back(interval_variables_last_start_min_[index]); + vars.push_back(interval_variables_[index]->SafeEndExpr(0)->Var()); + values.push_back(interval_variables_last_end_max_[index]); + } else { + vars.push_back(interval_variables_[index]->PerformedExpr()->Var()); + values.push_back(0); + } + } + if (model_->IsEnd(current)) break; + } + } + return s->MakeAssignVariablesValuesOrDoNothing(vars, values); + } + + void AtSolution() { + if (!is_initialized_) Initialize(); + const int num_integers = integer_variables_.size(); + // Variables may not be fixed at solution time, + // the decision builder is fine with the Min() of the unfixed variables. + for (int i = 0; i < num_integers; ++i) { + integer_variables_last_min_[i] = integer_variables_[i]->Min(); + } + const int num_intervals = interval_variables_.size(); + for (int i = 0; i < num_intervals; ++i) { + const bool is_performed = interval_variables_[i]->MustBePerformed(); + interval_variables_last_start_min_[i] = + is_performed ? interval_variables_[i]->StartMin() : 0; + interval_variables_last_end_max_[i] = + is_performed ? interval_variables_[i]->EndMax() : -1; + } + const int num_nodes = next_last_value_.size(); + for (int node = 0; node < num_nodes; ++node) { + if (model_->IsEnd(node)) continue; + next_last_value_[node] = model_->NextVar(node)->Value(); + } + } + + // Input data. + RoutingModel* const model_; + + // The valuation of the last solution. + std::vector next_last_value_; + // For every node, the indices of integer_variables_ and interval_variables_ + // that correspond to that node. + std::vector> node_to_integer_variable_indices_; + std::vector> node_to_interval_variable_indices_; + // Variables and the value they had in the previous solution. + std::vector integer_variables_; + std::vector integer_variables_last_min_; + std::vector interval_variables_; + std::vector interval_variables_last_start_min_; + std::vector interval_variables_last_end_max_; + + bool is_initialized_ = false; + bool must_return_decision_ = true; +}; +} // namespace + +DecisionBuilder* MakeRestoreDimensionValuesForUnchangedRoutes( + RoutingModel* model) { + return model->solver()->RevAlloc( + new RestoreDimensionValuesForUnchangedRoutes(model)); +} + +// FinalizerVariables + +void FinalizerVariables::AddWeightedVariableTarget(IntVar* var, int64_t target, + int64_t cost) { + CHECK(var != nullptr); + const int index = + gtl::LookupOrInsert(&weighted_finalizer_variable_index_, var, + weighted_finalizer_variable_targets_.size()); + if (index < weighted_finalizer_variable_targets_.size()) { + auto& [var_target, total_cost] = + weighted_finalizer_variable_targets_[index]; + DCHECK_EQ(var_target.var, var); + DCHECK_EQ(var_target.target, target); + total_cost = CapAdd(total_cost, cost); + } else { + DCHECK_EQ(index, weighted_finalizer_variable_targets_.size()); + weighted_finalizer_variable_targets_.push_back({{var, target}, cost}); + } +} + +void FinalizerVariables::AddWeightedVariableToMinimize(IntVar* var, + int64_t cost) { + AddWeightedVariableTarget(var, std::numeric_limits::min(), cost); +} + +void FinalizerVariables::AddWeightedVariableToMaximize(IntVar* var, + int64_t cost) { + AddWeightedVariableTarget(var, std::numeric_limits::max(), cost); +} + +void FinalizerVariables::AddVariableTarget(IntVar* var, int64_t target) { + CHECK(var != nullptr); + if (finalizer_variable_target_set_.contains(var)) return; + finalizer_variable_target_set_.insert(var); + finalizer_variable_targets_.push_back({var, target}); +} + +void FinalizerVariables::AddVariableToMaximize(IntVar* var) { + AddVariableTarget(var, std::numeric_limits::max()); +} + +void FinalizerVariables::AddVariableToMinimize(IntVar* var) { + AddVariableTarget(var, std::numeric_limits::min()); +} + +DecisionBuilder* FinalizerVariables::CreateFinalizer() { + std::stable_sort(weighted_finalizer_variable_targets_.begin(), + weighted_finalizer_variable_targets_.end(), + [](const std::pair& var_cost1, + const std::pair& var_cost2) { + return var_cost1.second > var_cost2.second; + }); + const int num_variables = weighted_finalizer_variable_targets_.size() + + finalizer_variable_targets_.size(); + std::vector variables; + std::vector targets; + variables.reserve(num_variables); + targets.reserve(num_variables); + for (const auto& [var_target, cost] : weighted_finalizer_variable_targets_) { + variables.push_back(var_target.var); + targets.push_back(var_target.target); + } + for (const auto& [var, target] : finalizer_variable_targets_) { + variables.push_back(var); + targets.push_back(target); + } + return MakeSetValuesFromTargets(solver_, std::move(variables), + std::move(targets)); +} + +} // namespace operations_research diff --git a/ortools/constraint_solver/routing_decision_builders.h b/ortools/constraint_solver/routing_decision_builders.h new file mode 100644 index 00000000000..3cace237ff0 --- /dev/null +++ b/ortools/constraint_solver/routing_decision_builders.h @@ -0,0 +1,118 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_DECISION_BUILDERS_H_ +#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_DECISION_BUILDERS_H_ + +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_lp_scheduling.h" + +namespace operations_research { + +/// A decision builder which tries to assign values to variables as close as +/// possible to target values first. +DecisionBuilder* MakeSetValuesFromTargets(Solver* solver, + std::vector variables, + std::vector targets); + +/// Functions returning decision builders which try to instantiate dimension +/// cumul variables using scheduling optimizers. + +/// Variant based on local optimizers, for which each route is handled +/// separately. +DecisionBuilder* MakeSetCumulsFromLocalDimensionCosts( + Solver* solver, LocalDimensionCumulOptimizer* local_optimizer, + LocalDimensionCumulOptimizer* local_mp_optimizer, SearchMonitor* monitor, + bool optimize_and_pack = false, + std::vector + dimension_travel_info_per_route = {}); + +/// Variant based on global optimizers, handling all routes together. +DecisionBuilder* MakeSetCumulsFromGlobalDimensionCosts( + Solver* solver, GlobalDimensionCumulOptimizer* global_optimizer, + GlobalDimensionCumulOptimizer* global_mp_optimizer, SearchMonitor* monitor, + bool optimize_and_pack = false, + std::vector + dimension_travel_info_per_route = {}); + +/// Variant taking into account resources. +DecisionBuilder* MakeSetCumulsFromResourceAssignmentCosts( + Solver* solver, LocalDimensionCumulOptimizer* lp_optimizer, + LocalDimensionCumulOptimizer* mp_optimizer, SearchMonitor* monitor); + +/// A decision builder that monitors solutions, and tries to fix dimension +/// variables whose route did not change in the candidate solution. +/// Dimension variables are Cumul, Slack and break variables of all dimensions. +/// The user must make sure that those variables will be always be fixed at +/// solution, typically by composing another DecisionBuilder after this one. +/// If this DecisionBuilder returns a non-nullptr value at some node of the +/// search tree, it will always return nullptr in the subtree of that node. +/// Moreover, the decision will be a simultaneous assignment of the dimension +/// variables of unchanged routes on the left branch, and an empty decision on +/// the right branch. +DecisionBuilder* MakeRestoreDimensionValuesForUnchangedRoutes( + RoutingModel* model); + +/// A container that allows to accumulate variables and weights to generate a +/// static DecisionBuilder that uses weights to prioritize the branching +/// decisions (by decreasing weight). +class FinalizerVariables { + public: + explicit FinalizerVariables(Solver* solver) : solver_(solver) {} + /// Adds a variable to minimize in the solution finalizer. The solution + /// finalizer is called each time a solution is found during the search and + /// allows to instantiate secondary variables (such as dimension cumul + /// variables). + void AddVariableToMinimize(IntVar* var); + /// Adds a variable to maximize in the solution finalizer (see above for + /// information on the solution finalizer). + void AddVariableToMaximize(IntVar* var); + /// Adds a variable to minimize in the solution finalizer, with a weighted + /// priority: the higher the more priority it has. + void AddWeightedVariableToMinimize(IntVar* var, int64_t cost); + /// Adds a variable to maximize in the solution finalizer, with a weighted + /// priority: the higher the more priority it has. + void AddWeightedVariableToMaximize(IntVar* var, int64_t cost); + /// Add a variable to set the closest possible to the target value in the + /// solution finalizer. + void AddVariableTarget(IntVar* var, int64_t target); + /// Same as above with a weighted priority: the higher the cost, the more + /// priority it has to be set close to the target value. + void AddWeightedVariableTarget(IntVar* var, int64_t target, int64_t cost); + /// + DecisionBuilder* CreateFinalizer(); + + private: + Solver* const solver_; +#ifndef SWIG + struct VarTarget { + IntVar* var; + int64_t target; + }; + std::vector> + weighted_finalizer_variable_targets_; + std::vector finalizer_variable_targets_; + absl::flat_hash_map weighted_finalizer_variable_index_; + absl::flat_hash_set finalizer_variable_target_set_; +#endif +}; + +} // namespace operations_research +#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_DECISION_BUILDERS_H_ diff --git a/ortools/constraint_solver/routing_insertion_lns.cc b/ortools/constraint_solver/routing_insertion_lns.cc new file mode 100644 index 00000000000..744538d073c --- /dev/null +++ b/ortools/constraint_solver/routing_insertion_lns.cc @@ -0,0 +1,549 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/constraint_solver/routing_insertion_lns.h" + +#include +#include +#include +#include +#include +#include + +#include "ortools/base/int_type.h" +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/constraint_solver/constraint_solveri.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_search.h" +#include "ortools/constraint_solver/routing_utils.h" +#include "ortools/util/bitset.h" + +namespace operations_research { + +// FilteredHeuristicLocalSearchOperator + +FilteredHeuristicLocalSearchOperator::FilteredHeuristicLocalSearchOperator( + std::unique_ptr heuristic, + bool keep_inverse_values) + : IntVarLocalSearchOperator(heuristic->model()->Nexts(), + keep_inverse_values), + model_(heuristic->model()), + removed_nodes_(model_->Size()), + heuristic_(std::move(heuristic)), + consider_vehicle_vars_(!model_->CostsAreHomogeneousAcrossVehicles()) { + if (consider_vehicle_vars_) { + AddVars(model_->VehicleVars()); + } +} + +bool FilteredHeuristicLocalSearchOperator::MakeOneNeighbor() { + while (IncrementPosition()) { + if (model_->CheckLimit()) { + // NOTE: Even though the limit is checked in the BuildSolutionFromRoutes() + // method of the heuristics, we still check it here to avoid calling + // IncrementPosition() and building a solution for every possible position + // if the time limit is reached. + return false; + } + // NOTE: No need to call RevertChanges() here as MakeChangeAndInsertNodes() + // will always return true if any change was made. + if (MakeChangesAndInsertNodes()) { + return true; + } + } + return false; +} + +bool FilteredHeuristicLocalSearchOperator::MakeChangesAndInsertNodes() { + removed_nodes_.SparseClearAll(); + + const std::function next_accessor = + SetupNextAccessorForNeighbor(); + if (next_accessor == nullptr) { + return false; + } + const Assignment* const result_assignment = + heuristic_->BuildSolutionFromRoutes(next_accessor); + + if (result_assignment == nullptr) { + return false; + } + + bool has_change = false; + const std::vector& elements = + result_assignment->IntVarContainer().elements(); + for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { + int64_t node_index = model_->Start(vehicle); + while (!model_->IsEnd(node_index)) { + // NOTE: When building the solution in the heuristic, Next vars are added + // to the assignment at the position corresponding to their index. + const IntVarElement& node_element = elements[node_index]; + DCHECK_EQ(node_element.Var(), model_->NextVar(node_index)); + + const int64_t new_node_value = node_element.Value(); + DCHECK_NE(new_node_value, node_index); + + const int64_t vehicle_var_index = VehicleVarIndex(node_index); + if (OldValue(node_index) != new_node_value || + (consider_vehicle_vars_ && OldValue(vehicle_var_index) != vehicle)) { + has_change = true; + SetValue(node_index, new_node_value); + if (consider_vehicle_vars_) { + SetValue(vehicle_var_index, vehicle); + } + } + node_index = new_node_value; + } + } + // Check for newly unperformed nodes among the ones removed for insertion by + // the heuristic. + for (int64_t node : removed_nodes_.PositionsSetAtLeastOnce()) { + const IntVarElement& node_element = elements[node]; + DCHECK_EQ(node_element.Var(), model_->NextVar(node)); + if (node_element.Value() == node) { + DCHECK_NE(OldValue(node), node); + has_change = true; + SetValue(node, node); + if (consider_vehicle_vars_) { + const int64_t vehicle_var_index = VehicleVarIndex(node); + DCHECK_NE(OldValue(vehicle_var_index), -1); + SetValue(vehicle_var_index, -1); + } + } + } + return has_change; +} + +// FilteredHeuristicPathLNSOperator + +FilteredHeuristicPathLNSOperator::FilteredHeuristicPathLNSOperator( + std::unique_ptr heuristic) + : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), + current_route_(0), + last_route_(0), + just_started_(false) {} + +void FilteredHeuristicPathLNSOperator::OnStart() { + // NOTE: We set last_route_ to current_route_ here to make sure all routes + // are scanned in IncrementCurrentRouteToNextNonEmpty(). + last_route_ = current_route_; + if (CurrentRouteIsEmpty()) { + IncrementCurrentRouteToNextNonEmpty(); + } + just_started_ = true; +} + +bool FilteredHeuristicPathLNSOperator::IncrementPosition() { + if (just_started_) { + just_started_ = false; + return !CurrentRouteIsEmpty(); + } + IncrementCurrentRouteToNextNonEmpty(); + return current_route_ != last_route_; +} + +bool FilteredHeuristicPathLNSOperator::CurrentRouteIsEmpty() const { + return model_->IsEnd(OldValue(model_->Start(current_route_))); +} + +void FilteredHeuristicPathLNSOperator::IncrementCurrentRouteToNextNonEmpty() { + const int num_routes = model_->vehicles(); + do { + ++current_route_ %= num_routes; + if (current_route_ == last_route_) { + // All routes have been scanned. + return; + } + } while (CurrentRouteIsEmpty()); +} + +std::function +FilteredHeuristicPathLNSOperator::SetupNextAccessorForNeighbor() { + const int64_t start_node = model_->Start(current_route_); + const int64_t end_node = model_->End(current_route_); + + int64_t node = Value(start_node); + while (node != end_node) { + removed_nodes_.Set(node); + node = Value(node); + } + + return [this, start_node, end_node](int64_t node) { + if (node == start_node) return end_node; + return Value(node); + }; +} + +// RelocatePathAndHeuristicInsertUnperformedOperator + +RelocatePathAndHeuristicInsertUnperformedOperator:: + RelocatePathAndHeuristicInsertUnperformedOperator( + std::unique_ptr heuristic) + : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), + route_to_relocate_index_(0), + empty_route_index_(0), + just_started_(false) {} + +void RelocatePathAndHeuristicInsertUnperformedOperator::OnStart() { + has_unperformed_nodes_ = false; + last_node_on_route_.resize(model_->vehicles()); + routes_to_relocate_.clear(); + empty_routes_.clear(); + std::vector empty_vehicle_of_vehicle_class_added( + model_->GetVehicleClassesCount(), false); + for (int64_t node = 0; node < model_->Size(); node++) { + const int64_t next = OldValue(node); + if (next == node) { + has_unperformed_nodes_ = true; + continue; + } + if (model_->IsEnd(next)) { + last_node_on_route_[model_->VehicleIndex(next)] = node; + } + } + + for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { + const int64_t next = OldValue(model_->Start(vehicle)); + if (!model_->IsEnd(next)) { + routes_to_relocate_.push_back(vehicle); + continue; + } + const int vehicle_class = + model_->GetVehicleClassIndexOfVehicle(vehicle).value(); + if (!empty_vehicle_of_vehicle_class_added[vehicle_class]) { + empty_routes_.push_back(vehicle); + empty_vehicle_of_vehicle_class_added[vehicle_class] = true; + } + } + + if (empty_route_index_ >= empty_routes_.size()) { + empty_route_index_ = 0; + } + if (route_to_relocate_index_ >= routes_to_relocate_.size()) { + route_to_relocate_index_ = 0; + } + last_empty_route_index_ = empty_route_index_; + last_route_to_relocate_index_ = route_to_relocate_index_; + + just_started_ = true; +} + +bool RelocatePathAndHeuristicInsertUnperformedOperator::IncrementPosition() { + if (!has_unperformed_nodes_ || empty_routes_.empty() || + routes_to_relocate_.empty()) { + return false; + } + if (just_started_) { + just_started_ = false; + return true; + } + return IncrementRoutes(); +} + +bool RelocatePathAndHeuristicInsertUnperformedOperator::IncrementRoutes() { + ++empty_route_index_ %= empty_routes_.size(); + if (empty_route_index_ != last_empty_route_index_) { + return true; + } + ++route_to_relocate_index_ %= routes_to_relocate_.size(); + return route_to_relocate_index_ != last_route_to_relocate_index_; +} + +std::function +RelocatePathAndHeuristicInsertUnperformedOperator:: + SetupNextAccessorForNeighbor() { + const int empty_route = empty_routes_[empty_route_index_]; + const int relocated_route = routes_to_relocate_[route_to_relocate_index_]; + if (model_->GetVehicleClassIndexOfVehicle(empty_route) == + model_->GetVehicleClassIndexOfVehicle(relocated_route)) { + // Don't try to relocate the route to an empty vehicle of the same class. + return nullptr; + } + + const int64_t empty_start_node = model_->Start(empty_route); + const int64_t empty_end_node = model_->End(empty_route); + + const int64_t relocated_route_start = model_->Start(relocated_route); + const int64_t first_relocated_node = OldValue(relocated_route_start); + const int64_t last_relocated_node = last_node_on_route_[relocated_route]; + const int64_t relocated_route_end = model_->End(relocated_route); + + return [this, empty_start_node, empty_end_node, first_relocated_node, + last_relocated_node, relocated_route_start, + relocated_route_end](int64_t node) { + if (node == relocated_route_start) return relocated_route_end; + if (node == empty_start_node) return first_relocated_node; + if (node == last_relocated_node) return empty_end_node; + return Value(node); + }; +} + +// FilteredHeuristicCloseNodesLNSOperator + +FilteredHeuristicCloseNodesLNSOperator::FilteredHeuristicCloseNodesLNSOperator( + std::unique_ptr heuristic, int num_close_nodes) + : FilteredHeuristicLocalSearchOperator(std::move(heuristic), + /*keep_inverse_values*/ true), + pickup_delivery_pairs_(model_->GetPickupAndDeliveryPairs()), + current_node_(0), + last_node_(0), + just_started_(false), + initialized_(false), + close_nodes_(model_->Size()), + num_close_nodes_(num_close_nodes), + new_nexts_(model_->Size()), + changed_nexts_(model_->Size()), + new_prevs_(model_->Size()), + changed_prevs_(model_->Size()) {} + +void FilteredHeuristicCloseNodesLNSOperator::Initialize() { + if (initialized_) return; + initialized_ = true; + const int64_t size = model_->Size(); + const int64_t max_num_neighbors = + std::max(0, size - 1 - model_->vehicles()); + const int64_t num_closest_neighbors = + std::min(num_close_nodes_, max_num_neighbors); + DCHECK_GE(num_closest_neighbors, 0); + + if (num_closest_neighbors == 0) return; + + const int64_t num_cost_classes = model_->GetCostClassesCount(); + + for (int64_t node = 0; node < size; node++) { + if (model_->IsStart(node) || model_->IsEnd(node)) continue; + + std::vector> + costed_after_nodes; + costed_after_nodes.reserve(size); + for (int64_t after_node = 0; after_node < size; after_node++) { + if (model_->IsStart(after_node) || model_->IsEnd(after_node) || + after_node == node) { + continue; + } + double total_cost = 0.0; + // NOTE: We don't consider the 'always-zero' cost class when searching for + // closest neighbors. + for (int cost_class = 1; cost_class < num_cost_classes; cost_class++) { + total_cost += model_->GetArcCostForClass(node, after_node, cost_class); + } + costed_after_nodes.emplace_back(total_cost, after_node); + } + + std::nth_element(costed_after_nodes.begin(), + costed_after_nodes.begin() + num_closest_neighbors - 1, + costed_after_nodes.end()); + std::vector& neighbors = close_nodes_[node]; + neighbors.reserve(num_closest_neighbors); + for (int index = 0; index < num_closest_neighbors; index++) { + neighbors.push_back(costed_after_nodes[index].second); + } + } +} + +void FilteredHeuristicCloseNodesLNSOperator::OnStart() { + Initialize(); + last_node_ = current_node_; + just_started_ = true; +} + +bool FilteredHeuristicCloseNodesLNSOperator::IncrementPosition() { + DCHECK(initialized_); + if (just_started_) { + just_started_ = false; + return true; + } + ++current_node_ %= model_->Size(); + return current_node_ != last_node_; +} + +void FilteredHeuristicCloseNodesLNSOperator::RemoveNode(int64_t node) { + DCHECK(!model_->IsEnd(node) && !model_->IsStart(node)); + DCHECK_NE(Value(node), node); + DCHECK(IsActive(node)); + + removed_nodes_.Set(node); + const int64_t prev = Prev(node); + const int64_t next = Next(node); + changed_nexts_.Set(prev); + new_nexts_[prev] = next; + if (next < model_->Size()) { + changed_prevs_.Set(next); + new_prevs_[next] = prev; + } +} + +void FilteredHeuristicCloseNodesLNSOperator::RemoveNodeAndActiveSibling( + int64_t node) { + if (!IsActive(node)) return; + RemoveNode(node); + + for (int64_t sibling_node : GetActiveSiblings(node)) { + if (!model_->IsStart(sibling_node) && !model_->IsEnd(sibling_node)) { + RemoveNode(sibling_node); + } + } +} + +std::vector FilteredHeuristicCloseNodesLNSOperator::GetActiveSiblings( + int64_t node) const { + // NOTE: In most use-cases, where each node is a pickup or delivery in a + // single index pair, this function is in O(k) where k is the number of + // alternative deliveries or pickups for this index pair. + std::vector active_siblings; + for (std::pair index_pair : + model_->GetPickupIndexPairs(node)) { + for (int64_t sibling_delivery : + pickup_delivery_pairs_[index_pair.first].second) { + if (IsActive(sibling_delivery)) { + active_siblings.push_back(sibling_delivery); + break; + } + } + } + for (std::pair index_pair : + model_->GetDeliveryIndexPairs(node)) { + for (int64_t sibling_pickup : + pickup_delivery_pairs_[index_pair.first].first) { + if (IsActive(sibling_pickup)) { + active_siblings.push_back(sibling_pickup); + break; + } + } + } + return active_siblings; +} + +std::function +FilteredHeuristicCloseNodesLNSOperator::SetupNextAccessorForNeighbor() { + DCHECK(initialized_); + if (model_->IsStart(current_node_)) { + return nullptr; + } + DCHECK(!model_->IsEnd(current_node_)); + + changed_nexts_.SparseClearAll(); + changed_prevs_.SparseClearAll(); + + RemoveNodeAndActiveSibling(current_node_); + + for (int64_t neighbor : close_nodes_[current_node_]) { + RemoveNodeAndActiveSibling(neighbor); + } + + return [this](int64_t node) { return Next(node); }; +} + +// FilteredHeuristicExpensiveChainLNSOperator + +FilteredHeuristicExpensiveChainLNSOperator:: + FilteredHeuristicExpensiveChainLNSOperator( + std::unique_ptr heuristic, + int num_arcs_to_consider, + std::function + arc_cost_for_route_start) + : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), + current_route_(0), + last_route_(0), + num_arcs_to_consider_(num_arcs_to_consider), + current_expensive_arc_indices_({-1, -1}), + arc_cost_for_route_start_(std::move(arc_cost_for_route_start)), + just_started_(false) { + DCHECK_GE(num_arcs_to_consider_, 2); +} + +void FilteredHeuristicExpensiveChainLNSOperator::OnStart() { + last_route_ = current_route_; + just_started_ = true; +} + +bool FilteredHeuristicExpensiveChainLNSOperator::IncrementPosition() { + if (just_started_) { + just_started_ = false; + return FindMostExpensiveChainsOnRemainingRoutes(); + } + + if (IncrementCurrentArcIndices()) return true; + + return IncrementRoute() && FindMostExpensiveChainsOnRemainingRoutes(); +} + +std::function +FilteredHeuristicExpensiveChainLNSOperator::SetupNextAccessorForNeighbor() { + const int first_arc_index = current_expensive_arc_indices_.first; + const int second_arc_index = current_expensive_arc_indices_.second; + DCHECK_LE(0, first_arc_index); + DCHECK_LT(first_arc_index, second_arc_index); + DCHECK_LT(second_arc_index, most_expensive_arc_starts_and_ranks_.size()); + + const std::pair& first_start_and_rank = + most_expensive_arc_starts_and_ranks_[first_arc_index]; + const std::pair& second_start_and_rank = + most_expensive_arc_starts_and_ranks_[second_arc_index]; + int64_t before_chain, after_chain; + if (first_start_and_rank.second < second_start_and_rank.second) { + before_chain = first_start_and_rank.first; + after_chain = OldValue(second_start_and_rank.first); + } else { + before_chain = second_start_and_rank.first; + after_chain = OldValue(first_start_and_rank.first); + } + + int node = Value(before_chain); + while (node != after_chain) { + removed_nodes_.Set(node); + node = Value(node); + } + + return [this, before_chain, after_chain](int64_t node) { + if (node == before_chain) return after_chain; + return OldValue(node); + }; +} + +bool FilteredHeuristicExpensiveChainLNSOperator::IncrementRoute() { + ++current_route_ %= model_->vehicles(); + return current_route_ != last_route_; +} + +bool FilteredHeuristicExpensiveChainLNSOperator::IncrementCurrentArcIndices() { + int& second_index = current_expensive_arc_indices_.second; + if (++second_index < most_expensive_arc_starts_and_ranks_.size()) { + return true; + } + int& first_index = current_expensive_arc_indices_.first; + if (first_index + 2 < most_expensive_arc_starts_and_ranks_.size()) { + first_index++; + second_index = first_index + 1; + return true; + } + return false; +} + +bool FilteredHeuristicExpensiveChainLNSOperator:: + FindMostExpensiveChainsOnRemainingRoutes() { + do { + if (FindMostExpensiveArcsOnRoute( + num_arcs_to_consider_, model_->Start(current_route_), + [this](int64_t i) { return OldValue(i); }, + [this](int64_t node) { return model_->IsEnd(node); }, + arc_cost_for_route_start_, &most_expensive_arc_starts_and_ranks_, + ¤t_expensive_arc_indices_)) { + return true; + } + } while (IncrementRoute()); + + return false; +} + +} // namespace operations_research diff --git a/ortools/constraint_solver/routing_insertion_lns.h b/ortools/constraint_solver/routing_insertion_lns.h new file mode 100644 index 00000000000..3f709c603f2 --- /dev/null +++ b/ortools/constraint_solver/routing_insertion_lns.h @@ -0,0 +1,247 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_INSERTION_LNS_H_ +#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_INSERTION_LNS_H_ + +#include +#include +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/constraint_solver/constraint_solveri.h" +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_search.h" +#include "ortools/util/bitset.h" + +namespace operations_research { + +/// Class of operators using a RoutingFilteredHeuristic to insert unperformed +/// nodes after changes have been made to the current solution. +// TODO(user): Put these methods in an object with helper methods instead +// of adding a layer to the class hierarchy. +class FilteredHeuristicLocalSearchOperator : public IntVarLocalSearchOperator { + public: + explicit FilteredHeuristicLocalSearchOperator( + std::unique_ptr heuristic, + bool keep_inverse_values = false); + ~FilteredHeuristicLocalSearchOperator() override {} + + protected: + virtual bool IncrementPosition() = 0; + /// Virtual method to return the next_accessor to be passed to the heuristic + /// to build a new solution. This method should also correctly set the + /// nodes being removed (if any) in removed_nodes_. + virtual std::function SetupNextAccessorForNeighbor() = 0; + + std::string HeuristicName() const { + std::string heuristic_name = heuristic_->DebugString(); + const int erase_pos = heuristic_name.find("FilteredHeuristic"); + if (erase_pos != std::string::npos) { + const int expected_name_size = heuristic_name.size() - 17; + heuristic_name.erase(erase_pos); + // NOTE: Verify that the "FilteredHeuristic" string was at the end of the + // heuristic name. + DCHECK_EQ(heuristic_name.size(), expected_name_size); + } + return heuristic_name; + } + + // TODO(user): Remove the dependency from RoutingModel by storing an + // IntVarFilteredHeuristic here instead and storing information on path + // start/ends like PathOperator does (instead of relying on the model). + RoutingModel* const model_; + /// Keeps track of removed nodes when making a neighbor. + SparseBitset<> removed_nodes_; + + private: + bool MakeOneNeighbor() override; + bool MakeChangesAndInsertNodes(); + + int64_t VehicleVarIndex(int64_t node) const { return model_->Size() + node; } + + const std::unique_ptr heuristic_; + const bool consider_vehicle_vars_; +}; + +/// LNS-like operator based on a filtered first solution heuristic to rebuild +/// the solution, after the destruction phase consisting of removing one route. +class FilteredHeuristicPathLNSOperator + : public FilteredHeuristicLocalSearchOperator { + public: + explicit FilteredHeuristicPathLNSOperator( + std::unique_ptr heuristic); + ~FilteredHeuristicPathLNSOperator() override {} + + std::string DebugString() const override { + return absl::StrCat("HeuristicPathLNS(", HeuristicName(), ")"); + } + + private: + void OnStart() override; + + bool IncrementPosition() override; + bool CurrentRouteIsEmpty() const; + void IncrementCurrentRouteToNextNonEmpty(); + + std::function SetupNextAccessorForNeighbor() override; + + int current_route_; + int last_route_; + bool just_started_; +}; + +/// Heuristic-based local search operator which relocates an entire route to +/// an empty vehicle of different vehicle class and then tries to insert +/// unperformed nodes using the heuristic. +class RelocatePathAndHeuristicInsertUnperformedOperator + : public FilteredHeuristicLocalSearchOperator { + public: + explicit RelocatePathAndHeuristicInsertUnperformedOperator( + std::unique_ptr heuristic); + ~RelocatePathAndHeuristicInsertUnperformedOperator() override {} + + std::string DebugString() const override { + return absl::StrCat("RelocatePathAndHeuristicInsertUnperformed(", + HeuristicName(), ")"); + } + + private: + void OnStart() override; + + bool IncrementPosition() override; + bool IncrementRoutes(); + + std::function SetupNextAccessorForNeighbor() override; + + int route_to_relocate_index_; + int last_route_to_relocate_index_; + int empty_route_index_; + int last_empty_route_index_; + std::vector routes_to_relocate_; + std::vector empty_routes_; + std::vector last_node_on_route_; + bool has_unperformed_nodes_; + bool just_started_; +}; + +/// Similar to the heuristic path LNS above, but instead of removing one route +/// entirely, the destruction phase consists of removing all nodes on an +/// "expensive" chain from a route. +class FilteredHeuristicExpensiveChainLNSOperator + : public FilteredHeuristicLocalSearchOperator { + public: + FilteredHeuristicExpensiveChainLNSOperator( + std::unique_ptr heuristic, + int num_arcs_to_consider, + std::function + arc_cost_for_route_start); + ~FilteredHeuristicExpensiveChainLNSOperator() override {} + + std::string DebugString() const override { + return absl::StrCat("HeuristicExpensiveChainLNS(", HeuristicName(), ")"); + } + + private: + void OnStart() override; + + bool IncrementPosition() override; + bool IncrementRoute(); + bool IncrementCurrentArcIndices(); + bool FindMostExpensiveChainsOnRemainingRoutes(); + + std::function SetupNextAccessorForNeighbor() override; + + int current_route_; + int last_route_; + + const int num_arcs_to_consider_; + std::vector> most_expensive_arc_starts_and_ranks_; + /// Indices in most_expensive_arc_starts_and_ranks_ corresponding to the first + /// and second arcs currently being considered for removal. + std::pair + current_expensive_arc_indices_; + std::function + arc_cost_for_route_start_; + + bool just_started_; +}; + +/// Filtered heuristic LNS operator, where the destruction phase consists of +/// removing a node and the 'num_close_nodes' nodes closest to it, along with +/// each of their corresponding sibling pickup/deliveries that are performed. +class FilteredHeuristicCloseNodesLNSOperator + : public FilteredHeuristicLocalSearchOperator { + public: + FilteredHeuristicCloseNodesLNSOperator( + std::unique_ptr heuristic, int num_close_nodes); + ~FilteredHeuristicCloseNodesLNSOperator() override {} + + std::string DebugString() const override { + return absl::StrCat("HeuristicCloseNodesLNS(", HeuristicName(), ")"); + } + + private: + void Initialize(); + + void OnStart() override; + + bool IncrementPosition() override; + + std::function SetupNextAccessorForNeighbor() override; + + void RemoveNode(int64_t node); + void RemoveNodeAndActiveSibling(int64_t node); + + bool IsActive(int64_t node) const { + DCHECK_LT(node, model_->Size()); + return Value(node) != node && !removed_nodes_[node]; + } + + int64_t Prev(int64_t node) const { + DCHECK_EQ(Value(InverseValue(node)), node); + DCHECK_LT(node, new_prevs_.size()); + return changed_prevs_[node] ? new_prevs_[node] : InverseValue(node); + } + int64_t Next(int64_t node) const { + DCHECK(!model_->IsEnd(node)); + return changed_nexts_[node] ? new_nexts_[node] : Value(node); + } + + std::vector GetActiveSiblings(int64_t node) const; + + const std::vector, std::vector>>& + pickup_delivery_pairs_; + + int current_node_; + int last_node_; + bool just_started_; + bool initialized_; + + std::vector> close_nodes_; + const int num_close_nodes_; + /// Keep track of changes when making a neighbor. + std::vector new_nexts_; + SparseBitset<> changed_nexts_; + std::vector new_prevs_; + SparseBitset<> changed_prevs_; +}; + +} // namespace operations_research + +#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_INSERTION_LNS_H_ diff --git a/ortools/constraint_solver/routing_neighborhoods.cc b/ortools/constraint_solver/routing_neighborhoods.cc index 87c5ad234a8..34f68c376be 100644 --- a/ortools/constraint_solver/routing_neighborhoods.cc +++ b/ortools/constraint_solver/routing_neighborhoods.cc @@ -16,22 +16,14 @@ #include #include #include -#include -#include -#include -#include #include #include -#include "ortools/base/int_type.h" #include "ortools/base/integral_types.h" -#include "ortools/base/logging.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_search.h" #include "ortools/constraint_solver/routing_types.h" -#include "ortools/util/bitset.h" +#include "ortools/constraint_solver/routing_utils.h" namespace operations_research { @@ -41,9 +33,11 @@ MakeRelocateNeighborsOperator::MakeRelocateNeighborsOperator( std::function start_empty_path_class, std::function&(int, int)> get_neighbors, RoutingTransitCallback2 arc_evaluator) - : PathOperator(vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, true, - false, std::move(start_empty_path_class), - std::move(get_neighbors)), + : PathOperator(vars, secondary_vars, + /*number_of_base_nodes=*/get_neighbors == nullptr ? 2 : 1, + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/false, + std::move(start_empty_path_class), std::move(get_neighbors)), arc_evaluator_(std::move(arc_evaluator)) {} bool MakeRelocateNeighborsOperator::MakeNeighbor() { @@ -404,8 +398,11 @@ GroupPairAndRelocateOperator::GroupPairAndRelocateOperator( std::function start_empty_path_class, std::function&(int, int)> get_neighbors, const RoutingIndexPairs& index_pairs) - : PathOperator(vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, true, - false, std::move(start_empty_path_class), + : PathOperator(vars, secondary_vars, + /*number_of_base_nodes=*/get_neighbors == nullptr ? 2 : 1, + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/false, + std::move(start_empty_path_class), std::move(get_neighbors)) { AddPairAlternativeSets(index_pairs); } @@ -433,9 +430,11 @@ LightPairRelocateOperator::LightPairRelocateOperator( std::function&(int, int)> get_neighbors, const RoutingIndexPairs& index_pairs, std::function force_lifo) - : PathOperator(vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, true, - false, std::move(start_empty_path_class), - std::move(get_neighbors)), + : PathOperator(vars, secondary_vars, + /*number_of_base_nodes=*/get_neighbors == nullptr ? 2 : 1, + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/false, + std::move(start_empty_path_class), std::move(get_neighbors)), force_lifo_(std::move(force_lifo)) { AddPairAlternativeSets(index_pairs); } @@ -517,8 +516,11 @@ PairExchangeOperator::PairExchangeOperator( std::function start_empty_path_class, std::function&(int, int)> get_neighbors, const RoutingIndexPairs& index_pairs) - : PathOperator(vars, secondary_vars, get_neighbors == nullptr ? 2 : 1, true, - true, std::move(start_empty_path_class), + : PathOperator(vars, secondary_vars, + /*number_of_base_nodes=*/get_neighbors == nullptr ? 2 : 1, + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/true, + std::move(start_empty_path_class), std::move(get_neighbors)) { AddPairAlternativeSets(index_pairs); } @@ -529,7 +531,14 @@ bool PairExchangeOperator::MakeNeighbor() { if (!GetPreviousAndSibling(node1, &prev1, &sibling1, &sibling_prev1)) { return false; } - const int64_t node2 = BaseNode(1); + int64_t node2 = -1; + if (!HasNeighbors()) { + node2 = BaseNode(1); + } else { + const int64_t neighbor = GetNeighborForBaseNode(0); + if (IsInactive(neighbor) || IsPathStart(neighbor)) return false; + node2 = Prev(neighbor); + } int64_t prev2, sibling2, sibling_prev2 = -1; if (!GetPreviousAndSibling(node2, &prev2, &sibling2, &sibling_prev2)) { return false; @@ -570,8 +579,11 @@ bool PairExchangeOperator::MakeNeighbor() { // Swapping alternatives in. SwapActiveAndInactive(sibling1, BaseSiblingAlternativeNode(0)); SwapActiveAndInactive(node1, BaseAlternativeNode(0)); - SwapActiveAndInactive(sibling2, BaseSiblingAlternativeNode(1)); - SwapActiveAndInactive(node2, BaseAlternativeNode(1)); + if (!HasNeighbors()) { + // TODO(user): Support alternatives with neighbors. + SwapActiveAndInactive(sibling2, BaseSiblingAlternativeNode(1)); + SwapActiveAndInactive(node2, BaseAlternativeNode(1)); + } return status; } @@ -725,7 +737,6 @@ bool PairExchangeRelocateOperator::GetPreviousAndSibling( SwapIndexPairOperator::SwapIndexPairOperator( const std::vector& vars, const std::vector& path_vars, - std::function start_empty_path_class, const RoutingIndexPairs& index_pairs) : IntVarLocalSearchOperator(vars), index_pairs_(index_pairs), @@ -880,584 +891,6 @@ void IndexPairSwapActiveOperator::OnNodeInitialization() { inactive_node_ = Size(); } -// FilteredHeuristicLocalSearchOperator - -FilteredHeuristicLocalSearchOperator::FilteredHeuristicLocalSearchOperator( - std::unique_ptr heuristic, - bool keep_inverse_values) - : IntVarLocalSearchOperator(heuristic->model()->Nexts(), - keep_inverse_values), - model_(heuristic->model()), - removed_nodes_(model_->Size()), - heuristic_(std::move(heuristic)), - consider_vehicle_vars_(!model_->CostsAreHomogeneousAcrossVehicles()) { - if (consider_vehicle_vars_) { - AddVars(model_->VehicleVars()); - } -} - -bool FilteredHeuristicLocalSearchOperator::MakeOneNeighbor() { - while (IncrementPosition()) { - if (model_->CheckLimit()) { - // NOTE: Even though the limit is checked in the BuildSolutionFromRoutes() - // method of the heuristics, we still check it here to avoid calling - // IncrementPosition() and building a solution for every possible position - // if the time limit is reached. - return false; - } - // NOTE: No need to call RevertChanges() here as MakeChangeAndInsertNodes() - // will always return true if any change was made. - if (MakeChangesAndInsertNodes()) { - return true; - } - } - return false; -} - -bool FilteredHeuristicLocalSearchOperator::MakeChangesAndInsertNodes() { - removed_nodes_.SparseClearAll(); - - const std::function next_accessor = - SetupNextAccessorForNeighbor(); - if (next_accessor == nullptr) { - return false; - } - const Assignment* const result_assignment = - heuristic_->BuildSolutionFromRoutes(next_accessor); - - if (result_assignment == nullptr) { - return false; - } - - bool has_change = false; - const std::vector& elements = - result_assignment->IntVarContainer().elements(); - for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { - int64_t node_index = model_->Start(vehicle); - while (!model_->IsEnd(node_index)) { - // NOTE: When building the solution in the heuristic, Next vars are added - // to the assignment at the position corresponding to their index. - const IntVarElement& node_element = elements[node_index]; - DCHECK_EQ(node_element.Var(), model_->NextVar(node_index)); - - const int64_t new_node_value = node_element.Value(); - DCHECK_NE(new_node_value, node_index); - - const int64_t vehicle_var_index = VehicleVarIndex(node_index); - if (OldValue(node_index) != new_node_value || - (consider_vehicle_vars_ && OldValue(vehicle_var_index) != vehicle)) { - has_change = true; - SetValue(node_index, new_node_value); - if (consider_vehicle_vars_) { - SetValue(vehicle_var_index, vehicle); - } - } - node_index = new_node_value; - } - } - // Check for newly unperformed nodes among the ones removed for insertion by - // the heuristic. - for (int64_t node : removed_nodes_.PositionsSetAtLeastOnce()) { - const IntVarElement& node_element = elements[node]; - DCHECK_EQ(node_element.Var(), model_->NextVar(node)); - if (node_element.Value() == node) { - DCHECK_NE(OldValue(node), node); - has_change = true; - SetValue(node, node); - if (consider_vehicle_vars_) { - const int64_t vehicle_var_index = VehicleVarIndex(node); - DCHECK_NE(OldValue(vehicle_var_index), -1); - SetValue(vehicle_var_index, -1); - } - } - } - return has_change; -} - -// FilteredHeuristicPathLNSOperator - -FilteredHeuristicPathLNSOperator::FilteredHeuristicPathLNSOperator( - std::unique_ptr heuristic) - : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), - current_route_(0), - last_route_(0), - just_started_(false) {} - -void FilteredHeuristicPathLNSOperator::OnStart() { - // NOTE: We set last_route_ to current_route_ here to make sure all routes - // are scanned in IncrementCurrentRouteToNextNonEmpty(). - last_route_ = current_route_; - if (CurrentRouteIsEmpty()) { - IncrementCurrentRouteToNextNonEmpty(); - } - just_started_ = true; -} - -bool FilteredHeuristicPathLNSOperator::IncrementPosition() { - if (just_started_) { - just_started_ = false; - return !CurrentRouteIsEmpty(); - } - IncrementCurrentRouteToNextNonEmpty(); - return current_route_ != last_route_; -} - -bool FilteredHeuristicPathLNSOperator::CurrentRouteIsEmpty() const { - return model_->IsEnd(OldValue(model_->Start(current_route_))); -} - -void FilteredHeuristicPathLNSOperator::IncrementCurrentRouteToNextNonEmpty() { - const int num_routes = model_->vehicles(); - do { - ++current_route_ %= num_routes; - if (current_route_ == last_route_) { - // All routes have been scanned. - return; - } - } while (CurrentRouteIsEmpty()); -} - -std::function -FilteredHeuristicPathLNSOperator::SetupNextAccessorForNeighbor() { - const int64_t start_node = model_->Start(current_route_); - const int64_t end_node = model_->End(current_route_); - - int64_t node = Value(start_node); - while (node != end_node) { - removed_nodes_.Set(node); - node = Value(node); - } - - return [this, start_node, end_node](int64_t node) { - if (node == start_node) return end_node; - return Value(node); - }; -} - -// RelocatePathAndHeuristicInsertUnperformedOperator - -RelocatePathAndHeuristicInsertUnperformedOperator:: - RelocatePathAndHeuristicInsertUnperformedOperator( - std::unique_ptr heuristic) - : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), - route_to_relocate_index_(0), - empty_route_index_(0), - just_started_(false) {} - -void RelocatePathAndHeuristicInsertUnperformedOperator::OnStart() { - has_unperformed_nodes_ = false; - last_node_on_route_.resize(model_->vehicles()); - routes_to_relocate_.clear(); - empty_routes_.clear(); - std::vector empty_vehicle_of_vehicle_class_added( - model_->GetVehicleClassesCount(), false); - for (int64_t node = 0; node < model_->Size(); node++) { - const int64_t next = OldValue(node); - if (next == node) { - has_unperformed_nodes_ = true; - continue; - } - if (model_->IsEnd(next)) { - last_node_on_route_[model_->VehicleIndex(next)] = node; - } - } - - for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { - const int64_t next = OldValue(model_->Start(vehicle)); - if (!model_->IsEnd(next)) { - routes_to_relocate_.push_back(vehicle); - continue; - } - const int vehicle_class = - model_->GetVehicleClassIndexOfVehicle(vehicle).value(); - if (!empty_vehicle_of_vehicle_class_added[vehicle_class]) { - empty_routes_.push_back(vehicle); - empty_vehicle_of_vehicle_class_added[vehicle_class] = true; - } - } - - if (empty_route_index_ >= empty_routes_.size()) { - empty_route_index_ = 0; - } - if (route_to_relocate_index_ >= routes_to_relocate_.size()) { - route_to_relocate_index_ = 0; - } - last_empty_route_index_ = empty_route_index_; - last_route_to_relocate_index_ = route_to_relocate_index_; - - just_started_ = true; -} - -bool RelocatePathAndHeuristicInsertUnperformedOperator::IncrementPosition() { - if (!has_unperformed_nodes_ || empty_routes_.empty() || - routes_to_relocate_.empty()) { - return false; - } - if (just_started_) { - just_started_ = false; - return true; - } - return IncrementRoutes(); -} - -bool RelocatePathAndHeuristicInsertUnperformedOperator::IncrementRoutes() { - ++empty_route_index_ %= empty_routes_.size(); - if (empty_route_index_ != last_empty_route_index_) { - return true; - } - ++route_to_relocate_index_ %= routes_to_relocate_.size(); - return route_to_relocate_index_ != last_route_to_relocate_index_; -} - -std::function -RelocatePathAndHeuristicInsertUnperformedOperator:: - SetupNextAccessorForNeighbor() { - const int empty_route = empty_routes_[empty_route_index_]; - const int relocated_route = routes_to_relocate_[route_to_relocate_index_]; - if (model_->GetVehicleClassIndexOfVehicle(empty_route) == - model_->GetVehicleClassIndexOfVehicle(relocated_route)) { - // Don't try to relocate the route to an empty vehicle of the same class. - return nullptr; - } - - const int64_t empty_start_node = model_->Start(empty_route); - const int64_t empty_end_node = model_->End(empty_route); - - const int64_t relocated_route_start = model_->Start(relocated_route); - const int64_t first_relocated_node = OldValue(relocated_route_start); - const int64_t last_relocated_node = last_node_on_route_[relocated_route]; - const int64_t relocated_route_end = model_->End(relocated_route); - - return [this, empty_start_node, empty_end_node, first_relocated_node, - last_relocated_node, relocated_route_start, - relocated_route_end](int64_t node) { - if (node == relocated_route_start) return relocated_route_end; - if (node == empty_start_node) return first_relocated_node; - if (node == last_relocated_node) return empty_end_node; - return Value(node); - }; -} - -// FilteredHeuristicCloseNodesLNSOperator - -FilteredHeuristicCloseNodesLNSOperator::FilteredHeuristicCloseNodesLNSOperator( - std::unique_ptr heuristic, int num_close_nodes) - : FilteredHeuristicLocalSearchOperator(std::move(heuristic), - /*keep_inverse_values*/ true), - pickup_delivery_pairs_(model_->GetPickupAndDeliveryPairs()), - current_node_(0), - last_node_(0), - just_started_(false), - initialized_(false), - close_nodes_(model_->Size()), - num_close_nodes_(num_close_nodes), - new_nexts_(model_->Size()), - changed_nexts_(model_->Size()), - new_prevs_(model_->Size()), - changed_prevs_(model_->Size()) {} - -void FilteredHeuristicCloseNodesLNSOperator::Initialize() { - if (initialized_) return; - initialized_ = true; - const int64_t size = model_->Size(); - const int64_t max_num_neighbors = - std::max(0, size - 1 - model_->vehicles()); - const int64_t num_closest_neighbors = - std::min(num_close_nodes_, max_num_neighbors); - DCHECK_GE(num_closest_neighbors, 0); - - if (num_closest_neighbors == 0) return; - - const int64_t num_cost_classes = model_->GetCostClassesCount(); - - for (int64_t node = 0; node < size; node++) { - if (model_->IsStart(node) || model_->IsEnd(node)) continue; - - std::vector> - costed_after_nodes; - costed_after_nodes.reserve(size); - for (int64_t after_node = 0; after_node < size; after_node++) { - if (model_->IsStart(after_node) || model_->IsEnd(after_node) || - after_node == node) { - continue; - } - double total_cost = 0.0; - // NOTE: We don't consider the 'always-zero' cost class when searching for - // closest neighbors. - for (int cost_class = 1; cost_class < num_cost_classes; cost_class++) { - total_cost += model_->GetArcCostForClass(node, after_node, cost_class); - } - costed_after_nodes.emplace_back(total_cost, after_node); - } - - std::nth_element(costed_after_nodes.begin(), - costed_after_nodes.begin() + num_closest_neighbors - 1, - costed_after_nodes.end()); - std::vector& neighbors = close_nodes_[node]; - neighbors.reserve(num_closest_neighbors); - for (int index = 0; index < num_closest_neighbors; index++) { - neighbors.push_back(costed_after_nodes[index].second); - } - } -} - -void FilteredHeuristicCloseNodesLNSOperator::OnStart() { - Initialize(); - last_node_ = current_node_; - just_started_ = true; -} - -bool FilteredHeuristicCloseNodesLNSOperator::IncrementPosition() { - DCHECK(initialized_); - if (just_started_) { - just_started_ = false; - return true; - } - ++current_node_ %= model_->Size(); - return current_node_ != last_node_; -} - -void FilteredHeuristicCloseNodesLNSOperator::RemoveNode(int64_t node) { - DCHECK(!model_->IsEnd(node) && !model_->IsStart(node)); - DCHECK_NE(Value(node), node); - DCHECK(IsActive(node)); - - removed_nodes_.Set(node); - const int64_t prev = Prev(node); - const int64_t next = Next(node); - changed_nexts_.Set(prev); - new_nexts_[prev] = next; - if (next < model_->Size()) { - changed_prevs_.Set(next); - new_prevs_[next] = prev; - } -} - -void FilteredHeuristicCloseNodesLNSOperator::RemoveNodeAndActiveSibling( - int64_t node) { - if (!IsActive(node)) return; - RemoveNode(node); - - for (int64_t sibling_node : GetActiveSiblings(node)) { - if (!model_->IsStart(sibling_node) && !model_->IsEnd(sibling_node)) { - RemoveNode(sibling_node); - } - } -} - -std::vector FilteredHeuristicCloseNodesLNSOperator::GetActiveSiblings( - int64_t node) const { - // NOTE: In most use-cases, where each node is a pickup or delivery in a - // single index pair, this function is in O(k) where k is the number of - // alternative deliveries or pickups for this index pair. - std::vector active_siblings; - for (std::pair index_pair : - model_->GetPickupIndexPairs(node)) { - for (int64_t sibling_delivery : - pickup_delivery_pairs_[index_pair.first].second) { - if (IsActive(sibling_delivery)) { - active_siblings.push_back(sibling_delivery); - break; - } - } - } - for (std::pair index_pair : - model_->GetDeliveryIndexPairs(node)) { - for (int64_t sibling_pickup : - pickup_delivery_pairs_[index_pair.first].first) { - if (IsActive(sibling_pickup)) { - active_siblings.push_back(sibling_pickup); - break; - } - } - } - return active_siblings; -} - -std::function -FilteredHeuristicCloseNodesLNSOperator::SetupNextAccessorForNeighbor() { - DCHECK(initialized_); - if (model_->IsStart(current_node_)) { - return nullptr; - } - DCHECK(!model_->IsEnd(current_node_)); - - changed_nexts_.SparseClearAll(); - changed_prevs_.SparseClearAll(); - - RemoveNodeAndActiveSibling(current_node_); - - for (int64_t neighbor : close_nodes_[current_node_]) { - RemoveNodeAndActiveSibling(neighbor); - } - - return [this](int64_t node) { return Next(node); }; -} - -// FilteredHeuristicExpensiveChainLNSOperator - -FilteredHeuristicExpensiveChainLNSOperator:: - FilteredHeuristicExpensiveChainLNSOperator( - std::unique_ptr heuristic, - int num_arcs_to_consider, - std::function - arc_cost_for_route_start) - : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), - current_route_(0), - last_route_(0), - num_arcs_to_consider_(num_arcs_to_consider), - current_expensive_arc_indices_({-1, -1}), - arc_cost_for_route_start_(std::move(arc_cost_for_route_start)), - just_started_(false) { - DCHECK_GE(num_arcs_to_consider_, 2); -} - -void FilteredHeuristicExpensiveChainLNSOperator::OnStart() { - last_route_ = current_route_; - just_started_ = true; -} - -bool FilteredHeuristicExpensiveChainLNSOperator::IncrementPosition() { - if (just_started_) { - just_started_ = false; - return FindMostExpensiveChainsOnRemainingRoutes(); - } - - if (IncrementCurrentArcIndices()) return true; - - return IncrementRoute() && FindMostExpensiveChainsOnRemainingRoutes(); -} - -std::function -FilteredHeuristicExpensiveChainLNSOperator::SetupNextAccessorForNeighbor() { - const int first_arc_index = current_expensive_arc_indices_.first; - const int second_arc_index = current_expensive_arc_indices_.second; - DCHECK_LE(0, first_arc_index); - DCHECK_LT(first_arc_index, second_arc_index); - DCHECK_LT(second_arc_index, most_expensive_arc_starts_and_ranks_.size()); - - const std::pair& first_start_and_rank = - most_expensive_arc_starts_and_ranks_[first_arc_index]; - const std::pair& second_start_and_rank = - most_expensive_arc_starts_and_ranks_[second_arc_index]; - int64_t before_chain, after_chain; - if (first_start_and_rank.second < second_start_and_rank.second) { - before_chain = first_start_and_rank.first; - after_chain = OldValue(second_start_and_rank.first); - } else { - before_chain = second_start_and_rank.first; - after_chain = OldValue(first_start_and_rank.first); - } - - int node = Value(before_chain); - while (node != after_chain) { - removed_nodes_.Set(node); - node = Value(node); - } - - return [this, before_chain, after_chain](int64_t node) { - if (node == before_chain) return after_chain; - return OldValue(node); - }; -} - -bool FilteredHeuristicExpensiveChainLNSOperator::IncrementRoute() { - ++current_route_ %= model_->vehicles(); - return current_route_ != last_route_; -} - -bool FilteredHeuristicExpensiveChainLNSOperator::IncrementCurrentArcIndices() { - int& second_index = current_expensive_arc_indices_.second; - if (++second_index < most_expensive_arc_starts_and_ranks_.size()) { - return true; - } - int& first_index = current_expensive_arc_indices_.first; - if (first_index + 2 < most_expensive_arc_starts_and_ranks_.size()) { - first_index++; - second_index = first_index + 1; - return true; - } - return false; -} - -namespace { - -// Returns false if the route starting with 'start' is empty. Otherwise sets -// most_expensive_arc_starts_and_ranks and first_expensive_arc_indices according -// to the most expensive chains on the route, and returns true. -bool FindMostExpensiveArcsOnRoute( - int num_arcs, int64_t start, - const std::function& next_accessor, - const std::function& is_end, - const std::function& - arc_cost_for_route_start, - std::vector>* most_expensive_arc_starts_and_ranks, - std::pair* first_expensive_arc_indices) { - if (is_end(next_accessor(start))) { - // Empty route. - *first_expensive_arc_indices = {-1, -1}; - return false; - } - - // NOTE: The negative ranks are so that for a given cost, lower ranks are - // given higher priority. - using ArcCostNegativeRankStart = std::tuple; - std::priority_queue, - std::greater> - arc_info_pq; - - int64_t before_node = start; - int rank = 0; - while (!is_end(before_node)) { - const int64_t after_node = next_accessor(before_node); - const int64_t arc_cost = - arc_cost_for_route_start(before_node, after_node, start); - arc_info_pq.emplace(arc_cost, -rank, before_node); - - before_node = after_node; - rank++; - - if (rank > num_arcs) { - arc_info_pq.pop(); - } - } - - DCHECK_GE(rank, 2); - DCHECK_EQ(arc_info_pq.size(), std::min(rank, num_arcs)); - - most_expensive_arc_starts_and_ranks->resize(arc_info_pq.size()); - int arc_index = arc_info_pq.size() - 1; - while (!arc_info_pq.empty()) { - const ArcCostNegativeRankStart& arc_info = arc_info_pq.top(); - (*most_expensive_arc_starts_and_ranks)[arc_index] = { - std::get<2>(arc_info), -std::get<1>(arc_info)}; - arc_index--; - arc_info_pq.pop(); - } - - *first_expensive_arc_indices = {0, 1}; - return true; -} - -} // namespace - -bool FilteredHeuristicExpensiveChainLNSOperator:: - FindMostExpensiveChainsOnRemainingRoutes() { - do { - if (FindMostExpensiveArcsOnRoute( - num_arcs_to_consider_, model_->Start(current_route_), - [this](int64_t i) { return OldValue(i); }, - [this](int64_t node) { return model_->IsEnd(node); }, - arc_cost_for_route_start_, &most_expensive_arc_starts_and_ranks_, - ¤t_expensive_arc_indices_)) { - return true; - } - } while (IncrementRoute()); - - return false; -} - RelocateExpensiveChain::RelocateExpensiveChain( const std::vector& vars, const std::vector& secondary_vars, @@ -1497,7 +930,6 @@ bool RelocateExpensiveChain::MakeNeighbor() { MoveChain(second_start_and_rank.first, first_start_and_rank.first, BaseNode(0)); } - bool RelocateExpensiveChain::MakeOneNeighbor() { while (has_non_empty_paths_to_explore_) { if (!PathOperator::MakeOneNeighbor()) { @@ -1570,10 +1002,11 @@ RelocateSubtrip::RelocateSubtrip( std::function start_empty_path_class, std::function&(int, int)> get_neighbors, const RoutingIndexPairs& pairs) - : PathOperator(vars, secondary_vars, - /*number_of_base_nodes*/ get_neighbors == nullptr ? 2 : 1, - true, false, std::move(start_empty_path_class), - std::move(get_neighbors)) { + : PathOperator( + vars, secondary_vars, + /*number_of_base_nodes=*/get_neighbors == nullptr ? 2 : 1, + /*skip_locally_optimal_paths=*/true, /*accept_path_end_base=*/false, + std::move(start_empty_path_class), std::move(get_neighbors)) { is_pickup_node_.resize(number_of_nexts_, false); is_delivery_node_.resize(number_of_nexts_, false); pair_of_node_.resize(number_of_nexts_, -1); @@ -1715,10 +1148,11 @@ ExchangeSubtrip::ExchangeSubtrip( std::function start_empty_path_class, std::function&(int, int)> get_neighbors, const RoutingIndexPairs& pairs) - : PathOperator(vars, secondary_vars, - /*number_of_base_nodes*/ get_neighbors == nullptr ? 2 : 1, - true, false, std::move(start_empty_path_class), - std::move(get_neighbors)) { + : PathOperator( + vars, secondary_vars, + /*number_of_base_nodes=*/get_neighbors == nullptr ? 2 : 1, + /*skip_locally_optimal_paths=*/true, /*accept_path_end_base=*/false, + std::move(start_empty_path_class), std::move(get_neighbors)) { is_pickup_node_.resize(number_of_nexts_, false); is_delivery_node_.resize(number_of_nexts_, false); pair_of_node_.resize(number_of_nexts_, -1); diff --git a/ortools/constraint_solver/routing_neighborhoods.h b/ortools/constraint_solver/routing_neighborhoods.h index 094a3af4248..9c1efee06a9 100644 --- a/ortools/constraint_solver/routing_neighborhoods.h +++ b/ortools/constraint_solver/routing_neighborhoods.h @@ -16,20 +16,13 @@ #include #include -#include #include #include #include -#include "absl/strings/str_cat.h" -#include "ortools/base/integral_types.h" -#include "ortools/base/logging.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_search.h" #include "ortools/constraint_solver/routing_types.h" -#include "ortools/util/bitset.h" namespace operations_research { @@ -175,7 +168,7 @@ class MakePairActiveOperator : public PathOperator { protected: bool MakeOneNeighbor() override; - bool OnSamePathAsPreviousBase(int64_t base_index) override { + bool OnSamePathAsPreviousBase(int64_t /*base_index*/) override { /// Both base nodes have to be on the same path since they represent the /// nodes after which unactive node pairs will be moved. return true; @@ -336,7 +329,9 @@ class PairExchangeOperator : public PathOperator { private: bool RestartAtPathStartOnSynchronize() override { return true; } - bool ConsiderAlternatives(int64_t base_index) const override { return true; } + bool ConsiderAlternatives(int64_t /*base_index*/) const override { + return true; + } bool GetPreviousAndSibling(int64_t node, int64_t* previous, int64_t* sibling, int64_t* sibling_previous) const; }; @@ -403,7 +398,6 @@ class SwapIndexPairOperator : public IntVarLocalSearchOperator { public: SwapIndexPairOperator(const std::vector& vars, const std::vector& path_vars, - std::function start_empty_path_class, const RoutingIndexPairs& index_pairs); ~SwapIndexPairOperator() override {} @@ -459,218 +453,6 @@ class IndexPairSwapActiveOperator : public PathOperator { int inactive_node_; }; -/// Class of operators using a RoutingFilteredHeuristic to insert unperformed -/// nodes after changes have been made to the current solution. -// TODO(user): Put these methods in an object with helper methods instead -// of adding a layer to the class hierarchy. -class FilteredHeuristicLocalSearchOperator : public IntVarLocalSearchOperator { - public: - explicit FilteredHeuristicLocalSearchOperator( - std::unique_ptr heuristic, - bool keep_inverse_values = false); - ~FilteredHeuristicLocalSearchOperator() override {} - - protected: - virtual bool IncrementPosition() = 0; - /// Virtual method to return the next_accessor to be passed to the heuristic - /// to build a new solution. This method should also correctly set the - /// nodes being removed (if any) in removed_nodes_. - virtual std::function SetupNextAccessorForNeighbor() = 0; - - std::string HeuristicName() const { - std::string heuristic_name = heuristic_->DebugString(); - const int erase_pos = heuristic_name.find("FilteredHeuristic"); - if (erase_pos != std::string::npos) { - const int expected_name_size = heuristic_name.size() - 17; - heuristic_name.erase(erase_pos); - // NOTE: Verify that the "FilteredHeuristic" string was at the end of the - // heuristic name. - DCHECK_EQ(heuristic_name.size(), expected_name_size); - } - return heuristic_name; - } - - // TODO(user): Remove the dependency from RoutingModel by storing an - // IntVarFilteredHeuristic here instead and storing information on path - // start/ends like PathOperator does (instead of relying on the model). - RoutingModel* const model_; - /// Keeps track of removed nodes when making a neighbor. - SparseBitset<> removed_nodes_; - - private: - bool MakeOneNeighbor() override; - bool MakeChangesAndInsertNodes(); - - int64_t VehicleVarIndex(int64_t node) const { return model_->Size() + node; } - - const std::unique_ptr heuristic_; - const bool consider_vehicle_vars_; -}; - -/// LNS-like operator based on a filtered first solution heuristic to rebuild -/// the solution, after the destruction phase consisting of removing one route. -class FilteredHeuristicPathLNSOperator - : public FilteredHeuristicLocalSearchOperator { - public: - explicit FilteredHeuristicPathLNSOperator( - std::unique_ptr heuristic); - ~FilteredHeuristicPathLNSOperator() override {} - - std::string DebugString() const override { - return absl::StrCat("HeuristicPathLNS(", HeuristicName(), ")"); - } - - private: - void OnStart() override; - - bool IncrementPosition() override; - bool CurrentRouteIsEmpty() const; - void IncrementCurrentRouteToNextNonEmpty(); - - std::function SetupNextAccessorForNeighbor() override; - - int current_route_; - int last_route_; - bool just_started_; -}; - -/// Heuristic-based local search operator which relocates an entire route to -/// an empty vehicle of different vehicle class and then tries to insert -/// unperformed nodes using the heuristic. -class RelocatePathAndHeuristicInsertUnperformedOperator - : public FilteredHeuristicLocalSearchOperator { - public: - explicit RelocatePathAndHeuristicInsertUnperformedOperator( - std::unique_ptr heuristic); - ~RelocatePathAndHeuristicInsertUnperformedOperator() override {} - - std::string DebugString() const override { - return absl::StrCat("RelocatePathAndHeuristicInsertUnperformed(", - HeuristicName(), ")"); - } - - private: - void OnStart() override; - - bool IncrementPosition() override; - bool IncrementRoutes(); - - std::function SetupNextAccessorForNeighbor() override; - - int route_to_relocate_index_; - int last_route_to_relocate_index_; - int empty_route_index_; - int last_empty_route_index_; - std::vector routes_to_relocate_; - std::vector empty_routes_; - std::vector last_node_on_route_; - bool has_unperformed_nodes_; - bool just_started_; -}; - -/// Similar to the heuristic path LNS above, but instead of removing one route -/// entirely, the destruction phase consists of removing all nodes on an -/// "expensive" chain from a route. -class FilteredHeuristicExpensiveChainLNSOperator - : public FilteredHeuristicLocalSearchOperator { - public: - FilteredHeuristicExpensiveChainLNSOperator( - std::unique_ptr heuristic, - int num_arcs_to_consider, - std::function - arc_cost_for_route_start); - ~FilteredHeuristicExpensiveChainLNSOperator() override {} - - std::string DebugString() const override { - return absl::StrCat("HeuristicExpensiveChainLNS(", HeuristicName(), ")"); - } - - private: - void OnStart() override; - - bool IncrementPosition() override; - bool IncrementRoute(); - bool IncrementCurrentArcIndices(); - bool FindMostExpensiveChainsOnRemainingRoutes(); - - std::function SetupNextAccessorForNeighbor() override; - - int current_route_; - int last_route_; - - const int num_arcs_to_consider_; - std::vector> most_expensive_arc_starts_and_ranks_; - /// Indices in most_expensive_arc_starts_and_ranks_ corresponding to the first - /// and second arcs currently being considered for removal. - std::pair - current_expensive_arc_indices_; - std::function - arc_cost_for_route_start_; - - bool just_started_; -}; - -/// Filtered heuristic LNS operator, where the destruction phase consists of -/// removing a node and the 'num_close_nodes' nodes closest to it, along with -/// each of their corresponding sibling pickup/deliveries that are performed. -class FilteredHeuristicCloseNodesLNSOperator - : public FilteredHeuristicLocalSearchOperator { - public: - FilteredHeuristicCloseNodesLNSOperator( - std::unique_ptr heuristic, int num_close_nodes); - ~FilteredHeuristicCloseNodesLNSOperator() override {} - - std::string DebugString() const override { - return absl::StrCat("HeuristicCloseNodesLNS(", HeuristicName(), ")"); - } - - private: - void Initialize(); - - void OnStart() override; - - bool IncrementPosition() override; - - std::function SetupNextAccessorForNeighbor() override; - - void RemoveNode(int64_t node); - void RemoveNodeAndActiveSibling(int64_t node); - - bool IsActive(int64_t node) const { - DCHECK_LT(node, model_->Size()); - return Value(node) != node && !removed_nodes_[node]; - } - - int64_t Prev(int64_t node) const { - DCHECK_EQ(Value(InverseValue(node)), node); - DCHECK_LT(node, new_prevs_.size()); - return changed_prevs_[node] ? new_prevs_[node] : InverseValue(node); - } - int64_t Next(int64_t node) const { - DCHECK(!model_->IsEnd(node)); - return changed_nexts_[node] ? new_nexts_[node] : Value(node); - } - - std::vector GetActiveSiblings(int64_t node) const; - - const std::vector, std::vector>>& - pickup_delivery_pairs_; - - int current_node_; - int last_node_; - bool just_started_; - bool initialized_; - - std::vector> close_nodes_; - const int num_close_nodes_; - /// Keep track of changes when making a neighbor. - std::vector new_nexts_; - SparseBitset<> changed_nexts_; - std::vector new_prevs_; - SparseBitset<> changed_prevs_; -}; - /// RelocateExpensiveChain /// /// Operator which relocates the most expensive subchains (given a cost @@ -740,7 +522,7 @@ class PairNodeSwapActiveOperator : public PathOperator { } protected: - bool OnSamePathAsPreviousBase(int64_t base_index) override { + bool OnSamePathAsPreviousBase(int64_t /*base_index*/) override { /// Both base nodes have to be on the same path since they represent the /// nodes after which unactive node pairs will be moved. return true; diff --git a/ortools/constraint_solver/routing_search.cc b/ortools/constraint_solver/routing_search.cc index fee9c2d4e65..0134b05877e 100644 --- a/ortools/constraint_solver/routing_search.cc +++ b/ortools/constraint_solver/routing_search.cc @@ -54,6 +54,7 @@ #include "ortools/constraint_solver/routing.h" #include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/constraint_solver/routing_types.h" +#include "ortools/constraint_solver/routing_utils.h" #include "ortools/graph/christofides.h" #include "ortools/util/bitset.h" #include "ortools/util/range_query_function.h" @@ -278,7 +279,6 @@ IntVarFilteredHeuristic::IntVarFilteredHeuristic( vars_(vars), base_vars_size_(vars.size()), delta_(solver->MakeAssignment()), - is_in_delta_(vars_.size(), false), empty_(solver->MakeAssignment()), filter_manager_(filter_manager), objective_upper_bound_(std::numeric_limits::max()), @@ -288,6 +288,7 @@ IntVarFilteredHeuristic::IntVarFilteredHeuristic( vars_.insert(vars_.end(), secondary_vars.begin(), secondary_vars.end()); } assignment_->MutableIntVarContainer()->Resize(vars_.size()); + is_in_delta_.resize(vars_.size(), false); delta_indices_.reserve(vars_.size()); } @@ -412,13 +413,12 @@ bool IntVarFilteredHeuristic::FilterAccept() { RoutingFilteredHeuristic::RoutingFilteredHeuristic( RoutingModel* model, std::function stop_search, - LocalSearchFilterManager* filter_manager, bool omit_secondary_vars) - : IntVarFilteredHeuristic( - model->solver(), model->Nexts(), - omit_secondary_vars || model->CostsAreHomogeneousAcrossVehicles() - ? std::vector() - : model->VehicleVars(), - filter_manager), + LocalSearchFilterManager* filter_manager) + : IntVarFilteredHeuristic(model->solver(), model->Nexts(), + model->CostsAreHomogeneousAcrossVehicles() + ? std::vector() + : model->VehicleVars(), + filter_manager), model_(model), stop_search_(std::move(stop_search)) {} @@ -485,6 +485,8 @@ void RoutingFilteredHeuristic::MakeDisjunctionNodesUnperformed(int64_t node) { } bool RoutingFilteredHeuristic::MakeUnassignedNodesUnperformed() { + // TODO(user): check that delta_ is empty. + SynchronizeFilters(); for (int index = 0; index < model_->Size(); ++index) { DCHECK(!IsSecondaryVar(index)); if (!Contains(index)) { @@ -498,7 +500,8 @@ bool RoutingFilteredHeuristic::MakeUnassignedNodesUnperformed() { } void RoutingFilteredHeuristic::MakePartiallyPerformedPairsUnperformed() { - std::vector to_make_unperformed(Size(), false); + const int num_nexts = model()->Nexts().size(); + std::vector to_make_unperformed(num_nexts, false); for (const auto& [pickups, deliveries] : model()->GetPickupAndDeliveryPairs()) { int64_t performed_pickup = -1; @@ -524,10 +527,10 @@ void RoutingFilteredHeuristic::MakePartiallyPerformedPairsUnperformed() { } } } - for (int index = 0; index < Size(); ++index) { + for (int index = 0; index < num_nexts; ++index) { if (to_make_unperformed[index] || !Contains(index)) continue; int64_t next = Value(index); - while (next < Size() && to_make_unperformed[next]) { + while (next < num_nexts && to_make_unperformed[next]) { const int64_t next_of_next = Value(next); SetValue(index, next_of_next); SetValue(next, next); @@ -543,8 +546,7 @@ CheapestInsertionFilteredHeuristic::CheapestInsertionFilteredHeuristic( std::function evaluator, std::function penalty_evaluator, LocalSearchFilterManager* filter_manager) - : RoutingFilteredHeuristic(model, std::move(stop_search), filter_manager, - /*omit_secondary_vars=*/evaluator != nullptr), + : RoutingFilteredHeuristic(model, std::move(stop_search), filter_manager), evaluator_(std::move(evaluator)), penalty_evaluator_(std::move(penalty_evaluator)) {} @@ -2604,8 +2606,9 @@ CheapestAdditionFilteredHeuristic::CheapestAdditionFilteredHeuristic( bool CheapestAdditionFilteredHeuristic::BuildSolutionInternal() { const int kUnassigned = -1; const RoutingModel::IndexPairs& pairs = model()->GetPickupAndDeliveryPairs(); - std::vector> deliveries(Size()); - std::vector> pickups(Size()); + const int num_nexts = model()->Nexts().size(); + std::vector> deliveries(num_nexts); + std::vector> pickups(num_nexts); for (const RoutingModel::IndexPair& pair : pairs) { for (int first : pair.first) { for (int second : pair.second) { @@ -3068,8 +3071,8 @@ class SavingsFilteredHeuristic::SavingsContainer { // skipped_savings_ vector of the nodes, if they're uncontained. void SkipSavingForArc(const SavingAndArc& saving_and_arc) { const Saving& saving = saving_and_arc.saving; - const int64_t before_node = savings_db_->GetBeforeNodeFromSaving(saving); - const int64_t after_node = savings_db_->GetAfterNodeFromSaving(saving); + const int64_t before_node = saving.before_node; + const int64_t after_node = saving.after_node; if (!savings_db_->Contains(before_node)) { skipped_savings_starting_at_[before_node].push_back(saving_and_arc); } @@ -3115,7 +3118,7 @@ class SavingsFilteredHeuristic::SavingsContainer { if (!next_saving_added && GetNextSavingForArcWithType(arc_index, type, &next_saving)) { - type_and_index.first = savings_db_->GetVehicleTypeFromSaving(next_saving); + type_and_index.first = next_saving.vehicle_type; if (previous_index >= 0) { // Update the previous saving. next_savings_[previous_index] = {next_saving, arc_index}; @@ -3211,7 +3214,7 @@ class SavingsFilteredHeuristic::SavingsContainer { bool found_saving = false; while (!costs_and_savings.empty() && !found_saving) { const Saving& saving = costs_and_savings.back().second; - if (type == -1 || savings_db_->GetVehicleTypeFromSaving(saving) == type) { + if (type == -1 || saving.vehicle_type == type) { *next_saving = saving; found_saving = true; } @@ -3257,8 +3260,6 @@ SavingsFilteredHeuristic::SavingsFilteredHeuristic( DCHECK_LE(savings_params_.neighbors_ratio, 1); DCHECK_GT(savings_params_.max_memory_usage_bytes, 0); DCHECK_GT(savings_params_.arc_coefficient, 0); - const int size = model->Size(); - size_squared_ = size * size; } SavingsFilteredHeuristic::~SavingsFilteredHeuristic() {} @@ -3500,10 +3501,10 @@ void SequentialSavingsFilteredHeuristic::BuildRoutesFromSavings() { const std::vector& sorted_savings_for_type = savings_container_->GetSortedSavingsForVehicleType(type); for (const Saving& saving : sorted_savings_for_type) { - DCHECK_EQ(GetVehicleTypeFromSaving(saving), type); - const int before_node = GetBeforeNodeFromSaving(saving); + DCHECK_EQ(saving.vehicle_type, type); + const int before_node = saving.before_node; in_savings_ptr[vehicle_type_offset + before_node].push_back(&saving); - const int after_node = GetAfterNodeFromSaving(saving); + const int after_node = saving.after_node; out_savings_ptr[vehicle_type_offset + after_node].push_back(&saving); } } @@ -3513,8 +3514,8 @@ void SequentialSavingsFilteredHeuristic::BuildRoutesFromSavings() { if (StopSearch()) return; // First find the best saving to start a new route. const Saving saving = savings_container_->GetSaving(); - int before_node = GetBeforeNodeFromSaving(saving); - int after_node = GetAfterNodeFromSaving(saving); + int before_node = saving.before_node; + int after_node = saving.after_node; const bool nodes_contained = Contains(before_node) || Contains(after_node); if (nodes_contained) { @@ -3523,7 +3524,7 @@ void SequentialSavingsFilteredHeuristic::BuildRoutesFromSavings() { } // Find the right vehicle to start the route with this Saving. - const int type = GetVehicleTypeFromSaving(saving); + const int type = saving.vehicle_type; const int vehicle = StartNewRouteWithBestVehicleOfType(type, before_node, after_node); if (vehicle < 0) { @@ -3550,17 +3551,18 @@ void SequentialSavingsFilteredHeuristic::BuildRoutesFromSavings() { if (out_index < out_savings_ptr[saving_offset + before_node].size()) { const Saving& out_saving = *(out_savings_ptr[saving_offset + before_node][out_index]); - if (GetSavingValue(in_saving) < GetSavingValue(out_saving)) { - after_after_node = GetAfterNodeFromSaving(in_saving); + if (in_saving.saving < out_saving.saving) { + after_after_node = in_saving.after_node; } else { - before_before_node = GetBeforeNodeFromSaving(out_saving); + before_before_node = out_saving.before_node; } } else { - after_after_node = GetAfterNodeFromSaving(in_saving); + after_after_node = in_saving.after_node; } } else { - before_before_node = GetBeforeNodeFromSaving( - *(out_savings_ptr[saving_offset + before_node][out_index])); + before_before_node = + out_savings_ptr[saving_offset + before_node][out_index] + ->before_node; } // Extend the route if (after_after_node != -1) { @@ -3629,9 +3631,9 @@ void ParallelSavingsFilteredHeuristic::BuildRoutesFromSavings() { while (savings_container_->HasSaving()) { if (StopSearch()) return; const Saving saving = savings_container_->GetSaving(); - const int64_t before_node = GetBeforeNodeFromSaving(saving); - const int64_t after_node = GetAfterNodeFromSaving(saving); - const int type = GetVehicleTypeFromSaving(saving); + const int64_t before_node = saving.before_node; + const int64_t after_node = saving.after_node; + const int type = saving.vehicle_type; if (!Contains(before_node) && !Contains(after_node)) { // Neither nodes are contained, start a new route. diff --git a/ortools/constraint_solver/routing_search.h b/ortools/constraint_solver/routing_search.h index 962d5306b8f..9ce5e4eb9cb 100644 --- a/ortools/constraint_solver/routing_search.h +++ b/ortools/constraint_solver/routing_search.h @@ -212,6 +212,7 @@ class IntVarFilteredHeuristic { /// Modifies the current solution by setting the variable of index 'index' to /// value 'value'. void SetValue(int64_t index, int64_t value) { + DCHECK_LT(index, is_in_delta_.size()); if (!is_in_delta_[index]) { delta_->FastAdd(vars_[index])->SetValue(value); delta_indices_.push_back(index); @@ -229,9 +230,6 @@ class IntVarFilteredHeuristic { bool Contains(int64_t index) const { return assignment_->IntVarContainer().Element(index).Var() != nullptr; } - /// Returns the number of variables the decision builder is trying to - /// instantiate. - int Size() const { return vars_.size(); } /// Returns the variable of index 'index'. IntVar* Var(int64_t index) const { return vars_[index]; } /// Returns the index of a secondary var. @@ -272,8 +270,7 @@ class RoutingFilteredHeuristic : public IntVarFilteredHeuristic { public: RoutingFilteredHeuristic(RoutingModel* model, std::function stop_search, - LocalSearchFilterManager* filter_manager, - bool omit_secondary_vars = true); + LocalSearchFilterManager* filter_manager); ~RoutingFilteredHeuristic() override {} /// Builds a solution starting from the routes formed by the next accessor. const Assignment* BuildSolutionFromRoutes( @@ -1022,7 +1019,7 @@ struct PickupDeliveryInsertion { } }; -/// Filter-base decision builder which builds a solution by inserting +/// Filter-based decision builder which builds a solution by inserting /// nodes at their cheapest position. The cost of a position is computed /// an arc-based cost callback. Node selected for insertion are considered in /// decreasing order of distance to the start/ends of the routes, i.e. farthest @@ -1215,7 +1212,17 @@ class SavingsFilteredHeuristic : public RoutingFilteredHeuristic { bool BuildSolutionInternal() override; protected: - typedef std::pair Saving; + struct Saving { + int64_t saving; + unsigned int vehicle_type : 20; + unsigned int before_node : 22; + unsigned int after_node : 22; + bool operator<(const Saving& other) const { + return std::tie(saving, vehicle_type, before_node, after_node) < + std::tie(other.saving, other.vehicle_type, other.before_node, + other.after_node); + } + }; template class SavingsContainer; @@ -1224,21 +1231,6 @@ class SavingsFilteredHeuristic : public RoutingFilteredHeuristic { virtual void BuildRoutesFromSavings() = 0; - /// Returns the cost class from a saving. - int64_t GetVehicleTypeFromSaving(const Saving& saving) const { - return saving.second / size_squared_; - } - /// Returns the "before node" from a saving. - int64_t GetBeforeNodeFromSaving(const Saving& saving) const { - return (saving.second % size_squared_) / Size(); - } - /// Returns the "after node" from a saving. - int64_t GetAfterNodeFromSaving(const Saving& saving) const { - return (saving.second % size_squared_) % Size(); - } - /// Returns the saving value from a saving. - int64_t GetSavingValue(const Saving& saving) const { return saving.first; } - /// Finds the best available vehicle of type "type" to start a new route to /// serve the arc before_node-->after_node. /// Since there are different vehicle classes for each vehicle type, each @@ -1278,8 +1270,9 @@ class SavingsFilteredHeuristic : public RoutingFilteredHeuristic { /// Builds a saving from a saving value, a vehicle type and two nodes. Saving BuildSaving(int64_t saving, int vehicle_type, int before_node, int after_node) const { - return std::make_pair(saving, vehicle_type * size_squared_ + - before_node * Size() + after_node); + return {saving, static_cast(vehicle_type), + static_cast(before_node), + static_cast(after_node)}; } /// Computes and returns the maximum number of (closest) neighbors to consider @@ -1288,7 +1281,6 @@ class SavingsFilteredHeuristic : public RoutingFilteredHeuristic { int64_t MaxNumNeighborsPerNode(int num_vehicle_types) const; const SavingsParameters savings_params_; - int64_t size_squared_; friend class SavingsFilteredHeuristicTestPeer; }; diff --git a/ortools/constraint_solver/routing_utils.cc b/ortools/constraint_solver/routing_utils.cc new file mode 100644 index 00000000000..9d0c52fd070 --- /dev/null +++ b/ortools/constraint_solver/routing_utils.cc @@ -0,0 +1,174 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/constraint_solver/routing_utils.h" + +#include +#include +#include +#include +#include +#include + +#include "ortools/util/saturated_arithmetic.h" + +namespace operations_research { + +void BinCapacities::AddDimension( + std::function load_demand_of_item_for_bin, + std::vector load_limit_per_bin) { + DCHECK_EQ(num_bins_, load_limit_per_bin.size()); + for (const LoadLimit& limit : load_limit_per_bin) { + const int64_t violation = std::max(0, CapOpp(limit.soft_max_load)); + total_cost_ = + CapAdd(total_cost_, CapProd(violation, limit.cost_above_soft_max_load)); + } + load_demands_per_dimension_.push_back(std::move(load_demand_of_item_for_bin)); + for (int b = 0; b < num_bins_; ++b) { + load_per_bin_[b].push_back(0); + load_limits_per_bin_[b].push_back(load_limit_per_bin[b]); + } +} + +bool BinCapacities::CheckAdditionFeasibility(int item, int bin) const { + return CheckAdditionsFeasibility({item}, bin); +} + +bool BinCapacities::CheckAdditionsFeasibility(const std::vector& items, + int bin) const { + for (size_t dim = 0; dim < load_demands_per_dimension_.size(); ++dim) { + const LoadLimit& limit = load_limits_per_bin_[bin][dim]; + int64_t new_load = load_per_bin_[bin][dim]; + for (const int item : items) { + new_load = CapAdd(new_load, load_demands_per_dimension_[dim](item, bin)); + } + // TODO(user): try to reorder on failure, so that tight dimensions are + // checked first. + if (new_load > limit.max_load) return false; + } + return true; +} + +bool BinCapacities::AddItemToBin(int item, int bin) { + bool feasible = true; + for (size_t dim = 0; dim < load_demands_per_dimension_.size(); ++dim) { + int64_t& load = load_per_bin_[bin][dim]; + const LoadLimit& limit = load_limits_per_bin_[bin][dim]; + const int64_t prev_violation = + std::max(0, CapSub(load, limit.soft_max_load)); + load = CapAdd(load, load_demands_per_dimension_[dim](item, bin)); + const int64_t curr_violation = + std::max(0, CapSub(load, limit.soft_max_load)); + total_cost_ = + CapAdd(total_cost_, CapProd(CapSub(curr_violation, prev_violation), + limit.cost_above_soft_max_load)); + feasible &= load <= limit.max_load; + } + return feasible; +} + +bool BinCapacities::RemoveItemFromBin(int item, int bin) { + bool feasible = true; + for (size_t dim = 0; dim < load_demands_per_dimension_.size(); ++dim) { + int64_t& load = load_per_bin_[bin][dim]; + const LoadLimit& limit = load_limits_per_bin_[bin][dim]; + const int64_t prev_violation = + std::max(0, CapSub(load, limit.soft_max_load)); + load = CapSub(load, load_demands_per_dimension_[dim](item, bin)); + const int64_t curr_violation = + std::max(0, CapSub(load, limit.soft_max_load)); + total_cost_ = + CapAdd(total_cost_, CapProd(CapSub(curr_violation, prev_violation), + limit.cost_above_soft_max_load)); + feasible &= load <= limit.max_load; + } + return feasible; +} + +void BinCapacities::ClearItemsOfBin(int bin) { + for (size_t dim = 0; dim < load_demands_per_dimension_.size(); ++dim) { + int64_t& load = load_per_bin_[bin][dim]; + const LoadLimit& limit = load_limits_per_bin_[bin][dim]; + const int64_t prev_violation = + std::max(0, CapSub(load, limit.soft_max_load)); + load = 0; + const int64_t curr_violation = + std::max(0, CapOpp(limit.soft_max_load)); + total_cost_ = + CapAdd(total_cost_, CapProd(CapSub(curr_violation, prev_violation), + limit.cost_above_soft_max_load)); + } +} + +void BinCapacities::ClearItems() { + for (int bin = 0; bin < num_bins_; ++bin) { + ClearItemsOfBin(bin); + } +} + +bool FindMostExpensiveArcsOnRoute( + int num_arcs, int64_t start, + const std::function& next_accessor, + const std::function& is_end, + const std::function& + arc_cost_for_route_start, + std::vector>* most_expensive_arc_starts_and_ranks, + std::pair* first_expensive_arc_indices) { + if (is_end(next_accessor(start))) { + // Empty route. + *first_expensive_arc_indices = {-1, -1}; + return false; + } + + // NOTE: The negative ranks are so that for a given cost, lower ranks are + // given higher priority. + using ArcCostNegativeRankStart = std::tuple; + std::priority_queue, + std::greater> + arc_info_pq; + + int64_t before_node = start; + int rank = 0; + while (!is_end(before_node)) { + const int64_t after_node = next_accessor(before_node); + const int64_t arc_cost = + arc_cost_for_route_start(before_node, after_node, start); + arc_info_pq.emplace(arc_cost, -rank, before_node); + + before_node = after_node; + rank++; + + if (rank > num_arcs) { + arc_info_pq.pop(); + } + } + + DCHECK_GE(rank, 2); + DCHECK_EQ(arc_info_pq.size(), std::min(rank, num_arcs)); + + most_expensive_arc_starts_and_ranks->resize(arc_info_pq.size()); + int arc_index = arc_info_pq.size() - 1; + while (!arc_info_pq.empty()) { + const ArcCostNegativeRankStart& arc_info = arc_info_pq.top(); + (*most_expensive_arc_starts_and_ranks)[arc_index] = { + std::get<2>(arc_info), -std::get<1>(arc_info)}; + arc_index--; + arc_info_pq.pop(); + } + + *first_expensive_arc_indices = {0, 1}; + return true; +} + +} // namespace operations_research diff --git a/ortools/constraint_solver/routing_utils.h b/ortools/constraint_solver/routing_utils.h new file mode 100644 index 00000000000..99d890108e4 --- /dev/null +++ b/ortools/constraint_solver/routing_utils.h @@ -0,0 +1,91 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_UTILS_H_ +#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_UTILS_H_ + +#include +#include +#include +#include + +namespace operations_research { + +// Tracks whether bins constrained by several nonnegative dimensions can contain +// items added incrementally. Also tracks soft violation costs. +class BinCapacities { + public: + explicit BinCapacities(int num_bins) + : num_bins_(num_bins), + load_per_bin_(num_bins), + load_limits_per_bin_(num_bins), + total_cost_(0) {} + + // Represents the limits of a bin with respect to a dimension: + // - max_load is a max total load, can cause TryAddItemToBin() to return false + // if load would exceed max_load. + // - soft_max_load is a max load that can be exceeded, causing the TotalCost() + // to increase. Initial value *may* be negative, to help with modelling. + // - cost_above_soft_max_load is the cost incurred per unit by which load + // exceeds soft_max_load. + struct LoadLimit { + int64_t max_load; + int64_t soft_max_load; + int64_t cost_above_soft_max_load; + }; + + void AddDimension( + std::function load_demand_of_item_for_bin, + std::vector load_limit_per_bin); + + // Checks whether adding item(s) is feasible w.r.t. dimensions. + bool CheckAdditionFeasibility(int item, int bin) const; + bool CheckAdditionsFeasibility(const std::vector& items, int bin) const; + // Adds item to bin, returns whether the bin is feasible. + // The item is still added even when infeasible. + bool AddItemToBin(int item, int bin); + // Removes item from bin, return whether the bin is feasible. + bool RemoveItemFromBin(int item, int bin); + // Returns the total cost incurred by violating soft costs. + int64_t TotalCost() const { return total_cost_; } + // Removes all items from given bin, resets the total cost. + void ClearItemsOfBin(int bin); + // Removes all items from all bins. + void ClearItems(); + + private: + const int num_bins_; + // load_per_bin_[bin][dimension] + std::vector> load_per_bin_; + // load_limits_per_bin_[bin][dimension]. + std::vector> load_limits_per_bin_; + // load_demands_per_dimension_[dimension](item, bin). + std::vector> load_demands_per_dimension_; + int64_t total_cost_; +}; + +// Returns false if the route starting with 'start' is empty. Otherwise sets +// most_expensive_arc_starts_and_ranks and first_expensive_arc_indices according +// to the most expensive chains on the route, and returns true. +bool FindMostExpensiveArcsOnRoute( + int num_arcs, int64_t start, + const std::function& next_accessor, + const std::function& is_end, + const std::function& + arc_cost_for_route_start, + std::vector>* most_expensive_arc_starts_and_ranks, + std::pair* first_expensive_arc_indices); + +} // namespace operations_research + +#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_UTILS_H_ diff --git a/ortools/constraint_solver/search.cc b/ortools/constraint_solver/search.cc index 972b917b2f0..3febf38ff8c 100644 --- a/ortools/constraint_solver/search.cc +++ b/ortools/constraint_solver/search.cc @@ -24,22 +24,14 @@ #include #include -#include "absl/base/casts.h" -#include "absl/container/flat_hash_map.h" -#include "absl/memory/memory.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" #include "absl/time/time.h" #include "ortools/base/bitmap.h" -#include "ortools/base/commandlineflags.h" -#include "ortools/base/hash.h" #include "ortools/base/integral_types.h" #include "ortools/base/logging.h" -#include "ortools/base/macros.h" -#include "ortools/base/map_util.h" #include "ortools/base/mathutil.h" -#include "ortools/base/stl_util.h" #include "ortools/base/timer.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" @@ -57,30 +49,28 @@ namespace operations_research { // ---------- Search Log --------- -SearchLog::SearchLog(Solver* const s, OptimizeVar* const obj, IntVar* const var, - double scaling_factor, double offset, +SearchLog::SearchLog(Solver* solver, std::vector vars, + std::string vars_name, std::vector scaling_factors, + std::vector offsets, std::function display_callback, bool display_on_new_solutions_only, int period) - : SearchMonitor(s), + : SearchMonitor(solver), period_(period), timer_(new WallTimer), - var_(var), - obj_(obj), - scaling_factor_(scaling_factor), - offset_(offset), + vars_(std::move(vars)), + vars_name_(std::move(vars_name)), + scaling_factors_(std::move(scaling_factors)), + offsets_(std::move(offsets)), display_callback_(std::move(display_callback)), display_on_new_solutions_only_(display_on_new_solutions_only), nsol_(0), tick_(0), - objective_min_(std::numeric_limits::max()), - objective_max_(std::numeric_limits::min()), + objective_min_(vars_.size(), std::numeric_limits::max()), + objective_max_(vars_.size(), std::numeric_limits::min()), min_right_depth_(std::numeric_limits::max()), max_depth_(0), sliding_min_depth_(0), - sliding_max_depth_(0) { - CHECK(obj == nullptr || var == nullptr) - << "Either var or obj need to be nullptr."; -} + sliding_max_depth_(0) {} SearchLog::~SearchLog() {} @@ -111,39 +101,57 @@ bool SearchLog::AtSolution() { Maintain(); const int depth = solver()->SearchDepth(); std::string obj_str = ""; - int64_t current = 0; + std::vector current; bool objective_updated = false; - const auto scaled_str = [this](int64_t value) { - if (scaling_factor_ != 1.0 || offset_ != 0.0) { - return absl::StrFormat("%d (%.8lf)", value, - scaling_factor_ * (value + offset_)); - } else { - return absl::StrCat(value); + const auto scaled_str = [this](const std::vector& values) { + std::vector value_strings(values.size()); + for (int i = 0; i < values.size(); ++i) { + if (scaling_factors_[i] != 1.0 || offsets_[i] != 0.0) { + value_strings[i] = + absl::StrFormat("%d (%.8lf)", values[i], + scaling_factors_[i] * (values[i] + offsets_[i])); + } else { + value_strings[i] = absl::StrCat(values[i]); + } } + return absl::StrJoin(value_strings, ", "); }; - if (obj_ != nullptr && obj_->var()->Bound()) { - current = obj_->var()->Value(); - obj_str = obj_->Print(); - objective_updated = true; - } else if (var_ != nullptr && var_->Bound()) { - current = var_->Value(); - absl::StrAppend(&obj_str, scaled_str(current), ", "); - objective_updated = true; + bool all_vars_bound = !vars_.empty(); + for (IntVar* var : vars_) { + all_vars_bound &= var->Bound(); + } + if (all_vars_bound) { + for (IntVar* var : vars_) { + current.push_back(var->Value()); + objective_updated = true; + } + absl::StrAppend(&obj_str, + vars_name_.empty() ? "" : absl::StrCat(vars_name_, " = "), + scaled_str(current), ", "); } else { - current = solver()->GetOrCreateLocalSearchState()->ObjectiveMin(); - absl::StrAppend(&obj_str, scaled_str(current), ", "); + current.push_back(solver()->GetOrCreateLocalSearchState()->ObjectiveMin()); + absl::StrAppend(&obj_str, "objective = ", scaled_str(current), ", "); objective_updated = true; } if (objective_updated) { - if (current > objective_min_) { + if (!objective_min_.empty() && + std::lexicographical_compare(objective_min_.begin(), + objective_min_.end(), current.begin(), + current.end())) { absl::StrAppend(&obj_str, - "objective minimum = ", scaled_str(objective_min_), ", "); + vars_name_.empty() ? "" : absl::StrCat(vars_name_, " "), + "minimum = ", scaled_str(objective_min_), ", "); + } else { objective_min_ = current; } - if (current < objective_max_) { + if (!objective_max_.empty() && + std::lexicographical_compare(current.begin(), current.end(), + objective_max_.begin(), + objective_max_.end())) { absl::StrAppend(&obj_str, - "objective maximum = ", scaled_str(objective_max_), ", "); + vars_name_.empty() ? "" : absl::StrCat(vars_name_, " "), + "maximum = ", scaled_str(objective_max_), ", "); } else { objective_max_ = current; } @@ -227,13 +235,15 @@ void SearchLog::OutputDecision() { sliding_min_depth_ = depth; sliding_max_depth_ = depth; } - if (obj_ != nullptr && - objective_min_ != std::numeric_limits::max() && - objective_max_ != std::numeric_limits::min()) { + if (!objective_min_.empty() && + objective_min_[0] != std::numeric_limits::max() && + objective_max_[0] != std::numeric_limits::min()) { + const std::string name = + vars_name_.empty() ? "" : absl::StrCat(" ", vars_name_); absl::StrAppendFormat(&buffer, - ", objective minimum = %d" - ", objective maximum = %d", - objective_min_, objective_max_); + ",%s minimum = %d" + ",%s maximum = %d", + name, objective_min_[0], name, objective_max_[0]); } const int progress = solver()->TopProgressPercent(); if (progress != SearchMonitor::kNoProgress) { @@ -288,46 +298,63 @@ std::string SearchLog::MemoryUsage() { } SearchMonitor* Solver::MakeSearchLog(int branch_period) { - return MakeSearchLog(branch_period, static_cast(nullptr)); + return MakeSearchLog(branch_period, std::vector{}, nullptr); } -SearchMonitor* Solver::MakeSearchLog(int branch_period, IntVar* const var) { - return MakeSearchLog(branch_period, var, nullptr); +SearchMonitor* Solver::MakeSearchLog(int branch_period, IntVar* var) { + return MakeSearchLog(branch_period, std::vector{var}, nullptr); } SearchMonitor* Solver::MakeSearchLog( int branch_period, std::function display_callback) { - return MakeSearchLog(branch_period, static_cast(nullptr), + return MakeSearchLog(branch_period, std::vector{}, std::move(display_callback)); } SearchMonitor* Solver::MakeSearchLog( - int branch_period, IntVar* const var, + int branch_period, IntVar* var, std::function display_callback) { - return RevAlloc(new SearchLog(this, nullptr, var, 1.0, 0.0, + return MakeSearchLog(branch_period, std::vector{var}, + std::move(display_callback)); +} + +SearchMonitor* Solver::MakeSearchLog( + int branch_period, std::vector vars, + std::function display_callback) { + return RevAlloc(new SearchLog(this, std::move(vars), "", {1.0}, {0.0}, std::move(display_callback), true, branch_period)); } -SearchMonitor* Solver::MakeSearchLog(int branch_period, - OptimizeVar* const opt_var) { +SearchMonitor* Solver::MakeSearchLog(int branch_period, OptimizeVar* opt_var) { return MakeSearchLog(branch_period, opt_var, nullptr); } SearchMonitor* Solver::MakeSearchLog( - int branch_period, OptimizeVar* const opt_var, + int branch_period, OptimizeVar* opt_var, std::function display_callback) { - return RevAlloc(new SearchLog(this, opt_var, nullptr, 1.0, 0.0, - std::move(display_callback), true, - branch_period)); + std::vector vars = opt_var->objective_vars(); + std::vector scaling_factors(vars.size(), 1.0); + std::vector offsets(vars.size(), 0.0); + return RevAlloc(new SearchLog( + this, std::move(vars), opt_var->Name(), std::move(scaling_factors), + std::move(offsets), std::move(display_callback), true, branch_period)); } SearchMonitor* Solver::MakeSearchLog(SearchLogParameters parameters) { - return RevAlloc(new SearchLog(this, parameters.objective, parameters.variable, - parameters.scaling_factor, parameters.offset, - std::move(parameters.display_callback), - parameters.display_on_new_solutions_only, - parameters.branch_period)); + DCHECK(parameters.objective == nullptr || parameters.variables.empty()) + << "Either variables are empty or objective is nullptr."; + std::vector vars = parameters.objective != nullptr + ? parameters.objective->objective_vars() + : parameters.variables; + std::vector scaling_factors = parameters.scaling_factors; + scaling_factors.resize(vars.size(), 1.0); + std::vector offsets = parameters.offsets; + offsets.resize(vars.size(), 0.0); + return RevAlloc(new SearchLog( + this, std::move(vars), "", std::move(scaling_factors), std::move(offsets), + std::move(parameters.display_callback), + parameters.display_on_new_solutions_only, parameters.branch_period)); } // ---------- Search Trace ---------- @@ -3072,14 +3099,7 @@ bool OptimizeVar::AcceptDelta(Assignment* delta, Assignment* deltadelta) { return true; } -std::string OptimizeVar::Print() const { - std::string out = - (Size() == 1) ? "objective value = " : "objective values = "; - for (int i = 0; i < Size(); ++i) { - absl::StrAppendFormat(&out, "%d, ", ObjectiveVar(i)->Value()); - } - return out; -} +std::string OptimizeVar::Name() const { return "objective"; } std::string OptimizeVar::DebugString() const { std::string out; @@ -3126,23 +3146,14 @@ class WeightedOptimizeVar : public OptimizeVar { } ~WeightedOptimizeVar() override {} - std::string Print() const override; + std::string Name() const override; private: const std::vector sub_objectives_; const std::vector weights_; }; -std::string WeightedOptimizeVar::Print() const { - std::string result(OptimizeVar::Print()); - result.append("\nWeighted Objective:\n"); - for (int i = 0; i < sub_objectives_.size(); ++i) { - absl::StrAppendFormat(&result, "Variable %s,\tvalue %d,\tweight %d\n", - sub_objectives_[i]->name(), - sub_objectives_[i]->Value(), weights_[i]); - } - return result; -} +std::string WeightedOptimizeVar::Name() const { return "weighted objective"; } } // namespace OptimizeVar* Solver::MakeWeightedOptimize(