From c1c8ebe71b8f26eb65cec033e9ca89d279b1ceaf Mon Sep 17 00:00:00 2001 From: aehogan Date: Fri, 14 Jul 2023 16:46:42 -0400 Subject: [PATCH 01/28] Initial FoyerForceField classes --- openff/evaluator/forcefield/forcefield.py | 40 +++++++++++++++++++++++ openff/evaluator/protocols/forcefield.py | 7 ++++ 2 files changed, 47 insertions(+) diff --git a/openff/evaluator/forcefield/forcefield.py b/openff/evaluator/forcefield/forcefield.py index e0add7cb..c52aebf7 100644 --- a/openff/evaluator/forcefield/forcefield.py +++ b/openff/evaluator/forcefield/forcefield.py @@ -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 leap_source(self): + """str: Foyer force field source.""" + return self._foyer_source + + @property + def cutoff(self): + """openff.evaluator.unit.Quantity: The non-bonded interaction cutoff.""" + return self._cutoff + + def __init__(self, foyer_source="", cutoff=0.9 * unit.nanometer): + """Constructs a new FoyerForceField Source + + Parameters + ---------- + foyer_source: str + The parameter file from Foyer + cutoff: openff.evaluator.unit.Quantity + The non-bonded interaction cutoff. + + Examples + -------- + To create a source for the Foyer force field: + + >>> foyer_source = FoyerForceFieldSource('') + """ + 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"] diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index 73019565..e1283fe2 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -1052,3 +1052,10 @@ def _execute(self, directory, available_resources): ) super(BuildTLeapSystem, self)._execute(directory, available_resources) + +@workflow_protocol +class BuildFoyerSystem(BaseBuildSystem): + """Parameterize a set of molecules with a Foyer force field source""" + + def _execute(self, directory, available_resources): + pass From 6eb14785e852bf134a621390a999e42bd8e02b52 Mon Sep 17 00:00:00 2001 From: aehogan Date: Mon, 17 Jul 2023 13:38:53 -0400 Subject: [PATCH 02/28] Initial version of _parameterize_molecule --- openff/evaluator/protocols/forcefield.py | 49 +++++++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index e1283fe2..fb89ff3d 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -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 ( @@ -1053,9 +1054,53 @@ def _execute(self, directory, available_resources): super(BuildTLeapSystem, self)._execute(directory, available_resources) -@workflow_protocol + +@workflow_protocol() class BuildFoyerSystem(BaseBuildSystem): """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. + """ + import mdtraj as md + from foyer import Forcefield + from openff.interchange import Interchange + from openff.toolkit import Topology + + if molecule.n_conformers == 0: + molecule.generate_conformers(n_conformers=1) + + topology: Topology = molecule.to_topology() + topology.mdtop = md.Topology.from_openmm(topology.to_openmm()) + + force_field: Forcefield = Forcefield(name="oplsaa") + + interchange = Interchange.from_foyer(topology=topology, force_field=force_field) + interchange["vdW"].mixing_rule = "lorentz-berthelot" + interchange.positions = molecule.conformers[0] + + openmm_system = interchange.to_openmm() + + return openmm_system + def _execute(self, directory, available_resources): - pass + 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) From 92b34354f89f86963414038ccee02eaf6fa5afa4 Mon Sep 17 00:00:00 2001 From: aehogan Date: Mon, 17 Jul 2023 14:03:15 -0400 Subject: [PATCH 03/28] Add foyer forcefield test --- .../tests/test_protocols/test_forcefield.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/openff/evaluator/tests/test_protocols/test_forcefield.py b/openff/evaluator/tests/test_protocols/test_forcefield.py index b825bb79..d612aad9 100644 --- a/openff/evaluator/tests/test_protocols/test_forcefield.py +++ b/openff/evaluator/tests/test_protocols/test_forcefield.py @@ -9,11 +9,12 @@ 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 ( BuildLigParGenSystem, BuildSmirnoffSystem, - BuildTLeapSystem, + BuildTLeapSystem, BuildFoyerSystem, ) from openff.evaluator.substances import Substance from openff.evaluator.tests.utils import build_tip3p_smirnoff_force_field @@ -161,3 +162,25 @@ 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_system(): + force_field_source = FoyerForceFieldSource("oplsaa") + substance = Substance.from_components("C", "O") + + 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) From 0ab68779cc7448e33b4f632c907097c6699fc791 Mon Sep 17 00:00:00 2001 From: aehogan Date: Mon, 17 Jul 2023 14:03:31 -0400 Subject: [PATCH 04/28] Add support for RBTorsionForce --- openff/evaluator/protocols/forcefield.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index fb89ff3d..5141e040 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -88,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 @@ -227,6 +228,20 @@ 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.getNumParticles()): + index = index_map[index] + + 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(): @@ -1056,7 +1071,7 @@ def _execute(self, directory, available_resources): @workflow_protocol() -class BuildFoyerSystem(BaseBuildSystem): +class BuildFoyerSystem(TemplateBuildSystem): """Parameterize a set of molecules with a Foyer force field source""" def _parameterize_molecule(self, molecule, force_field_source, cutoff): From dc0bf5e3847e9b08aaf6213d1052d2ad67f5131e Mon Sep 17 00:00:00 2001 From: aehogan Date: Mon, 17 Jul 2023 16:21:55 -0400 Subject: [PATCH 05/28] Add tests for both oplsaa and a custom foyer xml, bug fixes --- openff/evaluator/forcefield/forcefield.py | 6 +-- openff/evaluator/protocols/forcefield.py | 21 ++++----- .../tests/test_protocols/test_forcefield.py | 47 +++++++++++++++++-- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/openff/evaluator/forcefield/forcefield.py b/openff/evaluator/forcefield/forcefield.py index c52aebf7..6be2be1c 100644 --- a/openff/evaluator/forcefield/forcefield.py +++ b/openff/evaluator/forcefield/forcefield.py @@ -267,7 +267,7 @@ class FoyerForceFieldSource(ForceFieldSource): """A wrapper around Foyer force fields""" @property - def leap_source(self): + def foyer_source(self): """str: Foyer force field source.""" return self._foyer_source @@ -282,7 +282,7 @@ def __init__(self, foyer_source="", cutoff=0.9 * unit.nanometer): Parameters ---------- foyer_source: str - The parameter file from Foyer + 'oplsaa' or a Foyer XML forcefield file cutoff: openff.evaluator.unit.Quantity The non-bonded interaction cutoff. @@ -290,7 +290,7 @@ def __init__(self, foyer_source="", cutoff=0.9 * unit.nanometer): -------- To create a source for the Foyer force field: - >>> foyer_source = FoyerForceFieldSource('') + >>> foyer_source = FoyerForceFieldSource('oplsaa') """ self._foyer_source = foyer_source self._cutoff = cutoff diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index 5141e040..c082ff55 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -230,17 +230,12 @@ def _append_system(existing_system, system_to_append, index_map=None): elif isinstance(force_to_append, openmm.RBTorsionForce): # Support for RBTorisionForce needed for OPLSAA, etc - for index in range(force_to_append.getNumParticles()): - index = index_map[index] - + 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 - ) - + existing_force.addTorsion(*torsion_params) number_of_appended_forces += 1 @@ -1094,17 +1089,19 @@ def _parameterize_molecule(self, molecule, force_field_source, cutoff): from openff.interchange import Interchange from openff.toolkit import Topology - if molecule.n_conformers == 0: - molecule.generate_conformers(n_conformers=1) - topology: Topology = molecule.to_topology() topology.mdtop = md.Topology.from_openmm(topology.to_openmm()) - force_field: Forcefield = Forcefield(name="oplsaa") + force_field: Forcefield + if force_field_source.foyer_source.lower() == "oplsaa": + force_field = Forcefield(name="oplsaa") + elif force_field_source.foyer_source.lower() == "trappe-ua": + force_field = Forcefield(name="trappe-ua") + else: + force_field = Forcefield(forcefield_files=force_field_source.foyer_source) interchange = Interchange.from_foyer(topology=topology, force_field=force_field) interchange["vdW"].mixing_rule = "lorentz-berthelot" - interchange.positions = molecule.conformers[0] openmm_system = interchange.to_openmm() diff --git a/openff/evaluator/tests/test_protocols/test_forcefield.py b/openff/evaluator/tests/test_protocols/test_forcefield.py index d612aad9..8b4adc74 100644 --- a/openff/evaluator/tests/test_protocols/test_forcefield.py +++ b/openff/evaluator/tests/test_protocols/test_forcefield.py @@ -12,9 +12,10 @@ from openff.evaluator.forcefield.forcefield import FoyerForceFieldSource from openff.evaluator.protocols.coordinates import BuildCoordinatesPackmol from openff.evaluator.protocols.forcefield import ( + BuildFoyerSystem, BuildLigParGenSystem, BuildSmirnoffSystem, - BuildTLeapSystem, BuildFoyerSystem, + BuildTLeapSystem, ) from openff.evaluator.substances import Substance from openff.evaluator.tests.utils import build_tip3p_smirnoff_force_field @@ -163,9 +164,10 @@ def download_callback(_, context): assign_parameters.execute(directory) assert path.isfile(assign_parameters.parameterized_system.system_path) -def test_build_foyer_system(): + +def test_build_foyer_oplsaa_system(): force_field_source = FoyerForceFieldSource("oplsaa") - substance = Substance.from_components("C", "O") + substance = Substance.from_components("C", "O", "CCC", "C1CCCC1") with tempfile.TemporaryDirectory() as directory: force_field_path = path.join(directory, "ff.json") @@ -184,3 +186,42 @@ def test_build_foyer_system(): 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( + """ + + + + + + + + + + +""" + ) + + 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) From 7e9334277f5c7be2a0c797865fe3e35a5a36c51d Mon Sep 17 00:00:00 2001 From: aehogan Date: Mon, 17 Jul 2023 16:40:49 -0400 Subject: [PATCH 06/28] Add FoyerForceFieldSource to Forcefield init --- openff/evaluator/forcefield/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openff/evaluator/forcefield/__init__.py b/openff/evaluator/forcefield/__init__.py index dced6355..1d0d0968 100644 --- a/openff/evaluator/forcefield/__init__.py +++ b/openff/evaluator/forcefield/__init__.py @@ -3,6 +3,7 @@ LigParGenForceFieldSource, SmirnoffForceFieldSource, TLeapForceFieldSource, + FoyerForceFieldSource, ) from .gradients import ParameterGradient, ParameterGradientKey From 5bec898cb1bb187d6abf7c70d25304b72ccd68d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 21:43:43 +0000 Subject: [PATCH 07/28] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openff/evaluator/forcefield/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openff/evaluator/forcefield/__init__.py b/openff/evaluator/forcefield/__init__.py index 1d0d0968..05bfad06 100644 --- a/openff/evaluator/forcefield/__init__.py +++ b/openff/evaluator/forcefield/__init__.py @@ -1,9 +1,9 @@ from .forcefield import ( ForceFieldSource, + FoyerForceFieldSource, LigParGenForceFieldSource, SmirnoffForceFieldSource, TLeapForceFieldSource, - FoyerForceFieldSource, ) from .gradients import ParameterGradient, ParameterGradientKey From 34107457b75b10b5e016e6bf37e1e01b33e0f4a6 Mon Sep 17 00:00:00 2001 From: aehogan Date: Tue, 18 Jul 2023 11:53:15 -0400 Subject: [PATCH 08/28] Add foyer to test env --- devtools/conda-envs/test_env.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 9681b863..a9f1bbbe 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -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 From 197db4254cef05165beec8851e20ae2a60119e96 Mon Sep 17 00:00:00 2001 From: aehogan Date: Tue, 18 Jul 2023 12:19:35 -0400 Subject: [PATCH 09/28] Remove explicit support for trappeua (can still use the xml file if you want) --- openff/evaluator/protocols/forcefield.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index c082ff55..75076967 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -1095,8 +1095,6 @@ def _parameterize_molecule(self, molecule, force_field_source, cutoff): force_field: Forcefield if force_field_source.foyer_source.lower() == "oplsaa": force_field = Forcefield(name="oplsaa") - elif force_field_source.foyer_source.lower() == "trappe-ua": - force_field = Forcefield(name="trappe-ua") else: force_field = Forcefield(forcefield_files=force_field_source.foyer_source) From b41b42efeca733d37382e437c7ad21d5db826c5e Mon Sep 17 00:00:00 2001 From: aehogan Date: Tue, 18 Jul 2023 14:35:29 -0400 Subject: [PATCH 10/28] Add FoyerForceFieldSource to __all__ --- openff/evaluator/forcefield/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openff/evaluator/forcefield/__init__.py b/openff/evaluator/forcefield/__init__.py index 05bfad06..138466e2 100644 --- a/openff/evaluator/forcefield/__init__.py +++ b/openff/evaluator/forcefield/__init__.py @@ -1,9 +1,9 @@ from .forcefield import ( ForceFieldSource, - FoyerForceFieldSource, - LigParGenForceFieldSource, SmirnoffForceFieldSource, + LigParGenForceFieldSource, TLeapForceFieldSource, + FoyerForceFieldSource, ) from .gradients import ParameterGradient, ParameterGradientKey @@ -12,6 +12,7 @@ SmirnoffForceFieldSource, LigParGenForceFieldSource, TLeapForceFieldSource, + FoyerForceFieldSource, ParameterGradient, ParameterGradientKey, ] From 0f5062bbe1f70ee0b7d1bf8ca6e884df49735ec2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 18:37:09 +0000 Subject: [PATCH 11/28] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openff/evaluator/forcefield/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openff/evaluator/forcefield/__init__.py b/openff/evaluator/forcefield/__init__.py index 138466e2..f0279d6c 100644 --- a/openff/evaluator/forcefield/__init__.py +++ b/openff/evaluator/forcefield/__init__.py @@ -1,9 +1,9 @@ from .forcefield import ( ForceFieldSource, - SmirnoffForceFieldSource, + FoyerForceFieldSource, LigParGenForceFieldSource, + SmirnoffForceFieldSource, TLeapForceFieldSource, - FoyerForceFieldSource, ) from .gradients import ParameterGradient, ParameterGradientKey From 2ab59e7641b57550f19d9b4263e4dc6393de3cfa Mon Sep 17 00:00:00 2001 From: Adam Hogan Date: Tue, 18 Jul 2023 14:38:16 -0400 Subject: [PATCH 12/28] Add default cutoff to doc string Co-authored-by: Matt Thompson --- openff/evaluator/forcefield/forcefield.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openff/evaluator/forcefield/forcefield.py b/openff/evaluator/forcefield/forcefield.py index 6be2be1c..6a21e931 100644 --- a/openff/evaluator/forcefield/forcefield.py +++ b/openff/evaluator/forcefield/forcefield.py @@ -284,7 +284,7 @@ def __init__(self, foyer_source="", cutoff=0.9 * unit.nanometer): foyer_source: str 'oplsaa' or a Foyer XML forcefield file cutoff: openff.evaluator.unit.Quantity - The non-bonded interaction cutoff. + The non-bonded interaction cutoff, default 0.9 nanometers. Examples -------- From 7b9b3fd70793c0d85369e28b5fd74a29ced43243 Mon Sep 17 00:00:00 2001 From: aehogan Date: Tue, 18 Jul 2023 14:41:26 -0400 Subject: [PATCH 13/28] Remove forcing OPLS-AA to LB mixing rules --- openff/evaluator/protocols/forcefield.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index 75076967..0492a6af 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -1099,7 +1099,6 @@ def _parameterize_molecule(self, molecule, force_field_source, cutoff): force_field = Forcefield(forcefield_files=force_field_source.foyer_source) interchange = Interchange.from_foyer(topology=topology, force_field=force_field) - interchange["vdW"].mixing_rule = "lorentz-berthelot" openmm_system = interchange.to_openmm() From 37be6ea7ad15360acd67b30f7558a2558a4965ff Mon Sep 17 00:00:00 2001 From: aehogan Date: Tue, 18 Jul 2023 14:44:51 -0400 Subject: [PATCH 14/28] Rename ambiguous forcefield object from Foyer --- openff/evaluator/protocols/forcefield.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index 0492a6af..7d23be54 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -1084,19 +1084,17 @@ def _parameterize_molecule(self, molecule, force_field_source, cutoff): openmm.System The parameterized system. """ - import mdtraj as md - from foyer import Forcefield + from foyer import Forcefield as FoyerForceField from openff.interchange import Interchange from openff.toolkit import Topology topology: Topology = molecule.to_topology() - topology.mdtop = md.Topology.from_openmm(topology.to_openmm()) - force_field: Forcefield + force_field: FoyerForceField if force_field_source.foyer_source.lower() == "oplsaa": - force_field = Forcefield(name="oplsaa") + force_field = FoyerForceField(name="oplsaa") else: - force_field = Forcefield(forcefield_files=force_field_source.foyer_source) + force_field = FoyerForceField(forcefield_files=force_field_source.foyer_source) interchange = Interchange.from_foyer(topology=topology, force_field=force_field) From 6991efbe5604b07296461833c9501749c4b70fd8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 18:47:15 +0000 Subject: [PATCH 15/28] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openff/evaluator/protocols/forcefield.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index 7d23be54..685f7060 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -1094,7 +1094,9 @@ def _parameterize_molecule(self, molecule, force_field_source, cutoff): if force_field_source.foyer_source.lower() == "oplsaa": force_field = FoyerForceField(name="oplsaa") else: - force_field = FoyerForceField(forcefield_files=force_field_source.foyer_source) + force_field = FoyerForceField( + forcefield_files=force_field_source.foyer_source + ) interchange = Interchange.from_foyer(topology=topology, force_field=force_field) From 55700363a404bcad2980805e1de64f7d87e4f1b3 Mon Sep 17 00:00:00 2001 From: aehogan Date: Tue, 18 Jul 2023 15:09:19 -0400 Subject: [PATCH 16/28] Set combine_nonbonded_forces=false for Foyer forcefields to accomdate other mixing rules --- openff/evaluator/protocols/forcefield.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index 685f7060..c0dedbfb 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -1100,7 +1100,7 @@ def _parameterize_molecule(self, molecule, force_field_source, cutoff): interchange = Interchange.from_foyer(topology=topology, force_field=force_field) - openmm_system = interchange.to_openmm() + openmm_system = interchange.to_openmm(combine_nonbonded_forces=False) return openmm_system From c65f2aefe27cefc3d88a01a6e36bb4f125b376e2 Mon Sep 17 00:00:00 2001 From: aehogan Date: Wed, 19 Jul 2023 17:12:57 -0400 Subject: [PATCH 17/28] Use tip3p for foyer xml-based forcefield test --- .../tests/test_protocols/test_forcefield.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openff/evaluator/tests/test_protocols/test_forcefield.py b/openff/evaluator/tests/test_protocols/test_forcefield.py index 8b4adc74..60eb58b7 100644 --- a/openff/evaluator/tests/test_protocols/test_forcefield.py +++ b/openff/evaluator/tests/test_protocols/test_forcefield.py @@ -200,17 +200,21 @@ def test_build_foyer_xml_system(): with open(force_field_xml_path, "w") as file: file.write( - """ + """ - - - + + - - - + + + + + + + + """ ) From 53f62b31dde07386c2f8b0d9a5c2bb3e2be20a5e Mon Sep 17 00:00:00 2001 From: aehogan Date: Wed, 19 Jul 2023 17:33:42 -0400 Subject: [PATCH 18/28] Get foyer interchange box vectors from the coordinate file --- openff/evaluator/protocols/forcefield.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index c0dedbfb..2104a664 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -1100,6 +1100,10 @@ def _parameterize_molecule(self, molecule, force_field_source, cutoff): 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 From e5296bd1e12a46a6796bf65de3d51c18482a3158 Mon Sep 17 00:00:00 2001 From: aehogan Date: Wed, 19 Jul 2023 18:28:04 -0400 Subject: [PATCH 19/28] Add support for openmm.CustomBondForce and openmm.CustomNonbondedForce to _append_system --- openff/evaluator/protocols/forcefield.py | 42 ++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index 2104a664..e9de75b2 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -89,6 +89,8 @@ def _append_system(existing_system, system_to_append, index_map=None): openmm.PeriodicTorsionForce, openmm.NonbondedForce, openmm.RBTorsionForce, + openmm.CustomNonbondedForce, + openmm.CustomBondForce, ] number_of_appended_forces = 0 @@ -142,8 +144,34 @@ def _append_system(existing_system, system_to_append, index_map=None): break if existing_force is None: - existing_force = type(force_to_append)() - existing_system.addForce(existing_force) + if isinstance(force_to_append, openmm.CustomNonbondedForce): + existing_force = openmm.CustomNonbondedForce( + force_to_append.getEnergyFunction() + ) + for index in range(force_to_append.getNumGlobalParameters()): + existing_force.addGlobalParameter( + force_to_append.getGlobalParameterName(index), + force_to_append.getGlobalParameterDefaultValue(index), + ) + for index in range(force_to_append.getNumPerParticleParameters()): + existing_force.addPerParticleParameter( + force_to_append.getPerParticleParameterName(index) + ) + existing_system.addForce(existing_force) + elif isinstance(force_to_append, openmm.CustomBondForce): + existing_force = openmm.CustomBondForce(force_to_append.getEnergyFunction()) + for index in range(force_to_append.getNumGlobalParameters()): + existing_force.addGlobalParameter( + force_to_append.getGlobalParameterName(index), + force_to_append.getGlobalParameterDefaultValue(index), + ) + for index in range(force_to_append.getNumPerBondParameters()): + existing_force.addPerBondParameter( + force_to_append.getPerBondParameterName(index) + ) + else: + existing_force = type(force_to_append)() + existing_system.addForce(existing_force) if isinstance(force_to_append, openmm.HarmonicBondForce): # Add the bonds. @@ -237,6 +265,16 @@ def _append_system(existing_system, system_to_append, index_map=None): existing_force.addTorsion(*torsion_params) + elif isinstance(force_to_append, openmm.CustomNonbondedForce): + for index in range(force_to_append.getNumParticles()): + nb_params = force_to_append.getParticleParameters(index) + existing_force.addParticle(nb_params) + + elif isinstance(force_to_append, openmm.CustomBondForce): + for index in range(force_to_append.getNumBonds()): + bond_params = force_to_append.getBondParameters(index) + existing_force.addBond(*bond_params) + number_of_appended_forces += 1 if number_of_appended_forces != system_to_append.getNumForces(): From bdae81c43be3365fa97ca86d1355babe52e653cf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 22:29:22 +0000 Subject: [PATCH 20/28] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openff/evaluator/protocols/forcefield.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index e9de75b2..5b72939c 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -159,7 +159,9 @@ def _append_system(existing_system, system_to_append, index_map=None): ) existing_system.addForce(existing_force) elif isinstance(force_to_append, openmm.CustomBondForce): - existing_force = openmm.CustomBondForce(force_to_append.getEnergyFunction()) + existing_force = openmm.CustomBondForce( + force_to_append.getEnergyFunction() + ) for index in range(force_to_append.getNumGlobalParameters()): existing_force.addGlobalParameter( force_to_append.getGlobalParameterName(index), From 7924842c7f0e72e0e1de9972add5ab25bbeb9ad1 Mon Sep 17 00:00:00 2001 From: aehogan Date: Wed, 19 Jul 2023 18:30:29 -0400 Subject: [PATCH 21/28] CustomBondForce's weren't getting added to the system --- openff/evaluator/protocols/forcefield.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index e9de75b2..d376293a 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -158,8 +158,11 @@ def _append_system(existing_system, system_to_append, index_map=None): force_to_append.getPerParticleParameterName(index) ) existing_system.addForce(existing_force) + elif isinstance(force_to_append, openmm.CustomBondForce): - existing_force = openmm.CustomBondForce(force_to_append.getEnergyFunction()) + existing_force = openmm.CustomBondForce( + force_to_append.getEnergyFunction() + ) for index in range(force_to_append.getNumGlobalParameters()): existing_force.addGlobalParameter( force_to_append.getGlobalParameterName(index), @@ -169,6 +172,8 @@ def _append_system(existing_system, system_to_append, index_map=None): existing_force.addPerBondParameter( force_to_append.getPerBondParameterName(index) ) + existing_system.addForce(existing_force) + else: existing_force = type(force_to_append)() existing_system.addForce(existing_force) From 2f8e638fbbe344cbf990eb569fc83a1307cd697f Mon Sep 17 00:00:00 2001 From: aehogan Date: Wed, 19 Jul 2023 18:35:12 -0400 Subject: [PATCH 22/28] CustomBondForce and CustomNonbondedForce weren't getting index mapped properly --- openff/evaluator/protocols/forcefield.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index d376293a..5c197753 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -272,13 +272,17 @@ def _append_system(existing_system, system_to_append, index_map=None): elif isinstance(force_to_append, openmm.CustomNonbondedForce): for index in range(force_to_append.getNumParticles()): - nb_params = force_to_append.getParticleParameters(index) + nb_params = force_to_append.getParticleParameters(index_map[index]) existing_force.addParticle(nb_params) elif isinstance(force_to_append, openmm.CustomBondForce): for index in range(force_to_append.getNumBonds()): - bond_params = force_to_append.getBondParameters(index) - existing_force.addBond(*bond_params) + index_a, index_b, bond_params = force_to_append.getBondParameters(index) + + index_a = index_map[index_a] + index_offset + index_b = index_map[index_b] + index_offset + + existing_force.addBond(index_a, index_b, bond_params) number_of_appended_forces += 1 From 4a59916723c470b4b7da3aa4d58c7a30d06a8dfc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 22:38:15 +0000 Subject: [PATCH 23/28] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openff/evaluator/protocols/forcefield.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index 5c197753..b63e69a4 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -277,7 +277,9 @@ def _append_system(existing_system, system_to_append, index_map=None): elif isinstance(force_to_append, openmm.CustomBondForce): for index in range(force_to_append.getNumBonds()): - index_a, index_b, bond_params = force_to_append.getBondParameters(index) + index_a, index_b, bond_params = force_to_append.getBondParameters( + index + ) index_a = index_map[index_a] + index_offset index_b = index_map[index_b] + index_offset From 5cdb86a08ae1361150566e5be26550bec041b978 Mon Sep 17 00:00:00 2001 From: aehogan Date: Thu, 20 Jul 2023 13:19:29 -0400 Subject: [PATCH 24/28] Add BuildFoyerSystem to EvaluatorClient._default_protocol_replacements --- openff/evaluator/client/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openff/evaluator/client/client.py b/openff/evaluator/client/client.py index 273251a0..4b9c9298 100644 --- a/openff/evaluator/client/client.py +++ b/openff/evaluator/client/client.py @@ -14,6 +14,7 @@ from openff.evaluator.datasets import PhysicalPropertyDataSet from openff.evaluator.forcefield import ( ForceFieldSource, + FoyerForceFieldSource, LigParGenForceFieldSource, ParameterGradientKey, SmirnoffForceFieldSource, @@ -513,6 +514,8 @@ def _default_protocol_replacements(force_field_source): replacements["BaseBuildSystem"] = "BuildLigParGenSystem" elif isinstance(force_field_source, TLeapForceFieldSource): replacements["BaseBuildSystem"] = "BuildTLeapSystem" + elif isinstance(force_field_source, FoyerForceFieldSource): + replacements["BaseBuildSystem"] = "BuildFoyerSystem" return replacements From d12689579add019be34e88711c0c8561a6473b14 Mon Sep 17 00:00:00 2001 From: aehogan Date: Fri, 21 Jul 2023 18:23:58 -0400 Subject: [PATCH 25/28] Set exclusions for CustomNonbondedForce, change foyer xml test to methane, add energy minimization to oplsaa test to ensure openmm successfully runs --- openff/evaluator/protocols/forcefield.py | 28 +++++++++++++--- .../tests/test_protocols/test_forcefield.py | 32 +++++++++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index b63e69a4..5159f6d7 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -68,7 +68,7 @@ class BaseBuildSystem(Protocol, abc.ABC): ) @staticmethod - def _append_system(existing_system, system_to_append, index_map=None): + def _append_system(existing_system, system_to_append, cutoff, index_map=None): """Appends a system object onto the end of an existing system. Parameters @@ -77,6 +77,8 @@ def _append_system(existing_system, system_to_append, index_map=None): The base system to extend. system_to_append: openmm.System The system to append. + cutoff: openff.evaluator.unit.Quantity + The nonbonded cutoff index_map: dict of int and int, optional A map to apply to the indices of atoms in the `system_to_append`. This is predominantly to be used when the ordering of the atoms @@ -148,6 +150,10 @@ def _append_system(existing_system, system_to_append, index_map=None): existing_force = openmm.CustomNonbondedForce( force_to_append.getEnergyFunction() ) + existing_force.setCutoffDistance(cutoff) + existing_force.setNonbondedMethod( + openmm.CustomNonbondedForce.CutoffPeriodic + ) for index in range(force_to_append.getNumGlobalParameters()): existing_force.addGlobalParameter( force_to_append.getGlobalParameterName(index), @@ -275,6 +281,20 @@ def _append_system(existing_system, system_to_append, index_map=None): nb_params = force_to_append.getParticleParameters(index_map[index]) existing_force.addParticle(nb_params) + # Add the 1-2, 1-3 and 1-4 exceptions. + for index in range(force_to_append.getNumExclusions()): + ( + index_a, + index_b, + ) = force_to_append.getExclusionParticles(index) + + index_a = index_map[index_a] + index_b = index_map[index_b] + + existing_force.addExclusion( + index_a + index_offset, index_b + index_offset + ) + elif isinstance(force_to_append, openmm.CustomBondForce): for index in range(force_to_append.getNumBonds()): index_a, index_b, bond_params = force_to_append.getBondParameters( @@ -477,7 +497,7 @@ def _execute(self, directory, available_resources): for index, atom in enumerate(duplicate_molecule.atoms): index_map[atom.molecule_particle_index] = index - self._append_system(system, system_template, index_map) + self._append_system(system, system_template, cutoff, index_map) if openmm_pdb_file.topology.getPeriodicBoxVectors() is not None: system.setDefaultPeriodicBoxVectors( @@ -1149,9 +1169,7 @@ def _parameterize_molecule(self, molecule, force_field_source, cutoff): 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() + interchange.box = [10, 10, 10] * unit.nanometers openmm_system = interchange.to_openmm(combine_nonbonded_forces=False) diff --git a/openff/evaluator/tests/test_protocols/test_forcefield.py b/openff/evaluator/tests/test_protocols/test_forcefield.py index 60eb58b7..6329c438 100644 --- a/openff/evaluator/tests/test_protocols/test_forcefield.py +++ b/openff/evaluator/tests/test_protocols/test_forcefield.py @@ -7,7 +7,9 @@ from openff.toolkit.topology import Molecule from openff.toolkit.utils.rdkit_wrapper import RDKitToolkitWrapper +from openff.units import unit +from openff.evaluator.backends import ComputeResources from openff.evaluator.forcefield import LigParGenForceFieldSource, TLeapForceFieldSource from openff.evaluator.forcefield.forcefield import FoyerForceFieldSource from openff.evaluator.protocols.coordinates import BuildCoordinatesPackmol @@ -17,6 +19,7 @@ BuildSmirnoffSystem, BuildTLeapSystem, ) +from openff.evaluator.protocols.openmm import OpenMMEnergyMinimisation from openff.evaluator.substances import Substance from openff.evaluator.tests.utils import build_tip3p_smirnoff_force_field @@ -167,7 +170,7 @@ def download_callback(_, context): def test_build_foyer_oplsaa_system(): force_field_source = FoyerForceFieldSource("oplsaa") - substance = Substance.from_components("C", "O", "CCC", "C1CCCC1") + substance = Substance.from_components("C") with tempfile.TemporaryDirectory() as directory: force_field_path = path.join(directory, "ff.json") @@ -178,6 +181,7 @@ def test_build_foyer_oplsaa_system(): build_coordinates = BuildCoordinatesPackmol("build_coordinates") build_coordinates.max_molecules = 8 build_coordinates.substance = substance + build_coordinates.mass_density = 0.005 * unit.gram / unit.milliliter build_coordinates.execute(directory) assign_parameters = BuildFoyerSystem("assign_parameters") @@ -187,33 +191,43 @@ def test_build_foyer_oplsaa_system(): assign_parameters.execute(directory) assert path.isfile(assign_parameters.parameterized_system.system_path) + energy_minimisation = OpenMMEnergyMinimisation("energy_minimisation") + energy_minimisation.input_coordinate_file = ( + build_coordinates.coordinate_file_path + ) + energy_minimisation.parameterized_system = ( + assign_parameters.parameterized_system + ) + energy_minimisation.execute(directory, ComputeResources()) + assert path.isfile(energy_minimisation.output_coordinate_file) + 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") + substance = Substance.from_components("C") 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( - """ + """ - - + + - - + + - + - + """ ) From 6024b932ad9dc7a8bd1ddfebd283dba21ab6c80f Mon Sep 17 00:00:00 2001 From: aehogan Date: Mon, 24 Jul 2023 12:39:53 -0400 Subject: [PATCH 26/28] Add openmm energy minimization to foyer XML test to ensure openmm system is built correctly --- .../tests/test_protocols/test_forcefield.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openff/evaluator/tests/test_protocols/test_forcefield.py b/openff/evaluator/tests/test_protocols/test_forcefield.py index 6329c438..4f6542be 100644 --- a/openff/evaluator/tests/test_protocols/test_forcefield.py +++ b/openff/evaluator/tests/test_protocols/test_forcefield.py @@ -233,8 +233,9 @@ def test_build_foyer_xml_system(): ) build_coordinates = BuildCoordinatesPackmol("build_coordinates") - build_coordinates.max_molecules = 8 + build_coordinates.max_molecules = 1 build_coordinates.substance = substance + build_coordinates.mass_density = 0.005 * unit.gram / unit.milliliter build_coordinates.execute(directory) assign_parameters = BuildFoyerSystem("assign_parameters") @@ -243,3 +244,13 @@ def test_build_foyer_xml_system(): assign_parameters.substance = substance assign_parameters.execute(directory) assert path.isfile(assign_parameters.parameterized_system.system_path) + + energy_minimisation = OpenMMEnergyMinimisation("energy_minimisation") + energy_minimisation.input_coordinate_file = ( + build_coordinates.coordinate_file_path + ) + energy_minimisation.parameterized_system = ( + assign_parameters.parameterized_system + ) + energy_minimisation.execute(directory, ComputeResources()) + assert path.isfile(energy_minimisation.output_coordinate_file) From 281e9a5fec0470acf75fb587074b82d963a57864 Mon Sep 17 00:00:00 2001 From: aehogan Date: Mon, 24 Jul 2023 13:07:22 -0400 Subject: [PATCH 27/28] Check energy function of openmm custom forces for existence check --- openff/evaluator/protocols/forcefield.py | 6 ++++++ openff/evaluator/tests/test_protocols/test_forcefield.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openff/evaluator/protocols/forcefield.py b/openff/evaluator/protocols/forcefield.py index 5159f6d7..857ae2d9 100644 --- a/openff/evaluator/protocols/forcefield.py +++ b/openff/evaluator/protocols/forcefield.py @@ -142,6 +142,12 @@ def _append_system(existing_system, system_to_append, cutoff, index_map=None): if type(force_to_append) != type(force): continue + if isinstance( + force_to_append, openmm.CustomNonbondedForce + ) or isinstance(force_to_append, openmm.CustomBondForce): + if force_to_append.getEnergyFunction() != force.getEnergyFunction(): + continue + existing_force = force break diff --git a/openff/evaluator/tests/test_protocols/test_forcefield.py b/openff/evaluator/tests/test_protocols/test_forcefield.py index 4f6542be..b9dc020b 100644 --- a/openff/evaluator/tests/test_protocols/test_forcefield.py +++ b/openff/evaluator/tests/test_protocols/test_forcefield.py @@ -170,7 +170,7 @@ def download_callback(_, context): def test_build_foyer_oplsaa_system(): force_field_source = FoyerForceFieldSource("oplsaa") - substance = Substance.from_components("C") + substance = Substance.from_components("C", "CC", "c1ccccc1", "CC(=O)O") with tempfile.TemporaryDirectory() as directory: force_field_path = path.join(directory, "ff.json") From de3e3746370537d9bdd91a6a5748b839170612d9 Mon Sep 17 00:00:00 2001 From: aehogan Date: Mon, 24 Jul 2023 14:07:42 -0400 Subject: [PATCH 28/28] Update releasehistory.rst --- docs/releasehistory.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/releasehistory.rst b/docs/releasehistory.rst index d6567b47..94d21782 100644 --- a/docs/releasehistory.rst +++ b/docs/releasehistory.rst @@ -11,6 +11,8 @@ Releases follow the ``major.minor.micro`` scheme recommended by Current development ------------------- +* PR `#517 `_: Add support for Foyer forcefields + 0.4.4 - July 24, 2023 ---------------------