muMLE/transformation/matcher/mvs_adapter.py

313 lines
12 KiB
Python

from state.base import State
from uuid import UUID
from services.bottom.V0 import Bottom
from services.scd import SCD
from services.od import OD
from transformation.matcher.matcher import Graph, Edge, Vertex, MatcherVF2
from transformation import ramify
import itertools
import re
import functools
from util.timer import Timer
from services.primitives.integer_type import Integer
class _is_edge:
def __repr__(self):
return "EDGE"
def to_json(self):
return "EDGE"
# just a unique symbol that is only equal to itself
IS_EDGE = _is_edge()
class _is_modelref:
def __repr__(self):
return "REF"
def to_json(self):
return "REF"
IS_MODELREF = _is_modelref()
# 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:]})"
class NamedNode(Vertex):
def __init__(self, value, name):
super().__init__(value)
# the name of the node in the context of the model
# the matcher by default ignores this value
self.name = name
# MVS-nodes become vertices
class MVSNode(NamedNode):
def __init__(self, value, node_id, name):
super().__init__(value, name)
# useful for debugging
self.node_id = node_id
def __repr__(self):
if self.value == None:
return f"N({self.name})"
if isinstance(self.value, str):
return f"N({self.name}=\"{self.value}\")"
return f"N({self.name}={self.value})"
# if isinstance(self.value, str):
# return f"N({self.name}=\"{self.value}\",{str(self.node_id)[-4:]})"
# return f"N({self.name}={self.value},{str(self.node_id)[-4:]})"
# MVS-edges become vertices.
class MVSEdge(NamedNode):
def __init__(self, node_id, name):
super().__init__(IS_EDGE, name)
# useful for debugging
self.node_id = node_id
def __repr__(self):
return f"E({self.name})"
# return f"E({self.name}{str(self.node_id)[-4:]})"
# dirty way of detecting whether a node is a ModelRef
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 diagram in MVS state to the pattern matcher graph type
# ModelRefs are flattened
def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""):
# with Timer("model_to_graph"):
od = OD(model, metamodel, state)
scd = SCD(model, state)
scd_mm = SCD(metamodel, state)
bottom = Bottom(state)
graph = Graph()
mvs_edges = []
modelrefs = {}
# constraints = {}
def to_vtx(el, name):
# print("name:", name)
if bottom.is_edge(el):
# if filter_constraint:
# try:
# supposed_obj = bottom.read_edge_source(el)
# slot_node = od.get_slot(supposed_obj, "constraint")
# if el == slot_node:
# # `el` is the constraint-slot
# constraints[supposed_obj] = el
# return
# except:
# pass
mvs_edges.append(el)
return MVSEdge(el, name)
# 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.
value = bottom.read_value(el)
if isinstance(value, str):
if UUID_REGEX.match(value) != None:
# side-effect
modelrefs[el] = (UUID(value), name)
return MVSNode(IS_MODELREF, el, name)
return MVSNode(value, el, name)
# MVS-Nodes become vertices
uuid_to_vtx = { node: to_vtx(node, prefix+key) for key in bottom.read_keys(model) for node in bottom.read_outgoing_elements(model, key) }
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_edge],
tgt=uuid_to_vtx[mvs_tgt],
label="tgt"))
for node, (ref_m, name) in modelrefs.items():
vtx = uuid_to_vtx[node]
# Get MM of ref'ed model
ref_mm, = bottom.read_outgoing_elements(node, "Morphism")
# print("modelref type node:", type_node)
# Recursively convert ref'ed model to graph
# ref_graph = model_to_graph(state, ref_m, ref_mm, prefix=name+'/')
vtx.modelref = (ref_m, ref_mm)
# We no longer flatten:
# # 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"))
def add_types(node):
vtx = uuid_to_vtx[node]
type_node, = bottom.read_outgoing_elements(node, "Morphism")
# Put the type straight into the Vertex-object
# The benefit is that our Vertex-matching callback can then be coded cleverly, look at the types first, resulting in better performance
vtx.typ = type_node
# The old approach (creating special vertices containing the types), commented out:
# 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)
# Add typing information for:
# - classes
# - attributes
# - associations
for class_name, class_node in scd_mm.get_classes().items():
objects = scd.get_typed_by(class_node)
# print("typed by:", class_name, objects)
for obj_name, obj_node in objects.items():
add_types(obj_node)
for attr_name, attr_node in scd_mm.get_attributes(class_name).items():
attrs = scd.get_typed_by(attr_node)
for slot_name, slot_node in attrs.items():
add_types(slot_node)
for assoc_name, assoc_node in scd_mm.get_associations().items():
objects = scd.get_typed_by(assoc_node)
# print("typed by:", assoc_name, objects)
for link_name, link_node in objects.items():
add_types(link_node)
return graph
def match_od(state, host_m, host_mm, pattern_m, pattern_mm):
# 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, host_od):
self.bottom = bottom
self.host_od = host_od
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):
if supposed_subtype == supposed_supertype:
# reflexive:
return True
inheritance_node, = self.bottom.read_outgoing_elements(self.scd_model, "Inheritance")
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 match_types(self, g_vtx_type, h_vtx_type):
# types only match with their supertypes
# we assume that 'RAMifies'-traceability links have been created between guest and host types
try:
g_vtx_original_type = ramify.get_original_type(self.bottom, g_vtx_type)
except:
return False
return self.is_subtype_of(h_vtx_type, g_vtx_original_type)
# Memoizing the result of comparison gives a huge performance boost!
# Especially `is_subtype_of` is very slow, and will be performed many times over on the same pair of nodes during the matching process.
# Assuming the model is not altered *during* matching, this is safe.
@functools.cache
def __call__(self, g_vtx, h_vtx):
# First check if the types match (if we have type-information)
if hasattr(g_vtx, 'typ'):
if not hasattr(h_vtx, 'typ'):
# if guest has a type, host must have a type
return False
return self.match_types(g_vtx.typ, h_vtx.typ)
if hasattr(g_vtx, 'modelref'):
if not hasattr(h_vtx, 'modelref'):
return False
g_ref_m, g_ref_mm = g_vtx.modelref
h_ref_m, h_ref_mm = h_vtx.modelref
nested_matches = [m for m in match_od(state, h_ref_m, h_ref_mm, g_ref_m, g_ref_mm)]
# print('nested_matches:', nested_matches)
if len(nested_matches) == 0:
return False
elif len(nested_matches) == 1:
return True
else:
raise Exception("We have a problem: there is more than 1 match in the nested models.")
# Then, match by value
if g_vtx.value == None:
return h_vtx.value == None
# mvs-edges (which are converted to vertices) only match with mvs-edges
if g_vtx.value == IS_EDGE:
return h_vtx.value == IS_EDGE
if h_vtx.value == IS_EDGE:
return False
if g_vtx.value == IS_MODELREF:
return h_vtx.value == IS_MODELREF
if h_vtx.value == IS_MODELREF:
return False
# # print(g_vtx.value, h_vtx.value)
# def get_slot(h_vtx, slot_name: str):
# slot_node = self.host_od.get_slot(h_vtx.node_id, slot_name)
# return slot_node
# def read_int(slot: UUID):
# i = Integer(slot, self.bottom.state)
# return i.read()
try:
return eval(g_vtx.value, {}, {
'v': h_vtx.value,
# 'get_slot': functools.partial(get_slot, h_vtx),
# 'read_int': read_int,
})
except Exception as e:
return False
# Convert to format understood by matching algorithm
host = model_to_graph(state, host_m, host_mm)
guest = model_to_graph(state, pattern_m, pattern_mm)
matcher = MatcherVF2(host, guest, RAMCompare(Bottom(state), OD(host_mm, host_m, state)))
for m in matcher.match():
# print("\nMATCH:\n", m)
# Convert mapping
name_mapping = {}
for guest_vtx, host_vtx in m.mapping_vtxs.items():
if isinstance(guest_vtx, NamedNode) and isinstance(host_vtx, NamedNode):
name_mapping[guest_vtx.name] = host_vtx.name
yield name_mapping