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..4752aa5b1 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -8,20 +8,158 @@ 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 route_data = check_route_steps(input); + + if (!route_data.job_ranks.empty()) { + // Proceed with updating current route and all amounts. + set_route(input, route_data.job_ranks); + } + } +} + +InitRouteData RawRoute::check_route_steps(const Input& input) const { + // 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. + InitRouteData route_data(vehicle.steps.size()); + route_data.single_jobs_deliveries = + utils::get_single_jobs_deliveries(input, vehicle.steps); + + if (!(route_data.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 = route_data.single_jobs_deliveries; + Eval eval_sum; + std::optional previous_index; + if (vehicle.has_start()) { + previous_index = vehicle.start.value().index(); + } + + // Initialize first break count slots. + route_data.breaks_at_rank.push_back(0); + route_data.breaks_counts.push_back(0); + + std::unordered_set expected_delivery_ranks; + for (const auto& step : vehicle.steps) { + // Filter for real jobs while populating break data. + if (step.type != STEP_TYPE::JOB) { + if (step.type == STEP_TYPE::BREAK) { + route_data.breaks_at_rank.back() += 1; + route_data.breaks_counts.back() += 1; + } + continue; + } + + const auto job_rank = step.rank; + const auto& job = input.jobs[job_rank]; + route_data.job_ranks.push_back(job_rank); + route_data.breaks_at_rank.push_back(0); + route_data.breaks_counts.push_back(route_data.breaks_counts.back()); + + 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)); + } + } + assert(route_data.breaks_at_rank.size() == route_data.job_ranks.size() + 1); + assert(route_data.breaks_counts.size() == route_data.job_ranks.size() + 1); + + if (vehicle.has_end() && !route_data.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 < route_data.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 route_data; } 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..4056be1f8 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -15,6 +15,19 @@ All rights reserved (see LICENSE). namespace vroom { +struct InitRouteData { + std::vector job_ranks; + std::vector breaks_at_rank; + std::vector breaks_counts; + Amount single_jobs_deliveries; + + explicit InitRouteData(std::size_t steps_size) { + job_ranks.reserve(steps_size); + breaks_at_rank.reserve(steps_size + 1); + breaks_counts.reserve(steps_size + 1); + } +}; + class RawRoute { private: Amount _zero; @@ -53,6 +66,11 @@ class RawRoute { Amount _delivery_margin; Amount _pickup_margin; +protected: + // Throws if route for vehicle steps is invalid, else return useful + // info for further manual route setup. + InitRouteData check_route_steps(const Input& input) const; + public: Index v_rank; Index v_type; @@ -62,7 +80,9 @@ class RawRoute { std::vector route; - RawRoute(const Input& input, Index i, unsigned amount_size); + RawRoute(const Input& input, Index v); + + virtual 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..6e0b59a1f 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,317 @@ 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()) { + // Start by checking validity from a RawRoute perspective, making + // sure route is OK with regard to capacity, max_travel_time, + // max_tasks, precedence and skills constraints. + auto route_data = check_route_steps(input); + + if (vehicle.breaks.empty() || + route_data.breaks_counts.back() != vehicle.breaks.size()) { + // Vehicle has no break or not all breaks are provided in vehicle + // steps. In this case we do not account for user-provided breaks + // at all and check if route is OK for TW constraints based on our + // default break assignment heuristic. + this->populate_from_steps_with_break_heuristic(input, route_data); + } else { + // Try populating object data using user-provided breaks ordering. + this->populate_from_steps_with_breaks(input, route_data); + } + } +} + +void TWRoute::populate_from_steps_with_break_heuristic( + const Input& input, + const InitRouteData& route_data) { + if (!route_data.job_ranks.empty()) { + if (this->is_valid_addition_for_tw(input, + route_data.single_jobs_deliveries, + route_data.job_ranks.begin(), + route_data.job_ranks.end(), + 0, + 0)) { + this->replace(input, + route_data.single_jobs_deliveries, + route_data.job_ranks.begin(), + route_data.job_ranks.end(), + 0, + 0); + } else { + throw InputException(std::format("Infeasible route for vehicle {}.", + input.vehicles[v_rank].id)); + } + } +} + +void TWRoute::populate_from_steps_with_breaks(const Input& input, + const InitRouteData& route_data) { + if (route_data.job_ranks.empty()) { + // Default constructor already did all the break-related + // boilerplate. + return; + } + + const auto& v = input.vehicles[v_rank]; + assert(v.breaks.size() == route_data.breaks_counts.back()); + + const std::string error = + std::format("Infeasible route for vehicle {}.", v.id); + + // Handle parent RawRoute members. + this->set_route(input, route_data.job_ranks); + assert(route_data.job_ranks.size() == route.size()); + + // We already have break counts figured out. + breaks_at_rank = route_data.breaks_at_rank; + breaks_counts = route_data.breaks_counts; + + // Update members to be populated below while checking for timing + // validity. + earliest = std::vector(route.size()); + latest = std::vector(route.size()); + action_time = std::vector(route.size()); + + break_earliest = std::vector(v.breaks.size()); + break_latest = std::vector(v.breaks.size()); + + // TODO + // fwd_smallest_breaks_load_margin = std::vector(v.breaks.size()); + // bwd_smallest_breaks_load_margin = std::vector(v.breaks.size()); + + Duration current_earliest = v_start; + + std::optional previous_index; + if (v.has_start()) { + previous_index = v.start.value().index(); + } + + // Go forward through all breaks and jobs. + for (Index i = 0; i < route.size(); ++i) { + const auto& job = input.jobs[route[i]]; + + // Update earliest dates and margins for breaks before current + // job. + Duration remaining_travel_time = + (previous_index.has_value()) + ? v.duration(previous_index.value(), job.index()) + : 0; + previous_index = job.index(); + + assert(breaks_at_rank[i] <= breaks_counts[i]); + Index break_rank = breaks_counts[i] - breaks_at_rank[i]; + + for (Index r = 0; r < breaks_at_rank[i]; ++r, ++break_rank) { + const auto& b = v.breaks[break_rank]; + + const auto b_tw = std::ranges::find_if(b.tws, [&](const auto& tw) { + return current_earliest <= tw.end; + }); + if (b_tw == b.tws.end()) { + throw InputException(error); + } + + if (current_earliest < b_tw->start) { + if (const auto margin = b_tw->start - current_earliest; + margin < remaining_travel_time) { + remaining_travel_time -= margin; + } else { + remaining_travel_time = 0; + } + + current_earliest = b_tw->start; + } + + break_earliest[break_rank] = current_earliest; + current_earliest += v.breaks[break_rank].service; + } + + // Back to the job after breaks. + current_earliest += remaining_travel_time; + + const auto j_tw = std::ranges::find_if(job.tws, [&](const auto& tw) { + return current_earliest <= tw.end; + }); + if (j_tw == job.tws.end()) { + throw InputException(error); + } + + current_earliest = std::max(current_earliest, j_tw->start); + earliest[i] = current_earliest; + + action_time[i] = job.services[v.type]; + if (!previous_index.has_value() || + (previous_index.value() != job.index())) { + action_time[i] += job.setups[v.type]; + } + + current_earliest += action_time[i]; + } + + // Handle remaining breaks before route end. + assert(!route.empty()); + Duration last_remaining_travel_time = + (v.has_end()) ? v.duration(input.jobs.back().index(), v.end.value().index()) + : 0; + + assert(breaks_at_rank[route.size()] <= breaks_counts[route.size()]); + Index last_break_rank = + breaks_counts[route.size()] - breaks_at_rank[route.size()]; + + for (Index r = 0; r < breaks_at_rank[route.size()]; ++r, ++last_break_rank) { + const auto& b = v.breaks[last_break_rank]; + + const auto b_tw = std::ranges::find_if(b.tws, [&](const auto& tw) { + return current_earliest <= tw.end; + }); + if (b_tw == b.tws.end()) { + throw InputException(error); + } + + if (current_earliest < b_tw->start) { + if (const auto margin = b_tw->start - current_earliest; + margin < last_remaining_travel_time) { + last_remaining_travel_time -= margin; + } else { + last_remaining_travel_time = 0; + } + + current_earliest = b_tw->start; + } + + break_earliest[last_break_rank] = current_earliest; + current_earliest += v.breaks[last_break_rank].service; + } + + // Consistency check with vehicle TW end. + earliest_end = current_earliest + last_remaining_travel_time; + if (v_end < earliest_end) { + throw InputException(error); + } + + // Go backward through all breaks and jobs. + auto current_latest = v_end; + + std::optional next_index; + if (v.has_end()) { + next_index = v.end.value().index(); + } + + for (Index next_i = route.size(); next_i > 0; --next_i) { + const auto& previous_j = input.jobs[route[next_i - 1]]; + Duration remaining_travel_time = + (next_index.has_value()) + ? v.duration(previous_j.index(), next_index.value()) + : 0; + next_index = previous_j.index(); + + // Update latest dates and margins for breaks. + assert(breaks_at_rank[next_i] <= breaks_counts[next_i]); + Index break_rank = breaks_counts[next_i]; + + for (Index r = 0; r < breaks_at_rank[next_i]; ++r) { + --break_rank; + + const auto& b = v.breaks[break_rank]; + if (current_latest < b.service) { + throw InputException(error); + } + current_latest -= b.service; + + const auto b_tw = + std::find_if(b.tws.rbegin(), b.tws.rend(), [&](const auto& tw) { + return tw.start <= current_latest; + }); + if (b_tw == b.tws.rend()) { + throw InputException(error); + } + + if (b_tw->end < current_latest) { + if (const auto margin = current_latest - b_tw->end; + margin < remaining_travel_time) { + remaining_travel_time -= margin; + } else { + remaining_travel_time = 0; + } + + current_latest = b_tw->end; + } + + break_latest[break_rank] = current_latest; + } + + // Back to the job after breaks. + auto gap = action_time[next_i - 1] + remaining_travel_time; + if (current_latest < gap) { + throw InputException(error); + } + current_latest -= gap; + + const auto j_tw = + std::find_if(previous_j.tws.rbegin(), + previous_j.tws.rend(), + [&](const auto& tw) { return tw.start <= current_latest; }); + if (j_tw == previous_j.tws.rend()) { + throw InputException(error); + } + + current_latest = std::min(current_latest, j_tw->end); + latest[next_i - 1] = current_latest; + + if (latest[next_i - 1] < earliest[next_i - 1]) { + throw InputException(error); + } + } + + // Update latest dates and margins for breaks right before the + // first job. + Duration first_remaining_travel_time = + (v.has_start()) + ? v.duration(v.end.value().index(), input.jobs.front().index()) + : 0; + + assert(breaks_at_rank[0] <= breaks_counts[0]); + Index break_rank = breaks_counts[0]; + + for (Index r = 0; r < breaks_at_rank[0]; ++r) { + --break_rank; + const auto& b = v.breaks[break_rank]; + + if (current_latest < b.service) { + throw InputException(error); + } + current_latest -= b.service; + + const auto b_tw = + std::find_if(b.tws.rbegin(), b.tws.rend(), [&](const auto& tw) { + return tw.start <= current_latest; + }); + if (b_tw == b.tws.rend()) { + throw InputException(error); + } + + if (b_tw->end < current_latest) { + if (const auto margin = current_latest - b_tw->end; + margin < first_remaining_travel_time) { + first_remaining_travel_time -= margin; + } else { + first_remaining_travel_time = 0; + } + current_latest = b_tw->end; + } + + break_latest[break_rank] = current_latest; + } + + // Consistency check with vehicle TW start. + if (current_latest < v_start + first_remaining_travel_time) { + throw InputException(error); + } +} + 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..685a84cd8 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -73,6 +73,13 @@ class TWRoute : public RawRoute { void fwd_update_breaks_load_margin_from(const Input& input, Index rank); void bwd_update_breaks_load_margin_from(const Input& input, Index rank); + void + populate_from_steps_with_break_heuristic(const Input& input, + const InitRouteData& route_data); + + void populate_from_steps_with_breaks(const Input& input, + const InitRouteData& route_data); + // Define global policy wrt job/break respective insertion rule. OrderChoice order_choice(const Input& input, Index job_rank, @@ -120,7 +127,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) override; // 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(