Source code for gamdpy.interactions.potential_functions.apply_cubic_spline_cutoff
import numba
[docs]
def apply_cubic_spline_cutoff(pair_potential):
r""" Apply cubic spline cutoff to a pair-potential function
Actually the cubic spline is applied to the pair force as done by LAMMPS
(see https://docs.lammps.org/pair_lj_smooth.html).
The potential energy is given by a quartic function in the transition region.
Specifically, if :math:`u(r)` is the original potential, then the (modified) truncated potential is
.. math::
u_m(r) = u(r) + K \quad (r < r_i),
.. math::
u_m(r) = C_0 + C_1 [r-r_i] + \frac{C_2}{2} [r-r_i]^2 + \frac{C_3}{3} [r-r_i]^3 + \frac{C_4}{4} [r-r_i]^4 \quad (r_i \le r \le r_c),
and
.. math::
u_m(r) = 0 \quad (r > r_c),
The parameters (the :math:`C`'s and the :math:`K`) for a smooth truncation are computed automatically:
At the inner cutoff radius, the force and its first derivative match the unsmoothed potential (:math:`u(r)`).
At the outer cutoff radius, both the force and the first derivative are zero.
See SM of Ref. [Leoni2025]_ for more details.
The last two values of the parameter array (`params`) are the inner and outer cutoffs: `[..., r_i, r_c]`.
Parameters
----------
pair_potential: callable
a function that calculates a pair-potential:
u, s, umm = pair_potential(dist, params)
Returns
-------
potential: callable
a function where a cubic spline cutoff is applied to original function
References
----------
.. [Leoni2025] Fabio Leoni, John Russo, Francesco Sciortino, and Taiki Yanagishima.
"Generating Ultrastable Glasses by Homogenizing the Local Virial Stress".
Phys. Rev. Lett. 134, 128201 (2025)
https://doi.org/10.1103/PhysRevLett.134.128201
"""
# Cut-off by computing potential twice, avoiding changes to params
# Note: calls original potential function twice each time, avoiding changes to params.
# The four coefficients of the spline are also computed each time, along with the two energy
# shift constants, which results in a small but noticeable performance cost.
pair_pot = numba.njit(pair_potential)
# @numba.njit # Jit moved to 'last minut', i.e. PairPotential.get_kernel
def potential(dist, params): # pragma: no cover
cut_outer = params[-1]
if dist>cut_outer:
return numba.float32(0.0), numba.float32(0.0), numba.float32(0.0)
cut_inner = params[-2]
Delta_r = cut_outer - cut_inner
u_bare, s_bare, umm_bare = pair_pot(dist, params)
u_cut_inner, s_cut_inner, umm_cut_inner = pair_pot(cut_inner, params)
two = numba.float32(2.0)
three = numba.float32(3.0)
four = numba.float32(4.0)
C1, C2, C3, C4 = (s_cut_inner*cut_inner,
-umm_cut_inner,
(-three*s_cut_inner*cut_inner + two*umm_cut_inner * Delta_r) / Delta_r**2,
(two*s_cut_inner*cut_inner - umm_cut_inner*Delta_r)/Delta_r**3)
# for the potential, we integrate, and include a constant C0 whose value is such that the potential is zero at the outer cutoff
C0 = - (C1*Delta_r + C2*Delta_r**2/two + C3*Delta_r**3/three + C4*Delta_r**4/four)
# And now we have to shift the LJ potential inside the inner cutoff to match the potential at that point
K = -C0 - u_cut_inner
if dist < cut_inner:
s = s_bare
u = u_bare + K
umm = umm_bare
else:
delta_r = dist - cut_inner
u = - (C0 + C1*delta_r + C2*delta_r**2/two + C3*delta_r**3/three + C4*delta_r**4/four)
s = (C1 + C2*delta_r + C3*delta_r**2 + C4*delta_r**3)/dist
umm = - (C2 + two*C3*delta_r + three*C4*delta_r**2)
return u, s, umm
return potential