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()