From 1505a4645e159c0e634ce465fdcfe56789a5f28f Mon Sep 17 00:00:00 2001 From: Josh Horton Date: Fri, 24 Feb 2023 14:49:45 +0000 Subject: [PATCH 1/9] add openmm energy tests --- deforcefields/data/ethanol.sdf | 26 +++++++++++ deforcefields/data/water.sdf | 11 +++++ deforcefields/tests/conftest.py | 41 ++++++++++++++++++ deforcefields/tests/test_deforcefields.py | 53 ++++++++++++++++++++--- 4 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 deforcefields/data/ethanol.sdf create mode 100644 deforcefields/data/water.sdf create mode 100644 deforcefields/tests/conftest.py diff --git a/deforcefields/data/ethanol.sdf b/deforcefields/data/ethanol.sdf new file mode 100644 index 0000000..5723e56 --- /dev/null +++ b/deforcefields/data/ethanol.sdf @@ -0,0 +1,26 @@ + + -OEChem-02242314133D + + 9 8 0 0 0 0 0 0 0999 V2000 + 1.0616 -0.2681 -0.0006 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.9101 0.9126 -0.4220 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.5170 1.3236 -1.7228 O 0 0 0 0 0 0 0 0 0 0 0 0 + 1.3393 -0.6108 1.0001 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.0000 -0.0002 -0.0006 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1.1801 -1.0990 -0.7040 H 0 0 0 0 0 0 0 0 0 0 0 0 + 2.9684 0.6361 -0.4446 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1.7730 1.7499 0.2687 H 0 0 0 0 0 0 0 0 0 0 0 0 + 2.0787 2.0794 -1.9612 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 2 3 1 0 0 0 0 + 1 4 1 0 0 0 0 + 1 5 1 0 0 0 0 + 1 6 1 0 0 0 0 + 2 7 1 0 0 0 0 + 2 8 1 0 0 0 0 + 3 9 1 0 0 0 0 +M END +> +-0.097100 0.131430 -0.601340 0.044760 0.044760 0.044760 0.017320 0.017320 0.398090 + +$$$$ diff --git a/deforcefields/data/water.sdf b/deforcefields/data/water.sdf new file mode 100644 index 0000000..03f91ae --- /dev/null +++ b/deforcefields/data/water.sdf @@ -0,0 +1,11 @@ + + -OEChem-02242314343D + + 3 2 0 0 0 0 0 0 0999 V2000 + 0.0611 0.3887 0.0589 O 0 0 0 0 0 0 0 0 0 0 0 0 + 0.7238 -0.3116 -0.0382 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.7849 -0.0770 -0.0207 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 3 1 0 0 0 0 +M END +$$$$ diff --git a/deforcefields/tests/conftest.py b/deforcefields/tests/conftest.py new file mode 100644 index 0000000..d2a254e --- /dev/null +++ b/deforcefields/tests/conftest.py @@ -0,0 +1,41 @@ +import os.path + +import pytest +from openff.toolkit.topology import Molecule + + +def get_data(relative_path: str) -> str: + """ + Get the file path to some data in the package for testing. + + Args: + relative_path: The relative path of the file to be loaded from deforcefields/data + + Returns: + The absolute path to the requested file in deforcefields/data. + """ + from pkg_resources import resource_filename + + file_name = resource_filename("deforcefields", os.path.join("data", relative_path)) + if not os.path.exists(file_name): + raise ValueError( + f"{relative_path} does not exist. If you have just added it, you'll have to re-install." + ) + return file_name + + +@pytest.fixture() +def ethanol_with_charges() -> Molecule: + """ + Return and OpenFF Molecule model of ethanol with `am1bccelf10` charges computed with openeye + """ + ethanol = Molecule.from_file(get_data("ethanol.sdf")) + return ethanol + + +@pytest.fixture() +def water() -> Molecule: + """ + Return an OpenFF Molecule model of water with no charges. + """ + return Molecule.from_file(get_data("water.sdf")) diff --git a/deforcefields/tests/test_deforcefields.py b/deforcefields/tests/test_deforcefields.py index a7f9556..652a65c 100644 --- a/deforcefields/tests/test_deforcefields.py +++ b/deforcefields/tests/test_deforcefields.py @@ -1,11 +1,13 @@ """ Test loading DE-Force fields via the plugin interface through the toolkit. """ -import openmm import pytest -from openff.toolkit.topology import Molecule from openff.toolkit.typing.engines.smirnoff import ForceField -from openmm import unit +from openmm import openmm, unit +from smirnoff_plugins.utilities.openmm import ( + evaluate_energy, + evaluate_water_energy_at_distances, +) @pytest.mark.parametrize( @@ -15,15 +17,14 @@ pytest.param("de-force_unconstrained-1.0.0.offxml", id="Constraints"), ], ) -def test_load_de_ff(forcefield): +def test_load_de_ff(forcefield, ethanol_with_charges): """ Load the DE FF and create an OpenMM system. """ ff = ForceField(forcefield, load_plugins=True) - ethanol = Molecule.from_smiles("CCO") - system = ff.create_openmm_system(topology=ethanol.to_topology()) + system = ff.create_openmm_system(topology=ethanol_with_charges.to_topology()) forces = {force.__class__.__name__: force for force in system.getForces()} @@ -44,3 +45,43 @@ def test_load_de_ff(forcefield): epsilon, _ = custom_force.getParticleParameters(i) # no units with our custom force assert epsilon > 0 + + +@pytest.mark.parametrize( + "forcefield, ref_energy", + [ + pytest.param("de-force-1.0.0.offxml", 13.601144438830156, id="No constraints"), + pytest.param( + "de-force_unconstrained-1.0.0.offxml", 13.605201859835375, id="Constraints" + ), + ], +) +def test_energy_no_sites(forcefield, ref_energy, ethanol_with_charges): + """ + Test calculating the single point energy of ethanol using constrained and unconstrained DE-FF with pre-computed + partial charges from openeye. + """ + + ff = ForceField(forcefield, load_plugins=True) + + off_top = ethanol_with_charges.to_topology() + openmm_top = off_top.to_openmm() + system = ff.create_openmm_system( + topology=off_top, charge_from_molecules=[ethanol_with_charges] + ) + energy = evaluate_energy( + system=system, topology=openmm_top, positions=ethanol_with_charges.conformers[0] + ) + assert ref_energy == pytest.approx(energy) + + +def test_energy_sites(): + """ + Test calculating the energy for a system with two waters with virtual sites at set distances. + """ + + ff = ForceField("de-force-1.0.0.offxml", load_plugins=True) + energies = evaluate_water_energy_at_distances(force_field=ff, distances=[2, 3, 4]) + ref_values = [728.5424499511719, 35.060861110687256, 9.134421169757843] + for i, energy in enumerate(energies): + assert energy == pytest.approx(ref_values[i]) From 6388f1086f6ec70f399105a09c3901ff622455ae Mon Sep 17 00:00:00 2001 From: Josh Horton Date: Fri, 24 Feb 2023 14:57:51 +0000 Subject: [PATCH 2/9] use smirnoff-main --- devtools/conda-envs/test_env.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 73018cc..2fae548 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -12,4 +12,6 @@ dependencies: # Core-deps - openff-toolkit >=0.10.6, <0.11 - - smirnoff-plugins \ No newline at end of file +# - smirnoff-plugins + - pip: + - git+https://github.com/jthorton/de-forcefields From 3af70a8522ca0ef22f2e76f675ccb00073ba4f6c Mon Sep 17 00:00:00 2001 From: Josh Horton Date: Fri, 24 Feb 2023 14:58:33 +0000 Subject: [PATCH 3/9] use smirnoff-plugins main --- devtools/conda-envs/test_env.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 2fae548..00d1934 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -14,4 +14,4 @@ dependencies: - openff-toolkit >=0.10.6, <0.11 # - smirnoff-plugins - pip: - - git+https://github.com/jthorton/de-forcefields + - git+https://github.com/openforcefield/smirnoff-plugins From c72541e292468050715ec5467b3d44d38d2b8f16 Mon Sep 17 00:00:00 2001 From: Josh Horton Date: Fri, 24 Feb 2023 15:05:26 +0000 Subject: [PATCH 4/9] regenerate reference energies using rdkit positions --- deforcefields/tests/test_deforcefields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deforcefields/tests/test_deforcefields.py b/deforcefields/tests/test_deforcefields.py index 652a65c..e4208fb 100644 --- a/deforcefields/tests/test_deforcefields.py +++ b/deforcefields/tests/test_deforcefields.py @@ -82,6 +82,6 @@ def test_energy_sites(): ff = ForceField("de-force-1.0.0.offxml", load_plugins=True) energies = evaluate_water_energy_at_distances(force_field=ff, distances=[2, 3, 4]) - ref_values = [728.5424499511719, 35.060861110687256, 9.134421169757843] + ref_values = [1005.0846252441406, 44.696786403656006, 10.453390896320343] for i, energy in enumerate(energies): assert energy == pytest.approx(ref_values[i]) From 085594ac635247df1c22f14544616fc9376cbf22 Mon Sep 17 00:00:00 2001 From: Josh Horton Date: Fri, 24 Feb 2023 16:13:34 +0000 Subject: [PATCH 5/9] update smirnoff-plugins version --- devtools/conda-envs/test_env.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 00d1934..13adbc1 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -12,6 +12,4 @@ dependencies: # Core-deps - openff-toolkit >=0.10.6, <0.11 -# - smirnoff-plugins - - pip: - - git+https://github.com/openforcefield/smirnoff-plugins + - smirnoff-plugins >=0.0.3 From 8bd6f10204ba5a6e0ba42550180641bf0980b760 Mon Sep 17 00:00:00 2001 From: Josh Horton Date: Fri, 24 Feb 2023 16:30:28 +0000 Subject: [PATCH 6/9] trigger CI --- devtools/conda-envs/test_env.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 13adbc1..7747f15 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -12,4 +12,4 @@ dependencies: # Core-deps - openff-toolkit >=0.10.6, <0.11 - - smirnoff-plugins >=0.0.3 + - smirnoff-plugins >=0.0.3 \ No newline at end of file From 009ace0a89a0f9638f34e07c0eb2f0f6c060c4f4 Mon Sep 17 00:00:00 2001 From: Josh Horton Date: Fri, 24 Feb 2023 16:31:41 +0000 Subject: [PATCH 7/9] Revert "trigger CI" This reverts commit 8bd6f10204ba5a6e0ba42550180641bf0980b760. --- devtools/conda-envs/test_env.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 7747f15..13adbc1 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -12,4 +12,4 @@ dependencies: # Core-deps - openff-toolkit >=0.10.6, <0.11 - - smirnoff-plugins >=0.0.3 \ No newline at end of file + - smirnoff-plugins >=0.0.3 From 050409e1387b2e5fd428ab62cb453de97c52e2a3 Mon Sep 17 00:00:00 2001 From: "Matthew W. Thompson" Date: Fri, 8 Mar 2024 09:51:06 -0600 Subject: [PATCH 8/9] Update energy tests --- deforcefields/tests/test_deforcefields.py | 93 +++++++++++++++++------ devtools/conda-envs/test_env.yaml | 4 +- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/deforcefields/tests/test_deforcefields.py b/deforcefields/tests/test_deforcefields.py index c5ceae4..b111153 100644 --- a/deforcefields/tests/test_deforcefields.py +++ b/deforcefields/tests/test_deforcefields.py @@ -1,16 +1,13 @@ """ Test loading DE-Force fields via the plugin interface through the toolkit. """ - +import numpy import openmm import pytest +from openff.interchange.constants import kj_mol from openff.interchange.drivers.openmm import get_openmm_energies -from openff.toolkit import ForceField, Molecule +from openff.toolkit import ForceField, Molecule, Quantity, Topology from openmm import unit -from smirnoff_plugins.utilities.openmm import ( - evaluate_energy, - evaluate_water_energy_at_distances, -) @pytest.mark.parametrize( @@ -27,9 +24,9 @@ def test_load_de_ff(forcefield, ethanol_with_charges): ff = ForceField(forcefield, load_plugins=True) - system = ff.create_interchange(topology=ethanol_with_charges.to_topology()).to_openmm( - combine_nonbonded_forces=False, - ) + system = ff.create_interchange( + topology=ethanol_with_charges.to_topology(), + ).to_openmm(combine_nonbonded_forces=False) forces = {force.__class__.__name__: force for force in system.getForces()} @@ -97,9 +94,9 @@ def test_fails_unsupported_chemistry(): @pytest.mark.parametrize( "forcefield, ref_energy", [ - pytest.param("de-force-1.0.0.offxml", 13.601144438830156, id="No constraints"), + pytest.param("de-force-1.0.2.offxml", 13.601144438830156, id="No constraints"), pytest.param( - "de-force_unconstrained-1.0.0.offxml", 13.605201859835375, id="Constraints" + "de-force_unconstrained-1.0.2.offxml", 13.605201859835375, id="Constraints" ), ], ) @@ -111,24 +108,72 @@ def test_energy_no_sites(forcefield, ref_energy, ethanol_with_charges): ff = ForceField(forcefield, load_plugins=True) - off_top = ethanol_with_charges.to_topology() - openmm_top = off_top.to_openmm() - system = ff.create_openmm_system( - topology=off_top, charge_from_molecules=[ethanol_with_charges] + interchange = ff.create_interchange( + topology=ethanol_with_charges.to_topology(), + charge_from_molecules=[ethanol_with_charges], ) - energy = evaluate_energy( - system=system, topology=openmm_top, positions=ethanol_with_charges.conformers[0] - ) - assert ref_energy == pytest.approx(energy) + + found_energy = get_openmm_energies( + interchange, + combine_nonbonded_forces=False, + ).total_energy.m_as(kj_mol) + + assert found_energy == pytest.approx(ref_energy) +def evaluate_water_energy_at_distances( + force_field: ForceField, + distances: list[float], +) -> list[Quantity]: + """ + Evaluate a water dimer at specified distances (in Angstrom). + + Taken from smirnoff_plugins.utilities.openmm, which collates virtual particles + between molecules. Interchange (with OpenMM) puts all virtual sites at the END + of the topology; mismatching these causes NaNs. + """ + + water = Molecule.from_smiles("O") + water.generate_conformers(n_conformers=1) + topology = Topology.from_molecules([water, water]) + topology.box_vectors = unit.Quantity(numpy.eye(3) * 20, unit.nanometer) + + energies = list() + + for distance in distances: + topology.set_positions( + numpy.vstack( + [ + water.conformers[0], + water.conformers[0] + Quantity( + numpy.array([distance, 0, 0]), + "angstrom", + ), + ], + ), + ) + + energies.append( + get_openmm_energies( + force_field.create_interchange(topology), + combine_nonbonded_forces=False, + ).total_energy + ) + + return energies + def test_energy_sites(): """ Test calculating the energy for a system with two waters with virtual sites at set distances. """ - ff = ForceField("de-force-1.0.0.offxml", load_plugins=True) - energies = evaluate_water_energy_at_distances(force_field=ff, distances=[2, 3, 4]) - ref_values = [1005.0846252441406, 44.696786403656006, 10.453390896320343] - for i, energy in enumerate(energies): - assert energy == pytest.approx(ref_values[i]) + ff = ForceField("de-force-1.0.2.offxml", load_plugins=True) + + found_energies = evaluate_water_energy_at_distances( + force_field=ff, + distances=[2, 3, 4], + ) + ref_energies = [1005.0846252441406, 44.696786403656006, 10.453390896320343] + + for found, ref in zip(found_energies, ref_energies): + assert found.m_as(kj_mol) == pytest.approx(ref, rel=1e-3) diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 8cc60ec..d3762be 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -11,8 +11,8 @@ dependencies: - pytest # Core-deps - - openff-toolkit >=0.14.3 - - openff-interchange >=0.3.18 + - openff-toolkit >=0.15 + - openff-interchange >=0.3.23 # the above two constraints are pulled in by smirnoff-plugins; # could just drop them, but maybe it's better to be explicit - smirnoff-plugins >=2024.01.0 From c9e2fbdced0db45ca490ac317c5a349e594689fd Mon Sep 17 00:00:00 2001 From: "Matthew W. Thompson" Date: Fri, 8 Mar 2024 10:01:44 -0600 Subject: [PATCH 9/9] Parametrize over distances --- deforcefields/tests/test_deforcefields.py | 62 +++++++++++------------ 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/deforcefields/tests/test_deforcefields.py b/deforcefields/tests/test_deforcefields.py index b111153..bc6db20 100644 --- a/deforcefields/tests/test_deforcefields.py +++ b/deforcefields/tests/test_deforcefields.py @@ -1,6 +1,7 @@ """ Test loading DE-Force fields via the plugin interface through the toolkit. """ + import numpy import openmm import pytest @@ -121,9 +122,9 @@ def test_energy_no_sites(forcefield, ref_energy, ethanol_with_charges): assert found_energy == pytest.approx(ref_energy) -def evaluate_water_energy_at_distances( +def evaluate_water_energy_at_distance( force_field: ForceField, - distances: list[float], + distance: float, ) -> list[Quantity]: """ Evaluate a water dimer at specified distances (in Angstrom). @@ -138,42 +139,39 @@ def evaluate_water_energy_at_distances( topology = Topology.from_molecules([water, water]) topology.box_vectors = unit.Quantity(numpy.eye(3) * 20, unit.nanometer) - energies = list() - - for distance in distances: - topology.set_positions( - numpy.vstack( - [ - water.conformers[0], - water.conformers[0] + Quantity( - numpy.array([distance, 0, 0]), - "angstrom", - ), - ], - ), - ) - - energies.append( - get_openmm_energies( - force_field.create_interchange(topology), - combine_nonbonded_forces=False, - ).total_energy - ) - - return energies - -def test_energy_sites(): + topology.set_positions( + numpy.vstack( + [ + water.conformers[0], + water.conformers[0] + + Quantity( + numpy.array([distance, 0, 0]), + "angstrom", + ), + ], + ), + ) + + return get_openmm_energies( + force_field.create_interchange(topology), + combine_nonbonded_forces=False, + ).total_energy + + +@pytest.mark.parametrize( + ("distance", "ref_energy"), + [(2, 1005.0846252441406), (3, 44.696786403656006), (4, 10.453390896320343)], +) +def test_energy_sites(distance, ref_energy): """ Test calculating the energy for a system with two waters with virtual sites at set distances. """ ff = ForceField("de-force-1.0.2.offxml", load_plugins=True) - found_energies = evaluate_water_energy_at_distances( + found_energy = evaluate_water_energy_at_distance( force_field=ff, - distances=[2, 3, 4], + distance=distance, ) - ref_energies = [1005.0846252441406, 44.696786403656006, 10.453390896320343] - for found, ref in zip(found_energies, ref_energies): - assert found.m_as(kj_mol) == pytest.approx(ref, rel=1e-3) + assert found_energy.m_as(kj_mol) == pytest.approx(ref_energy, abs=0.01)