Repository: networkx/networkx

Issue:
Rooted product of graphs
Addresses issue https://github.com/networkx/networkx/issues/2017

Implements code to construct the rooted product of two graphs. Relevant information can be found at https://en.wikipedia.org/wiki/Rooted_product_of_graphs

This is my first pull request to this product so please let me know if there is anything I should fix.



Discussion:

Resolving patch:
diff --git a/networkx/algorithms/operators/product.py b/networkx/algorithms/operators/product.py
--- a/networkx/algorithms/operators/product.py
+++ b/networkx/algorithms/operators/product.py
@@ -19,7 +19,8 @@
 from networkx.utils import not_implemented_for
 
 __all__ = ['tensor_product', 'cartesian_product',
-           'lexicographic_product', 'strong_product', 'power']
+           'lexicographic_product', 'strong_product', 'power',
+           'rooted_product']
 
 
 def _dict_product(d1, d2):
@@ -188,9 +189,9 @@ def cartesian_product(G, H):
 
     The Cartesian product $P$ of the graphs $G$ and $H$ has a node set that
     is the Cartesian product of the node sets, $V(P)=V(G) \times V(H)$.
-    $P$ has an edge $((u,v),(x,y))$ if and only if either $u$ is equal to $x$ and
-    both $v$ and $y$ are adjacent in $H$ or if $v$ is equal to $y$ and both $u$
-    and $x$ are adjacent in $G$.
+    $P$ has an edge $((u,v),(x,y))$ if and only if either $u$ is equal to $x$
+    and both $v$ and $y$ are adjacent in $H$ or if $v$ is equal to $y$ and
+    both $u$ and $x$ are adjacent in $G$.
 
     Parameters
     ----------
@@ -434,3 +435,41 @@ def power(G, k):
             level += 1
         H.add_edges_from((n, nbr) for nbr in seen)
     return H
+
+
+@not_implemented_for('multigraph')
+def rooted_product(G, H, root):
+    """ Return the rooted product of graphs G and H rooted at root in H.
+
+    A new graph is constructed representing the rooted product of
+    the inputted graphs, G and H, with a root in H.
+    A rooted product duplicates H for each nodes in G with the root
+    of H corresponding to the node in G. Nodes are renamed as the direct
+    product of G and H. The result is a subgraph of the cartesian product.
+
+    Parameters
+    ----------
+    G,H : graph
+       A NetworkX graph
+    root : node
+       A node in H
+
+    Returns
+    -------
+    R : The rooted product of G and H with a specified root in H
+
+    Notes
+    -----
+    The nodes of R are the Cartesian Product of the nodes of G and H.
+    The nodes of G and H are not relabeled.
+    """
+    if root not in H:
+        raise nx.NetworkXError('root must be a vertex in H')
+
+    R = nx.Graph()
+    R.add_nodes_from(product(G, H))
+
+    R.add_edges_from(((e[0], root), (e[1], root)) for e in G.edges())
+    R.add_edges_from(((g, e[0]), (g, e[1])) for g in G for e in H.edges())
+
+    return R


Test patch:
diff --git a/networkx/algorithms/operators/tests/test_product.py b/networkx/algorithms/operators/tests/test_product.py
--- a/networkx/algorithms/operators/tests/test_product.py
+++ b/networkx/algorithms/operators/tests/test_product.py
@@ -1,11 +1,11 @@
 import networkx as nx
-from networkx import tensor_product, cartesian_product, lexicographic_product, strong_product
 from nose.tools import assert_true, assert_equal, raises
 from networkx.testing import assert_edges_equal
 
+
 @raises(nx.NetworkXError)
 def test_tensor_product_raises():
-    P = tensor_product(nx.DiGraph(), nx.Graph())
+    P = nx.tensor_product(nx.DiGraph(), nx.Graph())
 
 
 def test_tensor_product_null():
@@ -16,28 +16,28 @@ def test_tensor_product_null():
     P3 = nx.path_graph(3)
     P10 = nx.path_graph(10)
     # null graph
-    G = tensor_product(null, null)
+    G = nx.tensor_product(null, null)
     assert_true(nx.is_isomorphic(G, null))
     # null_graph X anything = null_graph and v.v.
-    G = tensor_product(null, empty10)
+    G = nx.tensor_product(null, empty10)
     assert_true(nx.is_isomorphic(G, null))
-    G = tensor_product(null, K3)
+    G = nx.tensor_product(null, K3)
     assert_true(nx.is_isomorphic(G, null))
-    G = tensor_product(null, K10)
+    G = nx.tensor_product(null, K10)
     assert_true(nx.is_isomorphic(G, null))
-    G = tensor_product(null, P3)
+    G = nx.tensor_product(null, P3)
     assert_true(nx.is_isomorphic(G, null))
-    G = tensor_product(null, P10)
+    G = nx.tensor_product(null, P10)
     assert_true(nx.is_isomorphic(G, null))
-    G = tensor_product(empty10, null)
+    G = nx.tensor_product(empty10, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = tensor_product(K3, null)
+    G = nx.tensor_product(K3, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = tensor_product(K10, null)
+    G = nx.tensor_product(K10, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = tensor_product(P3, null)
+    G = nx.tensor_product(P3, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = tensor_product(P10, null)
+    G = nx.tensor_product(P10, null)
     assert_true(nx.is_isomorphic(G, null))
 
 
@@ -46,9 +46,9 @@ def test_tensor_product_size():
     K3 = nx.complete_graph(3)
     K5 = nx.complete_graph(5)
 
-    G = tensor_product(P5, K3)
+    G = nx.tensor_product(P5, K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = tensor_product(K3, K5)
+    G = nx.tensor_product(K3, K5)
     assert_equal(nx.number_of_nodes(G), 3 * 5)
 
 
@@ -56,38 +56,38 @@ def test_tensor_product_combinations():
     # basic smoke test, more realistic tests would be usefule
     P5 = nx.path_graph(5)
     K3 = nx.complete_graph(3)
-    G = tensor_product(P5, K3)
+    G = nx.tensor_product(P5, K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = tensor_product(P5, nx.MultiGraph(K3))
+    G = nx.tensor_product(P5, nx.MultiGraph(K3))
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = tensor_product(nx.MultiGraph(P5), K3)
+    G = nx.tensor_product(nx.MultiGraph(P5), K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = tensor_product(nx.MultiGraph(P5), nx.MultiGraph(K3))
+    G = nx.tensor_product(nx.MultiGraph(P5), nx.MultiGraph(K3))
     assert_equal(nx.number_of_nodes(G), 5 * 3)
 
-    G = tensor_product(nx.DiGraph(P5), nx.DiGraph(K3))
+    G = nx.tensor_product(nx.DiGraph(P5), nx.DiGraph(K3))
     assert_equal(nx.number_of_nodes(G), 5 * 3)
 
 
 def test_tensor_product_classic_result():
     K2 = nx.complete_graph(2)
     G = nx.petersen_graph()
-    G = tensor_product(G, K2)
+    G = nx.tensor_product(G, K2)
     assert_true(nx.is_isomorphic(G, nx.desargues_graph()))
 
     G = nx.cycle_graph(5)
-    G = tensor_product(G, K2)
+    G = nx.tensor_product(G, K2)
     assert_true(nx.is_isomorphic(G, nx.cycle_graph(10)))
 
     G = nx.tetrahedral_graph()
-    G = tensor_product(G, K2)
+    G = nx.tensor_product(G, K2)
     assert_true(nx.is_isomorphic(G, nx.cubical_graph()))
 
 
 def test_tensor_product_random():
     G = nx.erdos_renyi_graph(10, 2 / 10.)
     H = nx.erdos_renyi_graph(10, 2 / 10.)
-    GH = tensor_product(G, H)
+    GH = nx.tensor_product(G, H)
 
     for (u_G, u_H) in GH.nodes():
         for (v_G, v_H) in GH.nodes():
@@ -104,7 +104,7 @@ def test_cartesian_product_multigraph():
     H = nx.MultiGraph()
     H.add_edge(3, 4, key=0)
     H.add_edge(3, 4, key=1)
-    GH = cartesian_product(G, H)
+    GH = nx.cartesian_product(G, H)
     assert_equal(set(GH), {(1, 3), (2, 3), (2, 4), (1, 4)})
     assert_equal({(frozenset([u, v]), k) for u, v, k in GH.edges(keys=True)},
                  {(frozenset([u, v]), k) for u, v, k in
@@ -116,7 +116,7 @@ def test_cartesian_product_multigraph():
 
 @raises(nx.NetworkXError)
 def test_cartesian_product_raises():
-    P = cartesian_product(nx.DiGraph(), nx.Graph())
+    P = nx.cartesian_product(nx.DiGraph(), nx.Graph())
 
 
 def test_cartesian_product_null():
@@ -127,28 +127,28 @@ def test_cartesian_product_null():
     P3 = nx.path_graph(3)
     P10 = nx.path_graph(10)
     # null graph
-    G = cartesian_product(null, null)
+    G = nx.cartesian_product(null, null)
     assert_true(nx.is_isomorphic(G, null))
     # null_graph X anything = null_graph and v.v.
-    G = cartesian_product(null, empty10)
+    G = nx.cartesian_product(null, empty10)
     assert_true(nx.is_isomorphic(G, null))
-    G = cartesian_product(null, K3)
+    G = nx.cartesian_product(null, K3)
     assert_true(nx.is_isomorphic(G, null))
-    G = cartesian_product(null, K10)
+    G = nx.cartesian_product(null, K10)
     assert_true(nx.is_isomorphic(G, null))
-    G = cartesian_product(null, P3)
+    G = nx.cartesian_product(null, P3)
     assert_true(nx.is_isomorphic(G, null))
-    G = cartesian_product(null, P10)
+    G = nx.cartesian_product(null, P10)
     assert_true(nx.is_isomorphic(G, null))
-    G = cartesian_product(empty10, null)
+    G = nx.cartesian_product(empty10, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = cartesian_product(K3, null)
+    G = nx.cartesian_product(K3, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = cartesian_product(K10, null)
+    G = nx.cartesian_product(K10, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = cartesian_product(P3, null)
+    G = nx.cartesian_product(P3, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = cartesian_product(P10, null)
+    G = nx.cartesian_product(P10, null)
     assert_true(nx.is_isomorphic(G, null))
 
 
@@ -157,12 +157,12 @@ def test_cartesian_product_size():
     K5 = nx.complete_graph(5)
     P5 = nx.path_graph(5)
     K3 = nx.complete_graph(3)
-    G = cartesian_product(P5, K3)
+    G = nx.cartesian_product(P5, K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
     assert_equal(nx.number_of_edges(G),
                  nx.number_of_edges(P5) * nx.number_of_nodes(K3) +
                  nx.number_of_edges(K3) * nx.number_of_nodes(P5))
-    G = cartesian_product(K3, K5)
+    G = nx.cartesian_product(K3, K5)
     assert_equal(nx.number_of_nodes(G), 3 * 5)
     assert_equal(nx.number_of_edges(G),
                  nx.number_of_edges(K5) * nx.number_of_nodes(K3) +
@@ -174,19 +174,19 @@ def test_cartesian_product_classic():
     P2 = nx.path_graph(2)
     P3 = nx.path_graph(3)
     # cube = 2-path X 2-path
-    G = cartesian_product(P2, P2)
-    G = cartesian_product(P2, G)
+    G = nx.cartesian_product(P2, P2)
+    G = nx.cartesian_product(P2, G)
     assert_true(nx.is_isomorphic(G, nx.cubical_graph()))
 
     # 3x3 grid
-    G = cartesian_product(P3, P3)
+    G = nx.cartesian_product(P3, P3)
     assert_true(nx.is_isomorphic(G, nx.grid_2d_graph(3, 3)))
 
 
 def test_cartesian_product_random():
     G = nx.erdos_renyi_graph(10, 2 / 10.)
     H = nx.erdos_renyi_graph(10, 2 / 10.)
-    GH = cartesian_product(G, H)
+    GH = nx.cartesian_product(G, H)
 
     for (u_G, u_H) in GH.nodes():
         for (v_G, v_H) in GH.nodes():
@@ -199,7 +199,7 @@ def test_cartesian_product_random():
 
 @raises(nx.NetworkXError)
 def test_lexicographic_product_raises():
-    P = lexicographic_product(nx.DiGraph(), nx.Graph())
+    P = nx.lexicographic_product(nx.DiGraph(), nx.Graph())
 
 
 def test_lexicographic_product_null():
@@ -210,28 +210,28 @@ def test_lexicographic_product_null():
     P3 = nx.path_graph(3)
     P10 = nx.path_graph(10)
     # null graph
-    G = lexicographic_product(null, null)
+    G = nx.lexicographic_product(null, null)
     assert_true(nx.is_isomorphic(G, null))
     # null_graph X anything = null_graph and v.v.
-    G = lexicographic_product(null, empty10)
+    G = nx.lexicographic_product(null, empty10)
     assert_true(nx.is_isomorphic(G, null))
-    G = lexicographic_product(null, K3)
+    G = nx.lexicographic_product(null, K3)
     assert_true(nx.is_isomorphic(G, null))
-    G = lexicographic_product(null, K10)
+    G = nx.lexicographic_product(null, K10)
     assert_true(nx.is_isomorphic(G, null))
-    G = lexicographic_product(null, P3)
+    G = nx.lexicographic_product(null, P3)
     assert_true(nx.is_isomorphic(G, null))
-    G = lexicographic_product(null, P10)
+    G = nx.lexicographic_product(null, P10)
     assert_true(nx.is_isomorphic(G, null))
-    G = lexicographic_product(empty10, null)
+    G = nx.lexicographic_product(empty10, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = lexicographic_product(K3, null)
+    G = nx.lexicographic_product(K3, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = lexicographic_product(K10, null)
+    G = nx.lexicographic_product(K10, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = lexicographic_product(P3, null)
+    G = nx.lexicographic_product(P3, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = lexicographic_product(P10, null)
+    G = nx.lexicographic_product(P10, null)
     assert_true(nx.is_isomorphic(G, null))
 
 
@@ -239,22 +239,22 @@ def test_lexicographic_product_size():
     K5 = nx.complete_graph(5)
     P5 = nx.path_graph(5)
     K3 = nx.complete_graph(3)
-    G = lexicographic_product(P5, K3)
+    G = nx.lexicographic_product(P5, K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = lexicographic_product(K3, K5)
+    G = nx.lexicographic_product(K3, K5)
     assert_equal(nx.number_of_nodes(G), 3 * 5)
 
 
 def test_lexicographic_product_combinations():
     P5 = nx.path_graph(5)
     K3 = nx.complete_graph(3)
-    G = lexicographic_product(P5, K3)
+    G = nx.lexicographic_product(P5, K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = lexicographic_product(nx.MultiGraph(P5), K3)
+    G = nx.lexicographic_product(nx.MultiGraph(P5), K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = lexicographic_product(P5, nx.MultiGraph(K3))
+    G = nx.lexicographic_product(P5, nx.MultiGraph(K3))
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = lexicographic_product(nx.MultiGraph(P5), nx.MultiGraph(K3))
+    G = nx.lexicographic_product(nx.MultiGraph(P5), nx.MultiGraph(K3))
     assert_equal(nx.number_of_nodes(G), 5 * 3)
 
     # No classic easily found classic results for lexicographic product
@@ -263,7 +263,7 @@ def test_lexicographic_product_combinations():
 def test_lexicographic_product_random():
     G = nx.erdos_renyi_graph(10, 2 / 10.)
     H = nx.erdos_renyi_graph(10, 2 / 10.)
-    GH = lexicographic_product(G, H)
+    GH = nx.lexicographic_product(G, H)
 
     for (u_G, u_H) in GH.nodes():
         for (v_G, v_H) in GH.nodes():
@@ -275,7 +275,7 @@ def test_lexicographic_product_random():
 
 @raises(nx.NetworkXError)
 def test_strong_product_raises():
-    P = strong_product(nx.DiGraph(), nx.Graph())
+    P = nx.strong_product(nx.DiGraph(), nx.Graph())
 
 
 def test_strong_product_null():
@@ -286,28 +286,28 @@ def test_strong_product_null():
     P3 = nx.path_graph(3)
     P10 = nx.path_graph(10)
     # null graph
-    G = strong_product(null, null)
+    G = nx.strong_product(null, null)
     assert_true(nx.is_isomorphic(G, null))
     # null_graph X anything = null_graph and v.v.
-    G = strong_product(null, empty10)
+    G = nx.strong_product(null, empty10)
     assert_true(nx.is_isomorphic(G, null))
-    G = strong_product(null, K3)
+    G = nx.strong_product(null, K3)
     assert_true(nx.is_isomorphic(G, null))
-    G = strong_product(null, K10)
+    G = nx.strong_product(null, K10)
     assert_true(nx.is_isomorphic(G, null))
-    G = strong_product(null, P3)
+    G = nx.strong_product(null, P3)
     assert_true(nx.is_isomorphic(G, null))
-    G = strong_product(null, P10)
+    G = nx.strong_product(null, P10)
     assert_true(nx.is_isomorphic(G, null))
-    G = strong_product(empty10, null)
+    G = nx.strong_product(empty10, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = strong_product(K3, null)
+    G = nx.strong_product(K3, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = strong_product(K10, null)
+    G = nx.strong_product(K10, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = strong_product(P3, null)
+    G = nx.strong_product(P3, null)
     assert_true(nx.is_isomorphic(G, null))
-    G = strong_product(P10, null)
+    G = nx.strong_product(P10, null)
     assert_true(nx.is_isomorphic(G, null))
 
 
@@ -315,22 +315,22 @@ def test_strong_product_size():
     K5 = nx.complete_graph(5)
     P5 = nx.path_graph(5)
     K3 = nx.complete_graph(3)
-    G = strong_product(P5, K3)
+    G = nx.strong_product(P5, K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = strong_product(K3, K5)
+    G = nx.strong_product(K3, K5)
     assert_equal(nx.number_of_nodes(G), 3 * 5)
 
 
 def test_strong_product_combinations():
     P5 = nx.path_graph(5)
     K3 = nx.complete_graph(3)
-    G = strong_product(P5, K3)
+    G = nx.strong_product(P5, K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = strong_product(nx.MultiGraph(P5), K3)
+    G = nx.strong_product(nx.MultiGraph(P5), K3)
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = strong_product(P5, nx.MultiGraph(K3))
+    G = nx.strong_product(P5, nx.MultiGraph(K3))
     assert_equal(nx.number_of_nodes(G), 5 * 3)
-    G = strong_product(nx.MultiGraph(P5), nx.MultiGraph(K3))
+    G = nx.strong_product(nx.MultiGraph(P5), nx.MultiGraph(K3))
     assert_equal(nx.number_of_nodes(G), 5 * 3)
 
     # No classic easily found classic results for strong product
@@ -339,7 +339,7 @@ def test_strong_product_combinations():
 def test_strong_product_random():
     G = nx.erdos_renyi_graph(10, 2 / 10.)
     H = nx.erdos_renyi_graph(10, 2 / 10.)
-    GH = strong_product(G, H)
+    GH = nx.strong_product(G, H)
 
     for (u_G, u_H) in GH.nodes():
         for (v_G, v_H) in GH.nodes():
@@ -365,11 +365,28 @@ def test_graph_power():
     G.add_edge(9, 2)
     H = nx.power(G, 2)
 
-    assert_edges_equal(list(H.edges()), [(0, 1), (0, 2), (0, 5), (0, 6), (0, 7), (1, 9),
-                             (1, 2), (1, 3), (1, 6), (2, 3), (2, 4), (2, 8),
-                             (2, 9), (3, 4), (3, 5), (3, 9), (4, 5), (4, 6),
-                             (5, 6), (5, 7), (6, 7), (6, 8), (7, 8), (7, 9),
-                             (8, 9)])
+    assert_edges_equal(list(H.edges()),
+                       [(0, 1), (0, 2), (0, 5), (0, 6), (0, 7), (1, 9),
+                        (1, 2), (1, 3), (1, 6), (2, 3), (2, 4), (2, 8),
+                        (2, 9), (3, 4), (3, 5), (3, 9), (4, 5), (4, 6),
+                        (5, 6), (5, 7), (6, 7), (6, 8), (7, 8), (7, 9),
+                        (8, 9)])
+
+
 @raises(ValueError)
 def test_graph_power_negative():
-    nx.power(nx.Graph(),-1)
+    nx.power(nx.Graph(), -1)
+
+
+@raises(nx.NetworkXError)
+def test_rooted_product_raises():
+    nx.rooted_product(nx.Graph(), nx.path_graph(2), 10)
+
+
+def test_rooted_product():
+    G = nx.cycle_graph(5)
+    H = nx.Graph()
+    H.add_edges_from([('a', 'b'), ('b', 'c'), ('b', 'd')])
+    R = nx.rooted_product(G, H, 'a')
+    assert_equal(len(R), len(G) * len(H))
+    assert_equal(R.size(), G.size() + len(G) * H.size())