From 0aa4d62f144ba68555e993a07a6a362a3be084c5 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Tue, 21 Apr 2026 15:11:11 +0200 Subject: [PATCH 01/18] Pass steps to Route ctor in place of set_initial_routes logic. --- src/algorithms/heuristics/heuristics.cpp | 16 ---------------- src/problems/vrp.h | 15 ++++++++++----- src/structures/vroom/input/input.cpp | 4 ---- src/structures/vroom/input/input.h | 2 -- src/structures/vroom/raw_route.h | 6 ++++++ src/structures/vroom/tw_route.h | 6 ++++++ 6 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/algorithms/heuristics/heuristics.cpp b/src/algorithms/heuristics/heuristics.cpp index a583d51f7..38aee1bed 100644 --- a/src/algorithms/heuristics/heuristics.cpp +++ b/src/algorithms/heuristics/heuristics.cpp @@ -854,14 +854,6 @@ void set_route(const Input& input, } } -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 +873,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 +889,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/problems/vrp.h b/src/problems/vrp.h index c21e0d15f..786a320b1 100644 --- a/src/problems/vrp.h +++ b/src/problems/vrp.h @@ -33,11 +33,16 @@ std::vector set_init_sol(const Input& input, 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); + const auto& vehicle = input.vehicles[v]; + if (vehicle.steps.empty()) { + init_sol.emplace_back(input, v, input.zero_amount().size()); + } else { + init_sol.emplace_back(input, + v, + input.zero_amount().size(), + input.vehicles[v].steps, + init_assigned); + } } return init_sol; diff --git a/src/structures/vroom/input/input.cpp b/src/structures/vroom/input/input.cpp index 048e2348f..63313b1ab 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]; } 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.h b/src/structures/vroom/raw_route.h index ec72657b2..7945bbb00 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -64,6 +64,12 @@ class RawRoute { RawRoute(const Input& input, Index i, unsigned amount_size); + RawRoute(const Input& input, + Index i, + unsigned amount_size, + const std::vector& steps, + std::unordered_set& assigned); + void set_route(const Input& input, const std::vector& r); bool empty() const { diff --git a/src/structures/vroom/tw_route.h b/src/structures/vroom/tw_route.h index e1f37ea6c..2f23c6f5b 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -122,6 +122,12 @@ class TWRoute : public RawRoute { TWRoute(const Input& input, Index v, unsigned amount_size); + TWRoute(const Input& input, + Index i, + unsigned amount_size, + const std::vector& steps, + std::unordered_set& assigned); + // Check validity for addition of job at job_rank in current route // at rank. bool is_valid_addition_for_tw(const Input& input, From aa2c07492aad27a6a594454e2af422f587db5607 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Tue, 21 Apr 2026 15:59:36 +0200 Subject: [PATCH 02/18] Move relevant heuristics::set_route checks to new RawRoute ctor. --- src/algorithms/heuristics/heuristics.cpp | 115 ------------------ src/problems/vrp.h | 1 - src/structures/vroom/raw_route.cpp | 146 +++++++++++++++++++++-- src/structures/vroom/raw_route.h | 7 +- src/structures/vroom/tw_route.h | 1 - 5 files changed, 142 insertions(+), 128 deletions(-) diff --git a/src/algorithms/heuristics/heuristics.cpp b/src/algorithms/heuristics/heuristics.cpp index 38aee1bed..53027c7d1 100644 --- a/src/algorithms/heuristics/heuristics.cpp +++ b/src/algorithms/heuristics/heuristics.cpp @@ -716,121 +716,6 @@ 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. diff --git a/src/problems/vrp.h b/src/problems/vrp.h index 786a320b1..8e64426ef 100644 --- a/src/problems/vrp.h +++ b/src/problems/vrp.h @@ -40,7 +40,6 @@ std::vector set_init_sol(const Input& input, init_sol.emplace_back(input, v, input.zero_amount().size(), - input.vehicles[v].steps, init_assigned); } } diff --git a/src/structures/vroom/raw_route.cpp b/src/structures/vroom/raw_route.cpp index b7fe08656..3e3756794 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -11,17 +11,147 @@ All rights reserved (see LICENSE). namespace vroom { -RawRoute::RawRoute(const Input& input, Index i, unsigned amount_size) +RawRoute::RawRoute(const Input& input, Index v, unsigned amount_size) : _zero(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) { +} + +RawRoute::RawRoute(const Input& input, + Index v, + unsigned amount_size, + std::unordered_set& assigned) + : RawRoute(input, v, amount_size) { + // 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]; + assert(!vehicle.steps.empty()); + + // 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(v, 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)); + } + + if (!job_ranks.empty()) { + // Proceed with updating current route and all amounts. + set_route(input, 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 7945bbb00..9ae33a065 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -62,12 +62,13 @@ class RawRoute { std::vector route; - RawRoute(const Input& input, Index i, unsigned amount_size); + // Used to create empty route. + RawRoute(const Input& input, Index v, unsigned amount_size); + // Used to create route populated from matching vehicle steps. RawRoute(const Input& input, - Index i, + Index v, unsigned amount_size, - const std::vector& steps, std::unordered_set& assigned); void set_route(const Input& input, const std::vector& r); diff --git a/src/structures/vroom/tw_route.h b/src/structures/vroom/tw_route.h index 2f23c6f5b..7e3fac56b 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -125,7 +125,6 @@ class TWRoute : public RawRoute { TWRoute(const Input& input, Index i, unsigned amount_size, - const std::vector& steps, std::unordered_set& assigned); // Check validity for addition of job at job_rank in current route From 1fbdbf3ef69de0b2ef60ea3344c78d07c1849b83 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Tue, 21 Apr 2026 16:38:38 +0200 Subject: [PATCH 03/18] Simplify ctor logic by putting checks in dedicated member function. --- src/problems/vrp.h | 17 ++++------------- src/structures/vroom/raw_route.cpp | 27 +++++++++++++-------------- src/structures/vroom/raw_route.h | 11 ++++------- src/structures/vroom/tw_route.h | 5 ----- 4 files changed, 21 insertions(+), 39 deletions(-) diff --git a/src/problems/vrp.h b/src/problems/vrp.h index 8e64426ef..e7ecb0cba 100644 --- a/src/problems/vrp.h +++ b/src/problems/vrp.h @@ -26,28 +26,19 @@ 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) { - const auto& vehicle = input.vehicles[v]; - if (vehicle.steps.empty()) { - init_sol.emplace_back(input, v, input.zero_amount().size()); - } else { - init_sol.emplace_back(input, - v, - input.zero_amount().size(), - init_assigned); - } + init_sol.emplace_back(input, v, input.zero_amount().size()); } return init_sol; } template struct SolvingContext { + // TODO get rid of init_assigned and compute "unassigned" directly. std::unordered_set init_assigned; const std::vector init_sol; std::set unassigned; @@ -59,7 +50,7 @@ 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) { diff --git a/src/structures/vroom/raw_route.cpp b/src/structures/vroom/raw_route.cpp index 3e3756794..44ea51ded 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -22,16 +22,21 @@ RawRoute::RawRoute(const Input& input, Index v, unsigned amount_size) has_start(input.vehicles[v].has_start()), has_end(input.vehicles[v].has_end()), capacity(input.vehicles[v].capacity) { + const auto& vehicle = input.vehicles[v]; + 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); + } + } } -RawRoute::RawRoute(const Input& input, - Index v, - unsigned amount_size, - std::unordered_set& assigned) - : RawRoute(input, v, amount_size) { +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]; + const auto& vehicle = input.vehicles[v_rank]; assert(!vehicle.steps.empty()); // Startup load is the sum of deliveries for (single) jobs. @@ -70,10 +75,7 @@ RawRoute::RawRoute(const Input& input, 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(v, 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 {}.", @@ -148,10 +150,7 @@ RawRoute::RawRoute(const Input& input, std::format("Invalid shipment in route for vehicle {}.", vehicle.id)); } - if (!job_ranks.empty()) { - // Proceed with updating current route and all amounts. - set_route(input, job_ranks); - } + 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 9ae33a065..1a25d46a3 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -53,6 +53,10 @@ class RawRoute { Amount _delivery_margin; Amount _pickup_margin; + // 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,15 +66,8 @@ class RawRoute { std::vector route; - // Used to create empty route. RawRoute(const Input& input, Index v, unsigned amount_size); - // Used to create route populated from matching vehicle steps. - RawRoute(const Input& input, - Index v, - unsigned amount_size, - std::unordered_set& assigned); - void set_route(const Input& input, const std::vector& r); bool empty() const { diff --git a/src/structures/vroom/tw_route.h b/src/structures/vroom/tw_route.h index 7e3fac56b..e1f37ea6c 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -122,11 +122,6 @@ class TWRoute : public RawRoute { TWRoute(const Input& input, Index v, unsigned amount_size); - TWRoute(const Input& input, - Index i, - unsigned amount_size, - std::unordered_set& assigned); - // Check validity for addition of job at job_rank in current route // at rank. bool is_valid_addition_for_tw(const Input& input, From 982e86448939eb6fbcae0edc8dc7f29cfce2f17b Mon Sep 17 00:00:00 2001 From: jcoupey Date: Tue, 21 Apr 2026 16:49:24 +0200 Subject: [PATCH 04/18] Move initializing empty route break stuff to dedicated TWRoute private member function. --- src/structures/vroom/tw_route.cpp | 10 ++++++++-- src/structures/vroom/tw_route.h | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index 0739e672b..32d14d677 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -24,14 +24,20 @@ TWRoute::TWRoute(const Input& input, Index v, unsigned amount_size) break_latest(input.vehicles[v].breaks.size()), fwd_smallest_breaks_load_margin(input.vehicles[v].breaks.size()), bwd_smallest_breaks_load_margin(input.vehicles[v].breaks.size()) { + init_break_setup(input); +} + +void TWRoute::init_break_setup(const Input& input) { const std::string break_error = - std::format("Inconsistent breaks for vehicle {}.", input.vehicles[v].id); + std::format("Inconsistent breaks for vehicle {}.", + input.vehicles[v_rank].id); - const auto& breaks = input.vehicles[v].breaks; + const auto& breaks = input.vehicles[v_rank].breaks; Duration previous_earliest = v_start; // Store smallest margin component-wise. + const auto amount_size = input.zero_amount().size(); Amount fwd_smallest_margin = utils::max_amount(amount_size); Amount bwd_smallest_margin = utils::max_amount(amount_size); diff --git a/src/structures/vroom/tw_route.h b/src/structures/vroom/tw_route.h index e1f37ea6c..f5a6d0b34 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -58,6 +58,8 @@ struct OrderChoice { class TWRoute : public RawRoute { private: + void init_break_setup(const Input& input); + PreviousInfo previous_info(const Input& input, Index job_rank, Index rank) const; From ee4a5318a5dbaf61ba2c0264ce2fa36e05d70118 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Tue, 21 Apr 2026 17:19:03 +0200 Subject: [PATCH 05/18] Compute single jobs deliveries in a helper function. --- src/structures/vroom/raw_route.cpp | 13 +++---------- src/utils/helpers.cpp | 16 ++++++++++++++++ src/utils/helpers.h | 3 +++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/structures/vroom/raw_route.cpp b/src/structures/vroom/raw_route.cpp index 44ea51ded..a559bf7c3 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -8,6 +8,7 @@ All rights reserved (see LICENSE). */ #include "structures/vroom/raw_route.h" +#include "utils/helpers.h" namespace vroom { @@ -40,16 +41,8 @@ std::vector RawRoute::check_route_steps(const Input& input) { assert(!vehicle.steps.empty()); // 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; - } - } - } + 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)); 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( From 27ae26657cf30f0ebaa4b593c1903940cb75da0d Mon Sep 17 00:00:00 2001 From: jcoupey Date: Tue, 21 Apr 2026 17:28:28 +0200 Subject: [PATCH 06/18] Adjust TWRoute ctor to reproduce current behavior wrt TW checks. --- src/algorithms/heuristics/heuristics.cpp | 28 --------------------- src/structures/vroom/tw_route.cpp | 32 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/algorithms/heuristics/heuristics.cpp b/src/algorithms/heuristics/heuristics.cpp index 53027c7d1..9140513d6 100644 --- a/src/algorithms/heuristics/heuristics.cpp +++ b/src/algorithms/heuristics/heuristics.cpp @@ -711,34 +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()); - - // 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); - } -} - using RawSolution = std::vector; using TWSolution = std::vector; diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index 32d14d677..b5a5e1deb 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -24,7 +24,39 @@ TWRoute::TWRoute(const Input& input, Index v, unsigned amount_size) break_latest(input.vehicles[v].breaks.size()), fwd_smallest_breaks_load_margin(input.vehicles[v].breaks.size()), bwd_smallest_breaks_load_margin(input.vehicles[v].breaks.size()) { + // Populate break data for empty route. init_break_setup(input); + + const auto& vehicle = input.vehicles[v]; + if (!vehicle.steps.empty()) { + // As per the call to RawRoute above, provided route is OK with + // regard to capacity, max_travel_time, max_tasks, precedence and + // skills constraints AND this->route has been updated. + + if (!route.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, + route.begin(), + route.end(), + 0, + route.size())) { + this->replace(input, + single_jobs_deliveries, + route.begin(), + route.end(), + 0, + route.size()); + } else { + throw InputException( + std::format("Infeasible route for vehicle {}.", vehicle.id)); + } + } + } } void TWRoute::init_break_setup(const Input& input) { From 90948638242c514a94792cc788ccf1fa88eede0b Mon Sep 17 00:00:00 2001 From: jcoupey Date: Tue, 21 Apr 2026 17:28:56 +0200 Subject: [PATCH 07/18] Directly compute unassigned jobs from init solution. --- src/problems/vrp.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/problems/vrp.h b/src/problems/vrp.h index e7ecb0cba..98fe3890b 100644 --- a/src/problems/vrp.h +++ b/src/problems/vrp.h @@ -38,8 +38,6 @@ template std::vector set_init_sol(const Input& input) { } template struct SolvingContext { - // TODO get rid of init_assigned and compute "unassigned" directly. - std::unordered_set init_assigned; const std::vector init_sol; std::set unassigned; std::vector vehicles_ranks; @@ -56,9 +54,15 @@ template struct SolvingContext { 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); }); From 2f197b5c0b11700285a189b840fe899cb3a8797c Mon Sep 17 00:00:00 2001 From: jcoupey Date: Tue, 21 Apr 2026 17:41:40 +0200 Subject: [PATCH 08/18] Remove unecessary amount size arg in route ctors. --- src/algorithms/local_search/route_split_utils.h | 2 +- src/problems/cvrp/cvrp.cpp | 2 +- src/problems/cvrp/operators/tsp_fix.cpp | 2 +- src/problems/tsp/tsp.cpp | 2 +- src/problems/vrp.h | 2 +- src/structures/vroom/input/input.cpp | 2 +- src/structures/vroom/raw_route.cpp | 4 ++-- src/structures/vroom/raw_route.h | 2 +- src/structures/vroom/tw_route.cpp | 6 +++--- src/structures/vroom/tw_route.h | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) 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 98fe3890b..bb7926c4f 100644 --- a/src/problems/vrp.h +++ b/src/problems/vrp.h @@ -31,7 +31,7 @@ template std::vector set_init_sol(const Input& input) { 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()); + init_sol.emplace_back(input, v); } return init_sol; diff --git a/src/structures/vroom/input/input.cpp b/src/structures/vroom/input/input.cpp index 63313b1ab..6376e7f52 100644 --- a/src/structures/vroom/input/input.cpp +++ b/src/structures/vroom/input/input.cpp @@ -547,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/raw_route.cpp b/src/structures/vroom/raw_route.cpp index a559bf7c3..79c425ac0 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -12,8 +12,8 @@ All rights reserved (see LICENSE). namespace vroom { -RawRoute::RawRoute(const Input& input, Index v, 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[v].capacity), diff --git a/src/structures/vroom/raw_route.h b/src/structures/vroom/raw_route.h index 1a25d46a3..ebee776e9 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -66,7 +66,7 @@ class RawRoute { std::vector route; - RawRoute(const Input& input, Index v, unsigned amount_size); + RawRoute(const Input& input, Index v); 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 b5a5e1deb..70f3c8f80 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())}), @@ -69,7 +69,7 @@ void TWRoute::init_break_setup(const Input& input) { Duration previous_earliest = v_start; // Store smallest margin component-wise. - const auto amount_size = input.zero_amount().size(); + 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); diff --git a/src/structures/vroom/tw_route.h b/src/structures/vroom/tw_route.h index f5a6d0b34..94927c82d 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -122,7 +122,7 @@ 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); // Check validity for addition of job at job_rank in current route // at rank. From 68628f14ad3b885ff050911f48daa07f29facefe Mon Sep 17 00:00:00 2001 From: jcoupey Date: Wed, 22 Apr 2026 11:18:14 +0200 Subject: [PATCH 09/18] Move populating routes from steps in separated function. --- src/problems/vrp.h | 2 +- src/structures/vroom/raw_route.cpp | 5 +- src/structures/vroom/raw_route.h | 3 ++ src/structures/vroom/tw_route.cpp | 79 +++++++++++++++--------------- src/structures/vroom/tw_route.h | 4 +- 5 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/problems/vrp.h b/src/problems/vrp.h index bb7926c4f..0a921693a 100644 --- a/src/problems/vrp.h +++ b/src/problems/vrp.h @@ -31,7 +31,7 @@ template std::vector set_init_sol(const Input& input) { init_sol.reserve(input.vehicles.size()); for (Index v = 0; v < input.vehicles.size(); ++v) { - init_sol.emplace_back(input, v); + init_sol.emplace_back(input, v).populate_from_steps(input); } return init_sol; diff --git a/src/structures/vroom/raw_route.cpp b/src/structures/vroom/raw_route.cpp index 79c425ac0..81e4c7817 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -23,7 +23,10 @@ RawRoute::RawRoute(const Input& input, Index v) has_start(input.vehicles[v].has_start()), has_end(input.vehicles[v].has_end()), capacity(input.vehicles[v].capacity) { - const auto& vehicle = input.vehicles[v]; +} + +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); diff --git a/src/structures/vroom/raw_route.h b/src/structures/vroom/raw_route.h index ebee776e9..f609082de 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -53,6 +53,7 @@ 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); @@ -68,6 +69,8 @@ class RawRoute { RawRoute(const Input& input, Index v); + void populate_from_steps(const Input& input); + void set_route(const Input& input, const std::vector& r); bool empty() const { diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index 70f3c8f80..32bab5d86 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -24,47 +24,10 @@ TWRoute::TWRoute(const Input& input, Index v) break_latest(input.vehicles[v].breaks.size()), fwd_smallest_breaks_load_margin(input.vehicles[v].breaks.size()), bwd_smallest_breaks_load_margin(input.vehicles[v].breaks.size()) { - // Populate break data for empty route. - init_break_setup(input); - - const auto& vehicle = input.vehicles[v]; - if (!vehicle.steps.empty()) { - // As per the call to RawRoute above, provided route is OK with - // regard to capacity, max_travel_time, max_tasks, precedence and - // skills constraints AND this->route has been updated. - - if (!route.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, - route.begin(), - route.end(), - 0, - route.size())) { - this->replace(input, - single_jobs_deliveries, - route.begin(), - route.end(), - 0, - route.size()); - } else { - throw InputException( - std::format("Infeasible route for vehicle {}.", vehicle.id)); - } - } - } -} - -void TWRoute::init_break_setup(const Input& input) { const std::string break_error = - std::format("Inconsistent breaks for vehicle {}.", - input.vehicles[v_rank].id); + std::format("Inconsistent breaks for vehicle {}.", input.vehicles[v].id); - const auto& breaks = input.vehicles[v_rank].breaks; + const auto& breaks = input.vehicles[v].breaks; Duration previous_earliest = v_start; @@ -135,6 +98,44 @@ void TWRoute::init_break_setup(const Input& input) { } } +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 94927c82d..8337f4378 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -58,8 +58,6 @@ struct OrderChoice { class TWRoute : public RawRoute { private: - void init_break_setup(const Input& input); - PreviousInfo previous_info(const Input& input, Index job_rank, Index rank) const; @@ -124,6 +122,8 @@ class TWRoute : public RawRoute { 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. bool is_valid_addition_for_tw(const Input& input, From 05f9793e27c1dea597e27d582671e4d95166ad12 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Wed, 6 May 2026 09:54:53 +0200 Subject: [PATCH 10/18] Wrap check_route_steps return in a struct. --- src/structures/vroom/raw_route.cpp | 19 +++++++++---------- src/structures/vroom/raw_route.h | 14 +++++++++++--- src/structures/vroom/tw_route.cpp | 12 ++++++------ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/structures/vroom/raw_route.cpp b/src/structures/vroom/raw_route.cpp index 81e4c7817..58cc89d09 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -28,16 +28,16 @@ RawRoute::RawRoute(const Input& input, Index v) 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); + const auto route_data = check_route_steps(input); - if (!job_ranks.empty()) { + if (!route_data.job_ranks.empty()) { // Proceed with updating current route and all amounts. - set_route(input, job_ranks); + set_route(input, route_data.job_ranks); } } } -std::vector RawRoute::check_route_steps(const Input& input) { +InitRouteData 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]; @@ -59,8 +59,7 @@ std::vector RawRoute::check_route_steps(const Input& input) { previous_index = vehicle.start.value().index(); } - std::vector job_ranks; - job_ranks.reserve(vehicle.steps.size()); + InitRouteData route_data(vehicle.steps.size()); std::unordered_set expected_delivery_ranks; for (const auto& step : vehicle.steps) { if (step.type != STEP_TYPE::JOB) { @@ -69,7 +68,7 @@ std::vector RawRoute::check_route_steps(const Input& input) { const auto job_rank = step.rank; const auto& job = input.jobs[job_rank]; - job_ranks.push_back(job_rank); + route_data.job_ranks.push_back(job_rank); if (!input.vehicle_ok_with_job(v_rank, job_rank)) { throw InputException( @@ -121,7 +120,7 @@ std::vector RawRoute::check_route_steps(const Input& input) { } } - if (vehicle.has_end() && !job_ranks.empty()) { + if (vehicle.has_end() && !route_data.job_ranks.empty()) { // Update with last route leg. assert(previous_index.has_value()); eval_sum += @@ -136,7 +135,7 @@ std::vector RawRoute::check_route_steps(const Input& input) { std::format("Route over max_distance for vehicle {}.", vehicle.id)); } - if (vehicle.max_tasks < job_ranks.size()) { + if (vehicle.max_tasks < route_data.job_ranks.size()) { throw InputException( std::format("Too many tasks for vehicle {}.", vehicle.id)); } @@ -146,7 +145,7 @@ std::vector RawRoute::check_route_steps(const Input& input) { std::format("Invalid shipment in route for vehicle {}.", vehicle.id)); } - return job_ranks; + 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 f609082de..0675ddede 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -15,6 +15,14 @@ All rights reserved (see LICENSE). namespace vroom { +struct InitRouteData { + std::vector job_ranks; + + InitRouteData(std::size_t steps_size) { + job_ranks.reserve(steps_size); + } +}; + class RawRoute { private: Amount _zero; @@ -54,9 +62,9 @@ class RawRoute { 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); + // Throws if route for vehicle steps is invalid, else return useful + // info for further manual route setup. + InitRouteData check_route_steps(const Input& input); public: Index v_rank; diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index 32bab5d86..5700cf581 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -107,11 +107,11 @@ void TWRoute::populate_from_steps(const Input& input) { // If vehicles steps are not empty, start by checking validity from // a RawRoute perspective. - const auto job_ranks = check_route_steps(input); + const auto route_data = 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()) { + if (!route_data.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 = @@ -119,14 +119,14 @@ void TWRoute::populate_from_steps(const Input& input) { if (this->is_valid_addition_for_tw(input, single_jobs_deliveries, - job_ranks.begin(), - job_ranks.end(), + route_data.job_ranks.begin(), + route_data.job_ranks.end(), 0, 0)) { this->replace(input, single_jobs_deliveries, - job_ranks.begin(), - job_ranks.end(), + route_data.job_ranks.begin(), + route_data.job_ranks.end(), 0, 0); } else { From d96b9711e56c0271e762e668431b6102c2951472 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Wed, 6 May 2026 10:21:16 +0200 Subject: [PATCH 11/18] Add break data in InitRouteData struct. --- src/structures/vroom/raw_route.cpp | 12 ++++++++++++ src/structures/vroom/raw_route.h | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/src/structures/vroom/raw_route.cpp b/src/structures/vroom/raw_route.cpp index 58cc89d09..a721b9965 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -60,15 +60,25 @@ InitRouteData RawRoute::check_route_steps(const Input& input) { } InitRouteData route_data(vehicle.steps.size()); + 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( @@ -119,6 +129,8 @@ InitRouteData RawRoute::check_route_steps(const Input& input) { 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. diff --git a/src/structures/vroom/raw_route.h b/src/structures/vroom/raw_route.h index 0675ddede..07d82cf5a 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -17,9 +17,13 @@ namespace vroom { struct InitRouteData { std::vector job_ranks; + std::vector breaks_at_rank; + std::vector breaks_counts; InitRouteData(std::size_t steps_size) { job_ranks.reserve(steps_size); + breaks_at_rank.reserve(steps_size + 1); + breaks_counts.reserve(steps_size + 1); } }; From 64bc42ef23b26af2de39c5e1a81163a5a24390cd Mon Sep 17 00:00:00 2001 From: jcoupey Date: Wed, 6 May 2026 11:10:03 +0200 Subject: [PATCH 12/18] Store single jobs amount in route data for later reuse. --- src/structures/vroom/raw_route.cpp | 10 ++++++---- src/structures/vroom/raw_route.h | 1 + src/structures/vroom/tw_route.cpp | 7 ++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/structures/vroom/raw_route.cpp b/src/structures/vroom/raw_route.cpp index a721b9965..a30df3c9e 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -44,22 +44,24 @@ InitRouteData RawRoute::check_route_steps(const Input& input) { assert(!vehicle.steps.empty()); // Startup load is the sum of deliveries for (single) jobs. - const auto single_jobs_deliveries = + InitRouteData route_data(vehicle.steps.size()); + route_data.single_jobs_deliveries = utils::get_single_jobs_deliveries(input, vehicle.steps); - if (!(single_jobs_deliveries <= vehicle.capacity)) { + + 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 = single_jobs_deliveries; + 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(); } - InitRouteData route_data(vehicle.steps.size()); + // Initialize first break count slots. route_data.breaks_at_rank.push_back(0); route_data.breaks_counts.push_back(0); diff --git a/src/structures/vroom/raw_route.h b/src/structures/vroom/raw_route.h index 07d82cf5a..5e56e903c 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -19,6 +19,7 @@ struct InitRouteData { std::vector job_ranks; std::vector breaks_at_rank; std::vector breaks_counts; + Amount single_jobs_deliveries; InitRouteData(std::size_t steps_size) { job_ranks.reserve(steps_size); diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index 5700cf581..f0f6c636b 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -114,17 +114,14 @@ void TWRoute::populate_from_steps(const Input& input) { if (!route_data.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, + route_data.single_jobs_deliveries, route_data.job_ranks.begin(), route_data.job_ranks.end(), 0, 0)) { this->replace(input, - single_jobs_deliveries, + route_data.single_jobs_deliveries, route_data.job_ranks.begin(), route_data.job_ranks.end(), 0, From 96e9342672281e74bdb8222d740048bfcd91fb8f Mon Sep 17 00:00:00 2001 From: jcoupey Date: Wed, 6 May 2026 11:41:32 +0200 Subject: [PATCH 13/18] Adjust feasibility check logic based on whether all breaks are provided in vehicle steps. --- src/structures/vroom/tw_route.cpp | 62 +++++++++++++++++-------------- src/structures/vroom/tw_route.h | 3 ++ 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index f0f6c636b..8cf9f858d 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -100,39 +100,47 @@ TWRoute::TWRoute(const Input& input, Index v) 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 route_data = check_route_steps(input); - // Steps route is OK with regard to capacity, max_travel_time, + // 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. - if (!route_data.job_ranks.empty()) { - // We want to first check if route is OK for TW constraints based - // on our default break assignment heuristic. - 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 {}.", vehicle.id)); + auto route_data = check_route_steps(input); + + if (route_data.breaks_counts.back() != vehicle.breaks.size()) { + // Not all breaks are provided in vehicle steps, so 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. + 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 {}.", vehicle.id)); + } } + } else { + // Try populating object data using user-provided breaks ordering. + this->populate_from_steps_with_breaks(input, std::move(route_data)); } } +void TWRoute::populate_from_steps_with_breaks(const Input& input, + InitRouteData&& route_data) { + throw InputException( + std::format("Infeasible route for vehicle {}.", input.vehicles[v_rank].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 8337f4378..e5a02863a 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -73,6 +73,9 @@ 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_breaks(const Input& input, + InitRouteData&& route_data); + // Define global policy wrt job/break respective insertion rule. OrderChoice order_choice(const Input& input, Index job_rank, From 60380dd943a9fc30374fe845601c9df87fe2f10e Mon Sep 17 00:00:00 2001 From: jcoupey Date: Wed, 6 May 2026 11:50:51 +0200 Subject: [PATCH 14/18] Move regular feasibility check to dedicated function. --- src/structures/vroom/tw_route.cpp | 43 ++++++++++++++++++------------- src/structures/vroom/tw_route.h | 4 +++ 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index 8cf9f858d..020b7e375 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -111,32 +111,39 @@ void TWRoute::populate_from_steps(const Input& input) { // account for user-provided breaks at all and check if route is // OK for TW constraints based on our default break assignment // heuristic. - 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 {}.", vehicle.id)); - } - } + 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, std::move(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, InitRouteData&& route_data) { + // TODO implement throw InputException( std::format("Infeasible route for vehicle {}.", input.vehicles[v_rank].id)); } diff --git a/src/structures/vroom/tw_route.h b/src/structures/vroom/tw_route.h index e5a02863a..72b7b1a0f 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -73,6 +73,10 @@ 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, InitRouteData&& route_data); From f4c1d744a2de98859709e35cca4027f01aea6ca7 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Wed, 6 May 2026 18:35:22 +0200 Subject: [PATCH 15/18] Fix a few SQ warnings. --- src/structures/vroom/raw_route.cpp | 2 +- src/structures/vroom/raw_route.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structures/vroom/raw_route.cpp b/src/structures/vroom/raw_route.cpp index a30df3c9e..4752aa5b1 100644 --- a/src/structures/vroom/raw_route.cpp +++ b/src/structures/vroom/raw_route.cpp @@ -37,7 +37,7 @@ void RawRoute::populate_from_steps(const Input& input) { } } -InitRouteData RawRoute::check_route_steps(const Input& input) { +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]; diff --git a/src/structures/vroom/raw_route.h b/src/structures/vroom/raw_route.h index 5e56e903c..4056be1f8 100644 --- a/src/structures/vroom/raw_route.h +++ b/src/structures/vroom/raw_route.h @@ -21,7 +21,7 @@ struct InitRouteData { std::vector breaks_counts; Amount single_jobs_deliveries; - InitRouteData(std::size_t steps_size) { + 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); @@ -69,7 +69,7 @@ class RawRoute { 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); + InitRouteData check_route_steps(const Input& input) const; public: Index v_rank; @@ -82,7 +82,7 @@ class RawRoute { RawRoute(const Input& input, Index v); - void populate_from_steps(const Input& input); + virtual void populate_from_steps(const Input& input); void set_route(const Input& input, const std::vector& r); From c75fce7f23fcf1bd753b6b6a3f827e5e1b9fc841 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Thu, 7 May 2026 17:58:44 +0200 Subject: [PATCH 16/18] First take at populating route with user-provided breaks. --- src/structures/vroom/tw_route.cpp | 278 ++++++++++++++++++++++++++++-- src/structures/vroom/tw_route.h | 2 +- 2 files changed, 269 insertions(+), 11 deletions(-) diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index 020b7e375..87155782b 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -106,15 +106,16 @@ void TWRoute::populate_from_steps(const Input& input) { // max_tasks, precedence and skills constraints. auto route_data = check_route_steps(input); - if (route_data.breaks_counts.back() != vehicle.breaks.size()) { - // Not all breaks are provided in vehicle steps, so 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. + 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, std::move(route_data)); + this->populate_from_steps_with_breaks(input, route_data); } } @@ -142,10 +143,267 @@ void TWRoute::populate_from_steps_with_break_heuristic( } void TWRoute::populate_from_steps_with_breaks(const Input& input, - InitRouteData&& route_data) { - // TODO implement - throw InputException( - std::format("Infeasible route for vehicle {}.", input.vehicles[v_rank].id)); + 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 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 break_rank = breaks_counts[route.size()] - breaks_at_rank[route.size()]; + + for (Index r = 0; r < breaks_at_rank[route.size()]; ++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; + } + + // Consistency check with vehicle TW end. + earliest_end = current_earliest + 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. + 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]); + 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 < remaining_travel_time) { + remaining_travel_time -= margin; + } else { + 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 + remaining_travel_time) { + throw InputException(error); + } } PreviousInfo TWRoute::previous_info(const Input& input, diff --git a/src/structures/vroom/tw_route.h b/src/structures/vroom/tw_route.h index 72b7b1a0f..92231a6e6 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -78,7 +78,7 @@ class TWRoute : public RawRoute { const InitRouteData& route_data); void populate_from_steps_with_breaks(const Input& input, - InitRouteData&& route_data); + const InitRouteData& route_data); // Define global policy wrt job/break respective insertion rule. OrderChoice order_choice(const Input& input, From 3ae5ec8486d94840257c4f881e5db68e577117b2 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Mon, 11 May 2026 17:27:19 +0200 Subject: [PATCH 17/18] Better scoping. --- src/structures/vroom/tw_route.cpp | 35 ++++++++++++++++--------------- src/structures/vroom/tw_route.h | 2 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index 87155782b..bbd9e44b3 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -207,7 +207,7 @@ void TWRoute::populate_from_steps_with_breaks(const Input& input, }); if (b_tw == b.tws.end()) { throw InputException(error); - }; + } if (current_earliest < b_tw->start) { if (const auto margin = b_tw->start - current_earliest; @@ -248,15 +248,16 @@ void TWRoute::populate_from_steps_with_breaks(const Input& input, // Handle remaining breaks before route end. assert(!route.empty()); - Duration remaining_travel_time = + 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 break_rank = breaks_counts[route.size()] - breaks_at_rank[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, ++break_rank) { - const auto& b = v.breaks[break_rank]; + 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; @@ -267,21 +268,21 @@ void TWRoute::populate_from_steps_with_breaks(const Input& input, if (current_earliest < b_tw->start) { if (const auto margin = b_tw->start - current_earliest; - margin < remaining_travel_time) { - remaining_travel_time -= margin; + margin < last_remaining_travel_time) { + last_remaining_travel_time -= margin; } else { - remaining_travel_time = 0; + last_remaining_travel_time = 0; } current_earliest = b_tw->start; } - break_earliest[break_rank] = current_earliest; - current_earliest += v.breaks[break_rank].service; + 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 + remaining_travel_time; + earliest_end = current_earliest + last_remaining_travel_time; if (v_end < earliest_end) { throw InputException(error); } @@ -362,13 +363,13 @@ void TWRoute::populate_from_steps_with_breaks(const Input& input, // Update latest dates and margins for breaks right before the // first job. - remaining_travel_time = + 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]); - break_rank = breaks_counts[0]; + Index break_rank = breaks_counts[0]; for (Index r = 0; r < breaks_at_rank[0]; ++r) { --break_rank; @@ -389,10 +390,10 @@ void TWRoute::populate_from_steps_with_breaks(const Input& input, if (b_tw->end < current_latest) { if (const auto margin = current_latest - b_tw->end; - margin < remaining_travel_time) { - remaining_travel_time -= margin; + margin < first_remaining_travel_time) { + first_remaining_travel_time -= margin; } else { - remaining_travel_time = 0; + first_remaining_travel_time = 0; } current_latest = b_tw->end; } @@ -401,7 +402,7 @@ void TWRoute::populate_from_steps_with_breaks(const Input& input, } // Consistency check with vehicle TW start. - if (current_latest < v_start + remaining_travel_time) { + if (current_latest < v_start + first_remaining_travel_time) { throw InputException(error); } } diff --git a/src/structures/vroom/tw_route.h b/src/structures/vroom/tw_route.h index 92231a6e6..685a84cd8 100644 --- a/src/structures/vroom/tw_route.h +++ b/src/structures/vroom/tw_route.h @@ -129,7 +129,7 @@ class TWRoute : public RawRoute { TWRoute(const Input& input, Index v); - void populate_from_steps(const Input& input); + void populate_from_steps(const Input& input) override; // Check validity for addition of job at job_rank in current route // at rank. From d4c13ffec67ffb4e6100a19b621a3b21b501c670 Mon Sep 17 00:00:00 2001 From: jcoupey Date: Mon, 11 May 2026 17:27:47 +0200 Subject: [PATCH 18/18] Add test for empty steps. --- src/structures/vroom/tw_route.cpp | 32 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/structures/vroom/tw_route.cpp b/src/structures/vroom/tw_route.cpp index bbd9e44b3..6e0b59a1f 100644 --- a/src/structures/vroom/tw_route.cpp +++ b/src/structures/vroom/tw_route.cpp @@ -101,21 +101,23 @@ TWRoute::TWRoute(const Input& input, Index v) void TWRoute::populate_from_steps(const Input& input) { const auto& vehicle = input.vehicles[v_rank]; - // 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); + 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); + } } }