Bohrium
robot
新建

空间站广场

论文
Notebooks
比赛
课程
Apps
我的主页
我的Notebooks
我的论文库
我的足迹

我的工作空间

任务
节点
文件
数据集
镜像
项目
数据库
公开
Particle mesh Ewald (PME) 基本原理及其C语言实现
计算化学
分子动力学
从算法原理到代码实现
计算化学分子动力学从算法原理到代码实现
王浩宇
TaipingHu
发布于 2023-12-12
推荐镜像 :Basic Image:ubuntu22.04-py3.10
推荐机型 :c2_m4_cpu
赞 1
1
前言
Ewald 求和
基本思想
物理图像
实空间
倒易空间
本章小结
PME 求和
基本思想
Lagrange 插值
物理图像
本章小结
PME 求和算法实现
简介
示例代码
示例输出
参考文献

本NB初衷:

在 AISI 电池团队的一个项目中,需要求周期性体系的静电相互作用能,最初是采用原始的 Ewald 进行实现,后期为了提高效率,决定采用 PME 替代 Ewald,因此有了本NB。

代码
文本

前言

在分子动力学(molecular dynamics, MD)模拟中,在为模拟系统选取边界条件时经常会采用周期性边界条件(periodic boundary conditions, PBC)以降低边界效应对系统中粒子的影响。考虑真空中单元系统内的 个点电荷(point charge),坐标分别为 ,且满足条件 。使用周期性边界条件的无穷大系综的静电相互作用能(electrostatic energy)的总和为 其中 为真空中介电常数(vacuum permittivity),),)为单元系统的边界矢量。引入 记号表示 ,当且仅当 ,即排除单元系统中点电荷与自身的静电相互作用。方程 指出,在周期性的无穷大系综中,直接计算带电粒子的静电相互作用能是困难的,这是因为静电相互作用能随 衰减,并且能量的无穷项求和是条件收敛的。

德国物理学家 Ewald 为了计算晶体内静电相互作用能,于1921年提出 Ewald 求和算法1。包含无穷多重复单元的周期性系综和包含无穷多相同晶胞的晶体在计算性质上是高度相似的,因此在分子动力学模拟中计算静电相互作用也可以采用相同的算法。Ewald 求和的时间复杂度为 ,而为了处理大体量系统中的静电相互作用,Darden、York 和 Pedersen 于1993年基于经典 Ewald 求和提出了 particle mesh Ewald(PME)求和,将 Ewald 求和中最耗时的一项的时间复杂度缩减为 2。1995年,Essmann、Perera 和 Berkowitz 等人通过改良 PME 求和的算法提高了计算精度,提出 smooth particle mesh Ewald(SPME)求和算法3

本文将简要介绍 Ewald 求和和 PME 求和的基本原理,并运用 C 语言实现 PME 求和对3D周期性系综的静电相互作用能的计算。

代码
文本

Ewald 求和

基本思想

Ewald 求和的基本思想是引入一个快速衰减函数 ,将 阶的长程库仑相互作用重写为两项之和: 其中,第一项 是快速衰减的,且其求和是收敛的。第二项 是慢变函数,可以通过 Fourier 变换将其变换到倒易空间(reciprocal space)后对波矢(wave vector) 求和得到。Ewald 求和选取了余误差函数(complementary error function)作为衰减函数,其表达式为:

alt

图 1. 通过选择余误差函数作为衰减函数,将长程静电相互作用(黑色实线)分离为短程项(蓝色实线)和慢衰减项(红色虚线)。

代码
文本

物理图像

下面将探讨方程 中对点电荷间长程静电相互作用进行的分离的物理意义。点电荷在空间中的电荷密度分布可以通过 Dirac delta 函数的集合来描述。特别地,点电荷 在位置 处的电荷密度为 其中 Dirac delta 函数满足 ,且 。 假想以点电荷的位置为中心在空间中引入一个总电荷量相同、电性相反、球对称(spherical symmetry)且平滑变化的电荷分布。这种云状的电荷分布有效地屏蔽了点电荷的长程静电相互作用(可以设想在无穷远处,电性相反的云状电荷分布近似等同于点电荷,完全抵消点电荷的静电作用),仅保留短程静电相互作用。同时为了补偿引入的电荷分布,还需引入与云状电荷分布形状相同,且与点电荷电性相同另一种电荷分布。在 Ewald 求和中选取 Gauss 分布描述云状电荷分布: 其中, 是 Gauss 分布的标准差,指示其分布宽度。值得注意的是,delta 函数也可以视为 Gauss 分布对 的极限,即 规定与对应点电荷电性相同且服从 Gauss 分布的电荷称为 Gauss 电荷,与 Gauss 电荷形状相同且电性相反的电荷称为反 Gauss 电荷,点电荷与反 Gauss 电荷合并称为屏蔽电荷(screened charge)。

alt

图2. 一组点电荷可以被认为是由 (a) 一组屏蔽电荷与 (b) 一组 Gauss 电荷组合而成。颜色用于指示电荷的符号。

引入 Poisson 方程: 其中 处的静电势(electrostatic potential)。可以解得点电荷 处产生的静电势为 Gauss 电荷在 处产生的静电势为 \phi_{i}^{\mathrm{G}}(\mathbf{r}) = \frac{1}{4\pi\varepsilon_{0}} \frac{q_i}{\left|\mathbf{r}-\mathbf{r}_i\right|} \operatorname{erf}\left(\frac{\left|\mathbf{r}-\mathbf{r}_i\right|}{\sqrt{2}\sigma}\right) \tag{10}\ 下面将分别在实空间(real space)中对屏蔽电荷、在倒易空间中对 Gauss 电荷的静电相互作用能进行求和,最后将两项相加即可得到系综的静电相互作用能总和。

代码
文本

实空间

点电荷 与其对应的反 Gauss 电荷组成的屏蔽电荷在 处产生的静电势为 点电荷 与系综中除自身的屏蔽电荷外所有屏蔽电荷的静电相互作用能为 单元系统在实空间的静电相互作用能为 此外,还需单独计算点电荷与其对应的反 Gauss 电荷的静电相互作用能。Gauss 电荷在 处产生的静电势为 自相互作用能为

代码
文本

倒易空间

系综中 Gauss 电荷在空间中的密度分布为 可知 Gauss 电荷在空间中周期性分布,对其进行 Fourier 变换: 其中倒易格矢量(reciprocal lattice vector)),,倒易矢量(reciprocal vector))由 (Kronecker delta 函数)定义。 同理,Gauss 电荷产生的静电势 也在空间中周期性分布。对 做 Fourier 变换: 两者的逆 Fourier 变换分别为 将方程 代入方程 即 Poisson 方程得 方程 指出,Gauss 电荷与其静电势在倒易空间的关系非常简洁。和将方程 代入方程 ,得 注意到方程 仅对 成立。当单元系统为电中性即 时, 项的贡献为零。系综中 Gauss 电荷对单元系统内点电荷的静电相互作用能的总和为 其中结构因子 的定义为

代码
文本

本章小结

在 Ewald 求和中,系统的静电相互作用能的总和可以写为实空间、倒易空间和自相互作用的静电相互作用能三项之和: 其中 静电相互作用能的实空间项 快速衰减,因此在实际计算中可以通过设置截断距离(cut-off distance) 得到带电粒子的近邻列表(neighbor list),并在列表内直接求和得到。若点电荷 的近邻列表为集合 ,有 Gauss 分布的标准差 的选择可以调节实空间和倒易空间所需算力的相对大小,从而影响 Ewald 求和的计算效率,但 Ewald 求和的最终结果与 的选择无关。Jackson 和 Catlow 通过预设每个实空间和倒易空间项的计算量相等,提出以下 的选取4 其中 为单元系统的体积。此外, 也是文献中常用的参数。

代码
文本

PME 求和

基本思想

前文提到,经典 Ewald 求和计算倒易空间部分能量的时间复杂度为 ,这是由离散 Fourier 变换(discrete Fourier transform,DFT)的基本性质决定的。对于较大体量的模拟系统,使用基本的离散 Fourier 变换处理的计算效率较低,如果可以将点电荷通过插值算法分布到空间中的均匀网格上,就取得了使用快速 Fourier 变换(fast Fourier transform,FFT)的条件,将时间复杂度降为 ,从而极大地提升计算效率。PME 求和算法正是在这种思想的指导下被开发出来。

代码
文本

Lagrange 插值

为了近似静电结构因子 ,需要对方程 中出现的复指数 进行插值。给定正整数 和单元系统中的位置 ,用 表示其比例分数坐标(scaled fractional coordinates),其中 )。由于周期性边界条件,可以假设 。有 对于实数 ,设 表示 的整数部分,即 是满足 的唯一整数。可以使用线性插值(linear interpolation)对方程 右侧的各个指数进行近似: 将方程 )分别代入方程 ,将方程 右侧的乘积三线性插值(trilinear interpolation)为8项的和。用 表示以下线性帽函数(linear hat function): 可以将方程 改写为 其中 。注意到方程 中的求和实际上是有限的,因为 是有界支撑的(bounded support)。复指数的高阶分段 Lagrange 插值可以用类似的方式表示。考虑在点 上的分段 阶 Lagrange 插值,定义分配函数: 注意到 时,上式与 的定义一致。利用该函数,可以写出复指数的一般分段 Lagrange 插值公式: 根据 Lagrange 插值的标准误差估计,该近似中的误差有界于

代码
文本

物理图像

下面讨论 Lagrange 插值的物理图像。为了简便性,考虑如图3所示的一维网格,假定网格宽度为 表示格点, 表示格子边界,即两个相邻格点的中点。 alt

图3. 一维网格示意图。

假设 为 Lagrange 插值阶数。若 为奇数,定义 为距离粒子最近的格点坐标;若 为偶数,定义 为距离粒子最近的格点中点坐标。将粒子的电荷以分配函数 分配到以 为中心、索引分别为 个格点上。下面给出 时的 Lagrange 插值分配函数 以供参考。

时: 时: 分配函数容易扩展到三维空间: 在周期性边界条件中,格点 上的电荷量为 alt

图4. 将点电荷分配到二维网格上()。蓝点表示初始点电荷,红点表示分配到网格上的电荷。圆点的大小指示电荷量。

代码
文本

本章小结

利用方程 对静电结构因子 进行近似,有 其中 表示数组 的离散 Fourier 变换。将方程 代入方程 ,有 注意到对倒易格矢量 的分量 的求和与对网格求和相同,这是由处理均匀取样信号的离散 Fourier 变换的性质决定的。

代码
文本

PME 求和算法实现

简介

  • 使用C语言对 PME 求和计算倒易空间的经典相互作用能的方法进行代码实现。
  • 代码调用 FFTW 库对网格上的分配电荷进行快速 Fourier 变换。
  • 代码力求可读性,不具备计算效率。
代码
文本

示例代码

代码
文本
[ ]
/* 导入头文件,定义常数和全局变量*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "fftw3.h"

#define MAX_NAT 700
#define MAX_NX 200
#define MAX_NY 200
#define MAX_NZ 600
#define PI 3.1415927
#define X 0
#define Y 1
#define Z 2
#define REAL 0
#define IMAG 1
#define EPSILON 8.854187817e-12 // dielectric constant
#define EC 1.6021766208e-19 // elementary charge

double ax, ay, az; // 3D sizes of the simulation box (Å)
double mesh_width = 0.4; // width of a single mesh square (Å)
int n[3]; // 3D mesh numbers in a unit cell
int nat; // total counts of atoms
double charge_atom[MAX_NAT]; // charges of atoms
double charge_mesh[MAX_NX][MAX_NY][MAX_NZ]; // charges on mesh point
double position[MAX_NAT][3]; // 3D coordinates of atoms
int order = 6; // pme order
double volume; // volume of the simulation box
double beta; // Ewald coefficient β = 1/(sqrt(2)*η) = sqrt(π)*(nat/volume^2)^(1/6)
代码
文本
[ ]
/* 子函数:计算3维矢量的模的平方 */

double vector_square(double vector[]) {
int i;
double result = 0.0;
for(i=0; i<3; i++) {
if(vector[i]<0)
vector[i] = -vector[i];
result += vector[i]*vector[i];
}
return result;
}
代码
文本
[ ]
/* 子函数:初始化 PME 所需数据,从文件中读入系统尺寸,原子的坐标和带电量,同时计算网格个数、系统体积和 Ewald 系数*/

int pme_initialization(void)
{
int i, num;
/* read total atomic counts and position information from POSCAR */
char position_filename[] = "POSCAR";
FILE *poscar = fopen(position_filename, "r");
/* omit the first two lines */
fscanf(poscar, "%*s %*s %*s %*s %*s %*s");
fscanf(poscar, "%*f");
/* read the size of the simulation box */
fscanf(poscar, "%le %*le %*le", &ax);
fscanf(poscar, "%*le %le %*le", &ay);
fscanf(poscar, "%*le %*le %le", &az);
printf("Size: %lf x %lf x %lf (Å)\n", ax, ay, az);
/* calculate the mesh points for the box */
n[X] = (int)(ax/mesh_width);
n[Y] = (int)(ay/mesh_width);
n[Z] = (int)(az/mesh_width);
printf("Mesh: %d x %d x %d\n", n[X], n[Y], n[Z]);
volume = ax*ay*az;
printf("Volume: %lf (nm^3)\n", volume/1000);
fscanf(poscar, "%*s %*s %*s %*s %*s %*s"); // omit the sixth line
/* sum up integers in the seventh line to nat */
while (fscanf(poscar, "%d", &num) == 1) {
nat += num;
}
printf("Atoms: %d\n", nat);
fscanf(poscar, "%*s"); // omit the eighth line
/* read atomic coordinates */
for (int i = 0; i < nat; i++) {
fscanf(poscar, "%lf %lf %lf", &position[i][X], &position[i][Y], &position[i][Z]);
}
fclose(poscar);
/* read atomic charges from QEQ_charge */
char charge_filename[] = "QEQ_charge";
FILE *charge = fopen(charge_filename, "r");
for (i = 0; i < nat; i++) {
fscanf(charge, "%lf", &charge_atom[i]);
}
fclose(charge);
beta = sqrt(PI)*pow((double)nat/pow(volume,2),1.0/6.0);
printf("beta: %lf\n", beta);
return 0;
}
代码
文本
[ ]
/* 子函数:计算 Lagrange 插值的分配系数(以插值阶数 P = 6 为例) */

double lagrangian_interpolation(int order, int local_mesh_index, double dx)
{
double w = 0.0; // interpolation coefficient
if(order == 6) {
if(local_mesh_index == 0)
w = (1 - 10*dx + 40*pow(dx,2) - 80*pow(dx,3) + 80*pow(dx,4) - 32*pow(dx,5))/3840;
else if(local_mesh_index == 1)
w = (237 - 750*dx + 840*pow(dx,2) - 240*pow(dx,3) - 240*pow(dx,4) + 160*pow(dx,5))/3840;
else if(local_mesh_index == 2)
w = (841 - 770*dx - 440*pow(dx,2) + 560*pow(dx,3) + 80*pow(dx,4) - 160*pow(dx,5))/1920;
else if(local_mesh_index == 3)
w = (841 + 770*dx - 440*pow(dx,2) - 560*pow(dx,3) + 80*pow(dx,4) + 160*pow(dx,5))/1920;
else if(local_mesh_index == 4)
w = (237 + 750*dx + 840*pow(dx,2) + 240*pow(dx,3) - 240*pow(dx,4) - 160*pow(dx,5))/3840;
else if(local_mesh_index == 5)
w = (1 + 10*dx + 40*pow(dx,2) + 80*pow(dx,3) + 80*pow(dx,4) + 32*pow(dx,5))/3840;
return w;
}
else {
printf("PME interpolation order not supported!");
return 0;
}
}
代码
文本
[ ]
/* 子函数:将点电荷分配到网格上,即预先计算 Q(m1,m2,m3) */

int charge_assignment(void)
{
int iat; // variable for atomic serial numbers
int i; // variable for 3 dimensions
double u[3]; // mesh coordinate of the atom
double midpoint[3]; // mesh coordinate of the nearest midpoint
double dx[3]; // mesh coordinate difference between the atom and nearest midpoint
int mesh_index[3] = {0,0,0}; // index of mesh points
int gross_index[3] = {0,0,0}; // gross index of mesh points (which could be <0 or >nx)
int ix,iy,iz; // variables for local mesh index (which be from 0 to p-1)
double charge_mesh_x = 0.0; // temporary variable for the charge on mesh point
double charge_mesh_xy = 0.0;
double charge_mesh_xyz = 0.0;

for(iat=0; iat<nat; iat++) {
if(charge_atom[iat]!=0) {
for(i=0; i<3; i++) {
u[i] = position[iat][i]/mesh_width;
midpoint[i] = (int)u[i] + 1.0/2;
dx[i] = u[i] - midpoint[i];
gross_index[i] = (int)u[i] - order/2 + 1;
}
for(ix=0; ix<order; ix++) {
gross_index[X]++;
charge_mesh_x = charge_atom[iat]*lagrangian_interpolation(order, ix, dx[X]);
for(iy=0; iy<order; iy++) {
gross_index[Y]++;
charge_mesh_xy = charge_mesh_x*lagrangian_interpolation(order, iy, dx[Y]);
for(iz=0; iz<order; iz++) {
gross_index[Z]++;
charge_mesh_xyz = charge_mesh_xy*lagrangian_interpolation(order, iz, dx[Z]);
for(i=0; i<3; i++) {
if(gross_index[i]>=0 && gross_index[i]<n[i])
mesh_index[i] = gross_index[i];
else if(gross_index[i]<0)
mesh_index[i] = gross_index[i] + n[i];
else if(gross_index[i]>=n[i])
mesh_index[i] = gross_index[i] - n[i];
}
charge_mesh[mesh_index[X]][mesh_index[Y]][mesh_index[Z]] += charge_mesh_xyz;
}
}
}
}
}
return 0;
}
代码
文本
[ ]
/* 调用 FFTW 库对网格上的电荷 Q(m1,m2,m3) 进行快速 Fourier 变换,再对倒易空间能量进行求和 */

double pme_summation(void)
{
int im, jm, km;
int mx, my, mz; // elements of the reciprocal lattice vector in Fourier space
double m[3]; // reciprocal vectors in Fourier space
double m_square;
double eterm; // eterm = exp(-π^2*m^2/β^2)/m^2
double structure_real = 0.0;
double structure_imag = 0.0;
double structure_square = 0.0;
double energy_reciprocal = 0.0;
fftw_complex q[n[X]][n[Y]][n[Z]];
fftw_complex s[n[X]][n[Y]][n[Z]];
for(im=0; im<n[X]; im++) {
for(jm=0; jm<n[Y]; jm++) {
for(km=0; km<n[Z]; km++) {
q[im][jm][km][REAL] = charge_mesh[im][jm][km];
q[im][jm][km][IMAG] = 0.0;
}
}
}
fftw_plan plan = fftw_plan_dft_3d(n[X], n[Y], n[Z], q, s, FFTW_FORWARD, FFTW_ESTIMATE);
fftw_execute(plan);
fftw_destroy_plan(plan);
for(mx=0; mx<n[X]; mx++) {
m[X] = (double)mx/ax;
for(my=0; my<n[Y]; my++) {
m[Y] = (double)my/ay;
for(mz=0; mz<n[Z]; mz++) {
m[Z] = (double)mz/az;
if(mx==0 && my==0 && mz==0)
continue;
m_square = vector_square(m);
eterm = exp(-PI*PI*m_square/(beta*beta))/m_square;
structure_real = s[mx][my][mz][REAL];
structure_imag = s[mx][my][mz][IMAG];
structure_square = structure_real*structure_real + structure_imag*structure_imag;
energy_reciprocal += eterm*structure_square;
}
}
}
energy_reciprocal *= 1.0/(8.0*PI*PI*EPSILON*volume)*EC*10e+10;
return energy_reciprocal;
}
代码
文本
[ ]
/* 主函数:调用各子函数,执行 PME 求和 */

int main(void) {
int i, j, k;
double energy_reciprocal;
pme_initialization();
double charge = 0.0;
for(i=0; i<nat; i++) {
charge += charge_atom[i];
}
printf("Totol charge in the box: %le\n", charge);
charge_assignment();

double mesh_charge = 0.0;
for(i=0; i<n[X]; i++) {
for(j=0; j<n[Y]; j++) {
for(k=0; k<n[Z]; k++) {
mesh_charge += charge_mesh[i][j][k];
}
}
}
printf("Totol charge on the mesh: %le\n", mesh_charge);
energy_reciprocal = pme_summation();
printf("Reciprocal energy = %lf (eV)\n", energy_reciprocal);

return 0;
}
代码
文本

示例输出

Size: 17.150000 x 17.160000 x 56.000000 (Å)

Mesh: 42 x 42 x 140

Volume: 16.480463 (nm^3)

Atoms: 698

beta: 0.207441

Totol charge in the box: 4.164000e-06

Totol charge on the mesh: -5.851899e-03

Reciprocal energy = 15.606500 (eV)

Program ended with exit code: 0

代码
文本

参考文献

[1] Paul P. Ewald. "Die Berechnung optischer und elektrostatischer Gitterpotentiale." Annalen der physik 369.3 (1921): 253-287. (https://doi.org/10.1002/andp.19213690304)

[2] Tom Darden, Darrin York, and Lee Pedersen. "Particle mesh Ewald: An N⋅log(N) method for Ewald sums in large systems." The Journal of chemical physics 98.12 (1993): 10089-10092. (https://doi.org/10.1063/1.464397)

[3] Ulrich Essmann, Lalith Perera, Max L. Berkowitz, Tom Darden, Hsing Lee, and Lee G. Pedersen. "A smooth particle mesh Ewald method." The Journal of chemical physics 103.19 (1995): 8577-8593. (https://doi.org/10.1063/1.470117)

[4] R. A. Jackson, and C. R. A. Catlow. "Computer simulation studies of zeolite structure." Molecular Simulation 1.4 (1988): 207-224. (https://doi.org/10.1080/08927028808080944)

代码
文本
计算化学
分子动力学
从算法原理到代码实现
计算化学分子动力学从算法原理到代码实现
已赞1
本文被以下合集收录
good notebooks collected by Taiping Hu
TaipingHu
更新于 2024-09-10
33 篇14 人关注
Great
mosey
更新于 2023-10-25
12 篇0 人关注
推荐阅读
公开
Abacus第一性原理计算初步教程——以铁单质及其简单化合物为例
notebook中文作业ABACUS使用教程
notebook中文作业ABACUS使用教程
why
发布于 2024-05-18
1 赞
公开
周期性体系中静电相互作用的计算方法: Ewald Summation和Partical Mesh Ewald
中文#经典分子力学 # 力场LaTex从算法原理到代码实现科普力场
中文#经典分子力学 # 力场LaTex从算法原理到代码实现科普力场
liangyy
发布于 2023-09-22
7 赞2 转存文件1 评论