diff --git a/src/algorithms/heuristics/heuristics.cpp b/src/algorithms/heuristics/heuristics.cpp index a583d51f7..9140513d6 100644 --- a/src/algorithms/heuristics/heuristics.cpp +++ b/src/algorithms/heuristics/heuristics.cpp @@ -711,157 +711,6 @@ Eval dynamic_vehicle_choice(const Input& input, return sol_eval; } -template -void set_route(const Input& input, - Route& route, - std::unordered_set& assigned) { - assert(route.empty()); - const auto& vehicle = input.vehicles[route.v_rank]; - - // Startup load is the sum of deliveries for (single) jobs. - Amount single_jobs_deliveries(input.zero_amount()); - for (const auto& step : vehicle.steps) { - if (step.type == STEP_TYPE::JOB) { - assert(step.job_type.has_value()); - - if (step.job_type.value() == JOB_TYPE::SINGLE) { - single_jobs_deliveries += input.jobs[step.rank].delivery; - } - } - } - if (!(single_jobs_deliveries <= vehicle.capacity)) { - throw InputException( - std::format("Route over capacity for vehicle {}.", vehicle.id)); - } - - // Track load and travel time during the route for validity. - Amount current_load = single_jobs_deliveries; - Eval eval_sum; - std::optional previous_index; - if (vehicle.has_start()) { - previous_index = vehicle.start.value().index(); - } - - std::vector job_ranks; - job_ranks.reserve(vehicle.steps.size()); - std::unordered_set expected_delivery_ranks; - for (const auto& step : vehicle.steps) { - if (step.type != STEP_TYPE::JOB) { - continue; - } - - const auto job_rank = step.rank; - const auto& job = input.jobs[job_rank]; - job_ranks.push_back(job_rank); - - assert(!assigned.contains(job_rank)); - assigned.insert(job_rank); - - if (!input.vehicle_ok_with_job(route.v_rank, job_rank)) { - throw InputException( - std::format("Missing skill or step out of reach for vehicle {} and " - "job {}.", - vehicle.id, - job.id)); - } - - // Update current travel time. - if (previous_index.has_value()) { - eval_sum += vehicle.eval(previous_index.value(), job.index()); - } - previous_index = job.index(); - - // Handle load. - assert(step.job_type.has_value()); - switch (step.job_type.value()) { - case JOB_TYPE::SINGLE: { - current_load += job.pickup; - current_load -= job.delivery; - break; - } - case JOB_TYPE::PICKUP: { - expected_delivery_ranks.insert(job_rank + 1); - - current_load += job.pickup; - break; - } - case JOB_TYPE::DELIVERY: { - auto search = expected_delivery_ranks.find(job_rank); - if (search == expected_delivery_ranks.end()) { - throw InputException( - std::format("Invalid shipment in route for vehicle {}.", vehicle.id)); - } - expected_delivery_ranks.erase(search); - - current_load -= job.delivery; - break; - } - default: - assert(false); - } - - // Check validity after this step wrt capacity. - if (!(current_load <= vehicle.capacity)) { - throw InputException( - std::format("Route over capacity for vehicle {}.", vehicle.id)); - } - } - - if (vehicle.has_end() && !job_ranks.empty()) { - // Update with last route leg. - assert(previous_index.has_value()); - eval_sum += - vehicle.eval(previous_index.value(), vehicle.end.value().index()); - } - if (!vehicle.ok_for_travel_time(eval_sum.duration)) { - throw InputException( - std::format("Route over max_travel_time for vehicle {}.", vehicle.id)); - } - if (!vehicle.ok_for_distance(eval_sum.distance)) { - throw InputException( - std::format("Route over max_distance for vehicle {}.", vehicle.id)); - } - - if (vehicle.max_tasks < job_ranks.size()) { - throw InputException( - std::format("Too many tasks for vehicle {}.", vehicle.id)); - } - - if (!expected_delivery_ranks.empty()) { - throw InputException( - std::format("Invalid shipment in route for vehicle {}.", vehicle.id)); - } - - // Now route is OK with regard to capacity, max_travel_time, - // max_tasks, precedence and skills constraints. - if (!job_ranks.empty()) { - if (!route.is_valid_addition_for_tw(input, - single_jobs_deliveries, - job_ranks.begin(), - job_ranks.end(), - 0, - 0)) { - throw InputException( - std::format("Infeasible route for vehicle {}.", vehicle.id)); - } - - route.replace(input, - single_jobs_deliveries, - job_ranks.begin(), - job_ranks.end(), - 0, - 0); - } -} - -template -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); }); -} - using RawSolution = std::vector; using TWSolution = std::vector; @@ -881,10 +730,6 @@ template Eval dynamic_vehicle_choice(const Input& input, double lambda, SORT sort); -template void set_initial_routes(const Input& input, - RawSolution& routes, - std::unordered_set& assigned); - template Eval basic(const Input& input, TWSolution& routes, std::set unassigned, @@ -901,8 +746,4 @@ template Eval dynamic_vehicle_choice(const Input& input, double lambda, SORT sort); -template void set_initial_routes(const Input& input, - TWSolution& routes, - std::unordered_set& assigned); - } // namespace vroom::heuristics diff --git a/src/algorithms/local_search/route_split_utils.h b/src/algorithms/local_search/route_split_utils.h index fcb3ff96f..09f0cc1da 100644 --- a/src/algorithms/local_search/route_split_utils.h +++ b/src/algorithms/local_search/route_split_utils.h @@ -42,7 +42,7 @@ compute_best_route_split_choice(const Input& input, std::vector empty_routes; empty_routes.reserve(empty_route_ranks.size()); for (auto v : empty_route_ranks) { - empty_routes.emplace_back(input, v, input.zero_amount().size()); + empty_routes.emplace_back(input, v); } for (Index r = 1; r < source.size(); ++r) { diff --git a/src/problems/cvrp/cvrp.cpp b/src/problems/cvrp/cvrp.cpp index 9666ce0a2..69f283cc4 100644 --- a/src/problems/cvrp/cvrp.cpp +++ b/src/problems/cvrp/cvrp.cpp @@ -157,7 +157,7 @@ Solution CVRP::solve(const unsigned nb_searches, const TSP p(_input, std::move(job_ranks), 0); - RawRoute r(_input, 0, 0); + RawRoute r(_input, 0); r.set_route(_input, p.raw_solve(nb_threads, timeout)); return utils::format_solution(_input, {r}); diff --git a/src/problems/cvrp/operators/tsp_fix.cpp b/src/problems/cvrp/operators/tsp_fix.cpp index a9254eefa..854f85f2c 100644 --- a/src/problems/cvrp/operators/tsp_fix.cpp +++ b/src/problems/cvrp/operators/tsp_fix.cpp @@ -45,7 +45,7 @@ bool TSPFix::is_valid() { bool valid = is_valid_for_source_range_bounds(); if (valid) { - const RawRoute route(_input, s_vehicle, _input.zero_amount().size()); + const RawRoute route(_input, s_vehicle); valid = route.is_valid_addition_for_capacity_inclusion(_input, _s_delivery, diff --git a/src/problems/tsp/tsp.cpp b/src/problems/tsp/tsp.cpp index 96816b0ad..0c83cf98e 100644 --- a/src/problems/tsp/tsp.cpp +++ b/src/problems/tsp/tsp.cpp @@ -298,7 +298,7 @@ Solution TSP::solve(unsigned, unsigned, unsigned nb_threads, const Timeout& timeout) const { - RawRoute r(_input, 0, 0); + RawRoute r(_input, 0); r.set_route(_input, raw_solve(nb_threads, timeout)); return utils::format_solution(_input, {r}); } diff --git a/src/problems/vrp.h b/src/problems/vrp.h index c21e0d15f..0a921693a 100644 --- a/src/problems/vrp.h +++ b/src/problems/vrp.h @@ -26,25 +26,18 @@ All rights reserved (see LICENSE). namespace vroom { -template -std::vector set_init_sol(const Input& input, - std::unordered_set& init_assigned) { +template std::vector set_init_sol(const Input& input) { 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()); - } - - if (input.has_initial_routes()) { - heuristics::set_initial_routes(input, init_sol, init_assigned); + init_sol.emplace_back(input, v).populate_from_steps(input); } return init_sol; } template struct SolvingContext { - std::unordered_set init_assigned; const std::vector init_sol; std::set unassigned; std::vector vehicles_ranks; @@ -55,15 +48,21 @@ template struct SolvingContext { std::mutex heuristic_indicators_m; SolvingContext(const Input& input, unsigned nb_searches) - : init_sol(set_init_sol(input, init_assigned)), + : init_sol(set_init_sol(input)), vehicles_ranks(input.vehicles.size()), solutions(nb_searches, init_sol), sol_indicators(nb_searches) { // Deduce unassigned jobs from initial solution. + std::unordered_set init_assigned; + for (const auto& r : init_sol) { + for (const Index i : r.route) { + init_assigned.insert(i); + } + } std::ranges::copy_if(std::views::iota(0u, input.jobs.size()), std::inserter(unassigned, unassigned.begin()), - [this](const Index j) { + [&](const Index j) { return !init_assigned.contains(j); }); diff --git a/src/structures/vroom/input/input.cpp b/src/structures/vroom/input/input.cpp index 048e2348f..6376e7f52 100644 --- a/src/structures/vroom/input/input.cpp +++ b/src/structures/vroom/input/input.cpp @@ -465,10 +465,6 @@ bool Input::has_homogeneous_costs() const { return _homogeneous_costs; } -bool Input::has_initial_routes() const { - return _has_initial_routes; -} - bool Input::vehicle_ok_with_vehicle(Index v1_index, Index v2_index) const { return _vehicle_to_vehicle_compatibility[v1_index][v2_index]; } @@ -551,7 +547,7 @@ void Input::set_extra_compatibility() { compatible_vehicles_for_job = std::vector>(jobs.size()); for (std::size_t v = 0; v < vehicles.size(); ++v) { - const TWRoute empty_route(*this, v, _zero.size()); + const TWRoute empty_route(*this, v); for (Index j = 0; j < jobs.size(); ++j) { if (!_vehicle_to_job_compatibility[v][j]) { continue; diff --git a/src/structures/vroom/input/input.h b/src/structures/vroom/input/input.h index ad0e96ce3..53501db65 100644 --- a/src/structures/vroom/input/input.h +++ b/src/structures/vroom/input/input.h @@ -196,8 +196,6 @@ class Input { bool has_homogeneous_costs() const; - bool has_initial_routes() const; - bool vehicle_ok_with_job(size_t v_index, size_t j_index) const { return static_cast(_vehicle_to_job_compatibility[v_index][j_index]); } diff --git a/src/structures/vroom/raw_route.cpp b/src/structures/vroom/raw_route.cpp index b7fe08656..81e4c7817 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -8,20 +8,145 @@ All rights reserved (see LICENSE). */ #include "structures/vroom/raw_route.h" +#include "utils/helpers.h" namespace vroom { -RawRoute::RawRoute(const Input& input, Index i, unsigned amount_size) - : _zero(amount_size), +RawRoute::RawRoute(const Input& input, Index v) + : _zero(input.get_amount_size()), _fwd_peaks(2, _zero), _bwd_peaks(2, _zero), - _delivery_margin(input.vehicles[i].capacity), - _pickup_margin(input.vehicles[i].capacity), - v_rank(i), - v_type(input.vehicles[i].type), - has_start(input.vehicles[i].has_start()), - has_end(input.vehicles[i].has_end()), - capacity(input.vehicles[i].capacity) { + _delivery_margin(input.vehicles[v].capacity), + _pickup_margin(input.vehicles[v].capacity), + v_rank(v), + v_type(input.vehicles[v].type), + has_start(input.vehicles[v].has_start()), + has_end(input.vehicles[v].has_end()), + capacity(input.vehicles[v].capacity) { +} + +void RawRoute::populate_from_steps(const Input& input) { + const auto& vehicle = input.vehicles[v_rank]; + if (!vehicle.steps.empty()) { + const auto job_ranks = check_route_steps(input); + + if (!job_ranks.empty()) { + // Proceed with updating current route and all amounts. + set_route(input, job_ranks); + } + } +} + +std::vector RawRoute::check_route_steps(const Input& input) { + // Check that provided route is OK with regard to capacity, + // max_travel_time, max_tasks, precedence and skills constraints. + const auto& vehicle = input.vehicles[v_rank]; + assert(!vehicle.steps.empty()); + + // Startup load is the sum of deliveries for (single) jobs. + const auto single_jobs_deliveries = + utils::get_single_jobs_deliveries(input, vehicle.steps); + if (!(single_jobs_deliveries <= vehicle.capacity)) { + throw InputException( + std::format("Route over capacity for vehicle {}.", vehicle.id)); + } + + // Track load and travel time during the route for validity. + Amount current_load = single_jobs_deliveries; + Eval eval_sum; + std::optional previous_index; + if (vehicle.has_start()) { + previous_index = vehicle.start.value().index(); + } + + std::vector job_ranks; + job_ranks.reserve(vehicle.steps.size()); + std::unordered_set expected_delivery_ranks; + for (const auto& step : vehicle.steps) { + if (step.type != STEP_TYPE::JOB) { + continue; + } + + const auto job_rank = step.rank; + const auto& job = input.jobs[job_rank]; + job_ranks.push_back(job_rank); + + if (!input.vehicle_ok_with_job(v_rank, job_rank)) { + throw InputException( + std::format("Missing skill or step out of reach for vehicle {} and " + "job {}.", + vehicle.id, + job.id)); + } + + // Update current travel time. + if (previous_index.has_value()) { + eval_sum += vehicle.eval(previous_index.value(), job.index()); + } + previous_index = job.index(); + + // Handle load. + assert(step.job_type.has_value()); + switch (step.job_type.value()) { + case JOB_TYPE::SINGLE: { + current_load += job.pickup; + current_load -= job.delivery; + break; + } + case JOB_TYPE::PICKUP: { + expected_delivery_ranks.insert(job_rank + 1); + + current_load += job.pickup; + break; + } + case JOB_TYPE::DELIVERY: { + auto search = expected_delivery_ranks.find(job_rank); + if (search == expected_delivery_ranks.end()) { + throw InputException( + std::format("Invalid shipment in route for vehicle {}.", vehicle.id)); + } + expected_delivery_ranks.erase(search); + + current_load -= job.delivery; + break; + } + default: + assert(false); + } + + // Check validity after this step wrt capacity. + if (!(current_load <= vehicle.capacity)) { + throw InputException( + std::format("Route over capacity for vehicle {}.", vehicle.id)); + } + } + + if (vehicle.has_end() && !job_ranks.empty()) { + // Update with last route leg. + assert(previous_index.has_value()); + eval_sum += + vehicle.eval(previous_index.value(), vehicle.end.value().index()); + } + if (!vehicle.ok_for_travel_time(eval_sum.duration)) { + throw InputException( + std::format("Route over max_travel_time for vehicle {}.", vehicle.id)); + } + if (!vehicle.ok_for_distance(eval_sum.distance)) { + throw InputException( + std::format("Route over max_distance for vehicle {}.", vehicle.id)); + } + + if (vehicle.max_tasks < job_ranks.size()) { + throw InputException( + std::format("Too many tasks for vehicle {}.", vehicle.id)); + } + + if (!expected_delivery_ranks.empty()) { + throw InputException( + std::format("Invalid shipment in route for vehicle {}.", vehicle.id)); + } + + return job_ranks; } void RawRoute::set_route(const Input& input, const std::vector& r) { diff --git a/src/structures/vroom/raw_route.h b/src/structures/vroom/raw_route.h index ec72657b2..f609082de 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -53,6 +53,11 @@ class RawRoute { Amount _delivery_margin; Amount _pickup_margin; +protected: + // Throws if route for vehicle steps is invalid, else return job + // ranks in current route. + std::vector check_route_steps(const Input& input); + public: Index v_rank; Index v_type; @@ -62,7 +67,9 @@ class RawRoute { std::vector route; - RawRoute(const Input& input, Index i, unsigned amount_size); + RawRoute(const Input& input, Index v); + + void populate_from_steps(const Input& input); void set_route(const Input& input, const std::vector& r); diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index 0739e672b..32bab5d86 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -14,8 +14,8 @@ All rights reserved (see LICENSE). namespace vroom { -TWRoute::TWRoute(const Input& input, Index v, unsigned amount_size) - : RawRoute(input, v, amount_size), +TWRoute::TWRoute(const Input& input, Index v) + : RawRoute(input, v), v_start(input.vehicles[v].tw.start), v_end(input.vehicles[v].tw.end), breaks_at_rank({static_cast(input.vehicles[v].breaks.size())}), @@ -32,6 +32,7 @@ TWRoute::TWRoute(const Input& input, Index v, unsigned amount_size) Duration previous_earliest = v_start; // Store smallest margin component-wise. + const auto amount_size = input.get_amount_size(); Amount fwd_smallest_margin = utils::max_amount(amount_size); Amount bwd_smallest_margin = utils::max_amount(amount_size); @@ -97,6 +98,44 @@ TWRoute::TWRoute(const Input& input, Index v, unsigned amount_size) } } +void TWRoute::populate_from_steps(const Input& input) { + const auto& vehicle = input.vehicles[v_rank]; + if (vehicle.steps.empty()) { + // Nothing to do. + return; + } + + // If vehicles steps are not empty, start by checking validity from + // a RawRoute perspective. + const auto job_ranks = check_route_steps(input); + + // Steps route is OK with regard to capacity, max_travel_time, + // max_tasks, precedence and skills constraints. + if (!job_ranks.empty()) { + // We want to first check if route is OK for TW constraints based + // on our default break assignment heuristic. + const auto single_jobs_deliveries = + utils::get_single_jobs_deliveries(input, vehicle.steps); + + if (this->is_valid_addition_for_tw(input, + single_jobs_deliveries, + job_ranks.begin(), + job_ranks.end(), + 0, + 0)) { + this->replace(input, + single_jobs_deliveries, + job_ranks.begin(), + job_ranks.end(), + 0, + 0); + } else { + throw InputException( + std::format("Infeasible route for vehicle {}.", vehicle.id)); + } + } +} + PreviousInfo TWRoute::previous_info(const Input& input, const Index job_rank, const Index rank) const { diff --git a/src/structures/vroom/tw_route.h b/src/structures/vroom/tw_route.h index e1f37ea6c..8337f4378 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -120,7 +120,9 @@ class TWRoute : public RawRoute { std::vector fwd_smallest_breaks_load_margin; std::vector bwd_smallest_breaks_load_margin; - TWRoute(const Input& input, Index v, unsigned amount_size); + TWRoute(const Input& input, Index v); + + void populate_from_steps(const Input& input); // Check validity for addition of job at job_rank in current route // at rank. diff --git a/src/utils/helpers.cpp b/src/utils/helpers.cpp index b49b14e0a..3ff351e32 100644 --- a/src/utils/helpers.cpp +++ b/src/utils/helpers.cpp @@ -28,6 +28,22 @@ Amount max_amount(std::size_t size) { return max; } +Amount get_single_jobs_deliveries(const Input& input, + const std::vector& steps) { + Amount single_jobs_deliveries(input.zero_amount()); + for (const auto& step : steps) { + if (step.type == STEP_TYPE::JOB) { + assert(step.job_type.has_value()); + + if (step.job_type.value() == JOB_TYPE::SINGLE) { + single_jobs_deliveries += input.jobs[step.rank].delivery; + } + } + } + + return single_jobs_deliveries; +} + Priority priority_sum_for_route(const Input& input, const std::vector& route) { return std::accumulate(route.begin(), diff --git a/src/utils/helpers.h b/src/utils/helpers.h index 1475bdb1f..d346c2f25 100644 --- a/src/utils/helpers.h +++ b/src/utils/helpers.h @@ -32,6 +32,9 @@ TimePoint now(); Amount max_amount(std::size_t size); +Amount get_single_jobs_deliveries(const Input& input, + const std::vector& steps); + inline UserCost add_without_overflow(UserCost a, UserCost b) { if (a > std::numeric_limits::max() - b) { throw InputException(