matcher.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. # This module contains a VF2-inspired graph matching algorithm
  2. # Author: Joeri Exelmans
  3. import itertools
  4. class Graph:
  5. def __init__(self):
  6. self.vtxs = []
  7. self.edges = []
  8. class Vertex:
  9. def __init__(self, value):
  10. self.incoming = []
  11. self.outgoing = []
  12. self.value = value
  13. def __repr__(self):
  14. return f"V({self.value})"
  15. class Edge:
  16. def __init__(self, src: Vertex, tgt: Vertex):
  17. self.src = src
  18. self.tgt = tgt
  19. self.src.outgoing.append(self)
  20. self.tgt.incoming.append(self)
  21. def __repr__(self):
  22. return f"E({self.src}->{self.tgt})"
  23. class MatcherState:
  24. def __init__(self):
  25. self.mapping_vtxs = {} # guest -> host
  26. self.mapping_edges = {} # guest -> host
  27. self.r_mapping_vtxs = {} # host -> guest
  28. self.r_mapping_edges = {} # host -> guest
  29. self.h_unmatched_vtxs = []
  30. self.g_unmatched_vtxs = []
  31. # the most recently added pair of (guest,host) vertices
  32. # will always try to grow mapping via outgoing/incoming edges of this pair before attempting other non-connected vertices
  33. self.boundary = None
  34. @staticmethod
  35. def make_initial(host, guest):
  36. state = MatcherState()
  37. state.h_unmatched_vtxs = host.vtxs
  38. state.g_unmatched_vtxs = guest.vtxs
  39. return state
  40. # Grow the match set (creating a new copy)
  41. def grow_edge(self, host_edge, guest_edge):
  42. new_state = MatcherState()
  43. new_state.mapping_vtxs = self.mapping_vtxs
  44. new_state.mapping_edges = dict(self.mapping_edges)
  45. new_state.mapping_edges[guest_edge] = host_edge
  46. new_state.r_mapping_vtxs = self.r_mapping_vtxs
  47. new_state.r_mapping_edges = dict(self.r_mapping_edges)
  48. new_state.r_mapping_edges[host_edge] = guest_edge
  49. new_state.h_unmatched_vtxs = self.h_unmatched_vtxs
  50. new_state.g_unmatched_vtxs = self.g_unmatched_vtxs
  51. return new_state
  52. # Grow the match set (creating a new copy)
  53. def grow_vtx(self, host_vtx, guest_vtx):
  54. new_state = MatcherState()
  55. new_state.mapping_vtxs = dict(self.mapping_vtxs)
  56. new_state.mapping_vtxs[guest_vtx] = host_vtx
  57. new_state.mapping_edges = self.mapping_edges
  58. new_state.r_mapping_vtxs = dict(self.r_mapping_vtxs)
  59. new_state.r_mapping_vtxs[host_vtx] = guest_vtx
  60. new_state.r_mapping_edges = self.r_mapping_edges
  61. new_state.h_unmatched_vtxs = [h_vtx for h_vtx in self.h_unmatched_vtxs if h_vtx != host_vtx]
  62. new_state.g_unmatched_vtxs = [g_vtx for g_vtx in self.g_unmatched_vtxs if g_vtx != guest_vtx]
  63. new_state.boundary = (guest_vtx, host_vtx)
  64. return new_state
  65. def make_hashable(self):
  66. return frozenset(itertools.chain(
  67. ((gv,hv) for gv,hv in self.mapping_vtxs.items()),
  68. ((ge,he) for ge,he in self.mapping_edges.items()),
  69. ))
  70. class MatcherVF2:
  71. # Guest is the pattern
  72. def __init__(self, host, guest, compare_fn):
  73. self.host = host
  74. self.guest = guest
  75. self.compare_fn = compare_fn
  76. def match(self):
  77. yield from self._match(
  78. state=MatcherState.make_initial(self.host, self.guest),
  79. already_visited=set())
  80. def _match(self, state, already_visited, indent=0):
  81. # print(" "*indent, "match")
  82. hashable_state = state.make_hashable()
  83. if hashable_state in already_visited:
  84. # print(" "*indent, " SKIP - ALREADY VISITED")
  85. # print(" "*indent, " ", hashable_state)
  86. return
  87. # print(" "*indent, " ADD STATE")
  88. # print(" "*indent, " ", hashable_state)
  89. already_visited.add(hashable_state)
  90. if len(state.mapping_edges) == len(self.guest.edges):
  91. # print(" "*indent, "GOT MATCH:")
  92. # print(" "*indent, " ", state.mapping_vtxs)
  93. # print(" "*indent, " ", state.mapping_edges)
  94. yield state
  95. return
  96. def read_edge(edge, direction):
  97. if direction == "outgoing":
  98. return edge.tgt
  99. elif direction == "incoming":
  100. return edge.src
  101. else:
  102. raise Exception("wtf!")
  103. def attempt_grow(direction, indent):
  104. # print(" "*indent, 'attempt_grow', direction)
  105. if state.boundary != None:
  106. g_vtx, h_vtx = state.boundary
  107. for g_candidate_edge in getattr(g_vtx, direction):
  108. # print(" "*indent, 'g_candidate_edge:', g_candidate_edge)
  109. if g_candidate_edge in state.mapping_edges:
  110. # print(" "*indent, " skip, guest edge already matched")
  111. continue # skip already matched guest edge
  112. g_candidate_vtx = read_edge(g_candidate_edge, direction)
  113. for h_candidate_edge in getattr(h_vtx, direction):
  114. # print(" "*indent, 'h_candidate_edge:', h_candidate_edge)
  115. if h_candidate_edge in state.r_mapping_edges:
  116. # print(" "*indent, " skip, host edge already matched")
  117. continue # skip already matched host edge
  118. h_candidate_vtx = read_edge(h_candidate_edge, direction)
  119. # print(" "*indent, 'grow edge', g_candidate_edge, ':', h_candidate_edge)
  120. new_state = state.grow_edge(h_candidate_edge, g_candidate_edge)
  121. yield from attempt_match_vtxs(
  122. new_state,
  123. g_candidate_vtx,
  124. h_candidate_vtx,
  125. indent+1)
  126. def attempt_match_vtxs(state, g_candidate_vtx, h_candidate_vtx, indent):
  127. # print(" "*indent, 'attempt_match_vtxs')
  128. if g_candidate_vtx in state.mapping_vtxs:
  129. if state.mapping_vtxs[g_candidate_vtx] != h_candidate_vtx:
  130. # print(" "*indent, " nope, guest already mapped (mismatch)")
  131. return # guest vtx is already mapped but doesn't match host vtx
  132. if h_candidate_vtx in state.r_mapping_vtxs:
  133. if state.r_mapping_vtxs[h_candidate_vtx] != g_candidate_vtx:
  134. # print(" "*indent, " nope, host already mapped (mismatch)")
  135. return # host vtx is already mapped but doesn't match guest vtx
  136. g_outdegree = len(g_candidate_vtx.outgoing)
  137. h_outdegree = len(h_candidate_vtx.outgoing)
  138. if g_outdegree > h_outdegree:
  139. return
  140. g_indegree = len(g_candidate_vtx.incoming)
  141. h_indegree = len(h_candidate_vtx.incoming)
  142. if g_indegree > h_indegree:
  143. return
  144. if not self.compare_fn(g_candidate_vtx.value, h_candidate_vtx.value):
  145. return
  146. new_state = state.grow_vtx(
  147. h_candidate_vtx,
  148. g_candidate_vtx)
  149. # print(" "*indent, 'grow vtx', g_candidate_vtx, ':', h_candidate_vtx)
  150. yield from self._match(new_state, already_visited, indent+1)
  151. # print(" "*indent, 'preferred...')
  152. yield from attempt_grow('outgoing', indent+1)
  153. yield from attempt_grow('incoming', indent+1)
  154. # print(" "*indent, 'least preferred...')
  155. for g_candidate_vtx in state.g_unmatched_vtxs:
  156. for h_candidate_vtx in state.h_unmatched_vtxs:
  157. yield from attempt_match_vtxs(state, g_candidate_vtx, h_candidate_vtx, indent+1)
  158. # if indent == 0:
  159. # print('visited', len(already_visited), 'states total')
  160. # demo time...
  161. if __name__ == "__main__":
  162. host = Graph()
  163. host.vtxs = [Vertex(0), Vertex(1), Vertex(2), Vertex(3)]
  164. host.edges = [
  165. Edge(host.vtxs[0], host.vtxs[1]),
  166. Edge(host.vtxs[1], host.vtxs[2]),
  167. Edge(host.vtxs[2], host.vtxs[0]),
  168. Edge(host.vtxs[2], host.vtxs[3]),
  169. Edge(host.vtxs[3], host.vtxs[2]),
  170. ]
  171. guest = Graph()
  172. guest.vtxs = [
  173. Vertex('v != 3'), # cannot be matched with Vertex(3) - changing this to True, you get 2 morphisms instead of one
  174. Vertex('True')] # can be matched with any node
  175. guest.edges = [
  176. # Look for a simple loop:
  177. Edge(guest.vtxs[0], guest.vtxs[1]),
  178. Edge(guest.vtxs[1], guest.vtxs[0]),
  179. ]
  180. m = MatcherVF2(host, guest, lambda g_val, h_val: eval(g_val, {}, {'v':h_val}))
  181. import time
  182. durations = 0
  183. iterations = 1
  184. print("Patience...")
  185. for n in range(iterations):
  186. time_start = time.perf_counter_ns()
  187. matches = [mm for mm in m.match()]
  188. time_end = time.perf_counter_ns()
  189. time_duration = time_end - time_start
  190. durations += time_duration
  191. print(f'{iterations} iterations, took {durations/1000000:.3f} ms, {durations/iterations/1000000:.3f} ms per iteration')
  192. print("found", len(matches), "matches")
  193. for mm in matches:
  194. print("match:")
  195. print(" ", mm.mapping_vtxs)
  196. print(" ", mm.mapping_edges)