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.
Files changed (173) hide show
  1. cpnpy/__init__.py +46 -0
  2. openprocess/__init__.py +57 -0
  3. openprocess/analysis/__init__.py +0 -0
  4. openprocess/analysis/state_space.py +521 -0
  5. openprocess/analysis/state_space_process.py +251 -0
  6. openprocess/cli.py +742 -0
  7. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
  8. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
  9. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
  10. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
  11. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
  12. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
  13. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
  14. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
  15. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
  16. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
  17. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
  18. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
  19. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
  20. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
  21. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
  22. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
  23. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
  24. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
  25. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
  26. openprocess/exercises/pack.md +14 -0
  27. openprocess/flow/__init__.py +50 -0
  28. openprocess/flow/box.py +466 -0
  29. openprocess/flow/boxes/__init__.py +7 -0
  30. openprocess/flow/boxes/check.py +119 -0
  31. openprocess/flow/boxes/compare.py +16 -0
  32. openprocess/flow/boxes/cpn.py +53 -0
  33. openprocess/flow/boxes/discover.py +124 -0
  34. openprocess/flow/boxes/filter.py +80 -0
  35. openprocess/flow/boxes/input.py +124 -0
  36. openprocess/flow/boxes/output.py +52 -0
  37. openprocess/flow/boxes/predict.py +186 -0
  38. openprocess/flow/boxes/science.py +159 -0
  39. openprocess/flow/boxes/sweeps.py +18 -0
  40. openprocess/flow/convert.py +187 -0
  41. openprocess/flow/datasets.py +198 -0
  42. openprocess/flow/explain.py +115 -0
  43. openprocess/flow/library.py +222 -0
  44. openprocess/flow/record.py +385 -0
  45. openprocess/flow/runner.py +357 -0
  46. openprocess/flow/sweep.py +92 -0
  47. openprocess/flow/types.py +290 -0
  48. openprocess/flow/workflow.py +628 -0
  49. openprocess/gui/__init__.py +0 -0
  50. openprocess/gui/app.py +90 -0
  51. openprocess/gui/arc_editing.py +295 -0
  52. openprocess/gui/canvas.py +1414 -0
  53. openprocess/gui/flow/__init__.py +8 -0
  54. openprocess/gui/flow/canvas.py +854 -0
  55. openprocess/gui/flow/page.py +972 -0
  56. openprocess/gui/flow/templates.py +131 -0
  57. openprocess/gui/flow/viewers.py +665 -0
  58. openprocess/gui/items.py +1275 -0
  59. openprocess/gui/learn/answer_boxes.py +978 -0
  60. openprocess/gui/learn/concealment.py +91 -0
  61. openprocess/gui/learn/mode.py +1181 -0
  62. openprocess/gui/panning.py +241 -0
  63. openprocess/gui/resources/openprocess-icon.png +0 -0
  64. openprocess/gui/studio/__init__.py +1 -0
  65. openprocess/gui/studio/__main__.py +3 -0
  66. openprocess/gui/studio/app.py +4031 -0
  67. openprocess/gui/studio/charts.py +115 -0
  68. openprocess/gui/studio/compare_page.py +487 -0
  69. openprocess/gui/studio/cpn_page.py +1858 -0
  70. openprocess/gui/studio/definition_view.py +284 -0
  71. openprocess/gui/studio/derivation_view.py +421 -0
  72. openprocess/gui/studio/documents.py +152 -0
  73. openprocess/gui/studio/dotted_chart.py +1401 -0
  74. openprocess/gui/studio/file_dialogs.py +143 -0
  75. openprocess/gui/studio/filter_dialog.py +247 -0
  76. openprocess/gui/studio/graph_builders.py +176 -0
  77. openprocess/gui/studio/graph_view.py +682 -0
  78. openprocess/gui/studio/instances.py +413 -0
  79. openprocess/gui/studio/log_editor.py +675 -0
  80. openprocess/gui/studio/log_page.py +800 -0
  81. openprocess/gui/studio/markdown_view.py +127 -0
  82. openprocess/gui/studio/mathtext.py +260 -0
  83. openprocess/gui/studio/ml_highlighter.py +75 -0
  84. openprocess/gui/studio/model_page.py +760 -0
  85. openprocess/gui/studio/net_comparison.py +124 -0
  86. openprocess/gui/studio/notes_overlay.py +275 -0
  87. openprocess/gui/studio/petri_page.py +844 -0
  88. openprocess/gui/studio/regions_view.py +502 -0
  89. openprocess/gui/studio/sidebar.py +149 -0
  90. openprocess/gui/studio/style.py +503 -0
  91. openprocess/gui/studio/tool_icons.py +134 -0
  92. openprocess/gui/studio/updates.py +439 -0
  93. openprocess/gui/studio/widgets.py +899 -0
  94. openprocess/gui/studio/workers.py +60 -0
  95. openprocess/gui/studio/workspace.py +447 -0
  96. openprocess/gui/theme.py +394 -0
  97. openprocess/gui/tidy.py +86 -0
  98. openprocess/io/__init__.py +0 -0
  99. openprocess/io/cpn_reader.py +389 -0
  100. openprocess/io/cpn_writer.py +357 -0
  101. openprocess/learn/__init__.py +23 -0
  102. openprocess/learn/answers.py +188 -0
  103. openprocess/learn/checks.py +953 -0
  104. openprocess/learn/computed.py +1180 -0
  105. openprocess/learn/context.py +145 -0
  106. openprocess/learn/exam.py +169 -0
  107. openprocess/learn/exercise-packs.md +325 -0
  108. openprocess/learn/importer.py +216 -0
  109. openprocess/learn/notation.py +474 -0
  110. openprocess/learn/pack.py +511 -0
  111. openprocess/learn/sheet.py +296 -0
  112. openprocess/mining/__init__.py +73 -0
  113. openprocess/mining/analysis.py +689 -0
  114. openprocess/mining/columns.py +282 -0
  115. openprocess/mining/compare_nets.py +246 -0
  116. openprocess/mining/conformance/__init__.py +0 -0
  117. openprocess/mining/conformance/alignments.py +263 -0
  118. openprocess/mining/conformance/quality.py +145 -0
  119. openprocess/mining/conformance/token_replay.py +252 -0
  120. openprocess/mining/csv_import.py +222 -0
  121. openprocess/mining/definitions.py +584 -0
  122. openprocess/mining/dfg.py +187 -0
  123. openprocess/mining/discovery/__init__.py +0 -0
  124. openprocess/mining/discovery/alpha.py +168 -0
  125. openprocess/mining/discovery/heuristics.py +332 -0
  126. openprocess/mining/discovery/inductive.py +477 -0
  127. openprocess/mining/discovery/state_regions.py +62 -0
  128. openprocess/mining/filtering.py +237 -0
  129. openprocess/mining/footprint.py +183 -0
  130. openprocess/mining/invariants.py +191 -0
  131. openprocess/mining/layout.py +279 -0
  132. openprocess/mining/log.py +364 -0
  133. openprocess/mining/petrinet.py +354 -0
  134. openprocess/mining/playout.py +75 -0
  135. openprocess/mining/pm4py_bridge.py +82 -0
  136. openprocess/mining/pnml.py +223 -0
  137. openprocess/mining/processtree.py +216 -0
  138. openprocess/mining/regions.py +476 -0
  139. openprocess/mining/stats.py +160 -0
  140. openprocess/mining/structure.py +374 -0
  141. openprocess/mining/transition_system.py +409 -0
  142. openprocess/mining/xes.py +399 -0
  143. openprocess/ml/__init__.py +0 -0
  144. openprocess/ml/ast_nodes.py +332 -0
  145. openprocess/ml/builtins.py +364 -0
  146. openprocess/ml/colorsets.py +522 -0
  147. openprocess/ml/errors.py +60 -0
  148. openprocess/ml/evaluator.py +754 -0
  149. openprocess/ml/lexer.py +277 -0
  150. openprocess/ml/multiset.py +417 -0
  151. openprocess/ml/parser.py +737 -0
  152. openprocess/ml/values.py +319 -0
  153. openprocess/model/__init__.py +0 -0
  154. openprocess/model/declarations.py +617 -0
  155. openprocess/model/examples.py +98 -0
  156. openprocess/model/net.py +701 -0
  157. openprocess/model/plain.py +192 -0
  158. openprocess/references.py +280 -0
  159. openprocess/sim/__init__.py +0 -0
  160. openprocess/sim/binding.py +620 -0
  161. openprocess/sim/export.py +66 -0
  162. openprocess/sim/simulator.py +315 -0
  163. openprocess/teaching/__init__.py +4 -0
  164. openprocess/teaching/answers.py +4 -0
  165. openprocess/teaching/checks.py +5 -0
  166. openprocess/teaching/pack.py +4 -0
  167. openprocess/teaching/sheet.py +4 -0
  168. openprocess-0.7.0.dist-info/METADATA +927 -0
  169. openprocess-0.7.0.dist-info/RECORD +173 -0
  170. openprocess-0.7.0.dist-info/WHEEL +5 -0
  171. openprocess-0.7.0.dist-info/entry_points.txt +6 -0
  172. openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
  173. 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}}"]