diff --git a/pm4py/algo/discovery/powl/inductive/variants/__init__.py b/pm4py/algo/discovery/powl/inductive/variants/__init__.py new file mode 100644 index 000000000..54ee4709c --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/__init__.py @@ -0,0 +1,2 @@ +from pm4py.algo.discovery.powl.inductive.variants import * + diff --git a/pm4py/algo/discovery/powl/inductive/variants/brute_force/__init__.py b/pm4py/algo/discovery/powl/inductive/variants/brute_force/__init__.py new file mode 100644 index 000000000..d16e5c615 --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/brute_force/__init__.py @@ -0,0 +1 @@ +from pm4py.algo.discovery.powl.inductive.variants.brute_force import * \ No newline at end of file diff --git a/pm4py/algo/discovery/powl/inductive/variants/brute_force/bf_partial_order_cut.py b/pm4py/algo/discovery/powl/inductive/variants/brute_force/bf_partial_order_cut.py new file mode 100644 index 000000000..00c0d8d31 --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/brute_force/bf_partial_order_cut.py @@ -0,0 +1,202 @@ +from abc import ABC +from collections import Counter +from itertools import combinations +from typing import Any, Optional, Dict, List, Generic, Tuple, Collection + +from pm4py.algo.discovery.inductive.cuts.abc import Cut, T + +from pm4py.algo.discovery.inductive.dtypes.im_ds import IMDataStructureUVCL +from pm4py.objects.powl.BinaryRelation import BinaryRelation +from pm4py.objects.powl.obj import StrictPartialOrder, POWL +from pm4py.objects.dfg import util as dfu +from pm4py.statistics.eventually_follows.uvcl.get import apply as to_efg + +# MAX_NUM_PARTITIONS = 1000 + +def remove(blocks, g): + res = [] + for g2 in blocks: + if not g2.__str__().__eq__(g.__str__()): + res.append(g2) + return res + + +def contains(blocks, g): + for g2 in blocks: + if g2.__str__().__eq__(g.__str__()): + return True + return False + + +def xor(a, b): + if a and not b: + return True + elif not a and b: + return True + else: + return False + + +def get_partitions_of_size_k(nodes, k=None): + n = len(nodes) + if k is not None: + if k < 1: + raise ValueError( + "Can't partition in a negative or zero number of groups" + ) + elif k > n: + return + + def set_partitions_helper(L, k): + n = len(L) + if k == 1: + yield [tuple(L)] + elif n == k: + yield [tuple([s]) for s in L] + else: + e, *M = L + for p in set_partitions_helper(M, k - 1): + yield [tuple([e]), *p] + for p in set_partitions_helper(M, k): + for i in range(len(p)): + yield p[:i] + [tuple([e]) + p[i]] + p[i + 1:] + + if k is None: + for k in range(1, n + 1): + yield from set_partitions_helper(nodes, k) + else: + yield from set_partitions_helper(nodes, k) + + +def partition(collection): + # count = 1 + i = len(collection) + while i > 1: + for part in get_partitions_of_size_k(collection, i): + # if count > MAX_NUM_PARTITIONS: + # return + # count = count + 1 + yield part + i = i - 1 + return + + +def generate_order(parts, efg): + nodes = parts + po = BinaryRelation(nodes) + for group_1, group_2 in combinations(nodes, 2): + all_ef_g1_g2 = True + all_ef_g2_g1 = True + none_ef_g1_g2 = True + none_ef_g2_g1 = True + for a in group_1: + for b in group_2: + if (a, b) in efg: + none_ef_g1_g2 = False + else: + all_ef_g1_g2 = False + + if (b, a) in efg: + none_ef_g2_g1 = False + else: + all_ef_g2_g1 = False + + if all_ef_g1_g2 and none_ef_g2_g1: + po.add_edge(group_1, group_2) + elif none_ef_g1_g2 and all_ef_g2_g1: + po.add_edge(group_2, group_1) + + return po + + +def is_valid_order(po, dfg_graph, efg): + if not po.is_strict_partial_order(): + return False + + start_blocks = po.nodes + end_blocks = po.nodes + + for group_1, group_2 in combinations(po.nodes, 2): + + edge_g1_g2 = po.is_edge(group_1, group_2) + edge_g2_g1 = po.is_edge(group_2, group_1) + + if edge_g1_g2: + start_blocks = remove(start_blocks, group_2) + end_blocks = remove(end_blocks, group_1) + if edge_g2_g1: + start_blocks = remove(start_blocks, group_1) + end_blocks = remove(end_blocks, group_2) + + all_ef_g1_g2 = True + all_ef_g2_g1 = True + + for a in group_1: + for b in group_2: + if not (a, b) in efg: + all_ef_g1_g2 = False + if not (b, a) in efg: + all_ef_g2_g1 = False + if all_ef_g1_g2 and all_ef_g2_g1 and (edge_g1_g2 or edge_g2_g1): + return False + if not edge_g1_g2 and not edge_g2_g1 and not (all_ef_g1_g2 and all_ef_g2_g1): + return False + + n = len(po.nodes) + for i in range(n): + group = po.nodes[i] + c1 = contains(start_blocks, group) + c2 = len(set(group).intersection(set(dfg_graph.start_activities.keys()))) > 0 + c3 = contains(end_blocks, group) + c4 = len(set(group).intersection(set(dfg_graph.end_activities.keys()))) > 0 + if (c1 and not c2) or (c3 and not c4): + return False + + return True + + +class BruteForcePartialOrderCut(Cut[T], ABC, Generic[T]): + + @classmethod + def operator(cls, parameters: Optional[Dict[str, Any]] = None) -> StrictPartialOrder: + return StrictPartialOrder([]) + + @classmethod + def holds(cls, obj: T, parameters: Optional[Dict[str, Any]] = None) -> Optional[BinaryRelation]: + dfg_graph = obj.dfg + efg = to_efg(obj) + alphabet = sorted(dfu.get_vertices(dfg_graph), key=lambda g: g.__str__()) + for part in partition(alphabet): + # print(part) + po = generate_order(part, efg) + if is_valid_order(po, dfg_graph, efg): + return po + return None + + @classmethod + def apply(cls, obj: T, parameters: Optional[Dict[str, Any]] = None) -> Optional[Tuple[StrictPartialOrder, List[POWL]]]: + g = cls.holds(obj, parameters) + if g is None: + return g + children = cls.project(obj, g.nodes, parameters) + po = StrictPartialOrder(children) + for i, j in combinations(range(len(g.nodes)), 2): + if g.is_edge_id(i, j): + po.order.add_edge(children[i], children[j]) + elif g.is_edge_id(j, i): + po.order.add_edge(children[j], children[i]) + return po, po.children + + +class BruteForcePartialOrderCutUVCL(BruteForcePartialOrderCut[IMDataStructureUVCL]): + + @classmethod + def project(cls, obj: IMDataStructureUVCL, groups: List[Collection[Any]], + parameters: Optional[Dict[str, Any]] = None) -> List[IMDataStructureUVCL]: + r = list() + for g in groups: + c = Counter() + for t in obj.data_structure: + c[tuple(filter(lambda e: e in g, t))] = obj.data_structure[t] + r.append(c) + return list(map(lambda l: IMDataStructureUVCL(l), r)) diff --git a/pm4py/algo/discovery/powl/inductive/variants/brute_force/factory.py b/pm4py/algo/discovery/powl/inductive/variants/brute_force/factory.py new file mode 100644 index 000000000..a640921fc --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/brute_force/factory.py @@ -0,0 +1,29 @@ +from typing import List, Optional, Dict, Any, Tuple, Type + +from pm4py.algo.discovery.powl.inductive.cuts.concurrency import POWLConcurrencyCutUVCL +from pm4py.algo.discovery.powl.inductive.cuts.factory import S, T, CutFactory +from pm4py.algo.discovery.powl.inductive.cuts.loop import POWLLoopCutUVCL +from pm4py.algo.discovery.powl.inductive.cuts.sequence import POWLStrictSequenceCutUVCL +from pm4py.algo.discovery.powl.inductive.cuts.xor import POWLExclusiveChoiceCutUVCL +from pm4py.algo.discovery.inductive.dtypes.im_ds import IMDataStructureUVCL, IMDataStructure +from pm4py.algo.discovery.powl.inductive.variants.brute_force.bf_partial_order_cut import BruteForcePartialOrderCutUVCL +from pm4py.algo.discovery.powl.inductive.variants.powl_discovery_varaints import POWLDiscoveryVariant +from pm4py.objects.powl.obj import POWL + + +class CutFactoryPOBF(CutFactory): + + @classmethod + def get_cuts(cls, obj: T, inst: POWLDiscoveryVariant, parameters: Optional[Dict[str, Any]] = None) -> List[Type[S]]: + if type(obj) is IMDataStructureUVCL: + return [POWLExclusiveChoiceCutUVCL, POWLStrictSequenceCutUVCL, POWLConcurrencyCutUVCL, POWLLoopCutUVCL, BruteForcePartialOrderCutUVCL] + return list() + + @classmethod + def find_cut(cls, obj: IMDataStructure, inst: POWLDiscoveryVariant, parameters: Optional[Dict[str, Any]] = None) -> Optional[ + Tuple[POWL, List[T]]]: + for c in CutFactoryPOBF.get_cuts(obj, inst): + r = c.apply(obj, parameters) + if r is not None: + return r + return None diff --git a/pm4py/algo/discovery/powl/inductive/variants/clustering/__init__.py b/pm4py/algo/discovery/powl/inductive/variants/clustering/__init__.py new file mode 100644 index 000000000..9bb038112 --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/clustering/__init__.py @@ -0,0 +1 @@ +from pm4py.algo.discovery.powl.inductive.variants.clustering import * \ No newline at end of file diff --git a/pm4py/algo/discovery/powl/inductive/variants/clustering/cluster_partial_order_cut_EFG.py b/pm4py/algo/discovery/powl/inductive/variants/clustering/cluster_partial_order_cut_EFG.py new file mode 100644 index 000000000..ee54d16e9 --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/clustering/cluster_partial_order_cut_EFG.py @@ -0,0 +1,166 @@ +from abc import ABC +from collections import Counter +from itertools import combinations +from typing import Any, Optional, Dict, List, Generic, Tuple, Collection + +from pm4py.algo.discovery.inductive.cuts.abc import Cut, T +from pm4py.algo.discovery.inductive.dtypes.im_ds import IMDataStructureUVCL +from pm4py.objects.powl.BinaryRelation import BinaryRelation +from pm4py.objects.powl.obj import StrictPartialOrder, POWL + + +def generate_initial_order(nodes, efg): + po = BinaryRelation(nodes) + for a, b in combinations(nodes, 2): + if (a, b) in efg: + if not (b, a) in efg: + po.add_edge(a, b) + else: + if (b, a) in efg: + po.add_edge(b, a) + return po + +def remove(blocks, g): + res = [] + for g2 in blocks: + if not g2.__str__().__eq__(g.__str__()): + res.append(g2) + return res + + +def contains(blocks, g): + for g2 in blocks: + if g2.__str__().__eq__(g.__str__()): + return True + return False + + +def is_valid_order(po, parameters): + if len(po.nodes) < 2: + return False + + if not po.is_strict_partial_order(): + return False + + start_blocks = po.nodes + end_blocks = po.nodes + efg = parameters["EFG"] + + for group_1, group_2 in combinations(po.nodes, 2): + + edge_g1_g2 = po.is_edge(group_1, group_2) + edge_g2_g1 = po.is_edge(group_2, group_1) + + if edge_g1_g2: + start_blocks = remove(start_blocks, group_2) + end_blocks = remove(end_blocks, group_1) + if edge_g2_g1: + start_blocks = remove(start_blocks, group_1) + end_blocks = remove(end_blocks, group_2) + + all_ef_g1_g2 = True + all_ef_g2_g1 = True + + for a in group_1: + for b in group_2: + if not (a, b) in efg: + all_ef_g1_g2 = False + if not (b, a) in efg: + all_ef_g2_g1 = False + if all_ef_g1_g2 and all_ef_g2_g1 and (edge_g1_g2 or edge_g2_g1): + return False + if not edge_g1_g2 and not edge_g2_g1 and not (all_ef_g1_g2 and all_ef_g2_g1): + return False + + n = len(po.nodes) + for i in range(n): + group = po.nodes[i] + c1 = contains(start_blocks, group) + c2 = len(set(group).intersection(set(parameters["start_activities"]))) > 0 + c3 = contains(end_blocks, group) + c4 = len(set(group).intersection(set(parameters["end_activities"]))) > 0 + if (c1 and not c2) or (c3 and not c4): + return False + + + return True + + +def cluster_order(binary_relation): + pre = {node: set() for node in binary_relation.nodes} + post = {node: set() for node in binary_relation.nodes} + for node1 in binary_relation.nodes: + for node2 in binary_relation.nodes: + if binary_relation.is_edge(node1, node2): + pre[node2].add(node1) + post[node1].add(node2) + + clusters = [] + for node in binary_relation.nodes: + matched = False + for i in range(len(clusters)): + cluster = clusters[i] + if pre[node] == pre[cluster[0]] and post[node] == post[cluster[0]]: + clusters[i].append(node) + matched = True + break + if not matched: + clusters.append([node]) + + new_relation = BinaryRelation([tuple(c) for c in clusters]) + for cluster1 in new_relation.nodes: + for cluster2 in new_relation.nodes: + node1 = cluster1[0] + node2 = cluster2[0] + if binary_relation.is_edge(node1, node2): + new_relation.add_edge(cluster1, cluster2) + + return new_relation + + +class ClusterPartialOrderEFGCut(Cut[T], ABC, Generic[T]): + + @classmethod + def operator(cls, parameters: Optional[Dict[str, Any]] = None) -> StrictPartialOrder: + return StrictPartialOrder([]) + + @classmethod + def holds(cls, obj: T, parameters: Optional[Dict[str, Any]] = None) -> Optional[BinaryRelation]: + + efg = parameters["EFG"] + alphabet = parameters["alphabet"] + po = generate_initial_order(alphabet, efg) + clustered_po = cluster_order(po) + + if is_valid_order(clustered_po, parameters): + return clustered_po + else: + return None + + @classmethod + def apply(cls, obj: T, parameters: Optional[Dict[str, Any]] = None) -> Optional[Tuple[StrictPartialOrder, List[POWL]]]: + g = cls.holds(obj, parameters) + if g is None: + return g + children = cls.project(obj, g.nodes, parameters) + po = StrictPartialOrder(children) + for i, j in combinations(range(len(g.nodes)), 2): + if g.is_edge_id(i, j): + po.order.add_edge(children[i], children[j]) + elif g.is_edge_id(j, i): + po.order.add_edge(children[j], children[i]) + return po, po.children + + +class ClusterPartialOrderEFGCutUVCL(ClusterPartialOrderEFGCut[IMDataStructureUVCL]): + + @classmethod + def project(cls, obj: IMDataStructureUVCL, groups: List[Collection[Any]], + parameters: Optional[Dict[str, Any]] = None) -> List[IMDataStructureUVCL]: + r = list() + for g in groups: + c = Counter() + for t in obj.data_structure: + c[tuple(filter(lambda e: e in g, t))] = obj.data_structure[t] + r.append(c) + return list(map(lambda l: IMDataStructureUVCL(l), r)) diff --git a/pm4py/algo/discovery/powl/inductive/variants/clustering/factory.py b/pm4py/algo/discovery/powl/inductive/variants/clustering/factory.py new file mode 100644 index 000000000..d04d72c6c --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/clustering/factory.py @@ -0,0 +1,39 @@ +from typing import List, Optional, Dict, Any, Tuple, Type + +from pm4py.algo.discovery.powl.inductive.cuts.concurrency import POWLConcurrencyCutUVCL +from pm4py.algo.discovery.powl.inductive.cuts.factory import S, T, CutFactory +from pm4py.algo.discovery.powl.inductive.cuts.loop import POWLLoopCutUVCL +from pm4py.algo.discovery.powl.inductive.cuts.sequence import POWLStrictSequenceCutUVCL +from pm4py.algo.discovery.powl.inductive.cuts.xor import POWLExclusiveChoiceCutUVCL +from pm4py.algo.discovery.inductive.dtypes.im_ds import IMDataStructureUVCL +from pm4py.algo.discovery.powl.inductive.variants.clustering.cluster_partial_order_cut_EFG import ClusterPartialOrderEFGCutUVCL +from pm4py.objects.powl.obj import POWL +from pm4py.statistics.eventually_follows.uvcl.get import apply as to_efg +from pm4py.objects.dfg import util as dfu + + +class CutFactoryPOCluster(CutFactory): + + @classmethod + def get_cuts(cls, obj: T, parameters: Optional[Dict[str, Any]] = None) -> List[Type[S]]: + if type(obj) is IMDataStructureUVCL: + return [POWLExclusiveChoiceCutUVCL, POWLStrictSequenceCutUVCL, POWLConcurrencyCutUVCL, POWLLoopCutUVCL, ClusterPartialOrderEFGCutUVCL] + return list() + + @classmethod + def find_cut(cls, obj: IMDataStructureUVCL, parameters: Optional[Dict[str, Any]] = None) -> Optional[ + Tuple[POWL, List[T]]]: + init_efg = to_efg(obj) + alphabet = sorted(dfu.get_vertices(obj.dfg), key=lambda g: g.__str__()) + if len(alphabet) < 2: + return None + parameters["EFG"] = init_efg + parameters["DFG"] = obj.dfg.graph + parameters["alphabet"] = alphabet + parameters["start_activities"] = sorted(list(obj.dfg.start_activities.keys())) + parameters["end_activities"] = sorted(list(obj.dfg.end_activities.keys())) + for c in CutFactoryPOCluster.get_cuts(obj): + r = c.apply(obj, parameters) + if r is not None: + return r + return None diff --git a/pm4py/algo/discovery/powl/inductive/variants/im_base.py b/pm4py/algo/discovery/powl/inductive/variants/im_base.py new file mode 100644 index 000000000..231dafe46 --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/im_base.py @@ -0,0 +1,60 @@ +from itertools import combinations +from typing import Optional, Tuple, List, TypeVar, Generic, Dict, Any + +from pm4py.algo.discovery.inductive.dtypes.im_ds import IMDataStructureUVCL +from pm4py.algo.discovery.powl.inductive.fall_through.empty_traces import POWLEmptyTracesUVCL +from pm4py.algo.discovery.inductive.variants.abc import InductiveMinerFramework + +from pm4py.algo.discovery.powl.inductive.base_case.factory import BaseCaseFactory +from pm4py.algo.discovery.powl.inductive.cuts.factory import CutFactory +from pm4py.algo.discovery.powl.inductive.fall_through.factory import FallThroughFactory +from pm4py.algo.discovery.powl.inductive.variants.powl_discovery_varaints import POWLDiscoveryVariant + +from pm4py.objects.powl.obj import POWL, StrictPartialOrder, Sequence + +T = TypeVar('T', bound=IMDataStructureUVCL) + + +class IMBasePOWL(Generic[T], InductiveMinerFramework[T]): + + def instance(self) -> POWLDiscoveryVariant: + return POWLDiscoveryVariant.IM_BASE + def apply(self, obj: IMDataStructureUVCL, parameters: Optional[Dict[str, Any]] = None) -> POWL: + empty_traces = POWLEmptyTracesUVCL.apply(obj, parameters) + if empty_traces is not None: + return self._recurse(empty_traces[0], empty_traces[1], parameters) + powl = self.apply_base_cases(obj, parameters) + if powl is None: + cut = self.find_cut(obj, parameters) + if cut is not None: + powl = self._recurse(cut[0], cut[1], parameters=parameters) + if powl is None: + ft = self.fall_through(obj, parameters) + powl = self._recurse(ft[0], ft[1], parameters=parameters) + return powl + + def apply_base_cases(self, obj: T, parameters: Optional[Dict[str, Any]] = None) -> Optional[POWL]: + return BaseCaseFactory.apply_base_cases(obj, parameters=parameters) + + def find_cut(self, obj: T, parameters: Optional[Dict[str, Any]] = None) -> Optional[Tuple[POWL, List[T]]]: + return CutFactory.find_cut(obj, parameters=parameters) + + def fall_through(self, obj: T, parameters: Optional[Dict[str, Any]] = None) -> Tuple[POWL, List[T]]: + return FallThroughFactory.fall_through(obj, self._pool, self._manager, parameters=parameters) + + def _recurse(self, powl: POWL, objs: List[T], parameters: Optional[Dict[str, Any]] = None): + children = [self.apply(obj, parameters=parameters) for obj in objs] + if isinstance(powl, StrictPartialOrder): + if isinstance(powl, Sequence): + return Sequence(children) + powl_new = StrictPartialOrder(children) + for i, j in combinations(range(len(powl.children)), 2): + if powl.order.is_edge_id(i, j): + powl_new.order.add_edge(children[i], children[j]) + elif powl.order.is_edge_id(j, i): + powl_new.order.add_edge(children[j], children[i]) + return powl_new + else: + powl.children.extend(children) + return powl + diff --git a/pm4py/algo/discovery/powl/inductive/variants/im_brute_force.py b/pm4py/algo/discovery/powl/inductive/variants/im_brute_force.py new file mode 100644 index 000000000..690ec553f --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/im_brute_force.py @@ -0,0 +1,18 @@ +from typing import Optional, Tuple, List, Dict, Any + +from pm4py.algo.discovery.powl.inductive.variants.brute_force.factory import CutFactoryPOBF +from pm4py.algo.discovery.powl.inductive.variants.im_base import IMBasePOWL, T +from pm4py.algo.discovery.powl.inductive.variants.powl_discovery_varaints import POWLDiscoveryVariant +from pm4py.objects.powl.obj import POWL + + +class BruteForcePOWL(IMBasePOWL): + + def instance(self) -> POWLDiscoveryVariant: + return POWLDiscoveryVariant.BRUTE_FORCE + + def find_cut(self, obj: T, parameters: Optional[Dict[str, Any]] = None) -> Optional[Tuple[POWL, List[T]]]: + res = CutFactoryPOBF.find_cut(obj, self.instance(), parameters=parameters) + return res + + diff --git a/pm4py/algo/discovery/powl/inductive/variants/im_cluster.py b/pm4py/algo/discovery/powl/inductive/variants/im_cluster.py new file mode 100644 index 000000000..ffe5b0911 --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/im_cluster.py @@ -0,0 +1,15 @@ +from typing import Optional, Tuple, List, Dict, Any + +from pm4py.algo.discovery.powl.inductive.variants.clustering.factory import CutFactoryPOCluster +from pm4py.algo.discovery.powl.inductive.variants.im_base import IMBasePOWL, T +from pm4py.algo.discovery.powl.inductive.variants.powl_discovery_varaints import POWLDiscoveryVariant +from pm4py.objects.powl.obj import POWL + +class ClusterPOWL(IMBasePOWL): + + def instance(self) -> POWLDiscoveryVariant: + return POWLDiscoveryVariant.CLUSTER + + def find_cut(self, obj: T, parameters: Optional[Dict[str, Any]] = None) -> Optional[Tuple[POWL, List[T]]]: + res = CutFactoryPOCluster.find_cut(obj, parameters=parameters) + return res diff --git a/pm4py/algo/discovery/powl/inductive/variants/powl_discovery_varaints.py b/pm4py/algo/discovery/powl/inductive/variants/powl_discovery_varaints.py new file mode 100644 index 000000000..b7e4ffc42 --- /dev/null +++ b/pm4py/algo/discovery/powl/inductive/variants/powl_discovery_varaints.py @@ -0,0 +1,6 @@ +from enum import Enum, auto + +class POWLDiscoveryVariant(Enum): + IM_BASE = auto() # this is base IM with no partial orders + BRUTE_FORCE = auto() # BPM paper + CLUSTER = auto() # ICPM paper diff --git a/setup.py b/setup.py index 3b0b1bad5..929d83a1e 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,8 @@ def read_file(filename): 'pm4py.algo.discovery.ocel.interleavings.variants', 'pm4py.algo.discovery.ocel.link_analysis', 'pm4py.algo.discovery.ocel.link_analysis.variants', 'pm4py.algo.discovery.powl', 'pm4py.algo.discovery.powl.inductive', 'pm4py.algo.discovery.powl.inductive.cuts', + 'pm4py.algo.discovery.powl.inductive.variants', 'pm4py.algo.discovery.powl.inductive.variants.clustering', + 'pm4py.algo.discovery.powl.inductive.variants.brute_force', 'pm4py.algo.discovery.powl.inductive.base_case', 'pm4py.algo.discovery.powl.inductive.fall_through', 'pm4py.algo.discovery.alpha', 'pm4py.algo.discovery.alpha.utils', 'pm4py.algo.discovery.alpha.variants', 'pm4py.algo.discovery.alpha.data_structures', 'pm4py.algo.discovery.causal', diff --git a/tests/powlTest.py b/tests/powlTest.py index 2026680c5..11eb81518 100644 --- a/tests/powlTest.py +++ b/tests/powlTest.py @@ -10,7 +10,7 @@ def discover_powl(event_log): - powl = powl_disc.apply(event_log, variant = POWLDiscoveryVariant.CLUSTER) + powl = powl_disc.apply(event_log, variant=POWLDiscoveryVariant.CLUSTER) vis_1 = visualize_powl(powl, parameters={"format": "svg"}) vis_1.view() pn, init, final = powl_to_pn(powl)