work on my matcher

This commit is contained in:
Joeri Exelmans 2024-09-04 17:05:24 +02:00
parent 4b1f61806c
commit a0ccb3f35d

View file

@ -1,3 +1,6 @@
# This module contains a VF2-inspired graph matching algorithm
# Author: Joeri Exelmans
import itertools import itertools
class Graph: class Graph:
@ -39,6 +42,13 @@ class MatcherState:
# will always try to grow mapping via outgoing/incoming edges of this pair before attempting other non-connected vertices # will always try to grow mapping via outgoing/incoming edges of this pair before attempting other non-connected vertices
self.boundary = None self.boundary = None
@staticmethod
def make_initial(host, guest):
state = MatcherState()
state.h_unmatched_vtxs = host.vtxs
state.g_unmatched_vtxs = guest.vtxs
return state
# Grow the match set (creating a new copy) # Grow the match set (creating a new copy)
def grow_edge(self, host_edge, guest_edge): def grow_edge(self, host_edge, guest_edge):
new_state = MatcherState() new_state = MatcherState()
@ -87,32 +97,31 @@ class MatcherVF2:
self.guest = guest self.guest = guest
self.compare_fn = compare_fn self.compare_fn = compare_fn
def match(self, state = None, already_visited = set(), indent=0): def match(self):
print(" "*indent, "match") yield from self._match(
if state == None: state=MatcherState.make_initial(self.host, self.guest),
state = MatcherState() already_visited=set())
state.h_unmatched_vtxs = self.host.vtxs
state.g_unmatched_vtxs = self.guest.vtxs def _match(self, state, already_visited, indent=0):
# print(" "*indent, "match")
def visit_for_first_time(state): def visit_for_first_time(state):
hashable_state = state.make_hashable() hashable_state = state.make_hashable()
if hashable_state in already_visited: if hashable_state in already_visited:
print(" "*indent, 'S K I P - A L R E A D Y V I S I T E D S T A T E')
return False return False
already_visited.add(hashable_state) already_visited.add(hashable_state)
# print('visisted', len(already_visited), 'states')
return True return True
if len(state.mapping_edges) == len(self.guest.edges): if len(state.mapping_edges) == len(self.guest.edges):
print(" "*indent, "GOT MATCH:") # print(" "*indent, "GOT MATCH:")
print(" "*indent, " ", state.mapping_vtxs) # print(" "*indent, " ", state.mapping_vtxs)
print(" "*indent, " ", state.mapping_edges) # print(" "*indent, " ", state.mapping_edges)
yield state yield state
return return
def attempt_grow(direction, indent): def attempt_grow(direction, indent):
print(" "*indent, 'attempt_grow', direction) # print(" "*indent, 'attempt_grow', direction)
if state.boundary != None: if state.boundary != None:
g_vtx, h_vtx = state.boundary g_vtx, h_vtx = state.boundary
for g_candidate_edge in getattr(g_vtx, direction): for g_candidate_edge in getattr(g_vtx, direction):
@ -126,7 +135,7 @@ class MatcherVF2:
h_candidate_vtx = h_candidate_edge.tgt h_candidate_vtx = h_candidate_edge.tgt
new_state = state.grow_edge(h_candidate_edge, g_candidate_edge) new_state = state.grow_edge(h_candidate_edge, g_candidate_edge)
if visit_for_first_time(new_state): if visit_for_first_time(new_state):
print(" "*indent, 'grow edge', g_candidate_edge, ':', h_candidate_edge) # print(" "*indent, 'grow edge', g_candidate_edge, ':', h_candidate_edge)
yield from attempt_match_vtxs( yield from attempt_match_vtxs(
new_state, new_state,
g_candidate_vtx, g_candidate_vtx,
@ -134,12 +143,11 @@ class MatcherVF2:
indent+1) indent+1)
def attempt_match_vtxs(state, g_candidate_vtx, h_candidate_vtx, indent): def attempt_match_vtxs(state, g_candidate_vtx, h_candidate_vtx, indent):
# It seems faster (benchmarked) to generate the new_state, even if we still have to check if it's a valid (partial) match, so we can put it in already_visited
new_state = state.grow_vtx( new_state = state.grow_vtx(
h_candidate_vtx, h_candidate_vtx,
g_candidate_vtx) g_candidate_vtx)
if visit_for_first_time(new_state): if visit_for_first_time(new_state):
print(" "*indent, 'attempt_match_vtxs') # print(" "*indent, 'attempt_match_vtxs')
if h_candidate_vtx in state.r_mapping_vtxs: if h_candidate_vtx in state.r_mapping_vtxs:
if state.r_mapping_vtxs[h_candidate_vtx] != g_candidate_vtx: if state.r_mapping_vtxs[h_candidate_vtx] != g_candidate_vtx:
return # host vtx is already mapped but doesn't match guest vtx return # host vtx is already mapped but doesn't match guest vtx
@ -153,48 +161,52 @@ class MatcherVF2:
return return
if not self.compare_fn(h_candidate_vtx.value, g_candidate_vtx.value): if not self.compare_fn(h_candidate_vtx.value, g_candidate_vtx.value):
return return
print(" "*indent, 'grow vtx', g_candidate_vtx, ':', h_candidate_vtx) # print(" "*indent, 'grow vtx', g_candidate_vtx, ':', h_candidate_vtx)
yield from self.match(new_state, already_visited, indent+1) yield from self._match(new_state, already_visited, indent+1)
print(" "*indent, 'preferred...') # print(" "*indent, 'preferred...')
yield from attempt_grow('outgoing', indent+1) yield from attempt_grow('outgoing', indent+1)
yield from attempt_grow('incoming', indent+1) yield from attempt_grow('incoming', indent+1)
print(" "*indent, 'least preferred...') # print(" "*indent, 'least preferred...')
for g_candidate_vtx in state.g_unmatched_vtxs: for g_candidate_vtx in state.g_unmatched_vtxs:
for h_candidate_vtx in state.h_unmatched_vtxs: for h_candidate_vtx in state.h_unmatched_vtxs:
yield from attempt_match_vtxs(state, g_candidate_vtx, h_candidate_vtx, indent+1) yield from attempt_match_vtxs(state, g_candidate_vtx, h_candidate_vtx, indent+1)
if indent == 0: # if indent == 0:
print('visited', len(already_visited), 'states total') # print('visited', len(already_visited), 'states total')
host = Graph()
host.vtxs = [Vertex(0), Vertex(1), Vertex(2)] # demo time...
host.edges = [ if __name__ == "__main__":
host = Graph()
host.vtxs = [Vertex(0), Vertex(1), Vertex(2)]
host.edges = [
Edge(host.vtxs[0], host.vtxs[1]), Edge(host.vtxs[0], host.vtxs[1]),
Edge(host.vtxs[1], host.vtxs[2]), Edge(host.vtxs[1], host.vtxs[2]),
Edge(host.vtxs[2], host.vtxs[0]), Edge(host.vtxs[2], host.vtxs[0]),
] ]
guest = Graph() guest = Graph()
guest.vtxs = [Vertex('src'), Vertex('tgt')] guest.vtxs = [Vertex('src'), Vertex('tgt')]
guest.edges = [ guest.edges = [
Edge(guest.vtxs[0], guest.vtxs[1]), Edge(guest.vtxs[0], guest.vtxs[1]),
] ]
m = MatcherVF2(host, guest, lambda hv, gv: True) m = MatcherVF2(host, guest, lambda hv, gv: True)
import time import time
durations = 0 durations = 0
for n in range(100000): iterations = 2000
for n in range(iterations):
time_start = time.perf_counter_ns() time_start = time.perf_counter_ns()
matches = [mm for mm in m.match()] matches = [mm for mm in m.match()]
print("found", len(matches), "matches") # print("found", len(matches), "matches")
time_end = time.perf_counter_ns() time_end = time.perf_counter_ns()
time_duration = time_end - time_start time_duration = time_end - time_start
durations += time_duration durations += time_duration
print(f'Took {durations/1000000} ms') print(f'{iterations} iterations, took {durations/1000000:.3f} ms, {durations/iterations/1000000:.3f} ms per iteration')
for mm in matches: for mm in matches:
print("match:") print("match:")
print(" ", mm.mapping_vtxs) print(" ", mm.mapping_vtxs)
print(" ", mm.mapping_edges) print(" ", mm.mapping_edges)