Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
30071e6
Add per_task_hour member in VehicleCosts.
jcoupey May 7, 2025
de96db3
Parse per_task_hour in vehicle.costs json object.
jcoupey May 7, 2025
df0efc8
Compute cost from task duration as seen from a vehicle.
jcoupey May 12, 2025
add9a79
Add task cost when formatting a RawRoute.
jcoupey May 12, 2025
e8a6d78
Add task cost when formatting a TWRoute.
jcoupey May 12, 2025
c5522e5
Add task cost when formatting solution in plan mode.
jcoupey May 12, 2025
3d1772d
Add Eval::task_duration member.
jcoupey May 28, 2025
5570201
Simplify route_eval_for_vehicle.
jcoupey May 28, 2025
8532f8b
Account for task costs in route_eval_for_vehicle.
jcoupey May 28, 2025
a2bbd75
Remove unused node_candidates and edge_candidates.
jcoupey May 28, 2025
5389267
Make previous index a global optional inside addition_cost.
jcoupey May 28, 2025
91111cd
Account for task cost in addition_cost.
jcoupey May 28, 2025
0c7811d
Account for task cost in SolutionState::node_gains.
jcoupey May 28, 2025
fc75ca7
Merge branch 'master' into feature/service-in-cost
jcoupey Jul 10, 2025
1dcc080
Slight helper adjustment.
jcoupey Jul 11, 2025
e24f8a0
Remove unnecessary intermediate variable.
jcoupey Jul 11, 2025
70f1dc3
Rename {f,b}wd_costs to {f,b}wd_evals.
jcoupey Jul 14, 2025
f59e942
Compute accumulated fwd and bwd service evals in routes.
jcoupey Jul 14, 2025
789d089
Account for setup time in fwd_task_evals.
jcoupey Jul 14, 2025
eaf7d37
Account for setup time in bwd_task_evals.
jcoupey Jul 14, 2025
4264f8a
Renaming in addition_cost.
jcoupey Jul 14, 2025
c6f6ebc
Account for task eval delta in get_range_removal_gain.
jcoupey Jul 14, 2025
ccff419
Account for task cost delta in addition_cost_delta for job insertion.
jcoupey Jul 14, 2025
7a9ecaa
Store {f,b}wd_service and {f,b}wd_setup separately.
jcoupey Jul 16, 2025
88abc50
No need for fwd and bwd service evals.
jcoupey Jul 16, 2025
58aeb6a
Update what bwd_setup_evals stands for.
jcoupey Jul 16, 2025
dc8f368
Include task cost delta in addition_cost_delta range insertion version.
jcoupey Jul 16, 2025
b7043c9
Update InsertionOption semantic from cost to eval.
jcoupey Jul 17, 2025
6bfc7df
Same cost->eval semantic update across all cost evaluation function.
jcoupey Jul 17, 2025
5fe63c5
Include task cost delta in in_place_delta_eval.
jcoupey Jul 17, 2025
436b6a6
Pass gain as const ref in SwapChoice ctor.
jcoupey Jul 17, 2025
99c0be4
Include task cost delta in addition_eval for pickup insertion.
jcoupey Jul 17, 2025
6e6e413
Account for task costs in heuristic regrets.
jcoupey Jul 21, 2025
7db9801
Adjust unassigned insertion lower bound used in heuristics to account…
jcoupey Jul 21, 2025
c42a36e
Document task cost changes.
jcoupey Jul 21, 2025
798b35c
Reducing TwoOpt scope to avoid operator overlap.
jcoupey Jul 22, 2025
00b7254
Update TwoOpt generation in LS.
jcoupey Jul 22, 2025
f11d35b
Fix assertion in addition_eval_delta.
jcoupey Jul 22, 2025
f0c5814
Fix sign error in addition_eval_delta for job vs range.
jcoupey Jul 22, 2025
8cc881f
Properly handle setup without start.
jcoupey Jul 24, 2025
abcfa8d
Simplify addition_eval_delta with separate implementation for removal…
jcoupey Jul 24, 2025
beda10e
Make Eval ctor explicit.
jcoupey Jul 24, 2025
fede964
Leftover from gain refactoring in IntraTwoOpt. #1267
jcoupey Jul 24, 2025
f3aaa21
Address some Sonarcloud comments.
jcoupey Jul 24, 2025
89d3364
Some more Sonarcloud stuff.
jcoupey Jul 25, 2025
0140bdd
Fix RouteExchange::compute_gain in case of empty routes.
jcoupey Sep 17, 2025
71dd5ba
Pass RawRoute directly as param for all SolutionState updaters.
jcoupey Sep 25, 2025
589f232
No need for SolutionState::setup template at route level.
jcoupey Sep 25, 2025
51ec995
Acknowledge that computing pd_gains relies on update_costs.
jcoupey Sep 25, 2025
9741e3c
Replace ad-hoc code for pd_gains by simple call to utils::removal_gain.
jcoupey Sep 25, 2025
ae438bb
Potential fixed cost gain is now included in pd_gains.
jcoupey Sep 25, 2025
2e05527
Remove unused variable.
jcoupey Sep 25, 2025
dea201e
Merge branch 'master' into feature/service-in-cost
jcoupey Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

#### Features

- Ability to set different tasks setup/service time per vehicle type (#336)
- Ability to set different task times per vehicle type (#336)
- Task times can be included in the cost used internally for optimization (#1130)
- Support for cost per hour spent on tasks on a vehicle basis (#1130)

#### Internals

Expand Down
1 change: 1 addition & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ A `cost` object has the following properties:
| ----------- | ----------- |
| [`fixed`] | integer defining the cost of using this vehicle in the solution (defaults to `0`) |
| [`per_hour`] | integer defining the cost for one hour of travel time with this vehicle (defaults to `3600`) |
| [`per_task_hour`] | integer defining the cost for one hour of task time (setup + service) with this vehicle (defaults to `0`) |
| [`per_km`] | integer defining the cost for one km of travel time with this vehicle (defaults to `0`) |

Using a non-default `per-hour` value means defining travel costs based
Expand Down
35 changes: 23 additions & 12 deletions src/algorithms/heuristics/heuristics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,31 +153,38 @@ template <class Route> struct UnassignedCosts {
min_unassigned_to_route(input.jobs.size(),
std::numeric_limits<Cost>::max()) {
for (const auto job_rank : unassigned) {
const auto unassigned_job_index = input.jobs[job_rank].index();
const auto& unassigned_job = input.jobs[job_rank];
const auto unassigned_job_index = unassigned_job.index();

// The purpose here is to generate insertion lower bounds so we
// only account for service times (no setup) which are
// independent of insertion rank.
const auto added_service = unassigned_job.services[vehicle.type];
const auto service_cost = vehicle.task_eval(added_service).cost;

if (vehicle.has_start()) {
const auto start_to_job =
vehicle.eval(vehicle.start.value().index(), unassigned_job_index)
.cost;
min_route_to_unassigned[job_rank] = start_to_job;
min_route_to_unassigned[job_rank] = start_to_job + service_cost;
}

if (vehicle.has_end()) {
const auto job_to_end =
vehicle.eval(unassigned_job_index, vehicle.end.value().index()).cost;
min_unassigned_to_route[job_rank] = job_to_end;
min_unassigned_to_route[job_rank] = job_to_end + service_cost;
}

for (const auto j : route.route) {
const auto job_index = input.jobs[j].index();

const auto job_to_unassigned =
vehicle.eval(job_index, unassigned_job_index).cost;
vehicle.eval(job_index, unassigned_job_index).cost + service_cost;
min_route_to_unassigned[job_rank] =
std::min(min_route_to_unassigned[job_rank], job_to_unassigned);

const auto unassigned_to_job =
vehicle.eval(unassigned_job_index, job_index).cost;
vehicle.eval(unassigned_job_index, job_index).cost + service_cost;
min_unassigned_to_route[job_rank] =
std::min(min_unassigned_to_route[job_rank], unassigned_to_job);
}
Expand Down Expand Up @@ -215,15 +222,19 @@ template <class Route> struct UnassignedCosts {
const std::set<Index>& unassigned,
Index inserted_index) {
for (const auto j : unassigned) {
const auto unassigned_job_index = input.jobs[j].index();
const auto& unassigned_job = input.jobs[j];
const auto unassigned_job_index = unassigned_job.index();

const auto added_service = unassigned_job.services[vehicle.type];
const auto service_cost = vehicle.task_eval(added_service).cost;

const auto to_unassigned =
vehicle.eval(inserted_index, unassigned_job_index).cost;
vehicle.eval(inserted_index, unassigned_job_index).cost + service_cost;
min_route_to_unassigned[j] =
std::min(min_route_to_unassigned[j], to_unassigned);

const auto from_unassigned =
vehicle.eval(unassigned_job_index, inserted_index).cost;
vehicle.eval(unassigned_job_index, inserted_index).cost + service_cost;
min_unassigned_to_route[j] =
std::min(min_unassigned_to_route[j], from_unassigned);
}
Expand Down Expand Up @@ -279,7 +290,7 @@ inline Eval fill_route(const Input& input,

for (Index r = 0; r <= route.size(); ++r) {
const auto current_eval =
utils::addition_cost(input, job_rank, vehicle, route.route, r);
utils::addition_eval(input, job_rank, vehicle, route.route, r);

const double current_cost =
static_cast<double>(current_eval.cost) -
Expand Down Expand Up @@ -317,7 +328,7 @@ inline Eval fill_route(const Input& input,
route.route.size() + 1);

for (unsigned d_rank = 0; d_rank <= route.route.size(); ++d_rank) {
d_adds[d_rank] = utils::addition_cost(input,
d_adds[d_rank] = utils::addition_eval(input,
job_rank + 1,
vehicle,
route.route,
Expand All @@ -329,7 +340,7 @@ inline Eval fill_route(const Input& input,
}

for (Index pickup_r = 0; pickup_r <= route.size(); ++pickup_r) {
const auto p_add = utils::addition_cost(input,
const auto p_add = utils::addition_eval(input,
job_rank,
vehicle,
route.route,
Expand Down Expand Up @@ -370,7 +381,7 @@ inline Eval fill_route(const Input& input,

Eval current_eval;
if (pickup_r == delivery_r) {
current_eval = utils::addition_cost(input,
current_eval = utils::addition_eval(input,
job_rank,
vehicle,
route.route,
Expand Down
8 changes: 4 additions & 4 deletions src/algorithms/local_search/insertion_search.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ compute_best_insertion_single(const Input& input,
rank < sol_state.insertion_ranks_end[v][j];
++rank) {
const Eval current_eval =
utils::addition_cost(input, j, v_target, route.route, rank);
utils::addition_eval(input, j, v_target, route.route, rank);
if (current_eval.cost < result.eval.cost &&
v_target.ok_for_range_bounds(sol_state.route_evals[v] +
current_eval) &&
Expand Down Expand Up @@ -113,7 +113,7 @@ RouteInsertion compute_best_insertion_pd(const Input& input,
bool found_valid = false;
for (unsigned d_rank = begin_d_rank; d_rank < end_d_rank; ++d_rank) {
d_adds[d_rank] =
utils::addition_cost(input, j + 1, v_target, route.route, d_rank);
utils::addition_eval(input, j + 1, v_target, route.route, d_rank);
if (result.eval < d_adds[d_rank]) {
valid_delivery_insertions[d_rank] = false;
} else {
Expand All @@ -132,7 +132,7 @@ RouteInsertion compute_best_insertion_pd(const Input& input,
pickup_r < sol_state.insertion_ranks_end[v][j];
++pickup_r) {
const Eval p_add =
utils::addition_cost(input, j, v_target, route.route, pickup_r);
utils::addition_eval(input, j, v_target, route.route, pickup_r);
if (result.eval < p_add) {
// Even without delivery insertion more expensive than current best.
continue;
Expand Down Expand Up @@ -173,7 +173,7 @@ RouteInsertion compute_best_insertion_pd(const Input& input,

Eval pd_eval;
if (pickup_r == delivery_r) {
pd_eval = utils::addition_cost(input,
pd_eval = utils::addition_eval(input,
j,
v_target,
route.route,
Expand Down
73 changes: 42 additions & 31 deletions src/algorithms/local_search/local_search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ LocalSearch<Route,

// Update best_route data required for consistency.
modified_vehicles.insert(best_route);
_sol_state.update_route_eval(_sol[best_route].route, best_route);
_sol_state.set_insertion_ranks(_sol[best_route], best_route);
_sol_state.update_route_eval(_sol[best_route]);
_sol_state.set_insertion_ranks(_sol[best_route]);

const auto fixed_cost =
_sol[best_route].empty() ? _input.vehicles[best_route].fixed_cost() : 0;
Expand All @@ -320,14 +320,14 @@ LocalSearch<Route,
// Update stored data for consistency (except update_route_eval and
// set_insertion_ranks done along the way).
for (const auto v : modified_vehicles) {
_sol_state.update_route_bbox(_sol[v].route, v);
_sol_state.update_costs(_sol[v].route, v);
_sol_state.update_skills(_sol[v].route, v);
_sol_state.update_priorities(_sol[v].route, v);
_sol_state.set_node_gains(_sol[v].route, v);
_sol_state.set_edge_gains(_sol[v].route, v);
_sol_state.set_pd_matching_ranks(_sol[v].route, v);
_sol_state.set_pd_gains(_sol[v].route, v);
_sol_state.update_route_bbox(_sol[v]);
_sol_state.update_costs(_sol[v]);
_sol_state.update_skills(_sol[v]);
_sol_state.update_priorities(_sol[v]);
_sol_state.set_node_gains(_sol[v]);
_sol_state.set_edge_gains(_sol[v]);
_sol_state.set_pd_matching_ranks(_sol[v]);
_sol_state.set_pd_gains(_sol[v]);
}

return modified_vehicles;
Expand Down Expand Up @@ -910,13 +910,16 @@ void LocalSearch<Route,
continue;
}

const unsigned jobs_moved_from_source =
_sol[source].size() - s_rank - 1;

const auto& s_fwd_delivery = _sol[source].fwd_deliveries(s_rank);
const auto& s_fwd_pickup = _sol[source].fwd_pickups(s_rank);
const auto& s_bwd_delivery = _sol[source].bwd_deliveries(s_rank);
const auto& s_bwd_pickup = _sol[source].bwd_pickups(s_rank);

Index end_t_rank = _sol[target].size();
if (s_rank + 1 < _sol[source].size()) {
if (jobs_moved_from_source > 0) {
// There is a route end after s_rank in source route.
const auto s_next_job_rank = _sol[source].route[s_rank + 1];
end_t_rank =
Expand All @@ -930,6 +933,14 @@ void LocalSearch<Route,
continue;
}

assert(static_cast<int>(_sol[target].size()) - t_rank - 1 >= 0);
if (const unsigned jobs_moved_from_target =
_sol[target].size() - static_cast<unsigned>(t_rank) - 1;
jobs_moved_from_source <= 2 && jobs_moved_from_target <= 2) {
// One of Relocate, OrOpt, SwapStar, MixedExchange, or no-opt.
continue;
}

if (t_rank + 1 < static_cast<int>(_sol[target].size())) {
// There is a route end after t_rank in target route.
const auto t_next_job_rank = _sol[target].route[t_rank + 1];
Expand Down Expand Up @@ -1824,16 +1835,16 @@ void LocalSearch<Route,
#endif

for (auto v_rank : update_candidates) {
_sol_state.update_route_eval(_sol[v_rank].route, v_rank);
_sol_state.update_route_bbox(_sol[v_rank].route, v_rank);
_sol_state.update_costs(_sol[v_rank].route, v_rank);
_sol_state.update_skills(_sol[v_rank].route, v_rank);
_sol_state.update_priorities(_sol[v_rank].route, v_rank);
_sol_state.set_insertion_ranks(_sol[v_rank], v_rank);
_sol_state.set_node_gains(_sol[v_rank].route, v_rank);
_sol_state.set_edge_gains(_sol[v_rank].route, v_rank);
_sol_state.set_pd_matching_ranks(_sol[v_rank].route, v_rank);
_sol_state.set_pd_gains(_sol[v_rank].route, v_rank);
_sol_state.update_route_eval(_sol[v_rank]);
_sol_state.update_route_bbox(_sol[v_rank]);
_sol_state.update_costs(_sol[v_rank]);
_sol_state.update_skills(_sol[v_rank]);
_sol_state.update_priorities(_sol[v_rank]);
_sol_state.set_insertion_ranks(_sol[v_rank]);
_sol_state.set_node_gains(_sol[v_rank]);
_sol_state.set_edge_gains(_sol[v_rank]);
_sol_state.set_pd_matching_ranks(_sol[v_rank]);
_sol_state.set_pd_gains(_sol[v_rank]);

assert(_sol[v_rank].size() <= _input.vehicles[v_rank].max_tasks);
assert(_input.vehicles[v_rank].ok_for_range_bounds(
Expand Down Expand Up @@ -2009,22 +2020,22 @@ void LocalSearch<Route,
for (std::size_t v = 0; v < _sol.size(); ++v) {
// Update what is required for consistency in
// remove_from_route.
_sol_state.update_route_eval(_sol[v].route, v);
_sol_state.update_route_bbox(_sol[v].route, v);
_sol_state.set_node_gains(_sol[v].route, v);
_sol_state.set_pd_matching_ranks(_sol[v].route, v);
_sol_state.set_pd_gains(_sol[v].route, v);
_sol_state.update_costs(_sol[v]);
_sol_state.update_route_eval(_sol[v]);
_sol_state.update_route_bbox(_sol[v]);
_sol_state.set_node_gains(_sol[v]);
_sol_state.set_pd_matching_ranks(_sol[v]);
_sol_state.set_pd_gains(_sol[v]);
}
}

// Update stored data that has not been maintained while
// removing.
for (std::size_t v = 0; v < _sol.size(); ++v) {
_sol_state.update_costs(_sol[v].route, v);
_sol_state.update_skills(_sol[v].route, v);
_sol_state.update_priorities(_sol[v].route, v);
_sol_state.set_insertion_ranks(_sol[v], v);
_sol_state.set_edge_gains(_sol[v].route, v);
_sol_state.update_skills(_sol[v]);
_sol_state.update_priorities(_sol[v]);
_sol_state.set_insertion_ranks(_sol[v]);
_sol_state.set_edge_gains(_sol[v]);
}

// Refill jobs.
Expand Down
4 changes: 2 additions & 2 deletions src/algorithms/local_search/route_split_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ compute_best_route_split_choice(const Input& input,
}

const auto current_end_eval =
-std::get<0>(utils::addition_cost_delta(input,
-std::get<0>(utils::addition_eval_delta(input,
sol_state,
empty_routes[v_rank],
0,
Expand Down Expand Up @@ -140,7 +140,7 @@ compute_best_route_split_choice(const Input& input,
}

const auto current_begin_eval =
-std::get<0>(utils::addition_cost_delta(input,
-std::get<0>(utils::addition_eval_delta(input,
sol_state,
empty_routes[v_rank],
0,
Expand Down
22 changes: 11 additions & 11 deletions src/algorithms/local_search/swap_star_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct SwapChoice {

SwapChoice() = default;

SwapChoice(Eval gain,
SwapChoice(const Eval& gain,
Index s_rank,
Index t_rank,
Index insertion_in_source,
Expand Down Expand Up @@ -219,7 +219,7 @@ SwapChoice compute_best_swap_star_choice(const Input& input,

for (unsigned s_rank = 0; s_rank < source.route.size(); ++s_rank) {
const auto& target_insertions = top_insertions_in_target[s_rank];
if (target_insertions[0].cost == NO_EVAL) {
if (target_insertions[0].eval == NO_EVAL) {
continue;
}

Expand All @@ -235,7 +235,7 @@ SwapChoice compute_best_swap_star_choice(const Input& input,

for (unsigned t_rank = 0; t_rank < target.route.size(); ++t_rank) {
const auto& source_insertions = top_insertions_in_source[t_rank];
if (source_insertions[0].cost == NO_EVAL) {
if (source_insertions[0].eval == NO_EVAL) {
continue;
}

Expand All @@ -252,14 +252,14 @@ SwapChoice compute_best_swap_star_choice(const Input& input,
}

const auto target_in_place_delta =
utils::in_place_delta_cost(input,
utils::in_place_delta_eval(input,
source.route[s_rank],
t_v,
target.route,
t_rank);

const auto source_in_place_delta =
utils::in_place_delta_cost(input,
utils::in_place_delta_eval(input,
target.route[t_rank],
s_v,
source.route,
Expand Down Expand Up @@ -295,8 +295,8 @@ SwapChoice compute_best_swap_star_choice(const Input& input,

for (const auto& ti : target_insertions) {
if ((ti.rank != t_rank) && (ti.rank != t_rank + 1) &&
(ti.cost != NO_EVAL)) {
const Eval t_gain = target_delta - ti.cost;
(ti.eval != NO_EVAL)) {
const Eval t_gain = target_delta - ti.eval;
current_gain = in_place_s_gain + t_gain;
if (best_gain < current_gain &&
t_v.ok_for_range_bounds(t_eval - t_gain)) {
Expand All @@ -320,8 +320,8 @@ SwapChoice compute_best_swap_star_choice(const Input& input,
// target_insertions.
for (const auto& si : source_insertions) {
if ((si.rank != s_rank) && (si.rank != s_rank + 1) &&
(si.cost != NO_EVAL)) {
const Eval s_gain = source_delta - si.cost;
(si.eval != NO_EVAL)) {
const Eval s_gain = source_delta - si.eval;

if (!s_v.ok_for_range_bounds(s_eval - s_gain)) {
// Don't bother further checking if max travel time
Expand All @@ -345,8 +345,8 @@ SwapChoice compute_best_swap_star_choice(const Input& input,

for (const auto& ti : target_insertions) {
if ((ti.rank != t_rank) && (ti.rank != t_rank + 1) &&
(ti.cost != NO_EVAL)) {
const Eval t_gain = target_delta - ti.cost;
(ti.eval != NO_EVAL)) {
const Eval t_gain = target_delta - ti.eval;
current_gain = s_gain + t_gain;
if (best_gain < current_gain &&
t_v.ok_for_range_bounds(t_eval - t_gain)) {
Expand Down
Loading