''' This file is part of PM4Py (More Info: https://pm4py.fit.fraunhofer.de). PM4Py is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. PM4Py 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 General Public License for more details. You should have received a copy of the GNU General Public License along with PM4Py. If not, see . ''' from enum import Enum from pm4py.algo.discovery.footprints import algorithm as fp_discovery from pm4py.objects.log.obj import EventLog, Trace, Event from pm4py.objects.process_tree.utils import bottomup as bottomup_discovery from pm4py.objects.process_tree.obj import Operator from pm4py.util import constants, xes_constants from pm4py.util import exec_utils from typing import Optional, Dict, Any, Union from pm4py.objects.log.obj import EventLog from pm4py.objects.process_tree.obj import ProcessTree TRACES = "traces" SKIPPABLE = "skippable" class Parameters(Enum): MIN_TRACE_LENGTH = "min_trace_length" MAX_TRACE_LENGTH = "max_trace_length" MAX_LOOP_OCC = "max_loop_occ" ACTIVITY_KEY = constants.PARAMETER_CONSTANT_ACTIVITY_KEY MAX_LIMIT_NUM_TRACES = "max_limit_num_traces" RETURN_SET_STRINGS = "return_set_strings" def get_playout_leaf(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces): """ Performs the playout of a leaf (activity or invisible), returning the traces allowed by the tree """ mr = min_rem_dict[node] mar = max_rem_dict[node] playout_dictio[node] = {TRACES: set()} if node.label is None: playout_dictio[node][TRACES].add(tuple([])) else: if mar + 1 >= min_trace_length and max_trace_length - mr >= 1: playout_dictio[node][TRACES].add((node.label,)) def get_playout_xor(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces): """ Performs the playout of a XOR node, returning the traces allowed by the tree """ mr = min_rem_dict[node] traces = set() for n in node.children: traces = traces.union(playout_dictio[n][TRACES]) if len(traces) > max_limit_num_traces: break playout_dictio[node] = {TRACES: traces} def get_min_remaining_length(traces): """ Minimum remaining length (for sequential, parallel cut detection) Parameters -------------- traces Traces """ min_len_traces = [] min_rem_length = [] for x in traces: if len(x) == 0: min_len_traces.append(0) else: min_len_traces.append(len(x[0])) min_rem_length.append(0) min_rem_length[-1] = 0 min_rem_length[-2] = min_len_traces[-1] j = len(traces) - 3 while j >= 0: min_rem_length[j] = min_rem_length[j + 1] + min_len_traces[j + 1] j = j - 1 return min_len_traces, min_rem_length def get_max_remaining_length(traces): """ Maximum remaining length (for sequential, parallel cut detection) Parameters -------------- traces Traces """ max_len_traces = [] max_rem_length = [] for x in traces: if len(x) == 0: max_len_traces.append(0) else: max_len_traces.append(len(x[-1])) max_rem_length.append(0) max_rem_length[-1] = 0 max_rem_length[-2] = max_len_traces[-1] j = len(traces) - 3 while j >= 0: max_rem_length[j] = max_rem_length[j + 1] + max_len_traces[j + 1] j = j - 1 return max_len_traces, max_rem_length def flatten(x): """ Flattens a list of tuples """ ret = [] for y in x: for z in y: ret.append(z) return ret def get_sequential_compositions_children(traces, min_trace_length, max_trace_length, mr, mar, max_limit_num_traces): """ Returns alls the possible sequential combinations between the children of a tree """ diff = max_trace_length - mr diff2 = min_trace_length - mar min_len_traces, min_rem_length = get_min_remaining_length(traces) max_len_traces, max_rem_length = get_max_remaining_length(traces) curr = list(traces[0]) i = 1 while i < len(traces): mrl = min_rem_length[i] marl = max_rem_length[i] to_visit = [] j = 0 while j < len(curr): x = curr[j] if not type(x) is list: x = [x] z = 0 while z < len(traces[i]): y = traces[i][z] xy = list(x) xy.append(y) val = sum(len(k) for k in xy) if val + mrl <= diff and val + marl >= diff2: to_visit.append(xy) z = z + 1 j = j + 1 curr = to_visit i = i + 1 return curr def get_playout_parallel(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces): """ Performs the playout of an AND node, returning the traces allowed by the tree """ mr = min_rem_dict[node] mar = max_rem_dict[node] traces = list(sorted(playout_dictio[x][TRACES], key=lambda x: len(x)) for x in node.children) sequential_compositions = get_sequential_compositions_children(traces, min_trace_length, max_trace_length, mr, mar, max_limit_num_traces) final_traces = list() for x in sequential_compositions: if len(final_traces) >= max_limit_num_traces: break to_visit = [[[]] + [len(y) for y in x]] while len(to_visit) > 0: curr = to_visit.pop(0) possible_choices = [i - 1 for i in range(1, len(curr)) if curr[i] > 0] for j in possible_choices: new = list(curr) new[0] = list(new[0]) new[0].append(x[j][len(x[j]) - curr[j + 1]]) new[j + 1] = new[j + 1] - 1 to_visit.append(new) if not possible_choices: final_traces.append(tuple(curr[0])) if len(final_traces) >= max_limit_num_traces: break playout_dictio[node] = {TRACES: set(final_traces)} def get_playout_sequence(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces): """ Performs the playout of a sequence node, returning the traces allowed by the tree """ mr = min_rem_dict[node] mar = max_rem_dict[node] final_traces = set() traces = list(sorted(playout_dictio[x][TRACES], key=lambda x: len(x)) for x in node.children) sequential_compositions = get_sequential_compositions_children(traces, min_trace_length, max_trace_length, mr, mar, max_limit_num_traces) for x in sequential_compositions: final_traces.add(tuple(flatten(x))) playout_dictio[node] = {TRACES: final_traces} def get_playout_loop(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces): """ Performs the playout of a loop node, returning the traces allowed by the tree """ mr = min_rem_dict[node] mar = max_rem_dict[node] final_traces = set() do_traces = sorted(list(playout_dictio[node.children[0]][TRACES]), key=lambda x: len(x)) redo_traces = sorted(list(playout_dictio[node.children[1]][TRACES]), key=lambda x: len(x)) min_do_trace = min(len(x) for x in do_traces) if do_traces else 0 to_visit = list((x, 0, 0) for x in do_traces) closed = set() diff1 = max_trace_length - mr diff2 = max_trace_length - min_do_trace - mr diff3 = min_trace_length - mar while to_visit: curr = to_visit.pop(0) curr_trace = curr[0] position = curr[1] num_loops = curr[2] if position == 0: if curr_trace in closed: continue closed.add(curr_trace) if diff3 <= len(curr_trace) <= diff1: final_traces.add(curr_trace) if len(final_traces) > max_limit_num_traces: break for y in redo_traces: new = curr_trace + y if len(new) <= diff2 and num_loops + 1 <= max_loop_occ: to_visit.append((new, 1, num_loops + 1)) else: break elif position == 1: for y in do_traces: new = curr_trace + y if len(new) <= diff1: to_visit.append((new, 0, num_loops)) else: break playout_dictio[node] = {TRACES: final_traces} def get_playout(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces): """ Performs a playout of an ode of the process tree, given the type """ if len(node.children) == 0: get_playout_leaf(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces) elif node.operator == Operator.XOR: get_playout_xor(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces) elif node.operator == Operator.PARALLEL: get_playout_parallel(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces) elif node.operator == Operator.SEQUENCE: get_playout_sequence(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces) elif node.operator == Operator.LOOP: get_playout_loop(node, playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces) def apply(tree: ProcessTree, parameters : Optional[Dict[Union[str, Parameters], Any]] = None) -> EventLog: """ Performs an extensive playout of the process tree Parameters ------------- tree Process tree parameters Possible parameters, including: - Parameters.MIN_TRACE_LENGTH => minimum length of a trace (default: 1) - Parameters.MAX_TRACE_LENGTH => maximum length of a trace (default: min_allowed_trace_length) - Parameters.MAX_LOOP_OCC => maximum number of occurrences for a loop (default: MAX_TRACE_LENGTH) - Parameters.ACTIVITY_KEY => activity key - Parameters.MAX_LIMIT_NUM_TRACES => maximum number to the limit of traces; the playout shall stop when the number is reached (default: 100000) Returns ------------- log Event log """ if parameters is None: parameters = {} activity_key = exec_utils.get_param_value(Parameters.ACTIVITY_KEY, parameters, xes_constants.DEFAULT_NAME_KEY) # to save memory in the returned log, allocate each activity once. to know the list of activities of the # process tree, use the footprints module fp_tree = fp_discovery.apply(tree, parameters=parameters) activities = fp_tree["activities"] activities = {act: Event({activity_key: act}) for act in activities} min_allowed_trace_length = bottomup_discovery.get_min_trace_length(tree, parameters=parameters) min_trace_length = exec_utils.get_param_value(Parameters.MIN_TRACE_LENGTH, parameters, 1) max_trace_length = exec_utils.get_param_value(Parameters.MAX_TRACE_LENGTH, parameters, min_allowed_trace_length) max_loop_occ = exec_utils.get_param_value(Parameters.MAX_LOOP_OCC, parameters, int(max_trace_length / 2)) max_limit_num_traces = exec_utils.get_param_value(Parameters.MAX_LIMIT_NUM_TRACES, parameters, 100000) return_set_strings = exec_utils.get_param_value(Parameters.RETURN_SET_STRINGS, parameters, False) bottomup = bottomup_discovery.get_bottomup_nodes(tree, parameters=parameters) min_rem_dict = bottomup_discovery.get_min_rem_dict(tree, parameters=parameters) max_rem_dict = bottomup_discovery.get_max_rem_dict(tree, parameters=parameters) playout_dictio = {} for i in range(len(bottomup)): get_playout(bottomup[i], playout_dictio, min_trace_length, max_trace_length, max_loop_occ, min_rem_dict, max_rem_dict, max_limit_num_traces) tree_playout_traces = playout_dictio[tree][TRACES] if return_set_strings: return tree_playout_traces log = EventLog() for tr0 in tree_playout_traces: trace = Trace() for act in tr0: trace.append(activities[act]) log.append(trace) return log