You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何加速Python中分子动力学模拟相关的for循环?

分子动力学模拟代码循环加速优化方案

问题背景

以下是用于分子动力学模拟的Python代码,其中resampling函数内的1000万次迭代循环运行极慢(100万步耗时约50分钟),尝试GPU加速后效果更差,需要优化循环效率:

import MDAnalysis as mda
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from tqdm import tqdm as tq
import MDAnalysis.analysis.pca as pca
import random
import math  # 修复原代码未导入math的问题

def PCA_projection(pdb,dcd,atomgroup):
    u = mda.Universe(pdb,dcd)
    PSF_pca = pca.PCA(u, select=atomgroup)
    PSF_pca.run(verbose=True)
    n_pcs = np.where(PSF_pca.results.cumulated_variance > 0.95)[0][0]
    atomgroup = u.select_atoms(atomgroup)
    pca_space = PSF_pca.transform(atomgroup, n_components=n_pcs)
    PC1_proj = [pca_space[i][0] for i in range(len(pca_space))]
    PC2_proj = [pca_space[i][1] for i in range(len(pca_space))]
    return PC1_proj, PC2_proj


def Read_bias_potential(bias_potential):
    Bias_potential = pd.read_csv(bias_potential)
    Bias_potential = Bias_potential['En-User']
    Bias_potential = Bias_potential.values.tolist()
    W = [math.exp((-1 * i) / (0.001987*300)) for i in Bias_potential]
    return W

def Bin(PC1_prj, PC2_prj, frame_num, min_br1, max_br1, min_br2, max_br2, bin_num, W):
    data1 = PC1_prj[0:frame_num]
    bins1 = np.linspace(min_br1, max_br1, bin_num)
    bins1 = np.round(bins1,2)
    digitized1 = np.digitize(data1, bins1)
    binc1 = np.arange(min_br1 + (max_br1 - min_br1)/2*bin_num, 
max_br1 + (max_br1 - min_br1)/2*bin_num, (max_br1 - min_br1)/bin_num, dtype = float)
    binc1 = np.around(binc1,3)

    data2 = PC2_prj[0:frame_num]
    bins2 = np.linspace(min_br2, max_br2, bin_num)
    bins2 = np.round(bins2,2)
    digitized2 = np.digitize(data2, bins2)
    binc2 = np.arange(min_br2 + (max_br2 - min_br2)/2*bin_num, max_br2 + (max_br2 - min_br2)/2*bin_num, (max_br2 - min_br2)/bin_num, dtype = float)
    binc2 = np.around(binc2,3)

    w_array = np.zeros((bin_num,bin_num))

    for j in range(frame_num):
        w_array[digitized1[j]][digitized2[j]] += (W[digitized1[j]] + W[digitized2[j]])

    for m in range(bin_num):
        for n in range(bin_num):
            if w_array[m][n] == 0:
                w_array[m][n] = 1e-100

    return w_array, binc1, binc2

def gaussian(Sj1,Slj1,Sj2,Slj2,count):
    sigma1 = 0.5
    sigma2 = 0.5
    Kb = 0.001987204
    T = 300
    h0 = 0.0001
    g = 0
    C1 = 0
    C2 = 0
    idx_j1 = np.where(Slj1 == Sj1)[0][0]
    idx_j2 = np.where(Slj2 == Sj2)[0][0]
    for i in range(idx_j2 -5, idx_j2 +6):
        C2 = i if 0<=i<1000 else (i+1000 if i<0 else i-1000)
        for j in range(idx_j1 -5, idx_j1 +6):
            C1 = j if 0<=j<1000 else (j+1000 if j<0 else j-1000)
            g += count[C2,C1] * h0 * np.exp( (-(Sj1 - Slj1[C1])**2/(2*sigma1**2)) + (-(Sj2 - Slj2[C2])**2/(2*sigma2**2)) )
    return np.exp(-g/(Kb*T))

def resampling(binc1, binc2, w_array):
    l =1000
    F = np.zeros((l,l))
    count = np.zeros((l,l))
    Wn = w_array

    for i in tq(range(10000000)):
        SK1 = random.choice(binc1)
        SK2 = random.choice(binc2)
        SL1 = random.choice(binc1)
        SL2 = random.choice(binc2)
        while SK1 == SL1:
            SL1 = random.choice(binc1)
        while SK2 == SL2:
            SL2 = random.choice(binc2)
        idx_sk1 = np.where(binc1 == SK1)[0][0]
        idx_sk2 = np.where(binc2 == SK2)[0][0]
        idx_sl1 = np.where(binc1 == SL1)[0][0]
        idx_sl2 = np.where(binc2 == SL2)[0][0]
        
        F[idx_sk2][idx_sk1] = gaussian(SK1,binc1,SK2,binc2,count)
        F[idx_sl2][idx_sl1] = gaussian(SL1,binc1,SL2,binc2,count)
        
        W_SK = Wn[idx_sk2][idx_sk1] * F[idx_sk2][idx_sk1]
        W_SL = Wn[idx_sl2][idx_sl1] * F[idx_sl2][idx_sl1]
        
        if W_SK <= W_SL:
            selected_idx1, selected_idx2 = idx_sl1, idx_sl2
        else:
            a = random.random()
            if W_SL/W_SK >= a:
                selected_idx1, selected_idx2 = idx_sl1, idx_sl2
            else:
                selected_idx1, selected_idx2 = idx_sk1, idx_sk2
        count[selected_idx2][selected_idx1] += 1
    return F

注:已修复原代码两处问题:补充math模块导入;修正resampling中重复赋值F的错误。


核心优化方案

1. 预构建值-索引映射,消除重复查找

原代码每次循环多次调用np.where查找索引,时间复杂度为O(n),提前构建字典映射可将查找降为O(1):

def resampling(binc1, binc2, w_array):
    # 预构建值到索引的映射
    binc1_map = {val: idx for idx, val in enumerate(binc1)}
    binc2_map = {val: idx for idx, val in enumerate(binc2)}
    l =1000
    F = np.zeros((l,l))
    count = np.zeros((l,l))
    Wn = w_array

    for i in tq(range(10000000)):
        SK1 = random.choice(binc1)
        SK2 = random.choice(binc2)
        SL1 = random.choice(binc1)
        SL2 = random.choice(binc2)
        while SK1 == SL1:
            SL1 = random.choice(binc1)
        while SK2 == SL2:
            SL2 = random.choice(binc2)
        # 直接通过字典获取索引
        idx_sk1 = binc1_map[SK1]
        idx_sk2 = binc2_map[SK2]
        idx_sl1 = binc1_map[SL1]
        idx_sl2 = binc2_map[SL2]
        # ... 后续代码不变

2. 向量化gaussian函数,消除内部嵌套循环

用numpy切片和广播替代gaussian中的嵌套循环,利用numpy的C级运算加速:

def gaussian(Sj1, Slj1, Sj2, Slj2, count, sigma1=0.5, sigma2=0.5, Kb=0.001987204, T=300, h0=0.0001):
    idx_j1 = np.where(Slj1 == Sj1)[0][0]
    idx_j2 = np.where(Slj2 == Sj2)[0][0]
    
    # 生成索引范围并处理边界循环
    idx_range1 = np.arange(idx_j1 -5, idx_j1 +6)
    idx_range1 = np.where(idx_range1 <0, idx_range1+1000, idx_range1)
    idx_range1 = np.where(idx_range1 >=1000, idx_range1-1000, idx_range1)
    
    idx_range2 = np.arange(idx_j2 -5, idx_j2 +6)
    idx_range2 = np.where(idx_range2 <0, idx_range2+1000, idx_range2)
    idx_range2 = np.where(idx_range2 >=1000, idx_range2-1000, idx_range2)
    
    # 广播生成二维矩阵,向量化计算
    slj1_vals = Slj1[idx_range1]
    slj2_vals = Slj2[idx_range2]
    count_vals = count[idx_range2[:, None], idx_range1]
    
    gauss_term1 = np.exp(-(Sj1 - slj1_vals)**2/(2*sigma1**2))
    gauss_term2 = np.exp(-(Sj2 - slj2_vals)**2/(2*sigma2**2))
    total_gauss = count_vals * h0 * gauss_term1 * gauss_term2
    
    g = total_gauss.sum()
    return np.exp(-g/(Kb*T))

3. 用Numba编译核心函数,将Python循环转为机器码

Numba对循环密集型代码加速效果显著,给gaussian和resampling添加编译装饰器:

from numba import njit, prange

@njit(fastmath=True)
def gaussian(Sj1, Slj1, Sj2, Slj2, count, sigma1=0.5, sigma2=0.5, Kb=0.001987204, T=300, h0=0.0001):
    idx_j1 = np.where(Slj1 == Sj1)[0][0]
    idx_j2 = np.where(Slj2 == Sj2)[0][0]
    g = 0.0
    for i in range(idx_j2 -5, idx_j2 +6):
        C2 = i
        if C2 <0:
            C2 +=1000
        elif C2 >=1000:
            C2 -=1000
        for j in range(idx_j1 -5, idx_j1 +6):
            C1 = j
            if C1 <0:
                C1 +=1000
            elif C1 >=1000:
                C1 -=1000
            term1 = -(Sj1 - Slj1[C1])**2/(2*sigma1**2)
            term2 = -(Sj2 - Slj2[C2])**2/(2*sigma2**2)
            g += count[C2, C1] * h0 * np.exp(term1 + term2)
    return np.exp(-g/(Kb*T))

@njit(fastmath=True)
def resampling(binc1, binc2, w_array):
    l = 1000
    F = np.zeros((l,l), dtype=np.float64)
    count = np.zeros((l,l), dtype=np.float64)
    Wn = w_array
    len_binc1 = len(binc1)
    len_binc2 = len(binc2)
    
    for i in range(10000000):
        # 直接随机选择索引,避免后续查找
        idx_sk1 = np.random.randint(0, len_binc1)
        idx_sl1 = np.random.randint(0, len_binc1-1)
        if idx_sl1 >= idx_sk1:
            idx_sl1 +=1
        
        idx_sk2 = np.random.randint(0, len_binc2)
        idx_sl2 = np.random.randint(0, len_binc2-1)
        if idx_sl2 >= idx_sk2:
            idx_sl2 +=1
        
        SK1 = binc1[idx_sk1]
        SK2 = binc2[idx_sk2]
        SL1 = binc1[idx_sl1]
        SL2 = binc2[idx_sl2]
        
        F[idx_sk2, idx_sk1] = gaussian(SK1, binc1, SK2, binc2, count)
        F[idx_sl2, idx_sl1] = gaussian(SL1, binc1, SL2, binc2, count)
        
        W_SK = Wn[idx_sk2, idx_sk1] * F[idx_sk2, idx_sk1]
        W_SL = Wn[idx_sl2, idx_sl1] * F[idx_sl2, idx_sl1]
        
        if W_SK <= W_SL:
            selected_idx1, selected_idx2 = idx_sl1, idx_sl2
        else:
            a = np.random.rand()
            if W_SL/W_SK >= a:
                selected_idx1, selected_idx2 = idx_sl1, idx_sl2
            else:
                selected_idx1, selected_idx2 = idx_sk1, idx_sk2
        count[selected_idx2, selected_idx1] +=1
    return F

4. 优化随机选择逻辑,消除while循环

原代码用while确保SK和SL不同,改用索引偏移法直接生成符合条件的随机数:

# 替换resampling中的随机选择部分
idx_sk1 = np.random.randint(0, len_binc1)
# 生成不等于idx_sk1的随机索引
idx_sl1 = np.random.randint(0, len_binc1-1)
if idx_sl1 >= idx_sk1:
    idx_sl1 +=1

idx_sk2 = np.random.randint(0, len_binc2)
idx_sl2 = np.random.randint(0, len_binc2-1)
if idx_sl2 >= idx_sk2:
    idx_sl2 +=1

效果预期

通过上述优化,尤其是Numba编译和向量化改造,循环速度可提升10-100倍,100万步耗时可压缩至几分钟以内。GPU效果差的原因是单步计算量过小,内存拷贝开销抵消了计算优势,而Numba的CPU编译更适配这种循环次数多、单步计算量小的场景。

内容的提问来源于stack exchange,提问作者Tianming Qu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 19:45:50