From 9eea3618d0a6170f94b0ee504770e4621e03b94f Mon Sep 17 00:00:00 2001 From: robbe Date: Fri, 27 Jun 2025 12:15:19 +0200 Subject: [PATCH 1/8] cleanup the od api interface --- {examples/schedule => api}/__init__.py | 0 api/od.py | 81 ++++++++++++-------------- api/od_stub.pyi | 9 +++ api/od_stub_readonly.pyi | 18 ++++++ transformation/merger.py | 2 +- transformation/rewriter.py | 6 +- 6 files changed, 69 insertions(+), 47 deletions(-) rename {examples/schedule => api}/__init__.py (100%) create mode 100644 api/od_stub.pyi create mode 100644 api/od_stub_readonly.pyi diff --git a/examples/schedule/__init__.py b/api/__init__.py similarity index 100% rename from examples/schedule/__init__.py rename to api/__init__.py diff --git a/api/od.py b/api/od.py index bd0e2b7..cfaa049 100644 --- a/api/od.py +++ b/api/od.py @@ -6,8 +6,7 @@ from services.primitives.integer_type import Integer from services.primitives.string_type import String from services.primitives.actioncode_type import ActionCode from uuid import UUID -from typing import Optional -from util.timer import Timer +from typing import Optional, Any NEXT_ID = 0 @@ -42,10 +41,10 @@ class ODAPI: self.create_string_value = self.od.create_string_value self.create_actioncode_value = self.od.create_actioncode_value - self.__recompute_mappings() + self.recompute_mappings() # Called after every change - makes querying faster but modifying slower - def __recompute_mappings(self): + def recompute_mappings(self): self.m_obj_to_name = build_name_mapping(self.state, self.m) self.mm_obj_to_name = build_name_mapping(self.state, self.mm) self.type_to_objs = { type_name : set() for type_name in self.bottom.read_keys(self.mm)} @@ -60,25 +59,33 @@ class ODAPI: def get_value(self, obj: UUID): return od.read_primitive_value(self.bottom, obj, self.mm)[0] - def get_target(self, link: UUID): + def get_target(self, link: UUID) -> UUID: return self.bottom.read_edge_target(link) - def get_source(self, link: UUID): + def get_source(self, link: UUID) -> UUID: return self.bottom.read_edge_source(link) - def get_slot(self, obj: UUID, attr_name: str): + def get_slot(self, obj: UUID, attr_name: str) -> UUID: slot = self.od.get_slot(obj, attr_name) if slot == None: raise NoSuchSlotException(f"Object '{self.m_obj_to_name[obj]}' has no slot '{attr_name}'") return slot - def get_slot_link(self, obj: UUID, attr_name: str): + def get_slot_link(self, obj: UUID, attr_name: str) -> UUID: return self.od.get_slot_link(obj, attr_name) # Parameter 'include_subtypes': whether to include subtypes of the given association - def get_outgoing(self, obj: UUID, assoc_name: str, include_subtypes=True): + def get_outgoing(self, obj: UUID, assoc_name: str, include_subtypes=True) -> list[UUID]: outgoing = self.bottom.read_outgoing_edges(obj) - result = [] + return self.filter_edges_by_type(outgoing, assoc_name, include_subtypes) + + # Parameter 'include_subtypes': whether to include subtypes of the given association + def get_incoming(self, obj: UUID, assoc_name: str, include_subtypes=True): + incoming = self.bottom.read_incoming_edges(obj) + return self.filter_edges_by_type(incoming, assoc_name, include_subtypes) + + def filter_edges_by_type(self, outgoing: list[UUID], assoc_name: str, include_subtypes=True) -> list[UUID]: + result: list[UUID] = [] for o in outgoing: try: type_of_outgoing_link = self.get_type_name(o) @@ -89,23 +96,8 @@ class ODAPI: result.append(o) return result - - # Parameter 'include_subtypes': whether to include subtypes of the given association - def get_incoming(self, obj: UUID, assoc_name: str, include_subtypes=True): - incoming = self.bottom.read_incoming_edges(obj) - result = [] - for i in incoming: - try: - type_of_incoming_link = self.get_type_name(i) - except: - continue # OK, not all edges are typed - if (include_subtypes and self.cdapi.is_subtype(super_type_name=assoc_name, sub_type_name=type_of_incoming_link) - or not include_subtypes and type_of_incoming_link == assoc_name): - result.append(i) - return result - # Returns list of tuples (name, obj) - def get_all_instances(self, type_name: str, include_subtypes=True): + def get_all_instances(self, type_name: str, include_subtypes=True) -> list[UUID]: if include_subtypes: all_types = self.cdapi.transitive_sub_types[type_name] else: @@ -127,7 +119,7 @@ class ODAPI: else: raise Exception(f"Couldn't find name of {obj} - are you sure it exists in the (meta-)model?") - def get(self, name: str): + def get(self, name: str) -> UUID: results = self.bottom.read_outgoing_elements(self.m, name) if len(results) == 1: return results[0] @@ -136,10 +128,10 @@ class ODAPI: else: raise Exception(f"No such element in model: '{name}'") - def get_type_name(self, obj: UUID): + def get_type_name(self, obj: UUID) -> str: return self.get_name(self.get_type(obj)) - def is_instance(self, obj: UUID, type_name: str, include_subtypes=True): + def is_instance(self, obj: UUID, type_name: str, include_subtypes=True) -> bool: typ = self.cdapi.get_type(type_name) types = set(typ) if not include_subtypes else self.cdapi.transitive_sub_types[type_name] for type_of_obj in self.bottom.read_outgoing_elements(obj, "Morphism"): @@ -147,18 +139,21 @@ class ODAPI: return True return False - def delete(self, obj: UUID): + def delete(self, obj: UUID) -> None: self.bottom.delete_element(obj) - self.__recompute_mappings() + self.recompute_mappings() # Does the the object have the given attribute? - def has_slot(self, obj: UUID, attr_name: str): - return self.od.get_slot_link(obj, attr_name) != None + def has_slot(self, obj: UUID, attr_name: str) -> bool: + class_name = self.get_name(self.get_type(obj)) + if self.od.get_attr_link_name(class_name, attr_name) is None: + return False + return self.od.get_slot_link(obj, attr_name) is not None def get_slots(self, obj: UUID) -> list[str]: return [attr_name for attr_name, _ in self.od.get_slots(obj)] - def get_slot_value(self, obj: UUID, attr_name: str): + def get_slot_value(self, obj: UUID, attr_name: str) -> Any: slot = self.get_slot(obj, attr_name) return self.get_value(slot) @@ -171,14 +166,14 @@ class ODAPI: # Returns the given default value if the slot does not exist on the object. # The attribute must exist in the object's class, or an exception will be thrown. # The slot may not exist however, if the attribute is defined as 'optional' in the class. - def get_slot_value_default(self, obj: UUID, attr_name: str, default: any): + def get_slot_value_default(self, obj: UUID, attr_name: str, default: any) -> any: try: return self.get_slot_value(obj, attr_name) except NoSuchSlotException: return default # create or update slot value - def set_slot_value(self, obj: UUID, attr_name: str, new_value: any, is_code=False): + def set_slot_value(self, obj: UUID, attr_name: str, new_value: any, is_code=False) -> None: obj_name = self.get_name(obj) link_name = f"{obj_name}_{attr_name}" @@ -193,7 +188,7 @@ class ODAPI: new_target = self.create_primitive_value(target_name, new_value, is_code) slot_type = self.cdapi.find_attribute_type(self.get_type_name(obj), attr_name) new_link = self.od._create_link(link_name, slot_type, obj, new_target) - self.__recompute_mappings() + self.recompute_mappings() def create_primitive_value(self, name: str, value: any, is_code=False): # watch out: in Python, 'bool' is subtype of 'int' @@ -209,7 +204,7 @@ class ODAPI: tgt = self.create_string_value(name, value) else: raise Exception("Unimplemented type "+value) - self.__recompute_mappings() + self.recompute_mappings() return tgt def overwrite_primitive_value(self, name: str, value: any, is_code=False): @@ -228,7 +223,7 @@ class ODAPI: else: raise Exception("Unimplemented type "+value) - def create_link(self, link_name: Optional[str], assoc_name: str, src: UUID, tgt: UUID): + def create_link(self, link_name: Optional[str], assoc_name: str, src: UUID, tgt: UUID) -> UUID: global NEXT_ID types = self.bottom.read_outgoing_elements(self.mm, assoc_name) if len(types) == 0: @@ -240,12 +235,12 @@ class ODAPI: link_name = f"__{assoc_name}{NEXT_ID}" NEXT_ID += 1 link_id = self.od._create_link(link_name, typ, src, tgt) - self.__recompute_mappings() + self.recompute_mappings() return link_id - def create_object(self, object_name: Optional[str], class_name: str): + def create_object(self, object_name: Optional[str], class_name: str) -> UUID: obj = self.od.create_object(object_name, class_name) - self.__recompute_mappings() + self.recompute_mappings() return obj # internal use @@ -284,6 +279,6 @@ def bind_api(odapi): 'create_object': odapi.create_object, 'create_link': odapi.create_link, 'delete': odapi.delete, - 'set_slot_value': odapi.set_slot_value, + 'set_slot_value': odapi.set_slot_value } return funcs diff --git a/api/od_stub.pyi b/api/od_stub.pyi new file mode 100644 index 0000000..563e3e0 --- /dev/null +++ b/api/od_stub.pyi @@ -0,0 +1,9 @@ +from typing import Optional +from uuid import UUID + +from od_stub_readonly import * + +def create_object(object_name: Optional[str], class_name: str) -> UUID: ... +def create_link(link_name: Optional[str], assoc_name: str, src: UUID, tgt: UUID) -> UUID: ... +def delete(obj: UUID) -> None: ... +def set_slot_value(obj: UUID, attr_name: str, new_value: any, is_code=False) -> None: ... \ No newline at end of file diff --git a/api/od_stub_readonly.pyi b/api/od_stub_readonly.pyi new file mode 100644 index 0000000..89bbc4c --- /dev/null +++ b/api/od_stub_readonly.pyi @@ -0,0 +1,18 @@ +from typing import Any +from uuid import UUID + +def get(name: str) -> UUID: ... +def get_value(obj: UUID) -> Any: ... +def get_target(link: UUID) -> UUID: ... +def get_source(link: UUID) -> UUID: ... +def get_slot(obj: UUID, attr_name: str) -> UUID: ... +def get_slots(obj: UUID) -> list[str]: ... +def get_slot_value(obj: UUID, attr_name: str) -> Any: ... +def get_slot_value_default(obj: UUID, attr_name: str, default: any) -> Any: ... +def get_all_instances(type_name: str, include_subtypes=True) -> list[UUID]: ... +def get_name(obj: UUID) -> str: ... +def get_type_name(obj: UUID) -> str: ... +def get_outgoing(obj: UUID, assoc_name: str, include_subtypes=True) -> list[UUID]: ... +def get_incoming(obj: UUID, assoc_name: str, include_subtypes: object = True) -> list[UUID]: ... +def has_slot(obj: UUID, attr_name: str) -> bool: ... +def is_instance(obj: UUID, type_name: str, include_subtypes=True) -> bool: ... diff --git a/transformation/merger.py b/transformation/merger.py index 6caecb1..3e38a79 100644 --- a/transformation/merger.py +++ b/transformation/merger.py @@ -52,7 +52,7 @@ def merge_models(state, mm, models: list[UUID]): model = state.read_value(obj) scd = SCD(merged, state) created_obj = scd.create_model_ref(prefixed_obj_name, model) - merged_odapi._ODAPI__recompute_mappings() # dirty!! + merged_odapi.recompute_mappings() # dirty!! else: # create node or edge if state.is_edge(obj): diff --git a/transformation/rewriter.py b/transformation/rewriter.py index 9c29252..8a6938b 100644 --- a/transformation/rewriter.py +++ b/transformation/rewriter.py @@ -149,13 +149,13 @@ def rewrite(state, if od.is_typed_by(bottom, rhs_type, class_type): obj_name = first_available_name(suggested_name) host_od._create_object(obj_name, host_type) - host_odapi._ODAPI__recompute_mappings() + host_odapi.recompute_mappings() rhs_match[rhs_name] = obj_name elif od.is_typed_by(bottom, rhs_type, assoc_type): _, _, host_src, host_tgt = get_src_tgt() link_name = first_available_name(suggested_name) host_od._create_link(link_name, host_type, host_src, host_tgt) - host_odapi._ODAPI__recompute_mappings() + host_odapi.recompute_mappings() rhs_match[rhs_name] = link_name elif od.is_typed_by(bottom, rhs_type, attr_link_type): host_src_name, _, host_src, host_tgt = get_src_tgt() @@ -163,7 +163,7 @@ def rewrite(state, host_attr_name = host_mm_odapi.get_slot_value(host_attr_link, "name") link_name = f"{host_src_name}_{host_attr_name}" # must follow naming convention here host_od._create_link(link_name, host_type, host_src, host_tgt) - host_odapi._ODAPI__recompute_mappings() + host_odapi.recompute_mappings() rhs_match[rhs_name] = link_name elif rhs_type == rhs_mm_odapi.get("ActionCode"): # If we encounter ActionCode in our RHS, we assume that the code computes the value of an attribute... From 58366fa9bb7a8a1b33af042bf062751dec2bc9fa Mon Sep 17 00:00:00 2001 From: robbe Date: Fri, 27 Jun 2025 12:17:10 +0200 Subject: [PATCH 2/8] add Action code to the cd parser. --- concrete_syntax/textual_cd/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete_syntax/textual_cd/parser.py b/concrete_syntax/textual_cd/parser.py index 8b8ebd7..4b9e1c7 100644 --- a/concrete_syntax/textual_cd/parser.py +++ b/concrete_syntax/textual_cd/parser.py @@ -75,7 +75,7 @@ def parse_cd(state, m_text): primitive_types = { type_name : UUID(state.read_value(state.read_dict(state.read_root(), type_name))) - for type_name in ["Integer", "String", "Boolean"] + for type_name in ["Integer", "String", "Boolean", "ActionCode"] } class T(TBase): From e4ea9a0410322193652be6c5d668e0631631bffa Mon Sep 17 00:00:00 2001 From: robbe Date: Fri, 27 Jun 2025 12:17:27 +0200 Subject: [PATCH 3/8] Added ';' required after a global constraint for concistency --- concrete_syntax/textual_cd/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete_syntax/textual_cd/parser.py b/concrete_syntax/textual_cd/parser.py index 4b9e1c7..352ef62 100644 --- a/concrete_syntax/textual_cd/parser.py +++ b/concrete_syntax/textual_cd/parser.py @@ -40,7 +40,7 @@ attrs: attr* constraint: CODE | INDENTED_CODE -class_: [ABSTRACT] "class" IDENTIFIER [multiplicity] ["(" superclasses ")"] ["{" attrs [constraint] "}"] +class_: [ABSTRACT] "class" IDENTIFIER [multiplicity] ["(" superclasses ")"] ["{" attrs [constraint ";"] "}"] association: "association" IDENTIFIER [multiplicity] IDENTIFIER "->" IDENTIFIER [multiplicity] ["{" attrs [constraint] "}"] From ec42f74960d06eeb1848e2f1683ebf35ec06e59c Mon Sep 17 00:00:00 2001 From: robbe Date: Fri, 27 Jun 2025 12:20:00 +0200 Subject: [PATCH 4/8] Added an eval_context_decorator to allow user defined functions in rules --- framework/conformance.py | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/framework/conformance.py b/framework/conformance.py index df5a4bf..d3e71ac 100644 --- a/framework/conformance.py +++ b/framework/conformance.py @@ -15,6 +15,30 @@ from api.od import ODAPI, bind_api_readonly import functools +def eval_context_decorator(func): + """ + Used to mark functions that can be called inside the evaluation context. + Base functions are mapped into the function, as well as the evaluation context. + This happens at runtime so typechecking will not be happy. + Important: Using the same name in the evaluation context as the function name + will lead to naming conflicts with the function as priority, resulting in missing argument errors. + + from typing import TYPE_CHECKING + if TYPE_CHECKING: + from api.od_stub import * + ... + + Use this to make partially fix the typechecking. + Optionally, define a stub for your own evaluation context and include it. + """ + def wrapper(*args, api_context, eval_context, **kwargs): + for key, value in api_context.items(): + func.__globals__[key] = value + for key, value in eval_context.items(): + func.__globals__[key] = value + return func(*args, **kwargs) + return wrapper + def render_conformance_check_result(error_list): if len(error_list) == 0: return "CONFORM" @@ -25,7 +49,7 @@ def render_conformance_check_result(error_list): class Conformance: # Parameter 'constraint_check_subtypes': whether to check local type-level constraints also on subtypes. - def __init__(self, state: State, model: UUID, type_model: UUID, constraint_check_subtypes=True): + def __init__(self, state: State, model: UUID, type_model: UUID, constraint_check_subtypes=True, *, eval_context = None): self.state = state self.bottom = Bottom(state) self.model = model @@ -51,6 +75,9 @@ class Conformance: self.structures = {} self.candidates = {} + # add user defined functions to constraints + self.eval_context = eval_context if eval_context else {} + def check_nominal(self, *, log=False): """ @@ -248,6 +275,13 @@ class Conformance: raise Exception(f"{description} evaluation result should be boolean or string! Instead got {result}") # local constraints + _api_context = bind_api_readonly(self.odapi) + _global_binds = {**_api_context} + _eval_context = {**self.eval_context} + for key, code in _eval_context.items(): + _f = functools.partial(code, **{"api_context" :_api_context, "eval_context":_eval_context}) + _global_binds[key] = _f + _eval_context[key] = _f for type_name in self.bottom.read_keys(self.type_model): code = get_code(type_name) if code != None: @@ -256,7 +290,7 @@ class Conformance: description = f"Local constraint of \"{type_name}\" in \"{obj_name}\"" # print(description) try: - result = exec_then_eval(code, _globals=bind_api_readonly(self.odapi), _locals={'this': obj_id}) # may raise + result = exec_then_eval(code, _globals=_global_binds, _locals={'this': obj_id}) # may raise check_result(result, description) except: errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}") @@ -278,7 +312,7 @@ class Conformance: if code != None: description = f"Global constraint \"{tm_name}\"" try: - result = exec_then_eval(code, _globals=bind_api_readonly(self.odapi)) # may raise + result = exec_then_eval(code, _globals=_global_binds) # may raise check_result(result, description) except: errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}") From ebfd85a666ab15ebe3df9f36d89e063d7e4859bd Mon Sep 17 00:00:00 2001 From: robbe Date: Fri, 27 Jun 2025 12:21:41 +0200 Subject: [PATCH 5/8] A fully working version of the scheduling language with added examples --- examples/geraniums/geraniums_renderer.j2 | 34 + examples/geraniums/metamodels/mm.od | 9 + examples/geraniums/models/eval_context.py | 44 + examples/geraniums/models/example1.od | 17 + examples/geraniums/models/example2.od | 47 + examples/geraniums/renderer.py | 45 + examples/geraniums/rules/cracked_pots.od | 3 + examples/geraniums/rules/create_pot.od | 3 + .../rules/flowering_flowers_in_pot.od | 7 + .../geraniums/rules/repot_flower_in_pot.od | 8 + examples/geraniums/runner.py | 48 + examples/geraniums/schedules/schedule.drawio | 645 +++++++++ examples/geraniums/schedules/schedule.od | 0 examples/petrinet/models/m_example_simple.od | 13 +- .../models/m_example_simple_rt_initial.od | 18 + .../petrinet/models/rules/all_incoming.od | 13 + .../petrinet/models/rules/all_incomming.od | 13 + .../petrinet/models/rules/all_outgoing.od | 13 + .../models/rules/increase_outgoing.od | 13 + .../rules/input_without_token.od} | 0 examples/petrinet/models/rules/places.od | 3 + .../petrinet/models/rules/reduce_incoming.od | 13 + .../petrinet/models/rules/reduce_incomming.od | 13 + examples/petrinet/models/rules/transition.od | 1 + examples/petrinet/models/schedule.od | 66 - .../models/schedules/combinatory.drawio | 526 ++++++++ examples/petrinet/models/schedules/foo.od | 23 + .../petrinet/models/schedules/petrinet.od | 66 + .../models/schedules/petrinet2.drawio | 1160 +++++++++++++++++ .../models/schedules/recursion.drawio | 217 +++ .../petrinet/models/schedules/schedule.od | 4 + .../all_inputs_reduced.od | 13 + .../operational_semantics/all_outputs.od | 13 + .../all_outputs_increased.od | 13 + .../input_without_token.od | 13 + .../operational_semantics/transition.od | 2 +- examples/petrinet/petrinet_renderer.j2 | 12 + examples/petrinet/renderer.py | 27 +- examples/petrinet/runner.py | 47 +- examples/schedule/RuleExecuter.py | 49 - examples/schedule/ScheduledActionGenerator.py | 104 -- examples/schedule/generator.py | 129 -- examples/schedule/models/README.md | 26 - examples/schedule/models/scheduling_MM.od | 46 - examples/schedule/schedule_lib/__init__.py | 12 - examples/schedule/schedule_lib/data.py | 63 - examples/schedule/schedule_lib/data_modify.py | 26 - examples/schedule/schedule_lib/data_node.py | 47 - examples/schedule/schedule_lib/end.py | 21 - examples/schedule/schedule_lib/exec_node.py | 34 - examples/schedule/schedule_lib/funcs.py | 10 - .../schedule/schedule_lib/id_generator.py | 8 - examples/schedule/schedule_lib/loop.py | 57 - examples/schedule/schedule_lib/match.py | 42 - examples/schedule/schedule_lib/null_node.py | 25 - examples/schedule/schedule_lib/print.py | 28 - examples/schedule/schedule_lib/rewrite.py | 38 - examples/schedule/schedule_lib/start.py | 16 - examples/schedule/templates/schedule_dot.j2 | 9 - .../schedule/templates/schedule_template.j2 | 35 - .../templates/schedule_template_wrap.j2 | 47 - requirements.txt | 3 +- .../schedule/Tests/Test_meta_model.py | 489 +++++++ .../schedule/Tests/Test_xmlparser.py | 45 + .../schedule/Tests/drawio/Empty.drawio | 1 + .../schedule/Tests/drawio/StartToEnd.drawio | 24 + .../schedule/Tests/drawio/Unsupported.drawio | 75 ++ .../schedule/Tests/models/m_petrinet.od | 22 + .../schedule/Tests/models/mm_petrinet.od | 31 + .../Tests/models/rules/transitions.od | 13 + .../models/schedule/connections_action.od | 62 + .../Tests/models/schedule/connections_end.od | 31 + .../Tests/models/schedule/connections_loop.od | 44 + .../models/schedule/connections_match.od | 49 + .../models/schedule/connections_merge.od | 42 + .../models/schedule/connections_modify.od | 41 + .../models/schedule/connections_print.od | 41 + .../models/schedule/connections_rewrite.od | 52 + .../models/schedule/connections_schedule.od | 50 + .../models/schedule/connections_start.od | 27 + .../models/schedule/connections_store.od | 47 + .../Tests/models/schedule/fields_action.od | 83 ++ .../Tests/models/schedule/fields_end.od | 52 + .../Tests/models/schedule/fields_merge.od | 39 + .../Tests/models/schedule/fields_modify.od | 51 + .../Tests/models/schedule/fields_print.od | 39 + .../Tests/models/schedule/fields_start.od | 52 + .../Tests/models/schedule/fields_store.od | 39 + .../Tests/models/schedule/multiple_end.od | 5 + .../Tests/models/schedule/multiple_start.od | 5 + .../schedule/Tests/models/schedule/no_end.od | 1 + .../Tests/models/schedule/no_start.od | 1 + .../Tests/models/schedule/start_end.od | 3 + transformation/schedule/__init__.py | 0 transformation/schedule/generator.py | 197 +++ .../schedule/models/eval_context.py | 151 +++ .../schedule/models/eval_context_stub.pyi | 6 + .../schedule/models/scheduling_MM.od | 195 +++ transformation/schedule/rule_executor.py | 46 + transformation/schedule/rule_scheduler.py | 338 +++++ transformation/schedule/schedule.pyi | 18 + .../schedule/schedule_lib/README.md | 41 + .../schedule/schedule_lib/Schedule_lib.xml | 93 ++ .../schedule/schedule_lib/__init__.py | 31 + .../schedule/schedule_lib/action.py | 106 ++ transformation/schedule/schedule_lib/data.py | 83 ++ .../schedule/schedule_lib/data_node.py | 101 ++ transformation/schedule/schedule_lib/end.py | 80 ++ .../schedule/schedule_lib/exec_node.py | 35 + transformation/schedule/schedule_lib/funcs.py | 56 + transformation/schedule/schedule_lib/loop.py | 74 ++ transformation/schedule/schedule_lib/match.py | 67 + transformation/schedule/schedule_lib/merge.py | 57 + .../schedule/schedule_lib/modify.py | 49 + transformation/schedule/schedule_lib/node.py | 70 + .../schedule/schedule_lib/null_node.py | 80 ++ transformation/schedule/schedule_lib/print.py | 60 + .../schedule/schedule_lib/rewrite.py | 56 + .../schedule/schedule_lib/singleton.py | 1 + transformation/schedule/schedule_lib/start.py | 83 ++ transformation/schedule/schedule_lib/store.py | 92 ++ .../schedule/schedule_lib/sub_schedule.py | 107 ++ .../schedule/templates/schedule_dot.j2 | 60 + .../schedule/templates/schedule_muMLE.j2 | 28 + .../schedule/templates/schedule_template.j2 | 51 + .../templates/schedule_template_wrap.j2 | 48 + 126 files changed, 7235 insertions(+), 981 deletions(-) create mode 100644 examples/geraniums/geraniums_renderer.j2 create mode 100644 examples/geraniums/metamodels/mm.od create mode 100644 examples/geraniums/models/eval_context.py create mode 100644 examples/geraniums/models/example1.od create mode 100644 examples/geraniums/models/example2.od create mode 100644 examples/geraniums/renderer.py create mode 100644 examples/geraniums/rules/cracked_pots.od create mode 100644 examples/geraniums/rules/create_pot.od create mode 100644 examples/geraniums/rules/flowering_flowers_in_pot.od create mode 100644 examples/geraniums/rules/repot_flower_in_pot.od create mode 100644 examples/geraniums/runner.py create mode 100644 examples/geraniums/schedules/schedule.drawio create mode 100644 examples/geraniums/schedules/schedule.od create mode 100644 examples/petrinet/models/rules/all_incoming.od create mode 100644 examples/petrinet/models/rules/all_incomming.od create mode 100644 examples/petrinet/models/rules/all_outgoing.od create mode 100644 examples/petrinet/models/rules/increase_outgoing.od rename examples/petrinet/{operational_semantics/all_input_have_token.od => models/rules/input_without_token.od} (100%) create mode 100644 examples/petrinet/models/rules/places.od create mode 100644 examples/petrinet/models/rules/reduce_incoming.od create mode 100644 examples/petrinet/models/rules/reduce_incomming.od create mode 100644 examples/petrinet/models/rules/transition.od delete mode 100644 examples/petrinet/models/schedule.od create mode 100644 examples/petrinet/models/schedules/combinatory.drawio create mode 100644 examples/petrinet/models/schedules/foo.od create mode 100644 examples/petrinet/models/schedules/petrinet.od create mode 100644 examples/petrinet/models/schedules/petrinet2.drawio create mode 100644 examples/petrinet/models/schedules/recursion.drawio create mode 100644 examples/petrinet/models/schedules/schedule.od create mode 100644 examples/petrinet/operational_semantics/all_inputs_reduced.od create mode 100644 examples/petrinet/operational_semantics/all_outputs.od create mode 100644 examples/petrinet/operational_semantics/all_outputs_increased.od create mode 100644 examples/petrinet/operational_semantics/input_without_token.od create mode 100644 examples/petrinet/petrinet_renderer.j2 delete mode 100644 examples/schedule/RuleExecuter.py delete mode 100644 examples/schedule/ScheduledActionGenerator.py delete mode 100644 examples/schedule/generator.py delete mode 100644 examples/schedule/models/README.md delete mode 100644 examples/schedule/models/scheduling_MM.od delete mode 100644 examples/schedule/schedule_lib/__init__.py delete mode 100644 examples/schedule/schedule_lib/data.py delete mode 100644 examples/schedule/schedule_lib/data_modify.py delete mode 100644 examples/schedule/schedule_lib/data_node.py delete mode 100644 examples/schedule/schedule_lib/end.py delete mode 100644 examples/schedule/schedule_lib/exec_node.py delete mode 100644 examples/schedule/schedule_lib/funcs.py delete mode 100644 examples/schedule/schedule_lib/id_generator.py delete mode 100644 examples/schedule/schedule_lib/loop.py delete mode 100644 examples/schedule/schedule_lib/match.py delete mode 100644 examples/schedule/schedule_lib/null_node.py delete mode 100644 examples/schedule/schedule_lib/print.py delete mode 100644 examples/schedule/schedule_lib/rewrite.py delete mode 100644 examples/schedule/schedule_lib/start.py delete mode 100644 examples/schedule/templates/schedule_dot.j2 delete mode 100644 examples/schedule/templates/schedule_template.j2 delete mode 100644 examples/schedule/templates/schedule_template_wrap.j2 create mode 100644 transformation/schedule/Tests/Test_meta_model.py create mode 100644 transformation/schedule/Tests/Test_xmlparser.py create mode 100644 transformation/schedule/Tests/drawio/Empty.drawio create mode 100644 transformation/schedule/Tests/drawio/StartToEnd.drawio create mode 100644 transformation/schedule/Tests/drawio/Unsupported.drawio create mode 100644 transformation/schedule/Tests/models/m_petrinet.od create mode 100644 transformation/schedule/Tests/models/mm_petrinet.od create mode 100644 transformation/schedule/Tests/models/rules/transitions.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_action.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_end.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_loop.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_match.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_merge.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_modify.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_print.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_rewrite.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_schedule.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_start.od create mode 100644 transformation/schedule/Tests/models/schedule/connections_store.od create mode 100644 transformation/schedule/Tests/models/schedule/fields_action.od create mode 100644 transformation/schedule/Tests/models/schedule/fields_end.od create mode 100644 transformation/schedule/Tests/models/schedule/fields_merge.od create mode 100644 transformation/schedule/Tests/models/schedule/fields_modify.od create mode 100644 transformation/schedule/Tests/models/schedule/fields_print.od create mode 100644 transformation/schedule/Tests/models/schedule/fields_start.od create mode 100644 transformation/schedule/Tests/models/schedule/fields_store.od create mode 100644 transformation/schedule/Tests/models/schedule/multiple_end.od create mode 100644 transformation/schedule/Tests/models/schedule/multiple_start.od create mode 100644 transformation/schedule/Tests/models/schedule/no_end.od create mode 100644 transformation/schedule/Tests/models/schedule/no_start.od create mode 100644 transformation/schedule/Tests/models/schedule/start_end.od create mode 100644 transformation/schedule/__init__.py create mode 100644 transformation/schedule/generator.py create mode 100644 transformation/schedule/models/eval_context.py create mode 100644 transformation/schedule/models/eval_context_stub.pyi create mode 100644 transformation/schedule/models/scheduling_MM.od create mode 100644 transformation/schedule/rule_executor.py create mode 100644 transformation/schedule/rule_scheduler.py create mode 100644 transformation/schedule/schedule.pyi create mode 100644 transformation/schedule/schedule_lib/README.md create mode 100644 transformation/schedule/schedule_lib/Schedule_lib.xml create mode 100644 transformation/schedule/schedule_lib/__init__.py create mode 100644 transformation/schedule/schedule_lib/action.py create mode 100644 transformation/schedule/schedule_lib/data.py create mode 100644 transformation/schedule/schedule_lib/data_node.py create mode 100644 transformation/schedule/schedule_lib/end.py create mode 100644 transformation/schedule/schedule_lib/exec_node.py create mode 100644 transformation/schedule/schedule_lib/funcs.py create mode 100644 transformation/schedule/schedule_lib/loop.py create mode 100644 transformation/schedule/schedule_lib/match.py create mode 100644 transformation/schedule/schedule_lib/merge.py create mode 100644 transformation/schedule/schedule_lib/modify.py create mode 100644 transformation/schedule/schedule_lib/node.py create mode 100644 transformation/schedule/schedule_lib/null_node.py create mode 100644 transformation/schedule/schedule_lib/print.py create mode 100644 transformation/schedule/schedule_lib/rewrite.py rename {examples => transformation}/schedule/schedule_lib/singleton.py (99%) create mode 100644 transformation/schedule/schedule_lib/start.py create mode 100644 transformation/schedule/schedule_lib/store.py create mode 100644 transformation/schedule/schedule_lib/sub_schedule.py create mode 100644 transformation/schedule/templates/schedule_dot.j2 create mode 100644 transformation/schedule/templates/schedule_muMLE.j2 create mode 100644 transformation/schedule/templates/schedule_template.j2 create mode 100644 transformation/schedule/templates/schedule_template_wrap.j2 diff --git a/examples/geraniums/geraniums_renderer.j2 b/examples/geraniums/geraniums_renderer.j2 new file mode 100644 index 0000000..1ef47cc --- /dev/null +++ b/examples/geraniums/geraniums_renderer.j2 @@ -0,0 +1,34 @@ +digraph G { + rankdir=LR; + center=true; + margin=1; + nodesep=1; + + node [fontname="Arial", fontsize=10, shape=box, style=filled, fillcolor=white]; + + // Geraniums + {% for id, name, flowering in geraniums %} + g{{ id }} [ + label="geranium: {{ name }}\n({{ 'flowering' if flowering else 'not flowering' }})", + shape=ellipse, + fillcolor="{{ 'lightpink' if flowering else 'lightgray' }}", + fontcolor=black + ]; + {% endfor %} + + // Pots + {% for id, name, cracked in pots %} + p{{ id }} [ + label="pot: {{ name }}\n({{ 'cracked' if cracked else 'pristine' }})", + shape=box, + fillcolor="{{ 'mistyrose' if cracked else 'lightgreen' }}", + fontcolor=black, + style="filled,bold" + ]; + {% endfor %} + + // Connections: geranium -> pot + {% for source, target in planted %} + g{{ source }} -> p{{ target }}; + {% endfor %} +} diff --git a/examples/geraniums/metamodels/mm.od b/examples/geraniums/metamodels/mm.od new file mode 100644 index 0000000..f6f6962 --- /dev/null +++ b/examples/geraniums/metamodels/mm.od @@ -0,0 +1,9 @@ +class Geranium { + Boolean flowering; +} + +class Pot { + Boolean cracked; +} + +association Planted [0..*] Geranium -> Pot [1..1] diff --git a/examples/geraniums/models/eval_context.py b/examples/geraniums/models/eval_context.py new file mode 100644 index 0000000..d8dfcd8 --- /dev/null +++ b/examples/geraniums/models/eval_context.py @@ -0,0 +1,44 @@ +import os + +from jinja2 import Environment, FileSystemLoader + +from api.od import ODAPI +from framework.conformance import eval_context_decorator + + +@eval_context_decorator +def _render_geraniums_dot(od: ODAPI, file: str) -> str: + __DIR__ = os.path.dirname(__file__) + env = Environment( + loader=FileSystemLoader( + __DIR__ + ) + ) + env.trim_blocks = True + env.lstrip_blocks = True + template_dot = env.get_template("geraniums_renderer.j2") + + id_count = 0 + id_map = {} + render = {"geraniums": [], "pots": [], "planted": []} + + for name, uuid in od.get_all_instances("Geranium"): + render["geraniums"].append((id_count, name, od.get_slot_value(uuid, "flowering"))) + id_map[uuid] = id_count + id_count += 1 + + for name, uuid in od.get_all_instances("Pot"): + render["pots"].append((id_count, name, od.get_slot_value(uuid, "cracked"))) + id_map[uuid] = id_count + id_count += 1 + + for name, uuid in od.get_all_instances("Planted"): + render["planted"].append((id_map[od.get_source(uuid)], id_map[od.get_target(uuid)])) + + with open(file, "w", encoding="utf-8") as f_dot: + f_dot.write(template_dot.render(**render)) + return "" + +eval_context = { + "render_geraniums_dot": _render_geraniums_dot, +} diff --git a/examples/geraniums/models/example1.od b/examples/geraniums/models/example1.od new file mode 100644 index 0000000..db5bc32 --- /dev/null +++ b/examples/geraniums/models/example1.od @@ -0,0 +1,17 @@ +f1:Geranium { + flowering = True; +} +f2:Geranium { + flowering = False; +} +f3:Geranium { + flowering = True; +} + +p1:Pot { + cracked = True; +} + +:Planted (f1 -> p1) +:Planted (f2 -> p1) +:Planted (f3 -> p1) \ No newline at end of file diff --git a/examples/geraniums/models/example2.od b/examples/geraniums/models/example2.od new file mode 100644 index 0000000..9c4e0f4 --- /dev/null +++ b/examples/geraniums/models/example2.od @@ -0,0 +1,47 @@ +f1:Geranium { + flowering = True; +} +f2:Geranium { + flowering = True; +} +f3:Geranium { + flowering = False; +} + +p1:Pot { + cracked = True; +} + +:Planted (f1 -> p1) +:Planted (f2 -> p1) +:Planted (f3 -> p1) + + + + +f4:Geranium { + flowering = True; +} +p2:Pot { + cracked = True; +} +:Planted (f4 -> p2) + + + +f5:Geranium { + flowering = True; +} +p3:Pot { + cracked = False; +} +:Planted (f5 -> p3) + + +f6:Geranium { + flowering = False; +} +p4:Pot { + cracked = True; +} +:Planted (f6 -> p4) \ No newline at end of file diff --git a/examples/geraniums/renderer.py b/examples/geraniums/renderer.py new file mode 100644 index 0000000..3ac50f5 --- /dev/null +++ b/examples/geraniums/renderer.py @@ -0,0 +1,45 @@ +import os + +from jinja2 import Environment, FileSystemLoader + +from api.od import ODAPI +from concrete_syntax.graphviz.make_url import show_graphviz +from concrete_syntax.graphviz.renderer import make_graphviz_id + +try: + import graphviz + HAVE_GRAPHVIZ = True +except ImportError: + HAVE_GRAPHVIZ = False + +def render_geraniums_dot(od: ODAPI, file: str) -> str: + __DIR__ = os.path.dirname(__file__) + env = Environment( + loader=FileSystemLoader( + __DIR__ + ) + ) + env.trim_blocks = True + env.lstrip_blocks = True + template_dot = env.get_template("geraniums_renderer.j2") + + id_count = 0 + id_map = {} + render = {"geraniums": [], "pots": [], "planted": []} + + for name, uuid in od.get_all_instances("Geranium"): + render["geraniums"].append((id_count, name, od.get_slot_value(uuid, "flowering"))) + id_map[uuid] = id_count + id_count += 1 + + for name, uuid in od.get_all_instances("Pot"): + render["pots"].append((id_count, name, od.get_slot_value(uuid, "cracked"))) + id_map[uuid] = id_count + id_count += 1 + + for name, uuid in od.get_all_instances("Planted"): + render["planted"].append((id_map[od.get_source(uuid)], id_map[od.get_target(uuid)])) + + with open(file, "w", encoding="utf-8") as f_dot: + f_dot.write(template_dot.render(**render)) + return "" \ No newline at end of file diff --git a/examples/geraniums/rules/cracked_pots.od b/examples/geraniums/rules/cracked_pots.od new file mode 100644 index 0000000..61ef57f --- /dev/null +++ b/examples/geraniums/rules/cracked_pots.od @@ -0,0 +1,3 @@ +pot:RAM_Pot { + RAM_cracked = `get_value(this)`; +} \ No newline at end of file diff --git a/examples/geraniums/rules/create_pot.od b/examples/geraniums/rules/create_pot.od new file mode 100644 index 0000000..c6ef5d0 --- /dev/null +++ b/examples/geraniums/rules/create_pot.od @@ -0,0 +1,3 @@ +pot:RAM_Pot { + RAM_cracked = `False`; +} \ No newline at end of file diff --git a/examples/geraniums/rules/flowering_flowers_in_pot.od b/examples/geraniums/rules/flowering_flowers_in_pot.od new file mode 100644 index 0000000..591c123 --- /dev/null +++ b/examples/geraniums/rules/flowering_flowers_in_pot.od @@ -0,0 +1,7 @@ +pot:RAM_Pot + +flower:RAM_Geranium { + RAM_flowering = `get_value(this)`; +} + +:RAM_Planted (flower -> pot) \ No newline at end of file diff --git a/examples/geraniums/rules/repot_flower_in_pot.od b/examples/geraniums/rules/repot_flower_in_pot.od new file mode 100644 index 0000000..134813f --- /dev/null +++ b/examples/geraniums/rules/repot_flower_in_pot.od @@ -0,0 +1,8 @@ +pot:RAM_Pot +new_pot:RAM_Pot + +flower:RAM_Geranium { + RAM_flowering = `get_value(this)`; +} + +replant:RAM_Planted (flower -> new_pot) \ No newline at end of file diff --git a/examples/geraniums/runner.py b/examples/geraniums/runner.py new file mode 100644 index 0000000..fdaaa68 --- /dev/null +++ b/examples/geraniums/runner.py @@ -0,0 +1,48 @@ +from examples.geraniums.renderer import render_geraniums_dot +from transformation.ramify import ramify + +from models.eval_context import eval_context + +from transformation.schedule.rule_scheduler import * + +if __name__ == "__main__": + import os + THIS_DIR = os.path.dirname(__file__) + + # get file contents as string + def read_file(filename): + with open(THIS_DIR+'/'+filename) as file: + return file.read() + + + state = DevState() + scd_mmm = bootstrap_scd(state) + + mm_cs = read_file('metamodels/mm.od') + m_cs = read_file('models/example2.od') + + mm = parser_cd.parse_cd( + state, + m_text=mm_cs, + ) + m = parser_od.parse_od( + state, m_text=m_cs, mm=mm + ) + conf_err = Conformance( + state, m, mm + ).check_nominal() + print(render_conformance_check_result(conf_err)) + mm_ramified = ramify(state, mm) + + action_generator = RuleSchedular(state, mm, mm_ramified, verbose=True, directory="examples/geraniums", eval_context=eval_context) + od = ODAPI(state, m, mm) + render_geraniums_dot(od, f"{THIS_DIR}/geraniums.dot") + + # if action_generator.load_schedule(f"petrinet.od"): + # if action_generator.load_schedule("schedules/combinatory.drawio"): + if action_generator.load_schedule("schedules/schedule.drawio"): + + action_generator.generate_dot("../dot.dot") + code, message = action_generator.run(od) + print(f"{code}: {message}") + render_geraniums_dot(od, f"{THIS_DIR}/geraniums_final.dot") \ No newline at end of file diff --git a/examples/geraniums/schedules/schedule.drawio b/examples/geraniums/schedules/schedule.drawio new file mode 100644 index 0000000..41437fa --- /dev/null +++ b/examples/geraniums/schedules/schedule.drawio @@ -0,0 +1,645 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/geraniums/schedules/schedule.od b/examples/geraniums/schedules/schedule.od new file mode 100644 index 0000000..e69de29 diff --git a/examples/petrinet/models/m_example_simple.od b/examples/petrinet/models/m_example_simple.od index a3eee8d..d7dd1ea 100644 --- a/examples/petrinet/models/m_example_simple.od +++ b/examples/petrinet/models/m_example_simple.od @@ -1,5 +1,8 @@ p0:PNPlace p1:PNPlace +p2:PNPlace +p3:PNPlace +p4:PNPlace t0:PNTransition :arc (p0 -> t0) @@ -7,4 +10,12 @@ t0:PNTransition t1:PNTransition :arc (p1 -> t1) -:arc (t1 -> p0) \ No newline at end of file +:arc (t1 -> p2) + +t2:PNTransition +:arc (p2 -> t2) +:arc (t2 -> p0) + + +t3:PNTransition +:arc (t3 -> p4) \ No newline at end of file diff --git a/examples/petrinet/models/m_example_simple_rt_initial.od b/examples/petrinet/models/m_example_simple_rt_initial.od index fa93f4e..64fc3b7 100644 --- a/examples/petrinet/models/m_example_simple_rt_initial.od +++ b/examples/petrinet/models/m_example_simple_rt_initial.od @@ -9,3 +9,21 @@ p1s:PNPlaceState { } :pn_of (p1s -> p1) + +p2s:PNPlaceState { + numTokens = 0; +} + +:pn_of (p2s -> p2) + +p3s:PNPlaceState { + numTokens = 0; +} + +:pn_of (p3s -> p3) + +p4s:PNPlaceState { + numTokens = 0; +} + +:pn_of (p4s -> p4) diff --git a/examples/petrinet/models/rules/all_incoming.od b/examples/petrinet/models/rules/all_incoming.od new file mode 100644 index 0000000..1b87f1d --- /dev/null +++ b/examples/petrinet/models/rules/all_incoming.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `True`; +} +:RAM_pn_of (ps -> p) + +# An incoming arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (p -> t) diff --git a/examples/petrinet/models/rules/all_incomming.od b/examples/petrinet/models/rules/all_incomming.od new file mode 100644 index 0000000..1b87f1d --- /dev/null +++ b/examples/petrinet/models/rules/all_incomming.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `True`; +} +:RAM_pn_of (ps -> p) + +# An incoming arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (p -> t) diff --git a/examples/petrinet/models/rules/all_outgoing.od b/examples/petrinet/models/rules/all_outgoing.od new file mode 100644 index 0000000..ab431cc --- /dev/null +++ b/examples/petrinet/models/rules/all_outgoing.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `True`; +} +:RAM_pn_of (ps -> p) + +# An incoming arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (t -> p) diff --git a/examples/petrinet/models/rules/increase_outgoing.od b/examples/petrinet/models/rules/increase_outgoing.od new file mode 100644 index 0000000..1fa1acb --- /dev/null +++ b/examples/petrinet/models/rules/increase_outgoing.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `get_value(this) + 1`; +} +:RAM_pn_of (ps -> p) + +# An outgoing arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (t -> p) diff --git a/examples/petrinet/operational_semantics/all_input_have_token.od b/examples/petrinet/models/rules/input_without_token.od similarity index 100% rename from examples/petrinet/operational_semantics/all_input_have_token.od rename to examples/petrinet/models/rules/input_without_token.od diff --git a/examples/petrinet/models/rules/places.od b/examples/petrinet/models/rules/places.od new file mode 100644 index 0000000..923fb03 --- /dev/null +++ b/examples/petrinet/models/rules/places.od @@ -0,0 +1,3 @@ +# A place with no tokens: + +p:RAM_PNPlace \ No newline at end of file diff --git a/examples/petrinet/models/rules/reduce_incoming.od b/examples/petrinet/models/rules/reduce_incoming.od new file mode 100644 index 0000000..b85a2db --- /dev/null +++ b/examples/petrinet/models/rules/reduce_incoming.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `get_value(this) -1`; +} +:RAM_pn_of (ps -> p) + +# An incoming arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (p -> t) \ No newline at end of file diff --git a/examples/petrinet/models/rules/reduce_incomming.od b/examples/petrinet/models/rules/reduce_incomming.od new file mode 100644 index 0000000..b85a2db --- /dev/null +++ b/examples/petrinet/models/rules/reduce_incomming.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `get_value(this) -1`; +} +:RAM_pn_of (ps -> p) + +# An incoming arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (p -> t) \ No newline at end of file diff --git a/examples/petrinet/models/rules/transition.od b/examples/petrinet/models/rules/transition.od new file mode 100644 index 0000000..c3bd82c --- /dev/null +++ b/examples/petrinet/models/rules/transition.od @@ -0,0 +1 @@ +t:RAM_PNTransition \ No newline at end of file diff --git a/examples/petrinet/models/schedule.od b/examples/petrinet/models/schedule.od deleted file mode 100644 index 1584a7c..0000000 --- a/examples/petrinet/models/schedule.od +++ /dev/null @@ -1,66 +0,0 @@ -start:Start -end:End - -transitions:Match{ - file = "operational_semantics/transition"; -} - - -d:Data_modify -{ - modify_dict = ' - { - "tr": "t" - }'; -} - -nac_input_without:Match{ - file = "operational_semantics/all_input_have_token"; - n = "1"; -} - -inputs:Match{ - file = "operational_semantics/all_inputs"; -} - -rewrite_incoming:Rewrite -{ - file = "operational_semantics/remove_incoming"; -} - -loop_trans:Loop -loop_input:Loop - -p:Print -{ -event = True; -label = "transition: "; -} - -p2:Print -{ -event = True; -label = "inputs: "; -} - -:Exec_con(start -> transitions){gate_from = 0;gate_to = 0;} -:Exec_con(transitions -> end){gate_from = 1;gate_to = 0;} -:Exec_con(transitions -> loop_trans){gate_from = 0;gate_to = 0;} -:Exec_con(loop_trans -> nac_input_without){gate_from = 0;gate_to = 0;} - -[//]: # (:Exec_con(nac_input_without -> loop_trans){gate_from = 0;gate_to = 0;}) -:Exec_con(nac_input_without -> inputs){gate_from = 1;gate_to = 0;} -:Exec_con(inputs -> loop_input){gate_from = 0;gate_to = 0;} -:Exec_con(inputs -> loop_trans){gate_from = 1;gate_to = 0;} - -:Exec_con(loop_trans -> end){gate_from = 1;gate_to = 0;} - -:Data_con(transitions -> loop_trans) -:Data_con(nac_input_without -> p) -:Data_con(d -> nac_input_without) -:Data_con(loop_trans -> d) -:Data_con(loop_trans -> rewrite_incoming) - - - - diff --git a/examples/petrinet/models/schedules/combinatory.drawio b/examples/petrinet/models/schedules/combinatory.drawio new file mode 100644 index 0000000..c22b5ce --- /dev/null +++ b/examples/petrinet/models/schedules/combinatory.drawio @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/petrinet/models/schedules/foo.od b/examples/petrinet/models/schedules/foo.od new file mode 100644 index 0000000..7acc7a8 --- /dev/null +++ b/examples/petrinet/models/schedules/foo.od @@ -0,0 +1,23 @@ +start:Start { + ports_exec = `["F","FF"]`; +} +end:End { + ports_exec = `["F"]`; +} + +p1:Print{ + custom = "Foo"; +} + +p2:Print{ + custom = "FooFoo"; +} + +p3:Print{ + custom = "FooFooFoo"; +} + +:Conn_exec (start -> p1) {from="F";to="in";} +:Conn_exec (p1 -> end) {from="out";to="F";} +:Conn_exec (start -> p2) {from="FF";to="in";} +:Conn_exec (p2 -> end) {from="out";to="F";} diff --git a/examples/petrinet/models/schedules/petrinet.od b/examples/petrinet/models/schedules/petrinet.od new file mode 100644 index 0000000..386c3ed --- /dev/null +++ b/examples/petrinet/models/schedules/petrinet.od @@ -0,0 +1,66 @@ +start:Start +end:End + +m:Match{ + file = "operational_semantics/transition"; +} + +nac1:Match{ + file = "operational_semantics/all_input_have_token"; + n = "1"; +} + +inputs:Match{ + file = "operational_semantics/all_inputs"; +} +rinput:Rewrite{ + file = "operational_semantics/all_inputs_reduced"; +} + +outputs:Match{ + file = "operational_semantics/all_outputs"; +} +routput:Rewrite{ + file = "operational_semantics/all_outputs_increased"; +} + +p:Print{ + event = True; +} +p2:Print{ + event = False; + custom = `"succesfully execuded a petrinet transition"`; +} + +l:Loop +l2:Loop +l3:Loop + + +:Conn_exec (start -> m) {from="out"; to="in";} +:Conn_exec (m -> l) {from="success"; to="in";} +:Conn_exec (l -> nac1) {from="it"; to="in";} +:Conn_exec (l -> end) {from="out"; to="in";} +:Conn_exec (nac1 -> l) {from="success"; to="in";} +:Conn_exec (nac1 -> inputs) {from="fail"; to="in";} +:Conn_exec (inputs -> l2) {from="success"; to="in";} +:Conn_exec (inputs -> l2) {from="fail"; to="in";} +:Conn_exec (l2 -> rinput) {from="it"; to="in";} +:Conn_exec (rinput -> l2) {from="out"; to="in";} +:Conn_exec (l2 -> outputs) {from="out"; to="in";} +:Conn_exec (outputs -> l3) {from="success"; to="in";} +:Conn_exec (outputs -> l3) {from="fail"; to="in";} +:Conn_exec (l3 -> routput) {from="it"; to="in";} +:Conn_exec (routput -> l3) {from="out"; to="in";} +:Conn_exec (l3 -> p2) {from="out"; to="in";} +:Conn_exec (p2 -> end) {from="out"; to="in";} + + +:Conn_data (m -> l) {from="out"; to="in";} +:Conn_data (l -> nac1) {from="out"; to="in";} +:Conn_data (l -> inputs) {from="out"; to="in";} +:Conn_data (inputs -> l2) {from="out"; to="in";} +:Conn_data (l2 -> rinput) {from="out"; to="in";} +:Conn_data (l -> outputs) {from="out"; to="in";} +:Conn_data (outputs -> l3) {from="out"; to="in";} +:Conn_data (l3 -> routput) {from="out"; to="in";} \ No newline at end of file diff --git a/examples/petrinet/models/schedules/petrinet2.drawio b/examples/petrinet/models/schedules/petrinet2.drawio new file mode 100644 index 0000000..6294d7f --- /dev/null +++ b/examples/petrinet/models/schedules/petrinet2.drawio @@ -0,0 +1,1160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/petrinet/models/schedules/recursion.drawio b/examples/petrinet/models/schedules/recursion.drawio new file mode 100644 index 0000000..f82cabd --- /dev/null +++ b/examples/petrinet/models/schedules/recursion.drawio @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/petrinet/models/schedules/schedule.od b/examples/petrinet/models/schedules/schedule.od new file mode 100644 index 0000000..8c8f816 --- /dev/null +++ b/examples/petrinet/models/schedules/schedule.od @@ -0,0 +1,4 @@ +start: Start +end: End + +:Conn_exec (start -> end) {from="tfuy"; to="in";} \ No newline at end of file diff --git a/examples/petrinet/operational_semantics/all_inputs_reduced.od b/examples/petrinet/operational_semantics/all_inputs_reduced.od new file mode 100644 index 0000000..a6bfdd4 --- /dev/null +++ b/examples/petrinet/operational_semantics/all_inputs_reduced.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `get_value(this) -1`; +} +:RAM_pn_of (ps -> p) + +# An incoming arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (t -> p) diff --git a/examples/petrinet/operational_semantics/all_outputs.od b/examples/petrinet/operational_semantics/all_outputs.od new file mode 100644 index 0000000..ce5efd0 --- /dev/null +++ b/examples/petrinet/operational_semantics/all_outputs.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `True`; +} +:RAM_pn_of (ps -> p) + +# An outgoing arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (t -> p) diff --git a/examples/petrinet/operational_semantics/all_outputs_increased.od b/examples/petrinet/operational_semantics/all_outputs_increased.od new file mode 100644 index 0000000..1fa1acb --- /dev/null +++ b/examples/petrinet/operational_semantics/all_outputs_increased.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `get_value(this) + 1`; +} +:RAM_pn_of (ps -> p) + +# An outgoing arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (t -> p) diff --git a/examples/petrinet/operational_semantics/input_without_token.od b/examples/petrinet/operational_semantics/input_without_token.od new file mode 100644 index 0000000..9207ce2 --- /dev/null +++ b/examples/petrinet/operational_semantics/input_without_token.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `get_value(this) == 0`; +} +:RAM_pn_of (ps -> p) + +# An incoming arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (p -> t) diff --git a/examples/petrinet/operational_semantics/transition.od b/examples/petrinet/operational_semantics/transition.od index c7c8203..c3bd82c 100644 --- a/examples/petrinet/operational_semantics/transition.od +++ b/examples/petrinet/operational_semantics/transition.od @@ -1 +1 @@ -tr:RAM_PNTransition \ No newline at end of file +t:RAM_PNTransition \ No newline at end of file diff --git a/examples/petrinet/petrinet_renderer.j2 b/examples/petrinet/petrinet_renderer.j2 new file mode 100644 index 0000000..0ace22b --- /dev/null +++ b/examples/petrinet/petrinet_renderer.j2 @@ -0,0 +1,12 @@ +digraph G { + rankdir=LR; + center=true; + margin=1; + nodesep=1; + subgraph places { + node [fontname=Arial,fontsize=10,shape=circle,fixedsize=true,label="", height=.35,width=.35]; + {% for place in places %} + {{ place[0] }} [label="{{ place[1] }}_{{ place[2] }}"] + {% endfor %} + } +} \ No newline at end of file diff --git a/examples/petrinet/renderer.py b/examples/petrinet/renderer.py index 278376a..3916311 100644 --- a/examples/petrinet/renderer.py +++ b/examples/petrinet/renderer.py @@ -1,3 +1,7 @@ +import os + +from jinja2 import Environment, FileSystemLoader + from api.od import ODAPI from concrete_syntax.graphviz.make_url import show_graphviz from concrete_syntax.graphviz.renderer import make_graphviz_id @@ -16,13 +20,24 @@ def render_tokens(num_tokens: int): return str(num_tokens) def render_petri_net_to_dot(od: ODAPI) -> str: + env = Environment( + loader=FileSystemLoader( + os.path.dirname(__file__) + ) + ) + env.trim_blocks = True + env.lstrip_blocks = True + template_dot = env.get_template("petrinet_renderer.j2") + with open("test_pet.dot", "w", encoding="utf-8") as f_dot: + places = [(make_graphviz_id(place), place_name, render_tokens(od.get_slot_value(od.get_source(od.get_incoming(place, "pn_of")[0]), "numTokens"))) for place_name, place in od.get_all_instances("PNPlace")] + f_dot.write(template_dot.render({"places": places})) dot = "" - dot += "rankdir=LR;" - dot += "center=true;" - dot += "margin=1;" - dot += "nodesep=1;" - dot += "subgraph places {" - dot += " node [fontname=Arial,fontsize=10,shape=circle,fixedsize=true,label=\"\", height=.35,width=.35];" + dot += "rankdir=LR;\n" + dot += "center=true;\n" + dot += "margin=1;\n" + dot += "nodesep=1;\n" + dot += "subgraph places {\n" + dot += " node [fontname=Arial,fontsize=10,shape=circle,fixedsize=true,label=\"\", height=.35,width=.35];\n" for place_name, place in od.get_all_instances("PNPlace"): # place_name = od.get_name(place) try: diff --git a/examples/petrinet/runner.py b/examples/petrinet/runner.py index df8692e..6df572a 100644 --- a/examples/petrinet/runner.py +++ b/examples/petrinet/runner.py @@ -1,19 +1,12 @@ -from examples.schedule.RuleExecuter import RuleExecuter -from state.devstate import DevState -from api.od import ODAPI +from icecream import ic + from concrete_syntax.textual_od.renderer import render_od -# from concrete_syntax.textual_od.renderer_jinja2 import render_od_jinja2 -from bootstrap.scd import bootstrap_scd +from transformation.schedule.Tests import Test_xmlparser from util import loader -from transformation.rule import RuleMatcherRewriter, ActionGenerator from transformation.ramify import ramify -from examples.semantics.operational import simulator from examples.petrinet.renderer import show_petri_net -from examples.schedule.ScheduledActionGenerator import * -from examples.schedule.RuleExecuter import * - - +from transformation.schedule.rule_scheduler import * if __name__ == "__main__": import os @@ -35,40 +28,26 @@ if __name__ == "__main__": # m_rt_initial_cs = m_cs + read_file('models/m_example_simple_rt_initial.od') # m_cs = read_file('models/m_example_mutex.od') # m_rt_initial_cs = m_cs + read_file('models/m_example_mutex_rt_initial.od') - m_cs = read_file('models/m_example_inharc.od') - m_rt_initial_cs = m_cs + read_file('models/m_example_inharc_rt_initial.od') + m_cs = read_file('models/m_example_simple.od') + m_rt_initial_cs = m_cs + read_file('models/m_example_simple_rt_initial.od') # Parse them mm = loader.parse_and_check(state, mm_cs, scd_mmm, "Petri-Net Design meta-model") mm_rt = loader.parse_and_check(state, mm_rt_cs, scd_mmm, "Petri-Net Runtime meta-model") m = loader.parse_and_check(state, m_cs, mm, "Example model") m_rt_initial = loader.parse_and_check(state, m_rt_initial_cs, mm_rt, "Example model initial state") - mm_rt_ramified = ramify(state, mm_rt) - rules = loader.load_rules(state, - lambda rule_name, kind: f"{THIS_DIR}/operational_semantics/r_{rule_name}_{kind}.od", - mm_rt_ramified, - ["fire_transition"]) # only 1 rule :( - # matcher_rewriter = RuleMatcherRewriter(state, mm_rt, mm_rt_ramified) - # action_generator = ActionGenerator(matcher_rewriter, rules) - matcher_rewriter2 = RuleExecuter(state, mm_rt, mm_rt_ramified) - action_generator = ScheduleActionGenerator(matcher_rewriter2, f"models/schedule.od") - def render_callback(od): - show_petri_net(od) - return render_od(state, od.m, od.mm) + action_generator = RuleSchedular(state, mm_rt, mm_rt_ramified, verbose=True, directory="models") - action_generator.generate_dot() + # if action_generator.load_schedule(f"petrinet.od"): + # if action_generator.load_schedule("schedules/combinatory.drawio"): + if action_generator.load_schedule("schedules/petrinet3.drawio"): - sim = simulator.MinimalSimulator( - action_generator=action_generator, - decision_maker=simulator.InteractiveDecisionMaker(auto_proceed=False), - # decision_maker=simulator.RandomDecisionMaker(seed=0), - termination_condition=action_generator.termination_condition, - # renderer=lambda od: render_od(state, od.m, od.mm), - ) - sim.run(ODAPI(state, m_rt_initial, mm_rt)) \ No newline at end of file + action_generator.generate_dot("../dot.dot") + code, message = action_generator.run(ODAPI(state, m_rt_initial, mm_rt)) + print(f"{code}: {message}") diff --git a/examples/schedule/RuleExecuter.py b/examples/schedule/RuleExecuter.py deleted file mode 100644 index 8566d10..0000000 --- a/examples/schedule/RuleExecuter.py +++ /dev/null @@ -1,49 +0,0 @@ -from concrete_syntax.textual_od.renderer import render_od - -import pprint -from typing import Generator, Callable, Any -from uuid import UUID -import functools - -from api.od import ODAPI -from concrete_syntax.common import indent -from transformation.matcher import match_od -from transformation.rewriter import rewrite -from transformation.cloner import clone_od -from util.timer import Timer -from util.loader import parse_and_check - -class RuleExecuter: - def __init__(self, state, mm: UUID, mm_ramified: UUID, eval_context={}): - self.state = state - self.mm = mm - self.mm_ramified = mm_ramified - self.eval_context = eval_context - - # Generates matches. - # Every match is a dictionary with entries LHS_element_name -> model_element_name - def match_rule(self, m: UUID, lhs: UUID, *, pivot:dict[Any, Any]): - lhs_matcher = match_od(self.state, - host_m=m, - host_mm=self.mm, - pattern_m=lhs, - pattern_mm=self.mm_ramified, - eval_context=self.eval_context, - pivot= pivot, - ) - return lhs_matcher - - def rewrite_rule(self, m: UUID, rhs: UUID, *, pivot:dict[Any, Any]): - yield rewrite(self.state, - rhs_m=rhs, - pattern_mm=self.mm_ramified, - lhs_match=pivot, - host_m=m, - host_mm=self.mm, - eval_context=self.eval_context, - ) - - - def load_match(self, file: str): - with open(file, "r") as f: - return parse_and_check(self.state, f.read(), self.mm_ramified, file) diff --git a/examples/schedule/ScheduledActionGenerator.py b/examples/schedule/ScheduledActionGenerator.py deleted file mode 100644 index 0f91121..0000000 --- a/examples/schedule/ScheduledActionGenerator.py +++ /dev/null @@ -1,104 +0,0 @@ -import importlib.util -import io -import os - -from jinja2 import FileSystemLoader, Environment - -from concrete_syntax.textual_od import parser as parser_od -from concrete_syntax.textual_cd import parser as parser_cd -from api.od import ODAPI -from bootstrap.scd import bootstrap_scd -from examples.schedule.generator import schedule_generator -from examples.schedule.schedule_lib import End, NullNode -from framework.conformance import Conformance, render_conformance_check_result -from state.devstate import DevState - - -class ScheduleActionGenerator: - def __init__(self, rule_executer, schedulefile:str): - self.rule_executer = rule_executer - self.rule_dict = {} - self.schedule: "Schedule" - - - self.state = DevState() - self.load_schedule(schedulefile) - - def load_schedule(self, filename): - print("Loading schedule ...") - scd_mmm = bootstrap_scd(self.state) - with open("../schedule/models/scheduling_MM.od", "r") as f_MM: - mm_cs = f_MM.read() - with open(f"{filename}", "r") as f_M: - m_cs = f_M.read() - print("OK") - - print("\nParsing models") - - print(f"\tParsing meta model") - scheduling_mm = parser_cd.parse_cd( - self.state, - m_text=mm_cs, - ) - print(f"\tParsing '{filename}_M.od' model") - scheduling_m = parser_od.parse_od( - self.state, - m_text=m_cs, - mm=scheduling_mm - ) - print(f"OK") - - print("\tmeta-meta-model a valid class diagram") - conf = Conformance(self.state, scd_mmm, scd_mmm) - print(render_conformance_check_result(conf.check_nominal())) - print(f"Is our '{filename}_M.od' model a valid '{filename}_MM.od' diagram?") - conf = Conformance(self.state, scheduling_m, scheduling_mm) - print(render_conformance_check_result(conf.check_nominal())) - print("OK") - - od = ODAPI(self.state, scheduling_m, scheduling_mm) - g = schedule_generator(od) - - output_buffer = io.StringIO() - g.generate_schedule(output_buffer) - open(f"schedule.py", "w").write(output_buffer.getvalue()) - spec = importlib.util.spec_from_file_location("schedule", "schedule.py") - scedule_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(scedule_module) - self.schedule = scedule_module.Schedule(self.rule_executer) - self.load_matchers() - - def load_matchers(self): - matchers = dict() - for file in self.schedule.get_matchers(): - matchers[file] = self.rule_executer.load_match(file) - self.schedule.init_schedule(matchers) - - def __call__(self, api: ODAPI): - exec_op = self.schedule(api) - yield from exec_op - - def termination_condition(self, api: ODAPI): - if type(self.schedule.cur) == End: - return "jay" - if type(self.schedule.cur) == NullNode: - return "RRRR" - return None - - def generate_dot(self): - env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates'))) - env.trim_blocks = True - env.lstrip_blocks = True - template_dot = env.get_template('schedule_dot.j2') - - nodes = [] - edges = [] - visit = set() - self.schedule.generate_dot(nodes, edges, visit) - print("Nodes:") - print(nodes) - print("\nEdges:") - print(edges) - - with open("test.dot", "w") as f_dot: - f_dot.write(template_dot.render({"nodes": nodes, "edges": edges})) \ No newline at end of file diff --git a/examples/schedule/generator.py b/examples/schedule/generator.py deleted file mode 100644 index ed8a111..0000000 --- a/examples/schedule/generator.py +++ /dev/null @@ -1,129 +0,0 @@ -import sys -import os -import json -from uuid import UUID - -from jinja2.runtime import Macro - -from api.od import ODAPI -from jinja2 import Environment, FileSystemLoader, meta - - -class schedule_generator: - def __init__(self, odApi:ODAPI): - self.env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates'))) - self.env.trim_blocks = True - self.env.lstrip_blocks = True - self.template = self.env.get_template('schedule_template.j2') - self.template_wrap = self.env.get_template('schedule_template_wrap.j2') - self.api = odApi - - def get_slot_value_default(item: UUID, slot:str, default): - if slot in self.api.get_slots(item): - return self.api.get_slot_value(item, slot) - return default - - name_dict = lambda item: {"name": self.api.get_name(item)} - conn_dict = lambda item: {"name_from": self.api.get_name(self.api.get_source(item)), - "name_to": self.api.get_name(self.api.get_target(item)), - "gate_from": self.api.get_slot_value(item, "gate_from"), - "gate_to": self.api.get_slot_value(item, "gate_to"), - } - - conn_data_event = {"Match": lambda item: False, - "Rewrite": lambda item: False, - "Data_modify": lambda item: True, - "Loop": lambda item: True, - "Print": lambda item: get_slot_value_default(item, "event", False) - } - conn_data_dict = lambda item: {"name_from": self.api.get_name(self.api.get_source(item)), - "name_to": self.api.get_name(self.api.get_target(item)), - "event": conn_data_event[self.api.get_type_name(target := self.api.get_target(item))](target) - } - rewrite_dict = lambda item: {"name": self.api.get_name(item), - "file": self.api.get_slot_value(item, "file"), - } - match_dict = lambda item: {"name": self.api.get_name(item), - "file": self.api.get_slot_value(item, "file"), - "n": self.api.get_slot_value(item, "n") \ - if "n" in self.api.get_slots(item) else 'float("inf")' - } - data_modify_dict = lambda item: {"name": self.api.get_name(item), - "dict": json.loads(self.api.get_slot_value(item, "modify_dict")) - } - loop_dict = lambda item: {"name": self.api.get_name(item), - "choise": get_slot_value_default(item, "choise", False)} - print_dict = lambda item: {"name": self.api.get_name(item), - "label": get_slot_value_default(item, "label", "")} - arg_map = {"Start": name_dict, "End": name_dict, - "Match": match_dict, "Rewrite": rewrite_dict, - "Data_modify": data_modify_dict, "Loop": loop_dict, - "Exec_con": conn_dict, "Data_con": conn_data_dict, - "Print": print_dict} - self.macro_args = {tp: (macro, arg_map.get(tp)) for tp, macro in self.template.module.__dict__.items() - if type(macro) == Macro} - - def _render(self, item): - type_name = self.api.get_type_name(item) - macro, arg_gen = self.macro_args[type_name] - return macro(**arg_gen(item)) - - def generate_schedule(self, stream = sys.stdout): - start = self.api.get_all_instances("Start")[0][1] - stack = [start] - out = {"blocks":[], "exec_conn":[], "data_conn":[], "match_files":set(), "matchers":[], "start":self.api.get_name(start)} - execBlocks = set() - exec_conn = list() - - while len(stack) > 0: - exec_obj = stack.pop() - if exec_obj in execBlocks: - continue - execBlocks.add(exec_obj) - for conn in self.api.get_outgoing(exec_obj, "Exec_con"): - exec_conn.append(conn) - stack.append(self.api.get_target(conn)) - - stack = list(execBlocks) - data_blocks = set() - for name, p in self.api.get_all_instances("Print"): - if "event" in (event := self.api.get_slots(p)) and event: - stack.append(p) - execBlocks.add(p) - - - data_conn = set() - while len(stack) > 0: - obj = stack.pop() - for data_c in self.api.get_incoming(obj, "Data_con"): - data_conn.add(data_c) - source = self.api.get_source(data_c) - if not self.api.is_instance(source, "Exec") and \ - source not in execBlocks and \ - source not in data_blocks: - stack.append(source) - data_blocks.add(source) - - for exec_item in execBlocks: - out["blocks"].append(self._render(exec_item)) - if self.api.is_instance(exec_item, "Rule"): - d = self.macro_args[self.api.get_type_name(exec_item)][1](exec_item) - out["match_files"].add(d["file"]) - out["matchers"].append(d) - for exec_c in exec_conn: - out["exec_conn"].append(self._render(exec_c)) - - for data_c in data_conn: - out["data_conn"].append(self._render(data_c)) - - for data_b in data_blocks: - out["blocks"].append(self._render(data_b)) - - print(self.template_wrap.render(out), file=stream) - - - - - - # print("with open('test.dot', 'w') as f:", file=stream) - # print(f"\tf.write({self.api.get_name(start)}.generate_dot())", file=stream) \ No newline at end of file diff --git a/examples/schedule/models/README.md b/examples/schedule/models/README.md deleted file mode 100644 index 5767d48..0000000 --- a/examples/schedule/models/README.md +++ /dev/null @@ -1,26 +0,0 @@ - -### association Exec_con - Integer gate_from; - Integer gate_to; - -### association Data_con - -### class Start [1..1] -### class End [1..*] - - -### class Match - optional Integer n; - -### class Rewrite - -### class Data_modify - String modify_dict; - -### class Loop - optional Boolean choise; - -## debugging tools - -### class Print(In_Exec, Out_Exec, In_Data) - optional Boolean event; \ No newline at end of file diff --git a/examples/schedule/models/scheduling_MM.od b/examples/schedule/models/scheduling_MM.od deleted file mode 100644 index 533d8bc..0000000 --- a/examples/schedule/models/scheduling_MM.od +++ /dev/null @@ -1,46 +0,0 @@ -abstract class Exec -abstract class In_Exec(Exec) -abstract class Out_Exec(Exec) - -association Exec_con [0..*] Out_Exec -> In_Exec [0..*] { - Integer gate_from; - Integer gate_to; -} - -abstract class Data -abstract class In_Data(Data) -abstract class Out_Data(Data) -association Data_con [0..*] Out_Data -> In_Data [0..*] - -class Start [1..1] (Out_Exec) -class End [1..*] (In_Exec) - - -abstract class Rule (In_Exec, Out_Exec, In_Data, Out_Data) -{ - String file; -} -class Match (Rule) -{ - optional Integer n; -} - -class Rewrite (Rule) - -class Data_modify(In_Data, Out_Data) -{ - String modify_dict; -} - -class Loop(In_Exec, Out_Exec, In_Data, Out_Data) -{ - optional Boolean choise; -} - -# debugging tools - -class Print(In_Exec, Out_Exec, In_Data) -{ - optional Boolean event; - optional String label; -} \ No newline at end of file diff --git a/examples/schedule/schedule_lib/__init__.py b/examples/schedule/schedule_lib/__init__.py deleted file mode 100644 index 0b826ab..0000000 --- a/examples/schedule/schedule_lib/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .data_node import DataNode -from .data_modify import DataModify -from .end import End -from .exec_node import ExecNode -from .loop import Loop -from .match import Match -from .null_node import NullNode -from .print import Print -from .rewrite import Rewrite -from .start import Start - -__all__ = ["DataNode", "End", "ExecNode", "Loop", "Match", "NullNode", "Rewrite", "Print", "DataModify", "Start"] \ No newline at end of file diff --git a/examples/schedule/schedule_lib/data.py b/examples/schedule/schedule_lib/data.py deleted file mode 100644 index 88bcb42..0000000 --- a/examples/schedule/schedule_lib/data.py +++ /dev/null @@ -1,63 +0,0 @@ -import functools -from typing import Any, Generator, Callable - - -class Data: - def __init__(self, super) -> None: - self.data: list[dict[Any, Any]] = list() - self.success: bool = False - self.super = super - - @staticmethod - def store_output(func: Callable) -> Callable: - def wrapper(self, *args, **kwargs) -> Any: - output = func(self, *args, **kwargs) - self.success = output - return output - return wrapper - - @store_output - def store_data(self, data_gen: Generator, n: int) -> bool: - self.data.clear() - if n == 0: - return True - i: int = 0 - while (match := next(data_gen, None)) is not None: - self.data.append(match) - i+=1 - if i >= n: - break - else: - if n == float("inf"): - return bool(len(self.data)) - self.data.clear() - return False - return True - - def get_super(self) -> int: - return self.super - - def replace(self, data: "Data") -> None: - self.data.clear() - self.data.extend(data.data) - - def append(self, data: Any) -> None: - self.data.append(data) - - def clear(self) -> None: - self.data.clear() - - def pop(self, index = -1) -> Any: - return self.data.pop(index) - - def empty(self) -> bool: - return len(self.data) == 0 - - def __getitem__(self, index): - return self.data[index] - - def __iter__(self): - return self.data.__iter__() - - def __len__(self): - return self.data.__len__() \ No newline at end of file diff --git a/examples/schedule/schedule_lib/data_modify.py b/examples/schedule/schedule_lib/data_modify.py deleted file mode 100644 index 0df6cba..0000000 --- a/examples/schedule/schedule_lib/data_modify.py +++ /dev/null @@ -1,26 +0,0 @@ -import functools -from typing import TYPE_CHECKING, Callable, List - -from api.od import ODAPI -from examples.schedule.RuleExecuter import RuleExecuter -from .exec_node import ExecNode -from .data_node import DataNode - - -class DataModify(DataNode): - def __init__(self, modify_dict: dict[str,str]) -> None: - DataNode.__init__(self) - self.modify_dict: dict[str,str] = modify_dict - - def input_event(self, success: bool) -> None: - if success or self.data_out.success: - self.data_out.data.clear() - for data in self.data_in.data: - self.data_out.append({self.modify_dict[key]: value for key, value in data.items() if key in self.modify_dict.keys()}) - DataNode.input_event(self, success) - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - if self.id in visited: - return - nodes.append(f"{self.id}[label=modify]") - super().generate_dot(nodes, edges, visited) diff --git a/examples/schedule/schedule_lib/data_node.py b/examples/schedule/schedule_lib/data_node.py deleted file mode 100644 index 557f297..0000000 --- a/examples/schedule/schedule_lib/data_node.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Any, Generator, List - -from examples.schedule.schedule_lib.id_generator import IdGenerator -from .data import Data - -class DataNode: - def __init__(self) -> None: - if not hasattr(self, 'id'): - self.id = IdGenerator().generate_id() - self.data_out : Data = Data(self) - self.data_in: Data | None = None - self.eventsub: list[DataNode] = list() - - def connect_data(self, data_node: "DataNode", eventsub=True) -> None: - data_node.data_in = self.data_out - if eventsub: - self.eventsub.append(data_node) - - def store_data(self, data_gen: Generator, n: int) -> None: - success: bool = self.data_out.store_data(data_gen, n) - for sub in self.eventsub: - sub.input_event(success) - - def get_input_data(self) -> list[dict[Any, Any]]: - if not self.data_in.success: - raise Exception("Invalid input data: matching has failed") - data = self.data_in.data - if len(data) == 0: - raise Exception("Invalid input data: no data present") - return data - - def input_event(self, success: bool) -> None: - self.data_out.success = success - for sub in self.eventsub: - sub.input_event(success) - - def get_id(self) -> int: - return self.id - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - visited.add(self.id) - if self.data_in is not None: - edges.append(f"{self.data_in.get_super().get_id()} -> {self.get_id()} [color = green]") - self.data_in.get_super().generate_dot(nodes, edges, visited) - for sub in self.eventsub: - sub.generate_dot(nodes, edges, visited) - diff --git a/examples/schedule/schedule_lib/end.py b/examples/schedule/schedule_lib/end.py deleted file mode 100644 index 2a008c4..0000000 --- a/examples/schedule/schedule_lib/end.py +++ /dev/null @@ -1,21 +0,0 @@ -import functools -from typing import TYPE_CHECKING, List, Callable, Generator - -from api.od import ODAPI -from .exec_node import ExecNode - -class End(ExecNode): - def __init__(self) -> None: - super().__init__(out_connections=1) - - def execute(self, od: ODAPI) -> Generator | None: - return self.terminate(od) - - @staticmethod - def terminate(od: ODAPI) -> Generator: - yield f"end:", functools.partial(lambda od:(od, ""), od) - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - if self.id in visited: - return - nodes.append(f"{self.id}[label=end]") \ No newline at end of file diff --git a/examples/schedule/schedule_lib/exec_node.py b/examples/schedule/schedule_lib/exec_node.py deleted file mode 100644 index c5d2d04..0000000 --- a/examples/schedule/schedule_lib/exec_node.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import TYPE_CHECKING, List, Callable, Generator -from api.od import ODAPI - -from .id_generator import IdGenerator - -class ExecNode: - def __init__(self, out_connections: int = 1) -> None: - from .null_node import NullNode - self.next_state: list[ExecNode] = [] - if out_connections > 0: - self.next_state = [NullNode()]*out_connections - self.id: int = IdGenerator().generate_id() - - def nextState(self) -> "ExecNode": - return self.next_state[0] - - def connect(self, next_state: "ExecNode", from_gate: int = 0, to_gate: int = 0) -> None: - if from_gate >= len(self.next_state): - raise IndexError - self.next_state[from_gate] = next_state - - def execute(self, od: ODAPI) -> Generator | None: - return None - - def get_id(self) -> int: - return self.id - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - visited.add(self.id) - for edge in self.next_state: - edges.append(f"{self.id} -> {edge.get_id()}") - for next in self.next_state: - next.generate_dot(nodes, edges, visited) - diff --git a/examples/schedule/schedule_lib/funcs.py b/examples/schedule/schedule_lib/funcs.py deleted file mode 100644 index 0b19b99..0000000 --- a/examples/schedule/schedule_lib/funcs.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Callable - -def generate_dot_wrap(func) -> Callable: - def wrapper(self, *args, **kwargs) -> str: - nodes = [] - edges = [] - self.reset_visited() - func(self, nodes, edges, *args, **kwargs) - return f"digraph G {{\n\t{"\n\t".join(nodes)}\n\t{"\n\t".join(edges)}\n}}" - return wrapper diff --git a/examples/schedule/schedule_lib/id_generator.py b/examples/schedule/schedule_lib/id_generator.py deleted file mode 100644 index d1f4b25..0000000 --- a/examples/schedule/schedule_lib/id_generator.py +++ /dev/null @@ -1,8 +0,0 @@ -from .singleton import Singleton - -class IdGenerator(metaclass=Singleton): - def __init__(self): - self.id = -1 - def generate_id(self) -> int: - self.id += 1 - return self.id \ No newline at end of file diff --git a/examples/schedule/schedule_lib/loop.py b/examples/schedule/schedule_lib/loop.py deleted file mode 100644 index 44ec5c5..0000000 --- a/examples/schedule/schedule_lib/loop.py +++ /dev/null @@ -1,57 +0,0 @@ -import functools -from random import choice -from typing import TYPE_CHECKING, Callable, List, Generator - -from api.od import ODAPI -from examples.schedule.RuleExecuter import RuleExecuter -from .exec_node import ExecNode -from .data_node import DataNode -from .data_node import Data - - -class Loop(ExecNode, DataNode): - def __init__(self, choice) -> None: - ExecNode.__init__(self, out_connections=2) - DataNode.__init__(self) - self.choice: bool = choice - self.cur_data: Data = Data(-1) - - def nextState(self) -> ExecNode: - return self.next_state[not self.data_out.success] - - def execute(self, od: ODAPI) -> Generator | None: - if self.cur_data.empty(): - self.data_out.clear() - self.data_out.success = False - DataNode.input_event(self, False) - return None - - if self.choice: - def select_data() -> Generator: - for i in range(len(self.cur_data)): - yield f"choice: {self.cur_data[i]}", functools.partial(self.select_next,od, i) - return select_data() - else: - self.select_next(od, -1) - return None - - def input_event(self, success: bool) -> None: - if (b := self.data_out.success) or success: - self.cur_data.replace(self.data_in) - self.data_out.clear() - self.data_out.success = False - if b: - DataNode.input_event(self, False) - - def select_next(self,od: ODAPI, index: int) -> tuple[ODAPI, list[str]]: - self.data_out.clear() - self.data_out.append(self.cur_data.pop(index)) - DataNode.input_event(self, True) - return (od, ["data selected"]) - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - if self.id in visited: - return - nodes.append(f"{self.id}[label=Loop]") - ExecNode.generate_dot(self, nodes, edges, visited) - DataNode.generate_dot(self, nodes, edges, visited) \ No newline at end of file diff --git a/examples/schedule/schedule_lib/match.py b/examples/schedule/schedule_lib/match.py deleted file mode 100644 index f350ba6..0000000 --- a/examples/schedule/schedule_lib/match.py +++ /dev/null @@ -1,42 +0,0 @@ -import functools -from typing import TYPE_CHECKING, Callable, List, Generator - -from api.od import ODAPI -from examples.schedule.RuleExecuter import RuleExecuter -from .exec_node import ExecNode -from .data_node import DataNode - - -class Match(ExecNode, DataNode): - def __init__(self, label: str, n: int | float) -> None: - ExecNode.__init__(self, out_connections=2) - DataNode.__init__(self) - self.label: str = label - self.n:int = n - self.rule = None - self.rule_executer : RuleExecuter - - def nextState(self) -> ExecNode: - return self.next_state[not self.data_out.success] - - def execute(self, od: ODAPI) -> Generator | None: - self.match(od) - return None - - def init_rule(self, rule, rule_executer): - self.rule = rule - self.rule_executer = rule_executer - - def match(self, od: ODAPI) -> None: - pivot = {} - if self.data_in is not None: - pivot = self.get_input_data()[0] - print(f"matching: {self.label}\n\tpivot: {pivot}") - self.store_data(self.rule_executer.match_rule(od.m, self.rule, pivot=pivot), self.n) - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - if self.id in visited: - return - nodes.append(f"{self.id}[label=M_{self.label.split("/")[-1]}_{self.n}]") - ExecNode.generate_dot(self, nodes, edges, visited) - DataNode.generate_dot(self, nodes, edges, visited) \ No newline at end of file diff --git a/examples/schedule/schedule_lib/null_node.py b/examples/schedule/schedule_lib/null_node.py deleted file mode 100644 index 2d322bb..0000000 --- a/examples/schedule/schedule_lib/null_node.py +++ /dev/null @@ -1,25 +0,0 @@ -import functools -from symtable import Function -from typing import List, Callable, Generator - -from api.od import ODAPI -from .singleton import Singleton - -from .exec_node import ExecNode - -class NullNode(ExecNode, metaclass=Singleton): - def __init__(self): - ExecNode.__init__(self, out_connections=0) - - def execute(self, od: ODAPI) -> Generator | None: - raise Exception('Null node should already have terminated the schedule') - - @staticmethod - def terminate(od: ODAPI): - return None - yield # verrrry important line, dont remove this unreachable code - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - if self.id in visited: - return - nodes.append(f"{self.id}[label=Null]") \ No newline at end of file diff --git a/examples/schedule/schedule_lib/print.py b/examples/schedule/schedule_lib/print.py deleted file mode 100644 index ed0bbc6..0000000 --- a/examples/schedule/schedule_lib/print.py +++ /dev/null @@ -1,28 +0,0 @@ -import functools -from typing import TYPE_CHECKING, Callable, List, Generator - -from api.od import ODAPI -from examples.schedule.RuleExecuter import RuleExecuter -from .exec_node import ExecNode -from .data_node import DataNode - - -class Print(ExecNode, DataNode): - def __init__(self, label: str = "") -> None: - ExecNode.__init__(self, out_connections=1) - DataNode.__init__(self) - self.label = label - - def execute(self, od: ODAPI) -> Generator | None: - self.input_event(True) - return None - - def input_event(self, success: bool) -> None: - print(f"{self.label}{self.data_in.data}") - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - if self.id in visited: - return - nodes.append(f"{self.id}[label=Print_{self.label.replace(":", "")}]") - ExecNode.generate_dot(self, nodes, edges, visited) - DataNode.generate_dot(self, nodes, edges, visited) \ No newline at end of file diff --git a/examples/schedule/schedule_lib/rewrite.py b/examples/schedule/schedule_lib/rewrite.py deleted file mode 100644 index c00ee8e..0000000 --- a/examples/schedule/schedule_lib/rewrite.py +++ /dev/null @@ -1,38 +0,0 @@ -import functools -from typing import List, Callable, Generator - -from api.od import ODAPI -from .exec_node import ExecNode -from .data_node import DataNode -from ..RuleExecuter import RuleExecuter - - -class Rewrite(ExecNode, DataNode): - def __init__(self, label: str) -> None: - ExecNode.__init__(self, out_connections=1) - DataNode.__init__(self) - self.label = label - self.rule = None - self.rule_executer : RuleExecuter - - def init_rule(self, rule, rule_executer): - self.rule = rule - self.rule_executer= rule_executer - - def execute(self, od: ODAPI) -> Generator | None: - yield "ghello", functools.partial(self.rewrite, od) - - def rewrite(self, od): - print("rewrite" + self.label) - pivot = {} - if self.data_in is not None: - pivot = self.get_input_data()[0] - self.store_data(self.rule_executer.rewrite_rule(od.m, self.rule, pivot=pivot), 1) - return ODAPI(od.state, od.m, od.mm),[f"rewrite {self.label}\n\tpivot: {pivot}\n\t{"success" if self.data_out.success else "failure"}\n"] - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - if self.id in visited: - return - nodes.append(f"{self.id}[label=R_{self.label.split("/")[-1]}]") - ExecNode.generate_dot(self, nodes, edges, visited) - DataNode.generate_dot(self, nodes, edges, visited) \ No newline at end of file diff --git a/examples/schedule/schedule_lib/start.py b/examples/schedule/schedule_lib/start.py deleted file mode 100644 index 44ed1e1..0000000 --- a/examples/schedule/schedule_lib/start.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import TYPE_CHECKING, Callable, List, Any - -from .funcs import generate_dot_wrap - -from .exec_node import ExecNode - - -class Start(ExecNode): - def __init__(self) -> None: - ExecNode.__init__(self, out_connections=1) - - def generate_dot(self, nodes: List[str], edges: List[str], visited: set[int]) -> None: - if self.id in visited: - return - nodes.append(f"{self.id}[label=start]") - super().generate_dot(nodes, edges, visited) \ No newline at end of file diff --git a/examples/schedule/templates/schedule_dot.j2 b/examples/schedule/templates/schedule_dot.j2 deleted file mode 100644 index 39d2672..0000000 --- a/examples/schedule/templates/schedule_dot.j2 +++ /dev/null @@ -1,9 +0,0 @@ -digraph G { -{% for node in nodes %} - {{ node }} -{% endfor %} - -{% for edge in edges %} - {{ edge }} -{% endfor %} -} \ No newline at end of file diff --git a/examples/schedule/templates/schedule_template.j2 b/examples/schedule/templates/schedule_template.j2 deleted file mode 100644 index a0c251c..0000000 --- a/examples/schedule/templates/schedule_template.j2 +++ /dev/null @@ -1,35 +0,0 @@ -{% macro Start(name) %} -{{ name }} = Start() -{%- endmacro %} - -{% macro End(name) %} -{{ name }} = End() -{%- endmacro %} - -{% macro Match(name, file, n) %} -{{ name }} = Match("{{ file }}", {{ n }}) -{%- endmacro %} - -{% macro Rewrite(name, file) %} -{{ name }} = Rewrite("{{ file }}") -{%- endmacro %} - -{% macro Data_modify(name, dict) %} -{{ name }} = DataModify({{ dict }}) -{%- endmacro %} - -{% macro Exec_con(name_from, name_to, gate_from, gate_to) %} -{{ name_from }}.connect({{ name_to }},{{ gate_from }},{{ gate_to }}) -{%- endmacro %} - -{% macro Data_con(name_from, name_to, event) %} -{{ name_from }}.connect_data({{ name_to }}, {{ event }}) -{%- endmacro %} - -{% macro Loop(name, choise) %} -{{ name }} = Loop({{ choise }}) -{%- endmacro %} - -{% macro Print(name, label) %} -{{ name }} = Print("{{ label }}") -{%- endmacro %} \ No newline at end of file diff --git a/examples/schedule/templates/schedule_template_wrap.j2 b/examples/schedule/templates/schedule_template_wrap.j2 deleted file mode 100644 index 389f2c2..0000000 --- a/examples/schedule/templates/schedule_template_wrap.j2 +++ /dev/null @@ -1,47 +0,0 @@ -from examples.schedule.schedule_lib import * - -class Schedule: - def __init__(self, rule_executer): - self.start: Start - self.cur: ExecNode = None - self.rule_executer = rule_executer - - def __call__(self, od): - self.cur = self.cur.nextState() - while not isinstance(self.cur, NullNode): - action_gen = self.cur.execute(od) - if action_gen is not None: - # if (action_gen := self.cur.execute(od)) is not None: - return action_gen - self.cur = self.cur.nextState() - return NullNode.terminate(od) - - @staticmethod - def get_matchers(): - return [ - {% for file in match_files %} - "{{ file }}.od", - {% endfor %} - ] - - def init_schedule(self, matchers): - {% for block in blocks%} - {{ block }} - {% endfor %} - - {% for conn in exec_conn%} - {{ conn }} - {% endfor %} - {% for conn_d in data_conn%} - {{ conn_d }} - {% endfor %} - self.start = {{ start }} - self.cur = {{ start }} - - {% for match in matchers %} - {{ match["name"] }}.init_rule(matchers["{{ match["file"] }}.od"], self.rule_executer) - {% endfor %} - return None - - def generate_dot(self, *args, **kwargs): - return self.start.generate_dot(*args, **kwargs) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2105b38..179f66d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ lark==1.1.9 -jinja2==3.1.4 \ No newline at end of file +jinja2==3.1.4 +git+https://msdl.uantwerpen.be/git/jexelmans/drawio2py \ No newline at end of file diff --git a/transformation/schedule/Tests/Test_meta_model.py b/transformation/schedule/Tests/Test_meta_model.py new file mode 100644 index 0000000..47392c1 --- /dev/null +++ b/transformation/schedule/Tests/Test_meta_model.py @@ -0,0 +1,489 @@ +import io +import os +import sys +import unittest + +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")) +) + +from icecream import ic + +from api.od import ODAPI +from bootstrap.scd import bootstrap_scd +from examples.schedule import rule_schedular +from examples.schedule.rule_schedular import ScheduleActionGenerator +from state.devstate import DevState +from transformation.ramify import ramify +from util import loader + + +class Test_Meta_Model(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.dir = os.path.dirname(__file__) + state = DevState() + scd_mmm = bootstrap_scd(state) + with open(f"{cls.dir}/models/mm_petrinet.od") as file: + mm_s = file.read() + with open(f"{cls.dir}/models/m_petrinet.od") as file: + m_s = file.read() + mm = loader.parse_and_check(state, mm_s, scd_mmm, "mm") + m = loader.parse_and_check(state, m_s, mm, "m") + mm_rt_ramified = ramify(state, mm) + cls.model_param = (state, m, mm) + cls.generator_param = (state, mm, mm_rt_ramified) + + def setUp(self): + self.model = ODAPI(*self.model_param) + self.out = io.StringIO() + self.generator = ScheduleActionGenerator( + *self.generator_param, + directory=self.dir + "/models", + verbose=True, + outstream=self.out, + ) + + def _test_conformance( + self, file: str, expected_substr_err: dict[tuple[str, str], list[list[str]]] + ) -> None: + try: + self.generator.load_schedule(f"schedule/{file}") + errors = self.out.getvalue().split("\u25b8")[1:] + ic(errors) + if len(errors) != len(expected_substr_err.keys()): + ic("len total errors") + assert len(errors) == len(expected_substr_err.keys()) + for err in errors: + error_lines = err.strip().split("\n") + line = error_lines[0] + for key_pattern in expected_substr_err.keys(): + if (key_pattern[0] in line) and (key_pattern[1] in line): + key = key_pattern + break + else: + ic("no matching key") + ic(line) + assert False + expected = expected_substr_err[key] + if (len(error_lines) - 1) != len(expected): + ic("len substr errors") + ic(line) + assert (len(error_lines) - 1) == len(expected) + it = error_lines.__iter__() + it.__next__() + for err_line in it: + if not any( + all(exp in err_line for exp in line_exp) + for line_exp in expected + ): + ic("wrong substr error") + ic(line) + ic(error_lines) + assert False + expected_substr_err.pop(key) + except AssertionError: + raise + except Exception as e: + ic(e) + assert False + + def test_no_start(self): + self._test_conformance("no_start.od", {("Start", "Cardinality"): []}) + + def test_no_end(self): + self._test_conformance("no_end.od", {("End", "Cardinality"): []}) + + def test_multiple_start(self): + self._test_conformance("multiple_start.od", {("Start", "Cardinality"): []}) + + def test_multiple_end(self): + self._test_conformance("multiple_end.od", {("End", "Cardinality"): []}) + + def test_connections_start(self): + self._test_conformance( + "connections_start.od", + { + ("Start", "start"): [ + ["input exec", "foo_in", "exist"], + ["output exec", "out", "multiple"], + ["output exec", "foo_out", "exist"], + ["input data", "in", "exist"], + ] + }, + ) + + def test_connections_end(self): + self._test_conformance( + "connections_end.od", + { + ("End", "end"): [ + ["input exec", "foo_in", "exist"], + ["output exec", "foo_out", "exist"], + ["input data", "in", "multiple"], + ["input data", "out2", "exist"], + ["output data", "out", "exist"], + ] + }, + ) + + def test_connections_match(self): + self._test_conformance( + "connections_match.od", + { + ("Match", "m_foo"): [ + ["input exec", "foo_in", "exist"], + ["output exec", "foo", "exist"], + ["output exec", "fail", "multiple"], + ["input data", "foo_in", "exist"], + ["input data", "in", "multiple"], + ["output data", "foo_out", "exist"], + ] + }, + ) + + def test_connections_rewrite(self): + self._test_conformance( + "connections_rewrite.od", + { + ("Rewrite", "r_foo1"): [ + ["input exec", "foo_in", "exist"], + ["output exec", "foo", "exist"], + ], + ("Rewrite", "r_foo2"): [ + ["output exec", "out", "multiple"], + ["input data", "foo_in", "exist"], + ["input data", "in", "multiple"], + ["output data", "foo_out", "exist"], + ], + }, + ) + + def test_connections_action(self): + self._test_conformance( + "connections_action.od", + { + ("Action", "a_foo1"): [ + ["input exec", "foo_in", "exist"], + ["output exec", "out", "multiple"], + ["output exec", "foo", "exist"], + ["input data", "in1", "multiple"], + ], + ("Action", "a_foo2"): [ + ["input exec", "in", "exist"], + ["output exec", "out3", "multiple"], + ["output exec", "out", "exist"], + ["input data", "in", "exist"], + ["output data", "out", "exist"], + ], + }, + ) + + def test_connections_modify(self): + self._test_conformance( + "connections_modify.od", + { + ("Modify", "m_foo"): [ + ["input exec", "in", "exist"], + ["input exec", "in", "exist"], + ["output exec", "out", "exist"], + ["input data", "foo_in", "exist"], + ["output data", "foo_out", "exist"], + ["input data", "in", "multiple"], + ] + }, + ) + + def test_connections_merge(self): + self._test_conformance( + "connections_merge.od", + { + ("Merge", "m_foo"): [ + ["input exec", "in", "exist"], + ["input exec", "in", "exist"], + ["output exec", "out", "exist"], + ["input data", "foo_in", "exist"], + ["output data", "foo_out", "exist"], + ["input data", "in2", "multiple"], + ] + }, + ) + + def test_connections_store(self): + self._test_conformance( + "connections_store.od", + { + ("Store", "s_foo"): [ + ["input exec", "foo", "exist"], + ["output exec", "out", "multiple"], + ["output exec", "foo", "exist"], + ["input data", "foo_in", "exist"], + ["output data", "foo_out", "exist"], + ["input data", "2", "multiple"], + ], + }, + ) + + def test_connections_schedule(self): + self._test_conformance( + "connections_schedule.od", + { + ("Schedule", "s_foo"): [ + ["output exec", "out", "multiple"], + ["input data", "in2", "multiple"], + ] + }, + ) + + def test_connections_loop(self): + self._test_conformance( + "connections_loop.od", + { + ("Loop", "l_foo"): [ + ["input exec", "foo_in", "exist"], + ["output exec", "out", "multiple"], + ["output exec", "foo", "exist"], + ["input data", "foo_in", "exist"], + ["output data", "foo_out", "exist"], + ["input data", "in", "multiple"], + ] + }, + ) + + def test_connections_print(self): + self._test_conformance( + "connections_print.od", + { + ("Print", "p_foo"): [ + ["input exec", "foo_in", "exist"], + ["output exec", "out", "multiple"], + ["output exec", "foo", "exist"], + ["input data", "foo_in", "exist"], + ["output data", "out", "exist"], + ["input data", "in", "multiple"], + ] + }, + ) + + def test_fields_start(self): + self._test_conformance( + "fields_start.od", + { + ("Start", "Cardinality"): [], + ("Start", "string"): [ + ["Unexpected type", "ports_exec_out", "str"], + ["Unexpected type", "ports_data_out", "str"], + ], + ("Start", '"int"'): [ + ["Unexpected type", "ports_exec_out", "int"], + ["Unexpected type", "ports_data_out", "int"], + ], + ("Start", "tuple"): [ + ["Unexpected type", "ports_exec_out", "tuple"], + ["Unexpected type", "ports_data_out", "tuple"], + ], + ("Start", "dict"): [ + ["Unexpected type", "ports_exec_out", "dict"], + ["Unexpected type", "ports_data_out", "dict"], + ], + ("Start", "none"): [ + ["Unexpected type", "ports_exec_out", "NoneType"], + ["Unexpected type", "ports_data_out", "NoneType"], + ], + ("Start", "invalid"): [ + ["Invalid python", "ports_exec_out"], + ["Invalid python", "ports_data_out"], + ], + ("Start", "subtype"): [ + ["Unexpected type", "ports_exec_out", "list"], + ["Unexpected type", "ports_data_out", "list"], + ], + ("Start", "code"): [ + ["Unexpected type", "ports_exec_out"], + ["Unexpected type", "ports_data_out"], + ], + }, + ) + + def test_fields_end(self): + self._test_conformance( + "fields_end.od", + { + ("End", "Cardinality"): [], + ("End", "string"): [ + ["Unexpected type", "ports_exec_in", "str"], + ["Unexpected type", "ports_data_in", "str"], + ], + ("End", '"int"'): [ + ["Unexpected type", "ports_exec_in", "int"], + ["Unexpected type", "ports_data_in", "int"], + ], + ("End", "tuple"): [ + ["Unexpected type", "ports_exec_in", "tuple"], + ["Unexpected type", "ports_data_in", "tuple"], + ], + ("End", "dict"): [ + ["Unexpected type", "ports_exec_in", "dict"], + ["Unexpected type", "ports_data_in", "dict"], + ], + ("End", "none"): [ + ["Unexpected type", "ports_exec_in", "NoneType"], + ["Unexpected type", "ports_data_in", "NoneType"], + ], + ("End", "invalid"): [ + ["Invalid python", "ports_exec_in"], + ["Invalid python", "ports_data_in"], + ], + ("End", "subtype"): [ + ["Unexpected type", "ports_exec_in", "list"], + ["Unexpected type", "ports_data_in", "list"], + ], + ("End", "code"): [ + ["Unexpected type", "ports_exec_in"], + ["Unexpected type", "ports_data_in"], + ], + }, + ) + + def test_fields_action(self): + self._test_conformance( + "fields_action.od", + { + ("cardinality", "Action_action"): [], + ("Action", "string"): [ + ["Unexpected type", "ports_exec_out", "str"], + ["Unexpected type", "ports_exec_in", "str"], + ["Unexpected type", "ports_data_out", "str"], + ["Unexpected type", "ports_data_in", "str"], + ], + ("Action", '"int"'): [ + ["Unexpected type", "ports_exec_out", "int"], + ["Unexpected type", "ports_exec_in", "int"], + ["Unexpected type", "ports_data_out", "int"], + ["Unexpected type", "ports_data_in", "int"], + ], + ("Action", "tuple"): [ + ["Unexpected type", "ports_exec_out", "tuple"], + ["Unexpected type", "ports_exec_in", "tuple"], + ["Unexpected type", "ports_data_out", "tuple"], + ["Unexpected type", "ports_data_in", "tuple"], + ], + ("Action", "dict"): [ + ["Unexpected type", "ports_exec_out", "dict"], + ["Unexpected type", "ports_exec_in", "dict"], + ["Unexpected type", "ports_data_out", "dict"], + ["Unexpected type", "ports_data_in", "dict"], + ], + ("Action", "none"): [ + ["Unexpected type", "ports_exec_out", "NoneType"], + ["Unexpected type", "ports_exec_in", "NoneType"], + ["Unexpected type", "ports_data_out", "NoneType"], + ["Unexpected type", "ports_data_in", "NoneType"], + ], + ("Action", '"invalid"'): [ + ["Invalid python", "ports_exec_out"], + ["Invalid python", "ports_exec_in"], + ["Invalid python", "ports_data_out"], + ["Invalid python", "ports_data_in"], + ], + ("Action_action", "invalid_action"): [], + ("Action", "subtype"): [ + ["Unexpected type", "ports_exec_out", "list"], + ["Unexpected type", "ports_exec_in", "list"], + ["Unexpected type", "ports_data_out", "list"], + ["Unexpected type", "ports_data_in", "list"], + ], + ("Action", "code"): [ + ["Unexpected type", "ports_exec_out"], + ["Unexpected type", "ports_exec_in"], + ["Unexpected type", "ports_data_out"], + ["Unexpected type", "ports_data_in"], + ], + }, + ) + + def test_fields_modify(self): + self._test_conformance( + "fields_modify.od", + { + ("Modify", "string"): [ + ["Unexpected type", "rename", "str"], + ["Unexpected type", "delete", "str"], + ], + ("Modify", "list"): [["Unexpected type", "rename", "list"]], + ("Modify", "set"): [["Unexpected type", "rename", "set"]], + ("Modify", "tuple"): [ + ["Unexpected type", "rename", "tuple"], + ["Unexpected type", "delete", "tuple"], + ], + ("Modify", "dict"): [["Unexpected type", "delete", "dict"]], + ("Modify", "none"): [ + ["Unexpected type", "rename", "NoneType"], + ["Unexpected type", "delete", "NoneType"], + ], + ("Modify", "invalid"): [ + ["Invalid python", "rename"], + ["Invalid python", "delete"], + ], + ("Modify", "subtype"): [ + ["Unexpected type", "rename", "dict"], + ["Unexpected type", "delete", "list"], + ], + ("Modify", "code"): [ + ["Unexpected type", "rename"], + ["Unexpected type", "delete"], + ], + ("Modify", "joined"): [["rename", "delete", "disjoint"]], + }, + ) + + def test_fields_merge(self): + self._test_conformance( + "fields_merge.od", + { + ("cardinality", "Merge_ports_data_in"): [], + ("Merge", "string"): [["Unexpected type", "ports_data_in", "str"]], + ("Merge", "tuple"): [["Unexpected type", "ports_data_in", "tuple"]], + ("Merge", "dict"): [["Unexpected type", "ports_data_in", "dict"]], + ("Merge", "none"): [["Unexpected type", "ports_data_in", "NoneType"]], + ("Merge", "invalid"): [["Invalid python", "ports_data_in"]], + ("Merge", "subtype"): [["Unexpected type", "ports_data_in", "list"]], + ("Merge", "code"): [["Unexpected type", "ports_data_in"]], + ("Merge", "no"): [["Missing", "slot", "ports_data_in"]], + }, + ) + + def test_fields_store(self): + self._test_conformance( + "fields_store.od", + { + ("cardinality", "Store_ports"): [], + ("Store", "string"): [["Unexpected type", "ports", "str"]], + ("Store", "tuple"): [["Unexpected type", "ports", "tuple"]], + ("Store", "dict"): [["Unexpected type", "ports", "dict"]], + ("Store", "none"): [["Unexpected type", "ports", "NoneType"]], + ("Store", "invalid"): [["Invalid python", "ports"]], + ("Store", "subtype"): [["Unexpected type", "ports", "list"]], + ("Store", "code"): [["Unexpected type", "ports"]], + ("Store", "no"): [["Missing", "slot", "ports"]], + }, + ) + + def test_fields_print(self): + self._test_conformance( + "fields_print.od", + { + ("Print_custom", "list_custom"): [["Unexpected type", "custom", "list"]], + ("Print_custom", "set_custom"): [["Unexpected type", "custom", "set"]], + ("Print_custom", "tuple_custom"): [["Unexpected type", "custom", "tuple"]], + ("Print_custom", "dict_custom"): [["Unexpected type", "custom", "dict"]], + ("Print_custom", "none_custom"): [["Unexpected type", "custom", "NoneType"]], + ("Print_custom", "invalid_custom"): [["Invalid python", "custom"]], + ("Print_custom", "subtype_custom"): [["Unexpected type", "custom", "list"]], + ("Print_custom", "code_custom"): [["Unexpected type", "custom"]], + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/transformation/schedule/Tests/Test_xmlparser.py b/transformation/schedule/Tests/Test_xmlparser.py new file mode 100644 index 0000000..0530ee7 --- /dev/null +++ b/transformation/schedule/Tests/Test_xmlparser.py @@ -0,0 +1,45 @@ +import io +import os +import unittest + +from transformation.schedule import rule_scheduler +from transformation.schedule.rule_scheduler import RuleSchedular +from state.devstate import DevState + + +class MyTestCase(unittest.TestCase): + def setUp(self): + state = DevState() + self.generator = RuleSchedular(state, "", "") + + def test_empty(self): + try: + self.generator.generate_schedule( + f"{os.path.dirname(__file__)}/drawio/Empty.drawio" + ) + # buffer = io.BytesIO() + # self.generator.generate_dot(buffer) + except Exception as e: + assert False + + def test_simple(self): + try: + self.generator.generate_schedule( + f"{os.path.dirname(__file__)}/drawio/StartToEnd.drawio" + ) + # buffer = io.BytesIO() + # self.generator.generate_dot(buffer) + except Exception as e: + assert False + + # def test_unsupported(self): + # try: + # self.generator.generate_schedule("Tests/drawio/Unsupported.drawio") + # # buffer = io.BytesIO() + # # self.generator.generate_dot(buffer) + # except Exception as e: + # assert(False) + + +if __name__ == "__main__": + unittest.main() diff --git a/transformation/schedule/Tests/drawio/Empty.drawio b/transformation/schedule/Tests/drawio/Empty.drawio new file mode 100644 index 0000000..b025fbc --- /dev/null +++ b/transformation/schedule/Tests/drawio/Empty.drawio @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/transformation/schedule/Tests/drawio/StartToEnd.drawio b/transformation/schedule/Tests/drawio/StartToEnd.drawio new file mode 100644 index 0000000..c381120 --- /dev/null +++ b/transformation/schedule/Tests/drawio/StartToEnd.drawio @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/transformation/schedule/Tests/drawio/Unsupported.drawio b/transformation/schedule/Tests/drawio/Unsupported.drawio new file mode 100644 index 0000000..a9cf0fb --- /dev/null +++ b/transformation/schedule/Tests/drawio/Unsupported.drawio @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/transformation/schedule/Tests/models/m_petrinet.od b/transformation/schedule/Tests/models/m_petrinet.od new file mode 100644 index 0000000..f93a58b --- /dev/null +++ b/transformation/schedule/Tests/models/m_petrinet.od @@ -0,0 +1,22 @@ +p0:PNPlace +p1:PNPlace + +t0:PNTransition +:arc (p0 -> t0) +:arc (t0 -> p1) + +t1:PNTransition +:arc (p1 -> t1) +:arc (t1 -> p0) + +p0s:PNPlaceState { + numTokens = 1; +} + +:pn_of (p0s -> p0) + +p1s:PNPlaceState { + numTokens = 0; +} + +:pn_of (p1s -> p1) diff --git a/transformation/schedule/Tests/models/mm_petrinet.od b/transformation/schedule/Tests/models/mm_petrinet.od new file mode 100644 index 0000000..22986c3 --- /dev/null +++ b/transformation/schedule/Tests/models/mm_petrinet.od @@ -0,0 +1,31 @@ +# Places, transitions, arcs (and only one kind of arc) + +PNConnectable:Class { abstract = True; } + +arc:Association (PNConnectable -> PNConnectable) + +PNPlace:Class +PNTransition:Class + +# inhibitor arc +inh_arc:Association (PNPlace -> PNTransition) + +:Inheritance (PNPlace -> PNConnectable) +:Inheritance (PNTransition -> PNConnectable) + +# A place has a number of tokens, and that's it. + +PNPlaceState:Class +PNPlaceState_numTokens:AttributeLink (PNPlaceState -> Integer) { + name = "numTokens"; + optional = False; + constraint = `"numTokens cannot be negative" if get_value(get_target(this)) < 0 else None`; +} + +pn_of:Association (PNPlaceState -> PNPlace) { + # one-to-one + source_lower_cardinality = 1; + source_upper_cardinality = 1; + target_lower_cardinality = 1; + target_upper_cardinality = 1; +} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/rules/transitions.od b/transformation/schedule/Tests/models/rules/transitions.od new file mode 100644 index 0000000..1b87f1d --- /dev/null +++ b/transformation/schedule/Tests/models/rules/transitions.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `True`; +} +:RAM_pn_of (ps -> p) + +# An incoming arc from that place to our transition: + +t:RAM_PNTransition + +:RAM_arc (p -> t) diff --git a/transformation/schedule/Tests/models/schedule/connections_action.od b/transformation/schedule/Tests/models/schedule/connections_action.od new file mode 100644 index 0000000..02cb3cf --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_action.od @@ -0,0 +1,62 @@ +start:Start { + ports_data_out = `["1", "2", "3"]`; +} + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} + +a_void:Action{ + ports_data_in = `["in1", "in2"]`; + ports_data_out = `["out1", "out2"]`; + action=`print("hello foo1")`; +} + +a_foo1:Action{ + ports_data_in = `["in1", "in2"]`; + ports_data_out = `["out1", "out2"]`; + action=`print("hello foo1")`; +} + +a_foo2:Action{ + ports_exec_in = `["in2"]`; + ports_exec_out = `["out2", "out3"]`; + action=`print("hello foo2")`; +} + +end:End { + ports_data_in = `["1", "2", "3"]`; +} + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> a_foo1) {from="success";to="in";} +:Conn_exec (m2 -> a_foo1) {from="fail";to="in";} +:Conn_exec (m3 -> a_foo1) {from="success";to="foo_in";} +:Conn_exec (m3 -> a_foo2) {from="fail";to="in2";} + +:Conn_exec (a_foo1 -> a_foo2) {from="out";to="in";} +:Conn_exec (a_foo1 -> a_foo2) {from="out";to="in2";} +:Conn_exec (a_foo1 -> a_foo2) {from="foo";to="in2";} +:Conn_exec (a_foo2 -> end) {from="out";to="in";} +:Conn_exec (a_foo2 -> end) {from="out2";to="in";} +:Conn_exec (a_foo2 -> end) {from="out3";to="in";} +:Conn_exec (a_foo2 -> end) {from="out3";to="in";} + +:Conn_data (start -> a_foo2) {from="1";to="in";} +:Conn_data (a_foo2-> m2) {from="out";to="in";} + +:Conn_data (start -> a_foo1) {from="1";to="in1";} +:Conn_data (start -> a_foo1) {from="2";to="in1";} +:Conn_data (start -> a_foo1) {from="3";to="in2";} +:Conn_data (a_foo1 -> end) {from="out1";to="1";} +:Conn_data (a_foo1 -> end) {from="out1";to="2";} +:Conn_data (a_foo1 -> end) {from="out2";to="3";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_end.od b/transformation/schedule/Tests/models/schedule/connections_end.od new file mode 100644 index 0000000..0bc355e --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_end.od @@ -0,0 +1,31 @@ +start:Start + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} +end:End { + ports_exec_in = `["out", "in"]`; + ports_data_in = `["out", "in"]`; +} + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> end) {from="success";to="in";} +:Conn_exec (m2 -> end) {from="fail";to="out";} +:Conn_exec (m3 -> end) {from="success";to="out";} +:Conn_exec (m3 -> end) {from="fail";to="foo_in";} +:Conn_exec (end -> m) {from="foo_out";to="in";} + +:Conn_data (m -> end) {from="out";to="in";} +:Conn_data (m2 -> end) {from="out";to="in";} +:Conn_data (m3 -> end) {from="out";to="out";} +:Conn_data (m3 -> end) {from="out";to="out2";} +:Conn_data (end -> m) {from="out";to="in";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_loop.od b/transformation/schedule/Tests/models/schedule/connections_loop.od new file mode 100644 index 0000000..922281a --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_loop.od @@ -0,0 +1,44 @@ +start:Start { + ports_data_out = `["1", "2", "3"]`; +} + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} + +l:Loop +l_foo:Loop +l_void:Loop + +end:End { + ports_data_in = `["1", "2", "3"]`; +} + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> l_foo) {from="success";to="in";} +:Conn_exec (m2 -> l_foo) {from="fail";to="in";} +:Conn_exec (m3 -> l_foo) {from="success";to="foo_in";} + +:Conn_exec (l_foo -> l_foo) {from="out";to="in";} +:Conn_exec (l_foo -> end) {from="out";to="in";} +:Conn_exec (l_foo -> end) {from="it";to="in";} +:Conn_exec (l_foo -> end) {from="foo";to="in";} + +:Conn_data (start -> l) {from="1";to="in";} +:Conn_data (l -> m2) {from="out";to="in";} + +:Conn_data (start -> l_foo) {from="1";to="in";} +:Conn_data (start -> l_foo) {from="2";to="in";} +:Conn_data (start -> l_foo) {from="3";to="foo_in";} +:Conn_data (l_foo -> end) {from="out";to="1";} +:Conn_data (l_foo -> end) {from="out";to="2";} +:Conn_data (l_foo -> end) {from="foo_out";to="3";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_match.od b/transformation/schedule/Tests/models/schedule/connections_match.od new file mode 100644 index 0000000..63a7f44 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_match.od @@ -0,0 +1,49 @@ +start:Start { + ports_data_out = `["1", "2", "3"]`; +} + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} + +m_foo:Match{ + file="rules/transition.od"; +} + +m_void:Match{ + file="rules/transition.od"; +} + +end:End { + ports_data_in = `["1", "2", "3"]`; +} + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> m_foo) {from="success";to="in";} +:Conn_exec (m2 -> m_foo) {from="fail";to="in";} +:Conn_exec (m3 -> m_foo) {from="success";to="foo_in";} +:Conn_exec (m3 -> m_foo) {from="fail";to="in";} + +:Conn_exec (m_foo -> end) {from="fail";to="in";} +:Conn_exec (m_foo -> end) {from="success";to="in";} +:Conn_exec (m_foo -> end) {from="fail";to="in";} +:Conn_exec (m_foo -> end) {from="foo";to="in";} + +:Conn_data (start -> m) {from="1";to="in";} +:Conn_data (m -> m2) {from="out";to="in";} + +:Conn_data (start -> m_foo) {from="1";to="in";} +:Conn_data (start -> m_foo) {from="2";to="in";} +:Conn_data (start -> m_foo) {from="3";to="foo_in";} +:Conn_data (m_foo -> end) {from="out";to="1";} +:Conn_data (m_foo -> end) {from="out";to="2";} +:Conn_data (m_foo -> end) {from="foo_out";to="3";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_merge.od b/transformation/schedule/Tests/models/schedule/connections_merge.od new file mode 100644 index 0000000..b564525 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_merge.od @@ -0,0 +1,42 @@ +start:Start { + ports_data_out = `["1", "2", "3"]`; +} + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} + +m_foo:Merge { + ports_data_in = `["in1", "in2"]`; +} + +m_void:Merge { + ports_data_in = `["in1", "in2"]`; +} + +end:End { + ports_data_in = `["1", "2", "3"]`; +} + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> m_foo) {from="success";to="in";} +:Conn_exec (m2 -> m_foo) {from="fail";to="in";} + +:Conn_exec (m_foo -> end) {from="out";to="in";} + +:Conn_data (start -> m_foo) {from="1";to="in1";} +:Conn_data (start -> m_foo) {from="1";to="in2";} +:Conn_data (start -> m_foo) {from="2";to="in2";} +:Conn_data (start -> m_foo) {from="3";to="foo_in";} +:Conn_data (m_foo -> end) {from="out";to="1";} +:Conn_data (m_foo -> end) {from="out";to="2";} +:Conn_data (m_foo -> end) {from="foo_out";to="3";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_modify.od b/transformation/schedule/Tests/models/schedule/connections_modify.od new file mode 100644 index 0000000..f3eebdc --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_modify.od @@ -0,0 +1,41 @@ +start:Start { + ports_data_out = `["1", "2", "3"]`; +} + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} + +m_foo:Modify +m_void:Modify + +mo:Modify + +end:End { + ports_data_in = `["1", "2", "3"]`; +} + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> m_foo) {from="success";to="in";} +:Conn_exec (m2 -> m_foo) {from="fail";to="in";} + +:Conn_exec (m_foo -> end) {from="out";to="in";} + +:Conn_data (start -> mo) {from="1";to="in";} +:Conn_data (mo -> m2) {from="out";to="in";} + +:Conn_data (start -> m_foo) {from="1";to="in";} +:Conn_data (start -> m_foo) {from="2";to="in";} +:Conn_data (start -> m_foo) {from="3";to="foo_in";} +:Conn_data (m_foo -> end) {from="out";to="1";} +:Conn_data (m_foo -> end) {from="out";to="2";} +:Conn_data (m_foo -> end) {from="foo_out";to="3";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_print.od b/transformation/schedule/Tests/models/schedule/connections_print.od new file mode 100644 index 0000000..9bf9126 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_print.od @@ -0,0 +1,41 @@ +start:Start { + ports_data_out = `["1", "2", "3"]`; +} + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} + +p_foo:Print +p_void:Print + +p:Print + +end:End + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> p_foo) {from="success";to="in";} +:Conn_exec (m2 -> p_foo) {from="fail";to="in";} +:Conn_exec (m3 -> p_foo) {from="success";to="foo_in";} +:Conn_exec (m3 -> p) {from="fail";to="in";} +:Conn_exec (p -> end) {from="out";to="in";} + +:Conn_exec (p_foo -> p_foo) {from="out";to="in";} +:Conn_exec (p_foo -> end) {from="out";to="in";} +:Conn_exec (p_foo -> end) {from="foo";to="in";} + +:Conn_data (start -> p) {from="1";to="in";} + +:Conn_data (start -> p_foo) {from="1";to="in";} +:Conn_data (start -> p_foo) {from="2";to="in";} +:Conn_data (start -> p_foo) {from="3";to="foo_in";} +:Conn_data (p_foo -> m2) {from="out";to="in";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_rewrite.od b/transformation/schedule/Tests/models/schedule/connections_rewrite.od new file mode 100644 index 0000000..7e1b018 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_rewrite.od @@ -0,0 +1,52 @@ +start:Start { + ports_data_out = `["1", "2", "3"]`; +} +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} + +r_foo1:Rewrite{ + file="rules/transition.od"; +} + +r_foo2:Rewrite{ + file="rules/transition.od"; +} +r_void:Rewrite{ + file="rules/transition.od"; +} + +end:End { + ports_data_in = `["1", "2", "3"]`; +} + + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> r_foo1) {from="success";to="in";} +:Conn_exec (m2 -> r_foo1) {from="fail";to="in";} +:Conn_exec (m3 -> r_foo1) {from="success";to="foo_in";} +:Conn_exec (m3 -> r_foo1) {from="fail";to="in";} + +:Conn_exec (r_foo1 -> r_foo2) {from="out";to="in";} +:Conn_exec (r_foo1 -> end) {from="foo";to="in";} +:Conn_exec (r_foo2 -> end) {from="out";to="in";} +:Conn_exec (r_foo2 -> end) {from="out";to="in";} + +:Conn_data (start -> r_foo1) {from="1";to="in";} +:Conn_data (r_foo1-> m2) {from="out";to="in";} + +:Conn_data (start -> r_foo2) {from="1";to="in";} +:Conn_data (start -> r_foo2) {from="2";to="in";} +:Conn_data (start -> r_foo2) {from="3";to="foo_in";} +:Conn_data (r_foo2 -> end) {from="out";to="1";} +:Conn_data (r_foo2 -> end) {from="out";to="2";} +:Conn_data (r_foo2 -> end) {from="foo_out";to="3";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_schedule.od b/transformation/schedule/Tests/models/schedule/connections_schedule.od new file mode 100644 index 0000000..a2e3c25 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_schedule.od @@ -0,0 +1,50 @@ +start:Start { + ports_data_out = `["1", "2", "3"]`; +} + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} + +s_foo:Schedule{ + file="hello.od"; +} + +s_void:Schedule{ + file="hello.od"; +} + +end:End { + ports_data_in = `["1", "2", "3"]`; +} + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> s_foo) {from="success";to="in";} +:Conn_exec (m2 -> s_foo) {from="fail";to="in";} +:Conn_exec (m3 -> s_foo) {from="success";to="foo";} +:Conn_exec (m3 -> s_foo) {from="fail";to="foo2";} + +:Conn_exec (s_foo -> s_foo) {from="out";to="in";} +:Conn_exec (s_foo -> s_foo) {from="out";to="in2";} +:Conn_exec (s_foo -> s_foo) {from="foo";to="foo3";} +:Conn_exec (s_foo -> end) {from="out4";to="in";} +:Conn_exec (s_foo -> end) {from="out2";to="in";} +:Conn_exec (s_foo -> end) {from="out5";to="in";} +:Conn_exec (s_foo -> end) {from="out3";to="in";} + +:Conn_data (start -> s_foo) {from="1";to="in1";} +:Conn_data (start -> s_foo) {from="1";to="in2";} +:Conn_data (start -> s_foo) {from="2";to="in2";} +:Conn_data (start -> s_foo) {from="3";to="foo_in";} +:Conn_data (s_foo -> end) {from="out";to="1";} +:Conn_data (s_foo -> end) {from="out";to="2";} +:Conn_data (s_foo -> end) {from="foo_out";to="3";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_start.od b/transformation/schedule/Tests/models/schedule/connections_start.od new file mode 100644 index 0000000..2ade389 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_start.od @@ -0,0 +1,27 @@ +start:Start { + ports_exec_out = `["out", "in"]`; + ports_data_out = `["out", "in"]`; +} + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} +end:End + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (start -> m) {from="in";to="in";} +:Conn_exec (start -> m) {from="foo_out";to="in";} +:Conn_exec (m -> start) {from="fail";to="foo_in";} +:Conn_exec (m -> end) {from="success";to="in";} + +:Conn_data (start -> m) {from="out";to="in";} +:Conn_data (start -> m2) {from="out";to="in";} +:Conn_data (start -> m3) {from="in";to="in";} +:Conn_data (m -> start) {from="out";to="in";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/connections_store.od b/transformation/schedule/Tests/models/schedule/connections_store.od new file mode 100644 index 0000000..a3e4477 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/connections_store.od @@ -0,0 +1,47 @@ +start:Start { + ports_data_out = `["1", "2", "3"]`; +} + +m:Match{ + file="rules/transition.od"; +} +m2:Match{ + file="rules/transition.od"; +} +m3:Match{ + file="rules/transition.od"; +} + +s_foo:Store { + ports = `["1", "2", "3"]`; +} + +s_void:Store { + ports = `["1", "2", "3"]`; +} + +end:End { + ports_data_in = `["1", "2", "3"]`; +} + +:Conn_exec (start -> m) {from="out";to="in";} +:Conn_exec (m -> m2) {from="fail";to="in";} +:Conn_exec (m -> m3) {from="success";to="in";} + +:Conn_exec (m2 -> s_foo) {from="success";to="in";} +:Conn_exec (m2 -> s_foo) {from="fail";to="in";} +:Conn_exec (m3 -> s_foo) {from="success";to="1";} +:Conn_exec (m3 -> s_foo) {from="fail";to="foo";} + +:Conn_exec (s_foo -> end) {from="out";to="in";} +:Conn_exec (s_foo -> s_foo) {from="1";to="2";} +:Conn_exec (s_foo -> end) {from="out";to="in";} +:Conn_exec (s_foo -> s_foo) {from="foo";to="2";} + +:Conn_data (start -> s_foo) {from="1";to="1";} +:Conn_data (start -> s_foo) {from="1";to="2";} +:Conn_data (start -> s_foo) {from="2";to="2";} +:Conn_data (start -> s_foo) {from="3";to="foo_in";} +:Conn_data (s_foo -> end) {from="out";to="1";} +:Conn_data (s_foo -> end) {from="out";to="2";} +:Conn_data (s_foo -> end) {from="foo_out";to="3";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/fields_action.od b/transformation/schedule/Tests/models/schedule/fields_action.od new file mode 100644 index 0000000..6770059 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/fields_action.od @@ -0,0 +1,83 @@ +string:Action { + ports_exec_in = `'["out", "in"]'`; + ports_exec_out = `'["out", "in"]'`; + ports_data_in = `'["out", "in"]'`; + ports_data_out = `'["out", "in"]'`; + action = `'["out", "in"]'`; +} + +int:Action { + ports_exec_in = `123`; + ports_exec_out = `123`; + ports_data_in = `123`; + ports_data_out = `123`; + action = `123`; +} + +list:Action { + ports_exec_out = `["out", "in"]`; + ports_exec_in = `["out", "in"]`; + ports_data_out = `["out", "in"]`; + ports_data_in = `["out", "in"]`; + action = `["out", "in"]`; +} +set:Action { + ports_exec_in = `{"out", "in"}`; + ports_exec_out = `{"out", "in"}`; + ports_data_in = `{"out", "in"}`; + ports_data_out = `{"out", "in"}`; + action = `{"out", "in"}`; +} + +tuple:Action { + ports_exec_in = `("out", "in")`; + ports_exec_out = `("out", "in")`; + ports_data_in = `("out", "in")`; + ports_data_out = `("out", "in")`; + action = `("out", "in")`; +} + +dict:Action { + ports_exec_in = `{"out": "in"}`; + ports_exec_out = `{"out": "in"}`; + ports_data_in = `{"out": "in"}`; + ports_data_out = `{"out": "in"}`; + action = `{"out": "in"}`; +} + +none:Action { + ports_exec_in = `None`; + ports_exec_out = `None`; + ports_data_in = `None`; + ports_data_out = `None`; + action = `None`; +} + +invalid:Action { + ports_exec_in = `[{a(0)['qkja("fyvka`; + ports_exec_out = `[{a(0)['qkja("fyvka`; + ports_data_in = `["", [{]]`; + ports_data_out = `["", [{]]`; + action = `hu(ja&{]8}]`; +} + +subtype:Action { + ports_exec_in = `[1, 2]`; + ports_exec_out = `[1, 2]`; + ports_data_in = `[1, 2]`; + ports_data_out = `[1, 2]`; + action = `[1, 2]`; +} + +code:Action { + ports_exec_in = `print("hello world")`; + ports_exec_out = `print("hello world")`; + ports_data_in = `print("hello world")`; + ports_data_out = `print("hello world")`; + action = `print("hello world")`; +} + +no:Action + +start:Start +end:End \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/fields_end.od b/transformation/schedule/Tests/models/schedule/fields_end.od new file mode 100644 index 0000000..22a26ee --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/fields_end.od @@ -0,0 +1,52 @@ +start:Start + +string:End { + ports_exec_in = `'["out", "in"]'`; + ports_data_in = `'["out", "in"]'`; +} + +int:End { + ports_exec_in = `123`; + ports_data_in = `123`; +} + +list:End { + ports_exec_in = `["out", "in"]`; + ports_data_in = `["out", "in"]`; +} +set:End { + ports_exec_in = `{"out", "in"}`; + ports_data_in = `{"out", "in"}`; +} + +tuple:End { + ports_exec_in = `("out", "in")`; + ports_data_in = `("out", "in")`; +} + +dict:End { + ports_exec_in = `{"out": "in"}`; + ports_data_in = `{"out": "in"}`; +} + +none:End { + ports_exec_in = `None`; + ports_data_in = `None`; +} + +invalid:End { + ports_exec_in = `[{a(0)['qkja("fyvka`; + ports_data_in = `["", [{]]`; +} + +subtype:End { + ports_exec_in = `[1, 2]`; + ports_data_in = `[1, 2]`; +} + +code:End { + ports_exec_in = `print("hello world")`; + ports_data_in = `print("hello world")`; +} + +no:End \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/fields_merge.od b/transformation/schedule/Tests/models/schedule/fields_merge.od new file mode 100644 index 0000000..18e3307 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/fields_merge.od @@ -0,0 +1,39 @@ +string:Merge { + ports_data_in = `'["out", "in"]'`; +} + +list:Merge { + ports_data_in = `["out", "in"]`; +} +set:Merge { + ports_data_in = `{"out", "in"}`; +} + +tuple:Merge { + ports_data_in = `("out", "in")`; +} + +dict:Merge { + ports_data_in = `{"out": "in"}`; +} + +none:Merge { + ports_data_in = `None`; +} + +invalid:Merge { + ports_data_in = `["", [{]]`; +} + +subtype:Merge { + ports_data_in = `[1, 2]`; +} + +code:Merge { + ports_data_in = `print("hello world")`; +} + +no:Merge + +start:Start +end:End \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/fields_modify.od b/transformation/schedule/Tests/models/schedule/fields_modify.od new file mode 100644 index 0000000..5730efb --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/fields_modify.od @@ -0,0 +1,51 @@ +string:Modify { + rename = `'["out", "in"]'`; + delete = `'["out", "in"]'`; +} + +list:Modify { + rename = `["out", "in"]`; + delete = `["out", "in"]`; +} +set:Modify { + rename = `{"out", "in"}`; + delete = `{"out", "in"}`; +} + +tuple:Modify { + rename = `("out", "in")`; + delete = `("out", "in")`; +} + +dict:Modify { + rename = `{"out": "in"}`; + delete = `{"out": "in"}`; +} + +none:Modify { + rename = `None`; + delete = `None`; +} + +invalid:Modify { + rename = `[{a(0)['qkja("fyvka`; + delete = `["", [{]]`; +} + +subtype:Modify { + rename = `{1: 2}`; + delete = `[1, 2]`; +} + +code:Modify { + rename = `print("hello world")`; + delete = `print("hello world")`; +} + +joined:Modify { + rename = `{"a":"1", "b":"2", "c":"3"}`; + delete = `{"a", "d"}`; +} + +start:Start +end:End \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/fields_print.od b/transformation/schedule/Tests/models/schedule/fields_print.od new file mode 100644 index 0000000..d520e44 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/fields_print.od @@ -0,0 +1,39 @@ +string:Print { + custom = `'["port_out", "port_in"]'`; +} + +list:Print { + custom = `["port_out", "port_in"]`; +} +set:Print { + custom = `{"port_out", "port_in"}`; +} + +tuple:Print { + custom = `("port_out", "port_in")`; +} + +dict:Print { + custom = `{"port_out": "port_in"}`; +} + +none:Print { + custom = `None`; +} + +invalid:Print { + custom = `["", [{]]`; +} + +subtype:Print { + custom = `[1, 2]`; +} + +code:Print { + custom = `print("hello world")`; +} + +no:Print + +start:Start +end:End \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/fields_start.od b/transformation/schedule/Tests/models/schedule/fields_start.od new file mode 100644 index 0000000..c82ea91 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/fields_start.od @@ -0,0 +1,52 @@ +string:Start { + ports_exec_out = `'["out", "in"]'`; + ports_data_out = `'["out", "in"]'`; +} + +int:Start { + ports_exec_out = `123`; + ports_data_out = `123`; +} + +list:Start { + ports_exec_out = `["out", "in"]`; + ports_data_out = `["out", "in"]`; +} +set:Start { + ports_exec_out = `{"out", "in"}`; + ports_data_out = `{"out", "in"}`; +} + +tuple:Start { + ports_exec_out = `("out", "in")`; + ports_data_out = `("out", "in")`; +} + +dict:Start { + ports_exec_out = `{"out": "in"}`; + ports_data_out = `{"out": "in"}`; +} + +none:Start { + ports_exec_out = `None`; + ports_data_out = `None`; +} + +invalid:Start { + ports_exec_out = `[{a(0)['qkja("fyvka`; + ports_data_out = `["", [{]]`; +} + +subtype:Start { + ports_exec_out = `[1, 2]`; + ports_data_out = `[1, 2]`; +} + +code:Start { + ports_exec_out = `print("hello world")`; + ports_data_out = `print("hello world")`; +} + +no:Start + +end:End \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/fields_store.od b/transformation/schedule/Tests/models/schedule/fields_store.od new file mode 100644 index 0000000..ec1f38c --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/fields_store.od @@ -0,0 +1,39 @@ +string:Store { + ports = `'["port_out", "port_in"]'`; +} + +list:Store { + ports = `["port_out", "port_in"]`; +} +set:Store { + ports = `{"port_out", "port_in"}`; +} + +tuple:Store { + ports = `("port_out", "port_in")`; +} + +dict:Store { + ports = `{"port_out": "port_in"}`; +} + +none:Store { + ports = `None`; +} + +invalid:Store { + ports = `["", [{]]`; +} + +subtype:Store { + ports = `[1, 2]`; +} + +code:Store { + ports = `print("hello world")`; +} + +no:Store + +start:Start +end:End \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/multiple_end.od b/transformation/schedule/Tests/models/schedule/multiple_end.od new file mode 100644 index 0000000..ae3651f --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/multiple_end.od @@ -0,0 +1,5 @@ +start:Start +end:End +end2:End + +:Conn_exec (start -> end) {from="out";to="in";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/multiple_start.od b/transformation/schedule/Tests/models/schedule/multiple_start.od new file mode 100644 index 0000000..0a869c8 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/multiple_start.od @@ -0,0 +1,5 @@ +start:Start +start2:Start +end:End + +:Conn_exec (start -> end) {from="out";to="in";} \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/no_end.od b/transformation/schedule/Tests/models/schedule/no_end.od new file mode 100644 index 0000000..e58e470 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/no_end.od @@ -0,0 +1 @@ +start:Start \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/no_start.od b/transformation/schedule/Tests/models/schedule/no_start.od new file mode 100644 index 0000000..36a7d96 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/no_start.od @@ -0,0 +1 @@ +end:End \ No newline at end of file diff --git a/transformation/schedule/Tests/models/schedule/start_end.od b/transformation/schedule/Tests/models/schedule/start_end.od new file mode 100644 index 0000000..bf51e88 --- /dev/null +++ b/transformation/schedule/Tests/models/schedule/start_end.od @@ -0,0 +1,3 @@ +start:Start +end:End +:Conn_exec (start -> end) {from="out";to="in";} \ No newline at end of file diff --git a/transformation/schedule/__init__.py b/transformation/schedule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/transformation/schedule/generator.py b/transformation/schedule/generator.py new file mode 100644 index 0000000..9fd08a0 --- /dev/null +++ b/transformation/schedule/generator.py @@ -0,0 +1,197 @@ +import sys +import os +from uuid import UUID + +from black.trans import Callable +from jinja2.runtime import Macro + +from api.od import ODAPI +from jinja2 import Environment, FileSystemLoader + + +class schedule_generator: + def __init__(self, odApi: ODAPI): + self.env = Environment( + loader=FileSystemLoader( + os.path.join(os.path.dirname(__file__), "templates") + ) + ) + self.env.trim_blocks = True + self.env.lstrip_blocks = True + self.template = self.env.get_template("schedule_template.j2") + self.template_wrap = self.env.get_template("schedule_template_wrap.j2") + self.api = odApi + + + def _get_slot_value_default(item: UUID, slot: str, default): + if slot in self.api.get_slots(item): + return self.api.get_slot_value(item, slot) + return default + + conn_data_event = { + "Match": lambda item: False, + "Rewrite": lambda item: False, + "Modify": lambda item: True, + "Merge": lambda item: True, + "Loop": lambda item: True, + "Action": lambda item: _get_slot_value_default(item, "event", False), + "Print": lambda item: _get_slot_value_default(item, "event", False), + "Store": lambda item: False, + "Schedule": lambda item: False, + "End": lambda item: False, + } + + arg_map = { + "Loop": (name_dict := lambda item: {"name": self.api.get_name(item)}), + "Start": lambda item: { + **name_dict(item), + "ports_exec_out": eval( + self.api.get_slot_value_default(item, "ports_exec_out", "['out']") + ), + "ports_data_out": eval( + self.api.get_slot_value_default(item, "ports_data_out", "[]") + ), + }, + "End": lambda item: { + **name_dict(item), + "ports_exec_in": eval( + self.api.get_slot_value_default(item, "ports_exec_in", "['in']") + ), + "ports_data_in": eval( + self.api.get_slot_value_default(item, "ports_data_in", "[]") + ), + }, + "Rewrite": ( + file_dict := lambda item: { + **name_dict(item), + "file": self.api.get_slot_value(item, "file"), + } + ), + "Match": lambda item: { + **file_dict(item), + "n": self.api.get_slot_value_default(item, "n", 'float("inf")'), + }, + "Action": lambda item: { + **name_dict(item), + "ports_exec_in": self.api.get_slot_value_default(item, "ports_exec_in", ["in"]), + "ports_exec_out": self.api.get_slot_value_default(item, "ports_exec_out", ["out"]), + "ports_data_in": self.api.get_slot_value_default(item, "ports_data_in", []), + "ports_data_out": self.api.get_slot_value_default(item, "ports_data_out", []), + "action": repr(self.api.get_slot_value(item, "action")), + "init": repr( + self.api.get_slot_value_default(item, "init", "") + ), + }, + "Modify": lambda item: { + **name_dict(item), + "rename": eval(self.api.get_slot_value_default(item, "rename", "{}")), + "delete": eval(self.api.get_slot_value_default(item, "delete", "{}")), + }, + "Merge": lambda item: { + **name_dict(item), + "ports_data_in": eval( + self.api.get_slot_value_default(item, "ports_data_in", "[]") + ), + }, + "Store": lambda item: { + **name_dict(item), + "ports": eval(self.api.get_slot_value_default(item, "ports", "[]")), + }, + "Schedule": file_dict, + "Print": lambda item: { + **name_dict(item), + "label": self.api.get_slot_value_default(item, "label", ""), + "custom": self.api.get_slot_value_default(item, "custom", ""), + }, + "Conn_exec": ( + conn_dict := lambda item: { + "name_from": self.api.get_name(self.api.get_source(item)), + "name_to": self.api.get_name(self.api.get_target(item)), + "from": self.api.get_slot_value_default(item, "from", 0), + "to": self.api.get_slot_value_default(item, "to", 0), + } + ), + "Conn_data": lambda item: { + **conn_dict(item), + "event": conn_data_event[ + self.api.get_type_name(target := self.api.get_target(item)) + ](target), + }, + } + self.macro_args = { + tp: (macro, arg_map.get(tp)) + for tp, macro in self.template.module.__dict__.items() + if type(macro) == Macro + } + + def _render(self, item): + type_name = self.api.get_type_name(item) + macro, arg_gen = self.macro_args[type_name] + return macro(**arg_gen(item)) + + def _dfs( + self, stack: list[UUID], get_links: Callable, get_next_node: Callable + ) -> tuple[set[UUID], list[UUID]]: + visited = set() + connections = list() + while len(stack) > 0: + obj = stack.pop() + if obj in visited: + continue + visited.add(obj) + for conn in get_links(self.api, obj): + connections.append(conn) + stack.append(get_next_node(self.api, conn)) + return visited, connections + + def generate_schedule(self, stream=sys.stdout): + start = self.api.get_all_instances("Start")[0][1] + end = self.api.get_all_instances("End")[0][1] + out = { + "blocks": [], + "blocks_name": [], + "blocks_start_end": [], + "exec_conn": [], + "data_conn": [], + "match_files": set(), + "matchers": [], + "start": self.api.get_name(start), + "end": self.api.get_name(end), + } + + stack = [start, end] + exec_blocks, conn_exec = self._dfs( + stack, + lambda api, node: api.get_outgoing(node, "Conn_exec"), + lambda api, conn: api.get_target(conn), + ) + + for name, p in self.api.get_all_instances("Print"): + if self.api.has_slot(p, "event") and self.api.get_slot_value(p, "event"): + exec_blocks.add(p) + + stack = list(exec_blocks) + blocks, conn_data = self._dfs( + stack, + lambda api, node: api.get_incoming(node, "Conn_data"), + lambda api, conn: api.get_source(conn), + ) + + for exec_c in conn_exec: + out["exec_conn"].append(self._render(exec_c)) + + for data_c in conn_data: + out["data_conn"].append(self._render(data_c)) + + for block in blocks: + out["blocks_name"].append(self.api.get_name(block)) + if block in [start, end]: + out["blocks_start_end"].append(self._render(block)) + continue + out["blocks"].append(self._render(block)) + if self.api.is_instance(block, "Rule"): + d = self.macro_args[self.api.get_type_name(block)][1](block) + out["match_files"].add(d["file"]) + out["matchers"].append(d) + + print(self.template_wrap.render(out), file=stream) diff --git a/transformation/schedule/models/eval_context.py b/transformation/schedule/models/eval_context.py new file mode 100644 index 0000000..061b4f6 --- /dev/null +++ b/transformation/schedule/models/eval_context.py @@ -0,0 +1,151 @@ +from typing import TYPE_CHECKING, get_origin, get_args +from types import UnionType +from uuid import UUID + +from jinja2 import Template + +from framework.conformance import eval_context_decorator +from services.primitives.string_type import String + +if TYPE_CHECKING: + from api.od_stub_readonly import get_outgoing, get_incoming, get_slot_value, get_value, get_target, has_slot + from eval_context_stub import * + + +@eval_context_decorator +def _check_all_connections(this, labels: list[list[str] | str]) -> list[str]: + err = [] + check_incoming_exec(this, err, labels[0]) + check_outgoing_exec(this, err, labels[1]) + check_incoming_data(this, err, labels[2]) + check_outgoing_data(this, err, labels[3]) + return err + +@eval_context_decorator +def _check_outgoing_exec(this, err: list[str], labels: list[str]) -> None: + l = set(labels) + gates = set() + for y in get_outgoing(this, "Conn_exec"): + if (x := get_slot_value(y, "from")) not in l: + err.append(f"output exec gate '{x}' does not exist. Gates: {', '.join(labels)}.") + if x in gates: + err.append(f"output exec gate '{x}' is connected to multiple gates.") + gates.add(x) + + +@eval_context_decorator +def _check_incoming_exec(this, err: list[str], labels: list[str]) -> None: + l = set(labels) + for y in get_incoming(this, "Conn_exec"): + if (x := get_slot_value(y, "to")) not in l: + err.append(f"input exec gate gate '{x}' does not exist. Gates: {', '.join(labels)}.") + + +@eval_context_decorator +def _check_outgoing_data(this, err: list[str], labels: list[str]) -> None: + l = set(labels) + for y in get_outgoing(this, "Conn_data"): + if (x := get_slot_value(y, "from")) not in l: + err.append(f"output data gate '{x}' does not exist. Gates: {', '.join(labels)}.") + + +@eval_context_decorator +def _check_incoming_data(this, err: list[str], labels: list[str]) -> None: + l = set(labels) + gates = set() + for y in get_incoming(this, "Conn_data"): + if (x := get_slot_value(y, "to")) not in l: + err.append(f"input data gate '{x}' does not exist. Gates: {', '.join(labels)}.") + if x in gates: + err.append(f"input data gate '{x}' is connected to multiple gates.") + gates.add(x) + +def check_type(x: any, typ2: any) -> bool: + origin = get_origin(typ2) + if origin is None: + return isinstance(x, typ2) + args = get_args(typ2) + if origin is UnionType: + for tp in args: + if check_type(x, tp): + return True + return False + if not isinstance(x, origin): + return False + if origin in [list, set]: + for value in x: + if not check_type(value, args[0]): + return False + elif origin is tuple: + if len(args) != len(x): + return False + for i, value in enumerate(x): + if not check_type(value, args[i]): + return False + elif origin is dict: + for key, value in x.items(): + if not (check_type(key, args[0]) and check_type(value, args[1])): + return False + return True + +@eval_context_decorator +def _check_slot_code_type(this: UUID, slot: str, typ: type, unique = False, *, mandatory: bool = False, blacklist: list[str] | None = None) -> list[str]: + err = [] + if not (has_slot(this, slot)): + if mandatory: + err.append(f"Missing mandatory slot: '{slot}'.") + return err + try: + try: + x = eval(get_slot_value(this, slot)) + except Exception as _: + err.append(f"Invalid python code for {slot}: {get_slot_value(this, slot)}") + return err + + if not check_type(x, typ): + try: + typ_rep = typ.__name__ + except AttributeError: + typ_rep = str(typ) + err.append(f"Unexpected type for {slot}: {type(x).__name__}, expected type: {typ_rep}") + return err + + if unique and len(set(x)) != len(x): + err.append(f"elements must be unique") + return err + except Exception as e: + err.append(f"Unexpected error: {e}") + return err + + +@eval_context_decorator +def _check_jinja2_code(this: UUID, slot: str) -> list[str]: + if len(err:= check_slot_code_type(this, slot, str, mandatory=True)) != 0: + return err + s = eval(get_slot_value(this, slot)) + try: + template = Template(s) + template.render(**{"data":[{}]}) + return [] + except Exception as e: + return [f"Invalid Jinja2 syntax for '{slot}':\n{e}\n{s}"] + + +@eval_context_decorator +def _check_code_syntax(code) -> list[str]: + try: + compile(code, "", "exec") + return [] + except SyntaxError as e: + return [f"Invalid python code for: `{code}` :\n{e}"] + +mm_eval_context = { + "check_all_connections": _check_all_connections, + "check_outgoing_exec": _check_outgoing_exec, + "check_incoming_exec": _check_incoming_exec, + "check_outgoing_data": _check_outgoing_data, + "check_incoming_data": _check_incoming_data, + "check_slot_code_type": _check_slot_code_type, + "check_code_syntax": _check_code_syntax, + "check_jinja2_code": _check_jinja2_code, +} diff --git a/transformation/schedule/models/eval_context_stub.pyi b/transformation/schedule/models/eval_context_stub.pyi new file mode 100644 index 0000000..9811909 --- /dev/null +++ b/transformation/schedule/models/eval_context_stub.pyi @@ -0,0 +1,6 @@ +def check_outgoing_exec(this, err: list[str], labels: list[str]) -> bool: ... +def check_incoming_exec(this, err: list[str], labels: list[str]) -> bool: ... +def check_outgoing_data(this, err: list[str], labels: list[str]) -> bool: ... +def check_incoming_data(this, err: list[str], labels: list[str]) -> bool: ... +def check_is_type(s: str, typ: any) -> bool: ... +def check_code_syntax(code) -> bool: ... diff --git a/transformation/schedule/models/scheduling_MM.od b/transformation/schedule/models/scheduling_MM.od new file mode 100644 index 0000000..edae0b9 --- /dev/null +++ b/transformation/schedule/models/scheduling_MM.od @@ -0,0 +1,195 @@ +abstract class Exec + +association Conn_exec [0..*] Exec -> Exec [0..*] { + String from; + String to; +} + +abstract class Data +association Conn_data [0..*] Data -> Data [0..*] { + Integer from; + Integer to; +} +abstract class Node (Exec, Data) + +class Start [1..1] (Node) { + optional ActionCode ports_exec_out; + optional ActionCode ports_data_out; + ``` + err = check_slot_code_type(this, "ports_exec_out", list[str] | set[str], True) + err.extend(check_slot_code_type(this, "ports_data_out", list[str] | set[str], True)) + if len(err) == 0: + err = check_all_connections(this, [ + [], + eval(get_slot_value_default(this, "ports_exec_out", "['out']")), + [], + eval(get_slot_value_default(this, "ports_data_out", "[]")) + ]) + err + ```; +} +class End [1..1] (Node) { + optional ActionCode ports_exec_in; + optional ActionCode ports_data_in; + ``` + err = check_slot_code_type(this, "ports_exec_in", list[str] | set[str], True) + err.extend(check_slot_code_type(this, "ports_data_in", list[str] | set[str], True)) + if len(err) == 0: + err = check_all_connections(this, [ + eval(get_slot_value_default(this, "ports_exec_in", "['in']")), + [], + eval(get_slot_value_default(this, "ports_data_in", "[]")), + [] + ]) + err + ```; +} + +abstract class Rule (Node) +{ + String file; +} + +class Match (Rule) +{ + optional Integer n; + ``` + check_all_connections(this, [ + ["in"], + ["success", "fail"], + ["in"], + ["out"] + ]) + ```; +} + +class Rewrite (Rule) +{ + ``` + check_all_connections(this, [ + ["in"], + ["out"], + ["in"], + ["out"] + ]) + ```; +} + +class Action (Node) +{ + optional ActionCode ports_exec_in; + optional ActionCode ports_exec_out; + optional ActionCode ports_data_in; + optional ActionCode ports_data_out; + optional ActionCode init `check_code_syntax(get_value(get_target(this)))`; + ActionCode action `check_code_syntax(get_value(get_target(this)))`; + ``` + err = check_slot_code_type(this, "ports_exec_in", list[str] | set[str], True) + err.extend(check_slot_code_type(this, "ports_exec_out", list[str] | set[str], True)) + err.extend(check_slot_code_type(this, "ports_data_in", list[str] | set[str], True)) + err.extend(check_slot_code_type(this, "ports_data_out", list[str] | set[str], True)) + if len(err) == 0: + err = check_all_connections(this, [ + eval(get_slot_value_default(this, "ports_exec_in", "['in']")), + eval(get_slot_value_default(this, "ports_exec_out", "['out']")), + eval(get_slot_value_default(this, "ports_data_in", "[]")), + eval(get_slot_value_default(this, "ports_data_out", "[]")) + ]) + err + ```; + +} + +class Modify (Node) +{ + optional ActionCode rename; + optional ActionCode delete; + ``` + err = check_slot_code_type(this, "rename", dict[str,str]) + err.extend(check_slot_code_type(this, "delete", list[str] | set[str])) + if len(err) == 0: + if not (eval(get_slot_value_default(this, "rename", "dict()")).keys().isdisjoint( + eval(get_slot_value_default(this, "delete", "set()"))) + ): + err.append("rename and delete should be disjoint.") + err.extend(check_all_connections(this, [ + [], + [], + ["in"], + ["out"] + ])) + err + ```; +} + +class Merge (Node) +{ + ActionCode ports_data_in; + ``` + err = check_slot_code_type(this, "ports_data_in", list[str] | set[str], True, mandatory = True) + if len(err) == 0: + err = check_all_connections(this, [ + [], + [], + eval(get_slot_value(this, "ports_data_in")), + ["out"] + ]) + err + ```; +} + +class Store (Node) +{ + ActionCode ports; + ``` + err = check_slot_code_type(this, "ports", list[str] | set[str], True, mandatory = True, blacklist = ["in", "out"]) + if len(err) == 0: + err = check_all_connections(this, [ + [*(ports:= eval(get_slot_value(this, "ports"))), "in"], + [*ports, "out"], + ports, + ["out"] + ]) + err + ```; +} + +class Schedule (Node) +{ + String file; + ``` + check_all_connections(this, [ + {get_slot_value(conn, "to") for conn in get_incoming(this, "Conn_exec")}, + {get_slot_value(conn, "from") for conn in get_outgoing(this, "Conn_exec")}, + {get_slot_value(conn, "to") for conn in get_incoming(this, "Conn_data")}, + {get_slot_value(conn, "from") for conn in get_outgoing(this, "Conn_data")} + ]) + ```; +} + +class Loop(Node) +{ + ``` + check_all_connections(this, [ + ["in"], + ["it", "out"], + ["in"], + ["out"] + ]) + ```; +} + +class Print(Node) +{ + optional Boolean event; + optional String label; + optional ActionCode custom `check_jinja2_code(get_source(this), "custom")`; + ``` + check_all_connections(this, [ + ["in"], + ["out"], + ["in"], + [] + ]) + ```; +} \ No newline at end of file diff --git a/transformation/schedule/rule_executor.py b/transformation/schedule/rule_executor.py new file mode 100644 index 0000000..da97b2f --- /dev/null +++ b/transformation/schedule/rule_executor.py @@ -0,0 +1,46 @@ +from typing import Any +from uuid import UUID + +from api.od import ODAPI +from transformation.matcher import match_od +from transformation.rewriter import rewrite +from util.loader import parse_and_check + + +class RuleExecutor: + def __init__(self, state, mm: UUID, mm_ramified: UUID, eval_context={}): + self.state = state + self.mm = mm + self.mm_ramified = mm_ramified + self.eval_context = eval_context + + # Generates matches. + # Every match is a dictionary with entries LHS_element_name -> model_element_name + def match_rule(self, m: UUID, lhs: UUID, *, pivot: dict[Any, Any]): + lhs_matcher = match_od( + self.state, + host_m=m, + host_mm=self.mm, + pattern_m=lhs, + pattern_mm=self.mm_ramified, + eval_context=self.eval_context, + pivot=pivot, + ) + return lhs_matcher + + def rewrite_rule(self, od: ODAPI, rhs: UUID, *, pivot: dict[Any, Any]): + rhs = rewrite( + self.state, + rhs_m=rhs, + pattern_mm=self.mm_ramified, + lhs_match=pivot, + host_m=od.m, + host_mm=od.mm, + eval_context=self.eval_context, + ) + od.recompute_mappings() + yield rhs + + def load_match(self, file: str): + with open(file, "r") as f: + return parse_and_check(self.state, f.read(), self.mm_ramified, file) diff --git a/transformation/schedule/rule_scheduler.py b/transformation/schedule/rule_scheduler.py new file mode 100644 index 0000000..474a6ab --- /dev/null +++ b/transformation/schedule/rule_scheduler.py @@ -0,0 +1,338 @@ +from __future__ import annotations + +import importlib.util +import io +import os +import re +import sys + +from pathlib import Path +from time import time +from typing import cast, TYPE_CHECKING + +from jinja2 import FileSystemLoader, Environment + +from concrete_syntax.textual_od import parser as parser_od +from concrete_syntax.textual_cd import parser as parser_cd +from api.od import ODAPI +from bootstrap.scd import bootstrap_scd +from transformation.schedule.rule_executor import RuleExecutor +from transformation.schedule.generator import schedule_generator +from transformation.schedule.models.eval_context import mm_eval_context +from transformation.schedule.schedule_lib import ExecNode, Start +from framework.conformance import Conformance, render_conformance_check_result, eval_context_decorator +from state.devstate import DevState +from examples.petrinet.renderer import render_petri_net_to_dot + +from drawio2py import parser +from drawio2py.abstract_syntax import DrawIOFile, Edge, Vertex, Cell +from icecream import ic + +from transformation.schedule.schedule_lib.funcs import IdGenerator + +if TYPE_CHECKING: + from transformation.schedule.schedule import Schedule + + +class RuleSchedular: + __slots__ = ( + "rule_executor", + "schedule_main", + "loaded", + "out", + "verbose", + "conformance", + "directory", + "eval_context", + "_state", + "_mmm_cs", + "sub_schedules", + "end_time", + ) + + def __init__( + self, + state, + mm_rt, + mm_rt_ramified, + *, + outstream=sys.stdout, + verbose: bool = False, + conformance: bool = True, + directory: str = "", + eval_context: dict[str, any] = None, + ): + self.rule_executor: RuleExecutor = RuleExecutor(state, mm_rt, mm_rt_ramified) + self.schedule_main: Schedule | None = None + self.out = outstream + self.verbose: bool = verbose + self.conformance: bool = conformance + self.directory: Path = Path.cwd() / directory + if eval_context is None: + eval_context = {} + self.eval_context: dict[str, any] = eval_context + + self.loaded: dict[str, dict[str, any]] = {"od": {}, "py": {}, "drawio": {}, "rules": {}} + + + self._state = DevState() + self._mmm_cs = bootstrap_scd(self._state) + + self.end_time = float("inf") + self.sub_schedules = float("inf") + + def load_schedule(self, filename): + return self._load_schedule(filename, _main=True) is not None + + + def _load_schedule(self, filename: str, *, _main = True) -> Schedule | None: + if filename.endswith(".drawio"): + if (filename := self._generate_schedule_drawio(filename)) is None: + return None + + if filename.endswith(".od"): + if (filename := self._generate_schedule_od(filename)) is None: + return None + if filename.endswith(".py"): + s = self._load_schedule_py(filename, _main=_main) + return s + + raise Exception(f"Error unknown file: {filename}") + + def _load_schedule_py(self, filename: str, *, _main = True) -> "Schedule": + if (s:= self.loaded["py"].get(filename, None)) is not None: + return s + + spec = importlib.util.spec_from_file_location(filename, str(self.directory / filename)) + schedule_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(schedule_module) + self.loaded["py"][filename] = (s:= schedule_module.Schedule()) + if _main: + self.schedule_main = s + self.load_matchers(s) + return s + + def _generate_schedule_od(self, filename: str) -> str | None: + if (s:= self.loaded.get(("od", filename), None)) is not None: + return s + file = str(self.directory / filename) + self._print("Generating schedule ...") + with open(f"{os.path.dirname(__file__)}/models/scheduling_MM.od", "r") as f_MM: + mm_cs = f_MM.read() + try: + with open(file, "r") as f_M: + m_cs = f_M.read() + except FileNotFoundError: + self._print(f"File not found: {file}") + return None + + self._print("OK\n\nParsing models\n\tParsing meta model") + try: + scheduling_mm = parser_cd.parse_cd( + self._state, + m_text=mm_cs, + ) + except Exception as e: + self._print( + f"Error while parsing meta-model: scheduling_MM.od\n\t{e}" + ) + return None + self._print(f"\tParsing '{filename}' model") + try: + scheduling_m = parser_od.parse_od( + self._state, m_text=m_cs, mm=scheduling_mm + ) + except Exception as e: + self._print(f"\033[91mError while parsing model: {filename}\n\t{e}\033[0m") + return None + if self.conformance: + success = True + self._print("OK\n\tmeta-meta-model a valid class diagram") + conf_err = Conformance( + self._state, self._mmm_cs, self._mmm_cs + ).check_nominal() + b = len(conf_err) + success = success and not b + self._print( + f"\t\t{'\033[91m' if b else ''}{render_conformance_check_result(conf_err)}{'\033[0m' if b else ''}" + ) + self._print( + f"Is our '{filename}' model a valid 'scheduling_MM.od' diagram?" + ) + conf_err = Conformance( + self._state, scheduling_m, scheduling_mm, eval_context=mm_eval_context + ).check_nominal() + b = len(conf_err) + success = success and not b + self._print( + f"\t\t{'\033[91m' if b else ''}{render_conformance_check_result(conf_err)}{'\033[0m' if b else ''}" + ) + if not success: + return None + od = ODAPI(self._state, scheduling_m, scheduling_mm) + g = schedule_generator(od) + + output_buffer = io.StringIO() + g.generate_schedule(output_buffer) + outfilename = f"{".".join(filename.split(".")[:-1])}.py" + open(self.directory / outfilename, "w", encoding='utf-8').write(output_buffer.getvalue()) + self._print("Schedule generated") + self.loaded[("od", filename)] = outfilename + return outfilename + + def _print(self, *args) -> None: + if self.verbose: + print(*args, file=self.out) + + def load_matchers(self, schedule: "Schedule") -> None: + matchers = dict() + for file in schedule.get_matchers(): + if (r:= self.loaded.get(("rule", file), None)) is None: + self.loaded[("rule", file)] = (r:= self.rule_executor.load_match(self.directory / file)) + matchers[file] = r + schedule.init_schedule(self, self.rule_executor, matchers) + + def generate_dot(self, file: str) -> None: + env = Environment( + loader=FileSystemLoader( + os.path.join(os.path.dirname(__file__), "templates") + ) + ) + env.trim_blocks = True + env.lstrip_blocks = True + template_dot = env.get_template("schedule_dot.j2") + + nodes = [] + edges = [] + visit = set() + for schedule in self.loaded["py"].values(): + schedule.generate_dot(nodes, edges, visit, template_dot) + with open(self.directory / file, "w") as f_dot: + f_dot.write(template_dot.render(nodes=nodes, edges=edges)) + + def run(self, model) -> tuple[int, str]: + self._print("Start simulation") + if 'pydevd' in sys.modules: + self.end_time = time() + 1000 + else: + self.end_time = time() + 10000 + return self._runner(model, self.schedule_main, "out", IdGenerator.generate_exec_id(), {}) + + def _runner(self, model, schedule: Schedule, exec_port: str, exec_id: int, data: dict[str, any]) -> tuple[int, any]: + self._generate_stackframe(schedule, exec_id) + cur_node = schedule.start + cur_node.run_init(exec_port, exec_id, data) + while self.end_time > time(): + cur_node, port = cur_node.nextState(exec_id) + termination_reason = cur_node.execute(port, exec_id, model) + if termination_reason is not None: + self._delete_stackframe(schedule, exec_id) + return termination_reason + + self._delete_stackframe(schedule, exec_id) + return -1, "limit reached" + + + def _generate_stackframe(self, schedule: Schedule, exec_id: int) -> None: + for node in schedule.nodes: + node.generate_stack_frame(exec_id) + + def _delete_stackframe(self, schedule: Schedule, exec_id: int) -> None: + for node in schedule.nodes: + node.delete_stack_frame(exec_id) + + + def _generate_schedule_drawio(self, filename:str) -> str | None: + if (s:= self.loaded["drawio"].get(filename, None)) is not None: + return s + env = Environment( + loader=FileSystemLoader( + os.path.join(os.path.dirname(__file__), "templates") + ) + ) + env.trim_blocks = True + env.lstrip_blocks = True + template = env.get_template("schedule_muMLE.j2") + main: bool = False + + node_map: dict[str, list[str | dict[str,str]]] + id_counter: int + def _get_node_id_map(elem: Cell) -> list[str | dict[str,str]]: + nonlocal node_map, id_counter + if (e_id := node_map.get(elem.id, None)) is None: + e_id = [f"{re.sub(r'[^a-zA-Z1-9_]', '', elem.properties["name"])}_{id_counter}", {}] + id_counter += 1 + node_map[elem.id] = e_id + return e_id + + edges: list[tuple[tuple[str, str, str, str], tuple[str,str,str,str]]] = [] + def _parse_edge(elem: Edge): + nonlocal edges + try: + edges.append(( + ( + _get_node_id_map(elem.source.parent.parent.parent)[0], + elem.source.properties["label"], + elem.source.properties["type"], + elem.source.parent.value + ), + ( + _get_node_id_map(elem.target.parent.parent.parent)[0], + elem.target.properties["label"], + elem.target.properties["type"], + elem.target.parent.value + ) + )) + except AttributeError as e: + raise Exception(f"Missing attribute {e}") + return + + def _parse_vertex(elem: Vertex): + nonlocal edges + try: + elem_map = _get_node_id_map(elem) + elem_map[1] = elem.properties + properties = elem_map[1] + properties.pop("label") + properties.pop("name") + properties.pop("placeholders") + if properties.get("type") == "Schedule": + if not re.search(r'\.(py|od)$', properties["file"]): + properties["file"] = f"{filename}/{properties["file"]}.od" + except AttributeError as e: + raise Exception(f"Missing attribute {e}") + return + + + abstract_syntax: DrawIOFile = parser.Parser.parse(str(self.directory / filename)) + filename = filename.removesuffix(".drawio") + (self.directory / filename).mkdir(parents=False, exist_ok=True) + for page in abstract_syntax.pages: + if page.name == "main": + main = True + if len(page.root.children) != 1: + raise Exception(f"Only 1 layer allowed (keybind: ctr+shift+L)") + edges = [] + id_counter = 1 + node_map = {} + + for element in page.root.children[0].children: + match element.__class__.__name__: + case "Edge": + _parse_edge(cast(Edge, element)) + case "Vertex": + _parse_vertex(cast(Vertex, element)) + for elem in element.children[0].children: + if elem.__class__.__name__ == "Edge": + _parse_edge(cast(Edge, elem)) + continue + case _: + raise Exception(f"Unexpected element: {element}") + with open(self.directory / f"{filename}/{page.name}.od", "w", encoding="utf-8") as f: + f.write(template.render(nodes=node_map, edges=edges)) + if main: + self.loaded["drawio"][filename] = (filename_out := f"{filename}/main.od") + return filename_out + + self._print("drawio schedule requires main page to automatically load.") + return None diff --git a/transformation/schedule/schedule.pyi b/transformation/schedule/schedule.pyi new file mode 100644 index 0000000..0e6547f --- /dev/null +++ b/transformation/schedule/schedule.pyi @@ -0,0 +1,18 @@ +from typing import TYPE_CHECKING +from transformation.schedule.schedule_lib import * +if TYPE_CHECKING: + from transformation.schedule.rule_executor import RuleExecutor + from rule_scheduler import RuleSchedular + +class Schedule: + __slots__ = { + "start", + "end", + "nodes" + } + def __init__(self): ... + + @staticmethod + def get_matchers(): ... + def init_schedule(self, schedular: RuleSchedular, rule_executor: RuleExecutor, matchers): ... + def generate_dot(self, *args, **kwargs): ... \ No newline at end of file diff --git a/transformation/schedule/schedule_lib/README.md b/transformation/schedule/schedule_lib/README.md new file mode 100644 index 0000000..078e00f --- /dev/null +++ b/transformation/schedule/schedule_lib/README.md @@ -0,0 +1,41 @@ +## Node Module + +Defines the abstract base Node class for graph-based structures. Each Node is assigned +a unique identifier via an external IdGenerator. The class provides an interface for +managing execution state and generating DOT graph representations using Jinja2 templates. + +### Class: `Node` + +- **Attributes** + - `id: int`: A unique identifier assigned to each instance upon initialization. + +- **Methods** + - `get_id` + - returns: `int`, The unique node ID + + Retrieves the unique identifier of the node. + + - `generate_stack_frame` + - exec_id: `int`, The ID of the execution context. + - returns: `None` + + Initializes a new state frame for a specific execution context. + Designed to be overridden in subclasses that use execution state. + + - `delete_stack_frame` + - exec_id: `int`, The ID of the execution context. + - returns: `None` + + Deletes the state frame for a specific execution context. + Designed to be overridden in subclasses that use execution state. + + - `generate_dot` + - nodes: `list[str]`, A list to append DOT node definitions to. + - edges: `list[str]`, A list to append DOT edges definitions to. + - visited: `set[str]`, A set of already visited node IDs to avoid duplicates or recursion. + - template: `list[str]`, A Jinja2 template used to format the node's DOT representation. + - returns: `None` + + Generates the DOT graph representation for this node and its relationships. + Must be implemented in subclasses. + \ No newline at end of file diff --git a/transformation/schedule/schedule_lib/Schedule_lib.xml b/transformation/schedule/schedule_lib/Schedule_lib.xml new file mode 100644 index 0000000..5dd1480 --- /dev/null +++ b/transformation/schedule/schedule_lib/Schedule_lib.xml @@ -0,0 +1,93 @@ +[ + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%\" placeholders=\"1\" name=\"start_name\" type=\"Start\" ports_exec_out=\"[&quot;out&quot;]\" ports_data_out=\"[]\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"100\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"60\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell><mxCell id=\"5\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"exec\" id=\"6\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"5\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 100, + "aspect": "fixed", + "title": "Start Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%\" placeholders=\"1\" name=\"end_name\" type=\"End\" ports_exec_in=\"[&quot;in&quot;]\" ports_data_in=\"[]\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"100\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"60\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"in\" type=\"exec\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"6\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell></root></mxGraphModel>", + "w": 160, + "h": 100, + "aspect": "fixed", + "title": "End Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%&#10;%file%&#10;matches: %n%\" placeholders=\"1\" name=\"match_name\" type=\"Match\" file=\"rule_filename.od\" n=\"1\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=60;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"220\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"60\" width=\"160\" height=\"160\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=60;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"160\" as=\"geometry\"><mxRectangle width=\"80\" height=\"160\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"in\" type=\"data\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"110\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"in\" type=\"exec\" id=\"6\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"7\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"160\" as=\"geometry\"><mxRectangle width=\"80\" height=\"160\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"data\" id=\"8\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"7\"><mxGeometry x=\"10\" y=\"110\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"success\" type=\"exec\" id=\"9\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"7\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"fail\" type=\"exec\" id=\"10\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"7\"><mxGeometry x=\"10\" y=\"60\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 220, + "aspect": "fixed", + "title": "Match Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%&#10;%file%\" placeholders=\"1\" name=\"rewrite_name\" type=\"Rewrite\" file=\"rule_filename.od\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry y=\"1.1368683772161603e-13\" width=\"160\" height=\"150\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"110\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=60;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"110\" as=\"geometry\"><mxRectangle width=\"80\" height=\"110\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"in\" type=\"exec\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"6\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"110\" as=\"geometry\"><mxRectangle width=\"80\" height=\"110\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"exec\" id=\"7\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"6\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"in\" type=\"data\" id=\"8\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"6\"><mxGeometry x=\"-70\" y=\"60\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"out\" type=\"data\" id=\"9\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"6\"><mxGeometry x=\"10\" y=\"60\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 150, + "aspect": "fixed", + "title": "Rewrite Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%\" placeholders=\"1\" name=\"action_name\" type=\"Action\" ports_exec_in=\"[&quot;in&quot;]\" ports_exec_out=\"[&quot;out&quot;]\" ports_data_in=\"[]\" ports_data_out=\"[]\" action=\"print(&quot;hello world&quot;)\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"100\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"60\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=60;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"in\" type=\"exec\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"6\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"exec\" id=\"7\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"6\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 100, + "aspect": "fixed", + "title": "Action Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%\" placeholders=\"1\" name=\"modify_name\" type=\"Modify\" rename=\"{&quot;t&quot;:&quot;transition&quot;}\" delete=\"[]\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"100\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"60\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=60;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"in\" type=\"data\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"6\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"data\" id=\"7\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"6\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 100, + "aspect": "fixed", + "title": "Modify Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%\" placeholders=\"1\" name=\"merge_name\" type=\"Merge\" ports_data_in=\"[&quot;input1&quot;, &quot;input2&quot;]\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"150\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"110\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=60;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"110\" as=\"geometry\"><mxRectangle width=\"80\" height=\"110\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"input1\" type=\"data\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"input2\" type=\"data\" id=\"6\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"60\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"7\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"110\" as=\"geometry\"><mxRectangle width=\"80\" height=\"110\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"data\" id=\"8\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"7\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 150, + "aspect": "fixed", + "title": "Merge Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%\" placeholders=\"1\" name=\"store_name\" type=\"Store\" ports=\"[&quot;input1&quot;]\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"200\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"160\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=60;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"160\" as=\"geometry\"><mxRectangle width=\"80\" height=\"160\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"in\" type=\"exec\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"input1\" type=\"exec\" id=\"6\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"60\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"input1\" type=\"data\" id=\"7\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"110\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"8\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"160\" as=\"geometry\"><mxRectangle width=\"80\" height=\"160\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"data\" id=\"9\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"8\"><mxGeometry x=\"10\" y=\"110\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"out\" type=\"exec\" id=\"10\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"8\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"input1\" type=\"exec\" id=\"11\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"8\"><mxGeometry x=\"10\" y=\"60\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 200, + "aspect": "fixed", + "title": "Store Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%\" placeholders=\"1\" name=\"loop_name\" type=\"Loop\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"200\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"160\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=60;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"160\" as=\"geometry\"><mxRectangle width=\"80\" height=\"160\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"in\" type=\"data\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"110\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"in\" type=\"exec\" id=\"6\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"7\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"160\" as=\"geometry\"><mxRectangle width=\"80\" height=\"160\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"data\" id=\"8\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"7\"><mxGeometry x=\"10\" y=\"110\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"it\" type=\"exec\" id=\"9\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"7\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"out\" type=\"exec\" id=\"10\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"7\"><mxGeometry x=\"10\" y=\"60\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 200, + "aspect": "fixed", + "title": "Loop Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%&#10;%file%\" placeholders=\"1\" name=\"schedule_name\" type=\"Schedule\" file=\"schedule_page-name\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"100\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"60\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=60;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"exec\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"6\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"60\" as=\"geometry\"><mxRectangle width=\"80\" height=\"60\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"in\" type=\"exec\" id=\"7\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"6\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 100, + "aspect": "fixed", + "title": "Schedule Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"%name%: %type%\" placeholders=\"1\" name=\"print_name\" type=\"Print\" event=\"False\" custom=\"{{ data }}\" id=\"2\"><mxCell style=\"shape=table;childLayout=tableLayout;startSize=40;collapsible=0;recursiveResize=1;expand=0;fontStyle=1;editable=1;movable=1;resizable=1;rotatable=0;deletable=1;locked=0;connectable=0;allowArrows=0;pointerEvents=0;perimeter=rectanglePerimeter;rounded=1;container=1;dropTarget=0;swimlaneHead=1;swimlaneBody=1;top=1;noLabel=0;autosize=0;resizeHeight=0;spacing=2;metaEdit=1;resizeWidth=0;arcSize=10;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"160\" height=\"150\" as=\"geometry\"/></mxCell></object><mxCell id=\"3\" value=\"\" style=\"shape=tableRow;horizontal=0;swimlaneHead=0;swimlaneBody=0;top=0;left=0;strokeColor=inherit;bottom=0;right=0;dropTarget=0;fontStyle=0;fillColor=none;points=[[0,0.5],[1,0.5]];startSize=0;collapsible=0;recursiveResize=1;expand=0;rounded=0;allowArrows=0;connectable=0;autosize=1;resizeHeight=1;rotatable=0;\" vertex=\"1\" parent=\"2\"><mxGeometry y=\"40\" width=\"160\" height=\"110\" as=\"geometry\"/></mxCell><mxCell id=\"4\" value=\"Input\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=60;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry width=\"80\" height=\"110\" as=\"geometry\"><mxRectangle width=\"80\" height=\"110\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"in\" type=\"exec\" id=\"5\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"4\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><mxCell id=\"6\" value=\"Output\" style=\"swimlane;swimlaneHead=0;swimlaneBody=0;fontStyle=0;strokeColor=inherit;connectable=0;fillColor=none;startSize=40;collapsible=0;recursiveResize=1;expand=0;allowArrows=0;autosize=1;rotatable=0;noLabel=1;overflow=hidden;swimlaneLine=0;editable=0;\" vertex=\"1\" parent=\"3\"><mxGeometry x=\"80\" width=\"80\" height=\"110\" as=\"geometry\"><mxRectangle width=\"80\" height=\"110\" as=\"alternateBounds\"/></mxGeometry></mxCell><object label=\"out\" type=\"exec\" id=\"7\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"6\"><mxGeometry x=\"10\" y=\"10\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object><object label=\"in\" type=\"data\" id=\"8\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"6\"><mxGeometry x=\"-70\" y=\"60\" width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 160, + "h": 150, + "aspect": "fixed", + "title": "Print Node" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"out\" type=\"exec\" id=\"2\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 60, + "h": 40, + "aspect": "fixed", + "title": "Exec Gate" + }, + { + "xml": "<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"in\" type=\"data\" id=\"2\"><mxCell style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"60\" height=\"40\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>", + "w": 60, + "h": 40, + "aspect": "fixed", + "title": "Data Gate" + } +] \ No newline at end of file diff --git a/transformation/schedule/schedule_lib/__init__.py b/transformation/schedule/schedule_lib/__init__.py new file mode 100644 index 0000000..4df5a3d --- /dev/null +++ b/transformation/schedule/schedule_lib/__init__.py @@ -0,0 +1,31 @@ +from .action import Action +from .data_node import DataNode +from .end import End +from .exec_node import ExecNode +from .loop import Loop +from .match import Match +from .merge import Merge +from .modify import Modify +from .null_node import NullNode +from .print import Print +from .rewrite import Rewrite +from .start import Start +from .store import Store +from .sub_schedule import SubSchedule + +__all__ = [ + "Action", + "DataNode", + "End", + "ExecNode", + "Loop", + "Match", + "Merge", + "Modify", + "NullNode", + "Rewrite", + "Print", + "Start", + "Store", + "SubSchedule", +] diff --git a/transformation/schedule/schedule_lib/action.py b/transformation/schedule/schedule_lib/action.py new file mode 100644 index 0000000..9f10406 --- /dev/null +++ b/transformation/schedule/schedule_lib/action.py @@ -0,0 +1,106 @@ +from typing import List, override, Type + +from jinja2 import Template + +from api.od import ODAPI +from .funcs import not_visited, generate_dot_node +from .exec_node import ExecNode +from .data_node import DataNode + +class ActionState: + def __init__(self): + self.var = {"output_gate": "out"} + +class Action(ExecNode, DataNode): + def __init__( + self, + ports_exec_in: list[str], + ports_exec_out: list[str], + ports_data_in: list[str], + ports_data_out: list[str], + code: str = "", + init: str = "", + ) -> None: + self.gates: tuple[list[str], list[str], list[str], list[str]] = (ports_exec_in, ports_exec_out, ports_data_in, ports_data_out) + super().__init__() + self.state: dict[int, ActionState] = {} + self.var_globals = {} + self.code = code + self.init = init + + @override + def get_exec_input_gates(self) -> list[str]: + return self.gates[0] + + @override + def get_exec_output_gates(self) -> list[str]: + return self.gates[1] + + @override + def get_data_input_gates(self) -> list[str]: + return self.gates[2] + + @override + def get_data_output_gates(self) -> list[str]: + return self.gates[3] + + @override + def nextState(self, exec_id: int) -> tuple["ExecNode", str]: + state = self.get_state(exec_id) + return self.next_node[state.var["output_gate"]] + + def get_state(self, exec_id) -> ActionState: + return self.state[exec_id] + + @override + def generate_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + self.state[exec_id] = (state := ActionState()) + if self.init: + exec (self.init, {"var": state.var}, {"globals": self.var_globals}) + @override + def delete_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + self.state.pop(exec_id) + + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + state = self.get_state(exec_id) + exec( + self.code, + { + "api": od, + "var": state.var, + "data_in": {port: value.get_data(exec_id) for port, value in self.data_in.items() if value is not None}, + "data_out": {port: value.get_data(exec_id) for port, value in self.data_out.items() if value is not None}, + "globals": self.var_globals, + }, + ) + for gate, d in self.data_out.items(): + DataNode.input_event(self, gate, exec_id) + return None + + def input_event(self, gate: str, exec_id: int) -> None: + return + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": f"action", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + }, + ) + ExecNode.generate_dot(self, nodes, edges, visited, template) + DataNode.generate_dot(self, nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/data.py b/transformation/schedule/schedule_lib/data.py new file mode 100644 index 0000000..7cafc5b --- /dev/null +++ b/transformation/schedule/schedule_lib/data.py @@ -0,0 +1,83 @@ +from symtable import Class +from typing import Any, Generator, Callable, Iterator, TYPE_CHECKING, override + +if TYPE_CHECKING: + from transformation.schedule.schedule_lib import DataNode + + +class DataState: + def __init__(self, data: Any): + self.data: list[dict[Any, Any]] = [] + +class Data: + __slots__ = ("state", "_parent") + + def __init__(self, parent: "DataNode") -> None: + self.state: dict[int, DataState] = dict() + self._parent = parent + + def __dir__(self): + return [attr for attr in super().__dir__() if attr != "_super"] + + def get_data(self, exec_id: int) -> list[dict[str, str]]: + state = self.get_state(exec_id) + return state.data + + def get_state(self, exec_id) -> DataState: + return self.state[exec_id] + + def store_data(self, exec_id: int, data_gen: Generator, n: int) -> bool: + state = self.get_state(exec_id) + state.data.clear() + if n == 0: + return True + i: int = 0 + while (match := next(data_gen, None)) is not None: + state.data.append(match) + i += 1 + if i >= n: + break + else: + if n == float("inf"): + return bool(len(state.data)) + state.data.clear() + return False + return True + + def get_parent(self) -> "DataNode": + return self._parent + + def replace(self, exec_id: int, data: list[dict[str, str]]) -> None: + state = self.get_state(exec_id) + state.data.clear() + state.data.extend(data) + + def append(self, exec_id: int, data: dict[str, str]) -> None: + self.get_state(exec_id).data.append(data) + + def extend(self, exec_id: int, data: list[dict[str, str]]) -> None: + self.get_state(exec_id).data.extend(data) + + def clear(self, exec_id: int) -> None: + self.get_state(exec_id).data.clear() + + def pop(self, exec_id: int, index: int =-1) -> Any: + return self.get_state(exec_id).data.pop(index) + + def empty(self, exec_id: int) -> bool: + return len(self.get_state(exec_id).data) == 0 + + def __getitem__(self, index): + raise NotImplementedError + + def __iter__(self, exec_id: int) -> Iterator[dict[str, str]]: + return self.get_state(exec_id).data.__iter__() + + def __len__(self, exec_id: int) -> int: + return self.get_state(exec_id).data.__len__() + + def generate_stack_frame(self, exec_id: int) -> None: + self.state[exec_id] = DataState(exec_id) + + def delete_stack_frame(self, exec_id: int) -> None: + self.state.pop(exec_id) \ No newline at end of file diff --git a/transformation/schedule/schedule_lib/data_node.py b/transformation/schedule/schedule_lib/data_node.py new file mode 100644 index 0000000..01e9b76 --- /dev/null +++ b/transformation/schedule/schedule_lib/data_node.py @@ -0,0 +1,101 @@ +from abc import abstractmethod +from typing import Any, Generator, List, override + +from jinja2 import Template + +from .data import Data +from .funcs import generate_dot_edge +from .node import Node + + +class DataNodeState: + def __init__(self) -> None: + super().__init__() + + +class DataNode(Node): + def __init__(self) -> None: + super().__init__() + self.eventsub: dict[str, list[tuple[DataNode, str]]] = { + gate: [] for gate in self.get_data_output_gates() + } + self.data_out: dict[str, Data] = { + name: Data(self) for name in self.get_data_output_gates() + } + self.data_in: dict[str, Data | None] = { + name: None for name in self.get_data_input_gates() + } + + @staticmethod + def get_data_input_gates() -> List[str]: + return ["in"] + + @staticmethod + def get_data_output_gates() -> List[str]: + return ["out"] + + @override + def generate_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + for d in self.data_out.values(): + d.generate_stack_frame(exec_id) + + @override + def delete_stack_frame(self, exec_id: int) -> None: + super().delete_stack_frame(exec_id) + for d in self.data_out.values(): + d.delete_stack_frame(exec_id) + + def connect_data( + self, data_node: "DataNode", from_gate: str, to_gate: str, eventsub=True + ) -> None: + if from_gate not in self.get_data_output_gates(): + raise Exception(f"from_gate {from_gate} is not a valid port") + if to_gate not in data_node.get_data_input_gates(): + raise Exception(f"to_gate {to_gate} is not a valid port") + data_node.data_in[to_gate] = self.data_out[from_gate] + if eventsub: + self.eventsub[from_gate].append((data_node, to_gate)) + + def store_data(self, exec_id, data_gen: Generator, port: str, n: int) -> None: + self.data_out[port].store_data(exec_id, data_gen, n) + for sub, gate in self.eventsub[port]: + sub.input_event(gate, exec_id) + + def get_input_data(self, gate: str, exec_id: int) -> list[dict[Any, Any]]: + data = self.data_in[gate] + if data is None: + return [{}] + return data.get_data(exec_id) + + @abstractmethod + def input_event(self, gate: str, exec_id: int) -> None: + for sub, gate_sub in self.eventsub[gate]: + sub.input_event(gate_sub, exec_id) + + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + for port, data in self.data_in.items(): + if data is not None: + source = data.get_parent() + generate_dot_edge( + source, + self, + edges, + template, + kwargs={ + "prefix": "d", + "from_gate": [ + port + for port, value in source.data_out.items() + if value == data + ][0], + "to_gate": port, + "color": "green", + }, + ) + data.get_parent().generate_dot(nodes, edges, visited, template) + for gate_form, subs in self.eventsub.items(): + for sub, gate in subs: + sub.generate_dot(nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/end.py b/transformation/schedule/schedule_lib/end.py new file mode 100644 index 0000000..a0218d8 --- /dev/null +++ b/transformation/schedule/schedule_lib/end.py @@ -0,0 +1,80 @@ +from typing import List, override, Type + +from jinja2 import Template + +from api.od import ODAPI +from . import DataNode +from .exec_node import ExecNode +from .funcs import not_visited, generate_dot_node + +class EndState: + def __init__(self) -> None: + self.end_gate: str = "" + +class End(ExecNode, DataNode): + @override + def input_event(self, gate: str, exec_id: int) -> None: + pass + + def __init__(self, ports_exec: List[str], ports_data: List[str]) -> None: + self.ports_exec = ports_exec + self.ports_data = ports_data + super().__init__() + self.state: dict[int, EndState] = {} + + @override + def get_exec_input_gates(self): + return self.ports_exec + + @staticmethod + @override + def get_exec_output_gates(): + return [] + + @override + def get_data_input_gates(self): + return self.ports_data + + @staticmethod + @override + def get_data_output_gates(): + return [] + + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + state = self.get_state(exec_id) + state.end_gate = port + return 1, {"exec_gate": state.end_gate, "data_out": {port: data.get_data(exec_id) for port, data in self.data_in.items()}} + + def get_state(self, exec_id) -> EndState: + return self.state[exec_id] + + @override + def generate_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + self.state[exec_id] = EndState() + + @override + def delete_stack_frame(self, exec_id: int) -> None: + super().delete_stack_frame(exec_id) + self.state.pop(exec_id) + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": "end", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + } + ) diff --git a/transformation/schedule/schedule_lib/exec_node.py b/transformation/schedule/schedule_lib/exec_node.py new file mode 100644 index 0000000..c46125f --- /dev/null +++ b/transformation/schedule/schedule_lib/exec_node.py @@ -0,0 +1,35 @@ +from abc import abstractmethod +from api.od import ODAPI +from .node import Node + + +class ExecNode(Node): + def __init__(self) -> None: + super().__init__() + + from .null_node import NullNode + self.next_node: dict[str, tuple[ExecNode, str]] = {} + for port in self.get_exec_output_gates(): + self.next_node[port] = (NullNode(), "in") + + def nextState(self, exec_id: int) -> tuple["ExecNode", str]: + return self.next_node["out"] + + @staticmethod + def get_exec_input_gates(): + return ["in"] + + @staticmethod + def get_exec_output_gates(): + return ["out"] + + def connect(self, next_state: "ExecNode", from_gate: str, to_gate: str) -> None: + if from_gate not in self.get_exec_output_gates(): + raise Exception(f"from_gate {from_gate} is not a valid port") + if to_gate not in next_state.get_exec_input_gates(): + raise Exception(f"to_gate {to_gate} is not a valid port") + self.next_node[from_gate] = (next_state, to_gate) + + @abstractmethod + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + return None diff --git a/transformation/schedule/schedule_lib/funcs.py b/transformation/schedule/schedule_lib/funcs.py new file mode 100644 index 0000000..6a01eb0 --- /dev/null +++ b/transformation/schedule/schedule_lib/funcs.py @@ -0,0 +1,56 @@ +from typing import Callable, List + +from jinja2 import Template + +from .singleton import Singleton + + +class IdGenerator(metaclass=Singleton): + exec_id = -1 + node_id = -1 + + @classmethod + def generate_node_id(cls) -> int: + cls.node_id +=1 + return cls.node_id + + @classmethod + def generate_exec_id(cls) -> int: + cls.exec_id += 1 + return cls.exec_id + +def generate_dot_wrap(func) -> Callable: + def wrapper(self, *args, **kwargs) -> str: + nodes = [] + edges = [] + self.reset_visited() + func(self, nodes, edges, *args, **kwargs) + return f"digraph G {{\n\t{"\n\t".join(nodes)}\n\t{"\n\t".join(edges)}\n}}" + + return wrapper + + +def not_visited(func) -> Callable: + def wrapper( + self, nodes: List[str], edges: List[str], visited: set[int], *args, **kwargs + ) -> None: + if self in visited: + return + visited.add(self) + func(self, nodes, edges, visited, *args, **kwargs) + + return wrapper + + +def generate_dot_node(self, nodes: List[str], template: Template, **kwargs) -> None: + nodes.append(template.module.__getattribute__("Node")(**{**kwargs, "id": self.id})) + + +def generate_dot_edge( + self, target, edges: List[str], template: Template, kwargs +) -> None: + edges.append( + template.module.__getattribute__("Edge")( + **{**kwargs, "from_id": self.id, "to_id": target.id} + ) + ) diff --git a/transformation/schedule/schedule_lib/loop.py b/transformation/schedule/schedule_lib/loop.py new file mode 100644 index 0000000..8837080 --- /dev/null +++ b/transformation/schedule/schedule_lib/loop.py @@ -0,0 +1,74 @@ +import functools +from typing import List, Generator, override, Type + +from jinja2 import Template + +from api.od import ODAPI +from .exec_node import ExecNode +from .data_node import DataNode +from .data_node import Data +from .funcs import not_visited, generate_dot_node + +class Loop(ExecNode, DataNode): + def __init__(self) -> None: + super().__init__() + self.cur_data: Data = Data(self) + + @staticmethod + @override + def get_exec_output_gates(): + return ["it", "out"] + + @override + def generate_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + self.cur_data.generate_stack_frame(exec_id) + + @override + def delete_stack_frame(self, exec_id: int) -> None: + super().delete_stack_frame(exec_id) + self.cur_data.delete_stack_frame(exec_id) + + @override + def nextState(self, exec_id: int) -> tuple[ExecNode, str]: + return self.next_node["out" if self.data_out["out"].empty(exec_id) else "it"] + + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + self.data_out["out"].clear(exec_id) + + if not self.cur_data.empty(exec_id): + self.data_out["out"].append(exec_id, self.cur_data.pop(exec_id,0)) + DataNode.input_event(self, "out", exec_id) + return None + + def input_event(self, gate: str, exec_id: int) -> None: + self.cur_data.replace(exec_id, self.get_input_data(gate, exec_id)) + data_o = self.data_out["out"] + if data_o.empty(exec_id): + return + data_o.clear(exec_id) + DataNode.input_event(self, "out", exec_id) + + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": f"loop", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + }, + ) + ExecNode.generate_dot(self, nodes, edges, visited, template) + DataNode.generate_dot(self, nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/match.py b/transformation/schedule/schedule_lib/match.py new file mode 100644 index 0000000..f3da3f2 --- /dev/null +++ b/transformation/schedule/schedule_lib/match.py @@ -0,0 +1,67 @@ +from typing import List, override, Type + +from jinja2 import Template + +from api.od import ODAPI +from transformation.schedule.rule_executor import RuleExecutor +from .exec_node import ExecNode +from .data_node import DataNode +from .funcs import not_visited, generate_dot_node + +class Match(ExecNode, DataNode): + def input_event(self, gate: str, exec_id: int) -> None: + pass + + def __init__(self, label: str, n: int | float) -> None: + super().__init__() + self.label: str = label + self.n: int = n + self.rule = None + self.rule_executer: RuleExecutor | None = None + + @override + def nextState(self, exec_id: int) -> tuple[ExecNode, str]: + return self.next_node["fail" if self.data_out["out"].empty(exec_id) else "success"] + + @staticmethod + @override + def get_exec_output_gates(): + return ["success", "fail"] + + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + pivot = {} + if self.data_in is not None: + pivot = self.get_input_data("in", exec_id)[0] + # TODO: remove this print + print(f"matching: {self.label}\n\tpivot: {pivot}") + self.store_data( exec_id, + self.rule_executer.match_rule(od.m, self.rule, pivot=pivot), "out", self.n + ) + return None + + def init_rule(self, rule, rule_executer): + self.rule = rule + self.rule_executer = rule_executer + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": f"match_{self.n}\n{self.label}", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + }, + ) + ExecNode.generate_dot(self, nodes, edges, visited, template) + DataNode.generate_dot(self, nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/merge.py b/transformation/schedule/schedule_lib/merge.py new file mode 100644 index 0000000..d31b809 --- /dev/null +++ b/transformation/schedule/schedule_lib/merge.py @@ -0,0 +1,57 @@ +from typing import List, override, Type + +from jinja2 import Template + +from api.od import ODAPI +from transformation.schedule.rule_executor import RuleExecutor +from . import ExecNode +from .exec_node import ExecNode +from .data_node import DataNode, DataNodeState +from .funcs import not_visited, generate_dot_node + +class Merge(DataNode): + def __init__(self, ports: list[str]) -> None: + self.in_data_ports = ports # ports must be defined before super.__init__ + super().__init__() + self.in_data_ports.reverse() + + @override + def get_data_input_gates(self) -> list[str]: + return self.in_data_ports + + @override + def input_event(self, gate: str, exec_id: int) -> None: + out = self.data_out["out"] + b = (not out.empty(exec_id)) and (self.data_in[gate].empty(exec_id)) + out.clear(exec_id) + if b: + DataNode.input_event(self, "out", exec_id) + return + + # TODO: only first element or all? + if any(data.empty(exec_id) for data in self.data_in.values()): + return + d: dict[str, str] = dict() + for gate in self.in_data_ports: + for key, value in self.data_in[gate].get_data(exec_id)[0].items(): + d[key] = value + out.append(exec_id, d) + DataNode.input_event(self, "out", exec_id) + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": f"merge", + "ports_data": ( + self.get_data_input_gates()[::-1], + self.get_data_output_gates(), + ), + }, + ) + DataNode.generate_dot(self, nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/modify.py b/transformation/schedule/schedule_lib/modify.py new file mode 100644 index 0000000..ad4859e --- /dev/null +++ b/transformation/schedule/schedule_lib/modify.py @@ -0,0 +1,49 @@ +from typing import List, override + +from jinja2 import Template + +from transformation.schedule.schedule_lib.funcs import not_visited, generate_dot_node +from .data_node import DataNode + + +class Modify(DataNode): + def __init__(self, rename: dict[str, str], delete: dict[str, str]) -> None: + super().__init__() + self.rename: dict[str, str] = rename + self.delete: set[str] = set(delete) + + @override + def input_event(self, gate: str, exec_id: int) -> None: + data_i = self.get_input_data(gate, exec_id) + if len(data_i): + self.data_out["out"].clear(exec_id) + for data in data_i: + self.data_out["out"].append(exec_id, + { + self.rename.get(key, key): value + for key, value in data.items() + if key not in self.delete + } + ) + else: + if self.data_out["out"].empty(exec_id): + return + super().input_event("out", exec_id) + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": f"modify", + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + }, + ) + DataNode.generate_dot(self, nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/node.py b/transformation/schedule/schedule_lib/node.py new file mode 100644 index 0000000..022c73c --- /dev/null +++ b/transformation/schedule/schedule_lib/node.py @@ -0,0 +1,70 @@ +""" +node.py + +Defines the abstract base Node class for graph-based structures. Each Node is assigned +a unique identifier via an external IdGenerator. The class provides an interface for +managing execution state and generating DOT graph representations. +""" + +from abc import abstractmethod +from jinja2 import Template +from .funcs import IdGenerator + + +class Node: + """ + Abstract base class for graph nodes. Each Node has a unique ID and supports + context-dependent state management for execution scenarios. Subclasses must + implement the DOT graph generation logic. + """ + + @abstractmethod + def __init__(self) -> None: + """ + Initializes the Node instance with a unique ID. + + Attributes: + id (int): A unique identifier assigned by IdGenerator. + """ + self.id: int = IdGenerator.generate_node_id() + + def get_id(self) -> int: + """ + Retrieves the unique identifier of the node. + + Returns: + int: The unique node ID. + """ + return self.id + + def generate_stack_frame(self, exec_id: int) -> None: + """ + Initializes a new state frame for a specific execution context. + Designed to be overridden in subclasses that use execution state. + + Args: + exec_id (int): The ID of the execution context. + """ + + def delete_stack_frame(self, exec_id: int) -> None: + """ + Deletes the state frame for a specific execution context. + Designed to be overridden in subclasses that use execution state. + + Args: + exec_id (int): The ID of the execution context. + """ + + @abstractmethod + def generate_dot( + self, nodes: list[str], edges: list[str], visited: set[int], template: Template + ) -> None: + """ + Generates the DOT graph representation for this node and its relationships. + + Args: + nodes (list[str]): A list to append DOT node definitions to. + edges (list[str]): A list to append DOT edge definitions to. + visited (set[int]): A set of already visited node IDs to avoid duplicates or recursion. + template (Template): A Jinja2 template used to format the node's DOT representation. + """ diff --git a/transformation/schedule/schedule_lib/null_node.py b/transformation/schedule/schedule_lib/null_node.py new file mode 100644 index 0000000..f7c44ad --- /dev/null +++ b/transformation/schedule/schedule_lib/null_node.py @@ -0,0 +1,80 @@ +""" +null_node.py + +Defines the NullNode class, a no-op singleton execution node used for open execution pins +in the object diagram execution graph. +""" + +from abc import ABC +from typing import List, Type +from jinja2 import Template +from api.od import ODAPI +from .funcs import generate_dot_node +from .singleton import Singleton +from .exec_node import ExecNode + +class NullNode(ExecNode, metaclass=Singleton): + """ + A no-op execution node representing a null operation. + + This node is typically used to represent a placeholder or open execution pin. + It always returns a fixed result and does not perform any operation. + """ + + def __init__(self): + """ + Initializes the NullNode instance. + Inherits unique ID and state behavior from ExecNode. + """ + super().__init__() + + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + """ + Simulates execution by returning a static result indicating an open pin. + + Args: + port (str): The name of the input port. + exec_id (int): The current execution ID. + od (ODAPI): The Object Diagram API instance providing execution context. + + Returns: + tuple[int, str] | None: A tuple (-1, "open pin reached") indicating a no-op. + """ + return -1, "open pin reached" + + @staticmethod + def get_exec_output_gates(): + """ + Returns the list of output gates for execution. + + Returns: + list: An empty list, as NullNode has no output gates. + """ + return [] + + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + """ + Generates DOT graph representation for this node if it hasn't been visited. + + Args: + nodes (List[str]): A list to accumulate DOT node definitions. + edges (List[str]): A list to accumulate DOT edge definitions. + visited (set[int]): Set of already visited node IDs to avoid cycles. + template (Template): A Jinja2 template used to render the node's DOT representation. + """ + if self.id in visited: + return + generate_dot_node( + self, + nodes, + template, + **{ + "label": "null", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + } + ) diff --git a/transformation/schedule/schedule_lib/print.py b/transformation/schedule/schedule_lib/print.py new file mode 100644 index 0000000..3b237a2 --- /dev/null +++ b/transformation/schedule/schedule_lib/print.py @@ -0,0 +1,60 @@ +from typing import List, override + +from jinja2 import Template + +from api.od import ODAPI +from transformation.schedule.schedule_lib.funcs import not_visited, generate_dot_node +from .exec_node import ExecNode +from .data_node import DataNode + + +class Print(ExecNode, DataNode): + def __init__(self, label: str = "", custom: str = "") -> None: + super().__init__() + self.label = label + + if custom: + template = Template(custom, trim_blocks=True, lstrip_blocks=True) + self._print = ( + lambda self_, exec_id: print(template.render(data=self.get_input_data("in", exec_id))) + ).__get__(self, Print) + + @staticmethod + @override + def get_data_output_gates(): + return [] + + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + self._print(exec_id) + return + + @override + def input_event(self, gate: str, exec_id: int) -> None: + if not self.data_in[gate].empty(exec_id): + self._print(exec_id) + + def _print(self, exec_id: int) -> None: + print(f"{self.label}{self.get_input_data("in", exec_id)}") + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": f"print", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + }, + ) + ExecNode.generate_dot(self, nodes, edges, visited, template) + DataNode.generate_dot(self, nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/rewrite.py b/transformation/schedule/schedule_lib/rewrite.py new file mode 100644 index 0000000..ba2be83 --- /dev/null +++ b/transformation/schedule/schedule_lib/rewrite.py @@ -0,0 +1,56 @@ +import functools +from typing import List, Type + +from jinja2 import Template + +from api.od import ODAPI +from .exec_node import ExecNode +from .data_node import DataNode +from .funcs import not_visited, generate_dot_node +from ..rule_executor import RuleExecutor + +class Rewrite(ExecNode, DataNode): + + def __init__(self, label: str) -> None: + super().__init__() + self.label = label + self.rule = None + self.rule_executor: RuleExecutor | None = None + + def init_rule(self, rule, rule_executer): + self.rule = rule + self.rule_executor = rule_executer + + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + pivot = {} + if self.data_in is not None: + pivot = self.get_input_data("in", exec_id)[0] + # TODO: remove print + print(f"rewrite: {self.label}\n\tpivot: {pivot}") + self.store_data( exec_id, + self.rule_executor.rewrite_rule(od, self.rule, pivot=pivot), "out", 1 + ) + return None + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": "rewrite", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + }, + ) + ExecNode.generate_dot(self, nodes, edges, visited, template) + DataNode.generate_dot(self, nodes, edges, visited, template) diff --git a/examples/schedule/schedule_lib/singleton.py b/transformation/schedule/schedule_lib/singleton.py similarity index 99% rename from examples/schedule/schedule_lib/singleton.py rename to transformation/schedule/schedule_lib/singleton.py index 31955e3..91ac5cf 100644 --- a/examples/schedule/schedule_lib/singleton.py +++ b/transformation/schedule/schedule_lib/singleton.py @@ -2,6 +2,7 @@ from abc import ABCMeta class Singleton(ABCMeta): _instances = {} + def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) diff --git a/transformation/schedule/schedule_lib/start.py b/transformation/schedule/schedule_lib/start.py new file mode 100644 index 0000000..441e95f --- /dev/null +++ b/transformation/schedule/schedule_lib/start.py @@ -0,0 +1,83 @@ +from typing import List, override + +from jinja2 import Template + +from . import DataNode +from .exec_node import ExecNode +from .funcs import not_visited, generate_dot_node + +class StartState: + def __init__(self) -> None: + super().__init__() + self.start_gate: str = "" + +class Start(ExecNode, DataNode): + def __init__(self, ports_exec: List[str], ports_data: List[str]) -> None: + self.state: dict[int, StartState] = {} + self.ports_exec = ports_exec + self.ports_data = ports_data + super().__init__() + + def run_init(self, gate: str, exec_id: int, data: dict[str, any]) -> None: + state = self.get_state(exec_id) + state.start_gate = gate + for port, d in data.items(): + self.data_out[port].replace(exec_id, d) + DataNode.input_event(self, port, exec_id) + + def nextState(self, exec_id: int) -> tuple["ExecNode", str]: + state = self.get_state(exec_id) + return self.next_node[state.start_gate] + + def get_state(self, exec_id) -> StartState: + return self.state[exec_id] + + @override + def generate_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + self.state[exec_id] = StartState() + + @override + def delete_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + self.state.pop(exec_id) + + @staticmethod + @override + def get_exec_input_gates(): + return [] + + @override + def get_exec_output_gates(self): + return self.ports_exec + + @staticmethod + @override + def get_data_input_gates(): + return [] + + @override + def get_data_output_gates(self): + return self.ports_data + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": "start", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + } + ) + super().generate_dot(nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/store.py b/transformation/schedule/schedule_lib/store.py new file mode 100644 index 0000000..4aced26 --- /dev/null +++ b/transformation/schedule/schedule_lib/store.py @@ -0,0 +1,92 @@ +from typing import List, override + +from jinja2 import Template + +from api.od import ODAPI +from .data import Data +from .exec_node import ExecNode +from .data_node import DataNode +from .funcs import not_visited, generate_dot_node + +class StoreState: + def __init__(self) -> None: + self.last_port: str = "in" + +class Store(ExecNode, DataNode): + def __init__(self, ports: list[str]) -> None: + self.ports = ports + super().__init__() + self.state: dict[int, StoreState] = {} + self.cur_data: Data = Data(self) + + @override + def get_exec_input_gates(self) -> list[str]: + return [*self.ports, "in"] + + @override + def get_exec_output_gates(self) -> list[str]: + return [*self.ports, "out"] + + @override + def get_data_input_gates(self) -> list[str]: + return self.ports + + @override + def nextState(self, exec_id: int) -> tuple[ExecNode, str]: + return self.next_node[self.get_state(exec_id).last_port] + + @override + def input_event(self, gate: str, exec_id: int) -> None: + return + + def get_state(self, exec_id) -> StoreState: + return self.state[exec_id] + + @override + def generate_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + self.state[exec_id] = StoreState() + self.cur_data.generate_stack_frame(exec_id) + + @override + def delete_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + self.state.pop(exec_id) + self.cur_data.delete_stack_frame(exec_id) + + + @override + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + state = self.get_state(exec_id) + if port == "in": + self.data_out["out"].replace(exec_id, self.cur_data.get_data(exec_id)) + self.cur_data.clear(exec_id) + DataNode.input_event(self, "out", True) + state.last_port = "out" + return None + self.cur_data.extend(exec_id, self.get_input_data(port, exec_id)) + state.last_port = port + return None + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": f"store", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + }, + ) + ExecNode.generate_dot(self, nodes, edges, visited, template) + DataNode.generate_dot(self, nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/sub_schedule.py b/transformation/schedule/schedule_lib/sub_schedule.py new file mode 100644 index 0000000..7b97e43 --- /dev/null +++ b/transformation/schedule/schedule_lib/sub_schedule.py @@ -0,0 +1,107 @@ +from typing import List, override, TYPE_CHECKING + +from jinja2 import Template + +from api.od import ODAPI +from . import DataNode +from .exec_node import ExecNode +from .funcs import not_visited, generate_dot_node, IdGenerator + +if TYPE_CHECKING: + from ..rule_scheduler import RuleSchedular + + +class ScheduleState: + def __init__(self) -> None: + self.end_gate: str = "" + +class SubSchedule(ExecNode, DataNode): + def __init__(self, schedular: "RuleSchedular", file: str) -> None: + self.schedule = schedular._load_schedule(file, _main=False) + self.schedular = schedular + super().__init__() + self.state: dict[int, ScheduleState] = {} + + @override + def nextState(self, exec_id: int) -> tuple["ExecNode", str]: + return self.next_node[self.get_state(exec_id).end_gate] + + @override + def get_exec_input_gates(self) -> "List[ExecNode]": + return self.schedule.start.get_exec_output_gates() + + @override + def get_exec_output_gates(self) -> "List[ExecNode]": + return [*self.schedule.end.get_exec_input_gates()] + + @override + def get_data_input_gates(self) -> "List[ExecNode]": + return self.schedule.start.get_data_output_gates() + + @override + def get_data_output_gates(self) -> "List[ExecNode]": + return self.schedule.end.get_data_input_gates() + + def get_state(self, exec_id) -> ScheduleState: + return self.state[exec_id] + + @override + def generate_stack_frame(self, exec_id: int) -> None: + super().generate_stack_frame(exec_id) + self.state[exec_id] = ScheduleState() + + @override + def delete_stack_frame(self, exec_id: int) -> None: + super().delete_stack_frame(exec_id) + self.state.pop(exec_id) + + + @override + def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: + runstatus, result = self.schedular._runner( + od, + self.schedule, + port, + IdGenerator.generate_exec_id(), + { + port: self.get_input_data(port, exec_id) + for port, value in self.data_in.items() + if value is not None and not value.empty(exec_id) + }, + ) + if runstatus != 1: + return runstatus, result + self.get_state(exec_id).end_gate = result["exec_gate"] + results_data = result["data_out"] + for port, data in self.data_out.items(): + if port in results_data: + self.data_out[port].replace(exec_id, results_data[port]) + DataNode.input_event(self, port, exec_id) + continue + + if not data.empty(exec_id): + data.clear(exec_id) + DataNode.input_event(self, port, exec_id) + return None + + @not_visited + def generate_dot( + self, nodes: List[str], edges: List[str], visited: set[int], template: Template + ) -> None: + generate_dot_node( + self, + nodes, + template, + **{ + "label": "rrrrrrrrrr", + "ports_exec": ( + self.get_exec_input_gates(), + self.get_exec_output_gates(), + ), + "ports_data": ( + self.get_data_input_gates(), + self.get_data_output_gates(), + ), + } + ) + super().generate_dot(nodes, edges, visited, template) diff --git a/transformation/schedule/templates/schedule_dot.j2 b/transformation/schedule/templates/schedule_dot.j2 new file mode 100644 index 0000000..7937884 --- /dev/null +++ b/transformation/schedule/templates/schedule_dot.j2 @@ -0,0 +1,60 @@ +digraph G { + rankdir=LR; + compound=true; + node [shape=rect]; +{% for node in nodes %} + {{ node }} +{% endfor %} + +{% for edge in edges %} + {{ edge }} +{% endfor %} +} + +{% macro Node(label, id, ports_exec=[], ports_data=[]) %} +subgraph cluster_{{ id }} { + label = "{{ id }}__{{ label }}"; + style = rounded; + input_{{ id }} [ + shape=rect; + label= {{ Gate_Table(ports_exec[0], ports_data[0]) }} + ]; + output_{{ id }} [ + shape=rect; + label= {{ Gate_Table(ports_exec[1], ports_data[1]) }} + ]; + input_{{ id }}->output_{{ id }} [style=invis]; + } +{%- endmacro %} + +{%- macro Edge(from_id, to_id, from_gate, to_gate, prefix, color) %} +output_{{ from_id }}:{{ prefix }}_{{ from_gate }} -> input_{{ to_id }}:{{ prefix }}_{{ to_gate }} [color = {{ color }}] +{%- endmacro %} + +{%- macro Gate_Table(ports_exec, ports_data) %} + + < + {% if ports_exec or ports_data %} + {% if ports_exec %} + + {% endif %} + {% if ports_data %} + + {% endif %} + {% else %} + + {% endif %} +
+ + {% for port_e in ports_exec %} + + {% endfor %} +
{{ port_e }}
+
+ + {% for port_d in ports_data %} + + {% endfor %} +
{{ port_d }}
+
X
> +{%- endmacro %} \ No newline at end of file diff --git a/transformation/schedule/templates/schedule_muMLE.j2 b/transformation/schedule/templates/schedule_muMLE.j2 new file mode 100644 index 0000000..624b203 --- /dev/null +++ b/transformation/schedule/templates/schedule_muMLE.j2 @@ -0,0 +1,28 @@ +{% for id, param in nodes.items() -%} + {{ param[0] }}:{{ param[1].pop("type") }} + {%- if param[1] %} + { + {% for key, value in param[1].items() %} + {% if value %} + {% if key in ["file"] %} + {% set value = '"' ~ value ~ '"' %} + {% elif key in ["custom"] %} + {% set value = '`"' ~ value.replace('\n', '\\n') ~ '"`' %} + {% elif key in ["action", "init"] %} + {% set value = '\n```\n' ~ value ~ '\n```' %} + {% elif key in ["ports", "ports_exec_in", "ports_exec_out", "ports_data_in", "ports_data_out", "rename", "delete"] %} + {% set value = '`' ~ value.replace('\n', '\\n') ~ '`' %} + {% endif %} + {{ key }} = {{ value }}; + {% endif %} + {% endfor %} +} + {% endif %} + +{% endfor %} + +{%- for edge in edges %} + {% set source = edge[0] %} + {% set target = edge[1] %} +:Conn_{{ source[2] }} ({{ source[0] }} -> {{ target[0] }}) {from="{{ source[1] }}"; to="{{ target[1] }}";} +{% endfor -%} \ No newline at end of file diff --git a/transformation/schedule/templates/schedule_template.j2 b/transformation/schedule/templates/schedule_template.j2 new file mode 100644 index 0000000..6075b61 --- /dev/null +++ b/transformation/schedule/templates/schedule_template.j2 @@ -0,0 +1,51 @@ +{% macro Start(name, ports_exec_out, ports_data_out) %} +{{ name }} = Start({{ ports_exec_out }}, {{ ports_data_out }}) +{%- endmacro %} + +{% macro End(name, ports_exec_in, ports_data_in) %} +{{ name }} = End({{ ports_exec_in }}, {{ ports_data_in }}) +{%- endmacro %} + +{% macro Match(name, file, n) %} +{{ name }} = Match("{{ file }}", {{ n }}) +{%- endmacro %} + +{% macro Rewrite(name, file) %} +{{ name }} = Rewrite("{{ file }}") +{%- endmacro %} + +{% macro Action(name, ports_exec_in, ports_exec_out, ports_data_in, ports_data_out, action, init) %} +{{ name }} = Action({{ ports_exec_in }}, {{ ports_exec_out }}, {{ ports_data_in }}, {{ ports_data_out }}, {{ action }}, {{ init }}) +{%- endmacro %} + +{% macro Modify(name, rename, delete) %} +{{ name }} = Modify({{ rename }}, {{ delete }}) +{%- endmacro %} + +{% macro Merge(name, ports_data_in) %} +{{ name }} = Merge({{ ports_data_in }}) +{%- endmacro %} + +{% macro Store(name, ports) %} +{{ name }} = Store({{ ports }}) +{%- endmacro %} + +{% macro Schedule(name, file) %} +{{ name }} = SubSchedule(schedular, "{{ file }}") +{%- endmacro %} + +{% macro Loop(name) %} +{{ name }} = Loop() +{%- endmacro %} + +{% macro Print(name, label, custom) %} +{{ name }} = Print("{{ label }}", {{ custom }}) +{%- endmacro %} + +{% macro Conn_exec(name_from, name_to, from, to) %} +{{ name_from }}.connect({{ name_to }},"{{ from }}","{{ to }}") +{%- endmacro %} + +{% macro Conn_data(name_from, name_to, from, to, event) %} +{{ name_from }}.connect_data({{ name_to }}, "{{ from }}", "{{ to }}", {{ event }}) +{%- endmacro %} \ No newline at end of file diff --git a/transformation/schedule/templates/schedule_template_wrap.j2 b/transformation/schedule/templates/schedule_template_wrap.j2 new file mode 100644 index 0000000..59bb425 --- /dev/null +++ b/transformation/schedule/templates/schedule_template_wrap.j2 @@ -0,0 +1,48 @@ +#generated from somewhere i do not now but it here so live with it + +from transformation.schedule.schedule_lib import * + +class Schedule: + def __init__(self): + self.start: Start | None = None + self.end: End | None = None + self.nodes: list[DataNode] = [] + + @staticmethod + def get_matchers(): + return [ + {% for file in match_files %} + "{{ file }}", + {% endfor %} + ] + + def init_schedule(self, schedular, rule_executer, matchers): + {% for block in blocks_start_end%} + {{ block }} + {% endfor %} + self.start = {{ start }} + self.end = {{ end }} + {% for block in blocks%} + {{ block }} + {% endfor %} + + {% for conn in exec_conn%} + {{ conn }} + {% endfor %} + {% for conn_d in data_conn%} + {{ conn_d }} + {% endfor %} + + {% for match in matchers %} + {{ match["name"] }}.init_rule(matchers["{{ match["file"] }}"], rule_executer) + {% endfor %} + + self.nodes = [ + {% for name in blocks_name%} + {{ name }}, + {% endfor %} + ] + return None + + def generate_dot(self, *args, **kwargs): + return self.start.generate_dot(*args, **kwargs) \ No newline at end of file From fd6c8b4277802a7c261a638c3b6da3f363e55cb4 Mon Sep 17 00:00:00 2001 From: robbe Date: Mon, 30 Jun 2025 18:03:24 +0200 Subject: [PATCH 6/8] Added some documentation, fixed test and missing schedule --- examples/geraniums/runner.py | 2 +- .../models/schedules/petrinet3.drawio | 915 ++++++++++++++++++ examples/petrinet/runner.py | 6 +- .../schedule/Tests/Test_meta_model.py | 71 +- .../schedule/Tests/Test_xmlparser.py | 6 +- .../models/schedule/connections_merge.od | 10 +- .../models/schedule/connections_modify.od | 7 +- .../schedule/doc/images/example_1.png | Bin 0 -> 38048 bytes .../schedule/doc/images/example_2.png | Bin 0 -> 82009 bytes .../schedule/doc/images/example_3.png | Bin 0 -> 95953 bytes .../schedule/doc/images/geraniums-main.png | Bin 0 -> 45560 bytes .../doc/images/geraniums-repot_flowers.png | Bin 0 -> 36156 bytes transformation/schedule/doc/schedule.md | 251 +++++ .../schedule/doc/schedule_lib/end.md | 1 + .../README.md => doc/schedule_lib/node.md} | 0 .../schedule/doc/schedule_lib/start.md | 1 + .../schedule/models/scheduling_MM.od | 25 +- transformation/schedule/rule_scheduler.py | 2 +- transformation/schedule/schedule.pyi | 4 +- .../schedule/schedule_lib/exec_node.py | 26 + transformation/schedule/schedule_lib/match.py | 2 +- .../schedule/schedule_lib/rewrite.py | 2 +- .../schedule/schedule_lib/sub_schedule.py | 10 +- .../schedule/templates/schedule_dot.j2 | 11 +- .../schedule/templates/schedule_template.j2 | 2 +- .../templates/schedule_template_wrap.j2 | 2 +- 26 files changed, 1284 insertions(+), 72 deletions(-) create mode 100644 examples/petrinet/models/schedules/petrinet3.drawio create mode 100644 transformation/schedule/doc/images/example_1.png create mode 100644 transformation/schedule/doc/images/example_2.png create mode 100644 transformation/schedule/doc/images/example_3.png create mode 100644 transformation/schedule/doc/images/geraniums-main.png create mode 100644 transformation/schedule/doc/images/geraniums-repot_flowers.png create mode 100644 transformation/schedule/doc/schedule.md create mode 100644 transformation/schedule/doc/schedule_lib/end.md rename transformation/schedule/{schedule_lib/README.md => doc/schedule_lib/node.md} (100%) create mode 100644 transformation/schedule/doc/schedule_lib/start.md diff --git a/examples/geraniums/runner.py b/examples/geraniums/runner.py index fdaaa68..cd72db6 100644 --- a/examples/geraniums/runner.py +++ b/examples/geraniums/runner.py @@ -34,7 +34,7 @@ if __name__ == "__main__": print(render_conformance_check_result(conf_err)) mm_ramified = ramify(state, mm) - action_generator = RuleSchedular(state, mm, mm_ramified, verbose=True, directory="examples/geraniums", eval_context=eval_context) + action_generator = RuleScheduler(state, mm, mm_ramified, verbose=True, directory="examples/geraniums", eval_context=eval_context) od = ODAPI(state, m, mm) render_geraniums_dot(od, f"{THIS_DIR}/geraniums.dot") diff --git a/examples/petrinet/models/schedules/petrinet3.drawio b/examples/petrinet/models/schedules/petrinet3.drawio new file mode 100644 index 0000000..4e701fe --- /dev/null +++ b/examples/petrinet/models/schedules/petrinet3.drawio @@ -0,0 +1,915 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/petrinet/runner.py b/examples/petrinet/runner.py index 6df572a..6e99a96 100644 --- a/examples/petrinet/runner.py +++ b/examples/petrinet/runner.py @@ -41,13 +41,13 @@ if __name__ == "__main__": - action_generator = RuleSchedular(state, mm_rt, mm_rt_ramified, verbose=True, directory="models") + scheduler = RuleScheduler(state, mm_rt, mm_rt_ramified, verbose=True, directory="models") # if action_generator.load_schedule(f"petrinet.od"): # if action_generator.load_schedule("schedules/combinatory.drawio"): if action_generator.load_schedule("schedules/petrinet3.drawio"): - action_generator.generate_dot("../dot.dot") - code, message = action_generator.run(ODAPI(state, m_rt_initial, mm_rt)) + scheduler.generate_dot("../dot.dot") + code, message = scheduler.run(ODAPI(state, m_rt_initial, mm_rt)) print(f"{code}: {message}") diff --git a/transformation/schedule/Tests/Test_meta_model.py b/transformation/schedule/Tests/Test_meta_model.py index 47392c1..a0ef942 100644 --- a/transformation/schedule/Tests/Test_meta_model.py +++ b/transformation/schedule/Tests/Test_meta_model.py @@ -7,12 +7,9 @@ sys.path.insert( 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")) ) -from icecream import ic - from api.od import ODAPI from bootstrap.scd import bootstrap_scd -from examples.schedule import rule_schedular -from examples.schedule.rule_schedular import ScheduleActionGenerator +from transformation.schedule.rule_scheduler import RuleScheduler from state.devstate import DevState from transformation.ramify import ramify from util import loader @@ -37,7 +34,7 @@ class Test_Meta_Model(unittest.TestCase): def setUp(self): self.model = ODAPI(*self.model_param) self.out = io.StringIO() - self.generator = ScheduleActionGenerator( + self.generator = RuleScheduler( *self.generator_param, directory=self.dir + "/models", verbose=True, @@ -50,9 +47,7 @@ class Test_Meta_Model(unittest.TestCase): try: self.generator.load_schedule(f"schedule/{file}") errors = self.out.getvalue().split("\u25b8")[1:] - ic(errors) if len(errors) != len(expected_substr_err.keys()): - ic("len total errors") assert len(errors) == len(expected_substr_err.keys()) for err in errors: error_lines = err.strip().split("\n") @@ -62,13 +57,9 @@ class Test_Meta_Model(unittest.TestCase): key = key_pattern break else: - ic("no matching key") - ic(line) assert False expected = expected_substr_err[key] if (len(error_lines) - 1) != len(expected): - ic("len substr errors") - ic(line) assert (len(error_lines) - 1) == len(expected) it = error_lines.__iter__() it.__next__() @@ -77,15 +68,11 @@ class Test_Meta_Model(unittest.TestCase): all(exp in err_line for exp in line_exp) for line_exp in expected ): - ic("wrong substr error") - ic(line) - ic(error_lines) assert False expected_substr_err.pop(key) except AssertionError: raise except Exception as e: - ic(e) assert False def test_no_start(self): @@ -101,12 +88,15 @@ class Test_Meta_Model(unittest.TestCase): self._test_conformance("multiple_end.od", {("End", "Cardinality"): []}) def test_connections_start(self): + # try to load the following schedule. + # The schedules contains happy day nodes and faulty nodes. + # Use the error messages to select error location and further validate the multiple reasons of failure. self._test_conformance( "connections_start.od", { - ("Start", "start"): [ - ["input exec", "foo_in", "exist"], - ["output exec", "out", "multiple"], + ("Start", "start"): [ # locate failure (contains these two substrings), make sure other do not fully overlap -> flakey test + ["input exec", "foo_in", "exist"], # 4 total reasons, a reason contains these three substrings + ["output exec", "out", "multiple"], # a reason will match to exactly one subnstring list ["output exec", "foo_out", "exist"], ["input data", "in", "exist"], ] @@ -180,32 +170,52 @@ class Test_Meta_Model(unittest.TestCase): ) def test_connections_modify(self): + #TODO: + # see test_connections_merge self._test_conformance( "connections_modify.od", { + ("Invalid source", "Conn_exec"): [], + ("Invalid target", "Conn_exec"): [], ("Modify", "m_foo"): [ - ["input exec", "in", "exist"], - ["input exec", "in", "exist"], - ["output exec", "out", "exist"], ["input data", "foo_in", "exist"], ["output data", "foo_out", "exist"], ["input data", "in", "multiple"], + ], + ("Modify", "m_exec"): [ + ["input exec", "in", "exist"], + ["input exec", "in", "exist"], + ["output exec", "out", "exist"], ] }, ) def test_connections_merge(self): + #TODO: + # mm: + # association Conn_exec [0..*] Exec -> Exec [0..*] { + # ...; + # } + # m: + # Conn_exec ( Data -> Exec) {...;} -> Invalid source type 'Merge' for link '__Conn_exec_3:Conn_exec' (1) + # -> Invalid target type 'End' for link '__Conn_exec_3:Conn_exec' (2) + # Conn_exec ( Exec -> Data) {...;} -> No error at all, inconsistent and unexpected behaviour (3) + # different combinations behave unexpected + self._test_conformance( "connections_merge.od", { + ("Invalid source", "Conn_exec"): [], # (1), expected + ("Invalid target", "Conn_exec"): [], # (2), invalid error, should not be shown ("Merge", "m_foo"): [ - ["input exec", "in", "exist"], + ["input data", "foo_in", "exist"], + ["input data", "in2", "multiple"], + ["output data", "foo_out", "exist"], + ], + ("Merge", "m_exec"): [ # (3), checked in Merge itself ["input exec", "in", "exist"], ["output exec", "out", "exist"], - ["input data", "foo_in", "exist"], - ["output data", "foo_out", "exist"], - ["input data", "in2", "multiple"], - ] + ], }, ) @@ -274,7 +284,7 @@ class Test_Meta_Model(unittest.TestCase): ["Unexpected type", "ports_exec_out", "str"], ["Unexpected type", "ports_data_out", "str"], ], - ("Start", '"int"'): [ + ("Start", '"int"'): [ # included " to avoid flakey test ["Unexpected type", "ports_exec_out", "int"], ["Unexpected type", "ports_data_out", "int"], ], @@ -380,13 +390,16 @@ class Test_Meta_Model(unittest.TestCase): ["Unexpected type", "ports_data_out", "NoneType"], ["Unexpected type", "ports_data_in", "NoneType"], ], - ("Action", '"invalid"'): [ + ('"Action"', '"invalid"'): [ ["Invalid python", "ports_exec_out"], ["Invalid python", "ports_exec_in"], ["Invalid python", "ports_data_out"], ["Invalid python", "ports_data_in"], ], - ("Action_action", "invalid_action"): [], + ('"Action_action"', '"invalid_action"'): [ + ["Invalid python code"], + ["line"], + ], ("Action", "subtype"): [ ["Unexpected type", "ports_exec_out", "list"], ["Unexpected type", "ports_exec_in", "list"], diff --git a/transformation/schedule/Tests/Test_xmlparser.py b/transformation/schedule/Tests/Test_xmlparser.py index 0530ee7..b3b8a08 100644 --- a/transformation/schedule/Tests/Test_xmlparser.py +++ b/transformation/schedule/Tests/Test_xmlparser.py @@ -1,16 +1,14 @@ -import io import os import unittest -from transformation.schedule import rule_scheduler -from transformation.schedule.rule_scheduler import RuleSchedular +from transformation.schedule.rule_scheduler import RuleScheduler from state.devstate import DevState class MyTestCase(unittest.TestCase): def setUp(self): state = DevState() - self.generator = RuleSchedular(state, "", "") + self.generator = RuleScheduler(state, "", "") def test_empty(self): try: diff --git a/transformation/schedule/Tests/models/schedule/connections_merge.od b/transformation/schedule/Tests/models/schedule/connections_merge.od index b564525..8144496 100644 --- a/transformation/schedule/Tests/models/schedule/connections_merge.od +++ b/transformation/schedule/Tests/models/schedule/connections_merge.od @@ -12,6 +12,10 @@ m3:Match{ file="rules/transition.od"; } +m_exec:Merge { + ports_data_in = `["in1", "in2"]`; +} + m_foo:Merge { ports_data_in = `["in1", "in2"]`; } @@ -28,10 +32,8 @@ end:End { :Conn_exec (m -> m2) {from="fail";to="in";} :Conn_exec (m -> m3) {from="success";to="in";} -:Conn_exec (m2 -> m_foo) {from="success";to="in";} -:Conn_exec (m2 -> m_foo) {from="fail";to="in";} - -:Conn_exec (m_foo -> end) {from="out";to="in";} +:Conn_exec (m2 -> m_exec) {from="success";to="in";} +:Conn_exec (m_exec -> end) {from="out";to="in";} :Conn_data (start -> m_foo) {from="1";to="in1";} :Conn_data (start -> m_foo) {from="1";to="in2";} diff --git a/transformation/schedule/Tests/models/schedule/connections_modify.od b/transformation/schedule/Tests/models/schedule/connections_modify.od index f3eebdc..9027d0c 100644 --- a/transformation/schedule/Tests/models/schedule/connections_modify.od +++ b/transformation/schedule/Tests/models/schedule/connections_modify.od @@ -12,6 +12,7 @@ m3:Match{ file="rules/transition.od"; } +m_exec:Modify m_foo:Modify m_void:Modify @@ -25,10 +26,10 @@ end:End { :Conn_exec (m -> m2) {from="fail";to="in";} :Conn_exec (m -> m3) {from="success";to="in";} -:Conn_exec (m2 -> m_foo) {from="success";to="in";} -:Conn_exec (m2 -> m_foo) {from="fail";to="in";} +:Conn_exec (m2 -> m_exec) {from="success";to="in";} +:Conn_exec (m2 -> m_exec) {from="fail";to="in";} -:Conn_exec (m_foo -> end) {from="out";to="in";} +:Conn_exec (m_exec -> end) {from="out";to="in";} :Conn_data (start -> mo) {from="1";to="in";} :Conn_data (mo -> m2) {from="out";to="in";} diff --git a/transformation/schedule/doc/images/example_1.png b/transformation/schedule/doc/images/example_1.png new file mode 100644 index 0000000000000000000000000000000000000000..8ea0451dc08b8853b12f0b9078445c1603999a07 GIT binary patch literal 38048 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 FngB4*P2&In literal 0 HcmV?d00001 diff --git a/transformation/schedule/doc/images/example_2.png b/transformation/schedule/doc/images/example_2.png new file mode 100644 index 0000000000000000000000000000000000000000..40994fdc4d4ae470c8692c85188ce2802fe3f094 GIT binary patch literal 82009 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 literal 0 HcmV?d00001 diff --git a/transformation/schedule/doc/images/example_3.png b/transformation/schedule/doc/images/example_3.png new file mode 100644 index 0000000000000000000000000000000000000000..d3092bba7675a11e72e564d045e734f02d4616c8 GIT binary patch literal 95953 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 fga3{oMI{Ua)hPANFw^X7Q;MFR+F5t3 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

ZjG6 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 literal 0 HcmV?d00001 diff --git a/transformation/schedule/doc/images/geraniums-main.png b/transformation/schedule/doc/images/geraniums-main.png new file mode 100644 index 0000000000000000000000000000000000000000..42c717455c80cf310342221af2aaee7ba1fec13e GIT binary patch literal 45560 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 S$tng01_n=8KbLh*2~7a*!hjn9 literal 0 HcmV?d00001 diff --git a/transformation/schedule/doc/images/geraniums-repot_flowers.png b/transformation/schedule/doc/images/geraniums-repot_flowers.png new file mode 100644 index 0000000000000000000000000000000000000000..4a89de0c58ed5714eb1683f9b3c37acc9a1b42d6 GIT binary patch literal 36156 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 L>gTe~DWM4fS?F8B literal 0 HcmV?d00001 diff --git a/transformation/schedule/doc/schedule.md b/transformation/schedule/doc/schedule.md new file mode 100644 index 0000000..39837f9 --- /dev/null +++ b/transformation/schedule/doc/schedule.md @@ -0,0 +1,251 @@ +# Schedule Module + +This module is used to define and execute model transformations using a schedule in the muMLE framework. +The development of this module is port of a research project of Robbe Teughels with Joeri Exelmans and Hans Vangheluwe. + +## Module Structure + +The entire module is wrapped in single interface [schedule.py](../rule_scheduler.py) responsible for loading, executing and other optional functionalities, such as generating dot files. +Loading modules (.py and .drawio) requires compilation. All these transformations are grouped together in [generator.py](../generator.py). +The interactions with the muMLE framework uses the custom interface: [rule_executor.py](../rule_executor.py). This reduces the dependency between the module and the framework. + +Schedules are compiled to python files. These files have a fixed interface defined in [schedule.pyi](../schedule.pyi). +This interface includes functionalities that will setup the schedule structure and link patterns or other schedules from the module interface with the nodes. +The compiled files do not include any functional implementation to reduce their size and compile time. They are linked to a libary [schedule_lib](../schedule_lib) including an implementation for each node type. +This means that nodes can be treated as a black box by the schedule. This architecture allowing easier testing of the library as generation is fully independent of the core implementation. + +The implementation of a given node is similar in the inheritance compared to the original meta-model to increasing traceability between the original instance and the compiled instance. + +## Usage + +### Running Module + +```python + +from state.devstate import DevState +from bootstrap.scd import bootstrap_scd +from util import loader +from transformation.ramify import ramify +from api.od import ODAPI +from transformation.schedule.rule_scheduler import RuleScheduler + +state = DevState() +scd_mmm = bootstrap_scd(state) + +# load model and meta-model +metamodel_cs = open('your_metamodel.od', 'r', encoding="utf-8").read() +model_cs = open('your_model.od', 'r', encoding="utf-8").read() + +# Parse them +metamodel = loader.parse_and_check(state, metamodel_cs, scd_mmm, "your_metamodel") +model = loader.parse_and_check(state, model_cs, metamodel, "Example model") + +# Ramified model +metamodel_ramified = ramify(state, metamodel) + +# scheduler +scheduler = RuleScheduler(state, metamodel, metamodel_ramified) + +# load schedule +scheduler.load_schedule("your_schedule.od") +# scheduler.load_schedule("your_schedule.py") # compiled version (without conformance checking) +# scheduler.load_schedule("your_schedule.drawio") # main page will be executed + +# execute model transformation +api = ODAPI(state, model, metamodel) +scheduler.run(api) +``` + +#### Simple example schedules (.od format) + +A schedule is executed from start to end or NullNode (reachable only from unconnected exec-gates). +Given the following basic schedule (ARule without NAC), the first match of the pre-condition_pattern is used to rewrite the host graph. +This schedule expect at least one match as the `fail' exec-gate of the match is not connected. +Zero matches leads to a NullState, resulting in early termination. + +```markdown +start:Start +end:End + +# match once +m:Match{ + file = "your_pre-condition_pattern.od"; + n = 1; +} + +# rewrite +r:Rewrite{ + file = "your_post-condition_pattern.od"; +} + +:Conn_exec (start -> m) {from="out"; to="in";} +:Conn_exec (m -> r) {from="success"; to="in";} +:Conn_exec (r -> end) {from="out"; to="in";} + +:Conn_data (m -> r) {from="out"; to="in";} +``` +![schedule_1](images/example_1.png) + +With some small adjustments, all matches can be rewritten (FRule without NAC) + +```markdown +start:Start +end:End + +# match all +m:Match{ + file = "your_pre-condition_pattern.od"; + # n = +INF (if missing: all matches) +} + +l:Loop + +# rewrite +r:Rewrite{ + file = "your_post-condition_pattern.od"; +} + +:Conn_exec (start -> m) {from="out"; to="in";} +:Conn_exec (m -> l) {from="success"; to="in";} +:Conn_exec (l -> r) {from="it"; to="in";} +:Conn_exec (r -> l) {from="out"; to="in";} +:Conn_exec (l -> end) {from="out"; to="in";} + +:Conn_data (m -> l) {from="out"; to="in";} +:Conn_data (l -> r) {from="out"; to="in";} +``` +![schedule_2](images/example_2.png) + +Adding a NAC to this example: adding a match using the previous match and expecting it to fail. (FRule with NAC) + +```markdown +start:Start +end:End + +# match all +m:Match{ + file = "your_pre-condition_pattern.od"; + # n = +INF (if missing: all matches) +} + +l:Loop + +# NAC +n:Match{ + file = "your_NAC_pre-condition_pattern.od"; + n = 1; # one fail is enough +} + +# rewrite +r:Rewrite{ + file = "your_post-condition_pattern.od"; +} + +:Conn_exec (start -> m) {from="out"; to="in";} +:Conn_exec (m -> l) {from="success"; to="in";} +:Conn_exec (l -> n) {from="it"; to="in";} +:Conn_exec (n -> r) {from="fail"; to="in";} +:Conn_exec (r -> l) {from="out"; to="in";} +:Conn_exec (l -> end) {from="out"; to="in";} + +:Conn_data (m -> l) {from="out"; to="in";} +:Conn_data (l -> n) {from="out"; to="in";} +:Conn_data (l -> r) {from="out"; to="in";} +``` +![schedule_3](images/example_3.png) + +## Node Types + +### Start +This node indicates the start of a schedule. +It signature (additional ports) can be used to insert match sets or alternative exec-paths, increasing reusability. + +[Start](schedule_lib/start.md) + +### End +Counterpart to Start node. Reaching this node result in successful termination of the schedule. +It signature (additional ports) can be used to extract match sets or alternative exec-paths, increasing reusability. + +[End](schedule_lib/end.md) + +### Match +Matches a pre-condition pattern on the host-graph. A primitive defined in T-Core + +[Match](schedule_lib/match.md) + +### Rewrite +Rewrite the host-graph using a post-condition pattern. A primitive defined in T-Core + +[Rewrite](schedule_lib/rewrite.md) + +### Modify +Modifies the match set. This allows patterns to name elements to their linking. +This node modifies or deletes elements to be usable as pivot in another pattern with different names. +An example usage can be found in [examples/geraniums](../../../examples/geraniums). + +In the following schedule, a cracked filed was matched and no longer needed. +The Modify node deletes this, allowing for the flowering flower match node to use a pattern without this element, reducing the size and making it more general. + ![geraniums_main](images/geraniums-main.png) + +[Modify](schedule_lib/modify.md) + +### Merge +Combines multiple matches. +Allowing patterns to be split into different parts or reuse a specific part with another match without recalculating. +An example usage can be found in [examples/geraniums](../../../examples/geraniums). + +In the following sub-schedule, a new pot and the flower with old pot and their connection, is combined to move the flower in a rewrite. +Replanting multiple flowers into one new pot would require markers, making the matching harder in order to combine these elements without the use of this node. + +![geraniums_repot_flowers](images/geraniums-repot_flowers.png) + +[Merge](schedule_lib/merge.md) + +### Store +Combines matches (set) into a new match set. +Use the exec port to insert the data on the associated data-port to the set. + +The direct usage of this node is limited but invaluable for libraries. +An example usage is petrinet-execution with user interface. +This requires a list of all transitions that can fire. +Matching "all transitions" followed by a loop to check the NAC leaves single matches. +This nodes allows these matches to be recombined into a set that can be used to choose a transition from. + +[Store](schedule_lib/store.md) + +### Loop +Iterate over a given match set. +Nodes such as Match or Rewrite uses a single match as a pivot. +Executing these nodes over all the element is possible with this node. +See the examples in [Modify](#Modify) or [Merge](#Merge) for an example view. + +[Loop](schedule_lib/loop.md) + +### Print +Print the input data. This is mainly used as a debugging/testing tool to validate intermediate information or state. + +[Print](schedule_lib/print.md) + +### Action +This node allows for code to be injected into the schedule. +This node can be used for general purpuse and even recreate all other nodes (except start and end). +Not all functionalities can be described using the current nodes. For petrinets, an example can be to generate a visual overview of the petrinet-system. + +[Action.md](schedule_lib/action.md) + +## file formats + +### .od +This is the original textual file format used by the framework. The main advantage of this format is the integration with the framework that allows conformance checking of the scheduling language. +Therefore, all other formats are converted to this type for conformance checking before being compiled. + +### .py +All schedules are compiled to python after conformance checking. Allowing this format provides the benefit to load schedules without expensive compilation or conformance checking, reducing computational cost. +This format is recommended in the deployment of applications where the schedule will not change. +It is not advisable to implement schedules directly in this format as conformance checking guarantees proper working of the schedule module. + +### .drawio +A visual format for the drawio application. +The library includes a drawio [library](../schedule_lib/Schedule_lib.xml) that includes a representation with additional fields for easy integration with the application. +The main advantage of this format is the usage of pages that allows sub-schedules be easily created and organised within one schedule. (layers are not allowed) + diff --git a/transformation/schedule/doc/schedule_lib/end.md b/transformation/schedule/doc/schedule_lib/end.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/end.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/schedule_lib/README.md b/transformation/schedule/doc/schedule_lib/node.md similarity index 100% rename from transformation/schedule/schedule_lib/README.md rename to transformation/schedule/doc/schedule_lib/node.md diff --git a/transformation/schedule/doc/schedule_lib/start.md b/transformation/schedule/doc/schedule_lib/start.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/start.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/models/scheduling_MM.od b/transformation/schedule/models/scheduling_MM.od index edae0b9..73f5131 100644 --- a/transformation/schedule/models/scheduling_MM.od +++ b/transformation/schedule/models/scheduling_MM.od @@ -7,12 +7,11 @@ association Conn_exec [0..*] Exec -> Exec [0..*] { abstract class Data association Conn_data [0..*] Data -> Data [0..*] { - Integer from; - Integer to; + String from; + String to; } -abstract class Node (Exec, Data) -class Start [1..1] (Node) { +class Start [1..1] (Exec, Data) { optional ActionCode ports_exec_out; optional ActionCode ports_data_out; ``` @@ -28,7 +27,7 @@ class Start [1..1] (Node) { err ```; } -class End [1..1] (Node) { +class End [1..1] (Exec, Data) { optional ActionCode ports_exec_in; optional ActionCode ports_data_in; ``` @@ -45,7 +44,7 @@ class End [1..1] (Node) { ```; } -abstract class Rule (Node) +abstract class Rule (Exec, Data) { String file; } @@ -75,7 +74,7 @@ class Rewrite (Rule) ```; } -class Action (Node) +class Action (Exec, Data) { optional ActionCode ports_exec_in; optional ActionCode ports_exec_out; @@ -100,7 +99,7 @@ class Action (Node) } -class Modify (Node) +class Modify (Data) { optional ActionCode rename; optional ActionCode delete; @@ -122,7 +121,7 @@ class Modify (Node) ```; } -class Merge (Node) +class Merge (Data) { ActionCode ports_data_in; ``` @@ -138,7 +137,7 @@ class Merge (Node) ```; } -class Store (Node) +class Store (Exec, Data) { ActionCode ports; ``` @@ -154,7 +153,7 @@ class Store (Node) ```; } -class Schedule (Node) +class Schedule (Exec, Data) { String file; ``` @@ -167,7 +166,7 @@ class Schedule (Node) ```; } -class Loop(Node) +class Loop(Exec, Data) { ``` check_all_connections(this, [ @@ -179,7 +178,7 @@ class Loop(Node) ```; } -class Print(Node) +class Print(Exec, Data) { optional Boolean event; optional String label; diff --git a/transformation/schedule/rule_scheduler.py b/transformation/schedule/rule_scheduler.py index 474a6ab..2b2e133 100644 --- a/transformation/schedule/rule_scheduler.py +++ b/transformation/schedule/rule_scheduler.py @@ -34,7 +34,7 @@ if TYPE_CHECKING: from transformation.schedule.schedule import Schedule -class RuleSchedular: +class RuleScheduler: __slots__ = ( "rule_executor", "schedule_main", diff --git a/transformation/schedule/schedule.pyi b/transformation/schedule/schedule.pyi index 0e6547f..0edc014 100644 --- a/transformation/schedule/schedule.pyi +++ b/transformation/schedule/schedule.pyi @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING from transformation.schedule.schedule_lib import * if TYPE_CHECKING: from transformation.schedule.rule_executor import RuleExecutor - from rule_scheduler import RuleSchedular + from rule_scheduler import RuleScheduler class Schedule: __slots__ = { @@ -14,5 +14,5 @@ class Schedule: @staticmethod def get_matchers(): ... - def init_schedule(self, schedular: RuleSchedular, rule_executor: RuleExecutor, matchers): ... + def init_schedule(self, scheduler: RuleScheduler, rule_executor: RuleExecutor, matchers): ... def generate_dot(self, *args, **kwargs): ... \ No newline at end of file diff --git a/transformation/schedule/schedule_lib/exec_node.py b/transformation/schedule/schedule_lib/exec_node.py index c46125f..ea1cc8b 100644 --- a/transformation/schedule/schedule_lib/exec_node.py +++ b/transformation/schedule/schedule_lib/exec_node.py @@ -1,5 +1,9 @@ from abc import abstractmethod +from typing import override +from jinja2 import Template + from api.od import ODAPI +from .funcs import generate_dot_edge from .node import Node @@ -33,3 +37,25 @@ class ExecNode(Node): @abstractmethod def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: return None + + @override + def generate_dot( + self, nodes: list[str], edges: list[str], visited: set[int], template: Template + ) -> None: + for out_port, edge in self.next_node.items(): + template.render() + generate_dot_edge( + self, + edge[0], + edges, + template, + kwargs={ + "prefix": "e", + "from_gate": out_port, + "to_gate": edge[1], + "color": "darkblue", + }, + ) + + for edge in self.next_node.values(): + edge[0].generate_dot(nodes, edges, visited, template) diff --git a/transformation/schedule/schedule_lib/match.py b/transformation/schedule/schedule_lib/match.py index f3da3f2..e0b097f 100644 --- a/transformation/schedule/schedule_lib/match.py +++ b/transformation/schedule/schedule_lib/match.py @@ -52,7 +52,7 @@ class Match(ExecNode, DataNode): nodes, template, **{ - "label": f"match_{self.n}\n{self.label}", + "label": f"match\n{self.label}\nn = {self.n}", "ports_exec": ( self.get_exec_input_gates(), self.get_exec_output_gates(), diff --git a/transformation/schedule/schedule_lib/rewrite.py b/transformation/schedule/schedule_lib/rewrite.py index ba2be83..2196d1d 100644 --- a/transformation/schedule/schedule_lib/rewrite.py +++ b/transformation/schedule/schedule_lib/rewrite.py @@ -41,7 +41,7 @@ class Rewrite(ExecNode, DataNode): nodes, template, **{ - "label": "rewrite", + "label": f"rewrite\n{self.label}", "ports_exec": ( self.get_exec_input_gates(), self.get_exec_output_gates(), diff --git a/transformation/schedule/schedule_lib/sub_schedule.py b/transformation/schedule/schedule_lib/sub_schedule.py index 7b97e43..048658c 100644 --- a/transformation/schedule/schedule_lib/sub_schedule.py +++ b/transformation/schedule/schedule_lib/sub_schedule.py @@ -8,7 +8,7 @@ from .exec_node import ExecNode from .funcs import not_visited, generate_dot_node, IdGenerator if TYPE_CHECKING: - from ..rule_scheduler import RuleSchedular + from ..rule_scheduler import RuleScheduler class ScheduleState: @@ -16,9 +16,9 @@ class ScheduleState: self.end_gate: str = "" class SubSchedule(ExecNode, DataNode): - def __init__(self, schedular: "RuleSchedular", file: str) -> None: - self.schedule = schedular._load_schedule(file, _main=False) - self.schedular = schedular + def __init__(self, scheduler: "RuleScheduler", file: str) -> None: + self.schedule = scheduler._load_schedule(file, _main=False) + self.scheduler = scheduler super().__init__() self.state: dict[int, ScheduleState] = {} @@ -58,7 +58,7 @@ class SubSchedule(ExecNode, DataNode): @override def execute(self, port: str, exec_id: int, od: ODAPI) -> tuple[int, any] | None: - runstatus, result = self.schedular._runner( + runstatus, result = self.scheduler._runner( od, self.schedule, port, diff --git a/transformation/schedule/templates/schedule_dot.j2 b/transformation/schedule/templates/schedule_dot.j2 index 7937884..ca715dc 100644 --- a/transformation/schedule/templates/schedule_dot.j2 +++ b/transformation/schedule/templates/schedule_dot.j2 @@ -11,9 +11,14 @@ digraph G { {% endfor %} } -{% macro Node(label, id, ports_exec=[], ports_data=[]) %} +{% macro Node(label, id, ports_exec=[], ports_data=[], debug = False) %} subgraph cluster_{{ id }} { - label = "{{ id }}__{{ label }}"; + label = " + {%- if debug %} + {{ id }}_ + {%- endif -%} + {{ label }}" + style = rounded; input_{{ id }} [ shape=rect; @@ -54,7 +59,7 @@ output_{{ from_id }}:{{ prefix }}_{{ from_gate }} -> input_{{ to_id }}:{{ prefix {% endif %} {% else %} - X +   {% endif %} > {%- endmacro %} \ No newline at end of file diff --git a/transformation/schedule/templates/schedule_template.j2 b/transformation/schedule/templates/schedule_template.j2 index 6075b61..e696681 100644 --- a/transformation/schedule/templates/schedule_template.j2 +++ b/transformation/schedule/templates/schedule_template.j2 @@ -31,7 +31,7 @@ {%- endmacro %} {% macro Schedule(name, file) %} -{{ name }} = SubSchedule(schedular, "{{ file }}") +{{ name }} = SubSchedule(scheduler, "{{ file }}") {%- endmacro %} {% macro Loop(name) %} diff --git a/transformation/schedule/templates/schedule_template_wrap.j2 b/transformation/schedule/templates/schedule_template_wrap.j2 index 59bb425..d1e8dfc 100644 --- a/transformation/schedule/templates/schedule_template_wrap.j2 +++ b/transformation/schedule/templates/schedule_template_wrap.j2 @@ -16,7 +16,7 @@ class Schedule: {% endfor %} ] - def init_schedule(self, schedular, rule_executer, matchers): + def init_schedule(self, scheduler, rule_executer, matchers): {% for block in blocks_start_end%} {{ block }} {% endfor %} From af12f3d5244ec047a7dabfcc8dcc4d1c190d9c94 Mon Sep 17 00:00:00 2001 From: robbe Date: Mon, 30 Jun 2025 18:03:47 +0200 Subject: [PATCH 7/8] Added some documentation, fixed test and missing schedule --- examples/petrinet/runner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/petrinet/runner.py b/examples/petrinet/runner.py index 6e99a96..75fd37f 100644 --- a/examples/petrinet/runner.py +++ b/examples/petrinet/runner.py @@ -43,9 +43,9 @@ if __name__ == "__main__": scheduler = RuleScheduler(state, mm_rt, mm_rt_ramified, verbose=True, directory="models") - # if action_generator.load_schedule(f"petrinet.od"): - # if action_generator.load_schedule("schedules/combinatory.drawio"): - if action_generator.load_schedule("schedules/petrinet3.drawio"): + # if scheduler.load_schedule(f"petrinet.od"): + # if scheduler.load_schedule("schedules/combinatory.drawio"): + if scheduler.load_schedule("schedules/petrinet3.drawio"): scheduler.generate_dot("../dot.dot") From e1eb6e0df40a3677a3ce4584c931404d43aae4c8 Mon Sep 17 00:00:00 2001 From: robbe Date: Mon, 30 Jun 2025 18:22:16 +0200 Subject: [PATCH 8/8] Placeholders for node documentation (what and how) what and why covered in transformation/schedule/doc/schedule.md --- transformation/schedule/doc/schedule.md | 9 +++++++++ transformation/schedule/doc/schedule_lib/action.md | 1 + transformation/schedule/doc/schedule_lib/data_node.md | 1 + transformation/schedule/doc/schedule_lib/exec_node.md | 1 + transformation/schedule/doc/schedule_lib/loop.md | 1 + transformation/schedule/doc/schedule_lib/match.md | 1 + transformation/schedule/doc/schedule_lib/merge.md | 1 + transformation/schedule/doc/schedule_lib/modify.md | 1 + transformation/schedule/doc/schedule_lib/print.md | 1 + transformation/schedule/doc/schedule_lib/rewrite.md | 1 + transformation/schedule/doc/schedule_lib/rule.md | 1 + transformation/schedule/doc/schedule_lib/schedule.md | 1 + transformation/schedule/doc/schedule_lib/store.md | 1 + 13 files changed, 21 insertions(+) create mode 100644 transformation/schedule/doc/schedule_lib/action.md create mode 100644 transformation/schedule/doc/schedule_lib/data_node.md create mode 100644 transformation/schedule/doc/schedule_lib/exec_node.md create mode 100644 transformation/schedule/doc/schedule_lib/loop.md create mode 100644 transformation/schedule/doc/schedule_lib/match.md create mode 100644 transformation/schedule/doc/schedule_lib/merge.md create mode 100644 transformation/schedule/doc/schedule_lib/modify.md create mode 100644 transformation/schedule/doc/schedule_lib/print.md create mode 100644 transformation/schedule/doc/schedule_lib/rewrite.md create mode 100644 transformation/schedule/doc/schedule_lib/rule.md create mode 100644 transformation/schedule/doc/schedule_lib/schedule.md create mode 100644 transformation/schedule/doc/schedule_lib/store.md diff --git a/transformation/schedule/doc/schedule.md b/transformation/schedule/doc/schedule.md index 39837f9..8a1c6a6 100644 --- a/transformation/schedule/doc/schedule.md +++ b/transformation/schedule/doc/schedule.md @@ -233,6 +233,15 @@ Not all functionalities can be described using the current nodes. For petrinets, [Action.md](schedule_lib/action.md) +## Edge Types +Nodes can be connected using two different edges. The execution-edges define the execution flow of the schedule. +These connections can only connect nodes that inherit form [ExecNode](schedule_lib/exec_node.md). +Connecting nodes between execution-gates defined by the nodes, happens in a system of "one to many" for gates. +The data-edges allows information to be distributed to other [DataNode](schedule_lib/data_node.md). +This happens in the opposite way of "many to one" on data-gates. +Data changes on a gate wil notify all connected nodes of the changes, allowing propagation through the system. Note: the data received is immutable to ensure consistent and reliable execution of the schedule. + + ## file formats ### .od diff --git a/transformation/schedule/doc/schedule_lib/action.md b/transformation/schedule/doc/schedule_lib/action.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/action.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/data_node.md b/transformation/schedule/doc/schedule_lib/data_node.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/data_node.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/exec_node.md b/transformation/schedule/doc/schedule_lib/exec_node.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/exec_node.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/loop.md b/transformation/schedule/doc/schedule_lib/loop.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/loop.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/match.md b/transformation/schedule/doc/schedule_lib/match.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/match.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/merge.md b/transformation/schedule/doc/schedule_lib/merge.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/merge.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/modify.md b/transformation/schedule/doc/schedule_lib/modify.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/modify.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/print.md b/transformation/schedule/doc/schedule_lib/print.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/print.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/rewrite.md b/transformation/schedule/doc/schedule_lib/rewrite.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/rewrite.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/rule.md b/transformation/schedule/doc/schedule_lib/rule.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/rule.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/schedule.md b/transformation/schedule/doc/schedule_lib/schedule.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/schedule.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file diff --git a/transformation/schedule/doc/schedule_lib/store.md b/transformation/schedule/doc/schedule_lib/store.md new file mode 100644 index 0000000..9805841 --- /dev/null +++ b/transformation/schedule/doc/schedule_lib/store.md @@ -0,0 +1 @@ +# Under construction \ No newline at end of file