diff --git a/pm4py/algo/simulation/playout/oc_causal_net/__init__.py b/pm4py/algo/simulation/playout/oc_causal_net/__init__.py new file mode 100644 index 0000000000..0f2a9692a7 --- /dev/null +++ b/pm4py/algo/simulation/playout/oc_causal_net/__init__.py @@ -0,0 +1,22 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.algo.simulation.playout.oc_causal_net import algorithm, variants diff --git a/pm4py/algo/simulation/playout/oc_causal_net/algorithm.py b/pm4py/algo/simulation/playout/oc_causal_net/algorithm.py new file mode 100644 index 0000000000..063c74f2b8 --- /dev/null +++ b/pm4py/algo/simulation/playout/oc_causal_net/algorithm.py @@ -0,0 +1,59 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.algo.simulation.playout.ocpn.variants import extensive +from pm4py.util import exec_utils +from enum import Enum +from pm4py.objects.oc_causal_net.obj import OCCausalNet +from typing import Optional, Dict, Any +from pm4py.objects.ocel.obj import OCEL + + +class Variants(Enum): + EXTENSIVE = extensive + + +DEFAULT_VARIANT = Variants.EXTENSIVE +VERSIONS = {Variants.EXTENSIVE} + + +def apply(occn: OCCausalNet, objects, parameters: Optional[Dict[Any, Any]] = None, variant=DEFAULT_VARIANT) -> OCEL: + """ + Do the playout of an object-centric causal net generating an OCEL. + + Parameters + ----------- + occn + Object-centric causal net to play-out + objects + Dictionary mapping object types to object ids. These objects will be introduced by the start activities of the occn at the beginning of every binding sequence. + parameters + Parameters of the algorithm + variant + Variant of the algorithm to use: + - Variants.EXTENSIVE: gets all the traces from the model. can be expensive + + Returns + ----------- + OCEL + Object-centric event log generated by the playout of the object-centric causal net + """ + return exec_utils.get_variant(variant).apply(occn, objects, parameters=parameters) diff --git a/pm4py/algo/simulation/playout/oc_causal_net/variants/__init__.py b/pm4py/algo/simulation/playout/oc_causal_net/variants/__init__.py new file mode 100644 index 0000000000..71d813047d --- /dev/null +++ b/pm4py/algo/simulation/playout/oc_causal_net/variants/__init__.py @@ -0,0 +1,22 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.algo.simulation.playout.oc_causal_net.variants import extensive diff --git a/pm4py/algo/simulation/playout/oc_causal_net/variants/extensive.py b/pm4py/algo/simulation/playout/oc_causal_net/variants/extensive.py new file mode 100644 index 0000000000..671e485d43 --- /dev/null +++ b/pm4py/algo/simulation/playout/oc_causal_net/variants/extensive.py @@ -0,0 +1,666 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from collections import Counter, namedtuple +import random +from typing import Any, Dict, Optional + +import pandas as pd +from pm4py.objects.oc_causal_net.obj import OCCausalNet +from pm4py.objects.oc_causal_net.semantics import OCCausalNetState, OCCausalNetSemantics +from pm4py.objects.ocel import constants +from pm4py.objects.ocel.obj import OCEL +from pm4py.util import exec_utils +from enum import Enum + + +class Parameters(Enum): + EVENT_ID = constants.PARAM_EVENT_ID + EVENT_ACTIVITY = constants.PARAM_EVENT_ACTIVITY + EVENT_TIMESTAMP = constants.PARAM_EVENT_TIMESTAMP + OBJECT_ID = constants.PARAM_OBJECT_ID + OBJECT_TYPE = constants.PARAM_OBJECT_TYPE + OBJECTS_UNIQUE_PER_SEQUENCE = "objects_unique_per_sequence" + RETURN_SEQUENCES = "return_sequences" + MAX_BINDINGS_PER_ACTIVITY = "maxBindingsPerActivity" + OCCN_SEMANTICS = "occn_semantics" + BRANCHING_FACTOR_ACTIVITIES = "branching_factor_activities" + BRANCHING_FACTOR_BINDINGS = "branching_factor_bindings" + + +FINAL_MARKER = "FINAL" + + +# Define memory-efficient data type for binding +# a binding is a tuple of activity id, consumed objects, and produced objects +# consumed / produces are tuples of (predecessor/successor activity id, objects_per_ot) +# where objects_per_ot is a tuples of entries (object_type, objects) +# where objects is a tuple (obj_id_1, obj_id_2, ...) +Binding = namedtuple("Binding", ["activity_id", "consumed", "produced"]) + + +def apply( + occn: OCCausalNet, objects: dict, parameters: Optional[Dict[Any, Any]] = None +) -> OCEL: + """ + Compute playout of an object-centric causal net generating an OCEL. + Extensive search, retrieves all valid binding sequences. + Starts by binding start activities with the objects specified and ends in the empty state. + The empty sequence is considered a valid sequence no other sequences are found. + + Parameters + ----------- + occn + Object-centric causal net to play-out + objects + Dictionary mapping object types to sets of object ids. These objects will be introduced by the start activities of the occn at the beginning of every binding sequence. + parameters + Parameters of the algorithm, including: + Parameters.MAX_BINDINGS_PER_ACTIVITY: Maximum number of bindings per activity (mandatory) + Parameters.RETURN_SEQUENCES: If True, return an iterator to all possible sequences of bindings instead of an OCEL + Parameters.OBJECTS_UNIQUE_PER_SEQUENCE: If True, objects in the resulting OCEL are make unique per sequence (default: False) + Parameters.OCCN_SEMANTICS: The semantics to be used for the causal net (default: OCCausalNetSemantics()) + Parameters.BRANCHING_FACTOR_ACTIVITIES: Maximum branching factor for exploring enabled activities (default: inf). Note that the play-out will generate a subset of all sequences if this is set. + Parameters.BRANCHING_FACTOR_BINDINGS: Maximum branching factor for exploring enabled bindings (default: inf). Note that the play-out will generate a subset of all sequences if this is set. + """ + if parameters is None: + parameters = {} + + return_sequences = exec_utils.get_param_value( + Parameters.RETURN_SEQUENCES, parameters, False + ) + if Parameters.MAX_BINDINGS_PER_ACTIVITY not in parameters: + raise ValueError( + "Parameter MAX_BINDINGS_PER_ACTIVITY must be specified for the extensive playout. This parameter limits the maximum number of times an activity may be executed." + ) + max_bindings_per_activity = exec_utils.get_param_value( + Parameters.MAX_BINDINGS_PER_ACTIVITY, parameters, None + ) + semantics = exec_utils.get_param_value( + Parameters.OCCN_SEMANTICS, + parameters, + OCCausalNetSemantics(), + ) + bf_act = exec_utils.get_param_value( + Parameters.BRANCHING_FACTOR_ACTIVITIES, parameters, float("inf") + ) + bf_bind = exec_utils.get_param_value( + Parameters.BRANCHING_FACTOR_BINDINGS, parameters, float("inf") + ) + + # create int id for every activity for memory efficiency + activity_to_id = {activity: i for i, activity in enumerate(occn.activities)} + id_to_activity = {i: activity for activity, i in activity_to_id.items()} + start_activities = set( + i for activity, i in activity_to_id.items() if activity.startswith("START_") + ) + # same for object types + object_type_to_id = { + object_type: i for i, object_type in enumerate(occn.object_types) + } + id_to_object_type = {i: object_type for object_type, i in object_type_to_id.items()} + + # Set up initial state with starting objects + # In the state, we denote activities by their id, not by their name + initial_state = OCCausalNetState() + + # Create fake obligations to start activities for all starting objects + for object_type, object_ids in objects.items(): + ot_id = object_type_to_id[object_type] + start_activity_id = activity_to_id[f"START_{object_type}"] + initial_state += OCCausalNetState( + {start_activity_id: Counter([(-1, obj_id, ot_id) for obj_id in object_ids])} + ) + + # Activity counts + # index is from `activity_to_id` + initial_activity_counts = (0,) * len(occn.activities) + + # State key used for memoization, see below + initial_state_key = (initial_state, initial_activity_counts) + + # Memoization cache: Dict[state_key, Union[Set[Tuple[Binding, next_key]], str]] + # where state_key is a tuple of (state, activity_counts) and the value is either + # FINAL_MARKER if the state is the empty state, + # or a set of tuples of bindings and next state keys that correspond to all successor + # states that can be reached from the current state using the respective bindings. + memo = {} + + # == Phase 1: Memoization DFS Graph Population == + _populate_memo_graph( + initial_state_key, + occn, + semantics, + max_bindings_per_activity, + start_activities, + activity_to_id, + id_to_activity, + object_type_to_id, + bf_act, + bf_bind, + memo, + ) + + # == Phase 2: Reconstruct traces from memo == + valid_sequences_iter = _reconstruct_sequences(initial_state_key, memo) + + # == Phase 3: Return data in the desired format == + if return_sequences: + # return valid_sequences along with a mapping from indices to activities and object types + return ( + valid_sequences_iter, + id_to_activity, + id_to_object_type, + ) + else: + return _valid_sequences_to_ocel(valid_sequences_iter, id_to_activity, id_to_object_type, parameters) + + +def _populate_memo_graph( + state_key: tuple, + occn: OCCausalNet, + semantics, + max_bindings: int, + start_activities, + act_to_idx: dict, + idx_to_act: dict, + ot_to_idx: dict, + bf_act: float, + bf_bind: float, + memo: dict, +) -> bool: + """ + Recursively explores the state space to build a compact, memoized graph of all valid binding sequences. + + This function performs a depth-first search from a given state_key. It populates a memoization + cache (`memo`). For each state (defined by the state_key), it stores the set of "next steps" (as tuples of + (binding, next_state_key)) that lie on a path to the empty state, + where binding is of type Binding. + + This approach avoids duplicate computation of two different sequences leading to the same state key. + + Parameters + ----------- + state_key : tuple + A tuple representing the current state in the form (state, activity_counts). + occn : OCCausalNet + The object-centric causal net being used. + semantics + The semantics to be used for the causal net. + max_bindings : int + Maximum number of bindings per activity. + start_activities + Collection of indices for start activities. + act_to_idx : dict + Dictionary mapping activities to their id. + idx_to_act : dict + Dictionary mapping activity ids to their names. + ot_to_idx : dict + Dictionary mapping object types to their id. + bf_act : float + Traversal will only explore this many enabled activities per step. If set, the play-out will generate a subset + of all sequences. Will be stochastically rounded if not an integer. + bf_bind : float + Traversal will only explore this many enabled bindings per activity. If set, the play-out will generate a subset + of all sequences. Will be stochastically rounded if not an integer. + memo : dict + The memoization cache where the state_key is mapped to a set of next steps or FINAL_MARKER if the state is the empty state. + + Returns + ------- + bool + Returns True if the state_key is reachable (i.e., not a deadlock), False otherwise. + If the state_key is a deadlock, it will be represented by an empty set in the memo. + """ + if state_key in memo: + # a deadlock is indicated by an empty set in the memo. + # an entry that is not empty indicates that the empty state is reachable + return bool(memo[state_key]) + + # state_key has not been explored yet, so we explore it + state, activity_counts = state_key + if not state.activities: + # empty state + memo[state_key] = FINAL_MARKER + return True + + next_steps = set() + enabled_activities = _get_enabled_activities( + occn, semantics, state, start_activities, act_to_idx, idx_to_act, ot_to_idx + ) + + # Limit the number of enabled activities to bf_act + if bf_act < float("inf"): + # Stochastically round bf_act to an integer + bf_act_rounded = int(bf_act) + (1 if random.random() < (bf_act % 1) else 0) + # Select random subset of enabled activities + enabled_activities = set(random.sample(list(enabled_activities), min(bf_act_rounded, len(enabled_activities)))) + + # explore all sucessor states by binding all enabled activities + for act in enabled_activities: + act_id = act_to_idx[act] + + if activity_counts[act_id] >= max_bindings: + continue + + new_activiy_counts = list(activity_counts) + new_activiy_counts[act_id] += 1 + new_activity_counts_tuple = tuple(new_activiy_counts) + + # Get all enabled bindings for this activity + if act_id in start_activities: + enabled_bindings = _get_bindings_start_activity( + occn, act, state, act_to_idx, ot_to_idx + ) + else: + enabled_bindings = semantics.enabled_bindings(occn, act, state, act_to_idx, ot_to_idx) + + # Limit the number of enabled bindings to bf_bind + if bf_bind < float("inf"): + # Stochastically round bf_bind to an integer + bf_bind_rounded = int(bf_bind) + (1 if random.random() < (bf_bind % 1) else 0) + # Select random subset of enabled bindings + enabled_bindings = set(random.sample(list(enabled_bindings), min(bf_bind_rounded, len(enabled_bindings)))) + + # explore all bindings + for binding in enabled_bindings: + new_state = semantics.bind_activity( + occn, + act=binding[0], + cons=_convert_binding_tuple_to_dict(binding[1]), + prod=_convert_binding_tuple_to_dict(binding[2]), + state=state, + ) + # clean up fake obligations for start activities + if act_id in start_activities: + new_state = _clean_fake_obligations( + occn, new_state, act, binding[2], act_to_idx, ot_to_idx + ) + new_state_key = (new_state, new_activity_counts_tuple) + + if _populate_memo_graph( + new_state_key, + occn, + semantics, + max_bindings, + start_activities, + act_to_idx, + idx_to_act, + ot_to_idx, + bf_act, + bf_bind, + memo + ): + next_steps.add((binding, new_state_key)) + + # Add all next steps to memo + # If state_key is a deadlock, this will be an empty set + memo[state_key] = next_steps + return bool(next_steps) + + +def _convert_binding_tuple_to_dict(binding_tuple): + """ + Converts a tuple from a binding (conumed or produced) into a nested dictionary. + None is converted to None. + + The inner values (object lists) are converted to sets. + """ + if not binding_tuple: + return None + return { + related_act: { + object_type: set(objects) for object_type, objects in objects_per_type + } + for related_act, objects_per_type in binding_tuple + } + + +def _get_enabled_activities( + occn: OCCausalNet, + semantics, + state: OCCausalNetState, + start_activities, + act_to_idx: dict, + idx_to_act: dict, + ot_to_idx: dict, +) -> set: + """ + Returns the enabled activities in the given state, including start activities + if they have "fake obligations". + + Parameters + ----------- + occn : OCCausalNet + The causal net being used. + semantics + The semantics to be used for the causal net. + state : OCCausalNetState + The current state of the causal net. + start_activities + Collection of indices for start activities + act_to_idx + Dictionary mapping activities to their id. + idx_to_act + Dictionary mapping activity ids to their names. + ot_to_idx + Dictionary mapping object types to their id. + + Returns + -------- + set + A set of ids for enabled activities in the given state. + """ + enabled_activities = set() + + # get start activities with outstanding fake obligations + start_activities_with_obligations = state.activities.intersection(start_activities) + # add names, not ids + enabled_activities.update(idx_to_act[act_id] for act_id in start_activities_with_obligations) + + # get all other enabled activities + enabled_activities.update( + semantics.enabled_activities( + occn, + state, + include_start_activities=False, + act_to_idx=act_to_idx, + ot_to_idx=ot_to_idx, + ) + ) + + return enabled_activities + + +def _get_bindings_start_activity( + occn: OCCausalNet, + act: str, + state: OCCausalNetState, + act_to_idx: dict, + ot_to_idx: dict, +): + """ + Computes all enabled bindings for a start activity with the given fake obligations + in the state. + + Parameters + ----------- + occn : OCCausalNet + The object-centric causal net + act : str + The start activity to bind + state : OCCausalNetState + The current state of the causal net, which contains the fake obligations for the start activity. + act_to_idx : dict + Dictionary mapping activities to their id. + ot_to_idx : dict + Dictionary mapping object types to their id. + + Returns + ----------- + tuple + A tuple of enabled bindings for the start activity. + Each binding is a tuple of (activity_id, consumed, produced). + The consumed and produced are tuples of (predecessor/successor activity id, objects_per_ot), + where objects_per_ot is a tuple of entries (object_type_id, objects). + """ + # get the outstanding fake obligations for the start activity + act_id = act_to_idx[act] + obligations = state[act_id] + if not obligations: + return () + outstanding_objects = set() + for (_, obj_id, _), _ in obligations.items(): + outstanding_objects.add(obj_id) + + # Extract object type + object_type = act.split("_", 1)[1] + + # Compute enabled bindings + bindings = OCCausalNetSemantics.enabled_bindings_start_activity( + occn, act, object_type, outstanding_objects, act_to_idx, ot_to_idx + ) + + return bindings + +def _clean_fake_obligations( + occn: OCCausalNet, + state: OCCausalNetState, + act: str, + produced: tuple, + act_to_idx: dict, + ot_to_idx: dict, +) -> OCCausalNetState: + """ + Cleans up fake obligations for start activities in the state after binding the start activity. + Since a firing of a start activity consumes no obligations, + we need to manually remove the fake obligations that were created for the start activity + for all objects that were bound to it. + + Parameters + ----------- + occn : OCCausalNet + The object-centric causal net. + state : OCCausalNetState + The current state of the causal net. + act : str + The activity that was bound. + produced : tuple + The produced tuple from the binding. + act_to_idx : dict + Dictionary mapping activities to their id. + ot_to_idx : dict + Dictionary mapping object types to their id. + + Returns + ----------- + OCCausalNetState + The updated state with cleaned fake obligations. + """ + # get the set of all objects involved + objects = set() + object_types = set() + for _, ot_to_obj in produced: + for ot, obj_ids in ot_to_obj: + objects.update(obj_ids) + object_types.add(ot) + + assert len(object_types) == 1, "Only one object type should be involved in a start activity binding" + ot_id = next(iter(object_types)) + + act_id = act_to_idx[act] + # remove all obligations for the start activity that are related to the objects + state -= OCCausalNetState( + {act_id: Counter([(-1, obj_id, ot_id) for obj_id in objects])} + ) + + return state + + +def _reconstruct_sequences(state_key: tuple, memo: dict): + """ + Reconstructs valid binding sequences from the memoization cache. + + This function iterates over the memoization cache and reconstructs all valid binding sequences + that lead to the empty state. It yields each sequence tuple of Binding objects. + + Parameters + ---------- + state_key : tuple + The key representing the current state in the memoization cache. + memo : dict + The memoization cache containing state keys and their corresponding next steps. + + Returns + ------- + Iterator[tuple[Binding]] + An iterator yielding tuples of bindings representing valid sequences. + """ + next_steps = memo.get(state_key) + + if next_steps == FINAL_MARKER: + # If we reached the empty state, yield an empty sequence + yield () + return + + if not next_steps: + # Deadlock state; this should only happen when there are 0 valid sequences + # Do not yield anything + return + + for binding, next_state_key in next_steps: + # Recursively reconstruct sequences from the next state + for sub_sequence in _reconstruct_sequences(next_state_key, memo): + # Yield the current binding followed by the sub-sequence + yield (binding,) + sub_sequence + + +def _valid_sequences_to_ocel(valid_sequences_iter, idx_to_act, idx_to_ot, parameters): + """ + Converts the valid sequences of bindings into an OCEL object. + + Parameters + ---------- + valid_sequences_iter : iter + An iterator over valid sequences of bindings, where each sequence is a tuple of Binding objects + idx_to_act : dict + Mapping from indices to activity names + idx_to_ot : dict + Mapping from indices to object types + parameters : dict + Additional parameters for the conversion, including: + Parameters.EVENT_ID: The column name for event IDs + Parameters.OBJECT_ID: The column name for object IDs + Parameters.OBJECT_TYPE: The column name for object types + Parameters.EVENT_TIMESTAMP: The column name for event timestamps + Parameters.EVENT_ACTIVITY: The column name for event activities + + Returns + ------- + OCEL + The resulting OCEL object. + """ + event_id_column = exec_utils.get_param_value( + Parameters.EVENT_ID, parameters, constants.DEFAULT_EVENT_ID + ) + object_id_column = exec_utils.get_param_value( + Parameters.OBJECT_ID, parameters, constants.DEFAULT_OBJECT_ID + ) + object_type_column = exec_utils.get_param_value( + Parameters.OBJECT_TYPE, parameters, constants.DEFAULT_OBJECT_TYPE + ) + + event_activity = exec_utils.get_param_value( + Parameters.EVENT_ACTIVITY, parameters, constants.DEFAULT_EVENT_ACTIVITY + ) + event_timestamp = exec_utils.get_param_value( + Parameters.EVENT_TIMESTAMP, parameters, constants.DEFAULT_EVENT_TIMESTAMP + ) + objects_unique_per_sequence = exec_utils.get_param_value( + Parameters.OBJECTS_UNIQUE_PER_SEQUENCE, parameters, False + ) + # Convert all found traces to OCEL format + + # Create the OCEL object + events_list = [] + objects_list = [] + relations_list = [] + + all_objects_seen = set() + event_id_counter = 0 + # assigns to each event an increased timestamp from 1970 + curr_timestamp = 10000000 + + if objects_unique_per_sequence: + object_id_counter = 0 + + for sequence in valid_sequences_iter: + # For each sequence, create events and objects + for binding in sequence: + activity_id = binding[0] + consumed = binding[1] + produced = binding[2] + + act = idx_to_act[activity_id] + + # do not add START / END activities + if act.startswith("START_") or act.startswith("END_"): + continue + + # Create event + event_id = f"event_{event_id_counter}" + event_id_counter += 1 + curr_timestamp += 1 + + events_list.append( + { + event_id_column: event_id, + event_activity: act, + event_timestamp: pd.to_datetime(curr_timestamp, unit="s"), + } + ) + + # Create objects and relations + # consumed and produced contain the same objects; we only need to create them once + for _, ot_to_obj in consumed: + for ot_id, objects in ot_to_obj: + obj_type = idx_to_ot[ot_id] + for obj_id in objects: + if objects_unique_per_sequence: + obj_id = f"{obj_id}_{object_id_counter}" + + # Add object + if obj_id not in all_objects_seen: + all_objects_seen.add(obj_id) + objects_list.append( + {object_id_column: obj_id, object_type_column: obj_type} + ) + + # Add relation + relations_list.append( + { + event_id_column: event_id, + event_activity: act, + event_timestamp: pd.to_datetime( + curr_timestamp, unit="s" + ), + object_id_column: obj_id, + object_type_column: obj_type, + } + ) + if objects_unique_per_sequence: + object_id_counter += 1 + + # Convert to dataframes + events_df = pd.DataFrame(events_list) + objects_df = pd.DataFrame(objects_list) + relations_df = pd.DataFrame(relations_list) + + # Create the OCEL object + ocel = OCEL( + events=events_df, + objects=objects_df, + relations=relations_df, + parameters=parameters, + ) + + return ocel diff --git a/pm4py/algo/simulation/playout/ocpn/__init__.py b/pm4py/algo/simulation/playout/ocpn/__init__.py new file mode 100644 index 0000000000..5e229546e6 --- /dev/null +++ b/pm4py/algo/simulation/playout/ocpn/__init__.py @@ -0,0 +1,22 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.algo.simulation.playout.ocpn import algorithm, variants diff --git a/pm4py/algo/simulation/playout/ocpn/algorithm.py b/pm4py/algo/simulation/playout/ocpn/algorithm.py new file mode 100644 index 0000000000..10ca9706f2 --- /dev/null +++ b/pm4py/algo/simulation/playout/ocpn/algorithm.py @@ -0,0 +1,62 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.algo.simulation.playout.ocpn.variants import extensive +from pm4py.util import exec_utils +from enum import Enum +from pm4py.objects.ocpn.obj import OCPetriNet, OCMarking +from typing import Optional, Dict, Any +from pm4py.objects.ocel.obj import OCEL + + +class Variants(Enum): + EXTENSIVE = extensive + + +DEFAULT_VARIANT = Variants.EXTENSIVE +VERSIONS = {Variants.EXTENSIVE} + + +def apply(ocpn: OCPetriNet, initial_marking: OCMarking, final_marking: OCMarking, parameters: Optional[Dict[Any, Any]] = None, variant=DEFAULT_VARIANT) -> OCEL: + """ + Do the playout of an object-centric Petri net generating an OCEL 2.0. + + Parameters + ----------- + ocpn + Object-centric Petri net to play-out + initial_marking + Initial marking of the object-centric Petri net + final_marking + Final marking of the object-centric Petri net + parameters + Parameters of the algorithm + variant + Variant of the algorithm to use: + - Variants.EXTENSIVE: gets all the traces from the model. Can be expensive + + Returns + ----------- + OCEL + Object-centric event log generated by the playout of the object-centric Petri net + """ + return exec_utils.get_variant(variant).apply(ocpn, initial_marking, final_marking=final_marking, + parameters=parameters) diff --git a/pm4py/algo/simulation/playout/ocpn/variants/__init__.py b/pm4py/algo/simulation/playout/ocpn/variants/__init__.py new file mode 100644 index 0000000000..42360453a6 --- /dev/null +++ b/pm4py/algo/simulation/playout/ocpn/variants/__init__.py @@ -0,0 +1,22 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.algo.simulation.playout.ocpn.variants import extensive diff --git a/pm4py/algo/simulation/playout/ocpn/variants/extensive.py b/pm4py/algo/simulation/playout/ocpn/variants/extensive.py new file mode 100644 index 0000000000..fbf7e5797f --- /dev/null +++ b/pm4py/algo/simulation/playout/ocpn/variants/extensive.py @@ -0,0 +1,370 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +import random +import sys +from pm4py.algo.simulation.playout.ocpn.variants.utils import feasible_traces_to_ocel +from pm4py.objects.ocpn.semantics import OCPetriNetSemantics +from pm4py.objects.ocel.obj import OCEL +from pm4py.objects.ocpn.obj import OCPetriNet, OCMarking +from pm4py.objects.ocel import constants +from pm4py.util import exec_utils +from typing import Optional, Dict, Any, Union, List +from enum import Enum + + +class Parameters(Enum): + EVENT_ID = constants.PARAM_EVENT_ID + EVENT_ACTIVITY = constants.PARAM_EVENT_ACTIVITY + EVENT_TIMESTAMP = constants.PARAM_EVENT_TIMESTAMP + OBJECT_ID = constants.PARAM_OBJECT_ID + OBJECT_TYPE = constants.PARAM_OBJECT_TYPE + MAX_BINDINGS_PER_ACTIVITY = "maxBindingsPerActivity" + BRANCHING_FACTOR_TRANSITIONS = "branchingFactorTransitions" + BRANCHING_FACTOR_BINDINGS = "branchingFactorBindings" + OBJECTS_UNIQUE_PER_TRACE = "objects_unique_per_trace" + RETURN_TRACES = "return_traces" + EXISTS_TRACE = "exists_trace" + OCPETRINET_SEMANTICS = "ocpetrinet_semantics" + IS_FINAL_FUNC = "is_final_func" + + +FINAL_MARKER = "FINAL" + + +def apply( + net: OCPetriNet, + initial_marking: OCMarking, + final_marking: OCMarking, + parameters: Optional[Dict[Union[str, Parameters], Any]] = None, +) -> OCEL: + """ + Compute playout of an object-centric Petri net generating an OCEL (extensive search; + any activity may only be executed a limited number of times as specified). + Uses an optimized Memoized DFS Graph Population algorithm. + + Parameters + ----------- + net + Object-centric Petri net to play-out + initial_marking + Initial marking of the object-centric Petri net + final_marking + Final marking of the object-centric Petri net + parameters + Parameters of the algorithm: + Parameters.MAX_BINDINGS_PER_ACTIVITY -> Maximum bindings per activity (mandatory) + Parameters.EXISTS_TRACE -> If True, return a boolean indicating if at least one trace exists instead of an OCEL + Parameters.OBJECTS_UNIQUE_PER_TRACE: If True, objects in the resulting OCEL are make unique per trace (default: False) + Parameters.RETURN_TRACES -> If True, return traces instead of OCEL + Parameters.BRANCHING_FACTOR_TRANSITIONS -> Maximum number of transitions to explore from a single + state (default: sys.maxsize, i.e., no limit). If set to a float, it will be stochastically rounded to an integer for every state. + Parameters.BRANCHING_FACTOR_BINDINGS -> Maximum number of bindings to explore for a single transition + (default: sys.maxsize, i.e., no limit). If set to a float, it will be stochastically rounded to an integer for every state. + Parameters.OCPETRINET_SEMANTICS -> Object-centric Petri net semantics + Parameters.IS_FINAL_FUNC -> Function that given a marking and the final marking returns whether the final marking is reached. + (default: marking == final_marking) + """ + if parameters is None: + parameters = {} + + return_traces = exec_utils.get_param_value( + Parameters.RETURN_TRACES, parameters, False + ) + exists_trace = exec_utils.get_param_value( + Parameters.EXISTS_TRACE, parameters, False + ) + if Parameters.MAX_BINDINGS_PER_ACTIVITY not in parameters: + raise ValueError( + "Parameter MAX_BINDINGS_PER_ACTIVITY must be specified for the extensive playout. This parameter limits the maximum number of times an activity may be executed." + ) + max_bindings_per_activity = exec_utils.get_param_value( + Parameters.MAX_BINDINGS_PER_ACTIVITY, parameters, None + ) + # How many enabled transitions to explore from a single state + # deactivated by default + bf_trans = exec_utils.get_param_value( + Parameters.BRANCHING_FACTOR_TRANSITIONS, parameters, sys.maxsize + ) + # How many bindings to explore for a single transition + # deactivated by default + bf_binds = exec_utils.get_param_value( + Parameters.BRANCHING_FACTOR_BINDINGS, parameters, sys.maxsize + ) + semantics = exec_utils.get_param_value( + Parameters.OCPETRINET_SEMANTICS, + parameters, + OCPetriNetSemantics(), + ) + is_final = exec_utils.get_param_value( + Parameters.IS_FINAL_FUNC, + parameters, + _is_final + ) + + # Save transitions as ids for memory efficiency; create lookup table for conversion + all_transitions = sorted(list(net.transitions), key=lambda t: t.name) + transition_to_idx = {t: i for i, t in enumerate(all_transitions)} + + # State key: (marking, number of firings per transition) + # Transition firing counts; Position corresponds to transitions_to_idx table + initial_transition_counts = (0,) * len(all_transitions) + + initial_state_key = (initial_marking, initial_transition_counts) + + # Memoization cache: Dict[state_key, Union[Set[Tuple[binding, next_key]], str]] + # where the state_key is a tuple of (marking, transition_counts) and the + # value is either FINAL_MARKER if the state is final, + # or a set of tuples (binding, next_state_key) with all possible bindings + # in the given state key and the state key they lead to + memo = {} + + # == Phase 1: Memoization DFS Graph Population == + trace_exists = _populate_memo_graph( + initial_state_key, + net, + final_marking, + semantics, + transition_to_idx, + max_bindings_per_activity, + bf_trans, + bf_binds, + exists_trace, + is_final, + memo, + ) + + # If we are only interested in whether a trace exists, return immediately + if exists_trace: + return trace_exists + + + # == Phase 2: Reconstruct traces from memo == + feasible_traces_iter = _reconstruct_traces( + initial_state_key, memo, transition_to_idx, all_transitions + ) + + # == Phase 3: Return data in desired format == + # We did not save the object types of objects in the events, so we need to reconstruct them from the initial marking + # Maps object ids to their types + id_to_obj_type = dict() + # Every object can be found in the initial marking + for p, obj_ids in initial_marking.items(): + ot = p.object_type + for obj_id in obj_ids: + id_to_obj_type[obj_id] = ot + + if return_traces: + # Inverse the transition_to_idx mapping to get transition labels + idx_to_transition = {v: k for k, v in transition_to_idx.items()} + return (list(feasible_traces_iter), idx_to_transition, id_to_obj_type) + else: + return feasible_traces_to_ocel( + feasible_traces_iter, all_transitions, id_to_obj_type, parameters + ) + + +def _populate_memo_graph( + state_key: tuple, + net: OCPetriNet, + final_marking: OCMarking, + semantics, + t_to_idx: dict, + max_bindings: int, + bf_trans: float, + bf_binds: float, + exists_trace: bool, + is_final, + memo: dict, +) -> bool: + """ + Recursively explores the state space to build a compact, memoized graph of all valid traces. + + This function performs a depth-first search from a given state_key. It populates a memoization + cache (`memo`). For each state (defined by the state_key), it stores the set of "next steps" (as tuples of + (transition_index, binding, next_state_key)) that lie on a path to the final marking, + where binding is a set of object IDs that were bound to the transition. + + This approach avoids duplicate computation of two different traces leading to the same state key. + + Parameters + ----------- + state_key + A tuple `(OCMarking, tuple)` representing the current state of the search. + net + The object-centric Petri net being explored. + final_marking + The target OCMarking that signifies the end of a successful trace. + semantics + The OCPN semantics object used for enabling and firing transitions. + t_to_idx + A lookup dictionary mapping transition objects to their integer indices. + max_bindings + The maximum number of times any single transition is allowed to fire. + bf_trans + The max branching factor for transitions, limiting how many enabled transitions to explore. + If set to a float, it will be stochastically rounded to an integer. + bf_binds + The max branching factor for bindings, limiting how many bindings to explore for each transition. + If set to a float, it will be stochastically rounded to an integer. + exists_trace + If `True`, the function will return a boolean indicating if at least one trace exists that + leads to the final marking, rather than populating the memo with all valid traces. + is_final + A function to determine if a marking is the final marking. + memo + The memoization cache, a dictionary that is modified in place by the function. + It maps state_keys to the set of valid next steps or a special marker. + + Returns + ----------- + bool + Returns `True` if the final_marking is reachable from the given state_key, + otherwise returns `False`. + """ + if state_key in memo: + # a deadlock is indicated by an empty set in the memo. + # an entry that is not empty indicates that the final marking is reachable + return bool(memo[state_key]) + + # state_key has not been explored yet, so we explore it + marking, transition_counts = state_key + if is_final(marking, final_marking): + # signal that the final marking is reached + memo[state_key] = FINAL_MARKER + return True + + next_steps = set() + enabled_transitions = semantics.enabled_transitions(net, marking) + # Stochastically round bf_trans to an integer + rounded_bf_trans = int(bf_trans) + (1 if random.random() < (bf_trans % 1) else 0) + # select a random subset if branching factor is limited + transitions_to_explore = random.sample( + list(enabled_transitions), k=min(rounded_bf_trans, len(enabled_transitions)) + ) + + # explore all successor states by firing enabled transitions + for t in transitions_to_explore: + transition_idx = t_to_idx[t] + if transition_counts[transition_idx] >= max_bindings: + continue + + # Create new transition counts + new_transition_counts = list(transition_counts) + new_transition_counts[transition_idx] += 1 + new_counts_tuple = tuple(new_transition_counts) + + # Get all possible bindings for the transition + bindings = list(semantics.get_possible_bindings(net, t, marking)) + + # Stochastically round bf_binds to an integer + rounded_bf_binds = int(bf_binds) + ( + 1 if random.random() < (bf_binds % 1) else 0 + ) + # select a random subset of bindings if branching factor is limited + bindings_to_explore = random.sample( + bindings, k=min(rounded_bf_binds, len(bindings)) + ) + + for binding in bindings_to_explore: + new_marking = semantics.fire(net, t, marking, binding) + new_state_key = (new_marking, new_counts_tuple) + object_ids = [oi for objs in binding.values() for oi in objs] + + # Recursively explore the next state. Only add to memo it if it leads to the final state + if _populate_memo_graph( + new_state_key, + net, + final_marking, + semantics, + t_to_idx, + max_bindings, + bf_trans, + bf_binds, + exists_trace, + is_final, + memo, + ): + next_steps.add((transition_idx, frozenset(object_ids), new_state_key)) + if exists_trace: + # If we are only interested in whether a trace exists, we can return early + return True + + # Add all next steps to the memoization cache + # If state_key is a deadlock, it will be an empty set + memo[state_key] = next_steps + return bool(next_steps) + + +def _reconstruct_traces( + state_key: tuple, memo: dict, t_to_idx: dict, all_transitions: List +) -> iter: + """ + Recursively reconstructs all valid traces by traversing the pre-computed graph in the memo cache. + + This function is a generator that operates on the populated `memo` dictionary created by + `_populate_memo_graph`. It starts from a given state_key and recursively follows all valid + "next steps" stored in the cache. When it reaches a path's end (marked by the FINAL_MARKER), + it yields a complete trace. + + Parameters + ----------- + state_key + The tuple `(OCMarking, tuple)` representing the current state from which to reconstruct paths. + memo + The pre-populated memoization cache containing the valid path graph from the + `_populate_memo_graph` function. + t_to_idx + A lookup dictionary mapping transition objects to their integer indices. + all_transitions + A ordered list of all transition objects in the Petri net. + + Yields + ----------- + tuple + A single, complete trace. Each yielded trace is a tuple of event tuples, where each + event is `(transition_idx, frozenset(object_ids))`. + """ + next_steps = memo.get(state_key) + + if next_steps == FINAL_MARKER: + # If we reached the final marking, yield an empty trace + yield () + return + + if not next_steps: + # This should not happen unless the initial marking is a deadlock + return + + for t_idx, object_ids, next_state_key in next_steps: + current_event = (t_idx, object_ids) + + # Recursively yield all traces from the next state + for tail in _reconstruct_traces( + next_state_key, memo, t_to_idx, all_transitions + ): + # Prepend the current event + yield (current_event,) + tail + + +def _is_final(marking, final_marking): + return marking == final_marking \ No newline at end of file diff --git a/pm4py/algo/simulation/playout/ocpn/variants/utils.py b/pm4py/algo/simulation/playout/ocpn/variants/utils.py new file mode 100644 index 0000000000..b04d26e286 --- /dev/null +++ b/pm4py/algo/simulation/playout/ocpn/variants/utils.py @@ -0,0 +1,154 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from enum import Enum +import pandas as pd +from pm4py.objects.ocel.obj import OCEL +from pm4py.objects.ocel import constants +from pm4py.util import exec_utils + + +class Parameters(Enum): + EVENT_ID = constants.PARAM_EVENT_ID + EVENT_ACTIVITY = constants.PARAM_EVENT_ACTIVITY + EVENT_TIMESTAMP = constants.PARAM_EVENT_TIMESTAMP + OBJECT_ID = constants.PARAM_OBJECT_ID + OBJECT_TYPE = constants.PARAM_OBJECT_TYPE + OBJECTS_UNIQUE_PER_TRACE = "objects_unique_per_trace" + +def feasible_traces_to_ocel( + feasible_traces_iter, all_transitions, id_to_obj_type, parameters +): + """ + Converts the feasible traces into an OCEL object. + + Parameters + ---------- + feasible_traces_iter : iter + An iterator over feasible traces, where each trace is a tuple of events + all_transitions : list + Ordered list of all transitions in the object-centric Petri net + id_to_obj_type : dict + Mapping from object IDs to their types + parameters : dict + Additional parameters for the conversion + + Returns + ------- + OCEL + The resulting OCEL object + """ + event_id_column = exec_utils.get_param_value( + Parameters.EVENT_ID, parameters, constants.DEFAULT_EVENT_ID + ) + object_id_column = exec_utils.get_param_value( + Parameters.OBJECT_ID, parameters, constants.DEFAULT_OBJECT_ID + ) + object_type_column = exec_utils.get_param_value( + Parameters.OBJECT_TYPE, parameters, constants.DEFAULT_OBJECT_TYPE + ) + + event_activity = exec_utils.get_param_value( + Parameters.EVENT_ACTIVITY, parameters, constants.DEFAULT_EVENT_ACTIVITY + ) + event_timestamp = exec_utils.get_param_value( + Parameters.EVENT_TIMESTAMP, parameters, constants.DEFAULT_EVENT_TIMESTAMP + ) + objects_unique_per_trace = exec_utils.get_param_value( + Parameters.OBJECTS_UNIQUE_PER_TRACE, parameters, False + ) + # Convert all found traces to OCEL format + + # Create the OCEL object + events_list = [] + objects_list = [] + relations_list = [] + + all_objects_seen = set() + event_id_counter = 0 + # assigns to each event an increased timestamp from 1970 + curr_timestamp = 10000000 + + if objects_unique_per_trace: + object_id_counter = 0 + + # convert trace to set of events, objects, and relations + for trace in feasible_traces_iter: + for event in trace: + transition_idx, object_ids = event + transition = all_transitions[transition_idx] + + # Do not add silent transitions to the log + if transition.label is not None: + # Create event + event_id = f"event_{event_id_counter}" + event_id_counter += 1 + curr_timestamp += 1 + + events_list.append( + { + event_id_column: event_id, + event_activity: transition.label, + event_timestamp: pd.to_datetime(curr_timestamp, unit="s"), + } + ) + + # Create objects and relations + for obj_id in object_ids: + obj_type = id_to_obj_type[obj_id] + if objects_unique_per_trace: + obj_id = f"{obj_id}_{object_id_counter}" + + # Add object + if obj_id not in all_objects_seen: + all_objects_seen.add(obj_id) + objects_list.append( + {object_id_column: obj_id, object_type_column: obj_type} + ) + + # Add relation + relations_list.append( + { + event_id_column: event_id, + event_activity: transition.label, + event_timestamp: pd.to_datetime(curr_timestamp, unit="s"), + object_id_column: obj_id, + object_type_column: obj_type, + } + ) + if objects_unique_per_trace: + object_id_counter += 1 + + # Convert to dfs + events_df = pd.DataFrame(events_list) + objects_df = pd.DataFrame(objects_list) + relations_df = pd.DataFrame(relations_list) + + # Create the OCEL object + ocel = OCEL( + events=events_df, + objects=objects_df, + relations=relations_df, + parameters=parameters, + ) + + return ocel \ No newline at end of file diff --git a/pm4py/objects/oc_causal_net/__init__.py b/pm4py/objects/oc_causal_net/__init__.py new file mode 100644 index 0000000000..3715965053 --- /dev/null +++ b/pm4py/objects/oc_causal_net/__init__.py @@ -0,0 +1,25 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.util import constants as pm4_constants + +if pm4_constants.ENABLE_INTERNAL_IMPORTS: + from pm4py.objects.oc_causal_net import obj, converter, semantics, creation, utils \ No newline at end of file diff --git a/pm4py/objects/oc_causal_net/converter.py b/pm4py/objects/oc_causal_net/converter.py new file mode 100644 index 0000000000..84713401b5 --- /dev/null +++ b/pm4py/objects/oc_causal_net/converter.py @@ -0,0 +1,48 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.objects.oc_causal_net.variants import to_ocpn +from pm4py.util import exec_utils +from enum import Enum + +class Variants(Enum): + TO_OCPN = to_ocpn + +def apply(oc_causal_net, parameters=None, variant=Variants.TO_OCPN): + """ + Method for converting from Object-centric Causal Net Object-centric Petri Net + + Parameters + ----------- + oc_causal_net + Object-centric Causal net + parameters + Parameters of the algorithm + variant + Chosen variant of the algorithm: + - Variants.TO_OCPN + + Returns + ----------- + OCPetriNet + Object-centric Petri net converted from the Object-centric Causal Net + """ + return exec_utils.get_variant(variant).apply(oc_causal_net, parameters=parameters) \ No newline at end of file diff --git a/pm4py/objects/oc_causal_net/creation/__init__.py b/pm4py/objects/oc_causal_net/creation/__init__.py new file mode 100644 index 0000000000..78f40d6c46 --- /dev/null +++ b/pm4py/objects/oc_causal_net/creation/__init__.py @@ -0,0 +1,22 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.objects.oc_causal_net.creation import factory \ No newline at end of file diff --git a/pm4py/objects/oc_causal_net/creation/factory.py b/pm4py/objects/oc_causal_net/creation/factory.py new file mode 100644 index 0000000000..78c671af87 --- /dev/null +++ b/pm4py/objects/oc_causal_net/creation/factory.py @@ -0,0 +1,156 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from pm4py.objects.oc_causal_net.obj import OCCausalNet +import networkx as nx + + +def create_oc_causal_net(marker_groups): + """ + Create an object-centric causal net from a list of marker groups. + Does not consider activity counts or the relative occurence threshold. + May mutate the input data. + + Parameters + ---------- + marker_groups : dict[str, ] + Dict of marker groups per activity. Syntax: + { + "activity_name": { + "img": [ + [ + (activity, object_type, (min_count, max_count), marker_key), + // -1 for max_count = inf; 0 for unique marker key + ... + ], + ... + ], + "omg": [ + ... + ] + } + ] + } + + Returns + ------- + OCCausalNet + Object-centric causal net + """ + # infer activities + activities = set(marker_groups.keys()) + + # get input and output marker groups + input_marker_groups = {} + output_marker_groups = {} + + # make all keys=0 unique + # find max key + max_key = max( + [ + key + for groups in marker_groups.values() + for group in groups.get("img", []) + groups.get("omg", []) + for _, _, _, key in group + ], + default=0, + ) + key_counter = max_key + 1 + + # give markers with key=0 a unique key and set inf as max count if max count is -1 + for groups in marker_groups.values(): + for group in groups.get("img", []) + groups.get("omg", []): + for i, ( + related_activity, + object_type, + count_range, + marker_key, + ) in enumerate(group): + if marker_key == 0: + group[i] = ( + related_activity, + object_type, + ( + count_range + if count_range[1] != -1 + else (count_range[0], float("inf")) + ), + key_counter, + ) + key_counter += 1 + key_counter = max_key + 1 + + for activity, groups in marker_groups.items(): + img = groups.get("img", []) + omg = groups.get("omg", []) + + if img: + input_marker_groups[activity] = [ + OCCausalNet.MarkerGroup( + markers=[ + OCCausalNet.Marker( + related_activity, object_type, count_range, marker_key + ) + for related_activity, object_type, count_range, marker_key in group + ] + ) + for group in img + ] + if omg: + output_marker_groups[activity] = [ + OCCausalNet.MarkerGroup( + markers=[ + OCCausalNet.Marker( + related_activity, object_type, count_range, marker_key + ) + for related_activity, object_type, count_range, marker_key in group + ] + ) + for group in omg + ] + + # infer arcs from the marker groups + arcs = dict() + for activity in activities: + for group in output_marker_groups.get(activity, []): + for marker in group.markers: + related_activity = marker.related_activity + object_type = marker.object_type + if activity not in arcs: + arcs[activity] = {} + if related_activity not in arcs[activity]: + arcs[activity][related_activity] = {} + if object_type not in arcs[activity][related_activity]: + arcs[activity][related_activity][object_type] = {} + arcs[activity][related_activity][object_type] = { + "object_type": object_type + } + # create the dependency graph + dependency_graph = nx.MultiDiGraph(arcs) + + # create the object-centric causal net + occn = OCCausalNet( + dependency_graph, + output_marker_groups, + input_marker_groups, + ) + return occn \ No newline at end of file diff --git a/pm4py/objects/oc_causal_net/obj.py b/pm4py/objects/oc_causal_net/obj.py new file mode 100644 index 0000000000..2fca9a16fa --- /dev/null +++ b/pm4py/objects/oc_causal_net/obj.py @@ -0,0 +1,357 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from itertools import combinations +import networkx as nx +from pm4py.objects.oc_causal_net.utils.filters import filter4 +from typing import Tuple, List, Dict +from collections import Counter, defaultdict +from functools import cached_property + + +class OCCausalNet(object): + """ + Object-Centric Causal Net capturing dependency graph and marker groups. + """ + + class Marker(object): + """ + Represents a single marker in an object-centric causal net. + """ + + def __init__( + self, related_activity, object_type, count_range: Tuple, marker_key: int + ): + """ + Constructor + + Parameters + ---------- + related_activity : str + Activity that has to fulfill the marker (predecessor or successor) + object_type : str + object type of the marker + count_range : Tuple + Min and max number of markers consumable ('cardinalities') + marker_key : int + Key of the marker + """ + self.__related_activity = related_activity + self.__object_type = object_type + self.__count_range = count_range + self.__marker_key = marker_key + + def __repr__(self): + return f"(a={self.related_activity}, ot={self.object_type}, c={self.count_range}, k={self.marker_key})" + + def __str__(self): + return self.__repr__() + + def __hash__(self): + return hash( + ( + self.related_activity, + self.object_type, + self.min_count, + self.max_count, + self.marker_key, + ) + ) + + def __get_related_activity(self): + return self.__related_activity + + def __get_object_type(self): + return self.__object_type + + def __get_count_range(self): + return self.__count_range + + def __get_min_count(self): + return self.__count_range[0] + + def __get_max_count(self): + return self.__count_range[1] + + def __get_marker_key(self): + return self.__marker_key + + def __set_marker_key(self, marker_key: int): + self.__marker_key = marker_key + + def __eq__(self, other): + if isinstance(other, OCCausalNet.Marker): + return ( + self.related_activity == other.related_activity + and self.object_type == other.object_type + and self.min_count == other.min_count + and self.max_count == other.max_count + and self.marker_key == other.marker_key + ) + return False + + related_activity = property(__get_related_activity) + object_type = property(__get_object_type) + count_range = property(__get_count_range) + min_count = property(__get_min_count) + max_count = property(__get_max_count) + marker_key = property(__get_marker_key, __set_marker_key) + + class MarkerGroup(object): + """ + Represents a group of markers. A group of markers semantically + represents the AND gate of all markers in the group. + """ + + def __init__( + self, + markers: List["OCCausalNet.Marker"], + support_count: int = float("inf"), + ): + """ + Constructor + + Parameters + ---------- + markers : List[OCCausalNet.Marker] + List of markers that comprise the group + support_count : int + Frequency of this marker group in the event log. May be used to + filter infrequent marker groups. + Default is inf. + """ + self.__markers = markers + self.__support_count = support_count + + def __repr__(self): + return f"({self.markers}, count={self.support_count})" + + def __str__(self): + return self.__repr__() + + def __eq__(self, other): + if isinstance(other, OCCausalNet.MarkerGroup): + return ( + Counter(self.markers) == Counter(other.markers) + and self.support_count == other.support_count + ) + return False + + def __hash__(self): + return hash( + ( + frozenset(self.markers), + self.support_count, + ) + ) + + def __get_markers(self): + return self.__markers + + def __get_support_count(self): + return self.__support_count + + @cached_property + def dict_representation(self): + """ + Returns a dictionary representation of the marker group for + efficient checking if the marker group can be bound with a + given set of objects per related activity and object type. + Is only computed once and cached. + Is invalid if the marker group is changed after initialization. + Assumes that the marker group is valid, i.e., there is at most one marker per + related activity and object type. + + Returns + ------- + defaultdict[str, defaultdict[str, tuple[int, int]]] + Dictionary representation of the marker group, mapping + related activities to objects types to min and max cardinalities. + """ + result = defaultdict(lambda: defaultdict(lambda: (float("inf"), 0))) + for marker in self.markers: + related_activity = marker.related_activity + object_type = marker.object_type + result[related_activity][object_type] = ( + marker.min_count, + marker.max_count if marker.max_count != -1 else float("inf"), + ) + return result + + @cached_property + def key_constraints(self): + """ + Returns all tuples (related_activity, object_type, related_activity_2) that + cannot share objects due to having the same key. + Is only computed once and cached. + Is invalid if the marker group is changed after initialization. + + Returns + ------- + List[Tuple[str, str, str]] + List of tuples (related_activity, object_type, related_activity_2) + that cannot share the same marker key. + """ + # group related activities by (marker_key, object_type) + grouped = defaultdict(list) + for marker in self.markers: + grouped[(marker.marker_key, marker.object_type)].append( + marker.related_activity + ) + + # Generate constraints from groups with >= 2 elements + constraints = [] + for (marker_key, object_type), related_activities in grouped.items(): + if len(related_activities) > 1: + for act1, act2 in combinations(related_activities, 2): + constraints.append((act1, object_type, act2)) + + return constraints + + markers = property(__get_markers) + support_count = property(__get_support_count) + + def __init__( + self, + dependency_graph: nx.MultiDiGraph, + output_marker_groups: Dict[str, List["OCCausalNet.MarkerGroup"]], + input_marker_groups: Dict[str, List["OCCausalNet.MarkerGroup"]], + activity_count: Dict[str, int] = None, + relative_occurrence_threshold: float = 0, + ): + """ + Constructor + + Parameters + ---------- + dependency_graph : nx.MultiDiGraph + Object-centric dependency graph + Arc (a, object_type, a') must be encoded as dg[a][a'][object_type] = {"object_type": object_type} + output_marker_groups : Dict[str, List[OCCausalNet.MarkerGroup]] + Output marker groups per activity + input_marker_groups : Dict[str, List[OCCausalNet.MarkerGroup]] + Input marker groups per activity + activity_count : Dict[str, int] + Activity counts in the event log for filtering of infrequent marker groups. + relative_occurrence_threshold : float + Relative threshold for filtering infrequent marker groups. Range is [0,1]. + Default is 0, meaning no filtering. + """ + self.__dependency_graph = dependency_graph + self.__activities = list(dependency_graph._node.keys()) + if activity_count is None: + activity_count = {act: 1 for act in self.activities} + self.__edges = dependency_graph._succ + self.__relative_occurrence_threshold = relative_occurrence_threshold + self.__input_marker_groups, self.__output_marker_groups = filter4( + input_marker_groups, + output_marker_groups, + self.__relative_occurrence_threshold, + activity_count, + ) + self.__object_types = { + o.object_type + for binds in self.__input_marker_groups.values() + for bs in binds + for o in bs.markers + } + self.__activity_count = activity_count + + def __repr__(self): + # a OCCN is fully defined by its activities and marker groups + ret = f"Activities: {self.activities}" + for act in self.activities: + img = ( + self.input_marker_groups[act] if act in self.input_marker_groups else [] + ) + ret += f"\nInput_marker_groups[{act}]: {img}\n" + omg = ( + self.output_marker_groups[act] + if act in self.output_marker_groups + else [] + ) + ret += f"Output_marker_groups[{act}]: {omg}" + return ret + + def __str__(self): + return self.__repr__() + + def __hash__(self): + return id(self) + + def __eq__(self, other): + if isinstance(other, OCCausalNet): + return ( + set(self.activities) == set(other.activities) + and set(self.edges) == set(other.edges) + and all( + Counter(self.input_marker_groups.get(a, [])) + == Counter(other.input_marker_groups.get(a, [])) + for a in self.activities + ) + and all( + Counter(self.output_marker_groups.get(a, [])) + == Counter(other.output_marker_groups.get(a, [])) + for a in self.activities + ) + and set(self.object_types) == set(other.object_types) + and all( + self.activity_count.get(a, 0) == other.activity_count.get(a, 0) + for a in self.activities + ) + and self.relative_occurrence_threshold + == other.relative_occurrence_threshold + ) + return False + + def __get_dependency_graph(self): + return self.__dependency_graph + + def __get_activities(self): + return self.__activities + + def __get_edges(self): + return self.__edges + + def __get_input_marker_groups(self): + return self.__input_marker_groups + + def __get_output_marker_groups(self): + return self.__output_marker_groups + + def __get_object_types(self): + return self.__object_types + + def __get_activity_count(self): + return self.__activity_count + + def __get_relative_occurrence_threshold(self): + return self.__relative_occurrence_threshold + + dependency_graph = property(__get_dependency_graph) + activities = property(__get_activities) + edges = property(__get_edges) + input_marker_groups = property(__get_input_marker_groups) + output_marker_groups = property(__get_output_marker_groups) + object_types = property(__get_object_types) + activity_count = property(__get_activity_count) + relative_occurrence_threshold = property(__get_relative_occurrence_threshold) diff --git a/pm4py/objects/oc_causal_net/semantics.py b/pm4py/objects/oc_causal_net/semantics.py new file mode 100644 index 0000000000..1f2c786b3f --- /dev/null +++ b/pm4py/objects/oc_causal_net/semantics.py @@ -0,0 +1,1062 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from collections import Counter, defaultdict +import itertools +from typing import Any, Generic, Set, TypeVar, Union +from copy import deepcopy +from pm4py.objects.oc_causal_net.obj import OCCausalNet + + +N = TypeVar("N", bound=OCCausalNet) +M = TypeVar("M", bound=OCCausalNet.Marker) +MG = TypeVar("MG", bound=OCCausalNet.MarkerGroup) + + +class OCCausalNetState(Generic[N], defaultdict): + """ + The state of an object-centric causal net is a mapping from activities act to + multisets of outstanding obligations (act2, object_id, object_type) from act2 to act. + + ``` + state = OCCausalNetState({act: Counter([(act2, object_id, object_type)])}) + ``` + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initializes the OCCausalNetState, querying unspecified activities defaults to an empty multiset.""" + super().__init__(Counter) + data_args = args + if args and args[0] is Counter: + data_args = args[1:] + initial_data = dict(*data_args, **kwargs) + for act, obligations in initial_data.items(): + self[act] = Counter(obligations) + + def __hash__(self): + return frozenset( + (act, frozenset(counter.items())) + for act, counter in self.items() + if counter + ).__hash__() + + def __eq__(self, other): + if not isinstance(other, OCCausalNetState): + return False + return all( + self.get(a, Counter()) == other.get(a, Counter()) + for a in set(self.keys()) | set(other.keys()) + ) + + def __le__(self, other): + for a, self_counter in self.items(): + other_counter = other.get(a, Counter()) + # Every obligation count in self must be less than or equal to the count in other. + if not all( + other_counter.get(pred, 0) >= count + for pred, count in self_counter.items() + ): + return False + return True + + def __add__(self, other): + result = OCCausalNetState() + for a, self_counter in self.items(): + result[a] += self_counter + for a, other_counter in other.items(): + result[a] += other_counter + return result + + def __sub__(self, other): + result = OCCausalNetState() + for a, self_counter in self.items(): + diff = self_counter - other.get(a, Counter()) + if diff != Counter(): + result[a] = diff + return result + + def __repr__(self): + # e.g. [(a, o1[order], a'), ...] + sorted_entries = sorted(self.items(), key=lambda item: item[0]) + obligations = [ + f"({a}, {obj_id}[{ot}], {ot}):{count}" + for (a, obl) in sorted_entries + for ((a_prime, obj_id, ot), count) in obl + ] + return f'[{", ".join(obligations) if obligations else ""}]' + + def __str__(self): + return self.__repr__() + + def __deepcopy__(self, memodict={}): + new_state = OCCausalNetState() + memodict[id(self)] = new_state + for act, obligations in self.items(): + act_copy = ( + memodict[id(act)] if id(act) in memodict else deepcopy(act, memodict) + ) + counter_copy = ( + memodict[id(obligations)] + if id(obligations) in memodict + else deepcopy(obligations, memodict) + ) + new_state[act_copy] = counter_copy + return new_state + + @property + def activities(self) -> Set: + """ + Set of activities with outstanding obligations. + """ + return set(act for act in self.keys() if self[act]) + + +class OCCausalNetSemantics(Generic[N]): + """ + Class for the semantics of object-centric causal nets + """ + + @classmethod + def enabled_activities( + cls, + occn: N, + state: OCCausalNetState, + include_start_activities=False, + act_to_idx: dict = None, + ot_to_idx: dict = None, + ) -> Set: + """ + Returns the enabled activities in the given state. + + Parameters + ---------- + occn + Object-centric causal net + state + State of the OCCN + include_start_activities + True if start activities should be included in the set. + Start activities are always enabled. + act_to_idx + If activities are denoted in the state by an id instead of their name, + a dictionary mapping activities to their index has to be provided here. + ot_to_idx + If object types are denoted in the state by an id instead of their name, + a dictionary mapping object types to their index has to be provided here. + + Returns + ------- + set + Set of all activities enabled. + """ + return set( + act + for act in occn.activities + if (include_start_activities or not act.startswith("START_")) + and cls.is_enabled(occn, act, state, act_to_idx, ot_to_idx) + ) + + @classmethod + def is_enabled( + cls, + occn: N, + act: str, + state: OCCausalNetState, + act_to_idx: dict = None, + ot_to_idx: dict = None, + ) -> bool: + """ + Checks whether a given activity is enabled in a given object-centric + casal net and state. + An activity is enabled if there exists an input marker group that can be + bound. Start activities are always enabled. + + Parameters + ---------- + occn + Object-centric causal net + act + Activity to check + state + State of the OCCN + act_to_idx + If activities are denoted in the state by an id instead of their name, + a dictionary mapping activities to their index has to be provided here. + ot_to_idx + If object types are denoted in the state by an id instead of their name, + a dictionary mapping object types to their index has to be provided here. + + Returns + ------- + bool + true if enabled, false otherwise + """ + # Start activities are always enabled + if act.startswith("START_"): + return True + + if act_to_idx: + act_id = act_to_idx[act] + else: + act_id = act + + + # preprocess Counter for this activity to a lookup table where values + # are sets of objects with outstanding obligations to this act + objects = defaultdict(set) + for rel_act, obj_id, ot_id in state[act_id].keys(): + objects[(rel_act, ot_id)].add(obj_id) + + + + imgs = occn.input_marker_groups[act] + + # if there are not outstanding obligations, the activity cannot be enabled + if not state[act_id]: + return False + + # check each img + for img in imgs: + # markers that allow for consumption of 0 obligations do not need to be enabled + # at least one marker of the img needs to be enabled + one_marker_enabled = False + for marker in img.markers: + min_count = marker.min_count + rel_act_id = ( + marker.related_activity + if not act_to_idx + else act_to_idx[marker.related_activity] + ) + ot_id = ( + marker.object_type + if not ot_to_idx + else ot_to_idx[marker.object_type] + ) + + num_objects = len(objects[(rel_act_id, ot_id)]) + if num_objects >= max(min_count, 1): + one_marker_enabled = True + elif min_count != 0: + # marker is not enabled, move on to next img + one_marker_enabled = False + break + if one_marker_enabled: + return True + return False + + @classmethod + def _find_matching_marker_group( + cls, obligations: dict, marker_groups: list + ) -> Union[MG, None]: + """ + Finda a matching marker group that is able to consume/produce the given obligations. + + Parameters + ---------- + obligations : dict + Obligations to consume, mapping related activities to a dict mapping + object types to a set of object ids + marker_groups : list + List of marker groups to check + + Returns + ------- + Union[MG, None] + A matching marker group if found, None otherwise. + """ + if not obligations: + return None + # Calculate object counts from the obligations + obj_counts = { + related_act: { + ot: len(obligations[related_act][ot]) for ot in obligations[related_act] + } + for related_act in obligations + } + + # check each group + for mg in marker_groups: + mg_dict = mg.dict_representation + + # Check that markers for all required related activities exist + # and all required ots are present + failure = False + for related_act in obj_counts: + if related_act not in mg_dict: + failure = True + for ot in obj_counts[related_act]: + if ot not in mg_dict[related_act]: + failure = True + if failure: + continue + + # 1: check that count matches (= is within cardinality bounds) + counts_match = all( + mg_dict[related_act][ot][1] + >= obj_counts.get(related_act, {}).get(ot, 0) + >= mg_dict[related_act][ot][0] + for related_act in mg_dict + for ot in mg_dict[related_act] + ) + if not counts_match: + continue + + # 2: check key constraints + constraints_violated = any( + obligations.get(rel_act_1, {}) + .get(ot, set()) + .intersection(obligations.get(rel_act_2, {}).get(ot, set())) + for (rel_act_1, ot, rel_act_2) in mg.key_constraints + ) + if constraints_violated: + continue + + # found matching group + return mg + + return None + + @classmethod + def is_binding_enabled( + cls, + net: N, + act: str, + cons: dict[str, dict[str, Set]], + prod: dict[str, dict[str, Set]], + state: OCCausalNetState, + ) -> Union[tuple[MG, MG], None]: + """ + Checks whether the given binding is enabled in the object-centric causal net. + A binding is enabled if the activity has input and output marker groupos that + match the given objects and the state contains all necessary obligations. + + + Parameters + ---------- + net : N + The object-centric causal net + act : str + The activity to bind + cons : dict[str, dict[str, Set]] + The obligations to consume, mapping predecessor activities to a dict mapping + object types to a set of object ids + prod : dict[str, dict[str, Set]] + The obligations to produce, mapping successor activities to a dict mapping + object types to a set of object ids + state : OCCausalNetState + The current state of the OCCN + + Returns + ------- + Union[tuple[MG, MG], None]: + The input and output marker groups enabling the binding if it is enabled, None otherwise. + """ + # 1: check that all consumed obligations are present in the state + if cons: + if any( + state[act].get((pred, obj_id, ot), 0) <= 0 + for pred in cons.keys() + for ot in cons[pred].keys() + for obj_id in cons[pred][ot] + ): + return None + else: + if not prod: + # we need to either consume or produce obligations + return None + + # 2: Find a matching input marker group + if act.startswith("START_"): + if cons: + return None + # For START activities, we do not consume obligations, cons has to be empty + matched_img = None + else: + matched_img = cls._find_matching_marker_group( + cons, net.input_marker_groups[act] + ) + if matched_img is None: + return None + + # 3: Find a matching output marker group + if act.startswith("END_"): + if prod: + return None + # For START activities, we do not consume obligations, prod has to be empty + matched_omg = None + else: + matched_omg = cls._find_matching_marker_group( + prod, net.output_marker_groups[act] + ) + if matched_omg is None: + return None + + return (matched_img, matched_omg) + + @classmethod + def bind_activity( + cls, + net: N, + act: str, + cons: dict[str, dict[str, Set]], + prod: dict[str, dict[str, Set]], + state: OCCausalNetState, + ) -> OCCausalNetState: + """ + Binds an activity in the object-centric causal net. + For performance reasons, this method does not check whether the binding + is valid given the current state. If necessary, the caller should + ensure this, e.g., using the `is_binding_enabled` method. + + Parameters + ---------- + net : N + The object-centric causal net + act : str + The activity to bind + cons : dict[str, dict[str, Set]] + The obligations to consume, mapping predecessor activities to a dict mapping + object types to a set of object ids + prod : dict[str, dict[str, Set]] + The obligations to produce, mapping successor activities to a dict mapping + object types to a set of object ids + state : OCCausalNetState + The current state of the OCCN + + Returns + ------- + OCCausalNetState + The new state after binding the activity + """ + # consume obligations + if cons: + consume = OCCausalNetState( + { + act: Counter( + [ + (pred, obj_id, ot) + for pred in cons + for ot in cons[pred] + for obj_id in cons[pred][ot] + ] + ) + } + ) + state -= consume + + # produce obligations + if prod: + produce = OCCausalNetState( + { + succ: Counter( + [ + (act, obj_id, ot) + for ot in prod[succ] + for obj_id in prod[succ][ot] + ] + ) + for succ in prod + } + ) + state += produce + + return state + + @classmethod + def replay(cls, occn: N, sequence: tuple) -> bool: + """ + Replays a sequence of bindings on the object-centric causal net. + + Parameters + ---------- + occn : N + The object-centric causal net to replay on. + sequence : tuple + A sequence of bindings, where each binding is a tuple of (activity_name, consumed_obligations, produced_obligations). + consumed_obligations and produced_obligations are dictionaries mapping + related activities to a dict mapping object types to a set of object ids. + + Returns + ------- + bool + True if the sequence can be replayed on the net, False otherwise. + """ + # start in the empty state + state = OCCausalNetState() + + # replay each binding + for act, cons, prod in sequence: + if not cls.is_binding_enabled(occn, act, cons, prod, state): + return False + + state = cls.bind_activity(occn, act, cons, prod, state) + + # check if we are in the empty state + if state.activities: + return False + + return True + + @classmethod + def enabled_bindings_start_activity( + cls, + occn: N, + act: str, + object_type: str, + objects: set, + act_to_idx: dict = None, + ot_to_idx: dict = None, + ) -> tuple: + """ + Computes all enabled bindings for a start activity with a given set of objects. + These bindings will produce obligations for at least one of the objects. + Based on the `enabled_bindings` method, but specialized for start activities. + + Parameters + ---------- + occn : N + The object-centric causal net + act : str + The start activity to bind + object_type : str + The object type of the start activity + objects : set + A set of objects to bind to the activity. All bindings will bind at least one. + act_to_idx : dict, optional + If activities are denoted in the state by an id instead of their name, + a dictionary mapping activities to their index has to be provided here. + ot_to_idx : dict, optional + If object types are denoted in the state by an id instead of their name, + a dictionary mapping object types to their index has to be provided here. + + Returns + ------- + tuple + A tuple where entries are tuples of activity id, consumed objects, and produced objects. + Consumed / produces are tuples of (predecessor/successor activity id, objects_per_ot) + where objects_per_ot is a tuples of entries (object_type, objects) + where objects is a tuple (obj_id_1, obj_id_2, ...). + If act_to_idx and ot_to_idx are provided, indices instead of names are + used for activities and object types. + """ + assert act.startswith("START_"), "This method is only for start activities." + if act_to_idx: + act_id = act_to_idx[act] + else: + act_id = act + + if ot_to_idx: + ot_id = ot_to_idx[object_type] + else: + ot_id = object_type + + # create a list of fake consumed tuples that binds the powerset of objects + fake_consumed = [] + for i in range(len(objects)): + for combo in itertools.combinations(objects, i + 1): + fake_consumed.append(( + (-1, ( + (ot_id, combo), + ) + ) + ,)) + + # create the corresponding produced tuples + memo_produced = {} + memo_ot_assignments = {} + + enabled_bindings = [] + + for consumed in fake_consumed: + possible_produced_for_consumed = cls.__generate_produced_for_consumed( + occn, + act, + consumed, + memo_produced, + memo_ot_assignments, + act_to_idx, + ot_to_idx, + ) + + for produced in possible_produced_for_consumed: + enabled_bindings.append((act_id, None, produced)) + + return tuple(enabled_bindings) + + @classmethod + def enabled_bindings( + cls, + occn: N, + act: str, + state: OCCausalNetState, + act_to_idx: dict = None, + ot_to_idx: dict = None, + ) -> tuple: + """ + Computes all enabled bindings for a given activity in a given state. + + Parameters + ---------- + occn : N + The object-centric causal net + act : str + The activity to bind + state : OCCausalNetState + The current state of the OCCN + act_to_idx : dict, optional + If activities are denoted in the state by an id instead of their name, + a dictionary mapping activities to their index has to be provided here. + ot_to_idx : dict, optional + If object types are denoted in the state by an id instead of their name, + a dictionary mapping object types to their index has to be provided here. + + Returns + ------- + tuple + A tuple where entries are tuples of activity id, consumed objects, and produced objects. + Consumed / produces are tuples of (predecessor/successor activity id, objects_per_ot) + where objects_per_ot is a tuples of entries (object_type, objects) + where objects is a tuple (obj_id_1, obj_id_2, ...). + If act_to_idx and ot_to_idx are provided, indices instead of names are + used for activities and object types. + """ + if act_to_idx: + act_id = act_to_idx[act] + else: + act_id = act + + # outstanding obligations for the activity + obligations = state[act_id] + + # pre-process obligations to a dict where keys are (related activity, object_type) + # and values are sets of object ids (neglecting the count) + obligations_dict = defaultdict(set) + for (related_act, obj_id, ot_id), _ in obligations.items(): + obligations_dict[(related_act, ot_id)].add(obj_id) + + # ----------- Create all possibilities for consumed ----------- + + possible_consumed = cls.__generate_consumed( + occn, act, obligations_dict, act_to_idx, ot_to_idx + ) + if not possible_consumed: + return () + + # ----------- Create all possible produced tuples per consumed tuple ----------- + + final_bindings = [] + # Memoization for produced tuples, keyed by comsumed object sets by object type + memo_produced = {} + # Memoization for sub-problem of assigning objects of one ot for one omg + memo_ot_assignments = {} + + for consumed in possible_consumed: + # End activities do not produce obligations + if act.startswith("END_"): + possible_produced_for_consumed = {None} + else: + possible_produced_for_consumed = cls.__generate_produced_for_consumed( + occn, + act, + consumed, + memo_produced, + memo_ot_assignments, + act_to_idx, + ot_to_idx, + ) + + # Create final bindings + for produced in possible_produced_for_consumed: + final_bindings.append((act_id, consumed, produced)) + + return tuple(final_bindings) + + @classmethod + def __generate_consumed( + cls, + occn: N, + act: str, + obligations_dict: dict, + act_to_idx: dict = None, + ot_to_idx: dict = None, + ) -> Set[tuple]: + """ + Generates all possible consumed tuples for a given activity and obligations. + """ + possible_consumed = set() + for img in occn.input_marker_groups[act]: + img_dict = img.dict_representation + + # preprocess key constraints if ids are used + key_constraints_by_id = [] + if act_to_idx: + for rel_act_1_id, ot_id, rel_act_2_id in img.key_constraints: + key_constraints_by_id.append( + ( + act_to_idx[rel_act_1_id], + ot_to_idx[ot_id], + act_to_idx[rel_act_2_id], + ) + ) + else: + key_constraints_by_id = img.key_constraints + + # get all combinations on which objects we can consume given the img + keys, combinations_iter_list = ( + cls.__generate_predecessor_object_combinations( + img_dict, obligations_dict, act_to_idx, ot_to_idx + ) + ) + + if not keys: + # img not enabled + continue + + # from the consumed_per_pred, create all combinations of consumed obligations + # these are added to possible_consumed + cls.__consumed_from_predecessor_combinations( + possible_consumed, keys, combinations_iter_list, key_constraints_by_id + ) + + return possible_consumed + + @classmethod + def __generate_predecessor_object_combinations( + cls, + img_dict: dict, + obligations_dict: dict, + act_to_idx: dict = None, + ot_to_idx: dict = None, + ): + """ + Generates all combinations of objects from obligations_dict that can be consumed given the img_dict. + """ + # we build two lists: keys of format (predecessor_id, ot_id) and + # a list of iterations where each iterator yields all possible object + # combinations for all (predecessor, ot) pairs + keys = [] + combinations_iter_list = [] + + for predecessor in img_dict: + for ot in img_dict[predecessor]: + min_count, max_count = img_dict[predecessor][ot] + + if act_to_idx: + pred_id = act_to_idx[predecessor] + else: + pred_id = predecessor + + if ot_to_idx: + ot_id = ot_to_idx[ot] + else: + ot_id = ot + + # get all objects for obligations of the predecessor activity and object type + objects_for_pred = sorted(obligations_dict[(pred_id, ot_id)]) + + if len(objects_for_pred) < min_count: + # get to next img, this one cannot be bound + return [], [] + if not objects_for_pred: + # no objects available for this (pred, ot) pair, skip + continue + else: + # get all combinations of objects + keys.append((pred_id, ot_id)) + combinations_for_req = itertools.chain.from_iterable( + [ + itertools.combinations(objects_for_pred, r) + for r in range( + # this (pred, ot) pair may consume min_count to max_count objects + min_count, + min(max_count, len(objects_for_pred)) + 1, + ) + ] + ) + combinations_iter_list.append(combinations_for_req) + + return keys, combinations_iter_list + + @staticmethod + def __consumed_from_predecessor_combinations( + possible_consumed: set, + keys: list, + combinations_iter_list: list, + key_constraints_by_id: list, + ): + """ + Generates all consumed tuples given all possible assignments from + (predecessor, object type) tuples to sets of objects that may be consumed + by this pair. + """ + # create cross product of all options per predecessor and object type + cross_product_iter = itertools.product(*combinations_iter_list) + for binding_selection in cross_product_iter: + # create dict mapping predecessor activitiy to (ot_id, objects) tuples + grouped_by_pred = defaultdict(dict) + for (pred_id, ot_id), objects in zip(keys, binding_selection): + if len(objects) > 0: + grouped_by_pred[pred_id][ot_id] = objects + + # if all markers of the img are optional, we can skip empty bindings + if not grouped_by_pred: + continue + + # Check key constraints + constraint_violated = False + for rel_act_1_id, ot_id, rel_act_2_id in key_constraints_by_id: + objects1 = grouped_by_pred[rel_act_1_id].get(ot_id) + objects2 = grouped_by_pred[rel_act_2_id].get(ot_id) + if objects1 and objects2 and not objects1.isdisjoint(objects2): + # key constraint violated, move on to next binding + constraint_violated = True + break + + if constraint_violated: + continue + + # convert to memory-efficient tuple representation + consumed_tuple = tuple( + # sorting necessary to ensure duplicate-free tuples in possible_consumed + (pred_id, tuple(sorted(ot_obj_pairs.items()))) + for pred_id, ot_obj_pairs in sorted(grouped_by_pred.items()) + ) + # add to possible consumed obligations (duplicate-free) + possible_consumed.add(consumed_tuple) + + @classmethod + def __generate_produced_for_consumed( + cls, + occn: N, + act: str, + consumed: tuple, + memo_produced: dict, + memo_ot_assignments: dict, + act_to_idx: dict = None, + ot_to_idx: dict = None, + ) -> Set[tuple]: + """ + Generates all possible produced tuples for a given consumed tuple. + """ + # 1. Process consumed tuple into sets of consumed objects per ot + consumed_objects_by_ot = defaultdict(set) + for _, ot_obj_pairs in consumed: + for ot_id, objects in ot_obj_pairs: + consumed_objects_by_ot[ot_id].update(objects) + + # Create hashable key for memo + consumed_key = cls.__make_hashable(consumed_objects_by_ot) + + # 2. Check memo cache + if consumed_key in memo_produced: + possible_produced_for_consumed = memo_produced[consumed_key] + else: + # Compute all possible produced tuples + possible_produced_for_consumed = set() # set to avoid duplicates + # Compute per omg; cache to avoid recomputation + for omg in occn.output_marker_groups[act]: + produced_for_omg = cls.__generate_produced_for_omg( + omg, + consumed_objects_by_ot, + memo_ot_assignments, + act_to_idx, + ot_to_idx, + ) + possible_produced_for_consumed.update(produced_for_omg) + + # Store result in cache + memo_produced[consumed_key] = possible_produced_for_consumed + + return possible_produced_for_consumed + + @classmethod + def __generate_produced_for_omg( + cls, omg, consumed_objects_by_ot, memo, act_to_idx, ot_to_idx + ): + """ + Generates all possible produced tuples for a given output marker group and + consumed objects by object type (these need to be produced). + """ + omg_dict = omg.dict_representation + + # Pre-process omg requirements and key constraints into efficient lookups in case ids are used + reqs_by_ot = defaultdict(list) + key_constraints_by_ot = defaultdict(set) + + for succ_name, ot_map in omg_dict.items(): + succ_id = act_to_idx[succ_name] if act_to_idx else succ_name + for ot_name, (min_c, max_c) in ot_map.items(): + ot_id = ot_to_idx[ot_name] if ot_to_idx else ot_name + reqs_by_ot[ot_id].append((succ_id, min_c, max_c)) + + for s1_name, ot_name, s2_name in omg.key_constraints: + s1_id = act_to_idx[s1_name] if act_to_idx else s1_name + ot_id = ot_to_idx[ot_name] if ot_to_idx else ot_name + s2_id = act_to_idx[s2_name] if act_to_idx else s2_name + key_constraints_by_ot[ot_id].add(frozenset([s1_id, s2_id])) + + # Check if objects types match for early exit + consumed_ots = set(consumed_objects_by_ot.keys()) + required_ots = set(reqs_by_ot.keys()) + + if not consumed_ots.issubset(required_ots): + # omg has no marker for some consumed object type + return [] + + missing_required_ots = required_ots - consumed_ots + for missing_ot in missing_required_ots: + # marker has an ot that was not consumed + # this is an issue if that marker is not optional + if any(min_c > 0 for _, min_c, _ in reqs_by_ot[missing_ot]): + return [] + + # Get all possible assignments of successor activities to consumed objects + # this is done per individually per object type + ot_ids_in_order, assignments_in_order = cls.__generate_successor_assignments( + omg, memo, consumed_objects_by_ot, reqs_by_ot, key_constraints_by_ot + ) + + # Get cross product of all assignments per object type to get all possible produced tuples + final_produced_tuples = cls.__produced_from_successor_assignments( + ot_ids_in_order, assignments_in_order + ) + + return final_produced_tuples + + @staticmethod + def __generate_successor_assignments( + omg: MG, + memo: dict, + consumed_objects_by_ot: dict, + reqs_by_ot: dict, + key_constraints_by_ot: dict, + ): + """ + Generates all possible assignments of consumed objects to successor activities. + """ + # we generate all possible assignments of consumed objects to successor activities + # and then prune the ones that violate key constraints and + # the ones that violate the min/max cardinality requirements + ot_ids_in_order = [] + assignments_in_order = [] + # Solve assignment problem for each object type separetly + # this is cached since different consumed_objects_by_ot may have + # the same object sets for the same ot_id + for ot_id, objects in consumed_objects_by_ot.items(): + memo_key = (id(omg), ot_id, frozenset(objects)) + if memo_key in memo: + valid_ot_assignments = memo[memo_key] + else: + ot_reqs = reqs_by_ot.get(ot_id, []) + successors_for_ot = [req[0] for req in ot_reqs] + ot_key_constraints = key_constraints_by_ot.get(ot_id, set()) + + per_object_choices = [] + for obj in objects: + obj_choices = [] + # object may be assigned to 1 or more successors + for i in range(1, len(successors_for_ot) + 1): + # get all combinations of successors of size i + for succ_set in itertools.combinations(successors_for_ot, i): + # check key constraints + is_valid = all( + frozenset(pair) not in ot_key_constraints + for pair in itertools.combinations(succ_set, 2) + ) + if is_valid: + obj_choices.append(succ_set) + if not obj_choices: + return [], [] # An object has no valid assignment + per_object_choices.append(obj_choices) + + # Create all combinations of assignments with cross-product over all objects + valid_ot_assignments = [] + for assignment_choice in itertools.product(*per_object_choices): + final_assignment = defaultdict(list) + for obj, succs in zip(objects, assignment_choice): + for succ in succs: + final_assignment[succ].append(obj) + + # Check cardinality constraints + counts_ok = True + for succ_id, min_c, max_c in ot_reqs: + count = len(final_assignment.get(succ_id, [])) + if not (min_c <= count <= max_c): + counts_ok = False + break + + if counts_ok: + # add to valid assignments + canonical_assignment = { + s: tuple(sorted(o)) for s, o in final_assignment.items() + } + valid_ot_assignments.append(canonical_assignment) + + # Store in memoization cache + memo[memo_key] = valid_ot_assignments + + # Only proceed if we have valid assignments for this ot_id + if not valid_ot_assignments: + return [], [] + + # Store ot_id and valid assignments + ot_ids_in_order.append(ot_id) + assignments_in_order.append(valid_ot_assignments) + + return ot_ids_in_order, assignments_in_order + + @staticmethod + def __produced_from_successor_assignments( + ot_ids_in_order: list, assignments_in_order: list + ) -> Set[tuple]: + """ + Generates all produced tuples given all possible assignments from + successor activities to sets of objects that may be produced for this activity. + """ + final_produced_tuples = set() # avoid duplicates + for final_choice in itertools.product(*assignments_in_order): + # convert to dict first + produced_grouped_by_succ = defaultdict(dict) + + for ot_id, ot_assignment_dict in zip(ot_ids_in_order, final_choice): + for succ_id, assigned_objects in ot_assignment_dict.items(): + if len(assigned_objects) > 0: + produced_grouped_by_succ[succ_id][ot_id] = assigned_objects + + # Has to produce at least one object + if not produced_grouped_by_succ: + continue + + # Convert to tuple representation + produced_tuple = tuple( + (succ_id, tuple(sorted(ot_obj_pairs.items()))) + for succ_id, ot_obj_pairs in sorted(produced_grouped_by_succ.items()) + ) + final_produced_tuples.add(produced_tuple) + return final_produced_tuples + + @staticmethod + def __make_hashable(d): + """Helper function to create a hashable key from a dictionary.""" + # Converts a dict of {ot_id: set(obj_ids)} to a frozenset of items + # so it can be used as a dictionary key for memoization. + return frozenset((k, frozenset(v)) for k, v in d.items()) diff --git a/pm4py/objects/oc_causal_net/utils/__init__.py b/pm4py/objects/oc_causal_net/utils/__init__.py new file mode 100644 index 0000000000..e61ca4e91e --- /dev/null +++ b/pm4py/objects/oc_causal_net/utils/__init__.py @@ -0,0 +1,22 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.objects.oc_causal_net.utils import filters, occn_utils \ No newline at end of file diff --git a/pm4py/objects/oc_causal_net/utils/filters.py b/pm4py/objects/oc_causal_net/utils/filters.py new file mode 100644 index 0000000000..24666e8861 --- /dev/null +++ b/pm4py/objects/oc_causal_net/utils/filters.py @@ -0,0 +1,187 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + + +def filter4(input_marker_groups, output_marker_groups, threshold, activity_count): + """ + Filters input_marker_groups and output_marker_groups by relative support, then + recursively keeps only those MarkerGroups that remain connected. + + Parameters + ---------- + input_marker_groups : Dict[str, List[OCCausalNet.MarkerGroup]] + Input marker groups of the activities + output_marker_groups : Dict[str, List[OCCausalNet.MarkerGroup]] + Output marker groups of the activities + threshold : float + Minimum relative support = support_count / activity_count[activity] + activity_count : Dict[str,int] + Absolute frequency of each activity in the event log. + + Returns + ------- + Tuple[ + Dict[str, List[OCCausalNet.MarkerGroup]], + Dict[str, List[OCCausalNet.MarkerGroup]] + ] + (filtered_input_marker_groups, filtered_output_marker_groups) + """ + + def filterByTreshold(marker_groups, activity): + """ + Filters a list of MarkerGroups by relative support. + + Parameters + ---------- + marker_groups : List[OCCausalNet.MarkerGroup] + The MarkerGroups to filter. + activity : str + Activity name whose frequency is used for relative support. + + Returns + ------- + List[OCCausalNet.MarkerGroup] + Those groups whose support_count / activity_count[activity] > threshold. + """ + filteredMarkerGroups = [ + marker_group + for marker_group in marker_groups + if marker_group.support_count / activity_count[activity] > threshold + ] + return filteredMarkerGroups + + def getMostFrequent(marker_groups): + """ + Returns the most frequent MarkerGroup in the list. + + Parameters + ---------- + marker_groups : List[OCCausalNet.MarkerGroup] + List of marker groups to examine. + + Returns + ------- + OCCausalNet.MarkerGroup + The one with the highest support_count. + """ + try: + most_frequent_marker_group = marker_groups[ + [marker_group.support_count for marker_group in marker_groups].index( + max([marker_group.support_count for marker_group in marker_groups]) + ) + ] + except ValueError as e: + raise Exception(f"Invalid marker groups provided for some activity. Error: {e}") + return most_frequent_marker_group + + def getSubsequentInputMarkerGroups(most_frequent_marker_group, activity): + """ + For every marker in *most_frequent_marker_group* (which belongs to an + **output** marker group of *activity*) look up the corresponding input + marker groups of the marker's successor activity. + Keeps only the most frequent ones and adds them to the + *filtered_input_marker_groups* structure. + + Parameters + ---------- + most_frequent_marker_group : OCCausalNet.MarkerGroup + activity : str + """ + for marker in most_frequent_marker_group.markers: + succ_activity = marker.related_activity + if succ_activity in input_marker_groups.keys(): + possible_input_marker_groups = [ + x + for x in input_marker_groups[succ_activity] + if (activity, marker.object_type) + in [(y.related_activity, y.object_type) for y in x.markers] + ] + most_frequent_marker_group = getMostFrequent(possible_input_marker_groups) + addToFilteredInputMarkerGroups(most_frequent_marker_group, succ_activity) + + def addToFilteredOutputMarkerGroups(most_frequent_marker_group, activity): + """ + Inserts *most_frequent_marker_group* into *filtered_output_marker_groups* and + triggers the recursive traversal to the input side. + + Parameters + ---------- + most_frequent_marker_group : OCCausalNet.MarkerGroup + activity : str + """ + if most_frequent_marker_group not in filtered_output_marker_groups[activity]: + filtered_output_marker_groups[activity].append(most_frequent_marker_group) + getSubsequentInputMarkerGroups(most_frequent_marker_group, activity) + + def getSubsequentOutputMarkerGroups(most_frequent_marker_group, activity): + """ + For every marker in *most_frequent_marker_group* (which belongs to an + **input** marker_group of *activity*) look up the corresponding output + marker groups of the marker's predecessor activity. + Keeps only the most frequent ones and adds them to the + *filtered_output_marker_groups* structure. + + Parameters + ---------- + most_frequent_marker_group : OCCausalNet.MarkerGroup + activity : str + """ + for marker in most_frequent_marker_group.markers: + pred_activity = marker.related_activity + if pred_activity in output_marker_groups.keys(): + possible_output_marker_groups = [ + x + for x in output_marker_groups[pred_activity] + if (activity, marker.object_type) + in [(y.related_activity, y.object_type) for y in x.markers] + ] + most_frequent_marker_group = getMostFrequent(possible_output_marker_groups) + addToFilteredOutputMarkerGroups(most_frequent_marker_group, pred_activity) + + def addToFilteredInputMarkerGroups(most_frequent_marker_group, activity): + """ + Inserts *most_frequent_marker_group* into *filtered_input_marker_groups* and + triggers the recursive traversal to the output side. + + Parameters + ---------- + most_frequent_marker_group : OCCausalNet.MarkerGroup + activity : str + """ + if most_frequent_marker_group not in filtered_input_marker_groups[activity]: + filtered_input_marker_groups[activity].append(most_frequent_marker_group) + getSubsequentOutputMarkerGroups(most_frequent_marker_group, activity) + + filtered_output_marker_groups = {act: [] for act in output_marker_groups.keys()} + filtered_input_marker_groups = {act: [] for act in input_marker_groups.keys()} + + for act in filtered_output_marker_groups.keys(): + filteredMarkerGroups = filterByTreshold(output_marker_groups[act], act) + for marker_group in filteredMarkerGroups: + addToFilteredOutputMarkerGroups(marker_group, act) + + for act in filtered_input_marker_groups.keys(): + filteredMarkerGroups = filterByTreshold(input_marker_groups[act], act) + for marker_group in filteredMarkerGroups: + addToFilteredInputMarkerGroups(marker_group, act) + + return filtered_input_marker_groups, filtered_output_marker_groups diff --git a/pm4py/objects/oc_causal_net/utils/occn_utils.py b/pm4py/objects/oc_causal_net/utils/occn_utils.py new file mode 100644 index 0000000000..5314c17433 --- /dev/null +++ b/pm4py/objects/oc_causal_net/utils/occn_utils.py @@ -0,0 +1,95 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' + +from typing import Set + + + +def pre_set(occn, activity: str, object_type: str = None) -> Set: + """ + Returns the set of predecessor activities for a given activity in an object-centric causal net. + Restricted to predecessors connected using arcs of the specified object type, if provided. + + Parameters + ---------- + occn : OCCausalNet + The object-centric causal net to query + activity : str + The name of the activity for which to get the predecessors + object_type : str, optional + The object type to restrict the predecessors to (default is None) + + Returns + ------- + Set + Set of predecessor activities of the specified object type. + """ + if activity not in occn.activities: + return set() + + dg = occn.dependency_graph + + if object_type is None: + return dg.predecessors(activity) + + return { + predecessor + for predecessor, _, edge_key in dg.in_edges(activity, keys=True) + if edge_key == object_type + } + +def post_set(occn, activity: str, object_type: str = None) -> Set: + """ + Returns the set of successor activities for a given activity in an object-centric causal net. + Restricted to successors connected using arcs of the specified object type, if provided. + + Parameters + ---------- + occn : OCCausalNet + The object-centric causal net to query + activity : str + The name of the activity for which to get the successors + object_type : str, optional + The object type to restrict the successors to (default is None) + + Returns + ------- + Set + Set of successor activities of the specified object type. + """ + if activity not in occn.activities: + return set() + + dg = occn.dependency_graph + + if object_type is None: + return dg.successors(activity) + + return { + successor + for _, successor, edge_key in dg.out_edges(activity, keys=True) + if edge_key == object_type + } + + + + \ No newline at end of file diff --git a/pm4py/objects/oc_causal_net/variants/__init__.py b/pm4py/objects/oc_causal_net/variants/__init__.py new file mode 100644 index 0000000000..60d5d66ff2 --- /dev/null +++ b/pm4py/objects/oc_causal_net/variants/__init__.py @@ -0,0 +1,22 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.objects.oc_causal_net.variants import to_ocpn diff --git a/pm4py/objects/oc_causal_net/variants/to_ocpn.py b/pm4py/objects/oc_causal_net/variants/to_ocpn.py new file mode 100644 index 0000000000..1045acbcf1 --- /dev/null +++ b/pm4py/objects/oc_causal_net/variants/to_ocpn.py @@ -0,0 +1,1620 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from pm4py.objects.oc_causal_net.obj import OCCausalNet +from pm4py.objects.ocpn.obj import OCPetriNet + +# object type for binding places used to redstric the OCPN to firing one input and output marker group per firing of the activity +BINDING_OBJECT_TYPE = "_binding" + +# prefix for silent transitions in the OCPN +SILENT_TRANSITION_PREFIX = "_silent" + + +def apply(occn: OCCausalNet, parameters=None) -> OCPetriNet: + """ + Converts an Object-centric Causal Net (OCCN) to an Object-centric Petri Net (OCPN) with empty markings. + All cardinalities c!=(1,1) are treated as c=(0, *). Keys for input marker groups are ignored. + + Parameters + ---------- + occn: OCCausalNet + The Object-centric Causal Net to convert. Start and end activities must be labeled "START_{object_type}" and "END_{object_type}" respectively. + parameters: dict, optional + Additional parameters for the conversion (not used in this implementation). + + Returns + ------- + OCPetriNet + The converted Object-centric Petri Net with empty markings. + """ + activities = occn.activities + object_types = occn.object_types + img = occn.input_marker_groups + omg = occn.output_marker_groups + + assert ( + BINDING_OBJECT_TYPE not in object_types + ), f"The binding object type {BINDING_OBJECT_TYPE} must not be present in the OCCN." + + assert not ( + any(activity.startswith(SILENT_TRANSITION_PREFIX) for activity in activities) + ), f"The OCCN must not contain any activities with prefix {SILENT_TRANSITION_PREFIX}." + + assert ( + all(f"START_{object_type}" in activities for object_type in object_types) + ), "All object types must have a corresponding start activity labeled 'START_{object_type}'." + + assert ( + all(f"END_{object_type}" in activities for object_type in object_types) + ), "All object types must have a corresponding end activity labeled 'END_{object_type}'." + + ordered_key_groups_input = build_ordered_key_groups(activities, img) + ordered_key_groups_output = build_ordered_key_groups(activities, omg) + + places = set() + place_names = set() + transitions = set() + arcs = set() + + # create global input binding place + p_input_binding = OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}_global_input", + object_type=BINDING_OBJECT_TYPE, + ) + + places.add(p_input_binding) + + # recursively create OCPN for every activity + for activity in activities: + transform_activity( + activity=activity, + places=places, + places_names=place_names, + transitions=transitions, + arcs=arcs, + input_marker_groups=ordered_key_groups_input.get(activity, []), + output_marker_groups=ordered_key_groups_output.get(activity, []), + object_types=object_types, + p_input_binding=p_input_binding, + ) + + # create OCPN + ocpn = OCPetriNet( + places=places, + transitions=transitions, + arcs=arcs, + ) + + return ocpn + + + +def build_ordered_key_groups(activities, marker_groups): + """ + Builds a hierarchy of object type groups, key groups and markers for each activity in the given list of activities. + Object type groups are ordered by object type name. + Key groups are ordered by the number of markers and by the number of markers with min_count=1 and max_count=1. + Markers in key groups are ordered by min_count and max_count, effectively putting markers with (1,1) first. + + Parameters + ---------- + activities: list + List of activities for which to build the key groups. + marker_groups: dict + Dictionary mapping activities to their corresponding marker groups. + The keys are activity names and the values are lists of marker groups. + + Returns + ------- + dict + A dictionary where keys are activities and values are lists of lists of tuples (object_type, object_type_group), + where each object_type_group is a sorted list of tuples (marker_key, marker_key_group), + where each marker_key_group is a sorted list of markers. + """ + # build the hiearchy of key groups, object type groups and ks for each marker group + key_groups = {} + for a in activities: + key_groups[a] = [] + # key_groups[a] is a list of marker groups (dicts) + # key: values of a marker group is object type: object type group + # key: values of object type groups are marker_key: key group + for marker_group in marker_groups.get(a, []): + marker_group_dict = {} + for marker in marker_group.markers: + # add the marker to the key group + marker_key = marker.marker_key + object_type = marker.object_type + if object_type not in marker_group_dict: + marker_group_dict[object_type] = {} + if marker_key not in marker_group_dict[object_type]: + marker_group_dict[object_type][marker_key] = [] + marker_group_dict[object_type][marker_key].append(marker) + if marker_group_dict: + key_groups[a].append(marker_group_dict) + + + # sort the key groups + for a in activities: + for i, marker_group in enumerate(key_groups[a]): + for object_type in marker_group: + for marker_key in marker_group[object_type]: + # sort markers by c=(1,1) and then by min_count and max_count + marker_group[object_type][marker_key].sort( + key=lambda m: (m.min_count != 1, m.max_count != 1, m.min_count, m.max_count) + ) + # sort key groups where key groups are ordered by the number of markers. + # Of the same number, key groups with more markers with c=(1,1) come first, + # effectively putting key groups with size 1 with 1 marker with c=(1,1) first + marker_group[object_type] = sorted( + marker_group[object_type].items(), + key=lambda x: ( + len(x[1]), # fewer markers first + -sum( + 1 for m in x[1] if m.min_count == 1 and m.max_count == 1 + ), # more (1,1) markers first + ), + ) + # sort the object type groups by alphabetical order of object types + marker_group = sorted( + marker_group.items(), key=lambda x: x[0] # sort by object type name + ) + key_groups[a][i] = marker_group + # marker groups for an activity do not need to be sorted + return key_groups + + +def transform_activity( + places, + places_names, + transitions, + arcs, + activity, + input_marker_groups, + output_marker_groups, + object_types, + p_input_binding +): + """ + Creates the necessary places, transitions, and arcs for an activity in the OCPN. + + Parameters + ---------- + places: set + Set of places in the OCPN. + places_names: set + Set of place names in the OCPN to avoid duplications. + transitions: set + Set of transitions in the OCPN. + arcs: set + Set of arcs in the OCPN. + activity: str + The name of the activity to transform. + input_marker_groups: list + List of input marker groups for the activity, where each marker group is a list of object type groups. + output_marker_groups: list + List of output marker groups for the activity, where each marker group is a list of object type groups. + object_types: set + Set of all object types in the OCPN. + p_input_binding: OCPetriNet.Place + The input binding place for the activity construct. + """ + is_start = activity.startswith("START_") + if is_start: + assert not input_marker_groups, f"Start activity {activity} is a START activity and must not have input marker groups." + is_end = activity.startswith("END_") + if is_end: + assert not output_marker_groups, f"End activity {activity} is an END activity and must not have output marker groups." + + # transition + t = OCPetriNet.Transition(name=activity, label=activity) + transitions.add(t) + + + # input and output places + non_variable_object_types, variable_object_types = get_activity_object_types( + object_types, input_marker_groups, output_marker_groups + ) + + + for object_type in non_variable_object_types | variable_object_types: + is_variable = object_type in variable_object_types + + p_i = add_place( + OCPetriNet.Place( + name=label_activity_place(activity, True, object_type), + object_type=object_type, + ), + places, + places_names + ) + p_o = add_place( + OCPetriNet.Place( + name=label_activity_place(activity, False, object_type), + object_type=object_type, + ), + places, + places_names + ) + + # arcs for input and output places + add_arc( + OCPetriNet.Arc( + source=p_i, + target=t, + object_type=object_type, + is_variable=is_variable, + ), + arcs + ) + add_arc( + OCPetriNet.Arc( + source=t, + target=p_o, + object_type=object_type, + is_variable=is_variable, + ), + arcs + ) + + # binding places + if not is_start: + p_i_t = add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{activity}_input", + object_type=BINDING_OBJECT_TYPE + ), + places, + places_names, + ) + if not is_end: + p_o_t = add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{activity}_output", + object_type=BINDING_OBJECT_TYPE + ), + places, + places_names, + ) + + # arcs for binding places + if is_start: + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t, + object_type=BINDING_OBJECT_TYPE, + ), + arcs + ) + else: + add_arc( + OCPetriNet.Arc( + source=p_i_t, + target=t, + object_type=BINDING_OBJECT_TYPE, + ), + arcs + ) + if is_end: + add_arc( + OCPetriNet.Arc( + source=t, + target=p_input_binding, + object_type=BINDING_OBJECT_TYPE, + ), + arcs + ) + else: + add_arc( + OCPetriNet.Arc( + source=t, + target=p_o_t, + object_type=BINDING_OBJECT_TYPE, + ), + arcs + ) + + # add marker groups + for marker_group in input_marker_groups: + transform_marker_group( + activity=activity, + places=places, + places_names=places_names, + transitions=transitions, + arcs=arcs, + marker_group=marker_group, + p_input_binding=p_input_binding, + p_output_binding=p_i_t, + is_output_marker_group=False, + ) + for marker_group in output_marker_groups: + transform_marker_group( + activity=activity, + places=places, + places_names=places_names, + transitions=transitions, + arcs=arcs, + marker_group=marker_group, + p_input_binding=p_o_t, + p_output_binding=p_input_binding, + is_output_marker_group=True, + ) + + + + +def get_activity_object_types(object_types, input_marker_groups, output_marker_groups): + """ + Returns a set of non-variable object types and a set of variable object types for the given marker groups. + Non-variable object types are those where exactly one object of that type is involved in every firing of the activity. + + Parameters + ---------- + object_types: set + Set of all object types in the OCPN. + input_marker_groups: list + List of input marker groups for the activity, where each marker group is a list of object type groups. + output_marker_groups: list + List of output marker groups for the activity, where each marker group is a list of object type groups. + + Returns + ------- + tuple + A tuple containing two sets: + - The first set contains non-variable object types. + - The second set contains variable object types. + """ + found_object_types = set() + # all object types are non-variable unless found otherwise + non_variable_object_types = {ot: True for ot in object_types} + + for marker_group in input_marker_groups + output_marker_groups: + for object_type in object_types: + # Find object type group in the marker_group + # (needs to be done like this to catch object types only present in input or output markers) + matches = [otg for otg in marker_group if otg[0] == object_type] + if len(matches) > 1: + raise ValueError(f"More than one object type group found for object type {object_type} in marker_group.") + object_type_group = matches[0] if matches else (object_type, []) + + object_type = object_type_group[0] + if matches: + found_object_types.add(object_type) + markers = [m for key_group in object_type_group[1] for m in key_group[1]] + # non-variable object types have exactly one marker in its object type group and its cardinalities are (1,1) + if not ( + len(markers) == 1 + and markers[0].min_count == 1 + and markers[0].max_count == 1 + ): + # not a non-variable object type + non_variable_object_types[object_type] = False + + variable_object_types = set( + ot for ot in found_object_types if not non_variable_object_types[ot] + ) + non_variable_object_types = set( + ot for ot in found_object_types if non_variable_object_types[ot] + ) + + return non_variable_object_types, variable_object_types + + +def transform_marker_group( + activity, + places, + places_names, + transitions, + arcs, + marker_group, + p_input_binding, + p_output_binding, + is_output_marker_group, +): + """ + Transforms a marker group into a construct of places, transitions, and arcs in the OCPN. + Places, transitions, and arcs are updated to include the marker group. + + Parameters + ---------- + activity: str + The name of the activity which the marker group belongs to. + places: set + Set of places in the OCPN. + places_names: set + Set of place names in the OCPN to avoid duplications. + transitions: set + Set of transitions in the OCPN. + arcs: set + Set of arcs in the OCPN. + marker_group: list + The marker group to transform, which is a list of object type groups (tuples of object type and list of key groups). + p_input_binding: OCPetriNet.Place + The input binding place for the marker group construct. + p_output_binding: OCPetriNet.Place + The output binding place for the marker group construct. + is_output_marker_group: bool + Indicates if the marker group is an output marker group (True) or an input marker group (False). + """ + silent_id = get_next_id() + p_b_i = p_input_binding + p_b_o = ( + p_output_binding + if len(marker_group) == 1 + else add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_1", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + ) + for i, object_type_group in enumerate(marker_group): + # create construct + + if is_output_marker_group: + transform_output_object_type_group( + activity=activity, + places=places, + places_names=places_names, + transitions=transitions, + arcs=arcs, + object_type_group=object_type_group, + p_input_binding=p_b_i, + p_output_binding=p_b_o, + ) + else: + transform_input_object_type_group( + activity=activity, + places=places, + places_names=places_names, + transitions=transitions, + arcs=arcs, + object_type_group=object_type_group, + p_input_binding=p_b_i, + p_output_binding=p_b_o, + ) + + # next input and output binding places + p_b_i = p_b_o + # for last object type group, p_output_binding is used. We check for >= in case len is 1. + p_b_o = ( + p_output_binding + if (i >= len(marker_group) - 2) + else add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_{i + 2}", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + ) + + +def transform_input_object_type_group( + activity, + places, + places_names, + transitions, + arcs, + object_type_group, + p_input_binding, + p_output_binding, +): + """ + Transforms an object type group of an input marker group into a construct of places, transitions, and arcs in the OCPN. + Places, transitions, and arcs are updated to include the object type group. + For input marker groups, we treat all keys as unique. + + Parameters + ---------- + activity: str + The name of the activity which the object type group belongs to. + places: set + Set of places in the OCPN. + places_names: set + Set of place names in the OCPN to avoid duplications. + transitions: set + Set of transitions in the OCPN. + arcs: set + Set of arcs in the OCPN. + object_type_group: (object_type, list) + The object type group to transform, which is a tuple of the object type and a sorted list of key groups. + p_input_binding: OCPetriNet.Place + The input binding place for the object type group construct. + p_output_binding: OCPetriNet.Place + The output binding place for the object type group construct. + """ + silent_id = get_next_id() + # we skip the key groups for input marker groups, as we ignore input marker keys + # grab all markers of the object type group + markers = [marker for key_group in object_type_group[1] for marker in key_group[1]] + # sort them by by c=(1,1) and then by min_count and max_count + markers.sort(key=lambda m: (m.min_count != 1, m.max_count != 1, m.min_count, m.max_count)) + + # proceed with the construct + ot = object_type_group[0] + p_b_i = p_input_binding + p_b_o = ( + p_output_binding + if len(markers) == 1 + else add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_1", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + ) + for i, marker in enumerate(markers): + # transform the marker + # input and output place + pi = add_place( + OCPetriNet.Place( + name=label_arc_place(marker.related_activity, activity, ot), + object_type=ot, + ), + places, + places_names, + ) + po = add_place( + OCPetriNet.Place( + name=label_activity_place(activity, True, ot), object_type=ot + ), + places, + places_names, + ) + # create construct + transform_marker( + places=places, + places_names=places_names, + transitions=transitions, + arcs=arcs, + marker=marker, + p_input=pi, + p_output=po, + p_input_binding=p_b_i, + p_output_binding=p_b_o, + is_output_marker=False, + key_group_length=1, # does not matter for input markers + is_last_key_group=True, # does not matter for input markers + is_first_marker=i==0 + ) + + # next input and output binding places + p_b_i = p_b_o + # for last key group, p_output_binding is used. We check for >= in case len is 1. + p_b_o = ( + p_output_binding + if (i >= len(markers) - 2) + else add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_{i + 2}", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + ) + + +def transform_output_object_type_group( + activity, + places, + places_names, + transitions, + arcs, + object_type_group, + p_input_binding, + p_output_binding, +): + """ + Transforms an object type group of an output marker group into a construct of places, transitions, and arcs in the OCPN. + Places, transitions, and arcs are updated to include the object type group. + + Parameters + ---------- + activity: str + The name of the activity which the object type group belongs to. + places: set + Set of places in the OCPN. + places_names: set + Set of place names in the OCPN to avoid duplications. + transitions: set + Set of transitions in the OCPN. + arcs: set + Set of arcs in the OCPN. + object_type_group: (object_type, list) + The object type group to transform, which is a tuple of the object type and a sorted list of key groups. + p_input_binding: OCPetriNet.Place + The input binding place for the object type group construct. + p_output_binding: OCPetriNet.Place + The output binding place for the object type group construct. + """ + silent_id = get_next_id() + p_b_i = p_input_binding + p_b_o = ( + p_output_binding + if len(object_type_group[1]) == 1 + else add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_1", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + ) + for i, key_group in enumerate(object_type_group[1]): + # transform the key group + is_last_key_group = i == len(object_type_group[1]) - 1 + transform_output_key_group( + activity=activity, + places=places, + places_names=places_names, + transitions=transitions, + arcs=arcs, + key_group=key_group, + p_input_binding=p_b_i, + p_output_binding=p_b_o, + is_last_key_group=is_last_key_group, + ) + + # next input and output binding places + p_b_i = p_b_o + # for last key group, p_output_binding is used. We check for >= in case len is 1. + p_b_o = ( + p_output_binding + if (i >= len(object_type_group[1]) - 2) + else add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_{i + 2}", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + ) + + +def transform_output_key_group( + activity, + places, + places_names, + transitions, + arcs, + key_group, + p_input_binding, + p_output_binding, + is_last_key_group, + p_input=None, +): + """ + Transforms a key group of an output marker group into a construct of places, transitions, and arcs in the OCPN. + Places, transitions, and arcs are updated to include the key group. + + Parameters + ---------- + activity: str + The name of the activity which the key group belongs to. + places: set + Set of places in the OCPN. + places_names: set + Set of place names in the OCPN to avoid duplications. + transitions: set + Set of transitions in the OCPN. + arcs: set + Set of arcs in the OCPN. + key_group: (marker_key, list) + The key group to transform, which is a tupel of the marker key and a sorted list of markers. + p_input_binding: OCPetriNet.Place + The input binding place for the key group construct. + p_output_binding: OCPetriNet.Place + The output binding place for the key group construct. + is_last_key_group: bool + Indicates if the key group is the last key group of its object type group. + p_input: OCPetriNet.Place, optional + The input place for case 2 of this construct. Only to be set when recursively calling this function from case 3. + """ + ot = key_group[1][0].object_type + # 3 cases can occur + if len(key_group[1]) == 1: + # case 1: key group with one marker + marker = key_group[1][0] + # input and output place + pi = add_place( + OCPetriNet.Place( + name=label_activity_place(activity, False, ot), object_type=ot + ), + places, + places_names, + ) + po = add_place( + OCPetriNet.Place( + name=label_arc_place(activity, marker.related_activity, ot), + object_type=ot, + ), + places, + places_names, + ) + # create construct + transform_marker( + places=places, + places_names=places_names, + transitions=transitions, + arcs=arcs, + marker=marker, + p_input=pi, + p_output=po, + p_input_binding=p_input_binding, + p_output_binding=p_output_binding, + is_output_marker=True, + key_group_length=len(key_group[1]), + is_last_key_group=is_last_key_group, + is_first_marker=True + ) + else: + silent_id = get_next_id() + if is_last_key_group: + # case 2: key group with multiple markers, last key group of its object type group + p_b_i = p_input_binding + p_b_o = add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_1", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + + for i, marker in enumerate(key_group[1]): + # input and output place + pi = ( + p_input + if p_input is not None + else add_place( # p_input is set when recursively calling this function from case 3 + OCPetriNet.Place( + name=label_activity_place(activity, False, ot), + object_type=ot, + ), + places, + places_names, + ) + ) + po = add_place( + OCPetriNet.Place( + name=label_arc_place(activity, marker.related_activity, ot), + object_type=ot, + ), + places, + places_names, + ) + # create construct + transform_marker( + places=places, + places_names=places_names, + transitions=transitions, + arcs=arcs, + marker=marker, + p_input=pi, + p_output=po, + p_input_binding=p_b_i, + p_output_binding=p_b_o, + is_output_marker=True, + key_group_length=len(key_group[1]), + is_last_key_group=is_last_key_group, + is_first_marker=i == 0, + ) + + # next input and output binding places + p_b_i = p_b_o + # for last marker, p_output_binding is used + p_b_o = ( + p_output_binding + if i >= (len(key_group[1]) - 2) + else add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_{i + 2}", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + ) + else: + # case 3: key group with multiple markers, not the last key group of its object type group + # this is the same as case 2 with some extra on top, so we create the extra and thenn call case 2 + + # places + px = add_place( + OCPetriNet.Place( + name=f"p#{silent_id}_X", + object_type=ot, + ), + places, + places_names, + ) + p_alpha = add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_alpha", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + p_beta = add_place( + OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}_beta", + object_type=BINDING_OBJECT_TYPE, + ), + places, + places_names, + ) + # transitions + t1 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_1", + ) + t2 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_2", + ) + transitions.update([t1, t2]) + # arcs + p_a_o_ot = add_place( + OCPetriNet.Place( + name=label_activity_place(activity, False, ot), + object_type=ot, + ), + places, + places_names, + ) + add_arc( + OCPetriNet.Arc( + source=p_a_o_ot, + target=t1, + object_type=ot, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_a_o_ot, + object_type=ot, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=px, + object_type=ot, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_a_o_ot, + target=t2, + object_type=ot, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=px, + object_type=ot, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t2, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=p_alpha, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_alpha, + target=t1, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_beta, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + + # call case 2 by setting is_last_key_group to True, provide px as input place + transform_output_key_group( + activity=activity, + places=places, + places_names=places_names, + transitions=transitions, + arcs=arcs, + key_group=key_group, + p_input_binding=p_beta, + p_output_binding=p_output_binding, + is_last_key_group=True, + p_input=px, + ) + + +def transform_marker( + places, + places_names, + transitions, + arcs, + marker, + p_input, + p_output, + p_input_binding, + p_output_binding, + is_output_marker, + key_group_length, + is_last_key_group, + is_first_marker +): + """ + Transforms a marker into a construct of places, transitions, and arcs in the OCPN. + Places, transitions, and arcs are updated to include the marker. + + Parameters + ---------- + places: set + Set of places in the OCPN. + places_names: set + Set of place names in the OCPN to which the place name will be added. + transitions: set + Set of transitions in the OCPN. + arcs: set + Set of arcs in the OCPN. + marker: Marker + The marker to transform. + p_input: OCPetriNet.Place + The input place for the marker construct. + p_output: OCPetriNet.Place + The output place for the marker construct. + p_input_binding: OCPetriNet.Place + The input binding place for the marker construct. + p_output_binding: OCPetriNet.Place + The output binding place for the marker construct. + is_output_marker: bool + True if the marker is part of an output marker group, False if it is part of an input marker group. + key_group_length: int + The length of the key group to which the marker belongs. Does not matter for input markers. + is_last_key_group: bool + Indicates if the marker is in the last key group of its object type group. Does not matter for input markers. + is_first_marker: bool + Indicates if the marker is the first marker in its input object type group. Does not matter for output markers. + """ + # id to avoid name clashes + silent_id = get_next_id() + + # 6 cases can occur + if marker.min_count == 1 and marker.max_count == 1: + if is_output_marker and not is_last_key_group and key_group_length == 1: + # case 1: (1,1) marker with duplication + # places + for p in [p_input, p_output, p_input_binding, p_output_binding]: + add_place(p, places, places_names) + # transitions + t1 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_1", + ) + t2 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_2", + ) + transitions.update([t1, t2]) + # arcs + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t1, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_input, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_output, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t2, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=p_output, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t1, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t2, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_output_binding, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=p_output_binding, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + elif (not is_output_marker) and (not is_first_marker): + # case 2: (1,1) marker with unification + # places + for p in [p_input, p_output, p_input_binding, p_output_binding]: + add_place(p, places, places_names) + # transitions + t1 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_1", + ) + t2 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_2", + ) + transitions.update([t1, t2]) + # arcs + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t1, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_output, + target=t1, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_output, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t2, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=p_output, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t1, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t2, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_output_binding, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=p_output_binding, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + else: + # case 2: (1,1) marker without duplication/unification + # places + places.update([p_input, p_output, p_input_binding, p_output_binding]) + # transitions + t = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}", + ) + transitions.add(t) + # arcs + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t, + target=p_output, + object_type=marker.object_type, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t, + target=p_output_binding, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + else: + # marker with min_count != 1 or max_count != 1 + if is_output_marker and not is_last_key_group and key_group_length == 1: + # case 4: square marker with duplication + # places + px = OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}", + object_type=BINDING_OBJECT_TYPE, + ) + places.update([p_input, p_output, p_input_binding, p_output_binding, px]) + # transitions + t1 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_1", + ) + t2 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_2", + ) + transitions.update([t1, t2]) + # arcs + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t1, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_input, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_output, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t2, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=p_output, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t2, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=px, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=px, + target=t1, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_output_binding, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + elif (not is_output_marker) and (not is_first_marker): + # case 5: square marker with unification + # places + px = OCPetriNet.Place( + name=f"p{BINDING_OBJECT_TYPE}#{silent_id}", + object_type=BINDING_OBJECT_TYPE, + ) + places.update([p_input, p_output, p_input_binding, p_output_binding, px]) + # transitions + t1 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_1", + ) + t2 = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}_2", + ) + transitions.update([t1, t2]) + # arcs + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t1, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_output, + target=t1, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=p_output, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t2, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=p_output, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t1, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t1, + target=px, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=px, + target=t2, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t2, + target=p_output_binding, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + else: + # case 6: square marker without duplication/unification + # places + places.update([p_input, p_output, p_input_binding, p_output_binding]) + # transitions + t = OCPetriNet.Transition( + name=f"{SILENT_TRANSITION_PREFIX}#{silent_id}", + ) + transitions.add(t) + # arcs + add_arc( + OCPetriNet.Arc( + source=p_input, + target=t, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t, + target=p_output, + object_type=marker.object_type, + is_variable=True, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=p_input_binding, + target=t, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + add_arc( + OCPetriNet.Arc( + source=t, + target=p_output_binding, + object_type=BINDING_OBJECT_TYPE, + is_variable=False, + ), + arcs, + ) + + +def add_arc(arc, arcs): + """ + Adds an arc to the OCPN and its source and target. + + Parameters + ---------- + arc: OCPetriNet.Arc + The arc to add to the OCPN. + arcs: set + Set of arcs in the OCPN to which the arc will be added. + """ + arcs.add(arc) + arc.source.add_out_arc(arc) + arc.target.add_in_arc(arc) + + +def add_place(place, places, places_names) -> OCPetriNet.Place: + """ + Adds a place to the OCPN and its name to the set of place names. + Does not add the place if a place with the same name already exists in the OCPN. + Returns the place if it was added, otherwise returns the existing place. + + Parameters + ---------- + place: OCPetriNet.Place + The place to add to the OCPN. + places: set + Set of places in the OCPN to which the place will be added. + places_names: set + Set of place names in the OCPN to which the place name will be added. + + Returns + ------- + OCPetriNet.Place + The place that was added or the existing place if it was not added. + """ + if place.name not in places_names: + places.add(place) + places_names.add(place.name) + return place + else: + # if the place already exists, return the existing place + for p in places: + if p.name == place.name: + return p + raise ValueError(f"Place with name {place.name} not found in places.") + + +def label_activity_place(activity_name, is_input, object_type): + """ + Returns the name for an input / output place of an activity. + + Parameters + ---------- + activity_name: str + The name of the activity. + is_input: bool + Indicates if the place is an input place (True) or an output place (False). + object_type: str + The object type of the activity. + + Returns + ------- + str + The name of the place. + """ + prefix = f"p_{activity_name}_i" if is_input else f"p_{activity_name}_o" + return f"{prefix}_{object_type}" + + +def label_arc_place(source, target, object_type): + """ + Returns the name for the place corresponding to an arc between two activities. + + Parameters + ---------- + source: str + The name of the source activity. + target: str + The name of the target activity. + object_type: str + The object type of the arc. + """ + return f"p_arc({source},{target})_{object_type}" + + +def get_next_id(): + """ + Returns a unique id for a silent transition, starting from 0. + """ + # initialize on first call + if not hasattr(get_next_id, "counter"): + get_next_id.counter = 0 + current = get_next_id.counter + get_next_id.counter += 1 + return current diff --git a/pm4py/objects/ocpn/__init__.py b/pm4py/objects/ocpn/__init__.py new file mode 100644 index 0000000000..93114d503c --- /dev/null +++ b/pm4py/objects/ocpn/__init__.py @@ -0,0 +1,25 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.util import constants as pm4_constants + +if pm4_constants.ENABLE_INTERNAL_IMPORTS: + from pm4py.objects.ocpn import obj, converter, factory, semantics, utils \ No newline at end of file diff --git a/pm4py/objects/ocpn/converter.py b/pm4py/objects/ocpn/converter.py new file mode 100644 index 0000000000..70b21e1b5c --- /dev/null +++ b/pm4py/objects/ocpn/converter.py @@ -0,0 +1,47 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.objects.ocpn.variants import to_oc_causal_net, to_alternative_format +from pm4py.util import exec_utils +from enum import Enum + +class Variants(Enum): + TO_OC_CAUSAL_NET = to_oc_causal_net + TO_ALTERNATIVE_FORMAT = to_alternative_format + +def apply(ocpn, parameters=None, variant=Variants.TO_OC_CAUSAL_NET): + """ + Method for converting an Object-centric Petri Net to Object-centric Causal Net + + Parameters + ----------- + ocpn: OCPetriNet + Object-centric Petri net + parameters: dict, optional + Parameters of the algorithm + variant + Chosen variant of the algorithm + + Returns + ----------- + Conversion result + """ + return exec_utils.get_variant(variant).apply(ocpn, parameters=parameters) \ No newline at end of file diff --git a/pm4py/objects/ocpn/factory.py b/pm4py/objects/ocpn/factory.py new file mode 100644 index 0000000000..1c95ef72f2 --- /dev/null +++ b/pm4py/objects/ocpn/factory.py @@ -0,0 +1,116 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from collections import Counter +import uuid +from pm4py.objects.ocpn.obj import OCMarking, OCPetriNet +from typing import Dict, Any + +from pm4py.objects.petri_net.obj import PetriNet + + +def create(ocpn: Dict[str, Any]) -> OCPetriNet: + """ + Creates an Object-centric Petri net object from its dictionary representation + specified in pm4py.algo.discovery.ocel.ocpn.variants.classic. + Only considers the properties `activities`, `petri_nets`, and `double_arcs_on_activity`. + All other information is ignored. + + Parameters + ----------------- + ocpn + Dictionary containing the properties of the Object-centric Petri net + + Returns + ---------------- + OCPetriNet + Object-centric Petri net object + """ + activities = ocpn["activities"] + petri_nets = ocpn["petri_nets"] + double_arcs_on_activity = ocpn["double_arcs_on_activity"] + + places = dict() + unlabeled_transitions = dict() + arcs = [] + initial_marking = OCMarking() + final_marking = OCMarking() + + # Labeled transitions + labeled_transitions = {label: OCPetriNet.Transition(label=label, name=str(uuid.uuid4())) for label in activities} + + for ot, net in petri_nets.items(): + pn, im, fm = net + + # transitions + for t in pn.transitions: + if not t.label: + # labeled transitions are already in labeled_transitions + name = f"{ot}_{t.name}" # make name unique + unlabeled_transitions[name] = OCPetriNet.Transition(name=name) + + # places + for p in pn.places: + name = f"{ot}_{p.name}" # make name unique + places[name] = OCPetriNet.Place(name=name, object_type=ot) + + # arcs + for arc in pn.arcs: + is_double = False + if isinstance(arc.source, PetriNet.Transition): + if arc.source.label: + source = labeled_transitions[arc.source.label] + is_double = double_arcs_on_activity[ot][arc.source.label] + else: + source = unlabeled_transitions[f"{ot}_{arc.source.name}"] + target = places[f"{ot}_{arc.target.name}"] + elif isinstance(arc.source, PetriNet.Place): + source = places[f"{ot}_{arc.source.name}"] + if arc.target.label: + target = labeled_transitions[arc.target.label] + is_double = double_arcs_on_activity[ot][arc.target.label] + else: + target = unlabeled_transitions[f"{ot}_{arc.target.name}"] + else: + raise ValueError("Unknown arc source type") + + # check for double arc + + a = OCPetriNet.Arc(source=source, target=target, object_type=ot, is_variable=is_double) + arcs.append(a) + source.add_out_arc(a) + target.add_in_arc(a) + + # markings + for p in im: + initial_marking += OCMarking({places[f"{ot}_{p.name}"]: Counter([f"{ot}_{i}" for i in range(im[p])])}) + for p in fm: + final_marking += OCMarking({places[f"{ot}_{p.name}"]: Counter([f"{ot}_{i}" for i in range(fm[p])])}) + + # create the OCPetriNet object + ocpn_obj = OCPetriNet( + places = set(places.values()), + transitions = set(labeled_transitions.values()) | set(unlabeled_transitions.values()), + arcs = set(arcs), + initial_marking=initial_marking, + final_marking=final_marking, + ) + return ocpn_obj \ No newline at end of file diff --git a/pm4py/objects/ocpn/obj.py b/pm4py/objects/ocpn/obj.py new file mode 100644 index 0000000000..afbe116660 --- /dev/null +++ b/pm4py/objects/ocpn/obj.py @@ -0,0 +1,435 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from collections import Counter, defaultdict +from copy import deepcopy +from typing import Collection, Dict, Any, Set +from pm4py.objects.petri_net.obj import PetriNet + + +class OCMarking(defaultdict): + """ + An object-centric marking represented as a mapping from places to multisets of object IDs. + + ``` + marking = OCMarking({p: Counter(["object1", "object2"])}) + ``` + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initializes the OCMarking, querying unspecified places defaults to an empty multiset.""" + super().__init__(Counter) + data_args = args + if args and args[0] is Counter: + data_args = args[1:] + initial_data = dict(*data_args, **kwargs) + for place, objects in initial_data.items(): + self[place] = Counter(objects) + + def __hash__(self): + return frozenset( + (place, frozenset(counter.items())) + for place, counter in self.items() + if counter + ).__hash__() + + def __eq__(self, other): + if not isinstance(other, OCMarking): + return False + return all( + self.get(p, Counter()) == other.get(p, Counter()) + for p in set(self.keys()) | set(other.keys()) + ) + + def __le__(self, other): + for place, self_counter in self.items(): + other_counter = other.get(place, Counter()) + # Every object count in self must be less than or equal to the count in other. + if not all( + other_counter.get(obj_id, 0) >= count + for obj_id, count in self_counter.items() + ): + return False + return True + + def __add__(self, other): + result = OCMarking() + for place, self_counter in self.items(): + result[place] += self_counter + for place, other_counter in other.items(): + result[place] += other_counter + return result + + def __sub__(self, other): + result = OCMarking() + for place, self_counter in self.items(): + diff = self_counter - other.get(place, Counter()) + if diff != Counter(): + result[place] = diff + return result + + def __repr__(self): + # e.g. ["p1:{o1, o2}", "p2: {o2, o3}", …] + sorted_entries = sorted(self.items(), key=lambda item: item[0].name) + return ( + ", ".join(f"{place.name}: {objects}" for (place, objects) in sorted_entries) + if sorted_entries + else "[]" + ) + + def __str__(self): + return self.__repr__() + + def __deepcopy__(self, memodict={}): + new_marking = OCMarking() + memodict[id(self)] = new_marking + for place, objects in self.items(): + place_copy = ( + memodict[id(place)] + if id(place) in memodict + else deepcopy(place, memodict) + ) + counter_copy = ( + memodict[id(objects)] + if id(objects) in memodict + else deepcopy(objects, memodict) + ) + new_marking[place_copy] = counter_copy + return new_marking + + @property + def places(self) -> Set: + """ + Returns the set of all places in the marking that contain tokens. + + Returns + ------------ + Set[str] + Set of place names in the marking. + """ + return set([p for p in self.keys() if self[p]]) + + +class OCPetriNet(PetriNet): + class Place(PetriNet.Place): + def __init__( + self, name, object_type, in_arcs=None, out_arcs=None, properties=None + ): + """ + Constructor + + Parameters + ------------ + name + human-readable identifier + object_type + the type/color of objects this place holds + in_arcs + set of incoming arcs + out_arcs + set of outgoing arcs + properties + dict of additional properties + """ + super().__init__( + name, in_arcs=in_arcs, out_arcs=out_arcs, properties=properties + ) + self.__object_type = object_type + + def add_in_arc(self, arc): + """ + Adds an incoming arc to the place. + + Parameters + ------------ + arc: OCPetriNet.Arc + the arc to add + """ + self.__in_arcs.add(arc) + assert arc.target == self + assert arc.object_type == self.object_type + + def add_out_arc(self, arc): + """ + Adds an outgoing arc to the place. + + Parameters + ------------ + arc: OCPetriNet.Arc + the arc to add + """ + self.__out_arcs.add(arc) + assert arc.source == self + assert arc.object_type == self.object_type + + def __get_object_type(self): + return self.__object_type + + def __repr__(self): + return f"{self.name}[{self.object_type}]" + + def __deepcopy__(self, memodict={}): + if id(self) in memodict: + return memodict[id(self)] + new_place = OCPetriNet.Place( + self.name, self.object_type, properties=self.properties + ) + memodict[id(self)] = new_place + # attached arcs + for arc in self.in_arcs: + arc_copy = deepcopy(arc, memodict) + new_place.in_arcs.add(arc_copy) + for arc in self.out_arcs: + arc_copy = deepcopy(arc, memodict) + new_place.out_arcs.add(arc_copy) + + return new_place + + object_type = property(__get_object_type) + + class Transition(PetriNet.Transition): + def add_in_arc(self, arc): + """ + Adds an incoming arc to the place. + + Parameters + ------------ + arc: OCPetriNet.Arc + the arc to add + """ + self.__in_arcs.add(arc) + assert arc.target == self + + def add_out_arc(self, arc): + """ + Adds an outgoing arc to the place. + + Parameters + ------------ + arc: OCPetriNet.Arc + the arc to add + """ + self.__out_arcs.add(arc) + assert arc.source == self + + + class Arc(PetriNet.Arc): + def __init__( + self, + source, + target, + object_type, + is_variable=False, + properties=None, + ): + """ + Constructor + + Parameters + ------------ + source + source place / transition + target + target place / transition + is_variable + whether the arc is a variable arc + properties + dict of additional properties + """ + super().__init__(source, target, weight=1, properties=properties) + self.__object_type = object_type + self.__is_variable = is_variable + + def __get_object_type(self): + return self.__object_type + + def __get_is_variable(self): + return self.__is_variable + + def __repr__(self): + base = super().__repr__() + var = "variable" if self.is_variable else "non-variable" + return f"{base}:{self.object_type}:{var}" + + def __deepcopy__(self, memodict={}): + if id(self) in memodict: + return memodict[id(self)] + new_source = memodict.get(id(self.source), deepcopy(self.source, memodict)) + new_target = memodict.get(id(self.target), deepcopy(self.target, memodict)) + new_arc = OCPetriNet.Arc( + new_source, + new_target, + self.object_type, + weight=self.weight, + is_variable=self.is_variable, + properties=self.properties, + ) + memodict[id(self)] = new_arc + # reattach + new_source.out_arcs.add(new_arc) + new_target.in_arcs.add(new_arc) + return new_arc + + object_type = property(__get_object_type) + is_variable = property(__get_is_variable) + + def __init__( + self, + name: str = None, + places: Collection[Place] = None, + transitions: Collection[Transition] = None, + arcs: Collection[Arc] = None, + initial_marking: OCMarking = None, + final_marking: OCMarking = None, + properties: Dict[str, Any] = None, + ): + """ + Constructor + + Parameters + ------------ + name + human-readable identifier + places + collection of places + transitions + collection of transitions + arcs + collection of arcs + initial_marking + initial marking of the net + final_marking + final marking of the net + properties + dict of additional properties + """ + super().__init__( + name=name, + places=places, + transitions=transitions, + arcs=arcs, + properties=properties, + ) + self.__initial_marking = initial_marking + self.__final_marking = final_marking + self.__assert_well_formed() + + def __get_initial_marking(self): + return self.__initial_marking + + def __get_final_marking(self): + return self.__final_marking + + def __deepcopy__(self, memodict={}): + new_net = OCPetriNet(self.name) + memodict[id(self)] = new_net + for p in self.places: + p_copy = OCPetriNet.Place(p.name, p.object_type, properties=p.properties) + new_net.places.add(p_copy) + memodict[id(p)] = p_copy + for t in self.transitions: + t_copy = OCPetriNet.Transition(t.name, t.label, properties=t.properties) + new_net.transitions.add(t_copy) + memodict[id(t)] = t_copy + for a in self.arcs: + src = memodict[id(a.source)] + tgt = memodict[id(a.target)] + a_copy = OCPetriNet.Arc( + src, + tgt, + a.object_type, + is_variable=a.is_variable, + properties=a.properties, + ) + src.out_arcs.add(a_copy) + tgt.in_arcs.add(a_copy) + new_net.arcs.add(a_copy) + memodict[id(a)] = a_copy + return new_net + + def __repr__(self): + ret = [f"OCPN {self.name}:\nobject_types: ["] + object_types_rep = [] + for ot in self.object_types: + object_types_rep.append(ot) + object_types_rep.sort() + ret.append(" " + ", ".join(object_types_rep) + " ") + ret.append("]\nplaces: [") + places_rep = [] + for place in self.places: + places_rep.append(repr(place)) + places_rep.sort() + ret.append(" " + ", ".join(places_rep) + " ") + ret.append("]\ntransitions: [") + trans_rep = [] + for trans in self.transitions: + trans_rep.append(repr(trans)) + trans_rep.sort() + ret.append(" " + ", ".join(trans_rep) + " ") + ret.append("]\narcs: [") + arcs_rep = [] + for arc in self.arcs: + arcs_rep.append(repr(arc)) + arcs_rep.sort() + ret.append(" " + ", ".join(arcs_rep) + " ") + ret.append("]\ninitial_marking: [") + initial_marking_rep = [repr(self.initial_marking)] + ret.append(" " + ", ".join(initial_marking_rep) + " ") + ret.append("]\nfinal_marking: [") + final_marking_rep = [repr(self.final_marking)] + ret.append(" " + ", ".join(final_marking_rep) + " ") + ret.append("]") + return "".join(ret) + + def __str__(self): + return self.__repr__() + + def __assert_well_formed(self): + """ + Asserts that the OCPN is well-formed, i.e., all transitions have, + for each object type, only either variable or non-variable arcs, but not both. + """ + for t in self.transitions: + for ot in self.object_types: + var_arcs = { + a for a in t.in_arcs if a.is_variable and a.object_type == ot + } + non_var_arcs = { + a for a in t.in_arcs if not a.is_variable and a.object_type == ot + } + if var_arcs and non_var_arcs: + raise ValueError(f"Transition {t} is not well-formed.") + + initial_marking = property(__get_initial_marking) + final_marking = property(__get_final_marking) + + @property + def object_types(self) -> Set[str]: + """ + Returns the set of all object types (colors) used in this net. + + Returns + ------------ + Set[str] + Set of object types (colors) used in this net. + """ + return {p.object_type for p in self.places} diff --git a/pm4py/objects/ocpn/semantics.py b/pm4py/objects/ocpn/semantics.py new file mode 100644 index 0000000000..65ee89364c --- /dev/null +++ b/pm4py/objects/ocpn/semantics.py @@ -0,0 +1,453 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from collections import defaultdict +import copy +from itertools import chain, combinations +from typing import Counter, Generic, TypeVar, Dict, Set, FrozenSet, Tuple, Iterator +from pm4py.objects.ocpn.obj import OCPetriNet, OCMarking +from pm4py.objects.ocpn.utils.ocpn_utils import pre_set + +N = TypeVar("N", bound=OCPetriNet) +T = TypeVar("T", bound=OCPetriNet.Transition) +P = TypeVar("P", bound=OCPetriNet.Place) + + +class OCPetriNetSemantics(Generic[N]): + + @classmethod + def is_enabled(cls, pn: N, transition: T, marking: OCMarking) -> bool: + """ + Checks whether a given transition is enabled in a given object-centric Petri net and marking. + A transition is enabled if every non-variable arc has at least one object in the corresponding place, + and at least one arc can consume a token. + + Parameters + ---------- + net + object-centric Petri net + transition + transition to check + marking + marking to check + + Returns + ------- + bool + true if enabled, false otherwise + """ + if transition not in pn.transitions: + return False + + any_enabled = False + for a in transition.in_arcs: + if marking[a.source] == Counter(): + if not a.is_variable: + return False + else: + any_enabled = True + return any_enabled + + @classmethod + def is_binding_enabled(cls, pn: N, transition: T, marking: OCMarking, objects: Dict[str, Set]) -> bool: + """ + Checks whether a given binding is enabled for a transition in a given object-centric Petri net and marking. + + Parameters + ---------- + pn + object-centric Petri net + transition + transition to check + marking + marking to check + objects + dict of objects per object type, e.g., {"order": {"order1", "order2"}} + + Returns + ------- + bool + true if binding is enabled, false otherwise + """ + # We need to consume at least one token + at_least_one_object = False + + # Check for every in-arc if: + # - the binding respects the variability of the in-arc + # - the state contains the necessary tokens + for a in transition.in_arcs: + ot = a.object_type + + obj_set = objects.get(ot, set()) + + # Check if variability of arc is respected + if (not a.is_variable) and (not len(obj_set) == 1): + # nv-arc must be bound to exactly one object + return False + + if not obj_set: + # We assign not objects to this variable arc + continue + + at_least_one_object = True + + # Check if state contains all tokens + place_tokens = marking[a.source].keys() + if not obj_set <= place_tokens: + # tokens not present + return False + + return at_least_one_object + + @classmethod + def check_and_fire( + cls, pn: N, transition: T, marking: OCMarking, objects: Dict[str, Set] + ) -> OCMarking: + """ + Checks if a transition is enabled and fires it with the given set of objects. + If the transition is not enabled, it returns None. + + Parameters + ---------- + pn + object-centric Petri net + transition + transition to fire + marking + marking to use + objects + dict of objects per object type, e.g., {"order": {"order1", "order2"}} + + Returns + ------- + OCMarking or None + New marking after firing the transition or None if the transition was not enabled. + """ + if cls.is_binding_enabled(pn, transition, marking, objects): + return cls.fire(pn, transition, marking, objects) + return None + + @classmethod + def fire( + cls, pn: N, transition: T, marking: OCMarking, objects: Dict[str, Set] + ) -> OCMarking: + """ + Execute a transition for a given set of objects per object type + For performance reasons, this function does not check if the transition is enabled or binding passed set of objects is valid, i.e., + this should be performed by the invoking algorithm (if needed). Hence, markings can become negative. + + Parameters + ---------- + pn + object-centric Petri net + transition + transition to execute + marking + marking to use + objects + dict of objects per object type, e.g., {"order": {"order1", "order2"}} + + Returns + ------- + OCMarking + newly reached marking + """ + m_out = copy.copy(marking) + for a in transition.in_arcs: + obj_set = objects.get(a.object_type, set()) + m_out[a.source] -= Counter(obj_set) + for a in transition.out_arcs: + obj_set = objects.get(a.object_type, set()) + m_out[a.target] += Counter(obj_set) + return m_out + + @classmethod + def replay(cls, ocpn: N, trace, initial_marking=None, final_marking=None): + """ + Replays a trace on the object-centric Petri net. + Starts with the initial marking and check if the trace reaches the final marking. + + Parameters + ---------- + ocpn : N + The object-centric Petri net to replay on. + trace : tuple + A trace from the object-centric Petri net, + represented as a tuple of (transition_name, {object_type: set(object_ids)}) tuples. + initial_marking : OCMarking, optional + The initial marking to start the replay from. If None, the initial marking of the ocpn is used. + final_marking : OCMarking, optional + The final marking to check after the replay. If None, the final marking of the ocpn is used. + """ + if not initial_marking: + initial_marking = ocpn.initial_marking + if not final_marking: + final_marking = ocpn.final_marking + + transitions = {t.name: t for t in ocpn.transitions} + + # start in the initial marking + marking = initial_marking + + # replay each binding + for transition_name, objects in trace: + t = transitions[transition_name] + + # Check if the binding is enabled + if not cls.is_binding_enabled(ocpn, t, marking, objects): + return False + + # Fire + marking = cls.fire(ocpn, t, marking, objects) + + # Check if we are in the final marking + return cls._is_final(marking, final_marking) + + @classmethod + def _is_final(cls, marking: OCMarking, final_marking: OCMarking) -> bool: + """ + Checks if the given marking is a valid final marking. + May be overriden by subclasses. + + Parameters + ---------- + marking + marking to check + final_marking + final marking to check against + + Returns + ------- + bool + true if marking is a final marking, false otherwise + """ + return marking == final_marking + + @classmethod + def enabled_transitions( + cls, pn: N, marking: OCMarking + ) -> Set[T]: + """ + Returns the enabled transitions in a given object-centric Petri net and marking + + Parameters + ---------- + net + object-centric Petri net + marking + marking to check + + Returns + ------- + Set[T] + Set of enabled transitions + """ + return {t for t in pn.transitions if cls.is_enabled(pn, t, marking)} + + @classmethod + def get_possible_bindings( + cls, pn: N, transition: T, marking: OCMarking + ) -> Iterator[Dict[str, Set]]: + """ + Returns an Iterator on the possible bindings for a given transition + in a given object-centric Petri net and marking. + + Parameters + ---------- + net + object-centric Petri net + transition + transition to check + marking + marking to check + + Returns + ------- + Iterator[Dict[str, Set]] + Iterator on all possible bindings (key is object type, value is a set of objects) + """ + + def get_common_objects(places: Set[P], marking: OCMarking) -> Set: + """ + Get the intersection of objects in a set of places for the given marking. + + The resulting set contains objects present in ALL specified places. + + Parameters + ---------- + places + Set of places to consider + marking + Marking to consider + + Returns + ------- + Set + Set containing the common objects + """ + if not places: + return set() + + # iter to avoid treating the first place as a special case + places_iter = iter(places) + + result_counter = marking[next(places_iter)].copy() + + for place in places_iter: + if not result_counter: + break + # intersect with the current place's objects + result_counter &= marking[place] + + return set(result_counter) + + def get_powerset(object_type: str, objects: Set) -> Set[FrozenSet[Tuple]]: + """ + Generate the powerset of a set of objects in the form (object_type, object_id). + + Parameters + ---------- + object_type + The type of the objects + objects + Set of objects to generate the powerset for + + Returns + ------- + Set[FrozenSet[Tuple]] + Set of frozen sets representing the powerset of the input set + """ + items = [(object_type, obj_id) for obj_id in objects] + powerset_iterator = chain.from_iterable( + combinations(items, r) for r in range(len(items) + 1) + ) + return {frozenset(subset) for subset in powerset_iterator} + + def recursive_bindings( + variable_object_types: Set[str], + non_variable_object_types: Set[str], + available_objects: Dict[str, Set], + ) -> Set[FrozenSet[Tuple]]: + """ + Recursively generate all possible bindings given the remaining object types and available objects. + + Parameters + ---------- + variable_object_types + Set of variable object types to consider + non_variable_object_types + Set of non-variable object types to consider + available_objects + Dictionary where keys are object types and values are sets of available objects for that type + + Returns + ------- + Set[FrozenSet[Tuple]] + Set of possible bindings (as frozen sets of tuples (object_type, object_id)) + """ + # Base case + if not non_variable_object_types and not variable_object_types: + # empty binding + return {frozenset()} + + # Recursive case + # Pick next object type to process. Non-variable object types are processed first. + is_variable = not non_variable_object_types + types_to_process = variable_object_types if is_variable else non_variable_object_types + ot = types_to_process.pop() + + try: + ot_objects = available_objects.get(ot, set()) + + # Get all possible object combinations for this object type + if is_variable: + # For variable object types, we can pick 0-all objects (powerset) + current_options = get_powerset(ot, ot_objects) + else: + # For non-variable object types, we must pick exactly one object + current_options = {frozenset({(ot, obj)}) for obj in ot_objects} + # No options -> no bindings possible + if not current_options: + return set() + + # Recursively get bindings for the remaining object types + sub_bindings = recursive_bindings( + variable_object_types, + non_variable_object_types, + available_objects, + ) + + # cross product current options with sub-bindings + bindings = { + sub_binding.union(option) + for option in current_options + for sub_binding in sub_bindings + } + + return bindings + + finally: + # restore the set of object types + types_to_process.add(ot) + + + if transition not in pn.transitions: + # no binding to yield + return + + # get object types involved in t and whether they are variable + non_variable_object_types = set() + variable_object_types = set() + for a in transition.in_arcs: + if a.is_variable: + variable_object_types.add(a.object_type) + else: + non_variable_object_types.add(a.object_type) + + # get predecessor places per object type + predecessors = { + ot: pre_set(transition, ot) + for ot in non_variable_object_types | variable_object_types + } + + # per ot, get objects present in all predecessor places + common_objects = { + ot: get_common_objects(predecessors[ot], marking) + for ot in non_variable_object_types | variable_object_types + } + + # get bindings + frozenset_bindings = recursive_bindings( + variable_object_types, non_variable_object_types, common_objects + ) + + # Loop through immutable bindings and yield dictionary one by one + for fs_binding in frozenset_bindings: + # do not yield the empty binding + if not fs_binding: + continue + + binding_dict = defaultdict(set) + for ot, obj in fs_binding: + binding_dict[ot].add(obj) + + yield binding_dict diff --git a/pm4py/objects/ocpn/utils/__init__.py b/pm4py/objects/ocpn/utils/__init__.py new file mode 100644 index 0000000000..6de3d75785 --- /dev/null +++ b/pm4py/objects/ocpn/utils/__init__.py @@ -0,0 +1,22 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.objects.ocpn.utils import ocpn_utils diff --git a/pm4py/objects/ocpn/utils/ocpn_utils.py b/pm4py/objects/ocpn/utils/ocpn_utils.py new file mode 100644 index 0000000000..dc969fd1ef --- /dev/null +++ b/pm4py/objects/ocpn/utils/ocpn_utils.py @@ -0,0 +1,70 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' + +from typing import Set + + +def pre_set(elem, object_type: str = None) -> Set: + """ + Returns the set of predecessors of an element (place or transition) in an object-centric Petri net. + Restricted to predecessors connected using arcs of the object type, if specified. + + Parameters + ---------- + elem + Element (place or transition) for which to get the predecessors + object_type + Object type to restrict the predecessors to (optional) + + Returns + ------- + Set + Set of predecessor elements (places or transitions) of the specified object type. + """ + pre = set() + for a in elem.in_arcs: + if object_type is None or a.object_type == object_type: + pre.add(a.source) + return pre + +def post_set(elem, object_type: str = None) -> Set: + """ + Returns the set of successors of an element (place or transition) in an object-centric Petri net. + Restricted to successors connected using arcs of the object type, if specified. + + Parameters + ---------- + elem + Element (place or transition) for which to get the successors + object_type + Object type to restrict the successors to (optional) + + Returns + ------- + Set + Set of successor elements (places or transitions) of the specified object type. + """ + post = set() + for a in elem.out_arcs: + if object_type is None or a.object_type == object_type: + post.add(a.target) + return post \ No newline at end of file diff --git a/pm4py/objects/ocpn/variants/__init__.py b/pm4py/objects/ocpn/variants/__init__.py new file mode 100644 index 0000000000..1d9c27b79b --- /dev/null +++ b/pm4py/objects/ocpn/variants/__init__.py @@ -0,0 +1,22 @@ +''' + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +''' +from pm4py.objects.ocpn.variants import to_oc_causal_net, to_alternative_format diff --git a/pm4py/objects/ocpn/variants/to_alternative_format.py b/pm4py/objects/ocpn/variants/to_alternative_format.py new file mode 100644 index 0000000000..c874ad3796 --- /dev/null +++ b/pm4py/objects/ocpn/variants/to_alternative_format.py @@ -0,0 +1,243 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from typing import Any, Dict +from pm4py.objects.ocpn.obj import OCMarking, OCPetriNet +from pm4py.objects.petri_net.obj import Marking, PetriNet +from pm4py.objects.petri_net.utils.petri_utils import add_arc_from_to + + +def apply(ocpn: OCPetriNet, parameters=None) -> Dict[str, Any]: + """ + Converts an OCPetriNet object to the alternative format as a Dict[str, Any] specified in pm4py.algo.discovery.ocel.ocpn.variants.classic. + Only the essential components of the OCPetriNet are retained in the alternative format: `activities`, `object_types`, `petri_nets`, `double_arcs_on_activity`, `start_activities`, and `end_activities`. + + Parameters + ---------- + ocpn: OCPetriNet + The object-centric Petri net to be converted. + parameters: dict, optional + Additional parameters for the conversion (not used in this implementation). + + Returns + ---------- + ocpn: Dict[str, Any] + Alternative format representation of the object-centric Petri net. + """ + if parameters is None: + parameters = {} + + object_types = ocpn.object_types + activities = {t.label for t in ocpn.transitions if t.label} + petri_nets = {ot: _project_ocpn_on_object_type(ocpn, ot) for ot in object_types} + double_arcs_on_activity = _get_double_arcs(ocpn) + start_activities = _get_start_end_activities(ocpn, ocpn.initial_marking) + end_activities = _get_start_end_activities(ocpn, ocpn.final_marking) + + alternative_format = { + "activities": activities, + "object_types": object_types, + "petri_nets": petri_nets, + "double_arcs_on_activity": double_arcs_on_activity, + "start_activities": start_activities, + "end_activities": end_activities, + # information not extracted in this implementation + "edges": { + "event_couples": {ot: {} for ot in object_types}, + "event_pairs": {ot: {} for ot in object_types}, + "total_objects": {ot: {} for ot in object_types}, + }, + "activities_indep": { + "events": {ot: {} for ot in object_types}, + "unique_objects": {ot: {} for ot in object_types}, + "total_objects": {ot: {} for ot in object_types}, + }, + "activities_ot": { + "events": {ot: {} for ot in object_types}, + "unique_objects": {ot: {} for ot in object_types}, + "total_objects": {ot: {} for ot in object_types}, + }, + "tbr_results": {}, + } + + return alternative_format + + +def _project_ocpn_on_object_type( + ocpn: OCPetriNet, object_type +) -> tuple[PetriNet, Marking, Marking]: + """ + Projects the OCPetriNet into a tuple containing the Petri net projection and the initial and final marking projections for the object type. + + Parameters + ---------- + ocpn: OCPetriNet + The object-centric Petri net to be split. + object_type: str + The object type for which the projection is to be created. + + Returns + ---------- + tuple[PetriNet, Marking, Marking] + A tuple containing the Petri net projection, initial marking, and final marking projection for the object type. + """ + # extract places by ot + places = [p for p in ocpn.places if p.object_type == object_type] + + # extract arcs from those places + arcs = [ + a + for p in places + for a in p.out_arcs | p.in_arcs + if a.object_type == object_type + ] + + # extract transitions as those used in the arcs + transitions = { + a.source for a in arcs if isinstance(a.source, OCPetriNet.Transition) + } | {a.target for a in arcs if isinstance(a.target, OCPetriNet.Transition)} + + # construct net projection + pn_places = {p: PetriNet.Place(p.name) for p in places} + pn_transitions = {t: PetriNet.Transition(name=t.name, label=t.label) for t in transitions} + + # create pn + pn = PetriNet( + name=f"{ocpn.name}_{object_type}", + places=set(pn_places.values()), + transitions=set(pn_transitions.values()), + ) + + # add arcs + for arc in arcs: + source = pn_places.get(arc.source, pn_transitions.get(arc.source)) + target = pn_places.get(arc.target, pn_transitions.get(arc.target)) + + add_arc_from_to(source, target, pn) + + # initial (& final) marking as multiset of places in the initial marking where the count is the number of objects of that type in the place + initial_marking = oc_marking_to_petri(ocpn.initial_marking, pn_places) + final_marking = oc_marking_to_petri(ocpn.final_marking, pn_places) + + return pn, initial_marking, final_marking + + +def oc_marking_to_petri( + oc_marking: OCMarking, + pn_places: Dict[OCPetriNet.Place, PetriNet.Place], +) -> Marking: + """ + Convert an object-centric marking to a classic Petri-net Marking that + contains only the places that are keys of the `pn_places` dictionary. + + Parameters + ---------- + oc_marking: OCMarking + The object-centric marking to convert. + pn_places: Dict[OCPetriNet.Place, PetriNet.Place] + A mapping from object-centric Petri net places to classic Petri net places. + + Returns + ---------- + Marking + A classic Petri net marking (place -> token count) + """ + petri_marking = Marking() + if not oc_marking: + return petri_marking + + # Aggregate multiplicities per place for the requested object type + for place, counter in oc_marking.items(): + if place in pn_places.keys(): + if pn_places[place] not in petri_marking: + petri_marking[pn_places[place]] = 0 + petri_marking[pn_places[place]] += sum(counter.values()) + + return petri_marking + + +def _get_double_arcs(ocpn: OCPetriNet) -> Dict[str, Any]: + """ + Returns a dictionary mapping each object type to a dict mapping an activity to True if only connected to variable arcs, or False if only connected to non-variable arcs. + + Parameters + ---------- + ocpn: OCPetriNet + The object-centric Petri net to analyze. + + Returns + ---------- + Dict[str, Any] + A dictionary where keys are object types and values are dicts where keys are + activity names and values are True if only connected to variable arcs, or False if only connected to non-variable arcs. + """ + double_arcs = {ot: {} for ot in ocpn.object_types} + for arc in ocpn.arcs: + ot = arc.object_type + act = arc.source.label if isinstance(arc.source, OCPetriNet.Transition) else arc.target.label + if act is None: + continue + if act in double_arcs[ot]: + if double_arcs[ot][act] != arc.is_variable: + raise ValueError( + f"Transition {act} in object type {ot} is connected to both variable and non-variable arcs. The given OCPetriNet is invalid." + ) + double_arcs[ot][act] = arc.is_variable + + return double_arcs + + +def _get_start_end_activities( + ocpn: OCPetriNet, + marking: OCMarking +) -> Dict[str, Any]: + """ + Returns a dictionary mapping each object type to a dict mapping the start/end activities to empty sets for events, unique_objects, and total_objects. + + Parameters + ---------- + ocpn: OCPetriNet + The object-centric Petri net to analyze. + marking: OCMarking + The initial or final marking of the OCPetriNet, used to determine start or end activities. + + Returns + ---------- + Dict[str, Any] + The start or end activities dictionaries. + """ + # activities are those occuring in the given marking + activities = {ot: {} for ot in ocpn.object_types} + + if marking is None: + return activities + + for p in marking.places: + ot = p.object_type + if p not in activities[ot]: + activities[ot][p.name] = { + "events": set(), + "unique_objects": set(), + "total_objects": set(), + } + + return activities \ No newline at end of file diff --git a/pm4py/objects/ocpn/variants/to_oc_causal_net.py b/pm4py/objects/ocpn/variants/to_oc_causal_net.py new file mode 100644 index 0000000000..a377d42d1b --- /dev/null +++ b/pm4py/objects/ocpn/variants/to_oc_causal_net.py @@ -0,0 +1,631 @@ +""" + PM4Py – A Process Mining Library for Python +Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see this software project's root or +visit . + +Website: https://processintelligence.solutions +Contact: info@processintelligence.solutions +""" + +from pm4py.objects.oc_causal_net.obj import OCCausalNet +from pm4py.objects.ocpn.obj import OCPetriNet +import networkx as nx + + +AUX_ACTIVITY_PREFIX = "_silent_aux_" +POST_AUX_ACTIVITY_PREFIX = AUX_ACTIVITY_PREFIX + "out_" +PRE_AUX_ACTIVITY_PREFIX = AUX_ACTIVITY_PREFIX + "in_" + + +def apply(ocpn: OCPetriNet, parameters=None) -> OCCausalNet: + """ + Convets an Object-centric Petri Net to an Object-centric Causal Net. + + Parameters + ---------- + ocpn: OCPetriNet + The Object-centric Petri Net to be converted. + parameters: dict, optional + Additional parameters for the conversion (not used in this implementation). + + Returns + ---------- + OCCausalNet: OCCausalNet + The resulting Object-centric Causal Net. + """ + # assert no place start with AUX_ACTIVITY_PREFIX to be able to identify auxiliary places + for p in ocpn.places: + if p.name.startswith(AUX_ACTIVITY_PREFIX): + raise ValueError( + f"Place {p.name} starts with the reserved prefix '{AUX_ACTIVITY_PREFIX}'." + ) + + places = ocpn.places + transitions = ocpn.transitions + object_types = ocpn.object_types + + dependencies = dict() + input_marker_groups = dict() + output_marker_groups = dict() + + # get multi-object types per transition + multi_ots = get_multi_object_types(ocpn) + + # create dependencies and marker groups for transitions + transition_dependencies_marker_groups( + transitions, + multi_ots, + dependencies, + input_marker_groups, + output_marker_groups, + ) + + # create dependencies and marker groups for places + place_dependencies_marker_groups( + places, + multi_ots, + dependencies, + input_marker_groups, + output_marker_groups, + ) + + # handle start and end places + start_places = { + ot: [p for p in ocpn.initial_marking.places if p.object_type == ot] + for ot in ocpn.object_types + } + end_places = { + ot: [p for p in ocpn.final_marking.places if p.object_type == ot] + for ot in ocpn.object_types + } + + # add START and END activities and their dependencies and marker groups + start_end_act_dependencies_marker_groups( + object_types, + dependencies, + input_marker_groups, + output_marker_groups, + start_places, + end_places, + ) + + # add START and END activities to marker groups of start and end places + add_start_end_act_markers( + object_types, + input_marker_groups, + output_marker_groups, + start_places, + end_places, + ) + + # create the occn + occn = OCCausalNet( + dependency_graph=nx.MultiDiGraph(dependencies), + output_marker_groups=output_marker_groups, + input_marker_groups=input_marker_groups, + ) + + return occn + + +def get_multi_object_types(ocpn: OCPetriNet) -> dict: + """ + Returns a dictionary mapping transition names to two dictionaries of multi-input/multi-output + object types + mapping object types to True if they are variable and False otherwise. + A multi-input/output object type is one that has at least two incoming/outgoing arcs + to/from the transition. + + Parameters + ---------- + ocpn: OCPetriNet + The Object-centric Petri Net to analyze. + + Returns + ------- + dict: Dictionary with transition names as keys a dictionary mapping "input" and "output" + to dictionaries of multi-input/multi-output object types to True/False. + """ + multi_ots = {} + for t in ocpn.transitions: + multi_ots[t.name] = {"input": dict(), "output": dict()} + in_ots = [arc.object_type for arc in t.in_arcs] + multi_ots[t.name]["output"] = dict() + out_ots = [arc.object_type for arc in t.out_arcs] + + for ot in set(in_ots): + if in_ots.count(ot) > 1: + multi_ots[t.name]["input"][ot] = any( + arc.is_variable for arc in t.in_arcs if arc.object_type == ot + ) + + for ot in set(out_ots): + if out_ots.count(ot) > 1: + multi_ots[t.name]["output"][ot] = any( + arc.is_variable for arc in t.out_arcs if arc.object_type == ot + ) + + return multi_ots + + +def get_aux_place_name(transition_name, object_type, is_input_aux): + """ + Returns the name of the auxiliary place for a given transition and object type. + + Parameters + ---------- + transition_name: str + The name of the transition. + object_type: str + The object type associated with the auxiliary place. + is_input_aux: bool + Whether the auxiliary place is an input auxiliary place (True) or output auxiliary place (False). + + Returns + ------- + str: The name of the auxiliary place. + """ + prefix = PRE_AUX_ACTIVITY_PREFIX if is_input_aux else POST_AUX_ACTIVITY_PREFIX + return f"{prefix}{transition_name}_{object_type}" + + +def transition_dependencies_marker_groups( + transitions, + multi_ots, + dependencies, + input_marker_groups, + output_marker_groups, +): + """ + Creates dependencies and marker groups for transitions in the Object-centric Petri Net. + Each transition has one input marker group featuring a marker for each input arc, + and one output marker group featuring a marker for each output arc. + + Parameters + ---------- + transitions: list + List of transitions in the Object-centric Petri Net. + multi_ots: dict + Dictionary mapping transition names to multi-object types. + dependencies: dict + Dictionary to store dependencies between activities. + input_marker_groups: dict + Dictionary to store input marker groups. + output_marker_groups: dict + Dictionary to store output marker groups. + """ + for t in transitions: + # dependencies + dependencies[t.name] = dict() # add as activity + + # get multi object types for this transition + multi_input = multi_ots.get(t.name, dict()).get("input", dict()) + multi_output = multi_ots.get(t.name, dict()).get("output", dict()) + + for arc in t.in_arcs: + if arc.object_type in multi_input: + # aux. place goes in between source place and transition + aux_place_name = get_aux_place_name( + t.name, arc.object_type, is_input_aux=True + ) + add_dependency( + dependencies, arc.source, aux_place_name, arc.object_type + ) + add_dependency(dependencies, aux_place_name, t, arc.object_type) + else: + add_dependency(dependencies, arc.source, t, arc.object_type) + for arc in t.out_arcs: + if arc.object_type in multi_output: + # aux. place goes in between transition and target place + aux_place_name = get_aux_place_name( + t.name, arc.object_type, is_input_aux=False + ) + add_dependency(dependencies, t, aux_place_name, arc.object_type) + add_dependency( + dependencies, aux_place_name, arc.target, arc.object_type + ) + else: + add_dependency(dependencies, t, arc.target, arc.object_type) + + # markers from input aux activities + input_aux_markers = [ + OCCausalNet.Marker( + related_activity=( + get_aux_place_name(t.name, object_type, is_input_aux=True) + ), + object_type=object_type, + count_range=(0, float("inf")) if multi_input[object_type] else (1, 1), + marker_key=get_next_key(), + ) + for object_type in multi_input + ] + + # single input marker group + input_marker_groups[t.name] = [ + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=arc.source.name, + object_type=arc.object_type, + count_range=(0, float("inf")) if arc.is_variable else (1, 1), + marker_key=get_next_key(), + ) + for arc in t.in_arcs + if arc.object_type not in multi_input + ] + + input_aux_markers + ) + ] + + # markers to output aux activities + output_aux_markers = [ + OCCausalNet.Marker( + related_activity=( + get_aux_place_name(t.name, object_type, is_input_aux=False) + ), + object_type=object_type, + count_range=(0, float("inf")) if multi_output[object_type] else (1, 1), + marker_key=get_next_key(), + ) + for object_type in multi_output + ] + + # single output marker group + output_marker_groups[t.name] = [ + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=(arc.target.name), + object_type=arc.object_type, + count_range=(0, float("inf")) if arc.is_variable else (1, 1), + marker_key=get_next_key(), + ) + for arc in t.out_arcs + if arc.object_type not in multi_output + ] + + output_aux_markers + ) + ] + + # add marker groups for input auxiliary places + for ot in multi_input: + aux_place_name = get_aux_place_name(t.name, ot, is_input_aux=True) + output_marker_groups[aux_place_name] = [ + # single marker group with one marker for the transition + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=t.name, + object_type=ot, + count_range=(1, 1), + marker_key=get_next_key(), + ) + ] + ) + ] + input_marker_groups[aux_place_name] = [ + # marker for every predecessor of t with this object type + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=arc.source.name, + object_type=ot, + count_range=(1, 1), + marker_key=get_next_key(), + ) + for arc in t.in_arcs + if arc.object_type == ot + ] + ) + ] + + # add marker groups for output auxiliary places + for ot in multi_output: + aux_place_name = get_aux_place_name(t.name, ot, is_input_aux=False) + input_marker_groups[aux_place_name] = [ + # single marker group with one marker for the transition + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=t.name, + object_type=ot, + count_range=(1, 1), + marker_key=get_next_key(), + ) + ] + ) + ] + output_marker_groups[aux_place_name] = [ + # marker for every successor of t with this object type + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=arc.target.name, + object_type=ot, + count_range=(1, 1), + marker_key=get_next_key(), + ) + for arc in t.out_arcs + if arc.object_type == ot + ] + ) + ] + + +def place_dependencies_marker_groups( + places, multi_ots, dependencies, input_marker_groups, output_marker_groups +): + """ + Creates dependencies and marker groups for places in the Object-centric Petri Net. + Each place has one input marker group per input arc having one marker each, + and one output marker group per output arc having one marker each. + + Parameters + ---------- + places: list + List of places in the Object-centric Petri Net. + multi_ots: dict + Dictionary mapping transition names to multi-object types. + dependencies: dict + Dictionary to store dependencies between activities. + input_marker_groups: dict + Dictionary to store input marker groups. + output_marker_groups: dict + Dictionary to store output marker groups. + """ + + def is_predecessor_transition_with_multi_variant(arc): + """ + Checks if the source of the arc is a transition with and the object type + of the arc is a multi-output object type for the given transition. + """ + return isinstance( + arc.source, OCPetriNet.Transition + ) and arc.object_type in multi_ots.get(arc.source.name, dict()).get( + "output", dict() + ) + + def is_successor_transition_with_multi_variant(arc): + """ + Checks if the target of the arc is a transition with and the object type + of the arc is a multi-input object type for the given transition. + """ + return isinstance( + arc.target, OCPetriNet.Transition + ) and arc.object_type in multi_ots.get(arc.target.name, dict()).get( + "input", dict() + ) + + for p in places: + # dependencies + if p.name not in dependencies: + dependencies[p.name] = dict() # add as activity + for arc in p.in_arcs: + if not is_predecessor_transition_with_multi_variant(arc): + add_dependency(dependencies, arc.source, p, arc.object_type) + else: + pass # dependency for auxiliary places was already added + for arc in p.out_arcs: + if not is_successor_transition_with_multi_variant(arc): + add_dependency(dependencies, p, arc.target, arc.object_type) + else: + pass # dependency for auxiliary places was already added + + # one-element marker group per arc + input_marker_groups[p.name] = [ + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=( + arc.source.name + if not is_predecessor_transition_with_multi_variant(arc) + else get_aux_place_name( + arc.source.name, arc.object_type, is_input_aux=False + ) + ), # connect to aux place instead if multi-variant ot + object_type=arc.object_type, + count_range=(1, float("inf")), + marker_key=get_next_key(), + ) + ] + ) + for arc in p.in_arcs + ] + + output_marker_groups[p.name] = [ + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=( + arc.target.name + if not is_successor_transition_with_multi_variant(arc) + else get_aux_place_name( + arc.target.name, arc.object_type, is_input_aux=True + ) + ), + object_type=arc.object_type, + count_range=(1, float("inf")), + marker_key=get_next_key(), + ) + ] + ) + for arc in p.out_arcs + ] + + +def start_end_act_dependencies_marker_groups( + object_types, + dependencies, + input_marker_groups, + output_marker_groups, + start_places, + end_places, +): + """ + Adds a START and END activity for each object type. + Creates dependencies and marker groups for those new activities. + Each START / END activity has one input / output marker group consisting + of markers for every start / end place. + + Parameters + ---------- + object_types: list + List of object types in the Object-centric Petri Net. + dependencies: dict + Dictionary to store dependencies between activities. + input_marker_groups: dict + Dictionary to store input marker groups. + output_marker_groups: dict + Dictionary to store output marker groups. + start_places: dict + Dictionary mapping object types to their start places. + end_places: dict + Dictionary mapping object types to their end places. + """ + for ot in object_types: + # START places + dependencies[f"START_{ot}"] = dict() + for p in start_places[ot]: + add_dependency(dependencies, f"START_{ot}", p, ot) + + # one output marker group + output_marker_groups[f"START_{ot}"] = [ + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=p.name, + object_type=ot, + count_range=(1, float("inf")), + marker_key=get_next_key(), + ) + for p in start_places[ot] + ] + ) + ] + + # END places + dependencies[f"END_{ot}"] = dict() + for p in end_places[ot]: + add_dependency(dependencies, p, f"END_{ot}", ot) + + # one input marker group + input_marker_groups[f"END_{ot}"] = [ + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=p.name, + object_type=ot, + count_range=(1, float("inf")), + marker_key=get_next_key(), + ) + for p in end_places[ot] + ] + ) + ] + + +def add_start_end_act_markers( + object_types, input_marker_groups, output_marker_groups, start_places, end_places +): + """ + Adds markers for the START and END activities to the start / end places. + These markers are the same as for any other predecessor / successor activity. + + Parameters + ---------- + object_types: list + List of object types in the Object-centric Petri Net. + input_marker_groups: dict + Dictionary to store input marker groups. + output_marker_groups: dict + Dictionary to store output marker groups. + start_places: dict + Dictionary mapping object types to their start places. + end_places: dict + Dictionary mapping object types to their end places. + """ + for ot in object_types: + for p in start_places[ot]: + # add new marker group + if p.name not in input_marker_groups: + input_marker_groups[p.name] = [] + input_marker_groups[p.name].append( + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=f"START_{ot}", + object_type=ot, + count_range=(1, float("inf")), + marker_key=get_next_key(), + ) + ] + ) + ) + for p in end_places[ot]: + # add new marker group + if p.name not in output_marker_groups: + output_marker_groups[p.name] = [] + output_marker_groups[p.name].append( + OCCausalNet.MarkerGroup( + [ + OCCausalNet.Marker( + related_activity=f"END_{ot}", + object_type=ot, + count_range=(1, float("inf")), + marker_key=get_next_key(), + ) + ] + ) + ) + + +def add_dependency(dependencies: dict, source, target, object_type): + """ + Adds a dependency to the dependencies dictionary. Ignores duplicate dependencies. + + Parameters + ---------- + dependencies: dict + The dictionary to which the dependency will be added. + source: str or OCPetriNet.Place or OCPetriNet.Transition + The source of the dependency arc. + target str or OCPetriNet.Place or OCPetriNet.Transition + The target of the dependency arc. + object_type + The object type associated with the dependency. + """ + source_label = source if isinstance(source, str) else source.name + target_label = target if isinstance(target, str) else target.name + if source_label not in dependencies: + dependencies[source_label] = dict() + if target_label not in dependencies[source_label]: + dependencies[source_label][target_label] = dict() + if object_type not in dependencies[source_label][target_label]: + dependencies[source_label][target_label][object_type] = { + "object_type": object_type + } + + +def get_next_key(): + """ + Will return a unique key for each call, starting from 0. + """ + # initialize on first call + if not hasattr(get_next_key, "counter"): + get_next_key.counter = 0 + current = get_next_key.counter + get_next_key.counter += 1 + return current diff --git a/tests/oc_causal_net_semantics_test.py b/tests/oc_causal_net_semantics_test.py new file mode 100644 index 0000000000..158cdbbeed --- /dev/null +++ b/tests/oc_causal_net_semantics_test.py @@ -0,0 +1,535 @@ +from collections import Counter +import unittest +from pm4py.objects.oc_causal_net.creation.factory import create_oc_causal_net +from pm4py.objects.oc_causal_net.semantics import OCCausalNetSemantics, OCCausalNetState + + +class OCCausalNetSemanticsTest(unittest.TestCase): + + def test_enabled_bindings(self): + occn = occn_multi_ot_multi_arc() + + state = OCCausalNetState() + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(enabled_bindings, ()) + + state = OCCausalNetState({"a": Counter([("START_order", "o1", "order")])}) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 0) + + state = OCCausalNetState( + { + "a": Counter( + [("START_order", "o1", "order"), ("START_item", "i1", "item")] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 1) + + state = OCCausalNetState( + { + "a": Counter( + [ + ("START_order", "o1", "order"), + ("START_item", "i1", "item"), + ("START_item", "i2", "item"), + ] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 3) + + def test_enabled_bindings_2(self): + occn = occn_multi_ot_multi_min_0() + + state = OCCausalNetState() + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(enabled_bindings, ()) + + state = OCCausalNetState({"a": Counter([("START_order", "o1", "order")])}) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 1) + + state = OCCausalNetState( + { + "a": Counter( + [("START_order", "o1", "order"), ("START_item", "i1", "item")] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 2) + + state = OCCausalNetState( + { + "a": Counter( + [ + ("START_order", "o1", "order"), + ("START_item", "i1", "item"), + ("START_item", "i2", "item"), + ] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 4) + + def test_enabled_bindings_3(self): + + act_to_idx = { + "START_order": 0, + "START_item": 1, + "a": 2, + "b": 3, + "END_order": 4, + "END_item": 5, + } + + ot_to_idx = {"order": 0, "item": 1} + + occn = occn_multi_ot_multi_min_0() + + state = OCCausalNetState() + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(enabled_bindings, ()) + + state = OCCausalNetState({2: Counter([(0, "o1", 0)])}) + enabled_bindings = OCCausalNetSemantics.enabled_bindings( + occn, "a", state, act_to_idx, ot_to_idx + ) + self.assertEqual(len(enabled_bindings), 1) + + state = OCCausalNetState({2: Counter([(0, "o1", 0), (1, "i1", 1)])}) + enabled_bindings = OCCausalNetSemantics.enabled_bindings( + occn, "a", state, act_to_idx, ot_to_idx + ) + self.assertEqual(len(enabled_bindings), 2) + + state = OCCausalNetState( + {2: Counter([(0, "o1", 0), (1, "i1", 1), (1, "i2", 1)])} + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings( + occn, "a", state, act_to_idx, ot_to_idx + ) + self.assertEqual(len(enabled_bindings), 4) + + def test_enabled_bindings_4(self): + occn = occn_multi_ot_multi_marker() + + state = OCCausalNetState() + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(enabled_bindings, ()) + + state = OCCausalNetState({"a": Counter([("START_order", "o1", "order")])}) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(enabled_bindings, ()) + + state = OCCausalNetState( + { + "a": Counter( + [("START_order", "o1", "order"), ("START_item", "i1", "item")] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 2) + + state = OCCausalNetState({"a": Counter([("START_item", "i1", "item")])}) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 1) + + state = OCCausalNetState( + { + "a": Counter( + [ + ("START_order", "o1", "order"), + ("START_item", "i1", "item"), + ("START_item", "i1", "item"), + ] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 2) + + state = OCCausalNetState( + { + "a": Counter( + [ + ("START_order", "o1", "order"), + ("START_item", "i1", "item"), + ("START_item", "i2", "item"), + ] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 6) + + def test_enabled_bindings_5(self): + occn = occn_multi_ot_multi_marker_redundant_mg() + + state = OCCausalNetState() + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(enabled_bindings, ()) + + state = OCCausalNetState({"a": Counter([("START_order", "o1", "order")])}) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(enabled_bindings, ()) + + state = OCCausalNetState( + { + "a": Counter( + [("START_order", "o1", "order"), ("START_item", "i1", "item")] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 2) + + state = OCCausalNetState({"a": Counter([("START_item", "i1", "item")])}) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 1) + + state = OCCausalNetState( + { + "a": Counter( + [ + ("START_order", "o1", "order"), + ("START_item", "i1", "item"), + ("START_item", "i1", "item"), + ] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 2) + + state = OCCausalNetState( + { + "a": Counter( + [ + ("START_order", "o1", "order"), + ("START_item", "i1", "item"), + ("START_item", "i2", "item"), + ] + ) + } + ) + enabled_bindings = OCCausalNetSemantics.enabled_bindings(occn, "a", state) + self.assertEqual(len(enabled_bindings), 6) + + def test_enabled_start_bindings(self): + occn = occn_start_parallel() + + enabled_bindings = OCCausalNetSemantics.enabled_bindings_start_activity( + occn, "START_order", "order", set() + ) + self.assertEqual(len(enabled_bindings), 0) + + enabled_bindings = OCCausalNetSemantics.enabled_bindings_start_activity( + occn, "START_order", "order", {"o1"} + ) + self.assertEqual(len(enabled_bindings), 3) + + enabled_bindings = OCCausalNetSemantics.enabled_bindings_start_activity( + occn, "START_order", "order", {"o1", "o2"} + ) + self.assertEqual(len(enabled_bindings), 10) + for binding in enabled_bindings: + prod_tuple = binding[2] + prod_dict = { + succ: + { + ot: set(objects) + for ot, objects in obj_per_ot + } + for succ, obj_per_ot in prod_tuple + } + self.assertIsNotNone(OCCausalNetSemantics.is_binding_enabled(occn, "START_order", None, prod_dict, OCCausalNetState())) + + +def occn_multi_ot_multi_arc(): + marker_groups = { + "START_order": { + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "START_item": { + "omg": [ + [("a", "item", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (1, -1), 0), + ], + ], + "omg": [ + [ + ("b", "order", (1, 1), 0), + ("b", "item", (1, -1), 0), + ], + ], + }, + "b": { + "img": [ + [ + ("a", "order", (1, 1), 0), + ("a", "item", (1, -1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ("END_item", "item", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("b", "order", (1, 1), 0)], + ] + }, + "END_item": { + "img": [ + [("b", "item", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + return occn + + +def occn_multi_ot_multi_min_0(): + marker_groups = { + "START_order": { + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "START_item": { + "omg": [ + [("a", "item", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (0, -1), 0), + ], + ], + "omg": [ + [ + ("b", "order", (1, 1), 0), + ("b", "item", (0, -1), 0), + ], + ], + }, + "b": { + "img": [ + [ + ("a", "order", (1, 1), 0), + ("a", "item", (0, -1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ("END_item", "item", (0, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("b", "order", (1, 1), 0)], + ] + }, + "END_item": { + "img": [ + [("b", "item", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + return occn + + +def occn_multi_ot_multi_marker(): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "START_item": { + "img": [], + "omg": [ + [("a", "item", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (1, -1), 0), + ], + [ + ("START_item", "item", (1, -1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ("END_item", "item", (1, -1), 0), + ], + [ + ("END_item", "item", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, 1), 0)], + ] + }, + "END_item": { + "img": [ + [("a", "item", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + return occn + + +def occn_multi_ot_multi_marker_redundant_mg(): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "START_item": { + "img": [], + "omg": [ + [("a", "item", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (1, -1), 0), + ], + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (1, 1), 0), + ], + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (2, 2), 0), + ], + [ + ("START_item", "item", (0, -1), 0), + ], + [ + ("START_item", "item", (1, 1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ("END_item", "item", (1, -1), 0), + ], + [ + ("END_order", "order", (1, 1), 0), + ("END_item", "item", (1, 1), 0), + ], + [ + ("END_item", "item", (0, -1), 0), + ], + [ + ("END_item", "item", (1, -1), 0), + ], + [ + ("END_item", "item", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, 1), 0)], + ] + }, + "END_item": { + "img": [ + [("a", "item", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + return occn + + +def occn_start_parallel(): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, 1), 0), ("b", "order", (1, 1), 0)], + [("a", "order", (1, -1), 0)], + [("b", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "b": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, 1), 0), ("b", "order", (1, 1), 0)], + [("a", "order", (1, -1), 0)], + [("b", "order", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + return occn + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/oc_causal_net_simulation_test.py b/tests/oc_causal_net_simulation_test.py new file mode 100644 index 0000000000..756191c3a2 --- /dev/null +++ b/tests/oc_causal_net_simulation_test.py @@ -0,0 +1,106 @@ +import unittest +from pm4py.objects.oc_causal_net.creation.factory import create_oc_causal_net +from pm4py.objects.oc_causal_net.semantics import OCCausalNetSemantics, OCCausalNetState +from pm4py.algo.simulation.playout.oc_causal_net.variants import extensive as playout_extensive + +class OCCausalNetSimulationTest(unittest.TestCase): + + def test_playout_occn_extensive(self): + occn = occn_ABC() + + parameters = { + playout_extensive.Parameters.MAX_BINDINGS_PER_ACTIVITY: 3, + playout_extensive.Parameters.RETURN_SEQUENCES: True, + } + + objects = { + "order": set() + } + valid_sequences_iter, _, _ = playout_extensive.apply(occn, objects, parameters) + valid_sequences = list(valid_sequences_iter) + self.assertEqual(len(valid_sequences), 1) + + objects = { + "order": {"o1"} + } + valid_sequences_iter, _, _ = playout_extensive.apply(occn, objects, parameters) + valid_sequences = list(valid_sequences_iter) + self.assertEqual(len(valid_sequences), 1) + + objects = { + "order": {"o1", "o2"} + } + valid_sequences_iter, _, _ = playout_extensive.apply(occn, objects, parameters) + valid_sequences = list(valid_sequences_iter) + self.assertEqual(len(valid_sequences), 252) + + def test_playout_occn_extensive_bf_limited(self): + occn = occn_ABC() + + parameters = { + playout_extensive.Parameters.MAX_BINDINGS_PER_ACTIVITY: 3, + playout_extensive.Parameters.RETURN_SEQUENCES: True, + } + objects = { + "order": {"o1", "o2"} + } + valid_sequences_iter, _, _ = playout_extensive.apply(occn, objects, parameters) + valid_sequences = list(valid_sequences_iter) + self.assertEqual(len(valid_sequences), 252) + + parameters = { + playout_extensive.Parameters.MAX_BINDINGS_PER_ACTIVITY: 3, + playout_extensive.Parameters.RETURN_SEQUENCES: True, + playout_extensive.Parameters.BRANCHING_FACTOR_ACTIVITIES: 1.5, + playout_extensive.Parameters.BRANCHING_FACTOR_BINDINGS: 1.5, + } + for _ in range (10): + valid_sequences_iter_sub, _, _ = playout_extensive.apply(occn, objects, parameters) + valid_sequences_sub = list(valid_sequences_iter_sub) + for seq in valid_sequences_sub: + self.assertTrue(seq in valid_sequences) + +def occn_ABC(): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, 1), 0)], + ], + "omg": [ + [("b", "order", (1, 1), 0)], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [("c", "order", (1, 1), 0)], + ], + }, + "c": { + "img": [ + [("b", "order", (1, 1), 0)], + ], + "omg": [ + [("END_order", "order", (1, 1), 0)], + ], + }, + "END_order": { + "img": [ + [("c", "order", (1, 1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + return occn + +if __name__ == "__main__": + unittest.main() diff --git a/tests/oc_causal_net_test.py b/tests/oc_causal_net_test.py new file mode 100644 index 0000000000..b733dc82c1 --- /dev/null +++ b/tests/oc_causal_net_test.py @@ -0,0 +1,6628 @@ +import unittest +import pm4py +from pm4py.objects.oc_causal_net.obj import OCCausalNet +from pm4py.objects.oc_causal_net import converter +import networkx as nx +import re + +from pm4py.objects.oc_causal_net.creation.factory import create_oc_causal_net +from pm4py.objects.ocpn.obj import OCPetriNet + + +class OCCausalNetTest(unittest.TestCase): + def test_constructor_01(self): + # create OCCN + arcs = dict() + arcs["START_order"] = {"a": {"order": {"object_type": "order"}}} + arcs["START_item"] = {"a": {"item": {"object_type": "item"}}} + arcs["a"] = { + "END_order": {"order": {"object_type": "order"}}, + "END_item": {"item": {"object_type": "item"}}, + } + + START_order_output_markers = OCCausalNet.MarkerGroup( + markers=[OCCausalNet.Marker("a", "order", (1, 1), 0)] + ) + + START_item_output_markers = OCCausalNet.MarkerGroup( + markers=[OCCausalNet.Marker("a", "item", (1, float("inf")), 0)] + ) + + a_input_markers = OCCausalNet.MarkerGroup( + markers=[ + OCCausalNet.Marker("START_order", "order", (1, 1), 0), + OCCausalNet.Marker("START_item", "item", (1, float("inf")), 1), + ] + ) + + a_output_markers = OCCausalNet.MarkerGroup( + markers=[ + OCCausalNet.Marker("END_order", "order", (1, 1), 0), + OCCausalNet.Marker("END_item", "item", (1, float("inf")), 1), + ] + ) + + END_order_input_markers = OCCausalNet.MarkerGroup( + markers=[OCCausalNet.Marker("a", "order", (1, 1), 0)] + ) + + END_item_input_markers = OCCausalNet.MarkerGroup( + markers=[OCCausalNet.Marker("a", "item", (1, float("inf")), 0)] + ) + + occn = OCCausalNet( + nx.MultiDiGraph(arcs), + { + "a": [a_output_markers], + "START_order": [START_order_output_markers], + "START_item": [START_item_output_markers], + }, + { + "a": [a_input_markers], + "END_order": [END_order_input_markers], + "END_item": [END_item_input_markers], + }, + ) + + print("\nTEST OCCN CONSTRUCTOR 01") + print(occn) + + def test_constructor_02(self): + # same net as above, but using the create_oc_causal_net function + marker_groups = { + "START_order": { + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "START_item": { + "omg": [ + [("a", "item", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (1, -1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ("END_item", "item", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + }, + "END_item": { + "img": [ + [("a", "item", (1, -1), 0)], + ], + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONSTRUCTOR 02") + print(occn) + + def test_constructor_03(self): + marker_groups = { + "START_container": { + "omg": [ + [("c", "container", (1, 1), 0), ("i", "container", (1, 1), 0)], + ], + }, + "c": { + "img": [ + [("START_container", "container", (1, 1), 0)], + ], + "omg": [ + [("e", "container", (1, 1), 0)], + ], + }, + "i": { + "img": [ + [("START_container", "container", (1, 1), 0)], + ], + "omg": [ + [("e", "container", (1, 1), 0)], + ], + }, + "e": { + "img": [ + [("c", "container", (1, 1), 0), ("i", "container", (1, 1), 0)], + ], + "omg": [ + [("s", "container", (1, 1), 0)], + ], + }, + "START_order": { + "omg": [ + [("a", "order", (1, 1), 0)], + [("b", "order", (1, 1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, 1), 0)], + ], + "omg": [ + [("b", "order", (1, 1), 0)], + ], + }, + "b": { + "img": [ + [("START_order", "order", (1, 1), 0)], + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [("s", "order", (1, 1), 0)], + ], + }, + "START_box": { + "omg": [ + [("d", "box", (1, 1), 0)], + ], + }, + "d": { + "img": [ + [("START_box", "box", (1, 1), 0)], + ], + "omg": [ + [("s", "box", (1, 1), 0)], + ], + }, + "s": { + "img": [ + [("e", "container", (1, 1), 0), ("b", "order", (1, -1), 0)], + [("d", "box", (1, 1), 0), ("b", "order", (1, 1), 0)], + ], + "omg": [ + [("END_container", "container", (1, 1), 0), ("r", "order", (1, -1), 0)], + [("END_box", "box", (1, 1), 0), ("r", "order", (1, 1), 0)], + ], + }, + "END_container": { + "img": [ + [("s", "container", (1, 1), 0)], + ], + }, + "END_box": { + "img": [ + [("s", "box", (1, 1), 0)], + ], + }, + "r": { + "img": [ + [("s", "order", (1, 1), 0)], + [("s", "order", (1, -1), 0)], + ], + "omg": [ + [("ti", "order", (1, 1), 1), ("si", "order", (0, -1), 1), ("da", "order", (1, 1), 2), ("ba", "order", (0, -1), 2)], + ], + }, + "ti": { + "img": [ + [("r", "order", (1, 1), 0)], + ], + "omg": [ + [("END_order", "order", (1, 1), 0)], + ], + }, + "si": { + "img": [ + [("r", "order", (1, -1), 0)], + ], + "omg": [ + [("END_order", "order", (1, -1), 0)], + ], + }, + "da": { + "img": [ + [("r", "order", (1, 1), 0)], + ], + "omg": [ + [("END_order", "order", (1, 1), 0)], + ], + }, + "ba": { + "img": [ + [("r", "order", (1, -1), 0)], + ], + "omg": [ + [("END_order", "order", (1, -1), 0)], + ], + }, + "END_order": { + "img": [ + [("ti", "order", (1, 1), 0)], + [("si", "order", (1, 1), 0)], + [("da", "order", (1, 1), 0)], + [("ba", "order", (1, 1), 0)], + ], + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONSTRUCTOR 03") + print(occn) + + # also test exceptions of the conversion to OCPN (no test of correctness here) + ocpn = converter.apply(occn) + print(ocpn) + + + def test_conversion_basic(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, 1), 0)], + ], + "omg": [ + [("END_order", "order", (1, 1), 0)], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, 1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\n================TEST OCCN CONVERSION BASIC================") + print("OCCN:") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + ] + + binding_place_names = [ + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding_global_input", + ] + + places = {} + for name in order_place_names: + places[name] = OCPetriNet.Place(name, "order") + for name in binding_place_names: + places[name] = OCPetriNet.Place(name, "_binding") + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + } + for n in [26, 29, 32, 35]: + t_name = f"_silent#{n}" + transitions[t_name] = OCPetriNet.Transition(t_name, None) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place --------------------------------------------------- + connect(transitions["END_order"], places["p_END_order_o_order"], "order", arcs) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], places["p_START_order_o_order"], "order", arcs + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect(transitions["_silent#26"], places["p_a_i_order"], "order", arcs) + connect( + transitions["_silent#26"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#29"], places["p_arc(a,END_order)_order"], "order", arcs + ) + connect( + transitions["_silent#29"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#32"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#32"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect(transitions["_silent#35"], places["p_END_order_i_order"], "order", arcs) + connect( + transitions["_silent#35"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect(transitions["a"], places["p_a_o_order"], "order", arcs) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + # place ➜ transition --------------------------------------------------- + connect(places["p_END_order_i_order"], transitions["END_order"], "order", arcs) + connect( + places["p_START_order_i_order"], transitions["START_order"], "order", arcs + ) + + connect( + places["p_START_order_o_order"], transitions["_silent#32"], "order", arcs + ) + connect(places["p_a_i_order"], transitions["a"], "order", arcs) + connect(places["p_a_o_order"], transitions["_silent#29"], "order", arcs) + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#26"], + "order", + arcs, + ) + connect( + places["p_arc(a,END_order)_order"], transitions["_silent#35"], "order", arcs + ) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#32"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#29"], "_binding", arcs + ) + + for tgt in ["START_order", "_silent#26", "_silent#35"]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN Basic", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_multi(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [("END_order", "order", (1, -1), 0)], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION MULTI") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + ] + + binding_place_names = [ + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "order") for n in order_place_names} + places.update({n: OCPetriNet.Place(n, "_binding") for n in binding_place_names}) + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + } + for num in [143, 146, 149, 152]: + t = f"_silent#{num}" + transitions[t] = OCPetriNet.Transition(t, None) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place + connect( + transitions["END_order"], + places["p_END_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#143"], + places["p_a_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#143"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#146"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#146"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#149"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#149"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#152"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#152"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + # place ➜ transition + connect( + places["p_END_order_i_order"], + transitions["END_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_START_order_o_order"], + transitions["_silent#149"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + connect( + places["p_a_o_order"], + transitions["_silent#146"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#143"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#152"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#149"], + "_binding", + arcs, + ) + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#146"], "_binding", arcs + ) + + for tgt in ["START_order", "_silent#143", "_silent#152"]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_combined(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, 1), 0)], + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, 1), 0)], + ], + "omg": [ + [("END_order", "order", (1, 1), 0)], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, 1), 0)], + [("a", "order", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION COMBINED") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + ] + + binding_place_names = [ + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding_global_input", + ] + + places = {} + for n in order_place_names: + places[n] = OCPetriNet.Place(n, "order") + for n in binding_place_names: + places[n] = OCPetriNet.Place(n, "_binding") + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + } + for num in [38, 41, 44, 47, 50, 53]: + t = f"_silent#{num}" + transitions[t] = OCPetriNet.Transition(t, None) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place + connect( + transitions["END_order"], + places["p_END_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect(transitions["_silent#38"], places["p_a_i_order"], "order", arcs) + connect( + transitions["_silent#38"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#41"], places["p_arc(a,END_order)_order"], "order", arcs + ) + connect( + transitions["_silent#41"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#44"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#44"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#47"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#47"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect(transitions["_silent#50"], places["p_END_order_i_order"], "order", arcs) + connect( + transitions["_silent#50"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#53"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#53"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect(transitions["a"], places["p_a_o_order"], "order", arcs) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + # place ➜ transition + connect( + places["p_END_order_i_order"], + transitions["END_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_START_order_o_order"], transitions["_silent#44"], "order", arcs + ) + connect( + places["p_START_order_o_order"], + transitions["_silent#47"], + "order", + arcs, + is_variable=True, + ) + + connect(places["p_a_i_order"], transitions["a"], "order", arcs) + connect(places["p_a_o_order"], transitions["_silent#41"], "order", arcs) + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#38"], + "order", + arcs, + ) + connect( + places["p_arc(a,END_order)_order"], transitions["_silent#50"], "order", arcs + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#53"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#44"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#47"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#41"], "_binding", arcs + ) + + for tgt in ["START_order", "_silent#38", "_silent#50", "_silent#53"]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_multi_marker(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (2, 2), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ("b", "order", (1, 1), 0), + ], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, 1), 0)], + [("b", "order", (1, 1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION MULTI MARKER") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Excpected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + "p_arc(a,b)_order", + "p_arc(b,END_order)_order", + "p_b_i_order", + "p_b_o_order", + ] + + binding_place_names = [ + "p_binding#230_1", + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding#b_input", + "p_binding#b_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "order") for n in order_place_names} + places.update({n: OCPetriNet.Place(n, "_binding") for n in binding_place_names}) + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + "b": OCPetriNet.Transition("b", "b"), + } + + for code in ["228", "231_1", "231_2", "232", "235", "238", "241", "244", "247"]: + transitions[f"_silent#{code}"] = OCPetriNet.Transition( + f"_silent#{code}", None + ) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place -------------------------------------------------- + connect( + transitions["END_order"], places["p_END_order_o_order"], "order", arcs + ) # non-variable + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#228"], + places["p_a_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#228"], places["p_binding#a_input"], "_binding", arcs + ) + + connect(transitions["_silent#231_1"], places["p_a_o_order"], "order", arcs) + connect( + transitions["_silent#231_1"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#231_1"], places["p_binding#230_1"], "_binding", arcs + ) + + connect( + transitions["_silent#231_2"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#231_2"], places["p_binding#230_1"], "_binding", arcs + ) + + connect(transitions["_silent#232"], places["p_arc(a,b)_order"], "order", arcs) + connect( + transitions["_silent#232"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect(transitions["_silent#235"], places["p_b_i_order"], "order", arcs) + connect( + transitions["_silent#235"], places["p_binding#b_input"], "_binding", arcs + ) + + connect( + transitions["_silent#238"], + places["p_arc(b,END_order)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#238"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#241"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#241"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#244"], places["p_END_order_i_order"], "order", arcs + ) + connect( + transitions["_silent#244"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#247"], places["p_END_order_i_order"], "order", arcs + ) + connect( + transitions["_silent#247"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + connect(transitions["b"], places["p_b_o_order"], "order", arcs) + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + # place ➜ transition --------------------------------------------------- + connect(places["p_END_order_i_order"], transitions["END_order"], "order", arcs) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_START_order_o_order"], + transitions["_silent#241"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + for t in ["_silent#231_1", "_silent#231_2", "_silent#232"]: + connect(places["p_a_o_order"], transitions[t], "order", arcs) + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#228"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#244"], + "order", + arcs, + ) + connect(places["p_arc(a,b)_order"], transitions["_silent#235"], "order", arcs) + connect( + places["p_arc(b,END_order)_order"], + transitions["_silent#247"], + "order", + arcs, + ) + + connect(places["p_b_i_order"], transitions["b"], "order", arcs) + connect(places["p_b_o_order"], transitions["_silent#238"], "order", arcs) + + connect(places["p_binding#230_1"], transitions["_silent#232"], "_binding", arcs) + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#241"], + "_binding", + arcs, + ) + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + for t in ["_silent#231_1", "_silent#231_2"]: + connect(places["p_binding#a_output"], transitions[t], "_binding", arcs) + + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#b_output"], transitions["_silent#238"], "_binding", arcs + ) + + for tgt in [ + "START_order", + "_silent#228", + "_silent#235", + "_silent#244", + "_silent#247", + ]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_multi_square_marker(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ("b", "order", (1, 1), 0), + ], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, -1), 0), ("b", "order", (1, 1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION MULTI SQUARE MARKER") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + place_specs = [ + # order places + ("p_END_order_i_order", "order"), + ("p_END_order_o_order", "order"), + ("p_START_order_i_order", "order"), + ("p_START_order_o_order", "order"), + ("p_a_i_order", "order"), + ("p_a_o_order", "order"), + ("p_arc(START_order,a)_order", "order"), + ("p_arc(a,END_order)_order", "order"), + ("p_arc(a,b)_order", "order"), + ("p_arc(b,END_order)_order", "order"), + ("p_b_i_order", "order"), + ("p_b_o_order", "order"), + # _binding places + ("p_binding#302_1", "_binding"), + ("p_binding#315_1", "_binding"), + ("p_binding#123123", "_binding"), # <- + ("p_binding#END_order_input", "_binding"), + ("p_binding#START_order_output", "_binding"), + ("p_binding#a_input", "_binding"), + ("p_binding#a_output", "_binding"), + ("p_binding#b_input", "_binding"), + ("p_binding#b_output", "_binding"), + ("p_binding_global_input", "_binding"), + ] + + places = {name: OCPetriNet.Place(name, ot) for (name, ot) in place_specs} + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transition_specs = [ + ("END_order", "END_order"), + ("START_order", "START_order"), + ("_silent#300", None), + ("_silent#303_1", None), + ("_silent#303_2", None), + ("_silent#304", None), + ("_silent#307", None), + ("_silent#310", None), + ("_silent#313", None), + ("_silent#316", None), + ("_silent#317", None), + ("_silent#123123", None), # <- + ("a", "a"), + ("b", "b"), + ] + + transitions = { + name: OCPetriNet.Transition(name, label) + for (name, label) in transition_specs + } + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # -- Transitions to places -- + connect( + transitions["END_order"], + places["p_END_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + connect( + transitions["_silent#300"], + places["p_a_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#300"], places["p_binding#a_input"], "_binding", arcs + ) + connect(transitions["_silent#303_1"], places["p_a_o_order"], "order", arcs) + connect(transitions["_silent#303_1"], places["p_arc(a,b)_order"], "order", arcs) + connect( + transitions["_silent#303_1"], places["p_binding#302_1"], "_binding", arcs + ) + connect(transitions["_silent#303_2"], places["p_arc(a,b)_order"], "order", arcs) + connect( + transitions["_silent#303_2"], places["p_binding#302_1"], "_binding", arcs + ) + connect( + transitions["_silent#304"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#304"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect(transitions["_silent#307"], places["p_b_i_order"], "order", arcs) + connect( + transitions["_silent#307"], places["p_binding#b_input"], "_binding", arcs + ) + connect( + transitions["_silent#310"], + places["p_arc(b,END_order)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#310"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#313"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#313"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#316"], places["p_END_order_i_order"], "order", arcs + ) + connect(transitions["_silent#316"], places["p_binding#315_1"], "_binding", arcs) + connect( + transitions["_silent#317"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#123123"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#317"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#317"], + places["p_binding#123123"], + "_binding", + arcs, + ) + connect( + places["p_binding#123123"], + transitions["_silent#123123"], + "_binding", + arcs, + ) + connect( + transitions["_silent#123123"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + connect(transitions["b"], places["p_b_o_order"], "order", arcs) + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + # -- Places to transitions -- + connect( + places["p_END_order_i_order"], + transitions["END_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_o_order"], + transitions["_silent#313"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + connect(places["p_a_o_order"], transitions["_silent#303_1"], "order", arcs) + connect(places["p_a_o_order"], transitions["_silent#303_2"], "order", arcs) + connect( + places["p_a_o_order"], + transitions["_silent#304"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#300"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#317"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#123123"], + "order", + arcs, + is_variable=True, + ) + connect(places["p_arc(a,b)_order"], transitions["_silent#307"], "order", arcs) + connect( + places["p_arc(b,END_order)_order"], + transitions["_silent#316"], + "order", + arcs, + ) + connect(places["p_b_i_order"], transitions["b"], "order", arcs) + connect(places["p_b_o_order"], transitions["_silent#310"], "order", arcs) + connect(places["p_binding#302_1"], transitions["_silent#304"], "_binding", arcs) + connect(places["p_binding#315_1"], transitions["_silent#317"], "_binding", arcs) + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#313"], + "_binding", + arcs, + ) + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#303_1"], "_binding", arcs + ) + connect( + places["p_binding#a_output"], transitions["_silent#303_2"], "_binding", arcs + ) + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#b_output"], transitions["_silent#310"], "_binding", arcs + ) + connect( + places["p_binding_global_input"], + transitions["START_order"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#300"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#307"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#316"], + "_binding", + arcs, + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_triple_marker(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ("b", "order", (1, 1), 0), + ("c", "order", (1, -1), 0), + ], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "c": { + "img": [ + [("a", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [ + ("a", "order", (1, -1), 0), + ("b", "order", (1, 1), 0), + ("c", "order", (1, -1), 0), + ], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION TRIPLE MARKER") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + "p_arc(a,b)_order", + "p_arc(a,c)_order", + "p_arc(b,END_order)_order", + "p_arc(c,END_order)_order", + "p_b_i_order", + "p_b_o_order", + "p_c_i_order", + "p_c_o_order", + ] + + binding_place_names = [ + "p_binding#331_1", + "p_binding#331_2", + "p_binding#333", + "p_binding#342_1", + "p_binding#342_2", + "p_binding#10", + "p_binding#20", + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding#b_input", + "p_binding#b_output", + "p_binding#c_input", + "p_binding#c_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "order") for n in order_place_names} + places.update({n: OCPetriNet.Place(n, "_binding") for n in binding_place_names}) + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + "b": OCPetriNet.Transition("b", "b"), + "c": OCPetriNet.Transition("c", "c"), + } + for code in [ + "320", + "323", + "326", + "329", + "332_1", + "332_2", + "333_1", + "333_2", + "334", + "337", + "340", + "343", + "344", + "345", + "1", + "2", + ]: + transitions[f"_silent#{code}"] = OCPetriNet.Transition( + f"_silent#{code}", None + ) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place -------------------------------------------------- + connect( + transitions["END_order"], + places["p_END_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#320"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#320"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#323"], places["p_binding#c_input"], "_binding", arcs + ) + connect( + transitions["_silent#323"], + places["p_c_i_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + transitions["_silent#326"], + places["p_arc(c,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#326"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#329"], + places["p_a_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#329"], places["p_binding#a_input"], "_binding", arcs + ) + + connect(transitions["_silent#332_1"], places["p_a_o_order"], "order", arcs) + connect(transitions["_silent#332_1"], places["p_arc(a,b)_order"], "order", arcs) + connect( + transitions["_silent#332_1"], places["p_binding#331_1"], "_binding", arcs + ) + + connect(transitions["_silent#332_2"], places["p_arc(a,b)_order"], "order", arcs) + connect( + transitions["_silent#332_2"], places["p_binding#331_1"], "_binding", arcs + ) + + connect( + transitions["_silent#333_1"], + places["p_a_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#333_1"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#333_1"], places["p_binding#333"], "_binding", arcs) + + connect( + transitions["_silent#333_2"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#333_2"], places["p_binding#331_2"], "_binding", arcs + ) + + connect( + transitions["_silent#334"], + places["p_arc(a,c)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#334"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect(transitions["_silent#337"], places["p_b_i_order"], "order", arcs) + connect( + transitions["_silent#337"], places["p_binding#b_input"], "_binding", arcs + ) + + connect( + transitions["_silent#340"], + places["p_arc(b,END_order)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#340"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#343"], places["p_END_order_i_order"], "order", arcs + ) + connect(transitions["_silent#343"], places["p_binding#342_1"], "_binding", arcs) + + connect( + transitions["_silent#344"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#344"], places["p_binding#342_2"], "_binding", arcs) + connect(transitions["_silent#1"], places["p_binding#10"], "_binding", arcs) + + connect( + transitions["_silent#345"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#2"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#345"], + places["p_binding#20"], + "_binding", + arcs, + ) + connect( + transitions["_silent#2"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + connect(transitions["b"], places["p_b_o_order"], "order", arcs) + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + connect( + transitions["c"], places["p_c_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["c"], places["p_binding#c_output"], "_binding", arcs) + + # place ➜ transition --------------------------------------------------- + connect( + places["p_END_order_i_order"], + transitions["END_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_START_order_o_order"], + transitions["_silent#320"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + + for t in ["_silent#332_1", "_silent#332_2"]: + connect(places["p_a_o_order"], transitions[t], "order", arcs) + for t in ["_silent#333_1", "_silent#333_2", "_silent#334"]: + connect( + places["p_a_o_order"], transitions[t], "order", arcs, is_variable=True + ) + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#329"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#344"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#344"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#1"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#1"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect(places["p_arc(a,b)_order"], transitions["_silent#337"], "order", arcs) + connect( + places["p_arc(a,c)_order"], + transitions["_silent#323"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(b,END_order)_order"], + transitions["_silent#343"], + "order", + arcs, + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#345"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#345"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#2"], + "order", + arcs, + is_variable=True, + ) + + connect(places["p_b_i_order"], transitions["b"], "order", arcs) + connect(places["p_b_o_order"], transitions["_silent#340"], "order", arcs) + + connect( + places["p_c_i_order"], transitions["c"], "order", arcs, is_variable=True + ) + connect( + places["p_c_o_order"], + transitions["_silent#326"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_binding#331_1"], transitions["_silent#333_1"], "_binding", arcs + ) + connect(places["p_binding#331_2"], transitions["_silent#334"], "_binding", arcs) + connect(places["p_binding#333"], transitions["_silent#333_2"], "_binding", arcs) + connect(places["p_binding#342_1"], transitions["_silent#344"], "_binding", arcs) + connect(places["p_binding#10"], transitions["_silent#345"], "_binding", arcs) + connect(places["p_binding#20"], transitions["_silent#2"], "_binding", arcs) + connect(places["p_binding#342_2"], transitions["_silent#1"], "_binding", arcs) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#320"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + for t in ["_silent#332_1", "_silent#332_2"]: + connect(places["p_binding#a_output"], transitions[t], "_binding", arcs) + + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#b_output"], transitions["_silent#340"], "_binding", arcs + ) + + connect(places["p_binding#c_input"], transitions["c"], "_binding", arcs) + connect( + places["p_binding#c_output"], transitions["_silent#326"], "_binding", arcs + ) + + for tgt in [ + "START_order", + "_silent#323", + "_silent#329", + "_silent#337", + "_silent#343", + ]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_key(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 1), + ("b", "order", (1, 1), 1), + ("c", "order", (1, -1), 1), + ], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "c": { + "img": [ + [("a", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [ + ("a", "order", (1, -1), 0), + ("b", "order", (1, 1), 0), + ("c", "order", (1, -1), 0), + ], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION KEY") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + "p_arc(a,b)_order", + "p_arc(a,c)_order", + "p_arc(b,END_order)_order", + "p_arc(c,END_order)_order", + "p_b_i_order", + "p_b_o_order", + "p_c_i_order", + "p_c_o_order", + ] + + binding_place_names = [ + "p_binding#10", + "p_binding#20", + "p_binding#68_1", + "p_binding#68_2", + "p_binding#79_1", + "p_binding#79_2", + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding#b_input", + "p_binding#b_output", + "p_binding#c_input", + "p_binding#c_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "order") for n in order_place_names} + places.update({n: OCPetriNet.Place(n, "_binding") for n in binding_place_names}) + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + "b": OCPetriNet.Transition("b", "b"), + "c": OCPetriNet.Transition("c", "c"), + } + for num in [56, 59, 62, 65, 69, 70, 71, 74, 77, 80, 81, 82, 1, 2]: + transitions[f"_silent#{num}"] = OCPetriNet.Transition( + f"_silent#{num}", None + ) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place --------------------------------------------------- + connect( + transitions["END_order"], + places["p_END_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#56"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#56"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#59"], + places["p_c_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#59"], places["p_binding#c_input"], "_binding", arcs + ) + + connect( + transitions["_silent#62"], + places["p_arc(c,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#62"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#65"], + places["p_a_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#65"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#69"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#69"], places["p_binding#68_1"], "_binding", arcs) + + connect( + transitions["_silent#70"], + places["p_arc(a,c)_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#70"], places["p_binding#68_2"], "_binding", arcs) + + connect( + transitions["_silent#71"], places["p_arc(a,b)_order"], "order", arcs + ) # non-variable + connect( + transitions["_silent#71"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#74"], places["p_b_i_order"], "order", arcs + ) # non-variable + connect( + transitions["_silent#74"], places["p_binding#b_input"], "_binding", arcs + ) + + connect( + transitions["_silent#77"], places["p_arc(b,END_order)_order"], "order", arcs + ) # non-variable + connect( + transitions["_silent#77"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#80"], places["p_END_order_i_order"], "order", arcs + ) # non-variable + connect(transitions["_silent#80"], places["p_binding#79_1"], "_binding", arcs) + + connect( + transitions["_silent#81"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#81"], places["p_binding#79_2"], "_binding", arcs) + + connect( + transitions["_silent#1"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#1"], places["p_binding#10"], "_binding", arcs) + + connect( + transitions["_silent#82"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#82"], + places["p_binding#20"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#2"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#2"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + connect(transitions["b"], places["p_b_o_order"], "order", arcs) # non-variable + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + connect( + transitions["c"], places["p_c_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["c"], places["p_binding#c_output"], "_binding", arcs) + + # place ➜ transition --------------------------------------------------- + connect( + places["p_END_order_i_order"], + transitions["END_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#81"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#82"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_START_order_o_order"], + transitions["_silent#56"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + connect( + places["p_a_o_order"], + transitions["_silent#69"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_a_o_order"], + transitions["_silent#70"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_a_o_order"], transitions["_silent#71"], "order", arcs + ) # non-variable + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#65"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#81"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#1"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,b)_order"], transitions["_silent#74"], "order", arcs + ) # non-variable + connect( + places["p_arc(a,c)_order"], + transitions["_silent#59"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(b,END_order)_order"], transitions["_silent#80"], "order", arcs + ) # non-variable + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#82"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#2"], + "order", + arcs, + is_variable=True, + ) + + connect(places["p_b_i_order"], transitions["b"], "order", arcs) # non-variable + connect( + places["p_b_o_order"], transitions["_silent#77"], "order", arcs + ) # non-variable + + connect(places["p_binding#10"], transitions["_silent#82"], "_binding", arcs) + connect(places["p_binding#20"], transitions["_silent#2"], "_binding", arcs) + connect(places["p_binding#68_1"], transitions["_silent#70"], "_binding", arcs) + connect(places["p_binding#68_2"], transitions["_silent#71"], "_binding", arcs) + connect(places["p_binding#79_1"], transitions["_silent#81"], "_binding", arcs) + connect(places["p_binding#79_2"], transitions["_silent#1"], "_binding", arcs) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#56"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#69"], "_binding", arcs + ) + + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#b_output"], transitions["_silent#77"], "_binding", arcs + ) + + connect(places["p_binding#c_input"], transitions["c"], "_binding", arcs) + connect( + places["p_binding#c_output"], transitions["_silent#62"], "_binding", arcs + ) + + for tgt in [ + "START_order", + "_silent#59", + "_silent#65", + "_silent#74", + "_silent#80", + ]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + connect( + places["p_c_i_order"], transitions["c"], "order", arcs, is_variable=True + ) + connect( + places["p_c_o_order"], + transitions["_silent#62"], + "order", + arcs, + is_variable=True, + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_key_input(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 1), + ("b", "order", (1, 1), 1), + ("c", "order", (1, -1), 1), + ], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "c": { + "img": [ + [("a", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [ + ("a", "order", (1, -1), 1), + ("b", "order", (1, 1), 1), + ("c", "order", (1, -1), 1), + ], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION KEY INPUT") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + "p_arc(a,b)_order", + "p_arc(a,c)_order", + "p_arc(b,END_order)_order", + "p_arc(c,END_order)_order", + "p_b_i_order", + "p_b_o_order", + "p_c_i_order", + "p_c_o_order", + ] + + binding_place_names = [ + "p_binding#108_1", + "p_binding#108_2", + "p_binding#97_1", + "p_binding#97_2", + "p_binding#10", + "p_binding#20", + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding#b_input", + "p_binding#b_output", + "p_binding#c_input", + "p_binding#c_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "order") for n in order_place_names} + places.update({n: OCPetriNet.Place(n, "_binding") for n in binding_place_names}) + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + "b": OCPetriNet.Transition("b", "b"), + "c": OCPetriNet.Transition("c", "c"), + } + + for num in [85, 88, 91, 94, 98, 99, 100, 103, 106, 109, 110, 111, 1, 2]: + transitions[f"_silent#{num}"] = OCPetriNet.Transition( + f"_silent#{num}", None + ) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place --------------------------------------------------- + connect( + transitions["END_order"], + places["p_END_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect(transitions["_silent#100"], places["p_arc(a,b)_order"], "order", arcs) + connect( + transitions["_silent#100"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect(transitions["_silent#103"], places["p_b_i_order"], "order", arcs) + connect( + transitions["_silent#103"], places["p_binding#b_input"], "_binding", arcs + ) + + connect( + transitions["_silent#106"], + places["p_arc(b,END_order)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#106"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#109"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#109"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#109"], places["p_binding#108_2"], "_binding", arcs) + connect(places["p_binding#108_1"], transitions["_silent#109"], "_binding", arcs) + connect(places["p_binding#108_2"], transitions["_silent#1"], "_binding", arcs) + + connect( + transitions["_silent#110"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#110"], places["p_binding#20"], "_binding", arcs) + + connect( + transitions["_silent#111"], places["p_END_order_i_order"], "order", arcs + ) + connect( + transitions["_silent#2"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#2"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#2"], + "order", + arcs, + is_variable=True + ) + connect( + transitions["_silent#111"], + places["p_binding#108_1"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#85"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#85"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#88"], + places["p_c_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#88"], places["p_binding#c_input"], "_binding", arcs + ) + + connect( + transitions["_silent#91"], + places["p_arc(c,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#91"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#94"], + places["p_a_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#94"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#98"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#1"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#1"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#1"], + places["p_binding#10"], + "_binding", + arcs, + ) + connect(transitions["_silent#98"], places["p_binding#97_1"], "_binding", arcs) + + connect( + transitions["_silent#99"], + places["p_arc(a,c)_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#99"], places["p_binding#97_2"], "_binding", arcs) + + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + connect(transitions["b"], places["p_b_o_order"], "order", arcs) + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + connect( + transitions["c"], places["p_c_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["c"], places["p_binding#c_output"], "_binding", arcs) + + # place ➜ transition --------------------------------------------------- + connect( + places["p_END_order_i_order"], + transitions["END_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_START_order_o_order"], + transitions["_silent#85"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + connect(places["p_a_o_order"], transitions["_silent#100"], "order", arcs) + connect( + places["p_a_o_order"], + transitions["_silent#98"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_a_o_order"], + transitions["_silent#99"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#94"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#109"], + "order", + arcs, + is_variable=True, + ) + connect(places["p_arc(a,b)_order"], transitions["_silent#103"], "order", arcs) + connect( + places["p_arc(a,c)_order"], + transitions["_silent#88"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(b,END_order)_order"], + transitions["_silent#111"], + "order", + arcs, + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#110"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#110"], + "order", + arcs, + is_variable=True, + ) + + connect(places["p_b_i_order"], transitions["b"], "order", arcs) + connect(places["p_b_o_order"], transitions["_silent#106"], "order", arcs) + + connect(places["p_binding#10"], transitions["_silent#110"], "_binding", arcs) + connect(places["p_binding#108_2"], transitions["_silent#111"], "_binding", arcs) + connect(places["p_binding#97_1"], transitions["_silent#99"], "_binding", arcs) + connect(places["p_binding#97_2"], transitions["_silent#100"], "_binding", arcs) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#85"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#98"], "_binding", arcs + ) + + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#b_output"], transitions["_silent#106"], "_binding", arcs + ) + + connect(places["p_binding#c_input"], transitions["c"], "_binding", arcs) + connect( + places["p_binding#c_output"], transitions["_silent#91"], "_binding", arcs + ) + + for tgt in [ + "START_order", + "_silent#103", + "_silent#109", + "_silent#88", + "_silent#94", + ]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + connect( + places["p_c_i_order"], transitions["c"], "order", arcs, is_variable=True + ) + connect( + places["p_c_o_order"], + transitions["_silent#91"], + "order", + arcs, + is_variable=True, + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_key_order(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 1), + ("b", "order", (1, 1), 0), + ("c", "order", (1, -1), 1), + ], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "c": { + "img": [ + [("a", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [ + ("a", "order", (1, -1), 0), + ("b", "order", (1, 1), 0), + ("c", "order", (1, -1), 0), + ], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION KEY ORDER") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + "p_arc(a,b)_order", + "p_arc(a,c)_order", + "p_arc(b,END_order)_order", + "p_arc(c,END_order)_order", + "p_b_i_order", + "p_b_o_order", + "p_c_i_order", + "p_c_o_order", + ] + + binding_place_names = [ + "p_binding#125_1", + "p_binding#127_1", + "p_binding#137_1", + "p_binding#137_2", + "p_binding#10", + "p_binding#20", + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding#b_input", + "p_binding#b_output", + "p_binding#c_input", + "p_binding#c_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "order") for n in order_place_names} + places.update({n: OCPetriNet.Place(n, "_binding") for n in binding_place_names}) + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + "b": OCPetriNet.Transition("b", "b"), + "c": OCPetriNet.Transition("c", "c"), + } + + for code in [ + "114", + "117", + "120", + "123", + "126_1", + "126_2", + "128", + "129", + "132", + "135", + "138", + "139", + "140", + "1", + "2" + ]: + name = f"_silent#{code}" + transitions[name] = OCPetriNet.Transition(name, None) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # ---- transition ➜ place -------------------------------------------- + connect( + transitions["END_order"], + places["p_END_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#114"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#114"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#117"], + places["p_c_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#117"], places["p_binding#c_input"], "_binding", arcs + ) + + connect( + transitions["_silent#120"], + places["p_arc(c,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#2"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#120"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#123"], + places["p_a_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#123"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#126_1"], places["p_a_o_order"], "order", arcs + ) # non-variable + connect(transitions["_silent#126_1"], places["p_arc(a,b)_order"], "order", arcs) + connect( + transitions["_silent#126_1"], places["p_binding#125_1"], "_binding", arcs + ) + + connect(transitions["_silent#126_2"], places["p_arc(a,b)_order"], "order", arcs) + connect( + transitions["_silent#126_2"], places["p_binding#125_1"], "_binding", arcs + ) + + connect( + transitions["_silent#128"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#128"], places["p_binding#127_1"], "_binding", arcs) + + connect( + transitions["_silent#129"], + places["p_arc(a,c)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#129"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#132"], places["p_b_i_order"], "order", arcs + ) # non-variable + connect( + transitions["_silent#132"], places["p_binding#b_input"], "_binding", arcs + ) + + connect( + transitions["_silent#135"], + places["p_arc(b,END_order)_order"], + "order", + arcs, + ) # non-variable + connect( + transitions["_silent#135"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#138"], places["p_END_order_i_order"], "order", arcs + ) # non-variable + connect(transitions["_silent#138"], places["p_binding#137_1"], "_binding", arcs) + + connect( + transitions["_silent#139"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#1"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#2"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#139"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#139"], places["p_binding#137_2"], "_binding", arcs) + + connect( + transitions["_silent#140"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#140"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#140"], + places["p_binding#20"], + "_binding", + arcs, + ) + connect( + places["p_binding#20"], + transitions["_silent#2"], + "_binding", + arcs, + ) + connect( + transitions["_silent#2"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + connect(transitions["b"], places["p_b_o_order"], "order", arcs) # non-variable + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + connect( + transitions["c"], places["p_c_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["c"], places["p_binding#c_output"], "_binding", arcs) + + # ---- place ➜ transition --------------------------------------------- + connect( + places["p_END_order_i_order"], + transitions["END_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_START_order_o_order"], + transitions["_silent#114"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + connect(places["p_a_o_order"], transitions["_silent#126_1"], "order", arcs) + connect(places["p_a_o_order"], transitions["_silent#126_2"], "order", arcs) + connect( + places["p_a_o_order"], + transitions["_silent#128"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_a_o_order"], + transitions["_silent#129"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#123"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#139"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#1"], + "order", + arcs, + is_variable=True, + ) + connect(places["p_arc(a,b)_order"], transitions["_silent#132"], "order", arcs) + connect( + places["p_arc(a,c)_order"], + transitions["_silent#117"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(b,END_order)_order"], + transitions["_silent#138"], + "order", + arcs, + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#140"], + "order", + arcs, + is_variable=True, + ) + + connect(places["p_b_i_order"], transitions["b"], "order", arcs) + connect(places["p_b_o_order"], transitions["_silent#135"], "order", arcs) + + connect(places["p_binding#125_1"], transitions["_silent#128"], "_binding", arcs) + connect(places["p_binding#127_1"], transitions["_silent#129"], "_binding", arcs) + connect(places["p_binding#137_1"], transitions["_silent#139"], "_binding", arcs) + connect(places["p_binding#137_2"], transitions["_silent#1"], "_binding", arcs) + connect(places["p_binding#10"], transitions["_silent#140"], "_binding", arcs) + connect(transitions["_silent#1"], places["p_binding#10"], "_binding", arcs) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#114"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#126_1"], "_binding", arcs + ) + connect( + places["p_binding#a_output"], transitions["_silent#126_2"], "_binding", arcs + ) + + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#b_output"], transitions["_silent#135"], "_binding", arcs + ) + + connect(places["p_binding#c_input"], transitions["c"], "_binding", arcs) + connect( + places["p_binding#c_output"], transitions["_silent#120"], "_binding", arcs + ) + + for tgt in [ + "START_order", + "_silent#117", + "_silent#123", + "_silent#132", + "_silent#138", + ]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + connect( + places["p_c_i_order"], transitions["c"], "order", arcs, is_variable=True + ) + connect( + places["p_c_o_order"], + transitions["_silent#120"], + "order", + arcs, + is_variable=True, + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_multi_key(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 1), + ("b", "order", (1, 1), 1), + ("c", "order", (1, -1), 1), + ], + [ + ("END_order", "order", (1, -1), 2), + ("b", "order", (1, 1), 2), + ], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "c": { + "img": [ + [("a", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [ + ("a", "order", (1, -1), 0), + ("b", "order", (1, 1), 0), + ("c", "order", (1, -1), 0), + ], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION MULTI KEY") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + "p_arc(a,b)_order", + "p_arc(a,c)_order", + "p_arc(b,END_order)_order", + "p_arc(c,END_order)_order", + "p_b_i_order", + "p_b_o_order", + "p_c_i_order", + "p_c_o_order", + ] + + binding_place_names = [ + "p_binding#167_1", + "p_binding#167_2", + "p_binding#173_1", + "p_binding#183_1", + "p_binding#183_2", + "p_binding#10", + "p_binding#20", + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding#b_input", + "p_binding#b_output", + "p_binding#c_input", + "p_binding#c_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "order") for n in order_place_names} + places.update({n: OCPetriNet.Place(n, "_binding") for n in binding_place_names}) + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + "b": OCPetriNet.Transition("b", "b"), + "c": OCPetriNet.Transition("c", "c"), + } + for num in [ + 155, + 158, + 161, + 164, + 168, + 169, + 170, + 174, + 175, + 178, + 181, + 184, + 185, + 186, + 1, + 2 + ]: + t_name = f"_silent#{num}" + transitions[t_name] = OCPetriNet.Transition(t_name, None) + + # --------------------------------------------------------------------- + # Arcs (using the `connect` helper defined earlier) + # --------------------------------------------------------------------- + arcs = [] + + # ---- transition ➜ place -------------------------------------------- + connect( + transitions["END_order"], + places["p_END_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#155"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#155"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#158"], + places["p_c_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#158"], places["p_binding#c_input"], "_binding", arcs + ) + + connect( + transitions["_silent#161"], + places["p_arc(c,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#2"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#2"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#161"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#164"], + places["p_a_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#164"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#168"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#168"], places["p_binding#167_1"], "_binding", arcs) + + connect( + transitions["_silent#169"], + places["p_arc(a,c)_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#169"], places["p_binding#167_2"], "_binding", arcs) + + connect( + transitions["_silent#170"], places["p_arc(a,b)_order"], "order", arcs + ) # non-variable + connect( + transitions["_silent#170"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#174"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#174"], places["p_binding#173_1"], "_binding", arcs) + + connect( + transitions["_silent#175"], places["p_arc(a,b)_order"], "order", arcs + ) # non-variable + connect( + transitions["_silent#175"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#178"], places["p_b_i_order"], "order", arcs + ) # non-variable + connect( + transitions["_silent#178"], places["p_binding#b_input"], "_binding", arcs + ) + + connect( + transitions["_silent#181"], + places["p_arc(b,END_order)_order"], + "order", + arcs, + ) # non-variable + connect( + transitions["_silent#181"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#184"], places["p_END_order_i_order"], "order", arcs + ) # non-variable + connect(transitions["_silent#184"], places["p_binding#183_1"], "_binding", arcs) + + connect( + transitions["_silent#185"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#185"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#185"], places["p_binding#183_2"], "_binding", arcs) + + connect( + transitions["_silent#186"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#186"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#186"], + places["p_binding#20"], + "_binding", + arcs, + ) + connect( + transitions["_silent#2"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + connect( + places["p_binding#20"], + transitions["_silent#2"], + "_binding", + arcs, + ) + + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + connect(transitions["b"], places["p_b_o_order"], "order", arcs) + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + connect( + transitions["c"], places["p_c_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["c"], places["p_binding#c_output"], "_binding", arcs) + + # ---- place ➜ transition --------------------------------------------- + connect( + places["p_END_order_i_order"], + transitions["END_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_START_order_o_order"], + transitions["_silent#155"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + connect( + places["p_a_o_order"], + transitions["_silent#168"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_a_o_order"], + transitions["_silent#169"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_a_o_order"], transitions["_silent#170"], "order", arcs + ) # non-variable + connect( + places["p_a_o_order"], + transitions["_silent#174"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_a_o_order"], transitions["_silent#175"], "order", arcs + ) # non-variable + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#164"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#185"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#1"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#1"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,b)_order"], transitions["_silent#178"], "order", arcs + ) # non-variable + connect( + places["p_arc(a,c)_order"], + transitions["_silent#158"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(b,END_order)_order"], + transitions["_silent#184"], + "order", + arcs, + ) # non-variable + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#186"], + "order", + arcs, + is_variable=True, + ) + + connect(places["p_b_i_order"], transitions["b"], "order", arcs) + connect(places["p_b_o_order"], transitions["_silent#181"], "order", arcs) + + connect(places["p_binding#167_1"], transitions["_silent#169"], "_binding", arcs) + connect(places["p_binding#167_2"], transitions["_silent#170"], "_binding", arcs) + connect(places["p_binding#173_1"], transitions["_silent#175"], "_binding", arcs) + connect(places["p_binding#183_1"], transitions["_silent#185"], "_binding", arcs) + connect(places["p_binding#183_2"], transitions["_silent#1"], "_binding", arcs) + connect(transitions["_silent#1"], places["p_binding#10"], "_binding", arcs) + connect(places["p_binding#10"], transitions["_silent#186"], "_binding", arcs) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#155"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#168"], "_binding", arcs + ) + connect( + places["p_binding#a_output"], transitions["_silent#174"], "_binding", arcs + ) + + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#b_output"], transitions["_silent#181"], "_binding", arcs + ) + + connect(places["p_binding#c_input"], transitions["c"], "_binding", arcs) + connect( + places["p_binding#c_output"], transitions["_silent#161"], "_binding", arcs + ) + + for tgt in [ + "START_order", + "_silent#158", + "_silent#164", + "_silent#178", + "_silent#184", + ]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + connect( + places["p_c_i_order"], transitions["c"], "order", arcs, is_variable=True + ) + connect( + places["p_c_o_order"], + transitions["_silent#161"], + "order", + arcs, + is_variable=True, + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_multi_key_2(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 1), + ("b", "order", (1, 1), 2), + ("c", "order", (1, -1), 1), + ("d", "order", (1, 1), 2), + ], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "c": { + "img": [ + [("a", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "d": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ], + ], + }, + "END_order": { + "img": [ + [ + ("a", "order", (1, -1), 0), + ("b", "order", (1, 1), 0), + ("c", "order", (1, -1), 0), + ("d", "order", (1, 1), 0), + ], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION MULTI KEY 2") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p#201_X", + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + "p_arc(a,b)_order", + "p_arc(a,c)_order", + "p_arc(a,d)_order", + "p_arc(b,END_order)_order", + "p_arc(c,END_order)_order", + "p_arc(d,END_order)_order", + "p_b_i_order", + "p_b_o_order", + "p_c_i_order", + "p_c_o_order", + "p_d_i_order", + "p_d_o_order", + ] + + binding_place_names = [ + "p_binding#200_1", + "p_binding#201_alpha", + "p_binding#201_beta", + "p_binding#202_1", + "p_binding#205_1", + "p_binding#221_1", + "p_binding#221_2", + "p_binding#221_3", + "p_binding#10", + "p_binding#20", + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding#b_input", + "p_binding#b_output", + "p_binding#c_input", + "p_binding#c_output", + "p_binding#d_input", + "p_binding#d_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "order") for n in order_place_names} + places.update({n: OCPetriNet.Place(n, "_binding") for n in binding_place_names}) + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + "b": OCPetriNet.Transition("b", "b"), + "c": OCPetriNet.Transition("c", "c"), + "d": OCPetriNet.Transition("d", "d"), + } + + for code in [ + "189", + "192", + "195", + "198", + "201_1", + "201_2", + "203", + "204", + "206", + "207", + "210", + "213", + "216", + "219", + "222", + "223", + "224", + "225", + "1", + "2", + "3", + ]: + transitions[f"_silent#{code}"] = OCPetriNet.Transition( + f"_silent#{code}", None + ) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place -------------------------------------------------- + connect( + transitions["END_order"], + places["p_END_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], + places["p_START_order_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#189"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#189"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#192"], places["p_binding#c_input"], "_binding", arcs + ) + connect( + transitions["_silent#192"], + places["p_c_i_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + transitions["_silent#195"], + places["p_arc(c,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#3"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#3"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#195"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#198"], + places["p_a_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#198"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#201_1"], + places["p#201_X"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#201_1"], + places["p_a_o_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#201_1"], + places["p_binding#201_alpha"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#201_2"], + places["p#201_X"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#201_2"], places["p_binding#201_beta"], "_binding", arcs + ) + + connect( + transitions["_silent#203"], places["p_arc(a,b)_order"], "order", arcs + ) # non-variable + connect(transitions["_silent#203"], places["p_binding#202_1"], "_binding", arcs) + + connect( + transitions["_silent#204"], places["p_arc(a,d)_order"], "order", arcs + ) # non-variable + connect(transitions["_silent#204"], places["p_binding#200_1"], "_binding", arcs) + + connect( + transitions["_silent#206"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#206"], places["p_binding#205_1"], "_binding", arcs) + + connect( + transitions["_silent#207"], + places["p_arc(a,c)_order"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#207"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#210"], places["p_binding#d_input"], "_binding", arcs + ) + connect( + transitions["_silent#210"], places["p_d_i_order"], "order", arcs + ) # non-variable + + connect( + transitions["_silent#213"], + places["p_arc(d,END_order)_order"], + "order", + arcs, + ) # non-variable + connect( + transitions["_silent#213"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#216"], places["p_b_i_order"], "order", arcs + ) # non-variable + connect( + transitions["_silent#216"], places["p_binding#b_input"], "_binding", arcs + ) + + connect( + transitions["_silent#219"], + places["p_arc(b,END_order)_order"], + "order", + arcs, + ) # non-variable + connect( + transitions["_silent#219"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#222"], places["p_END_order_i_order"], "order", arcs + ) # non-variable + connect(transitions["_silent#222"], places["p_binding#221_1"], "_binding", arcs) + + connect( + transitions["_silent#223"], places["p_END_order_i_order"], "order", arcs + ) # non-variable + connect( + transitions["_silent#1"], places["p_END_order_i_order"], "order", arcs + ) # non-variable + connect( + places["p_END_order_i_order"], + transitions["_silent#1"], + "order", arcs + ) # non-variable + + connect(transitions["_silent#223"], places["p_binding#221_2"], "_binding", arcs) + connect(transitions["_silent#1"], places["p_binding#221_2"], "_binding", arcs) + + connect( + transitions["_silent#224"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#224"], + "order", + arcs, + is_variable=True, + ) + connect(transitions["_silent#224"], places["p_binding#221_3"], "_binding", arcs) + + connect( + transitions["_silent#225"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_END_order_i_order"], + transitions["_silent#225"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#225"], + places["p_binding#20"], + "_binding", + arcs, + ) + connect( + places["p_binding#20"], + transitions["_silent#3"], + "_binding", + arcs, + ) + connect( + transitions["_silent#3"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + connect(transitions["b"], places["p_b_o_order"], "order", arcs) + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + connect( + transitions["c"], places["p_c_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["c"], places["p_binding#c_output"], "_binding", arcs) + + connect(transitions["d"], places["p_d_o_order"], "order", arcs) + connect(transitions["d"], places["p_binding#d_output"], "_binding", arcs) + + # place ➜ transition --------------------------------------------------- + connect(places["p#201_X"], transitions["_silent#203"], "order", arcs) + connect(places["p#201_X"], transitions["_silent#204"], "order", arcs) + + connect( + places["p_END_order_i_order"], + transitions["END_order"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], + transitions["START_order"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_START_order_o_order"], + transitions["_silent#189"], + "order", + arcs, + is_variable=True, + ) + + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + for t in ["_silent#201_1", "_silent#201_2", "_silent#206", "_silent#207"]: + connect( + places["p_a_o_order"], transitions[t], "order", arcs, is_variable=True + ) + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#198"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#224"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#2"], + "order", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#2"], + places["p_END_order_i_order"], + "order", + arcs, + is_variable=True, + ) + connect(places["p_arc(a,b)_order"], transitions["_silent#216"], "order", arcs) + connect( + places["p_arc(a,c)_order"], + transitions["_silent#192"], + "order", + arcs, + is_variable=True, + ) + connect(places["p_arc(a,d)_order"], transitions["_silent#210"], "order", arcs) + connect( + places["p_arc(b,END_order)_order"], + transitions["_silent#222"], + "order", + arcs, + ) + connect( + places["p_arc(c,END_order)_order"], + transitions["_silent#225"], + "order", + arcs, + is_variable=True, + ) + connect( + places["p_arc(d,END_order)_order"], + transitions["_silent#223"], + "order", + arcs, + ) + connect( + places["p_arc(d,END_order)_order"], + transitions["_silent#1"], + "order", + arcs, + ) + + connect(places["p_b_i_order"], transitions["b"], "order", arcs) + connect(places["p_b_o_order"], transitions["_silent#219"], "order", arcs) + + connect( + places["p_c_i_order"], transitions["c"], "order", arcs, is_variable=True + ) + connect( + places["p_c_o_order"], + transitions["_silent#195"], + "order", + arcs, + is_variable=True, + ) + + connect(places["p_d_i_order"], transitions["d"], "order", arcs) + connect(places["p_d_o_order"], transitions["_silent#213"], "order", arcs) + + connect(places["p_binding#200_1"], transitions["_silent#206"], "_binding", arcs) + connect( + places["p_binding#201_alpha"], + transitions["_silent#201_2"], + "_binding", + arcs, + ) + connect( + places["p_binding#201_beta"], transitions["_silent#203"], "_binding", arcs + ) + connect(places["p_binding#202_1"], transitions["_silent#204"], "_binding", arcs) + connect(places["p_binding#205_1"], transitions["_silent#207"], "_binding", arcs) + connect(places["p_binding#221_1"], transitions["_silent#223"], "_binding", arcs) + connect(places["p_binding#221_1"], transitions["_silent#1"], "_binding", arcs) + connect(places["p_binding#221_2"], transitions["_silent#224"], "_binding", arcs) + connect(places["p_binding#221_3"], transitions["_silent#2"], "_binding", arcs) + connect(transitions["_silent#2"], places["p_binding#10"], "_binding", arcs) + connect(places["p_binding#10"], transitions["_silent#225"], "_binding", arcs) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#189"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#201_1"], "_binding", arcs + ) + + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#b_output"], transitions["_silent#219"], "_binding", arcs + ) + + connect(places["p_binding#c_input"], transitions["c"], "_binding", arcs) + connect( + places["p_binding#c_output"], transitions["_silent#195"], "_binding", arcs + ) + + connect(places["p_binding#d_input"], transitions["d"], "_binding", arcs) + connect( + places["p_binding#d_output"], transitions["_silent#213"], "_binding", arcs + ) + + for tgt in [ + "START_order", + "_silent#192", + "_silent#198", + "_silent#210", + "_silent#216", + "_silent#222", + ]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_multi_ot(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "START_item": { + "img": [], + "omg": [ + [("a", "item", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (1, -1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ("END_item", "item", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, 1), 0)], + ] + }, + "END_item": { + "img": [ + [("a", "item", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION MULTI OT") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + place_specs = [ + # item + ("p_END_item_i_item", "item"), + ("p_END_item_o_item", "item"), + ("p_START_item_i_item", "item"), + ("p_START_item_o_item", "item"), + ("p_a_i_item", "item"), + ("p_a_o_item", "item"), + ("p_arc(START_item,a)_item", "item"), + ("p_arc(a,END_item)_item", "item"), + # order + ("p_END_order_i_order", "order"), + ("p_END_order_o_order", "order"), + ("p_START_order_i_order", "order"), + ("p_START_order_o_order", "order"), + ("p_a_i_order", "order"), + ("p_a_o_order", "order"), + ("p_arc(START_order,a)_order", "order"), + ("p_arc(a,END_order)_order", "order"), + # _binding + ("p_binding#251_1", "_binding"), + ("p_binding#256_1", "_binding"), + ("p_binding#END_item_input", "_binding"), + ("p_binding#END_order_input", "_binding"), + ("p_binding#START_item_output", "_binding"), + ("p_binding#START_order_output", "_binding"), + ("p_binding#a_input", "_binding"), + ("p_binding#a_output", "_binding"), + ("p_binding_global_input", "_binding"), + ] + + places = {name: OCPetriNet.Place(name, ot) for (name, ot) in place_specs} + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transition_specs = [ + ("END_item", "END_item"), + ("END_order", "END_order"), + ("START_item", "START_item"), + ("START_order", "START_order"), + ("_silent#250", None), + ("_silent#253", None), + ("_silent#255", None), + ("_silent#258", None), + ("_silent#260", None), + ("_silent#263", None), + ("_silent#266", None), + ("_silent#269", None), + ("a", "a"), + ] + + transitions = { + name: OCPetriNet.Transition(name, label) + for (name, label) in transition_specs + } + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # --- transitions to places --- + connect( + transitions["END_item"], + places["p_END_item_o_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["END_item"], places["p_binding_global_input"], "_binding", arcs + ) + connect(transitions["END_order"], places["p_END_order_o_order"], "order", arcs) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + connect( + transitions["START_item"], + places["p_START_item_o_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["START_item"], + places["p_binding#START_item_output"], + "_binding", + arcs, + ) + connect( + transitions["START_order"], places["p_START_order_o_order"], "order", arcs + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + connect( + transitions["_silent#250"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#250"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#253"], + places["p_a_i_item"], + "item", + arcs, + is_variable=True, + ) + connect(transitions["_silent#253"], places["p_binding#251_1"], "_binding", arcs) + connect(transitions["_silent#255"], places["p_a_i_order"], "order", arcs) + connect( + transitions["_silent#255"], places["p_binding#a_input"], "_binding", arcs + ) + connect( + transitions["_silent#258"], + places["p_arc(a,END_item)_item"], + "item", + arcs, + is_variable=True, + ) + connect(transitions["_silent#258"], places["p_binding#256_1"], "_binding", arcs) + connect( + transitions["_silent#260"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#260"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#263"], + places["p_arc(START_item,a)_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#263"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#266"], places["p_END_order_i_order"], "order", arcs + ) + connect( + transitions["_silent#266"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#269"], + places["p_END_item_i_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#269"], + places["p_binding#END_item_input"], + "_binding", + arcs, + ) + connect(transitions["a"], places["p_a_o_item"], "item", arcs, is_variable=True) + connect(transitions["a"], places["p_a_o_order"], "order", arcs) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + # --- places to transitions --- + connect( + places["p_END_item_i_item"], + transitions["END_item"], + "item", + arcs, + is_variable=True, + ) + connect(places["p_END_order_i_order"], transitions["END_order"], "order", arcs) + connect( + places["p_START_item_i_item"], + transitions["START_item"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_START_item_o_item"], + transitions["_silent#263"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], transitions["START_order"], "order", arcs + ) + connect( + places["p_START_order_o_order"], transitions["_silent#250"], "order", arcs + ) + connect(places["p_a_i_item"], transitions["a"], "item", arcs, is_variable=True) + connect(places["p_a_i_order"], transitions["a"], "order", arcs) + connect( + places["p_a_o_item"], + transitions["_silent#258"], + "item", + arcs, + is_variable=True, + ) + connect(places["p_a_o_order"], transitions["_silent#260"], "order", arcs) + connect( + places["p_arc(START_item,a)_item"], + transitions["_silent#253"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#255"], + "order", + arcs, + ) + connect( + places["p_arc(a,END_item)_item"], + transitions["_silent#269"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#266"], + "order", + arcs, + ) + connect(places["p_binding#251_1"], transitions["_silent#255"], "_binding", arcs) + connect(places["p_binding#256_1"], transitions["_silent#260"], "_binding", arcs) + connect( + places["p_binding#END_item_input"], + transitions["END_item"], + "_binding", + arcs, + ) + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_item_output"], + transitions["_silent#263"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#250"], + "_binding", + arcs, + ) + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#258"], "_binding", arcs + ) + connect( + places["p_binding_global_input"], + transitions["START_item"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["START_order"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#253"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#266"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#269"], + "_binding", + arcs, + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_multi_ot_multi_arc(self): + marker_groups = { + "START_order": { + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "START_item": { + "omg": [ + [("a", "item", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (1, -1), 0), + ], + ], + "omg": [ + [ + ("b", "order", (1, 1), 0), + ("b", "item", (1, -1), 0), + ], + ], + }, + "b": { + "img": [ + [ + ("a", "order", (1, 1), 0), + ("a", "item", (1, -1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ("END_item", "item", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("b", "order", (1, 1), 0)], + ] + }, + "END_item": { + "img": [ + [("b", "item", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION MULTI OT MULTI ARC") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + place_specs = [ + # item + ("p_END_item_i_item", "item"), + ("p_END_item_o_item", "item"), + ("p_START_item_i_item", "item"), + ("p_START_item_o_item", "item"), + ("p_a_i_item", "item"), + ("p_a_o_item", "item"), + ("p_b_i_item", "item"), + ("p_b_o_item", "item"), + ("p_arc(START_item,a)_item", "item"), + ("p_arc(a,b)_item", "item"), + ("p_arc(b,END_item)_item", "item"), + # order + ("p_END_order_i_order", "order"), + ("p_END_order_o_order", "order"), + ("p_START_order_i_order", "order"), + ("p_START_order_o_order", "order"), + ("p_a_i_order", "order"), + ("p_a_o_order", "order"), + ("p_b_i_order", "order"), + ("p_b_o_order", "order"), + ("p_arc(START_order,a)_order", "order"), + ("p_arc(a,b)_order", "order"), + ("p_arc(b,END_order)_order", "order"), + # _binding + ("p_binding#251_1", "_binding"), + ("p_binding#256_1", "_binding"), + ("p_binding#2512_1", "_binding"), + ("p_binding#2562_1", "_binding"), + ("p_binding#END_item_input", "_binding"), + ("p_binding#END_order_input", "_binding"), + ("p_binding#START_item_output", "_binding"), + ("p_binding#START_order_output", "_binding"), + ("p_binding#a_input", "_binding"), + ("p_binding#a_output", "_binding"), + ("p_binding#b_input", "_binding"), + ("p_binding#b_output", "_binding"), + ("p_binding_global_input", "_binding"), + ] + + places = {name: OCPetriNet.Place(name, ot) for (name, ot) in place_specs} + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transition_specs = [ + ("END_item", "END_item"), + ("END_order", "END_order"), + ("START_item", "START_item"), + ("START_order", "START_order"), + ("_silent#250", None), # arc START_order to a + ("_silent#253", None), # p_a_i_item + ("_silent#255", None), # p_a_i_order + ("_silent#2532", None), # p_b_i_item + ("_silent#2552", None), # p_b_i_order + ("_silent#258", None), # p_arc(a,END_item)_item -> now b + ("_silent#260", None), # p_arc(a,END_order)_order -> now b + ("_silent#2582", None), # p_arc(b,END_item)_item + ("_silent#2602", None), # p_arc(b,END_order)_order + ("_silent#263", None), # p_arc(START_item,a)_item + ("_silent#266", None), # p_END_order_i_order + ("_silent#269", None), # p_END_item_i_item + ("a", "a"), + ("b", "b"), + ] + + transitions = { + name: OCPetriNet.Transition(name, label) + for (name, label) in transition_specs + } + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # --- transitions to places --- + connect( + transitions["END_item"], + places["p_END_item_o_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["END_item"], places["p_binding_global_input"], "_binding", arcs + ) + connect(transitions["END_order"], places["p_END_order_o_order"], "order", arcs) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + connect( + transitions["START_item"], + places["p_START_item_o_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["START_item"], + places["p_binding#START_item_output"], + "_binding", + arcs, + ) + connect( + transitions["START_order"], places["p_START_order_o_order"], "order", arcs + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + connect( + transitions["_silent#250"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#250"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#253"], + places["p_a_i_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#2532"], + places["p_b_i_item"], + "item", + arcs, + is_variable=True, + ) + connect(transitions["_silent#253"], places["p_binding#251_1"], "_binding", arcs) + connect(transitions["_silent#2532"], places["p_binding#2512_1"], "_binding", arcs) + connect(transitions["_silent#255"], places["p_a_i_order"], "order", arcs) + connect(transitions["_silent#2552"], places["p_b_i_order"], "order", arcs) + connect( + transitions["_silent#255"], places["p_binding#a_input"], "_binding", arcs + ) + connect( + transitions["_silent#2552"], places["p_binding#b_input"], "_binding", arcs + ) + connect( + transitions["_silent#258"], + places["p_arc(a,b)_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#2582"], + places["p_arc(b,END_item)_item"], + "item", + arcs, + is_variable=True, + ) + connect(transitions["_silent#258"], places["p_binding#256_1"], "_binding", arcs) + connect(transitions["_silent#2582"], places["p_binding#2562_1"], "_binding", arcs) + connect( + transitions["_silent#260"], + places["p_arc(a,b)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#2602"], + places["p_arc(b,END_order)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#260"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#2602"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#263"], + places["p_arc(START_item,a)_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#263"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#266"], places["p_END_order_i_order"], "order", arcs + ) + connect( + transitions["_silent#266"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + connect( + transitions["_silent#269"], + places["p_END_item_i_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#269"], + places["p_binding#END_item_input"], + "_binding", + arcs, + ) + connect(transitions["a"], places["p_a_o_item"], "item", arcs, is_variable=True) + connect(transitions["a"], places["p_a_o_order"], "order", arcs) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + connect(transitions["b"], places["p_b_o_item"], "item", arcs, is_variable=True) + connect(transitions["b"], places["p_b_o_order"], "order", arcs) + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + # --- places to transitions --- + connect( + places["p_END_item_i_item"], + transitions["END_item"], + "item", + arcs, + is_variable=True, + ) + connect(places["p_END_order_i_order"], transitions["END_order"], "order", arcs) + connect( + places["p_START_item_i_item"], + transitions["START_item"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_START_item_o_item"], + transitions["_silent#263"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], transitions["START_order"], "order", arcs + ) + connect( + places["p_START_order_o_order"], transitions["_silent#250"], "order", arcs + ) + connect(places["p_a_i_item"], transitions["a"], "item", arcs, is_variable=True) + connect(places["p_a_i_order"], transitions["a"], "order", arcs) + connect(places["p_b_i_item"], transitions["b"], "item", arcs, is_variable=True) + connect(places["p_b_i_order"], transitions["b"], "order", arcs) + connect( + places["p_a_o_item"], + transitions["_silent#258"], + "item", + arcs, + is_variable=True, + ) + connect(places["p_a_o_order"], transitions["_silent#260"], "order", arcs) + connect( + places["p_b_o_item"], + transitions["_silent#2582"], + "item", + arcs, + is_variable=True, + ) + connect(places["p_b_o_order"], transitions["_silent#2602"], "order", arcs) + connect( + places["p_arc(START_item,a)_item"], + transitions["_silent#253"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#255"], + "order", + arcs, + ) + connect( + places["p_arc(a,b)_item"], + transitions["_silent#2532"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_arc(b,END_item)_item"], + transitions["_silent#269"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,b)_order"], + transitions["_silent#2552"], + "order", + arcs, + ) + connect( + places["p_arc(b,END_order)_order"], + transitions["_silent#266"], + "order", + arcs, + ) + connect(places["p_binding#251_1"], transitions["_silent#255"], "_binding", arcs) + connect(places["p_binding#256_1"], transitions["_silent#260"], "_binding", arcs) + connect(places["p_binding#2512_1"], transitions["_silent#2552"], "_binding", arcs) + connect(places["p_binding#2562_1"], transitions["_silent#2602"], "_binding", arcs) + connect( + places["p_binding#END_item_input"], + transitions["END_item"], + "_binding", + arcs, + ) + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_item_output"], + transitions["_silent#263"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#250"], + "_binding", + arcs, + ) + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#258"], "_binding", arcs + ) + connect( + places["p_binding#b_output"], transitions["_silent#2582"], "_binding", arcs + ) + connect( + places["p_binding_global_input"], + transitions["START_item"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["START_order"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#253"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#2532"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#266"], + "_binding", + arcs, + ) + connect( + places["p_binding_global_input"], + transitions["_silent#269"], + "_binding", + arcs, + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_multi_ot_multi_marker(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "START_item": { + "img": [], + "omg": [ + [("a", "item", (1, -1), 0)], + ], + }, + "a": { + "img": [ + [ + ("START_order", "order", (1, 1), 0), + ("START_item", "item", (1, -1), 0), + ], + [ + ("START_item", "item", (1, -1), 0), + ], + ], + "omg": [ + [ + ("END_order", "order", (1, 1), 0), + ("END_item", "item", (1, -1), 0), + ], + [ + ("END_item", "item", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("a", "order", (1, 1), 0)], + ] + }, + "END_item": { + "img": [ + [("a", "item", (1, -1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION MULTI OT MULTI MARKER") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + item_place_names = [ + "p_END_item_i_item", + "p_END_item_o_item", + "p_START_item_i_item", + "p_START_item_o_item", + "p_a_i_item", + "p_a_o_item", + "p_arc(START_item,a)_item", + "p_arc(a,END_item)_item", + ] + + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,END_order)_order", + ] + + binding_place_names = [ + "p_binding#273_1", + "p_binding#281_1", + "p_binding#END_item_input", + "p_binding#END_order_input", + "p_binding#START_item_output", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "item") for n in item_place_names} + places.update({n: OCPetriNet.Place(n, "order") for n in order_place_names}) + places.update({n: OCPetriNet.Place(n, "_binding") for n in binding_place_names}) + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_item": OCPetriNet.Transition("END_item", "END_item"), + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_item": OCPetriNet.Transition("START_item", "START_item"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + } + + for num in [272, 275, 277, 280, 283, 285, 288, 291, 294, 297]: + transitions[f"_silent#{num}"] = OCPetriNet.Transition( + f"_silent#{num}", None + ) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place -------------------------------------------------- + connect( + transitions["END_item"], + places["p_END_item_o_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["END_item"], places["p_binding_global_input"], "_binding", arcs + ) + + connect(transitions["END_order"], places["p_END_order_o_order"], "order", arcs) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_item"], + places["p_START_item_o_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["START_item"], + places["p_binding#START_item_output"], + "_binding", + arcs, + ) + + connect( + transitions["START_order"], places["p_START_order_o_order"], "order", arcs + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#272"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#272"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#275"], + places["p_a_i_item"], + "item", + arcs, + is_variable=True, + ) + connect(transitions["_silent#275"], places["p_binding#273_1"], "_binding", arcs) + + connect(transitions["_silent#277"], places["p_a_i_order"], "order", arcs) + connect( + transitions["_silent#277"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#280"], + places["p_a_i_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#280"], places["p_binding#a_input"], "_binding", arcs + ) + + connect( + transitions["_silent#283"], + places["p_arc(a,END_item)_item"], + "item", + arcs, + is_variable=True, + ) + connect(transitions["_silent#283"], places["p_binding#281_1"], "_binding", arcs) + + connect( + transitions["_silent#285"], + places["p_arc(a,END_order)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#285"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#288"], + places["p_arc(a,END_item)_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#288"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#291"], + places["p_arc(START_item,a)_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#291"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#294"], places["p_END_order_i_order"], "order", arcs + ) + connect( + transitions["_silent#294"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect( + transitions["_silent#297"], + places["p_END_item_i_item"], + "item", + arcs, + is_variable=True, + ) + connect( + transitions["_silent#297"], + places["p_binding#END_item_input"], + "_binding", + arcs, + ) + + connect(transitions["a"], places["p_a_o_item"], "item", arcs, is_variable=True) + connect( + transitions["a"], places["p_a_o_order"], "order", arcs, is_variable=True + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + # place ➜ transition --------------------------------------------------- + connect( + places["p_END_item_i_item"], + transitions["END_item"], + "item", + arcs, + is_variable=True, + ) + connect(places["p_END_order_i_order"], transitions["END_order"], "order", arcs) + + connect( + places["p_START_item_i_item"], + transitions["START_item"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_i_order"], transitions["START_order"], "order", arcs + ) + + connect( + places["p_START_item_o_item"], + transitions["_silent#291"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_START_order_o_order"], transitions["_silent#272"], "order", arcs + ) + + connect(places["p_a_i_item"], transitions["a"], "item", arcs, is_variable=True) + connect( + places["p_a_i_order"], transitions["a"], "order", arcs, is_variable=True + ) + + for t in ["_silent#283", "_silent#288"]: + connect( + places["p_a_o_item"], transitions[t], "item", arcs, is_variable=True + ) + connect(places["p_a_o_order"], transitions["_silent#285"], "order", arcs) + + for t in ["_silent#275", "_silent#280"]: + connect( + places["p_arc(START_item,a)_item"], + transitions[t], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#277"], + "order", + arcs, + ) + + connect( + places["p_arc(a,END_item)_item"], + transitions["_silent#297"], + "item", + arcs, + is_variable=True, + ) + connect( + places["p_arc(a,END_order)_order"], + transitions["_silent#294"], + "order", + arcs, + ) + + connect(places["p_binding#273_1"], transitions["_silent#277"], "_binding", arcs) + connect(places["p_binding#281_1"], transitions["_silent#285"], "_binding", arcs) + + connect( + places["p_binding#END_item_input"], + transitions["END_item"], + "_binding", + arcs, + ) + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + + connect( + places["p_binding#START_item_output"], + transitions["_silent#291"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#272"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + for t in ["_silent#283", "_silent#288"]: + connect(places["p_binding#a_output"], transitions[t], "_binding", arcs) + + # p_binding_global_input feeds several transitions + for tgt in [ + "START_item", + "START_order", + "_silent#275", + "_silent#280", + "_silent#294", + "_silent#297", + ]: + connect( + places["p_binding_global_input"], transitions[tgt], "_binding", arcs + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_ABC(self): + marker_groups = { + "START_order": { + "img": [], + "omg": [ + [("a", "order", (1, 1), 0)], + ], + }, + "a": { + "img": [ + [("START_order", "order", (1, 1), 0)], + ], + "omg": [ + [("b", "order", (1, 1), 0)], + ], + }, + "b": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [("c", "order", (1, 1), 0)], + ], + }, + "c": { + "img": [ + [("b", "order", (1, 1), 0)], + ], + "omg": [ + [("END_order", "order", (1, 1), 0)], + ], + }, + "END_order": { + "img": [ + [("c", "order", (1, 1), 0)], + ] + }, + } + + occn = create_oc_causal_net(marker_groups) + print("\nTEST OCCN CONVERSION ABC") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + order_place_names = [ + "p_END_order_i_order", + "p_END_order_o_order", + "p_START_order_i_order", + "p_START_order_o_order", + "p_a_i_order", + "p_a_o_order", + "p_arc(START_order,a)_order", + "p_arc(a,b)_order", + "p_arc(b,c)_order", + "p_arc(c,END_order)_order", + "p_b_i_order", + "p_b_o_order", + "p_c_i_order", + "p_c_o_order", + ] + + binding_place_names = [ + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding#b_input", + "p_binding#b_output", + "p_binding#c_input", + "p_binding#c_output", + "p_binding_global_input", + ] + + places = {} + + for name in order_place_names: + places[name] = OCPetriNet.Place(name, "order") + + for name in binding_place_names: + places[name] = OCPetriNet.Place(name, "_binding") + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + "b": OCPetriNet.Transition("b", "b"), + "c": OCPetriNet.Transition("c", "c"), + } + + for n in [11, 14, 17, 2, 20, 23, 5, 8]: + t_name = f"_silent#{n}" + transitions[t_name] = OCPetriNet.Transition(t_name, None) + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # --- from transitions to places ------------------------------------------------ + connect(transitions["END_order"], places["p_END_order_o_order"], "order", arcs) + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + + connect( + transitions["START_order"], places["p_START_order_o_order"], "order", arcs + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + + connect(transitions["_silent#11"], places["p_a_i_order"], "order", arcs) + connect( + transitions["_silent#11"], places["p_binding#a_input"], "_binding", arcs + ) + + connect(transitions["_silent#14"], places["p_arc(a,b)_order"], "order", arcs) + connect( + transitions["_silent#14"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect(transitions["_silent#17"], places["p_b_i_order"], "order", arcs) + connect( + transitions["_silent#17"], places["p_binding#b_input"], "_binding", arcs + ) + + connect( + transitions["_silent#2"], + places["p_arc(START_order,a)_order"], + "order", + arcs, + ) + connect( + transitions["_silent#2"], places["p_binding_global_input"], "_binding", arcs + ) + + connect(transitions["_silent#20"], places["p_arc(b,c)_order"], "order", arcs) + connect( + transitions["_silent#20"], + places["p_binding_global_input"], + "_binding", + arcs, + ) + + connect(transitions["_silent#23"], places["p_END_order_i_order"], "order", arcs) + connect( + transitions["_silent#23"], + places["p_binding#END_order_input"], + "_binding", + arcs, + ) + + connect(transitions["_silent#5"], places["p_c_i_order"], "order", arcs) + connect(transitions["_silent#5"], places["p_binding#c_input"], "_binding", arcs) + + connect( + transitions["_silent#8"], places["p_arc(c,END_order)_order"], "order", arcs + ) + connect( + transitions["_silent#8"], places["p_binding_global_input"], "_binding", arcs + ) + + connect(transitions["a"], places["p_a_o_order"], "order", arcs) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + connect(transitions["b"], places["p_b_o_order"], "order", arcs) + connect(transitions["b"], places["p_binding#b_output"], "_binding", arcs) + + connect(transitions["c"], places["p_c_o_order"], "order", arcs) + connect(transitions["c"], places["p_binding#c_output"], "_binding", arcs) + + # --- from places to transitions ------------------------------------------------- + connect(places["p_END_order_i_order"], transitions["END_order"], "order", arcs) + connect( + places["p_START_order_i_order"], transitions["START_order"], "order", arcs + ) + + connect( + places["p_START_order_o_order"], transitions["_silent#2"], "order", arcs + ) + connect(places["p_a_i_order"], transitions["a"], "order", arcs) + connect(places["p_a_o_order"], transitions["_silent#14"], "order", arcs) + + connect( + places["p_arc(START_order,a)_order"], + transitions["_silent#11"], + "order", + arcs, + ) + connect(places["p_arc(a,b)_order"], transitions["_silent#17"], "order", arcs) + connect(places["p_arc(b,c)_order"], transitions["_silent#5"], "order", arcs) + connect( + places["p_arc(c,END_order)_order"], transitions["_silent#23"], "order", arcs + ) + + connect(places["p_b_i_order"], transitions["b"], "order", arcs) + connect(places["p_b_o_order"], transitions["_silent#20"], "order", arcs) + + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect( + places["p_binding#START_order_output"], + transitions["_silent#2"], + "_binding", + arcs, + ) + + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding#a_output"], transitions["_silent#14"], "_binding", arcs + ) + + connect(places["p_binding#b_input"], transitions["b"], "_binding", arcs) + connect( + places["p_binding#b_output"], transitions["_silent#20"], "_binding", arcs + ) + + connect(places["p_binding#c_input"], transitions["c"], "_binding", arcs) + connect( + places["p_binding#c_output"], transitions["_silent#8"], "_binding", arcs + ) + + # p_binding_global_input produces tokens for several transitions + for t in ["START_order", "_silent#11", "_silent#17", "_silent#23", "_silent#5"]: + connect(places["p_binding_global_input"], transitions[t], "_binding", arcs) + + connect(places["p_c_i_order"], transitions["c"], "order", arcs) + connect(places["p_c_o_order"], transitions["_silent#8"], "order", arcs) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN ABC", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + def test_conversion_isolated(self): + arcs = dict() + arcs["a"] = {} + arcs["START_order"] = {} + arcs["END_order"] = {} + + occn = OCCausalNet( + nx.MultiDiGraph(arcs), + {}, + {}, + ) + + print("\nTEST OCCN CONVERSION ISOLATED") + print(occn) + ocpn = converter.apply(occn) + print(ocpn) + + # Expected OCPN: + # --------------------------------------------------------------------- + # Places + # --------------------------------------------------------------------- + binding_place_names = [ + "p_binding#END_order_input", + "p_binding#START_order_output", + "p_binding#a_input", + "p_binding#a_output", + "p_binding_global_input", + ] + + places = {n: OCPetriNet.Place(n, "_binding") for n in binding_place_names} + + # --------------------------------------------------------------------- + # Transitions + # --------------------------------------------------------------------- + transitions = { + "END_order": OCPetriNet.Transition("END_order", "END_order"), + "START_order": OCPetriNet.Transition("START_order", "START_order"), + "a": OCPetriNet.Transition("a", "a"), + } + + # --------------------------------------------------------------------- + # Arcs + # --------------------------------------------------------------------- + arcs = [] + + # transition ➜ place + connect( + transitions["END_order"], places["p_binding_global_input"], "_binding", arcs + ) + connect( + transitions["START_order"], + places["p_binding#START_order_output"], + "_binding", + arcs, + ) + connect(transitions["a"], places["p_binding#a_output"], "_binding", arcs) + + # place ➜ transition + connect( + places["p_binding#END_order_input"], + transitions["END_order"], + "_binding", + arcs, + ) + connect(places["p_binding#a_input"], transitions["a"], "_binding", arcs) + connect( + places["p_binding_global_input"], + transitions["START_order"], + "_binding", + arcs, + ) + + # --------------------------------------------------------------------- + # Assemble the net + # --------------------------------------------------------------------- + ocpn_expected = OCPetriNet( + name="Expected OCPN", + places=list(places.values()), + transitions=list(transitions.values()), + arcs=arcs, + initial_marking=None, + final_marking=None, + ) + + print(ocpn_expected) + self.assertTrue(are_ocpn_equal_no_ids(ocpn, ocpn_expected)) + + +def are_ocpn_equal_no_ids(ocpn1, ocpn2): + """ + Compare two OCPNs without considering the IDs of the places, transitions and arcs. + E.g., places with names "p_binding#79_1[_binding]" and "p_binding#12_2[_binding]" are considered equal if their arcs are the same. + + Ignores `properties` and `name` attributes of ocpns. + + Parameters + ---------- + ocpn1 : OCPN + The first OCPN to compare. + ocpn2 : OCPN + The second OCPN to compare. + + Returns + ------- + bool + True if the OCPNs are equal (ignoring IDs), False otherwise. + """ + + def are_names_equal_no_ids(name1, name2): + """ + Compare two names without considering their IDs. + + If both names contain an id (format #n or #n_m where n,m are natural numbers), + the ids are ignored for the comparison. + """ + # Regex pattern to match #n or #n_m where n and m are natural numbers + id_pattern = r"#\d+(?:_\d+)?" + + id1 = re.search(id_pattern, name1) + id2 = re.search(id_pattern, name2) + + if not id1 and not id2: + return name1 == name2 + + if bool(id1) != bool(id2): + return False + + # Remove the ID + name1_cleaned = re.sub(id_pattern, "", name1) + name2_cleaned = re.sub(id_pattern, "", name2) + + return name1_cleaned == name2_cleaned + + def are_places_equal(place1, place2): + """ + Compare the name and object type of two places without considering their IDs. + """ + if not are_names_equal_no_ids(place1.name, place2.name): + return False + + if place1.object_type != place2.object_type: + return False + + return True + + def are_transitions_equal(transition1, transition2): + """ + Compare the name of two transitions without considering their IDs. + """ + if not are_names_equal_no_ids(transition1.name, transition2.name): + return False + + return True + + def are_arcs_equal(arc1, arc2): + """ + Compare two arcs without considering IDs for their names and names of their source/target. + Does not consider the `properties` attribute of the arcs. + """ + + def are_ocpn_elements_equal(el1, el2): + """ + Compare two OCPetriNet elements (Place or Transition) + """ + if type(el1) != type(el2): + return False + + if isinstance(el1, OCPetriNet.Place): + return are_places_equal(el1, el2) + + if isinstance(el1, OCPetriNet.Transition): + return are_transitions_equal(el1, el2) + + return False + + # Compare source + if not are_ocpn_elements_equal(arc1.source, arc2.source): + return False + + # Compare target + if not are_ocpn_elements_equal(arc1.target, arc2.target): + return False + + if arc1.object_type != arc2.object_type: + return False + + if arc1.weight != arc2.weight: + return False + + if arc1.is_variable != arc2.is_variable: + return False + + return True + + def are_sets_equal(set1, set2, are_items_equal): + """ + Compare two sets of items using the provided `are_items_equal` function. + Assumes a bijection between items in each set if they are considered equal. + + Args: + set1 (Iterable): First set of items. + set2 (Iterable): Second set of items. + are_items_equal (Callable): Function taking two arguments and returning True if they are equal. + + Returns: + bool: True if sets are equal under the equality function, False otherwise. + """ + if len(set1) != len(set2): + return False + + # keep track of used items from set2; we will not reuse them as we assume a bijection + used = set() + for item1 in set1: + found_match = False + for item2 in set2: + if item2 not in used and are_items_equal(item1, item2): + used.add(item2) + found_match = True + break + if not found_match: + return False + return True + + if ocpn1.initial_marking != ocpn2.initial_marking: + return False + + if ocpn1.final_marking != ocpn2.final_marking: + return False + + # Check places + # we do not need to check the arcs of the places here since we compare the arcs of the ocpns at the end + if not are_sets_equal(ocpn1.places, ocpn2.places, are_places_equal): + return False + + # Check transitions + # we do not need to check the arcs of the transitions here since we compare the arcs of the ocpns at the end + if not are_sets_equal(ocpn1.transitions, ocpn2.transitions, are_transitions_equal): + return False + + # Check arcs + if not are_sets_equal(ocpn1.arcs, ocpn2.arcs, are_arcs_equal): + return False + + return True + + +def connect(source, target, object_type, arcs, *, is_variable=False): + """ + Create an OCPetriNet.Arc, attach it to source/target, store it in `arcs`, and return it. + """ + arc = OCPetriNet.Arc(source, target, object_type, is_variable=is_variable) + source.add_out_arc(arc) + target.add_in_arc(arc) + arcs.append(arc) + return arc + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ocpn_semantics_test.py b/tests/ocpn_semantics_test.py new file mode 100644 index 0000000000..71fa70e07f --- /dev/null +++ b/tests/ocpn_semantics_test.py @@ -0,0 +1,560 @@ +from collections import Counter +import unittest +from pm4py.objects.ocpn.obj import OCPetriNet, OCMarking +from pm4py.objects.ocpn.semantics import OCPetriNetSemantics + + +class OCPN_Semantics_Test(unittest.TestCase): + + def test_enabled(self): + + def assert_enabled_transitions(ocpn, marking, enabled_transitions): + self.assertEqual(enabled_transitions, OCPetriNetSemantics.enabled_transitions(ocpn, marking)) + + ocpn = ocpn_big() + places = {p.name: p for p in ocpn.places} + transitions = {t.name: t for t in ocpn.transitions} + + marking1 = OCMarking( + {places["o1"]: Counter(["order1"]), places["i1"]: Counter(["item1", "item2"])} + ) + enabled1 = {transitions["po"]} + assert_enabled_transitions(ocpn, marking1, enabled1) + + marking2 = OCMarking({places["o1"]: Counter(["order1"])}) + enabled2 = {transitions["po"]} + assert_enabled_transitions(ocpn, marking2, enabled2) + + marking3 = OCMarking({places["i1"]: Counter(["item1"])}) + enabled3 = set() + assert_enabled_transitions(ocpn, marking3, enabled3) + + marking4 = OCMarking( + {places["o3"]: Counter(["order1"]), places["i3"]: Counter(["item1", "item2"])} + ) + enabled4 = {transitions["sr"], transitions["pa"], transitions["sh"]} + assert_enabled_transitions(ocpn, marking4, enabled4) + + marking5 = OCMarking() + enabled5 = set() + assert_enabled_transitions(ocpn, marking5, enabled5) + + marking6 = OCMarking( + { + places["o1"]: Counter(["order1"]), + places["o2"]: Counter(["order1"]), + places["o3"]: Counter(["order1"]), + places["o4"]: Counter(["order1"]), + places["i1"]: Counter(["item1"]), + places["i2"]: Counter(["item1"]), + places["i3"]: Counter(["item1"]), + places["i4"]: Counter(["item1"]), + } + ) + enabled6 = set(transitions.values()) + assert_enabled_transitions(ocpn, marking6, enabled6) + + def test_fire(self): + ocpn = ocpn_big() + places = {p.name: p for p in ocpn.places} + transitions = {t.name: t for t in ocpn.transitions} + + marking = OCMarking( + {places["o1"]: Counter(["order1"]), places["i1"]: Counter(["item1", "item2"])} + ) + objects = {"order": {"order1"}, "item": {"item1", "item2"}} + new_marking = OCPetriNetSemantics.fire(ocpn, transitions["po"], marking, objects) + self.assertEqual(new_marking[places["o2"]], Counter(["order1"])) + self.assertEqual(new_marking[places["i2"]], Counter(["item1", "item2"])) + + marking = OCMarking( + {places["o1"]: Counter({"order1": 2}), places["i1"]: Counter(["item1", "item2"])} + ) + objects = {"order": {"order1"}, "item": {"item1", "item2"}} + new_marking = OCPetriNetSemantics.fire(ocpn, transitions["po"], marking, objects) + self.assertEqual(new_marking[places["o1"]], Counter(["order1"])) + self.assertEqual(new_marking[places["o2"]], Counter(["order1"])) + self.assertEqual(new_marking[places["i2"]], Counter(["item1", "item2"])) + + marking = OCMarking( + {places["o1"]: Counter({"order1": 2}), places["i1"]: Counter(["item1", "item2"])} + ) + objects = {"order": {"order1"}, "item": {"item1"}} + new_marking = OCPetriNetSemantics.fire(ocpn, transitions["po"], marking, objects) + self.assertEqual(new_marking[places["o1"]], Counter(["order1"])) + self.assertEqual(new_marking[places["o2"]], Counter(["order1"])) + self.assertEqual(new_marking[places["i1"]], Counter(["item2"])) + self.assertEqual(new_marking[places["i2"]], Counter(["item1"])) + + marking = OCMarking( + {places["o1"]: Counter({"order1": 2}), places["i1"]: Counter(["item1", "item2"]), places["i2"]: Counter(["item1"])} + ) + objects = {"order": {"order1"}, "item": {"item1"}} + new_marking = OCPetriNetSemantics.fire(ocpn, transitions["po"], marking, objects) + self.assertEqual(new_marking[places["o1"]], Counter(["order1"])) + self.assertEqual(new_marking[places["o2"]], Counter(["order1"])) + self.assertEqual(new_marking[places["i1"]], Counter(["item2"])) + self.assertEqual(new_marking[places["i2"]], Counter({"item1": 2})) + + marking = OCMarking( + {places["o1"]: Counter(["order1"])} + ) + objects = {"order": {"order1"}} + new_marking = OCPetriNetSemantics.fire(ocpn, transitions["po"], marking, objects) + self.assertEqual(new_marking[places["o1"]], Counter()) + self.assertEqual(new_marking[places["o2"]], Counter(["order1"])) + self.assertEqual(new_marking[places["i1"]], Counter()) + self.assertEqual(new_marking[places["i2"]], Counter()) + + def test_fire_2(self): + ocpn = ocpn_multi_start() + places = {p.name: p for p in ocpn.places} + transitions = {t.name: t for t in ocpn.transitions} + + marking = OCMarking( + {places["o1"]: Counter(["order1"]), places["o3"]: Counter(["order1"])} + ) + objects = {"order": {"order1"}} + new_marking = OCPetriNetSemantics.fire(ocpn, transitions["a"], marking, objects) + self.assertEqual(new_marking[places["o1"]], Counter()) + self.assertEqual(new_marking[places["o3"]], Counter()) + self.assertEqual(new_marking[places["o2"]], Counter(["order1"])) + self.assertEqual(new_marking[places["o4"]], Counter(["order1"])) + + marking = OCMarking( + {places["o1"]: Counter(["order1", "order2"]), places["o3"]: Counter(["order1"])} + ) + objects = {"order": {"order1"}} + new_marking = OCPetriNetSemantics.fire(ocpn, transitions["a"], marking, objects) + self.assertEqual(new_marking[places["o1"]], Counter(["order2"])) + self.assertEqual(new_marking[places["o3"]], Counter()) + self.assertEqual(new_marking[places["o2"]], Counter(["order1"])) + self.assertEqual(new_marking[places["o4"]], Counter(["order1"])) + + + def test_fire_3(self): + ocpn = ocpn_muli_variable_2() + places = {p.name: p for p in ocpn.places} + transitions = {t.name: t for t in ocpn.transitions} + + + marking = OCMarking( + {places["p1"]: Counter(["order1", "order2"]), places["p4"]: Counter(["box1", "box2", "box3"])} + ) + objects = {"order": {"order1", "order2"}, "box": {"box1", "box2"}} + new_marking = OCPetriNetSemantics.fire(ocpn, transitions["a"], marking, objects) + self.assertEqual(new_marking[places["p1"]], Counter()) + self.assertEqual(new_marking[places["p4"]], Counter(["box3"])) + self.assertEqual(new_marking[places["p2"]], Counter(["order1", "order2"])) + self.assertEqual(new_marking[places["p5"]], Counter(["box1", "box2"])) + self.assertEqual(new_marking[places["p6"]], Counter(["box1", "box2"])) + + def assert_bindings_equal(self, possible_bindings_iter, expected_bindings): + # Check that all iterator elements are in the expected bindings + for binding in possible_bindings_iter: + self.assertIn(binding, expected_bindings) + expected_bindings.remove(binding) + # Assert none are left + self.assertEqual(len(expected_bindings), 0) + + def test_possible_bindings(self): + ocpn = ocpn_big() + places = {p.name: p for p in ocpn.places} + transitions = {t.name: t for t in ocpn.transitions} + + marking = OCMarking( + {places["o1"]: Counter(["o1", "o2"]), places["i1"]: Counter(["i1", "i2"])} + ) + + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["po"], marking) + expected_bindings = [ + { + "order": {"o1"}, + }, + { + "order": {"o2"}, + }, + { + "order": {"o1"}, + "item": {"i1"} + }, + { + "order": {"o2"}, + "item": {"i1"} + }, + { + "order": {"o1"}, + "item": {"i2"} + }, + { + "order": {"o2"}, + "item": {"i2"} + }, + { + "order": {"o1"}, + "item": {"i1", "i2"} + }, + { + "order": {"o2"}, + "item": {"i1", "i2"} + }] + + + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + + marking = OCMarking( + {places["i1"]: Counter(["i1", "i2"])} + ) + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["po"], marking) + expected_bindings = [] + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + + marking = OCMarking( + {places["o1"]: Counter(["o1", "o2"])} + ) + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["po"], marking) + expected_bindings = [ + { + "order": {"o1"}, + }, + { + "order": {"o2"}, + },] + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + marking = OCMarking( + {places["o1"]: Counter(["o1", "o2"])} + ) + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["si"], marking) + expected_bindings = [] + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + + marking = OCMarking( + {places["o2"]: Counter(["o1"]), places["o3"]: Counter(["o1", "o2"])} + ) + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["si"], marking) + expected_bindings = [ + { + "order": {"o1"}, + }] + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + + + + + + + + def test_possible_bindings_2(self): + ocpn = ocpn_muli_variable_2() + places = {p.name: p for p in ocpn.places} + transitions = {t.name: t for t in ocpn.transitions} + + marking = OCMarking( + {places["p1"]: Counter(["o1"]), places["p4"]: Counter(["b1", "b2"])} + ) + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["a"], marking) + expected_bindings = [ + { + "box": {"b1"}, + }, + { + "box": {"b2"}, + }, + { + "box": {"b1", "b2"}, + }, + { + "order": {"o1"}, + }, + { + "order": {"o1"}, + "box": {"b1"}, + }, + { + "order": {"o1"}, + "box": {"b2"}, + }, + { + "order": {"o1"}, + "box": {"b1", "b2"}, + }, + ] + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + + def test_possible_bindings_3(self): + ocpn = ocpn_multi_start() + places = {p.name: p for p in ocpn.places} + transitions = {t.name: t for t in ocpn.transitions} + + marking = OCMarking( + {places["o1"]: Counter(["o1"])} + ) + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["a"], marking) + expected_bindings = [ + ] + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + + marking = OCMarking( + {places["o1"]: Counter(["o1"]), places["o3"]: Counter(["o2"])} + ) + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["a"], marking) + expected_bindings = [ + ] + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + marking = OCMarking( + {places["o1"]: Counter(["o1"]), places["o3"]: Counter(["o1", "o2"])} + ) + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["a"], marking) + expected_bindings = [ + { + "order": {"o1"} + } + ] + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + marking = OCMarking( + {places["o1"]: Counter(["o1", "o2"]), places["o3"]: Counter(["o1", "o2"])} + ) + possible_bindings_iter = OCPetriNetSemantics.get_possible_bindings(ocpn, transitions["a"], marking) + expected_bindings = [ + { + "order": {"o1"} + }, + { + "order": {"o2"} + } + ] + self.assert_bindings_equal(possible_bindings_iter, expected_bindings) + + + + +def ocpn_big(): + name = "OCPN_big" + o1 = OCPetriNet.Place("o1", "order") + o2 = OCPetriNet.Place("o2", "order") + o3 = OCPetriNet.Place("o3", "order") + o4 = OCPetriNet.Place("o4", "order") + o5 = OCPetriNet.Place("o5", "order") + + i1 = OCPetriNet.Place("i1", "item") + i2 = OCPetriNet.Place("i2", "item") + i3 = OCPetriNet.Place("i3", "item") + i4 = OCPetriNet.Place("i4", "item") + i5 = OCPetriNet.Place("i5", "item") + + po = OCPetriNet.Transition("po", "place_order") + si = OCPetriNet.Transition("si", "send_invoice") + sr = OCPetriNet.Transition("sr", "send_reminder") + pi = OCPetriNet.Transition("pi", "pick_item") + pa = OCPetriNet.Transition("pa", "pay_order") + sh = OCPetriNet.Transition("sh", "ship item") + co = OCPetriNet.Transition("co", "mark_as_completed") + + a1 = OCPetriNet.Arc(o1, po, "order", is_variable=False) + o1.add_out_arc(a1) + po.add_in_arc(a1) + + a2 = OCPetriNet.Arc(i1, po, "item", is_variable=True) + i1.add_out_arc(a2) + po.add_in_arc(a2) + + a3 = OCPetriNet.Arc(po, o2, "order", is_variable=False) + po.add_out_arc(a3) + o2.add_in_arc(a3) + + a4 = OCPetriNet.Arc(po, i2, "item", is_variable=True) + po.add_out_arc(a4) + i2.add_in_arc(a4) + + a5 = OCPetriNet.Arc(o2, si, "order", is_variable=False) + o2.add_out_arc(a5) + si.add_in_arc(a5) + + a6 = OCPetriNet.Arc(i2, pi, "item", is_variable=False) + i2.add_out_arc(a6) + pi.add_in_arc(a6) + + a7 = OCPetriNet.Arc(si, o3, "order", is_variable=False) + si.add_out_arc(a7) + o3.add_in_arc(a7) + + a8 = OCPetriNet.Arc(o3, sr, "order", is_variable=False) + o3.add_out_arc(a8) + sr.add_in_arc(a8) + + a9 = OCPetriNet.Arc(sr, o3, "order", is_variable=False) + sr.add_out_arc(a9) + o3.add_in_arc(a9) + + a10 = OCPetriNet.Arc(pi, i3, "item", is_variable=False) + pi.add_out_arc(a10) + i3.add_in_arc(a10) + + a11 = OCPetriNet.Arc(o3, pa, "order", is_variable=False) + o3.add_out_arc(a11) + pa.add_in_arc(a11) + + a12 = OCPetriNet.Arc(i3, sh, "item", is_variable=False) + i3.add_out_arc(a12) + sh.add_in_arc(a12) + + a13 = OCPetriNet.Arc(pa, o4, "order", is_variable=False) + pa.add_out_arc(a13) + o4.add_in_arc(a13) + + a14 = OCPetriNet.Arc(sh, i4, "item", is_variable=False) + sh.add_out_arc(a14) + i4.add_in_arc(a14) + + a15 = OCPetriNet.Arc(o4, co, "order", is_variable=False) + o4.add_out_arc(a15) + co.add_in_arc(a15) + + a16 = OCPetriNet.Arc(i4, co, "item", is_variable=True) + i4.add_out_arc(a16) + co.add_in_arc(a16) + + a17 = OCPetriNet.Arc(co, o5, "order", is_variable=False) + co.add_out_arc(a17) + o5.add_in_arc(a17) + + a18 = OCPetriNet.Arc(co, i5, "item", is_variable=True) + co.add_out_arc(a18) + i5.add_in_arc(a18) + + initial_marking = OCMarking({o1: {"order1"}, i1: {"item1", "item2"}}) + final_marking = OCMarking({o5: {"order1"}, i5: {"item1", "item2"}}) + + ocpn = OCPetriNet( + name, + places=[o1, o2, o3, o4, o5, i1, i2, i3, i4, i5], + transitions=[po, si, sr, pi, pa, sh, co], + arcs=[ + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + a17, + a18, + ], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + return ocpn + +def ocpn_multi_start(): + name = "OCPN_multi_start" + + o1 = OCPetriNet.Place("o1", "order") + o2 = OCPetriNet.Place("o2", "order") + o3 = OCPetriNet.Place("o3", "order") + o4 = OCPetriNet.Place("o4", "order") + + a = OCPetriNet.Transition("a", "create_order") + + a1 = OCPetriNet.Arc(o1, a, "order", is_variable=False) + o1.add_out_arc(a1) + a.add_in_arc(a1) + + a2 = OCPetriNet.Arc(a, o2, "order", is_variable=False) + a.add_out_arc(a2) + o2.add_in_arc(a2) + + a3 = OCPetriNet.Arc(o3, a, "order", is_variable=False) + o3.add_out_arc(a3) + a.add_in_arc(a3) + + a4 = OCPetriNet.Arc(a, o4, "order", is_variable=False) + a.add_out_arc(a4) + o4.add_in_arc(a4) + + initial_marking = OCMarking({o1: {"order1"}, o3: {"order2"}}) + final_marking = OCMarking({o2: {"order1"}, o4: {"order2"}}) + + ocpn = OCPetriNet( + name, + places=[o1, o2, o3, o4], + transitions=[a], + arcs=[a1, a2, a3, a4], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + return ocpn + +def ocpn_muli_variable_2(): + name = "OCPN_multi_variable" + + p1 = OCPetriNet.Place("p1", "order") + p2 = OCPetriNet.Place("p2", "order") + p3 = OCPetriNet.Place("p3", "order") + p4 = OCPetriNet.Place("p4", "box") + p5 = OCPetriNet.Place("p5", "box") + p6 = OCPetriNet.Place("p6", "box") + + a = OCPetriNet.Transition("a", "create_order") + + a1 = OCPetriNet.Arc(p1, a, "order", is_variable=True) + p1.add_out_arc(a1) + a.add_in_arc(a1) + + a2 = OCPetriNet.Arc(a, p2, "order", is_variable=True) + a.add_out_arc(a2) + p2.add_in_arc(a2) + + a3 = OCPetriNet.Arc(a, p3, "order", is_variable=True) + a.add_out_arc(a3) + p3.add_in_arc(a3) + + a4 = OCPetriNet.Arc(p4, a, "box", is_variable=True) + p4.add_out_arc(a4) + a.add_in_arc(a4) + + a5 = OCPetriNet.Arc(a, p5, "box", is_variable=True) + a.add_out_arc(a5) + p5.add_in_arc(a5) + + a6 = OCPetriNet.Arc(a, p6, "box", is_variable=True) + a.add_out_arc(a6) + p6.add_in_arc(a6) + + + + initial_marking = OCMarking({p1: {"order1"}, p4: {"box1"}}) + final_marking = OCMarking({p2: {"order1"}, p3: {"order1"}, p5: {"box1"}, p6: {"box1"}}) + + ocpn = OCPetriNet( + name, + places=[p1, p2, p3, p4, p5, p6], + transitions=[a], + arcs=[a1, a2, a3, a4, a5, a6], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + return ocpn + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ocpn_simulation_test.py b/tests/ocpn_simulation_test.py new file mode 100644 index 0000000000..19120ceab8 --- /dev/null +++ b/tests/ocpn_simulation_test.py @@ -0,0 +1,242 @@ +import unittest +import time +from pm4py.objects.ocpn.obj import OCPetriNet, OCMarking +from pm4py.objects.ocel.obj import OCEL +from pm4py.algo.simulation.playout.ocpn.variants import extensive as playout_extensive + + +class OCPNSimulationTest(unittest.TestCase): + + def print_traces(self, traces, idx_to_transition): + # convert transition indices to labels + for i, trace in enumerate(traces): + trace_list = [] + for event in trace: + trace_list.append((idx_to_transition[event[0]].name, event[1])) + traces[i] = tuple(trace_list) + + print(traces) + print(len(traces)) + + def test_playout_ocpn_extensive(self): + + params = { + playout_extensive.Parameters.MAX_BINDINGS_PER_ACTIVITY: 3, + playout_extensive.Parameters.RETURN_TRACES: True, + } + + ocpn = ocpn_multi_start() + places = {p.name: p for p in ocpn.places} + + initial_marking = OCMarking( + {places["o1"]: {"order1"}, places["o3"]: {"order1"}} + ) + final_marking = OCMarking({places["o2"]: {"order1"}, places["o4"]: {"order1"}}) + + (traces, _, _) = playout_extensive.apply( + ocpn, initial_marking, final_marking, parameters=params + ) + self.assertEqual(len(traces), 1) + + initial_marking = OCMarking( + {places["o1"]: {"order1"}, places["o3"]: {"order2"}} + ) + final_marking = OCMarking({places["o2"]: {"order1"}, places["o4"]: {"order1"}}) + + (traces, _, _) = playout_extensive.apply( + ocpn, initial_marking, final_marking, parameters=params + ) + self.assertEqual(len(traces), 0) + + def test_playout_ocpn_extensive_2(self): + + params = { + playout_extensive.Parameters.MAX_BINDINGS_PER_ACTIVITY: 3, + playout_extensive.Parameters.RETURN_TRACES: True, + } + + ocpn = ocpn_big() + places = {p.name: p for p in ocpn.places} + + initial_marking = OCMarking({places["o1"]: {"order1"}, places["i1"]: {"item1"}}) + final_marking = OCMarking({places["o5"]: {"order1"}, places["i5"]: {"item1"}}) + + (traces, _, _) = playout_extensive.apply( + ocpn, initial_marking, final_marking, parameters=params + ) + self.assertEqual(len(traces), 52) + + + + +def ocpn_big(): + name = "OCPN_big" + o1 = OCPetriNet.Place("o1", "order") + o2 = OCPetriNet.Place("o2", "order") + o3 = OCPetriNet.Place("o3", "order") + o4 = OCPetriNet.Place("o4", "order") + o5 = OCPetriNet.Place("o5", "order") + + i1 = OCPetriNet.Place("i1", "item") + i2 = OCPetriNet.Place("i2", "item") + i3 = OCPetriNet.Place("i3", "item") + i4 = OCPetriNet.Place("i4", "item") + i5 = OCPetriNet.Place("i5", "item") + + po = OCPetriNet.Transition("po", "place_order") + si = OCPetriNet.Transition("si", "send_invoice") + sr = OCPetriNet.Transition("sr", "send_reminder") + pi = OCPetriNet.Transition("pi", "pick_item") + pa = OCPetriNet.Transition("pa", "pay_order") + sh = OCPetriNet.Transition("sh", "ship item") + co = OCPetriNet.Transition("co", "mark_as_completed") + + a1 = OCPetriNet.Arc(o1, po, "order", is_variable=False) + o1.add_out_arc(a1) + po.add_in_arc(a1) + + a2 = OCPetriNet.Arc(i1, po, "item", is_variable=True) + i1.add_out_arc(a2) + po.add_in_arc(a2) + + a3 = OCPetriNet.Arc(po, o2, "order", is_variable=False) + po.add_out_arc(a3) + o2.add_in_arc(a3) + + a4 = OCPetriNet.Arc(po, i2, "item", is_variable=True) + po.add_out_arc(a4) + i2.add_in_arc(a4) + + a5 = OCPetriNet.Arc(o2, si, "order", is_variable=False) + o2.add_out_arc(a5) + si.add_in_arc(a5) + + a6 = OCPetriNet.Arc(i2, pi, "item", is_variable=False) + i2.add_out_arc(a6) + pi.add_in_arc(a6) + + a7 = OCPetriNet.Arc(si, o3, "order", is_variable=False) + si.add_out_arc(a7) + o3.add_in_arc(a7) + + a8 = OCPetriNet.Arc(o3, sr, "order", is_variable=False) + o3.add_out_arc(a8) + sr.add_in_arc(a8) + + a9 = OCPetriNet.Arc(sr, o3, "order", is_variable=False) + sr.add_out_arc(a9) + o3.add_in_arc(a9) + + a10 = OCPetriNet.Arc(pi, i3, "item", is_variable=False) + pi.add_out_arc(a10) + i3.add_in_arc(a10) + + a11 = OCPetriNet.Arc(o3, pa, "order", is_variable=False) + o3.add_out_arc(a11) + pa.add_in_arc(a11) + + a12 = OCPetriNet.Arc(i3, sh, "item", is_variable=False) + i3.add_out_arc(a12) + sh.add_in_arc(a12) + + a13 = OCPetriNet.Arc(pa, o4, "order", is_variable=False) + pa.add_out_arc(a13) + o4.add_in_arc(a13) + + a14 = OCPetriNet.Arc(sh, i4, "item", is_variable=False) + sh.add_out_arc(a14) + i4.add_in_arc(a14) + + a15 = OCPetriNet.Arc(o4, co, "order", is_variable=False) + o4.add_out_arc(a15) + co.add_in_arc(a15) + + a16 = OCPetriNet.Arc(i4, co, "item", is_variable=True) + i4.add_out_arc(a16) + co.add_in_arc(a16) + + a17 = OCPetriNet.Arc(co, o5, "order", is_variable=False) + co.add_out_arc(a17) + o5.add_in_arc(a17) + + a18 = OCPetriNet.Arc(co, i5, "item", is_variable=True) + co.add_out_arc(a18) + i5.add_in_arc(a18) + + initial_marking = OCMarking({o1: {"order1"}, i1: {"item1", "item2"}}) + final_marking = OCMarking({o5: {"order1"}, i5: {"item1", "item2"}}) + + ocpn = OCPetriNet( + name, + places=[o1, o2, o3, o4, o5, i1, i2, i3, i4, i5], + transitions=[po, si, sr, pi, pa, sh, co], + arcs=[ + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + a17, + a18, + ], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + return ocpn + + +def ocpn_multi_start(): + name = "OCPN_multi_start" + + o1 = OCPetriNet.Place("o1", "order") + o2 = OCPetriNet.Place("o2", "order") + o3 = OCPetriNet.Place("o3", "order") + o4 = OCPetriNet.Place("o4", "order") + + a = OCPetriNet.Transition("a", "create_order") + + a1 = OCPetriNet.Arc(o1, a, "order", is_variable=False) + o1.add_out_arc(a1) + a.add_in_arc(a1) + + a2 = OCPetriNet.Arc(a, o2, "order", is_variable=False) + a.add_out_arc(a2) + o2.add_in_arc(a2) + + a3 = OCPetriNet.Arc(o3, a, "order", is_variable=False) + o3.add_out_arc(a3) + a.add_in_arc(a3) + + a4 = OCPetriNet.Arc(a, o4, "order", is_variable=False) + a.add_out_arc(a4) + o4.add_in_arc(a4) + + initial_marking = OCMarking({o1: {"order1"}, o3: {"order2"}}) + final_marking = OCMarking({o2: {"order1"}, o4: {"order2"}}) + + ocpn = OCPetriNet( + name, + places=[o1, o2, o3, o4], + transitions=[a], + arcs=[a1, a2, a3, a4], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + return ocpn + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ocpn_test.py b/tests/ocpn_test.py new file mode 100644 index 0000000000..6572c39bc2 --- /dev/null +++ b/tests/ocpn_test.py @@ -0,0 +1,1094 @@ +import unittest +import pm4py +from pm4py.objects.oc_causal_net.creation.factory import create_oc_causal_net +from pm4py.objects.oc_causal_net.obj import OCCausalNet +from pm4py.objects.ocpn.obj import OCPetriNet, OCMarking +from pm4py.objects.ocpn import converter + + +class OCPN_Test(unittest.TestCase): + def test_conversion_multi_ot(self): + # create OCPN + name = "OCPN_multi_ot" + p1 = OCPetriNet.Place("p1", "order") + p2 = OCPetriNet.Place("p2", "item") + p3 = OCPetriNet.Place("p3", "order") + p4 = OCPetriNet.Place("p4", "item") + + a = OCPetriNet.Transition("a", "create_order") + + a1 = OCPetriNet.Arc(p1, a, "order", is_variable=False) + a2 = OCPetriNet.Arc(p2, a, "item", is_variable=True) + a3 = OCPetriNet.Arc(a, p3, "order", is_variable=False) + a4 = OCPetriNet.Arc(a, p4, "item", is_variable=True) + + p1.add_out_arc(a1) + p2.add_out_arc(a2) + a.add_in_arc(a1) + a.add_in_arc(a2) + a.add_out_arc(a3) + a.add_out_arc(a4) + p3.add_in_arc(a3) + p4.add_in_arc(a4) + + initial_marking = OCMarking({p1: {"o1"}, p2: {"o2", "o3"}}) + final_marking = OCMarking({p3: {"o1"}, p4: {"o2", "o3"}}) + + ocpn = OCPetriNet( + name, + places=[p1, p2, p3, p4], + transitions=[a], + arcs=[a1, a2, a3, a4], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + print("\n") + print(ocpn) + print("\nConverted OCCN:") + occn = converter.apply(ocpn) + print(occn) + + # correct OCCN + marker_groups = { + "START_order": { + "omg": [ + [("p1", "order", (1, -1), 0)], + ], + }, + "START_item": { + "omg": [ + [("p2", "item", (1, -1), 0)], + ], + }, + "p1": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("a", "order", (1, -1), 0), + ], + ], + }, + "p2": { + "img": [ + [("START_item", "item", (1, -1), 0)], + ], + "omg": [ + [ + ("a", "item", (1, -1), 0), + ], + ], + }, + "a": { + "img": [ + [ + ("p1", "order", (1, 1), 0), + ("p2", "item", (0, -1), 0), + ], + ], + "omg": [ + [ + ("p3", "order", (1, 1), 0), + ("p4", "item", (0, -1), 0), + ], + ], + }, + "p3": { + "img": [ + [("a", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "p4": { + "img": [ + [("a", "item", (1, -1), 0)], + ], + "omg": [ + [ + ("END_item", "item", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("p3", "order", (1, -1), 0)], + ], + }, + "END_item": { + "img": [ + [("p4", "item", (1, -1), 0)], + ], + }, + } + + expected_occn = create_oc_causal_net(marker_groups) + + print("\nExpected OCCN:") + print(expected_occn) + + self.assertTrue(eq_no_keys(occn, expected_occn)) + + def test_conversion_basic(self): + name = "OCPN_basic" + + o1 = OCPetriNet.Place("o1", "order") + o2 = OCPetriNet.Place("o2", "order") + + a = OCPetriNet.Transition("a", "create_order") + + a1 = OCPetriNet.Arc(o1, a, "order", is_variable=False) + o1.add_out_arc(a1) + a.add_in_arc(a1) + + a2 = OCPetriNet.Arc(a, o2, "order", is_variable=False) + a.add_out_arc(a2) + o2.add_in_arc(a2) + + initial_marking = OCMarking({o1: {"order1"}}) + final_marking = OCMarking({o2: {"order1"}}) + + ocpn = OCPetriNet( + name, + places=[o1, o2], + transitions=[a], + arcs=[a1, a2], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + print("\n") + print(ocpn) + print("\nConverted OCCN:") + occn = converter.apply(ocpn) + print(occn) + + # correct OCCN + marker_groups = { + "START_order": { + "omg": [ + [("o1", "order", (1, -1), 0)], + ], + }, + "o1": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("a", "order", (1, -1), 0), + ], + ], + }, + "a": { + "img": [ + [ + ("o1", "order", (1, 1), 0), + ], + ], + "omg": [ + [ + ("o2", "order", (1, 1), 0), + ], + ], + }, + "o2": { + "img": [ + [("a", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("o2", "order", (1, -1), 0)], + ], + }, + } + + expected_occn = create_oc_causal_net(marker_groups) + + print("\nExpected OCCN:") + print(expected_occn) + + self.assertTrue(eq_no_keys(occn, expected_occn)) + + def test_conversion_multi_start(self): + name = "OCPN_multi_start" + + o1 = OCPetriNet.Place("o1", "order") + o2 = OCPetriNet.Place("o2", "order") + o3 = OCPetriNet.Place("o3", "order") + o4 = OCPetriNet.Place("o4", "order") + + a = OCPetriNet.Transition("a", "create_order") + + a1 = OCPetriNet.Arc(o1, a, "order", is_variable=False) + o1.add_out_arc(a1) + a.add_in_arc(a1) + + a2 = OCPetriNet.Arc(a, o2, "order", is_variable=False) + a.add_out_arc(a2) + o2.add_in_arc(a2) + + a3 = OCPetriNet.Arc(o3, a, "order", is_variable=False) + o3.add_out_arc(a3) + a.add_in_arc(a3) + + a4 = OCPetriNet.Arc(a, o4, "order", is_variable=False) + a.add_out_arc(a4) + o4.add_in_arc(a4) + + initial_marking = OCMarking({o1: {"order1"}, o3: {"order2"}}) + final_marking = OCMarking({o2: {"order1"}, o4: {"order2"}}) + + ocpn = OCPetriNet( + name, + places=[o1, o2, o3, o4], + transitions=[a], + arcs=[a1, a2, a3, a4], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + print("\n") + print(ocpn) + print("\nConverted OCCN:") + occn = converter.apply(ocpn) + print(occn) + + # correct OCCN + marker_groups = { + "START_order": { + "omg": [ + [("o1", "order", (1, -1), 0), ("o3", "order", (1, -1), 0)], + ], + }, + "o1": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("_silent_aux_in_a_order", "order", (1, -1), 0), + ], + ], + }, + "o3": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("_silent_aux_in_a_order", "order", (1, -1), 0), + ], + ], + }, + "_silent_aux_in_a_order": { + "img": [ + [("o1", "order", (1, 1), 0), ("o3", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("a", "order", (1, 1), 0), + ], + ], + }, + "a": { + "img": [ + [ + ("_silent_aux_in_a_order", "order", (1, 1), 0), + ], + ], + "omg": [ + [ + ("_silent_aux_out_a_order", "order", (1, 1), 0), + ], + ], + }, + "_silent_aux_out_a_order": { + "img": [[("a", "order", (1, 1), 0)]], + "omg": [ + [("o2", "order", (1, 1), 0), ("o4", "order", (1, 1), 0)], + ], + }, + "o2": { + "img": [ + [("_silent_aux_out_a_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "o4": { + "img": [ + [("_silent_aux_out_a_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("o2", "order", (1, -1), 0), ("o4", "order", (1, -1), 0)], + ], + }, + } + + expected_occn = create_oc_causal_net(marker_groups) + + print("\nExpected OCCN:") + print(expected_occn) + + self.assertTrue(eq_no_keys(occn, expected_occn)) + + def test_conversion_marking(self): + name = "OCPN_marking" + + o1 = OCPetriNet.Place("o1", "order") + o2 = OCPetriNet.Place("o2", "order") + + a = OCPetriNet.Transition("a", "create_order") + + a1 = OCPetriNet.Arc(o1, a, "order", is_variable=False) + o1.add_out_arc(a1) + a.add_in_arc(a1) + + a2 = OCPetriNet.Arc(a, o2, "order", is_variable=False) + a.add_out_arc(a2) + o2.add_in_arc(a2) + + initial_marking = OCMarking({o1: {"order1"}, o2: {"order2"}}) + final_marking = OCMarking({o1: {"order1"}, o2: {"order2"}}) + + ocpn = OCPetriNet( + name, + places=[o1, o2], + transitions=[a], + arcs=[a1, a2], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + print("\n") + print(ocpn) + print("\nConverted OCCN:") + occn = converter.apply(ocpn) + print(occn) + + # correct OCCN + marker_groups = { + "START_order": { + "omg": [ + [("o1", "order", (1, -1), 0), ("o2", "order", (1, -1), 0)], + ], + }, + "o1": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("a", "order", (1, -1), 0), + ], + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "a": { + "img": [ + [ + ("o1", "order", (1, 1), 0), + ], + ], + "omg": [ + [ + ("o2", "order", (1, 1), 0), + ], + ], + }, + "o2": { + "img": [ + [("a", "order", (1, -1), 0)], + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("o2", "order", (1, -1), 0), ("o1", "order", (1, -1), 0)], + ], + }, + } + + expected_occn = create_oc_causal_net(marker_groups) + + print("\nExpected OCCN:") + print(expected_occn) + + self.assertTrue(eq_no_keys(occn, expected_occn)) + + def test_conversion_multi_variable(self): + name = "OCPN_multi_variable" + + p1 = OCPetriNet.Place("p1", "order") + p2 = OCPetriNet.Place("p2", "order") + p3 = OCPetriNet.Place("p3", "order") + + a = OCPetriNet.Transition("a", "create_order") + + a1 = OCPetriNet.Arc(p1, a, "order", is_variable=True) + p1.add_out_arc(a1) + a.add_in_arc(a1) + + a2 = OCPetriNet.Arc(a, p2, "order", is_variable=True) + a.add_out_arc(a2) + p2.add_in_arc(a2) + + a3 = OCPetriNet.Arc(a, p3, "order", is_variable=True) + a.add_out_arc(a3) + p3.add_in_arc(a3) + + initial_marking = OCMarking({p1: {"order1"}}) + final_marking = OCMarking({p2: {"order1"}, p3: {"order1"}}) + + ocpn = OCPetriNet( + name, + places=[p1, p2, p3], + transitions=[a], + arcs=[a1, a2, a3], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + print("\n") + print(ocpn) + print("\nConverted OCCN:") + occn = converter.apply(ocpn) + print(occn) + + # correct OCCN + marker_groups = { + "START_order": { + "omg": [ + [("p1", "order", (1, -1), 0)], + ], + }, + "p1": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("a", "order", (1, -1), 0), + ], + ], + }, + "a": { + "img": [ + [("p1", "order", (0, -1), 0)], + ], + "omg": [ + [ + ("_silent_aux_out_a_order", "order", (0, -1), 0), + ], + ], + }, + "_silent_aux_out_a_order": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("p2", "order", (1, 1), 0), + ("p3", "order", (1, 1), 0), + ], + ], + }, + "p2": { + "img": [ + [("_silent_aux_out_a_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "p3": { + "img": [ + [("_silent_aux_out_a_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("p2", "order", (1, -1), 0), ("p3", "order", (1, -1), 0)], + ], + }, + } + + expected_occn = create_oc_causal_net(marker_groups) + + print("\nExpected OCCN:") + print(expected_occn) + + self.assertTrue(eq_no_keys(occn, expected_occn)) + + def test_conversion_multi_variable_2(self): + name = "OCPN_multi_variable" + + p1 = OCPetriNet.Place("p1", "order") + p2 = OCPetriNet.Place("p2", "order") + p3 = OCPetriNet.Place("p3", "order") + p4 = OCPetriNet.Place("p4", "box") + p5 = OCPetriNet.Place("p5", "box") + p6 = OCPetriNet.Place("p6", "box") + + a = OCPetriNet.Transition("a", "create_order") + + a1 = OCPetriNet.Arc(p1, a, "order", is_variable=True) + p1.add_out_arc(a1) + a.add_in_arc(a1) + + a2 = OCPetriNet.Arc(a, p2, "order", is_variable=True) + a.add_out_arc(a2) + p2.add_in_arc(a2) + + a3 = OCPetriNet.Arc(a, p3, "order", is_variable=True) + a.add_out_arc(a3) + p3.add_in_arc(a3) + + a4 = OCPetriNet.Arc(p4, a, "box", is_variable=True) + p4.add_out_arc(a4) + a.add_in_arc(a4) + + a5 = OCPetriNet.Arc(a, p5, "box", is_variable=True) + a.add_out_arc(a5) + p5.add_in_arc(a5) + + a6 = OCPetriNet.Arc(a, p6, "box", is_variable=True) + a.add_out_arc(a6) + p6.add_in_arc(a6) + + initial_marking = OCMarking({p1: {"order1"}, p4: {"box1"}}) + final_marking = OCMarking( + {p2: {"order1"}, p3: {"order1"}, p5: {"box1"}, p6: {"box1"}} + ) + + ocpn = OCPetriNet( + name, + places=[p1, p2, p3, p4, p5, p6], + transitions=[a], + arcs=[a1, a2, a3, a4, a5, a6], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + print("\n") + print(ocpn) + print("\nConverted OCCN:") + occn = converter.apply(ocpn) + print(occn) + + # correct OCCN + marker_groups = { + "START_order": { + "omg": [ + [("p1", "order", (1, -1), 0)], + ], + }, + "p1": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("a", "order", (1, -1), 0), + ], + ], + }, + "START_box": { + "omg": [ + [("p4", "box", (1, -1), 0)], + ], + }, + "p4": { + "img": [ + [("START_box", "box", (1, -1), 0)], + ], + "omg": [ + [ + ("a", "box", (1, -1), 0), + ], + ], + }, + "a": { + "img": [ + [("p1", "order", (0, -1), 0), ("p4", "box", (0, -1), 0)], + ], + "omg": [ + [ + ("_silent_aux_out_a_order", "order", (0, -1), 0), + ("_silent_aux_out_a_box", "box", (0, -1), 0), + ], + ], + }, + "_silent_aux_out_a_order": { + "img": [ + [("a", "order", (1, 1), 0)], + ], + "omg": [ + [ + ("p2", "order", (1, 1), 0), + ("p3", "order", (1, 1), 0), + ], + ], + }, + "p2": { + "img": [ + [("_silent_aux_out_a_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "p3": { + "img": [ + [("_silent_aux_out_a_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("p2", "order", (1, -1), 0), ("p3", "order", (1, -1), 0)], + ], + }, + "_silent_aux_out_a_box": { + "img": [ + [("a", "box", (1, 1), 0)], + ], + "omg": [ + [ + ("p5", "box", (1, 1), 0), + ("p6", "box", (1, 1), 0), + ], + ], + }, + "p5": { + "img": [ + [("_silent_aux_out_a_box", "box", (1, -1), 0)], + ], + "omg": [ + [ + ("END_box", "box", (1, -1), 0), + ], + ], + }, + "p6": { + "img": [ + [("_silent_aux_out_a_box", "box", (1, -1), 0)], + ], + "omg": [ + [ + ("END_box", "box", (1, -1), 0), + ], + ], + }, + "END_box": { + "img": [ + [("p5", "box", (1, -1), 0), ("p6", "box", (1, -1), 0)], + ], + }, + } + + expected_occn = create_oc_causal_net(marker_groups) + + print("\nExpected OCCN:") + print(expected_occn) + + self.assertTrue(eq_no_keys(occn, expected_occn)) + + def test_conversion_big(self): + name = "OCPN_big" + o1 = OCPetriNet.Place("o1", "order") + o2 = OCPetriNet.Place("o2", "order") + o3 = OCPetriNet.Place("o3", "order") + o4 = OCPetriNet.Place("o4", "order") + o5 = OCPetriNet.Place("o5", "order") + + i1 = OCPetriNet.Place("i1", "item") + i2 = OCPetriNet.Place("i2", "item") + i3 = OCPetriNet.Place("i3", "item") + i4 = OCPetriNet.Place("i4", "item") + i5 = OCPetriNet.Place("i5", "item") + + po = OCPetriNet.Transition("po", "place_order") + si = OCPetriNet.Transition("si", "send_invoice") + sr = OCPetriNet.Transition("sr", "send_reminder") + pi = OCPetriNet.Transition("pi", "pick_item") + pa = OCPetriNet.Transition("pa", "pay_order") + sh = OCPetriNet.Transition("sh", "ship item") + co = OCPetriNet.Transition("co", "mark_as_completed") + + a1 = OCPetriNet.Arc(o1, po, "order", is_variable=False) + o1.add_out_arc(a1) + po.add_in_arc(a1) + + a2 = OCPetriNet.Arc(i1, po, "item", is_variable=True) + i1.add_out_arc(a2) + po.add_in_arc(a2) + + a3 = OCPetriNet.Arc(po, o2, "order", is_variable=False) + po.add_out_arc(a3) + o2.add_in_arc(a3) + + a4 = OCPetriNet.Arc(po, i2, "item", is_variable=True) + po.add_out_arc(a4) + i2.add_in_arc(a4) + + a5 = OCPetriNet.Arc(o2, si, "order", is_variable=False) + o2.add_out_arc(a5) + si.add_in_arc(a5) + + a6 = OCPetriNet.Arc(i2, pi, "item", is_variable=False) + i2.add_out_arc(a6) + pi.add_in_arc(a6) + + a7 = OCPetriNet.Arc(si, o3, "order", is_variable=False) + si.add_out_arc(a7) + o3.add_in_arc(a7) + + a8 = OCPetriNet.Arc(o3, sr, "order", is_variable=False) + o3.add_out_arc(a8) + sr.add_in_arc(a8) + + a9 = OCPetriNet.Arc(sr, o3, "order", is_variable=False) + sr.add_out_arc(a9) + o3.add_in_arc(a9) + + a10 = OCPetriNet.Arc(pi, i3, "item", is_variable=False) + pi.add_out_arc(a10) + i3.add_in_arc(a10) + + a11 = OCPetriNet.Arc(o3, pa, "order", is_variable=False) + o3.add_out_arc(a11) + pa.add_in_arc(a11) + + a12 = OCPetriNet.Arc(i3, sh, "item", is_variable=False) + i3.add_out_arc(a12) + sh.add_in_arc(a12) + + a13 = OCPetriNet.Arc(pa, o4, "order", is_variable=False) + pa.add_out_arc(a13) + o4.add_in_arc(a13) + + a14 = OCPetriNet.Arc(sh, i4, "item", is_variable=False) + sh.add_out_arc(a14) + i4.add_in_arc(a14) + + a15 = OCPetriNet.Arc(o4, co, "order", is_variable=False) + o4.add_out_arc(a15) + co.add_in_arc(a15) + + a16 = OCPetriNet.Arc(i4, co, "item", is_variable=True) + i4.add_out_arc(a16) + co.add_in_arc(a16) + + a17 = OCPetriNet.Arc(co, o5, "order", is_variable=False) + co.add_out_arc(a17) + o5.add_in_arc(a17) + + a18 = OCPetriNet.Arc(co, i5, "item", is_variable=True) + co.add_out_arc(a18) + i5.add_in_arc(a18) + + initial_marking = OCMarking({o1: {"order1"}, i1: {"item1", "item2"}}) + final_marking = OCMarking({o5: {"order1"}, i5: {"item1", "item2"}}) + + ocpn = OCPetriNet( + name, + places=[o1, o2, o3, o4, o5, i1, i2, i3, i4, i5], + transitions=[po, si, sr, pi, pa, sh, co], + arcs=[ + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + a17, + a18, + ], + initial_marking=initial_marking, + final_marking=final_marking, + ) + + print("\n") + print(ocpn) + print("\nConverted OCCN:") + occn = converter.apply(ocpn) + print(occn) + + # correct OCCN + marker_groups = { + "START_order": { + "omg": [ + [("o1", "order", (1, -1), 0)], + ], + }, + "o1": { + "img": [ + [("START_order", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("po", "order", (1, -1), 0), + ], + ], + }, + "START_item": { + "omg": [ + [ + ("i1", "item", (1, -1), 0), + ], + ], + }, + "i1": { + "img": [ + [("START_item", "item", (1, -1), 0)], + ], + "omg": [ + [ + ("po", "item", (1, -1), 0), + ], + ], + }, + "po": { + "img": [ + [("o1", "order", (1, 1), 0), ("i1", "item", (0, -1), 0)], + ], + "omg": [ + [("o2", "order", (1, 1), 0), ("i2", "item", (0, -1), 0)], + ], + }, + "o2": { + "img": [ + [("po", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("si", "order", (1, -1), 0), + ], + ], + }, + "i2": { + "img": [ + [("po", "item", (1, -1), 0)], + ], + "omg": [ + [ + ("pi", "item", (1, -1), 0), + ], + ], + }, + "si": { + "img": [ + [("o2", "order", (1, 1), 0)], + ], + "omg": [ + [("o3", "order", (1, 1), 0)], + ], + }, + "pi": { + "img": [ + [("i2", "item", (1, 1), 0)], + ], + "omg": [ + [("i3", "item", (1, 1), 0)], + ], + }, + "o3": { + "img": [ + [("si", "order", (1, -1), 0)], + [("sr", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("sr", "order", (1, -1), 0), + ], + [ + ("pa", "order", (1, -1), 0), + ], + ], + }, + "sr": { + "img": [ + [("o3", "order", (1, 1), 0)], + ], + "omg": [ + [("o3", "order", (1, 1), 0)], + ], + }, + "i3": { + "img": [ + [("pi", "item", (1, -1), 0)], + ], + "omg": [ + [ + ("sh", "item", (1, -1), 0), + ], + ], + }, + "pa": { + "img": [ + [("o3", "order", (1, 1), 0)], + ], + "omg": [ + [("o4", "order", (1, 1), 0)], + ], + }, + "sh": { + "img": [ + [("i3", "item", (1, 1), 0)], + ], + "omg": [ + [("i4", "item", (1, 1), 0)], + ], + }, + "o4": { + "img": [ + [("pa", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("co", "order", (1, -1), 0), + ], + ], + }, + "i4": { + "img": [ + [("sh", "item", (1, -1), 0)], + ], + "omg": [ + [ + ("co", "item", (1, -1), 0), + ], + ], + }, + "co": { + "img": [ + [("o4", "order", (1, 1), 0), ("i4", "item", (0, -1), 0)], + ], + "omg": [ + [("o5", "order", (1, 1), 0), ("i5", "item", (0, -1), 0)], + ], + }, + "o5": { + "img": [ + [("co", "order", (1, -1), 0)], + ], + "omg": [ + [ + ("END_order", "order", (1, -1), 0), + ], + ], + }, + "i5": { + "img": [ + [("co", "item", (1, -1), 0)], + ], + "omg": [ + [ + ("END_item", "item", (1, -1), 0), + ], + ], + }, + "END_order": { + "img": [ + [("o5", "order", (1, -1), 0)], + ], + }, + "END_item": { + "img": [ + [("i5", "item", (1, -1), 0)], + ], + }, + } + + expected_occn = create_oc_causal_net(marker_groups) + + print("\nExpected OCCN:") + print(expected_occn) + + self.assertTrue(eq_no_keys(occn, expected_occn)) + + +def eq_no_keys(occn: OCCausalNet, other: OCCausalNet) -> bool: + """ + Checks if two Object-centic Causal Nets are equal. + All keys are set to 0 before checking. + Mutates the original nets. + + Parameters + ---------- + occn: OCCausalNet + Object-centric Causal Net + other: OCCausalNet + Other Object-centric Causal Net + + Returns + ---------- + True if the `occn` == `other` after removing keys. + """ + # set all keys to 0 + for net in [occn, other]: + for a in net.activities: + for marker_group in net.input_marker_groups.get( + a, [] + ) + net.output_marker_groups.get(a, []): + for marker in marker_group.markers: + marker.marker_key = 0 + # compare + return occn == other + + +if __name__ == "__main__": + unittest.main()