openprocess 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cpnpy/__init__.py +46 -0
- openprocess/__init__.py +57 -0
- openprocess/analysis/__init__.py +0 -0
- openprocess/analysis/state_space.py +521 -0
- openprocess/analysis/state_space_process.py +251 -0
- openprocess/cli.py +742 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
- openprocess/exercises/pack.md +14 -0
- openprocess/flow/__init__.py +50 -0
- openprocess/flow/box.py +466 -0
- openprocess/flow/boxes/__init__.py +7 -0
- openprocess/flow/boxes/check.py +119 -0
- openprocess/flow/boxes/compare.py +16 -0
- openprocess/flow/boxes/cpn.py +53 -0
- openprocess/flow/boxes/discover.py +124 -0
- openprocess/flow/boxes/filter.py +80 -0
- openprocess/flow/boxes/input.py +124 -0
- openprocess/flow/boxes/output.py +52 -0
- openprocess/flow/boxes/predict.py +186 -0
- openprocess/flow/boxes/science.py +159 -0
- openprocess/flow/boxes/sweeps.py +18 -0
- openprocess/flow/convert.py +187 -0
- openprocess/flow/datasets.py +198 -0
- openprocess/flow/explain.py +115 -0
- openprocess/flow/library.py +222 -0
- openprocess/flow/record.py +385 -0
- openprocess/flow/runner.py +357 -0
- openprocess/flow/sweep.py +92 -0
- openprocess/flow/types.py +290 -0
- openprocess/flow/workflow.py +628 -0
- openprocess/gui/__init__.py +0 -0
- openprocess/gui/app.py +90 -0
- openprocess/gui/arc_editing.py +295 -0
- openprocess/gui/canvas.py +1414 -0
- openprocess/gui/flow/__init__.py +8 -0
- openprocess/gui/flow/canvas.py +854 -0
- openprocess/gui/flow/page.py +972 -0
- openprocess/gui/flow/templates.py +131 -0
- openprocess/gui/flow/viewers.py +665 -0
- openprocess/gui/items.py +1275 -0
- openprocess/gui/learn/answer_boxes.py +978 -0
- openprocess/gui/learn/concealment.py +91 -0
- openprocess/gui/learn/mode.py +1181 -0
- openprocess/gui/panning.py +241 -0
- openprocess/gui/resources/openprocess-icon.png +0 -0
- openprocess/gui/studio/__init__.py +1 -0
- openprocess/gui/studio/__main__.py +3 -0
- openprocess/gui/studio/app.py +4031 -0
- openprocess/gui/studio/charts.py +115 -0
- openprocess/gui/studio/compare_page.py +487 -0
- openprocess/gui/studio/cpn_page.py +1858 -0
- openprocess/gui/studio/definition_view.py +284 -0
- openprocess/gui/studio/derivation_view.py +421 -0
- openprocess/gui/studio/documents.py +152 -0
- openprocess/gui/studio/dotted_chart.py +1401 -0
- openprocess/gui/studio/file_dialogs.py +143 -0
- openprocess/gui/studio/filter_dialog.py +247 -0
- openprocess/gui/studio/graph_builders.py +176 -0
- openprocess/gui/studio/graph_view.py +682 -0
- openprocess/gui/studio/instances.py +413 -0
- openprocess/gui/studio/log_editor.py +675 -0
- openprocess/gui/studio/log_page.py +800 -0
- openprocess/gui/studio/markdown_view.py +127 -0
- openprocess/gui/studio/mathtext.py +260 -0
- openprocess/gui/studio/ml_highlighter.py +75 -0
- openprocess/gui/studio/model_page.py +760 -0
- openprocess/gui/studio/net_comparison.py +124 -0
- openprocess/gui/studio/notes_overlay.py +275 -0
- openprocess/gui/studio/petri_page.py +844 -0
- openprocess/gui/studio/regions_view.py +502 -0
- openprocess/gui/studio/sidebar.py +149 -0
- openprocess/gui/studio/style.py +503 -0
- openprocess/gui/studio/tool_icons.py +134 -0
- openprocess/gui/studio/updates.py +439 -0
- openprocess/gui/studio/widgets.py +899 -0
- openprocess/gui/studio/workers.py +60 -0
- openprocess/gui/studio/workspace.py +447 -0
- openprocess/gui/theme.py +394 -0
- openprocess/gui/tidy.py +86 -0
- openprocess/io/__init__.py +0 -0
- openprocess/io/cpn_reader.py +389 -0
- openprocess/io/cpn_writer.py +357 -0
- openprocess/learn/__init__.py +23 -0
- openprocess/learn/answers.py +188 -0
- openprocess/learn/checks.py +953 -0
- openprocess/learn/computed.py +1180 -0
- openprocess/learn/context.py +145 -0
- openprocess/learn/exam.py +169 -0
- openprocess/learn/exercise-packs.md +325 -0
- openprocess/learn/importer.py +216 -0
- openprocess/learn/notation.py +474 -0
- openprocess/learn/pack.py +511 -0
- openprocess/learn/sheet.py +296 -0
- openprocess/mining/__init__.py +73 -0
- openprocess/mining/analysis.py +689 -0
- openprocess/mining/columns.py +282 -0
- openprocess/mining/compare_nets.py +246 -0
- openprocess/mining/conformance/__init__.py +0 -0
- openprocess/mining/conformance/alignments.py +263 -0
- openprocess/mining/conformance/quality.py +145 -0
- openprocess/mining/conformance/token_replay.py +252 -0
- openprocess/mining/csv_import.py +222 -0
- openprocess/mining/definitions.py +584 -0
- openprocess/mining/dfg.py +187 -0
- openprocess/mining/discovery/__init__.py +0 -0
- openprocess/mining/discovery/alpha.py +168 -0
- openprocess/mining/discovery/heuristics.py +332 -0
- openprocess/mining/discovery/inductive.py +477 -0
- openprocess/mining/discovery/state_regions.py +62 -0
- openprocess/mining/filtering.py +237 -0
- openprocess/mining/footprint.py +183 -0
- openprocess/mining/invariants.py +191 -0
- openprocess/mining/layout.py +279 -0
- openprocess/mining/log.py +364 -0
- openprocess/mining/petrinet.py +354 -0
- openprocess/mining/playout.py +75 -0
- openprocess/mining/pm4py_bridge.py +82 -0
- openprocess/mining/pnml.py +223 -0
- openprocess/mining/processtree.py +216 -0
- openprocess/mining/regions.py +476 -0
- openprocess/mining/stats.py +160 -0
- openprocess/mining/structure.py +374 -0
- openprocess/mining/transition_system.py +409 -0
- openprocess/mining/xes.py +399 -0
- openprocess/ml/__init__.py +0 -0
- openprocess/ml/ast_nodes.py +332 -0
- openprocess/ml/builtins.py +364 -0
- openprocess/ml/colorsets.py +522 -0
- openprocess/ml/errors.py +60 -0
- openprocess/ml/evaluator.py +754 -0
- openprocess/ml/lexer.py +277 -0
- openprocess/ml/multiset.py +417 -0
- openprocess/ml/parser.py +737 -0
- openprocess/ml/values.py +319 -0
- openprocess/model/__init__.py +0 -0
- openprocess/model/declarations.py +617 -0
- openprocess/model/examples.py +98 -0
- openprocess/model/net.py +701 -0
- openprocess/model/plain.py +192 -0
- openprocess/references.py +280 -0
- openprocess/sim/__init__.py +0 -0
- openprocess/sim/binding.py +620 -0
- openprocess/sim/export.py +66 -0
- openprocess/sim/simulator.py +315 -0
- openprocess/teaching/__init__.py +4 -0
- openprocess/teaching/answers.py +4 -0
- openprocess/teaching/checks.py +5 -0
- openprocess/teaching/pack.py +4 -0
- openprocess/teaching/sheet.py +4 -0
- openprocess-0.7.0.dist-info/METADATA +927 -0
- openprocess-0.7.0.dist-info/RECORD +173 -0
- openprocess-0.7.0.dist-info/WHEEL +5 -0
- openprocess-0.7.0.dist-info/entry_points.txt +6 -0
- openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
- openprocess-0.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""PNML: the ISO/IEC 15909-2 interchange format for Petri nets.
|
|
2
|
+
|
|
3
|
+
PNML is what ProM, PM4Py, WoPeD and most academic tools read and write, so
|
|
4
|
+
it is the way to move a discovered model between this app and anything else.
|
|
5
|
+
|
|
6
|
+
The subset we handle (the "P/T net" type)::
|
|
7
|
+
|
|
8
|
+
<pnml>
|
|
9
|
+
<net id="net1" type="http://www.pnml.org/version-2009/grammar/ptnet">
|
|
10
|
+
<name><text>My net</text></name>
|
|
11
|
+
<page id="page1">
|
|
12
|
+
<place id="p1">
|
|
13
|
+
<name><text>start</text></name>
|
|
14
|
+
<initialMarking><text>1</text></initialMarking>
|
|
15
|
+
<graphics><position x="10" y="20"/></graphics>
|
|
16
|
+
</place>
|
|
17
|
+
<transition id="t1">
|
|
18
|
+
<name><text>register</text></name>
|
|
19
|
+
<toolspecific tool="ProM" version="6.4" activity="$invisible$"/>
|
|
20
|
+
</transition>
|
|
21
|
+
<arc id="a1" source="p1" target="t1">
|
|
22
|
+
<inscription><text>1</text></inscription>
|
|
23
|
+
</arc>
|
|
24
|
+
</page>
|
|
25
|
+
<finalmarkings> <- ProM extension
|
|
26
|
+
<marking><place idref="p9"><text>1</text></place></marking>
|
|
27
|
+
</finalmarkings>
|
|
28
|
+
</net>
|
|
29
|
+
</pnml>
|
|
30
|
+
|
|
31
|
+
Silent transitions are marked with ProM's ``toolspecific`` element
|
|
32
|
+
``activity="$invisible$"``; that is the convention PM4Py follows too.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from xml.etree import ElementTree as ET
|
|
39
|
+
from xml.sax.saxutils import escape, quoteattr
|
|
40
|
+
|
|
41
|
+
from .petrinet import Marking, PetriNet
|
|
42
|
+
|
|
43
|
+
_INVISIBLE = "$invisible$"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _local(tag: str) -> str:
|
|
47
|
+
return tag.rsplit("}", 1)[-1]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _child(element: ET.Element, name: str) -> ET.Element | None:
|
|
51
|
+
for child in element:
|
|
52
|
+
if _local(child.tag) == name:
|
|
53
|
+
return child
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _text_of(element: ET.Element | None) -> str | None:
|
|
58
|
+
"""The ``<text>`` content of a PNML label such as ``<name>``."""
|
|
59
|
+
if element is None:
|
|
60
|
+
return None
|
|
61
|
+
text = _child(element, "text")
|
|
62
|
+
return text.text.strip() if text is not None and text.text else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_pnml(text: str | bytes) -> PetriNet:
|
|
66
|
+
root = ET.fromstring(text)
|
|
67
|
+
net_element = root if _local(root.tag) == "net" else next(
|
|
68
|
+
(e for e in root.iter() if _local(e.tag) == "net"), None)
|
|
69
|
+
if net_element is None:
|
|
70
|
+
raise ValueError("No <net> element found; is this a PNML file?")
|
|
71
|
+
|
|
72
|
+
net = PetriNet(_text_of(_child(net_element, "name")) or net_element.get("id") or "Petri net")
|
|
73
|
+
initial: dict[str, int] = {}
|
|
74
|
+
arcs: list[tuple[str, str, int]] = []
|
|
75
|
+
|
|
76
|
+
# Nodes may sit inside (nested) <page> elements or directly under <net>.
|
|
77
|
+
for element in net_element.iter():
|
|
78
|
+
tag = _local(element.tag)
|
|
79
|
+
# <place idref=...> inside <finalmarkings> is a reference, not a node:
|
|
80
|
+
# only elements with an id define places, transitions and arcs.
|
|
81
|
+
if element.get("id") is None:
|
|
82
|
+
continue
|
|
83
|
+
if tag == "place":
|
|
84
|
+
place = net.add_place(_text_of(_child(element, "name")) or element.get("id"),
|
|
85
|
+
id=element.get("id"))
|
|
86
|
+
place.position = _position(element)
|
|
87
|
+
place.name_offset = _name_offset(element)
|
|
88
|
+
tokens = _text_of(_child(element, "initialMarking"))
|
|
89
|
+
if tokens and tokens.isdigit() and int(tokens) > 0:
|
|
90
|
+
initial[place.id] = int(tokens)
|
|
91
|
+
elif tag == "transition":
|
|
92
|
+
name = _text_of(_child(element, "name")) or element.get("id")
|
|
93
|
+
invisible = any(
|
|
94
|
+
_local(sub.tag) == "toolspecific" and sub.get("activity") == _INVISIBLE
|
|
95
|
+
for sub in element)
|
|
96
|
+
transition = net.add_transition(None if invisible else name, name=name,
|
|
97
|
+
id=element.get("id"))
|
|
98
|
+
transition.position = _position(element)
|
|
99
|
+
transition.name_offset = _name_offset(element)
|
|
100
|
+
elif tag == "arc":
|
|
101
|
+
weight_text = _text_of(_child(element, "inscription"))
|
|
102
|
+
weight = int(weight_text) if weight_text and weight_text.isdigit() else 1
|
|
103
|
+
graphics = _child(element, "graphics")
|
|
104
|
+
points = []
|
|
105
|
+
if graphics is not None:
|
|
106
|
+
for sub in graphics:
|
|
107
|
+
if _local(sub.tag) == "position":
|
|
108
|
+
points.append((float(sub.get("x", 0)), float(sub.get("y", 0))))
|
|
109
|
+
arcs.append((element.get("source"), element.get("target"), weight, points))
|
|
110
|
+
|
|
111
|
+
for source, target, weight, points in arcs:
|
|
112
|
+
net.add_arc(source, target, weight).points = points
|
|
113
|
+
for element in net_element:
|
|
114
|
+
if _local(element.tag) == "toolspecific" and element.get("tool") == "OpenProcess" \
|
|
115
|
+
and element.get("namesOutside") == "true":
|
|
116
|
+
net.info["names_outside"] = "true"
|
|
117
|
+
net.initial_marking = Marking(initial)
|
|
118
|
+
|
|
119
|
+
final: dict[str, int] = {}
|
|
120
|
+
finals = _child(net_element, "finalmarkings")
|
|
121
|
+
if finals is not None:
|
|
122
|
+
marking = _child(finals, "marking")
|
|
123
|
+
if marking is not None:
|
|
124
|
+
for place in marking:
|
|
125
|
+
tokens = _text_of(place)
|
|
126
|
+
if tokens and tokens.isdigit() and int(tokens) > 0:
|
|
127
|
+
final[place.get("idref")] = int(tokens)
|
|
128
|
+
if final:
|
|
129
|
+
net.final_marking = Marking(final)
|
|
130
|
+
else:
|
|
131
|
+
# No final marking stored: for a workflow net the obvious choice is
|
|
132
|
+
# one token in the sink place.
|
|
133
|
+
sinks = net.sink_places()
|
|
134
|
+
if len(sinks) == 1:
|
|
135
|
+
net.final_marking = Marking({sinks[0]: 1})
|
|
136
|
+
return net
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _position(element: ET.Element) -> tuple[float, float] | None:
|
|
140
|
+
graphics = _child(element, "graphics")
|
|
141
|
+
position = _child(graphics, "position") if graphics is not None else None
|
|
142
|
+
if position is None:
|
|
143
|
+
return None
|
|
144
|
+
try:
|
|
145
|
+
return float(position.get("x", 0)), float(position.get("y", 0))
|
|
146
|
+
except ValueError:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _name_offset(element: ET.Element) -> tuple[float, float] | None:
|
|
151
|
+
"""``<name><graphics><offset x y/></graphics></name>``: where the label is."""
|
|
152
|
+
name = _child(element, "name")
|
|
153
|
+
graphics = _child(name, "graphics") if name is not None else None
|
|
154
|
+
offset = _child(graphics, "offset") if graphics is not None else None
|
|
155
|
+
if offset is None:
|
|
156
|
+
return None
|
|
157
|
+
try:
|
|
158
|
+
return float(offset.get("x", 0)), float(offset.get("y", 0))
|
|
159
|
+
except ValueError:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def read_pnml(path: str | Path) -> PetriNet:
|
|
164
|
+
net = parse_pnml(Path(path).read_bytes())
|
|
165
|
+
net.info.setdefault("source", str(path))
|
|
166
|
+
return net
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def pnml_string(net: PetriNet, positions: dict[str, tuple[float, float]] | None = None) -> str:
|
|
170
|
+
"""Serialise ``net``. ``positions`` (e.g. from the layout) overrides stored ones."""
|
|
171
|
+
positions = positions or {}
|
|
172
|
+
|
|
173
|
+
def graphics(node_id: str, stored) -> str:
|
|
174
|
+
point = positions.get(node_id, stored)
|
|
175
|
+
if point is None:
|
|
176
|
+
return ""
|
|
177
|
+
return (f'<graphics><position x="{point[0]:.1f}" y="{point[1]:.1f}"/>'
|
|
178
|
+
f'<dimension x="40" y="40"/></graphics>')
|
|
179
|
+
|
|
180
|
+
def name(node) -> str:
|
|
181
|
+
"""The node's name, with the label position when one was set."""
|
|
182
|
+
offset = getattr(node, "name_offset", None)
|
|
183
|
+
where = (f'<graphics><offset x="{offset[0]:.1f}" y="{offset[1]:.1f}"/></graphics>'
|
|
184
|
+
if offset is not None else "")
|
|
185
|
+
return f"<name><text>{escape(node.name)}</text>{where}</name>"
|
|
186
|
+
|
|
187
|
+
lines = ['<?xml version="1.0" encoding="UTF-8"?>', "<pnml>",
|
|
188
|
+
f' <net id="net1" type="http://www.pnml.org/version-2009/grammar/pnmlcoremodel">',
|
|
189
|
+
f" <name><text>{escape(net.name)}</text></name>"]
|
|
190
|
+
if net.info.get("names_outside") == "true":
|
|
191
|
+
# OpenProcess's drawing option: names next to the nodes, not inside.
|
|
192
|
+
lines.append(' <toolspecific tool="OpenProcess" version="1" namesOutside="true"/>')
|
|
193
|
+
lines.append(' <page id="page1">')
|
|
194
|
+
for place in net.places.values():
|
|
195
|
+
tokens = net.initial_marking[place.id]
|
|
196
|
+
marking = (f"<initialMarking><text>{tokens}</text></initialMarking>" if tokens else "")
|
|
197
|
+
lines.append(f" <place id={quoteattr(place.id)}>{name(place)}"
|
|
198
|
+
f"{marking}{graphics(place.id, place.position)}</place>")
|
|
199
|
+
for transition in net.transitions.values():
|
|
200
|
+
tool = ('<toolspecific tool="ProM" version="6.4" activity="$invisible$" '
|
|
201
|
+
'localNodeID="" />' if transition.silent else "")
|
|
202
|
+
lines.append(f" <transition id={quoteattr(transition.id)}>{name(transition)}{tool}"
|
|
203
|
+
f"{graphics(transition.id, transition.position)}</transition>")
|
|
204
|
+
for index, arc in enumerate(net.arcs, 1):
|
|
205
|
+
inscription = (f"<inscription><text>{arc.weight}</text></inscription>"
|
|
206
|
+
if arc.weight != 1 else "")
|
|
207
|
+
bends = "".join(f'<position x="{x:.1f}" y="{y:.1f}"/>' for x, y in arc.points)
|
|
208
|
+
bends = f"<graphics>{bends}</graphics>" if bends else ""
|
|
209
|
+
lines.append(f" <arc id=\"arc{index}\" source={quoteattr(arc.source)} "
|
|
210
|
+
f"target={quoteattr(arc.target)}>{inscription}{bends}</arc>")
|
|
211
|
+
lines.append(" </page>")
|
|
212
|
+
if net.final_marking:
|
|
213
|
+
lines.append(" <finalmarkings><marking>")
|
|
214
|
+
for place_id, tokens in net.final_marking.items():
|
|
215
|
+
lines.append(f" <place idref={quoteattr(place_id)}><text>{tokens}</text></place>")
|
|
216
|
+
lines.append(" </marking></finalmarkings>")
|
|
217
|
+
lines += [" </net>", "</pnml>", ""]
|
|
218
|
+
return "\n".join(lines)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def write_pnml(net: PetriNet, path: str | Path,
|
|
222
|
+
positions: dict[str, tuple[float, float]] | None = None) -> None:
|
|
223
|
+
Path(path).write_text(pnml_string(net, positions), encoding="utf-8")
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Process trees: block-structured models that are sound by construction.
|
|
2
|
+
|
|
3
|
+
A process tree (Leemans, Fahland & van der Aalst) is a tree whose leaves are
|
|
4
|
+
activities or τ (silent), and whose inner nodes are operators:
|
|
5
|
+
|
|
6
|
+
========= ====== =====================================================
|
|
7
|
+
operator symbol meaning
|
|
8
|
+
========= ====== =====================================================
|
|
9
|
+
sequence → execute the children in order, left to right
|
|
10
|
+
choice × execute exactly one child (exclusive choice, XOR)
|
|
11
|
+
parallel ∧ execute all children, interleaved in any order (AND)
|
|
12
|
+
loop ↺ execute the first child ("do"); then, any number of
|
|
13
|
+
times, a redo child followed by the do child again
|
|
14
|
+
========= ====== =====================================================
|
|
15
|
+
|
|
16
|
+
Example: ``→(a, ×(b, c), ↺(d, τ))`` means "a, then b or c, then d one or
|
|
17
|
+
more times".
|
|
18
|
+
|
|
19
|
+
Why the Inductive Miner produces trees rather than nets directly: every tree
|
|
20
|
+
translates into a *sound* workflow net, so the IM can never return a model
|
|
21
|
+
that deadlocks or leaves tokens behind -- unlike the α-algorithm.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import Enum
|
|
28
|
+
|
|
29
|
+
from .petrinet import Marking, PetriNet
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Operator(Enum):
|
|
33
|
+
SEQUENCE = "→"
|
|
34
|
+
XOR = "×"
|
|
35
|
+
PARALLEL = "∧"
|
|
36
|
+
LOOP = "↺"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(eq=False)
|
|
40
|
+
class ProcessTree:
|
|
41
|
+
"""A node: an operator with children, or a leaf (``label`` or τ)."""
|
|
42
|
+
|
|
43
|
+
operator: Operator | None = None
|
|
44
|
+
children: list["ProcessTree"] = field(default_factory=list)
|
|
45
|
+
label: str | None = None # leaves only; None = τ
|
|
46
|
+
|
|
47
|
+
# -- constructors -----------------------------------------------------
|
|
48
|
+
@classmethod
|
|
49
|
+
def leaf(cls, label: str | None) -> "ProcessTree":
|
|
50
|
+
return cls(label=label)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def tau(cls) -> "ProcessTree":
|
|
54
|
+
return cls(label=None)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def node(cls, operator: Operator, children: list["ProcessTree"]) -> "ProcessTree":
|
|
58
|
+
return cls(operator=operator, children=list(children))
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_leaf(self) -> bool:
|
|
62
|
+
return self.operator is None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_tau(self) -> bool:
|
|
66
|
+
return self.is_leaf and self.label is None
|
|
67
|
+
|
|
68
|
+
# -- display ----------------------------------------------------------
|
|
69
|
+
def __str__(self) -> str:
|
|
70
|
+
if self.is_leaf:
|
|
71
|
+
return "τ" if self.label is None else self.label
|
|
72
|
+
return f"{self.operator.value}(" + ", ".join(map(str, self.children)) + ")"
|
|
73
|
+
|
|
74
|
+
__repr__ = __str__
|
|
75
|
+
|
|
76
|
+
def pretty(self, indent: str = "") -> str:
|
|
77
|
+
"""Multi-line indented rendering."""
|
|
78
|
+
if self.is_leaf:
|
|
79
|
+
return indent + str(self)
|
|
80
|
+
lines = [indent + self.operator.value]
|
|
81
|
+
lines += [child.pretty(indent + " ") for child in self.children]
|
|
82
|
+
return "\n".join(lines)
|
|
83
|
+
|
|
84
|
+
def activities(self) -> set[str]:
|
|
85
|
+
if self.is_leaf:
|
|
86
|
+
return set() if self.label is None else {self.label}
|
|
87
|
+
return set().union(*(child.activities() for child in self.children))
|
|
88
|
+
|
|
89
|
+
def simplified(self) -> "ProcessTree":
|
|
90
|
+
"""Flatten nested identical operators: →(a, →(b, c)) becomes →(a, b, c).
|
|
91
|
+
|
|
92
|
+
Loops are not flattened -- ↺ is not associative.
|
|
93
|
+
"""
|
|
94
|
+
if self.is_leaf:
|
|
95
|
+
return self
|
|
96
|
+
children = [child.simplified() for child in self.children]
|
|
97
|
+
flat: list[ProcessTree] = []
|
|
98
|
+
for child in children:
|
|
99
|
+
if (self.operator is not Operator.LOOP and child.operator is self.operator):
|
|
100
|
+
flat.extend(child.children)
|
|
101
|
+
else:
|
|
102
|
+
flat.append(child)
|
|
103
|
+
if len(flat) == 1 and self.operator is not Operator.LOOP:
|
|
104
|
+
return flat[0]
|
|
105
|
+
return ProcessTree.node(self.operator, flat)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Translation to a workflow net
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
def to_petri_net(tree: ProcessTree, name: str = "Process tree") -> PetriNet:
|
|
112
|
+
"""Translate a tree into a sound WF-net, block by block.
|
|
113
|
+
|
|
114
|
+
Every block is built between an *entry* place and an *exit* place:
|
|
115
|
+
|
|
116
|
+
* leaf ``a`` / τ: entry → [a] → exit
|
|
117
|
+
* ``→(C1..Cn)``: chain the children through fresh intermediate places
|
|
118
|
+
* ``×(C1..Cn)``: every child between the same entry and exit
|
|
119
|
+
* ``∧(C1..Cn)``: a τ *split* puts a token in each child's own entry
|
|
120
|
+
place; a τ *join* waits for all children's exit places
|
|
121
|
+
* ``↺(do, r1..rn)``: τ from entry to a loop place ``s``; ``do`` from s to
|
|
122
|
+
``e``; each redo from ``e`` back to ``s``; τ from ``e`` to exit. The
|
|
123
|
+
extra τs keep the loop's places private, so a redo can never re-enter
|
|
124
|
+
a choice made *outside* the loop.
|
|
125
|
+
|
|
126
|
+
Afterwards, τ transitions that merely connect two private places in
|
|
127
|
+
series are removed (Murata's "fusion of series places"), which keeps the
|
|
128
|
+
picture readable without changing behaviour.
|
|
129
|
+
"""
|
|
130
|
+
net = PetriNet(name)
|
|
131
|
+
source = net.add_place("source", id="source")
|
|
132
|
+
sink = net.add_place("sink", id="sink")
|
|
133
|
+
|
|
134
|
+
def build(node: ProcessTree, entry, exit_) -> None:
|
|
135
|
+
if node.is_leaf:
|
|
136
|
+
transition = net.add_transition(node.label)
|
|
137
|
+
net.add_arc(entry, transition)
|
|
138
|
+
net.add_arc(transition, exit_)
|
|
139
|
+
elif node.operator is Operator.SEQUENCE:
|
|
140
|
+
current = entry
|
|
141
|
+
for index, child in enumerate(node.children):
|
|
142
|
+
following = exit_ if index == len(node.children) - 1 else net.add_place()
|
|
143
|
+
build(child, current, following)
|
|
144
|
+
current = following
|
|
145
|
+
elif node.operator is Operator.XOR:
|
|
146
|
+
for child in node.children:
|
|
147
|
+
build(child, entry, exit_)
|
|
148
|
+
elif node.operator is Operator.PARALLEL:
|
|
149
|
+
split = net.add_transition(None, name="τ split")
|
|
150
|
+
join = net.add_transition(None, name="τ join")
|
|
151
|
+
net.add_arc(entry, split)
|
|
152
|
+
net.add_arc(join, exit_)
|
|
153
|
+
for child in node.children:
|
|
154
|
+
child_entry, child_exit = net.add_place(), net.add_place()
|
|
155
|
+
net.add_arc(split, child_entry)
|
|
156
|
+
net.add_arc(child_exit, join)
|
|
157
|
+
build(child, child_entry, child_exit)
|
|
158
|
+
elif node.operator is Operator.LOOP:
|
|
159
|
+
loop_start, loop_end = net.add_place(), net.add_place()
|
|
160
|
+
enter = net.add_transition(None, name="τ enter")
|
|
161
|
+
leave = net.add_transition(None, name="τ exit")
|
|
162
|
+
net.add_arc(entry, enter)
|
|
163
|
+
net.add_arc(enter, loop_start)
|
|
164
|
+
net.add_arc(loop_end, leave)
|
|
165
|
+
net.add_arc(leave, exit_)
|
|
166
|
+
build(node.children[0], loop_start, loop_end)
|
|
167
|
+
for redo in node.children[1:]:
|
|
168
|
+
build(redo, loop_end, loop_start)
|
|
169
|
+
|
|
170
|
+
build(tree, source, sink)
|
|
171
|
+
net.initial_marking = Marking({source.id: 1})
|
|
172
|
+
net.final_marking = Marking({sink.id: 1})
|
|
173
|
+
reduce_silent_transitions(net)
|
|
174
|
+
net.info["algorithm"] = "Process tree"
|
|
175
|
+
return net
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def reduce_silent_transitions(net: PetriNet) -> None:
|
|
179
|
+
"""Remove τ transitions that only connect two places in series.
|
|
180
|
+
|
|
181
|
+
Rule (fusion of series places): if τ has exactly one input place ``p`` and
|
|
182
|
+
one output place ``q``, ``p``'s only consumer is τ, and ``q``'s only
|
|
183
|
+
producer is τ, then firing τ is the *only* thing a token in p can do and
|
|
184
|
+
the *only* way into q -- so p and q can be merged and τ deleted without
|
|
185
|
+
changing the visible behaviour. The source and sink are never merged
|
|
186
|
+
away, so the result is still a WF-net with the same markings.
|
|
187
|
+
"""
|
|
188
|
+
changed = True
|
|
189
|
+
while changed:
|
|
190
|
+
changed = False
|
|
191
|
+
for transition in list(net.transitions.values()):
|
|
192
|
+
if not transition.silent:
|
|
193
|
+
continue
|
|
194
|
+
inputs, outputs = net.preset(transition.id), net.postset(transition.id)
|
|
195
|
+
if len(inputs) != 1 or len(outputs) != 1:
|
|
196
|
+
continue
|
|
197
|
+
(p,), (q,) = inputs, outputs
|
|
198
|
+
if p == q or net.postset(p) != {transition.id} or net.preset(q) != {transition.id}:
|
|
199
|
+
continue
|
|
200
|
+
if any(a.weight != 1 for a in net.arcs
|
|
201
|
+
if transition.id in (a.source, a.target)):
|
|
202
|
+
continue
|
|
203
|
+
# Keep source/sink ids stable: merge the other place into them.
|
|
204
|
+
keep, drop = (q, p) if p not in ("source", "sink") else (p, q)
|
|
205
|
+
if drop in ("source", "sink"):
|
|
206
|
+
continue
|
|
207
|
+
net.remove_transition(transition.id)
|
|
208
|
+
for arc in net.arcs:
|
|
209
|
+
if arc.source == drop:
|
|
210
|
+
arc.source = keep
|
|
211
|
+
if arc.target == drop:
|
|
212
|
+
arc.target = keep
|
|
213
|
+
net.places.pop(drop)
|
|
214
|
+
net._cache = None
|
|
215
|
+
changed = True
|
|
216
|
+
break
|