如何以最有效的方式编写这段代码

4 投票
4 回答
509 浏览
提问于 2025-04-18 04:30

我有一个数组(大小为10的4次方),我需要计算数组中每两个元素之间的差值(根据原子的坐标计算一个潜在值)。我正在用纯Python写代码,但效率真的不高,有人能告诉我怎么提高速度吗?(可以使用numpy或weave)。这里的x和y是原子的坐标数组(只是简单的一维数组)。

def potential(r):
   U = 4.*(np.power(r,-12) - np.power(r,-6))
   return U
def total_energy(x):
   E = 0.
   #need to speed up this part
   for i in range(N-1):
     for j in range(i):
         E += potential(np.sqrt((x[i]-x[j])**2))  
return E

4 个回答

0

谢谢你的帮助。以下是我找到的一些内容。

  1. 最简单的版本

    return sum( potential(np.sqrt((x[i]-x[:i])**2)).sum() for i in range(N-1))
    
  2. scipy版本也不错。

  3. 如果想要更快的版本,可以考虑使用f2py程序,也就是把性能较慢的部分用纯Fortran写出来(Fortran非常快),然后编译它,再把它作为库插入到你的Python代码中。例如,我有:

    program_lj.f90

$gfortran -c program_lj.f90

如果在Fortran程序中明确地定义所有类型,那我们就可以开始了。

$f2py -c -m program_lj program_lj.f90

编译完成后,剩下的就是在Python中调用这个程序了。在Python程序中:

import program_lj
result = program_lj.subroutine_in_program(parameters)

如果你需要更一般的参考,可以查看这个很棒的网页

1

这里是最终的答案和一些时间上的比较

0) 普通版本(真的很慢)

In [16]: %timeit total_energy(points)
1 loops, best of 3: 14.9 s per loop

1) SciPy 版本

In [9]: %timeit scipy_LJ(points)
10 loops, best of 3: 44 ms per loop

1-2) Numpy 版本

 %timeit sum( potential(np.sqrt((x[i]-x[:i])**2 + (y[i]-y[:i])**2 + (z[i] - z[:i])**2)).sum() for i in range(N-1))
10 loops, best of 3: 126 ms per loop

2) 超级快的 Fortran 版本(!表示注释)

    subroutine EnergyForces(Pos, PEnergy, 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
    real(8), intent(out) :: PEnergy
    real(8), dimension(Dim) :: rij, Posi
    real(8) :: d2, id2, id6, id12
    real(8) :: rc2, Shift
    integer :: i, j
    PEnergy = 0.
    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)
            id2 = 1. / d2            !inverse squared distance
            id6 = id2 * id2 * id2    !inverse sixth distance
            id12 = id6 * id6         !inverse twelvth distance
            PEnergy = PEnergy + 4. * (id12 - id6)
      enddo
    enddo
end subroutine

调用之后

In [14]: %timeit ljlib.energyforces(points.transpose(), 3, N)
10000 loops, best of 3: 61 us per loop

3) 结论是,Fortran 的速度比 SciPy 快 1000 倍,比 Numpy 快 3000 倍,比纯 Python 快几百万倍。这是因为 SciPy 版本需要先创建一个差值的矩阵,然后再进行分析,而 Fortran 版本则是直接实时处理的。

5

我建议你看看 scipy.spatial.distance 这个库。特别是使用 pdist 函数,它可以计算一个数组中所有元素之间的距离。

我假设你有一个形状为 (Nx3) 的数组,所以我们需要稍微修改一下你的代码:

def potential(r):
       U = 4.*(np.power(r,-12) - np.power(r,-6))
       return U
def total_energy(x):
   E = 0.
   #need to speed up this part
   for i in range(N):                                    #To N here
     for j in range(i):
         E += potential(np.sqrt(np.sum((x[i]-x[j])**2))) #Add sum here
   return E

现在我们用空间库来重写这个:

import scipy.spatial.distance as sd


def scipy_LJ(arr, sigma=None):
    """
    Computes the Lennard-Jones potential for an array (M x N) of M points
    in N dimensional space. Usage of a sigma parameter is optional.
    """

    if len(arr.shape)==1:
        arr = arr[:,None]

    r = sd.pdist(arr)

    if sigma==None:
        np.power(r, -6, out=r)
        return np.sum(r**2 - r)*4

    else:
       r *= sigma
       np.power(r, -6, out=r)
       return np.sum(r**2 - r)*4

让我们来做一些测试:

N = 1000
points = np.random.rand(N,3)+0.1

np.allclose(total_energy(points), scipy_LJ(points))
Out[43]: True

%timeit total_energy(points)
1 loops, best of 3: 13.6 s per loop

%timeit scipy_LJ(points)
10 loops, best of 3: 24.3 ms per loop

现在速度快了大约500倍!

N = 10000
points = np.random.rand(N,3)+0.1

%timeit scipy_LJ(points)
1 loops, best of 3: 3.05 s per loop

这个过程大约用了2GB的内存。

5

首先,你可以使用数组运算:

def potential(r):
    return 4.*(r**(-12) - r**(-6))

def total_energy(x):
    E = 0.
    for i in range(N-1):
        E += potential(np.sqrt((x[i]-x[:i])**2)).sum()
    return E

或者你可以试试完全向量化的版本:

def total_energy(x):
    b=np.diag(x).cumsum(1)-x
    return potential(abs(b[np.triu_indices_from(b,1)])).sum()

撰写回答