Source code for sarkas.potentials.egs

r"""
Module for handling Exact Gradient corrected Screened (EGS) Potential.

Potential
*********

The exact-gradient screened (EGS) potential introduces new parameters that can be easily calculated from initial inputs.
Density gradient corrections to the free energy functional lead to the first parameter, :math:`\nu`,

.. math::
   \nu = - \frac{3\lambda}{\pi^{3/2}}  \frac{4\pi \bar{e}^2 \beta }{\Lambda_{e}} \frac{d}{d\eta} \mathcal I_{-1/2}(\eta),

where :math:`\lambda` is a correction factor; :math:`\lambda = 1/9` for the true gradient corrected Thomas-Fermi model
and :math:`\lambda = 1` for the traditional von Weissaecker model, :math:`\mathcal I_{-1/2}[\eta_0]` is the
Fermi Integral of order :math:`-1/2`, and :math:`\Lambda_e` is the de Broglie wavelength of the electrons.

In the case :math:`\nu < 1` the EGS potential takes the form

.. math::
   U_{ab}(r) = \frac{Z_a Z_b \bar{e}^2 }{2r}\left [ ( 1+ \alpha ) e^{-r/\lambda_-} + ( 1 - \alpha) e^{-r/\lambda_+} \right ],

with

.. math::
   \lambda_\pm^2 = \frac{\nu \lambda_{\textrm{TF}}^2}{2b \pm 2b\sqrt{1 - \nu}}, \quad \alpha = \frac{b}{\sqrt{b - \nu}},

where the parameter :math:`b` arises from exchange-correlation contributions, see below.\n
On the other hand :math:`\nu > 1`, the pair potential has the form

.. math::
   U_{ab}(r) = \frac{Z_a Z_b \bar{e}^2}{r}\left [ \cos(r/\gamma_-) + \alpha' \sin(r/\gamma_-) \right ] e^{-r/\gamma_+}

with

.. math::
   \gamma_\pm^2 = \frac{\nu\lambda_{\textrm{TF}}^2}{\sqrt{\nu} \pm b}, \quad \alpha' = \frac{b}{\sqrt{\nu - b}}.

Neglect of exchange-correlational effects leads to :math:`b = 1` otherwise

.. math::
   b = 1 - \frac{2}{8} \frac{1}{k_{\textrm{F}}^2 \lambda_{\textrm{TF}}^2 }  \left [ h\left ( \Theta \right ) - 2 \Theta h'(\Theta) \right ]

where :math:`k_{\textrm{F}}` is the Fermi wavenumber and :math:`\Theta = (\beta E_{\textrm{F}})^{-1}` is the electron
degeneracy parameter` calculated from the Fermi energy.

.. math::
   h \left ( \Theta \right) = \frac{N(\Theta)}{D(\Theta)}\tanh \left( \Theta^{-1} \right ),

.. math::
   N(\Theta) = 1 + 2.8343\Theta^2 - 0.2151\Theta^3 + 5.2759\Theta^4,

.. math::
   D \left ( \Theta \right ) = 1 + 3.9431\Theta^2 + 7.9138\Theta^4.

Force Error
***********

The EGS potential is always smaller than pure Yukawa. Therefore the force error is chosen to be the same as Yukawa's

.. math::

    \Delta F = \frac{q^2}{4 \pi \epsilon_0} \sqrt{\frac{2 \pi n}{\lambda_{-}}}e^{-r_c/\lambda_-}

This overestimates it, but it doesn't matter.

Potential Attributes
********************

The elements of :attr:`sarkas.potentials.core.Potential.matrix` are

if :attr:`sarkas.core.Parameters.nu` less than 1:

.. code-block::

    matrix[0] = q_iq_j/4pi eps0
    matrix[1] = nu
    matrix[2] = 1 + alpha
    matrix[3] = 1 - alpha
    matrix[4] = 1.0 / lambda_minus
    matrix[5] = 1.0 / lambda_plus

else

.. code-block::

    matrix[0] = q_iq_j/4pi eps0
    matrix[1] = nu
    matrix[2] = 1.0
    matrix[3] = alpha prime
    matrix[4] = 1.0 / gamma_minus
    matrix[5] = 1.0 / gamma_plus

"""
from numba import jit
from numba.core.types import float64, UniTuple
from numpy import cos, cosh, exp, inf, pi, sin, sqrt, tanh, zeros
from scipy.integrate import quad

from ..utilities.exceptions import AlgorithmError
from ..utilities.fdints import fdm3h
from ..utilities.maths import force_error_analytic_lcl


[docs]def update_params(potential, species): """ Assign potential dependent simulation's parameters. Parameters ---------- potential : :class:`sarkas.potentials.core.Potential` Class handling potential form. species : list, List of species data (:class:`sarkas.plasma.Species`). Raises ------ `~sarkas.utilities.exceptions.AlgorithmError` If the chosen algorithm is pppm. """ # lambda factor : 1 = von Weizsaecker, 1/9 = Thomas-Fermi if not hasattr(potential, "lmbda"): potential.lmbda = 1.0 / 9.0 # Electron background eb = species[-1] # eq. (14) of Ref. [1]_ potential.nu = 3.0 / pi**1.5 * eb.landau_length / eb.deBroglie_wavelength dIdeta = -3.0 / 2.0 * fdm3h(eb.dimensionless_chemical_potential) potential.nu *= potential.lmbda * dIdeta # Degeneracy Parameter theta = eb.degeneracy_parameter if 0.1 <= theta <= 12: # Regime of validity of the following approximation Perrot et al. Phys Rev A 302619 (1984) # eq. (33) of Ref. [1]_ Ntheta = 1.0 + 2.8343 * theta**2 - 0.2151 * theta**3 + 5.2759 * theta**4 # eq. (34) of Ref. [1]_ Dtheta = 1.0 + 3.9431 * theta**2 + 7.9138 * theta**4 # eq. (32) of Ref. [1]_ h = Ntheta / Dtheta * tanh(1.0 / theta) # grad h(x) gradh = -(Ntheta / Dtheta) / cosh(1 / theta) ** 2 / (theta**2) - tanh( # derivative of tanh(1/x) 1.0 / theta ) * ( Ntheta * (7.8862 * theta + 31.6552 * theta**3) / Dtheta**2 # derivative of 1/Dtheta + (5.6686 * theta - 0.6453 * theta**2 + 21.1036 * theta**3) / Dtheta ) # derivative of Ntheta # eq.(31) of Ref. [1]_ b = 1.0 - 2.0 / (8.0 * (eb.Fermi_wavenumber * eb.ThomasFermi_wavelength) ** 2) * (h - 2.0 * theta * gradh) else: b = 1.0 potential.b = b # Monotonic decay if potential.nu <= 1: # eq. (29) of Ref. [1]_ potential.lambda_p = eb.ThomasFermi_wavelength * sqrt( potential.nu / (2.0 * b + 2.0 * sqrt(b**2 - potential.nu)) ) potential.lambda_m = eb.ThomasFermi_wavelength * sqrt( potential.nu / (2.0 * b - 2.0 * sqrt(b**2 - potential.nu)) ) potential.alpha = b / sqrt(b - potential.nu) # Oscillatory behavior elif potential.nu > 1: # eq. (29) of Ref. [1]_ potential.gamma_m = eb.ThomasFermi_wavelength * sqrt(potential.nu / (sqrt(potential.nu) - b)) potential.gamma_p = eb.ThomasFermi_wavelength * sqrt(potential.nu / (sqrt(potential.nu) + b)) potential.alphap = b / sqrt(potential.nu - b) potential.matrix = zeros((potential.num_species, potential.num_species, 7)) potential.matrix[:, :, 1] = potential.nu for i, q1 in enumerate(potential.species_charges): for j, q2 in enumerate(potential.species_charges): if potential.nu <= 1: potential.matrix[i, j, 0] = q1 * q2 / potential.fourpie0 potential.matrix[i, j, 2] = 1.0 + potential.alpha potential.matrix[i, j, 3] = 1.0 - potential.alpha potential.matrix[i, j, 4] = 1.0 / potential.lambda_m potential.matrix[i, j, 5] = 1.0 / potential.lambda_p elif potential.nu > 1: potential.matrix[i, j, 0] = q1 * q2 / potential.fourpie0 potential.matrix[i, j, 2] = 1.0 potential.matrix[i, j, 3] = potential.alphap potential.matrix[i, j, 4] = 1.0 / potential.gamma_m potential.matrix[i, j, 5] = 1.0 / potential.gamma_p potential.matrix[:, :, 6] = potential.a_rs if potential.method == "pppm": raise AlgorithmError("pppm algorithm not implemented yet.") potential.force = egs_force potential.potential_derivatives = potential_derivatives # EGS is always smaller than pure Yukawa. # Therefore the force error is chosen to be the same as Yukawa's. # This overestimates it, but it doesn't matter. # The rescaling constant is sqrt ( na^4 ) = sqrt( 3 a/(4pi) ) rescaling_constant = sqrt(3.0 * potential.a_ws / (4.0 * pi)) potential.force_error = force_error_analytic_lcl(potential.type, potential.rc, potential.matrix, rescaling_constant) potential.force_error = calc_force_error_quad(potential.a_ws, potential.rc, potential.matrix)
[docs]@jit(UniTuple(float64, 2)(float64, float64[:]), nopython=True) def egs_force(r_in, pot_matrix): """ Numba'd function to calculate the potential and force between particles using the EGS Potential. Parameters ---------- r_in : float Particles' distance. pot_matrix : numpy.ndarray EGS potential parameters. Shape = 6. Returns ------- u_r : float Potential. f_r : float Force. Examples -------- >>> from numpy import array, pi >>> from scipy.constants import epsilon_0 >>> r = 2.0 >>> alpha = 1.3616 >>> lambda_p = 1.778757e-09 >>> lambda_m = 4.546000e-09 >>> charge = 1.440961e-09 >>> c_const = charge**2/( 4.0 * pi * epsilon_0) >>> pot_mat = array([c_const * 0.5, 1.0 + alpha, 1.0 - alpha, 1.0/lambda_m, 1.0 / lambda_p, 1.0e-14]) >>> egs_force(r, pot_mat) (-0.9067719924627385, 270184640.33105946) """ rs = pot_matrix[6] # Branchless programming r = r_in * (r_in >= rs) + rs * (r_in < rs) q2_e0 = pot_matrix[0] nu = pot_matrix[1] if nu <= 1.0: inv_lambda_minus = pot_matrix[4] inv_lambda_plus = pot_matrix[5] alpha_plus = pot_matrix[2] # 1 + alpha alpha_minus = pot_matrix[3] # 1 - alpha temp1 = alpha_plus * exp(-r * inv_lambda_minus) temp2 = alpha_minus * exp(-r * inv_lambda_plus) # Potential u_r = 0.5 * q2_e0 * (temp1 + temp2) / r # First derivative f_r = u_r / r + 0.5 * q2_e0 * (temp1 * inv_lambda_minus + temp2 * inv_lambda_plus) / r else: inv_gamma_minus = pot_matrix[4] inv_gamma_plus = pot_matrix[5] alpha_prime = pot_matrix[3] # alpha` coskr = cos(r * inv_gamma_minus) sinkr = sin(r * inv_gamma_minus) expkr = exp(-r * inv_gamma_plus) # Potential u_r = (coskr + alpha_prime * sinkr) * expkr / r # First derivative dv_dr = -u_r / r # derivative of 1/r dv_dr += -u_r * inv_gamma_plus # derivative of exp temp = inv_gamma_minus * (alpha_prime * coskr - sinkr) * expkr / r dv_dr += temp # Force f_r = -dv_dr u_r *= q2_e0 f_r *= q2_e0 return u_r, f_r
[docs]def potential_derivatives(r, pot_matrix): """Calculate the first and second derivatives 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. """ q2_e0 = pot_matrix[0] nu = pot_matrix[1] r2 = r * r if nu <= 1.0: inv_lambda_minus = pot_matrix[4] inv_lambda_plus = pot_matrix[5] alpha_plus = pot_matrix[2] # 1 + alpha alpha_minus = pot_matrix[3] # 1 - alpha temp1 = alpha_plus * exp(-r * inv_lambda_minus) temp2 = alpha_minus * exp(-r * inv_lambda_plus) # Potential u_r = 0.5 * q2_e0 * (temp1 + temp2) / r # First derivative dv_dr = -u_r / r - 0.5 * q2_e0 * (temp1 * inv_lambda_minus + temp2 * inv_lambda_plus) / r # Second derivative d2v_dr2 = ( 2.0 * u_r / r**2 + 0.5 * q2_e0 * (temp1 * inv_lambda_minus + temp2 * inv_lambda_plus) / r**2 + 0.5 * q2_e0 * (temp1 * inv_lambda_minus**2 + temp2 * inv_lambda_plus**2) / r ) else: inv_gamma_minus = pot_matrix[4] inv_gamma_plus = pot_matrix[5] alpha_prime = pot_matrix[3] # alpha` coskr = cos(r * inv_gamma_minus) sinkr = sin(r * inv_gamma_minus) expkr = exp(-r * inv_gamma_plus) # Potential u_r = (coskr + alpha_prime * sinkr) * expkr / r # First derivative dv_dr = -u_r / r # derivative of 1/r dv_dr += -u_r * inv_gamma_plus # derivative of exp temp = inv_gamma_minus * (alpha_prime * coskr - sinkr) * expkr / r dv_dr += temp # Second derivative d2v_dr2 = ( u_r / r2 - (1.0 / r + inv_gamma_plus) * dv_dr - inv_gamma_minus**2 * u_r - (1.0 / r + inv_gamma_plus) * temp ) u_r *= q2_e0 dv_dr *= q2_e0 d2v_dr2 *= q2_e0 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. """ # Pre compute the units to be printed print(f"kappa = {potential.a_ws / potential.screening_length:.4f}") print(f"SGA Correction factor: lmbda = {potential.lmbda:.4f}") # print('lambda_TF = {:1.4e} '.format(potential.electron_TF_wavelength), end='') # print("[cm]" if potential.units == "cgs" else "[m]") print(f"nu = {potential.nu:.4f}") if potential.nu < 1: print("Exponential decay:") print(f"lambda_p = {potential.lambda_p:.6e} ", end="") print("[cm]" if potential.units == "cgs" else "[m]") print(f"lambda_m = {potential.lambda_m:.6e} ", end="") print("[cm]" if potential.units == "cgs" else "[m]") print(f"alpha = {potential.alpha:.4f}") # print('Theta = {:1.4e}'.format(potential.electron_degeneracy_parameter)) print(f"b = {potential.b:.4f}") else: print("Oscillatory potential:") print(f"gamma_p = {potential.gamma_p:.6e} ", end="") print("[cm]" if potential.units == "cgs" else "[m]") print(f"gamma_m = {potential.gamma_m:.6e} ", end="") print("[cm]" if potential.units == "cgs" else "[m]") print(f"alpha = {potential.alphap:.4f}") print(f"b = {potential.b:.4f}") print(f"Gamma_eff = {potential.coupling_constant:.2f}")
[docs]def force_error_integrand(r, pot_matrix): _, 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] = 1 # Un-dimensionalize the screening lengths. # Note that the screening lengths are in the same position in either case of nu params[4] *= a params[5] *= a r_c = rc / a result, _ = quad(force_error_integrand, a=r_c, b=inf, args=(params,)) f_err = sqrt(result) return f_err