1use crate::feasibility::{build_solution, evaluate_route};
12use crate::matrix::CostMatrix;
13use crate::model::{LocationId, Problem, Solution, Unassigned, UnassignedCode};
14
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
22pub enum SolveError {
23 #[error("no vehicles in the problem")]
25 NoVehicles,
26}
27
28pub fn clarke_wright(problem: &Problem, matrix: &impl CostMatrix) -> Result<Solution, SolveError> {
45 if problem.vehicles.is_empty() {
46 return Err(SolveError::NoVehicles);
47 }
48 let n = problem.stops.len();
49 let fleet = problem.vehicles.len();
50
51 let capacity = problem
53 .vehicles
54 .iter()
55 .map(|v| v.capacity)
56 .min()
57 .unwrap_or(0);
58
59 let mut unassigned: Vec<Unassigned> = Vec::new();
63 let mut excluded = vec![false; n];
64 for (idx, stop) in problem.stops.iter().enumerate() {
65 if stop.demand > capacity {
66 excluded[idx] = true;
67 unassigned.push(Unassigned {
68 stop: stop.id,
69 code: UnassignedCode::CapacityExceeded,
70 detail: format!("demand {} exceeds vehicle capacity {capacity}", stop.demand),
71 });
72 } else if evaluate_route(problem, matrix, capacity, &[idx + 1]).is_none() {
73 excluded[idx] = true;
74 unassigned.push(Unassigned {
75 stop: stop.id,
76 code: UnassignedCode::TimeWindowInfeasible,
77 detail: "cannot be reached and serviced within its time window, then \
78 returned before the depot closes — even alone on an empty route"
79 .to_string(),
80 });
81 }
82 }
83
84 let mut routes: Vec<Vec<usize>> = (1..=n)
89 .map(|loc| {
90 if excluded[loc - 1] {
91 Vec::new()
92 } else {
93 vec![loc]
94 }
95 })
96 .collect();
97 let mut route_of: Vec<usize> = (0..n).collect();
98 let mut load: Vec<u32> = problem.stops.iter().map(|s| s.demand).collect();
99
100 let depot = LocationId::DEPOT;
102 let mut savings: Vec<(f64, usize, usize)> = Vec::with_capacity(n * n / 2);
103 for i in 1..=n {
104 if excluded[i - 1] {
105 continue;
106 }
107 let d_di = matrix.distance(depot, LocationId(i));
108 for j in (i + 1)..=n {
109 if excluded[j - 1] {
110 continue;
111 }
112 let s = d_di + matrix.distance(depot, LocationId(j))
113 - matrix.distance(LocationId(i), LocationId(j));
114 savings.push((s, i, j));
115 }
116 }
117 savings.sort_by(|a, b| b.0.total_cmp(&a.0));
119
120 for (s, i, j) in savings {
121 if s <= 0.0 {
123 break;
124 }
125 let ri = route_of[i - 1];
126 let rj = route_of[j - 1];
127 if ri == rj {
128 continue;
129 }
130 if u64::from(load[ri]) + u64::from(load[rj]) > u64::from(capacity) {
134 continue;
135 }
136
137 let i_first = routes[ri].first().copied() == Some(i);
140 let i_last = routes[ri].last().copied() == Some(i);
141 let j_first = routes[rj].first().copied() == Some(j);
142 let j_last = routes[rj].last().copied() == Some(j);
143 if !(i_first || i_last) || !(j_first || j_last) {
144 continue;
145 }
146
147 let mut left = routes[ri].clone();
150 let mut right = routes[rj].clone();
151 if left.first().copied() == Some(i) {
152 left.reverse();
153 }
154 if right.last().copied() == Some(j) {
155 right.reverse();
156 }
157 left.extend_from_slice(&right);
158
159 if evaluate_route(problem, matrix, capacity, &left).is_none() {
161 continue;
162 }
163
164 for &loc in &left {
165 route_of[loc - 1] = ri;
166 }
167 load[ri] += load[rj];
168 load[rj] = 0;
169 routes[ri] = left;
170 routes[rj].clear(); }
172
173 let mut final_routes: Vec<Vec<usize>> = routes.into_iter().filter(|r| !r.is_empty()).collect();
174
175 if final_routes.len() > fleet {
182 final_routes.sort_by_key(|r| std::cmp::Reverse(r.len()));
183 let surplus = final_routes.split_off(fleet);
184 for route in surplus {
185 for loc in route {
186 place_or_diagnose(
187 problem,
188 matrix,
189 capacity,
190 &mut final_routes,
191 loc,
192 &mut unassigned,
193 );
194 }
195 }
196 }
197
198 let mut solution = build_solution(problem, matrix, capacity, &final_routes);
199 solution.feasible = solution.feasible && unassigned.is_empty();
200 solution.unassigned = unassigned;
201 Ok(solution)
202}
203
204fn place_or_diagnose(
210 problem: &Problem,
211 matrix: &impl CostMatrix,
212 capacity: u32,
213 kept: &mut [Vec<usize>],
214 loc: usize,
215 unassigned: &mut Vec<Unassigned>,
216) {
217 let stop = &problem.stops[loc - 1];
218 let mut all_capacity_blocked = true;
219
220 for route in kept.iter_mut() {
221 let route_load: u64 = route
224 .iter()
225 .map(|&l| u64::from(problem.stops[l - 1].demand))
226 .sum();
227 if route_load + u64::from(stop.demand) > u64::from(capacity) {
228 continue; }
230 all_capacity_blocked = false;
231 for pos in 0..=route.len() {
232 let mut candidate = route.clone();
233 candidate.insert(pos, loc);
234 if evaluate_route(problem, matrix, capacity, &candidate).is_some() {
235 *route = candidate;
236 return; }
238 }
239 }
240
241 let blocker = if all_capacity_blocked {
244 "every assigned route is already at capacity"
245 } else {
246 "no assigned route has a time-feasible slot for it"
247 };
248 unassigned.push(Unassigned {
249 stop: stop.id,
250 code: UnassignedCode::NoCompatibleVehicle,
251 detail: format!(
252 "the fleet of {} vehicle(s) is fully assigned and this stop could not be \
253 inserted into any existing route ({blocker})",
254 kept.len()
255 ),
256 });
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::matrix::HaversineMatrix;
263 use crate::model::{Coord, Stop, StopId, TimeWindow, Vehicle, VehicleId};
264 use std::collections::HashSet;
265
266 fn stop(id: u32, lat: f64, lon: f64, demand: u32) -> Stop {
267 Stop {
268 id: StopId(id),
269 coord: Coord::new(lat, lon),
270 demand,
271 time_window: None,
272 service_time: 0.1,
273 }
274 }
275
276 fn sample_problem() -> Problem {
277 Problem {
278 depot: Coord::new(48.8566, 2.3522),
279 stops: vec![
280 stop(1, 48.8606, 2.3376, 10),
281 stop(2, 48.8530, 2.3499, 15),
282 stop(3, 48.8738, 2.2950, 20),
283 stop(4, 48.8462, 2.3372, 12),
284 stop(5, 48.8867, 2.3431, 8),
285 stop(6, 48.8330, 2.3708, 25),
286 ],
287 vehicles: vec![
288 Vehicle {
289 id: VehicleId(1),
290 capacity: 50,
291 },
292 Vehicle {
293 id: VehicleId(2),
294 capacity: 50,
295 },
296 Vehicle {
297 id: VehicleId(3),
298 capacity: 50,
299 },
300 ],
301 depot_window: None,
302 }
303 }
304
305 #[test]
306 fn each_route_respects_capacity() {
307 let problem = sample_problem();
308 let matrix = HaversineMatrix::from_problem(&problem, 50.0);
309 let solution = clarke_wright(&problem, &matrix).expect("feasible");
310
311 assert!(solution.feasible);
312 for route in &solution.routes {
313 assert!(route.load <= 50, "route load {} > 50", route.load);
314 }
315 }
316
317 #[test]
318 fn total_load_equals_total_demand() {
319 let problem = sample_problem();
320 let matrix = HaversineMatrix::from_problem(&problem, 50.0);
321 let solution = clarke_wright(&problem, &matrix).expect("feasible");
322
323 let demand: u32 = problem.stops.iter().map(|s| s.demand).sum();
324 let load: u32 = solution.routes.iter().map(|r| r.load).sum();
325 assert_eq!(load, demand);
326 }
327
328 #[test]
329 fn every_stop_served_exactly_once() {
330 let problem = sample_problem();
331 let matrix = HaversineMatrix::from_problem(&problem, 50.0);
332 let solution = clarke_wright(&problem, &matrix).expect("feasible");
333
334 let mut seen: Vec<StopId> = solution
335 .routes
336 .iter()
337 .flat_map(|r| r.stop_ids.iter().copied())
338 .collect();
339 let unique: HashSet<StopId> = seen.iter().copied().collect();
340
341 assert_eq!(
342 seen.len(),
343 problem.stops.len(),
344 "wrong number of stops served"
345 );
346 assert_eq!(unique.len(), problem.stops.len(), "a stop was duplicated");
347
348 seen.sort();
349 let mut expected: Vec<StopId> = problem.stops.iter().map(|s| s.id).collect();
350 expected.sort();
351 assert_eq!(seen, expected);
352 }
353
354 #[test]
355 fn oversized_demand_is_unassigned() {
356 let mut problem = sample_problem();
358 problem.stops[0].demand = 999;
359 let matrix = HaversineMatrix::from_problem(&problem, 50.0);
360 let solution = clarke_wright(&problem, &matrix).expect("routable stops still solved");
361
362 assert!(
363 !solution.feasible,
364 "an unassigned stop must flip feasible to false"
365 );
366 assert_eq!(solution.unassigned.len(), 1);
367 let u = &solution.unassigned[0];
368 assert_eq!(u.stop, StopId(1));
369 assert_eq!(u.code, UnassignedCode::CapacityExceeded);
370 assert!(u.detail.contains("999"), "detail should quote the demand");
371
372 let served: usize = solution.routes.iter().map(|r| r.stop_ids.len()).sum();
374 assert_eq!(served, problem.stops.len() - 1);
375 assert!(
376 solution
377 .routes
378 .iter()
379 .flat_map(|r| &r.stop_ids)
380 .all(|s| *s != StopId(1))
381 );
382 }
383
384 #[test]
385 fn unreachable_window_is_unassigned() {
386 let mut problem = sample_problem();
389 problem.stops[2].time_window = Some(TimeWindow {
390 start: 0.0,
391 end: 0.001,
392 });
393 let matrix = HaversineMatrix::from_problem(&problem, 50.0);
394 let solution = clarke_wright(&problem, &matrix).expect("other stops solved");
395
396 let u = solution
397 .unassigned
398 .iter()
399 .find(|u| u.stop == StopId(3))
400 .expect("stop 3 should be unassigned");
401 assert_eq!(u.code, UnassignedCode::TimeWindowInfeasible);
402 assert!(!solution.feasible);
403 }
404
405 #[test]
406 fn fleet_exhaustion_unassigns_with_no_compatible_vehicle() {
407 let problem = Problem {
410 depot: Coord::new(48.8566, 2.3522),
411 stops: vec![
412 stop(1, 48.8606, 2.3376, 30),
413 stop(2, 48.8530, 2.3499, 30),
414 stop(3, 48.8738, 2.2950, 30),
415 stop(4, 48.8462, 2.3372, 30),
416 ],
417 vehicles: vec![
418 Vehicle {
419 id: VehicleId(1),
420 capacity: 30,
421 },
422 Vehicle {
423 id: VehicleId(2),
424 capacity: 30,
425 },
426 ],
427 depot_window: None,
428 };
429 let matrix = HaversineMatrix::from_problem(&problem, 50.0);
430 let solution = clarke_wright(&problem, &matrix).expect("two stops solved");
431
432 assert!(!solution.feasible);
433 assert_eq!(solution.routes.len(), 2);
434 assert_eq!(solution.unassigned.len(), 2);
435 assert!(
436 solution
437 .unassigned
438 .iter()
439 .all(|u| u.code == UnassignedCode::NoCompatibleVehicle)
440 );
441
442 let routed: usize = solution.routes.iter().map(|r| r.stop_ids.len()).sum();
444 assert_eq!(routed + solution.unassigned.len(), problem.stops.len());
445 }
446
447 #[test]
448 fn rejects_empty_fleet() {
449 let mut problem = sample_problem();
450 problem.vehicles.clear();
451 let matrix = HaversineMatrix::from_problem(&problem, 50.0);
452 assert_eq!(
453 clarke_wright(&problem, &matrix),
454 Err(SolveError::NoVehicles)
455 );
456 }
457
458 #[test]
459 fn merges_into_fewer_routes_than_stops() {
460 let problem = sample_problem();
462 let matrix = HaversineMatrix::from_problem(&problem, 50.0);
463 let solution = clarke_wright(&problem, &matrix).expect("feasible");
464 assert!(solution.routes.len() < problem.stops.len());
465 }
466}