Open: Обработка конформационных ансамблей в Python

From TheorChemGroup at ZIOC RAS

В чем заключается эта обработка и для чего её делать?

Программы для конформационного поиска обычно выдают результаты в виде XYZ файлов, структуры из которых обычно надо вписать в инпут-файлы для последующей квантовохимической оптимизации. Чтобы уменьшить количество расчетов вам может захотеться сделать следующие операции, перед тем, как генерировать инпут-файлы:

  • Отсеять конформации с чрезмерно высокой энергией;
  • Удалить дубликаты;
  • Проверить сохранение констрейнов и отбросить структуры, в которых они нарушены. Например, когда вы ищите конформеры переходного состояния в CREST, полученный конформационный набор может содержать структуры, в которых заданные констрейны сильно нарушены. Если начнете оптимизировать такие геометрии в DFT, то ПС не найдется, а вы потратите время.
  • Проанализировать ансамбль и узнать, какие слабые взаимодействия (водородные связи и т.п.) могут образовываться в вашей молекуле. Найденные контакты можно учесть в дополнительных запусках конформационного поиска с констрейнами, чтобы лучше перебрать варианты с данными взаимодействиями.
  • Объединить конформационные ансамбли из нескольких расчетов, полученные, например, как в пункте выше, и отсортировать структуры по их энергии.

Этот мануал посвящен моей библиотеке pyxyz, которая позволяет решать эти задачи несколькими строчками в Python. Также этот мануал может быть полезен при анализе XYZ-файлов, полученных в результате расчета молекулярной динамики.

Установка библиотеки

Для работы под Linux требуется Python версии 3.7 или выше. Для работы под Windows требуется Python версии 3.8 или выше. Из библиотек для Python будет нужен только Numpy.

Для установки на Linux нужно выполнить в терминале следующую команду:

pip install -i https://test.pypi.org/simple/ "pyxyz<1.0"

Для установки на Windows нужно выполнить в командной строке эту строчку:

pip install -i https://test.pypi.org/simple/ "pyxyz>=1.0"

Обратите внимание, что в некоторых случаях может понадобиться заменить pip на pip3 (это зависит от того, как у вас установлен Python).

После вызова pip install проверьте корректность установки: импортируется ли библиотека и выполняются ли тесты:

import pyxyz
import pyxyz.test
pyxyz.test.run_tests()

При импорте pyxyz под Linux может возникнуть ImportError, который исправляется изменение переменной LD_LIBRARY_PATH. Как это исправить библиотека пишет во время загрузки (строчка начинается с [WARNING]).

Ошибка выглядит так:

>>>  import pyxyz
[WARNING] In case you don't have gslcblas installed and get ImportError, call 'export LD_LIBRARY_PATH=/my/path/to/pyxyz:$LD_LIBRARY_PATH'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/xray/.local/lib/python3.8/site-packages/pyxyz/__init__.py", line 10, in <module>
    from confpool import Confpool
ImportError: libgslcblas.so.0: cannot open shared object file: No such file or directory

То есть перед запуском python нужно выполнить эту строчку и ImportError исчезнет (/my/path/to/pyxyz надо заменить на то, что будет у вас):

 export LD_LIBRARY_PATH=/my/path/to/pyxyz:$LD_LIBRARY_PATH

Пример обработки набора конформаций

Архив с примерами -- Pyxyz_manual.zip (File:Pyxyz manual.zip)

В качестве примера рассмотрим молекулу из файла diol_single.xyz и её конфнабор из CREST - diol_crest.xyz (см. архив). Рассмотрим несколько учебных задач, которые могли бы возникнуть на практике. Обратите внимание, что в пределах одного объекта Confpool количество атомов в анализируемых молекулах и последовательность атомов в импортируемых xyz-файлах должны быть постоянными.

Для работы потребуются следующие импорты:

from pyxyz import Confpool
import numpy as np

Для большей интерактивности с библиотекой можно работать не только запуская код из файла, но и через jupyter notebook.

Слияние конфнаборов и сортировка

Допустим, мы знаем, что энергия структуры в файле diol_single.xyz равна -24.961, и мы хотим добавить эту структуру к набору в файле diol_crest.xyz. Для начала, загружаем оба xyz-файла в один объект Confpool:

ensemble = Confpool()
ensemble.include_from_file("diol_crest.xyz", energy=lambda x: float(x.strip()))
ensemble.include_from_file("diol_single.xyz", energy=lambda x: -24.961)

Здесь мы передаем в include_from_file функцию energy, которая из описания структуры в xyz-файле вычленяет её энергию в виде дробного числа. Далее можно отсортировать полученный набор и сохранить его в файл:

ensemble.sort()
ensemble.update_description(lambda e, d: "Energy = {:6f} a.u.".format(e))
ensemble.save("result.xyz")

Структура из файла "diol_single.xyz" не имела комментария, поэтому мы изменили формат описания для всех структур в наборе функцией update_description. Желаемый формат комментария передается через lambda-функцию, где аргумент e - это энергия, а d - это старое описание.

Полезно понимать, что описание структуры и её энергия - это две разных характеристики структуры. Первое нужно, чтобы записываться в xyz-файл при вызове save, а вторая является ключём для сортировки при вызове sort. При этом энергию можно получать из описания на этапе парсинга xyz-файла, а обновлять описание к структурам можно с помощью функции update_description.

Анализ геометрии

Допустим, мы хотим узнать, сколько структур в наборе diol_crest.xyz имеют торсионный угол (5,6,7,19) близкий к 180. Для начала, готовим объект Confpool:

ensemble = Confpool()
ensemble.include_from_file("diol_crest.xyz", energy=lambda x: float(x.strip()))

И затем пользуемся функцией dihedral_count, чтобы узнать количество структур:

d = [5, 6, 7, 19]
n_conf = ensemble.dihedral_count(d[0], d[1], d[2], d[3], lambda x: 180.0 - abs(x) < 10.0)
# Или укороченный эквивалент:
n_conf = ensemble.dihedral_count(*d, lambda x: 180.0 - abs(x) < 10.0)
print("Number of structures = {}".format(n_conf))

Требование на двугранный угол выражается функцией lambda x: 180.0 - abs(x) < 10.0 - то есть отклонение от 180 по абсолютному значению должно быть не более 10 градусов (значения торсионных углов считаются в диапазоне от -180 до +180). Обратите внимание, что распаковка листа на аргументы делается короче, если пользоваться звёздочкой *.

Можно не только узнать количество структур, но и отфильтровать интересующие. Например, так выглядит код для фильтрования структур с контактами между парой атомов (1, 19) и сохранения результата в файл:

ensemble.distance_filter(1, 19, lambda x: x < 2.0)
ensemble.save("result2.xyz")

Передача данных в Python

Все данные, которые содержатся в объекте Confpool, доступны из кода Python. Количество структур внутри Confpool можно получить с помощью метода size:

print("We have {} structures".format(ensemble.size()))

Данные о структурах можно получить из их индекса путем вызова функции get_structure, но учтите, что нумерация начинается с нуля. Например, в ансамбле, после применения sort(), самая низкая по энергии структура будет иметь индекс 0:

ensemble.sort()
best_conf = ensemble.get_structure(0)

Полученный объект best_conf является словарем с тремя ключами: 'energy', 'descr' и 'xyz' (содержат энергию, описание и координаты, соответственно). Для того, чтобы полноценно работать со структурой, нужны ещё типы атомов; их можно получить в виде листа путем вызова get_atom_symbols. В качестве примера, можно сгенерировать структуру best_conf в xyz-формате:

atom_symbols = ensemble.get_atom_symbols()
for i, symbol in enumerate(atom_symbols):
    print("%2s %14.6f %14.6f %14.6f" % (symbol, *best_conf['xyz'][i]))

Основные принципы работы с библиотекой мы рассмотрели, а в следующем разделе приведено конкретное описание всех возможностей класса Confpool.

Подробное описание функционала библиотеки

Описание методов класса Confpool приведено по следующей схеме:

Имя функции(название аргумента: тип аргумента, ...) -> тип возвращаемого значения

Во всех примерах объект Confpool имеет название e. То есть он создавался строчкой:

e = Confpool()

Добавление структур в набор

include_from_file(filename: str, energy=None or function) -> None, energy(descr: str) -> float
Загрузка структур из XYZ-файле filename и (опционально) парсинг энергии из описания каждой структуры, с помощью функции energy.
filename - имя xyz-файла для импорта;
energy - keyword, который можно игнорировать или передавать функцию для выделения энергии из строчки описания в xyz-файле.
 
Пример, когда в описании к структуре энергия дается, как число без дополнительных подписей:
e.include_from_file("file.xyz", energy=lambda x: float(x.strip()))

Изменение данных

sort() -> None
Сортировка ансамбля по возрастанию энергии.
Пример: e.sort()
update_description(f: function) -> None, f(e: float, d: str) -> str
Используя функцию f сгенерировать новые описания к структурам из их энергии (float, первый аргумент f) и старого описания (str, второй аргумент f).
Пример: e.update_description(lambda ener, d: "E = {} a.u. (old descr = {})".format(ener, d))

Фильтры:

energy_filter(e_cutoff: float, etype='kcal/mol' or 'hartree'/'a.u.') -> None
Отбросить все конформеры, энергия которых выше энергии минимального на e_cutoff. В случае, когда etype=kcal/mol, разницы по энергии переводятся из a.u. в kcal/mol, после чего производится сравнение с e_cutoff. Во всех случаях предполагается, что энергия дана в a.u. Дефолтный etype=kcal/mol.
Пример: e.energy_filter(5.0, etype='kcal/mol')
distance_filter(idxA: int, idxB: int, condition: function) -> None, condition(dist: float) -> bool
Отбросить все конформеры, расстояние между атомами idxA и idxB которых не удовлетворяет условию condition. Функция condition получает на вход расстояние между атомами и, если она возвращает False, то структура отбрасывается.
Пример, в котором отбросятся структуры с расстоянием между атомами 1 и 2 больше, чем 1.7:
e.distance_filter(1, 2, lambda x: x < 1.7)
valence_filter(idxA: int, idxB: int, idxC: int, condition: function) -> None, condition(angle: float) -> bool
Отбросить все конформеры, валентный угол между атомами idxA, idxB и idxC которых не удовлетворяет условию condition. Функция condition получает на вход угол между атомами в градусах и, если она возвращает False, то структура отбрасывается.
Пример, в котором отбросятся структуры в углами между атомами 1, 2 и 3 близкими к развернутому:
e.valence_filter(1, 2, 3, lambda a: 180.0 - a < 10.0)
dihedral_filter(idxA: int, idxB: int, idxC: int, idxD: int, condition: function) -> None, condition(angle: float) -> bool
Отбросить все конформеры, торсионный угол между атомами idxA, idxB, idxC и idxD которых не удовлетворяет условию condition. Функция condition получает на вход угол между атомами в градусах (между -180 и 180) и, если она возвращает False, то структура отбрасывается.
Пример, в котором отбросятся структуры в торсионными углами между атомами 1, 2, 3 и 4 близкими к нулю:
e.dihedral_filter(1, 2, 3, 4, lambda a: abs(a) < 10.0)

Получение данных

save(filename: str) -> None
Выписать данный конформационный набор в файл с название filename в формате XYZ. Струтуры будут выписаны в определенном порядке, например, в котором они находились в ранее импортированном файле или порядке после сортировки по энергии. В полученном XYZ файле будут включены описания к каждой структуре.
Пример: e.save("result.xyz")
size() -> int
Возвращает количество структур в данном конформационном наборе.
Пример: print("We have {} structures".format(e.size()))
get_structure(index: int) -> dict = {'energy': float, 'descr': str, 'xyz': np.array}
Возвращает данные о структуре с индексом index. Нумерация структур начинается с нуля, то есть 0 <= index <= e.size() - 1. Данные передаются в виде словаря с ключами 'energy' (энергия), 'descr' (описание) и 'xyz' (numpy матрица N на 3).
Пример: print("The first structure has energy {}".format(e.get_structure(0)['energy']))
get_atom_symbols() -> list of str
Возвращает лист из символов атомов.

Подсчет структур:

energy_count(e_cutoff: float, etype='kcal/mol' or 'hartree'/'a.u.') -> int
Возвращает количество структур в пределах данного e_cutoff от структуры с минимальной энергией (см. подробности в energy_filter).
distance_count(idxA: int, idxB: int, condition: function) -> None, condition(dist: float) -> bool
Возвращает количество структур, удовлетворяющих условию condition на длину связей между атомами idxA и idxB (см. подробности в distance_filter).
valence_count(idxA: int, idxB: int, idxC: int, condition: function) -> None, condition(angle: float) -> bool
Возвращает количество структур, удовлетворяющих условию condition на валентный угол между атомами idxA, idxB и idxC (см. подробности в valence_filter).
dihedral_count(idxA: int, idxB: int, idxC: int, idxD: int, condition: function) -> None, condition(angle: float) -> bool
Возвращает количество структур, удовлетворяющих условию condition на торсионный угол между атомами idxA, idxB, idxC и idxD (см. подробности в dihedral_filter).

Сборка pyxyz из исходного кода

Этот раздел может быть полезным (1) если вы хотите добавить в pyxyz новую функцию или нашли баг, (2) если вам нужно собрать библиотеку под новую версию Python или под нестандартную операционную систему, (3) хотите научиться делать Python-модули с помощью pybind11/C++.

При загрузке на pyxyz на pypi мной используется немного странное соглашение о версии библиотеки: a.b.c, где а - это 1 (версия под Windows) или 0 (версия под Linux), b - обозначение версии Python (37, 38, 39 или 310), c - собственно версия кода библиотеки. Например, pyxyz версии 1.310.3 собирается для работы под Windows на Python 3.10 (и номер ревизии кода = 3). Когда хотите загрузить на pypi новую ревизию кода, увеличивайте номер ревизии (c) в файле setup.py (директории release_files и setup.py):

version='1.3{ver}.1', # здесь версия равна 1 (правая единица)

Сборка под Linux проверена на GCC 10.3.1, а под Windows - на GCC 12.1.0.

Сборка под Linux

Пререквизиты: make, cmake, g++, gsl, boost, pkg-config. Чтобы собрать библиотеку на одном компьютере без цели её распространения (для выполнения конкретной задачи или в процессе разработки), выполните следующее:

git clone --recurse-submodules https://github.com/nkrivoshchapov/confpool.git
cd confpool
python build_script.py 8 # Восьмерка соответствует x в версии Python3.x (может быть 8, 9 или 10)
cd release
pip install .

Готово!

Если стоит задача загрузить библиотеку на pypi, то понадобится ещё и conda, в которой должны быть созданы отдельные среды с разными версиями Python:

conda create -n py38 python=3.8 -y
conda activate py38
pip install numpy twine
conda deactivate
# Повторить для Python3.x (x = 7, 9, 10)

Таким образом, должно быть создано четыре среды: py37, py38, py39 и py310. Далее скачиваем исходный код:

git clone --recurse-submodules https://github.com/nkrivoshchapov/confpool.git
cd confpool

Затем для каждой версии Python выполняем сборку библиотеки и загружаем соответствующую версию на pypi:

conda activate py38
python build_script.py 8 # Восьмерка соответствует x в версии Python3.x
cd release
python setup.py sdist
twine upload --repository testpypi dist/* # Ввести логин и пароль
conda deactivate

Повторите тоже самое для остальных версий Python.

После запусков twine вам будет выписываться ссылка типа https://test.pypi.org/project/pyxyz/0.310.3/, по которой можно будет посмотреть команду для скачивания только что собранной версии. Более того, по аналогии с

pip install -i https://test.pypi.org/simple/ "pyxyz<1.0"

можно написать универсальную команду, чтобы устанавливать независимо от версии Python.

Сборка под Windows

Установите себе MSYS2 по ссылке (там надо нажать на 32-bit или 64-bit, а потом скачать свежий exe-шник msys2-x86_64-???.exe). После установки запустите через Пуск терминал MSYS2 MinGW x64 и установите некоторые пререквизиты:

pacman -Syu
pacman -S --needed base-devel mingw-w64-x86_64-toolchain
pacman -S mingw-w64-x86_64-gsl mingw-w64-x86_64-boost mingw-w64-x86_64-cmake mingw-w64-x86_64-pybind11 git

Стоит отметить, что MSYS2 и установленные только что библиотеки нужны только для сборки pyxyz под Windows, но не требуются для его использования.

Также нужно, чтобы у вас была установлена Anaconda (или Miniconda). Узнайте расположение файла conda.exe и добавьте в файл C:\msys64\home\*myusername*\.bash_profile следующую строчку:

eval "$('/c/tools/Anaconda3/Scripts/conda.exe' 'shell.bash' 'hook')"
# Проверьте путь к conda.exe!

Как и в Linux может быть два варианта сборки: только для своего компьютера и для загрузки на pypi. Инструкции для своего компьютера:

git clone --recurse-submodules https://github.com/nkrivoshchapov/confpool.git
cd confpool
python winbuild_script.py 8 # Восьмерка соответствует x в версии Python3.x (может быть 8, 9 или 10)
cd release
pip install .

Готово! Теперь даже из командной строки Anaconda (вне MSYS2) можно будет вызывать pyxyz.

Как и c линуксом, если стоит задача загрузить библиотеку на pypi, то понадобится создать отдельные среды в conda с разными версиями Python:

conda create -n py38 python=3.8 -y
conda activate py38
pip install numpy twine
conda deactivate
# Повторить для Python3.x (x = 9, 10)

Таким образом, должно быть создано три среды: py38, py39 и py310. Далее скачиваем исходный код:

git clone --recurse-submodules https://github.com/nkrivoshchapov/confpool.git
cd confpool

Затем для каждой версии Python выполняем сборку библиотеки и загружаем соответствующую версию на pypi. Шаги по загрузке на pypi делаются не в MSYS2, а в обычной командной строке Anaconda, потому что MSYS2 зависает на стадии ввода пароля при загрузке через twine.

conda activate py38
python winbuild_script.py 8 # Восьмерка соответствует x в версии Python3.x
conda deactivate
# В отдельном окне командной строки Anaconda (уже не MSYS2):
conda activate py38
cd *mypath*/confpool/release
python setup.py sdist
twine upload --repository testpypi dist/* # Ввести логин и пароль
conda deactivate

Повторяем эти шаги для версий Python 3.9 и 3.10. Универсальная ссылка на скачивание под Windows делается по следующему принципу:

pip install -i https://test.pypi.org/simple/ "pyxyz>=1.0"

TODO

1. RMSD filtering

2. Build connectivity graphs & search for VdW contacts

3. Complex filtering conditions (how to implement?)

4. Complex description templates (how to implement?)

TODO: include_from_dict

TODO: copy constructor, operator+