Create the model and its markings with Petri net management or process discovery. Compare the resulting behavior using log and model evaluation.
In PM4Py, we offer various simulation algorithms that, starting from a model, generate outputs that adhere to the model and the rules provided by the user.
Playout APIs return a legacy EventLog, so len(simulated_log) counts traces. To analyze its events with Pandas, use pm4py.convert_to_dataframe(simulated_log).
A playout of a Petri net takes a Petri net and an initial marking as input, returning a list of process executions permitted by the process model. We offer two types of playouts:
| Variant | Description |
|---|---|
| Variants.BASIC_PLAYOUT | A basic playout that accepts a Petri net along with an initial marking and returns a specified number of process executions (with possible repetitions). |
| Variants.EXTENSIVE | A playout that accepts a Petri net and an initial marking, returning all possible executions according to the model, up to a specified trace length (this may be computationally expensive). |
The parameters for each variant are as follows:
| Variant | Parameter | Description |
|---|---|---|
| Variants.BASIC_PLAYOUT | Parameters.ACTIVITY_KEY | The name of the attribute to use as activity in the playout log. |
| Parameters.TIMESTAMP_KEY | The name of the attribute to use as timestamp in the playout log. | |
| Parameters.CASE_ID_KEY | The trace attribute to be used as the case identifier in the playout log. | |
| Parameters.NO_TRACES | The number of traces to be included in the playout log. | |
| Parameters.MAX_TRACE_LENGTH | The maximum trace length, after which the playout of the trace stops. | |
| Variants.EXTENSIVE | Parameters.ACTIVITY_KEY | The name of the attribute to use as activity in the playout log. |
| Parameters.TIMESTAMP_KEY | The name of the attribute to use as timestamp in the playout log. | |
| Parameters.CASE_ID_KEY | The trace attribute to be used as case identifier in the playout log. | |
| Parameters.MAX_TRACE_LENGTH | The maximum trace length, after which the extensive playout stops. |
Here's an example of applying the basic playout, given a Petri net, to generate a log with 50 traces:
from pm4py.algo.simulation.playout.petri_net import algorithm as simulator
from pm4py.objects.log.obj import EventLog
if __name__ == "__main__":
simulated_log: EventLog = simulator.apply(net, im, variant=simulator.Variants.BASIC_PLAYOUT, parameters={simulator.Variants.BASIC_PLAYOUT.value.Parameters.NO_TRACES: 50})Here's an example of applying the extensive playout, given a Petri net, to generate a log with all executions of length ≤ 7:
from pm4py.algo.simulation.playout.petri_net import algorithm as simulator
from pm4py.objects.log.obj import EventLog
if __name__ == "__main__":
simulated_log: EventLog = simulator.apply(net, im, variant=simulator.Variants.EXTENSIVE, parameters={simulator.Variants.EXTENSIVE.value.Parameters.MAX_TRACE_LENGTH: 7}) The Monte Carlo simulator requires a legacy EventLog. The example converts the DataFrame at that API call and retains the original DataFrame for timestamp analysis.
A time-related simulation allows you to determine the probability of process execution completion within a given time frame. This leads to a better estimation of Service Level Agreements or a better identification of process instances likely to have long throughput times.
This process begins with a performance DFG (e.g., the one discovered from the running example log):
import os
import pm4py
import pandas
if __name__ == "__main__":
log: pandas.DataFrame = pm4py.read_xes(os.path.join("tests", "input_data", "running-example.xes"))
dfg_perf: dict
sa: dict
ea: dict
dfg_perf, sa, ea = pm4py.discover_performance_dfg(log)Additionally, the case arrival ratio must be known. This refers to the average (or median) time between the arrival of two consecutive cases. It can be provided by the user or inferred from the event log using the following command:
import pm4py
if __name__ == "__main__":
ratio: dict[str, int] = pm4py.get_rework_cases_per_activity(log)
print(ratio)Using the DFG mining approach, you can extract a Petri net model. This model is the “default” choice for Monte Carlo simulation, as its execution semantics are clear. Moreover, the Petri net generated by DFG mining is a sound workflow net, which ensures good properties for the model.
The DFG mining approach can be applied as follows:
import pm4py
from pm4py.objects.petri_net.obj import Marking, PetriNet
if __name__ == "__main__":
net: PetriNet
im: Marking
fm: Marking
net, im, fm = pm4py.convert_to_petri_net(dfg_perf, sa, ea)To perform a basic Monte Carlo simulation, the following code can be used. This example assumes a resource-constrained simulation, where a place can hold at most 1 token at a time. Later, we’ll show how to allow more tokens per place.
from pm4py.algo.simulation.montecarlo import algorithm as montecarlo_simulation
from pm4py.algo.conformance.tokenreplay.algorithm import Variants
from pm4py.objects.log.obj import EventLog
if __name__ == "__main__":
parameters: dict = {}
parameters[
montecarlo_simulation.Variants.PETRI_SEMAPH_FIFO.value.Parameters.TOKEN_REPLAY_VARIANT
] = Variants.BACKWARDS
parameters[
montecarlo_simulation.Variants.PETRI_SEMAPH_FIFO.value.Parameters.PARAM_CASE_ARRIVAL_RATIO
] = 10800
simulated_log: EventLog
res: dict
simulated_log, res = montecarlo_simulation.apply(
pm4py.convert_to_event_log(log), net, im, fm, parameters=parameters
)The main outputs of the simulation process are:
| Key | Description |
|---|---|
| simulated_log | The traces simulated during the process. |
| res | The result of the simulation (Python dictionary). |
The result dictionary, res, includes the following keys:
| Key | Description |
|---|---|
| output_places_interval_trees | An interval tree for each place, containing intervals when it was "full" according to the specified maximum tokens per place. |
| output_transitions_interval_trees | An interval tree for each transition (keyed by transition name), showing time intervals when the transition was enabled but not yet fired. |
| cases_ex_time | A list of throughput times for all cases in the log. |
| median_cases_ex_time | The median throughput time for cases in the simulated log. |
| case_arrival_ratio | The case arrival ratio used in simulation; if not provided, it is inferred from the event log. |
| total_cases_time | The time difference between the last and first timestamps of the simulated log. |
The last four items in the list are basic Python objects (e.g., floats and lists). The interval tree objects can be queried to obtain time-specific information. For example, the following code snippet prints, for a random transition, the number of overlapping intervals at 11 different time points (including the minimum and maximum timestamps):
import random
from typing import Any
if __name__ == "__main__":
last_timestamp: float = log["time:timestamp"].max().timestamp()
first_timestamp: float = log["time:timestamp"].min().timestamp()
pick_trans: Any = random.choice(list(res["output_transitions_interval_trees"]))
print(pick_trans)
n_div: int = 10
i: int = 0
while i < n_div:
timestamp = first_timestamp + (last_timestamp - first_timestamp)/n_div * i
print("\t", timestamp, len(res["output_transitions_interval_trees"][pick_trans][timestamp]))
i = i + 1Similarly, the following code snippet prints, for a random place, the number of overlapping intervals at 11 different time points:
import random
from typing import Any
if __name__ == "__main__":
last_timestamp: float = log["time:timestamp"].max().timestamp()
first_timestamp: float = log["time:timestamp"].min().timestamp()
pick_place: Any = random.choice(list(res["output_places_interval_trees"]))
print(pick_place)
n_div: int = 10
i: int = 0
while i < n_div:
timestamp = first_timestamp + (last_timestamp - first_timestamp)/n_div * i
print("\t", timestamp, len(res["output_places_interval_trees"][pick_place][timestamp]))
i = i + 1This information can be used to build graphs using tools like Microsoft Excel.
The simulation process can be summarized as follows:
Several parameters are essential for performing a Monte Carlo simulation. These parameters, found in the petri_semaph_fifo class, are listed below in order of importance:
| Variant | Parameter | Description |
|---|---|---|
| Variants.PETRI_SEMAPH_FIFO | Parameters.PARAM_NUM_SIMULATIONS | Number of simulations to run (the goal is to generate this number of traces in the model). |
| Parameters.PARAM_CASE_ARRIVAL_RATIO | The case arrival ratio; if not provided, it is inferred from the log. | |
| Parameters.PARAM_MAP_RESOURCES_PER_PLACE | A map specifying the maximum number of tokens for each place in the Petri net. | |
| Parameters.PARAM_DEFAULT_NUM_RESOURCES_PER_PLACE | If the map is not provided, this default number of resources per place is used. | |
| Parameters.PARAM_SMALL_SCALE_FACTOR | Compatibility parameter; not used in the current generator-based implementation. | |
| Parameters.ACTIVITY_KEY | The attribute of the log to use as activity. | |
| Parameters.TIMESTAMP_KEY | The attribute of the log to use as timestamp. | |
| Parameters.PARAM_FORCE_DISTRIBUTION | If specified, forces a specific distribution for the transitions (e.g., normal or exponential). | |
| Parameters.PARAM_PROVIDED_SMAP | Optional precomputed stochastic map for transitions; if not provided, it is estimated from the log and model. | |
| Parameters.PARAM_DIAGN_INTERVAL | Compatibility parameter; not used in the current generator-based implementation. | |
| Parameters.PARAM_ENABLE_DIAGNOSTICS | Compatibility parameter; not used in the current generator-based implementation. |
An extensive playout operation allows the generation (up to the specified limits) of the entire language of the process model. Performing an extensive playout on a Petri net can be computationally expensive, as it requires exploring the reachability graph. Process trees, with their bottom-up structure, make it much easier to generate the entire language of an event log. The process starts from the language of the leaves (which is straightforward) and follows specific merge rules for the operators.
However, since the language of a process tree can be vast (especially when parallel operators are involved) or even infinite (when loops are present), extensive playouts are possible only within certain limits:
Additionally, the structure of the process tree makes it easy to infer the minimum trace length allowed by the process model (following the bottom-up approach).
Some reasonable default settings for the extensive playout are as follows:
The list of parameters for the extensive playout is as follows:
| Parameter | Description |
|---|---|
| MAX_LIMIT_NUM_TRACES | Maximum number of traces returned by the algorithm. |
| MAX_TRACE_LENGTH | Maximum length of a trace output by the algorithm. |
| MAX_LOOP_OCC | Maximum number of times a loop can be entered. |
Below is an example of how the playout can be executed. First, a log can be imported:
import pm4py
import os
import pandas
if __name__ == "__main__":
log: pandas.DataFrame = pm4py.read_xes(os.path.join("tests", "input_data", "receipt.xes"))Next, a process tree can be discovered using the inductive miner algorithm:
from pm4py.objects.process_tree.obj import ProcessTree
if __name__ == "__main__":
tree: ProcessTree = pm4py.discover_process_tree_inductive(log)In this example, we specify retrieving traces of length at most 3, and we set the maximum number of traces to 100,000.
from pm4py.algo.simulation.playout.process_tree import algorithm as tree_playout
from pm4py.objects.log.obj import EventLog
from typing import Any
if __name__ == "__main__":
playout_variant: Any = tree_playout.Variants.EXTENSIVE
param: Any = tree_playout.Variants.EXTENSIVE.value.Parameters
simulated_log: EventLog = tree_playout.apply(
tree,
variant=playout_variant,
parameters={
param.MAX_TRACE_LENGTH: 3,
param.MAX_LIMIT_NUM_TRACES: 100000
}
)
print(len(simulated_log))At this point, the extensive playout operation is complete.