Source code for sarkas.potentials.yukawa

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.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, inf, pi, sqrt, zeros
from scipy.integrate import quad
from warnings import warn

from ..utilities.maths import force_error_analytic_lcl, force_error_analytic_pp


[docs]@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_r : float Potential value f_r : 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_r = ( 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) ) f_r = pot_matrix[0] * (f1 + f2 + f3) return u_r, f_r
[docs]@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_r : float Potential. f_r : 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_r = pot_matrix[0] * exp(-pot_matrix[1] * r) / r f_r = u_r * (1.0 / r + pot_matrix[1]) return u_r, f_r
[docs]def force_deriv(r, pot_matrix): """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 ------- d2v_dr2 : float, numpy.ndarray Second derivative of the potential. Raises ------ : DeprecationWarning """ warn( "Deprecated feature. It will be removed in a future release. \n" "Use potential_derivatives.", category=DeprecationWarning, ) _, _, d2v_dr2 = potential_derivatives(r, pot_matrix) return d2v_dr2
[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 : 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. """ kappa = pot_matrix[1] kappa_r = kappa * r u_r = exp(-kappa_r) / r dv_dr = -(1.0 + kappa_r) * u_r / r d2v_dr2 = -(1.0 / r + kappa) * dv_dr + u_r / r**2 u_r *= pot_matrix[0] dv_dr *= pot_matrix[0] d2v_dr2 *= pot_matrix[0] 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}" ) print(msg)
[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((potential.num_species, potential.num_species, 4)) else: potential.matrix = zeros((potential.num_species, potential.num_species, 3)) 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[i, j, 0] = q1 * q2 / potential.fourpie0 potential.matrix[:, :, -1] = potential.a_rs potential.potential_derivatives = potential_derivatives 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)) # ) potential.force_error = calc_force_error_quad(potential.a_ws, potential.rc, potential.matrix[0, 0]) # # 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[0, 0, 1] / potential.matrix[0, 0, 2]) ** 2 # alpha_times_rcut = -((potential.matrix[0, 0, 2] * 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 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.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`. Examples -------- >>> import numpy as np >>> potential_matrix = np.zeros(2) >>> a = 1.0 # Wigner-seitz radius >>> kappa = 2.0 # in units of a_ws >>> potential_matrix[1] = kappa >>> rc = 6.0 # in units of a_ws >>> calc_force_error_quad(a, rc, potential_matrix) 6.636507826720378e-06 """ params = pot_matrix.copy() params[0] = 1 # Un-dimensionalize the screening length. params[1] *= a r_c = rc / a result, _ = quad(force_error_integrand, a=r_c, b=inf, args=(params,)) f_err = sqrt(result) return f_err