Skip to main content

solverforge_cvrp/
helpers.rs

1use crate::{ProblemData, VrpSolution};
2
3#[inline]
4pub(crate) fn problem_data_for_entity<S: VrpSolution>(
5    plan: &S,
6    entity_idx: usize,
7) -> Option<&ProblemData> {
8    if entity_idx >= plan.vehicle_count() {
9        return None;
10    }
11    let ptr = plan.vehicle_data_ptr(entity_idx);
12    assert!(
13        !ptr.is_null(),
14        "VrpSolution::vehicle_data_ptr({entity_idx}) returned null for a non-empty fleet"
15    );
16    // SAFETY: VrpSolution implementors guarantee valid pointers for the duration
17    // of the solve call; null for a non-empty fleet is rejected above.
18    unsafe { ptr.as_ref() }
19}
20
21#[inline]
22fn optional_problem_data_for_entity<S: VrpSolution>(
23    plan: &S,
24    entity_idx: usize,
25) -> Option<&ProblemData> {
26    if entity_idx >= plan.vehicle_count() {
27        return None;
28    }
29    let ptr = plan.vehicle_data_ptr(entity_idx);
30    if ptr.is_null() {
31        return None;
32    }
33    // SAFETY: VrpSolution implementors guarantee valid pointers for the duration
34    // of the solve call; this path treats null as non-admissible instead of
35    // panicking because it is used by feasibility gates.
36    unsafe { ptr.as_ref() }
37}
38
39pub fn depot_for_entity<S: VrpSolution>(plan: &S, entity_idx: usize) -> usize {
40    problem_data_for_entity(plan, entity_idx).map_or(0, |data| data.depot)
41}
42
43/// Construction metric class for the route owner.
44///
45/// Owners that share the same `ProblemData` pointer share depot and distance
46/// behavior, so Clarke-Wright can compute their savings rows once.
47pub fn savings_metric_class<S: VrpSolution>(plan: &S, entity_idx: usize) -> usize {
48    if entity_idx >= plan.vehicle_count() {
49        return entity_idx;
50    }
51
52    let ptr = plan.vehicle_data_ptr(entity_idx);
53    assert!(
54        !ptr.is_null(),
55        "VrpSolution::vehicle_data_ptr({entity_idx}) returned null for a non-empty fleet"
56    );
57    ptr as usize
58}
59
60/// Depot token used by Clarke-Wright construction.
61pub fn savings_depot_for_entity<S: VrpSolution>(plan: &S, entity_idx: usize) -> usize {
62    depot_for_entity(plan, entity_idx)
63}
64
65/// Construction distance used by Clarke-Wright for models that share CVRP route data.
66pub fn savings_distance<S: VrpSolution>(
67    plan: &S,
68    entity_idx: usize,
69    from: usize,
70    to: usize,
71) -> i64 {
72    route_distance(plan, entity_idx, from, to)
73}
74
75/// Construction admissibility used by Clarke-Wright for stock CVRP route data.
76///
77/// This intentionally rejects only routes that cannot be evaluated safely.
78/// Capacity and time-window violations remain scoreable during construction.
79pub fn savings_feasible<S: VrpSolution>(plan: &S, entity_idx: usize, route: &[usize]) -> bool {
80    if route.is_empty() {
81        return true;
82    }
83    let Some(data) = optional_problem_data_for_entity(plan, entity_idx) else {
84        return false;
85    };
86    route_is_structurally_valid(route, data)
87}
88
89/// Distance between two element indices for the route owner.
90pub fn route_distance<S: VrpSolution>(plan: &S, entity_idx: usize, from: usize, to: usize) -> i64 {
91    problem_data_for_entity(plan, entity_idx).map_or(0, |data| data.distance_cost(from, to))
92}
93
94/// Replaces the current route for entity `entity_idx`.
95///
96/// Callers must pass a valid `entity_idx` for the current solution.
97pub fn replace_route<S: VrpSolution>(plan: &mut S, entity_idx: usize, route: Vec<usize>) {
98    *plan.vehicle_visits_mut(entity_idx) = route;
99}
100
101/// Returns a cloned snapshot of the route for entity `entity_idx`.
102///
103/// Callers must pass a valid `entity_idx` for the current solution.
104pub fn get_route<S: VrpSolution>(plan: &S, entity_idx: usize) -> Vec<usize> {
105    plan.vehicle_visits(entity_idx).to_vec()
106}
107
108/// Returns `true` if the route satisfies stock CVRP route-local feasibility.
109pub fn route_feasible<S: VrpSolution>(plan: &S, entity_idx: usize, route: &[usize]) -> bool {
110    if route.is_empty() {
111        return true;
112    }
113    let Some(data) = optional_problem_data_for_entity(plan, entity_idx) else {
114        return false;
115    };
116    route_is_structurally_valid(route, data)
117        && route_is_capacity_feasible(route, data)
118        && route_is_time_feasible(route, data)
119}
120
121/// Route-local hook bundle used by `#[planning_list_variable(domain = "cvrp")]`.
122pub mod route_hooks {
123    pub use super::depot_for_entity as depot;
124    pub use super::get_route as get;
125    pub use super::replace_route as set;
126    pub use super::route_distance as distance;
127    pub use super::route_feasible as feasible;
128}
129
130/// Clarke-Wright savings hook bundle used by `#[planning_list_variable(domain = "cvrp")]`.
131///
132/// Advanced users can wire this explicitly when custom macro attributes should
133/// still share stock CVRP construction data.
134pub mod savings_hooks {
135    pub use super::savings_depot_for_entity as depot;
136    pub use super::savings_distance as distance;
137    pub use super::savings_feasible as feasible;
138}
139
140fn route_is_structurally_valid(route: &[usize], data: &ProblemData) -> bool {
141    let Some(max_visit) = route.iter().copied().max() else {
142        return true;
143    };
144    let max_node = max_visit.max(data.depot);
145
146    if max_visit >= data.demands.len()
147        || max_visit >= data.time_windows.len()
148        || max_visit >= data.service_durations.len()
149        || max_node >= data.distance_matrix.len()
150        || max_node >= data.travel_times.len()
151    {
152        return false;
153    }
154
155    route
156        .iter()
157        .chain(std::iter::once(&data.depot))
158        .all(|&node| {
159            data.distance_matrix
160                .get(node)
161                .is_some_and(|row| row.len() > max_node)
162                && data
163                    .travel_times
164                    .get(node)
165                    .is_some_and(|row| row.len() > max_node)
166        })
167}
168
169fn route_is_capacity_feasible(route: &[usize], data: &ProblemData) -> bool {
170    let mut total = 0_i64;
171    for &visit in route {
172        let demand = i64::from(data.demands[visit]);
173        total = match total.checked_add(demand) {
174            Some(total) => total,
175            None => return false,
176        };
177    }
178    total <= data.capacity
179}
180
181fn route_is_time_feasible(route: &[usize], data: &ProblemData) -> bool {
182    let mut current_time = data.vehicle_departure_time;
183    let mut previous = data.depot;
184
185    for &visit in route {
186        let Some(travel_time) = data.travel_time(previous, visit) else {
187            return false;
188        };
189        current_time = match current_time.checked_add(travel_time) {
190            Some(current_time) => current_time,
191            None => return false,
192        };
193
194        let (min_start, max_end) = data.time_windows[visit];
195        if current_time < min_start {
196            current_time = min_start;
197        }
198
199        let service_duration = data.service_durations[visit];
200        if service_duration < 0 {
201            return false;
202        }
203        current_time = match current_time.checked_add(service_duration) {
204            Some(current_time) => current_time,
205            None => return false,
206        };
207        if current_time > max_end {
208            return false;
209        }
210
211        previous = visit;
212    }
213
214    let Some(return_time) = data.travel_time(previous, data.depot) else {
215        return false;
216    };
217    current_time.checked_add(return_time).is_some()
218}