isomorph.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. """
  2. Graph isomorphism functions.
  3. """
  4. import networkx as nx
  5. from networkx.exception import NetworkXError
  6. __all__ = [
  7. "could_be_isomorphic",
  8. "fast_could_be_isomorphic",
  9. "faster_could_be_isomorphic",
  10. "is_isomorphic",
  11. ]
  12. def could_be_isomorphic(G1, G2):
  13. """Returns False if graphs are definitely not isomorphic.
  14. True does NOT guarantee isomorphism.
  15. Parameters
  16. ----------
  17. G1, G2 : graphs
  18. The two graphs G1 and G2 must be the same type.
  19. Notes
  20. -----
  21. Checks for matching degree, triangle, and number of cliques sequences.
  22. The triangle sequence contains the number of triangles each node is part of.
  23. The clique sequence contains for each node the number of maximal cliques
  24. involving that node.
  25. """
  26. # Check global properties
  27. if G1.order() != G2.order():
  28. return False
  29. # Check local properties
  30. d1 = G1.degree()
  31. t1 = nx.triangles(G1)
  32. clqs_1 = list(nx.find_cliques(G1))
  33. c1 = {n: sum(1 for c in clqs_1 if n in c) for n in G1} # number of cliques
  34. props1 = [[d, t1[v], c1[v]] for v, d in d1]
  35. props1.sort()
  36. d2 = G2.degree()
  37. t2 = nx.triangles(G2)
  38. clqs_2 = list(nx.find_cliques(G2))
  39. c2 = {n: sum(1 for c in clqs_2 if n in c) for n in G2} # number of cliques
  40. props2 = [[d, t2[v], c2[v]] for v, d in d2]
  41. props2.sort()
  42. if props1 != props2:
  43. return False
  44. # OK...
  45. return True
  46. graph_could_be_isomorphic = could_be_isomorphic
  47. def fast_could_be_isomorphic(G1, G2):
  48. """Returns False if graphs are definitely not isomorphic.
  49. True does NOT guarantee isomorphism.
  50. Parameters
  51. ----------
  52. G1, G2 : graphs
  53. The two graphs G1 and G2 must be the same type.
  54. Notes
  55. -----
  56. Checks for matching degree and triangle sequences. The triangle
  57. sequence contains the number of triangles each node is part of.
  58. """
  59. # Check global properties
  60. if G1.order() != G2.order():
  61. return False
  62. # Check local properties
  63. d1 = G1.degree()
  64. t1 = nx.triangles(G1)
  65. props1 = [[d, t1[v]] for v, d in d1]
  66. props1.sort()
  67. d2 = G2.degree()
  68. t2 = nx.triangles(G2)
  69. props2 = [[d, t2[v]] for v, d in d2]
  70. props2.sort()
  71. if props1 != props2:
  72. return False
  73. # OK...
  74. return True
  75. fast_graph_could_be_isomorphic = fast_could_be_isomorphic
  76. def faster_could_be_isomorphic(G1, G2):
  77. """Returns False if graphs are definitely not isomorphic.
  78. True does NOT guarantee isomorphism.
  79. Parameters
  80. ----------
  81. G1, G2 : graphs
  82. The two graphs G1 and G2 must be the same type.
  83. Notes
  84. -----
  85. Checks for matching degree sequences.
  86. """
  87. # Check global properties
  88. if G1.order() != G2.order():
  89. return False
  90. # Check local properties
  91. d1 = sorted(d for n, d in G1.degree())
  92. d2 = sorted(d for n, d in G2.degree())
  93. if d1 != d2:
  94. return False
  95. # OK...
  96. return True
  97. faster_graph_could_be_isomorphic = faster_could_be_isomorphic
  98. def is_isomorphic(G1, G2, node_match=None, edge_match=None):
  99. """Returns True if the graphs G1 and G2 are isomorphic and False otherwise.
  100. Parameters
  101. ----------
  102. G1, G2: graphs
  103. The two graphs G1 and G2 must be the same type.
  104. node_match : callable
  105. A function that returns True if node n1 in G1 and n2 in G2 should
  106. be considered equal during the isomorphism test.
  107. If node_match is not specified then node attributes are not considered.
  108. The function will be called like
  109. node_match(G1.nodes[n1], G2.nodes[n2]).
  110. That is, the function will receive the node attribute dictionaries
  111. for n1 and n2 as inputs.
  112. edge_match : callable
  113. A function that returns True if the edge attribute dictionary
  114. for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
  115. be considered equal during the isomorphism test. If edge_match is
  116. not specified then edge attributes are not considered.
  117. The function will be called like
  118. edge_match(G1[u1][v1], G2[u2][v2]).
  119. That is, the function will receive the edge attribute dictionaries
  120. of the edges under consideration.
  121. Notes
  122. -----
  123. Uses the vf2 algorithm [1]_.
  124. Examples
  125. --------
  126. >>> import networkx.algorithms.isomorphism as iso
  127. For digraphs G1 and G2, using 'weight' edge attribute (default: 1)
  128. >>> G1 = nx.DiGraph()
  129. >>> G2 = nx.DiGraph()
  130. >>> nx.add_path(G1, [1, 2, 3, 4], weight=1)
  131. >>> nx.add_path(G2, [10, 20, 30, 40], weight=2)
  132. >>> em = iso.numerical_edge_match("weight", 1)
  133. >>> nx.is_isomorphic(G1, G2) # no weights considered
  134. True
  135. >>> nx.is_isomorphic(G1, G2, edge_match=em) # match weights
  136. False
  137. For multidigraphs G1 and G2, using 'fill' node attribute (default: '')
  138. >>> G1 = nx.MultiDiGraph()
  139. >>> G2 = nx.MultiDiGraph()
  140. >>> G1.add_nodes_from([1, 2, 3], fill="red")
  141. >>> G2.add_nodes_from([10, 20, 30, 40], fill="red")
  142. >>> nx.add_path(G1, [1, 2, 3, 4], weight=3, linewidth=2.5)
  143. >>> nx.add_path(G2, [10, 20, 30, 40], weight=3)
  144. >>> nm = iso.categorical_node_match("fill", "red")
  145. >>> nx.is_isomorphic(G1, G2, node_match=nm)
  146. True
  147. For multidigraphs G1 and G2, using 'weight' edge attribute (default: 7)
  148. >>> G1.add_edge(1, 2, weight=7)
  149. 1
  150. >>> G2.add_edge(10, 20)
  151. 1
  152. >>> em = iso.numerical_multiedge_match("weight", 7, rtol=1e-6)
  153. >>> nx.is_isomorphic(G1, G2, edge_match=em)
  154. True
  155. For multigraphs G1 and G2, using 'weight' and 'linewidth' edge attributes
  156. with default values 7 and 2.5. Also using 'fill' node attribute with
  157. default value 'red'.
  158. >>> em = iso.numerical_multiedge_match(["weight", "linewidth"], [7, 2.5])
  159. >>> nm = iso.categorical_node_match("fill", "red")
  160. >>> nx.is_isomorphic(G1, G2, edge_match=em, node_match=nm)
  161. True
  162. See Also
  163. --------
  164. numerical_node_match, numerical_edge_match, numerical_multiedge_match
  165. categorical_node_match, categorical_edge_match, categorical_multiedge_match
  166. References
  167. ----------
  168. .. [1] L. P. Cordella, P. Foggia, C. Sansone, M. Vento,
  169. "An Improved Algorithm for Matching Large Graphs",
  170. 3rd IAPR-TC15 Workshop on Graph-based Representations in
  171. Pattern Recognition, Cuen, pp. 149-159, 2001.
  172. https://www.researchgate.net/publication/200034365_An_Improved_Algorithm_for_Matching_Large_Graphs
  173. """
  174. if G1.is_directed() and G2.is_directed():
  175. GM = nx.algorithms.isomorphism.DiGraphMatcher
  176. elif (not G1.is_directed()) and (not G2.is_directed()):
  177. GM = nx.algorithms.isomorphism.GraphMatcher
  178. else:
  179. raise NetworkXError("Graphs G1 and G2 are not of the same type.")
  180. gm = GM(G1, G2, node_match=node_match, edge_match=edge_match)
  181. return gm.is_isomorphic()