geometry_strategy/cartesian/distance_pythagoras.rs
1//! Pythagorean point-to-point distance for the Cartesian family.
2//!
3//! Mirrors `boost/geometry/strategies/cartesian/distance_pythagoras.hpp`:
4//!
5//! * lines 44-66 — the `detail::compute_pythagoras<I, T>` template
6//! recursion that walks the coordinates of the two points, summing
7//! the squared per-dimension differences. The Rust port reproduces
8//! the recursion via a sealed `SumSquares` trait so that
9//! `Point::get::<D>` is always invoked with a *const-generic* `D`,
10//! matching the C++ `template <std::size_t Dim>` access requirement.
11//! * lines 71-117 — `namespace comparable::pythagoras`: the
12//! squared-distance form ([`ComparablePythagoras`]) that callers
13//! compare without paying for a `sqrt`.
14//! * lines 134-173 — `pythagoras`: the sqrt-paying companion
15//! ([`Pythagoras`]).
16//! * lines 276-283 — the `services::default_strategy<…, cartesian_tag,
17//! cartesian_tag>` specialisation that picks `pythagoras<>` as the
18//! default Cartesian × Cartesian distance strategy — reproduced as
19//! `impl DefaultDistance<CartesianFamily> for CartesianFamily`.
20//!
21//! For the calculation-type policy we deliberately deviate from the
22//! C++ side: Boost runs the two scalars through
23//! `util::calculation_type::geometric::binary` (`util/calculation_type.hpp`)
24//! which promotes the working type independently of the input pair.
25//! The v1 Rust port follows the T22 spec's "for simplicity" branch and
26//! requires `P2::Scalar = P1::Scalar`; the [`Promote`](geometry_coords::Promote) lattice is
27//! ready to fold in once a mixed-scalar caller appears
28//! (see `geometry-coords::Promote`).
29
30use geometry_coords::CoordinateScalar;
31use geometry_cs::{CartesianFamily, CoordinateSystem};
32use geometry_tag::SameAs;
33use geometry_trait::Point;
34
35use crate::distance::{DefaultDistance, DistanceStrategy};
36
37/// Pythagorean (Euclidean) distance for Cartesian points of any
38/// dimension supported by `MAX_DIM` (from `geometry-trait`).
39///
40/// Mirrors `boost::geometry::strategy::distance::pythagoras<CalcType>`
41/// from `strategies/cartesian/distance_pythagoras.hpp:134-173`. The
42/// associated [`DistanceStrategy::Comparable`] type is
43/// [`ComparablePythagoras`] — the squared-distance form, mirroring
44/// `boost::geometry::strategy::distance::comparable::pythagoras`.
45#[derive(Debug, Default, Clone, Copy)]
46pub struct Pythagoras;
47
48/// Squared Pythagorean distance for Cartesian points.
49///
50/// Mirrors `boost::geometry::strategy::distance::comparable::pythagoras`
51/// from `strategies/cartesian/distance_pythagoras.hpp:71-117`. The
52/// `Comparable = Self` projection matches Boost's
53/// `comparable_type<comparable::pythagoras<…>>` specialisation at
54/// `:242-246`.
55#[derive(Debug, Default, Clone, Copy)]
56pub struct ComparablePythagoras;
57
58// ---- DistanceStrategy impls ------------------------------------------
59//
60// The family-equality bound `SameAs<CartesianFamily>` is what enforces
61// the Cartesian-only rule. When a caller wires a `Geographic` or
62// `Spherical` point through here by mistake — the silent-Cartesian
63// trap from proposal §8 — the unsatisfied bound surfaces the
64// `#[diagnostic::on_unimplemented]` plate that lives on
65// `geometry_tag::SameAs`, which points users at `WithCs<_,
66// Geographic<Degree>>` / `WithCs<_, Spherical<Degree>>` and the
67// CS-appropriate strategies (Haversine, Andoyer, Vincenty).
68
69impl<P1, P2> DistanceStrategy<P1, P2> for Pythagoras
70where
71 P1: Point,
72 P2: Point<Scalar = P1::Scalar>,
73 <P1::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
74 <P2::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
75{
76 type Out = P1::Scalar;
77 type Comparable = ComparablePythagoras;
78
79 #[inline]
80 fn distance(&self, a: &P1, b: &P2) -> Self::Out {
81 // Pay the `sqrt` on top of the comparable form, mirroring
82 // `strategies/cartesian/distance_pythagoras.hpp:160-172`.
83 sum_squared_diffs::<P1, P2>(a, b).sqrt()
84 }
85
86 #[inline]
87 fn comparable(&self) -> Self::Comparable {
88 ComparablePythagoras
89 }
90}
91
92impl<P1, P2> DistanceStrategy<P1, P2> for ComparablePythagoras
93where
94 P1: Point,
95 P2: Point<Scalar = P1::Scalar>,
96 <P1::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
97 <P2::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
98{
99 type Out = P1::Scalar;
100 type Comparable = Self;
101
102 #[inline]
103 fn distance(&self, a: &P1, b: &P2) -> Self::Out {
104 sum_squared_diffs::<P1, P2>(a, b)
105 }
106
107 #[inline]
108 fn comparable(&self) -> Self::Comparable {
109 ComparablePythagoras
110 }
111}
112
113// ---- Default Cartesian × Cartesian = Pythagoras ----------------------
114
115/// Cartesian × Cartesian defaults to Pythagoras.
116///
117/// Mirrors the `services::default_strategy<point_tag, point_tag, P1,
118/// P2, cartesian_tag, cartesian_tag>` specialisation at
119/// `strategies/cartesian/distance_pythagoras.hpp:276-283`.
120impl DefaultDistance<CartesianFamily> for CartesianFamily {
121 type Strategy = Pythagoras;
122}
123
124// ---- Const-recursive helper ------------------------------------------
125//
126// Mirrors `detail::compute_pythagoras<I, T>` from
127// `strategies/cartesian/distance_pythagoras.hpp:44-66`. The C++ template
128// recursion counts *down* from `I = dimension<P1>::value` to `0`; we
129// count *up* via a sealed trait, the same shape as `geometry_trait`'s
130// `Recurse` helper (see `point.rs::fold_dims`). Counting up reads more
131// naturally in Rust without changing what gets computed.
132//
133// `Point::get::<D>` must be invoked with a const-generic `D`, so we
134// cannot use the runtime-`usize` `fold_dims` from `geometry-trait` here
135// — `fold_dims` was designed for closures that *label* the dimension
136// for diagnostics, not for kernels that need to *index* into it. Hence
137// the dedicated recursion below, unrolled up to [`MAX_DIM`].
138
139/// Largest `DIM` the squared-difference walk supports on stable Rust.
140/// Matches `geometry_trait::MAX_DIM` so any [`Point`] the kernel
141/// accepts has a Pythagoras impl as well.
142const MAX_DIM: usize = 4;
143
144/// Entry point of the squared-difference recursion.
145///
146/// Dispatches on `P1::DIM` to the right `(0, N)` start of the recursion,
147/// then descends one dimension at a time via [`SumSquares`].
148#[inline]
149fn sum_squared_diffs<P1, P2>(a: &P1, b: &P2) -> P1::Scalar
150where
151 P1: Point,
152 P2: Point<Scalar = P1::Scalar>,
153{
154 // `P1::DIM` is a monomorphisation-time constant but cannot appear
155 // in a const-generic position on stable Rust. Same shape as
156 // `geometry_trait::fold_dims` — match to the right `(0, N)` start.
157 match P1::DIM {
158 1 => <Walk<0, 1> as SumSquares<0, 1>>::step(P1::Scalar::ZERO, a, b),
159 2 => <Walk<0, 2> as SumSquares<0, 2>>::step(P1::Scalar::ZERO, a, b),
160 3 => <Walk<0, 3> as SumSquares<0, 3>>::step(P1::Scalar::ZERO, a, b),
161 4 => <Walk<0, 4> as SumSquares<0, 4>>::step(P1::Scalar::ZERO, a, b),
162 _ => panic!("Pythagoras: P1::DIM exceeds MAX_DIM ({MAX_DIM})"),
163 }
164}
165
166/// Cursor marker carrying `(current, end)` dimension indices.
167/// Private — reachable only via [`sum_squared_diffs`].
168struct Walk<const I: usize, const N: usize>;
169
170/// Sealed const-recursive iterator: at step `(I, N)` adds
171/// `(a.get::<I>() − b.get::<I>())²` to the accumulator and descends to
172/// `(I + 1, N)`. Base case is `I == N` — return the accumulator.
173///
174/// Mirrors `detail::compute_pythagoras<I, T>` from
175/// `strategies/cartesian/distance_pythagoras.hpp:44-66`.
176trait SumSquares<const I: usize, const N: usize>: sealed::Sealed<I, N> {
177 fn step<P1, P2>(acc: P1::Scalar, a: &P1, b: &P2) -> P1::Scalar
178 where
179 P1: Point,
180 P2: Point<Scalar = P1::Scalar>;
181}
182
183mod sealed {
184 pub trait Sealed<const I: usize, const N: usize> {}
185}
186
187// Base case: `I == N` — nothing left to visit. Counterpart to the
188// `compute_pythagoras<0, T>` partial specialisation at
189// `distance_pythagoras.hpp:57-65`.
190impl<const N: usize> sealed::Sealed<N, N> for Walk<N, N> {}
191impl<const N: usize> SumSquares<N, N> for Walk<N, N> {
192 #[inline]
193 fn step<P1, P2>(acc: P1::Scalar, _a: &P1, _b: &P2) -> P1::Scalar
194 where
195 P1: Point,
196 P2: Point<Scalar = P1::Scalar>,
197 {
198 acc
199 }
200}
201
202/// Inductive step macro: one impl per `(I, N)` pair with `I < N`.
203///
204/// We cannot write `I + 1` in a generic bound on stable Rust, so the
205/// recursion is unrolled — same trick as `geometry_trait::fold_dims`.
206/// Keep this in sync with [`MAX_DIM`].
207macro_rules! impl_sum_squares {
208 ($i:expr, $n:expr) => {
209 impl sealed::Sealed<$i, $n> for Walk<$i, $n> {}
210 impl SumSquares<$i, $n> for Walk<$i, $n> {
211 #[inline]
212 fn step<P1, P2>(acc: P1::Scalar, a: &P1, b: &P2) -> P1::Scalar
213 where
214 P1: Point,
215 P2: Point<Scalar = P1::Scalar>,
216 {
217 let d = a.get::<$i>() - b.get::<$i>();
218 let acc = acc + d * d;
219 <Walk<{ $i + 1 }, $n> as SumSquares<{ $i + 1 }, $n>>::step(acc, a, b)
220 }
221 }
222 };
223}
224
225// All `(I, N)` pairs with `0 <= I < N <= MAX_DIM`.
226impl_sum_squares!(0, 1);
227impl_sum_squares!(0, 2);
228impl_sum_squares!(1, 2);
229impl_sum_squares!(0, 3);
230impl_sum_squares!(1, 3);
231impl_sum_squares!(2, 3);
232impl_sum_squares!(0, 4);
233impl_sum_squares!(1, 4);
234impl_sum_squares!(2, 4);
235impl_sum_squares!(3, 4);
236
237// ---- Tests -----------------------------------------------------------
238
239#[cfg(test)]
240mod tests {
241 //! Reference values come from
242 //! `geometry/test/strategies/pythagoras.cpp`; each test cites the
243 //! line range it mirrors.
244
245 use super::{ComparablePythagoras, Pythagoras};
246 use crate::distance::DistanceStrategy;
247 use geometry_cs::Cartesian;
248 use geometry_model::{Point2D, Point3D};
249
250 /// `pythagoras.cpp:50-66` — arbitrary 2D pair, classic 3-4-5
251 /// triangle.
252 #[test]
253 fn three_four_five_2d() {
254 let a = Point2D::<f64, Cartesian>::new(0.0, 0.0);
255 let b = Point2D::<f64, Cartesian>::new(3.0, 4.0);
256 assert!((Pythagoras.distance(&a, &b) - 5.0).abs() < 1e-12);
257 assert!((ComparablePythagoras.distance(&a, &b) - 25.0).abs() < 1e-12);
258 }
259
260 /// `pythagoras.cpp:76-88` — unit axes in 3D.
261 #[test]
262 fn unit_axis_3d() {
263 let o = Point3D::<f64, Cartesian>::new(0.0, 0.0, 0.0);
264 let px = Point3D::<f64, Cartesian>::new(1.0, 0.0, 0.0);
265 let py = Point3D::<f64, Cartesian>::new(0.0, 1.0, 0.0);
266 let pz = Point3D::<f64, Cartesian>::new(0.0, 0.0, 1.0);
267 assert!((Pythagoras.distance(&o, &px) - 1.0).abs() < 1e-12);
268 assert!((Pythagoras.distance(&o, &py) - 1.0).abs() < 1e-12);
269 assert!((Pythagoras.distance(&o, &pz) - 1.0).abs() < 1e-12);
270 }
271
272 /// `pythagoras.cpp:90-115` — arbitrary 3D pair, squared = 116,
273 /// distance ≈ `10.770_329_614_27`.
274 #[test]
275 fn arbitrary_3d() {
276 let a = Point3D::<f64, Cartesian>::new(1.0, 2.0, 3.0);
277 let b = Point3D::<f64, Cartesian>::new(9.0, 8.0, 7.0);
278 let d = Pythagoras.distance(&a, &b);
279 assert!((d - 10.770_329_614_269_007).abs() < 1e-9);
280 assert!((ComparablePythagoras.distance(&a, &b) - 116.0).abs() < 1e-12);
281 }
282
283 /// `pythagoras.cpp:136-187` (`test_services`) — the strategy must
284 /// produce the same value when the arguments are swapped.
285 #[test]
286 fn symmetric_in_arguments() {
287 let a = Point3D::<f64, Cartesian>::new(1.0, 2.0, 3.0);
288 let b = Point3D::<f64, Cartesian>::new(4.0, 5.0, 6.0);
289 let ab = Pythagoras.distance(&a, &b);
290 let ba = Pythagoras.distance(&b, &a);
291 assert!((ab - ba).abs() < 1e-12);
292 // sqrt(3² + 3² + 3²) = sqrt(27).
293 assert!((ab - 27.0_f64.sqrt()).abs() < 1e-12);
294 }
295
296 /// `pythagoras.cpp:162-186` (`comparable_type`) — the comparable
297 /// form preserves order against the real distance form, which is
298 /// the whole point of skipping the sqrt.
299 #[test]
300 fn comparable_orders_match_real_distance() {
301 let o = Point2D::<f64, Cartesian>::new(0.0, 0.0);
302 // Distance² = 25.
303 let p_25 = Point2D::<f64, Cartesian>::new(3.0, 4.0);
304 // Distance² = 50.
305 let p_50 = Point2D::<f64, Cartesian>::new(5.0, 5.0);
306 let c25 = ComparablePythagoras.distance(&o, &p_25);
307 let c50 = ComparablePythagoras.distance(&o, &p_50);
308 assert!((c25 - 25.0).abs() < 1e-12);
309 assert!((c50 - 50.0).abs() < 1e-12);
310 assert!(c25 < c50);
311 }
312
313 // KC1.T2 witness: proves this strategy accepts a read-only `Point`
314 // (one that need not implement `PointMut`). If it compiles, the
315 // read-only bound is locked.
316 fn _accepts_readonly_point<P, S>(s: &S, a: &P, b: &P) -> S::Out
317 where
318 P: geometry_trait::Point,
319 S: DistanceStrategy<P, P>,
320 {
321 s.distance(a, b)
322 }
323
324 /// `ComparablePythagoras::comparable()` returns itself — the
325 /// comparable form is already sqrt-free.
326 #[test]
327 fn comparable_of_comparable_is_itself() {
328 let o = Point2D::<f64, Cartesian>::new(0.0, 0.0);
329 let p = Point2D::<f64, Cartesian>::new(3.0, 4.0);
330 let cmp = DistanceStrategy::<Point2D<f64, Cartesian>, Point2D<f64, Cartesian>>::comparable(
331 &ComparablePythagoras,
332 );
333 assert!((cmp.distance(&o, &p) - 25.0).abs() < 1e-12);
334 }
335
336 /// The read-only-point witness computes the same value when actually
337 /// invoked with a concrete strategy and points.
338 #[test]
339 #[allow(
340 clippy::used_underscore_items,
341 reason = "the test exists to run the compile-time witness's body"
342 )]
343 fn readonly_witness_computes_distance() {
344 let a = Point2D::<f64, Cartesian>::new(0.0, 0.0);
345 let b = Point2D::<f64, Cartesian>::new(3.0, 4.0);
346 assert!((_accepts_readonly_point(&Pythagoras, &a, &b) - 5.0).abs() < 1e-12);
347 }
348}
349
350#[cfg(test)]
351mod large_coordinate_tests {
352 //! Mirrors `boost/geometry/test/strategies/pythagoras.cpp:188-235`
353 //! (`test_big_2d_with`, `test_big_2d`, `test_big_2d_string`).
354 //! Exercises FP precision on coordinates around 10⁶ m, where naive
355 //! double-precision sums-of-squares lose digits to cancellation.
356 //!
357 //! Boost's reference value:
358 //! `1_076_554.548_583_395_567_829_438_778_905_7` metres, tolerance
359 //! `0.001 %` (Boost's `BOOST_CHECK_CLOSE` default for this test).
360
361 use super::{ComparablePythagoras, Pythagoras};
362 use crate::distance::DistanceStrategy;
363 use geometry_cs::Cartesian;
364 use geometry_model::Point2D;
365
366 /// Reference value from Boost's `test_big_2d_with` reference line
367 /// (`pythagoras.cpp:213`). The Rust port reproduces it within
368 /// 0.001%.
369 const REF: f64 = 1_076_554.548_583_395_567_829_438_778_905_7;
370
371 /// Tolerance matching Boost's `BOOST_CHECK_CLOSE(d, ref, 0.001)`
372 /// — 0.001 percent, i.e. `1e-5 * |ref|`.
373 fn close(actual: f64, expected: f64) -> bool {
374 (actual - expected).abs() <= expected.abs() * 1e-5
375 }
376
377 /// `test_big_2d_with` lifted with the f64/f64 row from `test_big_2d`.
378 #[test]
379 fn big_2d_f64_x_f64() {
380 let p1 = Point2D::<f64, Cartesian>::new(123_456.789_000_01, 234_567.891_000_01);
381 let p2 = Point2D::<f64, Cartesian>::new(987_654.321_000_01, 876_543.219_000_01);
382 let d = Pythagoras.distance(&p1, &p2);
383 assert!(close(d, REF), "got {d} expected ≈ {REF} (within 0.001%)");
384 }
385
386 /// Same inputs, comparable form. The squared sum is `REF * REF`,
387 /// so the comparable result has the same relative-tolerance
388 /// behaviour.
389 #[test]
390 fn big_2d_comparable() {
391 let p1 = Point2D::<f64, Cartesian>::new(123_456.789_000_01, 234_567.891_000_01);
392 let p2 = Point2D::<f64, Cartesian>::new(987_654.321_000_01, 876_543.219_000_01);
393 let cmp = ComparablePythagoras.distance(&p1, &p2);
394 let expected = REF * REF;
395 assert!(
396 (cmp - expected).abs() <= expected.abs() * 1e-5,
397 "got {cmp} expected ≈ {expected} (within 0.001%)",
398 );
399 }
400
401 /// Boost's `test_big_2d_string` parses coordinates from string
402 /// literals. Rust's `f64::from_str` goes through the same rounding
403 /// as a literal, so a single sanity check suffices.
404 #[test]
405 fn big_2d_from_string_parse() {
406 let p1 = Point2D::<f64, Cartesian>::new(
407 "123456.78900001".parse::<f64>().unwrap(),
408 "234567.89100001".parse::<f64>().unwrap(),
409 );
410 let p2 = Point2D::<f64, Cartesian>::new(
411 "987654.32100001".parse::<f64>().unwrap(),
412 "876543.21900001".parse::<f64>().unwrap(),
413 );
414 let d = Pythagoras.distance(&p1, &p2);
415 assert!(close(d, REF));
416 }
417}