Source code for sarkas.utilities.maths

"""Module of mathematical functions."""

import scipy.signal as scp_signal
from numba import njit
from numpy import arange, array, exp, inf, ndarray, pi, sqrt, trapz, zeros_like
from scipy.integrate import quad

TWOPI = 2.0 * pi


[docs]def correlationfunction(At, Bt): """ Calculate the correlation function between :math:`\\mathbf{A}(t)` and :math:`\\mathbf{B}(t)` using :func:`scipy.signal.correlate` .. math:: C_{AB}(\\tau) = \\sum_j^D \\sum_i^T A_j(t_i)B_j(t_i + \\tau) where :math:`D` is the number of dimensions and :math:`T` is the total length of the simulation. Parameters ---------- At : numpy.ndarray Observable to correlate. Bt : numpy.ndarray Observable to correlate. Returns ------- full_corr : numpy.ndarray Correlation function :math:`C_{AB}(\\tau)` Examples -------- >>> import numpy as np >>> t = np.linspace( 0.0, 6.0 * np.pi, 3000) >>> w0 = 0.5 >>> At = np.cos(w0 * t) >>> Bt = np.sin(w0 * t) >>> corr_t = correlationfunction(At, Bt) """ no_steps = At.size # Calculate the full correlation function. full_corr = scp_signal.correlate(At, Bt, mode="full") # Normalization of the full correlation function, Similar to norm_counter norm_corr = array([no_steps - ii for ii in range(no_steps)]) # Find the mid point of the array mid = full_corr.size // 2 # I want only the second half of the array, i.e. the positive lags only return full_corr[mid:] / norm_corr
@njit def fast_integral_loop(time, integrand): """Numba'd function to compute the following integral with a varying upper limit .. math:: I(\\tau) = \\int_0^{\\tau} f(t) dt It uses :func:`numpy.trapz`. This function is used in the calculation of the transport coefficients. Parameters ---------- time: numpy.ndarray Domain of integration integrand: numpy.ndarray Integrand. Returns ------- integral : numpy.ndarray Integral with increasing upper limit. Shape = (time.len()). """ integral = zeros_like(integrand) for it in range(1, len(time)): integral[it] = trapz(integrand[:it], x=time[:it]) return integral
[docs]def yukawa_green_function(k: float, alpha: float, kappa: float) -> float: """ Evaluate the Green's function of Coulomb/Yukawa potential. .. math:: G(k) = \\frac{4 \\pi }{\\kappa^2 + k^2} e^{- (k^2 + \\kappa^2)/4 \\alpha^2 } Parameters ---------- k : float, numpy.ndarray Range or value at which to calculate the function. alpha : float Ewald screening parameter. kappa : float Inverse screening length. Returns ------- _ : numpy.ndarray, float Green's function. See equation above Examples -------- >>> import numpy as np >>> k = np.linspace(0, 2, 100) >>> alpha = 0.2 >>> kappa = 0.5 >>> G_k = yukawa_green_function(k = k, alpha = alpha, kappa = kappa) """ return 4.0 * pi * exp(-(k**2 + kappa**2) / (2 * alpha) ** 2) / (kappa**2 + k**2)
[docs]def betamp(m: int, p: int, alpha: float, kappa: float) -> float: """ Calculate the integral of the Yukawa Green's function using :func:`scipy.integrate.quad`. .. math:: \\beta(p,m) = \\int_0^\\infty dk \\, G_k^2 k^{2 (p + m + 2)}. See eq.(37) in :cite:`Dharuman2017`. Parameters ---------- m : int :math:`m` index of the sum. p : int :math:`p` index of the sum alpha : float Ewald screening parameter :math:`\\alpha`. kappa : float Inverse screening length. Returns ------- intgrl : float The integral :math:`\\beta(m,p)`. """ exponent = 2 * (m + p + 2) intgrl, _ = quad(lambda x: yukawa_green_function(x, alpha, kappa) ** 2 * x ** (exponent), 0, inf) return intgrl
[docs]def force_error_approx_pppm(potential): r""" Calculates the force error, :math:`\Delta F_{\rm pm}`, for the PPPM algorithm using approximations given in :cite:`Dharuman2017`. The formula for :math:`\Delta F_{\rm pm}` can be found in :ref:`force_error`. Parameters ---------- potential: :class:`sarkas.potentials.core.Potential` Potential class with all the required information. Returns ------- tot_force_error: float Total force error given by the L2 norm of the PP and PM force errors. pppm_pm_err: float PM force error. pppm_pp_err: float PM force error. """ if potential.type in ["yukawa"]: kappa = potential.a_ws / potential.screening_length alpha = potential.pppm_alpha_ewald * potential.a_ws ha = potential.pppm_h_array[0] / potential.a_ws pppm_pm_err = force_error_approx_pm(kappa, potential.pppm_cao[0], ha, alpha) elif potential.type == "coulomb": alpha = potential.pppm_alpha_ewald * potential.a_ws ha = potential.pppm_h_array[0] / potential.a_ws pppm_pm_err = force_error_approx_pm(0.0, potential.pppm_cao[0], ha, alpha) rescaling_constant = sqrt(potential.total_num_density) * potential.a_ws**2 pppm_pp_err = force_error_analytic_pp( potential.type, potential.rc, potential.screening_length, potential.pppm_alpha_ewald, rescaling_constant ) pppm_pm_err *= sqrt(potential.total_num_density * potential.a_ws**3) force_error_tot = sqrt(pppm_pm_err**2 + pppm_pp_err**2) return force_error_tot, pppm_pm_err, pppm_pp_err
[docs]def force_error_approx_pm(kappa: float, p: int, h: float, alpha: float): r""" Calculates the PM part of the force error, :math:`\Delta F_{\rm pm}`, for a given value of the PPPM parameters. The formula for :math:`\Delta F_{\rm pm}` can be found in :ref:`force_error`. Parameters ---------- kappa : float Inverse screening length. p : int Charge assignment order. h : float Distance between two mesh points. Same for all directions. alpha : float Ewald screening parameter. Returns ------- pm_force_error: float PM force error. """ # Coefficients from :cite:`Deserno1998` if p == 1: Cmp = array([2 / 3]) elif p == 2: Cmp = array([2 / 45, 8 / 189]) elif p == 3: Cmp = array([4 / 495, 2 / 225, 8 / 1485]) elif p == 4: Cmp = array([2 / 4725, 16 / 10395, 5528 / 3869775, 32 / 42525]) elif p == 5: Cmp = array([4 / 93555, 2764 / 11609325, 8 / 25515, 7234 / 32531625, 350936 / 3206852775]) elif p == 6: Cmp = array( [ 2764 / 638512875, 16 / 467775, 7234 / 119282625, 1403744 / 25196700375, 1396888 / 40521009375, 2485856 / 152506344375, ] ) elif p == 7: Cmp = array( [ 8 / 18243225, 7234 / 1550674125, 701872 / 65511420975, 2793776 / 225759909375, 1242928 / 132172165125, 1890912728 / 352985880121875, 21053792 / 8533724574375, ] ) somma = 0.0 for m in arange(p): expp = 2 * (m + p) somma += Cmp[m] * (2 / (1 + expp)) * betamp(m, p, alpha, kappa) * (h / 2.0) ** expp # eq.(36) in :cite:`Dharuman2017` pm_force_error = sqrt(3.0 * somma) / (2.0 * pi) return pm_force_error
[docs]def force_error_analytic_pp( potential_type: str, cutoff_length: float, screening_length: float, alpha_ewald: float, rescaling_const: float ): """ Calculate the short-range part of the force error from the approximation formula given in :cite:`Dharuman2017`. Parameters ---------- potential_type: str Choice of potential. cutoff_length: float Short range cutoff. screening_length: float Screening length in case of screened potentials like yukawa. Pass 0 if coulomb alpha_ewald: float Ewald screening parameter. rescaling_const: float Constant by which to rescale the force error. \n In case of electric forces = :math:`Q^2/(4 \\pi \\epsilon_0) 1/a^2`. Returns ------- pppm_pp_err: float Short range force error in units of `rescaling_const`. """ if potential_type in ["yukawa"]: kappa = 1 / screening_length # PP force error calculation. Note that the equation was derived for a single component plasma. kappa_over_alpha = -0.25 * (kappa / alpha_ewald) ** 2 alpha_times_rcut = -((alpha_ewald * cutoff_length) ** 2) # eq.(30) from :cite:`Dharuman2017` pppm_pp_err = 2.0 * exp(kappa_over_alpha + alpha_times_rcut) / sqrt(cutoff_length) # Renormalize pppm_pp_err *= rescaling_const elif potential_type in ["coulomb", "qsp"]: # PP force error calculation. Note that the equation was derived for a single component plasma. alpha_times_rcut = -((alpha_ewald * cutoff_length) ** 2) pppm_pp_err = 2.0 * exp(alpha_times_rcut) / sqrt(cutoff_length) # Renormalize pppm_pp_err *= rescaling_const return pppm_pp_err
[docs]def force_error_analytic_lcl( potential_type: str, cutoff_length: float, potential_matrix: ndarray, rescaling_const: float ): """ Calculate the force error from its analytic formula. Parameters ---------- potential_type : str Type of potential used. potential_matrix : numpy.ndarray Potential parameters. cutoff_length : float Cutoff radius of the potential. rescaling_const: float Constant by which to rescale the force error. \n In case of electric forces = :math:`Q^2/(4 \\pi \\epsilon_0) 1/a^2`. Returns ------- force_error: float Force error in units of `rescaling_const`. Examples -------- Yukawa, QSP, EGS force errors are calculated the same way. >>> import numpy as np >>> # Look more about potential matrix >>> potential_matrix = np.zeros((2, 2, 2)) >>> kappa = 2.0 # in units of a_ws >>> potential_matrix[1,:,:] = kappa >>> rc = 6.0 # in units of a_ws >>> const = 1.0 # Rescaling const >>> force_error_analytic_lcl("yukawa", rc, potential_matrix, const) 2.1780665692875655e-05 Lennard jones potential >>> import numpy as np >>> potential_matrix = np.zeros( (5,2,2)) >>> sigma = 3.4e-10 >>> pot_const = 4.0 * 1.656e-21 # 4*epsilon >>> high_pow, low_pow = 12, 6 >>> potential_matrix[0] = pot_const >>> potential_matrix[1] = sigma >>> potential_matrix[2] = high_pow >>> potential_matrix[3] = low_pow >>> rc = 10 * sigma >>> force_error_analytic_lcl("lj", rc, potential_matrix, 1.0) 1.4590050212983888e-16 Moliere potential >>> import numpy as np >>> from scipy.constants import epsilon_0, pi, elementary_charge >>> charge = 4.0 * elementary_charge # = 4e [C] mks units >>> coul_const = 1.0 / (4.0 * pi * epsilon_0) >>> screening_charges = np.array([0.5, -0.5, 1.0]) >>> screening_lengths = np.array([5.99988000e-11, 1.47732309e-11, 1.47732309e-11]) # [m] >>> params_len = len(screening_lengths) >>> pot_mat = np.zeros((2 * params_len + 1, 2, 2)) >>> pot_mat[0] = coul_const * charge ** 2 >>> pot_mat[1: params_len + 1] = screening_charges.reshape((3, 1, 1)) >>> pot_mat[params_len + 1:] = 1. / screening_lengths.reshape((3, 1, 1)) >>> rc = 6.629e-10 >>> force_error_analytic_lcl("moliere", rc, pot_mat, 1.0) 2.1223648580087958e-14 """ if potential_type in ["yukawa", "egs", "qsp", "hs_yukawa"]: force_error = sqrt(TWOPI * potential_matrix[1, 0, 0]) * exp(-cutoff_length * potential_matrix[1, 0, 0]) elif potential_type == "moliere": # The first column of the potential matrix is 2*p + 1 long, where p is the # number of screening lengths. # The inverse of the screening_lengths is stored starting from p + 1. # Find p p = int((len(potential_matrix[:, 0, 0]) - 1) / 2) # Choose the smallest screening length ( i.e. max kappa) for force error calculation kappa = potential_matrix[p + 1 :, 0, 0].max() force_error = sqrt(TWOPI * kappa) * exp(-cutoff_length * kappa) elif potential_type == "lj": # choose the highest sigma in case of multispecies sigma = potential_matrix[1, :, :].max() high_pow = potential_matrix[2, 0, 0] exponent = 2 * high_pow - 1 force_error_tmp = high_pow**2 * sigma ** (2 * high_pow) / cutoff_length**exponent force_error_tmp /= exponent force_error = sqrt(force_error_tmp) # Renormalize force_error *= rescaling_const return force_error