Skip to content

Commit

Permalink
Merge pull request #946 from openforcefield/3fad
Browse files Browse the repository at this point in the history
Support GROMACS's `3fad` virtual sites
  • Loading branch information
mattwthompson authored Apr 8, 2024
2 parents ca26be7 + 160fc58 commit e054343
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 30 deletions.
23 changes: 21 additions & 2 deletions openff/interchange/_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,27 @@ def sage_with_planar_monovalent_carbonyl(sage):
outOfPlaneAngle=Quantity("0 * degree ** 1"),
inPlaneAngle=Quantity("120 * degree ** 1"),
charge_increment1="0.1 * elementary_charge ** 1",
charge_increment2="0.1 * elementary_charge ** 1",
charge_increment3="0.1 * elementary_charge ** 1",
charge_increment2="0.0 * elementary_charge ** 1",
charge_increment3="0.0 * elementary_charge ** 1",
),
)

return sage


@pytest.fixture
def sage_with_sigma_hole(sage):
"""Fixture that loads an SMIRNOFF XML with a C-Cl sigma hole."""
sage.get_parameter_handler("VirtualSites")
sage["VirtualSites"].add_parameter(
parameter=VirtualSiteType(
name="EP",
smirks="[#6:1]-[#17:2]",
distance=1.4 * unit.angstrom,
type="BondCharge",
match="once",
charge_increment1=0.1 * unit.elementary_charge,
charge_increment2=0.2 * unit.elementary_charge,
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
import numpy
import parmed
import pytest
from openff.toolkit import ForceField, Molecule, Topology
from openff.toolkit import ForceField, Molecule, Quantity, Topology, unit
from openff.toolkit.typing.engines.smirnoff import VirtualSiteHandler
from openff.units import unit
from openff.units.openmm import ensure_quantity
from openff.utilities import (
get_data_file_path,
Expand All @@ -24,11 +23,7 @@
from openff.interchange.components.nonbonded import BuckinghamvdWCollection
from openff.interchange.components.potentials import Potential
from openff.interchange.drivers import get_gromacs_energies, get_openmm_energies
from openff.interchange.exceptions import (
GMXMdrunError,
UnsupportedExportError,
VirtualSiteTypeNotImplementedError,
)
from openff.interchange.exceptions import GMXMdrunError, UnsupportedExportError
from openff.interchange.interop.gromacs._import._import import (
_read_box,
_read_coordinates,
Expand Down Expand Up @@ -76,9 +71,12 @@ def test_tip4p_dimer(self, tip4p, water_dimer):
@skip_if_missing("openmm")
@needs_gmx
class TestGROMACSGROFile:
_INTERMOL_PATH = resources.files(
"intermol.tests.gromacs.unit_tests",
)
try:
_INTERMOL_PATH = resources.files(
"intermol.tests.gromacs.unit_tests",
)
except ModuleNotFoundError:
_INTERMOL_PATH = None

@skip_if_missing("intermol")
def test_load_gro(self):
Expand Down Expand Up @@ -499,7 +497,6 @@ def test_common_boxes(self, pdb_file):


@needs_gmx
@pytest.mark.skip("Needs rewrite")
class TestGROMACSVirtualSites:
@pytest.fixture
def sigma_hole_type(self, sage):
Expand Down Expand Up @@ -558,23 +555,19 @@ def test_sigma_hole_example(self, sage_with_sigma_hole):

assert abs(numpy.sum([p.charge for p in gmx_top.atoms])) < 1e-3

def test_carbonyl_example(self, sage_with_monovalent_lone_pair):
"""Test that a single-molecule DivalentLonePair example runs"""
mol = MoleculeWithConformer.from_smiles("C=O", name="Carbon_monoxide")
def test_carbonyl_example(self, sage_with_planar_monovalent_carbonyl, ethanol):
"""Test that a single-molecule planar carbonyl example can run 0 steps."""
ethanol.generate_conformers(n_conformers=1)

out = Interchange.from_smirnoff(
force_field=sage_with_monovalent_lone_pair,
topology=mol.to_topology(),
)
out.box = [4, 4, 4]
out.positions = mol.conformers[0]
hexanal = MoleculeWithConformer.from_smiles("CCCCCC=O")
hexanal._conformers[0] += Quantity("2 nanometer")

with pytest.raises(
VirtualSiteTypeNotImplementedError,
match="MonovalentLonePair not implemented.",
):
# TODO: Sanity-check reported energies
get_gromacs_energies(out)
topology = Topology.from_molecules([ethanol, hexanal])
topology.box_vectors = Quantity([4, 4, 4], "nanometer")

get_gromacs_energies(
sage_with_planar_monovalent_carbonyl.create_interchange(topology),
)

@skip_if_missing("openmm")
def test_tip4p_charge_neutrality(self, tip4p, water_dimer):
Expand Down
15 changes: 15 additions & 0 deletions openff/interchange/interop/gromacs/export/_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
GROMACSSystem,
GROMACSVirtualSite2,
GROMACSVirtualSite3,
GROMACSVirtualSite3fad,
LennardJonesAtomType,
PeriodicImproperDihedral,
PeriodicProperDihedral,
Expand Down Expand Up @@ -247,6 +248,20 @@ def _write_virtual_sites(self, top, molecule_type):
"\n",
)

elif isinstance(gromacs_virtual_site, GROMACSVirtualSite3fad):
top.write("[ virtual_sites3 ]\n")
top.write("; parent, orientation atoms, func, theta, d\n")
top.write(
f"{gromacs_virtual_site.site}\t"
f"{gromacs_virtual_site.orientation_atoms[0]}\t"
f"{gromacs_virtual_site.orientation_atoms[1]}\t"
f"{gromacs_virtual_site.orientation_atoms[2]}\t"
f"{gromacs_virtual_site.func}\t"
f"{gromacs_virtual_site.theta}\t"
f"{gromacs_virtual_site.d}\t"
"\n",
)

else:
raise NotImplementedError()

Expand Down
37 changes: 35 additions & 2 deletions openff/interchange/interop/gromacs/export/_virtual_sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
GROMACSVirtualSite,
GROMACSVirtualSite2,
GROMACSVirtualSite3,
GROMACSVirtualSite3fad,
)
from openff.interchange.models import VirtualSiteKey
from openff.interchange.smirnoff._virtual_sites import (
_BondChargeVirtualSite,
_DivalentLonePairVirtualSite,
_MonovalentLonePairVirtualSite,
_VirtualSite,
)

Expand All @@ -26,10 +28,20 @@ def _create_gromacs_virtual_site(
virtual_site_key: VirtualSiteKey,
particle_map: dict[int | VirtualSiteKey, int],
) -> GROMACSVirtualSite:
offset = interchange.topology.atom_index(
interchange.topology.atom(min(virtual_site_key.orientation_atom_indices)),

# Orientation atom indices are topology indices, but here they need to be indexed as molecule
# indices. Store the difference between an orientation atom's molecule and topology indices.
# (It can probably be any of the orientation atoms.)
parent_atom = interchange.topology.atom(
virtual_site_key.orientation_atom_indices[0],
)

# This lookup scales poorly with system size, but it's not clear how to work around the
# tool's ~O(N) scaling of topology lookups
offset = interchange.topology.atom_index(
parent_atom,
) - parent_atom.molecule.atom_index(parent_atom)

# These are GROMACS "molecule" indices, already mapped back from the topology on to the molecule
gromacs_indices: list[int] = [
particle_map[openff_index] - offset + 1
Expand Down Expand Up @@ -98,4 +110,25 @@ def _create_gromacs_virtual_site(
else:
raise NotImplementedError()

if isinstance(virtual_site, _MonovalentLonePairVirtualSite):
if virtual_site.out_of_plane_angle != 0.0:
raise NotImplementedError(
"Non-zero out-of-plane angles not yet supported in GROMACS export.",
)

# In the plane of three atoms, GROMACS calls this 3fad and gives the example
#
# [ virtual_sites3 ]
# ; Site from funct theta d
# 5 1 2 3 3 120 0.5
# https://manual.gromacs.org/current/reference-manual/topologies/particle-type.html

return GROMACSVirtualSite3fad(
name=virtual_site_key.name,
site=particle_map[virtual_site_key] - offset + 1,
orientation_atoms=gromacs_indices,
theta=virtual_site.in_plane_angle.m_as(unit.degree),
d=virtual_site.distance.m_as(unit.nanometer),
)

raise NotImplementedError()
1 change: 1 addition & 0 deletions openff/interchange/interop/gromacs/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class GROMACSAtom(DefaultModel):
mass: Quantity


# Should the physical values (distance/angles) be float or Quantity?
class GROMACSVirtualSite(DefaultModel):
"""Base class for storing GROMACS virtual sites."""

Expand Down

0 comments on commit e054343

Please sign in to comment.