# coding: utf-8 """ Author: Sten Vercamman Univeristy of Antwerp Example code for paper: Efficient model transformations for novices url: http://msdl.cs.mcgill.ca/people/hv/teaching/MSBDesign/projects/Sten.Vercammen The main goal of this code is to give an overview, and an understandable implementation, of known techniques for pattern matching and solving the sub-graph homomorphism problem. The presented techniques do not include performance adaptations/optimizations. It is not optimized to be efficient but rather for the ease of understanding the workings of the algorithms. The paper does list some possible extensions/optimizations. It is intended as a guideline, even for novices, and provides an in-depth look at the workings behind various techniques for efficient pattern matching. """ from planGraph import * import collections import itertools # import numpy as np class PatternMatching(object): """ Returns an occurrence of a given pattern from the given Graph """ def __init__(self, matching_type='SP', optimize=True): # store the type of matching we want to use self.type = matching_type self.bound_vertices = {} # saves the currently bound vertices self.bound_edges = {} # saves the currently bound edges self.result = None self.previous = [] self.optimize = optimize def match(self, pattern, graph): """ Call this function to find an occurrence of the pattern in the (host) graph. Setting the type of matching (naive, SP, Ullmann, VF2) is done by setting self.matching_type to its name. """ if not (isinstance(pattern, SearchGraph) or isinstance(pattern, Graph)): raise TypeError('pattern must be a SearchGraph or Graph') if not (isinstance(graph, SearchGraph) or isinstance(graph, Graph)): raise TypeError('graph must be a SearchGraph or Graph') self.pattern = pattern self.graph = graph if self.type == 'naive': result = self.matchNaive(vertices=graph.vertices, edges=graph.edges) elif self.type == 'SP': result = self.matchSP() elif self.type == 'Ullmann': result = self.matchUllmann() elif self.type == 'VF2': result = self.matchVF2() else: raise ValueError('Unknown type for matching') # cleanup self.pattern = None self.graph = None self.bound_vertices = {} self.bound_edges = {} self.result = None return result def matchNaive(self, pattern_vertices=None, vertices=None, edges=None): """ Try to find an occurrence of the pattern in the Graph naively. """ # allow call with specific arguments if pattern_vertices == None: pattern_vertices = self.pattern.vertices if vertices == None: vertices = self.bound_vertices if edges == None: edges = self.bound_edges def visitEdge(pattern_vertices, p_edge, inc, g_edges, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): """ Visit a pattern edge, and try to bind it to a graph edge. (If the first fails, try the second, and so on...) """ for g_edge in g_edges: # only reckon the edge if its in edges and not visited # (as the graph might be a subgraph of a more complex graph) if g_edge not in edges.get(g_edge.type, []) or g_edge in visited_g_edges: continue if g_edge.type == p_edge.type and g_edge not in visited_g_edges: visited_p_edges[p_edge] = g_edge visited_g_edges.add(g_edge) if inc: p_vertex = p_edge.src else: p_vertex = p_edge.tgt if visitVertices(pattern_vertices, p_vertex, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): return True # remove added edges if they lead to no match, retry with others del visited_p_edges[p_edge] visited_g_edges.remove(g_edge) # no edge leads to a possitive match return False def visitEdges(pattern_vertices, p_edges, inc, g_edges, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): """ Visit all edges of the pattern vertex (edges given as argument). We need to try visiting them for all its permutations, as matching v -e1-> first and v -e2-> second and v -e3-> third, might not result in a matching an occurrence of the pattern, but matching v -e2-> first and v -e3-> second and v -e1-> third might. """ def removePrevEdge(visitedEdges, visited_p_edges, visited_g_edges): """ Undo the binding of the brevious edge, (the current bindinds do not lead to an occurrence of the pattern in the graph). """ for wrong_edge in visitedEdges: # remove binding (pattern edge to graph edge) wrong_g_edge = visited_p_edges.get(wrong_edge) del visited_p_edges[wrong_edge] # remove visited graph edge visited_g_edges.remove(wrong_g_edge) for it in itertools.permutations(p_edges): visitedEdges = [] foundallEdges = True for edge in it: if visited_p_edges.get(edge) == None: if not visitEdge(pattern_vertices, edge, inc, g_edges, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): # this did not work, so we have to undo all added edges # (the current edge is not added, as it failed) # we then can try a different permutation removePrevEdge(visitedEdges, visited_p_edges, visited_g_edges) foundallEdges = False break # try other order # add good visited (we know it succeeded) visitedEdges.append(edge) else: # we visited this pattern edge, and have the coressponding graph edge # if it is an incoming pattern edge, we need to make sure that # the graph target that is map from the pattern target # (of this incoming pattern edge, which has to be bound at this point) # has the graph adge as an incoming edge, # otherwise the graph is not properly connected if inc: if not visited_p_edges[edge] in visited_p_vertices[edge.tgt].incoming_edges: # did not work removePrevEdge(visitedEdges, visited_p_edges, visited_g_edges) foundallEdges = False break # try other order else: # analog for an outgoing edge if not visited_p_edges[edge] in visited_p_vertices[edge.src].outgoing_edges: # did not work removePrevEdge(visitedEdges, visited_p_edges, visited_g_edges) foundallEdges = False break # try other order # all edges are good, look no further if foundallEdges: break return foundallEdges def visitVertex(pattern_vertices, p_vertex, g_vertex, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): """ Visit a pattern vertex, and try to bind it to the graph vertex (both are given as argument). A binding is successful if all the pattern vertex his incoming and outgoing edges can be bound (to the graph vertex). """ if g_vertex in visited_g_vertices: return False # save visited graph vertex visited_g_vertices.add(g_vertex) # map pattern vertex to visited graph vertex visited_p_vertices[p_vertex] = g_vertex if visitEdges(pattern_vertices, p_vertex.incoming_edges, True, g_vertex.incoming_edges, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): if visitEdges(pattern_vertices, p_vertex.outgoing_edges, False, g_vertex.outgoing_edges, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): return True # cleanup, remove from visited as this does not lead to # an occurrence of the pttern in the graph visited_g_vertices.remove(g_vertex) del visited_p_vertices[p_vertex] return False def visitVertices(pattern_vertices, p_vertex, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): """ Visit a pattern vertex and try to bind a graph vertex to it. """ # if already matched or if it is a vertex not in the pattern_vertices # (second is for when you want to match the pattern partionally) if visited_p_vertices.get(p_vertex) != None or p_vertex not in pattern_vertices.get(p_vertex.type, set()): return True # try visiting graph vertices of same type as pattern vertex for g_vertex in vertices.get(p_vertex.type, []): if g_vertex not in visited_g_vertices: if visitVertex(pattern_vertices, p_vertex, g_vertex, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): return True return False visited_p_vertices = {} visited_p_edges = {} visited_g_vertices = set() visited_g_edges = set() # for loop is need for when pattern consists of multiple not connected structures allVertices = [] for _, p_vertices in pattern_vertices.items(): allVertices.extend(p_vertices) foundIt = False for it_p_vertices in itertools.permutations(allVertices): foundIt = True for p_vertex in it_p_vertices: if not visitVertices(pattern_vertices, p_vertex, visited_p_vertices, visited_p_edges, visited_g_vertices, visited_g_edges, vertices, edges): foundIt = False # reset visited visited_p_vertices = {} visited_p_edges = {} visited_g_vertices = set() visited_g_edges = set() break if foundIt: break if foundIt: return (visited_p_vertices, visited_p_edges) else: return None def matchSP(self): """ Find an occurrence of the pattern in the Graph by using the generated SearchPlan. """ if isinstance(self.graph, Graph): sg = SearchGraph(self.graph) elif isinstance(self.graph, SearchGraph): sg = self.graph else: raise TypeError('Pattern matching with a SearchPlan must be given a Graph or SearchGraph') pg = PlanGraph(self.pattern) SP = pg.Edmonds(sg) self.fileIndex = 0 def propConnected(): """ Checks if the found vertices and edges can be uniquely matched onto the pattern graph. """ self.result = self.matchNaive() return self.result != None def matchOP(elem, bound, ops, index): """ Execute a primitive operation, return whether ot not it succeeded. """ type_bound = bound.setdefault(elem.type, set()) # if elem not yet bound, bind it, and try matching the next operations if elem not in type_bound: type_bound.add(elem) # if matching of next operation failed, try with a different elem if matchAllOP(ops, index+1): return True else: type_bound.remove(elem) return False def matchAllOP(ops, index=0): """ Try to match an occurrence of the pattern in the graph, by recursivly ,atching elements that adhere to the SearchPlan """ # if we matched all elements, # check if the bound elements are properly connected if index == len(ops): return propConnected() op = ops[index] if op[0] == PRIM_OP.lkp: # lkp(elem) if op[2]: # lookup a vertex # If the graph does not have a vertex of the same vertex # type, we'll have to return False, happens if elems == []. elems = self.graph.vertices.get(op[1], []) bound = self.bound_vertices else: # loopup an edge # If the graph does not have an edge of the same edge # type, we'll have to return False, happens if elems == []. elems = self.graph.edges.get(op[1], []) bound = self.bound_edges # if elems == [], we'll skip the loop and return False for elem in elems: if matchOP(elem, bound, ops, index): return True # if all not bound elems fails, backtrack return False elif op[0] == PRIM_OP.src: # src(e): bind src of a bound edge e # Should always succeed, as the edge must be already bound # (there should be at least one elem in self.bound_edges[op[1]]). for edge in self.bound_edges[op[1]]: if matchOP(edge.src, self.bound_vertices, ops, index): return True # if all not bound elems fails, backtrack return False elif op[0] == PRIM_OP.tgt: # tgt(e): bind tgt of a bound edge e # Should always succeed, as the edge must be already bound # (there should be at least one elem in self.bound_edges[op[1]]). for edge in self.bound_edges[op[1]]: if matchOP(edge.tgt, self.bound_vertices, ops, index): return True # if all not bound elems fails, backtrack return False elif op[0] == PRIM_OP.inc: # in(v, e): bind incoming edge e of a bound vertex v # It's possible we will try to find a vertex of a certain type # in the bound_vertices which should be bound implicitly # (by a src/tgt op), that is not bound. Happens when implicit # binding bounded a "wrong" vertex. We then need to return False # (happens by skiping for loop by looping over []) for vertex in self.bound_vertices.get(op[1], []): for edge in vertex.incoming_edges: if edge.type == op[2]: if matchOP(edge, self.bound_edges, ops, index): return True # if all not bound elems fails, backtrack return False elif op[0] == PRIM_OP.out: # out(v, e): bind outgoing edge e of a bound vertex v # Return False if we expect an element to be bound that is not # bound (for the same reason as the inc op). for vertex in self.bound_vertices.get(op[1], []): for edge in vertex.outgoing_edges: if edge.type == op[2]: if matchOP(edge, self.bound_edges, ops, index): return True # if all not bound elems fails, backtrack return False else: raise TypeError('Unknown PRIM_OP type') # try and match all (primitive) operations from the SearchPlan matchAllOP(SP) # Either nothing is found, or we found an occurrence, # it is impossble to have a partionally matched occurrence for key, bound_elems in self.bound_vertices.items(): if len(bound_elems) == 0: # The pattern does not exist in the Graph return None else: # We found a pattern return self.result def createAdjacencyMatrixMap(self, graph): """ Return adjacency matrix and the order of the vertices. """ matrix = collections.OrderedDict() # { vertex, (index, [has edge from index to pos?]) } # contains all vertices we'll use for the AdjacencyMatrix allVertices = [] if self.optimize: # insert only the vertices from the graph which have a type # that is present in the pattern for vertex_type, _ in self.pattern.vertices.items(): graph_vertices = graph.vertices.get(vertex_type) if graph_vertices != None: allVertices.extend(graph_vertices) else: # we will not be able to find the pattern # as the pattern contains a vertex of a certain type # that is not present in the host graph return False else: # insert all vertices from the graph for _, vertices in graph.vertices.items(): allVertices.extend(vertices) # create squared zero matrix index = 0 for vertex in allVertices: matrix[vertex] = (index, [False] * len(allVertices)) index += 1 for _, edges in graph.edges.items(): for edge in edges: if self.optimize: if edge.tgt not in matrix or edge.src not in matrix: # skip adding edge if the target or source type # is not present in the pattern # (and therefor not added to the matrix) continue index = matrix[edge.tgt][0] matrix[edge.src][1][index] = True AM = [] vertices_order = [] for vertex, row in matrix.items(): AM.append(row[1]) vertices_order.append(vertex) return AM, vertices_order def matchUllmann(self): """ Find an occurrence of the pattern in the Graph by using Ullmann for solving the Constraint Satisfaction Problem (CSP). """ def createM_star(h, p): """ Create M*[v, w] = 1 if deg(v) <= deg(w), for v in V_P, w in V_H = 0 otherwise M and P are given to ensure corect order. """ m = [] # [[..], ...] for p_vertex in p: row = [] for g_vertex in h: # for the degree function, we choose to look at the # outgoing edges AND the incoming edges # (one might prefer to use only one of them) if self.optimize: # also check if type matches if p_vertex.type != g_vertex.type: row.append(False) continue row.append( len(p_vertex.incoming_edges) <= len(g_vertex.incoming_edges) and len(p_vertex.outgoing_edges) <= len(g_vertex.outgoing_edges)) m.append(row) return m def createDecreasingOrder(h): """ It turns out that the more edges a vertex has, the sooner it will fail in matching the pattern. For efficiency reasons, we want it to fail as fast as possible. """ order = [] # [(value, index), ...] index = 0 for g_vertex in h: order.append(( len(g_vertex.outgoing_edges) + len(g_vertex.outgoing_edges), index)) index += 1 order.sort(key = lambda elem: elem[0]) # sort and only return the indices (which specify the order) return [index for (_, index) in order] def propConnected(M, H, P, h, p): """ Checks if the vertices represented in M are isomorphic to P and if they can be matched onto the pattern graph. """ print(M, H, P, h, p) # P_candi = np.dot(M, np.transpose(np.dot(M, H))) """ # If we do not aply the refineM function, we will want to check if # this succeeds, as it checks for isomorphism. # If we apply the refineM function, it is garanteed to be isomorphic. index_column = 0 for row in P_candi: index_row = 0 for item in row: # for all i,j: P[i, j] = 1 : M(MH)^T [j, i] = 1 # (not the other way around) # (return False when item is 0 and P[i,j] is 1) if item < P[index_row][index_column]: return False index_row += 1 index_column += 1 """ vertices = {} index_column = 0 for row in M: index_row = 0 for item in row: # there should only be one item per row if item: vertex = h[index_row] vertices.setdefault(vertex.type, set()).add(vertex) break index_row += 1 index_column += 1 self.result = self.matchNaive(vertices=vertices, edges=self.graph.edges) return self.result != None def refineM(M, H, P, h, pp): """ Refine M, for every vertex from the pattern, check if each possible matching (candidate) his neighbours can also be matched. (M's column represents vertices from P, and the row represents its candidate.) If this is not possible set M[i,j] to false, refining/reducing the search space. """ any_changes=True while any_changes: any_changes = False # for all vertices from the pattern for i in range(0, len(P)): # P is a nxn-matrix # for all its possible assignments for j in range(0, len(H[0])): # if bound vertex of P, check if all neigbours are matchable if M[i][j]: # for all the pattern his neighbours for k in range(0, len(P)): # if it is a neighbour (from outgoing edges) if P[i][k]: match = False for p in range(0, len(H[0])): # check if we can match a candidate neighbour # (from M* to to the graph (H)) if M[k][p] and H[j][p]: if self.optimize: # also check correct type if pp[k].type != h[p].type: continue match = True break if not match: M[i][j] = False any_changes = True # if it is a neighbour (from incoming edges) if P[k][i]: match = False for p in range(0, len(H[0])): # check if we can match a candidate neighbour # (from M* to to the graph (H)) if M[k][p] and H[p][j]: if self.optimize: # also check correct type if pp[i].type != h[j].type: continue match = True break if not match: M[i][j] = False any_changes = True def findM(M_star, M, order, H, P, h, p, index_M=0): """ Find an isomorphic mapping for the vertices of P to H. This mapping is represented by a matrix M if, and only if M(MH)^T = P^T. """ # We are at the end, we found an candidate. # Remember that we are at the end, bu first check if there is # a row with ony False, if so, we do not need to check if it is # properly connected. check_prop = False if index_M == len(M): check_prop = True index_M -= 1 # we need to refer to this row old_row = M_star[index_M] # previous rows (these are sparse, 1 per row, save only its position) prev_pos = [] for i in range(0, index_M): row = M[i] only_false = True for j in range(0, len(old_row)): if row[j]: only_false = False prev_pos.append(j) break if only_false: # check if a row with only False occurs, # if so, we will not find an occurence return False # We are at the end, we found an candidate. if check_prop: index_M += 1 return propConnected(M, H, P, h, p) M[index_M] = [False] * len(old_row) index_order = 0 for index_order in range(0, len(order)): index_row = order[index_order] # put previous True back on False if index_order > 0: M[index_M][order[index_order - 1]] = False if old_row[index_row]: M[index_M][index_row] = True findMPart = True # 1 0 0 Assume 3th round, and we select x, # 0 1 0 no element at the same possition in the row, # 0 x 0 of the elements above itselve in the same # column may be 1. In the example it is, then try # selecting an other element. for index_column in range(0, index_M): if M[index_column][index_row]: findMPart = False break if not findMPart: continue refineM(M, H, P, h, p) if findM(M_star, M, order, H, P, h, p, index_M + 1): return True # reset previous rows their True's prev_row = 0 for pos in prev_pos: M[prev_row][pos] = True prev_row += 1 # reset rows below current row for index_column in range(index_M + 1, len(M)): # deep copy, we do not want to just copy pointer to array/list M[index_column] = M_star[index_column][:] # reset current row (the rest is already reset) M[index_M] = M_star[index_M][:] return False # create adjecency matrix of the graph H, h = self.createAdjacencyMatrixMap(self.graph) # create adjecency matrix of the pattern P, p = self.createAdjacencyMatrixMap(self.pattern) # create M* binary matrix M_star = createM_star(h, p) # create the order we will use later on order = createDecreasingOrder(h) # deepcopy M_s into M M = [row[:] for row in M_star] if self.optimize: refineM(M, H, P, h, p) findM(M_star, M, order, H, P, h, p) return self.result def matchVF2(self): class VF2_Obj(object): """ Structor for keeping the VF2 data. """ def __init__(self, len_graph_vertices, len_pattern_vertices): # represents if n-the element (h[n] or p[n]) matched self.core_graph = [False]*len_graph_vertices self.core_pattern = [False]*len_pattern_vertices # save mapping from pattern to graph self.mapping = {} # preference lvl 1 # ordered set of vertices adjecent to M_graph connected via an outgoing edge self.N_out_graph = [-1]*len_graph_vertices # ordered set of vertices adjecent to M_pattern connected via an outgoing edge self.N_out_pattern = [-1]*len_pattern_vertices # preference lvl 2 # ordered set of vertices adjecent to M_graph connected via an incoming edge self.N_inc_graph = [-1]*len_graph_vertices # ordered set of vertices adjecent to M_pattern connected via an incoming edge self.N_inc_pattern = [-1]*len_pattern_vertices # preference lvl 3 # not in the above def findM(H, P, h, p, VF2_obj, index_M=0): """ Find an isomorphic mapping for the vertices of P to H. This mapping is represented by a matrix M if, and only if M(MH)^T = P^T. This operates in a simular way as Ullmann. Ullmann has a predefind order for matching (sorted on most edges first). VF2's order is to first try to match the adjacency vertices connected via outgoing edges, then thos connected via incoming edges and then those that not connected to the currently mathed vertices. """ def addOutNeighbours(neighbours, N, index_M): """ Given outgoing neighbours (a row from an adjacency matrix), label them as added by saving when they got added (index_M represents this, otherwise it is -1) """ for neighbour_index in range(0, len(neighbours)): if neighbours[neighbour_index]: if N[neighbour_index] == -1: N[neighbour_index] = index_M def addIncNeighbours(G, j, N, index_M): """ Given the adjacency matrix, and the colum j, representing that we want to add the incoming edges to vertex j, label them as added by saving when they got added (index_M represents this, otherwise it is -1) """ for i in range(0, len(G)): if G[i][j]: if N[i] == -1: N[i] = index_M def delNeighbours(N, index_M): """ Remove neighbours that where added at index_M. If we call this function, we are backtracking and we want to remove the added neighbours from the just tried matching (n, m) pair (whiched failed). """ for n in range(0, len(N)): if N[n] == index_M: N[n] = -1 def feasibilityTest(H, P, h, p, VF2_obj, n, m): """ Examine all the nodes connected to n and m; if such nodes are in the current partial mapping, check if each branch from or to n has a corresponding branch from or to m and vice versa. If the nodes and the branches of the graphs being matched also carry semantic attributes, another condition must also hold for F(s, n, m) to be true; namely the attributes of the nodes and of the branches being paired must be compatible. Another pruning step is to check if the nr of ext_edges between the matched_vertices from the pattern and its adjecent vertices are less than or equal to the nr of ext_edges between matched_vertices from the graph and its adjecent vertices. And if the nr of ext_edges between those adjecent vertices from the pattern and the not connected vertices are less than or equal to the nr of ext_edges between those adjecent vertices from the graph and its adjecent vertices. """ # Get all neighbours from graph node n and pattern node m # (including n and m) neighbours_graph = {} neighbours_graph[h[n].type] = set([h[n]]) neighbours_pattern = {} neighbours_pattern[p[m].type] = set([p[m]]) # add all neihgbours of pattern vertex m for i in range(0, len(P)): # P is a nxn-matrix if (P[m][i] or P[i][m]) and VF2_obj.core_pattern[i]: neighbours_pattern.setdefault(p[i].type, set()).add(p[i]) # add all neihgbours of graph vertex n for i in range(0, len(H)): # P is a nxn-matrix if (H[n][i] or H[i][n]) and VF2_obj.core_graph[i]: neighbours_graph.setdefault(h[i].type, set()).add(h[i]) # take a coding shortcut, # use self.matchNaive function to see if it is feasable. # this way, we immidiatly test the semantic attributes if not self.matchNaive(pattern_vertices=neighbours_pattern, vertices=neighbours_graph, edges=self.graph.edges): return False # count ext_edges from core_graph to a adjecent vertices and # cuotn ext_edges for adjecent vertices and not matched vertices # connected via the ext_edges ext_edges_graph_ca = 0 ext_edges_graph_an = 0 # for all core vertices for x in range(0, len(VF2_obj.core_graph)): # for all its neighbours for y in range(0, len(H)): if H[x][y]: # if it is a neighbor and not yet matched if (VF2_obj.N_out_graph[y] != -1 or VF2_obj.N_inc_graph[y] != -1) and VF2_obj.core_graph[y]: # if we matched it if VF2_obj.core_graph[x] != -1: ext_edges_graph_ca += 1 else: ext_edges_graph_an += 1 # count ext_edges from core_pattern to a adjecent vertices # connected via the ext_edges ext_edges_pattern_ca = 0 ext_edges_pattern_an = 0 # for all core vertices for x in range(0, len(VF2_obj.core_pattern)): # for all its neighbours for y in range(0, len(P)): if P[x][y]: # if it is a neighbor and not yet matched if (VF2_obj.N_out_pattern[y] != -1 or VF2_obj.N_inc_pattern[y] != -1) and VF2_obj.core_pattern[y]: # if we matched it if VF2_obj.core_pattern[x] != -1: ext_edges_pattern_ca += 1 else: ext_edges_pattern_an += 1 # The nr of ext_edges between matched_vertices from the pattern # and its adjecent vertices must be less than or equal to the nr # of ext_edges between matched_vertices from the graph and its # adjecent vertices, otherwise we wont find an occurrence if ext_edges_pattern_ca > ext_edges_graph_ca: return False # The nr of ext_edges between those adjancent vertices from the # pattern and its not connected vertices must be less than or # equal to the nr of ext_edges between those adjacent vertices # from the graph and its not connected vertices, # otherwise we wont find an occurrence if ext_edges_pattern_an > ext_edges_graph_an: return False return True def matchPhase(H, P, h, p, index_M, VF2_obj, n, m): """ The matching fase of the VF2 algorithm. If the chosen n, m pair passes the feasibilityTest, the pair gets added and we start to search for the next matching pair. """ # all candidate pair (n, m) represent graph x pattern if feasibilityTest(H, P, h, p, VF2_obj, n, m): # adapt VF2_obj VF2_obj.core_graph[n] = True VF2_obj.core_pattern[m] = True VF2_obj.mapping[h[n]] = p[m] addOutNeighbours(H[n], VF2_obj.N_out_graph, index_M) addIncNeighbours(H, n, VF2_obj.N_inc_graph, index_M) addOutNeighbours(P[m], VF2_obj.N_out_pattern, index_M) addIncNeighbours(P, m, VF2_obj.N_inc_pattern, index_M) if findM(H, P, h, p, VF2_obj, index_M + 1): return True # else, cleanup, adapt VF2_obj VF2_obj.core_graph[n] = False VF2_obj.core_pattern[m] = False del VF2_obj.mapping[h[n]] delNeighbours(VF2_obj.N_out_graph, index_M) delNeighbours(VF2_obj.N_inc_graph, index_M) delNeighbours(VF2_obj.N_out_pattern, index_M) delNeighbours(VF2_obj.N_inc_pattern, index_M) return False def preferred(H, P, h, p, index_M, VF2_obj, N_graph, N_pattern): """ Try to match the adjacency vertices connected via outgoing or incoming edges. (Depending on what is given for N_graph and N_pattern.) """ for n in range(0, len(N_graph)): # skip graph vertices that are not in VF2_obj.N_out_graph # (or already matched) if N_graph[n] == -1 or VF2_obj.core_graph[n]: continue for m in range(0, len(N_pattern)): # skip graph vertices that are not in VF2_obj.N_out_pattern # (or already matched) if N_pattern[m] == -1 or VF2_obj.core_pattern[m]: continue if matchPhase(H, P, h, p, index_M, VF2_obj, n, m): return True return False def leastPreferred(H, P, h, p, index_M, VF2_obj): """ Try to match the vertices that are not connected to the curretly matched vertices. """ for n in range(0, len(VF2_obj.N_out_graph)): # skip vertices that are connected to the graph # (or already matched) if not (VF2_obj.N_out_graph[n] == -1 and VF2_obj.N_inc_graph[n] == -1) or VF2_obj.core_graph[n]: continue for m in range(0, len(VF2_obj.N_out_pattern)): # skip vertices that are connected to the graph # (or already matched) if not (VF2_obj.N_out_pattern[m] == -1 and VF2_obj.N_inc_pattern[m] == -1) or VF2_obj.core_pattern[m]: continue if matchPhase(H, P, h, p, index_M, VF2_obj, n, m): return True return False # We are at the end, we found an candidate. if index_M == len(p): bound_graph_vertices = {} for vertex_bound, _ in VF2_obj.mapping.items(): bound_graph_vertices.setdefault(vertex_bound.type, set()).add(vertex_bound) self.result = self.matchNaive(vertices=bound_graph_vertices, edges=self.graph.edges) return self.result != None # try the candidates is the preffered order # first try the adjacent vertices connected via the outgoing edges. if preferred(H, P, h, p, index_M, VF2_obj, VF2_obj.N_out_graph, VF2_obj.N_out_pattern): return True # then try the adjacent vertices connected via the incoming edges. if preferred(H, P, h, p, index_M, VF2_obj, VF2_obj.N_inc_graph, VF2_obj.N_inc_pattern): return True # and lastly, try the vertices not connected to the currently matched vertices if leastPreferred(H, P, h, p, index_M, VF2_obj): return True return False # create adjecency matrix of the graph H, h = self.createAdjacencyMatrixMap(self.graph) # create adjecency matrix of the pattern P, p = self.createAdjacencyMatrixMap(self.pattern) VF2_obj = VF2_Obj(len(h), len(p)) findM(H, P, h, p, VF2_obj) return self.result