From 4160a8953e9c989bc5e3d6ff0e7c1b8670031449 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Fri, 6 Sep 2024 21:10:23 +0200 Subject: [PATCH] Tweak matcher (compute connected components in advance). Simple pattern matching with RAMification (incl. Python expressions) seems to work. --- bootstrap/scd.py | 9 ++ experiments/exp_scd.py | 42 ++++++++- pattern_matching/generator.py | 30 ++++++ pattern_matching/graph.py | 5 +- pattern_matching/main.py | 35 ++----- pattern_matching/matcher.py | 143 +++++++++++++++++++++-------- pattern_matching/mvs_adapter.py | 156 ++++++++++++++++++++++++++++++++ services/bottom/V0.py | 3 + services/od.py | 15 ++- services/scd.py | 2 + state/pystate.py | 3 + transformation/ramify.py | 5 +- util/timer.py | 10 ++ 13 files changed, 388 insertions(+), 70 deletions(-) create mode 100644 pattern_matching/mvs_adapter.py create mode 100644 util/timer.py diff --git a/bootstrap/scd.py b/bootstrap/scd.py index 30a42f3..3d85e4a 100644 --- a/bootstrap/scd.py +++ b/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) diff --git a/experiments/exp_scd.py b/experiments/exp_scd.py index 3e2a110..6e40345 100644 --- a/experiments/exp_scd.py +++ b/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() diff --git a/pattern_matching/generator.py b/pattern_matching/generator.py index 53e4b78..2788f5c 100644 --- a/pattern_matching/generator.py +++ b/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) + diff --git a/pattern_matching/graph.py b/pattern_matching/graph.py index de98fe7..66ac366 100644 --- a/pattern_matching/graph.py +++ b/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): diff --git a/pattern_matching/main.py b/pattern_matching/main.py index 534ef3f..056b1b8 100644 --- a/pattern_matching/main.py +++ b/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 + random.seed(0) - 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)] + graph, pattern = get_random_host_and_guest( + nr_vtxs = 10, + nr_vtx_types = 0, + nr_edges = 20, + nr_edge_types = 0, + ) - # 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) - - graph = gg.getRandomGraph() - - 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 diff --git a/pattern_matching/matcher.py b/pattern_matching/matcher.py index cdcdc53..873b76a 100644 --- a/pattern_matching/matcher.py +++ b/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) - if g_candidate_edge in state.mapping_edges: - print_debug(" "*indent, " skip, guest edge already matched") - continue # skip already matched guest 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) - for h_candidate_edge in getattr(h_vtx, direction): - print_debug(" "*indent, 'h_candidate_edge:', h_candidate_edge) + # g_to_skip_vtxs.add(g_candidate_vtx) + if g_candidate_edge in state.mapping_edges: + print_debug(" skip, guest edge already matched") + continue # skip already matched guest 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') diff --git a/pattern_matching/mvs_adapter.py b/pattern_matching/mvs_adapter.py new file mode 100644 index 0000000..d5b3118 --- /dev/null +++ b/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}) diff --git a/services/bottom/V0.py b/services/bottom/V0.py index 1fc4293..4d1bf86 100644 --- a/services/bottom/V0.py +++ b/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 diff --git a/services/od.py b/services/od.py index 520a120..39abe3b 100644 --- a/services/od.py +++ b/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) diff --git a/services/scd.py b/services/scd.py index 75efd6b..ec4f8c2 100644 --- a/services/scd.py +++ b/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): diff --git a/state/pystate.py b/state/pystate.py index 6dc15a5..5bfc398 100644 --- a/state/pystate.py +++ b/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: diff --git a/transformation/ramify.py b/transformation/ramify.py index 5db0462..933f50b 100644 --- a/transformation/ramify.py +++ b/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") diff --git a/util/timer.py b/util/timer.py new file mode 100644 index 0000000..6eed38d --- /dev/null +++ b/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")