hoomd_interaction/univariate/overlap_penalty.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//! Implement `OverlapPenalty`.
5
6use serde::{Deserialize, Serialize};
7
8use super::UnivariateEnergy;
9
10/// Monotonically non-decreasing potential to push sites apart (*not differentiable*).
11///
12/// [`OverlapPenalty`] is specifically designed to work with the `QuickInsert`
13/// and `QuickCompress` algorithms to quickly prepare states with non-overlapping
14/// particles. Combine it with [`ApproximateShapeOverlap`] to compute an energy that
15/// penalizes hard particle overlaps.
16///
17/// The potential has three regions:
18/// ```math
19/// U(r) = \begin{cases}
20/// \infty & r < -d_\mathrm{max} \\
21/// \frac{1}{2} kr^2 + \varepsilon_\mathrm{shoulder} & r < 0 \\
22/// 0 & r \ge 0
23/// \end{cases}
24/// ```
25/// The first region describes a completely hard interaction when sites overlap
26/// too far. This prevents `QuickInsert` from creating too much strain with an
27/// insertion. The second part applies a harmonic potential that allows trial moves
28/// to gradually resolve overlaps. In the third region, sites are allowed to move
29/// freely when not overlapping. The shoulder potential prevents trial moves from
30/// creating new overlaps.
31///
32/// [`ApproximateShapeOverlap`]: crate::pairwise::ApproximateShapeOverlap
33///
34/// # Example
35///
36/// ```
37/// use hoomd_interaction::univariate::OverlapPenalty;
38///
39/// let overlap_penalty = OverlapPenalty::default();
40/// ```
41#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
42pub struct OverlapPenalty {
43 /// Spring stiffness $`[\mathrm{energy}] [\mathrm{length}]^{-2}`$.
44 pub k: f64,
45
46 /// The largest overlap distance to allow $`[\mathrm{length}]`$.
47 pub maximum_allowed_overlap: f64,
48
49 /// Height of the potential as $`r`$ approaches 0 from the left $`[\mathrm{energy}]`$
50 pub epsilon_shoulder: f64,
51}
52
53impl Default for OverlapPenalty {
54 /// Default overlap penalty parameters.
55 ///
56 /// The default values are tuned for use with `QuickInsert` and `QuickCompress`
57 /// applied to systems of spherical particles with diameter approximately 1.
58 ///
59 /// * $`k = 10,000`$
60 /// * $`d_\mathrm{max} = 0.2`$
61 /// * $`\varepsilon_\mathrm{shoulder} = 100`$
62 ///
63 /// Call [`OverlapPenalty::scaled_default`] to initialize with values scaled
64 /// for use with non-unit diameter sites.
65 ///
66 /// # Example
67 ///
68 /// ```
69 /// use hoomd_interaction::univariate::OverlapPenalty;
70 ///
71 /// let overlap_penalty = OverlapPenalty::default();
72 /// ```
73 #[inline]
74 fn default() -> Self {
75 Self {
76 k: 10_000.0,
77 maximum_allowed_overlap: 0.2,
78 epsilon_shoulder: 100.0,
79 }
80 }
81}
82
83impl OverlapPenalty {
84 /// Default overlap penalty parameters for a given diameter.
85 ///
86 /// Construct an [`OverlapPenalty`] with default parameters scaled to suit
87 /// a site with the given diameter.
88 ///
89 /// # Example
90 ///
91 /// ```
92 /// use hoomd_interaction::univariate::OverlapPenalty;
93 ///
94 /// let overlap_penalty = OverlapPenalty::scaled_default(2.0);
95 ///
96 /// assert_eq!(overlap_penalty.maximum_allowed_overlap, 0.4);
97 /// ```
98 #[must_use]
99 #[inline]
100 pub fn scaled_default(diameter: f64) -> Self {
101 let mut overlap_penalty = Self::default();
102 overlap_penalty.maximum_allowed_overlap *= diameter;
103 overlap_penalty
104 }
105}
106
107impl UnivariateEnergy for OverlapPenalty {
108 #[inline]
109 fn energy(&self, r: f64) -> f64 {
110 match r {
111 x if x < -self.maximum_allowed_overlap => f64::INFINITY,
112 x if x < 0.0 => 0.5 * self.k * r.powi(2) + self.epsilon_shoulder,
113 _ => 0.0,
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn shoulder() {
124 let k = 0.0;
125 let epsilon_shoulder = 15.0;
126 let maximum_allowed_overlap = 1.0;
127
128 let overlap_penalty = OverlapPenalty {
129 k,
130 maximum_allowed_overlap,
131 epsilon_shoulder,
132 };
133
134 assert_eq!(overlap_penalty.energy(1.0), 0.0);
135 assert_eq!(overlap_penalty.energy(0.0), 0.0);
136 assert_eq!(
137 overlap_penalty.energy(0.0_f64.next_down()),
138 epsilon_shoulder
139 );
140 assert_eq!(overlap_penalty.energy(-0.5), epsilon_shoulder);
141 }
142
143 #[test]
144 fn core() {
145 let k = 0.0;
146 let epsilon_shoulder = 0.0;
147 let maximum_allowed_overlap = 0.5;
148
149 let overlap_penalty = OverlapPenalty {
150 k,
151 maximum_allowed_overlap,
152 epsilon_shoulder,
153 };
154
155 assert_eq!(overlap_penalty.energy(1.0), 0.0);
156 assert_eq!(overlap_penalty.energy(0.0), 0.0);
157 assert_eq!(overlap_penalty.energy(0.0_f64.next_down()), 0.0);
158 assert_eq!(overlap_penalty.energy(-0.5), 0.0);
159 assert_eq!(
160 overlap_penalty.energy((-0.5_f64).next_down()),
161 f64::INFINITY
162 );
163 assert_eq!(overlap_penalty.energy(-1.0), f64::INFINITY);
164 }
165
166 #[test]
167 fn harmonic() {
168 let k = 6.0;
169 let epsilon_shoulder = 0.0;
170 let maximum_allowed_overlap = 10.0;
171
172 let overlap_penalty = OverlapPenalty {
173 k,
174 maximum_allowed_overlap,
175 epsilon_shoulder,
176 };
177
178 assert_eq!(overlap_penalty.energy(1.0), 0.0);
179 assert_eq!(overlap_penalty.energy(0.0), 0.0);
180 assert_eq!(overlap_penalty.energy(-0.125), 0.5 * k * 0.125_f64.powi(2));
181 }
182}