Repository: networkx/networkx

Issue:
graph edit distance doesn't work for non-large special, but not circular example
I stumbled about it, the programe doesn't come to end.
That's some minimal example:

> 
> #!/usr/bin/env python
> import networkx as nx
> import time
> 
> g1 = nx.Graph()
> g1.add_edges_from([(5, 4), (5, 6), (5, 7), (21, 8), (2, 1), (2, 0), (5, 3)])
> g2 = nx.Graph()
> g2.add_edges_from([(15, 13), (23, 16), (23, 24), (100, 7), (18, 19), (100, 23), (19, 20), (22, 21)])
> 
> start_time = time.time()
> print "starting"
> dst = nx.graph_edit_distance(g1, g2)
> print('  GED={0} took {1:.2f} seconds'.format(dst, time.time() - start_time))


Discussion:
Hi @kleinias , 
probably you'll have more luck with `optimize_graph_edit_distance`.
Any resolution here?  Should we change the docs somewhere to suggest trying the optimize version of the function? Should we just close this? What do you suggest?
@dschult I believe the procedure should **ultimately work** for the particular example, quickly.

However, **current** algorithm/implementation is sub-optimal, for this example. Moreover, I'm pretty sure it will always be possible to find an example for which exact GED calculation is slow, as it's NP-hard. So, `optimize_graph_edit_distance` will always have its uses. The algorithm interface was designed to be both simple (`graph_edit_distance`) and flexible (`optimize_graph_edit_distance`/`optimize_edit_paths`), and if the simple interface doesn't yield good results, one has to dig a bit deeper.

At the same time, I encourage capable people to investigate alternative GED algorithms, in order to fix this particular example.
That's a great summary. Maybe I'll try to put a paragraph "docstring" for the module (at the top) that simply says what you jut said. That way people know a little more which functions to try. I'll take a stab at the difference between optimize_edit_paths and optimal_edit_paths too.  

Resolving patch:
diff --git a/networkx/algorithms/similarity.py b/networkx/algorithms/similarity.py
--- a/networkx/algorithms/similarity.py
+++ b/networkx/algorithms/similarity.py
@@ -1,4 +1,26 @@
 # -*- coding: utf-8 -*-
+#    Copyright (C) 2010 by
+#    Aric Hagberg <hagberg@lanl.gov>
+#    Dan Schult <dschult@colgate.edu>
+#    Pieter Swart <swart@lanl.gov>
+#    All rights reserved.
+#    BSD license.
+#
+# Author:  Andrey Paramonov <paramon@acdlabs.ru>
+""" Functions measuring similarity using graph edit distance.
+
+The graph edit distance is the number of edge/node changes needed
+to make two graphs isomorphic.
+
+The default algorithm/implementation is sub-optimal for some graphs.
+The problem of finding the exact Graph Edit Distance (GED) is NP-hard
+so it is often slow. If the simple interface `graph_edit_distance`
+takes too long for your graph, try `optimize_graph_edit_distance`
+and/or `optimize_edit_paths`.
+
+At the same time, I encourage capable people to investigate
+alternative GED algorithms, in order to improve the choices available.
+"""
 from __future__ import print_function
 import math
 import networkx as nx
@@ -20,8 +42,10 @@ def debug_print(*args, **kwargs):
 
 
 def graph_edit_distance(G1, G2, node_match=None, edge_match=None,
-                        node_subst_cost=None, node_del_cost=None, node_ins_cost=None,
-                        edge_subst_cost=None, edge_del_cost=None, edge_ins_cost=None,
+                        node_subst_cost=None, node_del_cost=None,
+                        node_ins_cost=None,
+                        edge_subst_cost=None, edge_del_cost=None,
+                        edge_ins_cost=None,
                         upper_bound=None):
     """Returns GED (graph edit distance) between graphs G1 and G2.
 
@@ -152,8 +176,10 @@ def graph_edit_distance(G1, G2, node_match=None, edge_match=None,
 
 
 def optimal_edit_paths(G1, G2, node_match=None, edge_match=None,
-                       node_subst_cost=None, node_del_cost=None, node_ins_cost=None,
-                       edge_subst_cost=None, edge_del_cost=None, edge_ins_cost=None,
+                       node_subst_cost=None, node_del_cost=None,
+                       node_ins_cost=None,
+                       edge_subst_cost=None, edge_del_cost=None,
+                       edge_ins_cost=None,
                        upper_bound=None):
     """Returns all minimum-cost edit paths transforming G1 to G2.
 
@@ -296,8 +322,10 @@ def optimal_edit_paths(G1, G2, node_match=None, edge_match=None,
 
 
 def optimize_graph_edit_distance(G1, G2, node_match=None, edge_match=None,
-                                 node_subst_cost=None, node_del_cost=None, node_ins_cost=None,
-                                 edge_subst_cost=None, edge_del_cost=None, edge_ins_cost=None,
+                                 node_subst_cost=None, node_del_cost=None,
+                                 node_ins_cost=None,
+                                 edge_subst_cost=None, edge_del_cost=None,
+                                 edge_ins_cost=None,
                                  upper_bound=None):
     """Returns consecutive approximations of GED (graph edit distance)
     between graphs G1 and G2.
@@ -428,8 +456,10 @@ def optimize_graph_edit_distance(G1, G2, node_match=None, edge_match=None,
 
 
 def optimize_edit_paths(G1, G2, node_match=None, edge_match=None,
-                        node_subst_cost=None, node_del_cost=None, node_ins_cost=None,
-                        edge_subst_cost=None, edge_del_cost=None, edge_ins_cost=None,
+                        node_subst_cost=None, node_del_cost=None,
+                        node_ins_cost=None,
+                        edge_subst_cost=None, edge_del_cost=None,
+                        edge_ins_cost=None,
                         upper_bound=None, strictly_decreasing=True):
     """GED (graph edit distance) calculation: advanced interface.
 
@@ -574,18 +604,19 @@ def make_CostMatrix(C, m, n):
         lsa_row_ind, lsa_col_ind = linear_sum_assignment(C)
 
         # Fixup dummy assignments:
-        # each substitution i<->j should have corresponding dummy assignment m+j<->n+i
+        # each substitution i<->j should have dummy assignment m+j<->n+i
         # NOTE: fast reduce of Cv relies on it
         #assert len(lsa_row_ind) == len(lsa_col_ind)
-        subst_ind = list(k for k, i, j in zip(range(len(lsa_row_ind)), lsa_row_ind, lsa_col_ind)
-                         if i < m and j < n)
-        dummy_ind = list(k for k, i, j in zip(range(len(lsa_row_ind)), lsa_row_ind, lsa_col_ind)
-                         if i >= m and j >= n)
+        indexes = zip(range(len(lsa_row_ind)), lsa_row_ind, lsa_col_ind)
+        subst_ind = list(k for k, i, j in indexes if i < m and j < n)
+        indexes = zip(range(len(lsa_row_ind)), lsa_row_ind, lsa_col_ind)
+        dummy_ind = list(k for k, i, j in indexes if i >= m and j >= n)
         #assert len(subst_ind) == len(dummy_ind)
         lsa_row_ind[dummy_ind] = lsa_col_ind[subst_ind] + m
         lsa_col_ind[dummy_ind] = lsa_row_ind[subst_ind] + n
 
-        return CostMatrix(C, lsa_row_ind, lsa_col_ind, C[lsa_row_ind, lsa_col_ind].sum())
+        return CostMatrix(C, lsa_row_ind, lsa_col_ind,
+                          C[lsa_row_ind, lsa_col_ind].sum())
 
     def extract_C(C, i, j, m, n):
         #assert(C.shape == (m + n, m + n))
@@ -627,14 +658,12 @@ def match_edges(u, v, pending_g, pending_h, Ce, matched_uv=[]):
         N = len(pending_h)
         #assert Ce.C.shape == (M + N, M + N)
 
-        g_ind = list(i for i in range(M)
-                     if any(pending_g[i][:2] in ((p, u), (u, p))
-                            for p, q in matched_uv)
-                     or pending_g[i][:2] == (u, u))
-        h_ind = list(j for j in range(N)
-                     if any(pending_h[j][:2] in ((q, v), (v, q))
-                            for p, q in matched_uv)
-                     or pending_h[j][:2] == (v, v))
+        g_ind = [i for i in range(M) if pending_g[i][:2] == (u, u) or
+                 any(pending_g[i][:2] in ((p, u), (u, p))
+                     for p, q in matched_uv)]
+        h_ind = [j for j in range(N) if pending_h[j][:2] == (v, v) or
+                 any(pending_h[j][:2] in ((q, v), (v, q))
+                     for p, q in matched_uv)]
         m = len(g_ind)
         n = len(h_ind)
 
@@ -649,7 +678,8 @@ def match_edges(u, v, pending_g, pending_h, Ce, matched_uv=[]):
                 for l, j in zip(range(n), h_ind):
                     h = pending_h[j][:2]
                     if nx.is_directed(G1) or nx.is_directed(G2):
-                        if any(g == (p, u) and h == (q, v) or g == (u, p) and h == (v, q)
+                        if any(g == (p, u) and h == (q, v) or
+                               g == (u, p) and h == (v, q)
                                for p, q in matched_uv):
                             continue
                     else:
@@ -714,7 +744,8 @@ def get_edit_ops(matched_uv, pending_u, pending_v, Cv,
         # 1) a vertex mapping from optimal linear sum assignment
         i, j = min((k, l) for k, l in zip(Cv.lsa_row_ind, Cv.lsa_col_ind)
                    if k < m or l < n)
-        xy, localCe = match_edges(pending_u[i] if i < m else None, pending_v[j] if j < n else None,
+        xy, localCe = match_edges(pending_u[i] if i < m else None,
+                                  pending_v[j] if j < n else None,
                                   pending_g, pending_h, Ce, matched_uv)
         Ce_xy = reduce_Ce(Ce, xy, len(pending_g), len(pending_h))
         #assert Ce.ls <= localCe.ls + Ce_xy.ls
@@ -746,13 +777,15 @@ def get_edit_ops(matched_uv, pending_u, pending_v, Cv,
             #assert Cv.ls <= Cv.C[i, j] + Cv_ij.ls
             if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + Ce.ls):
                 continue
-            xy, localCe = match_edges(pending_u[i] if i < m else None, pending_v[j] if j < n else None,
+            xy, localCe = match_edges(pending_u[i] if i < m else None,
+                                      pending_v[j] if j < n else None,
                                       pending_g, pending_h, Ce, matched_uv)
             if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + localCe.ls):
                 continue
             Ce_xy = reduce_Ce(Ce, xy, len(pending_g), len(pending_h))
             #assert Ce.ls <= localCe.ls + Ce_xy.ls
-            if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + localCe.ls + Ce_xy.ls):
+            if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + localCe.ls +
+                     Ce_xy.ls):
                 continue
             other.append(((i, j), Cv_ij, xy, Ce_xy, Cv.C[i, j] + localCe.ls))
 
@@ -827,8 +860,10 @@ def get_edit_paths(matched_uv, pending_u, pending_v, Cv,
                 v = pending_v.pop(j) if j < len(pending_v) else None
                 matched_uv.append((u, v))
                 for x, y in xy:
-                    matched_gh.append((pending_g[x] if x < len(pending_g) else None,
-                                       pending_h[y] if y < len(pending_h) else None))
+                    len_g = len(pending_g)
+                    len_h = len(pending_h)
+                    matched_gh.append((pending_g[x] if x < len_g else None,
+                                       pending_h[y] if y < len_h else None))
                 sortedx = list(sorted(x for x, y in xy))
                 sortedy = list(sorted(y for x, y in xy))
                 G = list((pending_g.pop(x) if x < len(pending_g) else None)
@@ -837,15 +872,17 @@ def get_edit_paths(matched_uv, pending_u, pending_v, Cv,
                          for y in reversed(sortedy))
 
                 # yield from
-                for t in get_edit_paths(matched_uv, pending_u, pending_v, Cv_ij,
-                                        matched_gh, pending_g, pending_h, Ce_xy,
+                for t in get_edit_paths(matched_uv, pending_u, pending_v,
+                                        Cv_ij,
+                                        matched_gh, pending_g, pending_h,
+                                        Ce_xy,
                                         matched_cost + edit_cost):
                     yield t
 
                 # backtrack
-                if not u is None:
+                if u is not None:
                     pending_u.insert(i, u)
-                if not v is None:
+                if v is not None:
                     pending_v.insert(j, v)
                 matched_uv.pop()
                 for x, g in zip(sortedx, reversed(G)):
@@ -868,10 +905,12 @@ def get_edit_paths(matched_uv, pending_u, pending_v, Cv,
     C = np.zeros((m + n, m + n))
     if node_subst_cost:
         C[0:m, 0:n] = np.array([node_subst_cost(G1.nodes[u], G2.nodes[v])
-                                for u in pending_u for v in pending_v]).reshape(m, n)
+                                for u in pending_u for v in pending_v]
+                               ).reshape(m, n)
     elif node_match:
         C[0:m, 0:n] = np.array([1 - int(node_match(G1.nodes[u], G2.nodes[v]))
-                                for u in pending_u for v in pending_v]).reshape(m, n)
+                                for u in pending_u for v in pending_v]
+                               ).reshape(m, n)
     else:
         # all zeroes
         pass
@@ -888,9 +927,11 @@ def get_edit_paths(matched_uv, pending_u, pending_v, Cv,
     #assert not n or min(ins_costs) >= 0
     inf = C[0:m, 0:n].sum() + sum(del_costs) + sum(ins_costs) + 1
     C[0:m, n:n + m] = np.array([del_costs[i] if i == j else inf
-                                for i in range(m) for j in range(m)]).reshape(m, m)
+                                for i in range(m) for j in range(m)]
+                               ).reshape(m, m)
     C[m:m + n, 0:n] = np.array([ins_costs[i] if i == j else inf
-                                for i in range(n) for j in range(n)]).reshape(n, n)
+                                for i in range(n) for j in range(n)]
+                               ).reshape(n, n)
     Cv = make_CostMatrix(C, m, n)
     #debug_print('Cv: {} x {}'.format(m, n))
     #debug_print(Cv.C)
@@ -904,10 +945,12 @@ def get_edit_paths(matched_uv, pending_u, pending_v, Cv,
     C = np.zeros((m + n, m + n))
     if edge_subst_cost:
         C[0:m, 0:n] = np.array([edge_subst_cost(G1.edges[g], G2.edges[h])
-                                for g in pending_g for h in pending_h]).reshape(m, n)
+                                for g in pending_g for h in pending_h]
+                               ).reshape(m, n)
     elif edge_match:
         C[0:m, 0:n] = np.array([1 - int(edge_match(G1.edges[g], G2.edges[h]))
-                                for g in pending_g for h in pending_h]).reshape(m, n)
+                                for g in pending_g for h in pending_h]
+                               ).reshape(m, n)
     else:
         # all zeroes
         pass
@@ -924,9 +967,11 @@ def get_edit_paths(matched_uv, pending_u, pending_v, Cv,
     #assert not n or min(ins_costs) >= 0
     inf = C[0:m, 0:n].sum() + sum(del_costs) + sum(ins_costs) + 1
     C[0:m, n:n + m] = np.array([del_costs[i] if i == j else inf
-                                for i in range(m) for j in range(m)]).reshape(m, m)
+                                for i in range(m) for j in range(m)]
+                               ).reshape(m, m)
     C[m:m + n, 0:n] = np.array([ins_costs[i] if i == j else inf
-                                for i in range(n) for j in range(n)]).reshape(n, n)
+                                for i in range(n) for j in range(n)]
+                               ).reshape(n, n)
     Ce = make_CostMatrix(C, m, n)
     #debug_print('Ce: {} x {}'.format(m, n))
     #debug_print(Ce.C)
@@ -953,10 +998,10 @@ def prune(cost):
     for vertex_path, edge_path, cost in \
         get_edit_paths([], pending_u, pending_v, Cv,
                        [], pending_g, pending_h, Ce, 0):
-        #assert list(sorted(G1.nodes)) == list(sorted(list(u for u, v in vertex_path if u is not None)))
-        #assert list(sorted(G2.nodes)) == list(sorted(list(v for u, v in vertex_path if v is not None)))
-        #assert list(sorted(G1.edges)) == list(sorted(list(g for g, h in edge_path if g is not None)))
-        #assert list(sorted(G2.edges)) == list(sorted(list(h for g, h in edge_path if h is not None)))
+        #assert sorted(G1.nodes) == sorted(u for u, v in vertex_path if u is not None)
+        #assert sorted(G2.nodes) == sorted(v for u, v in vertex_path if v is not None)
+        #assert sorted(G1.edges) == sorted(g for g, h in edge_path if g is not None)
+        #assert sorted(G2.edges) == sorted(h for g, h in edge_path if h is not None)
         #print(vertex_path, edge_path, cost, file = sys.stderr)
         #assert cost == maxcost.value
         yield list(vertex_path), list(edge_path), cost


Test patch: