Расчеты энергии в молекулярном моделировании по своей сути полны циклов "за". Традиционно координаты для каждого атома/молекулы хранились в массивах. массивы довольно просты для векторизации, но структуры с хорошими кодами. Обработка молекул как отдельных объектов, каждая со своими координатами и другими свойствами, очень удобна и намного яснее, чем бухгалтерский учет.
Моя проблема в том, что я не могу понять, как векторизовать вычисления, когда я использую массив объектов... кажется, что цикл for нельзя избежать. Нужно ли мне использовать массивы, чтобы использовать numpy и векторизовать мой код?
Вот пример python, который использует массивы (строка 121 кода) и показывает быстрый (numpy) и медленный ("нормальный") расчет энергии питона.
https://github.com/Allen-Tildesley/examples/blob/master/python_examples/mc_lj_module.py
Расчет выполняется намного быстрее, используя метод ускорения numpy, поскольку он векторизован.
Как бы я вектурировал вычисление энергии, если бы я не использовал массивы, а массив объектов, каждый со своими координатами? Это, по-видимому, требует использования медленного цикла.
import numpy as np
import time
class Mol:
num = 0
def __init__(self, r):
Mol.num += 1
self.r = np.empty((3),dtype=np.float_)
self.r[0] = r[0]
self.r[1] = r[1]
self.r[2] = r[2]
""" Alot more useful things go in here in practice"""
################################################
# #
# Main Program #
# #
################################################
L = 5.0 # Length of simulation box (arbitrary)
r_cut_box_sq = L/2 # arbitrary cutoff - required
mol_list=[]
nmol = 1000 # number of molecules
part = 1 # arbitrary molecule to interact with rest of molecules
""" make 1000 molecules (1 atom per molecule), give random coordinates """
for i in range(nmol):
r = np.random.rand(3) * L
mol_list.append( Mol( r ) )
energy = 0.0
start = time.time()
################################################
# #
# Slow but functioning loop #
# #
################################################
for i in range(nmol):
if i == part:
continue
rij = mol_list[part].r - mol_list[i].r
rij = rij - np.rint(rij/L)*L # apply periodic boundary conditions
rij_sq = np.sum(rij**2) # Squared separations
in_range = rij_sq < r_cut_box_sq
sr2 = np.where ( in_range, 1.0 / rij_sq, 0.0 )
sr6 = sr2 ** 3
sr12 = sr6 ** 2
energy += sr12 - sr6
end = time.time()
print('slow: ', end-start)
print('energy: ', energy)
start = time.time()
################################################
# #
# Failed vectorization attempt #
# #
################################################
""" The next line is my problem, how do I vectorize this so I can avoid the for loop all together?
Leads to error AttributeError: 'list' object has no attribute 'r' """
""" I also must add in that part cannot interact with itself in mol_list"""
rij = mol_list[part].r - mol_list[:].r
rij = rij - np.rint(rij/L)*L # apply periodic boundary conditions
rij_sq = np.sum(rij**2)
in_range = rij_sq < r_cut_box_sq
sr2 = np.where ( in_range, 1.0 / rij_sq, 0.0 )
sr6 = sr2 ** 3
sr12 = sr6 ** 2
energy = sr12 - sr6
energy = sum(energy)
end = time.time()
print('faster??: ', end-start)
print('energy: ', energy)
Были ли затронуты любые возможные решения, если бы внутри вычисления энергии необходимо было циклы над каждым атомом в каждой молекуле, где их теперь больше 1 атома на молекулу, и не все молекулы имеют одинаковое число атомов, циклы для взаимодействий молекулы-молекулы, а не простых взаимодействий парных пар, используемых в настоящее время.
Использовать библиотеку itertools можно здесь. Предположим, вы завершаете вычисление энергии пары молекул в функции:
def calc_pairwise_energy((mol_a,mol_b)):
# function takes a 2 item tuple of molecules
# energy calculating code here
return pairwise_energy
Затем вы можете использовать itertools.combinations, чтобы получить все пары молекул и python, встроенные в списки (код внутри [] в последней строке ниже):
from itertools import combinations
pairs = combinations(mol_list,2)
energy = sum( [calc_pairwise_energy(pair) for pair in pairs] )
Я вернулся к этому ответу, поскольку понял, что не ответил на ваш вопрос. С тем, что я уже опубликовал, функция вычисления парной энергии выглядела так (я сделал несколько оптимизаций для вашего кода):
def calc_pairwise_energy(molecules):
rij = molecules[0].r - molecules[1].r
rij = rij - np.rint(rij/L)*L
rij_sq = np.sum(rij**2) # Squared separations
if rij_sq < r_cut_box_sq:
return (rij_sq ** -6) - (rij_sq ** - 3)
else:
return 0.0
В то время как векторная реализация, которая выполняет все парные вычисления в одном вызове, может выглядеть так:
def calc_all_energies(molecules):
energy = 0
for i in range(len(molecules)-1):
mol_a = molecules[i]
other_mols = molecules[i+1:]
coords = np.array([mol.r for mol in other_mols])
rijs = coords - mol_a.r
# np.apply_along_axis replaced as per @hpaulj comment (see below)
#rijs = np.apply_along_axis(lambda x: x - np.rint(x/L)*L,0,rijs)
rijs = rijs - np.rint(rijs/L)*L
rijs_sq = np.sum(rijs**2,axis=1)
rijs_in_range= rijs_sq[rijs_sq < r_cut_box_sq]
energy += sum(rijs_in_range ** -6 - rijs_in_range ** -3)
return energy
Это намного быстрее, но здесь еще много оптимизма.
apply_along_axis
просто перебирает другие измерения входного массива. Это удобная функция, но не настоящая «векторизация».
Если вы хотите вычислить энергии с координатами в качестве входных данных, я предполагаю, что вы ищете пары. Для этого вы должны заглянуть в библиотеку SciPy. В частности, я бы посмотрел на scipy.spatial.distance.pdist
. Документацию можно найти здесь.