miniball/enclosing.rs
1// Copyright © 2022-2024 Rouven Spreckels <rs@qu1x.dev>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7use super::{Deque, OVec};
8use core::mem::size_of;
9use nalgebra::{
10 base::allocator::Allocator, DefaultAllocator, DimName, DimNameAdd, DimNameSum, OPoint,
11 RealField, U1,
12};
13#[cfg(feature = "std")]
14use stacker::maybe_grow;
15
16#[cfg(not(feature = "std"))]
17#[inline]
18fn maybe_grow<R, F: FnOnce() -> R>(_red_zone: usize, _stack_size: usize, callback: F) -> R {
19 callback()
20}
21
22/// Minimum enclosing ball.
23pub trait Enclosing<T: RealField, D: DimName>
24where
25 Self: Clone,
26 DefaultAllocator: Allocator<T, D>,
27{
28 #[doc(hidden)]
29 /// Guaranteed stack size per recursion step.
30 const RED_ZONE: usize =
31 32 * 1_024 + (8 * D::USIZE + 2 * D::USIZE.pow(2)) * size_of::<OPoint<T, D>>();
32 #[doc(hidden)]
33 /// New stack space to allocate if within [`Self::RED_ZONE`].
34 const STACK_SIZE: usize = Self::RED_ZONE * 1_024;
35
36 /// Whether ball contains `point`.
37 #[must_use]
38 fn contains(&self, point: &OPoint<T, D>) -> bool;
39 /// Returns circumscribed ball with all `bounds` on surface or `None` if it does not exist.
40 ///
41 /// # Example
42 ///
43 /// Finds circumscribed 3-ball of 3-simplex (tetrahedron):
44 ///
45 /// ```
46 /// use miniball::{
47 /// nalgebra::{Point3, Vector3},
48 /// {Ball, Enclosing},
49 /// };
50 ///
51 /// // 3-simplex.
52 /// let a = Point3::new(1.0, 1.0, 1.0);
53 /// let b = Point3::new(1.0, -1.0, -1.0);
54 /// let c = Point3::new(-1.0, 1.0, -1.0);
55 /// let d = Point3::new(-1.0, -1.0, 1.0);
56 /// // Center of 3-simplex.
57 /// let offset = Vector3::new(-3.0, 7.0, 4.8);
58 /// // Computes circumscribed 3-ball of 3-simplex.
59 /// let Ball {
60 /// center,
61 /// radius_squared,
62 /// } = Ball::with_bounds(&[a, b, c, d].map(|bound| bound + offset)).unwrap();
63 /// // Ensures enclosing 3-ball is centered around 3-simplex.
64 /// assert_eq!(center, offset.into());
65 /// // Ensures enclosing 3-ball's radius matches center-to-point distances of 3-simplex.
66 /// assert_eq!(radius_squared, 3.0);
67 /// ```
68 #[must_use]
69 fn with_bounds(bounds: &[OPoint<T, D>]) -> Option<Self>
70 where
71 DefaultAllocator: Allocator<T, D, D>;
72
73 /// Returns minimum ball enclosing `points`.
74 ///
75 /// Points should be randomly permuted beforehand to ensure expected time complexity. Accepts
76 /// mutable reference to container implementing [`Deque`] to move potential points on surface to
77 /// the front. This does not converge towards a reproducible total order but significantly
78 /// speeds up further invocations if the use case involves adding new points, non-enclosed ones
79 /// to the front and enclosed ones to the back.
80 ///
81 /// Implements [Welzl's recursive algorithm] with move-to-front heuristic. No allocations happen
82 /// unless the real field `T` is not [`Copy`] or the stack size enters the dimension-dependant
83 /// red zone in which case temporary stack space will be allocated on the heap if the `std`
84 /// feature is enabled.
85 ///
86 /// [Welzl's recursive algorithm]: https://api.semanticscholar.org/CorpusID:17569809
87 ///
88 /// # Complexity
89 ///
90 /// Expected time complexity is *O*((*n*+1)(*n*+1)!*m*) for *m* randomly permuted
91 /// *n*-dimensional points. The complexity constant in *m* is significantly reduced by reusing
92 /// permuted points of previous invocations.
93 ///
94 /// # Stability
95 ///
96 /// Due to floating-point inaccuracies, the returned ball might not exactly be the minimum for
97 /// degenerate (e.g., co-spherical) `points`. The accuracy is depending on the shape and order
98 /// of `points` with an expected worst-case factor of `T::one() ± T::default_epsilon().sqrt()`
99 /// where `T::one()` is exact.
100 ///
101 /// # Example
102 ///
103 /// Finds minimum 4-ball enclosing 4-cube (tesseract):
104 ///
105 /// ```
106 /// use miniball::{
107 /// nalgebra::{distance, Point4, Vector4},
108 /// {Ball, Enclosing},
109 /// };
110 /// use std::collections::VecDeque;
111 ///
112 /// // Uniform distribution in 4-cube centered around `offset` with room `diagonal_halved`.
113 /// let offset = Vector4::new(-3.0, 7.0, 4.8, 1.2);
114 /// let diagonal_halved = 3.0;
115 /// let mut points = (0..60_000)
116 /// .map(|_point| Point4::<f64>::from(Vector4::new_random() - Vector4::from_element(0.5)))
117 /// .map(|point| point * diagonal_halved)
118 /// .map(|point| point + offset)
119 /// .collect::<VecDeque<_>>();
120 /// // Computes 4-ball enclosing 4-cube.
121 /// let Ball {
122 /// center,
123 /// radius_squared,
124 /// } = Ball::enclosing_points(&mut points);
125 /// let radius = radius_squared.sqrt();
126 /// // Ensures enclosing 4-ball is roughly centered around uniform distribution in 4-cube and
127 /// // radius roughly matches room diagonal halved, guaranteeing certain uniformity of randomly
128 /// // distributed points.
129 /// assert!((center - offset).map(f64::abs) <= Vector4::from_element(1.0).into());
130 /// assert!((radius - diagonal_halved).abs() <= 1.0);
131 /// // Epsilon of numerical stability for computing circumscribed 4-ball. This is related to
132 /// // robustness of `Enclosing::with_bounds()` regarding floating-point inaccuracies.
133 /// let epsilon = f64::EPSILON.sqrt();
134 /// // Ensures all points are enclosed by 4-ball.
135 /// let all_enclosed = points
136 /// .iter()
137 /// .all(|point| distance(point, ¢er) <= radius + epsilon);
138 /// assert!(all_enclosed);
139 /// // Ensures at least 2 points are on surface of 4-ball, mandatory to be minimum.
140 /// let bounds_count = points
141 /// .iter()
142 /// .map(|point| distance(point, ¢er))
143 /// .map(|distance| distance - radius)
144 /// .map(f64::abs)
145 /// .filter(|&deviation| deviation <= epsilon)
146 /// .count();
147 /// assert!(bounds_count >= 2);
148 /// ```
149 #[must_use]
150 #[inline]
151 fn enclosing_points(points: &mut impl Deque<OPoint<T, D>>) -> Self
152 where
153 D: DimNameAdd<U1>,
154 DefaultAllocator: Allocator<T, D, D> + Allocator<OPoint<T, D>, DimNameSum<D, U1>>,
155 <DefaultAllocator as Allocator<OPoint<T, D>, DimNameSum<D, U1>>>::Buffer: Default,
156 {
157 assert!(!points.is_empty(), "empty point set");
158 let mut bounds = OVec::<OPoint<T, D>, DimNameSum<D, U1>>::new();
159 (0..bounds.capacity())
160 .find_map(|_| {
161 maybe_grow(Self::RED_ZONE, Self::STACK_SIZE, || {
162 Self::enclosing_points_with_bounds(points, &mut bounds)
163 })
164 })
165 .expect("numerical instability")
166 }
167 /// Returns minimum ball enclosing `points` with `bounds`.
168 ///
169 /// Recursive helper for [`Self::enclosing_points()`].
170 #[doc(hidden)]
171 #[must_use]
172 fn enclosing_points_with_bounds(
173 points: &mut impl Deque<OPoint<T, D>>,
174 bounds: &mut OVec<OPoint<T, D>, DimNameSum<D, U1>>,
175 ) -> Option<Self>
176 where
177 D: DimNameAdd<U1>,
178 DefaultAllocator: Allocator<T, D, D> + Allocator<OPoint<T, D>, DimNameSum<D, U1>>,
179 <DefaultAllocator as Allocator<OPoint<T, D>, DimNameSum<D, U1>>>::Buffer: Default,
180 {
181 // Take point from back.
182 if let Some(point) = points.pop_back().filter(|_| !bounds.is_full()) {
183 let ball = maybe_grow(Self::RED_ZONE, Self::STACK_SIZE, || {
184 // Branch with one point less.
185 Self::enclosing_points_with_bounds(points, bounds)
186 });
187 if let Some(ball) = ball.filter(|ball| ball.contains(&point)) {
188 // Move point to back.
189 points.push_back(point);
190 Some(ball)
191 } else {
192 // Move point to bounds.
193 bounds.push(point);
194 let ball = maybe_grow(Self::RED_ZONE, Self::STACK_SIZE, || {
195 // Branch with one point less and one bound more.
196 Self::enclosing_points_with_bounds(points, bounds)
197 });
198 // Move point to front.
199 points.push_front(bounds.pop().unwrap());
200 ball
201 }
202 } else {
203 // Circumscribed ball with bounds.
204 Self::with_bounds(bounds.as_slice())
205 }
206 }
207}