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,413 @@
|
|
|
1
|
+
"""The definitions, filled in for the net being analysed.
|
|
2
|
+
|
|
3
|
+
A definition says ``∀M: [i] →* M ⇒ M →* [o]``; for the order-handling net
|
|
4
|
+
the pop-up should also say what that means *there* -- ``i`` is "start", and
|
|
5
|
+
when the property fails, which marking and which firing sequence show it:
|
|
6
|
+
``[start] →⟨register, check stock, reject⟩ [c4, end]`` and
|
|
7
|
+
``[c4, end] ↛* [end]``. Each function here returns such formulas, as LaTeX
|
|
8
|
+
lines for :mod:`.mathtext` (names go in ``\\text{…}`` via
|
|
9
|
+
:func:`.mathtext.name`, so any node name is safe).
|
|
10
|
+
|
|
11
|
+
Everything is computed from the analysis reports; nothing here touches Qt.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from ...mining.analysis import OMEGA, PropertyReport, ShortCircuitReport, SoundnessReport
|
|
17
|
+
from ...mining.petrinet import Marking, PetriNet
|
|
18
|
+
from .mathtext import name, name_set, sequence
|
|
19
|
+
|
|
20
|
+
T_STAR = "t_star"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def node(net: PetriNet, node_id: str) -> str:
|
|
24
|
+
"""A place or transition as it appears in formulas (``t*`` stays maths)."""
|
|
25
|
+
return "t^*" if node_id == T_STAR else name(net.node_name(node_id))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def marking(net: PetriNet, value: Marking) -> str:
|
|
29
|
+
"""``[\\text{c4}, 2\\,\\text{c2}]`` -- the multiset notation of the course."""
|
|
30
|
+
parts = []
|
|
31
|
+
for place, count in sorted(value.items(), key=lambda item: net.node_name(item[0])):
|
|
32
|
+
if count == OMEGA:
|
|
33
|
+
parts.append(f"\\omega\\, {node(net, place)}")
|
|
34
|
+
elif count == 1:
|
|
35
|
+
parts.append(node(net, place))
|
|
36
|
+
elif count:
|
|
37
|
+
parts.append(f"{int(count)}\\, {node(net, place)}")
|
|
38
|
+
return "[" + ", ".join(parts) + "]"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_SIGMA = "\x00sigma:"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def firing(net: PetriNet, path: list[str]) -> str:
|
|
45
|
+
"""The arrow for firing ``path``: labelled with the transition itself for
|
|
46
|
+
one step, and with σ for longer sequences -- σ is then spelled out on a
|
|
47
|
+
line of its own (see :func:`_spell_out`), which reads far better than a
|
|
48
|
+
long superscript."""
|
|
49
|
+
if not path:
|
|
50
|
+
return "\\xrightarrow{\\langle \\rangle}"
|
|
51
|
+
names = ["t*" if t == T_STAR else net.node_name(t) for t in path]
|
|
52
|
+
if len(names) == 1:
|
|
53
|
+
return "\\xrightarrow{" + node(net, path[0]) + "}"
|
|
54
|
+
return "\\xrightarrow{\\sigma}" + _SIGMA + sequence(names, limit=10) + _SIGMA
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _spell_out(function):
|
|
58
|
+
"""Move the σ = ⟨…⟩ that :func:`firing` tucked into a line onto a line of
|
|
59
|
+
its own, after the formulas that use it."""
|
|
60
|
+
def wrapper(*args, **kwargs) -> list[str]:
|
|
61
|
+
out, spelled_out = [], []
|
|
62
|
+
for line in function(*args, **kwargs):
|
|
63
|
+
if _SIGMA not in line:
|
|
64
|
+
out.append(line)
|
|
65
|
+
continue
|
|
66
|
+
before, spelled, after = line.split(_SIGMA, 2)
|
|
67
|
+
out.append(before + after)
|
|
68
|
+
where = f"\\text{{where }} \\sigma = {spelled}"
|
|
69
|
+
if where not in spelled_out:
|
|
70
|
+
spelled_out.append(where)
|
|
71
|
+
return out + spelled_out
|
|
72
|
+
wrapper.__doc__ = function.__doc__
|
|
73
|
+
wrapper.__name__ = function.__name__
|
|
74
|
+
return wrapper
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _transitions(net: PetriNet, ids, limit: int = 6) -> str:
|
|
78
|
+
items = [node(net, t) for t in ids]
|
|
79
|
+
if len(items) > limit:
|
|
80
|
+
items = items[:limit - 1] + ["\\ldots"]
|
|
81
|
+
return "\\lbrace " + ", ".join(items) + " \\rbrace" if items else "\\emptyset"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Soundness
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
@_spell_out
|
|
88
|
+
def soundness(report: SoundnessReport, key: str) -> list[str]:
|
|
89
|
+
"""Formulas for ``wf_net``, ``sound`` and the three soundness conditions."""
|
|
90
|
+
workflow = report.workflow
|
|
91
|
+
net = report.graph.net if report.graph is not None else None
|
|
92
|
+
if key == "wf_net":
|
|
93
|
+
if not workflow.is_workflow_net:
|
|
94
|
+
return ["\\text{" + problem.replace("{", "(").replace("}", ")") + "}"
|
|
95
|
+
for problem in workflow.problems[:3]]
|
|
96
|
+
return _wf(report)
|
|
97
|
+
if not workflow.is_workflow_net or net is None:
|
|
98
|
+
return []
|
|
99
|
+
i, o = node(net, workflow.source), node(net, workflow.sink)
|
|
100
|
+
start, end = f"[{i}]", f"[{o}]"
|
|
101
|
+
graph = report.graph
|
|
102
|
+
if key == "option_to_complete":
|
|
103
|
+
lines = [f"\\forall M \\colon {start} \\xrightarrow{{*}} M \\Rightarrow "
|
|
104
|
+
f"M \\xrightarrow{{*}} {end}"]
|
|
105
|
+
state = report.witness.get("option_to_complete")
|
|
106
|
+
if state is not None:
|
|
107
|
+
m = marking(net, graph.states[state])
|
|
108
|
+
lines += [f"\\text{{but }} {start} {firing(net, graph.path_to(state))} {m}",
|
|
109
|
+
f"\\text{{and }} {m} \\not\\xrightarrow{{*}} {end}"]
|
|
110
|
+
return lines
|
|
111
|
+
if key == "proper_completion":
|
|
112
|
+
lines = [f"\\forall M \\colon {start} \\xrightarrow{{*}} M \\land M \\geq {end} "
|
|
113
|
+
f"\\Rightarrow M = {end}"]
|
|
114
|
+
state = report.witness.get("proper_completion")
|
|
115
|
+
if state is not None:
|
|
116
|
+
m = marking(net, graph.states[state])
|
|
117
|
+
lines += [f"\\text{{but }} {start} {firing(net, graph.path_to(state))} {m}",
|
|
118
|
+
f"\\text{{and }} {m} \\geq {end}, \\; {m} \\neq {end}"]
|
|
119
|
+
return lines
|
|
120
|
+
if key == "no_dead_transitions":
|
|
121
|
+
if report.dead_transitions:
|
|
122
|
+
return [f"\\neg \\exists M \\colon {start} \\xrightarrow{{*}} M \\land "
|
|
123
|
+
f"{node(net, t)} \\text{{ enabled in }} M"
|
|
124
|
+
for t in report.dead_transitions[:3]]
|
|
125
|
+
return [f"\\forall t \\in {_transitions(net, net.transitions)} \\; \\exists M, M' "
|
|
126
|
+
f"\\colon {start} \\xrightarrow{{*}} M \\xrightarrow{{t}} M'"]
|
|
127
|
+
if key == "sound":
|
|
128
|
+
def verdict(value):
|
|
129
|
+
return "\\text{holds}" if value else (
|
|
130
|
+
"\\text{fails}" if value is False else "\\text{undecided}")
|
|
131
|
+
lines = [f"\\text{{(i) }} {verdict(report.option_to_complete)} \\; \\land \\; "
|
|
132
|
+
f"\\text{{(ii) }} {verdict(report.proper_completion)} \\; \\land \\; "
|
|
133
|
+
f"\\text{{(iii) }} {verdict(report.no_dead_transitions)}"]
|
|
134
|
+
if report.bounded is False:
|
|
135
|
+
state = report.witness.get("bounded")
|
|
136
|
+
if state is not None:
|
|
137
|
+
lines.append(f"\\text{{unbounded: }} {start} "
|
|
138
|
+
f"{firing(net, graph.path_to(state))} "
|
|
139
|
+
f"{marking(net, graph.states[state])}")
|
|
140
|
+
return lines
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _wf(report: SoundnessReport) -> list[str]:
|
|
145
|
+
net = report.graph.net if report.graph is not None else None
|
|
146
|
+
workflow = report.workflow
|
|
147
|
+
if net is None:
|
|
148
|
+
return []
|
|
149
|
+
i, o = node(net, workflow.source), node(net, workflow.sink)
|
|
150
|
+
return [f"i = {i}, \\quad o = {o}",
|
|
151
|
+
f"\\bullet {i} = \\emptyset, \\quad {o} \\bullet = \\emptyset"]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# The short-circuited net (soundness theorem)
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
@_spell_out
|
|
158
|
+
def short_circuit(report: SoundnessReport, key: str) -> list[str]:
|
|
159
|
+
closed: ShortCircuitReport | None = report.short_circuit
|
|
160
|
+
if closed is None:
|
|
161
|
+
return []
|
|
162
|
+
net, graph, props = closed.net, closed.properties.graph, closed.properties
|
|
163
|
+
workflow = report.workflow
|
|
164
|
+
i, o = node(net, workflow.source), node(net, workflow.sink)
|
|
165
|
+
marked = f"(\\overline{{N}}, [{i}])"
|
|
166
|
+
if key == "short_circuit":
|
|
167
|
+
return [f"\\overline{{N}} = (P, \\; T \\cup \\lbrace t^* \\rbrace, \\; F \\cup "
|
|
168
|
+
f"\\lbrace ({o}, t^*), (t^*, {i}) \\rbrace)"]
|
|
169
|
+
if key == "live":
|
|
170
|
+
if closed.not_live:
|
|
171
|
+
transition, state = next(iter(closed.not_live.items()))
|
|
172
|
+
m = marking(net, graph.states[state])
|
|
173
|
+
return [f"[{i}] {firing(net, graph.path_to(state))} {m} \\in R{marked[:-1]})",
|
|
174
|
+
f"\\neg \\exists M' \\in R(\\overline{{N}}, {m}) \\colon "
|
|
175
|
+
f"{node(net, transition)} \\text{{ enabled in }} M'"]
|
|
176
|
+
if closed.live:
|
|
177
|
+
return [f"\\forall t \\in T \\cup \\lbrace t^* \\rbrace \\; \\forall M \\in "
|
|
178
|
+
f"R{marked[:-1]}) \\; \\exists M' \\in R(\\overline{{N}}, M) \\colon "
|
|
179
|
+
"t \\text{ enabled in } M'"]
|
|
180
|
+
return ["\\text{not decided: the state space is unbounded or too large}"]
|
|
181
|
+
if key == "bounded":
|
|
182
|
+
return _bounds(net, props, f"R{marked[:-1]})")
|
|
183
|
+
if key == "safe":
|
|
184
|
+
lines = _bounds(net, props, f"R{marked[:-1]})")
|
|
185
|
+
return lines
|
|
186
|
+
if key == "deadlock_free":
|
|
187
|
+
if props.dead_markings:
|
|
188
|
+
state = props.dead_markings[0]
|
|
189
|
+
m = marking(net, graph.states[state])
|
|
190
|
+
return [f"[{i}] {firing(net, graph.path_to(state))} {m}",
|
|
191
|
+
f"\\forall t \\in T \\cup \\lbrace t^* \\rbrace \\colon t "
|
|
192
|
+
f"\\text{{ is not enabled in }} {m}"]
|
|
193
|
+
return [f"\\forall M \\in R{marked[:-1]}) \\; \\exists t \\in T \\cup "
|
|
194
|
+
f"\\lbrace t^* \\rbrace \\colon t \\text{{ enabled in }} M"]
|
|
195
|
+
if key == "soundness_theorem":
|
|
196
|
+
sound = {True: "\\text{sound}", False: "\\text{not sound}",
|
|
197
|
+
None: "\\text{undecided}"}[closed.sound]
|
|
198
|
+
if not closed.bounded:
|
|
199
|
+
return [f"{marked} \\text{{ is unbounded}} \\; \\Rightarrow \\; "
|
|
200
|
+
f"N \\text{{ is }} {sound}"]
|
|
201
|
+
live = {True: "\\text{live}", False: "\\text{not live}",
|
|
202
|
+
None: "\\text{(liveness undecided)}"}[closed.live]
|
|
203
|
+
return [f"{marked} \\text{{ is }} {live} \\land \\text{{bounded}} \\; "
|
|
204
|
+
f"\\Rightarrow \\; N \\text{{ is }} {sound}"]
|
|
205
|
+
return []
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _bounds(net: PetriNet, props: PropertyReport, reachable: str) -> list[str]:
|
|
209
|
+
"""``∀M ∈ R: ∀p: M(p) ≤ k`` or the place that is unbounded / holds most."""
|
|
210
|
+
bounds = props.place_bounds
|
|
211
|
+
if not bounds:
|
|
212
|
+
return []
|
|
213
|
+
if not props.bounded:
|
|
214
|
+
places = [p for p, b in bounds.items() if b == OMEGA][:3]
|
|
215
|
+
return [f"M({node(net, p)}) = \\omega \\text{{ (unbounded)}}" for p in places]
|
|
216
|
+
k = int(props.bound)
|
|
217
|
+
busiest = max(bounds, key=lambda p: bounds[p])
|
|
218
|
+
lines = [f"\\forall M \\in {reachable} \\; \\forall p \\in P \\colon M(p) \\leq {k}"]
|
|
219
|
+
if k > 1:
|
|
220
|
+
lines.append(f"\\text{{e.g. }} M({node(net, busiest)}) = {k} \\text{{ is reachable}}")
|
|
221
|
+
return lines
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# Properties of the net as drawn (from its own initial marking)
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
@_spell_out
|
|
228
|
+
def properties(props: PropertyReport, key: str) -> list[str]:
|
|
229
|
+
net, graph = props.graph.net, props.graph
|
|
230
|
+
m0 = marking(net, graph.states[0]) if graph.states else "[\\,]"
|
|
231
|
+
reach = f"R(N, {m0})"
|
|
232
|
+
if key in ("bounded", "safe"):
|
|
233
|
+
return _bounds(net, props, reach)
|
|
234
|
+
if key == "deadlock_free":
|
|
235
|
+
if props.dead_markings:
|
|
236
|
+
state = props.dead_markings[0]
|
|
237
|
+
m = marking(net, graph.states[state])
|
|
238
|
+
return [f"{m0} {firing(net, graph.path_to(state))} {m}",
|
|
239
|
+
f"\\forall t \\in T \\colon t \\text{{ is not enabled in }} {m}"]
|
|
240
|
+
return [f"\\forall M \\in {reach} \\; \\exists t \\in T \\colon t "
|
|
241
|
+
"\\text{ enabled in } M"]
|
|
242
|
+
if key == "dead_transition":
|
|
243
|
+
if props.dead_transitions:
|
|
244
|
+
return [f"\\neg \\exists M \\in {reach} \\colon {node(net, t)} "
|
|
245
|
+
"\\text{ enabled in } M" for t in props.dead_transitions[:3]]
|
|
246
|
+
return [f"\\forall t \\in {_transitions(net, net.transitions)} \\; \\exists M "
|
|
247
|
+
f"\\in {reach} \\colon t \\text{{ enabled in }} M"]
|
|
248
|
+
if key == "live":
|
|
249
|
+
if props.live_transitions is None:
|
|
250
|
+
return ["\\text{not decided: the state space is unbounded or too large}"]
|
|
251
|
+
dead = [t for t in net.transitions if t not in props.live_transitions]
|
|
252
|
+
if not dead:
|
|
253
|
+
return [f"\\forall t \\in T \\; \\forall M \\in {reach} \\; \\exists M' \\in "
|
|
254
|
+
"R(N, M) \\colon t \\text{ enabled in } M'"]
|
|
255
|
+
return [f"{_transitions(net, dead)} \\text{{ can stop being fireable for good}}"]
|
|
256
|
+
if key == "reversible":
|
|
257
|
+
if props.reversible is None:
|
|
258
|
+
return ["\\text{not decided}"]
|
|
259
|
+
if props.reversible:
|
|
260
|
+
return [f"\\forall M \\in {reach} \\colon M \\xrightarrow{{*}} {m0}"]
|
|
261
|
+
back = graph.backward_reachable({0})
|
|
262
|
+
stuck = next(s for s in range(len(graph.states)) if s not in back)
|
|
263
|
+
m = marking(net, graph.states[stuck])
|
|
264
|
+
return [f"{m0} {firing(net, graph.path_to(stuck))} {m}",
|
|
265
|
+
f"{m} \\not\\xrightarrow{{*}} {m0}"]
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# Structure
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
@_spell_out
|
|
273
|
+
def structure(report: SoundnessReport, net: PetriNet, key: str) -> list[str]:
|
|
274
|
+
result = report.structure
|
|
275
|
+
if result is None:
|
|
276
|
+
return []
|
|
277
|
+
|
|
278
|
+
def pre(t: str) -> str:
|
|
279
|
+
return name_set(net.node_name(p) for p in sorted(net.preset(t)))
|
|
280
|
+
|
|
281
|
+
if key == "free_choice":
|
|
282
|
+
if not result.free_choice:
|
|
283
|
+
return ["\\forall t_1, t_2 \\in T \\colon \\bullet t_1 \\cap \\bullet t_2 "
|
|
284
|
+
"\\neq \\emptyset \\Rightarrow \\bullet t_1 = \\bullet t_2"]
|
|
285
|
+
t1, t2, _ = result.free_choice[0]
|
|
286
|
+
a, b = node(net, t1), node(net, t2)
|
|
287
|
+
shared = name_set(net.node_name(p) for p in sorted(net.preset(t1) & net.preset(t2)))
|
|
288
|
+
return [f"\\bullet {a} \\cap \\bullet {b} = {shared} \\neq \\emptyset",
|
|
289
|
+
f"\\text{{but }} \\bullet {a} = {pre(t1)} \\neq {pre(t2)} = \\bullet {b}"]
|
|
290
|
+
if key == "well_structured":
|
|
291
|
+
if result.handles is None:
|
|
292
|
+
return ["\\text{not checked (the net is very large)}"]
|
|
293
|
+
if not result.handles:
|
|
294
|
+
return ["\\overline{N} \\text{ has no handles: every pair of elementary paths "
|
|
295
|
+
"between a place and a transition meets in between}"]
|
|
296
|
+
handle = result.handles[0]
|
|
297
|
+
|
|
298
|
+
def path(nodes):
|
|
299
|
+
names = [node(net, n[2:]) for n in nodes]
|
|
300
|
+
return "\\langle " + ", ".join(names) + " \\rangle"
|
|
301
|
+
|
|
302
|
+
x, y = node(net, handle.start[2:]), node(net, handle.end[2:])
|
|
303
|
+
return [f"C_1 = {path(handle.paths[0])}",
|
|
304
|
+
f"C_2 = {path(handle.paths[1])}",
|
|
305
|
+
f"\\alpha(C_1) \\cap \\alpha(C_2) = \\lbrace {x}, {y} \\rbrace \\land "
|
|
306
|
+
f"C_1 \\neq C_2 \\quad \\text{{({handle.kind}-handle)}}"]
|
|
307
|
+
if key == "s_coverable":
|
|
308
|
+
coverage = result.coverage
|
|
309
|
+
if coverage is None:
|
|
310
|
+
return ["\\text{not checked (the net is very large)}"]
|
|
311
|
+
lines = []
|
|
312
|
+
for number, component in enumerate(coverage.components[:4], start=1):
|
|
313
|
+
places = sorted(net.node_name(p) for p in component)
|
|
314
|
+
lines.append(f"P_{{{number}}} = {name_set(places)}")
|
|
315
|
+
for missing in coverage.uncovered[:3]:
|
|
316
|
+
lines.append(f"\\neg \\exists \\text{{ S-component }} N_s \\colon "
|
|
317
|
+
f"{node(net, missing)} \\in N_s")
|
|
318
|
+
return lines
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# Transition systems and regions
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
def _states(ts, states, limit: int = 8) -> str:
|
|
326
|
+
"""``{s0, s2}`` in the transition system's own order."""
|
|
327
|
+
position = {s: i for i, s in enumerate(ts.states)}
|
|
328
|
+
items = sorted(states, key=lambda s: position.get(s, len(position)))
|
|
329
|
+
if not items:
|
|
330
|
+
return "\\emptyset"
|
|
331
|
+
shown = [name(s) for s in items[:limit]] + (["\\ldots"] if len(items) > limit else [])
|
|
332
|
+
return "\\lbrace " + ", ".join(shown) + " \\rbrace"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def regions(analysis, key: str, synthesis=None) -> list[str]:
|
|
336
|
+
"""Formulas for the region definitions, filled in for ``analysis.ts``."""
|
|
337
|
+
ts = analysis.ts
|
|
338
|
+
events = list(analysis.by_event.values())
|
|
339
|
+
if key == "transition_system":
|
|
340
|
+
start = name(ts.initial_state) if ts.initial_state else "\\text{(" + \
|
|
341
|
+
str(len(ts.initial)) + " initial states)}"
|
|
342
|
+
return [f"|S| = {len(ts.states)}, \\quad E = "
|
|
343
|
+
+ "\\lbrace " + ", ".join(name(e) for e in ts.events[:10])
|
|
344
|
+
+ (", \\ldots" if len(ts.events) > 10 else "") + " \\rbrace"
|
|
345
|
+
+ f", \\quad |T| = {len(ts.transitions)}",
|
|
346
|
+
f"s_{{in}} = {start}"]
|
|
347
|
+
if key == "minimal_region":
|
|
348
|
+
lines = [f"\\text{{{len(analysis.regions)} non-trivial regions, "
|
|
349
|
+
f"{len(analysis.minimal)} minimal}}"]
|
|
350
|
+
for number, region in enumerate(analysis.minimal[:6], 1):
|
|
351
|
+
lines.append(f"r_{{{number}}} = {_states(ts, region)}")
|
|
352
|
+
return lines
|
|
353
|
+
if key == "pre_region":
|
|
354
|
+
lines = []
|
|
355
|
+
for item in events[:5]:
|
|
356
|
+
pre = ", ".join(f"r_{{{analysis.minimal.index(r) + 1}}}" for r in item.minimal_pre)
|
|
357
|
+
post = ", ".join(f"r_{{{analysis.minimal.index(r) + 1}}}" for r in item.minimal_post)
|
|
358
|
+
lines.append(f"\\text{{pre}}({name(item.event)}) \\cap \\text{{minimal}} = "
|
|
359
|
+
f"\\lbrace {pre or ' '} \\rbrace, \\quad \\text{{post}}"
|
|
360
|
+
f"({name(item.event)}) \\cap \\text{{minimal}} = "
|
|
361
|
+
f"\\lbrace {post or ' '} \\rbrace")
|
|
362
|
+
return lines
|
|
363
|
+
if key == "ger":
|
|
364
|
+
return [f"\\text{{GER}}({name(item.event)}) = {_states(ts, item.ger)}"
|
|
365
|
+
for item in events[:6]]
|
|
366
|
+
if key == "forward_closure":
|
|
367
|
+
lines = []
|
|
368
|
+
failing = [item for item in events if not item.forward_closed]
|
|
369
|
+
for item in (failing or events)[:5]:
|
|
370
|
+
relation = "=" if item.forward_closed else "\\neq"
|
|
371
|
+
lines.append(f"\\bigcap \\text{{pre}}({name(item.event)}) = "
|
|
372
|
+
f"{_states(ts, item.intersection)} {relation} "
|
|
373
|
+
f"\\text{{GER}}({name(item.event)}) = {_states(ts, item.ger)}")
|
|
374
|
+
return lines
|
|
375
|
+
if key == "state_separation":
|
|
376
|
+
if analysis.inseparable:
|
|
377
|
+
return [f"{name(pair.first)} \\neq {name(pair.second)}, \\text{{ but every region "
|
|
378
|
+
f"contains both or neither}}" for pair in analysis.inseparable[:4]]
|
|
379
|
+
return [f"\\text{{all {len(ts.states)} states lie in different sets of regions}}"]
|
|
380
|
+
if key == "elementary_ts":
|
|
381
|
+
def verdict(value):
|
|
382
|
+
return "\\text{holds}" if value else (
|
|
383
|
+
"\\text{fails}" if value is False else "\\text{undecided}")
|
|
384
|
+
return [f"\\text{{state separation }} {verdict(analysis.state_separation)} \\; "
|
|
385
|
+
f"\\land \\; \\text{{forward closure }} {verdict(analysis.forward_closure)}"]
|
|
386
|
+
if key == "region_synthesis" and synthesis is not None and synthesis.net is not None:
|
|
387
|
+
net = synthesis.net
|
|
388
|
+
marked = [p for p in net.initial_marking]
|
|
389
|
+
lines = [f"|P| = {len(net.places)}, \\quad |T| = {len(net.transitions)}, \\quad "
|
|
390
|
+
f"M_0 = [" + ", ".join(f"p_{{r_{{{p[1:]}}}}}" for p in marked) + "]"]
|
|
391
|
+
if synthesis.isomorphic is not None:
|
|
392
|
+
lines.append("RG(N, M_0) \\cong TS" if synthesis.isomorphic else
|
|
393
|
+
"RG(N, M_0) \\text{ and } TS \\text{ are not isomorphic}")
|
|
394
|
+
return lines
|
|
395
|
+
return []
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def region_check(check, ts) -> list[str]:
|
|
399
|
+
"""The definition of a region filled in for one set of states."""
|
|
400
|
+
if check.unknown:
|
|
401
|
+
return []
|
|
402
|
+
region = _states(ts, check.states)
|
|
403
|
+
if check.is_region:
|
|
404
|
+
parts = []
|
|
405
|
+
for event, what in list(check.events.items())[:6]:
|
|
406
|
+
word = {"enter": "enter", "exit": "exit"}.get(what, "nocross")
|
|
407
|
+
parts.append(f"\\text{{{word}}}({name(event)}, R)")
|
|
408
|
+
return [f"R = {region}", " \\land ".join(parts)]
|
|
409
|
+
(s1, t1, _), (s2, t2, _) = check.conflict[:2]
|
|
410
|
+
e = name(check.event)
|
|
411
|
+
return [f"R = {region}",
|
|
412
|
+
f"({name(s1)}, {e}, {name(t1)}) \\in T \\text{{ and }} ({name(s2)}, {e}, "
|
|
413
|
+
f"{name(t2)}) \\in T \\text{{ cross }} R \\text{{ differently}}"]
|