1use core::fmt;
34
35use axiolid_core::{Point2, Point3};
36
37pub const CANDIDATE_BUDGET: usize = 1 << 20;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum FrechetError {
43 EmptyPolyline,
45 NonFiniteInput,
47 InvalidBound,
49}
50
51impl fmt::Display for FrechetError {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::EmptyPolyline => formatter.write_str("a polyline has no points"),
55 Self::NonFiniteInput => formatter.write_str("polyline coordinates must be finite"),
56 Self::InvalidBound => {
57 formatter.write_str("the leash length must be finite and non-negative")
58 }
59 }
60 }
61}
62
63impl std::error::Error for FrechetError {}
64
65pub fn discrete_frechet_distance(a: &[Point3], b: &[Point3]) -> Result<f64, FrechetError> {
71 check(a)?;
72 check(b)?;
73 Ok(discrete(a, b))
74}
75
76pub fn discrete_frechet_distance_2d(a: &[Point2], b: &[Point2]) -> Result<f64, FrechetError> {
82 discrete_frechet_distance(&lift(a), &lift(b))
83}
84
85pub fn frechet_at_most(a: &[Point3], b: &[Point3], eps: f64) -> Result<bool, FrechetError> {
93 check(a)?;
94 check(b)?;
95 if !eps.is_finite() || eps < 0.0 {
96 return Err(FrechetError::InvalidBound);
97 }
98 Ok(decide(a, b, eps))
99}
100
101pub fn frechet_at_most_2d(a: &[Point2], b: &[Point2], eps: f64) -> Result<bool, FrechetError> {
107 frechet_at_most(&lift(a), &lift(b), eps)
108}
109
110pub fn frechet_distance(a: &[Point3], b: &[Point3]) -> Result<f64, FrechetError> {
116 check(a)?;
117 check(b)?;
118 Ok(continuous(a, b, CANDIDATE_BUDGET))
119}
120
121pub fn frechet_distance_2d(a: &[Point2], b: &[Point2]) -> Result<f64, FrechetError> {
127 frechet_distance(&lift(a), &lift(b))
128}
129
130fn check(points: &[Point3]) -> Result<(), FrechetError> {
131 if points.is_empty() {
132 return Err(FrechetError::EmptyPolyline);
133 }
134 if points.iter().any(|p| !p.is_finite()) {
135 return Err(FrechetError::NonFiniteInput);
136 }
137 Ok(())
138}
139
140fn lift(points: &[Point2]) -> Vec<Point3> {
141 points.iter().map(|p| Point3::new(p.x, p.y, 0.0)).collect()
142}
143
144fn distance(p: Point3, q: Point3) -> f64 {
145 (p - q).length()
146}
147
148fn discrete(a: &[Point3], b: &[Point3]) -> f64 {
149 let mut row = vec![0.0_f64; b.len()];
151 for (i, &p) in a.iter().enumerate() {
152 let mut diagonal = 0.0;
153 for (j, &q) in b.iter().enumerate() {
154 let here = distance(p, q);
155 let above = row[j];
156 let best = match (i, j) {
157 (0, 0) => 0.0,
158 (0, _) => row[j - 1],
159 (_, 0) => above,
160 _ => above.min(diagonal).min(row[j - 1]),
161 };
162 diagonal = above;
163 row[j] = here.max(best);
164 }
165 }
166 row[b.len() - 1]
167}
168
169fn foot(c: Point3, a: Point3, b: Point3) -> Option<(f64, f64)> {
172 let d = b - a;
173 let length_squared = d.length_squared();
174 if length_squared == 0.0 {
175 return None;
176 }
177 let t = (c - a).dot(d) / length_squared;
178 Some((t, distance(a + d * t, c)))
179}
180
181#[derive(Clone, Copy)]
183struct Span {
184 lo: f64,
185 hi: f64,
186}
187
188impl Span {
189 const EMPTY: Span = Span {
190 lo: f64::INFINITY,
191 hi: f64::NEG_INFINITY,
192 };
193
194 fn is_empty(self) -> bool {
195 self.lo > self.hi
196 }
197}
198
199fn free(c: Point3, a: Point3, b: Point3, eps: f64) -> Span {
206 let start = distance(a, c) <= eps;
207 let end = distance(b, c) <= eps;
208 let Some((t, h)) = foot(c, a, b) else {
209 return if start {
210 Span { lo: 0.0, hi: 1.0 }
211 } else {
212 Span::EMPTY
213 };
214 };
215 if h > eps {
216 return Span::EMPTY;
217 }
218 let w = (eps * eps - h * h).max(0.0).sqrt() / (b - a).length();
219 let lo = if start { 0.0 } else { (t - w).max(0.0) };
220 let hi = if end { 1.0 } else { (t + w).min(1.0) };
221 if lo > hi {
222 Span::EMPTY
223 } else {
224 Span { lo, hi }
225 }
226}
227
228fn decide(a: &[Point3], b: &[Point3], eps: f64) -> bool {
231 if distance(a[0], b[0]) > eps || distance(a[a.len() - 1], b[b.len() - 1]) > eps {
232 return false;
233 }
234 let (p, q) = (a.len() - 1, b.len() - 1);
235 if p == 0 || q == 0 {
236 let (point, other) = if p == 0 { (a[0], b) } else { (b[0], a) };
239 return other.iter().all(|&v| distance(point, v) <= eps);
240 }
241 let mut left: Vec<Span> = Vec::with_capacity(q);
245 let mut open = true;
246 for j in 0..q {
247 let span = free(a[0], b[j], b[j + 1], eps);
248 left.push(if open && span.lo == 0.0 {
249 span
250 } else {
251 Span::EMPTY
252 });
253 open = open && !span.is_empty() && span.lo == 0.0 && span.hi == 1.0;
254 }
255 let mut bottom_open = true;
256 for i in 0..p {
257 let span = free(b[0], a[i], a[i + 1], eps);
259 let mut bottom = if bottom_open && span.lo == 0.0 {
260 span
261 } else {
262 Span::EMPTY
263 };
264 bottom_open = bottom_open && !span.is_empty() && span.lo == 0.0 && span.hi == 1.0;
265 for j in 0..q {
266 let l = left[j];
267 let top = free(b[j + 1], a[i], a[i + 1], eps);
268 let right = free(a[i + 1], b[j], b[j + 1], eps);
269 let next_bottom = if !l.is_empty() {
270 top
271 } else if !bottom.is_empty() {
272 Span {
273 lo: bottom.lo.max(top.lo),
274 hi: top.hi,
275 }
276 } else {
277 Span::EMPTY
278 };
279 left[j] = if !bottom.is_empty() {
280 right
281 } else if !l.is_empty() {
282 Span {
283 lo: l.lo.max(right.lo),
284 hi: right.hi,
285 }
286 } else {
287 Span::EMPTY
288 };
289 bottom = next_bottom;
290 }
291 if i == p - 1 {
293 return (!bottom.is_empty() && bottom.hi == 1.0)
294 || (!left[q - 1].is_empty() && left[q - 1].hi == 1.0);
295 }
296 }
297 unreachable!("p >= 1 returns in the loop")
298}
299
300fn for_each_candidate(a: &[Point3], b: &[Point3], mut visit: impl FnMut(f64)) {
303 for &p in a {
304 for &q in b {
305 visit(distance(p, q));
306 }
307 }
308 for (points, other) in [(a, b), (b, a)] {
309 for &c in points {
310 for s in other.windows(2) {
311 if let Some((t, h)) = foot(c, s[0], s[1]) {
312 if (0.0..=1.0).contains(&t) {
313 visit(h);
314 }
315 }
316 }
317 }
318 for s in other.windows(2) {
319 let (start, d) = (s[0], s[1] - s[0]);
320 for k in 0..points.len() {
321 for l in k + 1..points.len() {
322 let n = points[l] - points[k];
323 let along = n.dot(d);
324 if along == 0.0 {
325 continue;
326 }
327 let middle = (points[k] + points[l]) * 0.5;
328 let t = n.dot(middle - start) / along;
329 if (0.0..=1.0).contains(&t) {
330 let x = start + d * t;
331 visit(distance(x, points[k]).max(distance(x, points[l])));
332 }
333 }
334 }
335 }
336 }
337}
338
339fn continuous(a: &[Point3], b: &[Point3], budget: usize) -> f64 {
340 let mut lo = distance(a[0], b[0]).max(distance(a[a.len() - 1], b[b.len() - 1]));
342 if decide(a, b, lo) {
343 return lo;
344 }
345 let mut hi = discrete(a, b);
347 loop {
349 let mut count = 0usize;
350 for_each_candidate(a, b, |c| {
351 if c > lo && c <= hi {
352 count += 1;
353 }
354 });
355 let mid = lo + (hi - lo) * 0.5;
356 if count <= budget || mid <= lo || mid >= hi {
357 break;
358 }
359 if decide(a, b, mid) {
360 hi = mid;
361 } else {
362 lo = mid;
363 }
364 }
365 let mut candidates = Vec::new();
366 for_each_candidate(a, b, |c| {
367 if c > lo && c <= hi {
368 candidates.push(c);
369 }
370 });
371 candidates.sort_by(f64::total_cmp);
372 candidates.dedup();
373 let first = candidates.partition_point(|&c| !decide(a, b, c));
375 candidates.get(first).copied().unwrap_or(hi)
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 fn walk(seed: u64, n: usize) -> Vec<Point3> {
384 let mut state = seed;
385 let mut next = || {
386 state = state
387 .wrapping_mul(6364136223846793005)
388 .wrapping_add(1442695040888963407);
389 ((state >> 11) as f64 / (1u64 << 53) as f64) * 2.0 - 1.0
390 };
391 let mut p = Point3::ZERO;
392 (0..n)
393 .map(|_| {
394 p += Point3::new(1.0 + next(), next(), next());
395 p
396 })
397 .collect()
398 }
399
400 #[test]
403 fn a_tiny_budget_bisects_to_the_same_answer() {
404 for seed in 0..16 {
405 let a = walk(seed, 12);
406 let jitter = walk(seed + 50, 12);
407 let b: Vec<Point3> = (0..12)
408 .map(|i| {
409 if i == 0 || i == 11 {
410 a[i]
411 } else {
412 a[i] + (jitter[i] - jitter[i - 1]) * 0.4
413 }
414 })
415 .collect();
416 let listed = continuous(&a, &b, usize::MAX);
417 let mut bisections = 0;
418 let mut lo = 0.0_f64;
419 let mut hi = discrete(&a, &b);
420 loop {
422 let mut count = 0usize;
423 for_each_candidate(&a, &b, |c| {
424 if c > lo && c <= hi {
425 count += 1;
426 }
427 });
428 let mid = lo + (hi - lo) * 0.5;
429 if count <= 2 || mid <= lo || mid >= hi {
430 break;
431 }
432 bisections += 1;
433 if decide(&a, &b, mid) {
434 hi = mid;
435 } else {
436 lo = mid;
437 }
438 }
439 assert!(bisections > 0, "{seed}");
440 assert_eq!(continuous(&a, &b, 2), listed, "{seed}");
441 assert!(listed > 0.0);
442 }
443 }
444}