前言

选择python做接口解析,数值计算部分采用fortran来实现的时候, 我们可以利用python/c的接口来实现,不过这需要对f90再做个wrap,这样有点麻烦,更好的方式是通过f2py来实现。

我们的目标是:

  1. 掌握f2py的使用
  2. 复习一下简单的分子动力学内容

我们用分子动力学里的经典LJ对势,写个F90函数计算原子体系的能量和力,用f2py生成库,然后在python中调用这个库函数 原子体系的总能等于原子对势之和

简单复习一下分子动力学

原子体系的总能等于原子对势之和

\begin{equation} U = \sum_{i \lt j} u(r_{ij}) \end{equation}

\begin{equation} u(r_{ij})=4 \epsilon [(\frac{r_{ij}}{\sigma})^{-12} - (\frac{r_{ij}}{\sigma})^{-6}] \end{equation}

那么原子在x方向上的力就是:

\begin{aligned} F_{i, x} &=-\frac{\partial U}{\partial x_{i}} \\\
&=-\frac{\partial}{\partial x_{i}} \sum_{j \neq i} u\left(r_{i j}\right) \\\
&=-\sum_{j \neq i} \frac{\partial r_{i j}}{\partial x_{i}} \frac{\partial}{\partial r_{i j}} u_{12}\left(r_{i j}\right) \end{aligned}

由\(r_{ij}^2=(x_i - x_j)^2 + (y_i - y_j)^2 + (z_i+z_j)^2\) , 我们有

\begin{equation} F_{i,x}= - \sum_{(j \neq i)} (\frac{x_i-x_j}{r_{ij}})\frac{\partial}{\partial r_{ij}} u(r_{ij}) \end{equation}

自然地我们得到3维情况下的力的公式,同时引入向量符号\(r_{ij} = r_j - r_i\),

\begin{aligned} \mathbf{F}_{i} &=\sum_{j \neq i}\left(\frac{\mathbf{r}_{i j}}{r_{i j}}\right) \frac{\partial}{\partial r_{i j}} u\left(r_{i j}\right) \\\
&=\sum_{j \neq i}\left(\frac{\mathbf{r}_{i j}}{r_{i j}}\right)\left(\frac{4 \epsilon}{\sigma}\right)\left[-12\left(\frac{r_{i j}}{\sigma}\right)^{-13}+6\left(\frac{r_{i j}}{\sigma}\right)^{-7}\right] \\\
&=\sum_{j \neq i} \mathbf{r}_{i j}\left(\frac{\epsilon}{\sigma}\right)\left[-48\left(\frac{r_{i j}}{\sigma}\right)^{-14}+24\left(\frac{r_{i j}}{\sigma}\right)^{-8}\right] \end{aligned}

考虑周期性边界条件:每个原子只考虑临近的周围镜像的原子,这个通过找2个粒子之间的最近镜像距离来确定是哪个image

\begin{equation} r_{ij}^0 = r_{ij} - L nint(r_{ij}/L) \end{equation}

L是盒子的长度,对于非正方盒子来讲 L是向量形式。

最后我们在对势函数中引入一个截断距离\(r_c\), 超过这个距离时势能为0。通常$r_c=2.5σ$或者更大值。

\begin{equation} u(r_{i j})=\{\begin{array}{c} 4 \epsilon[(\frac{r_{i j}}{\sigma})^{-12}-(\frac{r_{i j}}{\sigma})^{-6}]-4 \epsilon[(\frac{r_{c}}{\sigma})^{-12}-(\frac{r_{c}}{\sigma})^{-6}] \quad r_{i j} \leq r_{c} \\\
0 & r_{i j}>r_{c} \end{array} \end{equation}

为了简化,我们采用无量纲单位来表示, 这样对势方程组的上面部分的形式就变为:

\begin{equation} u(r_{i j})=4[r_{i j}^{-12}-r_{ij}^{-6}]-4[r_{c}^{-12}-r_{c}^{-6}] \end{equation}

最终的fortran部分的实现代码,保存为ljlib.F90文件:

subroutine EnergyForces(Pos, L, rc, PEnergy, Forces, Dim, NAtom)
  implicit none
  integer, intent(in) :: Dim, NAtom
  real(8), intent(in), dimension(0:NAtom-1, 0:Dim-1) :: Pos
  real(8), intent(in) :: L, rc
  real(8), intent(out) :: PEnergy
  real(8), intent(inout), dimension(0:NAtom-1, 0:Dim-1) :: Forces
  !f2py intent(in, out) :: Forces
  real(8), dimension(Dim) :: rij, Fij, Posi
  real(8) :: d2, id2, id6, id12
  real(8) :: rc2, Shift
  integer :: i, j
  PEnergy = 0.
  Forces = 0.
  Shift = -4. * (rc**(-12) - rc**(-6))
  rc2 = rc * rc
  do i = 0, NAtom - 1
     !store Pos(i,:) in a temporary array for faster access in j loop
     Posi = Pos(i,:)
     do j = i + 1, NAtom - 1
        rij = Pos(j,:) - Posi
        rij = rij - L * dnint(rij / L)
        !compute only the squared distance and compare to squared cut
        d2 = sum(rij * rij)
        if (d2 > rc2) then
           cycle
        end if
        id2 = 1. / d2 !inverse squared distance
        id6 = id2 * id2 * id2 !inverse sixth distance
        id12 = id6 * id6 !inverse twelvth distance
        PEnergy = PEnergy + 4. * (id12 - id6) + Shift
        Fij = rij * ((-48. * id12 + 24. * id6) * id2)
        Forces(i,:) = Forces(i,:) + Fij
        Forces(j,:) = Forces(j,:) - Fij
     enddo
  enddo
end subroutine EnergyForces

subroutine VVIntegrate(Pos, Vel, Accel, L, CutSq, dt, KEnergy, PEnergy, Dim, NAtom)
  implicit none
  integer, intent(in) :: Dim, NAtom
  real(8), intent(in) :: L, CutSq, dt
  real(8), intent(inout), dimension(0:NAtom-1, 0:Dim-1) :: Pos, Vel, Accel
  !f2py intent(in, out) :: Pos, Vel, Accel
  real(8), intent(out) :: KEnergy, PEnergy
  external :: EnergyForces
  Pos = Pos + dt * Vel + 0.5 * dt*dt * Accel
  Vel = Vel + 0.5 * dt * Accel
  call EnergyForces(Pos, L, CutSq, PEnergy, Accel, Dim, NAtom)
  Vel = Vel + 0.5 * dt * Accel
  KEnergy = 0.5 * sum(Vel*Vel)
end subroutine VVIntegrate

进入编译调试

 gfortran -c liblj.F90

看看代码有无报错,恩,应该是没有的,让我们继续

准备f2py前需要的知识点

  1. 如果fortran代码对所有的变量都有严格的类型定义,我们很少需要使用f2py对变量进行转换, 不过对于带有intent(inout)属性的数组变量,需要添加!f2py 来告诉f2py我们想处理的数据类型;写成形式

       !f2py intent(in,out) :: var
    
  2. 另外必须为所有输出参数明确指定尺寸!换句话说,输出自变量不能具有假定的大小或可分配的大小

       integer, allocatable, intent(out) :: array1(:)  ! Not valid
       integer, intent(out)              :: array2(:)  ! Not valid
       integer, intent(out)              :: array3(10) ! Valid
       integer, intent(in)               :: array4(:)  ! Valid
    
  3. 不支持派生类型

开始运行f2py

根据上面的知识点,这里对ljlib.F90里的EnergyForces函数,添加一行

  real(8), intent(inout), dimension(0:NAtom-1, 0:Dim-1) :: Forces
!f2py intent(in,out) :: Forces

对VVIntegral函数,添加一行!f2py

 real(8), intent(inout), dimension(0:NAtom-1, 0:Dim-1) :: Pos, Vel, Accel
!f2py intent(in,out) :: Pos, Vel, Accel

命令行下运行

 python -m numpy.f2py -c ljlib.F90 -m ljlib

在目录下会生成ljlib.cpythonxxx.so 的动态链接文件。

打开python解释器,导入这个ljlib模块

看起来python导入ljlib库完成了,f2py用起来还是很方便的吧

>>> import ljlib
>>> print(ljlib.__doc__)
This module 'ljlib' is auto-generated with f2py (version:2).
Functions:
  penergy,forces = energyforces(pos,l,rc,forces,dim=shape(pos,1),natom=shape(pos,0))
  pos,vel,accel,kenergy,penergy = vvintegrate(pos,vel,accel,l,cutsq,dt,dim=shape(pos,1),natom=shape(pos,0))

看看各个函数的接口

>>> print(ljlib.energyforces.__doc__)
penergy,forces = energyforces(pos,l,rc,forces,[dim,natom])

Wrapper for ``energyforces``.

Parameters
----------
pos : input rank-2 array('d') with bounds (natom,dim)
l : input float
rc : input float
forces : input rank-2 array('d') with bounds (natom,dim)

Other Parameters
----------------
dim : input int, optional
    Default: shape(pos,1)
natom : input int, optional
    Default: shape(pos,0)

Returns
-------
penergy : float
forces : rank-2 array('d') with bounds (natom,dim)

>>> print(ljlib.vvintegrate.__doc__)
pos,vel,accel,kenergy,penergy = vvintegrate(pos,vel,accel,l,cutsq,dt,[dim,natom])

Wrapper for ``vvintegrate``.

Parameters
----------
pos : input rank-2 array('d') with bounds (natom,dim)
vel : input rank-2 array('d') with bounds (natom,dim)
accel : input rank-2 array('d') with bounds (natom,dim)
l : input float
cutsq : input float
dt : input float

Other Parameters
----------------
dim : input int, optional
    Default: shape(pos,1)
natom : input int, optional
    Default: shape(pos,0)

Returns
-------
pos : rank-2 array('d') with bounds (natom,dim)
vel : rank-2 array('d') with bounds (natom,dim)
accel : rank-2 array('d') with bounds (natom,dim)
kenergy : float
penergy : float

看起来python导入ljlib库完成了,f2py用起来还是很方便的吧

参考资料:

  1. https://www.numfys.net/howto/F2PY/
  2. https://sites.engineering.ucsb.edu/~shell/che210d/f2py.pdf
  3. https://numpy.org/devdocs/f2py/usage.html%E2%80%8B