From 4f00c99a611bcbfef9a2837b55598c9adad8b2c1 Mon Sep 17 00:00:00 2001 From: Co Quach <43968221+daico007@users.noreply.github.com> Date: Wed, 9 Nov 2022 10:55:09 -0600 Subject: [PATCH] Restraint support for GROMACS top writer (#685) * add ability to write angle and dihedral restraint for gromacs top format * fix typo in dihedral, add docs for angle restraints * add #ifdef DIHRES for dihedral restrain section * change var name, better handling of scaling factors for top writer * fix tab for top writer * fix typo * better parsing unique molecules * reformat top writer to work properly with new changes * add test files for top and gro writer, make gro write out res info * adjust unit tests * add precision and adjust unit test * adjustments to element parsing, spacing of writing out gro file, speed up writer compatibility check * fix minor bugs * reformat top writer, add simplify_check for compatability check * fix unit test, add sanity check for top writer * truncating site name in gro writer * better handling use_molecule_info speedup during atomtyping * Add test with restraints * fix bug related to restraints section * update test, add more docs about angle and dihedral restraints, make top writer refer to element from atomtype (work better for non-atomistic * add more rigorous test for top and gro file * add write gro test * combine benzene ua and aa test * add harmonic bond restraint for bond class and top writer * add unit test for bond restraints (harmonic) * update test file * fix typo and removed unused imports * fixing unit tests --- gmso/core/angle.py | 17 + gmso/core/atom_type.py | 1 + gmso/core/bond.py | 16 + gmso/core/dihedral.py | 17 + gmso/external/convert_foyer_xml.py | 14 +- gmso/external/convert_mbuild.py | 6 +- gmso/external/convert_parmed.py | 5 + gmso/formats/gro.py | 87 ++-- gmso/formats/top.py | 486 +++++++++++++----- .../topology_parameterizer.py | 2 +- gmso/tests/base_test.py | 77 ++- gmso/tests/files/benzene.gro | 63 +++ gmso/tests/files/benzene.top | 99 ++++ gmso/tests/files/benzene_trappe-ua.xml | 17 + gmso/tests/files/restrained_benzene_ua.gro | 33 ++ gmso/tests/files/restrained_benzene_ua.top | 85 +++ gmso/tests/test_convert_foyer_xml.py | 15 +- gmso/tests/test_gro.py | 54 +- gmso/tests/test_top.py | 104 +++- gmso/utils/compatibility.py | 30 +- gmso/utils/io.py | 8 - 21 files changed, 1007 insertions(+), 229 deletions(-) create mode 100644 gmso/tests/files/benzene.gro create mode 100644 gmso/tests/files/benzene.top create mode 100755 gmso/tests/files/benzene_trappe-ua.xml create mode 100644 gmso/tests/files/restrained_benzene_ua.gro create mode 100644 gmso/tests/files/restrained_benzene_ua.top diff --git a/gmso/core/angle.py b/gmso/core/angle.py index 12a7979b5..17e62ba09 100644 --- a/gmso/core/angle.py +++ b/gmso/core/angle.py @@ -31,6 +31,16 @@ class Angle(Connection): default=None, description="AngleType of this angle." ) + restraint_: Optional[dict] = Field( + default=None, + description=""" + Restraint for this angle, must be a dict with the following keys: + 'k' (unit of energy/mol), 'theta_eq' (unit of angle), 'n' (multiplicity, unitless). + Refer to https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html + for more information. + """, + ) + @property def angle_type(self): """Return the angle type if the angle is parametrized.""" @@ -41,6 +51,11 @@ def connection_type(self): """Return the angle type if the angle is parametrized.""" return self.__dict__.get("angle_type_") + @property + def restraint(self): + """Return the restraint of this angle.""" + return self.__dict__.get("restraint_") + def equivalent_members(self): """Return a set of the equivalent connection member tuples. @@ -74,8 +89,10 @@ class Config: fields = { "connection_members_": "connection_members", "angle_type_": "angle_type", + "restraint_": "restraint", } alias_to_fields = { "connection_members": "connection_members_", "angle_type": "angle_type_", + "restraint": "restraint_", } diff --git a/gmso/core/atom_type.py b/gmso/core/atom_type.py index 4b48d5b27..cdee3bd80 100644 --- a/gmso/core/atom_type.py +++ b/gmso/core/atom_type.py @@ -132,6 +132,7 @@ def clone(self, fast_copy=False): """Clone this AtomType, faster alternative to deepcopying.""" return AtomType( name=str(self.name), + tags=self.tags, expression=None, parameters=None, independent_variables=None, diff --git a/gmso/core/bond.py b/gmso/core/bond.py index 67c5f082a..23dd7fe4e 100644 --- a/gmso/core/bond.py +++ b/gmso/core/bond.py @@ -30,6 +30,15 @@ class Bond(Connection): bond_type_: Optional[BondType] = Field( default=None, description="BondType of this bond." ) + restraint_: Optional[dict] = Field( + default=None, + description=""" + Restraint for this bond, must be a dict with the following keys: + 'b0' (unit of length), 'kb' (unit of energy/(mol * length**2)). + Refer to https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html + for more information. + """, + ) @property def bond_type(self): @@ -42,6 +51,11 @@ def connection_type(self): # ToDo: Deprecate this? return self.__dict__.get("bond_type_") + @property + def restraint(self): + """Return the restraint of this bond.""" + return self.__dict__.get("restraint_") + def equivalent_members(self): """Get a set of the equivalent connection member tuples. @@ -75,8 +89,10 @@ class Config: fields = { "bond_type_": "bond_type", "connection_members_": "connection_members", + "restraint_": "restraint", } alias_to_fields = { "bond_type": "bond_type_", "connection_members": "connection_members_", + "restraint": "restraint_", } diff --git a/gmso/core/dihedral.py b/gmso/core/dihedral.py index 430d90460..97859bc52 100644 --- a/gmso/core/dihedral.py +++ b/gmso/core/dihedral.py @@ -35,6 +35,16 @@ class Dihedral(Connection): default=None, description="DihedralType of this dihedral." ) + restraint_: Optional[dict] = Field( + default=None, + description=""" + Restraint for this dihedral, must be a dict with the following keys: + 'k' (unit of energy/(mol * angle**2)), 'phi_eq' (unit of angle), 'delta_phi' (unit of angle). + Refer to https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html + for more information. + """, + ) + @property def dihedral_type(self): return self.__dict__.get("dihedral_type_") @@ -44,6 +54,11 @@ def connection_type(self): # ToDo: Deprecate this? return self.__dict__.get("dihedral_type_") + @property + def restraint(self): + """Return the restraint of this dihedral.""" + return self.__dict__.get("restraint_") + def equivalent_members(self): """Get a set of the equivalent connection member tuples @@ -74,8 +89,10 @@ class Config: fields = { "dihedral_type_": "dihedral_type", "connection_members_": "connection_members", + "restraint_": "restraint", } alias_to_fields = { "dihedral_type": "dihedral_type_", "connection_members": "connection_members_", + "restraint": "restraint_", } diff --git a/gmso/external/convert_foyer_xml.py b/gmso/external/convert_foyer_xml.py index 5ce665668..3e0996739 100644 --- a/gmso/external/convert_foyer_xml.py +++ b/gmso/external/convert_foyer_xml.py @@ -529,16 +529,6 @@ def _create_sub_element(root_el, name, attrib_dict=None): def _validate_foyer(xml_path): - import warnings + from foyer.validator import Validator - from gmso.utils.io import has_foyer - - if not has_foyer: - warnings.warn( - "Cannot validate the xml using foyer, since foyer is not installed." - "Please install foyer using conda install -c conda-forge foyer." - ) - else: - from foyer.validator import Validator - - Validator(xml_path) + Validator(xml_path) diff --git a/gmso/external/convert_mbuild.py b/gmso/external/convert_mbuild.py index c87cafe06..d6dc4f5bc 100644 --- a/gmso/external/convert_mbuild.py +++ b/gmso/external/convert_mbuild.py @@ -250,11 +250,7 @@ def _parse_particle(particle_map, site): def _parse_site(site_map, particle, search_method): """Parse information for a gmso.Site from a mBuild.Compound adn add it to the site map.""" pos = particle.xyz[0] * u.nm - ele = ( - search_method(particle.element.symbol) - if particle.element - else search_method(particle.name) - ) + ele = search_method(particle.element.symbol) if particle.element else None charge = particle.charge * u.elementary_charge if particle.charge else None mass = particle.mass * u.amu if particle.mass else None diff --git a/gmso/external/convert_parmed.py b/gmso/external/convert_parmed.py index 3f6a8f71c..027a58d5f 100644 --- a/gmso/external/convert_parmed.py +++ b/gmso/external/convert_parmed.py @@ -256,9 +256,14 @@ def _atom_types_from_pmd(structure): unique_atom_types = list(unique_atom_types) pmd_top_atomtypes = {} for atom_type in unique_atom_types: + if atom_type.atomic_number: + element = element_by_atomic_number(atom_type.atomic_number).symbol + else: + element = atom_type.name top_atomtype = gmso.AtomType( name=atom_type.name, charge=atom_type.charge * u.elementary_charge, + tags={"element": element}, expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)", parameters={ "sigma": atom_type.sigma * u.angstrom, diff --git a/gmso/formats/gro.py b/gmso/formats/gro.py index af6715579..8d8aa76a5 100644 --- a/gmso/formats/gro.py +++ b/gmso/formats/gro.py @@ -1,5 +1,6 @@ """Read and write Gromos87 (.GRO) file format.""" import datetime +import re import warnings import numpy as np @@ -10,7 +11,6 @@ from gmso.core.atom import Atom from gmso.core.box import Box from gmso.core.topology import Topology -from gmso.exceptions import NotYetImplementedWarning from gmso.formats.formats_registry import loads_as, saves_as @@ -57,6 +57,7 @@ def read_gro(filename): coords = u.nm * np.zeros(shape=(n_atoms, 3)) for row, _ in enumerate(coords): line = gro_file.readline() + content = line.split() if not line: msg = ( "Incorrect number of lines in .gro file. Based on the " @@ -64,18 +65,23 @@ def read_gro(filename): "atoms were expected, but at least one fewer was found." ) raise ValueError(msg.format(n_atoms)) - resid = int(line[:5]) - res_name = line[5:10] - atom_name = line[10:15] - atom_id = int(line[15:20]) + + res = content[0] + atom_name = content[1] + atom_id = content[2] coords[row] = u.nm * np.array( [ - float(line[20:28]), - float(line[28:36]), - float(line[36:44]), + float(content[3]), + float(content[4]), + float(content[5]), ] ) site = Atom(name=atom_name, position=coords[row]) + + r = re.compile("([0-9]+)([a-zA-Z]+)") + m = r.match(res) + site.molecule = (m.group(2), int(m.group(1)) - 1) + site.residue = (m.group(2), int(m.group(1)) - 1) top.add_site(site, update_types=False) top.update_topology() @@ -97,7 +103,7 @@ def read_gro(filename): @saves_as(".gro") -def write_gro(top, filename): +def write_gro(top, filename, precision=3): """Write a topology to a gro file. The Gromos87 (gro) format is a common plain text structure file used @@ -113,7 +119,8 @@ def write_gro(top, filename): The `topology` to write out to the gro file. filename : str or file object The location and name of file to save to disk. - + precision : int, optional, default=3 + The number of sig fig to write out the position in. Notes ----- @@ -135,7 +142,7 @@ def write_gro(top, filename): ) ) out_file.write("{:d}\n".format(top.n_sites)) - out_file.write(_prepare_atoms(top, pos_array)) + out_file.write(_prepare_atoms(top, pos_array, precision)) out_file.write(_prepare_box(top)) @@ -154,30 +161,44 @@ def _validate_positions(pos_array): return pos_array -def _prepare_atoms(top, updated_positions): +def _prepare_atoms(top, updated_positions, precision): out_str = str() + warnings.warn( + "Residue information is parsed from site.molecule," + "or site.residue if site.molecule does not exist." + "Note that the residue idx will be bump by 1 since GROMACS utilize 1-index." + ) for idx, (site, pos) in enumerate(zip(top.sites, updated_positions)): - warnings.warn( - "Residue information is not currently " - "stored or written to GRO files.", - NotYetImplementedWarning, - ) - # TODO: assign residues - res_id = 1 - res_name = "X" - atom_name = site.name + if site.molecule: + res_id = site.molecule.number + 1 + res_name = site.molecule.name + elif site.residue: + res_id = site.residue.number + 1 + res_name = site.molecule.name[:3] + else: + res_id = 1 + res_name = "MOL" + if len(res_name) > 3: + res_name = res_name[:3] + + atom_name = site.name if len(site.name) <= 3 else site.name[:3] atom_id = idx + 1 - out_str = ( - out_str - + "{0:5d}{1:5s}{2:5s}{3:5d}{4:8.3f}{5:8.3f}{6:8.3f}\n".format( - res_id, - res_name, - atom_name, - atom_id, - pos[0].in_units(u.nm).value, - pos[1].in_units(u.nm).value, - pos[2].in_units(u.nm).value, - ) + + varwidth = 5 + precision + crdfmt = f"{{:{varwidth}.{precision}f}}" + + # preformat pos str + crt_x = crdfmt.format(pos[0].in_units(u.nm).value)[:varwidth] + crt_y = crdfmt.format(pos[1].in_units(u.nm).value)[:varwidth] + crt_z = crdfmt.format(pos[2].in_units(u.nm).value)[:varwidth] + out_str = out_str + "{0:5d}{1:5s}{2:5s}{3:5d}{4}{5}{6}\n".format( + res_id, + res_name, + atom_name, + atom_id, + crt_x, + crt_y, + crt_z, ) return out_str @@ -190,7 +211,7 @@ def _prepare_box(top): rtol=1e-5, atol=0.1 * u.degree, ): - out_str = out_str + " {:0.5f} {:0.5f} {:0.5f} \n".format( + out_str = out_str + " {:0.5f} {:0.5f} {:0.5f}\n".format( top.box.lengths[0].in_units(u.nm).value.round(6), top.box.lengths[1].in_units(u.nm).value.round(6), top.box.lengths[2].in_units(u.nm).value.round(6), diff --git a/gmso/formats/top.py b/gmso/formats/top.py index bb38bfa1a..f8e17c6de 100644 --- a/gmso/formats/top.py +++ b/gmso/formats/top.py @@ -1,21 +1,38 @@ """Write a GROMACS topology (.TOP) file.""" import datetime +import warnings import unyt as u +from gmso.core.dihedral import Dihedral from gmso.core.element import element_by_atom_type +from gmso.core.improper import Improper +from gmso.core.views import PotentialFilters from gmso.exceptions import GMSOError from gmso.formats.formats_registry import saves_as from gmso.lib.potential_templates import PotentialTemplateLibrary +from gmso.parameterization.molecule_utils import ( + molecule_angles, + molecule_bonds, + molecule_dihedrals, + molecule_impropers, +) from gmso.utils.compatibility import check_compatibility @saves_as(".top") -def write_top(top, filename, top_vars=None): +def write_top(top, filename, top_vars=None, simplify_check=False): """Write a gmso.core.Topology object to a GROMACS topology (.TOP) file.""" - pot_types = _validate_compatibility(top) + pot_types = _validate_compatibility(top, simplify_check) top_vars = _get_top_vars(top, top_vars) + # Sanity checks + msg = "System not fully typed" + for site in top.sites: + assert site.atom_type, msg + for connection in top.connections: + assert connection.connection_type, msg + with open(filename, "w") as out_file: out_file.write( "; File {} written by GMSO at {}\n\n".format( @@ -27,14 +44,14 @@ def write_top(top, filename, top_vars=None): "[ defaults ]\n" "; nbfunc\t" "comb-rule\t" - "gen-pairs\t" + "gen-pairs\t\t" "fudgeLJ\t" "fudgeQQ\n" ) out_file.write( "{0}\t\t\t" "{1}\t\t\t" - "{2}\t\t\t" + "{2}\t\t" "{3}\t\t" "{4}\n\n".format( top_vars["nbfunc"], @@ -48,24 +65,25 @@ def write_top(top, filename, top_vars=None): out_file.write( "[ atomtypes ]\n" "; name\t\t" - "at.num\t\t" + "at.num\t" "mass\t\t" "charge\t\t" - "ptype\t\t" - "sigma\t\t" + "ptype\t" + "sigma\t" "epsilon\n" ) - for atom_type in top.atom_types: + + for atom_type in top.atom_types(PotentialFilters.UNIQUE_NAME_CLASS): out_file.write( - "{0}\t\t\t" - "{1}\t\t\t" - "{2:.5f}\t\t" - "{3:.5f}\t\t" - "{4}\t\t\t" - "{5:.5f}\t\t\t" - "{6:.5f}\n".format( + "{0:12s}" + "{1:4s}" + "{2:12.5f}" + "{3:12.5f}\t" + "{4:4s}" + "{5:12.5f}" + "{6:12.5f}\n".format( atom_type.name, - _lookup_atomic_number(atom_type), + str(_lookup_atomic_number(atom_type)), atom_type.mass.in_units(u.amu).value, atom_type.charge.in_units(u.elementary_charge).value, "A", @@ -76,97 +94,155 @@ def write_top(top, filename, top_vars=None): ) ) - out_file.write("\n[ moleculetype ]\n" "; name\t\tnrexcl\n") - - # TODO: Better parsing of site.molecule and site.residue into residues/molecules - n_unique_molecule = len( - top.unique_site_labels("molecule", name_only=True) - ) - if n_unique_molecule > 1: - raise NotImplementedError - # Treat top without molecule as one residue-like "molecule" - elif n_unique_molecule == 0: + # Define unique molecule by name only + unique_molecules = _get_unique_molecules(top) + + # Section headers + headers = { + "bonds": "\n[ bonds ]\n" "; ai\taj\t\tfunct\tb0\t\tkb\n", + "bond_restraints": "\n[ bonds ] ;Harmonic potential restraint\n" + "; ai\taj\t\tfunct\tb0\t\tkb\n", + "angles": "\n[ angles ]\n" "; ai\taj\t\tak\t\tfunct\tphi_0\tk0\n", + "angle_restraints": ( + "\n[ angle_restraints ]\n" + "; ai\taj\t\tai\t\tak\t\tfunct\ttheta_eq\tk\tmultiplicity\n" + ), + "dihedrals": { + "RyckaertBellemansTorsionPotential": "\n[ dihedrals ]\n" + "; ai\taj\t\tak\t\tal\t\tfunct\t\tc0\t\tc1\t\tc2\t\tc3\t\tc4\t\tc5\n", + "PeriodicTorsionPotential": "\n[ dihedrals ]\n" + "; ai\taj\t\tak\t\tal\t\tfunct\tphi\tk_phi\tmulitplicity\n", + }, + "dihedral_restraints": "\n[ dihedral_restraints ]\n" + "#ifdef DIHRES\n" + "; ai\taj\t\tak\t\tal\t\tfunct\ttheta_eq\tdelta_theta\t\tkd\n", + } + for tag in unique_molecules: + """Write out nrexcl for each unique molecule.""" + out_file.write("\n[ moleculetype ]\n" "; name\tnrexcl\n") + + # TODO: Lookup and join nrexcl from each molecule object + out_file.write("{0}\t" "{1}\n".format(tag, top_vars["nrexcl"])) + + """Write out atoms for each unique molecule.""" out_file.write( - "{0}\t\t\t" - "{1}\n\n".format( - top.name, - top_vars["nrexcl"], # Typically exclude 3 nearest neighbors - ) + "[ atoms ]\n" + "; nr\ttype\tresnr\tresidue\t\tatom\tcgnr\tcharge\tmass\n" ) - # TODO: Lookup and join nrexcl from each molecule object - elif n_unique_molecule == 1: - out_file.write("{0}\t\t\t" "{1}\n\n".format(top.name, 3)) - - out_file.write( - "[ atoms ]\n" - "; nr\t\ttype\tresnr\tresidue\t\tatom\tcgnr\tcharge\t\tmass\n" - ) - for site in top.sites: - out_file.write( - "{0}\t\t\t" - "{1}\t\t" - "{2}\t\t" - "{3}\t" - "{4}\t\t" - "{5}\t\t" - "{6:.5f}\t\t" - "{7:.5f}\n".format( - top.get_index(site) + 1, - site.atom_type.name, - 1, # TODO: molecule number - top.name, # TODO: molecule.name - _lookup_element_symbol(site.atom_type), - 1, # TODO: care about charge groups - site.charge.in_units(u.elementary_charge).value, - site.atom_type.mass.in_units(u.amu).value, + # Each unique molecule need to be reindexed (restarting from 0) + # The shifted_idx_map is needed to make sure all the atom index used in + # latter connection sections are acurate + shifted_idx_map = dict() + for idx, site in enumerate(unique_molecules[tag]["sites"]): + shifted_idx_map[top.get_index(site)] = idx + out_file.write( + "{0:8s}" + "{1:12s}" + "{2:8s}" + "{3:12s}" + "{4:8s}" + "{5:4s}" + "{6:12.5f}" + "{7:12.5f}\n".format( + str(idx + 1), + site.atom_type.name, + str(site.molecule.number + 1 if site.molecule else 1), + tag, + site.atom_type.tags["element"], + "1", # TODO: care about charge groups + site.charge.in_units(u.elementary_charge).value, + site.atom_type.mass.in_units(u.amu).value, + ) ) - ) - out_file.write("\n[ bonds ]\n" "; ai aj funct c0 c1\n") - for bond in top.bonds: - out_file.write( - _write_connection(top, bond, pot_types[bond.connection_type]) - ) + for conn_group in [ + "bonds", + "bond_restraints", + "angles", + "angle_restraints", + "dihedrals", + "dihedral_restraints", + "impropers", + ]: + if unique_molecules[tag][conn_group]: + if conn_group in ["dihedrals", "impropers"]: + proper_groups = { + "RyckaertBellemansTorsionPotential": list(), + "PeriodicTorsionPotential": list(), + } + for dihedral in unique_molecules[tag][conn_group]: + ptype = pot_types[dihedral.connection_type] + proper_groups[ptype].append(dihedral) + + # Improper use same header as dihedral periodic header + if proper_groups["RyckaertBellemansTorsionPotential"]: + out_file.write( + headers["dihedrals"][ + "RyckaertBellemansTorsionPotential" + ] + ) + for conn in proper_groups[ + "RyckaertBellemansTorsionPotential" + ]: + for line in _write_connection( + top, + conn, + pot_types[conn.connection_type], + shifted_idx_map, + ): + out_file.write(line) + if proper_groups["PeriodicTorsionPotential"]: + out_file.write( + headers["dihedrals"]["PeriodicTorsionPotential"] + ) + for conn in proper_groups[ + "PeriodicTorsionPotential" + ]: + for line in _write_connection( + top, + conn, + pot_types[conn.connection_type], + shifted_idx_map, + ): + out_file.write(line) + elif "restraints" in conn_group: + if conn_group == "dihedral_restraints": + warnings.warn( + "The diehdral_restraints writer is designed to work with" + "`define = DDIHRES` clause in the GROMACS input file (.mdp)" + ) + out_file.write(headers[conn_group]) + for conn in unique_molecules[tag][conn_group]: + out_file.write( + _write_restraint( + top, + conn, + conn_group, + shifted_idx_map, + ) + ) + else: + out_file.write(headers[conn_group]) + for conn in unique_molecules[tag][conn_group]: + out_file.write( + _write_connection( + top, + conn, + pot_types[conn.connection_type], + shifted_idx_map, + ) + ) + if conn_group == "dihedral_restraints": + out_file.write("#endif DIHRES\n") - out_file.write( - "\n[ angles ]\n" "; ai aj ak funct c0 c1\n" - ) - for angle in top.angles: - out_file.write( - _write_connection(top, angle, pot_types[angle.connection_type]) - ) + out_file.write("\n[ system ]\n" "; name\n" "{0}\n\n".format(top.name)) - out_file.write( - "\n[ dihedrals ]\n" - "; ai aj ak al funct c0 c1 c2\n" - ) - for dihedral in top.dihedrals: + out_file.write("[ molecules ]\n" "; molecule\tnmols\n") + for tag in unique_molecules: out_file.write( - _write_connection( - top, dihedral, pot_types[dihedral.connection_type] - ) + "{0}\t{1}\n".format(tag, len(unique_molecules[tag]["subtags"])) ) - out_file.write("\n[ system ]\n" "; name\n" "{0}\n\n".format(top.name)) - - if len(top.unique_site_labels("molecule", name_only=True)) > 1: - raise NotImplementedError - - # TODO: Write out atom types for each unique `molecule` (name_only) in `atoms` section - # and write out number of molecules in `molecules` section - # if len(top.subtops) == 0: - out_file.write( - "[ molecules ]\n" - "; molecule\tnmols\n" - "{0}\t\t{1}".format(top.name, 1) - ) - # elif len(top.subtops) > 0: - # out_file.write( - # '[ molecules ]\n' - # '; molecule\tnmols\n' - # '{0}\t\t{1}'.format(top.subtops[0].name, top.n_subtops) - # ) - def _accepted_potentials(): """List of accepted potentials that GROMACS can support.""" @@ -186,9 +262,9 @@ def _accepted_potentials(): return accepted_potentials -def _validate_compatibility(top): +def _validate_compatibility(top, simplify_check): """Check compatability of topology object with GROMACS TOP format.""" - pot_types = check_compatibility(top, _accepted_potentials()) + pot_types = check_compatibility(top, _accepted_potentials(), simplify_check) return pot_types @@ -196,11 +272,11 @@ def _get_top_vars(top, top_vars): """Generate a dictionary of values for the defaults directive.""" combining_rule_to_gmx = {"lorentz": 2, "geometric": 3} default_top_vars = dict() - default_top_vars["nbfunc"] = 1 + default_top_vars["nbfunc"] = 1 # modify this to check for lj or buckingham default_top_vars["comb-rule"] = combining_rule_to_gmx[top.combining_rule] default_top_vars["gen-pairs"] = "no" - default_top_vars["fudgeLJ"] = 1 - default_top_vars["fudgeQQ"] = 1 + default_top_vars["fudgeLJ"] = top.scaling_factors[0][2] + default_top_vars["fudgeQQ"] = top.scaling_factors[1][2] default_top_vars["nrexcl"] = 3 if isinstance(top_vars, dict): @@ -209,6 +285,67 @@ def _get_top_vars(top, top_vars): return default_top_vars +def _get_unique_molecules(top): + unique_molecules = { + tag: { + "subtags": list(), + } + for tag in top.unique_site_labels("molecule", name_only=True) + } + + for molecule in top.unique_site_labels("molecule", name_only=False): + unique_molecules[molecule.name]["subtags"].append(molecule) + + if len(unique_molecules) == 0: + unique_molecules[top.name] = dict() + unique_molecules[top.name]["subtags"] = [top.name] + unique_molecules[top.name]["sites"] = list(top.sites) + unique_molecules[top.name]["bonds"] = list(top.bonds) + unique_molecules[top.name]["bond_restraints"] = list( + bond for bond in top.bonds if bond.restraint + ) + unique_molecules[top.name]["angles"] = list(top.angles) + unique_molecules[top.name]["angle_restraints"] = list( + angle for angle in top.angles if angle.restraint + ) + unique_molecules[top.name]["dihedrals"] = list(top.angles) + unique_molecules[top.name]["dihedral_restraints"] = list( + dihedral for dihedral in top.dihedrals if dihedral.restraint + ) + unique_molecules[molecule.name]["impropers"] = list(top.impropers) + + else: + for tag in unique_molecules: + molecule = unique_molecules[tag]["subtags"][0] + unique_molecules[tag]["sites"] = list( + top.iter_sites(key="molecule", value=molecule) + ) + unique_molecules[tag]["bonds"] = list(molecule_bonds(top, molecule)) + unique_molecules[tag]["bond_restraints"] = list( + bond for bond in molecule_bonds(top, molecule) if bond.restraint + ) + unique_molecules[tag]["angles"] = list( + molecule_angles(top, molecule) + ) + unique_molecules[tag]["angle_restraints"] = list( + angle + for angle in molecule_angles(top, molecule) + if angle.restraint + ) + unique_molecules[tag]["dihedrals"] = list( + molecule_dihedrals(top, molecule) + ) + unique_molecules[tag]["dihedral_restraints"] = list( + dihedral + for dihedral in molecule_dihedrals(top, molecule) + if dihedral.restraint + ) + unique_molecules[tag]["impropers"] = list( + molecule_impropers(top, molecule) + ) + return unique_molecules + + def _lookup_atomic_number(atom_type): """Look up an atomic_number based on atom type information, 0 if non-element type.""" try: @@ -227,7 +364,7 @@ def _lookup_element_symbol(atom_type): return "X" -def _write_connection(top, connection, potential_name): +def _write_connection(top, connection, potential_name, shifted_idx_map): """Worker function to write various connection information.""" worker_functions = { "HarmonicBondPotential": _harmonic_bond_potential_writer, @@ -236,14 +373,14 @@ def _write_connection(top, connection, potential_name): "PeriodicTorsionPotential": _periodic_torsion_writer, } - return worker_functions[potential_name](top, connection) + return worker_functions[potential_name](top, connection, shifted_idx_map) -def _harmonic_bond_potential_writer(top, bond): +def _harmonic_bond_potential_writer(top, bond, shifted_idx_map): """Write harmonic bond information.""" - line = "\t{0}\t{1}\t{2}\t{3:.5f}\t{4:.5f}\n".format( - top.get_index(bond.connection_members[0]) + 1, - top.get_index(bond.connection_members[1]) + 1, + line = "{0:8s}{1:8s}{2:4s}{3:15.5f}{4:15.5f}\n".format( + str(shifted_idx_map[top.get_index(bond.connection_members[0])] + 1), + str(shifted_idx_map[top.get_index(bond.connection_members[1])] + 1), "1", bond.connection_type.parameters["r_eq"].in_units(u.nm).value, bond.connection_type.parameters["k"] @@ -253,12 +390,12 @@ def _harmonic_bond_potential_writer(top, bond): return line -def _harmonic_angle_potential_writer(top, angle): +def _harmonic_angle_potential_writer(top, angle, shifted_idx_map): """Write harmonic angle information.""" - line = "\t{0}\t{1}\t{2}\t{3}\t{4:.5f}\t{5:.5f}\n".format( - top.get_index(angle.connection_members[0]) + 1, - top.get_index(angle.connection_members[1]) + 1, - top.get_index(angle.connection_members[2]) + 1, + line = "{0:8s}{1:8s}{2:8s}{3:4s}{4:15.5f}{5:15.5f}\n".format( + str(shifted_idx_map[top.get_index(angle.connection_members[0])] + 1), + str(shifted_idx_map[top.get_index(angle.connection_members[1])] + 1), + str(shifted_idx_map[top.get_index(angle.connection_members[2])] + 1), "1", angle.connection_type.parameters["theta_eq"].in_units(u.degree).value, angle.connection_type.parameters["k"] @@ -268,13 +405,13 @@ def _harmonic_angle_potential_writer(top, angle): return line -def _ryckaert_bellemans_torsion_writer(top, dihedral): +def _ryckaert_bellemans_torsion_writer(top, dihedral, shifted_idx_map): """Write Ryckaert-Bellemans Torsion information.""" - line = "\t{0}\t{1}\t{2}\t{3}\t{4}\t{5:.5f}\t{6:.5f}\t{7:.5f}\t{8:.5f}\t{9:.5f}\t{10:.5f}\n".format( - top.get_index(dihedral.connection_members[0]) + 1, - top.get_index(dihedral.connection_members[1]) + 1, - top.get_index(dihedral.connection_members[2]) + 1, - top.get_index(dihedral.connection_members[3]) + 1, + line = "{0:8s}{1:8s}{2:8s}{3:8s}{4:4s}{5:15.5f}{6:15.5f}{7:15.5f}{8:15.5f}{9:15.5f}{10:15.5f}\n".format( + str(shifted_idx_map[top.get_index(dihedral.connection_members[0])] + 1), + str(shifted_idx_map[top.get_index(dihedral.connection_members[1])] + 1), + str(shifted_idx_map[top.get_index(dihedral.connection_members[2])] + 1), + str(shifted_idx_map[top.get_index(dihedral.connection_members[3])] + 1), "3", dihedral.connection_type.parameters["c0"] .in_units(u.Unit("kJ/mol")) @@ -298,18 +435,105 @@ def _ryckaert_bellemans_torsion_writer(top, dihedral): return line -def _periodic_torsion_writer(top, dihedral): +def _periodic_torsion_writer(top, dihedral, shifted_idx_map): """Write periodic torsion information.""" - line = "\t{0}\t{1}\t{2}\t{3}\t{4}\t{5:.5f}\t{6:.5f}\t{7}\n".format( - top.get_index(dihedral.connection_members[0]) + 1, - top.get_index(dihedral.connection_members[1]) + 1, - top.get_index(dihedral.connection_members[2]) + 1, - top.get_index(dihedral.connection_members[3]) + 1, + if isinstance(dihedral, Dihedral): + if dihedral.connection_type.parameters["phi_eq"].size == 1: + # Normal dihedral + layers, funct = 1, "1" + for key, val in dihedral.connection_type.parameters.items(): + dihedral.connection_type.parameters[key] = val.reshape(layers) + else: + # Layered/Multiple dihedral + layers, funct = ( + dihedral.connection_type.parameters["phi_eq"].size, + "9", + ) + elif isinstance(dihedral, Improper): + layers, funct = 1, "4" + else: + raise TypeError(f"Type {type(dihedral)} not supported.") + + lines = list() + for i in range(layers): + line = "{0:8s}{1:8s}{2:8s}{3:8s}{4:4s}{5:15.5f}{6:15.5f}{7:4}\n".format( + str( + shifted_idx_map[top.get_index(dihedral.connection_members[0])] + + 1 + ), + str( + shifted_idx_map[top.get_index(dihedral.connection_members[1])] + + 1 + ), + str( + shifted_idx_map[top.get_index(dihedral.connection_members[2])] + + 1 + ), + str( + shifted_idx_map[top.get_index(dihedral.connection_members[3])] + + 1 + ), + funct, + dihedral.connection_type.parameters["phi_eq"][i] + .in_units(u.degree) + .value, + dihedral.connection_type.parameters["k"][i] + .in_units(u.Unit("kJ/(mol)")) + .value, + dihedral.connection_type.parameters["n"][i].value, + ) + lines.append(line) + return lines + + +def _write_restraint(top, connection, type, shifted_idx_map): + """Worker function to write various connection restraint information.""" + worker_functions = { + "bond_restraints": _bond_restraint_writer, + "angle_restraints": _angle_restraint_writer, + "dihedral_restraints": _dihedral_restraint_writer, + } + + return worker_functions[type](top, connection, shifted_idx_map) + + +def _bond_restraint_writer(top, bond, shifted_idx_map): + """Write bond restraint information.""" + line = "{0:8s}{1:8s}{2:4s}{3:15.5f}{4:15.5f}\n".format( + str(shifted_idx_map[top.get_index(bond.connection_members[1])] + 1), + str(shifted_idx_map[top.get_index(bond.connection_members[0])] + 1), + "6", + bond.restraint["r_eq"].in_units(u.nm).value, + bond.restraint["k"].in_units(u.Unit("kJ/(mol * nm**2)")).value, + ) + return line + + +def _angle_restraint_writer(top, angle, shifted_idx_map): + """Write angle restraint information.""" + line = "{0:8s}{1:8s}{2:8s}{3:8s}{4:4s}{5:15.5f}{6:15.5f}{7:4}\n".format( + str(shifted_idx_map[top.get_index(angle.connection_members[1])] + 1), + str(shifted_idx_map[top.get_index(angle.connection_members[0])] + 1), + str(shifted_idx_map[top.get_index(angle.connection_members[1])] + 1), + str(shifted_idx_map[top.get_index(angle.connection_members[2])] + 1), "1", - dihedral.connection_type.parameters["phi_eq"].in_units(u.degree).value, - dihedral.connection_type.parameters["k"] - .in_units(u.Unit("kJ/(mol)")) - .value, - dihedral.connection_type.parameters["n"].value, + angle.restraint["theta_eq"].in_units(u.degree).value, + angle.restraint["k"].in_units(u.Unit("kJ/mol")).value, + angle.restraint["n"], + ) + return line + + +def _dihedral_restraint_writer(top, dihedral, shifted_idx_map): + """Write dihedral restraint information.""" + line = "{0:8s}{1:8s}{2:8s}{3:8s}{4:4s}{5:15.5f}{6:15.5f}{7:15.5f}\n".format( + str(shifted_idx_map[top.get_index(dihedral.connection_members[0])] + 1), + str(shifted_idx_map[top.get_index(dihedral.connection_members[1])] + 1), + str(shifted_idx_map[top.get_index(dihedral.connection_members[2])] + 1), + str(shifted_idx_map[top.get_index(dihedral.connection_members[3])] + 1), + "1", + dihedral.restraint["phi_eq"].in_units(u.degree).value, + dihedral.restraint["delta_phi"].in_units(u.degree).value, + dihedral.restraint["k"].in_units(u.Unit("kJ/(mol * rad**2)")).value, ) return line diff --git a/gmso/parameterization/topology_parameterizer.py b/gmso/parameterization/topology_parameterizer.py index 9daf47346..381165015 100644 --- a/gmso/parameterization/topology_parameterizer.py +++ b/gmso/parameterization/topology_parameterizer.py @@ -459,7 +459,7 @@ def _get_atomtypes( # Assume nodes in repeated structures are in the same order for node, ref_node in zip( sorted(subgraph.nodes), - reference[molecule]["typemap"], + sorted(reference[molecule]["typemap"]), ): typemap[node] = reference[molecule]["typemap"][ ref_node diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py index fb6e6bbe9..1c0fe1f5d 100644 --- a/gmso/tests/base_test.py +++ b/gmso/tests/base_test.py @@ -3,6 +3,7 @@ import numpy as np import pytest import unyt as u +from foyer.tests.utils import get_fn from gmso.core.angle import Angle from gmso.core.atom import Atom @@ -17,7 +18,7 @@ from gmso.external import from_mbuild, from_parmed from gmso.external.convert_foyer_xml import from_foyer_xml from gmso.tests.utils import get_path -from gmso.utils.io import get_fn, has_foyer +from gmso.utils.io import get_fn class BaseTest: @@ -49,6 +50,44 @@ def box(self): def top(self): return Topology(name="mytop") + @pytest.fixture + def benzene_ua(self): + compound = mb.load(get_fn("benzene_ua.mol2")) + compound.children[0].name = "BenzeneUA" + top = from_mbuild(compound) + top.identify_connections() + return top + + @pytest.fixture + def benzene_ua_box(self): + compound = mb.load(get_fn("benzene_ua.mol2")) + compound.children[0].name = "BenzeneUA" + compound_box = mb.packing.fill_box( + compound=compound, n_compounds=5, density=1 + ) + top = from_mbuild(compound_box) + top.identify_connections() + return top + + @pytest.fixture + def benzene_aa(self): + compound = mb.load(get_fn("benzene.mol2")) + compound.children[0].name = "BenzeneAA" + top = from_mbuild(compound) + top.identify_connections() + return top + + @pytest.fixture + def benzene_aa_box(self): + compound = mb.load(get_fn("benzene.mol2")) + compound.children[0].name = "BenzeneAA" + compound_box = mb.packing.fill_box( + compound=compound, n_compounds=5, density=1 + ) + top = from_mbuild(compound_box) + top.identify_connections() + return top + @pytest.fixture def ar_system(self, n_ar_system): return from_mbuild(n_ar_system(), parse_label=True) @@ -56,7 +95,7 @@ def ar_system(self, n_ar_system): @pytest.fixture def n_ar_system(self): def _topology(n_sites=100): - ar = mb.Compound(name="Ar") + ar = mb.Compound(name="Ar", element="Ar") packed_system = mb.fill_box( compound=ar, @@ -226,9 +265,8 @@ def typed_water_system(self, water_system): @pytest.fixture def foyer_fullerene(self): - if has_foyer: - import foyer - from foyer.tests.utils import get_fn + from foyer.tests.utils import get_fn + from_foyer_xml(get_fn("fullerene.xml"), overwrite=True) gmso_ff = ForceField("fullerene_gmso.xml") @@ -237,9 +275,8 @@ def foyer_fullerene(self): @pytest.fixture def foyer_periodic(self): # TODO: this errors out with backend="ffutils" - if has_foyer: - import foyer - from foyer.tests.utils import get_fn + from foyer.tests.utils import get_fn + from_foyer_xml(get_fn("oplsaa-periodic.xml"), overwrite=True) gmso_ff = ForceField("oplsaa-periodic_gmso.xml", backend="gmso") @@ -248,27 +285,23 @@ def foyer_periodic(self): @pytest.fixture def foyer_urey_bradley(self): # TODO: this errors out with backend="ffutils" - if has_foyer: - import foyer - from foyer.tests.utils import get_fn + from foyer.tests.utils import get_fn - from_foyer_xml(get_fn("charmm36_cooh.xml"), overwrite=True) - gmso_ff = ForceField("charmm36_cooh_gmso.xml", backend="gmso") + from_foyer_xml(get_fn("charmm36_cooh.xml"), overwrite=True) + gmso_ff = ForceField("charmm36_cooh_gmso.xml", backend="gmso") - return gmso_ff + return gmso_ff @pytest.fixture def foyer_rb_torsion(self): - if has_foyer: - import foyer - from foyer.tests.utils import get_fn + from foyer.tests.utils import get_fn - from_foyer_xml( - get_fn("refs-multi.xml"), overwrite=True, validate_foyer=True - ) - gmso_ff = ForceField("refs-multi_gmso.xml") + from_foyer_xml( + get_fn("refs-multi.xml"), overwrite=True, validate_foyer=True + ) + gmso_ff = ForceField("refs-multi_gmso.xml") - return gmso_ff + return gmso_ff @pytest.fixture def methane(self): diff --git a/gmso/tests/files/benzene.gro b/gmso/tests/files/benzene.gro new file mode 100644 index 000000000..2ea50d5e8 --- /dev/null +++ b/gmso/tests/files/benzene.gro @@ -0,0 +1,63 @@ +Topology written by GMSO 0.9.1 at 2022-09-18 00:41:09.308507 +60 + 1Ben C 1 7.041 2.815 0.848 + 1Ben C 2 6.957 2.739 0.767 + 1Ben C 3 6.820 2.737 0.792 + 1Ben C 4 6.767 2.811 0.898 + 1Ben C 5 6.851 2.887 0.979 + 1Ben C 6 6.988 2.889 0.954 + 1Ben H 7 7.148 2.817 0.828 + 1Ben H 8 6.998 2.682 0.684 + 1Ben H 9 6.754 2.678 0.729 + 1Ben H 10 6.660 2.810 0.917 + 1Ben H 11 6.810 2.945 1.061 + 1Ben H 12 7.054 2.948 1.017 + 2Ben C 13 5.646 3.077 6.562 + 2Ben C 14 5.686 3.174 6.471 + 2Ben C 15 5.718 3.138 6.340 + 2Ben C 16 5.710 3.005 6.301 + 2Ben C 17 5.669 2.907 6.392 + 2Ben C 18 5.637 2.943 6.523 + 2Ben H 19 5.621 3.105 6.664 + 2Ben H 20 5.693 3.278 6.502 + 2Ben H 21 5.750 3.214 6.269 + 2Ben H 22 5.735 2.976 6.199 + 2Ben H 23 5.662 2.803 6.361 + 2Ben H 24 5.606 2.868 6.594 + 3Ben C 25 5.897 6.825 1.520 + 3Ben C 26 5.980 6.765 1.615 + 3Ben C 27 5.931 6.663 1.696 + 3Ben C 28 5.799 6.621 1.682 + 3Ben C 29 5.716 6.680 1.587 + 3Ben C 30 5.765 6.782 1.506 + 3Ben H 31 5.936 6.904 1.456 + 3Ben H 32 6.083 6.799 1.625 + 3Ben H 33 5.996 6.617 1.770 + 3Ben H 34 5.761 6.541 1.745 + 3Ben H 35 5.613 6.647 1.576 + 3Ben H 36 5.701 6.829 1.432 + 4Ben C 37 7.018 2.222 6.626 + 4Ben C 38 6.880 2.206 6.619 + 4Ben C 39 6.795 2.304 6.670 + 4Ben C 40 6.849 2.418 6.729 + 4Ben C 41 6.987 2.434 6.737 + 4Ben C 42 7.072 2.336 6.685 + 4Ben H 43 7.084 2.145 6.586 + 4Ben H 44 6.838 2.117 6.573 + 4Ben H 45 6.687 2.291 6.665 + 4Ben H 46 6.783 2.495 6.770 + 4Ben H 47 7.029 2.523 6.782 + 4Ben H 48 7.180 2.349 6.690 + 5Ben C 49 6.192 3.667 7.643 + 5Ben C 50 6.314 3.634 7.583 + 5Ben C 51 6.383 3.730 7.509 + 5Ben C 52 6.330 3.859 7.496 + 5Ben C 53 6.209 3.891 7.556 + 5Ben C 54 6.140 3.795 7.629 + 5Ben H 55 6.138 3.592 7.700 + 5Ben H 56 6.355 3.534 7.594 + 5Ben H 57 6.478 3.705 7.462 + 5Ben H 58 6.384 3.934 7.438 + 5Ben H 59 6.168 3.991 7.545 + 5Ben H 60 6.045 3.820 7.676 + 8.65597 8.65597 8.65597 diff --git a/gmso/tests/files/benzene.top b/gmso/tests/files/benzene.top new file mode 100644 index 000000000..d4de28f89 --- /dev/null +++ b/gmso/tests/files/benzene.top @@ -0,0 +1,99 @@ +; File Topology written by GMSO at 2022-10-28 01:05:54.340726 + +[ defaults ] +; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ +1 3 no 0.5 0.5 + +[ atomtypes ] +; name at.num mass charge ptype sigma epsilon +opls_145 6 12.01100 -0.11500 A 0.35500 0.29288 +opls_146 1 1.00800 0.11500 A 0.24200 0.12552 + +[ moleculetype ] +; name nrexcl +BenzeneAA 3 +[ atoms ] +; nr type resnr residue atom cgnr charge mass +1 opls_145 1 BenzeneAA C 1 -0.11500 12.01100 +2 opls_145 1 BenzeneAA C 1 -0.11500 12.01100 +3 opls_145 1 BenzeneAA C 1 -0.11500 12.01100 +4 opls_145 1 BenzeneAA C 1 -0.11500 12.01100 +5 opls_145 1 BenzeneAA C 1 -0.11500 12.01100 +6 opls_145 1 BenzeneAA C 1 -0.11500 12.01100 +7 opls_146 1 BenzeneAA H 1 0.11500 1.00800 +8 opls_146 1 BenzeneAA H 1 0.11500 1.00800 +9 opls_146 1 BenzeneAA H 1 0.11500 1.00800 +10 opls_146 1 BenzeneAA H 1 0.11500 1.00800 +11 opls_146 1 BenzeneAA H 1 0.11500 1.00800 +12 opls_146 1 BenzeneAA H 1 0.11500 1.00800 + +[ bonds ] +; ai aj funct b0 kb +2 1 1 0.14000 392459.20000 +6 1 1 0.14000 392459.20000 +7 1 1 0.10800 307105.60000 +3 2 1 0.14000 392459.20000 +8 2 1 0.10800 307105.60000 +4 3 1 0.14000 392459.20000 +9 3 1 0.10800 307105.60000 +5 4 1 0.14000 392459.20000 +10 4 1 0.10800 307105.60000 +6 5 1 0.14000 392459.20000 +11 5 1 0.10800 307105.60000 +12 6 1 0.10800 307105.60000 + +[ angles ] +; ai aj ak funct phi_0 k0 +6 1 7 1 120.00000 292.88000 +2 1 7 1 120.00000 292.88000 +1 2 8 1 120.00000 292.88000 +3 2 8 1 120.00000 292.88000 +4 3 9 1 120.00000 292.88000 +2 3 9 1 120.00000 292.88000 +5 4 10 1 120.00000 292.88000 +3 4 10 1 120.00000 292.88000 +4 5 11 1 120.00000 292.88000 +6 5 11 1 120.00000 292.88000 +1 6 12 1 120.00000 292.88000 +5 6 12 1 120.00000 292.88000 +3 4 5 1 120.00000 527.18400 +2 3 4 1 120.00000 527.18400 +4 5 6 1 120.00000 527.18400 +2 1 6 1 120.00000 527.18400 +1 2 3 1 120.00000 527.18400 +1 6 5 1 120.00000 527.18400 + +[ dihedrals ] +; ai aj ak al funct c0 c1 c2 c3 c4 c5 +7 1 6 5 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +7 1 6 12 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +7 1 2 8 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +7 1 2 3 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +8 2 1 6 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +8 2 3 4 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +8 2 3 9 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +9 3 4 5 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +9 3 4 10 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +9 3 2 1 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +10 4 5 6 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +10 4 5 11 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +10 4 3 2 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +11 5 4 3 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +11 5 6 1 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +11 5 6 12 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +12 6 1 2 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +12 6 5 4 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +3 4 5 6 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +4 3 2 1 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +5 4 3 2 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +4 5 6 1 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +2 1 6 5 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 +3 2 1 6 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000 + +[ system ] +; name +Topology + +[ molecules ] +; molecule nmols +BenzeneAA 5 diff --git a/gmso/tests/files/benzene_trappe-ua.xml b/gmso/tests/files/benzene_trappe-ua.xml new file mode 100755 index 000000000..a8ce966e5 --- /dev/null +++ b/gmso/tests/files/benzene_trappe-ua.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/gmso/tests/files/restrained_benzene_ua.gro b/gmso/tests/files/restrained_benzene_ua.gro new file mode 100644 index 000000000..92fc6fc2c --- /dev/null +++ b/gmso/tests/files/restrained_benzene_ua.gro @@ -0,0 +1,33 @@ +Topology written by GMSO 0.9.1 at 2022-11-07 11:51:34.770776 +30 + 1Com _CH 1 5.127 3.773 4.686 + 1Com _CH 2 5.204 3.757 4.802 + 1Com _CH 3 5.343 3.766 4.796 + 1Com _CH 4 5.407 3.793 4.674 + 1Com _CH 5 5.330 3.809 4.558 + 1Com _CH 6 5.191 3.800 4.564 + 2Com _CH 7 3.490 3.266 4.318 + 2Com _CH 8 3.547 3.139 4.298 + 2Com _CH 9 3.552 3.048 4.404 + 2Com _CH 10 3.500 3.084 4.530 + 2Com _CH 11 3.444 3.210 4.549 + 2Com _CH 12 3.439 3.302 4.443 + 3Com _CH 13 6.609 1.796 0.837 + 3Com _CH 14 6.516 1.728 0.758 + 3Com _CH 15 6.535 1.593 0.727 + 3Com _CH 16 6.648 1.526 0.776 + 3Com _CH 17 6.741 1.594 0.855 + 3Com _CH 18 6.721 1.729 0.886 + 4Com _CH 19 6.543 2.442 2.196 + 4Com _CH 20 6.657 2.484 2.126 + 4Com _CH 21 6.644 2.558 2.008 + 4Com _CH 22 6.517 2.591 1.960 + 4Com _CH 23 6.403 2.549 2.029 + 4Com _CH 24 6.416 2.474 2.147 + 5Com _CH 25 6.699 5.604 7.130 + 5Com _CH 26 6.652 5.661 7.011 + 5Com _CH 27 6.611 5.795 7.010 + 5Com _CH 28 6.618 5.872 7.127 + 5Com _CH 29 6.666 5.814 7.245 + 5Com _CH 30 6.706 5.680 7.247 + 8.42655 8.42655 8.42655 diff --git a/gmso/tests/files/restrained_benzene_ua.top b/gmso/tests/files/restrained_benzene_ua.top new file mode 100644 index 000000000..d1859f994 --- /dev/null +++ b/gmso/tests/files/restrained_benzene_ua.top @@ -0,0 +1,85 @@ +; File Topology written by GMSO at 2022-11-07 11:51:34.761305 + +[ defaults ] +; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ +1 2 no 0.0 0.0 + +[ atomtypes ] +; name at.num mass charge ptype sigma epsilon +CH_sp2 6 13.01900 0.00000 A 0.36950 0.41988 + +[ moleculetype ] +; name nrexcl +Compound 3 +[ atoms ] +; nr type resnr residue atom cgnr charge mass +1 CH_sp2 1 Compound _CH 1 0.00000 13.01900 +2 CH_sp2 1 Compound _CH 1 0.00000 13.01900 +3 CH_sp2 1 Compound _CH 1 0.00000 13.01900 +4 CH_sp2 1 Compound _CH 1 0.00000 13.01900 +5 CH_sp2 1 Compound _CH 1 0.00000 13.01900 +6 CH_sp2 1 Compound _CH 1 0.00000 13.01900 + +[ bonds ] +; ai aj funct b0 kb +2 1 1 0.14000 0.00000 +6 1 1 0.14000 0.00000 +3 2 1 0.14000 0.00000 +4 3 1 0.14000 0.00000 +5 4 1 0.14000 0.00000 +6 5 1 0.14000 0.00000 + +[ bonds ] ;Harmonic potential restraint +; ai aj funct b0 kb +1 2 6 0.14000 1000.00000 +1 6 6 0.14000 1000.00000 +2 3 6 0.14000 1000.00000 +3 4 6 0.14000 1000.00000 +4 5 6 0.14000 1000.00000 +5 6 6 0.14000 1000.00000 + +[ angles ] +; ai aj ak funct phi_0 k0 +2 3 4 1 120.00000 0.10000 +3 4 5 1 120.00000 0.10000 +4 5 6 1 120.00000 0.10000 +1 2 3 1 120.00000 0.10000 +2 1 6 1 120.00000 0.10000 +1 6 5 1 120.00000 0.10000 + +[ angle_restraints ] +; ai aj ai ak funct theta_eq k multiplicity +3 2 3 4 1 120.00000 1000.00000 1 +4 3 4 5 1 120.00000 1000.00000 1 +5 4 5 6 1 120.00000 1000.00000 1 +2 1 2 3 1 120.00000 1000.00000 1 +1 2 1 6 1 120.00000 1000.00000 1 +6 1 6 5 1 120.00000 1000.00000 1 + +[ dihedrals ] +; ai aj ak al funct c0 c1 c2 c3 c4 c5 +4 3 2 1 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 +3 4 5 6 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 +4 5 6 1 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 +5 4 3 2 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 +3 2 1 6 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 +2 1 6 5 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 + +[ dihedral_restraints ] +#ifdef DIHRES +; ai aj ak al funct theta_eq delta_theta kd +4 3 2 1 1 0.00000 0.00000 1000.00000 +3 4 5 6 1 0.00000 0.00000 1000.00000 +4 5 6 1 1 0.00000 0.00000 1000.00000 +5 4 3 2 1 0.00000 0.00000 1000.00000 +3 2 1 6 1 0.00000 0.00000 1000.00000 +2 1 6 5 1 0.00000 0.00000 1000.00000 +#endif DIHRES + +[ system ] +; name +Topology + +[ molecules ] +; molecule nmols +Compound 5 diff --git a/gmso/tests/test_convert_foyer_xml.py b/gmso/tests/test_convert_foyer_xml.py index 4317794b7..b25a7576a 100644 --- a/gmso/tests/test_convert_foyer_xml.py +++ b/gmso/tests/test_convert_foyer_xml.py @@ -9,32 +9,35 @@ from gmso.external.convert_foyer_xml import from_foyer_xml from gmso.tests.base_test import BaseTest from gmso.tests.utils import get_path -from gmso.utils.io import has_foyer - -if has_foyer: - from foyer.tests.utils import get_fn parameterized_ffs = ["fullerene.xml", "oplsaa-periodic.xml", "lj.xml"] -@pytest.mark.skipif(not has_foyer, reason="Foyer is not installed") class TestXMLConversion(BaseTest): @pytest.mark.parametrize("ff", parameterized_ffs) def test_from_foyer(self, ff): + from foyer.tests.utils import get_fn + from_foyer_xml(get_fn(ff), overwrite=True) @pytest.mark.parametrize("ff", parameterized_ffs) def test_from_foyer_overwrite_false(self, ff): + from foyer.tests.utils import get_fn + from_foyer_xml(get_fn(ff), overwrite=False) with pytest.raises(FileExistsError): from_foyer_xml(get_fn(ff), overwrite=False) @pytest.mark.parametrize("ff", parameterized_ffs) def test_from_foyer_different_name(self, ff): + from foyer.tests.utils import get_fn + from_foyer_xml(get_fn(ff), f"{ff}-gmso-converted.xml", overwrite=True) @pytest.mark.parametrize("ff", parameterized_ffs) def test_from_foyer_validate_foyer(self, ff): + from foyer.tests.utils import get_fn + from_foyer_xml( get_fn(ff), f"{ff}-gmso-converted.xml", @@ -44,6 +47,8 @@ def test_from_foyer_validate_foyer(self, ff): @pytest.mark.parametrize("ff", parameterized_ffs) def test_foyer_pathlib(self, ff): + from foyer.tests.utils import get_fn + file_path = Path(get_fn(ff)) from_foyer_xml(file_path, overwrite=True) diff --git a/gmso/tests/test_gro.py b/gmso/tests/test_gro.py index c90b8a7d8..234e5292a 100644 --- a/gmso/tests/test_gro.py +++ b/gmso/tests/test_gro.py @@ -7,11 +7,15 @@ from gmso.external.convert_parmed import from_parmed from gmso.formats.gro import read_gro, write_gro from gmso.tests.base_test import BaseTest -from gmso.utils.io import get_fn, has_parmed, import_ +from gmso.tests.utils import get_path +from gmso.utils.io import get_fn, has_mbuild, has_parmed, import_ if has_parmed: pmd = import_("parmed") +if has_mbuild: + mb = import_("mbuild") + @pytest.mark.skipif(not has_parmed, reason="ParmEd is not installed") class TestGro(BaseTest): @@ -33,7 +37,7 @@ def test_read_gro(self): ) def test_wrong_n_atoms(self): - with pytest.raises(ValueError): + with pytest.raises(IndexError): Topology.load(get_fn("too_few_atoms.gro")) with pytest.raises(ValueError): Topology.load(get_fn("too_many_atoms.gro")) @@ -46,3 +50,49 @@ def test_write_gro_non_orthogonal(self): top = from_parmed(pmd.load_file(get_fn("ethane.gro"), structure=True)) top.box.angles = u.degree * [90, 90, 120] top.save("out.gro") + + @pytest.mark.skipif(not has_mbuild, reason="mBuild not installed.") + def test_benzene_gro(self): + import mbuild as mb + from mbuild.packing import fill_box + + from gmso.external import from_mbuild + + benzene = mb.load(get_fn("benzene.mol2")) + benzene.children[0].name = "Benzene" + box_of_benzene = fill_box(compound=benzene, n_compounds=5, density=1) + top = from_mbuild(box_of_benzene) + top.save("benzene.gro") + + reread = Topology.load("benzene.gro") + for site, ref_site in zip(reread.sites, top.sites): + assert site.molecule.name == ref_site.molecule.name[:3] + assert site.molecule.number == ref_site.molecule.number + + @pytest.mark.parametrize("fixture", ["benzene_ua_box", "benzene_aa_box"]) + def test_full_loop_gro_molecule(self, fixture, request): + top = request.getfixturevalue(fixture) + top.save("benzene.gro") + + # Re-read in and compare with reference + top = Topology.load("benzene.gro") + + refs = { + "benzene_aa_box": "benzene.gro", + "benzene_ua_box": "restrained_benzene_ua.gro", + } + ref = Topology.load(get_path(refs[fixture])) + + assert len(top.sites) == len(ref.sites) + assert top.unique_site_labels("molecule") == ref.unique_site_labels( + "molecule" + ) + + if top == "benzene_ua_box": + for site in top.sites: + assert site.molecule.name == "Com" + assert site.name == "_CH" + elif top == "benzene_aa_box": + for site in top.sites: + assert site.molecule.name == "Ben" + assert site.name in ["C", "H"] diff --git a/gmso/tests/test_top.py b/gmso/tests/test_top.py index ec8911e2a..4bb2679b2 100644 --- a/gmso/tests/test_top.py +++ b/gmso/tests/test_top.py @@ -1,3 +1,4 @@ +import forcefield_utilities as ffutils import parmed as pmd import pytest import unyt as u @@ -5,9 +6,13 @@ import gmso from gmso.exceptions import EngineIncompatibilityError from gmso.formats.top import write_top +from gmso.parameterization import apply from gmso.tests.base_test import BaseTest from gmso.tests.utils import get_path -from gmso.utils.io import get_fn +from gmso.utils.io import get_fn, has_mbuild, import_ + +if has_mbuild: + mb = import_("mbuild") class TestTop(BaseTest): @@ -90,6 +95,10 @@ def test_ethane_periodic(self, typed_ethane): for dihedral in typed_ethane.dihedrals: dihedral.connection_type = periodic_dihedral_type + for i in range(typed_ethane.n_impropers - 1, -1, -1): + if not typed_ethane.impropers[i].improper_type: + typed_ethane._impropers.pop(i) + typed_ethane.update_connection_types() typed_ethane.save("system.top") @@ -105,3 +114,96 @@ def test_custom_defaults(self, typed_ethane): assert struct.defaults.gen_pairs == "yes" assert struct.defaults.fudgeLJ == 0.5 assert struct.defaults.fudgeQQ == 0.5 + + def test_benzene_top(self, benzene_aa_box): + top = benzene_aa_box + oplsaa = ffutils.FoyerFFs().load("oplsaa").to_gmso_ff() + top = apply(top=top, forcefields=oplsaa, remove_untyped=True) + top.save("benzene.top") + + with open("benzene.top") as f: + f_cont = f.readlines() + + with open(get_path("benzene.top")) as ref: + ref_cont = ref.readlines() + + assert len(f_cont) == len(ref_cont) + + def test_benzene_restraints(self, benzene_ua_box): + top = benzene_ua_box + trappe_benzene = ( + ffutils.FoyerFFs() + .load(get_path("benzene_trappe-ua.xml")) + .to_gmso_ff() + ) + top = apply(top=top, forcefields=trappe_benzene, remove_untyped=True) + + for bond in top.bonds: + bond.restraint = { + "r_eq": bond.bond_type.parameters["r_eq"], + "k": 1000 * u.kJ / (u.mol * u.nm**2), + } + for angle in top.angles: + # Apply restraint for angle + angle.restraint = { + "theta_eq": angle.angle_type.parameters["theta_eq"], + "k": 1000 * u.kJ / u.mol, + "n": 1, + } + + for dihedral in top.dihedrals: + # Apply restraint fot dihedral + dihedral.restraint = { + "phi_eq": 0 * u.degree, + "delta_phi": 0 * u.degree, + "k": 1000 * u.kJ / (u.mol * u.rad**2), + } + top.save("restrained_benzene_ua.top") + + with open("restrained_benzene_ua.top") as f: + f_cont = f.readlines() + + with open(get_path("restrained_benzene_ua.top")) as ref: + ref_cont = ref.readlines() + + assert len(f_cont) == len(ref_cont) + + ref_sections = dict() + sections = dict() + current_section = None + for line, ref in zip(f_cont[1:], ref_cont[1:]): + if line.startswith("["): + assert line == ref + current_section = line + sections[current_section] = set() + ref_sections[current_section] = set() + elif line.startswith("#"): + assert line == ref + elif current_section is not None: + sections[current_section].add(line) + ref_sections[current_section].add(ref) + + for section, ref_section in zip(sections, ref_sections): + assert section == ref_section + if "dihedral" in section: + # Need to deal with these separatelt due to member's order issue + # Each dict will have the keys be members and values be their parameters + print(section) + members = dict() + ref_members = dict() + for line, ref in zip( + sections[section], ref_sections[ref_section] + ): + line = line.split() + ref = ref.split() + members["-".join(line[:4])] = line[4:] + members["-".join(reversed(line[:4]))] = line[4:] + ref_members["-".join(ref[:4])] = ref[4:] + ref_members["-".join(reversed(ref[:4]))] = ref[4:] + + assert members == ref_members + for member in members: + assert members[member] == ref_members[member] + + else: + assert sections[section] == ref_sections[ref_section] diff --git a/gmso/utils/compatibility.py b/gmso/utils/compatibility.py index ed631654b..0437a26b2 100644 --- a/gmso/utils/compatibility.py +++ b/gmso/utils/compatibility.py @@ -1,10 +1,11 @@ """Determine if the parametrized gmso.topology can be written to an engine.""" import sympy +from gmso.core.views import PotentialFilters from gmso.exceptions import EngineIncompatibilityError -def check_compatibility(topology, accepted_potentials): +def check_compatibility(topology, accepted_potentials, simplify_check=False): """ Compare the potentials in a topology against a list of accepted potential templates. @@ -14,7 +15,8 @@ def check_compatibility(topology, accepted_potentials): The topology whose potentials to check. accepted_potentials: list A list of gmso.Potential objects to check against - + simplify_check : bool, optional, default=False + Simplify the sympy expression check, aka, only compare the expression string Returns ------- potential_forms_dict: dict @@ -24,8 +26,12 @@ def check_compatibility(topology, accepted_potentials): """ potential_forms_dict = dict() - for atom_type in topology.atom_types: - potential_form = _check_single_potential(atom_type, accepted_potentials) + for atom_type in topology.atom_types( + # filter_by=PotentialFilters.UNIQUE_NAME_CLASS + ): + potential_form = _check_single_potential( + atom_type, accepted_potentials, simplify_check + ) if not potential_form: raise EngineIncompatibilityError( f"Potential {atom_type} is not in the list of accepted_potentials {accepted_potentials}" @@ -33,9 +39,11 @@ def check_compatibility(topology, accepted_potentials): else: potential_forms_dict.update(potential_form) - for connection_type in topology.connection_types: + for connection_type in topology.connection_types( + # filter_by=PotentialFilters.UNIQUE_NAME_CLASS + ): potential_form = _check_single_potential( - connection_type, accepted_potentials + connection_type, accepted_potentials, simplify_check ) if not potential_form: raise EngineIncompatibilityError( @@ -47,10 +55,14 @@ def check_compatibility(topology, accepted_potentials): return potential_forms_dict -def _check_single_potential(potential, accepted_potentials): +def _check_single_potential(potential, accepted_potentials, simplify_check): """Check to see if a single given potential is in the list of accepted potentials.""" for ref in accepted_potentials: if ref.independent_variables == potential.independent_variables: - if sympy.simplify(ref.expression - potential.expression) == 0: - return {potential: ref.name} + if simplify_check: + if str(ref.expression) == str(potential.expression): + return {potential: ref.name} + else: + if sympy.simplify(ref.expression - potential.expression) == 0: + return {potential: ref.name} return False diff --git a/gmso/utils/io.py b/gmso/utils/io.py index 387b611aa..e2eafa2bc 100644 --- a/gmso/utils/io.py +++ b/gmso/utils/io.py @@ -132,14 +132,6 @@ def import_(module): except ImportError: has_mbuild = False -try: - import foyer - - has_foyer = True - del foyer -except ImportError: - has_foyer = False - try: import gsd