Merge branch 'main' into add/contributing

This commit is contained in:
Alyssa Travitz
2026-05-28 08:14:26 -07:00
committed by GitHub
465 changed files with 37604 additions and 19817 deletions

20
.git-blame-ignore-revs Normal file
View 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

View File

@@ -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)

View File

@@ -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/>.

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -1,4 +1,4 @@
name: Test Docker Image Building
name: "cron: docker image daily tests"
on:
push:

View 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

View File

@@ -1,4 +1,4 @@
name: "Daily package install tests."
name: "cron: package install daily tests"
on:
workflow_dispatch:
schedule:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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"

View File

@@ -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).

View File

@@ -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

View File

@@ -2,7 +2,7 @@
[![build](https://github.com/OpenFreeEnergy/openfe/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/OpenFreeEnergy/openfe/actions/workflows/ci.yaml)
[![coverage](https://codecov.io/gh/OpenFreeEnergy/openfe/branch/main/graph/badge.svg)](https://codecov.io/gh/OpenFreeEnergy/openfe)
[![documentation](https://readthedocs.org/projects/openfe/badge/?version=stable)](https://docs.openfree.energy/en/stable/?badge=stable)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8344248.svg)](https://doi.org/10.5281/zenodo.8344248)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8344248.svg)](https://doi.org/10.5281/zenodo.17258732)
# `openfe` - A Python package for executing alchemical free energy calculations.

View File

@@ -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()

View File

@@ -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()

View File

@@ -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:**

View File

@@ -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__)

View File

@@ -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) }}:
""")

View File

@@ -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

View File

@@ -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`.

View File

@@ -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
---------------------------

View File

@@ -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

View File

@@ -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.

View 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, 50585076
.. [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 529539 (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

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@@ -7,6 +7,7 @@ Details on the theory and behaviour of different Protocols are listed here.
.. toctree::
relativehybridtopology
absolutebinding
absolutesolvation
septop
plainmd

View File

@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -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.**

View File

@@ -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]_.

View File

@@ -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 proteinligand**
- | **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>`

View File

@@ -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.

View File

@@ -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
-----------------------------------------

View File

@@ -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

View File

@@ -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/>`_.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -16,7 +16,8 @@ Protocol API Specification
:toctree: generated/
PlainMDProtocol
PlainMDProtocolUnit
PlainMDSetupUnit
PlainMDSimulationUnit
PlainMDProtocolResult

View File

@@ -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.

View File

@@ -16,7 +16,9 @@ Protocol API specification
:toctree: generated/
RelativeHybridTopologyProtocol
RelativeHybridTopologyProtocolUnit
HybridTopologySetupUnit
HybridTopologyMultiStateSimulationUnit
HybridTopologyMultiStateAnalysisUnit
RelativeHybridTopologyProtocolResult
Protocol Settings

View File

@@ -18,8 +18,10 @@ Protocol API specification
SepTopProtocol
SepTopComplexSetupUnit
SepTopComplexRunUnit
SepTopComplexAnalysisUnit
SepTopSolventSetupUnit
SepTopSolventRunUnit
SepTopSolventAnalysisUnit
SepTopProtocolResult
Protocol Settings

View File

@@ -16,8 +16,12 @@ Protocol API specification
:toctree: generated/
AbsoluteSolvationProtocol
AbsoluteSolvationVacuumUnit
AbsoluteSolvationSolventUnit
AHFESolventAnalysisUnit
AHFESolventSetupUnit
AHFESolventSimUnit
AHFEVacuumAnalysisUnit
AHFEVacuumSetupUnit
AHFEVacuumSimUnit
AbsoluteSolvationProtocolResult
Protocol Settings

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
{
"path": "../ExampleNotebooks/abfe_tutorial/abfe_analysis.ipynb"
}

View File

@@ -0,0 +1,6 @@
{
"path": "../ExampleNotebooks/abfe_tutorial/abfe_tutorial.ipynb",
"extra-media": [
"../ExampleNotebooks/abfe_tutorial/abfe-cycle.png"
]
}

View File

@@ -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

View File

@@ -0,0 +1,3 @@
{
"path": "../ExampleNotebooks/membranes/rbfe_membrane_protein.ipynb"
}

View File

@@ -0,0 +1,3 @@
{
"path": "../ExampleNotebooks/openmm_septop/septop_analysis.ipynb"
}

View File

@@ -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

View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")

View File

@@ -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

View File

@@ -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.
"""

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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