Kaynağa Gözat

Tweak matcher (compute connected components in advance). Simple pattern matching with RAMification (incl. Python expressions) seems to work.

Joeri Exelmans 1 yıl önce
ebeveyn
işleme
4160a8953e

+ 9 - 0
bootstrap/scd.py

@@ -53,6 +53,8 @@ def bootstrap_scd(state: State) -> UUID:
         _optional_edge = add_edge_element(f"{attribute_element_name}.optional_link", attribute_element, _optional_node)
         return _name_model, _optional_model
 
+    ##### SCD META-MODEL #####
+
     # # CLASSES, i.e. elements typed by Class
     # # Element
     element_node = add_node_element("Element")
@@ -64,6 +66,7 @@ def bootstrap_scd(state: State) -> UUID:
     model_ref_node = add_node_element("ModelRef")
     # # Global Constraint
     glob_constr_node = add_node_element("GlobalConstraint")
+
     # # ASSOCIATIONS, i.e. elements typed by Association
     # # Association
     assoc_edge = add_edge_element("Association", class_node, class_node)
@@ -71,6 +74,7 @@ def bootstrap_scd(state: State) -> UUID:
     inh_edge = add_edge_element("Inheritance", element_node, element_node)
     # # Attribute Link
     attr_link_edge = add_edge_element("AttributeLink", element_node, attr_node)
+
     # # INHERITANCES, i.e. elements typed by Inheritance
     # # Class inherits from Element
     add_edge_element("class_inh_element", class_node, element_node)
@@ -84,9 +88,11 @@ def bootstrap_scd(state: State) -> UUID:
     add_edge_element("attr_link_inh_element", attr_link_edge, element_node)
     # # ModelRef inherits from Attribute
     add_edge_element("model_ref_inh_attr", model_ref_node, attr_node)
+
     # # ATTRIBUTES, i.e. elements typed by Attribute
     # # Action Code # TODO: Update to ModelRef when action code is explicitly modelled
     action_code_node = add_node_element("ActionCode")
+
     # # MODELREFS, i.e. elements typed by ModelRef
     # # Integer
     integer_node = add_node_element("Integer", str(integer_type_root))
@@ -94,6 +100,7 @@ def bootstrap_scd(state: State) -> UUID:
     string_node = add_node_element("String", str(string_type_root))
     # # Boolean
     boolean_node = add_node_element("Boolean", str(boolean_type_root))
+
     # # ATTRIBUTE LINKS, i.e. elements typed by AttributeLink
     # # name attribute of AttributeLink
     attr_name_edge = add_edge_element("AttributeLink_name", attr_link_edge, string_node)
@@ -111,6 +118,7 @@ def bootstrap_scd(state: State) -> UUID:
     assoc_s_u_c_edge = add_edge_element("Association_source_upper_cardinality", assoc_edge, integer_node)
     assoc_t_l_c_edge = add_edge_element("Association_target_lower_cardinality", assoc_edge, integer_node)
     assoc_t_u_c_edge = add_edge_element("Association_target_upper_cardinality", assoc_edge, integer_node)
+
     # # bootstrap primitive types
     # # order is important, integer must be first
     bootstrap_integer_type(mcl_root, integer_type_root, state)
@@ -118,6 +126,7 @@ def bootstrap_scd(state: State) -> UUID:
     bootstrap_float_type(mcl_root, float_type_root, state)
     bootstrap_string_type(mcl_root, string_type_root, state)
     bootstrap_type_type(mcl_root, type_type_root, state)
+
     # # ATTRIBUTE ATTRIBUTES, assign 'name' and 'optional' attributes to all AttributeLinks
     # # AttributeLink_name
     m_name, m_opt = add_attribute_attributes("AttributeLink_name", attr_name_edge)

+ 39 - 3
experiments/exp_scd.py

@@ -7,7 +7,10 @@ from services.scd import SCD
 from framework.conformance import Conformance
 from services.od import OD
 from transformation.ramify import ramify
+from services.bottom.V0 import Bottom
 from services.primitives.integer_type import Integer
+from pattern_matching import mvs_adapter
+from pattern_matching.matcher import MatcherVF2
 
 import sys
 
@@ -62,6 +65,9 @@ def main():
     int_type_id = state.read_dict(state.read_root(), "Integer")
     int_type = UUID(state.read_value(int_type_id))
 
+    string_type_id = state.read_dict(state.read_root(), "String")
+    string_type = UUID(state.read_value(string_type_id))
+
     # scd2 = SCD(scd_node, state)
     # for el in scd2.list_elements():
     #     print(el)
@@ -70,7 +76,7 @@ def main():
     model_id = state.create_node()
     scd = SCD(model_id, state)
     scd.create_class("Abstract", abstract=True)
-    scd.create_class("A", min_c=1, max_c=10)
+    scd.create_class("A", min_c=1, max_c=2)
     scd.create_inheritance("A", "Abstract")
     scd.create_model_ref("Integer", int_type)
     scd.create_attribute_link("A", "Integer", "size", False)
@@ -97,16 +103,46 @@ def main():
     od = OD(model_id, inst_id, state)
 
     od.create_object("a", "A")
+    od.create_object("a2", "A")
     od.create_object("b", "B")
     od.create_link("A2B", "a", "b")
+    od.create_link("A2B", "a2", "b")
 
-    od.create_slot("size", "a", od.create_integer_value(42))
+    od.create_slot("size", "a", od.create_integer_value("a.size", 42))
 
     print("checking conformance....")
     conf2 = Conformance(state, inst_id, model_id)
     print("conforms?", conf2.check_nominal(log=True))
 
-    ramify(state, model_id)
+    ramified_MM_id = ramify(state, model_id)
+
+    pattern_id = state.create_node()
+    pattern = OD(ramified_MM_id, pattern_id, state)
+
+    pattern.create_object("a1", "A")
+    pattern.create_slot("size", "a1", pattern.create_string_value("a1.size", 'v < 100'))
+    # pattern.create_object("a2", "A")
+    # pattern.create_slot("size", "a2", pattern.create_string_value("a2.size", '99'))
+
+    pattern.create_object("b1", "B")
+    # pattern.create_link("A2B", "a1", "b1")
+
+    conf3 = Conformance(state, pattern_id, ramified_MM_id)
+    print("conforms?", conf3.check_nominal(log=True))
+
+    host = mvs_adapter.model_to_graph(state, inst_id)
+    guest = mvs_adapter.model_to_graph(state, pattern_id)
+
+    print(host.vtxs)
+    print(host.edges)
+
+    print("matching...")
+    matcher = MatcherVF2(host, guest, mvs_adapter.RAMCompare(Bottom(state)))
+    prev = None
+    for m in matcher.match():
+        print("\nMATCH:\n", m)
+        input()
+    print("DONE")
 
 if __name__ == "__main__":
     main()

+ 30 - 0
pattern_matching/generator.py

@@ -200,3 +200,33 @@ class GraphGenerator(object):
 
         # ----------------------------------------------------------------------
         return pattern
+
+def get_random_host_and_guest(nr_vtxs, nr_vtx_types, nr_edges, nr_edge_types, pattern_nr_vtxs=3, pattern_nr_edges=15):
+    dv      = [random.randint(0, nr_vtx_types) for _ in range(nr_vtxs)]
+    de      = [random.randint(0, nr_edge_types) for _ in range(nr_edges)]
+    dc_inc    = [random.randint(0, nr_vtxs-1) for _ in range(nr_edges)]
+    dc_out    = [random.randint(0, nr_vtxs-1) for _ in range(nr_edges)]
+    
+    return get_host_and_guest(dv, de, dc_inc, dc_out, pattern_nr_vtxs, pattern_nr_edges)
+
+def get_host_and_guest(dv, de, dc_inc, dc_out, pattern_nr_vtxs=3, pattern_nr_edges=15):
+    gg    = GraphGenerator(dv, de, dc_inc, dc_out)
+    graph    = gg.getRandomGraph()
+    pattern    = gg.getRandomPattern(pattern_nr_vtxs, pattern_nr_edges, debug=False)
+    return (graph, pattern)
+
+
+def get_large_host_and_guest():
+    dv        = [ 10,5,4,0,8,6,8,0,4,8,5,5,7,0,10,0,5,6,10,4,0,3,0,8,2,7,5,8,1,0,2,10,0,0,1,6,8,4,7,6,4,2,10,10,6,4,6,0,2,7 ]
+    de        = [ 8,10,8,1,6,7,4,3,5,2,0,0,9,6,0,3,8,3,2,7,2,3,10,8,10,8,10,2,5,5,10,6,7,5,1,2,1,2,2,3,7,7,2,1,7,2,9,10,8,1,9,4,1,3,1,1,8,2,2,9,10,9,1,9,4,10,10,10,9,3,5,3,6,6,9,1,2,6,3,2,4,10,9,6,5,6,2,4,3,2,4,10,6,2,8,8,0,5,1,7,3,4,3,8,7,3,0,8,3,3,8,5,10,5,9,3,1,10,3,2,6,3,10,0,5,10,9,10,0,1,4,7,10,3,1,9,1,2,3,7,4,3,7,8,8,4,5,10,1,4 ]
+    dc_inc    = [ 0,25,18,47,22,25,16,45,38,25,5,45,15,44,17,46,6,17,35,8,16,29,48,47,25,34,4,20,24,1,47,44,8,25,32,3,16,6,33,21,6,13,41,10,17,25,21,33,31,30,5,4,45,26,16,42,12,25,29,3,32,30,14,26,11,13,7,13,3,43,43,22,48,37,20,28,15,40,19,33,43,16,49,36,11,25,9,42,3,22,16,40,42,44,27,30,1,18,10,35,19,6,9,43,37,38,45,19,41,14,37,45,0,31,29,31,24,20,44,46,8,45,43,3,38,38,35,12,19,45,7,34,20,28,12,17,45,17,35,49,20,21,49,1,35,38,38,36,33,30 ]
+    dc_out    = [ 9,2,49,49,37,33,16,21,5,46,4,15,9,6,14,22,16,33,23,21,15,31,37,23,47,3,30,26,35,9,29,21,39,32,22,43,5,9,41,30,31,30,37,33,31,34,23,22,34,26,44,36,38,33,48,5,9,34,13,7,48,41,43,26,26,7,12,6,12,28,22,8,29,22,24,27,16,4,31,41,32,15,19,20,38,0,26,18,43,46,40,17,29,14,34,14,32,17,32,47,16,45,7,4,35,22,42,11,38,2,0,29,4,38,17,44,9,23,5,10,31,17,1,11,16,5,37,27,35,32,45,16,18,1,14,4,42,24,43,31,21,38,6,34,39,46,20,1,38,47 ]
+    return get_host_and_guest(dv, de, dc_inc, dc_out)
+
+def get_small_host_and_guest():
+    dv = [0, 1, 0, 1, 0]
+    de = [0, 0, 0]
+    dc_inc = [0, 2, 4]
+    dc_out = [1, 3, 3]
+    return get_host_and_guest(dv, de, dc_inc, dc_out)
+

+ 4 - 1
pattern_matching/graph.py

@@ -94,8 +94,9 @@ class Graph(object):
     def __init__(self):
         # member variables:
         # redundant type keeping, "needed" for fast iterating over specific type
-        self.vertices    = {}    # {type, set(v1, v2, ...)}
+        self.vertices     = {}    # {type, set(v1, v2, ...)}
         self.edges        = {}    # {type, set(e1, e2, ...)}
+        self.num_vertices = 0
         
     def addCreateVertex(self, str_type):
         """
@@ -114,6 +115,8 @@ class Graph(object):
             raise TypeError('addVertex expects a Vertex')
         # add vertex, but it first creates a new set for the vertex type
         # if the type does not exist in the dictionary
+        if vertex not in self.vertices.get(vertex.type, set()):
+            self.num_vertices += 1
         self.vertices.setdefault(vertex.type, set()).add(vertex)
 
     def getVerticesOfType(self, str_type):

+ 9 - 26
pattern_matching/main.py

@@ -31,34 +31,17 @@ if __name__ == '__main__':
     """
     The main function called when running from the command line.
     """
-    nr_of_vertices        = 50
-    nr_of_diff_types_v    = 2
-    nr_of_edges            = 150
-    nr_of_diff_types_e    = 2
-
-    dv      = [random.randint(0, nr_of_diff_types_v) for _ in range(nr_of_vertices)]
-    de      = [random.randint(0, nr_of_diff_types_e) for _ in range(nr_of_edges)]
-    dc_inc    = [random.randint(0, nr_of_vertices-1) for _ in range(nr_of_edges)]
-    dc_out    = [random.randint(0, nr_of_vertices-1) for _ in range(nr_of_edges)]
-
-    # override random graph by copy pasting output from terminal
-    # dv        = [ 10,5,4,0,8,6,8,0,4,8,5,5,7,0,10,0,5,6,10,4,0,3,0,8,2,7,5,8,1,0,2,10,0,0,1,6,8,4,7,6,4,2,10,10,6,4,6,0,2,7 ]
-    # de        = [ 8,10,8,1,6,7,4,3,5,2,0,0,9,6,0,3,8,3,2,7,2,3,10,8,10,8,10,2,5,5,10,6,7,5,1,2,1,2,2,3,7,7,2,1,7,2,9,10,8,1,9,4,1,3,1,1,8,2,2,9,10,9,1,9,4,10,10,10,9,3,5,3,6,6,9,1,2,6,3,2,4,10,9,6,5,6,2,4,3,2,4,10,6,2,8,8,0,5,1,7,3,4,3,8,7,3,0,8,3,3,8,5,10,5,9,3,1,10,3,2,6,3,10,0,5,10,9,10,0,1,4,7,10,3,1,9,1,2,3,7,4,3,7,8,8,4,5,10,1,4 ]
-    # dc_inc    = [ 0,25,18,47,22,25,16,45,38,25,5,45,15,44,17,46,6,17,35,8,16,29,48,47,25,34,4,20,24,1,47,44,8,25,32,3,16,6,33,21,6,13,41,10,17,25,21,33,31,30,5,4,45,26,16,42,12,25,29,3,32,30,14,26,11,13,7,13,3,43,43,22,48,37,20,28,15,40,19,33,43,16,49,36,11,25,9,42,3,22,16,40,42,44,27,30,1,18,10,35,19,6,9,43,37,38,45,19,41,14,37,45,0,31,29,31,24,20,44,46,8,45,43,3,38,38,35,12,19,45,7,34,20,28,12,17,45,17,35,49,20,21,49,1,35,38,38,36,33,30 ]
-    # dc_out    = [ 9,2,49,49,37,33,16,21,5,46,4,15,9,6,14,22,16,33,23,21,15,31,37,23,47,3,30,26,35,9,29,21,39,32,22,43,5,9,41,30,31,30,37,33,31,34,23,22,34,26,44,36,38,33,48,5,9,34,13,7,48,41,43,26,26,7,12,6,12,28,22,8,29,22,24,27,16,4,31,41,32,15,19,20,38,0,26,18,43,46,40,17,29,14,34,14,32,17,32,47,16,45,7,4,35,22,42,11,38,2,0,29,4,38,17,44,9,23,5,10,31,17,1,11,16,5,37,27,35,32,45,16,18,1,14,4,42,24,43,31,21,38,6,34,39,46,20,1,38,47 ]
-
-    dv = [0, 1, 0, 1, 0]
-    de = [0, 0, 0]
-    dc_inc = [0, 2, 4]
-    dc_out = [1, 3, 3]
-    
-    gg    = GraphGenerator(dv, de, dc_inc, dc_out, debug)
+    random.seed(0)
 
-    graph    = gg.getRandomGraph()
+    graph, pattern = get_random_host_and_guest(
+        nr_vtxs        = 10,
+        nr_vtx_types   = 0,
+        nr_edges       = 20,
+        nr_edge_types  = 0,
+    )
 
-    print(graph.vertices)
-    pattern    = gg.getRandomPattern(3, 15, debug=debug)
-    print(pattern.vertices)
+    # graph, pattern = get_large_host_and_guest()
+    # graph, pattern = get_small_host_and_guest()
 
     # override random pattern by copy pasting output from terminal to create
     # pattern, paste it in the createConstantPattern function in the generator.py

+ 105 - 36
pattern_matching/matcher.py

@@ -3,6 +3,34 @@
 
 import itertools
 
+from util.timer import Timer
+
+# like finding the 'strongly connected componenets', but edges are navigable in any direction
+def find_connected_components(graph):
+    next_component = 0
+    vtx_to_component = {}
+    component_to_vtxs = []
+    for vtx in graph.vtxs:
+        if vtx in vtx_to_component:
+            continue
+        vtx_to_component[vtx] = next_component
+        vtxs = []
+        component_to_vtxs.append(vtxs)
+        add_recursively(vtx, vtxs, vtx_to_component, next_component)
+        next_component += 1
+    return (vtx_to_component, component_to_vtxs)
+
+def add_recursively(vtx, vtxs: list, d: dict, component: int, already_visited: set = set()):
+    if vtx in already_visited:
+        return
+    already_visited.add(vtx)
+    vtxs.append(vtx)
+    d[vtx] = component
+    for edge in vtx.outgoing:
+        add_recursively(edge.tgt, vtxs, d, component, already_visited)
+    for edge in vtx.incoming:
+        add_recursively(edge.src, vtxs, d, component, already_visited)
+
 class Graph:
     def __init__(self):
         self.vtxs = []
@@ -18,14 +46,20 @@ class Vertex:
         return f"V({self.value})"
 
 class Edge:
-    def __init__(self, src: Vertex, tgt: Vertex):
+    def __init__(self, src: Vertex, tgt: Vertex, label=None):
         self.src = src
         self.tgt = tgt
+        self.label = label
+
+        # Add ourselves to src/tgt vertices
         self.src.outgoing.append(self)
         self.tgt.incoming.append(self)
 
     def __repr__(self):
-        return f"E({self.src}->{self.tgt})"
+        if self.label != None:
+            return f"E({self.src}--{self.label}->{self.tgt})"
+        else:
+            return f"E({self.src}->{self.tgt})"
 
 class MatcherState:
     def __init__(self):
@@ -38,8 +72,7 @@ class MatcherState:
         self.h_unmatched_vtxs = []
         self.g_unmatched_vtxs = []
 
-        # the most recently added pair of (guest,host) vertices
-        # will always try to grow mapping via outgoing/incoming edges of this pair before attempting other non-connected vertices
+        # boundary is the most recently added (to the mapping) pair of (guest -> host) vertices
         self.boundary = None
 
     @staticmethod
@@ -89,6 +122,9 @@ class MatcherState:
             ((ge,he) for ge,he in self.mapping_edges.items()),
         ))
 
+    def __repr__(self):
+        # return self.make_hashable().__repr__()
+        return "VTXS: "+self.mapping_vtxs.__repr__()+"\nEDGES: "+self.mapping_edges.__repr__()
 
 class MatcherVF2:
     # Guest is the pattern
@@ -97,6 +133,14 @@ class MatcherVF2:
         self.guest = guest
         self.compare_fn = compare_fn
 
+        with Timer("find_connected_components - host"):
+            self.host_vtx_to_component, self.host_component_to_vtxs = find_connected_components(host)
+        with Timer("find_connected_components - guest"):
+            self.guest_vtx_to_component, self.guest_component_to_vtxs = find_connected_components(guest)
+
+        print("number of host connected components:", len(self.host_component_to_vtxs))
+        print("number of guest connected components:", len(self.guest_component_to_vtxs))
+
     def match(self):
         yield from self._match(
             state=MatcherState.make_initial(self.host, self.guest),
@@ -104,26 +148,30 @@ class MatcherVF2:
 
 
     def _match(self, state, already_visited, indent=0):
+        # input()
+
         def print_debug(*args):
             pass
-            # print(*args) # uncomment to see a trace of the matching process
+            # print("  "*indent, *args) # uncomment to see a trace of the matching process
 
-        print_debug("  "*indent, "match")
+        print_debug("match")
 
-        hashable_state = state.make_hashable()
-        if hashable_state in already_visited:
-            print_debug("  "*indent, "    SKIP - ALREADY VISITED")
-            print_debug("  "*indent, "   ", hashable_state)
+        # Keep track of the states in the search space that we already visited
+        hashable = state.make_hashable()
+        if hashable in already_visited:
+            print_debug("    SKIP - ALREADY VISITED")
+            # print_debug("   ", hashable)
             return
-        print_debug("  "*indent, "    ADD STATE")
-        print_debug("  "*indent, "   ", hashable_state)
-        already_visited.add(hashable_state)
+        # print_debug("   ", [hash(a) for a in already_visited])
+        # print_debug("    ADD STATE")
+        # print_debug("   ", hash(hashable))
+        already_visited.add(hashable)
 
 
         if len(state.mapping_vtxs) == len(self.guest.vtxs) and len(state.mapping_edges) == len(self.guest.edges):
-            print_debug("  "*indent, "GOT MATCH:")
-            print_debug("  "*indent, " ", state.mapping_vtxs)
-            print_debug("  "*indent, " ", state.mapping_edges)
+            print_debug("GOT MATCH:")
+            print_debug(" ", state.mapping_vtxs)
+            print_debug(" ", state.mapping_edges)
             yield state
             return
 
@@ -136,63 +184,84 @@ class MatcherVF2:
                 raise Exception("wtf!")
 
         def attempt_grow(direction, indent):
-            print_debug("  "*indent, 'attempt_grow', direction)
-            if state.boundary != None:
-                g_vtx, h_vtx = state.boundary
-                for g_candidate_edge in getattr(g_vtx, direction):
-                    print_debug("  "*indent, 'g_candidate_edge:', g_candidate_edge)
+            for g_matched_vtx, h_matched_vtx in state.mapping_vtxs.items():
+                print_debug('attempt_grow', direction)
+                for g_candidate_edge in getattr(g_matched_vtx, direction):
+                    print_debug('g_candidate_edge:', g_candidate_edge)
+                    g_candidate_vtx = read_edge(g_candidate_edge, direction)
+                    # g_to_skip_vtxs.add(g_candidate_vtx)
                     if g_candidate_edge in state.mapping_edges:
-                        print_debug("  "*indent, "  skip, guest edge already matched")
+                        print_debug("  skip, guest edge already matched")
                         continue # skip already matched guest edge
-                    g_candidate_vtx = read_edge(g_candidate_edge, direction)
-                    for h_candidate_edge in getattr(h_vtx, direction):
-                        print_debug("  "*indent, 'h_candidate_edge:', h_candidate_edge)
+                    for h_candidate_edge in getattr(h_matched_vtx, direction):
+                        if g_candidate_edge.label != h_candidate_edge.label:
+                            print_debug("  labels differ")
+                            continue
+                        print_debug('h_candidate_edge:', h_candidate_edge)
                         if h_candidate_edge in state.r_mapping_edges:
-                            print_debug("  "*indent, "  skip, host edge already matched")
+                            print_debug("  skip, host edge already matched")
                             continue # skip already matched host edge
                         h_candidate_vtx = read_edge(h_candidate_edge, direction)
-                        print_debug("  "*indent, 'grow edge', g_candidate_edge, ':', h_candidate_edge)
+                        print_debug('grow edge', g_candidate_edge, ':', h_candidate_edge, id(g_candidate_edge), id(h_candidate_edge))
                         new_state = state.grow_edge(h_candidate_edge, g_candidate_edge)
                         yield from attempt_match_vtxs(
                             new_state,
                             g_candidate_vtx,
                             h_candidate_vtx,
                             indent+1)
+                        print_debug('backtrack edge', g_candidate_edge, ':', h_candidate_edge, id(g_candidate_edge), id(h_candidate_edge))
 
         def attempt_match_vtxs(state, g_candidate_vtx, h_candidate_vtx, indent):
-            print_debug("  "*indent, 'attempt_match_vtxs')
+            print_debug('attempt_match_vtxs')
             if g_candidate_vtx in state.mapping_vtxs:
                 if state.mapping_vtxs[g_candidate_vtx] != h_candidate_vtx:
-                    print_debug("  "*indent, "  nope, guest already mapped (mismatch)")
+                    print_debug("  nope, guest already mapped (mismatch)")
                     return # guest vtx is already mapped but doesn't match host vtx
             if h_candidate_vtx in state.r_mapping_vtxs:
                 if state.r_mapping_vtxs[h_candidate_vtx] != g_candidate_vtx:
-                    print_debug("  "*indent, "  nope, host already mapped (mismatch)")
+                    print_debug("  nope, host already mapped (mismatch)")
                     return # host vtx is already mapped but doesn't match guest vtx
             g_outdegree = len(g_candidate_vtx.outgoing)
             h_outdegree = len(h_candidate_vtx.outgoing)
             if g_outdegree > h_outdegree:
+                print_debug("  nope, outdegree")
                 return
             g_indegree = len(g_candidate_vtx.incoming)
             h_indegree = len(h_candidate_vtx.incoming)
             if g_indegree > h_indegree:
+                print_debug("  nope, indegree")
                 return
             if not self.compare_fn(g_candidate_vtx.value, h_candidate_vtx.value):
+                print_debug("  nope, bad compare")
                 return
             new_state = state.grow_vtx(
                 h_candidate_vtx,
                 g_candidate_vtx)
-            print_debug("  "*indent, 'grow vtx', g_candidate_vtx, ':', h_candidate_vtx)
+            print_debug('grow vtx', g_candidate_vtx, ':', h_candidate_vtx, id(g_candidate_vtx), id(h_candidate_vtx))
             yield from self._match(new_state, already_visited, indent+1)
+            print_debug('backtrack vtx', g_candidate_vtx, ':', h_candidate_vtx, id(g_candidate_vtx), id(h_candidate_vtx))
 
-        print_debug("  "*indent, 'preferred...')
+        print_debug('preferred...')
         yield from attempt_grow('outgoing', indent+1)
         yield from attempt_grow('incoming', indent+1)
 
-        print_debug("  "*indent, 'least preferred...')
-        for g_candidate_vtx in state.g_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)
+        print_debug('least preferred...')
+        if state.boundary != None:
+            g_boundary_vtx, _ = state.boundary
+            guest_boundary_component = self.guest_vtx_to_component[g_boundary_vtx]
+            # only try guest vertices that are in a different component (all vertices in the same component are already discovered via 'attempt_grow')
+            guest_components_to_try = (c for i,c in enumerate(self.guest_component_to_vtxs) if i != guest_boundary_component)
+            # for the host vertices however, we have to try them from all components, because different connected components of our pattern (=guest) could be mapped onto the same connected component in the host
+        else:
+            guest_components_to_try = self.guest_component_to_vtxs
+
+        for g_candidate_vtxs in guest_components_to_try:
+            for g_candidate_vtx in g_candidate_vtxs:
+                if g_candidate_vtx in state.mapping_vtxs:
+                    print_debug("skip (already matched)", g_candidate_vtx)
+                    continue
+                for h_candidate_vtx in state.h_unmatched_vtxs:
+                    yield from attempt_match_vtxs(state, g_candidate_vtx, h_candidate_vtx, indent+1)
 
         if indent == 0:
             print_debug('visited', len(already_visited), 'states total')

+ 156 - 0
pattern_matching/mvs_adapter.py

@@ -0,0 +1,156 @@
+from state.base import State
+from uuid import UUID
+from services.bottom.V0 import Bottom
+from pattern_matching.matcher import Graph, Edge, Vertex
+import itertools
+import re
+
+from util.timer import Timer
+
+class _is_edge:
+    def __repr__(self):
+        return "EDGE"
+# just a unique symbol that is only equal to itself
+IS_EDGE = _is_edge()
+
+class IS_TYPE:
+    def __init__(self, type):
+        # mvs-node of the type
+        self.type = type
+
+    def __repr__(self):
+        return f"TYPE({str(self.type)[-4:]})"
+
+    # def __eq__(self, other):
+    #     if not isinstance(other, IS_TYPE):
+    #         return False
+    #     return other.type == self.type
+
+    # def __hash__(self):
+    #     return self.type.__hash__()
+
+
+UUID_REGEX = re.compile(r"[0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z]-[0-9a-z][0-9a-z][0-9a-z][0-9a-z]-[0-9a-z][0-9a-z][0-9a-z][0-9a-z]-[0-9a-z][0-9a-z][0-9a-z][0-9a-z]-[0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z]")
+
+
+# Converts an object/class diagram in MVS state to the pattern matcher graph type
+# ModelRefs are flattened
+def model_to_graph(state: State, model: UUID):
+    with Timer("model_to_graph"):
+        bottom = Bottom(state)
+
+        graph = Graph()
+
+        mvs_edges = []
+        modelrefs = {}
+        def extract_modelref(el):
+            value = bottom.read_value(el)
+            # If the value of the el is a ModelRef (only way to detect this is to match a regex - not very clean), then extract it. We'll create a link to the referred model later.
+            if bottom.is_edge(el):
+                mvs_edges.append(el)
+                return IS_EDGE
+            if isinstance(value, str):
+                if UUID_REGEX.match(value) != None:
+                    # side-effect
+                    modelrefs[el] = UUID(value)
+                    return None
+            return value
+
+        # MVS-Nodes become vertices
+        uuid_to_vtx = { node: Vertex(value=extract_modelref(node)) for node in bottom.read_outgoing_elements(model) }
+        graph.vtxs = [ vtx for vtx in uuid_to_vtx.values() ]
+
+        # For every MSV-Edge, two edges are created (for src and tgt)
+        for mvs_edge in mvs_edges:
+            mvs_src = bottom.read_edge_source(mvs_edge)
+            if mvs_src in uuid_to_vtx:
+                graph.edges.append(Edge(
+                    src=uuid_to_vtx[mvs_src],
+                    tgt=uuid_to_vtx[mvs_edge],
+                    label="outgoing"))
+            mvs_tgt = bottom.read_edge_target(mvs_edge)
+            if mvs_tgt in uuid_to_vtx:
+                graph.edges.append(Edge(
+                    src=uuid_to_vtx[mvs_tgt],
+                    tgt=uuid_to_vtx[mvs_edge],
+                    label="tgt"))
+
+
+        for node, ref in modelrefs.items():
+            # Recursively convert ref'ed model to graph
+            ref_model = model_to_graph(state, ref)
+
+            # Flatten and create link to ref'ed model
+            graph.vtxs += ref_model.vtxs
+            graph.edges += ref_model.edges
+            graph.edges.append(Edge(
+                src=uuid_to_vtx[node],
+                tgt=ref_model.vtxs[0], # which node to link to?? dirty
+                label="modelref"))
+
+        # # Add typing information
+        # for i,node in enumerate(bottom.read_outgoing_elements(model)):
+        #     type_node, = bottom.read_outgoing_elements(node, "Morphism")
+        #     print('node', node, 'has type', type_node)
+        #     # We create a Vertex storing the type
+        #     type_vertex = Vertex(value=IS_TYPE(type_node))
+        #     graph.vtxs.append(type_vertex)
+        #     type_edge = Edge(
+        #         src=uuid_to_vtx[node],
+        #         tgt=type_vertex,
+        #         label="type")
+        #     print(type_edge)
+        #     graph.edges.append(type_edge)
+
+        return graph
+
+# Function object for pattern matching. Decides whether to match host and guest vertices, where guest is a RAMified instance (e.g., the attributes are all strings with Python expressions), and the host is an instance (=object diagram) of the original model (=class diagram)
+class RAMCompare:
+    def __init__(self, bottom):
+        self.bottom = bottom
+
+        type_model_id = bottom.state.read_dict(bottom.state.read_root(), "SCD")
+        self.scd_model = UUID(bottom.state.read_value(type_model_id))
+
+    def is_subtype_of(self, supposed_subtype: UUID, supposed_supertype: UUID):
+        inheritance_node, = self.bottom.read_outgoing_elements(self.scd_model, "Inheritance")
+
+        if supposed_subtype == supposed_supertype:
+            # reflexive:
+            return True
+
+        for outgoing in self.bottom.read_outgoing_edges(supposed_subtype):
+            if inheritance_node in self.bottom.read_outgoing_elements(outgoing, "Morphism"):
+                # 'outgoing' is an inheritance link
+                supertype = self.bottom.read_edge_target(outgoing)
+                if supertype != supposed_subtype:
+                    if self.is_subtype_of(supertype, supposed_supertype):
+                        return True
+
+        return False
+
+    def __call__(self, g_val, h_val):
+        if g_val == None:
+            return h_val == None
+
+        # mvs-edges (which are converted to vertices) only match with mvs-edges
+        if g_val == IS_EDGE:
+            return h_val == IS_EDGE
+
+        if h_val == IS_EDGE:
+            return False
+
+        # types only match with their supertypes
+        if isinstance(g_val, IS_TYPE):
+            if not isinstance(h_val, IS_TYPE):
+                return False
+            g_val_original_type = self.bottom.read_outgoing_elements(g_val.type, "RAMifies")
+            result = self.is_subtype_of(h_val.type, g_val_original_type)
+            print("RESULT", result)
+            return result
+
+        if isinstance(h_val, IS_TYPE):
+            return False
+
+        # print(g_val, h_val)
+        return eval(g_val, {}, {'v': h_val})

+ 3 - 0
services/bottom/V0.py

@@ -81,6 +81,9 @@ class Bottom:
         result = self.state.read_edge(edge)
         return result[1] if result != None else result
 
+    def is_edge(self, elem: UUID) -> bool:
+        return self.state.is_edge(elem)
+
     def read_incoming_edges(self, target: UUID, label=None) -> List[UUID]:
         """
         Reads incoming edges of an element. Optionally, filter them based on their label

+ 13 - 2
services/od.py

@@ -59,16 +59,26 @@ class OD:
         # An attribute-link is indistinguishable from an ordinary link:
         return self.create_link(attr_link_name, object_name, target_name)
 
-    def create_integer_value(self, value: int):
+    def create_integer_value(self, name: str, value: int):
         from services.primitives.integer_type import Integer
         int_node = self.bottom.create_node()
         integer_t = Integer(int_node, self.bottom.state)
         integer_t.create(value)
-        name = 'int'+str(value) # name of the ref to the created integer
+        # name = 'int'+str(value) # name of the ref to the created integer
         # By convention, the type model must have a ModelRef named "Integer"
         self.create_model_ref(name, "Integer", int_node)
         return name
 
+    def create_string_value(self, name: str, value: str):
+        from services.primitives.string_type import String
+        string_node = self.bottom.create_node()
+        string_t = String(string_node, self.bottom.state)
+        string_t.create(value)
+        # name = 'str-'+value # name of the ref to the created integer
+        # By convention, the type model must have a ModelRef named "Integer"
+        self.create_model_ref(name, "String", string_node)
+        return name
+
     # Identical to the same SCD method:
     def create_model_ref(self, name: str, type_name: str, model: UUID):
         # create element + morphism links
@@ -79,6 +89,7 @@ class OD:
 
 
     def create_link(self, assoc_name: str, src_obj_name: str, tgt_obj_name: str):
+        print(tgt_obj_name)
         src_obj_node, = self.bottom.read_outgoing_elements(self.model, src_obj_name)
         tgt_obj_node, = self.bottom.read_outgoing_elements(self.model, tgt_obj_name)
 

+ 2 - 0
services/scd.py

@@ -72,6 +72,8 @@ class SCD:
         if max_c != None:
             set_cardinality("upper", max_c)
 
+        return class_node
+
     def create_association(self, name: str, source: str, target: str,
                            src_min_c: int = None, src_max_c: int = None,
                            tgt_min_c: int = None, tgt_max_c: int = None):

+ 3 - 0
state/pystate.py

@@ -105,6 +105,9 @@ class PyState(State):
         else:
             return None, None
 
+    def is_edge(self, elem: Element) -> bool:
+        return elem in self.edges
+
     def read_dict(self, elem: Element, value: Any) -> Optional[Element]:
         e = self.read_dict_edge(elem, value)
         if e == None:

+ 4 - 1
transformation/ramify.py

@@ -117,7 +117,10 @@ def ramify(state: State, model: UUID) -> UUID:
         #   - max-card: same as original
         upper_card = find_cardinality(class_node, class_upper_card_node)
         print('creating class', class_name, "with card 0 ..", upper_card)
-        ramified_scd.create_class(class_name, abstract=None, max_c=upper_card)
+        ramified_class = ramified_scd.create_class(class_name, abstract=None, max_c=upper_card)
+
+        # traceability link
+        bottom.create_edge(ramified_class, class_node, "RAMifies")
 
         for (attr_name, attr_type) in get_attributes(class_node):
             print('  creating attribute', attr_name, "with type String")

+ 10 - 0
util/timer.py

@@ -0,0 +1,10 @@
+import time
+
+class Timer:
+    def __init__(self, text):
+        self.text = text
+    def __enter__(self):
+        self.start_time = time.perf_counter_ns()
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.end_time = time.perf_counter_ns()
+        print(self.text, (self.end_time - self.start_time)/1000000, "ms")