From 5bfa15d5902c8d9a7c6eeeb2cf5c6ef7da2ff011 Mon Sep 17 00:00:00 2001
From: "Maximilian Josef Frank (https://orcid.org/0000-0002-0714-7748)" <>
Date: Thu, 12 Feb 2026 23:28:27 +0000
Subject: [PATCH] Implementing the Genetic Miner
---
docs/source/api.rst | 2 +
examples/genetic_miner.py | 17 +
pm4py/__init__.py | 1 +
pm4py/algo/discovery/__init__.py | 1 +
pm4py/algo/discovery/genetic/__init__.py | 22 +
pm4py/algo/discovery/genetic/algorithm.py | 77 ++++
pm4py/algo/discovery/genetic/util.py | 87 ++++
.../discovery/genetic/variants/__init__.py | 22 +
.../discovery/genetic/variants/classic.py | 377 ++++++++++++++++++
pm4py/cli.py | 7 +
pm4py/convert.py | 13 +-
pm4py/discovery.py | 83 +++-
pm4py/objects/__init__.py | 1 +
pm4py/objects/conversion/__init__.py | 1 +
.../conversion/genetic_matrix/__init__.py | 28 ++
.../conversion/genetic_matrix/converter.py | 50 +++
.../genetic_matrix/variants/__init__.py | 25 ++
.../genetic_matrix/variants/to_petri_net.py | 149 +++++++
pm4py/objects/genetic_matrix/__init__.py | 25 ++
pm4py/objects/genetic_matrix/obj.py | 63 +++
requirements.txt | 1 +
requirements_complete.txt | 1 +
requirements_stable.txt | 1 +
tests/geneticminer_test.py | 331 +++++++++++++++
tests/polars_process_discovery_test.py | 7 +
third_party/LICENSES_TRANSITIVE.md | 15 +-
third_party/func_timeout.LICENSE | 165 ++++++++
27 files changed, 1561 insertions(+), 11 deletions(-)
create mode 100644 examples/genetic_miner.py
create mode 100644 pm4py/algo/discovery/genetic/__init__.py
create mode 100644 pm4py/algo/discovery/genetic/algorithm.py
create mode 100644 pm4py/algo/discovery/genetic/util.py
create mode 100644 pm4py/algo/discovery/genetic/variants/__init__.py
create mode 100644 pm4py/algo/discovery/genetic/variants/classic.py
create mode 100644 pm4py/objects/conversion/genetic_matrix/__init__.py
create mode 100644 pm4py/objects/conversion/genetic_matrix/converter.py
create mode 100644 pm4py/objects/conversion/genetic_matrix/variants/__init__.py
create mode 100644 pm4py/objects/conversion/genetic_matrix/variants/to_petri_net.py
create mode 100644 pm4py/objects/genetic_matrix/__init__.py
create mode 100644 pm4py/objects/genetic_matrix/obj.py
create mode 100755 tests/geneticminer_test.py
create mode 100644 third_party/func_timeout.LICENSE
diff --git a/docs/source/api.rst b/docs/source/api.rst
index 82f6c19491..8db60c76e8 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -90,6 +90,7 @@ Among *procedural process models*, ``pm4py`` currently supports:
* :meth:`pm4py.discovery.discover_petri_net_inductive`; discovers a *Petri net* using the Inductive Miner algorithm.
* :meth:`pm4py.discovery.discover_petri_net_heuristics`; discovers a *Petri net* using the Heuristics Miner algorithm.
* :meth:`pm4py.discovery.discover_petri_net_ilp`; discovers a *Petri net* using the ILP Miner algorithm.
+ * :meth:`pm4py.discovery.discover_petri_net_genetic`; discovers a *Petri net* using the Genetic Miner algorithm.
* :meth:`pm4py.discovery.discover_process_tree_inductive`; discovers a *process tree* using the Inductive Miner algorithm.
* :meth:`pm4py.discovery.discover_bpmn_inductive`; discovers a *BPMN model* using the Inductive Miner algorithm.
* :meth:`pm4py.discovery.discover_heuristics_net`; discovers a *heuristics net* using the Heuristics Miner algorithm.
@@ -466,6 +467,7 @@ List of Methods
pm4py.discovery.discover_petri_net_inductive
pm4py.discovery.discover_petri_net_heuristics
pm4py.discovery.discover_petri_net_ilp
+ pm4py.discovery.discover_petri_net_genetic
pm4py.discovery.discover_process_tree_inductive
pm4py.discovery.discover_heuristics_net
pm4py.discovery.derive_minimum_self_distance
diff --git a/examples/genetic_miner.py b/examples/genetic_miner.py
new file mode 100644
index 0000000000..148d1110f4
--- /dev/null
+++ b/examples/genetic_miner.py
@@ -0,0 +1,17 @@
+import pm4py
+import os
+import importlib.util
+from examples import examples_conf
+from pm4py.algo.discovery.genetic.algorithm import Parameters
+
+
+def execute_script():
+ log = pm4py.read_xes(os.path.join("..", "tests", "input_data", "running-example.xes"))
+ net, im, fm = pm4py.discover_petri_net_genetic(log, population_size = 20, generations = 30)
+
+ if importlib.util.find_spec("graphviz"):
+ pm4py.view_petri_net(net, im, fm, format=examples_conf.TARGET_IMG_FORMAT)
+
+
+if __name__ == "__main__":
+ execute_script()
diff --git a/pm4py/__init__.py b/pm4py/__init__.py
index 063923e880..cffa9c56bf 100644
--- a/pm4py/__init__.py
+++ b/pm4py/__init__.py
@@ -138,6 +138,7 @@
discover_petri_net_ilp,
discover_petri_net_heuristics,
discover_petri_net_inductive,
+ discover_petri_net_genetic,
discover_process_tree_inductive,
discover_heuristics_net,
discover_dfg,
diff --git a/pm4py/algo/discovery/__init__.py b/pm4py/algo/discovery/__init__.py
index 957242ec02..458dabef47 100644
--- a/pm4py/algo/discovery/__init__.py
+++ b/pm4py/algo/discovery/__init__.py
@@ -33,6 +33,7 @@
heuristics,
ilp,
inductive,
+ genetic,
log_skeleton,
minimum_self_distance,
ocel,
diff --git a/pm4py/algo/discovery/genetic/__init__.py b/pm4py/algo/discovery/genetic/__init__.py
new file mode 100644
index 0000000000..2118412530
--- /dev/null
+++ b/pm4py/algo/discovery/genetic/__init__.py
@@ -0,0 +1,22 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.discovery.genetic import variants, algorithm
diff --git a/pm4py/algo/discovery/genetic/algorithm.py b/pm4py/algo/discovery/genetic/algorithm.py
new file mode 100644
index 0000000000..3afa746ef4
--- /dev/null
+++ b/pm4py/algo/discovery/genetic/algorithm.py
@@ -0,0 +1,77 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from enum import Enum
+from pm4py.util import exec_utils
+from pm4py.algo.discovery.genetic.variants import classic
+from typing import Union, Optional, Dict, Any, Tuple
+from pm4py.objects.petri_net.obj import PetriNet, Marking
+from pm4py.objects.log.obj import EventLog, EventStream
+import pandas as pd
+from pm4py.util import constants
+
+
+class Parameters(Enum):
+ ACTIVITY_KEY = constants.PARAMETER_CONSTANT_ACTIVITY_KEY
+ TIMESTAMP_KEY = constants.PARAMETER_CONSTANT_TIMESTAMP_KEY
+ CASE_ID_KEY = constants.PARAMETER_CONSTANT_CASEID_KEY
+ POPULATION_SIZE = "population_size"
+ ELITISM_RATE = "elitism_rate"
+ CROSSOVER_RATE = "crossover_rate"
+ MUTATION_RATE = "mutation_rate"
+ GENERATIONS = "generations"
+ ELITISM_MIN_SAMPLE = "elitism_min_sample"
+ TOURNAMENT_TIMEOUT = "tournament_timeout"
+ LOG_CSV = "log_csv"
+
+
+class Variants(Enum):
+ CLASSIC = classic
+
+
+def apply(
+ log: Union[EventLog, EventStream, pd.DataFrame],
+ variant=Variants.CLASSIC,
+ parameters: Optional[Dict[Any, Any]] = None,
+) -> Tuple[PetriNet, Marking, Marking]:
+ """
+ Discovers a Petri net using the genetic miner.
+
+ Parameters
+ ---------------
+ log
+ Event log / Event stream / Pandas dataframe
+ variant
+ Variant of the algorithm to be used, possible values:
+ - Variants.CLASSIC
+ parameters
+ Variant-specific parameters
+
+ Returns
+ ---------------
+ net
+ Petri net
+ im
+ Initial marking
+ fm
+ Final marking
+ """
+ return exec_utils.get_variant(variant).apply(log, parameters)
diff --git a/pm4py/algo/discovery/genetic/util.py b/pm4py/algo/discovery/genetic/util.py
new file mode 100644
index 0000000000..9abff25c41
--- /dev/null
+++ b/pm4py/algo/discovery/genetic/util.py
@@ -0,0 +1,87 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+
+# Author: Maximilian Josef Frank (https://orcid.org/0000-0002-0714-7748)
+
+import random
+import itertools
+
+# typing
+from collections.abc import Iterable
+InputMap = dict[str,list[frozenset]]
+OutputMap = dict[str,list[frozenset]]
+Individual = tuple[InputMap,OutputMap]
+
+class iset(frozenset):
+ "Indexable frozenset printing as set, i.e. without `frozenset(…)`"
+ def __repr__(self):
+ return "{" + repr(sorted(self))[1:-1] + "}"
+
+ @staticmethod
+ def flat(item: Iterable) -> Self:
+ return iset(itertools.chain(*item))
+
+def rand_partition(pool: Iterable) -> list[set]:
+ pool = set(pool)
+ # also ensures no activity in two partitions
+ # s. 4. Causal Matrix, Def. 4; https://doi.org/10.1007/11494744_5
+ partition = []
+ while pool:
+ draw = iset(random.sample(
+ tuple(pool),
+ random.randint(1, len(pool))
+ ))
+ partition.append(draw)
+ pool -= draw
+ return partition
+
+def get_src_sink_sets_for_wfnet(I: InputMap, O: OutputMap, T: list[str]) -> tuple[list[str],list[str]]:
+ """Determines input set and output set, which need to be connected by a place to create a WF-net"""
+ def add2graphs(graphs, t, nextT):
+ # find graph
+ graph = next((g for g in graphs if t in g), None)
+ if graph is None:
+ graph = [t]
+ graphs.append(graph)
+ # append plain/grouped T
+ successors = []
+ for S in nextT[t]:
+ if type(S) == str:
+ successors.append(S)
+ else:
+ successors.extend(S)
+ graph += successors
+ # merge if end = start
+ for tn in successors:
+ for g2 in graphs[:]: # [:] = copy
+ if g2[0] == tn and g2 != graph:
+ graph += g2
+ graphs.remove(g2)
+ break
+ return graphs
+ graphsI, graphsO = [], []
+ for t in T:
+ graphsI = add2graphs(graphsI, t, I)
+ graphsO = add2graphs(graphsO, t, O)
+ first = [g[0] for g in graphsO] # ⋃first = reachable via O[∀t]
+ last = [g[0] for g in graphsI] # ⋃first = reachable via I[∀t]
+ return first, last
diff --git a/pm4py/algo/discovery/genetic/variants/__init__.py b/pm4py/algo/discovery/genetic/variants/__init__.py
new file mode 100644
index 0000000000..f021ce92e8
--- /dev/null
+++ b/pm4py/algo/discovery/genetic/variants/__init__.py
@@ -0,0 +1,22 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.discovery.genetic.variants import classic
diff --git a/pm4py/algo/discovery/genetic/variants/classic.py b/pm4py/algo/discovery/genetic/variants/classic.py
new file mode 100644
index 0000000000..f62347ea58
--- /dev/null
+++ b/pm4py/algo/discovery/genetic/variants/classic.py
@@ -0,0 +1,377 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+
+# Author: Maximilian Josef Frank (https://orcid.org/0000-0002-0714-7748)
+
+from collections import defaultdict
+import copy
+import csv
+from datetime import datetime
+from func_timeout import func_timeout, FunctionTimedOut
+from tqdm import tqdm
+import numpy
+import random
+import itertools
+
+import pm4py
+from pm4py.util import exec_utils, constants
+from pm4py.util import xes_constants as xes
+from pm4py.algo.discovery.genetic.algorithm import Parameters
+from pm4py.objects.conversion.genetic_matrix.variants.to_petri_net import apply as matrix2petrinet
+from pm4py.algo.discovery.genetic.util import get_src_sink_sets_for_wfnet, iset, rand_partition
+from pm4py.objects.genetic_matrix.obj import GeneticMatrix
+
+# typing
+import typing
+from typing import Union, TextIO, Self
+from pandas.core.frame import DataFrame
+from pm4py.objects.log.obj import EventLog
+from pm4py.objects.petri_net.obj import PetriNet, Marking
+from pm4py.algo.discovery.genetic.util import InputMap, OutputMap, Individual
+
+
+def apply(
+ log: EventLog,
+ parameters: Optional[Dict[Union[str, Parameters], Any]] = None,
+) -> Tuple[PetriNet, Marking, Marking]:
+ """
+ Discovers a Petri net using Genetic Miner
+
+ Reference paper:
+ Maximilian Josef Frank. "Optimising and Implementing the Genetic Miner in PM4Py" (2026).
+
+ Parameters
+ ------------
+ log
+ Event log
+ parameters
+ Possible parameters of the algorithm,
+ including:
+ - Parameters.ACTIVITY_KEY
+ - Parameters.TIMESTAMP_KEY
+ - Parameters.CASE_ID_KEY
+ - Parameters.POPULATION_SIZE
+ - Parameters.ELITISM_RATE
+ - Parameters.CROSSOVER_RATE
+ - Parameters.MUTATION_RATE
+ - Parameters.GENERATIONS
+ - Parameters.ELITISM_MIN_SAMPLE
+ - Parameters.TOURNAMENT_TIMEOUT
+ - Parameters.LOG_CSV
+
+ Returns
+ ------------
+ net
+ Petri net
+ im
+ Initial marking
+ fm
+ Final marking
+ """
+ if parameters is None:
+ parameters = {}
+
+ activity_key = exec_utils.get_param_value(
+ Parameters.ACTIVITY_KEY, parameters, xes.DEFAULT_NAME_KEY
+ )
+ timestamp_key = exec_utils.get_param_value(
+ Parameters.TIMESTAMP_KEY, parameters, xes.DEFAULT_TIMESTAMP_KEY
+ )
+ case_id_key = exec_utils.get_param_value(
+ Parameters.CASE_ID_KEY, parameters, constants.CASE_CONCEPT_NAME
+ )
+
+ population_size = exec_utils.get_param_value(
+ Parameters.POPULATION_SIZE, parameters, 500
+ )
+ elitism_rate = exec_utils.get_param_value(
+ Parameters.ELITISM_RATE, parameters, 0.01
+ )
+ crossover_rate = exec_utils.get_param_value(
+ Parameters.CROSSOVER_RATE, parameters, 1.0
+ )
+ mutation_rate = exec_utils.get_param_value(
+ Parameters.MUTATION_RATE, parameters, 0.01
+ )
+ generations = exec_utils.get_param_value(
+ Parameters.GENERATIONS, parameters, 100
+ )
+ elitism_min_sample = exec_utils.get_param_value(
+ Parameters.ELITISM_MIN_SAMPLE, parameters, 5
+ )
+ tournament_timeout = exec_utils.get_param_value(
+ Parameters.TOURNAMENT_TIMEOUT, parameters, None
+ )
+ log_csv = exec_utils.get_param_value(
+ Parameters.LOG_CSV, parameters, None
+ )
+
+ # configure parameters
+ if population_size < 2:
+ raise ValueError("population_size < 2: You need at least two parents for each next generation, thus at least a population size of 2.")
+ if elitism_min_sample > population_size:
+ elitism_min_sample = population_size - 1
+ if elitism_min_sample < 1:
+ raise ValueError("elitism_min_sample < 1: No empty samples allowed.")
+ if log_csv:
+ log_csv = csv.writer(log_csv)
+ log_csv.writerow(['timestamp', 'generation'] + [f'fitness{i}' for i in range(population_size)])
+ # @src 6.3. Genetic Operations; https://doi.org/10.1007/11494744_5
+ T = tuple(log[activity_key].unique())
+ if tournament_timeout is None or tournament_timeout < 1:
+ tournament_timeout = 3 * len(T) * log.shape[0] * (46 / 1202267 / 26)
+ # based on previous runs: 46s for 1202267 traces with 26 activities
+ if tournament_timeout < 1:
+ tournament_timeout = 1
+ history = []
+ population = individuals(log, population_size, T, { # [(I,O), …]
+ "activity_key":activity_key, "timestamp_key":timestamp_key, "case_id_key":case_id_key
+ })
+ for _ in tqdm(range(generations), "Genetic generations"):
+ population, fitness = tournament(tqdm(population, f"└─Tournament {len(history)}"), log, T, sort=True, timeout=tournament_timeout)
+ if log_csv:
+ log_csv.writerow([datetime.now(), len(history)] + fitness)
+ if fitness[0] == 1 or (history and all(f == fitness[0] for f in history[-int(generations/2):])):
+ break
+ history.append(fitness[0])
+ # elitism
+ next_population = population[:round(elitism_rate * population_size)]
+ # fill with offsprings
+ while len(next_population) < population_size:
+ parent1, parent2 = sample_parents(population, fitness, elitism_min_sample)
+ if random.random() < crossover_rate:
+ offsprings = crossover(parent1, parent2, T)
+ else:
+ offsprings = copy.deepcopy((parent1, parent2))
+ for offspring in offsprings:
+ if len(next_population) < population_size:
+ offspring = mutate(offspring, mutation_rate)
+ next_population.append(offspring)
+ population = next_population
+ return matrix2petrinet(GeneticMatrix(*population[0], T))
+
+def individuals(log: Union[DataFrame, EventLog], sample_size=1, T=None, keys: dict[str,str] = {"activity_key":xes.DEFAULT_NAME_KEY, "timestamp_key":xes.DEFAULT_TIMESTAMP_KEY, "case_id_key":constants.CASE_CONCEPT_NAME}) -> list[Individual]:
+ if not T:
+ T = tuple(log[keys['activity_key']].unique())
+ # @src 6.1. Initial Population; https://doi.org/10.1007/11494744_5
+ # create matrix C
+ C = numpy.zeros((len(T), len(T)))
+ for _,group in tqdm(log.sort_values(keys['timestamp_key'], ascending=True).groupby(keys['case_id_key']), desc="Find consecutive activities"):
+ for row1,row2 in itertools.pairwise(map(lambda r: r[1], group.iterrows())):
+ i = T.index(row1[keys['activity_key']])
+ o = T.index(row2[keys['activity_key']])
+ C[i,o] += 1
+ Cn = C / C.sum(axis=1)[:,None] # normalise row-wise
+ samples = []
+ for sample in tqdm(range(sample_size), "Initial population"):
+ I,O = defaultdict(list), defaultdict(list)
+ for i,o in numpy.ndindex(C.shape):
+ if random.random() < Cn[i,o]: # [0,1[ < [0,1]; 0 < 0 = false
+ I[T[i]].append(T[o])
+ O[T[o]].append(T[i])
+ I,O = repair(I, O, C, T)
+ # partitioning already ensures no T in >1 partitions
+ # s. 4. Causal Matrix, Def. 4; https://doi.org/10.1007/11494744_5
+ for i in I.keys():
+ I[i] = rand_partition(I[i])
+ for o in O.keys():
+ O[o] = rand_partition(O[o])
+ samples.append((I,O))
+ return samples
+
+def tournament(population: list[Individual], log: Union[DataFrame, EventLog], T, sort=True, timeout=1) -> tuple[list[Individual],list[float]]:
+ """sort=True: sort descending by fitness (i.e. best first)"""
+ # @src 6.2. Fitness Calculation; https://doi.org/10.1007/11494744_5
+ fitness = []
+ for i,(I,O) in enumerate(population):
+ model = matrix2petrinet(GeneticMatrix(I,O,T))
+ try:
+ metrics = func_timeout(
+ timeout = timeout,
+ func = pm4py.fitness_token_based_replay,
+ args = (log, *model)
+ )
+ except FunctionTimedOut:
+ print("\tTimeout for individual", i)
+ metrics = defaultdict(lambda: 0)
+ fitness.append(
+ 0.4 * metrics['average_trace_fitness']
+ # @see https://pm4py-source.readthedocs.io/en/stable/_modules/pm4py/algo/evaluation/replay_fitness/variants/token_replay.html#evaluate
+ # average_fitness = sum_fitness / #traces
+ # = ∑ fitness per trace / #traces
+ # ∑ fitness per trace * len(log)/#traces
+ # = ––––––––––––––––––––––––––––––––––––––
+ # #traces * len(log)/#traces
+ # ∑ fitness per row in log ∑ fitness per activity
+ # = –––––––––––––––––––––––– = ––––––––––––––––––––––
+ # len(log) len(log)
+ +
+ 0.6 * metrics['percentage_of_fitting_traces']/100
+ )
+ if sort:
+ population, fitness = zip(*sorted(
+ zip(population, fitness),
+ key=lambda obj: obj[1],
+ reverse=True
+ ))
+ population, fitness = list(population), list(fitness)
+ return (population, fitness)
+
+def sample_parents(population: list[Individual], fitness: list[float], elitism_min_sample: int):
+ population_fitness = tuple(zip(population, fitness))
+ parent1 = sorted(
+ random.sample(population_fitness, k=elitism_min_sample),
+ key=lambda obj: obj[1],
+ reverse=True
+ )[0][0]
+ parent2 = sorted(
+ random.sample(
+ [(i,f) for i,f in population_fitness if i!=parent1],
+ k=elitism_min_sample
+ ),
+ key=lambda obj: obj[1],
+ reverse=True
+ )[0][0]
+ return (parent1, parent2)
+
+def crossover(parent1: Individual, parent2: Individual, T: list[str]) -> tuple[Individual,Individual]:
+ # @src 6.3. Genetic Operations: Crossover; https://doi.org/10.1007/11494744_5
+ # 1. cross-over point
+ t = random.choice(T)
+ # 2. clone parents
+ I1,O1 = offspring1 = copy.deepcopy(parent1)
+ I2,O2 = offspring2 = copy.deepcopy(parent2)
+ # 3. swap and recombine
+ if I1[t] and I2[t]:
+ swap_point = random.randrange(min(len(I1[t]), len(I2[t])))
+ toI1, toI2 = I2[t][swap_point:], I1[t][swap_point:]
+ # no T can exist twice in I/O[t], s. Def. 4; https://doi.org/10.1007/11494744_5
+ # COPY of I_i, else not properly removed in opposite I_j
+ I1_flat = iset.flat(I1[t][:swap_point-1])
+ toI1_dedup = [ iset(S-I1_flat) for S in toI1 ]
+ I2_flat = iset.flat(I2[t][:swap_point-1])
+ toI2_dedup = [ iset(S-I2_flat) for S in toI2 ]
+ # merge
+ I1[t], I2[t] = I1[t][:swap_point-1] + toI1_dedup, I2[t][:swap_point-1] + toI2_dedup
+ # @src 6.3. Genetic Operations: Update Related Elements; https://doi.org/10.1007/11494744_5
+ for c in iset.flat(toI1) - iset.flat(toI2): # no reassign staying T
+ O1[c].append(iset({t}))
+ for i,p in enumerate(O2[c]): # p = only local var
+ if t in p:
+ O2[c][i] = iset(p - {t})
+ if not O2[c][i]:
+ O2[c].remove(O2[c][i])
+ break
+ for c in iset.flat(toI2) - iset.flat(toI1): # no reassign staying T
+ O2[c].append(iset({t}))
+ for i,p in enumerate(O1[c]): # p = only local var
+ if t in p:
+ O1[c][i] = iset(p - {t})
+ if not O1[c][i]:
+ O1[c].remove(O1[c][i])
+ break
+ if O1[t] and O2[t]:
+ swap_point = random.randrange(min(len(O1[t]), len(O2[t])))
+ toO1, toO2 = O2[t][swap_point:], O1[t][swap_point:]
+ # no T can exist twice in I/O[t], s. Def. 4; https://doi.org/10.1007/11494744_5
+ # COPY of I_i, else not properly removed in opposite I_j
+ O1_flat = iset.flat(O1[t][:swap_point-1])
+ toO1_dedup = [ iset(S-O1_flat) for S in toO1 ]
+ O2_flat = iset.flat(O2[t][:swap_point-1])
+ toO2_dedup = [ iset(S-O2_flat) for S in toO2 ]
+ # merge
+ O1[t], O2[t] = O1[t][:swap_point-1] + toO1_dedup, O2[t][:swap_point-1] + toO2_dedup
+ # @src 6.3. Genetic Operations: Update Related Elements; https://doi.org/10.1007/11494744_5
+ for c in iset.flat(toO1) - iset.flat(toO2):
+ I1[c].append(iset({t}))
+ for i,p in enumerate(I2[c]): # p = only local var
+ if t in p:
+ I2[c][i] = iset(p - {t})
+ if not I2[c][i]:
+ I2[c].remove(I2[c][i])
+ break
+ for c in iset.flat(toO2) - iset.flat(toO1):
+ I2[c].append(iset({t}))
+ for i,p in enumerate(I1[c]): # p = only local var
+ if t in p:
+ I1[c][i] = iset(p - {t})
+ if not I1[c][i]:
+ I1[c].remove(I1[c][i])
+ break
+ return (offspring1, offspring2)
+
+def mutate(individual: Individual, rate: float = 0.01) -> Individual:
+ # @src 6.3. Genetic Operations: Mutation; https://doi.org/10.1007/11494744_5
+ I,O = individual
+ for t in I.keys():
+ if random.random() < rate:
+ I[t] = rand_partition(itertools.chain(*I[t]))
+ for t in O.keys():
+ if random.random() < rate:
+ O[t] = rand_partition(itertools.chain(*O[t]))
+ return (I,O)
+
+def repair(I: list[str], O: list[str], C: numpy.ndarray, T: list[str]) -> tuple[list[str]]:
+ """
+ Merging partitions until petri-net is (coherent) workflow net
+ s. last requirement in 4. Causal Matrix, Def. 4; https://doi.org/10.1007/11494744_5
+ """
+ # WF-Net Definition: each node on path from i to o
+ # → re-connect partitions of each I and O, not I and O together
+ left = set(T)
+ partitions = []
+ while left:
+ partition = set()
+ Tn = {left.pop()}
+ while Tn:
+ t = Tn.pop() # also removes from Tn
+ left.discard(t) # remove without Exception (s. initial Tn)
+ partition.add(t)
+ Tn |= (set(I[t]) | set(O[t])) & left
+ partitions.append(partition)
+ def rand_connect_one(I: InputMap, O: OutputMap, Ti: list[set], To: list[set], C: numpy.ndarray) -> Individual:
+ comb = tuple(itertools.product(Ti, To))
+ try:
+ ti,to = random.choices(
+ comb,
+ weights = [ C[T.index(ti),T.index(to)] for ti,to in comb ],
+ k=1
+ )[0]
+ except ValueError:
+ ti,to = random.choice(comb)
+ I[ti].append(to)
+ O[to].append(ti)
+ return I,O
+ while len(partitions) > 1:
+ random.shuffle(partitions)
+ # merge Ti / To of partition to other partition
+ first, last = get_src_sink_sets_for_wfnet(I, O, partitions[1])
+ for i in first:
+ # connect any→i
+ I,O = rand_connect_one(I, O, [i], partitions[0], C)
+ for o in last:
+ # connect o→any
+ I,O = rand_connect_one(I, O, partitions[0], [o], C)
+ # pop first + merge with new first (i.e. old second)
+ merging = partitions.pop(0)
+ partitions[0] |= merging
+ return (I,O)
diff --git a/pm4py/cli.py b/pm4py/cli.py
index 77a28a3a20..08a9f172f8 100644
--- a/pm4py/cli.py
+++ b/pm4py/cli.py
@@ -111,6 +111,13 @@
*pm4py.discover_petri_net_heuristics(__read_log(x[0])), x[1]
),
},
+ "DiscoverPetriNetGenetic": {
+ "inputs": [".xes"],
+ "output_extension": ".pnml",
+ "method": lambda x: pm4py.write_pnml(
+ *pm4py.discover_petri_net_genetic(__read_log(x[0])), x[1]
+ ),
+ },
"DiscoverBPMNInductive": {
"inputs": [".xes"],
"output_extension": ".bpmn",
diff --git a/pm4py/convert.py b/pm4py/convert.py
index aedf7252ba..569f0ee033 100644
--- a/pm4py/convert.py
+++ b/pm4py/convert.py
@@ -33,6 +33,7 @@
from pm4py.objects.ocel.obj import OCEL
from pm4py.objects.powl.obj import POWL
from pm4py.objects.heuristics_net.obj import HeuristicsNet
+from pm4py.objects.genetic_matrix.obj import GeneticMatrix
from pm4py.objects.log.obj import EventLog, EventStream
from pm4py.objects.petri_net.obj import Marking
from pm4py.objects.process_tree.obj import ProcessTree
@@ -207,17 +208,17 @@ def convert_to_bpmn(
def convert_to_petri_net(
- *args: Union[BPMN, ProcessTree, HeuristicsNet, POWL, dict]
+ *args: Union[BPMN, ProcessTree, HeuristicsNet, GeneticMatrix, POWL, dict]
) -> Tuple[PetriNet, Marking, Marking]:
"""
Converts an input model to an (accepting) Petri net.
- The input objects can be a process tree, BPMN model, Heuristic net, POWL model, or a dictionary representing a Directly-Follows Graph (DFG).
+ The input objects can be a process tree, BPMN model, Heuristic net, GeneticMatrix, POWL model, or a dictionary representing a Directly-Follows Graph (DFG).
The output is a tuple containing the Petri net and the initial and final markings.
The markings are only returned if they can be reasonably derived from the input model.
:param args:
- - If converting from a BPMN, ProcessTree, HeuristicsNet, or POWL: a single object of the respective type.
+ - If converting from a BPMN, ProcessTree, HeuristicsNet, GeneticMatrix, or POWL: a single object of the respective type.
- If converting from a DFG: a dictionary representing the DFG, followed by lists of start and end activities.
:return: A tuple of (``PetriNet``, ``Marking``, ``Marking``).
@@ -249,6 +250,12 @@ def convert_to_petri_net(
to_petri_net,
)
+ return to_petri_net.apply(args[0])
+ elif isinstance(args[0], GeneticMatrix):
+ from pm4py.objects.conversion.genetic_matrix.variants import (
+ to_petri_net,
+ )
+
return to_petri_net.apply(args[0])
elif isinstance(args[0], dict):
# DFG
diff --git a/pm4py/discovery.py b/pm4py/discovery.py
index 3ab5f4b1ee..618f2dc51f 100644
--- a/pm4py/discovery.py
+++ b/pm4py/discovery.py
@@ -23,7 +23,7 @@
The ``pm4py.discovery`` module contains the process discovery algorithms implemented in ``pm4py``.
"""
-from typing import Tuple, Union, List, Dict, Any, Optional, Set
+from typing import Tuple, Union, List, Dict, Any, Optional, Set, TextIO
from collections import Counter
import pandas as pd
@@ -433,6 +433,87 @@ def discover_petri_net_ilp(
)
+def discover_petri_net_genetic(
+ log: Union[EventLog, pd.DataFrame],
+ population_size: int = 500,
+ elitism_rate: float = 0.01,
+ crossover_rate: float = 1.0,
+ mutation_rate: float = 0.01,
+ generations: int = 100,
+ elitism_min_sample: int = 5,
+ tournament_timeout: int = None,
+ log_csv: TextIO = None,
+ activity_key: str = "concept:name",
+ timestamp_key: str = "time:timestamp",
+ case_id_key: str = "case:concept:name",
+) -> Tuple[PetriNet, Marking, Marking]:
+ """
+ Discovers a Petri net using the Genetic Miner.
+
+ Reference paper:
+ Maximilian Josef Frank. "Optimising and Implementing the Genetic Miner in PM4Py" (2026).
+
+ :param log: Event log or Pandas DataFrame.
+ :param population_size: Population size of genetic sampling (default: 500).
+ :param elitism_rate: Rate of best models used in next generation (default: 0.01).
+ :param crossover_rate: Rate of genetically combined models (offsprings) used in next generation (default: 1.0).
+ :param mutation_rate: Random model mutation rate (default: 0.01).
+ :param generations: Iterations of model improvement (default: 100).
+ :param elitism_min_sample: Minimum sample size for selecting best models (default: 5).
+ :param tournament_timeout: Timeout in seconds for assessing individuals. If the timeout is reached, the individual will not appear in the next generation (default: factor dependent on number of activities and log size)
+ :param log_csv: Output stream for CSV log (default: None).
+ :param activity_key: Attribute to be used for the activity (default: "concept:name").
+ :param timestamp_key: Attribute to be used for the timestamp (default: "time:timestamp").
+ :param case_id_key: Attribute to be used as case identifier (default: "case:concept:name").
+ :return: A tuple containing the Petri net, initial marking, and final marking.
+ :rtype: ``Tuple[PetriNet, Marking, Marking]``
+
+ .. code-block:: python3
+
+ import pm4py
+
+ net, im, fm = pm4py.discover_petri_net_genetic(
+ dataframe,
+ activity_key='concept:name',
+ timestamp_key='time:timestamp',
+ case_id_key='case:concept:name'
+ )
+ """
+ __event_log_deprecation_warning(log)
+
+ from pm4py.algo.discovery.genetic import algorithm as genetic_miner
+
+ genetic_parameters = genetic_miner.Parameters
+ parameters = get_properties(
+ log,
+ activity_key=activity_key,
+ timestamp_key=timestamp_key,
+ case_id_key=case_id_key,
+ )
+ parameters[genetic_parameters.POPULATION_SIZE] = population_size
+ parameters[genetic_parameters.ELITISM_RATE] = elitism_rate
+ parameters[genetic_parameters.CROSSOVER_RATE] = crossover_rate
+ parameters[genetic_parameters.MUTATION_RATE] = mutation_rate
+ parameters[genetic_parameters.GENERATIONS] = generations
+ parameters[genetic_parameters.ELITISM_MIN_SAMPLE] = elitism_min_sample
+ parameters[genetic_parameters.TOURNAMENT_TIMEOUT] = tournament_timeout
+ parameters[genetic_parameters.LOG_CSV] = log_csv
+
+ if check_is_pandas_dataframe(log):
+ check_pandas_dataframe_columns(
+ log,
+ activity_key=activity_key,
+ timestamp_key=timestamp_key,
+ case_id_key=case_id_key,
+ )
+
+ return genetic_miner.apply(
+ log,
+ variant=genetic_miner.Variants.CLASSIC,
+ parameters=parameters,
+ )
+
+
@deprecation.deprecated(
deprecated_in="2.3.0",
removed_in="3.0.0",
diff --git a/pm4py/objects/__init__.py b/pm4py/objects/__init__.py
index 4ac00c3df4..f1333c1382 100644
--- a/pm4py/objects/__init__.py
+++ b/pm4py/objects/__init__.py
@@ -32,6 +32,7 @@
trie,
org,
heuristics_net,
+ genetic_matrix,
ocel,
powl,
random_variables,
diff --git a/pm4py/objects/conversion/__init__.py b/pm4py/objects/conversion/__init__.py
index 87b65fda32..41d67f6619 100644
--- a/pm4py/objects/conversion/__init__.py
+++ b/pm4py/objects/conversion/__init__.py
@@ -25,6 +25,7 @@
from pm4py.objects.conversion import (
bpmn,
heuristics_net,
+ genetic_matrix,
process_tree,
wf_net,
log,
diff --git a/pm4py/objects/conversion/genetic_matrix/__init__.py b/pm4py/objects/conversion/genetic_matrix/__init__.py
new file mode 100644
index 0000000000..795a7aecca
--- /dev/null
+++ b/pm4py/objects/conversion/genetic_matrix/__init__.py
@@ -0,0 +1,28 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+
+# Author: Maximilian Josef Frank (https://orcid.org/0000-0002-0714-7748)
+
+from pm4py.util import constants as pm4_constants
+
+if pm4_constants.ENABLE_INTERNAL_IMPORTS:
+ from pm4py.objects.conversion.genetic_matrix import converter, variants
diff --git a/pm4py/objects/conversion/genetic_matrix/converter.py b/pm4py/objects/conversion/genetic_matrix/converter.py
new file mode 100644
index 0000000000..cec4c564e7
--- /dev/null
+++ b/pm4py/objects/conversion/genetic_matrix/converter.py
@@ -0,0 +1,50 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+
+# Author: Maximilian Josef Frank (https://orcid.org/0000-0002-0714-7748)
+
+from pm4py.objects.conversion.genetic_matrix.variants import to_petri_net
+from enum import Enum
+from pm4py.util import exec_utils
+
+
+class Variants(Enum):
+ TO_PETRI_NET = to_petri_net
+
+
+def apply(genetic_matrix, parameters=None, variant=Variants.TO_PETRI_NET):
+ """
+ Converts a GeneticMatrix to a different type of object
+
+ Parameters
+ --------------
+ genetic_matrix
+ Genetic matrix
+ parameters
+ Possible parameters of the algorithm
+ variant
+ Variant of the algorithm:
+ - Variants.TO_PETRI_NET
+ """
+ return exec_utils.get_variant(variant).apply(
+ genetic_matrix, parameters=parameters
+ )
diff --git a/pm4py/objects/conversion/genetic_matrix/variants/__init__.py b/pm4py/objects/conversion/genetic_matrix/variants/__init__.py
new file mode 100644
index 0000000000..e57d3e06c5
--- /dev/null
+++ b/pm4py/objects/conversion/genetic_matrix/variants/__init__.py
@@ -0,0 +1,25 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+
+# Author: Maximilian Josef Frank (https://orcid.org/0000-0002-0714-7748)
+
+from pm4py.objects.conversion.genetic_matrix.variants import to_petri_net
diff --git a/pm4py/objects/conversion/genetic_matrix/variants/to_petri_net.py b/pm4py/objects/conversion/genetic_matrix/variants/to_petri_net.py
new file mode 100644
index 0000000000..c5736d4896
--- /dev/null
+++ b/pm4py/objects/conversion/genetic_matrix/variants/to_petri_net.py
@@ -0,0 +1,149 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+
+# Author: Maximilian Josef Frank (https://orcid.org/0000-0002-0714-7748)
+
+from collections import defaultdict
+import copy
+import itertools
+
+from pm4py.objects.petri_net.obj import PetriNet, Marking
+from pm4py.objects.petri_net.utils import petri_utils
+
+
+def apply(genetic_matrix, parameters=None):
+ """
+ Converts a Genetic matrix to a Petri net
+
+ Reference paper:
+ Maximilian Josef Frank. "Optimising and Implementing the Genetic Miner in PM4Py" (2026).
+
+ Parameters
+ --------------
+ genetic_matrix
+ Genetic matrix
+ parameters
+ (Unused) Possible parameters of the algorithm
+
+ Returns
+ --------------
+ net
+ Petri net
+ im
+ Initial marking
+ fm
+ Final marking
+ """
+ # import here to resolve circular import dependencies
+ from pm4py.algo.discovery.genetic.util import get_src_sink_sets_for_wfnet, iset
+ I = genetic_matrix.input_map
+ O = genetic_matrix.output_map
+ T = genetic_matrix.transitions
+ # @src 5.3. Sophisticated Mapping, Def. 7; https://doi.org/10.1007/11494744_5
+ # original, but insanely inefficient (for |A|=23, |P(A)|=8388608):
+ # X = {(Ti, To) ∈ P(A) × P(A) | ∀t∈Ti To ∈ O(t) ∧ ∀t∈To Ti ∈ I(t)}
+ # equivalent and more efficient (for |A|=23, |I(∀t)×O(∀t)|=24×16=384):
+ # X = {(Ti, To) ∈ I(∀t) × O(∀t) | ∀t∈Ti To ∈ O(t) ∧ ∀t∈To Ti ∈ I(t)}
+ X = list(filter( # if no list(), X gets consumed as stream
+ lambda Tio: # = (Ti, To)
+ all(Tio[1] in O[t] for t in Tio[0]) and
+ all(Tio[0] in I[t] for t in Tio[1]),
+ # holds s. Def 4, constraint 3+4; https://doi.org/10.1007/11494744_5
+ itertools.product(
+ iset.flat(I.values()),
+ iset.flat(O.values())
+ )
+ ))
+ Xs = list(itertools.chain( # Xs = non-sophisticated / silent-transition places
+ [ (Si,iset([t])) for Ti,t in zip(I.values(), I.keys()) for Si in Ti],
+ [ (iset([t]),So) for t,To in zip(O.keys(), O.values()) for So in To]
+ ))
+ # remove places in Xs which are fully covered by X
+ Xs = [ Tio for Tio in Xs if not any(
+ Tio[0] <= Ti and Tio[1] <= To for Ti,To in X
+ )]
+ # remove Xs (silent transitions from non-simple matrix) from X (s. later)
+ def is_transitively_not_sophisticated(Tio, places):
+ """If place `Tio` is not in sophisticated mapping (i.e. place in `Xs`), all adjacent places (i.e. with same input/output transition) are replaced by non-sophisticated places and their silent transitions."""
+ return any(
+ len(Ti & Tio[0]) or len(To & Tio[1]) # & = intersect
+ for Ti,To in places
+ )
+ check = copy.copy(X)
+ while check:
+ Tio = check.pop(0)
+ if is_transitively_not_sophisticated(Tio, Xs):
+ Xs.append(Tio)
+ X.remove(Tio)
+ for Tio2 in X:
+ if Tio2 not in check and is_transitively_not_sophisticated(Tio2, Xs):
+ check.append(Tio2)
+ # build PetriNet
+ io = [PetriNet.Place('i'), PetriNet.Place('o')]
+ net = PetriNet(
+ places = [PetriNet.Place(p) for p in map(str, X)] + io,
+ transitions = [PetriNet.Transition(t) for t in T]
+ )
+ # original: F = {(i,t) | t∈T ∧ •t=∅} ∪ {(t,o) | t∈T ∧ t•=∅} ∪ …
+ T_noI, T_noO = get_src_sink_sets_for_wfnet(I, O, T)
+ arcs_pt = [ ('i', t) for t in T_noI ] + [
+ ((Ti,To), t) for (Ti,To),t in itertools.product(X, T) if t in To
+ ]
+ arcs_tp = [ (t, 'o') for t in T_noO ] + [
+ (t, (Ti,To)) for t,(Ti,To) in itertools.product(T, X) if t in Ti
+ ]
+ # convert to instances
+ getPlace = lambda name: next(filter(lambda p: p.name == name, net.places), None)
+ getTransition = lambda name: next(filter(lambda t: t.name == name, net.transitions), None)
+ arcs_pt = [(getPlace(str(p)), getTransition(t)) for p,t in arcs_pt]
+ arcs_tp = [(getTransition(t), getPlace(str(p))) for t,p in arcs_tp]
+ # add instance arcs
+ for i,o in itertools.chain(arcs_pt, arcs_tp):
+ petri_utils.add_arc_from_to(i, o, net)
+ # if causal matrix not simple (Def. 8; https://doi.org/10.1007/11494744_5):
+ # @src 5.2. Naive Mapping, Def. 6; https://doi.org/10.1007/11494744_5
+ # add incoming/outgoing silent places
+ Ti = set(itertools.chain(*(Ti for Ti,_ in Xs)))
+ To = set(itertools.chain(*(To for _,To in Xs)))
+ Ps_o = { name : PetriNet.Place("o"+str(name)) # output of prev non-silent T
+ for name in [ (t,s) for t in Ti for s in O[t] ]
+ }
+ Ps_i = { name : PetriNet.Place("i"+str(name)) # input of next non-silent T
+ for name in [ (t,s) for t in To for s in I[t] ]
+ }
+ net.places.extend(itertools.chain(Ps_o.values(), Ps_i.values()))
+ Ts = defaultdict(lambda: PetriNet.Transition(name="")) # name="" → silent
+ # add arcs
+ for (t1,So),p in Ps_o.items():
+ petri_utils.add_arc_from_to(getTransition(t1), p, net)
+ for t2 in So: # connect silent T
+ petri_utils.add_arc_from_to(p, Ts[t1,t2], net)
+ for (t2,Si),p in Ps_i.items():
+ petri_utils.add_arc_from_to(p, getTransition(t2), net)
+ for t1 in Si: # connect silent T
+ petri_utils.add_arc_from_to(Ts[t1,t2], p, net)
+ net.transitions.extend(Ts.values())
+ # add markings
+ markings = [Marking() for _ in io]
+ for m,p in zip(markings, io):
+ m[p] = 1
+ return (net, *markings)
diff --git a/pm4py/objects/genetic_matrix/__init__.py b/pm4py/objects/genetic_matrix/__init__.py
new file mode 100644
index 0000000000..eccb1010f0
--- /dev/null
+++ b/pm4py/objects/genetic_matrix/__init__.py
@@ -0,0 +1,25 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.util import constants as pm4_constants
+
+if pm4_constants.ENABLE_INTERNAL_IMPORTS:
+ from pm4py.objects.genetic_matrix import obj
diff --git a/pm4py/objects/genetic_matrix/obj.py b/pm4py/objects/genetic_matrix/obj.py
new file mode 100644
index 0000000000..689894e2a8
--- /dev/null
+++ b/pm4py/objects/genetic_matrix/obj.py
@@ -0,0 +1,63 @@
+'''
+ PM4Py – A Process Mining Library for Python
+Copyright (C) 2026 Process Intelligence Solutions UG (haftungsbeschränkt)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see this software project's root or
+visit .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+
+# Author: Maximilian Josef Frank (https://orcid.org/0000-0002-0714-7748)
+
+class GeneticMatrix:
+ def __init__(
+ self,
+ input_map: dict[str,list[frozenset]],
+ output_map: dict[str,list[frozenset]],
+ transitions: list[str],
+ ):
+ """
+ Initialize a Genetic matrix
+
+ Reference paper:
+ Maximilian Josef Frank. "Optimising and Implementing the Genetic Miner in PM4Py" (2026).
+
+ Parameters
+ -------------
+ input_map
+ Input map for each node
+ output_map
+ Output map for each node
+ transitions
+ List of transitions
+ """
+ self.input_map = input_map
+ self.output_map = output_map
+ self.transitions = transitions
+
+ def __repr__(self):
+ return str({
+ I: self.input_map,
+ O: self.output_map,
+ T: self.transitions
+ })
+
+ def __str__(self):
+ return str({
+ I: self.input_map,
+ O: self.output_map,
+ T: self.transitions
+ })
diff --git a/requirements.txt b/requirements.txt
index 1f3a0dc052..d0974aa834 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,4 +9,5 @@ pytz
tqdm
wheel
setuptools
+func_timeout
cvxopt;python_version<'3.14'
diff --git a/requirements_complete.txt b/requirements_complete.txt
index a96a6d6b71..2c43e174de 100644
--- a/requirements_complete.txt
+++ b/requirements_complete.txt
@@ -2,6 +2,7 @@ colorama
contourpy
cycler
fonttools
+func_timeout
graphviz
kiwisolver
lxml
diff --git a/requirements_stable.txt b/requirements_stable.txt
index b6e9b4e5a8..be5309d567 100644
--- a/requirements_stable.txt
+++ b/requirements_stable.txt
@@ -2,6 +2,7 @@ colorama==0.4.6
contourpy==1.3.3
cycler==0.12.1
fonttools==4.61.1
+func_timeout==4.3.5
graphviz==0.21
kiwisolver==1.4.9
lxml==6.0.2
diff --git a/tests/geneticminer_test.py b/tests/geneticminer_test.py
new file mode 100755
index 0000000000..f3a1e0d3a3
--- /dev/null
+++ b/tests/geneticminer_test.py
@@ -0,0 +1,331 @@
+#!/usr/bin/env python3 -m unittest tests.geneticminer_test
+# Author: Maximilian Josef Frank (https://orcid.org/0000-0002-0714-7748)
+
+import unittest
+from pm4py import save_vis_petri_net
+from pm4py.algo.discovery.genetic import algorithm as geneticminer
+from pm4py.algo.discovery.genetic.util import iset
+from pm4py.objects.conversion.genetic_matrix.variants.to_petri_net import apply as matrix2petrinet
+from pm4py.objects.genetic_matrix.obj import GeneticMatrix
+from pm4py.objects.petri_net.obj import PetriNet
+from pm4py.objects.petri_net import utils
+
+
+class TestGeneticMiner(unittest.TestCase):
+ def test_matrix2petrinet_or(self):
+ I = {
+ "A": [],
+ "B": [{"A"}],
+ "C": [{"A"}],
+ "D": [{"B","C"}]
+ }
+ O = {
+ "A": [{"B","C"}],
+ "B": [{"D"}],
+ "C": [{"D"}],
+ "D": []
+ }
+ for t in I:
+ I[t] = [iset(s) for s in I[t]]
+ O[t] = [iset(s) for s in O[t]]
+ res,_,_ = self._subject(I, O, T=I.keys())
+# self._visualise(res)
+ cmp = """places: [ ({'A'}, {'B', 'C'}), ({'B', 'C'}, {'D'}), i, o ]
+transitions: [ (A, None), (B, None), (C, None), (D, None) ]
+arcs: [ (A, None)->({'A'}, {'B', 'C'}), (B, None)->({'B', 'C'}, {'D'}), (C, None)->({'B', 'C'}, {'D'}), (D, None)->o, ({'A'}, {'B', 'C'})->(B, None), ({'A'}, {'B', 'C'})->(C, None), ({'B', 'C'}, {'D'})->(D, None), i->(A, None) ]"""
+ self.assertEqual(str(res), cmp, "Mismatching matrix and petri net")
+
+ def test_matrix2petrinet_or2(self):
+ I = {
+ "A": [],
+ "B": [],
+ "C": [{"A","B"}],
+ "D": [{"A","B"}]
+ }
+ O = {
+ "A": [{"C","D"}],
+ "B": [{"C","D"}],
+ "C": [],
+ "D": []
+ }
+ for t in I:
+ I[t] = [iset(s) for s in I[t]]
+ O[t] = [iset(s) for s in O[t]]
+ res,_,_ = self._subject(I, O, T=I.keys())
+# self._visualise(res)
+ cmp = """places: [ ({'A', 'B'}, {'C', 'D'}), i, o ]
+transitions: [ (A, None), (B, None), (C, None), (D, None) ]
+arcs: [ (A, None)->({'A', 'B'}, {'C', 'D'}), (B, None)->({'A', 'B'}, {'C', 'D'}), (C, None)->o, (D, None)->o, ({'A', 'B'}, {'C', 'D'})->(C, None), ({'A', 'B'}, {'C', 'D'})->(D, None), i->(A, None), i->(B, None) ]"""
+ self.assertEqual(str(res), cmp, "Mismatching matrix and petri net")
+
+ def test_matrix2petrinet_andOrCross(self):
+ I = {
+ "A": [],
+ "B": [],
+ "C": [{"A"},{"B"}],
+ "D": [{"A","B"}]
+ }
+ O = {
+ "A": [{"C","D"}],
+ "B": [{"C"},{"D"}],
+ "C": [],
+ "D": []
+ }
+ for t in I:
+ I[t] = [iset(s) for s in I[t]]
+ O[t] = [iset(s) for s in O[t]]
+ res,_,_ = self._subject(I, O, T=I.keys())
+# self._visualise(res)
+ cmp = """places: [ i, i('C', {'A'}), i('C', {'B'}), i('D', {'A', 'B'}), o, o('A', {'C', 'D'}), o('B', {'C'}), o('B', {'D'}) ]
+transitions: [ (, None), (, None), (, None), (, None), (A, None), (B, None), (C, None), (D, None) ]
+arcs: [ (, None)->i('C', {'A'}), (, None)->i('C', {'B'}), (, None)->i('D', {'A', 'B'}), (, None)->i('D', {'A', 'B'}), (A, None)->o('A', {'C', 'D'}), (B, None)->o('B', {'C'}), (B, None)->o('B', {'D'}), (C, None)->o, (D, None)->o, i('C', {'A'})->(C, None), i('C', {'B'})->(C, None), i('D', {'A', 'B'})->(D, None), i->(A, None), i->(B, None), o('A', {'C', 'D'})->(, None), o('A', {'C', 'D'})->(, None), o('B', {'C'})->(, None), o('B', {'D'})->(, None) ]"""
+ self.assertEqual(str(res), cmp, "Mismatching matrix and petri net")
+
+ def test_matrix2petrinet_orCross(self):
+ # Fig. 2 in https://doi.org/10.1007/11494744_5
+ I = {
+ "A": [],
+ "B": [],
+ "C": [{"A"}],
+ "D": [{"A","B"}]
+ }
+ O = {
+ "A": [{"C","D"}],
+ "B": [{"D"}],
+ "C": [],
+ "D": []
+ }
+ for t in I:
+ I[t] = [iset(s) for s in I[t]]
+ O[t] = [iset(s) for s in O[t]]
+ res,_,_ = self._subject(I, O, T=I.keys())
+# self._visualise(res)
+ cmp = """places: [ i, i('C', {'A'}), i('D', {'A', 'B'}), o, o('A', {'C', 'D'}), o('B', {'D'}) ]
+transitions: [ (, None), (, None), (, None), (A, None), (B, None), (C, None), (D, None) ]
+arcs: [ (, None)->i('C', {'A'}), (, None)->i('D', {'A', 'B'}), (, None)->i('D', {'A', 'B'}), (A, None)->o('A', {'C', 'D'}), (B, None)->o('B', {'D'}), (C, None)->o, (D, None)->o, i('C', {'A'})->(C, None), i('D', {'A', 'B'})->(D, None), i->(A, None), i->(B, None), o('A', {'C', 'D'})->(, None), o('A', {'C', 'D'})->(, None), o('B', {'D'})->(, None) ]"""
+ self.assertEqual(str(res), cmp, "Mismatching matrix and petri net")
+
+ def test_matrix2petrinet_and(self):
+ I = {
+ "A": [],
+ "B": [{"A"}],
+ "C": [{"A"}],
+ "D": [{"B"},{"C"}]
+ }
+ O = {
+ "A": [{"B"},{"C"}],
+ "B": [{"D"}],
+ "C": [{"D"}],
+ "D": []
+ }
+ for t in I:
+ I[t] = [iset(s) for s in I[t]]
+ O[t] = [iset(s) for s in O[t]]
+ res,_,_ = self._subject(I, O, T=I.keys())
+# self._visualise(res)
+ cmp = """places: [ ({'A'}, {'B'}), ({'A'}, {'C'}), ({'B'}, {'D'}), ({'C'}, {'D'}), i, o ]
+transitions: [ (A, None), (B, None), (C, None), (D, None) ]
+arcs: [ (A, None)->({'A'}, {'B'}), (A, None)->({'A'}, {'C'}), (B, None)->({'B'}, {'D'}), (C, None)->({'C'}, {'D'}), (D, None)->o, ({'A'}, {'B'})->(B, None), ({'A'}, {'C'})->(C, None), ({'B'}, {'D'})->(D, None), ({'C'}, {'D'})->(D, None), i->(A, None) ]"""
+ self.assertEqual(str(res), cmp, "Mismatching matrix and petri net")
+
+ def test_matrix2petrinet_and2(self):
+ I = {
+ "A": [],
+ "B": [],
+ "C": [{"A"},{"B"}],
+ "D": [{"A"},{"B"}]
+ }
+ O = {
+ "A": [{"C"},{"D"}],
+ "B": [{"C"},{"D"}],
+ "C": [],
+ "D": []
+ }
+ for t in I:
+ I[t] = [iset(s) for s in I[t]]
+ O[t] = [iset(s) for s in O[t]]
+ res,_,_ = self._subject(I, O, T=I.keys())
+# self._visualise(res)
+ cmp = """places: [ ({'A'}, {'C'}), ({'A'}, {'D'}), ({'B'}, {'C'}), ({'B'}, {'D'}), i, o ]
+transitions: [ (A, None), (B, None), (C, None), (D, None) ]
+arcs: [ (A, None)->({'A'}, {'C'}), (A, None)->({'A'}, {'D'}), (B, None)->({'B'}, {'C'}), (B, None)->({'B'}, {'D'}), (C, None)->o, (D, None)->o, ({'A'}, {'C'})->(C, None), ({'A'}, {'D'})->(D, None), ({'B'}, {'C'})->(C, None), ({'B'}, {'D'})->(D, None), i->(A, None), i->(B, None) ]"""
+ self.assertEqual(str(res), cmp, "Mismatching matrix and petri net")
+
+ def test_matrix2petrinet_full(self):
+ # Fig. 3 in https://doi.org/10.1007/11494744_5
+ I = {
+ "A": [],
+ "B": [{"A"}],
+ "C": [{"A"}],
+ "D": [{"A"}],
+ "E": [{"B"}, {"C"}],
+ "F": [{"B"}, {"D"}],
+ "G": [{"E"}, {"F"}]
+ }
+ O = {
+ "A": [{"B"}, {"C", "D"}],
+ "B": [{"E", "F"}],
+ "C": [{"E"}],
+ "D": [{"F"}],
+ "E": [{"G"}],
+ "F": [{"G"}],
+ "G": []
+ }
+ transitions = list(I.keys())
+ res,_,_ = self._subject(I, O, T=transitions)
+# self._visualise(res, file="full.png")
+ cmp = """places: [ ({'A'}, {'B'}), ({'A'}, {'C', 'D'}), ({'B'}, {'E', 'F'}), ({'C'}, {'E'}), ({'D'}, {'F'}), ({'E'}, {'G'}), ({'F'}, {'G'}), i, o ]
+transitions: [ (A, None), (B, None), (C, None), (D, None), (E, None), (F, None), (G, None) ]
+arcs: [ (A, None)->({'A'}, {'B'}), (A, None)->({'A'}, {'C', 'D'}), (B, None)->({'B'}, {'E', 'F'}), (C, None)->({'C'}, {'E'}), (D, None)->({'D'}, {'F'}), (E, None)->({'E'}, {'G'}), (F, None)->({'F'}, {'G'}), (G, None)->o, ({'A'}, {'B'})->(B, None), ({'A'}, {'C', 'D'})->(C, None), ({'A'}, {'C', 'D'})->(D, None), ({'B'}, {'E', 'F'})->(E, None), ({'B'}, {'E', 'F'})->(F, None), ({'C'}, {'E'})->(E, None), ({'D'}, {'F'})->(F, None), ({'E'}, {'G'})->(G, None), ({'F'}, {'G'})->(G, None), i->(A, None) ]"""
+ self.assertEqual(str(res), cmp, "Mismatching matrix and petri net")
+
+ def test_matrix2petrinet_mixed(self):
+ I = {
+ 'A': [{'B', 'Y'}],
+ 'B': [{'C'}],
+ 'C': [{'Y'}],
+ 'D': [{'Y'}],
+ 'E': [{'F'}],
+ 'F': [{'G'}],
+ 'G': [{'H'}],
+ 'H': [{'Y'}],
+ 'I': [{'I'}],
+ 'J': [{'I'}],
+ 'K': [{'Z'}, {'S'}],
+ 'L': [{'L'}],
+ 'M': [{'L'}],
+ 'N': [{'O'}],
+ 'O': [{'L', 'X'}],
+ 'P': [{'Q'}],
+ 'Q': [{'X'}],
+ 'R': [{'R', 'I'}],
+ 'S': [{'R'}],
+ 'T': [{'L', 'Y'}],
+ 'U': [{'U'}, {'Y'}],
+ 'V': [{'V', 'X'}],
+ 'W': [{'L', 'W'}],
+ 'X': [],
+ 'Y': [],
+ 'Z': [{'X'}]
+ }
+ O = {
+ 'B': [{'A'}],
+ 'Y': [{'A'}, {'D', 'T', 'U', 'C'}, {'H'}],
+ 'C': [{'B'}],
+ 'F': [{'E'}],
+ 'G': [{'F'}],
+ 'H': [{'G'}],
+ 'I': [{'J', 'R'}, {'I'}],
+ 'Z': [{'K'}],
+ 'L': [{'M', 'O', 'T', 'L'}, {'W'}],
+ 'O': [{'N'}],
+ 'X': [{'O', 'Q', 'Z', 'V'}],
+ 'Q': [{'P'}],
+ 'R': [{'S'}, {'R'}],
+ 'U': [{'U'}],
+ 'V': [{'V'}],
+ 'W': [{'W'}],
+ 'P': [],
+ 'N': [],
+ 'T': [],
+ 'M': [],
+ 'D': [],
+ 'A': [],
+ 'E': [],
+ 'K': [],
+ 'S': [{'K'}],
+ 'J': []
+ }
+ transitions = list(set(I.keys()) & set(O.keys()))
+ res,_,_ = self._subject(I, O, T=transitions)
+# self._visualise(res)
+ cmp = """places: [ ({'C'}, {'B'}), ({'F'}, {'E'}), ({'G'}, {'F'}), ({'H'}, {'G'}), ({'O'}, {'N'}), ({'Q'}, {'P'}), ({'S'}, {'K'}), ({'Z'}, {'K'}), i, i('A', {'B', 'Y'}), i('C', {'Y'}), i('D', {'Y'}), i('H', {'Y'}), i('I', {'I'}), i('J', {'I'}), i('L', {'L'}), i('M', {'L'}), i('O', {'L', 'X'}), i('Q', {'X'}), i('R', {'I', 'R'}), i('S', {'R'}), i('T', {'L', 'Y'}), i('U', {'U'}), i('U', {'Y'}), i('V', {'V', 'X'}), i('W', {'L', 'W'}), i('Z', {'X'}), o, o('B', {'A'}), o('I', {'I'}), o('I', {'J', 'R'}), o('L', {'L', 'M', 'O', 'T'}), o('L', {'W'}), o('R', {'R'}), o('R', {'S'}), o('U', {'U'}), o('V', {'V'}), o('W', {'W'}), o('X', {'O', 'Q', 'V', 'Z'}), o('Y', {'A'}), o('Y', {'C', 'D', 'T', 'U'}), o('Y', {'H'}) ]
+transitions: [ (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (A, None), (B, None), (C, None), (D, None), (E, None), (F, None), (G, None), (H, None), (I, None), (J, None), (K, None), (L, None), (M, None), (N, None), (O, None), (P, None), (Q, None), (R, None), (S, None), (T, None), (U, None), (V, None), (W, None), (X, None), (Y, None), (Z, None) ]
+arcs: [ (, None)->i('A', {'B', 'Y'}), (, None)->i('A', {'B', 'Y'}), (, None)->i('C', {'Y'}), (, None)->i('D', {'Y'}), (, None)->i('H', {'Y'}), (, None)->i('I', {'I'}), (, None)->i('J', {'I'}), (, None)->i('L', {'L'}), (, None)->i('M', {'L'}), (, None)->i('O', {'L', 'X'}), (, None)->i('O', {'L', 'X'}), (, None)->i('Q', {'X'}), (, None)->i('R', {'I', 'R'}), (, None)->i('R', {'I', 'R'}), (, None)->i('S', {'R'}), (, None)->i('T', {'L', 'Y'}), (, None)->i('T', {'L', 'Y'}), (, None)->i('U', {'U'}), (, None)->i('U', {'Y'}), (, None)->i('V', {'V', 'X'}), (, None)->i('V', {'V', 'X'}), (, None)->i('W', {'L', 'W'}), (, None)->i('W', {'L', 'W'}), (, None)->i('Z', {'X'}), (A, None)->o, (B, None)->o('B', {'A'}), (C, None)->({'C'}, {'B'}), (D, None)->o, (E, None)->o, (F, None)->({'F'}, {'E'}), (G, None)->({'G'}, {'F'}), (H, None)->({'H'}, {'G'}), (I, None)->o('I', {'I'}), (I, None)->o('I', {'J', 'R'}), (J, None)->o, (K, None)->o, (L, None)->o('L', {'L', 'M', 'O', 'T'}), (L, None)->o('L', {'W'}), (M, None)->o, (N, None)->o, (O, None)->({'O'}, {'N'}), (P, None)->o, (Q, None)->({'Q'}, {'P'}), (R, None)->o('R', {'R'}), (R, None)->o('R', {'S'}), (S, None)->({'S'}, {'K'}), (T, None)->o, (U, None)->o, (U, None)->o('U', {'U'}), (V, None)->o, (V, None)->o('V', {'V'}), (W, None)->o, (W, None)->o('W', {'W'}), (X, None)->o('X', {'O', 'Q', 'V', 'Z'}), (Y, None)->o('Y', {'A'}), (Y, None)->o('Y', {'C', 'D', 'T', 'U'}), (Y, None)->o('Y', {'H'}), (Z, None)->({'Z'}, {'K'}), ({'C'}, {'B'})->(B, None), ({'F'}, {'E'})->(E, None), ({'G'}, {'F'})->(F, None), ({'H'}, {'G'})->(G, None), ({'O'}, {'N'})->(N, None), ({'Q'}, {'P'})->(P, None), ({'S'}, {'K'})->(K, None), ({'Z'}, {'K'})->(K, None), i('A', {'B', 'Y'})->(A, None), i('C', {'Y'})->(C, None), i('D', {'Y'})->(D, None), i('H', {'Y'})->(H, None), i('I', {'I'})->(I, None), i('J', {'I'})->(J, None), i('L', {'L'})->(L, None), i('M', {'L'})->(M, None), i('O', {'L', 'X'})->(O, None), i('Q', {'X'})->(Q, None), i('R', {'I', 'R'})->(R, None), i('S', {'R'})->(S, None), i('T', {'L', 'Y'})->(T, None), i('U', {'U'})->(U, None), i('U', {'Y'})->(U, None), i('V', {'V', 'X'})->(V, None), i('W', {'L', 'W'})->(W, None), i('Z', {'X'})->(Z, None), i->(I, None), i->(L, None), i->(X, None), i->(Y, None), o('B', {'A'})->(, None), o('I', {'I'})->(, None), o('I', {'J', 'R'})->(, None), o('I', {'J', 'R'})->(, None), o('L', {'L', 'M', 'O', 'T'})->(, None), o('L', {'L', 'M', 'O', 'T'})->(, None), o('L', {'L', 'M', 'O', 'T'})->(, None), o('L', {'L', 'M', 'O', 'T'})->(, None), o('L', {'W'})->(, None), o('R', {'R'})->(, None), o('R', {'S'})->(, None), o('U', {'U'})->(, None), o('V', {'V'})->(, None), o('W', {'W'})->(, None), o('X', {'O', 'Q', 'V', 'Z'})->(, None), o('X', {'O', 'Q', 'V', 'Z'})->(, None), o('X', {'O', 'Q', 'V', 'Z'})->(, None), o('X', {'O', 'Q', 'V', 'Z'})->(, None), o('Y', {'A'})->(, None), o('Y', {'C', 'D', 'T', 'U'})->(, None), o('Y', {'C', 'D', 'T', 'U'})->(, None), o('Y', {'C', 'D', 'T', 'U'})->(, None), o('Y', {'C', 'D', 'T', 'U'})->(, None), o('Y', {'H'})->(, None) ]"""
+ self.assertEqual(str(res), cmp, "Mismatching matrix and petri net")
+
+ def test_matrix2petrinet_mixed2(self):
+ I = {
+ 'A': [{'B'}, {'C'}],
+ 'B': [{'Y'}],
+ 'C': [{'C'}],
+ 'D': [{'E'}, {'C'}],
+ 'E': [{'F'}],
+ 'F': [{'G'}],
+ 'G': [{'N'}, {'H'}],
+ 'H': [{'C'}],
+ 'I': [{'W', 'I', 'K'}],
+ 'J': [{'I'}],
+ 'K': [{'X'}, {'K'}],
+ 'L': [{'M', 'K'}],
+ 'M': [{'K'}],
+ 'N': [{'P', 'N'}],
+ 'O': [{'N'}],
+ 'P': [{'Q'}],
+ 'Q': [{'K'}],
+ 'R': [{'S'}],
+ 'S': [{'K'}],
+ 'T': [{'T'}, {'I'}],
+ 'U': [{'T'}],
+ 'V': [{'C'}],
+ 'W': [{'W'}],
+ 'X': [{'X'}],
+ 'Y': [],
+ 'Z': [{'E'}]
+ }
+ O = {
+ 'B': [{'A'}],
+ 'C': [{'D', 'A', 'H', 'V'}, {'C'}],
+ 'Y': [{'B'}],
+ 'E': [{'D', 'Z'}],
+ 'F': [{'E'}],
+ 'G': [{'F'}],
+ 'H': [{'G'}],
+ 'I': [{'J', 'I'}, {'T'}],
+ 'K': [{'L'}, {'Q', 'S', 'M', 'I', 'K'}],
+ 'M': [{'L'}],
+ 'N': [{'O', 'N'}, {'G'}],
+ 'Q': [{'P'}],
+ 'S': [{'R'}],
+ 'T': [{'T'}, {'U'}],
+ 'W': [{'I'}, {'W'}],
+ 'X': [{'X', 'K'}],
+ 'A': [],
+ 'D': [],
+ 'V': [],
+ 'L': [],
+ 'P': [{'N'}],
+ 'R': [],
+ 'U': [],
+ 'J': [],
+ 'O': [],
+ 'Z': []
+ }
+ transitions = list(set(I.keys()) & set(O.keys()))
+ res,_,_ = self._subject(I, O, T=transitions)
+# self._visualise(res)
+ cmp = """places: [ ({'B'}, {'A'}), ({'C'}, {'A', 'D', 'H', 'V'}), ({'C'}, {'C'}), ({'E'}, {'D', 'Z'}), ({'F'}, {'E'}), ({'G'}, {'F'}), ({'Q'}, {'P'}), ({'S'}, {'R'}), ({'Y'}, {'B'}), i, i('G', {'H'}), i('G', {'N'}), i('I', {'I', 'K', 'W'}), i('J', {'I'}), i('K', {'K'}), i('K', {'X'}), i('L', {'K', 'M'}), i('M', {'K'}), i('N', {'N', 'P'}), i('O', {'N'}), i('Q', {'K'}), i('S', {'K'}), i('T', {'I'}), i('T', {'T'}), i('U', {'T'}), i('W', {'W'}), i('X', {'X'}), o, o('H', {'G'}), o('I', {'I', 'J'}), o('I', {'T'}), o('K', {'I', 'K', 'M', 'Q', 'S'}), o('K', {'L'}), o('M', {'L'}), o('N', {'G'}), o('N', {'N', 'O'}), o('P', {'N'}), o('T', {'T'}), o('T', {'U'}), o('W', {'I'}), o('W', {'W'}), o('X', {'K', 'X'}) ]
+transitions: [ (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (, None), (A, None), (B, None), (C, None), (D, None), (E, None), (F, None), (G, None), (H, None), (I, None), (J, None), (K, None), (L, None), (M, None), (N, None), (O, None), (P, None), (Q, None), (R, None), (S, None), (T, None), (U, None), (V, None), (W, None), (X, None), (Y, None), (Z, None) ]
+arcs: [ (, None)->i('G', {'H'}), (, None)->i('G', {'N'}), (, None)->i('I', {'I', 'K', 'W'}), (, None)->i('I', {'I', 'K', 'W'}), (, None)->i('I', {'I', 'K', 'W'}), (, None)->i('J', {'I'}), (, None)->i('K', {'K'}), (, None)->i('K', {'X'}), (, None)->i('L', {'K', 'M'}), (, None)->i('L', {'K', 'M'}), (, None)->i('M', {'K'}), (, None)->i('N', {'N', 'P'}), (, None)->i('N', {'N', 'P'}), (, None)->i('O', {'N'}), (, None)->i('Q', {'K'}), (, None)->i('S', {'K'}), (, None)->i('T', {'I'}), (, None)->i('T', {'T'}), (, None)->i('U', {'T'}), (, None)->i('W', {'W'}), (, None)->i('X', {'X'}), (A, None)->o, (B, None)->({'B'}, {'A'}), (C, None)->({'C'}, {'A', 'D', 'H', 'V'}), (C, None)->({'C'}, {'C'}), (D, None)->o, (E, None)->({'E'}, {'D', 'Z'}), (F, None)->({'F'}, {'E'}), (G, None)->({'G'}, {'F'}), (H, None)->o('H', {'G'}), (I, None)->o('I', {'I', 'J'}), (I, None)->o('I', {'T'}), (J, None)->o, (K, None)->o('K', {'I', 'K', 'M', 'Q', 'S'}), (K, None)->o('K', {'L'}), (L, None)->o, (M, None)->o('M', {'L'}), (N, None)->o('N', {'G'}), (N, None)->o('N', {'N', 'O'}), (O, None)->o, (P, None)->o('P', {'N'}), (Q, None)->({'Q'}, {'P'}), (R, None)->o, (S, None)->({'S'}, {'R'}), (T, None)->o('T', {'T'}), (T, None)->o('T', {'U'}), (U, None)->o, (V, None)->o, (W, None)->o('W', {'I'}), (W, None)->o('W', {'W'}), (X, None)->o('X', {'K', 'X'}), (Y, None)->({'Y'}, {'B'}), (Z, None)->o, ({'B'}, {'A'})->(A, None), ({'C'}, {'A', 'D', 'H', 'V'})->(A, None), ({'C'}, {'A', 'D', 'H', 'V'})->(D, None), ({'C'}, {'A', 'D', 'H', 'V'})->(H, None), ({'C'}, {'A', 'D', 'H', 'V'})->(V, None), ({'C'}, {'C'})->(C, None), ({'E'}, {'D', 'Z'})->(D, None), ({'E'}, {'D', 'Z'})->(Z, None), ({'F'}, {'E'})->(E, None), ({'G'}, {'F'})->(F, None), ({'Q'}, {'P'})->(P, None), ({'S'}, {'R'})->(R, None), ({'Y'}, {'B'})->(B, None), i('G', {'H'})->(G, None), i('G', {'N'})->(G, None), i('I', {'I', 'K', 'W'})->(I, None), i('J', {'I'})->(J, None), i('K', {'K'})->(K, None), i('K', {'X'})->(K, None), i('L', {'K', 'M'})->(L, None), i('M', {'K'})->(M, None), i('N', {'N', 'P'})->(N, None), i('O', {'N'})->(O, None), i('Q', {'K'})->(Q, None), i('S', {'K'})->(S, None), i('T', {'I'})->(T, None), i('T', {'T'})->(T, None), i('U', {'T'})->(U, None), i('W', {'W'})->(W, None), i('X', {'X'})->(X, None), i->(C, None), i->(W, None), i->(X, None), i->(Y, None), o('H', {'G'})->(, None), o('I', {'I', 'J'})->(, None), o('I', {'I', 'J'})->(, None), o('I', {'T'})->(, None), o('K', {'I', 'K', 'M', 'Q', 'S'})->(, None), o('K', {'I', 'K', 'M', 'Q', 'S'})->(, None), o('K', {'I', 'K', 'M', 'Q', 'S'})->(, None), o('K', {'I', 'K', 'M', 'Q', 'S'})->(, None), o('K', {'I', 'K', 'M', 'Q', 'S'})->(, None), o('K', {'L'})->(, None), o('M', {'L'})->(, None), o('N', {'G'})->(, None), o('N', {'N', 'O'})->(, None), o('N', {'N', 'O'})->(, None), o('P', {'N'})->(, None), o('T', {'T'})->(, None), o('T', {'U'})->(, None), o('W', {'I'})->(, None), o('W', {'W'})->(, None), o('X', {'K', 'X'})->(, None), o('X', {'K', 'X'})->(, None) ]"""
+ self.assertEqual(str(res), cmp, "Mismatching matrix and petri net")
+
+ @staticmethod
+ def _subject(I, O, T):
+ # convert to indexable sets
+ for t in T:
+ I[t] = [iset(s) for s in I[t]]
+ O[t] = [iset(s) for s in O[t]]
+ return matrix2petrinet(GeneticMatrix(I, O, T))
+
+ @staticmethod
+ def _visualise(model, init = None, final = None, file = "test.png"):
+ save_vis_petri_net(
+ model,
+ init or utils.initial_marking.discover_initial_marking(model),
+ final or utils.final_marking.discover_final_marking(model),
+ file_path = file,
+ debug = True
+ )
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/polars_process_discovery_test.py b/tests/polars_process_discovery_test.py
index 7db03496ea..350e9f0b16 100644
--- a/tests/polars_process_discovery_test.py
+++ b/tests/polars_process_discovery_test.py
@@ -76,6 +76,13 @@ def test_discover_petri_net_heuristics(self):
self.assertTrue(len(im) > 0)
self.assertTrue(len(fm) > 0)
+ def test_discover_petri_net_genetic(self):
+ log = self._lazy_log()
+ net, im, fm = pm4py.discover_petri_net_genetic(log)
+ self.assertTrue(net.places and net.transitions)
+ self.assertTrue(len(im) > 0)
+ self.assertTrue(len(fm) > 0)
+
def test_discover_process_tree_inductive(self):
log = self._lazy_log()
process_tree = pm4py.discover_process_tree_inductive(log)
diff --git a/third_party/LICENSES_TRANSITIVE.md b/third_party/LICENSES_TRANSITIVE.md
index 08c4f8acb7..30db18917e 100644
--- a/third_party/LICENSES_TRANSITIVE.md
+++ b/third_party/LICENSES_TRANSITIVE.md
@@ -1,15 +1,16 @@
# PM4Py Third Party Dependencies
- PM4Py depends on third party libraries to implement some functionality. This document describes which libraries
- PM4Py depends upon. This is a best effort attempt to describe the library's dependencies, it is subject to change as
- libraries are added/removed.
-
- | Name | URL | License | Version |
- | --------------------------- | ------------------------------------------------------------ | --------------------------- | ------------------- |
- | colorama | https://pypi.org/pypi/colorama/json | BSD License | 0.4.6 |
+PM4Py depends on third party libraries to implement some functionality. This document describes which libraries
+PM4Py depends upon. This is a best effort attempt to describe the library's dependencies, it is subject to change as
+libraries are added/removed.
+
+| Name | URL | License | Version |
+| --------------------------- | ------------------------------------------------------------ | --------------------------- | ------------------- |
+| colorama | https://pypi.org/pypi/colorama/json | BSD License | 0.4.6 |
| contourpy | https://pypi.org/pypi/contourpy/json | BSD License | 1.3.3 |
| cycler | https://pypi.org/pypi/cycler/json | BSD License | 0.12.1 |
| fonttools | https://pypi.org/pypi/fonttools/json | Unspecified | 4.61.1 |
+| func_timeout | https://pypi.org/pypi/func-timeout/json | LGPLv2 License | 4.5.3 |
| graphviz | https://pypi.org/pypi/graphviz/json | Unspecified | 0.21 |
| kiwisolver | https://pypi.org/pypi/kiwisolver/json | BSD License | 1.4.9 |
| lxml | https://pypi.org/pypi/lxml/json | Unspecified | 6.0.2 |
diff --git a/third_party/func_timeout.LICENSE b/third_party/func_timeout.LICENSE
new file mode 100644
index 0000000000..65c5ca88a6
--- /dev/null
+++ b/third_party/func_timeout.LICENSE
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.