Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for openff-nagl for generation of partial charges #267

Merged
merged 15 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/devel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
SIRE_DONT_PHONEHOME: 1
SIRE_SILENT_PHONEHOME: 1
steps:
- uses: conda-incubator/setup-miniconda@v2
- uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}
Expand All @@ -49,7 +49,7 @@ jobs:
run: git clone -b devel https://github.com/openbiosim/biosimspace
#
- name: Setup Conda
run: mamba install -y -c conda-forge boa anaconda-client packaging=21 pip-requirements-parser
run: mamba install -y -c conda-forge boa anaconda-client packaging pip-requirements-parser
#
- name: Update Conda recipe
run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
SIRE_DONT_PHONEHOME: 1
SIRE_SILENT_PHONEHOME: 1
steps:
- uses: conda-incubator/setup-miniconda@v2
- uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}
Expand All @@ -43,7 +43,7 @@ jobs:
run: git clone -b main https://github.com/openbiosim/biosimspace
#
- name: Setup Conda
run: mamba install -y -c conda-forge boa anaconda-client packaging=21 pip-requirements-parser
run: mamba install -y -c conda-forge boa anaconda-client packaging pip-requirements-parser
#
- name: Update Conda recipe
run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
SIRE_SILENT_PHONEHOME: 1
REPO: "${{ github.event.pull_request.head.repo.full_name || github.repository }}"
steps:
- uses: conda-incubator/setup-miniconda@v2
- uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}
Expand All @@ -53,7 +53,7 @@ jobs:
run: git clone -b ${{ github.head_ref }} --single-branch https://github.com/${{ env.REPO }} biosimspace
#
- name: Setup Conda
run: mamba install -y -c conda-forge boa anaconda-client packaging=21 pip-requirements-parser
run: mamba install -y -c conda-forge boa anaconda-client packaging pip-requirements-parser
#
- name: Update Conda recipe
run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py
Expand Down
4 changes: 4 additions & 0 deletions python/BioSimSpace/IO/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
.. autosummary::
:toctree: generated/

clearCache
disableCache
enableCache
fileFormats
formatInfo
readMolecules
Expand All @@ -38,3 +41,4 @@
"""

from ._io import *
from ._file_cache import *
45 changes: 42 additions & 3 deletions python/BioSimSpace/IO/_file_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
__author__ = "Lester Hedges"
__email__ = "[email protected]"

__all__ = ["check_cache", "update_cache"]
__all__ = ["clearCache", "disableCache", "enableCache"]

import collections as _collections
import hashlib as _hashlib
Expand Down Expand Up @@ -80,8 +80,43 @@ def __delitem__(self, key):
# to the same format, allowing us to re-use the existing file.
_cache = _FixedSizeOrderedDict()

# Whether to use the cache.
_use_cache = True

def check_cache(

def clearCache():
"""
Clear the file cache.
"""
global _cache
_cache = _FixedSizeOrderedDict()


def disableCache():
"""
Disable the file cache.
"""
global _use_cache
_use_cache = False


def enableCache():
"""
Enable the file cache.
"""
global _use_cache
_use_cache = True


def _cache_active():
"""
Internal helper function to check whether the cache is active.
"""
global _use_cache
return _use_cache


def _check_cache(
system,
format,
filebase,
Expand Down Expand Up @@ -157,6 +192,8 @@ def check_cache(
if not isinstance(skip_water, bool):
raise TypeError("'skip_water' must be of type 'bool'.")

global _cache

# Create the key.
key = (
system._sire_object.uid().toString(),
Expand Down Expand Up @@ -221,7 +258,7 @@ def check_cache(
return ext


def update_cache(
def _update_cache(
system,
format,
path,
Expand Down Expand Up @@ -284,6 +321,8 @@ def update_cache(
if not isinstance(skip_water, bool):
raise TypeError("'skip_water' must be of type 'bool'.")

global _cache

# Convert to an absolute path.
path = _os.path.abspath(path)

Expand Down
45 changes: 29 additions & 16 deletions python/BioSimSpace/IO/_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@
from .._SireWrappers import System as _System
from .. import _Utils

from ._file_cache import check_cache as _check_cache
from ._file_cache import update_cache as _update_cache
from ._file_cache import _check_cache
from ._file_cache import _update_cache
from ._file_cache import _cache_active


# Context manager for capturing stdout.
Expand Down Expand Up @@ -430,12 +431,9 @@ def readMolecules(
)
_has_gmx_warned = True

# Glob string to catch wildcards and convert to list.
# Convert a single string to a list.
if isinstance(files, str):
if not files.startswith(("http", "www")):
files = _glob(files)
else:
files = [files]
files = [files]

# Check that all arguments are of type 'str'.
if isinstance(files, (list, tuple)):
Expand All @@ -449,6 +447,15 @@ def readMolecules(
else:
raise TypeError("'files' must be of type 'str', or a list of 'str' types.")

# Glob all files to catch wildcards.
new_files = []
for file in files:
if not file.startswith(("http", "www")):
new_files += _glob(file)
else:
new_files.append(file)
files = new_files

# Validate the molecule unwrapping flag.
if not isinstance(make_whole, bool):
raise TypeError("'make_whole' must be of type 'bool'.")
Expand Down Expand Up @@ -741,14 +748,17 @@ def saveMolecules(
# Save the system using each file format.
for format in formats:
# Copy an existing file if it exists in the cache.
ext = _check_cache(
system,
format,
filebase,
match_water=match_water,
property_map=property_map,
**kwargs,
)
if _cache_active():
ext = _check_cache(
system,
format,
filebase,
match_water=match_water,
property_map=property_map,
**kwargs,
)
else:
ext = None
if ext:
files.append(_os.path.abspath(filebase + ext))
continue
Expand Down Expand Up @@ -835,7 +845,10 @@ def saveMolecules(
files += file

# If this is a new file, then add it to the cache.
_update_cache(system, format, file[0], match_water=match_water, **kwargs)
if _cache_active():
_update_cache(
system, format, file[0], match_water=match_water, **kwargs
)

except Exception as e:
msg = "Failed to save system to format: '%s'" % format
Expand Down
62 changes: 58 additions & 4 deletions python/BioSimSpace/Parameters/_Protocol/_openforcefield.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,37 @@

_openff = _try_import("openff")

# Initialise the NAGL support flag.
_has_nagl = False

if _have_imported(_openff):
from openff.interchange import Interchange as _Interchange
from openff.toolkit.topology import Molecule as _OpenFFMolecule
from openff.toolkit.topology import Topology as _OpenFFTopology
from openff.toolkit.typing.engines.smirnoff import ForceField as _Forcefield

try:
from openff.toolkit.utils.nagl_wrapper import (
NAGLToolkitWrapper as _NAGLToolkitWrapper,
)

_has_nagl = _NAGLToolkitWrapper.is_available()
from openff.nagl_models import get_models_by_type as _get_models_by_type

_models = _get_models_by_type("am1bcc")
try:
# Find the most recent AM1-BCC release candidate.
_nagl = _NAGLToolkitWrapper()
_nagl_model = sorted(
[str(model) for model in _models if "rc" in str(model)], reverse=True
)[0]
except:
_has_nagl = False
del _models
except:
_has_nagl = False
else:
_Interchange = _openff
_OpenFFMolecule = _openff
_OpenFFTopology = _openff
_Forcefield = _openff

# Reset stderr.
Expand All @@ -105,7 +127,9 @@
class OpenForceField(_protocol.Protocol):
"""A class for handling protocols for Open Force Field models."""

def __init__(self, forcefield, ensure_compatible=True, property_map={}):
def __init__(
self, forcefield, ensure_compatible=True, use_nagl=True, property_map={}
):
"""
Constructor.

Expand All @@ -123,6 +147,11 @@ def __init__(self, forcefield, ensure_compatible=True, property_map={}):
original molecule, e.g. the original atom and residue names will be
kept.

use_nagl : bool
Whether to use NAGL to compute AM1-BCC charges. If False, the default
is to use AmberTools via antechamber and sqm. (This option is only
used if NAGL is available.)

property_map : dict
A dictionary that maps system "properties" to their user defined
values. This allows the user to refer to properties with their
Expand All @@ -136,6 +165,12 @@ def __init__(self, forcefield, ensure_compatible=True, property_map={}):
property_map=property_map,
)

if not isinstance(use_nagl, bool):
raise TypeError("'use_nagl' must be of type 'bool'")

# Set the NAGL flag.
self._use_nagl = use_nagl

# Set the compatibility flags.
self._tleap = False
self._pdb2gmx = False
Expand Down Expand Up @@ -291,6 +326,23 @@ def run(self, molecule, work_dir=None, queue=None):
else:
raise _ThirdPartyError(msg) from None

# Apply AM1-BCC charges using NAGL.
if _has_nagl and self._use_nagl:
try:
_nagl.assign_partial_charges(
off_molecule, partial_charge_method=_nagl_model
)
except Exception as e:
msg = "Failed to assign AM1-BCC charges using NAGL."
if _isVerbose():
msg += ": " + getattr(e, "message", repr(e))
raise _ThirdPartyError(msg) from e
else:
raise _ThirdPartyError(msg) from None
charge_from_molecules = [off_molecule]
else:
charge_from_molecules = None

# Extract the molecular topology.
try:
off_topology = off_molecule.to_topology()
Expand All @@ -317,7 +369,9 @@ def run(self, molecule, work_dir=None, queue=None):
# Create an Interchange object.
try:
interchange = _Interchange.from_smirnoff(
force_field=forcefield, topology=off_topology
force_field=forcefield,
topology=off_topology,
charge_from_molecules=charge_from_molecules,
)
except Exception as e:
msg = "Unable to create OpenFF Interchange object!"
Expand Down
Loading