diff --git a/src/pix_framework/io/bpm_graph.py b/src/pix_framework/io/bpm_graph.py
index 1fdf837..9382fa1 100644
--- a/src/pix_framework/io/bpm_graph.py
+++ b/src/pix_framework/io/bpm_graph.py
@@ -54,13 +54,28 @@ def __init__(self, bpmn_graph):
self.tokens = dict()
self.flow_date = dict()
self.state_mask = 0
+ self.frequency_count = dict()
+ self.visited_flow_arcs = set()
for flow_arc in bpmn_graph.flow_arcs:
self.tokens[flow_arc] = 0
+ def increment_frequency(self, flow_id):
+ if flow_id in self.tokens and self.tokens[flow_id] > 1:
+ return
+ if flow_id in self.visited_flow_arcs:
+ return
+ else:
+ self.visited_flow_arcs.add(flow_id)
+ if flow_id in self.frequency_count:
+ self.frequency_count[flow_id] += 1
+ else:
+ self.frequency_count[flow_id] = 1
+
def add_token(self, flow_id):
if flow_id in self.tokens:
self.tokens[flow_id] += 1
self.state_mask |= self.arcs_bitset[flow_id]
+ self.increment_frequency(flow_id)
def remove_token(self, flow_id):
if self.has_token(flow_id):
@@ -298,6 +313,21 @@ def update_process_state(self, e_id, p_state):
random.shuffle(enabled_tasks)
return enabled_tasks
+ def remove_tasks_from_enabled(self, enabled_elements: deque) -> deque:
+ """
+ Remove TASK elements from the enabled elements deque.
+
+ Args:
+ enabled_elements: A deque containing [ElementInfo, flow_id] pairs
+
+ Returns:
+ A new deque with TASK elements filtered out
+ """
+ return deque(
+ [elem for elem in enabled_elements if elem[0].type is not BPMNNodeType.TASK]
+ )
+
+
def replay_trace(
self, task_sequence: list, f_arcs_frequency: dict, post_p=True, trace=None
) -> Tuple[bool, List[bool], ProcessState]:
@@ -306,6 +336,8 @@ def replay_trace(
p_state = ProcessState(self)
fired_tasks = list()
fired_or_splits = set()
+ # fired_or_joins = set()
+
for flow_id in self.element_info[self.starting_event].outgoing_flows:
p_state.flow_date[flow_id] = self._c_trace[0].started_at if self._c_trace is not None else None
p_state.add_token(flow_id)
@@ -313,10 +345,14 @@ def replay_trace(
if self._c_trace:
self.update_flow_dates(self.element_info[self.starting_event], p_state, self._c_trace[0].started_at)
pending_tasks = dict()
+
for current_index in range(len(task_sequence)):
el_id = self.from_name.get(task_sequence[current_index])
fired_tasks.append(False)
+ if el_id == "F":
+ print("Hola")
+
in_flow = self.element_info[el_id].incoming_flows[0]
task_enabling.append(p_state.flow_date[in_flow] if in_flow in p_state.flow_date else None)
if self._c_trace:
@@ -338,7 +374,11 @@ def replay_trace(
el_id = self.from_name.get(task_sequence[current_index])
if el_id is None: # NOTE: skipping if no such element in self.from_name
continue
- p_state.add_token(self.element_info[el_id].outgoing_flows[0])
+
+ outgoing_flow = self.element_info[el_id].outgoing_flows[0]
+ p_state.add_token(outgoing_flow)
+
+
if current_index in pending_tasks:
for pending_index in pending_tasks[current_index]:
self.try_firing_alternative(
@@ -356,6 +396,8 @@ def replay_trace(
enabled_end, or_fired, path_decisions = self._find_enabled_predecessors(
self.element_info[self.end_event], p_state
)
+ enabled_end = self.remove_tasks_from_enabled(enabled_end)
+
self._fire_enabled_predecessors(
enabled_end,
p_state,
@@ -366,6 +408,10 @@ def replay_trace(
)
end_flow = self.element_info[self.end_event].incoming_flows[0]
if p_state.has_token(end_flow):
+ if end_flow not in f_arcs_frequency:
+ f_arcs_frequency[end_flow] = 1
+ else:
+ f_arcs_frequency[end_flow] += 1
p_state.tokens[end_flow] = 0
is_correct = True
@@ -378,7 +424,7 @@ def replay_trace(
if post_p:
self.postprocess_unfired_tasks(task_sequence, fired_tasks, f_arcs_frequency, task_enabling)
self._c_trace = None
- return is_correct, fired_tasks, p_state.pending_tokens()
+ return is_correct, fired_tasks, p_state.pending_tokens(), p_state.frequency_count
def update_flow_dates(self, e_info: ElementInfo, p_state: ProcessState, last_date):
visited_elements = set()
@@ -517,6 +563,15 @@ def try_firing_alternative(
task_info = self.element_info[el_id]
if not p_state.has_token(task_info.incoming_flows[0]):
enabled_pred, or_fired, path_decisions = self._find_enabled_predecessors(task_info, p_state)
+ enabled_pred = self.remove_tasks_from_enabled(enabled_pred) # TO BE considered: might have implications to certain cases
+
+ # # # Increment frequency of force enabled flow arc
+ # flow_id = task_info.incoming_flows[0]
+ # if flow_id not in f_arcs_frequency:
+ # f_arcs_frequency[flow_id] = 1
+ # else:
+ # f_arcs_frequency[flow_id] += 1
+
firing_index = self.find_firing_index(task_index, from_index, task_sequence, path_decisions, enabled_pred)
if firing_index == from_index:
self._fire_enabled_predecessors(
@@ -532,7 +587,12 @@ def try_firing_alternative(
else:
pending_tasks[firing_index].append(task_index)
if p_state.has_token(task_info.incoming_flows[0]):
- p_state.remove_token(task_info.incoming_flows[0])
+ flow_id = task_info.incoming_flows[0]
+ p_state.remove_token(flow_id)
+ # if flow_id not in f_arcs_frequency:
+ # f_arcs_frequency[flow_id] = 1
+ # else:
+ # f_arcs_frequency[flow_id] += 1
fired_tasks[task_index] = True
if self._c_trace:
self.current_attributes = self._c_trace[task_index].attributes
@@ -552,7 +612,9 @@ def closer_enabled_predecessors(
if self._is_enabled(e_info.id, p_state):
if dist not in enabled_pred:
enabled_pred[dist] = list()
+
enabled_pred[dist].append([e_info, flow_id])
+ visited.add(e_info.id)
min_dist[0] = max(min_dist[0], dist)
return dist, enabled_pred, or_firing, path_split
elif e_info.type is BPMNNodeType.INCLUSIVE_GATEWAY and e_info.is_join():
@@ -679,6 +741,10 @@ def _fire_enabled_predecessors(
[e_info, e_flow] = enabled_pred.popleft()
if self._is_enabled(e_info.id, p_state):
visited_elements.add(e_info.id)
+
+ for in_flow in e_info.incoming_flows:
+ p_state.remove_token(in_flow)
+
if e_info.type is BPMNNodeType.PARALLEL_GATEWAY:
for out_flow in e_info.outgoing_flows:
self._update_next(
@@ -706,6 +772,9 @@ def _fire_enabled_predecessors(
e_info.outgoing_flows
)
elif e_info.type is BPMNNodeType.INCLUSIVE_GATEWAY:
+ if e_info.is_split() and e_info.id in fired_or_split:
+ continue
+
self._update_next(
e_flow,
enabled_pred,
@@ -737,37 +806,87 @@ def _fire_enabled_predecessors(
e_info.outgoing_flows
)
- for in_flow in e_info.incoming_flows:
- p_state.remove_token(in_flow)
self.try_firing_or_join(enabled_pred, p_state, or_firing, path_decisions, f_arcs_frequency)
+ p_state.visited_flow_arcs.clear()
+
def try_firing_or_join(self, enabled_pred, p_state, or_firing, path_decisions, f_arcs_frequency):
- fired = set()
- or_firing_list = list()
- for or_join_id in or_firing:
- or_firing_list.append(or_join_id)
- for or_join_id in or_firing_list:
- if self._is_enabled(or_join_id, p_state) or not enabled_pred:
- fired.add(or_join_id)
- e_info = self.element_info[or_join_id]
- self._update_next(
- e_info.outgoing_flows[0],
- enabled_pred,
- p_state,
- or_firing,
- path_decisions,
- f_arcs_frequency,
- )
- for in_flow in e_info.incoming_flows:
- p_state.remove_token(in_flow)
- if enabled_pred:
- break
- if len(or_firing_list) != len(or_firing):
- for e_id in or_firing:
- if e_id not in or_firing_list:
- or_firing_list.append(e_id)
- for or_id in fired:
- del or_firing[or_id]
+ or_join_fired = True
+ while or_join_fired and (or_firing or enabled_pred):
+ or_join_fired = False
+ fired = set()
+ fired_flows = set()
+ or_firing_list = list(or_firing.keys())
+
+ for or_join_id in or_firing_list:
+ if self._is_enabled(or_join_id, p_state) or not enabled_pred:
+ or_join_fired = True
+ fired.add(or_join_id)
+ e_info = self.element_info[or_join_id]
+
+ if e_info.outgoing_flows[0] in fired_flows:
+ p_state.increment_frequency(e_info.outgoing_flows[0])
+ else:
+ self._update_next(
+ e_info.outgoing_flows[0],
+ enabled_pred,
+ p_state,
+ or_firing,
+ path_decisions,
+ f_arcs_frequency,
+ )
+
+ for in_flow in e_info.incoming_flows:
+ fired_flows.add(in_flow)
+ p_state.remove_token(in_flow)
+
+ if enabled_pred:
+ break
+
+ if len(or_firing_list) != len(or_firing):
+ for e_id in or_firing:
+ if e_id not in or_firing_list:
+ or_firing_list.append(e_id)
+
+ for or_id in fired:
+ if or_id in or_firing:
+ del or_firing[or_id]
+
+
+ # def try_firing_or_join(self, enabled_pred, p_state, or_firing, path_decisions, f_arcs_frequency):
+ # fired = set()
+ # fired_flows = set()
+ # or_firing_list = list()
+ # for or_join_id in or_firing:
+ # or_firing_list.append(or_join_id)
+ # for or_join_id in or_firing_list:
+ # if self._is_enabled(or_join_id, p_state) or not enabled_pred:
+ # fired.add(or_join_id)
+ # e_info = self.element_info[or_join_id]
+ #
+ # if e_info.outgoing_flows[0] in fired_flows:
+ # p_state.increment_frequency(e_info.outgoing_flows[0])
+ # else:
+ #
+ # self._update_next(
+ # e_info.outgoing_flows[0],
+ # enabled_pred,
+ # p_state,
+ # or_firing,
+ # path_decisions,
+ # f_arcs_frequency,
+ # )
+ # for in_flow in e_info.incoming_flows:
+ # fired_flows.add(in_flow)
+ # p_state.remove_token(in_flow)
+ # if enabled_pred:
+ # break
+ # if len(or_firing_list) != len(or_firing):
+ # for e_id in or_firing:
+ # if e_id not in or_firing_list:
+ # or_firing_list.append(e_id)
+ # for or_id in fired:
+ # del or_firing[or_id]
def check_unfired_or_splits(self, or_splits, f_arcs_frequency, p_state):
for or_id in or_splits:
@@ -843,7 +962,7 @@ def discover_gateway_probabilities(self, flow_arcs_frequency):
flow_arcs_probability,
total_frequency,
) = self._calculate_arcs_probabilities(e_id, flow_arcs_frequency)
- # recalculate not only pure zeros, but also low probabilities --- PONER ESTO DE REGRESO
+ # recalculate not only pure zeros, but also low probabilities
if min(flow_arcs_probability.values()) <= 0.005:
self._recalculate_arcs_probabilities(flow_arcs_frequency, flow_arcs_probability, total_frequency)
self._check_probabilities(flow_arcs_probability)
@@ -876,19 +995,18 @@ def _recalculate_arcs_probabilities(flow_arcs_frequency, flow_arcs_probability,
probability = 1.0 / float(number_of_invalid_arcs)
for flow_id in flow_arcs_probability:
flow_arcs_probability[flow_id] = probability
- # FIX THIS CORRECTION BECAUSE IT MAY LEAD TO NEGATIVE PROBABILITIES
- # else: # otherwise, we set min_probability instead of zero and balance probabilities for valid arcs
- # valid_probabilities = arcs_probabilities[arcs_probabilities > valid_probability_threshold].sum()
- # extra_probability = (number_of_invalid_arcs * min_probability) - (1.0 - valid_probabilities)
- # extra_probability_per_valid_arc = extra_probability / number_of_valid_arcs
- # for flow_id in flow_arcs_probability:
- # if flow_arcs_probability[flow_id] <= valid_probability_threshold:
- # # enforcing the minimum possible probability
- # probability = min_probability
- # else:
- # # balancing valid probabilities
- # probability = flow_arcs_probability[flow_id] - extra_probability_per_valid_arc
- # flow_arcs_probability[flow_id] = probability
+ else: # otherwise, we set min_probability instead of zero and balance probabilities for valid arcs
+ valid_probabilities = arcs_probabilities[arcs_probabilities > valid_probability_threshold].sum()
+ extra_probability = (number_of_invalid_arcs * min_probability) - (1.0 - valid_probabilities)
+ extra_probability_per_valid_arc = extra_probability / number_of_valid_arcs
+ for flow_id in flow_arcs_probability:
+ if flow_arcs_probability[flow_id] <= valid_probability_threshold:
+ # enforcing the minimum possible probability
+ probability = min_probability
+ else:
+ # balancing valid probabilities
+ probability = flow_arcs_probability[flow_id] - extra_probability_per_valid_arc
+ flow_arcs_probability[flow_id] = probability
@staticmethod
def _check_probabilities(flow_arcs_probability):
@@ -921,4 +1039,4 @@ def _find_next(self, f_arc, p_state, enabled_tasks, to_execute):
if self.element_info[next_e].type == BPMNNodeType.TASK:
enabled_tasks.append(next_e)
else:
- to_execute.append(next_e)
+ to_execute.append(next_e)
\ No newline at end of file
diff --git a/tests/pix_framework/assets/and_xor_event_log.csv b/tests/pix_framework/assets/and_xor_event_log.csv
new file mode 100644
index 0000000..cf09a1e
--- /dev/null
+++ b/tests/pix_framework/assets/and_xor_event_log.csv
@@ -0,0 +1,3 @@
+case_id,activity,enable_time,start_time,end_time,resource
+1,A,2025-01-29 09:15:00+00:00,2025-01-29 09:15:00+00:00,2025-01-29 09:20:00+00:00,Worker-1
+1,D,2025-01-29 09:35:00+00:00,2025-01-29 09:35:00+00:00,2025-01-29 09:40:00+00:00,Worker-1
\ No newline at end of file
diff --git a/tests/pix_framework/assets/and_xor_model.bpmn b/tests/pix_framework/assets/and_xor_model.bpmn
new file mode 100644
index 0000000..8ea8c69
--- /dev/null
+++ b/tests/pix_framework/assets/and_xor_model.bpmn
@@ -0,0 +1,154 @@
+
+
+
+
+ f1
+ f2
+
+
+ f10
+ f9
+ f11
+
+
+ f2
+ f3
+ f4
+
+
+ f5
+ f7
+
+
+ f6
+ f8
+
+
+ f4
+ f9
+
+
+ f11
+
+
+
+
+
+
+
+
+
+
+
+
+
+ f1
+
+
+ f7
+ f8
+ f10
+
+
+ f3
+ f6
+ f5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/pix_framework/assets/deep_or_gateways.bpmn b/tests/pix_framework/assets/deep_or_gateways.bpmn
new file mode 100644
index 0000000..f66ca76
--- /dev/null
+++ b/tests/pix_framework/assets/deep_or_gateways.bpmn
@@ -0,0 +1,277 @@
+
+
+
+
+ f1
+
+
+ f1
+ f2
+
+
+
+ f2
+ f3
+ f4
+
+
+
+ f3
+ f5
+ f6
+
+
+
+ f5
+ f7
+ f8
+
+
+
+ f22
+
+
+ f7
+ f11
+ f10
+
+
+
+ f4
+ f20
+ f21
+
+
+ f21
+ f22
+
+
+
+
+ f6
+ f9
+
+
+
+ f9
+ f19
+ f20
+
+
+
+ f8
+ f12
+
+
+
+ f17
+ f16
+ f18
+
+
+ f12
+ f18
+ f19
+
+
+
+
+
+
+ f11
+ f17
+
+
+
+ f10
+ f13
+ f14
+
+
+
+
+
+ f13
+ f15
+
+
+
+ f14
+ f15
+ f16
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/pix_framework/assets/deep_or_gateways_event_log.csv b/tests/pix_framework/assets/deep_or_gateways_event_log.csv
new file mode 100644
index 0000000..0636c59
--- /dev/null
+++ b/tests/pix_framework/assets/deep_or_gateways_event_log.csv
@@ -0,0 +1,14 @@
+case_id,activity,enable_time,start_time,end_time,resource
+1,A,2025-01-29 09:15:00+00:00,2025-01-29 09:15:00+00:00,2025-01-29 09:20:00+00:00,Worker-1
+1,B,2025-01-29 09:20:00+00:00,2025-01-29 09:20:00+00:00,2025-01-29 09:25:00+00:00,Worker-2
+1,C,2025-01-29 09:30:00+00:00,2025-01-29 09:30:00+00:00,2025-01-29 09:33:00+00:00,Worker-2
+1,D,2025-01-29 09:34:00+00:00,2025-01-29 09:34:00+00:00,2025-01-29 09:35:00+00:00,Worker-2
+1,E,2025-01-29 09:40:00+00:00,2025-01-29 09:40:00+00:00,2025-01-29 09:45:00+00:00,Worker-2
+1,F,2025-01-29 09:50:00+00:00,2025-01-29 09:50:00+00:00,2025-01-29 09:55:00+00:00,Worker-2
+
+2,A,2025-01-29 09:15:00+00:00,2025-01-29 09:15:00+00:00,2025-01-29 09:20:00+00:00,Worker-1
+2,F,2025-01-29 09:50:00+00:00,2025-01-29 09:50:00+00:00,2025-01-29 09:55:00+00:00,Worker-2
+
+3,A,2025-01-29 09:15:00+00:00,2025-01-29 09:15:00+00:00,2025-01-29 09:20:00+00:00,Worker-1
+3,B,2025-01-29 09:20:00+00:00,2025-01-29 09:20:00+00:00,2025-01-29 09:25:00+00:00,Worker-2
+3,F,2025-01-29 09:30:00+00:00,2025-01-29 09:30:00+00:00,2025-01-29 09:33:00+00:00,Worker-2
\ No newline at end of file
diff --git a/tests/pix_framework/assets/exclusive_gateway_event_log.csv b/tests/pix_framework/assets/exclusive_gateway_event_log.csv
new file mode 100644
index 0000000..f947048
--- /dev/null
+++ b/tests/pix_framework/assets/exclusive_gateway_event_log.csv
@@ -0,0 +1,2 @@
+case_id,activity,enable_time,start_time,end_time,resource
+1,C,2025-01-29 09:15:00+00:00,2025-01-29 09:15:00+00:00,2025-01-29 09:20:00+00:00,Worker-1
diff --git a/tests/pix_framework/assets/exclusive_gateway_simple.bpmn b/tests/pix_framework/assets/exclusive_gateway_simple.bpmn
new file mode 100644
index 0000000..122f21b
--- /dev/null
+++ b/tests/pix_framework/assets/exclusive_gateway_simple.bpmn
@@ -0,0 +1,100 @@
+
+
+
+
+ f1
+ f2
+
+
+ f3
+ f5
+
+
+ f4
+ f6
+
+
+ f7
+
+
+
+
+
+
+
+
+
+ f6
+ f5
+ f7
+
+
+ f2
+ f3
+ f4
+
+
+ f1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/pix_framework/assets/nested_and_gateways.bpmn b/tests/pix_framework/assets/nested_and_gateways.bpmn
new file mode 100644
index 0000000..e682668
--- /dev/null
+++ b/tests/pix_framework/assets/nested_and_gateways.bpmn
@@ -0,0 +1,196 @@
+
+
+
+
+ f1
+ f2
+
+
+ f5
+ f14
+ f15
+
+
+ f2
+ f4
+ f3
+
+
+ f12
+ f13
+ f14
+
+
+ f4
+ f6
+ f7
+
+
+ f10
+ f11
+ f13
+
+
+ f6
+ f8
+ f9
+
+
+ f3
+ f5
+
+
+ f8
+ f10
+
+
+ f9
+ f11
+
+
+ f7
+ f12
+
+
+ f15
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ f1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/pix_framework/assets/nested_and_gateways_event_log.csv b/tests/pix_framework/assets/nested_and_gateways_event_log.csv
new file mode 100644
index 0000000..120216a
--- /dev/null
+++ b/tests/pix_framework/assets/nested_and_gateways_event_log.csv
@@ -0,0 +1,6 @@
+case_id,activity,enable_time,start_time,end_time,resource
+1,A,2025-01-29 09:15:00+00:00,2025-01-29 09:15:00+00:00,2025-01-29 09:20:00+00:00,Worker-1
+1,B,2025-01-29 09:20:00+00:00,2025-01-29 09:20:00+00:00,2025-01-29 09:25:00+00:00,Worker-2
+1,C,2025-01-29 09:25:00+00:00,2025-01-29 09:30:00+00:00,2025-01-29 09:30:00+00:00,Worker-2
+1,D,2025-01-29 09:35:00+00:00,2025-01-29 09:35:00+00:00,2025-01-29 09:40:00+00:00,Worker-2
+1,E,2025-01-29 09:45:00+00:00,2025-01-29 09:45:00+00:00,2025-01-29 09:50:00+00:00,Worker-2
\ No newline at end of file
diff --git a/tests/pix_framework/assets/nested_or_gateways.bpmn b/tests/pix_framework/assets/nested_or_gateways.bpmn
new file mode 100644
index 0000000..68a04ed
--- /dev/null
+++ b/tests/pix_framework/assets/nested_or_gateways.bpmn
@@ -0,0 +1,208 @@
+
+
+
+
+ f1
+ f2
+
+
+ f3
+ f5
+
+
+ f6
+ f8
+
+
+ f9
+ f11
+
+
+ f10
+ f12
+
+
+ f16
+
+
+ f15
+ f16
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ f5
+ f14
+ f15
+
+
+ f2
+ f3
+ f4
+
+
+ f8
+ f13
+ f14
+
+
+ f4
+ f6
+ f7
+
+
+ f12
+ f11
+ f13
+
+
+ f7
+ f9
+ f10
+
+
+ f1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/pix_framework/assets/nested_or_gateways_event_log.csv b/tests/pix_framework/assets/nested_or_gateways_event_log.csv
new file mode 100644
index 0000000..f26732b
--- /dev/null
+++ b/tests/pix_framework/assets/nested_or_gateways_event_log.csv
@@ -0,0 +1,5 @@
+case_id,activity,enable_time,start_time,end_time,resource
+1,A,2025-01-29 09:15:00+00:00,2025-01-29 09:15:00+00:00,2025-01-29 09:20:00+00:00,Worker-1
+1,B,2025-01-29 09:20:00+00:00,2025-01-29 09:20:00+00:00,2025-01-29 09:25:00+00:00,Worker-2
+1,C,2025-01-29 09:50:00+00:00,2025-01-29 09:50:00+00:00,2025-01-29 10:00:00+00:00,Worker-2
+1,F,2025-01-29 12:50:00+00:00,2025-01-29 12:50:00+00:00,2025-01-29 13:00:00+00:00,Worker-2
\ No newline at end of file
diff --git a/tests/pix_framework/assets/simple_model.bpmn b/tests/pix_framework/assets/simple_model.bpmn
new file mode 100644
index 0000000..fc2c2cb
--- /dev/null
+++ b/tests/pix_framework/assets/simple_model.bpmn
@@ -0,0 +1,65 @@
+
+
+
+
+ f1
+ f2
+
+
+ f2
+ f3
+
+
+ f3
+ f4
+
+
+ f4
+
+
+
+
+
+
+ f1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/pix_framework/assets/simple_model_event_log.csv b/tests/pix_framework/assets/simple_model_event_log.csv
new file mode 100644
index 0000000..cfc85f9
--- /dev/null
+++ b/tests/pix_framework/assets/simple_model_event_log.csv
@@ -0,0 +1,5 @@
+case_id,activity,enable_time,start_time,end_time,resource
+1,A,2025-01-29 09:00:00+00:00,2025-01-29 09:00:00+00:00,2025-01-29 09:10:00+00:00,Worker-1
+1,B,2025-01-29 09:12:00+00:00,2025-01-29 09:12:00+00:00,2025-01-29 09:15:00+00:00,Worker-2
+1,C,2025-01-29 09:18:00+00:00,2025-01-29 09:18:00+00:00,2025-01-29 09:20:00+00:00,Worker-3
+2,B,2025-01-29 09:00:00+00:00,2025-01-29 09:00:00+00:00,2025-01-29 09:20:00+00:00,Worker-1
\ No newline at end of file
diff --git a/tests/pix_framework/assets/xor_or_gateways.bpmn b/tests/pix_framework/assets/xor_or_gateways.bpmn
new file mode 100644
index 0000000..52741b4
--- /dev/null
+++ b/tests/pix_framework/assets/xor_or_gateways.bpmn
@@ -0,0 +1,167 @@
+
+
+
+
+ f1
+
+
+ f1
+ f2
+ f3
+
+
+
+ f2
+ f4
+ f5
+
+
+
+ f4
+ f6
+
+
+
+ f5
+ f7
+ f8
+
+
+
+ f7
+ f9
+
+
+
+ f8
+ f10
+
+
+
+ f9
+ f10
+ f11
+
+
+
+
+ f6
+ f11
+ f12
+
+
+
+
+ f12
+ f3
+ f13
+
+
+
+
+ f13
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/pix_framework/assets/xor_or_gateways_event_log.csv b/tests/pix_framework/assets/xor_or_gateways_event_log.csv
new file mode 100644
index 0000000..12bd0a5
--- /dev/null
+++ b/tests/pix_framework/assets/xor_or_gateways_event_log.csv
@@ -0,0 +1,4 @@
+case_id,activity,enable_time,start_time,end_time,resource
+1,A,2025-01-29 09:15:00+00:00,2025-01-29 09:15:00+00:00,2025-01-29 09:20:00+00:00,Worker-1
+1,B,2025-01-29 09:20:00+00:00,2025-01-29 09:20:00+00:00,2025-01-29 09:25:00+00:00,Worker-2
+
diff --git a/tests/pix_framework/discovery/batch_processing/test_batch_discovery.py b/tests/pix_framework/discovery/batch_processing/test_batch_discovery.py
index 365f84e..b261a99 100644
--- a/tests/pix_framework/discovery/batch_processing/test_batch_discovery.py
+++ b/tests/pix_framework/discovery/batch_processing/test_batch_discovery.py
@@ -12,7 +12,7 @@
def test__identify_single_activity_batches():
# Read input event log
- event_log = pd.read_csv(assets_dir / "event_log_1.csv")
+ event_log = pd.read_csv(assets_dir / "exclusive_gateway_event_log.csv")
event_log[DEFAULT_CSV_IDS.enabled_time] = pd.to_datetime(event_log[DEFAULT_CSV_IDS.enabled_time], utc=True)
event_log[DEFAULT_CSV_IDS.start_time] = pd.to_datetime(event_log[DEFAULT_CSV_IDS.start_time], utc=True)
event_log[DEFAULT_CSV_IDS.end_time] = pd.to_datetime(event_log[DEFAULT_CSV_IDS.end_time], utc=True)
diff --git a/tests/pix_framework/discovery/prioritization/test_prioritization_discovery.py b/tests/pix_framework/discovery/prioritization/test_prioritization_discovery.py
index c1d71ed..3b9320a 100644
--- a/tests/pix_framework/discovery/prioritization/test_prioritization_discovery.py
+++ b/tests/pix_framework/discovery/prioritization/test_prioritization_discovery.py
@@ -13,7 +13,7 @@
def test_discover_prioritized_instances():
# Read event log
- event_log = pd.read_csv(assets_dir / "event_log_1.csv")
+ event_log = pd.read_csv(assets_dir / "exclusive_gateway_event_log.csv")
event_log[DEFAULT_CSV_IDS.enabled_time] = pd.to_datetime(event_log[DEFAULT_CSV_IDS.enabled_time], utc=True)
event_log[DEFAULT_CSV_IDS.start_time] = pd.to_datetime(event_log[DEFAULT_CSV_IDS.start_time], utc=True)
event_log[DEFAULT_CSV_IDS.end_time] = pd.to_datetime(event_log[DEFAULT_CSV_IDS.end_time], utc=True)
diff --git a/tests/pix_framework/io/test_bpm_graph.py b/tests/pix_framework/io/test_bpm_graph.py
index 3c12a0a..dabc131 100644
--- a/tests/pix_framework/io/test_bpm_graph.py
+++ b/tests/pix_framework/io/test_bpm_graph.py
@@ -1,15 +1,774 @@
from pathlib import Path
-
import pytest
from pix_framework.io.bpm_graph import BPMNGraph
+from pix_framework.io.event_log import EventLogIDs, read_csv_log
+from typing import Dict, List, Tuple
assets_dir = Path(__file__).parent.parent / "assets"
+class CaseExpectedResults:
+ """Class to hold expected test results for a specific case"""
+ def __init__(
+ self,
+ case_id: str,
+ flow_frequencies: Dict[str, int],
+ missed_tokens: [Dict[str, int]] = None,
+ left_tokens: [Dict[str, int]] = None
+ ):
+ self.case_id = case_id
+ self.flow_frequencies = flow_frequencies
+ self.missed_tokens = missed_tokens or {}
+ self.left_tokens = left_tokens or {}
+
+
+class FlowArcAnalysisTester:
+ """Class to handle flow arc analysis testing logic"""
+
+ def __init__(self, model_path: Path, log_path: Path, model_name: str = None):
+ self.model_path = model_path
+ self.log_path = log_path
+ self.model_name = model_name or model_path.stem
+ self.bpmn_graph = None
+ self.event_log = None
+ self.case_results = {}
+ self.log_ids = EventLogIDs(case="case_id", activity="activity", end_time="end_time")
+
+ def load_model_and_log(self) -> None:
+ self.bpmn_graph = BPMNGraph.from_bpmn_path(self.model_path)
+ self.event_log = read_csv_log(self.log_path, self.log_ids)
+ self.event_log = self.event_log.sort_values(self.log_ids.end_time)
+
+ print("\n\n" + "="*80)
+ print(f"ANALYZING MODEL: {self.model_name}")
+ print("="*80)
+
+ def analyze_cases(self) -> Dict[str, Dict]:
+ self.case_results = {}
+ case_count = len(self.event_log[self.log_ids.case].unique())
+ print(f"\nFound {case_count} cases in the event log")
+
+ for case_id, case_events in self.event_log.groupby(self.log_ids.case):
+ print("\n" + "-"*80)
+ print(f"CASE {case_id} ANALYSIS")
+ print("-"*80)
+
+ flow_arcs_frequency = {}
+ missed_tokens = {}
+ left_tokens = {}
+
+ trace = case_events[self.log_ids.activity].tolist()
+
+ is_correct, fired_tasks, pending, frequency_count = self.bpmn_graph.replay_trace(
+ trace, flow_arcs_frequency
+ )
+
+ trace_str = " → ".join(trace)
+ print(f"Trace: {trace_str}")
+ print(f"Conformance: {'✓ Correct' if is_correct else '✗ Incorrect'}")
+ print(f"Fired Tasks: {[i for i, v in enumerate(fired_tasks) if v]} Length: {len(fired_tasks)}")
+ if pending:
+ print(f"Pending Tokens: {pending}")
+ else:
+ print("Pending Tokens: None")
+
+ # left tokens - tokens that "were not consumed"
+ for flow_id in pending:
+ left_tokens[flow_id] = left_tokens.get(flow_id, 0) + 1
+
+ # missed tokens - missing to be consumed tokens
+ for i, fired in enumerate(fired_tasks):
+ if not fired and i < len(trace):
+ activity = trace[i]
+ if activity in self.bpmn_graph.from_name:
+ task_id = self.bpmn_graph.from_name[activity]
+ for flow_id in self.bpmn_graph.element_info[task_id].incoming_flows:
+ missed_tokens[flow_id] = missed_tokens.get(flow_id, 0) + 1
+
+ self.case_results[str(case_id)] = {
+ 'flow_frequencies': frequency_count,
+ 'missed_tokens': missed_tokens,
+ 'left_tokens': left_tokens
+ }
+
+ self.print_flow_arc_analysis(frequency_count, missed_tokens, left_tokens)
+ return self.case_results
+
+ def print_flow_arc_analysis(
+ self,
+ flow_arcs_frequency: Dict[str, int],
+ missed_tokens: Dict[str, int],
+ left_tokens: Dict[str, int]
+ ) -> None:
+ print("\nFLOW ARC ANALYSIS FOR THIS CASE:")
+ print("-" * 80)
+
+ print(f"{'FLOW ID':<10} {'SOURCE → TARGET':<30} {'FREQUENCY':<10} {'MISSED':<10} {'LEFT':<10}")
+ print("-" * 80)
+
+ def numeric_sort_key(flow_id):
+ if flow_id.startswith("f") and flow_id[1:].isdigit():
+ return int(flow_id[1:])
+ return flow_id
+
+ sorted_flow_ids = sorted(self.bpmn_graph.flow_arcs.keys(), key=numeric_sort_key)
+
+ for flow_id in sorted_flow_ids:
+ source = self.bpmn_graph.element_info[self.bpmn_graph.flow_arcs[flow_id][0]].name
+ target = self.bpmn_graph.element_info[self.bpmn_graph.flow_arcs[flow_id][1]].name
+
+ flow_name = f"{source} → {target}"
+ frequency = flow_arcs_frequency.get(flow_id, 0)
+ missed = missed_tokens.get(flow_id, 0)
+ left = left_tokens.get(flow_id, 0)
+
+ print(f"{flow_id:<10} {flow_name:<30} {frequency:<10} {missed:<10} {left:<10}")
+
+ def assert_case_results(self, expected_results_by_case: List[CaseExpectedResults]) -> None:
+ all_flow_ids = set(self.bpmn_graph.flow_arcs.keys())
+
+ print("\n" + "="*80)
+ print(f"ASSERTION RESULTS FOR MODEL: {self.model_name}")
+ print("="*80)
+
+ for expected in expected_results_by_case:
+ case_id = expected.case_id
+
+ print(f"\nChecking case {case_id}:")
+
+ if case_id not in self.case_results:
+ print(f" ✗ ERROR: Case ID {case_id} not found in event log")
+ assert case_id in self.case_results, f"Case ID {case_id} not found in event log"
+ continue
+
+ result = self.case_results[case_id]
+ all_passed = True
+
+ for flow_id in all_flow_ids:
+ expected_freq = expected.flow_frequencies.get(flow_id, 0)
+ actual_freq = result['flow_frequencies'].get(flow_id, 0)
+ if actual_freq != expected_freq:
+ all_passed = False
+ print(f" ✗ Flow {flow_id} frequency: expected {expected_freq}, got {actual_freq}")
+ assert actual_freq == expected_freq, (
+ f"Case {case_id}, Flow {flow_id} frequency mismatch: "
+ f"expected {expected_freq}, got {actual_freq}"
+ )
+
+ for flow_id in all_flow_ids:
+ expected_missed = expected.missed_tokens.get(flow_id, 0)
+ actual_missed = result['missed_tokens'].get(flow_id, 0)
+ if actual_missed != expected_missed:
+ all_passed = False
+ print(f" ✗ Flow {flow_id} missed tokens: expected {expected_missed}, got {actual_missed}")
+ assert actual_missed == expected_missed, (
+ f"Case {case_id}, Flow {flow_id} missed tokens mismatch: "
+ f"expected {expected_missed}, got {actual_missed}"
+ )
+
+ for flow_id in all_flow_ids:
+ expected_left = expected.left_tokens.get(flow_id, 0)
+ actual_left = result['left_tokens'].get(flow_id, 0)
+ if actual_left != expected_left:
+ all_passed = False
+ print(f" ✗ Flow {flow_id} left tokens: expected {expected_left}, got {actual_left}")
+ assert actual_left == expected_left, (
+ f"Case {case_id}, Flow {flow_id} left tokens mismatch: "
+ f"expected {expected_left}, got {actual_left}"
+ )
+
+ if all_passed:
+ print(f" ✓ All assertions passed")
+
+
+def run_flow_arc_analysis(
+ model_path: Path,
+ log_path: Path,
+ expected_results_by_case: List[CaseExpectedResults],
+ model_name: str = None
+) -> Tuple[bool, Dict[str, Dict]]:
+ tester = FlowArcAnalysisTester(model_path, log_path, model_name)
+ tester.load_model_and_log()
+ case_results = tester.analyze_cases()
+ tester.assert_case_results(expected_results_by_case)
+ return True, case_results
+
+
+TEST_CASES = [
+ {
+ "model_path": assets_dir / "simple_model.bpmn",
+ "log_path": assets_dir / "simple_model_event_log.csv",
+ "model_name": "Simple Sequential Process",
+ "expected_results": [
+ # Case 1: A->B->C
+ CaseExpectedResults(
+ case_id="1",
+ flow_frequencies={
+ "f1": 1,
+ "f2": 1,
+ "f3": 1,
+ "f4": 1,
+ },
+ missed_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ },
+ left_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ }
+ ),
+ # Case 2: Only B
+ CaseExpectedResults(
+ case_id="2",
+ flow_frequencies={
+ "f1": 1,
+ "f2": 0,
+ "f3": 1,
+ "f4": 0,
+ },
+ missed_tokens={
+ "f1": 0,
+ "f2": 1,
+ "f3": 0,
+ "f4": 0,
+ },
+ left_tokens={
+ "f1": 1,
+ "f2": 0,
+ "f3": 1,
+ "f4": 0,
+ }
+ ),
+ ]
+ },
+ {
+ "model_path": assets_dir / "exclusive_gateway_simple.bpmn",
+ "log_path": assets_dir / "exclusive_gateway_event_log.csv",
+ "model_name": "Exclusive Gateway Process",
+ "expected_results": [
+ # Case 1: Only C
+ CaseExpectedResults(
+ case_id="1",
+ flow_frequencies={
+ "f1": 1,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 1,
+ "f7": 1,
+ },
+ missed_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 1,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ },
+ left_tokens={
+ "f1": 1,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ }
+ ),
+ ]
+ },
+ {
+ "model_path": assets_dir / "and_xor_model.bpmn",
+ "log_path": assets_dir / "and_xor_event_log.csv",
+ "model_name": "AND & XOR Gateways Simple Process",
+ "expected_results": [
+ # Case 1: A->D
+ CaseExpectedResults(
+ case_id="1",
+ flow_frequencies={
+ "f1": 1,
+ "f2": 1,
+ "f3": 1,
+ "f4": 1,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 1,
+ "f10": 0,
+ "f11": 0,
+ },
+ missed_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ },
+ left_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 1,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 1,
+ "f10": 0,
+ "f11": 0,
+ }
+ ),
+ ]
+ },
+ {
+ "model_path": assets_dir / "nested_and_gateways.bpmn",
+ "log_path": assets_dir / "nested_and_gateways_event_log.csv",
+ "model_name": "Nested AND Gateways Process",
+ "expected_results": [
+ # Case 1: A->B->C->D->E
+ CaseExpectedResults(
+ case_id="1",
+ flow_frequencies={
+ "f1": 1,
+ "f2": 1,
+ "f3": 1,
+ "f4": 1,
+ "f5": 1,
+ "f6": 1,
+ "f7": 1,
+ "f8": 1,
+ "f9": 1,
+ "f10": 1,
+ "f11": 1,
+ "f12": 1,
+ "f13": 1,
+ "f14": 1,
+ "f15": 1,
+ },
+ missed_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 0,
+ "f15": 0,
+ },
+ left_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 0,
+ "f15": 0,
+ }
+ ),
+ ]
+ },
+ {
+ "model_path": assets_dir / "nested_or_gateways.bpmn",
+ "log_path": assets_dir / "nested_or_gateways_event_log.csv",
+ "model_name": "Nested OR Gateways Process",
+ "expected_results": [
+ # Case 1: A->B->C->F
+ CaseExpectedResults(
+ case_id="1",
+ flow_frequencies={
+ "f1": 1,
+ "f2": 1,
+ "f3": 1,
+ "f4": 1,
+ "f5": 1,
+ "f6": 1,
+ "f7": 1,
+ "f8": 1,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 1,
+ "f15": 1,
+ "f16": 1,
+ },
+ missed_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 0,
+ "f15": 0,
+ "f16": 0,
+ },
+ left_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 0,
+ "f15": 0,
+ "f16": 0,
+ }
+ ),
+ ]
+ },
+ {
+ "model_path": assets_dir / "xor_or_gateways.bpmn",
+ "log_path": assets_dir / "xor_or_gateways_event_log.csv",
+ "model_name": "Nested OR Gateways Process",
+ "expected_results": [
+ # Case 1: A->B
+ CaseExpectedResults(
+ case_id="1",
+ flow_frequencies={
+ "f1": 1,
+ "f2": 1,
+ "f3": 0,
+ "f4": 1,
+ "f5": 1,
+ "f6": 1,
+ "f7": 1,
+ "f8": 1,
+ "f9": 1,
+ "f10": 0,
+ "f11": 1,
+ "f12": 1,
+ "f13": 1,
+ },
+ missed_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ },
+ left_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ }
+ ),
+ ]
+ },
+{
+ "model_path": assets_dir / "deep_or_gateways.bpmn",
+ "log_path": assets_dir / "deep_or_gateways_event_log.csv",
+ "model_name": "Deep Nested OR Gateways Process",
+ "expected_results": [
+ # Case 1: A->B->C->D->E->F
+ CaseExpectedResults(
+ case_id="1",
+ flow_frequencies={
+ "f1": 1,
+ "f2": 1,
+ "f3": 1,
+ "f4": 1,
+ "f5": 1,
+ "f6": 1,
+ "f7": 1,
+ "f8": 1,
+ "f9": 1,
+ "f10": 1,
+ "f11": 1,
+ "f12": 1,
+ "f13": 1,
+ "f14": 1,
+ "f15": 1,
+ "f16": 1,
+ "f17": 1,
+ "f18": 1,
+ "f19": 1,
+ "f20": 1,
+ "f21": 1,
+ "f22": 1,
+ },
+ missed_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 0,
+ "f15": 0,
+ "f16": 0,
+ "f17": 0,
+ "f18": 0,
+ "f19": 0,
+ "f20": 0,
+ "f21": 0,
+ "f22": 0,
+ },
+ left_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 0,
+ "f15": 0,
+ "f16": 0,
+ "f17": 0,
+ "f18": 0,
+ "f19": 0,
+ "f20": 0,
+ "f21": 0,
+ "f22": 0,
+ }
+ ),
+ CaseExpectedResults(
+ case_id="2",
+ flow_frequencies={
+ "f1": 1,
+ "f2": 1,
+ "f3": 1,
+ "f4": 1,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 0,
+ "f15": 0,
+ "f16": 0,
+ "f17": 0,
+ "f18": 0,
+ "f19": 0,
+ "f20": 0,
+ "f21": 1,
+ "f22": 1,
+ },
+ missed_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 0,
+ "f15": 0,
+ "f16": 0,
+ "f17": 0,
+ "f18": 0,
+ "f19": 0,
+ "f20": 0,
+ "f21": 0,
+ "f22": 0,
+ },
+ left_tokens={
+ "f1": 0,
+ "f2": 0,
+ "f3": 0,
+ "f4": 0,
+ "f5": 0,
+ "f6": 0,
+ "f7": 0,
+ "f8": 0,
+ "f9": 0,
+ "f10": 0,
+ "f11": 0,
+ "f12": 0,
+ "f13": 0,
+ "f14": 0,
+ "f15": 0,
+ "f16": 0,
+ "f17": 0,
+ "f18": 0,
+ "f19": 0,
+ "f20": 0,
+ "f21": 0,
+ "f22": 0,
+ }
+ ),
+ ]
+ },
+]
+
+
+@pytest.mark.smoke
+@pytest.mark.parametrize("test_case", TEST_CASES)
+def test_flow_arc_analysis(test_case):
+ success, _ = run_flow_arc_analysis(
+ test_case["model_path"],
+ test_case["log_path"],
+ test_case["expected_results"],
+ test_case.get("model_name")
+ )
+ assert success
+
+
+
+# =========== PREVIOUS TEST SUITES ===============
+@pytest.mark.smoke
+@pytest.mark.parametrize("model_path,log_path", [
+ (assets_dir / "deep_or_gateways.bpmn", assets_dir / "deep_or_gateways_event_log.csv")
+])
+def test_flow_arc_analysis_previous(model_path: Path, log_path: Path):
+
+ selected_case_id = 3
+
+ bpmn_graph = BPMNGraph.from_bpmn_path(model_path)
+ log_ids = EventLogIDs(case="case_id", activity="activity", end_time="end_time")
+ event_log = read_csv_log(log_path, log_ids)
+ event_log = event_log.sort_values(log_ids.end_time)
+
+ # Filter to only include the selected case ID
+ if selected_case_id is not None:
+ event_log = event_log[event_log[log_ids.case] == selected_case_id]
+
+ flow_arcs_frequency = {}
+ missed_tokens = {}
+ left_tokens = {}
+
+ for case_id, case_events in event_log.groupby(log_ids.case):
+ trace = case_events[log_ids.activity].tolist()
+ is_correct, fired_tasks, pending, frequency_count = bpmn_graph.replay_trace(trace, flow_arcs_frequency)
+
+ print(f"\nIsCorrect: {is_correct}, FiredTasks: {fired_tasks}, Pending: {pending}")
+ print(f"BPMN flow arcs: {bpmn_graph.flow_arcs}")
+
+ #left tokens - "were not consumed"
+ for flow_id in pending:
+ left_tokens[flow_id] = left_tokens.get(flow_id, 0) + 1
+
+ #missed tokens - "were not there when needed"
+ for i, fired in enumerate(fired_tasks):
+ if not fired and i < len(trace):
+ activity = trace[i]
+ if activity in bpmn_graph.from_name:
+ task_id = bpmn_graph.from_name[activity]
+ for flow_id in bpmn_graph.element_info[task_id].incoming_flows:
+ missed_tokens[flow_id] = missed_tokens.get(flow_id, 0) + 1
+
+ def numeric_sort_key(flow_id):
+ if flow_id.startswith("f") and flow_id[1:].isdigit():
+ return int(flow_id[1:])
+ return flow_id
+
+ sorted_flow_ids = sorted(bpmn_graph.flow_arcs.keys(), key=numeric_sort_key)
+
+ print(f"\nFlow Arc Analysis: ")
+ for flow_id in sorted_flow_ids:
+ source = bpmn_graph.element_info[bpmn_graph.flow_arcs[flow_id][0]].name
+ target = bpmn_graph.element_info[bpmn_graph.flow_arcs[flow_id][1]].name
+ print(f"\nFlow {flow_id} ({source} -> {target}):")
+ print(f" Frequency: {frequency_count.get(flow_id, 0)}")
+ print(f" Missed tokens: {missed_tokens.get(flow_id, 0)}")
+ print(f" Left tokens: {left_tokens.get(flow_id, 0)}")
+
+ # Assertions
+ assert bpmn_graph is not None
+
+
+ for flow_id in bpmn_graph.flow_arcs:
+ # assert flow_id in flow_arcs_frequency, f"Flow arc {flow_id} not found in frequency dictionary"
+ # assert isinstance(flow_arcs_frequency[flow_id], int), f"Invalid frequency type for flow {flow_id}"
+
+ if flow_id in missed_tokens:
+ assert isinstance(missed_tokens[flow_id], int), f"Invalid missed tokens type for flow {flow_id}"
+
+ if flow_id in left_tokens:
+ assert isinstance(left_tokens[flow_id], int), f"Invalid left tokens type for flow {flow_id}"
+
+
+
@pytest.mark.smoke
@pytest.mark.parametrize("model_path", [(assets_dir / "PurchasingExample.bpmn")])
def test_from_bpmn_path(model_path: Path):
graph = BPMNGraph.from_bpmn_path(model_path)
assert graph is not None
assert graph.starting_event is not None
- assert len(graph.flow_arcs) > 0
+ assert len(graph.flow_arcs) > 0
\ No newline at end of file