diff --git a/docs/source/api.rst b/docs/source/api.rst index 82f6c1949..8db60c76e 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 000000000..148d1110f --- /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 063923e88..cffa9c56b 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 957242ec0..458dabef4 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 000000000..211841253 --- /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 000000000..3afa746ef --- /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 000000000..9abff25c4 --- /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 000000000..f021ce92e --- /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 000000000..f62347ea5 --- /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 77a28a3a2..08a9f172f 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 aedf7252b..569f0ee03 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 3ab5f4b1e..618f2dc51 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 4ac00c3df..f1333c138 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 87b65fda3..41d67f661 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 000000000..795a7aecc --- /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 000000000..cec4c564e --- /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 000000000..e57d3e06c --- /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 000000000..c5736d489 --- /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 000000000..eccb1010f --- /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 000000000..689894e2a --- /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 1f3a0dc05..d0974aa83 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 a96a6d6b7..2c43e174d 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 b6e9b4e5a..be5309d56 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 000000000..f3a1e0d3a --- /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 7db03496e..350e9f0b1 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 08c4f8acb..30db18917 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 000000000..65c5ca88a --- /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.