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 Foyer forcefields #517

Merged
merged 34 commits into from Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c1c8ebe
Initial FoyerForceField classes
aehogan Jul 14, 2023
6eb1478
Initial version of _parameterize_molecule
aehogan Jul 17, 2023
92b3435
Add foyer forcefield test
aehogan Jul 17, 2023
0ab6877
Add support for RBTorsionForce
aehogan Jul 17, 2023
dc0bf5e
Add tests for both oplsaa and a custom foyer xml, bug fixes
aehogan Jul 17, 2023
7e93342
Add FoyerForceFieldSource to Forcefield init
aehogan Jul 17, 2023
5bec898
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 17, 2023
3410745
Add foyer to test env
aehogan Jul 18, 2023
3a3c6c7
Merge remote-tracking branch 'origin/main'
aehogan Jul 18, 2023
5085cdd
Merge branch 'openforcefield:main' into main
aehogan Jul 18, 2023
197db42
Remove explicit support for trappeua (can still use the xml file if y…
aehogan Jul 18, 2023
1d6ae35
Merge remote-tracking branch 'origin/main'
aehogan Jul 18, 2023
b41b42e
Add FoyerForceFieldSource to __all__
aehogan Jul 18, 2023
0f5062b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 18, 2023
2ab59e7
Add default cutoff to doc string
aehogan Jul 18, 2023
7b9b3fd
Remove forcing OPLS-AA to LB mixing rules
aehogan Jul 18, 2023
93043b8
Merge remote-tracking branch 'origin/main'
aehogan Jul 18, 2023
37be6ea
Rename ambiguous forcefield object from Foyer
aehogan Jul 18, 2023
6991efb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 18, 2023
5570036
Set combine_nonbonded_forces=false for Foyer forcefields to accomdate…
aehogan Jul 18, 2023
c65f2ae
Use tip3p for foyer xml-based forcefield test
aehogan Jul 19, 2023
53f62b3
Get foyer interchange box vectors from the coordinate file
aehogan Jul 19, 2023
e5296bd
Add support for openmm.CustomBondForce and openmm.CustomNonbondedForc…
aehogan Jul 19, 2023
bdae81c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2023
7924842
CustomBondForce's weren't getting added to the system
aehogan Jul 19, 2023
8033b74
Merge remote-tracking branch 'origin/main'
aehogan Jul 19, 2023
2f8e638
CustomBondForce and CustomNonbondedForce weren't getting index mapped…
aehogan Jul 19, 2023
4a59916
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2023
5cdb86a
Add BuildFoyerSystem to EvaluatorClient._default_protocol_replacements
aehogan Jul 20, 2023
d126895
Set exclusions for CustomNonbondedForce, change foyer xml test to met…
aehogan Jul 21, 2023
6024b93
Add openmm energy minimization to foyer XML test to ensure openmm sys…
aehogan Jul 24, 2023
281e9a5
Check energy function of openmm custom forces for existence check
aehogan Jul 24, 2023
ccb30ad
Merge pull request #1 from openforcefield/main
aehogan Jul 24, 2023
de3e374
Update releasehistory.rst
aehogan Jul 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions devtools/conda-envs/test_env.yaml
Expand Up @@ -15,6 +15,7 @@ dependencies:
- requests-mock # For testing http requests.
# smirnoff-plugins =0.0.3
- coverage >=4.4
- foyer

# Shims
- pint =0.20.1
Expand Down
2 changes: 2 additions & 0 deletions openff/evaluator/forcefield/__init__.py
@@ -1,5 +1,6 @@
from .forcefield import (
ForceFieldSource,
FoyerForceFieldSource,
aehogan marked this conversation as resolved.
Show resolved Hide resolved
LigParGenForceFieldSource,
SmirnoffForceFieldSource,
TLeapForceFieldSource,
Expand All @@ -11,6 +12,7 @@
SmirnoffForceFieldSource,
LigParGenForceFieldSource,
TLeapForceFieldSource,
FoyerForceFieldSource,
ParameterGradient,
ParameterGradientKey,
]
40 changes: 40 additions & 0 deletions openff/evaluator/forcefield/forcefield.py
Expand Up @@ -261,3 +261,43 @@ def __setstate__(self, state):
self._cutoff = state["cutoff"]
self._request_url = state["request_url"]
self._download_url = state["download_url"]


class FoyerForceFieldSource(ForceFieldSource):
"""A wrapper around Foyer force fields"""

@property
def foyer_source(self):
"""str: Foyer force field source."""
return self._foyer_source

@property
def cutoff(self):
"""openff.evaluator.unit.Quantity: The non-bonded interaction cutoff."""
mattwthompson marked this conversation as resolved.
Show resolved Hide resolved
return self._cutoff

def __init__(self, foyer_source="", cutoff=0.9 * unit.nanometer):
"""Constructs a new FoyerForceField Source

Parameters
----------
foyer_source: str
'oplsaa' or a Foyer XML forcefield file
cutoff: openff.evaluator.unit.Quantity
The non-bonded interaction cutoff, default 0.9 nanometers.

Examples
--------
To create a source for the Foyer force field:

>>> foyer_source = FoyerForceFieldSource('oplsaa')
"""
self._foyer_source = foyer_source
self._cutoff = cutoff

def __getstate__(self):
return {"foyer_source": self._foyer_source, "cutoff": self._cutoff}

def __setstate__(self, state):
self._foyer_source = state["foyer_source"]
self._cutoff = state["cutoff"]
65 changes: 65 additions & 0 deletions openff/evaluator/protocols/forcefield.py
Expand Up @@ -24,6 +24,7 @@
SmirnoffForceFieldSource,
TLeapForceFieldSource,
)
from openff.evaluator.forcefield.forcefield import FoyerForceFieldSource
from openff.evaluator.forcefield.system import ParameterizedSystem
from openff.evaluator.substances import Substance
from openff.evaluator.utils.utils import (
Expand Down Expand Up @@ -87,6 +88,7 @@ def _append_system(existing_system, system_to_append, index_map=None):
openmm.HarmonicAngleForce,
openmm.PeriodicTorsionForce,
openmm.NonbondedForce,
openmm.RBTorsionForce,
]

number_of_appended_forces = 0
Expand Down Expand Up @@ -226,6 +228,15 @@ def _append_system(existing_system, system_to_append, index_map=None):
index_a + index_offset, index_b + index_offset, *parameters
)

elif isinstance(force_to_append, openmm.RBTorsionForce):
# Support for RBTorisionForce needed for OPLSAA, etc
for index in range(force_to_append.getNumTorsions()):
torsion_params = force_to_append.getTorsionParameters(index)
for i in range(4):
torsion_params[i] = index_map[torsion_params[i]] + index_offset

existing_force.addTorsion(*torsion_params)

number_of_appended_forces += 1

if number_of_appended_forces != system_to_append.getNumForces():
Expand Down Expand Up @@ -1052,3 +1063,57 @@ def _execute(self, directory, available_resources):
)

super(BuildTLeapSystem, self)._execute(directory, available_resources)


@workflow_protocol()
class BuildFoyerSystem(TemplateBuildSystem):
"""Parameterize a set of molecules with a Foyer force field source"""

def _parameterize_molecule(self, molecule, force_field_source, cutoff):
"""Parameterize the specified molecule.

Parameters
----------
molecule: openff.toolkit.topology.Molecule
The molecule to parameterize.
force_field_source: FoyerForceFieldSource
The foyer source which describes which parameters to apply.

Returns
-------
openmm.System
The parameterized system.
"""
from foyer import Forcefield as FoyerForceField
from openff.interchange import Interchange
from openff.toolkit import Topology

topology: Topology = molecule.to_topology()

force_field: FoyerForceField
if force_field_source.foyer_source.lower() == "oplsaa":
force_field = FoyerForceField(name="oplsaa")
else:
force_field = FoyerForceField(
forcefield_files=force_field_source.foyer_source
)

interchange = Interchange.from_foyer(topology=topology, force_field=force_field)

openmm_pdb_file = app.PDBFile(self.coordinate_file_path)
if openmm_pdb_file.topology.getPeriodicBoxVectors() is not None:
interchange.box = openmm_pdb_file.topology.getPeriodicBoxVectors()

openmm_system = interchange.to_openmm(combine_nonbonded_forces=False)

return openmm_system

def _execute(self, directory, available_resources):
force_field_source = ForceFieldSource.from_json(self.force_field_path)

if not isinstance(force_field_source, FoyerForceFieldSource):
raise ValueError(
"Only Foyer force field sources are supported by this protocol."
)

super(BuildFoyerSystem, self)._execute(directory, available_resources)
68 changes: 68 additions & 0 deletions openff/evaluator/tests/test_protocols/test_forcefield.py
Expand Up @@ -9,8 +9,10 @@
from openff.toolkit.utils.rdkit_wrapper import RDKitToolkitWrapper

from openff.evaluator.forcefield import LigParGenForceFieldSource, TLeapForceFieldSource
from openff.evaluator.forcefield.forcefield import FoyerForceFieldSource
from openff.evaluator.protocols.coordinates import BuildCoordinatesPackmol
from openff.evaluator.protocols.forcefield import (
BuildFoyerSystem,
BuildLigParGenSystem,
BuildSmirnoffSystem,
BuildTLeapSystem,
Expand Down Expand Up @@ -161,3 +163,69 @@ def download_callback(_, context):
assign_parameters.substance = substance
assign_parameters.execute(directory)
assert path.isfile(assign_parameters.parameterized_system.system_path)


def test_build_foyer_oplsaa_system():
force_field_source = FoyerForceFieldSource("oplsaa")
substance = Substance.from_components("C", "O", "CCC", "C1CCCC1")

with tempfile.TemporaryDirectory() as directory:
force_field_path = path.join(directory, "ff.json")

with open(force_field_path, "w") as file:
file.write(force_field_source.json())

build_coordinates = BuildCoordinatesPackmol("build_coordinates")
build_coordinates.max_molecules = 8
build_coordinates.substance = substance
build_coordinates.execute(directory)

assign_parameters = BuildFoyerSystem("assign_parameters")
assign_parameters.force_field_path = force_field_path
assign_parameters.coordinate_file_path = build_coordinates.coordinate_file_path
assign_parameters.substance = substance
assign_parameters.execute(directory)
assert path.isfile(assign_parameters.parameterized_system.system_path)


def test_build_foyer_xml_system():
with tempfile.TemporaryDirectory() as directory:
force_field_source_path = path.join(directory, "ff.json")
force_field_xml_path = path.join(directory, "foyer_ff.xml")
force_field_source = FoyerForceFieldSource(force_field_xml_path)
substance = Substance.from_components("O")

with open(force_field_source_path, "w") as file:
file.write(force_field_source.json())

with open(force_field_xml_path, "w") as file:
file.write(
"""<ForceField name="tip3p" version="0.0.3" combining_rule="geometric">
<AtomTypes>
<Type name="HW_tip3p" class="HW_tip3p" element="H" mass="1.008"/>
<Type name="OW_tip3p" class="OW_tip3p" element="O" mass="15.9994"/>
</AtomTypes>
<NonbondedForce coulomb14scale="0.5" lj14scale="0.5">
<Atom type="HW_tip3p" charge="0.417" sigma="1.0" epsilon="0.0"/>
<Atom type="OW_tip3p" charge="-0.834" sigma="0.315007" epsilon="0.63681228"/>
</NonbondedForce>
<HarmonicBondForce>
<Bond class1="OW_tip3p" class2="HW_tip3p" length="0.09572" k="1884.06"/>
</HarmonicBondForce>
<HarmonicAngleForce>
<Angle class1="HW_tip3p" class2="OW_tip3p" class3="HW_tip3p" angle="1.82421813" k="230.274"/>
</HarmonicAngleForce>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIP3P is supposed to be a fixed model, but I don't think Foyer support constraints in its force field schema? We always did that at simulation runtime, I think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test builds a rigid tip3p model HOWEVER this seems due to a short circuit in TemplateBuildSystem._execute that forces all water to be rigid tip3p with TemplateBuildSystem._build_tip3p_system. Is the intent to always override any water with tip3p in evaluator?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's the intent

</ForceField>"""
)

build_coordinates = BuildCoordinatesPackmol("build_coordinates")
build_coordinates.max_molecules = 8
build_coordinates.substance = substance
build_coordinates.execute(directory)

assign_parameters = BuildFoyerSystem("assign_parameters")
assign_parameters.force_field_path = force_field_source_path
assign_parameters.coordinate_file_path = build_coordinates.coordinate_file_path
assign_parameters.substance = substance
assign_parameters.execute(directory)
assert path.isfile(assign_parameters.parameterized_system.system_path)