Skip to content

Commit

Permalink
More changes to ParmEd comparisons for testing and connection convers…
Browse files Browse the repository at this point in the history
…ions
  • Loading branch information
CalCraven committed Feb 3, 2023
1 parent 2f16b6d commit 6119f1b
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 98 deletions.
37 changes: 21 additions & 16 deletions gmso/core/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -1438,28 +1438,33 @@ def load(cls, filename, **kwargs):
loader = LoadersRegistry.get_callable(filename.suffix)
return loader(filename, **kwargs)

def convert_expressions(self, expressMap={}):
def convert_potential_styles(self, expressionMap={}):
"""Convert from one parameter form to another.
Parameters
----------
expressMap : dict, default={}
expressionMap : dict, default={}
map with keys of the potential type and the potential to change to
Examples
________
# Convert from RB torsions to OPLS torsions
top.convert_expressions({"dihedrals": "OPLSTorsionPotential"})
top.convert_potential_styles({"dihedrals": "OPLSTorsionPotential"})
"""
# TODO: Move most of this functionality to gmso.utils.conversions
# TODO: Raise errors
from gmso.utils.conversions import convert_ryckaert_to_opls, convert_opls_to_ryckaert
conversions_map = {
("OPLSTorsionPotential", "RyckaertBellemansTorsionPotential"): convert_opls_to_ryckaert,
("RyckaertBellemansTorsionPotential", "OPLSTorsionPotential"): convert_ryckaert_to_opls,
} #Map of all accessible conversions currently supported
for conv in expressMap:
for conn in getattr(self, conv):
current_expression = getattr(conn, conv[:-1] + "_type")
conversions = (current_expression.name, expressMap[conv])
new_conn_type = conversions_map.get(conversions)(current_expression)
setattr(conn, conv[:-1] + "_type", new_conn_type)
from gmso.utils.conversions import convert_topology_expressions

return convert_topology_expressions(self, expressionMap)

def convert_unit_styles(self, unitSet=set):
"""Convert from one set of base units to another.
Parameters
----------
unitSet : dict, default=set
set of base units to use for all expressions of the topology
Examples
________
# TODO
"""
from gmso.utils.conversions import convert_topology_units

return convert_topology_units(self, unitSet)
18 changes: 13 additions & 5 deletions gmso/external/convert_parmed.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,12 +414,16 @@ def _dihedral_types_from_pmd(structure, dihedral_types_member_map=None):

member_types = dihedral_types_member_map.get(id(dihedraltype))

ryckaert_bellemans_torsion_potential = lib["RyckaertBellemansTorsionPotential"]
name = ryckaert_bellemans_torsion_potential.name
expression = ryckaert_bellemans_torsion_potential.expression
variables = ryckaert_bellemans_torsion_potential.independent_variables

top_dihedraltype = gmso.DihedralType(
name=name,
parameters=dihedral_params,
expression="c0 * cos(phi)**0 + c1 * cos(phi)**1 + "
+ "c2 * cos(phi)**2 + c3 * cos(phi)**3 + c4 * cos(phi)**4 + "
+ "c5 * cos(phi)**5",
independent_variables="phi",
expression=expression,
independent_variables=variables,
member_types=member_types,
)
pmd_top_dihedraltypes[id(dihedraltype)] = top_dihedraltype
Expand Down Expand Up @@ -661,11 +665,15 @@ def _atom_types_from_gmso(top, structure, atom_map):
)
atype_element = element_by_atom_type(atom_type)
atype_rmin = atype_sigma * 2 ** (1 / 6) / 2 # to rmin/2
if atom_type.mass:
atype_mass = atom_type.mass.to("amu").value
else:
atype_mass = atype_element.mass.to("amu").value
# Create unique Parmed AtomType object
atype = pmd.AtomType(
atype_name,
None,
atype_element.mass,
atype_mass,
atype_element.atomic_number,
atype_charge,
)
Expand Down
87 changes: 65 additions & 22 deletions gmso/formats/lammpsdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

@saves_as(".lammps", ".lammpsdata", ".data")
@mark_WIP("Testing in progress")
def write_lammpsdata(top, filename, atom_style="full", unit_style="real"):
def write_lammpsdata(top, filename, atom_style="full", unit_style="real", strict_potentials=False, strict_units=False):
"""Output a LAMMPS data file.
Outputs a LAMMPS data file in the 'full' atom style format.
Expand All @@ -49,6 +49,9 @@ def write_lammpsdata(top, filename, atom_style="full", unit_style="real"):
Defines the style of atoms to be saved in a LAMMPS data file.
The following atom styles are currently supported: 'full', 'atomic', 'charge', 'molecular'
see http://lammps.sandia.gov/doc/atom_style.html for more information on atom styles.
strict : bool, optional, default False
Tells the writer how to treat conversions. If strict=False, then check for conversions
of unit styles in #TODO
Notes
-----
Expand All @@ -70,26 +73,39 @@ def write_lammpsdata(top, filename, atom_style="full", unit_style="real"):
# TODO: Support various unit styles ["metal", "si", "cgs", "electron", "micro", "nano"]
if unit_style not in ["real"]:
raise ValueError(
'Atom style "{}" is invalid or is not currently supported'.format(
'Unit style "{}" is invalid or is not currently supported'.format(
unit_style
)
)
# Use gmso unit packages to get into correct lammps formats
default_unit_maps = {"real": "TODO"}
default_parameter_maps = { # Add more as needed
"dihedrals":"OPLSTorsionPotential",
"angles":"HarmonicAnglePotential",
"bonds":"HarmonicBondPotential",
#"atoms":"LennardJonesPotential",
#"electrostatics":"CoulombicPotential"
}

# TODO: Use strict_x to validate depth of topology checking
if strict_units:
_validate_unit_compatibility(top, default_unit_maps[unit_style])
else:
# Use gmso unit packages to get into correct lammps formats
unit_maps = {"real": "TODO"}
#top = top.convert_potentials({"all": unit_maps[unit_style]})
top = _try_default_unit_conversions(top, default_unit_maps[unit_style])

# Check for use of correct potential forms for lammps writer
pot_types = _validate_compatibility(top)

if strict_potentials:
print("I'm strict about potential forms")
_validate_potential_compatibility(top)
else:
top = _try_default_potential_conversions(top, default_parameter_maps)

# TODO: improve handling of various filenames
path = Path(filename)
if not path.parent.exists():
msg = "Provided path to file that does not exist"
raise FileNotFoundError(msg)

# TODO: More validating/preparing of top before writing stage

with open(path, "w") as out_file:
_write_header(out_file, top, atom_style)
_write_box(out_file, top)
Expand Down Expand Up @@ -415,22 +431,27 @@ def _accepted_potentials():
harmonic_bond_potential = templates["HarmonicBondPotential"]
harmonic_angle_potential = templates["HarmonicAnglePotential"]
periodic_torsion_potential = templates["PeriodicTorsionPotential"]
opls_torsion_potential = templates["OPLSTorsionPotential"]
accepted_potentials = [
fourier_torsion_potential = templates["FourierTorsionPotential"]
accepted_potentialsList = [
lennard_jones_potential,
harmonic_bond_potential,
harmonic_angle_potential,
periodic_torsion_potential,
opls_torsion_potential,
fourier_torsion_potential,
]
return accepted_potentials
return accepted_potentialsList


def _validate_compatibility(top):
"""Check compatability of topology object with GROMACS TOP format."""
def _validate_potential_compatibility(top):
"""Check compatability of topology object potentials with LAMMPSDATA format."""
pot_types = check_compatibility(top, _accepted_potentials())
return pot_types

def _validate_unit_compatibility(top, unitSet):
"""Check compatability of topology object units with LAMMPSDATA format."""
# TODO: Check to make sure all units are in the correct format
return True

# All writer worker function belows
def _write_header(out_file, top, atom_style):
"""Write Lammps file header"""
Expand All @@ -449,10 +470,14 @@ def _write_header(out_file, top, atom_style):

# TODO: allow users to specify filter_by syntax
out_file.write("\n{:d} atom types\n".format(len(top.atom_types(filter_by=pfilters.UNIQUE_NAME_CLASS))))
out_file.write("{:d} bond types\n".format(len(top.bond_types(filter_by=pfilters.UNIQUE_NAME_CLASS))))
out_file.write("{:d} angle types\n".format(len(top.angle_types(filter_by=pfilters.UNIQUE_NAME_CLASS))))
out_file.write("{:d} dihedral types\n".format(len(top.dihedral_types(filter_by=pfilters.UNIQUE_NAME_CLASS))))
out_file.write("{:d} improper types\n".format(len(top.improper_types(filter_by=pfilters.UNIQUE_NAME_CLASS))))
if top.n_bonds > 0:
out_file.write("{:d} bond types\n".format(len(top.bond_types(filter_by=pfilters.UNIQUE_NAME_CLASS))))
if top.n_angles > 0:
out_file.write("{:d} angle types\n".format(len(top.angle_types(filter_by=pfilters.UNIQUE_NAME_CLASS))))
if top.n_dihedrals > 0:
out_file.write("{:d} dihedral types\n".format(len(top.dihedral_types(filter_by=pfilters.UNIQUE_NAME_CLASS))))
if top.n_impropers > 0:
out_file.write("{:d} improper types\n".format(len(top.improper_types(filter_by=pfilters.UNIQUE_NAME_CLASS))))

out_file.write("\n")

Expand Down Expand Up @@ -551,7 +576,7 @@ def _write_pairtypes(out_file, top):
# TODO: Utilize unit styles and nonbonded equations properly
# Pair coefficients
test_atmtype = top.sites[0].atom_type
out_file.write(f"\nPair Coeffs #{test_atmtype.name}\n")
out_file.write(f"\nPair Coeffs # lj\n") # TODO: This should be pulled from the test_atmtype
# TODO: use unit style specified for writer
param_labels = map(lambda x: f"{x} ({test_atmtype.parameters[x].units})", test_atmtype.parameters)
out_file.write("#\t" + "\t".join(param_labels) + "\n")
Expand Down Expand Up @@ -616,9 +641,8 @@ def _write_angletypes(out_file, top):

def _write_dihedraltypes(out_file, top):
"""Write out dihedrals to LAMMPS file."""
# TODO: Make sure to perform unit conversions
# TODO: Use any accepted lammps parameters
test_dihtype = top.dihedrals[0].dihedral_type
print(test_dihtype.parameters)
out_file.write(f"\nDihedral Coeffs #{test_dihtype.name}\n")
param_labels = map(lambda x: f"{x} ({test_dihtype.parameters[x].units})", test_dihtype.parameters)
out_file.write("#\t" + "\t".join(param_labels) + "\n")
Expand Down Expand Up @@ -697,8 +721,27 @@ def _write_conn_data(out_file, top, connIter, connStr):
# TODO: Allow for unit system passing
# TODO: Validate that all connections are written in the correct order
out_file.write(f"\n{connStr.capitalize()}\n\n")
indexList = list(map(id, getattr(top, connStr[:-1] + '_types')(filter_by=pfilters.UNIQUE_NAME_CLASS)))
print(f"Indexed list for {connStr} is {indexList}")
for i, conn in enumerate(getattr(top, connStr)):
print(f"{connStr}: id:{id(conn.connection_type)} of form {conn.connection_type}")
typeStr = f"{i+1:d}\t{getattr(top, connStr[:-1] + '_types')(filter_by=pfilters.UNIQUE_NAME_CLASS).equality_index(conn.connection_type) + 1:1}\t"
indexStr = "\t".join(map(lambda x: str(top.sites.index(x)+1), conn.connection_members))
out_file.write(typeStr + indexStr + "\n")

def _try_default_potential_conversions(top, potentialsDict):
# TODO: Docstrings
return top.convert_potential_styles(potentialsDict)

def _try_default_unit_conversions(top, unitSet):
# TODO: Docstrings
try:
return top # TODO: Remote this once implemented
top = top.convert_unit_styles(unitSet)
except:
raise ValueError(
'Unit style "{}" cannot be converted from units used in potential expressions. Check the forcefield for consisten units'.format(
unit_style
)
)
return top
11 changes: 11 additions & 0 deletions gmso/lib/jsons/FourierTorsionPotential.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "FourierTorsionPotential",
"expression": "0.5 * k1 * (1 + cos(phi)) + 0.5 * k2 * (1 - cos(2*phi)) + 0.5 * k3 * (1 + cos(3*phi)) + 0.5 * k4 * (1 - cos(4*phi))",
"independent_variables": "phi",
"expected_parameters_dimensions": {
"k1": "energy",
"k2": "energy",
"k3": "energy",
"k4": "energy"
}
}
4 changes: 2 additions & 2 deletions gmso/tests/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ def typed_ethane(self):
def typed_ethane_opls(self, typed_ethane):
for dihedral in typed_ethane.dihedrals:
dihedral.dihedral_type.name = "RyckaertBellemansTorsionPotential"
typed_ethane.convert_expressions({"dihedrals": "OPLSTorsionPotential"})
return typed_ethane
top = typed_ethane.convert_potential_styles({"dihedrals": "FourierTorsionPotential"})
return top

@pytest.fixture
def parmed_ethane(self):
Expand Down
14 changes: 14 additions & 0 deletions gmso/tests/test_conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@


class TestKelvinToEnergy(BaseTest):
def test_convert_potential_styles(self, typed_ethane):
from sympy import sympify
rb_expr = sympify("c0 * cos(phi)**0 + c1 * cos(phi)**1 + c2 * cos(phi)**2 + c3 * cos(phi)**3 + c4 * cos(phi)**4 + c5 * cos(phi)**5")
assert typed_ethane.dihedrals[0].dihedral_type.expression == rb_expr
for dihedral in typed_ethane.dihedrals:
dihedral.dihedral_type.name = "RyckaertBellemansTorsionPotential"
typed_ethane.convert_potential_styles({"dihedrals": "OPLSTorsionPotential"})
opls_expr = sympify(
"0.5 * k0 + 0.5 * k1 * (1 + cos(phi)) + 0.5 * k2 * (1 - cos(2*phi)) + \
0.5 * k3 * (1 + cos(3*phi)) + 0.5 * k4 * (1 - cos(4*phi))"
)
assert typed_ethane.dihedrals[0].dihedral_type.expression == opls_expr
assert typed_ethane.dihedrals[0].dihedral_type.name == "OPLSTorsionPotential"

def test_K_to_kcal(self):
input_value = 1 * u.Kelvin / u.nm**2
new_value = convert_kelvin_to_energy_units(
Expand Down
Loading

0 comments on commit 6119f1b

Please sign in to comment.