From 91bc16851e09eb087247551bcb19025a257c44b9 Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Fri, 18 Jun 2021 10:50:45 -0500 Subject: [PATCH 01/12] WIP- #552, Initial serialization attempt --- gmso/abc/gmso_base.py | 7 +++ gmso/core/subtopology.py | 5 ++ gmso/core/topology.py | 108 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+) diff --git a/gmso/abc/gmso_base.py b/gmso/abc/gmso_base.py index 8d280f75a..9a48591ea 100644 --- a/gmso/abc/gmso_base.py +++ b/gmso/abc/gmso_base.py @@ -93,6 +93,13 @@ def json(self, **kwargs): return super(GMSOBase, self).json(**kwargs) + def json_dict(self, **kwargs): + """Return a JSON serializable dictionary from the object""" + import json + + raw_json = self.json(**kwargs) + return json.loads(raw_json) + @classmethod def validate(cls, value): """Ensure that the object is validated before use.""" diff --git a/gmso/core/subtopology.py b/gmso/core/subtopology.py index f75341562..b565d59ff 100644 --- a/gmso/core/subtopology.py +++ b/gmso/core/subtopology.py @@ -124,6 +124,11 @@ def __str__(self): f"id: {id(self)}>" ) + def json(self): + import json + + return {"id": f"{id(self)}", "atoms": json.loads()} + def _validate_parent(parent): """Ensure the parent is a topology.""" diff --git a/gmso/core/topology.py b/gmso/core/topology.py index 648e05817..b840bf5d4 100644 --- a/gmso/core/topology.py +++ b/gmso/core/topology.py @@ -757,6 +757,110 @@ 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 json(self, types=False, update=False): + """Return a json serializable dictionary of this topology. + + This is used for json serializing the topology + + Parameters + ---------- + types: bool, default=False + If true, include type info (i.e. Potentials) + update: bool, default=False + If true, update the topology before iterating through the files + + Returns + ------- + dict + A json serializable dictionary representing members of this Topology + """ + if types and not self.is_typed(): + raise ValueError( + "Cannot incorporate types because the topology is not typed." + ) + if update: + self.update_topology() + + json_dict = { + "name": self._name, + "scaling_factors": self.scaling_factors, + "subtopolgies": [], + "atoms": [], + "bonds": [], + "angles": [], + "dihedrals": [], + "impropers": [], + "atom_types": [], + "bond_types": [], + "angle_types": [], + "dihedral_types": [], + "improper_types": [], + } + + for atom in self._sites: + atom_dict = atom.json_dict(exclude={"atom_type"}) + if types and atom.atom_type: + # if not potentials.get(id(atom.atom_type)): + # potentials[id(atom.atom_type)] = [] + # potentials[id(atom.atom_type)].append(id(atom)) + atom_dict["atom_type"] = id(atom.atom_type) + + json_dict["atoms"].append(atom_dict) + + targets = { + Bond: json_dict["bonds"], + Angle: json_dict["angles"], + Dihedral: json_dict["dihedrals"], + Improper: json_dict["impropers"], + AtomType: json_dict["atom_types"], + BondType: json_dict["bond_types"], + AngleType: json_dict["angle_types"], + DihedralType: json_dict["dihedral_types"], + ImproperType: json_dict["improper_types"], + } + + for connections, exclude_attr in [ + (self._bonds, "bond_type"), + (self._angles, "angle_type"), + (self._dihedrals, "dihedral_type"), + (self._impropers, "improper_type"), + ]: + for connection in connections: + connection_dict = connection.json_dict( + exclude={exclude_attr, "connection_members"} + ) + target = targets[type(connection)] + connection_dict["connection_members"] = [ + self.get_index(member) + for member in connection.connection_members + ] + target.append(connection_dict) + connection_type = getattr(connection, exclude_attr) + if types and connection_type: + # if not potentials.get(id(connection_type)): + # potentials[id(connection_type)] = [] + # potentials[id(connection_type)].append( + # id(connection) + # ) + connection_dict[exclude_attr] = id(connection_type) + + for potentials in [ + self._atom_types.values(), + self._bond_types.values(), + self._angle_types.values(), + self._dihedral_types.values(), + self._improper_types.values(), + ]: + for potential in potentials: + potential_dict = potential.json_dict( + exclude={"topology", "set_ref"} + ) + target = targets[type(potential)] + potential_dict["id"] = id(potential) + target.append(potential_dict) + + return json_dict + def __repr__(self): """Return custom format to represent topology.""" return ( @@ -769,3 +873,7 @@ def __repr__(self): def __str__(self): """Return custom format to represent topology as a string.""" return f"" + + @classmethod + def from_json(cls, json_file_or_dict): + pass From c739c57fcfb2831c5272147ccc3dd3238c459096 Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Thu, 1 Jul 2021 14:54:12 -0500 Subject: [PATCH 02/12] WIP- Move serialization code to json.py --- gmso/formats/json.py | 173 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 gmso/formats/json.py diff --git a/gmso/formats/json.py b/gmso/formats/json.py new file mode 100644 index 000000000..5cf34061a --- /dev/null +++ b/gmso/formats/json.py @@ -0,0 +1,173 @@ +"""Serialization to json.""" +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 + + +def to_json(top, types=False, update=False): + """Return a json serializable from a topology. + + This is used for json serializing the topology + + Parameters + ---------- + top: gmso.Topology, required + The topology + types: bool, default=False + If true, include type info (i.e. Potentials) + update: bool, default=False + If true, update the topology before iterating through the files + + Returns + ------- + dict + A json serializable dictionary representing members of this Topology + """ + if types and not top.is_typed(): + raise ValueError( + "Cannot incorporate types because the topology is not typed." + ) + if update: + top.update_topology() + + json_dict = { + "name": top._name, + "scaling_factors": top.scaling_factors, + "subtopologies": [], + "atoms": [], + "bonds": [], + "angles": [], + "dihedrals": [], + "impropers": [], + "atom_types": [], + "bond_types": [], + "angle_types": [], + "dihedral_types": [], + "improper_types": [], + } + + for atom in top._sites: + atom_dict = atom.json_dict(exclude={"atom_type"}) + if types and atom.atom_type: + atom_dict["atom_type"] = id(atom.atom_type) + + json_dict["atoms"].append(atom_dict) + + targets = { + Bond: json_dict["bonds"], + Angle: json_dict["angles"], + Dihedral: json_dict["dihedrals"], + Improper: json_dict["impropers"], + AtomType: json_dict["atom_types"], + BondType: json_dict["bond_types"], + AngleType: json_dict["angle_types"], + DihedralType: json_dict["dihedral_types"], + ImproperType: json_dict["improper_types"], + } + + for connections, exclude_attr in [ + (top._bonds, "bond_type"), + (top._angles, "angle_type"), + (top._dihedrals, "dihedral_type"), + (top._impropers, "improper_type"), + ]: + for connection in connections: + connection_dict = connection.json_dict( + exclude={exclude_attr, "connection_members"} + ) + target = targets[type(connection)] + connection_dict["connection_members"] = [ + top.get_index(member) + for member in connection.connection_members + ] + target.append(connection_dict) + connection_type = getattr(connection, exclude_attr) + if types and connection_type: + 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(), + ]: + for potential in potentials: + potential_dict = potential.json_dict( + exclude={"topology", "set_ref"} + ) + target = targets[type(potential)] + potential_dict["id"] = id(potential) + target.append(potential_dict) + + for subtop in top.subtops: + subtop_dict = subtop.json_dict() + json_dict["subtopologies"].append(subtop_dict) + + return json_dict + + +def from_json(json_dict): + """Convert a json_dict into a topology. + + Parameters + ---------- + json_dict: dict + The json (dictionary) representation of a Topology + + Returns + ------- + gmso.Topology + the equivalent Topology representation from the dictionary + """ + from gmso.core.topology import Topology + + top = Topology( + name=json_dict["name"], + ) + top.scaling_factors = json_dict["scaling_factors"] + for atom_dict in json_dict["atoms"]: + atom = Atom.parse_obj(atom_dict) + top.add_site(atom) + + for bond_dict in json_dict["bonds"]: + bond_dict["connection_members"] = [ + top._sites[member_idx] + for member_idx in bond_dict["connection_members"] + ] + bond = Bond.parse_obj(bond_dict) + top.add_connection(bond) + + for angle_dict in json_dict["angles"]: + angle_dict["connection_members"] = [ + top._sites[member_idx] + for member_idx in bond_dict["connection_members"] + ] + angle = Angle.parse_obj(angle_dict) + top.add_connection(angle) + + for dihedral_dict in json_dict["dihedrals"]: + dihedral_dict["connection_members"] = [ + top._sites[member_idx] + for member_idx in bond_dict["connection_members"] + ] + dihedral = Dihedral.parse_obj(dihedral_dict) + top.add_connection(dihedral) + + for improper_dict in json_dict["impropers"]: + improper_dict["connection_members"] = [ + top._sites[member_idx] + for member_idx in bond_dict["connection_members"] + ] + improper = Improper.parse_obj(improper_dict) + top.add_connection(improper) + + top.update_topology() + return top From ea035055a8b242f95c8017c6014a0b359ef46066 Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Thu, 1 Jul 2021 15:11:15 -0500 Subject: [PATCH 03/12] WIP- Add framework for topology conversion --- gmso/formats/json.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/gmso/formats/json.py b/gmso/formats/json.py index 5cf34061a..e1958282f 100644 --- a/gmso/formats/json.py +++ b/gmso/formats/json.py @@ -133,9 +133,15 @@ def from_json(json_dict): name=json_dict["name"], ) top.scaling_factors = json_dict["scaling_factors"] + id_to_type_map = {} for atom_dict in json_dict["atoms"]: + atom_type_id = atom_dict.pop("atom_type", None) atom = Atom.parse_obj(atom_dict) top.add_site(atom) + if atom_type_id: + if not id_to_type_map[atom_type_id]: + id_to_type_map[atom_type_id] = [] + id_to_type_map[atom_type_id].append(atom) for bond_dict in json_dict["bonds"]: bond_dict["connection_members"] = [ @@ -148,7 +154,7 @@ def from_json(json_dict): for angle_dict in json_dict["angles"]: angle_dict["connection_members"] = [ top._sites[member_idx] - for member_idx in bond_dict["connection_members"] + for member_idx in angle_dict["connection_members"] ] angle = Angle.parse_obj(angle_dict) top.add_connection(angle) @@ -156,7 +162,7 @@ def from_json(json_dict): for dihedral_dict in json_dict["dihedrals"]: dihedral_dict["connection_members"] = [ top._sites[member_idx] - for member_idx in bond_dict["connection_members"] + for member_idx in dihedral_dict["connection_members"] ] dihedral = Dihedral.parse_obj(dihedral_dict) top.add_connection(dihedral) @@ -164,10 +170,17 @@ def from_json(json_dict): for improper_dict in json_dict["impropers"]: improper_dict["connection_members"] = [ top._sites[member_idx] - for member_idx in bond_dict["connection_members"] + for member_idx in improper_dict["connection_members"] ] improper = Improper.parse_obj(improper_dict) top.add_connection(improper) + for atom_type_dict in json_dict["atom_types"]: + atom_type_id = atom_type_dict.pop("id", None) + atom_type = AtomType.parse_obj(atom_type_dict) + if atom_type_id in id_to_type_map: + for associated_atom in id_to_type_map[atom_type_id]: + associated_atom.atom_type = atom_type + top.update_topology() return top From d29a40442588afd401bc3f0e92b3c68f17464de2 Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Wed, 7 Jul 2021 11:28:01 -0500 Subject: [PATCH 04/12] WIP- Full serialization and tests ready --- gmso/core/subtopology.py | 10 ++- gmso/core/topology.py | 133 ++++++++----------------------- gmso/formats/json.py | 52 +++++++++++- gmso/tests/base_test.py | 41 +++++++++- gmso/tests/test_serialization.py | 45 +++++++++++ 5 files changed, 175 insertions(+), 106 deletions(-) diff --git a/gmso/core/subtopology.py b/gmso/core/subtopology.py index b565d59ff..271022a98 100644 --- a/gmso/core/subtopology.py +++ b/gmso/core/subtopology.py @@ -124,10 +124,14 @@ def __str__(self): f"id: {id(self)}>" ) - def json(self): - import json + def json_dict(self): + """Return a json serializable dictionary of this subtopology.""" + subtop_dict = {"name": self.name, "atoms": []} - return {"id": f"{id(self)}", "atoms": json.loads()} + for site in self._sites: + subtop_dict["atoms"].append(self.parent.get_index(site)) + + return subtop_dict def _validate_parent(parent): diff --git a/gmso/core/topology.py b/gmso/core/topology.py index b840bf5d4..165e3e77b 100644 --- a/gmso/core/topology.py +++ b/gmso/core/topology.py @@ -1,5 +1,7 @@ """Base data structure for GMSO chemical systems.""" +import json import warnings +from pathlib import Path import numpy as np import unyt as u @@ -757,109 +759,37 @@ 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 json(self, types=False, update=False): - """Return a json serializable dictionary of this topology. - - This is used for json serializing the topology + def save(self, filename, overwrite=True, **kwargs): + """Save the topology to a file. Parameters ---------- - types: bool, default=False - If true, include type info (i.e. Potentials) - update: bool, default=False - If true, update the topology before iterating through the files - - Returns - ------- - dict - A json serializable dictionary representing members of this Topology + filename: str, pathlib.Path + The file to save the topology as + overwrite: bool, default=True + If True, overwrite the existing file if it exists + **kwargs: + The arguments to specific file savers listed below(as extensions): + * json: types, update, indent """ - if types and not self.is_typed(): - raise ValueError( - "Cannot incorporate types because the topology is not typed." - ) - if update: - self.update_topology() + if not isinstance(filename, Path): + filename = Path(filename).resolve() - json_dict = { - "name": self._name, - "scaling_factors": self.scaling_factors, - "subtopolgies": [], - "atoms": [], - "bonds": [], - "angles": [], - "dihedrals": [], - "impropers": [], - "atom_types": [], - "bond_types": [], - "angle_types": [], - "dihedral_types": [], - "improper_types": [], - } - - for atom in self._sites: - atom_dict = atom.json_dict(exclude={"atom_type"}) - if types and atom.atom_type: - # if not potentials.get(id(atom.atom_type)): - # potentials[id(atom.atom_type)] = [] - # potentials[id(atom.atom_type)].append(id(atom)) - atom_dict["atom_type"] = id(atom.atom_type) - - json_dict["atoms"].append(atom_dict) - - targets = { - Bond: json_dict["bonds"], - Angle: json_dict["angles"], - Dihedral: json_dict["dihedrals"], - Improper: json_dict["impropers"], - AtomType: json_dict["atom_types"], - BondType: json_dict["bond_types"], - AngleType: json_dict["angle_types"], - DihedralType: json_dict["dihedral_types"], - ImproperType: json_dict["improper_types"], - } + if filename.exists() and not overwrite: + raise FileExistsError( + f"The file {filename} exists. Please set " + f"overwrite=True if you wish to overwrite the existing file" + ) - for connections, exclude_attr in [ - (self._bonds, "bond_type"), - (self._angles, "angle_type"), - (self._dihedrals, "dihedral_type"), - (self._impropers, "improper_type"), - ]: - for connection in connections: - connection_dict = connection.json_dict( - exclude={exclude_attr, "connection_members"} - ) - target = targets[type(connection)] - connection_dict["connection_members"] = [ - self.get_index(member) - for member in connection.connection_members - ] - target.append(connection_dict) - connection_type = getattr(connection, exclude_attr) - if types and connection_type: - # if not potentials.get(id(connection_type)): - # potentials[id(connection_type)] = [] - # potentials[id(connection_type)].append( - # id(connection) - # ) - connection_dict[exclude_attr] = id(connection_type) - - for potentials in [ - self._atom_types.values(), - self._bond_types.values(), - self._angle_types.values(), - self._dihedral_types.values(), - self._improper_types.values(), - ]: - for potential in potentials: - potential_dict = potential.json_dict( - exclude={"topology", "set_ref"} - ) - target = targets[type(potential)] - potential_dict["id"] = id(potential) - target.append(potential_dict) + if filename.suffix == ".json": + from gmso.formats.json import _to_json - return json_dict + types = kwargs.get("types", False) + update = kwargs.get("update", True) + indent = kwargs.get("indent", 2) + top_json = _to_json(self, types=types, update=update) + with filename.open("w") as top_json_file: + json.dump(top_json, top_json_file, indent=indent) def __repr__(self): """Return custom format to represent topology.""" @@ -875,5 +805,12 @@ def __str__(self): return f"" @classmethod - def from_json(cls, json_file_or_dict): - pass + def load(cls, filename): + """Load a file to a topology""" + filename = Path(filename).resolve() + if filename.suffix == ".json": + from gmso.formats.json import _from_json + + with filename.open("r") as json_file: + top = _from_json(json.load(json_file)) + return top diff --git a/gmso/formats/json.py b/gmso/formats/json.py index e1958282f..8d3a66d2a 100644 --- a/gmso/formats/json.py +++ b/gmso/formats/json.py @@ -1,4 +1,6 @@ """Serialization to json.""" +from copy import deepcopy + from gmso.core.angle import Angle from gmso.core.angle_type import AngleType from gmso.core.atom import Atom @@ -11,7 +13,7 @@ from gmso.core.improper_type import ImproperType -def to_json(top, types=False, update=False): +def _to_json(top, types=False, update=True): """Return a json serializable from a topology. This is used for json serializing the topology @@ -114,7 +116,7 @@ def to_json(top, types=False, update=False): return json_dict -def from_json(json_dict): +def _from_json(json_dict): """Convert a json_dict into a topology. Parameters @@ -127,8 +129,12 @@ def from_json(json_dict): gmso.Topology the equivalent Topology representation from the dictionary """ + from gmso.core.subtopology import SubTopology from gmso.core.topology import Topology + # FixMe: DeepCopying a dictionary might not be the most efficient + json_dict = deepcopy(json_dict) + top = Topology( name=json_dict["name"], ) @@ -139,41 +145,60 @@ def from_json(json_dict): atom = Atom.parse_obj(atom_dict) top.add_site(atom) if atom_type_id: - if not id_to_type_map[atom_type_id]: + if not id_to_type_map.get(atom_type_id): id_to_type_map[atom_type_id] = [] id_to_type_map[atom_type_id].append(atom) for bond_dict in json_dict["bonds"]: + bond_type_id = bond_dict.pop("bond_type", None) bond_dict["connection_members"] = [ top._sites[member_idx] for member_idx in bond_dict["connection_members"] ] bond = Bond.parse_obj(bond_dict) top.add_connection(bond) + if bond_type_id: + if not id_to_type_map.get(bond_type_id): + id_to_type_map[bond_type_id] = [] + id_to_type_map[bond_type_id].append(bond) for angle_dict in json_dict["angles"]: + angle_type_id = angle_dict.pop("angle_type", None) angle_dict["connection_members"] = [ top._sites[member_idx] for member_idx in angle_dict["connection_members"] ] angle = Angle.parse_obj(angle_dict) top.add_connection(angle) + if angle_type_id: + if not id_to_type_map.get(angle_type_id): + id_to_type_map[angle_type_id] = [] + id_to_type_map[angle_type_id].append(angle) for dihedral_dict in json_dict["dihedrals"]: + dihedral_type_id = dihedral_dict.pop("dihedral_type", None) dihedral_dict["connection_members"] = [ top._sites[member_idx] for member_idx in dihedral_dict["connection_members"] ] dihedral = Dihedral.parse_obj(dihedral_dict) top.add_connection(dihedral) + if dihedral_type_id: + if not id_to_type_map.get(dihedral_type_id): + id_to_type_map[dihedral_type_id] = [] + id_to_type_map[dihedral_type_id].append(dihedral) for improper_dict in json_dict["impropers"]: + improper_type_id = improper_dict.pop("improper_type", None) improper_dict["connection_members"] = [ top._sites[member_idx] for member_idx in improper_dict["connection_members"] ] improper = Improper.parse_obj(improper_dict) - top.add_connection(improper) + if improper_type_id: + if not id_to_type_map.get(improper_type_id): + id_to_type_map[improper_type_id] = [] + id_to_type_map[improper_type_id].append(improper) for atom_type_dict in json_dict["atom_types"]: atom_type_id = atom_type_dict.pop("id", None) @@ -182,5 +207,24 @@ def from_json(json_dict): for associated_atom in id_to_type_map[atom_type_id]: associated_atom.atom_type = atom_type + for connection_types, Creator, attr in [ + (json_dict["bond_types"], BondType, "bond_type"), + (json_dict["angle_types"], AngleType, "angle_type"), + (json_dict["dihedral_types"], DihedralType, "dihedral_type"), + (json_dict["improper_types"], ImproperType, "improper_type"), + ]: + for connection_type_dict in connection_types: + connection_type_id = connection_type_dict.pop("id") + connection_type = Creator.parse_obj(connection_type_dict) + if connection_type_id in id_to_type_map: + for associated_connection in id_to_type_map[connection_type_id]: + setattr(associated_connection, attr, connection_type) + + for subtop_dict in json_dict["subtopologies"]: + subtop = SubTopology(name=subtop_dict["name"]) + for atom_idx in subtop_dict["atoms"]: + subtop.add_site(top.sites[atom_idx]) + top.add_subtopology(subtop, update=False) + top.update_topology() return top diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py index a7461af14..46da2321f 100644 --- a/gmso/tests/base_test.py +++ b/gmso/tests/base_test.py @@ -10,8 +10,10 @@ from gmso.core.atom_type import AtomType from gmso.core.bond import Bond from gmso.core.box import Box +from gmso.core.dihedral import Dihedral from gmso.core.element import Hydrogen, Oxygen from gmso.core.forcefield import ForceField +from gmso.core.improper import Improper from gmso.core.topology import Topology from gmso.external import from_mbuild, from_parmed from gmso.external.convert_foyer_xml import from_foyer_xml @@ -323,7 +325,7 @@ def ethane(self): return mytop - @pytest.fixture(scope="module") + @pytest.fixture(scope="session") def are_equivalent_atoms(self): def test_atom_equality(atom1, atom2): if not all(isinstance(x, Atom) for x in [atom1, atom2]): @@ -341,3 +343,40 @@ def test_atom_equality(atom1, atom2): return True return test_atom_equality + + @pytest.fixture(scope="session") + def are_equivalent_connections(self, are_equivalent_atoms): + connection_types_attrs_map = { + Bond: "bond_type", + Angle: "angle_type", + Dihedral: "dihedral_type", + Improper: "improper_type", + } + + def test_connection_equality(conn1, conn2): + if not type(conn1) == type(conn2): + return False + conn1_eq_members = conn1.equivalent_members() + conn2_eq_members = conn2.equivalent_members() + have_eq_members = False + for conn2_eq_member_tuple in conn2_eq_members: + for conn1_eq_member_tuple in conn1_eq_members: + if any( + are_equivalent_atoms(member1, member2) + for member1, member2 in zip( + conn1_eq_member_tuple, conn2_eq_member_tuple + ) + ): + have_eq_members = True + + if not have_eq_members: + return False + if conn1.name != conn2.name: + return False + if getattr( + conn1, connection_types_attrs_map[type(conn1)] + ) != getattr(conn2, connection_types_attrs_map[type(conn2)]): + return False + return True + + return test_connection_equality diff --git a/gmso/tests/test_serialization.py b/gmso/tests/test_serialization.py index 4aa4d6e22..f4ea0c1ba 100644 --- a/gmso/tests/test_serialization.py +++ b/gmso/tests/test_serialization.py @@ -12,6 +12,7 @@ from gmso.core.element import element_by_symbol from gmso.core.improper import Improper from gmso.core.improper_type import ImproperType +from gmso.core.topology import Topology from gmso.tests.base_test import BaseTest @@ -188,3 +189,47 @@ def test_include_and_exclude(self): assert "name" not in atom_json atom_json = atom.json(include={"mass_"}) assert "name" not in atom_json + + def test_full_serialization( + self, typed_ethane, are_equivalent_atoms, are_equivalent_connections + ): + typed_ethane.save("eth.json", types=True) + typed_ethane_copy = Topology.load("eth.json") + for atom1, atom2 in zip(typed_ethane._sites, typed_ethane_copy._sites): + assert are_equivalent_atoms(atom1, atom2) + assert typed_ethane.get_index(atom1) == typed_ethane_copy.get_index( + atom2 + ) + + for bond1, bond2 in zip(typed_ethane._bonds, typed_ethane_copy._bonds): + assert are_equivalent_connections(bond1, bond2) + + for angle1, angle2 in zip( + typed_ethane._angles, typed_ethane_copy._angles + ): + assert are_equivalent_connections(angle1, angle2) + + for dihedral1, dihedral2 in zip( + typed_ethane._dihedrals, typed_ethane_copy._dihedrals + ): + assert are_equivalent_connections(dihedral1, dihedral2) + + for improper1, improper2 in zip( + typed_ethane._impropers, typed_ethane_copy._impropers + ): + assert are_equivalent_connections(improper1, improper2) + + for atom_type in typed_ethane._atom_types: + assert atom_type in typed_ethane_copy._atom_types + + for bond_type in typed_ethane._bond_types: + assert bond_type in typed_ethane_copy._bond_types + + for angle_type in typed_ethane._angle_types: + assert angle_type in typed_ethane_copy._angle_types + + for dihedral_type in typed_ethane._dihedral_types: + assert dihedral_type in typed_ethane_copy._dihedral_types + + for improper_type in typed_ethane._improper_types: + assert improper_type in typed_ethane_copy._improper_types From f26edf9c8eacf5da3954b45e059c562dbec8ebbc Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Tue, 13 Jul 2021 12:32:56 -0500 Subject: [PATCH 05/12] Create format registries for loading and saving --- gmso/core/box.py | 8 +++++ gmso/core/topology.py | 23 +++++--------- gmso/formats/__init__.py | 2 ++ gmso/formats/formats_registry.py | 49 +++++++++++++++++++++++++++++ gmso/formats/json.py | 54 ++++++++++++++++++++++++++++++-- 5 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 gmso/formats/formats_registry.py diff --git a/gmso/core/box.py b/gmso/core/box.py index b2e9f836c..9eb7f1b04 100644 --- a/gmso/core/box.py +++ b/gmso/core/box.py @@ -179,6 +179,14 @@ def get_unit_vectors(self): """Return the normalized vectors of the box.""" return self._unit_vectors_from_angles() + def json_dict(self): + from gmso.abc.serialization_utils import unyt_to_dict + + return { + "lengths": unyt_to_dict(self._lengths), + "angles": unyt_to_dict(self._angles), + } + def __repr__(self): """Return formatted representation of the box.""" return "Box(a={}, b={}, c={}, alpha={}, beta={}, gamma={})".format( diff --git a/gmso/core/topology.py b/gmso/core/topology.py index 165e3e77b..bfdbf96b4 100644 --- a/gmso/core/topology.py +++ b/gmso/core/topology.py @@ -759,7 +759,7 @@ 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 save(self, filename, overwrite=True, **kwargs): + def save(self, filename, overwrite=False, **kwargs): """Save the topology to a file. Parameters @@ -781,15 +781,10 @@ def save(self, filename, overwrite=True, **kwargs): f"overwrite=True if you wish to overwrite the existing file" ) - if filename.suffix == ".json": - from gmso.formats.json import _to_json + from gmso.formats import SaversRegistry - types = kwargs.get("types", False) - update = kwargs.get("update", True) - indent = kwargs.get("indent", 2) - top_json = _to_json(self, types=types, update=update) - with filename.open("w") as top_json_file: - json.dump(top_json, top_json_file, indent=indent) + saver = SaversRegistry.get_callable(filename.suffix) + saver(self, filename, **kwargs) def __repr__(self): """Return custom format to represent topology.""" @@ -805,12 +800,10 @@ def __str__(self): return f"" @classmethod - def load(cls, filename): + def load(cls, filename, **kwargs): """Load a file to a topology""" filename = Path(filename).resolve() - if filename.suffix == ".json": - from gmso.formats.json import _from_json + from gmso.formats import LoadersRegistry - with filename.open("r") as json_file: - top = _from_json(json.load(json_file)) - return top + loader = LoadersRegistry.get_callable(filename.suffix) + return loader(filename, **kwargs) diff --git a/gmso/formats/__init__.py b/gmso/formats/__init__.py index 61ff553d9..42fb2b6bf 100644 --- a/gmso/formats/__init__.py +++ b/gmso/formats/__init__.py @@ -1,8 +1,10 @@ """Readers and writers for various file formats.""" from gmso.utils.io import has_ipywidgets +from .formats_registry import LoadersRegistry, SaversRegistry from .gro import read_gro, write_gro from .gsd import write_gsd +from .json import save_json from .lammpsdata import write_lammpsdata from .top import write_top from .xyz import read_xyz, write_xyz diff --git a/gmso/formats/formats_registry.py b/gmso/formats/formats_registry.py new file mode 100644 index 000000000..950ced389 --- /dev/null +++ b/gmso/formats/formats_registry.py @@ -0,0 +1,49 @@ +"""Registry utilities to handle formats for gmso Topology.""" + + +class UnsupportedFileFormatError(Exception): + """Exception to be raised whenever the file loading or saving is not supported.""" + + +class Registry: + """A registry to incorporate a callable with a file extension.""" + + def __init__(self): + self.handlers = {} + + def _assert_can_process(self, extension): + if extension not in self.handlers: + raise UnsupportedFileFormatError( + f"Extension {extension} is not registered" + ) + + def get_callable(self, extension): + """Get the callable associated with extension.""" + self._assert_can_process(extension) + return self.handlers[extension] + + +SaversRegistry = Registry() +LoadersRegistry = Registry() + + +class saves_as: + """Decorator to aid saving.""" + + def __init__(self, extension): + self.extension = extension + + def __call__(self, method): + """Register the method as saver for an extension.""" + SaversRegistry.handlers[self.extension] = method + + +class loads_as: + """Decorator to aid loading.""" + + def __init__(self, extension): + self.extension = extension + + def __call__(self, method): + """Register the method as loader for an extension.""" + LoadersRegistry.handlers[self.extension] = method diff --git a/gmso/formats/json.py b/gmso/formats/json.py index 8d3a66d2a..7f9bd0e8c 100644 --- a/gmso/formats/json.py +++ b/gmso/formats/json.py @@ -1,5 +1,7 @@ """Serialization to json.""" +import json from copy import deepcopy +from pathlib import Path from gmso.core.angle import Angle from gmso.core.angle_type import AngleType @@ -11,12 +13,13 @@ from gmso.core.dihedral_type import DihedralType from gmso.core.improper import Improper from gmso.core.improper_type import ImproperType +from gmso.formats.formats_registry import loads_as, saves_as def _to_json(top, types=False, update=True): - """Return a json serializable from a topology. + """Return a json serializable dictionary from a topology. - This is used for json serializing the topology + This method is used for json serializing the topology Parameters ---------- @@ -43,6 +46,7 @@ def _to_json(top, types=False, update=True): "name": top._name, "scaling_factors": top.scaling_factors, "subtopologies": [], + "box": top.box.json_dict() if top.box else None, "atoms": [], "bonds": [], "angles": [], @@ -228,3 +232,49 @@ def _from_json(json_dict): top.update_topology() return top + + +@saves_as(".json") +def save_json(top, filename, **kwargs): + """Save the topology as a JSON file. + + Parameters + ---------- + top: gmso.Topology + The topology to save + filename: str, pathlib.Path + The file to save to topology to, must be suffixed with .json + **kwargs: dict + The keyword arguments to _to_json and json.dump methods + """ + json_dict = _to_json( + top, update=kwargs.pop("update", True), types=kwargs.pop("types", False) + ) + if not isinstance(filename, Path): + filename = Path(filename).resolve() + + with filename.open("w") as json_file: + json.dump(json_dict, json_file, **kwargs) + + +@loads_as(".json") +def load_json(filename): + """Load a topology from a json file. + + Parameters + ---------- + filename: str, pathlib.Path + The file to load the topology from, must be suffixed with .json + + Returns + ------- + gmso.Topology + The Topology object obtained by loading the json file + """ + if not isinstance(filename, Path): + filename = Path(filename).resolve() + + with filename.open("r") as json_file: + json_dict = json.load(json_file) + top = _from_json(json_dict) + return top From e26992d7e3fc9ab3e6546b9495d883f2635fc6b5 Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Tue, 13 Jul 2021 12:47:31 -0500 Subject: [PATCH 06/12] WIP- Remove unused import; add test for box --- gmso/core/topology.py | 1 - gmso/formats/json.py | 13 +++++++++++++ gmso/tests/test_serialization.py | 7 +++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/gmso/core/topology.py b/gmso/core/topology.py index bfdbf96b4..17672f181 100644 --- a/gmso/core/topology.py +++ b/gmso/core/topology.py @@ -1,5 +1,4 @@ """Base data structure for GMSO chemical systems.""" -import json import warnings from pathlib import Path diff --git a/gmso/formats/json.py b/gmso/formats/json.py index 7f9bd0e8c..9c9877d9b 100644 --- a/gmso/formats/json.py +++ b/gmso/formats/json.py @@ -3,12 +3,15 @@ from copy import deepcopy from pathlib import Path +import unyt as u + 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.box import Box from gmso.core.dihedral import Dihedral from gmso.core.dihedral_type import DihedralType from gmso.core.improper import Improper @@ -230,6 +233,16 @@ def _from_json(json_dict): subtop.add_site(top.sites[atom_idx]) top.add_subtopology(subtop, update=False) + if json_dict.get("box"): + box_dict = json_dict["box"] + lengths = u.unyt_array( + box_dict["lengths"]["array"], box_dict["lengths"]["unit"] + ) + angles = u.unyt_array( + box_dict["angles"]["array"], box_dict["angles"]["unit"] + ) + top.box = Box(lengths=lengths, angles=angles) + top.update_topology() return top diff --git a/gmso/tests/test_serialization.py b/gmso/tests/test_serialization.py index f4ea0c1ba..51d0aa4d8 100644 --- a/gmso/tests/test_serialization.py +++ b/gmso/tests/test_serialization.py @@ -233,3 +233,10 @@ def test_full_serialization( for improper_type in typed_ethane._improper_types: assert improper_type in typed_ethane_copy._improper_types + + def test_serialization_with_box(self, n_typed_xe_mie): + top = n_typed_xe_mie(n_sites=20) + top.save("n_typed_xe_mie_20.json") + top_copy = Topology.load("n_typed_xe_mie_20.json") + assert u.allclose_units(top_copy.box.lengths, top.box.lengths) + assert u.allclose_units(top_copy.box.angles, top.box.angles) From 2d3af5ae90c05a6fb2d7306bd5c190c64cb83711 Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Tue, 13 Jul 2021 17:53:55 -0500 Subject: [PATCH 07/12] Make PairPotentialTypes serializable --- gmso/core/pairpotential_type.py | 17 ++++++++++++----- gmso/formats/json.py | 17 ++++++++++++++++- gmso/tests/base_test.py | 23 +++++++++++++++++++++++ gmso/tests/test_serialization.py | 10 ++++++++++ gmso/tests/test_topology.py | 31 ++++++++----------------------- 5 files changed, 69 insertions(+), 29 deletions(-) diff --git a/gmso/core/pairpotential_type.py b/gmso/core/pairpotential_type.py index 44c00900f..7353491eb 100644 --- a/gmso/core/pairpotential_type.py +++ b/gmso/core/pairpotential_type.py @@ -35,16 +35,21 @@ class PairPotentialType(ParametricPotential): def __init__( self, name="PairPotentialType", - expression="4 * eps * ((sigma / r)**12 - (sigma / r)**6)", + expression=None, parameters=None, independent_variables=None, + potential_expression=None, member_types=None, topology=None, + tags=None, ): - if parameters is None: - parameters = {"eps": 1 * u.Unit("kJ / mol"), "sigma": 1 * u.nm} - if independent_variables is None: - independent_variables = {"r"} + 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, @@ -53,7 +58,9 @@ def __init__( independent_variables=independent_variables, topology=topology, member_types=member_types, + potential_expression=potential_expression, set_ref=PAIRPOTENTIAL_TYPE_DICT, + tags=tags, ) @property diff --git a/gmso/formats/json.py b/gmso/formats/json.py index 9c9877d9b..daf58b17b 100644 --- a/gmso/formats/json.py +++ b/gmso/formats/json.py @@ -16,6 +16,7 @@ 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.formats.formats_registry import loads_as, saves_as @@ -60,6 +61,7 @@ def _to_json(top, types=False, update=True): "angle_types": [], "dihedral_types": [], "improper_types": [], + "pair_potentialtypes": [], } for atom in top._sites: @@ -116,6 +118,11 @@ 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(): + json_dict["pair_potentialtypes"].append( + pairpotential_type.json_dict(exclude={"topology", "set_ref"}) + ) + for subtop in top.subtops: subtop_dict = subtop.json_dict() json_dict["subtopologies"].append(subtop_dict) @@ -233,7 +240,7 @@ def _from_json(json_dict): subtop.add_site(top.sites[atom_idx]) top.add_subtopology(subtop, update=False) - if json_dict.get("box"): + if json_dict.get("box") is not None: box_dict = json_dict["box"] lengths = u.unyt_array( box_dict["lengths"]["array"], box_dict["lengths"]["unit"] @@ -244,6 +251,14 @@ def _from_json(json_dict): top.box = Box(lengths=lengths, angles=angles) top.update_topology() + + # AtomTypes need to be updated for pairpotentialtype addition + for pair_potentialtype_dict in json_dict["pair_potentialtypes"]: + pair_potentialtype = PairPotentialType.parse_obj( + pair_potentialtype_dict + ) + top.add_pairpotentialtype(pair_potentialtype, update=False) + return top diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py index 46da2321f..043f4c04d 100644 --- a/gmso/tests/base_test.py +++ b/gmso/tests/base_test.py @@ -14,6 +14,7 @@ from gmso.core.element import Hydrogen, Oxygen from gmso.core.forcefield import ForceField from gmso.core.improper import Improper +from gmso.core.pairpotential_type import PairPotentialType from gmso.core.topology import Topology from gmso.external import from_mbuild, from_parmed from gmso.external.convert_foyer_xml import from_foyer_xml @@ -380,3 +381,25 @@ def test_connection_equality(conn1, conn2): return True return test_connection_equality + + @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") + atom1 = Atom(name="a", atom_type=atype1) + atom2 = Atom(name="b", atom_type=atype2) + top.add_site(atom1) + top.add_site(atom2) + top.update_topology() + + pptype12 = PairPotentialType( + name="pp12", + expression="r + 1", + independent_variables="r", + parameters={}, + member_types=tuple(["a1", "a2"]), + ) + + top.add_pairpotentialtype(pptype12) + return top diff --git a/gmso/tests/test_serialization.py b/gmso/tests/test_serialization.py index 51d0aa4d8..6c0f3aa47 100644 --- a/gmso/tests/test_serialization.py +++ b/gmso/tests/test_serialization.py @@ -240,3 +240,13 @@ def test_serialization_with_box(self, n_typed_xe_mie): top_copy = Topology.load("n_typed_xe_mie_20.json") assert u.allclose_units(top_copy.box.lengths, top.box.lengths) assert u.allclose_units(top_copy.box.angles, top.box.angles) + + def test_serialization_with_pairpotential_types( + self, pairpotentialtype_top + ): + pairpotentialtype_top.save("pptype.json", types=True) + pptop_copy = Topology.load("pptype.json") + assert ( + pptop_copy.pairpotential_types[0] + == pairpotentialtype_top.pairpotential_types[0] + ) diff --git a/gmso/tests/test_topology.py b/gmso/tests/test_topology.py index e842d4b35..909fb10b1 100644 --- a/gmso/tests/test_topology.py +++ b/gmso/tests/test_topology.py @@ -385,30 +385,15 @@ def test_improper_impropertype_update(self): assert len(top.improper_type_expressions) == 1 assert len(top.atom_type_expressions) == 2 - def test_pairpotential_pairpotentialtype_update(self): - top = Topology() - atype1 = AtomType(name="a1", expression="sigma + epsilon*r") - atype2 = AtomType(name="a2", expression="sigma * epsilon*r") - atom1 = Atom(name="a", atom_type=atype1) - atom2 = Atom(name="b", atom_type=atype2) - top.add_site(atom1) - top.add_site(atom2) - top.update_topology() - - pptype12 = PairPotentialType( - name="pp12", - expression="r + 1", - independent_variables="r", - parameters={}, - member_types=tuple(["a1", "a2"]), - ) - - top.add_pairpotentialtype(pptype12) - assert len(top.pairpotential_types) == 1 - assert top._pairpotential_types_idx[pptype12] == 0 + 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 - top.remove_pairpotentialtype(["a1", "a2"]) - assert len(top.pairpotential_types) == 0 + pairpotentialtype_top.remove_pairpotentialtype(["a1", "a2"]) + assert len(pairpotentialtype_top.pairpotential_types) == 0 def test_add_subtopology(self): top = Topology() From e5f0074c745fb1b766db924e7c2b7d451f2ad1e3 Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Fri, 16 Jul 2021 11:26:48 -0500 Subject: [PATCH 08/12] WIP- Add more tests for exceptions --- gmso/formats/formats_registry.py | 3 ++- gmso/tests/test_serialization.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/gmso/formats/formats_registry.py b/gmso/formats/formats_registry.py index 950ced389..c646805f8 100644 --- a/gmso/formats/formats_registry.py +++ b/gmso/formats/formats_registry.py @@ -14,7 +14,8 @@ def __init__(self): def _assert_can_process(self, extension): if extension not in self.handlers: raise UnsupportedFileFormatError( - f"Extension {extension} is not registered" + f"Extension {extension} cannot be processed as no utility " + f" is defined in the current API to handle {extension} files." ) def get_callable(self, extension): diff --git a/gmso/tests/test_serialization.py b/gmso/tests/test_serialization.py index 6c0f3aa47..9290e8497 100644 --- a/gmso/tests/test_serialization.py +++ b/gmso/tests/test_serialization.py @@ -13,6 +13,7 @@ from gmso.core.improper import Improper from gmso.core.improper_type import ImproperType from gmso.core.topology import Topology +from gmso.formats.formats_registry import UnsupportedFileFormatError from gmso.tests.base_test import BaseTest @@ -250,3 +251,19 @@ def test_serialization_with_pairpotential_types( pptop_copy.pairpotential_types[0] == pairpotentialtype_top.pairpotential_types[0] ) + + def test_serialization_unsupported_file_format(self, ethane_from_scratch): + with pytest.raises(UnsupportedFileFormatError): + ethane_from_scratch.save("ethane_from_scratch.zip") + + def test_serialization_untyped_with_types_info(self, ethane_from_scratch): + with pytest.raises(ValueError): + ethane_from_scratch.save("ethane_from_scratch.json", types=True) + + def test_serialization_overwrite(self, ethane_from_scratch): + ethane_from_scratch.save("ethane_from_scratch.json", overwrite=False) + with pytest.raises(FileExistsError): + ethane_from_scratch.save( + "ethane_from_scratch.json", overwrite=False + ) + ethane_from_scratch.save("ethane_from_scratch.json", overwrite=True) From b45f34bedd14efeae9463b60b5370dc91241714b Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Fri, 16 Jul 2021 11:38:08 -0500 Subject: [PATCH 09/12] Import json moved to top level import in gmso_base.py --- gmso/abc/gmso_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gmso/abc/gmso_base.py b/gmso/abc/gmso_base.py index 9a48591ea..957e704da 100644 --- a/gmso/abc/gmso_base.py +++ b/gmso/abc/gmso_base.py @@ -1,4 +1,5 @@ """Base model all classes extend.""" +import json import warnings from abc import ABC from typing import Any, ClassVar, Type @@ -95,8 +96,6 @@ def json(self, **kwargs): def json_dict(self, **kwargs): """Return a JSON serializable dictionary from the object""" - import json - raw_json = self.json(**kwargs) return json.loads(raw_json) From d63718046efc63fdd9e10f32de286e6c08f5723f Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Tue, 20 Jul 2021 11:11:31 -0500 Subject: [PATCH 10/12] WIP- add are_equivalent_topologies fixture --- gmso/tests/base_test.py | 81 ++++++++++++++++++++++++++++++++ gmso/tests/test_serialization.py | 59 +++++------------------ 2 files changed, 93 insertions(+), 47 deletions(-) diff --git a/gmso/tests/base_test.py b/gmso/tests/base_test.py index 2bb96fe67..561da1b4b 100644 --- a/gmso/tests/base_test.py +++ b/gmso/tests/base_test.py @@ -382,6 +382,87 @@ def test_connection_equality(conn1, conn2): return test_connection_equality + @pytest.fixture(scope="session") + def have_equivalent_boxes(self): + def test_box_equivalence(top1, top2): + if top1.box and top2.box: + return u.allclose_units( + top1.box.lengths, top2.box.lengths + ) and u.allclose_units(top1.box.angles, top2.box.angles) + elif not top1.box and not top2.box: + return True + else: + return False + + return test_box_equivalence + + @pytest.fixture(scope="session") + def are_equivalent_topologies( + self, + have_equivalent_boxes, + are_equivalent_atoms, + are_equivalent_connections, + ): + def test_topology_equivalence(top1, top2): + if top1.n_sites != top2.n_sites: + return False, "Unequal number of sites" + if top1.n_bonds != top2.n_bonds: + return False, "Unequal number of bonds" + if top1.n_angles != top2.n_angles: + return False, "Unequal number of angles" + if top1.n_dihedrals != top2.n_dihedrals: + return False, "Unequal number of dihedrals" + if top1.n_impropers != top2.n_impropers: + return False, "Unequal number of impropers" + if top1.name != top2.name: + return False, "Dissimilar names" + + if top1.scaling_factors != top2.scaling_factors: + return False, f"Mismatch in scaling factors" + + if not have_equivalent_boxes(top1, top2): + return ( + False, + "Non equivalent boxes, differing in lengths and angles", + ) + + for atom1, atom2 in zip(top1.sites, top2.sites): + if not are_equivalent_atoms(atom1, atom2): + return False, f"Non equivalent atoms {atom1, atom2}" + + # Note: In these zipped iterators, index matches are implicitly checked + for bond1, bond2 in zip(top1.bonds, top2.bonds): + if not are_equivalent_connections(bond1, bond2): + return False, f"Non equivalent bonds {bond1, bond2}" + + for angle1, angle2 in zip(top1.angles, top2.angles): + if not are_equivalent_connections(angle1, angle2): + return False, f"Non equivalent angles {angle1, angle2}" + + for dihedral1, dihedral2 in zip(top1.dihedrals, top2.dihedrals): + if not are_equivalent_connections(dihedral1, dihedral2): + return ( + False, + f"Non equivalent dihedrals, {dihedral1, dihedral2}", + ) + + for improper1, improper2 in zip(top1.impropers, top2.impropers): + if not are_equivalent_connections(improper1, improper2): + return ( + False, + f"Non equivalent impropers, {improper1, improper2}", + ) + + for pp_type1, pp_type2 in zip( + top1.pairpotential_types, top2.pairpotential_types + ): + if pp_type1 != pp_type2: + return False, f"Pair-PotentialTypes mismatch" + + return True, f"{top1} and {top2} are equivalent" + + return test_topology_equivalence + @pytest.fixture(scope="session") def pairpotentialtype_top(self): top = Topology() diff --git a/gmso/tests/test_serialization.py b/gmso/tests/test_serialization.py index 9290e8497..98aea1ad2 100644 --- a/gmso/tests/test_serialization.py +++ b/gmso/tests/test_serialization.py @@ -192,65 +192,30 @@ def test_include_and_exclude(self): assert "name" not in atom_json def test_full_serialization( - self, typed_ethane, are_equivalent_atoms, are_equivalent_connections + self, + typed_ethane, + are_equivalent_atoms, + are_equivalent_connections, + are_equivalent_topologies, ): typed_ethane.save("eth.json", types=True) typed_ethane_copy = Topology.load("eth.json") - for atom1, atom2 in zip(typed_ethane._sites, typed_ethane_copy._sites): - assert are_equivalent_atoms(atom1, atom2) - assert typed_ethane.get_index(atom1) == typed_ethane_copy.get_index( - atom2 - ) - - for bond1, bond2 in zip(typed_ethane._bonds, typed_ethane_copy._bonds): - assert are_equivalent_connections(bond1, bond2) - - for angle1, angle2 in zip( - typed_ethane._angles, typed_ethane_copy._angles - ): - assert are_equivalent_connections(angle1, angle2) - - for dihedral1, dihedral2 in zip( - typed_ethane._dihedrals, typed_ethane_copy._dihedrals - ): - assert are_equivalent_connections(dihedral1, dihedral2) - - for improper1, improper2 in zip( - typed_ethane._impropers, typed_ethane_copy._impropers - ): - assert are_equivalent_connections(improper1, improper2) + assert are_equivalent_topologies(typed_ethane_copy, typed_ethane) - for atom_type in typed_ethane._atom_types: - assert atom_type in typed_ethane_copy._atom_types - - for bond_type in typed_ethane._bond_types: - assert bond_type in typed_ethane_copy._bond_types - - for angle_type in typed_ethane._angle_types: - assert angle_type in typed_ethane_copy._angle_types - - for dihedral_type in typed_ethane._dihedral_types: - assert dihedral_type in typed_ethane_copy._dihedral_types - - for improper_type in typed_ethane._improper_types: - assert improper_type in typed_ethane_copy._improper_types - - def test_serialization_with_box(self, n_typed_xe_mie): + def test_serialization_with_box( + self, n_typed_xe_mie, are_equivalent_topologies + ): top = n_typed_xe_mie(n_sites=20) top.save("n_typed_xe_mie_20.json") top_copy = Topology.load("n_typed_xe_mie_20.json") - assert u.allclose_units(top_copy.box.lengths, top.box.lengths) - assert u.allclose_units(top_copy.box.angles, top.box.angles) + assert are_equivalent_topologies(top, top_copy) def test_serialization_with_pairpotential_types( - self, pairpotentialtype_top + self, pairpotentialtype_top, are_equivalent_topologies ): pairpotentialtype_top.save("pptype.json", types=True) pptop_copy = Topology.load("pptype.json") - assert ( - pptop_copy.pairpotential_types[0] - == pairpotentialtype_top.pairpotential_types[0] - ) + assert are_equivalent_topologies(pptop_copy, pairpotentialtype_top) def test_serialization_unsupported_file_format(self, ethane_from_scratch): with pytest.raises(UnsupportedFileFormatError): From 5da5b313c1a125591fe7d98a2c0fdc1d5209bd4a Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Mon, 26 Jul 2021 19:46:46 -0500 Subject: [PATCH 11/12] WIP- Adress review comments --- gmso/formats/json.py | 18 ++++++++++++++++++ gmso/tests/test_serialization.py | 2 -- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/gmso/formats/json.py b/gmso/formats/json.py index daf58b17b..fa911f47e 100644 --- a/gmso/formats/json.py +++ b/gmso/formats/json.py @@ -1,5 +1,6 @@ """Serialization to json.""" import json +import warnings from copy import deepcopy from pathlib import Path @@ -43,6 +44,20 @@ def _to_json(top, types=False, update=True): raise ValueError( "Cannot incorporate types because the topology is not typed." ) + + if not types and top.is_typed(): + warnings.warn( + "The provided topology is typed and `types` is set to False. " + "The types(potentials) info will be lost in the serialized representation. " + "Please consider using `types=True` if this behavior is not intended. " + ) + + if types and not top.is_fully_typed(): + warnings.warn( + "The provided topology is not full typed and `types` is set to True. " + "Please consider using `types=False` if this behavior is not intended. " + ) + if update: top.update_topology() @@ -147,6 +162,9 @@ def _from_json(json_dict): from gmso.core.topology import Topology # FixMe: DeepCopying a dictionary might not be the most efficient + # DeepCopying the following structure is done because of the subsequent + # updates to the dictionary will modify the original one passed as function's + # argument json_dict = deepcopy(json_dict) top = Topology( diff --git a/gmso/tests/test_serialization.py b/gmso/tests/test_serialization.py index 98aea1ad2..1d82cf638 100644 --- a/gmso/tests/test_serialization.py +++ b/gmso/tests/test_serialization.py @@ -194,8 +194,6 @@ def test_include_and_exclude(self): def test_full_serialization( self, typed_ethane, - are_equivalent_atoms, - are_equivalent_connections, are_equivalent_topologies, ): typed_ethane.save("eth.json", types=True) From da6cf23b5f6fe5bb56b695a2e657cde3cd96308c Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Tue, 27 Jul 2021 11:24:06 -0500 Subject: [PATCH 12/12] WIP- Remove redundant whitespace in format_registries.py --- gmso/formats/formats_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gmso/formats/formats_registry.py b/gmso/formats/formats_registry.py index c646805f8..1d477263f 100644 --- a/gmso/formats/formats_registry.py +++ b/gmso/formats/formats_registry.py @@ -15,7 +15,7 @@ def _assert_can_process(self, extension): if extension not in self.handlers: raise UnsupportedFileFormatError( f"Extension {extension} cannot be processed as no utility " - f" is defined in the current API to handle {extension} files." + f"is defined in the current API to handle {extension} files." ) def get_callable(self, extension):