Science&Enigneering

Ewald summation and PPPM

##- 2023. 3. 10. 11:02
728x90

Ewald summation is a computational method used in materials science and computational chemistry to calculate the electrostatic potential energy and forces between charged particles in a periodic system. It was developed by German physicist Paul Ewald in 1921.

 

In a periodic system, the long-range Coulombic interactions between charged particles can be difficult to calculate accurately due to the infinite range of the Coulombic potential. Ewald summation solves this problem by splitting the Coulombic potential into two parts: a short-range part that is treated using standard pairwise methods, and a long-range part that is treated using Fourier methods.

 

The Ewald summation method involves several steps. First, the system is replicated periodically to form a supercell. Next, the charge density of the system is represented by a set of Gaussian functions, which allows for efficient Fourier transformations. The Coulombic potential is then split into a real-space part and a reciprocal-space part, which are calculated separately. The real-space part is calculated using the Gaussian functions, while the reciprocal-space part is calculated using a Fourier transform. Finally, the two parts are combined to give the total Coulombic potential.

Ewald summation is a powerful method for accurately calculating long-range electrostatic interactions in periodic systems. It is widely used in computational materials science and chemistry to study a wide range of phenomena, including ionic solutions, crystals, and biomolecules.

 

The Ewald summation formula for the electrostatic potential energy between charged particles in a periodic system can be written as:

  • U = U(real) + U(recip) + U(self)

where U is the total electrostatic potential energy, U(real) is the real-space contribution, U(recip) is the reciprocal-space contribution, and U(self) is the self-interaction energy.

The real-space contribution is given by:

  • U(real) = (1/2) * sum_i sum_j, i != j q_i*q_j erf(κr_ij)/r_ij

where q_i and q_j are the charges on particles i and j, r_ij is the distance between particles i and j, and κ is a parameter that controls the decay rate of the Gaussian functions used to represent the charge density. The error function, erf, accounts for the finite range of the Gaussian functions.

The reciprocal-space contribution is given by:

  • U(recip) = (1/2V) * sum_G ≠ 0 |F(G)|^2 / G^2 * e^(-G^2 / 4κ^2)

where V is the volume of the supercell, G is a reciprocal lattice vector, F(G) is the Fourier transform of the charge density, and κ is the same decay parameter used in the real-space contribution.

The self-interaction energy is given by:

  • U(self) = - (κ/√π) * sum_i q_i^2

This term accounts for the interaction of each charged particle with its own Gaussian charge density.

 

import numpy as np
from scipy.special import erfc

def ewald_summation(coords, charges, box_size, kappa):
    # Get the number of particles
    num_particles = len(coords)

    # Calculate the reciprocal lattice vectors
    recip_lattice = 2*np.pi*np.linalg.inv(box_size)

    # Calculate the real-space contributions
    energy_real = 0.0
    force_real = np.zeros((num_particles, 3))
    for i in range(num_particles):
        for j in range(i+1, num_particles):
            dr = coords[i] - coords[j]
            dr -= np.round(dr / box_size) * box_size
            r = np.linalg.norm(dr)
            energy_real += charges[i] * charges[j] * erfc(kappa * r) / r
            force_real[i] -= charges[i] * charges[j] * (dr * erfc(kappa * r) / r**3 + 2*kappa/np.sqrt(np.pi) * np.exp(-kappa**2 * r**2) * dr / r)
            force_real[j] += charges[i] * charges[j] * (dr * erfc(kappa * r) / r**3 + 2*kappa/np.sqrt(np.pi) * np.exp(-kappa**2 * r**2) * dr / r)

    # Calculate the reciprocal-space contributions
    energy_recip = 0.0
    force_recip = np.zeros((num_particles, 3))
    for h in range(-4, 5):
        for k in range(-4, 5):
            for l in range(-4, 5):
                if h == k == l == 0:
                    continue
                G = h*recip_lattice[0] + k*recip_lattice[1] + l*recip_lattice[2]
                G_norm = np.linalg.norm(G)
                factor = 4*np.pi/(G_norm**2 * box_size[0]*box_size[1]*box_size[2]) * np.exp(-G_norm**2 / (4*kappa**2))
                energy_recip += factor * np.abs(np.sum(charges * np.exp(-1j * np.dot(coords, G))))
                force_recip += factor * np.sum(charges[:, np.newaxis] * np.exp(-1j * np.dot(coords, G))[:, np.newaxis] * (1j * G - np.dot(G, np.identity(3)) * np.dot(coords.T, np.exp(-1j * np.dot(coords, G)))[:, np.newaxis]), axis=0)

    # Calculate the self-interaction energy and forces
    energy_self = -kappa/np.sqrt(np.pi) * np.sum(charges**2)
    force_self = -2*kappa/np.sqrt(np.pi) * charges

    # Calculate the total energy and forces
    energy_total = energy_real + energy_recip + energy_self
    force_total = force_real + force_recip + force_self

    return energy_total, force_total

 

 

Particle-particle particle-mesh (PPPM) is a computational method used to calculate electrostatic interactions between charged particles in a periodic system. It is similar to Ewald summation, but instead of using Gaussian functions to represent the charge density, it uses a grid-based approach called the particle mesh method.

In the PPPM method, the simulation box is divided into a uniform grid, and the charges of the particles are assigned to the grid points. The electrostatic potential at each grid point is calculated using the Fast Fourier Transform (FFT) algorithm. The potential is then interpolated to the particle positions using a tri-linear interpolation scheme, and the forces on the particles are calculated.

 

The PPPM method has several advantages over other electrostatic calculation methods. It is computationally efficient and scales well with the number of particles, making it suitable for large-scale simulations. It also has a high accuracy and can handle systems with periodic boundary conditions.

 

One potential disadvantage of the PPPM method is the loss of accuracy due to the finite size of the grid. This can be mitigated by using a larger grid, but this increases the computational cost. Another disadvantage is that it is not suitable for systems with high charge density or high dielectric constant, as these can cause numerical instabilities in the calculations.

 

The PPPM method can be mathematically represented by the following equations:

 

Charge assignment:

The charge of each particle i is assigned to the grid point closest to its position, using the following equation:

  • ρ(r) = q_i * δ(r - r_i)

where ρ(r) is the charge density at position r, q_i is the charge of particle i, and δ(r - r_i) is the Dirac delta function.

 

Mesh Fourier transform:

The charge density on the grid is Fourier transformed using the FFT algorithm, giving the Fourier coefficients:

  • ρ̃(k) = ∫ ρ(r) e^(-ik.r) dr

where k is the wavevector and ρ̃(k) is the Fourier coefficient at wavevector k.

 

Green's function:

The electrostatic potential at each grid point is calculated using the inverse Fourier transform of the screened Coulomb potential, which is given by the Green's function:

  • G(r) = 1 / (4πε₀r) * e^(-κr)

where ε₀ is the vacuum permittivity, κ is the screening parameter, and r is the distance between two points.

 

Mesh potential:

The mesh potential φ_m(k) at wavevector k is obtained by multiplying the Fourier coefficients ρ̃(k) with the Fourier transform of the Green's function:

  • φ_m(k) = G(k) * ρ̃(k)

Interpolation:

The potential at each particle position is calculated by interpolating the mesh potential φ_m(k) to the particle position using a tri-linear interpolation scheme. The potential energy of each particle is then calculated as:

  • U_i = q_i * φ(r_i)

where φ(r_i) is the interpolated potential at the position of particle i.

 

Force calculation:

The force on each particle is calculated by taking the negative gradient of the potential energy:

  • F_i = -∇U_i

where ∇ is the gradient operator.

 

import numpy as np
from scipy.fftpack import fftn, ifftn

def pppm_summation(coords, charges, box_size, kappa, grid_size):
    # Get the number of particles
    num_particles = len(coords)

    # Calculate the mesh spacing and the mesh size
    mesh_spacing = box_size / grid_size
    mesh_size = np.ceil(box_size / mesh_spacing).astype(int)

    # Assign charges to the mesh
    mesh_charges = np.zeros(mesh_size)
    mesh_coords = np.floor(coords / mesh_spacing).astype(int)
    for i in range(num_particles):
        mesh_charges[tuple(mesh_coords[i])] += charges[i]

    # Perform the mesh Fourier transform
    mesh_fft = fftn(mesh_charges)

    # Calculate the Green's function
    x = np.arange(-mesh_size[0]//2, mesh_size[0]//2) * mesh_spacing[0]
    y = np.arange(-mesh_size[1]//2, mesh_size[1]//2) * mesh_spacing[1]
    z = np.arange(-mesh_size[2]//2, mesh_size[2]//2) * mesh_spacing[2]
    xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
    r = np.sqrt(xx**2 + yy**2 + zz**2)
    green_func = 1/(4*np.pi*8.854187817620389e-12*r) * np.exp(-kappa*r)
    green_func[np.isnan(green_func)] = 0

    # Calculate the mesh potential
    mesh_potential = np.real(ifftn(mesh_fft * np.fft.fftn(green_func)))

    # Interpolate the potential to the particle positions
    potential = np.zeros(num_particles)
    for i in range(num_particles):
        ix, iy, iz = mesh_coords[i]
        fx, fy, fz = coords[i] / mesh_spacing - mesh_coords[i]
        potential[i] = (1-fx) * (1-fy) * (1-fz) * mesh_potential[ix, iy, iz] + \
                       fx * (1-fy) * (1-fz) * mesh_potential[ix+1, iy, iz] + \
                       (1-fx) * fy * (1-fz) * mesh_potential[ix, iy+1, iz] + \
                       fx * fy * (1-fz) * mesh_potential[ix+1, iy+1, iz] + \
                       (1-fx) * (1-fy) * fz * mesh_potential[ix, iy, iz+1] + \
                       fx * (1-fy) * fz * mesh_potential[ix+1, iy, iz+1] + \
                       (1-fx) * fy * fz * mesh_potential[ix, iy+1, iz+1] + \
                       fx * fy * fz * mesh_potential[ix+1, iy+1, iz+1]

    # Calculate the force on each particle
    force = np.zeros_like(coords)
    for i in range(num_particles):
        dr = coords - coords[i]
        dr -= np.round(dr / box_size) * box_size
        r = np.linalg.norm(dr, axis=1)
        force[i] = np.sum(charges * (dr.T * erfc(kappa * r) / r**3 + 2*kappa/np.sqrt(np.sqrt(np.pi) * np.exp(-kappa2 * r2) * dr.T / r), axis=1)
    # Calculate the total potential energy
    energy = 0.5 * np.sum(charges * potential)

    return energy, force

ChatGPT

300x250