Repository: networkx/networkx

Issue:
difference fails when applied to MultiDiGraph and Graph
Take G as a MultiDiGraph

Get a spanning tree assuming it is undirected

``` python
T = nx.minimum_spanning_tree (nx.MultiGraph (G))
```

Take the difference to (almost) get the chords of G

``` python
nx.difference (G,T)
```

The following error is produced

``` python
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-77-8d39870ba2cc> in <module>()
----> 1 nx.difference(G,T)

/usr/lib/pymodules/python2.7/networkx/algorithms/operators/binary.pyc in difference(G, H)
    223         edges=G.edges_iter()
    224     for e in edges:
--> 225         if not H.has_edge(*e):
    226             R.add_edge(*e)
    227     return R

TypeError: has_edge() takes exactly 3 arguments (4 given)
```

This is due to the fact that the iterator edges is created with key=True, but since T is just a Graph (should be MultiGraph) the method has_edge fails.



Discussion:
The following patch fixes the problem. I think most binary operations have to be fixed this way.

``` patch
# HG changeset patch
# User Juan Pablo Carbajal <ajuanpi+dev@gmail.com>
# Date 1361641591 -3600
# Node ID 0f47bd8643caab842eed98e5fda6fadfe65c7ef0
# Parent  94139db0a8fe39c832ba1779b1c17088c7440e1d
difference: compatibility between graphs and multigraphs

diff --git a/networkx/algorithms/operators/binary.py b/networkx/algorithms/operators/binary.py
--- a/networkx/algorithms/operators/binary.py
+++ b/networkx/algorithms/operators/binary.py
@@ -225,6 +225,7 @@

     if G.is_multigraph():
         edges=G.edges_iter(keys=True)
+        H = nx.MultiGraph (H)
     else:
         edges=G.edges_iter()
     for e in edges:
```

That fix might work.  But you could also in your case call the function with 

```
nx.difference (G,nx.MultiGraph(T))
```

to have the same effect.

I wonder if it might be safer instead to forbid comparisons between graphs and multigraphs.  That way there wouldn't be ambiguity in the case, e.g. when the multigraph G has two edges between a pair of nodes and the graph H only one.  I could see an argument for the difference() to be either 1 edge or 0 edges.  If we force the user to "cast" one of the graphs to the same type then it would be the users choice.

Since there is an if for multigrpah inputs, and th eidea is that an edge of a multigraph is univocally indetified with its key (that is why keys=true) then the difference is well defined. That sems to be the original intention of the if, it seem that the cast was missing. But maybe I am wrong.

Question: Can two edges of a multigraph joining the sames nodes have the same key? It seems to me they can't.

They keys used for MultiGraph() edges differentiate the edges.  So they can't be the same (if you try to add another edge with the same key 

``` python
In [1]: import networkx as nx

In [2]: G = nx.MultiGraph()

In [3]: G.add_edge(1,2,key='a')

In [4]: G.edges(keys=True)
Out[4]: [(1, 2, 'a')]

In [5]: G.add_edge(1,2,key='a')

In [6]: G.edges(keys=True)
Out[6]: [(1, 2, 'a')]

In [7]: G.add_edge(1,2,key='b')

In [8]: G.edges(keys=True)
Out[8]: [(1, 2, 'a'), (1, 2, 'b')]
```

I'm not sure we thought very carefully how the binary operators work with graphs and multigraphs when we wrote them.  When I look at it now it seems there could be some ambiguity (as above) in some cases when mixing graphs and multigraphs.  I was suggesting maybe we resolve that by disallowing the mixed case.  But I'm open to other suggestions too.

Hi,
Yes , that was what I understood. I would say that whenever the operator is well defined (no ambiguities as you pointed out) then the operator should be allowed.
Looking at the code I thought that the multigraph case it was just not complete. In the mixed case I am using (a multigraph and a tree derived from it) the keys of the edges are conserved, then the difference is well defined. In a general case it mgith indeed be ambiguous, but I can't put the finger on it. Do you have an example where the difference between a multigraph and a graph would give an ambiguous result? 
Or maybe one could just issue a warning for the mixed case!
"Warning: difference between different types of graphs may give unexpected results."

I'm thinking of something like this:

``` python
In [1]: import networkx as nx

In [2]: G = nx.MultiGraph()

In [3]: G.add_edge(1,2,key='a')

In [4]: G.add_edge(1,2,key='b')

In [5]: H = nx.Graph()

In [6]: H.add_edge(1,2)
```

These two results might be confusing:

```
In [8]: nx.difference(H,G).edges()
Out[8]: []
```

(and if we use your suggested fix)

```
In [9]: nx.difference(G,nx.MultiGraph(H)).edges()
Out[9]: [(1, 2), (1, 2)]
```

You could argue that it is the right answer but I think it could be simpler (for sure in the code) and safer to not allow mixed type graphs.

Indeed, as I see it. When you cast H to a multigraph the edge receives the default key (a "0") therefore the answer is right. Specially when you check the keys (that is the edge of a multigraph is composed by the nodes and the key). If you do

``` python
import networkx as nx
G = nx.MultiGraph()
G.add_edge(1,2,key=0)
G.add_edge(1,2,key='a')
H = nx.Graph()
H.add_edge(1,2)
nx.difference(G,nx.MultiGraph(H)).edges(keys=True)
```

You get the right answer. So if not forbidden one could issue a warning. If forbidden one should report the right error and not just let the function fail (in this case the if would have another if checking whether H is a multigraph and if it is not then issue the corresponding error). Any solution is fine with me.

Given all of the logic/errors/warnings that would need to be put in the code I'm still in favor of raising an error if the two graphs don't match types.  That way the user will have to make a decision about what it means to compare edges of graphs and multigraphs with edge keys.  But I think it is better that the user do that than for us try to guess or catch all of the possible cases.

Sounds good. I am going through some hectic time right now. But I could send you a patch in the following weeks.


Resolving patch:
diff --git a/networkx/algorithms/operators/all.py b/networkx/algorithms/operators/all.py
--- a/networkx/algorithms/operators/all.py
+++ b/networkx/algorithms/operators/all.py
@@ -1,6 +1,6 @@
 """Operations on many graphs.
 """
-#    Copyright (C) 2012 by
+#    Copyright (C) 2013 by
 #    Aric Hagberg <hagberg@lanl.gov>
 #    Dan Schult <dschult@colgate.edu>
 #    Pieter Swart <swart@lanl.gov>
@@ -11,7 +11,7 @@
 except ImportError: # Python3 has zip_longest
     from itertools import zip_longest
 import networkx as nx
-from networkx.utils import is_string_like
+#from networkx.utils import is_string_like
 
 __author__ = """\n""".join([ 'Robert King <kingrobertking@gmail.com>',
                              'Aric Hagberg <aric.hagberg@gmail.com>'])
@@ -55,10 +55,10 @@ def union_all(graphs, rename=(None,) , name=None):
     union
     disjoint_union_all
     """
-    graphs_names = zip_longest(graphs,rename)
+    graphs_names = zip_longest(graphs, rename)
     U, gname = next(graphs_names)
-    for H,hname in graphs_names:
-        U = nx.union(U, H, (gname,hname),name=name)
+    for H, hname in graphs_names:
+        U = nx.union(U, H, (gname, hname), name=name)
         gname = None
     return U
 
diff --git a/networkx/algorithms/operators/binary.py b/networkx/algorithms/operators/binary.py
--- a/networkx/algorithms/operators/binary.py
+++ b/networkx/algorithms/operators/binary.py
@@ -1,7 +1,7 @@
 """
 Operations on graphs including union, intersection, difference.
 """
-#    Copyright (C) 2004-2012 by
+#    Copyright (C) 2004-2013 by
 #    Aric Hagberg <hagberg@lanl.gov>
 #    Dan Schult <dschult@colgate.edu>
 #    Pieter Swart <swart@lanl.gov>
@@ -9,7 +9,7 @@
 #    BSD license.
 import networkx as nx
 from networkx.utils import is_string_like
-__author__ = """\n""".join(['Aric Hagberg (hagberg@lanl.gov)',
+__author__ = """\n""".join(['Aric Hagberg <aric.hagberg@gmail.com>',
                            'Pieter Swart (swart@lanl.gov)',
                            'Dan Schult(dschult@colgate.edu)'])
 __all__ = ['union', 'compose', 'disjoint_union', 'intersection',
@@ -53,6 +53,8 @@ def union(G, H, rename=(None, None), name=None):
     --------
     disjoint_union
     """
+    if not G.is_multigraph() == H.is_multigraph():
+        raise nx.NetworkXError('G and H must both be graphs or multigraphs.')
     # Union is the same type as G
     R = G.__class__()
     if name is None:
@@ -65,13 +67,13 @@ def add_prefix(graph, prefix):
             return graph
         def label(x):
             if is_string_like(x):
-                name=prefix+x
+                name = prefix+x
             else:
-                name=prefix+repr(x)
+                name = prefix+repr(x)
             return name
         return nx.relabel_nodes(graph, label)
-    G = add_prefix(G,rename[0])
-    H = add_prefix(H,rename[1])
+    G = add_prefix(G, rename[0])
+    H = add_prefix(H, rename[1])
     if set(G) & set(H):
         raise nx.NetworkXError('The node sets of G and H are not disjoint.',
                                'Use appropriate rename=(Gprefix,Hprefix)'
@@ -101,7 +103,7 @@ def label(x):
 
     return R
 
-def disjoint_union(G,H):
+def disjoint_union(G, H):
     """ Return the disjoint union of graphs G and H.
 
     This algorithm forces distinct integer node labels.
@@ -127,10 +129,10 @@ def disjoint_union(G,H):
     to the union graph.  If a graph attribute is present in both
     G and H the value from H is used.
     """
-    R1=nx.convert_node_labels_to_integers(G)
-    R2=nx.convert_node_labels_to_integers(H,first_label=len(R1))
-    R=union(R1,R2)
-    R.name="disjoint_union( %s, %s )"%(G.name,H.name)
+    R1 = nx.convert_node_labels_to_integers(G)
+    R2 = nx.convert_node_labels_to_integers(H, first_label=len(R1))
+    R = union(R1, R2)
+    R.name = "disjoint_union( %s, %s )"%(G.name, H.name)
     R.graph.update(G.graph)
     R.graph.update(H.graph)
     return R
@@ -164,26 +166,27 @@ def intersection(G, H):
     >>> R.remove_nodes_from(n for n in G if n not in H)
     """
     # create new graph
-    R=nx.create_empty_copy(G)
+    R = nx.create_empty_copy(G)
 
-    R.name="Intersection of (%s and %s)"%(G.name, H.name)
-
-    if set(G)!=set(H):
+    R.name = "Intersection of (%s and %s)"%(G.name, H.name)
+    if not G.is_multigraph() == H.is_multigraph():
+        raise nx.NetworkXError('G and H must both be graphs or multigraphs.')
+    if set(G) != set(H):
         raise nx.NetworkXError("Node sets of graphs are not equal")
 
-    if G.number_of_edges()<=H.number_of_edges():
+    if G.number_of_edges() <= H.number_of_edges():
         if G.is_multigraph():
-            edges=G.edges_iter(keys=True)
+            edges = G.edges_iter(keys=True)
         else:
-            edges=G.edges_iter()
+            edges = G.edges_iter()
         for e in edges:
             if H.has_edge(*e):
                 R.add_edge(*e)
     else:
         if H.is_multigraph():
-            edges=H.edges_iter(keys=True)
+            edges = H.edges_iter(keys=True)
         else:
-            edges=H.edges_iter()
+            edges = H.edges_iter()
         for e in edges:
             if G.has_edge(*e):
                 R.add_edge(*e)
@@ -211,22 +214,24 @@ def difference(G, H):
     with the attributes (including edge data) from G use remove_nodes_from()
     as follows:
 
-    >>> G=nx.path_graph(3)
-    >>> H=nx.path_graph(5)
-    >>> R=G.copy()
+    >>> G = nx.path_graph(3)
+    >>> H = nx.path_graph(5)
+    >>> R = G.copy()
     >>> R.remove_nodes_from(n for n in G if n in H)
     """
     # create new graph
-    R=nx.create_empty_copy(G)
-    R.name="Difference of (%s and %s)"%(G.name, H.name)
+    if not G.is_multigraph() == H.is_multigraph():
+        raise nx.NetworkXError('G and H must both be graphs or multigraphs.')
+    R = nx.create_empty_copy(G)
+    R.name = "Difference of (%s and %s)"%(G.name, H.name)
 
-    if set(G)!=set(H):
+    if set(G) != set(H):
         raise nx.NetworkXError("Node sets of graphs not equal")
 
     if G.is_multigraph():
-        edges=G.edges_iter(keys=True)
+        edges = G.edges_iter(keys=True)
     else:
-        edges=G.edges_iter()
+        edges = G.edges_iter()
     for e in edges:
         if not H.has_edge(*e):
             R.add_edge(*e)
@@ -252,21 +257,23 @@ def symmetric_difference(G, H):
     graph.
     """
     # create new graph
-    R=nx.create_empty_copy(G)
-    R.name="Symmetric difference of (%s and %s)"%(G.name, H.name)
+    if not G.is_multigraph() == H.is_multigraph():
+        raise nx.NetworkXError('G and H must both be graphs or multigraphs.')
+    R = nx.create_empty_copy(G)
+    R.name = "Symmetric difference of (%s and %s)"%(G.name, H.name)
 
-    if set(G)!=set(H):
+    if set(G) != set(H):
         raise nx.NetworkXError("Node sets of graphs not equal")
 
-    gnodes=set(G) # set of nodes in G
-    hnodes=set(H) # set of nodes in H
-    nodes=gnodes.symmetric_difference(hnodes)
+    gnodes = set(G) # set of nodes in G
+    hnodes = set(H) # set of nodes in H
+    nodes = gnodes.symmetric_difference(hnodes)
     R.add_nodes_from(nodes)
 
     if G.is_multigraph():
-        edges=G.edges_iter(keys=True)
+        edges = G.edges_iter(keys=True)
     else:
-        edges=G.edges_iter()
+        edges = G.edges_iter()
     # we could copy the data here but then this function doesn't
     # match intersection and difference
     for e in edges:
@@ -274,9 +281,9 @@ def symmetric_difference(G, H):
             R.add_edge(*e)
 
     if H.is_multigraph():
-        edges=H.edges_iter(keys=True)
+        edges = H.edges_iter(keys=True)
     else:
-        edges=H.edges_iter()
+        edges = H.edges_iter()
     for e in edges:
         if not G.has_edge(*e):
             R.add_edge(*e)
@@ -305,18 +312,20 @@ def compose(G, H, name=None):
     It is recommended that G and H be either both directed or both undirected.
     Attributes from H take precedent over attributes from G.
     """
+    if not G.is_multigraph() == H.is_multigraph():
+        raise nx.NetworkXError('G and H must both be graphs or multigraphs.')
     if name is None:
-        name="compose( %s, %s )"%(G.name,H.name)
-    R=G.__class__()
-    R.name=name
+        name = "compose( %s, %s )"%(G.name,H.name)
+    R = G.__class__()
+    R.name = name
     R.add_nodes_from(H.nodes())
     R.add_nodes_from(G.nodes())
     if H.is_multigraph():
-        R.add_edges_from(H.edges_iter(keys=True,data=True))
+        R.add_edges_from(H.edges_iter(keys=True, data=True))
     else:
         R.add_edges_from(H.edges_iter(data=True))
     if G.is_multigraph():
-        R.add_edges_from(G.edges_iter(keys=True,data=True))
+        R.add_edges_from(G.edges_iter(keys=True, data=True))
     else:
         R.add_edges_from(G.edges_iter(data=True))
 
diff --git a/networkx/algorithms/operators/unary.py b/networkx/algorithms/operators/unary.py
--- a/networkx/algorithms/operators/unary.py
+++ b/networkx/algorithms/operators/unary.py
@@ -1,13 +1,12 @@
 """Unary operations on graphs"""
-#    Copyright (C) 2004-2012 by
+#    Copyright (C) 2004-2013 by
 #    Aric Hagberg <hagberg@lanl.gov>
 #    Dan Schult <dschult@colgate.edu>
 #    Pieter Swart <swart@lanl.gov>
 #    All rights reserved.
 #    BSD license.
 import networkx as nx
-from networkx.utils import is_string_like
-__author__ = """\n""".join(['Aric Hagberg (hagberg@lanl.gov)',
+__author__ = """\n""".join(['Aric Hagberg <aric.hagberg@gmail.com>',
                            'Pieter Swart (swart@lanl.gov)',
                            'Dan Schult(dschult@colgate.edu)'])
 __all__ = ['complement', 'reverse']
@@ -35,11 +34,11 @@ def complement(G, name=None):
     Graph, node, and edge data are not propagated to the new graph.
     """
     if name is None:
-        name="complement(%s)"%(G.name)
-    R=G.__class__()
-    R.name=name
+        name = "complement(%s)"%(G.name)
+    R = G.__class__()
+    R.name = name
     R.add_nodes_from(G)
-    R.add_edges_from( ((n,n2)
+    R.add_edges_from( ((n, n2)
                        for n,nbrs in G.adjacency_iter()
                        for n2 in G if n2 not in nbrs
                        if n != n2) )


Test patch:
diff --git a/networkx/algorithms/operators/tests/test_all.py b/networkx/algorithms/operators/tests/test_all.py
--- a/networkx/algorithms/operators/tests/test_all.py
+++ b/networkx/algorithms/operators/tests/test_all.py
@@ -165,3 +165,32 @@ def test_input_output():
     l = [nx.Graph([(1,2)]),nx.Graph([(1,2)])]
     R = nx.intersection_all(l)
     assert_equal(len(l),2)
+
+
+@raises(nx.NetworkXError)
+def test_mixed_type_union():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    I = nx.Graph()
+    U = nx.union_all([G,H,I])
+
+@raises(nx.NetworkXError)
+def test_mixed_type_disjoint_union():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    I = nx.Graph()
+    U = nx.disjoint_union_all([G,H,I])
+
+@raises(nx.NetworkXError)
+def test_mixed_type_intersection():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    I = nx.Graph()
+    U = nx.intersection_all([G,H,I])
+
+@raises(nx.NetworkXError)
+def test_mixed_type_compose():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    I = nx.Graph()
+    U = nx.compose_all([G,H,I])
diff --git a/networkx/algorithms/operators/tests/test_binary.py b/networkx/algorithms/operators/tests/test_binary.py
--- a/networkx/algorithms/operators/tests/test_binary.py
+++ b/networkx/algorithms/operators/tests/test_binary.py
@@ -268,3 +268,41 @@ def test_compose_multigraph():
     assert_equal( set(GH) , set(G)|set(H))
     assert_equal( set(GH.edges(keys=True)) , 
                   set(G.edges(keys=True))|set(H.edges(keys=True)))    
+
+
+@raises(nx.NetworkXError)
+def test_mixed_type_union():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    U = nx.union(G,H)
+
+@raises(nx.NetworkXError)
+def test_mixed_type_disjoint_union():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    U = nx.disjoint_union(G,H)
+
+@raises(nx.NetworkXError)
+def test_mixed_type_intersection():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    U = nx.intersection(G,H)
+
+@raises(nx.NetworkXError)
+def test_mixed_type_difference():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    U = nx.difference(G,H)
+
+
+@raises(nx.NetworkXError)
+def test_mixed_type_symmetric_difference():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    U = nx.symmetric_difference(G,H)
+
+@raises(nx.NetworkXError)
+def test_mixed_type_compose():
+    G = nx.Graph()
+    H = nx.MultiGraph()
+    U = nx.compose(G,H)