From 12a7849b39578a71f390d3d23d1ce31008d07b89 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 15 Sep 2021 16:25:36 -0500
Subject: [PATCH 001/141] ForceField load optimization (Part-I) (#589)
* 1. Remove mixin class by consolidating metadata inside the AbstractPotential class
* Modifications to abstract potentials, more testing
* WIP- minor rewording in description
---
gmso/abc/abstract_potential.py | 53 +++++++++++++++++++++++++++----
gmso/abc/metadata_mixin.py | 48 ----------------------------
gmso/tests/test_atom_type.py | 38 ++++++++++++++++++++++
gmso/tests/test_metadata_mixin.py | 44 -------------------------
4 files changed, 85 insertions(+), 98 deletions(-)
delete mode 100644 gmso/abc/metadata_mixin.py
delete mode 100644 gmso/tests/test_metadata_mixin.py
diff --git a/gmso/abc/abstract_potential.py b/gmso/abc/abstract_potential.py
index 2c1bc88c8..07693169d 100644
--- a/gmso/abc/abstract_potential.py
+++ b/gmso/abc/abstract_potential.py
@@ -1,15 +1,14 @@
"""Abstract representation of a Potential object."""
from abc import abstractmethod
-from typing import Any
+from typing import Any, Dict, Iterator, List
from pydantic import Field, validator
from gmso.abc.gmso_base import GMSOBase
-from gmso.abc.metadata_mixin import MetadataMixin
from gmso.utils.expression import _PotentialExpression
-class AbstractPotential(GMSOBase, MetadataMixin):
+class AbstractPotential(GMSOBase):
__base_doc__ = """An abstract potential class.
AbstractPotential stores a general interaction between components of a chemical
@@ -30,6 +29,10 @@ class AbstractPotential(GMSOBase, MetadataMixin):
description="The mathematical expression for the potential",
)
+ tags_: Dict[str, Any] = Field(
+ {}, description="Tags associated with the potential"
+ )
+
def __init__(
self,
name="Potential",
@@ -50,11 +53,13 @@ def __init__(
independent_variables=independent_variables,
parameters=None,
)
+ tags = kwargs.pop("tags", None)
- MetadataMixin.__init__(self, tags=kwargs.get("tags"))
+ if not tags:
+ kwargs["tags"] = {}
- GMSOBase.__init__(
- self, name=name, potential_expression=potential_expression, **kwargs
+ super().__init__(
+ name=name, potential_expression=potential_expression, **kwargs
)
@property
@@ -77,6 +82,40 @@ def potential_expression(self):
"""Return the functional form of the potential."""
return self.__dict__.get("potential_expression_")
+ @property
+ def tags(self):
+ return self.__dict__.get("tags_")
+
+ @property
+ def tag_names(self) -> List[str]:
+ return list(self.__dict__.get("tags_"))
+
+ @property
+ def tag_names_iter(self) -> Iterator[str]:
+ return iter(self.__dict__.get("tags_"))
+
+ def add_tag(self, tag: str, value: Any, overwrite=True) -> None:
+ """Add metadata for a particular tag"""
+ if self.tags.get(tag) and not overwrite:
+ raise ValueError(
+ f"Tag {tag} already exists. "
+ f"Please use overwrite=True to overwrite"
+ )
+ self.tags[tag] = value
+
+ def get_tag(self, tag: str, throw=False) -> Any:
+ """Get value of a particular tag"""
+ if throw:
+ return self.tags[tag]
+ else:
+ return self.tags.get(tag)
+
+ def delete_tag(self, tag: str) -> None:
+ del self.tags[tag]
+
+ def pop_tag(self, tag: str) -> Any:
+ return self.tags.pop(tag, None)
+
@validator("potential_expression_", pre=True)
def validate_potential_expression(cls, v):
if isinstance(v, dict):
@@ -130,9 +169,11 @@ class Config:
fields = {
"name_": "name",
"potential_expression_": "potential_expression",
+ "tags_": "tags",
}
alias_to_fields = {
"name": "name_",
"potential_expression": "potential_expression_",
+ "tags": "tags_",
}
diff --git a/gmso/abc/metadata_mixin.py b/gmso/abc/metadata_mixin.py
deleted file mode 100644
index 3245b5df0..000000000
--- a/gmso/abc/metadata_mixin.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from typing import Any, Dict, Iterator, List
-
-from pydantic import BaseModel, Field, validator
-
-
-class MetadataMixin(BaseModel):
- tags: Dict[str, Any] = Field(
- default={}, description="Tags associated with the metadata"
- )
-
- @property
- def tag_names(self) -> List[str]:
- return list(self.__dict__.get("tags"))
-
- @property
- def tag_names_iter(self) -> Iterator[str]:
- return iter(self.__dict__.get("tags"))
-
- def add_tag(self, tag: str, value: Any, overwrite=True) -> None:
- """Add metadata for a particular tag"""
- if self.tags.get(tag) and not overwrite:
- raise ValueError(
- f"Tag {tag} already exists. "
- f"Please use overwrite=True to overwrite"
- )
- self.tags[tag] = value
-
- def get_tag(self, tag: str, throw=False) -> Any:
- """Get value of a particular tag"""
- if throw:
- return self.tags[tag]
- else:
- return self.tags.get(tag)
-
- def delete_tag(self, tag: str) -> None:
- del self.tags[tag]
-
- def pop_tag(self, tag: str) -> Any:
- return self.tags.pop(tag, None)
-
- @validator("tags", pre=True)
- def validate_tags(cls, value):
- if value is None:
- value = dict()
- return value
-
- class Config:
- validate_assignment = True
diff --git a/gmso/tests/test_atom_type.py b/gmso/tests/test_atom_type.py
index 7c4f71859..2b2a4b83f 100644
--- a/gmso/tests/test_atom_type.py
+++ b/gmso/tests/test_atom_type.py
@@ -12,6 +12,10 @@
class TestAtomType(BaseTest):
+ @pytest.fixture(scope="session")
+ def atomtype_metadata(self):
+ return AtomType()
+
def test_new_atom_type(self, charge, mass):
new_type = AtomType(
name="mytype",
@@ -308,3 +312,37 @@ def test_atom_type_copy(self, typed_ethane):
for atom_type in typed_ethane.atom_types:
assert atom_type.copy(deep=True) == atom_type
assert deepcopy(atom_type) == atom_type
+
+ def test_metadata_empty_tags(self, atomtype_metadata):
+ assert atomtype_metadata.tag_names == []
+ assert list(atomtype_metadata.tag_names_iter) == []
+
+ def test_metadata_add_tags(self, atomtype_metadata):
+ atomtype_metadata.add_tag("tag1", dict([("tag_name_1", "value_1")]))
+ atomtype_metadata.add_tag("tag2", dict([("tag_name_2", "value_2")]))
+ atomtype_metadata.add_tag("int_tag", 1)
+ assert len(atomtype_metadata.tag_names) == 3
+
+ def test_metadata_add_tags_overwrite(self, atomtype_metadata):
+ with pytest.raises(ValueError):
+ atomtype_metadata.add_tag("tag2", "new_value", overwrite=False)
+ atomtype_metadata.add_tag("tag2", "new_value", overwrite=True)
+ assert atomtype_metadata.get_tag("tag2") == "new_value"
+ assert len(atomtype_metadata.tag_names) == 3
+
+ def test_metadata_get_tags(self, atomtype_metadata):
+ assert atomtype_metadata.get_tag("tag1").get("tag_name_1") == "value_1"
+ assert atomtype_metadata.get_tag("int_tag") == 1
+ assert atomtype_metadata.get_tag("non_existent_tag") is None
+ with pytest.raises(KeyError):
+ atomtype_metadata.get_tag("non_existent_tag", throw=True)
+
+ def test_metadata_all_tags(self, atomtype_metadata):
+ assert "int_tag" in atomtype_metadata.tags
+
+ def test_metadata_delete_tags(self, atomtype_metadata):
+ with pytest.raises(KeyError):
+ atomtype_metadata.delete_tag("non_existent_tag")
+ assert atomtype_metadata.pop_tag("non_existent_tag") is None
+ atomtype_metadata.delete_tag("int_tag")
+ assert len(atomtype_metadata.tag_names) == 2
diff --git a/gmso/tests/test_metadata_mixin.py b/gmso/tests/test_metadata_mixin.py
deleted file mode 100644
index 0012db30a..000000000
--- a/gmso/tests/test_metadata_mixin.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import pytest
-
-from gmso.abc.metadata_mixin import MetadataMixin
-from gmso.tests.base_test import BaseTest
-
-
-class TestMetadataMixin(BaseTest):
- @pytest.fixture(scope="session")
- def metadata_mixin(self):
- return MetadataMixin()
-
- def test_metadata_empty_tags(self, metadata_mixin):
- assert metadata_mixin.tag_names == []
- assert list(metadata_mixin.tag_names_iter) == []
-
- def test_metadata_add_tags(self, metadata_mixin):
- metadata_mixin.add_tag("tag1", dict([("tag_name_1", "value_1")]))
- metadata_mixin.add_tag("tag2", dict([("tag_name_2", "value_2")]))
- metadata_mixin.add_tag("int_tag", 1)
- assert len(metadata_mixin.tag_names) == 3
-
- def test_metadata_mixin_add_tags_overwrite(self, metadata_mixin):
- with pytest.raises(ValueError):
- metadata_mixin.add_tag("tag2", "new_value", overwrite=False)
- metadata_mixin.add_tag("tag2", "new_value", overwrite=True)
- assert metadata_mixin.get_tag("tag2") == "new_value"
- assert len(metadata_mixin.tag_names) == 3
-
- def test_metadata_get_tags(self, metadata_mixin):
- assert metadata_mixin.get_tag("tag1").get("tag_name_1") == "value_1"
- assert metadata_mixin.get_tag("int_tag") == 1
- assert metadata_mixin.get_tag("non_existent_tag") is None
- with pytest.raises(KeyError):
- metadata_mixin.get_tag("non_existent_tag", throw=True)
-
- def test_metadata_mixin_all_tags(self, metadata_mixin):
- assert "int_tag" in metadata_mixin.tags
-
- def test_metadata_mixin_delete_tags(self, metadata_mixin):
- with pytest.raises(KeyError):
- metadata_mixin.delete_tag("non_existent_tag")
- assert metadata_mixin.pop_tag("non_existent_tag") is None
- metadata_mixin.delete_tag("int_tag")
- assert len(metadata_mixin.tag_names) == 2
From 6fcf09631bec9a7210a4d22dece99003ba6c89f9 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 15 Sep 2021 18:10:43 -0500
Subject: [PATCH 002/141] Add `index_only` option in `identify_connections`
(#588)
* Add index_only option in identify_connections
* WIP- add comments, improper test
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
---
gmso/tests/test_connectivity.py | 71 +++++++++++++++++++++++++++++++++
gmso/utils/connectivity.py | 38 ++++++++++++++----
2 files changed, 102 insertions(+), 7 deletions(-)
diff --git a/gmso/tests/test_connectivity.py b/gmso/tests/test_connectivity.py
index 3c604ef01..5842b2e03 100644
--- a/gmso/tests/test_connectivity.py
+++ b/gmso/tests/test_connectivity.py
@@ -4,6 +4,7 @@
from gmso.core.bond import Bond
from gmso.core.topology import Topology
from gmso.tests.base_test import BaseTest
+from gmso.utils.connectivity import identify_connections
class TestConnectivity(BaseTest):
@@ -96,3 +97,73 @@ def test_square_with_bridge(self):
assert mytop.n_angles == 8
assert mytop.n_dihedrals == 6
assert mytop.n_impropers == 2
+
+ def test_index_only(self):
+ atom1 = Atom(name="A")
+ atom2 = Atom(name="B")
+ atom3 = Atom(name="C")
+ atom4 = Atom(name="D")
+ atom5 = Atom(name="E")
+ atom6 = Atom(name="F")
+
+ bond1 = Bond(connection_members=[atom1, atom2])
+
+ bond2 = Bond(connection_members=[atom2, atom3])
+
+ bond3 = Bond(connection_members=[atom3, atom4])
+
+ bond4 = Bond(connection_members=[atom2, atom5])
+
+ bond5 = Bond(connection_members=[atom2, atom6])
+
+ top = Topology()
+ for site in [atom1, atom2, atom3, atom4, atom5, atom6]:
+ top.add_site(site, update_types=False)
+
+ for conn in [bond1, bond2, bond3, bond4, bond5]:
+ top.add_connection(conn, update_types=False)
+
+ top.update_topology()
+
+ indices = identify_connections(top, index_only=True)
+ assert len(indices["angles"]) == 7
+ angle_indices = [
+ (0, 1, 2),
+ (1, 2, 3),
+ (0, 1, 4),
+ (5, 1, 2),
+ (2, 1, 4),
+ (4, 1, 5),
+ (0, 1, 5),
+ ]
+
+ for idx_tuple in angle_indices:
+ assert (
+ idx_tuple in indices["angles"]
+ or (idx_tuple[-1], idx_tuple[-2], idx_tuple[-3])
+ in indices["angles"]
+ )
+
+ assert len(indices["dihedrals"]) == 3
+ dihedral_indices = [(0, 1, 2, 3), (3, 2, 1, 5), (3, 2, 1, 4)]
+
+ for idx_tuple in dihedral_indices:
+ assert (
+ idx_tuple in indices["dihedrals"]
+ or (idx_tuple[-1], idx_tuple[-2], idx_tuple[-3], idx_tuple[-4])
+ in indices["dihedrals"]
+ )
+
+ assert len(indices["impropers"]) == 4
+ improper_indices = [
+ (1, 0, 4, 5),
+ (1, 0, 5, 2),
+ (1, 0, 4, 2),
+ (1, 4, 5, 2),
+ ]
+
+ for idx_tuple in improper_indices:
+ assert all(
+ idx_tuple[0] == members_tuple[0]
+ for members_tuple in indices["impropers"]
+ )
diff --git a/gmso/utils/connectivity.py b/gmso/utils/connectivity.py
index 66354c3f2..90ef47ccb 100644
--- a/gmso/utils/connectivity.py
+++ b/gmso/utils/connectivity.py
@@ -14,9 +14,17 @@
}
-def identify_connections(top):
+def identify_connections(top, index_only=False):
"""Identify all possible connections within a topology.
+ Parameters
+ ----------
+ top: gmso.Topology
+ The gmso topology for which to identify connections for
+ index_only: bool, default=False
+ If True, return atom indices that would form the actual connections
+ rather than adding the connections to the topology
+
Notes: We are using networkx graph matching to match
the topology's bonding graph to smaller sub-graphs that
correspond to an angle, dihedral, improper etc.
@@ -48,12 +56,28 @@ def identify_connections(top):
compound_line_graph, type_="improper"
)
- for conn_matches, conn_type in zip(
- (angle_matches, dihedral_matches, improper_matches),
- ("angle", "dihedral", "improper"),
- ):
- if conn_matches:
- _add_connections(top, conn_matches, conn_type=conn_type)
+ if not index_only:
+ for conn_matches, conn_type in zip(
+ (angle_matches, dihedral_matches, improper_matches),
+ ("angle", "dihedral", "improper"),
+ ):
+ if conn_matches:
+ _add_connections(top, conn_matches, conn_type=conn_type)
+ else:
+ return {
+ "angles": [
+ tuple(map(lambda x: top.get_index(x), members))
+ for members in angle_matches
+ ],
+ "dihedrals": [
+ tuple(map(lambda x: top.get_index(x), members))
+ for members in dihedral_matches
+ ],
+ "impropers": [
+ tuple(map(lambda x: top.get_index(x), members))
+ for members in improper_matches
+ ],
+ }
return top
From 9e237e0a09e95eb3dea7ca6eef7adee054bf6ddc Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Thu, 16 Sep 2021 13:50:02 -0500
Subject: [PATCH 003/141] Refactor simtk to openmm in utils/conversion modules
(#586)
* Refactor simtk to openmm in utils/conversion modules
* pin foyer and parmed
* fix syntax in convert_openmm
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
Co-authored-by: Co Quach
---
environment-dev.yml | 4 ++--
gmso/external/convert_openmm.py | 17 ++++++++---------
gmso/tests/test_convert_openmm.py | 10 +++++-----
gmso/utils/io.py | 8 ++++----
4 files changed, 19 insertions(+), 20 deletions(-)
diff --git a/environment-dev.yml b/environment-dev.yml
index 34738007b..392b1abb7 100644
--- a/environment-dev.yml
+++ b/environment-dev.yml
@@ -12,9 +12,9 @@ dependencies:
- pytest
- mbuild >= 0.11.0
- openbabel >= 3.0.0
- - foyer
+ - foyer >= 0.9.4
- gsd >= 2.0
- - parmed
+ - parmed >= 3.4.3
- pytest-cov
- codecov
- bump2version
diff --git a/gmso/external/convert_openmm.py b/gmso/external/convert_openmm.py
index 529604407..5ea7740d0 100644
--- a/gmso/external/convert_openmm.py
+++ b/gmso/external/convert_openmm.py
@@ -1,12 +1,12 @@
"""Convert to and from an OpenMM Topology or System object."""
import unyt as u
-from gmso.utils.io import has_openmm, has_simtk_unit, import_
+from gmso.utils.io import has_openmm, has_openmm_unit, import_
-if has_openmm & has_simtk_unit:
- simtk_unit = import_("simtk.unit")
- from simtk.openmm import *
- from simtk.openmm.app import *
+if has_openmm & has_openmm_unit:
+ openmm_unit = import_("openmm.unit")
+ from openmm import *
+ from openmm.app import *
def to_openmm(topology, openmm_object="topology"):
@@ -30,10 +30,9 @@ def to_openmm(topology, openmm_object="topology"):
openmm_top = app.Topology()
# Get topology.positions into OpenMM form
- openmm_unit = 1 * simtk_unit.nanometer
- topology.positions.convert_to_units(openmm_unit.unit.get_symbol())
+ topology.positions.convert_to_units(u.nm)
value = [i.value for i in topology.positions]
- openmm_pos = simtk_unit.Quantity(value=value, unit=openmm_unit.unit)
+ openmm_pos = openmm_unit.Quantity(value=value, unit=openmm_unit.nanometer)
# Adding a default chain and residue temporarily
chain = openmm_top.addChain()
@@ -92,7 +91,7 @@ def to_system(
An untyped topology object.
nonbondedMethod : cutoff method, optional, default=None
Cutoff method specified for OpenMM system.
- Options supported are 'NoCutoff', 'CutoffNonPeriodic', 'CutoffPeriodic', 'PME', or Ewald objects from simtk.openmm.app.
+ Options supported are 'NoCutoff', 'CutoffNonPeriodic', 'CutoffPeriodic', 'PME', or Ewald objects from openmm.app.
nonbondedCutoff : unyt array or float, default=0.8*u.nm
The nonbonded cutoff must either be a float or a unyt array.
Float interpreted in units of nm.
diff --git a/gmso/tests/test_convert_openmm.py b/gmso/tests/test_convert_openmm.py
index ba45e4137..633b34771 100644
--- a/gmso/tests/test_convert_openmm.py
+++ b/gmso/tests/test_convert_openmm.py
@@ -6,14 +6,14 @@
from gmso.core.box import Box
from gmso.external.convert_openmm import to_openmm
from gmso.tests.base_test import BaseTest
-from gmso.utils.io import has_openmm, has_simtk_unit, import_
+from gmso.utils.io import has_openmm, has_openmm_unit, import_
-if has_openmm and has_simtk_unit:
- simtk_unit = import_("simtk.unit")
+if has_openmm and has_openmm_unit:
+ openmm_unit = import_("openmm.unit")
@pytest.mark.skipif(not has_openmm, reason="OpenMM is not installed")
-@pytest.mark.skipif(not has_simtk_unit, reason="SimTK is not installed")
+@pytest.mark.skipif(not has_openmm_unit, reason="OpenMM units is not installed")
class TestOpenMM(BaseTest):
def test_openmm_modeller(self, typed_ar_system):
to_openmm(typed_ar_system, openmm_object="modeller")
@@ -55,4 +55,4 @@ def test_position_units(self, typed_ar_system):
n_topology_sites = len(typed_ar_system.sites)
omm_top = to_openmm(typed_ar_system, openmm_object="modeller")
- assert isinstance(omm_top.positions.unit, type(simtk_unit.nanometer))
+ assert isinstance(omm_top.positions.unit, type(openmm_unit.nanometer))
diff --git a/gmso/utils/io.py b/gmso/utils/io.py
index 6c5a32718..387b611aa 100644
--- a/gmso/utils/io.py
+++ b/gmso/utils/io.py
@@ -165,7 +165,7 @@ def import_(module):
has_mdtraj = False
try:
- from simtk import openmm
+ import openmm
has_openmm = True
del openmm
@@ -173,12 +173,12 @@ def import_(module):
has_openmm = False
try:
- from simtk import unit
+ from openmm import unit
- has_simtk_unit = True
+ has_openmm_unit = True
del unit
except ImportError:
- has_simtk_unit = False
+ has_openmm_unit = False
try:
import ipywidgets
From b5354d4ec0c2cb9fb8d40bee4154f29d1ca4df56 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 20 Sep 2021 17:49:28 -0500
Subject: [PATCH 004/141] [pre-commit.ci] pre-commit autoupdate (#592)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/psf/black: 21.8b0 → 21.9b0](https://github.com/psf/black/compare/21.8b0...21.9b0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ad165477b..9c4daa1e4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -15,7 +15,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
- rev: 21.8b0
+ rev: 21.9b0
hooks:
- id: black
args: [--line-length=80]
From f2614d72e4044fc11794bf85f3788e8cdb31c246 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Tue, 21 Sep 2021 13:59:34 -0500
Subject: [PATCH 005/141] Fix bug in `_get_improper_type` in `ForceField`
(#593)
---
gmso/core/forcefield.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/gmso/core/forcefield.py b/gmso/core/forcefield.py
index 77e4dc11d..4e1c512c5 100644
--- a/gmso/core/forcefield.py
+++ b/gmso/core/forcefield.py
@@ -413,12 +413,12 @@ def _get_improper_type(self, atom_types, warn=False):
forward_match_key = FF_TOKENS_SEPARATOR.join(forward_pattern)
reverse_match_key = FF_TOKENS_SEPARATOR.join(reverse_pattern)
- if forward_match_key in self.dihedral_types:
- match = self.dihedral_types[forward_match_key]
+ if forward_match_key in self.improper_types:
+ match = self.improper_types[forward_match_key]
break
- if reverse_match_key in self.dihedral_types:
- match = self.dihedral_types[reverse_match_key]
+ if reverse_match_key in self.improper_types:
+ match = self.improper_types[reverse_match_key]
break
if match:
From c2bdbaa562ac496bb6efeacac8fbcb4c9df79d80 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 22 Sep 2021 16:12:34 -0500
Subject: [PATCH 006/141] Extend `Site` to store residue information (#554)
* WIP- Inital structure for residue support
* Testing and feature completeness
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* WIP- properly catch errors
* WIP- Remove print statement
* WIP- Proper fstring substution in ValueError
* Add iter_sites_by_residue_index
* WIP- Add tests to iter_sites_by_residue_index
* Fix topology.py being wierd (windows)
* WIP- Fix missing import after windows line endings change
* WIP- Refactor to use PDB naming convention
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
---
gmso/abc/abstract_site.py | 38 ++++++++++++++++++++++++---
gmso/core/topology.py | 52 +++++++++++++++++++++++++++++++++++++
gmso/tests/base_test.py | 14 ++++++++++
gmso/tests/test_topology.py | 30 +++++++++++++++++++++
4 files changed, 131 insertions(+), 3 deletions(-)
diff --git a/gmso/abc/abstract_site.py b/gmso/abc/abstract_site.py
index 989ccad19..a5662268f 100644
--- a/gmso/abc/abstract_site.py
+++ b/gmso/abc/abstract_site.py
@@ -1,10 +1,10 @@
"""Basic interaction site in GMSO that all other sites will derive from."""
import warnings
-from typing import Any, ClassVar, Sequence, TypeVar, Union
+from typing import Any, ClassVar, Optional, Sequence, TypeVar, Union
import numpy as np
import unyt as u
-from pydantic import Field, validator
+from pydantic import Field, StrictInt, StrictStr, validator
from unyt.exceptions import InvalidUnitOperation
from gmso.abc.gmso_base import GMSOBase
@@ -22,6 +22,12 @@ def default_position():
class Site(GMSOBase):
+ __iterable_attributes__: ClassVar[set] = {
+ "label",
+ "residue_name",
+ "residue_number",
+ }
+
__base_doc__: ClassVar[
str
] = """An interaction site object in the topology hierarchy.
@@ -45,6 +51,14 @@ class Site(GMSOBase):
label_: str = Field("", description="Label to be assigned to the site")
+ residue_number_: Optional[StrictInt] = Field(
+ None, description="Residue number for the site"
+ )
+
+ residue_name_: Optional[StrictStr] = Field(
+ None, description="Residue label for the site"
+ )
+
position_: PositionType = Field(
default_factory=default_position,
description="The 3D Cartesian coordinates of the position of the site",
@@ -65,6 +79,16 @@ def label(self) -> str:
"""Return the label assigned to the site."""
return self.__dict__.get("label_")
+ @property
+ def residue_name(self):
+ """Return the residue name assigned to the site."""
+ return self.__dict__.get("residue_name_")
+
+ @property
+ def residue_number(self):
+ """Return the reside number assigned to the site."""
+ return self.__dict__.get("residue_number_")
+
def __repr__(self):
"""Return the formatted representation of the site."""
return (
@@ -127,12 +151,20 @@ class Config:
arbitrary_types_allowed = True
- fields = {"name_": "name", "position_": "position", "label_": "label"}
+ fields = {
+ "name_": "name",
+ "position_": "position",
+ "label_": "label",
+ "residue_name_": "residue_name",
+ "residue_number_": "residue_number",
+ }
alias_to_fields = {
"name": "name_",
"position": "position_",
"label": "label_",
+ "residue_name": "residue_name_",
+ "residue_number": "residue_number_",
}
validate_assignment = True
diff --git a/gmso/core/topology.py b/gmso/core/topology.py
index b85c56e81..180cf15b8 100644
--- a/gmso/core/topology.py
+++ b/gmso/core/topology.py
@@ -6,6 +6,7 @@
import unyt as u
from boltons.setutils import IndexedSet
+from gmso.abc.abstract_site import Site
from gmso.core.angle import Angle
from gmso.core.angle_type import AngleType
from gmso.core.atom import Atom
@@ -978,6 +979,57 @@ def _reindex_connection_types(self, ref):
for i, ref_member in enumerate(self._set_refs[ref].keys()):
self._index_refs[ref][ref_member] = i
+ def iter_sites(self, key, value):
+ """Iterate through this topology's sites based on certain attribute and their values.
+
+ Parameters
+ ----------
+ key: str
+ The attribute of the site to look for
+ value:
+ The value that the given attribute should be equal to
+
+ Yields
+ ------
+ gmso.abc.abstract_site.Site
+ The site where getattr(site, key) == value
+ """
+ if key not in Site.__iterable_attributes__:
+
+ raise ValueError(
+ f"`{key}` is not an iterable attribute for Site. "
+ f"To check what the iterable attributes are see gmso.abc.abstract_site module."
+ )
+
+ if value is None:
+ raise ValueError(
+ "Expected `value` to be something other than None. Provided None."
+ )
+
+ for site in self.sites:
+ if getattr(site, key) == value:
+ yield site
+
+ def iter_sites_by_residue_name(self, name):
+ """Iterate through this topology's sites which contain this specific residue `name`.
+
+ See Also
+ --------
+ gmso.core.topology.Topology.iter_sites
+ The method to iterate over Topology's sites
+ """
+ return self.iter_sites("residue_name", name)
+
+ def iter_sites_by_residue_number(self, number):
+ """Iterate through this topology's sites which contain this specific residue `number`.
+
+ See Also
+ --------
+ gmso.core.topology.Topology.iter_sites
+ The method to iterate over Topology's sites
+ """
+ return self.iter_sites("residue_number", number)
+
def save(self, filename, overwrite=False, **kwargs):
"""Save the topology to a file.
diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py
index 561da1b4b..52487e873 100644
--- a/gmso/tests/base_test.py
+++ b/gmso/tests/base_test.py
@@ -484,3 +484,17 @@ def pairpotentialtype_top(self):
top.add_pairpotentialtype(pptype12)
return top
+
+ @pytest.fixture(scope="session")
+ def residue_top(self):
+ top = Topology()
+ for i in range(1, 26):
+ atom = Atom(
+ name=f"atom_{i + 1}",
+ residue_number=i % 5,
+ residue_name="MY_RES_EVEN" if i % 2 == 0 else f"MY_RES_ODD",
+ )
+ top.add_site(atom, update_types=False)
+ top.update_topology()
+
+ return top
diff --git a/gmso/tests/test_topology.py b/gmso/tests/test_topology.py
index 9c37bc231..6f2363c08 100644
--- a/gmso/tests/test_topology.py
+++ b/gmso/tests/test_topology.py
@@ -744,3 +744,33 @@ def test_cget_untyped(self, typed_chloroethanol):
with pytest.raises(ValueError):
clone.get_untyped(group="foo")
+
+ def test_iter_sites(self, residue_top):
+ for site in residue_top.iter_sites("residue_name", "MY_RES_EVEN"):
+ assert site.residue_name == "MY_RES_EVEN"
+
+ for site in residue_top.iter_sites("residue_name", "MY_RES_ODD"):
+ assert site.residue_name == "MY_RES_ODD"
+
+ sites = list(residue_top.iter_sites("residue_number", 4))
+ assert len(sites) == 5
+
+ def test_iter_sites_non_iterable_attribute(self, residue_top):
+ with pytest.raises(ValueError):
+ for site in residue_top.iter_sites("atom_type", "abc"):
+ pass
+
+ def test_iter_sites_none(self, residue_top):
+ with pytest.raises(ValueError):
+ for site in residue_top.iter_sites("residue_name", None):
+ pass
+
+ def test_iter_sites_by_residue_name(self, pairpotentialtype_top):
+ assert (
+ len(list(pairpotentialtype_top.iter_sites_by_residue_name("AAA")))
+ == 0
+ )
+
+ def test_iter_sites_by_residue_number(self, residue_top):
+ sites = list(residue_top.iter_sites_by_residue_number(4))
+ assert len(sites) == 5
From d0b0f8840b83accfabc5b284d0cdeb482677c4c4 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Thu, 23 Sep 2021 17:24:51 -0500
Subject: [PATCH 007/141] Proper exclude `kwarg` consolidation in
`ParametricPotential.dict` (#596)
* Proper exclude kwarg consolidation in ParametricPotential.dict
* Simple failsafe test
---
gmso/core/parametric_potential.py | 8 +++++++-
gmso/tests/test_atom_type.py | 6 ++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/gmso/core/parametric_potential.py b/gmso/core/parametric_potential.py
index 4d7ae50a3..428deb51d 100644
--- a/gmso/core/parametric_potential.py
+++ b/gmso/core/parametric_potential.py
@@ -157,7 +157,13 @@ def dict(
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> dict:
- exclude = {"topology_", "set_ref_"}
+ if exclude is None:
+ exclude = set()
+ if isinstance(exclude, dict):
+ exclude = set(exclude)
+
+ exclude = exclude.union({"topology_", "set_ref_"})
+
return super().dict(
include=include,
exclude=exclude,
diff --git a/gmso/tests/test_atom_type.py b/gmso/tests/test_atom_type.py
index 2b2a4b83f..fea761b13 100644
--- a/gmso/tests/test_atom_type.py
+++ b/gmso/tests/test_atom_type.py
@@ -346,3 +346,9 @@ def test_metadata_delete_tags(self, atomtype_metadata):
assert atomtype_metadata.pop_tag("non_existent_tag") is None
atomtype_metadata.delete_tag("int_tag")
assert len(atomtype_metadata.tag_names) == 2
+
+ def test_atom_type_dict(self):
+ atype = AtomType()
+ atype_dict = atype.dict(exclude={"potential_expression"})
+ assert "potential_expression" not in atype_dict
+ assert "charge" in atype_dict
From 5825f4d48a99d1e41908bfca3465519502c856b6 Mon Sep 17 00:00:00 2001
From: Justin Gilmer
Date: Mon, 27 Sep 2021 13:20:19 -0500
Subject: [PATCH 008/141] Update docker image and re-add anaconda user (#594)
* Update docker image and re-add anaconda user
* update python to 3.8
* update python to 3.8, fix extra PY_VERSION
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
---
Dockerfile | 38 ++++++++++++++++++++------------------
1 file changed, 20 insertions(+), 18 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 225e281d6..ec56f3404 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,10 @@
-ARG PY_VERSION=3.7
-FROM continuumio/miniconda3:4.8.2-alpine AS builder
-ARG PY_VERSION
+ARG PY_VERSION=3.8
+FROM continuumio/miniconda3:4.10.3-alpine AS builder
EXPOSE 8888
LABEL maintainer.name="mosdef-hub"\
- maintainer.url="https://mosdef.org"
+ maintainer.url="https://mosdef.org"
ENV PATH /opt/conda/bin:$PATH
@@ -15,22 +14,25 @@ ADD . /gmso
WORKDIR /gmso
+# Create a group and user
+RUN addgroup -S anaconda && adduser -S anaconda -G anaconda
+
RUN conda update conda -yq && \
- conda config --set always_yes yes --set changeps1 no && \
- . /opt/conda/etc/profile.d/conda.sh && \
- sed -i -E "s/python.*$/python="$PY_VERSION"/" environment-dev.yml && \
+ conda config --set always_yes yes --set changeps1 no && \
+ . /opt/conda/etc/profile.d/conda.sh && \
+ sed -i -E "s/python.*$/python="$PY_VERSION"/" environment-dev.yml && \
conda install -c conda-forge mamba && \
- mamba env create nomkl -f environment-dev.yml && \
- conda activate gmso-dev && \
- mamba install -c conda-forge nomkl jupyter && \
- python setup.py install && \
- echo "source activate gmso-dev" >> \
- /home/anaconda/.profile && \
- conda clean -afy && \
- mkdir /home/anaconda/data && \
- chown -R anaconda:anaconda /gmso && \
- chown -R anaconda:anaconda /opt && \
- chown -R anaconda:anaconda /home/anaconda
+ mamba env create nomkl -f environment-dev.yml && \
+ conda activate gmso-dev && \
+ mamba install -c conda-forge nomkl jupyter && \
+ python setup.py install && \
+ echo "source activate gmso-dev" >> \
+ /home/anaconda/.profile && \
+ conda clean -afy && \
+ mkdir -p /home/anaconda/data && \
+ chown -R anaconda:anaconda /gmso && \
+ chown -R anaconda:anaconda /opt && \
+ chown -R anaconda:anaconda /home/anaconda
WORKDIR /home/anaconda
From a8ab67d1999480aaf514a3f5264d7b265679bf33 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Tue, 28 Sep 2021 17:53:52 -0500
Subject: [PATCH 009/141] Update `__repr__()` method for `Potential` objects
(#591)
* update repr method for connection types
* add atomclass to atom_type __repr__
* Utilize the __repr__ from ParametricPotential more
* simplify __repr__
---
gmso/core/atom_type.py | 10 ++++++++++
gmso/core/parametric_potential.py | 9 ++++++++-
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/gmso/core/atom_type.py b/gmso/core/atom_type.py
index 04b8d569b..bb370b126 100644
--- a/gmso/core/atom_type.py
+++ b/gmso/core/atom_type.py
@@ -152,6 +152,16 @@ def __hash__(self):
)
)
+ def __repr__(self):
+ """Return a formatted representation of the atom type."""
+ desc = (
+ f"<{self.__class__.__name__} {self.name},\n "
+ f"expression: {self.expression},\n "
+ f"id: {id(self)},\n "
+ f"atomclass: {self.atomclass}>"
+ )
+ return desc
+
@validator("mass_", pre=True)
def validate_mass(cls, mass):
"""Check to see that a mass is a unyt array of the right dimension."""
diff --git a/gmso/core/parametric_potential.py b/gmso/core/parametric_potential.py
index 428deb51d..d9a63e7a4 100644
--- a/gmso/core/parametric_potential.py
+++ b/gmso/core/parametric_potential.py
@@ -227,7 +227,14 @@ def from_template(cls, potential_template, parameters, topology=None):
def __repr__(self):
"""Return formatted representation of the potential."""
desc = super().__repr__()
- desc = desc.replace(">", f", \n parameters: {self.parameters}>")
+ member_types = (
+ lambda x: x.member_types if hasattr(x, "member_types") else ""
+ )
+ desc = desc.replace(
+ ">",
+ f", \n parameters: {self.parameters},\n"
+ f"member types: {member_types(self)}>",
+ )
return desc
class Config:
From bc1bd58c620e6f1b3839a49fb3af1e97372e132f Mon Sep 17 00:00:00 2001
From: CalCraven <54594941+CalCraven@users.noreply.github.com>
Date: Tue, 5 Oct 2021 17:59:33 -0500
Subject: [PATCH 010/141] Add mol2 parser within MoSDeF ecosystem (#562)
* WIP- Inital structure for residue support
* create mol2 file
* basic creation of gmso/external/convert_mol2.py
* Add unyt conversions for mol2 positions, update errors for filepath
* Make sure unyt values are input as Angstroms to proper conversion are done to mBuild
* Throw an OSError if the filepath doesn't exit that is used for from_mol2
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* remove to_mol2 functionality
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Add support for boxes
* read in box information from CRYSIN record type indicator
* assume box lenghts in angstroms and box angles in degrees
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Add changes for reading in bond information even if there is an extra space at the end of the rti
* add support for dealing with extra lines at the end of ATOMS and BOX rti
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Add in support for lj site_type so reading in lj systems doesn't accidentally start setting elements
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* add support for a system with no charges
* Create initial set of tests for conver_mol2.py
* add files from a few different sources to test
* vmd, parmed, and a modified broken one
* create tests for reading in to a topology, and checking sites, bonds, and boxes
* check for warning messages if wrong file is sent to the function
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* add in doc strings, remove excess commented notes
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* fix missing end of multi-line string issue and format with black
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* update to pydocstyle
* switch from raising UserWarning when missing elements, to just warning about it
* add tests for a missing file path, and for a system that has a lj setup
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* add unit tests for second set of box information and check for proper warnings
* add lj methane.mol2 for testing. Also be sure only to pass line through these functions
* Move this functionality from gmso/external/convert_mol.py to gmso/formats/mol2.py
* switch site type from default 'Atom' to default value 'atom'
* fix doc strings from review comments, such as listing optional inputs for site_types
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Testing and feature completeness
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* remove unused os import in mol2.py
* add unit tests to cover warnings output
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* WIP- properly catch errors
* WIP- Remove print statement
* Fix doc string for from_mol2 and added unit test for tip3p positions
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* include decorator from formats_registry so Topology.loads('.mol2') method can be used
* add in Topology.load into test_mol2.py for test cases
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* update docstring for usage of Topology.load to grab a mol2 file
* Updated mol2 reader to read in residue information into a topology object
* Pulled changes from PR #554 to create a residue attribute in a site
* Added benzene_ua.mol2 to utils/files
* Added ethanol_aa.mol2 to utils/files
* Tested for proper reading of residue index and labels from above files
* Modified site_reader in mol2.py to grab the 6th and 7th lines from a mol2 file to get residue information
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Address @justinGilmer and @umesh-timalsina reviews
* Add .strip() method to strip lines with RTI in order to increase robustness
* Move a msg error statement for missing file path into conditional statement
* Use with open as f instead of f=open(path,"r")
* Add docstring for load_top_sites
* Add information about skipping unsupported RTI
* Use regex match for checking for warnings in test-mol2.py
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Use underscore functions for mol2 loader
* Create function that checks the end of a record type indicator in a mol2 file
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Create function that checks the end of a record type indicator in a mol2 file, last bits
* Use FileNotFoundError for missing mol2 file at path
Co-authored-by: Umesh Timalsina
* Improvements to pythonic statements in return statements
Co-authored-by: Umesh Timalsina
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Import Topology from gmso, instead of gmso.Topology
Co-authored-by: Umesh Timalsina
* Address review comments to polish and trim codebase
* For if statement with syntax 'if a in b', use syntax to also allow for b to be a nonetype and not return TypeError
Co-authored-by: Umesh Timalsina
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
---
gmso/formats/mol2.py | 215 +++++++++++++++++++++++++++++++
gmso/tests/test_mol2.py | 124 ++++++++++++++++++
gmso/utils/files/benzene_ua.mol2 | 27 ++++
gmso/utils/files/broken.mol2 | 34 +++++
gmso/utils/files/ethanegro.mol2 | 11 ++
gmso/utils/files/ethanol_aa.mol2 | 32 +++++
gmso/utils/files/methane.mol2 | 15 +++
gmso/utils/files/parmed.mol2 | 26 ++++
gmso/utils/files/tip3p.mol2 | 16 +++
gmso/utils/files/vmd.mol2 | 26 ++++
10 files changed, 526 insertions(+)
create mode 100644 gmso/formats/mol2.py
create mode 100644 gmso/tests/test_mol2.py
create mode 100644 gmso/utils/files/benzene_ua.mol2
create mode 100644 gmso/utils/files/broken.mol2
create mode 100644 gmso/utils/files/ethanegro.mol2
create mode 100644 gmso/utils/files/ethanol_aa.mol2
create mode 100644 gmso/utils/files/methane.mol2
create mode 100644 gmso/utils/files/parmed.mol2
create mode 100644 gmso/utils/files/tip3p.mol2
create mode 100644 gmso/utils/files/vmd.mol2
diff --git a/gmso/formats/mol2.py b/gmso/formats/mol2.py
new file mode 100644
index 000000000..cebe86c76
--- /dev/null
+++ b/gmso/formats/mol2.py
@@ -0,0 +1,215 @@
+"""Convert to and from a TRIPOS mol2 file."""
+import warnings
+from pathlib import Path
+
+import unyt as u
+
+from gmso import Atom, Bond, Box, Topology
+from gmso.core.element import element_by_name, element_by_symbol
+from gmso.formats.formats_registry import loads_as
+
+
+@loads_as(".mol2")
+def from_mol2(filename, site_type="atom"):
+ """Read in a TRIPOS mol2 file format into a gmso topology object.
+
+ Creates a Topology from a mol2 file structure. This will read in the
+ topological structure (sites, bonds, and box) information into gmso.
+ Note that parameterized information can be found in these objects, but
+ will not be converted to the Topology.
+
+ Parameters
+ ----------
+ filename : string
+ path to the file where the mol2 file is stored.
+ site_type : string ('atom' or 'lj'), default='atom'
+ tells the reader to consider the elements saved in the mol2 file, and
+ if the type is 'lj', to not try to identify the element of the site,
+ instead saving the site name.
+
+ Returns
+ -------
+ top : gmso.Topology
+
+ Notes
+ -----
+ It may be common to want to create an mBuild compound from a mol2 file. This is possible
+ by installing [mBuild](https://mbuild.mosdef.org/en/stable/index.html)
+ and converting using the following python code:
+
+ >>> from gmso import Topology
+ >>> from gmso.external.convert_mbuild import to_mbuild
+ >>> top = Topology.load('myfile.mol2')
+ >>> mbuild_compound = to_mbuild(top)
+ """
+ mol2path = Path(filename)
+ if not mol2path.exists():
+ msg = "Provided path to file that does not exist"
+ raise FileNotFoundError(msg)
+ # Initialize topology
+ topology = Topology(name=mol2path.stem)
+ # save the name from the filename
+ with open(mol2path, "r") as f:
+ line = f.readline()
+ while f:
+ # check for header character in line
+ if line.strip().startswith("@"):
+ # if header character in line, send to a function that will direct it properly
+ line = _parse_record_type_indicator(
+ f, line, topology, site_type
+ )
+ elif line == "":
+ # check for the end of file
+ break
+ else:
+ # else, skip to next line
+ line = f.readline()
+ topology.update_topology()
+ # TODO: read in parameters to correct attribute as well. This can be saved in various rti sections.
+ return topology
+
+
+def _load_top_sites(f, topology, site_type="atom"):
+ """Take a mol2 file section with the heading 'ATOM' and save to the topology.sites attribute.
+
+ Parameters
+ ----------
+ f : file pointer
+ pointer file where the mol2 file is stored. The pointer must be at the head of the rti for that
+ `@ATOM` section.
+ topology : gmso.Topology
+ topology to save the site information to.
+ site_type : string ('atom' or 'lj'), default='atom'
+ tells the reader to consider the elements saved in the mol2 file, and
+ if the type is 'lj', to not try to identify the element of the site,
+ instead saving the site name.
+
+ Returns
+ -------
+ line : string
+ returns the last line of the `@ATOM` section, and this is where the file pointer (`f`)
+ will now point to.
+
+ Notes
+ -----
+ Will modify the topology in place with the relevant site information. Indices will be appended to any
+ current site information.
+
+ """
+ while True:
+ line = f.readline()
+ if _is_end_of_rti(line):
+ line = line.split()
+ position = [float(x) for x in line[2:5]] * u.Å
+ # TODO: make sure charges are also saved as a unyt value
+ # TODO: add validation for element names
+ if site_type == "lj":
+ element = None
+ elif element_by_symbol(line[5]):
+ element = element_by_symbol(line[5])
+ elif element_by_name(line[5]):
+ element = element_by_name(line[5])
+ else:
+ warnings.warn(
+ "No element detected for site {} with index{}, consider manually adding the element to the topology".format(
+ line[1], len(topology.sites) + 1
+ )
+ )
+ element = None
+ try:
+ charge = float(line[8])
+ except IndexError:
+ warnings.warn(
+ "No charges were detected for site {} with index {}".format(
+ line[1], line[0]
+ )
+ )
+ charge = None
+ atom = Atom(
+ name=line[1],
+ position=position.to("nm"),
+ charge=charge,
+ element=element,
+ residue_name=line[7],
+ residue_number=int(line[6]),
+ )
+ topology.add_site(atom)
+ else:
+ break
+ return line
+
+
+def _load_top_bonds(f, topology, **kwargs):
+ """Take a mol2 file section with the heading '@BOND' and save to the topology.bonds attribute."""
+ while True:
+ line = f.readline()
+ if _is_end_of_rti(line):
+ line = line.split()
+ bond = Bond(
+ connection_members=(
+ topology.sites[int(line[1]) - 1],
+ topology.sites[int(line[2]) - 1],
+ )
+ )
+ topology.add_connection(bond)
+ else:
+ break
+ return line
+
+
+def _load_top_box(f, topology, **kwargs):
+ """Take a mol2 file section with the heading '@FF_PBC' or '@CRYSIN' and save to topology.box."""
+ if topology.box:
+ warnings.warn(
+ "This mol2 file has two boxes to be read in, only reading in one with dimensions {}".format(
+ topology.box
+ )
+ )
+ line = f.readline()
+ return line
+ while True:
+ line = f.readline()
+ if _is_end_of_rti(line):
+ line = line.split()
+ # TODO: write to box information
+ topology.box = Box(
+ lengths=[float(x) for x in line[0:3]] * u.Å,
+ angles=[float(x) for x in line[3:6]] * u.degree,
+ )
+ else:
+ break
+ return line
+
+
+def _parse_record_type_indicator(f, line, topology, site_type):
+ """Take a specific record type indicator (RTI) from a mol2 file format and save to the proper attribute of a gmso topology.
+
+ Supported record type indicators include Atom, Bond, FF_PBC, and CRYSIN.
+ """
+ supported_rti = {
+ "@ATOM": _load_top_sites,
+ "@BOND": _load_top_bonds,
+ "@CRYSIN": _load_top_box,
+ "@FF_PBC": _load_top_box,
+ }
+ # read in to atom attribute
+ try:
+ line = supported_rti[line.strip()](f, topology, site_type=site_type)
+ except KeyError:
+ warnings.warn(
+ "The record type indicator {} is not supported. Skipping current section and moving to the next RTI header.".format(
+ line
+ )
+ )
+ line = f.readline()
+ return line
+
+
+def _is_end_of_rti(line):
+ """Check if line in an rti is at the end of the section."""
+ return (
+ line
+ and "@" not in line
+ and not line == "\n"
+ and not line.strip().startswith("#")
+ )
diff --git a/gmso/tests/test_mol2.py b/gmso/tests/test_mol2.py
new file mode 100644
index 000000000..1a50d695d
--- /dev/null
+++ b/gmso/tests/test_mol2.py
@@ -0,0 +1,124 @@
+import numpy as np
+import pytest
+import unyt as u
+from unyt.testing import assert_allclose_units
+
+from gmso import Topology
+from gmso.formats.mol2 import from_mol2
+from gmso.tests.base_test import BaseTest
+from gmso.utils.io import get_fn
+
+
+class TestMol2(BaseTest):
+ def test_read_mol2(self):
+ top = Topology.load(get_fn("parmed.mol2"))
+ assert top.name == "parmed"
+ assert top.n_sites == 8
+ assert_allclose_units(
+ top.box.lengths,
+ ([8.2693, 7.9100, 6.6460] * u.Å).to("nm"),
+ rtol=1e-5,
+ atol=1e-8,
+ )
+ assert list(top.sites)[0].element.name == "carbon"
+ assert_allclose_units(
+ list(top.sites)[0].element.mass,
+ np.array(1.9944733e-26) * u.kg,
+ rtol=1e-5,
+ atol=1e-8,
+ )
+
+ top = Topology.load(get_fn("tip3p.mol2"))
+ assert top.name == "tip3p"
+ assert top.n_sites == 3
+ assert_allclose_units(
+ top.box.lengths, 3.0130 * np.ones(3) * u.Å, rtol=1e-5, atol=1e-8
+ )
+ positions_check = [
+ [0.061, 0.1, 0.1],
+ [0.017, 0.09, 0.177],
+ [0.011, 0.154, 0.04],
+ ]
+ for check, site in zip(positions_check, top.sites):
+ assert_allclose_units(
+ site.position,
+ check * u.nm,
+ rtol=1e-5,
+ atol=1e-8,
+ )
+
+ top = Topology.load(get_fn("vmd.mol2"))
+ assert top.name == "vmd"
+ assert top.n_sites == 6
+ assert len(top.bonds) == 5
+ assert top.bonds[0].connection_members[0] == top.sites[0]
+ assert top.box == None
+
+ with pytest.warns(
+ UserWarning,
+ match=r"No charges were detected for site C with index 1",
+ ):
+ top = Topology.load(get_fn("ethane.mol2"))
+ assert list(top.sites)[0].charge is None
+
+ with pytest.warns(
+ UserWarning,
+ match=r"No element detected for site C with index1\, consider manually adding the element to the topology",
+ ):
+ Topology.load(get_fn("benzene.mol2"))
+
+ def test_residue(self):
+ top = Topology.load(get_fn("ethanol_aa.mol2"))
+ assert np.all([site.residue_name == "ETO" for site in top.sites])
+ assert np.all([site.residue_number == 1 for site in top.sites])
+
+ top = Topology.load(get_fn("benzene_ua.mol2"), site_type="lj")
+ assert np.all(
+ [
+ site.residue_name == "BEN1"
+ for site in top.iter_sites("residue_name", "BEN1")
+ ]
+ )
+ assert np.all(
+ [
+ site.residue_number == 1
+ for site in top.iter_sites("residue_name", "BEN1")
+ ]
+ )
+ assert np.all(
+ [
+ site.residue_name == "BEN2"
+ for site in top.iter_sites("residue_name", "BEN2")
+ ]
+ )
+ assert np.all(
+ [
+ site.residue_number == 2
+ for site in top.iter_sites("residue_name", "BEN2")
+ ]
+ )
+
+ def test_lj_system(self):
+ top = Topology.load(get_fn("methane.mol2"), site_type="lj")
+ assert np.all([site.element == None for site in top.sites])
+
+ def test_wrong_path(self):
+ with pytest.raises(
+ OSError, match=r"Provided path to file that does not exist"
+ ):
+ Topology.load("not_a_file.mol2")
+ top = Topology.load(get_fn("ethanegro.mol2"))
+ assert len(top.sites) == 0
+ assert len(top.bonds) == 0
+
+ def test_broken_files(self):
+ with pytest.warns(
+ UserWarning,
+ match=r"The record type indicator @MOLECULE_extra_text\n is not supported. Skipping current section and moving to the next RTI header.",
+ ):
+ Topology.load(get_fn("broken.mol2"))
+ with pytest.warns(
+ UserWarning,
+ match=r"This mol2 file has two boxes to be read in, only reading in one with dimensions Box\(a=0.72",
+ ):
+ Topology.load(get_fn("broken.mol2"))
diff --git a/gmso/utils/files/benzene_ua.mol2 b/gmso/utils/files/benzene_ua.mol2
new file mode 100644
index 000000000..51c22d44a
--- /dev/null
+++ b/gmso/utils/files/benzene_ua.mol2
@@ -0,0 +1,27 @@
+@MOLECULE
+BEN
+ 6 6 1 0 0
+SMALL
+NO_CHARGES
+****
+Energy = 0
+
+@ATOM
+ 1 _CH 0.00000 0.00000 0.00000 C 1 BEN1 0.000000
+ 2 _CH 1.40000 0.00000 0.00000 C 1 BEN1 0.000000
+ 3 _CH 2.10000 1.21244 0.00000 C 1 BEN1 0.000000
+ 4 _CH 1.40000 2.42487 0.00000 C 2 BEN2 0.000000
+ 5 _CH 0.00000 2.42487 0.00000 C 2 BEN2 0.000000
+ 6 _CH -0.70000 1.21244 0.00000 C 2 BEN2 0.000000
+@BOND
+ 1 1 2 1
+ 2 1 6 2
+ 3 2 3 2
+ 4 3 4 1
+ 5 4 5 2
+ 6 5 6 1
+
+@SUBSTRUCTURE
+1 **** 1 TEMP 0 **** **** 0 ROOT
+
+#generated by VMD
diff --git a/gmso/utils/files/broken.mol2 b/gmso/utils/files/broken.mol2
new file mode 100644
index 000000000..e06d1c435
--- /dev/null
+++ b/gmso/utils/files/broken.mol2
@@ -0,0 +1,34 @@
+@MOLECULE_extra_text
+RES
+11 10 1 0 1
+SMALL
+USER_CHARGES
+@CRYSIN
+ 7.2000 9.3380 6.6460 90.0000 90.0000 90.0000 1 1
+@ATOM
+ 1 C -0.0000 -1.4000 -0.0000 C 1 RES -0.000000
+ 2 H -1.1000 -1.4000 0.0000 H 1 RES 0.000000
+ 3 H 1.1000 -1.4000 -0.0000 H 1 RES 0.000000
+ 4 C 0.0000 0.0000 0.0000 C 1 RES -0.000000
+ 5 H 1.0700 0.0000 0.0000 H 1 RES 0.000000
+ 6 H -0.3570 0.7690 0.6530 H 1 RES 0.000000
+ 7 H -0.3570 0.1810 -0.9930 H 1 RES 0.000000
+ 8 C 0.0000 -2.8000 -0.0000 C 1 RES -0.000000
+ 9 H -1.0700 -2.8000 -0.0000 H 1 RES 0.000000
+ 10 H 0.3570 -3.5690 0.6530 H 1 RES 0.000000
+ 11 H 0.3570 -2.9810 -0.9930 H 1 RES 0.000000
+@BOND
+ 1 1 4 1
+ 2 2 1 1
+ 3 3 1 1
+ 4 4 5 1
+ 5 4 7 1
+ 6 4 6 1
+ 7 8 11 1
+ 8 8 1 1
+ 9 8 9 1
+ 10 10 8 1
+@SUBSTRUCTURE
+ 1 RES 1 RESIDUE 0 **** ROOT 0
+@FF_PBC
+ 7.2000 9.3380 6.6460 90.0000 90.0000 90.0000 1 1
diff --git a/gmso/utils/files/ethanegro.mol2 b/gmso/utils/files/ethanegro.mol2
new file mode 100644
index 000000000..5185c494b
--- /dev/null
+++ b/gmso/utils/files/ethanegro.mol2
@@ -0,0 +1,11 @@
+GROningen MAchine for Chemical Simulation
+ 8
+ 1RES C 1 0.107 0.077 0.099
+ 1RES H 2 0.107 0.217 0.099
+ 1RES H 3 0.143 0.000 0.164
+ 1RES H 4 0.143 0.059 0.000
+ 1RES C 5 0.107 0.217 0.099
+ 1RES H 6 0.214 0.217 0.099
+ 1RES H 7 0.071 0.294 0.164
+ 1RES H 8 0.071 0.235 0.000
+ 0.71400 0.79380 0.66460
diff --git a/gmso/utils/files/ethanol_aa.mol2 b/gmso/utils/files/ethanol_aa.mol2
new file mode 100644
index 000000000..6fa793a9a
--- /dev/null
+++ b/gmso/utils/files/ethanol_aa.mol2
@@ -0,0 +1,32 @@
+@MOLECULE
+ETO
+ 9 8 1 0 0
+SMALL
+NO_CHARGES
+****
+Energy = 0
+
+@ATOM
+ 1 C 9.81590 16.23519 15.10426 C 1 ETO 0.000000
+ 2 C 9.08230 17.04506 16.17373 C 1 ETO 0.000000
+ 3 O 9.27716 16.64993 17.51314 O 1 ETO 0.000000
+ 4 H 10.19984 16.49403 17.64498 H 1 ETO 0.000000
+ 5 H 8.03382 17.00899 15.87795 H 1 ETO 0.000000
+ 6 H 9.33296 18.10582 16.18200 H 1 ETO 0.000000
+ 7 H 10.82002 16.65701 15.14800 H 1 ETO 0.000000
+ 8 H 9.41094 16.44303 14.11385 H 1 ETO 0.000000
+ 9 H 9.82400 15.15086 15.21501 H 1 ETO 0.000000
+@BOND
+ 1 1 2 1
+ 2 1 7 1
+ 3 1 8 1
+ 4 1 9 1
+ 5 2 3 1
+ 6 2 5 1
+ 7 2 6 1
+ 8 3 4 1
+
+@SUBSTRUCTURE
+1 **** 1 TEMP 0 **** **** 0 ROOT
+
+#generated by VMD
diff --git a/gmso/utils/files/methane.mol2 b/gmso/utils/files/methane.mol2
new file mode 100644
index 000000000..fa79ce2c3
--- /dev/null
+++ b/gmso/utils/files/methane.mol2
@@ -0,0 +1,15 @@
+@MOLECULE
+MET
+ 1 0 1 0 0
+SMALL
+NO_CHARGES
+****
+Energy = 0
+
+@ATOM
+ 1 _CH4 0.0000 0.0000 0.0000 C 1 MET 0.000000
+
+@SUBSTRUCTURE
+1 **** 1 TEMP 0 **** **** 0 ROOT
+
+#generated by VMD
diff --git a/gmso/utils/files/parmed.mol2 b/gmso/utils/files/parmed.mol2
new file mode 100644
index 000000000..cbde0cca5
--- /dev/null
+++ b/gmso/utils/files/parmed.mol2
@@ -0,0 +1,26 @@
+@MOLECULE
+RES
+8 7 1 0 1
+SMALL
+USER_CHARGES
+@CRYSIN
+ 8.2693 7.9100 6.6460 90.0000 90.0000 90.0000 1 1
+@ATOM
+ 1 C 0.0000 0.0000 0.0000 Carbon 1 RES 0.000000
+ 2 O -0.0000 1.2400 0.0000 O 1 RES -0.000000
+ 3 O -1.2124 -0.7000 0.0000 O 1 RES -0.000000
+ 4 H -1.2124 -1.6700 0.0000 H 1 RES 0.000000
+ 5 C 1.2124 -0.7000 0.0000 C 1 RES -0.000000
+ 6 H 0.6774 -1.6266 -0.0000 H 1 RES 0.000000
+ 7 H 2.0569 -0.7753 0.6530 H 1 RES 0.000000
+ 8 H 1.5477 -0.4813 -0.9930 H 1 RES 0.000000
+@BOND
+ 1 2 1 1
+ 2 3 1 1
+ 3 4 3 1
+ 4 5 1 1
+ 5 6 5 1
+ 6 7 5 1
+ 7 8 5 1
+@SUBSTRUCTURE
+ 1 RES 1 RESIDUE 0 **** ROOT 0
diff --git a/gmso/utils/files/tip3p.mol2 b/gmso/utils/files/tip3p.mol2
new file mode 100644
index 000000000..796375c1c
--- /dev/null
+++ b/gmso/utils/files/tip3p.mol2
@@ -0,0 +1,16 @@
+@MOLECULE
+RES
+3 2 1 0 1
+SMALL
+NO_CHARGES
+@CRYSIN
+ 3.0130 3.0130 3.0130 90.0000 90.0000 90.0000 1 1
+@ATOM
+ 1 O 0.6100 1.0000 1.0000 opls_111 1 opls_111
+ 2 H 0.1700 0.9000 1.7700 opls_112 1 opls_111
+ 3 H 0.1100 1.5400 0.4000 opls_112 1 opls_111
+@BOND
+ 1 1 2 1
+ 2 3 1 1
+@SUBSTRUCTURE
+ 1 RES 1 RESIDUE 0 **** ROOT 0
diff --git a/gmso/utils/files/vmd.mol2 b/gmso/utils/files/vmd.mol2
new file mode 100644
index 000000000..3f889d548
--- /dev/null
+++ b/gmso/utils/files/vmd.mol2
@@ -0,0 +1,26 @@
+@MOLECULE
+TMP
+ 6 5 1 0 0
+SMALL
+NO_CHARGES
+****
+Energy = 0
+
+@ATOM
+ 1 C 0.4688 0.1214 0.2103 C 1 TMP 0.000000
+ 2 C0 -1.0312 0.1214 0.2103 C 1 TMP 0.000000
+ 3 H 0.7944 -0.8241 0.2103 H 1 TMP 0.000000
+ 4 H0 0.7944 0.5942 -0.6086 H 1 TMP 0.000000
+ 5 H1 -1.7812 -0.5281 -0.9147 H 1 TMP 0.000000
+ 6 H2 -1.5312 0.5544 0.9603 H 1 TMP 0.000000
+@BOND
+ 1 1 2 1
+ 2 1 3 1
+ 3 1 4 1
+ 4 2 6 1
+ 5 2 5 1
+
+@SUBSTRUCTURE
+1 **** 1 TEMP 0 **** **** 0 ROOT
+
+#generated by VMD
From e367260bdf86f12e9fc574bb044db7ca7435b896 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 6 Oct 2021 11:02:12 -0500
Subject: [PATCH 011/141] Add member_classes attribute to parametric
potentials. (#549)
* Add member classes and restrict crisscrossing between types and classes
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* WIP- Fix assertion logic; create forcefield potential keys out of member_classes if types not available
* WIP- Remove comments in ff-utils
* Update gmso/core/improper_type.py
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
* Update gmso/core/angle_type.py
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
* WIP- Add additional attributes for Bond
* WIP- Remove extra print statement
* WIP- convert lists to sets while testing equality in test_bond
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
---
gmso/abc/abstract_connection.py | 34 ++++++++++++++++
gmso/core/angle_type.py | 24 ++++++++++--
gmso/core/bond_type.py | 24 ++++++++++--
gmso/core/dihedral_type.py | 26 +++++++++++--
gmso/core/improper_type.py | 24 ++++++++++--
gmso/exceptions.py | 4 ++
gmso/tests/files/ff-example0.xml | 6 +--
gmso/tests/test_bond.py | 33 ++++++++++++++++
gmso/tests/test_forcefield.py | 6 +--
gmso/utils/ff_utils.py | 66 +++++++++++++++++++++++++++++---
10 files changed, 222 insertions(+), 25 deletions(-)
diff --git a/gmso/abc/abstract_connection.py b/gmso/abc/abstract_connection.py
index 51da44da6..ef8bd4e06 100644
--- a/gmso/abc/abstract_connection.py
+++ b/gmso/abc/abstract_connection.py
@@ -31,6 +31,40 @@ def connection_members(self):
def name(self):
return self.__dict__.get("name_")
+ @property
+ def member_types(self):
+ """Return the atomtype of the connection members as a list of string."""
+ return self._get_members_types_or_classes("member_types_")
+
+ @property
+ def member_classes(self):
+ """Return the class of the connection members as a list of string."""
+ return self._get_members_types_or_classes("member_classes_")
+
+ def _has_typed_members(self):
+ """Check if all the members of this connection are typed."""
+ return all(
+ member.atom_type
+ for member in self.__dict__.get("connection_members_")
+ )
+
+ def _get_members_types_or_classes(self, to_return):
+ """Return types or classes for connection members if they exist."""
+ assert to_return in {"member_types_", "member_classes_"}
+ ctype = getattr(self, "connection_type")
+ ctype_attr = getattr(ctype, to_return) if ctype else None
+
+ if ctype_attr:
+ return list(ctype_attr)
+ elif self._has_typed_members():
+ tc = [
+ member.atom_type.name
+ if to_return == "member_types_"
+ else member.atom_type.atomclass
+ for member in self.__dict__.get("connection_members_")
+ ]
+ return tc if all(tc) else None
+
@root_validator(pre=True)
def validate_fields(cls, values):
connection_members = values.get("connection_members")
diff --git a/gmso/core/angle_type.py b/gmso/core/angle_type.py
index 4ad545fcb..d9b0a6d38 100644
--- a/gmso/core/angle_type.py
+++ b/gmso/core/angle_type.py
@@ -26,7 +26,13 @@ class AngleType(ParametricPotential):
member_types_: Optional[Tuple[str, str, str]] = Field(
None,
- description="List-like of gmso.AtomType.name or gmso.AtomType.atomclass "
+ description="List-like of gmso.AtomType.name "
+ "defining the members of this angle type",
+ )
+
+ member_classes_: Optional[Tuple[str, str, str]] = Field(
+ None,
+ description="List-like of gmso.AtomType.atomclass "
"defining the members of this angle type",
)
@@ -38,6 +44,7 @@ def __init__(
independent_variables=None,
potential_expression=None,
member_types=None,
+ member_classes=None,
topology=None,
tags=None,
):
@@ -61,6 +68,7 @@ def __init__(
potential_expression=potential_expression,
topology=topology,
member_types=member_types,
+ member_classes=member_classes,
set_ref=ANGLE_TYPE_DICT,
tags=tags,
)
@@ -69,7 +77,17 @@ def __init__(
def member_types(self):
return self.__dict__.get("member_types_")
+ @property
+ def member_classes(self):
+ return self.__dict__.get("member_classes_")
+
class Config:
- fields = {"member_types_": "member_types"}
+ fields = {
+ "member_types_": "member_types",
+ "member_classes_": "member_classes",
+ }
- alias_to_fields = {"member_types": "member_types_"}
+ alias_to_fields = {
+ "member_types": "member_types_",
+ "member_classes": "member_classes_",
+ }
diff --git a/gmso/core/bond_type.py b/gmso/core/bond_type.py
index 876c10751..3744ecf08 100644
--- a/gmso/core/bond_type.py
+++ b/gmso/core/bond_type.py
@@ -27,7 +27,13 @@ class BondType(ParametricPotential):
member_types_: Optional[Tuple[str, str]] = Field(
None,
- description="List-like of of gmso.AtomType.name or gmso.AtomType.atomclass "
+ description="List-like of of gmso.AtomType.name "
+ "defining the members of this bond type",
+ )
+
+ member_classes_: Optional[Tuple[str, str]] = Field(
+ None,
+ description="List-like of of gmso.AtomType.atomclass "
"defining the members of this bond type",
)
@@ -39,6 +45,7 @@ def __init__(
independent_variables=None,
potential_expression=None,
member_types=None,
+ member_classes=None,
topology=None,
tags=None,
):
@@ -62,6 +69,7 @@ def __init__(
potential_expression=potential_expression,
topology=topology,
member_types=member_types,
+ member_classes=member_classes,
set_ref=BOND_TYPE_DICT,
tags=tags,
)
@@ -71,9 +79,19 @@ def member_types(self):
"""Return the members involved in this bondtype."""
return self.__dict__.get("member_types_")
+ @property
+ def member_classes(self):
+ return self.__dict__.get("member_classes_")
+
class Config:
"""Pydantic configuration for class attributes."""
- fields = {"member_types_": "member_types"}
+ fields = {
+ "member_types_": "member_types",
+ "member_classes_": "member_classes",
+ }
- alias_to_fields = {"member_types": "member_types_"}
+ alias_to_fields = {
+ "member_types": "member_types_",
+ "member_classes": "member_classes_",
+ }
diff --git a/gmso/core/dihedral_type.py b/gmso/core/dihedral_type.py
index e440c5840..e7ce745ba 100644
--- a/gmso/core/dihedral_type.py
+++ b/gmso/core/dihedral_type.py
@@ -32,10 +32,16 @@ class DihedralType(ParametricPotential):
member_types_: Optional[Tuple[str, str, str, str]] = Field(
None,
- description="List-like of of gmso.AtomType.name or gmso.AtomType.atomclass "
+ description="List-like of of gmso.AtomType.name "
"defining the members of this dihedral type",
)
+ member_classes_: Optional[Tuple[str, str, str, str]] = Field(
+ None,
+ description="List-like of of gmso.AtomType.atomclass defining the "
+ "members of this dihedral type",
+ )
+
def __init__(
self,
name="DihedralType",
@@ -44,6 +50,7 @@ def __init__(
independent_variables=None,
potential_expression=None,
member_types=None,
+ member_classes=None,
topology=None,
tags=None,
):
@@ -68,6 +75,7 @@ def __init__(
potential_expression=potential_expression,
topology=topology,
member_types=member_types,
+ member_classes=member_classes,
set_ref=DIHEDRAL_TYPE_DICT,
tags=tags,
)
@@ -76,7 +84,17 @@ def __init__(
def member_types(self):
return self.__dict__.get("member_types_")
- class Config:
- fields = {"member_types_": "member_types"}
+ @property
+ def member_classes(self):
+ return self.__dict__.get("member_classes_")
- alias_to_fields = {"member_types": "member_types_"}
+ class Config:
+ fields = {
+ "member_types_": "member_types",
+ "member_classes_": "member_classes",
+ }
+
+ alias_to_fields = {
+ "member_types": "member_types_",
+ "member_classes": "member_classes_",
+ }
diff --git a/gmso/core/improper_type.py b/gmso/core/improper_type.py
index b9ae26890..daadb7bfa 100644
--- a/gmso/core/improper_type.py
+++ b/gmso/core/improper_type.py
@@ -38,7 +38,13 @@ class ImproperType(ParametricPotential):
member_types_: Optional[Tuple[str, str, str, str]] = Field(
None,
- description="List-like of of gmso.AtomType.name or gmso.AtomType.atomclass "
+ description="List-like of gmso.AtomType.name "
+ "defining the members of this improper type",
+ )
+
+ member_classes_: Optional[Tuple[str, str, str, str]] = Field(
+ None,
+ description="List-like of gmso.AtomType.atomclass "
"defining the members of this improper type",
)
@@ -50,6 +56,7 @@ def __init__(
independent_variables=None,
potential_expression=None,
member_types=None,
+ member_classes=None,
topology=None,
tags=None,
):
@@ -74,6 +81,7 @@ def __init__(
potential_expression=potential_expression,
topology=topology,
member_types=member_types,
+ member_classes=member_classes,
set_ref=IMPROPER_TYPE_DICT,
tags=tags,
)
@@ -83,9 +91,19 @@ def member_types(self):
"""Return member information for this ImproperType."""
return self.__dict__.get("member_types_")
+ @property
+ def member_classes(self):
+ return self.__dict__.get("member_classes_")
+
class Config:
"""Pydantic configuration for attributes."""
- fields = {"member_types_": "member_types"}
+ fields = {
+ "member_types_": "member_types",
+ "member_classes_": "member_classes",
+ }
- alias_to_fields = {"member_types": "member_types_"}
+ alias_to_fields = {
+ "member_types": "member_types_",
+ "member_classes": "member_classes_",
+ }
diff --git a/gmso/exceptions.py b/gmso/exceptions.py
index 1ad485206..d9ea3c028 100644
--- a/gmso/exceptions.py
+++ b/gmso/exceptions.py
@@ -26,5 +26,9 @@ class MissingAtomTypesError(ForceFieldParseError):
"""Error for missing AtomTypes when creating a ForceField from an XML file."""
+class MixedClassAndTypesError(ForceFieldParseError):
+ """Error for missing AtomTypes when creating a ForceField from an XML file."""
+
+
class MissingPotentialError(ForceFieldError):
"""Error for missing Potential when searching for Potentials in a ForceField."""
diff --git a/gmso/tests/files/ff-example0.xml b/gmso/tests/files/ff-example0.xml
index b1d90ba65..8c55f58f2 100644
--- a/gmso/tests/files/ff-example0.xml
+++ b/gmso/tests/files/ff-example0.xml
@@ -31,13 +31,13 @@
-
+
-
+
@@ -65,7 +65,7 @@
-
+
diff --git a/gmso/tests/test_bond.py b/gmso/tests/test_bond.py
index 025003bac..20ab40b96 100644
--- a/gmso/tests/test_bond.py
+++ b/gmso/tests/test_bond.py
@@ -100,3 +100,36 @@ def test_equivalent_members_set(self):
assert tuple(bond_eq.connection_members) in bond.equivalent_members()
assert tuple(bond.connection_members) in bond_eq.equivalent_members()
+
+ def test_bond_member_classes_none(self, typed_ethane):
+ bonds = typed_ethane.bonds
+ assert bonds[0].member_classes is None
+
+ def test_bond_member_types(self, typed_ethane):
+ bonds = typed_ethane.bonds
+ assert set(bonds[0].member_types) == set(["opls_135", "opls_140"])
+
+ def test_bond_member_classes_from_connection_members(self):
+ atype1 = AtomType(atomclass="CT", name="t1")
+
+ atype2 = AtomType(atomclass="CK", name="t2")
+
+ bond = Bond(
+ connection_members=[Atom(atom_type=atype1), Atom(atom_type=atype2)]
+ )
+ assert set(bond.member_classes) == set(["CT", "CK"])
+ assert set(bond.member_types) == set(["t1", "t2"])
+
+ def test_bond_member_types_classes_from_bond_type(self):
+ atom_type = AtomType()
+ atom1 = Atom(atom_type=atom_type)
+ atom2 = Atom(atom_type=atom_type)
+
+ btype = BondType(
+ name="atom1-atom2-bond",
+ member_types=["at1", "at2"],
+ member_classes=["XE", "XE"],
+ )
+ bond = Bond(connection_members=[atom1, atom2], bond_type=btype)
+ assert set(bond.member_classes) == set(["XE", "XE"])
+ assert set(bond.member_types) == set(["at1", "at2"])
diff --git a/gmso/tests/test_forcefield.py b/gmso/tests/test_forcefield.py
index 58c4694ef..c7cf69968 100644
--- a/gmso/tests/test_forcefield.py
+++ b/gmso/tests/test_forcefield.py
@@ -137,7 +137,7 @@ def test_ff_angletypes_from_xml(self, ff):
assert ff.angle_types["Xe~Xe~Xe"].parameters["z"] == u.unyt_quantity(
20, u.kJ / u.mol
)
- assert ff.angle_types["Xe~Xe~Xe"].member_types == ("Xe", "Xe", "Xe")
+ assert ff.angle_types["Xe~Xe~Xe"].member_classes == ("Xe", "Xe", "Xe")
def test_ff_dihedraltypes_from_xml(self, ff):
assert len(ff.dihedral_types) == 2
@@ -154,7 +154,7 @@ def test_ff_dihedraltypes_from_xml(self, ff):
assert ff.dihedral_types["Ar~Ar~Ar~Ar"].parameters[
"z"
] == u.unyt_quantity(100, u.kJ / u.mol)
- assert ff.dihedral_types["Ar~Ar~Ar~Ar"].member_types == (
+ assert ff.dihedral_types["Ar~Ar~Ar~Ar"].member_classes == (
"Ar",
"Ar",
"Ar",
@@ -171,7 +171,7 @@ def test_ff_dihedraltypes_from_xml(self, ff):
assert ff.dihedral_types["Xe~Xe~Xe~Xe"].parameters[
"z"
] == u.unyt_quantity(20, u.kJ / u.mol)
- assert ff.dihedral_types["Xe~Xe~Xe~Xe"].member_types == (
+ assert ff.dihedral_types["Xe~Xe~Xe~Xe"].member_classes == (
"Xe",
"Xe",
"Xe",
diff --git a/gmso/utils/ff_utils.py b/gmso/utils/ff_utils.py
index 70cb21bad..db9b909a2 100644
--- a/gmso/utils/ff_utils.py
+++ b/gmso/utils/ff_utils.py
@@ -16,6 +16,7 @@
ForceFieldError,
ForceFieldParseError,
MissingAtomTypesError,
+ MixedClassAndTypesError,
)
from gmso.utils._constants import FF_TOKENS_SEPARATOR
@@ -115,17 +116,31 @@ def _consolidate_params(params_dict, expression, update_orig=True):
def _get_member_types(tag):
"""Return the types of the members, handle wildcards."""
- at1 = tag.attrib.get("type1", tag.attrib.get("class1", None))
- at2 = tag.attrib.get("type2", tag.attrib.get("class2", None))
- at3 = tag.attrib.get("type3", tag.attrib.get("class3", None))
- at4 = tag.attrib.get("type4", tag.attrib.get("class4", None))
+ at1 = tag.attrib.get("type1")
+ at2 = tag.attrib.get("type2")
+ at3 = tag.attrib.get("type3")
+ at4 = tag.attrib.get("type4")
member_types = filter(lambda x: x is not None, [at1, at2, at3, at4])
member_types = [
"*" if mem_type == "" else mem_type for mem_type in member_types
]
+ return member_types or None
- return member_types
+
+def _get_member_classes(tag):
+ """Return the classes of the members, handle wildcards."""
+ at1 = tag.attrib.get("class1")
+ at2 = tag.attrib.get("class2")
+ at3 = tag.attrib.get("class3")
+ at4 = tag.attrib.get("class4")
+
+ member_classes = filter(lambda x: x is not None, [at1, at2, at3, at4])
+ member_classes = [
+ "*" if mem_type == "" else mem_type for mem_type in member_classes
+ ]
+
+ return member_classes or None
def _parse_default_units(unit_tag):
@@ -183,6 +198,7 @@ def validate(gmso_xml_or_etree, strict=True, greedy=True):
the entries of the `AtomTypes` section
"""
ff_etree = _validate_schema(xml_path_or_etree=gmso_xml_or_etree)
+ _assert_membertype_class_exclusivity(ff_etree)
if strict:
missing = _find_missing_atom_types_or_classes(ff_etree, greedy=greedy)
if missing:
@@ -194,6 +210,36 @@ def validate(gmso_xml_or_etree, strict=True, greedy=True):
)
+def _assert_membertype_class_exclusivity(root):
+ """Check if there's a criss-cross between type and class in Bonded Potentials."""
+ for idx, potential_tag in enumerate(
+ ["BondType", "AngleType", "DihedralType", "ImproperType"], start=2
+ ):
+ potential_iter = root.iterfind(f".//{potential_tag}")
+ for potential in potential_iter:
+ if potential_tag == "ImproperType":
+ iter_end_idx = idx
+ else:
+ iter_end_idx = idx + 1
+ types_and_classes = (
+ (
+ potential.attrib.get(f"type{j}"),
+ potential.attrib.get(f"class{j}"),
+ )
+ for j in range(1, iter_end_idx)
+ )
+ error_msg = (
+ f"{potential_tag} {potential.attrib['name']} has a mix "
+ f"of atom type and atom classes "
+ f"which is not allowed, please use uniform attributes.\n"
+ f"{etree.tostring(potential, encoding='utf-8', pretty_print=True).decode()}"
+ )
+
+ types, classes = zip(*types_and_classes)
+ if any(types) and any(classes):
+ raise MixedClassAndTypesError(error_msg)
+
+
def _find_missing_atom_types_or_classes(ff_etree, greedy=False):
"""Iterate through the forcefield tree and find any missing atomtypes or classes."""
atom_types_iter = ff_etree.iterfind(".//AtomType")
@@ -219,6 +265,8 @@ def _find_missing_atom_types_or_classes(ff_etree, greedy=False):
for potentials_type in remaining_potentials:
for potential_type in potentials_type:
types_or_classes = _get_member_types(potential_type)
+ if not types_or_classes:
+ types_or_classes = _get_member_classes(potential_type)
for type_or_class in types_or_classes:
member_types_or_classes.add(type_or_class)
@@ -372,6 +420,7 @@ def parse_ff_connection_types(connectiontypes_el, child_tag="BondType"):
"parameters": None,
"independent_variables": None,
"member_types": None,
+ "member_classes": None,
}
if connectiontype_expression:
ctor_kwargs["expression"] = connectiontype_expression
@@ -382,6 +431,9 @@ def parse_ff_connection_types(connectiontypes_el, child_tag="BondType"):
)
ctor_kwargs["member_types"] = _get_member_types(connection_type)
+ if not ctor_kwargs["member_types"]:
+ ctor_kwargs["member_classes"] = _get_member_classes(connection_type)
+
if not ctor_kwargs["parameters"]:
ctor_kwargs["parameters"] = _parse_params_values(
connection_type,
@@ -396,9 +448,11 @@ def parse_ff_connection_types(connectiontypes_el, child_tag="BondType"):
ctor_kwargs["independent_variables"] = (
sympify(connectiontype_expression).free_symbols - valued_param_vars
)
+
this_conn_type_key = FF_TOKENS_SEPARATOR.join(
- ctor_kwargs["member_types"]
+ ctor_kwargs.get("member_types") or ctor_kwargs.get("member_classes")
)
+
this_conn_type = TAG_TO_CLASS_MAP[child_tag](**ctor_kwargs)
connectiontypes_dict[this_conn_type_key] = this_conn_type
From 0f2ef1e79d43579da70e472c008e7fd25a9b2dee Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Thu, 7 Oct 2021 13:17:26 -0500
Subject: [PATCH 012/141] Use codecov uploader for uploading coverage reports
(#598)
---
azure-pipelines.yml | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 6070424d7..8b65cf6ff 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -83,9 +83,13 @@ stages:
- bash: |
source activate gmso-dev
- bash <(curl -s https://codecov.io/bash) -C $(Build.SourceVersion)
- condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['python.version'], '3.7' ) )
+ curl -Os https://uploader.codecov.io/latest/linux/codecov
+ chmod +x codecov
+ ./codecov -t ${CODECOV_UPLOAD_TOKEN} -C $(Build.SourceVersion)
+ condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['python.version'], '3.7' ) )
displayName: Upload coverage report to codecov.io
+ env:
+ CODECOV_UPLOAD_TOKEN: $(codecovUploadToken)
- task: PublishCodeCoverageResults@1
inputs:
From 8f58b22e49bc55623e200bddfb779917ec5d9d6e Mon Sep 17 00:00:00 2001
From: Co Quach
Date: Mon, 18 Oct 2021 15:40:24 -0500
Subject: [PATCH 013/141] Bump to version 0.7.0
---
docs/conf.py | 4 ++--
gmso/__init__.py | 2 +-
setup.cfg | 8 ++++----
setup.py | 2 +-
4 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index 23dadc466..fdda86445 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,8 +23,8 @@
author = "Matt Thompson, Alex Yang, Ray Matsumoto, Parashara Shamaprasad, Umesh Timalsina, Co Quach, Ryan S. DeFever, Justin Gilmer"
# The full version, including alpha/beta/rc tags
-version = "0.6.0"
-release = "0.6.0"
+version = "0.7.0"
+release = "0.7.0"
# -- General configuration ---------------------------------------------------
diff --git a/gmso/__init__.py b/gmso/__init__.py
index 569d5dba4..e817c2e2f 100644
--- a/gmso/__init__.py
+++ b/gmso/__init__.py
@@ -16,4 +16,4 @@
from .core.subtopology import SubTopology
from .core.topology import Topology
-__version__ = "0.6.0"
+__version__ = "0.7.0"
diff --git a/setup.cfg b/setup.cfg
index 709ed7f93..218bbb24d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.6.0
+current_version = 0.7.0
commit = True
tag = True
message = Bump to version {new_version}
@@ -9,15 +9,15 @@ tag_name = {new_version}
omit = gmso/tests/*
[coverage:report]
-exclude_lines =
+exclude_lines =
pragma: no cover
-
+
def __repr__
def __str__
except ImportError
if 0:
if __name__ == .__main__.:
-omit =
+omit =
gmso/tests/*
[bumpversion:file:gmso/__init__.py]
diff --git a/setup.py b/setup.py
index 456c704d4..428533650 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
from setuptools import find_packages, setup
#####################################
-VERSION = "0.6.0"
+VERSION = "0.7.0"
ISRELEASED = False
if ISRELEASED:
__version__ = VERSION
From f84af3751d6fc7a99ce51d814b013ac737f3ed51 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 20 Oct 2021 13:47:28 -0500
Subject: [PATCH 014/141] Ignore `setup.cfg` in pre-commit's `Trailing
Whitespace`. (#602)
* Ignore setup.cfg in pre-commit's EOF Fixer.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Properly exclude setup.cfg in trailing-whitespace
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 1 +
setup.cfg | 6 +++---
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9c4daa1e4..845025bfd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,6 +14,7 @@ repos:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
+ exclude: 'setup.cfg'
- repo: https://github.com/psf/black
rev: 21.9b0
hooks:
diff --git a/setup.cfg b/setup.cfg
index 218bbb24d..4f32f662f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,15 +9,15 @@ tag_name = {new_version}
omit = gmso/tests/*
[coverage:report]
-exclude_lines =
+exclude_lines =
pragma: no cover
-
+
def __repr__
def __str__
except ImportError
if 0:
if __name__ == .__main__.:
-omit =
+omit =
gmso/tests/*
[bumpversion:file:gmso/__init__.py]
From 7acc05e1f37cd6623d7aed11a2c67b02bcc7ec98 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 20 Oct 2021 14:58:36 -0500
Subject: [PATCH 015/141] Change warnings to conditional warnings during
element search (#599)
* Change warnings to conditional warnings during element search
* WIP- Add test
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Justin Gilmer
---
gmso/core/element.py | 48 ++++++++++++++++++++++----------------
gmso/tests/test_element.py | 22 +++++++++--------
2 files changed, 40 insertions(+), 30 deletions(-)
diff --git a/gmso/core/element.py b/gmso/core/element.py
index 6faf7522d..12b097b56 100644
--- a/gmso/core/element.py
+++ b/gmso/core/element.py
@@ -85,11 +85,14 @@ def element_by_symbol(symbol):
otherwise return None
"""
symbol_trimmed = sub(r"[0-9 -]", "", symbol).capitalize()
- msg = (
- f"Numbers and spaces are not considered when searching by element symbol.\n"
- f"{symbol} became {symbol_trimmed}"
- )
- warnings.warn(msg)
+
+ if symbol_trimmed != symbol:
+ msg = (
+ f"Numbers and spaces are not considered when searching by element symbol.\n"
+ f"{symbol} became {symbol_trimmed}"
+ )
+ warnings.warn(msg)
+
matched_element = symbol_dict.get(symbol_trimmed)
return matched_element
@@ -112,11 +115,14 @@ def element_by_name(name):
otherwise return None
"""
name_trimmed = sub(r"[0-9 -]", "", name).lower()
- msg = (
- "Numbers and spaces are not considered when searching by element name. \n"
- f"{name} became {name_trimmed}"
- )
- warnings.warn(msg)
+
+ if name_trimmed != name:
+ msg = (
+ "Numbers and spaces are not considered when searching by element name. \n"
+ f"{name} became {name_trimmed}"
+ )
+ warnings.warn(msg)
+
matched_element = name_dict.get(name_trimmed)
return matched_element
@@ -143,11 +149,12 @@ def element_by_atomic_number(atomic_number):
atomic_number_trimmed = int(
sub("[a-z -]", "", atomic_number.lower()).lstrip("0")
)
- msg = (
- f"Letters and spaces are not considered when searching by element atomic number. \n "
- f"{atomic_number} became {atomic_number_trimmed}"
- )
- warnings.warn(msg)
+ if str(atomic_number_trimmed) != atomic_number:
+ msg = (
+ f"Letters and spaces are not considered when searching by element atomic number. \n "
+ f"{atomic_number} became {atomic_number_trimmed}"
+ )
+ warnings.warn(msg)
else:
atomic_number_trimmed = atomic_number
matched_element = atomic_dict.get(atomic_number_trimmed)
@@ -184,11 +191,12 @@ def element_by_mass(mass, exact=True):
if isinstance(mass, str):
# Convert to float if a string is provided
mass_trimmed = np.round(float(sub(r"[a-z -]", "", mass.lower())))
- msg1 = (
- f"Letters and spaces are not considered when searching by element mass.\n"
- f"{mass} became {mass_trimmed}"
- )
- warnings.warn(msg1)
+ if str(mass_trimmed) != mass:
+ msg1 = (
+ f"Letters and spaces are not considered when searching by element mass.\n"
+ f"{mass} became {mass_trimmed}"
+ )
+ warnings.warn(msg1)
elif isinstance(mass, u.unyt_quantity):
# Convert to u.amu if a unyt_quantity is provided
mass_trimmed = np.round(float(mass.to("amu")), 1)
diff --git a/gmso/tests/test_element.py b/gmso/tests/test_element.py
index 0a557ace4..64ee293ba 100644
--- a/gmso/tests/test_element.py
+++ b/gmso/tests/test_element.py
@@ -18,20 +18,22 @@ def test_element(self):
assert carbon.mass == element.Carbon.mass
def test_element_by_name(self):
- for name in ["Carbon", "carbon", " CarBon 12 "]:
- carbon = element.element_by_name(name)
+ for idx, name in enumerate(["Carbon", "carbon", " CarBon 12 "]):
+ with pytest.warns(UserWarning if idx != 1 else None):
+ carbon = element.element_by_name(name)
- assert carbon.name == element.Carbon.name
- assert carbon.symbol == element.Carbon.symbol
- assert carbon.mass == element.Carbon.mass
+ assert carbon.name == element.Carbon.name
+ assert carbon.symbol == element.Carbon.symbol
+ assert carbon.mass == element.Carbon.mass
def test_element_by_symbol(self):
- for symbol in ["N", "n", " N7"]:
- nitrogen = element.element_by_symbol(symbol)
+ for idx, symbol in enumerate(["N", "n", " N7"]):
+ with pytest.warns(UserWarning if idx != 0 else None):
+ nitrogen = element.element_by_symbol(symbol)
- assert nitrogen.name == element.Nitrogen.name
- assert nitrogen.symbol == element.Nitrogen.symbol
- assert nitrogen.mass == element.Nitrogen.mass
+ assert nitrogen.name == element.Nitrogen.name
+ assert nitrogen.symbol == element.Nitrogen.symbol
+ assert nitrogen.mass == element.Nitrogen.mass
def test_element_by_atomic_number(self):
for number in [8, "8", "08", "Oxygen-08"]:
From 87440b4add27aca5aec3c3c1e75e12a406421b76 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Thu, 21 Oct 2021 14:43:42 -0500
Subject: [PATCH 016/141] Properly populate `tags` in `kwargs` for `Potentials`
(#601)
* Properly populate tags in kwargs for Potentials
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Justin Gilmer
---
gmso/abc/abstract_potential.py | 3 +--
gmso/tests/test_atom_type.py | 6 ++++++
gmso/tests/test_forcefield.py | 2 ++
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/gmso/abc/abstract_potential.py b/gmso/abc/abstract_potential.py
index 07693169d..815a48feb 100644
--- a/gmso/abc/abstract_potential.py
+++ b/gmso/abc/abstract_potential.py
@@ -53,9 +53,8 @@ def __init__(
independent_variables=independent_variables,
parameters=None,
)
- tags = kwargs.pop("tags", None)
- if not tags:
+ if not kwargs.get("tags"):
kwargs["tags"] = {}
super().__init__(
diff --git a/gmso/tests/test_atom_type.py b/gmso/tests/test_atom_type.py
index fea761b13..b89b686f4 100644
--- a/gmso/tests/test_atom_type.py
+++ b/gmso/tests/test_atom_type.py
@@ -16,6 +16,12 @@ class TestAtomType(BaseTest):
def atomtype_metadata(self):
return AtomType()
+ def test_atom_type_tag_kwarg(self):
+ at = AtomType(
+ tags={"element": "Li", "comesFrom": "ForceFieldExperiments"}
+ )
+ assert at.tag_names == ["element", "comesFrom"]
+
def test_new_atom_type(self, charge, mass):
new_type = AtomType(
name="mytype",
diff --git a/gmso/tests/test_forcefield.py b/gmso/tests/test_forcefield.py
index c7cf69968..c814528a1 100644
--- a/gmso/tests/test_forcefield.py
+++ b/gmso/tests/test_forcefield.py
@@ -57,6 +57,8 @@ def test_ff_atomtypes_from_xml(self, ff):
assert len(ff.atom_types) == 3
assert "Ar" in ff.atom_types
assert "Xe" in ff.atom_types
+ assert ff.atom_types["Ar"].get_tag("element") == "Ar"
+ assert ff.atom_types["Xe"].get_tag("element") == "Xe"
assert sympify("r") in ff.atom_types["Ar"].independent_variables
assert ff.atom_types["Ar"].parameters["A"] == u.unyt_quantity(
From 45036e986d14c34c2895ef899c180a2b93512119 Mon Sep 17 00:00:00 2001
From: Co Quach
Date: Fri, 22 Oct 2021 11:19:46 -0500
Subject: [PATCH 017/141] Bump to version 0.7.1
---
docs/conf.py | 4 ++--
gmso/__init__.py | 2 +-
setup.cfg | 8 ++++----
setup.py | 2 +-
4 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index fdda86445..a543ba55f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,8 +23,8 @@
author = "Matt Thompson, Alex Yang, Ray Matsumoto, Parashara Shamaprasad, Umesh Timalsina, Co Quach, Ryan S. DeFever, Justin Gilmer"
# The full version, including alpha/beta/rc tags
-version = "0.7.0"
-release = "0.7.0"
+version = "0.7.1"
+release = "0.7.1"
# -- General configuration ---------------------------------------------------
diff --git a/gmso/__init__.py b/gmso/__init__.py
index e817c2e2f..670c838ca 100644
--- a/gmso/__init__.py
+++ b/gmso/__init__.py
@@ -16,4 +16,4 @@
from .core.subtopology import SubTopology
from .core.topology import Topology
-__version__ = "0.7.0"
+__version__ = "0.7.1"
diff --git a/setup.cfg b/setup.cfg
index 4f32f662f..e9fb84c0b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.7.0
+current_version = 0.7.1
commit = True
tag = True
message = Bump to version {new_version}
@@ -9,15 +9,15 @@ tag_name = {new_version}
omit = gmso/tests/*
[coverage:report]
-exclude_lines =
+exclude_lines =
pragma: no cover
-
+
def __repr__
def __str__
except ImportError
if 0:
if __name__ == .__main__.:
-omit =
+omit =
gmso/tests/*
[bumpversion:file:gmso/__init__.py]
diff --git a/setup.py b/setup.py
index 428533650..5afcfca45 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
from setuptools import find_packages, setup
#####################################
-VERSION = "0.7.0"
+VERSION = "0.7.1"
ISRELEASED = False
if ISRELEASED:
__version__ = VERSION
From fb427e9232ac3022aa5aac6a7d438b293b35d083 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 1 Nov 2021 16:59:49 -0500
Subject: [PATCH 018/141] [pre-commit.ci] pre-commit autoupdate (#605)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/psf/black: 21.9b0 → 21.10b0](https://github.com/psf/black/compare/21.9b0...21.10b0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 845025bfd..61ac6703a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
- id: trailing-whitespace
exclude: 'setup.cfg'
- repo: https://github.com/psf/black
- rev: 21.9b0
+ rev: 21.10b0
hooks:
- id: black
args: [--line-length=80]
From 399667aa53fb291499132de0d55ca3a53d6573a0 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 3 Nov 2021 12:23:56 -0500
Subject: [PATCH 019/141] Use proper code-blocks section in installation.rst
(#606)
---
docs/installation.rst | 23 ++++++++++++++++-------
1 file changed, 16 insertions(+), 7 deletions(-)
diff --git a/docs/installation.rst b/docs/installation.rst
index b990f2c52..78578fa50 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -7,7 +7,8 @@ Installing with `conda `__
Starting from ``GMSO`` version ``0.3.0``, you can use `conda `_ to install ``GMSO`` in your preferred environment. This will also install the dependencies of ``GMSO``.
-::
+.. code-block:: bash
+
(your-env) $ conda install -c conda-forge gmso
@@ -16,7 +17,8 @@ Installing from source `conda `__
Dependencies of GMSO are listed in the files ``environment.yml`` (lightweight environment specification containing minimal dependencies) and ``environment-dev.yml`` (comprehensive environment specification including optional and testing packages for developers).
The ``gmso`` or ``gmso-dev`` conda environments can be created with
-::
+
+.. code-block:: bash
$ git clone https://github.com/mosdef-hub/gmso.git
$ cd gmso
@@ -38,7 +40,8 @@ Install an editable version from source
Once all dependencies have been installed and the ``conda`` environment has been created, the ``GMSO`` itself can be installed.
-::
+.. code-block:: bash
+
$ cd gmso
$ conda activate gmso-dev # or gmso depending on your installation
$ pip install -e .
@@ -57,25 +60,31 @@ Testing your installation
-------------------------
``GMSO`` uses ``py.test`` to execute its unit tests. To run them, first install the ``gmso-dev`` environment from above as well as ``gmso`` itself
-::
+
+.. code-block:: bash
$ conda activate gmso-dev
$ pip install -e .
And then run the tests with the ``py.test`` executable:
-::
+.. code-block:: bash
+
$ py.test -v
Install pre-commit
------------------
We use [pre-commit](https://pre-commit.com/) to automatically handle our code formatting and this package is included in the dev environment.
-With the ``gmso-dev`` conda environment active, pre-commit can be installed locally as a git hook by running::
+With the ``gmso-dev`` conda environment active, pre-commit can be installed locally as a git hook by running
+
+.. code-block:: bash
$ pre-commit install
-And (optional) all files can be checked by running::
+And (optional) all files can be checked by running
+
+.. code-block:: bash
$ pre-commit run --all-files
From 4564af6274af068b947c7f24d2e226ba080952e4 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 15 Nov 2021 16:25:19 -0600
Subject: [PATCH 020/141] [pre-commit.ci] pre-commit autoupdate (#607)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pycqa/isort: 5.9.3 → 5.10.1](https://github.com/pycqa/isort/compare/5.9.3...5.10.1)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 61ac6703a..4e51191f6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -21,7 +21,7 @@ repos:
- id: black
args: [--line-length=80]
- repo: https://github.com/pycqa/isort
- rev: 5.9.3
+ rev: 5.10.1
hooks:
- id: isort
name: isort (python)
From 0ffa3469a60c1437b2ff6529ec83c144b62103bb Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 23 Nov 2021 00:40:09 -0600
Subject: [PATCH 021/141] [pre-commit.ci] pre-commit autoupdate (#608)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/psf/black: 21.10b0 → 21.11b1](https://github.com/psf/black/compare/21.10b0...21.11b1)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4e51191f6..077593455 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
- id: trailing-whitespace
exclude: 'setup.cfg'
- repo: https://github.com/psf/black
- rev: 21.10b0
+ rev: 21.11b1
hooks:
- id: black
args: [--line-length=80]
From c12907ee7b36b480145ce96134dc69d127ba8821 Mon Sep 17 00:00:00 2001
From: Justin Gilmer
Date: Wed, 24 Nov 2021 12:19:10 -0600
Subject: [PATCH 022/141] Feat/writer registries (#578)
* Convert xyz file I/O to use the load/save methods
After PR #567, which brought in the addition of concept of Registries to
save/load files; Only the json methods are currently using the registries but can be extended to other formats as well.
This PR introduces the overhaul to the `xyz` file format, one of the
simpler cases. So far, this seems to have worked quite well.
* Overhaul write_gro and read_gro to use the new registry system.
* Update top, gsd, mcf, lammps tests and writer/readers
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Use deepcopy of the topology when modifying the position for gro files
* Support stacking loads_as, saves_as decorators for multiple file extensions
* Use cheaper deepcopy alternative, simplify decorator for multiple file extensions
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
Co-authored-by: Umesh Timalsina
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
gmso/formats/formats_registry.py | 18 ++--
gmso/formats/gro.py | 136 ++++++++++++++++++-------------
gmso/formats/gsd.py | 41 ++++++----
gmso/formats/lammpsdata.py | 3 +
gmso/formats/mcf.py | 2 +
gmso/formats/top.py | 2 +
gmso/formats/xyz.py | 34 ++++----
gmso/tests/test_gro.py | 15 ++--
gmso/tests/test_gsd.py | 5 +-
gmso/tests/test_lammps.py | 39 +++++----
gmso/tests/test_mcf.py | 16 ++--
gmso/tests/test_top.py | 15 ++--
gmso/tests/test_xyz.py | 20 ++---
13 files changed, 199 insertions(+), 147 deletions(-)
diff --git a/gmso/formats/formats_registry.py b/gmso/formats/formats_registry.py
index 1d477263f..4c155f18a 100644
--- a/gmso/formats/formats_registry.py
+++ b/gmso/formats/formats_registry.py
@@ -31,20 +31,26 @@ def get_callable(self, extension):
class saves_as:
"""Decorator to aid saving."""
- def __init__(self, extension):
- self.extension = extension
+ def __init__(self, *extensions):
+ extension_set = set(extensions)
+ self.extensions = extension_set
def __call__(self, method):
"""Register the method as saver for an extension."""
- SaversRegistry.handlers[self.extension] = method
+ for ext in self.extensions:
+ SaversRegistry.handlers[ext] = method
+ return method
class loads_as:
"""Decorator to aid loading."""
- def __init__(self, extension):
- self.extension = extension
+ def __init__(self, *extensions):
+ extension_set = set(extensions)
+ self.extensions = extension_set
def __call__(self, method):
"""Register the method as loader for an extension."""
- LoadersRegistry.handlers[self.extension] = method
+ for ext in self.extensions:
+ LoadersRegistry.handlers[ext] = method
+ return method
diff --git a/gmso/formats/gro.py b/gmso/formats/gro.py
index f52649550..a46ca2204 100644
--- a/gmso/formats/gro.py
+++ b/gmso/formats/gro.py
@@ -6,12 +6,15 @@
import unyt as u
from unyt.array import allclose_units
+import gmso
from gmso.core.atom import Atom
from gmso.core.box import Box
from gmso.core.topology import Topology
from gmso.exceptions import NotYetImplementedWarning
+from gmso.formats.formats_registry import loads_as, saves_as
+@loads_as(".gro")
def read_gro(filename):
"""Create a topology from a provided gro file.
@@ -93,6 +96,7 @@ def read_gro(filename):
return top
+@saves_as(".gro")
def write_gro(top, filename):
"""Write a topology to a gro file.
@@ -116,77 +120,93 @@ def write_gro(top, filename):
Multiple residue assignment has not been added, each `site` will belong to
the same resid of 1 currently.
+ Velocities are not written out.
+
"""
- top = _prepare_topology_to_gro(top)
+ pos_array = np.ndarray.copy(top.positions)
+ pos_array = _validate_positions(pos_array)
with open(filename, "w") as out_file:
out_file.write(
- "{} written by topology at {}\n".format(
+ "{} written by GMSO {} at {}\n".format(
top.name if top.name is not None else "",
+ gmso.__version__,
str(datetime.datetime.now()),
)
)
out_file.write("{:d}\n".format(top.n_sites))
- for idx, site in enumerate(top.sites):
- warnings.warn(
- "Residue information is not currently "
- "stored or written to GRO files.",
- NotYetImplementedWarning,
- )
- # TODO: assign residues
- res_id = 1
- res_name = "X"
- atom_name = site.name
- atom_id = idx + 1
- out_file.write(
- "{0:5d}{1:5s}{2:5s}{3:5d}{4:8.3f}{5:8.3f}{6:8.3f}\n".format(
- res_id,
- res_name,
- atom_name,
- atom_id,
- site.position[0].in_units(u.nm).value,
- site.position[1].in_units(u.nm).value,
- site.position[2].in_units(u.nm).value,
- )
- )
-
- if allclose_units(
- top.box.angles,
- u.degree * [90, 90, 90],
- rtol=1e-5,
- atol=0.1 * u.degree,
- ):
- out_file.write(
- " {:0.5f} {:0.5f} {:0.5f} \n".format(
- top.box.lengths[0].in_units(u.nm).value.round(6),
- top.box.lengths[1].in_units(u.nm).value.round(6),
- top.box.lengths[2].in_units(u.nm).value.round(6),
- )
- )
- else:
- # TODO: Work around GROMACS's triclinic limitations #30
- vectors = top.box.get_vectors()
- out_file.write(
- " {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} \n".format(
- vectors[0, 0].in_units(u.nm).value.round(6),
- vectors[1, 1].in_units(u.nm).value.round(6),
- vectors[2, 2].in_units(u.nm).value.round(6),
- vectors[0, 1].in_units(u.nm).value.round(6),
- vectors[0, 2].in_units(u.nm).value.round(6),
- vectors[1, 0].in_units(u.nm).value.round(6),
- vectors[1, 2].in_units(u.nm).value.round(6),
- vectors[2, 0].in_units(u.nm).value.round(6),
- vectors[2, 1].in_units(u.nm).value.round(6),
- )
- )
+ out_file.write(_prepare_atoms(top, pos_array))
+ out_file.write(_prepare_box(top))
-def _prepare_topology_to_gro(top):
- """Modify topology, as necessary, to fit limitations of the GRO format."""
- if np.min(top.positions) < 0:
+def _validate_positions(pos_array):
+ """Modify coordinates, as necessary, to fit limitations of the GRO format."""
+ if np.min(pos_array) < 0:
warnings.warn(
"Topology contains some negative positions. Translating "
"in order to ensure all coordinates are non-negative."
)
+ min_xyz = np.min(pos_array, axis=0)
+ for i, minimum in enumerate(min_xyz):
+ if minimum < 0.0:
+ for loc in pos_array:
+ loc[i] = loc[i] - minimum
+ return pos_array
- return top
+
+def _prepare_atoms(top, updated_positions):
+ out_str = str()
+ for idx, (site, pos) in enumerate(zip(top.sites, updated_positions)):
+ warnings.warn(
+ "Residue information is not currently "
+ "stored or written to GRO files.",
+ NotYetImplementedWarning,
+ )
+ # TODO: assign residues
+ res_id = 1
+ res_name = "X"
+ atom_name = site.name
+ atom_id = idx + 1
+ out_str = (
+ out_str
+ + "{0:5d}{1:5s}{2:5s}{3:5d}{4:8.3f}{5:8.3f}{6:8.3f}\n".format(
+ res_id,
+ res_name,
+ atom_name,
+ atom_id,
+ pos[0].in_units(u.nm).value,
+ pos[1].in_units(u.nm).value,
+ pos[2].in_units(u.nm).value,
+ )
+ )
+ return out_str
+
+
+def _prepare_box(top):
+ out_str = str()
+ if allclose_units(
+ top.box.angles,
+ u.degree * [90, 90, 90],
+ rtol=1e-5,
+ atol=0.1 * u.degree,
+ ):
+ out_str = out_str + " {:0.5f} {:0.5f} {:0.5f} \n".format(
+ top.box.lengths[0].in_units(u.nm).value.round(6),
+ top.box.lengths[1].in_units(u.nm).value.round(6),
+ top.box.lengths[2].in_units(u.nm).value.round(6),
+ )
+ else:
+ # TODO: Work around GROMACS's triclinic limitations #30
+ vectors = top.box.get_vectors()
+ out_str = out_str + " {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} \n".format(
+ vectors[0, 0].in_units(u.nm).value.round(6),
+ vectors[1, 1].in_units(u.nm).value.round(6),
+ vectors[2, 2].in_units(u.nm).value.round(6),
+ vectors[0, 1].in_units(u.nm).value.round(6),
+ vectors[0, 2].in_units(u.nm).value.round(6),
+ vectors[1, 0].in_units(u.nm).value.round(6),
+ vectors[1, 2].in_units(u.nm).value.round(6),
+ vectors[2, 0].in_units(u.nm).value.round(6),
+ vectors[2, 1].in_units(u.nm).value.round(6),
+ )
+ return out_str
diff --git a/gmso/formats/gsd.py b/gmso/formats/gsd.py
index 67cd2c8e6..e71f2df30 100644
--- a/gmso/formats/gsd.py
+++ b/gmso/formats/gsd.py
@@ -9,6 +9,7 @@
from gmso.core.bond import Bond
from gmso.exceptions import NotYetImplementedWarning
+from gmso.formats.formats_registry import saves_as
from gmso.utils.geometry import coord_shift
from gmso.utils.io import has_gsd
@@ -19,6 +20,7 @@
import gsd.hoomd
+@saves_as(".gsd")
def write_gsd(
top,
filename,
@@ -76,21 +78,11 @@ def write_gsd(
gsd_snapshot.configuration.dimensions = 3
# Write box information
- if allclose_units(
- top.box.angles, np.array([90, 90, 90]) * u.degree, rtol=1e-5, atol=1e-8
- ):
- warnings.warn("Orthorhombic box detected")
- gsd_snapshot.configuration.box = np.hstack(
- (top.box.lengths / ref_distance, np.zeros(3))
- )
- else:
- warnings.warn("Non-orthorhombic box detected")
- u_vectors = top.box.get_unit_vectors()
- lx, ly, lz = top.box.lengths / ref_distance
- xy = u_vectors[1][0]
- xz = u_vectors[2][0]
- yz = u_vectors[2][1]
- gsd_snapshot.configuration.box = np.array([lx, ly, lz, xy, xz, yz])
+ (lx, ly, lz, xy, xz, yz) = _prepare_box_information(top)
+ lx = lx / ref_distance
+ ly = ly / ref_distance
+ lz = lz / ref_distance
+ gsd_snapshot.configuration.box = np.array([lx, ly, lz, xy, xz, yz])
warnings.warn(
"Only writing particle and bond information."
@@ -342,3 +334,22 @@ def _write_dihedral_information(gsd_snapshot, structure):
# gsd_snapshot.dihedrals.typeid = dihedral_typeids
# gsd_snapshot.dihedrals.group = dihedral_groups
pass
+
+
+def _prepare_box_information(top):
+ """Prepare the box information for writing to gsd."""
+ lx = ly = lz = xy = xz = yz = 0.0
+ if allclose_units(
+ top.box.angles, np.array([90, 90, 90]) * u.degree, rtol=1e-5, atol=1e-8
+ ):
+ warnings.warn("Orthorhombic box detected")
+ lx, ly, lz = top.box.lengths
+ xy, xz, yz = 0.0, 0.0, 0.0
+ else:
+ warnings.warn("Non-orthorhombic box detected")
+ u_vectors = top.box.get_unit_vectors()
+ lx, ly, lz = top.box.lengths
+ xy = u_vectors[1][0]
+ xz = u_vectors[2][0]
+ yz = u_vectors[2][1]
+ return lx, ly, lz, xy, xz, yz
diff --git a/gmso/formats/lammpsdata.py b/gmso/formats/lammpsdata.py
index b1e05107a..d40735f15 100644
--- a/gmso/formats/lammpsdata.py
+++ b/gmso/formats/lammpsdata.py
@@ -18,6 +18,7 @@
from gmso.core.box import Box
from gmso.core.element import element_by_mass
from gmso.core.topology import Topology
+from gmso.formats.formats_registry import loads_as, saves_as
from gmso.lib.potential_templates import PotentialTemplateLibrary
from gmso.utils.conversions import (
convert_opls_to_ryckaert,
@@ -25,6 +26,7 @@
)
+@saves_as(".lammps", ".lammpsdata", ".data")
def write_lammpsdata(topology, filename, atom_style="full"):
"""Output a LAMMPS data file.
@@ -323,6 +325,7 @@ def write_lammpsdata(topology, filename, atom_style="full"):
)
+@loads_as(".lammps", ".lammpsdata", ".data")
def read_lammpsdata(
filename, atom_style="full", unit_style="real", potential="lj"
):
diff --git a/gmso/formats/mcf.py b/gmso/formats/mcf.py
index 4ccbcc4c0..af497db52 100644
--- a/gmso/formats/mcf.py
+++ b/gmso/formats/mcf.py
@@ -9,6 +9,7 @@
from gmso import __version__
from gmso.core.topology import Topology
from gmso.exceptions import GMSOError
+from gmso.formats.formats_registry import saves_as
from gmso.lib.potential_templates import PotentialTemplateLibrary
from gmso.utils.compatibility import check_compatibility
from gmso.utils.conversions import convert_ryckaert_to_opls
@@ -18,6 +19,7 @@
potential_templates = PotentialTemplateLibrary()
+@saves_as(".mcf")
def write_mcf(top, filename):
"""Generate a Cassandra MCF from a gmso.core.Topology object.
diff --git a/gmso/formats/top.py b/gmso/formats/top.py
index 18516dc04..67fa89bae 100644
--- a/gmso/formats/top.py
+++ b/gmso/formats/top.py
@@ -5,10 +5,12 @@
from gmso.core.element import element_by_atom_type
from gmso.exceptions import GMSOError
+from gmso.formats.formats_registry import saves_as
from gmso.lib.potential_templates import PotentialTemplateLibrary
from gmso.utils.compatibility import check_compatibility
+@saves_as(".top")
def write_top(top, filename, top_vars=None):
"""Write a gmso.core.Topology object to a GROMACS topology (.TOP) file."""
pot_types = _validate_compatibility(top)
diff --git a/gmso/formats/xyz.py b/gmso/formats/xyz.py
index a66f121a4..44cc7348c 100644
--- a/gmso/formats/xyz.py
+++ b/gmso/formats/xyz.py
@@ -6,8 +6,10 @@
from gmso.core.atom import Atom
from gmso.core.topology import Topology
+from gmso.formats.formats_registry import loads_as, saves_as
+@loads_as(".xyz")
def read_xyz(filename):
"""Reader for xyz file format.
@@ -57,6 +59,7 @@ def read_xyz(filename):
return top
+@saves_as(".xyz")
def write_xyz(top, filename):
"""Writer for xyz file format.
@@ -76,17 +79,20 @@ def write_xyz(top, filename):
top.name, filename, str(datetime.datetime.now())
)
)
- for idx, site in enumerate(top.sites):
- # TODO: Better handling of element guessing and site naming
- if site.element is not None:
- tmp_name = site.element.symbol
- else:
- tmp_name = "X"
- out_file.write(
- "{0} {1:8.3f} {2:8.3f} {3:8.3f}\n".format(
- tmp_name,
- site.position[0].in_units(u.angstrom).value,
- site.position[1].in_units(u.angstrom).value,
- site.position[2].in_units(u.angstrom).value,
- )
- )
+ out_file.write(_prepare_particles(top))
+
+
+def _prepare_particles(top: Topology) -> str:
+ atom_info = str()
+ for _, site in enumerate(top.sites):
+ # TODO: Better handling of element guessing and site naming
+ if site.element is not None:
+ tmp_name = site.element.symbol
+ else:
+ tmp_name = "X"
+
+ 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
+ atom_info = atom_info + f"{tmp_name} {x:8.3f} {y:8.3f} {z:8.3f}\n"
+ return atom_info
diff --git a/gmso/tests/test_gro.py b/gmso/tests/test_gro.py
index 1bfd2d728..c90b8a7d8 100644
--- a/gmso/tests/test_gro.py
+++ b/gmso/tests/test_gro.py
@@ -3,6 +3,7 @@
import unyt as u
from unyt.testing import assert_allclose_units
+from gmso import Topology
from gmso.external.convert_parmed import from_parmed
from gmso.formats.gro import read_gro, write_gro
from gmso.tests.base_test import BaseTest
@@ -15,7 +16,7 @@
@pytest.mark.skipif(not has_parmed, reason="ParmEd is not installed")
class TestGro(BaseTest):
def test_read_gro(self):
- top = read_gro(get_fn("acn.gro"))
+ top = Topology.load(get_fn("acn.gro"))
assert top.name == "ACN"
assert top.n_sites == 6
@@ -23,7 +24,7 @@ def test_read_gro(self):
top.box.lengths, 4 * np.ones(3) * u.nm, rtol=1e-5, atol=1e-8
)
- top = read_gro(get_fn("350-waters.gro"))
+ top = Topology.load(get_fn("350-waters.gro"))
assert top.name == "Generic title"
assert top.n_sites == 1050
@@ -33,17 +34,15 @@ def test_read_gro(self):
def test_wrong_n_atoms(self):
with pytest.raises(ValueError):
- read_gro(get_fn("too_few_atoms.gro"))
+ Topology.load(get_fn("too_few_atoms.gro"))
with pytest.raises(ValueError):
- read_gro(get_fn("too_many_atoms.gro"))
+ Topology.load(get_fn("too_many_atoms.gro"))
def test_write_gro(self):
top = from_parmed(pmd.load_file(get_fn("ethane.gro"), structure=True))
-
- write_gro(top, "out.gro")
+ top.save("out.gro")
def test_write_gro_non_orthogonal(self):
top = from_parmed(pmd.load_file(get_fn("ethane.gro"), structure=True))
top.box.angles = u.degree * [90, 90, 120]
-
- write_gro(top, "out.gro")
+ top.save("out.gro")
diff --git a/gmso/tests/test_gsd.py b/gmso/tests/test_gsd.py
index e0af2500c..c2fce1fda 100644
--- a/gmso/tests/test_gsd.py
+++ b/gmso/tests/test_gsd.py
@@ -2,7 +2,6 @@
import unyt as u
from gmso.external.convert_parmed import from_parmed
-from gmso.formats.gsd import write_gsd
from gmso.tests.base_test import BaseTest
from gmso.utils.io import get_fn, has_gsd, has_parmed, import_
@@ -19,7 +18,7 @@ def test_write_gsd(self):
pmd.load_file(get_fn("ethane.top"), xyz=get_fn("ethane.gro"))
)
- write_gsd(top, "out.gsd")
+ top.save("out.gsd")
def test_write_gsd_non_orthogonal(self):
top = from_parmed(
@@ -27,4 +26,4 @@ def test_write_gsd_non_orthogonal(self):
)
top.box.angles = u.degree * [90, 90, 120]
- write_gsd(top, "out.gsd")
+ top.save("out.gsd")
diff --git a/gmso/tests/test_lammps.py b/gmso/tests/test_lammps.py
index 79acf7b54..be7ae235f 100644
--- a/gmso/tests/test_lammps.py
+++ b/gmso/tests/test_lammps.py
@@ -1,3 +1,4 @@
+import pytest
import unyt as u
from unyt.testing import assert_allclose_units
@@ -9,35 +10,39 @@
class TestLammpsWriter(BaseTest):
- def test_write_lammps(self, typed_ar_system):
- write_lammpsdata(typed_ar_system, filename="data.lammps")
+ @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])
- write_lammpsdata(typed_ar_system, filename="data.triclinic")
+ typed_ar_system.save("triclinic.lammps")
def test_ethane_lammps(self, typed_ethane):
- write_lammpsdata(typed_ethane, "data.ethane")
+ typed_ethane.save("ethane.lammps")
def test_water_lammps(self, typed_water_system):
- write_lammpsdata(typed_water_system, "data.water")
+ typed_water_system.save("data.lammps")
def test_read_lammps(self, filename=get_path("data.lammps")):
- read_lammpsdata(filename)
+ top = gmso.Topology.load(filename)
def test_read_sites(self, filename=get_path("data.lammps")):
- read = read_lammpsdata(filename)
+ read = gmso.Topology.load(filename)
assert read.box == Box(lengths=[1, 1, 1])
def test_read_n_sites(self, typed_ar_system):
- write_lammpsdata(typed_ar_system, filename="data.ar")
- read = read_lammpsdata("data.ar")
+ 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")):
- read = read_lammpsdata(filename)
+ read = gmso.Topology.load(filename)
masses = [i.mass for i in read.atom_types]
assert_allclose_units(
@@ -45,7 +50,7 @@ def test_read_mass(self, filename=get_path("data.lammps")):
)
def test_read_charge(self, filename=get_path("data.lammps")):
- read = read_lammpsdata(filename)
+ read = gmso.Topology.load(filename)
charge = [i.charge for i in read.atom_types]
assert_allclose_units(
@@ -53,7 +58,7 @@ def test_read_charge(self, filename=get_path("data.lammps")):
)
def test_read_sigma(self, filename=get_path("data.lammps")):
- read = read_lammpsdata(filename)
+ read = gmso.Topology.load(filename)
lj = [i.parameters for i in read.atom_types][0]
assert_allclose_units(
@@ -61,7 +66,7 @@ def test_read_sigma(self, filename=get_path("data.lammps")):
)
def test_read_epsilon(self, filename=get_path("data.lammps")):
- read = read_lammpsdata(filename)
+ read = gmso.Topology.load(filename)
lj = [i.parameters for i in read.atom_types][0]
assert_allclose_units(
@@ -72,8 +77,8 @@ def test_read_epsilon(self, filename=get_path("data.lammps")):
)
def test_read_water(self, typed_water_system):
- write_lammpsdata(typed_water_system, filename="data.water")
- water = read_lammpsdata("data.water")
+ typed_water_system.save("water.lammps")
+ water = gmso.Topology.load("water.lammps")
assert_allclose_units(
water.sites[0].charge,
@@ -86,9 +91,9 @@ def test_read_water(self, typed_water_system):
def test_read_lammps_triclinic(self, typed_ar_system):
typed_ar_system.box = Box(lengths=[1, 1, 1], angles=[60, 90, 120])
- write_lammpsdata(typed_ar_system, filename="data.triclinic")
+ typed_ar_system.save("triclinic.lammps")
- read = read_lammpsdata("data.triclinic")
+ read = gmso.Topology.load("triclinic.lammps")
assert_allclose_units(
read.box.lengths,
u.unyt_array([1, 1, 1], u.nm),
diff --git a/gmso/tests/test_mcf.py b/gmso/tests/test_mcf.py
index b9a586ac2..e800be125 100644
--- a/gmso/tests/test_mcf.py
+++ b/gmso/tests/test_mcf.py
@@ -10,15 +10,15 @@
class TestMCF(BaseTest):
def test_write_lj_simple(self, n_typed_ar_system):
top = n_typed_ar_system(n_sites=1)
- write_mcf(top, "ar.mcf")
+ top.save("ar.mcf")
def test_write_mie_simple(self, n_typed_xe_mie):
top = n_typed_xe_mie()
- write_mcf(top, "xe.mcf")
+ top.save("xe.mcf")
def test_write_lj_full(self, n_typed_ar_system):
top = n_typed_ar_system(n_sites=1)
- write_mcf(top, "ar.mcf")
+ top.save("ar.mcf")
mcf_data = []
with open("ar.mcf") as f:
@@ -58,7 +58,7 @@ def test_write_lj_full(self, n_typed_ar_system):
def test_write_mie_full(self, n_typed_xe_mie):
top = n_typed_xe_mie()
- write_mcf(top, "xe.mcf")
+ top.save("xe.mcf")
mcf_data = []
with open("xe.mcf") as f:
@@ -111,16 +111,16 @@ def test_modified_potentials(self, n_typed_ar_system):
top.atom_types[0].set_expression("sigma + epsilon*r")
with pytest.raises(EngineIncompatibilityError):
- write_mcf(top, "out.mcf")
+ top.save("out.mcf")
alternate_lj = "4*epsilon*sigma**12/r**12 - 4*epsilon*sigma**6/r**6"
top.atom_types[0].set_expression(alternate_lj)
- write_mcf(top, "ar.mcf")
+ top.save("ar.mcf")
def test_scaling_factors(self, n_typed_ar_system):
top = n_typed_ar_system()
- write_mcf(top, "ar.mcf")
+ top.save("ar.mcf")
mcf_data = []
with open("ar.mcf") as f:
for line in f:
@@ -143,7 +143,7 @@ def test_scaling_factors(self, n_typed_ar_system):
"coul_14": 0.6,
}
- write_mcf(top, "ar.mcf")
+ top.save("ar.mcf", overwrite=True)
mcf_data = []
with open("ar.mcf") as f:
for line in f:
diff --git a/gmso/tests/test_top.py b/gmso/tests/test_top.py
index 27c114219..518b7be72 100644
--- a/gmso/tests/test_top.py
+++ b/gmso/tests/test_top.py
@@ -13,14 +13,14 @@
class TestTop(BaseTest):
def test_write_top(self, typed_ar_system):
top = typed_ar_system
- write_top(top, "ar.top")
+ top.save("ar.top")
@pytest.mark.parametrize(
"top", ["typed_ar_system", "typed_water_system", "typed_ethane"]
)
def test_pmd_loop(self, top, request):
top = request.getfixturevalue(top)
- write_top(top, "system.top")
+ top.save("system.top")
pmd.load_file("system.top")
def test_modified_potentials(self, ar_system):
@@ -36,12 +36,12 @@ def test_modified_potentials(self, ar_system):
top.atom_types[0].set_expression("sigma + epsilon*r")
with pytest.raises(EngineIncompatibilityError):
- write_top(top, "out.top")
+ top.save("out.top")
alternate_lj = "4*epsilon*sigma**12/r**12 - 4*epsilon*sigma**6/r**6"
top.atom_types[0].set_expression(alternate_lj)
- write_top(top, "ar.top")
+ top.save("ar.top")
def test_water_top(self, water_system):
top = water_system
@@ -71,7 +71,7 @@ def test_water_top(self, water_system):
top.update_angle_types()
- write_top(top, "water.top")
+ top.save("water.top")
def test_ethane_periodic(self, typed_ethane):
from gmso.core.parametric_potential import ParametricPotential
@@ -91,13 +91,12 @@ def test_ethane_periodic(self, typed_ethane):
typed_ethane.update_connection_types()
- write_top(typed_ethane, "system.top")
+ typed_ethane.save("system.top")
struct = pmd.load_file("system.top")
assert len(struct.dihedrals) == 9
def test_custom_defaults(self, typed_ethane):
- write_top(
- typed_ethane,
+ typed_ethane.save(
"system.top",
top_vars={"gen-pairs": "yes", "fudgeLJ": 0.5, "fudgeQQ": 0.5},
)
diff --git a/gmso/tests/test_xyz.py b/gmso/tests/test_xyz.py
index d1b852659..aa6c9c0a3 100644
--- a/gmso/tests/test_xyz.py
+++ b/gmso/tests/test_xyz.py
@@ -2,14 +2,14 @@
import unyt as u
from unyt.testing import assert_allclose_units
-from gmso.formats.xyz import read_xyz, write_xyz
+from gmso import Topology
from gmso.tests.base_test import BaseTest
from gmso.utils.io import get_fn
class TestXYZ(BaseTest):
def test_read_xyz(self):
- top = read_xyz(get_fn("ethane.xyz"))
+ top = Topology.load(get_fn("ethane.xyz"))
assert top.n_sites == 8
assert top.n_connections == 0
assert set([type(site.position) for site in top.sites]) == {
@@ -17,7 +17,7 @@ def test_read_xyz(self):
}
assert set([site.position.units for site in top.sites]) == {u.nm}
- top = read_xyz(get_fn("cu_block.xyz"))
+ top = Topology.load(get_fn("cu_block.xyz"))
assert top.n_sites == 108
assert top.n_connections == 0
@@ -28,19 +28,19 @@ def test_read_xyz(self):
def test_wrong_n_atoms(self):
with pytest.raises(ValueError):
- read_xyz(get_fn("too_few_atoms.xyz"))
+ Topology.load(get_fn("too_few_atoms.xyz"))
with pytest.raises(ValueError):
- read_xyz(get_fn("too_many_atoms.xyz"))
+ Topology.load(get_fn("too_many_atoms.xyz"))
def test_write_xyz(self):
- top = read_xyz(get_fn("ethane.xyz"))
- write_xyz(top, "tmp.xyz")
+ top = Topology.load(get_fn("ethane.xyz"))
+ top.save("tmp.xyz")
def test_full_io(self):
- original_top = read_xyz(get_fn("ethane.xyz"))
+ original_top = Topology.load(get_fn("ethane.xyz"))
- write_xyz(original_top, "full_conversion.xyz")
- new_top = read_xyz("full_conversion.xyz")
+ original_top.save("full_conversion.xyz")
+ new_top = Topology.load("full_conversion.xyz")
assert original_top.n_sites == new_top.n_sites
assert original_top.n_connections == new_top.n_connections
From 5739e2905cca584be2af328c47d788262174913e Mon Sep 17 00:00:00 2001
From: CalCraven <54594941+CalCraven@users.noreply.github.com>
Date: Wed, 1 Dec 2021 13:03:45 -0600
Subject: [PATCH 023/141] Fix epsilon parameter in conversion from foyer xml to
gmso xml for epsilon (#610)
* fix conversions in foyer to gmso xml scheme that grabbed the wrong potential name for epsilon (was ep)
* Fix tests in convert_foyer_xml to make sure to check for epsilon instead of ep parameters
---
gmso/external/convert_foyer_xml.py | 6 +++---
gmso/tests/test_convert_foyer_xml.py | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/gmso/external/convert_foyer_xml.py b/gmso/external/convert_foyer_xml.py
index a8b5bb6fd..35b6c18f9 100644
--- a/gmso/external/convert_foyer_xml.py
+++ b/gmso/external/convert_foyer_xml.py
@@ -246,10 +246,10 @@ def _write_nbforces(forcefield, ff_kwargs):
forcefield,
"AtomTypes",
attrib_dict={
- "expression": "ep * ((sigma/r)**12 - (sigma/r)**6)",
+ "expression": "epsilon * ((sigma/r)**12 - (sigma/r)**6)",
},
)
- parameters_units = {"ep": "kJ/mol", "sigma": "nm"}
+ parameters_units = {"epsilon": "kJ/mol", "sigma": "nm"}
# NonBondedForces
for name, unit in parameters_units.items():
@@ -279,7 +279,7 @@ def _write_nbforces(forcefield, ff_kwargs):
thisAtomType.attrib["name"] = atom_type.get("type", "AtomType")
thisAtomType.attrib["charge"] = atom_type.get("charge")
parameters = {
- "ep": atom_type.get("epsilon"),
+ "epsilon": atom_type.get("epsilon"),
"sigma": atom_type.get("sigma"),
}
_add_parameters(thisAtomType, parameters)
diff --git a/gmso/tests/test_convert_foyer_xml.py b/gmso/tests/test_convert_foyer_xml.py
index ba0d3ffeb..aefb9dbdf 100644
--- a/gmso/tests/test_convert_foyer_xml.py
+++ b/gmso/tests/test_convert_foyer_xml.py
@@ -74,7 +74,7 @@ def test_foyer_atomtypes(self, foyer_fullerene):
"sigma"
] == u.unyt_quantity(0.1, u.nm)
assert foyer_fullerene.atom_types["C"].parameters[
- "ep"
+ "epsilon"
] == u.unyt_quantity(0.1, u.kJ / u.mol)
assert foyer_fullerene.atom_types["C"].mass == u.unyt_quantity(
12.01, u.amu
@@ -85,7 +85,7 @@ def test_foyer_atomtypes(self, foyer_fullerene):
assert foyer_fullerene.atom_types["C"].description == "carbon"
assert foyer_fullerene.atom_types["C"].definition == "[C;r5;r6]"
assert foyer_fullerene.atom_types["C"].expression == sympify(
- "ep*(-sigma**6/r**6 + sigma**12/r**12)"
+ "epsilon*(-sigma**6/r**6 + sigma**12/r**12)"
)
def test_foyer_bonds(self, foyer_fullerene):
From c9b488f72d13cb39d1cebff2d4d0af9fc82eba84 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Thu, 2 Dec 2021 13:46:17 -0600
Subject: [PATCH 024/141] Add mol2 to formarts __init__.py (#611)
* Add mol2 to formarts __init__.py
* add write_mcf to __init__
---
gmso/formats/__init__.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/gmso/formats/__init__.py b/gmso/formats/__init__.py
index 42fb2b6bf..e054fd02b 100644
--- a/gmso/formats/__init__.py
+++ b/gmso/formats/__init__.py
@@ -6,6 +6,8 @@
from .gsd import write_gsd
from .json import save_json
from .lammpsdata import write_lammpsdata
+from .mcf import write_mcf
+from .mol2 import from_mol2
from .top import write_top
from .xyz import read_xyz, write_xyz
From 70acf7b62bfe004c5a86d24418bf68d963f2cda0 Mon Sep 17 00:00:00 2001
From: Co Quach
Date: Mon, 6 Dec 2021 11:12:48 -0600
Subject: [PATCH 025/141] Bump to version 0.7.2
---
docs/conf.py | 4 ++--
gmso/__init__.py | 2 +-
setup.cfg | 2 +-
setup.py | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index a543ba55f..482bb45e0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,8 +23,8 @@
author = "Matt Thompson, Alex Yang, Ray Matsumoto, Parashara Shamaprasad, Umesh Timalsina, Co Quach, Ryan S. DeFever, Justin Gilmer"
# The full version, including alpha/beta/rc tags
-version = "0.7.1"
-release = "0.7.1"
+version = "0.7.2"
+release = "0.7.2"
# -- General configuration ---------------------------------------------------
diff --git a/gmso/__init__.py b/gmso/__init__.py
index 670c838ca..5b91229a9 100644
--- a/gmso/__init__.py
+++ b/gmso/__init__.py
@@ -16,4 +16,4 @@
from .core.subtopology import SubTopology
from .core.topology import Topology
-__version__ = "0.7.1"
+__version__ = "0.7.2"
diff --git a/setup.cfg b/setup.cfg
index e9fb84c0b..c6327efe8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.7.1
+current_version = 0.7.2
commit = True
tag = True
message = Bump to version {new_version}
diff --git a/setup.py b/setup.py
index 5afcfca45..0f395bc1d 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
from setuptools import find_packages, setup
#####################################
-VERSION = "0.7.1"
+VERSION = "0.7.2"
ISRELEASED = False
if ISRELEASED:
__version__ = VERSION
From 3725e1a376b7e024cf9d61712d05922613f5f3f4 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 7 Dec 2021 10:46:54 -0600
Subject: [PATCH 026/141] [pre-commit.ci] pre-commit autoupdate (#612)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/psf/black: 21.11b1 → 21.12b0](https://github.com/psf/black/compare/21.11b1...21.12b0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 077593455..fc0fc2a71 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
- id: trailing-whitespace
exclude: 'setup.cfg'
- repo: https://github.com/psf/black
- rev: 21.11b1
+ rev: 21.12b0
hooks:
- id: black
args: [--line-length=80]
From 9d324f0988bce893655bd51e4dbeca27cd2fd9d6 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Fri, 17 Dec 2021 14:40:15 -0600
Subject: [PATCH 027/141] Fix XML unit conversion (#614)
* Fix unit conversion
Add the per mol in bond/angle/dihedral/improper force constant during conversion
* update tests unit
---
gmso/external/convert_foyer_xml.py | 10 +++++-----
gmso/tests/test_convert_foyer_xml.py | 6 +++---
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/gmso/external/convert_foyer_xml.py b/gmso/external/convert_foyer_xml.py
index 35b6c18f9..f410a5a62 100644
--- a/gmso/external/convert_foyer_xml.py
+++ b/gmso/external/convert_foyer_xml.py
@@ -294,7 +294,7 @@ def _write_harmonic_bonds(forcefield, ff_kwargs):
},
)
- parameters_units = {"k": "kJ/nm**2", "r_eq": "nm"}
+ parameters_units = {"k": "kJ/mol/nm**2", "r_eq": "nm"}
for name, unit in parameters_units.items():
_insert_parameters_units_def(harmonicBondTypes, name, unit)
@@ -327,7 +327,7 @@ def _write_harmonic_angles(forcefield, ff_kwargs):
},
)
- parameters_units = {"k": "kJ/radian**2", "theta_eq": "radian"}
+ parameters_units = {"k": "kJ/mol/radian**2", "theta_eq": "radian"}
for name, unit in parameters_units.items():
_insert_parameters_units_def(harmonicAngleTypes, name, unit)
@@ -361,7 +361,7 @@ def _write_ub_angles(forcefield, ff_kwargs):
},
)
- parameters_units = {"k": "kJ/radian**2", "w_0": "nm"}
+ parameters_units = {"k": "kJ/mol/radian**2", "w_0": "nm"}
for name, unit in parameters_units.items():
_insert_parameters_units_def(ureybradleyAngleTypes, name, unit)
@@ -421,7 +421,7 @@ def _write_periodic_dihedrals(forcefield, ff_kwargs):
for k in range(0, max_j):
_insert_parameters_units_def(
- periodicTorsionDihedralTypes, "k{}".format(k), "kJ"
+ periodicTorsionDihedralTypes, "k{}".format(k), "kJ/mol"
)
_insert_parameters_units_def(
periodicTorsionDihedralTypes, "n{}".format(k), "dimensionless"
@@ -463,7 +463,7 @@ def _write_periodic_impropers(forcefield, ff_kwargs):
for k in range(0, max_j):
_insert_parameters_units_def(
- periodicImproperTypes, "k{}".format(k), "kJ"
+ periodicImproperTypes, "k{}".format(k), "kJ/mol"
)
_insert_parameters_units_def(
periodicImproperTypes, "n{}".format(k), "dimensionless"
diff --git a/gmso/tests/test_convert_foyer_xml.py b/gmso/tests/test_convert_foyer_xml.py
index aefb9dbdf..c08acdc34 100644
--- a/gmso/tests/test_convert_foyer_xml.py
+++ b/gmso/tests/test_convert_foyer_xml.py
@@ -101,7 +101,7 @@ def test_foyer_bonds(self, foyer_fullerene):
] == u.unyt_quantity(0.1, u.nm)
assert foyer_fullerene.bond_types["C~C"].parameters[
"k"
- ] == u.unyt_quantity(1000, u.kJ / u.nm ** 2)
+ ] == u.unyt_quantity(1000, u.kJ / u.mol / u.nm ** 2)
assert foyer_fullerene.bond_types["C~C"].member_types == ("C", "C")
def test_foyer_angles(self, foyer_fullerene):
@@ -114,7 +114,7 @@ def test_foyer_angles(self, foyer_fullerene):
)
assert foyer_fullerene.angle_types["C~C~C"].parameters[
"k"
- ] == u.unyt_quantity(1000, u.kJ / u.rad ** 2)
+ ] == u.unyt_quantity(1000, u.kJ / u.mol / u.rad ** 2)
assert foyer_fullerene.angle_types["C~C~C"].parameters[
"theta_eq"
] == u.unyt_quantity(3.141592, u.rad)
@@ -139,7 +139,7 @@ def test_foyer_dihedrals(self, foyer_periodic):
)
assert foyer_periodic.dihedral_types[
"opls_140~opls_135~opls_135~opls_140"
- ].parameters["k"] == u.unyt_quantity(3.1, u.kJ)
+ ].parameters["k"] == u.unyt_quantity(3.1, u.kJ / u.mol)
assert foyer_periodic.dihedral_types[
"opls_140~opls_135~opls_135~opls_140"
].parameters["n"] == u.unyt_quantity(1, u.dimensionless)
From 855e514e879c5126a17748fec003ba28d47df8a9 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Tue, 21 Dec 2021 11:35:00 -0600
Subject: [PATCH 028/141] Fix bug in convert foyer XML (#622)
* Fix bug when atom class is not parsed correctly when converting connection types
* adjust tests
---
gmso/external/convert_foyer_xml.py | 2 +-
gmso/tests/test_convert_foyer_xml.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/gmso/external/convert_foyer_xml.py b/gmso/external/convert_foyer_xml.py
index f410a5a62..8185b77fa 100644
--- a/gmso/external/convert_foyer_xml.py
+++ b/gmso/external/convert_foyer_xml.py
@@ -235,7 +235,7 @@ def _populate_class_or_type_attrib(root, type_):
"type{}".format(j + 1), "c{}".format(j + 1)
)
elif "class" in item[0]:
- root.attrib["type{}".format(j + 1)] = type_.get(
+ root.attrib["class{}".format(j + 1)] = type_.get(
"class{}".format(j + 1), "c{}".format(j + 1)
)
diff --git a/gmso/tests/test_convert_foyer_xml.py b/gmso/tests/test_convert_foyer_xml.py
index c08acdc34..5865442c9 100644
--- a/gmso/tests/test_convert_foyer_xml.py
+++ b/gmso/tests/test_convert_foyer_xml.py
@@ -102,7 +102,7 @@ def test_foyer_bonds(self, foyer_fullerene):
assert foyer_fullerene.bond_types["C~C"].parameters[
"k"
] == u.unyt_quantity(1000, u.kJ / u.mol / u.nm ** 2)
- assert foyer_fullerene.bond_types["C~C"].member_types == ("C", "C")
+ assert foyer_fullerene.bond_types["C~C"].member_classes == ("C", "C")
def test_foyer_angles(self, foyer_fullerene):
assert len(foyer_fullerene.angle_types) == 1
@@ -118,7 +118,7 @@ def test_foyer_angles(self, foyer_fullerene):
assert foyer_fullerene.angle_types["C~C~C"].parameters[
"theta_eq"
] == u.unyt_quantity(3.141592, u.rad)
- assert foyer_fullerene.angle_types["C~C~C"].member_types == (
+ assert foyer_fullerene.angle_types["C~C~C"].member_classes == (
"C",
"C",
"C",
From c184e9543178cbe633dc1da09cab105ac1d65922 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Tue, 21 Dec 2021 15:25:46 -0600
Subject: [PATCH 029/141] Change scaling factors terminology in gmso.Topology
(#619)
* Change scaling factors terminology in gmso.Topology
To be in consistent with those specified in the forcefield.
* fix typo
* fix typo in get_potential docs
* clarify docs of get_potentials
---
gmso/core/forcefield.py | 4 ++--
gmso/core/topology.py | 24 +++++++++++-----------
gmso/formats/lammpsdata.py | 2 +-
gmso/formats/mcf.py | 8 ++++++--
gmso/tests/test_mcf.py | 12 +++++------
gmso/tests/test_topology.py | 40 ++++++++++++++++++-------------------
6 files changed, 47 insertions(+), 43 deletions(-)
diff --git a/gmso/core/forcefield.py b/gmso/core/forcefield.py
index 4e1c512c5..8217a7d1e 100644
--- a/gmso/core/forcefield.py
+++ b/gmso/core/forcefield.py
@@ -211,9 +211,9 @@ def get_potential(self, group, key, warn=False):
Parameters
----------
- group: {'atom_types', 'bond_types', 'angle_types', 'dihedral_types', 'improper_types'}
+ group: {'atom_type', 'bond_type', 'angle_type', 'dihedral_type', 'improper_type'}
The potential group to perform this search on
- key: str or list of str
+ key: str (for atom type) or list of str (for connection types)
The key to lookup for this potential group
warn: bool, default=False
If true, raise a warning instead of Error if no match found
diff --git a/gmso/core/topology.py b/gmso/core/topology.py
index 180cf15b8..37bca1241 100644
--- a/gmso/core/topology.py
+++ b/gmso/core/topology.py
@@ -173,12 +173,12 @@ def __init__(self, name="Topology", box=None):
self._pairpotential_types = {}
self._pairpotential_types_idx = {}
self._scaling_factors = {
- "vdw_12": 0.0,
- "vdw_13": 0.0,
- "vdw_14": 0.5,
- "coul_12": 0.0,
- "coul_13": 0.0,
- "coul_14": 0.5,
+ "nonBonded12Scale": 0.0,
+ "nonBonded13Scale": 0.0,
+ "nonBonded14Scale": 0.5,
+ "electrostatics12Scale": 0.0,
+ "electrostatics13Scale": 0.0,
+ "electrostatics14Scale": 0.5,
}
self._set_refs = {
ATOM_TYPE_DICT: self._atom_types,
@@ -251,12 +251,12 @@ def scaling_factors(self):
def scaling_factors(self, scaling_factors):
"""Set the scaling factors for the topology."""
expected_items = [
- "vdw_12",
- "vdw_13",
- "vdw_14",
- "coul_12",
- "coul_13",
- "coul_14",
+ "nonBonded12Scale",
+ "nonBonded13Scale",
+ "nonBonded14Scale",
+ "electrostatics12Scale",
+ "electrostatics13Scale",
+ "electrostatics14Scale",
]
if not isinstance(scaling_factors, dict):
raise GMSOError("Scaling factors should be a dictionary")
diff --git a/gmso/formats/lammpsdata.py b/gmso/formats/lammpsdata.py
index d40735f15..cd13bd37e 100644
--- a/gmso/formats/lammpsdata.py
+++ b/gmso/formats/lammpsdata.py
@@ -375,7 +375,7 @@ def read_lammpsdata(
# Validate 'unit_style'
if unit_style not in ["real"]:
- raiseValueError(
+ raise ValueError(
'Unit Style "{}" is invalid or is not currently supported'.format(
unit_style
)
diff --git a/gmso/formats/mcf.py b/gmso/formats/mcf.py
index af497db52..087739985 100644
--- a/gmso/formats/mcf.py
+++ b/gmso/formats/mcf.py
@@ -636,12 +636,16 @@ def _write_intrascaling_information(mcf, top):
mcf.write(header)
mcf.write(
"{:.4f} {:.4f} {:.4f} 1.0000\n".format(
- sf["vdw_12"], sf["vdw_13"], sf["vdw_14"]
+ sf["nonBonded12Scale"],
+ sf["nonBonded13Scale"],
+ sf["nonBonded14Scale"],
)
)
mcf.write(
"{:.4f} {:.4f} {:.4f} 1.0000\n".format(
- sf["coul_12"], sf["coul_13"], sf["coul_14"]
+ sf["electrostatics12Scale"],
+ sf["electrostatics13Scale"],
+ sf["electrostatics14Scale"],
)
)
diff --git a/gmso/tests/test_mcf.py b/gmso/tests/test_mcf.py
index e800be125..eca464638 100644
--- a/gmso/tests/test_mcf.py
+++ b/gmso/tests/test_mcf.py
@@ -135,12 +135,12 @@ def test_scaling_factors(self, n_typed_ar_system):
assert np.allclose(float(mcf_data[-4][3]), 1.0)
top.scaling_factors = {
- "vdw_12": 0.1,
- "vdw_13": 0.2,
- "vdw_14": 0.5,
- "coul_12": 0.2,
- "coul_13": 0.4,
- "coul_14": 0.6,
+ "nonBonded12Scale": 0.1,
+ "nonBonded13Scale": 0.2,
+ "nonBonded14Scale": 0.5,
+ "electrostatics12Scale": 0.2,
+ "electrostatics13Scale": 0.4,
+ "electrostatics14Scale": 0.6,
}
top.save("ar.mcf", overwrite=True)
diff --git a/gmso/tests/test_topology.py b/gmso/tests/test_topology.py
index 6f2363c08..9feefbc99 100644
--- a/gmso/tests/test_topology.py
+++ b/gmso/tests/test_topology.py
@@ -674,31 +674,31 @@ def test_topology_get_dihedrals_for(self, typed_methylnitroaniline):
def test_topology_scale_factors(self, typed_methylnitroaniline):
sf = typed_methylnitroaniline.scaling_factors
- assert np.allclose(sf["vdw_12"], 0.0)
- assert np.allclose(sf["vdw_13"], 0.0)
- assert np.allclose(sf["vdw_14"], 0.5)
- assert np.allclose(sf["coul_12"], 0.0)
- assert np.allclose(sf["coul_13"], 0.0)
- assert np.allclose(sf["coul_14"], 0.5)
+ assert np.allclose(sf["nonBonded12Scale"], 0.0)
+ assert np.allclose(sf["nonBonded13Scale"], 0.0)
+ assert np.allclose(sf["nonBonded14Scale"], 0.5)
+ assert np.allclose(sf["electrostatics12Scale"], 0.0)
+ assert np.allclose(sf["electrostatics13Scale"], 0.0)
+ assert np.allclose(sf["electrostatics14Scale"], 0.5)
def test_topology_change_scale_factors(self, typed_methylnitroaniline):
typed_methylnitroaniline.scaling_factors = {
- "vdw_12": 0.5,
- "vdw_13": 0.5,
- "vdw_14": 1.0,
- "coul_12": 1.0,
- "coul_13": 1.0,
- "coul_14": 1.0,
+ "nonBonded12Scale": 0.5,
+ "nonBonded13Scale": 0.5,
+ "nonBonded14Scale": 1.0,
+ "electrostatics12Scale": 1.0,
+ "electrostatics13Scale": 1.0,
+ "electrostatics14Scale": 1.0,
}
sf = typed_methylnitroaniline.scaling_factors
- assert np.allclose(sf["vdw_12"], 0.5)
- assert np.allclose(sf["vdw_13"], 0.5)
- assert np.allclose(sf["vdw_14"], 1.0)
- assert np.allclose(sf["coul_12"], 1.0)
- assert np.allclose(sf["coul_13"], 1.0)
- assert np.allclose(sf["coul_14"], 1.0)
- typed_methylnitroaniline.scaling_factors["vdw_12"] = 1.0
- assert np.allclose(sf["vdw_12"], 1.0)
+ assert np.allclose(sf["nonBonded12Scale"], 0.5)
+ assert np.allclose(sf["nonBonded13Scale"], 0.5)
+ assert np.allclose(sf["nonBonded14Scale"], 1.0)
+ assert np.allclose(sf["electrostatics12Scale"], 1.0)
+ assert np.allclose(sf["electrostatics13Scale"], 1.0)
+ assert np.allclose(sf["electrostatics14Scale"], 1.0)
+ typed_methylnitroaniline.scaling_factors["nonBonded12Scale"] = 1.0
+ assert np.allclose(sf["nonBonded12Scale"], 1.0)
def test_topology_invalid_scaling_factors(self, typed_methylnitroaniline):
with pytest.raises(GMSOError):
From 06fd824d3755f8defd7f4968c64618ba667711fd Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 3 Jan 2022 10:23:23 -0600
Subject: [PATCH 030/141] [pre-commit.ci] pre-commit autoupdate (#626)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fc0fc2a71..4bfc32092 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,7 +9,7 @@ ci:
submodules: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.0.1
+ rev: v4.1.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
From 073a57d6300a1a35ff9e0dd76235fd725f381172 Mon Sep 17 00:00:00 2001
From: Justin Gilmer
Date: Mon, 3 Jan 2022 15:32:24 -0600
Subject: [PATCH 031/141] Pin pydantic to versions less than 1.9 (#627)
With the release of `pydantic` 1.9.X, there have been some nuanced
changes to how json handling is done.
This has led to breaking changes in the serialization of some of our
data structures.
This does not happen with pydantic 1.8.2 or less.
The current patch is to pin to versions less than 1.9 until we have a
fix.
---
docs/docs-env.yml | 2 +-
environment-dev.yml | 2 +-
environment.yml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/docs-env.yml b/docs/docs-env.yml
index 2ee058ccf..74399928b 100644
--- a/docs/docs-env.yml
+++ b/docs/docs-env.yml
@@ -9,7 +9,7 @@ dependencies:
- unyt >= 2.4
- boltons
- lxml
- - pydantic
+ - pydantic < 1.9.0
- networkx
- ele >= 0.2.0
- numpydoc
diff --git a/environment-dev.yml b/environment-dev.yml
index 392b1abb7..dd2ed3d7d 100644
--- a/environment-dev.yml
+++ b/environment-dev.yml
@@ -7,7 +7,7 @@ dependencies:
- unyt >= 2.4
- boltons
- lxml
- - pydantic
+ - pydantic < 1.9.0
- networkx
- pytest
- mbuild >= 0.11.0
diff --git a/environment.yml b/environment.yml
index bba54d5d1..df1145fcf 100644
--- a/environment.yml
+++ b/environment.yml
@@ -7,6 +7,6 @@ dependencies:
- unyt >= 2.4
- boltons
- lxml
- - pydantic
+ - pydantic < 1.9.0
- networkx
- ele >= 0.2.0
From a12a7c448f37500a62dfd1aa7a6ae548561cdb91 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Tue, 4 Jan 2022 10:45:57 -0600
Subject: [PATCH 032/141] fix typos leading to a bug when getting dihedral and
improper types (#623)
---
gmso/core/forcefield.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/gmso/core/forcefield.py b/gmso/core/forcefield.py
index 8217a7d1e..c0d1ea9af 100644
--- a/gmso/core/forcefield.py
+++ b/gmso/core/forcefield.py
@@ -343,7 +343,7 @@ def _get_dihedral_type(self, atom_types, warn=False):
forward = FF_TOKENS_SEPARATOR.join(atom_types)
reverse = FF_TOKENS_SEPARATOR.join(reversed(atom_types))
- if forward is self.dihedral_types:
+ if forward in self.dihedral_types:
return self.dihedral_types[forward]
if reverse in self.dihedral_types:
return self.dihedral_types[reverse]
@@ -395,7 +395,7 @@ def _get_improper_type(self, atom_types, warn=False):
[atom_types[0], atom_types[2], atom_types[1], atom_types[3]]
)
- if forward is self.improper_types:
+ if forward in self.improper_types:
return self.improper_types[forward]
if reverse in self.improper_types:
return self.improper_types[reverse]
From cc0f42d8cfda9b4236c0f1f4b724dc99036427f3 Mon Sep 17 00:00:00 2001
From: Co Quach
Date: Fri, 7 Jan 2022 11:14:39 -0600
Subject: [PATCH 033/141] Bump to version 0.7.3
---
docs/conf.py | 4 ++--
gmso/__init__.py | 2 +-
setup.cfg | 2 +-
setup.py | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index 482bb45e0..5a37e1da6 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,8 +23,8 @@
author = "Matt Thompson, Alex Yang, Ray Matsumoto, Parashara Shamaprasad, Umesh Timalsina, Co Quach, Ryan S. DeFever, Justin Gilmer"
# The full version, including alpha/beta/rc tags
-version = "0.7.2"
-release = "0.7.2"
+version = "0.7.3"
+release = "0.7.3"
# -- General configuration ---------------------------------------------------
diff --git a/gmso/__init__.py b/gmso/__init__.py
index 5b91229a9..4f0775f7e 100644
--- a/gmso/__init__.py
+++ b/gmso/__init__.py
@@ -16,4 +16,4 @@
from .core.subtopology import SubTopology
from .core.topology import Topology
-__version__ = "0.7.2"
+__version__ = "0.7.3"
diff --git a/setup.cfg b/setup.cfg
index c6327efe8..f6987050e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.7.2
+current_version = 0.7.3
commit = True
tag = True
message = Bump to version {new_version}
diff --git a/setup.py b/setup.py
index 0f395bc1d..75563bb6c 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
from setuptools import find_packages, setup
#####################################
-VERSION = "0.7.2"
+VERSION = "0.7.3"
ISRELEASED = False
if ISRELEASED:
__version__ = VERSION
From 22be8c4ffb98141b3306228adeec70891453f53d Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 1 Feb 2022 12:46:36 -0600
Subject: [PATCH 034/141] [pre-commit.ci] pre-commit autoupdate (#630)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.12b0 → 22.1.0](https://github.com/psf/black/compare/21.12b0...22.1.0)
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
gmso/formats/gro.py | 23 +++++++++++++----------
gmso/formats/gsd.py | 2 +-
gmso/formats/lammpsdata.py | 8 ++++----
gmso/tests/test_convert_foyer_xml.py | 4 ++--
gmso/tests/test_forcefield.py | 12 ++++++------
gmso/tests/test_serialization.py | 2 +-
7 files changed, 28 insertions(+), 25 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4bfc32092..6c884ea4e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
- id: trailing-whitespace
exclude: 'setup.cfg'
- repo: https://github.com/psf/black
- rev: 21.12b0
+ rev: 22.1.0
hooks:
- id: black
args: [--line-length=80]
diff --git a/gmso/formats/gro.py b/gmso/formats/gro.py
index a46ca2204..af6715579 100644
--- a/gmso/formats/gro.py
+++ b/gmso/formats/gro.py
@@ -198,15 +198,18 @@ def _prepare_box(top):
else:
# TODO: Work around GROMACS's triclinic limitations #30
vectors = top.box.get_vectors()
- out_str = out_str + " {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} \n".format(
- vectors[0, 0].in_units(u.nm).value.round(6),
- vectors[1, 1].in_units(u.nm).value.round(6),
- vectors[2, 2].in_units(u.nm).value.round(6),
- vectors[0, 1].in_units(u.nm).value.round(6),
- vectors[0, 2].in_units(u.nm).value.round(6),
- vectors[1, 0].in_units(u.nm).value.round(6),
- vectors[1, 2].in_units(u.nm).value.round(6),
- vectors[2, 0].in_units(u.nm).value.round(6),
- vectors[2, 1].in_units(u.nm).value.round(6),
+ out_str = (
+ out_str
+ + " {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} \n".format(
+ vectors[0, 0].in_units(u.nm).value.round(6),
+ vectors[1, 1].in_units(u.nm).value.round(6),
+ vectors[2, 2].in_units(u.nm).value.round(6),
+ vectors[0, 1].in_units(u.nm).value.round(6),
+ vectors[0, 2].in_units(u.nm).value.round(6),
+ vectors[1, 0].in_units(u.nm).value.round(6),
+ vectors[1, 2].in_units(u.nm).value.round(6),
+ vectors[2, 0].in_units(u.nm).value.round(6),
+ vectors[2, 1].in_units(u.nm).value.round(6),
+ )
)
return out_str
diff --git a/gmso/formats/gsd.py b/gmso/formats/gsd.py
index e71f2df30..401826d6d 100644
--- a/gmso/formats/gsd.py
+++ b/gmso/formats/gsd.py
@@ -132,7 +132,7 @@ def _write_particle_information(
charges = np.array([site.charge for site in top.sites])
e0 = u.physical_constants.eps_0.in_units(
- u.elementary_charge ** 2 / u.Unit("kcal*angstrom/mol")
+ u.elementary_charge**2 / u.Unit("kcal*angstrom/mol")
)
"""
Permittivity of free space = 2.39725e-4 e^2/((kcal/mol)(angstrom)),
diff --git a/gmso/formats/lammpsdata.py b/gmso/formats/lammpsdata.py
index cd13bd37e..eb4bae2ce 100644
--- a/gmso/formats/lammpsdata.py
+++ b/gmso/formats/lammpsdata.py
@@ -120,9 +120,9 @@ def write_lammpsdata(topology, filename, atom_style="full"):
lx = a
xy = b * np.cos(gamma)
xz = c * np.cos(beta)
- ly = np.sqrt(b ** 2 - xy ** 2)
+ 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)
+ lz = np.sqrt(c**2 - xz**2 - yz**2)
xhi = vectors[0][0]
yhi = vectors[1][1]
@@ -564,8 +564,8 @@ def _get_box_coordinates(filename, unit_style, topology):
ly = yhi - ylo
lz = zhi - zlo
- c = np.sqrt(lz ** 2 + xz ** 2 + yz ** 2)
- b = np.sqrt(ly ** 2 + xy ** 2)
+ c = np.sqrt(lz**2 + xz**2 + yz**2)
+ b = np.sqrt(ly**2 + xy**2)
a = lx
alpha = np.arccos((yz * ly + xy * xz) / (b * c))
diff --git a/gmso/tests/test_convert_foyer_xml.py b/gmso/tests/test_convert_foyer_xml.py
index 5865442c9..ff1fbc89c 100644
--- a/gmso/tests/test_convert_foyer_xml.py
+++ b/gmso/tests/test_convert_foyer_xml.py
@@ -101,7 +101,7 @@ def test_foyer_bonds(self, foyer_fullerene):
] == u.unyt_quantity(0.1, u.nm)
assert foyer_fullerene.bond_types["C~C"].parameters[
"k"
- ] == u.unyt_quantity(1000, u.kJ / u.mol / u.nm ** 2)
+ ] == u.unyt_quantity(1000, u.kJ / u.mol / u.nm**2)
assert foyer_fullerene.bond_types["C~C"].member_classes == ("C", "C")
def test_foyer_angles(self, foyer_fullerene):
@@ -114,7 +114,7 @@ def test_foyer_angles(self, foyer_fullerene):
)
assert foyer_fullerene.angle_types["C~C~C"].parameters[
"k"
- ] == u.unyt_quantity(1000, u.kJ / u.mol / u.rad ** 2)
+ ] == u.unyt_quantity(1000, u.kJ / u.mol / u.rad**2)
assert foyer_fullerene.angle_types["C~C~C"].parameters[
"theta_eq"
] == u.unyt_quantity(3.141592, u.rad)
diff --git a/gmso/tests/test_forcefield.py b/gmso/tests/test_forcefield.py
index c814528a1..f3b876904 100644
--- a/gmso/tests/test_forcefield.py
+++ b/gmso/tests/test_forcefield.py
@@ -66,7 +66,7 @@ def test_ff_atomtypes_from_xml(self, ff):
)
assert ff.atom_types["Ar"].parameters["B"] == u.unyt_quantity(4.0, u.nm)
assert ff.atom_types["Ar"].parameters["C"] == u.unyt_quantity(
- 0.5, u.kcal / u.mol * u.nm ** 6
+ 0.5, u.kcal / u.mol * u.nm**6
)
assert ff.atom_types["Ar"].mass == u.unyt_quantity(39.948, u.amu)
assert ff.atom_types["Ar"].charge == u.unyt_quantity(0.0, u.coulomb)
@@ -83,7 +83,7 @@ def test_ff_atomtypes_from_xml(self, ff):
)
assert ff.atom_types["Xe"].parameters["B"] == u.unyt_quantity(5.0, u.nm)
assert ff.atom_types["Xe"].parameters["C"] == u.unyt_quantity(
- 0.3, u.kcal / u.mol * u.nm ** 6
+ 0.3, u.kcal / u.mol * u.nm**6
)
assert ff.atom_types["Xe"].mass == u.unyt_quantity(131.293, u.amu)
assert ff.atom_types["Xe"].charge == u.unyt_quantity(0.0, u.coulomb)
@@ -408,7 +408,7 @@ def test_forcefield_get_potential_bond_type(self, opls_ethane_foyer):
assert sympify("r") in bt.independent_variables
assert allclose_units_mixed(
- params.values(), [284512.0 * u.kJ / u.nm ** 2, 0.109 * u.nm]
+ params.values(), [284512.0 * u.kJ / u.nm**2, 0.109 * u.nm]
)
def test_forcefield_get_potential_bond_type_reversed(
@@ -426,7 +426,7 @@ def test_forcefield_get_parameters_bond_type(self, opls_ethane_foyer):
)
assert allclose_units_mixed(
- params.values(), [224262.4 * u.kJ / u.nm ** 2, 0.1529 * u.nm]
+ params.values(), [224262.4 * u.kJ / u.nm**2, 0.1529 * u.nm]
)
def test_forcefield_get_potential_angle_type(self, opls_ethane_foyer):
@@ -442,7 +442,7 @@ def test_forcefield_get_potential_angle_type(self, opls_ethane_foyer):
assert allclose_units_mixed(
params.values(),
- [313.8 * u.kJ / u.radian ** 2, 1.932079482 * u.radian],
+ [313.8 * u.kJ / u.radian**2, 1.932079482 * u.radian],
)
def test_forcefield_get_potential_angle_type_reversed(
@@ -461,7 +461,7 @@ def test_forcefield_get_parameters_angle_type(self, opls_ethane_foyer):
assert allclose_units_mixed(
params.values(),
- [276.144 * u.kJ / u.radian ** 2, 1.8814649337 * u.radian],
+ [276.144 * u.kJ / u.radian**2, 1.8814649337 * u.radian],
)
def test_forcefield_get_potential_dihedral_type(self, opls_ethane_foyer):
diff --git a/gmso/tests/test_serialization.py b/gmso/tests/test_serialization.py
index 1d82cf638..6a4fd1861 100644
--- a/gmso/tests/test_serialization.py
+++ b/gmso/tests/test_serialization.py
@@ -26,7 +26,7 @@ def full_atom_type(self):
independent_variables={"a"},
parameters={
"b": 2.0 * u.amu,
- "c": 3.0 * u.nm / u.kg ** 2,
+ "c": 3.0 * u.nm / u.kg**2,
"d": 5.0 * u.kJ / u.mol,
"e": 1.0 * u.C,
},
From b077e66c7d53d36f7709cbaf6e702817628ebae7 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 2 Feb 2022 11:56:05 -0600
Subject: [PATCH 035/141] Add combining rule to forcefield (#632)
---
gmso/core/forcefield.py | 3 +++
gmso/tests/files/ff-example0.xml | 2 +-
gmso/tests/test_forcefield.py | 4 ++++
gmso/utils/ff_utils.py | 5 ++++-
gmso/utils/schema/ff-gmso.xsd | 3 ++-
5 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/gmso/core/forcefield.py b/gmso/core/forcefield.py
index c0d1ea9af..774ebe239 100644
--- a/gmso/core/forcefield.py
+++ b/gmso/core/forcefield.py
@@ -90,6 +90,7 @@ def __init__(self, xml_loc=None, strict=True, greedy=True):
self.pairpotential_types = ff.pairpotential_types
self.potential_groups = ff.potential_groups
self.scaling_factors = ff.scaling_factors
+ self.combining_rule = ff.combining_rule
self.units = ff.units
else:
self.name = "ForceField"
@@ -102,6 +103,7 @@ def __init__(self, xml_loc=None, strict=True, greedy=True):
self.pairpotential_types = {}
self.potential_groups = {}
self.scaling_factors = {}
+ self.combining_rule = "geometric"
self.units = {}
@property
@@ -610,6 +612,7 @@ def from_xml(cls, xmls_or_etrees, strict=True, greedy=True):
ff.name = names[0]
ff.version = versions[0]
ff.scaling_factors = ff_meta_map["scaling_factors"]
+ ff.combining_rule = ff_meta_map["combining_rule"]
ff.units = ff_meta_map["Units"]
ff.atom_types = atom_types_dict.maps[0]
ff.bond_types = bond_types_dict
diff --git a/gmso/tests/files/ff-example0.xml b/gmso/tests/files/ff-example0.xml
index 8c55f58f2..03dab5bf4 100644
--- a/gmso/tests/files/ff-example0.xml
+++ b/gmso/tests/files/ff-example0.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/gmso/tests/test_forcefield.py b/gmso/tests/test_forcefield.py
index f3b876904..f9e3c8e40 100644
--- a/gmso/tests/test_forcefield.py
+++ b/gmso/tests/test_forcefield.py
@@ -37,6 +37,10 @@ def test_scaling_factors_from_xml(self, ff):
assert ff.scaling_factors["nonBonded14Scale"] == 0.67
assert ff.scaling_factors["electrostatics14Scale"] == 0.5
+ def test_ff_combining_rule(self, ff, opls_ethane_foyer):
+ assert ff.combining_rule == "lorentz"
+ assert opls_ethane_foyer.combining_rule == "geometric"
+
@pytest.mark.parametrize(
"unit_name,unit_value",
[
diff --git a/gmso/utils/ff_utils.py b/gmso/utils/ff_utils.py
index db9b909a2..b245a4679 100644
--- a/gmso/utils/ff_utils.py
+++ b/gmso/utils/ff_utils.py
@@ -321,7 +321,10 @@ def parse_ff_metadata(element):
"Units": _parse_default_units,
"ScalingFactors": _parse_scaling_factors,
}
- ff_meta = {"scaling_factors": parsers["ScalingFactors"](element)}
+ ff_meta = {
+ "scaling_factors": parsers["ScalingFactors"](element),
+ "combining_rule": element.get("combiningRule", "geometric"),
+ }
for metatype in element:
if metatype.tag in metatypes:
ff_meta[metatype.tag] = parsers[metatype.tag](metatype)
diff --git a/gmso/utils/schema/ff-gmso.xsd b/gmso/utils/schema/ff-gmso.xsd
index 63e4a52c1..43a843fe2 100644
--- a/gmso/utils/schema/ff-gmso.xsd
+++ b/gmso/utils/schema/ff-gmso.xsd
@@ -213,6 +213,7 @@
+
@@ -231,7 +232,7 @@
-
+
From 4956abc6409f801f464eecc9f97d7d65c7c3d510 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Thu, 3 Feb 2022 14:59:30 -0600
Subject: [PATCH 036/141] Fix bugs in convert_foyer_xml (#629)
* Fix bugs in convert_foyer_xml
Fix bug where combining_rule info is not properly transfered.
Fix wrong potential expression for nonbonded, harmonic bonds,
and harmonic angles.
* fixing expressions in foyer conversion and modifying lammps writer to considering sympy expressions
* Make tests more thorough
Relocate combining_rule to FFMeta, add combining_rule to xml schema.
Add bond/angle expression comparison to tests.
* updates to lammps writer including unit fixes in reader
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* revised sympy checks and adding more tests for lammps writer
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* failed to save file before committing
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* left in debug statement, removed
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Update gmso/external/convert_foyer_xml.py
Co-authored-by: Justin Gilmer
* revert ambuiguous checks
Co-authored-by: Chris Iacovella
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Justin Gilmer
Co-authored-by: Justin Gilmer
---
gmso/external/convert_foyer_xml.py | 14 ++--
gmso/formats/lammpsdata.py | 102 +++++++++++++++++++--------
gmso/tests/test_convert_foyer_xml.py | 8 ++-
gmso/tests/test_lammps.py | 85 +++++++++++++++++++++-
gmso/utils/schema/ff-gmso.xsd | 1 +
5 files changed, 173 insertions(+), 37 deletions(-)
diff --git a/gmso/external/convert_foyer_xml.py b/gmso/external/convert_foyer_xml.py
index 8185b77fa..fa31c52df 100644
--- a/gmso/external/convert_foyer_xml.py
+++ b/gmso/external/convert_foyer_xml.py
@@ -65,9 +65,11 @@ def from_foyer_xml(
ff_root = foyer_xml_tree.getroot()
name = ff_root.attrib.get("name")
version = ff_root.attrib.get("version")
+ combining_rule = ff_root.attrib.get("combining_rule")
f_kwargs = {
"name": name,
"version": version,
+ "combining_rule": combining_rule,
"coulomb14scale": [],
"lj14scale": [],
"atom_types": [],
@@ -143,6 +145,10 @@ def _write_gmso_xml(gmso_xml, **kwargs):
forcefield.attrib["version"] = "0.0.1"
ffMeta = _create_sub_element(forcefield, "FFMetaData")
+ if kwargs.get("combining_rule"):
+ ffMeta.attrib["combining_rule"] = kwargs.get("combining_rule")
+ else:
+ ffMeta.attrib["combining_rule"] = "geometric"
if kwargs["coulomb14scale"]:
ffMeta.attrib["electrostatics14Scale"] = kwargs["coulomb14scale"]
@@ -246,7 +252,7 @@ def _write_nbforces(forcefield, ff_kwargs):
forcefield,
"AtomTypes",
attrib_dict={
- "expression": "epsilon * ((sigma/r)**12 - (sigma/r)**6)",
+ "expression": "4 * epsilon * ((sigma/r)**12 - (sigma/r)**6)",
},
)
parameters_units = {"epsilon": "kJ/mol", "sigma": "nm"}
@@ -290,7 +296,7 @@ def _write_harmonic_bonds(forcefield, ff_kwargs):
forcefield,
"BondTypes",
attrib_dict={
- "expression": "k * (r-r_eq)**2",
+ "expression": "1/2 * k * (r-r_eq)**2",
},
)
@@ -323,7 +329,7 @@ def _write_harmonic_angles(forcefield, ff_kwargs):
forcefield,
"AngleTypes",
attrib_dict={
- "expression": "k * (theta - theta_eq)**2",
+ "expression": "1/2 * k * (theta - theta_eq)**2",
},
)
@@ -357,7 +363,7 @@ def _write_ub_angles(forcefield, ff_kwargs):
forcefield,
"AngleTypes",
attrib_dict={
- "expression": "k * (w - w_0) ** 2",
+ "expression": "1/2 * k * (w - w_0) ** 2",
},
)
diff --git a/gmso/formats/lammpsdata.py b/gmso/formats/lammpsdata.py
index eb4bae2ce..1256ca76e 100644
--- a/gmso/formats/lammpsdata.py
+++ b/gmso/formats/lammpsdata.py
@@ -6,7 +6,7 @@
import numpy as np
import unyt as u
-from sympy import sympify
+from sympy import simplify, sympify
from unyt.array import allclose_units
from gmso.core.angle import Angle
@@ -183,48 +183,90 @@ def write_lammpsdata(topology, filename, atom_style="full"):
# Pair coefficients
data.write("\nPair Coeffs # lj\n\n")
for idx, param in enumerate(topology.atom_types):
- data.write(
- "{}\t{:.5f}\t{:.5f}\n".format(
- idx + 1,
- param.parameters["epsilon"]
- .in_units(u.Unit("kcal/mol"))
- .value,
- param.parameters["sigma"].in_units(u.angstrom).value,
- )
+ # 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 topology.bonds:
- data.write("\nBond Coeffs\n\n")
- for idx, bond_type in enumerate(topology.bond_types):
+ 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"))
+ param.parameters["epsilon"]
+ .in_units(u.Unit("kcal/mol"))
.value
- / 2,
- bond_type.parameters["r_eq"]
- .in_units(u.Unit("angstrom"))
+ / float(scaling_factor),
+ param.parameters["sigma"]
+ .in_units(u.angstrom)
.value,
)
)
+ else:
+ 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
+ )
+
+ 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):
- data.write(
- "{}\t{:.5f}\t{:.5f}\n".format(
- idx + 1,
- angle_type.parameters["k"]
- .in_units(u.Unit("kcal/mol/radian**2"))
- .value
- / 2,
- angle_type.parameters["theta_eq"]
- .in_units(u.Unit("degree"))
- .value,
- )
+ # 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")
@@ -403,7 +445,7 @@ def get_units(unit_style):
# Need separate angle units for harmonic force constant and angle
unit_style_dict = {
"real": {
- "mass": u.g,
+ "mass": u.g / u.mol,
"distance": u.angstrom,
"energy": u.kcal / u.mol,
"angle_k": u.radian,
@@ -444,7 +486,7 @@ def _get_connection(filename, topology, unit_style, connection_type):
* 2
)
c_type.parameters["r_eq"] = float(line.split()[2]) * (
- get_units(unit_style)["distance"] ** 2
+ get_units(unit_style)["distance"]
)
elif connection_type == "angle":
c_type = AngleType(name=line.split()[0])
diff --git a/gmso/tests/test_convert_foyer_xml.py b/gmso/tests/test_convert_foyer_xml.py
index ff1fbc89c..2f7d593fb 100644
--- a/gmso/tests/test_convert_foyer_xml.py
+++ b/gmso/tests/test_convert_foyer_xml.py
@@ -85,13 +85,16 @@ def test_foyer_atomtypes(self, foyer_fullerene):
assert foyer_fullerene.atom_types["C"].description == "carbon"
assert foyer_fullerene.atom_types["C"].definition == "[C;r5;r6]"
assert foyer_fullerene.atom_types["C"].expression == sympify(
- "epsilon*(-sigma**6/r**6 + sigma**12/r**12)"
+ "4*epsilon*(-sigma**6/r**6 + sigma**12/r**12)"
)
def test_foyer_bonds(self, foyer_fullerene):
assert len(foyer_fullerene.bond_types) == 1
assert "C~C" in foyer_fullerene.bond_types
+ assert foyer_fullerene.bond_types["C~C"].expression == sympify(
+ "1/2 * k * (r-r_eq)**2"
+ )
assert (
sympify("r")
in foyer_fullerene.bond_types["C~C"].independent_variables
@@ -108,6 +111,9 @@ def test_foyer_angles(self, foyer_fullerene):
assert len(foyer_fullerene.angle_types) == 1
assert "C~C~C" in foyer_fullerene.angle_types
+ assert foyer_fullerene.angle_types["C~C~C"].expression == sympify(
+ "1/2 * k * (theta - theta_eq)**2"
+ )
assert (
sympify("theta")
in foyer_fullerene.angle_types["C~C~C"].independent_variables
diff --git a/gmso/tests/test_lammps.py b/gmso/tests/test_lammps.py
index be7ae235f..a74788ae1 100644
--- a/gmso/tests/test_lammps.py
+++ b/gmso/tests/test_lammps.py
@@ -30,7 +30,7 @@ def test_water_lammps(self, typed_water_system):
def test_read_lammps(self, filename=get_path("data.lammps")):
top = gmso.Topology.load(filename)
- def test_read_sites(self, filename=get_path("data.lammps")):
+ def test_read_box(self, filename=get_path("data.lammps")):
read = gmso.Topology.load(filename)
assert read.box == Box(lengths=[1, 1, 1])
@@ -46,7 +46,7 @@ def test_read_mass(self, filename=get_path("data.lammps")):
masses = [i.mass for i in read.atom_types]
assert_allclose_units(
- masses, u.unyt_array(1.0079, u.g), rtol=1e-5, atol=1e-8
+ masses, u.unyt_array(1.0079, u.g / u.mol), rtol=1e-5, atol=1e-8
)
def test_read_charge(self, filename=get_path("data.lammps")):
@@ -106,3 +106,84 @@ def test_read_lammps_triclinic(self, typed_ar_system):
rtol=1e-5,
atol=1e-8,
)
+
+ def test_read_n_bonds(self, typed_ethane):
+ typed_ethane.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")
+ read = gmso.Topology.load("ethane.lammps")
+
+ assert read.n_angles == 12
+
+ def test_read_bond_params(self, typed_ethane):
+ typed_ethane.save("ethane.lammps")
+ read = gmso.Topology.load("ethane.lammps")
+ bond_params = [i.parameters for i in read.bond_types]
+
+ assert_allclose_units(
+ bond_params[0]["k"],
+ u.unyt_array(680, (u.kcal / u.mol / u.angstrom / u.angstrom)),
+ rtol=1e-5,
+ atol=1e-8,
+ )
+ assert_allclose_units(
+ bond_params[0]["r_eq"],
+ u.unyt_array(1.09, (u.angstrom)),
+ rtol=1e-5,
+ atol=1e-8,
+ )
+ assert_allclose_units(
+ bond_params[1]["k"],
+ u.unyt_array(536, (u.kcal / u.mol / u.angstrom / u.angstrom)),
+ rtol=1e-5,
+ atol=1e-8,
+ )
+ assert_allclose_units(
+ bond_params[1]["r_eq"],
+ u.unyt_array(1.529, (u.angstrom)),
+ rtol=1e-5,
+ atol=1e-8,
+ )
+
+ def test_read_angle_params(self, typed_ethane):
+ typed_ethane.save("ethane.lammps")
+ read = gmso.Topology.load("ethane.lammps")
+ angle_params = [i.parameters for i in read.angle_types]
+
+ assert_allclose_units(
+ angle_params[0]["k"],
+ u.unyt_array(75, (u.kcal / u.mol / u.radian / u.radian)),
+ rtol=1e-5,
+ atol=1e-8,
+ )
+ assert_allclose_units(
+ angle_params[0]["theta_eq"],
+ u.unyt_array(110.7, (u.degree)),
+ rtol=1e-5,
+ atol=1e-8,
+ )
+ assert_allclose_units(
+ 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"],
+ 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")
+ read = gmso.Topology.load("ethane.lammps")
+
+ assert read.n_dihedrals == 9
+"""
diff --git a/gmso/utils/schema/ff-gmso.xsd b/gmso/utils/schema/ff-gmso.xsd
index 43a843fe2..f4bc4e983 100644
--- a/gmso/utils/schema/ff-gmso.xsd
+++ b/gmso/utils/schema/ff-gmso.xsd
@@ -211,6 +211,7 @@
+
From d592215d37b86697d6c1cda0e0a7c13f5fa515ec Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Mon, 7 Feb 2022 09:50:19 -0600
Subject: [PATCH 037/141] Drop python 3.6 (#633)
* Drop python 3.6
* WIP- Rename strategy matrix keys
Co-authored-by: Justin Gilmer
---
azure-pipelines.yml | 12 ++++++------
environment-dev.yml | 1 +
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 8b65cf6ff..2a1ed4cdf 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -26,24 +26,24 @@ stages:
- job: TestsForGMSO
strategy:
matrix:
- Python36Ubuntu:
- imageName: 'ubuntu-latest'
- python.version: 3.6
Python37Ubuntu:
imageName: 'ubuntu-latest'
python.version: 3.7
Python38Ubuntu:
imageName: 'ubuntu-latest'
python.version: 3.8
- Python36macOS:
- imageName: 'macOS-latest'
- python.version: 3.6
+ Python39Ubuntu:
+ imageName: 'ubuntu-latest'
+ python.version: 3.9
Python37macOS:
imageName: 'macOS-latest'
python.version: 3.7
Python38macOS:
imageName: 'macOS-latest'
python.version: 3.8
+ Python39macOS:
+ imageName: 'macOS-latest'
+ python.version: 3.9
pool:
vmImage: $(imageName)
diff --git a/environment-dev.yml b/environment-dev.yml
index dd2ed3d7d..8b90ae589 100644
--- a/environment-dev.yml
+++ b/environment-dev.yml
@@ -22,3 +22,4 @@ dependencies:
- ipywidgets
- ele >= 0.2.0
- pre-commit
+ - python=3.9
From 2e376d493db51ab89fdd75de428f76a6ded8eb22 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Tue, 22 Feb 2022 14:24:04 -0600
Subject: [PATCH 038/141] Prevent accidental defaults while creating Potentials
(#635)
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
---
gmso/abc/abstract_potential.py | 10 +++---
gmso/core/angle_type.py | 23 +++++++------
gmso/core/atom_type.py | 25 +++++++-------
gmso/core/bond_type.py | 24 ++++++-------
gmso/core/dihedral_type.py | 25 +++++++-------
gmso/core/improper_type.py | 25 +++++++-------
gmso/core/pairpotential_type.py | 17 +++++-----
gmso/core/parametric_potential.py | 56 +++++++++++++++++++++----------
gmso/external/convert_parmed.py | 14 ++++++--
gmso/lib/potential_templates.py | 4 +--
gmso/tests/base_test.py | 6 ++--
gmso/tests/test_atom_type.py | 13 +++++--
gmso/tests/test_expression.py | 38 ++++++++++-----------
gmso/tests/test_topology.py | 41 +++++++++++++++-------
gmso/utils/expression.py | 6 ++--
15 files changed, 191 insertions(+), 136 deletions(-)
diff --git a/gmso/abc/abstract_potential.py b/gmso/abc/abstract_potential.py
index 815a48feb..e16d2a883 100644
--- a/gmso/abc/abstract_potential.py
+++ b/gmso/abc/abstract_potential.py
@@ -5,7 +5,7 @@
from pydantic import Field, validator
from gmso.abc.gmso_base import GMSOBase
-from gmso.utils.expression import _PotentialExpression
+from gmso.utils.expression import PotentialExpression
class AbstractPotential(GMSOBase):
@@ -24,8 +24,8 @@ class AbstractPotential(GMSOBase):
"", description="The name of the potential. Defaults to class name"
)
- potential_expression_: _PotentialExpression = Field(
- _PotentialExpression(expression="a*x+b", independent_variables={"x"}),
+ potential_expression_: PotentialExpression = Field(
+ PotentialExpression(expression="a*x+b", independent_variables={"x"}),
description="The mathematical expression for the potential",
)
@@ -48,7 +48,7 @@ def __init__(
if independent_variables is None:
independent_variables = {"x"}
- potential_expression = _PotentialExpression(
+ potential_expression = PotentialExpression(
expression=expression,
independent_variables=independent_variables,
parameters=None,
@@ -118,7 +118,7 @@ def pop_tag(self, tag: str) -> Any:
@validator("potential_expression_", pre=True)
def validate_potential_expression(cls, v):
if isinstance(v, dict):
- v = _PotentialExpression(**v)
+ v = PotentialExpression(**v)
return v
@abstractmethod
diff --git a/gmso/core/angle_type.py b/gmso/core/angle_type.py
index d9b0a6d38..4c8d62e7f 100644
--- a/gmso/core/angle_type.py
+++ b/gmso/core/angle_type.py
@@ -5,6 +5,7 @@
from gmso.core.parametric_potential import ParametricPotential
from gmso.utils._constants import ANGLE_TYPE_DICT
+from gmso.utils.expression import PotentialExpression
class AngleType(ParametricPotential):
@@ -48,17 +49,6 @@ def __init__(
topology=None,
tags=None,
):
- if potential_expression is None:
- if expression is None:
- expression = "0.5 * k * (theta-theta_eq)**2"
-
- if parameters is None:
- parameters = {
- "k": 1000 * u.Unit("kJ / (deg**2)"),
- "theta_eq": 180 * u.deg,
- }
- if independent_variables is None:
- independent_variables = {"theta"}
super(AngleType, self).__init__(
name=name,
@@ -73,6 +63,17 @@ def __init__(
tags=tags,
)
+ @staticmethod
+ def _default_potential_expr():
+ return PotentialExpression(
+ expression="0.5 * k * (theta-theta_eq)**2",
+ parameters={
+ "k": 1000 * u.Unit("kJ / (deg**2)"),
+ "theta_eq": 180 * u.deg,
+ },
+ independent_variables={"theta"},
+ )
+
@property
def member_types(self):
return self.__dict__.get("member_types_")
diff --git a/gmso/core/atom_type.py b/gmso/core/atom_type.py
index bb370b126..0c20dd4e1 100644
--- a/gmso/core/atom_type.py
+++ b/gmso/core/atom_type.py
@@ -7,6 +7,7 @@
from gmso.core.parametric_potential import ParametricPotential
from gmso.utils._constants import ATOM_TYPE_DICT, UNIT_WARNING_STRING
+from gmso.utils.expression import PotentialExpression
from gmso.utils.misc import ensure_valid_dimensions, unyt_to_hashable
@@ -70,19 +71,6 @@ def __init__(
tags=None,
topology=None,
):
- if potential_expression is None:
- if expression is None:
- expression = "4*epsilon*((sigma/r)**12 - (sigma/r)**6)"
-
- if parameters is None:
- parameters = {
- "sigma": 0.3 * u.nm,
- "epsilon": 0.3 * u.Unit("kJ"),
- }
-
- if independent_variables is None:
- independent_variables = {"r"}
-
if overrides is None:
overrides = set()
@@ -187,6 +175,17 @@ def validate_charge(cls, charge):
return charge
+ @staticmethod
+ def _default_potential_expr():
+ return PotentialExpression(
+ expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
+ independent_variables={"r"},
+ parameters={
+ "sigma": 0.3 * u.nm,
+ "epsilon": 0.3 * u.Unit("kJ"),
+ },
+ )
+
class Config:
"""Pydantic configuration of the attributes for an atom_type."""
diff --git a/gmso/core/bond_type.py b/gmso/core/bond_type.py
index 3744ecf08..6a50219fe 100644
--- a/gmso/core/bond_type.py
+++ b/gmso/core/bond_type.py
@@ -6,6 +6,7 @@
from gmso.core.parametric_potential import ParametricPotential
from gmso.utils._constants import BOND_TYPE_DICT
+from gmso.utils.expression import PotentialExpression
class BondType(ParametricPotential):
@@ -49,18 +50,6 @@ def __init__(
topology=None,
tags=None,
):
- if potential_expression is None:
- if expression is None:
- expression = "0.5 * k * (r-r_eq)**2"
-
- if parameters is None:
- parameters = {
- "k": 1000 * u.Unit("kJ / (nm**2)"),
- "r_eq": 0.14 * u.nm,
- }
- if independent_variables is None:
- independent_variables = {"r"}
-
super(BondType, self).__init__(
name=name,
expression=expression,
@@ -83,6 +72,17 @@ def member_types(self):
def member_classes(self):
return self.__dict__.get("member_classes_")
+ @staticmethod
+ def _default_potential_expr():
+ return PotentialExpression(
+ expression="0.5 * k * (r-r_eq)**2",
+ independent_variables={"r"},
+ parameters={
+ "k": 1000 * u.Unit("kJ / (nm**2)"),
+ "r_eq": 0.14 * u.nm,
+ },
+ )
+
class Config:
"""Pydantic configuration for class attributes."""
diff --git a/gmso/core/dihedral_type.py b/gmso/core/dihedral_type.py
index e7ce745ba..383dae755 100644
--- a/gmso/core/dihedral_type.py
+++ b/gmso/core/dihedral_type.py
@@ -5,6 +5,7 @@
from gmso.core.parametric_potential import ParametricPotential
from gmso.utils._constants import DIHEDRAL_TYPE_DICT
+from gmso.utils.expression import PotentialExpression
class DihedralType(ParametricPotential):
@@ -54,18 +55,6 @@ def __init__(
topology=None,
tags=None,
):
- if potential_expression is None:
- if expression is None:
- expression = "k * (1 + cos(n * phi - phi_eq))**2"
-
- if parameters is None:
- parameters = {
- "k": 1000 * u.Unit("kJ / (deg**2)"),
- "phi_eq": 180 * u.deg,
- "n": 1 * u.dimensionless,
- }
- if independent_variables is None:
- independent_variables = {"phi"}
super(DihedralType, self).__init__(
name=name,
@@ -88,6 +77,18 @@ def member_types(self):
def member_classes(self):
return self.__dict__.get("member_classes_")
+ @staticmethod
+ def _default_potential_expr():
+ return PotentialExpression(
+ expression="k * (1 + cos(n * phi - phi_eq))**2",
+ parameters={
+ "k": 1000 * u.Unit("kJ / (deg**2)"),
+ "phi_eq": 180 * u.deg,
+ "n": 1 * u.dimensionless,
+ },
+ independent_variables={"phi"},
+ )
+
class Config:
fields = {
"member_types_": "member_types",
diff --git a/gmso/core/improper_type.py b/gmso/core/improper_type.py
index daadb7bfa..f15b0a057 100644
--- a/gmso/core/improper_type.py
+++ b/gmso/core/improper_type.py
@@ -6,6 +6,7 @@
from gmso.core.parametric_potential import ParametricPotential
from gmso.utils._constants import IMPROPER_TYPE_DICT
+from gmso.utils.expression import PotentialExpression
class ImproperType(ParametricPotential):
@@ -60,19 +61,6 @@ def __init__(
topology=None,
tags=None,
):
- if potential_expression is None:
- if expression is None:
- expression = "0.5 * k * ((phi - phi_eq))**2"
-
- if parameters is None:
- parameters = {
- "k": 1000 * u.Unit("kJ / (deg**2)"),
- "phi_eq": 0 * u.deg,
- }
-
- if independent_variables is None:
- independent_variables = {"phi"}
-
super(ImproperType, self).__init__(
name=name,
expression=expression,
@@ -95,6 +83,17 @@ def member_types(self):
def member_classes(self):
return self.__dict__.get("member_classes_")
+ @staticmethod
+ def _default_potential_expr():
+ return PotentialExpression(
+ expression="0.5 * k * ((phi - phi_eq))**2",
+ parameters={
+ "k": 1000 * u.Unit("kJ / (deg**2)"),
+ "phi_eq": 180 * u.deg,
+ },
+ independent_variables={"phi"},
+ )
+
class Config:
"""Pydantic configuration for attributes."""
diff --git a/gmso/core/pairpotential_type.py b/gmso/core/pairpotential_type.py
index 7353491eb..69b56418b 100644
--- a/gmso/core/pairpotential_type.py
+++ b/gmso/core/pairpotential_type.py
@@ -5,6 +5,7 @@
from gmso.core.parametric_potential import ParametricPotential
from gmso.utils._constants import PAIRPOTENTIAL_TYPE_DICT
+from gmso.utils.expression import PotentialExpression
class PairPotentialType(ParametricPotential):
@@ -43,14 +44,6 @@ def __init__(
topology=None,
tags=None,
):
- if potential_expression is None:
- if expression is None:
- expression = "4 * eps * ((sigma / r)**12 - (sigma / r)**6)"
- if parameters is None:
- parameters = {"eps": 1 * u.Unit("kJ / mol"), "sigma": 1 * u.nm}
- if independent_variables is None:
- independent_variables = {"r"}
-
super(PairPotentialType, self).__init__(
name=name,
expression=expression,
@@ -67,6 +60,14 @@ def __init__(
def member_types(self):
return self.__dict__.get("member_types_")
+ @staticmethod
+ def _default_potential_expr():
+ return PotentialExpression(
+ expression="4 * eps * ((sigma / r)**12 - (sigma / r)**6)",
+ independent_variables={"r"},
+ parameters={"eps": 1 * u.Unit("kJ / mol"), "sigma": 1 * u.nm},
+ )
+
class Config:
fields = {"member_types_": "member_types"}
diff --git a/gmso/core/parametric_potential.py b/gmso/core/parametric_potential.py
index d9a63e7a4..18d01cb89 100644
--- a/gmso/core/parametric_potential.py
+++ b/gmso/core/parametric_potential.py
@@ -6,7 +6,7 @@
from gmso.abc.abstract_potential import AbstractPotential
from gmso.exceptions import GMSOError
from gmso.utils.decorators import confirm_dict_existence
-from gmso.utils.expression import _PotentialExpression
+from gmso.utils.expression import PotentialExpression
class ParametricPotential(AbstractPotential):
@@ -36,7 +36,7 @@ class ParametricPotential(AbstractPotential):
def __init__(
self,
name="ParametricPotential",
- expression="a*x+b",
+ expression=None,
parameters=None,
potential_expression=None,
independent_variables=None,
@@ -53,23 +53,10 @@ def __init__(
"please do not provide arguments for "
"expression, independent_variables or parameters."
)
- if potential_expression is None:
- if expression is None:
- expression = "a*x+b"
-
- if parameters is None:
- parameters = {
- "a": 1.0 * u.dimensionless,
- "b": 1.0 * u.dimensionless,
- }
- if independent_variables is None:
- independent_variables = {"x"}
-
- _potential_expression = _PotentialExpression(
- expression=expression,
- independent_variables=independent_variables,
- parameters=parameters,
+ if potential_expression is None:
+ _potential_expression = self._get_expression(
+ expression, parameters, independent_variables
)
else:
_potential_expression = potential_expression
@@ -81,6 +68,39 @@ def __init__(
**kwargs,
)
+ def _get_expression(self, expression, parameters, indep_vars):
+ args = (expression, parameters, indep_vars)
+ all_provided = tuple(1 if param is not None else 0 for param in args)
+
+ if sum(all_provided) == 0:
+ return self._default_potential_expr()
+ elif sum(all_provided) < 3:
+ raise ValueError(
+ "When using keyword arguments `expression`, "
+ "`independent_variables` and `parameters` for "
+ "a potential, you are expected to provide all the values "
+ "or none of them to use defaults. However, you provided "
+ "the following and there's not enough information to form "
+ "a set of expression, idependent_variables and parameters.\n"
+ f"expression: {expression}\n"
+ f"parameters: {parameters}\n"
+ f"independent_variables: {indep_vars}\n"
+ )
+ else:
+ return PotentialExpression(
+ expression=expression,
+ independent_variables=indep_vars,
+ parameters=parameters,
+ )
+
+ @staticmethod
+ def _default_potential_expr():
+ return PotentialExpression(
+ expression="a*x+b",
+ parameters={"a": 1.0 * u.dimensionless, "b": 1.0 * u.dimensionless},
+ independent_variables={"x"},
+ )
+
@property
def parameters(self):
"""Optional[dict]\n\tThe parameters of the `Potential` expression and their corresponding values, as `unyt` quantities"""
diff --git a/gmso/external/convert_parmed.py b/gmso/external/convert_parmed.py
index b1825455a..0a70ea374 100644
--- a/gmso/external/convert_parmed.py
+++ b/gmso/external/convert_parmed.py
@@ -254,10 +254,12 @@ def _atom_types_from_pmd(structure):
top_atomtype = gmso.AtomType(
name=atom_type.name,
charge=atom_type.charge * u.elementary_charge,
+ expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
parameters={
"sigma": atom_type.sigma * u.angstrom,
"epsilon": atom_type.epsilon * u.Unit("kcal / mol"),
},
+ independent_variables={"r"},
mass=atom_type.mass,
)
pmd_top_atomtypes[atom_type] = top_atomtype
@@ -294,10 +296,12 @@ def _bond_types_from_pmd(structure, bond_types_members_map=None):
"k": (2 * btype.k * u.Unit("kcal / (angstrom**2 * mol)")),
"r_eq": btype.req * u.angstrom,
}
+ expr = gmso.BondType._default_potential_expr()
+ expr.set(parameters=bond_params)
member_types = bond_types_members_map.get(id(btype))
top_bondtype = gmso.BondType(
- parameters=bond_params, member_types=member_types
+ potential_expression=expr, member_types=member_types
)
pmd_top_bondtypes[btype] = top_bondtype
return pmd_top_bondtypes
@@ -334,13 +338,15 @@ def _angle_types_from_pmd(structure, angle_types_member_map=None):
"k": (2 * angletype.k * u.Unit("kcal / (rad**2 * mol)")),
"theta_eq": (angletype.theteq * u.degree),
}
+ expr = gmso.AngleType._default_potential_expr()
+ expr.parameters = angle_params
# Do we need to worry about Urey Bradley terms
# For Urey Bradley:
# k in (kcal/(angstrom**2 * mol))
# r_eq in angstrom
member_types = angle_types_member_map.get(id(angletype))
top_angletype = gmso.AngleType(
- parameters=angle_params, member_types=member_types
+ potential_expression=expr, member_types=member_types
)
pmd_top_angletypes[angletype] = top_angletype
return pmd_top_angletypes
@@ -378,9 +384,11 @@ def _dihedral_types_from_pmd(structure, dihedral_types_member_map=None):
"phi_eq": (dihedraltype.phase * u.degree),
"n": dihedraltype.per * u.dimensionless,
}
+ expr = gmso.DihedralType._default_potential_expr()
+ expr.parameters = dihedral_params
member_types = dihedral_types_member_map.get(id(dihedraltype))
top_dihedraltype = gmso.DihedralType(
- parameters=dihedral_params, member_types=member_types
+ potential_expression=expr, member_types=member_types
)
pmd_top_dihedraltypes[dihedraltype] = top_dihedraltype
diff --git a/gmso/lib/potential_templates.py b/gmso/lib/potential_templates.py
index ad3f30d19..c7fdc98cc 100644
--- a/gmso/lib/potential_templates.py
+++ b/gmso/lib/potential_templates.py
@@ -4,7 +4,7 @@
from gmso.abc.abstract_potential import AbstractPotential
from gmso.exceptions import GMSOError
-from gmso.utils.expression import _PotentialExpression
+from gmso.utils.expression import PotentialExpression
from gmso.utils.singleton import Singleton
POTENTIAL_JSONS = list(Path(__file__).parent.glob("jsons/*.json"))
@@ -51,7 +51,7 @@ def __init__(
independent_variables = set(independent_variables.split(","))
if potential_expression is None:
- _potential_expression = _PotentialExpression(
+ _potential_expression = PotentialExpression(
expression=expression,
independent_variables=independent_variables,
)
diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py
index 52487e873..3db264a96 100644
--- a/gmso/tests/base_test.py
+++ b/gmso/tests/base_test.py
@@ -466,8 +466,10 @@ def test_topology_equivalence(top1, top2):
@pytest.fixture(scope="session")
def pairpotentialtype_top(self):
top = Topology()
- atype1 = AtomType(name="a1", expression="sigma + epsilon*r")
- atype2 = AtomType(name="a2", expression="sigma * epsilon*r")
+ atype1 = AtomType(name="a1")
+ atype1.expression = "sigma + epsilon*r"
+ atype2 = AtomType(name="a2")
+ atype2.expression = "sigma * epsilon * r"
atom1 = Atom(name="a", atom_type=atype1)
atom2 = Atom(name="b", atom_type=atype2)
top.add_site(atom1)
diff --git a/gmso/tests/test_atom_type.py b/gmso/tests/test_atom_type.py
index b89b686f4..81310c6fc 100644
--- a/gmso/tests/test_atom_type.py
+++ b/gmso/tests/test_atom_type.py
@@ -27,6 +27,7 @@ def test_new_atom_type(self, charge, mass):
name="mytype",
charge=charge,
mass=mass,
+ expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
parameters={
"sigma": 1 * u.nm,
"epsilon": 10 * u.Unit("kcal / mol"),
@@ -98,39 +99,46 @@ def test_equivalance(self, charge):
first_type = AtomType(
name="mytype",
charge=charge,
+ expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
parameters={"sigma": 1 * u.m, "epsilon": 10 * u.m},
+ independent_variables={"r"},
)
same_type = AtomType(
name="mytype",
charge=charge,
+ expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
parameters={"sigma": 1 * u.m, "epsilon": 10 * u.m},
+ independent_variables={"r"},
)
different_name = AtomType(
name="difftype",
charge=charge,
- parameters={"sigma": 1 * u.m, "epsilon": 10 * u.m},
)
different_charge = AtomType(
name="mytype",
charge=4.0 * charge,
- parameters={"sigma": 1 * u.m, "epsilon": 10 * u.m},
)
different_function = AtomType(
name="mytype",
charge=charge,
parameters={"sigma": 1 * u.m, "epsilon": 10 * u.m},
expression="r * sigma * epsilon",
+ independent_variables={"r"},
)
different_params = AtomType(
name="mytype",
charge=charge,
+ expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
parameters={"sigma": 42 * u.m, "epsilon": 100000 * u.m},
+ independent_variables={"r"},
)
different_mass = AtomType(
name="mytype",
charge=charge,
mass=5 * u.kg / u.mol,
parameters={"sigma": 1 * u.m, "epsilon": 10 * u.m},
+ expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
+ independent_variables={"r"},
)
assert first_type == same_type
@@ -145,6 +153,7 @@ def test_set_nb_func(self, charge):
first_type = AtomType(
name="mytype",
charge=charge,
+ expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
parameters={"sigma": 1 * u.m, "epsilon": 10 * u.m},
independent_variables="r",
)
diff --git a/gmso/tests/test_expression.py b/gmso/tests/test_expression.py
index 5e9d67b05..8d064c367 100644
--- a/gmso/tests/test_expression.py
+++ b/gmso/tests/test_expression.py
@@ -3,12 +3,12 @@
import unyt as u
from gmso.tests.base_test import BaseTest
-from gmso.utils.expression import _PotentialExpression
+from gmso.utils.expression import PotentialExpression
class TestExpression(BaseTest):
def test_expression(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
expression="a*x+b",
independent_variables="x",
parameters={"a": 1.0 * u.dimensionless, "b": 2.0 * u.dimensionless},
@@ -21,7 +21,7 @@ def test_expression(self):
assert expression.parameters["b"] == 2.0 * u.dimensionless
def test_expression_multiple_indep_vars(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
expression="a^2+2*a*b+b^2+2*theta*phi",
independent_variables={"theta", "phi"},
parameters={"a": 2.0 * u.nm, "b": 2.0 * u.rad},
@@ -35,7 +35,7 @@ def test_expression_multiple_indep_vars(self):
def test_invalid_expression(self):
with pytest.raises(ValueError) as e:
- expression = _PotentialExpression(
+ expression = PotentialExpression(
expression="a*x+b",
independent_variables="x",
parameters={"sigma": 1.0 * u.nm, "phi": 1.0 * u.rad},
@@ -47,7 +47,7 @@ def test_invalid_expression(self):
def test_invalid_indep_vars(self):
with pytest.raises(ValueError) as e:
- expression = _PotentialExpression(
+ expression = PotentialExpression(
expression="a*x+b", independent_variables="j", parameters=None
)
assert (
@@ -57,7 +57,7 @@ def test_invalid_indep_vars(self):
)
def test_non_parametric_expression(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
expression="a^2+2*a*b+b^2",
independent_variables="a",
parameters=None,
@@ -71,7 +71,7 @@ def test_non_parametric_expression(self):
)
def test_set_indep_variables(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
expression="a^2+2*a*b+b^2",
independent_variables="a",
parameters=None,
@@ -81,7 +81,7 @@ def test_set_indep_variables(self):
assert sympy.Symbol("a") not in expression.independent_variables
def test_set_indep_variables_invalid(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
expression="a^2+2*a*b+b^2",
independent_variables="a",
parameters=None,
@@ -93,7 +93,7 @@ def test_set_indep_variables_invalid(self):
assert expression.independent_variables == {sympy.Symbol("a")}
def test_set_expression(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
"a^x + b^y + c^z",
independent_variables={"x", "y", "z"},
parameters={"a": 2.6 * u.nm, "b": 2.7 * u.nm, "c": 22.8 * u.hertz},
@@ -102,7 +102,7 @@ def test_set_expression(self):
assert sympy.Symbol("x") in expression.independent_variables
def test_set_expression_invalid(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
"a^x + b^y + c^z",
independent_variables={"x", "y", "z"},
parameters={"a": 2.6 * u.nm, "b": 2.7 * u.nm, "c": 22.8 * u.hertz},
@@ -113,7 +113,7 @@ def test_set_expression_invalid(self):
assert sympy.sympify("a^x + b^y + c^z") == expression.expression
def test_set_parameters(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
"a^x + b^y + c^z",
independent_variables={"x", "y", "z"},
parameters={"a": 2.6 * u.nm, "b": 2.7 * u.nm, "c": 22.8 * u.hertz},
@@ -130,7 +130,7 @@ def test_set_parameters(self):
)
def test_set_parameters_extra(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
"a^x + b^y + c^z",
independent_variables={"x", "y", "z"},
parameters={"a": 2.6 * u.nm, "b": 2.7 * u.nm, "c": 22.8 * u.hertz},
@@ -151,7 +151,7 @@ def test_set_parameters_extra(self):
assert "d" not in expression.parameters
def test_set_parameters_invalid(self):
- expression = _PotentialExpression(
+ expression = PotentialExpression(
"a^x + b^y + c^z",
independent_variables={"x", "y", "z"},
parameters={"a": 2.6 * u.nm, "b": 2.7 * u.nm, "c": 22.8 * u.hertz},
@@ -171,15 +171,15 @@ def test_set_parameters_invalid(self):
assert "l" not in expression.parameters
def test_expression_equality(self):
- expression_1 = _PotentialExpression(
+ expression_1 = PotentialExpression(
expression="exp(2)+exp(4)+2*phi", independent_variables={"phi"}
)
- expression_2 = _PotentialExpression(
+ expression_2 = PotentialExpression(
expression="exp(4) + exp(2) + phi*2", independent_variables={"phi"}
)
- expression_3 = _PotentialExpression(
+ expression_3 = PotentialExpression(
expression="exp(4) + exp(2) + phi * 8",
independent_variables={"phi"},
)
@@ -190,19 +190,19 @@ def test_expression_equality(self):
assert expression_1 != expression_3
def test_parametric_equality(self):
- expression_1 = _PotentialExpression(
+ expression_1 = PotentialExpression(
expression="e^2+e^4+2*phi",
independent_variables={"phi"},
parameters={"e": 2.2400 * u.dimensionless},
)
- expression_2 = _PotentialExpression(
+ expression_2 = PotentialExpression(
expression="e^4 + e^2 + phi*2",
independent_variables={"phi"},
parameters={"e": 2.2400 * u.dimensionless},
)
- expression_3 = _PotentialExpression(
+ expression_3 = PotentialExpression(
expression="e^4 + e^2 + phi * 8",
independent_variables={"phi"},
parameters={"e": 2.2400 * u.dimensionless},
diff --git a/gmso/tests/test_topology.py b/gmso/tests/test_topology.py
index 9feefbc99..ceeb560e7 100644
--- a/gmso/tests/test_topology.py
+++ b/gmso/tests/test_topology.py
@@ -118,8 +118,12 @@ def test_eq_sites(self, top, charge):
ref = deepcopy(top)
wrong_atom_type = deepcopy(top)
- ref.add_site(Atom(atom_type=AtomType(expression="epsilon*sigma*r")))
- wrong_atom_type.add_site(Atom(atom_type=AtomType(expression="sigma*r")))
+ at1 = AtomType()
+ at1.expression = "epsilon*sigma*r"
+ at2 = AtomType()
+ at2.expression = "sigma*r"
+ ref.add_site(Atom(atom_type=at1))
+ wrong_atom_type.add_site(Atom(atom_type=at2))
assert ref != wrong_atom_type
@pytest.mark.skipif(not has_parmed, reason="ParmEd is not installed")
@@ -263,7 +267,8 @@ def test_top_update(self):
assert len(top.connection_types) == 1
assert len(top.connection_type_expressions) == 1
- atom1.atom_type = AtomType(expression="sigma*epsilon*r")
+ atom1.atom_type = AtomType()
+ atom1.atom_type.expression = "sigma*epsilon*r"
assert top.n_sites == 2
assert len(top.atom_types) == 1
assert len(top.atom_type_expressions) == 1
@@ -285,8 +290,10 @@ def test_atomtype_update(self):
assert top.n_bonds == 0
assert top.n_connections == 0
- atype1 = AtomType(expression="sigma + epsilon*r")
- atype2 = AtomType(expression="sigma * epsilon*r")
+ atype1 = AtomType()
+ atype1.expression = "sigma + epsilon*r"
+ atype2 = AtomType()
+ atype2.expression = "sigma * epsilon*r"
atom1 = Atom(name="a", atom_type=atype1)
atom2 = Atom(name="b", atom_type=atype2)
top.add_site(atom1)
@@ -299,8 +306,10 @@ def test_atomtype_update(self):
def test_bond_bondtype_update(self):
top = Topology()
- atype1 = AtomType(expression="sigma + epsilon*r")
- atype2 = AtomType(expression="sigma * epsilon*r")
+ atype1 = AtomType()
+ atype1.expression = "sigma + epsilon*r"
+ atype2 = AtomType()
+ atype2.expression = "sigma * epsilon*r"
atom1 = Atom(name="a", atom_type=atype1)
atom2 = Atom(name="b", atom_type=atype2)
btype = BondType()
@@ -316,8 +325,10 @@ def test_bond_bondtype_update(self):
def test_angle_angletype_update(self):
top = Topology()
- atype1 = AtomType(expression="sigma + epsilon*r")
- atype2 = AtomType(expression="sigma * epsilon*r")
+ atype1 = AtomType()
+ atype1.expression = "sigma + epsilon*r"
+ atype2 = AtomType()
+ atype2.expression = "sigma * epsilon*r"
atom1 = Atom(name="a", atom_type=atype1)
atom2 = Atom(name="b", atom_type=atype2)
atom3 = Atom(name="c", atom_type=atype2)
@@ -340,8 +351,10 @@ def test_angle_angletype_update(self):
def test_dihedral_dihedraltype_update(self):
top = Topology()
- atype1 = AtomType(expression="sigma + epsilon*r")
- atype2 = AtomType(expression="sigma * epsilon*r")
+ atype1 = AtomType()
+ atype1.expression = "sigma + epsilon*r"
+ atype2 = AtomType()
+ atype2.expression = "sigma * epsilon*r"
atom1 = Atom(name="a", atom_type=atype1)
atom2 = Atom(name="b", atom_type=atype2)
atom3 = Atom(name="c", atom_type=atype2)
@@ -364,8 +377,10 @@ def test_dihedral_dihedraltype_update(self):
def test_improper_impropertype_update(self):
top = Topology()
- atype1 = AtomType(expression="sigma + epsilon*r")
- atype2 = AtomType(expression="sigma * epsilon*r")
+ atype1 = AtomType()
+ atype1.expression = "sigma + epsilon*r"
+ atype2 = AtomType()
+ atype2.expression = "sigma * epsilon*r"
atom1 = Atom(name="a", atom_type=atype1)
atom2 = Atom(name="b", atom_type=atype2)
atom3 = Atom(name="c", atom_type=atype2)
diff --git a/gmso/utils/expression.py b/gmso/utils/expression.py
index dcc73264b..629d07633 100644
--- a/gmso/utils/expression.py
+++ b/gmso/utils/expression.py
@@ -7,11 +7,11 @@
from gmso.utils.decorators import register_pydantic_json
from gmso.utils.misc import unyt_to_hashable
-__all__ = ["_PotentialExpression"]
+__all__ = ["PotentialExpression"]
@register_pydantic_json(method="json")
-class _PotentialExpression:
+class PotentialExpression:
"""A general Expression class with parameters.
This class is used by `gmso.core.potential.Potential` class and its
@@ -272,7 +272,7 @@ def _validate_independent_variables(indep_vars):
@staticmethod
def json(potential_expression):
"""Convert the provided potential expression to a json serializable dictionary."""
- if not isinstance(potential_expression, _PotentialExpression):
+ if not isinstance(potential_expression, PotentialExpression):
raise TypeError(
f"{potential_expression} is not of type _PotentialExpression"
)
From 2cc8da77727f4504ba9cfbb85c01dfa2946a95b3 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Mon, 28 Feb 2022 10:08:07 -0600
Subject: [PATCH 039/141] fix typo when converting foyer XML (#636)
* fix typo when converting foyer XML
* add unit tests to confirm combining rule
---
gmso/external/convert_foyer_xml.py | 4 ++--
gmso/tests/files/foyer-trappe-ua.xml | 18 ++++++++++++++++++
gmso/tests/test_convert_foyer_xml.py | 11 +++++++++++
3 files changed, 31 insertions(+), 2 deletions(-)
create mode 100644 gmso/tests/files/foyer-trappe-ua.xml
diff --git a/gmso/external/convert_foyer_xml.py b/gmso/external/convert_foyer_xml.py
index fa31c52df..5ce665668 100644
--- a/gmso/external/convert_foyer_xml.py
+++ b/gmso/external/convert_foyer_xml.py
@@ -146,9 +146,9 @@ def _write_gmso_xml(gmso_xml, **kwargs):
ffMeta = _create_sub_element(forcefield, "FFMetaData")
if kwargs.get("combining_rule"):
- ffMeta.attrib["combining_rule"] = kwargs.get("combining_rule")
+ ffMeta.attrib["combiningRule"] = kwargs.get("combining_rule")
else:
- ffMeta.attrib["combining_rule"] = "geometric"
+ ffMeta.attrib["combiningRule"] = "geometric"
if kwargs["coulomb14scale"]:
ffMeta.attrib["electrostatics14Scale"] = kwargs["coulomb14scale"]
diff --git a/gmso/tests/files/foyer-trappe-ua.xml b/gmso/tests/files/foyer-trappe-ua.xml
new file mode 100644
index 000000000..8759a064e
--- /dev/null
+++ b/gmso/tests/files/foyer-trappe-ua.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/tests/test_convert_foyer_xml.py b/gmso/tests/test_convert_foyer_xml.py
index 2f7d593fb..4317794b7 100644
--- a/gmso/tests/test_convert_foyer_xml.py
+++ b/gmso/tests/test_convert_foyer_xml.py
@@ -4,6 +4,7 @@
import unyt as u
from sympy import sympify
+from gmso.core.forcefield import ForceField
from gmso.exceptions import ForceFieldParseError
from gmso.external.convert_foyer_xml import from_foyer_xml
from gmso.tests.base_test import BaseTest
@@ -54,6 +55,16 @@ def test_foyer_file_not_found(self):
def test_foyer_version(self, foyer_fullerene):
assert foyer_fullerene.version == "0.0.1"
+ def test_foyer_combining_rule(self):
+ from_foyer_xml(get_path("foyer-trappe-ua.xml"))
+ loaded = ForceField("foyer-trappe-ua_gmso.xml")
+
+ assert loaded.name == "Trappe-UA"
+ assert loaded.version == "0.0.2"
+ assert loaded.combining_rule == "lorentz"
+ assert loaded.scaling_factors["electrostatics14Scale"] == 0
+ assert loaded.scaling_factors["nonBonded14Scale"] == 0
+
def test_foyer_14scale(self, foyer_fullerene):
assert foyer_fullerene.scaling_factors["electrostatics14Scale"] == 1.0
assert foyer_fullerene.scaling_factors["nonBonded14Scale"] == 1.0
From c6b7161667da06fb457cb9771fdedbb2d3a80bff Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Wed, 2 Mar 2022 14:36:23 -0600
Subject: [PATCH 040/141] Update parmed converter to use new residue
infrastructure (#613)
* populate site residue name and residue number from parmed
* populate pmd residue info from site residue name and residue number
* Modify test to correctly test the new changes
* Update tests in test convert parmed
* fix typo
---
gmso/external/convert_parmed.py | 32 ++++++++++++-------------------
gmso/tests/test_convert_parmed.py | 32 ++++++++++++++++++++++++-------
2 files changed, 37 insertions(+), 27 deletions(-)
diff --git a/gmso/external/convert_parmed.py b/gmso/external/convert_parmed.py
index 0a70ea374..579d560db 100644
--- a/gmso/external/convert_parmed.py
+++ b/gmso/external/convert_parmed.py
@@ -90,6 +90,8 @@ def from_parmed(structure, refer_type=True):
[atom.xx, atom.xy, atom.xz] * u.angstrom
).in_units(u.nm),
atom_type=pmd_top_atomtypes[atom.atom_type],
+ residue_name=residue.name,
+ residue_number=residue.idx,
)
else:
site = gmso.Atom(
@@ -99,6 +101,8 @@ def from_parmed(structure, refer_type=True):
[atom.xx, atom.xy, atom.xz] * u.angstrom
).in_units(u.nm),
atom_type=None,
+ residue_name=residue.name,
+ residue_number=residue.idx,
)
site_map[atom] = site
subtops[-1].add_site(site)
@@ -460,43 +464,31 @@ def to_parmed(top, refer_type=True):
dihedral_map = dict() # Map top's dihedral to structure's dihedral
# Set up unparametrized system
- # Build subtop_map (site -> top)
- default_residue = pmd.Residue("RES")
- for subtop in top.subtops:
- for site in subtop.sites:
- subtop_map[site] = subtop
-
# Build up atom
for site in top.sites:
- if site in subtop_map:
- residue = subtop_map[site].name
- residue_name = residue[: residue.find("[")]
- residue_idx = int(
- residue[residue.find("[") + 1 : residue.find("]")]
- )
- # since subtop contains information needed to build residue
- else:
- residue = default_residue
- # Check element
if site.element:
atomic_number = site.element.atomic_number
charge = site.element.charge
else:
atomic_number = 0
charge = 0
-
pmd_atom = pmd.Atom(
atomic_number=atomic_number,
name=site.name,
- mass=site.mass,
- charge=site.charge,
+ mass=site.mass.to(u.amu).value,
+ charge=site.charge.to(u.elementary_charge).value,
)
pmd_atom.xx, pmd_atom.xy, pmd_atom.xz = site.position.to(
"angstrom"
).value
# Add atom to structure
- structure.add_atom(pmd_atom, resname=residue_name, resnum=residue_idx)
+ if site.residue_name:
+ structure.add_atom(
+ pmd_atom, resname=site.residue_name, resnum=site.residue_number
+ )
+ else:
+ structure.add_atom(pmd_atom, resname="RES", resnum=-1)
atom_map[site] = pmd_atom
# "Claim" all of the item it contains and subsequently index all of its item
diff --git a/gmso/tests/test_convert_parmed.py b/gmso/tests/test_convert_parmed.py
index 423157345..e367da890 100644
--- a/gmso/tests/test_convert_parmed.py
+++ b/gmso/tests/test_convert_parmed.py
@@ -263,16 +263,34 @@ def test_residues_info(self, parmed_hexane_box):
top_from_struc = from_parmed(struc)
assert len(top_from_struc.subtops) == len(struc.residues)
- for i in range(len(top_from_struc.subtops)):
- assert len(top_from_struc.subtops[i].sites) == 20
- assert top_from_struc.subtops[i].name == "HEX[{}]".format(i)
+
+ for site in top_from_struc.sites:
+ assert site.residue_name == "HEX"
+ assert site.residue_number in list(range(6))
struc_from_top = to_parmed(top_from_struc)
assert len(struc_from_top.residues) == len(struc.residues)
- for i in range(len(top_from_struc.subtops)):
- assert len(struc_from_top.residues[i].atoms) == 20
- assert struc_from_top.residues[i].name == "HEX"
- assert struc_from_top.residues[i].idx == i
+
+ for residue_og, residue_cp in zip(
+ struc.residues, struc_from_top.residues
+ ):
+ assert residue_og.name == residue_cp.name
+ assert residue_og.number == residue_cp.number
+ assert len(residue_og.atoms) == len(residue_cp.atoms)
+
+ def test_default_residue_info(selfself, parmed_hexane_box):
+ struc = parmed_hexane_box
+ top_from_struc = from_parmed(struc)
+ assert len(top_from_struc.subtops) == len(struc.residues)
+
+ for site in top_from_struc.sites:
+ site.residue_name = None
+ site.residue_number = None
+
+ struc_from_top = to_parmed(top_from_struc)
+ assert len(struc_from_top.residues) == 1
+ assert struc_from_top.residues[0].name == "RES"
+ assert len(struc_from_top.atoms) == len(struc.atoms)
def test_box_info(self, parmed_hexane_box):
struc = parmed_hexane_box
From 13ef01d47f51a7e309548c574508332edc3ff483 Mon Sep 17 00:00:00 2001
From: Co Quach
Date: Thu, 3 Mar 2022 11:23:53 -0600
Subject: [PATCH 041/141] Bump to version 0.8.0
---
docs/conf.py | 4 ++--
gmso/__init__.py | 2 +-
setup.cfg | 2 +-
setup.py | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index 5a37e1da6..f89de8436 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,8 +23,8 @@
author = "Matt Thompson, Alex Yang, Ray Matsumoto, Parashara Shamaprasad, Umesh Timalsina, Co Quach, Ryan S. DeFever, Justin Gilmer"
# The full version, including alpha/beta/rc tags
-version = "0.7.3"
-release = "0.7.3"
+version = "0.8.0"
+release = "0.8.0"
# -- General configuration ---------------------------------------------------
diff --git a/gmso/__init__.py b/gmso/__init__.py
index 4f0775f7e..9a5985491 100644
--- a/gmso/__init__.py
+++ b/gmso/__init__.py
@@ -16,4 +16,4 @@
from .core.subtopology import SubTopology
from .core.topology import Topology
-__version__ = "0.7.3"
+__version__ = "0.8.0"
diff --git a/setup.cfg b/setup.cfg
index f6987050e..7e22d5eec 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.7.3
+current_version = 0.8.0
commit = True
tag = True
message = Bump to version {new_version}
diff --git a/setup.py b/setup.py
index 75563bb6c..0705ad8a3 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
from setuptools import find_packages, setup
#####################################
-VERSION = "0.7.3"
+VERSION = "0.8.0"
ISRELEASED = False
if ISRELEASED:
__version__ = VERSION
From 0f5429e8e511798289b3fc332212f5936fc993f7 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 28 Mar 2022 16:14:48 -0500
Subject: [PATCH 042/141] [pre-commit.ci] pre-commit autoupdate (#639)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/psf/black: 22.1.0 → 22.3.0](https://github.com/psf/black/compare/22.1.0...22.3.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6c884ea4e..15838aedf 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
- id: trailing-whitespace
exclude: 'setup.cfg'
- repo: https://github.com/psf/black
- rev: 22.1.0
+ rev: 22.3.0
hooks:
- id: black
args: [--line-length=80]
From c7de431d78c285464186babd48f6b8c338685b7e Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Mon, 4 Apr 2022 11:15:33 -0500
Subject: [PATCH 043/141] WIP-Remove hashing from all classes
---
gmso/abc/abstract_potential.py | 8 --------
gmso/core/atom_type.py | 13 -------------
gmso/core/element.py | 15 ---------------
gmso/utils/expression.py | 25 -------------------------
4 files changed, 61 deletions(-)
diff --git a/gmso/abc/abstract_potential.py b/gmso/abc/abstract_potential.py
index e16d2a883..4ab1febee 100644
--- a/gmso/abc/abstract_potential.py
+++ b/gmso/abc/abstract_potential.py
@@ -126,14 +126,6 @@ def set_expression(self):
"""Set the functional form of the expression."""
raise NotImplementedError
- def __eq__(self, other):
- """Compare two potentials for equivalence."""
- return hash(self) == hash(other)
-
- def __hash__(self):
- """Create a unique hash for the potential."""
- return hash(tuple((self.name, self.potential_expression)))
-
def __setattr__(self, key: Any, value: Any) -> None:
"""Set attributes of the potential."""
if key == "expression":
diff --git a/gmso/core/atom_type.py b/gmso/core/atom_type.py
index 0c20dd4e1..8074ce6a6 100644
--- a/gmso/core/atom_type.py
+++ b/gmso/core/atom_type.py
@@ -127,19 +127,6 @@ def definition(self):
"""Return the SMARTS string of the atom_type."""
return self.__dict__.get("definition_")
- def __hash__(self):
- """Return the hash of the atom_type."""
- return hash(
- tuple(
- (
- self.name,
- unyt_to_hashable(self.mass),
- unyt_to_hashable(self.charge),
- self.potential_expression,
- )
- )
- )
-
def __repr__(self):
"""Return a formatted representation of the atom type."""
desc = (
diff --git a/gmso/core/element.py b/gmso/core/element.py
index 12b097b56..ab957b0cf 100644
--- a/gmso/core/element.py
+++ b/gmso/core/element.py
@@ -45,21 +45,6 @@ def __repr__(self):
f"atomic number: {self.atomic_number}, mass: {self.mass.to('amu')}>"
)
- def __eq__(self, other):
- """Return true if the element is equvalent to another element."""
- return hash(self) == hash(other)
-
- def __hash__(self):
- """Generate an unique hash of the element for comparison."""
- return hash(
- (
- self.name,
- self.symbol,
- self.atomic_number,
- unyt_to_hashable(self.mass),
- )
- )
-
class Config:
"""Pydantic configuration for element."""
diff --git a/gmso/utils/expression.py b/gmso/utils/expression.py
index 629d07633..212da212c 100644
--- a/gmso/utils/expression.py
+++ b/gmso/utils/expression.py
@@ -171,31 +171,6 @@ def set(self, expression=None, parameters=None, independent_variables=None):
if sympy.Symbol(key) not in self.expression.free_symbols:
self._parameters.pop(key)
- def __hash__(self):
- """Return hash of the potential expression."""
- if self._is_parametric:
- return hash(
- tuple(
- (
- self.expression,
- tuple(self.independent_variables),
- tuple(self.parameters.keys()),
- tuple(
- unyt_to_hashable(val)
- for val in self.parameters.values()
- ),
- )
- )
- )
- else:
- return hash(
- tuple((self.expression, tuple(self.independent_variables)))
- )
-
- def __eq__(self, other):
- """Determine if two expressions are equivalent."""
- return hash(self) == hash(other)
-
def __repr__(self):
"""Representation of the potential expression."""
descr = list(f"
Date: Mon, 4 Apr 2022 12:55:01 -0500
Subject: [PATCH 044/141] WIP-Draft removed hasing based inefficencies
---
gmso/core/angle_type.py | 4 -
gmso/core/atom_type.py | 24 +++-
gmso/core/bond_type.py | 4 -
gmso/core/dihedral_type.py | 4 -
gmso/core/element.py | 12 ++
gmso/core/improper_type.py | 4 -
gmso/core/pairpotential_type.py | 4 -
gmso/core/parametric_potential.py | 54 ++------
gmso/core/topology.py | 208 +++++++++---------------------
gmso/formats/json.py | 14 +-
gmso/tests/base_test.py | 1 +
gmso/tests/test_atom_type.py | 10 +-
gmso/tests/test_expression.py | 2 -
gmso/tests/test_serialization.py | 4 -
gmso/tests/test_top.py | 4 +-
gmso/tests/test_topology.py | 58 +--------
gmso/utils/decorators.py | 21 ---
gmso/utils/expression.py | 18 +++
gmso/utils/ff_utils.py | 1 -
19 files changed, 141 insertions(+), 310 deletions(-)
diff --git a/gmso/core/angle_type.py b/gmso/core/angle_type.py
index 4c8d62e7f..c19c2ab76 100644
--- a/gmso/core/angle_type.py
+++ b/gmso/core/angle_type.py
@@ -4,7 +4,6 @@
from pydantic import Field
from gmso.core.parametric_potential import ParametricPotential
-from gmso.utils._constants import ANGLE_TYPE_DICT
from gmso.utils.expression import PotentialExpression
@@ -46,7 +45,6 @@ def __init__(
potential_expression=None,
member_types=None,
member_classes=None,
- topology=None,
tags=None,
):
@@ -56,10 +54,8 @@ def __init__(
parameters=parameters,
independent_variables=independent_variables,
potential_expression=potential_expression,
- topology=topology,
member_types=member_types,
member_classes=member_classes,
- set_ref=ANGLE_TYPE_DICT,
tags=tags,
)
diff --git a/gmso/core/atom_type.py b/gmso/core/atom_type.py
index 8074ce6a6..3a01f446b 100644
--- a/gmso/core/atom_type.py
+++ b/gmso/core/atom_type.py
@@ -6,7 +6,7 @@
from pydantic import Field, validator
from gmso.core.parametric_potential import ParametricPotential
-from gmso.utils._constants import ATOM_TYPE_DICT, UNIT_WARNING_STRING
+from gmso.utils._constants import UNIT_WARNING_STRING
from gmso.utils.expression import PotentialExpression
from gmso.utils.misc import ensure_valid_dimensions, unyt_to_hashable
@@ -69,7 +69,6 @@ def __init__(
definition="",
description="",
tags=None,
- topology=None,
):
if overrides is None:
overrides = set()
@@ -80,7 +79,6 @@ def __init__(
parameters=parameters,
independent_variables=independent_variables,
potential_expression=potential_expression,
- topology=topology,
mass=mass,
charge=charge,
atomclass=atomclass,
@@ -88,7 +86,6 @@ def __init__(
overrides=overrides,
description=description,
definition=definition,
- set_ref=ATOM_TYPE_DICT,
tags=tags,
)
@@ -127,6 +124,25 @@ def definition(self):
"""Return the SMARTS string of the atom_type."""
return self.__dict__.get("definition_")
+ def __eq__(self, other):
+ if other is self:
+ return True
+ if not isinstance(other, AtomType):
+ return False
+ return (
+ self.name == other.name
+ and self.expression == other.expression
+ and self.independent_variables == other.independent_variables
+ and self.parameters == other.parameters
+ and self.charge == other.charge
+ and self.atomclass == other.atomclass
+ and self.mass == other.mass
+ and self.doi == other.doi
+ and self.overrides == other.overrides
+ and self.definition == other.definition
+ and self.description == other.description
+ )
+
def __repr__(self):
"""Return a formatted representation of the atom type."""
desc = (
diff --git a/gmso/core/bond_type.py b/gmso/core/bond_type.py
index 6a50219fe..ed6a367a4 100644
--- a/gmso/core/bond_type.py
+++ b/gmso/core/bond_type.py
@@ -5,7 +5,6 @@
from pydantic import Field
from gmso.core.parametric_potential import ParametricPotential
-from gmso.utils._constants import BOND_TYPE_DICT
from gmso.utils.expression import PotentialExpression
@@ -47,7 +46,6 @@ def __init__(
potential_expression=None,
member_types=None,
member_classes=None,
- topology=None,
tags=None,
):
super(BondType, self).__init__(
@@ -56,10 +54,8 @@ def __init__(
parameters=parameters,
independent_variables=independent_variables,
potential_expression=potential_expression,
- topology=topology,
member_types=member_types,
member_classes=member_classes,
- set_ref=BOND_TYPE_DICT,
tags=tags,
)
diff --git a/gmso/core/dihedral_type.py b/gmso/core/dihedral_type.py
index 383dae755..9a31967d7 100644
--- a/gmso/core/dihedral_type.py
+++ b/gmso/core/dihedral_type.py
@@ -4,7 +4,6 @@
from pydantic import Field
from gmso.core.parametric_potential import ParametricPotential
-from gmso.utils._constants import DIHEDRAL_TYPE_DICT
from gmso.utils.expression import PotentialExpression
@@ -52,7 +51,6 @@ def __init__(
potential_expression=None,
member_types=None,
member_classes=None,
- topology=None,
tags=None,
):
@@ -62,10 +60,8 @@ def __init__(
parameters=parameters,
independent_variables=independent_variables,
potential_expression=potential_expression,
- topology=topology,
member_types=member_types,
member_classes=member_classes,
- set_ref=DIHEDRAL_TYPE_DICT,
tags=tags,
)
diff --git a/gmso/core/element.py b/gmso/core/element.py
index ab957b0cf..67107c1e5 100644
--- a/gmso/core/element.py
+++ b/gmso/core/element.py
@@ -45,6 +45,18 @@ def __repr__(self):
f"atomic number: {self.atomic_number}, mass: {self.mass.to('amu')}>"
)
+ def __eq__(self, other):
+ if other is self:
+ return True
+ if not isinstance(other, Element):
+ return False
+ return (
+ self.name == other.name
+ and self.mass == other.mass
+ and self.symbol == other.symbol
+ and self.atomic_number == other.atomic_number
+ )
+
class Config:
"""Pydantic configuration for element."""
diff --git a/gmso/core/improper_type.py b/gmso/core/improper_type.py
index f15b0a057..b579bbd96 100644
--- a/gmso/core/improper_type.py
+++ b/gmso/core/improper_type.py
@@ -5,7 +5,6 @@
from pydantic import Field
from gmso.core.parametric_potential import ParametricPotential
-from gmso.utils._constants import IMPROPER_TYPE_DICT
from gmso.utils.expression import PotentialExpression
@@ -58,7 +57,6 @@ def __init__(
potential_expression=None,
member_types=None,
member_classes=None,
- topology=None,
tags=None,
):
super(ImproperType, self).__init__(
@@ -67,10 +65,8 @@ def __init__(
parameters=parameters,
independent_variables=independent_variables,
potential_expression=potential_expression,
- topology=topology,
member_types=member_types,
member_classes=member_classes,
- set_ref=IMPROPER_TYPE_DICT,
tags=tags,
)
diff --git a/gmso/core/pairpotential_type.py b/gmso/core/pairpotential_type.py
index 69b56418b..307e9c429 100644
--- a/gmso/core/pairpotential_type.py
+++ b/gmso/core/pairpotential_type.py
@@ -4,7 +4,6 @@
from pydantic import Field
from gmso.core.parametric_potential import ParametricPotential
-from gmso.utils._constants import PAIRPOTENTIAL_TYPE_DICT
from gmso.utils.expression import PotentialExpression
@@ -41,7 +40,6 @@ def __init__(
independent_variables=None,
potential_expression=None,
member_types=None,
- topology=None,
tags=None,
):
super(PairPotentialType, self).__init__(
@@ -49,10 +47,8 @@ def __init__(
expression=expression,
parameters=parameters,
independent_variables=independent_variables,
- topology=topology,
member_types=member_types,
potential_expression=potential_expression,
- set_ref=PAIRPOTENTIAL_TYPE_DICT,
tags=tags,
)
diff --git a/gmso/core/parametric_potential.py b/gmso/core/parametric_potential.py
index 18d01cb89..0b264e5c6 100644
--- a/gmso/core/parametric_potential.py
+++ b/gmso/core/parametric_potential.py
@@ -5,7 +5,6 @@
from gmso.abc.abstract_potential import AbstractPotential
from gmso.exceptions import GMSOError
-from gmso.utils.decorators import confirm_dict_existence
from gmso.utils.expression import PotentialExpression
@@ -21,18 +20,6 @@ class ParametricPotential(AbstractPotential):
by classes that represent these potentials.
"""
- # FIXME: Use proper forward referencing??
- topology_: Optional[Any] = Field(
- None, description="the topology of which this potential is a part of"
- )
-
- set_ref_: Optional[str] = Field(
- None,
- description="The string name of the bookkeeping set in gmso.Topology class. "
- "This is used to track property based hashed object's "
- "changes so that a dictionary/set can keep track of them",
- )
-
def __init__(
self,
name="ParametricPotential",
@@ -40,7 +27,6 @@ def __init__(
parameters=None,
potential_expression=None,
independent_variables=None,
- topology=None,
**kwargs,
):
if potential_expression is not None and (
@@ -64,7 +50,6 @@ def __init__(
super().__init__(
name=name,
potential_expression=_potential_expression,
- topology=topology,
**kwargs,
)
@@ -106,31 +91,6 @@ def parameters(self):
"""Optional[dict]\n\tThe parameters of the `Potential` expression and their corresponding values, as `unyt` quantities"""
return self.potential_expression_.parameters
- @property
- def topology(self):
- """Return the associated topology with this potential."""
- return self.__dict__.get("topology_")
-
- @property
- def set_ref(self):
- """Set the string name of the bookkeeping set in gmso.topology to track potentials."""
- return self.__dict__.get("set_ref_")
-
- @validator("topology_")
- def is_valid_topology(cls, value):
- """Determine if the topology is a valid gmso topology."""
- if value is None:
- return None
- else:
- from gmso.core.topology import Topology
-
- if not isinstance(value, Topology):
- raise TypeError(
- f"{type(value).__name__} is not of type Topology"
- )
- return value
-
- @confirm_dict_existence
def __setattr__(self, key: Any, value: Any) -> None:
"""Set the attributes of the potential."""
if key == "parameters":
@@ -138,7 +98,6 @@ def __setattr__(self, key: Any, value: Any) -> None:
else:
super().__setattr__(key, value)
- @confirm_dict_existence
def set_expression(
self, expression=None, parameters=None, independent_variables=None
):
@@ -194,6 +153,18 @@ def dict(
exclude_none=exclude_none,
)
+ def __eq__(self, other):
+ if other is self:
+ return True
+ if not isinstance(other, type(self)):
+ return False
+ return (
+ self.expression == other.expression
+ and self.independent_variables == other.independent_variables
+ and self.name == other.name
+ and self.parameters == other.parameters
+ )
+
def get_parameters(self, copy=False):
"""Return parameters for this ParametricPotential."""
if copy:
@@ -241,7 +212,6 @@ def from_template(cls, potential_template, parameters, topology=None):
expression=potential_template.expression,
independent_variables=potential_template.independent_variables,
parameters=parameters,
- topology=topology,
)
def __repr__(self):
diff --git a/gmso/core/topology.py b/gmso/core/topology.py
index 37bca1241..12fec0f81 100644
--- a/gmso/core/topology.py
+++ b/gmso/core/topology.py
@@ -20,14 +20,6 @@
from gmso.core.pairpotential_type import PairPotentialType
from gmso.core.parametric_potential import ParametricPotential
from gmso.exceptions import GMSOError
-from gmso.utils._constants import (
- ANGLE_TYPE_DICT,
- ATOM_TYPE_DICT,
- BOND_TYPE_DICT,
- DIHEDRAL_TYPE_DICT,
- IMPROPER_TYPE_DICT,
- PAIRPOTENTIAL_TYPE_DICT,
-)
from gmso.utils.connectivity import (
identify_connections as _identify_connections,
)
@@ -158,20 +150,14 @@ def __init__(self, name="Topology", box=None):
self._dihedrals = IndexedSet()
self._impropers = IndexedSet()
self._subtops = IndexedSet()
- self._atom_types = {}
- self._atom_types_idx = {}
- self._connection_types = {}
- self._bond_types = {}
- self._bond_types_idx = {}
- self._angle_types = {}
- self._angle_types_idx = {}
- self._dihedral_types = {}
- self._dihedral_types_idx = {}
- self._improper_types = {}
- self._improper_types_idx = {}
+ self._atom_types = IndexedSet()
+ self._connection_types = IndexedSet()
+ self._bond_types = IndexedSet()
+ self._angle_types = IndexedSet()
+ self._dihedral_types = IndexedSet()
+ self._improper_types = IndexedSet()
self._combining_rule = "lorentz"
- self._pairpotential_types = {}
- self._pairpotential_types_idx = {}
+ self._pairpotential_types = IndexedSet()
self._scaling_factors = {
"nonBonded12Scale": 0.0,
"nonBonded13Scale": 0.0,
@@ -180,23 +166,6 @@ def __init__(self, name="Topology", box=None):
"electrostatics13Scale": 0.0,
"electrostatics14Scale": 0.5,
}
- self._set_refs = {
- ATOM_TYPE_DICT: self._atom_types,
- BOND_TYPE_DICT: self._bond_types,
- ANGLE_TYPE_DICT: self._angle_types,
- DIHEDRAL_TYPE_DICT: self._dihedral_types,
- IMPROPER_TYPE_DICT: self._improper_types,
- PAIRPOTENTIAL_TYPE_DICT: self._pairpotential_types,
- }
-
- self._index_refs = {
- ATOM_TYPE_DICT: self._atom_types_idx,
- BOND_TYPE_DICT: self._bond_types_idx,
- ANGLE_TYPE_DICT: self._angle_types_idx,
- DIHEDRAL_TYPE_DICT: self._dihedral_types_idx,
- IMPROPER_TYPE_DICT: self._improper_types_idx,
- PAIRPOTENTIAL_TYPE_DICT: self._pairpotential_types_idx,
- }
self._unique_connections = {}
@@ -282,32 +251,32 @@ def positions(self):
@property
def n_sites(self):
"""Return the number of sites in the topology."""
- return len(self.sites)
+ return len(self._sites)
@property
def n_connections(self):
"""Return the number of connections in the topology."""
- return len(self.connections)
+ return len(self._connections)
@property
def n_bonds(self):
"""Return the number of bonds in the topology."""
- return len(self.bonds)
+ return len(self._bonds)
@property
def n_angles(self):
"""Return the amount of angles in the topology."""
- return len(self.angles)
+ return len(self._angles)
@property
def n_dihedrals(self):
"""Return the amount of dihedrals in the topology."""
- return len(self.dihedrals)
+ return len(self._dihedrals)
@property
def n_impropers(self):
"""Return the number of impropers in the topology."""
- return len(self.impropers)
+ return len(self._impropers)
@property
def subtops(self):
@@ -322,103 +291,103 @@ def n_subtops(self):
@property
def sites(self):
"""Return all sites in the topology."""
- return tuple(self._sites)
+ return self._sites
@property
def connections(self):
"""Return all connections in topology."""
- return tuple(self._connections)
+ return self._connections
@property
def bonds(self):
"""Return all bonds in the topology."""
- return tuple(self._bonds)
+ return self._bonds
@property
def angles(self):
"""Return all angles in the topology."""
- return tuple(self._angles)
+ return self._angles
@property
def dihedrals(self):
"""Return all dihedrals in the topology."""
- return tuple(self._dihedrals)
+ return self._dihedrals
@property
def impropers(self):
"""Return all impropers in the topology."""
- return tuple(self._impropers)
+ return self._impropers
@property
def atom_types(self):
"""Return all atom_types in the topology."""
- return tuple(self._atom_types.values())
+ return self._atom_types
@property
def connection_types(self):
"""Return all connection_types in the topology."""
- return tuple(self._connection_types.values())
+ return self._connection_types
@property
def bond_types(self):
"""Return all bond_types in the topology."""
- return tuple(self._bond_types.values())
+ return self._bond_types
@property
def angle_types(self):
"""Return all angle_types in the topology."""
- return tuple(self._angle_types.values())
+ return self._angle_types
@property
def dihedral_types(self):
"""Return all dihedral_types in the topology."""
- return tuple(self._dihedral_types.values())
+ return self._dihedral_types
@property
def improper_types(self):
"""Return all improper_types in the topology."""
- return tuple(self._improper_types.values())
+ return self._improper_types
@property
def pairpotential_types(self):
- return tuple(self._pairpotential_types.values())
+ return self._pairpotential_types
@property
def atom_type_expressions(self):
"""Return all atom_type expressions in the topology."""
- return list(set([atype.expression for atype in self.atom_types]))
+ return list(set([atype.expression for atype in self._atom_types]))
@property
def connection_type_expressions(self):
"""Return all connection_type expressions in the topology."""
return list(
- set([contype.expression for contype in self.connection_types])
+ set([contype.expression for contype in self._connection_types])
)
@property
def bond_type_expressions(self):
"""Return all bond_type expressions in the topology."""
- return list(set([btype.expression for btype in self.bond_types]))
+ return list(set([btype.expression for btype in self._bond_types]))
@property
def angle_type_expressions(self):
"""Return all angle_type expressions in the topology."""
- return list(set([atype.expression for atype in self.angle_types]))
+ return list(set([atype.expression for atype in self._angle_types]))
@property
def dihedral_type_expressions(self):
"""Return all dihedral_type expressions in the topology."""
- return list(set([atype.expression for atype in self.dihedral_types]))
+ return list(set([dtype.expression for dtype in self._dihedral_types]))
@property
def improper_type_expressions(self):
"""Return all improper_type expressions in the topology."""
- return list(set([atype.expression for atype in self.improper_types]))
+ return list(set([itype.expression for itype in self._improper_types]))
@property
def pairpotential_type_expressions(self):
return list(
- set([atype.expression for atype in self.pairpotential_types])
+ set([ptype.expression for ptype in self._pairpotential_types])
)
def add_site(self, site, update_types=True):
@@ -440,12 +409,7 @@ def add_site(self, site, update_types=True):
"""
self._sites.add(site)
if update_types and site.atom_type:
- site.atom_type.topology = self
- if site.atom_type in self._atom_types:
- site.atom_type = self._atom_types[site.atom_type]
- else:
- self._atom_types[site.atom_type] = site.atom_type
- self._atom_types_idx[site.atom_type] = len(self._atom_types) - 1
+ self._atom_types.add(site.atom_type)
self.is_typed(updated=False)
def update_sites(self):
@@ -471,7 +435,7 @@ def update_sites(self):
gmso.Topology.add_connection : Add a Bond, an Angle or a Dihedral to the topology.
gmso.Topology.update_topology : Update the entire topology.
"""
- for connection in self.connections:
+ for connection in self._connections:
for member in connection.connection_members:
if member not in self._sites:
self.add_site(member)
@@ -512,8 +476,9 @@ def add_connection(self, connection, update_types=True):
connection = self._unique_connections[equivalent_members]
for conn_member in connection.connection_members:
- if conn_member not in self.sites:
+ if conn_member not in self._sites:
self.add_site(conn_member)
+
self._connections.add(connection)
self._unique_connections.update({equivalent_members: connection})
if isinstance(connection, Bond):
@@ -544,48 +509,21 @@ def update_connection_types(self):
--------
gmso.Topology.update_atom_types : Update atom types in the topology.
"""
+ targets = {
+ BondType: self._bond_types,
+ AngleType: self._angle_types,
+ DihedralType: self._dihedral_types,
+ ImproperType: self._improper_types,
+ }
for c in self.connections:
if c.connection_type is None:
warnings.warn(
"Non-parametrized Connection {} detected".format(c)
)
- elif not isinstance(c.connection_type, ParametricPotential):
- raise GMSOError(
- "Non-Potential {} found"
- "in Connection {}".format(c.connection_type, c)
- )
- elif c.connection_type not in self._connection_types:
- c.connection_type.topology = self
- self._connection_types[c.connection_type] = c.connection_type
- if isinstance(c.connection_type, BondType):
- self._bond_types[c.connection_type] = c.connection_type
- self._bond_types_idx[c.connection_type] = (
- len(self._bond_types) - 1
- )
- if isinstance(c.connection_type, AngleType):
- self._angle_types[c.connection_type] = c.connection_type
- self._angle_types_idx[c.connection_type] = (
- len(self._angle_types) - 1
- )
- if isinstance(c.connection_type, DihedralType):
- self._dihedral_types[c.connection_type] = c.connection_type
- self._dihedral_types_idx[c.connection_type] = (
- len(self._dihedral_types) - 1
- )
- if isinstance(c.connection_type, ImproperType):
- self._improper_types[c.connection_type] = c.connection_type
- self._improper_types_idx[c.connection_type] = (
- len(self._improper_types) - 1
- )
- elif c.connection_type in self.connection_types:
- if isinstance(c.connection_type, BondType):
- c.connection_type = self._bond_types[c.connection_type]
- if isinstance(c.connection_type, AngleType):
- c.connection_type = self._angle_types[c.connection_type]
- if isinstance(c.connection_type, DihedralType):
- c.connection_type = self._dihedral_types[c.connection_type]
- if isinstance(c.connection_type, ImproperType):
- c.connection_type = self._improper_types[c.connection_type]
+ else:
+ target_conn_type = targets[type(c.connection_type)]
+ target_conn_type.add(c.connection_type)
+ self._connection_types.add(c.connection_type)
def add_pairpotentialtype(self, pairpotentialtype, update=True):
"""add a PairPotentialType to the topology
@@ -613,17 +551,14 @@ def add_pairpotentialtype(self, pairpotentialtype, update=True):
"Non-PairPotentialType {} provided".format(pairpotentialtype)
)
for atype in pairpotentialtype.member_types:
- if atype not in [t.name for t in self.atom_types]:
- if atype not in [t.atomclass for t in self.atom_types]:
+ if atype not in {t.name for t in self.atom_types}:
+ if atype not in {t.atomclass for t in self.atom_types}:
raise GMSOError(
"There is no name/atomclass of AtomType {} in current topology".format(
atype
)
)
- self._pairpotential_types[pairpotentialtype] = pairpotentialtype
- self._pairpotential_types_idx[pairpotentialtype] = (
- len(self._pairpotential_types) - 1
- )
+ self._pairpotential_types.add(pairpotentialtype)
def remove_pairpotentialtype(self, pair_of_types):
"""Remove the custom pairwise potential between two AtomTypes/Atomclasses
@@ -640,8 +575,7 @@ def remove_pairpotentialtype(self, pair_of_types):
to_delete.append(t)
if len(to_delete) > 0:
for t in to_delete:
- del self._pairpotential_types[t]
- self._reindex_connection_types(PAIRPOTENTIAL_TYPE_DICT)
+ self._pairpotential_types.remove(t)
else:
warnings.warn(
"No pair potential specified for such pair of AtomTypes/atomclasses"
@@ -666,12 +600,9 @@ def update_atom_types(self):
raise GMSOError(
"Non AtomType instance found in site {}".format(site)
)
- elif site.atom_type not in self._atom_types:
- site.atom_type.topology = self
- self._atom_types[site.atom_type] = site.atom_type
- self._atom_types_idx[site.atom_type] = len(self._atom_types) - 1
- elif site.atom_type in self._atom_types:
- site.atom_type = self._atom_types[site.atom_type]
+ else:
+ self._atom_types.add(site.atom_type)
+
self.is_typed(updated=True)
def add_subtopology(self, subtop, update=True):
@@ -945,12 +876,12 @@ def get_index(self, member):
Angle: self._angles,
Dihedral: self._dihedrals,
Improper: self._impropers,
- AtomType: self._atom_types_idx,
- BondType: self._bond_types_idx,
- AngleType: self._angle_types_idx,
- DihedralType: self._dihedral_types_idx,
- ImproperType: self._improper_types_idx,
- PairPotentialType: self._pairpotential_types_idx,
+ AtomType: self._atom_types,
+ BondType: self._bond_types,
+ AngleType: self._angle_types,
+ DihedralType: self._dihedral_types,
+ ImproperType: self._improper_types,
+ PairPotentialType: self._pairpotential_types,
}
member_type = type(member)
@@ -960,25 +891,10 @@ def get_index(self, member):
f"Cannot index member of type {member_type.__name__}"
)
- try:
- index = refs[member_type].index(member)
- except AttributeError:
- index = refs[member_type][member]
+ index = refs[member_type].index(member)
return index
- def _reindex_connection_types(self, ref):
- """Re-generate the indices of the connection types in the topology."""
- if ref not in self._index_refs:
- raise GMSOError(
- f"cannot reindex {ref}. It should be one of "
- f"{ANGLE_TYPE_DICT}, {BOND_TYPE_DICT}, "
- f"{ANGLE_TYPE_DICT}, {DIHEDRAL_TYPE_DICT}, {IMPROPER_TYPE_DICT},"
- f"{PAIRPOTENTIAL_TYPE_DICT}"
- )
- for i, ref_member in enumerate(self._set_refs[ref].keys()):
- self._index_refs[ref][ref_member] = i
-
def iter_sites(self, key, value):
"""Iterate through this topology's sites based on certain attribute and their values.
@@ -1006,7 +922,7 @@ def iter_sites(self, key, value):
"Expected `value` to be something other than None. Provided None."
)
- for site in self.sites:
+ for site in self._sites:
if getattr(site, key) == value:
yield site
diff --git a/gmso/formats/json.py b/gmso/formats/json.py
index fa911f47e..994db15ac 100644
--- a/gmso/formats/json.py
+++ b/gmso/formats/json.py
@@ -119,11 +119,11 @@ def _to_json(top, types=False, update=True):
connection_dict[exclude_attr] = id(connection_type)
if types:
for potentials in [
- top._atom_types.values(),
- top._bond_types.values(),
- top._angle_types.values(),
- top._dihedral_types.values(),
- top._improper_types.values(),
+ top._atom_types,
+ top._bond_types,
+ top._angle_types,
+ top._dihedral_types,
+ top._improper_types,
]:
for potential in potentials:
potential_dict = potential.json_dict(
@@ -133,9 +133,9 @@ def _to_json(top, types=False, update=True):
potential_dict["id"] = id(potential)
target.append(potential_dict)
- for pairpotential_type in top._pairpotential_types.values():
+ for pairpotential_type in top._pairpotential_types:
json_dict["pair_potentialtypes"].append(
- pairpotential_type.json_dict(exclude={"topology", "set_ref"})
+ pairpotential_type.json_dict()
)
for subtop in top.subtops:
diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py
index 3db264a96..c6db9bcdc 100644
--- a/gmso/tests/base_test.py
+++ b/gmso/tests/base_test.py
@@ -340,6 +340,7 @@ def test_atom_equality(atom1, atom2):
)
for prop in atom1.dict(by_alias=True):
if not equal(atom2.dict().get(prop), atom1.dict().get(prop)):
+ print(atom2.dict().get(prop), atom1.dict().get(prop), prop)
return False
return True
diff --git a/gmso/tests/test_atom_type.py b/gmso/tests/test_atom_type.py
index 81310c6fc..c9ae1795a 100644
--- a/gmso/tests/test_atom_type.py
+++ b/gmso/tests/test_atom_type.py
@@ -292,11 +292,9 @@ def test_atom_type_with_topology_and_site(self):
site2.atom_type = atom_type2
top.add_site(site1)
top.add_site(site2)
- assert id(site1.atom_type) == id(site2.atom_type)
+ assert id(site1.atom_type) != id(site2.atom_type)
assert site1.atom_type is not None
- assert len(top.atom_types) == 1
- assert site1.atom_type.topology == top
- assert site2.atom_type.topology == top
+ assert len(top.atom_types) == 2
def test_atom_type_with_topology_and_site_change_properties(self):
site1 = Atom()
@@ -309,7 +307,7 @@ def test_atom_type_with_topology_and_site_change_properties(self):
top.add_site(site1)
top.add_site(site2)
site1.atom_type.mass = 250
- assert site2.atom_type.mass == 250
+ assert site1.atom_type.mass == 250
assert top.atom_types[0].mass == 250
def test_with_1000_atom_types(self):
@@ -320,7 +318,7 @@ def test_with_1000_atom_types(self):
site.atom_type = atom_type
top.add_site(site, update_types=False)
top.update_topology()
- assert len(top.atom_types) == 1
+ assert len(top.atom_types) == 1000
assert top.n_sites == 1000
def test_atom_type_copy(self, typed_ethane):
diff --git a/gmso/tests/test_expression.py b/gmso/tests/test_expression.py
index 8d064c367..7b0a493d0 100644
--- a/gmso/tests/test_expression.py
+++ b/gmso/tests/test_expression.py
@@ -185,7 +185,6 @@ def test_expression_equality(self):
)
assert expression_1.expression == expression_2.expression
- assert hash(expression_1) == hash(expression_2)
assert expression_3 != expression_2
assert expression_1 != expression_3
@@ -209,6 +208,5 @@ def test_parametric_equality(self):
)
assert expression_1.expression == expression_2.expression
- assert hash(expression_1) == hash(expression_2)
assert expression_3 != expression_2
assert expression_1 != expression_3
diff --git a/gmso/tests/test_serialization.py b/gmso/tests/test_serialization.py
index 6a4fd1861..6a6298a46 100644
--- a/gmso/tests/test_serialization.py
+++ b/gmso/tests/test_serialization.py
@@ -52,7 +52,6 @@ def test_atom_types_to_json_loop(self, typed_ethane):
for atom_type in atom_types_to_test:
atom_type_json = atom_type.json()
atom_type_copy = AtomType.parse_raw(atom_type_json)
- atom_type_copy.topology = atom_type.topology
assert atom_type_copy == atom_type
def test_bond_to_json_loop(self, typed_ethane, are_equivalent_atoms):
@@ -71,7 +70,6 @@ def test_bond_type_to_json_loop(self, typed_ethane):
for bond_type in bond_types_to_test:
bond_type_json = bond_type.json()
bond_type_copy = BondType.parse_raw(bond_type_json)
- bond_type_copy.topology = bond_type.topology
assert bond_type_copy == bond_type
def test_angle_to_json_loop(self, typed_ethane, are_equivalent_atoms):
@@ -89,7 +87,6 @@ def test_angle_type_to_json_loop(self, typed_ethane):
for angle_type in angle_types_to_test:
angle_type_json = angle_type.json()
angle_type_copy = AngleType.parse_raw(angle_type_json)
- angle_type_copy.topology = angle_type.topology
assert angle_type_copy == angle_type
def test_dihedral_to_json_loop(self, typed_ethane, are_equivalent_atoms):
@@ -107,7 +104,6 @@ def test_dihedral_types_to_json_loop(self, typed_ethane):
for dihedral_type in dihedral_types_to_test:
dihedral_type_json = dihedral_type.json()
dihedral_type_copy = DihedralType.parse_raw(dihedral_type_json)
- dihedral_type_copy.topology = dihedral_type.topology
assert dihedral_type_copy == dihedral_type
def test_improper_to_json_loop(self, typed_ethane, are_equivalent_atoms):
diff --git a/gmso/tests/test_top.py b/gmso/tests/test_top.py
index 518b7be72..389edbab5 100644
--- a/gmso/tests/test_top.py
+++ b/gmso/tests/test_top.py
@@ -74,7 +74,7 @@ def test_water_top(self, water_system):
top.save("water.top")
def test_ethane_periodic(self, typed_ethane):
- from gmso.core.parametric_potential import ParametricPotential
+ from gmso.core.dihedral_type import DihedralType
from gmso.lib.potential_templates import PotentialTemplateLibrary
per_torsion = PotentialTemplateLibrary()["PeriodicTorsionPotential"]
@@ -83,7 +83,7 @@ def test_ethane_periodic(self, typed_ethane):
"phi_eq": 15 * u.Unit("degree"),
"n": 3 * u.Unit("dimensionless"),
}
- periodic_dihedral_type = ParametricPotential.from_template(
+ periodic_dihedral_type = DihedralType.from_template(
potential_template=per_torsion, parameters=params
)
for dihedral in typed_ethane.dihedrals:
diff --git a/gmso/tests/test_topology.py b/gmso/tests/test_topology.py
index ceeb560e7..66a88c17e 100644
--- a/gmso/tests/test_topology.py
+++ b/gmso/tests/test_topology.py
@@ -404,8 +404,6 @@ def test_pairpotential_pairpotentialtype_update(
self, pairpotentialtype_top
):
assert len(pairpotentialtype_top.pairpotential_types) == 1
- pptype12 = pairpotentialtype_top.pairpotential_types[0]
- assert pairpotentialtype_top._pairpotential_types_idx[pptype12] == 0
pairpotentialtype_top.remove_pairpotentialtype(["a1", "a2"])
assert len(pairpotentialtype_top.pairpotential_types) == 0
@@ -422,7 +420,7 @@ def test_parametrization(self):
top = Topology()
assert top.typed == False
- top.add_site(Atom(atom_type=AtomType()))
+ top.add_site(Atom(atom_type=AtomType()), update_types=True)
assert top.typed == True
assert top.is_typed() == True
@@ -445,10 +443,9 @@ def test_topology_atom_type_changes(self):
site.atom_type = atom_type
top.add_site(site, update_types=False)
top.update_topology()
- assert len(top.atom_types) == 10
+ assert len(top.atom_types) == 100
top.sites[0].atom_type.name = "atom_type_changed"
- assert id(top.sites[0].atom_type) == id(top.sites[10].atom_type)
- assert top.sites[10].atom_type.name == "atom_type_changed"
+ assert top.sites[10].atom_type.name != "atom_type_changed"
assert top.is_typed()
def test_add_duplicate_connected_atom(self):
@@ -562,19 +559,6 @@ def test_topology_get_index_atom_type(self, typed_water_system):
== 1
)
- def test_topology_get_index_atom_type_after_change(
- self, typed_water_system
- ):
- typed_water_system.sites[0].atom_type.name = "atom_type_changed_name"
- assert (
- typed_water_system.get_index(typed_water_system.sites[0].atom_type)
- == 1
- )
- assert (
- typed_water_system.get_index(typed_water_system.sites[1].atom_type)
- == 0
- )
-
def test_topology_get_index_bond_type(self, typed_methylnitroaniline):
assert (
typed_methylnitroaniline.get_index(
@@ -589,17 +573,6 @@ def test_topology_get_index_bond_type(self, typed_methylnitroaniline):
int,
)
- def test_topology_get_index_bond_type_after_change(
- self, typed_methylnitroaniline
- ):
- typed_methylnitroaniline.bonds[0].connection_type.name = "changed name"
- assert (
- typed_methylnitroaniline.get_index(
- typed_methylnitroaniline.bonds[0].connection_type
- )
- != 0
- )
-
def test_topology_get_index_angle_type(self, typed_chloroethanol):
assert (
typed_chloroethanol.get_index(
@@ -614,16 +587,6 @@ def test_topology_get_index_angle_type(self, typed_chloroethanol):
== 1
)
- def test_topology_get_index_angle_type_after_change(
- self, typed_methylnitroaniline
- ):
- angle_type_to_test = typed_methylnitroaniline.angles[0].connection_type
- prev_idx = typed_methylnitroaniline.get_index(angle_type_to_test)
- typed_methylnitroaniline.angles[0].connection_type.name = "changed name"
- assert (
- typed_methylnitroaniline.get_index(angle_type_to_test) != prev_idx
- )
-
def test_topology_get_index_dihedral_type(self, typed_chloroethanol):
assert (
typed_chloroethanol.get_index(
@@ -638,21 +601,6 @@ def test_topology_get_index_dihedral_type(self, typed_chloroethanol):
== 3
)
- def test_topology_get_index_dihedral_type_after_change(
- self, typed_methylnitroaniline
- ):
- dihedral_type_to_test = typed_methylnitroaniline.dihedrals[
- 0
- ].connection_type
- prev_idx = typed_methylnitroaniline.get_index(dihedral_type_to_test)
- typed_methylnitroaniline.dihedrals[
- 0
- ].connection_type.name = "changed name"
- assert (
- typed_methylnitroaniline.get_index(dihedral_type_to_test)
- != prev_idx
- )
-
def test_topology_get_bonds_for(self, typed_methylnitroaniline):
site = list(typed_methylnitroaniline.sites)[0]
converted_bonds_list = typed_methylnitroaniline._get_bonds_for(site)
diff --git a/gmso/utils/decorators.py b/gmso/utils/decorators.py
index 214334b83..3675e9432 100644
--- a/gmso/utils/decorators.py
+++ b/gmso/utils/decorators.py
@@ -1,28 +1,7 @@
"""Various decorators for GMSO."""
-from functools import wraps
-
from gmso.abc import GMSOJSONHandler
-def confirm_dict_existence(setter_function):
- """Confirm that any core type member is in the topology's set.
-
- If it is used to wrap setters of the core type member class
- """
-
- @wraps(setter_function)
- def setter_with_dict_removal(self, *args, **kwargs):
- if self.topology:
- self.topology._set_refs[self.set_ref].pop(self, None)
- setter_function(self, *args, **kwargs)
- self.topology._set_refs[self.set_ref][self] = self
- self.topology._reindex_connection_types(self.set_ref)
- else:
- setter_function(self, *args, **kwargs)
-
- return setter_with_dict_removal
-
-
class register_pydantic_json(object):
"""Provides a way to register json encoders for a non-JSON serializable class."""
diff --git a/gmso/utils/expression.py b/gmso/utils/expression.py
index 212da212c..bcdbf0aaa 100644
--- a/gmso/utils/expression.py
+++ b/gmso/utils/expression.py
@@ -217,6 +217,24 @@ def _validate_parameters(parameters):
return parameters
+ def __eq__(self, other):
+ """Equality checks for two expressions."""
+ if other is self:
+ return True
+ if not isinstance(other, PotentialExpression):
+ return False
+ if not self.is_parametric:
+ return (
+ self.expression == other.expression
+ and self.independent_variables == other.independent_variables
+ )
+ else:
+ return (
+ self.expression == other.expression
+ and self.independent_variables == other.independent_variables
+ and self.parameters == other.parameters
+ )
+
@staticmethod
def _validate_independent_variables(indep_vars):
"""Check to see that independent_variables is a set of valid sympy symbols."""
diff --git a/gmso/utils/ff_utils.py b/gmso/utils/ff_utils.py
index b245a4679..1e55ae120 100644
--- a/gmso/utils/ff_utils.py
+++ b/gmso/utils/ff_utils.py
@@ -352,7 +352,6 @@ def parse_ff_atomtypes(atomtypes_el, ff_meta):
"overrides": "",
"definition": "",
"description": "",
- "topology": None,
"element": "",
}
From 24344b51af7d3de8ae7dcc43b85002a188f48bdb Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 11 Apr 2022 18:17:42 -0500
Subject: [PATCH 045/141] [pre-commit.ci] pre-commit autoupdate (#643)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 15838aedf..bc0f4aef3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,7 +9,7 @@ ci:
submodules: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.1.0
+ rev: v4.2.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
From a10c7c13de02763485cc2da23b088611f4648d17 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Tue, 12 Apr 2022 15:55:33 -0500
Subject: [PATCH 046/141] Add cloning support to Potentials (#642)
* Add cloning support in Potentials
* WIP- fix kwarg in lru_cache
* WIP-Include develop branch in azp checks
---
azure-pipelines.yml | 2 +
gmso/core/atom_type.py | 19 +++++++++
gmso/core/parametric_potential.py | 21 ++++++++++
gmso/tests/test_atom_type.py | 37 +++++++++++++++++
gmso/tests/test_expression.py | 22 ++++++++++
gmso/tests/test_potential.py | 41 +++++++++++++++++++
gmso/utils/expression.py | 67 +++++++++++++++++++++++++------
7 files changed, 196 insertions(+), 13 deletions(-)
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 2a1ed4cdf..968378aba 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -2,6 +2,7 @@ trigger:
branches:
include:
- master
+ - develop
tags:
include:
- 0.*
@@ -11,6 +12,7 @@ pr:
branches:
include:
- master
+ - develop
schedules:
- cron: "0 0 * * *"
diff --git a/gmso/core/atom_type.py b/gmso/core/atom_type.py
index 3a01f446b..3a1b39b30 100644
--- a/gmso/core/atom_type.py
+++ b/gmso/core/atom_type.py
@@ -124,6 +124,25 @@ def definition(self):
"""Return the SMARTS string of the atom_type."""
return self.__dict__.get("definition_")
+ def clone(self):
+ """Clone this AtomType, faster alternative to deepcopying."""
+ return AtomType(
+ name=str(self.name),
+ expression=None,
+ parameters=None,
+ independent_variables=None,
+ potential_expression=self.potential_expression_.clone(),
+ mass=u.unyt_quantity(self.mass_.value, self.mass_.units),
+ charge=u.unyt_quantity(self.charge_.value, self.charge_.units),
+ atomclass=self.atomclass_,
+ doi=self.doi_,
+ overrides=set(o for o in self.overrides_)
+ if self.overrides_
+ else None,
+ description=self.description_,
+ definition=self.definition_,
+ )
+
def __eq__(self, other):
if other is self:
return True
diff --git a/gmso/core/parametric_potential.py b/gmso/core/parametric_potential.py
index 0b264e5c6..efac3ef52 100644
--- a/gmso/core/parametric_potential.py
+++ b/gmso/core/parametric_potential.py
@@ -1,3 +1,4 @@
+from copy import copy, deepcopy
from typing import Any, Optional, Union
import unyt as u
@@ -177,6 +178,26 @@ def get_parameters(self, copy=False):
return params
+ def clone(self):
+ """Clone this parametric potential, faster alternative to deepcopying."""
+ Creator = self.__class__
+ kwargs = {"tags": deepcopy(self.tags_)}
+ if hasattr(self, "member_classes"):
+ kwargs["member_classes"] = (
+ copy(self.member_classes) if self.member_classes else None
+ )
+
+ if hasattr(self, "member_types"):
+ kwargs["member_types"] = (
+ copy(self.member_types) if self.member_types else None
+ )
+
+ return Creator(
+ name=self.name,
+ potential_expression=self.potential_expression_.clone(),
+ **kwargs,
+ )
+
@classmethod
def from_template(cls, potential_template, parameters, topology=None):
"""Create a potential object from the potential_template.
diff --git a/gmso/tests/test_atom_type.py b/gmso/tests/test_atom_type.py
index c9ae1795a..c3978ccaa 100644
--- a/gmso/tests/test_atom_type.py
+++ b/gmso/tests/test_atom_type.py
@@ -365,3 +365,40 @@ def test_atom_type_dict(self):
atype_dict = atype.dict(exclude={"potential_expression"})
assert "potential_expression" not in atype_dict
assert "charge" in atype_dict
+
+ def test_atom_type_clone(self):
+ top = Topology()
+ atype = AtomType(
+ name="ff_255",
+ expression="a*x+b+c*y",
+ independent_variables={"x", "y"},
+ parameters={"a": 200 * u.g, "b": 300 * u.K, "c": 400 * u.J},
+ mass=2.0 * u.g / u.mol,
+ charge=2.0 * u.elementary_charge,
+ atomclass="CX",
+ overrides={"ff_234"},
+ definition="CC-C",
+ description="Dummy Description",
+ )
+ atype_clone = atype.clone()
+
+ atom1 = Atom(name="1")
+ atom2 = Atom(name="2")
+ atom1.atom_type = atype
+ atom2.atom_type = atype_clone
+
+ top.add_site(atom1)
+ top.add_site(atom2)
+ top.update_topology()
+
+ assert len(top.atom_types) == 2
+
+ atype_dict = atype.dict(exclude={"topology", "set_ref"})
+ atype_clone_dict = atype_clone.dict(exclude={"topology", "set_ref"})
+
+ for key, value in atype_dict.items():
+ cloned = atype_clone_dict[key]
+ assert value == cloned
+ if id(value) == id(cloned):
+ assert isinstance(value, str)
+ assert isinstance(cloned, str)
diff --git a/gmso/tests/test_expression.py b/gmso/tests/test_expression.py
index 7b0a493d0..bfdb5cf3e 100644
--- a/gmso/tests/test_expression.py
+++ b/gmso/tests/test_expression.py
@@ -210,3 +210,25 @@ def test_parametric_equality(self):
assert expression_1.expression == expression_2.expression
assert expression_3 != expression_2
assert expression_1 != expression_3
+
+ def test_clone(self):
+ expr = PotentialExpression(
+ expression="a^2+2*a*b+b^2+2*theta*phi",
+ independent_variables={"theta", "phi"},
+ parameters={"a": 2.0 * u.nm, "b": 2.0 * u.rad},
+ )
+
+ expr_clone = expr.clone()
+
+ assert expr_clone.expression == expr.expression
+ assert id(expr_clone.expression) != id(expr.expression)
+
+ assert expr_clone.parameters == expr.parameters
+ assert id(expr_clone.parameters) != id(expr.parameters)
+
+ assert expr_clone.independent_variables == expr.independent_variables
+ assert id(expr_clone.independent_variables) != id(
+ expr.independent_variables
+ )
+
+ assert expr == expr_clone
diff --git a/gmso/tests/test_potential.py b/gmso/tests/test_potential.py
index ceb688987..a3f331bee 100644
--- a/gmso/tests/test_potential.py
+++ b/gmso/tests/test_potential.py
@@ -3,7 +3,11 @@
import unyt as u
from unyt.testing import assert_allclose_units
+from gmso.core.atom import Atom
+from gmso.core.bond import Bond
+from gmso.core.bond_type import BondType
from gmso.core.parametric_potential import ParametricPotential
+from gmso.core.topology import Topology
from gmso.exceptions import GMSOError
from gmso.lib.potential_templates import PotentialTemplateLibrary
from gmso.tests.base_test import BaseTest
@@ -229,3 +233,40 @@ def test_class_method_with_error(self):
template = object()
with pytest.raises(GMSOError):
ParametricPotential.from_template(template, parameters=None)
+
+ def test_bondtype_clone(self):
+ top = Topology()
+ btype = BondType(
+ name="ff_255~ff_256",
+ expression="a*x+b+c*y",
+ independent_variables={"x", "y"},
+ parameters={"a": 200 * u.g, "b": 300 * u.K, "c": 400 * u.J},
+ )
+ btype_clone = btype.clone()
+
+ atom1 = Atom(name="1")
+ atom2 = Atom(name="2")
+ bond1 = Bond(connection_members=[atom1, atom2])
+
+ atom3 = Atom(name="3")
+ atom4 = Atom(name="4")
+ bond2 = Bond(connection_members=[atom3, atom4])
+
+ bond1.bond_type = btype
+ bond2.bond_type = btype_clone
+
+ top.add_connection(bond1)
+ top.add_connection(bond2)
+ top.update_topology()
+
+ assert len(top.bond_types) == 2
+
+ btype_dict = btype.dict(exclude={"topology", "set_ref"})
+ btype_clone_dict = btype_clone.dict(exclude={"topology", "set_ref"})
+
+ for key, value in btype_dict.items():
+ cloned = btype_clone_dict[key]
+ assert value == cloned
+ if id(value) == id(cloned):
+ assert isinstance(value, (str, type(None)))
+ assert isinstance(cloned, (str, type(None)))
diff --git a/gmso/utils/expression.py b/gmso/utils/expression.py
index bcdbf0aaa..ed1d64083 100644
--- a/gmso/utils/expression.py
+++ b/gmso/utils/expression.py
@@ -1,5 +1,7 @@
"""Manage Potential functional expressions and variables."""
import warnings
+from copy import deepcopy
+from functools import lru_cache
import sympy
import unyt as u
@@ -34,6 +36,9 @@ class PotentialExpression:
parameters: dict, default=None
A dictionary of parameter whose key is a string and values are parameters
+
+ verify_validity: bool, default=True
+ If true verify validity of the expression, parameters and independent variables
"""
__slots__ = (
@@ -43,22 +48,38 @@ class PotentialExpression:
"_is_parametric",
)
- def __init__(self, expression, independent_variables, parameters=None):
- self._expression = self._validate_expression(expression)
- self._independent_variables = self._validate_independent_variables(
- independent_variables
+ def __init__(
+ self,
+ expression,
+ independent_variables,
+ parameters=None,
+ verify_validity=True,
+ ):
+ self._expression = (
+ self._validate_expression(expression)
+ if verify_validity
+ else expression
+ )
+ self._independent_variables = (
+ self._validate_independent_variables(independent_variables)
+ if verify_validity
+ else independent_variables
)
self._is_parametric = False
if parameters is not None:
self._is_parametric = True
- self._parameters = self._validate_parameters(parameters)
- self._verify_validity(
- self._expression, self._independent_variables, self._parameters
+ self._parameters = (
+ self._validate_parameters(parameters)
+ if verify_validity
+ else parameters
)
- else:
+
+ if verify_validity:
self._verify_validity(
- self._expression, self.independent_variables, None
+ self._expression,
+ frozenset(self._independent_variables),
+ frozenset(self._parameters) if self._is_parametric else None,
)
@property
@@ -157,11 +178,15 @@ def set(self, expression=None, parameters=None, independent_variables=None):
else:
parameters = self._parameters
- self._verify_validity(expression, independent_variables, parameters)
+ self._verify_validity(
+ expression,
+ frozenset(independent_variables),
+ frozenset(parameters),
+ )
self._parameters.update(parameters)
else:
- self._verify_validity(expression, independent_variables)
+ self._verify_validity(expression, frozenset(independent_variables))
self._expression = expression
self._independent_variables = independent_variables
@@ -182,6 +207,7 @@ def __repr__(self):
return "".join(descr)
@staticmethod
+ @lru_cache(maxsize=128)
def _validate_expression(expression):
"""Check to see that an expression is a valid sympy expression."""
if expression is None or isinstance(expression, sympy.Expr):
@@ -262,6 +288,20 @@ def _validate_independent_variables(indep_vars):
return indep_vars
+ def clone(self):
+ """Return a clone of this potential expression, faster alternative to deepcopying."""
+ return PotentialExpression(
+ deepcopy(self._expression),
+ deepcopy(self._independent_variables),
+ {
+ k: u.unyt_quantity(v.value, v.units)
+ for k, v in self._parameters.items()
+ }
+ if self._is_parametric
+ else None,
+ verify_validity=False,
+ )
+
@staticmethod
def json(potential_expression):
"""Convert the provided potential expression to a json serializable dictionary."""
@@ -283,6 +323,7 @@ def json(potential_expression):
return json_dict
@staticmethod
+ @lru_cache(maxsize=128)
def _verify_validity(
expression, independent_variables_symbols, parameters=None
):
@@ -295,7 +336,7 @@ def _verify_validity(
f"exist in the expression's free symbols {expression.free_symbols}"
)
if parameters is not None:
- parameter_symbols = sympy.symbols(set(parameters.keys()))
+ parameter_symbols = sympy.symbols(parameters)
used_symbols = parameter_symbols.union(
independent_variables_symbols
)
@@ -307,7 +348,7 @@ def _verify_validity(
)
if used_symbols != expression.free_symbols:
- symbols = sympy.symbols(set(parameters.keys()))
+ symbols = sympy.symbols(parameters)
if symbols != expression.free_symbols:
missing_syms = (
expression.free_symbols
From bfc9c407bca73f065fb138ae8a998ceeb3596dde Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Tue, 19 Apr 2022 13:23:23 -0500
Subject: [PATCH 047/141] Use github actions for CI (#646)
* Use GHA and ditch azp
* Add test branch trigger
* WIP- fix typo in macOS-latest
* WIP- fix 5711 in matrix.os
* Fix typo in mamba action name
* RM python dep from environment.yml
* Use GHA and ditch azp
* Add test branch trigger
* WIP- fix typo in macOS-latest
* WIP- fix 5711 in matrix.os
* Fix typo in mamba action name
* RM python dep from environment.yml
* WIP- Try to upload coverage report
* Verbose coverage upload
* Explicit xml coverage report
* WIP- Test Docker build action
* WIP- Use proper org name in docker
* Add tag support test
* Test tags only
* Remove prefixed $
* WIP- Fix refname
* WIP- Fix refname
* WIP- Fix github refname
* WIP- Finalize tag support
* Properly set environment varaible
* WIP- Echo image info
* Remove branch ref. Workflow ready :slightly_smiling_face:
* Replace AZP badge with GHA in README
---
.github/workflows/CI.yaml | 84 ++++++++++++++++++++++++
README.md | 2 +-
azure-pipelines.yml | 134 --------------------------------------
environment-dev.yml | 1 -
4 files changed, 85 insertions(+), 136 deletions(-)
create mode 100644 .github/workflows/CI.yaml
delete mode 100644 azure-pipelines.yml
diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
new file mode 100644
index 000000000..ac8dc6e12
--- /dev/null
+++ b/.github/workflows/CI.yaml
@@ -0,0 +1,84 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - "master"
+ pull_request:
+ branches:
+ - "master"
+ schedule:
+ - cron: "0 0 * * *"
+
+jobs:
+ test:
+ if: github.event.pull_request.draft == false
+ name: GMSO Tests
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [macOS-latest, ubuntu-latest]
+ python-version: [3.7, 3.8, 3.9]
+
+ defaults:
+ run:
+ shell: bash -l {0}
+
+ steps:
+ - uses: actions/checkout@v2
+ name: Checkout Branch / Pull Request
+
+ - name: Install Mamba
+ uses: mamba-org/provision-with-micromamba@main
+ with:
+ environment-file: environment-dev.yml
+ extra-specs: |
+ python=${{ matrix.python-version }}
+
+ - name: Install Package
+ run: python -m pip install -e .
+
+ - name: Test (OS -> ${{ matrix.os }} / Python -> ${{ matrix.python-version }})
+ run: python -m pytest -v --cov=gmso --cov-report=xml --cov-append --cov-config=setup.cfg --color yes --pyargs gmso
+
+ - name: Upload Coverage Report
+ uses: codecov/codecov-action@v2
+ with:
+ name: GMSO-Coverage
+ verbose: true
+
+ docker:
+ runs-on: ubuntu-latest
+ needs: test
+ name: Build Docker Image
+ if: github.event_name != 'pull_request'
+
+ steps:
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_PASSWORD }}
+
+ - name: Get Tagged Version
+ run: |
+ echo "DOCKER_TAGS=mosdef/gmso:${GITHUB_REF_NAME}, mosdef/gmso:stable" >> $GITHUB_ENV
+ if: github.ref_type == 'tag'
+
+ - name: Get Push Version
+ run: |
+ echo "DOCKER_TAGS=mosdef/gmso:${GITHUB_REF_NAME}, mosdef/gmso:latest" >> $GITHUB_ENV
+ if: github.ref_type == 'branch'
+
+ - name: Docker Image Info
+ run: |
+ echo Docker Image tags: ${DOCKER_TAGS}
+
+ - name: Build and Push
+ uses: docker/build-push-action@v2
+ with:
+ push: true
+ tags: ${{ env.DOCKER_TAGS }}
diff --git a/README.md b/README.md
index ab0e14400..1957a6a90 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
## GMSO: General Molecular Simulation Object
![](https://anaconda.org/conda-forge/gmso/badges/license.svg)
[![](https://anaconda.org/conda-forge/gmso/badges/version.svg)](https://anaconda.org/conda-forge/gmso)
-[![Build Status](https://dev.azure.com/mosdef/mosdef/_apis/build/status/mosdef-hub.gmso?branchName=master)](https://dev.azure.com/mosdef/mosdef/_build/latest?definitionId=9&branchName=master)
+[![CI](https://github.com/mosdef-hub/gmso/actions/workflows/CI.yaml/badge.svg)](https://github.com/mosdef-hub/gmso/actions/workflows/CI.yaml)
[![codecov](https://codecov.io/gh/mosdef-hub/gmso/branch/master/graph/badge.svg?token=rqPGwmXDzu)](undefined)
`GMSO`is a flexible storage of chemical topology for molecular simulation.
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index 2a1ed4cdf..000000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,134 +0,0 @@
-trigger:
- branches:
- include:
- - master
- tags:
- include:
- - 0.*
-
-pr:
- autoCancel: true
- branches:
- include:
- - master
-
-schedules:
-- cron: "0 0 * * *"
- displayName: Daily midnight build for master
- branches:
- include:
- - master
- always: true
-
-stages:
- - stage: Test
- jobs:
- - job: TestsForGMSO
- strategy:
- matrix:
- Python37Ubuntu:
- imageName: 'ubuntu-latest'
- python.version: 3.7
- Python38Ubuntu:
- imageName: 'ubuntu-latest'
- python.version: 3.8
- Python39Ubuntu:
- imageName: 'ubuntu-latest'
- python.version: 3.9
- Python37macOS:
- imageName: 'macOS-latest'
- python.version: 3.7
- Python38macOS:
- imageName: 'macOS-latest'
- python.version: 3.8
- Python39macOS:
- imageName: 'macOS-latest'
- python.version: 3.9
-
- pool:
- vmImage: $(imageName)
-
- steps:
- - bash: echo "##vso[task.prependpath]$CONDA/bin"
- displayName: Add conda to path
-
- - bash: sudo chown -R $USER $CONDA
- condition: eq( variables['Agent.OS'], 'Darwin' )
- displayName: Take ownership of conda installation
-
- - bash: |
- conda config --set always_yes yes --set changeps1 no
- displayName: Set conda configuration
-
- - bash: |
- conda install -y -c conda-forge mamba
- displayName: Install mamba
-
- - bash: |
- mamba update conda -yq
- displayName: Add relevant channels and update
-
- - bash: |
- sed -i -E 's/python.*$/python='$(python.version)'/' environment-dev.yml
- mamba env create -f environment-dev.yml
- source activate gmso-dev
- pip install -e .
- displayName: Install requirements and testing branch
-
- - bash: |
- source activate gmso-dev
- pip install pytest-azurepipelines
- python -m pytest -v --cov=gmso --cov-report=html --color=yes --pyargs gmso --no-coverage-upload
- displayName: Run tests
-
- - bash: |
- source activate gmso-dev
- curl -Os https://uploader.codecov.io/latest/linux/codecov
- chmod +x codecov
- ./codecov -t ${CODECOV_UPLOAD_TOKEN} -C $(Build.SourceVersion)
- condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['python.version'], '3.7' ) )
- displayName: Upload coverage report to codecov.io
- env:
- CODECOV_UPLOAD_TOKEN: $(codecovUploadToken)
-
- - task: PublishCodeCoverageResults@1
- inputs:
- codeCoverageTool: Cobertura
- summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
- reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov'
- condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['python.version'], '3.7' ) )
- displayName: Publish coverage report to Azure dashboard
-
- - stage: Docker
- dependsOn: Test
- condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), startsWith(variables['Build.SourceBranch'], 'refs/tags/0')), ne(variables['Build.Reason'], 'Schedule'))
- pool:
- vmImage: 'ubuntu-latest'
- jobs:
- - job: publishDocker
- steps:
- - bash: |
- if [[ $BUILD_SOURCEBRANCH == "refs/heads/master" ]]; then TAG='latest'; else TAG='stable'; fi
- if [[ $BUILD_SOURCEBRANCH != "refs/heads/master" ]]; then VERSION=$(Build.SourceBranch); fi;
- echo "##vso[task.setvariable variable=VERSION;]${VERSION:10}"
- echo "##vso[task.setvariable variable=DOCKER_TAG;]$TAG"
- displayName: Export Docker Tags
- - task: Docker@2
- displayName: Login to docker hub
- inputs:
- command: login
- containerRegistry: mosdefDockerLogin
-
- - task: Docker@2
- displayName: Build and Push
- inputs:
- command: buildAndPush
- repository: mosdef/gmso
- tags: |
- $(DOCKER_TAG)
- $(VERSION)
- - task: Docker@2
- displayName: Logout
- inputs:
- command: logout
- containerRegistry: mosdefDockerLogin
diff --git a/environment-dev.yml b/environment-dev.yml
index 8b90ae589..dd2ed3d7d 100644
--- a/environment-dev.yml
+++ b/environment-dev.yml
@@ -22,4 +22,3 @@ dependencies:
- ipywidgets
- ele >= 0.2.0
- pre-commit
- - python=3.9
From c78e2425ccb98ea952f024a569346d36045f6918 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Tue, 26 Apr 2022 10:50:18 -0500
Subject: [PATCH 048/141] Update workflow to reflect default branch name (#647)
---
.github/workflows/CI.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
index ac8dc6e12..e85f00ad2 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/CI.yaml
@@ -3,10 +3,10 @@ name: CI
on:
push:
branches:
- - "master"
+ - "main"
pull_request:
branches:
- - "master"
+ - "main"
schedule:
- cron: "0 0 * * *"
From 43e71f38500be6f41a1f6c7f9eef0270096e7f79 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Thu, 28 Apr 2022 13:35:36 -0500
Subject: [PATCH 049/141] Add workflow PR trigger for develop branch
---
.github/workflows/CI.yaml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
index e85f00ad2..9e4d67f53 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/CI.yaml
@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- "main"
+ - "develop"
schedule:
- cron: "0 0 * * *"
From 822247dd82f05fc0a3f8218eeb1c1c27ae529fac Mon Sep 17 00:00:00 2001
From: Brad Crawford <65550266+bc118@users.noreply.github.com>
Date: Thu, 28 Apr 2022 14:54:35 -0400
Subject: [PATCH 050/141] added Kelvin to standard energy converter via unit
(#640)
* added a Kelvin to standard energy converter, but if no K in the unyt units it just passes thru the original units.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* added a Kelvin to standard energy converter, but if no K in the unyt units it just passes thru the original units.
* added a Kelvin to standard energy converter, but if no K in the unyt units it just passes thru the original units.
* added changes to Cals comments
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
Co-authored-by: bc118
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
gmso/tests/test_conversions.py | 78 ++++++++++++++++++++++++++++++++
gmso/utils/conversions.py | 82 ++++++++++++++++++++++++++++++++++
2 files changed, 160 insertions(+)
create mode 100644 gmso/tests/test_conversions.py
diff --git a/gmso/tests/test_conversions.py b/gmso/tests/test_conversions.py
new file mode 100644
index 000000000..782b3bb0c
--- /dev/null
+++ b/gmso/tests/test_conversions.py
@@ -0,0 +1,78 @@
+from copy import deepcopy
+
+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
+
+
+class TestKelvinToEnergy(BaseTest):
+ def test_K_to_kcal(self):
+ input_value = 1 * u.Kelvin / u.nm**2
+ new_value = convert_kelvin_to_energy_units(
+ input_value,
+ "kcal/mol",
+ )
+
+ assert new_value == u.unyt_quantity(
+ 0.0019872041457050975, "kcal/(mol*nm**2)"
+ )
+
+ def test_kcal_per_mol_to_kJ_per_mol(self):
+ input_value = 2 * u.kcal / u.mol * u.gram**2
+ new_value = convert_kelvin_to_energy_units(
+ input_value,
+ "kJ/mol",
+ )
+
+ assert new_value == u.unyt_quantity(2, "kcal/mol*g**2")
+
+ def test_input_not_unyt_units(self):
+ with pytest.raises(
+ ValueError,
+ match=r"ERROR: The entered energy_input_unyt value is a , "
+ r"not a .",
+ ):
+ input_value = 2.0
+ convert_kelvin_to_energy_units(
+ input_value,
+ "kJ/mol",
+ )
+
+ def test_kcal_per_mol_to_float_output(self):
+ with pytest.raises(
+ ValueError,
+ match=r"ERROR: The entered energy_output_unyt_units_str value is a , "
+ r"not a .",
+ ):
+ input_value = 2 * u.kcal / u.mol * u.gram**2
+ convert_kelvin_to_energy_units(
+ input_value,
+ 1.0,
+ )
+
+ def test_output_units_in_K(self):
+ with pytest.raises(
+ ValueError,
+ match=r"ERROR: The entered energy_output_unyt_units_str can not be in K energy units.",
+ ):
+ input_value = 2 * u.kcal / u.mol * u.gram**2
+ convert_kelvin_to_energy_units(
+ input_value,
+ "K",
+ )
+
+ def test_kcal_per_mol_to_string_m(self):
+ with pytest.raises(
+ ValueError,
+ match=r"ERROR: The entered energy_output_unyt_units_str value must be in units of energy/mol, "
+ r"\(length\)\**2\*\(mass\)/\(time\)\**2, but not in K energy units.",
+ ):
+ input_value = 2 * u.kcal / u.mol * u.gram**2
+ convert_kelvin_to_energy_units(
+ input_value,
+ "m",
+ )
diff --git a/gmso/utils/conversions.py b/gmso/utils/conversions.py
index efe1a1894..269e8923d 100644
--- a/gmso/utils/conversions.py
+++ b/gmso/utils/conversions.py
@@ -1,6 +1,7 @@
"""Module for standard conversions needed in molecular simulations."""
import sympy
import unyt as u
+from unyt.dimensions import length, mass, time
import gmso
from gmso.exceptions import GMSOError
@@ -137,3 +138,84 @@ def convert_ryckaert_to_opls(ryckaert_connection_type):
)
return opls_connection_type
+
+
+def convert_kelvin_to_energy_units(
+ energy_input_unyt,
+ energy_output_unyt_units_str,
+):
+ """Convert the Kelvin (K) energy unit to a standard energy unit.
+
+ Check to see if the unyt energy value is in Kelvin (K) and converts it to
+ another energy unit (Ex: kcal/mol, kJ/mol, etc.). Otherwise, it passes thru the
+ existing unyt values.
+
+ Parameters
+ ----------
+ energy_input_unyt : unyt.unyt_quantity
+ The energy in units of 'energy / other_units' Example: 'energy/mol/angstroms**2' or 'K/angstroms**2'.
+ NOTE: The only valid temperature unit for thermal energy is Kelvin (K).
+ energy_output_unyt_units_str : str (unyt valid energy units only)
+ (Example - 'kcal/mol', 'kJ/mol', or any '(length)**2*(mass)/(time)**2' , but not 'K')
+ The energy units which a Kelvin (K) energy is converted into.
+ It does not convert any energy unit if the the energy_input_unyt is not in Kelvin (K).
+ NOTE and WARNING: the energy units of kcal, kJ will be accepted due to the way the
+ Unyt module does not accept mol as a recorded unit; however, this will result in
+ incorrect results from the intended function.
+
+
+ Returns
+ -------
+ energy_output_unyt : unyt.unyt_quantity
+ If the energy_input_unyt is in Kelvin (K), it converted to the specified energy_output_unyt_units_str.
+ Otherwise, it passes through the existing unyt values.
+
+ """
+ # check for input errors
+ # if not isinstance(energy_input_unyt, type(u.unyt_quantity(1, "K"))):
+ if not isinstance(energy_input_unyt, u.unyt_quantity):
+ print_error_message = (
+ f"ERROR: The entered energy_input_unyt value is a {type(energy_input_unyt)}, "
+ f"not a {type(u.Kelvin)}."
+ )
+ raise ValueError(print_error_message)
+
+ if not isinstance(energy_output_unyt_units_str, str):
+ print_error_message = (
+ f"ERROR: The entered energy_output_unyt_units_str value is a {type(energy_output_unyt_units_str)}, "
+ f"not a {type('string')}."
+ )
+ raise ValueError(print_error_message)
+
+ # check for K energy units and convert them to normal energy units;
+ # otherwise, just pass thru the original unyt units
+ if energy_output_unyt_units_str in ["K"]:
+ print_error_message = f"ERROR: The entered energy_output_unyt_units_str can not be in K energy units."
+ raise ValueError(print_error_message)
+
+ elif (length) ** 2 * (mass) / (time) ** 2 != u.unyt_quantity(
+ 1, energy_output_unyt_units_str
+ ).units.dimensions:
+ print_error_message = (
+ f"ERROR: The entered energy_output_unyt_units_str value must be in units of energy/mol, "
+ f"(length)**2*(mass)/(time)**2, but not in K energy units."
+ )
+ raise ValueError(print_error_message)
+
+ if ("K") in str(energy_input_unyt.units) and "temperature" in str(
+ energy_input_unyt.units.dimensions
+ ):
+ K_to_energy_conversion_constant = u.unyt_quantity(1, "K").to_value(
+ energy_output_unyt_units_str, equivalence="thermal"
+ )
+ energy_output_unyt = (
+ energy_input_unyt
+ / u.Kelvin
+ * u.unyt_quantity(
+ K_to_energy_conversion_constant, energy_output_unyt_units_str
+ )
+ )
+ else:
+ energy_output_unyt = energy_input_unyt
+
+ return energy_output_unyt
From 1818e46665c135a918eaebcb83bb6536b12a6b65 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Thu, 28 Apr 2022 14:04:19 -0500
Subject: [PATCH 051/141] Feature: Add non_element_types support for atomtyping
(#648)
* Feature: Add non_element_types support for atomtyping
* Minor fixes to imports/ comments
* Address review comments, couple of more asserts
* WIP- few more asserts
* WIP- Empty branch trigger for GHA
* Fix test case
---
gmso/core/forcefield.py | 16 ++++
gmso/tests/files/non-element-type-ff.xml | 104 +++++++++++++++++++++++
gmso/tests/test_forcefield.py | 36 ++++++++
3 files changed, 156 insertions(+)
create mode 100644 gmso/tests/files/non-element-type-ff.xml
diff --git a/gmso/core/forcefield.py b/gmso/core/forcefield.py
index 774ebe239..9cae979ff 100644
--- a/gmso/core/forcefield.py
+++ b/gmso/core/forcefield.py
@@ -5,6 +5,7 @@
from lxml import etree
+from gmso.core.element import element_by_symbol
from gmso.exceptions import MissingPotentialError
from gmso.utils._constants import FF_TOKENS_SEPARATOR
from gmso.utils.ff_utils import (
@@ -106,6 +107,21 @@ def __init__(self, xml_loc=None, strict=True, greedy=True):
self.combining_rule = "geometric"
self.units = {}
+ @property
+ def non_element_types(self):
+ """Get the non-element types in the ForceField."""
+ non_element_types = set()
+
+ for name, atom_type in self.atom_types.items():
+ element_symbol = atom_type.get_tag(
+ "element"
+ ) # FixMe: Should we make this a first class citizen?
+ if element_symbol:
+ element = element_by_symbol(element_symbol)
+ non_element_types.add(element_symbol) if not element else None
+
+ return non_element_types
+
@property
def atom_class_groups(self):
"""Return a dictionary of atomClasses in the Forcefield."""
diff --git a/gmso/tests/files/non-element-type-ff.xml b/gmso/tests/files/non-element-type-ff.xml
new file mode 100644
index 000000000..f1037d34e
--- /dev/null
+++ b/gmso/tests/files/non-element-type-ff.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/tests/test_forcefield.py b/gmso/tests/test_forcefield.py
index f9e3c8e40..fb96b07e6 100644
--- a/gmso/tests/test_forcefield.py
+++ b/gmso/tests/test_forcefield.py
@@ -29,6 +29,10 @@ def opls_ethane_foyer(self):
get_path(filename=get_path("oplsaa-ethane_foyer.xml"))
)
+ @pytest.fixture(scope="session")
+ def non_element_ff(self):
+ return ForceField(get_path(filename="non-element-type-ff.xml"))
+
def test_ff_name_version_from_xml(self, ff):
assert ff.name == "ForceFieldOne"
assert ff.version == "0.4.1"
@@ -560,3 +564,35 @@ def test_get_improper_type_missing(self, opls_ethane_foyer):
opls_ethane_foyer._get_improper_type(
["opls_359", "opls_600", "opls_700", "opls_800"], warn=True
)
+
+ def test_non_element_types(self, non_element_ff, opls_ethane_foyer):
+ assert "_CH3" in non_element_ff.non_element_types
+ assert "_CH2" in non_element_ff.non_element_types
+ assert opls_ethane_foyer.non_element_types == set()
+ assert len(opls_ethane_foyer.atom_types) > 0
+
+ assert (
+ non_element_ff.get_potential(
+ group="atom_type", key="CH2_sp3"
+ ).charge
+ == 0
+ )
+ assert (
+ non_element_ff.get_potential(
+ group="atom_type", key="CH3_sp3"
+ ).charge
+ == 0
+ )
+
+ assert (
+ non_element_ff.get_potential(
+ group="atom_type", key="CH3_sp3"
+ ).definition
+ == "[_CH3;X1][_CH3,_CH2]"
+ )
+ assert (
+ non_element_ff.get_potential(
+ group="atom_type", key="CH2_sp3"
+ ).definition
+ == "[_CH2;X2]([_CH3,_CH2])[_CH3,_CH2]"
+ )
From 1cb24acf3368034367f569f08911312e893b7dd5 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Thu, 28 Apr 2022 14:53:43 -0500
Subject: [PATCH 052/141] Run CI on develop push as well
---
.github/workflows/CI.yaml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
index 9e4d67f53..0b952431c 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/CI.yaml
@@ -4,6 +4,7 @@ on:
push:
branches:
- "main"
+ - "develop"
pull_request:
branches:
- "main"
From e9f72a3044f0c197bd5315307c3377d1a6667d59 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Thu, 28 Apr 2022 15:00:16 -0500
Subject: [PATCH 053/141] Skip docker build
---
.github/workflows/CI.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
index 0b952431c..86fec1a73 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/CI.yaml
@@ -53,7 +53,7 @@ jobs:
runs-on: ubuntu-latest
needs: test
name: Build Docker Image
- if: github.event_name != 'pull_request'
+ if: ${{ false }}
steps:
- name: Set up Docker Buildx
From 39e1af4648a51b1f60805c7f77293c2630df1d56 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Fri, 29 Apr 2022 13:02:52 -0500
Subject: [PATCH 054/141] Infer elements when converting from parmed. Close
#650 (#651)
* Infer elements when converting from parmed. Close #650
* WIP- Add non-atomistic test
---
gmso/external/convert_parmed.py | 8 ++++--
gmso/tests/base_test.py | 45 +++++++++++++++++++++++++++++++
gmso/tests/test_convert_parmed.py | 12 +++++++++
3 files changed, 63 insertions(+), 2 deletions(-)
diff --git a/gmso/external/convert_parmed.py b/gmso/external/convert_parmed.py
index 579d560db..403fa4ace 100644
--- a/gmso/external/convert_parmed.py
+++ b/gmso/external/convert_parmed.py
@@ -9,6 +9,7 @@
import gmso
from gmso.core.element import (
element_by_atom_type,
+ element_by_atomic_number,
element_by_name,
element_by_symbol,
)
@@ -82,6 +83,9 @@ def from_parmed(structure, refer_type=True):
subtop_name = ("{}[{}]").format(residue.name, residue.idx)
subtops.append(gmso.SubTopology(name=subtop_name, parent=top))
for atom in residue.atoms:
+ element = (
+ element_by_atomic_number(atom.element) if atom.element else None
+ )
if refer_type and isinstance(atom.atom_type, pmd.AtomType):
site = gmso.Atom(
name=atom.name,
@@ -92,6 +96,7 @@ def from_parmed(structure, refer_type=True):
atom_type=pmd_top_atomtypes[atom.atom_type],
residue_name=residue.name,
residue_number=residue.idx,
+ element=element,
)
else:
site = gmso.Atom(
@@ -103,6 +108,7 @@ def from_parmed(structure, refer_type=True):
atom_type=None,
residue_name=residue.name,
residue_number=residue.idx,
+ element=element,
)
site_map[atom] = site
subtops[-1].add_site(site)
@@ -468,10 +474,8 @@ def to_parmed(top, refer_type=True):
for site in top.sites:
if site.element:
atomic_number = site.element.atomic_number
- charge = site.element.charge
else:
atomic_number = 0
- charge = 0
pmd_atom = pmd.Atom(
atomic_number=atomic_number,
name=site.name,
diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py
index 3db264a96..68faa73e6 100644
--- a/gmso/tests/base_test.py
+++ b/gmso/tests/base_test.py
@@ -500,3 +500,48 @@ def residue_top(self):
top.update_topology()
return top
+
+ @pytest.fixture(scope="session")
+ def pentane_ua_mbuild(self):
+ class PentaneUA(mb.Compound):
+ """Create a united-atom pentane compound."""
+
+ def __init__(self):
+ super(PentaneUA, self).__init__()
+ # Calculate the angle between the two ports
+ angle = np.deg2rad(114)
+ x = 0
+ y = 0.077
+ z = 0
+ vec = [
+ x,
+ y * np.cos(angle) - z * np.sin(angle),
+ y * np.sin(angle) + z * np.cos(angle),
+ ]
+ # Create the end group compound
+ ch3 = mb.Compound()
+ ch3.add(mb.Particle(name="_CH3"))
+ ch3.add(mb.Port(anchor=ch3[0]), "up")
+ ch3["up"].translate([x, y, z])
+ # Create the internal monomer
+ ch2 = mb.Compound()
+ ch2.add(mb.Particle(name="_CH2"))
+ ch2.add(mb.Port(anchor=ch2[0]), "up")
+ ch2["up"].translate([x, y, z])
+ ch2.add(mb.Port(anchor=ch2[0], orientation=vec), "down")
+ ch2["down"].translate(vec)
+ pentane = mb.recipes.Polymer(
+ monomers=[ch2], end_groups=[ch3, mb.clone(ch3)]
+ )
+ pentane.build(n=3)
+ self.add(pentane, label="PNT")
+
+ return PentaneUA()
+
+ @pytest.fixture(scope="session")
+ def pentane_ua_parmed(self, pentane_ua_mbuild):
+ return mb.conversion.to_parmed(pentane_ua_mbuild)
+
+ @pytest.fixture(scope="session")
+ def pentane_ua_gmso(self, pentane_ua_mbuild):
+ return from_mbuild(pentane_ua_mbuild)
diff --git a/gmso/tests/test_convert_parmed.py b/gmso/tests/test_convert_parmed.py
index e367da890..e6fec88c8 100644
--- a/gmso/tests/test_convert_parmed.py
+++ b/gmso/tests/test_convert_parmed.py
@@ -323,3 +323,15 @@ def test_from_parmed_member_types(self):
]:
for potential in potential_types:
assert potential.member_types
+
+ def test_parmed_element(self):
+ struc = pmd.load_file(get_fn("ethane.top"), xyz=get_fn("ethane.gro"))
+ top = from_parmed(struc)
+ for gmso_atom, pmd_atom in zip(top.sites, struc.atoms):
+ assert gmso_atom.element.atomic_number == pmd_atom.element
+
+ def test_parmed_element_non_atomistic(self, pentane_ua_parmed):
+ top = from_parmed(pentane_ua_parmed)
+ for gmso_atom, pmd_atom in zip(top.sites, pentane_ua_parmed.atoms):
+ assert gmso_atom.element is None
+ assert pmd_atom.element == 0
From ca477cbc3b7cbfd1244c4fd29c5dd32c90e31255 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Mon, 2 May 2022 10:48:13 -0500
Subject: [PATCH 055/141] Update method to load in mol2 file (#645)
* Update method to load in mol2 file
Read in the whole file at the beginning then parse,
to avoid potential race condition (?) of the current method,
which sometime reads in the bond information incorrectly.
* add Cal review, fix a few typo
* remove unused kwargs
* add patch for mamba issue
* add patch for mamba issue
* fix minor bugs
---
azure-pipelines.yml | 135 ++++++++++++++++++++++++
gmso/formats/mol2.py | 225 +++++++++++++++++-----------------------
gmso/tests/test_mol2.py | 6 +-
3 files changed, 231 insertions(+), 135 deletions(-)
create mode 100644 azure-pipelines.yml
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 000000000..89430faf2
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,135 @@
+trigger:
+ branches:
+ include:
+ - master
+ tags:
+ include:
+ - 0.*
+
+pr:
+ autoCancel: true
+ branches:
+ include:
+ - master
+
+schedules:
+- cron: "0 0 * * *"
+ displayName: Daily midnight build for master
+ branches:
+ include:
+ - master
+ always: true
+
+stages:
+ - stage: Test
+ jobs:
+ - job: TestsForGMSO
+ strategy:
+ matrix:
+ Python37Ubuntu:
+ imageName: 'ubuntu-latest'
+ python.version: 3.7
+ Python38Ubuntu:
+ imageName: 'ubuntu-latest'
+ python.version: 3.8
+ Python39Ubuntu:
+ imageName: 'ubuntu-latest'
+ python.version: 3.9
+ Python37macOS:
+ imageName: 'macOS-latest'
+ python.version: 3.7
+ Python38macOS:
+ imageName: 'macOS-latest'
+ python.version: 3.8
+ Python39macOS:
+ imageName: 'macOS-latest'
+ python.version: 3.9
+
+ pool:
+ vmImage: $(imageName)
+
+ steps:
+ - bash: echo "##vso[task.prependpath]$CONDA/bin"
+ displayName: Add conda to path
+
+ - bash: sudo chown -R $USER $CONDA
+ condition: eq( variables['Agent.OS'], 'Darwin' )
+ displayName: Take ownership of conda installation
+
+ - bash: |
+ conda config --set always_yes yes --set changeps1 no
+ displayName: Set conda configuration
+
+ - bash: |
+ conda install -y -c conda-forge mamba
+ displayName: Install mamba
+
+ - bash: |
+ rm /usr/share/miniconda/pkgs/cache/*.json
+ mamba update conda -yq
+ displayName: Add relevant channels and update
+
+ - bash: |
+ sed -i -E 's/python.*$/python='$(python.version)'/' environment-dev.yml
+ mamba env create -f environment-dev.yml
+ source activate gmso-dev
+ pip install -e .
+ displayName: Install requirements and testing branch
+
+ - bash: |
+ source activate gmso-dev
+ pip install pytest-azurepipelines
+ python -m pytest -v --cov=gmso --cov-report=html --color=yes --pyargs gmso --no-coverage-upload
+ displayName: Run tests
+
+ - bash: |
+ source activate gmso-dev
+ curl -Os https://uploader.codecov.io/latest/linux/codecov
+ chmod +x codecov
+ ./codecov -t ${CODECOV_UPLOAD_TOKEN} -C $(Build.SourceVersion)
+ condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['python.version'], '3.7' ) )
+ displayName: Upload coverage report to codecov.io
+ env:
+ CODECOV_UPLOAD_TOKEN: $(codecovUploadToken)
+
+ - task: PublishCodeCoverageResults@1
+ inputs:
+ codeCoverageTool: Cobertura
+ summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
+ reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov'
+ condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['python.version'], '3.7' ) )
+ displayName: Publish coverage report to Azure dashboard
+
+ - stage: Docker
+ dependsOn: Test
+ condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), startsWith(variables['Build.SourceBranch'], 'refs/tags/0')), ne(variables['Build.Reason'], 'Schedule'))
+ pool:
+ vmImage: 'ubuntu-latest'
+ jobs:
+ - job: publishDocker
+ steps:
+ - bash: |
+ if [[ $BUILD_SOURCEBRANCH == "refs/heads/master" ]]; then TAG='latest'; else TAG='stable'; fi
+ if [[ $BUILD_SOURCEBRANCH != "refs/heads/master" ]]; then VERSION=$(Build.SourceBranch); fi;
+ echo "##vso[task.setvariable variable=VERSION;]${VERSION:10}"
+ echo "##vso[task.setvariable variable=DOCKER_TAG;]$TAG"
+ displayName: Export Docker Tags
+ - task: Docker@2
+ displayName: Login to docker hub
+ inputs:
+ command: login
+ containerRegistry: mosdefDockerLogin
+
+ - task: Docker@2
+ displayName: Build and Push
+ inputs:
+ command: buildAndPush
+ repository: mosdef/gmso
+ tags: |
+ $(DOCKER_TAG)
+ $(VERSION)
+ - task: Docker@2
+ displayName: Logout
+ inputs:
+ command: logout
+ containerRegistry: mosdefDockerLogin
diff --git a/gmso/formats/mol2.py b/gmso/formats/mol2.py
index cebe86c76..de2af9543 100644
--- a/gmso/formats/mol2.py
+++ b/gmso/formats/mol2.py
@@ -33,6 +33,7 @@ def from_mol2(filename, site_type="atom"):
Notes
-----
+ The position of atom in the mol2 file is assumed to be in Angstrom.
It may be common to want to create an mBuild compound from a mol2 file. This is possible
by installing [mBuild](https://mbuild.mosdef.org/en/stable/index.html)
and converting using the following python code:
@@ -49,167 +50,127 @@ def from_mol2(filename, site_type="atom"):
# Initialize topology
topology = Topology(name=mol2path.stem)
# save the name from the filename
- with open(mol2path, "r") as f:
- line = f.readline()
- while f:
- # check for header character in line
- if line.strip().startswith("@"):
- # if header character in line, send to a function that will direct it properly
- line = _parse_record_type_indicator(
- f, line, topology, site_type
- )
- elif line == "":
- # check for the end of file
- break
- else:
- # else, skip to next line
- line = f.readline()
+ with open(filename, "r") as f:
+ fcontents = f.readlines()
+
+ sections = {"Meta": list()}
+ section_key = "Meta" # Used to parse the meta info at top of the file
+ for line in fcontents:
+ if "@" in line:
+ section_key = line.strip("\n")
+ sections[section_key] = list()
+ else:
+ sections[section_key].append(line)
+
+ _parse_site = _parse_lj if site_type == "lj" else _parse_atom
+ supported_rti = {
+ "@ATOM": _parse_site,
+ "@BOND": _parse_bond,
+ "@CRYSIN": _parse_box,
+ "@FF_PBC": _parse_box,
+ }
+ for section in sections:
+ if section not in supported_rti:
+ warnings.warn(
+ f"The record type indicator {section} is not supported. "
+ "Skipping current section and moving to the next RTI header."
+ )
+ else:
+ supported_rti[section](topology, sections[section])
+
topology.update_topology()
# TODO: read in parameters to correct attribute as well. This can be saved in various rti sections.
return topology
-def _load_top_sites(f, topology, site_type="atom"):
- """Take a mol2 file section with the heading 'ATOM' and save to the topology.sites attribute.
+def _parse_lj(top, section):
+ """Parse atom of lj style from mol2 file."""
+ for line in section:
+ if line.strip():
+ content = line.split()
+ position = [float(x) for x in content[2:5]] * u.Å
- Parameters
- ----------
- f : file pointer
- pointer file where the mol2 file is stored. The pointer must be at the head of the rti for that
- `@ATOM` section.
- topology : gmso.Topology
- topology to save the site information to.
- site_type : string ('atom' or 'lj'), default='atom'
- tells the reader to consider the elements saved in the mol2 file, and
- if the type is 'lj', to not try to identify the element of the site,
- instead saving the site name.
+ try:
+ charge = float(content[8])
+ except IndexError:
+ warnings.warn(
+ f"No charge was detected for site {content[1]} with index {content[0]}"
+ )
- Returns
- -------
- line : string
- returns the last line of the `@ATOM` section, and this is where the file pointer (`f`)
- will now point to.
+ atom = Atom(
+ name=content[1],
+ position=position.to("nm"),
+ charge=charge,
+ residue_name=content[7],
+ residue_number=int(content[6]),
+ )
+ top.add_site(atom)
- Notes
- -----
- Will modify the topology in place with the relevant site information. Indices will be appended to any
- current site information.
- """
- while True:
- line = f.readline()
- if _is_end_of_rti(line):
- line = line.split()
- position = [float(x) for x in line[2:5]] * u.Å
- # TODO: make sure charges are also saved as a unyt value
- # TODO: add validation for element names
- if site_type == "lj":
- element = None
- elif element_by_symbol(line[5]):
- element = element_by_symbol(line[5])
- elif element_by_name(line[5]):
- element = element_by_name(line[5])
- else:
+def _parse_atom(top, section):
+ """Parse atom information from the mol2 file."""
+ parse_ele = (
+ lambda ele: element_by_symbol(ele)
+ if element_by_symbol(ele)
+ else element_by_name(ele)
+ )
+
+ for line in section:
+ if line.strip():
+ content = line.split()
+ position = [float(x) for x in content[2:5]] * u.Å
+ element = parse_ele(content[5])
+
+ if not element:
warnings.warn(
- "No element detected for site {} with index{}, consider manually adding the element to the topology".format(
- line[1], len(topology.sites) + 1
- )
+ f"No element detected for site {content[1]} with index {content[0]}, "
+ "consider manually adding the element to the topology"
)
- element = None
+
try:
- charge = float(line[8])
+ charge = float(content[8])
except IndexError:
warnings.warn(
- "No charges were detected for site {} with index {}".format(
- line[1], line[0]
- )
+ f"No charge was detected for site {content[1]} with index {content[0]}"
)
charge = None
+
atom = Atom(
- name=line[1],
+ name=content[1],
position=position.to("nm"),
- charge=charge,
element=element,
- residue_name=line[7],
- residue_number=int(line[6]),
+ charge=charge,
+ residue_name=content[7],
+ residue_number=int(content[6]),
)
- topology.add_site(atom)
- else:
- break
- return line
+ top.add_site(atom)
-def _load_top_bonds(f, topology, **kwargs):
- """Take a mol2 file section with the heading '@BOND' and save to the topology.bonds attribute."""
- while True:
- line = f.readline()
- if _is_end_of_rti(line):
- line = line.split()
+def _parse_bond(top, section):
+ """Parse bond information from the mol2 file."""
+ for line in section:
+ if line.strip():
+ content = line.split()
bond = Bond(
connection_members=(
- topology.sites[int(line[1]) - 1],
- topology.sites[int(line[2]) - 1],
+ top.sites[int(content[1]) - 1],
+ top.sites[int(content[2]) - 1],
)
)
- topology.add_connection(bond)
- else:
- break
- return line
+ top.add_connection(bond)
-def _load_top_box(f, topology, **kwargs):
- """Take a mol2 file section with the heading '@FF_PBC' or '@CRYSIN' and save to topology.box."""
- if topology.box:
+def _parse_box(top, section):
+ """Parse box information from the mol2 file."""
+ if top.box:
warnings.warn(
- "This mol2 file has two boxes to be read in, only reading in one with dimensions {}".format(
- topology.box
- )
+ f"This mol2 file has two boxes to be read in, only reading in one with dimensions {top.box}"
)
- line = f.readline()
- return line
- while True:
- line = f.readline()
- if _is_end_of_rti(line):
- line = line.split()
- # TODO: write to box information
- topology.box = Box(
- lengths=[float(x) for x in line[0:3]] * u.Å,
- angles=[float(x) for x in line[3:6]] * u.degree,
- )
- else:
- break
- return line
-
-def _parse_record_type_indicator(f, line, topology, site_type):
- """Take a specific record type indicator (RTI) from a mol2 file format and save to the proper attribute of a gmso topology.
-
- Supported record type indicators include Atom, Bond, FF_PBC, and CRYSIN.
- """
- supported_rti = {
- "@ATOM": _load_top_sites,
- "@BOND": _load_top_bonds,
- "@CRYSIN": _load_top_box,
- "@FF_PBC": _load_top_box,
- }
- # read in to atom attribute
- try:
- line = supported_rti[line.strip()](f, topology, site_type=site_type)
- except KeyError:
- warnings.warn(
- "The record type indicator {} is not supported. Skipping current section and moving to the next RTI header.".format(
- line
+ for line in section:
+ if line.strip():
+ content = line.split()
+ top.box = Box(
+ lengths=[float(x) for x in content[0:3]] * u.Å,
+ angles=[float(x) for x in content[3:6]] * u.degree,
)
- )
- line = f.readline()
- return line
-
-
-def _is_end_of_rti(line):
- """Check if line in an rti is at the end of the section."""
- return (
- line
- and "@" not in line
- and not line == "\n"
- and not line.strip().startswith("#")
- )
diff --git a/gmso/tests/test_mol2.py b/gmso/tests/test_mol2.py
index 1a50d695d..44197d394 100644
--- a/gmso/tests/test_mol2.py
+++ b/gmso/tests/test_mol2.py
@@ -56,14 +56,14 @@ def test_read_mol2(self):
with pytest.warns(
UserWarning,
- match=r"No charges were detected for site C with index 1",
+ match=r"No charge was detected for site C with index 1",
):
top = Topology.load(get_fn("ethane.mol2"))
assert list(top.sites)[0].charge is None
with pytest.warns(
UserWarning,
- match=r"No element detected for site C with index1\, consider manually adding the element to the topology",
+ match=r"No element detected for site C with index 1, consider manually adding the element to the topology",
):
Topology.load(get_fn("benzene.mol2"))
@@ -114,7 +114,7 @@ def test_wrong_path(self):
def test_broken_files(self):
with pytest.warns(
UserWarning,
- match=r"The record type indicator @MOLECULE_extra_text\n is not supported. Skipping current section and moving to the next RTI header.",
+ match=r"The record type indicator @MOLECULE_extra_text is not supported. Skipping current section and moving to the next RTI header.",
):
Topology.load(get_fn("broken.mol2"))
with pytest.warns(
From ff298a912082d6dc68e3ddd57c8c585e1b191416 Mon Sep 17 00:00:00 2001
From: Co Quach
Date: Thu, 5 May 2022 17:06:59 +0700
Subject: [PATCH 056/141] Remove azure-pipelines.yml which was accidentally
added
---
azure-pipelines.yml | 135 --------------------------------------------
1 file changed, 135 deletions(-)
delete mode 100644 azure-pipelines.yml
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index 89430faf2..000000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,135 +0,0 @@
-trigger:
- branches:
- include:
- - master
- tags:
- include:
- - 0.*
-
-pr:
- autoCancel: true
- branches:
- include:
- - master
-
-schedules:
-- cron: "0 0 * * *"
- displayName: Daily midnight build for master
- branches:
- include:
- - master
- always: true
-
-stages:
- - stage: Test
- jobs:
- - job: TestsForGMSO
- strategy:
- matrix:
- Python37Ubuntu:
- imageName: 'ubuntu-latest'
- python.version: 3.7
- Python38Ubuntu:
- imageName: 'ubuntu-latest'
- python.version: 3.8
- Python39Ubuntu:
- imageName: 'ubuntu-latest'
- python.version: 3.9
- Python37macOS:
- imageName: 'macOS-latest'
- python.version: 3.7
- Python38macOS:
- imageName: 'macOS-latest'
- python.version: 3.8
- Python39macOS:
- imageName: 'macOS-latest'
- python.version: 3.9
-
- pool:
- vmImage: $(imageName)
-
- steps:
- - bash: echo "##vso[task.prependpath]$CONDA/bin"
- displayName: Add conda to path
-
- - bash: sudo chown -R $USER $CONDA
- condition: eq( variables['Agent.OS'], 'Darwin' )
- displayName: Take ownership of conda installation
-
- - bash: |
- conda config --set always_yes yes --set changeps1 no
- displayName: Set conda configuration
-
- - bash: |
- conda install -y -c conda-forge mamba
- displayName: Install mamba
-
- - bash: |
- rm /usr/share/miniconda/pkgs/cache/*.json
- mamba update conda -yq
- displayName: Add relevant channels and update
-
- - bash: |
- sed -i -E 's/python.*$/python='$(python.version)'/' environment-dev.yml
- mamba env create -f environment-dev.yml
- source activate gmso-dev
- pip install -e .
- displayName: Install requirements and testing branch
-
- - bash: |
- source activate gmso-dev
- pip install pytest-azurepipelines
- python -m pytest -v --cov=gmso --cov-report=html --color=yes --pyargs gmso --no-coverage-upload
- displayName: Run tests
-
- - bash: |
- source activate gmso-dev
- curl -Os https://uploader.codecov.io/latest/linux/codecov
- chmod +x codecov
- ./codecov -t ${CODECOV_UPLOAD_TOKEN} -C $(Build.SourceVersion)
- condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['python.version'], '3.7' ) )
- displayName: Upload coverage report to codecov.io
- env:
- CODECOV_UPLOAD_TOKEN: $(codecovUploadToken)
-
- - task: PublishCodeCoverageResults@1
- inputs:
- codeCoverageTool: Cobertura
- summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
- reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov'
- condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['python.version'], '3.7' ) )
- displayName: Publish coverage report to Azure dashboard
-
- - stage: Docker
- dependsOn: Test
- condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), startsWith(variables['Build.SourceBranch'], 'refs/tags/0')), ne(variables['Build.Reason'], 'Schedule'))
- pool:
- vmImage: 'ubuntu-latest'
- jobs:
- - job: publishDocker
- steps:
- - bash: |
- if [[ $BUILD_SOURCEBRANCH == "refs/heads/master" ]]; then TAG='latest'; else TAG='stable'; fi
- if [[ $BUILD_SOURCEBRANCH != "refs/heads/master" ]]; then VERSION=$(Build.SourceBranch); fi;
- echo "##vso[task.setvariable variable=VERSION;]${VERSION:10}"
- echo "##vso[task.setvariable variable=DOCKER_TAG;]$TAG"
- displayName: Export Docker Tags
- - task: Docker@2
- displayName: Login to docker hub
- inputs:
- command: login
- containerRegistry: mosdefDockerLogin
-
- - task: Docker@2
- displayName: Build and Push
- inputs:
- command: buildAndPush
- repository: mosdef/gmso
- tags: |
- $(DOCKER_TAG)
- $(VERSION)
- - task: Docker@2
- displayName: Logout
- inputs:
- command: logout
- containerRegistry: mosdefDockerLogin
From 3a387c707109e64b5f178591249db94a134d2ac4 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Tue, 10 May 2022 08:44:26 -0500
Subject: [PATCH 057/141] Assign None to charge if not found in mol2 file
(#653)
---
gmso/formats/mol2.py | 1 +
gmso/tests/files/methane_missing_charge.mol2 | 12 ++++++++++++
gmso/tests/test_mol2.py | 10 ++++++++++
3 files changed, 23 insertions(+)
create mode 100644 gmso/tests/files/methane_missing_charge.mol2
diff --git a/gmso/formats/mol2.py b/gmso/formats/mol2.py
index de2af9543..7846c1133 100644
--- a/gmso/formats/mol2.py
+++ b/gmso/formats/mol2.py
@@ -96,6 +96,7 @@ def _parse_lj(top, section):
warnings.warn(
f"No charge was detected for site {content[1]} with index {content[0]}"
)
+ charge = None
atom = Atom(
name=content[1],
diff --git a/gmso/tests/files/methane_missing_charge.mol2 b/gmso/tests/files/methane_missing_charge.mol2
new file mode 100644
index 000000000..e67dfb74a
--- /dev/null
+++ b/gmso/tests/files/methane_missing_charge.mol2
@@ -0,0 +1,12 @@
+@MOLECULE
+RES
+1 0 1 0 1
+SMALL
+NO_CHARGES
+@CRYSIN
+ 5.0000 5.0000 5.0000 90.0000 90.0000 90.0000 1 1
+@ATOM
+ 1 _CH4 0.0000 0.0000 0.0000 CH4 1 RES
+@BOND
+@SUBSTRUCTURE
+ 1 RES 1 RESIDUE 0 **** ROOT 0
diff --git a/gmso/tests/test_mol2.py b/gmso/tests/test_mol2.py
index 44197d394..f00a1dc1e 100644
--- a/gmso/tests/test_mol2.py
+++ b/gmso/tests/test_mol2.py
@@ -6,6 +6,7 @@
from gmso import Topology
from gmso.formats.mol2 import from_mol2
from gmso.tests.base_test import BaseTest
+from gmso.tests.utils import get_path
from gmso.utils.io import get_fn
@@ -102,6 +103,15 @@ def test_lj_system(self):
top = Topology.load(get_fn("methane.mol2"), site_type="lj")
assert np.all([site.element == None for site in top.sites])
+ def test_no_charge_lj(self):
+ with pytest.warns(
+ UserWarning,
+ match="No charge was detected for site .* with index \d+$",
+ ):
+ top = Topology.load(
+ get_path("methane_missing_charge.mol2"), site_type="lj"
+ )
+
def test_wrong_path(self):
with pytest.raises(
OSError, match=r"Provided path to file that does not exist"
From f81b2a9ab2f0f5a0a2103ae0ab515ded9ffe5661 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 11 May 2022 10:29:35 -0500
Subject: [PATCH 058/141] Add view classes for handling potentials in a
topology (#649)
---
gmso/core/topology.py | 416 ++++++++++++++++++++++++++---------
gmso/core/views.py | 202 +++++++++++++++++
gmso/formats/json.py | 10 +-
gmso/tests/test_atom_type.py | 2 +-
gmso/tests/test_mcf.py | 4 +-
gmso/tests/test_top.py | 4 +-
gmso/tests/test_topology.py | 22 +-
gmso/tests/test_views.py | 140 ++++++++++++
8 files changed, 673 insertions(+), 127 deletions(-)
create mode 100644 gmso/core/views.py
create mode 100644 gmso/tests/test_views.py
diff --git a/gmso/core/topology.py b/gmso/core/topology.py
index 12fec0f81..6684da345 100644
--- a/gmso/core/topology.py
+++ b/gmso/core/topology.py
@@ -1,4 +1,5 @@
"""Base data structure for GMSO chemical systems."""
+import itertools
import warnings
from pathlib import Path
@@ -18,7 +19,7 @@
from gmso.core.improper import Improper
from gmso.core.improper_type import ImproperType
from gmso.core.pairpotential_type import PairPotentialType
-from gmso.core.parametric_potential import ParametricPotential
+from gmso.core.views import TopologyPotentialView
from gmso.exceptions import GMSOError
from gmso.utils.connectivity import (
identify_connections as _identify_connections,
@@ -150,12 +151,6 @@ def __init__(self, name="Topology", box=None):
self._dihedrals = IndexedSet()
self._impropers = IndexedSet()
self._subtops = IndexedSet()
- self._atom_types = IndexedSet()
- self._connection_types = IndexedSet()
- self._bond_types = IndexedSet()
- self._angle_types = IndexedSet()
- self._dihedral_types = IndexedSet()
- self._improper_types = IndexedSet()
self._combining_rule = "lorentz"
self._pairpotential_types = IndexedSet()
self._scaling_factors = {
@@ -166,6 +161,15 @@ def __init__(self, name="Topology", box=None):
"electrostatics13Scale": 0.0,
"electrostatics14Scale": 0.5,
}
+ self.is_updated = True
+ self._potentials_count = {
+ "atom_types": 0,
+ "bond_types": 0,
+ "angle_types": 0,
+ "dihedral_types": 0,
+ "improper_types": 0,
+ "pairpotential_types": 0,
+ }
self._unique_connections = {}
@@ -320,33 +324,276 @@ def impropers(self):
@property
def atom_types(self):
- """Return all atom_types in the topology."""
- return self._atom_types
+ """Return all atom_types in the topology.
+
+ Notes
+ -----
+ This returns a TopologyPotentialView object which can be used as
+ an iterator. By default, this will return a view with all the atom_types
+ in the topology (if multiple sites point to the same atom_type, only a
+ single reference is returned/iterated upon). Use, different filters(builtin or custom) to suit your needs.
+ See examples below.
+
+ Examples
+ --------
+ >>> from gmso.core.atom import Atom
+ >>> from gmso.core.atom_type import AtomType
+ >>> from gmso.core.topology import Topology
+ >>> from gmso.core.views import PotentialFilters
+ >>> top = Topology(name="my_top")
+ >>> atom_type = AtomType(name="my_atom_type")
+ >>> for j in range(100):
+ ... atom = Atom(name=f"atom_{j+1}")
+ ... atom.atom_type = atom_type
+ ... top.add_site(atom)
+ >>> len(top.atom_types)
+ 1
+ >>> len(top.atom_types(filter_by=PotentialFilters.REPEAT_DUPLICATES))
+ 100
+ >>> len(top.atom_types(filter_by=PotentialFilters.UNIQUE_NAME_CLASS))
+ 1
+
+ See Also
+ --------
+ gmso.core.views.TopologyPotentialView
+ An iterator/filter based view of Potentials in a gmso Topology.
+
+ gmso.core.views.PotentialFilters
+ Builtin filters for viewing potentials in a Topology.
+
+ Returns
+ -------
+ gmso.core.views.TopologyPotentialView
+ An iterator of the atom_types in the system filtered according to the
+ filter function supplied.
+ """
+ return TopologyPotentialView(self._sites)
@property
def connection_types(self):
- """Return all connection_types in the topology."""
- return self._connection_types
+ """Return all connection_types in the topology.
+
+ Notes
+ -----
+ This returns a TopologyPotentialView object which can be used as
+ an iterator.
+
+ See Also
+ --------
+ gmso.core.views.TopologyPotentialView
+ An iterator/filter based view of Potentials in a gmso Topology.
+ """
+
+ return TopologyPotentialView(
+ itertools.chain(
+ self.bonds, self.angles, self.dihedrals, self.impropers
+ )
+ )
@property
def bond_types(self):
- """Return all bond_types in the topology."""
- return self._bond_types
+ """Return all bond_types in the topology.
+
+ Notes
+ -----
+ This returns a TopologyPotentialView object which can be used as
+ an iterator.By default, this will return a view with all the bond_types
+ in the topology (if multiple bonds point to the same bond_type, only a
+ single reference is returned/iterated upon). Use, different filters(builtin or custom) to suit your needs.
+ See examples below.
+
+ Examples
+ --------
+ >>> from gmso.core.atom import Atom
+ >>> from gmso.core.bond import Bond
+ >>> from gmso.core.bond_type import BondType
+ >>> from gmso.core.topology import Topology
+ >>> from gmso.core.views import PotentialFilters
+ >>> top = Topology(name="my_top")
+ >>> for j in range(100):
+ ... atom1 = Atom(name=f"atom_A_{j+1}")
+ ... atom2 = Atom(name=f"atom_B_{j+1}")
+ ... bond = Bond(connection_members=[atom1, atom2])
+ ... bond.bond_type = BondType(name=f"bond_type", member_types=('atom_A', 'atom_B'))
+ ... conn = top.add_connection(bond)
+ >>> len(top.bond_types)
+ 100
+ >>> len(top.bond_types(filter_by=PotentialFilters.UNIQUE_ID))
+ 100
+ >>> len(top.bond_types(filter_by=PotentialFilters.UNIQUE_NAME_CLASS))
+ 1
+
+ See Also
+ --------
+ gmso.core.views.TopologyPotentialView
+ An iterator/filter based view of Potentials in a gmso Topology.
+
+ gmso.core.views.PotentialFilters
+ Builtin filters for viewing potentials in a Topology.
+
+ Returns
+ -------
+ gmso.core.views.TopologyPotentialView
+ An iterator of the bond_types in the system filtered according to the
+ filter function supplied.
+ """
+ return TopologyPotentialView(self._bonds)
@property
def angle_types(self):
- """Return all angle_types in the topology."""
- return self._angle_types
+ """Return all angle_types in the topology.
+
+ Notes
+ -----
+ This returns a TopologyPotentialView object which can be used as
+ an iterator. By default, this will return a view with all the angle_types
+ in the topology (if multiple angles point to the same angle_type, only a
+ single reference is returned/iterated upon). Use, different filters(builtin or custom) to suit
+ your needs. See examples below.
+
+ Examples
+ --------
+ >>> from gmso.core.atom import Atom
+ >>> from gmso.core.angle import Angle
+ >>> from gmso.core.angle_type import AngleType
+ >>> from gmso.core.topology import Topology
+ >>> from gmso.core.views import PotentialFilters
+ >>> for j in range(100):
+ ... atom1 = Atom(name=f"atom_A_{j+1}")
+ ... atom2 = Atom(name=f"atom_B_{j+1}")
+ ... atom3 = Atom(name=f"atom_C_{j+1}")
+ ... angle = Angle(connection_members=[atom1, atom2, atom3])
+ ... angle.angle_type = AngleType(name=f"angle_type", member_types=('atom_A', 'atom_B', 'atom_C'))
+ ... conn = top.add_connection(angle)
+ >>> len(top.angle_types)
+ 100
+ >>> len(top.angle_types(filter_by=PotentialFilters.UNIQUE_ID))
+ 100
+ >>> len(top.angle_types(filter_by=PotentialFilters.UNIQUE_NAME_CLASS))
+ 1
+
+
+ See Also
+ --------
+ gmso.core.views.TopologyPotentialView
+ An iterator/filter based view of Potentials in a gmso Topology.
+
+ gmso.core.views.PotentialFilters
+ Builtin filters for viewing potentials in a Topology.
+
+ Returns
+ -------
+ gmso.core.views.TopologyPotentialView
+ An iterator of the angle_types in the system filtered according to the
+ filter function supplied.
+ """
+ return TopologyPotentialView(self._angles)
@property
def dihedral_types(self):
- """Return all dihedral_types in the topology."""
- return self._dihedral_types
+ """Return all dihedral_types in the topology.
+
+ Notes
+ -----
+ This returns a TopologyPotentialView object which can be used as
+ an iterator. By default, this will return a view with all the dihedral_types
+ in the topology (if multiple dihedrals point to the same dihedral types, only a
+ single reference is returned/iterated upon). Use, different filters(builtin or custom)
+ to suit your needs. See examples below.
+
+ Examples
+ --------
+ >>> from gmso.core.atom import Atom
+ >>> from gmso.core.dihedral import Dihedral
+ >>> from gmso.core.dihedral_type import DihedralType
+ >>> from gmso.core.topology import Topology
+ >>> from gmso.core.views import PotentialFilters
+ >>> for j in range(100):
+ ... atom1 = Atom(name=f"atom_A_{j+1}")
+ ... atom2 = Atom(name=f"atom_B_{j+1}")
+ ... atom3 = Atom(name=f"atom_C_{j+1}")
+ ... atom4 = Atom(name=f"atom_D_{j+1}")
+ ... dihedral = Dihedral(connection_members=[atom1, atom2, atom3, atom4])
+ ... dihedral.dihedral_type = DihedralType(
+ ... name=f"dihedral_type",
+ ... member_types=('atom_A', 'atom_B', 'atom_C', 'atom_D')
+ ... )
+ ... conn = top.add_connection(dihedral)
+ >>> len(top.dihedral_types)
+ 100
+ >>> len(top.dihedral_types(filter_by=PotentialFilters.UNIQUE_ID))
+ 100
+ >>> len(top.dihedral_types(filter_by=PotentialFilters.UNIQUE_NAME_CLASS))
+ 1
+
+ See Also
+ --------
+ gmso.core.views.TopologyPotentialView
+ An iterator/filter based view of Potentials in a gmso Topology.
+
+ gmso.core.views.PotentialFilters
+ Builtin filters for viewing potentials in a Topology.
+
+ Returns
+ -------
+ gmso.core.views.TopologyPotentialView
+ An iterator of the dihedral_types in the system filtered according to the
+ filter function supplied.
+ """
+ return TopologyPotentialView(self._dihedrals)
@property
def improper_types(self):
- """Return all improper_types in the topology."""
- return self._improper_types
+ """Return all improper_types in the topology.
+
+ Notes
+ -----
+ This returns a TopologyPotentialView object which can be used as
+ an iterator. By default, this will return a view with all the improper_types
+ in the topology (if multiple impropers point to the same improper_type, only a
+ single reference is returned/iterated upon). Use, different filters(builtin or custom) to
+ suit your needs. See examples below.
+
+ Examples
+ --------
+ >>> from gmso.core.atom import Atom
+ >>> from gmso.core.improper import Improper
+ >>> from gmso.core.improper_type import ImproperType
+ >>> from gmso.core.topology import Topology
+ >>> from gmso.core.views import PotentialFilters
+ >>> for j in range(100):
+ ... atom1 = Atom(name=f"atom_A_{j+1}")
+ ... atom2 = Atom(name=f"atom_B_{j+1}")
+ ... atom3 = Atom(name=f"atom_C_{j+1}")
+ ... atom4 = Atom(name=f"atom_D_{j+1}")
+ ... improper = Improper(connection_members=[atom1, atom2, atom3, atom4])
+ ... improper.improper_type = ImproperType(
+ ... name=f"dihedral_type",
+ ... member_types=('atom_A', 'atom_B', 'atom_C', 'atom_D')
+ ... )
+ ... conn = top.add_connection(improper)
+ >>> len(top.improper_types)
+ 100
+ >>> len(top.improper_types(filter_by=PotentialFilters.UNIQUE_ID))
+ 100
+ >>> len(top.improper_types(filter_by=PotentialFilters.UNIQUE_NAME_CLASS))
+ 1
+
+ See Also
+ --------
+ gmso.core.views.TopologyPotentialView
+ An iterator/filter based view of Potentials in a gmso Topology.
+
+ gmso.core.views.PotentialFilters
+ Builtin filters for viewing potentials in a Topology.
+
+ Returns
+ -------
+ gmso.core.views.TopologyPotentialView
+ An iterator of the dihedral_types in the system filtered according to the
+ filter function supplied.
+ """
+ return TopologyPotentialView(self._impropers)
@property
def pairpotential_types(self):
@@ -355,34 +602,34 @@ def pairpotential_types(self):
@property
def atom_type_expressions(self):
"""Return all atom_type expressions in the topology."""
- return list(set([atype.expression for atype in self._atom_types]))
+ return list(set([atype.expression for atype in self.atom_types]))
@property
def connection_type_expressions(self):
"""Return all connection_type expressions in the topology."""
return list(
- set([contype.expression for contype in self._connection_types])
+ set([contype.expression for contype in self.connection_types])
)
@property
def bond_type_expressions(self):
"""Return all bond_type expressions in the topology."""
- return list(set([btype.expression for btype in self._bond_types]))
+ return list(set([btype.expression for btype in self.bond_types]))
@property
def angle_type_expressions(self):
"""Return all angle_type expressions in the topology."""
- return list(set([atype.expression for atype in self._angle_types]))
+ return list(set([atype.expression for atype in self.angle_types]))
@property
def dihedral_type_expressions(self):
"""Return all dihedral_type expressions in the topology."""
- return list(set([dtype.expression for dtype in self._dihedral_types]))
+ return list(set([dtype.expression for dtype in self.dihedral_types]))
@property
def improper_type_expressions(self):
"""Return all improper_type expressions in the topology."""
- return list(set([itype.expression for itype in self._improper_types]))
+ return list(set([itype.expression for itype in self.improper_types]))
@property
def pairpotential_type_expressions(self):
@@ -390,7 +637,7 @@ def pairpotential_type_expressions(self):
set([ptype.expression for ptype in self._pairpotential_types])
)
- def add_site(self, site, update_types=True):
+ def add_site(self, site, update_types=False):
"""Add a site to the topology.
This method will add a site to the existing topology, since
@@ -408,9 +655,9 @@ def add_site(self, site, update_types=True):
If true, add this site's atom type to the topology's set of AtomTypes
"""
self._sites.add(site)
- if update_types and site.atom_type:
- self._atom_types.add(site.atom_type)
- self.is_typed(updated=False)
+ self.is_updated = False
+ if update_types:
+ self.update_topology()
def update_sites(self):
"""Update the sites of the topology.
@@ -440,7 +687,7 @@ def update_sites(self):
if member not in self._sites:
self.add_site(member)
- def add_connection(self, connection, update_types=True):
+ def add_connection(self, connection, update_types=False):
"""Add a gmso.Connection object to the topology.
This method will add a gmso.Connection object to the
@@ -481,6 +728,7 @@ def add_connection(self, connection, update_types=True):
self._connections.add(connection)
self._unique_connections.update({equivalent_members: connection})
+
if isinstance(connection, Bond):
self._bonds.add(connection)
if isinstance(connection, Angle):
@@ -489,8 +737,9 @@ def add_connection(self, connection, update_types=True):
self._dihedrals.add(connection)
if isinstance(connection, Improper):
self._impropers.add(connection)
+
if update_types:
- self.update_connection_types()
+ self.update_topology()
return connection
@@ -498,32 +747,29 @@ def identify_connections(self):
"""Identify all connections in the topology."""
_identify_connections(self)
+ def update_atom_types(self):
+ """Keep an uptodate length of all the connection types."""
+ self.update_topology()
+
def update_connection_types(self):
- """Update the connection types based on the connection collection in the topology.
+ """Keep an upto date length of all the connection types."""
+ self.update_topology()
- This method looks into all the connection objects (Bonds, Angles, Dihedrals, Impropers) to
- check if any Potential object (BondType, AngleType, DihedralType, ImproperType) is not in the
- topology's respective collection and will add those objects there.
+ def update_topology(self):
+ """Update the entire topology."""
+ self._bookkeep_potentials()
+ self.is_updated = True
+ self.is_typed(updated=True)
- See Also
- --------
- gmso.Topology.update_atom_types : Update atom types in the topology.
- """
- targets = {
- BondType: self._bond_types,
- AngleType: self._angle_types,
- DihedralType: self._dihedral_types,
- ImproperType: self._improper_types,
+ def _bookkeep_potentials(self):
+ self._potentials_count = {
+ "atom_types": len(self.atom_types),
+ "bond_types": len(self.bond_types),
+ "angle_types": len(self.angle_types),
+ "dihedral_types": len(self.dihedral_types),
+ "improper_types": len(self.improper_types),
+ "pairpotential_types": len(self._pairpotential_types),
}
- for c in self.connections:
- if c.connection_type is None:
- warnings.warn(
- "Non-parametrized Connection {} detected".format(c)
- )
- else:
- target_conn_type = targets[type(c.connection_type)]
- target_conn_type.add(c.connection_type)
- self._connection_types.add(c.connection_type)
def add_pairpotentialtype(self, pairpotentialtype, update=True):
"""add a PairPotentialType to the topology
@@ -544,8 +790,6 @@ def add_pairpotentialtype(self, pairpotentialtype, update=True):
gmso.core.pairpotential_type: Pairwise potential that does not follow
combination rules
"""
- if update:
- self.update_atom_types()
if not isinstance(pairpotentialtype, PairPotentialType):
raise GMSOError(
"Non-PairPotentialType {} provided".format(pairpotentialtype)
@@ -581,30 +825,6 @@ def remove_pairpotentialtype(self, pair_of_types):
"No pair potential specified for such pair of AtomTypes/atomclasses"
)
- def update_atom_types(self):
- """Update atom types in the topology.
-
- This method checks all the sites in the topology which have an
- associated AtomType and if that AtomType is not in the topology's
- AtomTypes collection, it will add it there.
-
- See Also
- --------
- gmso.Topology.update_connection_types :
- Update the connection types based on the connection collection in the topology
- """
- for site in self._sites:
- if site.atom_type is None:
- warnings.warn("Non-parametrized site detected {}".format(site))
- elif not isinstance(site.atom_type, AtomType):
- raise GMSOError(
- "Non AtomType instance found in site {}".format(site)
- )
- else:
- self._atom_types.add(site.atom_type)
-
- self.is_typed(updated=True)
-
def add_subtopology(self, subtop, update=True):
"""Add a sub-topology to this topology.
@@ -631,16 +851,11 @@ def add_subtopology(self, subtop, update=True):
def is_typed(self, updated=False):
"""Verify if the topology is parametrized."""
if not updated:
- self.update_connection_types()
- self.update_atom_types()
-
- if len(self.atom_types) > 0 or len(self.connection_types) > 0:
- self._typed = True
- else:
- self._typed = False
+ self.update_topology()
+ self._typed = any(self._potentials_count.values())
return self._typed
- def is_fully_typed(self, updated=False, group="topology"):
+ def is_fully_typed(self, group="topology", updated=False):
"""Check if the topology or a specifc group of objects that make up the topology are fully typed
Parameters
@@ -666,9 +881,9 @@ def is_fully_typed(self, updated=False, group="topology"):
`self._type` is set to True as long as the Topology is at least
partially typed.
"""
+
if not updated:
- self.update_connection_types()
- self.update_atom_types()
+ self.update_topology()
typed_status = {
"sites": lambda top: all(site.atom_type for site in top._sites),
@@ -824,13 +1039,6 @@ def update_improper_types(self):
"""
self.update_connection_types()
- def update_topology(self):
- """Update the entire topology."""
- self.update_sites()
- self.update_atom_types()
- self.update_connection_types()
- self.is_typed(updated=True)
-
def _get_bonds_for(self, site):
"""Return a list of bonds in this Topology that the site is a part of."""
bonds = []
@@ -876,12 +1084,12 @@ def get_index(self, member):
Angle: self._angles,
Dihedral: self._dihedrals,
Improper: self._impropers,
- AtomType: self._atom_types,
- BondType: self._bond_types,
- AngleType: self._angle_types,
- DihedralType: self._dihedral_types,
- ImproperType: self._improper_types,
- PairPotentialType: self._pairpotential_types,
+ AtomType: self.atom_types,
+ BondType: self.bond_types,
+ AngleType: self.angle_types,
+ DihedralType: self.dihedral_types,
+ ImproperType: self.improper_types,
+ PairPotentialType: self.pairpotential_types,
}
member_type = type(member)
@@ -975,10 +1183,12 @@ def save(self, filename, overwrite=False, **kwargs):
def __repr__(self):
"""Return custom format to represent topology."""
+ if not self.is_updated:
+ self.update_topology()
return (
f""
)
diff --git a/gmso/core/views.py b/gmso/core/views.py
new file mode 100644
index 000000000..d1775c291
--- /dev/null
+++ b/gmso/core/views.py
@@ -0,0 +1,202 @@
+import uuid
+from collections import defaultdict
+
+from gmso.core.angle import Angle
+from gmso.core.angle_type import AngleType
+from gmso.core.atom import Atom
+from gmso.core.atom_type import AtomType
+from gmso.core.bond import Bond
+from gmso.core.bond_type import BondType
+from gmso.core.dihedral import Dihedral
+from gmso.core.dihedral_type import DihedralType
+from gmso.core.improper import Improper
+from gmso.core.improper_type import ImproperType
+
+__all__ = ["TopologyPotentialView", "PotentialFilters"]
+
+potential_attribute_map = {
+ Atom: "atom_type",
+ Bond: "bond_type",
+ Angle: "angle_type",
+ Dihedral: "dihedral_type",
+ Improper: "improper_type",
+}
+
+
+class MissingFilterError(KeyError):
+ """Error to be raised when there's a missing builtin filter."""
+
+
+def get_name_or_class(potential):
+ """Get identifier for a topology potential based on name or membertype/class."""
+ if isinstance(potential, AtomType):
+ return potential.name
+ if isinstance(potential, (BondType, AngleType, DihedralType, ImproperType)):
+ return potential.member_types or potential.member_classes
+
+
+def get_parameters(potential):
+ """Return hashable version of parameters for a potential."""
+
+ return (
+ tuple(potential.get_parameters().keys()),
+ tuple(map(lambda x: x.to_value(), potential.get_parameters().values())),
+ )
+
+
+def filtered_potentials(potential_types, identifier):
+ """Filter and return unique potentials based on pre-defined identifier function."""
+ visited = defaultdict(set)
+
+ for potential_type in potential_types:
+ potential_id = identifier(potential_type)
+ if potential_id not in visited[type(potential_type)]:
+ visited[type(potential_type)].add(potential_id)
+
+ yield potential_type
+
+
+class PotentialFilters:
+ UNIQUE_NAME_CLASS = "unique_name_class"
+ UNIQUE_EXPRESSION = "unique_expression"
+ UNIQUE_PARAMETERS = "unique_parameters"
+ UNIQUE_ID = "unique_id"
+ REPEAT_DUPLICATES = "repeat_duplicates"
+
+ @staticmethod
+ def all():
+ return set(
+ f"{PotentialFilters.__name__}.{k}"
+ for k, v in PotentialFilters.__dict__.items()
+ if not k.startswith("__") and not callable(v)
+ )
+
+
+potential_identifiers = {
+ PotentialFilters.UNIQUE_NAME_CLASS: get_name_or_class,
+ PotentialFilters.UNIQUE_EXPRESSION: lambda p: str(p.expression),
+ PotentialFilters.UNIQUE_PARAMETERS: get_parameters,
+ PotentialFilters.UNIQUE_ID: lambda p: id(p),
+ PotentialFilters.REPEAT_DUPLICATES: lambda _: str(uuid.uuid4()),
+}
+
+
+class TopologyPotentialView:
+ """A potential view based on different filters for a topology's potentials.
+
+ Parameters
+ ----------
+ iterator: typing.Iterator, required=True
+ An iterator of either topology sites or connections from which to extract potentials from
+
+ Parameters
+ ----------
+ filter_by: str or function, default=PotentialFilters.UNIQUE_ID
+ If provided, filter the collected potentials by some unique identifier
+ of a potential.
+
+ Notes
+ -----
+ If `filter_by` is provided and is a custom function, the collected potentials are
+ filtered on the basis of the return value of the `filter_by` function. A single potential
+ is passed to the filter_by function and it should return an identifier (should be hashable)
+ for that potential thus describing its uniqueness in the context of which the filter is being
+ used. Some simple examples are given below.
+
+ Examples
+ --------
+ To use a TopologyPotentialView, a Topology must have few sites with AtomTypes or BondTypes.
+
+ >>> from gmso.core.topology import Topology
+ >>> from gmso.core.atom import Atom
+ >>> from gmso.core.atom_type import AtomType
+ >>> top = Topology(name="ViewTopology")
+ >>> sites = [Atom(name=f"Atom_{j}") for j in range(10)]
+ >>> atom_type1 = AtomType(name='atom_type1')
+ >>> atom_type2 = AtomType(name='atom_type2')
+ >>> for site in sites:
+ ... site.atom_type = atom_type1 if int(site.name[-1]) % 2 == 0 else atom_type2
+ ... top.add_site(site)
+ >>> top.update_topology()
+ >>> for atom_type in top.atom_types:
+ ... print(atom_type.name)
+ atom_type1
+ atom_type2
+ >>> top.get_index(atom_type2)
+ 1
+
+ Notes
+ -----
+ The implementation of this class is inspired from networkx.classes.reportviews.NodeView by extending
+ the idea of a view with filteration capabilities. See the source for NodeView for further details
+
+ https://github.com/networkx/networkx/blob/12c1a00cd116701a763f7c57c230b8739d2ed085/networkx/classes/reportviews.py#L115-L279
+ """
+
+ def __init__(self, iterator, filter_by=PotentialFilters.UNIQUE_ID):
+ self.iterator = iterator
+ self.filter_by = filter_by
+
+ def __iter__(self):
+ yield from self.yield_view()
+
+ def index(self, item):
+ for j, potential in enumerate(self.yield_view()):
+ if potential is item:
+ return j
+
+ def _collect_potentials(self):
+ """Collect potentials from the iterator"""
+ for item in self.iterator:
+ potential = getattr(
+ item, potential_attribute_map[type(item)]
+ ) # Since this use is internal, KeyErrors N/A
+ if potential:
+ yield potential
+
+ def yield_view(self):
+ """Yield a view of the potentials of the iterator provided.
+
+ Yields
+ ------
+ gmso.core.ParametricPotential
+ An instance of gmso.core.ParametricPotential from the attributes of Sites/Connections
+ in the iterator.
+ """
+ if not self.filter_by:
+ yield from self._collect_potentials()
+
+ else:
+ if isinstance(self.filter_by, str):
+ try:
+ identifier_func = potential_identifiers[self.filter_by]
+ except KeyError:
+ raise MissingFilterError(
+ f"Potential filter {self.filter_by} is not among the built-in"
+ f"filters. Please use one of the builtin filters or define a custom "
+ f"filter callable. Builtin Filters are \n {PotentialFilters.all()}"
+ )
+ else:
+ identifier_func = self.filter_by
+
+ yield from filtered_potentials(
+ self._collect_potentials(), identifier=identifier_func
+ )
+
+ def __call__(self, filter_by=PotentialFilters.UNIQUE_ID):
+ """The call method, for turning property decorators into functions"""
+ if filter_by == self.filter_by:
+ return self
+
+ return TopologyPotentialView(
+ iterator=self.iterator, filter_by=filter_by
+ )
+
+ def __repr__(self):
+ name = self.__class__.__name__
+ return f"<{name}({tuple(self)})>"
+
+ def __len__(self):
+ return len(
+ list(self.yield_view())
+ ) # This will be costly? But How frequent?
diff --git a/gmso/formats/json.py b/gmso/formats/json.py
index 994db15ac..4d09c28ff 100644
--- a/gmso/formats/json.py
+++ b/gmso/formats/json.py
@@ -119,11 +119,11 @@ def _to_json(top, types=False, update=True):
connection_dict[exclude_attr] = id(connection_type)
if types:
for potentials in [
- top._atom_types,
- top._bond_types,
- top._angle_types,
- top._dihedral_types,
- top._improper_types,
+ top.atom_types,
+ top.bond_types,
+ top.angle_types,
+ top.dihedral_types,
+ top.improper_types,
]:
for potential in potentials:
potential_dict = potential.json_dict(
diff --git a/gmso/tests/test_atom_type.py b/gmso/tests/test_atom_type.py
index c3978ccaa..b6e6c0088 100644
--- a/gmso/tests/test_atom_type.py
+++ b/gmso/tests/test_atom_type.py
@@ -308,7 +308,7 @@ def test_atom_type_with_topology_and_site_change_properties(self):
top.add_site(site2)
site1.atom_type.mass = 250
assert site1.atom_type.mass == 250
- assert top.atom_types[0].mass == 250
+ assert next(iter(top.atom_types)).mass == 250
def test_with_1000_atom_types(self):
top = Topology()
diff --git a/gmso/tests/test_mcf.py b/gmso/tests/test_mcf.py
index eca464638..69805eb74 100644
--- a/gmso/tests/test_mcf.py
+++ b/gmso/tests/test_mcf.py
@@ -108,13 +108,13 @@ def test_write_mie_full(self, n_typed_xe_mie):
def test_modified_potentials(self, n_typed_ar_system):
top = n_typed_ar_system(n_sites=1)
- top.atom_types[0].set_expression("sigma + epsilon*r")
+ next(iter(top.atom_types)).set_expression("sigma + epsilon*r")
with pytest.raises(EngineIncompatibilityError):
top.save("out.mcf")
alternate_lj = "4*epsilon*sigma**12/r**12 - 4*epsilon*sigma**6/r**6"
- top.atom_types[0].set_expression(alternate_lj)
+ next(iter(top.atom_types)).set_expression(alternate_lj)
top.save("ar.mcf")
diff --git a/gmso/tests/test_top.py b/gmso/tests/test_top.py
index 389edbab5..f7e0c8e59 100644
--- a/gmso/tests/test_top.py
+++ b/gmso/tests/test_top.py
@@ -33,13 +33,13 @@ def test_modified_potentials(self, ar_system):
top.update_topology()
- top.atom_types[0].set_expression("sigma + epsilon*r")
+ list(top.atom_types)[0].set_expression("sigma + epsilon*r")
with pytest.raises(EngineIncompatibilityError):
top.save("out.top")
alternate_lj = "4*epsilon*sigma**12/r**12 - 4*epsilon*sigma**6/r**6"
- top.atom_types[0].set_expression(alternate_lj)
+ list(top.atom_types)[0].set_expression(alternate_lj)
top.save("ar.top")
diff --git a/gmso/tests/test_topology.py b/gmso/tests/test_topology.py
index 66a88c17e..281043b71 100644
--- a/gmso/tests/test_topology.py
+++ b/gmso/tests/test_topology.py
@@ -16,7 +16,6 @@
from gmso.core.dihedral_type import DihedralType
from gmso.core.improper import Improper
from gmso.core.improper_type import ImproperType
-from gmso.core.pairpotential_type import PairPotentialType
from gmso.core.subtopology import SubTopology
from gmso.core.topology import Topology
from gmso.exceptions import GMSOError
@@ -196,12 +195,16 @@ def test_add_typed_site_update(self):
top = Topology()
assert len(top.atom_types) == 0
top.add_site(typed_site, update_types=False)
- assert len(top.atom_types) == 0
+ assert (
+ len(top.atom_types) == 1
+ ) # Always upto date now (except for repr)
+ assert top._potentials_count["atom_types"] == 0
top = Topology()
assert len(top.atom_types) == 0
top.add_site(typed_site, update_types=True)
assert len(top.atom_types) == 1
+ assert top._potentials_count["atom_types"] == 1
def test_add_untyped_bond_update(self):
atom1 = Atom(atom_type=None)
@@ -227,7 +230,7 @@ def test_add_typed_bond_update(self):
top.add_site(atom1)
top.add_site(atom2)
top.add_connection(bond, update_types=False)
- assert len(top.connection_types) == 0
+ assert len(top.connection_types) == 1
top = Topology()
top.add_connection(bond, update_types=True)
@@ -270,8 +273,8 @@ def test_top_update(self):
atom1.atom_type = AtomType()
atom1.atom_type.expression = "sigma*epsilon*r"
assert top.n_sites == 2
- assert len(top.atom_types) == 1
- assert len(top.atom_type_expressions) == 1
+ assert len(top.atom_types) == 2
+ assert len(top.atom_type_expressions) == 2
assert top.n_connections == 1
assert len(top.connection_types) == 1
assert len(top.connection_type_expressions) == 1
@@ -426,15 +429,6 @@ def test_parametrization(self):
assert top.is_typed() == True
assert top.typed == True
- def test_parametrization_setter(self):
- top = Topology()
-
- assert top.typed == False
- assert top.is_typed() == False
- top.typed = True
- assert top.typed == True
- assert top.is_typed() == False
-
def test_topology_atom_type_changes(self):
top = Topology()
for i in range(100):
diff --git a/gmso/tests/test_views.py b/gmso/tests/test_views.py
new file mode 100644
index 000000000..e1d156ca9
--- /dev/null
+++ b/gmso/tests/test_views.py
@@ -0,0 +1,140 @@
+import pytest
+import unyt as u
+
+from gmso.core.atom import Atom
+from gmso.core.atom_type import AtomType
+from gmso.core.bond import Bond
+from gmso.core.bond_type import BondType
+from gmso.core.topology import Topology
+from gmso.core.views import PotentialFilters
+from gmso.tests.base_test import BaseTest
+from gmso.utils.misc import unyt_to_hashable
+
+
+def str_expr_dict_param(potential):
+ pot_id = (
+ str(potential.expression),
+ frozenset(potential.parameters),
+ tuple(unyt_to_hashable(list(potential.parameters.values()))),
+ )
+ return pot_id
+
+
+class TestViews(BaseTest):
+ @pytest.fixture(scope="session")
+ def custom_top(self):
+ custom_top = Topology()
+ atom_type1 = AtomType(
+ name="dummy_1",
+ expression="x+y*z",
+ parameters={"x": 2.0 * u.nm, "z": 33000 * u.dimensionless},
+ independent_variables={"y"},
+ )
+
+ atom_type2 = AtomType(
+ name="dummy_2",
+ expression="x+y*z",
+ parameters={"x": 5.0 * u.nm, "z": 33000 * u.dimensionless},
+ independent_variables={"y"},
+ )
+
+ for j in range(200):
+ custom_top.add_site(
+ Atom(
+ atom_type=atom_type1.clone()
+ if j % 2 == 0
+ else atom_type2.clone()
+ )
+ )
+
+ for j in range(0, 200, 2):
+ bond = Bond(
+ connection_members=[
+ custom_top.sites[j],
+ custom_top.sites[j + 1],
+ ],
+ bond_type=BondType(member_classes=(j, j + 1)),
+ )
+ custom_top.add_connection(bond)
+
+ custom_top.update_topology()
+ return custom_top
+
+ def test_view_atom_types_typed_ar_system(self, n_typed_ar_system):
+ atom_types = n_typed_ar_system().atom_types()
+ assert len(atom_types) == 1
+ atom_types_unique = n_typed_ar_system().atom_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ )
+ assert len(atom_types_unique) == 1
+
+ def test_ethane_views(self, typed_ethane):
+ atom_types = typed_ethane.atom_types
+ unique_atomtypes = atom_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ )
+ assert len(atom_types) == len(unique_atomtypes)
+
+ bond_types = typed_ethane.bond_types
+ unique_bondtypes = typed_ethane.bond_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ )
+ assert len(bond_types) == len(unique_bondtypes)
+ assert typed_ethane._potentials_count["bond_types"] == len(bond_types)
+
+ angle_types = typed_ethane.angle_types
+ unique_angletypes = typed_ethane.angle_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ )
+ assert len(angle_types) == len(unique_angletypes)
+ assert typed_ethane._potentials_count["angle_types"] == len(bond_types)
+
+ dihedral_types = typed_ethane.dihedral_types
+ unique_dihedraltypes = typed_ethane.dihedral_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ )
+ assert len(unique_dihedraltypes) == len(dihedral_types)
+ assert typed_ethane._potentials_count["dihedral_types"] == len(
+ dihedral_types
+ )
+
+ def test_custom_filter(self, custom_top):
+ assert len(custom_top.atom_types) == 200
+ assert len(custom_top.atom_types(filter_by=str_expr_dict_param)) == 2
+
+ def test_get_index(self, custom_top):
+ assert custom_top.get_index(custom_top.sites[50].atom_type) == 50
+
+ def test_bondtype_views(self, custom_top):
+ assert len(custom_top.bond_types) == 100
+ assert len(custom_top.bond_types(filter_by=str_expr_dict_param)) == 1
+
+ def test_call(self, custom_top):
+ atom_types = custom_top.atom_types
+ atom_types_new = atom_types()
+ atom_types_new_different_filter = atom_types(
+ filter_by=str_expr_dict_param
+ )
+
+ assert id(atom_types) == id(atom_types_new)
+ assert id(atom_types) != atom_types_new_different_filter
+
+ def test_default_filters(self, custom_top):
+ bond_types = custom_top.bond_types(
+ filter_by=PotentialFilters.UNIQUE_EXPRESSION
+ )
+ bond_types_params = bond_types(
+ filter_by=PotentialFilters.UNIQUE_PARAMETERS
+ )
+ bond_types_name_class = bond_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ )
+
+ bond_types_repeat = bond_types(
+ filter_by=PotentialFilters.REPEAT_DUPLICATES
+ )
+
+ assert len(bond_types) == 1
+ assert len(bond_types_params) == 1
+ assert len(bond_types_name_class) == 100
+ assert len(bond_types_repeat) == 100
From a6255a4f14396b644428ded96f10f04383874ec0 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 25 May 2022 15:37:17 -0500
Subject: [PATCH 059/141] Support `unyt_array` in `__eq__`/`clone` for
`PotentialExpression` (#660)
* Support unyt_array in `__eq__`/`clone` for `PotentialExpression`
* WIP-Fix docstring; add more tests
* Additional tests; more documentation
* WIP- Move method to test dict equality inmodule
---
gmso/tests/test_expression.py | 50 ++++++++++++++++++++++++++++++++++-
gmso/utils/expression.py | 23 ++++++++++++++--
2 files changed, 70 insertions(+), 3 deletions(-)
diff --git a/gmso/tests/test_expression.py b/gmso/tests/test_expression.py
index bfdb5cf3e..40ebbe664 100644
--- a/gmso/tests/test_expression.py
+++ b/gmso/tests/test_expression.py
@@ -3,7 +3,7 @@
import unyt as u
from gmso.tests.base_test import BaseTest
-from gmso.utils.expression import PotentialExpression
+from gmso.utils.expression import PotentialExpression, _are_equal_parameters
class TestExpression(BaseTest):
@@ -232,3 +232,51 @@ def test_clone(self):
)
assert expr == expr_clone
+
+ def test_clone_with_unyt_arrays(self):
+ expression = PotentialExpression(
+ expression="x**2 + y**2 + 2*x*y*theta",
+ independent_variables="theta",
+ parameters={
+ "x": [2.0, 4.5] * u.nm,
+ "y": [3.4, 4.5] * u.kcal / u.mol,
+ },
+ )
+
+ expression_clone = expression.clone()
+ assert expression_clone == expression
+
+ def test_expression_equality_different_params(self):
+ expr1 = PotentialExpression(
+ independent_variables="r",
+ parameters={"a": 2.0 * u.nm, "b": 3.0 * u.nm},
+ expression="a+r*b",
+ )
+
+ expr2 = PotentialExpression(
+ independent_variables="r",
+ parameters={"c": 2.0 * u.nm, "d": 3.0 * u.nm},
+ expression="c+r*d",
+ )
+
+ assert expr1 != expr2
+
+ def test_expression_equality_same_params_different_values(self):
+ expr1 = PotentialExpression(
+ independent_variables="r",
+ parameters={"a": 2.0 * u.nm, "b": 3.0 * u.nm},
+ expression="a+r*b",
+ )
+
+ expr2 = PotentialExpression(
+ independent_variables="r",
+ parameters={"a": 2.0 * u.nm, "b": 3.5 * u.nm},
+ expression="a+r*b",
+ )
+
+ assert expr1 != expr2
+
+ def test_are_equal_parameters(self):
+ u1 = {"a": 2.0 * u.nm, "b": 3.5 * u.nm}
+ u2 = {"c": 2.0 * u.nm, "d": 3.5 * u.nm}
+ assert _are_equal_parameters(u1, u2) is False
diff --git a/gmso/utils/expression.py b/gmso/utils/expression.py
index ed1d64083..b96af9c9c 100644
--- a/gmso/utils/expression.py
+++ b/gmso/utils/expression.py
@@ -7,11 +7,28 @@
import unyt as u
from gmso.utils.decorators import register_pydantic_json
-from gmso.utils.misc import unyt_to_hashable
__all__ = ["PotentialExpression"]
+def _are_equal_parameters(u1, u2):
+ """Compare two parameters of unyt quantities/arrays.
+
+ This method compares two dictionaries (`u1` and `u2`) of
+ `unyt_quantities` and returns True if:
+ * u1 and u2 have the exact same key set
+ * for each key, the value in u1 and u2 have the same unyt quantity
+ """
+ if u1.keys() != u2.keys():
+ return False
+ else:
+ for k, v in u1.items():
+ if not u.allclose_units(v, u2[k]):
+ return False
+
+ return True
+
+
@register_pydantic_json(method="json")
class PotentialExpression:
"""A general Expression class with parameters.
@@ -258,7 +275,7 @@ def __eq__(self, other):
return (
self.expression == other.expression
and self.independent_variables == other.independent_variables
- and self.parameters == other.parameters
+ and _are_equal_parameters(self.parameters, other.parameters)
)
@staticmethod
@@ -295,6 +312,8 @@ def clone(self):
deepcopy(self._independent_variables),
{
k: u.unyt_quantity(v.value, v.units)
+ if v.value.shape == ()
+ else u.unyt_array(v.value, v.units)
for k, v in self._parameters.items()
}
if self._is_parametric
From 04f28451d810f46947135c819ab9f6139d66d556 Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Wed, 1 Jun 2022 13:11:26 -0500
Subject: [PATCH 060/141] Expanded search for elements when reading mol2 (#661)
* Expanded search for elements when reading mol2
* Add test for benzene loading
* Add proper test for warnings
---
gmso/formats/mol2.py | 17 +++++++++++------
gmso/tests/test_mol2.py | 23 +++++++++++++++--------
gmso/utils/files/neopentane.mol2 | 17 +++++++++++++++++
3 files changed, 43 insertions(+), 14 deletions(-)
create mode 100644 gmso/utils/files/neopentane.mol2
diff --git a/gmso/formats/mol2.py b/gmso/formats/mol2.py
index 7846c1133..32751ec9f 100644
--- a/gmso/formats/mol2.py
+++ b/gmso/formats/mol2.py
@@ -1,4 +1,5 @@
"""Convert to and from a TRIPOS mol2 file."""
+import itertools
import warnings
from pathlib import Path
@@ -110,17 +111,21 @@ def _parse_lj(top, section):
def _parse_atom(top, section):
"""Parse atom information from the mol2 file."""
- parse_ele = (
- lambda ele: element_by_symbol(ele)
- if element_by_symbol(ele)
- else element_by_name(ele)
- )
+
+ def parse_ele(*symbols):
+ methods = [element_by_name, element_by_symbol]
+ elem = None
+ for symbol, method in itertools.product(symbols, methods):
+ elem = method(symbol)
+ if elem:
+ break
+ return elem
for line in section:
if line.strip():
content = line.split()
position = [float(x) for x in content[2:5]] * u.Å
- element = parse_ele(content[5])
+ element = parse_ele(content[5], content[1])
if not element:
warnings.warn(
diff --git a/gmso/tests/test_mol2.py b/gmso/tests/test_mol2.py
index f00a1dc1e..6d225d930 100644
--- a/gmso/tests/test_mol2.py
+++ b/gmso/tests/test_mol2.py
@@ -4,7 +4,6 @@
from unyt.testing import assert_allclose_units
from gmso import Topology
-from gmso.formats.mol2 import from_mol2
from gmso.tests.base_test import BaseTest
from gmso.tests.utils import get_path
from gmso.utils.io import get_fn
@@ -62,12 +61,6 @@ def test_read_mol2(self):
top = Topology.load(get_fn("ethane.mol2"))
assert list(top.sites)[0].charge is None
- with pytest.warns(
- UserWarning,
- match=r"No element detected for site C with index 1, consider manually adding the element to the topology",
- ):
- Topology.load(get_fn("benzene.mol2"))
-
def test_residue(self):
top = Topology.load(get_fn("ethanol_aa.mol2"))
assert np.all([site.residue_name == "ETO" for site in top.sites])
@@ -106,7 +99,7 @@ def test_lj_system(self):
def test_no_charge_lj(self):
with pytest.warns(
UserWarning,
- match="No charge was detected for site .* with index \d+$",
+ match=r"No charge was detected for site .* with index \d+$",
):
top = Topology.load(
get_path("methane_missing_charge.mol2"), site_type="lj"
@@ -132,3 +125,17 @@ def test_broken_files(self):
match=r"This mol2 file has two boxes to be read in, only reading in one with dimensions Box\(a=0.72",
):
Topology.load(get_fn("broken.mol2"))
+
+ def test_benzene_mol2_elements(self):
+ top = Topology.load(get_fn("benzene.mol2"))
+
+ for atom in top.sites:
+ assert atom.element.name in {"hydrogen", "carbon"}
+
+ def test_neopentane_mol2_elements(self):
+ with pytest.warns(
+ UserWarning,
+ match=r"No element detected for site .+ with index \d+, "
+ r"consider manually adding the element to the topology$",
+ ):
+ top = Topology.load(get_fn("neopentane.mol2"))
diff --git a/gmso/utils/files/neopentane.mol2 b/gmso/utils/files/neopentane.mol2
new file mode 100644
index 000000000..7a845e5d6
--- /dev/null
+++ b/gmso/utils/files/neopentane.mol2
@@ -0,0 +1,17 @@
+@MOLECULE
+*****
+ 5 4 0 0 0
+SMALL
+GASTEIGER
+
+@ATOM
+ 1 C -0.9514 0.4186 -0.0000 C_sp3 1 UNL1 0.0000
+ 2 _CH3 -0.9361 2.0625 -0.0000 CH3_sp3 1 UNL1 0.0000
+ 3 _CH3 0.8752 0.4338 -0.0000 CH3_sp3 1 UNL1 0.0000
+ 4 _CH3 -2.8388 0.4034 -0.0000 CH3_sp3 1 UNL1 0.0000
+ 5 _CH3 -0.9514 -1.2862 -0.0000 CH3_sp3 1 UNL1 0.0000
+@BOND
+ 1 1 2 1
+ 2 1 3 1
+ 3 1 4 1
+ 4 1 5 1
From 9bfa833b522708846b995c4178dcde9125205dc1 Mon Sep 17 00:00:00 2001
From: CalCraven <54594941+CalCraven@users.noreply.github.com>
Date: Tue, 7 Jun 2022 10:17:35 -0500
Subject: [PATCH 061/141] Parmed Conversion Improvement (#658)
* Include ImproperType reading in "from_parmed"
This PR is branched from PR #644. The parmed conversion to GMSO
topology was not able to read in various Improper connections to
GMSO. It would aggregate them with the Dihedral connections, from
when GMSO did not have explicit treatment of impropers, similar to
ParmEd. This PR aims to check a ParmEd object for structure.impropers
or structure.dihedrals with the dihedral.impropers flag set to
True and convert those to `gmso.Impropers`. Likewise, it looks to
read in periodic or harmonic impropers and generate the correct
`gmso.ImproperType` parametric potential for that type, if the flag
`refer_types` is set to True in the `from_parmed` call.
* Add pmd_improper_types_map function to convert_parmed
* Generate a parametric potential with harmonic type
expression from pmd.impropers
* Generate a parametric potential with periodic type
expression from pmd.dihedrals and dihedral.improper=True
* Validate testing with foyer opls_validation when using
functions in PR #644
Harmonic type expression for impropers is taken from
https://manual.gromacs.org/current/reference-manual/functions/bonded-interactions.html#improper-dihedrals-harmonic-type
This uses Xi as the independent variable. If someone testing this PR can
validate that this is the formalism we want to use when reading in
ImproperTypes from ParmEd improper_types, that would be a good sanity
check.
The periodic type expression is taken from
https://manual.gromacs.org/current/reference-manual/functions/bonded-interactions.html#equation-eqnperiodicpropdihedral
with phi as the independent variable.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Add test for number of impropers from NNdimethylformamide molecule
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* WIP- include gmso in impropertype testing
* WIP- Add support for NoneTypes in site charge/mass
* Address Review comments and create impropertype expression from HarmonicImproperPotential in library
* WIP- Add testing for impropers
* Modification for improper potential forms from the PotentialTemplateLibrary
* WIP- Use id instead of object for storing mapping; impropertypes test
* WIP- combined test for dihedral/impropertypes
* Fix test case and id mapping in conversion
* Add tests for harmonic impropers
* Add proper properties for improper_types
* WIP-Additional test cases
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Umesh Timalsina
---
gmso/external/convert_parmed.py | 252 ++++++++++++++++++----
gmso/tests/test_convert_parmed.py | 164 ++++++++++++++
gmso/utils/files/NN-dimethylformamide.gro | 15 ++
gmso/utils/files/NN-dimethylformamide.top | 106 +++++++++
4 files changed, 490 insertions(+), 47 deletions(-)
create mode 100644 gmso/utils/files/NN-dimethylformamide.gro
create mode 100644 gmso/utils/files/NN-dimethylformamide.top
diff --git a/gmso/external/convert_parmed.py b/gmso/external/convert_parmed.py
index 403fa4ace..dab24987a 100644
--- a/gmso/external/convert_parmed.py
+++ b/gmso/external/convert_parmed.py
@@ -13,19 +13,22 @@
element_by_name,
element_by_symbol,
)
+from gmso.lib.potential_templates import PotentialTemplateLibrary
from gmso.utils.io import has_parmed, import_
if has_parmed:
pmd = import_("parmed")
+lib = PotentialTemplateLibrary()
+
def from_parmed(structure, refer_type=True):
"""Convert a parmed.Structure to a gmso.Topology.
Convert a parametrized or un-parametrized parmed.Structure object to a topology.Topology.
Specifically, this method maps Structure to Topology and Atom to Site.
- At this point, this method can only convert AtomType, BondType and AngleType.
- Conversion of DihedralType will be implement in the near future.
+ This method can only convert AtomType, BondType AngleType, DihedralType, and
+ ImproperType.
Parameters
----------
@@ -33,7 +36,7 @@ def from_parmed(structure, refer_type=True):
parmed.Structure instance that need to be converted.
refer_type : bool, optional, default=True
Whether or not to transfer AtomType, BondType, AngleType,
- and DihedralType information
+ DihedralType, and ImproperType information
Returns
-------
@@ -72,11 +75,23 @@ def from_parmed(structure, refer_type=True):
structure, angle_types_member_map=angle_types_map
)
# Consolidate parmed dihedraltypes and relate to topology dihedraltypes
- dihedral_types_map = _get_types_map(structure, "dihedrals")
+ # TODO: CCC seperate structure dihedrals.improper = False
+ dihedral_types_map = _get_types_map(
+ structure, "dihedrals", impropers=False
+ )
dihedral_types_map.update(_get_types_map(structure, "rb_torsions"))
pmd_top_dihedraltypes = _dihedral_types_from_pmd(
structure, dihedral_types_member_map=dihedral_types_map
)
+ # Consolidate parmed dihedral/impropertypes and relate to topology impropertypes
+ # TODO: CCC seperate structure dihedrals.improper = True
+ improper_types_map = _get_types_map(structure, "impropers")
+ improper_types_map.update(
+ _get_types_map(structure, "dihedrals"), impropers=True
+ )
+ pmd_top_impropertypes = _improper_types_from_pmd(
+ structure, improper_types_member_map=improper_types_map
+ )
subtops = list()
for residue in structure.residues:
@@ -158,41 +173,73 @@ def from_parmed(structure, refer_type=True):
top.add_connection(top_connection, update_types=False)
for dihedral in structure.dihedrals:
- # Generate dihedral parameters for DihedralType that gets passed
- # to Dihedral
+ # Generate parameters for ImproperType or DihedralType that gets passed
+ # to corresponding Dihedral or Improper
# These all follow periodic torsions functions
- # (even if they are improper dihedrals)
# Which are the default expression in top.DihedralType
# These periodic torsion dihedrals get stored in top.dihedrals
+ # and periodic torsion impropers get stored in top.impropers
+
if dihedral.improper:
warnings.warn(
"ParmEd improper dihedral {} ".format(dihedral)
+ "following periodic torsion "
+ "expression detected, currently accounted for as "
- + "topology.Dihedral with a periodic torsion expression"
- )
- if refer_type and isinstance(dihedral.type, pmd.DihedralType):
- top_connection = gmso.Dihedral(
- connection_members=[
- site_map[dihedral.atom1],
- site_map[dihedral.atom2],
- site_map[dihedral.atom3],
- site_map[dihedral.atom4],
- ],
- dihedral_type=pmd_top_dihedraltypes[dihedral.type],
+ + "topology.Improper with a periodic improper expression"
)
- # No bond parameters, make Connection with no connection_type
+
+ if refer_type and isinstance(dihedral.type, pmd.DihedralType):
+ # TODO: Improper atom order is not always clear in a Parmed object.
+ # This reader assumes the order of impropers is central atom first,
+ # so that is where the central atom is located. This decision comes
+ # from .top files in utils/files/NN-dimethylformamide.top, which
+ # clearly places the periodic impropers with central atom listed first,
+ # and that is where the atom is placed in the parmed.dihedrals object.
+ top_connection = gmso.Improper(
+ connection_members=[
+ site_map[dihedral.atom1],
+ site_map[dihedral.atom2],
+ site_map[dihedral.atom3],
+ site_map[dihedral.atom4],
+ ],
+ improper_type=pmd_top_impropertypes[id(dihedral.type)],
+ )
+ # No bond parameters, make Connection with no connection_type
+ else:
+ top_connection = gmso.Improper(
+ connection_members=[
+ site_map[dihedral.atom1],
+ site_map[dihedral.atom2],
+ site_map[dihedral.atom3],
+ site_map[dihedral.atom4],
+ ],
+ improper_type=None,
+ )
+ top.add_connection(top_connection, update_types=False)
+
else:
- top_connection = gmso.Dihedral(
- connection_members=[
- site_map[dihedral.atom1],
- site_map[dihedral.atom2],
- site_map[dihedral.atom3],
- site_map[dihedral.atom4],
- ],
- dihedral_type=None,
- )
- top.add_connection(top_connection, update_types=False)
+ if refer_type and isinstance(dihedral.type, pmd.DihedralType):
+ top_connection = gmso.Dihedral(
+ connection_members=[
+ site_map[dihedral.atom1],
+ site_map[dihedral.atom2],
+ site_map[dihedral.atom3],
+ site_map[dihedral.atom4],
+ ],
+ dihedral_type=pmd_top_dihedraltypes[id(dihedral.type)],
+ )
+ # No bond parameters, make Connection with no connection_type
+ else:
+ top_connection = gmso.Dihedral(
+ connection_members=[
+ site_map[dihedral.atom1],
+ site_map[dihedral.atom2],
+ site_map[dihedral.atom3],
+ site_map[dihedral.atom4],
+ ],
+ dihedral_type=None,
+ )
+ top.add_connection(top_connection, update_types=False)
for rb_torsion in structure.rb_torsions:
# Generate dihedral parameters for DihedralType that gets passed
@@ -214,7 +261,7 @@ def from_parmed(structure, refer_type=True):
site_map[rb_torsion.atom3],
site_map[rb_torsion.atom4],
],
- dihedral_type=pmd_top_dihedraltypes[rb_torsion.type],
+ dihedral_type=pmd_top_dihedraltypes[id(rb_torsion.type)],
)
# No bond parameters, make Connection with no connection_type
else:
@@ -229,6 +276,36 @@ def from_parmed(structure, refer_type=True):
)
top.add_connection(top_connection, update_types=False)
+ for improper in structure.impropers:
+ if refer_type and isinstance(improper.type, pmd.ImproperType):
+ # TODO: Improper atom order is not always clear in a Parmed object.
+ # This reader assumes the order of impropers is central atom first,
+ # so that is where the central atom is located. This decision comes
+ # from .top files in utils/files/NN-dimethylformamide.top, which
+ # clearly places the periodic impropers with central atom listed first,
+ # and that is where the atom is placed in the parmed.dihedrals object.
+ top_connection = gmso.Improper(
+ connection_members=[
+ site_map[improper.atom1],
+ site_map[improper.atom2],
+ site_map[improper.atom3],
+ site_map[improper.atom4],
+ ],
+ improper_type=pmd_top_impropertypes[improper.type],
+ )
+ # No bond parameters, make Connection with no connection_type
+ else:
+ top_connection = gmso.Improper(
+ connection_members=[
+ site_map[improper.atom1],
+ site_map[improper.atom2],
+ site_map[improper.atom3],
+ site_map[improper.atom4],
+ ],
+ improper_type=None,
+ )
+ top.add_connection(top_connection, update_types=False)
+
top.update_topology()
top.combining_rule = structure.combining_rule
@@ -400,7 +477,7 @@ def _dihedral_types_from_pmd(structure, dihedral_types_member_map=None):
top_dihedraltype = gmso.DihedralType(
potential_expression=expr, member_types=member_types
)
- pmd_top_dihedraltypes[dihedraltype] = top_dihedraltype
+ pmd_top_dihedraltypes[id(dihedraltype)] = top_dihedraltype
for dihedraltype in structure.rb_torsion_types:
dihedral_params = {
@@ -422,10 +499,66 @@ def _dihedral_types_from_pmd(structure, dihedral_types_member_map=None):
independent_variables="phi",
member_types=member_types,
)
- pmd_top_dihedraltypes[dihedraltype] = top_dihedraltype
+ pmd_top_dihedraltypes[id(dihedraltype)] = top_dihedraltype
return pmd_top_dihedraltypes
+def _improper_types_from_pmd(structure, improper_types_member_map=None):
+ """Convert ParmEd improper types to GMSO ImproperType.
+
+ This function take in a Parmed Structure, iterate through its
+ improper_types and dihedral_types with the `improper=True` flag,
+ create a corresponding GMSO.ImproperType, and finally return
+ a dictionary containing all pairs of pmd.ImproperType
+ (or pmd.DihedralType) and GMSO.ImproperType
+
+ Parameters
+ ----------
+ structure: pmd.Structure
+ Parmed Structure that needed to be converted.
+ improper_types_member_map: optional, dict, default=None
+ The member types (atomtype string) for each atom associated with the improper_types the structure
+
+ Returns
+ -------
+ pmd_top_impropertypes : dict
+ A dictionary linking a pmd.ImproperType or pmd.DihedralType
+ object to its corresponding GMSO.ImproperType object.
+ """
+ pmd_top_impropertypes = dict()
+ improper_types_member_map = _assert_dict(
+ improper_types_member_map, "improper_types_member_map"
+ )
+
+ for dihedraltype in structure.dihedral_types:
+ improper_params = {
+ "k": (dihedraltype.phi_k * u.Unit("kcal / mol")),
+ "phi_eq": (dihedraltype.phase * u.degree),
+ "n": dihedraltype.per * u.dimensionless,
+ }
+ expr = lib["PeriodicImproperPotential"]
+ member_types = improper_types_member_map.get(id(dihedraltype))
+ top_impropertype = gmso.ImproperType.from_template(
+ potential_template=expr, parameters=improper_params
+ )
+ pmd_top_impropertypes[id(dihedraltype)] = top_impropertype
+ top_impropertype.member_types = member_types
+
+ for impropertype in structure.improper_types:
+ improper_params = {
+ "k": (impropertype.psi_k * u.Unit("kcal/mol")),
+ "phi_eq": (impropertype.psi_eq * u.Unit("kcal/mol")),
+ }
+ expr = lib["HarmonicImproperPotential"]
+ member_types = improper_types_member_map.get(id(impropertype))
+ top_impropertype = gmso.ImproperType.from_template(
+ potential_template=expr, parameters=improper_params
+ )
+ top_impropertype.member_types = member_types
+ pmd_top_impropertypes[impropertype] = top_impropertype
+ return pmd_top_impropertypes
+
+
def to_parmed(top, refer_type=True):
"""Convert a gmso.topology.Topology to a parmed.Structure.
@@ -460,6 +593,8 @@ def to_parmed(top, refer_type=True):
top.box.lengths.to("angstrom").value,
top.box.angles.to("degree").value,
)
+ if top.box
+ else None
)
# Maps
@@ -479,8 +614,10 @@ def to_parmed(top, refer_type=True):
pmd_atom = pmd.Atom(
atomic_number=atomic_number,
name=site.name,
- mass=site.mass.to(u.amu).value,
- charge=site.charge.to(u.elementary_charge).value,
+ mass=site.mass.to(u.amu).value if site.mass else None,
+ charge=site.charge.to(u.elementary_charge).value
+ if site.charge
+ else None,
)
pmd_atom.xx, pmd_atom.xy, pmd_atom.xz = site.position.to(
"angstrom"
@@ -758,35 +895,56 @@ def _dihedral_types_from_gmso(top, structure, dihedral_map):
structure.rb_torsions.claim()
-def _get_types_map(structure, attr):
+def _get_types_map(structure, attr, impropers=False):
"""Build `member_types` map for atoms, bonds, angles and dihedrals."""
- assert attr in {"atoms", "bonds", "angles", "dihedrals", "rb_torsions"}
+ assert attr in {
+ "atoms",
+ "bonds",
+ "angles",
+ "dihedrals",
+ "rb_torsions",
+ "impropers",
+ }
type_map = {}
for member in getattr(structure, attr):
- conn_type_id, member_types = _get_member_types_map_for(member)
+ conn_type_id, member_types = _get_member_types_map_for(
+ member, impropers
+ )
if conn_type_id not in type_map and all(member_types):
type_map[conn_type_id] = member_types
return type_map
-def _get_member_types_map_for(member):
+def _get_member_types_map_for(member, impropers=False):
if isinstance(member, pmd.Atom):
return id(member.atom_type), member.type
- if isinstance(member, pmd.Bond):
+ elif isinstance(member, pmd.Bond):
return id(member.type), (member.atom1.type, member.atom2.type)
- if isinstance(member, pmd.Angle):
+ elif isinstance(member, pmd.Angle):
return id(member.type), (
member.atom1.type,
member.atom2.type,
member.atom3.type,
)
- if isinstance(member, pmd.Dihedral):
- return id(member.type), (
- member.atom1.type,
- member.atom2.type,
- member.atom3.type,
- member.atom4.type,
- )
+ elif not impropers: # return dihedrals
+ if isinstance(member, pmd.Dihedral) and not member.improper:
+ return id(member.type), (
+ member.atom1.type,
+ member.atom2.type,
+ member.atom3.type,
+ member.atom4.type,
+ )
+ elif impropers: # return impropers
+ if (isinstance(member, pmd.Dihedral) and member.improper) or isinstance(
+ member, pmd.Improper
+ ):
+ return id(member.type), (
+ member.atom1.type,
+ member.atom2.type,
+ member.atom3.type,
+ member.atom4.type,
+ )
+ return None, (None, None)
def _assert_dict(input_dict, param):
diff --git a/gmso/tests/test_convert_parmed.py b/gmso/tests/test_convert_parmed.py
index e6fec88c8..d96531e24 100644
--- a/gmso/tests/test_convert_parmed.py
+++ b/gmso/tests/test_convert_parmed.py
@@ -1,3 +1,5 @@
+import random
+
import foyer
import mbuild as mb
import numpy as np
@@ -335,3 +337,165 @@ def test_parmed_element_non_atomistic(self, pentane_ua_parmed):
for gmso_atom, pmd_atom in zip(top.sites, pentane_ua_parmed.atoms):
assert gmso_atom.element is None
assert pmd_atom.element == 0
+
+ def test_from_parmed_impropers(self):
+ mol = "NN-dimethylformamide"
+ pmd_structure = pmd.load_file(
+ get_fn("{}.top".format(mol)),
+ xyz=get_fn("{}.gro".format(mol)),
+ parametrize=False,
+ )
+ assert all(dihedral.improper for dihedral in pmd_structure.dihedrals)
+ assert len(pmd_structure.rb_torsions) == 16
+
+ gmso_top = from_parmed(pmd_structure)
+ assert len(gmso_top.impropers) == 2
+ for gmso_improper, pmd_improper in zip(
+ gmso_top.impropers, pmd_structure.dihedrals
+ ):
+ pmd_member_names = list(
+ atom.name
+ for atom in [
+ getattr(pmd_improper, f"atom{j+1}") for j in range(4)
+ ]
+ )
+ gmso_member_names = list(
+ map(lambda a: a.name, gmso_improper.connection_members)
+ )
+ assert pmd_member_names == gmso_member_names
+ pmd_structure = pmd.load_file(
+ get_fn("{}.top".format(mol)),
+ xyz=get_fn("{}.gro".format(mol)),
+ parametrize=False,
+ )
+ assert all(dihedral.improper for dihedral in pmd_structure.dihedrals)
+ assert len(pmd_structure.rb_torsions) == 16
+ gmso_top = from_parmed(pmd_structure)
+ assert (
+ gmso_top.impropers[0].improper_type.name
+ == "PeriodicImproperPotential"
+ )
+
+ def test_simple_pmd_dihedrals_no_types(self):
+ struct = pmd.Structure()
+ all_atoms = []
+ for j in range(25):
+ atom = pmd.Atom(
+ atomic_number=j + 1,
+ type=f"atom_type_{j + 1}",
+ charge=random.randint(1, 10),
+ mass=1.0,
+ )
+ atom.xx, atom.xy, atom.xz = (
+ random.random(),
+ random.random(),
+ random.random(),
+ )
+ all_atoms.append(atom)
+ struct.add_atom(atom, "RES", 1)
+
+ for j in range(10):
+ dih = pmd.Dihedral(
+ *random.sample(struct.atoms, 4),
+ improper=True if j % 2 == 0 else False,
+ )
+ struct.dihedrals.append(dih)
+
+ gmso_top = from_parmed(struct)
+ assert len(gmso_top.impropers) == 5
+ assert len(gmso_top.dihedrals) == 5
+ assert len(gmso_top.improper_types) == 0
+ assert len(gmso_top.dihedral_types) == 0
+
+ def test_simple_pmd_dihedrals_impropers(self):
+ struct = pmd.Structure()
+ all_atoms = []
+ for j in range(25):
+ atom = pmd.Atom(
+ atomic_number=j + 1,
+ type=f"atom_type_{j + 1}",
+ charge=random.randint(1, 10),
+ mass=1.0,
+ )
+ atom.xx, atom.xy, atom.xz = (
+ random.random(),
+ random.random(),
+ random.random(),
+ )
+ all_atoms.append(atom)
+ struct.add_atom(atom, "RES", 1)
+
+ for j in range(10):
+ dih = pmd.Dihedral(
+ *random.sample(struct.atoms, 4),
+ improper=True if j % 2 == 0 else False,
+ )
+ struct.dihedrals.append(dih)
+ dtype = pmd.DihedralType(
+ random.random(), random.random(), random.random()
+ )
+ dih.type = dtype
+ struct.dihedral_types.append(dtype)
+
+ gmso_top = from_parmed(struct)
+ assert len(gmso_top.impropers) == 5
+ assert len(gmso_top.dihedrals) == 5
+ assert len(gmso_top.improper_types) == 5
+ assert len(gmso_top.dihedral_types) == 5
+
+ def test_pmd_improper_types(self):
+ struct = pmd.Structure()
+ all_atoms = []
+ for j in range(25):
+ atom = pmd.Atom(
+ atomic_number=j + 1,
+ type=f"atom_type_{j + 1}",
+ charge=random.randint(1, 10),
+ mass=1.0,
+ )
+ atom.xx, atom.xy, atom.xz = (
+ random.random(),
+ random.random(),
+ random.random(),
+ )
+ all_atoms.append(atom)
+ struct.add_atom(atom, "RES", 1)
+
+ for j in range(10):
+ struct.impropers.append(
+ pmd.Improper(*random.sample(struct.atoms, 4))
+ )
+ for improp in struct.impropers:
+ improp.type = pmd.ImproperType(random.random(), random.random())
+ struct.improper_types.append(improp.type)
+
+ gmso_top = from_parmed(struct)
+ assert len(gmso_top.impropers) == len(struct.impropers)
+ assert len(gmso_top.improper_types) == len(struct.improper_types)
+
+ def test_pmd_improper_no_types(self):
+ struct = pmd.Structure()
+ all_atoms = []
+ for j in range(25):
+ atom = pmd.Atom(
+ atomic_number=j + 1,
+ type=f"atom_type_{j + 1}",
+ charge=random.randint(1, 10),
+ mass=1.0,
+ )
+ atom.xx, atom.xy, atom.xz = (
+ random.random(),
+ random.random(),
+ random.random(),
+ )
+ all_atoms.append(atom)
+ struct.add_atom(atom, "RES", 1)
+
+ for j in range(10):
+ struct.impropers.append(
+ pmd.Improper(*random.sample(struct.atoms, 4))
+ )
+
+ gmso_top = from_parmed(struct)
+ assert len(gmso_top.impropers) == 10
+ assert len(gmso_top.improper_types) == 0
diff --git a/gmso/utils/files/NN-dimethylformamide.gro b/gmso/utils/files/NN-dimethylformamide.gro
new file mode 100644
index 000000000..15f8dab14
--- /dev/null
+++ b/gmso/utils/files/NN-dimethylformamide.gro
@@ -0,0 +1,15 @@
+NN-dimethylformamide GAS
+ 12
+ 1LIG C 1 4.999 5.057 4.878
+ 1LIG H 2 4.909 5.017 4.831
+ 1LIG H 3 4.992 5.165 4.868
+ 1LIG H 4 5.081 5.024 4.814
+ 1LIG N 5 5.010 5.010 5.015
+ 1LIG C 6 4.926 4.898 5.053
+ 1LIG H 7 4.967 4.814 4.996
+ 1LIG H 8 4.924 4.865 5.157
+ 1LIG H 9 4.824 4.920 5.023
+ 1LIG C 10 5.095 5.066 5.101
+ 1LIG H 11 5.095 5.013 5.197
+ 1LIG O 12 5.182 5.146 5.068
+ 10.00000 10.00000 10.00000
diff --git a/gmso/utils/files/NN-dimethylformamide.top b/gmso/utils/files/NN-dimethylformamide.top
new file mode 100644
index 000000000..106e9d897
--- /dev/null
+++ b/gmso/utils/files/NN-dimethylformamide.top
@@ -0,0 +1,106 @@
+; OPLSAA topology for NN-dimethylformamide
+;
+; Jorgensen, W. L.; Tirado-Rives, J. Proc. Natl. Acad. Sci. U.S.A. 2005, 102, 6665.
+; Carl Caleman, Paul J. van Maaren, Minyan Hong, Jochen S. Hub, Luciano T. Costa and David van der Spoel, Force Field Benchmark of Organic Liquids: Density, Enthalpy of Vaporization, Heat Capacities, Surface Tension, Isothermal Compressibility, Volumetric Expansion Coefficient, and Dielectric Constant, J. Chem. Theor. Comput. 8 (2012) http://dx.doi.org/10.1021/ct200731v
+;
+;
+;include "../oplsaa.ff/forcefield.itp"
+[ moleculetype ]
+; Name nrexcl
+NN-dimethylformamide 3
+
+[ atoms ]
+; nr type resnr residue atom cgnr charge mass typeB chargeB massB
+ 1 opls_243 1 LIG C 1 -0.11 12.011
+ 2 opls_140 1 LIG H 2 0.06 1.008
+ 3 opls_140 1 LIG H 3 0.06 1.008
+ 4 opls_140 1 LIG H 4 0.06 1.008
+ 5 opls_239 1 LIG N 5 -0.14 14.0067
+ 6 opls_243 1 LIG C 6 -0.11 12.011
+ 7 opls_140 1 LIG H 7 0.06 1.008
+ 8 opls_140 1 LIG H 8 0.06 1.008
+ 9 opls_140 1 LIG H 9 0.06 1.008
+ 10 opls_235 1 LIG C 10 0.5 12.011
+ 11 opls_279 1 LIG H 11 0.00 1.008
+ 12 opls_236 1 LIG O 12 -0.5 15.9994
+
+[ bonds ]
+; ai aj funct c0 c1 c2 c3
+ 1 2 1
+ 1 3 1
+ 1 4 1
+ 1 5 1
+ 5 6 1
+ 5 10 1
+ 6 7 1
+ 6 8 1
+ 6 9 1
+ 10 11 1
+ 10 12 1
+
+[ pairs ]
+; ai aj funct c0 c1 c2 c3
+ 1 7 1
+ 1 8 1
+ 1 9 1
+ 1 11 1
+ 1 12 1
+ 2 6 1
+ 2 10 1
+ 3 6 1
+ 3 10 1
+ 4 6 1
+ 4 10 1
+ 6 11 1
+ 6 12 1
+ 7 10 1
+ 8 10 1
+ 9 10 1
+
+[ angles ]
+; ai aj ak funct c0 c1 c2 c3
+ 2 1 3 1
+ 2 1 4 1
+ 2 1 5 1
+ 3 1 4 1
+ 3 1 5 1
+ 4 1 5 1
+ 1 5 6 1
+ 1 5 10 1
+ 6 5 10 1
+ 5 6 7 1
+ 5 6 8 1
+ 5 6 9 1
+ 7 6 8 1
+ 7 6 9 1
+ 8 6 9 1
+ 5 10 11 1
+ 5 10 12 1
+ 11 10 12 1
+
+[ dihedrals ]
+; ai aj ak al funct c0 c1 c2 c3 c4 c5
+ 2 1 5 6 3
+ 2 1 5 10 3
+ 3 1 5 6 3
+ 3 1 5 10 3
+ 4 1 5 6 3
+ 4 1 5 10 3
+ 1 5 6 7 3
+ 1 5 6 8 3
+ 1 5 6 9 3
+ 10 5 6 7 3
+ 10 5 6 8 3
+ 10 5 6 9 3
+ 1 5 10 11 3
+ 1 5 10 12 3
+ 6 5 10 11 3
+ 6 5 10 12 3
+
+; Added DvdS 2010-12-21
+10 12 5 11 4 180 4.6 2
+ 5 1 6 10 4 180 4.6 2
+ [ system ]
+NN-dimethylformamide GAS
+[ molecules ]
+NN-dimethylformamide 1
From 99e7d2c455c60053d8e521af9b2c2972b319a38e Mon Sep 17 00:00:00 2001
From: Umesh Timalsina
Date: Tue, 7 Jun 2022 10:23:05 -0500
Subject: [PATCH 062/141] Add expected parameters dimensions to
`PotentialTemplates` (#657)
* Add expected parameter dimensions to PotentialTemplates
* Extend expression class to support creation from non_parametric; Additional tests
* WIP- remove unused imports
* fix expecting unit for buckingham potential
* fix test for buckingham potential
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
Co-authored-by: Co Quach
---
gmso/core/parametric_potential.py | 33 ++---
gmso/exceptions.py | 26 ++++
gmso/lib/jsons/BuckinghamPotential.json | 7 +-
gmso/lib/jsons/HarmonicAnglePotential.json | 6 +-
gmso/lib/jsons/HarmonicBondPotential.json | 6 +-
gmso/lib/jsons/HarmonicImproperPotential.json | 6 +-
gmso/lib/jsons/HarmonicTorsionPotential.json | 6 +-
gmso/lib/jsons/LennardJonesPotential.json | 6 +-
gmso/lib/jsons/MiePotential.json | 8 +-
gmso/lib/jsons/OPLSTorsionPotential.json | 10 +-
gmso/lib/jsons/PeriodicImproperPotential.json | 7 +-
gmso/lib/jsons/PeriodicTorsionPotential.json | 7 +-
.../RyckaertBellemansTorsionPotential.json | 10 +-
gmso/lib/potential_templates.py | 91 +++++++++++++-
gmso/tests/test_expression.py | 67 ++++++++++
gmso/tests/test_potential.py | 18 ++-
gmso/tests/test_potential_templates.py | 79 +++++++++++-
gmso/tests/test_template.py | 115 ++++++++++++++++++
gmso/utils/expression.py | 53 ++++++++
19 files changed, 527 insertions(+), 34 deletions(-)
diff --git a/gmso/core/parametric_potential.py b/gmso/core/parametric_potential.py
index efac3ef52..e65bcc5ef 100644
--- a/gmso/core/parametric_potential.py
+++ b/gmso/core/parametric_potential.py
@@ -1,11 +1,9 @@
from copy import copy, deepcopy
-from typing import Any, Optional, Union
+from typing import Any, Union
import unyt as u
-from pydantic import Field, validator
from gmso.abc.abstract_potential import AbstractPotential
-from gmso.exceptions import GMSOError
from gmso.utils.expression import PotentialExpression
@@ -199,17 +197,20 @@ def clone(self):
)
@classmethod
- def from_template(cls, potential_template, parameters, topology=None):
+ def from_template(cls, potential_template, parameters, name=None, **kwargs):
"""Create a potential object from the potential_template.
Parameters
----------
potential_template : gmso.lib.potential_templates.PotentialTemplate,
- The potential template object
+ The potential template object
parameters : dict,
- The parameters of the potential object to create
- topology : gmso.Topology, default=None
- The topology to which the created potential object belongs to
+ The parameters of the potential object to create
+ name: str, default=None,
+ The new name for the created parametric potential, defaults to the
+ template name.
+ **kwargs: dict
+ The remaining keyword arguments to the Parametric potential's constructor.
Returns
-------
@@ -224,15 +225,19 @@ def from_template(cls, potential_template, parameters, topology=None):
from gmso.lib.potential_templates import PotentialTemplate
if not isinstance(potential_template, PotentialTemplate):
- raise GMSOError(
- f"Object {type(potential_template)} is not an instance of PotentialTemplate."
+ raise TypeError(
+ f"Object {potential_template} of type {type(potential_template)} is not an instance of "
+ f"PotentialTemplate."
)
+ potential_template.assert_can_parameterize_with(parameters)
+ new_expression = PotentialExpression.from_non_parametric(
+ potential_template.potential_expression, parameters, valid=True
+ )
return cls(
- name=potential_template.name,
- expression=potential_template.expression,
- independent_variables=potential_template.independent_variables,
- parameters=parameters,
+ name=name or potential_template.name,
+ potential_expression=new_expression,
+ **kwargs,
)
def __repr__(self):
diff --git a/gmso/exceptions.py b/gmso/exceptions.py
index d9ea3c028..00158e616 100644
--- a/gmso/exceptions.py
+++ b/gmso/exceptions.py
@@ -32,3 +32,29 @@ class MixedClassAndTypesError(ForceFieldParseError):
class MissingPotentialError(ForceFieldError):
"""Error for missing Potential when searching for Potentials in a ForceField."""
+
+
+class ParameterError(GMSOError):
+ """Errors related to parameters."""
+
+ def __init__(self, param, expected):
+ self.param = param
+ self.params = expected
+
+
+class UnknownParameterError(ParameterError):
+ """Errors to be raised when a parameter is unknown."""
+
+ def __str__(self):
+ """Error message."""
+ err = f"Parameter {self.param} is not one of the expected parameters {self.params}"
+ return err
+
+
+class MissingParameterError(ParameterError):
+ """Error to be raised when a parameter is missing."""
+
+ def __str__(self):
+ """Error message."""
+ err = f"Parameter '{self.param}' missing from the provided parameters {self.params}"
+ return err
diff --git a/gmso/lib/jsons/BuckinghamPotential.json b/gmso/lib/jsons/BuckinghamPotential.json
index e96ef819e..667e0ca0b 100644
--- a/gmso/lib/jsons/BuckinghamPotential.json
+++ b/gmso/lib/jsons/BuckinghamPotential.json
@@ -1,5 +1,10 @@
{
"name": "BuckinghamPotential",
"expression": "a*exp(-b*r) - c*r**-6",
- "independent_variables": "r"
+ "independent_variables": "r",
+ "expected_parameters_dimensions": {
+ "a": "energy",
+ "b": "1/length",
+ "c": "energy*length**6"
+ }
}
diff --git a/gmso/lib/jsons/HarmonicAnglePotential.json b/gmso/lib/jsons/HarmonicAnglePotential.json
index b7ac786f1..8a0be7cba 100644
--- a/gmso/lib/jsons/HarmonicAnglePotential.json
+++ b/gmso/lib/jsons/HarmonicAnglePotential.json
@@ -1,5 +1,9 @@
{
"name": "HarmonicAnglePotential",
"expression": "0.5 * k * (theta-theta_eq)**2",
- "independent_variables": "theta"
+ "independent_variables": "theta",
+ "expected_parameters_dimensions": {
+ "k":"energy/angle**2",
+ "theta_eq": "angle"
+ }
}
diff --git a/gmso/lib/jsons/HarmonicBondPotential.json b/gmso/lib/jsons/HarmonicBondPotential.json
index a41c0a3eb..ce897e028 100644
--- a/gmso/lib/jsons/HarmonicBondPotential.json
+++ b/gmso/lib/jsons/HarmonicBondPotential.json
@@ -1,5 +1,9 @@
{
"name": "HarmonicBondPotential",
"expression": "0.5 * k * (r-r_eq)**2",
- "independent_variables": "r"
+ "independent_variables": "r",
+ "expected_parameters_dimensions": {
+ "k": "energy/length**2",
+ "r_eq": "length"
+ }
}
diff --git a/gmso/lib/jsons/HarmonicImproperPotential.json b/gmso/lib/jsons/HarmonicImproperPotential.json
index 653e72dbd..c5b5a6f73 100644
--- a/gmso/lib/jsons/HarmonicImproperPotential.json
+++ b/gmso/lib/jsons/HarmonicImproperPotential.json
@@ -1,5 +1,9 @@
{
"name": "HarmonicImproperPotential",
"expression": "0.5 * k * (phi - phi_eq)**2",
- "independent_variables": "phi"
+ "independent_variables": "phi",
+ "expected_parameters_dimensions": {
+ "k": "energy/angle**2",
+ "phi_eq": "angle"
+ }
}
diff --git a/gmso/lib/jsons/HarmonicTorsionPotential.json b/gmso/lib/jsons/HarmonicTorsionPotential.json
index d65f2acf9..6c12d00ae 100644
--- a/gmso/lib/jsons/HarmonicTorsionPotential.json
+++ b/gmso/lib/jsons/HarmonicTorsionPotential.json
@@ -1,5 +1,9 @@
{
"name": "HarmonicTorsionPotential",
"expression": "0.5 * k * (phi - phi_eq)**2",
- "independent_variables": "phi"
+ "independent_variables": "phi",
+ "expected_parameters_dimensions": {
+ "k": "energy/angle**2",
+ "phi_eq": "angle"
+ }
}
diff --git a/gmso/lib/jsons/LennardJonesPotential.json b/gmso/lib/jsons/LennardJonesPotential.json
index 376de6dcd..ffeafaa91 100644
--- a/gmso/lib/jsons/LennardJonesPotential.json
+++ b/gmso/lib/jsons/LennardJonesPotential.json
@@ -1,5 +1,9 @@
{
"name": "LennardJonesPotential",
"expression": "4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
- "independent_variables": "r"
+ "independent_variables": "r",
+ "expected_parameters_dimensions": {
+ "sigma": "length",
+ "epsilon": "energy"
+ }
}
diff --git a/gmso/lib/jsons/MiePotential.json b/gmso/lib/jsons/MiePotential.json
index f26b92118..6f550194d 100644
--- a/gmso/lib/jsons/MiePotential.json
+++ b/gmso/lib/jsons/MiePotential.json
@@ -1,5 +1,11 @@
{
"name": "MiePotential",
"expression": "(n/(n-m)) * (n/m)**(m/(n-m)) * epsilon * ((sigma/r)**n - (sigma/r)**m)",
- "independent_variables": "r"
+ "independent_variables": "r",
+ "expected_parameters_dimensions": {
+ "n": "dimensionless",
+ "m": "dimensionless",
+ "epsilon": "energy",
+ "sigma": "length"
+ }
}
diff --git a/gmso/lib/jsons/OPLSTorsionPotential.json b/gmso/lib/jsons/OPLSTorsionPotential.json
index c792cfc8f..2adb76cc2 100644
--- a/gmso/lib/jsons/OPLSTorsionPotential.json
+++ b/gmso/lib/jsons/OPLSTorsionPotential.json
@@ -1,5 +1,13 @@
{
"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))",
- "independent_variables": "phi"
+ "independent_variables": "phi",
+ "expected_parameters_dimensions": {
+ "k0": "energy",
+ "k1": "energy",
+ "k2": "energy",
+ "k3": "energy",
+ "k4": "energy",
+ "k5": "energy"
+ }
}
diff --git a/gmso/lib/jsons/PeriodicImproperPotential.json b/gmso/lib/jsons/PeriodicImproperPotential.json
index 298d149fc..f945c8fec 100644
--- a/gmso/lib/jsons/PeriodicImproperPotential.json
+++ b/gmso/lib/jsons/PeriodicImproperPotential.json
@@ -1,5 +1,10 @@
{
"name": "PeriodicImproperPotential",
"expression": "k * (1 + cos(n * phi - phi_eq))",
- "independent_variables": "phi"
+ "independent_variables": "phi",
+ "expected_parameters_dimensions": {
+ "k": "energy",
+ "n": "dimensionless",
+ "phi_eq": "angle"
+ }
}
diff --git a/gmso/lib/jsons/PeriodicTorsionPotential.json b/gmso/lib/jsons/PeriodicTorsionPotential.json
index 84ada496b..85affe3d4 100644
--- a/gmso/lib/jsons/PeriodicTorsionPotential.json
+++ b/gmso/lib/jsons/PeriodicTorsionPotential.json
@@ -1,5 +1,10 @@
{
"name": "PeriodicTorsionPotential",
"expression": "k * (1 + cos(n * phi - phi_eq))",
- "independent_variables": "phi"
+ "independent_variables": "phi",
+ "expected_parameters_dimensions": {
+ "k": "energy",
+ "n": "dimensionless",
+ "phi_eq": "angle"
+ }
}
diff --git a/gmso/lib/jsons/RyckaertBellemansTorsionPotential.json b/gmso/lib/jsons/RyckaertBellemansTorsionPotential.json
index ac59d0fe2..e4f473b46 100644
--- a/gmso/lib/jsons/RyckaertBellemansTorsionPotential.json
+++ b/gmso/lib/jsons/RyckaertBellemansTorsionPotential.json
@@ -1,5 +1,13 @@
{
"name": "RyckaertBellemansTorsionPotential",
"expression": "c0 * cos(phi)**0 + c1 * cos(phi)**1 + c2 * cos(phi)**2 + c3 * cos(phi)**3 + c4 * cos(phi)**4 + c5 * cos(phi)**5",
- "independent_variables": "phi"
+ "independent_variables": "phi",
+ "expected_parameters_dimensions": {
+ "c0": "energy",
+ "c1": "energy",
+ "c2": "energy",
+ "c3": "energy",
+ "c4": "energy",
+ "c5": "energy"
+ }
}
diff --git a/gmso/lib/potential_templates.py b/gmso/lib/potential_templates.py
index c7fdc98cc..1e29c63ad 100644
--- a/gmso/lib/potential_templates.py
+++ b/gmso/lib/potential_templates.py
@@ -1,9 +1,18 @@
"""Module supporting template potential objects."""
import json
from pathlib import Path
+from typing import Dict
+
+import sympy
+import unyt as u
+from pydantic import Field, validator
from gmso.abc.abstract_potential import AbstractPotential
-from gmso.exceptions import GMSOError
+from gmso.exceptions import (
+ GMSOError,
+ MissingParameterError,
+ UnknownParameterError,
+)
from gmso.utils.expression import PotentialExpression
from gmso.utils.singleton import Singleton
@@ -39,13 +48,17 @@ def _load_template_json(item, json_dir=JSON_DIR):
class PotentialTemplate(AbstractPotential):
"""Template for potential objects to be re-used."""
+ expected_parameters_dimensions_: Dict[str, sympy.Expr] = Field(
+ ..., description="The expected dimensions for parameters."
+ )
+
def __init__(
self,
name="PotentialTemplate",
expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
independent_variables="r",
potential_expression=None,
- template=True,
+ expected_parameters_dimensions=None,
):
if not isinstance(independent_variables, set):
independent_variables = set(independent_variables.split(","))
@@ -59,17 +72,89 @@ def __init__(
_potential_expression = potential_expression
super(PotentialTemplate, self).__init__(
- name=name, potential_expression=_potential_expression
+ name=name,
+ potential_expression=_potential_expression,
+ expected_parameters_dimensions=expected_parameters_dimensions,
)
+ @validator("expected_parameters_dimensions_", pre=True, always=True)
+ def validate_expected_parameters(cls, dim_dict):
+ """Validate the expected parameters and dimensions for this template."""
+ if not isinstance(dim_dict, Dict):
+ raise TypeError(
+ f"Expected expected_parameters_dimensions to be a "
+ f"dictionary but found {type(dim_dict)}"
+ )
+ for param_name, dim in dim_dict.items():
+ if not isinstance(dim, sympy.Expr):
+ try:
+ dimension = getattr(u.dimensions, dim)
+ except AttributeError:
+ dimension_expr = sympy.sympify(dim)
+ subs = (
+ (symbol, getattr(u.dimensions, str(symbol)))
+ for symbol in dimension_expr.free_symbols
+ )
+ dimension = dimension_expr.subs(subs)
+ dim_dict[param_name] = dimension
+ return dim_dict
+
+ @property
+ def expected_parameters_dimensions(self):
+ """Return the expected dimensions of the parameters for this template."""
+ return self.__dict__.get("expected_parameters_dimensions_")
+
def set_expression(self, *args, **kwargs):
"""Set the expression of the PotentialTemplate."""
raise NotImplementedError
+ def assert_can_parameterize_with(
+ self, parameters: Dict[str, u.unyt_quantity]
+ ) -> None:
+ """Assert that a ParametricPotential can be instantiated from this template and provided parameters."""
+ if not isinstance(parameters, dict):
+ raise TypeError("Provided `parameters` is not a dictionary.")
+
+ for param_name in self.expected_parameters_dimensions:
+ if param_name not in parameters:
+ raise MissingParameterError(param_name, list(parameters))
+
+ for param_name, param_value in parameters.items():
+ quantity = param_value
+ if not (isinstance(param_value, u.unyt_array)):
+ raise ValueError(f"Parameter {param_name} lacks a unit.")
+
+ if param_name not in self.expected_parameters_dimensions:
+ raise UnknownParameterError(
+ param_name, list(self.expected_parameters_dimensions)
+ )
+ expected_param_dimension = self.expected_parameters_dimensions[
+ param_name
+ ]
+ param_dimension = quantity.units.dimensions
+ if param_dimension != expected_param_dimension:
+ if expected_param_dimension == 1:
+ expected_param_dimension = "dimensionless"
+ if param_dimension == 1:
+ param_dimension = "dimensionless"
+
+ raise AssertionError(
+ f"Expected parameter {param_name} to have "
+ f"dimension {expected_param_dimension} but found {param_dimension}. "
+ f"So, a {self.__class__.__name__} cannot be instantiated using the provided "
+ f"parameters: {parameters}"
+ )
+
class Config:
"""Pydantic configuration for potential template."""
allow_mutation = False
+ fields = {
+ "expected_parameters_dimensions_": "expected_parameters_dimensions"
+ }
+ alias_to_fields = {
+ "expected_parameters_dimensions": "expected_parameters_dimensions_"
+ }
class PotentialTemplateLibrary(Singleton):
diff --git a/gmso/tests/test_expression.py b/gmso/tests/test_expression.py
index 40ebbe664..b8a1a35ed 100644
--- a/gmso/tests/test_expression.py
+++ b/gmso/tests/test_expression.py
@@ -233,6 +233,73 @@ def test_clone(self):
assert expr == expr_clone
+ def test_from_non_parametric(self):
+ non_parametric = PotentialExpression(
+ expression="x**2+z*x*y+y**2", independent_variables={"z"}
+ )
+
+ parametric = PotentialExpression.from_non_parametric(
+ non_parametric,
+ parameters={"x": 2.9 * u.dimensionless, "y": 10000 * u.m},
+ valid=True,
+ )
+
+ assert parametric.expression == non_parametric.expression
+ assert id(parametric.expression) != id(non_parametric.expression)
+ assert (
+ parametric.independent_variables
+ == non_parametric.independent_variables
+ )
+ parametric.independent_variables.add("X")
+ assert (
+ parametric.independent_variables
+ != non_parametric.independent_variables
+ )
+
+ def test_from_non_parametric_errors(self):
+
+ with pytest.raises(
+ TypeError,
+ match="Expected
-
-
-
-
+
+
+
+
-
-
+
+
-
-
+
+
diff --git a/gmso/tests/files/opls_charmm_buck.xml b/gmso/tests/files/opls_charmm_buck.xml
index b50e64a34..f046b22aa 100644
--- a/gmso/tests/files/opls_charmm_buck.xml
+++ b/gmso/tests/files/opls_charmm_buck.xml
@@ -189,29 +189,20 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+ 0.0
+
+
+ 1.0
+
+
+ 0.0
+
diff --git a/gmso/tests/files/tmp.xml b/gmso/tests/files/tmp.xml
deleted file mode 100644
index e69de29bb..000000000
diff --git a/gmso/tests/files/trimmed_charmm.xml b/gmso/tests/files/trimmed_charmm.xml
index 7c84f9b00..8a6fc4b22 100644
--- a/gmso/tests/files/trimmed_charmm.xml
+++ b/gmso/tests/files/trimmed_charmm.xml
@@ -34,39 +34,36 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+ 0.0
+
+
+ 3.0
+
+
+ 0.003490658503988659
+
-
-
-
-
-
-
+
+ 0.6276
+ 35.564
+
+
+ 1.0
+ 2.0
+
+
+ 0.0
+ 3.141592653589793
+
diff --git a/gmso/tests/test_forcefield.py b/gmso/tests/test_forcefield.py
index c8242166b..eef46e423 100644
--- a/gmso/tests/test_forcefield.py
+++ b/gmso/tests/test_forcefield.py
@@ -3,6 +3,7 @@
import unyt as u
from lxml.etree import DocumentInvalid
from sympy import sympify
+from unyt.testing import assert_allclose_units
from gmso.core.forcefield import ForceField
from gmso.core.improper_type import ImproperType
@@ -50,17 +51,14 @@ def test_ff_combining_rule(self, ff, opls_ethane_foyer):
@pytest.mark.parametrize(
"unit_name,unit_value",
[
- ("energy", u.Unit(u.K * u.kb)),
+ ("energy", u.Unit(u.kb)),
("mass", u.gram / u.mol),
- ("temperature", u.K),
("charge", u.coulomb),
- ("angle", u.rad),
- ("time", u.ps),
("distance", u.nm),
],
)
def test_units_from_xml(self, ff, unit_name, unit_value):
- assert len(ff.units.keys()) == 7
+ assert len(ff.units.keys()) == 4
assert ff.units[unit_name] == unit_value
def test_ff_atomtypes_from_xml(self, ff):
@@ -228,14 +226,13 @@ def test_ff_pairpotentialtypes_from_xml(self, ff):
assert ff.pairpotential_types["Xe~Xe"].member_types == ("Xe", "Xe")
def test_ff_charmm_xml(self):
- charm_ff = ForceField(get_path("trimmed_charmm.xml"))
+ charm_ff = ForceField(get_path("trimmed_charmm.xml"), backend="gmso")
assert charm_ff.name == "topologyCharmm"
assert "*~CS~SS~*" in charm_ff.dihedral_types
- # Test list of parameters
assert isinstance(
- charm_ff.dihedral_types["*~CE1~CE1~*"].parameters["k"], list
+ charm_ff.dihedral_types["*~CE1~CE1~*"].parameters["k"], u.unyt_array
)
# This ensures that even though the parameters is a list, they can be hashed (by equality checks)
@@ -246,18 +243,23 @@ def test_ff_charmm_xml(self):
assert len(charm_ff.dihedral_types["*~CE1~CE1~*"].parameters["k"]) == 2
# Test Correct Parameter Values
- assert charm_ff.dihedral_types["*~CE1~CE1~*"].parameters["k"] == [
- u.unyt_quantity(0.6276, u.kJ),
- u.unyt_quantity(35.564, u.kJ),
- ]
+ assert_allclose_units(
+ charm_ff.dihedral_types["*~CE1~CE1~*"].parameters["k"],
+ [0.6276, 35.564] * u.kJ,
+ rtol=1e-5,
+ atol=1e-8,
+ )
def test_non_unique_params(self):
with pytest.raises(DocumentInvalid):
ForceField(get_path("ff-example-nonunique-params.xml"))
def test_missing_params(self):
+ # TODO: raise same error if backend loader is forcefield-utilities
with pytest.raises(ForceFieldParseError):
- ForceField(get_path("ff-example-missing-parameter.xml"))
+ ForceField(
+ get_path("ff-example-missing-parameter.xml"), backend="gmso"
+ )
def test_elementary_charge_to_coulomb(self, ff):
elementary_charge = ff.atom_types["Li"].charge.to(u.elementary_charge)
@@ -274,26 +276,34 @@ def test_ff_periodic_dihedrals_from_alphanumeric_symbols(self):
assert len(
ff.dihedral_types["opls_140~*~*~opls_140"].parameters["c0"]
)
- assert len(ff.dihedral_types["NH2~CT1~C~O"].parameters["delta"]) == 1
+ assert ff.dihedral_types["NH2~CT1~C~O"].parameters[
+ "delta"
+ ] == u.unyt_quantity(0.0, "degree")
def test_ff_from_etree(self):
+ # TODO: load using backend forcefield-utilities from etree
ff_etree = lxml.etree.parse(get_path("opls_charmm_buck.xml"))
- ff = ForceField(ff_etree)
+ ff = ForceField(ff_etree, backend="gmso")
assert ff
def test_ff_from_etree_iterable(self):
+ # TODO: load using backend forcefield-utilities from etree
ff_etrees = [
lxml.etree.parse(get_path("opls_charmm_buck.xml")),
lxml.etree.parse(get_path("trimmed_charmm.xml")),
]
- ff = ForceField(ff_etrees)
+ ff = ForceField(ff_etrees, backend="gmso")
assert ff
def test_ff_mixed_type_error(self):
with pytest.raises(TypeError):
ff = ForceField([5, "20"])
- def test_named_potential_groups(self, named_groups_ff):
+ def test_named_potential_groups(self):
+ # TODO: get potential groups using backend forcefield-utilities
+ named_groups_ff = ForceField(
+ get_path("ff-example1.xml"), backend="gmso"
+ )
assert named_groups_ff.potential_groups["BuckinghamPotential"]
assert (
named_groups_ff.angle_types["Xe~Xe~Xe"]
@@ -353,13 +363,15 @@ def test_potential_types_by_expression(self, named_groups_ff):
def test_forcefield_missing_atom_types(self):
with pytest.raises(MissingAtomTypesError):
ff = ForceField(
- get_path(filename=get_path("ff_missing_atom_types.xml"))
+ get_path(filename=get_path("ff_missing_atom_types.xml")),
+ backend="gmso",
)
def test_forcefield_missing_atom_types_non_strict(self):
ff = ForceField(
get_path(filename=get_path("ff_missing_atom_types.xml")),
strict=False,
+ backend="gmso",
)
def test_forcefeld_get_potential_atom_type(self, opls_ethane_foyer):
@@ -616,7 +628,7 @@ def test_forcefield_get_impropers_combinations(self):
assert imp1 is imp2
def test_write_xml(self, opls_ethane_foyer):
- opls_ethane_foyer.xml("test_xml_writer.xml")
+ opls_ethane_foyer.to_xml("test_xml_writer.xml")
reloaded_xml = ForceField("test_xml_writer.xml")
get_names = lambda ff, param: [
typed for typed in getattr(ff, param).keys()
@@ -633,7 +645,7 @@ def test_write_xml(self, opls_ethane_foyer):
def test_write_not_xml(self, opls_ethane_foyer):
with pytest.raises(ForceFieldError):
- opls_ethane_foyer.xml("bad_path")
+ opls_ethane_foyer.to_xml("bad_path")
def test_valid_sequence(self):
for j in range(10):
@@ -642,3 +654,7 @@ def test_valid_sequence(self):
params = dih_with_list.get_parameters()
assert u.allclose_units(params["theta_0"], [25, 32] * u.radian)
assert u.allclose_units(params["k"], [38, 45] * u.kJ / u.mol)
+
+ def test_deprecated_gmso(self):
+ with pytest.warns(DeprecationWarning):
+ ForceField(get_path("ff-example0.xml"), backend="gmso")
diff --git a/gmso/tests/test_xml_handling.py b/gmso/tests/test_xml_handling.py
new file mode 100644
index 000000000..320745890
--- /dev/null
+++ b/gmso/tests/test_xml_handling.py
@@ -0,0 +1,111 @@
+import glob
+import os
+
+import pytest
+from forcefield_utilities import GMSOFFs
+
+from gmso.core.forcefield import ForceField
+from gmso.tests.base_test import BaseTest
+from gmso.tests.utils import get_path
+from gmso.utils.io import get_fn
+
+# Make source directory for all xmls to grab from
+XML_DIR = get_fn("gmso_xmls")
+TEST_XMLS = glob.glob(os.path.join(XML_DIR, "*/*.xml"))
+
+
+def compare_xml_files(fn1, fn2):
+ """Hash files to check for lossless conversion."""
+ with open(fn1, "r") as f:
+ line2 = f.readlines()
+ with open(fn2, "r") as f:
+ line1 = f.readlines()
+ for l1, l2 in zip(line1, line2):
+ assert l1.replace(" ", "") == l2.replace(" ", "")
+ return True
+
+
+class TestXMLHandling(BaseTest):
+ @pytest.fixture
+ def ff(self):
+ return ForceField(get_path("ff-example0.xml"))
+
+ @pytest.fixture
+ def named_groups_ff(self):
+ return ForceField(get_path("ff-example1.xml"))
+
+ @pytest.fixture
+ def opls_ethane_foyer(self):
+ return ForceField(
+ get_path(filename=get_path("oplsaa-ethane_foyer.xml"))
+ )
+
+ def test_write_xml(self, opls_ethane_foyer):
+ opls_ethane_foyer.to_xml("test_xml_writer.xml")
+ reloaded_xml = ForceField("test_xml_writer.xml")
+ get_names = lambda ff, param: [
+ typed for typed in getattr(ff, param).keys()
+ ]
+ for param in [
+ "atom_types",
+ "bond_types",
+ "angle_types",
+ "dihedral_types",
+ ]:
+ assert get_names(opls_ethane_foyer, param) == get_names(
+ reloaded_xml, param
+ )
+
+ def test_foyer_xml_conversion(self):
+ """Validate xml converted from Foyer can be written out correctly."""
+ pass
+
+ def test_write_xml_from_topology(self):
+ """Validate xml from a typed topology matches loaded xmls."""
+ pass
+
+ @pytest.mark.parametrize("xml", TEST_XMLS)
+ def test_load__direct_from_forcefield_utilities(self, xml):
+ """Validate loaded xmls from ff-utils match original file."""
+ ff = GMSOFFs().load_xml(xml).to_gmso_ff()
+ assert isinstance(ff, ForceField)
+
+ @pytest.mark.parametrize("xml", TEST_XMLS)
+ def test_ffutils_backend(self, xml):
+ ff1 = ForceField(xml)
+ assert isinstance(ff1, ForceField)
+ ff2 = ForceField(xml, backend="gmso", strict=False)
+ assert isinstance(ff2, ForceField)
+ assert ff1 == ff2
+
+ @pytest.mark.parametrize("xml", TEST_XMLS)
+ def test_gmso_backend(self, xml):
+ ff = ForceField(xml, backend="gmso", strict=False)
+ assert isinstance(ff, ForceField)
+
+ @pytest.mark.parametrize("xml", TEST_XMLS)
+ def test_load_write_xmls_gmso_backend(self, xml):
+ """Validate loaded xmls written out match original file."""
+ ff1 = ForceField(xml, backend="forcefield_utilities")
+ ff1.to_xml("tmp.xml", overwrite=True)
+ ff2 = ForceField("tmp.xml", strict=False)
+ assert compare_xml_files(xml, "tmp.xml")
+ assert ff1 == ff2
+
+ @pytest.mark.parametrize("xml", TEST_XMLS)
+ def test_load_write_xmls_ffutils_backend(self, xml):
+ """Validate loaded xmls written out match original file."""
+ ff1 = ForceField(xml, backend="forcefield-utilities")
+ ff1.to_xml("tmp.xml", overwrite=True)
+ ff2 = GMSOFFs().load_xml("tmp.xml").to_gmso_ff()
+ assert compare_xml_files("tmp.xml", xml)
+ assert ff1 == ff2
+
+ def test_xml_error_handling(self):
+ """Validate bad xml formatting in xmls."""
+ pass
+
+ def test_kb_in_ffutils(self):
+ xml_path = get_path("ff-example0.xml")
+ ff = ForceField(xml_path, backend="forcefield-utilities")
+ assert ff
diff --git a/gmso/utils/compatibility.py b/gmso/utils/compatibility.py
index a06f8fb72..ed631654b 100644
--- a/gmso/utils/compatibility.py
+++ b/gmso/utils/compatibility.py
@@ -27,7 +27,9 @@ def check_compatibility(topology, accepted_potentials):
for atom_type in topology.atom_types:
potential_form = _check_single_potential(atom_type, accepted_potentials)
if not potential_form:
- raise EngineIncompatibilityError
+ raise EngineIncompatibilityError(
+ f"Potential {atom_type} is not in the list of accepted_potentials {accepted_potentials}"
+ )
else:
potential_forms_dict.update(potential_form)
@@ -36,7 +38,9 @@ def check_compatibility(topology, accepted_potentials):
connection_type, accepted_potentials
)
if not potential_form:
- raise EngineIncompatibilityError
+ raise EngineIncompatibilityError(
+ f"Potential {connection_type} is not in the list of accepted_potentials {accepted_potentials}"
+ )
else:
potential_forms_dict.update(potential_form)
diff --git a/gmso/utils/decorators.py b/gmso/utils/decorators.py
index 3675e9432..5a3b3b2e0 100644
--- a/gmso/utils/decorators.py
+++ b/gmso/utils/decorators.py
@@ -1,4 +1,7 @@
"""Various decorators for GMSO."""
+import functools
+import warnings
+
from gmso.abc import GMSOJSONHandler
@@ -13,3 +16,48 @@ def __call__(self, cls):
json_method = getattr(cls, self.method)
GMSOJSONHandler.register(cls, json_method)
return cls
+
+
+def deprecate_kwargs(deprecated_kwargs=None):
+ if deprecated_kwargs is None:
+ deprecated_kwargs = set()
+
+ def decorate_deprecate_kwargs(func):
+ @functools.wraps(func)
+ def wrapper(self_or_cls, *args, **kwargs):
+ _deprecate_kwargs(kwargs, deprecated_kwargs)
+ return func(self_or_cls, *args, **kwargs)
+
+ return wrapper
+
+ return decorate_deprecate_kwargs
+
+
+def _deprecate_kwargs(kwargs, deprecated_kwargs):
+ added_args = []
+ added_params = []
+ deprecated_args = [kwarg[0] for kwarg in deprecated_kwargs]
+ deprecated_params = [kwarg[1] for kwarg in deprecated_kwargs]
+ for kwarg in kwargs:
+ if kwarg in deprecated_args and kwargs[kwarg] in deprecated_params:
+ added_args.append(kwarg[0])
+ added_params.append(kwarg[1])
+ if len(added_args) > 1:
+ message = (
+ "Keyword arguments `{dep_args}={dep_params}` are deprecated and will be removed in the "
+ "next minor release of the package. Please update your code accordingly"
+ )
+ else:
+ message = (
+ "Keyword argument `{dep_args}={dep_params}` is deprecated and will be removed in the "
+ "next minor release of the package. Please update your code accordingly"
+ )
+ if added_args:
+ warnings.warn(
+ message.format(
+ dep_args=", ".join(added_args),
+ dep_params=", ".join(added_params),
+ ),
+ DeprecationWarning,
+ 3,
+ )
diff --git a/gmso/utils/ff_utils.py b/gmso/utils/ff_utils.py
index abdf22520..4eb6bf8a4 100644
--- a/gmso/utils/ff_utils.py
+++ b/gmso/utils/ff_utils.py
@@ -367,7 +367,7 @@ def parse_ff_atomtypes(atomtypes_el, ff_meta):
"independent_variables": None,
"atomclass": "",
"doi": "",
- "overrides": "",
+ "overrides": set(),
"definition": "",
"description": "",
"element": "",
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_trappe-ua.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_trappe-ua.xml
new file mode 100644
index 000000000..1c74c2148
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_trappe-ua.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/charmm36.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/charmm36.xml
new file mode 100644
index 000000000..0151233c1
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/charmm36.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.6
+ 2.5
+
+
+ 1.0
+ 2.0
+
+
+ 0.0
+ 180.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/ff14SB.txt b/gmso/utils/files/gmso_xmls/test_ffstyles/ff14SB.txt
new file mode 100644
index 000000000..a6d842e13
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/ff14SB.txt
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.6
+ 2.5
+
+
+ 1.0
+ 2.0
+
+
+ 0.0
+ 180.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/opls_charmm_buck.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/opls_charmm_buck.xml
new file mode 100644
index 000000000..8952bcb25
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/opls_charmm_buck.xml
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/oplsaa_from_foyer.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/oplsaa_from_foyer.xml
new file mode 100644
index 000000000..524bd2f0b
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/oplsaa_from_foyer.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/spce.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/spce.xml
new file mode 100644
index 000000000..186fad6e2
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/spce.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/tip3p.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/tip3p.xml
new file mode 100644
index 000000000..3c99a3a95
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/tip3p.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_2005.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_2005.xml
new file mode 100644
index 000000000..e213d53b9
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_2005.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_ew.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_ew.xml
new file mode 100644
index 000000000..f81c53a63
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_ew.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_molecules/alkanes.xml b/gmso/utils/files/gmso_xmls/test_molecules/alkanes.xml
new file mode 100644
index 000000000..ea4feb5a0
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_molecules/alkanes.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_molecules/alkenes.xml b/gmso/utils/files/gmso_xmls/test_molecules/alkenes.xml
new file mode 100644
index 000000000..ab2f17637
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_molecules/alkenes.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_molecules/alkynes.xml b/gmso/utils/files/gmso_xmls/test_molecules/alkynes.xml
new file mode 100644
index 000000000..ff0dfe2b5
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_molecules/alkynes.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_molecules/carbon.xml b/gmso/utils/files/gmso_xmls/test_molecules/carbon.xml
new file mode 100644
index 000000000..3a008f8aa
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_molecules/carbon.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_units/ang_kcal_coulomb_gram_degree_mol.xml b/gmso/utils/files/gmso_xmls/test_units/ang_kcal_coulomb_gram_degree_mol.xml
new file mode 100644
index 000000000..e15b236cb
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_units/ang_kcal_coulomb_gram_degree_mol.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_units/nm_kj_electroncharge_amu_rad_mol.xml b/gmso/utils/files/gmso_xmls/test_units/nm_kj_electroncharge_amu_rad_mol.xml
new file mode 100644
index 000000000..047ae25ce
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_units/nm_kj_electroncharge_amu_rad_mol.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/misc.py b/gmso/utils/misc.py
index 5c17a2571..6c92eb631 100644
--- a/gmso/utils/misc.py
+++ b/gmso/utils/misc.py
@@ -5,6 +5,19 @@
from unyt.exceptions import UnitConversionError
+@lru_cache(maxsize=128)
+def unyt_compare(units1, units2):
+ """Check if two parameter values have the same units."""
+ units1 = list(units1)
+ units2 = list(units2)
+ for unit1, unit2 in zip(units1, units2):
+ try:
+ u.testing.assert_allclose_units(unit1, unit2, rtol=1e-5, atol=1e-8)
+ except AssertionError:
+ return False
+ return True
+
+
def unyt_to_hashable(unyt_or_unyt_iter):
"""Convert a (list of) unyt array or quantity to a hashable tuple."""
if unyt_or_unyt_iter is None:
From 24d579c6496c0deb6fb7deba2c68a9de41499483 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Wed, 14 Sep 2022 11:44:36 -0500
Subject: [PATCH 085/141] pin foyer and ffutils version (#694)
* pin foyer and ffutils version
* Add some tests and fix precommit error
---
environment-dev.yml | 15 +++++++--------
environment.yml | 5 +++--
gmso/tests/test_forcefield.py | 13 +++++++++++++
gmso/utils/decorators.py | 1 +
4 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/environment-dev.yml b/environment-dev.yml
index 4b33c8145..d8cb7aa4c 100644
--- a/environment-dev.yml
+++ b/environment-dev.yml
@@ -11,17 +11,16 @@ dependencies:
- pydantic>1.8
- networkx
- pytest
- - mbuild >= 0.11.0
- - openbabel >= 3.0.0
- - foyer >= 0.11.1
- - gsd >= 2.0
- - parmed >= 3.4.3
+ - mbuild>=0.11.0
+ - openbabel>=3.0.0
+ - foyer>=0.11.3
+ - forcefield-utilities>=0.2.1
+ - gsd>=2.0
+ - parmed>=3.4.3
- pytest-cov
- codecov
- bump2version
- matplotlib
- ipywidgets
- - ele >= 0.2.0
+ - ele>=0.2.0
- pre-commit
- - pip:
- - "--editable=git+https://github.com/mosdef-hub/forcefield-utilities.git#egg=forcefield-utilities"
diff --git a/environment.yml b/environment.yml
index d0302bbb4..c4d2ae172 100644
--- a/environment.yml
+++ b/environment.yml
@@ -10,5 +10,6 @@ dependencies:
- lxml
- pydantic>1.8
- networkx
- - ele >= 0.2.0
- - forcefield-utilities
+ - ele>=0.2.0
+ - foyer>=0.11.3
+ - forcefield-utilities>=0.2.1
diff --git a/gmso/tests/test_forcefield.py b/gmso/tests/test_forcefield.py
index eef46e423..1f958626d 100644
--- a/gmso/tests/test_forcefield.py
+++ b/gmso/tests/test_forcefield.py
@@ -10,6 +10,7 @@
from gmso.exceptions import (
ForceFieldError,
ForceFieldParseError,
+ GMSOError,
MissingAtomTypesError,
MissingPotentialError,
)
@@ -658,3 +659,15 @@ def test_valid_sequence(self):
def test_deprecated_gmso(self):
with pytest.warns(DeprecationWarning):
ForceField(get_path("ff-example0.xml"), backend="gmso")
+
+ def test_not_supoprted_backend(self, opls_ethane_foyer):
+ # Unsupported ff parser backend
+ with pytest.raises(GMSOError):
+ ForceField(get_path("ff-example0.xml"), backend="bogus")
+
+ # Unsupported ff writer backend
+ with pytest.raises(NotImplementedError):
+ opls_ethane_foyer.to_xml("test_xml_writer.xml", backend="ffutils")
+
+ with pytest.raises(GMSOError):
+ opls_ethane_foyer.to_xml("test_xml_writer.xml", backend="bogus")
diff --git a/gmso/utils/decorators.py b/gmso/utils/decorators.py
index 5a3b3b2e0..77f6e3756 100644
--- a/gmso/utils/decorators.py
+++ b/gmso/utils/decorators.py
@@ -19,6 +19,7 @@ def __call__(self, cls):
def deprecate_kwargs(deprecated_kwargs=None):
+ """Decorate functions with deprecated/deprecating kwargs."""
if deprecated_kwargs is None:
deprecated_kwargs = set()
From 691371c64e5e8c7fa2024c2b707e5e3009c81776 Mon Sep 17 00:00:00 2001
From: Co Quach
Date: Wed, 14 Sep 2022 12:01:08 -0500
Subject: [PATCH 086/141] Bump to version 0.9.1
---
docs/conf.py | 4 ++--
gmso/__init__.py | 2 +-
setup.cfg | 2 +-
setup.py | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index fe5c4951b..d275e8b02 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,8 +23,8 @@
author = "Matt Thompson, Alex Yang, Ray Matsumoto, Parashara Shamaprasad, Umesh Timalsina, Co Quach, Ryan S. DeFever, Justin Gilmer"
# The full version, including alpha/beta/rc tags
-version = "0.9.0"
-release = "0.9.0"
+version = "0.9.1"
+release = "0.9.1"
# -- General configuration ---------------------------------------------------
diff --git a/gmso/__init__.py b/gmso/__init__.py
index 50ae9940a..63053fc14 100644
--- a/gmso/__init__.py
+++ b/gmso/__init__.py
@@ -15,4 +15,4 @@
from .core.pairpotential_type import PairPotentialType
from .core.topology import Topology
-__version__ = "0.9.0"
+__version__ = "0.9.1"
diff --git a/setup.cfg b/setup.cfg
index aa82395f3..ac7c614a3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.9.0
+current_version = 0.9.1
commit = True
tag = True
message = Bump to version {new_version}
diff --git a/setup.py b/setup.py
index e219435e1..b5185c3a3 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
from setuptools import find_packages, setup
#####################################
-VERSION = "0.9.0"
+VERSION = "0.9.1"
ISRELEASED = False
if ISRELEASED:
__version__ = VERSION
From bc557dd51550e8144d8b26d5e392f875ad706b4d Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 11 Oct 2022 11:17:57 -0500
Subject: [PATCH 087/141] [pre-commit.ci] pre-commit autoupdate (#697)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8f53a5a27..5f6aab2bd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
- id: trailing-whitespace
exclude: 'setup.cfg'
- repo: https://github.com/psf/black
- rev: 22.8.0
+ rev: 22.10.0
hooks:
- id: black
args: [--line-length=80]
From 4f00c99a611bcbfef9a2837b55598c9adad8b2c1 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Wed, 9 Nov 2022 10:55:09 -0600
Subject: [PATCH 088/141] Restraint support for GROMACS top writer (#685)
* add ability to write angle and dihedral restraint for gromacs top format
* fix typo in dihedral, add docs for angle restraints
* add #ifdef DIHRES for dihedral restrain section
* change var name, better handling of scaling factors for top writer
* fix tab for top writer
* fix typo
* better parsing unique molecules
* reformat top writer to work properly with new changes
* add test files for top and gro writer, make gro write out res info
* adjust unit tests
* add precision and adjust unit test
* adjustments to element parsing, spacing of writing out gro file, speed up writer compatibility check
* fix minor bugs
* reformat top writer, add simplify_check for compatability check
* fix unit test, add sanity check for top writer
* truncating site name in gro writer
* better handling use_molecule_info speedup during atomtyping
* Add test with restraints
* fix bug related to restraints section
* update test, add more docs about angle and dihedral restraints, make top writer refer to element from atomtype (work better for non-atomistic
* add more rigorous test for top and gro file
* add write gro test
* combine benzene ua and aa test
* add harmonic bond restraint for bond class and top writer
* add unit test for bond restraints (harmonic)
* update test file
* fix typo and removed unused imports
* fixing unit tests
---
gmso/core/angle.py | 17 +
gmso/core/atom_type.py | 1 +
gmso/core/bond.py | 16 +
gmso/core/dihedral.py | 17 +
gmso/external/convert_foyer_xml.py | 14 +-
gmso/external/convert_mbuild.py | 6 +-
gmso/external/convert_parmed.py | 5 +
gmso/formats/gro.py | 87 ++--
gmso/formats/top.py | 486 +++++++++++++-----
.../topology_parameterizer.py | 2 +-
gmso/tests/base_test.py | 77 ++-
gmso/tests/files/benzene.gro | 63 +++
gmso/tests/files/benzene.top | 99 ++++
gmso/tests/files/benzene_trappe-ua.xml | 17 +
gmso/tests/files/restrained_benzene_ua.gro | 33 ++
gmso/tests/files/restrained_benzene_ua.top | 85 +++
gmso/tests/test_convert_foyer_xml.py | 15 +-
gmso/tests/test_gro.py | 54 +-
gmso/tests/test_top.py | 104 +++-
gmso/utils/compatibility.py | 30 +-
gmso/utils/io.py | 8 -
21 files changed, 1007 insertions(+), 229 deletions(-)
create mode 100644 gmso/tests/files/benzene.gro
create mode 100644 gmso/tests/files/benzene.top
create mode 100755 gmso/tests/files/benzene_trappe-ua.xml
create mode 100644 gmso/tests/files/restrained_benzene_ua.gro
create mode 100644 gmso/tests/files/restrained_benzene_ua.top
diff --git a/gmso/core/angle.py b/gmso/core/angle.py
index 12a7979b5..17e62ba09 100644
--- a/gmso/core/angle.py
+++ b/gmso/core/angle.py
@@ -31,6 +31,16 @@ class Angle(Connection):
default=None, description="AngleType of this angle."
)
+ restraint_: Optional[dict] = Field(
+ default=None,
+ description="""
+ Restraint for this angle, must be a dict with the following keys:
+ 'k' (unit of energy/mol), 'theta_eq' (unit of angle), 'n' (multiplicity, unitless).
+ Refer to https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html
+ for more information.
+ """,
+ )
+
@property
def angle_type(self):
"""Return the angle type if the angle is parametrized."""
@@ -41,6 +51,11 @@ def connection_type(self):
"""Return the angle type if the angle is parametrized."""
return self.__dict__.get("angle_type_")
+ @property
+ def restraint(self):
+ """Return the restraint of this angle."""
+ return self.__dict__.get("restraint_")
+
def equivalent_members(self):
"""Return a set of the equivalent connection member tuples.
@@ -74,8 +89,10 @@ class Config:
fields = {
"connection_members_": "connection_members",
"angle_type_": "angle_type",
+ "restraint_": "restraint",
}
alias_to_fields = {
"connection_members": "connection_members_",
"angle_type": "angle_type_",
+ "restraint": "restraint_",
}
diff --git a/gmso/core/atom_type.py b/gmso/core/atom_type.py
index 4b48d5b27..cdee3bd80 100644
--- a/gmso/core/atom_type.py
+++ b/gmso/core/atom_type.py
@@ -132,6 +132,7 @@ def clone(self, fast_copy=False):
"""Clone this AtomType, faster alternative to deepcopying."""
return AtomType(
name=str(self.name),
+ tags=self.tags,
expression=None,
parameters=None,
independent_variables=None,
diff --git a/gmso/core/bond.py b/gmso/core/bond.py
index 67c5f082a..23dd7fe4e 100644
--- a/gmso/core/bond.py
+++ b/gmso/core/bond.py
@@ -30,6 +30,15 @@ class Bond(Connection):
bond_type_: Optional[BondType] = Field(
default=None, description="BondType of this bond."
)
+ restraint_: Optional[dict] = Field(
+ default=None,
+ description="""
+ Restraint for this bond, must be a dict with the following keys:
+ 'b0' (unit of length), 'kb' (unit of energy/(mol * length**2)).
+ Refer to https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html
+ for more information.
+ """,
+ )
@property
def bond_type(self):
@@ -42,6 +51,11 @@ def connection_type(self):
# ToDo: Deprecate this?
return self.__dict__.get("bond_type_")
+ @property
+ def restraint(self):
+ """Return the restraint of this bond."""
+ return self.__dict__.get("restraint_")
+
def equivalent_members(self):
"""Get a set of the equivalent connection member tuples.
@@ -75,8 +89,10 @@ class Config:
fields = {
"bond_type_": "bond_type",
"connection_members_": "connection_members",
+ "restraint_": "restraint",
}
alias_to_fields = {
"bond_type": "bond_type_",
"connection_members": "connection_members_",
+ "restraint": "restraint_",
}
diff --git a/gmso/core/dihedral.py b/gmso/core/dihedral.py
index 430d90460..97859bc52 100644
--- a/gmso/core/dihedral.py
+++ b/gmso/core/dihedral.py
@@ -35,6 +35,16 @@ class Dihedral(Connection):
default=None, description="DihedralType of this dihedral."
)
+ restraint_: Optional[dict] = Field(
+ default=None,
+ description="""
+ Restraint for this dihedral, must be a dict with the following keys:
+ 'k' (unit of energy/(mol * angle**2)), 'phi_eq' (unit of angle), 'delta_phi' (unit of angle).
+ Refer to https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html
+ for more information.
+ """,
+ )
+
@property
def dihedral_type(self):
return self.__dict__.get("dihedral_type_")
@@ -44,6 +54,11 @@ def connection_type(self):
# ToDo: Deprecate this?
return self.__dict__.get("dihedral_type_")
+ @property
+ def restraint(self):
+ """Return the restraint of this dihedral."""
+ return self.__dict__.get("restraint_")
+
def equivalent_members(self):
"""Get a set of the equivalent connection member tuples
@@ -74,8 +89,10 @@ class Config:
fields = {
"dihedral_type_": "dihedral_type",
"connection_members_": "connection_members",
+ "restraint_": "restraint",
}
alias_to_fields = {
"dihedral_type": "dihedral_type_",
"connection_members": "connection_members_",
+ "restraint": "restraint_",
}
diff --git a/gmso/external/convert_foyer_xml.py b/gmso/external/convert_foyer_xml.py
index 5ce665668..3e0996739 100644
--- a/gmso/external/convert_foyer_xml.py
+++ b/gmso/external/convert_foyer_xml.py
@@ -529,16 +529,6 @@ def _create_sub_element(root_el, name, attrib_dict=None):
def _validate_foyer(xml_path):
- import warnings
+ from foyer.validator import Validator
- from gmso.utils.io import has_foyer
-
- if not has_foyer:
- warnings.warn(
- "Cannot validate the xml using foyer, since foyer is not installed."
- "Please install foyer using conda install -c conda-forge foyer."
- )
- else:
- from foyer.validator import Validator
-
- Validator(xml_path)
+ Validator(xml_path)
diff --git a/gmso/external/convert_mbuild.py b/gmso/external/convert_mbuild.py
index c87cafe06..d6dc4f5bc 100644
--- a/gmso/external/convert_mbuild.py
+++ b/gmso/external/convert_mbuild.py
@@ -250,11 +250,7 @@ def _parse_particle(particle_map, site):
def _parse_site(site_map, particle, search_method):
"""Parse information for a gmso.Site from a mBuild.Compound adn add it to the site map."""
pos = particle.xyz[0] * u.nm
- ele = (
- search_method(particle.element.symbol)
- if particle.element
- else search_method(particle.name)
- )
+ ele = search_method(particle.element.symbol) if particle.element else None
charge = particle.charge * u.elementary_charge if particle.charge else None
mass = particle.mass * u.amu if particle.mass else None
diff --git a/gmso/external/convert_parmed.py b/gmso/external/convert_parmed.py
index 3f6a8f71c..027a58d5f 100644
--- a/gmso/external/convert_parmed.py
+++ b/gmso/external/convert_parmed.py
@@ -256,9 +256,14 @@ def _atom_types_from_pmd(structure):
unique_atom_types = list(unique_atom_types)
pmd_top_atomtypes = {}
for atom_type in unique_atom_types:
+ if atom_type.atomic_number:
+ element = element_by_atomic_number(atom_type.atomic_number).symbol
+ else:
+ element = atom_type.name
top_atomtype = gmso.AtomType(
name=atom_type.name,
charge=atom_type.charge * u.elementary_charge,
+ tags={"element": element},
expression="4*epsilon*((sigma/r)**12 - (sigma/r)**6)",
parameters={
"sigma": atom_type.sigma * u.angstrom,
diff --git a/gmso/formats/gro.py b/gmso/formats/gro.py
index af6715579..8d8aa76a5 100644
--- a/gmso/formats/gro.py
+++ b/gmso/formats/gro.py
@@ -1,5 +1,6 @@
"""Read and write Gromos87 (.GRO) file format."""
import datetime
+import re
import warnings
import numpy as np
@@ -10,7 +11,6 @@
from gmso.core.atom import Atom
from gmso.core.box import Box
from gmso.core.topology import Topology
-from gmso.exceptions import NotYetImplementedWarning
from gmso.formats.formats_registry import loads_as, saves_as
@@ -57,6 +57,7 @@ def read_gro(filename):
coords = u.nm * np.zeros(shape=(n_atoms, 3))
for row, _ in enumerate(coords):
line = gro_file.readline()
+ content = line.split()
if not line:
msg = (
"Incorrect number of lines in .gro file. Based on the "
@@ -64,18 +65,23 @@ def read_gro(filename):
"atoms were expected, but at least one fewer was found."
)
raise ValueError(msg.format(n_atoms))
- resid = int(line[:5])
- res_name = line[5:10]
- atom_name = line[10:15]
- atom_id = int(line[15:20])
+
+ res = content[0]
+ atom_name = content[1]
+ atom_id = content[2]
coords[row] = u.nm * np.array(
[
- float(line[20:28]),
- float(line[28:36]),
- float(line[36:44]),
+ float(content[3]),
+ float(content[4]),
+ float(content[5]),
]
)
site = Atom(name=atom_name, position=coords[row])
+
+ r = re.compile("([0-9]+)([a-zA-Z]+)")
+ m = r.match(res)
+ site.molecule = (m.group(2), int(m.group(1)) - 1)
+ site.residue = (m.group(2), int(m.group(1)) - 1)
top.add_site(site, update_types=False)
top.update_topology()
@@ -97,7 +103,7 @@ def read_gro(filename):
@saves_as(".gro")
-def write_gro(top, filename):
+def write_gro(top, filename, precision=3):
"""Write a topology to a gro file.
The Gromos87 (gro) format is a common plain text structure file used
@@ -113,7 +119,8 @@ def write_gro(top, filename):
The `topology` to write out to the gro file.
filename : str or file object
The location and name of file to save to disk.
-
+ precision : int, optional, default=3
+ The number of sig fig to write out the position in.
Notes
-----
@@ -135,7 +142,7 @@ def write_gro(top, filename):
)
)
out_file.write("{:d}\n".format(top.n_sites))
- out_file.write(_prepare_atoms(top, pos_array))
+ out_file.write(_prepare_atoms(top, pos_array, precision))
out_file.write(_prepare_box(top))
@@ -154,30 +161,44 @@ def _validate_positions(pos_array):
return pos_array
-def _prepare_atoms(top, updated_positions):
+def _prepare_atoms(top, updated_positions, precision):
out_str = str()
+ warnings.warn(
+ "Residue information is parsed from site.molecule,"
+ "or site.residue if site.molecule does not exist."
+ "Note that the residue idx will be bump by 1 since GROMACS utilize 1-index."
+ )
for idx, (site, pos) in enumerate(zip(top.sites, updated_positions)):
- warnings.warn(
- "Residue information is not currently "
- "stored or written to GRO files.",
- NotYetImplementedWarning,
- )
- # TODO: assign residues
- res_id = 1
- res_name = "X"
- atom_name = site.name
+ if site.molecule:
+ res_id = site.molecule.number + 1
+ res_name = site.molecule.name
+ elif site.residue:
+ res_id = site.residue.number + 1
+ res_name = site.molecule.name[:3]
+ else:
+ res_id = 1
+ res_name = "MOL"
+ if len(res_name) > 3:
+ res_name = res_name[:3]
+
+ atom_name = site.name if len(site.name) <= 3 else site.name[:3]
atom_id = idx + 1
- out_str = (
- out_str
- + "{0:5d}{1:5s}{2:5s}{3:5d}{4:8.3f}{5:8.3f}{6:8.3f}\n".format(
- res_id,
- res_name,
- atom_name,
- atom_id,
- pos[0].in_units(u.nm).value,
- pos[1].in_units(u.nm).value,
- pos[2].in_units(u.nm).value,
- )
+
+ varwidth = 5 + precision
+ crdfmt = f"{{:{varwidth}.{precision}f}}"
+
+ # preformat pos str
+ crt_x = crdfmt.format(pos[0].in_units(u.nm).value)[:varwidth]
+ crt_y = crdfmt.format(pos[1].in_units(u.nm).value)[:varwidth]
+ crt_z = crdfmt.format(pos[2].in_units(u.nm).value)[:varwidth]
+ out_str = out_str + "{0:5d}{1:5s}{2:5s}{3:5d}{4}{5}{6}\n".format(
+ res_id,
+ res_name,
+ atom_name,
+ atom_id,
+ crt_x,
+ crt_y,
+ crt_z,
)
return out_str
@@ -190,7 +211,7 @@ def _prepare_box(top):
rtol=1e-5,
atol=0.1 * u.degree,
):
- out_str = out_str + " {:0.5f} {:0.5f} {:0.5f} \n".format(
+ out_str = out_str + " {:0.5f} {:0.5f} {:0.5f}\n".format(
top.box.lengths[0].in_units(u.nm).value.round(6),
top.box.lengths[1].in_units(u.nm).value.round(6),
top.box.lengths[2].in_units(u.nm).value.round(6),
diff --git a/gmso/formats/top.py b/gmso/formats/top.py
index bb38bfa1a..f8e17c6de 100644
--- a/gmso/formats/top.py
+++ b/gmso/formats/top.py
@@ -1,21 +1,38 @@
"""Write a GROMACS topology (.TOP) file."""
import datetime
+import warnings
import unyt as u
+from gmso.core.dihedral import Dihedral
from gmso.core.element import element_by_atom_type
+from gmso.core.improper import Improper
+from gmso.core.views import PotentialFilters
from gmso.exceptions import GMSOError
from gmso.formats.formats_registry import saves_as
from gmso.lib.potential_templates import PotentialTemplateLibrary
+from gmso.parameterization.molecule_utils import (
+ molecule_angles,
+ molecule_bonds,
+ molecule_dihedrals,
+ molecule_impropers,
+)
from gmso.utils.compatibility import check_compatibility
@saves_as(".top")
-def write_top(top, filename, top_vars=None):
+def write_top(top, filename, top_vars=None, simplify_check=False):
"""Write a gmso.core.Topology object to a GROMACS topology (.TOP) file."""
- pot_types = _validate_compatibility(top)
+ pot_types = _validate_compatibility(top, simplify_check)
top_vars = _get_top_vars(top, top_vars)
+ # Sanity checks
+ msg = "System not fully typed"
+ for site in top.sites:
+ assert site.atom_type, msg
+ for connection in top.connections:
+ assert connection.connection_type, msg
+
with open(filename, "w") as out_file:
out_file.write(
"; File {} written by GMSO at {}\n\n".format(
@@ -27,14 +44,14 @@ def write_top(top, filename, top_vars=None):
"[ defaults ]\n"
"; nbfunc\t"
"comb-rule\t"
- "gen-pairs\t"
+ "gen-pairs\t\t"
"fudgeLJ\t"
"fudgeQQ\n"
)
out_file.write(
"{0}\t\t\t"
"{1}\t\t\t"
- "{2}\t\t\t"
+ "{2}\t\t"
"{3}\t\t"
"{4}\n\n".format(
top_vars["nbfunc"],
@@ -48,24 +65,25 @@ def write_top(top, filename, top_vars=None):
out_file.write(
"[ atomtypes ]\n"
"; name\t\t"
- "at.num\t\t"
+ "at.num\t"
"mass\t\t"
"charge\t\t"
- "ptype\t\t"
- "sigma\t\t"
+ "ptype\t"
+ "sigma\t"
"epsilon\n"
)
- for atom_type in top.atom_types:
+
+ for atom_type in top.atom_types(PotentialFilters.UNIQUE_NAME_CLASS):
out_file.write(
- "{0}\t\t\t"
- "{1}\t\t\t"
- "{2:.5f}\t\t"
- "{3:.5f}\t\t"
- "{4}\t\t\t"
- "{5:.5f}\t\t\t"
- "{6:.5f}\n".format(
+ "{0:12s}"
+ "{1:4s}"
+ "{2:12.5f}"
+ "{3:12.5f}\t"
+ "{4:4s}"
+ "{5:12.5f}"
+ "{6:12.5f}\n".format(
atom_type.name,
- _lookup_atomic_number(atom_type),
+ str(_lookup_atomic_number(atom_type)),
atom_type.mass.in_units(u.amu).value,
atom_type.charge.in_units(u.elementary_charge).value,
"A",
@@ -76,97 +94,155 @@ def write_top(top, filename, top_vars=None):
)
)
- out_file.write("\n[ moleculetype ]\n" "; name\t\tnrexcl\n")
-
- # TODO: Better parsing of site.molecule and site.residue into residues/molecules
- n_unique_molecule = len(
- top.unique_site_labels("molecule", name_only=True)
- )
- if n_unique_molecule > 1:
- raise NotImplementedError
- # Treat top without molecule as one residue-like "molecule"
- elif n_unique_molecule == 0:
+ # Define unique molecule by name only
+ unique_molecules = _get_unique_molecules(top)
+
+ # Section headers
+ headers = {
+ "bonds": "\n[ bonds ]\n" "; ai\taj\t\tfunct\tb0\t\tkb\n",
+ "bond_restraints": "\n[ bonds ] ;Harmonic potential restraint\n"
+ "; ai\taj\t\tfunct\tb0\t\tkb\n",
+ "angles": "\n[ angles ]\n" "; ai\taj\t\tak\t\tfunct\tphi_0\tk0\n",
+ "angle_restraints": (
+ "\n[ angle_restraints ]\n"
+ "; ai\taj\t\tai\t\tak\t\tfunct\ttheta_eq\tk\tmultiplicity\n"
+ ),
+ "dihedrals": {
+ "RyckaertBellemansTorsionPotential": "\n[ dihedrals ]\n"
+ "; ai\taj\t\tak\t\tal\t\tfunct\t\tc0\t\tc1\t\tc2\t\tc3\t\tc4\t\tc5\n",
+ "PeriodicTorsionPotential": "\n[ dihedrals ]\n"
+ "; ai\taj\t\tak\t\tal\t\tfunct\tphi\tk_phi\tmulitplicity\n",
+ },
+ "dihedral_restraints": "\n[ dihedral_restraints ]\n"
+ "#ifdef DIHRES\n"
+ "; ai\taj\t\tak\t\tal\t\tfunct\ttheta_eq\tdelta_theta\t\tkd\n",
+ }
+ for tag in unique_molecules:
+ """Write out nrexcl for each unique molecule."""
+ out_file.write("\n[ moleculetype ]\n" "; name\tnrexcl\n")
+
+ # TODO: Lookup and join nrexcl from each molecule object
+ out_file.write("{0}\t" "{1}\n".format(tag, top_vars["nrexcl"]))
+
+ """Write out atoms for each unique molecule."""
out_file.write(
- "{0}\t\t\t"
- "{1}\n\n".format(
- top.name,
- top_vars["nrexcl"], # Typically exclude 3 nearest neighbors
- )
+ "[ atoms ]\n"
+ "; nr\ttype\tresnr\tresidue\t\tatom\tcgnr\tcharge\tmass\n"
)
- # TODO: Lookup and join nrexcl from each molecule object
- elif n_unique_molecule == 1:
- out_file.write("{0}\t\t\t" "{1}\n\n".format(top.name, 3))
-
- out_file.write(
- "[ atoms ]\n"
- "; nr\t\ttype\tresnr\tresidue\t\tatom\tcgnr\tcharge\t\tmass\n"
- )
- for site in top.sites:
- out_file.write(
- "{0}\t\t\t"
- "{1}\t\t"
- "{2}\t\t"
- "{3}\t"
- "{4}\t\t"
- "{5}\t\t"
- "{6:.5f}\t\t"
- "{7:.5f}\n".format(
- top.get_index(site) + 1,
- site.atom_type.name,
- 1, # TODO: molecule number
- top.name, # TODO: molecule.name
- _lookup_element_symbol(site.atom_type),
- 1, # TODO: care about charge groups
- site.charge.in_units(u.elementary_charge).value,
- site.atom_type.mass.in_units(u.amu).value,
+ # Each unique molecule need to be reindexed (restarting from 0)
+ # The shifted_idx_map is needed to make sure all the atom index used in
+ # latter connection sections are acurate
+ shifted_idx_map = dict()
+ for idx, site in enumerate(unique_molecules[tag]["sites"]):
+ shifted_idx_map[top.get_index(site)] = idx
+ out_file.write(
+ "{0:8s}"
+ "{1:12s}"
+ "{2:8s}"
+ "{3:12s}"
+ "{4:8s}"
+ "{5:4s}"
+ "{6:12.5f}"
+ "{7:12.5f}\n".format(
+ str(idx + 1),
+ site.atom_type.name,
+ str(site.molecule.number + 1 if site.molecule else 1),
+ tag,
+ site.atom_type.tags["element"],
+ "1", # TODO: care about charge groups
+ site.charge.in_units(u.elementary_charge).value,
+ site.atom_type.mass.in_units(u.amu).value,
+ )
)
- )
- out_file.write("\n[ bonds ]\n" "; ai aj funct c0 c1\n")
- for bond in top.bonds:
- out_file.write(
- _write_connection(top, bond, pot_types[bond.connection_type])
- )
+ for conn_group in [
+ "bonds",
+ "bond_restraints",
+ "angles",
+ "angle_restraints",
+ "dihedrals",
+ "dihedral_restraints",
+ "impropers",
+ ]:
+ if unique_molecules[tag][conn_group]:
+ if conn_group in ["dihedrals", "impropers"]:
+ proper_groups = {
+ "RyckaertBellemansTorsionPotential": list(),
+ "PeriodicTorsionPotential": list(),
+ }
+ for dihedral in unique_molecules[tag][conn_group]:
+ ptype = pot_types[dihedral.connection_type]
+ proper_groups[ptype].append(dihedral)
+
+ # Improper use same header as dihedral periodic header
+ if proper_groups["RyckaertBellemansTorsionPotential"]:
+ out_file.write(
+ headers["dihedrals"][
+ "RyckaertBellemansTorsionPotential"
+ ]
+ )
+ for conn in proper_groups[
+ "RyckaertBellemansTorsionPotential"
+ ]:
+ for line in _write_connection(
+ top,
+ conn,
+ pot_types[conn.connection_type],
+ shifted_idx_map,
+ ):
+ out_file.write(line)
+ if proper_groups["PeriodicTorsionPotential"]:
+ out_file.write(
+ headers["dihedrals"]["PeriodicTorsionPotential"]
+ )
+ for conn in proper_groups[
+ "PeriodicTorsionPotential"
+ ]:
+ for line in _write_connection(
+ top,
+ conn,
+ pot_types[conn.connection_type],
+ shifted_idx_map,
+ ):
+ out_file.write(line)
+ elif "restraints" in conn_group:
+ if conn_group == "dihedral_restraints":
+ warnings.warn(
+ "The diehdral_restraints writer is designed to work with"
+ "`define = DDIHRES` clause in the GROMACS input file (.mdp)"
+ )
+ out_file.write(headers[conn_group])
+ for conn in unique_molecules[tag][conn_group]:
+ out_file.write(
+ _write_restraint(
+ top,
+ conn,
+ conn_group,
+ shifted_idx_map,
+ )
+ )
+ else:
+ out_file.write(headers[conn_group])
+ for conn in unique_molecules[tag][conn_group]:
+ out_file.write(
+ _write_connection(
+ top,
+ conn,
+ pot_types[conn.connection_type],
+ shifted_idx_map,
+ )
+ )
+ if conn_group == "dihedral_restraints":
+ out_file.write("#endif DIHRES\n")
- out_file.write(
- "\n[ angles ]\n" "; ai aj ak funct c0 c1\n"
- )
- for angle in top.angles:
- out_file.write(
- _write_connection(top, angle, pot_types[angle.connection_type])
- )
+ out_file.write("\n[ system ]\n" "; name\n" "{0}\n\n".format(top.name))
- out_file.write(
- "\n[ dihedrals ]\n"
- "; ai aj ak al funct c0 c1 c2\n"
- )
- for dihedral in top.dihedrals:
+ out_file.write("[ molecules ]\n" "; molecule\tnmols\n")
+ for tag in unique_molecules:
out_file.write(
- _write_connection(
- top, dihedral, pot_types[dihedral.connection_type]
- )
+ "{0}\t{1}\n".format(tag, len(unique_molecules[tag]["subtags"]))
)
- out_file.write("\n[ system ]\n" "; name\n" "{0}\n\n".format(top.name))
-
- if len(top.unique_site_labels("molecule", name_only=True)) > 1:
- raise NotImplementedError
-
- # TODO: Write out atom types for each unique `molecule` (name_only) in `atoms` section
- # and write out number of molecules in `molecules` section
- # if len(top.subtops) == 0:
- out_file.write(
- "[ molecules ]\n"
- "; molecule\tnmols\n"
- "{0}\t\t{1}".format(top.name, 1)
- )
- # elif len(top.subtops) > 0:
- # out_file.write(
- # '[ molecules ]\n'
- # '; molecule\tnmols\n'
- # '{0}\t\t{1}'.format(top.subtops[0].name, top.n_subtops)
- # )
-
def _accepted_potentials():
"""List of accepted potentials that GROMACS can support."""
@@ -186,9 +262,9 @@ def _accepted_potentials():
return accepted_potentials
-def _validate_compatibility(top):
+def _validate_compatibility(top, simplify_check):
"""Check compatability of topology object with GROMACS TOP format."""
- pot_types = check_compatibility(top, _accepted_potentials())
+ pot_types = check_compatibility(top, _accepted_potentials(), simplify_check)
return pot_types
@@ -196,11 +272,11 @@ def _get_top_vars(top, top_vars):
"""Generate a dictionary of values for the defaults directive."""
combining_rule_to_gmx = {"lorentz": 2, "geometric": 3}
default_top_vars = dict()
- default_top_vars["nbfunc"] = 1
+ default_top_vars["nbfunc"] = 1 # modify this to check for lj or buckingham
default_top_vars["comb-rule"] = combining_rule_to_gmx[top.combining_rule]
default_top_vars["gen-pairs"] = "no"
- default_top_vars["fudgeLJ"] = 1
- default_top_vars["fudgeQQ"] = 1
+ default_top_vars["fudgeLJ"] = top.scaling_factors[0][2]
+ default_top_vars["fudgeQQ"] = top.scaling_factors[1][2]
default_top_vars["nrexcl"] = 3
if isinstance(top_vars, dict):
@@ -209,6 +285,67 @@ def _get_top_vars(top, top_vars):
return default_top_vars
+def _get_unique_molecules(top):
+ unique_molecules = {
+ tag: {
+ "subtags": list(),
+ }
+ for tag in top.unique_site_labels("molecule", name_only=True)
+ }
+
+ for molecule in top.unique_site_labels("molecule", name_only=False):
+ unique_molecules[molecule.name]["subtags"].append(molecule)
+
+ if len(unique_molecules) == 0:
+ unique_molecules[top.name] = dict()
+ unique_molecules[top.name]["subtags"] = [top.name]
+ unique_molecules[top.name]["sites"] = list(top.sites)
+ unique_molecules[top.name]["bonds"] = list(top.bonds)
+ unique_molecules[top.name]["bond_restraints"] = list(
+ bond for bond in top.bonds if bond.restraint
+ )
+ unique_molecules[top.name]["angles"] = list(top.angles)
+ unique_molecules[top.name]["angle_restraints"] = list(
+ angle for angle in top.angles if angle.restraint
+ )
+ unique_molecules[top.name]["dihedrals"] = list(top.angles)
+ unique_molecules[top.name]["dihedral_restraints"] = list(
+ dihedral for dihedral in top.dihedrals if dihedral.restraint
+ )
+ unique_molecules[molecule.name]["impropers"] = list(top.impropers)
+
+ else:
+ for tag in unique_molecules:
+ molecule = unique_molecules[tag]["subtags"][0]
+ unique_molecules[tag]["sites"] = list(
+ top.iter_sites(key="molecule", value=molecule)
+ )
+ unique_molecules[tag]["bonds"] = list(molecule_bonds(top, molecule))
+ unique_molecules[tag]["bond_restraints"] = list(
+ bond for bond in molecule_bonds(top, molecule) if bond.restraint
+ )
+ unique_molecules[tag]["angles"] = list(
+ molecule_angles(top, molecule)
+ )
+ unique_molecules[tag]["angle_restraints"] = list(
+ angle
+ for angle in molecule_angles(top, molecule)
+ if angle.restraint
+ )
+ unique_molecules[tag]["dihedrals"] = list(
+ molecule_dihedrals(top, molecule)
+ )
+ unique_molecules[tag]["dihedral_restraints"] = list(
+ dihedral
+ for dihedral in molecule_dihedrals(top, molecule)
+ if dihedral.restraint
+ )
+ unique_molecules[tag]["impropers"] = list(
+ molecule_impropers(top, molecule)
+ )
+ return unique_molecules
+
+
def _lookup_atomic_number(atom_type):
"""Look up an atomic_number based on atom type information, 0 if non-element type."""
try:
@@ -227,7 +364,7 @@ def _lookup_element_symbol(atom_type):
return "X"
-def _write_connection(top, connection, potential_name):
+def _write_connection(top, connection, potential_name, shifted_idx_map):
"""Worker function to write various connection information."""
worker_functions = {
"HarmonicBondPotential": _harmonic_bond_potential_writer,
@@ -236,14 +373,14 @@ def _write_connection(top, connection, potential_name):
"PeriodicTorsionPotential": _periodic_torsion_writer,
}
- return worker_functions[potential_name](top, connection)
+ return worker_functions[potential_name](top, connection, shifted_idx_map)
-def _harmonic_bond_potential_writer(top, bond):
+def _harmonic_bond_potential_writer(top, bond, shifted_idx_map):
"""Write harmonic bond information."""
- line = "\t{0}\t{1}\t{2}\t{3:.5f}\t{4:.5f}\n".format(
- top.get_index(bond.connection_members[0]) + 1,
- top.get_index(bond.connection_members[1]) + 1,
+ line = "{0:8s}{1:8s}{2:4s}{3:15.5f}{4:15.5f}\n".format(
+ str(shifted_idx_map[top.get_index(bond.connection_members[0])] + 1),
+ str(shifted_idx_map[top.get_index(bond.connection_members[1])] + 1),
"1",
bond.connection_type.parameters["r_eq"].in_units(u.nm).value,
bond.connection_type.parameters["k"]
@@ -253,12 +390,12 @@ def _harmonic_bond_potential_writer(top, bond):
return line
-def _harmonic_angle_potential_writer(top, angle):
+def _harmonic_angle_potential_writer(top, angle, shifted_idx_map):
"""Write harmonic angle information."""
- line = "\t{0}\t{1}\t{2}\t{3}\t{4:.5f}\t{5:.5f}\n".format(
- top.get_index(angle.connection_members[0]) + 1,
- top.get_index(angle.connection_members[1]) + 1,
- top.get_index(angle.connection_members[2]) + 1,
+ line = "{0:8s}{1:8s}{2:8s}{3:4s}{4:15.5f}{5:15.5f}\n".format(
+ str(shifted_idx_map[top.get_index(angle.connection_members[0])] + 1),
+ str(shifted_idx_map[top.get_index(angle.connection_members[1])] + 1),
+ str(shifted_idx_map[top.get_index(angle.connection_members[2])] + 1),
"1",
angle.connection_type.parameters["theta_eq"].in_units(u.degree).value,
angle.connection_type.parameters["k"]
@@ -268,13 +405,13 @@ def _harmonic_angle_potential_writer(top, angle):
return line
-def _ryckaert_bellemans_torsion_writer(top, dihedral):
+def _ryckaert_bellemans_torsion_writer(top, dihedral, shifted_idx_map):
"""Write Ryckaert-Bellemans Torsion information."""
- line = "\t{0}\t{1}\t{2}\t{3}\t{4}\t{5:.5f}\t{6:.5f}\t{7:.5f}\t{8:.5f}\t{9:.5f}\t{10:.5f}\n".format(
- top.get_index(dihedral.connection_members[0]) + 1,
- top.get_index(dihedral.connection_members[1]) + 1,
- top.get_index(dihedral.connection_members[2]) + 1,
- top.get_index(dihedral.connection_members[3]) + 1,
+ line = "{0:8s}{1:8s}{2:8s}{3:8s}{4:4s}{5:15.5f}{6:15.5f}{7:15.5f}{8:15.5f}{9:15.5f}{10:15.5f}\n".format(
+ str(shifted_idx_map[top.get_index(dihedral.connection_members[0])] + 1),
+ str(shifted_idx_map[top.get_index(dihedral.connection_members[1])] + 1),
+ str(shifted_idx_map[top.get_index(dihedral.connection_members[2])] + 1),
+ str(shifted_idx_map[top.get_index(dihedral.connection_members[3])] + 1),
"3",
dihedral.connection_type.parameters["c0"]
.in_units(u.Unit("kJ/mol"))
@@ -298,18 +435,105 @@ def _ryckaert_bellemans_torsion_writer(top, dihedral):
return line
-def _periodic_torsion_writer(top, dihedral):
+def _periodic_torsion_writer(top, dihedral, shifted_idx_map):
"""Write periodic torsion information."""
- line = "\t{0}\t{1}\t{2}\t{3}\t{4}\t{5:.5f}\t{6:.5f}\t{7}\n".format(
- top.get_index(dihedral.connection_members[0]) + 1,
- top.get_index(dihedral.connection_members[1]) + 1,
- top.get_index(dihedral.connection_members[2]) + 1,
- top.get_index(dihedral.connection_members[3]) + 1,
+ if isinstance(dihedral, Dihedral):
+ if dihedral.connection_type.parameters["phi_eq"].size == 1:
+ # Normal dihedral
+ layers, funct = 1, "1"
+ for key, val in dihedral.connection_type.parameters.items():
+ dihedral.connection_type.parameters[key] = val.reshape(layers)
+ else:
+ # Layered/Multiple dihedral
+ layers, funct = (
+ dihedral.connection_type.parameters["phi_eq"].size,
+ "9",
+ )
+ elif isinstance(dihedral, Improper):
+ layers, funct = 1, "4"
+ else:
+ raise TypeError(f"Type {type(dihedral)} not supported.")
+
+ lines = list()
+ for i in range(layers):
+ line = "{0:8s}{1:8s}{2:8s}{3:8s}{4:4s}{5:15.5f}{6:15.5f}{7:4}\n".format(
+ str(
+ shifted_idx_map[top.get_index(dihedral.connection_members[0])]
+ + 1
+ ),
+ str(
+ shifted_idx_map[top.get_index(dihedral.connection_members[1])]
+ + 1
+ ),
+ str(
+ shifted_idx_map[top.get_index(dihedral.connection_members[2])]
+ + 1
+ ),
+ str(
+ shifted_idx_map[top.get_index(dihedral.connection_members[3])]
+ + 1
+ ),
+ funct,
+ dihedral.connection_type.parameters["phi_eq"][i]
+ .in_units(u.degree)
+ .value,
+ dihedral.connection_type.parameters["k"][i]
+ .in_units(u.Unit("kJ/(mol)"))
+ .value,
+ dihedral.connection_type.parameters["n"][i].value,
+ )
+ lines.append(line)
+ return lines
+
+
+def _write_restraint(top, connection, type, shifted_idx_map):
+ """Worker function to write various connection restraint information."""
+ worker_functions = {
+ "bond_restraints": _bond_restraint_writer,
+ "angle_restraints": _angle_restraint_writer,
+ "dihedral_restraints": _dihedral_restraint_writer,
+ }
+
+ return worker_functions[type](top, connection, shifted_idx_map)
+
+
+def _bond_restraint_writer(top, bond, shifted_idx_map):
+ """Write bond restraint information."""
+ line = "{0:8s}{1:8s}{2:4s}{3:15.5f}{4:15.5f}\n".format(
+ str(shifted_idx_map[top.get_index(bond.connection_members[1])] + 1),
+ str(shifted_idx_map[top.get_index(bond.connection_members[0])] + 1),
+ "6",
+ bond.restraint["r_eq"].in_units(u.nm).value,
+ bond.restraint["k"].in_units(u.Unit("kJ/(mol * nm**2)")).value,
+ )
+ return line
+
+
+def _angle_restraint_writer(top, angle, shifted_idx_map):
+ """Write angle restraint information."""
+ line = "{0:8s}{1:8s}{2:8s}{3:8s}{4:4s}{5:15.5f}{6:15.5f}{7:4}\n".format(
+ str(shifted_idx_map[top.get_index(angle.connection_members[1])] + 1),
+ str(shifted_idx_map[top.get_index(angle.connection_members[0])] + 1),
+ str(shifted_idx_map[top.get_index(angle.connection_members[1])] + 1),
+ str(shifted_idx_map[top.get_index(angle.connection_members[2])] + 1),
"1",
- dihedral.connection_type.parameters["phi_eq"].in_units(u.degree).value,
- dihedral.connection_type.parameters["k"]
- .in_units(u.Unit("kJ/(mol)"))
- .value,
- dihedral.connection_type.parameters["n"].value,
+ angle.restraint["theta_eq"].in_units(u.degree).value,
+ angle.restraint["k"].in_units(u.Unit("kJ/mol")).value,
+ angle.restraint["n"],
+ )
+ return line
+
+
+def _dihedral_restraint_writer(top, dihedral, shifted_idx_map):
+ """Write dihedral restraint information."""
+ line = "{0:8s}{1:8s}{2:8s}{3:8s}{4:4s}{5:15.5f}{6:15.5f}{7:15.5f}\n".format(
+ str(shifted_idx_map[top.get_index(dihedral.connection_members[0])] + 1),
+ str(shifted_idx_map[top.get_index(dihedral.connection_members[1])] + 1),
+ str(shifted_idx_map[top.get_index(dihedral.connection_members[2])] + 1),
+ str(shifted_idx_map[top.get_index(dihedral.connection_members[3])] + 1),
+ "1",
+ dihedral.restraint["phi_eq"].in_units(u.degree).value,
+ dihedral.restraint["delta_phi"].in_units(u.degree).value,
+ dihedral.restraint["k"].in_units(u.Unit("kJ/(mol * rad**2)")).value,
)
return line
diff --git a/gmso/parameterization/topology_parameterizer.py b/gmso/parameterization/topology_parameterizer.py
index 9daf47346..381165015 100644
--- a/gmso/parameterization/topology_parameterizer.py
+++ b/gmso/parameterization/topology_parameterizer.py
@@ -459,7 +459,7 @@ def _get_atomtypes(
# Assume nodes in repeated structures are in the same order
for node, ref_node in zip(
sorted(subgraph.nodes),
- reference[molecule]["typemap"],
+ sorted(reference[molecule]["typemap"]),
):
typemap[node] = reference[molecule]["typemap"][
ref_node
diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py
index fb6e6bbe9..1c0fe1f5d 100644
--- a/gmso/tests/base_test.py
+++ b/gmso/tests/base_test.py
@@ -3,6 +3,7 @@
import numpy as np
import pytest
import unyt as u
+from foyer.tests.utils import get_fn
from gmso.core.angle import Angle
from gmso.core.atom import Atom
@@ -17,7 +18,7 @@
from gmso.external import from_mbuild, from_parmed
from gmso.external.convert_foyer_xml import from_foyer_xml
from gmso.tests.utils import get_path
-from gmso.utils.io import get_fn, has_foyer
+from gmso.utils.io import get_fn
class BaseTest:
@@ -49,6 +50,44 @@ def box(self):
def top(self):
return Topology(name="mytop")
+ @pytest.fixture
+ def benzene_ua(self):
+ compound = mb.load(get_fn("benzene_ua.mol2"))
+ compound.children[0].name = "BenzeneUA"
+ top = from_mbuild(compound)
+ top.identify_connections()
+ return top
+
+ @pytest.fixture
+ def benzene_ua_box(self):
+ compound = mb.load(get_fn("benzene_ua.mol2"))
+ compound.children[0].name = "BenzeneUA"
+ compound_box = mb.packing.fill_box(
+ compound=compound, n_compounds=5, density=1
+ )
+ top = from_mbuild(compound_box)
+ top.identify_connections()
+ return top
+
+ @pytest.fixture
+ def benzene_aa(self):
+ compound = mb.load(get_fn("benzene.mol2"))
+ compound.children[0].name = "BenzeneAA"
+ top = from_mbuild(compound)
+ top.identify_connections()
+ return top
+
+ @pytest.fixture
+ def benzene_aa_box(self):
+ compound = mb.load(get_fn("benzene.mol2"))
+ compound.children[0].name = "BenzeneAA"
+ compound_box = mb.packing.fill_box(
+ compound=compound, n_compounds=5, density=1
+ )
+ top = from_mbuild(compound_box)
+ top.identify_connections()
+ return top
+
@pytest.fixture
def ar_system(self, n_ar_system):
return from_mbuild(n_ar_system(), parse_label=True)
@@ -56,7 +95,7 @@ def ar_system(self, n_ar_system):
@pytest.fixture
def n_ar_system(self):
def _topology(n_sites=100):
- ar = mb.Compound(name="Ar")
+ ar = mb.Compound(name="Ar", element="Ar")
packed_system = mb.fill_box(
compound=ar,
@@ -226,9 +265,8 @@ def typed_water_system(self, water_system):
@pytest.fixture
def foyer_fullerene(self):
- if has_foyer:
- import foyer
- from foyer.tests.utils import get_fn
+ from foyer.tests.utils import get_fn
+
from_foyer_xml(get_fn("fullerene.xml"), overwrite=True)
gmso_ff = ForceField("fullerene_gmso.xml")
@@ -237,9 +275,8 @@ def foyer_fullerene(self):
@pytest.fixture
def foyer_periodic(self):
# TODO: this errors out with backend="ffutils"
- if has_foyer:
- import foyer
- from foyer.tests.utils import get_fn
+ from foyer.tests.utils import get_fn
+
from_foyer_xml(get_fn("oplsaa-periodic.xml"), overwrite=True)
gmso_ff = ForceField("oplsaa-periodic_gmso.xml", backend="gmso")
@@ -248,27 +285,23 @@ def foyer_periodic(self):
@pytest.fixture
def foyer_urey_bradley(self):
# TODO: this errors out with backend="ffutils"
- if has_foyer:
- import foyer
- from foyer.tests.utils import get_fn
+ from foyer.tests.utils import get_fn
- from_foyer_xml(get_fn("charmm36_cooh.xml"), overwrite=True)
- gmso_ff = ForceField("charmm36_cooh_gmso.xml", backend="gmso")
+ from_foyer_xml(get_fn("charmm36_cooh.xml"), overwrite=True)
+ gmso_ff = ForceField("charmm36_cooh_gmso.xml", backend="gmso")
- return gmso_ff
+ return gmso_ff
@pytest.fixture
def foyer_rb_torsion(self):
- if has_foyer:
- import foyer
- from foyer.tests.utils import get_fn
+ from foyer.tests.utils import get_fn
- from_foyer_xml(
- get_fn("refs-multi.xml"), overwrite=True, validate_foyer=True
- )
- gmso_ff = ForceField("refs-multi_gmso.xml")
+ from_foyer_xml(
+ get_fn("refs-multi.xml"), overwrite=True, validate_foyer=True
+ )
+ gmso_ff = ForceField("refs-multi_gmso.xml")
- return gmso_ff
+ return gmso_ff
@pytest.fixture
def methane(self):
diff --git a/gmso/tests/files/benzene.gro b/gmso/tests/files/benzene.gro
new file mode 100644
index 000000000..2ea50d5e8
--- /dev/null
+++ b/gmso/tests/files/benzene.gro
@@ -0,0 +1,63 @@
+Topology written by GMSO 0.9.1 at 2022-09-18 00:41:09.308507
+60
+ 1Ben C 1 7.041 2.815 0.848
+ 1Ben C 2 6.957 2.739 0.767
+ 1Ben C 3 6.820 2.737 0.792
+ 1Ben C 4 6.767 2.811 0.898
+ 1Ben C 5 6.851 2.887 0.979
+ 1Ben C 6 6.988 2.889 0.954
+ 1Ben H 7 7.148 2.817 0.828
+ 1Ben H 8 6.998 2.682 0.684
+ 1Ben H 9 6.754 2.678 0.729
+ 1Ben H 10 6.660 2.810 0.917
+ 1Ben H 11 6.810 2.945 1.061
+ 1Ben H 12 7.054 2.948 1.017
+ 2Ben C 13 5.646 3.077 6.562
+ 2Ben C 14 5.686 3.174 6.471
+ 2Ben C 15 5.718 3.138 6.340
+ 2Ben C 16 5.710 3.005 6.301
+ 2Ben C 17 5.669 2.907 6.392
+ 2Ben C 18 5.637 2.943 6.523
+ 2Ben H 19 5.621 3.105 6.664
+ 2Ben H 20 5.693 3.278 6.502
+ 2Ben H 21 5.750 3.214 6.269
+ 2Ben H 22 5.735 2.976 6.199
+ 2Ben H 23 5.662 2.803 6.361
+ 2Ben H 24 5.606 2.868 6.594
+ 3Ben C 25 5.897 6.825 1.520
+ 3Ben C 26 5.980 6.765 1.615
+ 3Ben C 27 5.931 6.663 1.696
+ 3Ben C 28 5.799 6.621 1.682
+ 3Ben C 29 5.716 6.680 1.587
+ 3Ben C 30 5.765 6.782 1.506
+ 3Ben H 31 5.936 6.904 1.456
+ 3Ben H 32 6.083 6.799 1.625
+ 3Ben H 33 5.996 6.617 1.770
+ 3Ben H 34 5.761 6.541 1.745
+ 3Ben H 35 5.613 6.647 1.576
+ 3Ben H 36 5.701 6.829 1.432
+ 4Ben C 37 7.018 2.222 6.626
+ 4Ben C 38 6.880 2.206 6.619
+ 4Ben C 39 6.795 2.304 6.670
+ 4Ben C 40 6.849 2.418 6.729
+ 4Ben C 41 6.987 2.434 6.737
+ 4Ben C 42 7.072 2.336 6.685
+ 4Ben H 43 7.084 2.145 6.586
+ 4Ben H 44 6.838 2.117 6.573
+ 4Ben H 45 6.687 2.291 6.665
+ 4Ben H 46 6.783 2.495 6.770
+ 4Ben H 47 7.029 2.523 6.782
+ 4Ben H 48 7.180 2.349 6.690
+ 5Ben C 49 6.192 3.667 7.643
+ 5Ben C 50 6.314 3.634 7.583
+ 5Ben C 51 6.383 3.730 7.509
+ 5Ben C 52 6.330 3.859 7.496
+ 5Ben C 53 6.209 3.891 7.556
+ 5Ben C 54 6.140 3.795 7.629
+ 5Ben H 55 6.138 3.592 7.700
+ 5Ben H 56 6.355 3.534 7.594
+ 5Ben H 57 6.478 3.705 7.462
+ 5Ben H 58 6.384 3.934 7.438
+ 5Ben H 59 6.168 3.991 7.545
+ 5Ben H 60 6.045 3.820 7.676
+ 8.65597 8.65597 8.65597
diff --git a/gmso/tests/files/benzene.top b/gmso/tests/files/benzene.top
new file mode 100644
index 000000000..d4de28f89
--- /dev/null
+++ b/gmso/tests/files/benzene.top
@@ -0,0 +1,99 @@
+; File Topology written by GMSO at 2022-10-28 01:05:54.340726
+
+[ defaults ]
+; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ
+1 3 no 0.5 0.5
+
+[ atomtypes ]
+; name at.num mass charge ptype sigma epsilon
+opls_145 6 12.01100 -0.11500 A 0.35500 0.29288
+opls_146 1 1.00800 0.11500 A 0.24200 0.12552
+
+[ moleculetype ]
+; name nrexcl
+BenzeneAA 3
+[ atoms ]
+; nr type resnr residue atom cgnr charge mass
+1 opls_145 1 BenzeneAA C 1 -0.11500 12.01100
+2 opls_145 1 BenzeneAA C 1 -0.11500 12.01100
+3 opls_145 1 BenzeneAA C 1 -0.11500 12.01100
+4 opls_145 1 BenzeneAA C 1 -0.11500 12.01100
+5 opls_145 1 BenzeneAA C 1 -0.11500 12.01100
+6 opls_145 1 BenzeneAA C 1 -0.11500 12.01100
+7 opls_146 1 BenzeneAA H 1 0.11500 1.00800
+8 opls_146 1 BenzeneAA H 1 0.11500 1.00800
+9 opls_146 1 BenzeneAA H 1 0.11500 1.00800
+10 opls_146 1 BenzeneAA H 1 0.11500 1.00800
+11 opls_146 1 BenzeneAA H 1 0.11500 1.00800
+12 opls_146 1 BenzeneAA H 1 0.11500 1.00800
+
+[ bonds ]
+; ai aj funct b0 kb
+2 1 1 0.14000 392459.20000
+6 1 1 0.14000 392459.20000
+7 1 1 0.10800 307105.60000
+3 2 1 0.14000 392459.20000
+8 2 1 0.10800 307105.60000
+4 3 1 0.14000 392459.20000
+9 3 1 0.10800 307105.60000
+5 4 1 0.14000 392459.20000
+10 4 1 0.10800 307105.60000
+6 5 1 0.14000 392459.20000
+11 5 1 0.10800 307105.60000
+12 6 1 0.10800 307105.60000
+
+[ angles ]
+; ai aj ak funct phi_0 k0
+6 1 7 1 120.00000 292.88000
+2 1 7 1 120.00000 292.88000
+1 2 8 1 120.00000 292.88000
+3 2 8 1 120.00000 292.88000
+4 3 9 1 120.00000 292.88000
+2 3 9 1 120.00000 292.88000
+5 4 10 1 120.00000 292.88000
+3 4 10 1 120.00000 292.88000
+4 5 11 1 120.00000 292.88000
+6 5 11 1 120.00000 292.88000
+1 6 12 1 120.00000 292.88000
+5 6 12 1 120.00000 292.88000
+3 4 5 1 120.00000 527.18400
+2 3 4 1 120.00000 527.18400
+4 5 6 1 120.00000 527.18400
+2 1 6 1 120.00000 527.18400
+1 2 3 1 120.00000 527.18400
+1 6 5 1 120.00000 527.18400
+
+[ dihedrals ]
+; ai aj ak al funct c0 c1 c2 c3 c4 c5
+7 1 6 5 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+7 1 6 12 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+7 1 2 8 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+7 1 2 3 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+8 2 1 6 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+8 2 3 4 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+8 2 3 9 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+9 3 4 5 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+9 3 4 10 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+9 3 2 1 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+10 4 5 6 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+10 4 5 11 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+10 4 3 2 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+11 5 4 3 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+11 5 6 1 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+11 5 6 12 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+12 6 1 2 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+12 6 5 4 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+3 4 5 6 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+4 3 2 1 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+5 4 3 2 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+4 5 6 1 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+2 1 6 5 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+3 2 1 6 3 30.33400 0.00000 -30.33400 0.00000 0.00000 0.00000
+
+[ system ]
+; name
+Topology
+
+[ molecules ]
+; molecule nmols
+BenzeneAA 5
diff --git a/gmso/tests/files/benzene_trappe-ua.xml b/gmso/tests/files/benzene_trappe-ua.xml
new file mode 100755
index 000000000..a8ce966e5
--- /dev/null
+++ b/gmso/tests/files/benzene_trappe-ua.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/tests/files/restrained_benzene_ua.gro b/gmso/tests/files/restrained_benzene_ua.gro
new file mode 100644
index 000000000..92fc6fc2c
--- /dev/null
+++ b/gmso/tests/files/restrained_benzene_ua.gro
@@ -0,0 +1,33 @@
+Topology written by GMSO 0.9.1 at 2022-11-07 11:51:34.770776
+30
+ 1Com _CH 1 5.127 3.773 4.686
+ 1Com _CH 2 5.204 3.757 4.802
+ 1Com _CH 3 5.343 3.766 4.796
+ 1Com _CH 4 5.407 3.793 4.674
+ 1Com _CH 5 5.330 3.809 4.558
+ 1Com _CH 6 5.191 3.800 4.564
+ 2Com _CH 7 3.490 3.266 4.318
+ 2Com _CH 8 3.547 3.139 4.298
+ 2Com _CH 9 3.552 3.048 4.404
+ 2Com _CH 10 3.500 3.084 4.530
+ 2Com _CH 11 3.444 3.210 4.549
+ 2Com _CH 12 3.439 3.302 4.443
+ 3Com _CH 13 6.609 1.796 0.837
+ 3Com _CH 14 6.516 1.728 0.758
+ 3Com _CH 15 6.535 1.593 0.727
+ 3Com _CH 16 6.648 1.526 0.776
+ 3Com _CH 17 6.741 1.594 0.855
+ 3Com _CH 18 6.721 1.729 0.886
+ 4Com _CH 19 6.543 2.442 2.196
+ 4Com _CH 20 6.657 2.484 2.126
+ 4Com _CH 21 6.644 2.558 2.008
+ 4Com _CH 22 6.517 2.591 1.960
+ 4Com _CH 23 6.403 2.549 2.029
+ 4Com _CH 24 6.416 2.474 2.147
+ 5Com _CH 25 6.699 5.604 7.130
+ 5Com _CH 26 6.652 5.661 7.011
+ 5Com _CH 27 6.611 5.795 7.010
+ 5Com _CH 28 6.618 5.872 7.127
+ 5Com _CH 29 6.666 5.814 7.245
+ 5Com _CH 30 6.706 5.680 7.247
+ 8.42655 8.42655 8.42655
diff --git a/gmso/tests/files/restrained_benzene_ua.top b/gmso/tests/files/restrained_benzene_ua.top
new file mode 100644
index 000000000..d1859f994
--- /dev/null
+++ b/gmso/tests/files/restrained_benzene_ua.top
@@ -0,0 +1,85 @@
+; File Topology written by GMSO at 2022-11-07 11:51:34.761305
+
+[ defaults ]
+; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ
+1 2 no 0.0 0.0
+
+[ atomtypes ]
+; name at.num mass charge ptype sigma epsilon
+CH_sp2 6 13.01900 0.00000 A 0.36950 0.41988
+
+[ moleculetype ]
+; name nrexcl
+Compound 3
+[ atoms ]
+; nr type resnr residue atom cgnr charge mass
+1 CH_sp2 1 Compound _CH 1 0.00000 13.01900
+2 CH_sp2 1 Compound _CH 1 0.00000 13.01900
+3 CH_sp2 1 Compound _CH 1 0.00000 13.01900
+4 CH_sp2 1 Compound _CH 1 0.00000 13.01900
+5 CH_sp2 1 Compound _CH 1 0.00000 13.01900
+6 CH_sp2 1 Compound _CH 1 0.00000 13.01900
+
+[ bonds ]
+; ai aj funct b0 kb
+2 1 1 0.14000 0.00000
+6 1 1 0.14000 0.00000
+3 2 1 0.14000 0.00000
+4 3 1 0.14000 0.00000
+5 4 1 0.14000 0.00000
+6 5 1 0.14000 0.00000
+
+[ bonds ] ;Harmonic potential restraint
+; ai aj funct b0 kb
+1 2 6 0.14000 1000.00000
+1 6 6 0.14000 1000.00000
+2 3 6 0.14000 1000.00000
+3 4 6 0.14000 1000.00000
+4 5 6 0.14000 1000.00000
+5 6 6 0.14000 1000.00000
+
+[ angles ]
+; ai aj ak funct phi_0 k0
+2 3 4 1 120.00000 0.10000
+3 4 5 1 120.00000 0.10000
+4 5 6 1 120.00000 0.10000
+1 2 3 1 120.00000 0.10000
+2 1 6 1 120.00000 0.10000
+1 6 5 1 120.00000 0.10000
+
+[ angle_restraints ]
+; ai aj ai ak funct theta_eq k multiplicity
+3 2 3 4 1 120.00000 1000.00000 1
+4 3 4 5 1 120.00000 1000.00000 1
+5 4 5 6 1 120.00000 1000.00000 1
+2 1 2 3 1 120.00000 1000.00000 1
+1 2 1 6 1 120.00000 1000.00000 1
+6 1 6 5 1 120.00000 1000.00000 1
+
+[ dihedrals ]
+; ai aj ak al funct c0 c1 c2 c3 c4 c5
+4 3 2 1 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
+3 4 5 6 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
+4 5 6 1 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
+5 4 3 2 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
+3 2 1 6 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
+2 1 6 5 3 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
+
+[ dihedral_restraints ]
+#ifdef DIHRES
+; ai aj ak al funct theta_eq delta_theta kd
+4 3 2 1 1 0.00000 0.00000 1000.00000
+3 4 5 6 1 0.00000 0.00000 1000.00000
+4 5 6 1 1 0.00000 0.00000 1000.00000
+5 4 3 2 1 0.00000 0.00000 1000.00000
+3 2 1 6 1 0.00000 0.00000 1000.00000
+2 1 6 5 1 0.00000 0.00000 1000.00000
+#endif DIHRES
+
+[ system ]
+; name
+Topology
+
+[ molecules ]
+; molecule nmols
+Compound 5
diff --git a/gmso/tests/test_convert_foyer_xml.py b/gmso/tests/test_convert_foyer_xml.py
index 4317794b7..b25a7576a 100644
--- a/gmso/tests/test_convert_foyer_xml.py
+++ b/gmso/tests/test_convert_foyer_xml.py
@@ -9,32 +9,35 @@
from gmso.external.convert_foyer_xml import from_foyer_xml
from gmso.tests.base_test import BaseTest
from gmso.tests.utils import get_path
-from gmso.utils.io import has_foyer
-
-if has_foyer:
- from foyer.tests.utils import get_fn
parameterized_ffs = ["fullerene.xml", "oplsaa-periodic.xml", "lj.xml"]
-@pytest.mark.skipif(not has_foyer, reason="Foyer is not installed")
class TestXMLConversion(BaseTest):
@pytest.mark.parametrize("ff", parameterized_ffs)
def test_from_foyer(self, ff):
+ from foyer.tests.utils import get_fn
+
from_foyer_xml(get_fn(ff), overwrite=True)
@pytest.mark.parametrize("ff", parameterized_ffs)
def test_from_foyer_overwrite_false(self, ff):
+ from foyer.tests.utils import get_fn
+
from_foyer_xml(get_fn(ff), overwrite=False)
with pytest.raises(FileExistsError):
from_foyer_xml(get_fn(ff), overwrite=False)
@pytest.mark.parametrize("ff", parameterized_ffs)
def test_from_foyer_different_name(self, ff):
+ from foyer.tests.utils import get_fn
+
from_foyer_xml(get_fn(ff), f"{ff}-gmso-converted.xml", overwrite=True)
@pytest.mark.parametrize("ff", parameterized_ffs)
def test_from_foyer_validate_foyer(self, ff):
+ from foyer.tests.utils import get_fn
+
from_foyer_xml(
get_fn(ff),
f"{ff}-gmso-converted.xml",
@@ -44,6 +47,8 @@ def test_from_foyer_validate_foyer(self, ff):
@pytest.mark.parametrize("ff", parameterized_ffs)
def test_foyer_pathlib(self, ff):
+ from foyer.tests.utils import get_fn
+
file_path = Path(get_fn(ff))
from_foyer_xml(file_path, overwrite=True)
diff --git a/gmso/tests/test_gro.py b/gmso/tests/test_gro.py
index c90b8a7d8..234e5292a 100644
--- a/gmso/tests/test_gro.py
+++ b/gmso/tests/test_gro.py
@@ -7,11 +7,15 @@
from gmso.external.convert_parmed import from_parmed
from gmso.formats.gro import read_gro, write_gro
from gmso.tests.base_test import BaseTest
-from gmso.utils.io import get_fn, has_parmed, import_
+from gmso.tests.utils import get_path
+from gmso.utils.io import get_fn, has_mbuild, has_parmed, import_
if has_parmed:
pmd = import_("parmed")
+if has_mbuild:
+ mb = import_("mbuild")
+
@pytest.mark.skipif(not has_parmed, reason="ParmEd is not installed")
class TestGro(BaseTest):
@@ -33,7 +37,7 @@ def test_read_gro(self):
)
def test_wrong_n_atoms(self):
- with pytest.raises(ValueError):
+ with pytest.raises(IndexError):
Topology.load(get_fn("too_few_atoms.gro"))
with pytest.raises(ValueError):
Topology.load(get_fn("too_many_atoms.gro"))
@@ -46,3 +50,49 @@ def test_write_gro_non_orthogonal(self):
top = from_parmed(pmd.load_file(get_fn("ethane.gro"), structure=True))
top.box.angles = u.degree * [90, 90, 120]
top.save("out.gro")
+
+ @pytest.mark.skipif(not has_mbuild, reason="mBuild not installed.")
+ def test_benzene_gro(self):
+ import mbuild as mb
+ from mbuild.packing import fill_box
+
+ from gmso.external import from_mbuild
+
+ benzene = mb.load(get_fn("benzene.mol2"))
+ benzene.children[0].name = "Benzene"
+ box_of_benzene = fill_box(compound=benzene, n_compounds=5, density=1)
+ top = from_mbuild(box_of_benzene)
+ top.save("benzene.gro")
+
+ reread = Topology.load("benzene.gro")
+ for site, ref_site in zip(reread.sites, top.sites):
+ assert site.molecule.name == ref_site.molecule.name[:3]
+ assert site.molecule.number == ref_site.molecule.number
+
+ @pytest.mark.parametrize("fixture", ["benzene_ua_box", "benzene_aa_box"])
+ def test_full_loop_gro_molecule(self, fixture, request):
+ top = request.getfixturevalue(fixture)
+ top.save("benzene.gro")
+
+ # Re-read in and compare with reference
+ top = Topology.load("benzene.gro")
+
+ refs = {
+ "benzene_aa_box": "benzene.gro",
+ "benzene_ua_box": "restrained_benzene_ua.gro",
+ }
+ ref = Topology.load(get_path(refs[fixture]))
+
+ assert len(top.sites) == len(ref.sites)
+ assert top.unique_site_labels("molecule") == ref.unique_site_labels(
+ "molecule"
+ )
+
+ if top == "benzene_ua_box":
+ for site in top.sites:
+ assert site.molecule.name == "Com"
+ assert site.name == "_CH"
+ elif top == "benzene_aa_box":
+ for site in top.sites:
+ assert site.molecule.name == "Ben"
+ assert site.name in ["C", "H"]
diff --git a/gmso/tests/test_top.py b/gmso/tests/test_top.py
index ec8911e2a..4bb2679b2 100644
--- a/gmso/tests/test_top.py
+++ b/gmso/tests/test_top.py
@@ -1,3 +1,4 @@
+import forcefield_utilities as ffutils
import parmed as pmd
import pytest
import unyt as u
@@ -5,9 +6,13 @@
import gmso
from gmso.exceptions import EngineIncompatibilityError
from gmso.formats.top import write_top
+from gmso.parameterization import apply
from gmso.tests.base_test import BaseTest
from gmso.tests.utils import get_path
-from gmso.utils.io import get_fn
+from gmso.utils.io import get_fn, has_mbuild, import_
+
+if has_mbuild:
+ mb = import_("mbuild")
class TestTop(BaseTest):
@@ -90,6 +95,10 @@ def test_ethane_periodic(self, typed_ethane):
for dihedral in typed_ethane.dihedrals:
dihedral.connection_type = periodic_dihedral_type
+ for i in range(typed_ethane.n_impropers - 1, -1, -1):
+ if not typed_ethane.impropers[i].improper_type:
+ typed_ethane._impropers.pop(i)
+
typed_ethane.update_connection_types()
typed_ethane.save("system.top")
@@ -105,3 +114,96 @@ def test_custom_defaults(self, typed_ethane):
assert struct.defaults.gen_pairs == "yes"
assert struct.defaults.fudgeLJ == 0.5
assert struct.defaults.fudgeQQ == 0.5
+
+ def test_benzene_top(self, benzene_aa_box):
+ top = benzene_aa_box
+ oplsaa = ffutils.FoyerFFs().load("oplsaa").to_gmso_ff()
+ top = apply(top=top, forcefields=oplsaa, remove_untyped=True)
+ top.save("benzene.top")
+
+ with open("benzene.top") as f:
+ f_cont = f.readlines()
+
+ with open(get_path("benzene.top")) as ref:
+ ref_cont = ref.readlines()
+
+ assert len(f_cont) == len(ref_cont)
+
+ def test_benzene_restraints(self, benzene_ua_box):
+ top = benzene_ua_box
+ trappe_benzene = (
+ ffutils.FoyerFFs()
+ .load(get_path("benzene_trappe-ua.xml"))
+ .to_gmso_ff()
+ )
+ top = apply(top=top, forcefields=trappe_benzene, remove_untyped=True)
+
+ for bond in top.bonds:
+ bond.restraint = {
+ "r_eq": bond.bond_type.parameters["r_eq"],
+ "k": 1000 * u.kJ / (u.mol * u.nm**2),
+ }
+ for angle in top.angles:
+ # Apply restraint for angle
+ angle.restraint = {
+ "theta_eq": angle.angle_type.parameters["theta_eq"],
+ "k": 1000 * u.kJ / u.mol,
+ "n": 1,
+ }
+
+ for dihedral in top.dihedrals:
+ # Apply restraint fot dihedral
+ dihedral.restraint = {
+ "phi_eq": 0 * u.degree,
+ "delta_phi": 0 * u.degree,
+ "k": 1000 * u.kJ / (u.mol * u.rad**2),
+ }
+ top.save("restrained_benzene_ua.top")
+
+ with open("restrained_benzene_ua.top") as f:
+ f_cont = f.readlines()
+
+ with open(get_path("restrained_benzene_ua.top")) as ref:
+ ref_cont = ref.readlines()
+
+ assert len(f_cont) == len(ref_cont)
+
+ ref_sections = dict()
+ sections = dict()
+ current_section = None
+ for line, ref in zip(f_cont[1:], ref_cont[1:]):
+ if line.startswith("["):
+ assert line == ref
+ current_section = line
+ sections[current_section] = set()
+ ref_sections[current_section] = set()
+ elif line.startswith("#"):
+ assert line == ref
+ elif current_section is not None:
+ sections[current_section].add(line)
+ ref_sections[current_section].add(ref)
+
+ for section, ref_section in zip(sections, ref_sections):
+ assert section == ref_section
+ if "dihedral" in section:
+ # Need to deal with these separatelt due to member's order issue
+ # Each dict will have the keys be members and values be their parameters
+ print(section)
+ members = dict()
+ ref_members = dict()
+ for line, ref in zip(
+ sections[section], ref_sections[ref_section]
+ ):
+ line = line.split()
+ ref = ref.split()
+ members["-".join(line[:4])] = line[4:]
+ members["-".join(reversed(line[:4]))] = line[4:]
+ ref_members["-".join(ref[:4])] = ref[4:]
+ ref_members["-".join(reversed(ref[:4]))] = ref[4:]
+
+ assert members == ref_members
+ for member in members:
+ assert members[member] == ref_members[member]
+
+ else:
+ assert sections[section] == ref_sections[ref_section]
diff --git a/gmso/utils/compatibility.py b/gmso/utils/compatibility.py
index ed631654b..0437a26b2 100644
--- a/gmso/utils/compatibility.py
+++ b/gmso/utils/compatibility.py
@@ -1,10 +1,11 @@
"""Determine if the parametrized gmso.topology can be written to an engine."""
import sympy
+from gmso.core.views import PotentialFilters
from gmso.exceptions import EngineIncompatibilityError
-def check_compatibility(topology, accepted_potentials):
+def check_compatibility(topology, accepted_potentials, simplify_check=False):
"""
Compare the potentials in a topology against a list of accepted potential templates.
@@ -14,7 +15,8 @@ def check_compatibility(topology, accepted_potentials):
The topology whose potentials to check.
accepted_potentials: list
A list of gmso.Potential objects to check against
-
+ simplify_check : bool, optional, default=False
+ Simplify the sympy expression check, aka, only compare the expression string
Returns
-------
potential_forms_dict: dict
@@ -24,8 +26,12 @@ def check_compatibility(topology, accepted_potentials):
"""
potential_forms_dict = dict()
- for atom_type in topology.atom_types:
- potential_form = _check_single_potential(atom_type, accepted_potentials)
+ for atom_type in topology.atom_types(
+ # filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ):
+ potential_form = _check_single_potential(
+ atom_type, accepted_potentials, simplify_check
+ )
if not potential_form:
raise EngineIncompatibilityError(
f"Potential {atom_type} is not in the list of accepted_potentials {accepted_potentials}"
@@ -33,9 +39,11 @@ def check_compatibility(topology, accepted_potentials):
else:
potential_forms_dict.update(potential_form)
- for connection_type in topology.connection_types:
+ for connection_type in topology.connection_types(
+ # filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ):
potential_form = _check_single_potential(
- connection_type, accepted_potentials
+ connection_type, accepted_potentials, simplify_check
)
if not potential_form:
raise EngineIncompatibilityError(
@@ -47,10 +55,14 @@ def check_compatibility(topology, accepted_potentials):
return potential_forms_dict
-def _check_single_potential(potential, accepted_potentials):
+def _check_single_potential(potential, accepted_potentials, simplify_check):
"""Check to see if a single given potential is in the list of accepted potentials."""
for ref in accepted_potentials:
if ref.independent_variables == potential.independent_variables:
- if sympy.simplify(ref.expression - potential.expression) == 0:
- return {potential: ref.name}
+ if simplify_check:
+ if str(ref.expression) == str(potential.expression):
+ return {potential: ref.name}
+ else:
+ if sympy.simplify(ref.expression - potential.expression) == 0:
+ return {potential: ref.name}
return False
diff --git a/gmso/utils/io.py b/gmso/utils/io.py
index 387b611aa..e2eafa2bc 100644
--- a/gmso/utils/io.py
+++ b/gmso/utils/io.py
@@ -132,14 +132,6 @@ def import_(module):
except ImportError:
has_mbuild = False
-try:
- import foyer
-
- has_foyer = True
- del foyer
-except ImportError:
- has_foyer = False
-
try:
import gsd
From 6bcdfcc67671f1692a901a50ceb3a2ecee40fde3 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 29 Nov 2022 11:15:11 -0600
Subject: [PATCH 089/141] [pre-commit.ci] pre-commit autoupdate (#702)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5f6aab2bd..1fd52b097 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,7 +9,7 @@ ci:
submodules: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.3.0
+ rev: v4.4.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
From fe321e23740329b61080a3f0e2ebc9ad95ef15e0 Mon Sep 17 00:00:00 2001
From: "lgtm-com[bot]" <43144390+lgtm-com[bot]@users.noreply.github.com>
Date: Wed, 7 Dec 2022 15:24:23 -0600
Subject: [PATCH 090/141] Add CodeQL workflow for GitHub code scanning (#699)
* Add CodeQL workflow for GitHub code scanning
* fix codeql error
Co-authored-by: LGTM Migrator
Co-authored-by: Co Quach
---
.github/workflows/codeql.yml | 41 +++++++++++++++++++
gmso/formats/gsd.py | 1 -
.../test_parameterization_options.py | 3 ++
gmso/utils/nx_utils.py | 2 +-
4 files changed, 45 insertions(+), 2 deletions(-)
create mode 100644 .github/workflows/codeql.yml
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..89b7978fa
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,41 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ schedule:
+ - cron: "41 4 * * 6"
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ python ]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+ queries: +security-and-quality
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
+ with:
+ category: "/language:${{ matrix.language }}"
diff --git a/gmso/formats/gsd.py b/gmso/formats/gsd.py
index 4812c711c..58db4b2f7 100644
--- a/gmso/formats/gsd.py
+++ b/gmso/formats/gsd.py
@@ -325,7 +325,6 @@ def _write_dihedral_information(gsd_snapshot, top):
def _prepare_box_information(top):
"""Prepare the box information for writing to gsd."""
- lx = ly = lz = xy = xz = yz = 0.0
if allclose_units(
top.box.angles, np.array([90, 90, 90]) * u.degree, rtol=1e-5, atol=1e-8
):
diff --git a/gmso/tests/parameterization/test_parameterization_options.py b/gmso/tests/parameterization/test_parameterization_options.py
index 1c4a84282..bbf7a9ff3 100644
--- a/gmso/tests/parameterization/test_parameterization_options.py
+++ b/gmso/tests/parameterization/test_parameterization_options.py
@@ -268,6 +268,9 @@ def test_hierarchical_mol_structure(
}
elif match_ff_by == "group":
ff_dict = {"sol1": oplsaa_gmso, "sol2": tip3p}
+ else:
+ raise ValueError("Unexpected value provided match_ff_by.")
+
apply(
top,
ff_dict,
diff --git a/gmso/utils/nx_utils.py b/gmso/utils/nx_utils.py
index 7f4a18c5f..abdadbd39 100644
--- a/gmso/utils/nx_utils.py
+++ b/gmso/utils/nx_utils.py
@@ -586,7 +586,7 @@ def get_edges(networkx_graph, atom_name1, atom_name2):
for nodes in list(networkx_graph.edges.items()):
if nodes[1]["connection"].bond_type is None:
selectable_dict = create_dict_of_labels_for_edges(
- selectable_dict, edge
+ selectable_dict, nodes[0]
)
mia_bond_flag = 1
if not mia_bond_flag:
From 50856407bf4b54f4d21fadac8d1281fe072dcec4 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Tue, 13 Dec 2022 10:41:56 -0600
Subject: [PATCH 091/141] Speed up check_compatibility for relevant writer
method (#700)
* init commit, standard simplify_check for relevant write methods
* adapt new speed up for lammps
* revert changes in lammpsdata write in favor of new/separate PR
* remove simplify_check from topology save function
* better if condition for _check_single_potential
* implement better criteria to for first check in _check_single_potential
---
gmso/core/topology.py | 14 ++------------
gmso/formats/__init__.py | 4 ++--
gmso/formats/json.py | 2 +-
gmso/formats/mol2.py | 2 +-
gmso/formats/top.py | 24 +++++++++++++++++++-----
gmso/utils/compatibility.py | 25 +++++++++++++++----------
6 files changed, 40 insertions(+), 31 deletions(-)
diff --git a/gmso/core/topology.py b/gmso/core/topology.py
index f1199788c..36f2a1316 100644
--- a/gmso/core/topology.py
+++ b/gmso/core/topology.py
@@ -1158,18 +1158,6 @@ def get_index(self, member):
return index
- def _reindex_connection_types(self, ref):
- """Re-generate the indices of the connection types in the topology."""
- if ref not in self._index_refs:
- raise GMSOError(
- f"cannot reindex {ref}. It should be one of "
- f"{ANGLE_TYPE_DICT}, {BOND_TYPE_DICT}, "
- f"{ANGLE_TYPE_DICT}, {DIHEDRAL_TYPE_DICT}, {IMPROPER_TYPE_DICT},"
- f"{PAIRPOTENTIAL_TYPE_DICT}"
- )
- for i, ref_member in enumerate(self._set_refs[ref].keys()):
- self._index_refs[ref][ref_member] = i
-
def get_forcefield(self):
"""Get an instance of gmso.ForceField out of this topology
@@ -1399,6 +1387,8 @@ def save(self, filename, overwrite=False, **kwargs):
**kwargs:
The arguments to specific file savers listed below(as extensions):
* json: types, update, indent
+ * gro: precision
+ * lammps/lammpsdata: atom_style
"""
if not isinstance(filename, Path):
filename = Path(filename).resolve()
diff --git a/gmso/formats/__init__.py b/gmso/formats/__init__.py
index e054fd02b..ad9fa3568 100644
--- a/gmso/formats/__init__.py
+++ b/gmso/formats/__init__.py
@@ -4,10 +4,10 @@
from .formats_registry import LoadersRegistry, SaversRegistry
from .gro import read_gro, write_gro
from .gsd import write_gsd
-from .json import save_json
+from .json import write_json
from .lammpsdata import write_lammpsdata
from .mcf import write_mcf
-from .mol2 import from_mol2
+from .mol2 import read_mol2
from .top import write_top
from .xyz import read_xyz, write_xyz
diff --git a/gmso/formats/json.py b/gmso/formats/json.py
index 4a139e522..c3698ce6c 100644
--- a/gmso/formats/json.py
+++ b/gmso/formats/json.py
@@ -282,7 +282,7 @@ def _from_json(json_dict):
@saves_as(".json")
-def save_json(top, filename, **kwargs):
+def write_json(top, filename, **kwargs):
"""Save the topology as a JSON file.
Parameters
diff --git a/gmso/formats/mol2.py b/gmso/formats/mol2.py
index 5ed37ce87..69904549b 100644
--- a/gmso/formats/mol2.py
+++ b/gmso/formats/mol2.py
@@ -12,7 +12,7 @@
@loads_as(".mol2")
-def from_mol2(filename, site_type="atom"):
+def read_mol2(filename, site_type="atom"):
"""Read in a TRIPOS mol2 file format into a gmso topology object.
Creates a Topology from a mol2 file structure. This will read in the
diff --git a/gmso/formats/top.py b/gmso/formats/top.py
index f8e17c6de..23ac0654b 100644
--- a/gmso/formats/top.py
+++ b/gmso/formats/top.py
@@ -21,9 +21,23 @@
@saves_as(".top")
-def write_top(top, filename, top_vars=None, simplify_check=False):
- """Write a gmso.core.Topology object to a GROMACS topology (.TOP) file."""
- pot_types = _validate_compatibility(top, simplify_check)
+def write_top(top, filename, top_vars=None):
+ """Write a gmso.core.Topology object to a GROMACS topology (.TOP) file.
+
+ Parameters
+ ----------
+ top : gmso.Topology
+ A typed Topology Object
+ filename : str
+ Path of the output file
+
+ Notes
+ -----
+ See https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html for
+ a full description of the top file format. This method is a work in progress and do not currently
+ support the full GROMACS specs.
+ """
+ pot_types = _validate_compatibility(top)
top_vars = _get_top_vars(top, top_vars)
# Sanity checks
@@ -262,9 +276,9 @@ def _accepted_potentials():
return accepted_potentials
-def _validate_compatibility(top, simplify_check):
+def _validate_compatibility(top):
"""Check compatability of topology object with GROMACS TOP format."""
- pot_types = check_compatibility(top, _accepted_potentials(), simplify_check)
+ pot_types = check_compatibility(top, _accepted_potentials())
return pot_types
diff --git a/gmso/utils/compatibility.py b/gmso/utils/compatibility.py
index 0437a26b2..c79e7204d 100644
--- a/gmso/utils/compatibility.py
+++ b/gmso/utils/compatibility.py
@@ -5,7 +5,7 @@
from gmso.exceptions import EngineIncompatibilityError
-def check_compatibility(topology, accepted_potentials, simplify_check=False):
+def check_compatibility(topology, accepted_potentials):
"""
Compare the potentials in a topology against a list of accepted potential templates.
@@ -15,8 +15,7 @@ def check_compatibility(topology, accepted_potentials, simplify_check=False):
The topology whose potentials to check.
accepted_potentials: list
A list of gmso.Potential objects to check against
- simplify_check : bool, optional, default=False
- Simplify the sympy expression check, aka, only compare the expression string
+
Returns
-------
potential_forms_dict: dict
@@ -30,7 +29,8 @@ def check_compatibility(topology, accepted_potentials, simplify_check=False):
# filter_by=PotentialFilters.UNIQUE_NAME_CLASS
):
potential_form = _check_single_potential(
- atom_type, accepted_potentials, simplify_check
+ atom_type,
+ accepted_potentials,
)
if not potential_form:
raise EngineIncompatibilityError(
@@ -43,7 +43,8 @@ def check_compatibility(topology, accepted_potentials, simplify_check=False):
# filter_by=PotentialFilters.UNIQUE_NAME_CLASS
):
potential_form = _check_single_potential(
- connection_type, accepted_potentials, simplify_check
+ connection_type,
+ accepted_potentials,
)
if not potential_form:
raise EngineIncompatibilityError(
@@ -55,14 +56,18 @@ def check_compatibility(topology, accepted_potentials, simplify_check=False):
return potential_forms_dict
-def _check_single_potential(potential, accepted_potentials, simplify_check):
+def _check_single_potential(potential, accepted_potentials):
"""Check to see if a single given potential is in the list of accepted potentials."""
+ ind_var = potential.independent_variables
+ u_dims = {para.units.dimensions for para in potential.parameters.values()}
for ref in accepted_potentials:
- if ref.independent_variables == potential.independent_variables:
- if simplify_check:
- if str(ref.expression) == str(potential.expression):
- return {potential: ref.name}
+ ref_ind_var = ref.independent_variables
+ ref_u_dims = set(ref.expected_parameters_dimensions.values())
+ if len(ind_var) == len(ref_ind_var) and u_dims == ref_u_dims:
+ if str(ref.expression) == str(potential.expression):
+ return {potential: ref.name}
else:
+ print("Simpify", ref, potential)
if sympy.simplify(ref.expression - potential.expression) == 0:
return {potential: ref.name}
return False
From 406336a239393d6847aa2dc03eb66053f57fd7a6 Mon Sep 17 00:00:00 2001
From: Co Quach <43968221+daico007@users.noreply.github.com>
Date: Wed, 14 Dec 2022 16:06:45 -0600
Subject: [PATCH 092/141] turn off fail-fast & update readme (#703)
* turn off fail-fast
* fix typo
* update example and installation in readme
---
.github/workflows/CI.yaml | 1 +
README.md | 48 +++++++++++++++++++++++++++------------
2 files changed, 35 insertions(+), 14 deletions(-)
diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
index 132444f2d..049556f8d 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/CI.yaml
@@ -16,6 +16,7 @@ jobs:
name: GMSO Tests
runs-on: ${{ matrix.os }}
strategy:
+ fail-fast: false
matrix:
os: [macOS-latest, ubuntu-latest]
python-version: ["3.8", "3.9"]
diff --git a/README.md b/README.md
index 1957a6a90..495eeb7ad 100644
--- a/README.md
+++ b/README.md
@@ -14,23 +14,26 @@ To learn more, get started, or contribute, check out our [Documentation](https:/
-This is an example using `mBuild` and `Foyer` to build a `GMSO` topology through [`ParmEd`](https://parmed.github.io/ParmEd/html/index.html) and write out to [`LAMMPS`](https://docs.lammps.org/).
+This is an example using `mBuild` and `Foyer` to build a `GMSO` topology and write out to [`LAMMPS`](https://docs.lammps.org/).
```python
import foyer
+import forcefield_utilities as ffutils
from mbuild.lib.molecules import Ethane
-from gmso.external.convert_parmed import from_parmed
+from gmso.external.convert_mbuild import from_mbuild
+from gmso.parameterization import apply
from gmso.formats.lammpsdata import write_lammpsdata
# Start with a mBuild compound
mb_ethane = Ethane()
-oplsaa = foyer.Forcefield(name='oplsaa')
+oplsaa = ffutils.FoyerFFs().load('oplsaa').to_gmso_ff()
# atomtype the system with foyer, and convert the resulting structure to a topology
-typed_ethane = from_parmed(oplsaa.apply(mb_ethane))
-typed_ethane.name = 'ethane'
+gmso_ethane = from_mbuild(mb_ethane)
+apply(top=gmso_ethane,
+ forcefields=oplsaa,
+ identify_connections=True)
# Write out lammps datafile
-write_lammpsdata(typed_ethane, filename='ethane.lammps', atom_style='full')
+write_lammpsdata(gmso_ethane, filename='ethane.lammps', atom_style='full')
```
-
Introduction
------------
@@ -107,19 +110,36 @@ For full, detailed instructions, refer to the [documentation for installation](h
conda install -c conda-forge gmso
```
-### `pip` installation quickstart
-_Note: `GMSO` is not on `pypi` currently, but its dependencies are._
+### Installing from source
-```bash
-git clone https://github.com/mosdef-hub/gmso.git
+Dependencies of GMSO are listed in the files ``environment.yml`` (lightweight environment specification containing minimal dependencies) and ``environment-dev.yml`` (comprehensive environment specification including optional and testing packages for developers).
+The ``gmso`` or ``gmso-dev`` conda environments can be created with
+
+
+```.. code-block:: bash
+git clone https://github.com/mosdef-hub/gmso.git
cd gmso
-pip install -r requirements.txt
-pip install -e .
+# for gmso conda environment
+conda env create -f environment.yml
+conda activate gmso
+
+# for gmso-dev
+conda env create -f environment-dev.yml
+conda activate gmso
+
+# install a non-editable version of gmso
+pip install .
```
-`pip` quickstart will install `GMSO` in [`editable` mode](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs), which means that as you edit the source code of `GMSO` those edits will be reflected in your installation.
+### Install an editable version from source
+Once all dependencies have been installed and the ``conda`` environment has been created, the ``GMSO`` itself can be installed.
+``` code-block:: bash
+cd gmso
+conda activate gmso-dev # or gmso depending on your installation
+pip install -e .
+```
Documentation
-------------
From c687867c33c2bbb451e5980201f8a0094227120d Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Wed, 14 Dec 2022 16:11:36 -0600
Subject: [PATCH 093/141] [pre-commit.ci] pre-commit autoupdate (#704)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0)
- [github.com/pycqa/isort: 5.10.1 → 5.11.1](https://github.com/pycqa/isort/compare/5.10.1...5.11.1)
* fix small typo
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
Co-authored-by: Co Quach
---
.pre-commit-config.yaml | 4 ++--
README.md | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1fd52b097..912c1c34d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,12 +16,12 @@ repos:
- id: trailing-whitespace
exclude: 'setup.cfg'
- repo: https://github.com/psf/black
- rev: 22.10.0
+ rev: 22.12.0
hooks:
- id: black
args: [--line-length=80]
- repo: https://github.com/pycqa/isort
- rev: 5.10.1
+ rev: 5.11.1
hooks:
- id: isort
name: isort (python)
diff --git a/README.md b/README.md
index 495eeb7ad..f097a41a1 100644
--- a/README.md
+++ b/README.md
@@ -125,7 +125,7 @@ conda activate gmso
# for gmso-dev
conda env create -f environment-dev.yml
-conda activate gmso
+conda activate gmso-dev
# install a non-editable version of gmso
pip install .
From 46f3860df34fa824e3c6b34ac492454364f84029 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Wed, 4 Jan 2023 15:23:15 -0600
Subject: [PATCH 094/141] [pre-commit.ci] pre-commit autoupdate (#705)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0)
- [github.com/pycqa/isort: 5.10.1 → 5.11.1](https://github.com/pycqa/isort/compare/5.10.1...5.11.1)
* fix small typo
* [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pycqa/isort: 5.11.1 → 5.11.4](https://github.com/pycqa/isort/compare/5.11.1...5.11.4)
- [github.com/pycqa/pydocstyle: 6.1.1 → 6.2.0](https://github.com/pycqa/pydocstyle/compare/6.1.1...6.2.0)
* fix deprecated numpy call
* pin unyt to 2.9.2
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
Co-authored-by: Co Quach
---
.pre-commit-config.yaml | 4 ++--
environment-dev.yml | 2 +-
environment.yml | 2 +-
gmso/core/box.py | 2 +-
gmso/formats/xyz.py | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 912c1c34d..02ba96826 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -21,13 +21,13 @@ repos:
- id: black
args: [--line-length=80]
- repo: https://github.com/pycqa/isort
- rev: 5.11.1
+ rev: 5.11.4
hooks:
- id: isort
name: isort (python)
args: [--profile=black, --line-length=80]
- repo: https://github.com/pycqa/pydocstyle
- rev: '6.1.1'
+ rev: '6.2.0'
hooks:
- id: pydocstyle
exclude: ^(gmso/abc|gmso/core|gmso/tests/|docs/|devtools/|setup.py)
diff --git a/environment-dev.yml b/environment-dev.yml
index d8cb7aa4c..3e09cb976 100644
--- a/environment-dev.yml
+++ b/environment-dev.yml
@@ -5,7 +5,7 @@ dependencies:
- python>=3.8
- numpy
- sympy
- - unyt
+ - unyt<=2.9.2
- boltons
- lxml
- pydantic>1.8
diff --git a/environment.yml b/environment.yml
index c4d2ae172..2389d6cc6 100644
--- a/environment.yml
+++ b/environment.yml
@@ -5,7 +5,7 @@ dependencies:
- python>=3.8
- numpy
- sympy
- - unyt
+ - unyt<=2.9.2
- boltons
- lxml
- pydantic>1.8
diff --git a/gmso/core/box.py b/gmso/core/box.py
index 9eb7f1b04..20b9f843c 100644
--- a/gmso/core/box.py
+++ b/gmso/core/box.py
@@ -169,7 +169,7 @@ def _unit_vectors_from_angles(self):
# and then the xy plane, with the z-axis
box_vec = [[1, 0, 0], [cosg, sing, 0], [cosb, mat_coef_y, mat_coef_z]]
- return u.unyt_array(box_vec, u.dimensionless, dtype=np.float)
+ return u.unyt_array(box_vec, u.dimensionless, dtype=float)
def get_vectors(self):
"""Return the vectors of the box."""
diff --git a/gmso/formats/xyz.py b/gmso/formats/xyz.py
index 44cc7348c..e62d8d514 100644
--- a/gmso/formats/xyz.py
+++ b/gmso/formats/xyz.py
@@ -40,7 +40,7 @@ def read_xyz(filename):
"were expected, but at least one fewer was found."
)
raise ValueError(msg.format(n_atoms))
- tmp = np.array(line[1:4], dtype=np.float) * u.angstrom
+ tmp = np.array(line[1:4], dtype=float) * u.angstrom
coords[row] = tmp.in_units(u.nanometer)
site = Atom(name=line[0], position=coords[row])
top.add_site(site)
From 0ba3e94c9abbad2b9406fd4573c58706620ca24a Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 10 Jan 2023 10:00:25 -0600
Subject: [PATCH 095/141] [pre-commit.ci] pre-commit autoupdate (#707)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pycqa/pydocstyle: 6.2.0 → 6.2.3](https://github.com/pycqa/pydocstyle/compare/6.2.0...6.2.3)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 02ba96826..843025202 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,7 +27,7 @@ repos:
name: isort (python)
args: [--profile=black, --line-length=80]
- repo: https://github.com/pycqa/pydocstyle
- rev: '6.2.0'
+ rev: '6.2.3'
hooks:
- id: pydocstyle
exclude: ^(gmso/abc|gmso/core|gmso/tests/|docs/|devtools/|setup.py)
From 2f4be46c0bd5118edc4830dd7c59fbe2f554b793 Mon Sep 17 00:00:00 2001
From: Brad Crawford <65550266+bc118@users.noreply.github.com>
Date: Wed, 11 Jan 2023 17:25:49 -0500
Subject: [PATCH 096/141] gmso xml equation compare and scaling, and extracting
specific data from FFs (#698)
* gmso xml equation compare and scaling, and extracting specific data from each force field on a per molecule/residue basis.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* changed sympy code sections to be compatible with sympy 1.11
* added some test cases to specific_ff_to_residue, but still a failing case.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Fixes for element handling in convert_mbuild and atomtyping sites that look like elements but don't have the element information included
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* added tests for equation compare and specific_ff_to_residue
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* cleaned up specific_ff_to_residue.py
* cleaned up specific_ff_to_residue.py
* cleaned up equation_compare.py
* cleaned up equation_compare.py
* cleaned up equation_compare.py
* cleaned up equation_compare.py
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* cleaned up equation_compare.py
* cleaned up equation_compare.py
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* cleaned up equation_compare.py and specific_eqn_to_ff
* cleaned up equation_compare.py and specific_eqn_to_ff
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* fix xml test related to newly added XML
* Clean up print statement, fix SMARTS string of benzene C
* Apply suggestions from code review
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* some code refactor
* fix typo
* minor edits
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: CalCraven
Co-authored-by: Co Quach <43968221+daico007@users.noreply.github.com>
Co-authored-by: Co Quach
---
gmso/core/forcefield.py | 6 +-
gmso/external/convert_mbuild.py | 18 +-
gmso/parameterization/foyer_utils.py | 3 +-
gmso/tests/base_test.py | 20 +
gmso/tests/test_equation_compare.py | 482 +++++++++++
gmso/tests/test_specific_ff_to_residue.py | 604 +++++++++++++
gmso/tests/test_xml_handling.py | 10 +-
gmso/utils/equation_compare.py | 708 +++++++++++++++
gmso/utils/files/benzene_aa.mol2 | 39 +
gmso/utils/files/ethane_ua.mol2 | 18 +
gmso/utils/files/ethane_ua_mie.mol2 | 18 +
.../gmso_xmls/test_ffstyles/benzene_GAFF.xml | 85 ++
.../test_ffstyles/benzene_trappe-ua.xml | 2 +-
.../gmso_xmls/test_ffstyles/charmm36.xml | 15 +-
...thane_propane_ua_Mie_lorentz_combining.xml | 55 ++
...e_propane_ua_bad_eqn_lorentz_combining.xml | 55 ++
.../ethane_propane_ua_lorentz_combining.xml | 49 ++
.../test_ffstyles/opls_charmm_buck.xml | 2 +-
.../test_ffstyles/oplsaa_from_foyer.xml | 2 +-
.../files/gmso_xmls/test_ffstyles/spce.xml | 2 +-
.../spce_water__geometric_combining.xml | 42 +
.../spce_water__lorentz_combining.xml | 42 +
.../files/gmso_xmls/test_ffstyles/tip3p.xml | 2 +-
.../gmso_xmls/test_ffstyles/tip4p_2005.xml | 2 +-
.../gmso_xmls/test_ffstyles/tip4p_ew.xml | 2 +-
.../gmso_xmls/test_molecules/alkanes.xml | 2 +-
.../gmso_xmls/test_molecules/alkenes.xml | 2 +-
.../gmso_xmls/test_molecules/alkynes.xml | 2 +-
.../files/gmso_xmls/test_molecules/carbon.xml | 2 +-
.../ang_kcal_coulomb_gram_degree_mol.xml | 2 +-
.../nm_kj_electroncharge_amu_rad_mol.xml | 2 +-
gmso/utils/specific_ff_to_residue.py | 816 ++++++++++++++++++
32 files changed, 3085 insertions(+), 26 deletions(-)
create mode 100644 gmso/tests/test_equation_compare.py
create mode 100644 gmso/tests/test_specific_ff_to_residue.py
create mode 100644 gmso/utils/equation_compare.py
create mode 100644 gmso/utils/files/benzene_aa.mol2
create mode 100644 gmso/utils/files/ethane_ua.mol2
create mode 100644 gmso/utils/files/ethane_ua_mie.mol2
create mode 100755 gmso/utils/files/gmso_xmls/test_ffstyles/benzene_GAFF.xml
create mode 100755 gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_Mie_lorentz_combining.xml
create mode 100755 gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_bad_eqn_lorentz_combining.xml
create mode 100755 gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_lorentz_combining.xml
create mode 100755 gmso/utils/files/gmso_xmls/test_ffstyles/spce_water__geometric_combining.xml
create mode 100755 gmso/utils/files/gmso_xmls/test_ffstyles/spce_water__lorentz_combining.xml
create mode 100644 gmso/utils/specific_ff_to_residue.py
diff --git a/gmso/core/forcefield.py b/gmso/core/forcefield.py
index 441024948..032687a59 100644
--- a/gmso/core/forcefield.py
+++ b/gmso/core/forcefield.py
@@ -613,14 +613,16 @@ def _xml_from_gmso(self, filename, overwrite=False):
)
metadata = etree.SubElement(ff_el, "FFMetaData")
- if not self.scaling_factors.get("electrostatics14Scale") is None:
+ if self.scaling_factors.get("electrostatics14Scale") is not None:
metadata.attrib["electrostatics14Scale"] = str(
self.scaling_factors.get("electrostatics14Scale")
)
- if not self.scaling_factors.get("nonBonded14Scale") is None:
+ if self.scaling_factors.get("nonBonded14Scale") is not None:
metadata.attrib["nonBonded14Scale"] = str(
self.scaling_factors.get("nonBonded14Scale")
)
+ if self.combining_rule is not None:
+ metadata.attrib["combiningRule"] = str(self.combining_rule)
# ToDo: ParameterUnitsDefintions and DefaultUnits
if self.units:
diff --git a/gmso/external/convert_mbuild.py b/gmso/external/convert_mbuild.py
index d6dc4f5bc..af8cf9517 100644
--- a/gmso/external/convert_mbuild.py
+++ b/gmso/external/convert_mbuild.py
@@ -28,6 +28,7 @@ def from_mbuild(
search_method=element_by_symbol,
parse_label=True,
custom_groups=None,
+ infer_elements=False,
):
"""Convert an mbuild.Compound to a gmso.Topology.
@@ -75,6 +76,9 @@ def from_mbuild(
matching name on the way down from compound.children. Only the first match
while moving downwards will be assigned to the site. If parse_label=False,
this argument does nothing.
+ infer_elements : bool, default=False
+ Allows the reader to try to load element info from the mbuild Particle.name
+ instead of only from the populated Particle.element
Returns
-------
@@ -104,7 +108,9 @@ def from_mbuild(
# Use site map to apply Compound info to Topology.
for part in compound.particles():
- site = _parse_site(site_map, part, search_method)
+ site = _parse_site(
+ site_map, part, search_method, infer_element=infer_elements
+ )
top.add_site(site)
for b1, b2 in compound.bonds():
@@ -247,10 +253,14 @@ def _parse_particle(particle_map, site):
return particle
-def _parse_site(site_map, particle, search_method):
- """Parse information for a gmso.Site from a mBuild.Compound adn add it to the site map."""
+def _parse_site(site_map, particle, search_method, infer_element=False):
+ """Parse information for a gmso.Site from a mBuild.Compound and add it to the site map."""
pos = particle.xyz[0] * u.nm
- ele = search_method(particle.element.symbol) if particle.element else None
+ if particle.element:
+ ele = search_method(particle.element.symbol)
+ else:
+ ele = search_method(particle.name) if infer_element else None
+
charge = particle.charge * u.elementary_charge if particle.charge else None
mass = particle.mass * u.amu if particle.mass else None
diff --git a/gmso/parameterization/foyer_utils.py b/gmso/parameterization/foyer_utils.py
index 9f5c5745a..1552f33ba 100644
--- a/gmso/parameterization/foyer_utils.py
+++ b/gmso/parameterization/foyer_utils.py
@@ -60,7 +60,7 @@ def get_topology_graph(
if atomdata_populator
else {}
)
- if atom.name.startswith("_"):
+ if atom.name.startswith("_") or not atom.element:
top_graph.add_atom(
name=atom.name,
index=j, # Assumes order is preserved
@@ -70,7 +70,6 @@ def get_topology_graph(
molecule=atom.molecule.name if atom.molecule else None,
**kwargs,
)
-
else:
top_graph.add_atom(
name=atom.name,
diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py
index 1c0fe1f5d..6f724b732 100644
--- a/gmso/tests/base_test.py
+++ b/gmso/tests/base_test.py
@@ -634,3 +634,23 @@ def hierarchical_top(self, hierarchical_compound):
top = from_mbuild(hierarchical_compound) # Create GMSO topology
top.identify_connections()
return top
+
+ @pytest.fixture
+ def ethane_gomc(self):
+ ethane_gomc = mb.load("CC", smiles=True)
+ ethane_gomc.name = "ETH"
+
+ return ethane_gomc
+
+ @pytest.fixture
+ def ethanol_gomc(self):
+ ethanol_gomc = mb.load("CCO", smiles=True)
+ ethanol_gomc.name = "ETO"
+
+ return ethanol_gomc
+
+ @pytest.fixture
+ def methane_ua_gomc(self):
+ methane_ua_gomc = mb.Compound(name="_CH4")
+
+ return methane_ua_gomc
diff --git a/gmso/tests/test_equation_compare.py b/gmso/tests/test_equation_compare.py
new file mode 100644
index 000000000..975c97c9e
--- /dev/null
+++ b/gmso/tests/test_equation_compare.py
@@ -0,0 +1,482 @@
+import mbuild as mb
+import pytest
+from mbuild.utils.io import has_foyer
+
+from gmso.tests.base_test import BaseTest
+from gmso.utils.equation_compare import (
+ evaluate_harmonic_angle_format_with_scaler,
+ evaluate_harmonic_bond_format_with_scaler,
+ evaluate_harmonic_improper_format_with_scaler,
+ evaluate_harmonic_torsion_format_with_scaler,
+ evaluate_nonbonded_exp6_format_with_scaler,
+ evaluate_nonbonded_lj_format_with_scaler,
+ evaluate_nonbonded_mie_format_with_scaler,
+ evaluate_OPLS_torsion_format_with_scaler,
+ evaluate_periodic_improper_format_with_scaler,
+ evaluate_periodic_torsion_format_with_scaler,
+ evaluate_RB_torsion_format_with_scaler,
+ get_atom_type_expressions_and_scalars,
+)
+from gmso.utils.io import get_fn
+from gmso.utils.specific_ff_to_residue import specific_ff_to_residue
+
+# base forms
+input_base_lj_form = "4*epsilon * ((sigma/r)**12 - (sigma/r)**6)"
+input_base_mie_form = (
+ "(n/(n-m)) * (n/m)**(m/(n-m)) * epsilon * ((sigma/r)**n - (sigma/r)**m)"
+)
+input_base_exp6_form = (
+ "epsilon*alpha/(alpha-6) * (6/alpha*exp(alpha*(1-r/Rmin)) - (Rmin/r)**6)"
+)
+
+# main_output_types
+lj_output = "LJ"
+mie_output = "Mie"
+exp6_output = "Exp6"
+
+
+@pytest.mark.skipif(not has_foyer, reason="Foyer package not installed")
+class TestEqnCompare(BaseTest):
+ def test_wrong_lj_equation_form(self):
+ input_new_lj_form = "x"
+
+ [form_output, form_scalar] = evaluate_nonbonded_lj_format_with_scaler(
+ input_new_lj_form, input_base_lj_form
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_lj_equation_form(self):
+ input_new_lj_form = "8*epsilon * ((sigma/r)**12 - (sigma/r)**6)"
+
+ [form_output, form_scalar] = evaluate_nonbonded_lj_format_with_scaler(
+ input_new_lj_form, input_base_lj_form
+ )
+
+ assert form_output == lj_output
+ assert form_scalar == 2.0
+
+ def test_wrong_mie_equation_form(self):
+ input_new_mie_form = "x"
+
+ [form_output, form_scalar] = evaluate_nonbonded_mie_format_with_scaler(
+ input_new_mie_form, input_base_mie_form
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_mie_equation_form(self):
+ input_new_mie_form = "2*(n/(n-m)) * (n/m)**(m/(n-m)) * epsilon * ((sigma/r)**n - (sigma/r)**m)"
+
+ [form_output, form_scalar] = evaluate_nonbonded_mie_format_with_scaler(
+ input_new_mie_form, input_base_mie_form
+ )
+
+ assert form_output == mie_output
+ assert form_scalar == 2.0
+
+ def test_wrong_exp6_equation_form(self):
+ input_new_exp6_form = "x"
+
+ [form_output, form_scalar] = evaluate_nonbonded_exp6_format_with_scaler(
+ input_new_exp6_form, input_base_exp6_form
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_exp6_equation_form(self):
+ input_new_exp6_form = "2*epsilon*alpha/(alpha-6) * (6/alpha*exp(alpha*(1-r/Rmin)) - (Rmin/r)**6)"
+
+ [form_output, form_scalar] = evaluate_nonbonded_exp6_format_with_scaler(
+ input_new_exp6_form, input_base_exp6_form
+ )
+
+ assert form_output == exp6_output
+ assert form_scalar == 2.0
+
+ # the exp6 needs added when the standard exp6 is added to gmso overall.
+ def test_find_lj_mie_exp6_forms_and_scalars(self):
+ ethane_lj = mb.load(get_fn("ethane_ua.mol2"))
+ ethane_lj.name = "ETH"
+ ethane_mie = mb.load(get_fn("ethane_ua_mie.mol2"))
+ ethane_mie.name = "ETHM"
+ test_box = mb.fill_box(
+ compound=[ethane_lj, ethane_mie], n_compounds=[1, 1], box=[4, 4, 4]
+ )
+
+ [
+ test_topology,
+ test_residues_applied_list,
+ test_electrostatics14Scale_dict,
+ test_nonBonded14Scale_dict,
+ test_atom_types_dict,
+ test_bond_types_dict,
+ test_angle_types_dict,
+ test_dihedral_types_dict,
+ test_improper_types_dict,
+ test_combining_rule_dict,
+ ] = specific_ff_to_residue(
+ test_box,
+ forcefield_selection={
+ "ETH": f"{get_fn('gmso_xmls/test_ffstyles/ethane_propane_ua_lorentz_combining.xml')}",
+ "ETHM": f"{get_fn('gmso_xmls/test_ffstyles/ethane_propane_ua_Mie_lorentz_combining.xml')}",
+ },
+ residues=["ETH", "ETHM"],
+ boxes_for_simulation=1,
+ )
+
+ atom_types_data_expression_data_dict = (
+ get_atom_type_expressions_and_scalars(test_atom_types_dict)
+ )
+
+ assert (
+ str(atom_types_data_expression_data_dict["ETH_CH3"]["expression"])
+ == "4*epsilon*(-sigma**6/r**6 + sigma**12/r**12)"
+ )
+ assert (
+ atom_types_data_expression_data_dict["ETH_CH3"]["expression_form"]
+ == "LJ"
+ )
+ assert (
+ atom_types_data_expression_data_dict["ETH_CH3"]["expression_scalar"]
+ == 1.0
+ )
+
+ assert (
+ str(atom_types_data_expression_data_dict["ETHM_CH3"]["expression"])
+ == "epsilon*n*(n/m)**(m/(-m + n))*(-(sigma/r)**m + (sigma/r)**n)/(-m + n)"
+ )
+ assert (
+ atom_types_data_expression_data_dict["ETHM_CH3"]["expression_form"]
+ == "Mie"
+ )
+ assert (
+ atom_types_data_expression_data_dict["ETHM_CH3"][
+ "expression_scalar"
+ ]
+ == 1.0
+ )
+
+ # the exp6 needs added when the standard exp6 is added to gmso overall.
+ def test_bad_eqn_for_find_lj_mie_exp6_forms_and_scalars(self):
+ with pytest.raises(
+ ValueError,
+ match=r"ERROR: the ETHM_CH3 residue does not match the listed standard or scaled "
+ r"LJ, Mie, or Exp6 expressions in the get_atom_type_expressions_and_scalars function.",
+ ):
+ ethane_lj = mb.load(get_fn("ethane_ua.mol2"))
+ ethane_lj.name = "ETH"
+ ethane_mie = mb.load(get_fn("ethane_ua_mie.mol2"))
+ ethane_mie.name = "ETHM"
+ test_box = mb.fill_box(
+ compound=[ethane_lj, ethane_mie],
+ n_compounds=[1, 1],
+ box=[4, 4, 4],
+ )
+
+ [
+ test_topology,
+ test_residues_applied_list,
+ test_electrostatics14Scale_dict,
+ test_nonBonded14Scale_dict,
+ test_atom_types_dict,
+ test_bond_types_dict,
+ test_angle_types_dict,
+ test_dihedral_types_dict,
+ test_improper_types_dict,
+ test_combining_rule_dict,
+ ] = specific_ff_to_residue(
+ test_box,
+ forcefield_selection={
+ "ETH": f"{get_fn('gmso_xmls/test_ffstyles/ethane_propane_ua_lorentz_combining.xml')}",
+ "ETHM": f"{get_fn('gmso_xmls/test_ffstyles/ethane_propane_ua_bad_eqn_lorentz_combining.xml')}",
+ },
+ residues=["ETH", "ETHM"],
+ boxes_for_simulation=1,
+ )
+
+ atom_types_data_expression_data_dict = (
+ get_atom_type_expressions_and_scalars(test_atom_types_dict)
+ )
+
+ # harmonic bond
+ def test_wrong_harmonic_bond(self):
+ input_base_harmonic_bond = "1 * k * (r-r_eq)**2"
+ input_new_harmonic_form = "x"
+
+ [form_output, form_scalar] = evaluate_harmonic_bond_format_with_scaler(
+ input_new_harmonic_form, input_base_harmonic_bond
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_harmonic_bond(self):
+ # bond types
+ input_base_harmonic_bond = "1 * k * (r-r_eq)**2"
+ input_new_harmonic_form = "1/2 * k * (r-r_eq)**2"
+
+ [form_output, form_scalar] = evaluate_harmonic_bond_format_with_scaler(
+ input_new_harmonic_form, input_base_harmonic_bond
+ )
+
+ assert form_output == "HarmonicBondPotential"
+ assert form_scalar == 0.5
+
+ # harmonic angle
+ def test_wrong_harmonic_angle(self):
+ input_base_harmonic_angle = "1 * k * (theta - theta_eq)**2"
+ input_new_harmonic_form = "x"
+
+ [form_output, form_scalar] = evaluate_harmonic_angle_format_with_scaler(
+ input_new_harmonic_form, input_base_harmonic_angle
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_harmonic_angle(self):
+ # angle types
+ input_base_harmonic_angle = "1 * k * (theta - theta_eq)**2"
+ input_new_harmonic_form = "1/2 * k * (theta - theta_eq)**2"
+
+ [form_output, form_scalar] = evaluate_harmonic_angle_format_with_scaler(
+ input_new_harmonic_form, input_base_harmonic_angle
+ )
+
+ assert form_output == "HarmonicAnglePotential"
+ assert form_scalar == 0.5
+
+ # harmonic torsion
+ def test_wrong_harmonic_torsion(self):
+ input_base_harmonic_torsion = "1 * k * (phi - phi_eq)**2"
+ input_new_harmonic_form = "x"
+
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_harmonic_torsion_format_with_scaler(
+ input_new_harmonic_form, input_base_harmonic_torsion
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_harmonic_torsion(self):
+ # torsion types
+ input_base_harmonic_torsion = "1 * k * (phi - phi_eq)**2"
+ input_new_harmonic_form = "1/2 * k * (phi - phi_eq)**2"
+
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_harmonic_torsion_format_with_scaler(
+ input_new_harmonic_form, input_base_harmonic_torsion
+ )
+
+ assert form_output == "HarmonicTorsionPotential"
+ assert form_scalar == 0.5
+
+ # OPLS torsion
+ def test_wrong_opls_torsion(self):
+ input_base_opls_torsion = (
+ "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))"
+ )
+ input_new_opls_form = "x"
+
+ [form_output, form_scalar] = evaluate_OPLS_torsion_format_with_scaler(
+ input_new_opls_form, input_base_opls_torsion
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_opls_torsion(self):
+ # torsion types
+ input_base_opls_torsion = (
+ "1/2 * k0 + "
+ "1/2 * k1 * (1 + cos(phi)) + "
+ "1/2 * k2 * (1 - cos(2*phi)) + "
+ "1/2 * k3 * (1 + cos(3*phi)) + "
+ "1/2 * k4 * (1 - cos(4*phi))"
+ )
+
+ input_new_opls_form = (
+ "2 * k0 + "
+ "2 * k1 * (1 + cos(phi)) + "
+ "2 * k2 * (1 - cos(2*phi)) + "
+ "2 * k3 * (1 + cos(3*phi)) + "
+ "2 * k4 * (1 - cos(4*phi))"
+ )
+
+ [form_output, form_scalar] = evaluate_OPLS_torsion_format_with_scaler(
+ input_new_opls_form, input_base_opls_torsion
+ )
+
+ assert form_output == "OPLSTorsionPotential"
+ assert form_scalar == 4
+
+ # periodic torsion
+ def test_wrong_periodic_torsion(self):
+ input_base_periodic_torsion = "k * (1 + cos(n * phi - phi_eq))"
+ input_new_periodic_form = "x"
+
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_periodic_torsion_format_with_scaler(
+ input_new_periodic_form, input_base_periodic_torsion
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_periodic_torsion(self):
+ # torsion types
+ input_base_periodic_torsion = "k * (1 + cos(n * phi - phi_eq))"
+
+ input_new_periodic_form = "2 * k * (1 + cos(n * phi - phi_eq))"
+
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_periodic_torsion_format_with_scaler(
+ input_new_periodic_form, input_base_periodic_torsion
+ )
+
+ assert form_output == "PeriodicTorsionPotential"
+ assert form_scalar == 2
+
+ # RB torsion
+ def test_wrong_RB_torsion(self):
+ input_base_RB_torsion = (
+ "c0 * cos(phi)**0 + "
+ "c1 * cos(phi)**1 + "
+ "c2 * cos(phi)**2 + "
+ "c3 * cos(phi)**3 + "
+ "c4 * cos(phi)**4 + "
+ "c5 * cos(phi)**5"
+ )
+ input_new_RB_form = "x"
+
+ [form_output, form_scalar] = evaluate_RB_torsion_format_with_scaler(
+ input_new_RB_form, input_base_RB_torsion
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_RB_torsion(self):
+ # torsion types
+ input_base_RB_torsion = (
+ "c0 * cos(phi)**0 + "
+ "c1 * cos(phi)**1 + "
+ "c2 * cos(phi)**2 + "
+ "c3 * cos(phi)**3 + "
+ "c4 * cos(phi)**4 + "
+ "c5 * cos(phi)**5"
+ )
+
+ input_new_RB_form = (
+ "2 * c0 * cos(phi)**0 + "
+ "2 * c1 * cos(phi)**1 + "
+ "2 * c2 * cos(phi)**2 + "
+ "2 * c3 * cos(phi)**3 + "
+ "2 * c4 * cos(phi)**4 + "
+ "2 * c5 * cos(phi)**5"
+ )
+
+ [form_output, form_scalar] = evaluate_RB_torsion_format_with_scaler(
+ input_new_RB_form, input_base_RB_torsion
+ )
+
+ assert form_output == "RyckaertBellemansTorsionPotential"
+ assert form_scalar == 2
+
+ # RB torsion
+ def test_wrong_RB_torsion(self):
+ input_base_harmonic_improper = "k * (phi - phi_eq)**2"
+
+ input_new_harmonic_improper_form = "x"
+
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_harmonic_improper_format_with_scaler(
+ input_new_harmonic_improper_form,
+ input_base_harmonic_improper,
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ # scaled_harmonic_improper
+ def test_harmonic_improper(self):
+ input_base_harmonic_improper = "k * (phi - phi_eq)**2"
+ input_new_harmonic_improper_form = "x"
+
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_harmonic_improper_format_with_scaler(
+ input_new_harmonic_improper_form,
+ input_base_harmonic_improper,
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_harmonic_improper(self):
+ # torsion types
+ input_base_harmonic_improper = "k * (phi - phi_eq)**2"
+ input_new_harmonic_improper_form = "2 * k * (phi - phi_eq)**2"
+
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_harmonic_improper_format_with_scaler(
+ input_new_harmonic_improper_form,
+ input_base_harmonic_improper,
+ )
+
+ assert form_output == "HarmonicImproperPotential"
+ assert form_scalar == 2
+
+ # scaled_periodic_improper
+ def test_periodic_improper(self):
+ input_base_periodic_improper = "k * (1 + cos(n * phi - phi_eq))"
+ input_new_periodic_improper_form = "x"
+
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_periodic_improper_format_with_scaler(
+ input_new_periodic_improper_form,
+ input_base_periodic_improper,
+ )
+
+ assert form_output == None
+ assert form_scalar == None
+
+ def test_scaled_periodic_improper(self):
+ # periodic types
+ input_base_periodic_improper = "k * (1 + cos(n * phi - phi_eq))"
+ input_new_periodic_improper_form = "2* k * (1 + cos(n * phi - phi_eq))"
+
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_periodic_improper_format_with_scaler(
+ input_new_periodic_improper_form,
+ input_base_periodic_improper,
+ )
+
+ assert form_output == "PeriodicImproperPotential"
+ assert form_scalar == 2
diff --git a/gmso/tests/test_specific_ff_to_residue.py b/gmso/tests/test_specific_ff_to_residue.py
new file mode 100644
index 000000000..e887f5687
--- /dev/null
+++ b/gmso/tests/test_specific_ff_to_residue.py
@@ -0,0 +1,604 @@
+import mbuild as mb
+import pytest
+from foyer.forcefields import forcefields
+from mbuild import Box, Compound
+from mbuild.utils.io import has_foyer
+
+from gmso.exceptions import GMSOError
+from gmso.tests.base_test import BaseTest
+from gmso.utils.io import get_fn
+from gmso.utils.specific_ff_to_residue import specific_ff_to_residue
+
+
+@pytest.mark.skipif(not has_foyer, reason="Foyer package not installed")
+class TestSpecificFFToResidue(BaseTest):
+ # Tests for the mbuild.utils.specific_FF_to_residue.Specific_FF_to_residue() function
+ def test_specific_ff_ff_is_none(self, ethane_gomc):
+ with pytest.raises(
+ TypeError,
+ match=r"Please the force field selection \(forcefield_selection\) as a "
+ r"dictionary with all the residues specified to a force field "
+ '-> Ex: {"Water": "oplsaa", "OCT": "path/trappe-ua.xml"}, '
+ "Note: the file path must be specified the force field file "
+ "or by using the standard force field name provided the `foyer` package.",
+ ):
+ box_0 = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 4, 4]
+ )
+
+ specific_ff_to_residue(
+ box_0,
+ forcefield_selection=None,
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_wrong_ff_extension(self, ethane_gomc):
+ with pytest.raises(
+ ValueError,
+ match="Please make sure you are enterning the correct FF name or path with xml extension",
+ ):
+ box_0 = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 4, 4]
+ )
+
+ specific_ff_to_residue(
+ box_0,
+ forcefield_selection={ethane_gomc.name: "oplsaa.pdb"},
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_all_residue_not_input(self, ethane_gomc, ethanol_gomc):
+ with pytest.raises(
+ GMSOError,
+ match=f"A particle named C cannot be associated with the\n "
+ f"custom_groups \['ETH'\]. "
+ f"Be sure to specify a list of group names that will cover\n "
+ f"all particles in the compound. This particle is one level below ETO.",
+ ):
+ box = mb.fill_box(
+ compound=[ethane_gomc, ethanol_gomc],
+ box=[1, 1, 1],
+ n_compounds=[1, 1],
+ )
+
+ specific_ff_to_residue(
+ box,
+ forcefield_selection={ethane_gomc.name: "oplsaa"},
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=2,
+ )
+
+ def test_specific_ff_to_residue_ff_selection_not_dict(self, ethane_gomc):
+ with pytest.raises(
+ TypeError,
+ match=r"The force field selection \(forcefield_selection\) "
+ "is not a dictionary. Please enter a dictionary "
+ "with all the residues specified to a force field "
+ '-> Ex: {"Water": "oplsaa", "OCT": "path/trappe-ua.xml"}, '
+ "Note: the file path must be specified the force field file "
+ "or by using the standard force field name provided the `foyer` package.",
+ ):
+ box_0 = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 4, 4]
+ )
+
+ specific_ff_to_residue(
+ box_0,
+ forcefield_selection="oplsaa",
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_to_residue_is_none(self, ethane_gomc):
+ with pytest.raises(
+ TypeError,
+ match=r"Please enter the residues list in the specific_ff_to_residue.",
+ ):
+ box_0 = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 4, 4]
+ )
+
+ specific_ff_to_residue(
+ box_0,
+ forcefield_selection={ethane_gomc.name: "oplsaa"},
+ residues=None,
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_to_simulation_boxes_not_1_or_2(self, ethane_gomc):
+ with pytest.raises(
+ ValueError,
+ match=r"boxes_for_simulation must be either 1 or 2",
+ ):
+ test_box_ethane_gomc = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[2, 3, 4]
+ )
+
+ specific_ff_to_residue(
+ test_box_ethane_gomc,
+ forcefield_selection={ethane_gomc.name: "oplsaa"},
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=3,
+ )
+
+ def test_specific_ff_to_residue_ffselection_wrong_path(self, ethane_gomc):
+ with pytest.raises(
+ ValueError,
+ # match=r"FileNotFoundError: \[Errno 2\] No such file or directory: 'oplsaa.xml'"
+ match=r"Please make sure you are entering the correct foyer FF path, "
+ r"including the FF file name.xml. "
+ r"If you are using the pre-build FF files in foyer, "
+ r"only use the string name without any extension. "
+ r"The selected FF file could also could not formated properly, "
+ r"or there may be errors in the FF file itself.",
+ ):
+ test_box_ethane_gomc = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 5, 6]
+ )
+
+ specific_ff_to_residue(
+ test_box_ethane_gomc,
+ forcefield_selection={ethane_gomc.name: "oplsaa.xml"},
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_wrong_path(self, ethane_gomc):
+ with pytest.raises(
+ ValueError,
+ match=r"Please make sure you are entering the correct foyer FF path, "
+ r"including the FF file name.xml. "
+ r"If you are using the pre-build FF files in foyer, "
+ r"only use the string name without any extension. "
+ r"The selected FF file could also could not formated properly, "
+ r"or there may be errors in the FF file itself.",
+ ):
+ box_0 = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 4, 4]
+ )
+
+ specific_ff_to_residue(
+ box_0,
+ forcefield_selection={ethane_gomc.name: "oplsaa.xml"},
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_to_residue_input_string_as_compound(self, ethane_gomc):
+ with pytest.raises(
+ TypeError,
+ match=r"The structure expected to be of type: "
+ r" or , "
+ r"received: ",
+ ):
+ specific_ff_to_residue(
+ "ethane_gomc",
+ forcefield_selection={ethane_gomc.name: "oplsaa"},
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_to_residue_boxes_for_simulation_not_int(
+ self, ethane_gomc
+ ):
+ with pytest.raises(
+ ValueError, match=r"boxes_for_simulation must be either 1 or 2."
+ ):
+ box_0 = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 4, 4]
+ )
+
+ specific_ff_to_residue(
+ box_0,
+ forcefield_selection={ethane_gomc.name: "oplsaa"},
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1.1,
+ )
+
+ def test_specific_ff_to_residues_no_ff(self, ethane_gomc):
+ with pytest.raises(
+ ValueError,
+ match="The forcefield_selection variable are not provided, but there are residues provided.",
+ ):
+ box_0 = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 4, 4]
+ )
+
+ specific_ff_to_residue(
+ box_0,
+ forcefield_selection={},
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_to_no_residues(self, ethane_gomc):
+ with pytest.raises(
+ ValueError,
+ match=r"The residues variable is an empty list but there are "
+ "forcefield_selection variables provided.",
+ ):
+ box_0 = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 4, 4]
+ )
+
+ specific_ff_to_residue(
+ box_0,
+ forcefield_selection={ethane_gomc.name: "oplsaa"},
+ residues=[],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_wrong_foyer_name(self, ethane_gomc):
+ with pytest.raises(
+ ValueError,
+ match=r"Please make sure you are entering the correct foyer FF name, "
+ r"or the correct file extension \(i.e., .xml, if required\).",
+ ):
+ box_0 = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 4, 4]
+ )
+
+ specific_ff_to_residue(
+ box_0,
+ forcefield_selection={ethane_gomc.name: "xxx"},
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_to_residue_ff_selection_run(self, ethane_gomc):
+ test_box_ethane_gomc = mb.fill_box(
+ compound=[ethane_gomc], n_compounds=[1], box=[4, 5, 6]
+ )
+
+ [
+ test_topology,
+ test_residues_applied_list,
+ test_electrostatics14Scale_dict,
+ test_nonBonded14Scale_dict,
+ test_atom_types_dict,
+ test_bond_types_dict,
+ test_angle_types_dict,
+ test_dihedral_types_dict,
+ test_improper_types_dict,
+ test_combining_rule_dict,
+ ] = specific_ff_to_residue(
+ test_box_ethane_gomc,
+ forcefield_selection={
+ ethane_gomc.name: f"{forcefields.get_ff_path()[0]}/xml/oplsaa.xml"
+ },
+ residues=[ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+ assert test_electrostatics14Scale_dict == {"ETH": 0.5}
+ assert test_nonBonded14Scale_dict == {"ETH": 0.5}
+ assert test_residues_applied_list == ["ETH"]
+
+ def test_specific_ff_to_no_atoms_no_box_dims_in_residue(self):
+ with pytest.raises(
+ TypeError,
+ match=f"The structure, {mb.Compound} or {mb.Box}, needs to have have box lengths and angles.",
+ ):
+ empty_compound = mb.Compound()
+
+ specific_ff_to_residue(
+ empty_compound,
+ forcefield_selection={"empty_compound": "oplsaa"},
+ residues=["empty_compound"],
+ boxes_for_simulation=1,
+ )
+
+ def test_specific_ff_to_no_atoms_in_residue(self):
+ with pytest.raises(
+ ValueError,
+ match=r"The residues variable is an empty list but there "
+ r"are forcefield_selection variables provided.",
+ ):
+ empty_compound = mb.Compound()
+ empty_compound.box = Box([4, 4, 4])
+
+ specific_ff_to_residue(
+ empty_compound,
+ forcefield_selection={"empty_compound": "oplsaa"},
+ residues=[],
+ boxes_for_simulation=1,
+ )
+
+ # this test is not usable until the individual mb.Compounds can be force fielded in MosDeF-GOMC
+
+ def test_charmm_empty_compound_test_no_children(self, methane_ua_gomc):
+ empty_box = mb.Compound()
+ empty_box.box = mb.Box(lengths=[4, 4, 4])
+
+ with pytest.raises(
+ TypeError,
+ match=r"If you are not providing an empty box, "
+ r"you need to specify the atoms/beads as children in the mb.Compound. "
+ r"If you are providing and empty box, please do so by specifying and "
+ r"mbuild Box \({}\)".format(type(Box(lengths=[1, 1, 1]))),
+ ):
+
+ specific_ff_to_residue(
+ empty_box,
+ forcefield_selection={"AAA": "trappe-ua"},
+ residues=["AAA"],
+ boxes_for_simulation=1,
+ )
+
+ def test_charmm_a_few_mbuild_layers(self, ethane_gomc, ethanol_gomc):
+ box_reservior_1 = mb.fill_box(
+ compound=[ethane_gomc], box=[1, 1, 1], n_compounds=[1]
+ )
+ box_reservior_1.name = ethane_gomc.name
+ box_reservior_1.periodicity = (True, True, True)
+ box_reservior_2 = mb.fill_box(
+ compound=[ethanol_gomc], box=[1, 1, 1], n_compounds=[1]
+ )
+ box_reservior_2.name = ethanol_gomc.name
+ box_reservior_2.translate([0, 0, 1])
+
+ box_reservior_3 = mb.Compound()
+ box_reservior_3.name = "A"
+ box_reservior_3.box = Box(lengths=[3, 3, 3])
+ box_reservior_3.add(box_reservior_1, inherit_periodicity=False)
+ box_reservior_3.add(box_reservior_2, inherit_periodicity=False)
+
+ [
+ test_topology,
+ test_residues_applied_list,
+ test_electrostatics14Scale_dict,
+ test_nonBonded14Scale_dict,
+ test_atom_types_dict,
+ test_bond_types_dict,
+ test_angle_types_dict,
+ test_dihedral_types_dict,
+ test_improper_types_dict,
+ test_combining_rule_dict,
+ ] = specific_ff_to_residue(
+ box_reservior_3,
+ forcefield_selection={
+ ethanol_gomc.name: "oplsaa",
+ ethane_gomc.name: "oplsaa",
+ # box_reservior_3.name: "oplsaa",
+ },
+ residues=[ethanol_gomc.name, ethane_gomc.name],
+ # residues=[box_reservior_3.name],
+ boxes_for_simulation=1,
+ )
+
+ assert test_topology.n_sites == 17
+ assert test_electrostatics14Scale_dict == {"ETO": 0.5, "ETH": 0.5}
+ assert test_nonBonded14Scale_dict == {"ETO": 0.5, "ETH": 0.5}
+ assert test_residues_applied_list.sort() == ["ETO", "ETH"].sort()
+
+ def test_charmm_all_residues_not_in_dict_boxes_for_simulation_1(
+ self, ethane_gomc, ethanol_gomc
+ ):
+ with pytest.raises(
+ ValueError,
+ match=f"The {'ETO'} residues were not used from the forcefield_selection string or dictionary. "
+ "All the residues were not used from the forcefield_selection "
+ "string or dictionary. There may be residues below other "
+ "specified residues in the mbuild.Compound hierarchy. "
+ "If so, all the highest listed residues pass down the force "
+ "fields through the hierarchy. Alternatively, residues that "
+ "are not in the structure may have been specified. ",
+ ):
+ box_reservior_0 = mb.fill_box(
+ compound=[ethane_gomc], box=[1, 1, 1], n_compounds=[1]
+ )
+ specific_ff_to_residue(
+ box_reservior_0,
+ forcefield_selection={
+ ethanol_gomc.name: "oplsaa",
+ ethane_gomc.name: "oplsaa",
+ },
+ residues=[ethanol_gomc.name, ethane_gomc.name],
+ boxes_for_simulation=1,
+ )
+
+ def test_charmm_all_residues_not_in_dict_boxes_for_simulation_2(
+ self, ethane_gomc, ethanol_gomc
+ ):
+ with pytest.warns(
+ UserWarning,
+ match=f"The {'ETO'} residues were not used from the forcefield_selection string or dictionary. "
+ "All the residues were not used from the forcefield_selection "
+ "string or dictionary. There may be residues below other "
+ "specified residues in the mbuild.Compound hierarchy. "
+ "If so, all the highest listed residues pass down the force "
+ "fields through the hierarchy. Alternatively, residues that "
+ "are not in the structure may have been specified. "
+ f"NOTE: This warning will appear if you are using the CHARMM pdb and psf writers "
+ f"2 boxes, and the boxes do not contain all the residues in each box.",
+ ):
+ box_reservior_0 = mb.fill_box(
+ compound=[ethane_gomc], box=[1, 1, 1], n_compounds=[1]
+ )
+ specific_ff_to_residue(
+ box_reservior_0,
+ forcefield_selection={
+ ethanol_gomc.name: "oplsaa",
+ ethane_gomc.name: "oplsaa",
+ },
+ residues=[ethanol_gomc.name, ethane_gomc.name],
+ boxes_for_simulation=2,
+ )
+
+ def test_specific_ff_params_benzene_water_aa(self):
+ benzene_aa = mb.load(get_fn("benzene_aa.mol2"))
+ benzene_aa.name = "BEN"
+ water_aa = mb.load("O", smiles=True)
+ water_aa.name = "WAT"
+ test_box = mb.fill_box(
+ compound=[benzene_aa, water_aa], n_compounds=[1, 1], box=[4, 4, 4]
+ )
+
+ [
+ test_topology,
+ test_residues_applied_list,
+ test_electrostatics14Scale_dict,
+ test_nonBonded14Scale_dict,
+ test_atom_types_dict,
+ test_bond_types_dict,
+ test_angle_types_dict,
+ test_dihedral_types_dict,
+ test_improper_types_dict,
+ test_combining_rule_dict,
+ ] = specific_ff_to_residue(
+ test_box,
+ forcefield_selection={
+ benzene_aa.name: f"{get_fn('gmso_xmls/test_ffstyles/benzene_GAFF.xml')}",
+ water_aa.name: f"{get_fn('gmso_xmls/test_ffstyles/spce_water__lorentz_combining.xml')}",
+ },
+ residues=[benzene_aa.name, water_aa.name],
+ boxes_for_simulation=1,
+ )
+ assert test_topology.n_sites == 15
+ assert test_topology.n_bonds == 14
+ assert test_topology.n_angles == 19
+ assert test_topology.n_dihedrals == 24
+ assert test_topology.n_impropers == 6
+
+ assert test_electrostatics14Scale_dict == {"BEN": 0.833333333, "WAT": 0}
+ assert test_nonBonded14Scale_dict == {"BEN": 0.5, "WAT": 0}
+ assert test_residues_applied_list == ["BEN", "WAT"]
+ assert test_combining_rule_dict == "lorentz"
+
+ # atom tests
+ assert (
+ str(test_atom_types_dict["BEN"]["expression"])
+ == "4*epsilon*(-sigma**6/r**6 + sigma**12/r**12)"
+ )
+ assert (
+ len(list(test_atom_types_dict["BEN"]["atom_types"].yield_view()))
+ == 2
+ )
+
+ assert (
+ str(test_atom_types_dict["WAT"]["expression"])
+ == "4*epsilon*(-sigma**6/r**6 + sigma**12/r**12)"
+ )
+ assert (
+ len(list(test_atom_types_dict["WAT"]["atom_types"].yield_view()))
+ == 2
+ )
+
+ # bond tests
+ assert (
+ str(test_bond_types_dict["BEN"]["expression"])
+ == "k*(r - r_eq)**2/2"
+ )
+ assert (
+ len(list(test_bond_types_dict["BEN"]["bond_types"].yield_view()))
+ == 2
+ )
+
+ assert (
+ str(test_bond_types_dict["WAT"]["expression"]) == "k*(r - r_eq)**2"
+ )
+ assert (
+ len(list(test_bond_types_dict["WAT"]["bond_types"].yield_view()))
+ == 1
+ )
+
+ # angle tests
+ assert (
+ str(test_angle_types_dict["BEN"]["expression"])
+ == "k*(theta - theta_eq)**2/2"
+ )
+ assert (
+ len(list(test_angle_types_dict["BEN"]["angle_types"].yield_view()))
+ == 2
+ )
+
+ assert (
+ str(test_angle_types_dict["WAT"]["expression"])
+ == "k*(theta - theta_eq)**2"
+ )
+ assert (
+ len(list(test_angle_types_dict["WAT"]["angle_types"].yield_view()))
+ == 1
+ )
+
+ # dihedral tests
+ assert (
+ str(test_dihedral_types_dict["BEN"]["expression"])
+ == "k*(cos(n*phi - phi_eq) + 1)"
+ )
+ assert (
+ len(
+ list(
+ test_dihedral_types_dict["BEN"][
+ "dihedral_types"
+ ].yield_view()
+ )
+ )
+ == 3
+ )
+
+ # improper tests
+ assert (
+ str(test_improper_types_dict["BEN"]["expression"])
+ == "k*(cos(n*phi - phi_eq) + 1)"
+ )
+ assert (
+ len(
+ list(
+ test_improper_types_dict["BEN"][
+ "improper_types"
+ ].yield_view()
+ )
+ )
+ == 1
+ )
+
+ def test_specific_ff_params_benzene_aa_grouped(self):
+ methane_ua_bead_name = "_CH4"
+ methane_child_bead = mb.Compound(name=methane_ua_bead_name)
+ methane_box = mb.fill_box(
+ compound=methane_child_bead, n_compounds=4, box=[1, 2, 3]
+ )
+ methane_box.name = "MET"
+
+ [
+ test_topology,
+ test_residues_applied_list,
+ test_electrostatics14Scale_dict,
+ test_nonBonded14Scale_dict,
+ test_atom_types_dict,
+ test_bond_types_dict,
+ test_angle_types_dict,
+ test_dihedral_types_dict,
+ test_improper_types_dict,
+ test_combining_rule_dict,
+ ] = specific_ff_to_residue(
+ methane_box,
+ forcefield_selection={
+ methane_box.name: "trappe-ua",
+ },
+ residues=[methane_box.name],
+ gmso_match_ff_by="group",
+ boxes_for_simulation=1,
+ )
+ assert test_topology.n_sites == 4
+ assert test_topology.n_bonds == 0
+ assert test_topology.n_angles == 0
+ assert test_topology.n_dihedrals == 0
+ assert test_topology.n_impropers == 0
+
+ assert test_electrostatics14Scale_dict == {"MET": 0}
+ assert test_nonBonded14Scale_dict == {"MET": 0}
+ assert test_residues_applied_list == ["MET"]
+ assert test_combining_rule_dict == "lorentz"
+
+ # atom tests
+ assert (
+ str(test_atom_types_dict["MET"]["expression"])
+ == "4*epsilon*(-sigma**6/r**6 + sigma**12/r**12)"
+ )
+ assert (
+ len(list(test_atom_types_dict["MET"]["atom_types"].yield_view()))
+ == 1
+ )
diff --git a/gmso/tests/test_xml_handling.py b/gmso/tests/test_xml_handling.py
index 320745890..87fdd1196 100644
--- a/gmso/tests/test_xml_handling.py
+++ b/gmso/tests/test_xml_handling.py
@@ -17,9 +17,9 @@
def compare_xml_files(fn1, fn2):
"""Hash files to check for lossless conversion."""
with open(fn1, "r") as f:
- line2 = f.readlines()
- with open(fn2, "r") as f:
line1 = f.readlines()
+ with open(fn2, "r") as f:
+ line2 = f.readlines()
for l1, l2 in zip(line1, line2):
assert l1.replace(" ", "") == l2.replace(" ", "")
return True
@@ -89,7 +89,8 @@ def test_load_write_xmls_gmso_backend(self, xml):
ff1 = ForceField(xml, backend="forcefield_utilities")
ff1.to_xml("tmp.xml", overwrite=True)
ff2 = ForceField("tmp.xml", strict=False)
- assert compare_xml_files(xml, "tmp.xml")
+ if "test_ffstyles" not in xml:
+ assert compare_xml_files(xml, "tmp.xml")
assert ff1 == ff2
@pytest.mark.parametrize("xml", TEST_XMLS)
@@ -98,7 +99,8 @@ def test_load_write_xmls_ffutils_backend(self, xml):
ff1 = ForceField(xml, backend="forcefield-utilities")
ff1.to_xml("tmp.xml", overwrite=True)
ff2 = GMSOFFs().load_xml("tmp.xml").to_gmso_ff()
- assert compare_xml_files("tmp.xml", xml)
+ if "test_ffstyles" not in xml:
+ assert compare_xml_files("tmp.xml", xml)
assert ff1 == ff2
def test_xml_error_handling(self):
diff --git a/gmso/utils/equation_compare.py b/gmso/utils/equation_compare.py
new file mode 100644
index 000000000..df1c12925
--- /dev/null
+++ b/gmso/utils/equation_compare.py
@@ -0,0 +1,708 @@
+"""GMSO equation or expression comparisons."""
+import os
+
+import sympy
+import unyt as u
+
+from gmso.utils.io import get_fn
+
+
+# compare Lennard-Jones (LJ) non-bonded equations
+def evaluate_nonbonded_lj_format_with_scaler(new_lj_form, base_lj_form):
+ """Compare a new Lennard-Jones (LJ) form to a base LJ form (new LJ form / base LJ form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_lj_form : str
+ The new Lennard-Jones (LJ) form that will be compared or divided by the base form.
+ base_lj_form : str
+ The base LJ form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'LJ', if the new_lj_form variable is a LJ non-bonded form.
+ None, if the new_lj_form variable is not a LJ non-bonded form.
+ form_scalar : float
+ float, if the new_lj_form variable is a LJ non-bonded form.
+ None, if the new_lj_form variable is not a LJ non-bonded form.
+ """
+ try:
+ (
+ eqn_ratio,
+ epsilon,
+ sigma,
+ r,
+ Rmin,
+ two,
+ ) = sympy.symbols("eqn_ratio epsilon sigma r Rmin two")
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_lj_form) / sympy.sympify(base_lj_form),
+ Rmin - sigma * two ** (1 / 6),
+ two - 2,
+ ],
+ [eqn_ratio, Rmin, two],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "LJ"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+# compare Mie non-bonded equations
+def evaluate_nonbonded_mie_format_with_scaler(new_mie_form, base_mie_form):
+ """Compare a new Mie form to a base Mie form (new Mie form / base Mie form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_mie_form : str
+ The new Mie form that will be compared or divided by the base form.
+ base_mie_form : str
+ The base Mie form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'Mie', if the new_mie_form variable is a Mie non-bonded form.
+ None, if the new_mie_form variable is not a Mie non-bonded form.
+ form_scalar : float
+ float, if the new_mie_form variable is a Mie non-bonded form.
+ None, if the new_mie_form variable is not a Mie non-bonded form.
+ """
+ try:
+ (
+ eqn_ratio,
+ epsilon,
+ sigma,
+ r,
+ n,
+ ) = sympy.symbols("eqn_ratio epsilon sigma r n")
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_mie_form) / sympy.sympify(base_mie_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "Mie"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+# compare Exp6 non-bonded equations
+def evaluate_nonbonded_exp6_format_with_scaler(new_exp6_form, base_exp6_form):
+ """Compare a new Exp6 form to a base Exp6 form (new Mie form / base Mie form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_exp6_form : str
+ The new Exp6 form that will be compared or divided by the base form.
+ base_exp6_form : str
+ The base Exp6 form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'Exp6', if the new_exp6_form variable is an Exp6 non-bonded form.
+ None, if the new_exp6_form variable is not an Exp6 non-bonded form.
+ form_scalar : float
+ float, if the new_exp6_form variable is an Exp6 non-bonded form.
+ None, if the new_exp6_form variable is not an Exp6 non-bonded form.
+ """
+ try:
+ (
+ eqn_ratio,
+ epsilon,
+ sigma,
+ r,
+ Rmin,
+ alpha,
+ ) = sympy.symbols("eqn_ratio epsilon sigma r Rmin alpha")
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_exp6_form) / sympy.sympify(base_exp6_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "Exp6"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+def get_atom_type_expressions_and_scalars(atom_types_dict):
+ """Get the field file expressions and its scalar from the base forms used from the GMSO XML file.
+
+ The expression base forms are as follows.
+ 'LJ' = "4*epsilon * ((Rmin/r)**12 - 2*(Rmin/r)**6)"
+ 'Mie' = "(n/(n-m)) * (n/m)**(m/(n-m)) * epsilon * ((sigma/r)**n - (sigma/r)**m)",
+ 'Exp6' = "epsilon*alpha/(alpha-6) * (6/alpha*exp(alpha*(1-r/Rmin)) - (Rmin/r)**6)"
+
+ Note that when n=12 and m=6 in the 'Mie' form, it is the exact same as the 'LJ' form.
+ Note that the 'LJ' form above is equivalent to "epsilon * ((Rmin/r)**12 - 2*(Rmin/r)**6)"
+
+ Parameters
+ ----------
+ atom_types_dict: dict
+ A nested dict with the all the residues as the main keys.
+
+ {'residue_name': {
+ 'expression': expression_form_for_all_atom_types,
+ 'atom_types': Topology.atom_types}}
+ }
+
+ Example with an LJ expression
+
+ atom_types_dict = {
+ 'ETH': {'expression': epsilon*(-sigma**6/r**6 + sigma**12/r**12),
+ 'atom_types': (, )}
+ }
+
+ Returns
+ -------
+ atom_types_data_expression_data_dict : dict
+ The dictionary to append with the residue name and the GMSO force field expressions and units.
+
+ Example with an LJ expression
+
+ 'Residue_1': {
+ 'expression': '4 * epsilon * ((sigma/r)**12 - (sigma/r)**6)',
+ 'expression_form': 'LJ',
+ 'expression_scalar': 1.0,
+ 'q_units': 'coulomb',
+ 'sigma_units': 'nm',
+ 'epsilon_units': 'kJ/mol'}
+ }
+ """
+ eqn_gomc_std_forms_dict = {
+ "LJ": "4*epsilon * ((sigma/r)**12 - (sigma/r)**6)",
+ "Mie": "(n/(n-m)) * (n/m)**(m/(n-m)) * epsilon * ((sigma/r)**n - (sigma/r)**m)",
+ "Exp6": "epsilon*alpha/(alpha-6) * (6/alpha*exp(alpha*(1-r/Rmin)) - (Rmin/r)**6)",
+ }
+
+ atomtypes_data_expression_data_dict = {}
+ for res_i in atom_types_dict.keys():
+ for atom_type_m in atom_types_dict[res_i]["atom_types"]:
+ modified_atom_type_iter = f"{res_i}_{atom_type_m.name}"
+ atomtypes_data_dict_iter = {
+ modified_atom_type_iter: {
+ "expression": None,
+ "expression_form": None,
+ "expression_scalar": None,
+ }
+ }
+ expression_iter = atom_types_dict[res_i]["expression"]
+ if (
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_form"
+ ]
+ is None
+ and atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_scalar"
+ ]
+ is None
+ ):
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_nonbonded_lj_format_with_scaler(
+ expression_iter, eqn_gomc_std_forms_dict["LJ"]
+ )
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression"
+ ] = expression_iter
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_form"
+ ] = form_output
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_scalar"
+ ] = form_scalar
+
+ if (
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_form"
+ ]
+ is None
+ and atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_scalar"
+ ]
+ is None
+ ):
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_nonbonded_mie_format_with_scaler(
+ expression_iter, eqn_gomc_std_forms_dict["Mie"]
+ )
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression"
+ ] = expression_iter
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_form"
+ ] = form_output
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_scalar"
+ ] = form_scalar
+
+ if (
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_form"
+ ]
+ is None
+ and atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_scalar"
+ ]
+ is None
+ ):
+ [
+ form_output,
+ form_scalar,
+ ] = evaluate_nonbonded_exp6_format_with_scaler(
+ expression_iter, eqn_gomc_std_forms_dict["Exp6"]
+ )
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression"
+ ] = expression_iter
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_form"
+ ] = form_output
+ atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_scalar"
+ ] = form_scalar
+
+ if (
+ atomtypes_data_dict_iter[modified_atom_type_iter]["expression"]
+ is None
+ or atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_form"
+ ]
+ is None
+ or atomtypes_data_dict_iter[modified_atom_type_iter][
+ "expression_scalar"
+ ]
+ is None
+ ):
+ print_error_text = (
+ "ERROR: the {} residue does not match the listed standard or scaled "
+ "LJ, Mie, or Exp6 expressions in the {} function."
+ "".format(
+ modified_atom_type_iter,
+ "get_atom_type_expressions_and_scalars",
+ )
+ )
+ raise ValueError(print_error_text)
+
+ atomtypes_data_expression_data_dict.update(atomtypes_data_dict_iter)
+
+ return atomtypes_data_expression_data_dict
+
+
+# compare harmonic bond equations or expressions
+def evaluate_harmonic_bond_format_with_scaler(new_bond_form, base_bond_form):
+ """Compare a new harmonic bond form to a base harmonic bond form (new bond form / base bond form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_bond_form : str
+ The new bond form that will be compared or divided by the base form.
+ base_bond_form : str
+ The base bond form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'HarmonicBondPotential', if the new_bond_form variable is a harmonic bond.
+ None, if the new_bond_form variable is not a harmonic bond.
+ form_scalar : float
+ float, if the new_bond_form variable is a harmonic bond.
+ None, if the new_bond_form variable is not a harmonic bond.
+ """
+ try:
+ eqn_ratio, k, r, r_eq = sympy.symbols("eqn_ratio k r r_eq")
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_bond_form) / sympy.sympify(base_bond_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "HarmonicBondPotential"
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+# compare harmonic angle equations or expressions
+def evaluate_harmonic_angle_format_with_scaler(new_angle_form, base_angle_form):
+ """Compare a new harmonic angle form to a base harmonic angle form (new angle form / base angle form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_angle_form : str
+ The new angle form that will be compared or divided by the base form.
+ base_angle_form : str
+ The base angle form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'HarmonicAnglePotential', if the new_angle_form variable a harmonic angle.
+ None, if the new_angle_form variable is not a harmonic angle.
+ form_scalar : float
+ float, if the new_angle_form variable is a harmonic angle.
+ None, if the new_angle_form variable is not a harmonic.
+ """
+ try:
+ eqn_ratio, k, theta, theta_eq = sympy.symbols(
+ "eqn_ratio k theta theta_eq"
+ )
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_angle_form)
+ / sympy.sympify(base_angle_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "HarmonicAnglePotential"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+# check for the harmonic torsion potential equations or expressions
+def evaluate_harmonic_torsion_format_with_scaler(
+ new_torsion_form, base_torsion_form
+):
+ """Compare a new harmonic torsion form to a base harmonic torsion form (new torsion form / base torsion form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_torsion_form : str
+ The new harmonic torsion form that will be compared or divided by the base form.
+ base_torsion_form : str
+ The base harmonic torsion form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'HarmonicTorsionPotential', if the new_torsion_form variable is a harmonic torsion.
+ None, if the new_torsion_form variable is not a harmonic torsion.
+ form_scalar : float
+ float, if the new_torsion_form variable is a harmonic torsion.
+ None, if the new_torsion_form variable is not a harmonic torsion.
+ """
+ try:
+ eqn_ratio, k, phi, phi_eq = sympy.symbols("eqn_ratio k phi phi_eq")
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_torsion_form)
+ / sympy.sympify(base_torsion_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "HarmonicTorsionPotential"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+# check for the OPLS torsion potential equations or expressions
+def evaluate_OPLS_torsion_format_with_scaler(
+ new_torsion_form, base_torsion_form
+):
+ """Compare a new OPLS torsion form to a base OPLS torsion form (new torsion form / base torsion form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_torsion_form : str
+ The new OPLS torsion form that will be compared or divided by the base form.
+ base_torsion_form : str
+ The base OPLS torsion form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'OPLSTorsionPotential', if the new_torsion_form variable is an OPLS torsion.
+ None, if the new_torsion_form variable is not an OPLS torsion.
+ form_scalar : float
+ float, if the new_torsion_form variable is an OPLS torsion.
+ None, if the new_torsion_form variable is not an OPLS torsion.
+ """
+ try:
+ eqn_ratio, k0, k1, k2, k3, k4, phi = sympy.symbols(
+ "eqn_ratio k0 k1 k2 k3 k4 phi"
+ )
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_torsion_form)
+ / sympy.sympify(base_torsion_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "OPLSTorsionPotential"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+# check for the periodic torsion potential equations or expressions
+def evaluate_periodic_torsion_format_with_scaler(
+ new_torsion_form, base_torsion_form
+):
+ """Compare a new periodic torsion form to a base periodic torsion form (new torsion form / base torsion form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_torsion_form : str
+ The new periodic torsion form that will be compared or divided by the base formv
+ base_torsion_form : str
+ The base periodic torsion form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'PeriodicTorsionPotential', if the new_torsion_form variable is a periodic torsion.
+ None, if the new_torsion_form variable is not a periodic torsion.
+ form_scalar : float
+ float, if the new_torsion_form variable is a periodic torsion.
+ None, if the new_torsion_form variable is not a periodic torsion.
+ """
+ try:
+ eqn_ratio, k, n, phi, phi_eq = sympy.symbols("eqn_ratio k n phi phi_eq")
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_torsion_form)
+ / sympy.sympify(base_torsion_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "PeriodicTorsionPotential"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+# check for the RyckaertBellemans (RB) torsion potential equations or expressions
+def evaluate_RB_torsion_format_with_scaler(new_torsion_form, base_torsion_form):
+ """Compare a new Ryckaert-Bellemans (RB) torsion form to a base torsion form (new torsion form / base torsion form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_torsion_form : str
+ The new RB torsion form that will be compared or divided by the base formv
+ base_torsion_form : str
+ The base RB torsion form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'RyckaertBellemansTorsionPotential', if the new_torsion_form variable is an RB torsion.
+ None, if the new_torsion_form variable is not an RB torsion.
+ form_scalar : float
+ float, if the new_torsion_form variable is an RB torsion.
+ None, if the new_torsion_form variable is not an RB torsion.
+ """
+ try:
+ eqn_ratio, c0, c1, c2, c3, c4, c5, psi = sympy.symbols(
+ "eqn_ratio c0 c1 c2 c3 c4 c5 psi"
+ )
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_torsion_form)
+ / sympy.sympify(base_torsion_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "RyckaertBellemansTorsionPotential"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+# check for the harmonic improper potential equations or expressions
+def evaluate_harmonic_improper_format_with_scaler(
+ new_improper_form, base_improper_form
+):
+ """Compare a new harmonic improper form to a base harmonic improper form (new improper form / base improper form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_improper_form : str
+ The new harmonic improper form that will be compared or divided by the base form.
+ base_improper_form : str
+ The base harmonic improper form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'HarmonicImproperPotential', if the new_improper_form variable is a harmonic improper.
+ None, if the new_improper_form variable is not a harmonic improper.
+ form_scalar : float
+ float, if the new_improper_form variable is a harmonic improper.
+ None, if the new_improper_form variable is not a harmonic improper.
+ """
+ try:
+ eqn_ratio, k, phi, phi_eq = sympy.symbols("eqn_ratio k phi phi_eq")
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_improper_form)
+ / sympy.sympify(base_improper_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "HarmonicImproperPotential"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
+
+
+# check for the periodic improper potential equations or expressions
+def evaluate_periodic_improper_format_with_scaler(
+ new_improper_form, base_improper_form
+):
+ """Compare a new periodic improper form to a base periodic improper form (new improper form / base improper form).
+
+ If the new form is the same as the base form, other than a scaling factor,
+ it labels the form and provides its scalar from the base form.
+
+ Parameters
+ ----------
+ new_improper_form : str
+ The new periodic improper form that will be compared or divided by the base form.
+ base_improper_form : str
+ The base periodic improper form, which is the standard form.
+
+ Returns
+ -------
+ list, [form_output, form_scalar]
+ form_output : str
+ 'PeriodicImproperPotential', if the new_improper_form variable is a periodic improper.
+ None, if the new_improper_form variable is not a periodic improper.
+ form_scalar : float
+ float, if the new_improper_form variable is a periodic improper.
+ None, if the new_improper_form variable is not a periodic improper.
+ """
+ try:
+ eqn_ratio, k, n, phi, phi_eq = sympy.symbols("eqn_ratio k n phi phi_eq")
+ values = sympy.nonlinsolve(
+ [
+ eqn_ratio
+ - sympy.sympify(new_improper_form)
+ / sympy.sympify(base_improper_form),
+ ],
+ [eqn_ratio],
+ )
+
+ form_scalar = float(list(values)[0][0])
+ form_output = "PeriodicImproperPotential"
+
+ except:
+ form_scalar = None
+ form_output = None
+
+ return [form_output, form_scalar]
diff --git a/gmso/utils/files/benzene_aa.mol2 b/gmso/utils/files/benzene_aa.mol2
new file mode 100644
index 000000000..ad1957892
--- /dev/null
+++ b/gmso/utils/files/benzene_aa.mol2
@@ -0,0 +1,39 @@
+@MOLECULE
+BEN
+ 12 12 1 0 0
+SMALL
+NO_CHARGES
+****
+Energy = 0
+
+@ATOM
+ 1 C 0.0000 0.0000 0.0000 C 1 BEN 0.000000
+ 2 C 1.4000 0.0000 0.0000 C 1 BEN 0.000000
+ 3 C 2.1000 1.2124 0.0000 C 1 BEN 0.000000
+ 4 C 1.4000 2.4249 0.0000 C 1 BEN 0.000000
+ 5 C 0.0000 2.4249 0.0000 C 1 BEN 0.000000
+ 6 C -0.7000 1.2124 0.0000 C 1 BEN 0.000000
+ 7 H -0.5000 -0.8660 0.0000 H 1 BEN 0.000000
+ 8 H 1.9000 -0.8660 0.0000 H 1 BEN 0.000000
+ 9 H 3.1000 1.2124 0.0000 H 1 BEN 0.000000
+ 10 H 1.9000 3.2909 0.0000 H 1 BEN 0.000000
+ 11 H -0.5000 3.2909 0.0000 H 1 BEN 0.000000
+ 12 H -1.7000 1.2124 0.0000 H 1 BEN 0.000000
+@BOND
+ 1 1 2 2
+ 2 1 6 1
+ 3 1 7 1
+ 4 2 3 1
+ 5 2 8 1
+ 6 3 4 2
+ 7 3 9 1
+ 8 4 5 1
+ 9 4 10 1
+ 10 5 6 2
+ 11 5 11 1
+ 12 6 12 1
+
+@SUBSTRUCTURE
+1 **** 1 TEMP 0 **** **** 0 ROOT
+
+#generated by VMD
diff --git a/gmso/utils/files/ethane_ua.mol2 b/gmso/utils/files/ethane_ua.mol2
new file mode 100644
index 000000000..a5b932e6c
--- /dev/null
+++ b/gmso/utils/files/ethane_ua.mol2
@@ -0,0 +1,18 @@
+@MOLECULE
+ETH
+ 2 1 1 0 0
+SMALL
+NO_CHARGES
+****
+Energy = 0
+
+@ATOM
+ 1 _CH3 0.0000 0.0000 0.0000 C 1 ETH 0.000000
+ 2 _CH3 1.5400 0.0000 0.0000 C 1 ETH 0.000000
+@BOND
+ 1 1 2 1
+
+@SUBSTRUCTURE
+1 **** 1 TEMP 0 **** **** 0 ROOT
+
+#generated by VMD
diff --git a/gmso/utils/files/ethane_ua_mie.mol2 b/gmso/utils/files/ethane_ua_mie.mol2
new file mode 100644
index 000000000..56461816d
--- /dev/null
+++ b/gmso/utils/files/ethane_ua_mie.mol2
@@ -0,0 +1,18 @@
+@MOLECULE
+ETHM
+ 2 1 1 0 0
+SMALL
+NO_CHARGES
+****
+Energy = 0
+
+@ATOM
+ 1 _CH3 0.0000 0.0000 0.0000 C 1 ETHM 0.000000
+ 2 _CH3 1.5400 0.0000 0.0000 C 1 ETHM 0.000000
+@BOND
+ 1 1 2 1
+
+@SUBSTRUCTURE
+1 **** 1 TEMP 0 **** **** 0 ROOT
+
+#generated by VMD
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_GAFF.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_GAFF.xml
new file mode 100755
index 000000000..e7a9c87d5
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_GAFF.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+ 15.167
+
+
+ 3.141592653589793
+
+
+
+
+
+
+ 2
+
+
+ 4.6024
+
+
+ 3.141592653589793
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_trappe-ua.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_trappe-ua.xml
index 1c74c2148..ce43a9397 100644
--- a/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_trappe-ua.xml
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/benzene_trappe-ua.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/charmm36.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/charmm36.xml
index 0151233c1..23b125e39 100644
--- a/gmso/utils/files/gmso_xmls/test_ffstyles/charmm36.xml
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/charmm36.xml
@@ -1,6 +1,7 @@
+
-
+
@@ -12,6 +13,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_Mie_lorentz_combining.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_Mie_lorentz_combining.xml
new file mode 100755
index 000000000..4e3c0d8be
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_Mie_lorentz_combining.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_bad_eqn_lorentz_combining.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_bad_eqn_lorentz_combining.xml
new file mode 100755
index 000000000..db569edd4
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_bad_eqn_lorentz_combining.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_lorentz_combining.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_lorentz_combining.xml
new file mode 100755
index 000000000..87112d5a6
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/ethane_propane_ua_lorentz_combining.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/opls_charmm_buck.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/opls_charmm_buck.xml
index 8952bcb25..e89a5215a 100644
--- a/gmso/utils/files/gmso_xmls/test_ffstyles/opls_charmm_buck.xml
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/opls_charmm_buck.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/oplsaa_from_foyer.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/oplsaa_from_foyer.xml
index 524bd2f0b..b98a70853 100644
--- a/gmso/utils/files/gmso_xmls/test_ffstyles/oplsaa_from_foyer.xml
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/oplsaa_from_foyer.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/spce.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/spce.xml
index 186fad6e2..cde76944a 100644
--- a/gmso/utils/files/gmso_xmls/test_ffstyles/spce.xml
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/spce.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/spce_water__geometric_combining.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/spce_water__geometric_combining.xml
new file mode 100755
index 000000000..d028c4efe
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/spce_water__geometric_combining.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/spce_water__lorentz_combining.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/spce_water__lorentz_combining.xml
new file mode 100755
index 000000000..b077ad5e2
--- /dev/null
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/spce_water__lorentz_combining.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/tip3p.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/tip3p.xml
index 3c99a3a95..706a8ad9c 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/files/gmso_xmls/test_ffstyles/tip4p_2005.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_2005.xml
index e213d53b9..51d559fc1 100644
--- a/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_2005.xml
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_2005.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_ew.xml b/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_ew.xml
index f81c53a63..280d10446 100644
--- a/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_ew.xml
+++ b/gmso/utils/files/gmso_xmls/test_ffstyles/tip4p_ew.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_molecules/alkanes.xml b/gmso/utils/files/gmso_xmls/test_molecules/alkanes.xml
index ea4feb5a0..3fbe9cd98 100644
--- a/gmso/utils/files/gmso_xmls/test_molecules/alkanes.xml
+++ b/gmso/utils/files/gmso_xmls/test_molecules/alkanes.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_molecules/alkenes.xml b/gmso/utils/files/gmso_xmls/test_molecules/alkenes.xml
index ab2f17637..80b7e6edd 100644
--- a/gmso/utils/files/gmso_xmls/test_molecules/alkenes.xml
+++ b/gmso/utils/files/gmso_xmls/test_molecules/alkenes.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_molecules/alkynes.xml b/gmso/utils/files/gmso_xmls/test_molecules/alkynes.xml
index ff0dfe2b5..89b5b2f2a 100644
--- a/gmso/utils/files/gmso_xmls/test_molecules/alkynes.xml
+++ b/gmso/utils/files/gmso_xmls/test_molecules/alkynes.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_molecules/carbon.xml b/gmso/utils/files/gmso_xmls/test_molecules/carbon.xml
index 3a008f8aa..2ea58ec50 100644
--- a/gmso/utils/files/gmso_xmls/test_molecules/carbon.xml
+++ b/gmso/utils/files/gmso_xmls/test_molecules/carbon.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_units/ang_kcal_coulomb_gram_degree_mol.xml b/gmso/utils/files/gmso_xmls/test_units/ang_kcal_coulomb_gram_degree_mol.xml
index e15b236cb..63bec0c20 100644
--- a/gmso/utils/files/gmso_xmls/test_units/ang_kcal_coulomb_gram_degree_mol.xml
+++ b/gmso/utils/files/gmso_xmls/test_units/ang_kcal_coulomb_gram_degree_mol.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/files/gmso_xmls/test_units/nm_kj_electroncharge_amu_rad_mol.xml b/gmso/utils/files/gmso_xmls/test_units/nm_kj_electroncharge_amu_rad_mol.xml
index 047ae25ce..be28dbd84 100644
--- a/gmso/utils/files/gmso_xmls/test_units/nm_kj_electroncharge_amu_rad_mol.xml
+++ b/gmso/utils/files/gmso_xmls/test_units/nm_kj_electroncharge_amu_rad_mol.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/gmso/utils/specific_ff_to_residue.py b/gmso/utils/specific_ff_to_residue.py
new file mode 100644
index 000000000..0b5976d04
--- /dev/null
+++ b/gmso/utils/specific_ff_to_residue.py
@@ -0,0 +1,816 @@
+"""GMSO and foyer use specific residues to apply force fields and mapping molecule number to atom numbers."""
+import os
+from warnings import warn
+from xml.dom import minidom
+
+import mbuild as mb
+from forcefield_utilities.xml_loader import FoyerFFs, GMSOFFs
+from mbuild.compound import Compound
+from mbuild.utils.io import has_foyer
+
+import gmso
+from gmso.core.views import PotentialFilters
+from gmso.external.convert_mbuild import from_mbuild as mb_convert
+from gmso.parameterization import apply as gmso_apply
+
+
+def specific_ff_to_residue(
+ structure,
+ forcefield_selection=None,
+ gmso_match_ff_by="molecule",
+ residues=None,
+ boxes_for_simulation=1,
+):
+ """
+ Take the mbuild Compound or mbuild Box and applies the selected FF to the corresponding residue via foyer and GMSO.
+
+ Note: a residue is defined as a molecule in this case, so it is not
+ designed for applying a force field to a protein.
+
+ Parameters
+ ----------
+ structure: mbuild Compound object or mbuild Box object;
+ The mBuild Compound object with box lengths and angles or mbuild Box object, which contains the molecules
+ (or empty box) that will have the force field applied to them.
+ forcefield_selection: str or dictionary, default=None
+ Apply a force field to the output file by selecting a force field xml file with
+ its path or by using the standard force field name provided the `foyer` package.
+ Example dict for FF file: {'ETH': 'oplsaa.xml', 'OCT': 'path_to file/trappe-ua.xml'}
+ Example str for FF file: 'path_to_file/trappe-ua.xml'
+ Example dict for standard FF names: {'ETH': 'oplsaa', 'OCT': 'trappe-ua'}
+ Example str for standard FF names: 'trappe-ua'
+ Example of a mixed dict with both: {'ETH': 'oplsaa', 'OCT': 'path_to_file/'trappe-ua.xml'}
+ gmso_match_ff_by: str ("group" or "molecule"), default = "molecule"
+ How the GMSO force field is applied, using the molecules name/residue name (mbuild.Compound.name)
+ for GOMC and NAMD. This is regardless number of levels in the mbuild.Compound.
+
+ * "molecule" applies the force field using the molecule's name or atom's name for a
+ single atom molecule (1 atom/bead = molecule).
+ - Molecule > 1 atom ----> uses the "mbuild.Compound.name" (1 level above the atoms/beads)
+ as the molecule's name. This "mb.Compound.name" (1 level above the atoms/beads) needs
+ to be used in the Charmm object's residue_list and forcefield_selection (if >1 force field), and
+ will be the residue name in the PSF, PDB, and FF files.
+ - Molecule = 1 atom/bead ----> uses the "atom/bead's name" as the molecule's name.
+ This "atom/bead's name" needs to be used in the Charmm object's residue_list and
+ forcefield_selection (if >1 force field), and will be the residue name in the PSF, PDB, and FF files.
+
+ NOTE: Non-bonded zeolites or other fixed structures without bonds will use the
+ "atom/bead's name" as the molecule's name, if they are single non-bonded atoms.
+ However, the user may want to use the "group" option instead for this type of system,
+ if applicable (see the "group" option).
+
+ - Example (Charmm_writer selects the user changeable "ETH", in the Charmm object residue_list and
+ forcefield_selection (if >1 force field), which sets the residue "ETH" in the PSF, PDB, and FF files):
+ ethane = mbuild.load("CC", smiles=True)
+ ethane.name = "ETH"
+
+ ethane_box = mbuild.fill_box(
+ compound=[ethane],
+ n_compounds=[100],
+ box=[4, 4, 4]
+ )
+
+ - Example (Charmm_writer must to select the non-user changeable "_CH4" (per the foyer TraPPE force field),
+ in the Charmm object residue_list and forcefield_selection (if >1 force field),
+ which sets the residue "_CH4" in the PSF, PDB, and FF files):
+
+ methane_ua_bead_name = "_CH4"
+ methane_child_bead = mbuild.Compound(name=methane_ua_bead_name)
+ methane_box = mbuild.fill_box(
+ compound=methane_child_bead, n_compounds=4, box=[1, 2, 3]
+ )
+ methane_box.name = "MET"
+
+ - Example (Charmm_writer must to select the non-user changeable "Na" and "Cl"
+ in the Charmm object residue_list and forcefield_selection (if >1 force field),
+ which sets the residues "Na" and "Cl" in the PSF, PDB, and FF files):
+
+ sodium_atom_name = "Na"
+ sodium_child_atom = mbuild.Compound(name=sodium_atom_name)
+ sodium = mb.Compound(name="SOD")
+ sodium.add(sodium_child_atom, inherit_periodicity=False)
+
+ chloride_atom_name = "Cl"
+ chloride_child_bead = mbuild.Compound(name=chloride_atom_name)
+ chloride = mb.Compound(name="CHL")
+ chloride.add(chloride_child_atom, inherit_periodicity=False)
+
+ sodium_chloride_box = mbuild.fill_box(
+ compound=[sodium, chloride],
+ n_compounds=[4, 4],
+ box=[1, 2, 3]
+ )
+
+ - Example zeolite (Charmm_writer must to select the non-user changeable "Si" and "O"
+ in the Charmm object residue_list and forcefield_selection (if >1 force field),
+ which sets the residues "Si" and "O" in the PSF, PDB, and FF files):
+
+ lattice_cif_ETV_triclinic = load_cif(file_or_path=get_mosdef_gomc_fn("ETV_triclinic.cif"))
+ ETV_triclinic = lattice_cif_ETV_triclinic.populate(x=1, y=1, z=1)
+ ETV_triclinic.name = "ETV"
+
+ * "group" applies the force field 1 level under the top level mbuild.Compound, if only 2 levels exist,
+ taking the top levels name mbuild.Compound. Or in other words:
+ - For only 2 level (mbuild container-particles) group will grab the name of the mbuild container
+ - For > 2 levels (e.g., mbuild container-etc-molecule-residue-particle),
+ group will grab 1 level down from top
+
+ This is ideal to use when you are building simulation box(es) using mbuild.fill_box(),
+ with molecules, and it allows you to add another level to single atom molecules
+ (1 atom/bead = molecule) to rename the mbuild.Compound().name, changing the residue's
+ name and allowing keeping the atom/bead name so the force field is applied properly.
+
+ WARNING: This "group" option will take all the molecule below it, regardless if they
+ are selected to be separte from the group via the residue_list and forcefield_selection
+ (if >1 force field).
+
+ NOTE: This "group" option may be best for non-bonded zeolites or other fixed structures
+ without bonds, if they are single non-bonded atoms. Using this "group" option, the user
+ can select the residue name for the Charmm_writer's residue_list and forcefield_selection
+ (if >1 force field) to force field all the atoms with a single residue name, and output
+ this residue name in the PSF, PDB, and FF files.
+
+ - Example (Charmm_writer select the user changeable "MET" in the Charmm object residue_list
+ and forcefield_selection (if >1 force field), which sets the residue "MET" in the
+ PSF, PDB, and FF files):
+
+ methane_ua_bead_name = "_CH4"
+ methane_child_bead = mbuild.Compound(name=methane_ua_bead_name)
+ methane_box = mbuild.fill_box(
+ compound=methane_child_bead, n_compounds=4, box=[1, 2, 3]
+ )
+ methane_box.name = "MET"
+
+ - Example (Charmm_writer select the user changeable "MET" in the Charmm object residue_list
+ and forcefield_selection (if >1 force field), which sets the residue "MET" in the
+ PSF, PDB, and FF files):
+
+ methane_ua_bead_name = "_CH4"
+ methane_molecule_name = "MET"
+ methane = mb.Compound(name=methane_molecule_name)
+ methane_child_bead = mb.Compound(name=methane_ua_bead_name)
+ methane.add(methane_child_bead, inherit_periodicity=False)
+
+ methane_box = mb.fill_box(
+ compound=methane, n_compounds=10, box=[1, 2, 3]
+ )
+
+ - Example (Charmm_writer select the user changeable "MET" in the Charmm object residue_list
+ and forcefield_selection (if >1 force field), which sets the residue "MET" in the
+ PSF, PDB, and FF files):
+
+ methane_child_bead = mb.Compound(name="_CH4")
+ methane = mb.Compound(name="MET")
+ methane.add(methane_child_bead, inherit_periodicity=False)
+
+ box_liq = mb.fill_box(
+ compound=methane,
+ n_compounds=1230,
+ box=[4.5, 4.5, 4.5]
+ )
+
+ - Example zeolite (Charmm_writer select the user changeable "ETV" in the Charmm object residue_list
+ and forcefield_selection (if >1 force field), which sets the residue
+ "ETV" in the PSF, PDB, and FF files):
+
+ lattice_cif_ETV_triclinic = load_cif(file_or_path=get_mosdef_gomc_fn("ETV_triclinic.cif"))
+ ETV_triclinic = lattice_cif_ETV_triclinic.populate(x=1, y=1, z=1)
+ ETV_triclinic.name = "ETV"
+
+ residues: list, [str, ..., str], default=None
+ Labels of unique residues in the Compound. Residues are assigned by
+ checking against Compound.name. Only supply residue names as 4 characters
+ strings, as the residue names are truncated to 4 characters to fit in the
+ psf and pdb file.
+ boxes_for_simulation: either 1 or 2, default=1
+ Gibbs (GEMC) or grand canonical (GCMC) ensembles are examples of where the boxes_for_simulation would be 2.
+ Canonical (NVT) or isothermal–isobaric (NPT) ensembles are example with the boxes_for_simulation equal to 1.
+ Note: the only valid options are 1 or 2.
+
+ Returns
+ -------
+ list, [
+ topology,
+ unique_topology_groups_lists,
+ residues_applied_list,
+ electrostatics14Scale_dict,
+ nonBonded14Scale_dict,
+ atom_types_dict,
+ bond_types_dict,
+ angle_types_dict,
+ dihedral_types_dict,
+ improper_types_dict,
+ combining_rule,
+ ]
+
+ topology: gmso.Topology
+ gmso Topology with applied force field
+ unique_topology_groups_list: list
+ list of residues (i.e., list of strings).
+ These are all the residues in which the force field actually applied.
+ electrostatics14Scale_dict: dict
+ A dictionary with the 1,4-electrostatic/Coulombic scalars for each residue,
+ as the forcefields are specified by residue {'residue_name': '1-4_electrostatic_scaler'}.
+ nonBonded14Scale_dict: dict
+ A dictionary with the 1,4-non-bonded scalars for each residue,
+ as the forcefields are specified by residue {'residue_name': '1-4_nonBonded_scaler'}.
+ atom_types_dict: dict
+ A dict with the all the residues as the keys. The unique values are a list containing,
+ {'expression': confirmed singular atom types expression or equation,
+ 'atom_types': gmso Topology.atom_types}.
+ bond_types_dict: dict
+ A dict with the all the residues as the keys. The unique values are a list containing,
+ {'expression': confirmed singular bond types expression or equation,
+ 'bond_types': gmso Topology.bond_types}.
+ angle_types_dict: dict
+ A dict with the all the residues as the keys. The unique values are a list containing,
+ {'expression': confirmed singular angle types expression or equation,
+ 'angle_types': gmso Topology.angle_types}.
+ dihedral_types_dict: dict
+ A dict with the all the residues as the keys. The unique values are a list containing,
+ {'expression': confirmed singular dihedral types expression or equation,
+ 'dihedral_types': gmso Topology.dihedral_types}.
+ improper_types_dict: dict
+ A dict with the all the residues as the keys. The unique values are a list containing,
+ {'expression': confirmed singular improper types expression or equation,
+ 'improper_types': gmso Topology.improper_types}.
+ combining_rule: str
+ The possible mixing/combining rules are 'geometric' or 'lorentz',
+ which provide the geometric and arithmetic mixing rule, respectively.
+ NOTE: Arithmetic means the 'lorentz' combining or mixing rule.
+ NOTE: GMSO default to the 'lorentz' mixing rule if none is provided,
+ and this writers default is the GMSO default.
+
+ Notes
+ -----
+ To write the NAMD/GOMC force field, pdb, psf, and force field
+ (.inp) files, the residues and forcefields must be provided in
+ a str or dictionary. If a dictionary is provided all residues must
+ be specified to a force field if the boxes_for_simulation is equal to 1.
+
+ Generating an empty box (i.e., pdb and psf files):
+ Enter residues = [], but the accompanying structure must be an empty mb.Box.
+ However, when doing this, the forcefield_selection must be supplied,
+ or it will provide an error (i.e., forcefield_selection can not be equal to None).
+
+ In this current FF/psf/pdb writer, a residue type is essentially a molecule type.
+ Therefore, it can only correctly write systems where every bead/atom in the molecule
+ has the same residue name, and the residue name is specific to that molecule type.
+ For example: a protein molecule with many residue names is not currently supported,
+ but is planned to be supported in the future.
+ """
+ if has_foyer:
+ from foyer import Forcefield
+ from foyer.forcefields import forcefields
+ else:
+ error_msg = (
+ "Package foyer is not installed. "
+ "Please install it using conda install -c conda-forge foyer"
+ )
+ raise ImportError(error_msg)
+
+ # Validate inputs
+ _validate_boxes_for_simulation(boxes_for_simulation)
+ _validate_forcefield_selection_and_residues(forcefield_selection, residues)
+ new_gmso_topology, initial_no_atoms = _validate_structure(
+ structure, residues
+ )
+ gmso_compatible_forcefield_selection = _validate_forcefields(
+ forcefield_selection, residues
+ )
+
+ # can use match_ff_by="group" or "molecule", group was only chosen so everything is using the
+ # user selected mb.Compound.name...
+ gmso_apply(
+ new_gmso_topology,
+ gmso_compatible_forcefield_selection,
+ identify_connected_components=True,
+ identify_connections=True,
+ match_ff_by=gmso_match_ff_by,
+ use_molecule_info=True,
+ remove_untyped=True,
+ )
+ new_gmso_topology.update_topology()
+
+ # find mixing rule. If an empty.box mixing rule is set to None
+ combining_rule = (
+ new_gmso_topology._combining_rule
+ if isinstance(structure, Compound)
+ else None
+ )
+
+ # identify the bonded atoms and hence the molecule, label the GMSO objects
+ # and create the function outputs.
+ molecule_number = 0 # 0 sets the 1st molecule_number at 1
+ molecules_atom_number_dict = {}
+ unique_topology_groups_list = []
+ unique_topologies_groups_dict = {}
+ atom_types_dict = {}
+ bond_types_dict = {}
+ angle_types_dict = {}
+ dihedral_types_dict = {}
+ improper_types_dict = {}
+ nonBonded14Scale_dict = {}
+ electrostatics14Scale_dict = {}
+
+ for unique_group in new_gmso_topology.unique_site_labels(
+ gmso_match_ff_by, name_only=True
+ ):
+ if unique_group is not None:
+ unique_topology_groups_list.append(unique_group)
+
+ for unique_group in unique_topology_groups_list:
+ unique_subtop_group = new_gmso_topology.create_subtop(
+ label_type=gmso_match_ff_by, label=unique_group
+ )
+ unique_topologies_groups_dict[unique_group] = unique_subtop_group
+
+ nb_scalers_list = new_gmso_topology.get_lj_scale(
+ molecule_id=unique_group
+ )
+ electro_scalers_list = new_gmso_topology.get_electrostatics_scale(
+ molecule_id=unique_group
+ )
+ nonBonded14Scale_dict[unique_group] = (
+ None if nb_scalers_list is None else nb_scalers_list[2]
+ )
+ electrostatics14Scale_dict[unique_group] = (
+ None if electro_scalers_list is None else electro_scalers_list[2]
+ )
+
+ _cross_check_residues_and_unique_site_labels(
+ structure, residues, unique_topology_groups_list, boxes_for_simulation
+ )
+
+ # get all the bonded atoms, which is used for the bonded map to identify molecules
+ bonded_atom_number_set = set()
+ all_bonded_atoms_list = set()
+ for bond in new_gmso_topology.bonds:
+ bonded_atom_0_iter = new_gmso_topology.get_index(
+ bond.connection_members[0]
+ )
+ bonded_atom_1_iter = new_gmso_topology.get_index(
+ bond.connection_members[1]
+ )
+ bonded_atom_tuple_iter = sorted(
+ [bonded_atom_0_iter, bonded_atom_1_iter]
+ )
+ bonded_atom_number_set.add(tuple(bonded_atom_tuple_iter))
+ all_bonded_atoms_list.update(bonded_atom_tuple_iter)
+
+ # TODO: Refactor this section, might be able to use unique_site_by_labels(name_only=False)?
+ # map all bonded atoms as molecules
+ molecules_atom_number_list = []
+ for site_idx, site in enumerate(new_gmso_topology.sites):
+ if site_idx in all_bonded_atoms_list:
+ for bonded_atoms_n in bonded_atom_number_set:
+ if site_idx in bonded_atoms_n:
+ if len(molecules_atom_number_list) != 0:
+ for initiated_molecule in molecules_atom_number_list:
+ if site_idx in initiated_molecule:
+ initiated_molecule.update(bonded_atoms_n)
+ break
+ elif (
+ initiated_molecule
+ == molecules_atom_number_list[-1]
+ ):
+ molecules_atom_number_list.append(
+ {bonded_atoms_n[0], bonded_atoms_n[1]}
+ )
+ break
+ else:
+ molecules_atom_number_list.append(
+ {bonded_atoms_n[0], bonded_atoms_n[1]}
+ )
+ else:
+ molecules_atom_number_list.append({site_idx})
+
+ # create a molecule number to atom number dict
+ # Example: {molecule_number_x: {atom_number_1, ..., atom_number_y}, ...}
+ for molecule in molecules_atom_number_list:
+ molecules_atom_number_dict.update({molecule_number: molecule})
+ molecule_number += 1
+
+ for site in new_gmso_topology.sites:
+ site_atom_number_iter = new_gmso_topology.get_index(site)
+ # get molecule number
+ for mol_n, atom_set_n in molecules_atom_number_dict.items():
+ if site_atom_number_iter in atom_set_n:
+ molecule_p_number = mol_n
+
+ if gmso_match_ff_by == "group":
+ site.__dict__["residue_name_"] = site.__dict__["group_"]
+ elif gmso_match_ff_by == "molecule":
+ site.__dict__["residue_name_"] = site.__dict__["molecule_"].name
+
+ site.__dict__["residue_number_"] = molecule_p_number + 1
+
+ # create a topolgy only with the bonded parameters, including their residue/molecule type
+ # which permit force fielding in GOMC easier in the charmm_writer
+ # iterate thru the unique topologies and get the unique atom, bond, angle, dihedral, improper types
+ for (
+ unique_top_group_name_iter,
+ unique_top_iter,
+ ) in unique_topologies_groups_dict.items():
+ # get the unique non-bonded data, equations, and other info
+ atom_type_expression_set = set()
+ for atom_type in unique_top_iter.atom_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ):
+ atom_type.__dict__["tags_"] = {
+ "resname": unique_top_group_name_iter
+ }
+ atom_type_expression_set.add(atom_type.expression)
+ if len(atom_type_expression_set) == 1:
+ atom_types_dict.update(
+ {
+ unique_top_group_name_iter: {
+ "expression": list(atom_type_expression_set)[0],
+ "atom_types": unique_top_iter.atom_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ),
+ }
+ }
+ )
+ elif len(atom_type_expression_set) == 0:
+ atom_types_dict.update({unique_top_group_name_iter: None})
+ else:
+ raise ValueError(
+ "There is more than 1 nonbonded equation types per residue or molecules "
+ )
+
+ # get the unique bond data, equations, and other info
+ bond_type_expression_set = set()
+ for bond_type in unique_top_iter.bond_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ):
+ bond_type.__dict__["tags_"] = {
+ "resname": unique_top_group_name_iter
+ }
+ bond_type_expression_set.add(bond_type.expression)
+ if len(bond_type_expression_set) == 1:
+ bond_types_dict.update(
+ {
+ unique_top_group_name_iter: {
+ "expression": list(bond_type_expression_set)[0],
+ "bond_types": unique_top_iter.bond_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ),
+ }
+ }
+ )
+ elif len(bond_type_expression_set) == 0:
+ bond_types_dict.update({unique_top_group_name_iter: None})
+ else:
+ raise ValueError(
+ "There is more than 1 bond equation types per residue or molecules "
+ )
+
+ # get the unique angle data, equations, and other info
+ angle_type_expression_set = set()
+ for angle_type in unique_top_iter.angle_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ):
+ angle_type.__dict__["tags_"] = {
+ "resname": unique_top_group_name_iter
+ }
+ angle_type_expression_set.add(angle_type.expression)
+ if len(angle_type_expression_set) == 1:
+ angle_types_dict.update(
+ {
+ unique_top_group_name_iter: {
+ "expression": list(angle_type_expression_set)[0],
+ "angle_types": unique_top_iter.angle_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ),
+ }
+ }
+ )
+ elif len(angle_type_expression_set) == 0:
+ angle_types_dict.update({unique_top_group_name_iter: None})
+ else:
+ raise ValueError(
+ "There is more than 1 angle equation types per residue or molecules "
+ )
+
+ # get the unique dihedral data, equations, and other info
+ dihedral_type_expression_set = set()
+ for dihedral_type in unique_top_iter.dihedral_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ):
+ dihedral_type.__dict__["tags_"] = {
+ "resname": unique_top_group_name_iter
+ }
+ dihedral_type_expression_set.add(dihedral_type.expression)
+ if len(dihedral_type_expression_set) == 1:
+ dihedral_types_dict.update(
+ {
+ unique_top_group_name_iter: {
+ "expression": list(dihedral_type_expression_set)[0],
+ "dihedral_types": unique_top_iter.dihedral_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ),
+ }
+ }
+ )
+ elif len(dihedral_type_expression_set) == 0:
+ dihedral_types_dict.update({unique_top_group_name_iter: None})
+ else:
+ raise ValueError(
+ "There is more than 1 dihedral equation types per residue or molecules "
+ )
+
+ # get the unique improper data, equations, and other info
+ improper_type_expression_set = set()
+ for improper_type in unique_top_iter.improper_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ):
+ improper_type.__dict__["tags_"] = {
+ "resname": unique_top_group_name_iter
+ }
+ improper_type_expression_set.add(improper_type.expression)
+ if len(improper_type_expression_set) == 1:
+ improper_types_dict.update(
+ {
+ unique_top_group_name_iter: {
+ "expression": list(improper_type_expression_set)[0],
+ "improper_types": unique_top_iter.improper_types(
+ filter_by=PotentialFilters.UNIQUE_NAME_CLASS
+ ),
+ }
+ }
+ )
+ elif len(improper_type_expression_set) == 0:
+ improper_types_dict.update({unique_top_group_name_iter: None})
+ else:
+ raise ValueError(
+ "There is more than 1 improper equation types per residue or molecules "
+ )
+
+ # check to see if the non-bonded and electrostatic 1-4 interactions are in each group/molecule/residue
+ if unique_top_group_name_iter not in list(
+ nonBonded14Scale_dict.keys()
+ ) or unique_top_group_name_iter not in list(
+ electrostatics14Scale_dict.keys()
+ ):
+ raise ValueError(
+ f"The {unique_top_group_name_iter} residue is not provided for the "
+ f'{"nonBonded14Scale"} and {"electrostatics14Scale"} values'
+ )
+
+ topology = new_gmso_topology
+ # calculate the final number of atoms
+ final_no_atoms = topology.n_sites
+
+ if final_no_atoms != initial_no_atoms:
+ error_msg = (
+ "The initial number of atoms sent to the force field analysis is "
+ "not the same as the final number of atoms analyzed. "
+ f"The initial number of atoms was {initial_no_atoms}"
+ f"and the final number of atoms was {final_no_atoms}. "
+ "Please ensure that all the residues names that are in the initial "
+ "Compound are listed in the residues list "
+ "(i.e., the residues variable)."
+ )
+ raise ValueError(error_msg)
+
+ return [
+ topology,
+ unique_topology_groups_list,
+ electrostatics14Scale_dict,
+ nonBonded14Scale_dict,
+ atom_types_dict,
+ bond_types_dict,
+ angle_types_dict,
+ dihedral_types_dict,
+ improper_types_dict,
+ combining_rule,
+ ]
+
+
+def _validate_structure(structure, residues):
+ """Validate if input is an mb.Compound with initialized box or mb.Box."""
+ if isinstance(structure, (Compound, mb.Box)):
+ error_msg = f"The structure, {mb.Compound} or {mb.Box}, needs to have have box lengths and angles."
+ if isinstance(structure, Compound):
+ if structure.box is None:
+ raise TypeError(error_msg)
+
+ elif isinstance(structure, mb.Box):
+ if structure.lengths is None or structure.angles is None:
+ raise TypeError(error_msg)
+ else:
+ error_msg = (
+ "The structure expected to be of type: "
+ f"{mb.Compound} or {mb.Box}, received: {type(structure)}"
+ )
+ raise TypeError(error_msg)
+
+ # Check to see if it is an empty mbuild.Compound and set intial atoms to 0
+ # note empty mbuild.Compound will read 1 atoms but there is really noting there
+ # calculate the initial number of atoms for later comparison
+ # flatten the mbuild.compound, which is needed to get the mbuild.compound.names correctly from
+ # unflattend mbuild.compound.
+ # if mbuild.box do not flatten as it must be an empty box.
+ if isinstance(structure, Compound):
+ if len(structure.children) == 0:
+ # there are no real atoms in the Compound so the test fails. User should use mbuild.Box
+ error_msg = (
+ "If you are not providing an empty box, "
+ "you need to specify the atoms/beads as children in the mb.Compound. "
+ "If you are providing and empty box, please do so by specifying and "
+ f"mbuild Box ({mb.Box})"
+ )
+ raise TypeError(error_msg)
+
+ else:
+ initial_no_atoms = len(structure.to_parmed().atoms)
+ new_gmso_topology = mb_convert(structure, custom_groups=residues)
+ else:
+ initial_no_atoms = 0
+ lengths = structure.lengths
+ angles = structure.angles
+
+ # create a new empty gmso topology. This is needed because an empty mbuild.Compound
+ # can not be converted to gmso.Topology without counting at 1 atom
+ new_gmso_topology = gmso.Topology()
+ new_gmso_topology.box = gmso.Box(lengths=lengths, angles=angles)
+
+ return new_gmso_topology, initial_no_atoms
+
+
+def _validate_forcefield_selection_and_residues(forcefield_selection, residues):
+ """Validate if input forcefield_selection and reisudes is of the correct form."""
+ if forcefield_selection is None:
+ error_msg = (
+ "Please the force field selection (forcefield_selection) as a dictionary "
+ "with all the residues specified to a force field "
+ '-> Ex: {"Water": "oplsaa", "OCT": "path/trappe-ua.xml"}, '
+ "Note: the file path must be specified the force field file "
+ "or by using the standard force field name provided the `foyer` package."
+ )
+ raise TypeError(error_msg)
+
+ elif not isinstance(forcefield_selection, dict):
+ error_msg = (
+ "The force field selection (forcefield_selection) "
+ "is not a dictionary. Please enter a dictionary "
+ "with all the residues specified to a force field "
+ '-> Ex: {"Water": "oplsaa", "OCT": "path/trappe-ua.xml"}, '
+ "Note: the file path must be specified the force field file "
+ "or by using the standard force field name provided the `foyer` package."
+ )
+ raise TypeError(error_msg)
+
+ if not isinstance(residues, (list, tuple)):
+ error_msg = (
+ "Please enter the residues list in the specific_ff_to_residue."
+ )
+ raise TypeError(error_msg)
+
+ forcefield_keys_list = list(forcefield_selection.keys())
+ if forcefield_keys_list == [] and len(residues) != 0:
+ print_error_message = "The forcefield_selection variable are not provided, but there are residues provided."
+ raise ValueError(print_error_message)
+
+ elif forcefield_keys_list != [] and len(residues) == 0:
+ print_error_message = (
+ "The residues variable is an empty list but there are "
+ "forcefield_selection variables provided."
+ )
+ raise ValueError(print_error_message)
+
+
+def _validate_boxes_for_simulation(boxes_for_simulation):
+ """Validate if input boxes_for_simulation is of the correct form."""
+ if boxes_for_simulation not in [1, 2]:
+ boxes_for_simulation_error_msg = (
+ "boxes_for_simulation must be either 1 or 2."
+ )
+ raise ValueError(boxes_for_simulation_error_msg)
+
+
+def _validate_forcefields(forcefield_selection, residues):
+ """Validate and create GMSO ForceField object from the forcefield_selection."""
+ if has_foyer:
+ from foyer import Forcefield
+ from foyer.forcefields import forcefields
+
+ forcefield_keys_list = list(forcefield_selection.keys())
+ user_entered_ff_with_path_dict = {}
+ # True means user entered the path, False is a standard foyer FF with no path
+ for residue in residues:
+ if residue in forcefield_keys_list:
+ ff_extension = os.path.splitext(forcefield_selection[residue])[1]
+ if ff_extension == ".xml":
+ user_entered_ff_with_path_dict[residue] = True
+ elif ff_extension == "":
+ user_entered_ff_with_path_dict[residue] = False
+ else:
+ error_msg = "Please make sure you are enterning the correct FF name or path with xml extension"
+ # "Please make sure you are entering the correct foyer FF name or a path to a FF file (with .xml extension)."
+ raise ValueError(error_msg)
+
+ # check if FF files exist and create a forcefield selection with directory paths
+ # forcefield_selection_with_paths
+ forcefield_selection_with_paths = {}
+ for residue in forcefield_keys_list:
+ ff_for_residue = forcefield_selection[residue]
+ if user_entered_ff_with_path_dict[residue]:
+ ff_names_path_iteration = forcefield_selection[residue]
+ try:
+ read_xlm_iteration = minidom.parse(ff_names_path_iteration)
+ forcefield_selection_with_paths[
+ residue
+ ] = ff_names_path_iteration
+
+ except:
+ error_msg = (
+ "Please make sure you are entering the correct foyer FF path, "
+ "including the FF file name.xml. "
+ "If you are using the pre-build FF files in foyer, "
+ "only use the string name without any extension. "
+ "The selected FF file could also could not formated properly, or "
+ "there may be errors in the FF file itself."
+ )
+ raise ValueError(error_msg)
+ elif not user_entered_ff_with_path_dict[residue]:
+ ff_for_residue = forcefield_selection[residue]
+ ff_names_path_iteration = (
+ f"{forcefields.get_ff_path()[0]}/xml/{ff_for_residue}.xml"
+ )
+ try:
+ read_xlm_iteration = minidom.parse(ff_names_path_iteration)
+ forcefield_selection_with_paths[
+ residue
+ ] = ff_names_path_iteration
+ except:
+ error_msg = (
+ "Please make sure you are entering the correct foyer FF name, or the "
+ "correct file extension (i.e., .xml, if required)."
+ )
+ raise ValueError(error_msg)
+
+ # push the FF paths and/or name to the GMSO format and create the new GMSO topology format
+ gmso_compatable_forcefield_selection = {}
+ for ff_key_iter, ff_value_iter in forcefield_selection_with_paths.items():
+ # try to load the Foyer and GMSO FFs, if Foyer convert to GMSO; otherwise, it is an error.
+ try:
+ try:
+ ff_new_gmso_value_iter = FoyerFFs.get_ff(
+ ff_value_iter
+ ).to_gmso_ff()
+ except:
+ ff_new_gmso_value_iter = GMSOFFs.get_ff(
+ ff_value_iter
+ ).to_gmso_ff()
+
+ except:
+ error_msg = (
+ f"The supplied force field xml for the "
+ f"{ff_key_iter} residue is not a foyer or gmso xml, "
+ f"or the xml has errors and it not able to load properly."
+ )
+ raise TypeError(error_msg)
+
+ gmso_compatable_forcefield_selection.update(
+ {ff_key_iter: ff_new_gmso_value_iter}
+ )
+ return gmso_compatable_forcefield_selection
+
+
+def _cross_check_residues_and_unique_site_labels(
+ structure, residues, unique_topology_groups_list, boxes_for_simulation
+):
+ """Cross checking the residues list and unique_site_labels list."""
+ # Check that all residues in box are in the residue names
+ if isinstance(structure, mb.Compound):
+ for applied_res_i in unique_topology_groups_list:
+ if applied_res_i not in residues:
+ error_msg_all_res_not_specified = (
+ f"All the residues are not specified in the residue list, or "
+ f"the {applied_res_i} residue does not match the residues that "
+ f"were found in the foyer and GMSO force field application. "
+ )
+ raise ValueError(error_msg_all_res_not_specified)
+
+ # check if all the molecules/residues were found in in the mb.Compound/allowable input
+ msg2 = (
+ "All the residues were not used from the forcefield_selection "
+ "string or dictionary. There may be residues below other "
+ "specified residues in the mbuild.Compound hierarchy. "
+ "If so, all the highest listed residues pass down the force "
+ "fields through the hierarchy. Alternatively, residues that "
+ "are not in the structure may have been specified. "
+ )
+ msg3 = (
+ f"NOTE: This warning will appear if you are using the CHARMM pdb and psf writers "
+ f"2 boxes, and the boxes do not contain all the residues in each box."
+ )
+ for res_i in residues:
+ if res_i not in unique_topology_groups_list:
+ msg1 = f"The {res_i} residues were not used from the forcefield_selection string or dictionary. "
+ if boxes_for_simulation == 1:
+ raise ValueError(f"{msg1}{msg2}")
+ if boxes_for_simulation == 2:
+ warn(f"{msg1}{msg2}{msg3}")
From 35823c1a2ec24749e5116522fda5738b9531bc47 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 24 Jan 2023 11:21:12 -0600
Subject: [PATCH 097/141] [pre-commit.ci] pre-commit autoupdate (#708)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pycqa/pydocstyle: 6.2.3 → 6.3.0](https://github.com/pycqa/pydocstyle/compare/6.2.3...6.3.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 843025202..edf3c69dc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,7 +27,7 @@ repos:
name: isort (python)
args: [--profile=black, --line-length=80]
- repo: https://github.com/pycqa/pydocstyle
- rev: '6.2.3'
+ rev: '6.3.0'
hooks:
- id: pydocstyle
exclude: ^(gmso/abc|gmso/core|gmso/tests/|docs/|devtools/|setup.py)
From fae79181b7e05399c3fbfc71f9b41d1fe291787c Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 31 Jan 2023 11:38:52 -0600
Subject: [PATCH 098/141] [pre-commit.ci] pre-commit autoupdate (#712)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pycqa/isort: 5.11.4 → 5.12.0](https://github.com/pycqa/isort/compare/5.11.4...5.12.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index edf3c69dc..e16ab57d0 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -21,7 +21,7 @@ repos:
- id: black
args: [--line-length=80]
- repo: https://github.com/pycqa/isort
- rev: 5.11.4
+ rev: 5.12.0
hooks:
- id: isort
name: isort (python)
From 089db6f9cb2173fc52205a104a33100a02ba4e86 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 7 Feb 2023 10:07:57 -0600
Subject: [PATCH 099/141] [pre-commit.ci] pre-commit autoupdate (#713)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0)
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
gmso/core/angle_type.py | 1 -
gmso/core/dihedral_type.py | 1 -
gmso/core/forcefield.py | 1 -
gmso/core/topology.py | 2 --
gmso/external/convert_openmm.py | 1 -
gmso/formats/lammpsdata.py | 1 -
gmso/formats/mcf.py | 5 ++---
gmso/tests/test_expression.py | 1 -
gmso/tests/test_internal_conversions.py | 3 ---
gmso/tests/test_reference_xmls.py | 2 +-
gmso/tests/test_specific_ff_to_residue.py | 1 -
12 files changed, 4 insertions(+), 17 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e16ab57d0..56bbcb90b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
- id: trailing-whitespace
exclude: 'setup.cfg'
- repo: https://github.com/psf/black
- rev: 22.12.0
+ rev: 23.1.0
hooks:
- id: black
args: [--line-length=80]
diff --git a/gmso/core/angle_type.py b/gmso/core/angle_type.py
index c19c2ab76..0fa53df58 100644
--- a/gmso/core/angle_type.py
+++ b/gmso/core/angle_type.py
@@ -47,7 +47,6 @@ def __init__(
member_classes=None,
tags=None,
):
-
super(AngleType, self).__init__(
name=name,
expression=expression,
diff --git a/gmso/core/dihedral_type.py b/gmso/core/dihedral_type.py
index 9a31967d7..4740136cc 100644
--- a/gmso/core/dihedral_type.py
+++ b/gmso/core/dihedral_type.py
@@ -53,7 +53,6 @@ def __init__(
member_classes=None,
tags=None,
):
-
super(DihedralType, self).__init__(
name=name,
expression=expression,
diff --git a/gmso/core/forcefield.py b/gmso/core/forcefield.py
index 032687a59..e7f86699b 100644
--- a/gmso/core/forcefield.py
+++ b/gmso/core/forcefield.py
@@ -501,7 +501,6 @@ def _get_improper_type(
for eq, order in zip(equivalent, equiv_idx):
equiv_patterns = mask_with(eq, i)
for equiv_pattern in equiv_patterns:
-
equiv_pattern_key = FF_TOKENS_SEPARATOR.join(
equiv_pattern
)
diff --git a/gmso/core/topology.py b/gmso/core/topology.py
index 36f2a1316..11a4c4c86 100644
--- a/gmso/core/topology.py
+++ b/gmso/core/topology.py
@@ -135,7 +135,6 @@ class Topology(object):
"""
def __init__(self, name="Topology", box=None):
-
self.name = name
self._box = box
self._sites = IndexedSet()
@@ -1217,7 +1216,6 @@ def iter_sites(self, key, value):
The site where getattr(site, key) == value
"""
if key not in Site.__iterable_attributes__:
-
raise ValueError(
f"`{key}` is not an iterable attribute for Site. "
f"To check what the iterable attributes are see gmso.abc.abstract_site module."
diff --git a/gmso/external/convert_openmm.py b/gmso/external/convert_openmm.py
index 5ea7740d0..71c1ab121 100644
--- a/gmso/external/convert_openmm.py
+++ b/gmso/external/convert_openmm.py
@@ -53,7 +53,6 @@ def to_openmm(topology, openmm_object="topology"):
# TODO: Convert connections to OpenMM Bonds
if openmm_object == "topology":
-
return openmm_top
else:
diff --git a/gmso/formats/lammpsdata.py b/gmso/formats/lammpsdata.py
index be4448063..a4d826491 100644
--- a/gmso/formats/lammpsdata.py
+++ b/gmso/formats/lammpsdata.py
@@ -211,7 +211,6 @@ def write_lammpsdata(topology, filename, atom_style="full"):
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"
diff --git a/gmso/formats/mcf.py b/gmso/formats/mcf.py
index fb745157d..c4d5ade87 100644
--- a/gmso/formats/mcf.py
+++ b/gmso/formats/mcf.py
@@ -61,7 +61,6 @@ def write_mcf(top, filename):
# Now we write the MCF file
with open(filename, "w") as mcf:
-
header = (
"!***************************************"
"****************************************\n"
@@ -279,7 +278,7 @@ def _write_atom_information(mcf, top, in_ring):
mcf.write(header)
mcf.write("{:d}\n".format(len(top.sites)))
- for (idx, site) in enumerate(top.sites):
+ for idx, site in enumerate(top.sites):
mcf.write(
"{:<4d} "
"{:<6s} "
@@ -438,7 +437,7 @@ def _write_dihedral_information(mcf, top):
# TODO: Are impropers buried in dihedrals?
mcf.write("{:d}\n".format(len(top.dihedrals)))
- for (idx, dihedral) in enumerate(top.dihedrals):
+ for idx, dihedral in enumerate(top.dihedrals):
mcf.write(
"{:<4d} "
"{:<4d} "
diff --git a/gmso/tests/test_expression.py b/gmso/tests/test_expression.py
index b8a1a35ed..f7da1385e 100644
--- a/gmso/tests/test_expression.py
+++ b/gmso/tests/test_expression.py
@@ -257,7 +257,6 @@ def test_from_non_parametric(self):
)
def test_from_non_parametric_errors(self):
-
with pytest.raises(
TypeError,
match="Expected