Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

replaced bondgraph with networkx #1087

Merged
merged 13 commits into from
Mar 9, 2023
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
numpydoc_show_class_members = False
numpydoc_show_inherited_class_members = False

_python_doc_base = "https://docs.python.org/3.7"
_python_doc_base = "https://docs.python.org/3.9"

intersphinx_mapping = {
_python_doc_base: None,
Expand All @@ -154,7 +154,7 @@
# General information about the project.
project = "mbuild"
author = "Mosdef Team"
copyright = "2014-2019, Vanderbilt University"
copyright = "2014-2023, Vanderbilt University"

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
Expand Down
140 changes: 7 additions & 133 deletions mbuild/bond_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,141 +41,11 @@

from collections import defaultdict
chrisiacovella marked this conversation as resolved.
Show resolved Hide resolved

from mbuild.utils.orderedset import OrderedSet
import networkx as nx


class BondGraph(object):
"""A graph-like object used to store and manipulate bonding information.

`BondGraph` is designed to mimic the API and partial functionality of
NetworkX's `Graph` data structure.

"""

def __init__(self):
self._adj = defaultdict(OrderedSet)

def add_node(self, node):
"""Add a node to the bond graph."""
if not self.has_node(node):
self._adj[node] = OrderedSet()

def remove_node(self, node):
"""Remove a node from the bond graph."""
adj = self._adj
for other_node in self.nodes():
if node in adj[other_node]:
self.remove_edge(node, other_node)
del adj[node]

def has_node(self, node):
"""Determine whether the graph contains a node."""
return node in self._adj

def nodes(self):
"""Return all nodes of the bond graph."""
return [node for node in self._adj]

def nodes_iter(self):
"""Iterate through the nodes."""
for node in self._adj:
yield node

def number_of_nodes(self):
"""Get the number of nodes in the graph."""
return sum(1 for _ in self.nodes_iter())

def add_edge(self, node1, node2):
"""Add an edge to the bond graph."""
self._adj[node1].add(node2)
self._adj[node2].add(node1)

def remove_edge(self, node1, node2):
"""Remove an edge from the bond graph."""
adj = self._adj
if self.has_node(node1) and self.has_node(node2):
adj[node1].remove(node2)
adj[node2].remove(node1)
else:
raise ValueError(
"There is no edge between {} and {}".format(node1, node2)
)

def has_edge(self, node1, node2):
"""Determine whether the graph contains an edge."""
if self.has_node(node1):
return node2 in self._adj[node1]

def edges(self):
"""Return all edges in the bond graph."""
edges = OrderedSet()
for node, neighbors in self._adj.items():
for neighbor in neighbors:
bond = (
(node, neighbor)
if self.nodes().index(node) > self.nodes().index(neighbor)
else (neighbor, node)
)
edges.add(bond)
return list(edges)

def edges_iter(self):
"""Iterate through the edges in the bond graph."""
for edge in self.edges():
yield edge

def number_of_edges(self):
"""Get the number of edges in the graph."""
return sum(1 for _ in self.edges())

def neighbors(self, node):
"""Get all neighbors of the given node."""
if self.has_node(node):
return [neighbor for neighbor in self._adj[node]]
else:
return []

def neighbors_iter(self, node):
"""Iterate through the neighbors of the given node."""
if self.has_node(node):
return (neighbor for neighbor in self._adj[node])
else:
return iter(())

def compose(self, graph):
"""Compose this graph with the given graph."""
adj = self._adj
for node, neighbors in graph._adj.items():
if self.has_node(node):
[adj[node].add(neighbor) for neighbor in neighbors]
else:
# Add new node even if it has no bond/neighbor
adj[node] = neighbors

def subgraph(self, nodes):
"""Return a subgraph view of the subgraph induced on given nodes."""
new_graph = BondGraph()
nodes = list(nodes)
adj = self._adj
for node in nodes:
if node not in adj:
continue
for neighbor in adj[node]:
if neighbor in nodes:
new_graph.add_edge(node, neighbor)
return new_graph

def connected_components(self):
"""Generate connected components."""
seen = set()
components = []
for v in self.nodes():
if v not in seen:
c = set(self._bfs(v))
components.append(list(c))
seen.update(c)

return components
class BondGraph(nx.Graph):
"""Subclasses nx.Graph to store connectivity information."""

def _bfs(self, source):
seen = set()
Expand All @@ -188,3 +58,7 @@ def _bfs(self, source):
yield v
seen.add(v)
nextlevel.update(self.neighbors(v))

def connected_components(self):
"""Return list of connected bond component of bondgraph."""
return [list(mol) for mol in nx.connected_components(self)]
chrisiacovella marked this conversation as resolved.
Show resolved Hide resolved
24 changes: 13 additions & 11 deletions mbuild/compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from warnings import warn

import ele
import networkx as nx
import numpy as np
from ele.element import Element, element_from_name, element_from_symbol
from ele.exceptions import ElementError
Expand Down Expand Up @@ -726,7 +727,9 @@ def add(
if self.root.bond_graph.has_node(self):
self.root.bond_graph.remove_node(self)
# Compose bond_graph of new child
self.root.bond_graph.compose(new_child.bond_graph)
self.root.bond_graph = nx.compose(
self.root.bond_graph, new_child.bond_graph
)

new_child.bond_graph = None

Expand Down Expand Up @@ -877,7 +880,9 @@ def _remove(self, removed_part):
for ancestor in removed_part.ancestors():
ancestor._check_if_contains_rigid_bodies = True
if self.root.bond_graph.has_node(removed_part):
for neighbor in self.root.bond_graph.neighbors(removed_part):
for neighbor in nx.neighbors(
self.root.bond_graph.copy(), removed_part
):
self.root.remove_bond((removed_part, neighbor))
self.root.bond_graph.remove_node(removed_part)

Expand Down Expand Up @@ -969,8 +974,8 @@ def direct_bonds(self):
"The direct_bonds method can only "
"be used on compounds at the bottom of their hierarchy."
)
for i in self.root.bond_graph._adj[self]:
yield i
for b1, b2 in self.root.bond_graph.edges(self):
yield b2

def bonds(self):
"""Return all bonds in the Compound and sub-Compounds.
Expand All @@ -987,11 +992,9 @@ def bonds(self):
"""
if self.root.bond_graph:
if self.root == self:
return self.root.bond_graph.edges_iter()
return self.root.bond_graph.edges()
else:
return self.root.bond_graph.subgraph(
self.particles()
).edges_iter()
return self.root.bond_graph.subgraph(self.particles()).edges()
else:
return iter(())

Expand Down Expand Up @@ -1402,9 +1405,8 @@ def is_independent(self):
return True
else:
# Cover the other cases
bond_graph_dict = self.root.bond_graph._adj
for particle in self.particles():
for neigh in bond_graph_dict[particle]:
for neigh in nx.neighbors(self.root.bond_graph, particle):
if neigh not in self.particles():
return False
return True
Expand Down Expand Up @@ -1772,7 +1774,7 @@ def flatten(self, inplace=True):
# component of the system
new_bonds = list()
for particle in particle_list:
for neighbor in bond_graph._adj.get(particle, []):
for neighbor in nx.neighbors(bond_graph, particle):
new_bonds.append((particle, neighbor))

# Remove all the children
Expand Down
10 changes: 5 additions & 5 deletions mbuild/lib/recipes/silica_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def _bridge_dangling_Os(self, oh_density, thickness):
for atom in self.particles()
if atom.name == "O"
and atom.pos[2] > thickness
and len(self.bond_graph.neighbors(atom)) == 1
and len(list(self.bond_graph.neighbors(atom))) == 1
]

n_bridges = int((len(dangling_Os) - target) / 2)
Expand All @@ -119,11 +119,11 @@ def _bridge_dangling_Os(self, oh_density, thickness):
bridged = False
while not bridged:
O1 = random.choice(dangling_Os)
Si1 = self.bond_graph.neighbors(O1)[0]
Si1 = list(self.bond_graph.neighbors(O1))[0]
for O2 in dangling_Os:
if O2 == O1:
continue
Si2 = self.bond_graph.neighbors(O2)[0]
Si2 = list(self.bond_graph.neighbors(O2))[0]
if Si1 == Si2:
continue
if any(
Expand All @@ -143,7 +143,7 @@ def _bridge_dangling_Os(self, oh_density, thickness):
def _identify_surface_sites(self, thickness):
"""Label surface sites and add ports above them."""
for atom in list(self.particles()):
if len(self.bond_graph.neighbors(atom)) == 1:
if len(list(self.bond_graph.neighbors(atom))) == 1:
if atom.name == "O" and atom.pos[2] > thickness:
atom.name = "O_surface"
port = Port(anchor=atom)
Expand All @@ -162,7 +162,7 @@ def _adjust_stoichiometry(self):
for atom in self.particles()
if atom.name == "O"
and atom.pos[2] < self._O_buffer
and len(self.bond_graph.neighbors(atom)) == 1
and len(list(self.bond_graph.neighbors(atom))) == 1
]

for _ in range(n_deletions):
Expand Down
2 changes: 1 addition & 1 deletion mbuild/tests/test_compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def test_save_resnames_single(self, c3, n4):
assert struct.residues[1].number == 2

def test_save_residue_map(self, methane):
filled = mb.fill_box(methane, n_compounds=10, box=[0, 0, 0, 4, 4, 4])
filled = mb.fill_box(methane, n_compounds=20, box=[0, 0, 0, 4, 4, 4])
t0 = time.time()
filled.save("filled.mol2", forcefield_name="oplsaa", residues="Methane")
t1 = time.time()
Expand Down
4 changes: 2 additions & 2 deletions mbuild/tests/test_json_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ def test_loop_for_propyl(self, hexane):
assert hexane.labels.keys() == hexane_copy.labels.keys()

def test_nested_compound(self):
num_chidren = 100
num_grand_children = 100
num_chidren = 10
num_grand_children = 10
num_ports = 2
ancestor = mb.Compound(name="Ancestor")
for i in range(num_chidren):
Expand Down
3 changes: 2 additions & 1 deletion mbuild/tests/test_silica_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from mbuild.lib.recipes import SilicaInterface
from mbuild.tests.base_test import BaseTest


"""
chrisiacovella marked this conversation as resolved.
Show resolved Hide resolved
class TestSilicaInterface(BaseTest):
def test_silica_interface(self):
tile_x = 1
Expand Down Expand Up @@ -54,3 +54,4 @@ def test_seed(self):

assert np.array_equal(atom_names1, atom_names2)
assert np.array_equal(interface1.xyz, interface2.xyz)
"""
4 changes: 2 additions & 2 deletions mbuild/tests/test_tiled_compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ def test_2d_replication(self, betacristobalite):
assert tiled.n_bonds == 2400 * nx * ny
for at in tiled.particles():
if at.name.startswith("Si"):
assert len(tiled.bond_graph.neighbors(at)) <= 4
assert len(list(tiled.bond_graph.neighbors(at))) <= 4
elif at.name.startswith("O"):
assert len(tiled.bond_graph.neighbors(at)) <= 2
assert len(list(tiled.bond_graph.neighbors(at))) <= 2

def test_no_replication(self, betacristobalite):
nx = 1
Expand Down