hoomd_geometry/lib.rs
1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4#![doc(
5 html_favicon_url = "https://raw.githubusercontent.com/glotzerlab/hoomd-rs/7352214172a490cc716492e9724ff42720a0018a/doc/theme/favicon.svg"
6)]
7#![doc(
8 html_logo_url = "https://raw.githubusercontent.com/glotzerlab/hoomd-rs/7352214172a490cc716492e9724ff42720a0018a/doc/theme/favicon.svg"
9)]
10
11//! General, performant computational geometry code.
12//!
13//! `hoomd_geometry` implements common operations for widely-used geometric
14//! primitives, with additional functionality to accommodate hard-particle Monte
15//! Carlo simulations.
16//!
17//! ## Geometric Primitives
18//!
19//! The [`Hypersphere`][shape::Hypersphere] demonstrates the design philosophy of
20//! `hoomd_geometry`. The struct contains a single radius value, and immediately
21//! provides access to a variety of methods. Hyperspheres are well defined in
22//! arbitrary dimension, and therefore the implementation is parameterized with a
23//! const generic `N` representing the embedding dimension:
24//! ```
25//! use approxim::assert_relative_eq;
26//! use hoomd_geometry::{Volume, shape::Hypersphere};
27//! use std::f64::consts::PI;
28//!
29//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
30//! const N: usize = 3;
31//! let s = Hypersphere::<N>::with_radius(1.0.try_into()?);
32//! let volume = s.volume();
33//! assert_relative_eq!(volume, (4.0 / 3.0 * PI));
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! ## Traits
39//! [`Volume`] provides a notion of the amount of space a primitive
40//! occupies, and indicates the N-hypervolume of a given struct. For a
41//! [`Rectangle`][shape::Rectangle], for example, [`Volume`] returns the area in the
42//! plane, and for a [`Sphere`][shape::Sphere] returns the three-dimensional volume.
43//!
44//! [`IntersectsAt`] determines if there is an intersection between two shapes,
45//! where the second shape is placed in the coordinate system of the first.
46//! This is the most efficient way to test for intersections in Monte Carlo
47//! simulations as only the positions and orientations of the sites need to be
48//! modified.
49//!
50//! [`IsPointInside`] checks if a point is inside or outside a shape.
51//!
52//! Many shapes implement the `Distribution` trait from **rand** to randomly sample
53//! interior points.
54//!
55//! ## Intersection Tests
56//!
57//! For non-orientable shapes, or for bodies who have special intersection
58//! tests for particular orientations, and inherent method `intersects` can be
59//! implemented as well:
60//! ```
61//! use hoomd_geometry::{Convex, IntersectsAt, shape::Sphere};
62//! use hoomd_vector::{Cartesian, Versor};
63//!
64//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
65//! let s0 = Sphere {
66//! radius: 1.0.try_into()?,
67//! };
68//! let s1 = Sphere {
69//! radius: 1.0.try_into()?,
70//! };
71//!
72//! let q_id = Versor::default();
73//!
74//! assert!(s0.intersects_at(&s1, &Cartesian::from([1.9, 0.0, 0.0]), &q_id));
75//! assert!(!s0.intersects_at(&s1, &Cartesian::from([2.1, 0.0, 0.0]), &q_id));
76//! # Ok(())
77//! # }
78//! ```
79//!
80//! Any pair of shapes (with possibly different types) that both implement the
81//! [`SupportMapping`] trait can be tested for overlaps through the [`Convex`]
82//! newtype. [`IntersectsAt`] uses the [`xenocollide`] algorithm, provided for
83//! 2d and 3d shapes, to test for intersections between [`Convex`] shapes:
84//! ```
85//! use hoomd_geometry::{
86//! Convex, IntersectsAt,
87//! shape::{Hypercuboid, Sphere},
88//! };
89//! use hoomd_vector::Versor;
90//!
91//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
92//! let sphere = Convex(Sphere {
93//! radius: 1.0.try_into()?,
94//! });
95//! let cuboid = Convex(Hypercuboid {
96//! edge_lengths: [2.0.try_into()?, 2.0.try_into()?, 2.0.try_into()?],
97//! });
98//!
99//! assert!(sphere.intersects_at(
100//! &cuboid,
101//! &[1.9, 0.0, 0.0].into(),
102//! &Versor::default()
103//! ));
104//! assert!(!sphere.intersects_at(
105//! &cuboid,
106//! &[2.1, 0.0, 0.0].into(),
107//! &Versor::default()
108//! ));
109//! # Ok(())
110//! # }
111//! ```
112//!
113//! # Complete documentation
114//!
115//! `hoomd-geometry` is is a part of *hoomd-rs*. Read the [complete documentation]
116//! for more information.
117//!
118//! [complete documentation]: https://hoomd-rs.readthedocs.io
119
120use hoomd_utility::valid::PositiveReal;
121use hoomd_vector::InnerProduct;
122use thiserror::Error;
123
124mod convex;
125pub use convex::Convex;
126
127pub mod shape;
128pub mod xenocollide;
129
130/// The N-hypervolume of a geometry. In 2D, this is area and in 3D this is Volume.
131///
132/// # Example
133///
134/// ```
135/// use hoomd_geometry::{Volume, shape::Hypersphere};
136///
137/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
138/// const N: usize = 3;
139/// let s = Hypersphere::<N>::with_radius(1.0.try_into()?);
140/// let volume = s.volume();
141/// # Ok(())
142/// # }
143/// ```
144pub trait Volume {
145 /// The N-hypervolume of a geometry.
146 #[must_use]
147 fn volume(&self) -> f64;
148}
149
150/// Find the point on a shape that is the furthest in a given direction.
151///
152/// # Example
153///
154/// ```
155/// use hoomd_geometry::{SupportMapping, shape::Hypercuboid};
156///
157/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
158/// let cuboid = Hypercuboid {
159/// edge_lengths: [3.0.try_into()?, 2.0.try_into()?],
160/// };
161///
162/// let upper_right = cuboid.support_mapping(&[1.0, 1.0].into());
163/// let lower_right = cuboid.support_mapping(&[1.0, -1.0].into());
164///
165/// assert_eq!(upper_right, [1.5, 1.0].into());
166/// assert_eq!(lower_right, [1.5, -1.0].into());
167/// # Ok(())
168/// # }
169/// ```
170pub trait SupportMapping<V> {
171 /// Return the furthest extent of a shape in the direction of `n`.
172 fn support_mapping(&self, n: &V) -> V;
173}
174
175/// Test whether the set of points in one shape intersects with the set of another
176/// (in the global frame).
177///
178/// [`IntersectsAtGlobal`] supports hard-particle overlap checks for simulations
179/// defined in arbitrary metric spaces.
180pub trait IntersectsAtGlobal<S, P, R> {
181 /// Test whether the set of points in one shape intersects with the set of another
182 /// (in the global frame).
183 ///
184 /// Each shape (`self` and `other`) remain unmodified in their own local
185 /// coordinate systems. The intersection test is performed in a global
186 /// coordinate system where `self` has position/orientation `r_self`/`o_self`
187 /// and other has position/orientation `r_other`/`o_other`.
188 ///
189 /// When starting with shapes in the global frame (such as in Monte Carlo
190 /// simulations), `intersects_at_global` may be faster than `intersects_at`
191 /// as it is able to check whether the bounding spheres of the shapes
192 /// overlap *before* transforming into the local coordinate system about
193 /// `self`.
194 fn intersects_at_global(
195 &self,
196 other: &S,
197 r_self: &P,
198 o_self: &R,
199 r_other: &P,
200 o_other: &R,
201 ) -> bool;
202}
203
204/// Test whether two shapes share any points in space.
205///
206/// # Examples
207///
208/// Some shapes implement [`IntersectsAt`] directly:
209/// ```
210/// use hoomd_geometry::{Convex, IntersectsAt, shape::Sphere};
211/// use hoomd_vector::{Cartesian, Versor};
212///
213/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
214/// let s0 = Sphere {
215/// radius: 1.0.try_into()?,
216/// };
217/// let s1 = Sphere {
218/// radius: 1.0.try_into()?,
219/// };
220///
221/// let q_id = Versor::default();
222///
223/// assert!(s0.intersects_at(&s1, &Cartesian::from([1.9, 0.0, 0.0]), &q_id));
224/// assert!(!s0.intersects_at(&s1, &Cartesian::from([2.1, 0.0, 0.0]), &q_id));
225/// # Ok(())
226/// # }
227/// ```
228///
229/// Others must be wrapped in [`Convex`]:
230/// ```
231/// use hoomd_geometry::{
232/// Convex, IntersectsAt,
233/// shape::{Hypercuboid, Sphere},
234/// };
235/// use hoomd_vector::Versor;
236///
237/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
238/// let sphere = Convex(Sphere {
239/// radius: 1.0.try_into()?,
240/// });
241/// let cuboid = Convex(Hypercuboid {
242/// edge_lengths: [2.0.try_into()?, 2.0.try_into()?, 2.0.try_into()?],
243/// });
244///
245/// assert!(sphere.intersects_at(
246/// &cuboid,
247/// &[1.9, 0.0, 0.0].into(),
248/// &Versor::default()
249/// ));
250/// assert!(!sphere.intersects_at(
251/// &cuboid,
252/// &[2.1, 0.0, 0.0].into(),
253/// &Versor::default()
254/// ));
255/// # Ok(())
256/// # }
257/// ```
258pub trait IntersectsAt<S, V, R> {
259 /// Test whether the set of points in one shape intersects with the set of another
260 /// (in the local frame).
261 ///
262 /// Each shape (`self` and `other`) remain unmodified in their own local
263 /// coordinate systems. The intersection test is performed in the local
264 /// coordinate system of `self`. The vector `v_ij` points from the local
265 /// origin of `self` to the local origin of `other`. Similarly, `o_ij` is the
266 /// orientation of `other` in the local coordinate system of `self`.
267 ///
268 /// # See Also
269 ///
270 /// Call [`pair_system_to_local`] to compute `v_ij` and `o_ij` from the
271 /// system frame positions and orientations of two shapes.
272 ///
273 /// [`pair_system_to_local`]: hoomd_vector::pair_system_to_local
274 fn intersects_at(&self, other: &S, v_ij: &V, o_ij: &R) -> bool;
275
276 /// Approximate the amount of overlap between two shapes.
277 ///
278 /// Move `other` in along `v_ij` until the shapes no longer overlap. Return the
279 /// approximate* distance needed to move `other` (which is 0 if the shapes are
280 /// already separated). This is *not* the exact minimum separation distance and
281 /// the method does *not* solve for an optimal direction.
282 ///
283 /// `resolution` sets the size of the steps between distances in the
284 /// approximation.
285 ///
286 /// If `v_ij` has 0 norm, move `other` along the `V::default_unit()`.
287 ///
288 /// ```
289 /// use hoomd_geometry::{Convex, IntersectsAt, shape::Hypercuboid};
290 /// use hoomd_vector::Versor;
291 ///
292 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
293 /// let cuboid = Convex(Hypercuboid::with_equal_edges(2.0.try_into()?));
294 ///
295 /// let d = cuboid.approximate_separation_distance(
296 /// &cuboid,
297 /// &[1.8, 0.0, 0.0].into(),
298 /// &Versor::default(),
299 /// 0.01.try_into()?,
300 /// );
301 ///
302 /// assert!(d >= 0.2);
303 /// # Ok(())
304 /// # }
305 /// ```
306 #[inline]
307 fn approximate_separation_distance(
308 &self,
309 other: &S,
310 v_ij: &V,
311 o_ij: &R,
312 resolution: PositiveReal,
313 ) -> f64
314 where
315 V: InnerProduct,
316 {
317 let mut d = 0.0;
318
319 let direction = v_ij.to_unit().unwrap_or((V::default_unit(), 1.0)).0;
320
321 while self.intersects_at(other, &(*v_ij + *direction.get() * d), o_ij) {
322 d += resolution.get();
323 }
324
325 d
326 }
327}
328
329/// Radius of an N-dimensional hypersphere that bounds a shape.
330///
331/// The hypersphere has the same local origin as the shape `self`.
332///
333/// Some [`IntersectsAt`] tests use the bounding sphere radius as a first pass
334/// before calling a more expensive intersection test. To improve performance,
335/// the bounding sphere should be as tightly fitting as possible.
336///
337/// # Example
338///
339/// ```
340/// use hoomd_geometry::{BoundingSphereRadius, shape::Hypercuboid};
341///
342/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
343/// let cuboid = Hypercuboid {
344/// edge_lengths: [6.0.try_into()?, 8.0.try_into()?],
345/// };
346/// let bounding_radius = cuboid.bounding_sphere_radius();
347///
348/// assert_eq!(bounding_radius.get(), 5.0);
349/// # Ok(())
350/// # }
351/// ```
352pub trait BoundingSphereRadius {
353 /// Get the bounding radius.
354 fn bounding_sphere_radius(&self) -> PositiveReal;
355}
356
357/// Test whether a point is inside or outside a shape.
358///
359/// # Example
360///
361/// ```
362/// use hoomd_geometry::{IsPointInside, shape::Hypercuboid};
363///
364/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
365/// let cuboid = Hypercuboid {
366/// edge_lengths: [6.0.try_into()?, 8.0.try_into()?],
367/// };
368///
369/// assert!(cuboid.is_point_inside(&[2.5, -3.5].into()));
370/// assert!(!cuboid.is_point_inside(&[4.0, -3.5].into()));
371/// # Ok(())
372/// # }
373/// ```
374pub trait IsPointInside<V> {
375 /// Check if a point is inside the shape.
376 fn is_point_inside(&self, point: &V) -> bool;
377}
378
379/// Produce a new shape by uniform scaling.
380///
381/// # Example
382///
383/// ```
384/// use hoomd_geometry::{Scale, shape::Sphere};
385///
386/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
387/// let sphere = Sphere {
388/// radius: 5.0.try_into()?,
389/// };
390///
391/// let scaled_sphere = sphere.scale_length(0.5.try_into()?);
392///
393/// assert_eq!(scaled_sphere.radius.get(), 2.5);
394/// # Ok(())
395/// # }
396/// ```
397pub trait Scale {
398 /// Produce a new shape by uniformly scaling length.
399 #[must_use]
400 fn scale_length(&self, v: PositiveReal) -> Self;
401
402 /// Produce a new shape by uniformly scaling volume.
403 #[must_use]
404 fn scale_volume(&self, v: PositiveReal) -> Self;
405}
406
407/// Map a point from one shape to another.
408///
409/// # Example
410///
411/// ```
412/// use hoomd_geometry::{MapPoint, shape::Circle};
413/// use hoomd_vector::Cartesian;
414///
415/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
416/// let closed_a = Circle {
417/// radius: 10.0.try_into()?,
418/// };
419/// let closed_b = Circle {
420/// radius: 20.0.try_into()?,
421/// };
422///
423/// let mapped_point =
424/// closed_a.map_point(Cartesian::from([-1.0, 1.0]), &closed_b);
425///
426/// assert_eq!(mapped_point, Ok(Cartesian::from([-2.0, 2.0])));
427/// assert_eq!(
428/// closed_a.map_point(Cartesian::from([-100.0, 1.0]), &closed_b),
429/// Err(hoomd_geometry::Error::PointOutsideShape)
430/// );
431/// # Ok(())
432/// # }
433/// ```
434pub trait MapPoint<P> {
435 /// Map a point from one boundary to another.
436 ///
437 /// # Errors
438 ///
439 /// Returns [`Error::PointOutsideShape`] when `point` is outside the shape
440 /// `self`.
441 fn map_point(&self, point: P, other: &Self) -> Result<P, Error>;
442}
443
444/// Enumerate possible sources of error in fallible geometry methods.
445#[non_exhaustive]
446#[derive(Error, PartialEq, Debug)]
447pub enum Error {
448 /// Polytopes require at least one vertex.
449 #[error("a ConvexPolytope must have at least one vertex")]
450 DegeneratePolytope,
451
452 /// The point is outside the shape.
453 #[error("cannot map a point that is outside the shape")]
454 PointOutsideShape,
455
456 /// Too many vertices were provided.
457 #[error("too many vertices")]
458 TooManyVertices,
459}