Skip to main content

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}