From 54326e9a8353ff6595c9b408d1fb9956106043fa Mon Sep 17 00:00:00 2001 From: "Matthew W. Thompson" Date: Mon, 27 Mar 2023 13:01:53 -0500 Subject: [PATCH 1/2] REF: Restructure Amber module --- openff/interchange/components/interchange.py | 4 ++-- openff/interchange/interop/amber/__init__.py | 1 + openff/interchange/interop/amber/export/__init__.py | 1 + .../interop/{internal/amber.py => amber/export/export.py} | 0 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 openff/interchange/interop/amber/__init__.py create mode 100644 openff/interchange/interop/amber/export/__init__.py rename openff/interchange/interop/{internal/amber.py => amber/export/export.py} (100%) diff --git a/openff/interchange/components/interchange.py b/openff/interchange/components/interchange.py index f9918943b..04b34f2f7 100644 --- a/openff/interchange/components/interchange.py +++ b/openff/interchange/components/interchange.py @@ -363,7 +363,7 @@ def to_openmm_topology( def to_prmtop(self, file_path: Union[Path, str], writer="internal"): """Export this Interchange to an Amber .prmtop file.""" if writer == "internal": - from openff.interchange.interop.internal.amber import to_prmtop + from openff.interchange.interop.amber.export import to_prmtop to_prmtop(self, file_path) @@ -403,7 +403,7 @@ def to_crd(self, file_path: Union[Path, str]): def to_inpcrd(self, file_path: Union[Path, str], writer="internal"): """Export this Interchange to an Amber .inpcrd file.""" if writer == "internal": - from openff.interchange.interop.internal.amber import to_inpcrd + from openff.interchange.interop.amber.export import to_inpcrd to_inpcrd(self, file_path) diff --git a/openff/interchange/interop/amber/__init__.py b/openff/interchange/interop/amber/__init__.py new file mode 100644 index 000000000..c5b4654eb --- /dev/null +++ b/openff/interchange/interop/amber/__init__.py @@ -0,0 +1 @@ +"""Interoperability with Amber files.""" diff --git a/openff/interchange/interop/amber/export/__init__.py b/openff/interchange/interop/amber/export/__init__.py new file mode 100644 index 000000000..7c4c1ef11 --- /dev/null +++ b/openff/interchange/interop/amber/export/__init__.py @@ -0,0 +1 @@ +"""Exports to Amber files.""" \ No newline at end of file diff --git a/openff/interchange/interop/internal/amber.py b/openff/interchange/interop/amber/export/export.py similarity index 100% rename from openff/interchange/interop/internal/amber.py rename to openff/interchange/interop/amber/export/export.py From f16f8530610457150f94bae5303ecb3b87c2ed0b Mon Sep 17 00:00:00 2001 From: "Matthew W. Thompson" Date: Mon, 27 Mar 2023 22:18:34 -0500 Subject: [PATCH 2/2] ENH: Create `Topology`s from `.prmtop` files` --- openff/interchange/drivers/openmm.py | 2 +- .../interop/amber/_import/__init__.py | 1 + .../interop/amber/_import/_import.py | 91 +++++++++++++++++++ .../interop/amber/export/__init__.py | 3 +- openff/interchange/smirnoff/_gbsa.py | 3 +- setup.cfg | 1 + 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 openff/interchange/interop/amber/_import/__init__.py create mode 100644 openff/interchange/interop/amber/_import/_import.py diff --git a/openff/interchange/drivers/openmm.py b/openff/interchange/drivers/openmm.py index f0fd9e8f1..ea8871f03 100644 --- a/openff/interchange/drivers/openmm.py +++ b/openff/interchange/drivers/openmm.py @@ -95,7 +95,7 @@ def _get_openmm_energies( positions: openmm.unit.Quantity, round_positions: Optional[int], platform: str, -) -> EnergyReport: +) -> Dict[int, openmm.Force]: """Given prepared `openmm` objects, run a single-point energy calculation.""" for index, force in enumerate(system.getForces()): force.setForceGroup(index) diff --git a/openff/interchange/interop/amber/_import/__init__.py b/openff/interchange/interop/amber/_import/__init__.py new file mode 100644 index 000000000..4ebae99a1 --- /dev/null +++ b/openff/interchange/interop/amber/_import/__init__.py @@ -0,0 +1 @@ +"""Interfaces with Amber.""" diff --git a/openff/interchange/interop/amber/_import/_import.py b/openff/interchange/interop/amber/_import/_import.py new file mode 100644 index 000000000..0ca3098b3 --- /dev/null +++ b/openff/interchange/interop/amber/_import/_import.py @@ -0,0 +1,91 @@ +"""Interfaces with Amber.""" +from typing import TYPE_CHECKING, Dict, List + +if TYPE_CHECKING: + from openff.toolkit import Topology + + from openff.interchange import Interchange + + +def from_prmtop( + file: str, +) -> "Interchange": + """Import from a prmtop file.""" + from openff.interchange import Interchange + + interchange = Interchange() + + data: Dict[str, List[str]] = dict() + + with open(file) as f: + for chunk in f.read().split(r"%FLAG"): + tag, format, *_data = chunk.strip().split() + + if tag == "%VERSION": + continue + + data[tag] = _data + + interchange.topology = _make_topology(data) + + return interchange + + +def _make_topology(data: Dict[str, List[str]]) -> "Topology": + """Make a topology from the data.""" + from openff.toolkit import Topology + from openff.toolkit.topology._mm_molecule import _SimpleMolecule + + Topology._add_bond = _add_bond + + topology = Topology() + + start_index = 0 + + for molecule_index in range(int(data["POINTERS"][11])): + molecule = _SimpleMolecule() + + end_index = start_index + int(data["ATOMS_PER_MOLECULE"][molecule_index]) + + for atom_index in range(start_index, end_index): + # TODO: Check for isotopes (unsupported) or otherwise botches atomic masses + molecule.add_atom( + atomic_number=int(data["ATOMIC_NUMBER"][atom_index]), + name=data["ATOM_NAME"][atom_index], + ) + + topology.add_molecule(molecule) + + start_index = end_index + + bonds: List[str] = data["BONDS_INC_HYDROGEN"] + data["BONDS_WITHOUT_HYDROGEN"] + + # third value in each triplet is an index to the bond type + for n1, n2 in zip(bonds[::3], bonds[1::3]): + # See BONDS_INC_HYDROGEN in https://ambermd.org/prmtop.pdf + # For run-time efficiency, the atom indexes are actually indexes into a coordinate array, + # so the actual atom index A is calculated from the coordinate array index N by A = N/3 + 1 + + a1 = int(int(n1) / 3) + a2 = int(int(n2) / 3) + + topology._add_bond(int(a1), int(a2)) + + return topology + + +def _add_bond(self, atom1_index: int, atom2_index: int): + atom1 = self.atom(atom1_index) + atom2 = self.atom(atom2_index) + + if atom1.molecule is not atom2.molecule: + raise ValueError( + "Cannot add a bond between atoms in different molecules.", + ) + + molecule = atom1.molecule + + molecule.add_bond( + atom1, + atom2, + ) diff --git a/openff/interchange/interop/amber/export/__init__.py b/openff/interchange/interop/amber/export/__init__.py index 7c4c1ef11..10debe88f 100644 --- a/openff/interchange/interop/amber/export/__init__.py +++ b/openff/interchange/interop/amber/export/__init__.py @@ -1 +1,2 @@ -"""Exports to Amber files.""" \ No newline at end of file +"""Exports to Amber files.""" +from openff.interchange.interop.amber.export.export import to_inpcrd, to_prmtop diff --git a/openff/interchange/smirnoff/_gbsa.py b/openff/interchange/smirnoff/_gbsa.py index df4339b46..7f3d0723a 100644 --- a/openff/interchange/smirnoff/_gbsa.py +++ b/openff/interchange/smirnoff/_gbsa.py @@ -6,6 +6,7 @@ from openff.interchange.components.potentials import Potential from openff.interchange.constants import kcal_mol_a2 +from openff.interchange.exceptions import InvalidParameterHandlerError from openff.interchange.smirnoff._base import SMIRNOFFCollection if TYPE_CHECKING: @@ -70,7 +71,7 @@ def store_potentials(self, parameter_handler: GBSAHandler) -> None: self.potentials[potential_key] = potential @classmethod - def create( # type: ignore[override] + def create( cls, parameter_handler: GBSAHandler, topology: "Topology", diff --git a/setup.cfg b/setup.cfg index 78b350cae..a9cb108eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,7 @@ per-file-ignores = openff/interchange/components/*:F821 openff/interchange/__init__.py:F401 openff/interchange/tests/data/*:INP001 + openff/interchange/interop/amber/export/__init__.py:F401 plugins/*:INP001 [isort]