Repository: networkx/networkx

Issue:
lowest_common_ancestor for multigraph
Hello,
I am working with a multigraph and would like to find the lowest common ancestor for nodes. It looks like there's no implementation for multigraph. Can you please help me with this?


Discussion:
Hmmm...  How would that differ for multigraph and graph?
Can't you just use the same function?
This is what I get
![image](https://user-images.githubusercontent.com/41593805/144683566-76877580-78d5-4b7f-abb6-88b1d2117f82.png)

Also, the source code says that it is not implemented for multigraph
https://networkx.org/documentation/stable/_modules/networkx/algorithms/lowest_common_ancestors.html#lowest_common_ancestor
Hmmm...  You are right -- the code explicitly checks for multigraphs and rules them out.
We should probably look at that more closely. I can't think of a reason why that should be done.

A workaround is to convert the graph to a `nx.DiGraph` for the `lowest_common_ancestor` calculation:
```python
nx.lowest_common_ancestor(nx.DiGraph(final), "HP:0003388", "HP:0011968")
``` 
That works for now :) 
Thanks for the help!
I really appreciate it!
@dschult can we apply the same solution internally as well for finding multigraph's `lowest_common_ancestor`, if so then I can create a PR for that.
I think of this method (converting the MultiDiGraph to a DiGraph so it produces a lowest_common_denominator) as a workaround. I'd rather figure out why the algorithm doesn't work for a `MultiDiGraph` and fix it.  It seems like it should.
Is the `@not_implemented_for("multigraph")` needed for any of the LCA algorithms? According to the docs, these functions should work on any non-null DAG, which can include a MultiDiGraph.

A previous change refactored the decorator from `@not_implemented_for("multigraph", "undirected")` to the current setting [here](https://github.com/networkx/networkx/pull/2603). However, I think this should only need the `@not_implemented_for("undirected")` decorator. This would also throw an exception for a Multigraph, but allow MultiDiGraph to work as expected. 

If that's the case, I'm happy to file a PR for this one. If not then apologies for the noise here!
Can you figure out whether this code for LCA algorithms works for multigraphs?  Maybe take out the "not_implemented_for" line for multigraph and try it on a few carefully constructed MultiDiGraphs. If there are inconsistent results we could start with a test that shows the problem -- that might make it possible to adapt the code to work for MultiDiGraph. If there are no inconsistent results then it might be possible to remove the "multigraph" restriction.  But it is very simple to covert the MultiDiGraph to a DiGraph. So it might be sufficient to add that to the doc_string and maybe include an example of doing that.

There are definitely places in the code where `degree` is used and other places with `dfs_preorder` used. The degree could easily be a problem for MultiDiGraphs.  For example, the in-degree > 1 check would probably change if we had a MultiDiGraph instead of a DiGraph. 

Resolving patch:
diff --git a/networkx/algorithms/lowest_common_ancestors.py b/networkx/algorithms/lowest_common_ancestors.py
--- a/networkx/algorithms/lowest_common_ancestors.py
+++ b/networkx/algorithms/lowest_common_ancestors.py
@@ -14,7 +14,6 @@
 
 
 @not_implemented_for("undirected")
-@not_implemented_for("multigraph")
 def all_pairs_lowest_common_ancestor(G, pairs=None):
     """Return the lowest common ancestor of all pairs or the provided pairs
 
@@ -112,7 +111,6 @@ def generate_lca_from_pairs(G, pairs):
 
 
 @not_implemented_for("undirected")
-@not_implemented_for("multigraph")
 def lowest_common_ancestor(G, node1, node2, default=None):
     """Compute the lowest common ancestor of the given pair of nodes.
 
@@ -150,7 +148,6 @@ def lowest_common_ancestor(G, node1, node2, default=None):
 
 
 @not_implemented_for("undirected")
-@not_implemented_for("multigraph")
 def tree_all_pairs_lowest_common_ancestor(G, root=None, pairs=None):
     r"""Yield the lowest common ancestor for sets of pairs in a tree.
 
@@ -237,7 +234,8 @@ def tree_all_pairs_lowest_common_ancestor(G, root=None, pairs=None):
                     msg = "No root specified and tree has multiple sources."
                     raise nx.NetworkXError(msg)
                 root = n
-            elif deg > 1:
+            # checking deg>1 is not sufficient for MultiDiGraphs
+            elif deg > 1 and len(G.pred[n]) > 1:
                 msg = "Tree LCA only defined on trees; use DAG routine."
                 raise nx.NetworkXError(msg)
     if root is None:


Test patch:
diff --git a/networkx/algorithms/tests/test_lowest_common_ancestors.py b/networkx/algorithms/tests/test_lowest_common_ancestors.py
--- a/networkx/algorithms/tests/test_lowest_common_ancestors.py
+++ b/networkx/algorithms/tests/test_lowest_common_ancestors.py
@@ -136,12 +136,6 @@ def test_tree_all_pairs_lca_not_implemented(self):
         with pytest.raises(NNI):
             next(all_pairs_lca(G))
         pytest.raises(NNI, nx.lowest_common_ancestor, G, 0, 1)
-        G = nx.MultiDiGraph([(0, 1)])
-        with pytest.raises(NNI):
-            next(tree_all_pairs_lca(G))
-        with pytest.raises(NNI):
-            next(all_pairs_lca(G))
-        pytest.raises(NNI, nx.lowest_common_ancestor, G, 0, 1)
 
     def test_tree_all_pairs_lca_trees_without_LCAs(self):
         G = nx.DiGraph()
@@ -150,6 +144,41 @@ def test_tree_all_pairs_lca_trees_without_LCAs(self):
         assert ans == [((3, 3), 3)]
 
 
+class TestMultiTreeLCA(TestTreeLCA):
+    @classmethod
+    def setup_class(cls):
+        cls.DG = nx.MultiDiGraph()
+        edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
+        cls.DG.add_edges_from(edges)
+        cls.ans = dict(tree_all_pairs_lca(cls.DG, 0))
+        # add multiedges
+        cls.DG.add_edges_from(edges)
+
+        gold = {(n, n): n for n in cls.DG}
+        gold.update({(0, i): 0 for i in range(1, 7)})
+        gold.update(
+            {
+                (1, 2): 0,
+                (1, 3): 1,
+                (1, 4): 1,
+                (1, 5): 0,
+                (1, 6): 0,
+                (2, 3): 0,
+                (2, 4): 0,
+                (2, 5): 2,
+                (2, 6): 2,
+                (3, 4): 1,
+                (3, 5): 0,
+                (3, 6): 0,
+                (4, 5): 0,
+                (4, 6): 0,
+                (5, 6): 2,
+            }
+        )
+
+        cls.gold = gold
+
+
 class TestDAGLCA:
     @classmethod
     def setup_class(cls):
@@ -326,6 +355,62 @@ def test_all_pairs_lca_one_pair_gh4942(self):
         assert nx.lowest_common_ancestor(G, 1, 3) == 2
 
 
+class TestMultiDiGraph_DAGLCA(TestDAGLCA):
+    @classmethod
+    def setup_class(cls):
+        cls.DG = nx.MultiDiGraph()
+        nx.add_path(cls.DG, (0, 1, 2, 3))
+        # add multiedges
+        nx.add_path(cls.DG, (0, 1, 2, 3))
+        nx.add_path(cls.DG, (0, 4, 3))
+        nx.add_path(cls.DG, (0, 5, 6, 8, 3))
+        nx.add_path(cls.DG, (5, 7, 8))
+        cls.DG.add_edge(6, 2)
+        cls.DG.add_edge(7, 2)
+
+        cls.root_distance = nx.shortest_path_length(cls.DG, source=0)
+
+        cls.gold = {
+            (1, 1): 1,
+            (1, 2): 1,
+            (1, 3): 1,
+            (1, 4): 0,
+            (1, 5): 0,
+            (1, 6): 0,
+            (1, 7): 0,
+            (1, 8): 0,
+            (2, 2): 2,
+            (2, 3): 2,
+            (2, 4): 0,
+            (2, 5): 5,
+            (2, 6): 6,
+            (2, 7): 7,
+            (2, 8): 7,
+            (3, 3): 3,
+            (3, 4): 4,
+            (3, 5): 5,
+            (3, 6): 6,
+            (3, 7): 7,
+            (3, 8): 8,
+            (4, 4): 4,
+            (4, 5): 0,
+            (4, 6): 0,
+            (4, 7): 0,
+            (4, 8): 0,
+            (5, 5): 5,
+            (5, 6): 5,
+            (5, 7): 5,
+            (5, 8): 5,
+            (6, 6): 6,
+            (6, 7): 5,
+            (6, 8): 6,
+            (7, 7): 7,
+            (7, 8): 7,
+            (8, 8): 8,
+        }
+        cls.gold.update(((0, n), 0) for n in cls.DG)
+
+
 def test_all_pairs_lca_self_ancestors():
     """Self-ancestors should always be the node itself, i.e. lca of (0, 0) is 0.
     See gh-4458."""
@@ -334,6 +419,9 @@ def test_all_pairs_lca_self_ancestors():
     G.add_nodes_from(range(5))
     G.add_edges_from([(1, 0), (2, 0), (3, 2), (4, 1), (4, 3)])
 
-    assert all(
-        u == v == a for (u, v), a in nx.all_pairs_lowest_common_ancestor(G) if u == v
-    )
+    ap_lca = nx.all_pairs_lowest_common_ancestor
+    assert all(u == v == a for (u, v), a in ap_lca(G) if u == v)
+    MG = nx.MultiDiGraph(G)
+    assert all(u == v == a for (u, v), a in ap_lca(MG) if u == v)
+    MG.add_edges_from([(1, 0), (2, 0)])
+    assert all(u == v == a for (u, v), a in ap_lca(MG) if u == v)