From d00b9c25db0c2a95207e46299b82eb0cc5256c6b Mon Sep 17 00:00:00 2001 From: Inte Vleminckx Date: Tue, 3 Jun 2025 16:17:37 +0200 Subject: [PATCH] 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'(?