Структурирование списка объектов в Python для векторизации: можно ли векторизовать список структур (объектов) или требуются явные массивы

1

Расчеты энергии в молекулярном моделировании по своей сути полны циклов "за". Традиционно координаты для каждого атома/молекулы хранились в массивах. массивы довольно просты для векторизации, но структуры с хорошими кодами. Обработка молекул как отдельных объектов, каждая со своими координатами и другими свойствами, очень удобна и намного яснее, чем бухгалтерский учет.

Я использую Python версии 3.6

Моя проблема в том, что я не могу понять, как векторизовать вычисления, когда я использую массив объектов... кажется, что цикл for нельзя избежать. Нужно ли мне использовать массивы, чтобы использовать numpy и векторизовать мой код?

Вот пример python, который использует массивы (строка 121 кода) и показывает быстрый (numpy) и медленный ("нормальный") расчет энергии питона.

https://github.com/Allen-Tildesley/examples/blob/master/python_examples/mc_lj_module.py

Расчет выполняется намного быстрее, используя метод ускорения numpy, поскольку он векторизован.

Как бы я вектурировал вычисление энергии, если бы я не использовал массивы, а массив объектов, каждый со своими координатами? Это, по-видимому, требует использования медленного цикла.

Вот простой пример кода с рабочей медленной версией цикла for и попытки клонирования, которая не работает:

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 атома на молекулу, и не все молекулы имеют одинаковое число атомов, циклы для взаимодействий молекулы-молекулы, а не простых взаимодействий парных пар, используемых в настоящее время.

  • 0
    Какие конкретные расчеты вы стремитесь векторизовать?
  • 0
    Расчет энергии в последнем цикле. энергия - это сумма всех парных взаимодействий ... Я хотел бы рассчитать все парные взаимодействия одновременно, а затем суммировать их в конце, а не проходить через все парные взаимодействия
Показать ещё 3 комментария
Теги:
arrays
numpy
vectorization

2 ответа

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

Это намного быстрее, но здесь еще много оптимизма.

  • 0
    @T Burgis Я постоянно получаю, что второй метод, векторизованный метод (calc_all_energies), чуть более чем в 10 раз быстрее! Если вы играли с ним больше и нашли больше оптимизаций, пожалуйста, дайте мне знать.
  • 1
    apply_along_axis просто перебирает другие измерения входного массива. Это удобная функция, но не настоящая «векторизация».
Показать ещё 2 комментария
1

Если вы хотите вычислить энергии с координатами в качестве входных данных, я предполагаю, что вы ищете пары. Для этого вы должны заглянуть в библиотеку SciPy. В частности, я бы посмотрел на scipy.spatial.distance.pdist. Документацию можно найти здесь.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню