vf2userfunc.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. """
  2. Module to simplify the specification of user-defined equality functions for
  3. node and edge attributes during isomorphism checks.
  4. During the construction of an isomorphism, the algorithm considers two
  5. candidate nodes n1 in G1 and n2 in G2. The graphs G1 and G2 are then
  6. compared with respect to properties involving n1 and n2, and if the outcome
  7. is good, then the candidate nodes are considered isomorphic. NetworkX
  8. provides a simple mechanism for users to extend the comparisons to include
  9. node and edge attributes.
  10. Node attributes are handled by the node_match keyword. When considering
  11. n1 and n2, the algorithm passes their node attribute dictionaries to
  12. node_match, and if it returns False, then n1 and n2 cannot be
  13. considered to be isomorphic.
  14. Edge attributes are handled by the edge_match keyword. When considering
  15. n1 and n2, the algorithm must verify that outgoing edges from n1 are
  16. commensurate with the outgoing edges for n2. If the graph is directed,
  17. then a similar check is also performed for incoming edges.
  18. Focusing only on outgoing edges, we consider pairs of nodes (n1, v1) from
  19. G1 and (n2, v2) from G2. For graphs and digraphs, there is only one edge
  20. between (n1, v1) and only one edge between (n2, v2). Those edge attribute
  21. dictionaries are passed to edge_match, and if it returns False, then
  22. n1 and n2 cannot be considered isomorphic. For multigraphs and
  23. multidigraphs, there can be multiple edges between (n1, v1) and also
  24. multiple edges between (n2, v2). Now, there must exist an isomorphism
  25. from "all the edges between (n1, v1)" to "all the edges between (n2, v2)".
  26. So, all of the edge attribute dictionaries are passed to edge_match, and
  27. it must determine if there is an isomorphism between the two sets of edges.
  28. """
  29. from . import isomorphvf2 as vf2
  30. __all__ = ["GraphMatcher", "DiGraphMatcher", "MultiGraphMatcher", "MultiDiGraphMatcher"]
  31. def _semantic_feasibility(self, G1_node, G2_node):
  32. """Returns True if mapping G1_node to G2_node is semantically feasible."""
  33. # Make sure the nodes match
  34. if self.node_match is not None:
  35. nm = self.node_match(self.G1.nodes[G1_node], self.G2.nodes[G2_node])
  36. if not nm:
  37. return False
  38. # Make sure the edges match
  39. if self.edge_match is not None:
  40. # Cached lookups
  41. G1nbrs = self.G1_adj[G1_node]
  42. G2nbrs = self.G2_adj[G2_node]
  43. core_1 = self.core_1
  44. edge_match = self.edge_match
  45. for neighbor in G1nbrs:
  46. # G1_node is not in core_1, so we must handle R_self separately
  47. if neighbor == G1_node:
  48. if G2_node in G2nbrs and not edge_match(
  49. G1nbrs[G1_node], G2nbrs[G2_node]
  50. ):
  51. return False
  52. elif neighbor in core_1:
  53. G2_nbr = core_1[neighbor]
  54. if G2_nbr in G2nbrs and not edge_match(
  55. G1nbrs[neighbor], G2nbrs[G2_nbr]
  56. ):
  57. return False
  58. # syntactic check has already verified that neighbors are symmetric
  59. return True
  60. class GraphMatcher(vf2.GraphMatcher):
  61. """VF2 isomorphism checker for undirected graphs."""
  62. def __init__(self, G1, G2, node_match=None, edge_match=None):
  63. """Initialize graph matcher.
  64. Parameters
  65. ----------
  66. G1, G2: graph
  67. The graphs to be tested.
  68. node_match: callable
  69. A function that returns True iff node n1 in G1 and n2 in G2
  70. should be considered equal during the isomorphism test. The
  71. function will be called like::
  72. node_match(G1.nodes[n1], G2.nodes[n2])
  73. That is, the function will receive the node attribute dictionaries
  74. of the nodes under consideration. If None, then no attributes are
  75. considered when testing for an isomorphism.
  76. edge_match: callable
  77. A function that returns True iff the edge attribute dictionary for
  78. the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should be
  79. considered equal during the isomorphism test. The function will be
  80. called like::
  81. edge_match(G1[u1][v1], G2[u2][v2])
  82. That is, the function will receive the edge attribute dictionaries
  83. of the edges under consideration. If None, then no attributes are
  84. considered when testing for an isomorphism.
  85. """
  86. vf2.GraphMatcher.__init__(self, G1, G2)
  87. self.node_match = node_match
  88. self.edge_match = edge_match
  89. # These will be modified during checks to minimize code repeat.
  90. self.G1_adj = self.G1.adj
  91. self.G2_adj = self.G2.adj
  92. semantic_feasibility = _semantic_feasibility
  93. class DiGraphMatcher(vf2.DiGraphMatcher):
  94. """VF2 isomorphism checker for directed graphs."""
  95. def __init__(self, G1, G2, node_match=None, edge_match=None):
  96. """Initialize graph matcher.
  97. Parameters
  98. ----------
  99. G1, G2 : graph
  100. The graphs to be tested.
  101. node_match : callable
  102. A function that returns True iff node n1 in G1 and n2 in G2
  103. should be considered equal during the isomorphism test. The
  104. function will be called like::
  105. node_match(G1.nodes[n1], G2.nodes[n2])
  106. That is, the function will receive the node attribute dictionaries
  107. of the nodes under consideration. If None, then no attributes are
  108. considered when testing for an isomorphism.
  109. edge_match : callable
  110. A function that returns True iff the edge attribute dictionary for
  111. the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should be
  112. considered equal during the isomorphism test. The function will be
  113. called like::
  114. edge_match(G1[u1][v1], G2[u2][v2])
  115. That is, the function will receive the edge attribute dictionaries
  116. of the edges under consideration. If None, then no attributes are
  117. considered when testing for an isomorphism.
  118. """
  119. vf2.DiGraphMatcher.__init__(self, G1, G2)
  120. self.node_match = node_match
  121. self.edge_match = edge_match
  122. # These will be modified during checks to minimize code repeat.
  123. self.G1_adj = self.G1.adj
  124. self.G2_adj = self.G2.adj
  125. def semantic_feasibility(self, G1_node, G2_node):
  126. """Returns True if mapping G1_node to G2_node is semantically feasible."""
  127. # Test node_match and also test edge_match on successors
  128. feasible = _semantic_feasibility(self, G1_node, G2_node)
  129. if not feasible:
  130. return False
  131. # Test edge_match on predecessors
  132. self.G1_adj = self.G1.pred
  133. self.G2_adj = self.G2.pred
  134. feasible = _semantic_feasibility(self, G1_node, G2_node)
  135. self.G1_adj = self.G1.adj
  136. self.G2_adj = self.G2.adj
  137. return feasible
  138. # The "semantics" of edge_match are different for multi(di)graphs, but
  139. # the implementation is the same. So, technically we do not need to
  140. # provide "multi" versions, but we do so to match NetworkX's base classes.
  141. class MultiGraphMatcher(GraphMatcher):
  142. """VF2 isomorphism checker for undirected multigraphs."""
  143. class MultiDiGraphMatcher(DiGraphMatcher):
  144. """VF2 isomorphism checker for directed multigraphs."""