diff --git a/examples/obj_centr_conf_check.py b/examples/obj_centr_conf_check.py new file mode 100644 index 000000000..fb8416e4c --- /dev/null +++ b/examples/obj_centr_conf_check.py @@ -0,0 +1,43 @@ +import pm4py +from pm4py.algo.discovery.ocel.otg import algorithm as otg_discovery +from pm4py.algo.discovery.ocel.etot import algorithm as etot_discovery +from pm4py.algo.conformance.ocel.ocdfg import algorithm as ocdfg_conformance +from pm4py.algo.conformance.ocel.otg import algorithm as otg_conformance +from pm4py.algo.conformance.ocel.etot import algorithm as etot_conformance + + +def execute_script(): + ocel = pm4py.read_ocel("../tests/input_data/ocel/ocel_order_simulated.csv") + + # subset that we consider as normative + ocel1 = pm4py.sample_ocel_connected_components(ocel, 1) + # subset that we use to extract the 'normative' behavior + ocel2 = pm4py.sample_ocel_connected_components(ocel, 1) + + # object-centric DFG from OCEL2 + ocdfg2 = pm4py.discover_ocdfg(ocel2) + # OTG (object-type-graph) from OCEL2 + otg2 = otg_discovery.apply(ocel2) + # ETOT (ET-OT graph) from OCEL2 + etot2 = etot_discovery.apply(ocel2) + + # conformance checking + print("== OCDFG") + diagn_ocdfg = ocdfg_conformance.apply(ocel1, ocdfg2) + print(diagn_ocdfg) + + print("\n\n== OTG") + diagn_otg = otg_conformance.apply(ocel1, otg2) + print(diagn_otg) + + print("\n\n== ETOT") + diagn_etot = etot_conformance.apply(ocel1, etot2) + print(diagn_etot) + + +if __name__ == "__main__": + execute_script() + + +if __name__ == "__main__": + execute_script() diff --git a/pm4py/algo/conformance/ocel/__init__.py b/pm4py/algo/conformance/ocel/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/conformance/ocel/etot/__init__.py b/pm4py/algo/conformance/ocel/etot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/conformance/ocel/etot/algorithm.py b/pm4py/algo/conformance/ocel/etot/algorithm.py new file mode 100644 index 000000000..12243e1e4 --- /dev/null +++ b/pm4py/algo/conformance/ocel/etot/algorithm.py @@ -0,0 +1,34 @@ +from enum import Enum +from pm4py.util import exec_utils +from pm4py.objects.ocel.obj import OCEL +from typing import Optional, Dict, Any, Union, Tuple, Set +from pm4py.algo.conformance.ocel.etot.variants import graph_comparison + + +class Variants(Enum): + GRAPH_COMPARISON = graph_comparison + + +def apply(real: Union[OCEL, Tuple[Set[str], Set[str], Set[Tuple[str, str]], Dict[Tuple[str, str], int]]], normative: Tuple[Set[str], Set[str], Set[Tuple[str, str]], Dict[Tuple[str, str], int]], variant=Variants.GRAPH_COMPARISON, parameters: Optional[Dict[Any, Any]] = None) -> Dict[str, Any]: + """ + Applies ET-OT-based conformance checking between a 'real' object (either an OCEL or an ET-OT graph), + and a normative ET-OT graph. + + Parameters + ------------------- + real + Real object (OCEL, or ET-OT graph) + normative + Normative object (ET-OT graph) + variant + Variant of the algorithm to be used: + - Variants.GRAPH_COMPARISON + parameters + Variant-specific parameters. + + Returns + ------------------ + diagn_dict + Diagnostics dictionary + """ + return exec_utils.get_variant(variant).apply(real, normative, parameters) diff --git a/pm4py/algo/conformance/ocel/etot/variants/__init__.py b/pm4py/algo/conformance/ocel/etot/variants/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/conformance/ocel/etot/variants/graph_comparison.py b/pm4py/algo/conformance/ocel/etot/variants/graph_comparison.py new file mode 100644 index 000000000..c064b8d50 --- /dev/null +++ b/pm4py/algo/conformance/ocel/etot/variants/graph_comparison.py @@ -0,0 +1,97 @@ +from enum import Enum +from pm4py.util import exec_utils +from pm4py.objects.ocel.obj import OCEL +from typing import Optional, Dict, Any, Union, Tuple, Set + + +class Parameters(Enum): + ALPHA = "alpha" + BETA = "beta" + GAMMA = "gamma" + THETA_REL = "theta_real" + + +def apply(real: Union[OCEL, Tuple[Set[str], Set[str], Set[Tuple[str, str]], Dict[Tuple[str, str], int]]], normative: Tuple[Set[str], Set[str], Set[Tuple[str, str]], Dict[Tuple[str, str], int]], parameters: Optional[Dict[Any, Any]] = None) -> Dict[str, Any]: + """ + Applies ET-OT-based conformance checking between a 'real' object (either an OCEL or an ET-OT graph), + and a normative ET-OT graph. + + Parameters + ------------------- + real + Real object (OCEL, or ET-OT graph) + normative + Normative object (ET-OT graph) + parameters + Variant-specific parameters, including: + - Parameters.ALPHA + - Parameters.BETA + - Parameters.GAMMA + - Parameters.THETA_REAL + + Returns + ------------------ + diagn_dict + Diagnostics dictionary + """ + if parameters is None: + parameters = {} + + alpha = exec_utils.get_param_value(Parameters.ALPHA, parameters, 1) + beta = exec_utils.get_param_value(Parameters.BETA, parameters, 1) + gamma = exec_utils.get_param_value(Parameters.GAMMA, parameters, 1) + theta_rel = exec_utils.get_param_value(Parameters.THETA_REL, parameters, 0.1) + + if isinstance(real, OCEL): + from pm4py.algo.discovery.ocel.etot import algorithm as etot_discovery + real = etot_discovery.apply(real, parameters=parameters) + + return compute_conformance(real, normative, alpha=alpha, beta=beta, gamma=gamma, theta_rel=theta_rel) + + +def compute_conformance(G_L, G_M, alpha=1, beta=1, gamma=1, theta_rel=0.1): + A_L, OT_L, R_L, w_L = G_L + A_M, OT_M, R_M, w_M = G_M + + # Node Conformance + A_missing = A_M - A_L + A_additional = A_L - A_M + OT_missing = OT_M - OT_L + OT_additional = OT_L - OT_M + + # Edge Conformance + R_missing = R_M - R_L + R_additional = R_L - R_M + + # Edge Frequency Conformance + delta_rel_total = 0 + delta_rel = {} + for r in R_M.intersection(R_L): + w_M_r = w_M[r] + w_L_r = w_L[r] + delta = abs(w_L_r - w_M_r) / w_M_r + delta_rel[r] = delta + if delta > theta_rel: + delta_rel_total += 1 + + # Normalization constant + N = alpha * (len(A_M) + len(OT_M)) + beta * len(R_M) + gamma * len(R_M) + + # Compute numerator + numerator = alpha * (len(A_missing) + len(OT_missing)) + beta * len(R_missing) + gamma * delta_rel_total + + # Fitness value + phi = 1 - (numerator / N) + + # Details dictionary + details = { + 'A_missing': A_missing, + 'A_additional': A_additional, + 'OT_missing': OT_missing, + 'OT_additional': OT_additional, + 'R_missing': R_missing, + 'R_additional': R_additional, + 'delta_rel': delta_rel + } + + return {"fitness": phi, "details": details} diff --git a/pm4py/algo/conformance/ocel/ocdfg/__init__.py b/pm4py/algo/conformance/ocel/ocdfg/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/conformance/ocel/ocdfg/algorithm.py b/pm4py/algo/conformance/ocel/ocdfg/algorithm.py new file mode 100644 index 000000000..ab36e47ff --- /dev/null +++ b/pm4py/algo/conformance/ocel/ocdfg/algorithm.py @@ -0,0 +1,33 @@ +from pm4py.algo.conformance.ocel.ocdfg.variants import graph_comparison +from pm4py.util import exec_utils +from typing import Optional, Dict, Any, Union +from enum import Enum +from pm4py.objects.ocel.obj import OCEL + + +class Variants(Enum): + GRAPH_COMPARISON = graph_comparison + + +def apply(real: Union[OCEL, Dict[str, Any]], normative: Dict[str, Any], variant=Variants.GRAPH_COMPARISON, parameters: Optional[Dict[Any, Any]] = None) -> Dict[str, Any]: + """ + Applies object-centric conformance checking between the given real object (object-centric event log or DFG) + and a normative OC-DFG. + + Parameters + ----------------- + real + Real entity (OCEL or OC-DFG) + normative + Normative entity (OC-DFG) + variant + Variant of the algorithm to be used (default: Variants.GRAPH_COMPARISON) + parameters + Variant-specific parameters + + Returns + ----------------- + conf_diagn_dict + Dictionary with conformance diagnostics + """ + return exec_utils.get_variant(variant).apply(real, normative, parameters) diff --git a/pm4py/algo/conformance/ocel/ocdfg/variants/__init__.py b/pm4py/algo/conformance/ocel/ocdfg/variants/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/conformance/ocel/ocdfg/variants/graph_comparison.py b/pm4py/algo/conformance/ocel/ocdfg/variants/graph_comparison.py new file mode 100644 index 000000000..d8f49dac1 --- /dev/null +++ b/pm4py/algo/conformance/ocel/ocdfg/variants/graph_comparison.py @@ -0,0 +1,169 @@ +from pm4py.util import exec_utils +from typing import Optional, Dict, Any, Union +from enum import Enum +from pm4py.objects.ocel.obj import OCEL + + +class Parameters(Enum): + THETA_ACT = "theta_act" + THETA_FLOW = "theta_flow" + ALPHA = "alpha" + BETA = "beta" + GAMMA = "gamma" + DELTA = "delta" + + +def apply(real: Union[OCEL, Dict[str, Any]], normative: Dict[str, Any], parameters: Optional[Dict[Any, Any]] = None) -> Dict[str, Any]: + """ + Applies object-centric conformance checking between the given real object (object-centric event log or DFG) + and a normative OC-DFG. + + Parameters + ----------------- + real + Real entity (OCEL or OC-DFG) + normative + Normative entity (OC-DFG) + parameters + Variant-specific parameters: + - Parameters.THETA_ACT + - Parameters.THETA_FLOW + - Parameters.ALPHA + - Parameters.BETA + - Parameters.GAMMA + - Parameters.DELTA + + Returns + ----------------- + conf_diagn_dict + Dictionary with conformance diagnostics + """ + if parameters is None: + parameters = {} + + theta_act = exec_utils.get_param_value(Parameters.THETA_ACT, parameters, 0) + theta_flow = exec_utils.get_param_value(Parameters.THETA_FLOW, parameters, 0) + alpha = exec_utils.get_param_value(Parameters.ALPHA, parameters, 1) + beta = exec_utils.get_param_value(Parameters.BETA, parameters, 1) + gamma = exec_utils.get_param_value(Parameters.GAMMA, parameters, 1) + delta = exec_utils.get_param_value(Parameters.DELTA, parameters, 1) + + if isinstance(real, OCEL): + import pm4py + real = pm4py.discover_ocdfg(real) + + return compare_ocdfgs(real, normative, theta_act, theta_flow, alpha, beta, gamma, delta) + + +def compare_ocdfgs(ocdfg1, ocdfg2, theta_act=0, theta_flow=0, alpha=1, beta=1, gamma=1, delta=1): + """ + Compare two Object-Centric Directly-Follows Graphs (OCDFGs) and perform conformance checking. + + Parameters: + - ocdfg1: The first OCDFG to compare. + - ocdfg2: The second OCDFG to compare. + - theta_act: Threshold for activity measure difference. + - theta_flow: Threshold for flow measure difference. + - alpha, beta, gamma, delta: Weighting factors for fitness calculation. + + Returns: + - A dictionary containing conformance checking results. + """ + + # Extract components from OCDFG1 + A1 = set(ocdfg1.get('activities', set())) + edges1 = ocdfg1.get('edges', {}) + activities_indep1 = ocdfg1.get('activities_indep', {}) + + # Extract components from OCDFG2 + A2 = set(ocdfg2.get('activities', set())) + edges2 = ocdfg2.get('edges', {}) + activities_indep2 = ocdfg2.get('activities_indep', {}) + + # Union of activities + all_activities = A1.union(A2) + + # Activity Conformance + A_missing = A2 - A1 # Activities in ocdfg2 but not in ocdfg1 + A_additional = A1 - A2 # Activities in ocdfg1 but not in ocdfg2 + + # Flow (Edge) Conformance + # 'event_couples' is a parent of the object types + F1_set = set() + event_couples1 = edges1.get('event_couples', {}) + for ot in event_couples1: + flows1 = event_couples1[ot] + F1_set.update(flows1.keys()) + + F2_set = set() + event_couples2 = edges2.get('event_couples', {}) + for ot in event_couples2: + flows2 = event_couples2[ot] + F2_set.update(flows2.keys()) + + F_missing = F2_set - F1_set # Flows in ocdfg2 but not in ocdfg1 + F_additional = F1_set - F2_set # Flows in ocdfg1 but not in ocdfg2 + + # Measure Conformance for Activities + Delta_act = {} + delta_act = {} + events1 = activities_indep1.get('events', {}) + events2 = activities_indep2.get('events', {}) + for a in all_activities: + # Measure in ocdfg1 + measure1 = len(events1.get(a, [])) + # Measure in ocdfg2 + measure2 = len(events2.get(a, [])) + diff = abs(measure2 - measure1) + Delta_act[a] = diff + delta_act[a] = 1 if diff > theta_act else 0 + + # Measure Conformance for Flows + Delta_flow = {} + delta_flow = {} + all_flows = F1_set.union(F2_set) + for flow in all_flows: + measure1 = 0 + # Sum over all object types in event_couples1 + for ot in event_couples1: + flows1 = event_couples1[ot] + measure1 += len(flows1.get(flow, [])) + measure2 = 0 + # Sum over all object types in event_couples2 + for ot in event_couples2: + flows2 = event_couples2[ot] + measure2 += len(flows2.get(flow, [])) + diff = abs(measure2 - measure1) + Delta_flow[flow] = diff + delta_flow[flow] = 1 if diff > theta_flow else 0 + + # Fitness Calculation + N = alpha * len(all_activities) + beta * len(all_flows) + \ + gamma * len(all_activities) + delta * len(all_flows) + + # Calculate numerator components + fitness_numerator = (alpha * len(A_missing) + beta * len(F_missing) + + gamma * sum(delta_act.values()) + delta * sum(delta_flow.values())) + + # To avoid division by zero + if N == 0: + fitness = 1.0 + else: + fitness = 1 - (fitness_numerator / N) + fitness = max(0.0, min(fitness, 1.0)) # Ensure fitness is within [0,1] + + # Prepare the result dictionary + result = { + 'missing_activities': A_missing, + 'additional_activities': A_additional, + 'missing_flows': F_missing, + 'additional_flows': F_additional, + 'activity_measure_differences': Delta_act, + 'non_conforming_activities_in_measure': {a for a in Delta_act if Delta_act[a] > theta_act}, + 'flow_measure_differences': Delta_flow, + 'non_conforming_flows_in_measure': {f for f in Delta_flow if Delta_flow[f] > theta_flow}, + 'fitness': fitness + } + + return result + diff --git a/pm4py/algo/conformance/ocel/otg/__init__.py b/pm4py/algo/conformance/ocel/otg/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/conformance/ocel/otg/algorithm.py b/pm4py/algo/conformance/ocel/otg/algorithm.py new file mode 100644 index 000000000..9f8753ee7 --- /dev/null +++ b/pm4py/algo/conformance/ocel/otg/algorithm.py @@ -0,0 +1,32 @@ +from enum import Enum +from pm4py.util import exec_utils +from pm4py.objects.ocel.obj import OCEL +from typing import Optional, Dict, Any, Union, Tuple, Set +from pm4py.algo.conformance.ocel.otg.variants import graph_comparison + + +class Variants(Enum): + GRAPH_COMPARISON = graph_comparison + + +def apply(real: Union[OCEL, Tuple[Set[str], Dict[Tuple[str, str, str], int]]], normative: Tuple[Set[str], Dict[Tuple[str, str, str], int]], variant=Variants.GRAPH_COMPARISON, parameters: Optional[Dict[Any, Any]] = None) -> Dict[str, Any]: + """ + Applies OTG-based conformance checking between a 'real' object (OCEL or OTG) and a 'normative' OTG. + + Parameters + ----------------- + real + Real object (OCEL or OTG) + normative + Normative OTG + variant + Variant of the algorithm to be used (default: Variants.GRAPH_COMPARISON) + parameters + Variant-specific parameters + + Returns + ----------------- + conf_diagn + Diagnostics dictionary + """ + return exec_utils.get_variant(variant).apply(real, normative, parameters) diff --git a/pm4py/algo/conformance/ocel/otg/variants/__init__.py b/pm4py/algo/conformance/ocel/otg/variants/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/conformance/ocel/otg/variants/graph_comparison.py b/pm4py/algo/conformance/ocel/otg/variants/graph_comparison.py new file mode 100644 index 000000000..657962a61 --- /dev/null +++ b/pm4py/algo/conformance/ocel/otg/variants/graph_comparison.py @@ -0,0 +1,102 @@ +from enum import Enum +from pm4py.util import exec_utils +from pm4py.objects.ocel.obj import OCEL +from typing import Optional, Dict, Any, Union, Tuple, Set + + +class Parameters(Enum): + THETA = "theta" + ALPHA = "alpha" + BETA = "beta" + GAMMA = "gamma" + + +def apply(real: Union[OCEL, Tuple[Set[str], Dict[Tuple[str, str, str], int]]], normative: Tuple[Set[str], Dict[Tuple[str, str, str], int]], parameters: Optional[Dict[Any, Any]] = None) -> Dict[str, Any]: + """ + Applies OTG-based conformance checking between a 'real' object (OCEL or OTG) and a 'normative' OTG. + + Parameters + ----------------- + real + Real object (OCEL or OTG) + normative + Normative OTG + parameters + Variant-specific parameters: + - Parameters.THETA + - Parameters.ALPHA + - Parameters.BETA + - Parameters.GAMMA + + Returns + ----------------- + conf_diagn + Diagnostics dictionary + """ + if parameters is None: + parameters = {} + + theta = exec_utils.get_param_value(Parameters.THETA, parameters, None) + alpha = exec_utils.get_param_value(Parameters.ALPHA, parameters, 1) + beta = exec_utils.get_param_value(Parameters.BETA, parameters, 1) + gamma = exec_utils.get_param_value(Parameters.GAMMA, parameters, 1) + + if isinstance(real, OCEL): + from pm4py.algo.discovery.ocel.otg import algorithm as otg_discovery + real = otg_discovery.apply(real, parameters=parameters) + + return conformance_checking_multigraph(real, normative, theta=theta, alpha=alpha, beta=beta, gamma=gamma) + +def conformance_checking_multigraph(discovered_otg, normative_otg, theta=None, alpha=1, beta=1, gamma=1): + object_types_L, edges_L = discovered_otg + object_types_M, edges_M = normative_otg + + # Default thresholds for each relationship type if not provided + if theta is None: + theta = { + 'object_interaction': 0.2, + 'object_descendants': 0.2, + 'object_inheritance': 0.2, + 'object_cobirth': 0.2, + 'object_codeath': 0.2 + } + + # Object Type Conformance + missing_object_types = object_types_M - object_types_L + additional_object_types = object_types_L - object_types_M + + # Edge Conformance + missing_edges = set(edges_M.keys()) - set(edges_L.keys()) + additional_edges = set(edges_L.keys()) - set(edges_M.keys()) + + # Edge Frequency Conformance + non_conforming_edges = {} + for edge in set(edges_M.keys()) & set(edges_L.keys()): + f_M = edges_M[edge] + f_L = edges_L[edge] + sigma = edge[1] # Relationship type + delta = abs(f_L - f_M) / f_M if f_M != 0 else float('inf') + if delta > theta.get(sigma, 0.2): + non_conforming_edges[edge] = delta + + # Fitness Value Calculation + total_object_types = len(object_types_M) + total_edges = len(edges_M) + total_edge_checks = len(edges_M) + N = alpha * total_object_types + beta * total_edges + gamma * total_edge_checks + fitness = 1 - ( + alpha * len(missing_object_types) + + beta * len(missing_edges) + + gamma * len(non_conforming_edges) + ) / N + + # Prepare the result + result = { + 'missing_object_types': missing_object_types, + 'additional_object_types': additional_object_types, + 'missing_edges': missing_edges, + 'additional_edges': additional_edges, + 'non_conforming_edges': non_conforming_edges, + 'fitness': fitness + } + return result diff --git a/pm4py/algo/discovery/ocel/etot/__init__.py b/pm4py/algo/discovery/ocel/etot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/discovery/ocel/etot/algorithm.py b/pm4py/algo/discovery/ocel/etot/algorithm.py new file mode 100644 index 000000000..a18de32bd --- /dev/null +++ b/pm4py/algo/discovery/ocel/etot/algorithm.py @@ -0,0 +1,37 @@ +from collections import defaultdict +from typing import Optional, Dict, Any, Tuple, Set +from pm4py.objects.ocel.obj import OCEL +from enum import Enum +from pm4py.util import exec_utils +from pm4py.algo.discovery.ocel.etot.variants import classic + + +class Variants(Enum): + CLASSIC = classic + + +def apply(ocel: OCEL, variant=Variants.CLASSIC, parameters: Optional[Dict[Any, Any]] = None) -> Tuple[Set[str], Set[str], Set[Tuple[str, str]], Dict[Tuple[str, str], int]]: + """ + Discovers the ET-OT graph from an OCEL + + Parameters + --------------- + ocel + Object-centric event log + variant + Variant of the algorithm to be used (available: Variants.CLASSIC) + parameters + Variant-specific parameters + + Returns + ---------------- + activities + Set of activities + object_types + Set of object types + edges + Set of edges + edges_frequency + Dictionary associating to each edge a frequency + """ + return exec_utils.get_variant(variant).apply(ocel, parameters) diff --git a/pm4py/algo/discovery/ocel/etot/variants/__init__.py b/pm4py/algo/discovery/ocel/etot/variants/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/discovery/ocel/etot/variants/classic.py b/pm4py/algo/discovery/ocel/etot/variants/classic.py new file mode 100644 index 000000000..5768da95a --- /dev/null +++ b/pm4py/algo/discovery/ocel/etot/variants/classic.py @@ -0,0 +1,45 @@ +from collections import defaultdict +from typing import Optional, Dict, Any, Tuple, Set +from pm4py.objects.ocel.obj import OCEL + + +def apply(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None) -> Tuple[Set[str], Set[str], Set[Tuple[str, str]], Dict[Tuple[str, str], int]]: + """ + Discovers the ET-OT graph from an OCEL + + Parameters + --------------- + ocel + Object-centric event log + parameters + Variant-specific parameters + + Returns + ---------------- + activities + Set of activities + object_types + Set of object types + edges + Set of edges + edges_frequency + Dictionary associating to each edge a frequency + """ + if parameters is None: + parameters = {} + + A = set() + OT = set() + R = set() + w = defaultdict(int) + + # Iterate over the relations to build the ETOT Graph + for index, row in ocel.relations.iterrows(): + a = row['ocel:activity'] + ot = row['ocel:type'] + A.add(a) + OT.add(ot) + R.add((a, ot)) + w[(a, ot)] += 1 + + return A, OT, R, w diff --git a/pm4py/algo/discovery/ocel/otg/__init__.py b/pm4py/algo/discovery/ocel/otg/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/discovery/ocel/otg/algorithm.py b/pm4py/algo/discovery/ocel/otg/algorithm.py new file mode 100644 index 000000000..72d2d8ccd --- /dev/null +++ b/pm4py/algo/discovery/ocel/otg/algorithm.py @@ -0,0 +1,33 @@ +from pm4py.algo.discovery.ocel.otg.variants import classic +from enum import Enum +from pm4py.util import exec_utils +from pm4py.objects.ocel.obj import OCEL +from typing import Optional, Dict, Any, Tuple, Set + + +class Variants(Enum): + CLASSIC = classic + + +def apply(ocel: OCEL, variant=Variants.CLASSIC, parameters: Optional[Dict[Any, Any]] = None) -> Tuple[Set[str], Dict[Tuple[str, str, str], int]]: + """ + Discovers an OTG (object-type-graph) from the provided OCEL + + Parameters + ----------------- + ocel + OCEL + variant + Variant to be used (available: Variants.CLASSIC) + parameters + Variant-specific parameters + + Returns + ----------------- + otg + Object-type-graph (tuple; the first element is the set of object types, the second element is the OTG) + """ + if parameters is None: + parameters = {} + + return exec_utils.get_variant(variant).apply(ocel, parameters) diff --git a/pm4py/algo/discovery/ocel/otg/variants/__init__.py b/pm4py/algo/discovery/ocel/otg/variants/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pm4py/algo/discovery/ocel/otg/variants/classic.py b/pm4py/algo/discovery/ocel/otg/variants/classic.py new file mode 100644 index 000000000..f6dfc8a94 --- /dev/null +++ b/pm4py/algo/discovery/ocel/otg/variants/classic.py @@ -0,0 +1,43 @@ +from collections import defaultdict +from pm4py.objects.ocel.obj import OCEL +from typing import Optional, Dict, Any, Tuple, Set + + +def apply(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None) -> Tuple[Set[str], Dict[Tuple[str, str, str], int]]: + if parameters is None: + parameters = {} + + import pm4py + + # Available graph types + graph_types = [ + "object_interaction", + "object_descendants", + "object_inheritance", + "object_cobirth", + "object_codeath" + ] + + # Initialize OTG components + edges = defaultdict(int) + + object_types = set(ocel.objects["ocel:type"].unique()) + objects = ocel.objects.to_dict("records") + objects = {x["ocel:oid"]: x["ocel:type"] for x in objects} + + # Iterate over each relationship type + for sigma in graph_types: + # Discover the object graph for the relationship type + object_graph = pm4py.discover_objects_graph(ocel, graph_type=sigma) + + # Build the edges for the OTG + for o1, o2 in object_graph: + ot1 = objects[o1] + ot2 = objects[o2] + + ot_pair = (ot1, ot2) + + edge = (ot_pair[0], sigma, ot_pair[1]) + edges[edge] += 1 + + return object_types, dict(edges)