From 8fe7b0ea040df2447bd64775561fa7f9f190d7bf Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Mon, 27 Jan 2025 16:03:45 +0100 Subject: [PATCH 01/43] Commit some outstanding changes. Add TODO for cleanup. --- TODO.txt | 44 +++++++++++++++++++ .../textual_od/objectdiagrams.jinja2 | 23 ++++++---- examples/conformance/woods.py | 4 +- examples/model_transformation/woods.py | 6 +-- examples/petrinet/runner.py | 13 +++--- .../tapaal/tapaal.jinja2 | 15 ++++++- examples/semantics/operational/port/models.py | 10 ++--- .../operational/port/rulebased_runner.py | 2 +- .../rules/gen_pn/r_00_place2place_rhs.od | 4 +- .../rules/gen_pn/r_10_conn2trans_lhs.od | 4 +- .../rules/gen_pn/r_10_conn2trans_rhs.od | 20 +++++++-- .../translational/runner_translate.py | 4 +- 12 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 TODO.txt diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..8df5fc8 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,44 @@ +Things that need to be cleaned up: + + - At several places in the code, it is assumed that from the root node, there is an edge labeled 'SCD' containing the self-conforming meta-meta-model. It would be better for parts of the code that need the meta-meta-model to receive this model as a (function) parameter. + + - The whole 'ModelRef'-construct does not work as originally foreseen. It is currently only used for attributes of primitive types, where it unnecessarily complicates things. Better to get rid of it. + + +Known bugs: + - Cannot parse negative numbers + + + - When merging models, the model element names must not overlap. Maybe allow some kind of prefixing of the overlapping names? Difficulty porting existing models to the merged models if the type names have changed... + + + +Merging (meta-)models is a nightmare: + + - Prefixing the type names (to avoid naming collisions) is not an option: + (*) constraints (and transformation rules) already contain API calls that mention type names -> all of these would break + (*) don't want to prefix primitive types like "Integer", "String", ... because the existing code already assumes these exact names + + - Not prefixing the type names leads to naming collisions, even if names are carefully chosen: + (*) anonymous names, e.g., Inheritance-links still result in naming collisions (requiring auto-renaming?) + + +Feature requests: + + - Support custom functions in 'conditions' + + - When matching edge, match 'any' src/tgt + + - Support 'return'-statement in conditions? + + - Separate script for running LHS (+NAC) on any model, and visualizing the match. + + - Syntax highlighting: + most students use: + - VS Code + - PyCharm + i use: + - Sublime Text + nobody uses: + - Eclipse + diff --git a/concrete_syntax/textual_od/objectdiagrams.jinja2 b/concrete_syntax/textual_od/objectdiagrams.jinja2 index 51425d2..a559347 100644 --- a/concrete_syntax/textual_od/objectdiagrams.jinja2 +++ b/concrete_syntax/textual_od/objectdiagrams.jinja2 @@ -1,18 +1,23 @@ {% macro render_name(name) %}{{ name if not hide_names or name.startswith("__") else "" }}{% endmacro %} -{% macro render_attributes(obj) %} { +{% macro render_attributes(obj) %} +{% if len(odapi.get_slots(obj)) > 0 %} { {% for attr_name in odapi.get_slots(obj) %} {{ attr_name}} = {{ display_value( val=odapi.get_slot_value(obj, attr_name), type_name=odapi.get_type_name(odapi.get_slot(obj, attr_name)), indentation=4) }}; - {% endfor %} -}{% endmacro %} + {% endfor -%} +} +{% endif -%} +{%- endmacro %} -{% for obj_name, obj in objects %} -{{ render_name(obj_name) }}:{{ odapi.get_type_name(obj) }}{{ render_attributes(obj) }} -{% endfor %} +{%- for obj_name, obj in objects %} +{{ render_name(obj_name) }}:{{ odapi.get_type_name(obj) }} +{{- render_attributes(obj) }} +{% endfor -%} -{% for lnk_name, lnk in links %} -{{ render_name(obj_name) }}:{{ odapi.get_type_name(lnk) }} ({{odapi.get_name(odapi.get_source(lnk))}} -> {{odapi.get_name(odapi.get_target(lnk))}}){{ render_attributes(lnk) }} -{% endfor %} +{%- for lnk_name, lnk in links %} +{{ render_name(obj_name) }}:{{ odapi.get_type_name(lnk) }} ({{odapi.get_name(odapi.get_source(lnk))}} -> {{odapi.get_name(odapi.get_target(lnk))}}) +{{- render_attributes(lnk) }} +{% endfor -%} diff --git a/examples/conformance/woods.py b/examples/conformance/woods.py index 633e1e9..7475ac9 100644 --- a/examples/conformance/woods.py +++ b/examples/conformance/woods.py @@ -4,6 +4,7 @@ from framework.conformance import Conformance, render_conformance_check_result from concrete_syntax.textual_od import parser, renderer from concrete_syntax.common import indent from concrete_syntax.plantuml import renderer as plantuml +from concrete_syntax.plantuml.make_url import make_url from util.prompt import yes_no, pause state = DevState() @@ -153,6 +154,7 @@ woods_m_cs = """ bear2:Bear :afraidOf (george -> bear1) :afraidOf (george -> bear2) + :afraidOf (billy -> george) """ print() @@ -194,7 +196,7 @@ if yes_no("Print PlantUML?"): uml += plantuml.render_trace_conformance(state, woods_m, woods_mm) print("==================================") - print(uml) + print(make_url(uml)) print("==================================") print("Go to either:") print(" ▸ https://www.plantuml.com/plantuml/uml") diff --git a/examples/model_transformation/woods.py b/examples/model_transformation/woods.py index f321037..e6e88e5 100644 --- a/examples/model_transformation/woods.py +++ b/examples/model_transformation/woods.py @@ -113,9 +113,7 @@ def main(): # object to match man:{prefix}Man {{ # match only men heavy enough - {prefix}weight = ``` - get_value(this) > 60 - ```; + {prefix}weight = `get_value(this) > 60`; }} # object to delete @@ -142,6 +140,7 @@ def main(): # object to create bill:{prefix}Man {{ + # name = `"billie"+str(get_slot_value(matched("man"), "weight"))`; {prefix}weight = `100`; }} @@ -208,6 +207,7 @@ def main(): generator = match_od(state, dsl_m_id, dsl_mm_id, lhs_id, ramified_mm_id) for i, (match, color) in enumerate(zip(generator, ["red", "orange"])): + print("\nMATCH:\n", match) uml += plantuml.render_trace_match(state, match, lhs_id, dsl_m_id, color) # rewrite happens in-place (which sucks), so we will only modify a clone: diff --git a/examples/petrinet/runner.py b/examples/petrinet/runner.py index b2d0c51..df516e7 100644 --- a/examples/petrinet/runner.py +++ b/examples/petrinet/runner.py @@ -1,7 +1,7 @@ from state.devstate import DevState from api.od import ODAPI from concrete_syntax.textual_od.renderer import render_od -# from concrete_syntax.textual_od.renderer_jinja2 import render_od_jinja2 +from concrete_syntax.textual_od.renderer_jinja2 import render_od_jinja2 from bootstrap.scd import bootstrap_scd from util import loader from transformation.rule import RuleMatcherRewriter, ActionGenerator @@ -28,10 +28,10 @@ if __name__ == "__main__": mm_rt_cs = mm_cs + read_file('metamodels/mm_runtime.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') - # 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_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') # Parse them mm = loader.parse_and_check(state, mm_cs, scd_mmm, "Petri-Net Design meta-model") @@ -51,7 +51,8 @@ if __name__ == "__main__": def render_callback(od): show_petri_net(od) - return render_od(state, od.m, od.mm) + # return render_od(state, od.m, od.mm) + return render_od_jinja2(state, od.m, od.mm) sim = simulator.Simulator( action_generator=action_generator, diff --git a/examples/petrinet/translational_semantics/tapaal/tapaal.jinja2 b/examples/petrinet/translational_semantics/tapaal/tapaal.jinja2 index 445dc99..7c2596c 100644 --- a/examples/petrinet/translational_semantics/tapaal/tapaal.jinja2 +++ b/examples/petrinet/translational_semantics/tapaal/tapaal.jinja2 @@ -12,11 +12,22 @@ nameOffsetY="0" positionX="{{ i * 100 + 100 }}" positionY="100" - /> + /> {% endfor %} {% for i, (transition_name, transition) in enumerate(odapi.get_all_instances("PNTransition")) %} - + {% endfor %} {% for arc_name, arc in odapi.get_all_instances("arc") %} diff --git a/examples/semantics/operational/port/models.py b/examples/semantics/operational/port/models.py index dd7e7a3..b4a7c10 100644 --- a/examples/semantics/operational/port/models.py +++ b/examples/semantics/operational/port/models.py @@ -270,7 +270,7 @@ port_rt_m_cs = port_m_cs + """ time = 0; } - waitingState:PlaceState { numShips = 0; } :of (waitingState -> waiting) + waitingState:PlaceState { numShips = 2; } :of (waitingState -> waiting) inboundPassageState:PlaceState { numShips = 0; } :of (inboundPassageState -> inboundPassage) outboundPassageState:PlaceState { numShips = 0; } :of (outboundPassageState -> outboundPassage) @@ -282,7 +282,7 @@ port_rt_m_cs = port_m_cs + """ berth1State:BerthState { status = "empty"; numShips = 0; } :of (berth1State -> berth1) berth2State:BerthState { status = "empty"; numShips = 0; } :of (berth2State -> berth2) - servedState:PlaceState { numShips = 0; } :of (servedState -> served) + servedState:PlaceState { numShips = 1; } :of (servedState -> served) workersState:WorkerSetState :of (workersState -> workers) @@ -396,12 +396,12 @@ smaller_model2_rt_cs = smaller_model2_cs + """ } waitingState:PlaceState { numShips = 1; } :of (waitingState -> waiting) - berthState:BerthState { numShips = 0; status = "empty"; } :of (berthState -> berth) - servedState:PlaceState { numShips = 0; } :of (servedState -> served) + berthState:BerthState { numShips = 1; status = "served"; } :of (berthState -> berth) + servedState:PlaceState { numShips = 1; } :of (servedState -> served) gen2waitState:ConnectionState { moved = False; } :of (gen2waitState -> gen2wait) wait2berthState:ConnectionState { moved = False; } :of (wait2berthState -> wait2berth) - berth2servedState:ConnectionState { moved = False; } :of (berth2servedState -> berth2served) + berth2servedState:ConnectionState { moved = True; } :of (berth2servedState -> berth2served) workersState:WorkerSetState :of (workersState -> workers) """ diff --git a/examples/semantics/operational/port/rulebased_runner.py b/examples/semantics/operational/port/rulebased_runner.py index ce73ca5..56cf67f 100644 --- a/examples/semantics/operational/port/rulebased_runner.py +++ b/examples/semantics/operational/port/rulebased_runner.py @@ -53,7 +53,7 @@ sim = Simulator( termination_condition=termination_condition, check_conformance=True, verbose=True, - renderer=render_port_textual, + # renderer=render_port_textual, # renderer=render_port_graphviz, ) diff --git a/examples/semantics/translational/rules/gen_pn/r_00_place2place_rhs.od b/examples/semantics/translational/rules/gen_pn/r_00_place2place_rhs.od index 47e7b57..ea6a227 100644 --- a/examples/semantics/translational/rules/gen_pn/r_00_place2place_rhs.od +++ b/examples/semantics/translational/rules/gen_pn/r_00_place2place_rhs.od @@ -9,7 +9,7 @@ pn_place:RAM_PNPlace { # new feature: you can control the name of the object to be created: - name = `f"pn_{get_name(matched("port_place"))}"`; + name = `f"ships_{get_name(matched("port_place"))}"`; } place2place:RAM_generic_link (pn_place -> port_place) @@ -19,4 +19,4 @@ pn_place_state:RAM_PNPlaceState { RAM_numTokens = `get_slot_value(matched('port_place_state'), "numShips")`; } - :RAM_pn_of(pn_place_state -> pn_place) \ No newline at end of file + :RAM_pn_of(pn_place_state -> pn_place) diff --git a/examples/semantics/translational/rules/gen_pn/r_10_conn2trans_lhs.od b/examples/semantics/translational/rules/gen_pn/r_10_conn2trans_lhs.od index f15843f..bdde559 100644 --- a/examples/semantics/translational/rules/gen_pn/r_10_conn2trans_lhs.od +++ b/examples/semantics/translational/rules/gen_pn/r_10_conn2trans_lhs.od @@ -1,5 +1,7 @@ -# Just look for a connection: +# Just look for a connection and its state: port_src:RAM_Source port_snk:RAM_Sink port_conn:RAM_connection (port_src -> port_snk) +port_conn_state:RAM_ConnectionState +port_of:RAM_of (port_conn_state -> port_conn) \ No newline at end of file diff --git a/examples/semantics/translational/rules/gen_pn/r_10_conn2trans_rhs.od b/examples/semantics/translational/rules/gen_pn/r_10_conn2trans_rhs.od index edc7d07..a21b72f 100644 --- a/examples/semantics/translational/rules/gen_pn/r_10_conn2trans_rhs.od +++ b/examples/semantics/translational/rules/gen_pn/r_10_conn2trans_rhs.od @@ -3,12 +3,26 @@ port_src:RAM_Source port_snk:RAM_Sink port_conn:RAM_connection (port_src -> port_snk) +port_conn_state:RAM_ConnectionState +port_of:RAM_of (port_conn_state -> port_conn) # Create a Petri Net transition, and link it to our port-connection: -pn_transition:RAM_PNTransition { - name = `f"pn_{get_name(matched("port_conn"))}"`; +move_transition:RAM_PNTransition { + name = `f"move_{get_name(matched("port_conn"))}"`; } -trans2conn:RAM_generic_link (pn_transition -> port_conn) + +moved_place:RAM_PNPlace { + name = `f" moved_{get_name(matched("port_conn"))}"`; +} +moved_place_state:RAM_PNPlaceState { + RAM_numTokens = `1 if get_slot_value(matched('port_conn_state'), "moved") else 0`; +} +:RAM_pn_of (moved_place_state -> moved_place) +# when firing a 'move', put a token in the 'moved'-place +:RAM_arc (move_transition -> moved_place) + +trans2conn:RAM_generic_link (move_transition -> port_conn) +moved2conn:RAM_generic_link (moved_place -> port_conn) # Note that we are not yet creating any incoming/outgoing petri net arcs! This will be done in another rule. \ No newline at end of file diff --git a/examples/semantics/translational/runner_translate.py b/examples/semantics/translational/runner_translate.py index 7a6bf6e..3949b9d 100644 --- a/examples/semantics/translational/runner_translate.py +++ b/examples/semantics/translational/runner_translate.py @@ -62,9 +62,9 @@ if __name__ == "__main__": print('loading model...') port_m_rt_initial = loader.parse_and_check(state, - m_cs=models.port_rt_m_cs, # <-- your final solution should work with the full model + # m_cs=models.port_rt_m_cs, # <-- your final solution should work with the full model # m_cs=models.smaller_model_rt_cs, # <-- simpler model to try first - # m_cs=models.smaller_model2_rt_cs, # <-- simpler model to try first + m_cs=models.smaller_model2_rt_cs, # <-- simpler model to try first mm=merged_mm, descr="initial model", check_conformance=False, # no need to check conformance every time From 0ed716585cf9f738f3562de2f90254af142366ec Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Mon, 27 Jan 2025 16:53:39 +0100 Subject: [PATCH 02/43] update readme --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3c0638f..bd8c2a4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,22 @@ -# MV2 +# muMLE -This repository contains the code for my take on (a part of) the [Modelverse](https://msdl.uantwerpen.be/git/yentl/modelverse) for my Master's thesis. +Tiny (meta-)modeling framework. -## Development packages +Features: -Some packages were used during development, but are not needed for succesful runtime (e.g. linter, autoformatter). These can be found under `requirements_dev.txt`. + * mostly textual concrete syntax + * meta-modeling & constraint writing + * conformance checking + * model transformation primitives (match and rewrite) + * rule-based model transformation + * examples included: + - Class Diagrams (self-conforming) + - Causal Block Diagrams language + - Petri Net language -## Mandatory packages +## Dependencies -Python packages required to succesfully run/test the code in this repository can be found under `requirements.txt`. \ No newline at end of file + * Python 3.? + * Python libraries: + - Lark (for textual parsing) + - Jinja2 (not a hard requirement, only for model-to-text transformation) From ecf669425ffdc51fe496100df4df009364ff742d Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Mon, 27 Jan 2025 17:07:36 +0100 Subject: [PATCH 03/43] update readme --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index bd8c2a4..54639b5 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,13 @@ Features: * Python libraries: - Lark (for textual parsing) - Jinja2 (not a hard requirement, only for model-to-text transformation) + + +## Development + +The following branches exist: + + * `mde2425` - the branch containing a snapshot of the repo used for the MDE assignments 24-25. No breaking changes will be pushed here. After the re-exams (Sep 2025), this branch will be frozen. + * `master` - currently equivalent to `mde2425` (this is the branch that was cloned by the students). This branch will be deleted after Sep 2025, because the name is too vague. + * `cleaning` - in this branch, new development will occur, primarily cleaning up the code to prepare for next year's MDE classes. + From a245d0a406aba04022d3f9d92708da899f250615 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 29 Jan 2025 16:16:47 +0100 Subject: [PATCH 04/43] add a todo thingy --- TODO.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index 8df5fc8..bf1e3d4 100644 --- a/TODO.txt +++ b/TODO.txt @@ -29,7 +29,9 @@ Feature requests: - When matching edge, match 'any' src/tgt - - Support 'return'-statement in conditions? + - Support 'return'-statement in conditions? (just makes syntax nicer) + + - RAMification / matching: add `match_subtypes` attribute to each RAMified class. - Separate script for running LHS (+NAC) on any model, and visualizing the match. From ad6fcd7a244f56ffcce6ca42dce0bac253f32b89 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 29 Jan 2025 16:16:59 +0100 Subject: [PATCH 05/43] add type check when overwriting slot value --- api/od.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/od.py b/api/od.py index c23160d..2740772 100644 --- a/api/od.py +++ b/api/od.py @@ -215,16 +215,25 @@ class ODAPI: def overwrite_primitive_value(self, name: str, value: any, is_code=False): referred_model = UUID(self.bottom.read_value(self.get(name))) + to_overwrite_type = self.get_type_name(self.get(name)) # watch out: in Python, 'bool' is subtype of 'int' # so we must check for 'bool' first if isinstance(value, bool): + if to_overwrite_type != "Boolean": + raise Exception(f"Cannot assign boolean value '{value}' to value of type {to_overwrite_type}.") Boolean(referred_model, self.state).create(value) elif isinstance(value, int): + if to_overwrite_type != "Integer": + raise Exception(f"Cannot assign integer value '{value}' to value of type {to_overwrite_type}.") Integer(referred_model, self.state).create(value) elif isinstance(value, str): if is_code: + if to_overwrite_type != "ActionCode": + raise Exception(f"Cannot assign code to value of type {to_overwrite_type}.") ActionCode(referred_model, self.state).create(value) else: + if to_overwrite_type != "String": + raise Exception(f"Cannot assign string value '{value}' to value of type {to_overwrite_type}.") String(referred_model, self.state).create(value) else: raise Exception("Unimplemented type "+value) From 6c41c83f4ff65b1b40addabf6ca265a0cb15f182 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 5 Feb 2025 10:40:40 +0100 Subject: [PATCH 06/43] rename branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54639b5..1171f46 100644 --- a/README.md +++ b/README.md @@ -28,5 +28,5 @@ The following branches exist: * `mde2425` - the branch containing a snapshot of the repo used for the MDE assignments 24-25. No breaking changes will be pushed here. After the re-exams (Sep 2025), this branch will be frozen. * `master` - currently equivalent to `mde2425` (this is the branch that was cloned by the students). This branch will be deleted after Sep 2025, because the name is too vague. - * `cleaning` - in this branch, new development will occur, primarily cleaning up the code to prepare for next year's MDE classes. + * `development` - in this branch, new development will occur, primarily cleaning up the code to prepare for next year's MDE classes. From 86cd7027f307ce9d1068782595f75d61b522225d Mon Sep 17 00:00:00 2001 From: Inte Vleminckx Date: Wed, 5 Feb 2025 15:20:25 +0100 Subject: [PATCH 07/43] Adding bytes as a type --- api/od.py | 8 ++++++++ bootstrap/primitive.py | 4 +++- bootstrap/scd.py | 4 +++- concrete_syntax/textual_od/parser.py | 9 ++++++++- concrete_syntax/textual_od/renderer.py | 2 +- services/od.py | 10 ++++++++++ services/primitives/bytes_type.py | 24 ++++++++++++++++++++++++ 7 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 services/primitives/bytes_type.py diff --git a/api/od.py b/api/od.py index 2740772..ffdef1c 100644 --- a/api/od.py +++ b/api/od.py @@ -5,6 +5,7 @@ from services.primitives.boolean_type import Boolean from services.primitives.integer_type import Integer from services.primitives.string_type import String from services.primitives.actioncode_type import ActionCode +from services.primitives.bytes_type import Bytes from uuid import UUID from typing import Optional from util.timer import Timer @@ -41,6 +42,7 @@ class ODAPI: self.create_integer_value = self.od.create_integer_value self.create_string_value = self.od.create_string_value self.create_actioncode_value = self.od.create_actioncode_value + self.create_bytes_value = self.od.create_bytes_value self.__recompute_mappings() @@ -208,6 +210,8 @@ class ODAPI: tgt = self.create_actioncode_value(name, value) else: tgt = self.create_string_value(name, value) + elif isinstance(value, bytes): + tgt = self.create_bytes_value(name, value) else: raise Exception("Unimplemented type "+value) self.__recompute_mappings() @@ -235,6 +239,10 @@ class ODAPI: if to_overwrite_type != "String": raise Exception(f"Cannot assign string value '{value}' to value of type {to_overwrite_type}.") String(referred_model, self.state).create(value) + elif isinstance(value, bytes): + if to_overwrite_type != "Bytes": + raise Exception(f"Cannot assign bytes value '{value}' to value of type {to_overwrite_type}.") + Bytes(referred_model, self.state).create(value) else: raise Exception("Unimplemented type "+value) diff --git a/bootstrap/primitive.py b/bootstrap/primitive.py index 853b552..4e2b36f 100644 --- a/bootstrap/primitive.py +++ b/bootstrap/primitive.py @@ -47,7 +47,7 @@ def bootstrap_constraint(class_node, type_name: str, python_type: str, scd_root: bottom.create_edge(constraint_node, scd_node, "Morphism") bottom.create_edge(constraint_link, scd_link, "Morphism") -def bootstrap_primitive_types(scd_root, state, integer_type, boolean_type, float_type, string_type, type_type, actioncode_type): +def bootstrap_primitive_types(scd_root, state, integer_type, boolean_type, float_type, string_type, type_type, actioncode_type, bytes_type): # Order is important: Integer must come first class_integer = bootstrap_type("Integer", scd_root, integer_type, state) class_type = bootstrap_type("Type", scd_root, type_type, state) @@ -55,6 +55,7 @@ def bootstrap_primitive_types(scd_root, state, integer_type, boolean_type, float class_float = bootstrap_type("Float", scd_root, float_type, state) class_string = bootstrap_type("String", scd_root, string_type, state) class_actioncode = bootstrap_type("ActionCode", scd_root, actioncode_type, state) + class_bytes = bootstrap_type("Bytes", scd_root, bytes_type, state) # Can only create constraints after ActionCode type has been created: bootstrap_constraint(class_integer, "Integer", "int", scd_root, integer_type, actioncode_type, state) @@ -63,3 +64,4 @@ def bootstrap_primitive_types(scd_root, state, integer_type, boolean_type, float bootstrap_constraint(class_float, "Float", "float", scd_root, float_type, actioncode_type, state) bootstrap_constraint(class_string, "String", "str", scd_root, string_type, actioncode_type, state) bootstrap_constraint(class_actioncode, "ActionCode", "str", scd_root, actioncode_type, actioncode_type, state) + bootstrap_constraint(class_bytes, "Bytes", "bytes", scd_root, bytes_type, actioncode_type, state) diff --git a/bootstrap/scd.py b/bootstrap/scd.py index cac04c6..fb94b21 100644 --- a/bootstrap/scd.py +++ b/bootstrap/scd.py @@ -32,6 +32,7 @@ def bootstrap_scd(state: State) -> UUID: float_type_root = create_model_root(bottom, "Float") type_type_root = create_model_root(bottom, "Type") actioncode_type_root = create_model_root(bottom, "ActionCode") + bytes_type_root = create_model_root(bottom, "Bytes") # create MCL, without morphism links @@ -132,7 +133,8 @@ def bootstrap_scd(state: State) -> UUID: float_type_root, string_type_root, type_type_root, - actioncode_type_root) + actioncode_type_root, + bytes_type_root) # bootstrap_integer_type(mcl_root, integer_type_root, integer_type_root, actioncode_type_root, state) # bootstrap_boolean_type(mcl_root, boolean_type_root, integer_type_root, actioncode_type_root, state) # bootstrap_float_type(mcl_root, float_type_root, integer_type_root, actioncode_type_root, state) diff --git a/concrete_syntax/textual_od/parser.py b/concrete_syntax/textual_od/parser.py index b679210..35ca933 100644 --- a/concrete_syntax/textual_od/parser.py +++ b/concrete_syntax/textual_od/parser.py @@ -21,6 +21,7 @@ literal: INT | STR | BOOL | CODE + | BYTES | INDENTED_CODE INT: /[0-9]+/ @@ -28,6 +29,8 @@ STR: /"[^"]*"/ | /'[^']*'/ BOOL: "True" | "False" CODE: /`[^`]*`/ +BYTES: /b"[^"]*"/ + | /b'[^']*'/ INDENTED_CODE: /```[^`]*```/ type_name: IDENTIFIER @@ -67,7 +70,7 @@ def parse_od(state, primitive_types = { type_name : UUID(state.read_value(state.read_dict(state.read_root(), type_name))) - for type_name in ["Integer", "String", "Boolean", "ActionCode"] + for type_name in ["Integer", "String", "Boolean", "ActionCode", "Bytes"] } class T(Transformer): @@ -89,6 +92,10 @@ def parse_od(state, def CODE(self, token): return (_Code(str(token[1:-1])), token.line) # strip the `` + def BYTES(self, token): + # return (bytes(token[2:-1], "utf-8"), token.line) # Strip b"" or b'' + return (bytes(token[2:-1], "utf-8"), token.line) # Strip b"" or b'' + def INDENTED_CODE(self, token): skip = 4 # strip the ``` and the following newline character space_count = 0 diff --git a/concrete_syntax/textual_od/renderer.py b/concrete_syntax/textual_od/renderer.py index a3fc030..fae1e72 100644 --- a/concrete_syntax/textual_od/renderer.py +++ b/concrete_syntax/textual_od/renderer.py @@ -9,7 +9,7 @@ def render_od(state, m_id, mm_id, hide_names=True): m_od = od.OD(mm_id, m_id, state) - serialized = set(["Integer", "String", "Boolean", "ActionCode"]) # assume these types always already exist + serialized = set(["Integer", "String", "Boolean", "ActionCode", "Bytes"]) # assume these types always already exist def display_name(name: str): # object names that start with "__" are hidden diff --git a/services/od.py b/services/od.py index 3b8700a..816b225 100644 --- a/services/od.py +++ b/services/od.py @@ -5,6 +5,7 @@ from services.primitives.integer_type import Integer from services.primitives.string_type import String from services.primitives.boolean_type import Boolean from services.primitives.actioncode_type import ActionCode +from services.primitives.bytes_type import Bytes from api.cd import CDAPI from typing import Optional @@ -147,6 +148,13 @@ class OD: actioncode_t.create(value) return self.create_model_ref(name, "ActionCode", actioncode_node) + def create_bytes_value(self, name: str, value: str): + from services.primitives.bytes_type import Bytes + bytes_node = self.bottom.create_node() + bytes_t = Bytes(bytes_node, self.bottom.state) + bytes_t.create(value) + return self.create_model_ref(name, "Bytes", bytes_node) + # Identical to the same SCD method: def create_model_ref(self, name: str, type_name: str, model: UUID): # create element + morphism links @@ -389,6 +397,8 @@ def read_primitive_value(bottom, modelref: UUID, mm: UUID): return Boolean(referred_model, bottom.state).read(), typ_name elif typ_name == "ActionCode": return ActionCode(referred_model, bottom.state).read(), typ_name + elif typ_name == "Bytes": + return Bytes(referred_model, bottom.state).read(), typ_name else: raise Exception("Unimplemented type:", typ_name) diff --git a/services/primitives/bytes_type.py b/services/primitives/bytes_type.py new file mode 100644 index 0000000..04f001d --- /dev/null +++ b/services/primitives/bytes_type.py @@ -0,0 +1,24 @@ +from uuid import UUID +from state.base import State +from services.bottom.V0 import Bottom + + +class Bytes: + def __init__(self, model: UUID, state: State): + self.model = model + self.bottom = Bottom(state) + type_model_id_node, = self.bottom.read_outgoing_elements(state.read_root(), "Bytes") + self.type_model = UUID(self.bottom.read_value(type_model_id_node)) + + def create(self, value: bool): + if "bytes" in self.bottom.read_keys(self.model): + instance, = self.bottom.read_outgoing_elements(self.model, "bytes") + self.bottom.delete_element(instance) + _instance = self.bottom.create_node(value) + self.bottom.create_edge(self.model, _instance, "bytes") + _type, = self.bottom.read_outgoing_elements(self.type_model, "Bytes") + self.bottom.create_edge(_instance, _type, "Morphism") + + def read(self): + instance, = self.bottom.read_outgoing_elements(self.model, "bytes") + return self.bottom.read_value(instance) \ No newline at end of file From 98f36c4cf02b684e37a06f7c8153a9f62dd90a7f Mon Sep 17 00:00:00 2001 From: Inte Vleminckx Date: Wed, 5 Feb 2025 16:24:22 +0100 Subject: [PATCH 08/43] Adding bytes as a type --- concrete_syntax/common.py | 5 +++++ state/base.py | 5 +++-- transformation/merger.py | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/concrete_syntax/common.py b/concrete_syntax/common.py index 3427b03..1ab0d3c 100644 --- a/concrete_syntax/common.py +++ b/concrete_syntax/common.py @@ -16,6 +16,8 @@ def display_value(val: any, type_name: str, indentation=0, newline_character='\n return '"'+val+'"'.replace('\n', newline_character) elif type_name == "Integer" or type_name == "Boolean": return str(val) + elif type_name == "Bytes": + return val else: raise Exception("don't know how to display value" + type_name) @@ -48,6 +50,9 @@ class TBase(Transformer): def CODE(self, token): return _Code(str(token[1:-1])) # strip the `` + def BYTES(self, token): + return (bytes(token[2:-1], "utf-8"), token.line) # Strip b"" or b'' + def INDENTED_CODE(self, token): skip = 4 # strip the ``` and the following newline character space_count = 0 diff --git a/state/base.py b/state/base.py index 614ae9e..78c4307 100644 --- a/state/base.py +++ b/state/base.py @@ -2,13 +2,14 @@ from abc import ABC, abstractmethod from typing import Any, List, Tuple, Optional, Union from uuid import UUID, uuid4 -primitive_types = (int, float, str, bool) +primitive_types = (int, float, str, bool, bytes) INTEGER = ("Integer",) FLOAT = ("Float",) STRING = ("String",) BOOLEAN = ("Boolean",) TYPE = ("Type",) -type_values = (INTEGER, FLOAT, STRING, BOOLEAN, TYPE) +BYTES = ("Bytes",) +type_values = (INTEGER, FLOAT, STRING, BOOLEAN, TYPE, BYTES) Node = UUID diff --git a/transformation/merger.py b/transformation/merger.py index 6caecb1..fcf6f81 100644 --- a/transformation/merger.py +++ b/transformation/merger.py @@ -4,7 +4,7 @@ from concrete_syntax.textual_od import parser, renderer from services.scd import SCD from util.timer import Timer -PRIMITIVE_TYPES = set(["Integer", "String", "Boolean", "ActionCode"]) +PRIMITIVE_TYPES = set(["Integer", "String", "Boolean", "ActionCode", "Bytes"]) # Merges N models. The models must have the same meta-model. # Care should be taken to avoid naming collisions before calling this function. @@ -12,7 +12,7 @@ def merge_models(state, mm, models: list[UUID]): with Timer("merge_models"): primitive_types = { type_name : UUID(state.read_value(state.read_dict(state.read_root(), type_name))) - for type_name in ["Integer", "String", "Boolean", "ActionCode"] + for type_name in ["Integer", "String", "Boolean", "ActionCode", "Bytes"] } merged = state.create_node() From e0136937b9f9a30bb085432388a93a85d53c6d96 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 5 Feb 2025 16:26:22 +0100 Subject: [PATCH 09/43] cleanup --- bootstrap/scd.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/bootstrap/scd.py b/bootstrap/scd.py index fb94b21..facf176 100644 --- a/bootstrap/scd.py +++ b/bootstrap/scd.py @@ -2,15 +2,7 @@ from state.base import State, UUID from services.bottom.V0 import Bottom from services.primitives.boolean_type import Boolean from services.primitives.string_type import String -from bootstrap.primitive import ( - bootstrap_primitive_types - # bootstrap_boolean_type, - # bootstrap_float_type, - # bootstrap_integer_type, - # bootstrap_string_type, - # bootstrap_type_type, - # bootstrap_actioncode_type -) +from bootstrap.primitive import bootstrap_primitive_types def create_model_root(bottom: Bottom, model_name: str) -> UUID: From 51b8bdb00152a401ec7c5a0f7e89178acc1fe633 Mon Sep 17 00:00:00 2001 From: Inte Vleminckx Date: Wed, 5 Feb 2025 16:26:30 +0100 Subject: [PATCH 10/43] Add test for bytes type --- state/test/test_create_nodevalue.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/state/test/test_create_nodevalue.py b/state/test/test_create_nodevalue.py index 2fddbb5..ca4e8b9 100644 --- a/state/test/test_create_nodevalue.py +++ b/state/test/test_create_nodevalue.py @@ -171,3 +171,12 @@ def test_create_nodevalue_string_type(state): def test_create_nodevalue_invalid_type(state): id1 = state.create_nodevalue(("Class",)) assert id1 == None + + +@pytest.mark.usefixtures("state") +def test_create_nodevalue_bytes_type(state): + id1 = state.create_nodevalue(("Bytes",)) + assert id1 != None + + v = state.read_value(id1) + assert v == ("Bytes",) \ No newline at end of file From ced3edbd08fe83c2a51d4a1369553c69a5222eca Mon Sep 17 00:00:00 2001 From: Inte Vleminckx Date: Wed, 19 Feb 2025 10:36:05 +0100 Subject: [PATCH 11/43] fix bytes extraction + give created objects also a name when None is provided (same as links) --- api/od.py | 16 +++++++++++----- concrete_syntax/textual_od/parser.py | 4 ++-- services/od.py | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/api/od.py b/api/od.py index ffdef1c..24be76a 100644 --- a/api/od.py +++ b/api/od.py @@ -10,7 +10,8 @@ from uuid import UUID from typing import Optional from util.timer import Timer -NEXT_ID = 0 +NEXT_LINK_ID = 0 +NEXT_OBJ_ID = 0 # Models map names to elements # This builds the inverse mapping, so we can quickly lookup the name of an element @@ -247,7 +248,7 @@ class ODAPI: raise Exception("Unimplemented type "+value) def create_link(self, link_name: Optional[str], assoc_name: str, src: UUID, tgt: UUID): - global NEXT_ID + global NEXT_LINK_ID types = self.bottom.read_outgoing_elements(self.mm, assoc_name) if len(types) == 0: raise Exception(f"No such association: '{assoc_name}'") @@ -255,13 +256,18 @@ class ODAPI: raise Exception(f"More than one association exists with name '{assoc_name}' - this means the MM is invalid.") typ = types[0] if link_name == None: - link_name = f"__{assoc_name}{NEXT_ID}" - NEXT_ID += 1 + link_name = f"__{assoc_name}{NEXT_LINK_ID}" + NEXT_LINK_ID += 1 link_id = self.od._create_link(link_name, typ, src, tgt) self.__recompute_mappings() + return link_id def create_object(self, object_name: Optional[str], class_name: str): + global NEXT_OBJ_ID + if object_name == None: + object_name = f"__{class_name}{NEXT_OBJ_ID}" + NEXT_OBJ_ID += 1 obj = self.od.create_object(object_name, class_name) self.__recompute_mappings() return obj @@ -286,7 +292,7 @@ def bind_api_readonly(odapi): 'get_type_name': odapi.get_type_name, 'get_outgoing': odapi.get_outgoing, 'get_incoming': odapi.get_incoming, - 'has_slot': odapi.has_slot, + 'has_slot': odapi.has_slot } return funcs diff --git a/concrete_syntax/textual_od/parser.py b/concrete_syntax/textual_od/parser.py index 35ca933..3777250 100644 --- a/concrete_syntax/textual_od/parser.py +++ b/concrete_syntax/textual_od/parser.py @@ -93,8 +93,8 @@ def parse_od(state, return (_Code(str(token[1:-1])), token.line) # strip the `` def BYTES(self, token): - # return (bytes(token[2:-1], "utf-8"), token.line) # Strip b"" or b'' - return (bytes(token[2:-1], "utf-8"), token.line) # Strip b"" or b'' + # Strip b"" or b'', and make \\ back to \ (happens when reading the file as a string) + return (token[2:-1].encode().decode('unicode_escape').encode('raw_unicode_escape'), token.line) # Strip b"" or b'' def INDENTED_CODE(self, token): skip = 4 # strip the ``` and the following newline character diff --git a/services/od.py b/services/od.py index 816b225..a9cb583 100644 --- a/services/od.py +++ b/services/od.py @@ -148,7 +148,7 @@ class OD: actioncode_t.create(value) return self.create_model_ref(name, "ActionCode", actioncode_node) - def create_bytes_value(self, name: str, value: str): + def create_bytes_value(self, name: str, value: bytes): from services.primitives.bytes_type import Bytes bytes_node = self.bottom.create_node() bytes_t = Bytes(bytes_node, self.bottom.state) From 2c64ebda6743b2ecc9756b8618e53839f84cc93e Mon Sep 17 00:00:00 2001 From: robbe Date: Thu, 24 Apr 2025 12:23:07 +0200 Subject: [PATCH 12/43] Scheduler first commit --- examples/schedule/RuleExecuter.py | 49 +++++++ examples/schedule/ScheduledActionGenerator.py | 104 ++++++++++++++ examples/schedule/__init__.py | 0 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/singleton.py | 8 ++ 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 +++++++ 24 files changed, 880 insertions(+) create mode 100644 examples/schedule/RuleExecuter.py create mode 100644 examples/schedule/ScheduledActionGenerator.py create mode 100644 examples/schedule/__init__.py create mode 100644 examples/schedule/generator.py create mode 100644 examples/schedule/models/README.md create mode 100644 examples/schedule/models/scheduling_MM.od create mode 100644 examples/schedule/schedule_lib/__init__.py create mode 100644 examples/schedule/schedule_lib/data.py create mode 100644 examples/schedule/schedule_lib/data_modify.py create mode 100644 examples/schedule/schedule_lib/data_node.py create mode 100644 examples/schedule/schedule_lib/end.py create mode 100644 examples/schedule/schedule_lib/exec_node.py create mode 100644 examples/schedule/schedule_lib/funcs.py create mode 100644 examples/schedule/schedule_lib/id_generator.py create mode 100644 examples/schedule/schedule_lib/loop.py create mode 100644 examples/schedule/schedule_lib/match.py create mode 100644 examples/schedule/schedule_lib/null_node.py create mode 100644 examples/schedule/schedule_lib/print.py create mode 100644 examples/schedule/schedule_lib/rewrite.py create mode 100644 examples/schedule/schedule_lib/singleton.py create mode 100644 examples/schedule/schedule_lib/start.py create mode 100644 examples/schedule/templates/schedule_dot.j2 create mode 100644 examples/schedule/templates/schedule_template.j2 create mode 100644 examples/schedule/templates/schedule_template_wrap.j2 diff --git a/examples/schedule/RuleExecuter.py b/examples/schedule/RuleExecuter.py new file mode 100644 index 0000000..8566d10 --- /dev/null +++ b/examples/schedule/RuleExecuter.py @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..34b1b96 --- /dev/null +++ b/examples/schedule/ScheduledActionGenerator.py @@ -0,0 +1,104 @@ +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.petrinet.schedule import Schedule +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/__init__.py b/examples/schedule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/schedule/generator.py b/examples/schedule/generator.py new file mode 100644 index 0000000..ed8a111 --- /dev/null +++ b/examples/schedule/generator.py @@ -0,0 +1,129 @@ +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 new file mode 100644 index 0000000..5767d48 --- /dev/null +++ b/examples/schedule/models/README.md @@ -0,0 +1,26 @@ + +### 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 new file mode 100644 index 0000000..533d8bc --- /dev/null +++ b/examples/schedule/models/scheduling_MM.od @@ -0,0 +1,46 @@ +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 new file mode 100644 index 0000000..0b826ab --- /dev/null +++ b/examples/schedule/schedule_lib/__init__.py @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000..88bcb42 --- /dev/null +++ b/examples/schedule/schedule_lib/data.py @@ -0,0 +1,63 @@ +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 new file mode 100644 index 0000000..0df6cba --- /dev/null +++ b/examples/schedule/schedule_lib/data_modify.py @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..557f297 --- /dev/null +++ b/examples/schedule/schedule_lib/data_node.py @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..2a008c4 --- /dev/null +++ b/examples/schedule/schedule_lib/end.py @@ -0,0 +1,21 @@ +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 new file mode 100644 index 0000000..c5d2d04 --- /dev/null +++ b/examples/schedule/schedule_lib/exec_node.py @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000..0b19b99 --- /dev/null +++ b/examples/schedule/schedule_lib/funcs.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..d1f4b25 --- /dev/null +++ b/examples/schedule/schedule_lib/id_generator.py @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..44ec5c5 --- /dev/null +++ b/examples/schedule/schedule_lib/loop.py @@ -0,0 +1,57 @@ +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 new file mode 100644 index 0000000..f350ba6 --- /dev/null +++ b/examples/schedule/schedule_lib/match.py @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..2d322bb --- /dev/null +++ b/examples/schedule/schedule_lib/null_node.py @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..ed0bbc6 --- /dev/null +++ b/examples/schedule/schedule_lib/print.py @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..c00ee8e --- /dev/null +++ b/examples/schedule/schedule_lib/rewrite.py @@ -0,0 +1,38 @@ +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/singleton.py b/examples/schedule/schedule_lib/singleton.py new file mode 100644 index 0000000..31955e3 --- /dev/null +++ b/examples/schedule/schedule_lib/singleton.py @@ -0,0 +1,8 @@ +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) + return cls._instances[cls] diff --git a/examples/schedule/schedule_lib/start.py b/examples/schedule/schedule_lib/start.py new file mode 100644 index 0000000..44ed1e1 --- /dev/null +++ b/examples/schedule/schedule_lib/start.py @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..39d2672 --- /dev/null +++ b/examples/schedule/templates/schedule_dot.j2 @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..a0c251c --- /dev/null +++ b/examples/schedule/templates/schedule_template.j2 @@ -0,0 +1,35 @@ +{% 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 new file mode 100644 index 0000000..389f2c2 --- /dev/null +++ b/examples/schedule/templates/schedule_template_wrap.j2 @@ -0,0 +1,47 @@ +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 From 87fc7362dba5be1c8039e3da517839daac9d94ee Mon Sep 17 00:00:00 2001 From: robbe Date: Thu, 24 Apr 2025 12:23:29 +0200 Subject: [PATCH 13/43] Scheduler petrinet example --- examples/petrinet/models/schedule.od | 70 +++++++++++++++++++ .../all_input_have_token.od | 13 ++++ .../operational_semantics/all_inputs.od | 13 ++++ .../all_output_places.od | 13 ++++ .../all_output_places_update.od | 13 ++++ .../operational_semantics/delete_all.od | 0 .../r_fire_transition_lhs.od | 2 +- .../operational_semantics/transition.od | 1 + examples/petrinet/runner.py | 20 ++++-- 9 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 examples/petrinet/models/schedule.od create mode 100644 examples/petrinet/operational_semantics/all_input_have_token.od create mode 100644 examples/petrinet/operational_semantics/all_inputs.od create mode 100644 examples/petrinet/operational_semantics/all_output_places.od create mode 100644 examples/petrinet/operational_semantics/all_output_places_update.od create mode 100644 examples/petrinet/operational_semantics/delete_all.od create mode 100644 examples/petrinet/operational_semantics/transition.od diff --git a/examples/petrinet/models/schedule.od b/examples/petrinet/models/schedule.od new file mode 100644 index 0000000..3dce69d --- /dev/null +++ b/examples/petrinet/models/schedule.od @@ -0,0 +1,70 @@ +start:Start +end:End + +transitions:Match{ + file = "operational_semantics/transition"; +} + + +d:Data_modify +{ + rename = ' + { + "tr": null + }'; + delete = ' + { + "tr": null + }'; +} + +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/operational_semantics/all_input_have_token.od b/examples/petrinet/operational_semantics/all_input_have_token.od new file mode 100644 index 0000000..9207ce2 --- /dev/null +++ b/examples/petrinet/operational_semantics/all_input_have_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/all_inputs.od b/examples/petrinet/operational_semantics/all_inputs.od new file mode 100644 index 0000000..1b87f1d --- /dev/null +++ b/examples/petrinet/operational_semantics/all_inputs.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/operational_semantics/all_output_places.od b/examples/petrinet/operational_semantics/all_output_places.od new file mode 100644 index 0000000..ab431cc --- /dev/null +++ b/examples/petrinet/operational_semantics/all_output_places.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/operational_semantics/all_output_places_update.od b/examples/petrinet/operational_semantics/all_output_places_update.od new file mode 100644 index 0000000..8d2908e --- /dev/null +++ b/examples/petrinet/operational_semantics/all_output_places_update.od @@ -0,0 +1,13 @@ +# A place with no tokens: + +p:RAM_PNPlace +ps:RAM_PNPlaceState { + RAM_numTokens = `set_value(this, 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/delete_all.od b/examples/petrinet/operational_semantics/delete_all.od new file mode 100644 index 0000000..e69de29 diff --git a/examples/petrinet/operational_semantics/r_fire_transition_lhs.od b/examples/petrinet/operational_semantics/r_fire_transition_lhs.od index c3bd82c..c05515b 100644 --- a/examples/petrinet/operational_semantics/r_fire_transition_lhs.od +++ b/examples/petrinet/operational_semantics/r_fire_transition_lhs.od @@ -1 +1 @@ -t:RAM_PNTransition \ No newline at end of file +t:RAM_PNTransition diff --git a/examples/petrinet/operational_semantics/transition.od b/examples/petrinet/operational_semantics/transition.od new file mode 100644 index 0000000..c7c8203 --- /dev/null +++ b/examples/petrinet/operational_semantics/transition.od @@ -0,0 +1 @@ +tr:RAM_PNTransition \ No newline at end of file diff --git a/examples/petrinet/runner.py b/examples/petrinet/runner.py index b2d0c51..df8692e 100644 --- a/examples/petrinet/runner.py +++ b/examples/petrinet/runner.py @@ -1,3 +1,4 @@ +from examples.schedule.RuleExecuter import RuleExecuter from state.devstate import DevState from api.od import ODAPI from concrete_syntax.textual_od.renderer import render_od @@ -9,6 +10,10 @@ 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 * + + if __name__ == "__main__": import os @@ -46,19 +51,24 @@ if __name__ == "__main__": mm_rt_ramified, ["fire_transition"]) # only 1 rule :( - matcher_rewriter = RuleMatcherRewriter(state, mm_rt, mm_rt_ramified) - action_generator = ActionGenerator(matcher_rewriter, rules) + # 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) - sim = simulator.Simulator( + action_generator.generate_dot() + + sim = simulator.MinimalSimulator( action_generator=action_generator, decision_maker=simulator.InteractiveDecisionMaker(auto_proceed=False), # decision_maker=simulator.RandomDecisionMaker(seed=0), - renderer=render_callback, + 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)) + sim.run(ODAPI(state, m_rt_initial, mm_rt)) \ No newline at end of file From bad9e8e32af1b07d548651885380aa0913eefa3c Mon Sep 17 00:00:00 2001 From: robbe Date: Thu, 24 Apr 2025 12:24:51 +0200 Subject: [PATCH 14/43] removed unused variable --- transformation/rewriter.py | 1 - transformation/rule.py | 1 - 2 files changed, 2 deletions(-) diff --git a/transformation/rewriter.py b/transformation/rewriter.py index 100073f..9c29252 100644 --- a/transformation/rewriter.py +++ b/transformation/rewriter.py @@ -22,7 +22,6 @@ class TryAgainNextRound(Exception): # Rewrite is performed in-place (modifying `host_m`) def rewrite(state, - lhs_m: UUID, # LHS-pattern rhs_m: UUID, # RHS-pattern pattern_mm: UUID, # meta-model of both patterns (typically the RAMified host_mm) lhs_match: dict, # a match, morphism, from lhs_m to host_m (mapping pattern name -> host name), typically found by the 'match_od'-function. diff --git a/transformation/rule.py b/transformation/rule.py index 81436ad..7db576e 100644 --- a/transformation/rule.py +++ b/transformation/rule.py @@ -117,7 +117,6 @@ class RuleMatcherRewriter: try: rhs_match = rewrite(self.state, - lhs_m=lhs, rhs_m=rhs, pattern_mm=self.mm_ramified, lhs_match=lhs_match, From 5e5865d0d56485eb186ab0ae8af674946e19c3b0 Mon Sep 17 00:00:00 2001 From: robbe Date: Thu, 24 Apr 2025 12:25:44 +0200 Subject: [PATCH 15/43] base_case of len == 0 added (same as Interactive decisionMaker) --- util/simulator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/util/simulator.py b/util/simulator.py index cdbe6a6..c967bbd 100644 --- a/util/simulator.py +++ b/util/simulator.py @@ -27,6 +27,8 @@ class RandomDecisionMaker(DecisionMaker): def __call__(self, actions): arr = [action for descr, action in actions] + if len(arr) == 0: + return i = math.floor(self.r.random()*len(arr)) return arr[i] @@ -91,7 +93,7 @@ class MinimalSimulator: self._print("Start simulation") self._print(f"Decision maker: {self.decision_maker}") step_counter = 0 - while True: + while step_counter < 10: termination_reason = self.termination_condition(model) if termination_reason != None: self._print(f"Termination condition satisfied.\nReason: {termination_reason}.") From 756b3f30da1f163aa32b5ab00b05a7fd99e8f496 Mon Sep 17 00:00:00 2001 From: robbe Date: Thu, 24 Apr 2025 12:27:28 +0200 Subject: [PATCH 16/43] get_slots and is_instance added to readonly api + is_instance implementation --- api/cd.py | 5 ++++- api/od.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/cd.py b/api/cd.py index d18f7b0..16168d6 100644 --- a/api/cd.py +++ b/api/cd.py @@ -53,7 +53,7 @@ class CDAPI: return self.bottom.read_outgoing_elements(self.m, type_name)[0] def is_direct_subtype(self, super_type_name: str, sub_type_name: str): - return sub_type_name in self.direct_sub_types[super_type] + return sub_type_name in self.direct_sub_types[super_type_name] def is_direct_supertype(self, sub_type_name: str, super_type_name: str): return super_type_name in self.direct_super_types[sub_type_name] @@ -83,3 +83,6 @@ class CDAPI: result = self.find_attribute_type(supertype, attr_name) if result != None: return result + + def get_type(self, type_name: str): + return next(k for k, v in self.type_model_names.items() if v == type_name) diff --git a/api/od.py b/api/od.py index c23160d..b176abf 100644 --- a/api/od.py +++ b/api/od.py @@ -143,7 +143,7 @@ class ODAPI: 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"): - if type_of_obj in types: + if self.get_name(type_of_obj) in types: return True return False @@ -262,6 +262,7 @@ def bind_api_readonly(odapi): 'get_target': odapi.get_target, 'get_source': odapi.get_source, 'get_slot': odapi.get_slot, + 'get_slots': odapi.get_slots, 'get_slot_value': odapi.get_slot_value, 'get_slot_value_default': odapi.get_slot_value_default, 'get_all_instances': odapi.get_all_instances, @@ -270,6 +271,7 @@ def bind_api_readonly(odapi): 'get_outgoing': odapi.get_outgoing, 'get_incoming': odapi.get_incoming, 'has_slot': odapi.has_slot, + 'is_instance': odapi.is_instance, } return funcs From 8ee9fba4ea45fed8eb6c03d77722c62cc20187a9 Mon Sep 17 00:00:00 2001 From: robbe Date: Thu, 24 Apr 2025 12:33:38 +0200 Subject: [PATCH 17/43] petrinet example fixed --- examples/petrinet/models/schedule.od | 8 ++------ examples/schedule/ScheduledActionGenerator.py | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/petrinet/models/schedule.od b/examples/petrinet/models/schedule.od index 3dce69d..1584a7c 100644 --- a/examples/petrinet/models/schedule.od +++ b/examples/petrinet/models/schedule.od @@ -8,13 +8,9 @@ transitions:Match{ d:Data_modify { - rename = ' + modify_dict = ' { - "tr": null - }'; - delete = ' - { - "tr": null + "tr": "t" }'; } diff --git a/examples/schedule/ScheduledActionGenerator.py b/examples/schedule/ScheduledActionGenerator.py index 34b1b96..0f91121 100644 --- a/examples/schedule/ScheduledActionGenerator.py +++ b/examples/schedule/ScheduledActionGenerator.py @@ -8,17 +8,17 @@ 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.petrinet.schedule import Schedule 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.schedule: "Schedule" self.state = DevState() From 04a17f6ac8f17415f28a4008aed6c549c12b63d7 Mon Sep 17 00:00:00 2001 From: robbe Date: Thu, 24 Apr 2025 13:07:32 +0200 Subject: [PATCH 18/43] has_slot(obj) now works on instance ipv class. Useful for optional field --- api/od.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/od.py b/api/od.py index b176abf..bd0e2b7 100644 --- a/api/od.py +++ b/api/od.py @@ -151,10 +151,9 @@ class ODAPI: self.bottom.delete_element(obj) self.__recompute_mappings() - # Does the class of the object have the given attribute? + # Does the the object have the given attribute? def has_slot(self, obj: UUID, attr_name: str): - class_name = self.get_name(self.get_type(obj)) - return self.od.get_attr_link_name(class_name, attr_name) != None + return self.od.get_slot_link(obj, attr_name) != None def get_slots(self, obj: UUID) -> list[str]: return [attr_name for attr_name, _ in self.od.get_slots(obj)] From d00b9c25db0c2a95207e46299b82eb0cc5256c6b Mon Sep 17 00:00:00 2001 From: Inte Vleminckx Date: Tue, 3 Jun 2025 16:17:37 +0200 Subject: [PATCH 19/43] Add 'simplified' version of the FTG+PM++ formalism with operational semantics --- examples/ftg_pm_pt/ftg_pm_pt.py | 47 +++ examples/ftg_pm_pt/help_functions.py | 68 +++++ .../ftg_pm_pt/helpers/composite_activity.py | 272 ++++++++++++++++++ .../r_connect_process_trace_lhs.od | 2 + .../r_connect_process_trace_nac.od | 7 + .../r_connect_process_trace_rhs.od | 12 + .../r_exec_activity_lhs.od | 49 ++++ .../r_exec_activity_rhs.od | 42 +++ .../r_exec_composite_activity_lhs.od | 36 +++ .../r_exec_composite_activity_rhs.od | 29 ++ .../r_trigger_ctrl_flow_lhs.od | 20 ++ .../r_trigger_ctrl_flow_rhs.od | 42 +++ examples/ftg_pm_pt/pm/metamodels/mm_design.od | 200 +++++++++++++ .../ftg_pm_pt/pm/metamodels/mm_runtime.od | 38 +++ examples/ftg_pm_pt/pt/metamodels/mm_design.od | 109 +++++++ examples/ftg_pm_pt/runner.py | 162 +++++++++++ 16 files changed, 1135 insertions(+) create mode 100644 examples/ftg_pm_pt/ftg_pm_pt.py create mode 100644 examples/ftg_pm_pt/help_functions.py create mode 100644 examples/ftg_pm_pt/helpers/composite_activity.py create mode 100644 examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_lhs.od create mode 100644 examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_nac.od create mode 100644 examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_rhs.od create mode 100644 examples/ftg_pm_pt/operational_semantics/r_exec_activity_lhs.od create mode 100644 examples/ftg_pm_pt/operational_semantics/r_exec_activity_rhs.od create mode 100644 examples/ftg_pm_pt/operational_semantics/r_exec_composite_activity_lhs.od create mode 100644 examples/ftg_pm_pt/operational_semantics/r_exec_composite_activity_rhs.od create mode 100644 examples/ftg_pm_pt/operational_semantics/r_trigger_ctrl_flow_lhs.od create mode 100644 examples/ftg_pm_pt/operational_semantics/r_trigger_ctrl_flow_rhs.od create mode 100644 examples/ftg_pm_pt/pm/metamodels/mm_design.od create mode 100644 examples/ftg_pm_pt/pm/metamodels/mm_runtime.od create mode 100644 examples/ftg_pm_pt/pt/metamodels/mm_design.od create mode 100644 examples/ftg_pm_pt/runner.py diff --git a/examples/ftg_pm_pt/ftg_pm_pt.py b/examples/ftg_pm_pt/ftg_pm_pt.py new file mode 100644 index 0000000..aed0f77 --- /dev/null +++ b/examples/ftg_pm_pt/ftg_pm_pt.py @@ -0,0 +1,47 @@ +import os + +# Todo: remove src.backend.muMLE from the imports +from state.devstate import DevState +from bootstrap.scd import bootstrap_scd +from concrete_syntax.textual_od.parser import parse_od +from api.od import ODAPI +from concrete_syntax.textual_od.renderer import render_od as od_renderer +from concrete_syntax.plantuml import make_url as plant_url, renderer as plant_renderer +from concrete_syntax.graphviz import make_url as graphviz_url, renderer as graphviz_renderer + +class FtgPmPt: + + def __init__(self, name: str): + self.state = DevState() + self.scd_mmm = bootstrap_scd(self.state) + self.meta_model = self.load_metamodel() + self.model = None + self.odapi = None + self.name = name + + @staticmethod + def read_file(file_name): + with open(os.path.join(os.path.dirname(__file__), file_name)) as file: + return file.read() + + def load_metamodel(self): + mm_cs = self.read_file("pm/metamodels/mm_design.od") + mm_rt_cs = mm_cs + self.read_file("pm/metamodels/mm_runtime.od") + mm_total = mm_rt_cs + self.read_file("pt/metamodels/mm_design.od") + return parse_od(self.state, m_text=mm_total, mm=self.scd_mmm) + + def load_model(self, m_text: str | None = None): + m_text = "" if not m_text else m_text + self.model = parse_od(self.state, m_text=m_text, mm=self.meta_model) + self.odapi = ODAPI(self.state, self.model, self.meta_model) + + def render_od(self): + return od_renderer(self.state, self.model, self.meta_model, hide_names=False) + + def render_plantuml_object_diagram(self): + print(plant_url.make_url(plant_renderer.render_package( + self.name, plant_renderer.render_object_diagram(self.state, self.model, self.meta_model))) + ) + + def render_graphviz_object_diagram(self): + print(graphviz_url.make_url(graphviz_renderer.render_object_diagram(self.state, self.model, self.meta_model))) \ No newline at end of file diff --git a/examples/ftg_pm_pt/help_functions.py b/examples/ftg_pm_pt/help_functions.py new file mode 100644 index 0000000..2eab6ed --- /dev/null +++ b/examples/ftg_pm_pt/help_functions.py @@ -0,0 +1,68 @@ +import copy +import pickle + +from api.od import ODAPI + +from examples.ftg_pm_pt.helpers.composite_activity import execute_composite_workflow + +def serialize(obj): + return pickle.dumps(obj) + + +def deserialize(obj): + return pickle.loads(obj) + + +def create_activity_links(od: ODAPI, activity, prev_element, ctrl_port, end_trace=None, + relation_type="pt_IsFollowedBy"): + od.create_link(None, "pt_RelatesTo", activity, ctrl_port) + od.create_link(None, relation_type, prev_element, activity) + if end_trace: + od.create_link(None, "pt_IsFollowedBy", activity, end_trace) + + +def extract_input_data(od: ODAPI, activity): + input_data = {} + for has_data_in in od.get_outgoing(activity, "pm_HasDataIn"): + data_port = od.get_target(has_data_in) + artefact_state = od.get_source(od.get_incoming(od.get_source(od.get_incoming(data_port, "pm_DataFlowOut")[0]), "pm_Of")[0]) + input_data[od.get_name(data_port)] = deserialize(od.get_slot_value(artefact_state, "data")) + return input_data + + +def execute_activity(od: ODAPI, globs, activity, input_data): + inp = copy.deepcopy(input_data) # Necessary, otherwise the function changes the values inside the dictionary -> need the original values for process trace + func = globs[od.get_slot_value(activity, "func")] + return func(inp) if func.__code__.co_argcount > 0 else func() + + +def handle_artefact(od: ODAPI, activity, artefact_type, relation_type, data_port=None, data=None, + direction="DataFlowIn"): + artefact = od.create_object(None, "pt_Artefact") + if 'pt_Consumes' == relation_type: + od.create_link(None, relation_type, artefact, activity) + else: + od.create_link(None, relation_type, activity, artefact) + if data_port: + flow_direction = od.get_incoming if relation_type == 'pt_Consumes' else od.get_outgoing + ass_side = od.get_source if relation_type == 'pt_Consumes' else od.get_target + pm_artefact = ass_side(flow_direction(data_port, f"pm_{direction}")[0]) + prev_artefact = find_previous_artefact(od, od.get_incoming(pm_artefact, "pt_BelongsTo")) + if prev_artefact: + od.create_link(None, "pt_PrevVersion", artefact, prev_artefact) + od.create_link(None, "pt_BelongsTo", artefact, pm_artefact) + if data is not None: + artefact_state = od.get_source(od.get_incoming(pm_artefact, "pm_Of")[0]) + od.set_slot_value(artefact_state, "data", serialize(data)) + od.set_slot_value(artefact, "data", serialize(data)) + + +def find_previous_artefact(od: ODAPI, linked_artefacts): + return next((od.get_source(link) for link in linked_artefacts if + not od.get_incoming(od.get_source(link), "pt_PrevVersion")), None) + + +def update_control_states(od: ODAPI, activity, ctrl_out): + for has_ctrl_in in od.get_outgoing(activity, "pm_HasCtrlIn"): + od.set_slot_value(od.get_source(od.get_incoming(od.get_target(has_ctrl_in), "pm_Of")[0]), "active", False) + od.set_slot_value(od.get_source(od.get_incoming(ctrl_out, "pm_Of")[0]), "active", True) diff --git a/examples/ftg_pm_pt/helpers/composite_activity.py b/examples/ftg_pm_pt/helpers/composite_activity.py new file mode 100644 index 0000000..73063d6 --- /dev/null +++ b/examples/ftg_pm_pt/helpers/composite_activity.py @@ -0,0 +1,272 @@ +from uuid import UUID + +from api.od import ODAPI +from examples.ftg_pm_pt.ftg_pm_pt import FtgPmPt +from examples.ftg_pm_pt.runner import FtgPmPtRunner + + +def find_previous_artefact(od: ODAPI, linked_artefacts): + return next((od.get_source(link) for link in linked_artefacts if + not od.get_incoming(od.get_source(link), "pt_PrevVersion")), None) + + +def create_activity_links(od: ODAPI, activity, prev_element, ctrl_port, end_trace=None, + relation_type="pt_IsFollowedBy"): + od.create_link(None, "pt_RelatesTo", activity, ctrl_port) + od.create_link(None, relation_type, prev_element, activity) + if end_trace: + od.create_link(None, "pt_IsFollowedBy", activity, end_trace) + + +def get_workflow_path(od: ODAPI, activity: UUID): + return od.get_slot_value(activity, "subworkflow_path") + + +def get_workflow(workflow_path: str): + with open(workflow_path, "r") as f: + return f.read() + + +############################ + +def get_runtime_state(od: ODAPI, design_obj: UUID): + states = od.get_incoming(design_obj, "pm_Of") + if len(states) == 0: + print(f"Design object '{od.get_name(design_obj)}' has no runtime state.") + return None + return od.get_source(states[0]) + + +def get_source_incoming(od: ODAPI, obj: UUID, link_name: str): + links = od.get_incoming(obj, link_name) + if len(links) == 0: + print(f"Object '{od.get_name(obj)} has no incoming links of type '{link_name}'.") + return None + return od.get_source(links[0]) + + +def get_target_outgoing(od: ODAPI, obj: UUID, link_name: str): + links = od.get_outgoing(obj, link_name) + if len(links) == 0: + print(f"Object '{od.get_name(obj)} has no outgoing links of type '{link_name}'.") + return None + return od.get_target(links[0]) + + +def set_control_port_value(od: ODAPI, port: UUID, value: bool): + state = get_runtime_state(od, port) + od.set_slot_value(state, "active", value) + + +def set_artefact_data(od: ODAPI, artefact: UUID, value: bytes): + state = artefact + # Only the proces model of the artefact contains a runtime state + if od.get_type_name(state) == "pm_Artefact": + state = get_runtime_state(od, artefact) + od.set_slot_value(state, "data", value) + + +def get_artefact_data(od: ODAPI, artefact): + state = artefact + # Only the proces model of the artefact contains a runtime state + if od.get_type_name(state) == "pm_Artefact": + state = get_runtime_state(od, artefact) + return od.get_slot_value(state, "data") + + +############################ + +def set_workflow_control_source(workflow_model: FtgPmPt, ctrl_port_name: str, composite_linkage: dict): + od = workflow_model.odapi + source_port_name = composite_linkage[ctrl_port_name] + source_port = od.get(source_port_name) + set_control_port_value(od, source_port, True) + + +def set_workflow_artefacts(act_od: ODAPI, activity: UUID, workflow_model: FtgPmPt, composite_linkage: dict): + for data_port in [act_od.get_target(data_in) for data_in in act_od.get_outgoing(activity, "pm_HasDataIn")]: + # Get the data source port of the inner workflow + data_port_name = act_od.get_name(data_port) + source_port_name = composite_linkage[data_port_name] + source_port = workflow_model.odapi.get(source_port_name) + + # Get the artefact that is linked to the data port of the activity + act_artefact = get_source_incoming(act_od, data_port, "pm_DataFlowOut") + # Get the data of the artefact + artefact_data = get_artefact_data(act_od, act_artefact) + + # Get the artefact that is linked to the data port of the inner workflow + workflow_artefact = get_target_outgoing(workflow_model.odapi, source_port, "pm_DataFlowIn") + set_artefact_data(workflow_model.odapi, workflow_artefact, artefact_data) + + +def get_activity_port_from_inner_port(composite_linkage: dict, port_name: str): + for act_port_name, work_port_name in composite_linkage.items(): + if work_port_name == port_name: + return act_port_name + + +def execute_composite_workflow(od: ODAPI, activity: UUID, ctrl_port: UUID, composite_linkage: dict, + packages: dict | None, matched=None): + activity_name = od.get_slot_value(activity, "name") + + # First get the path of the object diagram file that contains the inner workflow of the activity + workflow_path = get_workflow_path(od, activity) + + # Read the object diagram file + workflow = get_workflow(workflow_path) + + # Create an FtgPmPt object + workflow_model = FtgPmPt(activity_name) + + # Load the workflow to the object + workflow_model.load_model(workflow) + + # Set the correct control source port of the workflow to active + set_workflow_control_source(workflow_model, od.get_name(ctrl_port), composite_linkage[activity_name]) + + # If a data port is linked, set the data of the artefact + set_workflow_artefacts(od, activity, workflow_model, composite_linkage[activity_name]) + + # Create an FtgPmPtRunner object with the FtgPmPt object + workflow_runner = FtgPmPtRunner(workflow_model) + + # Set the packages if present + workflow_runner.set_packages(packages, is_path=False) + + # Run the FtgPmPtRunner (is a subprocess necessary? This makes it more complicated because now we have direct access to the object) + workflow_runner.run() + + # Contains all the ports of the inner workflow -> map back to the activity ports, and so we can set the correct + # Control ports to active and also set the data artefacts correctly + ports = extract_inner_workflow(workflow_model.odapi) + start_act = None + end_act = None + for port in [port for port in ports if port]: + port_name = workflow_model.odapi.get_name(port) + activity_port_name = get_activity_port_from_inner_port(composite_linkage[activity_name], port_name) + activity_port = od.get(activity_port_name) + match workflow_model.odapi.get_type_name(port): + case "pm_CtrlSource": + start_act = handle_control_source(od, activity_port, matched("prev_trace_element")) + case "pm_CtrlSink": + end_act = handle_control_sink(od, activity_port, start_act, matched("end_trace")) + case "pm_DataSource": + handle_data_source(od, activity_port, start_act) + case "pm_DataSink": + handle_data_sink(od, workflow_model.odapi, activity_port, port, end_act) + + +def handle_control_source(od: ODAPI, port, prev_trace_elem): + set_control_port_value(od, port, False) + start_activity = od.create_object(None, "pt_StartActivity") + create_activity_links(od, start_activity, prev_trace_elem, port) + return start_activity + + +def handle_control_sink(od: ODAPI, port, start_act, end_trace): + set_control_port_value(od, port, True) + end_activity = od.create_object(None, "pt_EndActivity") + create_activity_links(od, end_activity, start_act, port, end_trace) + return end_activity + + +def handle_data_source(od: ODAPI, port, start_activity): + pt_artefact = od.create_object(None, "pt_Artefact") + od.create_link(None, "pt_Consumes", pt_artefact, start_activity) + + pm_artefact = get_source_incoming(od, port, "pm_DataFlowOut") + pm_artefact_data = get_artefact_data(od, pm_artefact) + set_artefact_data(od, pt_artefact, pm_artefact_data) + prev_pt_artefact = find_previous_artefact(od, od.get_incoming(pm_artefact, "pt_BelongsTo")) + if prev_pt_artefact: + od.create_link(None, "pt_PrevVersion", pt_artefact, prev_pt_artefact) + od.create_link(None, "pt_BelongsTo", pt_artefact, pm_artefact) + + +def handle_data_sink(act_od: ODAPI, work_od: ODAPI, act_port, work_port, end_activity): + pt_artefact = act_od.create_object(None, "pt_Artefact") + act_od.create_link(None, "pt_Produces", end_activity, pt_artefact) + + work_artefact = get_source_incoming(work_od, work_port, "pm_DataFlowOut") + work_artefact_data = get_artefact_data(work_od, work_artefact) + + act_artefact = get_target_outgoing(act_od, act_port, "pm_DataFlowIn") + + set_artefact_data(act_od, act_artefact, work_artefact_data) + set_artefact_data(act_od, pt_artefact, work_artefact_data) + + prev_pt_artefact = find_previous_artefact(act_od, act_od.get_incoming(act_artefact, "pt_BelongsTo")) + if prev_pt_artefact: + act_od.create_link(None, "pt_PrevVersion", pt_artefact, prev_pt_artefact) + act_od.create_link(None, "pt_BelongsTo", pt_artefact, act_artefact) + + +def extract_inner_workflow(workflow: ODAPI): + # Get the model, this should be only one + name, model = workflow.get_all_instances("pm_Model")[0] + + # Get the start of the process trace + start_trace = get_source_incoming(workflow, model, "pt_Starts") + # Get the end of the process trace + end_trace = get_source_incoming(workflow, model, "pt_Ends") + + # Get the first started activity + first_activity = get_target_outgoing(workflow, start_trace, "pt_IsFollowedBy") + # Get the last ended activity + end_activity = get_source_incoming(workflow, end_trace, "pt_IsFollowedBy") + + # Get the control port that started the activity + act_ctrl_in = get_target_outgoing(workflow, first_activity, "pt_RelatesTo") + # Get the control port that is activated when the activity is executed + act_ctrl_out = get_target_outgoing(workflow, end_activity, "pt_RelatesTo") + + # Get the control source of the workflow + ports = [] + for port in workflow.get_incoming(act_ctrl_in, "pm_CtrlFlow"): + source = workflow.get_source(port) + if workflow.get_type_name(source) == "pm_CtrlSource": + # Only one port can activate an activity + ports.append(source) + break + + # Get the control sink of the workflow + for port in workflow.get_outgoing(act_ctrl_out, "pm_CtrlFlow"): + sink = workflow.get_target(port) + if workflow.get_type_name(sink) == "pm_CtrlSink": + # Only one port can be set to active one an activity is ended + ports.append(sink) + break + + # Get the data port that the activity consumes (if used) + consumed_links = workflow.get_incoming(first_activity, "pt_Consumes") + if len(consumed_links) > 0: + pt_artefact = None + for link in consumed_links: + pt_artefact = workflow.get_source(link) + # Check if it is the first artefact -> contains no previous version + if len(workflow.get_outgoing(pt_artefact, "pt_PrevVersion")) == 0: + break + + pm_artefact = get_target_outgoing(workflow, pt_artefact, "pt_BelongsTo") + # Find the data source port + for link in workflow.get_incoming(pm_artefact, "pm_DataFlowIn"): + source = workflow.get_source(link) + if workflow.get_type_name(source) == "pm_DataSource": + # An activity can only use one artefact as input + ports.append(source) + break + + # Get all data ports that are connected to an artefact that is produced by an activity in the workflow, + # where the artefact is also part of main workflow + for port_name, data_sink in workflow.get_all_instances("pm_DataSink"): + pm_art = get_source_incoming(workflow, data_sink, "pm_DataFlowOut") + # If the pm_artefact is linked to a proces trace artefact that is produced, we can add to port + links = workflow.get_incoming(pm_art, "pt_BelongsTo") + if not len(links): + continue + # A data sink port linkage will only be added to the proces trace when an activity is ended and so an artefact + # is produced, meaning that if a belongsTo link exists, a proces trace artefact is linked to this data port + ports.append(data_sink) + + return ports diff --git a/examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_lhs.od b/examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_lhs.od new file mode 100644 index 0000000..b24c468 --- /dev/null +++ b/examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_lhs.od @@ -0,0 +1,2 @@ +# Match the model +model:RAM_pm_Model diff --git a/examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_nac.od b/examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_nac.od new file mode 100644 index 0000000..fb604ba --- /dev/null +++ b/examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_nac.od @@ -0,0 +1,7 @@ +model:RAM_pm_Model + +# Check if the model isn't already connected to a process trace +start_trace:RAM_pt_StartTrace + :RAM_pt_Starts (start_trace -> model) +end_trace:RAM_pt_EndTrace + :RAM_pt_Ends (end_trace -> model) diff --git a/examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_rhs.od b/examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_rhs.od new file mode 100644 index 0000000..f81fb45 --- /dev/null +++ b/examples/ftg_pm_pt/operational_semantics/r_connect_process_trace_rhs.od @@ -0,0 +1,12 @@ +# Keep the left hand side +model:RAM_pm_Model + +# Connect a process trace to it +start_trace:RAM_pt_StartTrace + starts:RAM_pt_Starts (start_trace -> model) + +end_trace:RAM_pt_EndTrace + ends:RAM_pt_Ends (end_trace -> model) + +# Connect the start with the end +:RAM_pt_IsFollowedBy (start_trace -> end_trace) diff --git a/examples/ftg_pm_pt/operational_semantics/r_exec_activity_lhs.od b/examples/ftg_pm_pt/operational_semantics/r_exec_activity_lhs.od new file mode 100644 index 0000000..50460e0 --- /dev/null +++ b/examples/ftg_pm_pt/operational_semantics/r_exec_activity_lhs.od @@ -0,0 +1,49 @@ +# When a control port is active and is connected to an activity, we want to execute the activity +# But, if the activity has input_and (input_or = False). It only can be activated if all its inputs are active + + +# Match the model +model:RAM_pm_Model + +# Match the a python automated activity +py_activity:RAM_pm_PythonAutomatedActivity { + # Check if all connected ports are active in case of input_and + condition = ``` + all_active = True + + # Check for or / and + if not get_slot_value(this, "input_or"): + # Get all the ctrl in ports + for has_ctrl_in in get_outgoing(this, "pm_HasCtrlIn"): + c_in_state = get_source(get_incoming(get_target(has_ctrl_in), "pm_Of")[0]) + # Check if the port is active or not + if not get_slot_value(c_in_state, "active"): + all_active = False + break + + all_active + ```; +} model_to_activity:RAM_pm_Owns (model -> py_activity) + + +# Match a control activity in port that is active +ctrl_in:RAM_pm_CtrlActivityIn + +ctrl_in_state:RAM_pm_CtrlPortState { + RAM_active = `get_value(this)`; +} + +state_to_port:RAM_pm_Of (ctrl_in_state -> ctrl_in) + +# Match the activity link to the port +activity_to_port:RAM_pm_HasCtrlIn (py_activity -> ctrl_in) + +# Match the end of the trace +end_trace:RAM_pt_EndTrace +ends:RAM_pt_Ends (end_trace -> model) + +# Match the previous trace element before the end trace +prev_trace_element:RAM_pt_Event + +followed_by:RAM_pt_IsFollowedBy (prev_trace_element -> end_trace) + diff --git a/examples/ftg_pm_pt/operational_semantics/r_exec_activity_rhs.od b/examples/ftg_pm_pt/operational_semantics/r_exec_activity_rhs.od new file mode 100644 index 0000000..27808eb --- /dev/null +++ b/examples/ftg_pm_pt/operational_semantics/r_exec_activity_rhs.od @@ -0,0 +1,42 @@ +model:RAM_pm_Model + +py_activity:RAM_pm_PythonAutomatedActivity { + + condition = ``` + start_activity = create_object(None, "pt_StartActivity") + create_activity_links(odapi, start_activity, matched("prev_trace_element"), matched("ctrl_in")) + input_data = extract_input_data(odapi, this) + result = execute_activity(odapi, globals()["packages"], this, input_data) + if len(result) == 3: + status_code, output_data, input_used = result + else: + status_code, output_data, input_used = *result, None + if input_used: + handle_artefact(odapi, start_activity, "pt_Artefact", "pt_Consumes", get(input_used), input_data[input_used], direction="DataFlowOut") + end_activity = create_object(None, "pt_EndActivity") + ctrl_out = get(status_code) + create_activity_links(odapi, end_activity, start_activity, ctrl_out, end_trace=matched("end_trace")) + if output_data: + port, data = output_data + handle_artefact(odapi, end_activity, "pt_Artefact", "pt_Produces", get(port), data, direction="DataFlowIn") + update_control_states(odapi, this, ctrl_out) + ```; +} + +model_to_activity:RAM_pm_Owns + +ctrl_in:RAM_pm_CtrlActivityIn + +ctrl_in_state:RAM_pm_CtrlPortState { + RAM_active = `False`; +} + +state_to_port:RAM_pm_Of (ctrl_in_state -> ctrl_in) + +activity_to_port:RAM_pm_HasCtrlIn (py_activity -> ctrl_in) + +end_trace:RAM_pt_EndTrace +ends:RAM_pt_Ends (end_trace -> model) + +prev_trace_element:RAM_pt_Event + diff --git a/examples/ftg_pm_pt/operational_semantics/r_exec_composite_activity_lhs.od b/examples/ftg_pm_pt/operational_semantics/r_exec_composite_activity_lhs.od new file mode 100644 index 0000000..b472cd0 --- /dev/null +++ b/examples/ftg_pm_pt/operational_semantics/r_exec_composite_activity_lhs.od @@ -0,0 +1,36 @@ +# When a control port is active and is connected to an activity, we want to execute the activity. If it is a composite one, we execute the inner workflow of it +# But, if the activity has input_and (input_or = False). It only can be activated if all its inputs are active + + +# Match the model +model:RAM_pm_Model + +# Match the a python automated activity +activity:RAM_pm_Activity { + + RAM_composite = `True`; + +} model_to_activity:RAM_pm_Owns (model -> activity) + + +# Match a control activity in port that is active +ctrl_in:RAM_pm_CtrlActivityIn + +ctrl_in_state:RAM_pm_CtrlPortState { + RAM_active = `get_value(this)`; +} + +state_to_port:RAM_pm_Of (ctrl_in_state -> ctrl_in) + +# Match the activity link to the port +activity_to_port:RAM_pm_HasCtrlIn (activity -> ctrl_in) + +# Match the end of the trace +end_trace:RAM_pt_EndTrace +ends:RAM_pt_Ends (end_trace -> model) + +# Match the previous trace element before the end trace +prev_trace_element:RAM_pt_Event + +followed_by:RAM_pt_IsFollowedBy (prev_trace_element -> end_trace) + diff --git a/examples/ftg_pm_pt/operational_semantics/r_exec_composite_activity_rhs.od b/examples/ftg_pm_pt/operational_semantics/r_exec_composite_activity_rhs.od new file mode 100644 index 0000000..dc5e1c0 --- /dev/null +++ b/examples/ftg_pm_pt/operational_semantics/r_exec_composite_activity_rhs.od @@ -0,0 +1,29 @@ +model:RAM_pm_Model + +activity:RAM_pm_Activity { + + RAM_composite = `True`; + + condition = ``` + # Execute inner workflow + execute_composite_workflow(odapi, this, matched("ctrl_in"), globals()["composite_linkage"], globals()["packages"], matched) + ```; +} + +model_to_activity:RAM_pm_Owns + +ctrl_in:RAM_pm_CtrlActivityIn + +ctrl_in_state:RAM_pm_CtrlPortState { + RAM_active = `False`; +} + +state_to_port:RAM_pm_Of (ctrl_in_state -> ctrl_in) + +activity_to_port:RAM_pm_HasCtrlIn (activity -> ctrl_in) + +end_trace:RAM_pt_EndTrace +ends:RAM_pt_Ends (end_trace -> model) + +prev_trace_element:RAM_pt_Event + diff --git a/examples/ftg_pm_pt/operational_semantics/r_trigger_ctrl_flow_lhs.od b/examples/ftg_pm_pt/operational_semantics/r_trigger_ctrl_flow_lhs.od new file mode 100644 index 0000000..66557e6 --- /dev/null +++ b/examples/ftg_pm_pt/operational_semantics/r_trigger_ctrl_flow_lhs.od @@ -0,0 +1,20 @@ +# Match an active control output port +out_state:RAM_pm_CtrlPortState { + RAM_active = `get_value(this)`; +} + +out:RAM_pm_CtrlOut + +state_to_out:RAM_pm_Of (out_state -> out) + +# Match an inactive control input port +in_state:RAM_pm_CtrlPortState { + RAM_active = `not get_value(this)`; +} + +in:RAM_pm_CtrlIn + +state_to_in:RAM_pm_Of (in_state -> in) + +# Match the connection between those two ports +flow:RAM_pm_CtrlFlow (out -> in) diff --git a/examples/ftg_pm_pt/operational_semantics/r_trigger_ctrl_flow_rhs.od b/examples/ftg_pm_pt/operational_semantics/r_trigger_ctrl_flow_rhs.od new file mode 100644 index 0000000..3861692 --- /dev/null +++ b/examples/ftg_pm_pt/operational_semantics/r_trigger_ctrl_flow_rhs.od @@ -0,0 +1,42 @@ +# Copy the left hand side + +out_state:RAM_pm_CtrlPortState { + # Only set the output port to inactive if all connected input ports are set to active + RAM_active = ``` + set_to_active = False + + output_port = matched("out") + outgoing_flows = get_outgoing(output_port, "pm_CtrlFlow") + + # for each flow: pm_CtrlFlow -> pm_CtrlIn <- pm_Of <- pm_CtrlPortState == state + all_input_port_states = [get_source(get_incoming(get_target(flow), "pm_Of")[0]) for flow in outgoing_flows] + input_port_state = matched("in_state") + + for state in all_input_port_states: + is_active = get_slot_value(state, "active") + + # If the state is not active and it is not the input port state we have matched and planned to set active + # Then we can't yet set this output port state to active + if not is_active and state != input_port_state: + set_to_active = True + break + + # Set the attribute to the assigned value + set_to_active + ```; +} + +out:RAM_pm_CtrlOut + +state_to_out:RAM_pm_Of (out_state -> out) + +in_state:RAM_pm_CtrlPortState { + # Set the input port active + RAM_active = `True`; +} + +in:RAM_pm_CtrlIn + +state_to_in:RAM_pm_Of (in_state -> in) + +flow:RAM_pm_CtrlFlow (out -> in) diff --git a/examples/ftg_pm_pt/pm/metamodels/mm_design.od b/examples/ftg_pm_pt/pm/metamodels/mm_design.od new file mode 100644 index 0000000..719347b --- /dev/null +++ b/examples/ftg_pm_pt/pm/metamodels/mm_design.od @@ -0,0 +1,200 @@ +################################################## + +pm_Model:Class + +################################################## + +pm_Stateful:Class + +################################################## + +pm_ModelElement:Class { + abstract = True; +} + +################################################## + +pm_Activity:Class + :Inheritance (pm_Activity -> pm_ModelElement) + +pm_Activity_name:AttributeLink (pm_Activity -> String) { + name = "name"; + optional = False; +} + +pm_Activity_composite:AttributeLink (pm_Activity -> Boolean) { + name = "composite"; + optional = False; +} + +pm_Activity_subworkflow_path:AttributeLink (pm_Activity -> String) { + name = "subworkflow_path"; + optional = True; +} + + +pm_AutomatedActivity:Class { + abstract = True; +} :Inheritance (pm_AutomatedActivity -> pm_Activity) + +pm_AutomatedActivity_input_or:AttributeLink (pm_AutomatedActivity -> Boolean) { + name = "input_or"; + optional = False; +} + +pm_PythonAutomatedActivity:Class + :Inheritance (pm_PythonAutomatedActivity -> pm_AutomatedActivity) + +pm_PythonAutomatedActivity_func:AttributeLink (pm_PythonAutomatedActivity -> ActionCode) { + name = "func"; + optional = False; +} + +################################################## + +pm_Artefact:Class + :Inheritance (pm_Artefact -> pm_ModelElement) + :Inheritance (pm_Artefact -> pm_Stateful) + +################################################## + +pm_CtrlPort:Class { + abstract = True; +} :Inheritance (pm_CtrlPort -> pm_Stateful) + +pm_CtrlIn:Class { + abstract = True; +} :Inheritance (pm_CtrlIn -> pm_CtrlPort) + +pm_CtrlSink:Class { + # 1) A control sink port must have at least one incoming control flow + # 2) A control sink port can't have any control flow output + constraint = ``` + has_incoming = len(get_incoming(this, "pm_CtrlFlow")) > 0 + no_outgoing = len(get_outgoing(this, "pm_CtrlFlow")) == 0 + + # Return constraint + has_incoming and no_outgoing + ```; +} :Inheritance (pm_CtrlSink -> pm_CtrlIn) + +pm_CtrlActivityIn:Class { + # 1) Must have at least one incoming control flow + constraint = ``` + has_incoming = len(get_incoming(this, "pm_CtrlFlow")) > 0 + # Return constraint + has_incoming + ```; +} :Inheritance (pm_CtrlActivityIn -> pm_CtrlIn) + +pm_CtrlOut:Class { + abstract = True; +} :Inheritance (pm_CtrlOut -> pm_CtrlPort) + +pm_CtrlSource:Class { + # 1) A control source port can't have any control flow inputs + # 2) A control source port must have at least one outgoing control flow + constraint = ``` + no_incoming = len(get_incoming(this, "pm_CtrlFlow")) == 0 + has_outgoing = len(get_outgoing(this, "pm_CtrlFlow")) > 0 + + # Return constraint + no_incoming and has_outgoing + ```; +} :Inheritance (pm_CtrlSource -> pm_CtrlOut) + +pm_CtrlActivityOut:Class { + # 1) Must have at least one outgoing control flow + constraint = ``` + has_outgoing = len(get_outgoing(this, "pm_CtrlFlow")) > 0 + + # Return constraint + has_outgoing + ```; +} :Inheritance (pm_CtrlActivityOut -> pm_CtrlOut) + +################################################## + +pm_DataPort:Class { + abstract = True; +} + +pm_DataIn:Class { + abstract = True; +} :Inheritance (pm_DataIn -> pm_DataPort) + +pm_DataSink:Class + :Inheritance (pm_DataSink -> pm_DataIn) + +pm_DataActivityIn:Class + :Inheritance (pm_DataActivityIn -> pm_DataIn) + +pm_DataOut:Class { + abstract = True; +} :Inheritance (pm_DataOut -> pm_DataPort) + +pm_DataSource:Class + :Inheritance (pm_DataSource -> pm_DataOut) + +pm_DataActivityOut:Class + :Inheritance (pm_DataActivityOut -> pm_DataOut) + +################################################## +################################################## + +pm_Owns:Association (pm_Model -> pm_ModelElement) { + source_lower_cardinality = 1; + source_upper_cardinality = 1; +} + +################################################## + +pm_CtrlFlow:Association (pm_CtrlPort -> pm_CtrlPort) + +################################################## + +pm_HasCtrlIn:Association (pm_Activity -> pm_CtrlIn) { + source_upper_cardinality = 1; + target_lower_cardinality = 1; +} + +pm_HasCtrlOut:Association (pm_Activity -> pm_CtrlOut) { + source_upper_cardinality = 1; + target_lower_cardinality = 1; +} + +pm_HasDataIn:Association (pm_Activity -> pm_DataIn) { + source_upper_cardinality = 1; +} + +pm_HasDataOut:Association (pm_Activity -> pm_DataOut) { + source_upper_cardinality = 1; +} + +################################################## + +pm_DataFlowIn:Association (pm_DataOut -> pm_Artefact) { + source_lower_cardinality = 1; + target_lower_cardinality = 1; +} + +pm_DataFlowOut:Association (pm_Artefact -> pm_DataIn) { + source_lower_cardinality = 1; + target_lower_cardinality = 1; +} + +################################################## +################################################## + +has_source_and_sink:GlobalConstraint { + # There should be at least one source and sink control port + constraint = ``` + contains_source = len(get_all_instances("pm_CtrlSource")) > 0 + contains_sink = len(get_all_instances("pm_CtrlSink")) > 0 + + # return constraint + contains_source and contains_sink + ```; +} + +################################################## diff --git a/examples/ftg_pm_pt/pm/metamodels/mm_runtime.od b/examples/ftg_pm_pt/pm/metamodels/mm_runtime.od new file mode 100644 index 0000000..c45daef --- /dev/null +++ b/examples/ftg_pm_pt/pm/metamodels/mm_runtime.od @@ -0,0 +1,38 @@ +################################################## + +pm_State:Class { + abstract = True; +} + +################################################## + +pm_ArtefactState:Class + :Inheritance (pm_ArtefactState -> pm_State) + +pm_ArtefactState_data:AttributeLink (pm_ArtefactState -> Bytes) { + name = "data"; + optional = False; +} + +################################################## + +pm_CtrlPortState:Class + :Inheritance (pm_CtrlPortState -> pm_State) + +pm_CtrlPortState_active:AttributeLink (pm_CtrlPortState -> Boolean) { + name = "active"; + optional = False; +} + +################################################## +################################################## + +pm_Of:Association (pm_State -> pm_Stateful) { + # one-to-one + source_lower_cardinality = 1; + source_upper_cardinality = 1; + target_lower_cardinality = 1; + target_upper_cardinality = 1; +} + +################################################## diff --git a/examples/ftg_pm_pt/pt/metamodels/mm_design.od b/examples/ftg_pm_pt/pt/metamodels/mm_design.od new file mode 100644 index 0000000..c6fa85c --- /dev/null +++ b/examples/ftg_pm_pt/pt/metamodels/mm_design.od @@ -0,0 +1,109 @@ +################################################## + +pt_Event:Class { + abstract = True; +} + +################################################## + +pt_Activity:Class { + abstract = True; +} :Inheritance (pt_Activity -> pt_Event) + +pt_StartActivity:Class { + # A start activity can only be related to a control in port + constraint = ``` + correct_related = True + + port = get_target(get_outgoing(this, "pt_RelatesTo")[0]) + correct_related = port in [uid for _, uid in get_all_instances("pm_CtrlIn")] + correct_related + ```; + +} :Inheritance (pt_StartActivity -> pt_Activity) + +pt_EndActivity:Class { + # A end activity can only be related to a control out port + constraint = ``` + correct_related = True + + port = get_target(get_outgoing(this, "pt_RelatesTo")[0]) + correct_related = port in [uid for _, uid in get_all_instances("pm_CtrlOut")] + + correct_related + ```; + +} :Inheritance (pt_EndActivity -> pt_Activity) + +################################################## + +pt_StartTrace:Class + :Inheritance (pt_StartTrace -> pt_Event) + +pt_EndTrace:Class + :Inheritance (pt_EndTrace -> pt_Event) + +################################################## + +pt_Artefact:Class + :Inheritance (pt_Artefact -> pt_Event) + +pt_Artefact_data:AttributeLink (pt_Artefact -> Bytes) { + name = "data"; + optional = False; +} + +################################################## +################################################## + +pt_IsFollowedBy:Association (pt_Event -> pt_Event) { + source_upper_cardinality = 1; + target_upper_cardinality = 1; +} + +################################################## + +pt_RelatesTo:Association (pt_Activity -> pm_CtrlPort) { + source_upper_cardinality = 1; + target_lower_cardinality = 1; + target_upper_cardinality = 1; +} + +pt_Consumes:Association (pt_Artefact -> pt_StartActivity) { + source_upper_cardinality = 1; + target_lower_cardinality = 1; + target_upper_cardinality = 1; +} + +pt_Produces:Association (pt_EndActivity -> pt_Artefact) { + source_lower_cardinality = 1; + source_upper_cardinality = 1; + target_upper_cardinality = 1; +} + +################################################## + +pt_Starts:Association (pt_StartTrace -> pm_Model) { + source_upper_cardinality = 1; + target_lower_cardinality = 1; + target_upper_cardinality = 1; +} + +pt_Ends:Association (pt_EndTrace -> pm_Model) { + source_upper_cardinality = 1; + target_lower_cardinality = 1; + target_upper_cardinality = 1; +} +################################################## + +pt_PrevVersion:Association (pt_Artefact -> pt_Artefact) { + source_upper_cardinality = 1; + target_upper_cardinality = 1; +} + +pt_BelongsTo:Association (pt_Artefact -> pm_Artefact) { + target_lower_cardinality = 1; + target_upper_cardinality = 1; +} + +################################################## diff --git a/examples/ftg_pm_pt/runner.py b/examples/ftg_pm_pt/runner.py new file mode 100644 index 0000000..282138c --- /dev/null +++ b/examples/ftg_pm_pt/runner.py @@ -0,0 +1,162 @@ +import re + +from state.devstate import DevState +from bootstrap.scd import bootstrap_scd +from util import loader +from transformation.rule import RuleMatcherRewriter +from transformation.ramify import ramify +from concrete_syntax.graphviz import renderer as graphviz +from concrete_syntax.graphviz.make_url import make_url +from concrete_syntax.plantuml import renderer as plantuml +from concrete_syntax.plantuml.make_url import make_url as plant_make_url +from api.od import ODAPI +import os +from os import listdir +from os.path import isfile, join +import importlib.util +from util.module_to_dict import module_to_dict +from examples.ftg_pm_pt import help_functions + +from examples.ftg_pm_pt.ftg_pm_pt import FtgPmPt + + + +class FtgPmPtRunner: + + def __init__(self, model: FtgPmPt, composite_linkage: dict | None = None): + self.model = model + self.ram_mm = ramify(self.model.state, self.model.meta_model) + self.rules = self.load_rules() + self.packages = None + self.composite_linkage = composite_linkage + + def load_rules(self): + return loader.load_rules( + self.model.state, + lambda rule_name, kind: os.path.join( + os.path.dirname(__file__), + f"operational_semantics/r_{rule_name}_{kind}.od" + ), + self.ram_mm, + ["connect_process_trace", "trigger_ctrl_flow", "exec_activity", "exec_composite_activity"] + ) + + def set_packages(self, packages: str | dict, is_path: bool): + if not is_path: + self.packages = packages + return + + self.packages = self.parse_packages(packages) + + def parse_packages(self, packages_path: str) -> dict: + return self.collect_functions_from_packages(packages_path, packages_path) + + def collect_functions_from_packages(self, base_path, current_path): + functions_dict = {} + + for entry in listdir(current_path): + entry_path = join(current_path, entry) + + if isfile(entry_path) and entry.endswith(".py"): + module_name = self.convert_path_to_module_name(base_path, entry_path) + module = self.load_module_from_file(entry_path) + + for func_name, func in module_to_dict(module).items(): + functions_dict[f"{module_name}.{func_name}"] = func + + elif not isfile(entry_path): + nested_functions = self.collect_functions_from_packages(base_path, entry_path) + functions_dict.update(nested_functions) + + return functions_dict + + @staticmethod + def convert_path_to_module_name(base_path, file_path): + return file_path.replace(base_path, "").replace(".py", "").replace("/", "") + + @staticmethod + def load_module_from_file(file_path): + spec = importlib.util.spec_from_file_location("", file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + def create_matcher(self): + packages = module_to_dict(help_functions) + + if self.packages: + packages.update({ "packages": self.packages }) + + if self.composite_linkage: + packages.update({ "composite_linkage": self.composite_linkage }) + + matcher_rewriter = RuleMatcherRewriter( + self.model.state, self.model.meta_model, self.ram_mm, eval_context=packages + ) + return matcher_rewriter + + def visualize_model(self): + print(make_url(graphviz.render_object_diagram(self.model.state, self.model.model, self.model.meta_model))) + print(plant_make_url(plantuml.render_object_diagram(self.model.state, self.model.model, self.model.meta_model))) + + @staticmethod + def __extract_artefact_info(od, pt_art): + """Extract artefact metadata and data.""" + data = od.get_slot_value(pt_art, "data") + pm_art = od.get_name(od.get_target(od.get_outgoing(pt_art, "pt_BelongsTo")[0])) + has_prev_version = bool(od.get_outgoing(pt_art, "pt_PrevVersion")) + is_last_version = not od.get_incoming(pt_art, "pt_PrevVersion") + return { + "Artefact Name": pm_art, + "Data": data, + "Has previous version": has_prev_version, + "Is last version": is_last_version + } + + def __extract_inputs(self, od, event_node): + """Extract all consumed artefacts for an event.""" + return [ + self.__extract_artefact_info(od, od.get_source(consumes)) + for consumes in od.get_incoming(event_node, "pt_Consumes") + ] + + def __extract_outputs(self, od, event_node): + """Extract all produced artefacts for an event.""" + return [ + self.__extract_artefact_info(od, od.get_target(produces)) + for produces in od.get_outgoing(event_node, "pt_Produces") + ] + + @staticmethod + def to_snake_case(experiment_type): + # Finds uppercase letters that are not at the start of the string. + # Example: AtomicExperiment -> atomic_experiment + return re.sub(r'(? Date: Fri, 27 Jun 2025 12:15:19 +0200 Subject: [PATCH 20/43] 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 21/43] 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 22/43] 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 23/43] 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 24/43] 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 25/43] 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 zcmeAS@N?(olHy`uVBq!ia0y~yV1C2Ez%0+f#=yYfb3fLefuVuH)5S5QBJRyy_APgg z{QdUv#+NjOFARozTP|e(o4~?h+Y;xzVwr@~dx4IZj6V7c+v9jz?)exloS7udv#eFf z*3%$JZJ4q3r8ige(@cJy^k*0aqaiRF0;3@? z8UpkRfeXUNW{5^cpnzN1KRX%04#;h9kOeUs%v=ueffx?bN(t;B#sXf!4a^|Mg*J{G zV8t2EEN?&p3|kbL%D{|U0*uyRMs5d*#z_78^ZESQ8HULb5fN28R=i_iU=VRVz_%dn z%8J0W3kw|6J{}b}zwB#1_vcCfx+kvCwEdp5nAo&q-LHCH^}V_< zulil)ukY{kUtjxlT0g&yUw+=YI#9(ZxPf`^jb?tkjA{n;Ss6;k1rHoPf4S_x+?-M1 zz+ry-oPQtM?Qb0T+h6}@apm*5<(FnNFff!jAK?2E)XdIrChTXi@Mc0YZ%)5L!>7BY z*JIQE{d{g-e8#X{(zdGPS1toXgC56?#<*vv_4k`d=j~WHGi8$J8O!H$Cf7+joV5G( zLV5Essq7gmm(4oG_JM(cp;ur7^WGCbK0eOv|HWf`Cc&*oVq%@NgUYII>o*&kUtV7? zZ~K;kVSyr3*#hyKb-&-{HW(B=Iq@Z$K`HpczGt(tZ$12Y=Q1Ay!z;xE_NvZq{e3t5 z^p@XT&A5e~w?96ik@d{8+4=M8xEUB48f0Ja-mCo{Yt8eoftk-^KkI^{CGyqt?P@EY zNHZ|JXlsyt!D=E>_ipENv-$P^Du3xRjzqbr|IeLJC|MD46z-K&+$ zmxZU#Dn4h)dE@p4yWek;r^S}t{F24MaG_gp1G8-2wa9eS^tq*BRi;))8W@?+6dsqI z9yd30-_K{t3Hi?AaTN<^=IwfUX*DB*gTC7VzLE|4`~O_}^7i)iJB7z(Uv3w;Sp9x) zx}VKQ7vroe8o&Q5_uE{0v-kVGXGg{3x4gIiey2F?|KIQC>~a+gDnFk!-(6Go<;BHs zb;Z4tBy1`Q-d5h-bu@AP-fx?}+vaaNsRmAk`uqPBy)EXq`Ot8)_`L1e1C7jQd(H1f zMCvpZO_LDko*;pW_WI~LTw|NH&E`S*L(>+inzvwoZM|Nph< z{F~qT?0zKNR`_sKJbnhde9eWt#R>NV5A6GROnSC#`JIb0RWBBrojs{Of6uqdEyp&W zx6AGi-Sm)^f#FLY%Nqyw-{s|X5;ZUg%%H6gtK1j?f+A>+ixFVr8jP_u|6BZHfthetg_o%Js=~dgJ1TSjSzn zJ3jn;K7ae~z>Yb-{{48I*34_R;r;%9zg}-Wzl6E$>$U9NOV;oIck7r>G$QW%W~fSEjh67&!^L~ORvXn z|F&yh$$@p%%eKy+ch0PT!;19`3<|vsvM&-?7~U%8AE>*TI(_EOXR}U!+p_!mS*CAS z!=lgLlmGvA`~7RzPR=k)&U(#p!F*+=-{&La%ZQ+hzuU2Qjez)_v-0pYlcE4M-cK4@K+HbAyuGicD z^KAZ^FPHuE)2sIX{dW7U&c~|%Z#JJ_SL^qF{l;Tb+ZZ2Qi_YIFeC@2sWuLS~ooYtE z-)!!;eB5Jvrb~NWM&6ExZBbUQ8m}8vKX2x@+wlA9o39yj%hs;1l6knmvH8rSZvDLZ z!s~4|ZP?Hf8?rs*c*ymT=L`oF6WFaHcC#_;J;|-VC*j+TWPWLr_ZtrLnQf3WOlqm! zGyB?0$rn{?7t|eSU@V(EZ~nha-ug2qD!b28ofdIOXTt%eGAE0-+4m}+Z~a}o`9Suw zinC1fW0oIpUc2Rz*S8zzEuYVMwrcgdO|QSsFh3iz-bCcd>f>>TUtiT;UphN?TjX@P z`Xg8KzLuVtp!n@h(7cj+Z;@(@&Cio|O`p)(y7YPXt4Xh>yqflE#;aMc=DeEsN`V0! zsCStd7<#vQ*Iw=DTRJW3RNo~Di-HAvmohuptSVkry{dUt_o~SZRr*Y23!1ljGcYXJ zS+-(5tcIvHSy1(lVN2hu1+NypTJlP*;a(>wvAh*vVEAzP)URJpr^nCYHNSJg$LOTW zvpL1*Jooc1Xbn*hH4k+U4QKqK2g+vIsSFGY(xXD-O}{r*eN3B?$YwpCso~q=T`R3t z?Rxc0oAH7**qxz_3=Q+LLu>EW{eJ7azwYlFm3D?((_gK4wd$2l!@XvZy=%D`7{0`4 zzj}RCJU&N#+a;Z=47cXLTJ>tpE4_w$jbIfV3=B_%ckkQx``zxcj<<~LG85{!9i)Tj zhTaX8WxMbbRQ`)uGcZhufAecq;@0Bxw%dRH`F#Fz{=(@$U#(t$Y4wF;A?cyzl2ZI% z_&|>DmSs>_({K0dL`?nPud&zeRz9B_cmC+L;%fQoHygiXZJ8i<(WK_bhcB;Y&G=e+ zXGdY$rju&M)8nc#f9ZZPYL_jWF*B)4^NjWTJ(KIWbA3v~OCw4p9nzIS$>|IOLt<>% z&D63Jk9&;IMMQtUTYi70r~2G0fBt>n@1GNSKc@I>s#tUm$eIJ}@-+#3HXj_K#mjGH zF8^}d)bRCcIbqYUSAs9^=DW?Y!o6Y{)8}`nNEx&u!=F@5=YT-n353y;Q z@Zfg-e$(V*J%;)Fey07>&6hg%{iOQ*8xQZ^3iq%1eAfKg<9_>X|LcFh-F{X&f6v5Q zkJ=L-=RLb~J-+^K&BvqSvG>C7Ov)%O!j*a^!-^(wtPb{in zec|YT8m}dGZVK4B@BhEwcdvDya^Yp#_hs*td7rL!T)JcyU;lUOT$NR_Ol42QpP!K} zzcaCpTQF1OSDSQR#&c7PS1T5uSzrG*`|}ZD{~O;!;=kRmuiyM~+3Z=NVUb6#6<_?# z%*M09`Nscm+xOorO`l)8t#<9xE5ZI-Uzg02FFK)U_Trky?DTn+$KGr_F8Az+u)oRn zyJfQ@>wZ2x+bSNH@p|{atJzm0ug*Pde*ezA_Rr_6*WZ1<^?F?P_m%hO8K1K_{ASZ> zy|QXk4YOae&N8bGuR0yM-;rTeKPX3?WooGZ^H{#@l2q>J>HGg2WfNW0?evxHRa~a{ zp4i&2SJNi@ni(#iU$-lEy@S`}jFW%A-_H+=DRkW}FK%(K;&HFphD%nlS2R8D)_%Wx zcFSeI?0^5h@1G5-SoSUFzM3X7N51OC!e{CBzh_#0K4W|~uKI1Ne$ban)`DFxLLQXA z*?9a`P5#!aVb9jr|6N^Me>F6G>+gUIUzXedJ^3zu?bd5Xo6p z3*R|jORxWRdH${6r)5o*G&Y3kZ`Yev|FJv%#@oBBSA5OyrhGPd*dlz(=*QLdb*XCe zDw3+-KYZM8A9uSyrr;oJ+SjY$+xM!yUb}tX-h?v7-F?lcUOl(}zf)FAU)il^!+qEL z!N*<1uV)>x`1xe=Stf3sg!|r?3Zv@(etl~Qs^8z5@!S8|Q0vdw)ONtn{BFs!Etma_ z-DL_}W~R-`e6z2#@twEhE~ZY)=W{Omr7g92J@eqL^ZE6^Z|m_&-OB4Vy=L%yZuvZ2 z@xALEV*4zgN&Jre`uFQz5seqsnlG+eE-7th=ihewmBiaUpU;^c2sBu{DO5noia+$< zWFMnW+k3zKte%Hnd$srd<>mc9-`D@ouKPUu{*9lE%jOiFTK2qEs-;BlrA)Bp%a|aA zDN7!&w|N!ua!#nhvMsk26Ry7%_HuWWiE=ItKe(l@xoUI2%_ooB!BuBXuirSP{deBJ zuh*i>qUWx@bE=hgpx9l!Oh^u=`1h+PZbKkC-sX1p|i%SE@cxl+<17ORgJ|9ZLH z{PS7!`8%J_tA5kER_)2f6fbYxts&FpDr@ay3zk(sxGWMduln82t=D%w2tDxjRN8sl z?_0z-^l6m5<-9_?Xhfsb+BA?)Mtq`dYVMHuX6LO_g6Ry65^YZhB|`|L1u#UsIhG zXRc^0Dmp#uivMxfN~`~WK4-t4vTfh`eXAEIuls9lb$_d`HOrfgmmJN$&t7(Hh00v> ztS5yf=T;ufJgGW;%lfa?%kJ-dV*70Sy{hc#zf^wlUi$fdg`roemTxcXubi+0>(u>p zHlH!tdVNuwXYD%SXQx(Zy)uGS>1Uqz?`$gg8k4{G>$R<=T>mERUG?g(4_oIV1=rtY zw%xDZdb0H`;tpI}d~j`>{OhgK+inWK+Iclyw8WB&PxOb@|Gugn<~I_WZ)IPc;1}Bv zS8~yHtE=P6Z_DGJyIr}w>cIK0zq+K%rm)A}S*7k{`~1~XuONZ-56(>e{jgm=Z@c%U z6AY$G7Fj6){B@IQk=D*;hE0wk>(`$ZbkTopjo)Otsq+)Q`)S&&iz-{8zyA-AYG2Pu@;;*^+?i zrHdwdo^lU6Q1|7c`|O|3=kJf6w`I?dN8MZB3W%3VzK{h~%Fliuk7?{?lH!#X=Y3lH zYUT1b|3!~z{s{2Tp424&RqC~z&qvN1qH|tuZL9b;r7h_7=96(gfzHj>b|?58oU=Ql zv}EEchS>Z1dtVg4^7V^vd37&&>gkYR->a$dp5ZZtt-jW_Em7O~GxoYMb+olEXD(xH z>(<+~;_rTIzn4WT%lUV={rS!EMyhEp&yA}a9`=P!YAfDdvm(*y_oWFH?zY)c)88EY zwouM-YVxJJ-*3~uFIBiLX___Vt!7ETt+!nK{D!}~Ua#AH_{Zhrq20UN<7_)l`QP$X zpPO>ohwY7&?!tzZ6YrMYK6|bGiwwsvzIE$%GH6t;-?QbkEhx=yb7S1i^g6BoRnXpx z=~o+;d=B59@!}iL-FFK=$L)`OS0=tZ@W8r+_}{0vZd`R(&=$Q#9G*D8_wQuN`g(4~ z@=)jIyS4W}-O65nv-S$NiO3nP4^zTe->}ORG~`+o*!q5DD*IZontit*>y1~netNsy zgZFxIox9XHeeoU*|Lu2*yr+K!bxdQ|H^j|iee-JXwQnyMZd7D0i#>6Dc5Lm(qvGqo zewi-2A={+w+tPa_tn6G&A{j6K6zu(N%X=gAnAJb4d}&U}4cfJ}YPEhZ%GlS2J2yUy zxbWZfWug$H^&~0QH?#gbZojvrSH^PF>DQnR#XYelJ3UM$&Z%2%s^n1|qLRP8u#4r* zs?$=Yljpq%{_0Tdu)uUx(Dbj;v-1K{6Ib4vv?r_s+{v^3nY@nMh*$TT!LuhzvUYU@ z8>~N|=lS}o_WL&f-3JVBu71o^W?mTE(O@fkCF{jL?wX}>2iDoV+Pq8L-t+M7&1&~{ zy;`;U)v}ePtg~@kVwGFZ7-}#-A-k_~L%`@&lH`;Ld# zn!eugn)UKprdgS>nXflzow%^!)05hFTKc(=rTD;Gw*thMn?Y+Kthp(xW%@Xi6 zQJlB!qDxDkTIOXd+oo0OUt%wPzpzU1pslDwV{;Yr-JK8JGBhq+*!y+SQc770hK`r*lV!>1Fa;rq@r7G;2khc3Hd%40*p( zI&OXSd(*)9swAtk)7B=$O$q<%w#xAstMRU{PQ9Wpi;Y*Rf8E*<@ls*7+{vYnC&-yD zxoE3*A~I*=(Hw_$#YTTKTza(%Kbu=*sa)FmTrT(JintYi%btH1=yeE+I{7y{C~)rj zS8L0+f7@|Ubys=Z{`^(tRf|}omfwA~>C?i`q5HT0s$Lyf{i}MtXXe+qUB@T2F4gKj z7eDv7)%?=9*MF{6uKR53_7~js^8Y_0TVwq@W53HPiMuWYC#>SUb7l7mv17mA?OwmF zw@Hz8Zk}2a>ylSrLw<1ueOX~~cFo%BTzOSiJ(pPDa7BpDS@k_ieAce}ahd({_V?J< zOD$X99c!wg!faxz=PADZR=XJMqh0)4Bw1J8TK;>EUfATxothSxk8Wvty!+j*(|rC{ zFBF?=&SbhYH7x36U6}1Yt~(+M+5(}GE7M~fmrL19$!EP_{cgwNxg}YxqG226Uw-^y zi~L!JwcUlLUOVnpPwjSdIAeA@$Lv68{>D$sOLsl+JNIDCr=Sfhxoe(=9?rY_d9LV| zT?^{&mR{fbQq!Z<*yHtPy^OC5A2@tJ`?}_$ytQf63dZBMRsqM|@4NmCv%4~3!7WZrG49FtGU_l?BZqJYs1TXOgE*sD8%kNr0_YOY-V)pUKt%LRs4IenFuS1l~ov2|S7wte+M@BN#jb2hpjPG0)^ z+GJ&y624{mu7QUapK~|2ZBXNN&Tte}>`j7rj2Y z#@+cqY2fmN755B0V&7;=lg8xy!!mhx|dastD6dZ>y~W_RG70w?q|xplIR90t5;8h z?+0eC;0cYBYFqd4`=oz$TG8$YmrIG=X{|2W{Umvw%fYpuR(8Kl(VtWGdeZ!0=5DFb zNv}mxegs`uJ&|qK{`ljo?oFtgn>{_gu5zt<%>9$K@$SbXdL=!%zFoeZS<-OMoAvO? z;)uq8Pu4rC3%;({`J`&<_NFgStfnqM<$7G%vdO9BpT>JF@2R1-tIDhHUuB= zCG)Ob-;=bIc|pwEfazB&)=bRHn&8v-fT@am?su#Iq5LuFUKyY^`RkMN#jFZn=Uu$5 zC*^C^{w*Z`l4Z(k&strjFe`>rS6}T~!$NW;yVW#`zXySO!&{vf z3C?f3xV+!D>f+va?_M&@n&7`Z@PPm3Us7IcN*JcB4lRv5Txa@nT9YtC?8>+mbuBV- z{-8nX`S#CtsqdaT`EZ6tU6GpKeAT}?2}c&HGcD&Pe093O*U6xs_QwA+m`~^<)KpFC znP>g?Zu$MC_nlf_OkBI`DDTrC+rV1Rk1T4S(YmAmlKB+&M|?F6v+8(%|Np=3H?!C8 z-L=LV)G<9O9)Bk_*7g;z)zc}#msVd?x?l6T_vdr_|1*DTT%J+-^X>Ng+x-9U_;^hE za{fZ4c{QIrH(zvh}G7W~{w_Pto+!uWPnk@&e5!`LB25&bXvyn^y`N zhLz6Sk@)NFtQn<|{UKM+Twgn9iP+J*#pi9m%~4F~cmC@7b=eE$PR<*Rd}5#bEpOdk z+;5ll@5gcbTb~oZ-!1pw54CmqWl$&mWoT~tg$0gxnZEVg{W?+e?PhxHwM>sU8xjxi zT65X#*0xu#WS3XHUX?J>RV;IMLu`KtXx#4d>OjNAZoNtg{XSpazAo>W#`*>{KeYY$ z{|y^LaxYfiIS2CQ@5=(GPpZ$K)3Dd~=&JP_FTGUpxBt5(vhb+r<<+$T|1tF_->4UaeV*4gMXy;Q1e*99l;(#UYp(>j}dZnM0x|9$g3c*ZQQx2y8i z%H|o6l!h-sEb9 z)#gq+H#>jd&83N#d=0)nY?r@wZRN+~f%$KrPLI!fzunia_}kp_dnFyqW%<4^3w;pEbXqvu@|JS-W~yN~Nt) z2>qUA|NTz!%k8;x`8yu6RiExS<>t5x)X7K#&7yhhZk=*=`@V4F`*pw9zC9Qgb-111 ze}C@F_pSH#e?Diuo7ebS`Oc@)qI2ejh&i|M7=FK79zQ)-=4t5fosavh&lnz;xvZNI zK2zPk`bwbt&By;iGm@t_Ui=sucge(ZYTw$v(>FFIU(Ub%sq#^$`kVjqbw3h!-`!F8 zI4Ih5d6BGT(UQOa_y7Bfaxa`)|Kr{rJy8cKIvUwDUpTkeS8jEZ=^=RJkUh z_GapIQ{yuR&9UE)N#|eLCU0N2XXo#CyI;#GhySzEv+}cAe5g*fi~RI}U+HHorxcT=G;meLlB5ZhGwszL%ew+4)Rv=j~n_S$x*? zt)=q;x&BwZulipF#w}%k^GkU#`|;OLW|ZHnJp1qW`~3Qj5}{Z8YknU-dnjnD>+|)$ zAFuu#1*@=bJFc;R_F;Kz4+#C<|)4P`@QP?%SWH@+>!NE zH6h=BR|wa#bsMKW(W4jc0V9vtN6CZ&mr- z(&=~O8ox9!GQX)4NjdrA;^MQbRG)erWsfhFcel@-^TeHT=DUkw4@&=jy?&b^M*I-hBA5H_UjxL$ zxR#X!?mn%z`^NV^>vtBL4={1hnE(IJ^Jg0l^KEvFng$xaU9<1kE3*fC-fp{nX3gev zS=C!)d9CBi@0RAy`)Z!D`K;My@q1Yt1fE{}Ya1B3D*gV{(?6fjkH6i&V$PRJ)?J`! zieP`+Qr(3AkGl2W{H|Ja;n(Z+`PX|T3=h@mzx5XX_{+{}-K&}Nle#o>`tvR=ogSCf z-~VYLL%icbPW3qn_k$hY^u0gGE}v6&r_g=&brkvs40( zd;Scz+~Ry--L5qykjYk!&bsz%an-rf3%Xw|=xeO&|02CO+kbCd`rOi-=3Ck8_olsG zbx!hX?Zx-AUbVd1xcGHwZC`_}+*RqRJ3(oa^$ow>j|G1fFSkf$T`knT>Y#qPa^2_H z=X=l9Uu_QS`g&ZZ?n?Ax`)$9PS8*H9)lQ#ZTlV(eC%5O_bx%q+-ppBfJ$r`RfqVOs zu7=Mqx$U9{?b2age&xes@6-jy*rqmCU3dL`abN7bD*@~N?z)%9 zy4$Lat3Xz^*z}r8=CvBx|4X)Bi&}m9{aeZOIf>ubvT0rPVBLGIs#hv7VSRCMuTIo@ zH}2~bmU8~x)OP3fj};5Gf4(U)EnAT@nb*?F?f1Lg`ER#Mm4vFj71(gxi`_ox!`e23 z>k8|S*Y91YR{UjTtrCVb>q6wCRFCwt9Jy=a78|eH&j=evHEvcXngIZi>&k9 zxHUHvUg<9Wa?$;IT=t8?UxG>fHlI4~ZkL+iVtcJ>F3%0ogv`0zH?AIDef^c-Tg_gr zi|JP)=I4iUYIRcr61yW8IQ`}_O)z1PZp*eZhZJ@!cT*D{sW9#1*2 z@9DJYTj$kYnrE+Cxy)$)@4N4_UYqcI2Sv);ub{!-@AYrDUcYwj=Y{;qRW~M2$$#t3 z>%cWbRAcwUZ(KGZ(_Z~7t6?g8>z%s9X7@3_+Ppr?XDjC3&(a9o^>X3gb>Wvy{W{!c z>?mF<_w~kWg#U1C_07v@FOxg5ucylME)1J6 z^I2P)c0=%l^$elwm(MMWl4jY(X6niQ=2zUStNwaY-&WLQ$me-v&8lkuTK6mR8rPLA zamocQ%K2ZcBsaX@b~`U^+s(Ai|F3@t-@E=c$D}^nZ#VK7celm=W_fc#qa#6G;JUvd`qr!--zwsw)=VXaZl$Pq6yjS7!FIG9Co^ajv zk=Eihioe$#RI#1jbS`kMKiivMcduDq?x^zoI`d2MjH^Eue9bx&=)dCM`kxGwZatOz z7V+|ddltv#${QYg8}9aI6fy3W3cYGD)8&9%e8(?gorLTB^Q%Nk*Z05M%lLu$D*ILL ztNm-ZTZ~$MOmwKo`sm00X4eDdg!gxou7=+)F_{v+w~tH0k;l%~W<**!E`1)_;sj**2%=q-_sv4d0pm={ohvpzA?Atz&eKL!~Z9D zpYr%VuH)(Rzu7=pXA3Ky?{&I?KcLedDgrJ4L24)*YU&bi)%gcj*FCW`h5{$M-sR+nfB2Uhnxnr0&((>jp1U9=ETU9{Ti|nrfEVhU~VYuX@YXJA)6b z`>SHgb>nX9{>gkd%Rc1M}+6ZSR}glsh+B?|`ozqXQ#otUj)K zZPE1;4NFQ6to!VH-}}CK<-1ogmyT2k86}kWnn>wRSaMP1*SV|{;gOlryp4WA2j<*DKWlDY zp_7c_EB^@%vAPRprFhrRV7#*Y*TSkrzl6LyQ{TE zDZWAd{nIf2U7y~_ZC|<|ym4ZlYvK2e+V!sv7WJ>Zbmrs9>V;RE*WSYX9$KZZr;hE}ir{qF1DFEgy&oqv?Z zuU;I;bIWb=k4`3g^Twr3LEmFd*CpQXo6TV$>G9)&VNtKnqT1{qdrvt{iGLfe%>3o9$@4}jdyxsdIp1B8 zSb40lcUHl1&*P4bd(Sa{U3Yrf?7Xb+OWP#AdcFFQomOAI=dM6$Id{k7yh@M0v`age zb3T!|s{Qu**P^T?U3~{*iqBq6HEDmw>> zID_jH`72)dx%9qC@4v$Bw_gZOsq$~!`&ddn_V8q;TGK--=dEZAdjI;)6|<=O8H=J@ zL>K)N;k)tmc+lb6T-%PF|(F2+fLf$W^V!mvpr)4alGXe->`b3gYDW~>mv^@yX<^!e$6M(==WifsiM+7n*O_bj|3iA#`!{I zPL}*D#_K&V`rcWFUs%qz{z=Z~uP=VD7LWRQY0+^l)%3mBFXZ(y1my3mIufc@6*Kd- z<+XozpNlrEy69YaNubn}WyPFQ!}Sl|^Vk1qzWZ+a{y$42bGKg2xqI#JudFGTbGf;S zL`$>+b%CozTPd1yky!>EWp7(|C*S?n?{vBFo zwY@~g^ycgp?t%AxQy1L4=ek{eh2UXX%CG{F+W!`9^p;BuEBX%Y~k0cL+w&A zSG-?-TJqW7U*&=@)0e2l!7{Sky%z!wEG@cHe%auM3hNK6V_6zO6V|oK&b_WRIfXwn z$p3Xw+{R0e;k#U&x8HmrCg1z3!1irciT6Fe<@W>U&Oce2RU-2DUdE9=wJTRXRGON- zEI)4Qw-8j3K5U4!*Z5`V$rs2JF#lEOEAOfuDUqNE*I{LRH!u9J;x*UxRVThgiSJ6i z8@`M?lXq#bmDT?#Ws5sEUXa@OD%<~ZV1EzOL^lzetY?o(7R+0caD-Pm?8Bq9Bz>() z{@r>>r59(tIqGxSb2 z%g4iypOt;AGIhF?=%w4@OCQhM_dTMdZxy4W?fj<2fkIXB_XA7zUAx_X`SPwg;cH47 z{6rT06XCk?mECu*)5+((OedE8T2vMMzR6&VfV~jIqmIz{>PdY}{I-5;Vohh2@Ldgt zjK{_5DTP@%sB;C@bFsdWDq5E4_GR7nu)SfA_gpy@lg4dT_aZfXuP6Ua9p?^tPKLrG ztJa^r6gXks$tTyM^KCc3Skzr*Te~3i&foVZA0A(!#+$7Fd)orP%WL%7g|uZC2)C}V zZ)N&6H9XGp)`d6|u_7kTvtOd}C8Q1ofd(_85k_A8dyVT#cvR-nTPzV9WvgB+ygRRW zUR+82#&_k{7C$ffU-ru|HN*C<_iVeom#Mv`*EW2A`r6`E-Txp0wn4}E!m-+r%^_}%%-PDXRwIK3P+D76h19Lb;}nNp=G^5gYC!?^*oHhx>NxNp_c{tqGfA@w2sr$AM$gUrTP*`VP? z?XcLsseBhYovzHk!}$BO{{9@{>t_yY^uP1J{@MEOn)O_3u1BWJ{;ofpw%UD%#)WG= zi}Lq;Y>Qq0e8#TCPf_dLckOt-<<&*gsx=*bbDOHpN~}G;<4*bg+A@z>*Is`#Kk0r! z!RD&;)UB@qXFC1yVCYbWjSj9lz3~#GRba=j$<_({6D?*{zu9=)XMf$_Gtv2bL!-?r zK0LVf|LW&;>9?(3IbJWGHhH_2&W;C7-)7G5vs<6MJbi}R%J#U|l0xzOBEG7JSuu2V zhsL9YrND;mmaOM@t&iFJ0@T&po_=5%bJ@k@Jd?}6|Nr}LZ@s0qO)_o6;d^s~{Vb2l z?fz1b!}P5p>FWH5mkb(u7|wJ)5XX49oqzk@CtCTw2Ic2%zhAkREwX`M!r;KUAOFr- zzrUgQ^<+cr_XENkwui0xC9EyWa1=cL#u%_JqSO+ayg6>%o>O(@c)xu9bZ)&J2ke%I z2%p{g`|4%J-S0}Te=eTv{D||$ZGU{)?{o+%pUK^RH>}#W=!r-4yIC(|O?J*{J8f-# zw`6i{{B28a{XH9QmR$DDUN6o1=K1{keX;A~DyL|N{q3B}*YGIm>UEI+!q@DQfo2Vs zH@SX{yQ@yzum4~BJ8JWGf8(gs?Sf`LBY*q9CAslZW$T}B%D!Fy|M&f^ zUpe&gc-?{f$SV7dmly+LpyS_Nq0^I>GM6k8_}bneo6(5J`OLZktN$JGVs|LS$Vu`{ zWeZsGq$IvA4j55<(|m6JzMsc>B#qltlLI9~IgfAodFjoJU)SdAu)Kjf)3Nl!@$Gk( z-jsi*1Gcl}^V*+5Pv&H@_GNw(`do2r)#-gNcf5^|(m#7@!`%X(+4^VuR_6ab>2v^Q zVF-JD&FR>#D_g;1?uUL}tLZG1ae6c(P2aFTea)^nv-@u6-Pn+LSIVgB_adW@nup5{ z8|Lo$As2lA*W^}sc&y<2_hZ`TAO9Yn69SE+yIb6s(pNk;Gwi{PYw=NqHpy30@0ZkU zf8Dz($}IG3InTb-$Gc<2;6bv$%0m5=;G!7kH;3KVl&GXdg$7^Uv@&bo>1$m@ol}?P zTkX63)h$$a_Nr&rZ1*>PysO8}Xnlcc<17C)B>}Q?gr9N*u2cN>P&v#>$aw9lm034K zOz)>=yY4*Vb=h3@{@bd>tETyeewJ;{zxg9?IS8I+xh&J1IJ5@1(JxT$b=vFD8xBPh4{QfM%w!LE|1Ien~rH*&;3u~h%}c|QNX z|7RY=ZGTby`&96{Oh_;UrA2L&npAbE;IeS)G~by{&#bQ*eu?aodp_amBW;MW;Vv_u z`&=wm@b1~9>5MIK`fQ%JaNlU>t=@W@)lXgT*F2UtiBo2Vt!Y~_Zw|X{VA{N4EPyWb?g*xX`uPVw~7@HHm2mp|TJ*I{&S zX25fYX%eZyZCH~+<+)B9#+<27f9|V1WvY9`uI7lB`zaso^M$WEKYfpn13US&Ub6hc zPXe1_YK}sZM;Axwo=K-}q(|Wi374MknPron|9+-ddk^w*%)DD;+!PV}0^-?dK`kQINztWoFpC$5(`(zTdxRlVKqON6!H=c|8G%Z{Esik^S z%$Y5dZSI@Ko+{Ij3pe>mxFMEqlB> zR&Zfoc#!08pYA}j)D>C#l0WE5-+#Nw1(Xn_udOX#wOwCJc)n4Nxy}m0wc~!48jq~p8_>!e>_gjC_8B#)&o3WZ3Ijb+~S{D4}^W0UZ zo=#GKkC9MRwAc3A{d)Dg;n&?SS@~!CF7JN7FZfLifDtp(h zSoBilZ_V;mMLVCItj5R}C%sNj{k-M!yu59<-<28X+*ti=cK$x!{A&}=7g~XqLQnR$ zn;G}^X!?KGl&QYf+%w;AI&HD?Vx<658o&Rp-DrOLyw7J=F1O3OSO35JiyE(~Rovs_ z{pPFJZkq+F>36O9elcUd;?HTmyH~K0pQXTQe11V%CK^^zrS)-j9%p@IgEs{ z=~B_tIWcPU|A=hLi_CCY=wbSqIS`E#k#Jg`^so~=4mi4<{Y3;V(e*St~ z^;y4j-%n|;zp}0R>(%hLyROIA@7;C|y!?6H&Sz4e*Vq3&UGqElyWF+^f8W# zaryn)?XuryEGzj^>B}~w?#eyTJn8-F4+q)L-p=2jd#~#C@x&SOwO=OQeZTknz3hAS z|7&NvpZvA2_X)gEJL#oed2wFMuP;9zD%Bn0^fy_(rf93>?>B*IZ*KTsl0RQMr)trk zzvgM#duGj$8-41BT%gg$#Lg}UhinP+UA)$ z&HLu3Cwng*Jp^MXBFFJlHTolt+)9Qm-dXO)1uGS{P}o%Yc6OD%I52F)ibN#?K}>e zx_bs1wb*NZ^XKXKe@Xv-UEhCeLCwph)3?3f|MS`GZ?>jtYc`*=GE><9=hNx4J;vuU zq*+tW%rHzlYkGal@4eq{WuHA?|F8J-`TBpC_dVbDzC$p-)Ox;@?%_7ukAKsbZT-LO z{^wYyH;28*!_|}+p}dh zs89d-eE$Kn8?_&g_VaG+Jtyg_w=%i<-OlINuJy{<-U4kv-4eBCxRqk)fG)Jlyzlp8Ny4iAW`+NTKncgepGeONfXi+mQ;<)XVjlNo&V@^#| zfB#i4O+>)g>gAGWo6p;=UVHiIgvAMVk{eDRlg_`Rnh>tA_35H^nzj12uPJ+P{UJ{E8`rr`l-jZ_(@0Luy`11i%+1$@u zf42BJ-l_e5H+^2^GfCs?F~v6-^w-|2ln>jt0GGwanoSB=-XS^ed+`1RjK*f+)f zvPJPZ%jGw-R|)PnGalbrc3P=B=F8v8s?h1F zGgJS4{As2@7*Su>NTg*G=Hy; zyI=WS``Nwf|JQSOe!2AUe!ltkzveslRX=w7d~DYfql+%;Geg7oKKpHDzwMpn;)}tc zb%GZQ?^nOy`_}63wbgOibEOKeSlCo-sQR${-@ot4e{ZEPy=Ujl9hslB3a!jlE7^CK ze`(!;%l5BQ_22cop~c^({^}RWvqRE$PSN$h_d51W&DX2pX_GwFHrn1uKHj(WbzI%g zr?-CZa`0k*Gdpirrf$OZ3tuyr%{=z4$2w~Hy}a)d>;C=yJsY$q=G#8K2l=4gj_*%^ z_Klm}Oc9ozuru`#r}`Gg8E-Zm=1W^PGtCIJO{n%SXlGN~5&4n}j%I%qo!f3)fAzN9 z`N!SwZ$0~?!~8eB7fO=1^IlW3C1vT=$=zkVljoOQnw;zKC~f+>^0;Nb1_xQTHpJ@f z{SvhJ^Z!=yxD{)I7oRV=>>FPF_Q#w39X6#KWn0dahe_TqI<0$O{=3~a^}82!jW?gS ztDfGrpH)0&gZF`T_c@=gU(UEYzqbD4QU2Ymt0E6(Rqg!uOMEx$k*{K3b(S~3{Icow zx~wU;uH(doBXwl|3k~uz{q(;r!>tJf*!29$9-4-oeB31Q!)**>w6+{#A_!*H|N!xlWl}iT!=?LdMq>R#_=C>i_+GTiva@E5A-{^Eq*lt<1*A-_C4)eq^m6`t1xWCKx)X&@Z|D|1jX;ZlAn`BqM!Pi!+N^LunnY*#O zeEpw~&z9T&UFmz;mQUmLZ?j48evgW_Zuq&AuHV~9oM{dUIpP;7l{@Cxk)~8+M%CYwP)dn$yefB6r4!Od4^KSwqLG(jnLfYv8jrl)op;4&W*C0^ySUHl)V}?o-DKTmZ@u^Cy<>`-eC}m!?1t<^JKW`K zuiWc@^Jzhx`PV1bTzBfe@4kQYf422msW&Ui@0zMz`MvKfa;xn0)X#^w<5T>s-xhxU z_uJm=%#RP2eo{{-&WQ=&^*+^aecZ)g>VnRrOuOIuN~?2jMV_chpZZbQ-zL%hs78T* zRO-~wHym>A{?j8$uZCu?|7yMHkE(Rmm53GU9v+`A_D!0K$XTF(oAUFF@&CJi-t>yIyndwc^BLp*xt?*854;St-j#9fLosjh^$lfT=j3m>=w@ag zRpP(v+tU@#V~zLj{(hBv*1qMjzc0BISH;`@nWcH-=VST*8P#W}ZhKc6U-@+E*%g6{ z-&D`4@Ywr#R%z7Ux4Rx(-+XK7z3;hdh>mAi)Z=5dk+U82`qPfT{P}$T_RRbZ2idlk zg7(A2E}v%}_;}*HxCO<}mrjpcm8*XCTYLzlAj%6n#Ga6wOC#lKT6M(Z zp`$|mcE47At2j0 z7Qfq|u_#FAX1;34&E->f{x>?kG^g@Y+*ZR2E7Er;vD-d{`v)>?l$q5L-2K-sOwp!(g;<+01#q`#YkS|h0A z1dtve5vg)!>dij1-uu+&7GGU{{?=cgb4moakWib5AYXYPpk38ft%h zc=#>yl=hkpO?TrMe}BCmpFjWhs^=R|RX(5lz4pS6J)m2#ibcmk@Lf6E^Yqe=6!#k`izt6_D6GezkR#4^VO!l z-EtSf{o$X_W}n}8ZSmo<`?ckZKl}A3gAQeSTNqpQa_QU6yTAH;*s|uYxacmvBU9Ah zpSSxxPxrL;x{7Ny9}Z;8?tfAJ>h~+OLG*Qf?Z5u|>Ay2y*K=P$b2Fr5kawQ1dIfdJ zyYO6#73PpPw3XG%q|JJtTX)NZolhotXT82M+3x9-;If%LIcH6;uVH=T_y6DDY{uQ- zZhV_$nn%vZ=DTIPZ!bF=^v>w#6ZP9i_r}%#l|JLlzc=y!e7kq&p3TmW4`^8Y>}JU`{vcl+Z$mQPoH`)YJL zCaupRX-DO&mF2lN&8^<`Rb86Qxcke?%g6t`&97g*SI)L-i>-U!u9wS>gxP0o`>*75 zy>Z5;Cv)Y#J^O?-Vtvj0e$DN-wx+ITAO)qFa6{bhg2#Wim@Z=9ZprC23uYAS!>s%gG2YRq?8TzdlA^%m^+a?`&X z$^E9FA*G++Zp*jnWiI>hbXxSf(`H;Z%5NmL-^ylvbJ^d%HmO_Q#LHSAxwyLfX7l{q zZ#N#xS-b30KYVi5)LMV0vb%N@@z;Q_XXo!XOtzB={`Wm#`<(Ad*oBz5`;H=lK)!+MFcfbAbk}o(?>u1nubhW>~nU>!v zbpN$hFX`#>F35Bu&%V^fRf~Tuu9_kanH+F>ZvFaE#hV)&%XaK$`Y!Bmb1`S{*K5yy zJnnzLsaNjrvU{ITs?9#5v**L1v?nJfnw^MGndtgv@%|Lhfqu6#mz)0m_0>2$wlwtZ z=E%(F50QF2lS033zgM*yv?(M@9%cMpLMXA{r2_UZ#H#<4wuOBe!9T1`HfZY zf`aWH7LmX8S3!Em(<7(7)j0qCEb`=|-i_T)PI~WRjaQt$s4+$vG6%U^mZ_}#pNK7Z zM(>P?=_bRU5qGMRo-K#YokBeu%mVZ3?dOkvY`q?5>~Hh2<>!~n{+IJ3Uo~F;UG-(U z&G$RSD|PE$Z2bS{^ZDItK6~y~2hWZ|rc5H8rq@Qz{H+feFT~Q)n8y9BZJ)`Nf7Ur? zHsl?i^I7lKbnX{jY(>4$f-&WQ+@~Kub>2zr{=6JIxw&lTr&HRq-)_Hew|7_fjGrrK z!Kaj0+*|Qa#PI;M9eA#|P{s?<4h;VC8QnEYdn61Gopb*1?RNh1`>#NQn8xOJ3YsPD z>uRQd1#MQ4?G$uEn;E^tSbecUTT{}b?M{BsOz?bTU*Ngk7Q1>-+j&Ojt1y?lbu-@m z@F@BzX_hlX(m0LBJ1KDH5vwr8LLX^;yW=5WUB9YTYUj(rbBc}l@r5mT`VwK0bsrA0 zr>$PQ&1iaD)ykiL-`D3ao0X;Yo86%M*@}RF@Av)A`yKUJV$GIIUS)It$)h{ji9N>C zkm)3(OCY;=jx=ZOzZMReS9Eo3-0+tvc_2^Km%; z<@ruow$YE1PP{!-e5?NTYs=opDt*)c?em({dcdyX)>gC7@4HXTe!MDV@B1}dPwt9| zO#L2Q_BUcG`l?WUBu zTUQ28{hQ}H_jl0OkHM+2o1{8wkKJBz?Dmsy-@mF#oO<-5(1@Evex7NYB}}i z^Q!ZH{CRFa|I6e2|KIW_M}I!bebV*gx8CZvTf>W1{i(kH``(9E{hEcZnTq-Cek53Y zx!`=*>h&7ov@33|ot+<%}yUN1d`$Se|M~U7(u?T-;nhN~xd8E1TquI`n zX1g5x3uBIa3_QB>125BWhOFziLhiob)f~M!taQccsM%{yPYo@zPraVM?Bky6cUM*A z?OqLuyj9HABKy1Lej6qpYFU^0aeB<0viEzxtJ(X{Hq-rSIq#F)(bXs9ORof;+!Ds} zEx$%N@B5Sum;I)& zrs^1HURts)6}0b5<#LqZjr`qjXBONo?Qefy^L*NeZ`=JjUau`r`*H7kop@YrX<^k= zr&8b5Nz##9vg`xr{(hxju=nS)*@=ICe0(^s`rS)I+iukSv;VRBdWb(ZCth6Sn)v_k zcmBoh{d%>hwO+6k${hJ9aNJc!K2!+RC#}1XeByWYT&d=z?_sRp+GZN3x4p0XzWd?p z`2SU#eD-M7INOxk`DME%y#H76IQz&^@$($-Yo1p=%+3G3{=?<@_wMKRCL!Eg|GTR0 z-{*(_cG_mo46Sd}j(d@Qq(kswcKq+C{+E$klsvwL&px>7RNeMP=W1WR|8a8lsa+|i z0>6E%pH2xrbYi0NWPg#z^0A34I*-@d{CjNC@pvI0B++ci-XE=ql;VT87RPpd*}I(~ zIxN4@HZJF#_5GiIpZ0zYkE>KYGt*c-GR=VHTbru)wDw^Cz1nN`|NHS_W4|xo=Crjo z(hEFdFR!}cCbssL|CRgu()L*xb9zVj-8TC3yY#=@vUBsKrs=)^D`uG)$rQfl-NEej zd#BC2vp$UF;UnxN7yfS09U~Jy!3B2EF;^<^KHE?{*{?OqpvxSfQ#_07ewn}~L zAAf4yeV>|OyWdl9Zr#2n|KH~okeralxNXHfrr(k2b4AT=Bs8znK48vN{k?iKe@Q}q z#jlsw58bPdm%J8N9b@tR&Tp=(rfa)$Q!cLh>+<*Q>#axEt0unGRo|zYQ*~?E$^GUQ zh0P!Q!~X`)`19b&hhLAbUtV3Z(yE~JdY8h@_*}Nt;3C=G-}Y~_xNFPX2`Zs%*4EsNOk;Sl#^?X=_mis|2r__KYp+r|3tSI8WH zY$s^{iK^&URy`Fjc_D({>yc=aPN@x1pp9^rTM1fK0N zTYkJMk}2Hg+@slR-#ywW9AEudQg-|Oy6TD_58JEH^hXJ*Y`&6auxiu&$k#IKoeMsn zH80=G_e5~Nl%W0JUl-c#t|V6>QiySP#ls7W`|YeilX8iNdCh%(t;n2v;n7E#{?`>X z=6VxubG0&8o1~qYQKxTW7!mKo){GiNucmFauhraLXWVPAOT)VZ;spjesj5W;?DTLufh{=Y)DMp{dOBSyG(&Y(ZLNr zWS{76zhe|rb~Dv;!vAaA_f1_RZr>ZfqabmHeSO`Jf8Y1d_p$zXMELOQ_22(?=7d$s zzdtimzxDf`=XXS3*XYNTUX{PJ*;xD29T8A6i~P3r>CEVF9B*E3JI8;Wb8)}?+&{mW zpVukKl)o-K`TgE()^!zS#}wOTgsw#-_ZFNnJg##2k-Qh^fXgNJANg2zzu#AVqvrG3 z$@BGhJZM^1x_FJ_rXHQGl0B>5l-HN`)%$dAul)1Uzkb%9w47}>(;l8s?)Q1N_t&e{ zC%;r!y=r8ao8i-4zaaCD=c6T){k)bPeVv%I@u*nh;$AbaJVnX)@I`6I{RKm>sjhCk zkuJzBrW5g0*xNUIyIB9@#vm>5Zn(d3XO_ms`LH=5=Sms-($lx*?*mn?@A9v_%1OQ7 z@^u-cQvJ+!HNO+a=GyW3lmh8{?VZ_P(#x|Jm%nXVM?N-EV~M zec$)JG+*ALI^mo@8-@U)$D7Wv@{nc^Hj=uf+Rc!+oB*(_7 z9N4y5segOmb@{(9%oER=p66hfuW>GF++_pHfK~CEabSeEnUc?YxzOzd>gXjL^Jud$M;KD{w<{^{?W%Kfr8P17#tetjI~|8M{6 zpS#r@_bPfn==y%-t=tX${eM0^*;vm1=A+2#inlHAUe)n-Mt!vjSqqBZ;yuwvcD-JA zJ2^^Z|0{v-|DW6cxBMBIkpI@M@tU^ijl~<2kE`A1%>J{~Sa;s6DgQ)*Uf)dWz18`A z%l~WFi=~cx%IFu~+2V9F{u!v@byIrlrdM~$LegVjg)MpS%9LZR{i)(|Y3;L_=7-wt zYcg-teC~YM$R6jRdw2I2vsXdS>*m#ceQEIVUFXC1`oDi~_{M)N&XRBc)~Fx5KmXV3 z`TzIFhvn)2x15?Q?YH;ul?}f>YCBhc7u&yE?*IPdN2|YZyyA^yntmW|uh#FoYtDi8 zKo=EW`|{oGarvdk{~nj$P;T72^*2k@`jUixeWAr}y`|Zee&PP_`|r)}*<3#F z>$!p(`Fz~lZ3?~n{V!~O^ZMCMbM@OdlJ1DjY^rjec0hE0-IX1yDoQT8imU#8SyyC%5Pv_m_i|r|K7~rm}pike*xNb!px1cTvA< zmoAh&S}1tjT}D0hPMOx~P5aNBUU%PTvw2qP)tzrwK#I?9mK$YZPd6RW2ua)c<<9{P5iJdy;lP4)UvA zPP&l2$L{)HDWTMrXA?qF_y1m9yhp3%k8?TCosVxiDx$Z&n?J`rJifNH=+z(VdmsBY zd_HHbe!lcg{GO^m=j;D{PMPUBSxt3&PtWNiPZuMq6i5(cTw4=)a_ais^QW<_5dk%~ z3+HSJf@ZJ%=2@v%;GUOeEPHiD{qUQcpRM*CJsw$_(Elj&FM8Pl0S+~IB);I=KA@wee;z-U1H{q{tLcp zalg5^CG+y6%cqhoGXGp^UZ~U8Y$5k(N5|vESD|&m-@Sp|`D-54Zts4Pe>)YLF_ky) zZw&+aQS^!V%_sX0soQ=0{Nb3iZGQOFQt8yxyXU|5$I5@(lX5;qX7B4Y<*ml=ER^@` zF{t)kwC1wE8cyX?N*&`w;vwr*04>%XJo z|BG1P*xc4we6vnWKUzj_&-X(c-u7?zeOLXDfBJvtA8#Mq>i?SRzPWa4Xz7Dj>)u_T zu;SdUD=Q&Anw7tPtlXp*$$P}E@!D#e4elSe-2V}K_s3r4@cwj-E=ZRq<7?)JyHo$B z#+Cg|`>ztn}5DCrp8^U!cyrwex**4%En>lv%kwFeLwX z3k78jT_gxNN z=ELbd?TgJ{ek)I#eUA&}`oCA)IsSP6RPhzWQ}a zyZeydZXd6ASLOWo>u>5++V%eV*Q4v(Coh{Xa0 z$p?c*eRjn=FTxo1F%Wg z4CzYMTG_o(Kc0o&=aHVfC$QqRXFl)odqp`FsSmYZx_WCzP1jk)7jyUOUn!xhk+ZEJ zJwE+Vd3$)Fx0U@{TSe;mkB^VH-z~py+aqbLR@r!Qi(kgaioWBeHv4|>YI^H{DC*0u z$CkG?^V?bE?Em}izdy`@J2aw{I-EV794i;UDS!KineTeznQU|8_22wl!M%O}VoMB-5BwFE*?F_2t9A@B8id ze4hK>r>;-@!|q32+LQKyjvarvX7f3(SMu*qS|=UpFi1Z)NA9&b+`X|0@8c)#D%+lO zb9UnW+WKut(&{bU`seDqH~-sX@V~X-h_AcK^1b;bkK7Ws&JE-KZda0Dwei%{wcf@r z4~XWQPPg-3l+k@#t4QWaV0!MWkktLV_iqdN`6o5A_jyDDJKJtX?l%s9VFga>s|xEq zZVCO5Y)g~l`>bBASeFUvof+xYnJ(P@Z(D2_q}j&*WcI7D?TZ}OdcJOvSzlWm!DkA`EFYh|8ieisB7%6OBQOCk=ZhhoW6mw}4)DpYd87Y%(`)m~@hke~&nxGDb8$oB z;Yq=dq|?sKnE2)Wt?c!0w}t$_t+yId3%*|WJR#`IMzNLIQOVNr|2~B#etB{Eu<3Q1 zS#c_x)t}o+{(HLrzv|9M)q6huo>@LmQcO2`%9+)R)<<65xjw9Qug!5sxzcOP{6fF& z4n3bZE$mz6I#|ggd}CoVtbpNmW2(08`m*<36`z``eD=g$%S!Jb`@Zk{?hU_QEpOFc zccMIdt9yKvNlogh@IxyeySJY8yR9?l>+j9GnyQ>dYtODaSG)C9Qb(np-3)hVF?_Xn z#?q;^^H#6%4g=Q>EM+eepzY;5Cd?e4m|&u7giANY4z{@;e|DCG_D#b-@pFE2vG`}NrK z2ba7^)(yJ8<=(cBhvn}{y7gF?#?*Z5$@Bhq^zQjPvfpO!iI*rlvth&A{_VWe@771| zpD%K5mZ|nle;NDI*KZ9s27x<-vD+J0ct0*rkh$#eTnH4lwakHKf752MF0N?Y*QNpD*_wka+b@N-k zf!aVnD?e^J>S^%*gTLLJFU8O2zmuu^X}|x?MnB_^W}S!I`Qx7^^_ISPZFq6t*39d5 z*K-wK2^}wxIbG<(Jl8v+oiFSguM4PAEY`Db&!wCo=*oh_wcqbv zPmbDCUhTX&H+&&R>f4+$NTx%<7yLjygpxV`R=oX zahlHzi^8O;qNlTLmn~cun|*oR-*vYguBi4sw3ya-PHlrTyY_FkMQ?tc72$P@M6`C0 z5_$gZh#e(~JCcrxJ`A2-uhnzo^W(&=57k@G%M>P6)cyN>_u85K?RU$#DTRtCV!siqkr%4#^dYu zJiYzo>vk*9-Phmg_tfyTXgl3^=~q?+o^+LN{3dujM`m|q zymrGkP||v>@A73iX4+cwT)E#yNN@L>O$B#LueY{xi^Y#CV zKNauEb`Y^owfgm9@#N#6k@%Fjo9|P^YuEJdx@Z0PUcvu6zYm)lujf?1Usm+v&eZwb zx94t-jrhCc>g3!$%Uquu-_w7$1?StF?rg8hzkTBKotpR6`?*)IuZ!C8YL)w?%|@1g zJ{&%*HakZsuHs?q7o7xwu4L1`n-*n_3mrjOQ-z@hnNTgVp$Zu{_D`~T)UeCfJn z@iP_h@aBfceb#N;_dL}-dA|Hkq5F?l;rnOld|s+wt0_}_#&BtKdjI23A6Guy_xv5V z`T2c2SO2X!cK&ed_c)1T|LbZjo=r-Qd7Hakx$uJ-HIC(l9Gf4L|0^I5;yuLw(QR zN&I`aX}|ZnXa9QRYn0b)dz|%f`Sw31Kl6Ug*8X>T^@-xkAI|3c@n)|nbldUqnEIv7 zzf7~Qt$C7l8PrF=yt%@2x*Iez zb~9I|=tOVg+O5Z0qw{O6Y*)u$UgjGu_~W_<)4@kayDx8UWE44`A~UXdeon|N6c6`@Pv;9=_Rp{+;39mCNUOE&Km^|G(@{pB>o0 z+4OJqh`oF&FZt&u^TUno|E$*RemC>s_x<&1w!7mEIdb-VJa$rZd+Pouh7jexqZVq9 zZ-`t=%mCM3ybP(=--o=7nXXi;FO~HB630zvt7Ag|tX< zn?ARg4#(?tyYoN|+$x{?N1f`E_8slM;QH~@`;X6$g+5#?apQV?{oi%-r5pb)o1OP+ zR+t;4tq_`{9JMK>^2ZbP^<2{Vf8rC5^@v~I{99B%O?173Y`(PU`>!^sr>0yAzt6s@ zGg-H9r^UBN5_e&6qRC*R&$RPihQ z&FiJp<7WM-`Tcf#JAd7W=ENf%f+wYK?6}h-X*?-7l}IGPRBJ$;bP6mEHS%^h!!!ENoXhf05Vx&V)1B zcgp-PZ+WEKx71?UqZ?hxI(=I$o;|v;`q5T~jCy#?&s{b5bE=l|NoZ% ze_DnVYdR;B3e7kU5ws3u;I)#sm`Z)a%p@yIFd_u_`-@9r4X z{rT}^C#Y7xL(+bG7kBY3Q9AB&QQ&r8m)$j`Qqa6nGA#P9O3f4H>22os z3K%UJGQ=SsOL%!{>FSzYYyK`h`fJNosd*@JP>WBWnhpERdtZSQ&1)PfT8v(UI#8X`+X2>|K+$ z^XbYB&t|2!)c?JHe_1*sLvshn6;l}-z6Cs)@j@&-~a3C z8gQdtxqr^Id8`aPLSVz$8QAtK9e4SDz-~&^a`tZ-mzK02{_t>loAJ3F*0!s|-aTDc z>Qwx{WPvECVZ$uQAaSQlMwqGkf7cbzJfetU-5>eM7wSG9HNLc2%eegA9ns&C@@4mH zzZ;~Tnen7BD@uJ!h0XEDR&w`0TClH}b%0McX8{vKbNIyMq?ezTuAXCMClXb2CsGu= zY(PV6o|t{nlgyKupF+F(Q{UX!sJ+eBC}+z>w}%&;`FVxad^pVS6gV5{gZrf3-N*B7 z`W{2($BsX?k$cZ<*w%PXb%Qgz^a+Lo51!0e;VxRbHc2{Frse}LxX{hK{A@}3({I0C zuWvuhZ*S8hWvT@_IVJJb6wSnwlT=?GU66d_v9;Xuj}`rp!59V&?}T4ne*`pn@93S_+`Te%s7hD|Hez%=JZ*Jt9|7N#ygh8Xb zpw{-Xbm!yU5CbJveB>zs)h#y`xpp4{W!JCQ`V%{CZd9&~L`9sTQ|EqAlFk||>yX!MAFKg{Jzh?p3D*CWhJkH~Mo1!vgY)jxc zt3x1@bqVMqIGs7g=PX~IYc2loB_Cf-{4S!`+gdZl^ar1f`+9_G9dEF)f6V__?&;grzM zXI}W|$cBHvUbnMyi!DhvWHD`edSzvB_|8`w!}c7ln#TDCbnn}Zq;B0qoa%EX%ri}R zr1`P_Yq&k%?zi7IKMDD{?+ADH(f6;8U9Nw8y`W(pXnaw*Pgr~r^CzJj3lE>2ZLYs& z=d)Q4Cwc36o=@V)VkuiYFU(}a{leq2Cx3x1vZ}h)@ayvYXKi1v%gcYeU+)#M@6XvU zZ(R0BmF+n&?fn|(`;cLH@dg`~G6|+fnjfQcw@&rH$q8=uUQS%QOYC*@5xd`Sgv;(# zKL7H>@!_-C^SMBc35zcmq%UutWFK%OI%21k%@=*pvY@ViiTo0?z$V3idptGkZ>l`o ze9+mlC$B!t-~ad9mo<)$SFM)gRCb#q)n~gcUH&*|fUDeU-X;I)3ee!Rt9a~{ogkE@68i`$gqS!Kw!@yFNhca65c4vFtm*s$FL zG^uptBlCpT1ANXNJRHB@ZjUdj`V(7zcj`ReEgmx6?D91QpV+s6mf+h~2mIb%zgu(7 z&S#Q|zu$i61^4 z!(jp8eErS;|NU;ixY%8Ndku4x_B^)z*W;>NORvY8|NK|AyEDA||DES`l5PCl7m zCTpI+<#UR>KH2X2(fK=J_f(ORRT3BOe_dCu`f$ZvyU6$NzFrq^Nj$ty%IsD~OvS5} zQxzvwfp#`L44(fh z+nHkhTls4qFrVBK9#^?EyPE0uf!z<=q(P;|WcvloU)&Sg6?OJ)yj^%)c5?Sc&`E0# zLBk9BOd$&Q(rxR?E!vi!umATs_w(GZ`_9ZV%|4neDSPkpyyC;Z@0B0ya?q%_qIyru zAkM@@&N}|*sqjO^=WVCQ zfsSySH2=Kq_d8FX|5#b|_|!*#n~yHntLFcGlYSVq7H{4B#m#(H8hie}&3|v$`|FEo z9YYn^MJC)b?y{z~JyNArKi=kd-&LP8?~yF)BlQOx@BRNd|G(t3b8~06d_ES-{%Gn0 z>G*>;Kszs~%& zBKQ8jD_1Y)o@tco6|twHa20#V+M7QZJ%n#CHpKkmmWluOsWkEL*W>NK-`6YJcF%cs zcJ}qhzc@aA`3AbE@_M$KPRtI2KX20Qy$}8P^z`)MGc%13+kU@udFxus;%8qX^x3|p z&#PQ^+@x`D-QQmMhIcGw3<}f#F&)3O^z`9*)&Fev{Cz8Y=(7KMn^ftDmHR(3UzmG< zkKxHA)q*|WZavy?m`}VF+zkA+%0G9Z;^`lZ83G`OZDQ+7pI51zv+?MW4aeoeTU5QL zEl)o@VM^&A1{Wr4h6&#D4j3jM>rQOuJ;%i^_v7^!9f?VwdgK`zWf@MW=0Di+blUPm zFD@=`SML9F+V)W5ic{8g4GVoiv8gF!p|}6fLxY+hD<7)c@3gn&ZkwX?m|bEQ*x`#B zA3vG=U%;);MmMJX?&Ft{1}P%4_6IJgfYQzsPMP@szw!_5dOc6_-Qs+>!V5u=pH8v$ncs^Lvis23eR$jL zIDuzpqo>T{m0kPh2Vi@qNO?(;jJtO_ailjdb&h4OJ z3krSE@javdbo8+d1=CWtYGD$XsFuwz%&=;g!J4ho!RT2rl!TJ$W9h z?9#rE%q4nY$#n-lw(9@U%GvSo=EIBb@t)_K`U0>2V6>VHc2W3)h%FhBKVG=&^Mktv zACI!Wy!r5$^n4HhrftCpb?FZ(UM_8YIQPAc{+nH|pL#arTRfX_d(!)z|3CJhFO8PE zBO23EB$@>4!6&jx)$;H zOTmN2@b#N&thYxjE(`i%DXXP>`twY98o74hV#K`_ncc5F-L?D6rS?e+=KV|ATiyLR z?fksyaa-4ltUQ}=Vcp+#(erjai4=1H#q>!ji%ojh1@do;t!J6f_u}QYce~Qt7WePd z-CGrXGJ5vGMX|qQx7(DS+^o|8ila%U1u3!XcF)eQ)B5xN|KG{W=5JWP_1caNKc5L7 z{`53|S$Mv*YnHLZmhAnnr!Cj3oW=^vf@@AM)s6m^X7T%tee2fia^h}1FKl=4OP6JR z>a5wV6*ar@8a8)LpB@(_!DscN?aAeDUsisS&Z{W1c+e2PEId;>HQP7*I2 z&R_dt(v#q?ZVCO0bzfc>AG)*i^JMM|S1(S}i=Fq*D|OW>-_={OxG~kV^3$0OACLXN z`t|R(i;MmF@BjbzWt~8L>F@dfD=qJ~o(hSb9R?a;*Y%J6q{FQM%E~FcGLh3yCm(ED z`mjTJS?ksC_{#oox!Wx5fBwI+(tq72&dQ5o+f_GwUxzIuJJjb@nEv^D|GyIFwb1t0 zJJzn+J~J#m_th0Fp%H#eb^4hVHZk09YO}A`UDri(g7)IRg4p9`m!kq_TVV_Tse$f0 z)+Nu*T)V_sedF0JVON&%y}k8**VC2W^HLeXMcJB0<<~{2R|MC+zL6HC zofX_(_O|@)rz@)GH!(mn*~HSFMzdO9i^ta(>O^nTvE3Pec3b3BoJM^ST?eWiH-PFF z&@iU$vh<7B%~oIOx^-nCwqWYncc}DFif!9h(56X(imeWn%HH92nI-{1TH zZRgbg`}y!z_Ilp@>f5&usoVcNxvq5ay={?OafYD@_jP5ro)=tiE*=$+pY!E8XzBN? zGSK}13-|6$yS>H>UqoG8?EanO&15xSuV0Y$a@LPJln+_GUUT_t@s*s}p_w=XZ*#$1 zzBkk3>waoA{ym}GFLI+8dmu*c5WBJZI<{o&wz&gmH*iO#+nI77)HvO>Y;MJ)&XZdzPH%4m&6M-V+u49_CW^iM>sH!SoQeKo^bXK6;KM&Y zKAwE}VZZIS8=y7DE!OY%82@>|Ztt;dZrQEO#A7{@4`*es^Q`OB)%$w zs=w{=in=rHz`fmAbF)sMjTj!V6#um8kZ8LFGNTMt*FZ(9VNoI`Wqoq#Lr zz>}0n1=yKcrrMk`#%X6f&cAXrco_|yRkbPYe5|G?4e50-C`@IeHj@mrI_~W%F4uVZ*E3y7t5Yx`hZt$ z-sOAI*Pa{qO}EH<^n>w)6R2LDZJr+&QGC|)umIHhYQ#R$?+_d3--EY<9zweb^k9~R5Amu1z zUr6B`h9{~Um>HJpMs3k}v;Y6U#4j%{=0&n5K3&WI7`zXLzd?zmjA4Nhs5=arsQlFV zNV(r;(mY;SeYx)+fAl|A=woj%0x1bX+5_RtZ);-n<3aOF*9DJ{fCleA?&v>WFT>7| z%K3)DL08Rxp3JtJX|p%n%39qD>t#MX#I5hMZicLVtIz$zQI~HmJ^I*O?l^Q!4Rm=D za|o!bl~wwF@Ao#8)$~oh%SxTPj~Cmld;C$hzqnx)$R2MUwJZ~cc-L){Vi*OZAut*OgE#~#|J1)#+^4!^Vw(rZm7cDCF6*2U 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 26/43] 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 27/43] 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 From 1dfeef767e384316c6de5854f0015c57f7ab7f9b Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Tue, 22 Jul 2025 16:47:46 +0200 Subject: [PATCH 28/43] start writing tutorials --- tutorial/00_metamodeling.py | 152 ++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 tutorial/00_metamodeling.py diff --git a/tutorial/00_metamodeling.py b/tutorial/00_metamodeling.py new file mode 100644 index 0000000..c1664d4 --- /dev/null +++ b/tutorial/00_metamodeling.py @@ -0,0 +1,152 @@ +# Before we can create a model in muMLE, we have to create a meta-model. + +# Here's an example of a (silly) meta-model. +# We use a textual concrete syntax: + +mm_cs = """ + # A class named 'A': + A:Class + + # A class named 'B': + B:Class + + # An association from 'A' to 'B': + a2b:Association (A -> B) { + # Every 'A' must be associated with at least one 'B' + target_lower_cardinality = 1; + } +""" + +# Now, we create a model that is an instance of our meta-model: + +m_cs = """ + myA:A + + myB:B + + myLnk:a2b (myA -> myB) +""" + +# So far we've only created text strings. To parse the models, we first create our 'state', which is a mutable graph that will contain our models and meta-models: + + +from state.devstate import DevState + +state = DevState() + + +# Next, we must load the Simple Class Diagrams (SCD) meta-meta-model into our 'state'. The SCD meta-meta-model is a meta-model for our meta-model, and it is also a meta-model for itself. + +# The meta-meta-model is not specified in textual syntax because it is typed by itself, and the parser cannot resolve circular dependencies. Therefore, we load the meta-meta-model by mutating the 'state' directly at a very low level: + +from bootstrap.scd import bootstrap_scd + +print("Loading meta-meta-model...") +mmm = bootstrap_scd(state) +print("OK") + +# Now that the meta-meta-model has been loaded, we can parse our meta-model: + +from concrete_syntax.textual_od import parser + +print() +print("Parsing 'woods' meta-model...") +mm = parser.parse_od( + state, + m_text=mm_cs, # the string of text to parse + mm=mmm, # the meta-model of class diagrams (= our meta-meta-model) +) +print("OK") + + +# And we can parse our model, the same way: + +print() +print("Parsing 'woods' model...") +m = parser.parse_od( + state, + m_text=m_cs, + mm=mm, # this time, the meta-model is the previous model we parsed +) +print("OK") + + +# Now we can do a conformance check: + +from framework.conformance import Conformance, render_conformance_check_result + +print() +print("Is our model a valid instance of our meta model?") +conf = Conformance(state, m, mm) +print(render_conformance_check_result(conf.check_nominal())) + +# Looks like it is OK! + + +# We can also check if our meta-model is a valid class diagram: + +print() +print("Is our meta-model a valid class diagram?") +conf = Conformance(state, mm, mmm) +print(render_conformance_check_result(conf.check_nominal())) + +# Also good. + + +# Finally, we can even check if the meta-meta-model is a valid instance of itself (it should be): + +print() +print("Is our meta-model a valid class diagram?") +conf = Conformance(state, mmm, mmm) +print(render_conformance_check_result(conf.check_nominal())) + +# All good! + + +# Now let's make things a bit more interesting and introduce non-conformance: + +m2_cs = """ + myA:A + myA2:A + + myB:B + + myLnk:a2b (myA -> myB) +""" + +# Parse it: + +m2 = parser.parse_od( + state, + m_text=m2_cs, + mm=mm, +) + +# The above model is non-conformant because 'myA2' should have at least one outgoing link of type 'a2b', but it doesn't. + +print() +print("Is model 'm2' a valid instance of our meta-model? (it should not be)") +conf = Conformance(state, m2, mm) +print(render_conformance_check_result(conf.check_nominal())) + +# It should be non-conformant. + + +# Finally, let's render everything as PlantUML: + +from concrete_syntax.plantuml import renderer as plantuml +from concrete_syntax.plantuml.make_url import make_url + +uml = ("" + + plantuml.render_package("Meta-model", plantuml.render_class_diagram(state, mm)) + + plantuml.render_package("Model", plantuml.render_object_diagram(state, m, mm)) + + plantuml.render_trace_conformance(state, m, mm) + # + plantuml.render_package("Meta-meta-model", plantuml.render_class_diagram(state, mmm)) + # + plantuml.render_trace_conformance(state, mm, mmm) +) + +print() +print("PlantUML output:", make_url(uml)) + + +# On to the next tutorial... From 069cb439cb93addc01220059c0eb92411b5ebf4b Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Tue, 22 Jul 2025 17:15:00 +0200 Subject: [PATCH 29/43] add two more tutorials --- tutorial/01_constraints.py | 92 ++++++++++++++++++++++++++++++++++++++ tutorial/02_inheritance.py | 64 ++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 tutorial/01_constraints.py create mode 100644 tutorial/02_inheritance.py diff --git a/tutorial/01_constraints.py b/tutorial/01_constraints.py new file mode 100644 index 0000000..fd0a193 --- /dev/null +++ b/tutorial/01_constraints.py @@ -0,0 +1,92 @@ +# We now make our meta-model more interesting by adding a 'price' attribute to B, and constraints to it. + +mm_cs = """ + # class named 'A': + A:Class + + # class named 'B': + B:Class { + constraint = ``` + # Price must be less than 100 + get_value(get_slot(this, "price")) < 100 + ```; + } + + # 'B' has an attribute 'price': + B_price:AttributeLink (B -> Integer) { + name = "price"; + optional = False; + } + + # An association from 'A' to 'B': + a2b:Association (A -> B) { + # Every 'A' must be associated with at least one 'B' + target_lower_cardinality = 1; + } + + totalPriceLessThan500:GlobalConstraint { + constraint = ``` + total_price = 0; + for b_name, b_id in get_all_instances("B"): + total_price += get_value(get_slot(b_id, "price")) + total_price < 500 + ```; + } +""" + +#### +# Note: The name 'B_price' follows a fixed format: _. +# This format must be followed! +#### + +# We update our model to include a price: + +m_cs = """ + myA:A + + myB:B { + price = 1000; + } + + myLnk:a2b (myA -> myB) +""" + + +# And do a conformance check: + +from state.devstate import DevState +from bootstrap.scd import bootstrap_scd +from concrete_syntax.textual_od import parser +from framework.conformance import Conformance, render_conformance_check_result + +state = DevState() +print("Loading meta-meta-model...") +mmm = bootstrap_scd(state) +print("OK") + +print() +print("Parsing meta-model...") +mm = parser.parse_od( + state, + m_text=mm_cs, # the string of text to parse + mm=mmm, # the meta-model of class diagrams (= our meta-meta-model) +) +print("OK") + +print() +print("Parsing model...") +m = parser.parse_od( + state, + m_text=m_cs, + mm=mm, # this time, the meta-model is the previous model we parsed +) +print("OK") + +print() +print("Is our model a valid instance of our meta model?") +conf = Conformance(state, m, mm) +print(render_conformance_check_result(conf.check_nominal())) + +# Can you fix the constraint violation? + + diff --git a/tutorial/02_inheritance.py b/tutorial/02_inheritance.py new file mode 100644 index 0000000..a8f6062 --- /dev/null +++ b/tutorial/02_inheritance.py @@ -0,0 +1,64 @@ + +# The following meta-model has an inheritance relation: + +mm_cs = """ + MyAbstractClass:Class { + abstract = True; + } + + MyConcreteClass:Class + + :Inheritance (MyConcreteClass -> MyAbstractClass) + + Z:Class + + myZ:Association (MyAbstractClass -> Z) { + target_lower_cardinality = 1; + } + +""" + +# Note that we didn't give our inheritance link a name. A unique name will be auto-generated by the parser. + + +# A (non-conforming) instance: + +m_nonconform_cs = """ + cc:MyConcreteClass + z:Z +""" + + +# Check conformance: + +from state.devstate import DevState +from bootstrap.scd import bootstrap_scd +# from concrete_syntax.textual_od import parser +# from framework.conformance import Conformance, render_conformance_check_result +from util import loader + +state = DevState() +mmm = bootstrap_scd(state) + +mm = loader.parse_and_check(state, mm_cs, mmm, "mm") + +print("should be non-conform:") +m_nonconform = loader.parse_and_check(state, m_nonconform_cs, mm, "m_nonconform") + + +# The reason for the non-conformance is that all cardinalities and constraints are inherited. Therefore 'MyConcreteClass' must have at least one outgoing 'myZ' link as well. + +# We fix the non-conformance by adding this link: + +m_conform_cs = m_nonconform_cs + """ + :myZ (cc -> z) +""" + +# Now everything will be fine + +print("should be conform:") +m_conform = loader.parse_and_check(state, m_conform_cs, mm, "m_conform") +print("OK") + + +# On to the next tutorial... \ No newline at end of file From 35f74aed847b9c25cce05f7d8b8ee06519ba332c Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Tue, 22 Jul 2025 17:16:11 +0200 Subject: [PATCH 30/43] get rid of unnecessary link --- examples/conformance/woods.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/conformance/woods.py b/examples/conformance/woods.py index 7475ac9..591eb91 100644 --- a/examples/conformance/woods.py +++ b/examples/conformance/woods.py @@ -198,7 +198,3 @@ if yes_no("Print PlantUML?"): print("==================================") print(make_url(uml)) print("==================================") - print("Go to either:") - print(" ▸ https://www.plantuml.com/plantuml/uml") - print(" ▸ https://mstro.duckdns.org/plantuml/uml") - print("and paste the above string.") From 66b9a2dc336681026935c477dbab790a4305e347 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 10:04:08 +0200 Subject: [PATCH 31/43] add tutorial --- tutorial/03_api.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tutorial/03_api.py diff --git a/tutorial/03_api.py b/tutorial/03_api.py new file mode 100644 index 0000000..f30ce6d --- /dev/null +++ b/tutorial/03_api.py @@ -0,0 +1,72 @@ +# We reuse our (meta-)model from the previous tutorial. For this tutorial, it doesn't really matter what the models look like. + +mm_cs = """ + MyAbstractClass:Class { + abstract = True; + } + + MyConcreteClass:Class + + :Inheritance (MyConcreteClass -> MyAbstractClass) + + Z:Class + + myZ:Association (MyAbstractClass -> Z) { + target_lower_cardinality = 1; + } + +""" + +m_cs = """ + cc:MyConcreteClass + z:Z + :myZ (cc -> z) +""" + + +# We parse everything: + +from state.devstate import DevState +from bootstrap.scd import bootstrap_scd +from util import loader + +state = DevState() +mmm = bootstrap_scd(state) +mm = loader.parse_and_check(state, mm_cs, mmm, "mm") +m = loader.parse_and_check(state, m_cs, mm, "m") + + +# We can query the model via an API called ODAPI (Object Diagram API): + +from api.od import ODAPI + +odapi = ODAPI(state, m, mm) + +ls = odapi.get_all_instances("MyAbstractClass", include_subtypes=True) + +print("result of get_all_instances:") +print(ls) + +# Observing the output above, we see that we got a list of tuples (object_name, UUID). +# We can also modify the model via the same API: + +(cc_name, cc_id) = ls[0] +z2 = odapi.create_object("z2", "Z") +odapi.create_link("lnk", "myZ", cc_id, z2) + +# And we can observe the modified model: + +from concrete_syntax.textual_od.renderer import render_od +from concrete_syntax.common import indent + +print() +print("the modified model:") +print(indent(render_od(state, m, mm, hide_names=False), 2)) + +# BTW, notice that the anonymous link of type 'myZ' from the original model was automatically given a unique name (starting with two underscores). + +# The full ODAPI is documented on page 6 of this PDF: +# http://msdl.uantwerpen.be/people/hv/teaching/MSBDesign/202425/assignments/assignment6.pdf + + +# On to the next tutorial... From 33a70c9a88ee0225e5073a89f0e6fefa3005854a Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 11:03:48 +0200 Subject: [PATCH 32/43] add model transformation tutorial --- tutorial/04_transformation.py | 160 ++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tutorial/04_transformation.py diff --git a/tutorial/04_transformation.py b/tutorial/04_transformation.py new file mode 100644 index 0000000..9343969 --- /dev/null +++ b/tutorial/04_transformation.py @@ -0,0 +1,160 @@ +# We now get to the interesting part: model transformation. + +# We start with a meta-model and a model, and parse them: + +from state.devstate import DevState +from bootstrap.scd import bootstrap_scd +from util import loader +from concrete_syntax.textual_od.renderer import render_od +from concrete_syntax.common import indent +from concrete_syntax.plantuml import renderer as plantuml +from concrete_syntax.plantuml.make_url import make_url as make_plantuml_url + +mm_cs = """ + Bear:Class + Animal:Class { + abstract = True; + } + Man:Class { + lower_cardinality = 1; + upper_cardinality = 2; + } + Man_weight:AttributeLink (Man -> Integer) { + name = "weight"; + optional = False; + } + afraidOf:Association (Man -> Animal) { + # Every Man afraid of at least one Animal + target_lower_cardinality = 1; + } + :Inheritance (Man -> Animal) + :Inheritance (Bear -> Animal) +""" + +m_cs = """ + george:Man { + weight = 80; + } + mrBrown:Bear + teddy:Bear + :afraidOf (george -> mrBrown) + :afraidOf (george -> teddy) +""" + +state = DevState() +mmm = bootstrap_scd(state) +mm = loader.parse_and_check(state, mm_cs, mmm, "mm") +m = loader.parse_and_check(state, m_cs, mm, "m") + + +# We will perform a simple model transformation, where we specify a Left Hand Side (LHS) and Right Hand Side (RHS) pattern. As we will see, both the LHS- and RHS-patterns are models too, and thus we need a meta-model for them. This meta-model can be auto-generated as follows: + +from transformation.ramify import ramify + +ramified_mm = ramify(state, mm) + +# Let's see what it looks like: + +print("RAMified meta-model:") +print(indent(render_od(state, ramified_mm, mmm), 2)) + +# We now specify our patterns. +# We create a rule that looks for a Man with weight > 60, who is afraid of an animal: + +lhs_cs = """ + # object to match + man:RAM_Man { + # match only men heavy enough + RAM_weight = `get_value(this) > 60`; + } + + scaryAnimal:RAM_Animal + manAfraidOfAnimal:RAM_afraidOf (man -> scaryAnimal) +""" + +lhs = loader.parse_and_check(state, lhs_cs, ramified_mm, "lhs") + +# As you can see, in our pattern-language, the names of the types have been prefixed with 'RAM_'. This is to distinguish them from the original types. +# Further, the type of the 'weight'-attribute has changed: it used to be Integer, but now it's ActionCode, meaning we can write Python-expression in it. In a LHS-pattern, we write an expression that evaluates to a (Python) boolean. In our example, the expression is evaluated on every Man-object. If the result is True, the object can be matched, otherwise it cannot. + + +# Let's see what happens if we match our LHS-pattern with our model: + +from transformation.matcher import match_od + +generator = match_od(state, m, mm, lhs, ramified_mm) + +# Matching is lazy: 'match_od' returns a generator object, so it will only look for the next match if you ask it to do so. The reason is that sometimes, we're only interested in the first match, whereas producing all the matches can take a lot of time on big models, and the number of matches can also be very big. But our example is small so let's just generate all the matches: + +all_matches = list(generator) # generate all matches + +import pprint + +print() +print("All matches:\n", pprint.pformat(all_matches)) + +# A match is just a Python dictionary mapping names of our LHS-pattern to names of our model. +# There should be 2 matches: 'man' will always be matched with 'george', but 'scaryAnimal' can be matched with either 'mrBrown' or 'teddy'. + + +# So far we've only queried our model. We can modify the model by specifying a RHS-pattern: +# Objects/links that occur in RHS but not in LHS are CREATED +# Objects/links that occur in LHS but not in RHS are DELETED +# Objects/links that occur in both LHS and RHS remain, but we can still UPDATE their attributes. + +# Here's a RHS-pattern: + +rhs_cs = """ + man:RAM_Man { + # man gains weight + RAM_weight = `get_value(this) + 5`; + } + + # to create: + bill:RAM_Man { + RAM_weight = `100`; + } + billAfraidOfMan:RAM_afraidOf (bill -> man) +""" + +rhs = loader.parse_and_check(state, rhs_cs, ramified_mm, "rhs") + + +# Our RHS-pattern does not contain the objects 'scaryAnimal' or 'manAfraidOfAnimal' of our LHS, so these will be deleted. The objects 'bill' and 'billAfraidOfMan' will be created. The attribute 'weight' of 'man' (matched with 'george' in our example) will be incremented by 5. + +# Notice that the weight of the new object 'bill' is the Python-expression `100` (in backticks), not the Integer 100. + +# Let's rewrite our model: + +from transformation.cloner import clone_od +from transformation import rewriter + +m_rewritten = clone_od(state, m, mm) # copy our model before rewriting (this is optional - we do this so we can later render the model before and after rewrite in a single PlantUML diagram) + +lhs_match = all_matches[0] # select one match +rhs_match = rewriter.rewrite(state, lhs, rhs, ramified_mm, lhs_match, m_rewritten, mm) + +# Let's render everything as PlantUML: + +uml = ("" + + plantuml.render_package("MM", plantuml.render_class_diagram(state, mm)) + + plantuml.render_package("RAMified MM", plantuml.render_class_diagram(state, ramified_mm)) + + plantuml.render_package("LHS", plantuml.render_object_diagram(state, lhs, ramified_mm)) + + plantuml.render_package("RHS", plantuml.render_object_diagram(state, rhs, ramified_mm)) + + plantuml.render_package("M (before rewrite)", plantuml.render_object_diagram(state, m, mm)) + + plantuml.render_package("M (after rewrite)", plantuml.render_object_diagram(state, m_rewritten, mm)) + + + plantuml.render_trace_ramifies(state, mm, ramified_mm) + + + plantuml.render_trace_match(state, lhs_match, lhs, m, "orange") + + plantuml.render_trace_match(state, rhs_match, rhs, m_rewritten, "red") + + + plantuml.render_trace_conformance(state, lhs, ramified_mm) + + plantuml.render_trace_conformance(state, rhs, ramified_mm) + + plantuml.render_trace_conformance(state, m, mm) + + plantuml.render_trace_conformance(state, m_rewritten, mm) +) + +print() +print("PlantUML:", make_plantuml_url(uml)) + From 790ba031cf553be1b6affb38740ba91dc97d048f Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 11:06:32 +0200 Subject: [PATCH 33/43] add conformance check to MT tutorial --- tutorial/04_transformation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tutorial/04_transformation.py b/tutorial/04_transformation.py index 9343969..b143622 100644 --- a/tutorial/04_transformation.py +++ b/tutorial/04_transformation.py @@ -9,6 +9,7 @@ from concrete_syntax.textual_od.renderer import render_od from concrete_syntax.common import indent from concrete_syntax.plantuml import renderer as plantuml from concrete_syntax.plantuml.make_url import make_url as make_plantuml_url +from framework.conformance import Conformance, render_conformance_check_result mm_cs = """ Bear:Class @@ -58,6 +59,12 @@ ramified_mm = ramify(state, mm) print("RAMified meta-model:") print(indent(render_od(state, ramified_mm, mmm), 2)) +# Note that our RAMified meta-model is also a valid class diagram: + +print() +print("Is valid class diagram?") +print(render_conformance_check_result(Conformance(state, ramified_mm, mmm).check_nominal())) + # We now specify our patterns. # We create a rule that looks for a Man with weight > 60, who is afraid of an animal: From fecce518280692303c8b0fce65295d6035de7ef1 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 13:41:28 +0200 Subject: [PATCH 34/43] add tutorial on model transformation with pivots --- README.md | 4 + tutorial/05_advanced_transformation.py | 176 +++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 tutorial/05_advanced_transformation.py diff --git a/README.md b/README.md index 1171f46..daffc34 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,7 @@ The following branches exist: * `master` - currently equivalent to `mde2425` (this is the branch that was cloned by the students). This branch will be deleted after Sep 2025, because the name is too vague. * `development` - in this branch, new development will occur, primarily cleaning up the code to prepare for next year's MDE classes. + +## Tutorial + +A good place to learn how to use muMLE is the `tutorial` directory. Each file is an executable Python script that explains muMLE step-by-step (read the comments). diff --git a/tutorial/05_advanced_transformation.py b/tutorial/05_advanced_transformation.py new file mode 100644 index 0000000..02d9d30 --- /dev/null +++ b/tutorial/05_advanced_transformation.py @@ -0,0 +1,176 @@ +# Consider the following Petri Net language meta-model: + +mm_cs = """ + Place:Class + Transition:Class + + Place_tokens:AttributeLink (Place -> Integer) { + optional = False; + name = "tokens"; + constraint = `get_value(get_target(this)) >= 0`; + } + + P2T:Association (Place -> Transition) + T2P:Association (Transition -> Place) + + P2T_weight:AttributeLink (P2T -> Integer) { + optional = False; + name = "weight"; + constraint = `get_value(get_target(this)) >= 0`; + } + + T2P_weight:AttributeLink (T2P -> Integer) { + optional = False; + name = "weight"; + constraint = `get_value(get_target(this)) >= 0`; + } +""" + +# We now create the following Petri Net: +# https://upload.wikimedia.org/wikipedia/commons/4/4d/Two-boundedness-cb.png + +m_cs = """ + p1:Place { tokens = 0; } + p2:Place { tokens = 0; } + cp1:Place { tokens = 2; } + cp2:Place { tokens = 2; } + + t1:Transition + t2:Transition + t3:Transition + + :T2P (t1 -> p1) { weight = 1; } + :P2T (p1 -> t2) { weight = 1; } + :T2P (t2 -> cp1) { weight = 1; } + :P2T (cp1 -> t1) { weight = 1; } + + :T2P (t2 -> p2) { weight = 1; } + :P2T (p2 -> t3) { weight = 1; } + :T2P (t3 -> cp2) { weight = 1; } + :P2T (cp2 -> t2) { weight = 1; } +""" + +# The usual... + +from state.devstate import DevState +from bootstrap.scd import bootstrap_scd +from util import loader +from transformation.ramify import ramify +from transformation.matcher import match_od +from transformation.cloner import clone_od +from transformation import rewriter +from concrete_syntax.textual_od.renderer import render_od +from concrete_syntax.common import indent + +state = DevState() +mmm = bootstrap_scd(state) +mm = loader.parse_and_check(state, mm_cs, mmm, "mm") +m = loader.parse_and_check(state, m_cs, mm, "m") + +mm_ramified = ramify(state, mm) + + +# We will now implement Petri Net operational semantics by means of model transformation. + + +# Look for any transition +lhs_transition_cs = """ + t:RAM_Transition +""" + +# A transition is disabled if it has an incoming arc (P2T) from a place with 0 tokens: +lhs_transition_disabled_cs = """ + t:RAM_Transition + p:RAM_Place + :RAM_P2T (p -> t) { + condition = ``` + place = get_source(this) + tokens = get_slot_value(place, "tokens") + weight = get_slot_value(this, "weight") + tokens < weight # True means: cannot fire + ```; + } +""" + +lhs_transition = loader.parse_and_check(state, lhs_transition_cs, mm_ramified, "lhs_transition") +lhs_transition_disabled = loader.parse_and_check(state, lhs_transition_disabled_cs, mm_ramified, "lhs_transition_disabled") + +# We write a generator function, that yields all enabled transitions. +# Notice that we nest two calls to 'match_od', and the result of the first call is passed as a pivot to the second: + +def find_enabled_transitions(m): + for match in match_od(state, m, mm, lhs_transition, mm_ramified): + for match_nac in match_od(state, m, mm, lhs_transition_disabled, mm_ramified, pivot=match): + # transition is disabled + break # find next transition + else: + # transition is enabled + yield match + + +enabled = list(find_enabled_transitions(m)) +print("enabled PN transitions:", enabled) + + +# To fire a transition, decrement the number of tokens of every incoming place: + +lhs_incoming_cs = """ + t:RAM_Transition + inplace:RAM_Place { + RAM_tokens = `True`; # this needs to be here, otherwise, the rewriter will try to create a new attribute rather than update the existing one + } + inarc:RAM_P2T (inplace -> t) +""" +rhs_incoming_cs = """ + t:RAM_Transition + inplace:RAM_Place { + RAM_tokens = ``` + weight = get_slot_value(matched("inarc"), "weight") + print("adding", weight, "tokens to", get_name(this)) + get_value(this) - weight + ```; + } + inarc:RAM_P2T (inplace -> t) +""" + +# And increment for every outgoing place: + +lhs_outgoing_cs = """ + t:RAM_Transition + outplace:RAM_Place { + RAM_tokens = `True`; # this needs to be here, otherwise, the rewriter will try to create a new attribute rather than update the existing one + } + outarc:RAM_T2P (t -> outplace) +""" +rhs_outgoing_cs = """ + t:RAM_Transition + outplace:RAM_Place { + RAM_tokens = ``` + weight = get_slot_value(matched("outarc"), "weight") + print("removing", weight, "tokens from", get_name(this)) + get_value(this) + weight + ```; + } + outarc:RAM_T2P (t -> outplace) +""" + +lhs_incoming = loader.parse_and_check(state, lhs_incoming_cs, mm_ramified, "lhs_incoming") +rhs_incoming = loader.parse_and_check(state, rhs_incoming_cs, mm_ramified, "rhs_incoming") +lhs_outgoing = loader.parse_and_check(state, lhs_outgoing_cs, mm_ramified, "lhs_outgoing") +rhs_outgoing = loader.parse_and_check(state, rhs_outgoing_cs, mm_ramified, "rhs_outgoing") + +def fire_transition(m, transition_match): + print("firing transition:", transition_match['t']) + for match_incoming in match_od(state, m, mm, lhs_incoming, mm_ramified, pivot=transition_match): + rewriter.rewrite(state, lhs_incoming, rhs_incoming, mm_ramified, match_incoming, m, mm) + for match_outgoing in match_od(state, m, mm, lhs_outgoing, mm_ramified, pivot=transition_match): + rewriter.rewrite(state, lhs_outgoing, rhs_outgoing, mm_ramified, match_outgoing, m, mm) + + +# Let's see if it works: +while len(enabled) > 0: + print("press ENTER to fire", enabled[0]['t']) + input() + fire_transition(m, enabled[0]) + enabled = list(find_enabled_transitions(m)) + print("\nenabled PN transitions:", enabled) From e046f2f972362a55d78b7419af447b4de1d8f0d8 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 13:54:32 +0200 Subject: [PATCH 35/43] nice visualization --- tutorial/00_metamodeling.py | 13 +++++++++---- tutorial/02_inheritance.py | 3 --- tutorial/03_api.py | 1 - tutorial/05_advanced_transformation.py | 24 +++++++++++++++++++++++- util/loader.py | 3 +++ 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/tutorial/00_metamodeling.py b/tutorial/00_metamodeling.py index c1664d4..6b12fe0 100644 --- a/tutorial/00_metamodeling.py +++ b/tutorial/00_metamodeling.py @@ -27,7 +27,10 @@ m_cs = """ myLnk:a2b (myA -> myB) """ -# So far we've only created text strings. To parse the models, we first create our 'state', which is a mutable graph that will contain our models and meta-models: +# Notice that the syntax for meta-model and model is the same: We always declare a named object/link, followed by a colon (:) and the name of the type. The type name refers to the name of an object/link in the meta-model of our model. + + +# So far we've only created text strings in Python. To parse them as models, we first create our 'state', which is a mutable graph that will contain our models and meta-models: from state.devstate import DevState @@ -37,7 +40,9 @@ state = DevState() # Next, we must load the Simple Class Diagrams (SCD) meta-meta-model into our 'state'. The SCD meta-meta-model is a meta-model for our meta-model, and it is also a meta-model for itself. -# The meta-meta-model is not specified in textual syntax because it is typed by itself, and the parser cannot resolve circular dependencies. Therefore, we load the meta-meta-model by mutating the 'state' directly at a very low level: +# The meta-meta-model is not specified in textual syntax because it is typed by itself. In textual syntax, it would contain things like: +# Class:Class +# which is an object typed by itself. The parser cannot handle this (or circular dependencies in general). Therefore, we load the meta-meta-model by mutating the 'state' directly at a very low level: from bootstrap.scd import bootstrap_scd @@ -50,7 +55,7 @@ print("OK") from concrete_syntax.textual_od import parser print() -print("Parsing 'woods' meta-model...") +print("Parsing meta-model...") mm = parser.parse_od( state, m_text=mm_cs, # the string of text to parse @@ -62,7 +67,7 @@ print("OK") # And we can parse our model, the same way: print() -print("Parsing 'woods' model...") +print("Parsing model...") m = parser.parse_od( state, m_text=m_cs, diff --git a/tutorial/02_inheritance.py b/tutorial/02_inheritance.py index a8f6062..9de434f 100644 --- a/tutorial/02_inheritance.py +++ b/tutorial/02_inheritance.py @@ -1,4 +1,3 @@ - # The following meta-model has an inheritance relation: mm_cs = """ @@ -33,8 +32,6 @@ m_nonconform_cs = """ from state.devstate import DevState from bootstrap.scd import bootstrap_scd -# from concrete_syntax.textual_od import parser -# from framework.conformance import Conformance, render_conformance_check_result from util import loader state = DevState() diff --git a/tutorial/03_api.py b/tutorial/03_api.py index f30ce6d..7cdd3ea 100644 --- a/tutorial/03_api.py +++ b/tutorial/03_api.py @@ -14,7 +14,6 @@ mm_cs = """ myZ:Association (MyAbstractClass -> Z) { target_lower_cardinality = 1; } - """ m_cs = """ diff --git a/tutorial/05_advanced_transformation.py b/tutorial/05_advanced_transformation.py index 02d9d30..194a381 100644 --- a/tutorial/05_advanced_transformation.py +++ b/tutorial/05_advanced_transformation.py @@ -61,6 +61,7 @@ from transformation.cloner import clone_od from transformation import rewriter from concrete_syntax.textual_od.renderer import render_od from concrete_syntax.common import indent +from api.od import ODAPI state = DevState() mmm = bootstrap_scd(state) @@ -166,11 +167,32 @@ def fire_transition(m, transition_match): for match_outgoing in match_od(state, m, mm, lhs_outgoing, mm_ramified, pivot=transition_match): rewriter.rewrite(state, lhs_outgoing, rhs_outgoing, mm_ramified, match_outgoing, m, mm) +def show_petri_net(m): + odapi = ODAPI(state, m, mm) + p1 = odapi.get_slot_value(odapi.get("p1"), "tokens") + p2 = odapi.get_slot_value(odapi.get("p2"), "tokens") + cp1 = odapi.get_slot_value(odapi.get("cp1"), "tokens") + cp2 = odapi.get_slot_value(odapi.get("cp2"), "tokens") + return f""" + t1 t2 t3 + ┌─┐ p1 ┌─┐ p2 ┌─┐ + │ │ --- │ │ --- │ │ + │ ├─────► ( {p1} )─────►│ │─────► ( {p2} )─────►│ │ + └─┘ --- └─┘ --- └─┘ + ▲ │ ▲ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ --- │ │ --- │ + └───────( {cp1} )◄──────┘ └──────( {cp2} )◄───────┘ + --- --- + cp1 cp2 """ # Let's see if it works: while len(enabled) > 0: + print(show_petri_net(m)) + print("\nenabled PN transitions:", enabled) print("press ENTER to fire", enabled[0]['t']) input() fire_transition(m, enabled[0]) enabled = list(find_enabled_transitions(m)) - print("\nenabled PN transitions:", enabled) diff --git a/util/loader.py b/util/loader.py index 4a29d63..3b5112b 100644 --- a/util/loader.py +++ b/util/loader.py @@ -39,8 +39,11 @@ KINDS = ["nac", "lhs", "rhs"] # Phony name generator that raises an error if you try to use it :) class LHSNameGenerator: def __call__(self, type_name): + if type_name == "GlobalCondition": + return parser.DefaultNameGenerator()(type_name) raise Exception(f"Error: Object or link of type '{type_name}' does not have a name.\nAnonymous objects/links are not allowed in the LHS of a rule, because they can have unintended consequences. Please give all of the elements in the LHS explicit names.") + # load model transformation rules def load_rules(state, get_filename, rt_mm_ramified, rule_names, check_conformance=True): rules = {} From 558772fbe4579c3c5d82cb88535d347c2cdf290b Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 13:57:25 +0200 Subject: [PATCH 36/43] commit some long outstanding changes --- bootstrap/scd.py | 3 +-- transformation/rewriter.py | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bootstrap/scd.py b/bootstrap/scd.py index facf176..3a3002d 100644 --- a/bootstrap/scd.py +++ b/bootstrap/scd.py @@ -78,8 +78,7 @@ def bootstrap_scd(state: State) -> UUID: add_edge_element("gc_inh_element", glob_constr_node, element_node) # # Attribute inherits from Element add_edge_element("attr_inh_element", attr_node, element_node) - # # Association inherits from Element - # add_edge_element("assoc_inh_element", assoc_edge, element_node) + # # Association inherits from Class add_edge_element("assoc_inh_element", assoc_edge, class_node) # # AttributeLink inherits from Element add_edge_element("attr_link_inh_element", attr_link_edge, element_node) diff --git a/transformation/rewriter.py b/transformation/rewriter.py index 100073f..51b8bca 100644 --- a/transformation/rewriter.py +++ b/transformation/rewriter.py @@ -223,7 +223,12 @@ def rewrite(state, result = exec_then_eval(python_expr, _globals=eval_globals, _locals={'this': host_obj}) # 'this' can be used to read the previous value of the slot - host_odapi.overwrite_primitive_value(host_obj_name, result, is_code=False) + # print("EVAL", common_name, python_expr, "RESULT", result, host_obj_name) + try: + host_odapi.overwrite_primitive_value(host_obj_name, result, is_code=False) + except Exception as e: + e.add_note(f"while evaluating attribute {common_name}") + raise else: msg = f"Don't know what to do with element '{common_name}' -> '{host_obj_name}:{host_type}')" # print(msg) From 457aac48b3bb01ef3d15c652f5a4575f53c0d6ad Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 15:15:17 +0200 Subject: [PATCH 37/43] fix --- tutorial/04_transformation.py | 2 +- tutorial/05_advanced_transformation.py | 43 +++++++++++++++++--------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/tutorial/04_transformation.py b/tutorial/04_transformation.py index b143622..2a50af4 100644 --- a/tutorial/04_transformation.py +++ b/tutorial/04_transformation.py @@ -139,7 +139,7 @@ from transformation import rewriter m_rewritten = clone_od(state, m, mm) # copy our model before rewriting (this is optional - we do this so we can later render the model before and after rewrite in a single PlantUML diagram) lhs_match = all_matches[0] # select one match -rhs_match = rewriter.rewrite(state, lhs, rhs, ramified_mm, lhs_match, m_rewritten, mm) +rhs_match = rewriter.rewrite(state, rhs, ramified_mm, lhs_match, m_rewritten, mm) # Let's render everything as PlantUML: diff --git a/tutorial/05_advanced_transformation.py b/tutorial/05_advanced_transformation.py index 194a381..f420b0e 100644 --- a/tutorial/05_advanced_transformation.py +++ b/tutorial/05_advanced_transformation.py @@ -1,3 +1,5 @@ +# In this tutorial, we implement the semantics of Petri Nets by means of model transformation. +# Compared to the previous tutorial, it only introduces one more feature: pivots. # Consider the following Petri Net language meta-model: mm_cs = """ @@ -74,12 +76,14 @@ mm_ramified = ramify(state, mm) # We will now implement Petri Net operational semantics by means of model transformation. -# Look for any transition +# Look for any transition: + lhs_transition_cs = """ t:RAM_Transition """ -# A transition is disabled if it has an incoming arc (P2T) from a place with 0 tokens: +# But, if that transition has an incoming arc (P2T) from a place with not enough tokens, the transition cannot fire. We can express this as a pattern: + lhs_transition_disabled_cs = """ t:RAM_Transition p:RAM_Place @@ -93,30 +97,37 @@ lhs_transition_disabled_cs = """ } """ +# Parse these patterns: lhs_transition = loader.parse_and_check(state, lhs_transition_cs, mm_ramified, "lhs_transition") lhs_transition_disabled = loader.parse_and_check(state, lhs_transition_disabled_cs, mm_ramified, "lhs_transition_disabled") -# We write a generator function, that yields all enabled transitions. -# Notice that we nest two calls to 'match_od', and the result of the first call is passed as a pivot to the second: +# To find enabled transitions, we first match our first pattern (looking for a transition), and then we try to 'grow' that match with our second, "Negative Application Condition" (NAC) pattern. If growing the match with the second pattern is possible, we abort and look for another transition. +# To grow a match, we use the 'pivot'-argument of the match-function. A pivot is a partial match that needs to be grown. +# This results in the following generator function: def find_enabled_transitions(m): for match in match_od(state, m, mm, lhs_transition, mm_ramified): - for match_nac in match_od(state, m, mm, lhs_transition_disabled, mm_ramified, pivot=match): + for match_nac in match_od(state, m, mm, lhs_transition_disabled, mm_ramified, pivot=match): # <-- notice the pivot :) # transition is disabled break # find next transition else: - # transition is enabled + # we've found an enabled transition: yield match +# Let's see if it works: enabled = list(find_enabled_transitions(m)) print("enabled PN transitions:", enabled) -# To fire a transition, decrement the number of tokens of every incoming place: +# Next, to fire a transition: +# - we decrement the number of tokens of every incoming place +# - we increment the number of tokens of every outgoing place +# We do this also by growing our match: given an enabled transition (already matched), we match for *any* incoming place, and rewrite that place to reduce its tokens. Next, we look for *any* outgoing place, and increment its tokens. +# Decrement incoming lhs_incoming_cs = """ - t:RAM_Transition + t:RAM_Transition # <-- we already know this transition is enabled inplace:RAM_Place { RAM_tokens = `True`; # this needs to be here, otherwise, the rewriter will try to create a new attribute rather than update the existing one } @@ -134,8 +145,7 @@ rhs_incoming_cs = """ inarc:RAM_P2T (inplace -> t) """ -# And increment for every outgoing place: - +# Increment outgoing lhs_outgoing_cs = """ t:RAM_Transition outplace:RAM_Place { @@ -155,17 +165,18 @@ rhs_outgoing_cs = """ outarc:RAM_T2P (t -> outplace) """ +# Parse all the patterns lhs_incoming = loader.parse_and_check(state, lhs_incoming_cs, mm_ramified, "lhs_incoming") rhs_incoming = loader.parse_and_check(state, rhs_incoming_cs, mm_ramified, "rhs_incoming") lhs_outgoing = loader.parse_and_check(state, lhs_outgoing_cs, mm_ramified, "lhs_outgoing") rhs_outgoing = loader.parse_and_check(state, rhs_outgoing_cs, mm_ramified, "rhs_outgoing") +# Firing is really simple: def fire_transition(m, transition_match): - print("firing transition:", transition_match['t']) for match_incoming in match_od(state, m, mm, lhs_incoming, mm_ramified, pivot=transition_match): - rewriter.rewrite(state, lhs_incoming, rhs_incoming, mm_ramified, match_incoming, m, mm) + rewriter.rewrite(state, rhs_incoming, mm_ramified, match_incoming, m, mm) for match_outgoing in match_od(state, m, mm, lhs_outgoing, mm_ramified, pivot=transition_match): - rewriter.rewrite(state, lhs_outgoing, rhs_outgoing, mm_ramified, match_outgoing, m, mm) + rewriter.rewrite(state, rhs_outgoing, mm_ramified, match_outgoing, m, mm) def show_petri_net(m): odapi = ODAPI(state, m, mm) @@ -192,7 +203,11 @@ def show_petri_net(m): while len(enabled) > 0: print(show_petri_net(m)) print("\nenabled PN transitions:", enabled) - print("press ENTER to fire", enabled[0]['t']) + to_fire = enabled[0]['t'] + print("press ENTER to fire", to_fire) input() + print("firing transition:", to_fire) fire_transition(m, enabled[0]) enabled = list(find_enabled_transitions(m)) + +# That's it! From 0b936a48d1497ccec011317a7c4563e0d345b794 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 16:00:42 +0200 Subject: [PATCH 38/43] get rid of outdated examples --- examples/conformance/abstract_assoc.py | 75 ------- examples/conformance/metacircularity.py | 27 --- examples/conformance/woods.py | 200 ------------------ examples/conformance/woods2.py | 133 ------------ examples/geraniums/schedules/schedule.od | 0 examples/petrinet/models/schedules/foo.od | 23 -- .../models/schedules/petrinet3.drawio | 22 +- tutorial/05_advanced_transformation.py | 8 +- 8 files changed, 15 insertions(+), 473 deletions(-) delete mode 100644 examples/conformance/abstract_assoc.py delete mode 100644 examples/conformance/metacircularity.py delete mode 100644 examples/conformance/woods.py delete mode 100644 examples/conformance/woods2.py delete mode 100644 examples/geraniums/schedules/schedule.od delete mode 100644 examples/petrinet/models/schedules/foo.od diff --git a/examples/conformance/abstract_assoc.py b/examples/conformance/abstract_assoc.py deleted file mode 100644 index 27d4c7c..0000000 --- a/examples/conformance/abstract_assoc.py +++ /dev/null @@ -1,75 +0,0 @@ -from state.devstate import DevState -from bootstrap.scd import bootstrap_scd -from framework.conformance import Conformance, render_conformance_check_result -from concrete_syntax.textual_od import parser, renderer -from concrete_syntax.common import indent -from concrete_syntax.plantuml import renderer as plantuml -from util.prompt import yes_no, pause - -state = DevState() -scd_mmm = bootstrap_scd(state) - - -mm_cs = """ - BaseA:Class { - abstract = True; - } - BaseB:Class { - abstract = True; - } - baseAssoc:Association (BaseA -> BaseB) { - abstract = True; - target_lower_cardinality = 1; - target_upper_cardinality = 2; # A has 1..2 B - } - A:Class - B:Class - assoc:Association (A -> B) { - # we can further restrict cardinality from baseAssoc: - target_upper_cardinality = 1; - - # relaxing cardinalities or constraints can be done (meaning: it will still be a valid meta-model), but will have no effect: for any instance of a type, the constraints defined on the type and its supertypes will be checked. - } - :Inheritance (A -> BaseA) - :Inheritance (B -> BaseB) - :Inheritance (assoc -> baseAssoc) -""" - -print() -print("Parsing meta-model...") -mm = parser.parse_od( - state, - m_text=mm_cs, # the string of text to parse - mm=scd_mmm, # the meta-model of class diagrams (= our meta-meta-model) -) -print("OK") - -print("Is our meta-model a valid class diagram?") -conf = Conformance(state, mm, scd_mmm) -print(render_conformance_check_result(conf.check_nominal())) - -m_cs = """ - a0:A - b0:B - b1:B - - # error: assoc (A -> B) must have tgt card 0..1 (and we have 2 instead) - :assoc (a0 -> b0) - :assoc (a0 -> b1) - - # error: baseAssoc (A -> B) must have tgt card 1..2 (and we have 0 instead) - a1:A -""" - -print() -print("Parsing model...") -m = parser.parse_od( - state, - m_text=m_cs, - mm=mm, # this time, the meta-model is the previous model we parsed -) -print("OK") - -print("Is our model a valid woods-diagram?") -conf = Conformance(state, m, mm) -print(render_conformance_check_result(conf.check_nominal())) diff --git a/examples/conformance/metacircularity.py b/examples/conformance/metacircularity.py deleted file mode 100644 index c591ee0..0000000 --- a/examples/conformance/metacircularity.py +++ /dev/null @@ -1,27 +0,0 @@ -from state.devstate import DevState -from bootstrap.scd import bootstrap_scd -from services.scd import SCD -from concrete_syntax.plantuml import renderer as plantuml - -def main(): - state = DevState() - root = state.read_root() # id: 0 - - scd_mm_id = bootstrap_scd(state) - - uml = "" - - # Render SCD Meta-Model as Object Diagram - uml += plantuml.render_package("Object Diagram", plantuml.render_object_diagram(state, scd_mm_id, scd_mm_id, prefix_ids="od_")) - - # Render SCD Meta-Model as Class Diagram - uml += plantuml.render_package("Class Diagram", plantuml.render_class_diagram(state, scd_mm_id, prefix_ids="cd_")) - - # Render conformance - uml += plantuml.render_trace_conformance(state, scd_mm_id, scd_mm_id, prefix_inst_ids="od_", prefix_type_ids="cd_") - - print(uml) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/examples/conformance/woods.py b/examples/conformance/woods.py deleted file mode 100644 index 591eb91..0000000 --- a/examples/conformance/woods.py +++ /dev/null @@ -1,200 +0,0 @@ -from state.devstate import DevState -from bootstrap.scd import bootstrap_scd -from framework.conformance import Conformance, render_conformance_check_result -from concrete_syntax.textual_od import parser, renderer -from concrete_syntax.common import indent -from concrete_syntax.plantuml import renderer as plantuml -from concrete_syntax.plantuml.make_url import make_url -from util.prompt import yes_no, pause - -state = DevState() - -print("Loading meta-meta-model...") -scd_mmm = bootstrap_scd(state) -print("OK") - -print("Is our meta-meta-model a valid class diagram?") -conf = Conformance(state, scd_mmm, scd_mmm) -print(render_conformance_check_result(conf.check_nominal())) - -# If you are curious, you can serialize the meta-meta-model: -# print("--------------") -# print(indent( -# renderer.render_od(state, -# m_id=scd_mmm, -# mm_id=scd_mmm), -# 4)) -# print("--------------") - - -# Change this: -woods_mm_cs = """ - Animal:Class { - # The class Animal is an abstract class: - abstract = True; - } - - # A class without attributes - # The `abstract` attribute shown above is optional (default: False) - Bear:Class - - # Inheritance between two Classes is expressed as follows: - :Inheritance (Bear -> Animal) # meaning: Bear is an Animal - - Man:Class { - # We can define lower and upper cardinalities on Classes - # (if unspecified, the lower-card is 0, and upper-card is infinity) - - lower_cardinality = 1; # there must be at least one Man in every model - upper_cardinality = 2; # there must be at most two Men in every model - - constraint = ``` - # Python code - # the last statement must be a boolean expression - - # When conformance checking, this code will be run for every Man-object. - # The variable 'this' refers to the current Man-object. - - # Every man weighs at least '20' - # (the attribute 'weight' is added further down) - get_value(get_slot(this, "weight")) > 20 - ```; - } - # Note that we can only declare the inheritance link after having declared both Man and Animal: We can only refer to earlier objects - :Inheritance (Man -> Animal) # Man is also an Animal - - - # BTW, we could also give the Inheritance-link a name, for instance: - # man_is_animal:Inheritance (Man -> Animal) - # - # Likewise, Classes, Associations, ... can also be nameless, for instance: - # :Class { ... } - # :Association (Man -> Man) { ... } - # However, we typically want to give names to classes and associations, because we want to refer to them later. - - - # We now add an attribute to 'Man' - # Attributes are not that different from Associations: both are represented by links - Man_weight:AttributeLink (Man -> Integer) { - name = "weight"; # mandatory! - optional = False; # <- meaning: every Man *must* have a weight - - # We can also define constraints on attributes - constraint = ``` - # Python code - # Here, 'this' refers to the LINK that connects a Man-object to an Integer - tgt = get_target(this) # <- we get the target of the LINK (an Integer-object) - weight = get_value(tgt) # <- get the Integer-value (e.g., 80) - weight > 20 - ```; - } - - # Create an Association from Man to Animal - afraidOf:Association (Man -> Animal) { - # An association has the following (optional) attributes: - # - source_lower_cardinality (default: 0) - # - source_upper_cardinality (default: infinity) - # - target_lower_cardinality (default: 0) - # - target_upper_cardinality (default: infinity) - - # Every Man is afraid of at least one Animal: - target_lower_cardinality = 1; - - # No more than 6 Men are afraid of the same Animal: - source_upper_cardinality = 6; - } - - # Create a GlobalConstraint - total_weight_small_enough:GlobalConstraint { - # Note: for GlobalConstraints, there is no 'this'-variable - constraint = ``` - # Python code - # compute sum of all weights - total_weight = 0 - for man_name, man_id in get_all_instances("Man"): - total_weight += get_value(get_slot(man_id, "weight")) - - # as usual, the last statement is a boolean expression that we think should be satisfied - total_weight < 85 - ```; - } -""" - -print() -print("Parsing 'woods' meta-model...") -woods_mm = parser.parse_od( - state, - m_text=woods_mm_cs, # the string of text to parse - mm=scd_mmm, # the meta-model of class diagrams (= our meta-meta-model) -) -print("OK") - -# As a double-check, you can serialize the parsed model: -# print("--------------") -# print(indent( -# renderer.render_od(state, -# m_id=woods_mm, -# mm_id=scd_mmm), -# 4)) -# print("--------------") - -print("Is our 'woods' meta-model a valid class diagram?") -conf = Conformance(state, woods_mm, scd_mmm) -print(render_conformance_check_result(conf.check_nominal())) - -# Change this: -woods_m_cs = """ - george:Man { - weight = 15; - } - billy:Man { - weight = 100; - } - bear1:Bear - bear2:Bear - :afraidOf (george -> bear1) - :afraidOf (george -> bear2) - :afraidOf (billy -> george) -""" - -print() -print("Parsing 'woods' model...") -woods_m = parser.parse_od( - state, - m_text=woods_m_cs, - mm=woods_mm, # this time, the meta-model is the previous model we parsed -) -print("OK") - -# As a double-check, you can serialize the parsed model: -# print("--------------") -# print(indent( -# renderer.render_od(state, -# m_id=woods_m, -# mm_id=woods_mm), -# 4)) -# print("--------------") - -print("Is our model a valid woods-diagram?") -conf = Conformance(state, woods_m, woods_mm) -print(render_conformance_check_result(conf.check_nominal())) - - -print() -print("==================================") -if yes_no("Print PlantUML?"): - print_mm = yes_no(" ▸ Print meta-model?") - print_m = yes_no(" ▸ Print model?") - print_conf = print_mm and print_m and yes_no(" ▸ Print conformance links?") - - uml = "" - if print_mm: - uml += plantuml.render_package("Meta-model", plantuml.render_class_diagram(state, woods_mm)) - if print_m: - uml += plantuml.render_package("Model", plantuml.render_object_diagram(state, woods_m, woods_mm)) - if print_conf: - uml += plantuml.render_trace_conformance(state, woods_m, woods_mm) - - print("==================================") - print(make_url(uml)) - print("==================================") diff --git a/examples/conformance/woods2.py b/examples/conformance/woods2.py deleted file mode 100644 index 656cddb..0000000 --- a/examples/conformance/woods2.py +++ /dev/null @@ -1,133 +0,0 @@ -from state.devstate import DevState -from bootstrap.scd import bootstrap_scd -from framework.conformance import Conformance, render_conformance_check_result -from concrete_syntax.textual_cd import parser as parser_cd -from concrete_syntax.textual_od import parser as parser_od -from concrete_syntax.textual_od import renderer as renderer_od -from concrete_syntax.common import indent -from concrete_syntax.plantuml import renderer as plantuml -from util.prompt import yes_no, pause - -state = DevState() - -print("Loading meta-meta-model...") -scd_mmm = bootstrap_scd(state) -print("OK") - -print("Is our meta-meta-model a valid class diagram?") -conf = Conformance(state, scd_mmm, scd_mmm) -print(render_conformance_check_result(conf.check_nominal())) - -# If you are curious, you can serialize the meta-meta-model: -# print("--------------") -# print(indent( -# renderer.render_od(state, -# m_id=scd_mmm, -# mm_id=scd_mmm), -# 4)) -# print("--------------") - - -# Change this: -woods_mm_cs = """ - abstract class Animal - - class Bear (Animal) # Bear inherits Animal - - class Man [1..2] (Animal) { - Integer weight `get_value(get_target(this)) > 20`; # <- constraint in context of attribute-link - - `get_value(get_slot(this, "weight")) > 20` # <- constraint in context of Man-object - } - - association afraidOf [0..6] Man -> Animal [1..2] - - global total_weight_small_enough ``` - total_weight = 0 - for man_name, man_id in get_all_instances("Man"): - total_weight += get_value(get_slot(man_id, "weight")) - total_weight < 85 - ``` -""" - -print() -print("Parsing 'woods' meta-model...") -woods_mm = parser_cd.parse_cd( - state, - m_text=woods_mm_cs, # the string of text to parse -) -print("OK") - -# We can serialize the class diagram to our object diagram syntax -# (because the class diagram IS also an object diagram): -print("--------------") -print(indent( - renderer_od.render_od(state, - m_id=woods_mm, - mm_id=scd_mmm), - 4)) -print("--------------") - -print("Is our 'woods' meta-model a valid class diagram?") -conf = Conformance(state, woods_mm, scd_mmm) -print(render_conformance_check_result(conf.check_nominal())) - -# Change this: -woods_m_cs = """ - george:Man { - weight = 15; - } - billy:Man { - weight = 100; - } - bear1:Bear - bear2:Bear - :afraidOf (george -> bear1) - :afraidOf (george -> bear2) -""" - -print() -print("Parsing 'woods' model...") -woods_m = parser_od.parse_od( - state, - m_text=woods_m_cs, - mm=woods_mm, # this time, the meta-model is the previous model we parsed -) -print("OK") - -# As a double-check, you can serialize the parsed model: -# print("--------------") -# print(indent( -# renderer.render_od(state, -# m_id=woods_m, -# mm_id=woods_mm), -# 4)) -# print("--------------") - -print("Is our model a valid woods-diagram?") -conf = Conformance(state, woods_m, woods_mm) -print(render_conformance_check_result(conf.check_nominal())) - - -print() -print("==================================") -if yes_no("Print PlantUML?"): - print_mm = yes_no(" ▸ Print meta-model?") - print_m = yes_no(" ▸ Print model?") - print_conf = print_mm and print_m and yes_no(" ▸ Print conformance links?") - - uml = "" - if print_mm: - uml += plantuml.render_package("Meta-model", plantuml.render_class_diagram(state, woods_mm)) - if print_m: - uml += plantuml.render_package("Model", plantuml.render_object_diagram(state, woods_m, woods_mm)) - if print_conf: - uml += plantuml.render_trace_conformance(state, woods_m, woods_mm) - - print("==================================") - print(uml) - print("==================================") - print("Go to either:") - print(" ▸ https://www.plantuml.com/plantuml/uml") - print(" ▸ https://mstro.duckdns.org/plantuml/uml") - print("and paste the above string.") diff --git a/examples/geraniums/schedules/schedule.od b/examples/geraniums/schedules/schedule.od deleted file mode 100644 index e69de29..0000000 diff --git a/examples/petrinet/models/schedules/foo.od b/examples/petrinet/models/schedules/foo.od deleted file mode 100644 index 7acc7a8..0000000 --- a/examples/petrinet/models/schedules/foo.od +++ /dev/null @@ -1,23 +0,0 @@ -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/petrinet3.drawio b/examples/petrinet/models/schedules/petrinet3.drawio index 4e701fe..a20ee2c 100644 --- a/examples/petrinet/models/schedules/petrinet3.drawio +++ b/examples/petrinet/models/schedules/petrinet3.drawio @@ -1,6 +1,6 @@ - + - + @@ -70,7 +70,7 @@ - + @@ -111,7 +111,7 @@ - + @@ -185,7 +185,7 @@ - + @@ -218,10 +218,10 @@ - + - + @@ -231,7 +231,7 @@ - + @@ -239,13 +239,13 @@ - + - + - + diff --git a/tutorial/05_advanced_transformation.py b/tutorial/05_advanced_transformation.py index f420b0e..7428535 100644 --- a/tutorial/05_advanced_transformation.py +++ b/tutorial/05_advanced_transformation.py @@ -187,16 +187,16 @@ def show_petri_net(m): return f""" t1 t2 t3 ┌─┐ p1 ┌─┐ p2 ┌─┐ - │ │ --- │ │ --- │ │ + │ │ │ │ │ │ │ ├─────► ( {p1} )─────►│ │─────► ( {p2} )─────►│ │ - └─┘ --- └─┘ --- └─┘ + └─┘ └─┘ └─┘ ▲ │ ▲ │ │ │ │ │ │ │ │ │ │ │ │ │ - │ --- │ │ --- │ + │ │ │ │ └───────( {cp1} )◄──────┘ └──────( {cp2} )◄───────┘ - --- --- + cp1 cp2 """ # Let's see if it works: From 7172ade9fac0320b094674595c6d604f82a72cfe Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 16:01:52 +0200 Subject: [PATCH 39/43] update readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index daffc34..8623cad 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,8 @@ Features: The following branches exist: - * `mde2425` - the branch containing a snapshot of the repo used for the MDE assignments 24-25. No breaking changes will be pushed here. After the re-exams (Sep 2025), this branch will be frozen. - * `master` - currently equivalent to `mde2425` (this is the branch that was cloned by the students). This branch will be deleted after Sep 2025, because the name is too vague. * `development` - in this branch, new development will occur, primarily cleaning up the code to prepare for next year's MDE classes. + * `mde2425` - contains a snapshot of the repo used for the MDE assignments 24-25. This branch should remain frozen. ## Tutorial From 7eca6ddda43476d827f4c8e8ccfe5f1974b0967a Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 16:04:28 +0200 Subject: [PATCH 40/43] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8623cad..9c054a8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Features: - Class Diagrams (self-conforming) - Causal Block Diagrams language - Petri Net language + - [Repotting the Geraniums](https://ris.utwente.nl/ws/portalfiles/portal/5312315/gtvmt2009.pdf) ## Dependencies From 7974b1a26d2c33a6b10d340f0ccbee389793ce72 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 16:24:24 +0200 Subject: [PATCH 41/43] add API LaTeX table --- doc/odapi/.gitignore | 3 + doc/odapi/api_table.pdf | Bin 0 -> 67801 bytes doc/odapi/api_table.tex | 121 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 doc/odapi/.gitignore create mode 100644 doc/odapi/api_table.pdf create mode 100644 doc/odapi/api_table.tex diff --git a/doc/odapi/.gitignore b/doc/odapi/.gitignore new file mode 100644 index 0000000..e477c8f --- /dev/null +++ b/doc/odapi/.gitignore @@ -0,0 +1,3 @@ +*.aux +*.log +*.out \ No newline at end of file diff --git a/doc/odapi/api_table.pdf b/doc/odapi/api_table.pdf new file mode 100644 index 0000000000000000000000000000000000000000..30dfbd3f172f6ce961faaacc0894b256b543898c GIT binary patch literal 67801 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%0Ax85n49 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

@mK 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 literal 0 HcmV?d00001 diff --git a/doc/odapi/api_table.tex b/doc/odapi/api_table.tex new file mode 100644 index 0000000..586053f --- /dev/null +++ b/doc/odapi/api_table.tex @@ -0,0 +1,121 @@ +\documentclass{article} + +\usepackage[left=1cm, right=1cm]{geometry} % reduce page margins + +\usepackage{amssymb} +\usepackage{booktabs} +\usepackage[normalem]{ulem} +\usepackage[utf8]{inputenc} +\usepackage{hyperref} +\usepackage{graphicx} +\usepackage{tikz} +\usepackage{color,listings} +\usepackage{awesomebox} + +\newcommand{\specialcell}[2][c]{% + \begin{tabular}[#1]{@{}l@{}}#2\end{tabular}} + +\def\ck{\checkmark} + +\begin{document} + +\centering +\begin{scriptsize} +\begin{tabular}{|l|c|c|c|c|c|l|} + \hline + & \multicolumn{5}{c|}{Availability in Context} & \\ + \hline + & \multicolumn{2}{c|}{ \specialcell{Meta-Model \\ Constraint}} & \multicolumn{2}{c|}{ \specialcell{Model Trans- \\ formation Rule} } & & \\ + + \hline + & \specialcell{ \textbf{Local} } + & \specialcell{ \textbf{Global} } + & \specialcell{ \textbf{NAC} \\ \textbf{LHS} } + & \textbf{RHS} + & \specialcell{ \textbf{OD-} \\ \textbf{API} } + & \textbf{Meaning} \\ + \hline + \hline + \multicolumn{7}{|l|}{\textit{Querying}} \\ + \hline + \texttt{this :obj} & \ck & & \ck & \ck & & Current object or link \\ + \hline + \texttt{get\_name(:obj) :str} & \ck & \ck & \ck & \ck & \ck & Get name of object or link \\ + \hline + \texttt{get(name:str) :obj} & \ck & \ck & \ck & \ck & \ck & Get object or link by name (inverse of \texttt{get\_name}) \\ + \hline + \texttt{get\_type(:obj) :obj} & \ck & \ck & \ck & \ck & \ck & {Get type of object or link} \\ + \hline + \texttt{get\_type\_name(:obj) :str} & \ck & \ck & \ck & \ck & \ck & {Same as \texttt{get\_name(get\_type(...))}} \\ + \hline + \specialcell{ + \texttt{is\_instance(:obj, type\_name:str} + \\ \texttt{ [,include\_subtypes:bool=True]) :bool} + } & \ck & \ck & \ck & \ck & \ck & \specialcell{Is object instance of given type\\(or subtype thereof)?} \\ + \hline + + \texttt{get\_value(:obj) :int|str|bool} & \ck & \ck & \ck & \ck & \ck & \specialcell{Get value (only works on Integer,\\String, Boolean objects)} \\ + \hline + \texttt{get\_target(:link) :obj} & \ck & \ck & \ck & \ck & \ck & {Get target of link} \\ + \hline + \texttt{get\_source(:link) :obj} & \ck & \ck & \ck & \ck & \ck & {Get source of link} \\ + \hline + \texttt{get\_slot(:obj, attr\_name:str) :link} & \ck & \ck & \ck & \ck & \ck & {Get slot-link (link connecting object to a value)} \\ + \hline + \specialcell{ + \texttt{get\_slot\_value(:obj,} + \\ \texttt{attr\_name:str) :int|str|bool} + } & \ck & \ck & \ck & \ck & \ck & {Same as \texttt{get\_value(get\_slot(...))})} \\ + \hline + + \specialcell{ + \texttt{get\_all\_instances(type\_name:str} + \\ \texttt{ [,include\_subtypes:bool=True]} + \\ \texttt{) :list<(str, obj)>} + } & \ck & \ck & \ck & \ck & \ck & \specialcell{Get list of tuples (name, object) \\ of given type (and its subtypes).} \\ + \hline + \specialcell{ + \texttt{get\_outgoing(:obj,} + \\ \texttt{ assoc\_name:str) :list} + } & \ck & \ck & \ck & \ck & \ck & {Get outgoing links of given type} \\ + \hline + \specialcell{ + \texttt{get\_incoming(:obj,} + \\ \texttt{ assoc\_name:str) :list} + } & \ck & \ck & \ck & \ck & \ck & {Get incoming links of given type} \\ + \hline + \texttt{has\_slot(:obj, attr\_name:str) :bool} & \ck & \ck & \ck & \ck & \ck & {Does object have given slot?} \\ + \hline + \texttt{matched(label:str) :obj} & & & \ck & \ck & & \specialcell{Get matched object by its label \\ (the name of the object in the pattern)} \\ + + \hline + \hline + \multicolumn{7}{|l|}{\textit{Modifying}} \\ + \hline + \texttt{delete(:obj)} & & & & \ck & \ck & {Delete object or link} \\ + \hline + + \specialcell{ + \texttt{set\_slot\_value(:obj, attr\_name:str,} + \\ \texttt{ val:int|str|bool)} + } & & & & \ck & \ck & \specialcell{Set value of slot. + \\ Creates slot if it doesn't exist yet.} \\ + \hline + + \specialcell{ + \texttt{create\_link(link\_name:str|None,} \\ + \texttt{ assoc\_name:str, src:obj, tgt:obj) :link} + } & & & & \ck & \ck & \specialcell{Create link (typed by given association). \\ + If \texttt{link\_name} is None, name is auto-generated.} \\ + \hline + \specialcell{ + \texttt{create\_object(object\_name:str|None,} \\ + \texttt{ class\_name:str) :obj} + } & & & & \ck & \ck & \specialcell{Create object (typed by given class). \\ + If \texttt{object\_name} is None, name is auto-generated.} \\ + \hline + % \texttt{print(*args)} & \multicolumn{2}{c|}{Python's print function (useful for debugging)} & no, use the real print() \\ +\end{tabular} +\end{scriptsize} + +\end{document} \ No newline at end of file From ccf075e2e7821ff40898a2752d41403803ec39f5 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 16:35:40 +0200 Subject: [PATCH 42/43] newline --- tutorial/05_advanced_transformation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tutorial/05_advanced_transformation.py b/tutorial/05_advanced_transformation.py index 7428535..3ba1c79 100644 --- a/tutorial/05_advanced_transformation.py +++ b/tutorial/05_advanced_transformation.py @@ -1,5 +1,6 @@ # In this tutorial, we implement the semantics of Petri Nets by means of model transformation. # Compared to the previous tutorial, it only introduces one more feature: pivots. + # Consider the following Petri Net language meta-model: mm_cs = """ From c9c6b4863d061278e2cd897b67ee74efab418a2f Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 23 Jul 2025 16:56:52 +0200 Subject: [PATCH 43/43] draw filled boxes for petri net example/tutorial --- tutorial/05_advanced_transformation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tutorial/05_advanced_transformation.py b/tutorial/05_advanced_transformation.py index 3ba1c79..deaf376 100644 --- a/tutorial/05_advanced_transformation.py +++ b/tutorial/05_advanced_transformation.py @@ -187,10 +187,10 @@ def show_petri_net(m): cp2 = odapi.get_slot_value(odapi.get("cp2"), "tokens") return f""" t1 t2 t3 - ┌─┐ p1 ┌─┐ p2 ┌─┐ - │ │ │ │ │ │ - │ ├─────► ( {p1} )─────►│ │─────► ( {p2} )─────►│ │ - └─┘ └─┘ └─┘ + ███ p1 ███ p2 ███ + ███ ███ ███ + ███─────► ( {p1} )─────►███─────► ( {p2} )─────►███ + ███ ███ ███ ▲ │ ▲ │ │ │ │ │ │ │ │ │