matcher.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. from api.cd import CDAPI
  2. from api.od import ODAPI, bind_api_readonly
  3. from util.eval import exec_then_eval
  4. from state.base import State
  5. from uuid import UUID
  6. from services.bottom.V0 import Bottom
  7. from services.scd import SCD
  8. from services import od as services_od
  9. from transformation.vf2 import Graph, Edge, Vertex, MatcherVF2
  10. from transformation import ramify
  11. import itertools
  12. import re
  13. import functools
  14. from util.timer import Timer, counted
  15. class _is_edge:
  16. def __repr__(self):
  17. return "EDGE"
  18. def to_json(self):
  19. return "EDGE"
  20. # just a unique symbol that is only equal to itself
  21. IS_EDGE = _is_edge()
  22. class _is_modelref:
  23. def __repr__(self):
  24. return "REF"
  25. def to_json(self):
  26. return "REF"
  27. IS_MODELREF = _is_modelref()
  28. # class IS_TYPE:
  29. # def __init__(self, type):
  30. # # mvs-node of the type
  31. # self.type = type
  32. # def __repr__(self):
  33. # return f"TYPE({str(self.type)[-4:]})"
  34. class NamedNode(Vertex):
  35. def __init__(self, value, name):
  36. super().__init__(value)
  37. # the name of the node in the context of the model
  38. # the matcher by default ignores this value
  39. self.name = name
  40. # MVS-nodes become vertices
  41. class MVSNode(NamedNode):
  42. def __init__(self, value, node_id, name):
  43. super().__init__(value, name)
  44. # useful for debugging
  45. self.node_id = node_id
  46. def __repr__(self):
  47. if self.value == None:
  48. return f"N({self.name})"
  49. if isinstance(self.value, str):
  50. return f"N({self.name}=\"{self.value}\")"
  51. return f"N({self.name}={self.value})"
  52. # if isinstance(self.value, str):
  53. # return f"N({self.name}=\"{self.value}\",{str(self.node_id)[-4:]})"
  54. # return f"N({self.name}={self.value},{str(self.node_id)[-4:]})"
  55. # MVS-edges become vertices.
  56. class MVSEdge(NamedNode):
  57. def __init__(self, node_id, name):
  58. super().__init__(IS_EDGE, name)
  59. # useful for debugging
  60. self.node_id = node_id
  61. def __repr__(self):
  62. return f"E({self.name})"
  63. # return f"E({self.name}{str(self.node_id)[-4:]})"
  64. # dirty way of detecting whether a node is a ModelRef
  65. 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]")
  66. # Converts an object diagram in MVS state to the pattern matcher graph type
  67. # ModelRefs are flattened
  68. def model_to_graph(state: State, model: UUID, metamodel: UUID,
  69. _filter=lambda node: True, prefix=""):
  70. # with Timer("model_to_graph"):
  71. od = services_od.OD(model, metamodel, state)
  72. scd = SCD(model, state)
  73. scd_mm = SCD(metamodel, state)
  74. bottom = Bottom(state)
  75. graph = Graph()
  76. mvs_edges = []
  77. modelrefs = {}
  78. names = {}
  79. def to_vtx(el, name):
  80. # print("name:", name)
  81. if bottom.is_edge(el):
  82. mvs_edges.append(el)
  83. edge = MVSEdge(el, name)
  84. names[name] = edge
  85. return edge
  86. # 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.
  87. value = bottom.read_value(el)
  88. if isinstance(value, str):
  89. if UUID_REGEX.match(value) != None:
  90. # side-effect
  91. modelrefs[el] = (UUID(value), name)
  92. return MVSNode(IS_MODELREF, el, name)
  93. node = MVSNode(value, el, name)
  94. names[name] = node
  95. return node
  96. # Objects and Links become vertices
  97. 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) if _filter(node) }
  98. graph.vtxs = [ vtx for vtx in uuid_to_vtx.values() ]
  99. # For every Link, two edges are created (for src and tgt)
  100. for mvs_edge in mvs_edges:
  101. mvs_src = bottom.read_edge_source(mvs_edge)
  102. if mvs_src in uuid_to_vtx:
  103. graph.edges.append(Edge(
  104. src=uuid_to_vtx[mvs_src],
  105. tgt=uuid_to_vtx[mvs_edge],
  106. label="outgoing"))
  107. mvs_tgt = bottom.read_edge_target(mvs_edge)
  108. if mvs_tgt in uuid_to_vtx:
  109. graph.edges.append(Edge(
  110. src=uuid_to_vtx[mvs_edge],
  111. tgt=uuid_to_vtx[mvs_tgt],
  112. label="tgt"))
  113. for node, (ref_m, name) in modelrefs.items():
  114. vtx = uuid_to_vtx[node]
  115. # Get MM of ref'ed model
  116. ref_mm, = bottom.read_outgoing_elements(node, "Morphism")
  117. vtx.modelref = (ref_m, ref_mm)
  118. def add_types(node):
  119. vtx = uuid_to_vtx[node]
  120. type_node, = bottom.read_outgoing_elements(node, "Morphism")
  121. # Put the type straight into the Vertex-object
  122. # The benefit is that our Vertex-matching callback can then be coded cleverly, look at the types first, resulting in better performance
  123. vtx.typ = type_node
  124. # Add typing information for:
  125. # - classes
  126. # - attributes
  127. # - associations
  128. for class_name, class_node in scd_mm.get_classes().items():
  129. objects = scd.get_typed_by(class_node)
  130. for obj_name, obj_node in objects.items():
  131. if _filter(obj_node):
  132. add_types(obj_node)
  133. for attr_name, attr_node in scd_mm.get_attributes(class_name).items():
  134. attrs = scd.get_typed_by(attr_node)
  135. for slot_name, slot_node in attrs.items():
  136. if _filter(slot_node):
  137. add_types(slot_node)
  138. for assoc_name, assoc_node in scd_mm.get_associations().items():
  139. objects = scd.get_typed_by(assoc_node)
  140. for link_name, link_node in objects.items():
  141. if _filter(link_node):
  142. add_types(link_node)
  143. return names, graph
  144. class _No_Matched(Exception):
  145. pass
  146. def _cannot_call_matched(_):
  147. raise _No_Matched()
  148. # This function returns a Generator of matches.
  149. # The idea is that the user can iterate over the match set, lazily generating it: if only interested in the first match, the entire match set doesn't have to be generated.
  150. def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
  151. bottom = Bottom(state)
  152. # compute subtype relations and such:
  153. cdapi = CDAPI(state, host_mm)
  154. odapi = ODAPI(state, host_m, host_mm)
  155. pattern_odapi = ODAPI(state, pattern_m, pattern_mm)
  156. pattern_mm_odapi = ODAPI(state, pattern_mm, cdapi.mm)
  157. # 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)
  158. class RAMCompare:
  159. def __init__(self, bottom, host_od):
  160. self.bottom = bottom
  161. self.host_od = host_od
  162. type_model_id = bottom.state.read_dict(bottom.state.read_root(), "SCD")
  163. self.scd_model = UUID(bottom.state.read_value(type_model_id))
  164. # constraints need to be checked at the very end, after a complete match is established, because constraint code may refer to matched elements by their name
  165. self.conditions_to_check = {}
  166. def match_types(self, g_vtx_type, h_vtx_type):
  167. # types only match with their supertypes
  168. # we assume that 'RAMifies'-traceability links have been created between guest and host types
  169. try:
  170. g_vtx_unramified_type = ramify.get_original_type(self.bottom, g_vtx_type)
  171. except:
  172. return False
  173. try:
  174. host_type_name = cdapi.type_model_names[h_vtx_type]
  175. guest_type_name_unramified = cdapi.type_model_names[g_vtx_unramified_type]
  176. except KeyError:
  177. return False
  178. types_ok = cdapi.is_subtype(
  179. super_type_name=guest_type_name_unramified,
  180. sub_type_name=host_type_name)
  181. return types_ok
  182. # Memoizing the result of comparison gives a huge performance boost!
  183. # Especially `is_subtype_of` is very slow, and will be performed many times over on the same pair of nodes during the matching process.
  184. # Assuming the model is not altered *during* matching, this is safe.
  185. @functools.cache
  186. def __call__(self, g_vtx, h_vtx):
  187. # First check if the types match (if we have type-information)
  188. if hasattr(g_vtx, 'typ'):
  189. if not hasattr(h_vtx, 'typ'):
  190. # if guest has a type, host must have a type
  191. return False
  192. if not self.match_types(g_vtx.typ, h_vtx.typ):
  193. return False
  194. if hasattr(g_vtx, 'modelref'):
  195. if not hasattr(h_vtx, 'modelref'):
  196. return False
  197. python_code = services_od.read_primitive_value(self.bottom, g_vtx.node_id, pattern_mm)[0]
  198. try:
  199. # Try to execute code, but the likelyhood of failing is high:
  200. # - the `matched` API function is not yet available
  201. # - incompatible slots may be matched (it is only when their AttributeLinks are matched, that we know the types will be compatible)
  202. with Timer(f'EVAL condition {g_vtx.name}'):
  203. ok = exec_then_eval(python_code,
  204. _globals={
  205. **bind_api_readonly(odapi),
  206. 'matched': _cannot_call_matched,
  207. },
  208. _locals={'this': h_vtx.node_id})
  209. self.conditions_to_check.pop(g_vtx.name, None)
  210. return ok
  211. except:
  212. self.conditions_to_check[g_vtx.name] = python_code
  213. return True # to be determined later, if it's actually a match
  214. if g_vtx.value == None:
  215. return h_vtx.value == None
  216. # mvs-edges (which are converted to vertices) only match with mvs-edges
  217. if g_vtx.value == IS_EDGE:
  218. return h_vtx.value == IS_EDGE
  219. if h_vtx.value == IS_EDGE:
  220. return False
  221. if g_vtx.value == IS_MODELREF:
  222. return h_vtx.value == IS_MODELREF
  223. if h_vtx.value == IS_MODELREF:
  224. return False
  225. return True
  226. # Convert to format understood by matching algorithm
  227. h_names, host = model_to_graph(state, host_m, host_mm)
  228. # Only match matchable pattern elements
  229. # E.g., the 'condition'-attribute that is added to every class, cannot be matched with anything
  230. def is_matchable(pattern_el):
  231. pattern_el_name = pattern_odapi.get_name(pattern_el)
  232. if pattern_odapi.get_type_name(pattern_el) == "GlobalCondition":
  233. return False
  234. # Super-cheap and unreliable way of filtering out the 'condition'-attribute, added to every class:
  235. return ((not pattern_el_name.endswith("condition")
  236. # as an extra safety measure, if the user defined her own 'condition' attribute, RAMification turned this into 'RAM_condition', and we can detect this
  237. # of course this breaks if the class name already ended with 'RAM', but let's hope that never happens
  238. # also, we are assuming the default "RAM_" prefix is used, but the user can change this...
  239. or pattern_el_name.endswith("RAM_condition"))
  240. and (
  241. not pattern_el_name.endswith("name")
  242. or pattern_el_name.endswith("RAM_name") # same thing here as with the condition, explained above.
  243. ))
  244. g_names, guest = model_to_graph(state, pattern_m, pattern_mm,
  245. _filter=is_matchable)
  246. # precompute the candidates for every guest vertex:
  247. guest_to_host_candidate_vtxs = {}
  248. vtxs_of_host_type = {}
  249. for g_vtx in guest.vtxs:
  250. object_node = g_vtx.node_id
  251. if hasattr(g_vtx, 'typ'):
  252. orig_class_node = ramify.get_original_type(bottom, g_vtx.typ)
  253. orig_class_name = odapi.get_name(orig_class_node)
  254. if orig_class_name in vtxs_of_host_type:
  255. cands = vtxs_of_host_type[orig_class_name]
  256. else:
  257. cands = vtxs_of_host_type[orig_class_name] = len(odapi.get_all_instances(orig_class_name, include_subtypes=True))
  258. else:
  259. cands = len(host.vtxs)
  260. guest_to_host_candidate_vtxs[g_vtx] = cands
  261. # print(guest_to_host_candidate_vtxs)
  262. # transform 'pivot' into something VF2 understands
  263. graph_pivot = {
  264. g_names[guest_name] : h_names[host_name]
  265. for guest_name, host_name in pivot.items()
  266. if guest_name in g_names
  267. }
  268. obj_conditions = []
  269. for class_name, class_node in pattern_mm_odapi.get_all_instances("Class"):
  270. for obj_name, obj_node in pattern_odapi.get_all_instances(class_name):
  271. python_code = pattern_odapi.get_slot_value_default(obj_node, "condition", 'True')
  272. if class_name == "GlobalCondition":
  273. obj_conditions.append((python_code, None))
  274. else:
  275. obj_conditions.append((python_code, obj_name))
  276. def check_conditions(name_mapping):
  277. def check(python_code: str, loc):
  278. return exec_then_eval(python_code,
  279. _globals={
  280. **bind_api_readonly(odapi),
  281. 'matched': lambda name: bottom.read_outgoing_elements(host_m, name_mapping[name])[0],
  282. },
  283. _locals=loc)
  284. # Attribute conditions
  285. for pattern_name, host_name in name_mapping.items():
  286. try:
  287. python_code = compare.conditions_to_check[pattern_name]
  288. except KeyError:
  289. continue
  290. host_node = odapi.get(host_name)
  291. with Timer(f'EVAL condition {pattern_name}'):
  292. if not check(python_code, {'this': host_node}):
  293. return False
  294. for python_code, pattern_el_name in obj_conditions:
  295. if pattern_el_name == None:
  296. # GlobalCondition
  297. with Timer(f'EVAL all global conditions'):
  298. if not check(python_code, {}):
  299. return False
  300. else:
  301. # object-lvl condition
  302. host_el_name = name_mapping[pattern_el_name]
  303. host_node = odapi.get(host_el_name)
  304. with Timer(f'EVAL local condition {pattern_el_name}'):
  305. if not check(python_code, {'this': host_node}):
  306. return False
  307. return True
  308. compare = RAMCompare(bottom, services_od.OD(host_mm, host_m, state))
  309. matcher = MatcherVF2(host, guest, compare, guest_to_host_candidate_vtxs)
  310. for m in matcher.match(graph_pivot):
  311. # Convert mapping
  312. name_mapping = {}
  313. for guest_vtx, host_vtx in m.mapping_vtxs.items():
  314. if isinstance(guest_vtx, NamedNode) and isinstance(host_vtx, NamedNode):
  315. name_mapping[guest_vtx.name] = host_vtx.name
  316. if not check_conditions(name_mapping):
  317. continue # not a match after all...
  318. yield name_mapping