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,119 @@
1
+ """Check boxes: how well a model fits a log, and what a net is like."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from ... import flow
8
+ from ...mining.analysis import check_soundness
9
+ from ...mining.compare_nets import compare_nets
10
+ from ...mining.conformance.alignments import AlignmentResult, align_log
11
+ from ...mining.conformance.quality import generalisation, precision, simplicity
12
+ from ...mining.conformance.token_replay import ReplayResult, token_replay as _replay
13
+ from ...mining.footprint import compare_footprints, footprint_of_log, footprint_of_net
14
+ from ...mining.log import EventLog
15
+ from ...mining.petrinet import PetriNet
16
+ from ..box import box
17
+ from ..types import Scores, Table
18
+
19
+
20
+ def _replay_table(replay: ReplayResult) -> Table:
21
+ rows = [["⟨" + ", ".join(t.trace) + "⟩", t.count, t.produced, t.consumed, t.missing, t.remaining,
22
+ round(t.fitness, 4)] for t in replay.traces]
23
+ return Table("Replay per variant", ["variant", "cases", "p", "c", "m", "r", "fitness"], rows,
24
+ note=f"Over the log: p = {replay.produced}, c = {replay.consumed}, "
25
+ f"m = {replay.missing}, r = {replay.remaining}.")
26
+
27
+
28
+ @box(name="Check fit", group="Check")
29
+ def check_fit(model: PetriNet, log: EventLog,
30
+ method: Literal["Token replay", "Alignments"] = "Token replay") -> Scores:
31
+ """Replays every case of the log on the model and scores fitness,
32
+ precision (escaping edges), generalisation and simplicity.
33
+
34
+ method: token replay (fast, the course's p c m r) or optimal alignments
35
+ """
36
+ simple = log.simple_log()
37
+ replay = _replay(model, simple)
38
+ flow.show(_replay_table(replay), "Replay per variant")
39
+ if method == "Alignments":
40
+ alignments = align_log(model, simple)
41
+ fitness = alignments.average_fitness
42
+ flow.note(f"{alignments.fitting_traces} of {alignments.trace_count} cases align without a deviation")
43
+ else:
44
+ fitness = replay.fitness
45
+ flow.note(f"{replay.fitting_traces} of {replay.trace_count} cases replay without missing or remaining tokens")
46
+ scores = Scores(model.name, {"fitness": fitness, "precision": precision(model, simple),
47
+ "generalisation": generalisation(replay), "simplicity": simplicity(model)})
48
+ scores.note = f"Checked against all {replay.trace_count} cases of {log.name} ({method.lower()})."
49
+ return scores
50
+
51
+
52
+ @box(name="Token replay", group="Check")
53
+ def token_replay(model: PetriNet, log: EventLog) -> ReplayResult:
54
+ """Token-based replay with the counts the course uses: produced,
55
+ consumed, missing and remaining tokens, per variant."""
56
+ replay = _replay(model, log.simple_log())
57
+ flow.show(_replay_table(replay), "Replay per variant")
58
+ return replay
59
+
60
+
61
+ @box(name="Alignments", group="Check")
62
+ def alignments(model: PetriNet, log: EventLog) -> AlignmentResult:
63
+ """Optimal alignments of every variant with the model (A* over the
64
+ synchronous product, standard cost function)."""
65
+ result = align_log(model, log.simple_log())
66
+ rows = [["⟨" + ", ".join(a.trace) + "⟩", a.count, a.cost, round(a.fitness, 4)] for a in result.alignments]
67
+ flow.show(Table("Alignments", ["variant", "cases", "cost", "fitness"], rows), "Alignment per variant")
68
+ return result
69
+
70
+
71
+ @box(name="Soundness", group="Check")
72
+ def soundness(model: PetriNet) -> Scores:
73
+ """Is the net a sound WF-net? The three conditions, with each
74
+ violation's counterexample in the notes."""
75
+ report = check_soundness(model)
76
+ for finding in report.findings:
77
+ flow.note(finding)
78
+ verdict = {True: "Sound", False: "Not sound", None: "Undecided"}[report.sound]
79
+ return Scores(model.name, {"soundness": verdict,
80
+ "WF-net": "Yes" if report.workflow.is_workflow_net else "No",
81
+ "option to complete": _yes(report.option_to_complete),
82
+ "proper completion": _yes(report.proper_completion),
83
+ "no dead transitions": _yes(report.no_dead_transitions)})
84
+
85
+
86
+ def _yes(value) -> str:
87
+ return {True: "Yes", False: "No", None: "?"}[value]
88
+
89
+
90
+ @box(name="Compare to true net", group="Check")
91
+ def compare_to_true_net(model: PetriNet, true_net: PetriNet, max_length: int = 12) -> Scores:
92
+ """Model to model: the share of the true net's complete traces the
93
+ model allows (recall), the share of the model's traces the true net
94
+ allows (precision), simplicity, and soundness."""
95
+ comparison = compare_nets(model, true_net, max_length=max_length)
96
+ only_model, only_truth = len(comparison.only_first), len(comparison.only_second)
97
+ for trace in comparison.only_first[:5]:
98
+ flow.note("The model allows ⟨" + ", ".join(trace) + "⟩, the true net does not")
99
+ for trace in comparison.only_second[:5]:
100
+ flow.note("The true net allows ⟨" + ", ".join(trace) + "⟩, the model does not")
101
+ sound = check_soundness(model).sound
102
+ recall = 1.0 if comparison.equivalent else max(0.0, 1 - only_truth / max(1, only_truth + 3))
103
+ prec = 1.0 if comparison.equivalent else max(0.0, 1 - only_model / max(1, only_model + 3))
104
+ flow.note(comparison.summary("the model", "the true net"))
105
+ return Scores(model.name, {"recall": recall, "precision": prec, "simplicity": simplicity(model),
106
+ "soundness": {True: "Sound", False: "Not sound", None: "Undecided"}[sound]},
107
+ note="Recall and precision here count the shortest differing traces; "
108
+ "equivalent nets score 1.")
109
+
110
+
111
+ @box(name="Footprint conformance", group="Check")
112
+ def footprint_conformance(log: EventLog, model: PetriNet) -> Scores:
113
+ """Compares the footprint of the log with the footprint of the net."""
114
+ comparison = compare_footprints(footprint_of_log(log.simple_log()), footprint_of_net(model))
115
+ cells = len(comparison.activities) ** 2
116
+ flow.note(f"{len(comparison.differences)} of {cells} cells differ")
117
+ for a, b, in_log, in_model in comparison.differences[:8]:
118
+ flow.note(f"{a} ? {b}: {in_log} in the log, {in_model} in the net")
119
+ return Scores(model.name, {"footprint conformance": comparison.fitness})
@@ -0,0 +1,16 @@
1
+ """Compare: any number of scores side by side."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ... import flow
6
+ from ..box import box
7
+ from ..types import Scores, Table
8
+
9
+
10
+ @box(name="Compare", group="Compare")
11
+ def compare(scores: list[Scores]) -> Table:
12
+ """Puts any number of scores side by side, with columns taken from the
13
+ scores themselves. Under a sweep it collects the scores of every value."""
14
+ table = Table.from_scores(scores, "Compare")
15
+ flow.note(f"{len(table)} models, {len(table.columns) - 1} columns")
16
+ return table
@@ -0,0 +1,53 @@
1
+ """Coloured net boxes: play a CPN out many times, and compute its state space."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from ... import flow
8
+ from ...analysis.state_space import StateSpace
9
+ from ...mining.log import EventLog
10
+ from ...model.net import CPNet
11
+ from ...sim.export import case_variables, simulation_to_log
12
+ from ...sim.simulator import DeadMarkingError, Simulator
13
+ from ..box import box
14
+ from ..types import Scores, Text
15
+
16
+
17
+ @box(name="Simulate CPN", group="Coloured nets")
18
+ def simulate_cpn(net: CPNet, steps: int = 500, case_variable: str = "", seed: int = 0,
19
+ unit: Literal["seconds", "minutes", "hours", "days"] = "minutes") -> EventLog:
20
+ """Runs the simulator for *steps* firings and turns the firing history
21
+ into an event log: one case per value of *case_variable* (the variable
22
+ that identifies a case in the inscriptions, e.g. ``p`` or ``order``),
23
+ one event per firing, the model time as the timestamp.
24
+
25
+ case_variable: the variable whose value is the case id (empty: the first one found)
26
+ """
27
+ simulator = Simulator(net, seed=seed)
28
+ try:
29
+ fired = simulator.run(steps)
30
+ except DeadMarkingError:
31
+ fired = simulator.step_count
32
+ variable = case_variable or next(iter(case_variables(net)), "")
33
+ if not variable:
34
+ raise ValueError("No variable to use as the case id: set case_variable")
35
+ log = simulation_to_log(net, simulator.log, variable, unit)
36
+ flow.note(f"{fired} firings, model time {simulator.clock}, case variable {variable}: "
37
+ f"{len(log)} cases, {log.event_count} events")
38
+ return log
39
+
40
+
41
+ @box(name="State space", group="Coloured nets", heavy=True)
42
+ def state_space(net: CPNet, max_nodes: int = 10000) -> Scores:
43
+ """The reachability graph of a coloured net and its report: dead and
44
+ home markings, bounds, dead and live transitions (as CPN Tools reports
45
+ them). The full report is in the notes."""
46
+ space = StateSpace(net).generate(max_nodes=max_nodes)
47
+ report = space.report()
48
+ flow.show(Text(report, "State space report"), "Report")
49
+ dead = space.dead_markings()
50
+ return Scores(net.name, {"nodes": space.node_count, "arcs": space.arc_count,
51
+ "dead markings": len(dead), "home markings": len(space.home_markings()),
52
+ "dead transitions": len(space.dead_transitions()),
53
+ "complete": "No (stopped at the limit)" if space.unexplored_count else "Yes"})
@@ -0,0 +1,124 @@
1
+ """Discover boxes: from a log to a model, each one showing how."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from ... import flow
8
+ from ...mining.dfg import DFG, discover_dfg
9
+ from ...mining.discovery import alpha as alpha_module
10
+ from ...mining.discovery.heuristics import heuristics_net
11
+ from ...mining.discovery.inductive import inductive_miner as _inductive
12
+ from ...mining.discovery.state_regions import region_result
13
+ from ...mining.footprint import Footprint, footprint_of_log
14
+ from ...mining.log import EventLog
15
+ from ...mining.petrinet import PetriNet
16
+ from ...mining.processtree import ProcessTree, to_petri_net
17
+ from ...mining.transition_system import TransitionSystem, transition_system_from_log
18
+ from ..box import box
19
+ from ..types import Table
20
+
21
+
22
+ @box(name="α-algorithm", group="Discover")
23
+ def alpha_miner(log: EventLog) -> PetriNet:
24
+ """The α-algorithm (van der Aalst, Weijters & Maruster, 2004): a Petri
25
+ net from the footprint of the log, in the eight steps of the book."""
26
+ result = alpha_module.alpha_miner(log.simple_log())
27
+ flow.show(footprint_of_log(log.simple_log()), "Footprint of the log")
28
+ flow.steps(result.steps())
29
+ flow.show(result, "The derivation, typeset")
30
+ for warning in result.warnings:
31
+ flow.note(f"Warning: {warning}")
32
+ result.net.name = f"α({log.name})"
33
+ return result.net
34
+
35
+
36
+ @box(name="Inductive Miner", group="Discover")
37
+ def inductive_miner(log: EventLog, noise: float = 0.0) -> PetriNet:
38
+ """Splits the log recursively into sequence, choice, parallel and loop
39
+ parts (Leemans, Fahland & van der Aalst). With *noise* above 0 this is
40
+ IMf, which ignores behaviour rarer than the threshold.
41
+
42
+ noise: the IMf noise threshold, 0 for the plain Inductive Miner
43
+ """
44
+ result = _inductive(log.simple_log(), noise_threshold=noise)
45
+ flow.show(result, "Process tree and the recursion")
46
+ flow.steps([(f"{' ' * s.depth}{s.kind}", s.text) for s in result.steps])
47
+ result.net.name = f"IM{'f' if noise > 0 else ''}({log.name})"
48
+ return result.net
49
+
50
+
51
+ @box(name="Inductive Miner tree", group="Discover")
52
+ def inductive_tree(log: EventLog, noise: float = 0.0) -> ProcessTree:
53
+ """The Inductive Miner's process tree itself, to convert or inspect."""
54
+ result = _inductive(log.simple_log(), noise_threshold=noise)
55
+ flow.steps([(f"{' ' * s.depth}{s.kind}", s.text) for s in result.steps])
56
+ return result.tree
57
+
58
+
59
+ @box(name="Tree to net", group="Discover")
60
+ def tree_to_net(tree: ProcessTree) -> PetriNet:
61
+ """Translates a process tree into a sound WF-net (block by block)."""
62
+ return to_petri_net(tree)
63
+
64
+
65
+ @box(name="Heuristics Miner", group="Discover")
66
+ def heuristics_miner(log: EventLog, dependency: float = 0.9) -> PetriNet:
67
+ """Keeps the directly-follows relations whose dependency measure
68
+ a ⇒ b = (|a>b| − |b>a|) / (|a>b| + |b>a| + 1) reaches the threshold, then
69
+ turns the dependency graph into a Petri net through a causal net.
70
+
71
+ dependency: the threshold a ⇒ b must reach
72
+ """
73
+ result = heuristics_net(log.simple_log(), dependency_threshold=dependency)
74
+ flow.show(result, "Dependency graph and bindings")
75
+ graph = result.graph
76
+ rows = [[a, b, round(measure, 3), "kept" if (a, b) in graph.edges else "dropped"]
77
+ for (a, b), measure in sorted(graph.all_dependencies.items())]
78
+ flow.show(Table("Dependency measures", ["a", "b", "a ⇒ b", "in the graph"], rows), "Dependency measures")
79
+ flow.note(f"{len(graph.edges)} relations reach {dependency:g}")
80
+ result.net.name = f"HM({log.name})"
81
+ return result.net
82
+
83
+
84
+ @box(name="Classical states", group="Discover")
85
+ def classical_states(log: EventLog, direction: Literal["prefix", "postfix", "both"] = "prefix",
86
+ representation: Literal["sequence", "multiset", "set"] = "set",
87
+ horizon: Literal["1", "2", "3", "all"] = "all") -> TransitionSystem:
88
+ """A transition system from the log with a classical state function:
89
+ each prefix (or postfix) becomes a state through its sequence, multiset
90
+ or set, over the last k events or all of them."""
91
+ k = None if horizon == "all" else int(horizon)
92
+ ts = transition_system_from_log(log.simple_log(), direction, representation, k)
93
+ flow.note(f"{len(ts.states)} states, {len(ts.transitions)} transitions ({ts.abstraction})")
94
+ return ts
95
+
96
+
97
+ @box(name="Regions to net", group="Discover")
98
+ def regions_to_net(ts: TransitionSystem) -> PetriNet:
99
+ """Synthesises a Petri net from the minimal regions of a transition
100
+ system: one place per minimal region (Cortadella et al.)."""
101
+ result = region_result(ts)
102
+ flow.show(result, "Regions, checks and synthesis")
103
+ flow.note(f"{len(result.analysis.regions)} regions, {len(result.analysis.minimal)} minimal")
104
+ if result.synthesis.isomorphic is not None:
105
+ flow.note("The net's reachability graph is isomorphic to the transition system"
106
+ if result.synthesis.isomorphic else
107
+ "The net's reachability graph is not isomorphic to the transition system")
108
+ if result.net is None:
109
+ raise ValueError(" ".join(result.warnings) or "No net could be synthesised.")
110
+ return result.net
111
+
112
+
113
+ @box(name="Directly-follows graph", group="Discover")
114
+ def directly_follows(log: EventLog) -> DFG:
115
+ """The process map: which activity directly follows which, how often."""
116
+ dfg = discover_dfg(log)
117
+ flow.note(f"{len(dfg.activities)} activities, {len(dfg.edges)} arcs")
118
+ return dfg
119
+
120
+
121
+ @box(name="Footprint", group="Discover")
122
+ def footprint(log: EventLog) -> Footprint:
123
+ """The → ← ‖ # matrix of the log's ordering relations."""
124
+ return footprint_of_log(log.simple_log())
@@ -0,0 +1,80 @@
1
+ """Filter boxes: keep part of a log, as the log viewer's Filter… does."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from ... import flow
8
+ from ...mining import filtering
9
+ from ...mining.log import EventLog
10
+ from ..box import box
11
+
12
+
13
+ def _names(text: str) -> set[str]:
14
+ return {part.strip() for part in text.split(",") if part.strip()}
15
+
16
+
17
+ @box(name="Top variants", group="Filter")
18
+ def top_variants(log: EventLog, k: int = 3) -> EventLog:
19
+ """Keeps the cases of the *k* most frequent variants.
20
+
21
+ k: how many variants to keep
22
+ """
23
+ before = log.simple_log()
24
+ kept = filtering.filter_variants(log, count=k)
25
+ flow.note(f"Kept {len(kept.simple_log())} of {len(before)} variants, "
26
+ f"{len(kept)} of {len(log)} cases")
27
+ return kept
28
+
29
+
30
+ @box(name="Variants covering", group="Filter")
31
+ def variants_covering(log: EventLog, percent: float = 80.0) -> EventLog:
32
+ """Keeps just enough of the most frequent variants to cover *percent* of the cases."""
33
+ kept = filtering.filter_variants(log, coverage=percent)
34
+ flow.note(f"Kept {len(kept)} of {len(log)} cases")
35
+ return kept
36
+
37
+
38
+ @box(name="Activities", group="Filter")
39
+ def filter_activities(log: EventLog, activities: str = "a, b",
40
+ mode: Literal["keep events", "mandatory", "forbidden"] = "keep events") -> EventLog:
41
+ """Keeps only the events of these activities, or the cases that must
42
+ (*mandatory*) or must not (*forbidden*) contain one of them.
43
+
44
+ activities: activity names, separated by commas
45
+ """
46
+ kept = filtering.filter_activities(log, _names(activities), mode)
47
+ flow.note(f"{len(kept)} of {len(log)} cases, {kept.event_count} of {log.event_count} events left")
48
+ return kept
49
+
50
+
51
+ @box(name="Start activities", group="Filter")
52
+ def start_activities(log: EventLog, activities: str = "a") -> EventLog:
53
+ """Keeps the cases that start with one of these activities."""
54
+ return filtering.filter_start_activities(log, _names(activities))
55
+
56
+
57
+ @box(name="End activities", group="Filter")
58
+ def end_activities(log: EventLog, activities: str = "d") -> EventLog:
59
+ """Keeps the cases that end with one of these activities."""
60
+ return filtering.filter_end_activities(log, _names(activities))
61
+
62
+
63
+ @box(name="Case length", group="Filter")
64
+ def case_length(log: EventLog, minimum: int = 1, maximum: int = 100) -> EventLog:
65
+ """Keeps the cases with between *minimum* and *maximum* events."""
66
+ return filtering.filter_case_length(log, minimum, maximum)
67
+
68
+
69
+ @box(name="Time frame", group="Filter")
70
+ def time_frame(log: EventLog, start: str = "2000-01-01", end: str = "2100-01-01",
71
+ mode: Literal["contained", "started", "intersecting"] = "contained") -> EventLog:
72
+ """Keeps the cases contained in, started in, or touching the period.
73
+
74
+ start: the first day, as YYYY-MM-DD
75
+ end: the last day, as YYYY-MM-DD
76
+ """
77
+ from datetime import datetime, timezone
78
+ begin = datetime.fromisoformat(start).replace(tzinfo=timezone.utc)
79
+ finish = datetime.fromisoformat(end).replace(tzinfo=timezone.utc, hour=23, minute=59, second=59)
80
+ return filtering.filter_time_frame(log, begin, finish, mode)
@@ -0,0 +1,124 @@
1
+ """Input boxes: what a workflow starts from."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ from ... import flow
10
+ from ...mining.log import KEY_NAME, Event, EventLog, Trace, parse_simple_log
11
+ from ...mining.petrinet import PetriNet
12
+ from ...mining.playout import play_out
13
+ from ...mining.transition_system import TransitionSystem, parse_transition_system
14
+ from ...model.net import CPNet
15
+ from ..box import box
16
+
17
+
18
+ def _resolve(file: Path) -> Path:
19
+ path = Path(file)
20
+ if not path.is_file():
21
+ raise FileNotFoundError(f"No such file: {path}")
22
+ return path
23
+
24
+
25
+ @box(name="Open log", group="Input")
26
+ def open_log(file: Path) -> EventLog:
27
+ """Reads an event log from the folder: XES (also gzipped), CSV, or a
28
+ ``.txt`` file in the course notation ``[<a,b,c>^3, <a,c>]``.
29
+
30
+ file: the log file, relative to the workflow's folder
31
+ """
32
+ path = _resolve(file)
33
+ lower = path.name.lower()
34
+ if lower.endswith(".csv"):
35
+ from ...mining.csv_import import read_csv
36
+ log = read_csv(path)
37
+ elif lower.endswith(".txt"):
38
+ log = EventLog.from_simple_log(parse_simple_log(path.read_text(encoding="utf-8")), path.stem)
39
+ else:
40
+ from ...mining.xes import read_xes
41
+ log = read_xes(path)
42
+ flow.note(f"{path.name}: {len(log)} cases, {log.event_count} events, "
43
+ f"classifier {log.default_classifier().name}")
44
+ return log
45
+
46
+
47
+ @box(name="Typed log", group="Input")
48
+ def typed_log(text: str = "[<a,b,c,d>^3, <a,c,b,d>^2, <a,e,d>]", name: str = "Typed log") -> EventLog:
49
+ """A log typed in the course notation: ``[<a,b,c,d>^3, <a,c,b,d>^2, <a,e,d>]``."""
50
+ return EventLog.from_simple_log(parse_simple_log(text), name)
51
+
52
+
53
+ @box(name="Open net", group="Input")
54
+ def open_net(file: Path) -> PetriNet:
55
+ """Opens a Petri net from the folder: a PNML file, or a net drawn in the editor."""
56
+ from ...mining.pnml import read_pnml
57
+ net = read_pnml(_resolve(file))
58
+ flow.note(f"{Path(file).name}: {net.summary()}")
59
+ return net
60
+
61
+
62
+ @box(name="Open coloured net", group="Input")
63
+ def open_cpn(file: Path) -> CPNet:
64
+ """Opens a coloured Petri net (a CPN Tools ``.cpn`` file)."""
65
+ from ...io.cpn_reader import read_cpn
66
+ net = read_cpn(str(_resolve(file)))
67
+ if net.errors:
68
+ raise ValueError("The model does not compile: " + "; ".join(str(e) for e in net.errors[:3]))
69
+ return net
70
+
71
+
72
+ @box(name="Open transition system", group="Input")
73
+ def open_transition_system(file: Path) -> TransitionSystem:
74
+ """Opens a transition system typed as ``s0 -a-> s1, s0 -b-> s2`` (a ``ts.txt`` file)."""
75
+ path = _resolve(file)
76
+ return parse_transition_system(path.read_text(encoding="utf-8"), path.stem)
77
+
78
+
79
+ @box(name="Open dataset", group="Input")
80
+ def open_dataset(name: Literal["BPI Challenge 2012", "BPI Challenge 2013 incidents", "BPI Challenge 2017",
81
+ "BPI Challenge 2019", "BPI Challenge 2020 domestic declarations",
82
+ "Sepsis cases", "Road traffic fine management", "Hospital billing"]
83
+ = "Sepsis cases") -> EventLog:
84
+ """A standard public log by name, fetched once into a shared cache and
85
+ checked against its fingerprint, so the same name means the same bytes
86
+ in every workflow and every paper (see ``openprocess datasets``)."""
87
+ from ..datasets import info, open_dataset as fetch_and_read
88
+ entry = info(name)
89
+ log = fetch_and_read(name)
90
+ flow.note(f"{entry.name}: {entry.citation or entry.page}")
91
+ flow.note(f"sha256 {log.attributes.get('openprocess:sha256', '')[:16]}… · {len(log)} cases, {log.event_count} events")
92
+ return log
93
+
94
+
95
+ @box(name="Simulate log", group="Input")
96
+ def simulate_log(net: PetriNet, cases: int = 200, noise: float = 0.0, seed: int = 0,
97
+ max_length: int = 200) -> EventLog:
98
+ """Plays the net out into a log, then adds noise: with probability
99
+ *noise* a case loses one event or has two neighbouring events swapped.
100
+ The seed makes it repeatable.
101
+
102
+ cases: how many cases to generate
103
+ noise: the chance that a case is damaged (0 keeps the log clean)
104
+ seed: the random seed, recorded in the workflow file
105
+ """
106
+ result = play_out(net, traces=cases, max_length=max_length, seed=seed,
107
+ name=f"Simulation of {net.name}")
108
+ rng = random.Random(seed * 7919 + 17)
109
+ damaged = 0
110
+ for trace in result.log:
111
+ if noise > 0 and len(trace.events) > 1 and rng.random() < noise:
112
+ events = trace.events
113
+ j = rng.randrange(1, len(events))
114
+ if rng.random() < 0.5:
115
+ del events[j]
116
+ elif j < len(events) - 1:
117
+ events[j], events[j + 1] = events[j + 1], events[j]
118
+ else:
119
+ events[j - 1], events[j] = events[j], events[j - 1]
120
+ trace.attributes["openprocess:noise"] = "yes"
121
+ damaged += 1
122
+ flow.note(f"{result.completed} runs completed, {result.deadlocked} deadlocked, "
123
+ f"{result.cut_off} cut off at {max_length} steps; {damaged} cases damaged by noise")
124
+ return result.log
@@ -0,0 +1,52 @@
1
+ """Output boxes: save a result into the folder, next to the workflow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from ... import flow
8
+ from ...mining.log import EventLog
9
+ from ...mining.petrinet import PetriNet
10
+ from ..box import box
11
+ from ..types import Figure, Table
12
+
13
+
14
+ def _target(file: str) -> Path:
15
+ path = Path(file)
16
+ path.parent.mkdir(parents=True, exist_ok=True)
17
+ return path
18
+
19
+
20
+ @box(name="Save as PNML", group="Output")
21
+ def save_pnml(model: PetriNet, file: str = "model.pnml") -> None:
22
+ """Writes the model as PNML (ProM, WoPeD and PM4Py read it)."""
23
+ from ...mining.pnml import write_pnml
24
+ write_pnml(model, _target(file))
25
+ flow.note(f"Wrote {file}")
26
+
27
+
28
+ @box(name="Save log", group="Output")
29
+ def save_log(log: EventLog, file: str = "log.xes") -> None:
30
+ """Writes the log as XES (``.xes`` or ``.xes.gz``) or CSV (``.csv``)."""
31
+ path = _target(file)
32
+ if path.suffix.lower() == ".csv":
33
+ from ...mining.csv_import import write_csv
34
+ write_csv(log, path)
35
+ else:
36
+ from ...mining.xes import write_xes
37
+ write_xes(log, path)
38
+ flow.note(f"Wrote {file}: {len(log)} cases")
39
+
40
+
41
+ @box(name="Save table", group="Output")
42
+ def save_table(table: Table, file: str = "table.csv") -> None:
43
+ """Writes a table as CSV."""
44
+ table.to_csv(_target(file))
45
+ flow.note(f"Wrote {file}: {len(table)} rows")
46
+
47
+
48
+ @box(name="Save figure", group="Output")
49
+ def save_figure(figure: Figure, file: str = "figure.svg") -> None:
50
+ """Writes a figure as SVG or PNG, by the file's extension."""
51
+ figure.save(_target(file))
52
+ flow.note(f"Wrote {file}")