Skip to content

Commit 67e0bf8

Browse files
author
Alessandro Berti
committed
Merge branch '423-dijkstra-based-alignments-for-reset-inhibitor-nets' into 'integration'
Dijkstra-based Alignments for Reset/Inhibitor Nets Closes #423 See merge request process-intelligence-solutions/pm4py!1439
2 parents 10c28cc + 0503e0c commit 67e0bf8

23 files changed

Lines changed: 2724 additions & 5 deletions
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""
2+
Play out and align a reset/inhibitor Petri net.
3+
4+
This example uses the hospital-discharge model stored in the test data. It
5+
demonstrates the two places where custom Petri-net semantics must be supplied:
6+
7+
1. basic playout, so inhibitor arcs affect enablement and reset arcs affect
8+
firing;
9+
2. the semantics-aware Dijkstra alignment variant, so its state-space search
10+
follows exactly the same rules as the model simulation.
11+
12+
Run this file from any working directory:
13+
14+
python examples/inhibitor_reset_playout_and_alignment.py
15+
"""
16+
17+
import sys
18+
from pathlib import Path
19+
20+
# A directly executed file puts examples/ (not the repository root) first on
21+
# sys.path. Add the checkout root before importing PM4Py so this example uses
22+
# the local implementation, including VERSION_DIJKSTRA_SEMANTICS.
23+
REPOSITORY_ROOT = Path(__file__).resolve().parent.parent
24+
sys.path.insert(0, str(REPOSITORY_ROOT))
25+
26+
import pm4py
27+
from pm4py.algo.conformance.alignments.petri_net import (
28+
algorithm as alignments,
29+
)
30+
from pm4py.algo.simulation.playout.petri_net import algorithm as playout
31+
from pm4py.objects.log.obj import Event, EventLog, Trace
32+
from pm4py.objects.petri_net.inhibitor_reset.semantics import (
33+
InhibitorResetSemantics,
34+
)
35+
from pm4py.objects.petri_net.obj import InhibitorNet, ResetNet
36+
37+
38+
# Resolve input files relative to this source file. Using __file__ instead of
39+
# the current working directory makes the example work both from the repository
40+
# root and from the examples directory.
41+
DATA_DIR = REPOSITORY_ROOT / "tests" / "input_data" / "inh_res_nets"
42+
MODEL_PATH = DATA_DIR / "hospital_discharge.pnml"
43+
LOG_PATH = DATA_DIR / "logs" / "hospital_discharge.xes"
44+
45+
46+
def activity_sequence(trace: Trace):
47+
"""Return only the readable activity names from a PM4Py trace."""
48+
return [event["concept:name"] for event in trace]
49+
50+
51+
def execute_script():
52+
# PNML preserves the specialized reset and inhibitor arc types. The PM4Py
53+
# importer therefore reconstructs a ResetInhibitorNet automatically.
54+
net, initial_marking, final_marking = pm4py.read_pnml(str(MODEL_PATH))
55+
56+
# The companion XES file was generated by basic playout. Requesting the
57+
# legacy EventLog representation gives us Trace/Event objects, which can be
58+
# passed directly to the alignment API used below.
59+
stored_log = pm4py.read_xes(
60+
str(LOG_PATH), return_legacy_log_object=True
61+
)
62+
63+
# A reset arc consumes every token from its source place when its transition
64+
# fires. An inhibitor arc does not consume tokens; instead, it enables its
65+
# transition only while the source place is empty. Checking the imported
66+
# objects is a useful sanity check that PNML round-tripping retained both.
67+
reset_arcs = [
68+
arc for arc in net.arcs if isinstance(arc, ResetNet.ResetArc)
69+
]
70+
inhibitor_arcs = [
71+
arc for arc in net.arcs if isinstance(arc, InhibitorNet.InhibitorArc)
72+
]
73+
assert reset_arcs, "the imported model should contain a reset arc"
74+
assert inhibitor_arcs, "the imported model should contain an inhibitor arc"
75+
76+
print("Model:", net.name)
77+
print("Visible activities:", len([t for t in net.transitions if t.label]))
78+
print("Stored traces:", len(stored_log))
79+
print("First stored trace:", activity_sequence(stored_log[0]))
80+
81+
# ClassicSemantics remains PM4Py's default because it is right for ordinary
82+
# Petri nets. This model is not ordinary, so the same specialized semantics
83+
# object is deliberately passed to both playout and alignment.
84+
semantics = InhibitorResetSemantics()
85+
86+
# Generate a few fresh traces. "add_only_if_fm_is_reached" excludes partial
87+
# runs stopped by the length limit, while maxTraceLength protects us from an
88+
# unlucky sequence that repeatedly chooses one of the model's optional
89+
# loops.
90+
simulated_log = playout.apply(
91+
net,
92+
initial_marking,
93+
final_marking,
94+
variant=playout.Variants.BASIC_PLAYOUT,
95+
parameters={
96+
"petri_semantics": semantics,
97+
"noTraces": 5,
98+
"maxTraceLength": 40,
99+
"add_only_if_fm_is_reached": True,
100+
},
101+
)
102+
103+
print("\nFreshly simulated traces:")
104+
for index, trace in enumerate(simulated_log):
105+
print(f" {index}: {activity_sequence(trace)}")
106+
107+
# VERSION_DIJKSTRA_SEMANTICS searches states of the form
108+
# (position in trace, model marking). For each model or synchronous move it
109+
# calls the supplied semantics to find enabled transitions and fire them.
110+
# This avoids flattening the model into a classic synchronous-product net,
111+
# which would lose the special behavior of reset and inhibitor arcs.
112+
alignment_results = alignments.apply(
113+
simulated_log,
114+
net,
115+
initial_marking,
116+
final_marking,
117+
variant=alignments.Variants.VERSION_DIJKSTRA_SEMANTICS,
118+
parameters={
119+
"petri_semantics": semantics,
120+
"show_progress_bar": False,
121+
},
122+
)
123+
124+
# Every trace came from this exact model, so every event should synchronize
125+
# with a model transition and every optimal alignment should cost zero.
126+
assert all(result["cost"] == 0 for result in alignment_results)
127+
print("\nAlignment costs for simulated traces:")
128+
print([result["cost"] for result in alignment_results])
129+
130+
# Finally, insert an event that the model does not know. Dijkstra represents
131+
# it as a log move: ("Unexpected Manual Override", ">>"). With PM4Py's
132+
# standard costs that deviation contributes 10,000 to the alignment.
133+
original = simulated_log[0]
134+
deviating_trace = Trace(
135+
[Event(dict(event)) for event in original],
136+
attributes=dict(original.attributes),
137+
)
138+
deviating_trace.insert(
139+
1, Event({"concept:name": "Unexpected Manual Override"})
140+
)
141+
deviation = alignments.apply(
142+
deviating_trace,
143+
net,
144+
initial_marking,
145+
final_marking,
146+
variant=alignments.Variants.VERSION_DIJKSTRA_SEMANTICS,
147+
parameters={"petri_semantics": semantics},
148+
)
149+
150+
assert deviation["cost"] == 10000
151+
assert ("Unexpected Manual Override", ">>") in deviation["alignment"]
152+
print("\nAlignment of a deliberately deviating trace:")
153+
print(" cost:", deviation["cost"])
154+
print(" moves:", deviation["alignment"])
155+
156+
# EventLog is imported above to make clear that both persisted and simulated
157+
# logs use PM4Py's normal log type; this assertion also documents the return
158+
# type of basic Petri-net playout.
159+
assert isinstance(simulated_log, EventLog)
160+
161+
162+
if __name__ == "__main__":
163+
execute_script()

pm4py/algo/conformance/alignments/petri_net/algorithm.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Variants(Enum):
2828
VERSION_STATE_EQUATION_A_STAR = variants.state_equation_a_star
2929
VERSION_DIJKSTRA_NO_HEURISTICS = variants.dijkstra_no_heuristics
3030
VERSION_DIJKSTRA_LESS_MEMORY = variants.dijkstra_less_memory
31+
VERSION_DIJKSTRA_SEMANTICS = variants.dijkstra_semantics
3132
VERSION_DISCOUNTED_A_STAR = variants.discounted_a_star
3233
APPROX_TANDEM_REPEATS = variants.approx_tandem_repeats
3334
APPROX_SLIDING_WINDOW = variants.approx_sliding_window
@@ -59,6 +60,7 @@ class Parameters(Enum):
5960
EXPONENT="theta"
6061
ENABLE_BEST_WORST_COST = "enable_best_worst_cost"
6162
UNPACK_VARIANT_ALIGNMENTS = "unpack_alignments"
63+
PETRI_SEMANTICS = "petri_semantics"
6264

6365

6466
def __variant_mapper(variant):
@@ -69,6 +71,8 @@ def __variant_mapper(variant):
6971
variant = Variants.VERSION_DIJKSTRA_NO_HEURISTICS
7072
elif variant == "Variants.VERSION_DIJKSTRA_LESS_MEMORY":
7173
variant = Variants.VERSION_DIJKSTRA_LESS_MEMORY
74+
elif variant == "Variants.VERSION_DIJKSTRA_SEMANTICS":
75+
variant = Variants.VERSION_DIJKSTRA_SEMANTICS
7276
elif variant == "Variants.VERSION_DISCOUNTED_A_STAR":
7377
variant = Variants.VERSION_DISCOUNTED_A_STAR
7478
elif variant == "Variants.APPROX_TANDEM_REPEATS":
@@ -88,6 +92,7 @@ def __variant_mapper(variant):
8892
VERSION_STATE_EQUATION_A_STAR = Variants.VERSION_STATE_EQUATION_A_STAR
8993
VERSION_DIJKSTRA_NO_HEURISTICS = Variants.VERSION_DIJKSTRA_NO_HEURISTICS
9094
VERSION_DIJKSTRA_LESS_MEMORY = Variants.VERSION_DIJKSTRA_LESS_MEMORY
95+
VERSION_DIJKSTRA_SEMANTICS = Variants.VERSION_DIJKSTRA_SEMANTICS
9196
VERSION_DISCOUNTED_A_STAR = Variants.VERSION_DISCOUNTED_A_STAR
9297
APPROX_TANDEM_REPEATS = Variants.APPROX_TANDEM_REPEATS
9398
APPROX_SLIDING_WINDOW = Variants.APPROX_SLIDING_WINDOW
@@ -97,6 +102,7 @@ def __variant_mapper(variant):
97102
Variants.VERSION_DIJKSTRA_NO_HEURISTICS,
98103
Variants.VERSION_STATE_EQUATION_A_STAR,
99104
Variants.VERSION_DIJKSTRA_LESS_MEMORY,
105+
Variants.VERSION_DIJKSTRA_SEMANTICS,
100106
Variants.VERSION_DISCOUNTED_A_STAR,
101107
Variants.APPROX_TANDEM_REPEATS,
102108
Variants.APPROX_SLIDING_WINDOW,
@@ -159,7 +165,9 @@ def apply_trace(
159165
selected variant of the algorithm. Approximation-oriented values are
160166
``Variants.APPROX_TANDEM_REPEATS``,
161167
``Variants.APPROX_SLIDING_WINDOW``, and
162-
``Variants.APPROX_FIXED_HORIZON``.
168+
``Variants.APPROX_FIXED_HORIZON``. Use
169+
``Variants.VERSION_DIJKSTRA_SEMANTICS`` for non-classic Petri-net
170+
semantics.
163171
parameters
164172
:class:`dict` parameters of the algorithm, for key \'state_equation_a_star\':
165173
Parameters.ACTIVITY_KEY -> Attribute in the log that contains the activity
@@ -169,6 +177,9 @@ def apply_trace(
169177
mapping of each transition in the model to corresponding model cost
170178
Parameters.PARAM_TRACE_COST_FUNCTION ->
171179
mapping of each index of the trace to a positive cost value
180+
Parameters.PETRI_SEMANTICS ->
181+
semantics used by ``VERSION_DIJKSTRA_SEMANTICS`` (classic by
182+
default)
172183
173184
Returns
174185
-------
@@ -248,7 +259,9 @@ def apply_log(
248259
selected variant of the algorithm. Approximation-oriented values are
249260
``Variants.APPROX_TANDEM_REPEATS``,
250261
``Variants.APPROX_SLIDING_WINDOW``, and
251-
``Variants.APPROX_FIXED_HORIZON``.
262+
``Variants.APPROX_FIXED_HORIZON``. Use
263+
``Variants.VERSION_DIJKSTRA_SEMANTICS`` for non-classic Petri-net
264+
semantics.
252265
parameters
253266
:class:`dict` parameters of the algorithm:
254267
@@ -287,7 +300,12 @@ def apply_log(
287300
Parameters.TIMESTAMP_KEY, parameters, xes_constants.DEFAULT_TIMESTAMP_KEY
288301
)
289302

290-
if solver.DEFAULT_LP_SOLVER_VARIANT is not None:
303+
variant = __variant_mapper(variant)
304+
305+
if (
306+
solver.DEFAULT_LP_SOLVER_VARIANT is not None
307+
and exec_utils.get_variant(variant) is not variants.dijkstra_semantics
308+
):
291309
if not check_soundness.check_easy_soundness_net_in_fin_marking(
292310
petri_net, initial_marking, final_marking
293311
):
@@ -299,8 +317,6 @@ def apply_log(
299317
Parameters.ENABLE_BEST_WORST_COST, parameters, True
300318
)
301319

302-
variant = __variant_mapper(variant)
303-
304320
start_time = time.time()
305321
max_align_time = exec_utils.get_param_value(
306322
Parameters.PARAM_MAX_ALIGN_TIME, parameters, sys.maxsize

pm4py/algo/conformance/alignments/petri_net/variants/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pm4py.algo.conformance.alignments.petri_net.variants import (
22
dijkstra_less_memory,
33
dijkstra_no_heuristics,
4+
dijkstra_semantics,
45
state_equation_a_star,
56
discounted_a_star,
67
approx_tandem_repeats,

0 commit comments

Comments
 (0)