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.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_pp, TWOPI
[docs]@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_r : float
Potential.
f_r : 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 * (1.0 / r + C) # 1/r derivative
# Pauli Term
u_pauli = D * log(1.0 - 0.5 * exp(-F * r2))
f_pauli = -r * D * F / (exp(F * r2) - 0.5)
u_r = u_ewald + u_diff + u_pauli
f_r = f_ewald + f_diff + f_pauli
return u_r, f_r
[docs]@jit(UniTuple(float64, 2)(float64, float64[:]), nopython=True)
def pauli_force(r, pot_matrix):
"""
Calculate Pauli term of the QSP potential
Parameters
----------
r : 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_r : float
Pauli Potential.
f_r : float
Pauli Force between two particles.
"""
D = pot_matrix[2]
F = pot_matrix[3]
r2 = r * r
# Pauli Term
u_r = D * log(1.0 - 0.5 * exp(-F * r2))
f_r = -D * (2.0 * r * F * exp(-F * r2)) / (1.0 - 0.5 * exp(-F * r2))
return u_r, f_r
[docs]@jit(UniTuple(float64, 2)(float64, float64[:]), nopython=True)
def hansen_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
[docs]@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_r : 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
# potential
u_r_diff = A * C * sqrt(pi) * erfc(C * r / sqrt(pi))
u_r_diff_1 = -A * exp(-C2 * r2 / pi) / r
# Force
dvdr_diff = A * C * sqrt(pi) * (2.0 * C2 * exp(-C2 * r2 / pi) / pi) # erfc derivative
dvdr_diff_1 = u_r_diff_1 * (1.0 / r + 2.0 * C2 * r / pi) # exp(r)/r derivative
# Pauli Term
U_pauli, f_pauli = pauli_force(r, pot_matrix)
u_r = U_ewald + u_r_diff + u_r_diff_1 + U_pauli
force = f_ewald + dvdr_diff + dvdr_diff_1 + f_pauli
return u_r, force
[docs]def deutsch_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.
"""
A = pot_matrix[0] # qi*qj/4*pi*eps0
C = pot_matrix[1] # 2pi/deBroglie
D = pot_matrix[2] # e-e Pauli term factor
F = pot_matrix[3] # e-e Pauli term exponent term
r2 = r * r
r3 = r2 * r
# Pauli term. Note that D = 0 if Pauli is false
u_r_pauli = D * log(1.0 - 0.5 * exp(-F * r2))
dvdr_pauli = r * F / (exp(F * r2) - 0.5)
d2v_dr2_pauli = -2.0 * F * (exp(F * r2) * (4 * F * r2 - 2) + 1) / (2.0 * exp(F * r2) - 1.0) ** 2
# Diffraction potential and force term
u_r_diff = -A * exp(-C * r) / r
dvdr_diff = -u_r_diff * (1.0 / r + C) # 1/r derivative
d2v_dr2_diff = u_r_diff / r2 + dvdr_diff * (1.0 / r + C)
# Coulomb part
u_r_coul = A / r
dvdr_coul = -A / r2
d2v_dr2_coul = 2.0 * A / r3
u_r = u_r_coul + u_r_diff + u_r_pauli
dv_dr = dvdr_coul + dvdr_diff + dvdr_pauli
d2v_dr2 = d2v_dr2_coul + d2v_dr2_diff + d2v_dr2_pauli
return u_r, dv_dr, d2v_dr2
[docs]def hansen_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.
"""
A = pot_matrix[0] # qi*qj/4*pi*eps0
C = pot_matrix[1] # 2pi/deBroglie
D = pot_matrix[2] # e-e Pauli term factor
F = pot_matrix[3] # e-e Pauli term exponent term
r2 = r * r
r3 = r2 * r
# Pauli potential and force terms. Note that D = 0 if Pauli is false
u_r_pauli = D * exp(-F * r2)
dvdr_pauli = 2.0 * F * r * u_r_pauli
d2v_dr2_pauli = 2.0 * F * u_r_pauli + dvdr_pauli * 2.0 * F * r
# Diffraction potential and force term
u_r_diff = -A * exp(-C * r) / r
dvdr_diff = -u_r_diff * (1.0 / r + C) # 1/r derivative
d2v_dr2_diff = u_r_diff / r2 + dvdr_diff * (1.0 / r + C)
# Coulomb part
u_r_coul = A / r
dvdr_coul = -A / r2
d2v_dr2_coul = 2.0 * A / r3
u_r = u_r_coul + u_r_diff + u_r_pauli
dv_dr = dvdr_coul + dvdr_diff + dvdr_pauli
d2v_dr2 = d2v_dr2_coul + d2v_dr2_diff + d2v_dr2_pauli
return u_r, dv_dr, d2v_dr2
[docs]def kelbg_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.
"""
A = pot_matrix[0] # qi*qj/4*pi*eps0
C = pot_matrix[1] # 2pi/deBroglie
D = pot_matrix[2] # e-e Pauli term factor
F = pot_matrix[3] # e-e Pauli term exponent term
r2 = r * r
r3 = r2 * r
# Pauli term. Note that D = 0 if Pauli is false
u_r_pauli = D * log(1.0 - 0.5 * exp(-F * r2))
dvdr_pauli = r * F / (exp(F * r2) - 0.5)
d2v_dr2_pauli = -2.0 * F * (exp(F * r2) * (4 * F * r2 - 2) + 1) / (2.0 * exp(F * r2) - 1.0) ** 2
# Diffraction potential and force term
C2 = C * C
# potential
u_r_diff = A * C * sqrt(pi) * r * erfc(C * r / sqrt(pi))
u_r_diff_1 = -A * exp(-C2 * r2 / pi) / r
# Force
dvdr_diff = -2.0 * A * C2 * exp(-C2 * r2 / pi) / pi # erfc derivative
dvdr_diff_1 = -u_r_diff_1 * (1.0 / r + 2.0 * C2 * r / pi) # exp(r)/r derivative
#
d2v_dr2_diff = dvdr_diff * (-2.0 * C2 * r / pi)
d2v_dr2_diff_1 = u_r_diff_1 * (1.0 / r2 - 2.0 * C2 / pi) + (1.0 / r + 2.0 * C2 * r / pi) * dvdr_diff_1
u_r_diff += u_r_diff_1
dvdr_diff += dvdr_diff_1
d2v_dr2_diff += d2v_dr2_diff_1
# Coulomb part
u_r_coul = A / r
dvdr_coul = -A / r2
d2v_dr2_coul = 2.0 * A / r3
u_r = u_r_coul + u_r_diff + u_r_pauli
dv_dr = dvdr_coul + dvdr_diff + dvdr_pauli
d2v_dr2 = d2v_dr2_coul + d2v_dr2_diff + d2v_dr2_pauli
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.
"""
ii_scr_len = 1.0 / potential.matrix[1, 1, 1]
ei_scr_len = 1.0 / potential.matrix[0, 1, 1]
ee_scr_len = 1.0 / potential.matrix[0, 0, 1]
e_deBroglie_lambda = sqrt(2.0) * pi / potential.matrix[0, 0, 1]
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"e-e screening kappa = {potential.matrix[0, 0, 1] * a_ws:.4e}")
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"i-i screening kappa = {potential.matrix[1, 1, 1] * a_ws:.4e}")
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}")
print(f"e-i screening kappa = {potential.matrix[0, 1, 1] * a_ws:.4e}")
[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,
List of species data (:class:`sarkas.plasma.Species`).
"""
# 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((potential.num_species, potential.num_species, 6))
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))
# Pauli term only for e-e interaction
if sp1.name == sp2.name: # e-e
if potential.qsp_type == "hansen":
potential.matrix[i, j, 2] = log_2 * potential.kB * sp1.temperature
potential.matrix[i, j, 3] = four_pi / (log_2 * lambda_deB**2)
else:
potential.matrix[i, j, 2] = -potential.kB * sp1.temperature
potential.matrix[i, j, 3] = TWOPI / (lambda_deB**2)
else:
# Use ion temperature in i-i interactions only
lambda_deB = sqrt(deBroglie_const / (reduced * total_ion_temperature))
potential.matrix[i, j, 0] = q1 * q2 / potential.fourpie0
potential.matrix[i, j, 1] = 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
potential.potential_derivatives = deutsch_potential_derivatives
# Calculate the PP Force error from the e-e diffraction term only since it is the largest.
potential.pppm_pp_err = force_error_analytic_pp(
potential.type,
potential.rc,
0.0,
potential.pppm_alpha_ewald,
sqrt(3.0 * potential.a_ws / (4.0 * pi)),
)
elif potential.qsp_type == "hansen":
potential.force = hansen_force
potential.potential_derivatives = hansen_potential_derivatives
# Calculate the PP Force error from the e-e diffraction term only since it is the largest.
potential.pppm_pp_err = force_error_analytic_pp(
potential.type,
potential.rc,
0.0,
potential.pppm_alpha_ewald,
sqrt(3.0 * potential.a_ws / (4.0 * pi)),
)
elif potential.qsp_type == "kelbg":
potential.force = kelbg_force
potential.potential_derivatives = kelbg_potential_derivatives
# 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_pp(
potential.type, potential.rc, potential.matrix, sqrt(3.0 * potential.a_ws / (4.0 * pi))
)