Skip to main content

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}