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}