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,421 @@
1
+ """Typeset derivations: how a discovery algorithm arrived at its model.
2
+
3
+ α-algorithm
4
+ The eight steps as a numbered list of formulas, typeset like the book:
5
+ ``T_L``, ``X_L``, ``Y_L`` with subscripts, sets in braces, one (A, B)
6
+ pair per line, and places written as ``p_(A,B)``.
7
+
8
+ Inductive Miner
9
+ Three views of the same result:
10
+
11
+ * the **process tree drawn as a tree** (operators as circles, activities
12
+ as boxes, τ as a black bar);
13
+ * the tree as a **formula** with the operators highlighted;
14
+ * the **recursion** as an expandable outline: every cut with its
15
+ partition, every base case and fall-through, nested exactly as the
16
+ algorithm recursed.
17
+
18
+ State-based regions
19
+ The transition system, its regions and the elementary checks
20
+ (:class:`.regions_view.RegionsPanel`).
21
+
22
+ Rendering uses Qt's rich-text engine (a subset of HTML): subscripts, italics
23
+ and a serif maths font give readable formulas without a browser engine.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from html import escape
29
+
30
+ from PySide6.QtCore import QSize, Qt, QTimer
31
+ from PySide6.QtGui import QAbstractTextDocumentLayout, QColor, QPalette, QTextDocument
32
+ from PySide6.QtWidgets import (
33
+ QFrame, QLabel, QSizePolicy, QStyle, QStyledItemDelegate, QStyleOptionViewItem, QTreeWidget,
34
+ QTreeWidgetItem, QVBoxLayout, QWidget,
35
+ )
36
+
37
+ from ...mining.discovery.alpha import AlphaResult
38
+ from ...mining.discovery.inductive import InductiveResult
39
+ from ...mining.processtree import Operator, ProcessTree
40
+ from . import style
41
+ from .graph_view import EdgeSpec, GraphView, NodeSpec, node_size
42
+ from .widgets import Verdict, label
43
+
44
+ MATH_FONT = ("'STIX Two Math', 'STIX Two Text', 'Cambria Math', 'Latin Modern Math', "
45
+ "'Times New Roman', serif")
46
+ OPERATOR_NAMES = {"→": "sequence", "×": "exclusive choice", "∧": "parallel", "↺": "loop"}
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Small typesetting helpers (return HTML fragments)
51
+ # ---------------------------------------------------------------------------
52
+ def _var(name: str, sub: str = "L") -> str:
53
+ """An italic maths symbol with a subscript, e.g. T_L."""
54
+ return f"<i>{escape(name)}</i><sub>{escape(sub)}</sub>"
55
+
56
+
57
+ def _activity(name: str) -> str:
58
+ """Activity names are words, set upright in the UI font."""
59
+ return f"<span style='font-family: sans-serif'>{escape(name)}</span>"
60
+
61
+
62
+ def _set(items) -> str:
63
+ items = sorted(items)
64
+ if not items:
65
+ return "∅"
66
+ return "{" + ", ".join(_activity(i) for i in items) + "}"
67
+
68
+
69
+ def _pair(pair) -> str:
70
+ a, b = pair
71
+ return f"({_set(a)}, {_set(b)})"
72
+
73
+
74
+ def _place(pair) -> str:
75
+ return f"<i>p</i><sub>{_pair(pair)}</sub>"
76
+
77
+
78
+ def _rich(html: str, size: int = 14) -> QLabel:
79
+ t = style.tokens()
80
+ widget = QLabel()
81
+ widget.setTextFormat(Qt.RichText)
82
+ widget.setWordWrap(True)
83
+ widget.setTextInteractionFlags(Qt.TextSelectableByMouse)
84
+ widget.setText(f"<div style='font-family: {MATH_FONT}; font-size: {size}px; "
85
+ f"color: {t.text}'>{html}</div>")
86
+ widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
87
+ return widget
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # α-algorithm
92
+ # ---------------------------------------------------------------------------
93
+ def alpha_view(result: AlphaResult) -> QWidget:
94
+ t = style.tokens()
95
+ host = QWidget()
96
+ layout = QVBoxLayout(host)
97
+ layout.setContentsMargins(0, 0, 0, 0)
98
+ layout.setSpacing(6)
99
+ for warning in result.warnings:
100
+ layout.addWidget(Verdict("Limitation", "warning", warning))
101
+
102
+ maximal = set(result.Y_L)
103
+ x_lines = "<br>".join(
104
+ (f"<b>{_pair(p)}</b>" if p in maximal else _pair(p)) for p in result.X_L) or "∅"
105
+ y_lines = "<br>".join(_pair(p) for p in result.Y_L) or "∅"
106
+ places = ", ".join([f"<i>i</i><sub>L</sub>"] + [_place(p) for p in result.Y_L]
107
+ + [f"<i>o</i><sub>L</sub>"])
108
+ arcs = []
109
+ for arc in result.net.arcs:
110
+ arcs.append(f"({escape(result.net.node_name(arc.source))}, "
111
+ f"{escape(result.net.node_name(arc.target))})")
112
+ steps = [
113
+ ("1", "all activities", f"{_var('T')} = {_set(result.T_L)}"),
114
+ ("2", "start activities", f"{_var('T', 'I')} = {_set(result.T_I)}"),
115
+ ("3", "end activities", f"{_var('T', 'O')} = {_set(result.T_O)}"),
116
+ ("4", "candidate pairs: A → B, members of A and of B pairwise # "
117
+ "(maximal ones in bold)", f"{_var('X')} = {{<br>{x_lines}<br>}}"),
118
+ ("5", "maximal pairs", f"{_var('Y')} = {{<br>{y_lines}<br>}}"),
119
+ ("6", "places: one per maximal pair, plus source and sink",
120
+ f"{_var('P')} = {{ {places} }}"),
121
+ ("7", f"flow relation: {len(result.net.arcs)} arcs",
122
+ f"{_var('F')} = {{ " + ", ".join(escape(a) for a in arcs[:40])
123
+ + (" …" if len(arcs) > 40 else "") + " }"),
124
+ ("8", "the discovered net", f"α(<i>L</i>) = ({_var('P')}, {_var('T')}, {_var('F')})"),
125
+ ]
126
+ rows = []
127
+ for number, caption, formula in steps:
128
+ rows.append(
129
+ f"<tr><td valign='top' style='padding: 6px 10px 6px 0; color: {t.text_muted}; "
130
+ f"font-family: sans-serif; font-size: 12px'><b>{number}</b></td>"
131
+ f"<td style='padding: 6px 0'>{formula}<br><span style='font-family: sans-serif; "
132
+ f"font-size: 11px; color: {t.text_muted}'>{escape(caption)}</span></td></tr>")
133
+ layout.addWidget(_rich("<table cellspacing='0' width='100%'>" + "".join(rows) + "</table>"))
134
+ layout.addStretch(1)
135
+ return host
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Process trees
140
+ # ---------------------------------------------------------------------------
141
+ def tree_formula(tree: ProcessTree) -> str:
142
+ """The tree as a formula with coloured operators (HTML)."""
143
+ accent = style.tokens().accent
144
+
145
+ def render(node: ProcessTree) -> str:
146
+ if node.is_leaf:
147
+ return "<i>τ</i>" if node.label is None else _activity(node.label)
148
+ inner = ", ".join(render(c) for c in node.children)
149
+ return f"<b style='color: {accent}'>{node.operator.value}</b>({inner})"
150
+ return render(tree)
151
+
152
+
153
+ #: Space between neighbouring subtrees, and between one level and the next.
154
+ TREE_SIBLING_GAP = 18.0
155
+ TREE_LEVEL_GAP = 56.0
156
+ #: The drawn tree's height on the Discover tab: at least, at most (pixels).
157
+ TREE_VIEW_HEIGHT = (160, 420)
158
+
159
+
160
+ def tree_specs(tree: ProcessTree) -> tuple[list[NodeSpec], list[EdgeSpec],
161
+ dict[str, tuple[float, float]]]:
162
+ """Nodes, edges and positions for drawing the tree with the graph canvas.
163
+
164
+ Drawn the way a process tree is in the course: the root at the top, each
165
+ operator's children in a row underneath it, in order (left to right is the
166
+ order of a sequence), each parent centred over its children, with plain
167
+ lines and τ as a leaf like any other (not the Petri net's black bar).
168
+ """
169
+ t = style.tokens()
170
+ nodes: list[NodeSpec] = []
171
+ edges: list[EdgeSpec] = []
172
+ #: Each node's children, and how wide its subtree is.
173
+ children: dict[str, list[str]] = {}
174
+ widths: dict[str, float] = {}
175
+ sizes: dict[str, tuple[float, float]] = {}
176
+ counter = [0]
177
+
178
+ def visit(node: ProcessTree) -> str:
179
+ counter[0] += 1
180
+ node_id = f"n{counter[0]}"
181
+ if node.is_leaf:
182
+ if node.label is None:
183
+ spec = NodeSpec(node_id, "transition", text="τ", tooltip="τ (silent step)")
184
+ else:
185
+ spec = NodeSpec(node_id, "transition", text=node.label,
186
+ tooltip=f"activity {node.label}")
187
+ else:
188
+ symbol = node.operator.value
189
+ spec = NodeSpec(node_id, "operator", text=symbol, fill=t.accent_soft,
190
+ stroke=t.accent, tooltip=f"{symbol} {OPERATOR_NAMES[symbol]}")
191
+ nodes.append(spec)
192
+ sizes[node_id] = node_size(spec)
193
+ children[node_id] = []
194
+ for child in node.children:
195
+ child_id = visit(child)
196
+ children[node_id].append(child_id)
197
+ # A plain line: a tree's edge means "contains", not "goes to".
198
+ edges.append(EdgeSpec(node_id, child_id, width=1.2, colour=t.text_muted,
199
+ arrow=False))
200
+ below = [widths[c] for c in children[node_id]]
201
+ widths[node_id] = max(sizes[node_id][0],
202
+ sum(below) + TREE_SIBLING_GAP * max(0, len(below) - 1))
203
+ return node_id
204
+
205
+ root = visit(tree)
206
+ # Each level is as tall as its tallest node, so rows line up.
207
+ depth_of: dict[str, int] = {}
208
+ heights: dict[int, float] = {}
209
+
210
+ def depths(node_id: str, depth: int) -> None:
211
+ depth_of[node_id] = depth
212
+ heights[depth] = max(heights.get(depth, 0.0), sizes[node_id][1])
213
+ for child in children[node_id]:
214
+ depths(child, depth + 1)
215
+
216
+ depths(root, 0)
217
+ tops, y = {}, 0.0
218
+ for depth in sorted(heights):
219
+ tops[depth] = y
220
+ y += heights[depth] + TREE_LEVEL_GAP
221
+ positions: dict[str, tuple[float, float]] = {}
222
+
223
+ def place(node_id: str, left: float) -> None:
224
+ depth = depth_of[node_id]
225
+ positions[node_id] = (left + widths[node_id] / 2, tops[depth] + heights[depth] / 2)
226
+ below = children[node_id]
227
+ span = sum(widths[c] for c in below) + TREE_SIBLING_GAP * max(0, len(below) - 1)
228
+ x = left + (widths[node_id] - span) / 2
229
+ for child in below:
230
+ place(child, x)
231
+ x += widths[child] + TREE_SIBLING_GAP
232
+
233
+ place(root, 0.0)
234
+ return nodes, edges, positions
235
+
236
+
237
+ class TreeView(GraphView):
238
+ """The drawn process tree: as tall as the tree is at the width it gets.
239
+
240
+ Stretched to fill the column instead, the canvas ran past the bottom of
241
+ the card (taking its zoom bar with it), with the tree a sliver in the
242
+ middle.
243
+ """
244
+
245
+ def fit_height(self) -> None:
246
+ rect = self.graph.sceneRect()
247
+ if rect.isEmpty():
248
+ return
249
+ scale = min(self.viewport().width() / rect.width(), 1.4) if self.width() > 1 else 1.0
250
+ height = rect.height() * scale + 2 * self.frameWidth()
251
+ height = int(min(max(height, TREE_VIEW_HEIGHT[0]), TREE_VIEW_HEIGHT[1]))
252
+ if height != self.height():
253
+ self.setFixedHeight(height)
254
+ # The zoom bar is placed for the old height until the layout has
255
+ # applied the new one: place it again once it has.
256
+ QTimer.singleShot(0, self._place_controls)
257
+ self.fit()
258
+
259
+ def resizeEvent(self, event) -> None: # noqa: N802
260
+ super().resizeEvent(event)
261
+ if event.size().width() != event.oldSize().width():
262
+ self.fit_height()
263
+
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # Rich-text rows in a tree widget
267
+ # ---------------------------------------------------------------------------
268
+ class HtmlDelegate(QStyledItemDelegate):
269
+ """Paints an item's text as rich text (HTML) instead of plain text."""
270
+
271
+ def _document(self, option, index) -> QTextDocument:
272
+ document = QTextDocument()
273
+ document.setDefaultFont(option.font)
274
+ document.setHtml(index.data(Qt.DisplayRole) or "")
275
+ document.setTextWidth(max(option.rect.width(), 200))
276
+ return document
277
+
278
+ def paint(self, painter, option, index) -> None:
279
+ options = QStyleOptionViewItem(option)
280
+ self.initStyleOption(options, index)
281
+ html = options.text
282
+ options.text = ""
283
+ style_ = options.widget.style() if options.widget else None
284
+ if style_ is not None:
285
+ style_.drawControl(QStyle.CE_ItemViewItem, options, painter, options.widget)
286
+ document = QTextDocument()
287
+ document.setDefaultFont(options.font)
288
+ document.setHtml(html)
289
+ document.setTextWidth(options.rect.width())
290
+ painter.save()
291
+ painter.translate(options.rect.topLeft())
292
+ context = QAbstractTextDocumentLayout.PaintContext()
293
+ context.palette.setColor(QPalette.Text, QColor(style.tokens().text))
294
+ document.documentLayout().draw(painter, context)
295
+ painter.restore()
296
+
297
+ def sizeHint(self, option, index) -> QSize: # noqa: N802
298
+ document = QTextDocument()
299
+ document.setDefaultFont(option.font)
300
+ document.setHtml(index.data(Qt.DisplayRole) or "")
301
+ # Wrap at the width the row will really get, or rows overlap.
302
+ width = option.rect.width()
303
+ widget = option.widget
304
+ if widget is not None and hasattr(widget, "viewport"):
305
+ indent = 0
306
+ if hasattr(widget, "indentation"):
307
+ depth, parent = 0, index.parent()
308
+ while parent.isValid():
309
+ depth, parent = depth + 1, parent.parent()
310
+ indent = widget.indentation() * (depth + 1)
311
+ width = widget.viewport().width() - indent - 6
312
+ document.setTextWidth(max(width, 200))
313
+ return QSize(int(document.idealWidth()), int(document.size().height()) + 6)
314
+
315
+
316
+ def _step_html(step) -> str:
317
+ t = style.tokens()
318
+ accent, muted = t.accent, t.text_muted
319
+ if step.kind == "cut":
320
+ groups = " &nbsp;|&nbsp; ".join(_set(g) for g in step.groups or [])
321
+ where = " <span style='color:%s'>(on the filtered DFG)</span>" % muted if step.filtered else ""
322
+ return (f"<b style='color:{accent}; font-size: 15px'>{step.operator}</b> "
323
+ f"<b>{OPERATOR_NAMES.get(step.operator, '')} cut</b>{where}<br>"
324
+ f"<span style='font-family:{MATH_FONT}'>{groups}</span>")
325
+ if step.kind == "base":
326
+ leaf = "<i>τ</i>" if step.result == "τ" else _activity(step.result or "")
327
+ return f"<b>base case</b> → {leaf}"
328
+ if step.kind == "filter":
329
+ return f"<b>IMf filter</b> <span style='color:{muted}'>{escape(step.detail or step.text)}</span>"
330
+ operator = f"<b style='color:{accent}'>{step.operator}</b> " if step.operator else ""
331
+ return (f"{operator}<b>fall-through</b> <span style='color:{muted}'>"
332
+ f"{escape(step.detail or step.text)}</span>")
333
+
334
+
335
+ def inductive_view(result: InductiveResult, draw_tree: bool = True) -> QWidget:
336
+ host = QWidget()
337
+ layout = QVBoxLayout(host)
338
+ layout.setContentsMargins(0, 0, 0, 0)
339
+ layout.setSpacing(8)
340
+
341
+ layout.addWidget(label("PROCESS TREE", "sectionLabel"))
342
+ layout.addWidget(_rich(tree_formula(result.tree), 15))
343
+ if draw_tree:
344
+ view = TreeView()
345
+ nodes, edges, positions = tree_specs(result.tree)
346
+ view.graph.populate(nodes, edges, positions=positions)
347
+ view.fit_height()
348
+ layout.addWidget(view)
349
+
350
+ layout.addWidget(label("RECURSION", "sectionLabel"))
351
+ tree = QTreeWidget()
352
+ tree.setHeaderHidden(True)
353
+ tree.setItemDelegate(HtmlDelegate(tree))
354
+ tree.setUniformRowHeights(False)
355
+ tree.setFrameShape(QFrame.NoFrame)
356
+ tree.setMinimumHeight(200)
357
+ stack: list[tuple[int, QTreeWidgetItem | QTreeWidget]] = [(-1, tree)]
358
+ for step in result.steps:
359
+ while stack and stack[-1][0] >= step.depth:
360
+ stack.pop()
361
+ parent = stack[-1][1]
362
+ item = QTreeWidgetItem(parent, [_step_html(step)]) if isinstance(parent, QTreeWidgetItem) \
363
+ else QTreeWidgetItem(tree, [_step_html(step)])
364
+ item.setToolTip(0, step.text)
365
+ stack.append((step.depth, item))
366
+ tree.expandAll()
367
+ layout.addWidget(tree, 1)
368
+ return host
369
+
370
+
371
+ def heuristics_view(result) -> QWidget:
372
+ """The causal net behind a Heuristics Miner Petri net: each activity's
373
+ input and output bindings, with how often the log used them."""
374
+ from ...mining.discovery.heuristics import END, START
375
+ host = QWidget()
376
+ layout = QVBoxLayout(host)
377
+ layout.setContentsMargins(0, 0, 0, 0)
378
+ layout.setSpacing(8)
379
+ graph = result.graph
380
+ layout.addWidget(label(
381
+ f"1. Dependency graph: {len(graph.edges)} arcs a → b with a ⇒ b ≥ "
382
+ f"{result.dependency_threshold:g}. 2. Bindings, from replaying the log: which "
383
+ "predecessors each activity waited for (in) and which successors it enabled (out). "
384
+ "A binding of several activities is an AND; different bindings are an XOR. "
385
+ "3. One place per arc, and a silent transition per binding where there is a choice.",
386
+ "muted", wrap=True))
387
+ layout.addWidget(label("BINDINGS", "sectionLabel"))
388
+
389
+ def shown(binding) -> str:
390
+ names = sorted({"start" if a == START else "end" if a == END else a for a in binding})
391
+ return "{" + ", ".join(escape(n) for n in names) + "}"
392
+
393
+ causal = result.causal
394
+ activities = sorted((set(causal.inputs) | set(causal.outputs)) - {START, END})
395
+ rows = []
396
+ for activity in activities:
397
+ ins = " or ".join(f"{shown(b)} ×{n}" for b, n in causal.inputs.get(activity, {}).most_common())
398
+ outs = " or ".join(f"{shown(b)} ×{n}" for b, n in causal.outputs.get(activity, {}).most_common())
399
+ rows.append(f"<b>{escape(activity)}</b><br>&nbsp;&nbsp;in: {ins or '–'}"
400
+ f"<br>&nbsp;&nbsp;out: {outs or '–'}")
401
+ layout.addWidget(_rich("<br>".join(rows), 13))
402
+ layout.addStretch(1)
403
+ return host
404
+
405
+
406
+ def derivation_view(payload, draw_tree: bool = True) -> QWidget:
407
+ from ...mining.discovery.heuristics import HeuristicsResult
408
+ if isinstance(payload, AlphaResult):
409
+ return alpha_view(payload)
410
+ if isinstance(payload, InductiveResult):
411
+ return inductive_view(payload, draw_tree)
412
+ if isinstance(payload, HeuristicsResult):
413
+ return heuristics_view(payload)
414
+ from ...mining.discovery.state_regions import RegionResult
415
+ if isinstance(payload, RegionResult):
416
+ from .regions_view import RegionsPanel
417
+ # The net is already drawn beside it (Discover tab, model page).
418
+ return RegionsPanel(payload, show_net=False, flat=True)
419
+ info = getattr(payload, "info", {}) or {}
420
+ return label("\n".join(f"{k}: {v}" for k, v in info.items()) or "No details.", "muted",
421
+ wrap=True, selectable=True)
@@ -0,0 +1,152 @@
1
+ """Open documents: the objects the sidebar lists.
2
+
3
+ A *document* wraps one thing the user opened or produced -- an event log or
4
+ a Petri net -- together with the derived data the views need (statistics,
5
+ activity colours) so that switching between pages never recomputes them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import itertools
11
+ from dataclasses import dataclass, field
12
+
13
+ from ...mining.log import Classifier, EventLog
14
+ from ...mining.petrinet import PetriNet
15
+ from ...mining.stats import LogSummary, summarise
16
+ from .style import ActivityColours
17
+
18
+ _ids = itertools.count(1)
19
+
20
+
21
+ @dataclass(eq=False)
22
+ class LogDocument:
23
+ log: EventLog
24
+ path: str | None = None
25
+ id: int = field(default_factory=lambda: next(_ids))
26
+ #: Its file was deleted or moved away while it was open.
27
+ missing: bool = False
28
+ #: The text the log was written in, for a log typed in the course's
29
+ #: notation or read from a ``….log.txt`` file (None for XES and CSV logs).
30
+ notation: str | None = None
31
+
32
+ def __post_init__(self) -> None:
33
+ self.classifier: Classifier = self.log.default_classifier()
34
+ self._summary: LogSummary | None = None
35
+ self._colours: ActivityColours | None = None
36
+
37
+ @property
38
+ def name(self) -> str:
39
+ return self.log.name
40
+
41
+ def replace_log(self, log: EventLog) -> None:
42
+ """The file was read again: show the new log (same classifier if it still applies)."""
43
+ classifier = self.classifier
44
+ self.log = log
45
+ self.classifier = (classifier if classifier in log.available_classifiers()
46
+ else log.default_classifier())
47
+ self._summary = None
48
+ self._colours = None
49
+
50
+ def set_classifier(self, classifier: Classifier) -> None:
51
+ self.classifier = classifier
52
+ self._summary = None
53
+ self._colours = None
54
+
55
+ @property
56
+ def summary(self) -> LogSummary:
57
+ if self._summary is None:
58
+ self._summary = summarise(self.log, self.classifier)
59
+ return self._summary
60
+
61
+ @property
62
+ def colours(self) -> ActivityColours:
63
+ if self._colours is None:
64
+ self._colours = ActivityColours([a.name for a in self.summary.activities])
65
+ return self._colours
66
+
67
+ def simple_log(self):
68
+ return self.log.simple_log(self.classifier)
69
+
70
+
71
+ @dataclass(eq=False)
72
+ class ModelDocument:
73
+ net: PetriNet
74
+ path: str | None = None
75
+ #: Where it came from: "α-algorithm on <log>", "PNML file", ...
76
+ origin: str = ""
77
+ #: Algorithm-specific extras (AlphaResult, InductiveResult, ...)
78
+ derivation: object | None = None
79
+ source_log: LogDocument | None = None
80
+ id: int = field(default_factory=lambda: next(_ids))
81
+ #: Its file was deleted or moved away while it was open.
82
+ missing: bool = False
83
+
84
+ @property
85
+ def name(self) -> str:
86
+ return self.net.name
87
+
88
+
89
+ @dataclass(eq=False)
90
+ class CpnDocument:
91
+ """A coloured Petri net (CPN Tools model) being edited and simulated."""
92
+
93
+ net: object # openprocess.model.net.CPNet
94
+ path: str | None = None
95
+ id: int = field(default_factory=lambda: next(_ids))
96
+ #: Edited since it was opened or last saved.
97
+ dirty: bool = False
98
+ #: Edits are saved by themselves (a file in the open folder): no "edited" mark.
99
+ autosave: bool = False
100
+ #: Its file was deleted or moved away while it was open.
101
+ missing: bool = False
102
+
103
+ @property
104
+ def name(self) -> str:
105
+ return self.net.name
106
+
107
+
108
+ @dataclass(eq=False)
109
+ class ComparisonDocument:
110
+ """Two or more open logs shown side by side (nothing is stored on disk)."""
111
+
112
+ logs: list
113
+ id: int = field(default_factory=lambda: next(_ids))
114
+ path: str | None = None
115
+
116
+ @property
117
+ def name(self) -> str:
118
+ if len(self.logs) == 2:
119
+ return f"{self.logs[0].name} vs {self.logs[1].name}"
120
+ return f"{len(self.logs)} logs compared"
121
+
122
+
123
+ @dataclass(eq=False)
124
+ class WorkflowDocument:
125
+ """A workflow of boxes (a ``.cpnflow`` file), with the record of its last save."""
126
+
127
+ workflow: object # openprocess.flow.workflow.Workflow
128
+ path: str | None = None
129
+ record: object | None = None # openprocess.flow.record.Record
130
+ id: int = field(default_factory=lambda: next(_ids))
131
+ dirty: bool = False
132
+ autosave: bool = False
133
+ missing: bool = False
134
+
135
+ @property
136
+ def name(self) -> str:
137
+ return self.workflow.name
138
+
139
+
140
+ @dataclass(eq=False)
141
+ class TransitionSystemDocument:
142
+ """A transition system, typed or from a ``ts.txt`` file, to study its regions."""
143
+
144
+ ts: object # openprocess.mining.transition_system.TransitionSystem
145
+ path: str | None = None
146
+ id: int = field(default_factory=lambda: next(_ids))
147
+ #: Its file was deleted or moved away while it was open.
148
+ missing: bool = False
149
+
150
+ @property
151
+ def name(self) -> str:
152
+ return self.ts.name