use crate::model::{Coord, LocationId, Problem};
const EARTH_RADIUS_KM: f64 = 6371.0;
pub trait CostMatrix {
fn distance(&self, a: LocationId, b: LocationId) -> f64;
fn time(&self, a: LocationId, b: LocationId) -> f64;
}
#[derive(Debug, Clone)]
pub struct HaversineMatrix {
coords: Vec<Coord>,
speed_kmh: f64,
}
impl HaversineMatrix {
#[must_use]
pub fn new(coords: Vec<Coord>, speed_kmh: f64) -> Self {
Self { coords, speed_kmh }
}
#[must_use]
pub fn from_problem(problem: &Problem, speed_kmh: f64) -> Self {
Self::new(problem.coords(), speed_kmh)
}
}
fn haversine_km(a: Coord, b: Coord) -> f64 {
let lat1 = a.lat.to_radians();
let lat2 = b.lat.to_radians();
let dlat = (b.lat - a.lat).to_radians();
let dlon = (b.lon - a.lon).to_radians();
let h = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * h.sqrt().min(1.0).asin();
EARTH_RADIUS_KM * c
}
impl CostMatrix for HaversineMatrix {
fn distance(&self, a: LocationId, b: LocationId) -> f64 {
haversine_km(self.coords[a.0], self.coords[b.0])
}
fn time(&self, a: LocationId, b: LocationId) -> f64 {
self.distance(a, b) / self.speed_kmh
}
}
#[derive(Debug, Clone)]
pub struct EuclideanMatrix {
coords: Vec<Coord>,
}
impl EuclideanMatrix {
#[must_use]
pub fn new(coords: Vec<Coord>) -> Self {
Self { coords }
}
#[must_use]
pub fn from_problem(problem: &Problem) -> Self {
Self::new(problem.coords())
}
}
fn euclidean(a: Coord, b: Coord) -> f64 {
let dx = a.lon - b.lon;
let dy = a.lat - b.lat;
dx.hypot(dy)
}
impl CostMatrix for EuclideanMatrix {
fn distance(&self, a: LocationId, b: LocationId) -> f64 {
euclidean(self.coords[a.0], self.coords[b.0])
}
fn time(&self, a: LocationId, b: LocationId) -> f64 {
self.distance(a, b)
}
}
#[derive(Debug, Clone)]
pub struct ProvidedMatrix {
n: usize,
distances: Vec<f64>, times: Vec<f64>, }
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum MatrixError {
#[error("matrix must be a non-empty square: {rows} rows but a row of {row_len}")]
NotSquare { rows: usize, row_len: usize },
#[error("distance matrix is {distance}×{distance} but time matrix is {time}×{time}")]
SizeMismatch { distance: usize, time: usize },
}
impl ProvidedMatrix {
pub fn new(distances: Vec<Vec<f64>>, times: Vec<Vec<f64>>) -> Result<Self, MatrixError> {
let (n_d, distances) = Self::flatten(distances)?;
let (n_t, times) = Self::flatten(times)?;
if n_d != n_t {
return Err(MatrixError::SizeMismatch {
distance: n_d,
time: n_t,
});
}
Ok(Self {
n: n_d,
distances,
times,
})
}
#[must_use]
pub fn densify<M: CostMatrix>(source: &M, n: usize) -> Self {
let mut distances = Vec::with_capacity(n * n);
let mut times = Vec::with_capacity(n * n);
for a in 0..n {
for b in 0..n {
distances.push(source.distance(LocationId(a), LocationId(b)));
times.push(source.time(LocationId(a), LocationId(b)));
}
}
Self {
n,
distances,
times,
}
}
#[must_use]
pub fn len(&self) -> usize {
self.n
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.n == 0
}
fn flatten(rows: Vec<Vec<f64>>) -> Result<(usize, Vec<f64>), MatrixError> {
let n = rows.len();
if n == 0 {
return Err(MatrixError::NotSquare {
rows: 0,
row_len: 0,
});
}
let mut flat = Vec::with_capacity(n * n);
for row in rows {
if row.len() != n {
return Err(MatrixError::NotSquare {
rows: n,
row_len: row.len(),
});
}
flat.extend(row);
}
Ok((n, flat))
}
}
impl CostMatrix for ProvidedMatrix {
fn distance(&self, a: LocationId, b: LocationId) -> f64 {
self.distances[a.0 * self.n + b.0]
}
fn time(&self, a: LocationId, b: LocationId) -> f64 {
self.times[a.0 * self.n + b.0]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn matrix() -> HaversineMatrix {
HaversineMatrix::new(
vec![
Coord::new(0.0, 0.0),
Coord::new(0.0, 1.0),
Coord::new(1.0, 0.0),
],
60.0,
)
}
#[test]
fn distance_to_self_is_zero() {
let m = matrix();
assert_eq!(m.distance(LocationId(0), LocationId(0)), 0.0);
}
#[test]
fn distance_is_symmetric() {
let m = matrix();
for a in 0..3 {
for b in 0..3 {
let ab = m.distance(LocationId(a), LocationId(b));
let ba = m.distance(LocationId(b), LocationId(a));
assert!((ab - ba).abs() < 1e-9, "asymmetry {a}->{b}: {ab} vs {ba}");
}
}
}
#[test]
fn one_degree_is_about_111_km() {
let m = matrix();
let d = m.distance(LocationId(0), LocationId(1));
assert!((d - 111.19).abs() < 0.5, "expected ~111.19 km, got {d}");
let d_lat = m.distance(LocationId(0), LocationId(2));
assert!(
(d_lat - 111.19).abs() < 0.5,
"expected ~111.19 km, got {d_lat}"
);
}
#[test]
fn time_is_distance_over_speed() {
let m = matrix();
let d = m.distance(LocationId(0), LocationId(1));
let t = m.time(LocationId(0), LocationId(1));
assert!((t - d / 60.0).abs() < 1e-9);
}
#[test]
fn euclidean_is_pythagorean_and_time_equals_distance() {
let m = EuclideanMatrix::new(vec![Coord::new(0.0, 0.0), Coord::new(4.0, 3.0)]);
let d = m.distance(LocationId(0), LocationId(1));
assert!((d - 5.0).abs() < 1e-9, "expected 5.0, got {d}");
assert_eq!(m.time(LocationId(0), LocationId(1)), d);
assert_eq!(m.distance(LocationId(0), LocationId(0)), 0.0);
}
#[test]
fn provided_matrix_indexes_row_major() {
let m = ProvidedMatrix::new(
vec![
vec![0.0, 1.0, 2.0],
vec![3.0, 0.0, 4.0],
vec![5.0, 6.0, 0.0],
],
vec![
vec![0.0, 10.0, 20.0],
vec![30.0, 0.0, 40.0],
vec![50.0, 60.0, 0.0],
],
)
.expect("square");
assert_eq!(m.len(), 3);
assert_eq!(m.distance(LocationId(1), LocationId(2)), 4.0);
assert_eq!(m.distance(LocationId(2), LocationId(0)), 5.0);
assert_eq!(m.time(LocationId(0), LocationId(2)), 20.0);
assert_ne!(
m.distance(LocationId(0), LocationId(1)),
m.distance(LocationId(1), LocationId(0))
);
}
#[test]
fn provided_matrix_rejects_non_square() {
let err = ProvidedMatrix::new(
vec![vec![0.0, 1.0], vec![1.0]],
vec![vec![0.0, 1.0], vec![1.0, 0.0]],
)
.unwrap_err();
assert_eq!(
err,
MatrixError::NotSquare {
rows: 2,
row_len: 1
}
);
assert_eq!(
ProvidedMatrix::new(vec![], vec![]).unwrap_err(),
MatrixError::NotSquare {
rows: 0,
row_len: 0
}
);
}
#[test]
fn densify_matches_source_exactly() {
let h = HaversineMatrix::new(
vec![
Coord::new(48.8566, 2.3522),
Coord::new(48.8606, 2.3376),
Coord::new(48.8530, 2.3499),
Coord::new(48.8738, 2.2950),
],
50.0,
);
let dense = ProvidedMatrix::densify(&h, 4);
assert_eq!(dense.len(), 4);
for a in 0..4 {
for b in 0..4 {
let (la, lb) = (LocationId(a), LocationId(b));
assert_eq!(dense.distance(la, lb), h.distance(la, lb));
assert_eq!(dense.time(la, lb), h.time(la, lb));
}
}
}
#[test]
fn provided_matrix_rejects_size_mismatch() {
let err =
ProvidedMatrix::new(vec![vec![0.0, 1.0], vec![1.0, 0.0]], vec![vec![0.0]]).unwrap_err();
assert_eq!(
err,
MatrixError::SizeMismatch {
distance: 2,
time: 1
}
);
}
}