Added some documentation, fixed test and missing schedule

This commit is contained in:
robbe 2025-06-30 18:03:24 +02:00
parent ebfd85a666
commit fd6c8b4277
26 changed files with 1284 additions and 72 deletions

View file

@ -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"],

View file

@ -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:

View file

@ -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";}

View file

@ -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";}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -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)

View file

@ -0,0 +1 @@
# Under construction

View file

@ -0,0 +1 @@
# Under construction

View file

@ -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;

View file

@ -34,7 +34,7 @@ if TYPE_CHECKING:
from transformation.schedule.schedule import Schedule
class RuleSchedular:
class RuleScheduler:
__slots__ = (
"rule_executor",
"schedule_main",

View file

@ -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): ...

View file

@ -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)

View file

@ -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(),

View file

@ -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(),

View file

@ -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,

View file

@ -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
</TD></TR>
{% endif %}
{% else %}
<TR><TD>X</TD></TR>
<TR><TD>&nbsp;</TD></TR>
{% endif %}
</TABLE>>
{%- endmacro %}

View file

@ -31,7 +31,7 @@
{%- endmacro %}
{% macro Schedule(name, file) %}
{{ name }} = SubSchedule(schedular, "{{ file }}")
{{ name }} = SubSchedule(scheduler, "{{ file }}")
{%- endmacro %}
{% macro Loop(name) %}

View file

@ -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 %}