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::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, &[1.9, 0.0, 0.0].into(), &q_id));
75//! assert!(!s0.intersects_at(&s1, &[2.1, 0.0, 0.0].into(), &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, Rotate, Rotation, Vector};
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::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, &[1.9, 0.0, 0.0].into(), &q_id));
224/// assert!(!s0.intersects_at(&s1, &[2.1, 0.0, 0.0].into(), &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>
259where
260    V: Vector,
261    R: Rotation + Rotate<V>,
262{
263    /// Test whether the set of points in one shape intersects with the set of another
264    /// (in the local frame).
265    ///
266    /// Each shape (`self` and `other`) remain unmodified in their own local
267    /// coordinate systems. The intersection test is performed in the local
268    /// coordinate system of `self`. The vector `v_ij` points from the local
269    /// origin of `self` to the local origin of `other`. Similarly, `o_ij` is the
270    /// orientation of `other` in the local coordinate system of `self`.
271    ///
272    /// # See Also
273    ///
274    /// Call [`pair_system_to_local`] to compute `v_ij` and `o_ij` from the
275    /// system frame positions and orientations of two shapes.
276    ///
277    /// [`pair_system_to_local`]: hoomd_vector::pair_system_to_local
278    fn intersects_at(&self, other: &S, v_ij: &V, o_ij: &R) -> bool;
279
280    /// Approximate the amount of overlap between two shapes.
281    ///
282    /// Move `other` in along `v_ij` until the shapes no longer overlap. Return the
283    /// approximate* distance needed to move `other` (which is 0 if the shapes are
284    /// already separated). This is *not* the exact minimum separation distance and
285    /// the method does *not* solve for an optimal direction.
286    ///
287    /// `resolution` sets the size of the steps between distances in the
288    /// approximation.
289    ///
290    /// If `v_ij` has 0 norm, move `other` along the `V::default_unit()`.
291    ///
292    /// ```
293    /// use hoomd_geometry::{Convex, IntersectsAt, shape::Hypercuboid};
294    /// use hoomd_vector::Versor;
295    ///
296    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
297    /// let cuboid = Convex(Hypercuboid::with_equal_edges(2.0.try_into()?));
298    ///
299    /// let d = cuboid.approximate_separation_distance(
300    ///     &cuboid,
301    ///     &[1.8, 0.0, 0.0].into(),
302    ///     &Versor::default(),
303    ///     0.01.try_into()?,
304    /// );
305    ///
306    /// assert!(d >= 0.2);
307    /// # Ok(())
308    /// # }
309    /// ```
310    #[inline]
311    fn approximate_separation_distance(
312        &self,
313        other: &S,
314        v_ij: &V,
315        o_ij: &R,
316        resolution: PositiveReal,
317    ) -> f64
318    where
319        V: InnerProduct,
320    {
321        let mut d = 0.0;
322
323        let direction = v_ij.to_unit().unwrap_or((V::default_unit(), 1.0)).0;
324
325        while self.intersects_at(other, &(*v_ij + *direction.get() * d), o_ij) {
326            d += resolution.get();
327        }
328
329        d
330    }
331}
332
333/// Radius of an N-dimensional hypersphere that bounds a shape.
334///
335/// The hypersphere has the same local origin as the shape `self`.
336///
337/// Some [`IntersectsAt`] tests use the bounding sphere radius as a first pass
338/// before calling a more expensive intersection test. To improve performance,
339/// the bounding sphere should be as tightly fitting as possible.
340///
341/// # Example
342///
343/// ```
344/// use hoomd_geometry::{BoundingSphereRadius, shape::Hypercuboid};
345///
346/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
347/// let cuboid = Hypercuboid {
348///     edge_lengths: [6.0.try_into()?, 8.0.try_into()?],
349/// };
350/// let bounding_radius = cuboid.bounding_sphere_radius();
351///
352/// assert_eq!(bounding_radius.get(), 5.0);
353/// # Ok(())
354/// # }
355/// ```
356pub trait BoundingSphereRadius {
357    /// Get the bounding radius.
358    fn bounding_sphere_radius(&self) -> PositiveReal;
359}
360
361/// Test whether a point is inside or outside a shape.
362///
363/// # Example
364///
365/// ```
366/// use hoomd_geometry::{IsPointInside, shape::Hypercuboid};
367///
368/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
369/// let cuboid = Hypercuboid {
370///     edge_lengths: [6.0.try_into()?, 8.0.try_into()?],
371/// };
372///
373/// assert!(cuboid.is_point_inside(&[2.5, -3.5].into()));
374/// assert!(!cuboid.is_point_inside(&[4.0, -3.5].into()));
375/// # Ok(())
376/// # }
377/// ```
378pub trait IsPointInside<V> {
379    /// Check if a point is inside the shape.
380    fn is_point_inside(&self, point: &V) -> bool;
381}
382
383/// Produce a new shape by uniform scaling.
384///
385/// # Example
386///
387/// ```
388/// use hoomd_geometry::{Scale, shape::Sphere};
389///
390/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
391/// let sphere = Sphere {
392///     radius: 5.0.try_into()?,
393/// };
394///
395/// let scaled_sphere = sphere.scale_length(0.5.try_into()?);
396///
397/// assert_eq!(scaled_sphere.radius.get(), 2.5);
398/// # Ok(())
399/// # }
400/// ```
401pub trait Scale {
402    /// Produce a new shape by uniformly scaling length.
403    #[must_use]
404    fn scale_length(&self, v: PositiveReal) -> Self;
405
406    /// Produce a new shape by uniformly scaling volume.
407    #[must_use]
408    fn scale_volume(&self, v: PositiveReal) -> Self;
409}
410
411/// Map a point from one shape to another.
412///
413/// # Example
414///
415/// ```
416/// use hoomd_geometry::{MapPoint, shape::Circle};
417/// use hoomd_vector::Cartesian;
418///
419/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
420/// let closed_a = Circle {
421///     radius: 10.0.try_into()?,
422/// };
423/// let closed_b = Circle {
424///     radius: 20.0.try_into()?,
425/// };
426///
427/// let mapped_point =
428///     closed_a.map_point(Cartesian::from([-1.0, 1.0]), &closed_b);
429///
430/// assert_eq!(mapped_point, Ok(Cartesian::from([-2.0, 2.0])));
431/// assert_eq!(
432///     closed_a.map_point(Cartesian::from([-100.0, 1.0]), &closed_b),
433///     Err(hoomd_geometry::Error::PointOutsideShape)
434/// );
435/// # Ok(())
436/// # }
437/// ```
438pub trait MapPoint<P> {
439    /// Map a point from one boundary to another.
440    ///
441    /// # Errors
442    ///
443    /// Returns [`Error::PointOutsideShape`] when `point` is outside the shape
444    /// `self`.
445    fn map_point(&self, point: P, other: &Self) -> Result<P, Error>;
446}
447
448/// Enumerate possible sources of error in fallible geometry methods.
449#[non_exhaustive]
450#[derive(Error, PartialEq, Debug)]
451pub enum Error {
452    /// Polytopes require at least one vertex.
453    #[error("a ConvexPolytope must have at least one vertex")]
454    DegeneratePolytope,
455
456    /// The point is outside the shape.
457    #[error("cannot map a point that is outside the shape")]
458    PointOutsideShape,
459}