diff --git a/CHANGELOG.md b/CHANGELOG.md index 6efb394a1..4191aad77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ #### Internals - Bypass matrix request in `plan` mode (#444) +- Account for heuristic time in timeout (#1196) - Refactor heuristics to reduce code duplication (#1181) - Refactor `Matrix` template class (#1089) - Refactor to use `std::format` whenever possible (#1081) @@ -22,6 +23,8 @@ - Remove amount consistency checks in `parse` in favor of upstream checks in `Input` (#1086) - Reduce code duplication in routing wrappers (#1184) - Allow passing `path` in `Server` ctor (#1192) +- Refactor to simplify `VRP::solve` (#1196) +- Remove heuristic synchronisation (#1188) #### CI diff --git a/src/algorithms/heuristics/heuristics.cpp b/src/algorithms/heuristics/heuristics.cpp index ef7f49207..91b3572a0 100644 --- a/src/algorithms/heuristics/heuristics.cpp +++ b/src/algorithms/heuristics/heuristics.cpp @@ -719,14 +719,11 @@ void set_route(const Input& input, } template -std::unordered_set set_initial_routes(const Input& input, - std::vector& routes) { - std::unordered_set assigned; - +void set_initial_routes(const Input& input, + std::vector& routes, + std::unordered_set& assigned) { std::ranges::for_each(routes, [&](auto& r) { set_route(input, r, assigned); }); - - return assigned; } using RawSolution = std::vector; @@ -748,8 +745,9 @@ template Eval dynamic_vehicle_choice(const Input& input, double lambda, SORT sort); -template std::unordered_set set_initial_routes(const Input& input, - RawSolution& routes); +template void set_initial_routes(const Input& input, + RawSolution& routes, + std::unordered_set& assigned); template Eval basic(const Input& input, TWSolution& routes, @@ -767,7 +765,8 @@ template Eval dynamic_vehicle_choice(const Input& input, double lambda, SORT sort); -template std::unordered_set set_initial_routes(const Input& input, - TWSolution& routes); +template void set_initial_routes(const Input& input, + TWSolution& routes, + std::unordered_set& assigned); } // namespace vroom::heuristics diff --git a/src/algorithms/heuristics/heuristics.h b/src/algorithms/heuristics/heuristics.h index 3b1278cfc..a1bce2180 100644 --- a/src/algorithms/heuristics/heuristics.h +++ b/src/algorithms/heuristics/heuristics.h @@ -40,8 +40,9 @@ Eval dynamic_vehicle_choice(const Input& input, // Populate routes with user-defined vehicle steps. template -std::unordered_set set_initial_routes(const Input& input, - std::vector& routes); +void set_initial_routes(const Input& input, + std::vector& routes, + std::unordered_set& assigned); } // namespace vroom::heuristics diff --git a/src/algorithms/local_search/local_search.cpp b/src/algorithms/local_search/local_search.cpp index ebe620807..ae2fb1d81 100644 --- a/src/algorithms/local_search/local_search.cpp +++ b/src/algorithms/local_search/local_search.cpp @@ -1923,14 +1923,6 @@ void LocalSearch::run() { bool try_ls_step = true; -#ifdef LOG_LS - steps.emplace_back(utils::now(), - log::EVENT::START, - OperatorName::MAX, - _best_sol_indicators, - utils::format_solution(_input, _best_sol)); -#endif - while (try_ls_step) { // A round of local search. run_ls_step(); diff --git a/src/algorithms/local_search/log_local_search.h b/src/algorithms/local_search/log_local_search.h index 714742688..74186cae3 100644 --- a/src/algorithms/local_search/log_local_search.h +++ b/src/algorithms/local_search/log_local_search.h @@ -17,6 +17,7 @@ namespace vroom::ls::log { enum class EVENT { START, + HEURISTIC, OPERATOR, LOCAL_MINIMA, JOB_ADDITION, diff --git a/src/problems/cvrp/cvrp.cpp b/src/problems/cvrp/cvrp.cpp index eda7d3448..ef877ea5f 100644 --- a/src/problems/cvrp/cvrp.cpp +++ b/src/problems/cvrp/cvrp.cpp @@ -143,9 +143,9 @@ const std::vector CVRP::heterogeneous_parameters = CVRP::CVRP(const Input& input) : VRP(input) { } -Solution CVRP::solve(unsigned nb_searches, - unsigned depth, - unsigned nb_threads, +Solution CVRP::solve(const unsigned nb_searches, + const unsigned depth, + const unsigned nb_threads, const Timeout& timeout, const std::vector& h_param) const { if (_input.vehicles.size() == 1 && !_input.has_skills() && diff --git a/src/problems/cvrp/cvrp.h b/src/problems/cvrp/cvrp.h index 58eb31bac..7f4633d0e 100644 --- a/src/problems/cvrp/cvrp.h +++ b/src/problems/cvrp/cvrp.h @@ -25,9 +25,9 @@ class CVRP : public VRP { explicit CVRP(const Input& input); Solution - solve(unsigned nb_searches, - unsigned depth, - unsigned nb_threads, + solve(const unsigned nb_searches, + const unsigned depth, + const unsigned nb_threads, const Timeout& timeout, const std::vector& h_param) const override; }; diff --git a/src/problems/vrp.h b/src/problems/vrp.h index 0cd97c709..8f51d9626 100644 --- a/src/problems/vrp.h +++ b/src/problems/vrp.h @@ -30,236 +30,274 @@ All rights reserved (see LICENSE). namespace vroom { -class VRP { - // Abstract class describing a VRP (vehicle routing problem). -protected: - const Input& _input; +template +std::vector set_init_sol(const Input& input, + std::unordered_set& init_assigned) { + std::vector init_sol; + init_sol.reserve(input.vehicles.size()); + + for (Index v = 0; v < input.vehicles.size(); ++v) { + init_sol.emplace_back(input, v, input.zero_amount().size()); + } - template - Solution solve( - unsigned nb_searches, - unsigned depth, - unsigned nb_threads, - const Timeout& timeout, - const std::vector& h_param, - const std::vector& homogeneous_parameters, - const std::vector& heterogeneous_parameters) const { - // Use vector of parameters when passed for debugging, else use - // predefined parameter set. - const auto& parameters = (!h_param.empty()) ? h_param - : (_input.has_homogeneous_locations()) - ? homogeneous_parameters - : heterogeneous_parameters; - assert(nb_searches != 0); - nb_searches = - std::min(nb_searches, static_cast(parameters.size())); + if (input.has_initial_routes()) { + heuristics::set_initial_routes(input, init_sol, init_assigned); + } - // Build initial solution to be filled by heuristics. Solution is - // empty at first but populated with input data if provided. - std::vector init_sol; - init_sol.reserve(_input.vehicles.size()); + return init_sol; +} - for (Index v = 0; v < _input.vehicles.size(); ++v) { - init_sol.emplace_back(_input, v, _input.zero_amount().size()); - } +template struct SolvingContext { + std::unordered_set init_assigned; + const std::vector init_sol; + std::set unassigned; + std::vector vehicles_ranks; + std::vector> solutions; + std::vector sol_indicators; - std::unordered_set init_assigned; - if (_input.has_initial_routes()) { - init_assigned = heuristics::set_initial_routes(_input, init_sol); - } + std::set heuristic_indicators; + std::mutex heuristic_indicators_m; - std::vector> solutions(nb_searches, init_sol); +#ifdef LOG_LS_OPERATORS + std::vector> ls_stats; +#endif #ifdef LOG_LS - std::vector ls_dumps; - ls_dumps.reserve(nb_searches); + std::vector ls_dumps; +#endif + + SolvingContext(const Input& input, unsigned nb_searches) + : init_sol(set_init_sol(input, init_assigned)), + vehicles_ranks(input.vehicles.size()), + solutions(nb_searches, init_sol), + sol_indicators(nb_searches) +#ifdef LOG_LS_OPERATORS + , + ls_stats(nb_searches) #endif + { - // Heuristics operate on all assigned jobs. - std::set unassigned; - std::ranges::copy_if(std::views::iota(0u, _input.jobs.size()), + // Deduce unassigned jobs from initial solution. + std::ranges::copy_if(std::views::iota(0u, input.jobs.size()), std::inserter(unassigned, unassigned.begin()), - [&init_assigned](const Index j) { + [this](const Index j) { return !init_assigned.contains(j); }); - // Heuristics operate on all vehicles. - std::vector vehicles_ranks(_input.vehicles.size()); + // Heuristics will operate on all vehicles. std::iota(vehicles_ranks.begin(), vehicles_ranks.end(), 0); + } - // Split the heuristic parameters among threads. - std::vector> - thread_ranks(nb_threads, std::vector()); - for (std::size_t i = 0; i < nb_searches; ++i) { - thread_ranks[i % nb_threads].push_back(i); + bool heuristic_solution_already_found(unsigned rank) { + assert(rank < sol_indicators.size()); + std::scoped_lock lock(heuristic_indicators_m); + const auto [dummy, insertion_ok] = + heuristic_indicators.insert(sol_indicators[rank]); + + return !insertion_ok; + } +}; + +template +void run_single_search(const Input& input, + const HeuristicParameters& p, + const unsigned rank, + const unsigned depth, + const Timeout& search_time, + SolvingContext& context) { + const auto heuristic_start = utils::now(); #ifdef LOG_LS - ls_dumps.push_back({parameters[i], {}}); + context.ls_dumps[rank].steps.emplace_back(heuristic_start, + ls::log::EVENT::START, + OperatorName::MAX); #endif - } - std::exception_ptr ep = nullptr; - std::mutex ep_m; + Eval h_eval; + switch (p.heuristic) { + case HEURISTIC::BASIC: + h_eval = heuristics::basic(input, + context.solutions[rank], + context.unassigned, + context.vehicles_ranks, + p.init, + p.regret_coeff, + p.sort); + break; + case HEURISTIC::DYNAMIC: + h_eval = heuristics::dynamic_vehicle_choice(input, + context.solutions[rank], + context.unassigned, + context.vehicles_ranks, + p.init, + p.regret_coeff, + p.sort); + break; + } - auto run_heuristics = [&](const std::vector& param_ranks) { - try { - for (auto rank : param_ranks) { - const auto& p = parameters[rank]; - - Eval h_eval; - switch (p.heuristic) { - case HEURISTIC::BASIC: - h_eval = heuristics::basic(_input, - solutions[rank], - unassigned, - vehicles_ranks, + if (!input.has_homogeneous_costs() && p.sort == SORT::AVAILABILITY) { + // Worth trying another vehicle ordering scheme in + // heuristic. + std::vector other_sol = context.init_sol; + + Eval h_other_eval; + switch (p.heuristic) { + case HEURISTIC::BASIC: + h_other_eval = heuristics::basic(input, + other_sol, + context.unassigned, + context.vehicles_ranks, p.init, p.regret_coeff, - p.sort); - break; - case HEURISTIC::DYNAMIC: - h_eval = heuristics::dynamic_vehicle_choice(_input, - solutions[rank], - unassigned, - vehicles_ranks, - p.init, - p.regret_coeff, - p.sort); - break; - } + SORT::COST); + break; + case HEURISTIC::DYNAMIC: + h_other_eval = + heuristics::dynamic_vehicle_choice(input, + other_sol, + context.unassigned, + context.vehicles_ranks, + p.init, + p.regret_coeff, + SORT::COST); + break; + } - if (!_input.has_homogeneous_costs() && h_param.empty() && - p.sort == SORT::AVAILABILITY) { - // Worth trying another vehicle ordering scheme in - // heuristic. - std::vector other_sol = init_sol; - - Eval h_other_eval; - switch (p.heuristic) { - case HEURISTIC::BASIC: - h_other_eval = heuristics::basic(_input, - other_sol, - unassigned, - vehicles_ranks, - p.init, - p.regret_coeff, - SORT::COST); - break; - case HEURISTIC::DYNAMIC: - h_other_eval = - heuristics::dynamic_vehicle_choice(_input, - other_sol, - unassigned, - vehicles_ranks, - p.init, - p.regret_coeff, - SORT::COST); - break; - } - - if (h_other_eval < h_eval) { - solutions[rank] = std::move(other_sol); + if (h_other_eval < h_eval) { + context.solutions[rank] = std::move(other_sol); #ifdef LOG_LS - ls_dumps[rank].heuristic_parameters.sort = SORT::COST; + context.ls_dumps[rank].heuristic_parameters.sort = SORT::COST; #endif - } - } - } - } catch (...) { - std::scoped_lock lock(ep_m); - ep = std::current_exception(); - } - }; + } + } - std::vector heuristics_threads; - heuristics_threads.reserve(nb_threads); + // Check if heuristic solution has been encountered before. + context.sol_indicators[rank] = + utils::SolutionIndicators(input, context.solutions[rank]); - for (const auto& param_ranks : thread_ranks) { - if (!param_ranks.empty()) { - heuristics_threads.emplace_back(run_heuristics, param_ranks); - } - } + const auto heuristic_end = utils::now(); - for (auto& t : heuristics_threads) { - t.join(); - } +#ifdef LOG_LS + context.ls_dumps[rank] + .steps.emplace_back(heuristic_end, + ls::log::EVENT::HEURISTIC, + OperatorName::MAX, + context.sol_indicators[rank], + utils::format_solution(input, context.solutions[rank])); +#endif - if (ep != nullptr) { - std::rethrow_exception(ep); - } + if (context.heuristic_solution_already_found(rank)) { + // Duplicate heuristic solution, so skip local search. + return; + } - // Filter out duplicate heuristics solutions. - std::set unique_indicators; - std::vector to_remove; - to_remove.reserve(solutions.size()); + Timeout ls_search_time; + if (search_time.has_value()) { + const auto heuristic_time = + std::chrono::duration_cast(heuristic_end - + heuristic_start); - for (unsigned i = 0; i < solutions.size(); ++i) { - const auto result = unique_indicators.emplace(_input, solutions[i]); - if (!result.second) { - // No insertion means an equivalent solution already exists. - to_remove.push_back(i); - } + if (search_time.value() <= heuristic_time) { + // No time left for local search! + return; } - for (auto remove_rank = to_remove.rbegin(); remove_rank != to_remove.rend(); - remove_rank++) { - solutions.erase(solutions.begin() + *remove_rank); -#ifdef LOG_LS - ls_dumps.erase(ls_dumps.begin() + *remove_rank); -#endif - } + ls_search_time = search_time.value() - heuristic_time; + } - // Split local searches across threads. - unsigned nb_solutions = solutions.size(); - std::vector sol_indicators(nb_solutions); + // Local search phase. + LocalSearch ls(input, context.solutions[rank], depth, ls_search_time); + ls.run(); + + // Store solution indicators. + context.sol_indicators[rank] = ls.indicators(); #ifdef LOG_LS_OPERATORS - std::vector> ls_stats( - nb_solutions); + context.ls_stats[rank] = ls.get_stats(); #endif +#ifdef LOG_LS + auto ls_steps = ls.get_steps(); - std::ranges::fill(thread_ranks, std::vector()); - for (std::size_t i = 0; i < nb_solutions; ++i) { - thread_ranks[i % nb_threads].push_back(i); - } + assert(context.ls_dumps[rank].steps.size() == 2); + context.ls_dumps[rank].steps.reserve(2 + ls_steps.size()); - auto run_ls = [&](const std::vector& sol_ranks) { - try { - // Decide time allocated for each search. - Timeout search_time; - if (timeout.has_value()) { - search_time = timeout.value() / sol_ranks.size(); - } + std::ranges::move(ls_steps, std::back_inserter(context.ls_dumps[rank].steps)); +#endif +} - for (auto rank : sol_ranks) { - // Local search phase. - LocalSearch ls(_input, solutions[rank], depth, search_time); - ls.run(); +class VRP { + // Abstract class describing a VRP (vehicle routing problem). +protected: + const Input& _input; + + template + Solution solve( + unsigned nb_searches, + const unsigned depth, + const unsigned nb_threads, + const Timeout& timeout, + const std::vector& h_param, + const std::vector& homogeneous_parameters, + const std::vector& heterogeneous_parameters) const { + // Use vector of parameters when passed for debugging, else use + // predefined parameter set. + const auto& parameters = (!h_param.empty()) ? h_param + : (_input.has_homogeneous_locations()) + ? homogeneous_parameters + : heterogeneous_parameters; + assert(nb_searches != 0); + nb_searches = + std::min(nb_searches, static_cast(parameters.size())); + + SolvingContext context(_input, nb_searches); + + // Split the heuristic parameters among threads. + std::vector> + thread_ranks(nb_threads, std::vector()); + for (std::size_t i = 0; i < nb_searches; ++i) { + thread_ranks[i % nb_threads].push_back(i); - // Store solution indicators. - sol_indicators[rank] = ls.indicators(); -#ifdef LOG_LS_OPERATORS - ls_stats[rank] = ls.get_stats(); -#endif #ifdef LOG_LS - ls_dumps[rank].steps = ls.get_steps(); + context.ls_dumps.push_back({parameters[i], {}}); #endif + } + + std::exception_ptr ep = nullptr; + std::mutex ep_m; + + auto run_solving = + [&context, ¶meters, &timeout, &ep, &ep_m, depth, this]( + const std::vector& param_ranks) { + try { + // Decide time allocated for each search. + Timeout search_time; + if (timeout.has_value()) { + search_time = timeout.value() / param_ranks.size(); + } + + for (auto rank : param_ranks) { + run_single_search(_input, + parameters[rank], + rank, + depth, + search_time, + context); + } + } catch (...) { + std::scoped_lock lock(ep_m); + ep = std::current_exception(); } - } catch (...) { - std::scoped_lock lock(ep_m); - ep = std::current_exception(); - } - }; + }; - std::vector ls_threads; - ls_threads.reserve(nb_threads); + std::vector solving_threads; + solving_threads.reserve(nb_threads); - for (const auto& sol_ranks : thread_ranks) { - if (!sol_ranks.empty()) { - ls_threads.emplace_back(run_ls, sol_ranks); + for (const auto& param_ranks : thread_ranks) { + if (!param_ranks.empty()) { + solving_threads.emplace_back(run_solving, param_ranks); } } - for (auto& t : ls_threads) { + for (auto& t : solving_threads) { t.join(); } @@ -268,20 +306,21 @@ class VRP { } #ifdef LOG_LS_OPERATORS - utils::log_LS_operators(ls_stats); + utils::log_LS_operators(context.ls_stats); #endif #ifdef LOG_LS - io::write_LS_logs_to_json(ls_dumps); + io::write_LS_logs_to_json(context.ls_dumps); #endif - auto best_indic = - std::min_element(sol_indicators.cbegin(), sol_indicators.cend()); + auto best_indic = std::min_element(context.sol_indicators.cbegin(), + context.sol_indicators.cend()); - return utils::format_solution(_input, - solutions[std::distance(sol_indicators - .cbegin(), - best_indic)]); + return utils:: + format_solution(_input, + context.solutions[std::distance(context.sol_indicators + .cbegin(), + best_indic)]); } public: @@ -290,9 +329,9 @@ class VRP { virtual ~VRP(); virtual Solution - solve(unsigned nb_searches, - unsigned depth, - unsigned nb_threads, + solve(const unsigned nb_searches, + const unsigned depth, + const unsigned nb_threads, const Timeout& timeout, const std::vector& h_param) const = 0; }; diff --git a/src/problems/vrptw/vrptw.cpp b/src/problems/vrptw/vrptw.cpp index 174aae3dd..f051bb650 100644 --- a/src/problems/vrptw/vrptw.cpp +++ b/src/problems/vrptw/vrptw.cpp @@ -142,9 +142,9 @@ const std::vector VRPTW::heterogeneous_parameters = VRPTW::VRPTW(const Input& input) : VRP(input) { } -Solution VRPTW::solve(unsigned nb_searches, - unsigned depth, - unsigned nb_threads, +Solution VRPTW::solve(const unsigned nb_searches, + const unsigned depth, + const unsigned nb_threads, const Timeout& timeout, const std::vector& h_param) const { return VRP::solve(nb_searches, diff --git a/src/problems/vrptw/vrptw.h b/src/problems/vrptw/vrptw.h index fc6557dab..f7f98cddc 100644 --- a/src/problems/vrptw/vrptw.h +++ b/src/problems/vrptw/vrptw.h @@ -23,9 +23,9 @@ class VRPTW : public VRP { explicit VRPTW(const Input& input); Solution - solve(unsigned nb_searches, - unsigned depth, - unsigned nb_threads, + solve(const unsigned nb_searches, + const unsigned depth, + const unsigned nb_threads, const Timeout& timeout, const std::vector& h_param) const override; }; diff --git a/src/structures/vroom/input/input.cpp b/src/structures/vroom/input/input.cpp index 74df05fdb..1152580ca 100644 --- a/src/structures/vroom/input/input.cpp +++ b/src/structures/vroom/input/input.cpp @@ -1118,8 +1118,8 @@ std::unique_ptr Input::get_problem() const { return std::make_unique(*this); } -Solution Input::solve(unsigned exploration_level, - unsigned nb_thread, +Solution Input::solve(const unsigned exploration_level, + const unsigned nb_thread, const Timeout& timeout, const std::vector& h_param) { return solve(utils::get_nb_searches(exploration_level), @@ -1129,9 +1129,9 @@ Solution Input::solve(unsigned exploration_level, h_param); } -Solution Input::solve(unsigned nb_searches, - unsigned depth, - unsigned nb_thread, +Solution Input::solve(const unsigned nb_searches, + const unsigned depth, + const unsigned nb_thread, const Timeout& timeout, const std::vector& h_param) { run_basic_checks(); diff --git a/src/structures/vroom/input/input.h b/src/structures/vroom/input/input.h index c7122e2e8..93ee31a62 100644 --- a/src/structures/vroom/input/input.h +++ b/src/structures/vroom/input/input.h @@ -193,17 +193,17 @@ class Input { // Returns true iff both vehicles have common job candidates. bool vehicle_ok_with_vehicle(Index v1_index, Index v2_index) const; - Solution solve(unsigned nb_searches, - unsigned depth, - unsigned nb_thread, + Solution solve(const unsigned nb_searches, + const unsigned depth, + const unsigned nb_thread, const Timeout& timeout = Timeout(), const std::vector& h_param = std::vector()); // Overload designed to expose the same interface as the `-x` // command-line flag for out-of-the-box setup of exploration level. - Solution solve(unsigned exploration_level, - unsigned nb_thread, + Solution solve(const unsigned exploration_level, + const unsigned nb_thread, const Timeout& timeout = Timeout(), const std::vector& h_param = std::vector()); diff --git a/src/utils/output_json.cpp b/src/utils/output_json.cpp index e417b5841..902586a73 100644 --- a/src/utils/output_json.cpp +++ b/src/utils/output_json.cpp @@ -432,6 +432,9 @@ rapidjson::Value to_json(const std::vector& steps, case START: event = "Start"; break; + case HEURISTIC: + event = "Heuristic"; + break; case OPERATOR: event = OPERATOR_NAMES[step.operator_name]; break; @@ -456,14 +459,16 @@ rapidjson::Value to_json(const std::vector& steps, json_step.AddMember("event", rapidjson::Value(), allocator); json_step["event"].SetString(event.c_str(), event.size(), allocator); - rapidjson::Value json_score(rapidjson::kObjectType); - json_score.AddMember("priority", step.indicators.priority_sum, allocator); - json_score.AddMember("assigned", step.indicators.assigned, allocator); - json_score.AddMember("cost", - utils::scale_to_user_cost(step.indicators.eval.cost), - allocator); + if (step.event != ls::log::EVENT::START) { + rapidjson::Value json_score(rapidjson::kObjectType); + json_score.AddMember("priority", step.indicators.priority_sum, allocator); + json_score.AddMember("assigned", step.indicators.assigned, allocator); + json_score.AddMember("cost", + utils::scale_to_user_cost(step.indicators.eval.cost), + allocator); - json_step.AddMember("score", json_score, allocator); + json_step.AddMember("score", json_score, allocator); + } if (step.solution.has_value()) { rapidjson::Value step_solution(rapidjson::kObjectType);