|
@@ -0,0 +1,947 @@
|
|
|
+# 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
|