From 3fec83a142a77644bf0da8b7b831a35fb3a5fca0 Mon Sep 17 00:00:00 2001 From: shyngys Date: Wed, 19 Mar 2025 13:04:28 +0200 Subject: [PATCH] or gateways --- src/pix_framework/io/bpm_graph.py | 210 +++-- .../assets/and_xor_event_log.csv | 3 + tests/pix_framework/assets/and_xor_model.bpmn | 154 ++++ .../assets/deep_or_gateways.bpmn | 277 +++++++ .../assets/deep_or_gateways_event_log.csv | 14 + .../assets/exclusive_gateway_event_log.csv | 2 + .../assets/exclusive_gateway_simple.bpmn | 100 +++ .../assets/nested_and_gateways.bpmn | 196 +++++ .../assets/nested_and_gateways_event_log.csv | 6 + .../assets/nested_or_gateways.bpmn | 208 +++++ .../assets/nested_or_gateways_event_log.csv | 5 + tests/pix_framework/assets/simple_model.bpmn | 65 ++ .../assets/simple_model_event_log.csv | 5 + .../pix_framework/assets/xor_or_gateways.bpmn | 167 ++++ .../assets/xor_or_gateways_event_log.csv | 4 + .../batch_processing/test_batch_discovery.py | 2 +- .../test_prioritization_discovery.py | 2 +- tests/pix_framework/io/test_bpm_graph.py | 763 +++++++++++++++++- 18 files changed, 2133 insertions(+), 50 deletions(-) create mode 100644 tests/pix_framework/assets/and_xor_event_log.csv create mode 100644 tests/pix_framework/assets/and_xor_model.bpmn create mode 100644 tests/pix_framework/assets/deep_or_gateways.bpmn create mode 100644 tests/pix_framework/assets/deep_or_gateways_event_log.csv create mode 100644 tests/pix_framework/assets/exclusive_gateway_event_log.csv create mode 100644 tests/pix_framework/assets/exclusive_gateway_simple.bpmn create mode 100644 tests/pix_framework/assets/nested_and_gateways.bpmn create mode 100644 tests/pix_framework/assets/nested_and_gateways_event_log.csv create mode 100644 tests/pix_framework/assets/nested_or_gateways.bpmn create mode 100644 tests/pix_framework/assets/nested_or_gateways_event_log.csv create mode 100644 tests/pix_framework/assets/simple_model.bpmn create mode 100644 tests/pix_framework/assets/simple_model_event_log.csv create mode 100644 tests/pix_framework/assets/xor_or_gateways.bpmn create mode 100644 tests/pix_framework/assets/xor_or_gateways_event_log.csv 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