solverforge_cvrp/
helpers.rs1use 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 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 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
43pub 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
60pub fn savings_depot_for_entity<S: VrpSolution>(plan: &S, entity_idx: usize) -> usize {
62 depot_for_entity(plan, entity_idx)
63}
64
65pub 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
75pub 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
89pub 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
94pub fn replace_route<S: VrpSolution>(plan: &mut S, entity_idx: usize, route: Vec<usize>) {
98 *plan.vehicle_visits_mut(entity_idx) = route;
99}
100
101pub fn get_route<S: VrpSolution>(plan: &S, entity_idx: usize) -> Vec<usize> {
105 plan.vehicle_visits(entity_idx).to_vec()
106}
107
108pub 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
121pub 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
130pub 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}