diff --git a/openff/interchange/_tests/__init__.py b/openff/interchange/_tests/__init__.py index 84d0fee57..d1014631e 100644 --- a/openff/interchange/_tests/__init__.py +++ b/openff/interchange/_tests/__init__.py @@ -76,9 +76,9 @@ class MoleculeWithConformer(Molecule): """Thin wrapper around `Molecule` to produce an instance with a conformer in one call.""" @classmethod - def from_smiles(self, smiles, name="", **kwargs): + def from_smiles(self, smiles, name="", *args, **kwargs): """Create from smiles and generate a single conformer.""" - molecule = super().from_smiles(smiles, **kwargs) + molecule = super().from_smiles(smiles, *args, **kwargs) molecule.generate_conformers(n_conformers=1) molecule.name = name diff --git a/openff/interchange/_tests/_gromacs.py b/openff/interchange/_tests/_gromacs.py new file mode 100644 index 000000000..afa2501ee --- /dev/null +++ b/openff/interchange/_tests/_gromacs.py @@ -0,0 +1,21 @@ +"""Helpers for testing GROMACS interoperability.""" + +from openff.utilities import temporary_cd + +from openff.interchange import Interchange +from openff.interchange.components.mdconfig import get_smirnoff_defaults +from openff.interchange.drivers import get_gromacs_energies + + +def gmx_roundtrip(state: Interchange, apply_smirnoff_defaults: bool = False): + with temporary_cd(): + state.to_gromacs(prefix="state", decimal=8) + new_state = Interchange.from_gromacs(topology_file="state.top", gro_file="state.gro") + + get_smirnoff_defaults(periodic=True).apply(new_state) + + # TODO: More helpful handling of failures, i.e. + # * Detect differences in positions + # * Detect differences in box vectors + # * Detect differences in non-bonded settings + get_gromacs_energies(state).compare(get_gromacs_energies(new_state)) diff --git a/openff/interchange/_tests/conftest.py b/openff/interchange/_tests/conftest.py index 25a2ff70a..33b919399 100644 --- a/openff/interchange/_tests/conftest.py +++ b/openff/interchange/_tests/conftest.py @@ -287,6 +287,21 @@ def gbsa_force_field() -> ForceField: ) +@pytest.fixture +def ff14sb() -> ForceField: + return ForceField("ff14sb_off_impropers_0.0.3.offxml") + + +@pytest.fixture +def ligand(): + return MoleculeWithConformer.from_smiles("CC[C@@](/C=C\\[H])(C=C)O", allow_undefined_stereo=True) + + +@pytest.fixture +def caffeine(): + return MoleculeWithConformer.from_smiles("Cn1cnc2c1c(=O)n(C)c(=O)n2C") + + @pytest.fixture def basic_top() -> Topology: topology = MoleculeWithConformer.from_smiles("C").to_topology() @@ -469,27 +484,33 @@ def ethanol_top(ethanol): @pytest.fixture -def mainchain_ala(): - molecule = Molecule.from_file( - get_data_file_path("proteins/MainChain_ALA.sdf", "openff.toolkit"), +def alanine_dipeptide() -> Topology: + return Topology.from_pdb( + get_data_file_path( + "proteins/MainChain_ALA_ALA.pdb", + "openff.toolkit", + ), ) - molecule._add_default_hierarchy_schemes() - molecule.perceive_residues() - molecule.perceive_hierarchy() - - return molecule @pytest.fixture -def mainchain_arg(): - molecule = Molecule.from_file( - get_data_file_path("proteins/MainChain_ARG.sdf", "openff.toolkit"), - ) - molecule._add_default_hierarchy_schemes() - molecule.perceive_residues() - molecule.perceive_hierarchy() +def mainchain_ala() -> Molecule: + return Topology.from_pdb( + get_data_file_path( + "proteins/MainChain_ALA.pdb", + "openff.toolkit", + ), + ).molecule(0) - return molecule + +@pytest.fixture +def mainchain_arg() -> Molecule: + return Topology.from_pdb( + get_data_file_path( + "proteins/MainChain_ARG.pdb", + "openff.toolkit", + ), + ).molecule(0) @pytest.fixture diff --git a/openff/interchange/_tests/interoperability_tests/gromacs/__init__.py b/openff/interchange/_tests/interoperability_tests/gromacs/__init__.py new file mode 100644 index 000000000..5583a8e05 --- /dev/null +++ b/openff/interchange/_tests/interoperability_tests/gromacs/__init__.py @@ -0,0 +1 @@ +"""Interoperability tests with GROMACS.""" diff --git a/openff/interchange/_tests/interoperability_tests/gromacs/test_systems.py b/openff/interchange/_tests/interoperability_tests/gromacs/test_systems.py new file mode 100644 index 000000000..c7de5da09 --- /dev/null +++ b/openff/interchange/_tests/interoperability_tests/gromacs/test_systems.py @@ -0,0 +1,24 @@ +from openff.toolkit import Quantity + +from openff.interchange._tests._gromacs import gmx_roundtrip + + +def test_ligand_vacuum(caffeine, sage_unconstrained, monkeypatch): + monkeypatch.setenv("INTERCHANGE_EXPERIMENTAL", "1") + + topology = caffeine.to_topology() + topology.box_vectors = Quantity([4, 4, 4], "nanometer") + + gmx_roundtrip(sage_unconstrained.create_interchange(topology)) + + +def test_water_dimer(water_dimer, tip3p, monkeypatch): + monkeypatch.setenv("INTERCHANGE_EXPERIMENTAL", "1") + + gmx_roundtrip(tip3p.create_interchange(water_dimer)) + + +def test_alanine_dipeptide(alanine_dipeptide, ff14sb, monkeypatch): + monkeypatch.setenv("INTERCHANGE_EXPERIMENTAL", "1") + + gmx_roundtrip(ff14sb.create_interchange(alanine_dipeptide)) diff --git a/openff/interchange/_tests/unit_tests/drivers/test_openmm.py b/openff/interchange/_tests/unit_tests/drivers/test_openmm.py index ad275bc62..0e650bb8c 100644 --- a/openff/interchange/_tests/unit_tests/drivers/test_openmm.py +++ b/openff/interchange/_tests/unit_tests/drivers/test_openmm.py @@ -99,10 +99,6 @@ class TestReportWithPlugins: pytest.importorskip("smirnoff_plugins") pytest.importorskip("openeye") - @pytest.fixture - def ligand(self): - return MoleculeWithConformer.from_smiles("CC[C@@](/C=C\\[H])(C=C)O") - @pytest.fixture def de_force_field(self) -> ForceField: return ForceField( diff --git a/openff/interchange/components/mdconfig.py b/openff/interchange/components/mdconfig.py index 7b80b7eba..71cc9cc28 100644 --- a/openff/interchange/components/mdconfig.py +++ b/openff/interchange/components/mdconfig.py @@ -462,12 +462,15 @@ def _infer_constraints(interchange: "Interchange") -> str: if num_constraints == num_h_bonds: return "h-bonds" - elif num_constraints == len(interchange["Bonds"].key_map): + elif num_constraints == num_bonds: return "all-bonds" elif num_constraints == (num_bonds + num_angles): return "all-angles" else: + # TODO: Rigid waters may not have bond and angle parameters, but still have 3 constraints + # per molecule. There should be a better way to process these, but it's non-trivial + # to detect water molecules in a performance, scalable way without false positives. warnings.warn( "Ambiguous failure while processing constraints. Constraining h-bonds as a stopgap.", ) diff --git a/openff/interchange/drivers/__init__.py b/openff/interchange/drivers/__init__.py index ea5fe7edf..fd659d35a 100644 --- a/openff/interchange/drivers/__init__.py +++ b/openff/interchange/drivers/__init__.py @@ -5,8 +5,10 @@ from openff.interchange.drivers.gromacs import get_gromacs_energies from openff.interchange.drivers.lammps import get_lammps_energies from openff.interchange.drivers.openmm import get_openmm_energies +from openff.interchange.drivers.report import EnergyReport __all__ = [ + "EnergyReport", "get_all_energies", "get_amber_energies", "get_gromacs_energies",