diff --git a/CHANGELOG.md b/CHANGELOG.md index 5957b3d55..d5160abf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # PM4Py Changelog +## PM4Py 2.2.13 (2021.09.03) + +### Fixed + +### Removed + +### Deprecated + +### Changed +* 5723df7b + * xes exporter now reports on xes features and xmlns +* 3b632548 + * graphviz based visualizations now expose background color as a parameter + +### Added +* 0592157b + * new dfg playout including performance specification +* 85739ba0 + * allow pandas df to be used as an iterable for streaming simulation +* 2fa9993f + * path filter that filters the cases of an event log where there is at least one occurrence of the provided path + occurring in a given time range. +* a7ee73a8 + * added filter based on rework detection +* c03b6188 + * add petri net, reset/inhibitor net and data petri net semantics +### Other + + +--- + ## PM4Py 2.2.12 (2021.08.19) ### Fixed diff --git a/docs/source/conf.py b/docs/source/conf.py index a1026952b..54c2f796a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ # The short X.Y version version = '2.2' # The full version, including alpha/beta/rc tags -release = '2.2.12' +release = '2.2.13' # -- General configuration --------------------------------------------------- diff --git a/examples/pandas_iterable.py b/examples/pandas_iterable.py new file mode 100644 index 000000000..f176cdfc7 --- /dev/null +++ b/examples/pandas_iterable.py @@ -0,0 +1,18 @@ +import pandas as pd +import pm4py +import os +from pm4py.streaming.conversion import from_pandas + + +def execute_script(): + df = pd.read_csv(os.path.join("..", "tests", "input_data", "receipt.csv")) + df = pm4py.format_dataframe(df) + it = from_pandas.apply(df) + count = 0 + for trace in it: + print(count, trace) + count = count + 1 + + +if __name__ == "__main__": + execute_script() diff --git a/examples/pandas_iterable_to_trace_stream.py b/examples/pandas_iterable_to_trace_stream.py new file mode 100644 index 000000000..d5c619bb7 --- /dev/null +++ b/examples/pandas_iterable_to_trace_stream.py @@ -0,0 +1,22 @@ +import pandas as pd +import pm4py +import os +from pm4py.streaming.conversion import from_pandas +from pm4py.streaming.stream.live_trace_stream import LiveTraceStream +from pm4py.streaming.util import trace_stream_printer + + +def execute_script(): + df = pd.read_csv(os.path.join("..", "tests", "input_data", "receipt.csv")) + df = pm4py.format_dataframe(df) + it = from_pandas.apply(df) + printer = trace_stream_printer.TraceStreamPrinter() + trace_stream = LiveTraceStream() + trace_stream.register(printer) + trace_stream.start() + it.to_trace_stream(trace_stream) + trace_stream.stop() + + +if __name__ == "__main__": + execute_script() diff --git a/examples/performance_dfg_simulation.py b/examples/performance_dfg_simulation.py new file mode 100644 index 000000000..957a73886 --- /dev/null +++ b/examples/performance_dfg_simulation.py @@ -0,0 +1,17 @@ +import os + +import pm4py +from pm4py.algo.simulation.playout.dfg import algorithm as dfg_simulator + + +def execute_script(): + log = pm4py.read_xes(os.path.join("..", "tests", "input_data", "receipt.xes")) + frequency_dfg, sa, ea = pm4py.discover_dfg(log) + performance_dfg, sa, ea = pm4py.discover_performance_dfg(log) + simulated_log = dfg_simulator.apply(frequency_dfg, sa, ea, variant=dfg_simulator.Variants.PERFORMANCE, + parameters={"performance_dfg": performance_dfg}) + print(simulated_log) + + +if __name__ == "__main__": + execute_script() diff --git a/pm4py/algo/filtering/log/paths/paths_filter.py b/pm4py/algo/filtering/log/paths/paths_filter.py index ea71821c8..091757bcb 100644 --- a/pm4py/algo/filtering/log/paths/paths_filter.py +++ b/pm4py/algo/filtering/log/paths/paths_filter.py @@ -21,8 +21,9 @@ from pm4py.objects.log.obj import EventLog, Trace from pm4py.util import exec_utils from pm4py.util import xes_constants as xes -from pm4py.util.constants import PARAMETER_CONSTANT_ATTRIBUTE_KEY +from pm4py.util.constants import PARAMETER_CONSTANT_ATTRIBUTE_KEY, PARAMETER_CONSTANT_TIMESTAMP_KEY import deprecation +import sys from typing import Optional, Dict, Any, Union, Tuple, List from pm4py.objects.log.obj import EventLog, EventStream, Trace @@ -32,6 +33,9 @@ class Parameters(Enum): ATTRIBUTE_KEY = PARAMETER_CONSTANT_ATTRIBUTE_KEY DECREASING_FACTOR = "decreasingFactor" POSITIVE = "positive" + TIMESTAMP_KEY = PARAMETER_CONSTANT_TIMESTAMP_KEY + MIN_PERFORMANCE = "min_performance" + MAX_PERFORMANCE = "max_performance" def apply(log: EventLog, paths: List[Tuple[str, str]], parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> EventLog: @@ -72,6 +76,53 @@ def apply(log: EventLog, paths: List[Tuple[str, str]], parameters: Optional[Dict return filtered_log +def apply_performance(log: EventLog, provided_path: Tuple[str, str], parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> EventLog: + """ + Filters the cases of an event log where there is at least one occurrence of the provided path + occurring in the defined timedelta range. + + Parameters + ---------------- + log + Event log + provided_path + Path between two activities (expressed as tuple) + parameters + Parameters of the filter, including: + Parameters.ATTRIBUTE_KEY -> Attribute identifying the activity in the log + Parameters.TIMESTAMP_KEY -> Attribute identifying the timestamp in the log + Parameters.POSITIVE -> Indicate if events should be kept/removed + Parameters.MIN_PERFORMANCE -> Minimal allowed performance of the provided path + Parameters.MAX_PERFORMANCE -> Maximal allowed performance of the provided path + + Returns + ---------------- + filtered_log + Filtered event log + """ + if parameters is None: + parameters = {} + attribute_key = exec_utils.get_param_value(Parameters.ATTRIBUTE_KEY, parameters, xes.DEFAULT_NAME_KEY) + timestamp_key = exec_utils.get_param_value(Parameters.TIMESTAMP_KEY, parameters, xes.DEFAULT_TIMESTAMP_KEY) + min_performance = exec_utils.get_param_value(Parameters.MIN_PERFORMANCE, parameters, 0) + max_performance = exec_utils.get_param_value(Parameters.MAX_PERFORMANCE, parameters, sys.maxsize) + positive = exec_utils.get_param_value(Parameters.POSITIVE, parameters, True) + filtered_log = EventLog(list(), attributes=log.attributes, extensions=log.extensions, classifiers=log.classifiers, + omni_present=log.omni_present, properties=log.properties) + for trace in log: + found = False + for i in range(len(trace) - 1): + path = (trace[i][attribute_key], trace[i + 1][attribute_key]) + if path == provided_path: + timediff = trace[i + 1][timestamp_key].timestamp() - trace[i][timestamp_key].timestamp() + if min_performance <= timediff <= max_performance: + found = True + break + if (found and positive) or (not found and not positive): + filtered_log.append(trace) + return filtered_log + + def get_paths_from_log(log, attribute_key="concept:name"): """ Get the paths of the log along with their count diff --git a/pm4py/algo/filtering/log/rework/__init__.py b/pm4py/algo/filtering/log/rework/__init__.py new file mode 100644 index 000000000..6ece5258d --- /dev/null +++ b/pm4py/algo/filtering/log/rework/__init__.py @@ -0,0 +1,17 @@ +''' + 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 pm4py.algo.filtering.log.rework import rework_filter diff --git a/pm4py/algo/filtering/log/rework/rework_filter.py b/pm4py/algo/filtering/log/rework/rework_filter.py new file mode 100644 index 000000000..b25781ec4 --- /dev/null +++ b/pm4py/algo/filtering/log/rework/rework_filter.py @@ -0,0 +1,74 @@ +''' + 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.util import constants, xes_constants, exec_utils +from pm4py.objects.log.obj import EventLog +from collections import Counter +from typing import Optional, Dict, Any + + +class Parameters(Enum): + ACTIVITY_KEY = constants.PARAMETER_CONSTANT_ACTIVITY_KEY + MIN_OCCURRENCES = "min_occurrences" + POSITIVE = "positive" + + +def apply(log: EventLog, activity: str, parameters: Optional[Dict[Any, Any]] = None) -> EventLog: + """ + Applies the rework filter on the provided event log and activity. + This filter the cases of the log having at least Parameters.MIN_OCCURRENCES (default: 2) occurrences + of the given activity. + + It is also possible (setting Parameters.POSITIVE to False) to retrieve the cases of the log not having the + given activity or having the activity occurred less than Parameters.MIN_OCCURRENCES times. + + Parameters + ------------------- + log + Event log + activity + Activity of which the rework shall be filtered + parameters + Parameters of the filter, including: + - Parameters.ACTIVITY_KEY => the attribute to use as activity + - Parameters.MIN_OCCURRENCES => the minimum number of occurrences for the activity + - Parameters.POSITIVE => if True, filters the cases of the log having at least MIN_OCCURRENCES occurrences. + if False, filters the cases of the log where such behavior does not occur. + + Returns + ----------------- + filtered_log + Filtered event log + """ + if parameters is None: + parameters = {} + + activity_key = exec_utils.get_param_value(Parameters.ACTIVITY_KEY, parameters, xes_constants.DEFAULT_NAME_KEY) + min_occurrences = exec_utils.get_param_value(Parameters.MIN_OCCURRENCES, parameters, 2) + positive = exec_utils.get_param_value(Parameters.POSITIVE, parameters, True) + + filtered_log = EventLog(list(), attributes=log.attributes, extensions=log.extensions, classifiers=log.classifiers, + omni_present=log.omni_present, properties=log.properties) + + for trace in log: + act_counter = Counter([x[activity_key] for x in trace]) + if positive and activity in act_counter and act_counter[activity] >= min_occurrences: + filtered_log.append(trace) + elif not positive and (activity not in act_counter or act_counter[activity] < min_occurrences): + filtered_log.append(trace) + + return filtered_log diff --git a/pm4py/algo/filtering/pandas/paths/paths_filter.py b/pm4py/algo/filtering/pandas/paths/paths_filter.py index 6c4060b78..b34d47dd6 100644 --- a/pm4py/algo/filtering/pandas/paths/paths_filter.py +++ b/pm4py/algo/filtering/pandas/paths/paths_filter.py @@ -28,6 +28,7 @@ import deprecation from typing import Optional, Dict, Any, Union, Tuple, List import pandas as pd +import sys class Parameters(Enum): @@ -36,6 +37,8 @@ class Parameters(Enum): TIMESTAMP_KEY = PARAMETER_CONSTANT_TIMESTAMP_KEY DECREASING_FACTOR = "decreasingFactor" POSITIVE = "positive" + MIN_PERFORMANCE = "min_performance" + MAX_PERFORMANCE = "max_performance" def apply(df: pd.DataFrame, paths: List[Tuple[str, str]], parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> pd.DataFrame: @@ -61,11 +64,11 @@ def apply(df: pd.DataFrame, paths: List[Tuple[str, str]], parameters: Optional[D """ if parameters is None: parameters = {} - paths = [path[0] + "," + path[1] for path in paths] case_id_glue = exec_utils.get_param_value(Parameters.CASE_ID_KEY, parameters, CASE_CONCEPT_NAME) attribute_key = exec_utils.get_param_value(Parameters.ATTRIBUTE_KEY, parameters, DEFAULT_NAME_KEY) timestamp_key = exec_utils.get_param_value(Parameters.TIMESTAMP_KEY, parameters, DEFAULT_TIMESTAMP_KEY) positive = exec_utils.get_param_value(Parameters.POSITIVE, parameters, True) + paths = [path[0] + "," + path[1] for path in paths] df = df.sort_values([case_id_glue, timestamp_key]) filt_df = df[[case_id_glue, attribute_key]] filt_dif_shifted = filt_df.shift(-1) @@ -84,6 +87,62 @@ def apply(df: pd.DataFrame, paths: List[Tuple[str, str]], parameters: Optional[D return ret +def apply_performance(df: pd.DataFrame, provided_path: Tuple[str, str], parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> pd.DataFrame: + """ + Filters the cases of a dataframe where there is at least one occurrence of the provided path + occurring in the defined timedelta range. + + Parameters + ---------- + df + Dataframe + paths + Paths to filter on + parameters + Possible parameters of the algorithm, including: + Parameters.CASE_ID_KEY -> Case ID column in the dataframe + Parameters.ATTRIBUTE_KEY -> Attribute we want to filter + Parameters.TIMESTAMP_KEY -> Attribute identifying the timestamp in the log + Parameters.POSITIVE -> Specifies if the filter should be applied including traces (positive=True) + or excluding traces (positive=False) + Parameters.MIN_PERFORMANCE -> Minimal allowed performance of the provided path + Parameters.MAX_PERFORMANCE -> Maximal allowed performance of the provided path + + Returns + ---------- + df + Filtered dataframe + """ + if parameters is None: + parameters = {} + case_id_glue = exec_utils.get_param_value(Parameters.CASE_ID_KEY, parameters, CASE_CONCEPT_NAME) + attribute_key = exec_utils.get_param_value(Parameters.ATTRIBUTE_KEY, parameters, DEFAULT_NAME_KEY) + timestamp_key = exec_utils.get_param_value(Parameters.TIMESTAMP_KEY, parameters, DEFAULT_TIMESTAMP_KEY) + positive = exec_utils.get_param_value(Parameters.POSITIVE, parameters, True) + provided_path = provided_path[0] + "," + provided_path[1] + min_performance = exec_utils.get_param_value(Parameters.MIN_PERFORMANCE, parameters, 0) + max_performance = exec_utils.get_param_value(Parameters.MAX_PERFORMANCE, parameters, sys.maxsize) + df = df.sort_values([case_id_glue, timestamp_key]) + filt_df = df[[case_id_glue, attribute_key, timestamp_key]] + filt_dif_shifted = filt_df.shift(-1) + filt_dif_shifted.columns = [str(col) + '_2' for col in filt_dif_shifted.columns] + stacked_df = pd.concat([filt_df, filt_dif_shifted], axis=1) + stacked_df["@@path"] = stacked_df[attribute_key] + "," + stacked_df[attribute_key + "_2"] + stacked_df = stacked_df[stacked_df["@@path"] == provided_path] + stacked_df["@@timedelta"] = (stacked_df[timestamp_key + "_2"] - stacked_df[timestamp_key]).astype('timedelta64[s]') + stacked_df = stacked_df[stacked_df["@@timedelta"] >= min_performance] + stacked_df = stacked_df[stacked_df["@@timedelta"] <= max_performance] + i1 = df.set_index(case_id_glue).index + i2 = stacked_df.set_index(case_id_glue).index + if positive: + ret = df[i1.isin(i2)] + else: + ret = df[~i1.isin(i2)] + + ret.attrs = copy(df.attrs) if hasattr(df, 'attrs') else {} + return ret + + @deprecation.deprecated("2.2.11", "3.0.0", details="Removed") def apply_auto_filter(df, parameters=None): del df diff --git a/pm4py/algo/filtering/pandas/rework/__init__.py b/pm4py/algo/filtering/pandas/rework/__init__.py new file mode 100644 index 000000000..3f0559c31 --- /dev/null +++ b/pm4py/algo/filtering/pandas/rework/__init__.py @@ -0,0 +1,17 @@ +''' + 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 pm4py.algo.filtering.pandas.rework import rework_filter diff --git a/pm4py/algo/filtering/pandas/rework/rework_filter.py b/pm4py/algo/filtering/pandas/rework/rework_filter.py new file mode 100644 index 000000000..b136d66da --- /dev/null +++ b/pm4py/algo/filtering/pandas/rework/rework_filter.py @@ -0,0 +1,82 @@ +''' + 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.util import constants, xes_constants, exec_utils +from typing import Optional, Dict, Any +import pandas as pd +from copy import copy + + +class Parameters(Enum): + ACTIVITY_KEY = constants.PARAMETER_CONSTANT_ACTIVITY_KEY + CASE_ID_KEY = constants.PARAMETER_CONSTANT_CASEID_KEY + MIN_OCCURRENCES = "min_occurrences" + POSITIVE = "positive" + + +INT_CASE_ACT_SIZE = "@@int_case_act_size" + + +def apply(df0: pd.DataFrame, activity: str, parameters: Optional[Dict[Any, Any]] = None) -> pd.DataFrame: + """ + Applies the rework filter on the provided dataframe and activity. + This filter the cases of the log having at least Parameters.MIN_OCCURRENCES (default: 2) occurrences + of the given activity. + + It is also possible (setting Parameters.POSITIVE to False) to retrieve the cases of the log not having the + given activity or having the activity occurred less than Parameters.MIN_OCCURRENCES times. + + Parameters + ------------------- + df0 + Dataframe + activity + Activity of which the rework shall be filtered + parameters + Parameters of the filter, including: + - Parameters.ACTIVITY_KEY => the attribute to use as activity + - Parameters.CASE_ID_KEY => the attribute to use as case ID + - Parameters.MIN_OCCURRENCES => the minimum number of occurrences for the activity + - Parameters.POSITIVE => if True, filters the cases of the log having at least MIN_OCCURRENCES occurrences. + if False, filters the cases of the log where such behavior does not occur. + + Returns + ----------------- + filtered_df + Filtered dataframe + """ + if parameters is None: + parameters = {} + + activity_key = exec_utils.get_param_value(Parameters.ACTIVITY_KEY, parameters, xes_constants.DEFAULT_NAME_KEY) + case_id_key = exec_utils.get_param_value(Parameters.CASE_ID_KEY, parameters, constants.CASE_CONCEPT_NAME) + min_occurrences = exec_utils.get_param_value(Parameters.MIN_OCCURRENCES, parameters, 2) + positive = exec_utils.get_param_value(Parameters.POSITIVE, parameters, True) + + df = df0.copy() + df = df[{activity_key, case_id_key}] + df = df[df[activity_key] == activity] + df[INT_CASE_ACT_SIZE] = df.groupby([activity_key, case_id_key]).cumcount() + cases = df[df[INT_CASE_ACT_SIZE] >= (min_occurrences-1)][case_id_key].unique() + + if positive: + ret = df0[df0[case_id_key].isin(cases)] + else: + ret = df0[~df0[case_id_key].isin(cases)] + + ret.attrs = copy(df0.attrs) if hasattr(df0, 'attrs') else {} + return ret diff --git a/pm4py/algo/simulation/playout/dfg/algorithm.py b/pm4py/algo/simulation/playout/dfg/algorithm.py index 6146e813d..e04212d52 100644 --- a/pm4py/algo/simulation/playout/dfg/algorithm.py +++ b/pm4py/algo/simulation/playout/dfg/algorithm.py @@ -14,7 +14,7 @@ You should have received a copy of the GNU General Public License along with PM4Py. If not, see . ''' -from pm4py.algo.simulation.playout.dfg.variants import classic +from pm4py.algo.simulation.playout.dfg.variants import classic, performance from enum import Enum from pm4py.util import exec_utils from typing import Optional, Dict, Any, Union, Tuple @@ -23,6 +23,7 @@ class Variants(Enum): CLASSIC = classic + PERFORMANCE = performance def apply(dfg: Dict[Tuple[str, str], int], start_activities: Dict[str, int], end_activities: Dict[str, int], variant=Variants.CLASSIC, parameters: Optional[Dict[Any, Any]] = None) -> Union[EventLog, Dict[Tuple[str, str], int]]: @@ -40,6 +41,7 @@ def apply(dfg: Dict[Tuple[str, str], int], start_activities: Dict[str, int], end variant Variant of the playout to be used, possible values: - Variants.CLASSIC + - Variants.PERFORMANCE parameters Parameters of the algorithm diff --git a/pm4py/algo/simulation/playout/dfg/variants/__init__.py b/pm4py/algo/simulation/playout/dfg/variants/__init__.py index 9d30f84c5..9c6a37105 100644 --- a/pm4py/algo/simulation/playout/dfg/variants/__init__.py +++ b/pm4py/algo/simulation/playout/dfg/variants/__init__.py @@ -14,4 +14,4 @@ You should have received a copy of the GNU General Public License along with PM4Py. If not, see . ''' -from pm4py.algo.simulation.playout.dfg.variants import classic +from pm4py.algo.simulation.playout.dfg.variants import classic, performance diff --git a/pm4py/algo/simulation/playout/dfg/variants/performance.py b/pm4py/algo/simulation/playout/dfg/variants/performance.py new file mode 100644 index 000000000..a9d732b1c --- /dev/null +++ b/pm4py/algo/simulation/playout/dfg/variants/performance.py @@ -0,0 +1,145 @@ +''' + 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 copy import copy +from datetime import datetime +from enum import Enum +from typing import Optional, Dict, Any, Tuple + +from numpy.random import choice, exponential + +from pm4py.objects.log.obj import EventLog, Trace, Event +from pm4py.util import exec_utils, constants, xes_constants + + +class Parameters(Enum): + NUM_TRACES = "num_traces" + ACTIVITY_KEY = constants.PARAMETER_CONSTANT_ACTIVITY_KEY + TIMESTAMP_KEY = constants.PARAMETER_CONSTANT_TIMESTAMP_KEY + CASE_ID_KEY = constants.PARAMETER_CONSTANT_CASEID_KEY + CASE_ARRIVAL_RATE = "case_arrival_rate" + PERFORMANCE_DFG = "performance_dfg" + + +def dict_based_choice(dct: Dict[str, float]) -> str: + """ + Performs a weighted choice, given a dictionary associating + a weight to each possible choice + + Parameters + ----------------- + dct + Dictionary associating a weight to each choice + + Returns + ----------------- + choice + Choice + """ + X = [] + Y = [] + summ = 0 + for x, y in dct.items(): + X.append(x) + Y.append(y) + summ += y + if summ > 0: + for i in range(len(Y)): + Y[i] = Y[i] / summ + return list(choice(X, 1, p=Y))[0] + + +def apply(frequency_dfg: Dict[Tuple[str, str], int], start_activities: Dict[str, int], end_activities: Dict[str, int], + parameters: Optional[Dict[Any, Any]] = None) -> EventLog: + """ + Simulates a log out with the transition probabilities provided by the frequency DFG, + and the time deltas provided by the performance DFG + + Parameters + --------------- + frequency_dfg + Frequency DFG + start_activities + Start activities + end_activities + End activities + parameters + Parameters of the algorithm, including: + - Parameters.NUM_TRACES: the number of traces of the simulated log + - Parameters.ACTIVITY_KEY: the activity key to be used in the simulated log + - Parameters.TIMESTAMP_KEY: the timestamp key to be used in the simulated log + - Parameters.CASE_ID_KEY: the case identifier key to be used in the simulated log + - Parameters.CASE_ARRIVAL_RATE: the average distance (in seconds) between the start of two cases (default: 1) + - Parameters.PERFORMANCE_DFG: (mandatory) the performance DFG that is used for the time deltas. + + Returns + --------------- + simulated_log + Simulated log + """ + if parameters is None: + parameters = {} + + num_traces = exec_utils.get_param_value(Parameters.NUM_TRACES, parameters, 1000) + activity_key = exec_utils.get_param_value(Parameters.ACTIVITY_KEY, parameters, xes_constants.DEFAULT_NAME_KEY) + timestamp_key = exec_utils.get_param_value(Parameters.TIMESTAMP_KEY, parameters, + xes_constants.DEFAULT_TIMESTAMP_KEY) + case_id_key = exec_utils.get_param_value(Parameters.CASE_ID_KEY, parameters, xes_constants.DEFAULT_TRACEID_KEY) + case_arrival_rate = exec_utils.get_param_value(Parameters.CASE_ARRIVAL_RATE, parameters, 1) + performance_dfg = copy(exec_utils.get_param_value(Parameters.PERFORMANCE_DFG, parameters, None)) + frequency_dfg = copy(frequency_dfg) + + for sa in start_activities: + frequency_dfg[("▶", sa)] = start_activities[sa] + performance_dfg[("▶", sa)] = 0 + + for ea in end_activities: + frequency_dfg[(ea, "■")] = end_activities[ea] + performance_dfg[(ea, "■")] = 0 + + choices = {} + for el in frequency_dfg: + if not el[0] in choices: + choices[el[0]] = {} + choices[el[0]][el[1]] = frequency_dfg[el] + + if performance_dfg is None: + raise Exception( + "performance DFG simulation requires the Parameters.PERFORMANCE_DFG ('performance_dfg') parameter specification.") + + log = EventLog() + curr_st = 10000000 + + for i in range(num_traces): + curr_st += case_arrival_rate + curr_t = curr_st + trace = Trace(attributes={case_id_key: str(i)}) + log.append(trace) + curr_act = "▶" + while True: + next_act = dict_based_choice(choices[curr_act]) + if next_act == "■" or next_act is None: + break + perf = performance_dfg[(curr_act, next_act)] + if type(perf) is dict: + perf = perf["mean"] + perf = 0 if perf == 0 else exponential(perf) + curr_t += perf + curr_act = next_act + eve = Event({activity_key: curr_act, timestamp_key: datetime.fromtimestamp(curr_t)}) + trace.append(eve) + + return log diff --git a/pm4py/algo/simulation/playout/petri_net/variants/basic_playout.py b/pm4py/algo/simulation/playout/petri_net/variants/basic_playout.py index 1a6e6c228..e4da65f78 100644 --- a/pm4py/algo/simulation/playout/petri_net/variants/basic_playout.py +++ b/pm4py/algo/simulation/playout/petri_net/variants/basic_playout.py @@ -18,18 +18,16 @@ from copy import copy from enum import Enum from random import choice +from typing import Optional, Dict, Any, Union +from pm4py.objects import petri_net from pm4py.objects.log import obj as log_instance -from pm4py.objects.petri_net import semantics -from pm4py.objects.petri_net.obj import PetriNet +from pm4py.objects.log.obj import EventLog +from pm4py.objects.petri_net.obj import PetriNet, Marking from pm4py.util import constants from pm4py.util import exec_utils from pm4py.util import xes_constants -from pm4py.objects.petri_net.obj import PetriNet, Marking -from typing import Optional, Dict, Any, Union, Tuple -from pm4py.objects.log.obj import EventLog, EventStream - class Parameters(Enum): ACTIVITY_KEY = constants.PARAMETER_CONSTANT_ACTIVITY_KEY @@ -38,12 +36,13 @@ class Parameters(Enum): RETURN_VISITED_ELEMENTS = "return_visited_elements" NO_TRACES = "noTraces" MAX_TRACE_LENGTH = "maxTraceLength" + PETRI_SEMANTICS = "petri_semantics" def apply_playout(net, initial_marking, no_traces=100, max_trace_length=100, case_id_key=xes_constants.DEFAULT_TRACEID_KEY, activity_key=xes_constants.DEFAULT_NAME_KEY, timestamp_key=xes_constants.DEFAULT_TIMESTAMP_KEY, - final_marking=None, return_visited_elements=False): + final_marking=None, return_visited_elements=False, semantics=petri_net.semantics.ClassicSemantics()): """ Do the playout of a Petrinet generating a log @@ -65,6 +64,8 @@ def apply_playout(net, initial_marking, no_traces=100, max_trace_length=100, Event attribute that corresponds to the timestamp final_marking If provided, the final marking of the Petri net + semantics + Semantics of the Petri net to be used (default: petri_net.semantics.ClassicSemantics()) """ # assigns to each event an increased timestamp from 1970 curr_timestamp = 10000000 @@ -117,7 +118,8 @@ def apply_playout(net, initial_marking, no_traces=100, max_trace_length=100, return log -def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None, parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> EventLog: +def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None, + parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> EventLog: """ Do the playout of a Petrinet generating a log @@ -133,6 +135,7 @@ def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None Parameters of the algorithm: Parameters.NO_TRACES -> Number of traces of the log to generate Parameters.MAX_TRACE_LENGTH -> Maximum trace length + Parameters.PETRI_SEMANTICS -> Petri net semantics to be used (default: petri_nets.semantics.ClassicSemantics()) """ if parameters is None: parameters = {} @@ -143,7 +146,9 @@ def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None no_traces = exec_utils.get_param_value(Parameters.NO_TRACES, parameters, 1000) max_trace_length = exec_utils.get_param_value(Parameters.MAX_TRACE_LENGTH, parameters, 1000) return_visited_elements = exec_utils.get_param_value(Parameters.RETURN_VISITED_ELEMENTS, parameters, False) + semantics = exec_utils.get_param_value(Parameters.PETRI_SEMANTICS, parameters, petri_net.semantics.ClassicSemantics()) return apply_playout(net, initial_marking, max_trace_length=max_trace_length, no_traces=no_traces, case_id_key=case_id_key, activity_key=activity_key, timestamp_key=timestamp_key, - final_marking=final_marking, return_visited_elements=return_visited_elements) + final_marking=final_marking, return_visited_elements=return_visited_elements, + semantics=semantics) diff --git a/pm4py/algo/simulation/playout/petri_net/variants/extensive.py b/pm4py/algo/simulation/playout/petri_net/variants/extensive.py index b1c67c468..b47494b20 100644 --- a/pm4py/algo/simulation/playout/petri_net/variants/extensive.py +++ b/pm4py/algo/simulation/playout/petri_net/variants/extensive.py @@ -15,20 +15,18 @@ along with PM4Py. If not, see . ''' import datetime +import sys from collections import Counter from enum import Enum +from typing import Optional, Dict, Any, Union +from pm4py.objects import petri_net from pm4py.objects.log import obj as log_instance -from pm4py.objects.petri_net import semantics -from pm4py.objects.petri_net.obj import PetriNet +from pm4py.objects.log.obj import EventLog +from pm4py.objects.petri_net.obj import PetriNet, Marking from pm4py.util import constants from pm4py.util import exec_utils from pm4py.util import xes_constants -import sys - -from pm4py.objects.petri_net.obj import PetriNet, Marking -from typing import Optional, Dict, Any, Union, Tuple -from pm4py.objects.log.obj import EventLog, EventStream class Parameters(Enum): @@ -38,6 +36,7 @@ class Parameters(Enum): MAX_TRACE_LENGTH = "maxTraceLength" RETURN_ELEMENTS = "return_elements" MAX_MARKING_OCC = "max_marking_occ" + PETRI_SEMANTICS = "petri_semantics" POSITION_MARKING = 0 @@ -45,7 +44,8 @@ class Parameters(Enum): POSITION_ELEMENTS = 2 -def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None, parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> EventLog: +def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None, + parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> EventLog: """ Do the playout of a Petrinet generating a log (extensive search; stop at the maximum trace length specified @@ -61,6 +61,7 @@ def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None parameters Parameters of the algorithm: Parameters.MAX_TRACE_LENGTH -> Maximum trace length + Parameters.PETRI_SEMANTICS -> Petri net semantics """ if parameters is None: parameters = {} @@ -72,6 +73,7 @@ def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None max_trace_length = exec_utils.get_param_value(Parameters.MAX_TRACE_LENGTH, parameters, 10) return_elements = exec_utils.get_param_value(Parameters.RETURN_ELEMENTS, parameters, False) max_marking_occ = exec_utils.get_param_value(Parameters.MAX_MARKING_OCC, parameters, sys.maxsize) + semantics = exec_utils.get_param_value(Parameters.PETRI_SEMANTICS, parameters, petri_net.semantics.ClassicSemantics()) # assigns to each event an increased timestamp from 1970 curr_timestamp = 10000000 @@ -107,7 +109,7 @@ def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None if counter_elements[m] > max_marking_occ: continue - new_m = semantics.weak_execute(t, m) + new_m = semantics.weak_execute(t, net, m) if t.label is not None: new_trace = trace + (t.label,) else: diff --git a/pm4py/algo/simulation/playout/petri_net/variants/stochastic_playout.py b/pm4py/algo/simulation/playout/petri_net/variants/stochastic_playout.py index 7ffd49a9d..9e7f84858 100644 --- a/pm4py/algo/simulation/playout/petri_net/variants/stochastic_playout.py +++ b/pm4py/algo/simulation/playout/petri_net/variants/stochastic_playout.py @@ -17,21 +17,19 @@ import datetime from copy import copy from enum import Enum +from typing import Optional, Dict, Any, Union +from pm4py.algo.simulation.montecarlo.utils import replay +from pm4py.objects import petri_net from pm4py.objects.log import obj as log_instance -from pm4py.objects.petri_net import semantics +from pm4py.objects.log.obj import EventLog +from pm4py.objects.petri_net.obj import PetriNet, Marking from pm4py.objects.petri_net.utils import final_marking as final_marking_discovery -from pm4py.objects.petri_net.obj import PetriNet from pm4py.objects.stochastic_petri import utils as stochastic_utils -from pm4py.algo.simulation.montecarlo.utils import replay from pm4py.util import constants from pm4py.util import exec_utils from pm4py.util import xes_constants -from pm4py.objects.petri_net.obj import PetriNet, Marking -from typing import Optional, Dict, Any, Union, Tuple -from pm4py.objects.log.obj import EventLog, EventStream - class Parameters(Enum): ACTIVITY_KEY = constants.PARAMETER_CONSTANT_ACTIVITY_KEY @@ -42,12 +40,14 @@ class Parameters(Enum): MAX_TRACE_LENGTH = "maxTraceLength" LOG = "log" STOCHASTIC_MAP = "stochastic_map" + PETRI_SEMANTICS = "petri_semantics" def apply_playout(net, initial_marking, no_traces=100, max_trace_length=100, case_id_key=xes_constants.DEFAULT_TRACEID_KEY, activity_key=xes_constants.DEFAULT_NAME_KEY, timestamp_key=xes_constants.DEFAULT_TIMESTAMP_KEY, - final_marking=None, smap=None, log=None, return_visited_elements=False): + final_marking=None, smap=None, log=None, return_visited_elements=False, + semantics=petri_net.semantics.ClassicSemantics()): """ Do the playout of a Petrinet generating a log @@ -73,6 +73,8 @@ def apply_playout(net, initial_marking, no_traces=100, max_trace_length=100, Stochastic map log Log + semantics + Semantics of the Petri net to be used (default: petri_net.semantics.ClassicSemantics()) """ if final_marking is None: # infer the final marking from the net @@ -137,7 +139,8 @@ def apply_playout(net, initial_marking, no_traces=100, max_trace_length=100, return log -def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None, parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> EventLog: +def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None, + parameters: Optional[Dict[Union[str, Parameters], Any]] = None) -> EventLog: """ Do the playout of a Petrinet generating a log @@ -153,6 +156,7 @@ def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None Parameters of the algorithm: Parameters.NO_TRACES -> Number of traces of the log to generate Parameters.MAX_TRACE_LENGTH -> Maximum trace length + Parameters.PETRI_SEMANTICS -> Petri net semantics to be used (default: petri_nets.semantics.ClassicSemantics()) """ if parameters is None: parameters = {} @@ -165,7 +169,10 @@ def apply(net: PetriNet, initial_marking: Marking, final_marking: Marking = None smap = exec_utils.get_param_value(Parameters.STOCHASTIC_MAP, parameters, None) log = exec_utils.get_param_value(Parameters.LOG, parameters, None) return_visited_elements = exec_utils.get_param_value(Parameters.RETURN_VISITED_ELEMENTS, parameters, False) + semantics = exec_utils.get_param_value(Parameters.PETRI_SEMANTICS, parameters, petri_net.semantics.ClassicSemantics()) return apply_playout(net, initial_marking, max_trace_length=max_trace_length, no_traces=no_traces, case_id_key=case_id_key, activity_key=activity_key, timestamp_key=timestamp_key, - final_marking=final_marking, smap=smap, log=log, return_visited_elements=return_visited_elements) + final_marking=final_marking, smap=smap, log=log, + return_visited_elements=return_visited_elements, + semantics=semantics) diff --git a/pm4py/meta.py b/pm4py/meta.py index cdb893198..d1e70bce7 100644 --- a/pm4py/meta.py +++ b/pm4py/meta.py @@ -14,7 +14,7 @@ You should have received a copy of the GNU General Public License along with PM4Py. If not, see . ''' -VERSION = '2.2.12' +VERSION = '2.2.13' __name__ = 'pm4py' __version__ = VERSION diff --git a/pm4py/objects/log/exporter/xes/variants/etree_xes_exp.py b/pm4py/objects/log/exporter/xes/variants/etree_xes_exp.py index 029f9bb59..04fdf32a9 100644 --- a/pm4py/objects/log/exporter/xes/variants/etree_xes_exp.py +++ b/pm4py/objects/log/exporter/xes/variants/etree_xes_exp.py @@ -290,6 +290,8 @@ def export_log_tree(log, parameters=None): log = log_converter.apply(log) root = etree.Element(xes_util.TAG_LOG) root.set(xes_util.TAG_VERSION, xes_util.VALUE_XES_VERSION) + root.set(xes_util.TAG_FEATURES, xes_util.VALUE_XES_FEATURES) + root.set(xes_util.TAG_XMLNS, xes_util.VALUE_XMLNS) # add attributes at the log level __export_attributes(log, root) diff --git a/pm4py/objects/log/exporter/xes/variants/line_by_line.py b/pm4py/objects/log/exporter/xes/variants/line_by_line.py index 873d5234a..3d820b906 100644 --- a/pm4py/objects/log/exporter/xes/variants/line_by_line.py +++ b/pm4py/objects/log/exporter/xes/variants/line_by_line.py @@ -192,7 +192,7 @@ def export_log_line_by_line(log, fp_obj, encoding, parameters=None): fp_obj.write(("\n").encode( encoding)) - fp_obj.write(("\n").encode(encoding)) + fp_obj.write(("\n").encode(encoding)) for ext_name, ext_value in log.extensions.items(): fp_obj.write((get_tab_indent(1) + "\n" % ( ext_name, ext_value[xes_util.KEY_PREFIX], ext_value[xes_util.KEY_URI])).encode(encoding)) diff --git a/pm4py/objects/log/util/xes.py b/pm4py/objects/log/util/xes.py index 913bd8672..536ea4f28 100644 --- a/pm4py/objects/log/util/xes.py +++ b/pm4py/objects/log/util/xes.py @@ -30,6 +30,8 @@ TAG_TRACE = 'trace' TAG_VALUES = 'values' TAG_VERSION = 'xes.version' +TAG_FEATURES = 'xes.features' +TAG_XMLNS = 'xmlns' # XES/INTERNAL KEYS KEY_CHILDREN = 'children' @@ -49,4 +51,5 @@ DEFAULT_TRANSITION_KEY = "lifecycle:transition" VALUE_XES_VERSION = '1849-2016' - +VALUE_XES_FEATURES = "nested-attributes" +VALUE_XMLNS = 'http://www.xes-standard.org/' diff --git a/pm4py/objects/petri_net/data_petri_nets/semantics.py b/pm4py/objects/petri_net/data_petri_nets/semantics.py index ca21ee855..d1c752ed7 100644 --- a/pm4py/objects/petri_net/data_petri_nets/semantics.py +++ b/pm4py/objects/petri_net/data_petri_nets/semantics.py @@ -16,6 +16,81 @@ ''' import copy from pm4py.objects.petri_net import properties as petri_properties +from pm4py.objects.petri_net.sem_interface import Semantics + + +class DataPetriNetSemantics(Semantics): + def is_enabled(self, t, pn, m, **kwargs): + """ + Verifies whether a given transition is enabled in a given Petri net and marking + + Parameters + ---------- + :param t: transition to check + :param pn: Petri net + :param m: marking to check + :param e: associated event (optional, as keyword argument) + + Returns + ------- + :return: true if enabled, false otherwise + """ + e = kwargs["e"] if "e" in kwargs else {} + return is_enabled(t, pn, m, e) + + def execute(self, t, pn, m, **kwargs): + """ + Executes a given transition in a given Petri net, the given data marking and the associated event + + Parameters + ---------- + :param t: transition to execute + :param pn: Petri net + :param m: marking to use + :param e: associated event (optional, as keyword argument) + + Returns + ------- + :return: newly reached marking if :param t: is enabled, None otherwise + """ + e = kwargs["e"] if "e" in kwargs else {} + return execute(t, pn, m, e) + + def weak_execute(self, t, pn, m, **kwargs): + """ + Executes a given transition in a given Petri net, the given data marking and the associated event, + even if not fully enabled + + Parameters + ---------- + :param t: transition to execute + :param pn: Petri net + :param m: marking to use + :param e: associated event (optional, as keyword argument) + + Returns + ------- + :return: newly reached marking + """ + e = kwargs["e"] if "e" in kwargs else {} + return weak_execute(t, m, e) + + def enabled_transitions(self, pn, m, **kwargs): + """ + Returns a set of enabled transitions in a Petri net, the given data marking and the associated event + + Parameters + ---------- + :param pn: Petri net + :param m: marking of the pn + :param e: associated event (optional, as keyword argument) + + Returns + ------- + :return: set of enabled transitions + """ + e = kwargs["e"] if "e" in kwargs else {} + return enabled_transitions(pn, m, e) def evaluate_guard(guard, read_variables, data): @@ -46,22 +121,12 @@ def evaluate_guard(guard, read_variables, data): return False -def is_enabled(t, pn, m, e): - """ - Verifies whether a given transition is enabled in a given Petri net and marking - - Parameters - ---------- - :param t: transition to check - :param pn: Petri net - :param m: marking to check - :param e: associated event - - Returns - ------- - :return: true if enabled, false otherwise - """ +# 29/08/2021: the following methods have been incapsulated in the DataPetriNetSemantics class. +# the long term idea is to remove them. However, first we need to adapt the existing code to the new +# structure. Moreover, for performance reason, it is better to leave the code here, without having +# to instantiate a DataPetriNetSemantics object. +def is_enabled(t, pn, m, e): if t not in pn.transitions: return False else: @@ -80,21 +145,6 @@ def is_enabled(t, pn, m, e): def execute(t, pn, m, e): - """ - Executes a given transition in a given Petri net, the given data marking and the associated event - - Parameters - ---------- - :param t: transition to execute - :param pn: Petri net - :param m: marking to use - :param e: associated event - - Returns - ------- - :return: newly reached marking if :param t: is enabled, None otherwise - """ - if not is_enabled(t, pn, m, e): return None @@ -112,20 +162,21 @@ def execute(t, pn, m, e): return m_out -def enabled_transitions(pn, m, e): - """ - Returns a set of enabled transitions in a Petri net, the given data marking and the associated event +def weak_execute(t, m, e): + m_out = copy.copy(m) + for a in t.in_arcs: + m_out[a.source] -= a.weight + if m_out[a.source] <= 0: + del m_out[a.source] + for a in t.out_arcs: + m_out[a.target] += a.weight - Parameters - ---------- - :param pn: Petri net - :param m: marking of the pn - :param e: associated event + m_out.data_dict.update(e) - Returns - ------- - :return: set of enabled transitions - """ + return m_out + + +def enabled_transitions(pn, m, e): enabled = set() for t in pn.transitions: if is_enabled(t, pn, m, e): diff --git a/pm4py/objects/petri_net/inhibitor_reset/semantics.py b/pm4py/objects/petri_net/inhibitor_reset/semantics.py index c82e4f40d..3485a093c 100644 --- a/pm4py/objects/petri_net/inhibitor_reset/semantics.py +++ b/pm4py/objects/petri_net/inhibitor_reset/semantics.py @@ -16,23 +16,79 @@ ''' import copy from pm4py.objects.petri_net import properties +from pm4py.objects.petri_net.sem_interface import Semantics + + +class InhibitorResetSemantics(Semantics): + def is_enabled(self, t, pn, m, **kwargs): + """ + Verifies whether a given transition is enabled in a given Petri net and marking + + Parameters + ---------- + :param t: transition to check + :param pn: Petri net + :param m: marking to check + + Returns + ------- + :return: true if enabled, false otherwise + """ + return is_enabled(t, pn, m) + + def execute(self, t, pn, m, **kwargs): + """ + Executes a given transition in a given Petri net and Marking + + Parameters + ---------- + :param t: transition to execute + :param pn: Petri net + :param m: marking to use + + Returns + ------- + :return: newly reached marking if :param t: is enabled, None otherwise + """ + return execute(t, pn, m) - + def weak_execute(self, t, pn, m, **kwargs): + """ + Execute a transition even if it is not fully enabled + + Parameters + ---------- + :param t: transition to execute + :param pn: Petri net + :param m: marking to use + + Returns + ------- + :return: newly reached marking if :param t: is enabled, None otherwise + """ + return weak_execute(t, m) + + def enabled_transitions(self, pn, m, **kwargs): + """ + Returns a set of enabled transitions in a Petri net and given marking + + Parameters + ---------- + :param pn: Petri net + :param m: marking of the pn + + Returns + ------- + :return: set of enabled transitions + """ + return enabled_transitions(pn, m) + + +# 29/08/2021: the following methods have been incapsulated in the InhibitorResetSemantics class. +# the long term idea is to remove them. However, first we need to adapt the existing code to the new +# structure. Moreover, for performance reason, it is better to leave the code here, without having +# to instantiate a InhibitorResetSemantics object. def is_enabled(t, pn, m): - """ - Verifies whether a given transition is enabled in a given Petri net and marking - - Parameters - ---------- - :param t: transition to check - :param pn: Petri net - :param m: marking to check - - Returns - ------- - :return: true if enabled, false otherwise - """ - if t not in pn.transitions: return False else: @@ -48,20 +104,6 @@ def is_enabled(t, pn, m): def execute(t, pn, m): - """ - Executes a given transition in a given Petri net and Marking - - Parameters - ---------- - :param t: transition to execute - :param pn: Petri net - :param m: marking to use - - Returns - ------- - :return: newly reached marking if :param t: is enabled, None otherwise - """ - if not is_enabled(t, pn, m): return None @@ -83,27 +125,7 @@ def execute(t, pn, m): return m_out -def try_to_execute(t, pn, m): - if not is_enabled(t, pn, m): - return None - else: - return execute(t, pn, m) - - def weak_execute(t, m): - """ - Execute a transition even if it is not fully enabled - - Parameters - ---------- - :param t: transition to execute - :param m: marking to use - - Returns - ------- - :return: newly reached marking if :param t: is enabled, None otherwise - """ - m_out = copy.copy(m) for a in t.in_arcs: if properties.ARCTYPE in a.properties and a.properties[properties.ARCTYPE] == properties.RESET_ARC: @@ -121,18 +143,6 @@ def weak_execute(t, m): def enabled_transitions(pn, m): - """ - Returns a set of enabled transitions in a Petri net and given marking - - Parameters - ---------- - :param pn: Petri net - :param m: marking of the pn - - Returns - ------- - :return: set of enabled transitions - """ enabled = set() for t in pn.transitions: if is_enabled(t, pn, m): diff --git a/pm4py/objects/petri_net/sem_interface.py b/pm4py/objects/petri_net/sem_interface.py new file mode 100644 index 000000000..ceef621e4 --- /dev/null +++ b/pm4py/objects/petri_net/sem_interface.py @@ -0,0 +1,28 @@ +''' + 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 . +''' +class Semantics(object): + def is_enabled(self, t, pn, m, **kwargs): + pass + + def execute(self, t, pn, m, **kwargs): + pass + + def weak_execute(self, t, pn, m, **kwargs): + pass + + def enabled_transitions(self, pn, m, **kwargs): + pass diff --git a/pm4py/objects/petri_net/semantics.py b/pm4py/objects/petri_net/semantics.py index ea9416ab7..cee125cb6 100644 --- a/pm4py/objects/petri_net/semantics.py +++ b/pm4py/objects/petri_net/semantics.py @@ -15,23 +15,80 @@ along with PM4Py. If not, see . ''' import copy +import deprecation +from pm4py.objects.petri_net.sem_interface import Semantics + + +class ClassicSemantics(Semantics): + def is_enabled(self, t, pn, m, **kwargs): + """ + Verifies whether a given transition is enabled in a given Petri net and marking + + Parameters + ---------- + :param t: transition to check + :param pn: Petri net + :param m: marking to check + + Returns + ------- + :return: true if enabled, false otherwise + """ + return is_enabled(t, pn, m) + + def execute(self, t, pn, m, **kwargs): + """ + Executes a given transition in a given Petri net and Marking + + Parameters + ---------- + :param t: transition to execute + :param pn: Petri net + :param m: marking to use + + Returns + ------- + :return: newly reached marking if :param t: is enabled, None otherwise + """ + return execute(t, pn, m) - + def weak_execute(self, t, pn, m, **kwargs): + """ + Execute a transition even if it is not fully enabled + + Parameters + ---------- + :param t: transition to execute + :param pn: Petri net + :param m: marking to use + + Returns + ------- + :return: newly reached marking if :param t: is enabled, None otherwise + """ + return weak_execute(t, m) + + def enabled_transitions(self, pn, m, **kwargs): + """ + Returns a set of enabled transitions in a Petri net and given marking + + Parameters + ---------- + :param pn: Petri net + :param m: marking of the pn + + Returns + ------- + :return: set of enabled transitions + """ + return enabled_transitions(pn, m) + + +# 29/08/2021: the following methods have been incapsulated in the ClassicSemantics class. +# the long term idea is to remove them. However, first we need to adapt the existing code to the new +# structure. Moreover, for performance reason, it is better to leave the code here, without having +# to instantiate a ClassicSemantics object. def is_enabled(t, pn, m): - """ - Verifies whether a given transition is enabled in a given Petri net and marking - - Parameters - ---------- - :param t: transition to check - :param pn: Petri net - :param m: marking to check - - Returns - ------- - :return: true if enabled, false otherwise - """ - if t not in pn.transitions: return False else: @@ -42,20 +99,6 @@ def is_enabled(t, pn, m): def execute(t, pn, m): - """ - Executes a given transition in a given Petri net and Marking - - Parameters - ---------- - :param t: transition to execute - :param pn: Petri net - :param m: marking to use - - Returns - ------- - :return: newly reached marking if :param t: is enabled, None otherwise - """ - if not is_enabled(t, pn, m): return None @@ -71,6 +114,7 @@ def execute(t, pn, m): return m_out +@deprecation.deprecated("2.2.13", "3.0.0", details="please use the execute method.") def try_to_execute(t, pn, m): if not is_enabled(t, pn, m): return None @@ -79,19 +123,6 @@ def try_to_execute(t, pn, m): def weak_execute(t, m): - """ - Execute a transition even if it is not fully enabled - - Parameters - ---------- - :param t: transition to execute - :param m: marking to use - - Returns - ------- - :return: newly reached marking if :param t: is enabled, None otherwise - """ - m_out = copy.copy(m) for a in t.in_arcs: m_out[a.source] -= a.weight @@ -103,18 +134,6 @@ def weak_execute(t, m): def enabled_transitions(pn, m): - """ - Returns a set of enabled transitions in a Petri net and given marking - - Parameters - ---------- - :param pn: Petri net - :param m: marking of the pn - - Returns - ------- - :return: set of enabled transitions - """ enabled = set() for t in pn.transitions: if is_enabled(t, pn, m): diff --git a/pm4py/streaming/__init__.py b/pm4py/streaming/__init__.py index 36af06ddc..7e4ef3208 100644 --- a/pm4py/streaming/__init__.py +++ b/pm4py/streaming/__init__.py @@ -14,4 +14,4 @@ You should have received a copy of the GNU General Public License along with PM4Py. If not, see . ''' -from pm4py.streaming import algo, stream, importer, util +from pm4py.streaming import algo, stream, importer, util, conversion diff --git a/pm4py/streaming/conversion/__init__.py b/pm4py/streaming/conversion/__init__.py new file mode 100644 index 000000000..4d998a1df --- /dev/null +++ b/pm4py/streaming/conversion/__init__.py @@ -0,0 +1,17 @@ +''' + 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 pm4py.streaming.conversion import from_pandas diff --git a/pm4py/streaming/conversion/from_pandas.py b/pm4py/streaming/conversion/from_pandas.py new file mode 100644 index 000000000..ecb911ec2 --- /dev/null +++ b/pm4py/streaming/conversion/from_pandas.py @@ -0,0 +1,125 @@ +''' + 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 typing import Optional, Dict, Any + +import numpy as np +import pandas as pd + +from pm4py.objects.log.obj import Trace, Event +from pm4py.streaming.stream.live_trace_stream import LiveTraceStream +from pm4py.util import constants, xes_constants, exec_utils, pandas_utils + + +class Parameters(Enum): + CASE_ID_KEY = constants.PARAMETER_CONSTANT_CASEID_KEY + ACTIVITY_KEY = constants.PARAMETER_CONSTANT_ACTIVITY_KEY + TIMESTAMP_KEY = constants.PARAMETER_CONSTANT_TIMESTAMP_KEY + INDEX_KEY = "index_key" + + +class PandasDataframeAsIterable(object): + def __init__(self, dataframe: pd.DataFrame, parameters: Optional[Dict[Any, Any]] = None): + if parameters is None: + parameters = {} + + case_id_key = exec_utils.get_param_value(Parameters.CASE_ID_KEY, parameters, constants.CASE_CONCEPT_NAME) + activity_key = exec_utils.get_param_value(Parameters.ACTIVITY_KEY, parameters, xes_constants.DEFAULT_NAME_KEY) + timestamp_key = exec_utils.get_param_value(Parameters.TIMESTAMP_KEY, parameters, + xes_constants.DEFAULT_TIMESTAMP_KEY) + index_key = exec_utils.get_param_value(Parameters.INDEX_KEY, parameters, constants.DEFAULT_INDEX_KEY) + + if not (hasattr(dataframe, "attrs") and dataframe.attrs): + # dataframe has not been initialized through format_dataframe + dataframe = pandas_utils.insert_index(dataframe, index_key) + dataframe.sort_values([case_id_key, timestamp_key, index_key]) + + cases = dataframe[case_id_key].to_numpy() + + self.activities = dataframe[activity_key].to_numpy() + self.timestamps = dataframe[timestamp_key].to_numpy() + self.c_unq, self.c_ind, self.c_counts = np.unique(cases, return_index=True, return_counts=True) + self.no_traces = len(self.c_ind) + self.i = 0 + + def read_trace(self) -> Trace: + if self.i < self.no_traces: + case_id = self.c_unq[self.i] + si = self.c_ind[self.i] + ei = si + self.c_counts[self.i] + trace = Trace(attributes={xes_constants.DEFAULT_TRACEID_KEY: case_id}) + for j in range(si, ei): + event = Event({xes_constants.DEFAULT_NAME_KEY: self.activities[j], + xes_constants.DEFAULT_TIMESTAMP_KEY: self.timestamps[j]}) + trace.append(event) + self.i = self.i + 1 + return trace + + def reset(self): + self.i = 0 + + def __iter__(self): + """ + Starts the iteration + """ + return self + + def __next__(self): + """ + Gets the next trace + """ + trace = self.read_trace() + if trace is None: + raise StopIteration + return trace + + def to_trace_stream(self, trace_stream: LiveTraceStream): + """ + Sends the content of the dataframe to a trace stream + + Parameters + -------------- + trace_stream + Trace stream + """ + trace = self.read_trace() + while trace is not None: + trace_stream.append(trace) + trace = self.read_trace() + + +def apply(dataframe, parameters=None) -> PandasDataframeAsIterable: + """ + Transforms the Pandas dataframe object to an iterable + + Parameters + ---------------- + dataframe + Pandas dataframe + parameters + Parameters of the algorithm, including: + - Parameters.CASE_ID_KEY => the attribute to be used as case identifier (default: constants.CASE_CONCEPT_NAME) + - Parameters.ACTIVITY_KEY => the attribute to be used as activity (default: xes_constants.DEFAULT_NAME_KEY) + - Parameters.TIMESTAMP_KEY => the attribute to be used as timestamp (default: xes_constants.DEFAULT_TIMESTAMP_KEY) + + Returns + ---------------- + log_iterable + Iterable log object, which can be iterated directly or added to a live trace stream + (using the method to_trace_stream). + """ + return PandasDataframeAsIterable(dataframe, parameters=parameters) diff --git a/pm4py/visualization/bpmn/variants/classic.py b/pm4py/visualization/bpmn/variants/classic.py index 9378a9c64..61a319bdd 100644 --- a/pm4py/visualization/bpmn/variants/classic.py +++ b/pm4py/visualization/bpmn/variants/classic.py @@ -28,6 +28,7 @@ class Parameters(Enum): FORMAT = "format" RANKDIR = "rankdir" FONT_SIZE = "font_size" + BGCOLOR = "bgcolor" def apply(bpmn_graph: BPMN, parameters: Optional[Dict[Any, Any]] = None) -> graphviz.Digraph: @@ -58,9 +59,10 @@ def apply(bpmn_graph: BPMN, parameters: Optional[Dict[Any, Any]] = None) -> grap rankdir = exec_utils.get_param_value(Parameters.RANKDIR, parameters, "LR") font_size = exec_utils.get_param_value(Parameters.FONT_SIZE, parameters, 12) font_size = str(font_size) + bgcolor = exec_utils.get_param_value(Parameters.BGCOLOR, parameters, "transparent") filename = tempfile.NamedTemporaryFile(suffix='.gv') - viz = Digraph("", filename=filename.name, engine='dot', graph_attr={'bgcolor': 'transparent'}) + viz = Digraph("", filename=filename.name, engine='dot', graph_attr={'bgcolor': bgcolor}) viz.graph_attr['rankdir'] = rankdir nodes, edges = get_sorted_nodes_edges(bpmn_graph) diff --git a/pm4py/visualization/dfg/variants/frequency.py b/pm4py/visualization/dfg/variants/frequency.py index bc098b1f8..5e5f27315 100644 --- a/pm4py/visualization/dfg/variants/frequency.py +++ b/pm4py/visualization/dfg/variants/frequency.py @@ -43,6 +43,7 @@ class Parameters(Enum): TIMESTAMP_KEY = constants.PARAMETER_CONSTANT_TIMESTAMP_KEY START_TIMESTAMP_KEY = constants.PARAMETER_CONSTANT_START_TIMESTAMP_KEY FONT_SIZE = "font_size" + BGCOLOR = "bgcolor" def get_min_max_value(dfg): @@ -132,7 +133,7 @@ def get_activities_color(activities_count): def graphviz_visualization(activities_count, dfg, image_format="png", measure="frequency", max_no_of_edges_in_diagram=100000, start_activities=None, end_activities=None, soj_time=None, - font_size="12"): + font_size="12", bgcolor="transparent"): """ Do GraphViz visualization of a DFG graph @@ -166,7 +167,7 @@ def graphviz_visualization(activities_count, dfg, image_format="png", measure="f end_activities = {} filename = tempfile.NamedTemporaryFile(suffix='.gv') - viz = Digraph("", filename=filename.name, engine='dot', graph_attr={'bgcolor': 'transparent'}) + viz = Digraph("", filename=filename.name, engine='dot', graph_attr={'bgcolor': bgcolor}) # first, remove edges in diagram that exceeds the maximum number of edges in the diagram dfg_key_value_list = [] @@ -280,6 +281,7 @@ def apply(dfg: Dict[Tuple[str, str], int], log: EventLog = None, parameters: Opt font_size = exec_utils.get_param_value(Parameters.FONT_SIZE, parameters, 12) font_size = str(font_size) activities = dfg_utils.get_activities_from_dfg(dfg) + bgcolor = exec_utils.get_param_value(Parameters.BGCOLOR, parameters, "transparent") if activities_count is None: if log is not None: @@ -296,4 +298,4 @@ def apply(dfg: Dict[Tuple[str, str], int], log: EventLog = None, parameters: Opt return graphviz_visualization(activities_count, dfg, image_format=image_format, measure="frequency", max_no_of_edges_in_diagram=max_no_of_edges_in_diagram, start_activities=start_activities, end_activities=end_activities, soj_time=soj_time, - font_size=font_size) + font_size=font_size, bgcolor=bgcolor) diff --git a/pm4py/visualization/dfg/variants/performance.py b/pm4py/visualization/dfg/variants/performance.py index 6e56ae18a..02ffbf380 100644 --- a/pm4py/visualization/dfg/variants/performance.py +++ b/pm4py/visualization/dfg/variants/performance.py @@ -45,6 +45,7 @@ class Parameters(Enum): START_TIMESTAMP_KEY = constants.PARAMETER_CONSTANT_START_TIMESTAMP_KEY FONT_SIZE = "font_size" AGGREGATION_MEASURE = "aggregation_measure" + BGCOLOR = "bgcolor" def get_min_max_value(dfg): @@ -132,7 +133,7 @@ def get_activities_color_soj_time(soj_time): def graphviz_visualization(activities_count, dfg, image_format="png", measure="frequency", max_no_of_edges_in_diagram=100000, start_activities=None, end_activities=None, soj_time=None, - font_size="12"): + font_size="12", bgcolor="transparent"): """ Do GraphViz visualization of a DFG graph @@ -166,7 +167,7 @@ def graphviz_visualization(activities_count, dfg, image_format="png", measure="f end_activities = [] filename = tempfile.NamedTemporaryFile(suffix='.gv') - viz = Digraph("", filename=filename.name, engine='dot', graph_attr={'bgcolor': 'transparent'}) + viz = Digraph("", filename=filename.name, engine='dot', graph_attr={'bgcolor': bgcolor}) # first, remove edges in diagram that exceeds the maximum number of edges in the diagram dfg_key_value_list = [] @@ -281,6 +282,7 @@ def apply(dfg: Dict[Tuple[str, str], int], log: EventLog = None, parameters: Opt font_size = str(font_size) activities = dfg_utils.get_activities_from_dfg(dfg) aggregation_measure = exec_utils.get_param_value(Parameters.AGGREGATION_MEASURE, parameters, "mean") + bgcolor = exec_utils.get_param_value(Parameters.BGCOLOR, parameters, "transparent") if activities_count is None: if log is not None: @@ -310,4 +312,4 @@ def apply(dfg: Dict[Tuple[str, str], int], log: EventLog = None, parameters: Opt return graphviz_visualization(activities_count, dfg, image_format=image_format, measure="performance", max_no_of_edges_in_diagram=max_no_of_edges_in_diagram, start_activities=start_activities, end_activities=end_activities, soj_time=soj_time, - font_size=font_size) + font_size=font_size, bgcolor=bgcolor) diff --git a/pm4py/visualization/petri_net/common/visualize.py b/pm4py/visualization/petri_net/common/visualize.py index 03630a772..fec4b892d 100644 --- a/pm4py/visualization/petri_net/common/visualize.py +++ b/pm4py/visualization/petri_net/common/visualize.py @@ -33,6 +33,7 @@ class Parameters(Enum): TIMESTAMP_KEY = PARAMETER_CONSTANT_TIMESTAMP_KEY AGGREGATION_MEASURE = "aggregationMeasure" FONT_SIZE = "font_size" + BGCOLOR = "bgcolor" def apply(net, initial_marking, final_marking, decorations=None, parameters=None): @@ -65,14 +66,15 @@ def apply(net, initial_marking, final_marking, decorations=None, parameters=None debug = exec_utils.get_param_value(Parameters.DEBUG, parameters, False) set_rankdir = exec_utils.get_param_value(Parameters.RANKDIR, parameters, None) font_size = exec_utils.get_param_value(Parameters.FONT_SIZE, parameters, "12") + bgcolor = exec_utils.get_param_value(Parameters.BGCOLOR, parameters, "transparent") return graphviz_visualization(net, image_format=image_format, initial_marking=initial_marking, final_marking=final_marking, decorations=decorations, debug=debug, - set_rankdir=set_rankdir, font_size=font_size) + set_rankdir=set_rankdir, font_size=font_size, bgcolor=bgcolor) def graphviz_visualization(net, image_format="png", initial_marking=None, final_marking=None, decorations=None, - debug=False, set_rankdir=None, font_size="12"): + debug=False, set_rankdir=None, font_size="12", bgcolor="transparent"): """ Provides visualization for the petrinet @@ -108,7 +110,7 @@ def graphviz_visualization(net, image_format="png", initial_marking=None, final_ font_size = str(font_size) filename = tempfile.NamedTemporaryFile(suffix='.gv') - viz = Digraph(net.name, filename=filename.name, engine='dot', graph_attr={'bgcolor': 'transparent'}) + viz = Digraph(net.name, filename=filename.name, engine='dot', graph_attr={'bgcolor': bgcolor}) if set_rankdir: viz.graph_attr['rankdir'] = set_rankdir else: diff --git a/pm4py/visualization/petrinet/common/visualize.py b/pm4py/visualization/petrinet/common/visualize.py index e4d65577c..c2a1f77b9 100644 --- a/pm4py/visualization/petrinet/common/visualize.py +++ b/pm4py/visualization/petrinet/common/visualize.py @@ -64,7 +64,7 @@ def apply(net, initial_marking, final_marking, decorations=None, parameters=None def graphviz_visualization(net, image_format="png", initial_marking=None, final_marking=None, decorations=None, - debug=False, set_rankdir=None, font_size="12"): + debug=False, set_rankdir=None, font_size="12", bgcolor="transparent"): """ Provides visualization for the petrinet @@ -100,7 +100,7 @@ def graphviz_visualization(net, image_format="png", initial_marking=None, final_ font_size = str(font_size) filename = tempfile.NamedTemporaryFile(suffix='.gv') - viz = Digraph(net.name, filename=filename.name, engine='dot', graph_attr={'bgcolor': 'transparent'}) + viz = Digraph(net.name, filename=filename.name, engine='dot', graph_attr={'bgcolor': bgcolor}) if set_rankdir: viz.graph_attr['rankdir'] = set_rankdir else: diff --git a/pm4py/visualization/process_tree/variants/symbolic.py b/pm4py/visualization/process_tree/variants/symbolic.py index 99b37f1cc..35caca19d 100644 --- a/pm4py/visualization/process_tree/variants/symbolic.py +++ b/pm4py/visualization/process_tree/variants/symbolic.py @@ -33,6 +33,7 @@ class Parameters(Enum): COLOR_MAP = "color_map" ENABLE_DEEPCOPY = "enable_deepcopy" FONT_SIZE = "font_size" + BGCOLOR = "bgcolor" def get_color(node, color_map): @@ -95,7 +96,10 @@ def apply(tree: ProcessTree, parameters: Optional[Dict[Union[str, Parameters], A parameters = {} filename = tempfile.NamedTemporaryFile(suffix='.gv') - viz = Graph("pt", filename=filename.name, engine='dot', graph_attr={'bgcolor': 'transparent'}) + + 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") diff --git a/pm4py/visualization/process_tree/variants/wo_decoration.py b/pm4py/visualization/process_tree/variants/wo_decoration.py index 3440a6398..854f975fd 100644 --- a/pm4py/visualization/process_tree/variants/wo_decoration.py +++ b/pm4py/visualization/process_tree/variants/wo_decoration.py @@ -33,6 +33,7 @@ class Parameters(Enum): COLOR_MAP = "color_map" ENABLE_DEEPCOPY = "enable_deepcopy" FONT_SIZE = "font_size" + BGCOLOR = "bgcolor" # maps the operators to the ProM strings @@ -140,7 +141,10 @@ def apply(tree: ProcessTree, parameters: Optional[Dict[Union[str, Parameters], A parameters = {} filename = tempfile.NamedTemporaryFile(suffix='.gv') - viz = Graph("pt", filename=filename.name, engine='dot', graph_attr={'bgcolor': 'transparent'}) + + 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") diff --git a/pm4py/visualization/transition_system/util/visualize_graphviz.py b/pm4py/visualization/transition_system/util/visualize_graphviz.py index 74d96ce65..a0a002182 100644 --- a/pm4py/visualization/transition_system/util/visualize_graphviz.py +++ b/pm4py/visualization/transition_system/util/visualize_graphviz.py @@ -29,6 +29,7 @@ class Parameters(Enum): FORCE_NAMES = "force_names" FILLCOLORS = "fillcolors" FONT_SIZE = "font_size" + BGCOLOR = "bgcolor" def visualize(ts, parameters=None): @@ -42,6 +43,7 @@ def visualize(ts, parameters=None): fillcolors = exec_utils.get_param_value(Parameters.FILLCOLORS, parameters, {}) font_size = exec_utils.get_param_value(Parameters.FONT_SIZE, parameters, 11) font_size = str(font_size) + bgcolor = exec_utils.get_param_value(Parameters.BGCOLOR, parameters, "transparent") for state in ts.states: state.label = state.name @@ -57,7 +59,7 @@ def visualize(ts, parameters=None): ts = nts filename = tempfile.NamedTemporaryFile(suffix='.gv') - viz = Digraph(ts.name, filename=filename.name, engine='dot', graph_attr={'bgcolor': 'transparent'}) + viz = Digraph(ts.name, filename=filename.name, engine='dot', graph_attr={'bgcolor': bgcolor}) # states viz.attr('node') diff --git a/pm4py/visualization/trie/variants/classic.py b/pm4py/visualization/trie/variants/classic.py index 0ee933901..cff7a851b 100644 --- a/pm4py/visualization/trie/variants/classic.py +++ b/pm4py/visualization/trie/variants/classic.py @@ -26,6 +26,7 @@ class Parameters(Enum): FORMAT = "format" + BGCOLOR = "bgcolor" def draw_recursive(trie_node: Trie, parent: Union[str, None], gviz: Graph): @@ -71,9 +72,10 @@ def apply(trie: Trie, parameters: Optional[Dict[Union[str, Parameters], Any]] = parameters = {} image_format = exec_utils.get_param_value(Parameters.FORMAT, parameters, "png") + bgcolor = exec_utils.get_param_value(Parameters.BGCOLOR, parameters, "transparent") filename = tempfile.NamedTemporaryFile(suffix='.gv') - viz = Graph("pt", filename=filename.name, engine='dot', graph_attr={'bgcolor': 'transparent'}) + viz = Graph("pt", filename=filename.name, engine='dot', graph_attr={'bgcolor': bgcolor}) viz.attr('node', shape='ellipse', fixedsize='false') draw_recursive(trie, None, viz) diff --git a/setup.py b/setup.py index d49624f29..26cf35eab 100644 --- a/setup.py +++ b/setup.py @@ -62,20 +62,20 @@ def read_file(filename): 'pm4py.algo.discovery.minimum_self_distance', 'pm4py.algo.discovery.minimum_self_distance.variants', 'pm4py.algo.filtering', 'pm4py.algo.filtering.dfg', 'pm4py.algo.filtering.log', 'pm4py.algo.filtering.log.ltl', 'pm4py.algo.filtering.log.cases', 'pm4py.algo.filtering.log.paths', - 'pm4py.algo.filtering.log.between', 'pm4py.algo.filtering.log.variants', - 'pm4py.algo.filtering.log.timestamp', 'pm4py.algo.filtering.log.attributes', - 'pm4py.algo.filtering.log.auto_filter', 'pm4py.algo.filtering.log.end_activities', - 'pm4py.algo.filtering.log.start_activities', 'pm4py.algo.filtering.log.attr_value_repetition', - 'pm4py.algo.filtering.common', 'pm4py.algo.filtering.common.timestamp', - 'pm4py.algo.filtering.common.attributes', 'pm4py.algo.filtering.common.end_activities', - 'pm4py.algo.filtering.common.start_activities', 'pm4py.algo.filtering.pandas', - 'pm4py.algo.filtering.pandas.ltl', 'pm4py.algo.filtering.pandas.cases', - 'pm4py.algo.filtering.pandas.paths', 'pm4py.algo.filtering.pandas.between', - 'pm4py.algo.filtering.pandas.variants', 'pm4py.algo.filtering.pandas.timestamp', - 'pm4py.algo.filtering.pandas.attributes', 'pm4py.algo.filtering.pandas.auto_filter', - 'pm4py.algo.filtering.pandas.end_activities', 'pm4py.algo.filtering.pandas.start_activities', - 'pm4py.algo.filtering.pandas.attr_value_repetition', 'pm4py.algo.reduction', - 'pm4py.algo.reduction.process_tree', 'pm4py.algo.reduction.process_tree.variants', + 'pm4py.algo.filtering.log.rework', 'pm4py.algo.filtering.log.between', + 'pm4py.algo.filtering.log.variants', 'pm4py.algo.filtering.log.timestamp', + 'pm4py.algo.filtering.log.attributes', 'pm4py.algo.filtering.log.auto_filter', + 'pm4py.algo.filtering.log.end_activities', 'pm4py.algo.filtering.log.start_activities', + 'pm4py.algo.filtering.log.attr_value_repetition', 'pm4py.algo.filtering.common', + 'pm4py.algo.filtering.common.timestamp', 'pm4py.algo.filtering.common.attributes', + 'pm4py.algo.filtering.common.end_activities', 'pm4py.algo.filtering.common.start_activities', + 'pm4py.algo.filtering.pandas', 'pm4py.algo.filtering.pandas.ltl', 'pm4py.algo.filtering.pandas.cases', + 'pm4py.algo.filtering.pandas.paths', 'pm4py.algo.filtering.pandas.rework', + 'pm4py.algo.filtering.pandas.between', 'pm4py.algo.filtering.pandas.variants', + 'pm4py.algo.filtering.pandas.timestamp', 'pm4py.algo.filtering.pandas.attributes', + 'pm4py.algo.filtering.pandas.auto_filter', 'pm4py.algo.filtering.pandas.end_activities', + 'pm4py.algo.filtering.pandas.start_activities', 'pm4py.algo.filtering.pandas.attr_value_repetition', + 'pm4py.algo.reduction', 'pm4py.algo.reduction.process_tree', 'pm4py.algo.reduction.process_tree.variants', 'pm4py.algo.clustering', 'pm4py.algo.clustering.trace_attribute_driven', 'pm4py.algo.clustering.trace_attribute_driven.dfg', 'pm4py.algo.clustering.trace_attribute_driven.util', 'pm4py.algo.clustering.trace_attribute_driven.variants', @@ -166,9 +166,9 @@ def read_file(filename): 'pm4py.streaming.util', 'pm4py.streaming.util.dictio', 'pm4py.streaming.util.dictio.versions', 'pm4py.streaming.stream', 'pm4py.streaming.importer', 'pm4py.streaming.importer.csv', 'pm4py.streaming.importer.csv.variants', 'pm4py.streaming.importer.xes', - 'pm4py.streaming.importer.xes.variants', 'pm4py.evaluation', 'pm4py.evaluation.wf_net', - 'pm4py.evaluation.wf_net.variants', 'pm4py.evaluation.precision', 'pm4py.evaluation.precision.variants', - 'pm4py.evaluation.soundness', 'pm4py.evaluation.soundness.woflan', + 'pm4py.streaming.importer.xes.variants', 'pm4py.streaming.conversion', 'pm4py.evaluation', + 'pm4py.evaluation.wf_net', 'pm4py.evaluation.wf_net.variants', 'pm4py.evaluation.precision', + 'pm4py.evaluation.precision.variants', 'pm4py.evaluation.soundness', 'pm4py.evaluation.soundness.woflan', 'pm4py.evaluation.soundness.woflan.graphs', 'pm4py.evaluation.soundness.woflan.graphs.reachability_graph', 'pm4py.evaluation.soundness.woflan.graphs.minimal_coverability_graph', 'pm4py.evaluation.soundness.woflan.graphs.restricted_coverability_graph', diff --git a/tests/input_data/receipt.parquet b/tests/input_data/receipt.parquet index d5ed3b462..b661021c9 100644 Binary files a/tests/input_data/receipt.parquet and b/tests/input_data/receipt.parquet differ diff --git a/tests/input_data/running-example.parquet b/tests/input_data/running-example.parquet index 933a72537..f9bc15aec 100644 Binary files a/tests/input_data/running-example.parquet and b/tests/input_data/running-example.parquet differ