r"""
Module for handling Yukawa potential.
Potential
*********
The Yukawa potential between two charges :math:`q_i` and :math:`q_j` at distant :math:`r` is defined as
.. math::
U_{ab}(r) = \frac{q_a q_b}{4 \pi \epsilon_0} \frac{e^{- \kappa r} }{r}.
where :math:`\kappa = 1/\lambda` is the screening parameter.
Potential Attributes
********************
The elements of the :attr:`sarkas.potentials.core.Potential.pot_matrix` are:
.. code-block:: python
pot_matrix[0] = q_iq_j^2/(4 pi eps0)
pot_matrix[1] = 1/lambda
pot_matrix[2] = Ewald screening parameter
"""
from math import erfc
from numba import jit
from numba.core.types import float64, UniTuple
from numpy import exp, pi, sqrt, zeros
from warnings import warn
from ..utilities.maths import force_error_analytic_lcl, force_error_analytic_pp
@jit(UniTuple(float64, 2)(float64, float64[:]), nopython=True)
def yukawa_force_pppm(r_in, pot_matrix):
"""
Numba'd function to calculate Potential and Force between two particles when the pppm algorithm is chosen.
Parameters
----------
r_in : float
Distance between two particles.
pot_matrix : numpy.ndarray
It contains potential dependent variables. \n
Shape = (4, :attr:`sarkas.core.Parameters.num_species`, :attr:`sarkas.core.Parameters.num_species`)
Returns
-------
U : float
Potential value
fr : float
Force between two particles calculated using eq.(22) in :cite:`Dharuman2017`.
Examples
--------
>>> import numpy as np
>>> r = 2.0
>>> pot_matrix = np.array([ 1.0, 0.5, 0.25, 0.0001])
>>> yukawa_force_pppm(r, pot_matrix)
(0.16287410244138842, 0.18025091684402375)
"""
kappa = pot_matrix[1]
alpha = pot_matrix[2] # Ewald parameter alpha
# Short-range cutoff to deal with divergence of the Coulomb potential
rs = pot_matrix[-1]
# Branchless programming
r = r_in * (r_in >= rs) + rs * (r_in < rs)
kappa_alpha = kappa / alpha
alpha_r = alpha * r
kappa_r = kappa * r
U = (
pot_matrix[0]
* (0.5 / r)
* (exp(kappa_r) * erfc(alpha_r + 0.5 * kappa_alpha) + exp(-kappa_r) * erfc(alpha_r - 0.5 * kappa_alpha))
)
# Derivative of the exponential term and 1/r
f1 = (0.5 / r) * exp(kappa * r) * erfc(alpha_r + 0.5 * kappa_alpha) * (1.0 / r - kappa)
f2 = (0.5 / r) * exp(-kappa * r) * erfc(alpha_r - 0.5 * kappa_alpha) * (1.0 / r + kappa)
# Derivative of erfc(a r) = 2a/sqrt(pi) e^{-a^2 r^2}* (x/r)
f3 = (alpha / sqrt(pi) / r) * (
exp(-((alpha_r + 0.5 * kappa_alpha) ** 2)) * exp(kappa_r)
+ exp(-((alpha_r - 0.5 * kappa_alpha) ** 2)) * exp(-kappa_r)
)
fr = pot_matrix[0] * (f1 + f2 + f3)
return U, fr
@jit(UniTuple(float64, 2)(float64, float64[:]), nopython=True)
def yukawa_force(r_in, pot_matrix):
"""
Numba'd function to calculate Potential and Force between two particles.
Parameters
----------
r_in : float
Distance between two particles.
pot_matrix : numpy.ndarray
It contains potential dependent variables. \n
Shape = (3, :attr:`sarkas.core.Parameters.num_species`, :attr:`sarkas.core.Parameters.num_species`)
Returns
-------
U : float
Potential.
force : float
Force between two particles.
Examples
--------
>>> import numpy as np
>>> r = 2.0
>>> pot_matrix = np.array([ 1.0, 1.0, 0.0001])
>>> yukawa_force(r, pot_matrix)
(0.06766764161830635, 0.10150146242745953)
"""
# Short-range cutoff to deal with divergence of the Coulomb potential
rs = pot_matrix[-1]
# Branchless programming
r = r_in * (r_in >= rs) + rs * (r_in < rs)
U = pot_matrix[0] * exp(-pot_matrix[1] * r) / r
force = U * (1.0 / r + pot_matrix[1])
return U, force
@jit(float64(float64, float64[:]), nopython=True)
def force_deriv(r, pot_matrix):
"""Numba'd function to calculate the second derivative of the potential.
Parameters
----------
r : float
Distance between particles
pot_matrix : numpy.ndarray
Values of the potential constants. \n
Shape = (3, :attr:`sarkas.core.Parameters.num_species`, :attr:`sarkas.core.Parameters.num_species`)
Returns
-------
f_dev : float
Second derivative of potential.
"""
kappa_r = pot_matrix[1] * r
U2 = pot_matrix[0] * exp(-kappa_r) / r**3
f_dev = U2 * (2.0 * (1.0 + kappa_r) + kappa_r**2)
return f_dev
[docs]def update_params(potential):
"""
Assign potential dependent simulation's parameters.
Parameters
----------
potential : :class:`sarkas.potentials.core.Potential`
Class handling potential form.
"""
if potential.method == "pppm":
potential.matrix = zeros((4, potential.num_species, potential.num_species))
else:
potential.matrix = zeros((3, potential.num_species, potential.num_species))
potential.matrix[1, :, :] = 1.0 / potential.screening_length
# potential.matrix[0, :, :] = potential.species_charges.reshape((len(potential.species_charge), 1))
# * potential.species_charges / potential.fourpie0
# the above line is the Python version of the for loops below. I believe that the for loops are easier to understand
for i, q1 in enumerate(potential.species_charges):
for j, q2 in enumerate(potential.species_charges):
potential.matrix[0, i, j] = q1 * q2 / potential.fourpie0
potential.matrix[-1, :, :] = potential.a_rs
if potential.method == "pp":
# The rescaling constant is sqrt ( na^4 ) = sqrt( 3 a/(4pi) )
potential.force = yukawa_force
potential.force_error = force_error_analytic_lcl(
potential.type, potential.rc, potential.matrix, sqrt(3.0 * potential.a_ws / (4.0 * pi))
)
# # Force error calculated from eq.(43) in Ref.[1]_
# potential.force_error = sqrt( TWOPI / potential.electron_TF_wavelength) * exp(- potential.rc / potential.electron_TF_wavelength)
# # Renormalize
# potential.force_error *= potential.a_ws ** 2 * sqrt(potential.total_num_ptcls / potential.pbox_volume)
elif potential.method == "pppm":
potential.force = yukawa_force_pppm
potential.matrix[2, :, :] = potential.pppm_alpha_ewald
rescaling_constant = sqrt(potential.total_num_ptcls) * potential.a_ws**2 / sqrt(potential.pbox_volume)
potential.pppm_pp_err = force_error_analytic_pp(
potential.type, potential.rc, potential.screening_length, potential.pppm_alpha_ewald, rescaling_constant
)
# PP force error calculation. Note that the equation was derived for a single component plasma.
# kappa_over_alpha = -0.25 * (potential.matrix[1, 0, 0] / potential.matrix[2, 0, 0]) ** 2
# alpha_times_rcut = -((potential.matrix[2, 0, 0] * potential.rc) ** 2)
# potential.pppm_pp_err = 2.0 * exp(kappa_over_alpha + alpha_times_rcut) / sqrt(potential.rc)
# potential.pppm_pp_err *= sqrt(potential.total_num_ptcls) * potential.a_ws ** 2 / sqrt(potential.pbox_volume)
[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.
"""
print(f"screening type : {potential.screening_length_type}")
print(f"screening length = {potential.screening_length:.6e} ", end="")
print("[cm]" if potential.units == "cgs" else "[m]")
print(f"kappa = {potential.a_ws / potential.screening_length:.4f}")
print(f"Gamma_eff = {potential.coupling_constant:.2f}")