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, &center) <= 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, &center))
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}