mirror of
https://github.com/OpenFreeEnergy/openfe.git
synced 2026-06-04 14:14:22 +08:00
Merge branch 'main' into add/contributing
This commit is contained in:
20
.git-blame-ignore-revs
Normal file
20
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,20 @@
|
||||
# https://github.com/OpenFreeEnergy/openfe/pull/1604 - ruff formatting part 1
|
||||
3a08b6809fc57662e4146db3c7ccedfbc7c7c8df
|
||||
|
||||
# https://github.com/OpenFreeEnergy/openfe/pull/1610 - ruff formatting part 2
|
||||
2311a2f2d956dd30e95c180841ce19b921d89e1f
|
||||
|
||||
# https://github.com/OpenFreeEnergy/openfe/pull/1622 - ruff formatting part 3
|
||||
d7196d119e2f65d88e488afc665f2521e4f68042
|
||||
|
||||
# https://github.com/OpenFreeEnergy/openfe/pull/1623 - ruff formatting part 4
|
||||
036869ae81670c6dcfa2532f125ee88c3f35936c
|
||||
|
||||
# https://github.com/OpenFreeEnergy/openfe/pull/1665 - ruff isort
|
||||
588f552ca9200a99fd77aed993ea3766b154ee53
|
||||
|
||||
# https://github.com/OpenFreeEnergy/openfe/pull/1667 - ruff f-strings and whitespace
|
||||
18f211db974cdde38a5d88d6e74aaaf78fda8897
|
||||
|
||||
# https://github.com/OpenFreeEnergy/openfe/pull/1668 - ruff pycodestyle changes
|
||||
b693d37c8ac0e30283bd8b5f13386fdc98901cf8
|
||||
@@ -3,13 +3,16 @@ Checklist for releasing a new version of openfe.
|
||||
-->
|
||||
|
||||
Make the PR:
|
||||
* [ ] Create a new release-prep branch corresponding to the version name, e.g. `release-prep-v1.2.0`. Note: please follow [semantic versioning](https://semver.org/).
|
||||
* [ ] Create a new release prep branch corresponding to the version name, e.g. `release/v1.2.0`. Note: please follow [semantic versioning](https://semver.org/).
|
||||
* [ ] Check that all user-relevant updates are included in the `news/` rever `.rst` files. You can backfill any additional items by making a new .rst, e.g. `backfill.rst`
|
||||
* [ ] Run [rever](https://regro.github.io/rever-docs/index.html#), e.g. `rever 1.2.0`. This will auto-commit `docs/CHANGELOG.md` and remove the `.rst` files from `news/`.
|
||||
* [ ] Verify that`docs/CHANGELOG.rst` looks correct.
|
||||
* [ ] Verify that`docs/CHANGELOG.rst` looks correct and that it renders as expected in the docs preview.
|
||||
* [ ] If needed, create a release of the [example notebooks repository](https://github.com/OpenFreeEnergy/ExampleNotebooks) and update the pinned release version in the `openfe/docs/conf.py`.
|
||||
* [ ] Make the PR and verify that CI/CD passes.
|
||||
* [ ] [feedstock packaging tests](https://github.com/OpenFreeEnergy/openfe/actions/workflows/release-prep-feedstock.yaml)
|
||||
* [ ] [example notebooks](https://github.com/OpenFreeEnergy/openfe/actions/workflows/release-prep-examplenotebooks.yaml)
|
||||
* [ ] [GPU tests](https://github.com/OpenFreeEnergy/openfe/actions/workflows/aws-gpu-integration-tests.yaml)
|
||||
* [ ] Merge the PR into `main`.
|
||||
* [ ] Make a PR into the [example notebooks repository](https://github.com/OpenFreeEnergy/ExampleNotebooks) to update the version used in `showcase/openfe_showcase.ipynb` and `.binder/environment.yml`
|
||||
|
||||
After Merging the PR [follow this guide](https://github.com/OpenFreeEnergy/openfe/wiki/How-to-create-a-new-release)
|
||||
|
||||
|
||||
10
.github/pull_request_template.md
vendored
10
.github/pull_request_template.md
vendored
@@ -12,7 +12,15 @@ see https://regro.github.io/rever-docs/news.html for details on how to add news
|
||||
-->
|
||||
|
||||
Checklist
|
||||
* [ ] Added a ``news`` entry
|
||||
* [ ] All new code is appropriately documented (user-facing code _must_ have complete docstrings).
|
||||
* [ ] Added a ``news`` entry, or the changes are not user-facing.
|
||||
* [ ] Ran pre-commit: you can run [pre-commit](https://pre-commit.com) locally or comment on this PR with `pre-commit.ci autofix`.
|
||||
|
||||
Manual Tests: these are slow so don't need to be run every commit, only before merging and when relevant changes are made (generally at reviewer-discretion).
|
||||
* [ ] [GPU integration tests](https://github.com/OpenFreeEnergy/openfe/actions/workflows/aws-gpu-integration-tests.yaml)
|
||||
* [ ] [example notebook testing](https://github.com/OpenFreeEnergy/openfe/actions/workflows/release-prep-examplenotebooks.yaml)
|
||||
* [ ] [packaging tests](https://github.com/OpenFreeEnergy/openfe/actions/workflows/cron-package-test.yaml): run this for any large feature PRs or PRs that add test data.
|
||||
|
||||
|
||||
## Developers certificate of origin
|
||||
- [ ] I certify that this contribution is covered by the MIT License [here](https://github.com/OpenFreeEnergy/openfe/blob/main/LICENSE) and the **Developer Certificate of Origin** at <https://developercertificate.org/>.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: CPU Long Tests (manual dispatch)
|
||||
name: "manual AWS: CPU long tests"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -60,6 +60,7 @@ jobs:
|
||||
espaloma_charge==0.0.8
|
||||
espaloma==0.4.0
|
||||
openeye-toolkits
|
||||
python=3.12
|
||||
|
||||
- name: "Check if OpenMM can get a GPU"
|
||||
run: python -m openmm.testInstallation
|
||||
@@ -91,7 +92,7 @@ jobs:
|
||||
DUECREDIT_ENABLE: 'yes'
|
||||
OFE_INTEGRATION_TESTS: FALSE
|
||||
run: |
|
||||
pytest -n logical -vv --durations=10 openfecli/tests/ openfe/tests/
|
||||
pytest -n logical -vv --durations=10 --runslow src/openfecli/tests/ src/openfe/tests/
|
||||
|
||||
stop-aws-runner:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1,4 +1,4 @@
|
||||
name: GPU Integration Tests (manual dispatch)
|
||||
name: "manual AWS: GPU integration tests"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
id: aws-start
|
||||
uses: omsf/start-aws-gha-runner@v1.0.0
|
||||
with:
|
||||
aws_image_id: ami-0b7f661c228e6a4bb
|
||||
aws_image_id: ami-076a54ed41e67782d
|
||||
aws_instance_type: g4dn.xlarge
|
||||
aws_home_dir: /home/ubuntu
|
||||
aws_root_device_size: 125
|
||||
@@ -63,6 +63,8 @@ jobs:
|
||||
espaloma_charge==0.0.8
|
||||
espaloma==0.4.0
|
||||
openeye-toolkits
|
||||
python=3.12
|
||||
cuda-version=12.8
|
||||
|
||||
- name: "Check if OpenMM can get a GPU"
|
||||
run: python -m openmm.testInstallation
|
||||
@@ -90,12 +92,11 @@ jobs:
|
||||
|
||||
- name: "Run tests"
|
||||
env:
|
||||
# Set the OFE_SLOW_TESTS to True if running a Cron job
|
||||
OFE_SLOW_TESTS: "true"
|
||||
DUECREDIT_ENABLE: 'yes'
|
||||
OFE_INTEGRATION_TESTS: TRUE
|
||||
run: |
|
||||
pytest -n logical -vv --durations=10
|
||||
# The -m flag will only run tests with @pytest.mark.integration
|
||||
pytest -n logical -vv --durations=10 -m integration src/openfecli/tests/ src/openfe/tests/
|
||||
|
||||
stop-aws-runner:
|
||||
runs-on: ubuntu-latest
|
||||
28
.github/workflows/ci.yaml
vendored
28
.github/workflows/ci.yaml
vendored
@@ -1,14 +1,15 @@
|
||||
name: "CI"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
# Skip CI if changed files only affect the following folders
|
||||
# - docs: documentation changes don't need code validation
|
||||
# See here for more details: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-excluding-paths
|
||||
paths-ignore:
|
||||
- "docs/*"
|
||||
- "news/*"
|
||||
- ".readthedocs.yaml"
|
||||
- ".github/workflows/cpu-long-tests.yaml"
|
||||
- ".github/workflows/gpu-integration-tests.yaml"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -34,19 +35,19 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macos-latest"]
|
||||
os: ["ubuntu-latest"]
|
||||
openeye: ["no"]
|
||||
python-version:
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
openeye: ["no"]
|
||||
include: # NOTE: openeye does not yet support 3.13.
|
||||
include:
|
||||
- os: "ubuntu-latest"
|
||||
python-version: "3.11"
|
||||
python-version: "3.13"
|
||||
openeye: "yes"
|
||||
- os: "macos-latest"
|
||||
python-version: "3.12"
|
||||
openeye: "yes"
|
||||
openeye: "no"
|
||||
|
||||
env:
|
||||
OE_LICENSE: ${{ github.workspace }}/oe_license.txt
|
||||
@@ -116,6 +117,19 @@ jobs:
|
||||
# if we add more to this, consider changing to for + env vars
|
||||
python -Ic "import openfe; print(openfe.__version__)"
|
||||
|
||||
- name: Cache Pooch data
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
# linux cache location
|
||||
~/.cache/openfe
|
||||
# osx cache location
|
||||
~/Library/Caches/openfe
|
||||
# When files are added or changed in a pooch registry
|
||||
# bump this key to create a new cache, for example if
|
||||
# the key is pooch-${{ matrix.os }}-1 change it to pooch-${{ matrix.os }}-2
|
||||
key: pooch-${{ matrix.os }}-1
|
||||
|
||||
- name: "Run tests"
|
||||
env:
|
||||
# Set the OFE_SLOW_TESTS to True if running a Cron job
|
||||
|
||||
31
.github/workflows/clean-pr-caches.yaml
vendored
Normal file
31
.github/workflows/clean-pr-caches.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# from https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manage-caches#force-deleting-cache-entries
|
||||
name: "clean up github runner caches on closed pull requests"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Cleanup
|
||||
run: |
|
||||
echo "Fetching list of cache keys"
|
||||
cacheKeysForPR=$(gh cache list --ref $BRANCH --limit 100 --json id --jq '.[].id')
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh cache delete $cacheKey
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
34
.github/workflows/clean_cache.yaml
vendored
34
.github/workflows/clean_cache.yaml
vendored
@@ -1,34 +0,0 @@
|
||||
# from https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||
name: cleanup caches by a branch
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
|
||||
REPO=${{ github.repository }}
|
||||
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "conda_cron"
|
||||
name: "cron: conda builds daily tests"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
@@ -20,11 +20,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ['ubuntu-latest', 'macos-latest']
|
||||
os: ['ubuntu-latest']
|
||||
python-version:
|
||||
- "3.10" # bump to 3.13 after 1.6.0 is released
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
include:
|
||||
- os: "macos-latest"
|
||||
python-version: "3.12"
|
||||
openeye: "no"
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
@@ -36,9 +40,9 @@ jobs:
|
||||
id: latest-version
|
||||
working-directory: openfe_repo
|
||||
run: |
|
||||
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
# slice off the v, ie v0.7.2 -> 0.7.2
|
||||
VERSION=${LATEST_TAG:1}
|
||||
REPO="${{ github.repository }}"
|
||||
VERSION=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/$REPO/releases/latest" | jq -r '.tag_name | ltrimstr("v")')
|
||||
echo $VERSION
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Test Docker Image Building
|
||||
name: "cron: docker image daily tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
86
.github/workflows/cron-feedstock-build-tests.yaml
vendored
Normal file
86
.github/workflows/cron-feedstock-build-tests.yaml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
# tests this openfe commit and gufe main to check for
|
||||
# conda-feedstock build issues
|
||||
name: "cron: weekly feedstock package build tests"
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -leo pipefail {0}
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# 3 am weekly on monday
|
||||
- cron: "0 3 * * MON"
|
||||
|
||||
jobs:
|
||||
test-conda-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout openfe repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: openfe
|
||||
|
||||
- name: Checkout conda-forge feedstock
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: conda-forge/openfe-feedstock
|
||||
path: openfe-feedstock
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install conda-build dependencies
|
||||
run: |
|
||||
pip install pyyaml
|
||||
|
||||
# TODO just checkout the repo where we need it?
|
||||
- name: Copy source code to recipe folder
|
||||
run: cp -r openfe openfe-feedstock/recipe/openfe_source
|
||||
|
||||
- name: Modify feedstock to use local path
|
||||
run: |
|
||||
cd openfe-feedstock
|
||||
|
||||
# Backup original recipe.yaml
|
||||
cp recipe/recipe.yaml recipe/recipe.yaml.bak
|
||||
|
||||
# NOTE: now that we use v1 feedstock, we can use yq to directly parse the YAML here.
|
||||
# Add path after source: and delete url line
|
||||
sed -i '/^source:/a\ path: ./openfe_source' recipe/recipe.yaml
|
||||
sed -i '/^ url:/d' recipe/recipe.yaml
|
||||
|
||||
echo "Modified recipe.yaml:"
|
||||
cat recipe/recipe.yaml
|
||||
|
||||
- name: Run conda-forge build test
|
||||
run: |
|
||||
cd openfe-feedstock
|
||||
python build-locally.py
|
||||
continue-on-error: true
|
||||
id: build_test
|
||||
|
||||
# Uncomment if build_artifacts is needed to troubleshoot build
|
||||
|
||||
# - name: Upload build logs
|
||||
# if: always()
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: conda-build-logs
|
||||
# path: |
|
||||
# openfe-feedstock/build_artifacts/
|
||||
# openfe-feedstock/recipe/recipe.yaml
|
||||
# openfe-feedstock/recipe/recipe.yaml.bak
|
||||
# if-no-files-found: warn
|
||||
|
||||
- name: Check build status
|
||||
if: steps.build_test.outcome == 'failure'
|
||||
run: |
|
||||
echo "❌ Conda forge build test failed. Check the uploaded logs for details."
|
||||
exit 1
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Daily package install tests."
|
||||
name: "cron: package install daily tests"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Check for API breaks
|
||||
name: "PR: griffe check for API breaks"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -27,8 +27,8 @@ jobs:
|
||||
id: check
|
||||
run: |
|
||||
pip install griffe
|
||||
griffe check "openfe" --verbose -a origin/main
|
||||
griffe check "openfecli" --verbose -a origin/main
|
||||
griffe check "openfe" -s src --verbose -a origin/main
|
||||
griffe check "openfecli" -s src --verbose -a origin/main
|
||||
|
||||
- name: Manage PR Comments
|
||||
uses: actions/github-script@v7
|
||||
4
.github/workflows/mypy.yaml
vendored
4
.github/workflows/mypy.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "mypy static type checking"
|
||||
name: "PR: mypy static type checking"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -36,9 +36,7 @@ jobs:
|
||||
cache-downloads-key: downloads-${{ steps.date.outputs.date }}
|
||||
create-args: >-
|
||||
python=3.12
|
||||
rdkit=2023.09.5
|
||||
mypy>=1.17.0
|
||||
types-setuptools
|
||||
|
||||
init-shell: bash
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# You can also reference a tag or branch, but the action may change without warning.
|
||||
|
||||
# Workflow to automate docker image building during the openfe release process.
|
||||
name: Create and publish a Docker image
|
||||
name: "release: create and publish a docker image"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -29,6 +29,47 @@ jobs:
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
# see https://dev.to/mathio/squeezing-disk-space-from-github-actions-runners-an-engineers-guide-3pjg
|
||||
- name: Aggressive cleanup
|
||||
run: |
|
||||
# remove unneeded packages to avoid running out of memory
|
||||
# Remove Java (JDKs)
|
||||
sudo rm -rf /usr/lib/jvm
|
||||
|
||||
# Remove .NET SDKs
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
|
||||
# Remove Swift toolchain
|
||||
sudo rm -rf /usr/share/swift
|
||||
|
||||
# Remove Haskell (GHC)
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
|
||||
# Remove Julia
|
||||
sudo rm -rf /usr/local/julia*
|
||||
|
||||
# Remove Android SDKs
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
|
||||
# Remove Chromium (optional if not using for browser tests)
|
||||
sudo rm -rf /usr/local/share/chromium
|
||||
|
||||
# Remove Microsoft/Edge and Google Chrome builds
|
||||
sudo rm -rf /opt/microsoft /opt/google
|
||||
|
||||
# Remove Azure CLI
|
||||
sudo rm -rf /opt/az
|
||||
|
||||
# Remove PowerShell
|
||||
sudo rm -rf /usr/local/share/powershell
|
||||
|
||||
# Remove CodeQL and other toolcaches
|
||||
sudo rm -rf /opt/hostedtoolcache
|
||||
|
||||
docker system prune -af || true
|
||||
docker builder prune -af || true
|
||||
df -h
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -37,11 +78,9 @@ jobs:
|
||||
- name: Get Latest Version
|
||||
id: latest-version
|
||||
run: |
|
||||
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
echo $LATEST_TAG
|
||||
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
# slice off the v, ie v0.7.2 -> 0.7.2
|
||||
VERSION=${LATEST_TAG:1}
|
||||
REPO="${{ github.repository }}"
|
||||
VERSION=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/$REPO/releases/latest" | jq -r '.tag_name | ltrimstr("v")')
|
||||
echo $VERSION
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Make single-file installers (manual dispatch)
|
||||
name: "release: make single-file installers"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -25,9 +25,9 @@ jobs:
|
||||
- name: Get Latest Version
|
||||
id: latest-version
|
||||
run: |
|
||||
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
# slice off the v, ie v0.7.2 -> 0.7.2
|
||||
VERSION=${LATEST_TAG:1}
|
||||
REPO="${{ github.repository }}"
|
||||
VERSION=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/$REPO/releases/latest" | jq -r '.tag_name | ltrimstr("v")')
|
||||
echo $VERSION
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Create OpenFE Conda-Lock File
|
||||
name: "release: create openfe conda-lock file"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -22,14 +22,10 @@ jobs:
|
||||
# This saves me some time since we only need the latest tag
|
||||
- name: Get latest tag
|
||||
id: latest-version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s -H "Authorization: token $GH_TOKEN" \
|
||||
https://api.github.com/repos/$REPO/tags \
|
||||
| jq -r '.[0].name')
|
||||
VERSION=${LATEST_TAG:1}
|
||||
REPO="${{ github.repository }}"
|
||||
VERSION=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/$REPO/releases/latest" | jq -r '.tag_name | ltrimstr("v")')
|
||||
echo $VERSION
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
51
.github/workflows/release-prep-examplenotebooks.yaml
vendored
Normal file
51
.github/workflows/release-prep-examplenotebooks.yaml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: "release prep: test example notebooks"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -leo pipefail {0}
|
||||
|
||||
jobs:
|
||||
test-example-notebooks:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout openfe repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: openfe
|
||||
|
||||
- name: Checkout example notebooks
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: openfreeenergy/ExampleNotebooks
|
||||
path: example-notebooks
|
||||
|
||||
- name: Setup Micromamba
|
||||
uses: mamba-org/setup-micromamba@v2
|
||||
with:
|
||||
environment-file: openfe/environment.yml
|
||||
environment-name: openfe_env
|
||||
create-args: >-
|
||||
python=3.12
|
||||
nbval
|
||||
init-shell: bash
|
||||
|
||||
- name: Install OpenFE
|
||||
run: python -m pip install --no-deps -e ./openfe
|
||||
|
||||
- name: Environment Information
|
||||
run: |
|
||||
micromamba info
|
||||
micromamba list
|
||||
|
||||
- name: Run example notebooks
|
||||
run: |
|
||||
cd example-notebooks
|
||||
python -m pytest -v --nbval-lax --nbval-cell-timeout=3000 -n auto --dist loadscope
|
||||
99
.github/workflows/release-prep-feedstock.yaml
vendored
Normal file
99
.github/workflows/release-prep-feedstock.yaml
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
# tests this openfe commit with the latest gufe release
|
||||
# meant to be used for release prep to catch feedstock issues before releasing on github
|
||||
name: "release prep: test conda-forge package build"
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -leo pipefail {0}
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# TODO: run when "release prep" label is added
|
||||
|
||||
jobs:
|
||||
test-conda-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout openfe repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: openfe
|
||||
|
||||
- name: Checkout conda-forge feedstock
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: conda-forge/openfe-feedstock
|
||||
path: openfe-feedstock
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install conda-build dependencies
|
||||
run: |
|
||||
pip install pyyaml
|
||||
|
||||
# TODO just checkout the repo where we need it?
|
||||
- name: Copy source code to recipe folder
|
||||
run: cp -r openfe openfe-feedstock/recipe/openfe_source
|
||||
|
||||
- name: Get Latest gufe Version
|
||||
id: latest-gufe-version
|
||||
run: |
|
||||
REPO="openfreeenergy/gufe"
|
||||
VERSION=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/$REPO/releases/latest" | jq -r '.tag_name | ltrimstr("v")')
|
||||
echo $VERSION
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Modify feedstock to use local path and latest gufe
|
||||
uses: mikefarah/yq@master
|
||||
with:
|
||||
cmd: |
|
||||
cd openfe-feedstock
|
||||
|
||||
# Backup original recipe.yaml
|
||||
cp recipe/recipe.yaml recipe/recipe.yaml.bak
|
||||
|
||||
# Add path after 'source:' and delete url line
|
||||
yq -i '.source.path="./openfe_source"' recipe/recipe.yaml
|
||||
yq -i 'del(.source.url)' recipe/recipe.yaml
|
||||
|
||||
# remove existing gufe entry and add the gufe pin we want
|
||||
yq -i 'del(.outputs.[0].requirements.run[] | select(. =="*gufe*"))' recipe/recipe.yaml
|
||||
yq -i '.outputs.[0].requirements.run += "gufe==${{ steps.latest-gufe-version.outputs.VERSION }}"' recipe/recipe.yaml
|
||||
|
||||
|
||||
echo "Modified recipe.yaml:"
|
||||
cat recipe/recipe.yaml
|
||||
|
||||
- name: Run conda-forge build test
|
||||
run: |
|
||||
cd openfe-feedstock
|
||||
python build-locally.py
|
||||
continue-on-error: true
|
||||
id: build_test
|
||||
|
||||
# Uncomment if build_artifacts is needed to troubleshoot build
|
||||
|
||||
# - name: Upload build logs
|
||||
# if: always()
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: conda-build-logs
|
||||
# path: |
|
||||
# openfe-feedstock/build_artifacts/
|
||||
# openfe-feedstock/recipe/recipe.yaml
|
||||
# openfe-feedstock/recipe/recipe.yaml.bak
|
||||
# if-no-files-found: warn
|
||||
|
||||
- name: Check build status
|
||||
if: steps.build_test.outcome == 'failure'
|
||||
run: |
|
||||
echo "❌ Conda forge build test failed. Check the uploaded logs for details."
|
||||
exit 1
|
||||
@@ -1,22 +1,34 @@
|
||||
ci:
|
||||
autofix_commit_msg: |
|
||||
[pre-commit.ci] auto fixes from pre-commit.com hooks
|
||||
|
||||
for more information, see https://pre-commit.ci
|
||||
autofix_prs: true
|
||||
autoupdate_branch: ''
|
||||
autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate'
|
||||
autoupdate_schedule: quarterly
|
||||
# comment / label "pre-commit.ci autofix" to a pull request to manually trigger auto-fixing
|
||||
autofix_prs: false
|
||||
skip: []
|
||||
submodules: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
args: ["--maxkb=900"]
|
||||
- id: check-case-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-symlinks
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
exclude: devtools/installer/construct.yaml # not a true YAML file
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: "v2.7.0"
|
||||
rev: "v2.21.0"
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.9
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [--fix ]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
|
||||
@@ -22,7 +22,7 @@ authors:
|
||||
- family-names: "Eastwood"
|
||||
given-names: "James R. B."
|
||||
orcid: "https://orcid.org/0000-0003-3895-5227"
|
||||
- given-names: "Joshua A."
|
||||
- given-names: "Ashley"
|
||||
family-names: "Mitchell"
|
||||
orcid: 'https://orcid.org/0000-0002-8246-5113'
|
||||
- given-names: "David"
|
||||
|
||||
@@ -39,4 +39,4 @@ Enforcement of this Code of Conduct will be respectful and not include any haras
|
||||
|
||||
You deserve sincere thanks for helping to make this a welcoming, friendly community for all.
|
||||
|
||||
This Code of Conduct was adpated from the [cmelab](https://github.com/cmelab/getting-started/blob/master/wiki/pages/Code_of_Conduct.md).
|
||||
This Code of Conduct was adapted from the [cmelab](https://github.com/cmelab/getting-started/blob/master/wiki/pages/Code_of_Conduct.md).
|
||||
|
||||
33
MANIFEST.in
33
MANIFEST.in
@@ -1,15 +1,18 @@
|
||||
recursive-include openfe/tests/data/ *.sdf
|
||||
recursive-include openfe/tests/data/ *.pdb
|
||||
recursive-include openfe/tests/data/ *.mol2
|
||||
recursive-include openfe/tests/data/ *.xml
|
||||
recursive-include openfe/tests/data/ *.graphml
|
||||
recursive-include openfe/tests/data/ *.edge
|
||||
recursive-include openfe/tests/data/ *.dat
|
||||
recursive-include openfe/tests/data/ *json.gz
|
||||
recursive-include openfe/tests/data/ *json_results.gz
|
||||
include openfecli/tests/data/*.json
|
||||
include openfecli/tests/data/*.tar.gz
|
||||
include openfecli/tests/commands/test_gather/*.tsv
|
||||
recursive-include openfecli/tests/ *.sdf
|
||||
recursive-include openfecli/tests/ *.pdb
|
||||
include openfe/tests/data/openmm_rfe/vacuum_nocoord.nc
|
||||
recursive-include src/openfe/tests/data/ *.sdf
|
||||
recursive-include src/openfe/tests/data/ *.bz2
|
||||
recursive-include src/openfe/tests/data/ *.csv
|
||||
recursive-include src/openfe/tests/data/ *.pdb
|
||||
recursive-include src/openfe/tests/data/ *.mol2
|
||||
recursive-include src/openfe/tests/data/ *.xml
|
||||
recursive-include src/openfe/tests/data/ *.graphml
|
||||
recursive-include src/openfe/tests/data/ *.edge
|
||||
recursive-include src/openfe/tests/data/ *.dat
|
||||
recursive-include src/openfe/tests/data/ *.txt
|
||||
recursive-include src/openfe/tests/data/ *.gz
|
||||
recursive-include src/openfe/tests/data/ *json_results.gz
|
||||
include src/openfecli/tests/data/*.json
|
||||
include src/openfecli/tests/data/*.tar.gz
|
||||
include src/openfecli/tests/commands/test_gather/*.tsv
|
||||
recursive-include src/openfecli/tests/ *.sdf
|
||||
recursive-include src/openfecli/tests/ *.pdb
|
||||
include src/openfe/tests/data/openmm_rfe/vacuum_nocoord.nc
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[](https://github.com/OpenFreeEnergy/openfe/actions/workflows/ci.yaml)
|
||||
[](https://codecov.io/gh/OpenFreeEnergy/openfe)
|
||||
[](https://docs.openfree.energy/en/stable/?badge=stable)
|
||||
[](https://doi.org/10.5281/zenodo.8344248)
|
||||
[](https://doi.org/10.5281/zenodo.17258732)
|
||||
|
||||
|
||||
# `openfe` - A Python package for executing alchemical free energy calculations.
|
||||
|
||||
@@ -4,13 +4,16 @@ Useful if Settings are ever changed in a backwards-incompatible way
|
||||
|
||||
Will expect "rbfe_results.tar.gz" in this directory, will overwrite this file
|
||||
"""
|
||||
from gufe.tokenization import JSON_HANDLER
|
||||
|
||||
import glob
|
||||
import json
|
||||
from openfe.protocols import openmm_rfe
|
||||
import os.path
|
||||
import tarfile
|
||||
|
||||
from gufe.tokenization import JSON_HANDLER
|
||||
|
||||
from openfe.protocols import openmm_rfe
|
||||
|
||||
|
||||
def untar(fn):
|
||||
"""extract tarfile called *fn*"""
|
||||
@@ -20,38 +23,38 @@ def untar(fn):
|
||||
|
||||
def retar(loc, name):
|
||||
"""create tar.gz called *name* of directory *loc*"""
|
||||
with tarfile.open(name, mode='w:gz') as f:
|
||||
with tarfile.open(name, mode="w:gz") as f:
|
||||
f.add(loc, arcname=os.path.basename(loc))
|
||||
|
||||
|
||||
def replace_settings(fn, new_settings):
|
||||
"""replace settings instances in *fn* with *new_settings*"""
|
||||
with open(fn, 'r') as f:
|
||||
with open(fn, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
for k in data['protocol_result']['data']:
|
||||
data['protocol_result']['data'][k][0]['inputs']['settings'] = new_settings
|
||||
for k in data["protocol_result"]["data"]:
|
||||
data["protocol_result"]["data"][k][0]["inputs"]["settings"] = new_settings
|
||||
|
||||
for k in data['unit_results']:
|
||||
data['unit_results'][k]['inputs']['settings'] = new_settings
|
||||
for k in data["unit_results"]:
|
||||
data["unit_results"][k]["inputs"]["settings"] = new_settings
|
||||
|
||||
with open(fn, 'w') as f:
|
||||
with open(fn, "w") as f:
|
||||
json.dump(data, f, cls=JSON_HANDLER.encoder)
|
||||
|
||||
|
||||
def fix_rbfe_results():
|
||||
untar('rbfe_results.tar.gz')
|
||||
untar("rbfe_results.tar.gz")
|
||||
|
||||
# generate valid settings as defaults
|
||||
new_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings()
|
||||
|
||||
# walk over all result jsons
|
||||
for fn in glob.glob('./results/*json'):
|
||||
for fn in glob.glob("./results/*json"):
|
||||
# replace instances of settings within with valid settings
|
||||
replace_settings(fn, new_settings)
|
||||
|
||||
retar('results', 'rbfe_results.tar.gz')
|
||||
retar("results", "rbfe_results.tar.gz")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
fix_rbfe_results()
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Dev script to generate some result jsons that are used for testing
|
||||
|
||||
Generates
|
||||
- ABFEProtocol_json_results.gz
|
||||
- used in abfe_results_json fixture
|
||||
- SepTopProtocol_json_results.gy
|
||||
- used in septop_json fixture
|
||||
- AHFEProtocol_json_results.gz
|
||||
@@ -11,38 +13,49 @@ Generates
|
||||
- MDProtocol_json_results.gz
|
||||
- used in md_json fixture
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
from rdkit import Chem
|
||||
from openff.toolkit import (
|
||||
Molecule, RDKitToolkitWrapper, AmberToolsToolkitWrapper
|
||||
)
|
||||
from openff.toolkit.utils.toolkit_registry import (
|
||||
toolkit_registry_manager, ToolkitRegistry
|
||||
)
|
||||
from openff.units import unit
|
||||
from kartograf.atom_aligner import align_mol_shape
|
||||
from kartograf import KartografAtomMapper
|
||||
|
||||
import gufe
|
||||
from gufe.tokenization import JSON_HANDLER
|
||||
from kartograf import KartografAtomMapper
|
||||
from kartograf.atom_aligner import align_mol_shape
|
||||
from openff.toolkit import AmberToolsToolkitWrapper, Molecule, RDKitToolkitWrapper
|
||||
from openff.toolkit.utils.toolkit_registry import ToolkitRegistry, toolkit_registry_manager
|
||||
from openff.units import unit
|
||||
from rdkit import Chem
|
||||
|
||||
import openfe
|
||||
from openfe.protocols.openmm_afe import (
|
||||
AbsoluteBindingProtocol,
|
||||
AbsoluteSolvationProtocol,
|
||||
)
|
||||
from openfe.protocols.openmm_md.plain_md_methods import PlainMDProtocol
|
||||
from openfe.protocols.openmm_afe import AbsoluteSolvationProtocol
|
||||
from openfe.protocols.openmm_rfe import RelativeHybridTopologyProtocol
|
||||
from openfe.protocols.openmm_septop import SepTopProtocol
|
||||
from openfecli.utils import configure_logger
|
||||
|
||||
# avoid problems with output not showing if queueing system kills a job
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
|
||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||
configure_logger("gufekey", handler=stdout_handler)
|
||||
configure_logger("gufe", handler=stdout_handler)
|
||||
configure_logger("openfe", handler=stdout_handler)
|
||||
configure_logger("openmmtools.multistate.multistatereporter", level=logging.DEBUG, handler=stdout_handler) # fmt: skip
|
||||
configure_logger("openmmtools.multistate.multistatesampler", level=logging.DEBUG, handler=stdout_handler) # fmt: skip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LIGA = "[H]C([H])([H])C([H])([H])C(=O)C([H])([H])C([H])([H])[H]"
|
||||
LIGB = "[H]C([H])([H])C(=O)C([H])([H])C([H])([H])C([H])([H])[H]"
|
||||
|
||||
amber_rdkit = ToolkitRegistry(
|
||||
[RDKitToolkitWrapper(), AmberToolsToolkitWrapper()]
|
||||
)
|
||||
amber_rdkit = ToolkitRegistry([RDKitToolkitWrapper(), AmberToolsToolkitWrapper()])
|
||||
|
||||
|
||||
def get_molecule(smi, name):
|
||||
@@ -54,17 +67,39 @@ def get_molecule(smi, name):
|
||||
|
||||
|
||||
def get_hif2a_inputs():
|
||||
with gzip.open('inputs/hif2a_protein.pdb.gz', 'r') as f:
|
||||
protcomp = openfe.ProteinComponent.from_pdb_file(f, name='hif2a_prot')
|
||||
with gzip.open("inputs/hif2a_protein.pdb.gz", "r") as f:
|
||||
protcomp = openfe.ProteinComponent.from_pdb_file(f, name="hif2a_prot")
|
||||
|
||||
with gzip.open('inputs/hif2a_ligands.sdf.gz', 'r') as f:
|
||||
smcs = [openfe.SmallMoleculeComponent(mol) for mol in
|
||||
list(Chem.ForwardSDMolSupplier(f, removeHs=False))]
|
||||
with gzip.open("inputs/hif2a_ligands.sdf.gz", "r") as f:
|
||||
smcs = [
|
||||
openfe.SmallMoleculeComponent(mol)
|
||||
for mol in list(Chem.ForwardSDMolSupplier(f, removeHs=False))
|
||||
]
|
||||
|
||||
return smcs, protcomp
|
||||
|
||||
|
||||
def execute_and_serialize(dag, protocol, simname):
|
||||
def execute_and_serialize(
|
||||
dag,
|
||||
protocol,
|
||||
simname,
|
||||
new_serialization: bool = False
|
||||
): # fmt: skip
|
||||
"""
|
||||
Execute & serialize a DAG
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dag : gufe.ProtocolDAG
|
||||
The DAG to execute & serialize.
|
||||
protocol : gufe.Protocol
|
||||
The Protocol to which the DAG belongs.
|
||||
simname : str
|
||||
The name of the simulation, used for the serialized file name.
|
||||
new_serialization : bool
|
||||
Whether or not we should use the "new" `to_json` serialization.
|
||||
Default is False (for now).
|
||||
"""
|
||||
logger.info(f"running {simname}")
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workdir = pathlib.Path(tmpdir)
|
||||
@@ -73,22 +108,27 @@ def execute_and_serialize(dag, protocol, simname):
|
||||
shared_basedir=workdir,
|
||||
scratch_basedir=workdir,
|
||||
keep_shared=True,
|
||||
n_retries=3
|
||||
raise_error=True,
|
||||
n_retries=2,
|
||||
)
|
||||
protres = protocol.gather([dagres])
|
||||
|
||||
outdict = {
|
||||
"estimate": protres.get_estimate(),
|
||||
"uncertainty": protres.get_uncertainty(),
|
||||
"protocol_result": protres.to_dict(),
|
||||
"unit_results": {
|
||||
unit.key: unit.to_keyed_dict()
|
||||
for unit in dagres.protocol_unit_results
|
||||
}
|
||||
}
|
||||
if new_serialization:
|
||||
protres.to_json(f"{simname}_json_results.json")
|
||||
|
||||
with gzip.open(f"{simname}_json_results.gz", 'wt') as zipfile:
|
||||
json.dump(outdict, zipfile, cls=JSON_HANDLER.encoder)
|
||||
else:
|
||||
outdict = {
|
||||
"estimate": protres.get_estimate(),
|
||||
"uncertainty": protres.get_uncertainty(),
|
||||
"protocol_result": protres.to_dict(),
|
||||
"unit_results": {
|
||||
unit.key: unit.to_keyed_dict()
|
||||
for unit in dagres.protocol_unit_results
|
||||
}
|
||||
} # fmt: skip
|
||||
|
||||
with gzip.open(f"{simname}_json_results.gz", "wt") as zipfile:
|
||||
json.dump(outdict, zipfile, cls=JSON_HANDLER.encoder)
|
||||
|
||||
|
||||
def generate_md_settings():
|
||||
@@ -109,6 +149,52 @@ def generate_md_json(smc):
|
||||
execute_and_serialize(dag, protocol, "MDProtocol")
|
||||
|
||||
|
||||
def generate_abfe_settings():
|
||||
settings = AbsoluteBindingProtocol.default_settings()
|
||||
settings.solvent_equil_simulation_settings.equilibration_length_nvt = 10 * unit.picosecond
|
||||
settings.solvent_equil_simulation_settings.equilibration_length = 10 * unit.picosecond
|
||||
settings.solvent_equil_simulation_settings.production_length = 10 * unit.picosecond
|
||||
settings.solvent_simulation_settings.equilibration_length = 100 * unit.picosecond
|
||||
settings.solvent_simulation_settings.production_length = 500 * unit.picosecond
|
||||
settings.solvent_simulation_settings.time_per_iteration = 2.5 * unit.ps
|
||||
settings.complex_equil_simulation_settings.equilibration_length_nvt = 10 * unit.picosecond
|
||||
settings.complex_equil_simulation_settings.equilibration_length = 10 * unit.picosecond
|
||||
settings.complex_equil_simulation_settings.production_length = 100 * unit.picosecond
|
||||
settings.complex_simulation_settings.equilibration_length = 100 * unit.picosecond
|
||||
settings.complex_simulation_settings.production_length = 500 * unit.picosecond
|
||||
settings.complex_simulation_settings.time_per_iteration = 2.5 * unit.ps
|
||||
settings.solvent_solvation_settings.box_shape = "dodecahedron"
|
||||
settings.complex_solvation_settings.box_shape = "dodecahedron"
|
||||
settings.solvent_solvation_settings.solvent_padding = 1.5 * unit.nanometer
|
||||
settings.complex_solvation_settings.solvent_padding = 1.0 * unit.nanometer
|
||||
settings.forcefield_settings.nonbonded_cutoff = 0.8 * unit.nanometer
|
||||
settings.protocol_repeats = 3
|
||||
settings.engine_settings.compute_platform = "CUDA"
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
def generate_abfe_json():
|
||||
ligands, protein = get_hif2a_inputs()
|
||||
protocol = AbsoluteBindingProtocol(settings=generate_abfe_settings())
|
||||
sysA = openfe.ChemicalSystem(
|
||||
{
|
||||
"ligand": ligands[0],
|
||||
"protein": protein,
|
||||
"solvent": openfe.SolventComponent(),
|
||||
}
|
||||
)
|
||||
sysB = openfe.ChemicalSystem(
|
||||
{
|
||||
"protein": protein,
|
||||
"solvent": openfe.SolventComponent(),
|
||||
}
|
||||
)
|
||||
|
||||
dag = protocol.create(stateA=sysA, stateB=sysB, mapping=None)
|
||||
execute_and_serialize(dag, protocol, "ABFEProtocol", new_serialization=True)
|
||||
|
||||
|
||||
def generate_ahfe_settings():
|
||||
settings = AbsoluteSolvationProtocol.default_settings()
|
||||
settings.solvent_equil_simulation_settings.equilibration_length_nvt = 10 * unit.picosecond
|
||||
@@ -122,29 +208,25 @@ def generate_ahfe_settings():
|
||||
settings.vacuum_simulation_settings.production_length = 1000 * unit.picosecond
|
||||
settings.lambda_settings.lambda_elec = [0.0, 0.25, 0.5, 0.75, 1.0, 1.0,
|
||||
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
1.0]
|
||||
1.0] # fmt: skip
|
||||
settings.lambda_settings.lambda_vdw = [0.0, 0.0, 0.0, 0.0, 0.0, 0.12, 0.24,
|
||||
0.36, 0.48, 0.6, 0.7, 0.77, 0.85,
|
||||
1.0]
|
||||
1.0] # fmt: skip
|
||||
settings.protocol_repeats = 3
|
||||
settings.solvent_simulation_settings.n_replicas = 14
|
||||
settings.vacuum_simulation_settings.n_replicas = 14
|
||||
settings.solvent_simulation_settings.early_termination_target_error = 0.12 * unit.kilocalorie_per_mole
|
||||
settings.vacuum_simulation_settings.early_termination_target_error = 0.12 * unit.kilocalorie_per_mole
|
||||
settings.vacuum_engine_settings.compute_platform = 'CPU'
|
||||
settings.solvent_engine_settings.compute_platform = 'CUDA'
|
||||
settings.solvent_simulation_settings.early_termination_target_error = 0.12 * unit.kilocalorie_per_mole # fmt: skip
|
||||
settings.vacuum_simulation_settings.early_termination_target_error = 0.12 * unit.kilocalorie_per_mole # fmt: skip
|
||||
settings.vacuum_engine_settings.compute_platform = "CPU"
|
||||
settings.solvent_engine_settings.compute_platform = "CUDA"
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
|
||||
def generate_ahfe_json(smc):
|
||||
protocol = AbsoluteSolvationProtocol(settings=generate_ahfe_settings())
|
||||
sysA = openfe.ChemicalSystem(
|
||||
{"ligand": smc, "solvent": openfe.SolventComponent()}
|
||||
)
|
||||
sysB = openfe.ChemicalSystem(
|
||||
{"solvent": openfe.SolventComponent()}
|
||||
)
|
||||
sysA = openfe.ChemicalSystem({"ligand": smc, "solvent": openfe.SolventComponent()})
|
||||
sysB = openfe.ChemicalSystem({"solvent": openfe.SolventComponent()})
|
||||
|
||||
dag = protocol.create(stateA=sysA, stateB=sysB, mapping=None)
|
||||
|
||||
@@ -156,7 +238,7 @@ def generate_rfe_settings():
|
||||
settings.simulation_settings.equilibration_length = 10 * unit.picosecond
|
||||
settings.simulation_settings.production_length = 250 * unit.picosecond
|
||||
settings.forcefield_settings.nonbonded_method = "nocutoff"
|
||||
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
@@ -167,12 +249,10 @@ def generate_rfe_json(smcA, smcB):
|
||||
mapper = KartografAtomMapper(atom_map_hydrogens=True)
|
||||
mapping = next(mapper.suggest_mappings(smcA, a_smcB))
|
||||
|
||||
systemA = openfe.ChemicalSystem({'ligand': smcA})
|
||||
systemB = openfe.ChemicalSystem({'ligand': a_smcB})
|
||||
systemA = openfe.ChemicalSystem({"ligand": smcA})
|
||||
systemB = openfe.ChemicalSystem({"ligand": a_smcB})
|
||||
|
||||
dag = protocol.create(
|
||||
stateA=systemA, stateB=systemB, mapping=mapping
|
||||
)
|
||||
dag = protocol.create(stateA=systemA, stateB=systemB, mapping=mapping)
|
||||
|
||||
execute_and_serialize(dag, protocol, "RHFEProtocol")
|
||||
|
||||
@@ -191,13 +271,13 @@ def generate_septop_settings():
|
||||
settings.complex_simulation_settings.equilibration_length = 10 * unit.picosecond
|
||||
settings.complex_simulation_settings.production_length = 50 * unit.picosecond
|
||||
settings.complex_simulation_settings.time_per_iteration = 2.5 * unit.ps
|
||||
settings.solvent_solvation_settings.box_shape = 'dodecahedron'
|
||||
settings.complex_solvation_settings.box_shape = 'dodecahedron'
|
||||
settings.solvent_solvation_settings.box_shape = "dodecahedron"
|
||||
settings.complex_solvation_settings.box_shape = "dodecahedron"
|
||||
settings.solvent_solvation_settings.solvent_padding = 1.2 * unit.nanometer
|
||||
settings.complex_solvation_settings.solvent_padding = 1.0 * unit.nanometer
|
||||
settings.forcefield_settings.nonbonded_cutoff = 0.9 * unit.nanometer
|
||||
settings.protocol_repeats = 1
|
||||
settings.engine_settings.compute_platform = 'CUDA'
|
||||
settings.engine_settings.compute_platform = "CUDA"
|
||||
|
||||
return settings
|
||||
|
||||
@@ -222,12 +302,13 @@ def generate_septop_json():
|
||||
|
||||
dag = protocol.create(stateA=sysA, stateB=sysB, mapping=None)
|
||||
execute_and_serialize(dag, protocol, "SepTopProtocol")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
molA = get_molecule(LIGA, "ligandA")
|
||||
molB = get_molecule(LIGB, "ligandB")
|
||||
generate_md_json(molA)
|
||||
generate_abfe_json()
|
||||
generate_ahfe_json(molA)
|
||||
generate_rfe_json(molA, molB)
|
||||
generate_septop_json()
|
||||
|
||||
@@ -4,9 +4,219 @@ Changelog
|
||||
|
||||
.. current developments
|
||||
|
||||
v1.11.1
|
||||
====================
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* Fixed slow response time of CLI commands (`PR #1972 <https://github.com/OpenFreeEnergy/openfe/pull/1972>`_).
|
||||
|
||||
|
||||
|
||||
|
||||
v1.11.0
|
||||
====================
|
||||
|
||||
* **openfe v1.11.0** introduces support for protein-membrane systems both with the Python API and the CLI. See our tutorial `RBFE calculations of a Protein-Membrane System <https://docs.openfree.energy/en/latest/tutorials/rbfe_membrane_protein.html>`_ for details.
|
||||
|
||||
The `ability to resume execution of incomplete transformations <https://docs.openfree.energy/en/v1.10.0/guide/execution/quickrun_execution.html>`_ that was introduced in ``openfe v1.10.0`` is now available for the plain MD and SepTop protocols.
|
||||
|
||||
See below for the full changelog for this release:
|
||||
|
||||
**Added:**
|
||||
|
||||
* Added support for systems with membranes to the following protocols:
|
||||
PlainMDProtocol, RelativeHybridTopologyProtocol, SepTopProtocol, and AbsoluteBindingProtocol (`PR #1561 <https://github.com/OpenFreeEnergy/openfe/pull/1561>`_).
|
||||
* Added support for membrane systems to ``openfe plan-rbfe-network``.
|
||||
Use ``--protein-membrane`` instead of the ``--protein`` argument, and see the tutorial on preparing membrane systems (`PR #1896 <https://github.com/OpenFreeEnergy/openfe/pull/1896>`_).
|
||||
* Added API support for resuming the PlainMDProtocol (`PR #1884 <https://github.com/OpenFreeEnergy/openfe/pull/1884>`_).
|
||||
* Added API support for resuming the SepTopProtocol. (`PR #1949 <https://github.com/OpenFreeEnergy/openfe/pull/1949>`_).
|
||||
* The ``validate`` method for the SepTopProtocol has been implemented.
|
||||
This means that settings and system validation can mostly be done prior to Protocol execuation by calling ``SepTopProtocol.validate(stateA, stateB, mapping=None)`` (`PR #1946 <https://github.com/OpenFreeEnergy/openfe/pull/1946>`_).
|
||||
|
||||
**Changed:**
|
||||
|
||||
* The SepTopProtocol now has a dedicated Analysis unit.
|
||||
At the top level API, this does not change behavior, but if you are directly interfacing with th ProtocolUnits, you will have to account for this change.
|
||||
The SepTopProtocolResult now solely uses the Analysis units (`PR #1937 <https://github.com/OpenFreeEnergy/openfe/pull/1937>`_).
|
||||
* Updated the chemical systems user guide and the defining protocols user guide to reflect recent protocol updates, including adding membrane support (`PR #1933 <https://github.com/OpenFreeEnergy/openfe/pull/1933>`_).
|
||||
* The default value for the Hybrid TopologyProtocol setting ``turn_off_core_unique_exceptions`` has been changed to ``True``.
|
||||
This means 1-4 interactions involving the unique alchemical atoms and core regions will now be interpolated on/off accordingly by default (`PR #1856 <https://github.com/OpenFreeEnergy/openfe/pull/1856>`_).
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* Perses atom mapper and scorer functionality is deprecated, now slated to be removed in ``openfe v1.12``.
|
||||
This includes ``PersesAtomMapper`` and ``default_perses_scorer`` (`PR #1857 <https://github.com/OpenFreeEnergy/openfe/pull/1857>`_).
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* Fix erroneous logging information message which would mention setting up the alchemical system when running simulation or analysis units with the hybrid topology, AHFE or ABFE Protocols (`PR #1915 <https://github.com/OpenFreeEnergy/openfe/pull/1915>`_).
|
||||
* System equality checks on resuming no longer expect complete equality in the force parameters.
|
||||
This fixes a scenario where small changes in precision due to running on different machines would prevent users from restarting their simulations (`PR #1914 <https://github.com/OpenFreeEnergy/openfe/pull/1914>`_).
|
||||
|
||||
|
||||
|
||||
v1.10.0
|
||||
====================
|
||||
|
||||
This release introduces the ability to resume execution of an incomplete transformation using ``openfe quickrun`` with the ``--resume`` flag.
|
||||
See the `quickrun documentation <https://docs.openfree.energy/en/v1.10.0/guide/execution/quickrun_execution.html>`_ details.
|
||||
|
||||
**Added:**
|
||||
|
||||
* Added ``--resume`` flag to ``openfe quickrun``.
|
||||
Quickrun now temporarily caches ``protocolDAG`` information and, when used with the ``--resume`` flag, quickrun will attempt to resume execution of an incomplete transformation (`PR #1848 <https://github.com/OpenFreeEnergy/openfe/pull/1848>`_).
|
||||
* Added API support to resume ``RelativeHybridTopologyProtocol`` simulations (`PR #1774 <https://github.com/OpenFreeEnergy/openfe/pull/1774>`_).
|
||||
* Added API support to resume ``AbsoluteBindingProtocol`` and ``AbsoluteSolvationProtocol`` simulations (`PR #1808 <https://github.com/OpenFreeEnergy/openfe/pull/1808>`_).
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* Perses atom mapper and scorer functionality is deprecated, slated to be removed in ``openfe v2.0``.
|
||||
This includes ``PersesAtomMapper`` and ``default_perses_scorer`` (`PR #1857 <https://github.com/OpenFreeEnergy/openfe/pull/1857>`_).
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* Fixed bug introduced in v1.9.0 to ``openfe gather-abfe --report=raw`` where additional unit results for Setup and Simulation units would be shown.
|
||||
This fix restores the behavior prior to v1.9.0 (`PR #1876 <https://github.com/OpenFreeEnergy/openfe/pull/1876>`_).
|
||||
|
||||
|
||||
|
||||
v1.9.1
|
||||
====================
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* Fixed a bug in Protocol termination for the HybridTop and AFE Protocols which would unnecessarily declare an ``UnboundLocalError``.
|
||||
* Updated ``openfe_analysis`` dependency to fix issue with RMSD analysis (`Issue #1834 <https://github.com/OpenFreeEnergy/openfe/issues/1834>`_).
|
||||
|
||||
|
||||
|
||||
v1.9.0
|
||||
====================
|
||||
|
||||
**Added:**
|
||||
|
||||
* The ``validate`` method for the RelativeHybridTopologyProtocol has been implemented.
|
||||
This means that settings and system validation can mostly be done prior to Protocol execution by calling ``RelativeHybridTopologyProtocol.validate(stateA, stateB, mapping)`` (`PR #1740 <https://github.com/OpenFreeEnergy/openfe/pull/1740>`_).
|
||||
|
||||
* Added ``openfe test --download-only`` flag, which downloads all test data stored remotely to the local cache (`PR #1814 <https://github.com/OpenFreeEnergy/openfe/pull/1814>`_).
|
||||
|
||||
**Changed:**
|
||||
|
||||
* The absolute free energy protocols (AbsoluteBindingProtocol and AbsoluteSolvationProtocol) have been broken into multiple
|
||||
protocol units, allowing for setup, run, and analysis to happen
|
||||
separately in the future when relevant changes to protocol execution are
|
||||
made (`PR #1776 <https://github.com/OpenFreeEnergy/openfe/pull/1776>`_).
|
||||
* The relative free energy protocol (RelativeHybridTopologyProtocol) has been
|
||||
broken into multiple protocol units, allowing for the setup, run, analysis to happen
|
||||
separately (`PR #1773 <https://github.com/OpenFreeEnergy/openfe/pull/1773>`_).
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* Fixed bug in ligand network visualization (such as with ``openfe view-ligand-network``) so that ligand names are no longer cut off by the plot border (`PR #1822 <https://github.com/OpenFreeEnergy/openfe/pull/1822>`_).
|
||||
* Endstates in the RelativeHybridTopologyProtocol are now being created
|
||||
in a manner that allows for isomorphic molecules that differ between
|
||||
endstates to have different parameters (`PR #1772 <https://github.com/OpenFreeEnergy/openfe/pull/1772>`_).
|
||||
|
||||
|
||||
|
||||
v1.8.1
|
||||
====================
|
||||
|
||||
**Added:**
|
||||
|
||||
* Added a progress bar for ``openfe gather`` JSON loading (`PR #1786 <https://github.com/OpenFreeEnergy/openfe/pull/1786>`_).
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* Due to issues with OpenFF's handling of toolkit registries
|
||||
with NAGL, the use of NAGL models (e.g. AshGC) when OpenEye
|
||||
is installed but not requested as the charge backend has been
|
||||
disabled (Issue #1760, `PR #1762 <https://github.com/OpenFreeEnergy/openfe/pull/1762>`_).
|
||||
* Fixed bug in ligand network visualization (such as with ``openfe view-ligand-network``) so that ligand names are no longer cut off by the plot border (`PR #1822 <https://github.com/OpenFreeEnergy/openfe/pull/1822>`_).
|
||||
|
||||
|
||||
|
||||
v1.8.0
|
||||
====================
|
||||
|
||||
**Added:**
|
||||
|
||||
* The ``HybridTopologyFactory`` supports building hybrid OpenMM systems which contain ``CMAPTorsionForces`` on non-alchemical atoms.
|
||||
This should allow for simulations using Amber ff19SB (`PR #1695 <https://github.com/OpenFreeEnergy/openfe/pull/1695>`_).
|
||||
* Added experimental features ``openfe gather-septop`` and ``openfe gather-abfe``, which are analogous to ``openfe gather`` and allow for gathering results generated by the Separated Topologies and Absolute Binding Free Energy protocols, respectively. These commands are experimental and are liable to be changed in a future release.
|
||||
* Emit a clarifying log message when a user gets a warning from JAX (`PR #1585 <https://github.com/OpenFreeEnergy/openfe/pull/1585>`_, fixes `Issue #1499 <https://github.com/OpenFreeEnergy/openfe/issues/1499>`_).
|
||||
* Disable JAX acceleration by default, see https://docs.openfree.energy/en/latest/guide/troubleshooting.html#pymbar-disable-jax for more information (`PR #1694 <https://github.com/OpenFreeEnergy/openfe/pull/1692>`_).
|
||||
* New options have been added to the ``AlchemicalSettings`` of the ``SepTopProtocol``, ``AbsoluteSolvationProtocol`` and ``AbsoluteBindingProtocol``. Notably, these options allow users to control the softcore parameters as well as the use of long range dispersion corrections (`PR #1742 <https://github.com/OpenFreeEnergy/openfe/pull/1742>`_).
|
||||
|
||||
**Changed:**
|
||||
|
||||
* ``openfe gather`` is now more rigorous in extracting ligand names and run types. These are now determined directly from component attributes, rather than relying on naming conventions. (`PR #1691 <https://github.com/OpenFreeEnergy/openfe/pull/1702>`_).
|
||||
* Updated installation docs to recommend ``miniconda`` with ``conda-lock`` as the preferred installation method (`PR #1692 <https://github.com/OpenFreeEnergy/openfe/pull/1692>`_).
|
||||
|
||||
|
||||
|
||||
v1.7.0
|
||||
====================
|
||||
|
||||
This release brings several long awaited features to OpenFE, including the SepTop and ABFE Protocols, as well as the adoption of more computationally efficient settings in the CLI and across the Python API.
|
||||
|
||||
The v1.7.0 release also comes with some API changes and breaks, including:
|
||||
* "CUDA" is now the default platform in the settings, you will need to change this if you run on a non-NVIDIA-powered platform.
|
||||
* The default solvation cutoff is now 1.5 nm, to avoid issues with small boxes when dealing with ligands in solvent. When calculating complexes using the MD or HybridTopology Protocols with the API, you will need to reduce this value to ~ 1 nm to avoid excessively large water boxes.
|
||||
* The API has fully migrated to Pydantic V2 and the ``GufeQuantity`` scheme. This only affects Protocol developers. If needed, please see the `gufe typing documentation <https://gufe.openfree.energy/en/latest/generated/gufe.settings.typing.html>`_ for more details.
|
||||
|
||||
Note that if you want to use NAGL to assign partial charges, you must use ``python >= 3.11``.
|
||||
Python 3.10 support is no longer maintained according to `SPEC 0 <https://scientific-python.org/specs/spec-0000/>`_ guidelines.
|
||||
The openfe lock file and docker and apptainer images use Python 3.12, and so charge assignment with NAGL will work without intervention.
|
||||
|
||||
**Added:**
|
||||
|
||||
* Addition of an Absolute Binding Free Energy Protocol (`PR #1045 <https://github.com/OpenFreeEnergy/openfe/pull/1045>`_).
|
||||
* Added `a cookbook for using jq to inspect JSON files <https://docs.openfree.energy/en/v1.7.0/cookbook/jq_inspection.html>`_.
|
||||
* The AbsoluteSolvationProtocol now properly implements the ``validate`` method,
|
||||
allowing users to verify inputs by calling the method directly (`PR #1572 <https://github.com/OpenFreeEnergy/openfe/pull/1572>`_).
|
||||
* Added a new RBFE protocol based on Separated Topologies (`PR #1057 <https://github.com/OpenFreeEnergy/openfe/pull/1057>`_).
|
||||
|
||||
**Changed:**
|
||||
|
||||
* The default atom mapper used in the CLI has been changed from ``LomapAtomMapper`` to ``KartografAtomMapper`` in line with the recommended defaults from the industry benchmarking paper. Users who wish to continue to use ``LomapAtomMapper`` can do so via the YAML configuration file. See the `documentation <https://docs.openfree.energy/en/latest/tutorials/rbfe_cli_tutorial.html#customize-your-campaign-setup>`_ for details (`PR #1530 <https://github.com/OpenFreeEnergy/openfe/pull/1530>`_).
|
||||
* An improved error message is now shown when a mapping involving a changing constraint length cannot be fixed (`PR #1529 <https://github.com/OpenFreeEnergy/openfe/pull/1529>`_).
|
||||
* The default platform for OpenMM-based Protocols is now CUDA and will fail by default on a non-Nvidia GPU enabled system (`PR #1576 <https://github.com/OpenFreeEnergy/openfe/pull/1576>`_).
|
||||
* Remove unnecessary limit on residues ids (``resids``) when getting mappings from topology in ``topology_helpers.py`` utility module (`PR #1539 <https://github.com/OpenFreeEnergy/openfe/pull/1539>`_).
|
||||
* The relative hybrid topology protocol no longer runs the FIRE minimizer when ``dry=True`` (`PR #1468 <https://github.com/OpenFreeEnergy/openfe/pull/1468>`_).
|
||||
* Units must be explicitly assigned when defining ``Settings`` parameters, and values will be converted to match the default units for a given field. For example, use ``1.0 * units.bar`` or ``"1 bar"`` for pressure, and ``300 * unit.kelvin`` or ``"300 kelvin"`` for temperature.
|
||||
* For protocol developers: ``FloatQuantity`` is no longer supported. Instead, use ``GufeQuantity`` and ``specify_quantity_units()`` to make a ``TypeAlias``. See the `gufe typing documentation <https://gufe.openfree.energy/en/latest/generated/gufe.settings.typing.html>`_ for more details.
|
||||
* The default ``time_per_iteration`` setting of the ``MultiStateSimulationSettings`` class has been increased from 1.0 ps to 2.5 ps as part of the fast settings update (`PR #1523 <https://github.com/OpenFreeEnergy/openfe/pull/1523>`_).
|
||||
|
||||
* The default ``box_shape`` setting of the ``OpenMMSolvationSettings`` class has been changed from ``cubic`` to ``dodecahedron`` to improve simulation efficiency as part of the fast settings update (`PR #1523 <https://github.com/OpenFreeEnergy/openfe/pull/1523>`_).
|
||||
|
||||
* The default ``solvent_padding`` settings of the ``OpenMMSolvationSettings`` class has been increased from 1.2 nm to 1.5 nm to be compatible with the new ``box_shape`` default as part of the fast settings update (`PR #1523 <https://github.com/OpenFreeEnergy/openfe/pull/1523>`_).
|
||||
|
||||
* The default ``nonbonded_cutoff`` setting of the ``OpenMMSystemGeneratorFFSettings`` class has been decreased to 0.9 nm from 1.0 nm, in line with current force fields best practices and our newly validated fast settings (`PR #1523 <https://github.com/OpenFreeEnergy/openfe/pull/1523>`_).
|
||||
|
||||
* When calling the CLI ``openfe plan_rbfe_network``, the ``RelativeHybridTopologyProtocol`` settings now reflects the above "fast" settings updates. This includes;
|
||||
|
||||
* Dodecahedron box solvation
|
||||
* Solvation cutoff of 1.5 nm in solvent-only legs, and 1.0 nm in complex legs
|
||||
* A replica exchange rate of 2.5 ps
|
||||
* A 0.9 nm nonbonded cutoff
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* Deprecated ``openfe.utils.visualization_3D.view_mapping_3d()``. Use the method ``LigandAtomMapping.view_3d()`` instead (`PR #1592 <https://github.com/OpenFreeEnergy/openfe/pull/1592>`_).
|
||||
* Deprecated ``openfe.utils.ligand_utils.get_alchemical_charge_difference()``, which is replaced by ``LigandAtomMapping.get_alchemical_charge_difference()`` in ``gufe`` (`PR #1479 <https://github.com/OpenFreeEnergy/openfe/pull/1479>`_).
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* Charged molecules are now explicitly disallowed in the
|
||||
AbsoluteSolvationProtocol(`PR #1572 <https://github.com/OpenFreeEnergy/openfe/pull/1572>`_).
|
||||
|
||||
|
||||
|
||||
v1.6.1
|
||||
====================
|
||||
This release includes minor fixes and updates to tests.
|
||||
This release includes minor fixes and updates to tests.
|
||||
|
||||
**Added:**
|
||||
|
||||
@@ -37,7 +247,7 @@ This release adds support for OpenMM 8.3.0 and Python 3.13.
|
||||
|
||||
v1.5.0
|
||||
====================
|
||||
This release includes support for openmm 8.2 and numpy v2. Checkpoint interval default frequency has changed, resulting in much smaller file sizes. There are also a few minor changes as a result of migrating to use **konnektor** as the backend for many network generators.
|
||||
This release includes support for openmm 8.2 and numpy v2. Checkpoint interval default frequency has changed, resulting in much smaller file sizes. There are also a few minor changes as a result of migrating to use **konnektor** as the backend for many network generators.
|
||||
|
||||
|
||||
**Added:**
|
||||
@@ -70,7 +280,7 @@ This release includes support for openmm 8.2 and numpy v2. Checkpoint interval d
|
||||
v1.4.0
|
||||
====================
|
||||
|
||||
This release includes significant quality of life improvements for the CLI's ``openfe gather`` command.
|
||||
This release includes significant quality of life improvements for the CLI's ``openfe gather`` command.
|
||||
|
||||
**Added:**
|
||||
|
||||
|
||||
@@ -17,13 +17,11 @@ from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
import sass
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.environment import BuildEnvironment
|
||||
from sphinx.util import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
53
docs/conf.py
53
docs/conf.py
@@ -13,15 +13,15 @@
|
||||
import os
|
||||
import sys
|
||||
from importlib.metadata import version
|
||||
from packaging.version import parse
|
||||
from pathlib import Path
|
||||
from inspect import cleandoc
|
||||
from pathlib import Path
|
||||
|
||||
from git import Repo
|
||||
import nbsphinx
|
||||
import git
|
||||
import nbformat
|
||||
import nbsphinx
|
||||
from packaging.version import parse
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../'))
|
||||
sys.path.insert(0, os.path.abspath("../"))
|
||||
|
||||
|
||||
os.environ["SPHINX"] = "True"
|
||||
@@ -31,7 +31,7 @@ os.environ["SPHINX"] = "True"
|
||||
project = "OpenFE"
|
||||
copyright = "2022, The OpenFE Development Team"
|
||||
author = "The OpenFE Development Team"
|
||||
# don't include patch version (https://github.com/OpenFreeEnergy/openfe/issues/1261)
|
||||
# don't include patch version (https://github.com/OpenFreeEnergy/openfe/issues/1261)
|
||||
version = f"{parse(version('openfe')).major}.{parse(version('openfe')).minor}"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
@@ -55,14 +55,13 @@ extensions = [
|
||||
"nbsphinx_link",
|
||||
"sphinx.ext.mathjax",
|
||||
]
|
||||
suppress_warnings = ["config.cache"] # https://github.com/sphinx-doc/sphinx/issues/12300
|
||||
suppress_warnings = ["config.cache"] # https://github.com/sphinx-doc/sphinx/issues/12300
|
||||
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3.9", None),
|
||||
"numpy": ("https://numpy.org/doc/stable", None),
|
||||
"scipy": ("https://docs.scipy.org/doc/scipy", None),
|
||||
"scikit.learn": ("https://scikit-learn.org/stable", None),
|
||||
"openmm": ("http://docs.openmm.org/latest/api-python/", None),
|
||||
"openmm": ("https://docs.openmm.org/latest/api-python/", None),
|
||||
"rdkit": ("https://www.rdkit.org/docs", None),
|
||||
"openeye": ("https://docs.eyesopen.com/toolkits/python/", None),
|
||||
"mdtraj": ("https://www.mdtraj.org/1.9.5/", None),
|
||||
@@ -103,6 +102,8 @@ exclude_patterns = [
|
||||
]
|
||||
|
||||
autodoc_mock_imports = [
|
||||
"cinnabar",
|
||||
"dill",
|
||||
"MDAnalysis",
|
||||
"matplotlib",
|
||||
"mdtraj",
|
||||
@@ -114,6 +115,7 @@ autodoc_mock_imports = [
|
||||
"openmmforcefields",
|
||||
"psutil",
|
||||
"py3Dmol",
|
||||
"zstandard",
|
||||
]
|
||||
|
||||
# Extensions for the myst parser
|
||||
@@ -147,11 +149,11 @@ html_theme_options = {
|
||||
"navigation_with_keys": False,
|
||||
}
|
||||
html_logo = "_static/OFE-color-icon.svg"
|
||||
html_favicon = '_static/OFE-color-icon.svg'
|
||||
html_favicon = "_static/OFE-color-icon.svg"
|
||||
# temporary fix, see https://github.com/pydata/pydata-sphinx-theme/issues/1662
|
||||
html_sidebars = {
|
||||
"installation": [],
|
||||
"CHANGELOG":[],
|
||||
"CHANGELOG": [],
|
||||
}
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
@@ -182,12 +184,16 @@ sass_out_dir = "_static/css"
|
||||
example_notebooks_path = Path("ExampleNotebooks")
|
||||
try:
|
||||
if example_notebooks_path.exists():
|
||||
repo = Repo(example_notebooks_path)
|
||||
repo.remote('origin').pull()
|
||||
repo = git.Repo(example_notebooks_path)
|
||||
try:
|
||||
repo.remote("origin").pull()
|
||||
except git.exc.GitCommandError:
|
||||
# cannot pull if on a tag
|
||||
pass
|
||||
else:
|
||||
repo = Repo.clone_from(
|
||||
repo = git.Repo.clone_from(
|
||||
"https://github.com/OpenFreeEnergy/ExampleNotebooks.git",
|
||||
branch='oct-2025',
|
||||
branch="2026.04.28",
|
||||
to_path=example_notebooks_path,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -195,7 +201,7 @@ except Exception as e:
|
||||
|
||||
filename = e.__traceback__.tb_frame.f_code.co_filename
|
||||
lineno = e.__traceback__.tb_lineno
|
||||
getLogger('sphinx.ext.openfe_git').warning(
|
||||
getLogger("sphinx.ext.openfe_git").warning(
|
||||
f"Getting ExampleNotebooks failed in {filename} line {lineno}: {e}"
|
||||
)
|
||||
|
||||
@@ -229,14 +235,6 @@ nbsphinx_prolog = cleandoc(r"""
|
||||
~ "/"
|
||||
~ path
|
||||
-%}
|
||||
{%- set colab_url =
|
||||
"http://colab.research.google.com/github/"
|
||||
~ gh_repo
|
||||
~ "/blob/"
|
||||
~ gh_branch
|
||||
~ "/"
|
||||
~ path
|
||||
-%}
|
||||
|
||||
.. container:: ofe-top-of-notebook
|
||||
|
||||
@@ -254,12 +252,5 @@ nbsphinx_prolog = cleandoc(r"""
|
||||
|
||||
:octicon:`download` Download Notebook
|
||||
|
||||
.. button-link:: {{colab_url}}
|
||||
:color: primary
|
||||
:shadow:
|
||||
:outline:
|
||||
|
||||
:octicon:`rocket` Run in Colab
|
||||
|
||||
.. _{{ env.doc2path(env.docname, base=None) }}:
|
||||
""")
|
||||
|
||||
@@ -28,7 +28,7 @@ dependencies:
|
||||
- pip:
|
||||
- git+https://github.com/OpenFreeEnergy/gufe@main
|
||||
- git+https://github.com/OpenFreeEnergy/ofe-sphinx-theme@v0.3.1
|
||||
# pip install these so that conda-forge gufe doesn't pull in ambertools and cause a memory error
|
||||
# pip install these so that we can make sure docs build on main while these packages' docs are under development
|
||||
- git+https://github.com/OpenFreeEnergy/kartograf@main
|
||||
- git+https://github.com/OpenFreeEnergy/konnektor@main
|
||||
- git+https://github.com/OpenFreeEnergy/lomap@main
|
||||
|
||||
@@ -69,6 +69,8 @@ the subcommand name, e.g., ``openfe quickrun --help``, which returns
|
||||
exist, it will be created at runtime.
|
||||
-o PATH Filepath at which to create and write the JSON-
|
||||
formatted results.
|
||||
--resume Attempt to resume this transformation's execution
|
||||
using the cache.
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
For more details on various commands, see the :ref:`cli-reference`.
|
||||
|
||||
@@ -10,7 +10,7 @@ This settings file has a series of sections for customising the different algori
|
||||
For example, the settings file which re-specifies the default behaviour would look like ::
|
||||
|
||||
network:
|
||||
method: plan_minimal_spanning_tree
|
||||
method: generate_minimal_spanning_network
|
||||
mapper:
|
||||
method: LomapAtomMapper
|
||||
settings:
|
||||
@@ -29,7 +29,7 @@ All sections of the file ``network:``, ``mapper:`` and ``partial_charge:`` are
|
||||
|
||||
The settings YAML file is then provided to the ``-s`` option of ``openfe plan-rbfe-network``: ::
|
||||
|
||||
openfe plan-rbfe-network -M molecules.sdf -P protein.pdb -s settings.yaml
|
||||
openfe plan-rbfe-network -M molecules.sdf -p protein.pdb -s settings.yaml
|
||||
|
||||
Customising the atom mapper
|
||||
---------------------------
|
||||
|
||||
@@ -31,7 +31,7 @@ the :class:`.Protocol` may create one or more ``ProtocolDAG``\ s, the
|
||||
:class:`.ProtocolResult` will be made from one or more
|
||||
:class:`.ProtocolDAGResult`\ s. Finally, each :class:`.ProtocolDAGResult`
|
||||
may carry information about multiple :class:`.ProtocolUnitResult`\ s, just a
|
||||
single ``ProtocolDAG`` may involve mutliple ``ProtocolUnit``\ s.
|
||||
single ``ProtocolDAG`` may involve multiple ``ProtocolUnit``\ s.
|
||||
|
||||
.. TODO FUTURE: figure showing the relations of protocol objects and result
|
||||
objects
|
||||
|
||||
@@ -9,12 +9,72 @@ Doing this requires storing and sending the details of the simulation from the l
|
||||
These serialized JSON files are the currency of executing a campaign of simulations and contain all the information required to execute a single simulation.
|
||||
|
||||
To read the ``Transformation`` information and execute the simulation, the command line interface provides the ``openfe quickrun`` command, the full details of which are given in :ref:`the CLI reference section<cli_quickrun>`.
|
||||
Briefly, this command takes in the ``Transformation`` information represented as JSON, then executes a simulation according to those specifications.
|
||||
|
||||
|
||||
Basic quickrun usage
|
||||
--------------------
|
||||
|
||||
The ``quickrun`` command takes in the ``Transformation`` information represented as JSON, then executes a simulation according to those specifications.
|
||||
For example, the following command executes a simulation defined by ``transformation.json`` and produces a results file named ``results.json``.
|
||||
|
||||
::
|
||||
|
||||
openfe quickrun transformation.json -o results.json
|
||||
> openfe quickrun transformation.json -d workdir/ -o workdir/results.json
|
||||
|
||||
The ``-d`` / ``--work-dir`` flag controls where working files (checkpoints, trajectory data, etc...) are written.
|
||||
If it is omitted, the current directory will be used.
|
||||
|
||||
The ``-o`` flag controls where the results file will be written.
|
||||
If it is omitted, results are written to a file named ``<transformation_key>_results.json`` in the working directory, where ``<transformation_key>`` is a unique identifier.
|
||||
|
||||
|
||||
Resuming a halted job
|
||||
---------------------
|
||||
|
||||
When ``openfe quickrun`` starts, it saves a plan of the simulation to a cache file before execution begins:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
<work-dir>/quickrun_cache/dag-cache-<key>.json
|
||||
|
||||
Where ``<key>`` is a unique identifier based on the ``-o`` file path and Transformation.
|
||||
This cache is automatically removed once the job completes.
|
||||
|
||||
If a job is interrupted (e.g. due to a wall-time limit, node failure, or manual cancellation), you can resume the interrupted job by passing the ``--resume`` flag:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
> openfe quickrun transformation.json -d workdir/ -o workdir/results.json --resume
|
||||
|
||||
The planned simulation cache will be used to identify where in the simulation process it left off and, if supported by the Transformation Protocol, how to resume.
|
||||
|
||||
.. note::
|
||||
|
||||
The same ``-d`` / ``--work-dir`` and ``-o`` flag arguments used in the
|
||||
original run must be specified so that ``quickrun`` can locate the cache file.
|
||||
|
||||
If you pass ``--resume`` but no cache file is found (e.g. the job never started), the following warning is printed and a fresh execution begins.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
openfe quickrun was run with --resume, but no cached results found at
|
||||
<path-to-cache-file>. Starting new execution.
|
||||
|
||||
If the cache file is corrupted (e.g. due to an incomplete write at the moment of interruption), ``quickrun --resume`` will raise an error with instructions to rerun the simulation:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
Recovery failed, please remove <work-dir>/quickrun_cache/dag-cache-<key>.json
|
||||
before executing a new transformation simulation.
|
||||
|
||||
If you do not pass the ``--resume`` flag, the code will detect the partially complete transformation and prevent you from accidentally starting a duplicate run.
|
||||
The following error will be raised:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
Transformation has been started but is incomplete. Please remove
|
||||
<work-dir>/quickrun_cache/dag-cache-<key>.json and rerun, or resume
|
||||
execution using the ``--resume`` flag.
|
||||
|
||||
|
||||
Executing within a job submission script
|
||||
@@ -23,7 +83,7 @@ Executing within a job submission script
|
||||
You may need to submit computational jobs to a queueing engine, such as Slurm.
|
||||
The ``openfe quickrun`` command can be used within a submission script as follows:
|
||||
|
||||
::
|
||||
.. code-block:: bash
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
@@ -33,7 +93,7 @@ The ``openfe quickrun`` command can be used within a submission script as follow
|
||||
# activate an appropriate conda environment, or any "module load" commands required to
|
||||
conda activate openfe_env
|
||||
|
||||
openfe quickrun transformation.json -o results.json
|
||||
openfe quickrun transformation.json -d workdir/ -o workdir/results.json
|
||||
|
||||
|
||||
Parallel execution of repeats with Quickrun
|
||||
@@ -43,8 +103,8 @@ Serial execution of multiple repeats of a transformation can be inefficient when
|
||||
Higher throughput can be achieved with parallel execution by running one repeat per HPC job.
|
||||
Most protocols are set up to run three repeats in serial by default, but this can be changed by either:
|
||||
|
||||
1. Defining the protocol setting ``protocol_repeats`` - see the :ref:`protocol configuration guide <cookbook/choose_protocol.nblink>` for more details.
|
||||
2. Using the ``openfe plan-rhfe-network`` (or ``plan-rbfe-network``) command line flag ``--n-protocol-repeats``.
|
||||
1. Defining the protocol setting ``protocol_repeats`` - see the :ref:`protocol configuration guide <cookbook/choose_protocol.nblink>` for more details.
|
||||
2. Using the ``openfe plan-rhfe-network`` (or ``plan-rbfe-network``) command line flag ``--n-protocol-repeats``.
|
||||
|
||||
Each transformation can then be executed multiple times via the ``openfe quickrun`` command to produce a set of repeats.
|
||||
However, **you must use unique results files for each repeat to ensure they don't overwrite each other**.
|
||||
@@ -73,32 +133,71 @@ This should result in the following file structure after execution:
|
||||
|
||||
::
|
||||
|
||||
results_parallel/
|
||||
results_parallel
|
||||
├── results_0
|
||||
│ ├── rbfe_lig_ejm_31_complex_lig_ejm_42_complex
|
||||
│ │ └── shared_RelativeHybridTopologyProtocolUnit-79c279f04ec84218b7935bc0447539a9_attempt_0
|
||||
│ │ ├── checkpoint.nc
|
||||
│ │ ├── simulation.nc
|
||||
│ ├── rbfe_lig_ejm_31_complex_lig_ejm_42_complex.json
|
||||
│ ├── shared_HybridTopologyMultiStateAnalysisUnit-5e0825de1dd045818cdc3428205c1cf7_attempt_0
|
||||
│ │ ├── forward_reverse_convergence.png
|
||||
│ │ ├── ligand_RMSD.png
|
||||
│ │ ├── mbar_overlap_matrix.png
|
||||
│ │ ├── replica_exchange_matrix.png
|
||||
│ │ ├── replica_state_timeseries.png
|
||||
│ │ └── structural_analysis.npz
|
||||
│ ├── shared_HybridTopologyMultiStateSimulationUnit-144be594cf024cb19152cfe5e0b3fb7d_attempt_0
|
||||
│ │ ├── checkpoint.chk
|
||||
│ │ ├── simulation.nc
|
||||
│ │ └── simulation_real_time_analysis.yaml
|
||||
│ └── shared_HybridTopologySetupUnit-01b5afe1972c4e2f9d0943da43b4b19c_attempt_0
|
||||
│ ├── A_db.json
|
||||
│ ├── B_db.json
|
||||
│ ├── hybrid_positions.npy
|
||||
│ ├── hybrid_system.pdb
|
||||
│ └── hybrid_system.xml.bz2
|
||||
├── results_1
|
||||
│ ├── rbfe_lig_ejm_31_complex_lig_ejm_42_complex
|
||||
│ │ └── shared_RelativeHybridTopologyProtocolUnit-a3cef34132aa4e9cbb824fcbcd043b0e_attempt_0
|
||||
│ │ ├── checkpoint.nc
|
||||
│ │ ├── simulation.nc
|
||||
│ ├── rbfe_lig_ejm_31_complex_lig_ejm_42_complex.json
|
||||
│ ├── shared_HybridTopologyMultiStateAnalysisUnit-7986bec616a74929aee85e900535f4a2_attempt_0
|
||||
│ │ ├── forward_reverse_convergence.png
|
||||
│ │ ├── ligand_RMSD.png
|
||||
│ │ ├── mbar_overlap_matrix.png
|
||||
│ │ ├── replica_exchange_matrix.png
|
||||
│ │ ├── replica_state_timeseries.png
|
||||
│ │ └── structural_analysis.npz
|
||||
│ ├── shared_HybridTopologyMultiStateSimulationUnit-18eb295b7123444f9ac66ff3caffcab8_attempt_0
|
||||
│ │ ├── checkpoint.chk
|
||||
│ │ ├── simulation.nc
|
||||
│ │ └── simulation_real_time_analysis.yaml
|
||||
│ └── shared_HybridTopologySetupUnit-3d8ccb1ef5124bd4ba20e0047aad0b5f_attempt_0
|
||||
│ ├── A_db.json
|
||||
│ ├── B_db.json
|
||||
│ ├── hybrid_positions.npy
|
||||
│ ├── hybrid_system.pdb
|
||||
│ └── hybrid_system.xml.bz2
|
||||
└── results_2
|
||||
├── rbfe_lig_ejm_31_complex_lig_ejm_42_complex
|
||||
│ └── shared_RelativeHybridTopologyProtocolUnit-abb2b104151c45fc8b0993fa0a7ee0af_attempt_0
|
||||
│ ├── checkpoint.nc
|
||||
│ ├── simulation.nc
|
||||
└── rbfe_lig_ejm_31_complex_lig_ejm_42_complex.json
|
||||
├── rbfe_lig_ejm_31_complex_lig_ejm_42_complex.json
|
||||
├── shared_HybridTopologyMultiStateAnalysisUnit-ac5fad8ad1fb49598f80018713dce070_attempt_0
|
||||
│ ├── forward_reverse_convergence.png
|
||||
│ ├── ligand_RMSD.png
|
||||
│ ├── mbar_overlap_matrix.png
|
||||
│ ├── replica_exchange_matrix.png
|
||||
│ ├── replica_state_timeseries.png
|
||||
│ └── structural_analysis.npz
|
||||
├── shared_HybridTopologyMultiStateSimulationUnit-73abea21b423444881bd8f21415c937f_attempt_0
|
||||
│ ├── checkpoint.chk
|
||||
│ ├── simulation.nc
|
||||
│ └── simulation_real_time_analysis.yaml
|
||||
└── shared_HybridTopologySetupUnit-79bc9b63321945338a3b69d9f94ee15b_attempt_0
|
||||
├── A_db.json
|
||||
├── B_db.json
|
||||
├── hybrid_positions.npy
|
||||
├── hybrid_system.pdb
|
||||
└── hybrid_system.xml.bz2
|
||||
|
||||
The results of which can be gathered from the CLI using the ``openfe gather`` command, in this case you should direct
|
||||
it to the root directory which includes the repeat results and it will automatically collate the information
|
||||
|
||||
::
|
||||
|
||||
openfe gather results_parallel
|
||||
> openfe gather results_parallel
|
||||
|
||||
Optimizing GPU performance with NVIDIA MPS
|
||||
==========================================
|
||||
@@ -109,4 +208,6 @@ See NVIDIA's documentation on `MPS for OpenFE free energy calculations <https://
|
||||
See Also
|
||||
--------
|
||||
|
||||
For details on inspecting these results, refer to :ref:`userguide_results`.
|
||||
- :ref:`userguide_results` - details on inspecting these results.
|
||||
- :ref:`cli-reference` - full CLI reference for ``openfe quickrun``
|
||||
- :ref:`rbfe_cli_tutorial` - a tutorial on how to use the CLI to run hybrid topology relative binding free energy calculations.
|
||||
|
||||
137
docs/guide/protocols/absolutebinding.rst
Normal file
137
docs/guide/protocols/absolutebinding.rst
Normal file
@@ -0,0 +1,137 @@
|
||||
.. _userguide_abfe_protocol:
|
||||
|
||||
Absolute Binding Protocol
|
||||
=========================
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
The :class:`AbsoluteBindingProtocol <.AbsoluteBindingProtocol>` calculates the absolute binding free energy,
|
||||
which is the free energy difference between a ligand in solution and the ligand bound to a protein.
|
||||
|
||||
The absolute binding free energy is calculated through a thermodynamic cycle.
|
||||
In this cycle, the interactions of the molecule are decoupled, meaning turned off,
|
||||
using a partial annihilation scheme (see below) both in the solvent and in the complex phases.
|
||||
|
||||
Restraints are required to keep the weakly
|
||||
coupled and fully decoupled ligand in the binding site region and thereby reduce the phase
|
||||
space that needs to be sampled. In the :class:`AbsoluteBindingProtocol <.AbsoluteBindingProtocol>`
|
||||
we apply orientational, or Boresch-style, restraints, as described below.
|
||||
|
||||
The absolute binding free energy is then obtained via summation of free energy differences along the thermodynamic cycle.
|
||||
|
||||
.. figure:: img/abfe-cycle.png
|
||||
:scale: 50%
|
||||
|
||||
Thermodynamic cycle for the absolute binding free energy protocol.
|
||||
|
||||
Scientific Details
|
||||
------------------
|
||||
|
||||
Orientational restraints
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Orientational, or Boresch-style, restraints are automatically (unless manually specified) applied between three
|
||||
protein and three ligand atoms using one bond, two angle, and three dihedral restraints.
|
||||
Reference atoms are picked based on different criteria, such as the root mean squared
|
||||
fluctuation of the atoms in a short MD simulation, the secondary structure of the protein,
|
||||
and the distance between atoms, based on heuristics from Baumann et al. [1]_ and Alibay et al. [2]_.
|
||||
Two strategies for selecting protein atoms are available, either picking atoms that are bonded to each other or that can span multiple residues.
|
||||
This can be specified using the ``restraint_settings.anchor_finding_strategy`` settings.
|
||||
|
||||
Partial annihilation scheme
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In the :class:`.AbsoluteBindingProtocol` the coulombic interactions of the molecule are fully turned off (annihilated).
|
||||
The Lennard-Jones interactions are instead decoupled, meaning the intermolecular interactions are turned off, keeping the intramolecular Lennard-Jones interactions.
|
||||
|
||||
The lambda schedule
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Molecular interactions are turned off during an alchemical path using a discrete set of lambda windows.
|
||||
For the transformation in the binding site, the following steps are carried out, starting with the ligand fully interacting in the binding site.
|
||||
|
||||
1. Restrain the ligand using orientational restraints.
|
||||
2. Turn off the electrostatic interactions of the ligand.
|
||||
3. Decouple Lennard-Jones interactions of the ligand.
|
||||
4. Release the restraints of the now dummy ligand analytically.
|
||||
|
||||
The lambda schedule in the solvent phase is similar to the one in the complex, except that no restraints are applied.
|
||||
A soft-core potential is applied to the Lennard-Jones potential to avoid instablilites in intermediate lambda windows.
|
||||
The soft-core potential function from Beutler et al. [3]_ is used by default.
|
||||
The lambda schedule is defined in the ``lambda_settings`` objects ``lambda_elec``, ``lambda_vdw``, and ``lambda_restraints``.
|
||||
|
||||
Simulation overview
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :class:`.ProtocolDAG` of the :class:`.AbsoluteBindingProtocol` contains :class:`.ProtocolUnit`\ s from both the complex and solvent transformations.
|
||||
This means that both legs of the thermodynamic cycle are constructed and run concurrently in the same :class:`.ProtocolDAG`.
|
||||
This is different from the :class:`.RelativeHybridTopologyProtocol` where the :class:`.ProtocolDAG` only runs a single leg of a thermodynamic cycle.
|
||||
If multiple ``protocol_repeats`` are run (default: ``protocol_repeats=3``), the :class:`.ProtocolDAG` contains multiple :class:`.ProtocolUnit`\ s of both complex and solvent transformations.
|
||||
|
||||
Simulation steps
|
||||
""""""""""""""""
|
||||
|
||||
Each :class:`.ProtocolUnit` (whether complex or solvent) carries out the following steps:
|
||||
|
||||
1. Parameterize the system using `OpenMMForceFields <https://github.com/openmm/openmmforcefields>`_ and `Open Force Field <https://github.com/openforcefield/openff-forcefields>`_.
|
||||
2. Equilibrate the fully interacting system using a short MD simulation using the same approach as the :class:`.PlainMDProtocol` (including rounds of NVT and NPT equilibration).
|
||||
3. Create an alchemical system.
|
||||
4. Add orientational restraints to the complex system.
|
||||
5. Minimize the alchemical system.
|
||||
6. Equilibrate and production simulate the alchemical system using the chosen multistate sampling method (under NPT conditions).
|
||||
7. Analyze results for the transformation.
|
||||
|
||||
.. note:: Three different types of multistate sampling (i.e. replica swapping between lambda states) methods can be chosen; HREX, SAMS, and independent (no lambda swaps attempted).
|
||||
By default the HREX approach is selected, this can be altered using ``solvent_simulation_settings.sampler_method`` or ``complex_simulation_settings.sampler_method`` (default: ``repex``).
|
||||
|
||||
Simulation details
|
||||
""""""""""""""""""
|
||||
|
||||
Here are some details of how the simulation is carried out which are not detailed in the :class:`.AbsoluteBindingSettings`:
|
||||
|
||||
* The protocol applies a `LangevinMiddleIntegrator <https://openmmtools.readthedocs.io/en/latest/api/generated/openmmtools.mcmc.LangevinDynamicsMove.html>`_ which uses Langevin dynamics, with the LFMiddle discretization [4]_.
|
||||
* A MonteCarloBarostat is used in the NPT ensemble to maintain constant pressure.
|
||||
|
||||
Getting the free energy estimate
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The free energy differences are obtained from simulation data using the `MBAR estimator <https://www.alchemistry.org/wiki/Multistate_Bennett_Acceptance_Ratio>`_ (multistate Bennett acceptance ratio estimator) as implemented in the `PyMBAR package <https://pymbar.readthedocs.io/en/master/mbar.html>`_.
|
||||
Both the MBAR estimates of the two legs of the thermodynamic cycle, and the overall absolute binding free energy (of the entire cycle) are obtained,
|
||||
which is different compared to the results in the :class:`.RelativeHybridTopologyProtocol` where results from two legs of the thermodynamic cycle are obtained separately.
|
||||
|
||||
In addition to the estimates of the free energy changes and their uncertainty, the protocol also returns some metrics to help assess convergence of the results, these are detailed in the :ref:`multistate analysis section <multistate_analysis>`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
|
||||
**Setting up AFE calculations**
|
||||
|
||||
* :ref:`Defining the Protocol <defining-protocols>`
|
||||
|
||||
|
||||
**Tutorials**
|
||||
|
||||
* :any:`Absolute Binding Free Energies tutorial <../../tutorials/abfe_tutorial>`
|
||||
|
||||
**Cookbooks**
|
||||
|
||||
:ref:`Cookbooks <cookbooks>`
|
||||
|
||||
**API Documentation**
|
||||
|
||||
* :ref:`OpenMM Absolute Binding Free Energy <afe binding protocol api>`
|
||||
* :ref:`OpenMM Protocol Settings <openmm protocol settings api>`
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
* `pymbar <https://pymbar.readthedocs.io/en/stable/>`_
|
||||
* `yank <http://getyank.org/latest/>`_
|
||||
* `OpenMMTools <https://openmmtools.readthedocs.io/en/stable/>`_
|
||||
* `OpenMM <https://openmm.org/>`_
|
||||
|
||||
.. [1] Broadening the Scope of Binding Free Energy Calculations Using a Separated Topologies Approach, H. Baumann, E. Dybeck, C. McClendon, F. Pickard IV, V. Gapsys, L. Pérez-Benito, D. Hahn, G. Tresadern, A. Mathiowetz, D. Mobley, J. Chem. Theory Comput., 2023, 19, 15, 5058–5076
|
||||
.. [2] Evaluating the use of absolute binding free energy in the fragment optimisation process, I. Alibay, A. Magarkar, D. Seeliger, P. Biggin, Commun Chem 5, 105 (2022)
|
||||
.. [3] Avoiding singularities and numerical instabilities in free energy calculations based on molecular simulations, T.C. Beutler, A.E. Mark, R.C. van Schaik, P.R. Greber, and W.F. van Gunsteren, Chem. Phys. Lett., 222 529–539 (1994)
|
||||
.. [4] Unified Efficient Thermostat Scheme for the Canonical Ensemble with Holonomic or Isokinetic Constraints via Molecular Dynamics, Zhijun Zhang, Xinzijian Liu, Kangyu Yan, Mark E. Tuckerman, and Jian Liu, J. Phys. Chem. A 2019, 123, 28, 6056-6079
|
||||
BIN
docs/guide/protocols/img/abfe-cycle.png
Normal file
BIN
docs/guide/protocols/img/abfe-cycle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
@@ -7,6 +7,7 @@ Details on the theory and behaviour of different Protocols are listed here.
|
||||
|
||||
.. toctree::
|
||||
relativehybridtopology
|
||||
absolutebinding
|
||||
absolutesolvation
|
||||
septop
|
||||
plainmd
|
||||
|
||||
@@ -43,7 +43,7 @@ If there is a ``SolventComponent`` in the :class:`.ChemicalSystem`, the each :cl
|
||||
A MonteCarloBarostat is used in the NPT ensemble to maintain constant pressure.
|
||||
Relevant settings under solvent conditions include the solvation settings that control the ``solvent_model`` and ``solvent_padding``.
|
||||
|
||||
If the :class:`.ChemicalSystem` does not contain a ``SolventComponent``, the protocol runs an MD simulation in vacuum. After a minimization, the protocol performs an equilibration, followed by a production run with no periodic boundary conditions and infinite cutoffs. Settings that control the barostat or the solvation are ignored for vaccum MD simulations.
|
||||
If the :class:`.ChemicalSystem` does not contain a ``SolventComponent``, the protocol runs an MD simulation in vacuum. After a minimization, the protocol performs an equilibration, followed by a production run with no periodic boundary conditions and infinite cutoffs. Settings that control the barostat or the solvation are ignored for vacuum MD simulations.
|
||||
|
||||
Performance consideration for gas phase MD simulations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -38,10 +38,11 @@ The :class:`.RelativeHybridTopologyProtocol` uses a hybrid topology approach to
|
||||
ligands, meaning that a single set of coordinates is used to represent the
|
||||
common core of the two ligands while the atoms that differ between the two
|
||||
ligands are represented separately. An atom map defines which atoms belong
|
||||
to the core (mapped atoms) and which atoms are unmapped and represented
|
||||
separately (see :ref:`Creating atom mappings <Creating Atom Mappings>`). During the alchemical transformation, mapped atoms are switched
|
||||
from the type in one ligand to the type in the other ligands, while unmapped
|
||||
atoms are switched on or off, depending on which ligand they belong to.
|
||||
to the core (mapped atoms) and which atoms are unique and represented
|
||||
separately (see :ref:`Creating atom mappings <Creating Atom Mappings>`). During the alchemical transformation, mapped atoms are interpolated
|
||||
from their type in the ligand at state A to the type in the other ligand at state B, while unique atoms
|
||||
atoms (commonly known as dummy atoms) are switched, inserted or uncoupled, depending on which ligand they belong to. By default all nonbonded interactions between the
|
||||
dummy region and the core region are removed to avoid coupling their motion.
|
||||
|
||||
.. note:: In this hybrid topology approach, all bonded interactions between the dummy region and the core region are kept.
|
||||
As pointed out by Fleck et al. [1]_, this can lead to systematic errors if the contribution of the dummy group does not cancel out
|
||||
@@ -74,6 +75,12 @@ Each :class:`.ProtocolUnit` carries out the following steps:
|
||||
2. Create an alchemical system (hybrid topology).
|
||||
3. Minimize the alchemical system.
|
||||
4. Equilibrate and production simulate the alchemical system using the chosen multistate sampling method (under NPT conditions if solvent is present).
|
||||
|
||||
.. note::
|
||||
**Equilibration method:**
|
||||
The current implementation uses a simple equilibration protocol **without any positional restraints** or **temperature annealing**.
|
||||
The system is equilibrated directly under the target thermodynamic conditions, therefore the input structures should be stable under these conditions.
|
||||
|
||||
5. Analyze results for the transformation (for a single leg in the thermodynamic cycle).
|
||||
|
||||
Note: three different types of multistate sampling (i.e. replica swapping between lambda states) methods can be chosen; HREX, SAMS, and independent (no lambda swaps attempted). By default the HREX approach is selected, this can be altered using ``simulation_settings.sampler_method`` (default: ``repex``).
|
||||
@@ -146,7 +153,7 @@ difference produced.
|
||||
this analysis calculates the free energy difference, both in forward and backward directions.
|
||||
In this analysis, forward and backward estimates that agree within error using only a fraction of the total data
|
||||
suggest convergence [5]_. Note: the error bars reported in this plot are
|
||||
MBAR analytical errors instead of boostrap errors.
|
||||
MBAR analytical errors instead of bootstrap errors.
|
||||
- .. image:: img/forward_reverse_convergence.png
|
||||
* - **Timeseries of replica states.**
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _userguide_septop_protocol:
|
||||
|
||||
Separated Topologies Protocol
|
||||
=============================
|
||||
|
||||
@@ -32,7 +34,7 @@ Scientific Details
|
||||
Orientational restraints
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Orientational, or Boresch-style, restraints are automaticallly (unless manually specified) applied between three protein and three ligand atoms using one bond,
|
||||
Orientational, or Boresch-style, restraints are automatically (unless manually specified) applied between three protein and three ligand atoms using one bond,
|
||||
two angle, and three dihedral restraints. Reference atoms are picked based on different criteria, such as the root mean squared
|
||||
fluctuation of the atoms in a short MD simulation, the secondary structure of the protein, and the distance between atoms,
|
||||
based on heuristics from Baumann et al. [2]_ and Alibay et al. [3]_.
|
||||
|
||||
@@ -1,55 +1,201 @@
|
||||
.. _userguide_chemicalsystems_and_components:
|
||||
|
||||
Chemical Systems, Components and Thermodynamic Cycles
|
||||
Components, Chemical Systems and Thermodynamic Cycles
|
||||
=====================================================
|
||||
|
||||
.. _userguide_chemical_systems:
|
||||
|
||||
Chemical Systems
|
||||
----------------
|
||||
|
||||
A :class:`.ChemicalSystem` represents the end state of an alchemical transformation,
|
||||
which can then be input to a :class:`.Protocol`.
|
||||
|
||||
A :class:`.ChemicalSystem` **does** contain the following information (when present):
|
||||
|
||||
* exact atomic information (including protonation state) of protein, ligands, co-factors, and any crystallographic
|
||||
waters
|
||||
* atomic positions of all explicitly defined components such as ligands or proteins
|
||||
* the abstract definition of the solvation environment, if present
|
||||
|
||||
A :class:`.ChemicalSystem` does **NOT** include the following:
|
||||
|
||||
* forcefield applied to any component, including details on water model or virtual particles
|
||||
* thermodynamic conditions (e.g. temperature or pressure)
|
||||
This page describes the core building blocks used to define simulation states in openfe:
|
||||
:class:`.Component`\s, which describe what is physically present in a system;
|
||||
:class:`.ChemicalSystem`\s, which combine components into a complete end state;
|
||||
and thermodynamic cycles, which connect end states via alchemical transformations.
|
||||
|
||||
.. _userguide_components:
|
||||
|
||||
Components
|
||||
----------
|
||||
|
||||
A :class:`.ChemicalSystem` is composed of many 'component' objects, which together define overall system.
|
||||
Components are the composable building blocks that define the chemical
|
||||
composition of a simulated system. Splitting a system into components serves three purposes:
|
||||
|
||||
Examples of components include:
|
||||
1. Alchemical transformations can be easily understood by comparing the differences in components.
|
||||
2. Components can be reused to compose different systems.
|
||||
3. :class:`.Protocol`\s can apply component-specific behaviour, e.g. different force fields per component.
|
||||
|
||||
* :class:`.ProteinComponent`: an entire biological assembly, typically the contents of a PDB file.
|
||||
* :class:`.SmallMoleculeComponent`: typically ligands and cofactors
|
||||
* :class:`.SolventComponent`: solvent conditions
|
||||
|
||||
Splitting the total system into components serves three purposes:
|
||||
Component types — overview
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 25 30 45
|
||||
|
||||
* - Component
|
||||
- Role
|
||||
- Key notes
|
||||
* - :class:`.ProteinComponent`
|
||||
- Biological assembly
|
||||
- Typically the contents of a PDB file. May include crystallographic waters and ions (defined as HETATM entries),
|
||||
and disulfide bonds (defined via CONECT records).
|
||||
* - :class:`.SmallMoleculeComponent`
|
||||
- Ligands and cofactors
|
||||
- Can optionally contain atomic partial charges. If present, those will be used in the simulation.
|
||||
* - :class:`.SolventComponent`
|
||||
- Abstract solvent definition
|
||||
- Defines solvent conditions and ion concentration. Does **not** include coordinates or box vectors. Solvent is added by the protocol at runtime.
|
||||
* - :class:`.SolvatedPDBComponent`
|
||||
- Explicitly solvated system
|
||||
- Includes atomic coordinates and box vectors. Solvent is already present,
|
||||
the protocol does not add any further solvation.
|
||||
* - :class:`.ProteinMembraneComponent`
|
||||
- Protein-membrane complex
|
||||
- Subclass of :class:`.SolvatedPDBComponent`. Includes protein, membrane, solvent,
|
||||
and box vectors. Replaces :class:`.ProteinComponent` in membrane systems.
|
||||
|
||||
.. _userguide_solvation_models:
|
||||
|
||||
Abstract vs explicit solvation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These two approaches are **mutually exclusive**:
|
||||
|
||||
* **Abstract solvation** — use a :class:`.SolventComponent`. The protocol adds solvent
|
||||
during system preparation.
|
||||
* **Explicit solvation** — use a :class:`.SolvatedPDBComponent` or
|
||||
:class:`.ProteinMembraneComponent`. Solvent molecule coordinates (waters and ions) are explicitly defined in the inputs.
|
||||
|
||||
Either define the solvent abstractly, or provide a fully solvated system — do not mix
|
||||
both for the same leg of a transformation.
|
||||
|
||||
.. note::
|
||||
|
||||
Some protocols, such as :class:`.SepTopProtocol` and :class:`.AbsoluteBindingProtocol`,
|
||||
use a single :class:`.ChemicalSystem` to represent both the complex and solvent legs.
|
||||
In this case, a :class:`.ChemicalSystem` may contain both a :class:`.SolventComponent`
|
||||
and a :class:`.ProteinMembraneComponent`. However, these apply to *different* legs: the
|
||||
:class:`.SolventComponent` is used only for the solvent leg, and the
|
||||
:class:`.ProteinMembraneComponent` (which is already explicitly solvated) is used only
|
||||
for the complex leg. The mutual exclusivity rule still holds per leg.
|
||||
|
||||
Box vectors for explicitly solvated systems
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The components :class:`.SolvatedPDBComponent` and :class:`.ProteinMembraneComponent`
|
||||
require periodic box vectors. These can be provided in three ways:
|
||||
|
||||
1. **CRYST record in the PDB file** — OpenMM reads box vectors automatically. No additional arguments are needed::
|
||||
|
||||
membrane_protein = openfe.ProteinMembraneComponent.from_pdb_file('./protein_membrane.pdb')
|
||||
|
||||
2. **Manual specification** — box vectors can be provided explicitly as numpy arrays with OpenFF units in OpenMM format via the ``box_vectors`` argument::
|
||||
|
||||
import numpy as np
|
||||
import openff.units as offunit
|
||||
|
||||
box_vectors = np.array([
|
||||
[6.9587, 0.0, 0.0],
|
||||
[0.0, 5.9164, 0.0],
|
||||
[0.0, 0.0, 9.2692]
|
||||
]) * offunit.nanometer
|
||||
|
||||
membrane_protein = openfe.ProteinMembraneComponent.from_pdb_file(
|
||||
'./protein_membrane.pdb', box_vectors=box_vectors
|
||||
)
|
||||
|
||||
3. **Inference from atomic coordinates** — box vectors can be estimated from the atomic
|
||||
positions by passing ``infer_box_vectors=True``::
|
||||
|
||||
membrane_protein = openfe.ProteinMembraneComponent.from_pdb_file(
|
||||
'./protein_membrane.pdb', infer_box_vectors=True
|
||||
)
|
||||
|
||||
.. warning::
|
||||
|
||||
Inferring box vectors from atomic positions can be inaccurate if the PDB originates
|
||||
from a previous simulation where atoms may be distributed across periodic images.
|
||||
|
||||
|
||||
.. _userguide_chemical_systems:
|
||||
|
||||
ChemicalSystem
|
||||
--------------
|
||||
|
||||
A :class:`.ChemicalSystem` is composed of components that together describe a model of the system to be simulated.
|
||||
simulated system. It represents the **end state** of an alchemical transformation
|
||||
and is the primary input a :class:`.Protocol` consumes to define a simulation state.
|
||||
|
||||
**What a ChemicalSystem defines**
|
||||
|
||||
* Exact atomic information (including protonation state) of protein, ligands,
|
||||
cofactors, and any crystallographic waters.
|
||||
* Atomic positions of all explicitly defined components such as ligands or proteins.
|
||||
* The abstract or explicit definition of the solvent environment (SolventComponent).
|
||||
|
||||
**What a ChemicalSystem does NOT define**, and are instead handled by the Protocol:
|
||||
|
||||
Any simulation parameters including:
|
||||
* Forcefield applied to any component, including water model or virtual particles.
|
||||
* Thermodynamic conditions (e.g. temperature or pressure).
|
||||
* These are handled by the :class:`.Protocol`.
|
||||
|
||||
.. _userguide_system_composition:
|
||||
|
||||
System composition examples
|
||||
---------------------------
|
||||
|
||||
The components that make up each :class:`.ChemicalSystem` depend on the protocol and
|
||||
the nature of the system. The table below summarises the composition for each combination.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Protocol-specific behaviour:
|
||||
For :class:`.SepTopProtocol` and :class:`.AbsoluteBindingProtocol`, a single
|
||||
:class:`.ChemicalSystem` represents both legs of the thermodynamic cycle. The protocol
|
||||
determines internally what is the complex leg and what is the solvent leg.
|
||||
This differs from the :class:`.RelativeHybridTopologyProtocol`, where each leg (e.g. complex and solvent) is defined by
|
||||
separate :class:`.ChemicalSystem`\s. This behaviour is expected to change in future versions.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 20 40 40
|
||||
|
||||
* - System
|
||||
- :ref:`RBFE <userguide_relative_hybrid_topology_protocol>` (:class:`.RelativeHybridTopologyProtocol`)
|
||||
- :ref:`SepTop <userguide_septop_protocol>` / :ref:`ABFE <userguide_abfe_protocol>` (:class:`.SepTopProtocol`, :class:`.AbsoluteBindingProtocol`)
|
||||
* - **Standard protein–ligand**
|
||||
- | **Complex leg:**
|
||||
| :class:`.ProteinComponent` + :class:`.SmallMoleculeComponent`\s + :class:`.SolventComponent`
|
||||
|
|
||||
| **Solvent leg:**
|
||||
| :class:`.SmallMoleculeComponent`\s + :class:`.SolventComponent`
|
||||
- | **Single ChemicalSystem (both legs):**
|
||||
| :class:`.ProteinComponent` + :class:`.SmallMoleculeComponent`\s + :class:`.SolventComponent`
|
||||
* - **Membrane system**
|
||||
- | **Complex leg:**
|
||||
| :class:`.ProteinMembraneComponent` + :class:`.SmallMoleculeComponent`\s
|
||||
| *(no* :class:`.SolventComponent` *— already explicitly solvated)*
|
||||
|
|
||||
| **Solvent leg:**
|
||||
| :class:`.SmallMoleculeComponent`\s + :class:`.SolventComponent`
|
||||
- | **Single ChemicalSystem (both legs):**
|
||||
| :class:`.ProteinMembraneComponent` + :class:`.SmallMoleculeComponent`\s + :class:`.SolventComponent`
|
||||
| *(protocol applies* :class:`.SolventComponent` *only in the solvent leg)*
|
||||
|
||||
1. alchemical transformations can be easily understood by comparing the differences in components.
|
||||
2. components can be reused to compose different systems.
|
||||
3. :class:`.Protocol`\s can have component-specific behavior. E.g. different force fields for each component.
|
||||
|
||||
Thermodynamic Cycles
|
||||
--------------------
|
||||
|
||||
We can now describe a thermodynamic cycle as a set of :class:`.ChemicalSystem`\s.
|
||||
The exact end states to construct are detailed in the :ref:`pages for each specific Protocol <userguide_protocols>`.
|
||||
A thermodynamic cycle can be described as a set of :class:`.ChemicalSystem`\s (nodes) connected by
|
||||
alchemical transformations (edges). The :class:`.Protocol` defines how the
|
||||
:class:`.ChemicalSystem`\s map onto the cycle and how they are used in practice.
|
||||
The same :class:`.ChemicalSystem` can be reused across multiple thermodynamic states
|
||||
depending on the protocol. For details of which end states to construct, consult the
|
||||
:ref:`pages for each specific Protocol <userguide_protocols>`.
|
||||
|
||||
As an example, we can construct the classic relative binding free energy cycle by defining four components: two ligands,
|
||||
a protein, and a solvent:
|
||||
Hybrid topology RBFE example
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As an example, the relative binding free energy cycle requires four
|
||||
:class:`.ChemicalSystem`\s — one for each node in the cycle:
|
||||
|
||||
.. figure:: ../protocols/img/rbfe_thermocycle.png
|
||||
:scale: 40%
|
||||
@@ -75,12 +221,27 @@ a protein, and a solvent:
|
||||
ligand_B_complex = openfe.ChemicalSystem(components={'ligand': ligand_B, 'protein': protein, 'solvent': solvent})
|
||||
# ligand_A + solvent
|
||||
ligand_A_solvent = openfe.ChemicalSystem(components={'ligand': ligand_A, 'solvent': solvent})
|
||||
# ligand_A + solvent
|
||||
# ligand_B + solvent
|
||||
ligand_B_solvent = openfe.ChemicalSystem(components={'ligand': ligand_B, 'solvent': solvent})
|
||||
|
||||
Explicitly solvated variant
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When using a :class:`.SolvatedPDBComponent` or :class:`.ProteinMembraneComponent`, replace :class:`.ProteinComponent`
|
||||
and :class:`.SolventComponent` for the complex leg. No separate :class:`.SolventComponent`
|
||||
is required:
|
||||
|
||||
::
|
||||
|
||||
# explicitly solvated protein-membrane complex (box vectors read from CRYST1 record)
|
||||
protein_membrane = openfe.ProteinMembraneComponent.from_pdb_file('./protein_membrane.pdb')
|
||||
|
||||
# ligand_A + explicitly solvated protein-membrane — no SolventComponent needed
|
||||
ligand_A_complex = openfe.ChemicalSystem(components={'ligand': ligand_A, 'protein_membrane': protein_membrane})
|
||||
|
||||
|
||||
See Also
|
||||
--------
|
||||
|
||||
* To see how to construct a :class:`.ChemicalSystem` \s from your files, see :ref:`the cookbook entry on loading molecules <Loading Molecules>`
|
||||
* For details of what thermodynamic cycles to construct, consult the :ref:`pages for each specific Protocol <userguide_protocols>`
|
||||
* To see how to construct a :class:`.ChemicalSystem` from your files, see :ref:`the cookbook entry on loading molecules <Loading Molecules>`
|
||||
* For details of which thermodynamic cycles to construct, consult the :ref:`pages for each specific Protocol <userguide_protocols>`
|
||||
|
||||
@@ -80,13 +80,13 @@ The centre view shows both molecules overlaid, allowing the spatial corresponden
|
||||
|
||||
from openfe.utils import visualization_3D
|
||||
|
||||
view = visualization_3D.view_mapping_3d(mapping)
|
||||
view = mapping.view_3d()
|
||||
|
||||
|
||||
.. image:: img/3d_mapping.png
|
||||
:width: 90%
|
||||
:align: center
|
||||
:alt: Sample output of view_mapping_3d function
|
||||
:alt: Sample output of view_3d() function
|
||||
|
||||
|
||||
The cartesian distance between pairs of atom mapping is also available via the :meth:`.get_distances()` method.
|
||||
|
||||
@@ -10,6 +10,8 @@ there are multiple available ``Protocol``\s to choose from.
|
||||
|
||||
For example, included in the ``openfe`` package are the following:
|
||||
* :class:`.RelativeHybridTopologyProtocol`
|
||||
* :class:`.AbsoluteBindingProtocol`
|
||||
* :class:`.SepTopProtocol`
|
||||
* :class:`.AbsoluteSolvationProtocol`
|
||||
* :class:`.PlainMDProtocol`
|
||||
|
||||
@@ -47,6 +49,40 @@ For example, to customise the production run length of the RFE Protocol::
|
||||
|
||||
protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings)
|
||||
|
||||
Adaptive Settings
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. warning::
|
||||
|
||||
The ``_adaptive_settings()`` method is experimental and subject to change.
|
||||
|
||||
In addition to the ``.default_settings()`` method, some protocols
|
||||
provide an ``_adaptive_settings`` method. This method generates recommended settings
|
||||
based on properties of the input :class:`.ChemicalSystem`\s and, where required, the :class:`.AtomMapping`.
|
||||
|
||||
For example::
|
||||
|
||||
from openfe.protocols import openmm_rfe
|
||||
|
||||
settings = openmm_rfe.RelativeHybridTopologyProtocol._adaptive_settings(
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
mapping=mapping,
|
||||
)
|
||||
|
||||
protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings)
|
||||
|
||||
The adaptive settings may modify parameters based on properties of the input systems.
|
||||
For example (:class:`.RelativeHybridTopologyProtocol`):
|
||||
|
||||
* Transformations involving a change in net charge use a larger number of lambda windows and longer production simulations.
|
||||
* If both states contain a :class:`.ProteinComponent`, the solvation padding is set to 1 nm.
|
||||
|
||||
Optionally, you can pass a preexisting settings object to the ``_adaptive_settings`` method via the ``initial_settings`` argument. If provided, an adapted copy of these settings will be returned instead
|
||||
of using the default settings.
|
||||
|
||||
In systems containing membrane-protein complexes (i.e. using a
|
||||
:class:`.ProteinMembraneComponent`), adaptive settings select a membrane-appropriate barostat, the ``MonteCarloMembraneBarostat``.
|
||||
|
||||
Creating Transformations from Protocols
|
||||
-----------------------------------------
|
||||
|
||||
@@ -58,3 +58,38 @@ Save this configuration file as ``debug_logging.conf`` and then run ``openfe qui
|
||||
Note that the ``--log debug_logging.conf`` argument goes between ``openfe`` and ``quickrun`` on the command line.
|
||||
|
||||
This will cause every package to log at the debug level, which may be quite verbose and noisy but should aid in identify what is going on right before the exception is thrown.
|
||||
|
||||
JAX warnings
|
||||
------------
|
||||
|
||||
We use ``pymbar`` to analyze the free energy of the system.
|
||||
``pymbar`` uses JAX to accelerate computation.
|
||||
The JAX library can utilize a GPU to further accelerate computation.
|
||||
If the necessary libraries for GPU acceleration are not installed and JAX detects a GPU, JAX will print a warning like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
WARNING:2025-06-10 09:01:40,857:jax._src.xla_bridge:966: An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.
|
||||
|
||||
This warning does not mean that the *molecular dynamics* simulation will fall back to using the CPU.
|
||||
The simulation will still use the computing platform specified in the settings.
|
||||
|
||||
PYMBAR_DISABLE_JAX
|
||||
------------------
|
||||
|
||||
Due to a suspected memory leak in the JAX acceleration code in ``pymbar`` we disable JAX acceleration by default.
|
||||
This memory leak may result in the simulation crashing, wasting compute time.
|
||||
The error message may look like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
LLVM compilation error: Cannot allocate memory
|
||||
LLVM ERROR: Unable to allocate section memory!
|
||||
|
||||
We have decided to disable JAX acceleration by default to prevent wasted compute.
|
||||
However, if you wish to use the JAX acceleration, you may set ``PYMBAR_DISABLE_JAX`` to ``TRUE`` (e.g. put ``export PYMBAR_DISABLE_JAX=FALSE`` in your submission script before running ``openfe quickrun``).
|
||||
For more information, see these issues on github:
|
||||
|
||||
- https://github.com/choderalab/pymbar/issues/564
|
||||
- https://github.com/OpenFreeEnergy/openfe/issues/1534
|
||||
- https://github.com/OpenFreeEnergy/openfe/issues/1654
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
.. module:: openfe
|
||||
|
||||
====================================
|
||||
Welcome to the OpenFE documentation!
|
||||
====================================
|
||||
=====================================
|
||||
Welcome to OpenFE's documentation!
|
||||
=====================================
|
||||
|
||||
The **OpenFE** toolkit provides a free and open-source framework for alchemical free energy calculations.
|
||||
Using this toolkit you can plan, execute and analyse free energy calculations using a variety of methods.
|
||||
Using this toolkit you can plan, execute, and analyze free energy calculations using a variety of methods.
|
||||
|
||||
**Useful Links**:
|
||||
`OpenFE Website <https://openfree.energy/>`__ |
|
||||
@@ -18,13 +18,6 @@ Using this toolkit you can plan, execute and analyse free energy calculations us
|
||||
.. grid:: 1 2 2 4
|
||||
:gutter: 3
|
||||
|
||||
.. grid-item-card:: :fas:`laptop-code` Try openfe in your browser
|
||||
:text-align: center
|
||||
:link: http://try.openfree.energy
|
||||
:link-type: url
|
||||
|
||||
Curious about **openfe**? Start here and run the openfe showcase notebook in your browser.
|
||||
|
||||
.. grid-item-card:: :fas:`download` Install openfe
|
||||
:text-align: center
|
||||
:link: installation
|
||||
@@ -32,6 +25,12 @@ Using this toolkit you can plan, execute and analyse free energy calculations us
|
||||
|
||||
Follow our installation guide to get **openfe** running on your machine!
|
||||
|
||||
.. grid-item-card:: :fas:`laptop-code` CLI Quickstart
|
||||
:text-align: center
|
||||
:link: tutorials/rbfe_cli_tutorial
|
||||
:link-type: doc
|
||||
|
||||
Get started with **openfe**\'s command line interface.
|
||||
|
||||
.. grid-item-card:: :fas:`person-chalkboard` Tutorials
|
||||
:text-align: center
|
||||
@@ -59,7 +58,7 @@ Using this toolkit you can plan, execute and analyse free energy calculations us
|
||||
:link: reference/index
|
||||
:link-type: doc
|
||||
|
||||
Comprehensive details of both the **openfe** CLI and Python API.
|
||||
Comprehensive details of the **openfe** Python and CLI APIs.
|
||||
|
||||
.. grid-item-card:: :fas:`gears` Protocols
|
||||
:text-align: center
|
||||
@@ -86,9 +85,19 @@ Using this toolkit you can plan, execute and analyse free energy calculations us
|
||||
reference/index
|
||||
CHANGELOG
|
||||
|
||||
Indices and tables
|
||||
------------------
|
||||
Other OpenFE Ecosystem Projects:
|
||||
--------------------------------
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
**openfe** is Open Free Energy's user-facing software for performing alchemical free energy calculations.
|
||||
Below are other software projects the Open Free Energy team maintains, many of which are used by **openfe** itself.
|
||||
|
||||
* `konnektor <https://github.com/OpenFreeEnergy/konnektor/>`_: free energy network planning, modification, and analysis
|
||||
* `kartograf <https://github.com/OpenFreeEnergy/kartograf/>`_: atom mappings focusing on 3D geometries
|
||||
* `Lomap <https://github.com/OpenFreeEnergy/Lomap/>`_: planning perturbation networks for free energy calculations
|
||||
* `cinnabar <https://github.com/OpenFreeEnergy/cinnabar/>`_ (formerly arsenic): plotting free energy calculation results
|
||||
* `gufe <https://gufe.openfree.energy/en/latest/>`_ : data structures and models underlying the OpenFE ecosystem
|
||||
|
||||
Community-Developed Projects:
|
||||
-----------------------------
|
||||
|
||||
* `alchemiscale <https://docs.alchemiscale.org/en/stable/>`_: high-throughput alchemical free energy execution, developed by `Datryllic <https://datryllic.com/>`_.
|
||||
|
||||
@@ -1,196 +1,83 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
**openfe** is currently only compatible with POSIX systems (macOS and UNIX/Linux).
|
||||
**openfe** is currently only compatible with POSIX systems (macOS and UNIX/Linux).
|
||||
See `Supported Hardware`_ for more details.
|
||||
|
||||
We try to follow `SPEC0 <https://scientific-python.org/specs/spec-0000/>`_ as far as minimum supported dependencies, with the following caveats:
|
||||
|
||||
- OpenMM 8.0, 8.1.2, 8.2.0 - **we do not yet support OpenMM v8.3.0**
|
||||
- `OpenEye Toolkits` is not yet compatible with Python 3.13, so **openfe** cannot use openeye functionality with Python 3.13.
|
||||
- OpenMM 8.0, 8.1.2, 8.2, and 8.4 - **OpenMM v8.3.0 is not supported**
|
||||
|
||||
When you install ``openfe`` through any of the methods described below, you
|
||||
will install both the core library and the command line interface (CLI).
|
||||
When you install **openfe** through any of the methods described below, you will install both the core library and the command line interface (CLI).
|
||||
|
||||
|
||||
Installation with ``miniforge`` (recommended)
|
||||
Installation with ``micromamba`` (recommended)
|
||||
----------------------------------------------
|
||||
|
||||
.. _Miniforge: https://github.com/conda-forge/miniforge?tab=readme-ov-file#miniforge
|
||||
OpenFE recommends ``micromamba`` as a package manager for most users, as it is a lightweight version of ``mamba``, which is a must faster drop-in replacement for ``conda`` .
|
||||
|
||||
We recommend installing ``openfe`` with `Miniforge`_ because it provides easy
|
||||
installation of other software that ``openfe`` needs, such as OpenMM and
|
||||
AmberTools. We recommend ``miniforge`` because it is faster than ``conda`` and
|
||||
comes preconfigured to use ``conda-forge``.
|
||||
If you prefer to use ``mamba`` or ``conda`` instead of ``micromamba`` because of its additional functionality, we suggest following our `Miniforge Installation Guide`_.
|
||||
|
||||
To install and configure ``miniforge``, you need to know your operating
|
||||
system, your machine architecture (output of ``uname -m``), and your shell
|
||||
(in most cases, can be determined from ``echo $SHELL``). Select
|
||||
your operating system and architecture from the tool below, and run the
|
||||
commands it suggests.
|
||||
In the instructions below, we will use the ``micromamba`` command, but you can use ``conda`` or ``mamba`` in the same way.
|
||||
|
||||
.. raw:: html
|
||||
Once you have one of `micromamba <https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html>`_, `mamba <https://mamba.readthedocs.io/en/latest/installation/mamba-installation.html>`_, or `conda <https://docs.conda.io/projects/conda/en/stable/user-guide/install/index.html>`_ installed, you can continue to the **openfe** installation instructions below.
|
||||
|
||||
<select id="miniforge-os" onchange="javascript: setArchitectureOptions(this.options[this.selectedIndex].value)">
|
||||
<option value="Linux">Linux</option>
|
||||
<option value="MacOSX">macOS</option>
|
||||
</select>
|
||||
<select id="miniforge-architecture" onchange="updateInstructions()">
|
||||
</select>
|
||||
<select id="miniforge-shell" onchange="updateInstructions()">
|
||||
<option value="bash">bash</option>
|
||||
<option value="zsh">zsh</option>
|
||||
<option value="tcsh">tcsh</option>
|
||||
<option value="fish">fish</option>
|
||||
<option value="xonsh">xonsh</option>
|
||||
</select>
|
||||
<br />
|
||||
<pre><span id="miniforge-curl-install"></span></pre>
|
||||
<script>
|
||||
function setArchitectureOptions(os) {
|
||||
let options = {
|
||||
"MacOSX": [
|
||||
["x86_64", ""],
|
||||
["arm64", " (Apple Silicon)"]
|
||||
],
|
||||
"Linux": [
|
||||
["x86_64", " (amd64)"],
|
||||
["aarch64", " (arm64)"],
|
||||
["ppc64le", " (POWER8/9)"]
|
||||
]
|
||||
};
|
||||
choices = options[os];
|
||||
let htmlString = ""
|
||||
for (const [val, extra] of choices) {
|
||||
htmlString += `<option value="${val}">${val}${extra}</option>`;
|
||||
}
|
||||
let arch = document.getElementById("miniforge-architecture");
|
||||
arch.innerHTML = htmlString
|
||||
updateInstructions()
|
||||
}
|
||||
.. note::
|
||||
|
||||
function updateInstructions() {
|
||||
let cmd = document.getElementById("miniforge-curl-install");
|
||||
let osElem = document.getElementById("miniforge-os");
|
||||
let archElem = document.getElementById("miniforge-architecture");
|
||||
let shellElem = document.getElementById("miniforge-shell");
|
||||
let os = osElem[osElem.selectedIndex].value;
|
||||
let arch = archElem[archElem.selectedIndex].value;
|
||||
let shell = shellElem[shellElem.selectedIndex].value;
|
||||
let filename = "Miniforge3-" + os + "-" + arch + ".sh"
|
||||
let cmdArr = [
|
||||
(
|
||||
"curl -OL https://github.com/conda-forge/miniforge/"
|
||||
+ "releases/latest/download/" + filename
|
||||
),
|
||||
"sh " + filename + " -b",
|
||||
"~/miniforge3/bin/mamba init " + shell,
|
||||
"rm -f " + filename,
|
||||
]
|
||||
cmd.innerHTML = cmdArr.join("\n")
|
||||
}
|
||||
After installing, you must run ``micromamba activate openfe`` in each shell session where you want to use **openfe**!
|
||||
|
||||
setArchitectureOptions("Linux"); // default
|
||||
</script>
|
||||
|
||||
You should then close your current session and open a fresh login to ensure
|
||||
that everything is properly registered.
|
||||
|
||||
Next we will create an environment called ``openfe_env`` with the ``openfe`` package and all required dependencies:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
mamba create -n openfe_env openfe=\ |version|
|
||||
|
||||
Now we need to activate our new environment ::
|
||||
|
||||
mamba activate openfe_env
|
||||
|
||||
To quickly check this is working, run the tests ::
|
||||
|
||||
openfe test
|
||||
|
||||
The very first time you run this, the
|
||||
initial check that you can import ``openfe`` will take a while, because some
|
||||
code is compiled the first time it is encountered. That compilation only
|
||||
happens once per installation.
|
||||
|
||||
A more expansive test suite can be run using ::
|
||||
|
||||
openfe test --long
|
||||
|
||||
This test suite contains several hundred individual tests. This may take up to
|
||||
an hour, and all tests should complete with status either passed,
|
||||
skipped, or xfailed (expected fail).
|
||||
This "long" test suite should be run as a job on the compute
|
||||
hardware intended to run openfe jobs, as it will test GPU specific features.
|
||||
|
||||
With that, you should be ready to use ``openfe``!
|
||||
|
||||
Installation with ``mamba``
|
||||
---------------------------
|
||||
|
||||
If you already have a `Mamba <https://mamba.readthedocs.io/en/latest/installation/mamba-installation.html>`_
|
||||
(or `Micromamba <https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html>`_ ) installation, you can install ``openfe`` with:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
mamba create -c conda-forge -n openfe_env openfe=\ |version|
|
||||
mamba activate openfe_env
|
||||
|
||||
Note that you must run the latter line in each shell session where you want to use ``openfe``. OpenFE recommends the Mamba package manager for most users as it is orders of magnitude faster than the default Conda package manager. Mamba is a drop in replacement for Conda.
|
||||
|
||||
Reproducible builds with a ``conda-lock`` file
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. _conda-lock: https://github.com/conda/conda-lock?tab=readme-ov-file#conda-lock
|
||||
|
||||
A `conda-lock`_ file is a cross-platform way of specifying a conda environment to build packages in a reproducible way.
|
||||
We recommend building from **openfe**'s ``conda-lock`` file in most cases, since it allows for building packages in a reproducible way on multiple platforms.
|
||||
|
||||
Unlike the single file installer, an internet connection is required to install from a ``conda-lock`` file.
|
||||
We recommend the use of a ``conda-lock`` file when the same conda environment is required across different systems.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
You will likely need to install ``conda-lock``.
|
||||
We strongly recommend installing ``conda-lock`` in a new virtual environment.
|
||||
This will reduce the chance of dependency conflicts ::
|
||||
|
||||
$ # Install conda lock into a virtual environment
|
||||
$ conda create -n conda-lock -c conda-lock
|
||||
$ # Activate the environment to use the conda-lock command
|
||||
$ conda activate conda-lock
|
||||
|
||||
See https://github.com/conda/conda-lock?tab=readme-ov-file#conda-lock for more information on ``conda-lock``.
|
||||
|
||||
The latest version of the `conda-lock` file we provide can be downloaded with ::
|
||||
The ``conda-lock`` files for the latest version of **openfe** can be downloaded with ::
|
||||
|
||||
$ curl -LOJ https://github.com/OpenFreeEnergy/openfe/releases/latest/download/openfe-conda-lock.yml
|
||||
|
||||
If a particular version is required, the URL will look like this (using the ``openfe 1.0.1`` release as an example) ::
|
||||
If a particular version is required, the URL will look like this (using the ``openfe 1.6.1`` release as an example) ::
|
||||
|
||||
$ curl -LOJ https://github.com/OpenFreeEnergy/openfe/releases/download/v1.0.1/openfe-1.0.1-conda-lock.yml
|
||||
$ curl -LOJ https://github.com/OpenFreeEnergy/openfe/releases/download/v1.6.1/openfe-1.6.1-conda-lock.yml
|
||||
|
||||
Create a conda environment from the lock file and activate it::
|
||||
``micromamba`` supports ``conda-lock`` files and can be used directly to create a virtual environment ::
|
||||
|
||||
$ micromamba create -n openfe --file openfe-conda-lock.yml
|
||||
$ micromamba activate openfe
|
||||
|
||||
$ conda-lock install -n openfe openfe-conda-lock.yml
|
||||
$ conda activate openfe
|
||||
|
||||
.. note::
|
||||
|
||||
micromamba also supports ``conda-lock`` files and can be used to create a virtual environment ::
|
||||
If you are having trouble building from the conda-lock file, you may need to build directly with ``conda-lock``.
|
||||
We recommend installing ``conda-lock`` in a new virtual environment.
|
||||
This will reduce the chance of dependency conflicts ::
|
||||
|
||||
$ micromamba create -n openfe --file openfe-conda-lock.yml
|
||||
$ # Install conda lock into a virtual environment
|
||||
$ micromamba create -n conda-lock conda-lock
|
||||
$ # Activate the environment to use the conda-lock command
|
||||
$ micromamba activate conda-lock
|
||||
$ conda-lock install -n openfe openfe-conda-lock.yml
|
||||
$ micromamba activate openfe
|
||||
|
||||
To make sure everything is working, run the tests ::
|
||||
To make sure everything is working, :ref:`run the tests <testing>`.
|
||||
|
||||
$ pytest --pyargs openfe openfecli
|
||||
With that, you should be ready to use **openfe**!
|
||||
|
||||
The test suite contains several hundred individual tests. This will take a
|
||||
few minutes, and all tests should complete with status either passed,
|
||||
skipped, or xfailed (expected fail).
|
||||
Standard Installation with ``micromamba``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There may be some instances where you don't want to use a lock-file, e.g. you may want to specify a dependency that differs from the lock file.
|
||||
|
||||
In these cases, you can simply install **openfe** from conda-forge:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
micromamba create -c conda-forge -n openfe openfe=\ |version|
|
||||
micromamba activate openfe
|
||||
|
||||
With that, you should be ready to use ``openfe``!
|
||||
|
||||
Single file installer
|
||||
---------------------
|
||||
@@ -213,7 +100,7 @@ And the MacOS (arm64) installer ::
|
||||
|
||||
MacOS x86_64 is no longer supported.
|
||||
|
||||
The single file installer contains all of the dependencies required for ``openfe`` and does not require internet access to use.
|
||||
The single file installer contains all of the dependencies required for **openfe** and does not require internet access to use.
|
||||
|
||||
Both ``conda`` and ``mamba`` are also available in the environment created by the single file installer and can be used to install additional packages.
|
||||
The installer can be installed in batch mode or interactively ::
|
||||
@@ -395,17 +282,9 @@ Now the CLI tool should work as well ::
|
||||
test Run the OpenFE test suite
|
||||
|
||||
|
||||
To make sure everything is working, :ref:`run the tests <testing>`.
|
||||
|
||||
|
||||
To make sure everything is working, run the tests ::
|
||||
|
||||
$ pytest --pyargs openfe openfecli
|
||||
|
||||
The test suite contains several hundred individual tests. This will take a
|
||||
few minutes, and all tests should complete with status either passed,
|
||||
skipped, or xfailed (expected fail).
|
||||
|
||||
With that, you should be ready to use ``openfe``!
|
||||
With that, you should be ready to use **openfe**!
|
||||
|
||||
.. _installation:containers:
|
||||
|
||||
@@ -413,7 +292,7 @@ Containerized Distributions
|
||||
----------------------------
|
||||
|
||||
We provide an official docker and Apptainer (formerly Singularity) image.
|
||||
The docker image is tagged with the version of ``openfe`` on the image and can be pulled with ::
|
||||
The docker image is tagged with the version of **openfe** on the image and can be pulled with ::
|
||||
|
||||
$ docker pull ghcr.io/openfreeenergy/openfe:latest
|
||||
|
||||
@@ -455,163 +334,91 @@ This can be done with the following command ::
|
||||
The ``--nv`` flag is required for the Apptainer image to access the GPU on the host.
|
||||
Your output may produce different values for the forces, but should list the CUDA platform if everything is working properly.
|
||||
|
||||
You can access the ``openfe`` CLI from the Singularity image with ::
|
||||
You can access the **openfe** CLI from the Singularity image with ::
|
||||
|
||||
$ singularity run --nv openfe_latest-apptainer.sif openfe --help
|
||||
|
||||
To make sure everything is working, run the tests ::
|
||||
|
||||
$ singularity run --nv openfe_latest-apptainer.sif pytest --pyargs openfe openfecli
|
||||
$ singularity run --nv openfe_latest-apptainer.sif openfe test
|
||||
|
||||
The test suite contains several hundred individual tests. This will take a
|
||||
few minutes, and all tests should complete with status either passed,
|
||||
skipped, or xfailed (expected fail).
|
||||
You can also run the long tests with ``openfe test --long``, as explained in `Testing Your Installation`_.
|
||||
|
||||
With that, you should be ready to use ``openfe``!
|
||||
With that, you should be ready to use **openfe**!
|
||||
|
||||
.. note::
|
||||
|
||||
If building a custom docker image, you may need to need to add ``--ulimit nofile=262144:262144`` to the ``docker build`` command.
|
||||
See this `issue <https://github.com/OpenFreeEnergy/openfe/issues/1269>`_ for details.
|
||||
See this `issue <https://github.com/OpenFreeEnergy/openfe/issues/1269>`_ for details.
|
||||
|
||||
HPC Environments
|
||||
----------------
|
||||
|
||||
When using High Performance Computing resources, jobs are typically submitted to a queue from a "login node" and then run at a later time, often on different hardware and in a different software environment.
|
||||
This can complicate installation as getting something working on the login node does not guarantee it will work in the job.
|
||||
We recommend using `Apptainer (formerly Singularity) <https://apptainer.org/>`_ when running ``openfe`` workflows in HPC environments.
|
||||
We recommend using `Apptainer (formerly Singularity) <https://apptainer.org/>`_ when running **openfe** workflows in HPC environments.
|
||||
This images provide a software environment that is isolated from the host which can make workflow execution easier to setup and more reproducible.
|
||||
See our guide on :ref:`containers <installation:containers>` for how to get started using Apptainer/Singularity.
|
||||
|
||||
.. _installation:mamba_hpc:
|
||||
|
||||
``mamba`` in HPC Environments
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
``micromamba`` in HPC Environments
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. _virtual packages: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-virtual.html#managing-virtual-packages
|
||||
|
||||
We recommend using a :ref:`container <installation:containers>` to install ``openfe`` in HPC environments.
|
||||
Nonetheless, ``openfe`` can be installed via Conda Forge on these environments also.
|
||||
We recommend using a :ref:`container <installation:containers>` to install **openfe** in HPC environments.
|
||||
Nonetheless, **openfe** can be installed via Conda Forge on these environments also.
|
||||
Conda Forge distributes its own CUDA binaries for interfacing with the GPU, rather than use the host drivers.
|
||||
``conda``, ``mamba`` and ``micromamba`` all use `virtual packages`_ to detect and specify which version of CUDA should be installed.
|
||||
This is a common point of difference in hardware between the login and job nodes in an HPC environment.
|
||||
For example, on a login node where there likely is not a GPU or a CUDA environment, ``mamba info`` may produce output that looks like this ::
|
||||
|
||||
$ mamba info
|
||||
|
||||
mamba version : 1.5.1
|
||||
active environment : base
|
||||
active env location : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0
|
||||
shell level : 1
|
||||
user config file : /home/henrym3/.condarc
|
||||
populated config files : /lila/home/henrym3/.condarc
|
||||
conda version : 23.7.4
|
||||
conda-build version : not installed
|
||||
python version : 3.11.5.final.0
|
||||
virtual packages : __archspec=1=x86_64
|
||||
__glibc=2.17=0
|
||||
__linux=3.10.0=0
|
||||
__unix=0=0
|
||||
base environment : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0 (writable)
|
||||
conda av data dir : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0/etc/conda
|
||||
conda av metadata url : None
|
||||
channel URLs : https://conda.anaconda.org/conda-forge/linux-64
|
||||
https://conda.anaconda.org/conda-forge/noarch
|
||||
package cache : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0/pkgs
|
||||
/home/henrym3/.conda/pkgs
|
||||
envs directories : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0/envs
|
||||
/home/henrym3/.conda/envs
|
||||
platform : linux-64
|
||||
user-agent : conda/23.7.4 requests/2.31.0 CPython/3.11.5 Linux/3.10.0-957.12.2.el7.x86_64 centos/7.6.1810 glibc/2.17
|
||||
UID:GID : 1987:3008
|
||||
netrc file : None
|
||||
offline mode : False
|
||||
|
||||
Now if we run the same command on a HPC node that has a GPU ::
|
||||
|
||||
$ mamba info
|
||||
|
||||
mamba version : 1.5.1
|
||||
active environment : base
|
||||
active env location : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0
|
||||
shell level : 1
|
||||
user config file : /home/henrym3/.condarc
|
||||
populated config files : /lila/home/henrym3/.condarc
|
||||
conda version : 23.7.4
|
||||
conda-build version : not installed
|
||||
python version : 3.11.5.final.0
|
||||
virtual packages : __archspec=1=x86_64
|
||||
__cuda=11.7=0
|
||||
__glibc=2.17=0
|
||||
__linux=3.10.0=0
|
||||
__unix=0=0
|
||||
base environment : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0 (writable)
|
||||
conda av data dir : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0/etc/conda
|
||||
conda av metadata url : None
|
||||
channel URLs : https://conda.anaconda.org/conda-forge/linux-64
|
||||
https://conda.anaconda.org/conda-forge/noarch
|
||||
package cache : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0/pkgs
|
||||
/home/henrym3/.conda/pkgs
|
||||
envs directories : /lila/home/henrym3/mamba/envs/QA-openfe-0.14.0/envs
|
||||
/home/henrym3/.conda/envs
|
||||
platform : linux-64
|
||||
user-agent : conda/23.7.4 requests/2.31.0 CPython/3.11.5 Linux/3.10.0-1160.45.1.el7.x86_64 centos/7.9.2009 glibc/2.17
|
||||
UID:GID : 1987:3008
|
||||
netrc file : None
|
||||
offline mode : False
|
||||
|
||||
|
||||
We can see that there is a virtual package ``__cuda=11.7=0``.
|
||||
This means that if we run a ``mamba install`` command on a node with a GPU, the solver will install the correct version of the ``cudatoolkit``.
|
||||
However, if we ran the same command on the login node, the solver may install the wrong version of the ``cudatoolkit``, or depending on how the Conda packages are setup, a CPU only version of the package.
|
||||
We can control the virtual package with the environmental variable ``CONDA_OVERRIDE_CUDA``.
|
||||
|
||||
In order to determine the correct ``cudatoolkit`` version, we recommend connecting to the node where the simulation will be executed and run ``nvidia-smi``.
|
||||
In order to determine the correct ``cuda-version`` version, we recommend connecting to the node where the simulation will be executed and run ``nvidia-smi``.
|
||||
For example ::
|
||||
|
||||
$ nvidia-smi
|
||||
Tue Jun 13 17:47:11 2023
|
||||
+-----------------------------------------------------------------------------+
|
||||
| NVIDIA-SMI 515.43.04 Driver Version: 515.43.04 CUDA Version: 11.7 |
|
||||
|-------------------------------+----------------------+----------------------+
|
||||
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
|
||||
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|
||||
| | | MIG M. |
|
||||
|===============================+======================+======================|
|
||||
| 0 NVIDIA A40 On | 00000000:65:00.0 Off | 0 |
|
||||
| 0% 30C P8 32W / 300W | 0MiB / 46068MiB | 0% Default |
|
||||
| | | N/A |
|
||||
+-------------------------------+----------------------+----------------------+
|
||||
Tue Mar 31 19:46:32 2026
|
||||
+-----------------------------------------------------------------------------------------+
|
||||
| NVIDIA-SMI 590.48.01 Driver Version: 590.48.01 CUDA Version: 13.1 |
|
||||
+-----------------------------------------+------------------------+----------------------+
|
||||
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
|
||||
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
|
||||
| | | MIG M. |
|
||||
|=========================================+========================+======================|
|
||||
| 0 NVIDIA A100 80GB PCIe On | 00000000:65:00.0 Off | 0 |
|
||||
| N/A 32C P0 44W / 300W | 0MiB / 81920MiB | 0% Default |
|
||||
| | | Disabled |
|
||||
+-----------------------------------------+------------------------+----------------------+
|
||||
|
||||
+-----------------------------------------------------------------------------------------+
|
||||
| Processes: |
|
||||
| GPU GI CI PID Type Process name GPU Memory |
|
||||
| ID ID Usage |
|
||||
|=========================================================================================|
|
||||
| No running processes found |
|
||||
+-----------------------------------------------------------------------------------------+
|
||||
|
||||
+-----------------------------------------------------------------------------+
|
||||
| Processes: |
|
||||
| GPU GI CI PID Type Process name GPU Memory |
|
||||
| ID ID Usage |
|
||||
|=============================================================================|
|
||||
| No running processes found |
|
||||
+-----------------------------------------------------------------------------+
|
||||
|
||||
in this output of ``nvidia-smi`` we can see in the upper right of the output ``CUDA Version: 11.7`` which means the installed driver will support a ``cudatoolkit`` version up to ``11.7``
|
||||
|
||||
So on the login node, we can run ``CONDA_OVERRIDE_CUDA=11.7 mamba info`` and see that the "correct" virtual CUDA is listed.
|
||||
For example, to install a version of ``openfe`` which is compatible with ``cudatoolkit 11.7``, run:
|
||||
in this output of ``nvidia-smi`` we can see in the upper right of the output ``CUDA Version: 13.1`` which means the installed driver will support a CUDA version up to ``13.1``.
|
||||
To install a version of **openfe** which is compatible with CUDA ``13.1``, run:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
$ CONDA_OVERRIDE_CUDA=11.7 mamba create -n openfe_env openfe=\ |version|
|
||||
$ micromamba create -n openfe cuda-version=13.1 openfe=\ |version|
|
||||
|
||||
|
||||
|
||||
Developer install
|
||||
-----------------
|
||||
|
||||
If you're going to be developing for ``openfe``, you will want an
|
||||
If you're going to be developing for **openfe**, you will want an
|
||||
installation where your changes to the code are immediately reflected in the
|
||||
functionality. This is called a "developer" or "editable" installation.
|
||||
|
||||
Getting a developer installation for ``openfe`` first installing the
|
||||
Getting a developer installation for **openfe** first installing the
|
||||
requirements, and then creating the editable installation. We recommend
|
||||
doing that with ``mamba`` using the following procedure:
|
||||
doing that with ``micromamba`` using the following procedure:
|
||||
|
||||
First, clone the ``openfe`` repository, and switch into its root directory::
|
||||
First, clone the **openfe** repository, and switch into its root directory::
|
||||
|
||||
$ git clone https://github.com/OpenFreeEnergy/openfe.git
|
||||
$ cd openfe
|
||||
@@ -619,11 +426,11 @@ First, clone the ``openfe`` repository, and switch into its root directory::
|
||||
Next create a ``conda`` environment containing the requirements from the
|
||||
specification in that directory::
|
||||
|
||||
$ mamba create -f environment.yml
|
||||
$ micromamba create -f environment.yml
|
||||
|
||||
Then activate the ``openfe`` environment with::
|
||||
Then activate the openfe environment with::
|
||||
|
||||
$ mamba activate openfe_env
|
||||
$ micromamba activate openfe_env
|
||||
|
||||
Finally, create the editable installation::
|
||||
|
||||
@@ -632,6 +439,31 @@ Finally, create the editable installation::
|
||||
Note the ``.`` at the end of that command, which indicates the current
|
||||
directory.
|
||||
|
||||
|
||||
.. _testing:
|
||||
|
||||
Testing Your Installation
|
||||
-------------------------
|
||||
|
||||
After installing **openfe**, make sure everything is working as expected by running the test suite with ::
|
||||
|
||||
$ openfe test
|
||||
|
||||
The test suite contains several hundred individual tests.
|
||||
This will take a few minutes, and all tests should complete with status either passed, skipped, or xfailed (expected fail).
|
||||
|
||||
The very first time you run this, the initial check that you can import ``openfe`` will take a while, because some code is compiled the first time it is encountered.
|
||||
That compilation only happens once per installation, and so subsequent calls to ``openfe`` will be faster.
|
||||
|
||||
A more expansive test suite can be run using ::
|
||||
|
||||
$ openfe test --long
|
||||
|
||||
This test suite contains several hundred individual tests.
|
||||
This may take up to an hour, and all tests should complete with status either passed, skipped, or xfailed (expected fail).
|
||||
This "long" test suite should be run as a job on the compute hardware intended to run openfe jobs, as it will test GPU specific features.
|
||||
|
||||
|
||||
Troubleshooting Your Installation
|
||||
---------------------------------
|
||||
|
||||
@@ -690,3 +522,82 @@ While OpenMM supports OpenCL, we do not regularly test that platform (the CUDA p
|
||||
For production use, we recommend the ``linux-64`` platform with NVIDIA GPUs for optimal performance.
|
||||
When using an OpenMM based protocol on NVIDIA GPUs, we recommend driver version ``525.60.13`` or greater.
|
||||
The minimum driver version required when installing from conda-forge is ``450.36.06``, but newer versions of OpenMM may not support that driver version as CUDA 11 will be removed the build matrix.
|
||||
|
||||
|
||||
Miniforge Installation Guide
|
||||
----------------------------
|
||||
|
||||
.. _Miniforge: https://github.com/conda-forge/miniforge?tab=readme-ov-file#miniforge
|
||||
|
||||
`Miniforge`_ provides minimal installers for either ``conda`` or ``mamba``, and enables easy installation of other software that ``openfe`` needs, such as OpenMM and AmberTools.
|
||||
We recommend using ``miniforge`` to install ``mamaba`` because it is faster than ``conda`` and comes preconfigured to use ``conda-forge``.
|
||||
|
||||
To install and configure ``miniforge``, you need to know your operating system, your machine architecture (output of ``uname -m``), and your shell (in most cases, can be determined from ``echo $SHELL``).
|
||||
Select your operating system and architecture from the tool below, and run the commands it suggests.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<select id="miniforge-os" onchange="javascript: setArchitectureOptions(this.options[this.selectedIndex].value)">
|
||||
<option value="Linux">Linux</option>
|
||||
<option value="MacOSX">macOS</option>
|
||||
</select>
|
||||
<select id="miniforge-architecture" onchange="updateInstructions()">
|
||||
</select>
|
||||
<select id="miniforge-shell" onchange="updateInstructions()">
|
||||
<option value="bash">bash</option>
|
||||
<option value="zsh">zsh</option>
|
||||
<option value="tcsh">tcsh</option>
|
||||
<option value="fish">fish</option>
|
||||
<option value="xonsh">xonsh</option>
|
||||
</select>
|
||||
<br />
|
||||
<pre><span id="miniforge-curl-install"></span></pre>
|
||||
<script>
|
||||
function setArchitectureOptions(os) {
|
||||
let options = {
|
||||
"MacOSX": [
|
||||
["x86_64", ""],
|
||||
["arm64", " (Apple Silicon)"]
|
||||
],
|
||||
"Linux": [
|
||||
["x86_64", " (amd64)"],
|
||||
["aarch64", " (arm64)"],
|
||||
["ppc64le", " (POWER8/9)"]
|
||||
]
|
||||
};
|
||||
choices = options[os];
|
||||
let htmlString = ""
|
||||
for (const [val, extra] of choices) {
|
||||
htmlString += `<option value="${val}">${val}${extra}</option>`;
|
||||
}
|
||||
let arch = document.getElementById("miniforge-architecture");
|
||||
arch.innerHTML = htmlString
|
||||
updateInstructions()
|
||||
}
|
||||
|
||||
function updateInstructions() {
|
||||
let cmd = document.getElementById("miniforge-curl-install");
|
||||
let osElem = document.getElementById("miniforge-os");
|
||||
let archElem = document.getElementById("miniforge-architecture");
|
||||
let shellElem = document.getElementById("miniforge-shell");
|
||||
let os = osElem[osElem.selectedIndex].value;
|
||||
let arch = archElem[archElem.selectedIndex].value;
|
||||
let shell = shellElem[shellElem.selectedIndex].value;
|
||||
let filename = "Miniforge3-" + os + "-" + arch + ".sh"
|
||||
let cmdArr = [
|
||||
(
|
||||
"curl -OL https://github.com/conda-forge/miniforge/"
|
||||
+ "releases/latest/download/" + filename
|
||||
),
|
||||
"sh " + filename + " -b",
|
||||
"~/miniforge3/bin/mamba init " + shell,
|
||||
"rm -f " + filename,
|
||||
]
|
||||
cmd.innerHTML = cmdArr.join("\n")
|
||||
}
|
||||
|
||||
setArchitectureOptions("Linux"); // default
|
||||
</script>
|
||||
|
||||
You should then close your current session and open a fresh login to ensure that everything is properly registered.
|
||||
You can now proceed to use ``mamba`` commands as instructed above.
|
||||
|
||||
@@ -21,6 +21,7 @@ Tools for mapping atoms in one molecule to those in another. Used to generate ef
|
||||
:nosignatures:
|
||||
:toctree: generated/
|
||||
|
||||
KartografAtomMapper
|
||||
LomapAtomMapper
|
||||
PersesAtomMapper
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
We have reproduced API documentation from the `gufe`_ package here for convenience.
|
||||
`gufe`_ serves as a foundation layer for openfe, providing abstract base classes and object models, and so might be more useful for developers.
|
||||
|
||||
OpenFE API Reference
|
||||
Python API Reference
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
@@ -17,6 +17,7 @@ OpenFE API Reference
|
||||
defining_and_executing_simulations
|
||||
openmm_rfe
|
||||
openmm_solvation_afe
|
||||
openmm_binding_afe
|
||||
openmm_septop
|
||||
openmm_md
|
||||
openmm_protocol_settings
|
||||
|
||||
46
docs/reference/api/openmm_binding_afe.rst
Normal file
46
docs/reference/api/openmm_binding_afe.rst
Normal file
@@ -0,0 +1,46 @@
|
||||
OpenMM Absolute Binding Free Energy Protocol
|
||||
============================================
|
||||
|
||||
.. _afe binding protocol api:
|
||||
|
||||
This section provides details about the OpenMM Absolute Binding Free Energy Protocol
|
||||
implemented in OpenFE.
|
||||
|
||||
Protocol API specification
|
||||
--------------------------
|
||||
|
||||
.. module:: openfe.protocols.openmm_afe.equil_binding_afe_method
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
:toctree: generated/
|
||||
|
||||
AbsoluteBindingProtocol
|
||||
ABFEComplexAnalysisUnit
|
||||
ABFEComplexSetupUnit
|
||||
ABFEComplexSimUnit
|
||||
ABFESolventAnalysisUnit
|
||||
ABFESolventSetupUnit
|
||||
ABFESolventSimUnit
|
||||
AbsoluteBindingProtocolResult
|
||||
|
||||
Protocol Settings
|
||||
-----------------
|
||||
|
||||
|
||||
Below are the settings which can be tweaked in the protocol. The default settings (accessed using :meth:`AbsoluteBindingProtocol.default_settings`) will automatically populate settings which we have found to be useful for running binding free energy calculations. There will however be some cases (such as when calculating difficult to converge systems) where you will need to tweak some of the following settings.
|
||||
|
||||
|
||||
.. module:: openfe.protocols.openmm_afe.equil_afe_settings
|
||||
|
||||
.. autopydantic_model:: AbsoluteBindingSettings
|
||||
:model-show-json: False
|
||||
:model-show-field-summary: False
|
||||
:model-show-config-member: False
|
||||
:model-show-config-summary: False
|
||||
:model-show-validator-members: False
|
||||
:model-show-validator-summary: False
|
||||
:field-list-validators: False
|
||||
:inherited-members: SettingsBaseModel
|
||||
:exclude-members: get_defaults
|
||||
:member-order: bysource
|
||||
@@ -16,7 +16,8 @@ Protocol API Specification
|
||||
:toctree: generated/
|
||||
|
||||
PlainMDProtocol
|
||||
PlainMDProtocolUnit
|
||||
PlainMDSetupUnit
|
||||
PlainMDSimulationUnit
|
||||
PlainMDProtocolResult
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ can be found on the individual Protocol API reference documentation pages:
|
||||
Shared OpenMM Protocol Settings
|
||||
-------------------------------
|
||||
|
||||
The following are Settings clases which are shared between multiple
|
||||
The following are Settings classes which are shared between multiple
|
||||
OpenMM-based Protocols. Please note that not all Protocols use these
|
||||
Settings classes.
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ Protocol API specification
|
||||
:toctree: generated/
|
||||
|
||||
RelativeHybridTopologyProtocol
|
||||
RelativeHybridTopologyProtocolUnit
|
||||
HybridTopologySetupUnit
|
||||
HybridTopologyMultiStateSimulationUnit
|
||||
HybridTopologyMultiStateAnalysisUnit
|
||||
RelativeHybridTopologyProtocolResult
|
||||
|
||||
Protocol Settings
|
||||
|
||||
@@ -18,8 +18,10 @@ Protocol API specification
|
||||
SepTopProtocol
|
||||
SepTopComplexSetupUnit
|
||||
SepTopComplexRunUnit
|
||||
SepTopComplexAnalysisUnit
|
||||
SepTopSolventSetupUnit
|
||||
SepTopSolventRunUnit
|
||||
SepTopSolventAnalysisUnit
|
||||
SepTopProtocolResult
|
||||
|
||||
Protocol Settings
|
||||
|
||||
@@ -16,8 +16,12 @@ Protocol API specification
|
||||
:toctree: generated/
|
||||
|
||||
AbsoluteSolvationProtocol
|
||||
AbsoluteSolvationVacuumUnit
|
||||
AbsoluteSolvationSolventUnit
|
||||
AHFESolventAnalysisUnit
|
||||
AHFESolventSetupUnit
|
||||
AHFESolventSimUnit
|
||||
AHFEVacuumAnalysisUnit
|
||||
AHFEVacuumSetupUnit
|
||||
AHFEVacuumSimUnit
|
||||
AbsoluteSolvationProtocolResult
|
||||
|
||||
Protocol Settings
|
||||
|
||||
@@ -10,10 +10,14 @@ We describe a chemical system as being made up of one or more "components," e.g.
|
||||
:nosignatures:
|
||||
:toctree: generated/
|
||||
|
||||
ChemicalSystem
|
||||
Transformation
|
||||
Component
|
||||
SmallMoleculeComponent
|
||||
ProteinComponent
|
||||
ProteinMembraneComponent
|
||||
SolventComponent
|
||||
ChemicalSystem
|
||||
SolvatedPDBComponent
|
||||
|
||||
|
||||
Chemical System Generators
|
||||
|
||||
@@ -3,5 +3,21 @@
|
||||
``gather`` command
|
||||
====================
|
||||
|
||||
Currently, ``openfe gather`` is only able to gather results from Relative Binding Free Energy (RBFE) calculations.
|
||||
|
||||
To gather results from ABFE or SepTop protocols, you may use the experimental :ref:`openfe gather-abfe <gather-abfe>` and :ref:`openfe gather-septop <gather-septop>` CLI commands, but please note that these commands are still under development and liable to change in future releases, and meant to be used only for exploratory work.
|
||||
|
||||
.. click:: openfecli.commands.gather:gather
|
||||
:prog: openfe gather
|
||||
|
||||
|
||||
.. _gather-abfe:
|
||||
|
||||
.. click:: openfecli.commands.gather_abfe:gather_abfe
|
||||
:prog: openfe gather-abfe
|
||||
|
||||
|
||||
.. _gather-septop:
|
||||
|
||||
.. click:: openfecli.commands.gather_septop:gather_septop
|
||||
:prog: openfe gather-septop
|
||||
|
||||
3
docs/tutorials/abfe_analysis_tutorial.nblink
Normal file
3
docs/tutorials/abfe_analysis_tutorial.nblink
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"path": "../ExampleNotebooks/abfe_tutorial/abfe_analysis.ipynb"
|
||||
}
|
||||
6
docs/tutorials/abfe_tutorial.nblink
Normal file
6
docs/tutorials/abfe_tutorial.nblink
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"path": "../ExampleNotebooks/abfe_tutorial/abfe_tutorial.ipynb",
|
||||
"extra-media": [
|
||||
"../ExampleNotebooks/abfe_tutorial/abfe-cycle.png"
|
||||
]
|
||||
}
|
||||
@@ -12,12 +12,14 @@ Relative Free Energies
|
||||
----------------------
|
||||
|
||||
- :any:`Python API Showcase <showcase_notebook>`: Start here! An introduction to OpenFE's Python API and approach to performing a relative binding free energy calculation.
|
||||
- :any:`RBFE with the Python API <rbfe_python_tutorial>`: A step-by-step tutorial for using the Python API to calculate relative binding free energies for TYK2.
|
||||
- :ref:`RBFE with the CLI <rbfe_cli_tutorial>`: A step-by-step tutorial for using the OpenFE command line interface (CLI) to calculate relative binding free energies for TYK2.
|
||||
- :any:`RBFE using the Python API <rbfe_python_tutorial>`: A step-by-step tutorial for using the Python API to calculate relative binding free energies for TYK2.
|
||||
- :ref:`RBFE using the CLI <rbfe_cli_tutorial>`: A step-by-step tutorial for using the OpenFE command line interface (CLI) to calculate relative binding free energies for TYK2.
|
||||
- :any:`RBFE with membrane systems <rbfe_membrane_protein>`: A step-by-step guide to setting up an RBFE calculation with special considerations for membrane systems.
|
||||
|
||||
Absolute Free Energies
|
||||
----------------------
|
||||
|
||||
- :any:`Absolute Absolute Free Energy Protocol <abfe_tutorial>`: A walk-through of calculating the absolute binding free energy of toluene to T4 Lysozyme.
|
||||
- :any:`Absolute Solvation Free Energy Protocol <ahfe_tutorial>`: A walk-through of calculating the hydration free energy of a benzene ligand.
|
||||
|
||||
Relative Free Energies using Separated Topologies
|
||||
@@ -49,8 +51,12 @@ Generating Partial Charges
|
||||
showcase_notebook
|
||||
rbfe_python_tutorial
|
||||
rbfe_cli_tutorial
|
||||
rbfe_membrane_protein
|
||||
abfe_tutorial
|
||||
abfe_analysis_tutorial
|
||||
ahfe_tutorial
|
||||
septop_tutorial
|
||||
septop_analysis_tutorial
|
||||
md_tutorial
|
||||
plotting_with_cinnabar
|
||||
charge_molecules_cli_tutorial
|
||||
|
||||
3
docs/tutorials/rbfe_membrane_protein.nblink
Normal file
3
docs/tutorials/rbfe_membrane_protein.nblink
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"path": "../ExampleNotebooks/membranes/rbfe_membrane_protein.ipynb"
|
||||
}
|
||||
3
docs/tutorials/septop_analysis_tutorial.nblink
Normal file
3
docs/tutorials/septop_analysis_tutorial.nblink
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"path": "../ExampleNotebooks/openmm_septop/septop_analysis.ipynb"
|
||||
}
|
||||
@@ -5,28 +5,30 @@ dependencies:
|
||||
- cinnabar ~=0.5.0
|
||||
- click >=8.2.0
|
||||
- coverage
|
||||
- dask>=2025 # temporary fix for https://github.com/openforcefield/openff-units/issues/140
|
||||
- duecredit<0.10
|
||||
- kartograf>=1.2.0
|
||||
- konnektor~=0.2.0
|
||||
- lomap2>=3.2.1
|
||||
- networkx
|
||||
- numpy<2.3
|
||||
- openfe-analysis>=0.3.1
|
||||
- openff-interchange-base
|
||||
- numpy
|
||||
- openfe-analysis>=0.4.0 # min pin https://github.com/OpenFreeEnergy/openfe/issues/1834#issuecomment-3920079481, no max to check issues with new versions
|
||||
- openff-interchange-base >=0.5.0,!= 0.5.1 # https://github.com/openforcefield/openff-interchange/issues/1450 and https://github.com/OpenFreeEnergy/openfe/pull/1901
|
||||
- openff-nagl-base >=0.3.3
|
||||
- openff-nagl-models>=0.1.2
|
||||
- openff-toolkit-base >=0.16.2
|
||||
- openff-units==0.3.1 # https://github.com/OpenFreeEnergy/openfe/pull/1374
|
||||
- openmm ~=8.3.0
|
||||
- openmmforcefields
|
||||
- openmmtools >=0.25.0
|
||||
- openmm ~=8.4.0 # omit 8.3.0 and 8.3.1 due to https://github.com/openmm/openmm/pull/5069
|
||||
- openmmforcefields >=0.15.1 # min needed for https://github.com/openmm/openmmforcefields/pull/414
|
||||
- openmmtools >=0.26 # fix to support membrane barostat: https://github.com/choderalab/openmmtools/pull/798
|
||||
- packaging
|
||||
- pandas
|
||||
- parmed >=4.3.1 # fix to support numpy >=2.3: https://github.com/ParmEd/ParmEd/pull/1387
|
||||
- perses>=0.10.3
|
||||
- plugcli
|
||||
- pint>=0.24.0
|
||||
- pip
|
||||
- pooch
|
||||
- pooch >= 1.9.0 # min needed for https://github.com/fatiando/pooch/issues/502
|
||||
- py3dmol
|
||||
- pydantic >= 2.0.0, <2.12.0 # https://github.com/openforcefield/openff-interchange/issues/1346
|
||||
- pygraphviz
|
||||
@@ -52,3 +54,6 @@ dependencies:
|
||||
- threadpoolctl
|
||||
- pip:
|
||||
- git+https://github.com/OpenFreeEnergy/gufe@main
|
||||
- run_constrained:
|
||||
# drop this pin when handled upstream in espaloma-feedstock
|
||||
- smirnoff99frosst>=1.1.0.1 #https://github.com/openforcefield/smirnoff99Frosst/issues/109
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
**Added:**
|
||||
|
||||
* Added a cookbook for using ``jq`` to inspect JSON files.
|
||||
|
||||
**Changed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
25
news/afe-uuid.rst
Normal file
25
news/afe-uuid.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
**Added:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Changed:**
|
||||
|
||||
* AFE Protocols (AbsoluteBindingProtocol and AbsoluteSolvationProtocol)
|
||||
now assign a single uuid for all ProtocolUnits in a repeat rather than
|
||||
separating the uuid by legs of the transformation. PR #1948
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
@@ -1,25 +0,0 @@
|
||||
**Added:**
|
||||
|
||||
* The AbsoluteSolvationProtocol now properly implements the `validate` method,
|
||||
allowing users to verify inputs by calling the method directly (PR #1572).
|
||||
|
||||
**Changed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* Charged molecules are now explicitly disallowed in the
|
||||
AbsoluteSolvationProtocol (PR #1572).
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
@@ -1,23 +0,0 @@
|
||||
**Added:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Changed:**
|
||||
|
||||
* The default atom mapper used in the CLI has been changed from ``LomapAtomMapper`` to ``KartografAtomMapper`` in line with the recommended defaults from the industry benchmarking paper. Users who whish to continue to use ``LomapAtomMapper`` can so via the YAML configuration file, see the `documentation <https://docs.openfree.energy/en/latest/tutorials/rbfe_cli_tutorial.html#customize-your-campaign-setup>`_ for details (PR `#1530 <https://github.com/OpenFreeEnergy/openfe/pull/1530>`_).
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
@@ -1,23 +0,0 @@
|
||||
**Added:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Changed:**
|
||||
|
||||
* An improved error message is now shown when a mapping involving a changing constraint length cannot be fixed (PR `#1529 <https://github.com/OpenFreeEnergy/openfe/pull/1529>`_).
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
@@ -1,23 +0,0 @@
|
||||
**Added:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Changed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* Deprecated ``openfe.utils.ligand_utils.get_alchemical_charge_difference()``, which is replaced by ``LigandAtomMapping.get_alchemical_charge_difference()`` in ``gufe`` (PR #1479).
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
@@ -1,23 +0,0 @@
|
||||
**Added:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Changed:**
|
||||
|
||||
* Remove unnecessary limit on residues ids (``resids``) when getting mappings from topology in ``topology_helpers.py`` utility module.
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
@@ -1,23 +0,0 @@
|
||||
**Added:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Changed:**
|
||||
|
||||
* The relative hybrid topology protocol no longer runs the FIRE minimizer when ``dry=True``.
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
@@ -1,25 +0,0 @@
|
||||
**Added:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Changed:**
|
||||
|
||||
* Units must be explicitly assigned when defining ``Settings`` parameters, and values will be converted to match the default units for a given field. For example, use ``1.0 * units.bar`` or ``"1 bar"`` for pressure, and ``300 * unit.kelvin`` or ``"300 kelvin"`` for temperature.
|
||||
* For protocol developers: ``FloatQuantity`` is no longer supported. Instead, use `GufeQuantity` and `specify_quantity_units()` to make a `TypeAlias`.
|
||||
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
@@ -1,23 +0,0 @@
|
||||
**Added:**
|
||||
|
||||
* Added a new RBFE protocol based on Separated Topologies.
|
||||
|
||||
**Changed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
||||
@@ -1,48 +0,0 @@
|
||||
# silence pymbar logging warnings
|
||||
import logging
|
||||
def _mute_timeseries(record):
|
||||
return not "Warning on use of the timeseries module:" in record.msg
|
||||
def _mute_jax(record):
|
||||
return not "****** PyMBAR will use 64-bit JAX! *******" in record.msg
|
||||
_mbar_log = logging.getLogger("pymbar.timeseries")
|
||||
_mbar_log.addFilter(_mute_timeseries)
|
||||
_mbar_log = logging.getLogger("pymbar.mbar_solvers")
|
||||
_mbar_log.addFilter(_mute_jax)
|
||||
|
||||
from gufe import (
|
||||
ChemicalSystem,
|
||||
Component,
|
||||
ProteinComponent,
|
||||
SmallMoleculeComponent,
|
||||
SolventComponent,
|
||||
Transformation,
|
||||
NonTransformation,
|
||||
AlchemicalNetwork,
|
||||
LigandAtomMapping,
|
||||
)
|
||||
from gufe.protocols import (
|
||||
Protocol,
|
||||
ProtocolDAG,
|
||||
ProtocolUnit,
|
||||
ProtocolUnitResult, ProtocolUnitFailure,
|
||||
ProtocolDAGResult,
|
||||
ProtocolResult,
|
||||
execute_DAG,
|
||||
)
|
||||
|
||||
from . import utils
|
||||
from . import setup
|
||||
from .setup import (
|
||||
LomapAtomMapper,
|
||||
lomap_scorers,
|
||||
PersesAtomMapper,
|
||||
perses_scorers,
|
||||
ligand_network_planning,
|
||||
LigandNetwork,
|
||||
LigandAtomMapper,
|
||||
)
|
||||
from . import orchestration
|
||||
from . import analysis
|
||||
|
||||
from importlib.metadata import version
|
||||
__version__ = version("openfe")
|
||||
@@ -1,22 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
"""
|
||||
Run absolute free energy calculations using OpenMM and OpenMMTools.
|
||||
|
||||
"""
|
||||
|
||||
from .equil_solvation_afe_method import (
|
||||
AbsoluteSolvationProtocol,
|
||||
AbsoluteSolvationSettings,
|
||||
AbsoluteSolvationProtocolResult,
|
||||
AbsoluteSolvationVacuumUnit,
|
||||
AbsoluteSolvationSolventUnit,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AbsoluteSolvationProtocol",
|
||||
"AbsoluteSolvationSettings",
|
||||
"AbsoluteSolvationProtocolResult",
|
||||
"AbsoluteVacuumUnit",
|
||||
"AbsoluteSolventUnit",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,219 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
|
||||
"""Settings class for equilibrium AFE Protocols using OpenMM + OpenMMTools
|
||||
|
||||
This module implements the necessary settings necessary to run absolute free
|
||||
energies using OpenMM.
|
||||
|
||||
See Also
|
||||
--------
|
||||
openfe.protocols.openmm_afe.AbsoluteSolvationProtocol
|
||||
|
||||
TODO
|
||||
----
|
||||
* Add support for restraints
|
||||
|
||||
"""
|
||||
from gufe.settings import (
|
||||
SettingsBaseModel,
|
||||
OpenMMSystemGeneratorFFSettings,
|
||||
ThermoSettings,
|
||||
)
|
||||
from openfe.protocols.openmm_utils.omm_settings import (
|
||||
MultiStateSimulationSettings,
|
||||
BaseSolvationSettings,
|
||||
OpenMMSolvationSettings,
|
||||
OpenMMEngineSettings,
|
||||
IntegratorSettings,
|
||||
OpenFFPartialChargeSettings,
|
||||
MultiStateOutputSettings,
|
||||
MDSimulationSettings,
|
||||
MDOutputSettings,
|
||||
)
|
||||
import numpy as np
|
||||
|
||||
from pydantic import field_validator
|
||||
|
||||
|
||||
class AlchemicalSettings(SettingsBaseModel):
|
||||
"""Settings for the alchemical protocol
|
||||
|
||||
Empty place holder for right now.
|
||||
"""
|
||||
|
||||
|
||||
class LambdaSettings(SettingsBaseModel):
|
||||
"""Lambda schedule settings.
|
||||
|
||||
Defines lists of floats to control various aspects of the alchemical
|
||||
transformation.
|
||||
|
||||
Notes
|
||||
-----
|
||||
* In all cases a lambda value of 0 defines a fully interacting state A and
|
||||
a non-interacting state B, whilst a value of 1 defines a fully interacting
|
||||
state B and a non-interacting state A.
|
||||
* ``lambda_elec``, `lambda_vdw``, and ``lambda_restraints`` must all be of
|
||||
the same length, defining all the windows of the transformation.
|
||||
|
||||
"""
|
||||
lambda_elec: list[float] = [
|
||||
0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
]
|
||||
"""
|
||||
List of floats of lambda values for the electrostatics.
|
||||
Zero means state A and 1 means state B.
|
||||
Length of this list needs to match length of lambda_vdw and lambda_restraints.
|
||||
"""
|
||||
lambda_vdw: list[float] = [
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.05, 0.1, 0.2, 0.3, 0.4,
|
||||
0.5, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0,
|
||||
]
|
||||
"""
|
||||
List of floats of lambda values for the van der Waals.
|
||||
Zero means state A and 1 means state B.
|
||||
Length of this list needs to match length of lambda_elec and lambda_restraints.
|
||||
"""
|
||||
lambda_restraints: list[float] = [
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
]
|
||||
"""
|
||||
List of floats of lambda values for the restraints.
|
||||
Zero means state A and 1 means state B.
|
||||
Length of this list needs to match length of lambda_vdw and lambda_elec.
|
||||
"""
|
||||
|
||||
@field_validator('lambda_elec', 'lambda_vdw', 'lambda_restraints')
|
||||
def must_be_between_0_and_1(cls, v):
|
||||
for window in v:
|
||||
if not 0 <= window <= 1:
|
||||
errmsg = ("Lambda windows must be between 0 and 1, got a"
|
||||
f" window with value {window}.")
|
||||
raise ValueError(errmsg)
|
||||
return v
|
||||
|
||||
@field_validator('lambda_elec', 'lambda_vdw', 'lambda_restraints')
|
||||
def must_be_monotonic(cls, v):
|
||||
|
||||
difference = np.diff(v)
|
||||
|
||||
if not all(i >= 0. for i in difference):
|
||||
errmsg = f"The lambda schedule is not monotonic, got schedule {v}."
|
||||
raise ValueError(errmsg)
|
||||
|
||||
return v
|
||||
|
||||
|
||||
# This subclasses from SettingsBaseModel as it has vacuum_forcefield and
|
||||
# solvent_forcefield fields, not just a single forcefield_settings field
|
||||
class AbsoluteSolvationSettings(SettingsBaseModel):
|
||||
"""
|
||||
Configuration object for ``AbsoluteSolvationProtocol``.
|
||||
|
||||
See Also
|
||||
--------
|
||||
openfe.protocols.openmm_afe.AbsoluteSolvationProtocol
|
||||
"""
|
||||
protocol_repeats: int
|
||||
"""
|
||||
The number of completely independent repeats of the entire sampling
|
||||
process. The mean of the repeats defines the final estimate of FE
|
||||
difference, while the variance between repeats is used as the uncertainty.
|
||||
"""
|
||||
|
||||
@field_validator('protocol_repeats')
|
||||
def must_be_positive(cls, v):
|
||||
if v <= 0:
|
||||
errmsg = f"protocol_repeats must be a positive value, got {v}."
|
||||
raise ValueError(errmsg)
|
||||
return v
|
||||
|
||||
# Inherited things
|
||||
solvent_forcefield_settings: OpenMMSystemGeneratorFFSettings
|
||||
vacuum_forcefield_settings: OpenMMSystemGeneratorFFSettings
|
||||
"""Parameters to set up the force field with OpenMM Force Fields"""
|
||||
thermo_settings: ThermoSettings
|
||||
"""Settings for thermodynamic parameters"""
|
||||
|
||||
solvation_settings: OpenMMSolvationSettings
|
||||
"""Settings for solvating the system."""
|
||||
|
||||
# Alchemical settings
|
||||
alchemical_settings: AlchemicalSettings
|
||||
"""
|
||||
Alchemical protocol settings.
|
||||
"""
|
||||
lambda_settings: LambdaSettings
|
||||
"""
|
||||
Settings for controlling the lambda schedule for the different components
|
||||
(vdw, elec, restraints).
|
||||
"""
|
||||
|
||||
# MD Engine things
|
||||
vacuum_engine_settings: OpenMMEngineSettings
|
||||
"""
|
||||
Settings specific to the OpenMM engine, such as the compute platform
|
||||
for the vacuum transformation.
|
||||
"""
|
||||
solvent_engine_settings: OpenMMEngineSettings
|
||||
"""
|
||||
Settings specific to the OpenMM engine, such as the compute platform
|
||||
for the solvent transformation.
|
||||
"""
|
||||
|
||||
# Sampling State defining things
|
||||
integrator_settings: IntegratorSettings
|
||||
"""
|
||||
Settings for controlling the integrator, such as the timestep and
|
||||
barostat settings.
|
||||
"""
|
||||
|
||||
# Simulation run settings
|
||||
vacuum_equil_simulation_settings: MDSimulationSettings
|
||||
"""
|
||||
Pre-alchemical vacuum simulation control settings.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The `NVT` equilibration should be set to 0 * unit.nanosecond
|
||||
as it will not be run.
|
||||
"""
|
||||
vacuum_simulation_settings: MultiStateSimulationSettings
|
||||
"""
|
||||
Simulation control settings, including simulation lengths
|
||||
for the vacuum transformation.
|
||||
"""
|
||||
solvent_equil_simulation_settings: MDSimulationSettings
|
||||
"""
|
||||
Pre-alchemical solvent simulation control settings.
|
||||
"""
|
||||
solvent_simulation_settings: MultiStateSimulationSettings
|
||||
"""
|
||||
Simulation control settings, including simulation lengths
|
||||
for the solvent transformation.
|
||||
"""
|
||||
vacuum_equil_output_settings: MDOutputSettings
|
||||
"""
|
||||
Simulation output settings for the vacuum non-alchemical equilibration.
|
||||
"""
|
||||
vacuum_output_settings: MultiStateOutputSettings
|
||||
"""
|
||||
Simulation output settings for the vacuum transformation.
|
||||
"""
|
||||
solvent_equil_output_settings: MDOutputSettings
|
||||
"""
|
||||
Simulation output settings for the solvent non-alchemical equilibration.
|
||||
"""
|
||||
solvent_output_settings: MultiStateOutputSettings
|
||||
"""
|
||||
Simulation output settings for the solvent transformation.
|
||||
"""
|
||||
partial_charge_settings: OpenFFPartialChargeSettings
|
||||
"""
|
||||
Settings for controlling how to assign partial charges,
|
||||
including the partial charge assignment method, and the
|
||||
number of conformers used to generate the partial charges.
|
||||
"""
|
||||
@@ -1,969 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
"""OpenMM Equilibrium Solvation AFE Protocol --- :mod:`openfe.protocols.openmm_afe.equil_solvation_afe_method`
|
||||
===============================================================================================================
|
||||
|
||||
This module implements the necessary methodology tooling to run calculate an
|
||||
absolute solvation free energy using OpenMM tools and one of the following
|
||||
alchemical sampling methods:
|
||||
|
||||
* Hamiltonian Replica Exchange
|
||||
* Self-adjusted mixture sampling
|
||||
* Independent window sampling
|
||||
|
||||
Current limitations
|
||||
-------------------
|
||||
* Alchemical species with a net charge are not currently supported.
|
||||
* Disapearing molecules are only allowed in state A. Support for
|
||||
appearing molecules will be added in due course.
|
||||
* Only small molecules are allowed to act as alchemical molecules.
|
||||
Alchemically changing protein or solvent components would induce
|
||||
perturbations which are too large to be handled by this Protocol.
|
||||
|
||||
|
||||
Acknowledgements
|
||||
----------------
|
||||
* Originally based on hydration.py in
|
||||
`espaloma_charge <https://github.com/choderalab/espaloma_charge>`_
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import logging
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
import gufe
|
||||
from gufe.components import Component
|
||||
import itertools
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from openff.units import unit, Quantity
|
||||
from openmmtools import multistate
|
||||
from typing import Optional, Union
|
||||
from typing import Any, Iterable
|
||||
import uuid
|
||||
|
||||
from gufe import (
|
||||
settings,
|
||||
ChemicalSystem, SmallMoleculeComponent,
|
||||
ProteinComponent, SolventComponent
|
||||
)
|
||||
from openfe.protocols.openmm_afe.equil_afe_settings import (
|
||||
AbsoluteSolvationSettings,
|
||||
OpenMMSolvationSettings, AlchemicalSettings, LambdaSettings,
|
||||
MDSimulationSettings, MDOutputSettings,
|
||||
MultiStateSimulationSettings, OpenMMEngineSettings,
|
||||
IntegratorSettings, MultiStateOutputSettings,
|
||||
OpenFFPartialChargeSettings,
|
||||
SettingsBaseModel,
|
||||
)
|
||||
from ..openmm_utils import system_validation, settings_validation
|
||||
from .base import BaseAbsoluteUnit
|
||||
from openfe.utils import log_system_probe
|
||||
from openfe.due import due, Doi
|
||||
|
||||
|
||||
due.cite(Doi("10.5281/zenodo.596504"),
|
||||
description="Yank",
|
||||
path="openfe.protocols.openmm_afe.equil_solvation_afe_method",
|
||||
cite_module=True)
|
||||
|
||||
due.cite(Doi("10.48550/ARXIV.2302.06758"),
|
||||
description="EspalomaCharge",
|
||||
path="openfe.protocols.openmm_afe.equil_solvation_afe_method",
|
||||
cite_module=True)
|
||||
|
||||
due.cite(Doi("10.5281/zenodo.596622"),
|
||||
description="OpenMMTools",
|
||||
path="openfe.protocols.openmm_afe.equil_solvation_afe_method",
|
||||
cite_module=True)
|
||||
|
||||
due.cite(Doi("10.1371/journal.pcbi.1005659"),
|
||||
description="OpenMM",
|
||||
path="openfe.protocols.openmm_afe.equil_solvation_afe_method",
|
||||
cite_module=True)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AbsoluteSolvationProtocolResult(gufe.ProtocolResult):
|
||||
"""Dict-like container for the output of a AbsoluteSolvationProtocol
|
||||
"""
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
# TODO: Detect when we have extensions and stitch these together?
|
||||
if any(len(pur_list) > 2 for pur_list
|
||||
in itertools.chain(self.data['solvent'].values(), self.data['vacuum'].values())):
|
||||
raise NotImplementedError("Can't stitch together results yet")
|
||||
|
||||
def get_individual_estimates(self) -> dict[str, list[tuple[Quantity, Quantity]]]:
|
||||
"""
|
||||
Get the individual estimate of the free energies.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dGs : dict[str, list[tuple[openff.units.Quantity, openff.units.Quantity]]]
|
||||
A dictionary, keyed `solvent` and `vacuum` for each leg
|
||||
of the thermodynamic cycle, with lists of tuples containing
|
||||
the individual free energy estimates and associated MBAR
|
||||
uncertainties for each repeat of that simulation type.
|
||||
"""
|
||||
vac_dGs = []
|
||||
solv_dGs = []
|
||||
|
||||
for pus in self.data['vacuum'].values():
|
||||
vac_dGs.append((
|
||||
pus[0].outputs['unit_estimate'],
|
||||
pus[0].outputs['unit_estimate_error']
|
||||
))
|
||||
|
||||
for pus in self.data['solvent'].values():
|
||||
solv_dGs.append((
|
||||
pus[0].outputs['unit_estimate'],
|
||||
pus[0].outputs['unit_estimate_error']
|
||||
))
|
||||
|
||||
return {'solvent': solv_dGs, 'vacuum': vac_dGs}
|
||||
|
||||
def get_estimate(self):
|
||||
"""Get the solvation free energy estimate for this calculation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dG : openff.units.Quantity
|
||||
The solvation free energy. This is a Quantity defined with units.
|
||||
"""
|
||||
def _get_average(estimates):
|
||||
# Get the unit value of the first value in the estimates
|
||||
u = estimates[0][0].u
|
||||
# Loop through estimates and get the free energy values
|
||||
# in the unit of the first estimate
|
||||
dGs = [i[0].to(u).m for i in estimates]
|
||||
|
||||
return np.average(dGs) * u
|
||||
|
||||
individual_estimates = self.get_individual_estimates()
|
||||
vac_dG = _get_average(individual_estimates['vacuum'])
|
||||
solv_dG = _get_average(individual_estimates['solvent'])
|
||||
|
||||
return vac_dG - solv_dG
|
||||
|
||||
def get_uncertainty(self):
|
||||
"""Get the solvation free energy error for this calculation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
err : openff.units.Quantity
|
||||
The standard deviation between estimates of the solvation free
|
||||
energy. This is a Quantity defined with units.
|
||||
"""
|
||||
def _get_stdev(estimates):
|
||||
# Get the unit value of the first value in the estimates
|
||||
u = estimates[0][0].u
|
||||
# Loop through estimates and get the free energy values
|
||||
# in the unit of the first estimate
|
||||
dGs = [i[0].to(u).m for i in estimates]
|
||||
|
||||
return np.std(dGs) * u
|
||||
|
||||
individual_estimates = self.get_individual_estimates()
|
||||
vac_err = _get_stdev(individual_estimates['vacuum'])
|
||||
solv_err = _get_stdev(individual_estimates['solvent'])
|
||||
|
||||
# return the combined error
|
||||
return np.sqrt(vac_err**2 + solv_err**2)
|
||||
|
||||
def get_forward_and_reverse_energy_analysis(self) -> dict[str, list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]]:
|
||||
"""
|
||||
Get the reverse and forward analysis of the free energies.
|
||||
|
||||
Returns
|
||||
-------
|
||||
forward_reverse : dict[str, list[Optional[dict[str, Union[npt.NDArray, openff.units.Quantity]]]]]
|
||||
A dictionary, keyed `solvent` and `vacuum` for each leg of the
|
||||
thermodynamic cycle which each contain a list of dictionaries
|
||||
containing the forward and reverse analysis of each repeat
|
||||
of that simulation type.
|
||||
|
||||
The forward and reverse analysis dictionaries contain:
|
||||
- `fractions`: npt.NDArray
|
||||
The fractions of data used for the estimates
|
||||
- `forward_DGs`, `reverse_DGs`: openff.units.Quantity
|
||||
The forward and reverse estimates for each fraction of data
|
||||
- `forward_dDGs`, `reverse_dDGs`: openff.units.Quantity
|
||||
The forward and reverse estimate uncertainty for each
|
||||
fraction of data.
|
||||
|
||||
If one of the cycle leg list entries is ``None``, this indicates
|
||||
that the analysis could not be carried out for that repeat. This
|
||||
is most likely caused by MBAR convergence issues when attempting to
|
||||
calculate free energies from too few samples.
|
||||
|
||||
Raises
|
||||
------
|
||||
UserWarning
|
||||
* If any of the forward and reverse dictionaries are ``None`` in a
|
||||
given thermodynamic cycle leg.
|
||||
"""
|
||||
|
||||
forward_reverse: dict[str, list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]] = {}
|
||||
|
||||
for key in ['solvent', 'vacuum']:
|
||||
forward_reverse[key] = [
|
||||
pus[0].outputs['forward_and_reverse_energies']
|
||||
for pus in self.data[key].values()
|
||||
]
|
||||
|
||||
if None in forward_reverse[key]:
|
||||
wmsg = (
|
||||
"One or more ``None`` entries were found in the forward "
|
||||
f"and reverse dictionaries of the repeats of the {key} "
|
||||
"calculations. This is likely caused by an MBAR convergence "
|
||||
"failure caused by too few independent samples when "
|
||||
"calculating the free energies of the 10% timeseries slice."
|
||||
)
|
||||
warnings.warn(wmsg)
|
||||
|
||||
return forward_reverse
|
||||
|
||||
def get_overlap_matrices(self) -> dict[str, list[dict[str, npt.NDArray]]]:
|
||||
"""
|
||||
Get a the MBAR overlap estimates for all legs of the simulation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
overlap_stats : dict[str, list[dict[str, npt.NDArray]]]
|
||||
A dictionary with keys `solvent` and `vacuum` for each
|
||||
leg of the thermodynamic cycle, which each containing a
|
||||
list of dictionaries with the MBAR overlap estimates of
|
||||
each repeat of that simulation type.
|
||||
|
||||
The underlying MBAR dictionaries contain the following keys:
|
||||
* ``scalar``: One minus the largest nontrivial eigenvalue
|
||||
* ``eigenvalues``: The sorted (descending) eigenvalues of the
|
||||
overlap matrix
|
||||
* ``matrix``: Estimated overlap matrix of observing a sample from
|
||||
state i in state j
|
||||
"""
|
||||
# Loop through and get the repeats and get the matrices
|
||||
overlap_stats: dict[str, list[dict[str, npt.NDArray]]] = {}
|
||||
|
||||
for key in ['solvent', 'vacuum']:
|
||||
overlap_stats[key] = [
|
||||
pus[0].outputs['unit_mbar_overlap']
|
||||
for pus in self.data[key].values()
|
||||
]
|
||||
|
||||
return overlap_stats
|
||||
|
||||
def get_replica_transition_statistics(self) -> dict[str, list[dict[str, npt.NDArray]]]:
|
||||
"""
|
||||
Get the replica exchange transition statistics for all
|
||||
legs of the simulation.
|
||||
|
||||
Note
|
||||
----
|
||||
This is currently only available in cases where a replica exchange
|
||||
simulation was run.
|
||||
|
||||
Returns
|
||||
-------
|
||||
repex_stats : dict[str, list[dict[str, npt.NDArray]]]
|
||||
A dictionary with keys `solvent` and `vacuum` for each
|
||||
leg of the thermodynamic cycle, which each containing
|
||||
a list of dictionaries containing the replica transition
|
||||
statistics for each repeat of that simulation type.
|
||||
|
||||
The replica transition statistics dictionaries contain the following:
|
||||
* ``eigenvalues``: The sorted (descending) eigenvalues of the
|
||||
lambda state transition matrix
|
||||
* ``matrix``: The transition matrix estimate of a replica switching
|
||||
from state i to state j.
|
||||
"""
|
||||
repex_stats: dict[str, list[dict[str, npt.NDArray]]] = {}
|
||||
try:
|
||||
for key in ['solvent', 'vacuum']:
|
||||
repex_stats[key] = [
|
||||
pus[0].outputs['replica_exchange_statistics']
|
||||
for pus in self.data[key].values()
|
||||
]
|
||||
except KeyError:
|
||||
errmsg = ("Replica exchange statistics were not found, "
|
||||
"did you run a repex calculation?")
|
||||
raise ValueError(errmsg)
|
||||
|
||||
return repex_stats
|
||||
|
||||
def get_replica_states(self) -> dict[str, list[npt.NDArray]]:
|
||||
"""
|
||||
Get the timeseries of replica states for all simulation legs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
replica_states : dict[str, list[npt.NDArray]]
|
||||
Dictionary keyed `solvent` and `vacuum` for each leg of
|
||||
the thermodynamic cycle, with lists of replica states
|
||||
timeseries for each repeat of that simulation type.
|
||||
"""
|
||||
replica_states: dict[str, list[npt.NDArray]] = {
|
||||
'solvent': [], 'vacuum': []
|
||||
}
|
||||
|
||||
def is_file(filename: str):
|
||||
p = pathlib.Path(filename)
|
||||
|
||||
if not p.exists():
|
||||
errmsg = f"File could not be found {p}"
|
||||
raise ValueError(errmsg)
|
||||
|
||||
return p
|
||||
|
||||
def get_replica_state(nc, chk):
|
||||
nc = is_file(nc)
|
||||
dir_path = nc.parents[0]
|
||||
chk = is_file(dir_path / chk).name
|
||||
|
||||
reporter = multistate.MultiStateReporter(
|
||||
storage=nc, checkpoint_storage=chk, open_mode='r'
|
||||
)
|
||||
|
||||
retval = np.asarray(reporter.read_replica_thermodynamic_states())
|
||||
reporter.close()
|
||||
|
||||
return retval
|
||||
|
||||
for key in ['solvent', 'vacuum']:
|
||||
for pus in self.data[key].values():
|
||||
states = get_replica_state(
|
||||
pus[0].outputs['nc'],
|
||||
pus[0].outputs['last_checkpoint'],
|
||||
)
|
||||
replica_states[key].append(states)
|
||||
|
||||
return replica_states
|
||||
|
||||
def equilibration_iterations(self) -> dict[str, list[float]]:
|
||||
"""
|
||||
Get the number of equilibration iterations for each simulation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
equilibration_lengths : dict[str, list[float]]
|
||||
Dictionary keyed `solvent` and `vacuum` for each leg
|
||||
of the thermodynamic cycle, with lists containing the
|
||||
number of equilibration iterations for each repeat
|
||||
of that simulation type.
|
||||
"""
|
||||
equilibration_lengths: dict[str, list[float]] = {}
|
||||
|
||||
for key in ['solvent', 'vacuum']:
|
||||
equilibration_lengths[key] = [
|
||||
pus[0].outputs['equilibration_iterations']
|
||||
for pus in self.data[key].values()
|
||||
]
|
||||
|
||||
return equilibration_lengths
|
||||
|
||||
def production_iterations(self) -> dict[str, list[float]]:
|
||||
"""
|
||||
Get the number of production iterations for each simulation.
|
||||
Returns the number of uncorrelated production samples for each
|
||||
repeat of the calculation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
production_lengths : dict[str, list[float]]
|
||||
Dictionary keyed `solvent` and `vacuum` for each leg of the
|
||||
thermodynamic cycle, with lists with the number
|
||||
of production iterations for each repeat of that simulation
|
||||
type.
|
||||
"""
|
||||
production_lengths: dict[str, list[float]] = {}
|
||||
|
||||
for key in ['solvent', 'vacuum']:
|
||||
production_lengths[key] = [
|
||||
pus[0].outputs['production_iterations']
|
||||
for pus in self.data[key].values()
|
||||
]
|
||||
|
||||
return production_lengths
|
||||
|
||||
|
||||
class AbsoluteSolvationProtocol(gufe.Protocol):
|
||||
"""
|
||||
Absolute solvation free energy calculations using OpenMM and OpenMMTools.
|
||||
|
||||
See Also
|
||||
--------
|
||||
:mod:`openfe.protocols`
|
||||
:class:`openfe.protocols.openmm_afe.AbsoluteSolvationSettings`
|
||||
:class:`openfe.protocols.openmm_afe.AbsoluteSolvationProtocolResult`
|
||||
:class:`openfe.protocols.openmm_afe.AbsoluteSolvationVacuumUnit`
|
||||
:class:`openfe.protocols.openmm_afe.AbsoluteSolvationSolventUnit`
|
||||
"""
|
||||
result_cls = AbsoluteSolvationProtocolResult
|
||||
_settings_cls = AbsoluteSolvationSettings
|
||||
_settings: AbsoluteSolvationSettings
|
||||
|
||||
@classmethod
|
||||
def _default_settings(cls):
|
||||
"""A dictionary of initial settings for this creating this Protocol
|
||||
|
||||
These settings are intended as a suitable starting point for creating
|
||||
an instance of this protocol. It is recommended, however that care is
|
||||
taken to inspect and customize these before performing a Protocol.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Settings
|
||||
a set of default settings
|
||||
"""
|
||||
return AbsoluteSolvationSettings(
|
||||
protocol_repeats=3,
|
||||
solvent_forcefield_settings=settings.OpenMMSystemGeneratorFFSettings(),
|
||||
vacuum_forcefield_settings=settings.OpenMMSystemGeneratorFFSettings(
|
||||
nonbonded_method='nocutoff',
|
||||
),
|
||||
thermo_settings=settings.ThermoSettings(
|
||||
temperature=298.15 * unit.kelvin,
|
||||
pressure=1 * unit.bar,
|
||||
),
|
||||
alchemical_settings=AlchemicalSettings(),
|
||||
lambda_settings=LambdaSettings(
|
||||
lambda_elec=[
|
||||
0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0,
|
||||
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
|
||||
lambda_vdw=[
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.12, 0.24,
|
||||
0.36, 0.48, 0.6, 0.7, 0.77, 0.85, 1.0],
|
||||
lambda_restraints=[
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
),
|
||||
partial_charge_settings=OpenFFPartialChargeSettings(),
|
||||
solvation_settings=OpenMMSolvationSettings(),
|
||||
vacuum_engine_settings=OpenMMEngineSettings(),
|
||||
solvent_engine_settings=OpenMMEngineSettings(),
|
||||
integrator_settings=IntegratorSettings(),
|
||||
solvent_equil_simulation_settings=MDSimulationSettings(
|
||||
equilibration_length_nvt=0.1 * unit.nanosecond,
|
||||
equilibration_length=0.2 * unit.nanosecond,
|
||||
production_length=0.5 * unit.nanosecond,
|
||||
),
|
||||
solvent_equil_output_settings=MDOutputSettings(
|
||||
equil_nvt_structure='equil_nvt_structure.pdb',
|
||||
equil_npt_structure='equil_npt_structure.pdb',
|
||||
production_trajectory_filename='production_equil.xtc',
|
||||
log_output='equil_simulation.log',
|
||||
),
|
||||
solvent_simulation_settings=MultiStateSimulationSettings(
|
||||
n_replicas=14,
|
||||
equilibration_length=1.0 * unit.nanosecond,
|
||||
production_length=10.0 * unit.nanosecond,
|
||||
),
|
||||
solvent_output_settings=MultiStateOutputSettings(
|
||||
output_filename='solvent.nc',
|
||||
checkpoint_storage_filename='solvent_checkpoint.nc',
|
||||
),
|
||||
vacuum_equil_simulation_settings=MDSimulationSettings(
|
||||
equilibration_length_nvt=None,
|
||||
equilibration_length=0.2 * unit.nanosecond,
|
||||
production_length=0.5 * unit.nanosecond,
|
||||
),
|
||||
vacuum_equil_output_settings=MDOutputSettings(
|
||||
equil_nvt_structure=None,
|
||||
equil_npt_structure='equil_structure.pdb',
|
||||
production_trajectory_filename='production_equil.xtc',
|
||||
log_output='equil_simulation.log',
|
||||
),
|
||||
vacuum_simulation_settings=MultiStateSimulationSettings(
|
||||
n_replicas=14,
|
||||
equilibration_length=0.5 * unit.nanosecond,
|
||||
production_length=2.0 * unit.nanosecond,
|
||||
),
|
||||
vacuum_output_settings=MultiStateOutputSettings(
|
||||
output_filename='vacuum.nc',
|
||||
checkpoint_storage_filename='vacuum_checkpoint.nc'
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_endstates(
|
||||
stateA: ChemicalSystem, stateB: ChemicalSystem,
|
||||
) -> None:
|
||||
"""
|
||||
A solvent transformation is defined (in terms of gufe components)
|
||||
as starting from one or more ligands in solvent and
|
||||
ending up in a state with one less ligand.
|
||||
|
||||
No protein components are allowed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stateA : ChemicalSystem
|
||||
The chemical system of end state A
|
||||
stateB : ChemicalSystem
|
||||
The chemical system of end state B
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If stateA or stateB contains a ProteinComponent.
|
||||
If there is no SolventComponent in either stateA or stateB.
|
||||
If there are alchemical components in state B.
|
||||
If there are non SmallMoleculeComponent alchemical species.
|
||||
If there are more than one alchemical species.
|
||||
If the alchemical species is charged.
|
||||
|
||||
Notes
|
||||
-----
|
||||
* Currently doesn't support alchemical components in state B.
|
||||
* Currently doesn't support alchemical components which are not
|
||||
SmallMoleculeComponents.
|
||||
* Currently doesn't support more than one alchemical component
|
||||
being desolvated.
|
||||
* Currently doesn't support charged alchemical components.
|
||||
* Solvent must always be present in both end states.
|
||||
"""
|
||||
# Check that there are no protein components
|
||||
if stateA.contains(ProteinComponent) or stateB.contains(ProteinComponent):
|
||||
errmsg = ("Protein components are not allowed for "
|
||||
"absolute solvation free energies.")
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# Check that there is a solvent component in both end states
|
||||
if not (stateA.contains(SolventComponent) and stateB.contains(SolventComponent)):
|
||||
errmsg = "No SolventComponent found in stateA and/or stateB"
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# Now we check the alchemical Components
|
||||
diff = stateA.component_diff(stateB)
|
||||
|
||||
# Check that there's only one state A unique Component
|
||||
if len(diff[0]) != 1:
|
||||
errmsg = (
|
||||
"Only one alchemical species is supported "
|
||||
"for absolute solvation free energies. "
|
||||
f"Number of unique components found in stateA: {len(diff[0])}."
|
||||
)
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# Make sure that the state A unique is an SMC
|
||||
if not isinstance(diff[0][0], SmallMoleculeComponent):
|
||||
errmsg = (
|
||||
"Only dissapearing SmallMoleculeComponents "
|
||||
"are supported by this protocol. "
|
||||
f"Found a {type(diff[0][0])}"
|
||||
)
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# Check that the state A unique isn't charged
|
||||
if diff[0][0].total_charge != 0:
|
||||
errmsg = (
|
||||
"Charged alchemical molecules are not currently "
|
||||
"supported for solvation free energies. "
|
||||
f"Molecule total charge: {diff[0][0].total_charge}."
|
||||
)
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# If there are any alchemical Components in state B
|
||||
if len(diff[1]) > 0:
|
||||
errmsg = ("Components appearing in state B are not "
|
||||
"currently supported")
|
||||
raise ValueError(errmsg)
|
||||
|
||||
@staticmethod
|
||||
def _validate_lambda_schedule(
|
||||
lambda_settings: LambdaSettings,
|
||||
simulation_settings: MultiStateSimulationSettings,
|
||||
) -> None:
|
||||
"""
|
||||
Checks that the lambda schedule is set up correctly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lambda_settings : LambdaSettings
|
||||
the lambda schedule Settings
|
||||
simulation_settings : MultiStateSimulationSettings
|
||||
the settings for either the vacuum or solvent phase
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the number of lambda windows differs for electrostatics and sterics.
|
||||
If the number of replicas does not match the number of lambda windows.
|
||||
If there are states with naked charges.
|
||||
Warnings
|
||||
If there are non-zero values for restraints (lambda_restraints).
|
||||
"""
|
||||
|
||||
lambda_elec = lambda_settings.lambda_elec
|
||||
lambda_vdw = lambda_settings.lambda_vdw
|
||||
lambda_restraints = lambda_settings.lambda_restraints
|
||||
n_replicas = simulation_settings.n_replicas
|
||||
|
||||
# Ensure that all lambda components have equal amount of windows
|
||||
lambda_components = [lambda_vdw, lambda_elec, lambda_restraints]
|
||||
it = iter(lambda_components)
|
||||
the_len = len(next(it))
|
||||
if not all(len(l) == the_len for l in it):
|
||||
errmsg = (
|
||||
"Components elec, vdw, and restraints must have equal amount"
|
||||
f" of lambda windows. Got {len(lambda_elec)} elec lambda"
|
||||
f" windows, {len(lambda_vdw)} vdw lambda windows, and"
|
||||
f"{len(lambda_restraints)} restraints lambda windows.")
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# Ensure that number of overall lambda windows matches number of lambda
|
||||
# windows for individual components
|
||||
if n_replicas != len(lambda_vdw):
|
||||
errmsg = (f"Number of replicas {n_replicas} does not equal the"
|
||||
f" number of lambda windows {len(lambda_vdw)}")
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# Check if there are lambda windows with naked charges
|
||||
for inx, lam in enumerate(lambda_elec):
|
||||
if lam < 1 and lambda_vdw[inx] == 1:
|
||||
errmsg = (
|
||||
"There are states along this lambda schedule "
|
||||
"where there are atoms with charges but no LJ "
|
||||
f"interactions: lambda {inx}: "
|
||||
f"elec {lam} vdW {lambda_vdw[inx]}")
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# Check if there are lambda windows with non-zero restraints
|
||||
if len([r for r in lambda_restraints if r != 0]) > 0:
|
||||
wmsg = ("Non-zero restraint lambdas applied. The absolute "
|
||||
"solvation protocol doesn't apply restraints, "
|
||||
"therefore restraints won't be applied. "
|
||||
f"Given lambda_restraints: {lambda_restraints}")
|
||||
logger.warning(wmsg)
|
||||
warnings.warn(wmsg)
|
||||
|
||||
def _validate(
|
||||
self,
|
||||
*,
|
||||
stateA: ChemicalSystem,
|
||||
stateB: ChemicalSystem,
|
||||
mapping: Optional[Union[gufe.ComponentMapping, list[gufe.ComponentMapping]]] = None,
|
||||
extends: Optional[gufe.ProtocolDAGResult] = None,
|
||||
):
|
||||
# Check we're not extending
|
||||
if extends is not None:
|
||||
# This should be a NotImplementedError, but the underlying
|
||||
# `validate` method wraps a call to `_validate` around a
|
||||
# NotImplementedError exception guard
|
||||
raise ValueError("Can't extend simulations yet")
|
||||
|
||||
# Check we're not using a mapping, since we're not doing anything with it
|
||||
if mapping is not None:
|
||||
wmsg = "A mapping was passed but is not used by this Protocol."
|
||||
warnings.warn(wmsg)
|
||||
|
||||
# Validate the endstates & alchemical components
|
||||
self._validate_endstates(stateA, stateB)
|
||||
|
||||
# Validate the lambda schedule
|
||||
for solv_sets in (
|
||||
self.settings.solvent_simulation_settings,
|
||||
self.settings.vacuum_simulation_settings
|
||||
):
|
||||
self._validate_lambda_schedule(
|
||||
self.settings.lambda_settings,
|
||||
solv_sets,
|
||||
)
|
||||
|
||||
# Check nonbond & solvent compatibility
|
||||
solv_nonbonded_method = self.settings.solvent_forcefield_settings.nonbonded_method
|
||||
vac_nonbonded_method = self.settings.vacuum_forcefield_settings.nonbonded_method
|
||||
|
||||
# Use the more complete system validation solvent checks
|
||||
system_validation.validate_solvent(stateA, solv_nonbonded_method)
|
||||
|
||||
# Gas phase is always gas phase
|
||||
if vac_nonbonded_method.lower() != 'nocutoff':
|
||||
errmsg = ("Only the nocutoff nonbonded_method is supported for "
|
||||
f"vacuum calculations, {vac_nonbonded_method} was "
|
||||
"passed")
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# Validate solvation settings
|
||||
settings_validation.validate_openmm_solvation_settings(
|
||||
self.settings.solvation_settings
|
||||
)
|
||||
|
||||
# Check vacuum equilibration MD settings is 0 ns
|
||||
nvt_time = self.settings.vacuum_equil_simulation_settings.equilibration_length_nvt
|
||||
if nvt_time is not None:
|
||||
if not np.allclose(nvt_time, 0 * unit.nanosecond):
|
||||
errmsg = "NVT equilibration cannot be run in vacuum simulation"
|
||||
raise ValueError(errmsg)
|
||||
|
||||
def _create(
|
||||
self,
|
||||
stateA: ChemicalSystem,
|
||||
stateB: ChemicalSystem,
|
||||
mapping: Optional[Union[gufe.ComponentMapping, list[gufe.ComponentMapping]]] = None,
|
||||
extends: Optional[gufe.ProtocolDAGResult] = None,
|
||||
) -> list[gufe.ProtocolUnit]:
|
||||
# Validate inputs
|
||||
self.validate(
|
||||
stateA=stateA, stateB=stateB, mapping=mapping, extends=extends
|
||||
)
|
||||
|
||||
# Get the alchemical components
|
||||
alchem_comps = system_validation.get_alchemical_components(
|
||||
stateA, stateB,
|
||||
)
|
||||
|
||||
# Get the name of the alchemical species
|
||||
alchname = alchem_comps['stateA'][0].name
|
||||
|
||||
# Create list units for vacuum and solvent transforms
|
||||
solvent_units = [
|
||||
AbsoluteSolvationSolventUnit(
|
||||
protocol=self,
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
alchemical_components=alchem_comps,
|
||||
generation=0, repeat_id=int(uuid.uuid4()),
|
||||
name=(f"Absolute Solvation, {alchname} solvent leg: "
|
||||
f"repeat {i} generation 0"),
|
||||
)
|
||||
for i in range(self.settings.protocol_repeats)
|
||||
]
|
||||
|
||||
vacuum_units = [
|
||||
AbsoluteSolvationVacuumUnit(
|
||||
# These don't really reflect the actual transform
|
||||
# Should these be overriden to be ChemicalSystem{smc} -> ChemicalSystem{} ?
|
||||
protocol=self,
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
alchemical_components=alchem_comps,
|
||||
generation=0, repeat_id=int(uuid.uuid4()),
|
||||
name=(f"Absolute Solvation, {alchname} vacuum leg: "
|
||||
f"repeat {i} generation 0"),
|
||||
)
|
||||
for i in range(self.settings.protocol_repeats)
|
||||
]
|
||||
|
||||
return solvent_units + vacuum_units
|
||||
|
||||
def _gather(
|
||||
self, protocol_dag_results: Iterable[gufe.ProtocolDAGResult]
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
# result units will have a repeat_id and generation
|
||||
# first group according to repeat_id
|
||||
unsorted_solvent_repeats = defaultdict(list)
|
||||
unsorted_vacuum_repeats = defaultdict(list)
|
||||
for d in protocol_dag_results:
|
||||
pu: gufe.ProtocolUnitResult
|
||||
for pu in d.protocol_unit_results:
|
||||
if not pu.ok():
|
||||
continue
|
||||
if pu.outputs['simtype'] == 'solvent':
|
||||
unsorted_solvent_repeats[pu.outputs['repeat_id']].append(pu)
|
||||
else:
|
||||
unsorted_vacuum_repeats[pu.outputs['repeat_id']].append(pu)
|
||||
|
||||
repeats: dict[str, dict[str, list[gufe.ProtocolUnitResult]]] = {
|
||||
'solvent': {}, 'vacuum': {},
|
||||
}
|
||||
for k, v in unsorted_solvent_repeats.items():
|
||||
repeats['solvent'][str(k)] = sorted(v, key=lambda x: x.outputs['generation'])
|
||||
|
||||
for k, v in unsorted_vacuum_repeats.items():
|
||||
repeats['vacuum'][str(k)] = sorted(v, key=lambda x: x.outputs['generation'])
|
||||
return repeats
|
||||
|
||||
|
||||
class AbsoluteSolvationVacuumUnit(BaseAbsoluteUnit):
|
||||
"""
|
||||
Protocol Unit for the vacuum phase of an absolute solvation free energy
|
||||
"""
|
||||
def _get_components(self):
|
||||
"""
|
||||
Get the relevant components for a vacuum transformation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
alchem_comps : dict[str, list[Component]]
|
||||
A list of alchemical components
|
||||
solv_comp : None
|
||||
For the gas phase transformation, None will always be returned
|
||||
for the solvent component of the chemical system.
|
||||
prot_comp : Optional[ProteinComponent]
|
||||
The protein component of the system, if it exists.
|
||||
small_mols : dict[Component, OpenFF Molecule]
|
||||
The openff Molecules to add to the system. This
|
||||
is equivalent to the alchemical components in stateA (since
|
||||
we only allow for disappearing ligands).
|
||||
"""
|
||||
stateA = self._inputs['stateA']
|
||||
alchem_comps = self._inputs['alchemical_components']
|
||||
|
||||
off_comps = {m: m.to_openff()
|
||||
for m in alchem_comps['stateA']}
|
||||
|
||||
_, prot_comp, _ = system_validation.get_components(stateA)
|
||||
|
||||
# Notes:
|
||||
# 1. Our input state will contain a solvent, we ``None`` that out
|
||||
# since this is the gas phase unit.
|
||||
# 2. Our small molecules will always just be the alchemical components
|
||||
# (of stateA since we enforce only one disappearing ligand)
|
||||
return alchem_comps, None, prot_comp, off_comps
|
||||
|
||||
def _handle_settings(self) -> dict[str, SettingsBaseModel]:
|
||||
"""
|
||||
Extract the relevant settings for a vacuum transformation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
settings : dict[str, SettingsBaseModel]
|
||||
A dictionary with the following entries:
|
||||
* forcefield_settings : OpenMMSystemGeneratorFFSettings
|
||||
* thermo_settings : ThermoSettings
|
||||
* charge_settings : OpenFFPartialChargeSettings
|
||||
* solvation_settings : OpenMMSolvationSettings
|
||||
* alchemical_settings : AlchemicalSettings
|
||||
* lambda_settings : LambdaSettings
|
||||
* engine_settings : OpenMMEngineSettings
|
||||
* integrator_settings : IntegratorSettings
|
||||
* equil_simulation_settings : MDSimulationSettings
|
||||
* equil_output_settings : MDOutputSettings
|
||||
* simulation_settings : SimulationSettings
|
||||
* output_settings: MultiStateOutputSettings
|
||||
"""
|
||||
prot_settings = self._inputs['protocol'].settings
|
||||
|
||||
settings = {}
|
||||
settings['forcefield_settings'] = prot_settings.vacuum_forcefield_settings
|
||||
settings['thermo_settings'] = prot_settings.thermo_settings
|
||||
settings['charge_settings'] = prot_settings.partial_charge_settings
|
||||
settings['solvation_settings'] = prot_settings.solvation_settings
|
||||
settings['alchemical_settings'] = prot_settings.alchemical_settings
|
||||
settings['lambda_settings'] = prot_settings.lambda_settings
|
||||
settings['engine_settings'] = prot_settings.vacuum_engine_settings
|
||||
settings['integrator_settings'] = prot_settings.integrator_settings
|
||||
settings['equil_simulation_settings'] = prot_settings.vacuum_equil_simulation_settings
|
||||
settings['equil_output_settings'] = prot_settings.vacuum_equil_output_settings
|
||||
settings['simulation_settings'] = prot_settings.vacuum_simulation_settings
|
||||
settings['output_settings'] = prot_settings.vacuum_output_settings
|
||||
|
||||
settings_validation.validate_timestep(
|
||||
settings['forcefield_settings'].hydrogen_mass,
|
||||
settings['integrator_settings'].timestep
|
||||
)
|
||||
|
||||
return settings
|
||||
|
||||
def _execute(
|
||||
self, ctx: gufe.Context, **kwargs,
|
||||
) -> dict[str, Any]:
|
||||
log_system_probe(logging.INFO, paths=[ctx.scratch])
|
||||
|
||||
outputs = self.run(scratch_basepath=ctx.scratch,
|
||||
shared_basepath=ctx.shared)
|
||||
|
||||
return {
|
||||
'repeat_id': self._inputs['repeat_id'],
|
||||
'generation': self._inputs['generation'],
|
||||
'simtype': 'vacuum',
|
||||
**outputs
|
||||
}
|
||||
|
||||
|
||||
class AbsoluteSolvationSolventUnit(BaseAbsoluteUnit):
|
||||
"""
|
||||
Protocol Unit for the solvent phase of an absolute solvation free energy
|
||||
"""
|
||||
def _get_components(self):
|
||||
"""
|
||||
Get the relevant components for a solvent transformation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
alchem_comps : dict[str, Component]
|
||||
A list of alchemical components
|
||||
solv_comp : SolventComponent
|
||||
The SolventComponent of the system
|
||||
prot_comp : Optional[ProteinComponent]
|
||||
The protein component of the system, if it exists.
|
||||
small_mols : dict[SmallMoleculeComponent: OFFMolecule]
|
||||
SmallMoleculeComponents to add to the system.
|
||||
"""
|
||||
stateA = self._inputs['stateA']
|
||||
alchem_comps = self._inputs['alchemical_components']
|
||||
|
||||
solv_comp, prot_comp, small_mols = system_validation.get_components(stateA)
|
||||
off_comps = {m: m.to_openff() for m in small_mols}
|
||||
|
||||
# We don't need to check that solv_comp is not None, otherwise
|
||||
# an error will have been raised when calling `validate_solvent`
|
||||
# in the Protocol's `_create`.
|
||||
# Similarly we don't need to check prot_comp since that's also
|
||||
# disallowed on create
|
||||
return alchem_comps, solv_comp, prot_comp, off_comps
|
||||
|
||||
def _handle_settings(self) -> dict[str, SettingsBaseModel]:
|
||||
"""
|
||||
Extract the relevant settings for a vacuum transformation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
settings : dict[str, SettingsBaseModel]
|
||||
A dictionary with the following entries:
|
||||
* forcefield_settings : OpenMMSystemGeneratorFFSettings
|
||||
* thermo_settings : ThermoSettings
|
||||
* charge_settings : OpenFFPartialChargeSettings
|
||||
* solvation_settings : OpenMMSolvationSettings
|
||||
* alchemical_settings : AlchemicalSettings
|
||||
* lambda_settings : LambdaSettings
|
||||
* engine_settings : OpenMMEngineSettings
|
||||
* integrator_settings : IntegratorSettings
|
||||
* equil_simulation_settings : MDSimulationSettings
|
||||
* equil_output_settings : MDOutputSettings
|
||||
* simulation_settings : MultiStateSimulationSettings
|
||||
* output_settings: MultiStateOutputSettings
|
||||
"""
|
||||
prot_settings = self._inputs['protocol'].settings
|
||||
|
||||
settings = {}
|
||||
settings['forcefield_settings'] = prot_settings.solvent_forcefield_settings
|
||||
settings['thermo_settings'] = prot_settings.thermo_settings
|
||||
settings['charge_settings'] = prot_settings.partial_charge_settings
|
||||
settings['solvation_settings'] = prot_settings.solvation_settings
|
||||
settings['alchemical_settings'] = prot_settings.alchemical_settings
|
||||
settings['lambda_settings'] = prot_settings.lambda_settings
|
||||
settings['engine_settings'] = prot_settings.solvent_engine_settings
|
||||
settings['integrator_settings'] = prot_settings.integrator_settings
|
||||
settings['equil_simulation_settings'] = prot_settings.solvent_equil_simulation_settings
|
||||
settings['equil_output_settings'] = prot_settings.solvent_equil_output_settings
|
||||
settings['simulation_settings'] = prot_settings.solvent_simulation_settings
|
||||
settings['output_settings'] = prot_settings.solvent_output_settings
|
||||
|
||||
settings_validation.validate_timestep(
|
||||
settings['forcefield_settings'].hydrogen_mass,
|
||||
settings['integrator_settings'].timestep
|
||||
)
|
||||
|
||||
return settings
|
||||
|
||||
def _execute(
|
||||
self, ctx: gufe.Context, **kwargs,
|
||||
) -> dict[str, Any]:
|
||||
log_system_probe(logging.INFO, paths=[ctx.scratch])
|
||||
|
||||
outputs = self.run(scratch_basepath=ctx.scratch,
|
||||
shared_basepath=ctx.shared)
|
||||
|
||||
return {
|
||||
'repeat_id': self._inputs['repeat_id'],
|
||||
'generation': self._inputs['generation'],
|
||||
'simtype': 'solvent',
|
||||
**outputs
|
||||
}
|
||||
@@ -1,721 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
|
||||
"""OpenMM MD Protocol --- :mod:`openfe.protocols.openmm_md.plain_md_methods`
|
||||
===========================================================================================
|
||||
|
||||
This module implements the necessary methodology tools to run an MD
|
||||
simulation using OpenMM tools.
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
import gufe
|
||||
import openmm
|
||||
from openff.units import unit, Quantity
|
||||
from openff.units.openmm import from_openmm, to_openmm
|
||||
import openmm.unit as omm_unit
|
||||
from typing import Optional
|
||||
import pathlib
|
||||
from typing import Any, Iterable
|
||||
import uuid
|
||||
import time
|
||||
import mdtraj
|
||||
from mdtraj.reporters import XTCReporter
|
||||
from openfe.utils import without_oechem_backend, log_system_probe
|
||||
from gufe import (
|
||||
settings,
|
||||
ChemicalSystem,
|
||||
SmallMoleculeComponent,
|
||||
)
|
||||
from gufe.settings.types import KelvinQuantity
|
||||
from openfe.protocols.openmm_utils.omm_settings import (
|
||||
BasePartialChargeSettings,
|
||||
FemtosecondQuantity
|
||||
)
|
||||
from openfe.protocols.openmm_md.plain_md_settings import (
|
||||
PlainMDProtocolSettings,
|
||||
OpenFFPartialChargeSettings,
|
||||
OpenMMSolvationSettings, OpenMMEngineSettings,
|
||||
IntegratorSettings, MDSimulationSettings, MDOutputSettings,
|
||||
)
|
||||
from openff.toolkit.topology import Molecule as OFFMolecule
|
||||
|
||||
from openfe.protocols.openmm_utils import (
|
||||
system_validation, settings_validation, system_creation,
|
||||
charge_generation, omm_compute
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlainMDProtocolResult(gufe.ProtocolResult):
|
||||
"""
|
||||
Dict-like container for the output of a PlainMDProtocol.
|
||||
|
||||
Provides access to simulation outputs including the pre-minimized
|
||||
system PDB and production trajectory files.
|
||||
"""
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
# data is mapping of str(repeat_id): list[protocolunitresults]
|
||||
if any(len(pur_list) > 2 for pur_list in self.data.values()):
|
||||
raise NotImplementedError("Can't stitch together results yet")
|
||||
|
||||
def get_estimate(self):
|
||||
"""Since no results as output --> returns None
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
def get_uncertainty(self):
|
||||
"""Since no results as output --> returns None"""
|
||||
|
||||
return None
|
||||
|
||||
def get_traj_filename(self) -> list[pathlib.Path]:
|
||||
"""
|
||||
Get a list of trajectory paths
|
||||
|
||||
Returns
|
||||
-------
|
||||
traj : list[pathlib.Path]
|
||||
list of paths (pathlib.Path) to the simulation trajectory
|
||||
"""
|
||||
traj = [pus[0].outputs['nc'] for pus in self.data.values()]
|
||||
|
||||
return traj
|
||||
|
||||
def get_pdb_filename(self) -> list[pathlib.Path]:
|
||||
"""
|
||||
Get a list of paths to the pdb files of the pre-minimized system.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pdbs : list[pathlib.Path]
|
||||
list of paths (pathlib.Path) to the pdb files
|
||||
"""
|
||||
pdbs = [pus[0].outputs['system_pdb'] for pus in self.data.values()]
|
||||
|
||||
return pdbs
|
||||
|
||||
|
||||
class PlainMDProtocol(gufe.Protocol):
|
||||
"""
|
||||
Protocol for running Molecular Dynamics simulations using OpenMM.
|
||||
|
||||
See Also
|
||||
--------
|
||||
:mod:`openfe.protocols`
|
||||
:class:`openfe.protocols.openmm_md.PlainMDProtocolSettings`
|
||||
:class:`openfe.protocols.openmm_md.PlainMDProtocolUnit`
|
||||
:class:`openfe.protocols.openmm_md.PlainMDProtocolResult`
|
||||
"""
|
||||
result_cls = PlainMDProtocolResult
|
||||
_settings_cls = PlainMDProtocolSettings
|
||||
_settings: PlainMDProtocolSettings
|
||||
|
||||
@classmethod
|
||||
def _default_settings(cls):
|
||||
"""A dictionary of initial settings for this creating this Protocol
|
||||
|
||||
These settings are intended as a suitable starting point for creating
|
||||
an instance of this protocol. It is recommended, however that care is
|
||||
taken to inspect and customize these before performing a Protocol.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Settings
|
||||
a set of default settings
|
||||
"""
|
||||
return PlainMDProtocolSettings(
|
||||
forcefield_settings=settings.OpenMMSystemGeneratorFFSettings(),
|
||||
thermo_settings=settings.ThermoSettings(
|
||||
temperature=298.15 * unit.kelvin,
|
||||
pressure=1 * unit.bar,
|
||||
),
|
||||
partial_charge_settings=OpenFFPartialChargeSettings(),
|
||||
solvation_settings=OpenMMSolvationSettings(),
|
||||
engine_settings=OpenMMEngineSettings(),
|
||||
integrator_settings=IntegratorSettings(),
|
||||
simulation_settings=MDSimulationSettings(
|
||||
equilibration_length_nvt=0.1 * unit.nanosecond,
|
||||
equilibration_length=1.0 * unit.nanosecond,
|
||||
production_length=5.0 * unit.nanosecond,
|
||||
),
|
||||
output_settings=MDOutputSettings(),
|
||||
protocol_repeats=1,
|
||||
)
|
||||
|
||||
def _create(
|
||||
self,
|
||||
stateA: ChemicalSystem,
|
||||
stateB: ChemicalSystem,
|
||||
mapping: Optional[dict[str, gufe.ComponentMapping]] = None,
|
||||
extends: Optional[gufe.ProtocolDAGResult] = None,
|
||||
) -> list[gufe.ProtocolUnit]:
|
||||
# TODO: Extensions?
|
||||
if extends:
|
||||
raise NotImplementedError("Can't extend simulations yet")
|
||||
|
||||
# Validate solvent component
|
||||
nonbond = self.settings.forcefield_settings.nonbonded_method
|
||||
system_validation.validate_solvent(stateA, nonbond)
|
||||
|
||||
# Validate protein component
|
||||
system_validation.validate_protein(stateA)
|
||||
|
||||
# Validate solvation settings
|
||||
settings_validation.validate_openmm_solvation_settings(
|
||||
self.settings.solvation_settings
|
||||
)
|
||||
|
||||
# actually create and return Units
|
||||
# TODO: Deal with multiple ProteinComponents
|
||||
solvent_comp, protein_comp, small_mols = system_validation.get_components(stateA)
|
||||
|
||||
system_name = "Solvent MD" if solvent_comp is not None else "Vacuum MD"
|
||||
|
||||
for comp in [protein_comp] + small_mols:
|
||||
if comp is not None:
|
||||
comp_type = comp.__class__.__name__
|
||||
if len(comp.name) == 0:
|
||||
comp_name = 'NoName'
|
||||
else:
|
||||
comp_name = comp.name
|
||||
system_name += f" {comp_type}:{comp_name}"
|
||||
|
||||
# our DAG has no dependencies, so just list units
|
||||
n_repeats = self.settings.protocol_repeats
|
||||
units = [PlainMDProtocolUnit(
|
||||
protocol=self,
|
||||
stateA=stateA,
|
||||
generation=0, repeat_id=int(uuid.uuid4()),
|
||||
name=f'{system_name} repeat {i} generation 0')
|
||||
for i in range(n_repeats)]
|
||||
|
||||
return units
|
||||
|
||||
def _gather(
|
||||
self, protocol_dag_results: Iterable[gufe.ProtocolDAGResult]
|
||||
) -> dict[str, Any]:
|
||||
# result units will have a repeat_id and generations within this
|
||||
# repeat_id
|
||||
# first group according to repeat_id
|
||||
unsorted_repeats = defaultdict(list)
|
||||
for d in protocol_dag_results:
|
||||
pu: gufe.ProtocolUnitResult
|
||||
for pu in d.protocol_unit_results:
|
||||
if not pu.ok():
|
||||
continue
|
||||
|
||||
unsorted_repeats[pu.outputs['repeat_id']].append(pu)
|
||||
|
||||
# then sort by generation within each repeat_id list
|
||||
repeats: dict[str, list[gufe.ProtocolUnitResult]] = {}
|
||||
for k, v in unsorted_repeats.items():
|
||||
repeats[str(k)] = sorted(v, key=lambda x: x.outputs['generation'])
|
||||
|
||||
# returns a dict of repeat_id: sorted list of ProtocolUnitResult
|
||||
return repeats
|
||||
|
||||
|
||||
class PlainMDProtocolUnit(gufe.ProtocolUnit):
|
||||
"""
|
||||
Protocol unit for plain MD simulations (NonTransformation).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
protocol: PlainMDProtocol,
|
||||
stateA: ChemicalSystem,
|
||||
generation: int,
|
||||
repeat_id: int,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
protocol : PlainMDProtocol
|
||||
protocol used to create this Unit. Contains key information such
|
||||
as the settings.
|
||||
stateA : ChemicalSystem
|
||||
the chemical system for the MD simulation
|
||||
repeat_id : int
|
||||
identifier for which repeat (aka replica/clone) this Unit is
|
||||
generation : int
|
||||
counter for how many times this repeat has been extended
|
||||
name : str, optional
|
||||
human-readable identifier for this Unit
|
||||
|
||||
Notes
|
||||
-----
|
||||
The mapping used must not involve any elemental changes. A check for
|
||||
this is done on class creation.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
protocol=protocol,
|
||||
stateA=stateA,
|
||||
repeat_id=repeat_id,
|
||||
generation=generation
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _run_MD(simulation: openmm.app.Simulation,
|
||||
positions: omm_unit.Quantity,
|
||||
simulation_settings: MDSimulationSettings,
|
||||
output_settings: MDOutputSettings,
|
||||
temperature: KelvinQuantity,
|
||||
barostat_frequency: Quantity,
|
||||
timestep: FemtosecondQuantity,
|
||||
equil_steps_nvt: Optional[int],
|
||||
equil_steps_npt: int,
|
||||
prod_steps: int,
|
||||
verbose=True,
|
||||
shared_basepath=None) -> None:
|
||||
|
||||
"""
|
||||
Energy minimization, Equilibration and Production MD to be reused
|
||||
in multiple protocols
|
||||
|
||||
Parameters
|
||||
----------
|
||||
simulation : openmm.app.Simulation
|
||||
An OpenMM simulation to simulate.
|
||||
positions : openmm.unit.Quantity
|
||||
Initial positions for the system.
|
||||
simulation_settings : SimulationSettingsMD
|
||||
Settings for MD simulation
|
||||
output_settings: OutputSettingsMD
|
||||
Settings for output of MD simulation
|
||||
temperature: KelvinQuantity
|
||||
temperature setting
|
||||
barostat_frequency: openff.units.Quantity
|
||||
Frequency for the barostat
|
||||
timestep: FemtosecondQuantity
|
||||
Simulation integration timestep
|
||||
equil_steps_nvt: Optional[int]
|
||||
number of steps for NVT equilibration
|
||||
if None, no NVT equilibration will be performed
|
||||
equil_steps_npt: int
|
||||
number of steps for NPT equilibration
|
||||
prod_steps: int
|
||||
number of steps for the production run
|
||||
verbose: bool
|
||||
Verbose output of the simulation progress. Output is provided via
|
||||
INFO level logging.
|
||||
shared_basepath : Pathlike, optional
|
||||
Where to run the calculation, defaults to current working directory
|
||||
|
||||
"""
|
||||
if shared_basepath is None:
|
||||
shared_basepath = pathlib.Path('.')
|
||||
simulation.context.setPositions(positions)
|
||||
# minimize
|
||||
if verbose:
|
||||
logger.info("minimizing systems")
|
||||
|
||||
simulation.minimizeEnergy(
|
||||
maxIterations=simulation_settings.minimization_steps
|
||||
)
|
||||
|
||||
# Get the sub selection of the system to save coords for
|
||||
selection_indices = mdtraj.Topology.from_openmm(
|
||||
simulation.topology).select(output_settings.output_indices)
|
||||
|
||||
positions = to_openmm(from_openmm(
|
||||
simulation.context.getState(getPositions=True,
|
||||
enforcePeriodicBox=False
|
||||
).getPositions()))
|
||||
# Store subset of atoms, specified in input, as PDB file
|
||||
mdtraj_top = mdtraj.Topology.from_openmm(simulation.topology)
|
||||
traj = mdtraj.Trajectory(
|
||||
positions[selection_indices, :],
|
||||
mdtraj_top.subset(selection_indices),
|
||||
)
|
||||
|
||||
if output_settings.minimized_structure:
|
||||
traj.save_pdb(
|
||||
shared_basepath / output_settings.minimized_structure
|
||||
)
|
||||
# equilibrate
|
||||
# NVT equilibration
|
||||
if equil_steps_nvt:
|
||||
if verbose:
|
||||
logger.info("Running NVT equilibration")
|
||||
|
||||
# Set barostat frequency to zero for NVT
|
||||
for x in simulation.context.getSystem().getForces():
|
||||
if x.getName() == 'MonteCarloBarostat':
|
||||
x.setFrequency(0)
|
||||
|
||||
simulation.context.setVelocitiesToTemperature(
|
||||
to_openmm(temperature))
|
||||
|
||||
t0 = time.time()
|
||||
simulation.step(equil_steps_nvt)
|
||||
t1 = time.time()
|
||||
if verbose:
|
||||
logger.info(
|
||||
f"Completed NVT equilibration in {t1 - t0} seconds")
|
||||
|
||||
# Save last frame NVT equilibration
|
||||
positions = to_openmm(
|
||||
from_openmm(simulation.context.getState(
|
||||
getPositions=True, enforcePeriodicBox=False
|
||||
).getPositions()))
|
||||
|
||||
traj = mdtraj.Trajectory(
|
||||
positions[selection_indices, :],
|
||||
mdtraj_top.subset(selection_indices),
|
||||
)
|
||||
if output_settings.equil_nvt_structure is not None:
|
||||
traj.save_pdb(
|
||||
shared_basepath / output_settings.equil_nvt_structure
|
||||
)
|
||||
|
||||
# NPT equilibration
|
||||
if verbose:
|
||||
logger.info("Running NPT equilibration")
|
||||
simulation.context.setVelocitiesToTemperature(
|
||||
to_openmm(temperature))
|
||||
|
||||
# Enable the barostat for NPT
|
||||
for x in simulation.context.getSystem().getForces():
|
||||
if x.getName() == 'MonteCarloBarostat':
|
||||
x.setFrequency(barostat_frequency.m)
|
||||
|
||||
t0 = time.time()
|
||||
simulation.step(equil_steps_npt)
|
||||
t1 = time.time()
|
||||
if verbose:
|
||||
logger.info(
|
||||
f"Completed NPT equilibration in {t1 - t0} seconds")
|
||||
|
||||
# Save last frame NPT equilibration
|
||||
positions = to_openmm(
|
||||
from_openmm(simulation.context.getState(
|
||||
getPositions=True, enforcePeriodicBox=False
|
||||
).getPositions()))
|
||||
|
||||
traj = mdtraj.Trajectory(
|
||||
positions[selection_indices, :],
|
||||
mdtraj_top.subset(selection_indices),
|
||||
)
|
||||
if output_settings.equil_npt_structure is not None:
|
||||
traj.save_pdb(
|
||||
shared_basepath / output_settings.equil_npt_structure
|
||||
)
|
||||
|
||||
# production
|
||||
if verbose:
|
||||
logger.info("running production phase")
|
||||
|
||||
# Setup the reporters
|
||||
write_interval = settings_validation.divmod_time_and_check(
|
||||
output_settings.trajectory_write_interval,
|
||||
timestep,
|
||||
"trajectory_write_interval",
|
||||
"timestep",
|
||||
)
|
||||
|
||||
checkpoint_interval = settings_validation.get_simsteps(
|
||||
sim_length=output_settings.checkpoint_interval,
|
||||
timestep=timestep,
|
||||
mc_steps=1,
|
||||
)
|
||||
|
||||
if output_settings.production_trajectory_filename:
|
||||
simulation.reporters.append(XTCReporter(
|
||||
file=str(
|
||||
shared_basepath /
|
||||
output_settings.production_trajectory_filename),
|
||||
reportInterval=write_interval,
|
||||
atomSubset=selection_indices))
|
||||
if output_settings.checkpoint_storage_filename:
|
||||
simulation.reporters.append(openmm.app.CheckpointReporter(
|
||||
file=str(
|
||||
shared_basepath /
|
||||
output_settings.checkpoint_storage_filename),
|
||||
reportInterval=checkpoint_interval))
|
||||
if output_settings.log_output:
|
||||
simulation.reporters.append(openmm.app.StateDataReporter(
|
||||
str(shared_basepath / output_settings.log_output),
|
||||
checkpoint_interval,
|
||||
step=True,
|
||||
time=True,
|
||||
potentialEnergy=True,
|
||||
kineticEnergy=True,
|
||||
totalEnergy=True,
|
||||
temperature=True,
|
||||
volume=True,
|
||||
density=True,
|
||||
speed=True,
|
||||
))
|
||||
t0 = time.time()
|
||||
simulation.step(prod_steps)
|
||||
t1 = time.time()
|
||||
if verbose:
|
||||
logger.info(f"Completed simulation in {t1 - t0} seconds")
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _assign_partial_charges(
|
||||
charge_settings: OpenFFPartialChargeSettings,
|
||||
smc_components: dict[SmallMoleculeComponent, OFFMolecule],
|
||||
) -> None:
|
||||
"""
|
||||
Assign partial charges to SMCs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
charge_settings : OpenFFPartialChargeSettings
|
||||
Settings for controlling how the partial charges are assigned.
|
||||
smc_components : dict[SmallMoleculeComponent, openff.toolkit.Molecule]
|
||||
Dictionary of OpenFF Molecules to add, keyed by
|
||||
SmallMoleculeComponent.
|
||||
"""
|
||||
for mol in smc_components.values():
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
offmol=mol,
|
||||
overwrite=False,
|
||||
method=charge_settings.partial_charge_method,
|
||||
toolkit_backend=charge_settings.off_toolkit_backend,
|
||||
generate_n_conformers=charge_settings.number_of_conformers,
|
||||
nagl_model=charge_settings.nagl_model,
|
||||
)
|
||||
|
||||
def run(self, *, dry=False, verbose=True,
|
||||
scratch_basepath=None,
|
||||
shared_basepath=None) -> dict[str, Any]:
|
||||
"""Run the MD simulation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dry : bool
|
||||
Do a dry run of the calculation, creating all necessary hybrid
|
||||
system components (topology, system, sampler, etc...) but without
|
||||
running the simulation.
|
||||
verbose : bool
|
||||
Verbose output of the simulation progress. Output is provided via
|
||||
INFO level logging.
|
||||
scratch_basepath: Pathlike, optional
|
||||
Where to store temporary files, defaults to current working directory
|
||||
shared_basepath : Pathlike, optional
|
||||
Where to run the calculation, defaults to current working directory
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Outputs created in the basepath directory or the debug objects
|
||||
(i.e. sampler) if ``dry==True``.
|
||||
|
||||
Raises
|
||||
------
|
||||
error
|
||||
Exception if anything failed
|
||||
"""
|
||||
if verbose:
|
||||
self.logger.info("Creating system")
|
||||
if shared_basepath is None:
|
||||
# use cwd
|
||||
shared_basepath = pathlib.Path('.')
|
||||
|
||||
# 0. General setup and settings dependency resolution step
|
||||
|
||||
# Extract relevant settings
|
||||
protocol_settings: PlainMDProtocolSettings = self._inputs['protocol'].settings
|
||||
stateA = self._inputs['stateA']
|
||||
|
||||
forcefield_settings: settings.OpenMMSystemGeneratorFFSettings = protocol_settings.forcefield_settings
|
||||
thermo_settings: settings.ThermoSettings = protocol_settings.thermo_settings
|
||||
solvation_settings: OpenMMSolvationSettings = protocol_settings.solvation_settings
|
||||
charge_settings: BasePartialChargeSettings = protocol_settings.partial_charge_settings
|
||||
sim_settings: MDSimulationSettings = protocol_settings.simulation_settings
|
||||
output_settings: MDOutputSettings = protocol_settings.output_settings
|
||||
timestep = protocol_settings.integrator_settings.timestep
|
||||
integrator_settings = protocol_settings.integrator_settings
|
||||
|
||||
# is the timestep good for the mass?
|
||||
settings_validation.validate_timestep(
|
||||
forcefield_settings.hydrogen_mass, timestep
|
||||
)
|
||||
|
||||
if sim_settings.equilibration_length_nvt is not None:
|
||||
equil_steps_nvt = settings_validation.get_simsteps(
|
||||
sim_length=sim_settings.equilibration_length_nvt,
|
||||
timestep=timestep, mc_steps=1,
|
||||
)
|
||||
else:
|
||||
equil_steps_nvt = None
|
||||
|
||||
equil_steps_npt = settings_validation.get_simsteps(
|
||||
sim_length=sim_settings.equilibration_length,
|
||||
timestep=timestep, mc_steps=1,
|
||||
)
|
||||
prod_steps = settings_validation.get_simsteps(
|
||||
sim_length=sim_settings.production_length,
|
||||
timestep=timestep, mc_steps=1,
|
||||
)
|
||||
|
||||
solvent_comp, protein_comp, small_mols = system_validation.get_components(stateA)
|
||||
|
||||
# 1. Create stateA system
|
||||
# Create a dictionary of OFFMol for each SMC for bookeeping
|
||||
smc_components: dict[SmallMoleculeComponent, OFFMolecule]
|
||||
|
||||
smc_components = {i: i.to_openff() for i in small_mols}
|
||||
|
||||
# a. assign partial charges to smcs
|
||||
self._assign_partial_charges(charge_settings, smc_components)
|
||||
|
||||
# b. get a system generator
|
||||
if output_settings.forcefield_cache is not None:
|
||||
ffcache = shared_basepath / output_settings.forcefield_cache
|
||||
else:
|
||||
ffcache = None
|
||||
|
||||
# Note: we block out the oechem backend for all systemgenerator
|
||||
# linked operations to avoid any smiles operations that can
|
||||
# go wrong when doing rdkit->OEchem roundtripping
|
||||
with without_oechem_backend():
|
||||
system_generator = system_creation.get_system_generator(
|
||||
forcefield_settings=forcefield_settings,
|
||||
integrator_settings=integrator_settings,
|
||||
thermo_settings=thermo_settings,
|
||||
cache=ffcache,
|
||||
has_solvent=solvent_comp is not None,
|
||||
)
|
||||
|
||||
# Force creation of smc templates so we can solvate later
|
||||
for mol in smc_components.values():
|
||||
system_generator.create_system(
|
||||
mol.to_topology().to_openmm(), molecules=[mol]
|
||||
)
|
||||
|
||||
# c. get OpenMM Modeller + a resids dictionary for each component
|
||||
stateA_modeller, comp_resids = system_creation.get_omm_modeller(
|
||||
protein_comp=protein_comp,
|
||||
solvent_comp=solvent_comp,
|
||||
small_mols=smc_components,
|
||||
omm_forcefield=system_generator.forcefield,
|
||||
solvent_settings=solvation_settings,
|
||||
)
|
||||
|
||||
# d. get topology & positions
|
||||
# Note: roundtrip positions to remove vec3 issues
|
||||
stateA_topology = stateA_modeller.getTopology()
|
||||
stateA_positions = to_openmm(
|
||||
from_openmm(stateA_modeller.getPositions())
|
||||
)
|
||||
|
||||
# e. create the stateA System
|
||||
stateA_system = system_generator.create_system(
|
||||
stateA_topology,
|
||||
molecules=[s.to_openff() for s in small_mols],
|
||||
)
|
||||
|
||||
# f. Save pdb of entire system
|
||||
if output_settings.preminimized_structure:
|
||||
with open(
|
||||
shared_basepath /
|
||||
output_settings.preminimized_structure, "w") as f:
|
||||
openmm.app.PDBFile.writeFile(
|
||||
stateA_topology, stateA_positions, file=f, keepIds=True
|
||||
)
|
||||
|
||||
# 10. Get platform
|
||||
restrict_cpu = forcefield_settings.nonbonded_method.lower() == 'nocutoff'
|
||||
platform = omm_compute.get_openmm_platform(
|
||||
platform_name=protocol_settings.engine_settings.compute_platform,
|
||||
gpu_device_index=protocol_settings.engine_settings.gpu_device_index,
|
||||
restrict_cpu_count=restrict_cpu
|
||||
)
|
||||
|
||||
# 11. Set the integrator
|
||||
integrator = openmm.LangevinMiddleIntegrator(
|
||||
to_openmm(thermo_settings.temperature),
|
||||
to_openmm(integrator_settings.langevin_collision_rate),
|
||||
to_openmm(timestep),
|
||||
)
|
||||
|
||||
simulation = openmm.app.Simulation(
|
||||
stateA_modeller.topology,
|
||||
stateA_system,
|
||||
integrator,
|
||||
platform=platform
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
if not dry: # pragma: no-cover
|
||||
self._run_MD(
|
||||
simulation,
|
||||
stateA_positions,
|
||||
sim_settings,
|
||||
output_settings,
|
||||
thermo_settings.temperature,
|
||||
integrator_settings.barostat_frequency,
|
||||
timestep,
|
||||
equil_steps_nvt,
|
||||
equil_steps_npt,
|
||||
prod_steps,
|
||||
shared_basepath=shared_basepath,
|
||||
)
|
||||
|
||||
finally:
|
||||
|
||||
if not dry:
|
||||
del integrator, simulation
|
||||
|
||||
if not dry: # pragma: no-cover
|
||||
output = {
|
||||
'system_pdb': shared_basepath / output_settings.preminimized_structure,
|
||||
'minimized_pdb': shared_basepath / output_settings.minimized_structure,
|
||||
'nc': shared_basepath / output_settings.production_trajectory_filename,
|
||||
'last_checkpoint': shared_basepath / output_settings.checkpoint_storage_filename,
|
||||
}
|
||||
# The checkpoint file can not exist if frequency > sim length
|
||||
if not output['last_checkpoint'].exists():
|
||||
output['last_checkpoint'] = None
|
||||
|
||||
# The NVT PDB can be ommitted if we don't run the simulation
|
||||
# Note: we could also just check the file exist
|
||||
if (
|
||||
output_settings.equil_nvt_structure
|
||||
and sim_settings.equilibration_length_nvt is not None
|
||||
):
|
||||
output['nvt_equil_pdb'] = shared_basepath / output_settings.equil_nvt_structure
|
||||
else:
|
||||
output['nvt_equil_pdb'] = None
|
||||
|
||||
if output_settings.equil_npt_structure:
|
||||
output['npt_equil_pdb'] = shared_basepath / output_settings.equil_npt_structure
|
||||
|
||||
return output
|
||||
else:
|
||||
return {'debug': {'system': stateA_system}}
|
||||
|
||||
def _execute(
|
||||
self, ctx: gufe.Context, **kwargs,
|
||||
) -> dict[str, Any]:
|
||||
log_system_probe(logging.INFO, paths=[ctx.scratch])
|
||||
|
||||
outputs = self.run(scratch_basepath=ctx.scratch,
|
||||
shared_basepath=ctx.shared)
|
||||
|
||||
return {
|
||||
'repeat_id': self._inputs['repeat_id'],
|
||||
'generation': self._inputs['generation'],
|
||||
**outputs
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
|
||||
from . import _rfe_utils
|
||||
|
||||
from .equil_rfe_settings import (
|
||||
RelativeHybridTopologyProtocolSettings,
|
||||
)
|
||||
|
||||
from .equil_rfe_methods import (
|
||||
RelativeHybridTopologyProtocol,
|
||||
RelativeHybridTopologyProtocolResult,
|
||||
RelativeHybridTopologyProtocolUnit,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,177 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
"""
|
||||
Reusable utility methods to validate input systems to OpenMM-based alchemical
|
||||
Protocols.
|
||||
"""
|
||||
from typing import Optional, Tuple
|
||||
from openff.toolkit import Molecule as OFFMol
|
||||
from gufe import (
|
||||
Component, ChemicalSystem, SolventComponent, ProteinComponent,
|
||||
SmallMoleculeComponent
|
||||
)
|
||||
|
||||
|
||||
def get_alchemical_components(
|
||||
stateA: ChemicalSystem,
|
||||
stateB: ChemicalSystem,
|
||||
) -> dict[str, list[Component]]:
|
||||
"""
|
||||
Checks the equality between Components of two end state ChemicalSystems
|
||||
and identify which components do not match.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stateA : ChemicalSystem
|
||||
The chemical system of end state A.
|
||||
stateB : ChemicalSystem
|
||||
The chemical system of end state B.
|
||||
|
||||
Returns
|
||||
-------
|
||||
alchemical_components : dict[str, list[Component]]
|
||||
Dictionary containing a list of alchemical components for each state.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If there are any duplicate components in states A or B.
|
||||
"""
|
||||
matched_components: dict[Component, Component] = {}
|
||||
alchemical_components: dict[str, list[Component]] = {
|
||||
'stateA': [], 'stateB': [],
|
||||
}
|
||||
|
||||
for keyA, valA in stateA.components.items():
|
||||
for keyB, valB in stateB.components.items():
|
||||
if valA == valB:
|
||||
if valA not in matched_components.keys():
|
||||
matched_components[valA] = valB
|
||||
else:
|
||||
# Could be that either we have a duplicate component
|
||||
# in stateA or in stateB
|
||||
errmsg = (f"state A components {keyA}: {valA} matches "
|
||||
"multiple components in stateA or stateB")
|
||||
raise ValueError(errmsg)
|
||||
|
||||
# populate stateA alchemical components
|
||||
for valA in stateA.components.values():
|
||||
if valA not in matched_components.keys():
|
||||
alchemical_components['stateA'].append(valA)
|
||||
|
||||
# populate stateB alchemical components
|
||||
for valB in stateB.components.values():
|
||||
if valB not in matched_components.values():
|
||||
alchemical_components['stateB'].append(valB)
|
||||
|
||||
return alchemical_components
|
||||
|
||||
|
||||
def validate_solvent(state: ChemicalSystem, nonbonded_method: str):
|
||||
"""
|
||||
Checks that the ChemicalSystem component has the right solvent
|
||||
composition for an input nonbonded_methtod.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : ChemicalSystem
|
||||
The chemical system to inspect.
|
||||
nonbonded_method : str
|
||||
The nonbonded method to be applied for the simulation.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
* If there are multiple SolventComponents in the ChemicalSystem.
|
||||
* If there is a SolventComponent and the `nonbonded_method` is
|
||||
`nocutoff`.
|
||||
* If the SolventComponent solvent is not water.
|
||||
"""
|
||||
solv = [comp for comp in state.values()
|
||||
if isinstance(comp, SolventComponent)]
|
||||
|
||||
if len(solv) > 0 and nonbonded_method.lower() == "nocutoff":
|
||||
errmsg = "nocutoff cannot be used for solvent transformations"
|
||||
raise ValueError(errmsg)
|
||||
|
||||
if len(solv) == 0 and nonbonded_method.lower() == 'pme':
|
||||
errmsg = "PME cannot be used for vacuum transform"
|
||||
raise ValueError(errmsg)
|
||||
|
||||
if len(solv) > 1:
|
||||
errmsg = "Multiple SolventComponent found, only one is supported"
|
||||
raise ValueError(errmsg)
|
||||
|
||||
if len(solv) > 0 and solv[0].smiles != 'O':
|
||||
errmsg = "Non water solvent is not currently supported"
|
||||
raise ValueError(errmsg)
|
||||
|
||||
|
||||
def validate_protein(state: ChemicalSystem):
|
||||
"""
|
||||
Checks that the ChemicalSystem's ProteinComponent are suitable for the
|
||||
alchemical protocol.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : ChemicalSystem
|
||||
The chemical system to inspect.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If there are multiple ProteinComponent in the ChemicalSystem.
|
||||
"""
|
||||
nprot = sum(1 for comp in state.values()
|
||||
if isinstance(comp, ProteinComponent))
|
||||
|
||||
if nprot > 1:
|
||||
errmsg = "Multiple ProteinComponent found, only one is supported"
|
||||
raise ValueError(errmsg)
|
||||
|
||||
|
||||
ParseCompRet = Tuple[
|
||||
Optional[SolventComponent], Optional[ProteinComponent],
|
||||
list[SmallMoleculeComponent],
|
||||
]
|
||||
|
||||
|
||||
def get_components(state: ChemicalSystem) -> ParseCompRet:
|
||||
"""
|
||||
Establish all necessary Components for the transformation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : ChemicalSystem
|
||||
ChemicalSystem to get all necessary components from.
|
||||
|
||||
Returns
|
||||
-------
|
||||
solvent_comp : Optional[SolventComponent]
|
||||
If it exists, the SolventComponent for the state, otherwise None.
|
||||
protein_comp : Optional[ProteinComponent]
|
||||
If it exists, the ProteinComponent for the state, otherwise None.
|
||||
small_mols : list[SmallMoleculeComponent]
|
||||
"""
|
||||
def _get_single_comps(comp_list, comptype):
|
||||
ret_comps = [comp for comp in comp_list
|
||||
if isinstance(comp, comptype)]
|
||||
if ret_comps:
|
||||
return ret_comps[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
solvent_comp: Optional[SolventComponent] = _get_single_comps(
|
||||
list(state.values()), SolventComponent
|
||||
)
|
||||
|
||||
protein_comp: Optional[ProteinComponent] = _get_single_comps(
|
||||
list(state.values()), ProteinComponent
|
||||
)
|
||||
|
||||
small_mols = []
|
||||
for comp in state.components.values():
|
||||
if isinstance(comp, SmallMoleculeComponent):
|
||||
small_mols.append(comp)
|
||||
|
||||
return solvent_comp, protein_comp, small_mols
|
||||
@@ -1,14 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
|
||||
|
||||
from .atom_mapping import (LigandAtomMapping,
|
||||
LigandAtomMapper,
|
||||
LomapAtomMapper, lomap_scorers,
|
||||
PersesAtomMapper, perses_scorers,
|
||||
KartografAtomMapper,)
|
||||
|
||||
from gufe import LigandNetwork
|
||||
from . import ligand_network_planning
|
||||
|
||||
from .alchemical_network_planner import RHFEAlchemicalNetworkPlanner, RBFEAlchemicalNetworkPlanner
|
||||
@@ -1,368 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import mdtraj
|
||||
import pytest
|
||||
from importlib import resources
|
||||
from rdkit import Chem
|
||||
from rdkit.Chem import AllChem
|
||||
from openff.units import unit
|
||||
import urllib.request
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
import gufe
|
||||
import openfe
|
||||
from openfe.protocols.openmm_septop.utils import deserialize
|
||||
from gufe import AtomMapper, SmallMoleculeComponent, LigandAtomMapping
|
||||
|
||||
|
||||
class SlowTests:
|
||||
"""Plugin for handling fixtures that skips slow tests
|
||||
|
||||
Fixtures
|
||||
--------
|
||||
|
||||
Currently two fixture types are handled:
|
||||
* `integration`:
|
||||
Extremely slow tests that are meant to be run to truly put the code
|
||||
through a real run.
|
||||
|
||||
* `slow`:
|
||||
Unit tests that just take too long to be running regularly.
|
||||
|
||||
|
||||
How to use the fixtures
|
||||
-----------------------
|
||||
|
||||
To add these fixtures simply add `@pytest.mark.integration` or
|
||||
`@pytest.mark.slow` decorator to the relevant function or class.
|
||||
|
||||
|
||||
How to run tests marked by these fixtures
|
||||
-----------------------------------------
|
||||
|
||||
To run the `integration` tests, either use the `--integration` flag
|
||||
when invoking pytest, or set the environment variable
|
||||
`OFE_INTEGRATION_TESTS` to `true`. Note: triggering `integration` will
|
||||
automatically also trigger tests marked as `slow`.
|
||||
|
||||
To run the `slow` tests, either use the `--runslow` flag when invoking
|
||||
pytest, or set the environment variable `OFE_SLOW_TESTS` to `true`
|
||||
"""
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
@staticmethod
|
||||
def _modify_slow(items, config):
|
||||
msg = ("need --runslow pytest cli option or the environment variable "
|
||||
"`OFE_SLOW_TESTS` set to `True` to run")
|
||||
skip_slow = pytest.mark.skip(reason=msg)
|
||||
for item in items:
|
||||
if "slow" in item.keywords:
|
||||
item.add_marker(skip_slow)
|
||||
|
||||
@staticmethod
|
||||
def _modify_integration(items, config):
|
||||
msg = ("need --integration pytest cli option or the environment "
|
||||
"variable `OFE_INTEGRATION_TESTS` set to `True` to run")
|
||||
skip_int = pytest.mark.skip(reason=msg)
|
||||
for item in items:
|
||||
if "integration" in item.keywords:
|
||||
item.add_marker(skip_int)
|
||||
|
||||
def pytest_collection_modifyitems(self, items, config):
|
||||
if (config.getoption('--integration') or
|
||||
os.getenv("OFE_INTEGRATION_TESTS", default="false").lower() == 'true'):
|
||||
return
|
||||
elif (config.getoption('--runslow') or
|
||||
os.getenv("OFE_SLOW_TESTS", default="false").lower() == 'true'):
|
||||
self._modify_integration(items, config)
|
||||
else:
|
||||
self._modify_integration(items, config)
|
||||
self._modify_slow(items, config)
|
||||
|
||||
|
||||
# allow for optional slow tests
|
||||
# See: https://docs.pytest.org/en/latest/example/simple.html
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--runslow", action="store_true", default=False, help="run slow tests"
|
||||
)
|
||||
parser.addoption(
|
||||
"--integration", action="store_true", default=False,
|
||||
help="run long integration tests",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.pluginmanager.register(SlowTests(config), "slow")
|
||||
config.addinivalue_line("markers", "slow: mark test as slow")
|
||||
config.addinivalue_line(
|
||||
"markers", "integration: mark test as long integration test")
|
||||
|
||||
|
||||
def mol_from_smiles(smiles: str) -> Chem.Mol:
|
||||
m = Chem.MolFromSmiles(smiles)
|
||||
AllChem.Compute2DCoords(m)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def ethane():
|
||||
return SmallMoleculeComponent(mol_from_smiles('CC'))
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def simple_mapping():
|
||||
"""Disappearing oxygen on end
|
||||
|
||||
C C O
|
||||
|
||||
C C
|
||||
"""
|
||||
molA = SmallMoleculeComponent(mol_from_smiles('CCO'))
|
||||
molB = SmallMoleculeComponent(mol_from_smiles('CC'))
|
||||
|
||||
m = LigandAtomMapping(molA, molB, componentA_to_componentB={0: 0, 1: 1})
|
||||
|
||||
return m
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def other_mapping():
|
||||
"""Disappearing middle carbon
|
||||
|
||||
C C O
|
||||
|
||||
C C
|
||||
"""
|
||||
molA = SmallMoleculeComponent(mol_from_smiles('CCO'))
|
||||
molB = SmallMoleculeComponent(mol_from_smiles('CC'))
|
||||
|
||||
m = LigandAtomMapping(molA, molB, componentA_to_componentB={0: 0, 2: 1})
|
||||
|
||||
return m
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def lomap_basic_test_files_dir(tmpdir_factory):
|
||||
# for lomap, which wants the files in a directory
|
||||
lomap_files = tmpdir_factory.mktemp('lomap_files')
|
||||
lomap_basic = 'openfe.tests.data.lomap_basic'
|
||||
|
||||
for f in resources.contents(lomap_basic):
|
||||
if not f.endswith('mol2'):
|
||||
continue
|
||||
stuff = resources.read_binary(lomap_basic, f)
|
||||
|
||||
with open(str(lomap_files.join(f)), 'wb') as fout:
|
||||
fout.write(stuff)
|
||||
|
||||
yield str(lomap_files)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def atom_mapping_basic_test_files():
|
||||
# a dict of {filenames.strip(mol2): SmallMoleculeComponent} for a simple
|
||||
# set of ligands
|
||||
files = {}
|
||||
for f in [
|
||||
'1,3,7-trimethylnaphthalene',
|
||||
'1-butyl-4-methylbenzene',
|
||||
'2,6-dimethylnaphthalene',
|
||||
'2-methyl-6-propylnaphthalene',
|
||||
'2-methylnaphthalene',
|
||||
'2-naftanol',
|
||||
'methylcyclohexane',
|
||||
'toluene']:
|
||||
with resources.as_file(resources.files('openfe.tests.data.lomap_basic')) as d:
|
||||
fn = str(d / (f + '.mol2'))
|
||||
mol = Chem.MolFromMol2File(fn, removeHs=False)
|
||||
files[f] = SmallMoleculeComponent(mol, name=f)
|
||||
|
||||
return files
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def lomap_old_mapper() -> AtomMapper:
|
||||
"""
|
||||
LomapAtomMapper with the old default settings.
|
||||
|
||||
This is necessary as atom_mapping_basic_test_files are not all fully aligned
|
||||
and need both shift and a large max3d value.
|
||||
"""
|
||||
return openfe.setup.atom_mapping.LomapAtomMapper(
|
||||
time=20,
|
||||
threed=True,
|
||||
max3d=1000.0,
|
||||
element_change=True,
|
||||
seed="",
|
||||
shift=True,
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def benzene_toluene_topology():
|
||||
"""Load the mdtraj hybrid topology reference for benzene to toluene."""
|
||||
with resources.as_file(resources.files("openfe.tests.data.openmm_rfe")) as d:
|
||||
atoms = pd.read_csv(d / "benzene_toluene_hybrid_top"/ "hybrid_topology_atoms.csv")
|
||||
bonds = np.loadtxt(d / "benzene_toluene_hybrid_top"/ "hybrid_topology_bonds.txt")
|
||||
return mdtraj.Topology.from_dataframe(atoms=atoms, bonds=bonds)
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def benzene_modifications():
|
||||
files = {}
|
||||
with resources.as_file(resources.files('openfe.tests.data')) as d:
|
||||
fn = str(d / 'benzene_modifications.sdf')
|
||||
supp = Chem.SDMolSupplier(str(fn), removeHs=False)
|
||||
for rdmol in supp:
|
||||
files[rdmol.GetProp('_Name')] = SmallMoleculeComponent(rdmol)
|
||||
return files
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def charged_benzene_modifications():
|
||||
files = {}
|
||||
with resources.as_file(resources.files('openfe.tests.data.openmm_rfe')) as d:
|
||||
fn = str(d / 'charged_benzenes.sdf')
|
||||
supp = Chem.SDMolSupplier(str(fn), removeHs=False)
|
||||
for rdmol in supp:
|
||||
files[rdmol.GetProp('_Name')] = SmallMoleculeComponent(rdmol)
|
||||
return files
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def T4L_reference_xml():
|
||||
with resources.as_file(resources.files('openfe.tests.data.openmm_septop')) as d:
|
||||
f = str(d / 'system.xml.bz2')
|
||||
return deserialize(pathlib.Path(f))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def serialization_template():
|
||||
def inner(filename):
|
||||
loc = "openfe.tests.data.serialization"
|
||||
tmpl = resources.read_text(loc, filename)
|
||||
return tmpl.replace('{OFE_VERSION}', openfe.__version__)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def benzene_transforms():
|
||||
# a dict of Molecules for benzene transformations
|
||||
mols = {}
|
||||
with resources.as_file(resources.files('openfe.tests.data')) as d:
|
||||
fn = str(d / 'benzene_modifications.sdf')
|
||||
supplier = Chem.SDMolSupplier(fn, removeHs=False)
|
||||
for mol in supplier:
|
||||
mols[mol.GetProp('_Name')] = SmallMoleculeComponent(mol)
|
||||
return mols
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def T4_protein_component():
|
||||
with resources.as_file(resources.files('openfe.tests.data')) as d:
|
||||
fn = str(d / '181l_only.pdb')
|
||||
comp = gufe.ProteinComponent.from_pdb_file(fn, name="T4_protein")
|
||||
|
||||
return comp
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def eg5_protein_pdb():
|
||||
with resources.as_file(resources.files('openfe.tests.data.eg5')) as d:
|
||||
yield str(d / 'eg5_protein.pdb')
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def eg5_ligands_sdf():
|
||||
with resources.as_file(resources.files('openfe.tests.data.eg5')) as d:
|
||||
yield str(d / 'eg5_ligands.sdf')
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def eg5_cofactor_sdf():
|
||||
with resources.as_file(resources.files('openfe.tests.data.eg5')) as d:
|
||||
yield str(d / 'eg5_cofactor.sdf')
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def eg5_protein(eg5_protein_pdb) -> openfe.ProteinComponent:
|
||||
return openfe.ProteinComponent.from_pdb_file(eg5_protein_pdb)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def eg5_ligands(eg5_ligands_sdf) -> list[SmallMoleculeComponent]:
|
||||
return [SmallMoleculeComponent(m)
|
||||
for m in Chem.SDMolSupplier(eg5_ligands_sdf, removeHs=False)]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def eg5_cofactor(eg5_cofactor_sdf) -> SmallMoleculeComponent:
|
||||
return SmallMoleculeComponent.from_sdf_file(eg5_cofactor_sdf)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def orion_network():
|
||||
with resources.as_file(resources.files('openfe.tests.data.external_formats')) as d:
|
||||
yield str(d / 'somebenzenes_nes.dat')
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def fepplus_network():
|
||||
with resources.as_file(resources.files('openfe.tests.data.external_formats')) as d:
|
||||
yield str(d / 'somebenzenes_edges.edge')
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def CN_molecule():
|
||||
"""
|
||||
A basic CH3NH2 molecule for quick testing.
|
||||
"""
|
||||
with resources.as_file(resources.files('openfe.tests.data')) as d:
|
||||
fn = str(d / 'CN.sdf')
|
||||
supp = Chem.SDMolSupplier(str(fn), removeHs=False)
|
||||
|
||||
smc = [SmallMoleculeComponent(i) for i in supp][0]
|
||||
|
||||
return smc
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def am1bcc_ref_charges():
|
||||
ref_chgs = {
|
||||
'ambertools': [
|
||||
0.146957, -0.918943, 0.025557, 0.025557,
|
||||
0.025557, 0.347657, 0.347657
|
||||
] * unit.elementary_charge,
|
||||
'openeye': [
|
||||
0.14713, -0.92016, 0.02595, 0.02595,
|
||||
0.02595, 0.34759, 0.34759
|
||||
] * unit.elementary_charge,
|
||||
'nagl': [
|
||||
0.170413, -0.930417, 0.021593, 0.021593,
|
||||
0.021593, 0.347612, 0.347612
|
||||
] * unit.elementary_charge,
|
||||
'espaloma': [
|
||||
0.017702, -0.966793, 0.063076, 0.063076,
|
||||
0.063076, 0.379931, 0.379931
|
||||
] * unit.elementary_charge,
|
||||
}
|
||||
return ref_chgs
|
||||
|
||||
try:
|
||||
urllib.request.urlopen('https://www.google.com')
|
||||
except: # -no-cov-
|
||||
HAS_INTERNET = False
|
||||
else:
|
||||
HAS_INTERNET = True
|
||||
|
||||
try:
|
||||
import espaloma
|
||||
HAS_ESPALOMA = True
|
||||
except ModuleNotFoundError:
|
||||
HAS_ESPALOMA = False
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,357 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
import gzip
|
||||
import pytest
|
||||
import pooch
|
||||
from importlib import resources
|
||||
from typing import Optional
|
||||
from rdkit import Chem
|
||||
from rdkit.Geometry import Point3D
|
||||
import openmm
|
||||
from openmm import Platform
|
||||
import openfe
|
||||
from openff.units.openmm import from_openmm
|
||||
from openff.units import unit, Quantity
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def available_platforms() -> set[str]:
|
||||
return {
|
||||
Platform.getPlatform(i).getName()
|
||||
for i in range(Platform.getNumPlatforms())
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def benzene_vacuum_system(benzene_modifications):
|
||||
return openfe.ChemicalSystem(
|
||||
{'ligand': benzene_modifications['benzene']},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def benzene_system(benzene_modifications):
|
||||
return openfe.ChemicalSystem(
|
||||
{'ligand': benzene_modifications['benzene'],
|
||||
'solvent': openfe.SolventComponent(
|
||||
positive_ion='Na', negative_ion='Cl',
|
||||
ion_concentration=0.15 * unit.molar)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def benzene_complex_system(benzene_modifications, T4_protein_component):
|
||||
return openfe.ChemicalSystem(
|
||||
{'ligand': benzene_modifications['benzene'],
|
||||
'solvent': openfe.SolventComponent(
|
||||
positive_ion='Na', negative_ion='Cl',
|
||||
ion_concentration=0.15 * unit.molar),
|
||||
'protein': T4_protein_component,}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def toluene_vacuum_system(benzene_modifications):
|
||||
return openfe.ChemicalSystem(
|
||||
{'ligand': benzene_modifications['toluene']},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def toluene_system(benzene_modifications):
|
||||
return openfe.ChemicalSystem(
|
||||
{'ligand': benzene_modifications['toluene'],
|
||||
'solvent': openfe.SolventComponent(
|
||||
positive_ion='Na', negative_ion='Cl',
|
||||
ion_concentration=0.15 * unit.molar),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def toluene_complex_system(benzene_modifications, T4_protein_component):
|
||||
return openfe.ChemicalSystem(
|
||||
{'ligand': benzene_modifications['toluene'],
|
||||
'solvent': openfe.SolventComponent(
|
||||
positive_ion='Na', negative_ion='Cl',
|
||||
ion_concentration=0.15 * unit.molar),
|
||||
'protein': T4_protein_component,}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def benzene_to_toluene_mapping(benzene_modifications):
|
||||
mapper = openfe.setup.LomapAtomMapper(element_change=False)
|
||||
|
||||
molA = benzene_modifications['benzene']
|
||||
molB = benzene_modifications['toluene']
|
||||
|
||||
return next(mapper.suggest_mappings(molA, molB))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def benzene_charges():
|
||||
files = {}
|
||||
with resources.as_file(resources.files('openfe.tests.data.openmm_rfe')) as d:
|
||||
fn = str(d / 'charged_benzenes.sdf')
|
||||
supp = Chem.SDMolSupplier(str(fn), removeHs=False)
|
||||
for rdmol in supp:
|
||||
files[rdmol.GetProp('_Name')] = openfe.SmallMoleculeComponent(rdmol)
|
||||
return files
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def benzene_to_benzoic_mapping(benzene_charges):
|
||||
mapper = openfe.setup.LomapAtomMapper(element_change=False)
|
||||
molA = benzene_charges['benzene']
|
||||
molB = benzene_charges['benzoic_acid']
|
||||
return next(mapper.suggest_mappings(molA, molB))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def benzoic_to_benzene_mapping(benzene_charges):
|
||||
mapper = openfe.setup.LomapAtomMapper(element_change=False)
|
||||
molA = benzene_charges['benzoic_acid']
|
||||
molB = benzene_charges['benzene']
|
||||
return next(mapper.suggest_mappings(molA, molB))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def benzene_to_aniline_mapping(benzene_charges):
|
||||
mapper = openfe.setup.LomapAtomMapper(element_change=False)
|
||||
molA = benzene_charges['benzene']
|
||||
molB = benzene_charges['aniline']
|
||||
return next(mapper.suggest_mappings(molA, molB))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aniline_to_benzene_mapping(benzene_charges):
|
||||
mapper = openfe.setup.LomapAtomMapper(element_change=False)
|
||||
molA = benzene_charges['aniline']
|
||||
molB = benzene_charges['benzene']
|
||||
return next(mapper.suggest_mappings(molA, molB))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aniline_to_benzoic_mapping(benzene_charges):
|
||||
mapper = openfe.setup.LomapAtomMapper(element_change=False)
|
||||
molA = benzene_charges['aniline']
|
||||
molB = benzene_charges['benzoic_acid']
|
||||
return next(mapper.suggest_mappings(molA, molB))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def benzene_many_solv_system(benzene_modifications):
|
||||
|
||||
rdmol_phenol = benzene_modifications['phenol'].to_rdkit()
|
||||
rdmol_benzo = benzene_modifications['benzonitrile'].to_rdkit()
|
||||
|
||||
conf_phenol = rdmol_phenol.GetConformer()
|
||||
conf_benzo = rdmol_benzo.GetConformer()
|
||||
|
||||
for atm in range(rdmol_phenol.GetNumAtoms()):
|
||||
x, y, z = conf_phenol.GetAtomPosition(atm)
|
||||
conf_phenol.SetAtomPosition(atm, Point3D(x+30, y, z))
|
||||
|
||||
for atm in range(rdmol_benzo.GetNumAtoms()):
|
||||
x, y, z = conf_benzo.GetAtomPosition(atm)
|
||||
conf_benzo.SetAtomPosition(atm, Point3D(x, y+30, z))
|
||||
|
||||
phenol = openfe.SmallMoleculeComponent.from_rdkit(
|
||||
rdmol_phenol, name='phenol'
|
||||
)
|
||||
|
||||
benzo = openfe.SmallMoleculeComponent.from_rdkit(
|
||||
rdmol_benzo, name='benzonitrile'
|
||||
)
|
||||
|
||||
return openfe.ChemicalSystem(
|
||||
{'whatligand': benzene_modifications['benzene'],
|
||||
"foo": phenol,
|
||||
"bar": benzo,
|
||||
"solvent": openfe.SolventComponent()},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def toluene_many_solv_system(benzene_modifications):
|
||||
|
||||
rdmol_phenol = benzene_modifications['phenol'].to_rdkit()
|
||||
rdmol_benzo = benzene_modifications['benzonitrile'].to_rdkit()
|
||||
|
||||
conf_phenol = rdmol_phenol.GetConformer()
|
||||
conf_benzo = rdmol_benzo.GetConformer()
|
||||
|
||||
for atm in range(rdmol_phenol.GetNumAtoms()):
|
||||
x, y, z = conf_phenol.GetAtomPosition(atm)
|
||||
conf_phenol.SetAtomPosition(atm, Point3D(x+30, y, z))
|
||||
|
||||
for atm in range(rdmol_benzo.GetNumAtoms()):
|
||||
x, y, z = conf_benzo.GetAtomPosition(atm)
|
||||
conf_benzo.SetAtomPosition(atm, Point3D(x, y+30, z))
|
||||
|
||||
phenol = openfe.SmallMoleculeComponent.from_rdkit(
|
||||
rdmol_phenol, name='phenol'
|
||||
)
|
||||
|
||||
benzo = openfe.SmallMoleculeComponent.from_rdkit(
|
||||
rdmol_benzo, name='benzonitrile'
|
||||
)
|
||||
return openfe.ChemicalSystem(
|
||||
{'whatligand': benzene_modifications['toluene'],
|
||||
"foo": phenol,
|
||||
"bar": benzo,
|
||||
"solvent": openfe.SolventComponent()},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rfe_transformation_json() -> str:
|
||||
"""string of a RFE results similar to quickrun
|
||||
|
||||
generated with gen-serialized-results.py
|
||||
"""
|
||||
d = resources.files('openfe.tests.data.openmm_rfe')
|
||||
|
||||
with gzip.open((d / 'RHFEProtocol_json_results.gz').as_posix(), 'r') as f: # type: ignore
|
||||
return f.read().decode() # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def afe_solv_transformation_json() -> str:
|
||||
"""
|
||||
string of a Absolute Solvation result (CN in water) generated by quickrun
|
||||
|
||||
generated with gen-serialized-results.py
|
||||
"""
|
||||
d = resources.files('openfe.tests.data.openmm_afe')
|
||||
fname = "AHFEProtocol_json_results.gz"
|
||||
|
||||
with gzip.open((d / fname).as_posix(), 'r') as f: # type: ignore
|
||||
return f.read().decode() # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def md_json() -> str:
|
||||
"""
|
||||
string of a MD result (TYK ligand lig_ejm_31 in water) generated by quickrun
|
||||
|
||||
generated with gen-serialized-results.py
|
||||
"""
|
||||
d = resources.files('openfe.tests.data.openmm_md')
|
||||
fname = "MDProtocol_json_results.gz"
|
||||
|
||||
with gzip.open((d / fname).as_posix(), 'r') as f: # type: ignore
|
||||
return f.read().decode() # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def septop_json() -> str:
|
||||
"""
|
||||
Path to a SepTop result (hif2a)
|
||||
|
||||
generated with gen-serialized-results.py
|
||||
"""
|
||||
d = resources.files('openfe.tests.data.openmm_septop')
|
||||
fname = "SepTopProtocol_json_results.gz"
|
||||
|
||||
with gzip.open((d / fname).as_posix(), 'r') as f: # type: ignore
|
||||
return f.read().decode() # type: ignore
|
||||
|
||||
|
||||
RFE_OUTPUT = pooch.create(
|
||||
path=pooch.os_cache("openfe_analysis"),
|
||||
base_url="doi:10.6084/m9.figshare.24101655",
|
||||
registry={
|
||||
"checkpoint.nc": "5af398cb14340fddf7492114998b244424b6c3f4514b2e07e4bd411484c08464",
|
||||
"db.json": "b671f9eb4daf9853f3e1645f9fd7c18150fd2a9bf17c18f23c5cf0c9fd5ca5b3",
|
||||
"hybrid_system.pdb": "07203679cb14b840b36e4320484df2360f45e323faadb02d6eacac244fddd517",
|
||||
"simulation.nc": "92361a0864d4359a75399470135f56642b72c605069a4c33dbc4be6f91f28b31",
|
||||
"simulation_real_time_analysis.yaml": "65706002f371fafba96037f29b054fd7e050e442915205df88567f48f5e5e1cf",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simulation_nc():
|
||||
return RFE_OUTPUT.fetch("simulation.nc")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def get_available_openmm_platforms() -> set[str]:
|
||||
"""
|
||||
OpenMM Platforms that are available and functional on system
|
||||
"""
|
||||
import openmm
|
||||
from openmm import Platform
|
||||
# Get platforms that openmm was built with
|
||||
platforms = {Platform.getPlatform(i).getName() for i in range(Platform.getNumPlatforms())}
|
||||
|
||||
# Now check if we can actually use the platforms
|
||||
working_platforms = set()
|
||||
for platform in platforms:
|
||||
system = openmm.System()
|
||||
system.addParticle(1.0)
|
||||
integrator = openmm.VerletIntegrator(0.001)
|
||||
try:
|
||||
context = openmm.Context(system, integrator, Platform.getPlatformByName(platform))
|
||||
working_platforms.add(platform)
|
||||
del context
|
||||
except openmm.OpenMMException:
|
||||
continue
|
||||
finally:
|
||||
del system, integrator
|
||||
|
||||
|
||||
return working_platforms
|
||||
|
||||
|
||||
def compute_energy(
|
||||
system: openmm.System,
|
||||
positions: openmm.unit.Quantity,
|
||||
box_vectors: Optional[openmm.unit.Quantity],
|
||||
context_params: Optional[dict[str, float]] = None,
|
||||
platform=None,
|
||||
) -> Quantity:
|
||||
"""
|
||||
Computes the potential energy of a system at a given set of positions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
system: openmm.System
|
||||
The system to compute the energy of.
|
||||
positions: openmm.unit.Quantity
|
||||
The positions to compute the energy at.
|
||||
box_vectors: Optional[openmm.unit.Quantity]
|
||||
The box vectors to use if any.
|
||||
context_params: Optional[dict[str, float]]
|
||||
Any global context parameters to set.
|
||||
platform: str
|
||||
The platform to use.
|
||||
|
||||
Returns
|
||||
-------
|
||||
potential : openff.units.Quantity
|
||||
The computed potential energy in openff unit.
|
||||
"""
|
||||
context_params = context_params if context_params is not None else {}
|
||||
|
||||
integrator = openmm.VerletIntegrator(0.0001 * openmm.unit.femtoseconds)
|
||||
|
||||
if platform is None:
|
||||
context = openmm.Context(system, integrator)
|
||||
if platform is not None:
|
||||
context = openmm.Context(system, integrator, platform)
|
||||
|
||||
for key, value in context_params.items():
|
||||
context.setParameter(key, value)
|
||||
|
||||
if box_vectors is not None:
|
||||
context.setPeriodicBoxVectors(*box_vectors)
|
||||
context.setPositions(positions)
|
||||
|
||||
state = context.getState(getEnergy=True)
|
||||
potential = state.getPotentialEnergy()
|
||||
del context, integrator, state
|
||||
return from_openmm(potential)
|
||||
@@ -1,722 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
import itertools
|
||||
import json
|
||||
from math import sqrt
|
||||
import sys
|
||||
import pytest
|
||||
from unittest import mock
|
||||
from openmm import NonbondedForce, CustomNonbondedForce
|
||||
from openmmtools.multistate.multistatesampler import MultiStateSampler
|
||||
from openff.units import unit as offunit
|
||||
from openff.units.openmm import ensure_quantity, from_openmm
|
||||
import mdtraj as mdt
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose
|
||||
import gufe
|
||||
import openfe
|
||||
from openfe import ChemicalSystem, SolventComponent
|
||||
from openfe.protocols import openmm_afe
|
||||
from openfe.protocols.openmm_afe import (
|
||||
AbsoluteSolvationSolventUnit,
|
||||
AbsoluteSolvationVacuumUnit,
|
||||
AbsoluteSolvationProtocol,
|
||||
)
|
||||
|
||||
from openfe.protocols.openmm_utils import system_validation
|
||||
from openfe.protocols.openmm_utils.charge_generation import (
|
||||
HAS_NAGL, HAS_OPENEYE, HAS_ESPALOMA_CHARGE
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def default_settings():
|
||||
return AbsoluteSolvationProtocol.default_settings()
|
||||
|
||||
|
||||
def test_create_default_protocol(default_settings):
|
||||
# this is roughly how it should be created
|
||||
protocol = AbsoluteSolvationProtocol(
|
||||
settings=default_settings,
|
||||
)
|
||||
assert protocol
|
||||
|
||||
|
||||
def test_serialize_protocol(default_settings):
|
||||
protocol = AbsoluteSolvationProtocol(
|
||||
settings=default_settings,
|
||||
)
|
||||
|
||||
ser = protocol.to_dict()
|
||||
ret = AbsoluteSolvationProtocol.from_dict(ser)
|
||||
assert protocol == ret
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', [
|
||||
'repex', 'sams', 'independent', 'InDePeNdENT'
|
||||
])
|
||||
def test_dry_run_vac_benzene(benzene_modifications,
|
||||
method, tmpdir):
|
||||
s = openmm_afe.AbsoluteSolvationProtocol.default_settings()
|
||||
s.protocol_repeats = 1
|
||||
s.vacuum_simulation_settings.sampler_method = method
|
||||
|
||||
protocol = openmm_afe.AbsoluteSolvationProtocol(
|
||||
settings=s,
|
||||
)
|
||||
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': benzene_modifications['benzene'],
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
# Create DAG from protocol, get the vacuum and solvent units
|
||||
# and eventually dry run the first vacuum unit
|
||||
dag = protocol.create(
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
mapping=None,
|
||||
)
|
||||
prot_units = list(dag.protocol_units)
|
||||
|
||||
assert len(prot_units) == 2
|
||||
|
||||
vac_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationVacuumUnit)]
|
||||
sol_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationSolventUnit)]
|
||||
|
||||
assert len(vac_unit) == 1
|
||||
assert len(sol_unit) == 1
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
vac_sampler = vac_unit[0].run(dry=True)['debug']['sampler']
|
||||
assert not vac_sampler.is_periodic
|
||||
|
||||
|
||||
def test_confgen_fail_AFE(benzene_modifications, tmpdir):
|
||||
# check system parametrisation works even if confgen fails
|
||||
s = openmm_afe.AbsoluteSolvationProtocol.default_settings()
|
||||
s.protocol_repeats = 1
|
||||
|
||||
protocol = openmm_afe.AbsoluteSolvationProtocol(
|
||||
settings=s,
|
||||
)
|
||||
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': benzene_modifications['benzene'],
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
# Create DAG from protocol, get the vacuum and solvent units
|
||||
# and eventually dry run the first vacuum unit
|
||||
dag = protocol.create(
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
mapping=None,
|
||||
)
|
||||
prot_units = list(dag.protocol_units)
|
||||
vac_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationVacuumUnit)]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
with mock.patch('rdkit.Chem.AllChem.EmbedMultipleConfs', return_value=0):
|
||||
vac_sampler = vac_unit[0].run(dry=True)['debug']['sampler']
|
||||
|
||||
assert vac_sampler
|
||||
|
||||
|
||||
def test_dry_run_solv_benzene(benzene_modifications, tmpdir):
|
||||
s = openmm_afe.AbsoluteSolvationProtocol.default_settings()
|
||||
s.protocol_repeats = 1
|
||||
s.solvent_output_settings.output_indices = "resname UNK"
|
||||
|
||||
protocol = openmm_afe.AbsoluteSolvationProtocol(
|
||||
settings=s,
|
||||
)
|
||||
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': benzene_modifications['benzene'],
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
# Create DAG from protocol, get the vacuum and solvent units
|
||||
# and eventually dry run the first solvent unit
|
||||
dag = protocol.create(
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
mapping=None,
|
||||
)
|
||||
prot_units = list(dag.protocol_units)
|
||||
|
||||
assert len(prot_units) == 2
|
||||
|
||||
vac_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationVacuumUnit)]
|
||||
sol_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationSolventUnit)]
|
||||
|
||||
assert len(vac_unit) == 1
|
||||
assert len(sol_unit) == 1
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
sol_sampler = sol_unit[0].run(dry=True)['debug']['sampler']
|
||||
assert sol_sampler.is_periodic
|
||||
|
||||
pdb = mdt.load_pdb('hybrid_system.pdb')
|
||||
assert pdb.n_atoms == 12
|
||||
|
||||
|
||||
def test_dry_run_solv_benzene_tip4p(benzene_modifications, tmpdir):
|
||||
s = AbsoluteSolvationProtocol.default_settings()
|
||||
s.protocol_repeats = 1
|
||||
s.vacuum_forcefield_settings.forcefields = [
|
||||
"amber/ff14SB.xml", # ff14SB protein force field
|
||||
"amber/tip4pew_standard.xml", # FF we are testsing with the fun VS
|
||||
"amber/phosaa10.xml", # Handles THE TPO
|
||||
]
|
||||
s.solvent_forcefield_settings.forcefields = [
|
||||
"amber/ff14SB.xml", # ff14SB protein force field
|
||||
"amber/tip4pew_standard.xml", # FF we are testsing with the fun VS
|
||||
"amber/phosaa10.xml", # Handles THE TPO
|
||||
]
|
||||
s.solvation_settings.solvent_model = 'tip4pew'
|
||||
s.integrator_settings.reassign_velocities = True
|
||||
|
||||
protocol = AbsoluteSolvationProtocol(
|
||||
settings=s,
|
||||
)
|
||||
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': benzene_modifications['benzene'],
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
# Create DAG from protocol, get the vacuum and solvent units
|
||||
# and eventually dry run the first solvent unit
|
||||
dag = protocol.create(
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
mapping=None,
|
||||
)
|
||||
prot_units = list(dag.protocol_units)
|
||||
|
||||
sol_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationSolventUnit)]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
sol_sampler = sol_unit[0].run(dry=True)['debug']['sampler']
|
||||
assert sol_sampler.is_periodic
|
||||
|
||||
|
||||
def test_dry_run_solv_benzene_noncubic(
|
||||
benzene_modifications, tmpdir
|
||||
):
|
||||
s = AbsoluteSolvationProtocol.default_settings()
|
||||
s.solvation_settings.solvent_padding = 1.5 * offunit.nanometer
|
||||
s.solvation_settings.box_shape = 'dodecahedron'
|
||||
|
||||
protocol = AbsoluteSolvationProtocol(settings=s)
|
||||
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': benzene_modifications['benzene'],
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
# Create DAG from protocol, get the vacuum and solvent units
|
||||
# and eventually dry run the first solvent unit
|
||||
dag = protocol.create(
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
mapping=None,
|
||||
)
|
||||
prot_units = list(dag.protocol_units)
|
||||
|
||||
sol_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationSolventUnit)]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
sampler = sol_unit[0].run(dry=True)['debug']['sampler']
|
||||
system = sampler._thermodynamic_states[0].system
|
||||
|
||||
vectors = system.getDefaultPeriodicBoxVectors()
|
||||
width = float(from_openmm(vectors)[0][0].to('nanometer').m)
|
||||
|
||||
# dodecahedron has the following shape:
|
||||
# [width, 0, 0], [0, width, 0], [0.5, 0.5, 0.5 * sqrt(2)] * width
|
||||
|
||||
expected_vectors = [
|
||||
[width, 0, 0],
|
||||
[0, width, 0],
|
||||
[0.5 * width, 0.5 * width, 0.5 * sqrt(2) * width],
|
||||
] * offunit.nanometer
|
||||
assert_allclose(
|
||||
expected_vectors,
|
||||
from_openmm(vectors)
|
||||
)
|
||||
|
||||
|
||||
def test_dry_run_solv_user_charges_benzene(benzene_modifications, tmpdir):
|
||||
"""
|
||||
Create a test system with fictitious user supplied charges and
|
||||
ensure that they are properly passed through to the constructed
|
||||
alchemical system.
|
||||
"""
|
||||
s = openmm_afe.AbsoluteSolvationProtocol.default_settings()
|
||||
s.protocol_repeats = 1
|
||||
|
||||
protocol = openmm_afe.AbsoluteSolvationProtocol(
|
||||
settings=s,
|
||||
)
|
||||
|
||||
def assign_fictitious_charges(offmol):
|
||||
"""
|
||||
Get a random array of fake partial charges for your offmol.
|
||||
"""
|
||||
rand_arr = np.random.randint(1, 10, size=offmol.n_atoms) / 100
|
||||
rand_arr[-1] = -sum(rand_arr[:-1])
|
||||
return rand_arr * offunit.elementary_charge
|
||||
|
||||
benzene_offmol = benzene_modifications['benzene'].to_openff()
|
||||
offmol_pchgs = assign_fictitious_charges(benzene_offmol)
|
||||
benzene_offmol.partial_charges = offmol_pchgs
|
||||
benzene_smc = openfe.SmallMoleculeComponent.from_openff(benzene_offmol)
|
||||
|
||||
# check propchgs
|
||||
prop_chgs = benzene_smc.to_dict()['molprops']['atom.dprop.PartialCharge']
|
||||
prop_chgs = np.array(prop_chgs.split(), dtype=float)
|
||||
np.testing.assert_allclose(prop_chgs, offmol_pchgs)
|
||||
|
||||
# Create ChemicalSystems
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': benzene_smc,
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
# Create DAG from protocol, get the vacuum and solvent units
|
||||
# and eventually dry run the first solvent unit
|
||||
dag = protocol.create(stateA=stateA, stateB=stateB, mapping=None,)
|
||||
prot_units = list(dag.protocol_units)
|
||||
|
||||
vac_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationVacuumUnit)][0]
|
||||
sol_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationSolventUnit)][0]
|
||||
|
||||
# check sol_unit charges
|
||||
with tmpdir.as_cwd():
|
||||
sampler = sol_unit.run(dry=True)['debug']['sampler']
|
||||
system = sampler._thermodynamic_states[0].system
|
||||
nonbond = [f for f in system.getForces()
|
||||
if isinstance(f, NonbondedForce)]
|
||||
|
||||
assert len(nonbond) == 1
|
||||
|
||||
# loop through the 12 benzene atoms
|
||||
# partial charge is stored in the offset
|
||||
for i in range(12):
|
||||
offsets = nonbond[0].getParticleParameterOffset(i)
|
||||
c = ensure_quantity(offsets[2], 'openff')
|
||||
assert pytest.approx(c) == prop_chgs[i]
|
||||
|
||||
# check vac_unit charges
|
||||
with tmpdir.as_cwd():
|
||||
sampler = vac_unit.run(dry=True)['debug']['sampler']
|
||||
system = sampler._thermodynamic_states[0].system
|
||||
nonbond = [f for f in system.getForces()
|
||||
if isinstance(f, CustomNonbondedForce)]
|
||||
assert len(nonbond) == 4
|
||||
|
||||
custom_elec = [
|
||||
n for n in nonbond if
|
||||
n.getGlobalParameterName(0) == 'lambda_electrostatics'][0]
|
||||
|
||||
# loop through the 12 benzene atoms
|
||||
for i in range(12):
|
||||
c, s = custom_elec.getParticleParameters(i)
|
||||
c = ensure_quantity(c, 'openff')
|
||||
assert pytest.approx(c) == prop_chgs[i]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method, backend, ref_key', [
|
||||
('am1bcc', 'ambertools', 'ambertools'),
|
||||
pytest.param(
|
||||
'am1bcc', 'openeye', 'openeye',
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_OPENEYE, reason='needs oechem',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'nagl', 'rdkit', 'nagl',
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_NAGL or sys.platform.startswith('darwin'),
|
||||
reason='needs NAGL and/or on macos',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'espaloma', 'rdkit', 'espaloma',
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_ESPALOMA_CHARGE, reason='needs espaloma charge',
|
||||
),
|
||||
),
|
||||
])
|
||||
def test_dry_run_charge_backends(
|
||||
CN_molecule, tmpdir, method, backend, ref_key, am1bcc_ref_charges
|
||||
):
|
||||
"""
|
||||
Check that partial charge generation with different backends
|
||||
works as expected.
|
||||
"""
|
||||
s = openmm_afe.AbsoluteSolvationProtocol.default_settings()
|
||||
s.protocol_repeats = 1
|
||||
s.partial_charge_settings.partial_charge_method = method
|
||||
s.partial_charge_settings.off_toolkit_backend = backend
|
||||
s.partial_charge_settings.nagl_model = 'openff-gnn-am1bcc-0.1.0-rc.1.pt'
|
||||
|
||||
protocol = openmm_afe.AbsoluteSolvationProtocol(settings=s)
|
||||
|
||||
# Create ChemicalSystems
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': CN_molecule,
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
# Create DAG from protocol, get the vacuum and solvent units
|
||||
# and eventually dry run the first solvent unit
|
||||
dag = protocol.create(stateA=stateA, stateB=stateB, mapping=None)
|
||||
prot_units = list(dag.protocol_units)
|
||||
|
||||
vac_unit = [u for u in prot_units
|
||||
if isinstance(u, AbsoluteSolvationVacuumUnit)][0]
|
||||
|
||||
# check vac_unit charges
|
||||
with tmpdir.as_cwd():
|
||||
sampler = vac_unit.run(dry=True)['debug']['sampler']
|
||||
system = sampler._thermodynamic_states[0].system
|
||||
nonbond = [f for f in system.getForces()
|
||||
if isinstance(f, CustomNonbondedForce)]
|
||||
assert len(nonbond) == 4
|
||||
|
||||
custom_elec = [
|
||||
n for n in nonbond if
|
||||
n.getGlobalParameterName(0) == 'lambda_electrostatics'][0]
|
||||
|
||||
charges = []
|
||||
for i in range(system.getNumParticles()):
|
||||
c, s = custom_elec.getParticleParameters(i)
|
||||
charges.append(c)
|
||||
|
||||
assert_allclose(
|
||||
am1bcc_ref_charges[ref_key],
|
||||
charges * offunit.elementary_charge,
|
||||
rtol=1e-4,
|
||||
)
|
||||
|
||||
|
||||
def test_high_timestep(benzene_modifications, tmpdir):
|
||||
s = AbsoluteSolvationProtocol.default_settings()
|
||||
s.protocol_repeats = 1
|
||||
s.solvent_forcefield_settings.hydrogen_mass = 1.0
|
||||
s.vacuum_forcefield_settings.hydrogen_mass = 1.0
|
||||
|
||||
protocol = AbsoluteSolvationProtocol(
|
||||
settings=s,
|
||||
)
|
||||
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': benzene_modifications['benzene'],
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
dag = protocol.create(
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
mapping=None,
|
||||
)
|
||||
prot_units = list(dag.protocol_units)
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
errmsg = "too large for hydrogen mass"
|
||||
with pytest.raises(ValueError, match=errmsg):
|
||||
prot_units[0].run(dry=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def benzene_solvation_dag(benzene_modifications):
|
||||
s = AbsoluteSolvationProtocol.default_settings()
|
||||
|
||||
protocol = openmm_afe.AbsoluteSolvationProtocol(
|
||||
settings=s,
|
||||
)
|
||||
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': benzene_modifications['benzene'],
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
return protocol.create(stateA=stateA, stateB=stateB, mapping=None)
|
||||
|
||||
|
||||
def test_unit_tagging(benzene_solvation_dag, tmpdir):
|
||||
# test that executing the units includes correct gen and repeat info
|
||||
|
||||
dag_units = benzene_solvation_dag.protocol_units
|
||||
|
||||
with (
|
||||
mock.patch('openfe.protocols.openmm_afe.equil_solvation_afe_method.AbsoluteSolvationSolventUnit.run',
|
||||
return_value={'nc': 'file.nc', 'last_checkpoint': 'chck.nc'}),
|
||||
mock.patch('openfe.protocols.openmm_afe.equil_solvation_afe_method.AbsoluteSolvationVacuumUnit.run',
|
||||
return_value={'nc': 'file.nc', 'last_checkpoint': 'chck.nc'}),
|
||||
):
|
||||
results = []
|
||||
for u in dag_units:
|
||||
ret = u.execute(context=gufe.Context(tmpdir, tmpdir))
|
||||
results.append(ret)
|
||||
|
||||
solv_repeats = set()
|
||||
vac_repeats = set()
|
||||
for ret in results:
|
||||
assert isinstance(ret, gufe.ProtocolUnitResult)
|
||||
assert ret.outputs['generation'] == 0
|
||||
if ret.outputs['simtype'] == 'vacuum':
|
||||
vac_repeats.add(ret.outputs['repeat_id'])
|
||||
else:
|
||||
solv_repeats.add(ret.outputs['repeat_id'])
|
||||
# Repeat ids are random ints so just check their lengths
|
||||
assert len(vac_repeats) == len(solv_repeats) == 3
|
||||
|
||||
|
||||
def test_gather(benzene_solvation_dag, tmpdir):
|
||||
# check that .gather behaves as expected
|
||||
with (
|
||||
mock.patch('openfe.protocols.openmm_afe.equil_solvation_afe_method.AbsoluteSolvationSolventUnit.run',
|
||||
return_value={'nc': 'file.nc', 'last_checkpoint': 'chck.nc'}),
|
||||
mock.patch('openfe.protocols.openmm_afe.equil_solvation_afe_method.AbsoluteSolvationVacuumUnit.run',
|
||||
return_value={'nc': 'file.nc', 'last_checkpoint': 'chck.nc'}),
|
||||
):
|
||||
dagres = gufe.protocols.execute_DAG(benzene_solvation_dag,
|
||||
shared_basedir=tmpdir,
|
||||
scratch_basedir=tmpdir,
|
||||
keep_shared=True)
|
||||
|
||||
protocol = AbsoluteSolvationProtocol(
|
||||
settings=AbsoluteSolvationProtocol.default_settings(),
|
||||
)
|
||||
|
||||
res = protocol.gather([dagres])
|
||||
|
||||
assert isinstance(res, openmm_afe.AbsoluteSolvationProtocolResult)
|
||||
|
||||
|
||||
class TestProtocolResult:
|
||||
@pytest.fixture()
|
||||
def protocolresult(self, afe_solv_transformation_json):
|
||||
d = json.loads(afe_solv_transformation_json,
|
||||
cls=gufe.tokenization.JSON_HANDLER.decoder)
|
||||
|
||||
pr = openfe.ProtocolResult.from_dict(d['protocol_result'])
|
||||
|
||||
return pr
|
||||
|
||||
def test_reload_protocol_result(self, afe_solv_transformation_json):
|
||||
d = json.loads(afe_solv_transformation_json,
|
||||
cls=gufe.tokenization.JSON_HANDLER.decoder)
|
||||
|
||||
pr = openmm_afe.AbsoluteSolvationProtocolResult.from_dict(d['protocol_result'])
|
||||
|
||||
assert pr
|
||||
|
||||
def test_get_estimate(self, protocolresult):
|
||||
est = protocolresult.get_estimate()
|
||||
|
||||
assert est
|
||||
assert est.m == pytest.approx(-2.47, abs=0.5)
|
||||
assert isinstance(est, offunit.Quantity)
|
||||
assert est.is_compatible_with(offunit.kilojoule_per_mole)
|
||||
|
||||
def test_get_uncertainty(self, protocolresult):
|
||||
est = protocolresult.get_uncertainty()
|
||||
|
||||
assert est
|
||||
assert est.m == pytest.approx(0.2, abs=0.2)
|
||||
assert isinstance(est, offunit.Quantity)
|
||||
assert est.is_compatible_with(offunit.kilojoule_per_mole)
|
||||
|
||||
def test_get_individual(self, protocolresult):
|
||||
inds = protocolresult.get_individual_estimates()
|
||||
|
||||
assert isinstance(inds, dict)
|
||||
assert isinstance(inds['solvent'], list)
|
||||
assert isinstance(inds['vacuum'], list)
|
||||
assert len(inds['solvent']) == len(inds['vacuum']) == 3
|
||||
for e, u in itertools.chain(inds['solvent'], inds['vacuum']):
|
||||
assert e.is_compatible_with(offunit.kilojoule_per_mole)
|
||||
assert u.is_compatible_with(offunit.kilojoule_per_mole)
|
||||
|
||||
@pytest.mark.parametrize('key', ['solvent', 'vacuum'])
|
||||
def test_get_forwards_etc(self, key, protocolresult):
|
||||
far = protocolresult.get_forward_and_reverse_energy_analysis()
|
||||
|
||||
assert isinstance(far, dict)
|
||||
assert isinstance(far[key], list)
|
||||
far1 = far[key][0]
|
||||
assert isinstance(far1, dict)
|
||||
|
||||
for k in ['fractions', 'forward_DGs', 'forward_dDGs',
|
||||
'reverse_DGs', 'reverse_dDGs']:
|
||||
assert k in far1
|
||||
|
||||
if k == 'fractions':
|
||||
assert isinstance(far1[k], np.ndarray)
|
||||
|
||||
@pytest.mark.parametrize('key', ['solvent', 'vacuum'])
|
||||
def test_get_frwd_reverse_none_return(self, key, protocolresult):
|
||||
# fetch the first result of type key
|
||||
data = [i for i in protocolresult.data[key].values()][0][0]
|
||||
# set the output to None
|
||||
data.outputs['forward_and_reverse_energies'] = None
|
||||
|
||||
# now fetch the analysis results and expect a warning
|
||||
wmsg = ("were found in the forward and reverse dictionaries "
|
||||
f"of the repeats of the {key}")
|
||||
with pytest.warns(UserWarning, match=wmsg):
|
||||
protocolresult.get_forward_and_reverse_energy_analysis()
|
||||
|
||||
@pytest.mark.parametrize('key', ['solvent', 'vacuum'])
|
||||
def test_get_overlap_matrices(self, key, protocolresult):
|
||||
ovp = protocolresult.get_overlap_matrices()
|
||||
|
||||
assert isinstance(ovp, dict)
|
||||
assert isinstance(ovp[key], list)
|
||||
assert len(ovp[key]) == 3
|
||||
|
||||
ovp1 = ovp[key][0]
|
||||
assert isinstance(ovp1['matrix'], np.ndarray)
|
||||
assert ovp1['matrix'].shape == (14, 14)
|
||||
|
||||
@pytest.mark.parametrize('key', ['solvent', 'vacuum'])
|
||||
def test_get_replica_transition_statistics(self, key, protocolresult):
|
||||
rpx = protocolresult.get_replica_transition_statistics()
|
||||
|
||||
assert isinstance(rpx, dict)
|
||||
assert isinstance(rpx[key], list)
|
||||
assert len(rpx[key]) == 3
|
||||
rpx1 = rpx[key][0]
|
||||
assert 'eigenvalues' in rpx1
|
||||
assert 'matrix' in rpx1
|
||||
assert rpx1['eigenvalues'].shape == (14,)
|
||||
assert rpx1['matrix'].shape == (14, 14)
|
||||
|
||||
@pytest.mark.parametrize('key', ['solvent', 'vacuum'])
|
||||
def test_equilibration_iterations(self, key, protocolresult):
|
||||
eq = protocolresult.equilibration_iterations()
|
||||
|
||||
assert isinstance(eq, dict)
|
||||
assert isinstance(eq[key], list)
|
||||
assert len(eq[key]) == 3
|
||||
assert all(isinstance(v, float) for v in eq[key])
|
||||
|
||||
@pytest.mark.parametrize('key', ['solvent', 'vacuum'])
|
||||
def test_production_iterations(self, key, protocolresult):
|
||||
prod = protocolresult.production_iterations()
|
||||
|
||||
assert isinstance(prod, dict)
|
||||
assert isinstance(prod[key], list)
|
||||
assert len(prod[key]) == 3
|
||||
assert all(isinstance(v, float) for v in prod[key])
|
||||
|
||||
def test_filenotfound_replica_states(self, protocolresult):
|
||||
errmsg = "File could not be found"
|
||||
|
||||
with pytest.raises(ValueError, match=errmsg):
|
||||
protocolresult.get_replica_states()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('positions_write_frequency,velocities_write_frequency',
|
||||
[[100 * offunit.picosecond, None],
|
||||
[None, None],
|
||||
[None, 100 * offunit.picosecond]])
|
||||
def test_dry_run_vacuum_write_frequency(benzene_modifications,
|
||||
positions_write_frequency,
|
||||
velocities_write_frequency,
|
||||
tmpdir):
|
||||
s = openmm_afe.AbsoluteSolvationProtocol.default_settings()
|
||||
s.protocol_repeats = 1
|
||||
s.solvent_output_settings.output_indices = "resname UNK"
|
||||
s.solvent_output_settings.positions_write_frequency = positions_write_frequency
|
||||
s.solvent_output_settings.velocities_write_frequency = velocities_write_frequency
|
||||
s.vacuum_output_settings.positions_write_frequency = positions_write_frequency
|
||||
s.vacuum_output_settings.velocities_write_frequency = velocities_write_frequency
|
||||
|
||||
protocol = openmm_afe.AbsoluteSolvationProtocol(
|
||||
settings=s,
|
||||
)
|
||||
|
||||
stateA = ChemicalSystem({
|
||||
'benzene': benzene_modifications['benzene'],
|
||||
'solvent': SolventComponent()
|
||||
})
|
||||
|
||||
stateB = ChemicalSystem({
|
||||
'solvent': SolventComponent(),
|
||||
})
|
||||
|
||||
# Create DAG from protocol, get the vacuum and solvent units
|
||||
# and eventually dry run the first solvent unit
|
||||
dag = protocol.create(
|
||||
stateA=stateA,
|
||||
stateB=stateB,
|
||||
mapping=None,
|
||||
)
|
||||
prot_units = list(dag.protocol_units)
|
||||
|
||||
assert len(prot_units) == 2
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
for u in prot_units:
|
||||
sampler = u.run(dry=True)['debug']['sampler']
|
||||
reporter = sampler._reporter
|
||||
if positions_write_frequency:
|
||||
assert reporter.position_interval == positions_write_frequency.m
|
||||
else:
|
||||
assert reporter.position_interval == 0
|
||||
if velocities_write_frequency:
|
||||
assert reporter.velocity_interval == velocities_write_frequency.m
|
||||
else:
|
||||
assert reporter.velocity_interval == 0
|
||||
@@ -1,115 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
import json
|
||||
import openfe
|
||||
from openfe.protocols import openmm_afe
|
||||
import gufe
|
||||
from gufe.tests.test_tokenization import GufeTokenizableTestsMixin
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def protocol():
|
||||
return openmm_afe.AbsoluteSolvationProtocol(
|
||||
openmm_afe.AbsoluteSolvationProtocol.default_settings()
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def protocol_units(protocol, benzene_system):
|
||||
pus = protocol.create(
|
||||
stateA=benzene_system,
|
||||
stateB=openfe.ChemicalSystem({'solvent': openfe.SolventComponent()}),
|
||||
mapping=None,
|
||||
)
|
||||
return list(pus.protocol_units)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def solvent_protocol_unit(protocol_units):
|
||||
for pu in protocol_units:
|
||||
if isinstance(pu, openmm_afe.AbsoluteSolvationSolventUnit):
|
||||
return pu
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vacuum_protocol_unit(protocol_units):
|
||||
for pu in protocol_units:
|
||||
if isinstance(pu, openmm_afe.AbsoluteSolvationVacuumUnit):
|
||||
return pu
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def protocol_result(afe_solv_transformation_json):
|
||||
d = json.loads(afe_solv_transformation_json,
|
||||
cls=gufe.tokenization.JSON_HANDLER.decoder)
|
||||
pr = openmm_afe.AbsoluteSolvationProtocolResult.from_dict(d['protocol_result'])
|
||||
return pr
|
||||
|
||||
|
||||
class TestAbsoluteSolvationProtocol(GufeTokenizableTestsMixin):
|
||||
cls = openmm_afe.AbsoluteSolvationProtocol
|
||||
key = None
|
||||
repr = "AbsoluteSolvationProtocol-"
|
||||
|
||||
@pytest.fixture()
|
||||
def instance(self, protocol):
|
||||
return protocol
|
||||
|
||||
def test_repr(self, instance):
|
||||
"""
|
||||
Overwrites the base `test_repr` call.
|
||||
"""
|
||||
assert isinstance(repr(instance), str)
|
||||
assert self.repr in repr(instance)
|
||||
|
||||
|
||||
class TestAbsoluteSolvationSolventUnit(GufeTokenizableTestsMixin):
|
||||
cls = openmm_afe.AbsoluteSolvationSolventUnit
|
||||
repr = "AbsoluteSolvationSolventUnit(Absolute Solvation, benzene solvent leg"
|
||||
key = None
|
||||
|
||||
@pytest.fixture()
|
||||
def instance(self, solvent_protocol_unit):
|
||||
return solvent_protocol_unit
|
||||
|
||||
def test_repr(self, instance):
|
||||
"""
|
||||
Overwrites the base `test_repr` call.
|
||||
"""
|
||||
assert isinstance(repr(instance), str)
|
||||
assert self.repr in repr(instance)
|
||||
|
||||
|
||||
class TestAbsoluteSolvationVacuumUnit(GufeTokenizableTestsMixin):
|
||||
cls = openmm_afe.AbsoluteSolvationVacuumUnit
|
||||
repr = "AbsoluteSolvationVacuumUnit(Absolute Solvation, benzene vacuum leg"
|
||||
key = None
|
||||
|
||||
@pytest.fixture()
|
||||
def instance(self, vacuum_protocol_unit):
|
||||
return vacuum_protocol_unit
|
||||
|
||||
def test_repr(self, instance):
|
||||
"""
|
||||
Overwrites the base `test_repr` call.
|
||||
"""
|
||||
assert isinstance(repr(instance), str)
|
||||
assert self.repr in repr(instance)
|
||||
|
||||
|
||||
class TestAbsoluteSolvationProtocolResult(GufeTokenizableTestsMixin):
|
||||
cls = openmm_afe.AbsoluteSolvationProtocolResult
|
||||
key = None
|
||||
repr = "AbsoluteSolvationProtocolResult-"
|
||||
|
||||
@pytest.fixture()
|
||||
def instance(self, protocol_result):
|
||||
return protocol_result
|
||||
|
||||
def test_repr(self, instance):
|
||||
"""
|
||||
Overwrites the base `test_repr` call.
|
||||
"""
|
||||
assert isinstance(repr(instance), str)
|
||||
assert self.repr in repr(instance)
|
||||
@@ -1,547 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
import sys
|
||||
import gufe
|
||||
from pydantic import ValidationError
|
||||
import pytest
|
||||
from unittest import mock
|
||||
from numpy.testing import assert_allclose
|
||||
from openff.units import unit
|
||||
from openmm import unit as omm_unit
|
||||
from openmm import NonbondedForce
|
||||
from openff.units.openmm import to_openmm, from_openmm
|
||||
from openmmtools.states import ThermodynamicState
|
||||
from openmm import MonteCarloBarostat
|
||||
from openfe.protocols.openmm_md.plain_md_methods import (
|
||||
PlainMDProtocol, PlainMDProtocolUnit, PlainMDProtocolResult,
|
||||
)
|
||||
from openfe.protocols.openmm_utils.charge_generation import (
|
||||
HAS_NAGL, HAS_OPENEYE, HAS_ESPALOMA_CHARGE
|
||||
)
|
||||
import json
|
||||
import openfe
|
||||
from openfe.protocols import openmm_md
|
||||
import pathlib
|
||||
import logging
|
||||
from openfe.tests.conftest import HAS_ESPALOMA
|
||||
|
||||
|
||||
def test_create_default_settings():
|
||||
settings = PlainMDProtocol.default_settings()
|
||||
|
||||
assert settings
|
||||
|
||||
|
||||
def test_create_default_protocol():
|
||||
# this is roughly how it should be created
|
||||
protocol = PlainMDProtocol(
|
||||
settings=PlainMDProtocol.default_settings(),
|
||||
)
|
||||
|
||||
assert protocol
|
||||
|
||||
def test_invalid_protocol_repeats():
|
||||
settings = PlainMDProtocol.default_settings()
|
||||
with pytest.raises(ValueError, match="must be a positive value"):
|
||||
settings.protocol_repeats = -1
|
||||
|
||||
def test_serialize_protocol():
|
||||
protocol = PlainMDProtocol(
|
||||
settings=PlainMDProtocol.default_settings(),
|
||||
)
|
||||
|
||||
ser = protocol.to_dict()
|
||||
|
||||
ret = PlainMDProtocol.from_dict(ser)
|
||||
|
||||
assert protocol == ret
|
||||
|
||||
|
||||
def test_create_independent_repeat_ids(benzene_system):
|
||||
# if we create two dags each with 3 repeats, they should give 6 repeat_ids
|
||||
# this allows multiple DAGs in flight for one Transformation that don't clash on gather
|
||||
settings = PlainMDProtocol.default_settings()
|
||||
# Default protocol is 1 repeat, change to 3 repeats
|
||||
settings.protocol_repeats = 3
|
||||
protocol = PlainMDProtocol(
|
||||
settings=settings,
|
||||
)
|
||||
dag1 = protocol.create(
|
||||
stateA=benzene_system,
|
||||
stateB=benzene_system,
|
||||
mapping=None,
|
||||
)
|
||||
dag2 = protocol.create(
|
||||
stateA=benzene_system,
|
||||
stateB=benzene_system,
|
||||
mapping=None,
|
||||
)
|
||||
|
||||
repeat_ids = set()
|
||||
u: PlainMDProtocolUnit
|
||||
for u in dag1.protocol_units:
|
||||
repeat_ids.add(u.inputs['repeat_id'])
|
||||
for u in dag2.protocol_units:
|
||||
repeat_ids.add(u.inputs['repeat_id'])
|
||||
|
||||
assert len(repeat_ids) == 6
|
||||
|
||||
|
||||
def test_dry_run_default_vacuum(benzene_vacuum_system, tmpdir):
|
||||
|
||||
vac_settings = PlainMDProtocol.default_settings()
|
||||
vac_settings.forcefield_settings.nonbonded_method = 'nocutoff'
|
||||
|
||||
protocol = PlainMDProtocol(
|
||||
settings=vac_settings,
|
||||
)
|
||||
|
||||
# create DAG from protocol and take first (and only) work unit from within
|
||||
dag = protocol.create(
|
||||
stateA=benzene_vacuum_system,
|
||||
stateB=benzene_vacuum_system,
|
||||
mapping=None,
|
||||
)
|
||||
dag_unit = list(dag.protocol_units)[0]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
sim = dag_unit.run(dry=True, verbose=True)['debug']['system']
|
||||
assert not ThermodynamicState(sim, temperature=to_openmm(
|
||||
protocol.settings.thermo_settings.temperature)).is_periodic
|
||||
assert ThermodynamicState(sim, temperature=to_openmm(
|
||||
protocol.settings.thermo_settings.temperature)).barostat is None
|
||||
|
||||
|
||||
def test_dry_run_logger_output(benzene_vacuum_system, tmpdir, caplog):
|
||||
|
||||
vac_settings = PlainMDProtocol.default_settings()
|
||||
vac_settings.forcefield_settings.nonbonded_method = 'nocutoff'
|
||||
vac_settings.simulation_settings.equilibration_length_nvt = 1 * unit.picosecond
|
||||
vac_settings.simulation_settings.equilibration_length = 1 * unit.picosecond
|
||||
vac_settings.simulation_settings.production_length = 1 * unit.picosecond
|
||||
|
||||
protocol = PlainMDProtocol(
|
||||
settings=vac_settings,
|
||||
)
|
||||
|
||||
# create DAG from protocol and take first (and only) work unit from within
|
||||
dag = protocol.create(
|
||||
stateA=benzene_vacuum_system,
|
||||
stateB=benzene_vacuum_system,
|
||||
mapping=None,
|
||||
)
|
||||
dag_unit = list(dag.protocol_units)[0]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
caplog.set_level(logging.INFO)
|
||||
dag_unit.run(dry=False, verbose=True)
|
||||
|
||||
messages = [r.message for r in caplog.records]
|
||||
assert "minimizing systems" in messages
|
||||
assert "Running NVT equilibration" in messages
|
||||
assert "Running NPT equilibration" in messages
|
||||
assert "running production phase" in messages
|
||||
|
||||
|
||||
def test_dry_run_ffcache_none_vacuum(benzene_vacuum_system, tmpdir):
|
||||
|
||||
vac_settings = PlainMDProtocol.default_settings()
|
||||
vac_settings.forcefield_settings.nonbonded_method = 'nocutoff'
|
||||
vac_settings.output_settings.forcefield_cache = None
|
||||
|
||||
protocol = PlainMDProtocol(
|
||||
settings=vac_settings,
|
||||
)
|
||||
assert protocol.settings.output_settings.forcefield_cache is None
|
||||
|
||||
# create DAG from protocol and take first (and only) work unit from within
|
||||
dag = protocol.create(
|
||||
stateA=benzene_vacuum_system,
|
||||
stateB=benzene_vacuum_system,
|
||||
mapping=None,
|
||||
)
|
||||
dag_unit = list(dag.protocol_units)[0]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
dag_unit.run(dry=True)['debug']['system']
|
||||
|
||||
|
||||
def test_dry_run_gaff_vacuum(benzene_vacuum_system, tmpdir):
|
||||
vac_settings = PlainMDProtocol.default_settings()
|
||||
vac_settings.forcefield_settings.nonbonded_method = 'nocutoff'
|
||||
vac_settings.forcefield_settings.small_molecule_forcefield = 'gaff-2.11'
|
||||
|
||||
protocol = PlainMDProtocol(
|
||||
settings=vac_settings,
|
||||
)
|
||||
|
||||
# create DAG from protocol and take first (and only) work unit from within
|
||||
dag = protocol.create(
|
||||
stateA=benzene_vacuum_system,
|
||||
stateB=benzene_vacuum_system,
|
||||
mapping=None,
|
||||
)
|
||||
unit = list(dag.protocol_units)[0]
|
||||
with tmpdir.as_cwd():
|
||||
system = unit.run(dry=True)["debug"]["system"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_ESPALOMA, reason='espaloma is not available')
|
||||
def test_dry_run_espaloma_vacuum(benzene_vacuum_system, tmpdir):
|
||||
vac_settings = PlainMDProtocol.default_settings()
|
||||
vac_settings.forcefield_settings.nonbonded_method = 'nocutoff'
|
||||
vac_settings.forcefield_settings.small_molecule_forcefield = 'espaloma-0.3.2'
|
||||
|
||||
protocol = PlainMDProtocol(
|
||||
settings=vac_settings,
|
||||
)
|
||||
|
||||
# create DAG from protocol and take first (and only) work unit from within
|
||||
dag = protocol.create(
|
||||
stateA=benzene_vacuum_system,
|
||||
stateB=benzene_vacuum_system,
|
||||
mapping=None,
|
||||
)
|
||||
unit = list(dag.protocol_units)[0]
|
||||
with tmpdir.as_cwd():
|
||||
system = unit.run(dry=True)["debug"]["system"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method, backend, ref_key', [
|
||||
('am1bcc', 'ambertools', 'ambertools'),
|
||||
pytest.param(
|
||||
'am1bcc', 'openeye', 'openeye',
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_OPENEYE, reason='needs oechem',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'nagl', 'rdkit', 'nagl',
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_NAGL or sys.platform.startswith('darwin'),
|
||||
reason='needs NAGL and/or on macos',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'espaloma', 'rdkit', 'espaloma',
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_ESPALOMA_CHARGE, reason='needs espaloma charge',
|
||||
),
|
||||
),
|
||||
])
|
||||
def test_dry_run_charge_backends(
|
||||
CN_molecule, tmpdir, method, backend, ref_key, am1bcc_ref_charges
|
||||
):
|
||||
vac_settings = PlainMDProtocol.default_settings()
|
||||
vac_settings.forcefield_settings.nonbonded_method = 'nocutoff'
|
||||
vac_settings.partial_charge_settings.partial_charge_method = method
|
||||
vac_settings.partial_charge_settings.off_toolkit_backend = backend
|
||||
vac_settings.partial_charge_settings.nagl_model = "openff-gnn-am1bcc-0.1.0-rc.1.pt"
|
||||
|
||||
protocol = PlainMDProtocol(settings=vac_settings)
|
||||
|
||||
csystem = openfe.ChemicalSystem({'ligand': CN_molecule})
|
||||
|
||||
dag = protocol.create(stateA=csystem, stateB=csystem, mapping=None)
|
||||
md_unit = list(dag.protocol_units)[0]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
system = md_unit.run(dry=True)['debug']['system']
|
||||
|
||||
nonbond = [f for f in system.getForces()
|
||||
if isinstance(f, NonbondedForce)][0]
|
||||
|
||||
charges = []
|
||||
for i in range(system.getNumParticles()):
|
||||
c, s, e = nonbond.getParticleParameters(i)
|
||||
charges.append(from_openmm(c))
|
||||
|
||||
charges = unit.Quantity.from_list(charges)
|
||||
|
||||
assert_allclose(am1bcc_ref_charges[ref_key], charges, rtol=1e-4)
|
||||
|
||||
|
||||
def test_dry_many_molecules_solvent(
|
||||
benzene_many_solv_system, tmpdir
|
||||
):
|
||||
"""
|
||||
A basic test flushing "will it work if you pass multiple molecules"
|
||||
"""
|
||||
settings = PlainMDProtocol.default_settings()
|
||||
|
||||
protocol = PlainMDProtocol(
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
# create DAG from protocol and take first (and only) work unit from within
|
||||
dag = protocol.create(
|
||||
stateA=benzene_many_solv_system,
|
||||
stateB=benzene_many_solv_system,
|
||||
mapping=None,
|
||||
)
|
||||
unit = list(dag.protocol_units)[0]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
system = unit.run(dry=True)['debug']['system']
|
||||
|
||||
|
||||
BENZ = """\
|
||||
benzene
|
||||
PyMOL2.5 3D 0
|
||||
|
||||
12 12 0 0 0 0 0 0 0 0999 V2000
|
||||
1.4045 -0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.7022 1.2164 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7023 1.2164 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4045 -0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7023 -1.2164 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.7023 -1.2164 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.5079 -0.0000 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.2540 2.1720 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2540 2.1720 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.5079 -0.0000 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2540 -2.1719 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.2540 -2.1720 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 2 0 0 0 0
|
||||
1 6 1 0 0 0 0
|
||||
1 7 1 0 0 0 0
|
||||
2 3 1 0 0 0 0
|
||||
2 8 1 0 0 0 0
|
||||
3 4 2 0 0 0 0
|
||||
3 9 1 0 0 0 0
|
||||
4 5 1 0 0 0 0
|
||||
4 10 1 0 0 0 0
|
||||
5 6 2 0 0 0 0
|
||||
5 11 1 0 0 0 0
|
||||
6 12 1 0 0 0 0
|
||||
M END
|
||||
$$$$
|
||||
"""
|
||||
|
||||
|
||||
PYRIDINE = """\
|
||||
pyridine
|
||||
PyMOL2.5 3D 0
|
||||
|
||||
11 11 0 0 0 0 0 0 0 0999 V2000
|
||||
1.4045 -0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7023 1.2164 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4045 -0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7023 -1.2164 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.7023 -1.2164 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.4940 -0.0325 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.2473 -2.1604 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2473 -2.1604 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.4945 -0.0000 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2753 2.1437 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.7525 1.3034 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 5 1 0 0 0 0
|
||||
1 6 1 0 0 0 0
|
||||
1 11 2 0 0 0 0
|
||||
2 3 2 0 0 0 0
|
||||
2 10 1 0 0 0 0
|
||||
3 4 1 0 0 0 0
|
||||
3 9 1 0 0 0 0
|
||||
4 5 2 0 0 0 0
|
||||
4 8 1 0 0 0 0
|
||||
5 7 1 0 0 0 0
|
||||
2 11 1 0 0 0 0
|
||||
M END
|
||||
$$$$
|
||||
"""
|
||||
|
||||
|
||||
def test_dry_run_ligand_tip4p(benzene_system, tmpdir):
|
||||
"""
|
||||
Test that we can create a system with virtual sites in the
|
||||
environment (waters)
|
||||
"""
|
||||
settings = PlainMDProtocol.default_settings()
|
||||
settings.forcefield_settings.forcefields = [
|
||||
"amber/ff14SB.xml", # ff14SB protein force field
|
||||
"amber/tip4pew_standard.xml", # FF we are testsing with the fun VS
|
||||
"amber/phosaa10.xml", # Handles THE TPO
|
||||
]
|
||||
settings.solvation_settings.solvent_padding = 1.0 * unit.nanometer
|
||||
settings.forcefield_settings.nonbonded_cutoff = 0.9 * unit.nanometer
|
||||
settings.solvation_settings.solvent_model = 'tip4pew'
|
||||
settings.integrator_settings.reassign_velocities = True
|
||||
|
||||
protocol = PlainMDProtocol(
|
||||
settings=settings,
|
||||
)
|
||||
dag = protocol.create(
|
||||
stateA=benzene_system,
|
||||
stateB=benzene_system,
|
||||
mapping=None,
|
||||
)
|
||||
dag_unit = list(dag.protocol_units)[0]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
system = dag_unit.run(dry=True)['debug']['system']
|
||||
assert system
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_dry_run_complex(benzene_complex_system, tmpdir):
|
||||
# this will be very time consuming
|
||||
settings = PlainMDProtocol.default_settings()
|
||||
|
||||
protocol = PlainMDProtocol(
|
||||
settings=settings,
|
||||
)
|
||||
dag = protocol.create(
|
||||
stateA=benzene_complex_system,
|
||||
stateB=benzene_complex_system,
|
||||
mapping=None,
|
||||
)
|
||||
dag_unit = list(dag.protocol_units)[0]
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
sim = dag_unit.run(dry=True)['debug']['system']
|
||||
assert ThermodynamicState(sim, temperature=
|
||||
to_openmm(protocol.settings.thermo_settings.temperature)).is_periodic
|
||||
assert isinstance(ThermodynamicState(sim, temperature=
|
||||
to_openmm(protocol.settings.thermo_settings.temperature)).barostat,
|
||||
MonteCarloBarostat)
|
||||
assert ThermodynamicState(sim, temperature=
|
||||
to_openmm(protocol.settings.thermo_settings.temperature)).pressure == 1 * omm_unit.bar
|
||||
|
||||
|
||||
def test_hightimestep(benzene_vacuum_system, tmpdir):
|
||||
settings = PlainMDProtocol.default_settings()
|
||||
settings.forcefield_settings.hydrogen_mass = 1.0
|
||||
settings.forcefield_settings.nonbonded_method = 'nocutoff'
|
||||
|
||||
p = PlainMDProtocol(
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
dag = p.create(
|
||||
stateA=benzene_vacuum_system,
|
||||
stateB=benzene_vacuum_system,
|
||||
mapping=None,
|
||||
)
|
||||
dag_unit = list(dag.protocol_units)[0]
|
||||
|
||||
errmsg = "too large for hydrogen mass"
|
||||
with tmpdir.as_cwd():
|
||||
with pytest.raises(ValueError, match=errmsg):
|
||||
dag_unit.run(dry=True)
|
||||
|
||||
|
||||
def test_vaccuum_PME_error(benzene_vacuum_system):
|
||||
|
||||
p = PlainMDProtocol(
|
||||
settings=PlainMDProtocol.default_settings(),
|
||||
)
|
||||
errmsg = "PME cannot be used for vacuum transform"
|
||||
with pytest.raises(ValueError, match=errmsg):
|
||||
_ = p.create(
|
||||
stateA=benzene_vacuum_system,
|
||||
stateB=benzene_vacuum_system,
|
||||
mapping=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def solvent_protocol_dag(benzene_system):
|
||||
settings = PlainMDProtocol.default_settings()
|
||||
settings.protocol_repeats = 3
|
||||
protocol = PlainMDProtocol(
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
return protocol.create(
|
||||
stateA=benzene_system, stateB=benzene_system,
|
||||
mapping=None,
|
||||
)
|
||||
|
||||
|
||||
def test_unit_tagging(solvent_protocol_dag, tmpdir):
|
||||
# test that executing the Units includes correct generation and repeat info
|
||||
dag_units = solvent_protocol_dag.protocol_units
|
||||
with mock.patch(
|
||||
'openfe.protocols.openmm_md.plain_md_methods.PlainMDProtocolUnit.run',
|
||||
return_value={
|
||||
'nc': 'simulation.xtc',
|
||||
'last_checkpoint': 'checkpoint.chk'
|
||||
}
|
||||
):
|
||||
results = []
|
||||
for u in dag_units:
|
||||
ret = u.execute(context=gufe.Context(tmpdir, tmpdir))
|
||||
results.append(ret)
|
||||
|
||||
repeats = set()
|
||||
for ret in results:
|
||||
assert isinstance(ret, gufe.ProtocolUnitResult)
|
||||
assert ret.outputs['generation'] == 0
|
||||
repeats.add(ret.outputs['repeat_id'])
|
||||
# repeats are random ints, so check we got 3 individual numbers
|
||||
assert len(repeats) == 3
|
||||
|
||||
|
||||
def test_gather(solvent_protocol_dag, tmpdir):
|
||||
# check .gather behaves as expected
|
||||
with mock.patch(
|
||||
'openfe.protocols.openmm_md.plain_md_methods.PlainMDProtocolUnit.run',
|
||||
return_value={
|
||||
'nc': 'simulation.xtc',
|
||||
'last_checkpoint': 'checkpoint.chk'
|
||||
}
|
||||
):
|
||||
dagres = gufe.protocols.execute_DAG(solvent_protocol_dag,
|
||||
shared_basedir=tmpdir,
|
||||
scratch_basedir=tmpdir,
|
||||
keep_shared=True)
|
||||
|
||||
settings = PlainMDProtocol.default_settings()
|
||||
settings.protocol_repeats = 3
|
||||
prot = PlainMDProtocol(
|
||||
settings=settings
|
||||
)
|
||||
|
||||
res = prot.gather([dagres])
|
||||
|
||||
assert isinstance(res, PlainMDProtocolResult)
|
||||
|
||||
|
||||
class TestProtocolResult:
|
||||
@pytest.fixture()
|
||||
def protocolresult(self, md_json):
|
||||
d = json.loads(md_json, cls=gufe.tokenization.JSON_HANDLER.decoder)
|
||||
|
||||
pr = openfe.ProtocolResult.from_dict(d['protocol_result'])
|
||||
|
||||
return pr
|
||||
|
||||
def test_reload_protocol_result(self, md_json):
|
||||
d = json.loads(md_json, cls=gufe.tokenization.JSON_HANDLER.decoder)
|
||||
|
||||
pr = openmm_md.plain_md_methods.PlainMDProtocolResult.from_dict(d['protocol_result'])
|
||||
|
||||
assert pr
|
||||
|
||||
def test_get_estimate(self, protocolresult):
|
||||
est = protocolresult.get_estimate()
|
||||
|
||||
assert est is None
|
||||
|
||||
|
||||
def test_get_uncertainty(self, protocolresult):
|
||||
est = protocolresult.get_uncertainty()
|
||||
|
||||
assert est is None
|
||||
|
||||
def test_get_traj_filename(self, protocolresult):
|
||||
traj = protocolresult.get_traj_filename()
|
||||
|
||||
assert isinstance(traj, list)
|
||||
assert isinstance(traj[0], pathlib.Path)
|
||||
|
||||
def test_get_pdb_filename(self, protocolresult):
|
||||
pdb = protocolresult.get_pdb_filename()
|
||||
|
||||
assert isinstance(pdb, list)
|
||||
assert isinstance(pdb[0], pathlib.Path)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,187 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose
|
||||
from gufe.protocols import execute_DAG
|
||||
import pytest
|
||||
from openff.units import unit
|
||||
import pathlib
|
||||
|
||||
import openfe
|
||||
from openfe.protocols import openmm_rfe
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.flaky(reruns=3) # pytest-rerunfailures; we can get bad minimization
|
||||
@pytest.mark.parametrize('platform', ['CPU', 'CUDA'])
|
||||
def test_openmm_run_engine(
|
||||
benzene_vacuum_system,
|
||||
platform,
|
||||
get_available_openmm_platforms,
|
||||
benzene_modifications,
|
||||
tmpdir
|
||||
):
|
||||
if platform not in get_available_openmm_platforms:
|
||||
pytest.skip(f"OpenMM Platform: {platform} not available")
|
||||
# this test actually runs MD
|
||||
# these settings are a small self to self sim, that has enough eq that
|
||||
# it doesn't occasionally crash
|
||||
s = openfe.protocols.openmm_rfe.RelativeHybridTopologyProtocol.default_settings()
|
||||
s.simulation_settings.equilibration_length = 0.1 * unit.picosecond
|
||||
s.simulation_settings.production_length = 0.1 * unit.picosecond
|
||||
s.simulation_settings.time_per_iteration = 20 * unit.femtosecond
|
||||
s.forcefield_settings.nonbonded_method = 'nocutoff'
|
||||
s.protocol_repeats = 1
|
||||
s.engine_settings.compute_platform = platform
|
||||
s.output_settings.checkpoint_interval = 20 * unit.femtosecond
|
||||
s.output_settings.positions_write_frequency = 20 * unit.femtosecond
|
||||
|
||||
p = openmm_rfe.RelativeHybridTopologyProtocol(s)
|
||||
|
||||
b = benzene_vacuum_system['ligand']
|
||||
|
||||
# make a copy with a different name
|
||||
rdmol = benzene_modifications['benzene'].to_rdkit()
|
||||
b_alt = openfe.SmallMoleculeComponent.from_rdkit(rdmol, name='alt')
|
||||
benzene_vacuum_alt_system = openfe.ChemicalSystem({
|
||||
'ligand': b_alt
|
||||
})
|
||||
|
||||
m = openfe.LigandAtomMapping(
|
||||
componentA=b,
|
||||
componentB=b_alt,
|
||||
componentA_to_componentB={i: i for i in range(12)}
|
||||
)
|
||||
dag = p.create(
|
||||
stateA=benzene_vacuum_system,
|
||||
stateB=benzene_vacuum_alt_system,
|
||||
mapping=[m]
|
||||
)
|
||||
|
||||
cwd = pathlib.Path(str(tmpdir))
|
||||
r = execute_DAG(dag, shared_basedir=cwd, scratch_basedir=cwd,
|
||||
keep_shared=True)
|
||||
|
||||
assert r.ok()
|
||||
for pur in r.protocol_unit_results:
|
||||
unit_shared = tmpdir / f"shared_{pur.source_key}_attempt_0"
|
||||
assert unit_shared.exists()
|
||||
assert pathlib.Path(unit_shared).is_dir()
|
||||
|
||||
# Check the checkpoint file exists
|
||||
checkpoint = pur.outputs['last_checkpoint']
|
||||
assert checkpoint == "checkpoint.chk"
|
||||
assert (unit_shared / checkpoint).exists()
|
||||
|
||||
# Check the nc simulation file exists
|
||||
# TODO: assert the number of frames
|
||||
nc = pur.outputs['nc']
|
||||
assert nc == unit_shared / "simulation.nc"
|
||||
assert nc.exists()
|
||||
|
||||
# Check structural analysis contents
|
||||
structural_analysis_file = unit_shared / "structural_analysis.npz"
|
||||
assert (structural_analysis_file).exists()
|
||||
assert pur.outputs['structural_analysis'] == structural_analysis_file
|
||||
|
||||
structural_data = np.load(pur.outputs['structural_analysis'])
|
||||
structural_keys = [
|
||||
'protein_RMSD', 'ligand_RMSD', 'ligand_COM_drift',
|
||||
'protein_2D_RMSD', 'time_ps'
|
||||
]
|
||||
for key in structural_keys:
|
||||
assert key in structural_data.keys()
|
||||
|
||||
# 6 frames being written to file
|
||||
assert_allclose(structural_data['time_ps'], [0.0, 0.02, 0.04, 0.06, 0.08, 0.1])
|
||||
assert structural_data['ligand_RMSD'].shape == (11, 6)
|
||||
assert structural_data['ligand_COM_drift'].shape == (11, 6)
|
||||
# No protein so should be empty
|
||||
assert structural_data['protein_RMSD'].size == 0
|
||||
assert structural_data['protein_2D_RMSD'].size == 0
|
||||
|
||||
# Test results methods that need files present
|
||||
results = p.gather([r])
|
||||
states = results.get_replica_states()
|
||||
assert len(states) == 1
|
||||
assert states[0].shape[1] == 11
|
||||
|
||||
|
||||
@pytest.mark.integration # takes ~7 minutes to run
|
||||
@pytest.mark.flaky(reruns=3)
|
||||
def test_run_eg5_sim(eg5_protein, eg5_ligands, eg5_cofactor, tmpdir):
|
||||
# this runs a very short eg5 complex leg
|
||||
# different to previous test:
|
||||
# - has a cofactor
|
||||
# - has an alchemical swap present
|
||||
# - runs in solvated protein
|
||||
# if this passes 99.9% chance of a good time
|
||||
s = openfe.protocols.openmm_rfe.RelativeHybridTopologyProtocol.default_settings()
|
||||
s.simulation_settings.equilibration_length = 0.1 * unit.picosecond
|
||||
s.simulation_settings.production_length = 0.1 * unit.picosecond
|
||||
s.simulation_settings.time_per_iteration = 20 * unit.femtosecond
|
||||
s.protocol_repeats = 1
|
||||
s.output_settings.checkpoint_interval = 20 * unit.femtosecond
|
||||
|
||||
p = openmm_rfe.RelativeHybridTopologyProtocol(s)
|
||||
|
||||
base_sys = {
|
||||
'protein': eg5_protein,
|
||||
'cofactor': eg5_cofactor,
|
||||
'solvent': openfe.SolventComponent(),
|
||||
}
|
||||
# this is just a simple (unmapped) *-H -> *-F switch
|
||||
l1, l2 = eg5_ligands[0], eg5_ligands[1]
|
||||
m = openfe.LigandAtomMapping(
|
||||
componentA=l1, componentB=l2,
|
||||
# a bit lucky, first 51 atoms map to each other, H->F swap is at 52
|
||||
componentA_to_componentB={i: i for i in range(51)}
|
||||
)
|
||||
|
||||
sys1 = openfe.ChemicalSystem(components={**base_sys, 'ligand': l1})
|
||||
sys2 = openfe.ChemicalSystem(components={**base_sys, 'ligand': l2})
|
||||
|
||||
dag = p.create(stateA=sys1, stateB=sys2,
|
||||
mapping=[m])
|
||||
|
||||
cwd = pathlib.Path(str(tmpdir))
|
||||
r = execute_DAG(dag, shared_basedir=cwd, scratch_basedir=cwd,
|
||||
keep_shared=True)
|
||||
|
||||
assert r.ok()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.flaky(reruns=3)
|
||||
def test_run_dodecahedron_sim(
|
||||
benzene_system, toluene_system, benzene_to_toluene_mapping, tmpdir
|
||||
):
|
||||
"""
|
||||
Test that we can run a ligand in solvent RFE with a non-cubic box
|
||||
"""
|
||||
settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings()
|
||||
settings.solvation_settings.solvent_padding = 1.5 * unit.nanometer
|
||||
settings.solvation_settings.box_shape = 'dodecahedron'
|
||||
settings.protocol_repeats = 1
|
||||
settings.simulation_settings.equilibration_length = 0.1 * unit.picosecond
|
||||
settings.simulation_settings.production_length = 0.1 * unit.picosecond
|
||||
settings.simulation_settings.time_per_iteration = 20 * unit.femtosecond
|
||||
settings.output_settings.checkpoint_interval = 20 * unit.femtosecond
|
||||
protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings=settings)
|
||||
|
||||
dag = protocol.create(
|
||||
stateA=benzene_system,
|
||||
stateB=toluene_system,
|
||||
mapping=benzene_to_toluene_mapping,
|
||||
)
|
||||
|
||||
cwd = pathlib.Path(str(tmpdir))
|
||||
|
||||
r = execute_DAG(
|
||||
dag,
|
||||
shared_basedir=cwd,
|
||||
scratch_basedir=cwd,
|
||||
keep_shared=True
|
||||
)
|
||||
|
||||
assert r.ok()
|
||||
@@ -1,97 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
|
||||
from openfe.protocols import openmm_rfe
|
||||
from gufe.tests.test_tokenization import GufeTokenizableTestsMixin
|
||||
from openff.units import unit
|
||||
import pytest
|
||||
|
||||
"""
|
||||
todo:
|
||||
- RelativeHybridTopologyProtocolResult
|
||||
- RelativeHybridTopologyProtocol
|
||||
- RelativeHybridTopologyProtocolUnit
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def rfe_protocol():
|
||||
return openmm_rfe.RelativeHybridTopologyProtocol(openmm_rfe.RelativeHybridTopologyProtocol.default_settings())
|
||||
|
||||
@pytest.fixture
|
||||
def rfe_protocol_other_units():
|
||||
"""Identical to rfe_protocol, but with `kcal / mol` as input unit instead of `kilocalorie_per_mole`."""
|
||||
new_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings()
|
||||
new_settings.simulation_settings.early_termination_target_error = 0.0 * unit.kilocalorie/unit.mol
|
||||
return openmm_rfe.RelativeHybridTopologyProtocol(new_settings)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def protocol_unit(rfe_protocol, benzene_system, toluene_system, benzene_to_toluene_mapping):
|
||||
pus = rfe_protocol.create(
|
||||
stateA=benzene_system, stateB=toluene_system,
|
||||
mapping=[benzene_to_toluene_mapping],
|
||||
)
|
||||
return list(pus.protocol_units)[0]
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
class TestRelativeHybridTopologyProtocolResult(GufeTokenizableTestsMixin):
|
||||
cls = openmm_rfe.RelativeHybridTopologyProtocolResult
|
||||
repr = ""
|
||||
key = ""
|
||||
|
||||
@pytest.fixture()
|
||||
def instance(self):
|
||||
pass
|
||||
|
||||
class TestRelativeHybridTopologyProtocolOtherUnits(GufeTokenizableTestsMixin):
|
||||
cls = openmm_rfe.RelativeHybridTopologyProtocol
|
||||
key = None
|
||||
repr = "<RelativeHybridTopologyProtocol-"
|
||||
|
||||
@pytest.fixture()
|
||||
def instance(self, rfe_protocol_other_units):
|
||||
return rfe_protocol_other_units
|
||||
|
||||
def test_repr(self, instance):
|
||||
"""
|
||||
Overwrites the base `test_repr` call.
|
||||
"""
|
||||
assert isinstance(repr(instance), str)
|
||||
assert self.repr in repr(instance)
|
||||
|
||||
class TestRelativeHybridTopologyProtocol(GufeTokenizableTestsMixin):
|
||||
cls = openmm_rfe.RelativeHybridTopologyProtocol
|
||||
key = None
|
||||
repr = "<RelativeHybridTopologyProtocol-"
|
||||
|
||||
@pytest.fixture()
|
||||
def instance(self, rfe_protocol):
|
||||
return rfe_protocol
|
||||
|
||||
def test_repr(self, instance):
|
||||
"""
|
||||
Overwrites the base `test_repr` call.
|
||||
"""
|
||||
assert isinstance(repr(instance), str)
|
||||
assert self.repr in repr(instance)
|
||||
|
||||
|
||||
class TestRelativeHybridTopologyProtocolUnit(GufeTokenizableTestsMixin):
|
||||
cls = openmm_rfe.RelativeHybridTopologyProtocolUnit
|
||||
repr = "RelativeHybridTopologyProtocolUnit(benzene to toluene repeat"
|
||||
key = None
|
||||
|
||||
@pytest.fixture()
|
||||
def instance(self, protocol_unit):
|
||||
return protocol_unit
|
||||
|
||||
def test_key_stable(self):
|
||||
pytest.skip()
|
||||
|
||||
def test_repr(self, instance):
|
||||
"""
|
||||
Overwrites the base `test_repr` call.
|
||||
"""
|
||||
assert isinstance(repr(instance), str)
|
||||
assert self.repr in repr(instance)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,983 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
import openfe
|
||||
import pooch
|
||||
import pytest
|
||||
from gufe.settings import OpenMMSystemGeneratorFFSettings, ThermoSettings
|
||||
from numpy.testing import assert_allclose, assert_equal
|
||||
from openfe.protocols.openmm_rfe.equil_rfe_settings import (
|
||||
IntegratorSettings, OpenMMSolvationSettings)
|
||||
from openfe.protocols.openmm_utils import (charge_generation,
|
||||
multistate_analysis, omm_settings,
|
||||
settings_validation,
|
||||
system_creation, system_validation)
|
||||
from openfe.protocols.openmm_utils.charge_generation import (HAS_ESPALOMA_CHARGE,
|
||||
HAS_NAGL,
|
||||
HAS_OPENEYE)
|
||||
from openff.toolkit import Molecule as OFFMol
|
||||
from openff.toolkit.utils.toolkit_registry import ToolkitRegistry
|
||||
from openff.toolkit.utils.toolkits import RDKitToolkitWrapper
|
||||
from openff.units import unit
|
||||
from openff.units.openmm import ensure_quantity, from_openmm
|
||||
from openmm import MonteCarloBarostat, NonbondedForce, app
|
||||
from openmm import unit as ommunit
|
||||
from openmmtools import multistate
|
||||
from pymbar.utils import ParameterError
|
||||
|
||||
from openfe.tests.conftest import HAS_INTERNET
|
||||
|
||||
|
||||
@pytest.mark.parametrize('padding, number_solv, box_vectors, box_size', [
|
||||
[1.2 * unit.nanometer, 20, 20 * np.identity(3) * unit.angstrom,
|
||||
[2, 2, 2] * unit.angstrom],
|
||||
[1.2 * unit.nanometer, None, None, [2, 2, 2] * unit.angstrom],
|
||||
[1.2 * unit.nanometer, None, 20 * np.identity(3) * unit.angstrom, None],
|
||||
[1.2 * unit.nanometer, 20, None, None],
|
||||
])
|
||||
def test_validate_ommsolvation_settings_unique_settings(
|
||||
padding, number_solv, box_vectors, box_size
|
||||
):
|
||||
settings = OpenMMSolvationSettings(
|
||||
solvent_padding=padding,
|
||||
number_of_solvent_molecules=number_solv,
|
||||
box_vectors=box_vectors,
|
||||
box_size=box_size,
|
||||
)
|
||||
|
||||
errmsg = "Only one of solvent_padding, number_of_solvent_molecules,"
|
||||
with pytest.raises(ValueError, match=errmsg):
|
||||
settings_validation.validate_openmm_solvation_settings(settings)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('box_vectors, box_size', [
|
||||
[20 * np.identity(3) * unit.angstrom, None],
|
||||
[None, [2, 2, 2] * unit.angstrom],
|
||||
])
|
||||
def test_validate_ommsolvation_settings_shape_conflicts(
|
||||
box_vectors, box_size,
|
||||
):
|
||||
settings = OpenMMSolvationSettings(
|
||||
solvent_padding=None,
|
||||
box_vectors=box_vectors,
|
||||
box_size=box_size,
|
||||
box_shape='cube',
|
||||
)
|
||||
|
||||
errmsg = "box_shape cannot be defined alongside either box_size"
|
||||
with pytest.raises(ValueError, match=errmsg):
|
||||
settings_validation.validate_openmm_solvation_settings(settings)
|
||||
|
||||
|
||||
def test_validate_timestep():
|
||||
with pytest.raises(ValueError, match="too large for hydrogen mass"):
|
||||
settings_validation.validate_timestep(2.0, 4.0 * unit.femtoseconds)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('s,ts,mc,es', [
|
||||
[5 * unit.nanoseconds, 4 * unit.femtoseconds, 250, 1250000],
|
||||
[1 * unit.nanoseconds, 4 * unit.femtoseconds, 250, 250000],
|
||||
[1 * unit.picoseconds, 2 * unit.femtoseconds, 250, 500],
|
||||
])
|
||||
def test_get_simsteps(s, ts, mc, es):
|
||||
sim_steps = settings_validation.get_simsteps(s, ts, mc)
|
||||
|
||||
assert sim_steps == es
|
||||
|
||||
|
||||
def test_get_simsteps_indivisible_simtime():
|
||||
errmsg = "Simulation time not divisible by timestep"
|
||||
timelength = 1.003 * unit.picosecond
|
||||
with pytest.raises(ValueError, match=errmsg):
|
||||
settings_validation.get_simsteps(timelength, 2 * unit.femtoseconds, 100)
|
||||
|
||||
|
||||
def test_mc_indivisible():
|
||||
errmsg = "Simulation time 1.0 ps should contain"
|
||||
timelength = 1 * unit.picoseconds
|
||||
with pytest.raises(ValueError, match=errmsg):
|
||||
settings_validation.get_simsteps(
|
||||
timelength, 2 * unit.femtoseconds, 1000)
|
||||
|
||||
|
||||
def test_get_alchemical_components(benzene_modifications,
|
||||
T4_protein_component):
|
||||
|
||||
stateA = openfe.ChemicalSystem({'A': benzene_modifications['benzene'],
|
||||
'B': benzene_modifications['toluene'],
|
||||
'P': T4_protein_component,
|
||||
'S': openfe.SolventComponent(smiles='C')})
|
||||
stateB = openfe.ChemicalSystem({'A': benzene_modifications['benzene'],
|
||||
'B': benzene_modifications['benzonitrile'],
|
||||
'P': T4_protein_component,
|
||||
'S': openfe.SolventComponent()})
|
||||
|
||||
alchem_comps = system_validation.get_alchemical_components(stateA, stateB)
|
||||
|
||||
assert len(alchem_comps['stateA']) == 2
|
||||
assert benzene_modifications['toluene'] in alchem_comps['stateA']
|
||||
assert openfe.SolventComponent(smiles='C') in alchem_comps['stateA']
|
||||
assert len(alchem_comps['stateB']) == 2
|
||||
assert benzene_modifications['benzonitrile'] in alchem_comps['stateB']
|
||||
assert openfe.SolventComponent() in alchem_comps['stateB']
|
||||
|
||||
|
||||
def test_duplicate_chemical_components(benzene_modifications):
|
||||
stateA = openfe.ChemicalSystem({'A': benzene_modifications['toluene'],
|
||||
'B': benzene_modifications['toluene'], })
|
||||
stateB = openfe.ChemicalSystem({'A': benzene_modifications['toluene']})
|
||||
|
||||
errmsg = "state A components B:"
|
||||
|
||||
with pytest.raises(ValueError, match=errmsg):
|
||||
system_validation.get_alchemical_components(stateA, stateB)
|
||||
|
||||
|
||||
def test_validate_solvent_nocutoff(benzene_modifications):
|
||||
|
||||
state = openfe.ChemicalSystem({'A': benzene_modifications['toluene'],
|
||||
'S': openfe.SolventComponent()})
|
||||
|
||||
with pytest.raises(ValueError, match="nocutoff cannot be used"):
|
||||
system_validation.validate_solvent(state, 'nocutoff')
|
||||
|
||||
|
||||
def test_validate_solvent_multiple_solvent(benzene_modifications):
|
||||
|
||||
state = openfe.ChemicalSystem({'A': benzene_modifications['toluene'],
|
||||
'S': openfe.SolventComponent(),
|
||||
'S2': openfe.SolventComponent()})
|
||||
|
||||
with pytest.raises(ValueError, match="Multiple SolventComponent"):
|
||||
system_validation.validate_solvent(state, 'pme')
|
||||
|
||||
|
||||
def test_not_water_solvent(benzene_modifications):
|
||||
|
||||
state = openfe.ChemicalSystem({'A': benzene_modifications['toluene'],
|
||||
'S': openfe.SolventComponent(smiles='C')})
|
||||
|
||||
with pytest.raises(ValueError, match="Non water solvent"):
|
||||
system_validation.validate_solvent(state, 'pme')
|
||||
|
||||
|
||||
def test_multiple_proteins(T4_protein_component):
|
||||
|
||||
state = openfe.ChemicalSystem({'A': T4_protein_component,
|
||||
'B': T4_protein_component})
|
||||
|
||||
with pytest.raises(ValueError, match="Multiple ProteinComponent"):
|
||||
system_validation.validate_protein(state)
|
||||
|
||||
|
||||
def test_get_components_gas(benzene_modifications):
|
||||
|
||||
state = openfe.ChemicalSystem({'A': benzene_modifications['benzene'],
|
||||
'B': benzene_modifications['toluene'], })
|
||||
|
||||
s, p, mols = system_validation.get_components(state)
|
||||
|
||||
assert s is None
|
||||
assert p is None
|
||||
assert len(mols) == 2
|
||||
|
||||
|
||||
def test_components_solvent(benzene_modifications):
|
||||
|
||||
state = openfe.ChemicalSystem({'S': openfe.SolventComponent(),
|
||||
'A': benzene_modifications['benzene'],
|
||||
'B': benzene_modifications['toluene'], })
|
||||
|
||||
s, p, mols = system_validation.get_components(state)
|
||||
|
||||
assert s == openfe.SolventComponent()
|
||||
assert p is None
|
||||
assert len(mols) == 2
|
||||
|
||||
|
||||
def test_components_complex(T4_protein_component, benzene_modifications):
|
||||
|
||||
state = openfe.ChemicalSystem({'S': openfe.SolventComponent(),
|
||||
'A': benzene_modifications['benzene'],
|
||||
'B': benzene_modifications['toluene'],
|
||||
'P': T4_protein_component,})
|
||||
|
||||
s, p, mols = system_validation.get_components(state)
|
||||
|
||||
assert s == openfe.SolventComponent()
|
||||
assert p == T4_protein_component
|
||||
assert len(mols) == 2
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def get_settings():
|
||||
forcefield_settings = OpenMMSystemGeneratorFFSettings()
|
||||
integrator_settings = IntegratorSettings()
|
||||
thermo_settings = ThermoSettings(
|
||||
temperature=298.15 * unit.kelvin,
|
||||
pressure=1 * unit.bar,
|
||||
)
|
||||
|
||||
return forcefield_settings, integrator_settings, thermo_settings
|
||||
|
||||
|
||||
class TestFEAnalysis:
|
||||
|
||||
# Note: class scope _will_ cause this to segfault - the reporter has to close
|
||||
@pytest.fixture(scope='function')
|
||||
def reporter(self):
|
||||
with resources.as_file(resources.files('openfe.tests.data.openmm_rfe')) as d:
|
||||
ncfile = str(d / 'vacuum_nocoord.nc')
|
||||
|
||||
with resources.as_file(resources.files('openfe.tests.data.openmm_rfe')) as d:
|
||||
chkfile = str(d / 'vacuum_nocoord_checkpoint.nc')
|
||||
|
||||
r = multistate.MultiStateReporter(
|
||||
storage=ncfile, checkpoint_storage=chkfile
|
||||
)
|
||||
try:
|
||||
yield r
|
||||
finally:
|
||||
r.close()
|
||||
|
||||
@pytest.fixture()
|
||||
def analyzer(self, reporter):
|
||||
return multistate_analysis.MultistateEquilFEAnalysis(
|
||||
reporter, sampling_method='repex',
|
||||
result_units=unit.kilocalorie_per_mole,
|
||||
)
|
||||
|
||||
def test_free_energies(self, analyzer):
|
||||
ret_dict = analyzer.unit_results_dict
|
||||
assert len(ret_dict.items()) == 7
|
||||
assert pytest.approx(ret_dict['unit_estimate'].m) == -47.9606
|
||||
# more variation when using bootstrap errors so we need a loser tolerance
|
||||
assert pytest.approx(ret_dict['unit_estimate_error'].m, rel=1e4) == 0.0251
|
||||
# forward and reverse (since we do this ourselves)
|
||||
assert_allclose(
|
||||
ret_dict['forward_and_reverse_energies']['fractions'],
|
||||
np.array([0.08988764, 0.191011, 0.292135, 0.393258, 0.494382,
|
||||
0.595506, 0.696629, 0.797753, 0.898876, 1.0]),
|
||||
rtol=1e-04,
|
||||
)
|
||||
assert_allclose(
|
||||
ret_dict['forward_and_reverse_energies']['forward_DGs'].m,
|
||||
np.array([-48.057326, -48.038367, -48.033994, -48.0228, -48.028532,
|
||||
-48.025258, -48.006349, -47.986304, -47.972138, -47.960623]),
|
||||
rtol=1e-04,
|
||||
)
|
||||
# results generated using pymbar3 with 1000 bootstrap iterations
|
||||
assert_allclose(
|
||||
ret_dict['forward_and_reverse_energies']['forward_dDGs'].m,
|
||||
np.array([0.077645, 0.054695, 0.044680, 0.03947, 0.034822,
|
||||
0.033443, 0.030793, 0.028777, 0.026683, 0.026199]),
|
||||
rtol=5e-01,
|
||||
)
|
||||
assert_allclose(
|
||||
ret_dict['forward_and_reverse_energies']['reverse_DGs'].m,
|
||||
np.array([-47.823839, -47.833107, -47.845866, -47.858173, -47.883887,
|
||||
-47.915963, -47.93319, -47.939125, -47.949016, -47.960623]),
|
||||
rtol=1e-04,
|
||||
)
|
||||
# results generated using pymbar3 with 1000 bootstrap iterations
|
||||
assert_allclose(
|
||||
ret_dict['forward_and_reverse_energies']['reverse_dDGs'].m,
|
||||
np.array([0.088335, 0.059483, 0.046254, 0.041504, 0.03877,
|
||||
0.035495, 0.031981, 0.029707, 0.027095, 0.026296]),
|
||||
rtol=5e-01,
|
||||
)
|
||||
|
||||
def test_plots(self, analyzer, tmpdir):
|
||||
with tmpdir.as_cwd():
|
||||
analyzer.plot(filepath=Path('.'), filename_prefix='')
|
||||
assert Path('forward_reverse_convergence.png').is_file()
|
||||
assert Path('mbar_overlap_matrix.png').is_file()
|
||||
assert Path('replica_exchange_matrix.png').is_file()
|
||||
assert Path('replica_state_timeseries.png').is_file()
|
||||
|
||||
def test_plot_convergence_bad_units(self, analyzer):
|
||||
|
||||
with pytest.raises(ValueError, match='Unknown plotting units'):
|
||||
openfe.analysis.plotting.plot_convergence(
|
||||
analyzer.forward_and_reverse_free_energies,
|
||||
unit.nanometer,
|
||||
)
|
||||
|
||||
def test_analyze_unknown_method_warning_and_error(self, reporter):
|
||||
|
||||
with pytest.warns(UserWarning, match='Unknown sampling method'):
|
||||
ana = multistate_analysis.MultistateEquilFEAnalysis(
|
||||
reporter, sampling_method='replex',
|
||||
result_units=unit.kilocalorie_per_mole,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Exchange matrix"):
|
||||
ana.replica_exchange_statistics
|
||||
|
||||
|
||||
class TestSystemCreation:
|
||||
def test_system_generator_nosolv_nocache(self, get_settings):
|
||||
ffsets, intsets, thermosets = get_settings
|
||||
generator = system_creation.get_system_generator(
|
||||
ffsets, thermosets, intsets, None, False
|
||||
)
|
||||
assert generator.barostat is None
|
||||
assert generator.template_generator._cache is None
|
||||
assert not generator.postprocess_system
|
||||
|
||||
forcefield_kwargs = {
|
||||
'constraints': app.HBonds,
|
||||
'rigidWater': True,
|
||||
'removeCMMotion': False,
|
||||
'hydrogenMass': 3.0 * ommunit.amu
|
||||
}
|
||||
assert generator.forcefield_kwargs == forcefield_kwargs
|
||||
periodic_kwargs = {
|
||||
'nonbondedMethod': app.PME,
|
||||
'nonbondedCutoff': 1.0 * ommunit.nanometer
|
||||
}
|
||||
nonperiodic_kwargs = {'nonbondedMethod': app.NoCutoff,}
|
||||
assert generator.nonperiodic_forcefield_kwargs == nonperiodic_kwargs
|
||||
assert generator.periodic_forcefield_kwargs == periodic_kwargs
|
||||
|
||||
def test_system_generator_solv_cache(self, get_settings):
|
||||
ffsets, intsets, thermosets = get_settings
|
||||
|
||||
thermosets.temperature = 320 * unit.kelvin
|
||||
thermosets.pressure = 1.25 * unit.bar
|
||||
intsets.barostat_frequency = 200 * unit.timestep
|
||||
generator = system_creation.get_system_generator(
|
||||
ffsets, thermosets, intsets, Path('./db.json'), True
|
||||
)
|
||||
|
||||
# Check barostat conditions
|
||||
assert isinstance(generator.barostat, MonteCarloBarostat)
|
||||
|
||||
pressure = ensure_quantity(
|
||||
generator.barostat.getDefaultPressure(), 'openff',
|
||||
)
|
||||
temperature = ensure_quantity(
|
||||
generator.barostat.getDefaultTemperature(), 'openff',
|
||||
)
|
||||
assert pressure.m == pytest.approx(1.25)
|
||||
assert pressure.units == unit.bar
|
||||
assert temperature.m == pytest.approx(320)
|
||||
assert temperature.units == unit.kelvin
|
||||
assert generator.barostat.getFrequency() == 200
|
||||
|
||||
# Check cache file
|
||||
assert generator.template_generator._cache == 'db.json'
|
||||
|
||||
def test_get_omm_modeller_complex(self, T4_protein_component,
|
||||
benzene_modifications,
|
||||
get_settings):
|
||||
ffsets, intsets, thermosets = get_settings
|
||||
generator = system_creation.get_system_generator(
|
||||
ffsets, thermosets, intsets, None, True
|
||||
)
|
||||
|
||||
smc = benzene_modifications['toluene']
|
||||
mol = smc.to_openff()
|
||||
generator.create_system(mol.to_topology().to_openmm(),
|
||||
molecules=[mol])
|
||||
|
||||
model, comp_resids = system_creation.get_omm_modeller(
|
||||
T4_protein_component, openfe.SolventComponent(),
|
||||
{smc: mol},
|
||||
generator.forcefield,
|
||||
OpenMMSolvationSettings())
|
||||
|
||||
resids = [r for r in model.topology.residues()]
|
||||
assert resids[163].name == 'NME'
|
||||
assert resids[164].name == 'UNK'
|
||||
assert resids[165].name == 'HOH'
|
||||
assert_equal(comp_resids[T4_protein_component], np.linspace(0, 163, 164))
|
||||
assert_equal(comp_resids[smc], np.array([164]))
|
||||
assert_equal(comp_resids[openfe.SolventComponent()],
|
||||
np.linspace(165, len(resids)-1, len(resids)-165))
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def ligand_mol_and_generator(self, get_settings):
|
||||
# Create offmol
|
||||
offmol = OFFMol.from_smiles('[O-]C=O')
|
||||
offmol.generate_conformers()
|
||||
offmol.assign_partial_charges(partial_charge_method='am1bcc')
|
||||
smc = openfe.SmallMoleculeComponent.from_openff(offmol)
|
||||
|
||||
ffsets, intsets, thermosets = get_settings
|
||||
generator = system_creation.get_system_generator(
|
||||
ffsets, thermosets, intsets, None, True
|
||||
)
|
||||
|
||||
# Register offmol in generator
|
||||
generator.create_system(offmol.to_topology().to_openmm(),
|
||||
molecules=[offmol])
|
||||
|
||||
return (offmol, smc, generator)
|
||||
|
||||
def test_get_omm_modeller_ligand_no_neutralize(
|
||||
self, ligand_mol_and_generator
|
||||
):
|
||||
|
||||
offmol, smc, generator = ligand_mol_and_generator
|
||||
|
||||
model, comp_resids = system_creation.get_omm_modeller(
|
||||
None,
|
||||
openfe.SolventComponent(neutralize=False),
|
||||
{smc: offmol},
|
||||
generator.forcefield,
|
||||
OpenMMSolvationSettings(),
|
||||
)
|
||||
|
||||
system = generator.create_system(
|
||||
model.topology,
|
||||
molecules=[offmol]
|
||||
)
|
||||
|
||||
# Now let's check the total charge
|
||||
nonbonded = [f for f in system.getForces()
|
||||
if isinstance(f, NonbondedForce)][0]
|
||||
|
||||
charge = 0 * ommunit.elementary_charge
|
||||
|
||||
for i in range(system.getNumParticles()):
|
||||
c, s, e = nonbonded.getParticleParameters(i)
|
||||
charge += c
|
||||
|
||||
charge = ensure_quantity(charge, 'openff')
|
||||
|
||||
assert pytest.approx(charge.m) == -1.0
|
||||
|
||||
@pytest.mark.parametrize('n_expected, neutralize, shape',
|
||||
[[400, False, 'cube'], [399, True, 'dodecahedron'],
|
||||
[400, False, 'octahedron']]
|
||||
)
|
||||
def test_omm_modeller_ligand_n_solv(
|
||||
self, ligand_mol_and_generator, n_expected, neutralize, shape
|
||||
):
|
||||
offmol, smc, generator = ligand_mol_and_generator
|
||||
|
||||
solv_settings = OpenMMSolvationSettings(
|
||||
solvent_padding=None,
|
||||
number_of_solvent_molecules=400,
|
||||
box_vectors=None,
|
||||
box_size=None,
|
||||
box_shape=shape
|
||||
)
|
||||
|
||||
model, comp_resids = system_creation.get_omm_modeller(
|
||||
None,
|
||||
openfe.SolventComponent(
|
||||
neutralize=neutralize,
|
||||
ion_concentration = 0 * unit.molar
|
||||
),
|
||||
{smc: offmol},
|
||||
generator.forcefield,
|
||||
solv_settings,
|
||||
)
|
||||
|
||||
waters = [r for r in model.topology.residues() if r.name == 'HOH']
|
||||
assert len(waters) == n_expected
|
||||
|
||||
def test_omm_modeller_box_size(self, ligand_mol_and_generator):
|
||||
offmol, smc, generator = ligand_mol_and_generator
|
||||
|
||||
solv_settings = OpenMMSolvationSettings(
|
||||
solvent_padding=None,
|
||||
number_of_solvent_molecules=None,
|
||||
box_vectors=None,
|
||||
box_size=[2, 2, 2]*unit.nanometer,
|
||||
box_shape=None
|
||||
)
|
||||
|
||||
model, comp_resids = system_creation.get_omm_modeller(
|
||||
None,
|
||||
openfe.SolventComponent(),
|
||||
{smc: offmol},
|
||||
generator.forcefield,
|
||||
solv_settings
|
||||
)
|
||||
|
||||
vectors = model.topology.getPeriodicBoxVectors()
|
||||
|
||||
assert_allclose(
|
||||
from_openmm(vectors),
|
||||
[[2, 0, 0], [0, 2, 0], [0, 0, 2]] * unit.nanometer
|
||||
)
|
||||
|
||||
def test_omm_modeller_box_vectors(self, ligand_mol_and_generator):
|
||||
offmol, smc, generator = ligand_mol_and_generator
|
||||
|
||||
solv_settings = OpenMMSolvationSettings(
|
||||
solvent_padding=None,
|
||||
number_of_solvent_molecules=None,
|
||||
box_vectors=[
|
||||
[2, 0, 0], [0, 2, 0], [0, 0, 5]
|
||||
] * unit.nanometer,
|
||||
box_size=None,
|
||||
box_shape=None,
|
||||
)
|
||||
|
||||
model, comp_resids = system_creation.get_omm_modeller(
|
||||
None,
|
||||
openfe.SolventComponent(),
|
||||
{smc: offmol},
|
||||
generator.forcefield,
|
||||
solv_settings
|
||||
)
|
||||
|
||||
vectors = model.topology.getPeriodicBoxVectors()
|
||||
|
||||
assert_allclose(
|
||||
from_openmm(vectors),
|
||||
[[2, 0, 0], [0, 2, 0], [0, 0, 5]] * unit.nanometer
|
||||
)
|
||||
|
||||
def test_convert_steps_per_iteration():
|
||||
sim = omm_settings.MultiStateSimulationSettings(
|
||||
equilibration_length='10 ps',
|
||||
production_length='10 ps',
|
||||
time_per_iteration='1.0 ps',
|
||||
)
|
||||
inty = omm_settings.IntegratorSettings(
|
||||
timestep='4 fs'
|
||||
)
|
||||
|
||||
spi = settings_validation.convert_steps_per_iteration(sim, inty)
|
||||
|
||||
assert spi == 250
|
||||
|
||||
|
||||
def test_convert_steps_per_iteration_failure():
|
||||
sim = omm_settings.MultiStateSimulationSettings(
|
||||
equilibration_length='10 ps',
|
||||
production_length='10 ps',
|
||||
time_per_iteration='1.0 ps',
|
||||
)
|
||||
inty = omm_settings.IntegratorSettings(
|
||||
timestep='3 fs'
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="does not evenly divide"):
|
||||
settings_validation.convert_steps_per_iteration(sim, inty)
|
||||
|
||||
|
||||
def test_convert_real_time_analysis_iterations():
|
||||
sim = omm_settings.MultiStateSimulationSettings(
|
||||
equilibration_length='10 ps',
|
||||
production_length='10 ps',
|
||||
time_per_iteration='1.0 ps',
|
||||
real_time_analysis_interval='250 ps',
|
||||
real_time_analysis_minimum_time='500 ps',
|
||||
)
|
||||
|
||||
rta_its, rta_min_its = settings_validation.convert_real_time_analysis_iterations(sim)
|
||||
|
||||
assert rta_its == 250, 500
|
||||
|
||||
|
||||
def test_convert_real_time_analysis_iterations_interval_fail():
|
||||
# shouldn't like 250.5 ps / 1.0 ps
|
||||
sim = omm_settings.MultiStateSimulationSettings(
|
||||
equilibration_length='10 ps',
|
||||
production_length='10 ps',
|
||||
time_per_iteration='1.0 ps',
|
||||
real_time_analysis_interval='250.5 ps',
|
||||
real_time_analysis_minimum_time='500 ps',
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match='does not evenly divide'):
|
||||
settings_validation.convert_real_time_analysis_iterations(sim)
|
||||
|
||||
|
||||
def test_convert_real_time_analysis_iterations_min_interval_fail():
|
||||
# shouldn't like 500.5 ps / 1 ps
|
||||
sim = omm_settings.MultiStateSimulationSettings(
|
||||
equilibration_length='10 ps',
|
||||
production_length='10 ps',
|
||||
time_per_iteration='1.0 ps',
|
||||
real_time_analysis_interval='250 ps',
|
||||
real_time_analysis_minimum_time='500.5 ps',
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match='does not evenly divide'):
|
||||
settings_validation.convert_real_time_analysis_iterations(sim)
|
||||
|
||||
|
||||
def test_convert_real_time_analysis_iterations_None():
|
||||
sim = omm_settings.MultiStateSimulationSettings(
|
||||
equilibration_length='10 ps',
|
||||
production_length='10 ps',
|
||||
time_per_iteration='1.0 ps',
|
||||
real_time_analysis_interval=None,
|
||||
real_time_analysis_minimum_time='500 ps',
|
||||
)
|
||||
|
||||
rta_its, rta_min_its = settings_validation.convert_real_time_analysis_iterations(sim)
|
||||
|
||||
assert rta_its is None
|
||||
assert rta_min_its is None
|
||||
|
||||
|
||||
def test_convert_target_error_from_kcal_per_mole_to_kT():
|
||||
kT = settings_validation.convert_target_error_from_kcal_per_mole_to_kT(
|
||||
temperature=298.15 * unit.kelvin,
|
||||
target_error=0.12 * unit.kilocalorie_per_mole,
|
||||
)
|
||||
|
||||
assert kT == pytest.approx(0.20253681663365392)
|
||||
|
||||
|
||||
def test_convert_target_error_from_kcal_per_mole_to_kT_zero():
|
||||
# special case, 0 input gives 0 output
|
||||
kT = settings_validation.convert_target_error_from_kcal_per_mole_to_kT(
|
||||
temperature=298.15 * unit.kelvin,
|
||||
target_error=0.0 * unit.kilocalorie_per_mole,
|
||||
)
|
||||
|
||||
assert kT == 0.0
|
||||
|
||||
|
||||
class TestOFFPartialCharge:
|
||||
@pytest.fixture(scope='function')
|
||||
def uncharged_mol(self, CN_molecule):
|
||||
return CN_molecule.to_openff()
|
||||
|
||||
@pytest.mark.parametrize('overwrite', [True, False])
|
||||
def test_offmol_chg_gen_charged_overwrite(
|
||||
self, overwrite, uncharged_mol
|
||||
):
|
||||
chg = [
|
||||
1 for _ in range(len(uncharged_mol.atoms))
|
||||
] * unit.elementary_charge
|
||||
|
||||
uncharged_mol.partial_charges = copy.deepcopy(chg)
|
||||
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=overwrite,
|
||||
method='am1bcc',
|
||||
toolkit_backend='ambertools',
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None,
|
||||
)
|
||||
|
||||
assert np.allclose(uncharged_mol.partial_charges, chg) != overwrite
|
||||
|
||||
def test_unknown_method(self, uncharged_mol):
|
||||
with pytest.raises(ValueError, match="Unknown partial charge method"):
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method='foo',
|
||||
toolkit_backend='ambertools',
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('method, backend', [
|
||||
['am1bcc', 'rdkit'],
|
||||
['am1bccelf10', 'ambertools'],
|
||||
['nagl', 'bar'],
|
||||
['espaloma', 'openeye'],
|
||||
])
|
||||
def test_incompatible_backend_am1bcc(
|
||||
self, method, backend, uncharged_mol
|
||||
):
|
||||
with pytest.raises(ValueError, match='Selected toolkit_backend'):
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method=method,
|
||||
toolkit_backend=backend,
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None
|
||||
)
|
||||
|
||||
def test_no_conformers(self, uncharged_mol):
|
||||
uncharged_mol._conformers = None
|
||||
|
||||
with pytest.raises(ValueError, match='No conformers'):
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method='am1bcc',
|
||||
toolkit_backend='ambertools',
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None,
|
||||
)
|
||||
|
||||
def test_too_many_existing_conformers(self, uncharged_mol):
|
||||
uncharged_mol.generate_conformers(
|
||||
n_conformers=2,
|
||||
rms_cutoff=0.001 * unit.angstrom,
|
||||
toolkit_registry=RDKitToolkitWrapper(),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="too many conformers"):
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method='am1bcc',
|
||||
toolkit_backend='ambertools',
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None,
|
||||
)
|
||||
|
||||
def test_too_many_requested_conformers(self, uncharged_mol):
|
||||
|
||||
with pytest.raises(ValueError, match="5 conformers were requested"):
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method='am1bcc',
|
||||
toolkit_backend='ambertools',
|
||||
generate_n_conformers=5,
|
||||
nagl_model=None,
|
||||
)
|
||||
|
||||
def test_am1bcc_no_conformer(self, uncharged_mol):
|
||||
|
||||
uncharged_mol._conformers = None
|
||||
|
||||
with pytest.raises(ValueError, match='at least one conformer'):
|
||||
charge_generation.assign_offmol_am1bcc_charges(
|
||||
uncharged_mol,
|
||||
partial_charge_method='am1bcc',
|
||||
toolkit_registry=ToolkitRegistry([RDKitToolkitWrapper()])
|
||||
)
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_am1bcc_conformer_nochange(self, eg5_ligands):
|
||||
|
||||
lig = eg5_ligands[0].to_openff()
|
||||
|
||||
conf = copy.deepcopy(lig.conformers)
|
||||
|
||||
# Get charges without conf generation
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
lig,
|
||||
overwrite=False,
|
||||
method='am1bcc',
|
||||
toolkit_backend='ambertools',
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None,
|
||||
)
|
||||
|
||||
# check the conformation hasn't changed
|
||||
assert_allclose(conf, lig.conformers)
|
||||
|
||||
# copy the charges to check that the conf gen will change things
|
||||
charges = copy.deepcopy(lig.partial_charges)
|
||||
|
||||
# now with conformer generation
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
lig,
|
||||
overwrite=True,
|
||||
method='am1bcc',
|
||||
toolkit_backend='ambertools',
|
||||
generate_n_conformers=1,
|
||||
nagl_model=None
|
||||
)
|
||||
|
||||
# conformer shouldn't have changed
|
||||
assert_allclose(conf, lig.conformers)
|
||||
|
||||
# but the charges should have
|
||||
assert not np.allclose(charges, lig.partial_charges)
|
||||
|
||||
@pytest.mark.skipif(not HAS_NAGL, reason="NAGL is not available")
|
||||
def test_latest_production_nagl(self, uncharged_mol):
|
||||
"""We expect to find a NAGL model and be able to generate partial charges with it."""
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method="nagl",
|
||||
toolkit_backend="rdkit",
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None,
|
||||
)
|
||||
assert uncharged_mol.partial_charges.units == "elementary_charge"
|
||||
|
||||
@pytest.mark.skipif(not HAS_NAGL, reason="NAGL is not available")
|
||||
def test_no_production_nagl(self, uncharged_mol):
|
||||
"""Cleanly handle the case where a NAGL model isn't found."""
|
||||
with mock.patch("openfe.protocols.openmm_utils.charge_generation.get_models_by_type", return_value=[]):
|
||||
with pytest.raises(ValueError, match="No production am1bcc NAGL"):
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method="nagl",
|
||||
toolkit_backend="rdkit",
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None,
|
||||
)
|
||||
|
||||
# Note: skipping nagl tests on macos/darwin due to known issues
|
||||
# see: https://github.com/openforcefield/openff-nagl/issues/78
|
||||
@pytest.mark.parametrize('method, backend, ref_key, confs', [
|
||||
('am1bcc', 'ambertools', 'ambertools', None),
|
||||
pytest.param(
|
||||
'am1bcc', 'openeye', 'openeye', None,
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_OPENEYE, reason='needs oechem',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'am1bccelf10', 'openeye', 'openeye', 500,
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_OPENEYE, reason='needs oechem',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'nagl', 'rdkit', 'nagl', None,
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_NAGL or sys.platform.startswith('darwin'),
|
||||
reason='needs NAGL and/or on macos',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'nagl', 'ambertools', 'nagl', None,
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_NAGL or sys.platform.startswith('darwin')
|
||||
, reason='needs NAGL and/or on macos',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'nagl', 'openeye', 'nagl', None,
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_NAGL or not HAS_OPENEYE or sys.platform.startswith('darwin'),
|
||||
reason='needs NAGL and oechem and not on macos',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'espaloma', 'rdkit', 'espaloma', None,
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_ESPALOMA_CHARGE, reason='needs espaloma charge',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
'espaloma', 'ambertools', 'espaloma', None,
|
||||
marks=pytest.mark.skipif(
|
||||
not HAS_ESPALOMA_CHARGE, reason='needs espaloma charge',
|
||||
),
|
||||
),
|
||||
])
|
||||
def test_am1bcc_reference(
|
||||
self, uncharged_mol, method, backend, ref_key, confs,
|
||||
am1bcc_ref_charges,
|
||||
):
|
||||
"""
|
||||
Check partial charge generation using what would
|
||||
be intended default settings for a CN molecule
|
||||
"""
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method=method,
|
||||
toolkit_backend=backend,
|
||||
generate_n_conformers=None,
|
||||
nagl_model="openff-gnn-am1bcc-0.1.0-rc.1.pt",
|
||||
)
|
||||
|
||||
assert_allclose(
|
||||
am1bcc_ref_charges[ref_key],
|
||||
uncharged_mol.partial_charges,
|
||||
rtol=1e-4
|
||||
)
|
||||
|
||||
def test_nagl_import_error(self, monkeypatch, uncharged_mol):
|
||||
monkeypatch.setattr(
|
||||
sys.modules['openfe.protocols.openmm_utils.charge_generation'],
|
||||
'HAS_NAGL',
|
||||
False
|
||||
)
|
||||
|
||||
with pytest.raises(ImportError, match='NAGL toolkit is not available'):
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method='nagl',
|
||||
toolkit_backend='rdkit',
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None
|
||||
)
|
||||
|
||||
def test_espaloma_import_error(self, monkeypatch, uncharged_mol):
|
||||
monkeypatch.setattr(
|
||||
sys.modules['openfe.protocols.openmm_utils.charge_generation'],
|
||||
'HAS_ESPALOMA_CHARGE',
|
||||
False
|
||||
)
|
||||
|
||||
with pytest.raises(ImportError, match='Espaloma'):
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method='espaloma',
|
||||
toolkit_backend='rdkit',
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None,
|
||||
)
|
||||
|
||||
def test_openeye_import_error(self, monkeypatch, uncharged_mol):
|
||||
monkeypatch.setattr(
|
||||
sys.modules['openfe.protocols.openmm_utils.charge_generation'],
|
||||
'HAS_OPENEYE',
|
||||
False
|
||||
)
|
||||
|
||||
with pytest.raises(ImportError, match='OpenEye is not available'):
|
||||
charge_generation.assign_offmol_partial_charges(
|
||||
uncharged_mol,
|
||||
overwrite=False,
|
||||
method='am1bcc',
|
||||
toolkit_backend='openeye',
|
||||
generate_n_conformers=None,
|
||||
nagl_model=None,
|
||||
)
|
||||
|
||||
|
||||
POOCH_CACHE = pooch.os_cache('openfe')
|
||||
RFE_OUTPUT = pooch.create(
|
||||
path=POOCH_CACHE,
|
||||
base_url="doi:10.5281/zenodo.15375081",
|
||||
registry={
|
||||
"checkpoint.nc": "md5:3cfd70a4cbe463403d6ec7cca84fc31a",
|
||||
"db.json": "md5:33c8c1a0b629a52dcc291beff59fabc6",
|
||||
"hybrid_system.pdb": "md5:44a1e78294360037acf419b95be18fb3",
|
||||
"simulation.nc": "md5:bc4e842b47de17704d804ae345b91599",
|
||||
"simulation_real_time_analysis.yaml": "md5:68a7d81462c42353a91bbbe5e64fd418",
|
||||
},
|
||||
retry_if_failed=5,
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def simulation_nc():
|
||||
return RFE_OUTPUT.fetch("simulation.nc")
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.skipif(not os.path.exists(POOCH_CACHE) and not HAS_INTERNET,reason="Internet seems to be unavailable and test data is not cached locally.")
|
||||
def test_forward_backwards_failure(simulation_nc):
|
||||
rep = multistate.multistatereporter.MultiStateReporter(
|
||||
simulation_nc,
|
||||
open_mode='r'
|
||||
)
|
||||
ana = multistate_analysis.MultistateEquilFEAnalysis(
|
||||
rep,
|
||||
sampling_method='repex',
|
||||
result_units=unit.kilocalorie_per_mole,
|
||||
)
|
||||
|
||||
with mock.patch('openfe.protocols.openmm_utils.multistate_analysis.MultistateEquilFEAnalysis._get_free_energy',
|
||||
side_effect=ParameterError):
|
||||
ret = ana.get_forward_and_reverse_analysis()
|
||||
|
||||
assert ret is None
|
||||
@@ -1,62 +0,0 @@
|
||||
# This code is part of OpenFE and is licensed under the MIT license.
|
||||
# For details, see https://github.com/OpenFreeEnergy/openfe
|
||||
import pytest
|
||||
from openfe.setup.atom_mapping import PersesAtomMapper, LigandAtomMapping
|
||||
from openff.units import unit
|
||||
|
||||
pytest.importorskip('perses')
|
||||
pytest.importorskip('openeye')
|
||||
|
||||
|
||||
def test_simple(atom_mapping_basic_test_files):
|
||||
# basic sanity check on the LigandAtomMapper
|
||||
mol1 = atom_mapping_basic_test_files['methylcyclohexane']
|
||||
mol2 = atom_mapping_basic_test_files['toluene']
|
||||
|
||||
mapper = PersesAtomMapper()
|
||||
|
||||
mapping_gen = mapper.suggest_mappings(mol1, mol2)
|
||||
|
||||
mapping = next(mapping_gen)
|
||||
assert isinstance(mapping, LigandAtomMapping)
|
||||
# maps (CH3) off methyl and (6C + 5H) on ring
|
||||
assert len(mapping.componentA_to_componentB) == 4
|
||||
|
||||
|
||||
def test_generator_length(atom_mapping_basic_test_files):
|
||||
# check that we get one mapping back from Lomap LigandAtomMapper then the
|
||||
# generator stops correctly
|
||||
mol1 = atom_mapping_basic_test_files['methylcyclohexane']
|
||||
mol2 = atom_mapping_basic_test_files['toluene']
|
||||
|
||||
mapper = PersesAtomMapper()
|
||||
|
||||
mapping_gen = mapper.suggest_mappings(mol1, mol2)
|
||||
|
||||
_ = next(mapping_gen)
|
||||
with pytest.raises(StopIteration):
|
||||
next(mapping_gen)
|
||||
|
||||
|
||||
def test_empty_atommappings(mol_pair_to_shock_perses_mapper):
|
||||
mol1, mol2 = mol_pair_to_shock_perses_mapper
|
||||
mapper = PersesAtomMapper()
|
||||
|
||||
mapping_gen = mapper.suggest_mappings(mol1, mol2)
|
||||
|
||||
# The expected return is an empty mapping
|
||||
assert len(list(mapping_gen)) == 0
|
||||
|
||||
with pytest.raises(StopIteration):
|
||||
next(mapping_gen)
|
||||
|
||||
|
||||
def test_dict_round_trip():
|
||||
# use some none defaults
|
||||
mapper1 = PersesAtomMapper(
|
||||
allow_ring_breaking=False,
|
||||
preserve_chirality=False,
|
||||
coordinate_tolerance=0.01 * unit.nanometer
|
||||
)
|
||||
mapper2 = PersesAtomMapper.from_dict(mapper1.to_dict())
|
||||
assert mapper2.to_dict() == mapper1.to_dict()
|
||||
@@ -1,37 +0,0 @@
|
||||
import os
|
||||
import importlib
|
||||
import pytest
|
||||
|
||||
import openfe
|
||||
|
||||
|
||||
pytest.importorskip('duecredit')
|
||||
|
||||
|
||||
@pytest.mark.skipif((os.environ.get('DUECREDIT_ENABLE', 'no').lower()
|
||||
in ('no', '0', 'false')),
|
||||
reason="duecredit is disabled")
|
||||
class TestDuecredit:
|
||||
|
||||
@pytest.mark.parametrize('module, dois', [
|
||||
['openfe.protocols.openmm_afe.equil_solvation_afe_method',
|
||||
['10.5281/zenodo.596504', '10.48550/arxiv.2302.06758',
|
||||
'10.5281/zenodo.596622', '10.1371/journal.pcbi.1005659']],
|
||||
['openfe.protocols.openmm_rfe.equil_rfe_methods',
|
||||
['10.5281/zenodo.1297683', '10.5281/zenodo.596622',
|
||||
'10.1371/journal.pcbi.1005659']],
|
||||
['openfe.protocols.openmm_utils.multistate_analysis',
|
||||
["10.5281/zenodo.596622", "10.1063/1.2978177",
|
||||
"10.1021/ct0502864", "10.1021/acs.jctc.5b00784",
|
||||
"10.5281/zenodo.596220"]],
|
||||
['openfe.protocols.openmm_septop.equil_septop_method',
|
||||
['10.1021/acs.jctc.3c00282',
|
||||
'10.5281/zenodo.596622', '10.1371/journal.pcbi.1005659']],
|
||||
])
|
||||
def test_duecredit_protocol_collection(self, module, dois):
|
||||
importlib.import_module(module)
|
||||
for doi in dois:
|
||||
assert openfe.due.due.citations[(module, doi)].cites_module
|
||||
|
||||
def test_duecredit_active(self):
|
||||
assert openfe.due.due.active
|
||||
@@ -1,18 +0,0 @@
|
||||
import logging
|
||||
|
||||
class MsgIncludesStringFilter:
|
||||
"""Logging filter to silence specfic log messages.
|
||||
|
||||
See https://docs.python.org/3/library/logging.html#filter-objects
|
||||
|
||||
Parameters
|
||||
----------
|
||||
string : str
|
||||
if an exact for this is included in the log message, the log record
|
||||
is suppressed
|
||||
"""
|
||||
def __init__(self, string):
|
||||
self.string = string
|
||||
|
||||
def filter(self, record):
|
||||
return not self.string in record.msg
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user