routik_solver/objective.rs
1//! The objective function the metaheuristic minimises.
2//!
3//! Centralised here so every part of the solver scores a solution the same
4//! way (per the solver rules: "Objectif centralisé dans `cost`"). By default
5//! it minimises total distance; optional weights let callers also penalise the
6//! number of vehicles used or the total time on the road.
7
8use crate::model::Solution;
9
10/// Weights of the linear objective `cost = w_d·distance + w_v·vehicles + w_t·time`.
11///
12/// The default is pure distance minimisation (`distance = 1`, the rest `0`),
13/// which is what the classic VRPTW benchmarks score against.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct Objective {
16 /// Weight on total travelled distance.
17 pub distance: f64,
18 /// Weight on the number of vehicles (routes) used.
19 pub vehicles: f64,
20 /// Weight on total elapsed time (travel + wait + service).
21 pub time: f64,
22}
23
24impl Default for Objective {
25 fn default() -> Self {
26 Self {
27 distance: 1.0,
28 vehicles: 0.0,
29 time: 0.0,
30 }
31 }
32}
33
34impl Objective {
35 /// Minimise total distance only.
36 #[must_use]
37 pub fn distance_only() -> Self {
38 Self::default()
39 }
40
41 /// Score from already rolled-up totals. Kept separate from [`Self::of`] so
42 /// the search loop can score incrementally without owning a [`Solution`].
43 #[must_use]
44 pub fn score(&self, total_distance: f64, route_count: usize, total_time: f64) -> f64 {
45 self.distance * total_distance + self.vehicles * route_count as f64 + self.time * total_time
46 }
47
48 /// Score a complete [`Solution`].
49 #[must_use]
50 pub fn of(&self, solution: &Solution) -> f64 {
51 self.score(
52 solution.total_distance,
53 solution.routes.len(),
54 solution.total_time,
55 )
56 }
57}