Source code for sarkas.potentials.qsp

r"""
Module for handling Quantum Statistical Potentials.

Potential
*********

Quantum Statistical Potentials are defined by three terms

.. math::
    U(r) = U_{\rm pauli}(r) + U_{\rm coul} + U_{\rm diff} (r)

where

.. math::
    U_{\rm pauli}(r) = k_BT \ln (2)  e^{ - 4\pi r^2/ \Lambda_{ab}^2 }

is due to the Pauli exclusion principle,

.. math::
    U_{\rm coul}(r) = \frac{q_iq_j}{4\pi \epsilon_0} \frac{1}{r}

is the usual Coulomb interaction, and :math:`U_{\rm diff}(r)` is a diffraction term.

There are two possibilities for the diffraction term. The most common is the Deutsch Potential

.. math::
    U_{\rm deutsch}(r) = \frac{q_aq_b}{4\pi \epsilon_0} \frac{e^{- 2 \pi r/\Lambda_{ab}} }{r}.

The second most common form is the Kelbg potential

.. math::
    U_{\rm kelbg}(r) = - \frac{q_aq_b}{4\pi \epsilon_0} \frac{1}{r} \left [  e^{- 2 \pi r^2/\Lambda_{ab}^2 }
    - \sqrt{2} \pi \dfrac{r}{\Lambda_{ab}} \textrm{erfc} \left ( \sqrt{ 2\pi}  r/ \Lambda_{ab} \right )
    \right ].

In the above equations the screening length :math:`\Lambda_{ab}` is the thermal de Broglie wavelength
between the two charges defined as

.. math::
   \Lambda_{ab} = \sqrt{\frac{2\pi \hbar^2}{\mu_{ab} k_BT}}, \quad  \mu_{ab} = \frac{m_a m_b}{m_a + m_b}


Note that in Ref. :cite:`Hansen1981` the DeBroglie wavelength is defined as

.. math::
   \Lambda_{ee} = \sqrt{ \dfrac{\hbar^2}{2 \pi \mu_{ee} k_{B} T}},

while in statistical physics textbooks is defined as

.. math::
   \Lambda_{ee} = \sqrt{ \dfrac{2 \pi \hbar^2}{\mu_{ee} k_{B} T}} .

The latter will be used in Sarkas. The difference is in the factor of :math:`2\pi`, i.e. the difference between
a wave number and wave length.

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

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

.. code-block:: python

    pot_matrix[0] = qi*qj/4*pi*eps0
    pot_matrix[1] = 2pi/deBroglie
    pot_matrix[2] = e-e Pauli term factor
    pot_matrix[3] = e-e Pauli term exponent term
    pot_matrix[4] = Ewald parameter
    pot_matrix[5] = Short-range cutoff

"""

from math import erfc
from numba import jit
from numba.core.types import float64, UniTuple
from numpy import exp, log, pi, sqrt, zeros
from warnings import warn

from ..utilities.exceptions import AlgorithmWarning
from ..utilities.maths import force_error_analytic_lcl, TWOPI


[docs]def update_params(potential, species): """ Create potential dependent simulation's parameters. Parameters ---------- potential : :class:`sarkas.potentials.core.Potential` Class handling potential form. species : list, [:class:`sarkas.plasma.Species`,] Species' data. """ # Do a bunch of checks # pppm algorithm only if potential.method != "pppm": raise ValueError("QSP interaction can only be calculated using pppm algorithm.") # Check for neutrality if potential.total_net_charge != 0: warn("Total net charge is not zero.", category=AlgorithmWarning) # Default attributes if not hasattr(potential, "qsp_type"): potential.qsp_type = "deutsch" if not hasattr(potential, "qsp_pauli"): potential.qsp_pauli = True # Enforce consistency potential.qsp_type = potential.qsp_type.lower() four_pi = 2.0 * TWOPI log_2 = log(2.0) # Redefine ion temperatures and ion total number density total_ion_temperature = 0.0 total_ion_number_density = 0.0 for is1, sp1 in enumerate(species[1:]): total_ion_temperature += sp1.concentration * sp1.temperature total_ion_number_density += sp1.number_density # Calculate the total and ion Wigner-Seitz Radius from the total density ai = (3.0 / (four_pi * total_ion_number_density)) ** (1.0 / 3.0) # Ion WS deBroglie_const = TWOPI * potential.hbar**2 / potential.kB potential.matrix = zeros((6, potential.num_species, potential.num_species)) for i, sp1 in enumerate(species): m1 = sp1.mass q1 = sp1.charge for j, sp2 in enumerate(species): m2 = sp2.mass q2 = sp2.charge reduced = (m1 * m2) / (m1 + m2) if sp1.name == "e" or sp2.name == "e": # Use electron temperature in e-e and e-i interactions lambda_deB = sqrt(deBroglie_const / (reduced * species[0].temperature)) else: # Use ion temperature in i-i interactions only lambda_deB = sqrt(deBroglie_const / (reduced * total_ion_temperature)) if sp1.name == sp2.name: # e-e potential.matrix[2, i, j] = log_2 * potential.kB * sp1.temperature potential.matrix[3, i, j] = four_pi / (log_2 * lambda_deB**2) potential.matrix[0, i, j] = q1 * q2 / potential.fourpie0 potential.matrix[1, i, j] = TWOPI / lambda_deB if not potential.qsp_pauli: potential.matrix[2, :, :] = 0.0 potential.matrix[4, :, :] = potential.pppm_alpha_ewald potential.matrix[5, :, :] = potential.a_rs if potential.qsp_type == "deutsch": potential.force = deutsch_force # Calculate the LCL Force error from the e-e diffraction term only since it is the largest. potential.pppm_pp_err = force_error_analytic_lcl( potential.type, potential.rc, potential.matrix, sqrt(3.0 * potential.a_ws / (4.0 * pi)) ) elif potential.qsp_type == "kelbg": potential.force = kelbg_force # TODO: Calculate the PP Force error from the e-e diffraction term only. # the following is a placeholder potential.pppm_pp_err = force_error_analytic_lcl( potential.type, potential.rc, potential.matrix, sqrt(3.0 * potential.a_ws / (4.0 * pi)) )
@jit(UniTuple(float64, 2)(float64, float64[:]), nopython=True) def deutsch_force(r_in, pot_matrix): """ Calculate Deutsch QSP Force between two particles. Parameters ---------- r_in : float Distance between two particles. pot_matrix : numpy.ndarray It contains potential dependent variables. \n Shape = (6, :attr:`sarkas.core.Parameters.num_species`, :attr:`sarkas.core.Parameters.num_species`) Returns ------- U : float Potential. force : float Force between two particles. """ A = pot_matrix[0] C = pot_matrix[1] D = pot_matrix[2] F = pot_matrix[3] alpha = pot_matrix[4] rs = pot_matrix[5] # Branchless programming r = r_in * (r_in >= rs) + rs * (r_in < rs) a2 = alpha * alpha r2 = r * r # Ewald short-range potential and force terms U_ewald = A * erfc(alpha * r) / r f_ewald = U_ewald / r # 1/r derivative f_ewald += A * (2.0 * alpha / sqrt(pi)) * exp(-a2 * r2) / r # erfc derivative # Diffraction potential and force term U_diff = -A * exp(-C * r) / r f_diff = U_diff / r # 1/r derivative f_diff += -A * C * exp(-C * r) / r # exp derivative # Pauli potential and force terms U_pauli = D * exp(-F * r2) f_pauli = 2.0 * r * D * F * exp(-F * r2) U = U_ewald + U_diff + U_pauli force = f_ewald + f_diff + f_pauli return U, force @jit(UniTuple(float64, 2)(float64, float64[:]), nopython=True) def kelbg_force(r_in, pot_matrix): """ Calculates the QSP 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 = (6, :attr:`sarkas.core.Parameters.num_species`, :attr:`sarkas.core.Parameters.num_species`) Returns ------- U : float Potential. force : float Force between two particles. """ A = pot_matrix[0] C = pot_matrix[1] D = pot_matrix[2] F = pot_matrix[3] alpha = pot_matrix[4] rs = pot_matrix[5] # Branchless programming r = r_in * (r_in >= rs) + rs * (r_in < rs) C2 = C * C a2 = alpha * alpha r2 = r * r # Ewald short-range potential and force terms U_ewald = A * erfc(alpha * r) / r f_ewald = U_ewald / r2 # 1/r derivative f_ewald += A * (2.0 * alpha / sqrt(pi) / r2) * exp(-a2 * r2) # erfc derivative # Diffraction potential and force term U_diff = -A * exp(-C2 * r2 / pi) / r U_diff += A * C * erfc(C * r / sqrt(pi)) f_diff = -A * (2.0 * C2 * r2 + pi) * exp(-C2 * r2 / pi) / (pi * r * r2) # exp(r)/r derivative f_diff += 2.0 * A * C2 * exp(-C2 * r2 / pi) / r / pi # erfc derivative # Pauli Term U_pauli = D * exp(-F * r2) f_pauli = 2.0 * D * F * exp(-F * r2) U = U_ewald + U_diff + U_pauli force = f_ewald + f_diff + f_pauli return U, force
[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. """ ii_scr_len = 1.0 / potential.matrix[1, 1, 1] ei_scr_len = 1.0 / potential.matrix[1, 0, 1] ee_scr_len = 1.0 / potential.matrix[1, 0, 0] e_deBroglie_lambda = sqrt(2.0) * pi / potential.matrix[1, 0, 0] i_deBroglie_lambda = sqrt(2.0) * pi / potential.matrix[1, 1, 1] a_ws = potential.a_ws print(f"QSP type: {potential.qsp_type}") print(f"Pauli term: {potential.qsp_pauli}") print(f"e de Broglie wavelength = {e_deBroglie_lambda / a_ws:.4f} a_ws = {e_deBroglie_lambda:.6e} ", end="") print("[cm]" if potential.units == "cgs" else "[m]") print(f"ion de Broglie wavelength = {i_deBroglie_lambda / a_ws:.4f} a_ws = {i_deBroglie_lambda:.6e} ", end="") print("[cm]" if potential.units == "cgs" else "[m]") print(f"e-e screening length = {ee_scr_len / a_ws:.4f} a_ws = {ee_scr_len:.6e} ", end="") print("[cm]" if potential.units == "cgs" else "[m]") print(f"i-i screening length = {ii_scr_len / a_ws:.4f} a_ws = {ii_scr_len:.6e} ", end="") print("[cm]" if potential.units == "cgs" else "[m]") print(f"e-i screening length = {ei_scr_len / a_ws:.4f} a_ws = {ei_scr_len:.6e} ", end="") print("[cm]" if potential.units == "cgs" else "[m]") print(f"e-i coupling constant = {potential.coupling_constant:.4f}")