1use crate::model::{Coord, LocationId, Problem};
11
12const EARTH_RADIUS_KM: f64 = 6371.0;
14
15pub trait CostMatrix {
19 fn distance(&self, a: LocationId, b: LocationId) -> f64;
21
22 fn time(&self, a: LocationId, b: LocationId) -> f64;
24}
25
26#[derive(Debug, Clone)]
31pub struct HaversineMatrix {
32 coords: Vec<Coord>,
33 speed_kmh: f64,
34}
35
36impl HaversineMatrix {
37 #[must_use]
41 pub fn new(coords: Vec<Coord>, speed_kmh: f64) -> Self {
42 Self { coords, speed_kmh }
43 }
44
45 #[must_use]
48 pub fn from_problem(problem: &Problem, speed_kmh: f64) -> Self {
49 Self::new(problem.coords(), speed_kmh)
50 }
51}
52
53fn haversine_km(a: Coord, b: Coord) -> f64 {
55 let lat1 = a.lat.to_radians();
56 let lat2 = b.lat.to_radians();
57 let dlat = (b.lat - a.lat).to_radians();
58 let dlon = (b.lon - a.lon).to_radians();
59
60 let h = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
61 let c = 2.0 * h.sqrt().min(1.0).asin();
63 EARTH_RADIUS_KM * c
64}
65
66impl CostMatrix for HaversineMatrix {
67 fn distance(&self, a: LocationId, b: LocationId) -> f64 {
68 haversine_km(self.coords[a.0], self.coords[b.0])
69 }
70
71 fn time(&self, a: LocationId, b: LocationId) -> f64 {
72 self.distance(a, b) / self.speed_kmh
73 }
74}
75
76#[derive(Debug, Clone)]
84pub struct EuclideanMatrix {
85 coords: Vec<Coord>,
86}
87
88impl EuclideanMatrix {
89 #[must_use]
91 pub fn new(coords: Vec<Coord>) -> Self {
92 Self { coords }
93 }
94
95 #[must_use]
98 pub fn from_problem(problem: &Problem) -> Self {
99 Self::new(problem.coords())
100 }
101}
102
103fn euclidean(a: Coord, b: Coord) -> f64 {
105 let dx = a.lon - b.lon;
106 let dy = a.lat - b.lat;
107 dx.hypot(dy)
108}
109
110impl CostMatrix for EuclideanMatrix {
111 fn distance(&self, a: LocationId, b: LocationId) -> f64 {
112 euclidean(self.coords[a.0], self.coords[b.0])
113 }
114
115 fn time(&self, a: LocationId, b: LocationId) -> f64 {
116 self.distance(a, b)
118 }
119}
120
121#[derive(Debug, Clone)]
128pub struct ProvidedMatrix {
129 n: usize,
130 distances: Vec<f64>, times: Vec<f64>, }
133
134#[derive(Debug, thiserror::Error, PartialEq, Eq)]
136pub enum MatrixError {
137 #[error("matrix must be a non-empty square: {rows} rows but a row of {row_len}")]
139 NotSquare { rows: usize, row_len: usize },
140 #[error("distance matrix is {distance}×{distance} but time matrix is {time}×{time}")]
142 SizeMismatch { distance: usize, time: usize },
143}
144
145impl ProvidedMatrix {
146 pub fn new(distances: Vec<Vec<f64>>, times: Vec<Vec<f64>>) -> Result<Self, MatrixError> {
154 let (n_d, distances) = Self::flatten(distances)?;
155 let (n_t, times) = Self::flatten(times)?;
156 if n_d != n_t {
157 return Err(MatrixError::SizeMismatch {
158 distance: n_d,
159 time: n_t,
160 });
161 }
162 Ok(Self {
163 n: n_d,
164 distances,
165 times,
166 })
167 }
168
169 #[must_use]
178 pub fn densify<M: CostMatrix>(source: &M, n: usize) -> Self {
179 let mut distances = Vec::with_capacity(n * n);
180 let mut times = Vec::with_capacity(n * n);
181 for a in 0..n {
182 for b in 0..n {
183 distances.push(source.distance(LocationId(a), LocationId(b)));
184 times.push(source.time(LocationId(a), LocationId(b)));
185 }
186 }
187 Self {
188 n,
189 distances,
190 times,
191 }
192 }
193
194 #[must_use]
196 pub fn len(&self) -> usize {
197 self.n
198 }
199
200 #[must_use]
202 pub fn is_empty(&self) -> bool {
203 self.n == 0
204 }
205
206 fn flatten(rows: Vec<Vec<f64>>) -> Result<(usize, Vec<f64>), MatrixError> {
208 let n = rows.len();
209 if n == 0 {
210 return Err(MatrixError::NotSquare {
211 rows: 0,
212 row_len: 0,
213 });
214 }
215 let mut flat = Vec::with_capacity(n * n);
216 for row in rows {
217 if row.len() != n {
218 return Err(MatrixError::NotSquare {
219 rows: n,
220 row_len: row.len(),
221 });
222 }
223 flat.extend(row);
224 }
225 Ok((n, flat))
226 }
227}
228
229impl CostMatrix for ProvidedMatrix {
230 fn distance(&self, a: LocationId, b: LocationId) -> f64 {
231 self.distances[a.0 * self.n + b.0]
232 }
233
234 fn time(&self, a: LocationId, b: LocationId) -> f64 {
235 self.times[a.0 * self.n + b.0]
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn matrix() -> HaversineMatrix {
244 HaversineMatrix::new(
246 vec![
247 Coord::new(0.0, 0.0),
248 Coord::new(0.0, 1.0),
249 Coord::new(1.0, 0.0),
250 ],
251 60.0,
252 )
253 }
254
255 #[test]
256 fn distance_to_self_is_zero() {
257 let m = matrix();
258 assert_eq!(m.distance(LocationId(0), LocationId(0)), 0.0);
259 }
260
261 #[test]
262 fn distance_is_symmetric() {
263 let m = matrix();
264 for a in 0..3 {
265 for b in 0..3 {
266 let ab = m.distance(LocationId(a), LocationId(b));
267 let ba = m.distance(LocationId(b), LocationId(a));
268 assert!((ab - ba).abs() < 1e-9, "asymmetry {a}->{b}: {ab} vs {ba}");
269 }
270 }
271 }
272
273 #[test]
274 fn one_degree_is_about_111_km() {
275 let m = matrix();
276 let d = m.distance(LocationId(0), LocationId(1));
278 assert!((d - 111.19).abs() < 0.5, "expected ~111.19 km, got {d}");
279 let d_lat = m.distance(LocationId(0), LocationId(2));
281 assert!(
282 (d_lat - 111.19).abs() < 0.5,
283 "expected ~111.19 km, got {d_lat}"
284 );
285 }
286
287 #[test]
288 fn time_is_distance_over_speed() {
289 let m = matrix();
290 let d = m.distance(LocationId(0), LocationId(1));
291 let t = m.time(LocationId(0), LocationId(1));
292 assert!((t - d / 60.0).abs() < 1e-9);
293 }
294
295 #[test]
296 fn euclidean_is_pythagorean_and_time_equals_distance() {
297 let m = EuclideanMatrix::new(vec![Coord::new(0.0, 0.0), Coord::new(4.0, 3.0)]);
299 let d = m.distance(LocationId(0), LocationId(1));
300 assert!((d - 5.0).abs() < 1e-9, "expected 5.0, got {d}");
301 assert_eq!(m.time(LocationId(0), LocationId(1)), d);
303 assert_eq!(m.distance(LocationId(0), LocationId(0)), 0.0);
304 }
305
306 #[test]
307 fn provided_matrix_indexes_row_major() {
308 let m = ProvidedMatrix::new(
309 vec![
310 vec![0.0, 1.0, 2.0],
311 vec![3.0, 0.0, 4.0],
312 vec![5.0, 6.0, 0.0],
313 ],
314 vec![
315 vec![0.0, 10.0, 20.0],
316 vec![30.0, 0.0, 40.0],
317 vec![50.0, 60.0, 0.0],
318 ],
319 )
320 .expect("square");
321 assert_eq!(m.len(), 3);
322 assert_eq!(m.distance(LocationId(1), LocationId(2)), 4.0);
323 assert_eq!(m.distance(LocationId(2), LocationId(0)), 5.0);
324 assert_eq!(m.time(LocationId(0), LocationId(2)), 20.0);
325 assert_ne!(
327 m.distance(LocationId(0), LocationId(1)),
328 m.distance(LocationId(1), LocationId(0))
329 );
330 }
331
332 #[test]
333 fn provided_matrix_rejects_non_square() {
334 let err = ProvidedMatrix::new(
335 vec![vec![0.0, 1.0], vec![1.0]],
336 vec![vec![0.0, 1.0], vec![1.0, 0.0]],
337 )
338 .unwrap_err();
339 assert_eq!(
340 err,
341 MatrixError::NotSquare {
342 rows: 2,
343 row_len: 1
344 }
345 );
346 assert_eq!(
347 ProvidedMatrix::new(vec![], vec![]).unwrap_err(),
348 MatrixError::NotSquare {
349 rows: 0,
350 row_len: 0
351 }
352 );
353 }
354
355 #[test]
356 fn densify_matches_source_exactly() {
357 let h = HaversineMatrix::new(
360 vec![
361 Coord::new(48.8566, 2.3522),
362 Coord::new(48.8606, 2.3376),
363 Coord::new(48.8530, 2.3499),
364 Coord::new(48.8738, 2.2950),
365 ],
366 50.0,
367 );
368 let dense = ProvidedMatrix::densify(&h, 4);
369 assert_eq!(dense.len(), 4);
370 for a in 0..4 {
371 for b in 0..4 {
372 let (la, lb) = (LocationId(a), LocationId(b));
373 assert_eq!(dense.distance(la, lb), h.distance(la, lb));
374 assert_eq!(dense.time(la, lb), h.time(la, lb));
375 }
376 }
377 }
378
379 #[test]
380 fn provided_matrix_rejects_size_mismatch() {
381 let err =
382 ProvidedMatrix::new(vec![vec![0.0, 1.0], vec![1.0, 0.0]], vec![vec![0.0]]).unwrap_err();
383 assert_eq!(
384 err,
385 MatrixError::SizeMismatch {
386 distance: 2,
387 time: 1
388 }
389 );
390 }
391}