Skip to content

Commit

Permalink
Merge branch 'ft-1516-frequency-annotation-trees-tree-alignments' int…
Browse files Browse the repository at this point in the history
…o 'integration'

PM4PY-1516 Frequency annotation of process tree using process tree alignments

See merge request process-mining/pm4py/pm4py-core!572
  • Loading branch information
fit-sebastiaan-van-zelst committed Jan 6, 2022
2 parents b82dd92 + 87caf1e commit 8da0f41
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 5 deletions.
17 changes: 17 additions & 0 deletions examples/process_tree_frequency_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pm4py
import os
from pm4py.algo.conformance.alignments.process_tree.util import search_graph_pt_frequency_annotation
from pm4py.visualization.process_tree import visualizer as pt_visualizer


def execute_script():
log = pm4py.read_xes(os.path.join("..", "tests", "input_data", "receipt.xes"))
tree = pm4py.discover_process_tree_inductive(log)
aligned_traces = pm4py.conformance_diagnostics_alignments(log, tree)
tree = search_graph_pt_frequency_annotation.apply(tree, aligned_traces)
gviz = pt_visualizer.apply(tree, parameters={"format": "svg"}, variant=pt_visualizer.Variants.FREQUENCY_ANNOTATION)
pt_visualizer.view(gviz)


if __name__ == "__main__":
execute_script()
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from pm4py.algo.conformance.alignments.process_tree.util import search_graph_pt_replay_semantics
from pm4py.algo.conformance.alignments.process_tree.util import search_graph_pt_replay_semantics, search_graph_pt_frequency_annotation
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from pm4py.objects.process_tree.obj import ProcessTree
from typing import Optional, Dict, Any, Union
from pm4py.util import typing, exec_utils
from enum import Enum
from collections import Counter
from pm4py.objects.process_tree.utils import bottomup


class Parameters(Enum):
NUM_EVENTS_PROPERTY = "num_events_property"
NUM_CASES_PROPERTY = "num_cases_property"


def apply(pt: ProcessTree, align_result: Union[typing.AlignmentResult, typing.ListAlignments],
parameters: Optional[Dict[Any, Any]] = None) -> ProcessTree:
"""
Annotate a process tree with frequency information (number of events / number of cases),
given the results of an alignment performed on the process tree.
Parameters
----------------
pt
Process tree
parameters
Parameters of the algorithm, including:
- Parameters.NUM_EVENTS_PROPERTY => number of events
- Parameters.NUM_CASES_PROPERTY => number of cases
Returns
----------------
pt
Annotated process tree
"""
if parameters is None:
parameters = {}

num_events_property = exec_utils.get_param_value(Parameters.NUM_EVENTS_PROPERTY, parameters, "num_events")
num_cases_property = exec_utils.get_param_value(Parameters.NUM_CASES_PROPERTY, parameters, "num_cases")
bottomup_nodes = bottomup.get_bottomup_nodes(pt, parameters=parameters)

all_paths_open_enabled_events = []
all_paths_open_enabled_cases = []
for trace in align_result:
state = trace["state"]
paths = []
while state.parent is not None:
if state.path:
paths.append(state.path)
state = state.parent
paths.reverse()
paths_enabled = [y[0] for x in paths for y in x if y[1] is ProcessTree.OperatorState.ENABLED]
paths_open = [y[0] for x in paths for y in x if y[1] is ProcessTree.OperatorState.OPEN if
y[0] not in paths_enabled]
all_paths_open_enabled_events = all_paths_open_enabled_events + paths_enabled + paths_open
all_paths_open_enabled_cases = all_paths_open_enabled_cases + list(set(paths_enabled + paths_open))
all_paths_open_enabled_events_counter = Counter(all_paths_open_enabled_events)
all_paths_open_enabled_cases_counter = Counter(all_paths_open_enabled_cases)

for node in bottomup_nodes:
node._properties[num_events_property] = all_paths_open_enabled_events_counter[node]
node._properties[num_cases_property] = all_paths_open_enabled_cases_counter[node]

return pt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(self, costs: float, index: int, state: pt_sem.ProcessTreeState,
self.leaves = leaves if leaves is not None else list() # leaves that 'got you here'
self.parent = parent # parent search state
self.children = children if children is not None else set() # successor search states
self.path = []

def __lt__(self, other):
if self.costs < other.costs:
Expand Down Expand Up @@ -178,10 +179,12 @@ def align_variant(variant, tree_leaf_set, pt):
sync_path, new_state = pt_sem.shortest_path_to_close(leaf, new_state)
path.extend(sync_path)
leaves = _obtain_leaves_from_state_path(path, include_tau=True)
open_set, closed_set = _add_new_state(
SGASearchState(sga_state.costs + len(model_moves),
new_state = SGASearchState(sga_state.costs + len(model_moves),
sga_state.index + 1,
new_state, leaves=leaves, parent=sga_state), sga_state,
new_state, leaves=leaves, parent=sga_state)
new_state.path = tuple(path)
open_set, closed_set = _add_new_state(
new_state, sga_state,
open_set,
closed_set)
if need_log_move:
Expand Down
1 change: 1 addition & 0 deletions pm4py/objects/process_tree/obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def __init__(self, operator=None, parent=None, children=None, label=None):
self._parent = parent
self._children = list() if children is None else children
self._label = label
self._properties = {}

def __hash__(self):
if self.label is not None:
Expand Down
109 changes: 109 additions & 0 deletions pm4py/visualization/process_tree/variants/frequency_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import tempfile
import uuid
from copy import deepcopy, copy
from enum import Enum

from graphviz import Graph

from pm4py.objects.process_tree.utils import generic
from pm4py.util import exec_utils
from typing import Optional, Dict, Any, Union
from pm4py.objects.process_tree.obj import ProcessTree
import graphviz
from pm4py.util import vis_utils


class Parameters(Enum):
FORMAT = "format"
ENABLE_DEEPCOPY = "enable_deepcopy"
FONT_SIZE = "font_size"
BGCOLOR = "bgcolor"
NUM_EVENTS_PROPERTY = "num_events_property"
NUM_CASES_PROPERTY = "num_cases_property"


# maps the operators to the ProM strings
operators_mapping = {"->": "seq", "X": "xor", "+": "and", "*": "xor loop", "O": "or"}

# root node parameter
ROOT_NODE_PARAMETER = "@@root_node"


def repr_tree_2(tree, viz, parameters):
num_events_property = exec_utils.get_param_value(Parameters.NUM_EVENTS_PROPERTY, parameters, "num_events")
num_cases_property = exec_utils.get_param_value(Parameters.NUM_CASES_PROPERTY, parameters, "num_cases")
root_node = parameters[ROOT_NODE_PARAMETER]

root_node_num_cases = root_node._properties[num_cases_property]
this_node_num_cases = tree._properties[num_cases_property] if num_cases_property in tree._properties else 0
this_node_num_events = tree._properties[num_events_property] if num_events_property in tree._properties else 0

font_size = exec_utils.get_param_value(Parameters.FONT_SIZE, parameters, 15)
font_size = str(font_size)

this_node_id = str(id(tree))

if tree.operator is None:
if tree.label is None:
viz.node(this_node_id, "tau", style='filled', fillcolor='black', shape='point', width="0.075", fontsize=font_size)
else:
node_color = vis_utils.get_trans_freq_color(this_node_num_cases, 0, root_node_num_cases)
node_label = str(tree) + "\nC=%d E=%d" % (this_node_num_cases, this_node_num_events)
viz.node(this_node_id, node_label, fontsize=font_size, style="filled", fillcolor=node_color)
else:
node_color = vis_utils.get_trans_freq_color(this_node_num_cases, 0, root_node_num_cases)
viz.node(this_node_id, operators_mapping[str(tree.operator)], fontsize=font_size, style="filled", fillcolor=node_color)

for child in tree.children:
repr_tree_2(child, viz, parameters)

if tree.parent is not None:
viz.edge(str(id(tree.parent)), this_node_id, dirType='none')


def apply(tree: ProcessTree, parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> graphviz.Graph:
"""
Obtain a Process Tree representation through GraphViz
Parameters
-----------
tree
Process tree
parameters
Possible parameters of the algorithm
Returns
-----------
gviz
GraphViz object
"""
if parameters is None:
parameters = {}

parameters = copy(parameters)
parameters[ROOT_NODE_PARAMETER] = tree

filename = tempfile.NamedTemporaryFile(suffix='.gv')

bgcolor = exec_utils.get_param_value(Parameters.BGCOLOR, parameters, "transparent")

viz = Graph("pt", filename=filename.name, engine='dot', graph_attr={'bgcolor': bgcolor})
viz.attr('node', shape='ellipse', fixedsize='false')

image_format = exec_utils.get_param_value(Parameters.FORMAT, parameters, "png")

enable_deepcopy = exec_utils.get_param_value(Parameters.ENABLE_DEEPCOPY, parameters, False)

if enable_deepcopy:
# since the process tree object needs to be sorted in the visualization, make a deepcopy of it before
# proceeding
tree = deepcopy(tree)
generic.tree_sort(tree)

repr_tree_2(tree, viz, parameters)

viz.attr(overlap='false')
viz.attr(splines='false')
viz.format = image_format

return viz
3 changes: 2 additions & 1 deletion pm4py/visualization/process_tree/visualizer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pm4py.visualization.common import gview
from pm4py.visualization.common import save as gsave
from pm4py.visualization.process_tree.variants import wo_decoration, symbolic
from pm4py.visualization.process_tree.variants import wo_decoration, symbolic, frequency_annotation
from enum import Enum
from pm4py.util import exec_utils
from pm4py.visualization.common.gview import serialize, serialize_dot
Expand All @@ -12,6 +12,7 @@
class Variants(Enum):
WO_DECORATION = wo_decoration
SYMBOLIC = symbolic
FREQUENCY_ANNOTATION = frequency_annotation


DEFAULT_VARIANT = Variants.WO_DECORATION
Expand Down

0 comments on commit 8da0f41

Please sign in to comment.