Skip to content

Commit 4697107

Browse files
committed
feat(potentials): Enhance Buckingham potential with local maximum reflection
1 parent 87d175a commit 4697107

1 file changed

Lines changed: 37 additions & 51 deletions

File tree

  • src/potentials/nonbonded

src/potentials/nonbonded/vdw.rs

Lines changed: 37 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ impl<T: Real> PairKernel<T> for LennardJones {
9999
/// - `a`: The repulsion pre-factor $A = \frac{6 D_0}{\zeta-6} e^{\zeta}$.
100100
/// - `b`: The repulsion decay constant $B = \zeta / R_0$.
101101
/// - `c`: The attraction pre-factor $C = \frac{\zeta D_0 R_0^6}{\zeta-6}$.
102-
/// - `r_fusion_sq`: The squared distance threshold for regularization.
102+
/// - `r_max_sq`: The squared distance of the local energy maximum $r_{\text{max}}^2$.
103+
/// - `two_e_max`: Twice the energy at the local maximum, $2 E(r_{\text{max}})$.
103104
///
104105
/// # Inputs
105106
///
@@ -108,105 +109,90 @@ impl<T: Real> PairKernel<T> for LennardJones {
108109
/// # Implementation Notes
109110
///
110111
/// - The kernel operates on the computationally efficient $A, B, C$ form.
111-
/// - A branchless regularization is applied for $r^2 < r_{fusion}^2$ using a mathematical mask
112-
/// to prevent energy collapse at very short distances, ensuring numerical stability.
112+
/// - For $r < r_{\text{max}}$, the energy is reflected about the local maximum:
113+
/// $E_{\text{ref}}(r) = 2 E_{\text{max}} - E(r)$. This produces a repulsive wall
114+
/// that diverges to $+\infty$ as $r \to 0$ via the $C/r^6$ attraction term,
115+
/// while maintaining $C^1$ continuity at $r_{\text{max}}$ (where $E'(r_{\text{max}}) = 0$).
116+
/// - A branchless sign-flip replaces the traditional constant penalty, providing
117+
/// physically correct short-range behavior at zero additional runtime cost.
113118
/// - Requires one `sqrt` and one `exp` call, making it computationally more demanding than LJ.
114119
/// - Power chain `inv_r2 -> inv_r6 -> inv_r8` is used for the attractive term.
115120
#[derive(Clone, Copy, Debug, Default)]
116121
pub struct Buckingham;
117122

118123
impl<T: Real> PairKernel<T> for Buckingham {
119-
type Params = (T, T, T, T);
124+
type Params = (T, T, T, T, T);
120125

121126
/// Computes only the potential energy.
122127
///
123128
/// # Formula
124129
///
125-
/// $$ E = A e^{-B r} - C / r^6 $$
130+
/// $$ E(r) = \begin{cases} A e^{-Br} - C/r^6 & r \ge r_{\text{max}} \\ 2 E_{\text{max}} - (A e^{-Br} - C/r^6) & r < r_{\text{max}} \end{cases} $$
126131
#[inline(always)]
127-
fn energy(r_sq: T, (a, b, c, r_fusion_sq): Self::Params) -> T {
128-
let is_safe = T::from((r_sq >= r_fusion_sq) as u8 as f32);
129-
let effective_r_sq = r_sq.max(r_fusion_sq);
132+
fn energy(r_sq: T, (a, b, c, r_max_sq, two_e_max): Self::Params) -> T {
133+
let mask = T::from((r_sq >= r_max_sq) as u8 as f32);
134+
let sign = mask + mask - T::from(1.0);
130135

131-
let r = effective_r_sq.sqrt();
132-
let inv_r2 = effective_r_sq.recip();
136+
let r = r_sq.sqrt();
137+
let inv_r2 = r_sq.recip();
133138
let inv_r6 = inv_r2 * inv_r2 * inv_r2;
134139

135-
let repulsion = a * T::exp(-b * r);
136-
let attraction = c * inv_r6;
137-
let energy_unsafe = repulsion - attraction;
138-
139-
const FUSION_ENERGY_PENALTY: f32 = 1.0e6;
140-
let penalty = T::from(FUSION_ENERGY_PENALTY);
140+
let e_buck = a * T::exp(-b * r) - c * inv_r6;
141141

142-
energy_unsafe * is_safe + penalty * (T::from(1.0) - is_safe)
142+
sign * e_buck + (T::from(1.0) - mask) * two_e_max
143143
}
144144

145145
/// Computes only the force pre-factor $D$.
146146
///
147147
/// # Formula
148148
///
149-
/// $$ D = \frac{A B e^{-B r}}{r} - \frac{6 C}{r^8} $$
149+
/// $$ D(r) = \text{sign}(r) \left( \frac{A B e^{-Br}}{r} - \frac{6C}{r^8} \right) $$
150+
///
151+
/// where $\text{sign}(r) = +1$ for $r \ge r_{\text{max}}$ and $-1$ otherwise.
152+
/// At the maximum, $D(r_{\text{max}}) = 0$ from both sides, ensuring $C^1$ continuity.
150153
///
151154
/// This factor is defined such that the force vector can be computed
152155
/// by a single vector multiplication: $\vec{F} = -D \cdot \vec{r}$.
153156
#[inline(always)]
154-
fn diff(r_sq: T, (a, b, c, r_fusion_sq): Self::Params) -> T {
155-
let is_safe = T::from((r_sq >= r_fusion_sq) as u8 as f32);
156-
let effective_r_sq = r_sq.max(r_fusion_sq);
157+
fn diff(r_sq: T, (a, b, c, r_max_sq, _two_e_max): Self::Params) -> T {
158+
let mask = T::from((r_sq >= r_max_sq) as u8 as f32);
159+
let sign = mask + mask - T::from(1.0);
157160

158-
let inv_r = effective_r_sq.rsqrt();
159-
let r = effective_r_sq * inv_r;
161+
let inv_r = r_sq.rsqrt();
162+
let r = r_sq * inv_r;
160163
let inv_r2 = inv_r * inv_r;
161164
let inv_r4 = inv_r2 * inv_r2;
162165
let inv_r8 = inv_r4 * inv_r4;
163166

164167
let exp_term = T::exp(-b * r);
168+
let d_buck = a * b * exp_term * inv_r - T::from(6.0) * c * inv_r8;
165169

166-
let repulsion_factor = a * b * exp_term * inv_r;
167-
let attraction_factor = T::from(6.0) * c * inv_r8;
168-
let diff_unsafe = repulsion_factor - attraction_factor;
169-
170-
const FUSION_FORCE_PENALTY: f32 = 1.0e6;
171-
let penalty = T::from(FUSION_FORCE_PENALTY);
172-
173-
diff_unsafe * is_safe + penalty * (T::from(1.0) - is_safe)
170+
sign * d_buck
174171
}
175172

176173
/// Computes both energy and force pre-factor efficiently.
177174
///
178175
/// This method reuses intermediate calculations to minimize operations.
179176
#[inline(always)]
180-
fn compute(r_sq: T, (a, b, c, r_fusion_sq): Self::Params) -> EnergyDiff<T> {
181-
let is_safe = T::from((r_sq >= r_fusion_sq) as u8 as f32);
182-
let effective_r_sq = r_sq.max(r_fusion_sq);
177+
fn compute(r_sq: T, (a, b, c, r_max_sq, two_e_max): Self::Params) -> EnergyDiff<T> {
178+
let mask = T::from((r_sq >= r_max_sq) as u8 as f32);
179+
let sign = mask + mask - T::from(1.0);
183180

184-
let inv_r = effective_r_sq.rsqrt();
185-
let r = effective_r_sq * inv_r;
181+
let inv_r = r_sq.rsqrt();
182+
let r = r_sq * inv_r;
186183
let inv_r2 = inv_r * inv_r;
187184
let inv_r4 = inv_r2 * inv_r2;
188185
let inv_r6 = inv_r4 * inv_r2;
189186
let inv_r8 = inv_r6 * inv_r2;
190187

191188
let exp_term = T::exp(-b * r);
192189

193-
let repulsion_energy = a * exp_term;
194-
let attraction_energy = c * inv_r6;
195-
let energy_unsafe = repulsion_energy - attraction_energy;
196-
197-
let repulsion_force = repulsion_energy * b * inv_r;
198-
let attraction_force = T::from(6.0) * c * inv_r8;
199-
let diff_unsafe = repulsion_force - attraction_force;
200-
201-
const FUSION_ENERGY_PENALTY: f32 = 1.0e6;
202-
const FUSION_FORCE_PENALTY: f32 = 1.0e6;
203-
204-
let energy_penalty = T::from(FUSION_ENERGY_PENALTY);
205-
let force_penalty = T::from(FUSION_FORCE_PENALTY);
190+
let e_buck = a * exp_term - c * inv_r6;
191+
let d_buck = a * b * exp_term * inv_r - T::from(6.0) * c * inv_r8;
206192

207193
EnergyDiff {
208-
energy: energy_unsafe * is_safe + energy_penalty * (T::from(1.0) - is_safe),
209-
diff: diff_unsafe * is_safe + force_penalty * (T::from(1.0) - is_safe),
194+
energy: sign * e_buck + (T::from(1.0) - mask) * two_e_max,
195+
diff: sign * d_buck,
210196
}
211197
}
212198
}

0 commit comments

Comments
 (0)