petri_net_nn.discovery¶
discovery ¶
Inductive process mining — discover a sound Petri net from event logs.
Implementation of the basic Inductive Miner (Leemans, Fahland,
van der Aalst, 2013, "Discovering Block-Structured Process
Models from Event Logs - A Constructive Approach"). Given a
list of traces (each a sequence of activity names from the
event.name field), recursively find structural cuts in the
directly-follows graph until a process tree emerges, then
translate the process tree into a Petri net.
The output net is sound by construction — option to
complete, proper completion, and no dead transitions all hold
for any output of this algorithm. That guarantee composes
cleanly with the rest of PETRA's pipeline:
:func:discover_and_train chains discovery, a soundness check,
compilation, and training in one call.
Algorithm sketch¶
-
From the trace set, build the directly-follows graph (DFG) — nodes are activities, an edge
(a, b)meansawas directly followed bybin some trace. -
Try the four cut shapes in priority order:
-
Exclusive choice (
×): activities partition into disjoint groups that never co-occur in a single trace. Detected as connected components of the undirected DFG. -
Sequence (
→): activities partition into ordered groupsΣ₁, …, Σₙsuch that for each pairi<j, every activity inΣᵢreaches every activity inΣⱼvia DFG paths and none inΣⱼreaches any inΣᵢ. Detected via strongly-connected components and a total topological ordering. -
Parallel (
∧): activities partition into groups such that every cross-group pair(a, b)has botha→bandb→ain the DFG, and every group contains at least one trace-start activity and one trace-end activity. -
Loop (
↻): designate one group as the body and the rest as redo parts. The body alone contains all start and end activities of the log; the redos can only be entered after the body ends and exited by re-entering the body. -
If a cut is found, split the log accordingly and recurse on each sub-log.
-
Base cases:
-
Empty log →
Tau(silent transition). -
Single activity log →
Activityleaf. -
Fallback: when no cut applies, return a flower model — an XOR of all activities inside a self-loop. This is a degenerate output that always accepts the log; it appears when the log is too unstructured for the cut detectors to handle.
Limitations of this first delivery¶
- Basic IM only. Not IMf (infrequent-noise filtering) or IMi (incremental). Noisy logs may collapse into the flower-model fallback.
- Loop cut handles body+redo pairs. Nested loops work via recursion, but esoteric loop topologies may degrade.
- Timestamps are ignored — only activity sequences matter
for the structural discovery. PETRA's later training step
can read attributes via the standard
train_on_tracespath. - Event attributes are not used for discovery.
Tau
dataclass
¶
Silent / τ transition — produced for empty sub-logs and for the τ branches of loop and exclusive cuts that allow skipping.
Activity
dataclass
¶
Leaf node carrying a single activity name from the log. Becomes a single labelled transition in the emitted net.
Parallel
dataclass
¶
∧ — all children execute concurrently and synchronise
at the join.
Loop
dataclass
¶
↻ — children[0] is the body; children[1:] are
redo parts. The body executes; from its end, either continue
(loop exits) or do one of the redos and re-enter the body.
A loop with no explicit redo is equivalent to Loop(body,
Tau()) — every body iteration can be followed by either
another body execution or termination.
discover_inductive ¶
Discover a Petri net from a trace set via the basic Inductive Miner.
Parameters¶
traces
Either a list of :class:XESTrace objects (the
natural shape after :func:parse_xes / parse_csv
/ parse_json) or a raw list of activity-name
tuples. XESTrace inputs are projected to their event
names; everything else about the traces (attributes,
timestamps) is deliberately ignored — discovery is
purely structural.
Returns¶
PetriNet
Sound by construction (every Petri net the basic IM
emits is option-to-complete, properly-terminating, and
free of dead transitions; verify with
:func:check_soundness if you want explicit assurance).
The initial marking places one token at the source.
Transition labels are the activity names from the log;
silent (τ) transitions are flagged via
:attr:PetriNet.silent_transitions so weak bisimulation
treats them correctly.
Notes¶
Activity names that are pure Python identifiers map cleanly
onto place / transition IDs. Names with spaces or special
characters are stored verbatim on the transition labels
but never appear in IDs (the translator mints synthetic
t_N / p_N IDs).
Source code in petri_net_nn/discovery.py
discover_and_train ¶
End-to-end discover → verify → compile → train pipeline.
- Mine a Petri net from
tracesvia :func:discover_inductive. - Verify the mined net is sound via
:func:
petri_net_nn.soundness.check_soundness. By construction the basic IM always yields sound nets; this check guards against future changes to the miner or to the soundness definition. - Compile the net into a :class:
PetriNetModule. - Train on the same
tracesvia :func:petri_net_nn.traces.train_on_traces.
Returns (net, module, losses) — the discovered net,
the trained module, and the per-step loss trajectory.
The caller can then extract rules, score anomalies, run
bisimulation against another variant, etc., using the
standard PETRA pipeline.
Source code in petri_net_nn/discovery.py
discover_transition_system ¶
Discover an order-preserving Petri net from a trace set.
The Inductive Miner abstracts a log into block structure
(sequence / choice / parallel / loop). That is the right
abstraction for workflow logs, but it deliberately forgets
which order repeated activities came in: a log of day-by-day
states such as (ENTER, FALL, FALL, EXIT) and
(ENTER, UP, FALL, EXIT) is mined into independent loops, so
"two FALL days in a row" is not representable in the net.
This miner keeps the order. It builds the classic transition-system abstraction (van der Aalst et al., 2010, "Process mining: a two-step approach to balance between underfitting and overfitting") with a k-tail state abstraction and emits it as a state-machine Petri net:
- one place per observed state, where a state is the tuple of
the last
horizonactivities (the empty tuple is the start state, holding the single initial token); - one labelled transition per observed
(state, activity)pair, from that state's place to the place of the successor state; - one silent (τ) transition from every state in which some
trace ended to a single sink place
s_end.
horizon trades precision for generality: horizon=1 is a
first-order (directly-follows) model; horizon=2 distinguishes
"FALL after FALL" from "FALL after UP"; larger values approach
a prefix tree of the log.
Place labels carry the state ("FALL > FALL"; "start",
"end"), so rules read naturally. Because every transition has
exactly one input and one output place, the net is a state
machine: it is 1-bounded, every place lies on the path of some
trace from the start place to s_end (option to complete),
and every transition was fired by some trace (no dead
transitions) — i.e. sound by construction; verify with
:func:check_soundness. The net is generally cyclic; pair it
with :func:petri_net_nn.replay.train_by_replay, which needs no
unrolling.
Raises ValueError for horizon < 1 or an empty log.
Source code in petri_net_nn/discovery.py
994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 | |