Source code for sarkas.potentials.force_pm

"""
Module for handling the Particle-Mesh part of the force and potential calculation.
"""

from numba import jit
from numba.core.types import complex128, float64, int64, Tuple, UniTuple
from numpy import arange, array, exp, mod, pi, rint, sin, sqrt, zeros, zeros_like
from numpy.fft import fftshift, ifftshift
from pyfftw.builders import fftn, ifftn


[docs]@jit(float64[:](int64, float64), nopython=True) def assgnmnt_func(cao, x): """ Calculate the charge assignment function as given in Ref.:cite:`Deserno1998` Parameters ---------- cao : int Charge assignment order. x : float Distance to the closest mesh point. Returns ------ W : numpy.ndarray Charge Assignment Function. Each element is the fraction of the charge on each of the `cao` mesh points starting from the far left. """ W = zeros(cao) if cao == 1: W[0] = 1.0 elif cao == 2: W[0] = 0.5 * (1.0 - 2.0 * x) W[1] = 0.5 * (1.0 + 2.0 * x) elif cao == 3: W[0] = (1.0 - 4.0 * x + 4.0 * x**2) / 8.0 W[1] = (3.0 - 4.0 * x**2) / 4.0 W[2] = (1.0 + 4.0 * x + 4.0 * x**2) / 8.0 elif cao == 4: W[0] = (1.0 - 6.0 * x + 12.0 * x**2 - 8.0 * x**3) / 48.0 W[1] = (23.0 - 30.0 * x - 12.0 * x**2 + 24.0 * x**3) / 48.0 W[2] = (23.0 + 30.0 * x - 12.0 * x**2 - 24.0 * x**3) / 48.0 W[3] = (1.0 + 6.0 * x + 12.0 * x**2 + 8.0 * x**3) / 48.0 elif cao == 5: W[0] = (1.0 - 8.0 * x + 24.0 * x**2 - 32.0 * x**3 + 16.0 * x**4) / 384.0 W[1] = (19.0 - 44.0 * x + 24.0 * x**2 + 16.0 * x**3 - 16.0 * x**4) / 96.0 W[2] = (115.0 - 120.0 * x**2 + 48.0 * x**4) / 192.0 W[3] = (19.0 + 44.0 * x + 24.0 * x**2 - 16.0 * x**3 - 16.0 * x**4) / 96.0 W[4] = (1.0 + 8.0 * x + 24.0 * x**2 + 32.0 * x**3 + 16.0 * x**4) / 384.0 elif cao == 6: W[0] = (1.0 - 10.0 * x + 40.0 * x**2 - 80.0 * x**3 + 80.0 * x**4 - 32.0 * x**5) / 3840.0 W[1] = (237.0 - 750.0 * x + 840.0 * x**2 - 240.0 * x**3 - 240.0 * x**4 + 160.0 * x**5) / 3840.0 W[2] = (841.0 - 770.0 * x - 440.0 * x**2 + 560.0 * x**3 + 80.0 * x**4 - 160.0 * x**5) / 1920.0 W[3] = (841.0 + 770.0 * x - 440.0 * x**2 - 560.0 * x**3 + 80.0 * x**4 + 160.0 * x**5) / 1920.0 W[4] = (237.0 + 750.0 * x + 840.0 * x**2 + 240.0 * x**3 - 240.0 * x**4 - 160.0 * x**5) / 3840.0 W[5] = (1.0 + 10.0 * x + 40.0 * x**2 + 80.0 * x**3 + 80.0 * x**4 + 32.0 * x**5) / 3840.0 elif cao == 7: W[0] = ( 1.0 - 12.0 * x + 60.0 * x * 2 - 160.0 * x**3 + 240.0 * x**4 - 192.0 * x**5 + 64.0 * x**6 ) / 46080.0 W[1] = ( 361.0 - 1416.0 * x + 2220.0 * x**2 - 1600.0 * x**3 + 240.0 * x**4 + 384.0 * x**5 - 192.0 * x**6 ) / 23040.0 W[2] = ( 10543.0 - 17340.0 * x + 4740.0 * x**2 + 6880.0 * x**3 - 4080.0 * x**4 - 960.0 * x**5 + 960.0 * x**6 ) / 46080.0 W[3] = (5887.0 - 4620.0 * x**2 + 1680.0 * x**4 - 320.0 * x**6) / 11520.0 W[4] = ( 10543.0 + 17340.0 * x + 4740.0 * x**2 - 6880.0 * x**3 - 4080.0 * x**4 + 960.0 * x**5 + 960.0 * x**6 ) / 46080.0 W[5] = ( 361.0 + 1416.0 * x + 2220.0 * x**2 + 1600.0 * x**3 + 240.0 * x**4 - 384.0 * x**5 - 192.0 * x**6 ) / 23040.0 W[6] = ( 1.0 + 12.0 * x + 60.0 * x**2 + 160.0 * x**3 + 240.0 * x**4 + 192.0 * x**5 + 64.0 * x**6 ) / 46080.0 return W
[docs]@jit( float64[:, :]( float64[:, :, :], # E_x_r float64[:, :, :], # E_y_r float64[:, :, :], # E_z_r float64[:, :], # mesh_pos int64[:, :], # mesh_points float64[:], # charges / masses # float64[:], # masses int64[:], # cao int64[:], # mesh_sz float64[:], # mid int64[:], # pshift ), nopython=True, ) def calc_acc_pm(E_x_r, E_y_r, E_z_r, mesh_pos, mesh_points, q_over_m, cao, mesh_sz, mid, pshift): """ Calculates the long range part of particles' accelerations. Parameters ---------- E_x_r : numpy.ndarray Electric field along x-axis. E_y_r : numpy.ndarray Electric field along y-axis. E_z_r : numpy.ndarray Electric field along z-axis. mesh_pos: numpy.ndarray Particles' positions relative to the mesh. mesh_points: numpy.ndarray Particles' positions on the mesh. q_over_m : numpy.ndarray Particles' charges divided by their masses. cao : int Charge assignment order. mesh_sz: numpy.ndarray Mesh points per direction. mid: numpy.ndarray Midpoint flag for the three directions. pshift: numpy.ndarray Midpoint shift in each direction. Returns ------- acc : numpy.ndarray Acceleration from Electric Field. """ E_x_p = zeros_like(q_over_m) E_y_p = zeros_like(q_over_m) E_z_p = zeros_like(q_over_m) acc = zeros_like(mesh_pos) for ipart, q_m in enumerate(q_over_m): ix = mesh_points[0, ipart] x = mesh_pos[0, ipart] - (ix + mid[0]) iy = mesh_points[1, ipart] y = mesh_pos[1, ipart] - (iy + mid[1]) iz = mesh_points[2, ipart] z = mesh_pos[2, ipart] - (iz + mid[2]) wx = assgnmnt_func(cao[0], x) wy = assgnmnt_func(cao[1], y) wz = assgnmnt_func(cao[2], z) izn = iz - pshift[2] # min. index along z-axis for g in range(cao[2]): # # if izn < 0: # r_g = izn + mesh_sz[2] # elif izn > (mesh_sz[2] - 1): # r_g = izn - mesh_sz[2] # else: # r_g = izn r_g = izn + mesh_sz[2] * (izn < 0) - mesh_sz[2] * (izn > (mesh_sz[2] - 1)) iyn = iy - pshift[1] # min. index along y-axis for i in range(cao[1]): # if iyn < 0: # r_i = iyn + mesh_sz[1] # elif iyn > (mesh_sz[1] - 1): # r_i = iyn - mesh_sz[1] # else: # r_i = iyn r_i = iyn + mesh_sz[1] * (iyn < 0) - mesh_sz[1] * (iyn > (mesh_sz[1] - 1)) ixn = ix - pshift[0] # min. index along x-axis for j in range(cao[0]): r_j = ixn + mesh_sz[0] * (ixn < 0) - mesh_sz[0] * (ixn > (mesh_sz[0] - 1)) # if ixn < 0: # r_j = ixn + mesh_sz[0] # elif ixn > (mesh_sz[0] - 1): # r_j = ixn - mesh_sz[0] # else: # r_j = ixn # q_over_m = charges[ipart] / masses[ipart] E_x_p[ipart] += q_m * E_x_r[r_g, r_i, r_j] * wz[g] * wy[i] * wx[j] E_y_p[ipart] += q_m * E_y_r[r_g, r_i, r_j] * wz[g] * wy[i] * wx[j] E_z_p[ipart] += q_m * E_z_r[r_g, r_i, r_j] * wz[g] * wy[i] * wx[j] ixn += 1 iyn += 1 izn += 1 acc[0, :] = E_x_p acc[1, :] = E_y_p acc[2, :] = E_z_p return acc
[docs]@jit(float64[:, :, :](float64[:, :], int64[:, :], float64[:], int64[:], int64[:], float64[:], int64[:]), nopython=True) def calc_charge_dens(mesh_pos, mesh_points, charges, cao, mesh_sz, mid, pshift): """ Assigns Charges to Mesh Points. Parameters ---------- mesh_pos: numpy.ndarray Particles' positions relative to the mesh. mesh_points: numpy.ndarray Particles' positions on the mesh. charges: numpy.ndarray Particles' charges. cao: numpy.ndarray Charge assignment order. mesh_sz: numpy.ndarray Mesh points per direction. mid: numpy.ndarray Midpoint flag for the three directions. pshift: numpy.ndarray Midpoint shift in each direction. Returns ------- rho_r: numpy.ndarray Charge density distributed on mesh. """ rho_r = zeros((mesh_sz[2], mesh_sz[1], mesh_sz[0]), dtype=float64) # ix = x-coord of the (left) closest mesh point # (ix + 0.5)*h_array[0] = midpoint between the two mesh points closest to the particle for ipart in range(len(charges)): ix = mesh_points[0, ipart] delta_x = mesh_pos[0, ipart] - (ix + mid[0]) iy = mesh_points[1, ipart] delta_y = mesh_pos[1, ipart] - (iy + mid[1]) iz = mesh_points[2, ipart] delta_z = mesh_pos[2, ipart] - (iz + mid[2]) # delta_x, delta_y, delta_z = particle's distances to the closest (mid)-point of the mesh wx = assgnmnt_func(cao[0], delta_x) wy = assgnmnt_func(cao[1], delta_y) wz = assgnmnt_func(cao[2], delta_z) izn = iz - pshift[2] # min. index along z-axis for g in range(cao[2]): # if izn < 0: # r_g = izn + mesh_sz[2] # elif izn > (mesh_sz[2] - 1): # r_g = izn - mesh_sz[2] # else: # r_g = izn r_g = izn + mesh_sz[2] * (izn < 0) - mesh_sz[2] * (izn > (mesh_sz[2] - 1)) iyn = iy - pshift[1] # min. index along y-axis for i in range(cao[1]): r_i = iyn + mesh_sz[1] * (iyn < 0) - mesh_sz[1] * (iyn > (mesh_sz[1] - 1)) # if iyn < 0: # r_i = iyn + mesh_sz[1] # elif iyn > (mesh_sz[1] - 1): # r_i = iyn - mesh_sz[1] # else: # r_i = iyn ixn = ix - pshift[0] # min. index along x-axis for j in range(cao[0]): r_j = ixn + mesh_sz[0] * (ixn < 0) - mesh_sz[0] * (ixn > (mesh_sz[0] - 1)) # if ixn < 0: # r_j = ixn + mesh_sz[0] # elif ixn > (mesh_sz[0] - 1): # r_j = ixn - mesh_sz[0] # else: # r_j = ixn rho_r[r_g, r_i, r_j] += charges[ipart] * wz[g] * wy[i] * wx[j] ixn += 1 * (mesh_sz[0] > 1) # Do not increase the index if there is only 1 point mesh iyn += 1 * (mesh_sz[1] > 1) # Do not increase the index if there is only 1 point mesh izn += 1 * (mesh_sz[2] > 1) # Do not increase the index if there is only 1 point mesh. This is kinda redundant because if there is only # one point then also cao == 1. add a test for this! return rho_r
[docs]@jit( UniTuple(complex128[:, :, :], 3)(complex128[:, :, :], float64[:, :], float64[:, :], float64[:, :, :]), nopython=True, ) def calc_field(phi_k, kx_v, ky_v, kz_v): """ Numba'd function that calculates the Electric field in Fourier space. Parameters ---------- phi_k : numpy.ndarray, numba.complex128 3D array of the Potential. kx_v : numpy.ndarray, numba.float64 2D array containing the values of kx. ky_v : numpy.ndarray, numba.float64 2D array containing the values of ky. kz_v : numpy.ndarray, numba.float64 3D array containing the values of kz. Returns ------- E_kx : numpy.ndarray, numba.complex128 Electric Field along kx-axis. E_ky : numpy.ndarray, numba.complex128 Electric Field along ky-axis. E_kz : numpy.ndarray, numba.complex128 Electric Field along kz-axis. """ E_kx = -1j * kx_v * phi_k E_ky = -1j * ky_v * phi_k E_kz = -1j * kz_v * phi_k return E_kx, E_ky, E_kz
[docs]@jit(Tuple((float64[:, :], int64[:, :]))(float64[:, :], float64[:], int64[:]), nopython=True) def calc_mesh_coord(pos, h_array, cao): """ Calculate the particles positions with respect to the mesh and their closest point on the mesh. Parameters ---------- pos: numpy.ndarray Particles' positions. h_array: numpy.ndarray Width of the mesh cells. cao: numpy.ndarray Charge assignment order. Returns ------- mesh_pos: numpy.ndarray Particles' positions relative to the mesh, i.e. pos/h_array. mesh_points: numpy.ndarray Particles' positions on the mesh. """ # Avoid division by zero. if mesh_sz[i] == 0 then there is no mesh in that direction, h_array = 0 non_zero_hs = h_array.copy() non_zero_hs += 1.0 * (h_array == 0) # Calculate the particles' coordinates relative to the mesh mesh_pos = pos / non_zero_hs # Calculate the particles' closest grid points (if cao is odd) or closest mid points if cao is even mesh_points = rint(mesh_pos - 0.5 * (cao % 2 == 0)) return mesh_pos, mesh_points.astype(int64)
[docs]@jit( float64[:]( float64[:, :, :], # E_x_r float64[:, :], # mesh_pos int64[:, :], # mesh_points float64[:], # charges int64[:], # cao int64[:], # mesh_sz float64[:], # mid int64[:], # pshift ), nopython=True, ) def calc_pot_pm(phi_r, mesh_pos, mesh_points, charges, cao, mesh_sz, mid, pshift): """ Calculates the long range part of particles' accelerations. Parameters ---------- phi_r : numpy.ndarray Potential energy at mesh points. mesh_pos: numpy.ndarray Particles' positions relative to the mesh. mesh_points: numpy.ndarray Particles' positions on the mesh. charges : numpy.ndarray Particles' charges. cao : int Charge assignment order. mesh_sz: numpy.ndarray Mesh points per direction. mid: numpy.ndarray Midpoint flag for the three directions. pshift: numpy.ndarray Midpoint shift in each direction. Returns ------- pot_p : numpy.ndarray Potential energy of each particle. """ pot_p = zeros_like(charges) # potential energy for for ipart, q in enumerate(charges): ix = mesh_points[0, ipart] x = mesh_pos[0, ipart] - (ix + mid[0]) iy = mesh_points[1, ipart] y = mesh_pos[1, ipart] - (iy + mid[1]) iz = mesh_points[2, ipart] z = mesh_pos[2, ipart] - (iz + mid[2]) wx = assgnmnt_func(cao[0], x) wy = assgnmnt_func(cao[1], y) wz = assgnmnt_func(cao[2], z) izn = iz - pshift[2] # min. index along z-axis for g in range(cao[2]): # # if izn < 0: # r_g = izn + mesh_sz[2] # elif izn > (mesh_sz[2] - 1): # r_g = izn - mesh_sz[2] # else: # r_g = izn r_g = izn + mesh_sz[2] * (izn < 0) - mesh_sz[2] * (izn > (mesh_sz[2] - 1)) iyn = iy - pshift[1] # min. index along y-axis for i in range(cao[1]): # if iyn < 0: # r_i = iyn + mesh_sz[1] # elif iyn > (mesh_sz[1] - 1): # r_i = iyn - mesh_sz[1] # else: # r_i = iyn r_i = iyn + mesh_sz[1] * (iyn < 0) - mesh_sz[1] * (iyn > (mesh_sz[1] - 1)) ixn = ix - pshift[0] # min. index along x-axis for j in range(cao[0]): r_j = ixn + mesh_sz[0] * (ixn < 0) - mesh_sz[0] * (ixn > (mesh_sz[0] - 1)) # if ixn < 0: # r_j = ixn + mesh_sz[0] # elif ixn > (mesh_sz[0] - 1): # r_j = ixn - mesh_sz[0] # else: # r_j = ixn # q_over_m = charges[ipart] / masses[ipart] pot_p[ipart] += q * phi_r[r_g, r_i, r_j] * wz[g] * wy[i] * wx[j] ixn += 1 iyn += 1 izn += 1 return pot_p
[docs]@jit(UniTuple(float64[:, :], 3)(int64[:], int64[:], float64[:]), nopython=True) def create_k_aliases(aliases, mesh_sizes, non_zero_box_lengths): """Calculate the alias arrays of the reciprocal space arrays for anti-aliasing. Parameters ---------- aliases : numpy.ndarray, numba.int64 Number of aliases per dimension. mesh_sizes : numpy.ndarray, numba.int64 Number of mesh points in x,y,z. non_zero_box_lengths : numpy.ndarray, numba.float64 Length of simulation's box in each direction. Note that no element should be equal to 0.0. If the dimensionality of the problem is lower than 3, then use 1.0 as the box length for those dimensions. Example: 2D non_zero_box_lengths = [Lx, Ly, 1.0]. Returns ------- kx_M : numpy.ndarray Array of aliases for each kx value. Shape=( mesh_size[0], 2 * aliases[0] + 1) ky_M : numpy.ndarray Array of aliases for each ky value. Shape=( mesh_size[1], 2 * aliases[1] + 1) kz_M : numpy.ndarray Array of aliases for each kz value. Shape=( mesh_size[2], 2 * aliases[2] + 1) """ nz_mid = mesh_sizes[2] / 2 if mod(mesh_sizes[2], 2) == 0 else (mesh_sizes[2] - 1) / 2 ny_mid = mesh_sizes[1] / 2 if mod(mesh_sizes[1], 2) == 0 else (mesh_sizes[1] - 1) / 2 nx_mid = mesh_sizes[0] / 2 if mod(mesh_sizes[0], 2) == 0 else (mesh_sizes[0] - 1) / 2 two_pi = 2.0 * pi kx_M = zeros((mesh_sizes[0], 2 * aliases[0] + 1), dtype=float64) ky_M = zeros((mesh_sizes[1], 2 * aliases[1] + 1), dtype=float64) kz_M = zeros((mesh_sizes[2], 2 * aliases[2] + 1), dtype=float64) for nz in range(mesh_sizes[2]): nz_sh = nz - nz_mid for mz in range(-aliases[2], aliases[2] + 1): kz_M[nz, mz + aliases[2]] = two_pi * (nz_sh + mz * mesh_sizes[2]) / non_zero_box_lengths[2] for ny in range(mesh_sizes[1]): ny_sh = ny - ny_mid for my in range(-aliases[1], aliases[1] + 1): ky_M[ny, my + aliases[1]] = two_pi * (ny_sh + my * mesh_sizes[1]) / non_zero_box_lengths[1] for nx in range(mesh_sizes[0]): nx_sh = nx - nx_mid for mx in range(-aliases[0], aliases[0] + 1): kx_M[nx, mx + aliases[0]] = two_pi * (nx_sh + mx * mesh_sizes[0]) / non_zero_box_lengths[0] return kx_M, ky_M, kz_M
[docs]@jit(Tuple((float64[:, :], float64[:, :], float64[:, :, :]))(int64[:], float64[:]), nopython=True) def create_k_arrays(mesh_sizes, non_zero_box_lengths): """Calculate the reciprocal space arrays. Parameters ---------- non_zero_box_lengths : numpy.ndarray Length of simulation's box in each direction. Note that no element should be equal to 0.0. If the dimensionality of the problem is lower than 3, then use 1.0 as the box length for those dimensions. Example: 2D non_zero_box_lengths = [Lx, Ly, 1.0]. mesh_sizes : numpy.ndarray Number of mesh points in x,y,z. Returns ------- kx_v : numpy.ndarray Array of reciprocal space vectors along the x-axis ky_v : numpy.ndarray Array of reciprocal space vectors along the y-axis kz_v : numpy.ndarray Array of reciprocal space vectors along the z-axis """ nz_mid = mesh_sizes[2] / 2 if mod(mesh_sizes[2], 2) == 0 else (mesh_sizes[2] - 1) / 2 ny_mid = mesh_sizes[1] / 2 if mod(mesh_sizes[1], 2) == 0 else (mesh_sizes[1] - 1) / 2 nx_mid = mesh_sizes[0] / 2 if mod(mesh_sizes[0], 2) == 0 else (mesh_sizes[0] - 1) / 2 # nx_v = arange(mesh_sizes[0]).reshape((1, mesh_sizes[0])) # ny_v = arange(mesh_sizes[1]).reshape((mesh_sizes[1], 1)) # nz_v = arange(mesh_sizes[2]).reshape((mesh_sizes[2], 1, 1)) # Dev Note: # The above three lines where giving a problem with Numba in Windows only. # I replaced them with the ones below. I don't know why it was giving a problem. nx_v = zeros((1, mesh_sizes[0]), dtype=int64) nx_v[0, :] = arange(mesh_sizes[0]) ny_v = zeros((mesh_sizes[1], 1), dtype=int64) ny_v[:, 0] = arange(mesh_sizes[1]) nz_v = zeros((mesh_sizes[2], 1, 1), dtype=int64) nz_v[:, 0, 0] = arange(mesh_sizes[2]) two_pi = 2.0 * pi kx_v = two_pi * (nx_v - nx_mid) / non_zero_box_lengths[0] ky_v = two_pi * (ny_v - ny_mid) / non_zero_box_lengths[1] kz_v = two_pi * (nz_v - nz_mid) / non_zero_box_lengths[2] return kx_v, ky_v, kz_v
[docs]@jit( UniTuple(float64, 2)( float64, float64, float64, float64[:], float64[:], float64[:], float64[:], int64[:], float64, float64, float64 ), nopython=True, ) def sum_over_aliases(kx, ky, kz, kx_M, ky_M, kz_M, h_array, p, four_pi, alpha_sq, kappa_sq): """ Perform the sum over aliases in each direction. Parameters ---------- kx : float Value of the k_x wavenumber. ky : float Value of the k_y wavenumber. kz : float Value of the k_z wavenumber. kx_M : numpy.ndarray Array of aliases for each kx value. Shape=( 2 * aliases[0] + 1) ky_M : numpy.ndarray Array of aliases for each ky value. Shape=(2 * aliases[1] + 1) kz_M : numpy.ndarray Array of aliases for each kz value. Shape=(2 * aliases[2] + 1) h_array : numpy.ndarray Mesh spacings. p : numpy.ndarray Charge assignment order for each direction. i.e. cao_x, cao_y, cao_z four_pi: float Multiplier constant. :math:`4 \\pi` if cgs units or :math:`4 \\pi \\eplison_0` if mks units. alpha_sq: float Ewald parameter squared, :math:`\\alpha^2`. kappa_sq: float Screening parameter squared. It is equal to 0 (zero) in case of Coulomb interaction. Returns ------- U_G_k : float Product of the Green's function and the FFT of the B-splines squared. i.e. The numerator of eq.(31) in :cite:`Dharuman2017`. U_k_sq : float Sqared sum of the FFT of the B-spline. i.e. The denominator (without the :math:`|k_n|^2`:) in cite:`Dharuman2017`. """ U_k_sq = 0.0 U_G_k = 0.0 # Sum over the aliases for mz, kzm in enumerate(kz_M): kz_M_arg = 0.5 * kzm * h_array[2] U_kz_M = (sin(kz_M_arg) / kz_M_arg) ** p[2] if kz_M_arg != 0.0 else 1.0 for my, kym in enumerate(ky_M): ky_M_arg = 0.5 * kym * h_array[1] U_ky_M = (sin(ky_M_arg) / ky_M_arg) ** p[1] if ky_M_arg != 0.0 else 1.0 for mx, kxm in enumerate(kx_M): kx_M_arg = 0.5 * kxm * h_array[0] U_kx_M = (sin(kx_M_arg) / kx_M_arg) ** p[0] if kx_M_arg != 0.0 else 1.0 k_M_sq = kxm * kxm + kym * kym + kzm * kzm U_k_M = U_kx_M * U_ky_M * U_kz_M U_k_M_sq = U_k_M * U_k_M G_k_M = four_pi * exp(-0.25 * (kappa_sq + k_M_sq) / alpha_sq) / (kappa_sq + k_M_sq) k_dot_k_M = kx * kxm + ky * kym + kz * kzm U_G_k += U_k_M_sq * G_k_M * k_dot_k_M U_k_sq += U_k_M_sq return U_G_k, U_k_sq
[docs]@jit( Tuple((float64[:, :, :], float64[:, :], float64[:, :], float64[:, :, :], float64))( float64[:], float64[:], int64[:], int64[:], int64[:], float64[:] ), nopython=True, ) def force_optimized_green_function(box_lengths, h_array, mesh_sizes, aliases, p, constants): """ Numba'd function to calculate the Optimized Green Function given by eq.(22) of Ref.:cite:`Stern2008`. Parameters ---------- box_lengths : numpy.ndarray Length of simulation's box in each direction. h_array : numpy.ndarray Mesh spacings. mesh_sizes : numpy.ndarray Number of mesh points in x,y,z. aliases : numpy.ndarray Number of aliases in each direction. p : numpy.ndarray Array of charge assignment order (cao) for each dimension. constants : numpy.ndarray Screening parameter, Ewald parameter, :math:`4 \\pi \\eplison_0`. Returns ------- G_k : numpy.ndarray Optimal Green Function PM_err : float Error in the force calculation due to the optimized Green's function. eq.(28) of :cite:`Dharuman2017` . """ kappa = constants[0] Gew = constants[1] fourpie0 = constants[2] four_pi = 4.0 * pi if fourpie0 == 1.0 else 4.0 * pi / fourpie0 mask = box_lengths.nonzero() non_zero_box_lengths = array([1.0, 1.0, 1.0], dtype=float64) non_zero_box_lengths[mask] = box_lengths[mask].copy() kappa_sq = kappa * kappa Gew_sq = Gew * Gew G_k = zeros((mesh_sizes[2], mesh_sizes[1], mesh_sizes[0])) PM_err = 0.0 kx_v, ky_v, kz_v = create_k_arrays(mesh_sizes, non_zero_box_lengths) kx_M, ky_M, kz_M = create_k_aliases(aliases, mesh_sizes, non_zero_box_lengths) for nz, kz in enumerate(kz_v[:, 0, 0]): for ny, ky in enumerate(ky_v[:, 0]): for nx, kx in enumerate(kx_v[0, :]): k_sq = kx * kx + ky * ky + kz * kz if k_sq != 0.0: # old code # U_k_sq = 0.0 # U_G_k = 0.0 # Sum over the aliases # for mz in range(-aliases[2], aliases[2] + 1): # kz_M = two_pi * (nz_sh + mz * mesh_sizes[2]) / non_zero_box_lengths[2] # kz_M_arg = 0.5 * kz_M * h_array[2] # U_kz_M = (sin(kz_M_arg) / kz_M_arg) ** p[2] if kz_M_arg != 0.0 else 1.0 # # for my in range(-aliases[1], aliases[1] + 1): # ky_M = two_pi * (ny_sh + my * mesh_sizes[1]) / non_zero_box_lengths[1] # ky_M_arg = 0.5 * ky_M * h_array[1] # U_ky_M = (sin(ky_M_arg) / ky_M_arg) ** p[1] if ky_M_arg != 0.0 else 1.0 # # for mx in range(-aliases[0], aliases[0] + 1): # kx_M = two_pi * (nx_sh + mx * mesh_sizes[0]) / non_zero_box_lengths[0] # kx_M_arg = 0.5 * kx_M * h_array[0] # U_kx_M = (sin(kx_M_arg) / kx_M_arg) ** p[0] if kx_M_arg != 0.0 else 1.0 # # # print(mx, my, mz, kx_M, ky_M, kz_M) # k_M_sq = kx_M * kx_M + ky_M * ky_M + kz_M * kz_M # # U_k_M = U_kx_M * U_ky_M * U_kz_M # U_k_M_sq = U_k_M * U_k_M # # G_k_M = four_pi * exp(-0.25 * (kappa_sq + k_M_sq) / Gew_sq) / (kappa_sq + k_M_sq) # # k_dot_k_M = kx * kx_M + ky * ky_M + kz * kz_M # # U_G_k += U_k_M_sq * G_k_M * k_dot_k_M # U_k_sq += U_k_M_sq # eq.(22) of Ref.[Dharuman2017]_ U_G_k, U_k_sq = sum_over_aliases( kx, ky, kz, kx_M[nx], ky_M[ny], kz_M[nz], h_array, p, four_pi, Gew_sq, kappa_sq ) G_k[nz, ny, nx] = U_G_k / ((U_k_sq**2) * k_sq) Gk_hat = four_pi * exp(-0.25 * (kappa_sq + k_sq) / Gew_sq) / (kappa_sq + k_sq) # eq.(28) of Ref.[Dharuman2017]_ PM_err += Gk_hat * Gk_hat * k_sq - U_G_k**2 / ((U_k_sq**2) * k_sq) PM_err = sqrt(abs(PM_err)) / non_zero_box_lengths.prod() ** (1.0 / len(box_lengths.nonzero()[0])) return G_k, kx_v, ky_v, kz_v, PM_err
[docs]@jit(Tuple((float64[:], int64[:]))(int64[:]), nopython=True) def mesh_point_shift(cao): """ Calculate the required shift based on the parity of the charge assignment orders. Parameters ---------- cao: numpy.ndarray Charge assignment order per direction. Returns ------- mid: numpy.ndarray Midpoint shift if cao is even, otherwise no shift. pshift: numpy.ndarray Shift to the closest mesh point. """ pshift = zeros(len(cao), dtype=int64) mid = zeros(len(cao), dtype=float64) # Mid point calculation for ic, p in enumerate(cao): # Choose the midpoint between the two closest mesh point to the particle's position if cao is even otherwise # take the closest mesh-point mid[ic] = 0.5 * (p % 2 == 0) pshift[ic] = int(0.5 * p - 1 * (p % 2 == 0)) return mid, pshift
# FFTW version
[docs]@jit( Tuple((float64[:], float64[:, :]))( float64[:, :], # pos float64[:], # charges float64[:], # masses int64[:], # mesh_sizes float64[:], # mesh_spacings float64, # mesh_volume float64, # box_volume float64[:, :, :], # G_k float64[:, :], # kx_v float64[:, :], # ky_v float64[:, :, :], # kz_v int64[:], ), nopython=False, forceobj=True, # This is needed so that it doesn't throw an error nor warning ) def update(pos, charges, masses, mesh_sizes, mesh_spacings, mesh_volume, box_volume, G_k, kx_v, ky_v, kz_v, cao): """ Calculate the long range part of particles' accelerations. Parameters ---------- pos: numpy.ndarray Particles' positions. charges: numpy.ndarray Particles' charges. masses: numpy.ndarray Particles' masses. mesh_sizes: numpy.ndarray Mesh points per direction. mesh_spacings: numpy.ndarray Width of the mesh cells. mesh_volume: float Non-zero volume of the mesh. box_volume: float Non-zero box volume (area in 2D). G_k : numpy.ndarray Optimized Green's function. kx_v : numpy.ndarray Array of kx values. ky_v : numpy.ndarray Array of ky values. kz_v : numpy.ndarray Array of kz values. cao : numpy.ndarray Charge order parameter. Returns ------- pot_particle : numpy.ndarray Long range part of the potential for each particle. acc_f : numpy.ndarray Long range part of particles' accelerations. """ # Mesh spacings = h_x, h_y, h_z # mesh_spacings = box_lengths / mesh_sizes # Calculate the necessary shifts mid, pshift = mesh_point_shift(cao) # Calculate particles' position relative to the mesh points mesh_pos, mesh_points = calc_mesh_coord(pos, mesh_spacings, cao) # Calculate charge density on mesh rho_r = calc_charge_dens(mesh_pos, mesh_points, charges, cao, mesh_sizes, mid, pshift) # Prepare for fft fftw_n = fftn(rho_r) # Calculate fft rho_k_fft = fftw_n() # Shift the DC value at the center of the ndarray rho_k = fftshift(rho_k_fft) # Potential from Poisson eq. phi_k = G_k * rho_k # Charge density rho_k_real = rho_k.real rho_k_imag = rho_k.imag rho_k_sq = rho_k_real * rho_k_real + rho_k_imag * rho_k_imag # Calculate the Electric field's component on the mesh E_kx, E_ky, E_kz = calc_field(phi_k, kx_v, ky_v, kz_v) # Prepare for fft. Shift the DC value back to its original position that is [0, 0, 0] E_kx_unsh = ifftshift(E_kx) E_ky_unsh = ifftshift(E_ky) E_kz_unsh = ifftshift(E_kz) # Prepare and compute IFFT ifftw_n = ifftn(E_kx_unsh) E_x = ifftw_n() ifftw_n = ifftn(E_ky_unsh) E_y = ifftw_n() ifftw_n = ifftn(E_kz_unsh) E_z = ifftw_n() # FFT normalization E_x_r = E_x.real / mesh_volume E_y_r = E_y.real / mesh_volume E_z_r = E_z.real / mesh_volume q_over_m = charges / masses acc_f = calc_acc_pm(E_x_r, E_y_r, E_z_r, mesh_pos, mesh_points, q_over_m, cao, mesh_sizes, mid, pshift) # Calculate the potential energy of each particle phi_k_shift = ifftshift(phi_k) ifftw_n = ifftn(phi_k_shift) phi_r_cmplx = ifftw_n() phi_r = phi_r_cmplx.real / mesh_volume # Potential of each particle pot_particle = calc_pot_pm(phi_r, mesh_pos, mesh_points, charges, cao, mesh_sizes, mid, pshift) # The sum of this is equal to U_f, i.e. Long range part of the potential. It was tested. # I leave it here for future testing # U_f = 0.5 * (rho_k_sq * G_k).sum() / box_volume return pot_particle, acc_f