Skip to main content

vrp_gpu/
solution.rs

1//! Route representations and solution cost calculations for CVRP.
2
3use crate::instance::SolomonInstance;
4
5/// Represents a single vehicle route servicing a sequence of customers.
6///
7/// The route implicitly starts at the depot (node 0) and ends at the depot (node 0).
8/// The `nodes` vector stores only the customer sequence (nodes >= 1).
9#[derive(Debug, Clone, PartialEq, Eq, Default)]
10pub struct Route {
11    /// Ordered sequence of customer indices visited by this vehicle.
12    pub nodes: Vec<usize>,
13}
14
15impl Route {
16    /// Creates a new empty route.
17    #[inline]
18    pub fn new() -> Self {
19        Self { nodes: Vec::new() }
20    }
21
22    /// Creates a route with a predefined sequence of customer nodes.
23    #[inline]
24    pub fn from_nodes(nodes: Vec<usize>) -> Self {
25        Self { nodes }
26    }
27
28    /// Returns the number of customers in this route.
29    #[inline]
30    pub fn len(&self) -> usize {
31        self.nodes.len()
32    }
33
34    /// Returns `true` if the route contains no customers.
35    #[inline]
36    pub fn is_empty(&self) -> bool {
37        self.nodes.is_empty()
38    }
39
40    /// Calculates the total euclidean distance traveled by the route:
41    /// `depot (0) -> nodes[0] -> ... -> nodes[n-1] -> depot (0)`.
42    /// An empty route has a distance of `0.0`.
43    pub fn distance(&self, instance: &SolomonInstance) -> f32 {
44        if self.nodes.is_empty() {
45            return 0.0;
46        }
47
48        let mut total_dist = instance.distance(0, self.nodes[0]);
49        for window in self.nodes.windows(2) {
50            total_dist += instance.distance(window[0], window[1]);
51        }
52        total_dist += instance.distance(*self.nodes.last().unwrap(), 0);
53
54        total_dist
55    }
56
57    /// Calculates the total demand delivered to all customers in this route.
58    pub fn total_demand(&self, instance: &SolomonInstance) -> f32 {
59        self.nodes.iter().map(|&node| instance.demand(node)).sum()
60    }
61
62    /// Returns `true` if the total route demand does not exceed vehicle capacity.
63    #[inline]
64    pub fn is_capacity_feasible(&self, instance: &SolomonInstance) -> bool {
65        self.total_demand(instance) <= instance.vehicle.capacity
66    }
67}
68
69/// Represents a complete CVRP solution comprising multiple vehicle routes.
70#[derive(Debug, Clone, PartialEq, Default)]
71pub struct Solution {
72    /// Collection of active vehicle routes.
73    pub routes: Vec<Route>,
74}
75
76impl Solution {
77    /// Creates a new solution from a vector of routes.
78    #[inline]
79    pub fn new(routes: Vec<Route>) -> Self {
80        Self { routes }
81    }
82
83    /// Creates an empty solution with no routes.
84    #[inline]
85    pub fn empty() -> Self {
86        Self { routes: Vec::new() }
87    }
88
89    /// Calculates the total distance across all routes in the solution.
90    pub fn total_distance(&self, instance: &SolomonInstance) -> f32 {
91        self.routes
92            .iter()
93            .map(|route| route.distance(instance))
94            .sum()
95    }
96
97    /// Returns the total number of customers visited across all routes.
98    pub fn total_customers_visited(&self) -> usize {
99        self.routes.iter().map(Route::len).sum()
100    }
101
102    /// Validates full solution feasibility:
103    /// 1. Number of active non-empty routes <= `instance.vehicle.num_vehicles`.
104    /// 2. Each route respects vehicle capacity constraints (`total_demand <= capacity`).
105    /// 3. Every customer node `1..instance.num_nodes` is visited exactly once.
106    pub fn is_feasible(&self, instance: &SolomonInstance) -> bool {
107        if instance.validate().is_err() {
108            return false;
109        }
110        let active_routes = self.routes.iter().filter(|r| !r.is_empty());
111
112        // 1. Fleet size constraint
113        if active_routes.clone().count() > instance.vehicle.num_vehicles {
114            return false;
115        }
116
117        // 3. Customer visitation constraint (each customer 1..num_nodes visited exactly once)
118        let mut visited = vec![false; instance.num_nodes];
119
120        for route in active_routes {
121            for &node in &route.nodes {
122                // Depot cannot be in route.nodes, and node must be within valid range
123                if node == 0 || node >= instance.num_nodes {
124                    return false;
125                }
126                // Duplicate visit check
127                if visited[node] {
128                    return false;
129                }
130                visited[node] = true;
131            }
132            // Validate IDs before indexing demands in the capacity calculation.
133            if !route.is_capacity_feasible(instance) {
134                return false;
135            }
136        }
137
138        // Verify that all customers 1..num_nodes were visited
139        visited[1..].iter().all(|&v| v)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::instance::VehicleConfig;
147
148    #[test]
149    fn test_solution_rejects_invalid_ids_without_panicking() {
150        let instance = create_mock_instance();
151        for node in [instance.num_nodes, usize::MAX] {
152            let solution = Solution::new(vec![Route::from_nodes(vec![node])]);
153            assert!(!solution.is_feasible(&instance));
154        }
155    }
156
157    #[test]
158    fn test_solution_rejects_malformed_instance() {
159        let mut instance = create_mock_instance();
160        instance.demands.clear();
161        let solution = Solution::new(vec![Route::from_nodes(vec![1, 2, 3])]);
162        assert!(!solution.is_feasible(&instance));
163    }
164
165    #[test]
166    fn test_solution_ignores_empty_routes_in_fleet_count() {
167        let instance = create_mock_instance();
168        let solution = Solution::new(vec![
169            Route::new(),
170            Route::from_nodes(vec![1, 2]),
171            Route::from_nodes(vec![3]),
172        ]);
173        assert!(solution.is_feasible(&instance));
174    }
175
176    fn create_mock_instance() -> SolomonInstance {
177        // Depot (0, 0)
178        // Node 1: (3, 0), demand 10
179        // Node 2: (3, 4), demand 20
180        // Node 3: (0, 4), demand 15
181        // Vehicle: 2 vehicles, capacity 40.0
182        let xs = vec![0.0, 3.0, 3.0, 0.0];
183        let ys = vec![0.0, 0.0, 4.0, 4.0];
184        let demands = vec![0.0, 10.0, 20.0, 15.0];
185        let ready_times = vec![0.0, 0.0, 0.0, 0.0];
186        let due_times = vec![1000.0, 1000.0, 1000.0, 1000.0];
187        let service_times = vec![0.0, 10.0, 10.0, 10.0];
188        let distance_matrix = crate::instance::compute_distance_matrix(&xs, &ys);
189
190        SolomonInstance {
191            name: "MockInstance".into(),
192            vehicle: VehicleConfig {
193                num_vehicles: 2,
194                capacity: 40.0,
195            },
196            num_nodes: 4,
197            xs,
198            ys,
199            demands,
200            ready_times,
201            due_times,
202            service_times,
203            distance_matrix,
204        }
205    }
206
207    #[test]
208    fn test_empty_route_and_solution() {
209        let instance = create_mock_instance();
210        let route = Route::new();
211        assert!(route.is_empty());
212        assert_eq!(route.len(), 0);
213        assert_eq!(route.distance(&instance), 0.0);
214        assert_eq!(route.total_demand(&instance), 0.0);
215        assert!(route.is_capacity_feasible(&instance));
216
217        let solution = Solution::empty();
218        assert_eq!(solution.total_distance(&instance), 0.0);
219        assert_eq!(solution.total_customers_visited(), 0);
220    }
221
222    #[test]
223    fn test_route_distance_and_demand() {
224        let instance = create_mock_instance();
225        // Route: 0 -> 1 -> 2 -> 0
226        // dist(0, 1) = 3.0
227        // dist(1, 2) = 4.0
228        // dist(2, 0) = 5.0 (hypotenuse sqrt(3^2 + 4^2))
229        // total dist = 12.0
230        // total demand = 10 + 20 = 30.0 <= 40.0
231        let route = Route::from_nodes(vec![1, 2]);
232
233        assert_eq!(route.len(), 2);
234        assert!(!route.is_empty());
235        assert_eq!(route.total_demand(&instance), 30.0);
236        assert!(route.is_capacity_feasible(&instance));
237
238        let dist = route.distance(&instance);
239        assert!((dist - 12.0).abs() < 1e-5);
240    }
241
242    #[test]
243    fn test_solution_feasible_valid() {
244        let instance = create_mock_instance();
245        // 2 routes covering all customers 1, 2, 3
246        // Route 1: 0 -> 1 -> 2 -> 0 (demand 30 <= 40)
247        // Route 2: 0 -> 3 -> 0 (demand 15 <= 40)
248        let r1 = Route::from_nodes(vec![1, 2]);
249        let r2 = Route::from_nodes(vec![3]);
250        let solution = Solution::new(vec![r1, r2]);
251
252        assert_eq!(solution.total_customers_visited(), 3);
253        assert!(solution.is_feasible(&instance));
254    }
255
256    #[test]
257    fn test_solution_infeasible_exceeds_capacity() {
258        let instance = create_mock_instance();
259        // Route covering 1, 2, 3: demand = 10 + 20 + 15 = 45 > 40.0 capacity
260        let r = Route::from_nodes(vec![1, 2, 3]);
261        let solution = Solution::new(vec![r]);
262
263        assert!(!solution.is_feasible(&instance));
264    }
265
266    #[test]
267    fn test_solution_infeasible_duplicate_customer() {
268        let instance = create_mock_instance();
269        // Customer 1 visited twice
270        let r1 = Route::from_nodes(vec![1, 2]);
271        let r2 = Route::from_nodes(vec![1, 3]);
272        let solution = Solution::new(vec![r1, r2]);
273
274        assert!(!solution.is_feasible(&instance));
275    }
276
277    #[test]
278    fn test_solution_infeasible_missing_customer() {
279        let instance = create_mock_instance();
280        // Customer 3 missing
281        let r1 = Route::from_nodes(vec![1, 2]);
282        let solution = Solution::new(vec![r1]);
283
284        assert!(!solution.is_feasible(&instance));
285    }
286
287    #[test]
288    fn test_solution_infeasible_too_many_vehicles() {
289        let instance = create_mock_instance();
290        // 3 active routes when max vehicles is 2
291        let r1 = Route::from_nodes(vec![1]);
292        let r2 = Route::from_nodes(vec![2]);
293        let r3 = Route::from_nodes(vec![3]);
294        let solution = Solution::new(vec![r1, r2, r3]);
295
296        assert!(!solution.is_feasible(&instance));
297    }
298
299    #[test]
300    fn test_solution_infeasible_depot_in_route() {
301        let instance = create_mock_instance();
302        // Depot node 0 explicitly included in nodes list
303        let r1 = Route::from_nodes(vec![0, 1, 2]);
304        let r2 = Route::from_nodes(vec![3]);
305        let solution = Solution::new(vec![r1, r2]);
306
307        assert!(!solution.is_feasible(&instance));
308    }
309}