Repository: networkx/networkx

Issue:
transitive_closure loses self loops
Consider the following example:

    import numpy as np
    import networkx as nx
    a = np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]])
    G = nx.from_numpy_matrix(a, create_using=nx.MultiDiGraph())
    T = nx.transitive_closure(G)
    print(nx.to_numpy_matrix(T))

The transitive closure lacks the expected self loops. Why? (The documentation link does not work, and the docstring implies that self loops should be kept.) By "expected", I mean "according to standard definitions", such as the Wikipedia definition. I anticipate that a different definition is being used, but what is it?

The current [answer at StackExchange](https://stackoverflow.com/questions/52710488/transitive-closure-in-networkx/52711544#52711544) claims this is an implementation error.


Discussion:
I would claim it is a definitional difference rather than an implementation error.
The question is whether self-loops should be added to the closure.

The documentation link you refer to is the link to eppstein's webpage, right?

Looking at the Wikipedia page and it's reference to Nuutila (1995):
http://www.cs.hut.fi/~enu/thesis.pdf
I tend to agree that these loops should be included.

Thanks!
Yes, the current documentation link is http://www.ics.uci.edu/~eppstein/PADS/PartialOrder.py, which fails.  [Note: This link does work as of September 2019.]

I have not been able to find a definition of the transitive closure of a directed graph that does not imply that self loops should be included.  Absence of evidence may not be evidence of absence, but my belief is that a *closure* must embed the original relation and all its reachable points.
We should make this clear at the very least -- and probably change the current default behavior. But as the link you reference above suggests, some people do not expect self-loops in the closure. Just because the mathematical meaning of a word like closure seems clear doesn't mean the word will be clear. Besides, your definition of "reachable points" may be different from others.  

It seems to me there are two types of self-loops that arise and we need to be clear about which is treated which way.  Is a node reachable from itself in zero steps? If so, we need self-loops on every node. I believe this is called "reflexive transitive closure".  If we enforce that paths must have positive length, then we only get self-loops when there exists a cycle that includes that node.

I gather that you are saying that we should include self-loops due to cycles in the graph, but exclude self-loops if no such cycle exists.  If this is what you are suggesting, then I agree.
Yes, you understand me.  But I would understand an option (say, `forceReflexive`) for those who want the reflexive transitive closure to get it.
I agree..  An option ```reflexive=True``` could include all selfloops, ```reflexive=False``` could be the default with self-loops only for nodes on cycles.  And we could add an option: ```reflexive=None``` which does not include any self-loops -- if that doesn't make the code to convoluted.

In any case, it means we'll have to separate this from using ```dfs_preorder_nodes``` because we need to see when cycles are formed.

Resolving patch:
diff --git a/networkx/algorithms/dag.py b/networkx/algorithms/dag.py
--- a/networkx/algorithms/dag.py
+++ b/networkx/algorithms/dag.py
@@ -481,18 +481,30 @@ def is_aperiodic(G):
 
 
 @not_implemented_for('undirected')
-def transitive_closure(G):
+def transitive_closure(G, reflexive=False):
     """ Returns transitive closure of a directed 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
-    is a non-null path from v to w in G.
+    for all v, w in V there is an edge (v, w) in E+ if and only if there
+    is a path from v to w in G.
+
+    Handling of paths from v to v has some flexibility within this definition.
+    A reflexive transitive closure creates a self-loop for the path
+    from v to v of length 0. The usual transitive closure creates a
+    self-loop only if a cycle exists (a path from v to v with length > 0).
+    We also allow an option for no self-loops.
 
     Parameters
     ----------
     G : NetworkX DiGraph
         A directed graph
-
+    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
+        is a reflexive tranistive closure of G.
+        If False (the default) non-trivial cycles create self-loops.
+        If None, self-loops are not created.
+        
     Returns
     -------
     NetworkX DiGraph
@@ -510,10 +522,23 @@ def transitive_closure(G):
     TODO this function applies to all directed graphs and is probably misplaced
          here in dag.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()
     for v in G:
-        TC.add_edges_from((v, u) for u in nx.dfs_preorder_nodes(G, source=v)
-                          if v != u)
+        edges = ((v, w) for u, w in nx.edge_dfs(G, v))
+        TC.add_edges_from(edges)
     return TC
 
 
@@ -525,7 +550,7 @@ def transitive_closure_dag(G, topo_order=None):
     if the graph has a cycle.
 
     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
+    for all v, w in V there is an edge (v, w) in E+ if and only if there
     is a non-null path from v to w in G.
 
     Parameters


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,27 +290,55 @@ def test_descendants(self):
 
     def test_transitive_closure(self):
         G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
-        transitive_closure = nx.algorithms.dag.transitive_closure
         solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
-        assert_edges_equal(transitive_closure(G).edges(), solution)
+        assert_edges_equal(nx.transitive_closure(G).edges(), solution)
         G = nx.DiGraph([(1, 2), (2, 3), (2, 4)])
         solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]
-        assert_edges_equal(transitive_closure(G).edges(), solution)
+        assert_edges_equal(nx.transitive_closure(G).edges(), solution)
+        G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
+        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)])
-        assert_raises(nx.NetworkXNotImplemented, transitive_closure, G)
+        assert_raises(nx.NetworkXNotImplemented, nx.transitive_closure, G)
 
         # test if edge data is copied
         G = nx.DiGraph([(1, 2, {"a": 3}), (2, 3, {"b": 0}), (3, 4)])
-        H = transitive_closure(G)
+        H = nx.transitive_closure(G)
         for u, v in G.edges():
             assert_equal(G.get_edge_data(u, v), H.get_edge_data(u, v))
 
         k = 10
         G = nx.DiGraph((i, i + 1, {"f": "b", "weight": i}) for i in range(k))
-        H = transitive_closure(G)
+        H = nx.transitive_closure(G)
         for u, v in G.edges():
             assert_equal(G.get_edge_data(u, v), H.get_edge_data(u, v))
 
+    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)]
+        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.DiGraph([(1, 2), (2, 3), (2, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 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.DiGraph([(1, 2), (2, 3), (3, 1)])
+        solution = sorted([(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)
+        assert_edges_equal(sorted(nx.transitive_closure(G, False).edges()), soln)
+        assert_edges_equal(sorted(nx.transitive_closure(G, None).edges()), solution)
+        assert_edges_equal(sorted(nx.transitive_closure(G, True).edges()), soln)
+
     def test_transitive_closure_dag(self):
         G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
         transitive_closure = nx.algorithms.dag.transitive_closure_dag