openprocess 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cpnpy/__init__.py +46 -0
- openprocess/__init__.py +57 -0
- openprocess/analysis/__init__.py +0 -0
- openprocess/analysis/state_space.py +521 -0
- openprocess/analysis/state_space_process.py +251 -0
- openprocess/cli.py +742 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
- openprocess/exercises/pack.md +14 -0
- openprocess/flow/__init__.py +50 -0
- openprocess/flow/box.py +466 -0
- openprocess/flow/boxes/__init__.py +7 -0
- openprocess/flow/boxes/check.py +119 -0
- openprocess/flow/boxes/compare.py +16 -0
- openprocess/flow/boxes/cpn.py +53 -0
- openprocess/flow/boxes/discover.py +124 -0
- openprocess/flow/boxes/filter.py +80 -0
- openprocess/flow/boxes/input.py +124 -0
- openprocess/flow/boxes/output.py +52 -0
- openprocess/flow/boxes/predict.py +186 -0
- openprocess/flow/boxes/science.py +159 -0
- openprocess/flow/boxes/sweeps.py +18 -0
- openprocess/flow/convert.py +187 -0
- openprocess/flow/datasets.py +198 -0
- openprocess/flow/explain.py +115 -0
- openprocess/flow/library.py +222 -0
- openprocess/flow/record.py +385 -0
- openprocess/flow/runner.py +357 -0
- openprocess/flow/sweep.py +92 -0
- openprocess/flow/types.py +290 -0
- openprocess/flow/workflow.py +628 -0
- openprocess/gui/__init__.py +0 -0
- openprocess/gui/app.py +90 -0
- openprocess/gui/arc_editing.py +295 -0
- openprocess/gui/canvas.py +1414 -0
- openprocess/gui/flow/__init__.py +8 -0
- openprocess/gui/flow/canvas.py +854 -0
- openprocess/gui/flow/page.py +972 -0
- openprocess/gui/flow/templates.py +131 -0
- openprocess/gui/flow/viewers.py +665 -0
- openprocess/gui/items.py +1275 -0
- openprocess/gui/learn/answer_boxes.py +978 -0
- openprocess/gui/learn/concealment.py +91 -0
- openprocess/gui/learn/mode.py +1181 -0
- openprocess/gui/panning.py +241 -0
- openprocess/gui/resources/openprocess-icon.png +0 -0
- openprocess/gui/studio/__init__.py +1 -0
- openprocess/gui/studio/__main__.py +3 -0
- openprocess/gui/studio/app.py +4031 -0
- openprocess/gui/studio/charts.py +115 -0
- openprocess/gui/studio/compare_page.py +487 -0
- openprocess/gui/studio/cpn_page.py +1858 -0
- openprocess/gui/studio/definition_view.py +284 -0
- openprocess/gui/studio/derivation_view.py +421 -0
- openprocess/gui/studio/documents.py +152 -0
- openprocess/gui/studio/dotted_chart.py +1401 -0
- openprocess/gui/studio/file_dialogs.py +143 -0
- openprocess/gui/studio/filter_dialog.py +247 -0
- openprocess/gui/studio/graph_builders.py +176 -0
- openprocess/gui/studio/graph_view.py +682 -0
- openprocess/gui/studio/instances.py +413 -0
- openprocess/gui/studio/log_editor.py +675 -0
- openprocess/gui/studio/log_page.py +800 -0
- openprocess/gui/studio/markdown_view.py +127 -0
- openprocess/gui/studio/mathtext.py +260 -0
- openprocess/gui/studio/ml_highlighter.py +75 -0
- openprocess/gui/studio/model_page.py +760 -0
- openprocess/gui/studio/net_comparison.py +124 -0
- openprocess/gui/studio/notes_overlay.py +275 -0
- openprocess/gui/studio/petri_page.py +844 -0
- openprocess/gui/studio/regions_view.py +502 -0
- openprocess/gui/studio/sidebar.py +149 -0
- openprocess/gui/studio/style.py +503 -0
- openprocess/gui/studio/tool_icons.py +134 -0
- openprocess/gui/studio/updates.py +439 -0
- openprocess/gui/studio/widgets.py +899 -0
- openprocess/gui/studio/workers.py +60 -0
- openprocess/gui/studio/workspace.py +447 -0
- openprocess/gui/theme.py +394 -0
- openprocess/gui/tidy.py +86 -0
- openprocess/io/__init__.py +0 -0
- openprocess/io/cpn_reader.py +389 -0
- openprocess/io/cpn_writer.py +357 -0
- openprocess/learn/__init__.py +23 -0
- openprocess/learn/answers.py +188 -0
- openprocess/learn/checks.py +953 -0
- openprocess/learn/computed.py +1180 -0
- openprocess/learn/context.py +145 -0
- openprocess/learn/exam.py +169 -0
- openprocess/learn/exercise-packs.md +325 -0
- openprocess/learn/importer.py +216 -0
- openprocess/learn/notation.py +474 -0
- openprocess/learn/pack.py +511 -0
- openprocess/learn/sheet.py +296 -0
- openprocess/mining/__init__.py +73 -0
- openprocess/mining/analysis.py +689 -0
- openprocess/mining/columns.py +282 -0
- openprocess/mining/compare_nets.py +246 -0
- openprocess/mining/conformance/__init__.py +0 -0
- openprocess/mining/conformance/alignments.py +263 -0
- openprocess/mining/conformance/quality.py +145 -0
- openprocess/mining/conformance/token_replay.py +252 -0
- openprocess/mining/csv_import.py +222 -0
- openprocess/mining/definitions.py +584 -0
- openprocess/mining/dfg.py +187 -0
- openprocess/mining/discovery/__init__.py +0 -0
- openprocess/mining/discovery/alpha.py +168 -0
- openprocess/mining/discovery/heuristics.py +332 -0
- openprocess/mining/discovery/inductive.py +477 -0
- openprocess/mining/discovery/state_regions.py +62 -0
- openprocess/mining/filtering.py +237 -0
- openprocess/mining/footprint.py +183 -0
- openprocess/mining/invariants.py +191 -0
- openprocess/mining/layout.py +279 -0
- openprocess/mining/log.py +364 -0
- openprocess/mining/petrinet.py +354 -0
- openprocess/mining/playout.py +75 -0
- openprocess/mining/pm4py_bridge.py +82 -0
- openprocess/mining/pnml.py +223 -0
- openprocess/mining/processtree.py +216 -0
- openprocess/mining/regions.py +476 -0
- openprocess/mining/stats.py +160 -0
- openprocess/mining/structure.py +374 -0
- openprocess/mining/transition_system.py +409 -0
- openprocess/mining/xes.py +399 -0
- openprocess/ml/__init__.py +0 -0
- openprocess/ml/ast_nodes.py +332 -0
- openprocess/ml/builtins.py +364 -0
- openprocess/ml/colorsets.py +522 -0
- openprocess/ml/errors.py +60 -0
- openprocess/ml/evaluator.py +754 -0
- openprocess/ml/lexer.py +277 -0
- openprocess/ml/multiset.py +417 -0
- openprocess/ml/parser.py +737 -0
- openprocess/ml/values.py +319 -0
- openprocess/model/__init__.py +0 -0
- openprocess/model/declarations.py +617 -0
- openprocess/model/examples.py +98 -0
- openprocess/model/net.py +701 -0
- openprocess/model/plain.py +192 -0
- openprocess/references.py +280 -0
- openprocess/sim/__init__.py +0 -0
- openprocess/sim/binding.py +620 -0
- openprocess/sim/export.py +66 -0
- openprocess/sim/simulator.py +315 -0
- openprocess/teaching/__init__.py +4 -0
- openprocess/teaching/answers.py +4 -0
- openprocess/teaching/checks.py +5 -0
- openprocess/teaching/pack.py +4 -0
- openprocess/teaching/sheet.py +4 -0
- openprocess-0.7.0.dist-info/METADATA +927 -0
- openprocess-0.7.0.dist-info/RECORD +173 -0
- openprocess-0.7.0.dist-info/WHEEL +5 -0
- openprocess-0.7.0.dist-info/entry_points.txt +6 -0
- openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
- openprocess-0.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,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}")
|