r"""
Module for handling custom potential of the form given below.
Average Atom Fit Potential
**************************
The form of the potential is
.. math::
U(r) = \frac{q_i q_j}{4 \pi \epsilon_0} a \frac{e^{- \kappa r} }{r} \frac{1}{a + b e^{c (r - d)}} + h \cos\left ( (r-i) j e^{-kr} \right ) e^{-l r},
where :math:`\kappa, a, b, c, d, h, i, j, k, l` are fit parameters to be passed. Remember to use the correct units.
Force Error
***********
The force error is calculated from the ratio
.. math::
\Delta F = \frac{F(r_c)}{F(2a_{ws})},
where :math:`F(x)` is the force between two particles at distance :math:`x`, and :math:`a_{ws}` is the Wigner-Seitz radius.
Potential Attributes
********************
The elements of the :attr:`sarkas.potentials.core.Potential.matrix` are:
.. code-block:: python
pot_matrix[0] = q_iq_je^2/(4 pi eps_0) Force factor between two particles.
pot_matrix[1] = kappa
pot_matrix[2] = a
pot_matrix[3] = b
pot_matrix[4] = c
pot_matrix[5] = d
pot_matrix[6] = a_rs. Short-range cutoff.
"""
from numba import jit
from numba.core.types import float64, UniTuple
from numpy import array, cos, exp, inf, pi, sin, sqrt, zeros
from scipy.integrate import quad
[docs]@jit(UniTuple(float64, 2)(float64, float64[:]), nopython=True)
def fit_force(r, pot_matrix):
"""
Numba'd function to calculate the PP force between particles using the Moliere Potential.
Parameters
----------
r : float
Particles' distance.
pot_matrix : numpy.ndarray
Slice of `sarkas.potentials.Potential.matrix` containing the potential parameters.
Returns
-------
u_r : float
Potential.
f_r : float
Force between two particles.
"""
# Unpack the parameters
a = pot_matrix[0]
b = pot_matrix[1]
c = pot_matrix[2]
d = pot_matrix[3]
e = pot_matrix[4]
f = pot_matrix[5]
g = pot_matrix[6]
h = pot_matrix[7]
i = pot_matrix[8]
j = pot_matrix[9]
k = pot_matrix[10]
l = pot_matrix[11]
yukawa = a * exp(-b * r) / r
dyuk_dr = -(1.0 / r + b) * yukawa
sigmoid = 1.0 / (1.0 + exp(c * (r - d)))
dsig_dr = -c * sigmoid**2 * exp(c * (r - d))
angle = (r - f) * g * exp(-h * r)
dangle_dr = -h * angle + g * exp(-h * r)
cos_term = e * cos(angle) * exp(-i * r)
arg = -((k - r) ** 2) / l
gaussian_term = j * exp(arg)
u_r = yukawa * sigmoid + cos_term + gaussian_term
# derivative of the yukawa term
f1 = -dyuk_dr * sigmoid - yukawa * dsig_dr
# derivative of the cos term
# dcos_term = - e * sin(angle) * dangle_dr * exp(-i * r) - i * e * cos(angle)*exp(-i*r)
f2 = e * sin(angle) * dangle_dr * exp(-i * r) + i * cos_term
# derivative of the exp term
f3 = -2.0 * (k - r) / l * gaussian_term
force = f1 + f2 + f3
return u_r, force
[docs]def potential_derivatives(r, pot_matrix):
"""Calculate the first and second derivative of the potential.
Parameters
----------
r_in : float
Distance between two particles.
pot_matrix : numpy.ndarray
It contains potential dependent variables.
Returns
-------
u_r : float, numpy.ndarray
Potential value.
dv_dr : float, numpy.ndarray
First derivative of the potential.
d2v_dr2 : float, numpy.ndarray
Second derivative of the potential.
"""
# Unpack the parameters
a = pot_matrix[0]
b = pot_matrix[1]
c = pot_matrix[2]
d = pot_matrix[3]
e = pot_matrix[4]
f = pot_matrix[5]
g = pot_matrix[6]
h = pot_matrix[7]
i = pot_matrix[8]
j = pot_matrix[9]
k = pot_matrix[10]
l = pot_matrix[11]
yukawa = a * exp(-b * r) / r
dyuk_dr = -(1.0 / r + b) * yukawa
d2yuk_dr2 = (1.0 / r**2) * yukawa - (1.0 / r + b) * dyuk_dr
sigmoid = 1.0 / (1.0 + exp(c * (r - d)))
dsig_dr = -c * exp(c * (r - d)) * sigmoid**2
d2sig_dr2 = -2.0 * c * exp(c * (r - d)) * sigmoid * dsig_dr + c * dsig_dr
angle = (r - f) * g * exp(-h * r)
dangle_dr = -h * angle + g * exp(-h * r)
d2angle_dr2 = -h * dangle_dr - h * g * exp(-h * r)
cos_term = e * cos(angle) * exp(-i * r)
arg = -((k - r) ** 2) / l
gaussian_term = j * exp(arg)
u_r = yukawa * sigmoid + cos_term + gaussian_term
# derivative of the yukawa term
f1 = dyuk_dr * sigmoid + yukawa * dsig_dr
# derivative of the cos term
f2 = -e * sin(angle) * dangle_dr * exp(-i * r) - i * cos_term
# derivative of the exp term
f3 = 2.0 * (k - r) / l * gaussian_term
dv_dr = f1 + f2 + f3
# Derivative of f1
v1 = d2yuk_dr2 * sigmoid + dyuk_dr * dsig_dr + dyuk_dr * dsig_dr + yukawa * d2sig_dr2
# derivative of f2
v2 = -e * (cos(angle) * dangle_dr**2 + sin(angle) * (d2angle_dr2 - i * dangle_dr)) * exp(-i * r) - i * f2
# derivative of f3
v3 = -2.0 / l * gaussian_term + (2.0 * (k - r) / l) * f3
d2v_dr2 = v1 + v2 + v3
return u_r, dv_dr, d2v_dr2
[docs]def pretty_print_info(potential):
"""
Print potential specific parameters in a user-friendly way.
Parameters
----------
potential : :class:`sarkas.potentials.core.Potential`
Class handling potential form.
"""
msg = (
f"screening type : {potential.screening_length_type}\n"
f"screening length = {potential.screening_length:.6e} {potential.units_dict['length']}\n"
f"kappa = {potential.a_ws / potential.screening_length:.4f}\n"
f"Gamma_eff = {potential.coupling_constant:.2f}\n"
f"Fit params:\n"
f"a = {potential.fit_params[0]:6e} beta/a_ws = {potential.matrix[0, 0, 0]:.6e} {potential.units_dict['energy']}\n"
f"b = {potential.fit_params[1]:.6e} / a_ws = {potential.matrix[0,0,1]:.6e} {potential.units_dict['inverse length']}\n"
f"c = {potential.fit_params[2]:.6e} / a_ws = {potential.matrix[0,0,2]:.6e} {potential.units_dict['inverse length']}\n"
f"d = {potential.fit_params[3]:.6e} a_ws = {potential.matrix[0,0,3]:.6e} {potential.units_dict['length']}\n"
f"e = {potential.fit_params[4]:.6e} beta = {potential.matrix[0,0,4]:.6e} {potential.units_dict['energy']}\n"
f"f = {potential.fit_params[5]:.6e} a_ws = {potential.matrix[0,0,5]:.6e} {potential.units_dict['length']}\n"
f"g = {potential.fit_params[6]:.6e} / a_ws = {potential.matrix[0,0,6]:.6e} {potential.units_dict['inverse length']}\n"
f"h = {potential.fit_params[7]:.6e} / a_ws = {potential.matrix[0,0,7]:.6e} {potential.units_dict['inverse length']}\n"
f"i = {potential.fit_params[8]:.6e} / a_ws = {potential.matrix[0,0,8]:.6e} {potential.units_dict['inverse length']}\n"
f"j = {potential.fit_params[9]:.6e} beta = {potential.matrix[0,0,9]:.6e} {potential.units_dict['energy']}\n"
f"k = {potential.fit_params[10]:.6e} a_ws = {potential.matrix[0,0,10]:.6e} {potential.units_dict['length']}\n"
f"l = {potential.fit_params[11]:.6e} a_ws^2 = {potential.matrix[0,0,11]:.6e} {potential.units_dict['length']}"
)
print(msg)
[docs]def update_params(potential):
"""
Assign potential dependent simulation's parameters.
Parameters
----------
potential : :class:`sarkas.potentials.core.Potential`
Class handling potential form.
"""
potential.fit_params = array(potential.fit_params)
params_len = len(potential.fit_params)
potential.matrix = zeros((potential.num_species, potential.num_species, params_len + 1))
beta = 1.0 / (potential.kB * potential.electron_temperature)
for i, q1 in enumerate(potential.species_charges):
for j, q2 in enumerate(potential.species_charges):
potential.matrix[i, j, 0] = potential.fit_params[0] * potential.a_ws / beta # a
potential.matrix[i, j, 1] = potential.fit_params[1] / potential.a_ws # b
potential.matrix[i, j, 2] = potential.fit_params[2] / potential.a_ws # c
potential.matrix[i, j, 3] = potential.fit_params[3] * potential.a_ws # d
potential.matrix[i, j, 4] = potential.fit_params[4] / beta # e
potential.matrix[i, j, 5] = potential.fit_params[5] * potential.a_ws # f
potential.matrix[i, j, 6] = potential.fit_params[6] / potential.a_ws # g
potential.matrix[i, j, 7] = potential.fit_params[7] / potential.a_ws # h
potential.matrix[i, j, 8] = potential.fit_params[8] / potential.a_ws # i
potential.matrix[i, j, 9] = potential.fit_params[9] / beta # j
potential.matrix[i, j, 10] = potential.fit_params[10] * potential.a_ws # k
potential.matrix[i, j, 11] = potential.fit_params[11] * potential.a_ws**2 # l
potential.matrix[i, j, 12] = potential.a_rs
potential.force = fit_force
potential.potential_derivatives = potential_derivatives
potential.force_error = calc_force_error_quad(potential.a_ws, potential.rc, potential.matrix[0, 0, :])
[docs]def force_error_integrand(r, pot_matrix):
r"""Auxiliary function to be used in `scipy.integrate.quad` to calculate the integrand.
Parameters
----------
r_in : float
Distance between two particles.
pot_matrix : numpy.ndarray
Slice of the `sarkas.potentials.core.Potential.matrix` containing the necessary potential parameters.
Returns
-------
_ : float
Integrand :math:`4\pi r^2 ( d r\phi(r)/dr )^2`
"""
_, dv_dr, _ = potential_derivatives(r, pot_matrix)
return 4.0 * pi * r**2 * dv_dr**2
[docs]def calc_force_error_quad(a, rc, pot_matrix):
r"""
Calculate the force error by integrating the square modulus of the force over the neglected volume.\n
The force error is calculated from
.. math::
\Delta F = \left [ 4 \pi \int_{r_c}^{\infty} dr \, r^2 \left ( \frac{d\phi(r)}{r} \right )^2 ]^{1/2}
where :math:`\phi(r)` is only the radial part of the potential, :math:`r_c` is the cutoff radius, and :math:`r` is scaled by the input parameter `a`.\n
The integral is calculated using `scipy.integrate.quad`. The derivative of the potential is obtained from :meth:`potential_derivatives`.
Parameters
----------
a : float
Rescaling length. Usually it is the Wigner-Seitz radius.
rc : float
Cutoff radius to be used as the lower limit of the integral. The lower limit is actually `rc /a`.
pot_matrix: numpy.ndarray
Slice of the `sarkas.potentials.Potential.matrix` containing the parameters of the potential. It must be a 1D-array.
Returns
-------
f_err: float
Force error. It is the sqrt root of the integral. It is calculated using `scipy.integrate.quad` and :func:`potential_derivatives`.
"""
params = pot_matrix.copy()
params[0] /= a # a
params[1] *= a # b
params[2] *= a # c
params[3] /= a # d
params[5] /= a # f
params[6] *= a # g
params[7] *= a # h
params[8] *= a # i
params[10] /= a # k
params[11] /= a**2 # l
r_c = rc / a
result, _ = quad(force_error_integrand, a=r_c, b=inf, args=(params,))
f_err = sqrt(result)
return f_err