Repository: networkx/networkx

Issue:
Refactor `transitive_closure` to support multigraphs
By definition (see below and #5016) `transitive_closure` also works with multigraphs.
* **transitive closure of G** is the graph G* = (V, E*), where E* = {(i, j) : there is a path from vertex i to vertex j in G}"

But now this function does not work as expected.

### Current Behavior
```python
>>> import networkx as nx
>>> g = nx.MultiDiGraph([(0, 1), (1, 2)])
>>> print(nx.transitive_closure(g).edges())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<class 'networkx.utils.decorators.argmap'> compilation 8", line 4, in argmap_transitive_closure_5
  File "/home/vdshk/.local/lib/python3.8/site-packages/networkx/algorithms/dag.py", line 576, in transitive_closure
    TC.add_edges_from(edges)
  File "/home/vdshk/.local/lib/python3.8/site-packages/networkx/classes/multigraph.py", line 550, in add_edges_from
    for e in ebunch_to_add:
  File "/home/vdshk/.local/lib/python3.8/site-packages/networkx/algorithms/dag.py", line 575, in <genexpr>
    edges = ((v, w) for u, w in nx.edge_dfs(G, v))
ValueError: too many values to unpack (expected 2)
```

### Expected Behavior
```python
>>> import networkx as nx
>>> g = nx.MultiDiGraph([(0, 1), (1, 2)])
>>> print(nx.transitive_closure(g).edges())
[(0, 1), (0, 2), (1, 2)]
```

### Environment
Python version: `3.8.10`
NetworkX version: `2.6.2`

### Proposal

I think this function needs to be refactored.
Maybe it should be done in #5042 PR.
What do you think?


Discussion:
+1 for adding support for multigraphs. I'd recommend putting it in a separate PR though rather than adding it to #5042 - IMO it would make it easier to review if there were separate PRs for each proposal. If you want to work on it before #5042 is in, you can always open a new PR that depends on #5042 (i.e. originates from a commit on that branch) and just mark it as draft for now.

Resolving patch:
diff --git a/networkx/algorithms/dag.py b/networkx/algorithms/dag.py
--- a/networkx/algorithms/dag.py
+++ b/networkx/algorithms/dag.py
@@ -601,9 +601,8 @@ def is_aperiodic(G):
         return g == 1 and nx.is_aperiodic(G.subgraph(set(G) - set(levels)))
 
 
-@not_implemented_for("undirected")
 def transitive_closure(G, reflexive=False):
-    """Returns transitive closure of a directed graph
+    """Returns transitive closure of a graph
 
     The transitive closure of G = (V,E) is a graph G+ = (V,E+) such that
     for all v, w in V there is an edge (v, w) in E+ if and only if there
@@ -617,8 +616,8 @@ def transitive_closure(G, reflexive=False):
 
     Parameters
     ----------
-    G : NetworkX DiGraph
-        A directed graph
+    G : NetworkX Graph
+        A directed/undirected graph/multigraph.
     reflexive : Bool or None, optional (default: False)
         Determines when cycles create self-loops in the Transitive Closure.
         If True, trivial cycles (length 0) create self-loops. The result
@@ -628,13 +627,13 @@ def transitive_closure(G, reflexive=False):
 
     Returns
     -------
-    NetworkX DiGraph
+    NetworkX graph
         The transitive closure of `G`
 
     Raises
     ------
-    NetworkXNotImplemented
-        If `G` is not directed
+    NetworkXError
+        If `reflexive` not in `{None, True, False}`
 
     Examples
     --------
@@ -673,28 +672,25 @@ def transitive_closure(G, reflexive=False):
 
     References
     ----------
-    .. [1] http://www.ics.uci.edu/~eppstein/PADS/PartialOrder.py
-
-    TODO this function applies to all directed graphs and is probably misplaced
-         here in dag.py
+    .. [1] https://www.ics.uci.edu/~eppstein/PADS/PartialOrder.py
     """
-    if reflexive is None:
-        TC = G.copy()
-        for v in G:
-            edges = ((v, u) for u in nx.dfs_preorder_nodes(G, v) if v != u)
-            TC.add_edges_from(edges)
-        return TC
-    if reflexive is True:
-        TC = G.copy()
-        for v in G:
-            edges = ((v, u) for u in nx.dfs_preorder_nodes(G, v))
-            TC.add_edges_from(edges)
-        return TC
-    # reflexive is False
     TC = G.copy()
+
+    if reflexive not in {None, True, False}:
+        raise nx.NetworkXError("Incorrect value for the parameter `reflexive`")
+
     for v in G:
-        edges = ((v, w) for u, w in nx.edge_dfs(G, v))
-        TC.add_edges_from(edges)
+        if reflexive is None:
+            TC.add_edges_from(((v, u) for u in nx.descendants(G, v) if u not in TC[v]))
+        elif reflexive is True:
+            TC.add_edges_from(
+                ((v, u) for u in nx.descendants(G, v) | {v} if u not in TC[v])
+            )
+        elif reflexive is False:
+            TC.add_edges_from(
+                ((v, e[1]) for e in nx.edge_bfs(G, v) if e[1] not in TC[v])
+            )
+
     return TC
 
 


Test patch:
diff --git a/networkx/algorithms/tests/test_dag.py b/networkx/algorithms/tests/test_dag.py
--- a/networkx/algorithms/tests/test_dag.py
+++ b/networkx/algorithms/tests/test_dag.py
@@ -290,8 +290,18 @@ def test_transitive_closure(self):
         solution = [(1, 2), (2, 1), (2, 3), (3, 2), (1, 3), (3, 1)]
         soln = sorted(solution + [(n, n) for n in G])
         assert edges_equal(sorted(nx.transitive_closure(G).edges()), soln)
+
         G = nx.Graph([(1, 2), (2, 3), (3, 4)])
-        pytest.raises(nx.NetworkXNotImplemented, nx.transitive_closure, G)
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution)
+
+        G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution)
+
+        G = nx.MultiDiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution)
 
         # test if edge data is copied
         G = nx.DiGraph([(1, 2, {"a": 3}), (2, 3, {"b": 0}), (3, 4)])
@@ -305,6 +315,10 @@ def test_transitive_closure(self):
         for u, v in G.edges():
             assert G.get_edge_data(u, v) == H.get_edge_data(u, v)
 
+        G = nx.Graph()
+        with pytest.raises(nx.NetworkXError):
+            nx.transitive_closure(G, reflexive="wrong input")
+
     def test_reflexive_transitive_closure(self):
         G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
         solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
@@ -330,6 +344,30 @@ def test_reflexive_transitive_closure(self):
         assert edges_equal(sorted(nx.transitive_closure(G, None).edges()), solution)
         assert edges_equal(sorted(nx.transitive_closure(G, True).edges()), soln)
 
+        G = nx.Graph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
+        assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
+
+        G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
+        assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
+
+        G = nx.MultiDiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
+        assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
+
     def test_transitive_closure_dag(self):
         G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
         transitive_closure = nx.algorithms.dag.transitive_closure_dag