Skip to main content

solverforge_cvrp/
lib.rs

1/* CVRP domain helpers for SolverForge.
2
3Provides `ProblemData`, `MatrixDistanceMeter`, `MatrixIntraDistanceMeter`,
4the `VrpSolution` trait, and a suite of free functions for Clarke-Wright
5and k-opt construction phases.
6*/
7
8use solverforge_solver::CrossEntityDistanceMeter;
9
10/* ============================================================================
11Problem data
12============================================================================
13*/
14
15/// Immutable problem data shared by all vehicles.
16///
17/// Stored via raw pointer in each vehicle so the framework can clone vehicles
18/// freely during local search without copying matrices.
19#[derive(Clone, Debug)]
20pub struct ProblemData {
21    pub capacity: i64,
22    pub depot: usize,
23    pub demands: Vec<i32>,
24    pub distance_matrix: Vec<Vec<i64>>,
25    pub time_windows: Vec<(i64, i64)>,
26    pub service_durations: Vec<i64>,
27    pub travel_times: Vec<Vec<i64>>,
28    pub vehicle_departure_time: i64,
29}
30
31/* ============================================================================
32VrpSolution trait
33============================================================================
34*/
35
36/// Trait implemented by a planning solution that holds a fleet of vehicles,
37/// each carrying a `*const ProblemData` pointer and a list of visited stops.
38///
39/// # Safety
40/// Implementors must ensure every `vehicle_data_ptr` points to a valid
41/// `ProblemData` for the entire duration of a solve call. Returning a null
42/// pointer for a non-empty fleet is an initialization bug and will panic in
43/// the helper functions below.
44pub trait VrpSolution {
45    fn vehicle_data_ptr(&self, entity_idx: usize) -> *const ProblemData;
46    fn vehicle_visits(&self, entity_idx: usize) -> &[usize];
47    fn vehicle_visits_mut(&mut self, entity_idx: usize) -> &mut Vec<usize>;
48    fn vehicle_count(&self) -> usize;
49}
50
51/* ============================================================================
52Free functions (callable as fn-pointer fields in list stock hooks)
53============================================================================
54*/
55
56#[inline]
57fn problem_data_for_entity<S: VrpSolution>(plan: &S, entity_idx: usize) -> Option<&ProblemData> {
58    let ptr = plan.vehicle_data_ptr(entity_idx);
59    assert!(
60        !ptr.is_null(),
61        "VrpSolution::vehicle_data_ptr({entity_idx}) returned null for a non-empty fleet"
62    );
63    // SAFETY: VrpSolution implementors guarantee valid pointers for the duration
64    // of the solve call; null for a non-empty fleet is rejected above.
65    unsafe { ptr.as_ref() }
66}
67
68#[inline]
69fn first_problem_data<S: VrpSolution>(plan: &S) -> Option<&ProblemData> {
70    if plan.vehicle_count() == 0 {
71        return None;
72    }
73    problem_data_for_entity(plan, 0)
74}
75
76/// Distance between two element indices using the first vehicle's data pointer.
77pub fn distance<S: VrpSolution>(plan: &S, i: usize, j: usize) -> i64 {
78    first_problem_data(plan).map_or(0, |data| data.distance_matrix[i][j])
79}
80
81pub fn depot_for_entity<S: VrpSolution>(plan: &S, _entity_idx: usize) -> usize {
82    first_problem_data(plan).map_or(0, |data| data.depot)
83}
84
85pub fn depot_for_cw<S: VrpSolution>(plan: &S) -> usize {
86    first_problem_data(plan).map_or(0, |data| data.depot)
87}
88
89pub fn element_load<S: VrpSolution>(plan: &S, elem: usize) -> i64 {
90    first_problem_data(plan).map_or(0, |data| data.demands[elem] as i64)
91}
92
93pub fn capacity<S: VrpSolution>(plan: &S) -> i64 {
94    first_problem_data(plan).map_or(i64::MAX, |data| data.capacity)
95}
96
97/// Replaces the current route for entity `entity_idx`.
98///
99/// Callers must pass a valid `entity_idx` for the current solution.
100pub fn replace_route<S: VrpSolution>(plan: &mut S, entity_idx: usize, route: Vec<usize>) {
101    *plan.vehicle_visits_mut(entity_idx) = route;
102}
103
104/// Returns a cloned snapshot of the route for entity `entity_idx`.
105///
106/// Callers must pass a valid `entity_idx` for the current solution.
107pub fn get_route<S: VrpSolution>(plan: &S, entity_idx: usize) -> Vec<usize> {
108    plan.vehicle_visits(entity_idx).to_vec()
109}
110
111/// Returns `true` if the route satisfies all time-window constraints.
112pub fn is_time_feasible<S: VrpSolution>(plan: &S, route: &[usize]) -> bool {
113    if route.is_empty() {
114        return true;
115    }
116    match first_problem_data(plan) {
117        Some(data) => check_time_feasible(route, data),
118        None => true,
119    }
120}
121
122/// K-opt feasibility gate: returns `true` if the route satisfies all time-window constraints.
123/// The `entity_idx` parameter is ignored — time windows are uniform across vehicles.
124pub fn is_kopt_feasible<S: VrpSolution>(plan: &S, _entity_idx: usize, route: &[usize]) -> bool {
125    is_time_feasible(plan, route)
126}
127
128fn check_time_feasible(route: &[usize], data: &ProblemData) -> bool {
129    let mut current_time = data.vehicle_departure_time;
130    let mut prev = data.depot;
131
132    for &visit in route {
133        current_time += data.travel_times[prev][visit];
134
135        let (min_start, max_end) = data.time_windows[visit];
136
137        if current_time < min_start {
138            current_time = min_start;
139        }
140
141        let service_end = current_time + data.service_durations[visit];
142
143        if service_end > max_end {
144            return false;
145        }
146
147        current_time = service_end;
148        prev = visit;
149    }
150
151    true
152}
153
154/* ============================================================================
155Distance meters
156============================================================================
157*/
158
159// Cross-entity distance meter backed by the solution's distance matrix.
160#[derive(Clone, Default)]
161pub struct MatrixDistanceMeter;
162
163impl<S: VrpSolution> CrossEntityDistanceMeter<S> for MatrixDistanceMeter {
164    fn distance(
165        &self,
166        solution: &S,
167        src_entity: usize,
168        src_pos: usize,
169        dst_entity: usize,
170        dst_pos: usize,
171    ) -> f64 {
172        let src_visits = solution.vehicle_visits(src_entity);
173        let dst_visits = solution.vehicle_visits(dst_entity);
174        if src_pos >= src_visits.len() || dst_pos >= dst_visits.len() {
175            return f64::INFINITY;
176        }
177        problem_data_for_entity(solution, src_entity).map_or(f64::INFINITY, |data| {
178            data.distance_matrix[src_visits[src_pos]][dst_visits[dst_pos]] as f64
179        })
180    }
181}
182
183// Intra-entity distance meter backed by the solution's distance matrix.
184#[derive(Clone, Default)]
185pub struct MatrixIntraDistanceMeter;
186
187impl<S: VrpSolution> CrossEntityDistanceMeter<S> for MatrixIntraDistanceMeter {
188    fn distance(
189        &self,
190        solution: &S,
191        src_entity: usize,
192        src_pos: usize,
193        _dst_entity: usize,
194        dst_pos: usize,
195    ) -> f64 {
196        let visits = solution.vehicle_visits(src_entity);
197        if src_pos >= visits.len() || dst_pos >= visits.len() {
198            return f64::INFINITY;
199        }
200        problem_data_for_entity(solution, src_entity).map_or(f64::INFINITY, |data| {
201            data.distance_matrix[visits[src_pos]][visits[dst_pos]] as f64
202        })
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    struct TestSolution {
211        data: Box<ProblemData>,
212        routes: Vec<Vec<usize>>,
213    }
214
215    impl TestSolution {
216        fn new(routes: Vec<Vec<usize>>) -> Self {
217            Self {
218                data: Box::new(ProblemData {
219                    capacity: 10,
220                    depot: 0,
221                    demands: vec![0, 2, 3, 4],
222                    distance_matrix: vec![
223                        vec![0, 5, 7, 9],
224                        vec![5, 0, 4, 6],
225                        vec![7, 4, 0, 3],
226                        vec![9, 6, 3, 0],
227                    ],
228                    time_windows: vec![(0, 100), (0, 10), (7, 14), (0, 12)],
229                    service_durations: vec![0, 2, 2, 3],
230                    travel_times: vec![
231                        vec![0, 5, 7, 9],
232                        vec![5, 0, 4, 6],
233                        vec![7, 4, 0, 3],
234                        vec![9, 6, 3, 0],
235                    ],
236                    vehicle_departure_time: 0,
237                }),
238                routes,
239            }
240        }
241    }
242
243    impl VrpSolution for TestSolution {
244        fn vehicle_data_ptr(&self, _entity_idx: usize) -> *const ProblemData {
245            self.data.as_ref() as *const ProblemData
246        }
247
248        fn vehicle_visits(&self, entity_idx: usize) -> &[usize] {
249            &self.routes[entity_idx]
250        }
251
252        fn vehicle_visits_mut(&mut self, entity_idx: usize) -> &mut Vec<usize> {
253            &mut self.routes[entity_idx]
254        }
255
256        fn vehicle_count(&self) -> usize {
257            self.routes.len()
258        }
259    }
260
261    struct NullDataSolution {
262        routes: Vec<Vec<usize>>,
263    }
264
265    impl VrpSolution for NullDataSolution {
266        fn vehicle_data_ptr(&self, _entity_idx: usize) -> *const ProblemData {
267            std::ptr::null()
268        }
269
270        fn vehicle_visits(&self, entity_idx: usize) -> &[usize] {
271            &self.routes[entity_idx]
272        }
273
274        fn vehicle_visits_mut(&mut self, entity_idx: usize) -> &mut Vec<usize> {
275            &mut self.routes[entity_idx]
276        }
277
278        fn vehicle_count(&self) -> usize {
279            self.routes.len()
280        }
281    }
282
283    #[test]
284    fn helpers_use_problem_data_from_first_vehicle() {
285        let solution = TestSolution::new(vec![vec![1, 2], vec![3]]);
286
287        assert_eq!(distance(&solution, 1, 3), 6);
288        assert_eq!(depot_for_entity(&solution, 1), 0);
289        assert_eq!(depot_for_cw(&solution), 0);
290        assert_eq!(element_load(&solution, 2), 3);
291        assert_eq!(capacity(&solution), 10);
292    }
293
294    #[test]
295    fn helpers_handle_empty_fleets() {
296        let solution = TestSolution::new(vec![]);
297
298        assert_eq!(distance(&solution, 1, 2), 0);
299        assert_eq!(depot_for_entity(&solution, 0), 0);
300        assert_eq!(depot_for_cw(&solution), 0);
301        assert_eq!(element_load(&solution, 1), 0);
302        assert_eq!(capacity(&solution), i64::MAX);
303        assert!(is_time_feasible(&solution, &[1, 2]));
304        assert!(is_kopt_feasible(&solution, 0, &[1, 2]));
305    }
306
307    #[test]
308    #[should_panic(expected = "vehicle_data_ptr(0) returned null")]
309    fn helpers_reject_missing_problem_data_for_non_empty_fleets() {
310        let solution = NullDataSolution {
311            routes: vec![vec![1, 2]],
312        };
313
314        let _ = distance(&solution, 1, 2);
315    }
316
317    #[test]
318    fn route_helpers_replace_and_clone_routes() {
319        let mut solution = TestSolution::new(vec![vec![1, 2], vec![3]]);
320
321        replace_route(&mut solution, 0, vec![2, 3]);
322        assert_eq!(solution.routes[0], vec![2, 3]);
323        assert_eq!(get_route(&solution, 0), vec![2, 3]);
324
325        replace_route(&mut solution, 1, vec![1]);
326        assert_eq!(solution.routes[1], vec![1]);
327    }
328
329    #[test]
330    fn time_feasibility_checks_waiting_and_deadlines() {
331        let solution = TestSolution::new(vec![vec![1, 2], vec![3]]);
332
333        assert!(
334            is_time_feasible(&solution, &[1, 2]),
335            "route should wait for customer 2 and still finish in time"
336        );
337        assert!(
338            !is_time_feasible(&solution, &[2, 3]),
339            "route should miss customer 3's latest end"
340        );
341        assert!(is_kopt_feasible(&solution, 1, &[1, 2]));
342    }
343
344    #[test]
345    fn distance_meters_cover_invalid_positions() {
346        let solution = TestSolution::new(vec![vec![1, 2], vec![3]]);
347
348        assert_eq!(MatrixDistanceMeter.distance(&solution, 0, 0, 1, 0), 6.0);
349        assert_eq!(
350            MatrixIntraDistanceMeter.distance(&solution, 0, 0, 0, 1),
351            4.0
352        );
353        assert!(MatrixDistanceMeter
354            .distance(&solution, 0, 4, 1, 0)
355            .is_infinite());
356        assert!(MatrixIntraDistanceMeter
357            .distance(&solution, 0, 0, 0, 4)
358            .is_infinite());
359    }
360}