diff --git a/gmso/abc/abstract_site.py b/gmso/abc/abstract_site.py
index 9672141a2..3ea20c829 100644
--- a/gmso/abc/abstract_site.py
+++ b/gmso/abc/abstract_site.py
@@ -137,7 +137,8 @@ def is_valid_position(cls, position):
try:
position = np.reshape(position, newshape=(3,), order="C")
- position.convert_to_units(u.nm)
+ if position.units != u.dimensionless:
+ position.convert_to_units(u.nm)
except ValueError:
raise ValueError(
f"Position of shape {position.shape} is not valid. "
diff --git a/gmso/core/box.py b/gmso/core/box.py
index 20b9f843c..922a6f840 100644
--- a/gmso/core/box.py
+++ b/gmso/core/box.py
@@ -21,7 +21,8 @@ def _validate_lengths(lengths):
np.reshape(lengths, newshape=(3,), order="C")
lengths *= input_unit
- lengths.convert_to_units(u.nm)
+ if input_unit != u.Unit("dimensionless"):
+ lengths.convert_to_units(u.nm)
if np.any(
np.less(
diff --git a/gmso/core/topology.py b/gmso/core/topology.py
index 4de86a658..6cebb2772 100644
--- a/gmso/core/topology.py
+++ b/gmso/core/topology.py
@@ -26,6 +26,10 @@
from gmso.utils.connectivity import (
identify_connections as _identify_connections,
)
+from gmso.utils.conversions import (
+ convert_params_units,
+ convert_topology_expressions,
+)
from gmso.utils.units import GMSO_UnitRegsitry as UnitReg
scaling_interaction_idxes = {"12": 0, "13": 1, "14": 2}
@@ -1693,6 +1697,62 @@ def load(cls, filename, **kwargs):
loader = LoadersRegistry.get_callable(filename.suffix)
return loader(filename, **kwargs)
+ def convert_potential_styles(self, expressionMap={}):
+ """Convert from one parameter form to another.
+
+ Parameters
+ ----------
+ expressionMap : dict, default={}
+ Map where the keys represent the current potential
+ type and the corresponding values represent the desired
+ potential type. The desired potential style can be
+ either a string with the corresponding name, or
+ a gmso.utils.expression.PotentialExpression type.
+
+ Examples
+ ________
+ # Convert from RB torsions to OPLS torsions
+ top.convert_potential_styles({"dihedrals": "OPLSTorsionPotential"})
+ # TODO: convert_potential_styles with PotentialExpression
+ """
+ # TODO: raise warnings for improper values or keys in expressionMap
+
+ return convert_topology_expressions(self, expressionMap)
+
+ def convert_unit_styles(self, unitsystem, exp_unitsDict):
+ """Convert from one set of base units to another.
+
+ Parameters
+ ----------
+ unitsystem : unyt.UnitSystem
+ set of base units to use for all expressions of the topology
+ in `unyt package _`
+ exp_unitsDict : dict
+ keys with topology attributes that should be converted and
+ values with dictionary of parameter: expected_dimension
+
+ Examples
+ ________
+ top.convert_unit_styles(
+ u.UnitSystem(
+ "lammps_real", "Å", "amu", "fs", "K", "rad",
+ ),
+ {"bond":{"k":"energy/length**2", "r_eq":"length"}},
+ )
+ """
+
+ ref_values = {"energy": "kJ/mol", "length": "nm", "angle": "radians"}
+
+ # all potContainer ["atom", "bond", "angle", "dihedral", "improper"]
+ for potStr in exp_unitsDict:
+ potContainer = getattr(self, potStr + "_types")
+ convert_params_units(
+ potContainer,
+ expected_units_dim=exp_unitsDict[potStr],
+ base_units=unitsystem,
+ ref_values=ref_values,
+ )
+
def _return_float_for_unyt(unyt_quant, unyts_bool):
try:
diff --git a/gmso/core/views.py b/gmso/core/views.py
index 007710a58..0ed3be1e4 100644
--- a/gmso/core/views.py
+++ b/gmso/core/views.py
@@ -57,7 +57,13 @@ def get_sorted_names(potential):
else:
return potential.member_types
elif isinstance(potential, ImproperType):
- return (potential.member_types[0], *sorted(potential.member_types[1:]))
+ return (
+ potential.member_types[0],
+ *potential.member_types[1:],
+ ) # could sort using `sorted`
+ return ValueError(
+ f"Potential {potential} not one of {potential_attribute_map.values()}"
+ )
def get_parameters(potential):
@@ -170,6 +176,13 @@ def index(self, item):
for j, potential in enumerate(self.yield_view()):
if potential is item:
return j
+ return None
+
+ def equality_index(self, item):
+ for j, potential in enumerate(self.yield_view()):
+ if potential == item:
+ return j
+ return None
def _collect_potentials(self):
"""Collect potentials from the iterator"""
diff --git a/gmso/external/convert_mbuild.py b/gmso/external/convert_mbuild.py
index 7d77f957d..40d2bcfdf 100644
--- a/gmso/external/convert_mbuild.py
+++ b/gmso/external/convert_mbuild.py
@@ -314,7 +314,7 @@ def _parse_molecule_residue(site_map, compound):
if molecule_tag.name in molecule_tracker:
molecule_tracker[molecule_tag.name] += 1
else:
- molecule_tracker[molecule_tag.name] = 0
+ molecule_tracker[molecule_tag.name] = 1
molecule_number = molecule_tracker[molecule_tag.name]
"""End of molecule parsing"""
@@ -329,7 +329,7 @@ def _parse_molecule_residue(site_map, compound):
residue_tracker[residue_tag.name]
)
else:
- residue_tracker[residue_tag.name] = {residue_tag: 0}
+ residue_tracker[residue_tag.name] = {residue_tag: 1}
residue_number = residue_tracker[residue_tag.name][residue_tag]
site_map[particle]["residue"] = (residue_tag.name, residue_number)
diff --git a/gmso/external/convert_parmed.py b/gmso/external/convert_parmed.py
index f8f2fba19..6338c80a6 100644
--- a/gmso/external/convert_parmed.py
+++ b/gmso/external/convert_parmed.py
@@ -1,4 +1,5 @@
"""Module support for converting to/from ParmEd objects."""
+import copy
import warnings
from operator import attrgetter, itemgetter
@@ -7,7 +8,7 @@
from symengine import expand
import gmso
-from gmso.core.element import element_by_atom_type, element_by_atomic_number
+from gmso.core.element import element_by_atomic_number, element_by_symbol
from gmso.core.views import PotentialFilters, get_parameters
pfilter = PotentialFilters.UNIQUE_PARAMETERS
@@ -71,12 +72,12 @@ def from_parmed(structure, refer_type=True):
charge=atom.charge * u.elementary_charge,
position=[atom.xx, atom.xy, atom.xz] * u.angstrom,
atom_type=None,
- residue=(residue.name, residue.idx),
+ residue=(residue.name, residue.idx + 1),
element=element,
)
- site.molecule = (residue.name, residue.idx) if ind_res else None
+ site.molecule = (residue.name, residue.idx + 1) if ind_res else None
site.atom_type = (
- pmd_top_atomtypes[atom.atom_type]
+ copy.deepcopy(pmd_top_atomtypes[atom.atom_type])
if refer_type and isinstance(atom.atom_type, pmd.AtomType)
else None
)
@@ -102,6 +103,7 @@ def from_parmed(structure, refer_type=True):
}
_add_conn_type_from_pmd(
connStr="BondType",
+ pmd_conn=bond,
gmso_conn=top_connection,
conn_params=conn_params,
name=name,
@@ -124,11 +126,12 @@ def from_parmed(structure, refer_type=True):
)
if refer_type and isinstance(angle.type, pmd.AngleType):
conn_params = {
- "k": (2 * angle.type.k * u.Unit("kcal / (rad**2 * mol)")),
+ "k": (2 * angle.type.k * u.Unit("kcal / (radian**2 * mol)")),
"theta_eq": (angle.type.theteq * u.degree),
}
_add_conn_type_from_pmd(
connStr="AngleType",
+ pmd_conn=angle,
gmso_conn=top_connection,
conn_params=conn_params,
name=name,
@@ -164,6 +167,7 @@ def from_parmed(structure, refer_type=True):
}
_add_conn_type_from_pmd(
connStr="ImproperType",
+ pmd_conn=dihedral,
gmso_conn=top_connection,
conn_params=conn_params,
name=name_improper,
@@ -186,6 +190,7 @@ def from_parmed(structure, refer_type=True):
}
_add_conn_type_from_pmd(
connStr="DihedralType",
+ pmd_conn=dihedral,
gmso_conn=top_connection,
conn_params=conn_params,
name=name_proper,
@@ -221,6 +226,7 @@ def from_parmed(structure, refer_type=True):
}
_add_conn_type_from_pmd(
connStr="DihedralType",
+ pmd_conn=rb_torsion,
gmso_conn=top_connection,
conn_params=conn_params,
name=name,
@@ -240,7 +246,7 @@ def from_parmed(structure, refer_type=True):
connection_members=_sort_improper_members(
top,
site_map,
- *attrgetter("atom1", "atom2", "atom3", "atom4")(improper),
+ *attrgetter("atom3", "atom2", "atom1", "atom4")(improper),
)
)
if refer_type and isinstance(improper.type, pmd.ImproperType):
@@ -250,6 +256,7 @@ def from_parmed(structure, refer_type=True):
}
_add_conn_type_from_pmd(
connStr="ImproperType",
+ pmd_conn=improper,
gmso_conn=top_connection,
conn_params=conn_params,
name=name,
@@ -281,11 +288,11 @@ def _atom_types_from_pmd(structure):
A dictionary linking a pmd.AtomType object to its
corresponding GMSO.AtomType object.
"""
- unique_atom_types = set()
- for atom in structure.atoms:
- if isinstance(atom.atom_type, pmd.AtomType):
- unique_atom_types.add(atom.atom_type)
- unique_atom_types = list(unique_atom_types)
+ unique_atom_types = [
+ atom.atom_type
+ for atom in structure.atoms
+ if isinstance(atom.atom_type, pmd.AtomType)
+ ]
pmd_top_atomtypes = {}
for atom_type in unique_atom_types:
if atom_type.atomic_number:
@@ -302,7 +309,7 @@ def _atom_types_from_pmd(structure):
"epsilon": atom_type.epsilon * u.Unit("kcal / mol"),
},
independent_variables={"r"},
- mass=atom_type.mass,
+ mass=copy.deepcopy(atom_type.mass),
)
pmd_top_atomtypes[atom_type] = top_atomtype
return pmd_top_atomtypes
@@ -342,7 +349,7 @@ def _sort_improper_members(top, site_map, atom1, atom2, atom3, atom4):
def _add_conn_type_from_pmd(
- connStr, gmso_conn, conn_params, name, expression, variables
+ connStr, pmd_conn, gmso_conn, conn_params, name, expression, variables
):
"""Convert ParmEd dihedral types to GMSO DihedralType.
@@ -408,6 +415,9 @@ def to_parmed(top, refer_type=True):
msg = "Provided argument is not a topology.Topology."
assert isinstance(top, gmso.Topology)
+ # Copy structure to not overwrite object in memory
+ top = copy.deepcopy(top)
+
# Set up Parmed structure and define general properties
structure = pmd.Structure()
structure.title = top.name
@@ -450,7 +460,9 @@ def to_parmed(top, refer_type=True):
# Add atom to structure
if site.residue:
structure.add_atom(
- pmd_atom, resname=site.residue.name, resnum=site.residue.number
+ pmd_atom,
+ resname=site.residue.name,
+ resnum=site.residue.number - 1,
)
else:
structure.add_atom(pmd_atom, resname="RES", resnum=-1)
@@ -561,14 +573,20 @@ def _atom_types_from_gmso(top, structure, atom_map):
atype_epsilon = float(
atom_type.parameters["epsilon"].to("kcal/mol").value
)
- atype_element = element_by_atom_type(atom_type)
+ if atom_type.mass:
+ atype_mass = atom_type.mass.to("amu").value
+ else:
+ atype_mass = element_by_symbol(atom_type.name).mass.to("amu").value
+ atype_atomic_number = getattr(
+ element_by_symbol(atom_type.name), "atomic_number", None
+ )
atype_rmin = atype_sigma * 2 ** (1 / 6) / 2 # to rmin/2
# Create unique Parmed AtomType object
atype = pmd.AtomType(
atype_name,
None,
atype_mass,
- atype_element.atomic_number,
+ atype_atomic_number,
atype_charge,
)
atype.set_lj_params(atype_epsilon, atype_rmin)
@@ -645,7 +663,7 @@ def _angle_types_from_gmso(top, structure, angle_map):
), msg
# Extract Topology angle_type information
agltype_k = 0.5 * float(
- angle_type.parameters["k"].to("kcal / (rad**2 * mol)").value
+ angle_type.parameters["k"].to("kcal / (radian**2 * mol)").value
)
agltype_theta_eq = float(
angle_type.parameters["theta_eq"].to("degree").value
@@ -729,10 +747,17 @@ def _dihedral_types_from_gmso(top, structure, dihedral_map):
)
# Create unique DihedralType object
dtype = pmd.RBTorsionType(
- dtype_c0, dtype_c1, dtype_c2, dtype_c3, dtype_c4, dtype_c5
+ dtype_c0,
+ dtype_c1,
+ dtype_c2,
+ dtype_c3,
+ dtype_c4,
+ dtype_c5,
+ list=structure.rb_torsion_types,
)
# Add RBTorsionType to structure.rb_torsion_types
structure.rb_torsion_types.append(dtype)
+ # dtype._idx = len(structure.rb_torsion_types) - 1
else:
raise GMSOError("msg")
dtype_map[get_parameters(dihedral_type)] = dtype
diff --git a/gmso/formats/gro.py b/gmso/formats/gro.py
index a702f68d6..bc113a2b8 100644
--- a/gmso/formats/gro.py
+++ b/gmso/formats/gro.py
@@ -80,8 +80,8 @@ def read_gro(filename):
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)
+ site.molecule = (m.group(2), int(m.group(1)))
+ site.residue = (m.group(2), int(m.group(1)))
top.add_site(site, update_types=False)
top.update_topology()
diff --git a/gmso/formats/lammpsdata.py b/gmso/formats/lammpsdata.py
index a4d826491..97c865b8d 100644
--- a/gmso/formats/lammpsdata.py
+++ b/gmso/formats/lammpsdata.py
@@ -1,14 +1,21 @@
"""Read and write LAMMPS data files."""
from __future__ import division
+import copy
import datetime
+import os
+import re
import warnings
+from pathlib import Path
import numpy as np
import unyt as u
-from sympy import simplify, sympify
+from sympy import Symbol
+from unyt import UnitRegistry
from unyt.array import allclose_units
+import gmso
+from gmso.abc.abstract_site import MoleculeType
from gmso.core.angle import Angle
from gmso.core.angle_type import AngleType
from gmso.core.atom import Atom
@@ -16,18 +23,242 @@
from gmso.core.bond import Bond
from gmso.core.bond_type import BondType
from gmso.core.box import Box
+from gmso.core.dihedral import Dihedral
from gmso.core.element import element_by_mass
+from gmso.core.improper import Improper
from gmso.core.topology import Topology
+from gmso.core.views import PotentialFilters, get_sorted_names
+
+pfilter = PotentialFilters.UNIQUE_SORTED_NAMES
+from gmso.exceptions import NotYetImplementedWarning
from gmso.formats.formats_registry import loads_as, saves_as
from gmso.lib.potential_templates import PotentialTemplateLibrary
+from gmso.utils.compatibility import check_compatibility
from gmso.utils.conversions import (
convert_opls_to_ryckaert,
convert_ryckaert_to_opls,
)
+# TODO: move this to gmso.utils.units.py
+reg = UnitRegistry()
+dim = u.dimensions.current_mks * u.dimensions.time
+conversion = 1 * getattr(u.physical_constants, "elementary_charge").value
+reg.add(
+ "elementary_charge",
+ base_value=conversion,
+ dimensions=dim,
+ tex_repr=r"\rm{e}",
+)
+conversion = 1 * getattr(u.physical_constants, "boltzmann_constant_mks").value
+dim = u.dimensions.energy / u.dimensions.temperature
+reg.add(
+ "kb", base_value=conversion, dimensions=dim, tex_repr=r"\rm{kb}"
+) # boltzmann temperature
+conversion = (
+ 4
+ * np.pi
+ * getattr(u.physical_constants, "reduced_planck_constant").value ** 2
+ * getattr(u.physical_constants, "eps_0").value
+ / (
+ getattr(u.physical_constants, "electron_charge").value ** 2
+ * getattr(u.physical_constants, "electron_mass").value
+ )
+)
+dim = u.dimensions.length
+reg.add(
+ "a0", base_value=conversion, dimensions=dim, tex_repr=r"\rm{a0}"
+) # bohr radius
+conversion = (
+ getattr(u.physical_constants, "reduced_planck_constant").value ** 2
+ / u.Unit("a0", registry=reg).base_value ** 2
+ / getattr(u.physical_constants, "electron_mass").value
+)
+dim = u.dimensions.energy
+reg.add(
+ "Ehartree", base_value=conversion, dimensions=dim, tex_repr=r"\rm{Ehartree}"
+) # Hartree energy
+conversion = np.sqrt(
+ 10**9 / (4 * np.pi * getattr(u.physical_constants, "eps_0").value)
+)
+dim = u.dimensions.charge
+reg.add(
+ "Statcoulomb_charge",
+ base_value=conversion,
+ dimensions=dim,
+ tex_repr=r"\rm{Statcoulomb_charge}",
+) # Static charge
+
+
+def _unit_style_factory(style: str):
+ # NOTE: the when an angle is measured in lammps is not straightforwards. It depends not on the unit_style, but on the
+ # angle_style, dihedral_style, or improper_style. For examples, harmonic angles, k is specificed in energy/radian, but the
+ # theta_eq is written in degrees. For fourier dihedrals, d_eq is specified in degrees. When adding new styles, make sure that
+ # this behavior is accounted for when converting the specific potential_type in the function
+ # _parameter_converted_to_float
+ if style == "real":
+ base_units = u.UnitSystem(
+ "lammps_real", "Å", "amu", "fs", "K", "rad", registry=reg
+ )
+ base_units["energy"] = "kcal/mol"
+ base_units["charge"] = "elementary_charge"
+ elif style == "metal":
+ base_units = u.UnitSystem(
+ "lammps_metal", "Å", "amu", "picosecond", "K", "rad", registry=reg
+ )
+ base_units["energy"] = "eV"
+ base_units["charge"] = "elementary_charge"
+ elif style == "si":
+ base_units = u.UnitSystem(
+ "lammps_si", "m", "kg", "s", "K", "rad", registry=reg
+ )
+ base_units["energy"] = "joule"
+ base_units["charge"] = "coulomb"
+ elif style == "cgs":
+ base_units = u.UnitSystem(
+ "lammps_cgs", "cm", "g", "s", "K", "rad", registry=reg
+ )
+ base_units["energy"] = "erg"
+ # Statcoulomb is strange. It is not a 1:1 correspondance to charge, with base units of
+ # mass**1/2*length**3/2*time**-1.
+ # However, assuming it is referring to a static charge and not a flux, it can be
+ # converted to coulomb units. See the registry for the unit conversion to Coulombs
+ base_units["charge"] = "Statcoulomb_charge"
+ elif style == "electron":
+ base_units = u.UnitSystem(
+ "lammps_electron", "a0", "amu", "s", "K", "rad", registry=reg
+ )
+ base_units["energy"] = "Ehartree"
+ base_units["charge"] = "elementary_charge"
+ elif style == "micro":
+ base_units = u.UnitSystem(
+ "lammps_micro", "um", "picogram", "us", "K", "rad", registry=reg
+ )
+ base_units["energy"] = "ug*um**2/us**2"
+ base_units["charge"] = "picocoulomb"
+ elif style == "nano":
+ base_units = u.UnitSystem(
+ "lammps_nano", "nm", "attogram", "ns", "K", "rad", registry=reg
+ )
+ base_units["energy"] = "attogram*nm**2/ns**2"
+ base_units["charge"] = "elementary_charge"
+ elif style == "lj":
+ base_units = ljUnitSystem()
+ else:
+ raise NotYetImplementedWarning
+
+ return base_units
+
+
+class ljUnitSystem:
+ """Use this so the empty unitsystem has getitem magic method."""
+
+ def __init__(self):
+ self.registry = reg
+ self.name = "lj"
+
+ def __getitem__(self, items):
+ """Return dimensionless units."""
+ return "dimensionless"
+
+
+def _parameter_converted_to_float(
+ parameter,
+ base_unyts,
+ conversion_factorDict=None,
+ n_decimals=3,
+ name="",
+):
+ """Take a given parameter, and return a float of the parameter in the given style.
+
+ This function will check the base_unyts, which is a unyt.UnitSystem object,
+ and convert the parameter to those units based on its dimensions. It can
+ also generate dimensionless units via normalization from conversion_factorsDict.
+ # TODO: move this to gmso.utils.units.py
+ """
+ # TODO: now I think phi_eq is what is really saved in the improper angle
+ if name in ["theta_eq", "chieq"]: # eq angle are always in degrees
+ return round(float(parameter.to("degree").value), n_decimals)
+ new_dims = _dimensions_to_energy(parameter.units.dimensions)
+ new_dims = _dimensions_to_charge(new_dims)
+ if conversion_factorDict and isinstance(base_unyts, ljUnitSystem):
+ # multiply object -> split into length, mass, energy, charge -> grab conversion factor from dict
+ # first replace energy for (length)**2*(mass)/(time)**2 u.dimensions.energy. Then iterate through the free symbols
+ # and figure out a way how to add those to the overall conversion factor
+ dim_info = new_dims.as_terms()
+ conversion_factor = 1
+ for exponent, ind_dim in zip(dim_info[0][0][1][1], dim_info[1]):
+ factor = conversion_factorDict.get(
+ ind_dim.name[1:-1], 1
+ ) # replace () in name
+ conversion_factor *= float(factor) ** exponent
+ return float(
+ parameter / conversion_factor
+ ) # Assuming that conversion factor is in right units
+ new_dimStr = str(new_dims)
+ ind_units = re.sub("[^a-zA-Z]+", " ", new_dimStr).split()
+ for unit in ind_units:
+ new_dimStr = new_dimStr.replace(unit, str(base_unyts[unit]))
+
+ return round(
+ float(parameter.to(u.Unit(new_dimStr, registry=base_unyts.registry))),
+ n_decimals,
+ )
+
+
+def _dimensions_to_energy(dims):
+ """Take a set of dimensions and substitute in Symbol("energy") where possible."""
+ # TODO: move this to gmso.utils.units.py
+ symsStr = str(dims.free_symbols)
+ energy_inBool = np.all([dimStr in symsStr for dimStr in ["time", "mass"]])
+ if not energy_inBool:
+ return dims
+ energySym = Symbol("(energy)") # create dummy symbol to replace in equation
+ dim_info = dims.as_terms()
+ time_idx = np.where(list(map(lambda x: x.name == "(time)", dim_info[1])))[
+ 0
+ ][0]
+ energy_exp = (
+ dim_info[0][0][1][1][time_idx] // 2
+ ) # energy has 1/time**2 in it, so this is the hint of how many
+ return (
+ dims
+ * u.dimensions.energy**energy_exp
+ * energySym ** (-1 * energy_exp)
+ )
+
+
+def _dimensions_to_charge(dims):
+ """Take a set of dimensions and substitute in Symbol("charge") where possible."""
+ # TODO: move this to gmso.utils.units.py
+ symsStr = str(dims.free_symbols)
+ charge_inBool = np.all([dimStr in symsStr for dimStr in ["current_mks"]])
+ if not charge_inBool:
+ return dims
+ chargeSym = Symbol("(charge)") # create dummy symbol to replace in equation
+ dim_info = dims.as_terms()
+ time_idx = np.where(
+ list(map(lambda x: x.name == "(current_mks)", dim_info[1]))
+ )[0][0]
+ charge_exp = dim_info[0][0][1][1][
+ time_idx
+ ] # charge has (current_mks) in it, so this is the hint of how many
+ return (
+ dims
+ * u.dimensions.charge ** (-1 * charge_exp)
+ * chargeSym**charge_exp
+ )
+
@saves_as(".lammps", ".lammpsdata", ".data")
-def write_lammpsdata(topology, filename, atom_style="full"):
+def write_lammpsdata(
+ top,
+ filename,
+ atom_style="full",
+ unit_style="real",
+ strict_potentials=False,
+ strict_units=False,
+ lj_cfactorsDict=None,
+):
"""Output a LAMMPS data file.
Outputs a LAMMPS data file in the 'full' atom style format.
@@ -44,326 +275,127 @@ def write_lammpsdata(topology, filename, atom_style="full"):
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.
+ unit_style : str, optional, default='real'
+ Can be any of "real", "lj", "metal", "si", "cgs", "electron", "micro", "nano". Otherwise
+ an error will be thrown. These are defined in _unit_style_factory. See
+ https://docs.lammps.org/units.html for LAMMPS documentation.
+ strict_potentials : bool, optional, default False
+ Tells the writer how to treat conversions. If False, then check for conversions
+ to usable potential styles found in default_parameterMaps. If True, then error if
+ potentials are not compatible.
+ strict_units : bool, optional, default False
+ Tells the writer how to treat unit conversions. If False, then check for conversions
+ to unit styles defined in _unit_style_factory. If True, then error if parameter units
+ do not match.
+ lj_cfactorsDict : (None, dict), optional, default None
+ If using unit_style="lj" only, can pass a dictionary with keys of ("mass", "energy",
+ "length", "charge"), or any combination of these, and they will be used to non-
+ dimensionalize all values in the topology. If any key is not passed, default values
+ will be pulled from the topology (see _default_lj_val). These are the largest: sigma,
+ epsilon, atomtype.mass, and atomtype.charge from the topology.
Notes
-----
See http://lammps.sandia.gov/doc/2001/data_format.html for a full description of the LAMMPS data format.
- This is a work in progress, as only atoms, masses, and atom_type information can be written out.
+ This is a work in progress, as only a subset of everything LAMMPS supports is currently available.
+ However, please raise issues as the current writer has been set up to eventually grow to support
+ all LAMMPS styles.
Some of this function has been adopted from `mdtraj`'s support of the LAMMPSTRJ trajectory format.
See https://github.com/mdtraj/mdtraj/blob/master/mdtraj/formats/lammpstrj.py for details.
"""
- if atom_style not in ["atomic", "charge", "molecular", "full"]:
+ if atom_style not in ["full", "atomic", "molecular", "charge"]:
raise ValueError(
'Atom style "{}" is invalid or is not currently supported'.format(
atom_style
)
)
- # TODO: Support various unit styles
-
- box = topology.box
-
- with open(filename, "w") as data:
- data.write(
- "{} written by topology at {}\n\n".format(
- topology.name if topology.name is not None else "",
- str(datetime.datetime.now()),
+ if unit_style not in [
+ "real",
+ "lj",
+ "metal",
+ "si",
+ "cgs",
+ "electron",
+ "micro",
+ "nano",
+ ]:
+ raise ValueError(
+ 'Unit style "{}" is invalid or is not currently supported'.format(
+ unit_style
)
)
- data.write("{:d} atoms\n".format(topology.n_sites))
- if atom_style in ["full", "molecular"]:
- if topology.n_bonds != 0:
- data.write("{:d} bonds\n".format(topology.n_bonds))
- else:
- data.write("0 bonds\n")
- if topology.n_angles != 0:
- data.write("{:d} angles\n".format(topology.n_angles))
- else:
- data.write("0 angles\n")
- if topology.n_dihedrals != 0:
- data.write("{:d} dihedrals\n\n".format(topology.n_dihedrals))
- else:
- data.write("0 dihedrals\n\n")
-
- data.write("\n{:d} atom types\n".format(len(topology.atom_types)))
- data.write("{:d} bond types\n".format(len(topology.bond_types)))
- data.write("{:d} angle types\n".format(len(topology.angle_types)))
- data.write("{:d} dihedral types\n".format(len(topology.dihedral_types)))
-
- data.write("\n")
-
- # Box data
- if allclose_units(
- box.angles,
- u.unyt_array([90, 90, 90], "degree"),
- rtol=1e-5,
- atol=1e-8,
- ):
- warnings.warn("Orthorhombic box detected")
- box.lengths.convert_to_units(u.angstrom)
- for i, dim in enumerate(["x", "y", "z"]):
- data.write(
- "{0:.6f} {1:.6f} {2}lo {2}hi\n".format(
- 0, box.lengths.value[i], dim
- )
- )
- else:
- warnings.warn("Non-orthorhombic box detected")
- box.lengths.convert_to_units(u.angstrom)
- box.angles.convert_to_units(u.radian)
- vectors = box.get_vectors()
- a, b, c = box.lengths
- alpha, beta, gamma = box.angles
-
- lx = a
- xy = b * np.cos(gamma)
- xz = c * np.cos(beta)
- ly = np.sqrt(b**2 - xy**2)
- yz = (b * c * np.cos(alpha) - xy * xz) / ly
- lz = np.sqrt(c**2 - xz**2 - yz**2)
-
- xhi = vectors[0][0]
- yhi = vectors[1][1]
- zhi = vectors[2][2]
- xy = vectors[1][0]
- xz = vectors[2][0]
- yz = vectors[2][1]
- xlo = u.unyt_array(0, xy.units)
- ylo = u.unyt_array(0, xy.units)
- zlo = u.unyt_array(0, xy.units)
-
- xlo_bound = xlo + u.unyt_array(
- np.min([0.0, xy, xz, xy + xz]), xy.units
- )
- xhi_bound = xhi + u.unyt_array(
- np.max([0.0, xy, xz, xy + xz]), xy.units
- )
- ylo_bound = ylo + u.unyt_array(np.min([0.0, yz]), xy.units)
- yhi_bound = yhi + u.unyt_array(np.max([0.0, yz]), xy.units)
- zlo_bound = zlo
- zhi_bound = zhi
-
- data.write(
- "{0:.6f} {1:.6f} xlo xhi\n".format(
- xlo_bound.value, xhi_bound.value
- )
- )
- data.write(
- "{0:.6f} {1:.6f} ylo yhi\n".format(
- ylo_bound.value, yhi_bound.value
- )
- )
- data.write(
- "{0:.6f} {1:.6f} zlo zhi\n".format(
- zlo_bound.value, zhi_bound.value
- )
- )
- data.write(
- "{0:.6f} {1:.6f} {2:.6f} xy xz yz\n".format(
- xy.value, xz.value, yz.value
- )
- )
+ if unit_style != "lj" and lj_cfactorsDict:
+ raise ValueError(
+ "lj_cfactorsDict argument is only used if unit_style is lj."
+ )
+ base_unyts = _unit_style_factory(unit_style)
+ default_parameterMaps = { # TODO: sites are not checked currently because gmso
+ # doesn't store pair potential eqn the same way as the connections.
+ "impropers": "HarmonicImproperPotential",
+ "dihedrals": "OPLSTorsionPotential",
+ "angles": "LAMMPSHarmonicAnglePotential",
+ "bonds": "LAMMPSHarmonicBondPotential",
+ # "sites":"LennardJonesPotential",
+ # "sites":"CoulombicPotential"
+ }
- # TODO: Get a dictionary of indices and atom types
- if topology.is_typed():
- # Write out mass data
- data.write("\nMasses\n\n")
- for atom_type in topology.atom_types:
- data.write(
- "{:d}\t{:.6f}\t# {}\n".format(
- topology.atom_types.index(atom_type) + 1,
- atom_type.mass.in_units(u.g / u.mol).value,
- atom_type.name,
- )
- )
+ # TODO: Use strict_x, (i.e. x=bonds) to validate what topology attrs to convert
- # TODO: Modified cross-interactions
- # Pair coefficients
- data.write("\nPair Coeffs # lj\n\n")
- for idx, param in enumerate(topology.atom_types):
- # expected expression for lammps for standard LJ
- lj_expression = "4.0 * epsilon * ((sigma/r)**12 - (sigma/r)**6)"
- scaling_factor = simplify(lj_expression) / simplify(
- param.expression
- )
+ if strict_potentials:
+ _validate_potential_compatibility(top)
+ else:
+ _try_default_potential_conversions(top, default_parameterMaps)
- if scaling_factor.is_real:
- data.write(
- "{}\t{:.5f}\t{:.5f}\n".format(
- idx + 1,
- param.parameters["epsilon"]
- .in_units(u.Unit("kcal/mol"))
- .value
- / float(scaling_factor),
- param.parameters["sigma"]
- .in_units(u.angstrom)
- .value,
- )
- )
- else:
+ if strict_units:
+ _validate_unit_compatibility(top, base_unyts)
+ else:
+ if base_unyts and unit_style != "lj":
+ lj_cfactorsDict = None
+ else: # LJ unit styles
+ if lj_cfactorsDict is None:
+ lj_cfactorsDict = {}
+ source_factorsList = list(lj_cfactorsDict.keys())
+ defaultsList = ["length", "energy", "mass", "charge"]
+ for source_factor in defaultsList + source_factorsList:
+ if source_factor not in defaultsList:
raise ValueError(
- 'Pair Style "{}" is invalid or is not currently supported'.format(
- param.expression
- )
- )
- if topology.bonds:
- data.write("\nBond Coeffs\n\n")
- for idx, bond_type in enumerate(topology.bond_types):
- # expected harmonic potential expression for lammps
- bond_expression = "k * (r-r_eq)**2"
-
- scaling_factor = simplify(bond_expression) / simplify(
- bond_type.expression
+ f"Conversion factor {source_factor} is not used. Pleas only provide some of {defaultsList}"
)
-
- if scaling_factor.is_real:
- data.write(
- "{}\t{:.5f}\t{:.5f}\n".format(
- idx + 1,
- bond_type.parameters["k"]
- .in_units(u.Unit("kcal/mol/angstrom**2"))
- .value
- / float(scaling_factor),
- bond_type.parameters["r_eq"]
- .in_units(u.Unit("angstrom"))
- .value,
- )
- )
- else:
- raise ValueError(
- 'Bond Style "{}" is invalid or is not currently supported'.format(
- bond_type.expression
- )
- )
-
- if topology.angles:
- data.write("\nAngle Coeffs\n\n")
- for idx, angle_type in enumerate(topology.angle_types):
- # expected lammps harmonic angle expression
- angle_expression = "k * (theta - theta_eq)**2"
- scaling_factor = simplify(angle_expression) / simplify(
- angle_type.expression
- )
-
- if scaling_factor.is_real:
- data.write(
- "{}\t{:.5f}\t{:.5f}\n".format(
- idx + 1,
- angle_type.parameters["k"]
- .in_units(u.Unit("kcal/mol/radian**2"))
- .value
- / float(scaling_factor),
- angle_type.parameters["theta_eq"]
- .in_units(u.Unit("degree"))
- .value,
- )
- )
- else:
- raise ValueError(
- 'Angle Style "{}" is invalid or is not currently supported'.format(
- angle_type.expression
- )
- )
- # TODO: Write out multiple dihedral styles
- if topology.dihedrals:
- data.write("\nDihedral Coeffs\n\n")
- for idx, dihedral_type in enumerate(topology.dihedral_types):
- rbtorsion = PotentialTemplateLibrary()[
- "RyckaertBellemansTorsionPotential"
- ]
- if (
- dihedral_type.expression
- == sympify(rbtorsion.expression)
- or dihedral_type.name == rbtorsion.name
- ):
- dihedral_type = convert_ryckaert_to_opls(dihedral_type)
- data.write(
- "{}\t{:.5f}\t{:5f}\t{:5f}\t{:.5f}\n".format(
- idx + 1,
- dihedral_type.parameters["k1"]
- .in_units(u.Unit("kcal/mol"))
- .value,
- dihedral_type.parameters["k2"]
- .in_units(u.Unit("kcal/mol"))
- .value,
- dihedral_type.parameters["k3"]
- .in_units(u.Unit("kcal/mol"))
- .value,
- dihedral_type.parameters["k4"]
- .in_units(u.Unit("kcal/mol"))
- .value,
- )
- )
-
- # Atom data
- data.write("\nAtoms\n\n")
- if atom_style == "atomic":
- atom_line = "{index:d}\t{type_index:d}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n"
- elif atom_style == "charge":
- atom_line = "{index:d}\t{type_index:d}\t{charge:.6f}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n"
- elif atom_style == "molecular":
- atom_line = "{index:d}\t{zero:d}\t{type_index:d}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n"
- elif atom_style == "full":
- atom_line = "{index:d}\t{zero:d}\t{type_index:d}\t{charge:.6f}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n"
-
- for i, site in enumerate(topology.sites):
- data.write(
- atom_line.format(
- index=topology.sites.index(site) + 1,
- type_index=topology.atom_types.index(site.atom_type) + 1,
- zero=0,
- charge=site.charge.to(u.elementary_charge).value,
- x=site.position[0].in_units(u.angstrom).value,
- y=site.position[1].in_units(u.angstrom).value,
- z=site.position[2].in_units(u.angstrom).value,
+ if lj_cfactorsDict.get(source_factor):
+ continue
+ default_val_from_topology = _default_lj_val(top, source_factor)
+ lj_cfactorsDict[source_factor] = lj_cfactorsDict.get(
+ source_factor, default_val_from_topology
)
- )
- if topology.bonds:
- data.write("\nBonds\n\n")
- for i, bond in enumerate(topology.bonds):
- data.write(
- "{:d}\t{:d}\t{:d}\t{:d}\n".format(
- i + 1,
- topology.bond_types.index(bond.connection_type) + 1,
- topology.sites.index(bond.connection_members[0]) + 1,
- topology.sites.index(bond.connection_members[1]) + 1,
- )
- )
-
- if topology.angles:
- data.write("\nAngles\n\n")
- for i, angle in enumerate(topology.angles):
- data.write(
- "{:d}\t{:d}\t{:d}\t{:d}\t{:d}\n".format(
- i + 1,
- topology.angle_types.index(angle.connection_type) + 1,
- topology.sites.index(angle.connection_members[0]) + 1,
- topology.sites.index(angle.connection_members[1]) + 1,
- topology.sites.index(angle.connection_members[2]) + 1,
- )
- )
-
- if topology.dihedrals:
- data.write("\nDihedrals\n\n")
- for i, dihedral in enumerate(topology.dihedrals):
- data.write(
- "{:d}\t{:d}\t{:d}\t{:d}\t{:d}\t{:d}\n".format(
- i + 1,
- topology.dihedral_types.index(dihedral.connection_type)
- + 1,
- topology.sites.index(dihedral.connection_members[0])
- + 1,
- topology.sites.index(dihedral.connection_members[1])
- + 1,
- topology.sites.index(dihedral.connection_members[2])
- + 1,
- topology.sites.index(dihedral.connection_members[3])
- + 1,
- )
- )
+ path = Path(filename)
+ if not path.parent.exists():
+ msg = "Provided path to file that does not exist"
+ raise FileNotFoundError(msg)
+
+ with open(path, "w") as out_file:
+ _write_header(out_file, top, atom_style)
+ _write_box(out_file, top, base_unyts, lj_cfactorsDict)
+ if top.is_fully_typed():
+ _write_atomtypes(out_file, top, base_unyts, lj_cfactorsDict)
+ _write_pairtypes(out_file, top, base_unyts, lj_cfactorsDict)
+ if top.bond_types:
+ _write_bondtypes(out_file, top, base_unyts, lj_cfactorsDict)
+ if top.angle_types:
+ _write_angletypes(out_file, top, base_unyts, lj_cfactorsDict)
+ if top.dihedral_types:
+ _write_dihedraltypes(out_file, top, base_unyts, lj_cfactorsDict)
+ if top.improper_types:
+ _write_impropertypes(out_file, top, base_unyts, lj_cfactorsDict)
+
+ _write_site_data(out_file, top, atom_style, base_unyts, lj_cfactorsDict)
+ for conn in ["bonds", "angles", "dihedrals", "impropers"]:
+ connIter = getattr(top, conn)
+ if connIter:
+ _write_conn_data(out_file, top, connIter, conn)
@loads_as(".lammps", ".lammpsdata", ".data")
@@ -377,9 +409,13 @@ def read_lammpsdata(
filename : str
LAMMPS data file
atom_style : str, optional, default='full'
- Inferred atom style defined by LAMMPS
+ Inferred atom style defined by LAMMPS, be certain that this is provided
+ accurately.
+ unit_style : str, optional, default='real
+ LAMMPS unit style used for writing the datafile. Can be "real", "lj",
+ "metal", "si", "cgs", "electron", "micro", "nano".
potential: str, optional, default='lj'
- Potential type defined in data file
+ Potential type defined in data file. Only supporting lj as of now.
Returns
-------
@@ -394,16 +430,17 @@ def read_lammpsdata(
Currently supporting the following atom styles: 'full'
- Currently supporting the following unit styles: 'real'
+ Currently supporting the following unit styles: 'real', "real", "lj", "metal", "si", "cgs",
+ "electron", "micro", "nano".
Currently supporting the following potential styles: 'lj'
- Proper dihedrals can be read in but is currently not tested.
-
- Currently not supporting improper dihedrals.
+ Currently supporting the following bond styles: 'harmonic'
+ Currently supporting the following angle styles: 'harmonic'
+ Currently supporting the following dihedral styles: 'opls'
+ Currently supporting the following improper styles: 'harmonic'
"""
- # TODO: Add argument to ask if user wants to infer bond type
top = Topology()
# Validate 'atom_style'
@@ -415,7 +452,16 @@ def read_lammpsdata(
)
# Validate 'unit_style'
- if unit_style not in ["real"]:
+ if unit_style not in [
+ "real",
+ "lj",
+ "metal",
+ "si",
+ "cgs",
+ "electron",
+ "micro",
+ "nano",
+ ]:
raise ValueError(
'Unit Style "{}" is invalid or is not currently supported'.format(
unit_style
@@ -428,36 +474,39 @@ def read_lammpsdata(
top, type_list = _get_ff_information(filename, unit_style, top)
# Parse atom information
_get_atoms(filename, top, unit_style, type_list)
- # Parse connection (bonds, angles, dihedrals) information
+ # Parse connection (bonds, angles, dihedrals, impropers) information
# TODO: Add more atom styles
if atom_style in ["full"]:
_get_connection(filename, top, unit_style, connection_type="bond")
_get_connection(filename, top, unit_style, connection_type="angle")
+ _get_connection(filename, top, unit_style, connection_type="dihedral")
+ _get_connection(filename, top, unit_style, connection_type="improper")
top.update_topology()
return top
-def get_units(unit_style):
- """Get units for specific LAMMPS unit style."""
+def get_units(unit_style, dimension):
+ """Get u.Unit for specific LAMMPS unit style with given dimension."""
# Need separate angle units for harmonic force constant and angle
- unit_style_dict = {
- "real": {
- "mass": u.g / u.mol,
- "distance": u.angstrom,
- "energy": u.kcal / u.mol,
- "angle_k": u.radian,
- "angle": u.degree,
- "charge": u.elementary_charge,
- }
- }
+ if unit_style == "lj":
+ if dimension == "angle":
+ return u.radian
+ return u.dimensionless
+
+ usystem = _unit_style_factory(unit_style)
+ if dimension == "angle_eq":
+ return (
+ u.degree
+ ) # LAMMPS specifies different units for some angles, such as equilibrium angles
- return unit_style_dict[unit_style]
+ return u.Unit(usystem[dimension], registry=reg)
def _get_connection(filename, topology, unit_style, connection_type):
"""Parse connection types."""
+ # TODO: check for other connection types besides the defaults
with open(filename, "r") as lammps_file:
types = False
for i, line in enumerate(lammps_file):
@@ -468,38 +517,87 @@ def _get_connection(filename, topology, unit_style, connection_type):
break
if types == False:
return topology
+ templates = PotentialTemplateLibrary()
connection_type_lines = open(filename, "r").readlines()[
i + 2 : i + n_connection_types + 2
]
connection_type_list = list()
for line in connection_type_lines:
if connection_type == "bond":
- c_type = BondType(name=line.split()[0])
+ template_potential = templates["LAMMPSHarmonicBondPotential"]
# Multiply 'k' by 2 since LAMMPS includes 1/2 in the term
- c_type.parameters["k"] = (
- float(line.split()[1])
- * u.Unit(
- get_units(unit_style)["energy"]
- / get_units(unit_style)["distance"] ** 2
- )
- * 2
- )
- c_type.parameters["r_eq"] = float(line.split()[2]) * (
- get_units(unit_style)["distance"]
+ conn_params = {
+ "k": float(line.split()[1])
+ * get_units(unit_style, "energy")
+ / get_units(unit_style, "length") ** 2
+ * 2,
+ "r_eq": float(line.split()[2])
+ * get_units(unit_style, "length"),
+ }
+ name = template_potential.name
+ expression = template_potential.expression
+ variables = template_potential.independent_variables
+ c_type = getattr(gmso, "BondType")(
+ name=name,
+ parameters=conn_params,
+ expression=expression,
+ independent_variables=variables,
)
elif connection_type == "angle":
- c_type = AngleType(name=line.split()[0])
+ template_potential = templates["LAMMPSHarmonicAnglePotential"]
# Multiply 'k' by 2 since LAMMPS includes 1/2 in the term
- c_type.parameters["k"] = (
- float(line.split()[1])
- * u.Unit(
- get_units(unit_style)["energy"]
- / get_units(unit_style)["angle_k"] ** 2
- )
- * 2
+ conn_params = {
+ "k": float(line.split()[1])
+ * get_units(unit_style, "energy")
+ / get_units(unit_style, "angle") ** 2
+ * 2,
+ "theta_eq": float(line.split()[2])
+ * get_units(unit_style, "angle_eq"),
+ }
+ name = template_potential.name
+ expression = template_potential.expression
+ variables = template_potential.independent_variables
+ c_type = getattr(gmso, "AngleType")(
+ name=name,
+ parameters=conn_params,
+ expression=expression,
+ independent_variables=variables,
)
- c_type.parameters["theta_eq"] = float(line.split()[2]) * u.Unit(
- get_units(unit_style)["angle"]
+ elif connection_type == "dihedral":
+ template_potential = templates["OPLSTorsionPotential"]
+ conn_params = {
+ "k1": float(line.split()[1]) * get_units(unit_style, "energy"),
+ "k2": float(line.split()[2]) * get_units(unit_style, "energy"),
+ "k3": float(line.split()[3]) * get_units(unit_style, "energy"),
+ "k4": float(line.split()[4]) * get_units(unit_style, "energy"),
+ }
+ name = template_potential.name
+ expression = template_potential.expression
+ variables = template_potential.independent_variables
+ c_type = getattr(gmso, "DihedralType")(
+ name=name,
+ parameters=conn_params,
+ expression=expression,
+ independent_variables=variables,
+ )
+ elif connection_type == "improper":
+ template_potential = templates["HarmonicImproperPotential"]
+ conn_params = {
+ "k": float(line.split()[2])
+ * get_units(unit_style, "energy")
+ / get_units(unit_style, "energy") ** 2
+ * 2,
+ "phi_eq": float(line.split()[3])
+ * get_units(unit_style, "angle_eq"),
+ }
+ name = template_potential.name
+ expression = template_potential.expression
+ variables = template_potential.independent_variables
+ c_type = getattr(gmso, "ImproperType")(
+ name=name,
+ parameters=conn_params,
+ expression=expression,
+ independent_variables=variables,
)
connection_type_list.append(c_type)
@@ -525,15 +623,27 @@ def _get_connection(filename, topology, unit_style, connection_type):
for j in range(n_sites):
site = topology.sites[int(line.split()[j + 2]) - 1]
site_list.append(site)
+ ctype = copy.copy(connection_type_list[int(line.split()[1]) - 1])
+ ctype.member_types = tuple(map(lambda x: x.atom_type.name, site_list))
if connection_type == "bond":
connection = Bond(
connection_members=site_list,
- bond_type=connection_type_list[int(line.split()[1]) - 1],
+ bond_type=ctype,
)
elif connection_type == "angle":
connection = Angle(
connection_members=site_list,
- angle_type=connection_type_list[int(line.split()[1]) - 1],
+ angle_type=ctype,
+ )
+ elif connection_type == "dihedral":
+ connection = Dihedral(
+ connection_members=site_list,
+ dihedral_type=ctype,
+ )
+ elif connection_type == "improper":
+ connection = Improper(
+ connection_members=site_list,
+ improper_type=ctype,
)
topology.add_connection(connection)
@@ -553,18 +663,19 @@ def _get_atoms(filename, topology, unit_style, type_list):
atom_line = line.split()
atom_type = atom_line[2]
charge = u.unyt_quantity(
- float(atom_line[3]), get_units(unit_style)["charge"]
+ float(atom_line[3]), get_units(unit_style, "charge")
)
- coord = u.angstrom * u.unyt_array(
+ coord = u.unyt_array(
[float(atom_line[4]), float(atom_line[5]), float(atom_line[6])]
- )
+ ) * get_units(unit_style, "length")
site = Atom(
charge=charge,
position=coord,
- atom_type=type_list[int(atom_type) - 1],
+ atom_type=copy.deepcopy(type_list[int(atom_type) - 1]), # 0-index
+ molecule=MoleculeType(atom_line[1], int(atom_line[1])),
)
element = element_by_mass(site.atom_type.mass.value)
- site.name = element.name
+ site.name = element.name if element else site.atom_type.name
site.element = element
topology.add_site(site)
@@ -612,13 +723,14 @@ def _get_box_coordinates(filename, unit_style, topology):
gamma = np.arccos(xy / b)
# Box Information
- lengths = u.unyt_array([a, b, c], get_units(unit_style)["distance"])
- angles = u.unyt_array([alpha, beta, gamma], u.radian)
- angles.to(get_units(unit_style)["angle"])
+ lengths = u.unyt_array([a, b, c], get_units(unit_style, "length"))
+ angles = u.unyt_array(
+ [alpha, beta, gamma], get_units(unit_style, "angle")
+ )
topology.box = Box(lengths, angles)
else:
# Box Information
- lengths = u.unyt_array([x, y, z], get_units(unit_style)["distance"])
+ lengths = u.unyt_array([x, y, z], get_units(unit_style, "length"))
topology.box = Box(lengths)
return topology
@@ -641,7 +753,7 @@ def _get_ff_information(filename, unit_style, topology):
for line in mass_lines:
atom_type = AtomType(
name=line.split()[0],
- mass=float(line.split()[1]) * get_units(unit_style)["mass"],
+ mass=float(line.split()[1]) * get_units(unit_style, "mass"),
)
type_list.append(atom_type)
@@ -651,16 +763,517 @@ def _get_ff_information(filename, unit_style, topology):
break
# Need to figure out if we're going have mixing rules printed out
# Currently only reading in LJ params
+ warn_ljcutBool = False
pair_lines = open(filename, "r").readlines()[i + 2 : i + n_atomtypes + 2]
for i, pair in enumerate(pair_lines):
if len(pair.split()) == 3:
- type_list[i].parameters["sigma"] = (
- float(pair.split()[2]) * get_units(unit_style)["distance"]
- )
- type_list[i].parameters["epsilon"] = (
- float(pair.split()[1]) * get_units(unit_style)["energy"]
- )
+ type_list[i].parameters["sigma"] = float(
+ pair.split()[2]
+ ) * get_units(unit_style, "length")
+ type_list[i].parameters["epsilon"] = float(
+ pair.split()[1]
+ ) * get_units(unit_style, "energy")
elif len(pair.split()) == 4:
- warnings.warn("Currently not reading in mixing rules")
+ warn_ljcutBool = True
+
+ if warn_ljcutBool:
+ warnings.warn(
+ "Currently not reading in LJ cutoff values."
+ "These should be specified in the engine run files."
+ )
return topology, type_list
+
+
+def _accepted_potentials():
+ """List of accepted potentials that LAMMPS can support."""
+ templates = PotentialTemplateLibrary()
+ lennard_jones_potential = templates["LennardJonesPotential"]
+ harmonic_bond_potential = templates["LAMMPSHarmonicBondPotential"]
+ harmonic_angle_potential = templates["LAMMPSHarmonicAnglePotential"]
+ periodic_torsion_potential = templates["PeriodicTorsionPotential"]
+ harmonic_improper_potential = templates["HarmonicImproperPotential"]
+ opls_torsion_potential = templates["OPLSTorsionPotential"]
+ accepted_potentialsList = [
+ lennard_jones_potential,
+ harmonic_bond_potential,
+ harmonic_angle_potential,
+ periodic_torsion_potential,
+ harmonic_improper_potential,
+ opls_torsion_potential,
+ ]
+ return accepted_potentialsList
+
+
+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, base_unyts):
+ """Check compatability of topology object units with LAMMPSDATA format."""
+ for attribute in ["sites", "bonds", "angles", "dihedrals", "impropers"]:
+ if attribute == "sites":
+ atype = "atom_types"
+ else:
+ atype = attribute[:-1] + "_types"
+ parametersList = [
+ (parameter, name)
+ for attr_type in getattr(top, atype)
+ for name, parameter in attr_type.parameters.items()
+ ]
+ for parameter, name in parametersList:
+ assert np.isclose(
+ _parameter_converted_to_float(
+ parameter, base_unyts, n_decimals=6, name=name
+ ),
+ parameter.value,
+ atol=1e-3,
+ ), f"Units System {base_unyts} is not compatible with {atype} with value {parameter}"
+
+
+def _write_header(out_file, top, atom_style):
+ """Write Lammps file header."""
+ out_file.write(
+ "{} written by {} at {} using the GMSO LAMMPS Writer\n\n".format(
+ os.environ.get("USER"),
+ top.name if top.name is not None else "",
+ str(datetime.datetime.now()),
+ )
+ )
+ out_file.write("{:d} atoms\n".format(top.n_sites))
+ if atom_style in ["full", "molecular"]:
+ out_file.write("{:d} bonds\n".format(top.n_bonds))
+ out_file.write("{:d} angles\n".format(top.n_angles))
+ out_file.write("{:d} dihedrals\n".format(top.n_dihedrals))
+ out_file.write("{:d} impropers\n\n".format(top.n_impropers))
+
+ # TODO: allow users to specify filter_by syntax
+ out_file.write(
+ "{:d} atom types\n".format(len(top.atom_types(filter_by=pfilter)))
+ )
+ if top.n_bonds > 0 and atom_style in ["full", "molecular"]:
+ out_file.write(
+ "{:d} bond types\n".format(len(top.bond_types(filter_by=pfilter)))
+ )
+ if top.n_angles > 0 and atom_style in ["full", "molecular"]:
+ out_file.write(
+ "{:d} angle types\n".format(len(top.angle_types(filter_by=pfilter)))
+ )
+ if top.n_dihedrals > 0 and atom_style in ["full", "molecular"]:
+ out_file.write(
+ "{:d} dihedral types\n".format(
+ len(top.dihedral_types(filter_by=pfilter))
+ )
+ )
+ if top.n_impropers > 0 and atom_style in ["full", "molecular"]:
+ out_file.write(
+ "{:d} improper types\n".format(
+ len(top.improper_types(filter_by=pfilter))
+ )
+ )
+
+ out_file.write("\n")
+
+
+def _write_box(out_file, top, base_unyts, cfactorsDict):
+ """Write GMSO Topology box to LAMMPS file."""
+ if allclose_units(
+ top.box.angles,
+ u.unyt_array([90, 90, 90], "degree"),
+ rtol=1e-5,
+ atol=1e-8,
+ ):
+ box_lengths = [
+ _parameter_converted_to_float(
+ top.box.lengths[i], base_unyts, cfactorsDict
+ )
+ for i in range(3)
+ ]
+ for i, dim in enumerate(["x", "y", "z"]):
+ out_file.write(
+ "{0:.6f} {1:.6f} {2}lo {2}hi\n".format(0, box_lengths[i], dim)
+ )
+ out_file.write("0.000000 0.000000 0.000000 xy xz yz\n")
+ else:
+ box_lengths = [
+ _parameter_converted_to_float(
+ top.box.lengths[i], base_unyts, cfactorsDict
+ )
+ for i in range(3)
+ ]
+ vectors = (box_lengths * top.box.get_unit_vectors().T).T
+
+ xhi = vectors[0][0]
+ yhi = vectors[1][1]
+ zhi = vectors[2][2]
+ xy = vectors[1][0]
+ xz = vectors[2][0]
+ yz = vectors[2][1]
+ xlo = u.unyt_array(0, xy.units)
+ ylo = u.unyt_array(0, xy.units)
+ zlo = u.unyt_array(0, xy.units)
+
+ xlo_bound = xlo + u.unyt_array(np.min([0.0, xy, xz, xy + xz]), xy.units)
+ xhi_bound = xhi + u.unyt_array(np.max([0.0, xy, xz, xy + xz]), xy.units)
+ ylo_bound = ylo + u.unyt_array(np.min([0.0, yz]), xy.units)
+ yhi_bound = yhi + u.unyt_array(np.max([0.0, yz]), xy.units)
+ zlo_bound = zlo
+ zhi_bound = zhi
+
+ out_file.write(
+ "{0:.6f} {1:.6f} xlo xhi\n".format(xlo_bound.value, xhi_bound.value)
+ )
+ out_file.write(
+ "{0:.6f} {1:.6f} ylo yhi\n".format(ylo_bound.value, yhi_bound.value)
+ )
+ out_file.write(
+ "{0:.6f} {1:.6f} zlo zhi\n".format(zlo_bound.value, zhi_bound.value)
+ )
+ out_file.write(
+ "{0:.6f} {1:.6f} {2:.6f} xy xz yz\n".format(
+ xy.value, xz.value, yz.value
+ )
+ )
+
+
+def _write_atomtypes(out_file, top, base_unyts, cfactorsDict):
+ """Write out atomtypes in GMSO topology to LAMMPS file."""
+ out_file.write("\nMasses\n")
+ out_file.write(f"#\tmass ({base_unyts['mass']})\n")
+ atypesView = sorted(top.atom_types(filter_by=pfilter), key=lambda x: x.name)
+ for atom_type in atypesView:
+ out_file.write(
+ "{:d}\t{:.6f}\t# {}\n".format(
+ atypesView.index(atom_type) + 1,
+ _parameter_converted_to_float(
+ atom_type.mass, base_unyts, cfactorsDict
+ ),
+ atom_type.name,
+ )
+ )
+
+
+def _write_pairtypes(out_file, top, base_unyts, cfactorsDict):
+ """Write out pair interaction to LAMMPS file."""
+ # TODO: Handling of modified cross-interactions is not considered from top.pairpotential_types
+ # Pair coefficients
+ test_atomtype = top.sites[0].atom_type
+ out_file.write(f"\nPair Coeffs # {test_atomtype.expression}\n")
+ nb_style_orderTuple = (
+ "epsilon",
+ "sigma",
+ ) # this will vary with new pair styles
+ param_labels = [
+ _write_out_parameter_w_units(
+ key, test_atomtype.parameters[key], base_unyts
+ )
+ for key in nb_style_orderTuple
+ ]
+ out_file.write("#\t" + "\t".join(param_labels) + "\n")
+ sorted_atomtypes = sorted(
+ top.atom_types(filter_by=pfilter), key=lambda x: x.name
+ )
+ for idx, param in enumerate(sorted_atomtypes):
+ out_file.write(
+ "{}\t{:7.5f}\t\t{:7.5f}\t\t# {}\n".format(
+ idx + 1,
+ *[
+ _parameter_converted_to_float(
+ param.parameters[key],
+ base_unyts,
+ cfactorsDict,
+ n_decimals=5,
+ )
+ for key in nb_style_orderTuple
+ ],
+ param.name,
+ )
+ )
+
+
+def _write_bondtypes(out_file, top, base_unyts, cfactorsDict):
+ """Write out bonds to LAMMPS file."""
+ # TODO: Use any accepted lammps styles (only takes harmonic now)
+ test_bondtype = top.bonds[0].bond_type
+ out_file.write(f"\nBond Coeffs #{test_bondtype.name}\n")
+ bond_style_orderTuple = ("k", "r_eq")
+ param_labels = [
+ _write_out_parameter_w_units(
+ key, test_bondtype.parameters[key], base_unyts
+ )
+ for key in bond_style_orderTuple
+ ]
+
+ out_file.write("#\t" + "\t".join(param_labels) + "\n")
+ bond_types = list(top.bond_types(filter_by=pfilter))
+ bond_types.sort(key=lambda x: sorted(x.member_types))
+ for idx, bond_type in enumerate(bond_types):
+ member_types = sorted(
+ [bond_type.member_types[0], bond_type.member_types[1]]
+ )
+ out_file.write(
+ "{}\t{:7.5f}\t{:7.5f}\t\t# {}\t{}\n".format(
+ idx + 1,
+ *[
+ _parameter_converted_to_float(
+ bond_type.parameters[key], base_unyts, cfactorsDict
+ )
+ for key in bond_style_orderTuple
+ ],
+ *member_types,
+ )
+ )
+
+
+def _write_angletypes(out_file, top, base_unyts, cfactorsDict):
+ """Write out angles to LAMMPS file."""
+ # TODO: Use any accepted lammps parameters, only harmonic now
+ test_angletype = top.angles[0].angle_type
+ out_file.write(f"\nAngle Coeffs #{test_angletype.name}\n")
+ angle_style_orderTuple = (
+ "k",
+ "theta_eq",
+ ) # this will vary with new angle styles
+ param_labels = [
+ _write_out_parameter_w_units(
+ key, test_angletype.parameters[key], base_unyts
+ )
+ for key in angle_style_orderTuple
+ ]
+ out_file.write("#\t" + "\t".join(param_labels) + "\n")
+ indexList = list(top.angle_types(filter_by=pfilter))
+ indexList.sort(
+ key=lambda x: (
+ x.member_types[1],
+ min(x.member_types[::2]),
+ max(x.member_types[::2]),
+ )
+ )
+ for idx, angle_type in enumerate(indexList):
+ out_file.write(
+ "{}\t{:7.5f}\t{:7.5f}\t#{:11s}\t{:11s}\t{:11s}\n".format(
+ idx + 1,
+ *[
+ _parameter_converted_to_float(
+ angle_type.parameters[key],
+ base_unyts,
+ cfactorsDict,
+ name=key,
+ )
+ for key in angle_style_orderTuple
+ ],
+ *angle_type.member_types,
+ )
+ )
+
+
+def _write_dihedraltypes(out_file, top, base_unyts, cfactorsDict):
+ """Write out dihedrals to LAMMPS file."""
+ test_dihedraltype = top.dihedrals[0].dihedral_type
+ out_file.write(f"\nDihedral Coeffs #{test_dihedraltype.name}\n")
+ dihedral_style_orderTuple = (
+ "k1",
+ "k2",
+ "k3",
+ "k4",
+ ) # this will vary with new dihedral styles
+ param_labels = [
+ _write_out_parameter_w_units(
+ key, test_dihedraltype.parameters[key], base_unyts
+ )
+ for key in dihedral_style_orderTuple
+ ]
+ out_file.write("#\t" + "\t".join(param_labels) + "\n")
+ indexList = list(top.dihedral_types(filter_by=pfilter))
+ index_membersList = [
+ (dihedral_type, get_sorted_names(dihedral_type))
+ for dihedral_type in indexList
+ ]
+ index_membersList.sort(key=lambda x: ([x[1][i] for i in [1, 2, 0, 3]]))
+ for idx, (dihedral_type, members) in enumerate(index_membersList):
+ out_file.write(
+ "{}\t{:8.5f}\t{:8.5f}\t{:8.5f}\t{:8.5f}\t# {}\t{}\t{}\t{}\n".format(
+ idx + 1,
+ *[
+ _parameter_converted_to_float(
+ dihedral_type.parameters[parameterStr],
+ base_unyts,
+ cfactorsDict,
+ )
+ for parameterStr in dihedral_style_orderTuple
+ ],
+ *members,
+ )
+ )
+
+
+def _write_impropertypes(out_file, top, base_unyts, cfactorsDict):
+ """Write out impropers to LAMMPS file."""
+ # TODO: Use any accepted lammps parameters, only harmonic now
+ test_impropertype = top.impropers[0].improper_type
+ out_file.write(f"\nImproper Coeffs #{test_impropertype.name}\n")
+ improper_style_orderTuple = (
+ "k",
+ "phi_eq",
+ ) # this will vary with new improper styles
+ param_labels = [
+ _write_out_parameter_w_units(
+ key, test_impropertype.parameters[key], base_unyts
+ )
+ for key in improper_style_orderTuple
+ ]
+ out_file.write("#\t" + "\t".join(param_labels) + "\n")
+ indexList = list(top.improper_types(filter_by=pfilter))
+ index_membersList = [
+ (improper_type, get_sorted_names(improper_type))
+ for improper_type in indexList
+ ]
+ index_membersList.sort(key=lambda x: ([x[1][i] for i in [0, 1, 2, 3]]))
+ for idx, (improper_type, members) in enumerate(index_membersList):
+ out_file.write(
+ "{}\t{:7.5f}\t{:7.5f}\n".format(
+ idx + 1,
+ *[
+ _parameter_converted_to_float(
+ improper_type.parameters[parameterStr],
+ base_unyts,
+ cfactorsDict,
+ name=parameterStr,
+ )
+ for parameterStr in improper_style_orderTuple
+ ],
+ *improper_type,
+ )
+ )
+
+
+def _write_site_data(out_file, top, atom_style, base_unyts, cfactorsDict):
+ """Write atomic positions and charges to LAMMPS file.."""
+ out_file.write(f"\nAtoms #{atom_style}\n\n")
+ if atom_style == "atomic":
+ atom_line = "{index:d}\t{type_index:d}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n"
+ elif atom_style == "charge":
+ atom_line = "{index:d}\t{type_index:d}\t{charge:.6f}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n"
+ elif atom_style == "molecular":
+ atom_line = "{index:d}\t{moleculeid:d}\t{type_index:d}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n"
+ elif atom_style == "full":
+ atom_line = "{index:d}\t{moleculeid:d}\t{type_index:d}\t{charge:.6f}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n"
+
+ unique_sorted_typesList = sorted(
+ top.atom_types(filter_by=pfilter), key=lambda x: x.name
+ )
+ for i, site in enumerate(top.sites):
+ out_file.write(
+ atom_line.format(
+ index=i + 1,
+ moleculeid=site.molecule.number,
+ type_index=unique_sorted_typesList.index(site.atom_type) + 1,
+ charge=_parameter_converted_to_float(
+ site.charge, base_unyts, cfactorsDict
+ ),
+ x=_parameter_converted_to_float(
+ site.position[0], base_unyts, cfactorsDict, n_decimals=6
+ ),
+ y=_parameter_converted_to_float(
+ site.position[1], base_unyts, cfactorsDict, n_decimals=6
+ ),
+ z=_parameter_converted_to_float(
+ site.position[2], base_unyts, cfactorsDict, n_decimals=6
+ ),
+ )
+ )
+
+
+def _angle_order_sorter(angle_typesList):
+ return [angle_typesList[i] for i in [1, 0, 2]]
+
+
+def _dihedral_order_sorter(dihedral_typesList):
+ return [dihedral_typesList[i] for i in [1, 2, 0, 3]]
+
+
+def _improper_order_sorter(improper_typesList):
+ return [improper_typesList[i] for i in [0, 1, 2, 3]]
+
+
+sorting_funcDict = {
+ "bonds": None,
+ "angles": _angle_order_sorter,
+ "dihedrals": _dihedral_order_sorter,
+ "impropers": _improper_order_sorter,
+}
+
+
+def _write_conn_data(out_file, top, connIter, connStr):
+ """Write all connections to LAMMPS datafile."""
+ out_file.write(f"\n{connStr.capitalize()}\n\n")
+ indexList = list(
+ map(
+ get_sorted_names,
+ getattr(top, connStr[:-1] + "_types")(filter_by=pfilter),
+ )
+ )
+ indexList.sort(key=sorting_funcDict[connStr])
+
+ for i, conn in enumerate(getattr(top, connStr)):
+ typeStr = f"{i+1:<6d}\t{indexList.index(get_sorted_names(conn.connection_type))+1:<6d}\t"
+ indexStr = "\t".join(
+ map(
+ lambda x: str(top.sites.index(x) + 1).ljust(6),
+ conn.connection_members,
+ )
+ )
+ out_file.write(typeStr + indexStr + "\n")
+
+
+def _try_default_potential_conversions(top, potentialsDict):
+ """Take a topology a convert all potentials to the style in potentialDict."""
+ for pot_container in potentialsDict:
+ if getattr(top, pot_container[:-1] + "_types"):
+ top.convert_potential_styles(
+ {pot_container: potentialsDict[pot_container]}
+ )
+ elif getattr(top, pot_container):
+ raise AttributeError(
+ f"Missing parameters in {pot_container} for {top.get_untyped(pot_container)}"
+ )
+
+
+def _default_lj_val(top, source):
+ """Generate default lj non-dimensional values from topology."""
+ if source == "length":
+ return copy.deepcopy(
+ max(list(map(lambda x: x.parameters["sigma"], top.atom_types)))
+ )
+ elif source == "energy":
+ return copy.deepcopy(
+ max(list(map(lambda x: x.parameters["epsilon"], top.atom_types)))
+ )
+ elif source == "mass":
+ return copy.deepcopy(max(list(map(lambda x: x.mass, top.atom_types))))
+ elif source == "charge":
+ return copy.deepcopy(max(list(map(lambda x: x.charge, top.atom_types))))
+ else:
+ raise ValueError(
+ f"Provided {source} for default LJ cannot be found in the topology."
+ )
+
+
+def _write_out_parameter_w_units(parameter_name, parameter, base_unyts):
+ if parameter_name in ["theta_eq", "phi_eq"]:
+ return f"{parameter_name} ({'degrees'})"
+ if base_unyts.name == "lj":
+ return f"{parameter_name} ({'dimensionless'})"
+ new_dims = _dimensions_to_energy(parameter.units.dimensions)
+ new_dims = _dimensions_to_charge(new_dims)
+ new_dimStr = str(new_dims)
+ ind_units = re.sub("[^a-zA-Z]+", " ", new_dimStr).split()
+ for unit in ind_units:
+ new_dimStr = new_dimStr.replace(unit, str(base_unyts[unit]))
+
+ outputUnyt = str(
+ parameter.to(u.Unit(new_dimStr, registry=base_unyts.registry)).units
+ )
+ return f"{parameter_name} ({outputUnyt})"
diff --git a/gmso/formats/mol2.py b/gmso/formats/mol2.py
index 1067f3684..b3bdd7bc9 100644
--- a/gmso/formats/mol2.py
+++ b/gmso/formats/mol2.py
@@ -162,9 +162,14 @@ def _parse_bond(top, section, verbose):
if line.strip():
content = line.split()
bond = Bond(
- connection_members=(
- top.sites[int(content[1]) - 1],
- top.sites[int(content[2]) - 1],
+ connection_members=tuple(
+ sorted(
+ [
+ top.sites[int(content[1]) - 1],
+ top.sites[int(content[2]) - 1],
+ ],
+ key=lambda x: top.get_index(x),
+ )
)
)
top.add_connection(bond)
diff --git a/gmso/formats/top.py b/gmso/formats/top.py
index 8ffef07af..24b83b4e6 100644
--- a/gmso/formats/top.py
+++ b/gmso/formats/top.py
@@ -163,7 +163,7 @@ def write_top(top, filename, top_vars=None):
"{7:12.5f}\n".format(
str(idx + 1),
site.atom_type.name,
- str(site.molecule.number + 1 if site.molecule else 1),
+ str(site.molecule.number if site.molecule else 1),
tag,
site.atom_type.tags["element"],
"1", # TODO: care about charge groups
diff --git a/gmso/lib/jsons/FourierTorsionPotential.json b/gmso/lib/jsons/FourierTorsionPotential.json
new file mode 100644
index 000000000..3f7b1c7a1
--- /dev/null
+++ b/gmso/lib/jsons/FourierTorsionPotential.json
@@ -0,0 +1,12 @@
+{
+ "name": "FourierTorsionPotential",
+ "expression": "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))",
+ "independent_variables": "phi",
+ "expected_parameters_dimensions": {
+ "k0": "energy",
+ "k1": "energy",
+ "k2": "energy",
+ "k3": "energy",
+ "k4": "energy"
+ }
+}
diff --git a/gmso/lib/jsons/LAMMPSHarmonicAnglePotential.json b/gmso/lib/jsons/LAMMPSHarmonicAnglePotential.json
new file mode 100644
index 000000000..cdc6b9bd2
--- /dev/null
+++ b/gmso/lib/jsons/LAMMPSHarmonicAnglePotential.json
@@ -0,0 +1,9 @@
+{
+ "name": "LAMMPSHarmonicAnglePotential",
+ "expression": "k * (theta-theta_eq)**2",
+ "independent_variables": "theta",
+ "expected_parameters_dimensions": {
+ "k":"energy/angle**2",
+ "theta_eq": "angle"
+ }
+}
diff --git a/gmso/lib/jsons/LAMMPSHarmonicBondPotential.json b/gmso/lib/jsons/LAMMPSHarmonicBondPotential.json
new file mode 100644
index 000000000..b801de15a
--- /dev/null
+++ b/gmso/lib/jsons/LAMMPSHarmonicBondPotential.json
@@ -0,0 +1,9 @@
+{
+ "name": "LAMMPSHarmonicBondPotential",
+ "expression": "k * (r-r_eq)**2",
+ "independent_variables": "r",
+ "expected_parameters_dimensions": {
+ "k": "energy/length**2",
+ "r_eq": "length"
+ }
+ }
diff --git a/gmso/lib/jsons/OPLSTorsionPotential.json b/gmso/lib/jsons/OPLSTorsionPotential.json
index 2adb76cc2..0c3ba4bc8 100644
--- a/gmso/lib/jsons/OPLSTorsionPotential.json
+++ b/gmso/lib/jsons/OPLSTorsionPotential.json
@@ -1,13 +1,11 @@
{
"name": "OPLSTorsionPotential",
- "expression": "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))",
+ "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": {
- "k0": "energy",
"k1": "energy",
"k2": "energy",
"k3": "energy",
- "k4": "energy",
- "k5": "energy"
+ "k4": "energy"
}
}
diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py
index 1998bada2..32d5ec1a6 100644
--- a/gmso/tests/base_test.py
+++ b/gmso/tests/base_test.py
@@ -173,16 +173,9 @@ def _typed_topology(n_sites=100):
@pytest.fixture
def water_system(self):
- water = mb.load(get_path("tip3p.mol2"))
- water.name = "water"
- water[0].name = "opls_111"
- water[1].name = water[2].name = "opls_112"
-
- packed_system = mb.fill_box(
- compound=water, n_compounds=2, box=mb.Box([2, 2, 2])
- )
-
- return from_mbuild(packed_system, parse_label=True)
+ water = Topology(name="water")
+ water = water.load(get_path("tip3p.mol2"))
+ return water
@pytest.fixture
def ethane(self):
@@ -202,6 +195,15 @@ def typed_ethane(self):
top.name = "ethane"
return top
+ @pytest.fixture
+ def typed_ethane_opls(self, typed_ethane):
+ for dihedral in typed_ethane.dihedrals:
+ dihedral.dihedral_type.name = "RyckaertBellemansTorsionPotential"
+ top = typed_ethane.convert_potential_styles(
+ {"dihedrals": "FourierTorsionPotential"}
+ )
+ return top
+
@pytest.fixture
def parmed_ethane(self):
from mbuild.lib.molecules import Ethane
@@ -241,6 +243,14 @@ def typed_chloroethanol(self):
top = from_parmed(pmd_structure)
return top
+ @pytest.fixture
+ def typed_methaneUA(self):
+ compound = mb.Compound(name="_CH4", charge=0.0)
+ trappe = foyer.Forcefield(name="trappe-ua")
+ pmd_structure = trappe.apply(compound)
+ top = from_parmed(pmd_structure)
+ return top
+
@pytest.fixture
def parmed_hexane_box(self):
compound = mb.recipes.Alkane(6)
@@ -253,31 +263,9 @@ def parmed_hexane_box(self):
@pytest.fixture
def typed_water_system(self, water_system):
top = water_system
-
+ top.identify_connections()
ff = ForceField(get_path("tip3p.xml"))
-
- element_map = {"O": "opls_111", "H": "opls_112"}
-
- for atom in top.sites:
- atom.atom_type = ff.atom_types[atom.name]
-
- for bond in top.bonds:
- bond.bond_type = ff.bond_types["opls_111~opls_112"]
-
- molecule_tags = top.unique_site_labels(
- label_type="molecule", name_only=False
- )
- for tag in molecule_tags:
- angle = Angle(
- connection_members=[
- site for site in top.iter_sites("molecule", tag)
- ],
- name="opls_112~opls_111~opls_112",
- angle_type=ff.angle_types["opls_112~opls_111~opls_112"],
- )
- top.add_connection(angle)
-
- top.update_topology()
+ top = apply(top, ff)
return top
@pytest.fixture
@@ -682,3 +670,31 @@ def parmed_benzene(self):
untyped_benzene, assert_dihedral_params=False
)
return benzene
+
+ # TODO: now
+ # add in some fixtures for (connects), amber
+
+ @pytest.fixture
+ def harmonic_parmed_types_charmm(self):
+ from mbuild.formats.lammpsdata import write_lammpsdata
+
+ system = mb.Compound()
+ first = mb.Particle(name="_CTL2", pos=[-1, 0, 0])
+ second = mb.Particle(name="_CL", pos=[0, 0, 0])
+ third = mb.Particle(name="_OBL", pos=[1, 0, 0])
+ fourth = mb.Particle(name="_OHL", pos=[0, 1, 0])
+
+ system.add([first, second, third, fourth])
+
+ system.add_bond((first, second))
+ system.add_bond((second, third))
+ system.add_bond((second, fourth))
+
+ ff = foyer.Forcefield(forcefield_files=[get_path("charmm36_cooh.xml")])
+ struc = ff.apply(
+ system,
+ assert_angle_params=False,
+ assert_dihedral_params=False,
+ assert_improper_params=False,
+ )
+ return struc
diff --git a/gmso/tests/files/charmm36_cooh.xml b/gmso/tests/files/charmm36_cooh.xml
new file mode 100644
index 000000000..322f9bc42
--- /dev/null
+++ b/gmso/tests/files/charmm36_cooh.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/tests/files/tip3p.mol2 b/gmso/tests/files/tip3p.mol2
index 5d14e3fa4..4ad2a98b0 100644
--- a/gmso/tests/files/tip3p.mol2
+++ b/gmso/tests/files/tip3p.mol2
@@ -11,6 +11,6 @@ NO_CHARGES
3 H 0.1100 1.5400 0.4000 opls_112 1 tip3p
@BOND
1 1 2 1
- 2 3 1 1
+ 2 1 3 1
@SUBSTRUCTURE
1 RES 1 RESIDUE 0 **** ROOT 0
diff --git a/gmso/tests/files/tip3p.xml b/gmso/tests/files/tip3p.xml
index 2ebe891b7..4c4d8e8e1 100644
--- a/gmso/tests/files/tip3p.xml
+++ b/gmso/tests/files/tip3p.xml
@@ -1,18 +1,18 @@
-
-
+
+
-
+
-
+
diff --git a/gmso/tests/files/typed_water_system_ref.top b/gmso/tests/files/typed_water_system_ref.top
index 7c6600372..f08e3d6c0 100644
--- a/gmso/tests/files/typed_water_system_ref.top
+++ b/gmso/tests/files/typed_water_system_ref.top
@@ -11,13 +11,13 @@ opls_112 1 1.01100 0.41700 A 1.00000 0.00000
[ moleculetype ]
; name nrexcl
-tip3p 3
+RES 3
[ atoms ]
; nr type resnr residue atom cgnr charge mass
-1 opls_111 1 tip3p O 1 -0.83400 16.00000
-2 opls_112 1 tip3p H 1 0.41700 1.01100
-3 opls_112 1 tip3p H 1 0.41700 1.01100
+1 opls_111 1 RES O 1 -0.83400 16.00000
+2 opls_112 1 RES H 1 0.41700 1.01100
+3 opls_112 1 RES H 1 0.41700 1.01100
[ bonds ]
; ai aj funct b0 kb
@@ -26,12 +26,12 @@ tip3p 3
[ angles ]
; ai aj ak funct phi_0 k0
-1 2 3 1 104.52000 682.02000
+2 1 3 1 104.52000 682.02000
[ system ]
; name
-Topology
+tip3p
[ molecules ]
; molecule nmols
-tip3p 2
+RES 1
diff --git a/gmso/tests/parameterization/test_molecule_utils.py b/gmso/tests/parameterization/test_molecule_utils.py
index d87b529f8..322ac6ffa 100644
--- a/gmso/tests/parameterization/test_molecule_utils.py
+++ b/gmso/tests/parameterization/test_molecule_utils.py
@@ -26,11 +26,10 @@ def ethane_box_gmso(self):
identify_connections(ethane_box_gmso)
return ethane_box_gmso
- def test_no_boundary_bonds_ethane(self, ethane):
- for site in ethane.sites:
- site.molecule = site.residue
+ def test_no_boundary_bonds(self, benzene_ua_box):
+ benzene_ua_box.sites[0].molecule = benzene_ua_box.sites[6].molecule
with pytest.raises(AssertionError):
- assert_no_boundary_bonds(ethane)
+ assert_no_boundary_bonds(benzene_ua_box)
def test_no_boundary_bonds_ethane_box(self, ethane_box_gmso):
assert_no_boundary_bonds(ethane_box_gmso)
diff --git a/gmso/tests/parameterization/test_parameterization_options.py b/gmso/tests/parameterization/test_parameterization_options.py
index bbf7a9ff3..64f2fd2a2 100644
--- a/gmso/tests/parameterization/test_parameterization_options.py
+++ b/gmso/tests/parameterization/test_parameterization_options.py
@@ -259,15 +259,15 @@ def test_hierarchical_mol_structure(
):
top = deepcopy(hierarchical_top)
# Load forcefield dicts
- tip3p = ForceField(get_path("tip3p.xml"))
+ spce = ForceField(get_path("spce.xml"))
if match_ff_by == "molecule":
ff_dict = {
"polymer": oplsaa_gmso,
"cyclopentane": oplsaa_gmso,
- "water": tip3p,
+ "water": spce,
}
elif match_ff_by == "group":
- ff_dict = {"sol1": oplsaa_gmso, "sol2": tip3p}
+ ff_dict = {"sol1": oplsaa_gmso, "sol2": spce}
else:
raise ValueError("Unexpected value provided match_ff_by.")
diff --git a/gmso/tests/parameterization/test_trappe_gmso.py b/gmso/tests/parameterization/test_trappe_gmso.py
index 84e489e40..7442a5135 100644
--- a/gmso/tests/parameterization/test_trappe_gmso.py
+++ b/gmso/tests/parameterization/test_trappe_gmso.py
@@ -45,7 +45,12 @@ def test_foyer_trappe_files(
mol2_file = system_dir / f"{system_dir.name}.mol2"
gmso_top = Topology.load(mol2_file)
struct_pmd = trappe_ua_foyer.apply(to_parmed(gmso_top))
- apply(gmso_top, trappe_ua_gmso, identify_connected_components=False)
+ apply(
+ gmso_top,
+ trappe_ua_gmso,
+ identify_connected_components=False,
+ identify_connections=True,
+ )
gmso_top_from_parmeterized_pmd = from_parmed(struct_pmd)
assert_same_atom_params(gmso_top_from_parmeterized_pmd, gmso_top)
diff --git a/gmso/tests/test_conversions.py b/gmso/tests/test_conversions.py
index 782b3bb0c..cf1704949 100644
--- a/gmso/tests/test_conversions.py
+++ b/gmso/tests/test_conversions.py
@@ -1,15 +1,48 @@
from copy import deepcopy
+import numpy as np
import pytest
import sympy
import unyt as u
-from mbuild.tests.base_test import BaseTest
from unyt.testing import assert_allclose_units
-from gmso.utils.conversions import convert_kelvin_to_energy_units
+from gmso.tests.base_test import BaseTest
+from gmso.utils.conversions import (
+ convert_kelvin_to_energy_units,
+ convert_params_units,
+)
+
+
+def _convert_potential_types(top, connStr, expected_units_dim, base_units):
+ potentials = getattr(top, connStr + "_types")
+ ref_values = {"energy": "kJ/mol", "length": "nm", "angle": "radians"}
+ convert_params_units(potentials, expected_units_dim, base_units, ref_values)
+ return potentials
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 * 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(
@@ -76,3 +109,162 @@ def test_kcal_per_mol_to_string_m(self):
input_value,
"m",
)
+
+ def test_conversion_for_topology_dihedrals(self, typed_ethane):
+ expected_units_dim = dict(
+ zip(["c0", "c1", "c2", "c3", "c4", "c5"], ["energy"] * 6)
+ )
+ base_units = u.UnitSystem("atomic", "Å", "mp", "fs", "nK", "rad")
+ base_units["energy"] = "kcal/mol"
+ potentials = _convert_potential_types(
+ typed_ethane, "dihedral", expected_units_dim, base_units
+ )
+ assert (
+ str(
+ potentials.iterator[0]
+ .dihedral_type.parameters["c0"]
+ .units.dimensions
+ )
+ == "(length)**2*(mass)/(time)**2"
+ )
+ assert potentials.iterator[0].dihedral_type.parameters[
+ "c0"
+ ].units == u.Unit("kcal/mol")
+
+ def test_conversion_for_topology_angles(self, typed_ethane):
+ expected_units_dim = dict(k="energy/angle**2", theta_eq="angle")
+ base_units = u.UnitSystem("atomic", "Å", "mp", "fs", "nK", "rad")
+ base_units["energy"] = "kcal/mol"
+ potentials = _convert_potential_types(
+ typed_ethane, "angle", expected_units_dim, base_units
+ )
+ assert (
+ str(
+ potentials.iterator[0]
+ .angle_type.parameters["k"]
+ .units.dimensions
+ )
+ == "(length)**2*(mass)/((angle)**2*(time)**2)"
+ )
+ assert potentials.iterator[0].angle_type.parameters[
+ "k"
+ ].units == u.Unit("kcal/mol/rad**2")
+
+ def test_conversion_for_topology_bonds(self, typed_ethane):
+ expected_units_dim = dict(k="energy/length**2", r_eq="length")
+ base_units = u.UnitSystem("atomic", "Å", "mp", "fs", "nK", "rad")
+ base_units["energy"] = "kcal/mol"
+ potentials = _convert_potential_types(
+ typed_ethane, "bond", expected_units_dim, base_units
+ )
+ assert (
+ str(
+ potentials.iterator[0]
+ .bond_type.parameters["k"]
+ .units.dimensions
+ )
+ == "(mass)/(time)**2"
+ )
+ assert potentials.iterator[0].bond_type.parameters["k"].units == u.Unit(
+ "kcal/mol/angstrom**2"
+ )
+
+ def test_conversion_for_topology_sites(self, typed_ethane):
+ expected_units_dim = dict(sigma="length", epsilon="energy")
+ base_units = u.UnitSystem("atomic", "Å", "mp", "fs", "nK", "rad")
+ base_units["energy"] = "kcal/mol"
+ potentials = _convert_potential_types(
+ typed_ethane, "atom", expected_units_dim, base_units
+ )
+ assert (
+ str(
+ potentials.iterator[0]
+ .atom_type.parameters["epsilon"]
+ .units.dimensions
+ )
+ == "(length)**2*(mass)/(time)**2"
+ )
+ assert potentials.iterator[0].atom_type.parameters[
+ "epsilon"
+ ].units == u.Unit("kcal/mol")
+
+ def test_lammps_dimensions_to_energy(self):
+ from gmso.formats.lammpsdata import _dimensions_to_energy
+
+ units = u.Unit("kg")
+ outdims = _dimensions_to_energy(units.dimensions)
+ assert outdims == units.dimensions == u.dimensions.mass
+ units = u.Unit("J")
+ outdims = _dimensions_to_energy(units.dimensions)
+ assert outdims == sympy.Symbol("(energy)")
+ assert (
+ units.dimensions
+ == u.dimensions.length**2
+ * u.dimensions.mass
+ / u.dimensions.time**2
+ )
+ units = u.Unit("kcal/nm")
+ outdims = _dimensions_to_energy(units.dimensions)
+ assert outdims == sympy.Symbol("(energy)") / u.dimensions.length
+ assert (
+ units.dimensions
+ == u.dimensions.length * u.dimensions.mass / u.dimensions.time**2
+ )
+
+ def test_lammps_conversion_parameters_base_units(self):
+ from gmso.formats.lammpsdata import (
+ _parameter_converted_to_float,
+ _unit_style_factory,
+ )
+
+ parameter = 100 * u.Unit("kcal/mol*fs/Å")
+ base_unyts = _unit_style_factory(
+ "real"
+ ) # "lammps_real", "Å", "amu", "fs", "K", "rad"
+ float_param = _parameter_converted_to_float(
+ parameter, base_unyts, conversion_factorDict=None
+ )
+ assert float_param == 100
+ parameter = 100 * u.Unit("K*fs/amu/nm")
+ float_param = _parameter_converted_to_float(
+ parameter, base_unyts, conversion_factorDict=None
+ )
+ assert float_param == 10
+ parameter = 100 * u.Unit("km*g*ms*kJ*degree")
+ base_unyts = _unit_style_factory(
+ "si"
+ ) # "lammps_si", "m", "kg", "s", "K", "rad",
+ float_param = _parameter_converted_to_float(
+ parameter, base_unyts, conversion_factorDict=None, n_decimals=6
+ )
+ assert float_param == round(100 * np.pi / 180, 6)
+ parameter = 1 * u.Unit("g*kJ*Coulomb*m*degree")
+ base_unyts = _unit_style_factory(
+ "si"
+ ) # "lammps_si", "m", "kg", "s", "K", "rad"
+ float_param = _parameter_converted_to_float(
+ parameter, base_unyts, conversion_factorDict=None, n_decimals=6
+ )
+ assert np.isclose(float_param, np.pi / 180, 1e-3)
+
+ def test_lammps_conversion_parameters_lj(self):
+ from gmso.formats.lammpsdata import (
+ _parameter_converted_to_float,
+ _unit_style_factory,
+ )
+
+ parameter = 1 * u.Unit("g*kJ*Coulomb*m*degree")
+ conversion_factorDict = {
+ "mass": 3 * u.Unit("g"),
+ "energy": 3 * u.Unit("kJ"),
+ "charge": 3 * u.Unit("Coulomb"),
+ "length": 3 * u.Unit("m"),
+ }
+ base_unyts = _unit_style_factory("lj")
+ float_param = _parameter_converted_to_float(
+ parameter,
+ base_unyts,
+ conversion_factorDict=conversion_factorDict,
+ n_decimals=6,
+ )
+ assert np.isclose(float_param, 1 / 3**4, atol=1e-6)
diff --git a/gmso/tests/test_convert_mbuild.py b/gmso/tests/test_convert_mbuild.py
index 11e3ab08c..cadd5a6d6 100644
--- a/gmso/tests/test_convert_mbuild.py
+++ b/gmso/tests/test_convert_mbuild.py
@@ -97,8 +97,8 @@ def test_3_layer_compound(self):
top = from_mbuild(top_cmpnd, parse_label=True)
assert top.n_sites == 1
- assert top.sites[0].molecule == ("bot", 0)
- assert top.sites[0].residue == ("bot", 0)
+ assert top.sites[0].molecule == ("bot", 1)
+ assert top.sites[0].residue == ("bot", 1)
def test_4_layer_compound(self):
l0_cmpnd = mb.Compound(name="l0")
@@ -115,7 +115,7 @@ def test_4_layer_compound(self):
top = from_mbuild(l0_cmpnd, parse_label=True)
assert top.n_sites == 1
- assert top.sites[0].molecule == ("particle", 0)
+ assert top.sites[0].molecule == ("particle", 1)
def test_uneven_hierarchy(self):
top_cmpnd = mb.Compound(name="top")
@@ -135,9 +135,9 @@ def test_uneven_hierarchy(self):
for site in top.sites:
if site.name == "particle2":
assert site.group == "mid"
- assert site.molecule == ("particle2", 0)
+ assert site.molecule == ("particle2", 1)
elif site.name == "particle1":
- assert site.molecule == ("particle1", 0)
+ assert site.molecule == ("particle1", 1)
def test_pass_box(self, mb_ethane):
mb_box = Box(lengths=[3, 3, 3])
diff --git a/gmso/tests/test_convert_parmed.py b/gmso/tests/test_convert_parmed.py
index 2bc5eb6c3..47d10a7db 100644
--- a/gmso/tests/test_convert_parmed.py
+++ b/gmso/tests/test_convert_parmed.py
@@ -295,7 +295,7 @@ def test_residues_info(self, parmed_hexane_box):
for site in top_from_struc.sites:
assert site.residue[0] == "HEX"
- assert site.residue[1] in list(range(6))
+ assert site.residue[1] in list(range(1, 7))
struc_from_top = to_parmed(top_from_struc)
assert len(struc_from_top.residues) == len(struc.residues)
@@ -538,7 +538,7 @@ def test_pmd_complex_typed(self, parmed_methylnitroaniline):
assert top.n_dihedrals == len(struc.rb_torsions)
# check typing
- assert len(top.atom_types) == len(
+ assert len(top.atom_types(filter_by=pfilter)) == len(
Counter(map(attrgetter("atom_type.name"), struc.atoms))
)
bonds_list = list(
diff --git a/gmso/tests/test_gro.py b/gmso/tests/test_gro.py
index 90ca92071..75534fc13 100644
--- a/gmso/tests/test_gro.py
+++ b/gmso/tests/test_gro.py
@@ -108,7 +108,7 @@ def test_resid_for_mol(self):
reread = Topology.load("ethane_methane.gro")
nums = set([site.molecule.number for site in reread.sites])
- assert nums == {0, 1, 2, 3}
+ assert nums == {1, 2, 3, 4}
def test_no_mol_name(self):
# here we will just add sites with no molecule information to
@@ -123,7 +123,7 @@ def test_no_mol_name(self):
top.save("temp_system.gro")
reread = Topology.load("temp_system.gro")
nums = set([site.molecule.number for site in reread.sites])
- assert nums == {0}
+ assert nums == {1}
def test_res_naming(self):
top = Topology()
@@ -142,7 +142,7 @@ def test_res_naming(self):
reread = Topology.load("temp1.gro")
nums = set([site.molecule.number for site in reread.sites])
- assert nums == {0, 1}
+ assert nums == {1, 2}
top = Topology()
ref = Atom(
@@ -172,7 +172,7 @@ def test_res_naming(self):
reread = Topology.load("temp2.gro")
nums = set([site.molecule.number for site in reread.sites])
- assert nums == {0, 1, 2}
+ assert nums == {1, 2, 3}
top = Topology()
ref = Atom(
@@ -198,7 +198,7 @@ def test_res_naming(self):
reread = Topology.load("temp3.gro")
nums = set([site.molecule.number for site in reread.sites])
- assert nums == {0, 1, 2, 3}
+ assert nums == {1, 2, 3, 4}
@pytest.mark.parametrize("fixture", ["benzene_ua_box", "benzene_aa_box"])
def test_full_loop_gro_molecule(self, fixture, request):
diff --git a/gmso/tests/test_internal_conversions.py b/gmso/tests/test_internal_conversions.py
index 2ef15a27a..3f40d8a62 100644
--- a/gmso/tests/test_internal_conversions.py
+++ b/gmso/tests/test_internal_conversions.py
@@ -9,7 +9,7 @@
from gmso.tests.base_test import BaseTest
from gmso.utils.conversions import (
convert_opls_to_ryckaert,
- convert_ryckaert_to_opls,
+ convert_ryckaert_to_fourier,
)
@@ -45,9 +45,7 @@ def test_invalid_connection_type(self, templates):
)
with pytest.raises(GMSOError, match="Cannot use"):
- opls_connection_type = convert_ryckaert_to_opls(
- ryckaert_connection_type
- )
+ convert_ryckaert_to_fourier(ryckaert_connection_type)
expression = "c0+c1+c2+c3+c4+c5+phi"
variables = ryckaert_bellemans_torsion_potential.independent_variables
@@ -59,9 +57,7 @@ def test_invalid_connection_type(self, templates):
)
with pytest.raises(GMSOError, match="Cannot use"):
- opls_connection_type = convert_ryckaert_to_opls(
- ryckaert_connection_type
- )
+ convert_ryckaert_to_fourier(ryckaert_connection_type)
# Pick some OPLS parameters at random
params = {
@@ -106,7 +102,7 @@ def test_invalid_connection_type(self, templates):
opls_connection_type
)
- def test_ryckaert_to_opls(self, templates):
+ def test_ryckaert_to_fourier(self, templates):
# Pick some RB parameters at random
params = {
"c0": 1.53 * u.Unit("kJ/mol"),
@@ -132,8 +128,8 @@ def test_ryckaert_to_opls(self, templates):
parameters=params,
)
- # Convert connection to OPLS
- opls_connection_type = convert_ryckaert_to_opls(
+ # Convert connection to Fourier
+ opls_connection_type = convert_ryckaert_to_fourier(
ryckaert_connection_type
)
@@ -176,7 +172,7 @@ def test_opls_to_ryckaert(self, templates):
"k4": 1.44 * u.Unit("kJ/mol"),
}
- opls_torsion_potential = templates["OPLSTorsionPotential"]
+ opls_torsion_potential = templates["FourierTorsionPotential"]
name = opls_torsion_potential.name
expression = opls_torsion_potential.expression
variables = opls_torsion_potential.independent_variables
@@ -232,7 +228,7 @@ def test_double_conversion(self, templates):
"k4": 1.44 * u.Unit("kJ/mol"),
}
- opls_torsion_potential = templates["OPLSTorsionPotential"]
+ opls_torsion_potential = templates["FourierTorsionPotential"]
name = opls_torsion_potential.name
expression = opls_torsion_potential.expression
@@ -251,7 +247,7 @@ def test_double_conversion(self, templates):
)
# Convert connection back to OPLS
- final_connection_type = convert_ryckaert_to_opls(
+ final_connection_type = convert_ryckaert_to_fourier(
ryckaert_connection_type
)
diff --git a/gmso/tests/test_lammps.py b/gmso/tests/test_lammps.py
index 1816aaae9..625465e9b 100644
--- a/gmso/tests/test_lammps.py
+++ b/gmso/tests/test_lammps.py
@@ -1,45 +1,107 @@
+import copy
+
+import numpy as np
import pytest
import unyt as u
from unyt.testing import assert_allclose_units
import gmso
+from gmso import Topology
from gmso.core.box import Box
from gmso.core.views import PotentialFilters
+
+pfilter = PotentialFilters.UNIQUE_SORTED_NAMES
+from gmso.exceptions import EngineIncompatibilityError
+from gmso.external import from_parmed, to_parmed
+from gmso.formats.formats_registry import UnsupportedFileFormatError
from gmso.formats.lammpsdata import read_lammpsdata, write_lammpsdata
from gmso.tests.base_test import BaseTest
from gmso.tests.utils import get_path
+def compare_lammps_files(fn1, fn2, skip_linesList=[], offsets=None):
+ """Check for line by line equality between lammps files, by any values.
+
+ offsets = [file1: [(start, step)], file2: [(start, step)]
+ """
+ with open(fn1, "r") as f:
+ line1 = f.readlines()
+ with open(fn2, "r") as f:
+ line2 = f.readlines()
+ length1 = len(line1)
+ length2 = len(line2)
+ line_counter1 = 0
+ line_counter2 = 0
+ while True:
+ # check for offsets
+ if offsets:
+ if offsets[0][0][0] == line_counter1:
+ line_counter1 += offsets[0][0][1]
+ offsets[0].pop(0)
+ elif offsets[1][0][0] == line_counter2:
+ line_counter2 += offsets[1][0][1]
+ offsets[1].pop(0)
+ if line_counter1 in skip_linesList and line_counter1 == line_counter2:
+ line_counter1 += 1
+ line_counter2 += 1
+ continue
+ l1 = line1[line_counter1]
+ l2 = line2[line_counter2]
+ print(f"############### ({line_counter1}) ({line_counter2})")
+ print(l1, l2)
+
+ for arg1, arg2 in zip(l1.split(), l2.split()):
+ try:
+ comp1 = float(arg1)
+ comp2 = float(arg2)
+ except ValueError:
+ comp1 = str(arg1)
+ comp2 = str(arg2)
+ if isinstance(comp1, float):
+ assert np.isclose(
+ comp1, comp2, 1e-3
+ ), f"The following two lines have not been found to have equality {l1} and {l2}"
+ line_counter1 += 1
+ line_counter2 += 1
+ if line_counter1 >= length1 or line_counter2 >= length2:
+ break
+ return True
+
+
class TestLammpsWriter(BaseTest):
@pytest.mark.parametrize(
"fname", ["data.lammps", "data.data", "data.lammpsdata"]
)
def test_write_lammps(self, fname, typed_ar_system):
- print(fname)
typed_ar_system.save(fname)
- def test_write_lammps_triclinic(self, typed_ar_system):
- typed_ar_system.box = Box(lengths=[1, 1, 1], angles=[60, 90, 120])
- typed_ar_system.save("triclinic.lammps")
-
- def test_ethane_lammps(self, typed_ethane):
+ def test_ethane_lammps_conversion(
+ self, typed_ethane, are_equivalent_topologies
+ ):
typed_ethane.save("ethane.lammps")
+ read_top = Topology.load("ethane.lammps")
+ assert are_equivalent_topologies(read_top, typed_ethane)
- def test_water_lammps(self, typed_water_system):
- typed_water_system.save("data.lammps")
+ def test_opls_lammps(self, typed_ethane_opls, are_equivalent_topologies):
+ typed_ethane_opls.save("ethane.lammps")
+ read_top = Topology.load("ethane.lammps")
+ assert are_equivalent_topologies(read_top, typed_ethane_opls)
+
+ def test_water_lammps(self, typed_water_system, are_equivalent_topologies):
+ typed_water_system.save("water.lammps")
+ read_top = Topology.load("water.lammps")
+ assert are_equivalent_topologies(read_top, typed_water_system)
def test_read_lammps(self, filename=get_path("data.lammps")):
- top = gmso.Topology.load(filename)
+ gmso.Topology.load(filename)
def test_read_box(self, filename=get_path("data.lammps")):
read = gmso.Topology.load(filename)
-
assert read.box == Box(lengths=[1, 1, 1])
def test_read_n_sites(self, typed_ar_system):
typed_ar_system.save("ar.lammps")
read = gmso.Topology.load("ar.lammps")
-
assert read.n_sites == 100
def test_read_mass(self, filename=get_path("data.lammps")):
@@ -87,8 +149,8 @@ def test_read_water(self, typed_water_system):
rtol=1e-5,
atol=1e-8,
)
- assert water.n_sites == 6
- assert water.n_connections == 6
+ assert water.n_sites == 3
+ assert water.n_connections == 3
def test_read_lammps_triclinic(self, typed_ar_system):
typed_ar_system.box = Box(lengths=[1, 1, 1], angles=[60, 90, 120])
@@ -108,27 +170,22 @@ def test_read_lammps_triclinic(self, typed_ar_system):
atol=1e-8,
)
- def test_read_n_bonds(self, typed_ethane):
- typed_ethane.save("ethane.lammps")
+ def test_read_n_bonds(self, typed_ethane_opls):
+ typed_ethane_opls.save("ethane.lammps")
read = gmso.Topology.load("ethane.lammps")
assert read.n_bonds == 7
- def test_read_n_angles(self, typed_ethane):
- typed_ethane.save("ethane.lammps")
+ def test_read_n_angles(self, typed_ethane_opls):
+ typed_ethane_opls.save("ethane.lammps")
read = gmso.Topology.load("ethane.lammps")
assert read.n_angles == 12
- def test_read_bond_params(self, typed_ethane):
- typed_ethane.save("ethane.lammps")
+ def test_read_bond_params(self, typed_ethane_opls):
+ typed_ethane_opls.save("ethane.lammps")
read = gmso.Topology.load("ethane.lammps")
- bond_params = [
- i.parameters
- for i in read.bond_types(
- filter_by=PotentialFilters.UNIQUE_PARAMETERS
- )
- ]
+ bond_params = [i.parameters for i in read.bond_types(filter_by=pfilter)]
assert_allclose_units(
bond_params[0]["k"],
@@ -155,10 +212,12 @@ def test_read_bond_params(self, typed_ethane):
atol=1e-8,
)
- def test_read_angle_params(self, typed_ethane):
- typed_ethane.save("ethane.lammps")
+ def test_read_angle_params(self, typed_ethane_opls):
+ typed_ethane_opls.save("ethane.lammps")
read = gmso.Topology.load("ethane.lammps")
- angle_params = [i.parameters for i in read.angle_types]
+ angle_params = [
+ i.parameters for i in read.angle_types(filter_by=pfilter)
+ ]
assert_allclose_units(
angle_params[0]["k"],
@@ -173,23 +232,386 @@ def test_read_angle_params(self, typed_ethane):
atol=1e-8,
)
assert_allclose_units(
- angle_params[-1]["k"],
+ angle_params[1]["k"],
u.unyt_array(66, (u.kcal / u.mol / u.radian / u.radian)),
rtol=1e-5,
atol=1e-8,
)
assert_allclose_units(
- angle_params[-1]["theta_eq"],
+ angle_params[1]["theta_eq"],
u.unyt_array(107.8, (u.degree)),
rtol=1e-5,
atol=1e-8,
)
-
-"""
- def test_read_n_diherals(self, typed_ethane):
- typed_ethane.save("ethane.lammps")
+ def test_read_n_diherals(self, typed_ethane_opls):
+ typed_ethane_opls.save("ethane.lammps")
read = gmso.Topology.load("ethane.lammps")
assert read.n_dihedrals == 9
-"""
+ assert len(read.dihedral_types(filter_by=pfilter)) == 1
+
+ # TODO: would be good to create a library of molecules and styles to test
+ # Test potential styles that are directly comparable to ParmEd writers.
+ @pytest.mark.parametrize(
+ "top",
+ [
+ "typed_ethane",
+ "typed_methylnitroaniline",
+ "typed_methaneUA",
+ "typed_water_system",
+ ],
+ )
+ def test_lammps_vs_parmed_by_mol(self, top, request):
+ """Parmed LAMMPSDATA Compare outputs.
+
+ atom_style = 'full', 'atomic', 'charge', 'molecular'
+ unit_style = 'real', 'lj'
+ improper_style = "cvff"
+ dihedral_style = 'CHARMM', 'OPLS',
+ angle_style = 'harmonic', 'urey_bradleys'
+ bond_style = 'harmonic
+ pair_style = 'lj
+ """
+ top = request.getfixturevalue(top)
+ pmd_top = to_parmed(top)
+ top.save("gmso.lammps")
+ pmd_top.impropers = []
+ from mbuild.formats.lammpsdata import (
+ write_lammpsdata as mb_write_lammps,
+ )
+
+ mb_write_lammps(
+ structure=pmd_top,
+ filename="pmd.lammps",
+ detect_forcefield_style=True,
+ use_dihedrals=False,
+ use_rb_torsions=True,
+ mins=[0, 0, 0],
+ maxs=top.box.lengths.convert_to_units(u.nm),
+ )
+ assert compare_lammps_files(
+ "gmso.lammps",
+ "pmd.lammps",
+ skip_linesList=[0],
+ )
+
+ @pytest.mark.parametrize(
+ "atom_style", ["atomic", "charge", "molecular", "full"]
+ )
+ def test_lammps_vs_parmed_by_styles(
+ self, atom_style, typed_ethane, parmed_ethane
+ ):
+ """Test all support styles in lammps writer.
+ _______References_______
+ See https://docs.lammps.org/atom_style.html for more info.
+ """
+ typed_ethane.save("gmso.lammps", atom_style=atom_style)
+ from mbuild.formats.lammpsdata import (
+ write_lammpsdata as mb_write_lammps,
+ )
+
+ mb_write_lammps(
+ structure=parmed_ethane,
+ filename="pmd.lammps",
+ atom_style=atom_style,
+ detect_forcefield_style=True,
+ use_dihedrals=False,
+ use_rb_torsions=True,
+ mins=[0, 0, 0],
+ maxs=typed_ethane.box.lengths.convert_to_units(u.nm),
+ )
+ assert compare_lammps_files(
+ "gmso.lammps",
+ "pmd.lammps",
+ skip_linesList=[0],
+ )
+
+ def test_lammps_default_conversions(
+ self, typed_ethane, harmonic_parmed_types_charmm
+ ):
+ """Test for parameter intraconversions with potential styles.
+
+ These include:
+ bonds: factor of 2 harmonic k
+ angles: factor of 2 harmonic k
+ dihedrals: RB torsions to OPLS
+ impropers: factor of 2 harmonic k
+ pairs:
+ additional: All styles to zero and none
+ """
+ typed_ethane.save("opls.lammps")
+ with open("opls.lammps", "r") as f:
+ lines = f.readlines()
+ assert lines[38:41] == [
+ "Dihedral Coeffs #OPLSTorsionPotential\n",
+ "#\tk1 (kcal/mol)\tk2 (kcal/mol)\tk3 (kcal/mol)\tk4 (kcal/mol)\n",
+ "1\t 0.00000\t-0.00000\t 0.30000\t-0.00000\t# opls_140\topls_135\topls_135\topls_140\n",
+ ]
+
+ struc = harmonic_parmed_types_charmm
+ from mbuild.formats.lammpsdata import (
+ write_lammpsdata as mb_write_lammps,
+ )
+
+ mb_write_lammps(struc, "pmd.lammps")
+ top = from_parmed(struc)
+ top.save("gmso.lammps")
+ assert compare_lammps_files(
+ "gmso.lammps",
+ "pmd.lammps",
+ skip_linesList=[0],
+ offsets=[[[16, 1], ["none", "none"]], [["none", "none"]]],
+ )
+ out_lammps = open("gmso.lammps", "r").readlines()
+ found_impropers = False
+ for i, line in enumerate(out_lammps):
+ if "Improper Coeffs" in line:
+ assert "HarmonicImproperPotential" in line
+ assert "k" in out_lammps[i + 1]
+ assert "phi_eq" in out_lammps[i + 1]
+ assert len(out_lammps[i + 2].split("#")[0].split()) == 3
+ assert out_lammps[i + 2].split("#")[0].split()[0] == "1"
+ found_impropers = True
+ assert found_impropers
+
+ def test_lammps_strict_true(self, typed_ethane):
+ with pytest.raises(EngineIncompatibilityError):
+ typed_ethane.save("error.lammps", strict_potentials=True)
+ typed_ethane = typed_ethane.convert_potential_styles(
+ {
+ "dihedrals": "OPLSTorsionPotential",
+ "bonds": "LAMMPSHarmonicBondPotential",
+ "angles": "LAMMPSHarmonicAnglePotential",
+ }
+ )
+ typed_ethane.save("test2.lammps", strict_potentials=True)
+
+ # TODO: Need to add a list of forcefield templates to check with
+ def test_lammps_potential_styles(self, typed_ethane):
+ """Test for parameter handling of potential styles.
+
+ ______Styles______
+ The GMSO topology potentials must be written in one of these formats in order to be compatable in lammps
+ bond_styles: ["none", "zero", "fene", "gromos", "morse", "harmonic"]
+ angle_styles: ["none", "zero", "amoeba", "charmm", "cosine", "fourier", "harmonic"]
+ dihedral_styles: ["none", "zero", "charmm", "fourier", "harmonic", "opls", "quadratic"]
+ improper_styles: ["none", "zero", "amoeba", "fourier", "harmonic", "umbrella"]
+ pair_styles: ["none", "zero", "amoeba", "buck", "coul", "dpd", "gauss", "harmonic", "lj", "mie", "morse", "yukawa"]
+ Additional Styles: ['special_bonds']
+
+ _______References_______
+ See https://docs.lammps.org/Commands_category.html#force-fields for more info.
+ """
+ # TODO: Create a library of molecules that use the above styles
+ typed_ethane.save("test.lammps")
+ # TODO: Read and check test.lammps for correct writing
+
+ @pytest.mark.parametrize(
+ "unit_style",
+ ["real", "metal", "si", "cgs", "electron", "micro", "nano", "lj"],
+ )
+ def test_lammps_units(self, typed_ethane, unit_style):
+ """Generate topoogy with different units and check the output.
+ Supporte styles are: ["real", "lj", "metal", "si", "cgs", "electron", "micro", "nano"]
+ _______References_______
+ https://docs.lammps.org/units.html
+ """
+ # check the initial set of units
+ from gmso.formats.lammpsdata import get_units
+
+ # real units should be in: [g/mol, angstroms, fs, kcal/mol, kelvin, electon charge, ...]
+ mass_multiplierDict = {
+ "si": 1,
+ "cgs": 1 / 1000,
+ "micro": 1e-15,
+ "nano": 1e-21,
+ }
+ length_multiplierDict = {"si": 1e10, "cgs": 1e8}
+ if unit_style in ["si", "cgs", "micro", "nano"]:
+ typed_ethane.box.lengths *= length_multiplierDict.get(unit_style, 1)
+ for atype in typed_ethane.atom_types:
+ atype.mass = 12 * mass_multiplierDict[unit_style] * u.kg
+ typed_ethane.save("ethane.lammps", unit_style=unit_style)
+ real_top = Topology().load("ethane.lammps", unit_style=unit_style)
+ energy_unit = get_units(unit_style, "energy")
+ angle_unit = get_units(unit_style, "angle_eq")
+ length_unit = get_units(unit_style, "length")
+ charge_unit = get_units(unit_style, "charge")
+ assert (
+ real_top.dihedrals[0].dihedral_type.parameters["k1"].units
+ == energy_unit
+ )
+ assert (
+ real_top.angles[0].angle_type.parameters["theta_eq"].units
+ == angle_unit
+ )
+ assert (
+ real_top.bonds[0].bond_type.parameters["r_eq"].units == length_unit
+ )
+ assert real_top.sites[0].charge.units == charge_unit
+ if unit_style == "lj":
+ largest_eps = max(
+ list(
+ map(
+ lambda x: x.parameters["epsilon"],
+ typed_ethane.atom_types,
+ )
+ )
+ )
+ largest_sig = max(
+ list(
+ map(
+ lambda x: x.parameters["sigma"], typed_ethane.atom_types
+ )
+ )
+ )
+ assert_allclose_units(
+ real_top.dihedrals[0].dihedral_type.parameters["k1"],
+ (
+ typed_ethane.dihedrals[0].dihedral_type.parameters["k1"]
+ / largest_eps
+ ),
+ rtol=1e-5,
+ atol=1e-8,
+ )
+ assert (
+ real_top.dihedrals[0].dihedral_type.parameters["k1"]
+ == typed_ethane.dihedrals[0].dihedral_type.parameters["k1"]
+ / largest_eps
+ )
+ assert_allclose_units(
+ real_top.bonds[0].bond_type.parameters["r_eq"],
+ (
+ typed_ethane.bonds[0].bond_type.parameters["r_eq"]
+ / largest_sig
+ ),
+ rtol=1e-5,
+ atol=1e-8,
+ )
+
+ from gmso.exceptions import EngineIncompatibilityError
+
+ def test_lammps_errors(self, typed_ethane):
+ with pytest.raises(UnsupportedFileFormatError):
+ typed_ethane.save("e.lammmps")
+ missing_bonds_top = copy.deepcopy(typed_ethane)
+ for bond in missing_bonds_top.bonds:
+ bond.bond_type = None
+ with pytest.raises(AttributeError):
+ missing_bonds_top.save("e.lammps")
+ with pytest.raises(ValueError):
+ typed_ethane.save(
+ "e.lammps", unit_style="lj", lj_cfactorsDict={"bonds": 1}
+ )
+ with pytest.raises(ValueError):
+ typed_ethane.save("e.lammps", lj_cfactorsDict={"energy": "kJ/mol"})
+
+ with pytest.raises(ValueError):
+ typed_ethane.save("error.lammps", atom_style="None")
+
+ with pytest.raises(ValueError):
+ typed_ethane.save("error.lammps", unit_style="None")
+
+ def test_lammps_units(self, typed_methylnitroaniline):
+ from gmso.formats.lammpsdata import _validate_unit_compatibility
+
+ usys = u.unit_systems.mks_unit_system
+ with pytest.raises(AssertionError):
+ _validate_unit_compatibility(typed_methylnitroaniline, usys)
+ from gmso.formats.lammpsdata import _unit_style_factory
+
+ usys = _unit_style_factory("real")
+ _validate_unit_compatibility(typed_methylnitroaniline, usys)
+
+ def test_units_in_headers(self, typed_ethane):
+ """Make sure units are written out properly."""
+ typed_ethane.save("ethane.lammps")
+ with open("ethane.lammps", "r") as f:
+ lines = f.readlines()
+
+ unitsDict = {
+ "Pair": {"epsilon": "kcal/mol", "sigma": "Å"},
+ "Bond": {"k": "kcal/(mol*Å**2)", "r_eq": "Å"},
+ "Angle": {"k": "kcal/(mol*rad**2)", "theta_eq": "degrees"},
+ "Dihedral": {"k1": "kcal/mol"},
+ "Improper": {"k": "kcal/(mol*rad**2)", "phi_eq": "degrees"},
+ }
+ for i, line in enumerate(lines):
+ if "Coeffs" in line:
+ units = lines[i + 1].split(" \n")
+ for j in range(len(units[1:-1:2])):
+ assert units[j * 2 + 1] == unitsDict[units[j * 2 + 2]]
+
+ def test_atom_style_printing(self, typed_ethane):
+ """Check writers for correctly printing potential eqn."""
+ typed_ethane.save("ethane.lammps")
+ with open("ethane.lammps", "r") as f:
+ lines = f.readlines()
+
+ stylesDict = {
+ "Pair": "4*epsilon*(-sigma**6/r**6+sigma**12/r**12)",
+ "Bond": "#LAMMPSHarmonicBondPotential",
+ "Angle": "#LAMMPSHarmonicAnglePotential",
+ "Dihedral": "#OPLSTorsionPotential",
+ "Improper": "#HarmonicImproperPotential",
+ }
+ for i, line in enumerate(lines):
+ if "Coeffs" in line:
+ styleLine = lines[i].split()
+ if styleLine[0] == "Pair":
+ assert "".join(styleLine[-3:]) == stylesDict[styleLine[0]]
+ else:
+ assert styleLine[-1] == stylesDict[styleLine[0]]
+
+ def test_lj_passed_units(self, typed_ethane):
+ largest_eps = max(
+ list(
+ map(
+ lambda x: x.parameters["epsilon"],
+ typed_ethane.atom_types,
+ )
+ )
+ )
+ typed_ethane.save(
+ "ethane.lammps",
+ unit_style="lj",
+ lj_cfactorsDict={"energy": largest_eps * 2},
+ )
+ with open("ethane.lammps", "r") as f:
+ lines = f.readlines()
+ start = 0
+ end = 1
+ for i in range(len(lines)):
+ if "Pair Coeffs" in lines[i]:
+ start = i
+ if start > 0 and lines[i] == "\n":
+ end = i
+ break
+ largest_eps_written = max(
+ [
+ obj
+ for obj in map(
+ lambda x: float(x.split()[1]), lines[start + 2 : end]
+ )
+ ]
+ )
+ assert largest_eps_written == 0.5
+
+ def test_unit_style_factor(self):
+ from gmso.formats.lammpsdata import _unit_style_factory
+
+ for styleStr in [
+ "real",
+ "metal",
+ "si",
+ "cgs",
+ "electron",
+ "micro",
+ "nano",
+ ]:
+ assert _unit_style_factory(styleStr).name == "lammps_" + styleStr
+ from gmso.exceptions import NotYetImplementedWarning
+
+ with pytest.raises(NotYetImplementedWarning):
+ _unit_style_factory("None")
diff --git a/gmso/tests/test_potential_templates.py b/gmso/tests/test_potential_templates.py
index dd87a24ba..e8ac358ab 100644
--- a/gmso/tests/test_potential_templates.py
+++ b/gmso/tests/test_potential_templates.py
@@ -48,11 +48,32 @@ def test_mie_potential(self, templates):
"sigma": ud.length,
}
+ def test_fourier_torsion_potential(self, templates):
+ fourier_torsion_potential = templates["FourierTorsionPotential"]
+ assert fourier_torsion_potential.name == "FourierTorsionPotential"
+ assert fourier_torsion_potential.expression == sympy.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 fourier_torsion_potential.independent_variables == {
+ sympy.sympify("phi")
+ }
+
+ assert fourier_torsion_potential.expected_parameters_dimensions == {
+ "k0": ud.energy,
+ "k1": ud.energy,
+ "k2": ud.energy,
+ "k3": ud.energy,
+ "k4": ud.energy,
+ }
+
def test_opls_torsion_potential(self, templates):
opls_torsion_potential = templates["OPLSTorsionPotential"]
assert opls_torsion_potential.name == "OPLSTorsionPotential"
assert opls_torsion_potential.expression == sympy.sympify(
- "0.5 * k0 + 0.5 * k1 * (1 + cos(phi)) +"
+ "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))"
@@ -62,12 +83,10 @@ def test_opls_torsion_potential(self, templates):
}
assert opls_torsion_potential.expected_parameters_dimensions == {
- "k0": ud.energy,
"k1": ud.energy,
"k2": ud.energy,
"k3": ud.energy,
"k4": ud.energy,
- "k5": ud.energy,
}
def test_periodic_torsion_potential(self, templates):
diff --git a/gmso/tests/test_top.py b/gmso/tests/test_top.py
index f8e155922..433b758a1 100644
--- a/gmso/tests/test_top.py
+++ b/gmso/tests/test_top.py
@@ -82,19 +82,16 @@ def test_water_top(self, water_system):
top = water_system
ff = gmso.ForceField(get_path("tip3p.xml"))
+ top = apply(top, ff)
for site in top.sites:
- site.atom_type = ff.atom_types[site.name]
-
- top.update_atom_types()
+ site.atom_type = ff.atom_types[site.atom_type.name]
for bond in top.bonds:
bond.bond_type = bond.connection_type = ff.bond_types[
"opls_111~opls_112"
]
- top.update_bond_types()
-
for molecule in top.unique_site_labels("molecule"):
angle = gmso.core.angle.Angle(
connection_members=[
diff --git a/gmso/tests/test_views.py b/gmso/tests/test_views.py
index 4dfb2f1c6..5ccd783ea 100644
--- a/gmso/tests/test_views.py
+++ b/gmso/tests/test_views.py
@@ -74,7 +74,7 @@ def test_ethane_views(self, typed_ethane):
unique_atomtypes = atom_types(
filter_by=PotentialFilters.UNIQUE_NAME_CLASS
)
- assert len(atom_types) == 2
+ assert len(atom_types) == 8
assert len(unique_atomtypes) == 2
bond_types = typed_ethane.bond_types
diff --git a/gmso/utils/conversions.py b/gmso/utils/conversions.py
index 269e8923d..42fed96fb 100644
--- a/gmso/utils/conversions.py
+++ b/gmso/utils/conversions.py
@@ -1,4 +1,8 @@
"""Module for standard conversions needed in molecular simulations."""
+import re
+from functools import lru_cache
+
+import numpy as np
import sympy
import unyt as u
from unyt.dimensions import length, mass, time
@@ -7,7 +11,116 @@
from gmso.exceptions import GMSOError
from gmso.lib.potential_templates import PotentialTemplateLibrary
+templates = PotentialTemplateLibrary()
+
+
+@lru_cache(maxsize=128)
+def _constant_multiplier(pot1, pot2):
+ # TODO: Doc string
+ # TODO: Test outputs
+ # TODO: Check speed
+ try:
+ constant = sympy.simplify(pot1.expression / pot2.expression)
+ if constant.is_number:
+ for eq_term in pot1.expression.args:
+ if eq_term.is_symbol:
+ key = str(eq_term)
+ return {key: pot1.parameters[key] * float(constant)}
+ except Exception:
+ # return nothing if the sympy conversion errors out
+ pass
+ return None
+
+
+sympy_conversionsList = [_constant_multiplier]
+
+
+def _try_sympy_conversions(pot1, pot2):
+ # TODO: Doc string
+ # TODO: Test outputs
+ # TODO: Check speed
+ convertersList = []
+ for conversion in sympy_conversionsList:
+ convertersList.append(conversion(pot1, pot2))
+ completed_conversions = np.where(convertersList)[0]
+ if len(completed_conversions) > 0: # check to see if any conversions worked
+ return convertersList[
+ completed_conversions[0]
+ ] # return first completed value
+ return None
+
+
+def convert_topology_expressions(top, expressionMap={}):
+ """Convert from one parameter form to another.
+
+ Parameters
+ ----------
+ 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"})
+ """
+ # TODO: Raise errors
+
+ # Apply from predefined conversions or easy sympy conversions
+ conversions_map = {
+ (
+ "OPLSTorsionPotential",
+ "RyckaertBellemansTorsionPotential",
+ ): convert_opls_to_ryckaert,
+ (
+ "RyckaertBellemansTorsionPotential",
+ "OPLSTorsionPotential",
+ ): convert_ryckaert_to_opls,
+ (
+ "RyckaertBellemansTorsionPotential",
+ "FourierTorsionPotential",
+ ): convert_ryckaert_to_opls,
+ } # map of all accessible conversions currently supported
+ for conv in expressionMap:
+ # check all connections with these types for compatibility
+ for conn in getattr(top, conv):
+ current_expression = getattr(conn, conv[:-1] + "_type")
+ if (
+ current_expression.name == expressionMap[conv]
+ ): # check to see if we can skip this one
+ # TODO: Do something instead of just comparing the names
+ continue
+
+ # convert it using pre-defined conversion functions
+ conversion_from_conversion_toTuple = (
+ current_expression.name,
+ expressionMap[conv],
+ )
+ if (
+ conversion_from_conversion_toTuple in conversions_map
+ ): # Try mapped conversions
+ new_conn_type = conversions_map.get(
+ conversion_from_conversion_toTuple
+ )(current_expression)
+ setattr(conn, conv[:-1] + "_type", new_conn_type)
+ continue
+
+ # convert it using sympy expression conversion
+ new_potential = templates[expressionMap[conv]]
+ modified_connection_parametersDict = _try_sympy_conversions(
+ current_expression, new_potential
+ )
+ if modified_connection_parametersDict: # try sympy conversions
+ current_expression.name = new_potential.name
+ current_expression.expression = new_potential.expression
+ current_expression.parameters.update(
+ modified_connection_parametersDict
+ )
+
+ return top
+
+
+@lru_cache(maxsize=128)
def convert_opls_to_ryckaert(opls_connection_type):
"""Convert an OPLS dihedral to Ryckaert-Bellemans dihedral.
@@ -19,8 +132,8 @@ def convert_opls_to_ryckaert(opls_connection_type):
for OPLS and RB torsions. OPLS torsions are defined with
phi_cis = 0 while RB torsions are defined as phi_trans = 0.
"""
- templates = PotentialTemplateLibrary()
- opls_torsion_potential = templates["OPLSTorsionPotential"]
+ # TODO: this function really converts the fourier torsion to rb, not opls
+ opls_torsion_potential = templates["FourierTorsionPotential"]
valid_connection_type = False
if (
opls_connection_type.independent_variables
@@ -67,14 +180,48 @@ def convert_opls_to_ryckaert(opls_connection_type):
expression=expression,
independent_variables=variables,
parameters=converted_params,
+ member_types=opls_connection_type.member_types,
)
return ryckaert_connection_type
+@lru_cache(maxsize=128)
def convert_ryckaert_to_opls(ryckaert_connection_type):
"""Convert Ryckaert-Bellemans dihedral to OPLS.
+ NOTE: the conventions defining the dihedral angle are different
+ for OPLS and RB torsions. OPLS torsions are defined with
+ phi_cis = 0 while RB torsions are defined as phi_trans = 0.
+ """
+ fourier_connection_type = convert_ryckaert_to_fourier(
+ ryckaert_connection_type
+ )
+ opls_torsion_potential = templates["OPLSTorsionPotential"]
+ converted_params = {
+ k: fourier_connection_type.parameters.get(k, None)
+ for k in ["k1", "k2", "k3", "k4"]
+ }
+
+ name = opls_torsion_potential.name
+ expression = opls_torsion_potential.expression
+ variables = opls_torsion_potential.independent_variables
+
+ opls_connection_type = gmso.DihedralType(
+ name=name,
+ expression=expression,
+ independent_variables=variables,
+ parameters=converted_params,
+ member_types=ryckaert_connection_type.member_types,
+ )
+
+ return opls_connection_type
+
+
+@lru_cache(maxsize=128)
+def convert_ryckaert_to_fourier(ryckaert_connection_type):
+ """Convert Ryckaert-Bellemans dihedral to Fourier.
+
NOTE: the conventions defining the dihedral angle are different
for OPLS and RB torsions. OPLS torsions are defined with
phi_cis = 0 while RB torsions are defined as phi_trans = 0.
@@ -83,7 +230,7 @@ def convert_ryckaert_to_opls(ryckaert_connection_type):
ryckaert_bellemans_torsion_potential = templates[
"RyckaertBellemansTorsionPotential"
]
- opls_torsion_potential = templates["OPLSTorsionPotential"]
+ fourier_torsion_potential = templates["FourierTorsionPotential"]
valid_connection_type = False
if (
@@ -100,7 +247,7 @@ def convert_ryckaert_to_opls(ryckaert_connection_type):
valid_connection_type = True
if not valid_connection_type:
raise GMSOError(
- "Cannot use convert_ryckaert_to_opls "
+ "Cannot use convert_ryckaert_to_fourier "
"function to convert a ConnectionType that is not an "
"RyckaertBellemansTorsionPotential"
)
@@ -115,7 +262,7 @@ def convert_ryckaert_to_opls(ryckaert_connection_type):
if c5 != 0.0:
raise GMSOError(
"Cannot convert Ryckaert-Bellemans dihedral "
- "to OPLS dihedral if c5 is not equal to zero."
+ "to Fourier dihedral if c5 is not equal to zero."
)
converted_params = {
@@ -126,18 +273,19 @@ def convert_ryckaert_to_opls(ryckaert_connection_type):
"k4": ((-1.0 / 4.0) * c4),
}
- name = opls_torsion_potential.name
- expression = opls_torsion_potential.expression
- variables = opls_torsion_potential.independent_variables
+ name = fourier_torsion_potential.name
+ expression = fourier_torsion_potential.expression
+ variables = fourier_torsion_potential.independent_variables
- opls_connection_type = gmso.DihedralType(
+ fourier_connection_type = gmso.DihedralType(
name=name,
expression=expression,
independent_variables=variables,
parameters=converted_params,
+ member_types=ryckaert_connection_type.member_types,
)
- return opls_connection_type
+ return fourier_connection_type
def convert_kelvin_to_energy_units(
@@ -219,3 +367,28 @@ def convert_kelvin_to_energy_units(
energy_output_unyt = energy_input_unyt
return energy_output_unyt
+
+
+def convert_params_units(
+ potentials, expected_units_dim, base_units, ref_values
+):
+ """Convert parameters' units in the potential to that specified in the base_units."""
+ converted_potentials = list()
+ for potential in potentials:
+ converted_params = dict()
+ for parameter in potential.parameters:
+ unit_dim = expected_units_dim[parameter]
+ ind_units = re.sub("[^a-zA-Z]+", " ", unit_dim).split()
+ for unit in ind_units:
+ if unit != "angle":
+ unit_dim = unit_dim.replace(unit, f"{base_units[unit]}")
+ else:
+ # angle doesn't show up, degree or radian does
+ unit_dim = unit_dim.replace(unit, str(base_units[unit]))
+
+ converted_params[parameter] = potential.parameters[parameter].to(
+ u.Unit(unit_dim, registry=base_units.registry)
+ )
+ potential.parameters = converted_params
+ converted_potentials.append(potential)
+ return converted_potentials
diff --git a/gmso/utils/decorators.py b/gmso/utils/decorators.py
index 77f6e3756..56e89e0c9 100644
--- a/gmso/utils/decorators.py
+++ b/gmso/utils/decorators.py
@@ -62,3 +62,23 @@ def _deprecate_kwargs(kwargs, deprecated_kwargs):
DeprecationWarning,
3,
)
+
+
+def mark_WIP(message=""):
+ """Decorate functions with WIP marking."""
+
+ def _function_wrapper(function):
+ @functools.wraps(function)
+ def _inner(*args, **kwargs):
+ warnings.simplefilter("always", UserWarning) # turn off filter
+ warnings.warn(
+ "Call to function {} is WIP.".format(function.__name__),
+ category=UserWarning,
+ stacklevel=2,
+ )
+ warnings.simplefilter("default", UserWarning) # reset filter
+ return function(*args, **kwargs)
+
+ return _inner
+
+ return _function_wrapper
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/tip3p.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/tip3p.xml
index 706a8ad9c..41b2555c4 100644
--- a/gmso/utils/files/gmso_xmls/test_ffstyles/tip3p.xml
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/tip3p.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/misc.py b/gmso/utils/misc.py
index 6c92eb631..eea849cfb 100644
--- a/gmso/utils/misc.py
+++ b/gmso/utils/misc.py
@@ -60,7 +60,9 @@ def ensure_valid_dimensions(
quantity_1: u.unyt_quantity
quantity_2: u.unyt_quantity
"""
- if quantity_1.units.dimensions != quantity_2.units.dimensions:
+ if quantity_1.units == u.dimensionless:
+ return
+ elif quantity_1.units.dimensions != quantity_2.units.dimensions:
raise UnitConversionError(
quantity_1.units,
quantity_1.units.dimensions,