use crate::matrix::CostMatrix;
use crate::model::{LocationId, Problem, Route, Solution};
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct RouteMetrics {
pub distance: f64,
pub duration: f64,
pub load: u32,
}
pub(crate) fn walk_route(
problem: &Problem,
matrix: &impl CostMatrix,
capacity: u32,
seq: &[usize],
) -> (RouteMetrics, bool) {
if seq.is_empty() {
let zero = RouteMetrics {
distance: 0.0,
duration: 0.0,
load: 0,
};
return (zero, true);
}
let depart = problem.depot_open();
let mut prev = LocationId::DEPOT;
let mut t = depart;
let mut distance = 0.0;
let mut load: u64 = 0;
let mut feasible = true;
for &loc in seq {
let cur = LocationId(loc);
distance += matrix.distance(prev, cur);
let arrive = t + matrix.time(prev, cur);
let stop = &problem.stops[loc - 1];
let begin = match stop.time_window {
Some(tw) => {
let begin = arrive.max(tw.start);
if begin > tw.end {
feasible = false; }
begin
}
None => arrive,
};
t = begin + stop.service_time;
load += u64::from(stop.demand);
prev = cur;
}
distance += matrix.distance(prev, LocationId::DEPOT);
t += matrix.time(prev, LocationId::DEPOT);
feasible &= t <= problem.depot_due(); feasible &= load <= u64::from(capacity);
let metrics = RouteMetrics {
distance,
duration: t - depart,
load: u32::try_from(load).unwrap_or(u32::MAX),
};
(metrics, feasible)
}
pub(crate) fn evaluate_route(
problem: &Problem,
matrix: &impl CostMatrix,
capacity: u32,
seq: &[usize],
) -> Option<RouteMetrics> {
match walk_route(problem, matrix, capacity, seq) {
(metrics, true) => Some(metrics),
(_, false) => None,
}
}
pub(crate) fn build_solution(
problem: &Problem,
matrix: &impl CostMatrix,
capacity: u32,
routes: &[Vec<usize>],
) -> Solution {
let mut out_routes = Vec::new();
let mut total_distance = 0.0;
let mut total_time = 0.0;
let mut feasible = true;
for (route, vehicle) in routes
.iter()
.filter(|r| !r.is_empty())
.zip(&problem.vehicles)
{
let (metrics, ok) = walk_route(problem, matrix, capacity, route);
feasible &= ok;
let stop_ids = route.iter().map(|&loc| problem.stops[loc - 1].id).collect();
total_distance += metrics.distance;
total_time += metrics.duration;
out_routes.push(Route {
vehicle_id: vehicle.id,
stop_ids,
load: metrics.load,
distance: metrics.distance,
time: metrics.duration,
});
}
let non_empty = routes.iter().filter(|r| !r.is_empty()).count();
if non_empty > out_routes.len() {
feasible = false; }
Solution {
routes: out_routes,
total_distance,
total_time,
feasible,
unassigned: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::matrix::EuclideanMatrix;
use crate::model::{Coord, Stop, StopId, TimeWindow, Vehicle, VehicleId};
fn problem(windows: [Option<(f64, f64)>; 3], depot_due: f64) -> Problem {
let mk = |id: u32, x: f64, w: Option<(f64, f64)>| Stop {
id: StopId(id),
coord: Coord::new(0.0, x), demand: 10,
time_window: w.map(|(start, end)| TimeWindow { start, end }),
service_time: 5.0,
};
Problem {
depot: Coord::new(0.0, 0.0),
stops: vec![
mk(1, 10.0, windows[0]),
mk(2, 20.0, windows[1]),
mk(3, 30.0, windows[2]),
],
vehicles: vec![Vehicle {
id: VehicleId(1),
capacity: 100,
}],
depot_window: Some(TimeWindow {
start: 0.0,
end: depot_due,
}),
}
}
#[test]
fn no_windows_route_is_feasible_and_metrics_add_up() {
let p = problem([None, None, None], f64::INFINITY);
let m = EuclideanMatrix::from_problem(&p);
let metrics = evaluate_route(&p, &m, 100, &[1, 2, 3]).expect("feasible");
assert!((metrics.distance - 60.0).abs() < 1e-9);
assert!((metrics.duration - 75.0).abs() < 1e-9);
assert_eq!(metrics.load, 30);
}
#[test]
fn late_window_is_rejected() {
let p = problem([None, None, Some((0.0, 15.0))], f64::INFINITY);
let m = EuclideanMatrix::from_problem(&p);
assert!(evaluate_route(&p, &m, 100, &[1, 2, 3]).is_none());
}
#[test]
fn early_arrival_waits_then_serves() {
let p = problem([Some((100.0, 200.0)), None, None], f64::INFINITY);
let m = EuclideanMatrix::from_problem(&p);
let metrics = evaluate_route(&p, &m, 100, &[1, 2, 3]).expect("feasible");
assert!(
(metrics.duration - 165.0).abs() < 1e-9,
"got {}",
metrics.duration
);
}
#[test]
fn late_depot_return_is_rejected() {
let p = problem([None, None, None], 50.0); let m = EuclideanMatrix::from_problem(&p);
assert!(evaluate_route(&p, &m, 100, &[1, 2, 3]).is_none());
}
#[test]
fn overload_is_rejected() {
let p = problem([None, None, None], f64::INFINITY);
let m = EuclideanMatrix::from_problem(&p);
assert!(evaluate_route(&p, &m, 25, &[1, 2, 3]).is_none()); }
}